From cd3ecc45075864b554232a7fc8b8760c3afaab7b Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sat, 24 Jan 2026 19:35:00 -0300 Subject: [PATCH] fix: only force-reconnect on tab visibility change, not on activity gaps The 60s stale threshold was triggering during normal use (reading, thinking) causing constant reconnects and presence join/leave toast spam. Now force- reconnect only happens when the tab was hidden for 3+ minutes and becomes visible again. Regular activity (mouse/keyboard) only resets the inactivity timer without ever forcing a reconnect. Co-Authored-By: Claude Opus 4.5 --- .../editor/[projectId]/FlowchartEditor.tsx | 2 +- src/lib/collaboration/realtime.ts | 24 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/app/editor/[projectId]/FlowchartEditor.tsx b/src/app/editor/[projectId]/FlowchartEditor.tsx index 879df51..d134a63 100644 --- a/src/app/editor/[projectId]/FlowchartEditor.tsx +++ b/src/app/editor/[projectId]/FlowchartEditor.tsx @@ -691,7 +691,7 @@ function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName, useEffect(() => { const handleVisibilityChange = () => { if (!document.hidden) { - realtimeRef.current?.notifyActivity() + realtimeRef.current?.notifyVisibilityResumed() } } diff --git a/src/lib/collaboration/realtime.ts b/src/lib/collaboration/realtime.ts index 800e273..e494385 100644 --- a/src/lib/collaboration/realtime.ts +++ b/src/lib/collaboration/realtime.ts @@ -44,7 +44,7 @@ const RECONNECT_BASE_DELAY_MS = 1000 const RECONNECT_MAX_DELAY_MS = 30_000 const INACTIVITY_TIMEOUT_MS = 5 * 60_000 // 5 minutes of inactivity before pausing const CONNECTION_TIMEOUT_MS = 15_000 // 15s timeout for initial connection -const STALE_THRESHOLD_MS = 60_000 // 60s of inactivity = force fresh reconnect on return +const STALE_THRESHOLD_MS = 3 * 60_000 // 3 minutes hidden before forcing fresh reconnect export class RealtimeConnection { private channel: RealtimeChannel | null = null @@ -262,21 +262,31 @@ export class RealtimeConnection { /** * Notify that user activity has occurred, resetting the inactivity timer. * If the connection was paused due to inactivity, it will resume. - * If the connection was idle for a significant period, force a fresh reconnect - * to ensure the WebSocket isn't stale. */ notifyActivity(): void { + if (this.isDestroyed) return + this.lastActivityTime = Date.now() + this.resetInactivityTimer() + + if (this.isPaused) { + this.resume() + } + } + + /** + * Called when the tab becomes visible again. If the tab was hidden for a + * significant period, force a fresh reconnect to handle stale WebSockets. + */ + notifyVisibilityResumed(): void { if (this.isDestroyed) return const now = Date.now() - const idleDuration = now - this.lastActivityTime + const hiddenDuration = now - this.lastActivityTime this.lastActivityTime = now this.resetInactivityTimer() if (this.isPaused) { this.resume() - } else if (idleDuration > STALE_THRESHOLD_MS && this.channel) { - // Connection may appear alive but WebSocket could be stale after idle. - // Force a fresh reconnect to ensure broadcasts actually work. + } else if (hiddenDuration > STALE_THRESHOLD_MS && this.channel) { this.forceReconnect() } }