Skip to content

Update on tab change#2235

Merged
simo6529 merged 15 commits intomainfrom
update-on-tab-change
Apr 9, 2026
Merged

Update on tab change#2235
simo6529 merged 15 commits intomainfrom
update-on-tab-change

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented Apr 8, 2026

Summary by CodeRabbit

  • New Features

    • Time labels now refresh on tab visibility or window focus (with 1s cooldown); periodic refresh retained.
  • Bug Fixes

    • WebSocket lifecycle hardened to ignore stale socket events, avoid duplicate reconnects, and better handle socket replacement.
  • Refactor

    • Stream sync and resume logic centralized and stabilized for safer lifecycle handling.
  • Tests

    • Expanded tests for visibility/focus and WebSocket health/replacement; removed an outdated integration-style test.
  • Chores

    • Added development debug logging for WebSocket resume events.

simo6529 added 6 commits April 7, 2026 16:24
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Browser-resume and focus events now trigger time-label refreshes and resume health checks that may request websocket reconnects; WebSocket provider ignores stale socket events and logs debug info; stream sync/refetch was centralized; several tests were added/updated and one integration-style stream-context test file was removed. (50 words)

Changes

Cohort / File(s) Summary
WebSocket Provider & Debug
services/websocket/WebSocketProvider.tsx, services/websocket/webSocketDebug.ts, services/websocket/AppWebSocketProvider.tsx
Added development debug logger and extensive debug statements; guarded websocket event handlers to ignore stale sockets; clarified provider responsibilities in comments.
Health Check & Resume Logic
services/websocket/useWebSocketHealth.ts, __tests__/services/websocket/useWebSocketHealth.test.ts
Introduced resume/focus listeners, hidden-duration/dedupe/cooldown constants, performHealthCheckForSource and performResumeHealthCheck, wired hiddenAtRef and interval sources, and expanded tests for resume/reconnect scenarios.
WebSocket Provider Tests
__tests__/services/websocket/WebSocketProvider.test.tsx
Extended tests to ensure replaced/stale sockets are closed/ignored, provider remains CONNECTING until the current socket opens, and that stale socket opens are ignored.
Stream Context Logic
contexts/wave/MyStreamContext.tsx, __tests__/contexts/wave/MyStreamContext.test.tsx (deleted)
Centralized sync/refetch via useEffectEvent; switched gating to WebSocketStatus.CONNECTED; added browser-resume sync with 1s cooldown; moved waves ref updates to useLayoutEffect; removed integration-style test file.
Time-display Component & Tests
components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.tsx, __tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.test.tsx
Replaced now state with forceRefresh driven by useEffectEvent; stopped passing now to getTimeAgoShort; added visibilitychange and focus listeners and tests that mock visibility/time to validate refreshes.
Tests Cleanup
__tests__/contexts/wave/MyStreamContext.test.tsx
Removed a previously present integration-style test file covering MyStreamProvider and related hooks.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant Component as BrainLeftSidebarWaveDropTime
    participant Health as useWebSocketHealth
    participant Provider as WebSocketProvider
    participant Context as MyStreamContext

    Browser->>Component: visibilitychange / focus
    Component->>Component: refreshNow() (useEffectEvent)
    Component->>Component: re-render -> getTimeAgoShort(time)

    Browser->>Health: visibilitychange / focus
    Health->>Health: dedupe & cooldown checks
    alt No reconnect needed
        Health->>Health: no action
    else Reconnect needed
        Health->>Provider: request connect(currentToken)
        Provider->>Provider: create new WebSocket (ws2)
        Provider->>Provider: ignore events from stale ws1
        Provider->>Health: ws2.onopen -> report CONNECTED
    end

    Health->>Context: trigger sync + refetch
    Context->>Context: registerWave + refetchAllWaves
    Context->>Browser: UI updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ragnep
  • prxt6529

Poem

🐰 I hop when tabs wake, ears quick and keen,

