* fix(watchtogether): harden realtime sync, room lifecycle, and lobby UX
Remediates all findings from a deep review of the Watch Together feature.
Server:
- Serialize every websocket write (pong/error replies bypassed the write
mutex, racing broadcasts on the same gorilla conn)
- Send room_closed with a reason on terminal connect failures so clients
stop reconnecting to dead rooms
- Persist room state outside the service-wide mutex via a shared
generation-CAS helper; drop ~450 lines of dead duplicated methods
- Measure transport latency from server-side ping/pong RTT (was one-way
client-clock delta, poisoned by clock skew) and clamp the lead time
- Re-evaluate readiness when a waiting participant disconnects and add a
30s waiting deadline that skips stragglers (activates ignoreWait)
- Guard the host-disconnect close timer against reconnect races
- Clamp buffering-report anchor moves; clear stale member sessions on
selection change
- Janitor: evict empty live rooms and close rooms idle >24h
- Snapshot gains an additive members list with profile display names
Web:
- Surface terminal room errors (REST 404/410/403 and WS error codes) as
closedReason instead of reconnecting forever on "Connecting..."
- Memoize the playback-sync hook and narrow VideoPlayer's video-listener
effect deps to stop re-subscribing 13 listeners on every render
- Preserve invite-link destination through login/profile guards
- Lobby: terminal ended/missing-token states with CTAs, End-room confirm
dialog, toast feedback via shared action helpers (dedup with player),
participant list with guest Leave, mobile-visible connection status,
document title, a11y labels/focus reveal, unified status dot component
- Delete dead useWatchTogetherRoom hook (345 lines, zero importers)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(watchtogether): guard indexed access in join-page keyboard nav for noUncheckedIndexedAccess
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(watchtogether): reconcile CAS conflicts without regressing newer state; roll back unpersisted waiting-resume
Addresses Codex review on PR #273:
- persistRoomChangeLocked now undoes the failed writer's optimistic
generation increment and only adopts the refreshed database row when it
is at least as new as the local copy, so a stale conflict refresh can
no longer overwrite a concurrent writer's newer in-memory state (and a
failed write can no longer leave a phantom generation)
- maybeResumeFromWaitingLocked restores the waiting state and re-arms the
deadline when the resume transition fails to persist, instead of
broadcasting a resume the database never recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>