livekit-client 2.22.3 → 2.23.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 (272) 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 +71 -8
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +1746 -547
  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 +2 -1
  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 +1 -0
  17. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  18. package/dist/src/api/utils.d.ts +1 -1
  19. package/dist/src/api/utils.d.ts.map +1 -1
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  22. package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
  23. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  24. package/dist/src/index.d.ts +2 -2
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/options.d.ts +14 -0
  27. package/dist/src/options.d.ts.map +1 -1
  28. package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
  29. package/dist/src/room/PCTransport.d.ts +100 -7
  30. package/dist/src/room/PCTransport.d.ts.map +1 -1
  31. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  32. package/dist/src/room/RTCEngine.d.ts +16 -0
  33. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  34. package/dist/src/room/Room.d.ts +54 -1
  35. package/dist/src/room/Room.d.ts.map +1 -1
  36. package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
  37. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
  38. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
  39. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
  40. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
  41. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
  42. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  43. package/dist/src/room/defaults.d.ts.map +1 -1
  44. package/dist/src/room/errors.d.ts +18 -0
  45. package/dist/src/room/errors.d.ts.map +1 -1
  46. package/dist/src/room/events.d.ts +3 -2
  47. package/dist/src/room/events.d.ts.map +1 -1
  48. package/dist/src/room/participant/LocalParticipant.d.ts +4 -0
  49. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  50. package/dist/src/room/participant/Participant.d.ts +1 -1
  51. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  52. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  53. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  54. package/dist/src/room/rpc/utils.d.ts.map +1 -1
  55. package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
  56. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/LocalTrack.d.ts +18 -0
  58. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  59. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  60. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  61. package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
  62. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  63. package/dist/src/room/track/processor/types.d.ts +11 -0
  64. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  65. package/dist/src/room/track/utils.d.ts +13 -0
  66. package/dist/src/room/track/utils.d.ts.map +1 -1
  67. package/dist/src/room/utils.d.ts +60 -1
  68. package/dist/src/room/utils.d.ts.map +1 -1
  69. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  70. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  71. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  72. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  73. package/dist/ts4.2/api/WebSocketStream.d.ts +34 -33
  74. package/dist/ts4.2/api/utils.d.ts +9 -9
  75. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  76. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  77. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  78. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  79. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  80. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  81. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  82. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  83. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  84. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  85. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
  86. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  87. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  88. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  89. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  90. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  91. package/dist/ts4.2/e2ee/types.d.ts +231 -231
  92. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  93. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  94. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
  95. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
  96. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  97. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  98. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +39 -28
  99. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  100. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  101. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  102. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  103. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  104. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  105. package/dist/ts4.2/index.d.ts +76 -76
  106. package/dist/ts4.2/logger.d.ts +66 -66
  107. package/dist/ts4.2/options.d.ts +163 -149
  108. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  109. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  110. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  111. package/dist/ts4.2/room/PCTransport.d.ts +268 -175
  112. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  113. package/dist/ts4.2/room/RTCEngine.d.ts +277 -262
  114. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  115. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  116. package/dist/ts4.2/room/Room.d.ts +407 -365
  117. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  118. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +63 -62
  119. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +107 -95
  120. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  121. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  122. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  123. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  124. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  125. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  126. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  127. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  128. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  129. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  130. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  131. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  132. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  133. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  134. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  135. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  136. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  137. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  138. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  139. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  140. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  141. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  142. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  143. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  144. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  145. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  146. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  147. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  148. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  149. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  150. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  151. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  152. package/dist/ts4.2/room/debounce.d.ts +11 -11
  153. package/dist/ts4.2/room/defaults.d.ts +9 -9
  154. package/dist/ts4.2/room/errors.d.ts +180 -162
  155. package/dist/ts4.2/room/events.d.ts +638 -637
  156. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +291 -287
  157. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  158. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  159. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  160. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
  161. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  162. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  163. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  164. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  165. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  166. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  167. package/dist/ts4.2/room/stats.d.ts +71 -71
  168. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  169. package/dist/ts4.2/room/timers.d.ts +12 -12
  170. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  171. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  172. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  173. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  174. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +62 -42
  175. package/dist/ts4.2/room/track/LocalTrack.d.ts +143 -125
  176. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  177. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +109 -100
  178. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +61 -55
  179. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  180. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  181. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  182. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  183. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  184. package/dist/ts4.2/room/track/create.d.ts +25 -25
  185. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  186. package/dist/ts4.2/room/track/options.d.ts +403 -403
  187. package/dist/ts4.2/room/track/processor/types.d.ts +42 -31
  188. package/dist/ts4.2/room/track/record.d.ts +13 -13
  189. package/dist/ts4.2/room/track/types.d.ts +28 -28
  190. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  191. package/dist/ts4.2/room/types.d.ts +127 -127
  192. package/dist/ts4.2/room/utils.d.ts +273 -214
  193. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  194. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  195. package/dist/ts4.2/test/mocks.d.ts +12 -12
  196. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  197. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  198. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  199. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  200. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  201. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  202. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  203. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  204. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  205. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  206. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  207. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  208. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  209. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  210. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  211. package/dist/ts4.2/version.d.ts +15 -15
  212. package/package.json +5 -4
  213. package/src/api/SignalClient.e2e.test.ts +1 -3
  214. package/src/api/SignalClient.test.ts +394 -10
  215. package/src/api/SignalClient.ts +132 -76
  216. package/src/api/WebSocketStream.test.ts +11 -0
  217. package/src/api/WebSocketStream.ts +22 -1
  218. package/src/api/utils.ts +1 -1
  219. package/src/e2ee/E2eeManager.test.ts +43 -0
  220. package/src/e2ee/E2eeManager.ts +6 -2
  221. package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
  222. package/src/e2ee/worker/FrameCryptor.ts +21 -4
  223. package/src/e2ee/worker/naluUtils.test.ts +123 -0
  224. package/src/e2ee/worker/naluUtils.ts +57 -2
  225. package/src/index.ts +2 -0
  226. package/src/options.ts +16 -0
  227. package/src/room/DefaultReconnectPolicy.test.ts +58 -0
  228. package/src/room/DefaultReconnectPolicy.ts +14 -2
  229. package/src/room/PCTransport.test.ts +327 -9
  230. package/src/room/PCTransport.ts +339 -36
  231. package/src/room/PCTransportManager.ts +3 -1
  232. package/src/room/RTCEngine.test.ts +318 -2
  233. package/src/room/RTCEngine.ts +126 -12
  234. package/src/room/Room.test.ts +148 -6
  235. package/src/room/Room.ts +312 -80
  236. package/src/room/data-channel/DataChannelManager.test.ts +24 -7
  237. package/src/room/data-channel/DataChannelManager.ts +43 -38
  238. package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
  239. package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
  240. package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
  241. package/src/room/data-channel/LossyDataChannel.ts +10 -1
  242. package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
  243. package/src/room/data-channel/ReliableDataChannel.ts +16 -5
  244. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +47 -20
  245. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +16 -5
  246. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  247. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  248. package/src/room/defaults.ts +1 -0
  249. package/src/room/errors.test.ts +36 -0
  250. package/src/room/errors.ts +27 -0
  251. package/src/room/events.ts +2 -1
  252. package/src/room/participant/LocalParticipant.test.ts +136 -1
  253. package/src/room/participant/LocalParticipant.ts +66 -17
  254. package/src/room/participant/Participant.ts +6 -3
  255. package/src/room/participant/publishUtils.ts +0 -19
  256. package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
  257. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  258. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  259. package/src/room/rpc/utils.test.ts +32 -0
  260. package/src/room/rpc/utils.ts +7 -0
  261. package/src/room/track/LocalAudioTrack.ts +101 -10
  262. package/src/room/track/LocalTrack.ts +87 -19
  263. package/src/room/track/LocalVideoTrack.ts +17 -3
  264. package/src/room/track/RemoteAudioTrack.ts +59 -14
  265. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  266. package/src/room/track/RemoteVideoTrack.ts +29 -3
  267. package/src/room/track/processor/types.ts +11 -0
  268. package/src/room/track/utils.test.ts +206 -2
  269. package/src/room/track/utils.ts +117 -5
  270. package/src/room/utils.test.ts +250 -0
  271. package/src/room/utils.ts +152 -19
  272. package/src/test/compressionStreamStubs.ts +53 -0
