@pluv/client 0.8.1 → 0.9.1

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
  };
@@ -367,6 +464,27 @@ var PluvRoom = class extends AbstractRoom {
367
464
  { presence }
368
465
  );
369
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;
370
488
  this._debug = debug;
371
489
  this._endpoints = { authEndpoint, wsEndpoint };
372
490
  this._listeners = {
@@ -383,8 +501,7 @@ var PluvRoom = class extends AbstractRoom {
383
501
  initialPresence,
384
502
  presence
385
503
  });
386
- if (initialStorage)
387
- this._initialStorage = initialStorage;
504
+ this._crdtManager = new CrdtManager({ initialStorage });
388
505
  }
389
506
  get webSocket() {
390
507
  return this._state.webSocket;
@@ -413,19 +530,17 @@ var PluvRoom = class extends AbstractRoom {
413
530
  oldState.connection.state = "Connecting" /* Connecting */;
414
531
  return oldState;
415
532
  });
533
+ yield this._storageStore.initialize();
534
+ yield this._applyStorageStore();
416
535
  const url = new URL(this._getWsEndpoint(this.id));
417
536
  let authToken = null;
418
- try {
419
- authToken = yield this._getAuthorization(this.id);
420
- } catch (err) {
421
- this._onAuthorizationFail(err);
422
- return;
423
- }
424
- authToken && url.searchParams.set("token", encodeURIComponent(authToken));
425
537
  let webSocket;
426
538
  try {
539
+ authToken = yield this._getAuthorization(this.id);
540
+ authToken && url.searchParams.set("token", encodeURIComponent(authToken));
427
541
  webSocket = new WebSocket(url.toString());
428
542
  } catch (err) {
543
+ yield this._storageStore.destroy();
429
544
  this._onAuthorizationFail(err);
430
545
  return;
431
546
  }
@@ -439,27 +554,24 @@ var PluvRoom = class extends AbstractRoom {
439
554
  });
440
555
  }
441
556
  disconnect() {
442
- var _a;
443
- if (!this._endpoints.wsEndpoint) {
444
- throw new Error("Must provide an wsEndpoint.");
445
- }
446
- if (!this._state.webSocket)
447
- return;
448
- const canDisconnect = [
449
- "Connecting" /* Connecting */,
450
- "Open" /* Open */
451
- ].some((state) => this._state.connection.state === state);
452
- if (!canDisconnect)
453
- return;
454
- this._clearInterval(this._intervals.heartbeat);
455
- this._closeWs();
456
- (_a = this._crdtManager) == null ? void 0 : _a.destroy();
457
- this._stateNotifier.subjects["my-presence"].next(null);
458
- this._updateState((oldState) => {
459
- oldState.authorization.token = null;
460
- oldState.connection.state = "Closed" /* Closed */;
461
- oldState.webSocket = null;
462
- 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);
463
575
  });
464
576
  }
465
577
  getConnection() {
@@ -467,6 +579,25 @@ var PluvRoom = class extends AbstractRoom {
467
579
  JSON.parse(JSON.stringify(this._state.connection))
468
580
  );
469
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
+ }
470
601
  _attachWindowListeners() {
471
602
  if (typeof window === "undefined")
472
603
  return;
@@ -539,7 +670,6 @@ var PluvRoom = class extends AbstractRoom {
539
670
  if (canClose)
540
671
  this._state.webSocket.close();
541
672
  this._detachWsListeners();
542
- this._stateNotifier.subjects["myself"].next(null);
543
673
  }
544
674
  _detachWindowListeners() {
545
675
  if (!this._windowListeners)
@@ -579,23 +709,11 @@ var PluvRoom = class extends AbstractRoom {
579
709
  );
580
710
  this._wsListeners = null;
581
711
  }
582
- _getAuthorization(room) {
583
- return __async(this, null, function* () {
584
- const fetchOptions = this._getAuthFetchOptions(room);
585
- if (!fetchOptions)
586
- return null;
587
- const { url, options } = fetchOptions;
588
- const res = yield fetch(url, options);
589
- if (!res.ok || res.status !== 200) {
590
- throw new Error("Room is unauthorized");
591
- }
592
- try {
593
- return yield res.text().then((text2) => text2.trim());
594
- } catch (err) {
595
- throw new Error(
596
- err instanceof Error ? err.message : "Room is unauthorized"
597
- );
598
- }
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);
599
717
  });
600
718
  }
