livekit-client 2.5.9 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. package/README.md +54 -0
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +500 -5114
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +519 -127
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/room/PCTransport.d.ts +2 -0
  14. package/dist/src/room/PCTransport.d.ts.map +1 -1
  15. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/participant/LocalParticipant.d.ts +56 -0
  19. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  20. package/dist/src/room/rpc.d.ts +96 -0
  21. package/dist/src/room/rpc.d.ts.map +1 -0
  22. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  23. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  24. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/utils.d.ts +2 -2
  26. package/dist/src/room/track/utils.d.ts.map +1 -1
  27. package/dist/src/room/utils.d.ts +0 -10
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/ts4.2/src/index.d.ts +4 -1
  30. package/dist/ts4.2/src/room/PCTransport.d.ts +2 -0
  31. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +56 -0
  32. package/dist/ts4.2/src/room/rpc.d.ts +96 -0
  33. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -1
  34. package/dist/ts4.2/src/room/track/utils.d.ts +2 -2
  35. package/dist/ts4.2/src/room/utils.d.ts +0 -10
  36. package/package.json +3 -2
  37. package/src/api/SignalClient.ts +2 -1
  38. package/src/index.ts +3 -1
  39. package/src/room/PCTransport.ts +42 -29
  40. package/src/room/PCTransportManager.ts +2 -1
  41. package/src/room/RTCEngine.ts +3 -1
  42. package/src/room/Room.ts +2 -1
  43. package/src/room/participant/LocalParticipant.test.ts +304 -0
  44. package/src/room/participant/LocalParticipant.ts +340 -1
  45. package/src/room/rpc.ts +172 -0
  46. package/src/room/track/LocalTrack.ts +2 -1
  47. package/src/room/track/LocalVideoTrack.ts +2 -1
  48. package/src/room/track/options.ts +5 -5
  49. package/src/room/track/utils.ts +1 -6
  50. package/src/room/utils.ts +0 -38
  51. package/src/utils/AsyncQueue.test.ts +2 -2
  52. package/src/utils/AsyncQueue.ts +1 -1
@@ -0,0 +1,96 @@
1
+ import { RpcError as RpcError_Proto } from '@livekit/protocol';
2
+ /** Parameters for initiating an RPC call */
3
+ export interface PerformRpcParams {
4
+ /** The `identity` of the destination participant */
5
+ destinationIdentity: string;
6
+ /** The method name to call */
7
+ method: string;
8
+ /** The method payload */
9
+ payload: string;
10
+ /** Timeout for receiving a response after initial connection (milliseconds). Default: 10000 */
11
+ responseTimeout?: number;
12
+ }
13
+ /**
14
+ * Data passed to method handler for incoming RPC invocations
15
+ */
16
+ export interface RpcInvocationData {
17
+ /**
18
+ * The unique request ID. Will match at both sides of the call, useful for debugging or logging.
19
+ */
20
+ requestId: string;
21
+ /**
22
+ * The unique participant identity of the caller.
23
+ */
24
+ callerIdentity: string;
25
+ /**
26
+ * The payload of the request. User-definable format, typically JSON.
27
+ */
28
+ payload: string;
29
+ /**
30
+ * The maximum time the caller will wait for a response.
31
+ */
32
+ responseTimeout: number;
33
+ }
34
+ /**
35
+ * Specialized error handling for RPC methods.
36
+ *
37
+ * Instances of this type, when thrown in a method handler, will have their `message`
38
+ * serialized and sent across the wire. The sender will receive an equivalent error on the other side.
39
+ *
40
+ * Built-in types are included but developers may use any string, with a max length of 256 bytes.
41
+ */
42
+ export declare class RpcError extends Error {
43
+ static MAX_MESSAGE_BYTES: number;
44
+ static MAX_DATA_BYTES: number;
45
+ code: number;
46
+ data?: string;
47
+ /**
48
+ * Creates an error object with the given code and message, plus an optional data payload.
49
+ *
50
+ * If thrown in an RPC method handler, the error will be sent back to the caller.
51
+ *
52
+ * Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings).
53
+ */
54
+ constructor(code: number, message: string, data?: string);
55
+ /**
56
+ * @internal
57
+ */
58
+ static fromProto(proto: RpcError_Proto): RpcError;
59
+ /**
60
+ * @internal
61
+ */
62
+ toProto(): RpcError_Proto;
63
+ static ErrorCode: {
64
+ readonly APPLICATION_ERROR: 1500;
65
+ readonly CONNECTION_TIMEOUT: 1501;
66
+ readonly RESPONSE_TIMEOUT: 1502;
67
+ readonly RECIPIENT_DISCONNECTED: 1503;
68
+ readonly RESPONSE_PAYLOAD_TOO_LARGE: 1504;
69
+ readonly SEND_FAILED: 1505;
70
+ readonly UNSUPPORTED_METHOD: 1400;
71
+ readonly RECIPIENT_NOT_FOUND: 1401;
72
+ readonly REQUEST_PAYLOAD_TOO_LARGE: 1402;
73
+ readonly UNSUPPORTED_SERVER: 1403;
74
+ readonly UNSUPPORTED_VERSION: 1404;
75
+ };
76
+ /**
77
+ * @internal
78
+ */
79
+ static ErrorMessage: Record<keyof typeof RpcError.ErrorCode, string>;
80
+ /**
81
+ * Creates an error object from the code, with an auto-populated message.
82
+ *
83
+ * @internal
84
+ */
85
+ static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError;
86
+ }
87
+ export declare const MAX_PAYLOAD_BYTES = 15360;
88
+ /**
89
+ * @internal
90
+ */
91
+ export declare function byteLength(str: string): number;
92
+ /**
93
+ * @internal
94
+ */
95
+ export declare function truncateBytes(str: string, maxBytes: number): string;
96
+ //# sourceMappingURL=rpc.d.ts.map
@@ -1,5 +1,5 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import type { LoggerOptions } from '../types';
2
- import { Mutex } from '../utils';
3
3
  import { Track } from './Track';