@@ -1,15 +1,22 @@
1
1
  import {
2
2
  DataPacket,
3
3
  DataPacket_Kind,
4
+ JoinResponse,
4
5
  ConnectionQuality as ProtoConnectionQuality,
5
6
  UserPacket,
6
7
  } from '@livekit/protocol';
7
8
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { SignalConnectionState, type SignalOptions } from '../api/SignalClient';
8
10
  import type { DataPacketBuffer } from '../utils/dataPacketBuffer';
11
+ import DefaultReconnectPolicy from './DefaultReconnectPolicy';
9
12
  import { PCTransportState } from './PCTransportManager';
10
13
  import RTCEngine, { DataChannelKind } from './RTCEngine';
14
+ import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
11
15
  import { roomOptionDefaults } from './defaults';
12
- import { PublishDataError, UnexpectedConnectionState } from './errors';
16
+ import { ConnectionError, PublishDataError, UnexpectedConnectionState } from './errors';
17
+ import { EngineEvent } from './events';
18
+ import CriticalTimers from './timers';
19
+ import { Future } from './utils';
13
20
 
14
21
  describe('RTCEngine', () => {
15
22
  const originalRTCRtpSender = window.RTCRtpSender;
@@ -309,7 +316,13 @@ describe('RTCEngine', () => {
309
316
 
310
317
  bufferedAmountLowThreshold = 64 * 1024;
311
318
 
319
+ readyState: RTCDataChannelState = 'open';
320
+
312
321
  send = vi.fn();
322
+
323
+ close = vi.fn(() => {
324
+ this.readyState = 'closing';
325
+ });
313
326
  }
314
327
 
315
328
  const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
@@ -536,6 +549,37 @@ describe('RTCEngine', () => {
536
549
  });
537
550
  });
538
551
 
552
+ describe('createDataChannels', () => {
553
+ it('makes the next sender wait for replacement channels to open', () => {
554
+ const engine = new RTCEngine(roomOptionDefaults);
555
+ const pcManager = {
556
+ createPublisherDataChannel: vi.fn((label: string) =>
557
+ Object.assign(new FakeDataChannel(), { label }),
558
+ ),
559
+ };
560
+ const connected = Promise.resolve();
561
+ Object.assign(engine as unknown as Record<string, unknown>, {
562
+ pcManager,
563
+ publisherConnectionPromise: connected,
564
+ });
565
+ const internals = engine as unknown as {
566
+ createDataChannels: () => void;
567
+ publisherConnectionPromise?: Promise<void>;
568
+ dataChannels: { lossy: { stopThresholdTuning: () => void } };
569
+ };
570
+
571
+ // First creation (nothing to replace) keeps the memoized readiness check.
572
+ internals.createDataChannels();
573
+ expect(internals.publisherConnectionPromise).toBe(connected);
574
+
575
+ // Recreation (the Safari null-id resume path) swaps in channels that start out connecting.
576
+ internals.createDataChannels();
577
+ expect(internals.publisherConnectionPromise).toBeUndefined();
578
+
579
+ internals.dataChannels.lossy.stopThresholdTuning();
580
+ });
581
+ });
582
+
539
583
  describe('handleDataChannelClose', () => {
540
584
  function stubCloseEnv(
541
585
  engine: RTCEngine,
@@ -700,6 +744,123 @@ describe('RTCEngine', () => {
700
744
  });
701
745
  });
702
746
 
747
+ describe('closing during a full reconnect', () => {
748
+ interface ReconnectInternals {
749
+ _isClosed: boolean;
750
+ url: string;
751
+ token: string;
752
+ signalOpts: SignalOptions;
753
+ attemptReconnect: () => Promise<void>;
754
+ configure: () => Promise<void>;
755
+ waitForPCReconnected: () => Promise<void>;
756
+ reconnectAbortController?: AbortController;
757
+ }
758
+
759
+ function primeEngine() {
760
+ const engine = new RTCEngine({ ...roomOptionDefaults, singlePeerConnection: false });
761
+ const internals = engine as unknown as ReconnectInternals;
762
+ internals._isClosed = false;
763
+ internals.url = 'wss://test.livekit.io';
764
+ internals.token = 'test-token';
765
+ internals.signalOpts = { autoSubscribe: true, maxRetries: 1, websocketTimeout: 15_000 };
766
+ engine.fullReconnectOnNext = true;
767
+ const configure = vi.spyOn(internals, 'configure').mockResolvedValue();
768
+ vi.spyOn(internals, 'waitForPCReconnected').mockResolvedValue();
769
+ const join = vi
770
+ .spyOn(engine.client, 'join')
771
+ .mockResolvedValue(new JoinResponse({ subscriberPrimary: true }));
772
+ return { engine, internals, configure, join };
773
+ }
774
+
775
+ it('does not join after close completes while reconnect cleanup is pending', async () => {
776
+ const { engine, internals, join } = primeEngine();
777
+ const cleanupStarted = new Future<void, Error>();
778
+ const finishCleanup = new Future<void, Error>();
779
+ vi.spyOn(engine, 'cleanupPeerConnections').mockImplementationOnce(async () => {
780
+ cleanupStarted.resolve?.();
781
+ await finishCleanup.promise;
782
+ });
783
+
784
+ const reconnect = internals.attemptReconnect();
785
+ await cleanupStarted.promise;
786
+ const controller = internals.reconnectAbortController;
787
+ expect(controller).toBeDefined();
788
+ await engine.close();
789
+ expect(controller?.signal.aborted).toBe(true);
790
+ expect(internals.reconnectAbortController).toBeUndefined();
791
+ finishCleanup.resolve?.();
792
+ await reconnect;
793
+
794
+ expect(internals.reconnectAbortController).toBeUndefined();
795
+ expect(join).not.toHaveBeenCalled();
796
+ expect(engine.isClosed).toBe(true);
797
+ });
798
+
799
+ it('does not join another region after close completes during region selection', async () => {
800
+ const { engine, internals, join } = primeEngine();
801
+ join.mockRejectedValue(ConnectionError.internal('signal connection failed'));
802
+ const selectingRegion = new Future<void, Error>();
803
+ const nextRegion = new Future<string | null, Error>();
804
+ const getNextUrl = vi.fn(async () => {
805
+ selectingRegion.resolve?.();
806
+ return nextRegion.promise;
807
+ });
808
+ engine.setRegionStrategy({ getNextUrl, resetAttempts: () => {} });
809
+
810
+ const reconnect = internals.attemptReconnect();
811
+ await selectingRegion.promise;
812
+ await engine.close();
813
+ nextRegion.resolve?.('wss://another-region.livekit.io');
814
+ // A second selection must terminate even on the unfixed implementation.
815
+ getNextUrl.mockResolvedValue(null);
816
+ await reconnect;
817
+
818
+ expect(join).toHaveBeenCalledTimes(1);
819
+ expect(getNextUrl).toHaveBeenCalledTimes(1);
820
+ expect(engine.isClosed).toBe(true);
821
+ });
822
+
823
+ it('does not reopen the engine when a signal join resolves after close', async () => {
824
+ const { engine, internals, configure, join } = primeEngine();
825
+ const joining = new Future<void, Error>();
826
+ const joinResponse = new Future<JoinResponse, Error>();
827
+ join.mockImplementationOnce(async () => {
828
+ joining.resolve?.();
829
+ return joinResponse.promise;
830
+ });
831
+
832
+ const reconnect = internals.attemptReconnect();
833
+ await joining.promise;
834
+ await engine.close();
835
+ joinResponse.resolve?.(new JoinResponse({ subscriberPrimary: true }));
836
+ await reconnect;
837
+
838
+ expect(engine.isClosed).toBe(true);
839
+ expect(configure).not.toHaveBeenCalled();
840
+ expect(join.mock.calls[0][3]?.aborted).toBe(true);
841
+ });
842
+
843
+ it('still completes a full reconnect while the engine is active', async () => {
844
+ const { engine, internals, join } = primeEngine();
845
+ vi.spyOn(engine.client, 'sendLeave').mockResolvedValue();
846
+ vi.spyOn(engine.client, 'currentState', 'get').mockReturnValue(
847
+ SignalConnectionState.CONNECTED,
848
+ );
849
+ const restarted = vi.fn();
850
+ engine.on(EngineEvent.Restarted, restarted);
851
+
852
+ try {
853
+ await internals.attemptReconnect();
854
+
855
+ expect(join).toHaveBeenCalledTimes(1);
856
+ expect(restarted).toHaveBeenCalledTimes(1);
857
+ expect(engine.isClosed).toBe(false);
858
+ } finally {
859
+ await engine.close();
860
+ }
861
+ });
862
+ });
863
+
703
864
  describe('reconnect requested mid-attempt', () => {
704
865
  // A full reconnect requested while a resume is already in flight (e.g. a server
705
866
  // RECONNECT leave racing the resume) sets `fullReconnectOnNext` mid-attempt. A
@@ -710,7 +871,7 @@ describe('RTCEngine', () => {
710
871
  clientConfiguration: unknown;
711
872
  pcManager: unknown;
712
873
  resumeConnection: (reason?: number) => Promise<void>;
713
- restartConnection: (regionUrl?: string) => Promise<void>;
874
+ restartConnection: (abortSignal: AbortSignal, regionUrl?: string) => Promise<void>;
714
875
  clearPendingReconnect: () => void;
715
876
  handleDisconnect: (connection: string, reason?: number) => void;
716
877
  attemptReconnect: (reason?: number) => Promise<void>;
@@ -919,4 +1080,159 @@ describe('RTCEngine', () => {
919
1080
  expect(handleDisconnect).not.toHaveBeenCalled();
920
1081
  });
921
1082
  });
1083
+
1084
+ describe('negotiate', () => {
1085
+ /**
1086
+ * An engine whose `pcManager` parks in `negotiate()` until its abort controller fires, so
1087
+ * several negotiations can be held in flight at once. The controllers are captured so tests
1088
+ * can check the abort actually reached each in-flight call.
1089
+ */
1090
+ function stubNegotiableEngine(engine: RTCEngine) {
1091
+ const controllers: AbortController[] = [];
1092
+ const negotiate = vi.fn((abortController: AbortController) => {
1093
+ controllers.push(abortController);
1094
+ return new Promise<void>((_resolve, reject) => {
1095
+ abortController.signal.addEventListener(
1096
+ 'abort',
1097
+ () => reject(new Error('negotiation aborted')),
1098
+ { once: true },
1099
+ );
1100
+ });
1101
+ });
1102
+ Object.assign(engine as unknown as Record<string, unknown>, {
1103
+ _isClosed: false,
1104
+ pcManager: {
1105
+ requirePublisher: vi.fn(),
1106
+ // a non-empty transceiver list keeps negotiate() off the createDataChannels path
1107
+ publisher: { getTransceivers: () => [{}], off: vi.fn(), once: vi.fn() },
1108
+ negotiate,
1109
+ },
1110
+ handleDisconnect: vi.fn(),
1111
+ });
1112
+ return { negotiate, controllers };
1113
+ }
1114
+
1115
+ const pendingAborts = (engine: RTCEngine) =>
1116
+ (engine as unknown as { pendingNegotiationAborts: Set<() => void> }).pendingNegotiationAborts;
1117
+
1118
+ it('does not add a Closing/Restarting listener per in-flight negotiate call', async () => {
1119
+ const engine = new RTCEngine(roomOptionDefaults);
1120
+ const { negotiate, controllers } = stubNegotiableEngine(engine);
1121
+
1122
+ // A burst of server-initiated renegotiations used to stack a listener pair per call, which
1123
+ // trips the emitter's max-listener warning at 11.
1124
+ const closingBaseline = engine.listenerCount(EngineEvent.Closing);
1125
+ const restartingBaseline = engine.listenerCount(EngineEvent.Restarting);
1126
+
1127
+ const pending = Array.from({ length: 20 }, () => engine.negotiate());
1128
+ await tick();
1129
+
1130
+ expect(negotiate).toHaveBeenCalledTimes(20);
1131
+ expect(engine.listenerCount(EngineEvent.Closing)).toBe(closingBaseline);
1132
+ expect(engine.listenerCount(EngineEvent.Restarting)).toBe(restartingBaseline);
1133
+
1134
+ // The single central listener still has to reach every one of them.
1135
+ engine.emit(EngineEvent.Closing);
1136
+ await expect(Promise.all(pending)).resolves.toHaveLength(20);
1137
+ expect(controllers).toHaveLength(20);
1138
+ expect(controllers.every((c) => c.signal.aborted)).toBe(true);
1139
+ expect(pendingAborts(engine).size).toBe(0);
1140
+ });
1141
+
1142
+ it('aborts in-flight negotiations on restart without disarming later ones', async () => {
1143
+ const engine = new RTCEngine(roomOptionDefaults);
1144
+ const { controllers } = stubNegotiableEngine(engine);
1145
+
1146
+ const firstBatch = [engine.negotiate(), engine.negotiate()];
1147
+ await tick();
1148
+ engine.emit(EngineEvent.Restarting);
1149
+ await expect(Promise.all(firstBatch)).resolves.toHaveLength(2);
1150
+ expect(controllers.every((c) => c.signal.aborted)).toBe(true);
1151
+
1152
+ // Restarting fires on every reconnect attempt, so the fan-out point has to re-arm: a
1153
+ // negotiation started after a restart must not be aborted on arrival.
1154
+ const afterRestart = engine.negotiate();
1155
+ await tick();
1156
+ expect(controllers).toHaveLength(3);
1157
+ expect(controllers[2].signal.aborted).toBe(false);
1158
+ expect(pendingAborts(engine).size).toBe(1);
1159
+
1160
+ engine.emit(EngineEvent.Restarting);
1161
+ await expect(afterRestart).resolves.toBeUndefined();
1162
+ expect(controllers[2].signal.aborted).toBe(true);
1163
+ expect(pendingAborts(engine).size).toBe(0);
1164
+ });
1165
+ });
1166
+
1167
+ describe('server-requested reconnect delay', () => {
1168
+ // A RECONNECT or RESUME leave from the server skips any remaining backoff. It must still go
1169
+ // through the reconnect policy, so that clients told to reconnect together spread out.
1170
+ interface DisconnectInternals {
1171
+ _isClosed: boolean;
1172
+ reconnectAttempts: number;
1173
+ handleDisconnect: (connection: string, reason?: number) => void;
1174
+ }
1175
+
1176
+ function scheduledDelay(
1177
+ nextRetryDelayInMs: ReconnectPolicy['nextRetryDelayInMs'],
1178
+ attempts = 0,
1179
+ ) {
1180
+ const engine = new RTCEngine({
1181
+ ...roomOptionDefaults,
1182
+ reconnectPolicy: { nextRetryDelayInMs },
1183
+ });
1184
+ const internals = engine as unknown as DisconnectInternals;
1185
+ internals._isClosed = false;
1186
+ internals.reconnectAttempts = attempts;
1187
+ const setTimeoutSpy = vi
1188
+ .spyOn(CriticalTimers, 'setTimeout')
1189
+ .mockReturnValue(0 as unknown as ReturnType<typeof setTimeout>);
1190
+ internals.handleDisconnect('leave-reconnect');
1191
+ expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
1192
+ return setTimeoutSpy.mock.calls[0][1];
1193
+ }
1194
+
1195
+ afterEach(() => {
1196
+ vi.restoreAllMocks();
1197
+ });
1198
+
1199
+ it('uses the first retry delay of the policy instead of reconnecting immediately', () => {
1200
+ const nextRetryDelayInMs = vi.fn((context: ReconnectContext) =>
1201
+ context.retryCount === 0 ? 321 : 5000,
1202
+ );
1203
+ expect(scheduledDelay(nextRetryDelayInMs)).toBe(321);
1204
+ });
1205
+
1206
+ it('skips the remaining backoff when the request arrives during a later retry', () => {
1207
+ const nextRetryDelayInMs = vi.fn((context: ReconnectContext) =>
1208
+ context.retryCount === 0 ? 321 : 5000,
1209
+ );
1210
+ expect(scheduledDelay(nextRetryDelayInMs, 3)).toBe(321);
1211
+ });
1212
+
1213
+ it('gives up when the policy stops on the first retry delay', () => {
1214
+ const engine = new RTCEngine({
1215
+ ...roomOptionDefaults,
1216
+ reconnectPolicy: {
1217
+ nextRetryDelayInMs: (context: ReconnectContext) =>
1218
+ context.retryCount === 0 ? null : 5000,
1219
+ },
1220
+ });
1221
+ const internals = engine as unknown as DisconnectInternals;
1222
+ internals._isClosed = false;
1223
+ internals.reconnectAttempts = 3;
1224
+ const setTimeoutSpy = vi.spyOn(CriticalTimers, 'setTimeout');
1225
+ const disconnected = vi.fn();
1226
+ engine.on(EngineEvent.Disconnected, disconnected);
1227
+ internals.handleDisconnect('leave-reconnect');
1228
+ expect(setTimeoutSpy).not.toHaveBeenCalled();
1229
+ expect(disconnected).toHaveBeenCalledTimes(1);
1230
+ });
1231
+
1232
+ it('spreads out clients that use the default policy', () => {
1233
+ const defaultPolicy = new DefaultReconnectPolicy();
1234
+ vi.spyOn(Math, 'random').mockReturnValue(0.5);
1235
+ expect(scheduledDelay((context) => defaultPolicy.nextRetryDelayInMs(context))).toBe(250);
1236
+ });
1237
+ });
922
1238
  });
@@ -227,6 +227,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
227
227
 
228
228
  private attemptingReconnect: boolean = false;
229
229
 
230
+ private reconnectAbortController?: AbortController;
231
+
230
232
  private reconnectPolicy: ReconnectPolicy;
231
233
 
232
234
  private reconnectTimeout?: ReturnType<typeof setTimeout>;
@@ -236,6 +238,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
236
238
  /** keeps track of how often an initial join connection has been tried */
237
239
  private joinAttempts: number = 0;
238
240
 
241
+ /**
242
+ * When the current join attempt began (`performance.now()`), taken at the top of `join` before
243
+ * the signal connection or the peer connections exist. Every attempt (a join retry, or the join
244
+ * inside a full restart) times itself from scratch. Consumed when the primary transport first
245
+ * connects, to hand the publisher its connection setup time; resumes never set it.
246
+ */
247
+ private joinStartedAt?: number;
248
+
239
249
  /** specifies how often an initial join connection is allowed to retry */
240
250
  private maxJoinAttempts: number = 1;
241
251
 
@@ -268,6 +278,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
268
278
  /** timestamp (ms) the primary transport entered `CONNECTING`, used to bound how long we tolerate it */
269
279
  private transportConnectingSince?: number;
270
280
 
281
+ /**
282
+ * Abort handlers for the in-flight `negotiate()` calls, which a single central
283
+ * Closing/Restarting listener pair fans out to. Registering a listener pair per call instead
284
+ * accumulates them on the engine emitter and trips its max-listener warning under
285
+ * renegotiation bursts.
286
+ */
287
+ private pendingNegotiationAborts = new Set<() => void>();
288
+
271
289
  constructor(private options: InternalRoomOptions) {
272
290
  super();
273
291
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
@@ -308,6 +326,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
308
326
  this.client.onParticipantUpdate = (updates) =>
309
327
  this.emit(EngineEvent.ParticipantUpdate, updates);
310
328
  this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
329
+
330
+ const abortPendingNegotiations = () => {
331
+ // Iterate a copy: each handler removes itself from the set as its negotiation settles.
332
+ for (const abort of Array.from(this.pendingNegotiationAborts)) {
333
+ abort();
334
+ }
335
+ };
336
+ this.on(EngineEvent.Closing, abortPendingNegotiations);
337
+ this.on(EngineEvent.Restarting, abortPendingNegotiations);
311
338
  }
312
339
 
313
340
  /** @internal */
@@ -335,6 +362,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
335
362
  this.maxJoinAttempts = opts.maxRetries;
336
363
  try {
337
364
  this.joinAttempts += 1;
365
+ this.joinStartedAt = performance.now();
338
366
 
339
367
  this.setupSignalClientCallbacks();
340
368
  // Whether the initial publisher offer is bundled with the join request. Computed once and
@@ -375,6 +403,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
375
403
  useV0Path,
376
404
  offerProto,
377
405
  );
406
+ if (abortSignal?.aborted) {
407
+ this.log.info('join cancelled after the signal join response');
408
+ throw ConnectionError.cancelled('Connection aborted');
409
+ }
378
410
  this._isClosed = false;
379
411
  this.latestJoinResponse = joinResponse;
380
412
  this.participantSid = joinResponse.participant?.sid;
@@ -388,6 +420,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
388
420
  // the server's ICE servers and topology, then negotiate separately rather than bundling
389
421
  // the offer with the join.
390
422
  await this.configure(joinResponse, !useV0Path);
423
+ if (abortSignal?.aborted) {
424
+ this.log.info('join cancelled after configuring peer connections');
425
+ throw ConnectionError.cancelled('Connection aborted');
426
+ }
391
427
  if (!useV0Path) {
392
428
  // The V1 first offer must carry the media layout so Firefox binds receive decoders for
393
429
  // subscribed tracks — without it, subscribed audio/video arrive as RTP but
@@ -403,6 +439,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
403
439
  }
404
440
  }
405
441
 
442
+ if (this.joinStartedAt !== undefined) {
443
+ // Lets a video published before the transports connect be seeded from the time elapsed
444
+ // so far, rather than delaying its offer until the setup time is measured.
445
+ this.pcManager?.publisher.setConnectionStartedAt(this.joinStartedAt);
446
+ }
447
+
406
448
  this.registerOnLineListener();
407
449
  this.clientConfiguration = joinResponse.clientConfiguration;
408
450
  this.emit(EngineEvent.SignalConnected, joinResponse);
@@ -446,6 +488,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
446
488
  * having given up on reconnecting.
447
489
  */
448
490
  async close(reason?: string) {
491
+ this.reconnectAbortController?.abort();
492
+ this.reconnectAbortController = undefined;
449
493
  const unlock = await this.closingLock.lock();
450
494
  if (this.isClosed) {
451
495
  unlock();
@@ -454,6 +498,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
454
498
  try {
455
499
  this._isClosed = true;
456
500
  this.joinAttempts = 0;
501
+ this.joinStartedAt = undefined;
457
502
  this.emit(EngineEvent.Closing);
458
503
  this.removeAllListeners();
459
504
  this.deregisterOnLineListener();
@@ -612,6 +657,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
612
657
  this.publisherConnectionPromise = undefined;
613
658
  }
614
659
  if (connectionState === PCTransportState.CONNECTED) {
660
+ if (this.joinStartedAt !== undefined) {
661
+ // Connection setup time is the only network signal there is before the first video
662
+ // offer. Set synchronously, so it is in place before `room.connect()` resolves and the
663
+ // app can publish. A full restart joins again and builds new transports, so it is
664
+ // measured again; a resume keeps its estimator and never restarts the timer.
665
+ const setupTimeMs = Math.round(performance.now() - this.joinStartedAt);
666
+ this.joinStartedAt = undefined;
667
+ this.log.info(`connection setup took ${setupTimeMs} ms`);
668
+ this.pcManager?.publisher.setConnectionSetupTime(setupTimeMs);
669
+ }
615
670
  const shouldEmit = this.pcState === PCState.New;
616
671
  this.pcState = PCState.Connected;
617
672
  if (shouldEmit) {
@@ -669,6 +724,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
669
724
  this.midToTrackId = midToTrackId;
670
725
  }
671
726
  await this.pcManager.setPublisherAnswer(sd, offerId);
727
+ this.emit(EngineEvent.RequestSubscribedCodecRefresh);
672
728
  };
673
729
 
674
730
  // add candidate on trickle
@@ -738,7 +794,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
738
794
 
739
795
  this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
740
796
  this.addMediaSections(requirement.numAudios, requirement.numVideos);
741
- this.negotiate();
797
+ this.negotiate().catch((err) => {
798
+ this.log.error(err);
799
+ });
742
800
  };
743
801
 
744
802
  this.client.onPublishDataTrackResponse = (event: PublishDataTrackResponse) => {
@@ -879,6 +937,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
879
937
  return;
880
938
  }
881
939
 
940
+ if (this.dataChannels.hasPublisherChannels) {
941
+ // Replacing live channels (the Safari null-id path in resumeConnection): the new ones start
942
+ // out connecting, so drop the memoized readiness check and make the next sender wait for
943
+ // them to open.
944
+ this.publisherConnectionPromise = undefined;
945
+ }
882
946
  this.dataChannels.createPublisherChannels(this.pcManager);
883
947
  }
884
948
 
@@ -1201,7 +1265,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1201
1265
  return;
1202
1266
  }
