Skip to content

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

Draft
ggazzo wants to merge 3 commits into
developfrom
refactor/media-session-instance-lifecycle
Draft

refactor(ui-voip): own the media session lifecycle from an effect#41691
ggazzo wants to merge 3 commits into
developfrom
refactor/media-session-instance-lifecycle

Conversation

@ggazzo

@ggazzo ggazzo commented Aug 4, 2026

Copy link
Copy Markdown
Member

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.

Review in cubic

The session was exposed through useSyncExternalStore, which meant the
store had to be fed by three separate effects (send signal fn, webrtc
processor factory, instance creation) before it could hand out an
instance, and any ICE setting change replaced the factory.

Now the hook creates the session in an effect and keeps it in state:

- iceServers/iceGatheringTimeout are read through a stable getter on
  every processor creation, so setting changes reach new calls without
  recreating the session;
- getInstance is idempotent per userId, so a re-render (StrictMode,
  discarded render) no longer ends a live session;
- getOldSessionId ignores an id this tab created itself, so a recreated
  instance doesn't try to resume the session it just replaced;
- the store no longer emits 'change': with the instance in React state
  there were no subscribers left.
@dionisio-bot

dionisio-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ba7aec4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b834437-d2bc-4acf-b000-eb1ed6895909

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.63%. Comparing base (9bf5285) to head (ba7aec4).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #41691      +/-   ##
===========================================
+ Coverage    68.61%   68.63%   +0.02%     
===========================================
  Files         4162     4162              
  Lines       158824   158824              
  Branches     28174    28156      -18     
===========================================
+ Hits        108976   109012      +36     
+ Misses       44665    44631      -34     
+ Partials      5183     5181       -2     
Flag Coverage Δ
e2e 58.89% <ø> (+0.02%) ⬆️
e2e-api 45.72% <ø> (-0.01%) ⬇️
unit 70.55% <ø> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ggazzo ggazzo added this to the 8.8.0 milestone Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant