@@ -303,16 +303,43 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
303303 return ev . message . case == 'disconnect' && ev . message . value . asyncId == res . asyncId ;
304304 } ) ;
305305
306+ this . cleanupOnDisconnect ( ) ;
307+
308+ FfiClient . instance . removeListener ( FfiClientEvent . FfiEvent , this . onFfiEvent ) ;
309+ this . removeAllListeners ( ) ;
310+ }
311+
312+ private cleanupOnDisconnect ( ) {
313+ // Error all in-progress stream controllers to prevent FD leaks.
314+ // Streams that were receiving data but never got a trailer (e.g. the sender
315+ // disconnected mid-transfer) would otherwise keep their ReadableStream open
316+ // indefinitely, leaking the underlying controller and any buffered chunks.
317+ // Using error() instead of close() signals an abnormal termination to consumers.
318+ for ( const [ , streamController ] of this . byteStreamControllers ) {
319+ try {
320+ streamController . controller . error ( new Error ( 'Disconnected while receiving' ) ) ;
321+ } catch {
322+ // controller may already be closed or errored
323+ }
324+ }
325+ this . byteStreamControllers . clear ( ) ;
326+
327+ for ( const [ , streamController ] of this . textStreamControllers ) {
328+ try {
329+ streamController . controller . error ( new Error ( 'Disconnected while receiving' ) ) ;
330+ } catch {
331+ // controller may already be closed or errored
332+ }
333+ }
334+ this . textStreamControllers . clear ( ) ;
335+
306336 // Clear sidPromise before removing listeners so that a reconnect
307337 // doesn't return a stale, permanently-pending promise.
308338 this . sidPromise = undefined ;
309339 // Abort all pending FfiClient.waitFor() listeners so they don't leak.
310340 // This causes any in-flight operations (publishData, publishTrack, etc.)
311341 // to reject and clean up their event listeners.
312342 this . disconnectController . abort ( ) ;
313-
314- FfiClient . instance . removeListener ( FfiClientEvent . FfiEvent , this . onFfiEvent ) ;
315- this . removeAllListeners ( ) ;
316343 }
317344
318345 /**
@@ -630,9 +657,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
630657 /*} else if (ev.case == 'connected') {
631658 this.emit(RoomEvent.Connected);*/
632659 } else if ( ev . case == 'disconnected' ) {
633- // Abort pending waitFor() listeners on server-initiated disconnect too,
634- // not just on explicit disconnect() calls.
635- this . disconnectController . abort ( ) ;
660+ this . cleanupOnDisconnect ( ) ;
636661 this . emit ( RoomEvent . Disconnected , ev . value . reason ! ) ;
637662 } else if ( ev . case == 'reconnecting' ) {
638663 this . emit ( RoomEvent . Reconnecting ) ;
0 commit comments