livekit-client 2.22.2 → 2.22.4

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 (245) hide show
  1. package/README.md +35 -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 +488 -438
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +954 -301
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +10 -2
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/api/SignalClient.d.ts +4 -2
  15. package/dist/src/api/SignalClient.d.ts.map +1 -1
  16. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  17. package/dist/src/api/utils.d.ts +1 -0
  18. package/dist/src/api/utils.d.ts.map +1 -1
  19. package/dist/src/e2ee/E2eeManager.d.ts +26 -0
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/types.d.ts +15 -1
  22. package/dist/src/e2ee/types.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  25. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
  26. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -18
  27. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  28. package/dist/src/logger.d.ts +4 -0
  29. package/dist/src/logger.d.ts.map +1 -1
  30. package/dist/src/options.d.ts +14 -0
  31. package/dist/src/options.d.ts.map +1 -1
  32. package/dist/src/room/PCTransport.d.ts +78 -7
  33. package/dist/src/room/PCTransport.d.ts.map +1 -1
  34. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  35. package/dist/src/room/RTCEngine.d.ts +9 -0
  36. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  37. package/dist/src/room/Room.d.ts +1 -1
  38. package/dist/src/room/Room.d.ts.map +1 -1
  39. package/dist/src/room/defaults.d.ts.map +1 -1
  40. package/dist/src/room/errors.d.ts +18 -0
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/events.d.ts +3 -2
  43. package/dist/src/room/events.d.ts.map +1 -1
  44. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  45. package/dist/src/room/participant/publishUtils.d.ts +16 -0
  46. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  47. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  48. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  49. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  50. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  51. package/dist/src/room/track/options.d.ts +1 -1
  52. package/dist/src/room/track/utils.d.ts +13 -0
  53. package/dist/src/room/track/utils.d.ts.map +1 -1
  54. package/dist/src/room/utils.d.ts +66 -0
  55. package/dist/src/room/utils.d.ts.map +1 -1
  56. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  57. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  58. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  59. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  60. package/dist/ts4.2/api/WebSocketStream.d.ts +33 -33
  61. package/dist/ts4.2/api/utils.d.ts +9 -8
  62. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  63. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  64. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  65. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  66. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  67. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  68. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  69. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  70. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  71. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  72. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -85
  73. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  74. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  75. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  76. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  77. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  78. package/dist/ts4.2/e2ee/types.d.ts +231 -217
  79. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  80. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  81. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  82. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -226
  83. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  84. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  85. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +28 -28
  86. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  87. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  88. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  89. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  90. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  91. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  92. package/dist/ts4.2/index.d.ts +76 -76
  93. package/dist/ts4.2/logger.d.ts +66 -62
  94. package/dist/ts4.2/options.d.ts +163 -149
  95. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  96. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  97. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  98. package/dist/ts4.2/room/PCTransport.d.ts +246 -175
  99. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  100. package/dist/ts4.2/room/RTCEngine.d.ts +269 -261
  101. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  102. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  103. package/dist/ts4.2/room/Room.d.ts +354 -365
  104. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  105. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
  106. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -95
  107. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  108. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  109. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  110. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  111. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  112. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  113. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  114. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  115. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  116. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  117. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  118. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  119. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  120. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  121. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  122. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  123. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  124. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  125. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  126. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  127. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  128. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  129. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  130. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  131. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  132. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  133. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  134. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  135. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  136. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  137. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  138. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  139. package/dist/ts4.2/room/debounce.d.ts +11 -11
  140. package/dist/ts4.2/room/defaults.d.ts +9 -9
  141. package/dist/ts4.2/room/errors.d.ts +180 -162
  142. package/dist/ts4.2/room/events.d.ts +638 -637
  143. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +287 -287
  144. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  145. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  146. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  147. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -37
  148. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  149. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  150. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  151. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  152. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  153. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  154. package/dist/ts4.2/room/stats.d.ts +71 -71
  155. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  156. package/dist/ts4.2/room/timers.d.ts +12 -12
  157. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  158. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  159. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  160. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  161. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +39 -42
  162. package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
  163. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  164. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -93
  165. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -55
  166. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  167. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  168. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  169. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  170. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  171. package/dist/ts4.2/room/track/create.d.ts +25 -25
  172. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  173. package/dist/ts4.2/room/track/options.d.ts +403 -403
  174. package/dist/ts4.2/room/track/processor/types.d.ts +31 -31
  175. package/dist/ts4.2/room/track/record.d.ts +13 -13
  176. package/dist/ts4.2/room/track/types.d.ts +28 -28
  177. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  178. package/dist/ts4.2/room/types.d.ts +127 -127
  179. package/dist/ts4.2/room/utils.d.ts +246 -180
  180. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  181. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  182. package/dist/ts4.2/test/mocks.d.ts +12 -12
  183. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  184. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  185. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  186. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  187. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  188. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  189. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  190. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  191. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  192. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  193. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  194. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  195. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  196. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  197. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  198. package/dist/ts4.2/version.d.ts +15 -15
  199. package/package.json +5 -4
  200. package/src/api/SignalClient.e2e.test.ts +1 -3
  201. package/src/api/SignalClient.test.ts +160 -10
  202. package/src/api/SignalClient.ts +75 -54
  203. package/src/api/WebSocketStream.ts +3 -8
  204. package/src/api/utils.ts +10 -0
  205. package/src/e2ee/E2eeManager.test.ts +196 -0
  206. package/src/e2ee/E2eeManager.ts +114 -19
  207. package/src/e2ee/types.ts +19 -1
  208. package/src/e2ee/worker/DataCryptor.ts +2 -1
  209. package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
  210. package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
  211. package/src/e2ee/worker/FrameCryptor.ts +20 -70
  212. package/src/e2ee/worker/e2ee.worker.ts +34 -11
  213. package/src/logger.ts +22 -0
  214. package/src/options.ts +16 -0
  215. package/src/room/PCTransport.test.ts +183 -7
  216. package/src/room/PCTransport.ts +197 -36
  217. package/src/room/PCTransportManager.ts +3 -1
  218. package/src/room/RTCEngine.test.ts +84 -0
  219. package/src/room/RTCEngine.ts +52 -12
  220. package/src/room/Room.test.ts +32 -6
  221. package/src/room/Room.ts +27 -7
  222. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
  223. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -5
  224. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  225. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  226. package/src/room/defaults.ts +1 -0
  227. package/src/room/errors.test.ts +36 -0
  228. package/src/room/errors.ts +27 -0
  229. package/src/room/events.ts +2 -1
  230. package/src/room/participant/LocalParticipant.ts +30 -14
  231. package/src/room/participant/publishUtils.test.ts +133 -0
  232. package/src/room/participant/publishUtils.ts +54 -38
  233. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  234. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  235. package/src/room/track/LocalTrack.ts +18 -7
  236. package/src/room/track/LocalVideoTrack.ts +25 -8
  237. package/src/room/track/RemoteAudioTrack.ts +3 -3
  238. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  239. package/src/room/track/RemoteVideoTrack.ts +29 -3
  240. package/src/room/track/options.ts +1 -1
  241. package/src/room/track/utils.test.ts +201 -2
  242. package/src/room/track/utils.ts +88 -0
  243. package/src/room/utils.test.ts +211 -0
  244. package/src/room/utils.ts +129 -0
  245. package/src/test/compressionStreamStubs.ts +53 -0
