livekit-client 2.5.10 → 2.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. package/README.md +54 -0
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +431 -45
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/api/SignalClient.d.ts.map +1 -1
  9. package/dist/src/index.d.ts +1 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/room/PCTransport.d.ts +2 -0
  12. package/dist/src/room/PCTransport.d.ts.map +1 -1
  13. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  16. package/dist/src/room/Room.d.ts.map +1 -1
  17. package/dist/src/room/errors.d.ts +2 -2
  18. package/dist/src/room/errors.d.ts.map +1 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts +56 -0
  20. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  21. package/dist/src/room/rpc.d.ts +96 -0
  22. package/dist/src/room/rpc.d.ts.map +1 -0
  23. package/dist/src/room/track/RemoteAudioTrack.d.ts +1 -1
  24. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
  26. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  27. package/dist/src/room/track/utils.d.ts +2 -2
  28. package/dist/src/room/track/utils.d.ts.map +1 -1
  29. package/dist/ts4.2/src/index.d.ts +2 -0
  30. package/dist/ts4.2/src/room/PCTransport.d.ts +2 -0
  31. package/dist/ts4.2/src/room/errors.d.ts +2 -2
  32. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +56 -0
  33. package/dist/ts4.2/src/room/rpc.d.ts +96 -0
  34. package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +1 -1
  35. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
  36. package/dist/ts4.2/src/room/track/utils.d.ts +2 -2
  37. package/package.json +2 -2
  38. package/src/api/SignalClient.ts +19 -3
  39. package/src/index.ts +2 -0
  40. package/src/room/PCTransport.ts +42 -29
  41. package/src/room/PCTransportManager.ts +6 -1
  42. package/src/room/RTCEngine.ts +13 -3
  43. package/src/room/RegionUrlProvider.ts +3 -1
  44. package/src/room/Room.ts +9 -3
  45. package/src/room/errors.ts +2 -2
  46. package/src/room/participant/LocalParticipant.test.ts +304 -0
  47. package/src/room/participant/LocalParticipant.ts +340 -1
  48. package/src/room/rpc.ts +172 -0
  49. package/src/room/track/RemoteAudioTrack.ts +1 -1
  50. package/src/room/track/RemoteVideoTrack.ts +1 -1
  51. package/src/room/track/utils.ts +1 -6
