Skip to content

Commit f6cd3b7

Browse files
ggazzoclaude
andcommitted
fix(sdk): only bridge result/updated frames when Meteor still has the invoker
After force-logout cycles (e.g. resetUserE2EKey → ws.terminate → SDK reconnect), stale result/updated frames for methods whose Meteor invoker already cleared can arrive over the SDK socket. _process_updated then throws "No callback invoker for method N" out of an async generator — the throw escapes the bridge's try/catch as an unhandled rejection and aborts Meteor's frame queue, so subsequent login result frames never land and the page stays wedged on /login. Gate the result/updated bridge on _methodInvokers[id] existing so stale frames are silently dropped instead of corrupting Meteor's frame processing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 310a44f commit f6cd3b7

1 file changed

Lines changed: 22 additions & 2 deletions

File tree

apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ const SUBSCRIPTION_LIFECYCLE_FRAMES = new Set(['ready', 'nosub']);
3939
// bridge so SDK's own callAsync flows aren't surfaced into Meteor.
4040
const isSdkInternalId = (id: unknown): boolean => typeof id === 'string' && id.startsWith('rc-ddp-client-');
4141

42+
const getMeteorMethodInvokers = (): Record<string, unknown> => {
43+
return (Meteor.connection as unknown as { _methodInvokers?: Record<string, unknown> })._methodInvokers ?? {};
44+
};
45+
46+
const meteorHasInvoker = (id: unknown): boolean => typeof id === 'string' && id in getMeteorMethodInvokers();
47+
4248
const shouldBridgeToMeteor = (frame: ParsedDdpFrame): boolean => {
4349
if (!frame || typeof frame.msg !== 'string') return false;
4450

@@ -47,15 +53,29 @@ const shouldBridgeToMeteor = (frame: ParsedDdpFrame): boolean => {
4753
}
4854

4955
if (frame.msg === 'result') {
50-
return !isSdkInternalId(frame.id);
56+
if (isSdkInternalId(frame.id)) return false;
57+
// Drop result frames whose Meteor invoker is gone — feeding them
58+
// surfaces as "Received method result but no methods outstanding" and
59+
// can race with `updated` to leave Meteor's _processOneDataMessage in a
60+
// half-processed state (force-logout cycles invalidate the in-flight
61+
// method's invoker before its result returns over the SDK socket).
62+
return meteorHasInvoker(frame.id);
5163
}
5264
if (frame.msg === 'updated') {
5365
const methods = Array.isArray(frame.methods) ? (frame.methods as unknown[]) : [];
66+
if (methods.length === 0) return false;
5467
// If any of the methodIds in the `updated` frame is SDK-internal, drop
5568
// the whole frame: Meteor processes every id and would throw on the
5669
// first miss. In practice an `updated` frame carries ids from a single
5770
// originating method call, so this is all-or-nothing anyway.
58-
return methods.length > 0 && !methods.some(isSdkInternalId);
71+
if (methods.some(isSdkInternalId)) return false;
72+
// Same defence as for `result` — bridge only when Meteor still has an
73+
// invoker for every methodId. Without this, `_process_updated` throws
74+
// "No callback invoker for method N" out of an async generator and the
75+
// throw escapes the try/catch below as an unhandled rejection,
76+
// aborting Meteor's frame queue (the next login's result never lands,
77+
// userId stays null, and the page is stuck on /login).
78+
return methods.every(meteorHasInvoker);
5979
}
6080

6181
return false;

0 commit comments

Comments
 (0)