music on atproto
plyr.fm
1# queue design
2
3## overview
4
5The queue is a cross-device, server-authoritative data model with optimistic local updates. Every device performs queue mutations locally, pushes a full snapshot to the API, and receives hydrated track metadata back. Servers keep an in-memory cache (per process) in sync via Postgres LISTEN/NOTIFY so horizontally scaled instances observe the latest queue state without adding Redis or similar infra.
6
7## server implementation
8
9- `queue_state` table (`did`, `state`, `revision`, `updated_at`). `state` is JSONB containing `track_ids`, `current_index`, `current_track_id`, `shuffle`, `repeat_mode`, `original_order_ids`.
10- `QueueService` keeps a TTL LRU cache (`maxsize 100`, `ttl 5m`). Cache entries include both the raw state and the hydrated track list.
11- On startup the service opens an asyncpg connection, registers a `queue_changes` listener, and reconnects on failure. Notifications simply invalidate the cache entry; consumers fetch on demand.
12- `GET /queue/` returns `{ state, revision, tracks }`. `tracks` is hydrated server-side by joining against `tracks`+`artists`. Duplicate queue entries are preserved—hydration walks the `track_ids` array by index so the same `file_id` can appear multiple times. Response includes an ETag (`"revision"`).
13- `PUT /queue/` expects an optional `If-Match: "revision"`. Mismatched revisions return 409. Successful writes increment the revision, emit LISTEN/NOTIFY, and rehydrate so the response mirrors GET semantics.
14- Hydration preserves order even when duplicates exist by pairing each `track_id` position with the track returned by the DB. We never de-duplicate on the server.
15
16## client implementation (Svelte 5)
17
18- Global queue store (`frontend/src/lib/queue.svelte.ts`) uses runes-backed `$state` fields for `tracks`, `currentIndex`, `shuffle`, etc. Methods mutate these states synchronously so the UI remains responsive.
19- A 250 ms debounce batches PUTs. We skip background GETs while a PUT is pending/in-flight to avoid stomping optimistic state.
20- Conflict handling: on 409 the client performs a forced `fetchQueue(true)` which ignores local ETag and applies the server snapshot if the revision is newer. Older revisions received out-of-order are ignored.
21- Before unload / visibility change flushes pending work to reduce data loss when navigating away.
22- Helper getters (`getCurrentTrack`, `getUpNextEntries`) supplement state but UI components bind directly to `$state` so Svelte reactivity tracks mutations correctly.
23- Duplicates: adding the same track repeatedly simply appends another copy. Removal is disabled for the currently playing entry (conceptually index 0); the queue sidebar only allows removing future items.
24
25## UI behavior
26
27- sidebar shows "now playing" card with prev/next buttons
28- shuffle control in player footer (always visible)
29- "up next" lists tracks beyond `currentIndex`
30- drag-and-drop reordering supported for upcoming tracks
31- removing a track updates local state and syncs to server
32- `queue.playNow(track)` inserts track at position 0, preserves existing up-next order
33- duplicate tracks allowed - same track can appear multiple times in queue
34- auto-play preference controls automatic advancement to next track
35- persisted via `/preferences/` API and localStorage
36- queue toggle button opens/closes sidebar
37- responsive positioning for mobile viewports
38- cannot remove currently playing track (index 0)
39
40## shuffle
41
42- shuffle is an action, not a toggle mode
43- each shuffle operation randomly reorders upcoming tracks (after current track)
44- preserves everything before and including the current track
45- uses fisher-yates algorithm with retry logic to ensure different permutation
46- original order preserved in `original_order_ids` for server persistence
47
48## cross-tab synchronization
49
50- uses BroadcastChannel API for same-browser tab sync
51- each tab has unique `tabId` stored in sessionStorage
52- queue updates broadcast to other tabs via `queue-updated` message
53- tabs ignore their own broadcasts and duplicate revisions
54- receiving tabs fetch latest state from server
55- `lastUpdateWasLocal` flag tracks update origin
56
57## future work
58
59- realtime push via SSE/WebSocket for instant cross-device updates
60- UI affordances for "queue updated on another device" notifications
61- repeat modes (currently not implemented)
62- clear up-next functionality exposed in UI