601
719
  _getAuthFetchOptions(room) {
@@ -616,6 +734,25 @@ var PluvRoom = class extends AbstractRoom {
616
734
  const result = this._endpoints.authEndpoint(room);
617
735
  return typeof result === "string" ? { url: result, options: {} } : result;
618
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
+ }
619
756
  _getWsEndpoint(room) {
620
757
  switch (typeof this._endpoints.wsEndpoint) {
621
758
  case "undefined":
@@ -711,28 +848,28 @@ var PluvRoom = class extends AbstractRoom {
711
848
  });
712
849
  }
713
850
  _handleStorageReceivedMessage(message) {
714
- var _a;
715
- const { connectionId } = message;
716
- if (!connectionId)
717
- return;
718
- if (!this._state.webSocket)
719
- throw new Error("Could not find WebSocket");
720
- const data = message.data;
721
- this._crdtManager = new CrdtManager({
722
- encodedState: data.state,
723
- initialStorage: (_a = this._initialStorage) != null ? _a : void 0
724
- });
725
- this._observeCrdt();
726
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
727
- Object.keys(sharedTypes).forEach((key) => {
728
- const sharedType = sharedTypes[key];
729
- const serialized = sharedType.toJSON();
730
- this._crdtNotifier.subject(key).next(serialized);
731
- });
732
- const update = this._crdtManager.doc.encodeStateAsUpdate();
733
- this._sendMessage({
734
- type: "$UPDATE_STORAGE",
735
- 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
+ });
736
873
  });
737
874
  }
738
875
  _handleStorageUpdatedMessage(message) {
@@ -746,14 +883,11 @@ var PluvRoom = class extends AbstractRoom {
746
883
  return;
747
884
  this._crdtManager.doc.applyUpdate(data.state, "$STORAGE_UPDATED");
748
885
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
749
- Object.keys(sharedTypes).forEach((key) => {
886
+ Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
750
887
  if (!this._crdtManager)
751
888
  return;
752
- const sharedType = sharedTypes[key];
753
- if (!sharedType)
754
- return;
755
889
  const serialized = sharedType.toJSON();
756
- this._crdtNotifier.subject(key).next(serialized);
890
+ this._crdtNotifier.subject(prop).next(serialized);
757
891
  });
758
892
  }
759
893
  _handleUserJoinedMessage(message) {
@@ -801,7 +935,9 @@ var PluvRoom = class extends AbstractRoom {
801
935
  const unsubscribe = this._crdtManager.doc.subscribe(
802
936
  (update, _origin) => {
803
937
  const origin = _origin != null ? _origin : null;
804
- if (!this._crdtManager)
938
+ this._addToStorageStore(update);
939
+ this._emitSharedTypes();
940
+ if (origin === Y_ORIGIN_INITIALIZED || origin === this)
805
941
  return;
806
942
  if (!this._state.webSocket)
807
943
  return;
@@ -811,13 +947,6 @@ var PluvRoom = class extends AbstractRoom {
811
947
  return;
812
948
  if (origin === "$STORAGE_UPDATED")
813
949
  return;
814
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
815
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
816
- if (!this._crdtManager)
817
- return;
818
- const serialized = sharedType.toJSON();
819
- this._crdtNotifier.subject(prop).next(serialized);
820
- });
821
950
  this._sendMessage({
822
951
  type: "$UPDATE_STORAGE",
823
952
  data: { origin, update }
@@ -842,14 +971,18 @@ var PluvRoom = class extends AbstractRoom {
842
971
  * @date August 19, 2022
843
972
  */
844
973
  _onClose(event) {
845
- this._logDebug("WebSocket closed");
846
- !!event.reason && this._logDebug(event.reason);
847
- this._clearInterval(this._intervals.heartbeat);
848
- this._clearTimeout(this._timeouts.pong);
849
- this._updateState((oldState) => {
850
- oldState.connection.state = "Closed" /* Closed */;
851
- oldState.webSocket = null;
852
- 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
+ });
853
986
  });
854
987
  }
855
988
  /**
@@ -1022,6 +1155,7 @@ var PluvClient = class {
1022
1155
  if (oldRoom)
1023
1156
  return oldRoom;
1024
1157
  const newRoom = new PluvRoom(room, {
1158
+ addons: options.addons,
1025
1159
  authEndpoint: this._authEndpoint,
1026
1160
  debug: options.debug,
1027
1161
  initialPresence: options.initialPresence,
@@ -1050,14 +1184,14 @@ var PluvClient = class {
1050
1184
  this.getRooms = () => {
1051
1185
  return Array.from(this._rooms.values());
1052
1186
  };
1053
- this.leave = (room) => {
1187
+ this.leave = (room) => __async(this, null, function* () {
1054
1188
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
1055
1189
  if (!toLeave)
1056
1190
  return;
1057
- toLeave.disconnect();
1191
+ yield toLeave.disconnect();
1058
1192
  this._rooms.delete(toLeave.id);
1059
1193
  this._logDebug(`Left and deleted room: ${room}`);
1060
- };
1194
+ });
1061
1195
  const { authEndpoint, debug = false, wsEndpoint } = options;
1062
1196
  this._authEndpoint = authEndpoint;
1063
1197
  this._debug = debug;
@@ -1204,6 +1338,7 @@ var import_crdt_yjs2 = require("@pluv/crdt-yjs");
1204
1338
  // Annotate the CommonJS export names for ESM import in node:
1205
1339
  0 && (module.exports = {
1206
1340
  AbstractRoom,
1341
+ AbstractStorageStore,
1207
1342
  ConnectionState,
1208
1343
  MockedRoom,
1209
1344
  PluvClient,