4
4
  import type { VideoCodec } from './options';
5
5
  import type { TrackProcessor } from './processor/types';
@@ -1,7 +1,7 @@
1
1
  import { TrackPublishedResponse } from '@livekit/protocol';
2
2
  import { Track } from './Track';
3
3
  import type { TrackPublication } from './TrackPublication';
4
- import type { AudioCaptureOptions, CreateLocalTracksOptions, ScreenShareCaptureOptions, VideoCaptureOptions } from './options';
4
+ import type { AudioCaptureOptions, CreateLocalTracksOptions, ScreenShareCaptureOptions, VideoCaptureOptions, VideoCodec } from './options';
5
5
  import type { AudioTrack } from './types';
6
6
  export declare function mergeDefaultOptions(options?: CreateLocalTracksOptions, audioDefaults?: AudioCaptureOptions, videoDefaults?: VideoCaptureOptions): CreateLocalTracksOptions;
7
7
  export declare function constraintsForOptions(options: CreateLocalTracksOptions): MediaStreamConstraints;
@@ -26,7 +26,7 @@ export declare function sourceToKind(source: Track.Source): MediaDeviceKind | un
26
26
  * @internal
27
27
  */
28
28
  export declare function screenCaptureToDisplayMediaStreamOptions(options: ScreenShareCaptureOptions): DisplayMediaStreamOptions;
29
- export declare function mimeTypeToVideoCodecString(mimeType: string): "vp8" | "h264" | "vp9" | "av1";
29
+ export declare function mimeTypeToVideoCodecString(mimeType: string): VideoCodec;
30
30
  export declare function getTrackPublicationInfo<T extends TrackPublication>(tracks: T[]): TrackPublishedResponse[];
31
31
  export declare function getLogContextFromTrack(track: Track | TrackPublication): Record<string, unknown>;
32
32
  export declare function supportsSynchronizationSources(): boolean;
@@ -80,16 +80,6 @@ export declare function createAudioAnalyser(track: LocalAudioTrack | RemoteAudio
80
80
  analyser: AnalyserNode;
81
81
  cleanup: () => Promise<void>;
82
82
  };
83
- /**
84
- * @internal
85
- */
86
- export declare class Mutex {
87
- private _locking;
88
- private _locks;
89
- constructor();
90
- isLocked(): boolean;
91
- lock(): Promise<() => void>;
92
- }
93
83
  export declare function isVideoCodec(maybeCodec: string): maybeCodec is VideoCodec;
94
84
  export declare function unwrapConstraint(constraint: ConstrainDOMString): string;
95
85
  export declare function unwrapConstraint(constraint: ConstrainULong): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.5.9",
3
+ "version": "2.6.0",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -36,6 +36,7 @@
36
36
  "author": "David Zhao <david@davidzhao.com>",
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
+ "@livekit/mutex": "1.0.0",
39
40
  "@livekit/protocol": "1.24.0",
