@stream-io/video-client 1.11.11 → 1.11.13

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/dist/index.es.js CHANGED
@@ -3298,7 +3298,7 @@ const retryable = async (rpc, signal) => {
3298
3298
  return result;
3299
3299
  };
3300
3300
 
3301
- const version = "1.11.11";
3301
+ const version = "1.11.13";
3302
3302
  const [major, minor, patch] = version.split('.');
3303
3303
  let sdkInfo = {
3304
3304
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6375,6 +6375,32 @@ const createWebSocketSignalChannel = (opts) => {
6375
6375
  return ws;
6376
6376
  };
6377
6377
 
6378
+ /**
6379
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
6380
+ * since rejecting the promise causes an unhandled rejection error (even if the
6381
+ * rejection is handled everywhere promise result is expected).
6382
+ *
6383
+ * To avoid that, we add both resolution and rejection handlers to the promise.
6384
+ * That way, the saved promise never rejects. A callback is provided as return
6385
+ * value to build a *new* promise, that resolves and rejects along with
6386
+ * the original promise.
6387
+ * @param promise Promise to wrap, which possibly rejects
6388
+ * @returns Callback to build a new promise, which resolves and rejects along
6389
+ * with the original promise
6390
+ */
6391
+ function makeSafePromise(promise) {
6392
+ let isPending = true;
6393
+ const safePromise = promise
6394
+ .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
6395
+ .finally(() => (isPending = false));
6396
+ const unwrapPromise = () => safePromise.then((fulfillment) => {
6397
+ if (fulfillment.status === 'rejected')
6398
+ throw fulfillment.error;
6399
+ return fulfillment.result;
6400
+ });
6401
+ unwrapPromise.checkPending = () => isPending;
6402
+ return unwrapPromise;
6403
+ }
6378
6404
  /**
6379
6405
  * Creates a new promise with resolvers.
6380
6406
  *
@@ -6407,6 +6433,151 @@ const promiseWithResolvers = () => {
6407
6433
  };
6408
6434
  };
6409
6435
 
6436
+ const uninitialized = Symbol('uninitialized');
6437
+ /**
6438
+ * Lazily creates a value using a provided factory
6439
+ */
6440
+ function lazy(factory) {
6441
+ let value = uninitialized;
6442
+ return () => {
6443
+ if (value === uninitialized) {
6444
+ value = factory();
6445
+ }
6446
+ return value;
6447
+ };
6448
+ }
6449
+
6450
+ const timerWorker = {
6451
+ src: `var timerIdMapping = new Map();
6452
+ self.addEventListener('message', function (event) {
6453
+ var request = event.data;
6454
+ switch (request.type) {
6455
+ case 'setTimeout':
6456
+ case 'setInterval':
6457
+ timerIdMapping.set(request.id, (request.type === 'setTimeout' ? setTimeout : setInterval)(function () {
6458
+ tick(request.id);
6459
+ if (request.type === 'setTimeout') {
6460
+ timerIdMapping.delete(request.id);
6461
+ }
6462
+ }, request.timeout));
6463
+ break;
6464
+ case 'clearTimeout':
6465
+ case 'clearInterval':
6466
+ (request.type === 'clearTimeout' ? clearTimeout : clearInterval)(timerIdMapping.get(request.id));
6467
+ timerIdMapping.delete(request.id);
6468
+ break;
6469
+ }
6470
+ });
6471
+ function tick(id) {
6472
+ var message = { type: 'tick', id: id };
6473
+ self.postMessage(message);
6474
+ }`,
6475
+ };
6476
+
6477
+ class TimerWorker {
6478
+ constructor() {
6479
+ this.currentTimerId = 1;
6480
+ this.callbacks = new Map();
6481
+ this.fallback = false;
6482
+ }
6483
+ setup({ useTimerWorker = true } = {}) {
6484
+ if (!useTimerWorker) {
6485
+ this.fallback = true;
6486
+ return;
6487
+ }
6488
+ try {
6489
+ const source = timerWorker.src;
6490
+ const blob = new Blob([source], {
6491
+ type: 'application/javascript; charset=utf-8',
6492
+ });
6493
+ const script = URL.createObjectURL(blob);
6494
+ this.worker = new Worker(script, { name: 'str-timer-worker' });
6495
+ this.worker.addEventListener('message', (event) => {
6496
+ const { type, id } = event.data;
6497
+ if (type === 'tick') {
6498
+ this.callbacks.get(id)?.();
6499
+ }
6500
+ });
6501
+ }
6502
+ catch (err) {
6503
+ getLogger(['timer-worker'])('error', err);
6504
+ this.fallback = true;
6505
+ }
6506
+ }
6507
+ destroy() {
6508
+ this.callbacks.clear();
6509
+ this.worker?.terminate();
6510
+ this.worker = undefined;
6511
+ this.fallback = false;
6512
+ }
6513
+ get ready() {
6514
+ return this.fallback || Boolean(this.worker);
6515
+ }
6516
+ setInterval(callback, timeout) {
6517
+ return this.setTimer('setInterval', callback, timeout);
6518
+ }
6519
+ clearInterval(id) {
6520
+ this.clearTimer('clearInterval', id);
6521
+ }
6522
+ setTimeout(callback, timeout) {
6523
+ return this.setTimer('setTimeout', callback, timeout);
6524
+ }
6525
+ clearTimeout(id) {
6526
+ this.clearTimer('clearTimeout', id);
6527
+ }
6528
+ setTimer(type, callback, timeout) {
6529
+ if (!this.ready) {
6530
+ this.setup();
6531
+ }
6532
+ if (this.fallback) {
6533
+ return (type === 'setTimeout' ? setTimeout : setInterval)(callback, timeout);
6534
+ }
6535
+ const id = this.getTimerId();
6536
+ this.callbacks.set(id, () => {
6537
+ callback();
6538
+ // Timeouts are one-off operations, so no need to keep callback reference
6539
+ // after timer has fired
6540
+ if (type === 'setTimeout') {
6541
+ this.callbacks.delete(id);
6542
+ }
6543
+ });
6544
+ this.sendMessage({ type, id, timeout });
6545
+ return id;
6546
+ }
6547
+ clearTimer(type, id) {
6548
+ if (!id) {
6549
+ return;
6550
+ }
6551
+ if (!this.ready) {
6552
+ this.setup();
6553
+ }
6554
+ if (this.fallback) {
6555
+ (type === 'clearTimeout' ? clearTimeout : clearInterval)(id);
6556
+ return;
6557
+ }
6558
+ this.callbacks.delete(id);
6559
+ this.sendMessage({ type, id });
6560
+ }
6561
+ getTimerId() {
6562
+ return this.currentTimerId++;
6563
+ }
6564
+ sendMessage(message) {
6565
+ if (!this.worker) {
6566
+ throw new Error("Cannot use timer worker before it's set up");
6567
+ }
6568
+ this.worker.postMessage(message);
6569
+ }
6570
+ }
6571
+ let timerWorkerEnabled = false;
6572
+ const enableTimerWorker = () => {
6573
+ timerWorkerEnabled = true;
6574
+ };
6575
+ const getTimers = lazy(() => {
6576
+ const instance = new TimerWorker();
6577
+ instance.setup({ useTimerWorker: timerWorkerEnabled });
6578
+ return instance;
6579
+ });
6580
+
6410
6581
  /**
6411
6582
  * The client used for exchanging information with the SFU.
6412
6583
  */
@@ -6427,7 +6598,6 @@ class StreamSfuClient {
6427
6598
  this.isLeaving = false;
6428
6599
  this.pingIntervalInMs = 10 * 1000;
6429
6600
  this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
6430
- this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
6431
6601
  /**
6432
6602
  * Promise that resolves when the JoinResponse is received.
6433
6603
  * Rejects after a certain threshold if the response is not received.
@@ -6448,31 +6618,25 @@ class StreamSfuClient {
6448
6618
  },
6449
6619
  });
6450
6620
  this.signalWs.addEventListener('close', this.handleWebSocketClose);
6451
- this.signalWs.addEventListener('error', this.restoreWebSocket);
6452
- this.signalReady = new Promise((resolve) => {
6453
- const onOpen = () => {
6454
- this.signalWs.removeEventListener('open', onOpen);
6455
- resolve(this.signalWs);
6456
- };
6457
- this.signalWs.addEventListener('open', onOpen);
6458
- });
6621
+ this.signalReady = makeSafePromise(Promise.race([
6622
+ new Promise((resolve) => {
6623
+ const onOpen = () => {
6624
+ this.signalWs.removeEventListener('open', onOpen);
6625
+ resolve(this.signalWs);
6626
+ };
6627
+ this.signalWs.addEventListener('open', onOpen);
6628
+ }),
6629
+ new Promise((resolve, reject) => {
6630
+ setTimeout(() => reject(new Error('SFU WS connection timed out')), this.joinResponseTimeout);
6631
+ }),
6632
+ ]));
6459
6633
  };
6460
6634
  this.cleanUpWebSocket = () => {
6461
- this.signalWs.removeEventListener('error', this.restoreWebSocket);
6462
6635
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6463
6636
  };
6464
- this.restoreWebSocket = () => {
6465
- withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
6466
- await this.networkAvailableTask?.promise;
6467
- this.logger('debug', 'Restoring SFU WS connection');
6468
- this.cleanUpWebSocket();
6469
- await sleep(500);
6470
- this.createWebSocket();
6471
- }).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
6472
- };
6473
6637
  this.handleWebSocketClose = () => {
6474
6638
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6475
- clearInterval(this.keepAliveInterval);
6639
+ getTimers().clearInterval(this.keepAliveInterval);
6476
6640
  clearTimeout(this.connectionCheckTimeout);
6477
6641
  this.onSignalClose?.();
6478
6642
  };
@@ -6563,7 +6727,7 @@ class StreamSfuClient {
6563
6727
  };
6564
6728
  this.join = async (data) => {
6565
6729
  // wait for the signal web socket to be ready before sending "joinRequest"
6566
- await this.signalReady;
6730
+ await this.signalReady();
6567
6731
  if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
6568
6732
  // we need to lock the RPC requests until we receive a JoinResponse.
6569
6733
  // that's why we have this primitive lock mechanism.
@@ -6618,7 +6782,7 @@ class StreamSfuClient {
6618
6782
  }));
6619
6783
  };
6620
6784
  this.send = async (message) => {
6621
- await this.signalReady; // wait for the signal ws to be open
6785
+ await this.signalReady(); // wait for the signal ws to be open
6622
6786
  const msgJson = SfuRequest.toJson(message);
6623
6787
  if (this.signalWs.readyState !== WebSocket.OPEN) {
6624
6788
  this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
@@ -6628,8 +6792,9 @@ class StreamSfuClient {
6628
6792
  this.signalWs.send(SfuRequest.toBinary(message));
6629
6793
  };
6630
6794
  this.keepAlive = () => {
6631
- clearInterval(this.keepAliveInterval);
6632
- this.keepAliveInterval = setInterval(() => {
6795
+ const timers = getTimers();
6796
+ timers.clearInterval(this.keepAliveInterval);
6797
+ this.keepAliveInterval = timers.setInterval(() => {
6633
6798
  this.ping().catch((e) => {
6634
6799
  this.logger('error', 'Error sending healthCheckRequest to SFU', e);
6635
6800
  });
@@ -8210,20 +8375,6 @@ function canQueryPermissions() {
8210
8375
  !!navigator.permissions?.query);
8211
8376
  }
8212
8377
 
8213
- const uninitialized = Symbol('uninitialized');
8214
- /**
8215
- * Lazily creates a value using a provided factory
8216
- */
8217
- function lazy(factory) {
8218
- let value = uninitialized;
8219
- return () => {
8220
- if (value === uninitialized) {
8221
- value = factory();
8222
- }
8223
- return value;
8224
- };
8225
- }
8226
-
8227
8378
  /**
8228
8379
  * Returns an Observable that emits the list of available devices
8229
8380
  * that meet the given constraints.
@@ -9835,6 +9986,7 @@ class Call {
9835
9986
  this.reconnectAttempts = 0;
9836
9987
  this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
9837
9988
  this.fastReconnectDeadlineSeconds = 0;
9989
+ this.disconnectionTimeoutSeconds = 0;
9838
9990
  this.lastOfflineTimestamp = 0;
9839
9991
  // maintain the order of publishing tracks to restore them after a reconnection
9840
9992
  // it shouldn't contain duplicates
@@ -10351,6 +10503,10 @@ class Call {
10351
10503
  */
10352
10504
  this.handleSfuSignalClose = (sfuClient) => {
10353
10505
  this.logger('debug', '[Reconnect] SFU signal connection closed');
10506
+ // SFU WS closed before we finished current join, no need to schedule reconnect
10507
+ // because join operation will fail
10508
+ if (this.state.callingState === CallingState.JOINING)
10509
+ return;
10354
10510
  // normal close, no need to reconnect
10355
10511
  if (sfuClient.isLeaving)
10356
10512
  return;
@@ -10366,10 +10522,21 @@ class Call {
10366
10522
  * @param strategy the reconnection strategy to use.
10367
10523
  */
10368
10524
  this.reconnect = async (strategy) => {
10525
+ if (this.state.callingState === CallingState.RECONNECTING ||
10526
+ this.state.callingState === CallingState.RECONNECTING_FAILED)
10527
+ return;
10369
10528
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
10370
10529
  this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
10530
+ let reconnectStartTime = Date.now();
10371
10531
  this.reconnectStrategy = strategy;
10372
10532
  do {
10533
+ if (this.disconnectionTimeoutSeconds > 0 &&
10534
+ (Date.now() - reconnectStartTime) / 1000 >
10535
+ this.disconnectionTimeoutSeconds) {
10536
+ this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
10537
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
10538
+ return;
10539
+ }
10373
10540
  // we don't increment reconnect attempts for the FAST strategy.
10374
10541
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
10375
10542
  this.reconnectAttempts++;
@@ -10469,7 +10636,7 @@ class Call {
10469
10636
  const currentPublisher = this.publisher;
10470
10637
  currentSubscriber?.detachEventHandlers();
10471
10638
  currentPublisher?.detachEventHandlers();
10472
- const migrationTask = currentSfuClient.enterMigration();
10639
+ const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
10473
10640
  try {
10474
10641
  const currentSfu = currentSfuClient.edgeName;
10475
10642
  await this.join({ ...this.joinCallData, migrating_from: currentSfu });
@@ -10485,7 +10652,7 @@ class Call {
10485
10652
  // Wait for the migration to complete, then close the previous SFU client
10486
10653
  // and the peer connection instances. In case of failure, the migration
10487
10654
  // task would throw an error and REJOIN would be attempted.
10488
- await migrationTask;
10655
+ await migrationTask();
10489
10656
  // in MIGRATE, we can consider the call as joined only after
10490
10657
  // `participantMigrationComplete` event is received, signaled by
10491
10658
  // the `migrationTask`
@@ -11341,6 +11508,14 @@ class Call {
11341
11508
  this.dynascaleManager.setVideoTrackSubscriptionOverrides(enabled ? undefined : { enabled: false });
11342
11509
  this.dynascaleManager.applyTrackSubscriptions();
11343
11510
  };
11511
+ /**
11512
+ * Sets the maximum amount of time a user can remain waiting for a reconnect
11513
+ * after a network disruption
11514
+ * @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
11515
+ */
11516
+ this.setDisconnectionTimeout = (timeoutSeconds) => {
11517
+ this.disconnectionTimeoutSeconds = timeoutSeconds;
11518
+ };
11344
11519
  this.type = type;
11345
11520
  this.id = id;
11346
11521
  this.cid = `${type}:${id}`;
@@ -11494,33 +11669,6 @@ class Call {
11494
11669
  }
11495
11670
  }
11496
11671
 
11497
- /**
11498
- * Saving a long-lived reference to a promise that can reject can be unsafe,
11499
- * since rejecting the promise causes an unhandled rejection error (even if the
11500
- * rejection is handled everywhere promise result is expected).
11501
- *
11502
- * To avoid that, we add both resolution and rejection handlers to the promise.
11503
- * That way, the saved promise never rejects. A callback is provided as return
11504
- * value to build a *new* promise, that resolves and rejects along with
11505
- * the original promise.
11506
- * @param promise Promise to wrap, which possibly rejects
11507
- * @returns Callback to build a new promise, which resolves and rejects along
11508
- * with the original promise
11509
- */
11510
- function makeSafePromise(promise) {
11511
- let isPending = true;
11512
- const safePromise = promise
11513
- .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
11514
- .finally(() => (isPending = false));
11515
- const unwrapPromise = () => safePromise.then((fulfillment) => {
11516
- if (fulfillment.status === 'rejected')
11517
- throw fulfillment.error;
11518
- return fulfillment.result;
11519
- });
11520
- unwrapPromise.checkPending = () => isPending;
11521
- return unwrapPromise;
11522
- }
11523
-
11524
11672
  /**
11525
11673
  * StableWSConnection - A WS connection that reconnects upon failure.
11526
11674
  * - the browser will sometimes report that you're online or offline
@@ -11773,9 +11921,12 @@ class StableWSConnection {
11773
11921
  * Schedules a next health check ping for websocket.
11774
11922
  */
11775
11923
  this.scheduleNextPing = () => {
11924
+ const timers = getTimers();
11925
+ if (this.healthCheckTimeoutRef) {
11926
+ timers.clearTimeout(this.healthCheckTimeoutRef);
11927
+ }
11776
11928
  // 30 seconds is the recommended interval (messenger uses this)
11777
- clearTimeout(this.healthCheckTimeoutRef);
11778
- this.healthCheckTimeoutRef = setTimeout(() => {
11929
+ this.healthCheckTimeoutRef = timers.setTimeout(() => {
11779
11930
  // send the healthcheck..., server replies with a health check event
11780
11931
  const data = [{ type: 'health.check', client_id: this.client.clientID }];
11781
11932
  // try to send on the connection
@@ -11918,8 +12069,12 @@ class StableWSConnection {
11918
12069
  this.isConnecting = false;
11919
12070
  this.isDisconnected = true;
11920
12071
  // start by removing all the listeners
11921
- clearInterval(this.healthCheckTimeoutRef);
11922
- clearInterval(this.connectionCheckTimeoutRef);
12072
+ if (this.healthCheckTimeoutRef) {
12073
+ getTimers().clearInterval(this.healthCheckTimeoutRef);
12074
+ }
12075
+ if (this.connectionCheckTimeoutRef) {
12076
+ clearInterval(this.connectionCheckTimeoutRef);
12077
+ }
11923
12078
  removeConnectionEventListeners(this.onlineStatusChanged);
11924
12079
  this.isHealthy = false;
11925
12080
  let isClosedPromise;
@@ -12619,7 +12774,7 @@ class StreamClient {
12619
12774
  return await this.wsConnection.connect(this.defaultWSTimeout);
12620
12775
  };
12621
12776
  this.getUserAgent = () => {
12622
- const version = "1.11.11";
12777
+ const version = "1.11.13";
12623
12778
  return (this.userAgent ||
12624
12779
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12625
12780
  };
@@ -13030,10 +13185,14 @@ class StreamVideoClient {
13030
13185
  if (typeof apiKeyOrArgs === 'string') {
13031
13186
  logLevel = opts?.logLevel || logLevel;
13032
13187
  logger = opts?.logger || logger;
13188
+ if (opts?.expertimental_enableTimerWorker)
13189
+ enableTimerWorker();
13033
13190
  }
13034
13191
  else {
13035
13192
  logLevel = apiKeyOrArgs.options?.logLevel || logLevel;
13036
13193
  logger = apiKeyOrArgs.options?.logger || logger;
13194
+ if (apiKeyOrArgs.options?.expertimental_enableTimerWorker)
13195
+ enableTimerWorker();
13037
13196
  }
13038
13197
  setLogger(logger, logLevel);
13039
13198
  this.logger = getLogger(['client']);