@@ -0,0 +1,304 @@
1
+ import { DataPacket, DataPacket_Kind } from '@livekit/protocol';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import type { InternalRoomOptions } from '../../options';
4
+ import type RTCEngine from '../RTCEngine';
5
+ import { RpcError } from '../rpc';
6
+ import LocalParticipant from './LocalParticipant';
7
+ import { ParticipantKind } from './Participant';
8
+ import RemoteParticipant from './RemoteParticipant';
9
+
10
+ describe('LocalParticipant', () => {
11
+ describe('registerRpcMethod', () => {
12
+ let localParticipant: LocalParticipant;
13
+ let mockEngine: RTCEngine;
14
+ let mockRoomOptions: InternalRoomOptions;
15
+ let mockSendDataPacket: ReturnType<typeof vi.fn>;
16
+
17
+ beforeEach(() => {
18
+ mockSendDataPacket = vi.fn();
19
+ mockEngine = {
20
+ client: {
21
+ sendUpdateLocalMetadata: vi.fn(),
22
+ },
23
+ on: vi.fn().mockReturnThis(),
24
+ sendDataPacket: mockSendDataPacket,
25
+ } as unknown as RTCEngine;
26
+
27
+ mockRoomOptions = {} as InternalRoomOptions;
28
+
29
+ localParticipant = new LocalParticipant(
30
+ 'test-sid',
31
+ 'test-identity',
32
+ mockEngine,
33
+ mockRoomOptions,
34
+ );
35
+ });
36
+
37
+ it('should register an RPC method handler', async () => {
38
+ const methodName = 'testMethod';
39
+ const handler = vi.fn().mockResolvedValue('test response');
40
+
41
+ localParticipant.registerRpcMethod(methodName, handler);
42
+
43
+ const mockCaller = new RemoteParticipant(
44
+ {} as any,
45
+ 'remote-sid',
46
+ 'remote-identity',
47
+ 'Remote Participant',
48
+ '',
49
+ undefined,
50
+ ParticipantKind.STANDARD,
51
+ );
52
+
53
+ await localParticipant.handleIncomingRpcRequest(
54
+ mockCaller.identity,
55
+ 'test-request-id',
56
+ methodName,
57
+ 'test payload',
58
+ 5000,
59
+ 1,
60
+ );
61
+
62
+ expect(handler).toHaveBeenCalledWith({
63
+ requestId: 'test-request-id',
64
+ callerIdentity: mockCaller.identity,
65
+ payload: 'test payload',
66
+ responseTimeout: 5000,
67
+ });
68
+
69
+ // Check if sendDataPacket was called twice (once for ACK and once for response)
70
+ expect(mockSendDataPacket).toHaveBeenCalledTimes(2);
71
+
72
+ // Check if the first call was for ACK
73
+ expect(mockSendDataPacket.mock.calls[0][0].value.case).toBe('rpcAck');
74
+ expect(mockSendDataPacket.mock.calls[0][1]).toBe(DataPacket_Kind.RELIABLE);
75
+
76
+ // Check if the second call was for response
77
+ expect(mockSendDataPacket.mock.calls[1][0].value.case).toBe('rpcResponse');
78
+ expect(mockSendDataPacket.mock.calls[1][1]).toBe(DataPacket_Kind.RELIABLE);
79
+ });
80
+
81
+ it('should catch and transform unhandled errors in the RPC method handler', async () => {
82
+ const methodName = 'errorMethod';
83
+ const errorMessage = 'Test error';
84
+ const handler = vi.fn().mockRejectedValue(new Error(errorMessage));
85
+
86
+ localParticipant.registerRpcMethod(methodName, handler);
87
+
88
+ const mockCaller = new RemoteParticipant(
89
+ {} as any,
90
+ 'remote-sid',
91
+ 'remote-identity',
92
+ 'Remote Participant',
93
+ '',
94
+ undefined,
95
+ ParticipantKind.STANDARD,
96
+ );
97
+
98
+ await localParticipant.handleIncomingRpcRequest(
99
+ mockCaller.identity,
100
+ 'test-error-request-id',
101
+ methodName,
102
+ 'test payload',
103
+ 5000,
104
+ 1,
105
+ );
106
+
107
+ expect(handler).toHaveBeenCalledWith({
108
+ requestId: 'test-error-request-id',
109
+ callerIdentity: mockCaller.identity,
110
+ payload: 'test payload',
111
+ responseTimeout: 5000,
112
+ });
113
+
114
+ // Check if sendDataPacket was called twice (once for ACK and once for error response)
115
+ expect(mockSendDataPacket).toHaveBeenCalledTimes(2);
116
+
117
+ // Check if the second call was for error response
118
+ const errorResponse = mockSendDataPacket.mock.calls[1][0].value.value.value.value;
119
+ expect(errorResponse.code).toBe(RpcError.ErrorCode.APPLICATION_ERROR);
120
+ });
121
+
122
+ it('should pass through RpcError thrown by the RPC method handler', async () => {
123
+ const methodName = 'rpcErrorMethod';
124
+ const errorCode = 101;
125
+ const errorMessage = 'some-error-message';
126
+ const handler = vi.fn().mockRejectedValue(new RpcError(errorCode, errorMessage));
127
+
128
+ localParticipant.registerRpcMethod(methodName, handler);
129
+
130
+ const mockCaller = new RemoteParticipant(
131
+ {} as any,
132
+ 'remote-sid',
133
+ 'remote-identity',
134
+ 'Remote Participant',
135
+ '',
136
+ undefined,
137
+ ParticipantKind.STANDARD,
138
+ );
139
+
140
+ await localParticipant.handleIncomingRpcRequest(
141
+ mockCaller.identity,
142
+ 'test-rpc-error-request-id',
143
+ methodName,
144
+ 'test payload',
145
+ 5000,
146
+ 1,
147
+ );
148
+
149
+ expect(handler).toHaveBeenCalledWith({
150
+ requestId: 'test-rpc-error-request-id',
151
+ callerIdentity: mockCaller.identity,
152
+ payload: 'test payload',
153
+ responseTimeout: 5000,
154
+ });
155
+
156
+ // Check if sendDataPacket was called twice (once for ACK and once for error response)
157
+ expect(mockSendDataPacket).toHaveBeenCalledTimes(2);
158
+
159
+ // Check if the second call was for error response
160
+ const errorResponse = mockSendDataPacket.mock.calls[1][0].value.value.value.value;
161
+ expect(errorResponse.code).toBe(errorCode);
162
+ expect(errorResponse.message).toBe(errorMessage);
163
+ });
164
+ });
165
+
166
+ describe('performRpc', () => {
167
+ let localParticipant: LocalParticipant;
168
+ let mockRemoteParticipant: RemoteParticipant;
169
+ let mockEngine: RTCEngine;
170
+ let mockRoomOptions: InternalRoomOptions;
171
+ let mockSendDataPacket: ReturnType<typeof vi.fn>;
172
+
173
+ beforeEach(() => {
174
+ mockSendDataPacket = vi.fn();
175
+ mockEngine = {
176
+ client: {
177
+ sendUpdateLocalMetadata: vi.fn(),
178
+ },
179
+ on: vi.fn().mockReturnThis(),
180
+ sendDataPacket: mockSendDataPacket,
181
+ } as unknown as RTCEngine;
182
+
183
+ mockRoomOptions = {} as InternalRoomOptions;
184
+
185
+ localParticipant = new LocalParticipant(
186
+ 'local-sid',
187
+ 'local-identity',
188
+ mockEngine,
189
+ mockRoomOptions,
190
+ );
191
+
192
+ mockRemoteParticipant = new RemoteParticipant(
193
+ {} as any,
194
+ 'remote-sid',
195
+ 'remote-identity',
196
+ 'Remote Participant',
197
+ '',
198
+ undefined,
199
+ ParticipantKind.STANDARD,
200
+ );
201
+ });
202
+
203
+ it('should send RPC request and receive successful response', async () => {
204
+ const method = 'testMethod';
205
+ const payload = 'testPayload';
206
+ const responsePayload = 'responsePayload';
207
+
208
+ mockSendDataPacket.mockImplementationOnce((packet: DataPacket) => {
209
+ const requestId = packet.value.value.id;
210
+ setTimeout(() => {
211
+ localParticipant.handleIncomingRpcAck(requestId);
212
+ setTimeout(() => {
213
+ localParticipant.handleIncomingRpcResponse(requestId, responsePayload, null);
214
+ }, 10);
215
+ }, 10);
216
+ });
217
+
218
+ const result = await localParticipant.performRpc({
219
+ destinationIdentity: mockRemoteParticipant.identity,
220
+ method,
221
+ payload,
222
+ });
223
+
224
+ expect(mockSendDataPacket).toHaveBeenCalledTimes(1);
225
+ expect(result).toBe(responsePayload);
226
+ });
227
+
228
+ it('should handle RPC request timeout', async () => {
229
+ const method = 'timeoutMethod';
230
+ const payload = 'timeoutPayload';
231
+
232
+ const timeout = 50;
233
+
234
+ const resultPromise = localParticipant.performRpc({
235
+ destinationIdentity: mockRemoteParticipant.identity,
236
+ method,
237
+ payload,
238
+ responseTimeout: timeout,
239
+ });
240
+
241
+ mockSendDataPacket.mockImplementationOnce(() => {
242
+ return new Promise((resolve) => {
243
+ setTimeout(resolve, timeout + 10);
244
+ });
245
+ });
246
+
247
+ const startTime = Date.now();
248
+
249
+ await expect(resultPromise).rejects.toThrow('Response timeout');
250
+
251
+ const elapsedTime = Date.now() - startTime;
252
+ expect(elapsedTime).toBeGreaterThanOrEqual(timeout);
253
+ expect(elapsedTime).toBeLessThan(timeout + 50); // Allow some margin for test execution
254
+
255
+ expect(mockSendDataPacket).toHaveBeenCalledTimes(1);
256
+ });
257
+
258
+ it('should handle RPC error response', async () => {
259
+ const method = 'errorMethod';
260
+ const payload = 'errorPayload';
261
+ const errorCode = 101;
262
+ const errorMessage = 'Test error message';
263
+
264
+ mockSendDataPacket.mockImplementationOnce((packet: DataPacket) => {
265
+ const requestId = packet.value.value.id;
266
+ setTimeout(() => {
267
+ localParticipant.handleIncomingRpcAck(requestId);
268
+ localParticipant.handleIncomingRpcResponse(
269
+ requestId,
270
+ null,
271
+ new RpcError(errorCode, errorMessage),
272
+ );
273
+ }, 10);
274
+ });
275
+
276
+ await expect(
277
+ localParticipant.performRpc({
278
+ destinationIdentity: mockRemoteParticipant.identity,
279
+ method,
280
+ payload,
281
+ }),
282
+ ).rejects.toThrow(errorMessage);
283
+ });
284
+
285
+ it('should handle participant disconnection during RPC request', async () => {
286
+ const method = 'disconnectMethod';
287
+ const payload = 'disconnectPayload';
288
+
289
+ mockSendDataPacket.mockImplementationOnce(() => Promise.resolve());
290
+
291
+ const resultPromise = localParticipant.performRpc({
292
+ destinationIdentity: mockRemoteParticipant.identity,
293
+ method,
294
+ payload,
295
+ });
296
+
297
+ // Simulate a small delay before disconnection
298
+ await new Promise((resolve) => setTimeout(resolve, 200));
299
+ localParticipant.handleParticipantDisconnected(mockRemoteParticipant.identity);
300
+
301
+ await expect(resultPromise).rejects.toThrow('Recipient disconnected');
302
+ });
303
+ });
304
+ });
@@ -9,6 +9,9 @@ import {
9
9
  ParticipantPermission,
10
10
  RequestResponse,
11
11
  RequestResponse_Reason,
12
+ RpcAck,
13
+ RpcRequest,
14
+ RpcResponse,
12
15
  SimulcastCodec,
13
16
  SipDTMF,
14
17
  SubscribedQualityUpdate,
@@ -29,6 +32,13 @@ import {
29
32
  UnexpectedConnectionState,
30
33
  } from '../errors';
31
34
  import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
35
+ import {
36
+ MAX_PAYLOAD_BYTES,
37
+ type PerformRpcParams,
38
+ RpcError,
39
+ type RpcInvocationData,
40
+ byteLength,
41
+ } from '../rpc';
32
42
  import LocalAudioTrack from '../track/LocalAudioTrack';
33
43
  import LocalTrack from '../track/LocalTrack';
34
44
  import LocalTrackPublication from '../track/LocalTrackPublication';
@@ -54,6 +64,7 @@ import {
54
64
  import type { ChatMessage, DataPublishOptions } from '../types';
55
65
  import {
56
66
  Future,
67
+ compareVersions,
57
68
  isE2EESimulcastSupported,
58
69
  isFireFox,
59
70
  isSVCCodec,
@@ -119,6 +130,18 @@ export default class LocalParticipant extends Participant {
119
130
 
120
131
  private enabledPublishVideoCodecs: Codec[] = [];
121
132
 
133
+ private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
134
+
135
+ private pendingAcks = new Map<string, { resolve: () => void; participantIdentity: string }>();
136
+
137
+ private pendingResponses = new Map<
138
+ string,
139
+ {
140
+ resolve: (payload: string | null, error: RpcError | null) => void;
141
+ participantIdentity: string;
142
+ }
143
+ >();
144
+
122
145
  /** @internal */
123
146
  constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
124
147
  super(sid, identity, undefined, undefined, {
@@ -187,7 +210,8 @@ export default class LocalParticipant extends Participant {
187
210
  .on(EngineEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
188
211
  .on(EngineEvent.SubscribedQualityUpdate, this.handleSubscribedQualityUpdate)
189
212
  .on(EngineEvent.Disconnected, this.handleDisconnected)
190
- .on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse);
213
+ .on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse)
214
+ .on(EngineEvent.DataPacketReceived, this.handleDataPacket);
191
215
  }
192
216
 
193
217
  private handleReconnecting = () => {
@@ -221,6 +245,38 @@ export default class LocalParticipant extends Participant {
221
245
  }
222
246
  };
223
247
 
248
+ private handleDataPacket = (packet: DataPacket) => {
249
+ switch (packet.value.case) {
250
+ case 'rpcRequest':
251
+ let rpcRequest = packet.value.value as RpcRequest;
252
+ this.handleIncomingRpcRequest(
253
+ packet.participantIdentity,
254
+ rpcRequest.id,
255
+ rpcRequest.method,
256
+ rpcRequest.payload,
257
+ rpcRequest.responseTimeoutMs,
258
+ rpcRequest.version,
259
+ );
260
+ break;
261
+ case 'rpcResponse':
262
+ let rpcResponse = packet.value.value as RpcResponse;
263
+ let payload: string | null = null;
264
+ let error: RpcError | null = null;
265
+
266
+ if (rpcResponse.value.case === 'payload') {
267
+ payload = rpcResponse.value.value;
268
+ } else if (rpcResponse.value.case === 'error') {
269
+ error = RpcError.fromProto(rpcResponse.value.value);
270
+ }
271
+ this.handleIncomingRpcResponse(rpcResponse.requestId, payload, error);
272
+ break;
273
+ case 'rpcAck':
274
+ let rpcAck = packet.value.value as RpcAck;
275
+ this.handleIncomingRpcAck(rpcAck.requestId);
276
+ break;
277
+ }
278
+ };
279
+
224
280
  /**
225
281
  * Sets and updates the metadata of the local participant.
226
282
  * Note: this requires `canUpdateOwnMetadata` permission.
@@ -1415,6 +1471,121 @@ export default class LocalParticipant extends Participant {
1415
1471
  return msg;
1416
1472
  }
1417
1473
 
1474
+ /**
1475
+ * Initiate an RPC call to a remote participant
1476
+ * @param params - Parameters for initiating the RPC call, see {@link PerformRpcParams}
1477
+ * @returns A promise that resolves with the response payload or rejects with an error.
1478
+ * @throws Error on failure. Details in `message`.
1479
+ */
1480
+ async performRpc({
1481
+ destinationIdentity,
1482
+ method,
1483
+ payload,
1484
+ responseTimeout = 10000,
1485
+ }: PerformRpcParams): Promise<string> {
1486
+ const maxRoundTripLatency = 2000;
1487
+
1488
+ return new Promise(async (resolve, reject) => {
1489
+ if (byteLength(payload) > MAX_PAYLOAD_BYTES) {
1490
+ reject(RpcError.builtIn('REQUEST_PAYLOAD_TOO_LARGE'));
1491
+ return;
1492
+ }
1493
+
1494
+ if (
1495
+ this.engine.latestJoinResponse?.serverInfo?.version &&
1496
+ compareVersions(this.engine.latestJoinResponse?.serverInfo?.version, '1.8.0') < 0
1497
+ ) {
1498
+ reject(RpcError.builtIn('UNSUPPORTED_SERVER'));
1499
+ return;
1500
+ }
1501
+
1502
+ const id = crypto.randomUUID();
1503
+ await this.publishRpcRequest(
1504
+ destinationIdentity,
1505
+ id,
1506
+ method,
1507
+ payload,
1508
+ responseTimeout - maxRoundTripLatency,
1509
+ );
1510
+
1511
+ const ackTimeoutId = setTimeout(() => {
1512
+ this.pendingAcks.delete(id);
1513
+ reject(RpcError.builtIn('CONNECTION_TIMEOUT'));
1514
+ this.pendingResponses.delete(id);
1515
+ clearTimeout(responseTimeoutId);
1516
+ }, maxRoundTripLatency);
1517
+
1518
+ this.pendingAcks.set(id, {
1519
+ resolve: () => {
1520
+ clearTimeout(ackTimeoutId);
1521
+ },
1522
+ participantIdentity: destinationIdentity,
1523
+ });
1524
+
1525
+ const responseTimeoutId = setTimeout(() => {
1526
+ this.pendingResponses.delete(id);
1527
+ reject(RpcError.builtIn('RESPONSE_TIMEOUT'));
1528
+ }, responseTimeout);
1529
+
1530
+ this.pendingResponses.set(id, {
1531
+ resolve: (responsePayload: string | null, responseError: RpcError | null) => {
1532
+ clearTimeout(responseTimeoutId);
1533
+ if (this.pendingAcks.has(id)) {
1534
+ console.warn('RPC response received before ack', id);
1535
+ this.pendingAcks.delete(id);
1536
+ clearTimeout(ackTimeoutId);
1537
+ }
1538
+
1539
+ if (responseError) {
1540
+ reject(responseError);
1541
+ } else {
1542
+ resolve(responsePayload ?? '');
1543
+ }
1544
+ },
1545
+ participantIdentity: destinationIdentity,
1546
+ });
1547
+ });
1548
+ }
1549
+
1550
+ /**
1551
+ * Establishes the participant as a receiver for calls of the specified RPC method.
1552
+ * Will overwrite any existing callback for the same method.
1553
+ *
1554
+ * @param method - The name of the indicated RPC method
1555
+ * @param handler - Will be invoked when an RPC request for this method is received
1556
+ * @returns A promise that resolves when the method is successfully registered
1557
+ *
1558
+ * @example
1559
+ * ```typescript
1560
+ * room.localParticipant?.registerRpcMethod(
1561
+ * 'greet',
1562
+ * async (data: RpcInvocationData) => {
1563
+ * console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`);
1564
+ * return `Hello, ${data.callerIdentity}!`;
1565
+ * }
1566
+ * );
1567
+ * ```
1568
+ *
1569
+ * The handler should return a Promise that resolves to a string.
1570
+ * If unable to respond within `responseTimeout`, the request will result in an error on the caller's side.
1571
+ *
1572
+ * You may throw errors of type `RpcError` with a string `message` in the handler,
1573
+ * and they will be received on the caller's side with the message intact.
1574
+ * Other errors thrown in your handler will not be transmitted as-is, and will instead arrive to the caller as `1500` ("Application Error").
1575
+ */
1576
+ registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
1577
+ this.rpcHandlers.set(method, handler);
1578
+ }
1579
+
1580
+ /**
1581
+ * Unregisters a previously registered RPC method.
1582
+ *
1583
+ * @param method - The name of the RPC method to unregister
1584
+ */
1585
+ unregisterRpcMethod(method: string) {
1586
+ this.rpcHandlers.delete(method);
1587
+ }
1588
+
1418
1589
  /**
1419
1590
  * Control who can subscribe to LocalParticipant's published tracks.
1420
1591
  *
@@ -1443,6 +1614,174 @@ export default class LocalParticipant extends Participant {
1443
1614
  }
1444
1615
  }
1445
1616
 
1617
+ private handleIncomingRpcAck(requestId: string) {
1618
+ const handler = this.pendingAcks.get(requestId);
1619
+ if (handler) {
1620
+ handler.resolve();
1621
+ this.pendingAcks.delete(requestId);
1622
+ } else {
1623
+ console.error('Ack received for unexpected RPC request', requestId);
1624
+ }
1625
+ }
1626
+
1627
+ private handleIncomingRpcResponse(
1628
+ requestId: string,
1629
+ payload: string | null,
1630
+ error: RpcError | null,
1631
+ ) {
1632
+ const handler = this.pendingResponses.get(requestId);
1633
+ if (handler) {
1634
+ handler.resolve(payload, error);
1635
+ this.pendingResponses.delete(requestId);
1636
+ } else {
1637
+ console.error('Response received for unexpected RPC request', requestId);
1638
+ }
1639
+ }
1640
+
1641
+ private async handleIncomingRpcRequest(
1642
+ callerIdentity: string,
1643
+ requestId: string,
1644
+ method: string,
1645
+ payload: string,
1646
+ responseTimeout: number,
1647
+ version: number,
1648
+ ) {
1649
+ await this.publishRpcAck(callerIdentity, requestId);
1650
+
1651
+ if (version !== 1) {
1652
+ await this.publishRpcResponse(
1653
+ callerIdentity,
1654
+ requestId,
1655
+ null,
1656
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
1657
+ );
1658
+ return;
1659
+ }
1660
+
1661
+ const handler = this.rpcHandlers.get(method);
1662
+
1663
+ if (!handler) {
1664
+ await this.publishRpcResponse(
1665
+ callerIdentity,
1666
+ requestId,
1667
+ null,
1668
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
1669
+ );
1670
+ return;
1671
+ }
1672
+
1673
+ let responseError: RpcError | null = null;
1674
+ let responsePayload: string | null = null;
1675
+
1676
+ try {
1677
+ const response = await handler({
1678
+ requestId,
1679
+ callerIdentity,
1680
+ payload,
1681
+ responseTimeout,
1682
+ });
1683
+ if (byteLength(response) > MAX_PAYLOAD_BYTES) {
1684
+ responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
1685
+ console.warn(`RPC Response payload too large for ${method}`);
1686
+ } else {
1687
+ responsePayload = response;
1688
+ }
1689
+ } catch (error) {
1690
+ if (error instanceof RpcError) {
1691
+ responseError = error;
1692
+ } else {
1693
+ console.warn(
1694
+ `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
1695
+ error,
1696
+ );
1697
+ responseError = RpcError.builtIn('APPLICATION_ERROR');
1698
+ }
1699
+ }
1700
+ await this.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
1701
+ }
1702
+
1703
+ /** @internal */
1704
+ private async publishRpcRequest(
1705
+ destinationIdentity: string,
1706
+ requestId: string,
1707
+ method: string,
1708
+ payload: string,
1709
+ responseTimeout: number,
1710
+ ) {
1711
+ const packet = new DataPacket({
1712
+ destinationIdentities: [destinationIdentity],
1713
+ kind: DataPacket_Kind.RELIABLE,
1714
+ value: {
1715
+ case: 'rpcRequest',
1716
+ value: new RpcRequest({
1717
+ id: requestId,
1718
+ method,
1719
+ payload,
1720
+ responseTimeoutMs: responseTimeout,
1721
+ version: 1,
1722
+ }),
1723
+ },
1724
+ });
1725
+
1726
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1727
+ }
1728
+
1729
+ /** @internal */
1730
+ private async publishRpcResponse(
1731
+ destinationIdentity: string,
1732
+ requestId: string,
1733
+ payload: string | null,
1734
+ error: RpcError | null,
1735
+ ) {
1736
+ const packet = new DataPacket({
1737
+ destinationIdentities: [destinationIdentity],
1738
+ kind: DataPacket_Kind.RELIABLE,
1739
+ value: {
1740
+ case: 'rpcResponse',
1741
+ value: new RpcResponse({
1742
+ requestId,
1743
+ value: error
1744
+ ? { case: 'error', value: error.toProto() }
1745
+ : { case: 'payload', value: payload ?? '' },
1746
+ }),
1747
+ },
1748
+ });
1749
+
1750
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1751
+ }
1752
+
1753
+ /** @internal */
1754
+ private async publishRpcAck(destinationIdentity: string, requestId: string) {
1755
+ const packet = new DataPacket({
1756
+ destinationIdentities: [destinationIdentity],
1757
+ kind: DataPacket_Kind.RELIABLE,
1758
+ value: {
1759
+ case: 'rpcAck',
1760
+ value: new RpcAck({
1761
+ requestId,
1762
+ }),
1763
+ },
1764
+ });
1765
+
1766
+ await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1767
+ }
1768
+
1769
+ /** @internal */
1770
+ handleParticipantDisconnected(participantIdentity: string) {
1771
+ for (const [id, { participantIdentity: pendingIdentity }] of this.pendingAcks) {
1772
+ if (pendingIdentity === participantIdentity) {
1773
+ this.pendingAcks.delete(id);
1774
+ }
1775
+ }
1776
+
1777
+ for (const [id, { participantIdentity: pendingIdentity, resolve }] of this.pendingResponses) {
1778
+ if (pendingIdentity === participantIdentity) {
1779
+ resolve(null, RpcError.builtIn('RECIPIENT_DISCONNECTED'));
1780
+ this.pendingResponses.delete(id);
1781
+ }
1782
+ }
1783
+ }
1784
+
1446
1785
  /** @internal */
1447
1786
  setEnabledPublishCodecs(codecs: Codec[]) {
1448
1787
  this.enabledPublishVideoCodecs = codecs.filter(