@livekit/rtc-node 0.13.32 → 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/async_queue.d.ts +2 -3
- package/dist/audio_filter.d.ts +2 -3
- package/dist/audio_frame.d.ts +5 -6
- package/dist/audio_mixer.d.ts +3 -6
- package/dist/audio_resampler.d.ts +3 -6
- package/dist/audio_source.d.ts +5 -6
- package/dist/{audio_stream-4M8XlO4-.d.cts → audio_stream-BNVaONuH.d.cts} +36 -10
- package/dist/audio_stream.d.cts +2 -2
- package/dist/audio_stream.d.ts +61 -18
- package/dist/data_streams/index.d.cts +1 -1
- package/dist/data_streams/index.d.ts +4 -3
- 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 +65 -2
- 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/stream_writer.d.ts +5 -7
- 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 +47 -2
- 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 +11 -11
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/ffi_client.d.ts +8 -10
- package/dist/frame_processor.d.ts +8 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +22 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -107
- package/dist/log.d.ts +5 -7
- package/dist/participant.cjs +179 -192
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -2
- package/dist/participant.d.ts +169 -18
- package/dist/participant.d.ts.map +1 -1
- package/dist/room.cjs +166 -120
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +2 -2
- package/dist/room.d.ts +228 -18
- package/dist/room.d.ts.map +1 -1
- package/dist/rpc.d.ts +7 -9
- 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.d.ts +67 -18
- package/dist/track_publication.d.cts +2 -2
- package/dist/track_publication.d.ts +45 -18
- package/dist/transcription.d.ts +3 -4
- package/dist/{types-_PdPVish.d.ts → types-f7wTlFjs.d.cts} +34 -11
- package/dist/types.d.ts +2 -3
- 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 +10 -5
- 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 +2 -3
- package/dist/video_frame.d.ts +4 -5
- package/dist/video_source.d.ts +6 -8
- package/dist/video_stream.d.cts +2 -2
- package/dist/video_stream.d.ts +5 -22
- package/package.json +6 -7
- 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/index.ts +8 -1
- package/src/participant.ts +252 -223
- package/src/room.ts +233 -134
- 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/dist/async_queue.js +0 -56
- package/dist/async_queue.js.map +0 -1
- package/dist/audio_filter.js +0 -24
- package/dist/audio_filter.js.map +0 -1
- package/dist/audio_frame.js +0 -78
- package/dist/audio_frame.js.map +0 -1
- package/dist/audio_mixer.js +0 -269
- package/dist/audio_mixer.js.map +0 -1
- package/dist/audio_resampler.js +0 -154
- package/dist/audio_resampler.js.map +0 -1
- package/dist/audio_source.js +0 -134
- package/dist/audio_source.js.map +0 -1
- package/dist/audio_stream-CTyN1Ldy.d.ts +0 -523
- package/dist/audio_stream.js +0 -122
- package/dist/audio_stream.js.map +0 -1
- package/dist/data_streams/index.js +0 -3
- package/dist/data_streams/index.js.map +0 -1
- package/dist/data_streams/stream_reader.js +0 -124
- package/dist/data_streams/stream_reader.js.map +0 -1
- package/dist/data_streams/stream_writer.js +0 -26
- package/dist/data_streams/stream_writer.js.map +0 -1
- package/dist/data_streams/types.js +0 -1
- package/dist/data_streams/types.js.map +0 -1
- package/dist/e2ee.js +0 -251
- package/dist/e2ee.js.map +0 -1
- package/dist/ffi_client.js +0 -89
- package/dist/ffi_client.js.map +0 -1
- package/dist/frame_processor.js +0 -29
- package/dist/frame_processor.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/log.js +0 -9
- package/dist/log.js.map +0 -1
- package/dist/participant.js +0 -715
- package/dist/participant.js.map +0 -1
- package/dist/room.js +0 -900
- package/dist/room.js.map +0 -1
- package/dist/rpc.js +0 -65
- package/dist/rpc.js.map +0 -1
- package/dist/track.js +0 -234
- package/dist/track.js.map +0 -1
- package/dist/track_publication.js +0 -100
- package/dist/track_publication.js.map +0 -1
- package/dist/transcription.js +0 -1
- package/dist/transcription.js.map +0 -1
- package/dist/types-_PdPVish.d.cts +0 -87
- package/dist/types.js +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js +0 -35
- package/dist/utils.js.map +0 -1
- package/dist/version.js +0 -5
- package/dist/version.js.map +0 -1
- package/dist/video_frame.js +0 -207
- package/dist/video_frame.js.map +0 -1
- package/dist/video_source.js +0 -53
- package/dist/video_source.js.map +0 -1
- package/dist/video_stream.js +0 -70
- package/dist/video_stream.js.map +0 -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,29 +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
32
|
type RoomInfo,
|
|
33
33
|
type SimulateScenarioCallback,
|
|
34
34
|
type SimulateScenarioKind,
|
|
35
35
|
type SimulateScenarioResponse,
|
|
36
|
+
TextStreamReaderReadIncrementalRequest,
|
|
37
|
+
type TextStreamReaderReadIncrementalResponse,
|
|
36
38
|
} from '@livekit/rtc-ffi-bindings';
|
|
37
39
|
import { TrackKind } from '@livekit/rtc-ffi-bindings';
|
|
38
40
|
import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
|
|
39
41
|
import EventEmitter from 'events';
|
|
40
42
|
import { ByteStreamReader, TextStreamReader } from './data_streams/stream_reader.js';
|
|
41
|
-
import type
|
|
42
|
-
ByteStreamHandler,
|
|
43
|
-
ByteStreamInfo,
|
|
44
|
-
StreamController,
|
|
45
|
-
TextStreamHandler,
|
|
46
|
-
TextStreamInfo,
|
|
47
|
-
} from './data_streams/types.js';
|
|
43
|
+
import { type ByteStreamHandler, type TextStreamHandler } from './data_streams/types.js';
|
|
48
44
|
import type { E2EEOptions } from './e2ee.js';
|
|
49
45
|
import { E2EEManager, defaultE2EEOptions } from './e2ee.js';
|
|
50
46
|
import { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js';
|
|
@@ -56,7 +52,12 @@ import { RemoteAudioTrack, RemoteVideoTrack } from './track.js';
|
|
|
56
52
|
import type { LocalTrackPublication, TrackPublication } from './track_publication.js';
|
|
57
53
|
import { RemoteTrackPublication } from './track_publication.js';
|
|
58
54
|
import type { ChatMessage } from './types.js';
|
|
59
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
bigIntToNumber,
|
|
57
|
+
byteStreamInfoFromProto,
|
|
58
|
+
numberToBigInt,
|
|
59
|
+
textStreamInfoFromProto,
|
|
60
|
+
} from './utils.js';
|
|
60
61
|
|
|
61
62
|
export interface RtcConfiguration {
|
|
62
63
|
iceTransportType: IceTransportType;
|
|
@@ -70,6 +71,15 @@ export const defaultRtcConfiguration: RtcConfiguration = {
|
|
|
70
71
|
iceServers: [],
|
|
71
72
|
};
|
|
72
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
|
+
|
|
73
83
|
export interface RoomOptions {
|
|
74
84
|
autoSubscribe: boolean;
|
|
75
85
|
dynacast: boolean;
|
|
@@ -79,6 +89,8 @@ export interface RoomOptions {
|
|
|
79
89
|
e2ee?: E2EEOptions;
|
|
80
90
|
encryption?: E2EEOptions;
|
|
81
91
|
rtcConfig?: RtcConfiguration;
|
|
92
|
+
/** Options controlling data stream behavior for this room. */
|
|
93
|
+
dataStream?: RoomDataStreamOptions;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export const defaultRoomOptions = new FfiRoomOptions({
|
|
@@ -101,8 +113,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
101
113
|
*/
|
|
102
114
|
private ffiEventLock = new Mutex();
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
>();
|
|
106
125
|
private byteStreamHandlers = new Map<string, ByteStreamHandler>();
|
|
107
126
|
private textStreamHandlers = new Map<string, TextStreamHandler>();
|
|
108
127
|
|
|
@@ -272,16 +291,39 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
272
291
|
* @throws ConnectError - if connection fails
|
|
273
292
|
*/
|
|
274
293
|
async connect(url: string, token: string, opts?: RoomOptions) {
|
|
275
|
-
|
|
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 };
|
|
276
298
|
const e2eeEnabled = options.encryption || options.e2ee;
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
}
|
|
280
322
|
|
|
281
323
|
const req = new ConnectRequest({
|
|
282
324
|
url: url,
|
|
283
325
|
token: token,
|
|
284
|
-
options,
|
|
326
|
+
options: mergedOptions,
|
|
285
327
|
});
|
|
286
328
|
|
|
287
329
|
FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onFfiEvent);
|
|
@@ -421,28 +463,20 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
421
463
|
if (this.hasCleanedUp) return;
|
|
422
464
|
this.hasCleanedUp = true;
|
|
423
465
|
|
|
424
|
-
// Error all in-progress stream
|
|
425
|
-
// Streams that were receiving data but never
|
|
426
|
-
// disconnected mid-transfer) would otherwise keep their ReadableStream
|
|
427
|
-
// 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.
|
|
428
470
|
// Using error() instead of close() signals an abnormal termination to consumers.
|
|
429
|
-
for (const [,
|
|
430
|
-
|
|
431
|
-
streamController.controller.error(new Error('Disconnected while receiving'));
|
|
432
|
-
} catch {
|
|
433
|
-
// controller may already be closed or errored
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
this.byteStreamControllers.clear();
|
|
437
|
-
|
|
438
|
-
for (const [, streamController] of this.textStreamControllers) {
|
|
471
|
+
for (const [, { controller, listener }] of this.streamReaders) {
|
|
472
|
+
FfiClient.instance.off(FfiClientEvent.FfiEvent, listener);
|
|
439
473
|
try {
|
|
440
|
-
|
|
474
|
+
controller.error(new Error('Disconnected while receiving'));
|
|
441
475
|
} catch {
|
|
442
476
|
// controller may already be closed or errored
|
|
443
477
|
}
|
|
444
478
|
}
|
|
445
|
-
this.
|
|
479
|
+
this.streamReaders.clear();
|
|
446
480
|
|
|
447
481
|
// Detach every track from this room so attached FrameProcessors receive
|
|
448
482
|
// onStreamInfoCleared/onCredentialsCleared and the tokenRefreshed listener
|
|
@@ -451,6 +485,11 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
451
485
|
// listener until GC. setRoom(null) is idempotent, so this is safe even if a
|
|
452
486
|
// track was already detached (e.g. via unsubscribe/unpublish).
|
|
453
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
|
+
|
|
454
493
|
for (const pub of this.localParticipant.trackPublications.values()) {
|
|
455
494
|
pub.track?.setRoom(null);
|
|
456
495
|
}
|
|
@@ -840,12 +879,10 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
840
879
|
this.emit(RoomEvent.Reconnected);
|
|
841
880
|
} else if (ev.case == 'roomSidChanged') {
|
|
842
881
|
this.emit(RoomEvent.RoomSidChanged, ev.value.sid!);
|
|
843
|
-
} else if (ev.case === '
|
|
844
|
-
this.
|
|
845
|
-
} else if (ev.case === '
|
|
846
|
-
this.
|
|
847
|
-
} else if (ev.case === 'streamTrailerReceived' && ev.value.trailer) {
|
|
848
|
-
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!);
|
|
849
886
|
} else if (ev.case === 'roomUpdated') {
|
|
850
887
|
this.info = ev.value;
|
|
851
888
|
this.emit(RoomEvent.RoomUpdated);
|
|
@@ -930,104 +967,166 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
930
967
|
return participant;
|
|
931
968
|
}
|
|
932
969
|
|
|
933
|
-
private
|
|
934
|
-
|
|
935
|
-
|
|
970
|
+
private handleByteStreamOpened(ownedReader: OwnedByteStreamReader, participantIdentity: string) {
|
|
971
|
+
const info = byteStreamInfoFromProto(ownedReader.info!);
|
|
972
|
+
const readerHandle = ownedReader.handle!.id!;
|
|
936
973
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
startTime: Date.now(),
|
|
952
|
-
});
|
|
953
|
-
},
|
|
954
|
-
});
|
|
955
|
-
const info: ByteStreamInfo = {
|
|
956
|
-
streamId: streamHeader.streamId!,
|
|
957
|
-
name: streamHeader.contentHeader.value.name ?? 'unknown',
|
|
958
|
-
mimeType: streamHeader.mimeType!,
|
|
959
|
-
totalSize: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
|
|
960
|
-
topic: streamHeader.topic!,
|
|
961
|
-
timestamp: bigIntToNumber(streamHeader.timestamp!),
|
|
962
|
-
attributes: streamHeader.attributes,
|
|
963
|
-
};
|
|
964
|
-
streamHandlerCallback(
|
|
965
|
-
new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
|
|
966
|
-
{ identity: participantIdentity },
|
|
967
|
-
);
|
|
968
|
-
} else if (streamHeader.contentHeader.case === 'textHeader') {
|
|
969
|
-
const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic ?? '');
|
|
970
|
-
|
|
971
|
-
if (!streamHandlerCallback) {
|
|
972
|
-
log.debug(
|
|
973
|
-
'ignoring incoming text stream due to no handler for topic: %s',
|
|
974
|
-
streamHeader.topic ?? 'undefined',
|
|
975
|
-
);
|
|
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) {
|
|
976
988
|
return;
|
|
977
989
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
+
);
|
|
1002
1048
|
}
|
|
1003
1049
|
|
|
1004
|
-
private
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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;
|
|
1010
1061
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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;
|
|
1015
1071
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
+
});
|
|
1018
1125
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
fileBuffer.controller.close();
|
|
1024
|
-
this.byteStreamControllers.delete(streamId);
|
|
1025
|
-
}
|
|
1026
|
-
const textBuffer = this.textStreamControllers.get(streamId);
|
|
1027
|
-
if (textBuffer) {
|
|
1028
|
-
textBuffer.controller.close();
|
|
1029
|
-
this.textStreamControllers.delete(streamId);
|
|
1030
|
-
}
|
|
1126
|
+
streamHandlerCallback(
|
|
1127
|
+
new TextStreamReader(info, stream, bigIntToNumber(ownedReader.info!.totalLength)),
|
|
1128
|
+
{ identity: participantIdentity },
|
|
1129
|
+
);
|
|
1031
1130
|
}
|
|
1032
1131
|
}
|
|
1033
1132
|
|