feat: jams — shared listening rooms (#949)
* feat: add jams — shared listening rooms via queue bridge pattern
Jams let users listen together in real-time. The queue becomes the shared
state — a jam is "your queue, but shared." Any participant can change
playback, add/remove tracks, or seek.
Backend: Redis Streams for real-time state broadcast, WebSocket for
bidirectional sync, Postgres for jam/participant persistence. All
commands go through a central handler with revision-based ordering.
Frontend: Bridge pattern — queue is the single gate for all playback
mutations. When a jam is active, queue methods route through a JamBridge
that sends WebSocket commands instead of local mutations. No scattered
`if (jam.active)` conditionals in UI components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: jam join uses goto() instead of full reload, reconnect on startup
- join page: window.location.href → goto('/') to preserve runtime state
- layout: fetchActive() on startup reconnects to active jams after refresh
- layout: $effect auto-opens queue panel when jam activates
- docs: comprehensive rewrite of jams.md with current implementation state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 5 review items for jams feature
1. bridge read-path: jam state now syncs tracks/index into queue so
hasNext/hasPrevious/handleTrackEnded work correctly for joiners
2. WS track metadata: include hydrated tracks in Redis stream events
when tracks_changed, so clients can update their track list
3. concurrent command race: SELECT FOR UPDATE serializes commands
4. WS membership check: verify participant before ws.accept()
5. keyboard shortcuts: space/seek/previous route through queue methods
so jam bridge intercepts them
adds 2 regression tests (non-participant rejection, sequential revisions)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: close remaining jam bridge bypasses and stale WS connections
1. route player.togglePlayPause() through queue in track/album/playlist
pages so the jam bridge intercepts play/pause during active jams
2. route Media Session seek handlers through queue.seek() instead of
directly mutating audioElement.currentTime
3. always include tracks array in WS events when tracks_changed (even
when empty) so clients can clear their track list
4. track WS connections by DID and close stale sockets when a user
connects to a new jam (prevents ghost listeners after auto-leave)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: block unbridged queue mutations during jams, add DID socket test
- Guard setQueue/toggleShuffle/moveTrack/clearUpNext in queue class
when jamBridge is active (no backend commands exist for these)
- Block playQueue() during jams with user-facing toast
- Hide shuffle/clear/drag-reorder controls in jam-mode Queue.svelte
- Add unit test for DID socket replacement behavior (code 4010)
- Update jams.md: resolved unbridged methods, noted product semantics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle terminal WS close codes, implement participant event handler
- Stop reconnect loop on codes 4003 (not participant) and 4010 (replaced);
reset local jam state and restore personal queue instead
- Implement handleParticipantMessage: fetch fresh participant list with
metadata on join/leave events so avatars update in real time
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: report flag margin position, queue stuck open during jam
- Header .margin-right: use right:0 instead of right:var(--queue-width)
since the header is already constrained by parent margin-right
- Queue auto-open effect: untrack showQueue so it only fires on
jam.active transition, not on every queue toggle (was a reactive loop)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: gate fetchActive behind jams flag to skip wasted 403 for non-flagged users
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
authored by
zzstoatzz.io