From 6dd8818ee6eeb4f60d3f167d06528b74f6cc925d Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 29 May 2026 17:49:10 -0300 Subject: [PATCH] fix: re-subscribe rooms stream after forced socket reopen A long background marks the DDP socket stale, so foregrounding triggers checkAndReopen -> forceReopen, which drops all SDK subscriptions and reopens the socket without going through connect(). The module-level roomsSubscription guard therefore stayed set, so subscribeRooms() skipped re-subscribing stream-notify-user and the rooms list silently stopped reflecting subscriptions/favorites/reads until a manual reconnect. Reset the guard from the socket 'close' listener (via unsubscribeRooms), the same teardown connect() already performs, so the resume-login that follows the reopen re-subscribes stream-notify-user. Other streams (permissions, presence, settings, roles) subscribe unconditionally and were never affected. --- app/lib/services/connect.test.ts | 31 +++++++++++++++++++++++++++++++ app/lib/services/connect.ts | 5 +++++ 2 files changed, 36 insertions(+) diff --git a/app/lib/services/connect.test.ts b/app/lib/services/connect.test.ts index 01b2fc2dd73..9d20bbf30ca 100644 --- a/app/lib/services/connect.test.ts +++ b/app/lib/services/connect.test.ts @@ -1,6 +1,7 @@ import { connect, determineAuthType, disconnect } from './connect'; import { mediaSessionInstance } from './voip/MediaSessionInstance'; import { pendingHangups } from './voip/pendingHangups'; +import { unsubscribeRooms } from '../methods/subscribeRooms'; jest.mock('./voip/MediaSessionInstance', () => ({ mediaSessionInstance: { reset: jest.fn(), drainPendingHangups: jest.fn() } @@ -481,4 +482,34 @@ describe('connect — pendingHangups drain on reconnect', () => { }); }); +describe('connect — rooms subscription guard reset on close', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockOnStreamDataStops.length = 0; + mockStoreGetState.mockReturnValue({ + meteor: { connected: false }, + login: { user: null, isAuthenticated: false }, + settings: {} + }); + }); + + // Regression: a long background marks the DDP socket stale, so foregrounding triggers + // `checkAndReopen` → `forceReopen`, which wipes the SDK subscriptions and emits 'close' while + // bypassing `connect()`. The rooms-list `stream-notify-user` feed only re-subscribes when the + // module-level guard in `subscribeRooms` is clear, and `unsubscribeRooms()` is what clears it. + // If the 'close' handler stops calling `unsubscribeRooms()`, the guard stays set after reconnect + // and the rooms list silently stops updating (subscriptions/favorites/reads). + it('calls unsubscribeRooms when the socket "close" fires', async () => { + await connect({ server: 'https://example.com' }); + + // connect() itself calls unsubscribeRooms() once while tearing down prior listeners; ignore it. + (unsubscribeRooms as jest.Mock).mockClear(); + + const closeHandler = getHandlersByEvent('close')[0]; + closeHandler(); + + expect(unsubscribeRooms).toHaveBeenCalledTimes(1); + }); +}); + // Note: Apple authentication when isIOS is true is tested in connect.ios.test.ts diff --git a/app/lib/services/connect.ts b/app/lib/services/connect.ts index 9ba42c8d781..12348aeaf98 100644 --- a/app/lib/services/connect.ts +++ b/app/lib/services/connect.ts @@ -132,6 +132,11 @@ function connect({ server, logoutOnError = false }: { server: string; logoutOnEr let pendingHangupsDrainArmed = false; closeListener = sdk.current.onStreamData('close', () => { + // Reset the rooms-subscription guard on every socket close. `forceReopen` (triggered by + // `checkAndReopen` after a long background) wipes the SDK subscriptions and emits 'close' + // but bypasses `connect()`, so without this the guard in `subscribeRooms` stays set and + // `stream-notify-user` is never re-subscribed — the rooms list silently stops updating. + unsubscribeRooms(); pendingHangupsDrainArmed = true; store.dispatch(disconnectAction()); });