I nudge the clocks so labels stay seen.
When focus returns and sockets restart,
I guard the old ones and do my part.
A tiny twitch — the UI hums at heart.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Update on tab change' directly aligns with the main objective of the PR, which adds resume synchronization mechanisms triggered by document visibility changes and window focus events when tabs become active.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch update-on-tab-change

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f201afa382

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.tsx (1)

29-47: Consider lifting these global listeners out of each row component.

Every BrainLeftSidebarWaveDropTime instance now adds its own visibilitychange and focus listeners. In a longer sidebar list, one tab resume fans out into N listeners and N state updates, and both events can fire for the same resume. A shared resume tick at the list/context level would keep the refresh behavior without multiplying listeners per row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.tsx` around
lines 29 - 47, The component BrainLeftSidebarWaveDropTime currently registers
document.visibilitychange and window.focus listeners inside its useEffect,
causing N listeners and repeated refreshNow calls; lift these global listeners
to the parent/list/context level (e.g., the waves list component or a new
useWindowResume hook) so a single handler calls a shared refresh tick and then
propagate that tick into each row via a prop or context subscription; remove the
visibility/focus useEffect from BrainLeftSidebarWaveDropTime and instead call
the existing refreshNow from the single centralized handler to avoid per-row
listener multiplication.
contexts/wave/MyStreamContext.tsx (1)

182-197: Avoid double full-wave refetches on browser resume.

runBrowserResumeSync() always calls syncActiveWaveAndRefetch(). On the same visibilitychange/focus event, services/websocket/useWebSocketHealth.ts can reconnect the socket, and Line 199 then runs the same sync again when status flips back to CONNECTED. That gives the common stale-tab-resume path two refetchAllWaves() calls back-to-back.

♻️ Possible adjustment
   lastBrowserResumeSyncAtRef.current = now;
-  syncActiveWaveAndRefetch();
+  if (websocketStatus === WebSocketStatus.CONNECTED) {
+    syncActiveWaveAndRefetch();
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contexts/wave/MyStreamContext.tsx` around lines 182 - 197,
runBrowserResumeSync currently always calls syncActiveWaveAndRefetch which can
duplicate a full refetch when useWebSocketHealth also triggers a
reconnect-triggered refetch; modify runBrowserResumeSync so it first checks the
websocket health/status (from useWebSocketHealth) and only calls
syncActiveWaveAndRefetch if the socket is not already CONNECTED OR if the last
full refetch timestamp is older than BROWSER_RESUME_SYNC_COOLDOWN_MS (e.g.,
track lastRefetchAllWavesAt similar to lastBrowserResumeSyncAtRef); this
prevents back-to-back refetchAllWaves calls while keeping the existing cooldown
logic.
__tests__/services/websocket/WebSocketProvider.test.tsx (1)

683-770: Add a stale-message regression case too.

The provider now guards onmessage/onerror for replaced sockets, but these tests only pin open and close. A late message from ws1 after connect("token2") is the path that would duplicate downstream updates, so it is worth asserting that subscribers are not notified from the replaced socket.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/services/websocket/WebSocketProvider.test.tsx` around lines 683 -
770, Add a regression test that after calling result.current.connect("token2")
and obtaining ws1 and ws2, firing a late message from the replaced socket (call
ws1.triggerMessage(...) or the MockWebSocket equivalent) does not notify
subscribers or mutate context state; e.g., assert the context’s state
(result.current.status) and any message handler mocks remain unchanged after
ws1.triggerMessage and only change when ws2.triggerMessage is fired. Locate the
test block near the existing "ignores open events from a replaced socket" and
reuse MockWebSocket, result.current.connect, and WebSocketStatus to implement
this assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@__tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.test.tsx`:
- Around line 34-67: The test still asserts two-argument calls to
getTimeAgoShort even though BrainLeftSidebarWaveDropTime now calls
getTimeAgoShort(time) — update the test to stop expecting the second timestamp:
either change the expectations to check call counts/that getTimeAgoShort was
called with a single arg (e.g. toHaveBeenLastCalledWith(10) or
toHaveBeenCalledTimes(...)) and/or assert the rendered output/rerender count
after triggering setDocumentVisibilityState("visible")/visibilitychange and
window.dispatchEvent(new Event("focus")); alternatively adjust the mocked
getTimeAgoShort implementation in this test to itself call Date.now() so the
single-argument call still yields different return values across the simulated
times. Ensure references to getTimeAgoShort and BrainLeftSidebarWaveDropTime are
updated accordingly.