@@ -0,0 +1,196 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { LogLevel, getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger';
3
+ import Room from '../room/Room';
4
+ import { E2EEManager } from './E2eeManager';
5
+ import { BaseKeyProvider } from './KeyProvider';
6
+
7
+ /**
8
+ * Install just enough of the DOM to let isE2EESupported() return true so
9
+ * setup() doesn't throw.
10
+ */
11
+ function installE2EEShims() {
12
+ const w = window as unknown as Record<string, any>;
13
+ if (typeof w.RTCRtpSender === 'undefined') {
14
+ w.RTCRtpSender = class {};
15
+ }
16
+ w.RTCRtpSender.prototype.createEncodedStreams = () => {};
17
+ }
18
+
19
+ class FakeWorker {
20
+ postMessage = vi.fn();
21
+
22
+ onmessage: unknown = null;
23
+
24
+ onerror: unknown = null;
25
+
26
+ levelMessages(): LogLevel[] {
27
+ return this.postMessage.mock.calls
28
+ .map(([m]) => m)
29
+ .filter((m: any) => m?.kind === 'setLogLevel')
30
+ .map((m: any) => m.data.level);
31
+ }
32
+ }
33
+
34
+ function makeManager() {
35
+ installE2EEShims();
36
+ const room = new Room();
37
+ const worker = new FakeWorker();
38
+ const manager = new E2EEManager(
39
+ { keyProvider: new BaseKeyProvider({ sharedKey: true }), worker: worker as unknown as Worker },
40
+ false,
41
+ );
42
+ return { room, worker, manager };
43
+ }
44
+
45
+ describe('E2EEManager log-level listener lifecycle', () => {
46
+ const startingLevel = workerLogger.getLevel();
47
+ const startingCount = getWorkerLogLevelListenerCount();
48
+
49
+ afterEach(() => {
50
+ setLogLevel(startingLevel);
51
+ });
52
+
53
+ it('forwards level changes to the worker while subscribed', () => {
54
+ const { room, worker, manager } = makeManager();
55
+ manager.setup(room);
56
+ worker.postMessage.mockClear();
57
+
58
+ setLogLevel(LogLevel.debug);
59
+
60
+ expect(worker.levelMessages()).toEqual([LogLevel.debug]);
61
+ manager.dispose();
62
+ });
63
+
64
+ it('dispose() removes the listener and stops forwarding', () => {
65
+ const { room, worker, manager } = makeManager();
66
+ manager.setup(room);
67
+ manager.dispose();
68
+ worker.postMessage.mockClear();
69
+
70
+ setLogLevel(LogLevel.warn);
71
+
72
+ expect(worker.postMessage).not.toHaveBeenCalled();
73
+ expect(getWorkerLogLevelListenerCount()).toBe(startingCount);
74
+ });
75
+
76
+ it('re-setup with a new room does not stack listeners', () => {
77
+ const { worker, manager } = makeManager();
78
+ const roomA = new Room();
79
+ const roomB = new Room();
80
+ manager.setup(roomA);
81
+ const countAfterFirst = getWorkerLogLevelListenerCount();
82
+ manager.setup(roomB);
83
+ expect(getWorkerLogLevelListenerCount()).toBe(countAfterFirst);
84
+
85
+ worker.postMessage.mockClear();
86
+ setLogLevel(LogLevel.debug);
87
+ expect(worker.levelMessages()).toEqual([LogLevel.debug]); // exactly one delivery
88
+
89
+ manager.dispose();
90
+ });
91
+
92
+ it('dispose() is idempotent', () => {
93
+ const { room, manager } = makeManager();
94
+ manager.setup(room);
95
+ manager.dispose();
96
+ manager.dispose();
97
+ expect(getWorkerLogLevelListenerCount()).toBe(startingCount);
98
+ });
99
+
100
+ it('dispose() rejects pending encrypt/decrypt futures and clears both maps', async () => {
101
+ const { room, manager } = makeManager();
102
+ manager.setup(room);
103
+
104
+ const encrypting = manager.encryptData(new Uint8Array([1, 2, 3]) as any);
105
+ const decrypting = manager.handleEncryptedData(
106
+ new Uint8Array([4, 5, 6]) as any,
107
+ new Uint8Array([7, 8, 9]) as any,
108
+ 'peer',
109
+ 0,
110
+ );
111
+
112
+ const priv = manager as unknown as {
113
+ encryptDataRequests: Map<string, unknown>;
114
+ decryptDataRequests: Map<string, unknown>;
115
+ };
116
+ expect(priv.encryptDataRequests.size).toBe(1);
117
+ expect(priv.decryptDataRequests.size).toBe(1);
118
+
119
+ manager.dispose();
120
+
121
+ await expect(encrypting).rejects.toThrow(/disposed/);
122
+ await expect(decrypting).rejects.toThrow(/disposed/);
123
+ expect(priv.encryptDataRequests.size).toBe(0);
124
+ expect(priv.decryptDataRequests.size).toBe(0);
125
+
126
+ // Second dispose while maps are empty must not throw.
127
+ expect(() => manager.dispose()).not.toThrow();
128
+ });
129
+ });
130
+
131
+ /**
132
+ * GC-path test. Flaky by construction — FinalizationRegistry callbacks are
133
+ * best-effort. Skipped unless vitest is run with `--expose-gc`:
134
+ *
135
+ * NODE_OPTIONS="--expose-gc" pnpm exec vitest run src/e2ee/E2eeManager.test.ts
136
+ *
137
+ * Deliberately bypasses `manager.setup(room)`. `new Room()` on its own is not
138
+ * collectable in this test environment (device-change listeners, timers), and
139
+ * that leak is not what this test is about — it would only mask what we
140
+ * actually want to verify: that the log-level listener wiring holds nothing
141
+ * strongly.
142
+ */
143
+ describe('E2EEManager GC cleanup', () => {
144
+ const startingLevel = workerLogger.getLevel();
145
+
146
+ beforeEach(() => {
147
+ installE2EEShims();
148
+ });
149
+
150
+ afterEach(() => {
151
+ setLogLevel(startingLevel);
152
+ });
153
+
154
+ it.skipIf(!(globalThis as any).gc)(
155
+ 'releases the log-level listener when the manager is garbage collected',
156
+ async () => {
157
+ const before = getWorkerLogLevelListenerCount();
158
+
159
+ // Construct + subscribe in an IIFE so nothing lives on the test's stack.
160
+ // Direct call to the private subscription — no Room, no leaky graph.
161
+ const managerRef = ((): WeakRef<E2EEManager> => {
162
+ const worker = new FakeWorker();
163
+ const manager = new E2EEManager(
164
+ {
165
+ keyProvider: new BaseKeyProvider({ sharedKey: true }),
166
+ worker: worker as unknown as Worker,
167
+ },
168
+ false,
169
+ );
170
+ (manager as unknown as { subscribeToLogLevelChanges(): void }).subscribeToLogLevelChanges();
171
+ expect(getWorkerLogLevelListenerCount()).toBe(before + 1);
172
+ return new WeakRef(manager);
173
+ })();
174
+
175
+ // Full major GC + macrotask yield in a loop, with allocation pressure to
176
+ // force the major sweep FinalizationRegistry needs.
177
+ //
178
+ // Crucial: do NOT call `managerRef.deref()` inside the loop. Per spec,
179
+ // `WeakRef.prototype.deref` keeps the referent alive until the end of the
180
+ // current job — calling it in the check would pin the manager forever.
181
+ // Read the listener count (which does not touch the referent) instead.
182
+ const gc = (globalThis as any).gc as (opts?: { type?: 'major'; execution?: 'sync' }) => void;
183
+ for (let i = 0; i < 50; i++) {
184
+ // eslint-disable-next-line no-void
185
+ void new Array(100_000).fill({ i });
186
+ gc({ type: 'major', execution: 'sync' });
187
+ await new Promise((r) => setImmediate(r));
188
+ if (getWorkerLogLevelListenerCount() === before) break;
189
+ }
190
+
191
+ // Diagnostic: separate "manager wasn't collected" from "FR didn't fire".
192
+ expect(managerRef.deref(), 'manager was not collected — strong ref leaked').toBeUndefined();
193
+ expect(getWorkerLogLevelListenerCount()).toBe(before);
194
+ },
195
+ );
196
+ });
@@ -3,7 +3,7 @@ import { EventEmitter } from 'events';
3
3
  import type TypedEventEmitter from 'typed-emitter';
4
4
  import type { FrameMetadata } from '../frameMetadata/types';
5
5
  import { hasFrameMetadataPublishOptions } from '../frameMetadata/utils';
6
- import log, { LogLevel, workerLogger } from '../logger';
6
+ import { LogLevel, LoggerNames, getLogger, onWorkerLogLevelChanged, workerLogger } from '../logger';
7
7
  import type RTCEngine from '../room/RTCEngine';
8
8
  import type Room from '../room/Room';
9
9
  import { ConnectionState } from '../room/Room';
@@ -25,6 +25,7 @@ import {
25
25
  import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
26
26
  import type { BaseKeyProvider } from './KeyProvider';
27
27
  import { E2EE_FLAG, E2EE_TRACK_ID } from './constants';
28
+ import { CryptorError, CryptorErrorReason } from './errors';
28
29
  import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
29
30
  import type {
30
31
  DecryptDataRequestMessage,
@@ -62,6 +63,7 @@ export interface BaseE2EEManager {
62
63
  keyIndex: number,
63
64
  ): Promise<DecryptDataResponseMessage['data']>;
64
65
  on<E extends keyof E2EEManagerCallbacks>(event: E, listener: E2EEManagerCallbacks[E]): this;
66
+ dispose?(): void;
65
67
  }
66
68
 
67
69
  /**
@@ -87,6 +89,29 @@ export class E2EEManager
87
89
 
88
90
  private dataChannelEncryptionEnabled: boolean;
89
91
 
92
+ private unsubscribeLogLevel?: () => void;
93
+
94
+ private log = getLogger(LoggerNames.E2EE, () => this.logContext);
95
+
96
+ get logContext() {
97
+ return {
98
+ room: this.room?.name,
99
+ participant: this.room?.localParticipant.identity,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Runs a cleanup callback once this manager is garbage collected. Lets the
105
+ * log-level listener (held in a module-global Set on the main-thread logger)
106
+ * fall out of scope even when the consumer forgets to call `dispose()`.
107
+ */
108
+ private static disposeRegistry =
109
+ typeof FinalizationRegistry !== 'undefined' &&
110
+ typeof WeakRef !== 'undefined' &&
111
+ new FinalizationRegistry((cleanup: () => void) => {
112
+ cleanup();
113
+ });
114
+
90
115
  constructor(options: E2EEManagerOptions, dcEncryptionEnabled: boolean) {
91
116
  super();
92
117
  this.keyProvider = options.keyProvider;
@@ -112,7 +137,7 @@ export class E2EEManager
112
137
  'tried to setup end-to-end encryption on an unsupported browser',
113
138
  );
114
139
  }
115
- log.info('setting up e2ee');
140
+ this.log.info('setting up e2ee');
116
141
  if (room !== this.room) {
117
142
  this.room = room;
118
143
  this.setupEventListeners(room, this.keyProvider);
@@ -125,19 +150,89 @@ export class E2EEManager
125
150
  },
126
151
  };
