diff --git a/docs/architecture/pwa-self-hosted.md b/docs/architecture/pwa-self-hosted.md index 69cef5714..4ba2fddd4 100644 --- a/docs/architecture/pwa-self-hosted.md +++ b/docs/architecture/pwa-self-hosted.md @@ -88,7 +88,10 @@ Server storage adds versioned routes under `/v1`, which the PWA uses in replaced - `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 + positions, one JSON value per namespace and playlist; `GET` adds a + content tag per entry (`tags`), and a write with `If-Match: ""` or + `If-None-Match: *` answers `412` with the current value and tag when the + entry changed since - `GET|PUT /v1/app-state/:key` Server storage lives in `apps/web-backend/src/app/storage/`. With @@ -208,9 +211,12 @@ settings and guards read them in every build. (`iptvnator-server:cache-owner`); another one behind the same address starts clean. Also, re-reads run one at a time and keep entries edited while they were in - flight or not yet saved. A re-read whose result does not fit the cache - keeps the old cache and baseline together. A response body cut off after - its headers counts as offline. Edits made before the first successful read + flight or not yet saved. A response body cut off after its headers counts + as offline. Each write is sent on the tag of the entry it was made + against (`UserDataWriteQueue`); after a `412` the edit (before → after) is + replayed on the server's current value, so another device's change to the + same playlist's entry is kept. Deleting a playlist first drops its queued + edits and waits for writes still travelling. Edits made before the first successful read are held as before → after and applied to the server's value once it arrives (`applyEdit`): a new device or a stale cache adds its favorite instead of replacing the server's list. They are saved in the tab too diff --git a/docs/superpowers/specs/2026-10-08-server-storage-design.md b/docs/superpowers/specs/2026-10-08-server-storage-design.md index 7cdbd9512..6b500c3a6 100644 --- a/docs/superpowers/specs/2026-10-08-server-storage-design.md +++ b/docs/superpowers/specs/2026-10-08-server-storage-design.md @@ -132,8 +132,8 @@ All JSON. Errors: `{ message, status, code? }`. | `DELETE /v1/playlists/:id` | Delete the row and its user-state entries. | | `DELETE /v1/playlists` | Delete all playlists and all user state. | | `GET/PUT /v1/app-state/:key` | `{ value }`. | -| `GET /v1/user-state` | `{ entries: { [namespace]: { [playlistId]: value } } }`. | -| `PUT/DELETE /v1/user-state/:namespace/:playlistId` | One entry. Namespaces are an allow-list. | +| `GET /v1/user-state` | `{ entries: { [namespace]: { [playlistId]: value } }, tags }`, a content tag per entry. | +| `PUT/DELETE /v1/user-state/:namespace/:playlistId` | One entry. Namespaces are an allow-list. `If-Match: ""` or `If-None-Match: *` makes it conditional: `412` with the current value and tag. | ETags are `"-"` from an in-memory per-playlist revision counter; the server is the only writer of its database. @@ -209,9 +209,10 @@ https://claude.ai/artifact/3WheJAgeNL5ag6kmtPrJcD ## 11. Risks -- **Concurrent edits on two devices**: last write wins per document or per - Xtream map entry. Every playlist mutation re-reads first, so the window is - one round trip. +- **Concurrent edits on two devices**: last write wins per document. + 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 documents, as IndexedDB does today. - **Open instances**: storing credentials raises the cost of an unprotected