@pluv/client 0.38.14 → 0.39.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.
package/dist/index.js CHANGED
@@ -438,7 +438,9 @@ var debounce = (fn, options) => {
438
438
  var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
439
439
  var HEARTBEAT_INTERVAL_MS = 1e4;
440
440
  var PONG_TIMEOUT_MS = 2e3;
441
- var RECONNECT_TIMEOUT_MS = 3e4;
441
+ var RECONNECT_TIMEOUT_MS = 5e3;
442
+ var MIN_RECONNECT_TIMEOUT_MS = 1e3;
443
+ var MAX_RECONNECT_TIMEOUT_MS = 6e4;
442
444
  var ORIGIN_INITIALIZED = "$initialized";
443
445
  var ORIGIN_STORAGE_UPDATED = "$storageUpdated";
444
446
  var DEFAULT_PLUV_CLIENT_ADDON = (input) => ({
@@ -456,6 +458,7 @@ var PluvRoom = class extends AbstractRoom {
456
458
  onAuthorizationFail,
457
459
  presence,
458
460
  publicKey,
461
+ reconnectTimeoutMs = RECONNECT_TIMEOUT_MS,
459
462
  router,
460
463
  wsEndpoint
461
464
  } = options;
@@ -465,7 +468,6 @@ var PluvRoom = class extends AbstractRoom {
465
468
  this._intervals = {
466
469
  heartbeat: null
467
470
  };
468
- this._metadata = {};
469
471
  this._otherNotifier = new OtherNotifier();
470
472
  this._publicKey = null;
471
473
  this._stateNotifier = new StateNotifier();
@@ -476,12 +478,14 @@ var PluvRoom = class extends AbstractRoom {
476
478
  pong: null,
477
479
  reconnect: null
478
480
  };
481
+ this._lastMetadata = null;
479
482
  this._state = {
480
483
  authorization: {
481
484
  token: null,
482
485
  user: null
483
486
  },
484
487
  connection: {
488
+ attempts: 0,
485
489
  count: 0,
486
490
  id: null,
487
491
  state: "Untouched" /* Untouched */
@@ -620,18 +624,14 @@ var PluvRoom = class extends AbstractRoom {
620
624
  };
621
625
  const addon = this._getAddon(addons);
622
626
  const { storage } = __spreadValues(__spreadValues({}, DEFAULT_PLUV_CLIENT_ADDON({ room: this })), addon({ room: this }));
627
+ this.metadata = metadata;
623
628
  this._debug = debug;
624
629
  this._endpoints = { authEndpoint, wsEndpoint };
630
+ this._reconnectTimeoutMs = reconnectTimeoutMs;
625
631
  this._storageStore = storage;
626
- if (typeof metadata !== "undefined") this._metadata = metadata;
627
632
  if (!!publicKey) this._publicKey = publicKey;
628
633
  this._listeners = {
629
634
  onAuthorizationFail: (error) => {
630
- this._clearTimeout(this._timeouts.reconnect);
631
- this._timeouts.reconnect = setTimeout(
632
- this._reconnect.bind(this),
633
- RECONNECT_TIMEOUT_MS
634
- );
635
635
  onAuthorizationFail == null ? void 0 : onAuthorizationFail(error);
636
636
  }
637
637
  };
@@ -645,11 +645,13 @@ var PluvRoom = class extends AbstractRoom {
645
645
  get webSocket() {
646
646
  return this._state.webSocket;
647
647
  }
648
- connect() {
648
+ connect(...args) {
649
649
  return __async(this, null, function* () {
650
- if (!this._endpoints.wsEndpoint) {
651
- throw new Error("Must provide an wsEndpoint.");
652
- }
650
+ var _a;
651
+ const params = (_a = args[0]) != null ? _a : {};
652
+ const metadata = params.metadata;
653
+ this._setMetadata(metadata);
654
+ if (!this._endpoints.wsEndpoint) throw new Error("Must provide a wsEndpoint.");
653
655
  const canConnect = ["Closed" /* Closed */, "Untouched" /* Untouched */].some(
654
656
  (state) => this._state.connection.state === state
655
657
  );
@@ -661,21 +663,27 @@ var PluvRoom = class extends AbstractRoom {
661
663
  });
662
664
  yield this._storageStore.initialize();
663
665
  yield this._applyStorageStore();
664
- const url = new URL(this._getWsEndpoint(this.id));
666
+ const url = new URL(this._getWsEndpoint(this.id, params));
665
667
  let authToken = null;
666
668
  let webSocket;
667
669
  try {
668
- const publicKey = this._getPublicKey();
669
- authToken = yield this._getAuthorization(this.id);
670
+ const publicKey = this._getPublicKey(params);
671
+ authToken = yield this._getAuthorization(this.id, params);
670
672
  authToken && url.searchParams.set("token", encodeURIComponent(authToken));
671
673
  publicKey && url.searchParams.set("public_key", encodeURIComponent(publicKey));
672
674
  webSocket = new WebSocket(url.toString());
673
675
  } catch (err) {
676
+ this._updateState((oldState) => {
677
+ oldState.connection.attempts += 1;
678
+ return oldState;
679
+ });
674
680
  yield this._storageStore.destroy();
675
- this._onAuthorizationFail(err);
681
+ this._pollReconnect();
682
+ yield Promise.resolve(this._onAuthorizationFail(err));
676
683
  return;
677
684
  }
678
685
  this._updateState((oldState) => {
686
+ oldState.connection.attempts = 0;
679
687
  oldState.authorization.token = authToken;
680
688
  oldState.webSocket = webSocket;
681
689
  return oldState;
@@ -686,16 +694,21 @@ var PluvRoom = class extends AbstractRoom {
686
694
  }
687
695
  disconnect() {
688
696
  return __async(this, null, function* () {
689
- if (!this._endpoints.wsEndpoint) {
690
- throw new Error("Must provide an wsEndpoint.");
691
- }
697
+ if (!this._endpoints.wsEndpoint) throw new Error("Must provide a wsEndpoint.");
692
698
  if (!this._state.webSocket) return;
693
699
  const canDisconnect = ["Connecting" /* Connecting */, "Open" /* Open */].some(
694
700
  (state) => this._state.connection.state === state
695
701
  );
696
702
  if (!canDisconnect) return;
697
- this._clearInterval(this._intervals.heartbeat);
698
703
  this._closeWs();
704
+ this._clearInterval(this._intervals.heartbeat);
705
+ this._clearTimeout(this._timeouts.reconnect);
706
+ this._updateState((oldState) => {
707
+ oldState.authorization.token = null;
708
+ oldState.connection.state = "Closed" /* Closed */;
709
+ oldState.webSocket = null;
710
+ return oldState;
711
+ });
699
712
  yield this._storageStore.destroy();
700
713
  this._crdtManager.destroy();
701
714
  this._stateNotifier.subjects["my-presence"].next(null);
@@ -732,9 +745,11 @@ var PluvRoom = class extends AbstractRoom {
732
745
  if (typeof window === "undefined") return;
733
746
  if (typeof document === "undefined") return;
734
747
  this._windowListeners = {
748
+ onNavigatorOffline: this._onNavigatorOffline.bind(this),
735
749
  onNavigatorOnline: this._onNavigatorOnline.bind(this),
736
750
  onVisibilityChange: this._onVisibilityChange.bind(this)
737
751
  };
752
+ window.addEventListener("offline", this._windowListeners.onNavigatorOffline);
738
753
  window.addEventListener("online", this._windowListeners.onNavigatorOnline);
739
754
  document.addEventListener("visibilitychange", this._windowListeners.onVisibilityChange);
740
755
  }
@@ -760,24 +775,17 @@ var PluvRoom = class extends AbstractRoom {
760
775
  clearTimeout(timeout);
761
776
  }
762
777
  _closeWs() {
763
- var _a, _b;
764
- if (!this._state.webSocket) return;
778
+ var _a, _b, _c;
765
779
  (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
780
+ this._clearInterval(this._intervals.heartbeat);
766
781
  this._clearTimeout(this._timeouts.pong);
767
- this._clearTimeout(this._timeouts.reconnect);
768
782
  this._detachWindowListeners();
769
783
  this._usersManager.removeMyself();
770
784
  this._usersManager.removeUsers();
771
785
  this._otherNotifier.clear();
772
786
  this._stateNotifier.subjects.myself.next(null);
773
787
  this._stateNotifier.subjects.others.next([]);
774
- const canClose = [WebSocket.CONNECTING, WebSocket.OPEN].some(
775
- (readyState) => {
776
- var _a2;
777
- return readyState === ((_a2 = this._state.webSocket) == null ? void 0 : _a2.readyState);
778
- }
779
- );
780
- if (canClose) this._state.webSocket.close();
788
+ (_c = this._state.webSocket) == null ? void 0 : _c.close();
781
789
  this._detachWsListeners();
782
790
  }
783
791
  _detachWindowListeners() {
@@ -808,7 +816,8 @@ var PluvRoom = class extends AbstractRoom {
808
816
  );
809
817
  this._crdtNotifier.rootSubject.next(storageRoot);
810
818
  }
811
- _getAuthFetchOptions(room) {
819
+ _getAuthFetchOptions(room, params) {
820
+ const metadata = params.metadata;
812
821
  if (typeof this._endpoints.authEndpoint === "undefined") return null;
813
822
  if (this._endpoints.authEndpoint === true) {
814
823
  return {
@@ -822,18 +831,16 @@ var PluvRoom = class extends AbstractRoom {
822
831
  options: {}
823
832
  };
824
833
  }
825
- const result = this._endpoints.authEndpoint({ metadata: this._metadata, room });
834
+ const result = this._endpoints.authEndpoint({ metadata, room });
826
835
  return typeof result === "string" ? { url: result, options: {} } : result;
827
836
  }
828
- _getAuthorization(room) {
837
+ _getAuthorization(room, params) {
829
838
  return __async(this, null, function* () {
830
- const fetchOptions = this._getAuthFetchOptions(room);
839
+ const fetchOptions = this._getAuthFetchOptions(room, params);
831
840
  if (!fetchOptions) return null;
832
841
  const { url, options } = fetchOptions;
833
842
  const res = yield fetch(url, options);
834
- if (!res.ok || res.status !== 200) {
835
- throw new Error("Room is unauthorized");
836
- }
843
+ if (!res.ok || res.status !== 200) throw new Error("Room is unauthorized");
837
844
  try {
838
845
  return yield res.text().then((text) => text.trim());
839
846
  } catch (err) {
@@ -841,19 +848,21 @@ var PluvRoom = class extends AbstractRoom {
841
848
  }
842
849
  });
843
850
  }
844
- _getPublicKey() {
851
+ _getPublicKey(params) {
852
+ const metadata = params.metadata;
845
853
  if (!this._publicKey) return null;
846
854
  if (typeof this._publicKey === "string") return this._publicKey;
847
- return this._publicKey({ metadata: this._metadata });
855
+ return this._publicKey({ metadata });
848
856
  }
849
- _getWsEndpoint(room) {
857
+ _getWsEndpoint(room, params) {
858
+ const metadata = params.metadata;
850
859
  switch (typeof this._endpoints.wsEndpoint) {
851
860
  case "undefined":
852
- return !!this._getPublicKey() ? `wss://rooms.pluv.io/api/room/${room}` : `/api/pluv/room/${room}`;
861
+ return !!this._getPublicKey(params) ? `wss://rooms.pluv.io/api/room/${room}` : `/api/pluv/room/${room}`;
853
862
  case "string":
854
863
  return this._endpoints.wsEndpoint;
855
864
  default:
856
- return this._endpoints.wsEndpoint({ metadata: this._metadata, room });
865
+ return this._endpoints.wsEndpoint({ metadata, room });
857
866
  }
858
867
  }
859
868
  _handleExit(message) {
@@ -1020,23 +1029,30 @@ var PluvRoom = class extends AbstractRoom {
1020
1029
  return oldState;
1021
1030
  });
1022
1031
  }
1023
- /**
1024
- * TODO
1025
- * @description Determine if ws was closed with a code for closing without retries.
1026
- * Otherwise, attempt to retry again with incremental backoff
1027
- * @author leedavidcs
1028
- * @date August 19, 2022
1029
- */
1030
1032
  _onClose(event) {
1031
1033
  return __async(this, null, function* () {
1032
1034
  this._logDebug("WebSocket closed");
1033
1035
  !!event.reason && this._logDebug(event.reason);
1034
- this._clearInterval(this._intervals.heartbeat);
1035
- this._clearTimeout(this._timeouts.pong);
1036
- this._stateNotifier.subjects["myself"].next(null);
1036
+ const shouldRetry = [
1037
+ // Going away: Client/server is shutting down or navigating
1038
+ 1001,
1039
+ // Abnormal closure: Usually network error
1040
+ 1006,
1041
+ // Internal error: may be a larger problem, but try reconnecting for now
1042
+ 1011,
1043
+ // Service restart: Server restarted
1044
+ 1012,
1045
+ // Try again later: server overloaded
1046
+ 1013,
1047
+ // Bad gateway
1048
+ 1014
1049
+ ].some((okToRetryCode) => event.code === okToRetryCode);
1050
+ this._closeWs();
1051
+ if (!shouldRetry) this._clearTimeout(this._timeouts.reconnect);
1052
+ else this._pollReconnect();
1037
1053
  this._updateState((oldState) => {
1038
1054
  oldState.authorization.token = null;
1039
- oldState.connection.state = "Closed" /* Closed */;
1055
+ oldState.connection.state = shouldRetry ? "Unavailable" /* Unavailable */ : "Closed" /* Closed */;
1040
1056
  oldState.webSocket = null;
1041
1057
  return oldState;
1042
1058
  });
@@ -1045,7 +1061,6 @@ var PluvRoom = class extends AbstractRoom {
1045
1061
  /**
1046
1062
  * TODO
1047
1063
  * @description Handle websocket errors in a meaningful way, should they occur
1048
- * @author leedavidcs
1049
1064
  * @date August 19, 2022
1050
1065
  */
1051
1066
  _onError() {
@@ -1104,11 +1119,18 @@ var PluvRoom = class extends AbstractRoom {
1104
1119
  this._clearInterval(this._intervals.heartbeat);
1105
1120
  this._intervals.heartbeat = setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1106
1121
  }
1122
+ _onNavigatorOffline() {
1123
+ return __async(this, null, function* () {
1124
+ if (this._state.connection.state === "Closed" /* Closed */) return;
1125
+ this._updateState((oldState) => {
1126
+ oldState.connection.state = "Unavailable" /* Unavailable */;
1127
+ return oldState;
1128
+ });
1129
+ });
1130
+ }
1107
1131
  _onNavigatorOnline() {
1108
1132
  return __async(this, null, function* () {
1109
- if (this._state.connection.state !== "Unavailable" /* Unavailable */) {
1110
- return;
1111
- }
1133
+ if (this._state.connection.state !== "Unavailable" /* Unavailable */) return;
1112
1134
  yield this._reconnect();
1113
1135
  });
1114
1136
  }
@@ -1140,6 +1162,12 @@ var PluvRoom = class extends AbstractRoom {
1140
1162
  return null;
1141
1163
  }
1142
1164
  }
1165
+ _pollReconnect() {
1166
+ this._clearTimeout(this._timeouts.reconnect);
1167
+ const timeoutMs = typeof this._reconnectTimeoutMs === "number" ? this._reconnectTimeoutMs : this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
1168
+ const clampedMs = Math.max(Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs), MAX_RECONNECT_TIMEOUT_MS);
1169
+ this._timeouts.reconnect = setTimeout(this._reconnect.bind(this), clampedMs);
1170
+ }
1143
1171
  _reconnect() {
1144
1172
  return __async(this, null, function* () {
1145
1173
  if (!this._endpoints.wsEndpoint) {
@@ -1147,13 +1175,9 @@ var PluvRoom = class extends AbstractRoom {
1147
1175
  }
1148
1176
  if (!this._state.webSocket) return;
1149
1177
  this._closeWs();
1150
- this._updateState((oldState) => {
1151
- oldState.authorization.token = null;
1152
- oldState.connection.state = "Closed" /* Closed */;
1153
- oldState.webSocket = null;
1154
- return oldState;
1155
- });
1156
- yield this.connect();
1178
+ const metadata = this._lastMetadata;
1179
+ const params = typeof metadata === "undefined" ? [] : [{ metadata }];
1180
+ yield this.connect(...params);
1157
1181
  });
1158
1182
  }
1159
1183
  _sendMessage(data) {
@@ -1174,6 +1198,9 @@ var PluvRoom = class extends AbstractRoom {
1174
1198
  const message = typeof data === "string" ? data : JSON.stringify(data);
1175
1199
  webSocket.send(message);
1176
1200
  }
1201
+ _setMetadata(metadata) {
1202
+ return this.metadata ? this.metadata.parse(metadata) : metadata;
1203
+ }
1177
1204
  _updateState(updater) {
1178
1205
  let authorization;
1179
1206
  try {
@@ -1203,17 +1230,17 @@ var PluvClient = class {
1203
1230
  var _a;
1204
1231
  const oldRoom = this.getRoom(room);
1205
1232
  if (oldRoom) return oldRoom;
1206
- const metadata = this._metadata ? this._metadata.parse(options.metadata) : options.metadata;
1207
1233
  const newRoom = new PluvRoom(room, {
1208
1234
  addons: options.addons,
1209
1235
  authEndpoint: this._authEndpoint,
1210
1236
  debug: options.debug,
1211
1237
  initialPresence: options.initialPresence,
1212
1238
  initialStorage: this._initialStorage,
1213
- metadata,
1239
+ metadata: this.metadata,
1214
1240
  onAuthorizationFail: options.onAuthorizationFail,
1215
1241
  presence: this._presence,
1216
1242
  publicKey: (_a = this._publicKey) != null ? _a : void 0,
1243
+ reconnectTimeoutMs: options.reconnectTimeoutMs,
1217
1244
  router: options.router,
1218
1245
  wsEndpoint: this._wsEndpoint
1219
1246
  });
@@ -1221,11 +1248,11 @@ var PluvClient = class {
1221
1248
  this._logDebug(`New room was created: ${room}`);
1222
1249
  return newRoom;
1223
1250
  };
1224
- this.enter = (room) => __async(this, null, function* () {
1251
+ this.enter = (room, ...args) => __async(this, null, function* () {
1225
1252
  const toEnter = typeof room === "string" ? this.getRoom(room) : room;
1226
1253
  if (!toEnter) throw new Error(`Could not find room: ${room}.`);
1227
1254
  this._rooms.set(toEnter.id, toEnter);
1228
- yield toEnter.connect();
1255
+ yield toEnter.connect(...args);
1229
1256
  this._logDebug(`Entered room: ${room}`);
1230
1257
  return toEnter;
1231
1258
  });
@@ -1244,10 +1271,10 @@ var PluvClient = class {
1244
1271
  this._logDebug(`Left and deleted room: ${room}`);
1245
1272
  });
1246
1273
  const { authEndpoint, debug = false, initialStorage, metadata, presence, publicKey, wsEndpoint } = options;
1274
+ this.metadata = metadata;
1247
1275
  this._authEndpoint = authEndpoint;
1248
1276
  this._debug = debug;
1249
1277
  this._initialStorage = initialStorage;
1250
- this._metadata = metadata;
1251
1278
  this._presence = presence;
1252
1279
  this._wsEndpoint = wsEndpoint;
1253
1280
  if (!!publicKey) this._publicKey = publicKey;
@@ -1296,6 +1323,7 @@ var MockedRoom = class extends AbstractRoom {
1296
1323
  user: null
1297
1324
  },
1298
1325
  connection: {
1326
+ attempts: 0,
1299
1327
  count: 1,
1300
1328
  id: null,
1301
1329
  state: "Untouched" /* Untouched */