127
152
  if (this.worker) {
128
- log.info(`initializing worker`, { worker: this.worker });
153
+ this.log.info(`initializing worker`, { worker: this.worker });
129
154
  this.worker.onmessage = this.onWorkerMessage;
130
155
  this.worker.onerror = this.onWorkerError;
131
156
  this.worker.postMessage(msg);
157
+ this.subscribeToLogLevelChanges();
132
158
  }
133
159
  }
134
160
  }
135
161
 
162
+ /**
163
+ * Subscribe the current worker to main-thread `workerLogger` level changes,
164
+ * without strongly retaining `this` or `this.worker` from the module-global
165
+ * listener Set on the logger. See {@link disposeRegistry}.
166
+ */
167
+ private subscribeToLogLevelChanges() {
168
+ // Guard against duplicate registration on re-setup.
169
+ this.unsubscribeLogLevel?.();
170
+
171
+ let unsub: (() => void) | undefined;
172
+ if (E2EEManager.disposeRegistry) {
173
+ // Modern engines: hold the worker weakly so the module-global listener Set
174
+ // on the logger can't retain this manager, and clean up the entry on GC.
175
+ const workerRef = new WeakRef(this.worker);
176
+ unsub = onWorkerLogLevelChanged((level) => {
177
+ const worker = workerRef.deref();
178
+ if (!worker) {
179
+ unsub?.();
180
+ return;
181
+ }
182
+ worker.postMessage({ kind: 'setLogLevel', data: { level } });
183
+ });
184
+ E2EEManager.disposeRegistry.register(this, unsub, this);
185
+ } else {
186
+ // Safari <14.1 and similar: no WeakRef. Fall back to a strong reference;
187
+ // the leak lives until the consumer calls `dispose()`.
188
+ const worker = this.worker;
189
+ unsub = onWorkerLogLevelChanged((level) => {
190
+ worker.postMessage({ kind: 'setLogLevel', data: { level } });
191
+ });
192
+ }
193
+ this.unsubscribeLogLevel = unsub;
194
+ }
195
+
196
+ /**
197
+ * @internal
198
+ * Release the log-level subscription, reject any pending encrypt/decrypt
199
+ * futures, and detach the worker message handlers. The worker itself is
200
+ * caller-owned and is not terminated. Idempotent.
201
+ */
202
+ dispose() {
203
+ this.unsubscribeLogLevel?.();
204
+ this.unsubscribeLogLevel = undefined;
205
+ if (E2EEManager.disposeRegistry) {
206
+ E2EEManager.disposeRegistry.unregister(this);
207
+ }
208
+
209
+ // Reject pending futures BEFORE detaching worker handlers, so any late
210
+ // response can't resolve one after we've cut the pipe. Each future's
211
+ // `onFinally` deletes its own map entry, so both maps drain themselves.
212
+ // Snapshot before iterating in case a rejection handler mutates the map.
213
+ const disposalError = new CryptorError(
214
+ 'E2EEManager disposed',
215
+ CryptorErrorReason.InternalError,
216
+ );
217
+ for (const future of [...this.encryptDataRequests.values()]) {
218
+ future.reject?.(disposalError);
219
+ }
220
+ for (const future of [...this.decryptDataRequests.values()]) {
221
+ future.reject?.(disposalError);
222
+ }
223
+
224
+ if (this.worker) {
225
+ this.worker.onmessage = null;
226
+ this.worker.onerror = null;
227
+ }
228
+ this.removeAllListeners();
229
+ }
230
+
136
231
  /**
137
232
  * @internal
138
233
  */
139
234
  setParticipantCryptorEnabled(enabled: boolean, participantIdentity: string) {
140
- log.debug(`set e2ee to ${enabled} for participant ${participantIdentity}`);
235
+ this.log.debug(`set e2ee to ${enabled} for participant ${participantIdentity}`);
141
236
  this.postEnable(enabled, participantIdentity);
142
237
  }
143
238
 
@@ -146,7 +241,7 @@ export class E2EEManager
146
241
  */
147
242
  setSifTrailer(trailer: NonSharedUint8Array) {
148
243
  if (!trailer || trailer.length === 0) {
149
- log.warn("ignoring server sent trailer as it's empty");
244
+ this.log.warn("ignoring server sent trailer as it's empty");
150
245
  } else {
151
246
  this.postSifTrailer(trailer);
152
247
  }
@@ -156,25 +251,23 @@ export class E2EEManager
156
251
  const { kind, data } = ev.data;
157
252
  switch (kind) {
158
253
  case 'error':
159
- log.error(data.error.message);
160
-
161
- // If error has uuid, it's from an async operation (encrypt/decrypt)
162
- // Reject the corresponding future
254
+ // If error has uuid, it's from an async operation (encrypt/decrypt).
255
+ // Reject the corresponding future and let the caller decide how to log/handle;
256
+ // logging here would duplicate whatever the caller does.
163
257
  if (data.uuid) {
164
258
  const decryptFuture = this.decryptDataRequests.get(data.uuid);
165
259
  if (decryptFuture?.reject) {
166
260
  decryptFuture.reject(data.error);
167
- break; // Don't emit general error if it's handled by future
261
+ break;
168
262
  }
169
263
 
170
264
  const encryptFuture = this.encryptDataRequests.get(data.uuid);
171
265
  if (encryptFuture?.reject) {
172
266
  encryptFuture.reject(data.error);
173
- break; // Don't emit general error if it's handled by future
267
+ break;
174
268
  }
175
269
  }
176
-
177
- // Emit general error event for unhandled errors
270
+ this.log.error(data.error.message);
178
271
  this.emit(EncryptionEvent.EncryptionError, data.error, data.participantIdentity);
179
272
  break;
180
273
  case 'initAck':
@@ -235,13 +328,16 @@ export class E2EEManager
235
328
  case 'packetTrailerMetadata':
236
329
  this.handleFrameMetadata(data.trackId, data.rtpTimestamp, data.ssrc, data.metadata);
237
330
  break;
331
+ case 'log':
332
+ workerLogger[data.level](data.msg, data.context);
333
+ break;
238
334
  default:
239
335
  break;
240
336
  }
241
337
  };
