Decompose webSessionSetup into reusable parts (Phase 3)#788
Conversation
ae4a718 to
9b8b335
Compare
da8c219 to
7db4e16
Compare
Extract setupWebRTCSession into common platform/VoiceSessionSetup.ts so React Native no longer calls the web-only webSessionSetup function. - Add setupWebRTCSession() to common code (platform-agnostic WebRTC session setup that extracts input/output from WebRTCConnection) - Replace web setupInputOutput with setupWebSocketIO (WebSocket-only) - Update internal.ts exports: remove webSessionSetup, add setupWebRTCSession + createConnection - Update React Native to use createConnection + setupWebRTCSession directly instead of webSessionSetup This fixes all TS6307 errors where internal.ts was dragging web files into the common tsconfig build. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7db4e16 to
abb1cd4
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR continues the platform-isolation work by decomposing the web-only webSessionSetup flow into reusable, platform-agnostic pieces so React Native can set up WebRTC voice sessions without importing any platform/web/* DOM-dependent modules.
Changes:
- Introduces a common
setupWebRTCSession()helper that extracts input/output controllers directly fromWebRTCConnection. - Refactors the web setup strategy to keep WebSocket media-device I/O web-only while delegating WebRTC setup to the common helper.
- Updates
@elevenlabs/client/internalexports and switches the React Native package tocreateConnection()+setupWebRTCSession().
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native/src/index.react-native.ts | Switch RN setup strategy from webSessionSetup to createConnection + setupWebRTCSession. |
| packages/client/src/platform/web/VoiceSessionSetup.ts | Splits WebSocket-only I/O setup from WebRTC setup; delegates WebRTC path to common helper. |
| packages/client/src/platform/VoiceSessionSetup.ts | Adds platform-agnostic setupWebRTCSession() helper. |
| packages/client/src/platform/VoiceSessionSetup.test.ts | Adds unit coverage for setupWebRTCSession() behavior and validation. |
| packages/client/src/internal.ts | Removes webSessionSetup export; adds setupWebRTCSession and createConnection exports. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -2,6 +2,7 @@ import type { Options } from "../../BaseConversation.js"; | |||
| import type { BaseConnection } from "../../utils/BaseConnection.js"; | |||
| throw new Error( | ||
| "setupWebRTCSession requires a WebRTCConnection. " + | ||
| `Received: ${connection.constructor.name}` |
There was a problem hiding this comment.
Fixed in b4f4512: changed to connection?.constructor?.name ?? typeof connection so null/undefined inputs produce "Received: object" or "Received: undefined" instead of throwing a TypeError. Added tests for both edge cases.
| const connection = await createConnection(options); | ||
| const result = attachNativeVolume(setupWebRTCSession(connection)); | ||
|
|
There was a problem hiding this comment.
Fixed in b4f4512: added an early guard that checks for connectionType: "websocket" or signedUrl and throws a clear RN-specific error: "WebSocket connections are not supported on React Native." Also updated the doc comment to document the limitation.
| import type { BaseConnection } from "../../utils/BaseConnection.js"; | ||
| import { | ||
| setSetupStrategy, | ||
| setupWebRTCSession, |
There was a problem hiding this comment.
Unused WebRTCConnection import after refactoring
Low Severity
WebRTCConnection is imported as a value on line 11 but is never referenced in the file after the instanceof WebRTCConnection check was moved into setupWebRTCSession. The project has @typescript-eslint/no-unused-vars disabled and noUnusedLocals is not set, so no tooling catches this. The type-only BaseConnection import on line 2 is also unused.
Reviewed by Cursor Bugbot for commit d51ad5f. Configure here.
There was a problem hiding this comment.
Fixed in Phase 4 (88eaf14): the web setup file was rewritten — WebRTCConnection is used again (for the instanceof check in the WebRTC branch) and BaseConnection is no longer imported.
- Use defensive access in setupWebRTCSession error message to handle null/undefined inputs without masking the validation error - Add early guard in React Native strategy for unsupported WebSocket connections with a clear, RN-specific error message - Update doc comment to reflect actual RN strategy behavior - Add tests for null/undefined edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b4f4512. Configure here.
| "Only WebRTC connections are available. " + | ||
| "Remove the connectionType/signedUrl option or use connectionType: 'webrtc'." | ||
| ); | ||
| } |
There was a problem hiding this comment.
React Native guard misses textOnly WebSocket path
Medium Severity
The early guard intended to catch unsupported WebSocket connections on React Native only checks for options.connectionType === "websocket" and options.signedUrl, but createConnection also infers "websocket" when textOnly: true is set (via determineConnectionType in ConnectionFactory.ts). Passing { agentId: "...", textOnly: true } bypasses the guard, starts the AudioSession, then fails inside setupWebRTCSession with a confusing "requires a WebRTCConnection" error instead of the friendly "WebSocket connections are not supported on React Native" message.
Reviewed by Cursor Bugbot for commit b4f4512. Configure here.
9f4b356
into
kh/client-dom-api-isolation
* Isolate DOM APIs from common client code Split tsconfig.build.json into tsconfig.common.json (no DOM lib) and tsconfig.web.json (with DOM), add runtime.d.ts for cross-platform globals, and introduce platform/web/ entry point. This is the landing pad for migrating DOM usage out of common code and eventually injecting React Native alternatives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Expand runtime.ts with cross-platform type declarations Rename runtime.d.ts → runtime.ts and add declarations for fetch, WebSocket, URL, encoding, and event types needed by tsconfig.common.json (lib: ["ES2022"], no DOM). Also adds assertRuntimeCompatibility() for early environment checks. Fixes window.btoa/window.atob → btoa/atob in audio.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move web-only files to platform/web/ (Phase 1) (#786) * Move web-only files to platform/web/ (Phase 1) Move files whose consumers are entirely within web code to src/platform/web/: compatibility.ts, addLibsamplerateModule.ts, input.ts (MediaDeviceInput), output.ts (MediaDeviceOutput). Split VoiceSessionSetup into common (types + injectable strategy variable) and web (implementation + side-effect registration). Extract shared types (InputConfig, InputEventTarget, OutputConfig, PlaybackEventTarget, etc.) into common InputController.ts and OutputController.ts so they remain accessible without DOM. Refactor applyDelay to accept a resolved delay value directly, removing the dependency on web-only compatibility.ts. Files still depending on common code (rawAudioProcessor, scribeAudioProcessor, createWorkletModuleLoader, createAnalyserVolumeProvider) stay in src/utils/ until their injection refactoring in Phases 5-6. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove unused type re-exports from web input/output Types are defined in common InputController.ts / OutputController.ts and imported from there by all consumers. The re-exports from the web files were dead code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix IIFE bundle entry point to include web strategy The IIFE build (for CDN/script-tag users) used src/index.ts as entry, which no longer imports the web session setup strategy after the platform split. Change entry to src/platform/web/index.ts which re-exports everything plus the side-effect registration. Also adds a TODO for the lost Android delay default in resolveDelay. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Call assertRuntimeCompatibility() in Conversation.startSession Validates that required cross-platform globals (fetch, WebSocket, TextEncoder, etc.) are available before starting a session, giving users a clear error message instead of cryptic failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Replace DOM event constructors with platform-agnostic types (Phase 2) (#787) * Replace DOM event constructors with platform-agnostic types - Rename runtime.d.ts → runtime.ts with expanded cross-platform global declarations (WebSocket, fetch, URL, encoding, events) and a runtime assertion function - Replace Event/CloseEvent constructors in DisconnectionDetails with plain DisconnectionContext objects ({ type, reason?, code? }) - Fix window.btoa/window.atob → btoa/atob in audio.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update changeset: remove runtime.ts reference (now in p1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add disconnection context tests for WebSocket, BaseConversation, and WebRTC Cover the new platform-agnostic DisconnectionContext shape across all disconnect paths: WebSocket close/error events, end_call and max_duration_exceeded in BaseConversation, and LiveKit room disconnect events in WebRTCConnection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Decompose webSessionSetup into reusable parts (Phase 3) (#788) * Decompose webSessionSetup into reusable parts (Phase 3) Extract setupWebRTCSession into common platform/VoiceSessionSetup.ts so React Native no longer calls the web-only webSessionSetup function. - Add setupWebRTCSession() to common code (platform-agnostic WebRTC session setup that extracts input/output from WebRTCConnection) - Replace web setupInputOutput with setupWebSocketIO (WebSocket-only) - Update internal.ts exports: remove webSessionSetup, add setupWebRTCSession + createConnection - Update React Native to use createConnection + setupWebRTCSession directly instead of webSessionSetup This fixes all TS6307 errors where internal.ts was dragging web files into the common tsconfig build. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add tests for setupWebRTCSession Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address PR review feedback - Use defensive access in setupWebRTCSession error message to handle null/undefined inputs without masking the validation error - Add early guard in React Native strategy for unsupported WebSocket connections with a clear, RN-specific error message - Update doc comment to reflect actual RN strategy behavior - Add tests for null/undefined edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Make VoiceConversation platform-agnostic (Phase 4) (#792) * Make VoiceConversation platform-agnostic (Phase 4) Move wake lock management, preliminary mic permission, platform-specific delay, and visibility change handling from VoiceConversation into the web setup strategy. VoiceConversation now has zero DOM references. The detach callback becomes async to support wake lock release on cleanup. Also restores the previously-lost Android platform delay by detecting the platform in the web strategy via compatibility.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review comments: cleanup ordering and error handling - VoiceConversation: detach IO before closing connection (cleanUp/close before super.handleEndSession) to avoid listeners firing during teardown - VoiceSessionSetup: close connection if IO setup throws to prevent leaking network resources - React Native: use try/finally so AudioSession.stopAudioSession() runs even if upstream detach fails; await the stop call - nativeVolume: make detach async and await originalDetach to honor the Promise<void> contract Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Inject platform audio via WebRTCAudioAdapter (Phase 5) (#794) * Inject platform audio behavior into WebRTCConnection via adapter (Phase 5) Introduce WebRTCAudioAdapter interface so WebRTCConnection delegates all DOM-dependent audio operations (AudioContext, HTMLAudioElement, AudioWorklet) to a platform-specific adapter instead of calling web APIs directly. - Add WebRTCAudioAdapter interface + module-level factory registration - Create WebAudioAdapter (web implementation) in platform/web/ - Move generated worklet files (raw/concat) to platform/web/ - Move createAnalyserVolumeProvider to platform/web/volumeProvider.ts - Add MediaStream/MediaStreamTrack declarations to runtime.ts - Register web adapter as side-effect in platform/web/index.ts - Address Phase 4 review comments: IO error cleanup, detach ordering, async detach in nativeVolume, connection leak fix in webSessionSetup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review comments: stale refs, debug event, dead code - Reset inputAnalyser/outputAnalyser/volumeProviders in close() after adapter cleanup so consumers get undefined/zero instead of stale refs - Move audio_element_ready debug event inside the adapter guard so it only fires when playback was actually set up - Remove write-only outputDeviceId field from WebAudioAdapter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Make Scribe module platform-agnostic (Phase 6) (#800) * Make Scribe module platform-agnostic (Phase 6) Move createWorkletModuleLoader and scribeAudioProcessor to platform/web/ where they belong (both use web-only APIs like Blob, URL.createObjectURL, and AudioWorklet). Extract microphone streaming from ScribeRealtime into an injectable ScribeMicrophoneSetup strategy, with the web implementation in platform/web/scribeMicrophone.ts. The scribe module now has zero DOM references — microphone mode uses the same dependency injection pattern as VoiceConversation and WebRTCAudioAdapter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Vendor ConstrainDOMString as MediaDeviceConstraint in runtime.ts Declare MediaDeviceConstraint (and MediaDeviceConstraintParameters) in runtime.ts as a semantic replacement for ConstrainDOMString. Uses a unique name to avoid conflicts with DOM lib's ConstrainDOMString when consumers compile with lib: ["DOM"]. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Fix remaining tsconfig.common.json type errors (Phase 7) (#801) * Fix remaining tsconfig.common.json type errors (Phase 7) Resolve the last 5 type errors in tsconfig.common.json (no DOM lib), completing the platform isolation effort: - Declare minimal AnalyserNode interface for deprecated getAnalyser() - Add type assertion for response.json() in BaseConversation and errors - Fix Promise<void> typing in applyDelay - Fix Function type lint error in WebRTCConnection.test.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add isJsonObject type guard for response.json() calls Replace unsafe type assertions on response.json() with a proper type guard that validates the value is a non-null, non-array object before accessing properties. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move isJsonObject to shared utils/assert.ts Consolidate the duplicate isPlainObject from mergeOptions.ts and isJsonObject from errors.ts into a single shared type guard in utils/assert.ts. Add assertJsonObject assertion function and use it in BaseConversation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Fix browser test import to use web platform entry point The test was importing from the common index.ts which no longer auto-registers the web setup strategy after the Phase 3-7 refactoring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Restore Android 3-second default delay when connectionDelay is undefined The refactoring of applyDelay into resolveDelay dropped the default DelayConfig that provided a 3-second delay on Android (for AudioManager mode switching). When connectionDelay was undefined, resolveDelay returned 0, removing the delay Android web users relied on. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Restore original shutdown ordering: close connection before input/output The refactoring moved super.handleEndSession() (which closes the connection) to after input.close()/output.close(). Restore the original order: detach listeners, close connection, then close input/output. This prevents the connection from delivering events to already-closing controllers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove dead WebRTC volume control tests These tests set audioElements on WebRTCConnection directly, but that field was moved to WebAudioAdapter during Phase 5. The tests were silently passing (wrapped in try/catch) with zero coverage. The volume-setting and cleanup behavior is trivially correct and best tested via browser integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Clean up VoiceSessionSetup: fix redundant spread and stale comment Restructure the web caller to build VoiceSessionSetupResult directly per branch — WebSocket constructs from parts, WebRTC gets the complete result from setupWebRTCSession. This avoids the redundant spread of connection from both the explicit property and the io object. Also update stale comment that referenced the old webSessionSetup name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix double audio connection in WebAudioAdapter.setupOutputAnalysis The audio graph had source connected to both analyser and worklet directly (source→analyser→worklet AND source→worklet), causing the worklet to sum two copies of the signal at double amplitude. Remove the duplicate connection and reorder so the message listener and format configuration are set up before connecting the graph. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Handle rejected promises from endSessionWithDetails Three call sites invoked the async endSessionWithDetails without awaiting or catching — rejections from handleEndSession were lost. Forward errors through onError callback. Also extract duplicated end_call DisconnectionDetails literal into a module-level constant. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove unnecessary async from RoomEvent.Connected handler The handler body is synchronous (just sets this.isConnected = true), so the async keyword is unnecessary and causes the event emitter to receive a promise it silently ignores. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move volumeThreshold to module-level constant Avoids recreating the constant on every audio callback invocation in the hot path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Simplify streamFromMicrophone config passing Pass options.microphone directly instead of copying each property individually — the types are identical and the callee only reads from the config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove dead-store inputAnalyser and outputAnalyser fields These fields were written but never read. The analyser nodes are already returned to the caller via the result objects; storing them on the instance only to null them during cleanup served no purpose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use element.remove() instead of parentNode.removeChild Simpler modern DOM API — remove() is a no-op when the element isn't attached, so the parentNode null check is unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Merge InputAnalysisResult and OutputAnalysisResult into AnalysisResult The two types had identical shapes. The method names on WebRTCAudioAdapter already distinguish input vs output context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace forEach with for...of on media stream tracks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Extract closeCode/closeReason variables in WebSocket close handler Removes triple-repeated event.code and event.reason || undefined expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use shared arrayBufferToBase64 in scribe microphone handler Replaces hand-rolled base64 encoding loop with the existing utility from utils/audio.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Void audioContext.close() in scribe microphone cleanup Explicitly marks the discarded promise to avoid silent unhandled rejections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Deduplicate getAudioTracks() call in scribe microphone setup Extract audioTrack once at the top instead of calling getAudioTracks() twice. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Consolidate duplicate re-exports from InputController and OutputController Merge separate export statements from the same module into single import blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix synchronous throw in startSession and AudioContext leak in setupOutputAnalysis Make startSession async so assertRuntimeCompatibility() errors are caught by .catch() callers. Add cleanup of previous audioCaptureContext in setupOutputAnalysis, matching the existing pattern in setupInputAnalysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add React Native DOM API leak changeset Co-authored-by: Kræn Hansen <mail@kraenhansen.dk> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>


Summary
setupWebRTCSession()into commonplatform/VoiceSessionSetup.ts— platform-agnostic WebRTC session setup that extracts input/output fromWebRTCConnectionsetupInputOutput(handled both connection types) withsetupWebSocketIO(WebSocket-only), delegating WebRTC to the common functioninternal.tsexports: removewebSessionSetup(which imported web-only code), addsetupWebRTCSession+createConnectioncreateConnection()+setupWebRTCSession()directly instead ofwebSessionSetup()This fixes all TS6307 errors where
internal.tswas draggingplatform/web/files into the common tsconfig build. React Native no longer transitively imports web-onlyMediaDeviceInput/MediaDeviceOutput.Stacked on #784 (Phase 2).
Breaking changes
webSessionSetupremoved from@elevenlabs/client/internalBefore:
After: Use
createConnection+setupWebRTCSessionfor WebRTC, or the registered platform strategy for full setup:Why:
webSessionSetupimported web-only code (MediaDeviceInput/Output, AudioContext), which pulled DOM dependencies into any consumer — including React Native. Splitting intocreateConnection(platform-agnostic) andsetupWebRTCSession(also platform-agnostic) lets non-web platforms set up voice sessions without importing web APIs.setupInputOutputremoved fromplatform/web/VoiceSessionSetup.tsexportsBefore:
After: This function is no longer exported. WebRTC setup is handled by
setupWebRTCSessionfromplatform/VoiceSessionSetup.ts. WebSocket I/O setup is internal to the web strategy.Why:
setupInputOutputmixed WebRTC and WebSocket concerns in a single function, making it impossible to use either path independently. WebRTC setup is now platform-agnostic, and WebSocket setup is an internal detail of the web platform strategy.Changes
packages/client/src/internal.ts— RemovewebSessionSetupexport, addsetupWebRTCSessionandcreateConnectionexportspackages/client/src/platform/VoiceSessionSetup.ts— AddsetupWebRTCSession()function for platform-agnostic WebRTC session setuppackages/client/src/platform/web/VoiceSessionSetup.ts— Replace exportedsetupInputOutputwith privatesetupWebSocketIO;webSessionSetupnow delegates WebRTC tosetupWebRTCSessionpackages/react-native/src/index.react-native.ts— UsecreateConnection()+setupWebRTCSession()instead ofwebSessionSetup()Test plan
tsc --noEmit🤖 Generated with Claude Code
Note
Medium Risk
Medium risk due to a breaking change in
@elevenlabs/client/internalexports and refactoring of session setup branching across web and React Native, which could affect voice initialization paths if edge cases were missed.Overview
WebRTC session setup is extracted into a reusable, platform-agnostic helper.
platform/VoiceSessionSetup.tsnow exposessetupWebRTCSession()(with type-checking and clearer errors) and adds unit tests for expected/invalid inputs.Web and React Native setup paths are refactored to use the new split. Web
webSessionSetupnow only builds MediaDevice I/O forWebSocketConnectionand delegates WebRTC tosetupWebRTCSession, while React Native stops importing the web strategy, explicitly creates a connection viacreateConnection, usessetupWebRTCSession, and throws early if WebSocket/signedUrl options are provided.internal.tsupdates exports to dropwebSessionSetupand instead exportsetupWebRTCSessionpluscreateConnection.Reviewed by Cursor Bugbot for commit b4f4512. Bugbot is set up for automated code reviews on this repo. Configure here.