In `@services/websocket/useWebSocketHealth.ts`:
- Around line 145-149: The reconnect logic only resumes sockets when
webSocketStateRef.current.status === WebSocketStatus.CONNECTED, skipping sockets
stuck in CONNECTING; update the recovery in the branch that reads const {
status: currentStatus, connect: currentConnect } = webSocketStateRef.current to
also handle WebSocketStatus.CONNECTING (i.e., treat CONNECTING like CONNECTED or
explicitly replace/resume that socket), so call currentConnect(currentToken)
when currentStatus is either CONNECTED or CONNECTING (or otherwise ensure
CONNECTING sockets are recreated), and keep performHealthCheck() behavior intact
to avoid leaving stale in-flight connections.

---

Nitpick comments:
In `@__tests__/services/websocket/WebSocketProvider.test.tsx`:
- Around line 683-770: Add a regression test that after calling
result.current.connect("token2") and obtaining ws1 and ws2, firing a late
message from the replaced socket (call ws1.triggerMessage(...) or the
MockWebSocket equivalent) does not notify subscribers or mutate context state;
e.g., assert the context’s state (result.current.status) and any message handler
mocks remain unchanged after ws1.triggerMessage and only change when
ws2.triggerMessage is fired. Locate the test block near the existing "ignores
open events from a replaced socket" and reuse MockWebSocket,
result.current.connect, and WebSocketStatus to implement this assertion.

In `@components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.tsx`:
- Around line 29-47: The component BrainLeftSidebarWaveDropTime currently
registers document.visibilitychange and window.focus listeners inside its
useEffect, causing N listeners and repeated refreshNow calls; lift these global
listeners to the parent/list/context level (e.g., the waves list component or a
new useWindowResume hook) so a single handler calls a shared refresh tick and
then propagate that tick into each row via a prop or context subscription;
remove the visibility/focus useEffect from BrainLeftSidebarWaveDropTime and
instead call the existing refreshNow from the single centralized handler to
avoid per-row listener multiplication.

In `@contexts/wave/MyStreamContext.tsx`:
- Around line 182-197: runBrowserResumeSync currently always calls
syncActiveWaveAndRefetch which can duplicate a full refetch when
useWebSocketHealth also triggers a reconnect-triggered refetch; modify
runBrowserResumeSync so it first checks the websocket health/status (from
useWebSocketHealth) and only calls syncActiveWaveAndRefetch if the socket is not
already CONNECTED OR if the last full refetch timestamp is older than
BROWSER_RESUME_SYNC_COOLDOWN_MS (e.g., track lastRefetchAllWavesAt similar to
lastBrowserResumeSyncAtRef); this prevents back-to-back refetchAllWaves calls
while keeping the existing cooldown logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbd84d2e-d75b-491e-845e-b84d5977f781

📥 Commits

Reviewing files that changed from the base of the PR and between 444f5c8 and f201afa.

📒 Files selected for processing (9)
  • __tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.test.tsx
  • __tests__/contexts/wave/MyStreamContext.test.tsx
  • __tests__/services/websocket/WebSocketProvider.test.tsx
  • __tests__/services/websocket/useWebSocketHealth.test.ts
  • components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime.tsx
  • contexts/wave/MyStreamContext.tsx
  • services/websocket/AppWebSocketProvider.tsx
  • services/websocket/WebSocketProvider.tsx
  • services/websocket/useWebSocketHealth.ts
💤 Files with no reviewable changes (1)
  • tests/contexts/wave/MyStreamContext.test.tsx

