|
1 | | -import { Accounts } from 'meteor/accounts-base'; |
2 | 1 | import { DDPCommon } from 'meteor/ddp-common'; |
3 | 2 | import { Meteor } from 'meteor/meteor'; |
4 | 3 | import { Tracker } from 'meteor/tracker'; |
@@ -214,67 +213,51 @@ queueMicrotask(() => { |
214 | 213 | }); |
215 | 214 |
|
216 | 215 | // 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). |
224 | 222 | // |
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. |
235 | 239 | const sdk = getDdpSdk(); |
236 | 240 | let firstConnectHandled = false; |
237 | | -let reconnectCount = 0; |
238 | 241 | sdk.connection.on('connected', () => { |
239 | 242 | if (!firstConnectHandled) { |
240 | 243 | firstConnectHandled = true; |
241 | | - console.warn('[stubMeteorStream] first SDK connected (no reset)'); |
242 | 244 | return; |
243 | 245 | } |
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 | | - }); |
250 | 246 | 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?.(); |
252 | 260 | } catch (err) { |
253 | | - console.warn('[stubMeteorStream] reset on SDK reconnect failed', err); |
| 261 | + console.warn('[stubMeteorStream] custom reset on SDK reconnect failed', err); |
254 | 262 | } |
255 | 263 | }); |
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