livekit-client 2.5.9 → 2.6.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.
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
+ });