Comment thread services/websocket/useWebSocketHealth.ts Outdated
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/websocket/useWebSocketHealth.ts`:
- Around line 125-128: The resume-dedupe logic currently prevents a valid resume
check if a focus happened just before a hide/show cycle because
lastResumeCheckAtRef.current isn't cleared on hide; update the hide handler
(where hiddenAtRef is set) to also reset lastResumeCheckAtRef.current (or set it
to 0/null) so each new hidden->visible cycle triggers one resume check; apply
the same change in both places noted around the checks that reference
RESUME_EVENT_DEDUPE_WINDOW_MS, lastResumeCheckAtRef, and hiddenAtRef so the
dedupe window is cleared whenever the document becomes hidden.
- Around line 88-98: The two branches that call currentConnect(currentToken) and
set action = "connect" are duplicated; replace them with a single conditional
that checks currentToken && (currentStatus === WebSocketStatus.DISCONNECTED ||
currentToken !== previousToken) and then calls currentConnect(currentToken) and
sets action = "connect" (references: currentToken, currentStatus,
WebSocketStatus.DISCONNECTED, previousToken, currentConnect, action).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87fbd852-4c8d-4b8a-8827-dde345f820ef

📥 Commits

Reviewing files that changed from the base of the PR and between f201afa and ca9f1a0.

📒 Files selected for processing (2)
  • __tests__/services/websocket/useWebSocketHealth.test.ts
  • services/websocket/useWebSocketHealth.ts

Comment thread services/websocket/useWebSocketHealth.ts Outdated
Comment thread services/websocket/useWebSocketHealth.ts Outdated
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
services/websocket/useWebSocketHealth.ts (2)

145-149: ⚠️ Potential issue | 🟠 Major

Handle CONNECTING sockets in the forced-resume path.

This still skips recovery when the provider is stuck in CONNECTING after a long hidden period, so the tab can resume on a stale in-flight socket.

🔧 Minimal fix
-    if (currentStatus === WebSocketStatus.CONNECTED) {
+    if (currentStatus !== WebSocketStatus.DISCONNECTED) {
       currentConnect(currentToken);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/websocket/useWebSocketHealth.ts` around lines 145 - 149, The
forced-resume branch only calls currentConnect when
webSocketStateRef.current.status equals WebSocketStatus.CONNECTED, which skips
recovery if the socket is stuck in WebSocketStatus.CONNECTING; update the
condition in the forced-resume path to also treat WebSocketStatus.CONNECTING as
a case that should call currentConnect(currentToken) (i.e., when
webSocketStateRef.current.status === WebSocketStatus.CONNECTED ||
webSocketStateRef.current.status === WebSocketStatus.CONNECTING) so both
CONNECTED and CONNECTING sockets are re-attached; references: webSocketStateRef,
WebSocketStatus.CONNECTED, WebSocketStatus.CONNECTING, currentConnect,
currentToken.

161-164: ⚠️ Potential issue | 🟠 Major

Reset the resume dedupe marker when a new hidden cycle starts.

Without clearing lastResumeCheckAtRef.current here, a focus shortly before a real hide/show cycle can suppress the next valid resume check and delay reconnect until the interval tick.

