Support multi-instance unbind for pusher callbacks#85356
Support multi-instance unbind for pusher callbacks#85356rlinoz merged 6 commits intoExpensify:mainfrom
Conversation
…bind
The Pusher module now tracks individual callbacks per channel+event using
a Map<Channel, Map<EventName, Set<Callback>>> structure. subscribe()
returns a PusherSubscription (Promise & { unsubscribe }) that removes
only the specific callback when called, leaving other subscribers intact.
This enables multiple components to independently subscribe to and
unsubscribe from the same Pusher channel+event without interfering with
each other. The existing unsubscribe(channel, event) API is preserved
for bulk removal of all callbacks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
Extend PusherSubscribeTest with 6 new test cases covering the per-callback subscription behavior: - subscribe() returns a PusherSubscription with .unsubscribe() - Two subscribers to the same channel+event both receive events - Unsubscribing one callback does not affect the other - Channel stays subscribed until the last callback unsubscribes - Unsubscribe before subscription completes (disposed flag) - Multiple events on same channel with independent cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 388d9fe962
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The web Pusher disconnect() was missing eventsBoundToChannels.clear(), unlike the native implementation. This caused callback closures to accumulate across disconnect/reconnect cycles (logout/login, upgrade teardown), leaking memory over time. Add the missing clear() call and a test verifying old callbacks do not fire after a disconnect/re-init cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@MelvinBot Could you review this PR? |
|
@codex review |
🤖 PR ReviewOverall this is a solid, well-structured change. The approach of returning a What's good
Minor concerns
VerdictThe code is clean, tests are thorough, and CI passes (only failing check is |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 002044c62c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@adhorodyski Could you check the above comments? The rest looks fine to me |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-03-17.at.16.29.51.movAndroid: mWeb ChromeScreen.Recording.2026-03-17.at.16.27.31.moviOS: HybridAppScreen.Recording.2026-03-17.at.16.24.40.moviOS: mWeb SafariScreen.Recording.2026-03-17.at.16.26.04.movMacOS: Chrome / SafariScreen.Recording.2026-03-17.at.16.21.08.mov |
|
@rlinoz Could you generate an ad-hoc build and ask QA to test a few flows before proceeding? Since this change affects the Pusher handler, I’d prefer additional testing before merging. |
JmillsExpensify
left a comment
There was a problem hiding this comment.
No product review required.
If handle.unsubscribe() is called after socket.subscribe() starts but before onSubscriptionSucceeded fires, wrappedCb is still undefined so the unsubscribe handler returns early without cleaning up the channel. This leaves a live channel with no listeners that accumulates across fast mount/unmount cycles. Fix: in onSubscriptionSucceeded, when disposed is true, check if the channel has any remaining callbacks in eventsBoundToChannels. If not, unsubscribe the channel entirely. Applied to both web and native. Also null wrappedCb/resolvedChannel after removal in unsubscribe handlers for defensive cleanup (cosmetic, addresses review feedback). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@DylanDylann back to you |
Two fixes: 1. Guard `channel` with `else if (channel)` in onSubscriptionSucceeded cleanup block — `channel` is typed `Channel | undefined` and eventsBoundToChannels is keyed by `Channel`. 2. Import MockedPusher directly from the __mocks__ file instead of the real SDK module. The jest __mocks__ override only affects runtime, not TypeScript's type resolution, so importing from the real package gives the wrong types. Direct import gives proper typing for .channels, .subscribe(), .trigger() — eliminates all `as` casts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
🚧 @rlinoz has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
|
Let's wait for QA to finish testing before we merge. |
|
🚧 @rlinoz has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/rlinoz in version: 9.3.40-0 🚀
|
|
🚀 Deployed to staging by https://github.com/rlinoz in version: 9.3.40-0 🚀
|
Explanation of Change
The Pusher module previously tracked subscriptions by channel+event key only, meaning multiple components subscribing to the same channel and event would share a single callback slot. When any one component unsubscribed, it would remove the binding for all subscribers — breaking other active listeners.
This change introduces per-callback tracking via a
Mapkeyed by a unique handle ID.subscribe()now returns aPusherSubscriptionobject (aPromiseextended with anunsubscribe()method) that scopes the unbind operation to only the specific callback that was registered. Multiple components can now independently subscribe and unsubscribe from the same channel+event without interfering with each other.This is PR 2 of a series decomposing ReportScreen into smaller, independently mountable units — a prerequisite for those components to safely manage their own Pusher subscriptions. Part of #84971.
Fixed Issues
$ #84895
PROPOSAL:
Tests
USER_IS_TYPINGper report)USER_IS_LEAVING_ROOM)- Join a group chat or room
- Have another user leave the room → verify the membership update appears
- Navigate away and back → verify no stale subscriptions
onResubscribecallback)- Open a chat, toggle airplane mode on/off (or kill network briefly)
- Verify Pusher reconnects and real-time events resume (typing, new messages via push)
PusherUtils.tsto private user channel)- Send a message from another device/user → verify it appears in real-time without refresh
- Do this across multiple reports to verify channel multiplexing works
Offline tests
N/A
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari