Skip to content

fix: cancel in-flight connect on disconnect to prevent second-attempt bug#182

Open
staging-devin-ai-integration[bot] wants to merge 3 commits intodevin/1774202776-declarative-client-sectionfrom
devin/1774210453-fix-connect-lifecycle
Open

fix: cancel in-flight connect on disconnect to prevent second-attempt bug#182
staging-devin-ai-integration[bot] wants to merge 3 commits intodevin/1774202776-declarative-client-sectionfrom
devin/1774210453-fix-connect-lifecycle

Conversation

@staging-devin-ai-integration
Copy link
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot commented Mar 22, 2026

Summary

Fixes a race condition where disconnect() during an in-flight performConnect() could not cancel it, causing the "start a pipeline → destroy → start a new one" flow to break reliably.

Root cause: performConnect() creates MoQ resources (Camera, Connection, etc.) in local variables that aren't stored in the Zustand store until the final success set(). When disconnect() runs mid-connect, cleanupConnectAttempt() only sees null refs in the store — the in-flight objects are never cleaned up. The stale performConnect₁ then overwrites the store when it eventually completes or times out, clobbering performConnect₂'s state.

Fix: Add an AbortController to the store. disconnect() and connect() abort any in-flight attempt. AbortSignal is threaded through every async wait (waitForSignalValue, waitForBroadcastAnnouncement, setupMediaSources, setupPublishPath). An aborted performConnect cleans up its local resources and silently returns without writing to the store.

UX improvements:

  • Added connectingStep state ('devices''relay''pipeline') shown during the connecting phase so users see what's happening instead of a generic "Connecting…"
  • Auto-connect errors after session creation are now surfaced to the user instead of being silently logged

Refactors:

  • Extracted createConnectionAndHealth() and connectWatchPath() helpers from performConnect to stay under the max-statements lint threshold

Review & Testing Checklist for Human

  • Critical: Test the second-attempt flow — Create a pipeline session, let it connect, destroy it, then immediately create a new one. The new session should connect successfully without getting stuck.
  • Test rapid disconnect/reconnect — While "Connecting…" is showing, click disconnect, then immediately create a new session and connect. Should not hang or show stale state.
  • Verify connecting step labels — During connect, the status line should show phases like "Connecting — Requesting device access", "Connecting — Connecting to relay", etc.
  • Verify error surfacing — If the MoQ gateway is unreachable, the error should be shown to the user after session creation (not silently swallowed).

Notes

  • All 303 existing UI tests pass, plus 7 new tests for abort signal cancellation and disconnect-during-connect behavior.
  • Lint passes clean (0 errors, only 1 pre-existing warning in an unrelated file).
  • The AbortSignal integration uses DOMException('Aborted', 'AbortError') which is the standard Web API pattern.

Link to Devin session: https://staging.itsdev.in/sessions/8352254860d447efb51e249658d0eaf0
Requested by: @streamer45


Staging: Open in Devin

streamkit-devin and others added 2 commits March 22, 2026 20:14
… bug

The core issue: disconnect() during an in-flight performConnect() could not
cancel it. The local 'attempt' objects (Camera, Connection, etc.) lived in
performConnect's stack frame and were never cleaned up by disconnect()'s
cleanupConnectAttempt() call (which only saw the store's null refs).

When the user started a new pipeline, performConnect₁ would eventually
complete and overwrite the store — clobbering performConnect₂'s state,
leaking resources, or flipping the UI back to 'disconnected'.

Changes:
- Add AbortController to stream store; abort on disconnect() and connect()
- Thread AbortSignal through waitForSignalValue, waitForBroadcastAnnouncement,
  setupMediaSources, setupPublishPath, and performConnect
- Aborted performConnect silently discards its attempt (no store writes)
- Add connectingStep state for granular UX feedback during connect phases
  (devices → relay → pipeline)
- Surface auto-connect errors in StreamView instead of silently logging
- Extract createConnectionAndHealth() and connectWatchPath() helpers to
  keep performConnect under the max-statements lint threshold
- Add tests for abort signal cancellation in waitForSignalValue and
  disconnect-during-connect in streamStore

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 3 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

…ort in waitForBroadcastAnnouncement

- Add connectAbort and connectingStep to the beforeEach setState reset
  to prevent test pollution across runs.
- Replace polling abort check in waitForBroadcastAnnouncement with an
  event-driven abortPromise in Promise.race so abort fires immediately
  even when announcements.next() is blocked.

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Contributor Author

E2E Test Results — Connect Lifecycle Fixes

Ran Puppeteer headless Chrome (--use-fake-device-for-media-stream) against local dev environment (skit backend :4545, Vite UI :3045). Template: "Webcam Circle PiP (MoQ Stream)".

Test Results (5/5 passed)
  • TC1: Connecting step feedback shows granular phases — passed. MutationObserver captured "Connecting — Requesting device access • Watch: loading… • Mic: requesting permission… • Camera: requesting permission…". The em-dash + step text are unique to the new code.
  • TC2a: Disconnect returns to disconnected state — passed. "Connect & Stream" button reappeared and was enabled after clicking Disconnect.
  • TC2b: Second connect attempt works (not stuck) — passed. Fresh "Connecting — Requesting device access" captured on reconnect, reached Connected state. This was the critical bug fix — previously the stale first performConnect would clobber the second attempt's state.
  • TC3a: Destroy session returns to initial state — passed. Confirm dialog worked, UI returned to initial state with "Create Session" enabled.
  • TC3b: New session after destroy auto-connects successfully — passed. Auto-connect fired with fresh connecting step feedback, reached Relay: connected • Watch: live • Mic: ready • Camera: ready.
Status progression captured by MutationObserver (TC1)
1. STATUS:Disconnected
2. Connecting — Requesting device access • Watch: loading… • Mic: requesting permission… • Camera: requesting permission…
3. Relay: connected • Watch: loading… • Mic: requesting permission… • Camera: requesting permission…
4. STATUS:Connected
5. Relay: connected • Watch: live • Mic: ready • Camera: ready
Caveats
  • Fake media devices provide instant access, so only the "devices" connecting step was captured. "Connecting to relay" and "Waiting for pipeline" steps transition too fast with a local backend.
  • Not tested: real camera permission UX, WebTransport over real network, error recovery when backend is down.
Screenshots
Initial State Connected
Initial Connected
After Disconnect Second Connect Result
Disconnect Reconnect
Destroy Confirm After Destroy
Confirm Destroyed
New Session After Destroy
New Session

Devin session

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