From 5b084e569b43d44318394481f6cbbbb4f62b8e29 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Thu, 18 Jun 2026 23:11:52 -0400 Subject: [PATCH] fix(collab): skip live-doc registration grace when the direct connection timed out loadCanonicalYDoc bounds the openDirectConnection attempt with COLLAB_DIRECT_CONNECTION_TIMEOUT_MS, but on timeout it still waited the full COLLAB_LIVE_DOC_REGISTRATION_GRACE_MS (default 1500ms) for a live doc to register - so a stalled direct connection took ~direct-timeout + 1500ms instead of bailing promptly, defeating the bounded timeout. A timed-out direct connection establishes nothing for the live-doc map to register, so propagate a timedOut flag from getOrLoadHocuspocusDoc and skip the registration grace in that case. An already-registered live doc is still returned by the point-in-time check before the grace, so a concurrently present live doc is unaffected. Verified against the existing collab-direct-connection-timeout-regression test (now returns null in well under the bound). --- server/collab.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/server/collab.ts b/server/collab.ts index 721ffe1c..bf29352f 100644 --- a/server/collab.ts +++ b/server/collab.ts @@ -7300,11 +7300,16 @@ export async function loadCanonicalYDoc( degradedReason: null, }; } - const { doc, cleanup } = await getOrLoadHocuspocusDoc(slug, { + const { doc, cleanup, timedOut } = await getOrLoadHocuspocusDoc(slug, { allowDirectConnection: options.liveRequired === true, }); let registeredLiveDoc = getLiveHocuspocusDoc(slug); - if (!registeredLiveDoc && options.liveRequired) { + // Only wait out the registration grace when the direct connection itself did + // not time out. A stalled/timed-out direct connection establishes nothing for + // the live-doc map to register, so the bounded direct-connection timeout would + // otherwise be followed by the full (much longer) registration grace, defeating + // its purpose. If the doc is already registered, the check above returns it. + if (!registeredLiveDoc && options.liveRequired && !timedOut) { registeredLiveDoc = await waitForLiveHocuspocusDocRegistration(slug); } if (registeredLiveDoc) { @@ -9560,7 +9565,7 @@ type DirectConnectionLike = { async function getOrLoadHocuspocusDoc( slug: string, options: { allowDirectConnection?: boolean } = {}, -): Promise<{ doc: Y.Doc | null; cleanup?: () => Promise }> { +): Promise<{ doc: Y.Doc | null; cleanup?: () => Promise; timedOut?: boolean }> { const existing = getLiveHocuspocusDoc(slug); if (existing) return { doc: existing }; if (!options.allowDirectConnection) return { doc: null }; @@ -9621,9 +9626,9 @@ async function getOrLoadHocuspocusDoc( } }; - if (doc && typeof (doc as any).getText === 'function') return { doc, cleanup }; - if (direct) return { doc: null, cleanup }; - return { doc: null }; + if (doc && typeof (doc as any).getText === 'function') return { doc, cleanup, timedOut: directTimedOut }; + if (direct) return { doc: null, cleanup, timedOut: directTimedOut }; + return { doc: null, timedOut: directTimedOut }; } async function waitForLiveHocuspocusDocRegistration(slug: string): Promise {