1203
1267
  if (connection === leaveReconnect) {
1204
- delay = 0;
1268
+ // The server asked for a reconnect, so skip any remaining backoff. Use the policy's first
1269
+ // retry delay instead of 0 so that clients told to reconnect together don't all reconnect
1270
+ // at the same instant.
1271
+ delay = this.getNextRetryDelay({ elapsedMs: duration, retryCount: 0 });
1272
+ if (delay === null) {
1273
+ disconnect(duration);
1274
+ return;
1275
+ }
1205
1276
  }
1206
1277
 
1207
1278
  this.log.debug(`reconnecting in ${delay}ms`);
@@ -1318,16 +1389,26 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1318
1389
  this.fullReconnectOnNext = false;
1319
1390
 
1320
1391
  let succeeded = false;
1392
+ const abortController = new AbortController();
1393
+ this.reconnectAbortController = abortController;
1321
1394
  try {
1322
1395
  this.attemptingReconnect = true;
1323
1396
  if (fullReconnect) {
1324
- await this.restartConnection();
1397
+ await this.restartConnection(abortController.signal);
1325
1398
  } else {
1326
1399
  await this.resumeConnection(reason);
1327
1400
  }
1401
+ if (abortController.signal.aborted) {
1402
+ this.log.info('reconnect attempt cancelled, not marking it as succeeded');
1403
+ return;
1404
+ }
1328
1405
  this.clearPendingReconnect();
1329
1406
  succeeded = true;
1330
1407
  } catch (e) {
1408
+ if (abortController.signal.aborted) {
1409
+ this.log.info('reconnect attempt cancelled, not retrying', { error: e });
1410
+ return;
1411
+ }
1331
1412
  this.reconnectAttempts += 1;
1332
1413
  let recoverable = true;
1333
1414
  if (e instanceof UnexpectedConnectionState) {
@@ -1356,6 +1437,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1356
1437
  );
1357
1438
  }
