@pluv/client 0.8.0 → 0.9.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
@@ -58,6 +58,7 @@ var __async = (__this, __arguments, generator) => {
58
58
  var src_exports = {};
59
59
  __export(src_exports, {
60
60
  AbstractRoom: () => AbstractRoom,
61
+ AbstractStorageStore: () => AbstractStorageStore,
61
62
  ConnectionState: () => ConnectionState,
62
63
  MockedRoom: () => MockedRoom,
63
64
  PluvClient: () => PluvClient,
@@ -74,10 +75,18 @@ var AbstractRoom = class {
74
75
  }
75
76
  };
76
77
 
78
+ // src/AbstractStorageStore.ts
79
+ var AbstractStorageStore = class {
80
+ constructor(room) {
81
+ this.room = room;
82
+ }
83
+ };
84
+
77
85
  // src/CrdtManager.ts
78
86
  var import_crdt_yjs = require("@pluv/crdt-yjs");
79
87
  var CrdtManager = class {
80
88
  constructor(options = {}) {
89
+ this.initialized = false;
81
90
  const { encodedState, initialStorage } = options;
82
91
  const _doc = encodedState ? (0, import_crdt_yjs.doc)().applyUpdate(encodedState) : null;
83
92
  if (_doc && !!Object.keys(_doc.toJSON()).length) {
@@ -87,12 +96,50 @@ var CrdtManager = class {
87
96
  _doc == null ? void 0 : _doc.destroy();
88
97
  this.doc = (0, import_crdt_yjs.doc)(initialStorage == null ? void 0 : initialStorage());
89
98
  }
99
+ /**
100
+ * !HACK
101
+ * @description If this is called before the doc is initialized. Omit the
102
+ * update. This unfortunately means updates can be dropped entirely
103
+ * @date June 13, 2023
104
+ */
105
+ applyUpdate(update, origin) {
106
+ if (!this.initialized)
107
+ return this;
108
+ const updates = typeof update === "string" ? [update] : update;
109
+ this._applyDocUpdates(this.doc, updates, origin);
110
+ return this;
111
+ }
90
112
  destroy() {
91
113
  this.doc.destroy();
92
114
  }
93
115
  get(key) {
94
116
  return this.doc.get(key);
95
117
  }
118
+ initialize(params) {
119
+ const { onInitialized, origin, update } = params;
120
+ const updates = typeof update === "string" ? [update] : update;
121
+ if (!updates.length)
122
+ return this;
123
+ if (this.initialized) {
124
+ this._applyDocUpdates(this.doc, updates, origin);
125
+ return this;
126
+ }
127
+ const _doc = this._applyDocUpdates((0, import_crdt_yjs.doc)(), updates, origin);
128
+ this.initialized = true;
129
+ if (!!Object.keys(_doc.toJSON()).length) {
130
+ this.doc = _doc;
131
+ } else {
132
+ _doc.destroy();
133
+ }
134
+ onInitialized == null ? void 0 : onInitialized();
135
+ return this;
136
+ }
137
+ _applyDocUpdates(yjsDoc, updates, origin) {
138
+ yjsDoc.transact(() => {
139
+ updates.reduce((acc, _update) => acc.applyUpdate(_update), yjsDoc);
140
+ }, origin);
141
+ return yjsDoc;
142
+ }
96
143
  };
97
144
 
98
145
  // src/CrdtNotifier.ts
@@ -176,6 +223,31 @@ var StateNotifier = class {
176
223
  }
177
224
  };
178
225
 
226
+ // src/StorageStore.ts
227
+ var StorageStore = class extends AbstractStorageStore {
228
+ addUpdate() {
229
+ return Promise.resolve();
230
+ }
231
+ applyUpdates() {
232
+ return Promise.resolve();
233
+ }
234
+ destroy() {
235
+ return Promise.resolve();
236
+ }
237
+ flatten() {
238
+ return Promise.resolve();
239
+ }
240
+ getShouldFlatten() {
241
+ return Promise.resolve(false);
242
+ }
243
+ getUpdates() {
244
+ return Promise.resolve([]);
245
+ }
246
+ initialize() {
247
+ return Promise.resolve();
248
+ }
249
+ };
250
+
179
251
  // src/types.ts
180
252
  var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
181
253
  ConnectionState2["Closed"] = "Closed";
@@ -281,13 +353,40 @@ var UsersManager = class {
281
353
  }
282
354
  };
283
355
 
356
+ // src/utils/debounce.ts
357
+ var debounce = (fn, options) => {
358
+ const { wait, immediate = false } = options != null ? options : {};
359
+ let prevResult;
360
+ let timeout = null;
361
+ return (...args) => {
362
+ if (typeof timeout === "number")
363
+ clearTimeout(timeout);
364
+ timeout = setTimeout(() => {
365
+ timeout = null;
366
+ if (!immediate)
367
+ prevResult = fn(args);
368
+ }, wait);
369
+ if (immediate && !timeout) {
370
+ fn(args);
371
+ return;
372
+ }
373
+ return prevResult;
374
+ };
375
+ };
376
+
284
377
  // src/PluvRoom.ts
378
+ var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
285
379
  var HEARTBEAT_INTERVAL_MS = 1e4;
286
380
  var PONG_TIMEOUT_MS = 2e3;
287
381
  var RECONNECT_TIMEOUT_MS = 3e4;
382
+ var Y_ORIGIN_INITIALIZED = "$INITIALIZED";
383
+ var DEFAULT_PLUV_CLIENT_ADDON = (input) => ({
384
+ storage: new StorageStore(input.room.id)
385
+ });
288
386
  var PluvRoom = class extends AbstractRoom {
289
387
  constructor(room, options) {
290
388
  const {
389
+ addons = [],
291
390
  authEndpoint,
292
391
  debug = false,
293
392
  initialPresence,
@@ -297,10 +396,8 @@ var PluvRoom = class extends AbstractRoom {
297
396
  wsEndpoint
298
397
  } = options;
299
398
  super(room);
300
- this._crdtManager = null;
301
399
  this._crdtNotifier = new CrdtNotifier();
302
400
  this._eventNotifier = new EventNotifier();
303
- this._initialStorage = null;
304
401
  this._intervals = {
305
402
  heartbeat: null
306
403
  };
@@ -331,8 +428,7 @@ var PluvRoom = class extends AbstractRoom {
331
428
  return this._eventNotifier.subscribe(event, callback);
332
429
  };
333
430
  this.getMyPresence = () => {
334
- var _a, _b;
335
- return (_b = (_a = this._usersManager.myself) == null ? void 0 : _a.presence) != null ? _b : null;
431
+ return this._usersManager.myPresence;
336
432
  };
337
433
  this.getMyself = () => {
338
434
  return this._usersManager.myself;
@@ -368,6 +464,27 @@ var PluvRoom = class extends AbstractRoom {
368
464
  { presence }
369
465
  );
370
466
  };
467
+ this._flattenStorageStore = debounce(
468
+ () => __async(this, null, function* () {
469
+ const shouldFlatten = yield this._storageStore.getShouldFlatten();
470
+ if (!shouldFlatten)
471
+ return;
472
+ const updates = yield this._storageStore.getUpdates();
473
+ this._crdtManager.applyUpdate(updates, this);
474
+ const encodedState = this._crdtManager.doc.encodeStateAsUpdate();
475
+ yield this._storageStore.flatten(encodedState);
476
+ }),
477
+ { wait: ADD_TO_STORAGE_STATE_DEBOUNCE_MS }
478
+ );
479
+ this._getAddon = (addons) => {
480
+ return addons.reduce(
481
+ (acc, addon) => () => __spreadValues(__spreadValues({}, acc({ room: this })), addon({ room: this })),
482
+ DEFAULT_PLUV_CLIENT_ADDON
483
+ );
484
+ };
485
+ const addon = this._getAddon(addons);
486
+ const { storage } = __spreadValues(__spreadValues({}, DEFAULT_PLUV_CLIENT_ADDON({ room: this })), addon({ room: this }));
487
+ this._storageStore = storage;
371
488
  this._debug = debug;
372
489
  this._endpoints = { authEndpoint, wsEndpoint };
373
490
  this._listeners = {
@@ -384,8 +501,7 @@ var PluvRoom = class extends AbstractRoom {
384
501
  initialPresence,
385
502
  presence
386
503
  });
387
- if (initialStorage)
388
- this._initialStorage = initialStorage;
504
+ this._crdtManager = new CrdtManager({ initialStorage });
389
505
  }
390
506
  get webSocket() {
391
507
  return this._state.webSocket;
@@ -414,19 +530,17 @@ var PluvRoom = class extends AbstractRoom {
414
530
  oldState.connection.state = "Connecting" /* Connecting */;
415
531
  return oldState;
416
532
  });
533
+ yield this._storageStore.initialize();
534
+ yield this._applyStorageStore();
417
535
  const url = new URL(this._getWsEndpoint(this.id));
418
536
  let authToken = null;
419
- try {
420
- authToken = yield this._getAuthorization(this.id);
421
- } catch (err) {
422
- this._onAuthorizationFail(err);
423
- return;
424
- }
425
- authToken && url.searchParams.set("token", encodeURIComponent(authToken));
426
537
  let webSocket;
427
538
  try {
539
+ authToken = yield this._getAuthorization(this.id);
540
+ authToken && url.searchParams.set("token", encodeURIComponent(authToken));
428
541
  webSocket = new WebSocket(url.toString());
429
542
  } catch (err) {
543
+ yield this._storageStore.destroy();
430
544
  this._onAuthorizationFail(err);
431
545
  return;
432
546
  }
@@ -440,27 +554,24 @@ var PluvRoom = class extends AbstractRoom {
440
554
  });
441
555
  }
442
556
  disconnect() {
443
- var _a;
444
- if (!this._endpoints.wsEndpoint) {
445
- throw new Error("Must provide an wsEndpoint.");
446
- }
447
- if (!this._state.webSocket)
448
- return;
449
- const canDisconnect = [
450
- "Connecting" /* Connecting */,
451
- "Open" /* Open */
452
- ].some((state) => this._state.connection.state === state);
453
- if (!canDisconnect)
454
- return;
455
- this._clearInterval(this._intervals.heartbeat);
456
- this._closeWs();
457
- (_a = this._crdtManager) == null ? void 0 : _a.destroy();
458
- this._stateNotifier.subjects["my-presence"].next(null);
459
- this._updateState((oldState) => {
460
- oldState.authorization.token = null;
461
- oldState.connection.state = "Closed" /* Closed */;
462
- oldState.webSocket = null;
463
- return oldState;
557
+ return __async(this, null, function* () {
558
+ var _a;
559
+ if (!this._endpoints.wsEndpoint) {
560
+ throw new Error("Must provide an wsEndpoint.");
561
+ }
562
+ if (!this._state.webSocket)
563
+ return;
564
+ const canDisconnect = [
565
+ "Connecting" /* Connecting */,
566
+ "Open" /* Open */
567
+ ].some((state) => this._state.connection.state === state);
568
+ if (!canDisconnect)
569
+ return;
570
+ this._clearInterval(this._intervals.heartbeat);
571
+ this._closeWs();
572
+ yield this._storageStore.destroy();
573
+ (_a = this._crdtManager) == null ? void 0 : _a.destroy();
574
+ this._stateNotifier.subjects["my-presence"].next(null);
464
575
  });
465
576
  }
466
577
  getConnection() {
@@ -468,6 +579,25 @@ var PluvRoom = class extends AbstractRoom {
468
579
  JSON.parse(JSON.stringify(this._state.connection))
469
580
  );
470
581
  }
582
+ _addToStorageStore(update) {
583
+ return __async(this, null, function* () {
584
+ yield this._storageStore.addUpdate(update);
585
+ this._flattenStorageStore();
586
+ });
587
+ }
588
+ _applyStorageStore() {
589
+ return __async(this, null, function* () {
590
+ const updates = yield this._storageStore.getUpdates();
591
+ this._crdtManager.initialize({
592
+ onInitialized: () => {
593
+ this._observeCrdt();
594
+ },
595
+ origin: Y_ORIGIN_INITIALIZED,
596
+ update: updates
597
+ });
598
+ this._emitSharedTypes();
599
+ });
600
+ }
471
601
  _attachWindowListeners() {
472
602
  if (typeof window === "undefined")
473
603
  return;
@@ -540,7 +670,6 @@ var PluvRoom = class extends AbstractRoom {
540
670
  if (canClose)
541
671
  this._state.webSocket.close();
542
672
  this._detachWsListeners();
543
- this._stateNotifier.subjects["myself"].next(null);
544
673
  }
545
674
  _detachWindowListeners() {
546
675
  if (!this._windowListeners)
@@ -580,23 +709,11 @@ var PluvRoom = class extends AbstractRoom {
580
709
  );
581
710
  this._wsListeners = null;
582
711
  }
583
- _getAuthorization(room) {
584
- return __async(this, null, function* () {
585
- const fetchOptions = this._getAuthFetchOptions(room);
586
- if (!fetchOptions)
587
- return null;
588
- const { url, options } = fetchOptions;
589
- const res = yield fetch(url, options);
590
- if (!res.ok || res.status !== 200) {
591
- throw new Error("Room is unauthorized");
592
- }
593
- try {
594
- return yield res.text().then((text2) => text2.trim());
595
- } catch (err) {
596
- throw new Error(
597
- err instanceof Error ? err.message : "Room is unauthorized"
598
- );
599
- }
712
+ _emitSharedTypes() {
713
+ const sharedTypes = this._crdtManager.doc.getSharedTypes();
714
+ Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
715
+ const serialized = sharedType.toJSON();
716
+ this._crdtNotifier.subject(prop).next(serialized);
600
717
  });
601
718
  }
602
719
  _getAuthFetchOptions(room) {
@@ -617,6 +734,25 @@ var PluvRoom = class extends AbstractRoom {
617
734
  const result = this._endpoints.authEndpoint(room);
618
735
  return typeof result === "string" ? { url: result, options: {} } : result;
619
736
  }
737
+ _getAuthorization(room) {
738
+ return __async(this, null, function* () {
739
+ const fetchOptions = this._getAuthFetchOptions(room);
740
+ if (!fetchOptions)
741
+ return null;
742
+ const { url, options } = fetchOptions;
743
+ const res = yield fetch(url, options);
744
+ if (!res.ok || res.status !== 200) {
745
+ throw new Error("Room is unauthorized");
746
+ }
747
+ try {
748
+ return yield res.text().then((text2) => text2.trim());
749
+ } catch (err) {
750
+ throw new Error(
751
+ err instanceof Error ? err.message : "Room is unauthorized"
752
+ );
753
+ }
754
+ });
755
+ }
620
756
  _getWsEndpoint(room) {
621
757
  switch (typeof this._endpoints.wsEndpoint) {
622
758
  case "undefined":
@@ -712,28 +848,28 @@ var PluvRoom = class extends AbstractRoom {
712
848
  });
713
849
  }
714
850
  _handleStorageReceivedMessage(message) {
715
- var _a;
716
- const { connectionId } = message;
717
- if (!connectionId)
718
- return;
719
- if (!this._state.webSocket)
720
- throw new Error("Could not find WebSocket");
721
- const data = message.data;
722
- this._crdtManager = new CrdtManager({
723
- encodedState: data.state,
724
- initialStorage: (_a = this._initialStorage) != null ? _a : void 0
725
- });
726
- this._observeCrdt();
727
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
728
- Object.keys(sharedTypes).forEach((key) => {
729
- const sharedType = sharedTypes[key];
730
- const serialized = sharedType.toJSON();
731
- this._crdtNotifier.subject(key).next(serialized);
732
- });
733
- const update = this._crdtManager.doc.encodeStateAsUpdate();
734
- this._sendMessage({
735
- type: "$UPDATE_STORAGE",
736
- data: { origin: "$STORAGE_RECEIVED", update }
851
+ return __async(this, null, function* () {
852
+ const { connectionId } = message;
853
+ if (!connectionId)
854
+ return;
855
+ if (!this._state.webSocket)
856
+ throw new Error("Could not find WebSocket");
857
+ const data = message.data;
858
+ this._crdtManager.initialize({
859
+ onInitialized: () => __async(this, null, function* () {
860
+ this._observeCrdt();
861
+ }),
862
+ origin: Y_ORIGIN_INITIALIZED,
863
+ update: data.state
864
+ });
865
+ const encodedState = this._crdtManager.doc.encodeStateAsUpdate();
866
+ yield this._storageStore.addUpdate(encodedState);
867
+ this._emitSharedTypes();
868
+ const update = this._crdtManager.doc.encodeStateAsUpdate();
869
+ this._sendMessage({
870
+ type: "$UPDATE_STORAGE",
871
+ data: { origin: Y_ORIGIN_INITIALIZED, update }
872
+ });
737
873
  });
738
874
  }
739
875
  _handleStorageUpdatedMessage(message) {
@@ -747,14 +883,11 @@ var PluvRoom = class extends AbstractRoom {
747
883
  return;
748
884
  this._crdtManager.doc.applyUpdate(data.state, "$STORAGE_UPDATED");
749
885
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
750
- Object.keys(sharedTypes).forEach((key) => {
886
+ Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
751
887
  if (!this._crdtManager)
752
888
  return;
753
- const sharedType = sharedTypes[key];
754
- if (!sharedType)
755
- return;
756
889
  const serialized = sharedType.toJSON();
757
- this._crdtNotifier.subject(key).next(serialized);
890
+ this._crdtNotifier.subject(prop).next(serialized);
758
891
  });
759
892
  }
760
893
  _handleUserJoinedMessage(message) {
@@ -802,7 +935,9 @@ var PluvRoom = class extends AbstractRoom {
802
935
  const unsubscribe = this._crdtManager.doc.subscribe(
803
936
  (update, _origin) => {
804
937
  const origin = _origin != null ? _origin : null;
805
- if (!this._crdtManager)
938
+ this._addToStorageStore(update);
939
+ this._emitSharedTypes();
940
+ if (origin === Y_ORIGIN_INITIALIZED || origin === this)
806
941
  return;
807
942
  if (!this._state.webSocket)
808
943
  return;
@@ -812,13 +947,6 @@ var PluvRoom = class extends AbstractRoom {
812
947
  return;
813
948
  if (origin === "$STORAGE_UPDATED")
814
949
  return;
815
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
816
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
817
- if (!this._crdtManager)
818
- return;
819
- const serialized = sharedType.toJSON();
820
- this._crdtNotifier.subject(prop).next(serialized);
821
- });
822
950
  this._sendMessage({
823
951
  type: "$UPDATE_STORAGE",
824
952
  data: { origin, update }
@@ -843,14 +971,18 @@ var PluvRoom = class extends AbstractRoom {
843
971
  * @date August 19, 2022
844
972
  */
845
973
  _onClose(event) {
846
- this._logDebug("WebSocket closed");
847
- !!event.reason && this._logDebug(event.reason);
848
- this._clearInterval(this._intervals.heartbeat);
849
- this._clearTimeout(this._timeouts.pong);
850
- this._updateState((oldState) => {
851
- oldState.connection.state = "Closed" /* Closed */;
852
- oldState.webSocket = null;
853
- return oldState;
974
+ return __async(this, null, function* () {
975
+ this._logDebug("WebSocket closed");
976
+ !!event.reason && this._logDebug(event.reason);
977
+ this._clearInterval(this._intervals.heartbeat);
978
+ this._clearTimeout(this._timeouts.pong);
979
+ this._stateNotifier.subjects["myself"].next(null);
980
+ this._updateState((oldState) => {
981
+ oldState.authorization.token = null;
982
+ oldState.connection.state = "Closed" /* Closed */;
983
+ oldState.webSocket = null;
984
+ return oldState;
985
+ });
854
986
  });
855
987
  }
856
988
  /**
@@ -1023,6 +1155,7 @@ var PluvClient = class {
1023
1155
  if (oldRoom)
1024
1156
  return oldRoom;
1025
1157
  const newRoom = new PluvRoom(room, {
1158
+ addons: options.addons,
1026
1159
  authEndpoint: this._authEndpoint,
1027
1160
  debug: options.debug,
1028
1161
  initialPresence: options.initialPresence,
@@ -1051,14 +1184,14 @@ var PluvClient = class {
1051
1184
  this.getRooms = () => {
1052
1185
  return Array.from(this._rooms.values());
1053
1186
  };
1054
- this.leave = (room) => {
1187
+ this.leave = (room) => __async(this, null, function* () {
1055
1188
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
1056
1189
  if (!toLeave)
1057
1190
  return;
1058
- toLeave.disconnect();
1191
+ yield toLeave.disconnect();
1059
1192
  this._rooms.delete(toLeave.id);
1060
1193
  this._logDebug(`Left and deleted room: ${room}`);
1061
- };
1194
+ });
1062
1195
  const { authEndpoint, debug = false, wsEndpoint } = options;
1063
1196
  this._authEndpoint = authEndpoint;
1064
1197
  this._debug = debug;
@@ -1106,8 +1239,7 @@ var MockedRoom = class extends AbstractRoom {
1106
1239
  return this._eventNotifier.subscribe(event, callback);
1107
1240
  };
1108
1241
  this.getMyPresence = () => {
1109
- var _a, _b;
1110
- return (_b = (_a = this._usersManager.myself) == null ? void 0 : _a.presence) != null ? _b : null;
1242
+ return this._usersManager.myPresence;
1111
1243
  };
1112
1244
  this.getMyself = () => {
1113
1245
  return this._usersManager.myself;
@@ -1206,6 +1338,7 @@ var import_crdt_yjs2 = require("@pluv/crdt-yjs");
1206
1338
  // Annotate the CommonJS export names for ESM import in node:
1207
1339
  0 && (module.exports = {
1208
1340
  AbstractRoom,
1341
+ AbstractStorageStore,
1209
1342
  ConnectionState,
1210
1343
  MockedRoom,
1211
1344
  PluvClient,