40
41
  "events": "^3.3.0",
41
42
  "loglevel": "^1.8.0",
@@ -87,7 +88,7 @@
87
88
  "build": "rollup --config --bundleConfigAsCjs && rollup --config rollup.config.worker.js --bundleConfigAsCjs && pnpm downlevel-dts",
88
89
  "build:watch": "rollup --watch --config --bundleConfigAsCjs",
89
90
  "build:worker:watch": "rollup --watch --config rollup.config.worker.js --bundleConfigAsCjs",
90
- "build-docs": "typedoc",
91
+ "build-docs": "typedoc && mkdir -p docs/assets/github && cp .github/*.png docs/assets/github/ && find docs -name '*.html' -type f -exec sed -i.bak 's|=\"/.github/|=\"assets/github/|g' {} + && find docs -name '*.bak' -delete",
91
92
  "proto": "protoc --es_out src/proto --es_opt target=ts -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto",
92
93
  "examples:demo": "vite examples/demo -c vite.config.mjs",
93
94
  "lint": "eslint src",
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import {
2
3
  AddTrackRequest,
3
4
  AudioTrackFeature,
@@ -42,7 +43,7 @@ import log, { LoggerNames, getLogger } from '../logger';
42
43
  import { ConnectionError, ConnectionErrorReason } from '../room/errors';
43
44
  import CriticalTimers from '../room/timers';
44
45
  import type { LoggerOptions } from '../room/types';
45
- import { Mutex, getClientInfo, isReactNative, sleep, toWebsocketUrl } from '../room/utils';
46
+ import { getClientInfo, isReactNative, sleep, toWebsocketUrl } from '../room/utils';
46
47
  import { AsyncQueue } from '../utils/AsyncQueue';
47
48
 
48
49
  // internal options
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import { DataPacket_Kind, DisconnectReason, SubscriptionError } from '@livekit/protocol';
2
3
  import { LogLevel, LoggerNames, getLogger, setLogExtension, setLogLevel } from './logger';
3
4
  import DefaultReconnectPolicy from './room/DefaultReconnectPolicy';
@@ -26,7 +27,6 @@ import { TrackPublication } from './room/track/TrackPublication';
26
27
  import type { LiveKitReactNativeInfo } from './room/types';
27
28
  import type { AudioAnalyserOptions } from './room/utils';
28
29
  import {
29
- Mutex,
30
30
  compareVersions,
31
31
  createAudioAnalyser,
32
32
  getEmptyAudioStreamTrack,
@@ -39,6 +39,8 @@ import {
39
39
  } from './room/utils';
40
40
  import { getBrowser } from './utils/browserParser';
41
41
 
42
+ export { RpcError, type RpcInvocationData, type PerformRpcParams } from './room/rpc';
43
+
42
44
  export * from './connectionHelper/ConnectionCheck';
43
45
  export * from './connectionHelper/checks/Checker';
44
46
  export * from './e2ee';
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
- import type { MediaDescription } from 'sdp-transform';
2
+ import type { MediaDescription, SessionDescription } from 'sdp-transform';
3
3
  import { parse, write } from 'sdp-transform';
4
4
  import { debounce } from 'ts-debounce';
5
5
  import log, { LoggerNames, getLogger } from '../logger';
@@ -48,6 +48,8 @@ export default class PCTransport extends EventEmitter {
48
48
 
49
49
  private loggerOptions: LoggerOptions;
50
50
 
51
+ private ddExtID = 0;
52
+
51
53
  pendingCandidates: RTCIceCandidateInit[] = [];
52
54
 
53
55
  restartingIce: boolean = false;
@@ -288,7 +290,7 @@ export default class PCTransport extends EventEmitter {
288
290
  }
289
291
 
290
292
  if (isSVCCodec(trackbr.codec)) {
291
- ensureVideoDDExtensionForSVC(media);
293
+ this.ensureVideoDDExtensionForSVC(media, sdpParsed);
292
294
  }
293
295
 
294
296
  // TODO: av1 slow starting issue already fixed in chrome 124, clean this after some versions
@@ -503,6 +505,44 @@ export default class PCTransport extends EventEmitter {
503
505
  throw new NegotiationError(msg);
504
506
  }
505
507
  }
508
+
509
+ private ensureVideoDDExtensionForSVC(
510
+ media: {
511
+ type: string;
512
+ port: number;
513
+ protocol: string;
514
+ payloads?: string | undefined;
515
+ } & MediaDescription,
516
+ sdp: SessionDescription,
517
+ ) {
518
+ const ddFound = media.ext?.some((ext): boolean => {
519
+ if (ext.uri === ddExtensionURI) {
520
+ return true;
521
+ }
522
+ return false;
523
+ });
524
+
525
+ if (!ddFound) {
526
+ if (this.ddExtID === 0) {
527
+ let maxID = 0;
528
+ sdp.media.forEach((m) => {
529
+ if (m.type !== 'video') {
530
+ return;
531
+ }
532
+ m.ext?.forEach((ext) => {
533
+ if (ext.value > maxID) {
534
+ maxID = ext.value;
535
+ }
536
+ });
537
+ });
538
+ this.ddExtID = maxID + 1;
539
+ }
540
+ media.ext?.push({
541
+ value: this.ddExtID,
542
+ uri: ddExtensionURI,
543
+ });
544
+ }
545
+ }
506
546
  }
507
547
 
508
548
  function ensureAudioNackAndStereo(
@@ -555,33 +595,6 @@ function ensureAudioNackAndStereo(
555
595
  }
556
596
  }
557
597
 
558
- function ensureVideoDDExtensionForSVC(
559
- media: {
560
- type: string;
561
- port: number;
562
- protocol: string;
563
- payloads?: string | undefined;
564
- } & MediaDescription,
565
- ) {
566
- let maxID = 0;
567
- const ddFound = media.ext?.some((ext): boolean => {
568
- if (ext.uri === ddExtensionURI) {
569
- return true;
570
- }
571
- if (ext.value > maxID) {
572
- maxID = ext.value;
573
- }
574
- return false;
575
- });
576
-
577
- if (!ddFound) {
578
- media.ext?.push({
579
- value: maxID + 1,
580
- uri: ddExtensionURI,
581
- });
582
- }
583
- }
584
-
585
598
  function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
586
599
  stereoMids: string[];
587
600
  nackMids: string[];
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import { SignalTarget } from '@livekit/protocol';
2
3
  import log, { LoggerNames, getLogger } from '../logger';
3
4
  import PCTransport, { PCEvents } from './PCTransport';
@@ -5,7 +6,7 @@ import { roomConnectOptionDefaults } from './defaults';
5
6
  import { ConnectionError, ConnectionErrorReason } from './errors';
6
7
  import CriticalTimers from './timers';
7
8
  import type { LoggerOptions } from './types';
8
- import { Mutex, sleep } from './utils';
9
+ import { sleep } from './utils';
9
10
 
10
11
  export enum PCTransportState {
11
12
  NEW,
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import {
2
3
  type AddTrackRequest,
3
4
  ClientConfigSetting,
@@ -63,7 +64,7 @@ import type { Track } from './track/Track';
63
64
  import type { TrackPublishOptions, VideoCodec } from './track/options';
64
65
  import { getTrackPublicationInfo } from './track/utils';
65
66
  import type { LoggerOptions } from './types';
66
- import { Mutex, isVideoCodec, isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';
67
+ import { isVideoCodec, isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';
67
68
 
68
69
  const lossyDataChannel = '_lossy';
69
70
  const reliableDataChannel = '_reliable';
@@ -262,6 +263,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
262
263
  }
263
264
  try {
264
265
  this._isClosed = true;
266
+ this.joinAttempts = 0;
265
267
  this.emit(EngineEvent.Closing);
266
268
  this.removeAllListeners();
267
269
  this.deregisterOnLineListener();
package/src/room/Room.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import {
2
3
  ChatMessage as ChatMessageModel,
3
4
  ConnectionQualityUpdate,
@@ -77,7 +78,6 @@ import type {
77
78
  } from './types';
78
79
  import {
79
80
  Future,
80
- Mutex,
81
81
  createDummyVideoStreamTrack,
82
82
  extractChatMessage,
83
83
  extractTranscriptionSegments,
@@ -1414,6 +1414,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1414
1414
  participant.unpublishTrack(publication.trackSid, true);
1415
1415
  });
1416
1416
  this.emit(RoomEvent.ParticipantDisconnected, participant);
1417
+ this.localParticipant?.handleParticipantDisconnected(participant.identity);
1417
1418
  }
1418
1419
 
1419
1420
  // updates are sent only when there's a change to speaker ordering
@@ -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
+ });