1358
1439
  } finally {
1440
+ // close() may already have cleared this (or a later attempt replaced it); only release our own
1441
+ if (this.reconnectAbortController === abortController) {
1442
+ this.reconnectAbortController = undefined;
1443
+ }
1359
1444
  this.attemptingReconnect = false;
1360
1445
 
1361
1446
  // A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
@@ -1379,8 +1464,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1379
1464
  return null;
1380
1465
  }
1381
1466
 
1382
- private async restartConnection(regionUrl?: string) {
1467
+ private async restartConnection(abortSignal: AbortSignal, regionUrl?: string) {
1383
1468
  try {
1469
+ if (abortSignal.aborted) {
1470
+ this.log.info('full reconnect cancelled before starting', { regionUrl });
1471
+ return;
1472
+ }
1384
1473
  if (!this.url || !this.token) {
1385
1474
  // permanent failure, don't attempt reconnection
1386
1475
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
@@ -1394,6 +1483,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1394
1483
  }
1395
1484
  await this.cleanupPeerConnections();
1396
1485
  await this.cleanupClient();
1486
+ if (abortSignal.aborted) {
1487
+ this.log.info('full reconnect cancelled after cleaning up the previous session', {
1488
+ regionUrl,
1489
+ });
1490
+ return;
1491
+ }
1397
1492
 
1398
1493
  let joinResponse: JoinResponse;
1399
1494
  try {
@@ -1407,7 +1502,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1407
1502
  regionUrl ?? this.url,
1408
1503
  this.token,
1409
1504
  this.signalOpts,
1410
- undefined,
1505
+ abortSignal,
1411
1506
  !this.options.singlePeerConnection,
1412
1507
  )
1413
1508
  ).joinResponse;
@@ -1417,6 +1512,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1417
1512
  }
1418
1513
  throw new SignalReconnectError();
1419
1514
  }
1515
+ if (abortSignal.aborted) {
1516
+ this.log.info('full reconnect cancelled after the signal join response', { regionUrl });
1517
+ return;
1518
+ }
1420
1519
 
1421
1520
  if (this.shouldFailNext) {
1422
1521
  this.shouldFailNext = false;
@@ -1427,6 +1526,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1427
1526
  this.emit(EngineEvent.SignalRestarted, joinResponse);
1428
1527
 
1429
1528
  await this.waitForPCReconnected();
1529
+ if (abortSignal.aborted) {
1530
+ this.log.info('full reconnect cancelled after peer connections reconnected', { regionUrl });
1531
+ return;
1532
+ }
1430
1533
 
1431
1534
  // re-check signal connection state before setting engine as resumed
1432
1535
  if (this.client.currentState !== SignalConnectionState.CONNECTED) {
@@ -1437,9 +1540,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1437
1540
  // reconnect success
1438
1541
  this.emit(EngineEvent.Restarted);
1439
1542
  } catch (error) {
1440
- const nextRegionUrl = await this.regionStrategy?.getNextUrl();
1543
+ if (abortSignal.aborted) {
1544
+ this.log.info('full reconnect cancelled after a failed attempt', { regionUrl });
1545
+ return;
1546
+ }
1547
+ const nextRegionUrl = await this.regionStrategy?.getNextUrl(abortSignal);
1548
+ if (abortSignal.aborted) {
1549
+ this.log.info('full reconnect cancelled during region selection', { regionUrl });
1550
+ return;
1551
+ }
1441
1552
  if (nextRegionUrl) {
1442
- await this.restartConnection(nextRegionUrl);
1553
+ await this.restartConnection(abortSignal, nextRegionUrl);
1443
1554
  return;
1444
1555
  } else {
1445
1556
  // no more regions to try (or we're not on cloud)
@@ -1673,7 +1784,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1673
1784
  * {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
1674
1785
  */
1675
1786
  async waitForBufferHeadroom(kind: DataChannelKind) {
1676
- return this.flowControlFor(kind).waitForHeadroomWithLock();
1787
+ const dc = this.dataChannelForKind(kind);
1788
+ if (!dc) {
1789
+ throw new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`);
1790
+ }
1791
+ return this.flowControlFor(kind).waitForHeadroomWithLock(dc);
1677
1792
  }
1678
1793
 
1679
1794
  /**
@@ -1807,8 +1922,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1807
1922
  if (this.isClosed) {
1808
1923
  reject(new NegotiationError('cannot negotiate on closed engine'));
1809
1924
  }
1810
- this.on(EngineEvent.Closing, handleClosed);
1811
- this.on(EngineEvent.Restarting, handleClosed);
1925
+ this.pendingNegotiationAborts.add(handleClosed);
1812
1926
  this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1813
1927
  this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1814
1928
 
@@ -1832,8 +1946,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1832
1946
  reject(new Error(String(e)));
1833
1947
  }
1834
1948
  } finally {
1835
- this.off(EngineEvent.Closing, handleClosed);
1836
- this.off(EngineEvent.Restarting, handleClosed);
1949
+ this.pendingNegotiationAborts.delete(handleClosed);
1837
1950
  }
1838
1951
  });
1839
1952
  }
@@ -2095,6 +2208,7 @@ export type EngineEventCallbacks = {
2095
2208
  joined: (joinResponse: JoinResponse) => void;
2096
2209
  tokenRefreshed: (token: string) => void;
2097
2210
  serverRegionsReported: (regions: RegionSettings) => void;
2211
+ requestSubscribedCodecRefresh: () => void;
2098
2212
  };
2099
2213
 
2100
2214
  export interface RegionStrategy {