livekit-client 2.18.9 → 2.19.0
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/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +5609 -644
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +3553 -2813
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.pt.worker.js +2 -0
- package/dist/livekit-client.pt.worker.js.map +1 -0
- package/dist/livekit-client.pt.worker.mjs +5834 -0
- package/dist/livekit-client.pt.worker.mjs.map +1 -0
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +2 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +8 -7
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +35 -8
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +5 -5
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +5 -5
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +21 -4
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +7 -7
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +7 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/packetTrailer/PacketTrailerManager.d.ts +49 -0
- package/dist/src/packetTrailer/PacketTrailerManager.d.ts.map +1 -0
- package/dist/src/packetTrailer/packetTrailer.d.ts +32 -0
- package/dist/src/packetTrailer/packetTrailer.d.ts.map +1 -0
- package/dist/src/packetTrailer/types.d.ts +57 -0
- package/dist/src/packetTrailer/types.d.ts.map +1 -0
- package/dist/src/packetTrailer/utils.d.ts +9 -0
- package/dist/src/packetTrailer/utils.d.ts.map +1 -0
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +1 -0
- package/dist/src/room/RTCEngine.d.ts +2 -4
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +7 -3
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -1
- package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts +12 -4
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
- package/dist/src/room/data-track/frame.d.ts +3 -3
- package/dist/src/room/data-track/frame.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
- package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +2 -2
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
- package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/index.d.ts +5 -5
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
- package/dist/src/room/data-track/types.d.ts +7 -0
- package/dist/src/room/data-track/types.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -2
- package/dist/src/room/participant/LocalParticipant.d.ts +8 -14
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +5 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
- package/dist/src/room/rpc/client/events.d.ts +8 -0
- package/dist/src/room/rpc/client/events.d.ts.map +1 -0
- package/dist/src/room/rpc/index.d.ts +6 -0
- package/dist/src/room/rpc/index.d.ts.map +1 -0
- package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
- package/dist/src/room/rpc/server/events.d.ts +8 -0
- package/dist/src/room/rpc/server/events.d.ts.map +1 -0
- package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
- package/dist/src/room/rpc/utils.d.ts.map +1 -0
- package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
- package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +10 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +4 -3
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
- package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/src/version.d.ts +9 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/api/SignalClient.d.ts +2 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
- package/dist/ts4.2/e2ee/types.d.ts +35 -8
- package/dist/ts4.2/e2ee/utils.d.ts +5 -5
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
- package/dist/ts4.2/index.d.ts +5 -1
- package/dist/ts4.2/options.d.ts +7 -0
- package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
- package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
- package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
- package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
- package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +2 -4
- package/dist/ts4.2/room/Room.d.ts +7 -3
- package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
- package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
- package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
- package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
- package/dist/ts4.2/room/data-track/types.d.ts +7 -0
- package/dist/ts4.2/room/events.d.ts +2 -2
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +8 -14
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
- package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
- package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
- package/dist/ts4.2/room/rpc/index.d.ts +7 -0
- package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
- package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
- package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
- package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
- package/dist/ts4.2/room/track/Track.d.ts +1 -1
- package/dist/ts4.2/room/track/options.d.ts +10 -0
- package/dist/ts4.2/room/utils.d.ts +4 -3
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
- package/dist/ts4.2/version.d.ts +9 -1
- package/package.json +24 -16
- package/src/api/SignalClient.test.ts +102 -10
- package/src/api/SignalClient.ts +4 -2
- package/src/api/WebSocketStream.test.ts +0 -1
- package/src/e2ee/E2eeManager.ts +82 -30
- package/src/e2ee/types.ts +37 -8
- package/src/e2ee/utils.ts +7 -6
- package/src/e2ee/worker/DataCryptor.ts +6 -6
- package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
- package/src/e2ee/worker/FrameCryptor.ts +94 -14
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
- package/src/e2ee/worker/e2ee.worker.ts +13 -5
- package/src/e2ee/worker/naluUtils.ts +4 -4
- package/src/e2ee/worker/sifPayload.ts +10 -8
- package/src/index.ts +7 -0
- package/src/options.ts +8 -0
- package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
- package/src/packetTrailer/PacketTrailerManager.ts +250 -0
- package/src/packetTrailer/packetTrailer.test.ts +174 -0
- package/src/packetTrailer/packetTrailer.ts +276 -0
- package/src/packetTrailer/types.ts +75 -0
- package/src/packetTrailer/utils.test.ts +105 -0
- package/src/packetTrailer/utils.ts +50 -0
- package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
- package/src/packetTrailer/worker/tsconfig.json +14 -0
- package/src/room/BackOffStrategy.test.ts +1 -1
- package/src/room/RTCEngine.test.ts +219 -0
- package/src/room/RTCEngine.ts +86 -46
- package/src/room/Room.test.ts +62 -1
- package/src/room/Room.ts +111 -86
- package/src/room/data-track/RemoteDataTrack.ts +8 -1
- package/src/room/data-track/depacketizer.test.ts +433 -1
- package/src/room/data-track/depacketizer.ts +79 -61
- package/src/room/data-track/frame.ts +2 -2
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
- package/src/room/data-track/incoming/pipeline.ts +13 -2
- package/src/room/data-track/outgoing/types.ts +3 -2
- package/src/room/data-track/packet/extensions.ts +2 -2
- package/src/room/data-track/packet/index.ts +6 -6
- package/src/room/data-track/packet/serializable.ts +1 -1
- package/src/room/data-track/types.ts +8 -0
- package/src/room/events.ts +2 -2
- package/src/room/participant/LocalParticipant.test.ts +81 -0
- package/src/room/participant/LocalParticipant.ts +64 -187
- package/src/room/participant/Participant.ts +1 -1
- package/src/room/participant/RemoteParticipant.ts +9 -0
- package/src/room/participant/publishUtils.ts +1 -1
- package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
- package/src/room/rpc/client/RpcClientManager.ts +269 -0
- package/src/room/rpc/client/events.ts +9 -0
- package/src/room/rpc/index.ts +14 -0
- package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
- package/src/room/rpc/server/RpcServerManager.ts +293 -0
- package/src/room/rpc/server/events.ts +9 -0
- package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
- package/src/room/track/PacketTrailerExtractor.ts +43 -0
- package/src/room/track/RemoteVideoTrack.ts +23 -2
- package/src/room/track/Track.ts +1 -1
- package/src/room/track/create.ts +0 -4
- package/src/room/track/options.ts +11 -0
- package/src/room/track/record.ts +1 -1
- package/src/room/track/utils.ts +4 -1
- package/src/room/utils.test.ts +14 -1
- package/src/room/utils.ts +19 -4
- package/src/test/MockMediaStreamTrack.ts +0 -1
- package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
- package/src/utils/dataPacketBuffer.ts +1 -1
- package/src/version.ts +11 -1
- package/dist/src/room/rpc.d.ts.map +0 -1
- package/src/room/rpc.test.ts +0 -301
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import log from '../../../logger';
|
|
3
|
+
import { subscribeToEvents } from '../../../utils/subscribeToEvents';
|
|
4
|
+
import { CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT } from '../../../version';
|
|
5
|
+
import type RTCEngine from '../../RTCEngine';
|
|
6
|
+
import OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
|
|
7
|
+
import { RPC_REQUEST_DATA_STREAM_TOPIC, RpcError, RpcRequestAttrs } from '../utils';
|
|
8
|
+
import RpcClientManager from './RpcClientManager';
|
|
9
|
+
import type { RpcClientManagerCallbacks } from './events';
|
|
10
|
+
|
|
11
|
+
describe('RpcClientManager', () => {
|
|
12
|
+
describe('v2 -> v1', () => {
|
|
13
|
+
let rpcClientManager: RpcClientManager;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
const outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
17
|
+
{} as unknown as RTCEngine,
|
|
18
|
+
log,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
rpcClientManager = new RpcClientManager(
|
|
22
|
+
log,
|
|
23
|
+
outgoingDataStreamManager,
|
|
24
|
+
(_identity) => CLIENT_PROTOCOL_DEFAULT, // (other participant is "v1")
|
|
25
|
+
() => undefined,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should send v1 RPC request to a "legacy" client (happy path)', async () => {
|
|
30
|
+
const managerEvents = subscribeToEvents<RpcClientManagerCallbacks>(rpcClientManager, [
|
|
31
|
+
'sendDataPacket',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
35
|
+
destinationIdentity: 'remoteIdentity',
|
|
36
|
+
method: 'testMethod',
|
|
37
|
+
payload: 'testPayload',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Verify exactly one packet was emitted
|
|
41
|
+
const { packet } = await managerEvents.waitFor('sendDataPacket');
|
|
42
|
+
assert(packet.value.case === 'rpcRequest');
|
|
43
|
+
expect(packet.value.value.id).toStrictEqual(requestId);
|
|
44
|
+
expect(packet.value.value.method).toStrictEqual('testMethod');
|
|
45
|
+
expect(packet.value.value.payload).toStrictEqual('testPayload');
|
|
46
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
47
|
+
|
|
48
|
+
// Asynchronously send a response back
|
|
49
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
50
|
+
rpcClientManager.handleIncomingRpcResponseSuccess(requestId, 'response payload');
|
|
51
|
+
|
|
52
|
+
// Make sure the response came out the other end
|
|
53
|
+
const result = await completionPromise;
|
|
54
|
+
expect(result).toStrictEqual('response payload');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should fail to send long (> 15kb) v1 RPC request', async () => {
|
|
58
|
+
const longPayload = new Array<string>(20_000).fill('A').join('');
|
|
59
|
+
|
|
60
|
+
const performRpcPromise = rpcClientManager.performRpc({
|
|
61
|
+
destinationIdentity: 'destination-identity',
|
|
62
|
+
method: 'test-method',
|
|
63
|
+
payload: longPayload,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await expect(performRpcPromise).rejects.toThrow('Request payload too large');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle v1 RPC request timeout', async () => {
|
|
70
|
+
vi.useFakeTimers();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const method = 'timeoutMethod';
|
|
74
|
+
const payload = 'timeoutPayload';
|
|
75
|
+
const timeout = 50;
|
|
76
|
+
|
|
77
|
+
const [, completionPromise] = await rpcClientManager.performRpc({
|
|
78
|
+
destinationIdentity: 'remote-identity',
|
|
79
|
+
method,
|
|
80
|
+
payload,
|
|
81
|
+
responseTimeout: timeout,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Register the rejection handler before advancing so the rejection is caught
|
|
85
|
+
const rejectPromise = expect(completionPromise).rejects.toThrow('Response timeout');
|
|
86
|
+
|
|
87
|
+
// Response timeout (50ms) fires before ack timeout (7000ms)
|
|
88
|
+
await vi.advanceTimersByTimeAsync(timeout);
|
|
89
|
+
|
|
90
|
+
await rejectPromise;
|
|
91
|
+
} finally {
|
|
92
|
+
vi.useRealTimers();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle v1 RPC error response', async () => {
|
|
97
|
+
const method = 'errorMethod';
|
|
98
|
+
const payload = 'errorPayload';
|
|
99
|
+
const errorCode = 101;
|
|
100
|
+
const errorMessage = 'Test error message';
|
|
101
|
+
|
|
102
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
103
|
+
destinationIdentity: 'remote-identity',
|
|
104
|
+
method,
|
|
105
|
+
payload,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
109
|
+
rpcClientManager.handleIncomingRpcResponseFailure(
|
|
110
|
+
requestId,
|
|
111
|
+
new RpcError(errorCode, errorMessage),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await expect(completionPromise).rejects.toThrow(errorMessage);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle participant disconnection during v1 RPC request', async () => {
|
|
118
|
+
const method = 'disconnectMethod';
|
|
119
|
+
const payload = 'disconnectPayload';
|
|
120
|
+
|
|
121
|
+
const [, completionPromise] = await rpcClientManager.performRpc({
|
|
122
|
+
destinationIdentity: 'remote-identity',
|
|
123
|
+
method,
|
|
124
|
+
payload,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
rpcClientManager.handleParticipantDisconnected('remote-identity');
|
|
128
|
+
|
|
129
|
+
await expect(completionPromise).rejects.toThrow('Recipient disconnected');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('v2 -> v2', () => {
|
|
134
|
+
let rpcClientManager: RpcClientManager;
|
|
135
|
+
let mockStreamTextWriter: {
|
|
136
|
+
write: ReturnType<typeof vi.fn>;
|
|
137
|
+
close: ReturnType<typeof vi.fn>;
|
|
138
|
+
};
|
|
139
|
+
let mockOutgoingDataStreamManager: OutgoingDataStreamManager;
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
mockStreamTextWriter = {
|
|
143
|
+
write: vi.fn().mockResolvedValue(undefined),
|
|
144
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
145
|
+
};
|
|
146
|
+
mockOutgoingDataStreamManager = {
|
|
147
|
+
streamText: vi.fn().mockResolvedValue(mockStreamTextWriter),
|
|
148
|
+
} as unknown as OutgoingDataStreamManager;
|
|
149
|
+
|
|
150
|
+
rpcClientManager = new RpcClientManager(
|
|
151
|
+
log,
|
|
152
|
+
mockOutgoingDataStreamManager,
|
|
153
|
+
(_identity) => CLIENT_PROTOCOL_DATA_STREAM_RPC, // (other participant is "v2")
|
|
154
|
+
() => undefined,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function mockTextStreamReader(payload: string) {
|
|
159
|
+
return { readAll: vi.fn().mockResolvedValue(payload) } as any;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
it('should send short v2 RPC request via data stream (happy path)', async () => {
|
|
163
|
+
const managerEvents = subscribeToEvents<RpcClientManagerCallbacks>(rpcClientManager, [
|
|
164
|
+
'sendDataPacket',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
168
|
+
destinationIdentity: 'destination-identity',
|
|
169
|
+
method: 'test-method',
|
|
170
|
+
payload: 'request-payload',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Verify the data stream was used with correct attributes
|
|
174
|
+
expect(mockOutgoingDataStreamManager.streamText).toHaveBeenCalledWith(
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
topic: RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
177
|
+
destinationIdentities: ['destination-identity'],
|
|
178
|
+
attributes: expect.objectContaining({
|
|
179
|
+
[RpcRequestAttrs.RPC_REQUEST_ID]: requestId,
|
|
180
|
+
[RpcRequestAttrs.RPC_REQUEST_METHOD]: 'test-method',
|
|
181
|
+
[RpcRequestAttrs.RPC_REQUEST_VERSION]: '2',
|
|
182
|
+
}),
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
expect(mockStreamTextWriter.write).toHaveBeenCalledWith('request-payload');
|
|
186
|
+
expect(mockStreamTextWriter.close).toHaveBeenCalled();
|
|
187
|
+
|
|
188
|
+
// No packet should have been emitted
|
|
189
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
190
|
+
|
|
191
|
+
// Asynchronously send a response back
|
|
192
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
193
|
+
await rpcClientManager.handleIncomingDataStream(
|
|
194
|
+
mockTextStreamReader('response-payload'),
|
|
195
|
+
'destination-identity',
|
|
196
|
+
{ [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await expect(completionPromise).resolves.toStrictEqual('response-payload');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should send long (> 15kb) v2 RPC request via data stream', async () => {
|
|
203
|
+
const managerEvents = subscribeToEvents<RpcClientManagerCallbacks>(rpcClientManager, [
|
|
204
|
+
'sendDataPacket',
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
const longPayload = new Array<string>(20_000).fill('A').join('');
|
|
208
|
+
|
|
209
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
210
|
+
destinationIdentity: 'destination-identity',
|
|
211
|
+
method: 'test-method',
|
|
212
|
+
payload: longPayload,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Verify the data stream was used with correct attributes
|
|
216
|
+
expect(mockOutgoingDataStreamManager.streamText).toHaveBeenCalledWith(
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
topic: RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
219
|
+
destinationIdentities: ['destination-identity'],
|
|
220
|
+
attributes: expect.objectContaining({
|
|
221
|
+
[RpcRequestAttrs.RPC_REQUEST_ID]: requestId,
|
|
222
|
+
[RpcRequestAttrs.RPC_REQUEST_METHOD]: 'test-method',
|
|
223
|
+
[RpcRequestAttrs.RPC_REQUEST_VERSION]: '2',
|
|
224
|
+
}),
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
expect(mockStreamTextWriter.write).toHaveBeenCalledWith(longPayload);
|
|
228
|
+
expect(mockStreamTextWriter.close).toHaveBeenCalled();
|
|
229
|
+
|
|
230
|
+
// No packet should have been emitted
|
|
231
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
232
|
+
|
|
233
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
234
|
+
await rpcClientManager.handleIncomingDataStream(
|
|
235
|
+
mockTextStreamReader('response-payload'),
|
|
236
|
+
'destination-identity',
|
|
237
|
+
{ [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await expect(completionPromise).resolves.toStrictEqual('response-payload');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle a v2 RPC request timeout', async () => {
|
|
244
|
+
vi.useFakeTimers();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const timeout = 50;
|
|
248
|
+
|
|
249
|
+
const [, completionPromise] = await rpcClientManager.performRpc({
|
|
250
|
+
destinationIdentity: 'remote-identity',
|
|
251
|
+
method: 'timeoutMethod',
|
|
252
|
+
payload: 'timeoutPayload',
|
|
253
|
+
responseTimeout: timeout,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const rejectPromise = expect(completionPromise).rejects.toThrow('Response timeout');
|
|
257
|
+
await vi.advanceTimersByTimeAsync(timeout);
|
|
258
|
+
await rejectPromise;
|
|
259
|
+
} finally {
|
|
260
|
+
vi.useRealTimers();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should handle a v2 RPC error response', async () => {
|
|
265
|
+
const errorCode = 101;
|
|
266
|
+
const errorMessage = 'Test error message';
|
|
267
|
+
|
|
268
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
269
|
+
destinationIdentity: 'remote-identity',
|
|
270
|
+
method: 'errorMethod',
|
|
271
|
+
payload: 'errorPayload',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
275
|
+
rpcClientManager.handleIncomingRpcResponseFailure(
|
|
276
|
+
requestId,
|
|
277
|
+
new RpcError(errorCode, errorMessage),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await expect(completionPromise).rejects.toThrow(errorMessage);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should handle participant disconnection during v2 RPC request', async () => {
|
|
284
|
+
const [, completionPromise] = await rpcClientManager.performRpc({
|
|
285
|
+
destinationIdentity: 'remote-identity',
|
|
286
|
+
method: 'disconnectMethod',
|
|
287
|
+
payload: 'disconnectPayload',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
rpcClientManager.handleParticipantDisconnected('remote-identity');
|
|
291
|
+
|
|
292
|
+
await expect(completionPromise).rejects.toThrow('Recipient disconnected');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should send V2 RPC request and ensure that a non matching response does not complete the RPC', async () => {
|
|
296
|
+
// Step 1: send an example rpc request
|
|
297
|
+
const [, completionPromise] = await rpcClientManager.performRpc({
|
|
298
|
+
destinationIdentity: 'destination-identity',
|
|
299
|
+
method: 'test-method',
|
|
300
|
+
payload: 'test payload',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Step 2: send an acknowledgement / response for a different rpc
|
|
304
|
+
rpcClientManager.handleIncomingRpcAck('bogus request id');
|
|
305
|
+
await rpcClientManager.handleIncomingDataStream(
|
|
306
|
+
mockTextStreamReader('response-payload'),
|
|
307
|
+
'destination-identity',
|
|
308
|
+
{ [RpcRequestAttrs.RPC_REQUEST_ID]: 'bogus request id' },
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Step 3: Make sure that the completion promise didn't resolve.
|
|
312
|
+
await expect(
|
|
313
|
+
Promise.race([completionPromise, Promise.resolve('still pending')]),
|
|
314
|
+
).resolves.toStrictEqual('still pending');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should ensure that many rpc requests generate different request ids', async () => {
|
|
318
|
+
const requestIds: Array<string> = [];
|
|
319
|
+
for (let i = 0; i < 5; i += 1) {
|
|
320
|
+
const [requestId] = await rpcClientManager.performRpc({
|
|
321
|
+
destinationIdentity: 'destination-identity',
|
|
322
|
+
method: 'test-method',
|
|
323
|
+
payload: 'test payload',
|
|
324
|
+
});
|
|
325
|
+
requestIds.push(requestId);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Make sure all request ids are unique
|
|
329
|
+
for (let i = 0; i < requestIds.length; i += 1) {
|
|
330
|
+
for (let j = 0; j < requestIds.length; j += 1) {
|
|
331
|
+
if (i === j) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
expect(requestIds[i]).not.toStrictEqual(requestIds[j]);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should not drop ack and response that arrive before publish completes', async () => {
|
|
340
|
+
// Hold the publish path open by blocking writer.close() until we explicitly resolve it.
|
|
341
|
+
let resolveClose!: () => void;
|
|
342
|
+
const closeBlocked = new Promise<void>((resolve) => {
|
|
343
|
+
resolveClose = resolve;
|
|
344
|
+
});
|
|
345
|
+
mockStreamTextWriter.close = vi.fn().mockReturnValue(closeBlocked);
|
|
346
|
+
|
|
347
|
+
// Start performRpc but don't await its return yet. The synchronous prefix runs streamText.
|
|
348
|
+
const performRpcPromise = rpcClientManager.performRpc({
|
|
349
|
+
destinationIdentity: 'destination-identity',
|
|
350
|
+
method: 'test-method',
|
|
351
|
+
payload: 'request-payload',
|
|
352
|
+
responseTimeout: 200,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// streamText was called synchronously; pull the request id out of the attributes.
|
|
356
|
+
const streamTextCalls = (mockOutgoingDataStreamManager.streamText as ReturnType<typeof vi.fn>)
|
|
357
|
+
.mock.calls;
|
|
358
|
+
expect(streamTextCalls.length).toBe(1);
|
|
359
|
+
const requestId = streamTextCalls[0][0].attributes[RpcRequestAttrs.RPC_REQUEST_ID];
|
|
360
|
+
|
|
361
|
+
// Deliver ack and response BEFORE close() unblocks - the publish has not yet returned.
|
|
362
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
363
|
+
await rpcClientManager.handleIncomingDataStream(
|
|
364
|
+
mockTextStreamReader('response-payload'),
|
|
365
|
+
'destination-identity',
|
|
366
|
+
{ [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Now allow the publish path to complete.
|
|
370
|
+
resolveClose();
|
|
371
|
+
|
|
372
|
+
const [, completionPromise] = await performRpcPromise;
|
|
373
|
+
await expect(completionPromise).resolves.toStrictEqual('response-payload');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should ignore a late ack and response that arrive after ack-timeout fires', async () => {
|
|
377
|
+
vi.useFakeTimers();
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
381
|
+
destinationIdentity: 'remote-identity',
|
|
382
|
+
method: 'test-method',
|
|
383
|
+
payload: 'test-payload',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Register the rejection handler before advancing so the rejection is caught.
|
|
387
|
+
const rejectPromise = expect(completionPromise).rejects.toThrow(/Connection timeout/i);
|
|
388
|
+
|
|
389
|
+
// Advance past the ack-timeout window (maxRoundTripLatencyMs = 7000ms).
|
|
390
|
+
await vi.advanceTimersByTimeAsync(7001);
|
|
391
|
+
|
|
392
|
+
await rejectPromise;
|
|
393
|
+
|
|
394
|
+
// A delayed ack and response now arrive for the same request id - should be silently
|
|
395
|
+
// ignored: no throw, no second resolution, no unhandled rejection.
|
|
396
|
+
expect(() => rpcClientManager.handleIncomingRpcAck(requestId)).not.toThrow();
|
|
397
|
+
await rpcClientManager.handleIncomingDataStream(
|
|
398
|
+
mockTextStreamReader('late response'),
|
|
399
|
+
'remote-identity',
|
|
400
|
+
{ [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
401
|
+
);
|
|
402
|
+
} finally {
|
|
403
|
+
vi.useRealTimers();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should not resolve a v2 response stream that comes from a wrong sender identity', async () => {
|
|
408
|
+
const [requestId, completionPromise] = await rpcClientManager.performRpc({
|
|
409
|
+
destinationIdentity: 'alice',
|
|
410
|
+
method: 'test-method',
|
|
411
|
+
payload: 'test payload',
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
rpcClientManager.handleIncomingRpcAck(requestId);
|
|
415
|
+
|
|
416
|
+
// Simulate a v2 response data stream that arrived purportedly from "mallory" rather
|
|
417
|
+
// than the destination identity "alice".
|
|
418
|
+
await rpcClientManager.handleIncomingDataStream(
|
|
419
|
+
mockTextStreamReader('malicious response'),
|
|
420
|
+
'mallory',
|
|
421
|
+
{ [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// The completionPromise must remain pending - the wrong-sender response is ignored.
|
|
425
|
+
await expect(
|
|
426
|
+
Promise.race([completionPromise, Promise.resolve('still pending')]),
|
|
427
|
+
).resolves.toStrictEqual('still pending');
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { DataPacket, DataPacket_Kind, RpcRequest } from '@livekit/protocol';
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
import type TypedEmitter from 'typed-emitter';
|
|
4
|
+
import { type StructuredLogger } from '../../../logger';
|
|
5
|
+
import { CLIENT_PROTOCOL_DATA_STREAM_RPC } from '../../../version';
|
|
6
|
+
import { type TextStreamReader } from '../../data-stream/incoming/StreamReader';
|
|
7
|
+
import type OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
|
|
8
|
+
import type Participant from '../../participant/Participant';
|
|
9
|
+
import { Future, compareVersions } from '../../utils';
|
|
10
|
+
import {
|
|
11
|
+
MAX_V1_PAYLOAD_BYTES,
|
|
12
|
+
type PerformRpcParams,
|
|
13
|
+
RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
14
|
+
RPC_VERSION_V1,
|
|
15
|
+
RPC_VERSION_V2,
|
|
16
|
+
RpcError,
|
|
17
|
+
RpcRequestAttrs,
|
|
18
|
+
byteLength,
|
|
19
|
+
} from '../utils';
|
|
20
|
+
import type { RpcClientManagerCallbacks } from './events';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manages the client (caller) side of RPC: sending requests, tracking pending
|
|
24
|
+
* ack/response state, and handling incoming ack/response packets.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export default class RpcClientManager extends (EventEmitter as new () => TypedEmitter<RpcClientManagerCallbacks>) {
|
|
28
|
+
private log: StructuredLogger;
|
|
29
|
+
|
|
30
|
+
private outgoingDataStreamManager: OutgoingDataStreamManager;
|
|
31
|
+
|
|
32
|
+
private getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number;
|
|
33
|
+
|
|
34
|
+
private getServerVersion: () => string | undefined;
|
|
35
|
+
|
|
36
|
+
private pendingAcks = new Map<string, { resolve: () => void; participantIdentity: string }>();
|
|
37
|
+
|
|
38
|
+
private pendingResponses = new Map<
|
|
39
|
+
string /* request id */,
|
|
40
|
+
{
|
|
41
|
+
completionFuture: Future<string, RpcError>;
|
|
42
|
+
participantIdentity: string;
|
|
43
|
+
}
|
|
44
|
+
>();
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
log: StructuredLogger,
|
|
48
|
+
outgoingDataStreamManager: OutgoingDataStreamManager,
|
|
49
|
+
getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number,
|
|
50
|
+
getServerVersion: () => string | undefined,
|
|
51
|
+
) {
|
|
52
|
+
super();
|
|
53
|
+
this.log = log;
|
|
54
|
+
this.outgoingDataStreamManager = outgoingDataStreamManager;
|
|
55
|
+
this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
|
|
56
|
+
this.getServerVersion = getServerVersion;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async performRpc({
|
|
60
|
+
destinationIdentity,
|
|
61
|
+
method,
|
|
62
|
+
payload,
|
|
63
|
+
responseTimeout: responseTimeoutMs = 15000,
|
|
64
|
+
}: PerformRpcParams): Promise<[id: string, completionPromise: Promise<string>]> {
|
|
65
|
+
const maxRoundTripLatencyMs = 7000;
|
|
66
|
+
const minEffectiveTimeoutMs = maxRoundTripLatencyMs + 1000;
|
|
67
|
+
|
|
68
|
+
const remoteClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
|
|
69
|
+
const payloadBytes = byteLength(payload);
|
|
70
|
+
|
|
71
|
+
// Only enforce the legacy size limit when on rpc v1
|
|
72
|
+
if (
|
|
73
|
+
payloadBytes > MAX_V1_PAYLOAD_BYTES &&
|
|
74
|
+
remoteClientProtocol < CLIENT_PROTOCOL_DATA_STREAM_RPC
|
|
75
|
+
) {
|
|
76
|
+
throw RpcError.builtIn('REQUEST_PAYLOAD_TOO_LARGE');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const serverVersion = this.getServerVersion();
|
|
80
|
+
if (serverVersion && compareVersions(serverVersion, '1.8.0') < 0) {
|
|
81
|
+
throw RpcError.builtIn('UNSUPPORTED_SERVER');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const effectiveTimeoutMs = Math.max(responseTimeoutMs, minEffectiveTimeoutMs);
|
|
85
|
+
const id = crypto.randomUUID();
|
|
86
|
+
|
|
87
|
+
const completionFuture = new Future<string, RpcError>();
|
|
88
|
+
|
|
89
|
+
const ackTimeoutId = setTimeout(() => {
|
|
90
|
+
this.pendingAcks.delete(id);
|
|
91
|
+
completionFuture.reject?.(RpcError.builtIn('CONNECTION_TIMEOUT'));
|
|
92
|
+
this.pendingResponses.delete(id);
|
|
93
|
+
clearTimeout(responseTimeoutId);
|
|
94
|
+
}, maxRoundTripLatencyMs);
|
|
95
|
+
|
|
96
|
+
this.pendingAcks.set(id, {
|
|
97
|
+
resolve: () => {
|
|
98
|
+
clearTimeout(ackTimeoutId);
|
|
99
|
+
},
|
|
100
|
+
participantIdentity: destinationIdentity,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.pendingResponses.set(id, {
|
|
104
|
+
completionFuture,
|
|
105
|
+
participantIdentity: destinationIdentity,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await this.publishRpcRequest(
|
|
109
|
+
destinationIdentity,
|
|
110
|
+
id,
|
|
111
|
+
method,
|
|
112
|
+
payload,
|
|
113
|
+
effectiveTimeoutMs,
|
|
114
|
+
remoteClientProtocol,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const responseTimeoutId = setTimeout(() => {
|
|
118
|
+
this.pendingResponses.delete(id);
|
|
119
|
+
completionFuture.reject?.(RpcError.builtIn('RESPONSE_TIMEOUT'));
|
|
120
|
+
}, responseTimeoutMs);
|
|
121
|
+
|
|
122
|
+
const completionPromise = completionFuture.promise.finally(() => {
|
|
123
|
+
clearTimeout(responseTimeoutId);
|
|
124
|
+
|
|
125
|
+
if (this.pendingAcks.has(id)) {
|
|
126
|
+
this.log.warn('RPC response received before ack', id);
|
|
127
|
+
this.pendingAcks.delete(id);
|
|
128
|
+
clearTimeout(ackTimeoutId);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return [id, completionPromise];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async publishRpcRequest(
|
|
136
|
+
destinationIdentity: string,
|
|
137
|
+
requestId: string,
|
|
138
|
+
method: string,
|
|
139
|
+
payload: string,
|
|
140
|
+
responseTimeout: number,
|
|
141
|
+
remoteClientProtocol: number,
|
|
142
|
+
) {
|
|
143
|
+
if (remoteClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
|
|
144
|
+
// Send payload as a data stream - a "version 2" rpc request.
|
|
145
|
+
const writer = await this.outgoingDataStreamManager.streamText({
|
|
146
|
+
topic: RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
147
|
+
destinationIdentities: [destinationIdentity],
|
|
148
|
+
attributes: {
|
|
149
|
+
[RpcRequestAttrs.RPC_REQUEST_ID]: requestId,
|
|
150
|
+
[RpcRequestAttrs.RPC_REQUEST_METHOD]: method,
|
|
151
|
+
[RpcRequestAttrs.RPC_REQUEST_RESPONSE_TIMEOUT_MS]: `${responseTimeout}`,
|
|
152
|
+
[RpcRequestAttrs.RPC_REQUEST_VERSION]: `${RPC_VERSION_V2}`,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await writer.write(payload);
|
|
157
|
+
await writer.close();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fallback to sending a literal RpcRequest - a "version 1" rpc request.
|
|
162
|
+
this.emit('sendDataPacket', {
|
|
163
|
+
packet: new DataPacket({
|
|
164
|
+
destinationIdentities: [destinationIdentity],
|
|
165
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
166
|
+
value: {
|
|
167
|
+
case: 'rpcRequest',
|
|
168
|
+
value: new RpcRequest({
|
|
169
|
+
id: requestId,
|
|
170
|
+
method,
|
|
171
|
+
payload,
|
|
172
|
+
responseTimeoutMs: responseTimeout,
|
|
173
|
+
version: RPC_VERSION_V1,
|
|
174
|
+
}),
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Handle an incoming data stream containing an RPC response payload.
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
184
|
+
async handleIncomingDataStream(
|
|
185
|
+
reader: TextStreamReader,
|
|
186
|
+
senderIdentity: Participant['identity'],
|
|
187
|
+
attributes: Record<string, string>,
|
|
188
|
+
) {
|
|
189
|
+
const associatedRequestId = attributes[RpcRequestAttrs.RPC_REQUEST_ID];
|
|
190
|
+
if (!associatedRequestId) {
|
|
191
|
+
this.log.warn(`RPC data stream malformed: ${RpcRequestAttrs.RPC_REQUEST_ID} not set.`);
|
|
192
|
+
// NOTE: no response can be sent here, because there's no request id so associate
|
|
193
|
+
// so logging is the best we can do here.
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const pending = this.pendingResponses.get(associatedRequestId);
|
|
198
|
+
if (pending && pending.participantIdentity !== senderIdentity) {
|
|
199
|
+
this.log.warn(
|
|
200
|
+
`RPC response stream for ${associatedRequestId} arrived from unexpected sender ${senderIdentity}, expected ${pending.participantIdentity}. Ignoring.`,
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let payload: string;
|
|
206
|
+
try {
|
|
207
|
+
payload = await reader.readAll();
|
|
208
|
+
} catch (e) {
|
|
209
|
+
this.log.warn(`Error reading RPC response payload: ${e}`);
|
|
210
|
+
this.handleIncomingRpcResponseFailure(
|
|
211
|
+
associatedRequestId,
|
|
212
|
+
RpcError.builtIn('APPLICATION_ERROR', 'Error reading RPC response payload', { cause: e }),
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.handleIncomingRpcResponseSuccess(associatedRequestId, payload);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @internal */
|
|
221
|
+
handleIncomingRpcResponseSuccess(requestId: string, payload: string) {
|
|
222
|
+
const handler = this.pendingResponses.get(requestId);
|
|
223
|
+
if (handler) {
|
|
224
|
+
handler.completionFuture.resolve?.(payload);
|
|
225
|
+
this.pendingResponses.delete(requestId);
|
|
226
|
+
} else {
|
|
227
|
+
this.log.error('Response received for unexpected RPC request', requestId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @internal */
|
|
232
|
+
handleIncomingRpcResponseFailure(requestId: string, error: RpcError) {
|
|
233
|
+
const handler = this.pendingResponses.get(requestId);
|
|
234
|
+
if (handler) {
|
|
235
|
+
handler.completionFuture.reject?.(error);
|
|
236
|
+
this.pendingResponses.delete(requestId);
|
|
237
|
+
} else {
|
|
238
|
+
this.log.error('Response received for unexpected RPC request', requestId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @internal */
|
|
243
|
+
handleIncomingRpcAck(requestId: string) {
|
|
244
|
+
const handler = this.pendingAcks.get(requestId);
|
|
245
|
+
if (handler) {
|
|
246
|
+
handler.resolve();
|
|
247
|
+
this.pendingAcks.delete(requestId);
|
|
248
|
+
} else {
|
|
249
|
+
this.log.error(`Ack received for unexpected RPC request: ${requestId}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** @internal */
|
|
254
|
+
handleParticipantDisconnected(participantIdentity: string) {
|
|
255
|
+
for (const [id, { participantIdentity: pendingIdentity }] of this.pendingAcks) {
|
|
256
|
+
if (pendingIdentity === participantIdentity) {
|
|
257
|
+
this.pendingAcks.delete(id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const [id, { participantIdentity: pendingIdentity, completionFuture }] of this
|
|
262
|
+
.pendingResponses) {
|
|
263
|
+
if (pendingIdentity === participantIdentity) {
|
|
264
|
+
completionFuture.reject?.(RpcError.builtIn('RECIPIENT_DISCONNECTED'));
|
|
265
|
+
this.pendingResponses.delete(id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|