🔧 Minimal fix
     const handleVisibilityChange = () => {
       if (document.visibilityState === "hidden") {
+        lastResumeCheckAtRef.current = 0;
         hiddenAtRef.current = Date.now();
         return;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/websocket/useWebSocketHealth.ts` around lines 161 - 164, In
handleVisibilityChange, when document.visibilityState === "hidden" reset the
resume-dedupe marker by clearing lastResumeCheckAtRef.current (in addition to
setting hiddenAtRef.current = Date.now()) so a new hidden/shown cycle won't be
suppressed by a prior lastResumeCheckAtRef value; update the function that
manages visibility events (handleVisibilityChange) to set
lastResumeCheckAtRef.current = 0 or null immediately when starting a hidden
cycle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@services/websocket/useWebSocketHealth.ts`:
- Around line 145-149: The forced-resume branch only calls currentConnect when
webSocketStateRef.current.status equals WebSocketStatus.CONNECTED, which skips
recovery if the socket is stuck in WebSocketStatus.CONNECTING; update the
condition in the forced-resume path to also treat WebSocketStatus.CONNECTING as
a case that should call currentConnect(currentToken) (i.e., when
webSocketStateRef.current.status === WebSocketStatus.CONNECTED ||
webSocketStateRef.current.status === WebSocketStatus.CONNECTING) so both
CONNECTED and CONNECTING sockets are re-attached; references: webSocketStateRef,
WebSocketStatus.CONNECTED, WebSocketStatus.CONNECTING, currentConnect,
currentToken.
- Around line 161-164: In handleVisibilityChange, when document.visibilityState
=== "hidden" reset the resume-dedupe marker by clearing
lastResumeCheckAtRef.current (in addition to setting hiddenAtRef.current =
Date.now()) so a new hidden/shown cycle won't be suppressed by a prior
lastResumeCheckAtRef value; update the function that manages visibility events
(handleVisibilityChange) to set lastResumeCheckAtRef.current = 0 or null
immediately when starting a hidden cycle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a5f7525-cc5a-4961-a065-91a5d49592ab

📥 Commits

Reviewing files that changed from the base of the PR and between ca9f1a0 and da11c6b.

📒 Files selected for processing (2)
  • __tests__/services/websocket/useWebSocketHealth.test.ts
  • services/websocket/useWebSocketHealth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/services/websocket/useWebSocketHealth.test.ts

simo6529 added 2 commits April 9, 2026 09:12
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/websocket/WebSocketProvider.tsx (1)

152-196: ⚠️ Potential issue | 🟠 Major

Reconnect timer calls a stale connect closure when config.url changes.

attemptReconnect at Line 152 schedules a timeout that calls connect(reconnectTokenRef.current) at Line 194. However, connect is not listed in the dependency array at Line 196 ([config.maxReconnectAttempts, config.reconnectDelay]). Since connect depends on config.url (used at Line 234), when the URL changes, connect is recreated but attemptReconnect retains the old closure, causing scheduled retries to reconnect with outdated config.

Store connect in a ref and update it after each render to ensure scheduled callbacks use the latest version without creating circular hook dependencies.

💡 Proposed fix
   const reconnectAttemptsRef = useRef(0);
   const reconnectTimerRef = useRef<NodeJS.Timeout | null>(null);
   const isManualDisconnectRef = useRef(false);
   const reconnectTokenRef = useRef<string | undefined>(undefined);
+  const connectRef = useRef<(token?: string) => void>(() => {});

   const connect = useCallback(
     (token?: string) => {
       // ... existing implementation ...
     },
     [config.url, handleMessage, clearReconnectTimer, attemptReconnect]
   );
+
+  connectRef.current = connect;

     reconnectTimerRef.current = setTimeout(() => {
       reconnectAttemptsRef.current += 1;
       logWebSocketDebug("Running scheduled reconnect attempt", {
         attempt: reconnectAttemptsRef.current,
         hasToken: Boolean(reconnectTokenRef.current),
       });
-      connect(reconnectTokenRef.current);
+      connectRef.current(reconnectTokenRef.current);
     }, delay);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/websocket/WebSocketProvider.tsx` around lines 152 - 196, The
scheduled reconnect uses a stale connect closure because attemptReconnect
doesn't capture updates to connect when config.url changes; fix this by creating
a ref (e.g., connectRef) that is assigned connect on every render and updating
attemptReconnect to call connectRef.current(reconnectTokenRef.current) inside
the setTimeout callback, ensuring reconnectTimerRef and reconnectAttemptsRef
behavior is unchanged; update any hook dependencies accordingly so
attemptReconnect only depends on config.maxReconnectAttempts and
config.reconnectDelay (not connect) and ensure the ref is kept in sync after
each render.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/services/websocket/useWebSocketHealth.test.ts`:
- Around line 72-77: The test helper setDocumentVisibilityState currently
overrides document.visibilityState via Object.defineProperty but never restores
the original descriptor; update the test teardown (the afterAll block) to remove
or restore that override by calling Object.defineProperty(document,
"visibilityState", originalDescriptor) or delete the property so the jsdom
global isn't mutated, and capture the original descriptor before
setDocumentVisibilityState mutates it (store it in a variable near
setDocumentVisibilityState) so afterAll can reassign it back or delete the
custom descriptor to avoid leaking into other suites.

---

Outside diff comments:
In `@services/websocket/WebSocketProvider.tsx`:
- Around line 152-196: The scheduled reconnect uses a stale connect closure
because attemptReconnect doesn't capture updates to connect when config.url
changes; fix this by creating a ref (e.g., connectRef) that is assigned connect
on every render and updating attemptReconnect to call
connectRef.current(reconnectTokenRef.current) inside the setTimeout callback,
ensuring reconnectTimerRef and reconnectAttemptsRef behavior is unchanged;
update any hook dependencies accordingly so attemptReconnect only depends on
config.maxReconnectAttempts and config.reconnectDelay (not connect) and ensure
the ref is kept in sync after each render.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34ee60cf-aa89-4550-b8f5-86e658149dfe

📥 Commits

Reviewing files that changed from the base of the PR and between da11c6b and 4f6cbbf.

📒 Files selected for processing (5)
  • __tests__/services/websocket/useWebSocketHealth.test.ts
  • contexts/wave/MyStreamContext.tsx
  • services/websocket/WebSocketProvider.tsx
  • services/websocket/useWebSocketHealth.ts
  • services/websocket/webSocketDebug.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • contexts/wave/MyStreamContext.tsx
  • services/websocket/useWebSocketHealth.ts

Comment thread __tests__/services/websocket/useWebSocketHealth.test.ts
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
services/websocket/useWebSocketHealth.ts (1)

269-275: ⚠️ Potential issue | 🟠 Major

Reset the resume dedupe marker when the tab becomes hidden.

This earlier issue still looks unresolved. If a focus event lands shortly before a real hide/show cycle, lastResumeCheckAtRef.current can still be inside the 1s dedupe window, so the next visible event is discarded, hiddenAtRef gets cleared in performResumeHealthCheck, and the long-hidden reconnect path is skipped until the interval check runs. Reset the dedupe timestamp in the hidden branch.

🔧 Minimal fix
     const handleVisibilityChange = () => {
       if (document.visibilityState === "hidden") {
+        lastResumeCheckAtRef.current = 0;
         hiddenAtRef.current = Date.now();
         logWebSocketDebug("Document became hidden", {
           hiddenAt: hiddenAtRef.current,
         });
         return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/websocket/useWebSocketHealth.ts` around lines 269 - 275, In
handleVisibilityChange's "hidden" branch, clear the resume-dedupe marker so a
subsequent visible event won't be dropped; specifically reset
lastResumeCheckAtRef.current (e.g., set to 0 or null) when you set
hiddenAtRef.current and log the hide, so performResumeHealthCheck / the next
visibility "visible" will not be suppressed by the 1s dedupe window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/websocket/useWebSocketHealth.ts`:
- Line 52: Replace the nullish coalescing with logical OR in the health check so
deleted-cookie hits aren't ignored when matchChanged is explicitly false: update
the return expression in useWebSocketHealth (the line returning
Boolean(matchChanged ?? matchDeleted)) to use Boolean(matchChanged ||
matchDeleted) so that either matchChanged or matchDeleted being true will
trigger the disconnection logic.

---

Duplicate comments:
In `@services/websocket/useWebSocketHealth.ts`:
- Around line 269-275: In handleVisibilityChange's "hidden" branch, clear the
resume-dedupe marker so a subsequent visible event won't be dropped;
specifically reset lastResumeCheckAtRef.current (e.g., set to 0 or null) when
you set hiddenAtRef.current and log the hide, so performResumeHealthCheck / the
next visibility "visible" will not be suppressed by the 1s dedupe window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c07583c9-17b0-4c38-a0b0-c329d890f0d1

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6cbbf and 4ef5d44.

📒 Files selected for processing (1)
  • services/websocket/useWebSocketHealth.ts

Comment thread services/websocket/useWebSocketHealth.ts Outdated
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 9, 2026

@simo6529 simo6529 merged commit 1a15dcb into main Apr 9, 2026
8 checks passed
@simo6529 simo6529 deleted the update-on-tab-change branch April 9, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants