@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.cjs.js CHANGED
@@ -3318,7 +3318,7 @@ const retryable = async (rpc, signal) => {
3318
3318
  return result;
3319
3319
  };
3320
3320
 
3321
- const version = "1.11.11";
3321
+ const version = "1.11.13";
3322
3322
  const [major, minor, patch] = version.split('.');
3323
3323
  let sdkInfo = {
3324
3324
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6395,6 +6395,32 @@ const createWebSocketSignalChannel = (opts) => {
6395
6395
  return ws;
6396
6396
  };
6397
6397
 
6398
+ /**
6399
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
6400
+ * since rejecting the promise causes an unhandled rejection error (even if the
6401
+ * rejection is handled everywhere promise result is expected).
6402
+ *
6403
+ * To avoid that, we add both resolution and rejection handlers to the promise.
6404
+ * That way, the saved promise never rejects. A callback is provided as return
6405
+ * value to build a *new* promise, that resolves and rejects along with
6406
+ * the original promise.
6407
+ * @param promise Promise to wrap, which possibly rejects
6408
+ * @returns Callback to build a new promise, which resolves and rejects along
6409
+ * with the original promise
6410
+ */
6411
+ function makeSafePromise(promise) {
6412
+ let isPending = true;
6413
+ const safePromise = promise
6414
+ .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
6415
+ .finally(() => (isPending = false));
6416
+ const unwrapPromise = () => safePromise.then((fulfillment) => {
6417
+ if (fulfillment.status === 'rejected')
6418
+ throw fulfillment.error;
6419
+ return fulfillment.result;
6420
+ });
6421
+ unwrapPromise.checkPending = () => isPending;
6422
+ return unwrapPromise;
6423
+ }
6398
6424
  /**
6399
6425
  * Creates a new promise with resolvers.
6400
6426
  *
@@ -6427,6 +6453,151 @@ const promiseWithResolvers = () => {
6427
6453
  };
6428
6454
  };
6429
6455
 
6456
+ const uninitialized = Symbol('uninitialized');
6457
+ /**
6458
+ * Lazily creates a value using a provided factory
6459
+ */
6460
+ function lazy(factory) {
6461
+ let value = uninitialized;
6462
+ return () => {
6463
+ if (value === uninitialized) {
6464
+ value = factory();
6465
+ }
6466
+ return value;
6467
+ };
6468
+ }
6469
+
6470
+ const timerWorker = {
6471
+ src: `var timerIdMapping = new Map();
6472
+ self.addEventListener('message', function (event) {
6473
+ var request = event.data;
6474
+ switch (request.type) {
6475
+ case 'setTimeout':
6476
+ case 'setInterval':
6477
+ timerIdMapping.set(request.id, (request.type === 'setTimeout' ? setTimeout : setInterval)(function () {
6478
+ tick(request.id);
6479
+ if (request.type === 'setTimeout') {
6480
+ timerIdMapping.delete(request.id);
6481
+ }
6482
+ }, request.timeout));
6483
+ break;
6484
+ case 'clearTimeout':
6485
+ case 'clearInterval':
6486
+ (request.type === 'clearTimeout' ? clearTimeout : clearInterval)(timerIdMapping.get(request.id));
6487
+ timerIdMapping.delete(request.id);
6488
+ break;
6489
+ }
6490
+ });
6491
+ function tick(id) {
6492
+ var message = { type: 'tick', id: id };
6493
+ self.postMessage(message);
6494
+ }`,
6495
+ };
6496
+
6497
+ class TimerWorker {
6498
+ constructor() {
6499
+ this.currentTimerId = 1;
6500
+ this.callbacks = new Map();
6501
+ this.fallback = false;
6502
+ }
6503
+ setup({ useTimerWorker = true } = {}) {
6504
+ if (!useTimerWorker) {
6505
+ this.fallback = true;
6506
+ return;
6507
+ }
6508
+ try {
6509
+ const source = timerWorker.src;
6510
+ const blob = new Blob([source], {
6511
+ type: 'application/javascript; charset=utf-8',
6512
+ });
6513
+ const script = URL.createObjectURL(blob);
6514
+ this.worker = new Worker(script, { name: 'str-timer-worker' });
6515
+ this.worker.addEventListener('message', (event) => {
6516
+ const { type, id } = event.data;
6517
+ if (type === 'tick') {
6518
+ this.callbacks.get(id)?.();
6519
+ }
6520
+ });
6521
+ }
6522
+ catch (err) {
6523
+ getLogger(['timer-worker'])('error', err);
6524
+ this.fallback = true;
6525
+ }
6526
+ }
6527
+ destroy() {
6528
+ this.callbacks.clear();
6529
+ this.worker?.terminate();
6530
+ this.worker = undefined;
6531
+ this.fallback = false;
6532
+ }
6533
+ get ready() {
6534
+ return this.fallback || Boolean(this.worker);
6535
+ }
6536
+ setInterval(callback, timeout) {
6537
+ return this.setTimer('setInterval', callback, timeout);
6538
+ }
6539
+ clearInterval(id) {
6540
+ this.clearTimer('clearInterval', id);
6541
+ }
6542
+ setTimeout(callback, timeout) {
6543
+ return this.setTimer('setTimeout', callback, timeout);
6544
+ }
6545
+ clearTimeout(id) {
6546
+ this.clearTimer('clearTimeout', id);
6547
+ }
6548
+ setTimer(type, callback, timeout) {
6549
+ if (!this.ready) {
6550
+ this.setup();
6551
+ }
6552
+ if (this.fallback) {
6553
+ return (type === 'setTimeout' ? setTimeout : setInterval)(callback, timeout);
6554
+ }
6555
+ const id = this.getTimerId();
6556
+ this.callbacks.set(id, () => {
6557
+ callback();
6558
+ // Timeouts are one-off operations, so no need to keep callback reference
6559
+ // after timer has fired
6560
+ if (type === 'setTimeout') {
6561
+ this.callbacks.delete(id);
6562
+ }
6563
+ });
6564
+ this.sendMessage({ type, id, timeout });
6565
+ return id;
6566
+ }
6567
+ clearTimer(type, id) {
6568
+ if (!id) {
6569
+ return;
6570
+ }
6571
+ if (!this.ready) {
6572
+ this.setup();
6573
+ }
6574
+ if (this.fallback) {
6575
+ (type === 'clearTimeout' ? clearTimeout : clearInterval)(id);
6576
+ return;
6577
+ }
6578
+ this.callbacks.delete(id);
6579
+ this.sendMessage({ type, id });
6580
+ }
6581
+ getTimerId() {
6582
+ return this.currentTimerId++;
6583
+ }
6584
+ sendMessage(message) {
6585
+ if (!this.worker) {
6586
+ throw new Error("Cannot use timer worker before it's set up");
6587
+ }
6588
+ this.worker.postMessage(message);
6589
+ }
6590
+ }
6591
+ let timerWorkerEnabled = false;
6592
+ const enableTimerWorker = () => {
6593
+ timerWorkerEnabled = true;
6594
+ };
6595
+ const getTimers = lazy(() => {
6596
+ const instance = new TimerWorker();
6597
+ instance.setup({ useTimerWorker: timerWorkerEnabled });
6598
+ return instance;
6599
+ });
6600
+
6430
6601
  /**
6431
6602
  * The client used for exchanging information with the SFU.
6432
6603
  */
@@ -6447,7 +6618,6 @@ class StreamSfuClient {
6447
6618
  this.isLeaving = false;
6448
6619
  this.pingIntervalInMs = 10 * 1000;
6449
6620
  this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
6450
- this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
6451
6621
  /**
6452
6622
  * Promise that resolves when the JoinResponse is received.
6453
6623
  * Rejects after a certain threshold if the response is not received.
@@ -6468,31 +6638,25 @@ class StreamSfuClient {
6468
6638
  },
6469
6639
  });
6470
6640
  this.signalWs.addEventListener('close', this.handleWebSocketClose);
6471
- this.signalWs.addEventListener('error', this.restoreWebSocket);
6472
- this.signalReady = new Promise((resolve) => {
6473
- const onOpen = () => {
6474
- this.signalWs.removeEventListener('open', onOpen);
6475
- resolve(this.signalWs);
6476
- };
6477
- this.signalWs.addEventListener('open', onOpen);
6478
- });
6641
+ this.signalReady = makeSafePromise(Promise.race([
6642
+ new Promise((resolve) => {
6643
+ const onOpen = () => {
6644
+ this.signalWs.removeEventListener('open', onOpen);
6645
+ resolve(this.signalWs);
6646
+ };
6647
+ this.signalWs.addEventListener('open', onOpen);
6648
+ }),
6649
+ new Promise((resolve, reject) => {
6650
+ setTimeout(() => reject(new Error('SFU WS connection timed out')), this.joinResponseTimeout);
6651
+ }),
6652
+ ]));
6479
6653
  };
6480
6654
  this.cleanUpWebSocket = () => {
6481
- this.signalWs.removeEventListener('error', this.restoreWebSocket);
6482
6655
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6483
6656
  };
6484
- this.restoreWebSocket = () => {
6485
- withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
6486
- await this.networkAvailableTask?.promise;
6487
- this.logger('debug', 'Restoring SFU WS connection');
6488
- this.cleanUpWebSocket();
6489
- await sleep(500);
6490
- this.createWebSocket();
6491
- }).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
6492
- };
6493
6657
  this.handleWebSocketClose = () => {
6494
6658
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6495
- clearInterval(this.keepAliveInterval);
6659
+ getTimers().clearInterval(this.keepAliveInterval);
6496
6660
  clearTimeout(this.connectionCheckTimeout);
6497
6661
  this.onSignalClose?.();
6498
6662
  };
@@ -6583,7 +6747,7 @@ class StreamSfuClient {
6583
6747
  };
6584
6748
  this.join = async (data) => {
6585
6749
  // wait for the signal web socket to be ready before sending "joinRequest"
6586
- await this.signalReady;
6750
+ await this.signalReady();
6587
6751
  if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
6588
6752
  // we need to lock the RPC requests until we receive a JoinResponse.
6589
6753
  // that's why we have this primitive lock mechanism.
@@ -6638,7 +6802,7 @@ class StreamSfuClient {
6638
6802
  }));
6639
6803
  };
6640
6804
  this.send = async (message) => {
6641
- await this.signalReady; // wait for the signal ws to be open
6805
+ await this.signalReady(); // wait for the signal ws to be open
6642
6806
  const msgJson = SfuRequest.toJson(message);
6643
6807
  if (this.signalWs.readyState !== WebSocket.OPEN) {
6644
6808
  this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
@@ -6648,8 +6812,9 @@ class StreamSfuClient {
6648
6812
  this.signalWs.send(SfuRequest.toBinary(message));
6649
6813
  };
6650
6814
  this.keepAlive = () => {
6651
- clearInterval(this.keepAliveInterval);
6652
- this.keepAliveInterval = setInterval(() => {
6815
+ const timers = getTimers();
6816
+ timers.clearInterval(this.keepAliveInterval);
6817
+ this.keepAliveInterval = timers.setInterval(() => {
6653
6818
  this.ping().catch((e) => {
6654
6819
  this.logger('error', 'Error sending healthCheckRequest to SFU', e);
6655
6820
  });
@@ -8230,20 +8395,6 @@ function canQueryPermissions() {
8230
8395
  !!navigator.permissions?.query);
8231
8396
  }
8232
8397
 
8233
- const uninitialized = Symbol('uninitialized');
8234
- /**
8235
- * Lazily creates a value using a provided factory
8236
- */
8237
- function lazy(factory) {
8238
- let value = uninitialized;
8239
- return () => {
8240
- if (value === uninitialized) {
8241
- value = factory();
8242
- }
8243
- return value;
8244
- };
8245
- }
8246
-
8247
8398
  /**
8248
8399
  * Returns an Observable that emits the list of available devices
8249
8400
  * that meet the given constraints.
@@ -9855,6 +10006,7 @@ class Call {
9855
10006
  this.reconnectAttempts = 0;
9856
10007
  this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
9857
10008
  this.fastReconnectDeadlineSeconds = 0;
10009
+ this.disconnectionTimeoutSeconds = 0;
9858
10010
  this.lastOfflineTimestamp = 0;
9859
10011
  // maintain the order of publishing tracks to restore them after a reconnection
9860
10012
  // it shouldn't contain duplicates
@@ -10371,6 +10523,10 @@ class Call {
10371
10523
  */
10372
10524
  this.handleSfuSignalClose = (sfuClient) => {
10373
10525
  this.logger('debug', '[Reconnect] SFU signal connection closed');
10526
+ // SFU WS closed before we finished current join, no need to schedule reconnect
10527
+ // because join operation will fail
10528
+ if (this.state.callingState === exports.CallingState.JOINING)
10529
+ return;
10374
10530
  // normal close, no need to reconnect
10375
10531
  if (sfuClient.isLeaving)
10376
10532
  return;
@@ -10386,10 +10542,21 @@ class Call {
10386
10542
  * @param strategy the reconnection strategy to use.
10387
10543
  */
10388
10544
  this.reconnect = async (strategy) => {
10545
+ if (this.state.callingState === exports.CallingState.RECONNECTING ||
10546
+ this.state.callingState === exports.CallingState.RECONNECTING_FAILED)
10547
+ return;
10389
10548
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
10390
10549
  this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
10550
+ let reconnectStartTime = Date.now();
10391
10551
  this.reconnectStrategy = strategy;
10392
10552
  do {
10553
+ if (this.disconnectionTimeoutSeconds > 0 &&
10554
+ (Date.now() - reconnectStartTime) / 1000 >
10555
+ this.disconnectionTimeoutSeconds) {
10556
+ this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
10557
+ this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
10558
+ return;
10559
+ }
10393
10560
  // we don't increment reconnect attempts for the FAST strategy.
10394
10561
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
10395
10562
  this.reconnectAttempts++;
@@ -10489,7 +10656,7 @@ class Call {
10489
10656
  const currentPublisher = this.publisher;
10490
10657
  currentSubscriber?.detachEventHandlers();
10491
10658
  currentPublisher?.detachEventHandlers();
10492
- const migrationTask = currentSfuClient.enterMigration();
10659
+ const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
10493
10660
  try {
10494
10661
  const currentSfu = currentSfuClient.edgeName;
10495
10662
  await this.join({ ...this.joinCallData, migrating_from: currentSfu });
@@ -10505,7 +10672,7 @@ class Call {
10505
10672
  // Wait for the migration to complete, then close the previous SFU client
10506
10673
  // and the peer connection instances. In case of failure, the migration
10507
10674
  // task would throw an error and REJOIN would be attempted.
10508
- await migrationTask;
10675
+ await migrationTask();
10509
10676
  // in MIGRATE, we can consider the call as joined only after
10510
10677
  // `participantMigrationComplete` event is received, signaled by
10511
10678
  // the `migrationTask`
@@ -11361,6 +11528,14 @@ class Call {
11361
11528
  this.dynascaleManager.setVideoTrackSubscriptionOverrides(enabled ? undefined : { enabled: false });
11362
11529
  this.dynascaleManager.applyTrackSubscriptions();
11363
11530
  };
11531
+ /**
11532
+ * Sets the maximum amount of time a user can remain waiting for a reconnect
11533
+ * after a network disruption
11534
+ * @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
11535
+ */
11536
+ this.setDisconnectionTimeout = (timeoutSeconds) => {
11537
+ this.disconnectionTimeoutSeconds = timeoutSeconds;
11538
+ };
11364
11539
  this.type = type;
11365
11540
  this.id = id;
11366
11541
  this.cid = `${type}:${id}`;
@@ -11514,33 +11689,6 @@ class Call {
11514
11689
  }
11515
11690
  }
11516
11691
 
11517
- /**
11518
- * Saving a long-lived reference to a promise that can reject can be unsafe,
11519
- * since rejecting the promise causes an unhandled rejection error (even if the
11520
- * rejection is handled everywhere promise result is expected).
11521
- *
11522
- * To avoid that, we add both resolution and rejection handlers to the promise.
11523
- * That way, the saved promise never rejects. A callback is provided as return
11524
- * value to build a *new* promise, that resolves and rejects along with
11525
- * the original promise.
11526
- * @param promise Promise to wrap, which possibly rejects
11527
- * @returns Callback to build a new promise, which resolves and rejects along
11528
- * with the original promise
11529
- */
11530
- function makeSafePromise(promise) {
11531
- let isPending = true;
11532
- const safePromise = promise
11533
- .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
11534
- .finally(() => (isPending = false));
11535
- const unwrapPromise = () => safePromise.then((fulfillment) => {
11536
- if (fulfillment.status === 'rejected')
11537
- throw fulfillment.error;
11538
- return fulfillment.result;
11539
- });
11540
- unwrapPromise.checkPending = () => isPending;
11541
- return unwrapPromise;
11542
- }
11543
-
11544
11692
  /**
11545
11693
  * StableWSConnection - A WS connection that reconnects upon failure.
11546
11694
  * - the browser will sometimes report that you're online or offline
@@ -11793,9 +11941,12 @@ class StableWSConnection {
11793
11941
  * Schedules a next health check ping for websocket.
11794
11942
  */
11795
11943
  this.scheduleNextPing = () => {
11944
+ const timers = getTimers();
11945
+ if (this.healthCheckTimeoutRef) {
11946
+ timers.clearTimeout(this.healthCheckTimeoutRef);
11947
+ }
11796
11948
  // 30 seconds is the recommended interval (messenger uses this)
11797
- clearTimeout(this.healthCheckTimeoutRef);
11798
- this.healthCheckTimeoutRef = setTimeout(() => {
11949
+ this.healthCheckTimeoutRef = timers.setTimeout(() => {
11799
11950
  // send the healthcheck..., server replies with a health check event
11800
11951
  const data = [{ type: 'health.check', client_id: this.client.clientID }];
11801
11952
  // try to send on the connection
@@ -11938,8 +12089,12 @@ class StableWSConnection {
11938
12089
  this.isConnecting = false;
11939
12090
  this.isDisconnected = true;
11940
12091
  // start by removing all the listeners
11941
- clearInterval(this.healthCheckTimeoutRef);
11942
- clearInterval(this.connectionCheckTimeoutRef);
12092
+ if (this.healthCheckTimeoutRef) {
12093
+ getTimers().clearInterval(this.healthCheckTimeoutRef);
12094
+ }
12095
+ if (this.connectionCheckTimeoutRef) {
12096
+ clearInterval(this.connectionCheckTimeoutRef);
12097
+ }
11943
12098
  removeConnectionEventListeners(this.onlineStatusChanged);
11944
12099
  this.isHealthy = false;
11945
12100
  let isClosedPromise;
@@ -12639,7 +12794,7 @@ class StreamClient {
12639
12794
  return await this.wsConnection.connect(this.defaultWSTimeout);
12640
12795
  };
12641
12796
  this.getUserAgent = () => {
12642
- const version = "1.11.11";
12797
+ const version = "1.11.13";
12643
12798
  return (this.userAgent ||
12644
12799
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12645
12800
  };
@@ -13050,10 +13205,14 @@ class StreamVideoClient {
13050
13205
  if (typeof apiKeyOrArgs === 'string') {
13051
13206
  logLevel = opts?.logLevel || logLevel;
13052
13207
  logger = opts?.logger || logger;
13208
+ if (opts?.expertimental_enableTimerWorker)
13209
+ enableTimerWorker();
13053
13210
  }
13054
13211
  else {
13055
13212
  logLevel = apiKeyOrArgs.options?.logLevel || logLevel;
13056
13213
  logger = apiKeyOrArgs.options?.logger || logger;
13214
+ if (apiKeyOrArgs.options?.expertimental_enableTimerWorker)
13215
+ enableTimerWorker();
13057
13216
  }
13058
13217
  setLogger(logger, logLevel);
13059
13218
  this.logger = getLogger(['client']);