refactor(ui-voip): own the media session lifecycle from an effect
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.
makeInstancetakes agetWebRTCConfig()getter (auseStableCallback, 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. getInstanceis idempotent peruserId. Creating the session is a side effect; without a guard, a re-render (StrictMode double render, a discarded render) calledendSession()on the live session — which ignores every known call — and built a new one.getOldSessionIdignores ids this tab created. The session id is written tosessionStorageon 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, sincelastSessionIdis in-memory only.- Signal transport is set before construction. The
MediaSignalingSessionconstructor already sends the register signal, sosendSignalFnhas 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-signalis subscribed with the instance in the dependency list and delivers toinstance.processSignal, which removes the store-leveluserIdcomparison that existed only because the store was a singleton. - Dead code removed. With the instance in React state nothing subscribed to the store's
changeevent, sochange()/onChange()/ thechangeentry in the event map are gone, along withsetSendSignalFn(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
- Log in and confirm a single media session is registered (one register signal, one
sessionIdinsessionStorage). - Make a call, then change
VoIP_TeamCollab_Ice_Gathering_Timeoutin admin: the ongoing call is unaffected; the next call uses the new value. - Reload the page during a call: the session is resumed with the previous
sessionId. - 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.