@livekit/rtc-node 0.13.33 → 0.13.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{audio_stream-CnODFAmo.d.cts → audio_stream-BNVaONuH.d.cts} +36 -11
- package/dist/audio_stream.d.cts +2 -2
- package/dist/data_streams/index.d.cts +1 -1
- package/dist/data_streams/stream_reader.cjs +97 -58
- package/dist/data_streams/stream_reader.cjs.map +1 -1
- package/dist/data_streams/stream_reader.d.cts +1 -1
- package/dist/data_streams/stream_reader.d.ts +28 -11
- package/dist/data_streams/stream_reader.d.ts.map +1 -1
- package/dist/data_streams/stream_writer.d.cts +1 -1
- package/dist/data_streams/types.cjs.map +1 -1
- package/dist/data_streams/types.d.cts +1 -1
- package/dist/data_streams/types.d.ts +6 -0
- package/dist/data_streams/types.d.ts.map +1 -1
- package/dist/e2ee.cjs +5 -1
- package/dist/e2ee.cjs.map +1 -1
- package/dist/e2ee.d.cts +3 -1
- package/dist/e2ee.d.ts +3 -1
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/participant.cjs +178 -196
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -2
- package/dist/participant.d.ts +22 -5
- package/dist/participant.d.ts.map +1 -1
- package/dist/room.cjs +166 -126
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +3 -3
- package/dist/room.d.ts +16 -8
- package/dist/room.d.ts.map +1 -1
- package/dist/tests/e2e_common.cjs +157 -0
- package/dist/tests/e2e_common.cjs.map +1 -0
- package/dist/tests/e2e_common.d.cts +56 -0
- package/dist/tests/e2e_common.d.ts +37 -0
- package/dist/tests/e2e_common.d.ts.map +1 -0
- package/dist/track.d.cts +2 -2
- package/dist/track_publication.d.cts +2 -2
- package/dist/{types-_PdPVish.d.cts → types-f7wTlFjs.d.cts} +34 -11
- package/dist/utils.cjs +27 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +8 -1
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/video_stream.d.cts +2 -2
- package/package.json +2 -2
- package/src/audio_stream_room_lifecycle.test.ts +4 -1
- package/src/data_streams/stream_reader.ts +132 -82
- package/src/data_streams/types.ts +6 -0
- package/src/e2ee.ts +7 -0
- package/src/participant.ts +251 -228
- package/src/room.ts +233 -144
- package/src/tests/e2e.test.ts +50 -139
- package/src/tests/e2e_common.ts +163 -0
- package/src/tests/e2e_data_streams.test.ts +639 -0
- package/src/utils.ts +30 -0
- package/src/version.ts +1 -1
package/src/room.ts
CHANGED
|
@@ -9,12 +9,10 @@ import type {
|
|
|
9
9
|
GetSessionStatsResponse,
|
|
10
10
|
} from '@livekit/rtc-ffi-bindings';
|
|
11
11
|
import { DisconnectReason, type OwnedParticipant } from '@livekit/rtc-ffi-bindings';
|
|
12
|
-
import type
|
|
13
|
-
DataStream_Trailer,
|
|
14
|
-
DisconnectCallback,
|
|
15
|
-
TrackPublicationInfo,
|
|
16
|
-
} from '@livekit/rtc-ffi-bindings';
|
|
12
|
+
import { type DisconnectCallback, type TrackPublicationInfo } from '@livekit/rtc-ffi-bindings';
|
|
17
13
|
import {
|
|
14
|
+
ByteStreamReaderReadIncrementalRequest,
|
|
15
|
+
type ByteStreamReaderReadIncrementalResponse,
|
|
18
16
|
type ConnectCallback,
|
|
19
17
|
ConnectRequest,
|
|
20
18
|
type ConnectResponse,
|
|
@@ -22,30 +20,27 @@ import {
|
|
|
22
20
|
ConnectionState,
|
|
23
21
|
ContinualGatheringPolicy,
|
|
24
22
|
type DataPacketKind,
|
|
25
|
-
|
|
26
|
-
type DataStream_Header,
|
|
23
|
+
DataStream_Chunk,
|
|
27
24
|
type DisconnectResponse,
|
|
25
|
+
RoomDataStreamOptions as FfiRoomDataStreamOptions,
|
|
28
26
|
RoomOptions as FfiRoomOptions,
|
|
29
27
|
type IceServer,
|
|
30
28
|
IceTransportType,
|
|
29
|
+
type OwnedByteStreamReader,
|
|
30
|
+
type OwnedTextStreamReader,
|
|
31
31
|
type ReadyForRoomEventResponse,
|
|
32
|
-
RoomDataStreamOptions,
|
|
33
32
|
type RoomInfo,
|
|
34
33
|
type SimulateScenarioCallback,
|
|
35
34
|
type SimulateScenarioKind,
|
|
36
35
|
type SimulateScenarioResponse,
|
|
36
|
+
TextStreamReaderReadIncrementalRequest,
|
|
37
|
+
type TextStreamReaderReadIncrementalResponse,
|
|
37
38
|
} from '@livekit/rtc-ffi-bindings';
|
|
38
39
|
import { TrackKind } from '@livekit/rtc-ffi-bindings';
|
|
39
40
|
import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
|
|
40
41
|
import EventEmitter from 'events';
|
|
41
42
|
import { ByteStreamReader, TextStreamReader } from './data_streams/stream_reader.js';
|
|
42
|
-
import type
|
|
43
|
-
ByteStreamHandler,
|
|
44
|
-
ByteStreamInfo,
|
|
45
|
-
StreamController,
|
|
46
|
-
TextStreamHandler,
|
|
47
|
-
TextStreamInfo,
|
|
48
|
-
} from './data_streams/types.js';
|
|
43
|
+
import { type ByteStreamHandler, type TextStreamHandler } from './data_streams/types.js';
|
|
49
44
|
import type { E2EEOptions } from './e2ee.js';
|
|
50
45
|
import { E2EEManager, defaultE2EEOptions } from './e2ee.js';
|
|
51
46
|
import { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js';
|
|
@@ -57,9 +52,12 @@ import { RemoteAudioTrack, RemoteVideoTrack } from './track.js';
|
|
|
57
52
|
import type { LocalTrackPublication, TrackPublication } from './track_publication.js';
|
|
58
53
|
import { RemoteTrackPublication } from './track_publication.js';
|
|
59
54
|
import type { ChatMessage } from './types.js';
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
import {
|
|
56
|
+
bigIntToNumber,
|
|
57
|
+
byteStreamInfoFromProto,
|
|
58
|
+
numberToBigInt,
|
|
59
|
+
textStreamInfoFromProto,
|
|
60
|
+
} from './utils.js';
|
|
63
61
|
|
|
64
62
|
export interface RtcConfiguration {
|
|
65
63
|
iceTransportType: IceTransportType;
|
|
@@ -73,6 +71,15 @@ export const defaultRtcConfiguration: RtcConfiguration = {
|
|
|
73
71
|
iceServers: [],
|
|
74
72
|
};
|
|
75
73
|
|
|
74
|
+
export interface RoomDataStreamOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Maximum decompressed payload size in bytes accepted for a single incoming
|
|
77
|
+
* data stream. Incoming streams exceeding this limit terminate with an error
|
|
78
|
+
* on the receiving side. Unset falls back to the SDK default of 5gb.
|
|
79
|
+
*/
|
|
80
|
+
maxPayloadByteLength?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
export interface RoomOptions {
|
|
77
84
|
autoSubscribe: boolean;
|
|
78
85
|
dynacast: boolean;
|
|
@@ -82,6 +89,8 @@ export interface RoomOptions {
|
|
|
82
89
|
e2ee?: E2EEOptions;
|
|
83
90
|
encryption?: E2EEOptions;
|
|
84
91
|
rtcConfig?: RtcConfiguration;
|
|
92
|
+
/** Options controlling data stream behavior for this room. */
|
|
93
|
+
dataStream?: RoomDataStreamOptions;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
export const defaultRoomOptions = new FfiRoomOptions({
|
|
@@ -104,8 +113,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
104
113
|
*/
|
|
105
114
|
private ffiEventLock = new Mutex();
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
// Active incoming stream readers keyed by their FFI reader handle id. Each entry
|
|
117
|
+
// owns the FfiClient listener feeding reader events into the ReadableStream.
|
|
118
|
+
private streamReaders = new Map<
|
|
119
|
+
bigint,
|
|
120
|
+
{
|
|
121
|
+
controller: ReadableStreamDefaultController<DataStream_Chunk>;
|
|
122
|
+
listener: (ev: FfiEvent) => void;
|
|
123
|
+
}
|
|
124
|
+
>();
|
|
109
125
|
private byteStreamHandlers = new Map<string, ByteStreamHandler>();
|
|
110
126
|
private textStreamHandlers = new Map<string, TextStreamHandler>();
|
|
111
127
|
|
|
@@ -275,23 +291,39 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
275
291
|
* @throws ConnectError - if connection fails
|
|
276
292
|
*/
|
|
277
293
|
async connect(url: string, token: string, opts?: RoomOptions) {
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
options.dataStream = new RoomDataStreamOptions({
|
|
283
|
-
...options.dataStream,
|
|
284
|
-
useLegacyClientImplementation: true,
|
|
285
|
-
});
|
|
294
|
+
// dataStream is a plain user-facing object; convert it to its proto
|
|
295
|
+
// message separately instead of spreading it into the FFI options.
|
|
296
|
+
const { dataStream, ...restOpts } = opts ?? {};
|
|
297
|
+
const options = { ...defaultRoomOptions, ...restOpts };
|
|
286
298
|
const e2eeEnabled = options.encryption || options.e2ee;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
299
|
+
const userE2EEOptions = options.encryption ?? options.e2ee;
|
|
300
|
+
const e2eeOptions: E2EEOptions = {
|
|
301
|
+
...defaultE2EEOptions,
|
|
302
|
+
...userE2EEOptions,
|
|
303
|
+
keyProviderOptions: {
|
|
304
|
+
...defaultE2EEOptions.keyProviderOptions,
|
|
305
|
+
...userE2EEOptions?.keyProviderOptions,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const mergedOptions = { ...options };
|
|
310
|
+
|
|
311
|
+
if (options.encryption) {
|
|
312
|
+
mergedOptions.encryption = e2eeOptions;
|
|
313
|
+
} else if (options.e2ee) {
|
|
314
|
+
mergedOptions.e2ee = e2eeOptions;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (dataStream) {
|
|
318
|
+
mergedOptions.dataStream = new FfiRoomDataStreamOptions({
|
|
319
|
+
maxPayloadByteLength: numberToBigInt(dataStream.maxPayloadByteLength),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
290
322
|
|
|
291
323
|
const req = new ConnectRequest({
|
|
292
324
|
url: url,
|
|
293
325
|
token: token,
|
|
294
|
-
options,
|
|
326
|
+
options: mergedOptions,
|
|
295
327
|
});
|
|
296
328
|
|
|
297
329
|
FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onFfiEvent);
|
|
@@ -431,28 +463,20 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
431
463
|
if (this.hasCleanedUp) return;
|
|
432
464
|
this.hasCleanedUp = true;
|
|
433
465
|
|
|
434
|
-
// Error all in-progress stream
|
|
435
|
-
// Streams that were receiving data but never
|
|
436
|
-
// disconnected mid-transfer) would otherwise keep their ReadableStream
|
|
437
|
-
// indefinitely, leaking the underlying controller and any buffered chunks.
|
|
466
|
+
// Error all in-progress stream readers to prevent FD leaks.
|
|
467
|
+
// Streams that were receiving data but never reached end-of-stream (e.g. the
|
|
468
|
+
// sender disconnected mid-transfer) would otherwise keep their ReadableStream
|
|
469
|
+
// open indefinitely, leaking the underlying controller and any buffered chunks.
|
|
438
470
|
// Using error() instead of close() signals an abnormal termination to consumers.
|
|
439
|
-
for (const [,
|
|
471
|
+
for (const [, { controller, listener }] of this.streamReaders) {
|
|
472
|
+
FfiClient.instance.off(FfiClientEvent.FfiEvent, listener);
|
|
440
473
|
try {
|
|
441
|
-
|
|
474
|
+
controller.error(new Error('Disconnected while receiving'));
|
|
442
475
|
} catch {
|
|
443
476
|
// controller may already be closed or errored
|
|
444
477
|
}
|
|
445
478
|
}
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
for (const [, streamController] of this.textStreamControllers) {
|
|
449
|
-
try {
|
|
450
|
-
streamController.controller.error(new Error('Disconnected while receiving'));
|
|
451
|
-
} catch {
|
|
452
|
-
// controller may already be closed or errored
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
this.textStreamControllers.clear();
|
|
479
|
+
this.streamReaders.clear();
|
|
456
480
|
|
|
457
481
|
// Detach every track from this room so attached FrameProcessors receive
|
|
458
482
|
// onStreamInfoCleared/onCredentialsCleared and the tokenRefreshed listener
|
|
@@ -461,6 +485,11 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
461
485
|
// listener until GC. setRoom(null) is idempotent, so this is safe even if a
|
|
462
486
|
// track was already detached (e.g. via unsubscribe/unpublish).
|
|
463
487
|
if (this.localParticipant) {
|
|
488
|
+
// Data stream writers that were never closed (abandoned by the caller, or
|
|
489
|
+
// left errored by a failed write) keep their native writer alive until
|
|
490
|
+
// their handle is dropped.
|
|
491
|
+
this.localParticipant.disposeOpenStreamWriters();
|
|
492
|
+
|
|
464
493
|
for (const pub of this.localParticipant.trackPublications.values()) {
|
|
465
494
|
pub.track?.setRoom(null);
|
|
466
495
|
}
|
|
@@ -850,12 +879,10 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
850
879
|
this.emit(RoomEvent.Reconnected);
|
|
851
880
|
} else if (ev.case == 'roomSidChanged') {
|
|
852
881
|
this.emit(RoomEvent.RoomSidChanged, ev.value.sid!);
|
|
853
|
-
} else if (ev.case === '
|
|
854
|
-
this.
|
|
855
|
-
} else if (ev.case === '
|
|
856
|
-
this.
|
|
857
|
-
} else if (ev.case === 'streamTrailerReceived' && ev.value.trailer) {
|
|
858
|
-
this.handleStreamTrailer(ev.value.trailer);
|
|
882
|
+
} else if (ev.case === 'byteStreamOpened' && ev.value.reader) {
|
|
883
|
+
this.handleByteStreamOpened(ev.value.reader, ev.value.participantIdentity!);
|
|
884
|
+
} else if (ev.case === 'textStreamOpened' && ev.value.reader) {
|
|
885
|
+
this.handleTextStreamOpened(ev.value.reader, ev.value.participantIdentity!);
|
|
859
886
|
} else if (ev.case === 'roomUpdated') {
|
|
860
887
|
this.info = ev.value;
|
|
861
888
|
this.emit(RoomEvent.RoomUpdated);
|
|
@@ -940,104 +967,166 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
940
967
|
return participant;
|
|
941
968
|
}
|
|
942
969
|
|
|
943
|
-
private
|
|
944
|
-
|
|
945
|
-
|
|
970
|
+
private handleByteStreamOpened(ownedReader: OwnedByteStreamReader, participantIdentity: string) {
|
|
971
|
+
const info = byteStreamInfoFromProto(ownedReader.info!);
|
|
972
|
+
const readerHandle = ownedReader.handle!.id!;
|
|
946
973
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
startTime: Date.now(),
|
|
962
|
-
});
|
|
963
|
-
},
|
|
964
|
-
});
|
|
965
|
-
const info: ByteStreamInfo = {
|
|
966
|
-
streamId: streamHeader.streamId!,
|
|
967
|
-
name: streamHeader.contentHeader.value.name ?? 'unknown',
|
|
968
|
-
mimeType: streamHeader.mimeType!,
|
|
969
|
-
totalSize: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
|
|
970
|
-
topic: streamHeader.topic!,
|
|
971
|
-
timestamp: bigIntToNumber(streamHeader.timestamp!),
|
|
972
|
-
attributes: streamHeader.attributes,
|
|
973
|
-
};
|
|
974
|
-
streamHandlerCallback(
|
|
975
|
-
new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
|
|
976
|
-
{ identity: participantIdentity },
|
|
977
|
-
);
|
|
978
|
-
} else if (streamHeader.contentHeader.case === 'textHeader') {
|
|
979
|
-
const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic ?? '');
|
|
980
|
-
|
|
981
|
-
if (!streamHandlerCallback) {
|
|
982
|
-
log.debug(
|
|
983
|
-
'ignoring incoming text stream due to no handler for topic: %s',
|
|
984
|
-
streamHeader.topic ?? 'undefined',
|
|
985
|
-
);
|
|
974
|
+
const streamHandlerCallback = this.byteStreamHandlers.get(info.topic);
|
|
975
|
+
if (!streamHandlerCallback) {
|
|
976
|
+
log.debug('ignoring incoming byte stream due to no handler for topic: %s', info.topic);
|
|
977
|
+
// The native reader was never consumed — dispose it to free the handle
|
|
978
|
+
// and any chunks it has buffered.
|
|
979
|
+
new FfiHandle(readerHandle).dispose();
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Assigned by start(); unsubscribe() drops it once the stream is done with,
|
|
984
|
+
// whether that's end-of-stream or the consumer walking away early.
|
|
985
|
+
let listener: ((ev: FfiEvent) => void) | null = null;
|
|
986
|
+
const unsubscribe = () => {
|
|
987
|
+
if (!listener) {
|
|
986
988
|
return;
|
|
987
989
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
990
|
+
FfiClient.instance.off(FfiClientEvent.FfiEvent, listener);
|
|
991
|
+
listener = null;
|
|
992
|
+
this.streamReaders.delete(readerHandle);
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const stream = new ReadableStream<DataStream_Chunk>({
|
|
996
|
+
start: (controller) => {
|
|
997
|
+
listener = (ev: FfiEvent) => {
|
|
998
|
+
if (
|
|
999
|
+
ev.message.case !== 'byteStreamReaderEvent' ||
|
|
1000
|
+
ev.message.value.readerHandle !== readerHandle
|
|
1001
|
+
) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const detail = ev.message.value.detail;
|
|
1005
|
+
if (detail.case === 'chunkReceived') {
|
|
1006
|
+
const content = detail.value.content!;
|
|
1007
|
+
if (content.length > 0) {
|
|
1008
|
+
controller.enqueue(new DataStream_Chunk({ content }));
|
|
1009
|
+
}
|
|
1010
|
+
} else if (detail.case === 'eos') {
|
|
1011
|
+
unsubscribe();
|
|
1012
|
+
const error = detail.value.error;
|
|
1013
|
+
if (error) {
|
|
1014
|
+
// Abnormal termination (e.g. remote abort, payload over the
|
|
1015
|
+
// receiver's size limit): surface the error to the consumer
|
|
1016
|
+
// instead of presenting a truncated payload as a clean EOF.
|
|
1017
|
+
controller.error(new Error(error.description ?? 'stream terminated'));
|
|
1018
|
+
} else {
|
|
1019
|
+
controller.close();
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
FfiClient.instance.on(FfiClientEvent.FfiEvent, listener);
|
|
1024
|
+
this.streamReaders.set(readerHandle, { controller, listener });
|
|
1025
|
+
|
|
1026
|
+
// Start the incremental read; the native reader buffers chunks received
|
|
1027
|
+
// before this request, so nothing is lost. This consumes the FFI handle.
|
|
1028
|
+
FfiClient.instance.request<ByteStreamReaderReadIncrementalResponse>({
|
|
1029
|
+
message: {
|
|
1030
|
+
case: 'byteReadIncremental',
|
|
1031
|
+
value: new ByteStreamReaderReadIncrementalRequest({ readerHandle }),
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
},
|
|
1035
|
+
// The consumer stopped reading before end-of-stream — ByteStreamReader
|
|
1036
|
+
// .close(), or breaking out of a for-await. The eos event that would have
|
|
1037
|
+
// unsubscribed is never consumed, so the listener would otherwise be run
|
|
1038
|
+
// against every FFI event for the rest of the process. Only the
|
|
1039
|
+
// subscription is released: the reader handle was consumed by the
|
|
1040
|
+
// readIncremental request above, so there is nothing left to dispose.
|
|
1041
|
+
cancel: () => unsubscribe(),
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
streamHandlerCallback(
|
|
1045
|
+
new ByteStreamReader(info, stream, bigIntToNumber(ownedReader.info!.totalLength)),
|
|
1046
|
+
{ identity: participantIdentity },
|
|
1047
|
+
);
|
|
1012
1048
|
}
|
|
1013
1049
|
|
|
1014
|
-
private
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1050
|
+
private handleTextStreamOpened(ownedReader: OwnedTextStreamReader, participantIdentity: string) {
|
|
1051
|
+
const info = textStreamInfoFromProto(ownedReader.info!);
|
|
1052
|
+
const readerHandle = ownedReader.handle!.id!;
|
|
1053
|
+
|
|
1054
|
+
const streamHandlerCallback = this.textStreamHandlers.get(info.topic);
|
|
1055
|
+
if (!streamHandlerCallback) {
|
|
1056
|
+
log.debug('ignoring incoming text stream due to no handler for topic: %s', info.topic);
|
|
1057
|
+
// The native reader was never consumed — dispose it to free the handle
|
|
1058
|
+
// and any chunks it has buffered.
|
|
1059
|
+
new FfiHandle(readerHandle).dispose();
|
|
1060
|
+
return;
|
|
1020
1061
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1062
|
+
|
|
1063
|
+
const textEncoder = new TextEncoder();
|
|
1064
|
+
|
|
1065
|
+
// Assigned by start(); unsubscribe() drops it once the stream is done with,
|
|
1066
|
+
// whether that's end-of-stream or the consumer walking away early.
|
|
1067
|
+
let listener: ((ev: FfiEvent) => void) | null = null;
|
|
1068
|
+
const unsubscribe = () => {
|
|
1069
|
+
if (!listener) {
|
|
1070
|
+
return;
|
|
1025
1071
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1072
|
+
FfiClient.instance.off(FfiClientEvent.FfiEvent, listener);
|
|
1073
|
+
listener = null;
|
|
1074
|
+
this.streamReaders.delete(readerHandle);
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
const stream = new ReadableStream<DataStream_Chunk>({
|
|
1078
|
+
start: (controller) => {
|
|
1079
|
+
listener = (ev: FfiEvent) => {
|
|
1080
|
+
if (
|
|
1081
|
+
ev.message.case !== 'textStreamReaderEvent' ||
|
|
1082
|
+
ev.message.value.readerHandle !== readerHandle
|
|
1083
|
+
) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const detail = ev.message.value.detail;
|
|
1087
|
+
if (detail.case === 'chunkReceived') {
|
|
1088
|
+
const content = textEncoder.encode(detail.value.content!);
|
|
1089
|
+
if (content.length > 0) {
|
|
1090
|
+
controller.enqueue(new DataStream_Chunk({ content }));
|
|
1091
|
+
}
|
|
1092
|
+
} else if (detail.case === 'eos') {
|
|
1093
|
+
unsubscribe();
|
|
1094
|
+
const error = detail.value.error;
|
|
1095
|
+
if (error) {
|
|
1096
|
+
// Abnormal termination (e.g. remote abort, payload over the
|
|
1097
|
+
// receiver's size limit): surface the error to the consumer
|
|
1098
|
+
// instead of presenting a truncated payload as a clean EOF.
|
|
1099
|
+
controller.error(new Error(error.description ?? 'stream terminated'));
|
|
1100
|
+
} else {
|
|
1101
|
+
controller.close();
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
FfiClient.instance.on(FfiClientEvent.FfiEvent, listener);
|
|
1106
|
+
this.streamReaders.set(readerHandle, { controller, listener });
|
|
1107
|
+
|
|
1108
|
+
// Start the incremental read; the native reader buffers chunks received
|
|
1109
|
+
// before this request, so nothing is lost. This consumes the FFI handle.
|
|
1110
|
+
FfiClient.instance.request<TextStreamReaderReadIncrementalResponse>({
|
|
1111
|
+
message: {
|
|
1112
|
+
case: 'textReadIncremental',
|
|
1113
|
+
value: new TextStreamReaderReadIncrementalRequest({ readerHandle }),
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
},
|
|
1117
|
+
// The consumer stopped reading before end-of-stream — TextStreamReader
|
|
1118
|
+
// .close(), or breaking out of a for-await. The eos event that would have
|
|
1119
|
+
// unsubscribed is never consumed, so the listener would otherwise be run
|
|
1120
|
+
// against every FFI event for the rest of the process. Only the
|
|
1121
|
+
// subscription is released: the reader handle was consumed by the
|
|
1122
|
+
// readIncremental request above, so there is nothing left to dispose.
|
|
1123
|
+
cancel: () => unsubscribe(),
|
|
1124
|
+
});
|
|
1028
1125
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
fileBuffer.controller.close();
|
|
1034
|
-
this.byteStreamControllers.delete(streamId);
|
|
1035
|
-
}
|
|
1036
|
-
const textBuffer = this.textStreamControllers.get(streamId);
|
|
1037
|
-
if (textBuffer) {
|
|
1038
|
-
textBuffer.controller.close();
|
|
1039
|
-
this.textStreamControllers.delete(streamId);
|
|
1040
|
-
}
|
|
1126
|
+
streamHandlerCallback(
|
|
1127
|
+
new TextStreamReader(info, stream, bigIntToNumber(ownedReader.info!.totalLength)),
|
|
1128
|
+
{ identity: participantIdentity },
|
|
1129
|
+
);
|
|
1041
1130
|
}
|
|
1042
1131
|
}
|
|
1043
1132
|
|