fix(server-storage): conditional user-data writes, scoped token, deletes

- User-state entries carry a content tag; PUT/DELETE accept If-Match or
  If-None-Match: * and answer 412 with the current value and tag. The PWA
  sends each edit on the tag it read and replays it (before → after) on
  the server's value after a conflict, so another device's change to the
  same playlist's favorites or positions is kept instead of overwritten.
- The access token goes only to the backend URL (same origin and path),
  never to another host or a sibling path.
- Deleting a playlist (or all) first drops its queued user-data edits and
  waits for writes in flight, so none recreates deleted data.
- The migration copy writes on the tags it read.
- The write queue moves out of ServerUserDataStorage into
  UserDataWriteQueue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 08:57:48 +02:00
1 parent f49ad59549
commit 340b1c3efa
18 files changed
+1157 -290

No files matched your search

@@ -58,10 +58,30 @@ export interface ServerStorageStatusResponse {
export interface ServerUserStateResponse {
readonly entries: Record<ServerUserStateNamespace, Record<string, unknown>>;
/**
* Version tag of every entry. A write sends the tag it is based on
* (`If-Match: "<tag>"`, or `If-None-Match: *` for an entry the client
* saw absent); a stale tag gets `412` with the current entry.
*/
readonly tags?: Record<ServerUserStateNamespace, Record<string, string>>;
}
/** Answer to a user-state write: the entry's new tag, `null` once deleted. */
export interface ServerUserStateWriteResponse {
readonly tag: string | null;
}
/** The entry as the server has it, sent with a `412` conflict. */
export interface ServerUserStateCurrent {
/** Absent when the entry does not exist. */
readonly value?: unknown;
readonly tag: string | null;
}
export interface ServerStorageErrorBody {
readonly message: string;
readonly status: number;
readonly code?: string;
/** With `code: 'conflict'` (412): the user-state entry as it is now. */
readonly current?: ServerUserStateCurrent;
}