Skip to content

Commit 8861751

Browse files
ggazzoclaude
andcommitted
fix(sdk): resend outstanding methods on SDK reconnect, skip _reconnectStopper
The full fire('reset') was firing accounts-base's _reconnectStopper, which retries login with the captured `result.token` from the original callLoginMethod scope. After force-logout, that token is the stale one the server just invalidated. The retry runs on a wait:true block that queues AHEAD of the test's own loginByUserState; the stopper's userCallback then calls makeClientLoggedOut, which clobbers the credentials the test just injected, and the test's queued login never sends a frame. With the ddp-streamer ws.close() change, useForceLogout now reliably fires (the notify-user/<uid>/force_logout stream message arrives before the socket closes), so we don't need accounts-base's reconnect-time relogin retry at all. We still need to resend in-flight methods so that tests like message-actions / report-message / e2ee-encryption-decryption don't wedge. Mirror onReset's _handleOutstandingMethodsOnReset + _sendOutstandingMethodBlocksMessages + _resendSubscriptions directly, skipping _callOnReconnectAndSendAppropriateOutstandingMethods (which is where _reconnectStopper would fire). Also drop the diagnostic logging now that we have a fix in mind. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 64f0445 commit 8861751

1 file changed

Lines changed: 36 additions & 53 deletions

File tree

apps/meteor/client/meteor/overrides/stubMeteorStream.ts

Lines changed: 36 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Accounts } from 'meteor/accounts-base';
21
import { DDPCommon } from 'meteor/ddp-common';
32
import { Meteor } from 'meteor/meteor';
43
import { Tracker } from 'meteor/tracker';
@@ -214,67 +213,51 @@ queueMicrotask(() => {
214213
});
215214

216215
// When the underlying SDK socket reconnects (e.g. after a server-side
217-
// ws.terminate from force-logout in microservices), Meteor's connection sees
218-
// no transport event because the stub keeps reporting 'connected'. As a
219-
// result, Meteor's normal reconnect machinery — `_streamHandlers.onReset` →
220-
// `_callOnReconnectAndSendAppropriateOutstandingMethods` → DDP.onReconnect
221-
// callbacks → the per-call `_reconnectStopper` that retries login with the
222-
// latest stored token (and calls `makeClientLoggedOut` on failure) — never
223-
// fires. The user is left with stale credentials.
216+
// ws.terminate / ws.close from force-logout in microservices), Meteor's
217+
// connection sees no transport event because the stub keeps reporting
218+
// 'connected'. Methods that were sent but not yet completed on the prior
219+
// SDK session are stranded with sentMessage=true, so any test that fires a
220+
// method right when the socket churns wedges (message-actions /
221+
// report-message / e2ee-encryption-decryption all timed out before this).
224222
//
225-
// Fire `reset` on every subsequent SDK 'connected' event so accounts-base's
226-
// onReconnect callback retries login with whatever's currently in
227-
// localStorage AND so methods that were sent but not yet completed get
228-
// resent on the new session (skipping this part wedges tests that fire a
229-
// method right when the SDK socket churns: message-actions / report-message
230-
// / e2ee-encryption-decryption all timed out without it). The first connect
231-
// is handled by the queueMicrotask above; skip it here. The "method result
232-
// but no methods outstanding" warnings that the resent blocks generate are
233-
// caught and suppressed by the bridge's async catch in
234-
// ddpSdkCollectionBridge.
223+
// Resend the in-flight method blocks on every subsequent SDK 'connected'
224+
// event. Mirror everything Meteor's `_streamHandlers.onReset` does EXCEPT
225+
// invoke `DDP._reconnectHook` callbacks: the per-call `_reconnectStopper`
226+
// that callLoginMethod registers (accounts_client.js:292) retries login
227+
// with the captured `result.token` from the *original* successful login,
228+
// which is now the stale token the server just invalidated. That retry
229+
// runs on a `wait:true` block which queues *ahead of* the test's own
230+
// loginByUserState login; if it fails (it will), the stopper's userCallback
231+
// calls `makeClientLoggedOut`, which clobbers the credentials the test
232+
// just injected — and the test's queued login then never sends a frame
233+
// because the stopper's wait block drains in a way that confuses the
234+
// outstanding queue. Force-logout cleanup is now handled by the
235+
// `useForceLogout` client hook (notify-user/<uid>/force_logout stream
236+
// message) which arrives reliably since DDPStreamer.ts:62 was switched to
237+
// ws.close(). The first connect is handled by the queueMicrotask above;
238+
// skip it here.
235239
const sdk = getDdpSdk();
236240
let firstConnectHandled = false;
237-
let reconnectCount = 0;
238241
sdk.connection.on('connected', () => {
239242
if (!firstConnectHandled) {
240243
firstConnectHandled = true;
241-
console.warn('[stubMeteorStream] first SDK connected (no reset)');
242244
return;
243245
}
244-
reconnectCount += 1;
245-
const storedToken = typeof window !== 'undefined' ? window.localStorage.getItem('Meteor.loginToken') : null;
246-
console.warn(`[stubMeteorStream] SDK reconnect #${reconnectCount} — firing reset`, {
247-
storedToken: storedToken ? `${storedToken.slice(0, 6)}…` : null,
248-
userId: (Meteor.connection as unknown as { _userId?: string | null })._userId ?? null,
249-
});
250246
try {
251-
fire('reset');
247+
const c = conn as unknown as {
248+
_outstandingMethodBlocks: Array<{ wait: boolean; methods: unknown[] }>;
249+
_sendOutstandingMethodBlocksMessages(blocks: Array<{ wait: boolean; methods: unknown[] }>): void;
250+
_streamHandlers: {
251+
_handleOutstandingMethodsOnReset?: () => void;
252+
_resendSubscriptions?: () => void;
253+
};
254+
};
255+
c._streamHandlers._handleOutstandingMethodsOnReset?.();
256+
const oldBlocks = c._outstandingMethodBlocks;
257+
c._outstandingMethodBlocks = [];
258+
c._sendOutstandingMethodBlocksMessages(oldBlocks);
259+
c._streamHandlers._resendSubscriptions?.();
252260
} catch (err) {
253-
console.warn('[stubMeteorStream] reset on SDK reconnect failed', err);
261+
console.warn('[stubMeteorStream] custom reset on SDK reconnect failed', err);
254262
}
255263
});
256-
257-
// Diagnostic: wrap _pollStoredLoginToken to log every call so we can see
258-
// whether the test's loginByUserState actually triggers a login on the
259-
// failing :87 cycle, or whether it short-circuits because the cached
260-
// _lastLoginTokenWhenPolled still matches what's in storage.
261-
{
262-
const acc = Accounts as unknown as {
263-
_pollStoredLoginToken?: () => void;
264-
_lastLoginTokenWhenPolled?: string | null;
265-
_storedLoginToken?: () => string | null;
266-
};
267-
const originalPoll = acc._pollStoredLoginToken;
268-
if (typeof originalPoll === 'function') {
269-
acc._pollStoredLoginToken = function patchedPoll(this: typeof acc): void {
270-
const current = acc._storedLoginToken?.() ?? null;
271-
const last = acc._lastLoginTokenWhenPolled ?? null;
272-
console.warn('[stubMeteorStream] _pollStoredLoginToken', {
273-
current: current ? `${current.slice(0, 6)}…` : null,
274-
last: last ? `${last.slice(0, 6)}…` : null,
275-
willFire: current !== last,
276-
});
277-
return originalPoll.call(this);
278-
};
279-
}
280-
}

0 commit comments

Comments
 (0)