@stream-io/video-client 1.11.11 → 1.11.12

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.11.12](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.11...@stream-io/video-client-1.11.12) (2024-12-03)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * handle timeout on SFU WS connections ([#1600](https://github.com/GetStream/stream-video-js/issues/1600)) ([5f2db7b](https://github.com/GetStream/stream-video-js/commit/5f2db7bd5cfdf57cdc04d6a6ed752f43e5b06657))
11
+
5
12
  ## [1.11.11](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.10...@stream-io/video-client-1.11.11) (2024-11-29)
6
13
 
7
14
 
@@ -3297,7 +3297,7 @@ const retryable = async (rpc, signal) => {
3297
3297
  return result;
3298
3298
  };
3299
3299
 
3300
- const version = "1.11.11";
3300
+ const version = "1.11.12";
3301
3301
  const [major, minor, patch] = version.split('.');
3302
3302
  let sdkInfo = {
3303
3303
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6374,6 +6374,32 @@ const createWebSocketSignalChannel = (opts) => {
6374
6374
  return ws;
6375
6375
  };
6376
6376
 
6377
+ /**
6378
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
6379
+ * since rejecting the promise causes an unhandled rejection error (even if the
6380
+ * rejection is handled everywhere promise result is expected).
6381
+ *
6382
+ * To avoid that, we add both resolution and rejection handlers to the promise.
6383
+ * That way, the saved promise never rejects. A callback is provided as return
6384
+ * value to build a *new* promise, that resolves and rejects along with
6385
+ * the original promise.
6386
+ * @param promise Promise to wrap, which possibly rejects
6387
+ * @returns Callback to build a new promise, which resolves and rejects along
6388
+ * with the original promise
6389
+ */
6390
+ function makeSafePromise(promise) {
6391
+ let isPending = true;
6392
+ const safePromise = promise
6393
+ .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
6394
+ .finally(() => (isPending = false));
6395
+ const unwrapPromise = () => safePromise.then((fulfillment) => {
6396
+ if (fulfillment.status === 'rejected')
6397
+ throw fulfillment.error;
6398
+ return fulfillment.result;
6399
+ });
6400
+ unwrapPromise.checkPending = () => isPending;
6401
+ return unwrapPromise;
6402
+ }
6377
6403
  /**
6378
6404
  * Creates a new promise with resolvers.
6379
6405
  *
@@ -6426,7 +6452,6 @@ class StreamSfuClient {
6426
6452
  this.isLeaving = false;
6427
6453
  this.pingIntervalInMs = 10 * 1000;
6428
6454
  this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
6429
- this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
6430
6455
  /**
6431
6456
  * Promise that resolves when the JoinResponse is received.
6432
6457
  * Rejects after a certain threshold if the response is not received.
@@ -6447,28 +6472,22 @@ class StreamSfuClient {
6447
6472
  },
6448
6473
  });
6449
6474
  this.signalWs.addEventListener('close', this.handleWebSocketClose);
6450
- this.signalWs.addEventListener('error', this.restoreWebSocket);
6451
- this.signalReady = new Promise((resolve) => {
6452
- const onOpen = () => {
6453
- this.signalWs.removeEventListener('open', onOpen);
6454
- resolve(this.signalWs);
6455
- };
6456
- this.signalWs.addEventListener('open', onOpen);
6457
- });
6475
+ this.signalReady = makeSafePromise(Promise.race([
6476
+ new Promise((resolve) => {
6477
+ const onOpen = () => {
6478
+ this.signalWs.removeEventListener('open', onOpen);
6479
+ resolve(this.signalWs);
6480
+ };
6481
+ this.signalWs.addEventListener('open', onOpen);
6482
+ }),
6483
+ new Promise((resolve, reject) => {
6484
+ setTimeout(() => reject(new Error('SFU WS connection timed out')), this.joinResponseTimeout);
6485
+ }),
6486
+ ]));
6458
6487
  };
6459
6488
  this.cleanUpWebSocket = () => {
6460
- this.signalWs.removeEventListener('error', this.restoreWebSocket);
6461
6489
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6462
6490
  };
6463
- this.restoreWebSocket = () => {
6464
- withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
6465
- await this.networkAvailableTask?.promise;
6466
- this.logger('debug', 'Restoring SFU WS connection');
6467
- this.cleanUpWebSocket();
6468
- await sleep(500);
6469
- this.createWebSocket();
6470
- }).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
6471
- };
6472
6491
  this.handleWebSocketClose = () => {
6473
6492
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6474
6493
  clearInterval(this.keepAliveInterval);
@@ -6562,7 +6581,7 @@ class StreamSfuClient {
6562
6581
  };
6563
6582
  this.join = async (data) => {
6564
6583
  // wait for the signal web socket to be ready before sending "joinRequest"
6565
- await this.signalReady;
6584
+ await this.signalReady();
6566
6585
  if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
6567
6586
  // we need to lock the RPC requests until we receive a JoinResponse.
6568
6587
  // that's why we have this primitive lock mechanism.
@@ -6617,7 +6636,7 @@ class StreamSfuClient {
6617
6636
  }));
6618
6637
  };
6619
6638
  this.send = async (message) => {
6620
- await this.signalReady; // wait for the signal ws to be open
6639
+ await this.signalReady(); // wait for the signal ws to be open
6621
6640
  const msgJson = SfuRequest.toJson(message);
6622
6641
  if (this.signalWs.readyState !== WebSocket.OPEN) {
6623
6642
  this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
@@ -9834,6 +9853,7 @@ class Call {
9834
9853
  this.reconnectAttempts = 0;
9835
9854
  this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
9836
9855
  this.fastReconnectDeadlineSeconds = 0;
9856
+ this.disconnectionTimeoutSeconds = 0;
9837
9857
  this.lastOfflineTimestamp = 0;
9838
9858
  // maintain the order of publishing tracks to restore them after a reconnection
9839
9859
  // it shouldn't contain duplicates
@@ -10350,6 +10370,10 @@ class Call {
10350
10370
  */
10351
10371
  this.handleSfuSignalClose = (sfuClient) => {
10352
10372
  this.logger('debug', '[Reconnect] SFU signal connection closed');
10373
+ // SFU WS closed before we finished current join, no need to schedule reconnect
10374
+ // because join operation will fail
10375
+ if (this.state.callingState === CallingState.JOINING)
10376
+ return;
10353
10377
  // normal close, no need to reconnect
10354
10378
  if (sfuClient.isLeaving)
10355
10379
  return;
@@ -10365,10 +10389,21 @@ class Call {
10365
10389
  * @param strategy the reconnection strategy to use.
10366
10390
  */
10367
10391
  this.reconnect = async (strategy) => {
10392
+ if (this.state.callingState === CallingState.RECONNECTING ||
10393
+ this.state.callingState === CallingState.RECONNECTING_FAILED)
10394
+ return;
10368
10395
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
10369
10396
  this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
10397
+ let reconnectStartTime = Date.now();
10370
10398
  this.reconnectStrategy = strategy;
10371
10399
  do {
10400
+ if (this.disconnectionTimeoutSeconds > 0 &&
10401
+ (Date.now() - reconnectStartTime) / 1000 >
10402
+ this.disconnectionTimeoutSeconds) {
10403
+ this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
10404
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
10405
+ return;
10406
+ }
10372
10407
  // we don't increment reconnect attempts for the FAST strategy.
10373
10408
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
10374
10409
  this.reconnectAttempts++;
@@ -10468,7 +10503,7 @@ class Call {
10468
10503
  const currentPublisher = this.publisher;
10469
10504
  currentSubscriber?.detachEventHandlers();
10470
10505
  currentPublisher?.detachEventHandlers();
10471
- const migrationTask = currentSfuClient.enterMigration();
10506
+ const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
10472
10507
  try {
10473
10508
  const currentSfu = currentSfuClient.edgeName;
10474
10509
  await this.join({ ...this.joinCallData, migrating_from: currentSfu });
@@ -10484,7 +10519,7 @@ class Call {
10484
10519
  // Wait for the migration to complete, then close the previous SFU client
10485
10520
  // and the peer connection instances. In case of failure, the migration
10486
10521
  // task would throw an error and REJOIN would be attempted.
10487
- await migrationTask;
10522
+ await migrationTask();
10488
10523
  // in MIGRATE, we can consider the call as joined only after
10489
10524
  // `participantMigrationComplete` event is received, signaled by
10490
10525
  // the `migrationTask`
@@ -11340,6 +11375,14 @@ class Call {
11340
11375
  this.dynascaleManager.setVideoTrackSubscriptionOverrides(enabled ? undefined : { enabled: false });
11341
11376
  this.dynascaleManager.applyTrackSubscriptions();
11342
11377
  };
11378
+ /**
11379
+ * Sets the maximum amount of time a user can remain waiting for a reconnect
11380
+ * after a network disruption
11381
+ * @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
11382
+ */
11383
+ this.setDisconnectionTimeout = (timeoutSeconds) => {
11384
+ this.disconnectionTimeoutSeconds = timeoutSeconds;
11385
+ };
11343
11386
  this.type = type;
11344
11387
  this.id = id;
11345
11388
  this.cid = `${type}:${id}`;
@@ -11495,33 +11538,6 @@ class Call {
11495
11538
 
11496
11539
  var https = null;
11497
11540
 
11498
- /**
11499
- * Saving a long-lived reference to a promise that can reject can be unsafe,
11500
- * since rejecting the promise causes an unhandled rejection error (even if the
11501
- * rejection is handled everywhere promise result is expected).
11502
- *
11503
- * To avoid that, we add both resolution and rejection handlers to the promise.
11504
- * That way, the saved promise never rejects. A callback is provided as return
11505
- * value to build a *new* promise, that resolves and rejects along with
11506
- * the original promise.
11507
- * @param promise Promise to wrap, which possibly rejects
11508
- * @returns Callback to build a new promise, which resolves and rejects along
11509
- * with the original promise
11510
- */
11511
- function makeSafePromise(promise) {
11512
- let isPending = true;
11513
- const safePromise = promise
11514
- .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
11515
- .finally(() => (isPending = false));
11516
- const unwrapPromise = () => safePromise.then((fulfillment) => {
11517
- if (fulfillment.status === 'rejected')
11518
- throw fulfillment.error;
11519
- return fulfillment.result;
11520
- });
11521
- unwrapPromise.checkPending = () => isPending;
11522
- return unwrapPromise;
11523
- }
11524
-
11525
11541
  /**
11526
11542
  * StableWSConnection - A WS connection that reconnects upon failure.
11527
11543
  * - the browser will sometimes report that you're online or offline
@@ -12620,7 +12636,7 @@ class StreamClient {
12620
12636
  return await this.wsConnection.connect(this.defaultWSTimeout);
12621
12637
  };
12622
12638
  this.getUserAgent = () => {
12623
- const version = "1.11.11";
12639
+ const version = "1.11.12";
12624
12640
  return (this.userAgent ||
12625
12641
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12626
12642
  };