Commit graph

2 commits

Author SHA1 Message Date
5d535d18d8 Switch to Socket.IO; fix two real memory leaks
All checks were successful
Publier l'image Docker / publish (push) Successful in 10s
Requested: Socket.IO instead of raw WebSockets, and explicit attention
to memory leaks. Auditing for the second turned up two real, concrete
leaks, independent of the transport library:

1. Server-side: a raw `ws` server has no idea a client is gone unless
   it gets a proper close frame - a sleeping laptop, a network drop, a
   killed tab never fire 'close', so the PartyMember's socket
   reference (and everything it closes over) sat in memory forever,
   and the party was never cleaned up by anything else either
   (removeIdleParties only looks at lastActivityUtc). Socket.IO's
   built-in heartbeat (pingInterval/pingTimeout) detects and closes
   these out on its own - a real, not just cosmetic, reason to prefer
   it here.

2. Client-side: router.js only ever did currentRoot.innerHTML = ''
   between routes, destroying DOM but never telling the outgoing
   view's JS to stop. Leaving the player any way other than its own
   "Quitter" button (a sidebar link, browser back) left the 3s
   heartbeat setInterval, the realtime connection, and the hls.js
   instance all running against a detached <video> indefinitely, for
   as long as the tab stayed open.

Fixes:
- server/src/socketHandler.js replaces wsHandler.js: same protocol
  (see its own top comment), over Socket.IO. Rooms (socket.join/
  io.to) replace manually iterating party.members to broadcast.
  Error event renamed to "errorMessage" - Socket.IO reserves "error".
- server/src/index.js: socket.io server instead of a raw
  WebSocketServer, same-origin (no CORS config needed).
- client/js/party.js: socket.io-client (window.io, loaded via
  <script> in index.html) instead of hand-rolled WebSocket + JSON
  framing - also gets automatic reconnection for free.
- router.js: route() handlers may now return a cleanup function;
  render() calls whatever the previous view returned before switching
  to the next route, regardless of how navigation happened.
- player.js returns a cleanup that clears the heartbeat, destroys
  hls, disconnects the socket, and removes its document-level click
  listener (now named, not inline-anonymous, specifically so it CAN
  be removed - an anonymous one added fresh per visit would itself
  have been a slow leak even with everything else fixed). The
  "Quitter" button is now just navigate('/home') - cleanup runs the
  same way regardless of how you leave.

Verified for real (see session history): a real Socket.IO client
connected to a really-running local server end to end. Then, with
document.addEventListener/removeEventListener and setInterval/
clearInterval instrumented, drove the router through repeated "join a
party, then navigate away without clicking Quitter" cycles - adds and
removes matched exactly every time (3/3 listeners over three cycles,
2/2 intervals over two), nothing left pending.

docs/architecture.md has the full writeup under "Realtime transport:
Socket.IO, and two real memory leaks".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:07:51 +02:00
6ff9aa29e2 Pivot to a standalone app - Emby plugin hit a hard wall
No plugin API exists for adding a button to Emby's player or item
detail page (confirmed by an Emby developer on their own forum, and
by research since). The only way to reach that would be patching
Emby's shipped web client files directly - outside the plugin,
breaks on every image update, needs manual reapplication. Not worth
the ongoing cost, per discussion.

New direction: a standalone app with its own player. Reads Emby's
catalog/streams directly from the browser (no plugin, no menu
integration fights); a small Node backend only coordinates party
membership and relays playback state over WebSocket - it never
touches a media byte. Auth is Emby-only, as chosen: the browser signs
in against Emby directly and this backend validates whatever token it
gets handed by asking Emby itself (GET /Users/{userId}), never seeing
a password.

Control model changed too: now that we own the player, host controls
playback and everyone else follows (event-driven play/pause/seek +
a 3s heartbeat, drift-corrected on the follower side) - the old
plugin's "everyone keeps their own controls, shared reference
position" was specifically a concession to nudging pre-existing Emby
app sessions we didn't control, which no longer applies.

- server/: Express + ws backend (party lifecycle, WebSocket protocol,
  Emby token validation)
- client/: plain-JS/ES-modules frontend (login, search, item detail,
  party player) - no build step
- Smoke-tested locally end to end against a stubbed Emby URL: server
  starts, routing/auth-gating works, error handling degrades
  gracefully (network failure, invalid token) - see the session
  transcript for the actual browser test run
- docs/architecture.md rewritten around the new design, including why
  the plugin was abandoned and a fresh "assumptions to verify" list
  (CORS between this app's origin and Emby's is the biggest unknown)
- src/WatchParty/ (the Emby plugin) kept as-is for reference, no
  longer the active direction

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:48:55 +02:00