Conversation
…overlay When iOS Safari backgrounds the app, the OS silently kills the WebSocket connection. Previously the UI stayed fully interactive while changes silently failed to sync. This adds real-time WebSocket connection state tracking via graphql-ws on.connected/on.closed callbacks, a visibilitychange handler to proactively detect dead sockets on iOS foreground return, and a "Reconnecting..." overlay on the queue control bar that blocks interaction until the connection recovers. https://claude.ai/code/session_016bdrJ2mAmjzSmZ2z6vWGN9
The on.connected callback was setting isWebSocketConnected=true immediately, before handleReconnect could finish its async joinSession/delta-sync path. This caused isReconnecting to become false prematurely, re-enabling queue mutations while stale state was still being replayed. Fix: onConnectionStateChange now receives an isReconnect flag. On reconnections, the persistent session defers setting isWebSocketConnected=true until handleReconnect's finally block, keeping the reconnecting overlay and viewOnlyMode active throughout the entire resync. Also adds iOS background connection loss documentation to docs/websocket-implementation.md (section 7 under Failure States). https://claude.ai/code/session_016bdrJ2mAmjzSmZ2z6vWGN9
- graphql-client.test.ts: Tests onConnectionStateChange callback fires with correct (connected, isReconnect) flags across initial connect, disconnect, and reconnection sequences. Verifies callback ordering (stateChange fires before onReconnect). - connection-state.test.ts: Tests isReconnecting derivation truth table, reconnect lock timing (isReconnecting stays true during resync), and visibilitychange debounce behavior (300ms delay, rapid-fire cancellation, hidden state ignored). https://claude.ai/code/session_016bdrJ2mAmjzSmZ2z6vWGN9
The full-page overlay blocked all user interaction. Replace it with a bottom-center Snackbar/Alert that shows "Reconnecting..." with a spinner and automatically disappears when the connection is re-established. Queue mutations are still prevented by viewOnlyMode during reconnection. https://claude.ai/code/session_016bdrJ2mAmjzSmZ2z6vWGN9
Break the BoardSessionBridge reactivation loop that caused infinite connect/disconnect cycles after join failures. Track failed session IDs so the bridge doesn't re-activate a session that was just cleared. Also harden getSessionMembers() against partial Redis data (missing username would violate String! schema constraint) and add production-safe logging to the joinSession resolver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "detect failed session" effect in BoardSessionBridge was marking sessions as failed on initial mount when activeSession is null (not yet set), preventing the activation effect from ever running. Now only marks a session as failed after it was previously active and then cleared. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The EVENTS_REPLAY query was using GraphQL field aliases (addedItem: item, currentItem: item), so the response already had aliased field names. But transformToSubscriptionEvent was reading event.item (the un-aliased name), which was undefined at runtime. Remove the aliases so the response matches the QueueEvent type, and the transform function correctly remaps the fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nnection Extract the 400-line connection useEffect from persistent-session-context.tsx into two testable, non-React classes: - ConnectionStateMachine: explicit FSM replacing 6+ scattered boolean state variables, eliminating inconsistent state combinations - SessionConnection: encapsulates client lifecycle, session join/leave, subscription setup, reconnection with delta sync, and transient retry logic This reduces persistent-session-context.tsx from 1418 to ~990 lines and makes the connection orchestration independently testable. https://claude.ai/code/session_01TimjGNkUn4VMvRCK4kP76s
Generated patch from the WebSocket ConnectionStateMachine + SessionConnection refactor since git push is returning 403. https://claude.ai/code/session_01TimjGNkUn4VMvRCK4kP76s
- Remove websocket-refactor.patch (77KB development artifact) - Add resyncInProgress guard to prevent concurrent resyncs when triggerResync() is called rapidly (e.g., visibility + corruption) - Reset state to IDLE before scheduleRetry() so the retry timer's connect() call isn't blocked by the duplicate-connection guard - Gate CONNECTED transition on successful joinSession in handleReconnect() so transient reconnect failures don't falsely mark the session as connected https://claude.ai/code/session_01FRKMbaqzmvrCb5CKHnGmBN
Claude Review✅ Ready to merge - Minor issues noted below, but nothing blocking. This is a well-structured refactor that extracts connection logic into a testable Minor Issues
DocumentationThe |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eacdf1a372
ℹ️ 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".
| if (this.disposed || !sessionData) { | ||
| // joinSession failed — don't transition to CONNECTED | ||
| if (DEBUG) console.log('[SessionConnection] Reconnect joinSession failed, staying in current state'); | ||
| return; |
There was a problem hiding this comment.
Transition out of RECONNECTING when rejoin fails
When joinSession() returns null during handleReconnect, this branch returns without any state transition or session cleanup, so the connection can stay stuck in RECONNECTING indefinitely after a failed rejoin (for example, if the session was ended while the client was offline). In this commit, isReconnecting is now used to force view-only mode, so this leaves users permanently locked out of queue mutations until a full page reload or manual URL/state reset.
Useful? React with 👍 / 👎.
| if (!activeSession && sessionIdFromUrl && hadActiveSessionRef.current) { | ||
| failedSessionIdsRef.current.add(sessionIdFromUrl); |
There was a problem hiding this comment.
Allow retrying same session ID after transient failures
This adds the current sessionIdFromUrl to a permanent in-memory failed set whenever activeSession is cleared, and activation is then unconditionally skipped for that same URL param. Because entries are only removed when the URL session param changes, a temporary backend outage that clears activeSession makes the same invite URL unrecoverable in-tab even after the backend is healthy again.
Useful? React with 👍 / 👎.
- Replace permanent failed session set with time-based cooldown (30s) so transient backend outages become retryable without page reload - Transition to FAILED and clear session when handleReconnect's joinSession() returns null, preventing stuck RECONNECTING state - Fix username initialized to '' instead of undefined to avoid backend treating empty string differently from missing value - Improve eslint-disable comment documenting why handleQueueEvent and handleSessionEvent are intentionally omitted from deps https://claude.ai/code/session_01FRKMbaqzmvrCb5CKHnGmBN
Claude Review
Issues
DocumentationDocumentation appropriately updated - new section added to |
No description provided.