242
338
 
243
339
  private onWorkerError = (ev: ErrorEvent) => {
244
- log.error('e2ee worker encountered an error:', { error: ev.error });
340
+ this.log.error('e2ee worker encountered an error:', { error: ev.error });
245
341
  this.emit(EncryptionEvent.EncryptionError, ev.error, undefined);
246
342
  };
247
343
 
@@ -488,8 +584,7 @@ export class E2EEManager
488
584
  participantIdentity: string,
489
585
  ) {
490
586
  if (!pub.trackInfo) {
491
- log.warn('skipping e2ee enabled update for publication without trackInfo', {
492
- participant: participantIdentity,
587
+ this.log.warn('skipping e2ee enabled update for publication without trackInfo', {
493
588
  trackSid: pub.trackSid,
494
589
  });
495
590
  return;
@@ -522,7 +617,7 @@ export class E2EEManager
522
617
 
523
618
  private setupE2EESender(track: Track, sender: RTCRtpSender) {
524
619
  if (!isLocalTrack(track) || !sender) {
525
- if (!sender) log.warn('early return because sender is not ready');
620
+ if (!sender) this.log.warn('early return because sender is not ready');
526
621
  return;
527
622
  }
528
623
  this.handleSender(
@@ -638,7 +733,7 @@ export class E2EEManager
638
733
  }
639
734
 
640
735
  if (isScriptTransformSupportedForWorker()) {
641
- log.info('initialize script transform');
736
+ this.log.info('initialize script transform');
642
737
  const options: ScriptTransformOptions = {
643
738
  kind: 'encode',
644
739
  participantIdentity: this.room.localParticipant.identity,
@@ -650,7 +745,7 @@ export class E2EEManager
650
745
  // @ts-ignore
651
746
  sender.transform = new RTCRtpScriptTransform(this.worker, options);
652
747
  } else {
653
- log.info('initialize encoded streams');
748
+ this.log.info('initialize encoded streams');
654
749
  // @ts-ignore
655
750
  const senderStreams = sender.createEncodedStreams();
656
751
  const msg: EncodeMessage = {
package/src/e2ee/types.ts CHANGED
@@ -176,6 +176,22 @@ export interface PTMetadataFromE2EEMessage extends BaseMessage {
176
176
  data: FrameMetadataPayload;
177
177
  }
178
178
 
179
+ export interface LogMessage extends BaseMessage {
180
+ kind: 'log';
181
+ data: {
182
+ level: 'trace' | 'debug' | 'info' | 'warn' | 'error';
183
+ msg: string;
184
+ context?: object;
185
+ };
186
+ }
187
+
188
+ export interface SetLogLevelMessage extends BaseMessage {
189
+ kind: 'setLogLevel';
190
+ data: {
191
+ level: LogLevel;
192
+ };
193
+ }
194
+
179
195
  export type E2EEWorkerMessage =
180
196
  | InitMessage
181
197
  | SetKeyMessage
@@ -193,7 +209,9 @@ export type E2EEWorkerMessage =
193
209
  | DecryptDataResponseMessage
194
210
  | EncryptDataRequestMessage
195
211
  | EncryptDataResponseMessage
196
- | PTMetadataFromE2EEMessage;
212
+ | PTMetadataFromE2EEMessage
213
+ | LogMessage
214
+ | SetLogLevelMessage;
197
215
 
198
216
  export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
199
217
 
@@ -1,3 +1,4 @@
1
+ import { getErrorDescription } from '../../api/utils';
1
2
  import { workerLogger } from '../../logger';
2
3
  import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
3
4
  import { ENCRYPTION_ALGORITHM } from '../constants';
@@ -135,7 +136,7 @@ export class DataCryptor {
135
136
  }
136
137
  } else {
137
138
  throw new CryptorError(
138
- `DataCryptor: Decryption failed: ${error.message}`,
139
+ `DataCryptor: Decryption failed: ${getErrorDescription(error, 'decryption')}`,
139
140
  CryptorErrorReason.InvalidKey,
140
141
  keys.participantIdentity,
141
142
  );
@@ -0,0 +1,53 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ErrorRateLimiter } from './ErrorRateLimiter';
3
+
4
+ describe('ErrorRateLimiter', () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ vi.setSystemTime(1_000_000);
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+
14
+ it('emits first call, throttles the immediate next, then allows after throttle window', () => {
15
+ const l = new ErrorRateLimiter(1000, 60_000, 5);
16
+ expect(l.shouldEmit('k')).toBe(true);
17
+ vi.setSystemTime(1_000_500);
18
+ expect(l.shouldEmit('k')).toBe(false);
19
+ vi.setSystemTime(1_001_600);
20
+ expect(l.shouldEmit('k')).toBe(true);
21
+ });
22
+
23
+ it('caps at maxPerWindow and invokes onSuppress once', () => {
24
+ const l = new ErrorRateLimiter(0, 60_000, 3);
25
+ const onSuppress = vi.fn();
26
+ // one free emit on window reset (count stays 0), then increments to 3
27
+ expect(l.shouldEmit('k', onSuppress)).toBe(true);
28
+ expect(l.shouldEmit('k', onSuppress)).toBe(true);
29
+ expect(l.shouldEmit('k', onSuppress)).toBe(true);
30
+ expect(l.shouldEmit('k', onSuppress)).toBe(true);
31
+ // now count == 3 == max → suppressed, callback fires once
32
+ expect(l.shouldEmit('k', onSuppress)).toBe(false);
33
+ expect(l.shouldEmit('k', onSuppress)).toBe(false);
34
+ expect(onSuppress).toHaveBeenCalledTimes(1);
35
+ });
36
+
37
+ it('resets count after windowMs', () => {
38
+ const l = new ErrorRateLimiter(0, 1000, 2);
39
+ l.shouldEmit('k');
40
+ l.shouldEmit('k');
41
+ l.shouldEmit('k');
42
+ expect(l.shouldEmit('k')).toBe(false);
43
+ vi.setSystemTime(1_003_000);
44
+ expect(l.shouldEmit('k')).toBe(true);
45
+ });
46
+
47
+ it('tracks keys independently', () => {
48
+ const l = new ErrorRateLimiter(1000);
49
+ expect(l.shouldEmit('a')).toBe(true);
50
+ expect(l.shouldEmit('b')).toBe(true);
51
+ expect(l.shouldEmit('a')).toBe(false);
52
+ });
53
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Per-key rate limiter for repeated errors. Prevents log/emit floods and the
3
+ * unbounded map growth that a per-event log would cause when a broken key
4
+ * keeps producing failures.
5
+ */
6
+ export class ErrorRateLimiter {
7
+ private lastAt: Map<string, number> = new Map();
8
+
9
+ private counts: Map<string, number> = new Map();
10
+
11
+ constructor(
12
+ private readonly throttleMs: number = 1000,
13
+ private readonly windowMs: number = 60_000,
14
+ private readonly maxPerWindow: number = 5,
15
+ ) {}
16
+
17
+ reset() {
18
+ this.lastAt.clear();
19
+ this.counts.clear();
20
+ }
21
+
22
+ countFor(key: string): number {
23
+ return this.counts.get(key) ?? 0;
24
+ }
25
+
26
+ /**
27
+ * Returns true if the caller should emit for this key. Invokes `onSuppress`
28
+ * exactly once per window when the per-window limit is first crossed.
29
+ */
30
+ shouldEmit(key: string, onSuppress?: () => void): boolean {
31
+ const now = Date.now();
32
+ const last = this.lastAt.get(key) ?? 0;
33
+ const count = this.counts.get(key) ?? 0;
34
+
35
+ if (now - last > this.windowMs) {
36
+ this.counts.set(key, 0);
37
+ this.lastAt.set(key, now);
38
+ return true;
39
+ }
40
+ if (now - last < this.throttleMs) return false;
41
+ if (count >= this.maxPerWindow) {
42
+ if (count === this.maxPerWindow) {
43
+ onSuppress?.();
44
+ this.counts.set(key, count + 1);
45
+ }
46
+ return false;
47
+ }
48
+ this.lastAt.set(key, now);
49
+ this.counts.set(key, count + 1);
50
+ return true;
51
+ }
52
+ }