@@ -33,14 +33,16 @@ export interface TranscriptionDispatcher extends WorkerEntrypoint<Env> {
3333 * Transcription message from container
3434 */
3535interface TranscriptionMessage {
36- type : 'transcription' | 'interim_transcription' ;
36+ type : 'transcription-result' ;
37+ is_interim : boolean ;
3738 participant : {
3839 id ?: string ;
3940 } ;
4041 transcript : Array < {
4142 text : string ;
4243 } > ;
4344 timestamp : number ;
45+ language ?: string ;
4446}
4547
4648/**
@@ -168,6 +170,112 @@ async function selectContainerInstance(request: Request, env: Env): Promise<stri
168170 }
169171}
170172
173+ /**
174+ * Handle WebSocket connection with dispatcher interception.
175+ * Creates a proxy between client and container, dispatching transcriptions asynchronously.
176+ */
177+ async function handleWebSocketWithDispatcher (
178+ request : Request ,
179+ container : ReturnType < typeof getContainer > ,
180+ env : Env ,
181+ ctx : ExecutionContext ,
182+ sessionId : string ,
183+ ) : Promise < Response > {
184+ // Create WebSocket pair for the client
185+ const clientPair = new WebSocketPair ( ) ;
186+ const [ clientWs , serverWs ] = Object . values ( clientPair ) ;
187+
188+ // Accept the server side of the client connection
189+ serverWs . accept ( ) ;
190+
191+ // Forward the upgrade request to the container
192+ const containerResponse = await container . fetch ( request ) ;
193+
194+ if ( containerResponse . status !== 101 || ! containerResponse . webSocket ) {
195+ // Container didn't upgrade - return error to client
196+ serverWs . close ( 1011 , 'Container failed to upgrade WebSocket' ) ;
197+ return containerResponse ;
198+ }
199+
200+ const containerWs = containerResponse . webSocket ;
201+ containerWs . accept ( ) ;
202+
203+ const dispatcher = env . TRANSCRIPTION_DISPATCHER ! ;
204+
205+ // Pipe: client → container (upstream, no interception needed)
206+ serverWs . addEventListener ( 'message' , ( event ) => {
207+ if ( containerWs . readyState === WebSocket . READY_STATE_OPEN ) {
208+ containerWs . send ( event . data ) ;
209+ }
210+ } ) ;
211+
212+ // Pipe: container → client (downstream, intercept for dispatcher)
213+ containerWs . addEventListener ( 'message' , ( event ) => {
214+ // Forward to client immediately
215+ if ( serverWs . readyState === WebSocket . READY_STATE_OPEN ) {
216+ serverWs . send ( event . data ) ;
217+ }
218+
219+ // Dispatch transcriptions asynchronously (non-blocking)
220+ if ( typeof event . data === 'string' ) {
221+ try {
222+ const data = JSON . parse ( event . data ) as TranscriptionMessage ;
223+ if ( data . type === 'transcription-result' && ! data . is_interim ) {
224+ const dispatcherMessage : DispatcherTranscriptionMessage = {
225+ sessionId,
226+ endpointId : data . participant ?. id || 'unknown' ,
227+ text : data . transcript . map ( ( t ) => t . text ) . join ( ' ' ) ,
228+ timestamp : data . timestamp ,
229+ language : data . language ,
230+ } ;
231+
232+ // Fire and forget - don't block the client
233+ dispatcher
234+ . dispatch ( dispatcherMessage )
235+ . then ( ( response ) => {
236+ if ( ! response . success || response . errors ) {
237+ console . error ( 'Dispatcher error:' , {
238+ message : response . message ,
239+ errors : response . errors ,
240+ } ) ;
241+ }
242+ } )
243+ . catch ( ( error ) => {
244+ const msg = error instanceof Error ? error . message : String ( error ) ;
245+ console . error ( 'Dispatcher RPC failed:' , msg ) ;
246+ } ) ;
247+ }
248+ } catch {
249+ // Not JSON or parse error - ignore, still forwarded to client
250+ }
251+ }
252+ } ) ;
253+
254+ // Handle close events
255+ serverWs . addEventListener ( 'close' , ( event ) => {
256+ containerWs . close ( event . code , event . reason ) ;
257+ } ) ;
258+
259+ containerWs . addEventListener ( 'close' , ( event ) => {
260+ serverWs . close ( event . code , event . reason ) ;
261+ } ) ;
262+
263+ // Handle errors
264+ serverWs . addEventListener ( 'error' , ( ) => {
265+ containerWs . close ( 1011 , 'Client WebSocket error' ) ;
266+ } ) ;
267+
268+ containerWs . addEventListener ( 'error' , ( ) => {
269+ serverWs . close ( 1011 , 'Container WebSocket error' ) ;
270+ } ) ;
271+
272+ // Return the client WebSocket
273+ return new Response ( null , {
274+ status : 101 ,
275+ webSocket : clientWs ,
276+ } ) ;
277+ }
278+
171279export default {
172280 async fetch ( request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > {
173281 const url = new URL ( request . url ) ;
@@ -185,7 +293,11 @@ export default {
185293 }
186294 }
187295
188- const useDispatcher = url . searchParams . get ( 'useDispatcher' ) === 'true' ;
296+ // Check query param first, fall back to env var
297+ const useDispatcherParam = url . searchParams . get ( 'useDispatcher' ) ;
298+ const useDispatcher = useDispatcherParam !== null
299+ ? useDispatcherParam === 'true'
300+ : env . USE_DISPATCHER === 'true' ;
189301 const sessionId = url . searchParams . get ( 'sessionId' ) || 'unknown' ;
190302
191303 // Select which container instance to use based on routing strategy
@@ -194,13 +306,9 @@ export default {
194306 // Get the container instance
195307 const container = getContainer ( env . TRANSCRIBER , containerInstanceId ) ;
196308
197- // Start the container and wait for ports to be ready
198- // This is required for the fetch to work properly
199- await container . startAndWaitForPorts ( ) ;
200-
201- // For now, just forward all requests directly to the container
202- // TODO: Implement WebSocket interception for dispatcher fan-out
203- // This requires more complex WebSocket handling with Cloudflare Containers
309+ // Start the container and wait for ports to be ready
310+ // This is required for the fetch to work properly
311+ await container . startAndWaitForPorts ( ) ;
204312
205313 // Report connection tracking for autoscale mode
206314 const routingMode = env . ROUTING_MODE || 'session' ;
@@ -220,13 +328,14 @@ export default {
220328 console . error ( 'Failed to report connection opened:' , error ) ;
221329 } ) ,
222330 ) ;
331+ }
223332
224- // Note: We can't easily detect when WebSocket closes from here
225- // The container would need to report back, or we'd need WebSocket interception
226- // For now, rely on scale-down idle timeout
333+ // If dispatcher is enabled and this is a WebSocket upgrade, intercept the connection
334+ if ( useDispatcher && upgradeHeader === 'websocket' && env . TRANSCRIPTION_DISPATCHER ) {
335+ return handleWebSocketWithDispatcher ( request , container , env , ctx , sessionId ) ;
227336 }
228337
229- // Forward request directly to container
338+ // Forward request directly to container (pass-through mode)
230339 return container . fetch ( request ) ;
231340
232341 } ,
0 commit comments