refactor(ui-voip): own the media session lifecycle from an effect

Draft
GG
24d ago
+62-100

Proposed changes (including videos or screenshots)

useMediaSessionInstance exposed the session through useSyncExternalStore, so MediaSessionStore could only build an instance after three separate effects had fed it (setSendSignalFn, setWebRTCProcessorFactory, then instance creation on the next snapshot). Every change to VoIP_TeamCollab_Ice_Gathering_Timeout or to the ICE server list replaced the processor factory, and instance creation was driven by an impure getSnapshot.

The hook now creates the session in an effect and keeps it in state. Concretely:

  • ICE config is read lazily. makeInstance takes a getWebRTCConfig() getter (a useStableCallback, stable identity + latest values) which the webrtc processor factory calls on every processor creation. New calls pick up new settings; the live session is left alone.
  • getInstance is idempotent per userId. Creating the session is a side effect; without a guard, a re-render (StrictMode double render, a discarded render) called endSession() on the live session — which ignores every known call — and built a new one.
  • getOldSessionId ignores ids this tab created. The session id is written to sessionStorage on creation, so a recreated instance read back the id of the instance it had just replaced and asked the server to resume it. Session recovery after a real page refresh still works, since lastSessionId is in-memory only.
  • Signal transport is set before construction. The MediaSignalingSession constructor already sends the register signal, so sendSignalFn has to be in place first — that ordering is now explicit instead of depending on effect order.
  • Signal routing is per instance. notify-user/<uid>/media-signal is subscribed with the instance in the dependency list and delivers to instance.processSignal, which removes the store-level userId comparison that existed only because the store was a singleton.
  • Dead code removed. With the instance in React state nothing subscribed to the store's change event, so change() / onChange() / the change entry in the event map are gone, along with setSendSignalFn (its returned unsubscribe had no callers) and three unused imports.

No behavior change intended other than the session no longer being recreated.

Issue(s)

Steps to test or reproduce

  1. Log in and confirm a single media session is registered (one register signal, one sessionId in sessionStorage).
  2. Make a call, then change VoIP_TeamCollab_Ice_Gathering_Timeout in admin: the ongoing call is unaffected; the next call uses the new value.
  3. Reload the page during a call: the session is resumed with the previous sessionId.
  4. Log out and log in as another user: the previous session is ended and a new one is registered.

Further comments

The remaining rough edge is that MediaSignalingSession does its side effects in the constructor (sends register, starts the state report interval). Splitting that into start()/stop() in @rocket.chat/media-signaling would make construction pure and let the effect own the lifecycle outright, removing the need for the idempotency guard here. Left out of this PR to keep it scoped to the hook.