fix(server-storage): conditional playlist metadata writes, retried on a fresh read

A favorite or rename (keepPayload PUT) is sent with If-Match on the
version it was read at. The server answers 412 when another device wrote
since; the store throws PlaylistWriteConflict and PlaylistsService's
per-playlist write queue runs the mutation again, which reads the
playlist afresh, so two devices' metadata edits both survive. A retry on
the same unread copy writes unconditionally, as before; refresh and
import writes stay last write wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 21:17:18 +02:00
1 parent 2d6f28f8c8
commit 7afb9758f4
9 files changed
+159 -12

No files matched your search

+12 -7
View File
@@ -85,7 +85,8 @@ Server storage adds versioned routes under `/v1`, which the PWA uses in
`PUT ...?keepPayload=1` keeps the stored `playlist`, `items` and
`header` fields (`409 payload_missing` without a stored row); every `PUT`
answers with the new `ETag` and `X-Replaced-ETag`, the version it
replaced
replaced, and one with `If-Match` answers `412 conflict` when another
write replaced that version
- `GET /v1/user-state`, `PUT|DELETE /v1/user-state/:namespace/:playlistId`:
Xtream favorites, recently viewed, collection snapshots and playback
positions, one JSON value per namespace and playlist; `GET` adds a
@@ -193,12 +194,16 @@ settings and guards read them in every build.
- **Playlists.** `ServerStorageSession.start()` (an app initializer) plugs
`ServerPlaylistDocumentStore` into `PlaylistStorageBackend` (kinds
`electron`, `server`, `indexeddb`), so `PlaylistsService` is unchanged. The
store caches full documents by ETag and uploads a document whose payload it
served unchanged with `keepPayload=1`, so a favorite or a rename does not
resend a large M3U. It keeps its copy after such a write only when
`X-Replaced-ETag` names the version its payload came from; otherwise
another device wrote in between and the next read fetches the document.
`electron`, `server`, `indexeddb`). The store caches full documents by
ETag and uploads a document whose payload it served unchanged with
`keepPayload=1`, so a favorite or a rename does not resend a large M3U.
Such a write carries `If-Match` with the version it was read at: when
another device wrote in between, the server answers `412`, the store
throws `PlaylistWriteConflict`, and `PlaylistsService`'s per-playlist
write queue runs the mutation again on a fresh read (a retry on the same,
unread copy writes unconditionally, as before). After a write the store
keeps its copy only when `X-Replaced-ETag` names the version its payload
came from.
- **Xtream user data.** `USER_DATA_STORAGE` becomes `ServerUserDataStorage`:
the four synced namespaces are cached under the `iptvnator-server:` key
prefix (the legacy unprefixed keys are never written in server mode) and
@@ -209,8 +209,11 @@ https://claude.ai/artifact/3WheJAgeNL5ag6kmtPrJcD
## 11. Risks
- **Concurrent edits on two devices**: last write wins per document.
Every playlist mutation re-reads first, so the window is one round trip.
- **Concurrent edits on two devices**: a metadata edit (`keepPayload`) is
sent with `If-Match` on the version it was read at; on a `412` the
per-playlist write queue runs the mutation again on a fresh read. Other
document writes (refresh, import) stay last write wins; every playlist
mutation re-reads first, so the window is one round trip.
Xtream map entries are written on the tag they were read at and replayed
on the server's value after a `412`, so both devices' edits are kept.
- **Large payloads**: M3U global favorites and backup export read all full