From 28bfa371995cf56197a0a1d7e23509c5b1a452f4 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 30 Mar 2026 20:16:01 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20collab=20overlay=20stuck=20on=20'Connect?= =?UTF-8?q?ing'=20=E2=80=94=20init=20race=20+=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Runtime now sets isInitialized after IndexedDB (before WS connect) so the overlay can detect it within ~500ms instead of waiting 30s+ - Overlay no longer gives up polling after 15s (slows to 2s instead) - Overlay subscribes to runtime onConnect/onDisconnect for live updates Co-Authored-By: Claude Opus 4.6 --- shared/components/rstack-collab-overlay.ts | 27 ++++++++++++++++++---- shared/local-first/runtime.ts | 10 ++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/shared/components/rstack-collab-overlay.ts b/shared/components/rstack-collab-overlay.ts index 572dae7..d663e5c 100644 --- a/shared/components/rstack-collab-overlay.ts +++ b/shared/components/rstack-collab-overlay.ts @@ -121,6 +121,8 @@ export class RStackCollabOverlay extends HTMLElement { } if (this.#gcInterval) clearInterval(this.#gcInterval); this.#gcInterval = null; + if (this.#runtimePollInterval) clearInterval(this.#runtimePollInterval); + this.#runtimePollInterval = null; } // ── Public bridge API (for canvas CommunitySync) ── @@ -171,21 +173,36 @@ export class RStackCollabOverlay extends HTMLElement { // ── Runtime connection ── + #runtimePollInterval: ReturnType | null = null; + #tryConnect() { const runtime = (window as any).__rspaceOfflineRuntime; if (runtime?.isInitialized) { this.#onRuntimeReady(runtime); } else { - // Retry until runtime is ready - const check = setInterval(() => { + // Poll until runtime is ready (no timeout — WS connect can take 30s+) + let polls = 0; + this.#runtimePollInterval = setInterval(() => { const rt = (window as any).__rspaceOfflineRuntime; if (rt?.isInitialized) { - clearInterval(check); + clearInterval(this.#runtimePollInterval!); + this.#runtimePollInterval = null; this.#onRuntimeReady(rt); } + // Slow down after 15s (30 polls × 500ms) to reduce overhead + polls++; + if (polls === 30 && this.#runtimePollInterval) { + clearInterval(this.#runtimePollInterval); + this.#runtimePollInterval = setInterval(() => { + const rt2 = (window as any).__rspaceOfflineRuntime; + if (rt2?.isInitialized) { + clearInterval(this.#runtimePollInterval!); + this.#runtimePollInterval = null; + this.#onRuntimeReady(rt2); + } + }, 2000); + } }, 500); - // Give up after 15s - setTimeout(() => clearInterval(check), 15000); } } diff --git a/shared/local-first/runtime.ts b/shared/local-first/runtime.ts index f1d7681..db41378 100644 --- a/shared/local-first/runtime.ts +++ b/shared/local-first/runtime.ts @@ -88,7 +88,11 @@ export class RSpaceOfflineRuntime { // 1. Open IndexedDB await this.#store.open(); - // 2. Connect WebSocket (non-blocking — works offline) + // 2. Mark initialized early so UI components can start tracking state + // (WS connect can take 30s+ if the server is slow to respond) + this.#initialized = true; + + // 3. Connect WebSocket (non-blocking — works offline) const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${proto}//${location.host}/ws/${this.#activeSpace}`; @@ -103,9 +107,7 @@ export class RSpaceOfflineRuntime { this.#setStatus('offline'); } - this.#initialized = true; - - // 3. Storage housekeeping (non-blocking) + // 4. Storage housekeeping (non-blocking) this.#runStorageHousekeeping(); } catch (e) { this.#setStatus('error');