@pluv/client 0.8.1 → 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.mjs CHANGED
@@ -49,10 +49,18 @@ var AbstractRoom = class {
49
49
  }
50
50
  };
51
51
 
52
+ // src/AbstractStorageStore.ts
53
+ var AbstractStorageStore = class {
54
+ constructor(room) {
55
+ this.room = room;
56
+ }
57
+ };
58
+
52
59
  // src/CrdtManager.ts
53
60
  import { doc } from "@pluv/crdt-yjs";
54
61
  var CrdtManager = class {
55
62
  constructor(options = {}) {
63
+ this.initialized = false;
56
64
  const { encodedState, initialStorage } = options;
57
65
  const _doc = encodedState ? doc().applyUpdate(encodedState) : null;
58
66
  if (_doc && !!Object.keys(_doc.toJSON()).length) {
@@ -62,12 +70,50 @@ var CrdtManager = class {
62
70
  _doc == null ? void 0 : _doc.destroy();
63
71
  this.doc = doc(initialStorage == null ? void 0 : initialStorage());
64
72
  }
73
+ /**
74
+ * !HACK
75
+ * @description If this is called before the doc is initialized. Omit the
76
+ * update. This unfortunately means updates can be dropped entirely
77
+ * @date June 13, 2023
78
+ */
79
+ applyUpdate(update, origin) {
80
+ if (!this.initialized)
81
+ return this;
82
+ const updates = typeof update === "string" ? [update] : update;
83
+ this._applyDocUpdates(this.doc, updates, origin);
84
+ return this;
85
+ }
65
86
  destroy() {
66
87
  this.doc.destroy();
67
88
  }
68
89
  get(key) {
69
90
  return this.doc.get(key);
70
91
  }
92
+ initialize(params) {
93
+ const { onInitialized, origin, update } = params;
94
+ const updates = typeof update === "string" ? [update] : update;
95
+ if (!updates.length)
96
+ return this;
97
+ if (this.initialized) {
98
+ this._applyDocUpdates(this.doc, updates, origin);
99
+ return this;
100
+ }
101
+ const _doc = this._applyDocUpdates(doc(), updates, origin);
102
+ this.initialized = true;
103
+ if (!!Object.keys(_doc.toJSON()).length) {
104
+ this.doc = _doc;
105
+ } else {
106
+ _doc.destroy();
107
+ }
108
+ onInitialized == null ? void 0 : onInitialized();
109
+ return this;
110
+ }
111
+ _applyDocUpdates(yjsDoc, updates, origin) {
112
+ yjsDoc.transact(() => {
113
+ updates.reduce((acc, _update) => acc.applyUpdate(_update), yjsDoc);
114
+ }, origin);
115
+ return yjsDoc;
116
+ }
71
117
  };
72
118
 
73
119
  // src/CrdtNotifier.ts
@@ -151,6 +197,31 @@ var StateNotifier = class {
151
197
  }
152
198
  };
153
199
 
200
+ // src/StorageStore.ts
201
+ var StorageStore = class extends AbstractStorageStore {
202
+ addUpdate() {
203
+ return Promise.resolve();
204
+ }
205
+ applyUpdates() {
206
+ return Promise.resolve();
207
+ }
208
+ destroy() {
209
+ return Promise.resolve();
210
+ }
211
+ flatten() {
212
+ return Promise.resolve();
213
+ }
214
+ getShouldFlatten() {
215
+ return Promise.resolve(false);
216
+ }
217
+ getUpdates() {
218
+ return Promise.resolve([]);
219
+ }
220
+ initialize() {
221
+ return Promise.resolve();
222
+ }
223
+ };
224
+
154
225
  // src/types.ts
155
226
  var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
156
227
  ConnectionState2["Closed"] = "Closed";
@@ -256,13 +327,40 @@ var UsersManager = class {
256
327
  }
257
328
  };
258
329
 
330
+ // src/utils/debounce.ts
331
+ var debounce = (fn, options) => {
332
+ const { wait, immediate = false } = options != null ? options : {};
333
+ let prevResult;
334
+ let timeout = null;
335
+ return (...args) => {
336
+ if (typeof timeout === "number")
337
+ clearTimeout(timeout);
338
+ timeout = setTimeout(() => {
339
+ timeout = null;
340
+ if (!immediate)
341
+ prevResult = fn(args);
342
+ }, wait);
343
+ if (immediate && !timeout) {
344
+ fn(args);
345
+ return;
346
+ }
347
+ return prevResult;
348
+ };
349
+ };
350
+
259
351
  // src/PluvRoom.ts
352
+ var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
260
353
  var HEARTBEAT_INTERVAL_MS = 1e4;
261
354
  var PONG_TIMEOUT_MS = 2e3;
262
355
  var RECONNECT_TIMEOUT_MS = 3e4;
356
+ var Y_ORIGIN_INITIALIZED = "$INITIALIZED";
357
+ var DEFAULT_PLUV_CLIENT_ADDON = (input) => ({
358
+ storage: new StorageStore(input.room.id)
359
+ });
263
360
  var PluvRoom = class extends AbstractRoom {
264
361
  constructor(room, options) {
265
362
  const {
363
+ addons = [],
266
364
  authEndpoint,
267
365
  debug = false,
268
366
  initialPresence,
@@ -272,10 +370,8 @@ var PluvRoom = class extends AbstractRoom {
272
370
  wsEndpoint
273
371
  } = options;
274
372
  super(room);
275
- this._crdtManager = null;
276
373
  this._crdtNotifier = new CrdtNotifier();
277
374
  this._eventNotifier = new EventNotifier();
278
- this._initialStorage = null;
279
375
  this._intervals = {
280
376
  heartbeat: null
281
377
  };
@@ -342,6 +438,27 @@ var PluvRoom = class extends AbstractRoom {
342
438
  { presence }
343
439
  );
344
440
  };
441
+ this._flattenStorageStore = debounce(
442
+ () => __async(this, null, function* () {
443
+ const shouldFlatten = yield this._storageStore.getShouldFlatten();
444
+ if (!shouldFlatten)
445
+ return;
446
+ const updates = yield this._storageStore.getUpdates();
447
+ this._crdtManager.applyUpdate(updates, this);
448
+ const encodedState = this._crdtManager.doc.encodeStateAsUpdate();
449
+ yield this._storageStore.flatten(encodedState);
450
+ }),
451
+ { wait: ADD_TO_STORAGE_STATE_DEBOUNCE_MS }
452
+ );
453
+ this._getAddon = (addons) => {
454
+ return addons.reduce(
455
+ (acc, addon) => () => __spreadValues(__spreadValues({}, acc({ room: this })), addon({ room: this })),
456
+ DEFAULT_PLUV_CLIENT_ADDON
457
+ );
458
+ };
459
+ const addon = this._getAddon(addons);
460
+ const { storage } = __spreadValues(__spreadValues({}, DEFAULT_PLUV_CLIENT_ADDON({ room: this })), addon({ room: this }));
461
+ this._storageStore = storage;
345
462
  this._debug = debug;
346
463
  this._endpoints = { authEndpoint, wsEndpoint };
347
464
  this._listeners = {
@@ -358,8 +475,7 @@ var PluvRoom = class extends AbstractRoom {
358
475
  initialPresence,
359
476
  presence
360
477
  });
361
- if (initialStorage)
362
- this._initialStorage = initialStorage;
478
+ this._crdtManager = new CrdtManager({ initialStorage });
363
479
  }
364
480
  get webSocket() {
365
481
  return this._state.webSocket;
@@ -388,19 +504,17 @@ var PluvRoom = class extends AbstractRoom {
388
504
  oldState.connection.state = "Connecting" /* Connecting */;
389
505
  return oldState;
390
506
  });
507
+ yield this._storageStore.initialize();
508
+ yield this._applyStorageStore();
391
509
  const url = new URL(this._getWsEndpoint(this.id));
392
510
  let authToken = null;
393
- try {
394
- authToken = yield this._getAuthorization(this.id);
395
- } catch (err) {
396
- this._onAuthorizationFail(err);
397
- return;
398
- }
399
- authToken && url.searchParams.set("token", encodeURIComponent(authToken));
400
511
  let webSocket;
401
512
  try {
513
+ authToken = yield this._getAuthorization(this.id);
514
+ authToken && url.searchParams.set("token", encodeURIComponent(authToken));
402
515
  webSocket = new WebSocket(url.toString());
403
516
  } catch (err) {
517
+ yield this._storageStore.destroy();
404
518
  this._onAuthorizationFail(err);
405
519
  return;
406
520
  }
@@ -414,27 +528,24 @@ var PluvRoom = class extends AbstractRoom {
414
528
  });
415
529
  }
416
530
  disconnect() {
417
- var _a;
418
- if (!this._endpoints.wsEndpoint) {
419
- throw new Error("Must provide an wsEndpoint.");
420
- }
421
- if (!this._state.webSocket)
422
- return;
423
- const canDisconnect = [
424
- "Connecting" /* Connecting */,
425
- "Open" /* Open */
426
- ].some((state) => this._state.connection.state === state);
427
- if (!canDisconnect)
428
- return;
429
- this._clearInterval(this._intervals.heartbeat);
430
- this._closeWs();
431
- (_a = this._crdtManager) == null ? void 0 : _a.destroy();
432
- this._stateNotifier.subjects["my-presence"].next(null);
433
- this._updateState((oldState) => {
434
- oldState.authorization.token = null;
435
- oldState.connection.state = "Closed" /* Closed */;
436
- oldState.webSocket = null;
437
- return oldState;
531
+ return __async(this, null, function* () {
532
+ var _a;
533
+ if (!this._endpoints.wsEndpoint) {
534
+ throw new Error("Must provide an wsEndpoint.");
535
+ }
536
+ if (!this._state.webSocket)
537
+ return;
538
+ const canDisconnect = [
539
+ "Connecting" /* Connecting */,
540
+ "Open" /* Open */
541
+ ].some((state) => this._state.connection.state === state);
542
+ if (!canDisconnect)
543
+ return;
544
+ this._clearInterval(this._intervals.heartbeat);
545
+ this._closeWs();
546
+ yield this._storageStore.destroy();
547
+ (_a = this._crdtManager) == null ? void 0 : _a.destroy();
548
+ this._stateNotifier.subjects["my-presence"].next(null);
438
549
  });
439
550
  }
440
551
  getConnection() {
@@ -442,6 +553,25 @@ var PluvRoom = class extends AbstractRoom {
442
553
  JSON.parse(JSON.stringify(this._state.connection))
443
554
  );
444
555
  }
556
+ _addToStorageStore(update) {
557
+ return __async(this, null, function* () {
558
+ yield this._storageStore.addUpdate(update);
559
+ this._flattenStorageStore();
560
+ });
561
+ }
562
+ _applyStorageStore() {
563
+ return __async(this, null, function* () {
564
+ const updates = yield this._storageStore.getUpdates();
565
+ this._crdtManager.initialize({
566
+ onInitialized: () => {
567
+ this._observeCrdt();
568
+ },
569
+ origin: Y_ORIGIN_INITIALIZED,
570
+ update: updates
571
+ });
572
+ this._emitSharedTypes();
573
+ });
574
+ }
445
575
  _attachWindowListeners() {
446
576
  if (typeof window === "undefined")
447
577
  return;
@@ -514,7 +644,6 @@ var PluvRoom = class extends AbstractRoom {
514
644
  if (canClose)
515
645
  this._state.webSocket.close();
516
646
  this._detachWsListeners();
517
- this._stateNotifier.subjects["myself"].next(null);
518
647
  }
519
648
  _detachWindowListeners() {
520
649
  if (!this._windowListeners)
@@ -554,23 +683,11 @@ var PluvRoom = class extends AbstractRoom {
554
683
  );
555
684
  this._wsListeners = null;
556
685
  }
557
- _getAuthorization(room) {
558
- return __async(this, null, function* () {
559
- const fetchOptions = this._getAuthFetchOptions(room);
560
- if (!fetchOptions)
561
- return null;
562
- const { url, options } = fetchOptions;
563
- const res = yield fetch(url, options);
564
- if (!res.ok || res.status !== 200) {
565
- throw new Error("Room is unauthorized");
566
- }
567
- try {
568
- return yield res.text().then((text2) => text2.trim());
569
- } catch (err) {
570
- throw new Error(
571
- err instanceof Error ? err.message : "Room is unauthorized"
572
- );
573
- }
686
+ _emitSharedTypes() {
687
+ const sharedTypes = this._crdtManager.doc.getSharedTypes();
688
+ Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
689
+ const serialized = sharedType.toJSON();
690
+ this._crdtNotifier.subject(prop).next(serialized);
574
691
  });
575
692
  }
576
693
  _getAuthFetchOptions(room) {
@@ -591,6 +708,25 @@ var PluvRoom = class extends AbstractRoom {
591
708
  const result = this._endpoints.authEndpoint(room);
592
709
  return typeof result === "string" ? { url: result, options: {} } : result;
593
710
  }
711
+ _getAuthorization(room) {
712
+ return __async(this, null, function* () {
713
+ const fetchOptions = this._getAuthFetchOptions(room);
714
+ if (!fetchOptions)
715
+ return null;
716
+ const { url, options } = fetchOptions;
717
+ const res = yield fetch(url, options);
718
+ if (!res.ok || res.status !== 200) {
719
+ throw new Error("Room is unauthorized");
720
+ }
721
+ try {
722
+ return yield res.text().then((text2) => text2.trim());
723
+ } catch (err) {
724
+ throw new Error(
725
+ err instanceof Error ? err.message : "Room is unauthorized"
726
+ );
727
+ }
728
+ });
729
+ }
594
730
  _getWsEndpoint(room) {
595
731
  switch (typeof this._endpoints.wsEndpoint) {
596
732
  case "undefined":
@@ -686,28 +822,28 @@ var PluvRoom = class extends AbstractRoom {
686
822
  });
687
823
  }
688
824
  _handleStorageReceivedMessage(message) {
689
- var _a;
690
- const { connectionId } = message;
691
- if (!connectionId)
692
- return;
693
- if (!this._state.webSocket)
694
- throw new Error("Could not find WebSocket");
695
- const data = message.data;
696
- this._crdtManager = new CrdtManager({
697
- encodedState: data.state,
698
- initialStorage: (_a = this._initialStorage) != null ? _a : void 0
699
- });
700
- this._observeCrdt();
701
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
702
- Object.keys(sharedTypes).forEach((key) => {
703
- const sharedType = sharedTypes[key];
704
- const serialized = sharedType.toJSON();
705
- this._crdtNotifier.subject(key).next(serialized);
706
- });
707
- const update = this._crdtManager.doc.encodeStateAsUpdate();
708
- this._sendMessage({
709
- type: "$UPDATE_STORAGE",
710
- data: { origin: "$STORAGE_RECEIVED", update }
825
+ return __async(this, null, function* () {
826
+ const { connectionId } = message;
827
+ if (!connectionId)
828
+ return;
829
+ if (!this._state.webSocket)
830
+ throw new Error("Could not find WebSocket");
831
+ const data = message.data;
832
+ this._crdtManager.initialize({
833
+ onInitialized: () => __async(this, null, function* () {
834
+ this._observeCrdt();
835
+ }),
836
+ origin: Y_ORIGIN_INITIALIZED,
837
+ update: data.state
838
+ });
839
+ const encodedState = this._crdtManager.doc.encodeStateAsUpdate();
840
+ yield this._storageStore.addUpdate(encodedState);
841
+ this._emitSharedTypes();
842
+ const update = this._crdtManager.doc.encodeStateAsUpdate();
843
+ this._sendMessage({
844
+ type: "$UPDATE_STORAGE",
845
+ data: { origin: Y_ORIGIN_INITIALIZED, update }
846
+ });
711
847
  });
712
848
  }
713
849
  _handleStorageUpdatedMessage(message) {
@@ -721,14 +857,11 @@ var PluvRoom = class extends AbstractRoom {
721
857
  return;
722
858
  this._crdtManager.doc.applyUpdate(data.state, "$STORAGE_UPDATED");
723
859
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
724
- Object.keys(sharedTypes).forEach((key) => {
860
+ Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
725
861
  if (!this._crdtManager)
726
862
  return;
727
- const sharedType = sharedTypes[key];
728
- if (!sharedType)
729
- return;
730
863
  const serialized = sharedType.toJSON();
731
- this._crdtNotifier.subject(key).next(serialized);
864
+ this._crdtNotifier.subject(prop).next(serialized);
732
865
  });
733
866
  }
734
867
  _handleUserJoinedMessage(message) {
@@ -776,7 +909,9 @@ var PluvRoom = class extends AbstractRoom {
776
909
  const unsubscribe = this._crdtManager.doc.subscribe(
777
910
  (update, _origin) => {
778
911
  const origin = _origin != null ? _origin : null;
779
- if (!this._crdtManager)
912
+ this._addToStorageStore(update);
913
+ this._emitSharedTypes();
914
+ if (origin === Y_ORIGIN_INITIALIZED || origin === this)
780
915
  return;
781
916
  if (!this._state.webSocket)
782
917
  return;
@@ -786,13 +921,6 @@ var PluvRoom = class extends AbstractRoom {
786
921
  return;
787
922
  if (origin === "$STORAGE_UPDATED")
788
923
  return;
789
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
790
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
791
- if (!this._crdtManager)
792
- return;
793
- const serialized = sharedType.toJSON();
794
- this._crdtNotifier.subject(prop).next(serialized);
795
- });
796
924
  this._sendMessage({
797
925
  type: "$UPDATE_STORAGE",
798
926
  data: { origin, update }
@@ -817,14 +945,18 @@ var PluvRoom = class extends AbstractRoom {
817
945
  * @date August 19, 2022
818
946
  */
819
947
  _onClose(event) {
820
- this._logDebug("WebSocket closed");
821
- !!event.reason && this._logDebug(event.reason);
822
- this._clearInterval(this._intervals.heartbeat);
823
- this._clearTimeout(this._timeouts.pong);
824
- this._updateState((oldState) => {
825
- oldState.connection.state = "Closed" /* Closed */;
826
- oldState.webSocket = null;
827
- return oldState;
948
+ return __async(this, null, function* () {
949
+ this._logDebug("WebSocket closed");
950
+ !!event.reason && this._logDebug(event.reason);
951
+ this._clearInterval(this._intervals.heartbeat);
952
+ this._clearTimeout(this._timeouts.pong);
953
+ this._stateNotifier.subjects["myself"].next(null);
954
+ this._updateState((oldState) => {
955
+ oldState.authorization.token = null;
956
+ oldState.connection.state = "Closed" /* Closed */;
957
+ oldState.webSocket = null;
958
+ return oldState;
959
+ });
828
960
  });
829
961
  }
830
962
  /**
@@ -997,6 +1129,7 @@ var PluvClient = class {
997
1129
  if (oldRoom)
998
1130
  return oldRoom;
999
1131
  const newRoom = new PluvRoom(room, {
1132
+ addons: options.addons,
1000
1133
  authEndpoint: this._authEndpoint,
1001
1134
  debug: options.debug,
1002
1135
  initialPresence: options.initialPresence,
@@ -1025,14 +1158,14 @@ var PluvClient = class {
1025
1158
  this.getRooms = () => {
1026
1159
  return Array.from(this._rooms.values());
1027
1160
  };
1028
- this.leave = (room) => {
1161
+ this.leave = (room) => __async(this, null, function* () {
1029
1162
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
1030
1163
  if (!toLeave)
1031
1164
  return;
1032
- toLeave.disconnect();
1165
+ yield toLeave.disconnect();
1033
1166
  this._rooms.delete(toLeave.id);
1034
1167
  this._logDebug(`Left and deleted room: ${room}`);
1035
- };
1168
+ });
1036
1169
  const { authEndpoint, debug = false, wsEndpoint } = options;
1037
1170
  this._authEndpoint = authEndpoint;
1038
1171
  this._debug = debug;
@@ -1178,6 +1311,7 @@ __export(y_exports, {
1178
1311
  import { array, map, text, object } from "@pluv/crdt-yjs";
1179
1312
  export {
1180
1313
  AbstractRoom,
1314
+ AbstractStorageStore,
1181
1315
  ConnectionState,
1182
1316
  MockedRoom,
1183
1317
  PluvClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "@pluv/io frontend client",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -17,19 +17,21 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
+ "@playwright/test": "^1.35.1",
21
+ "@types/react": "^18.2.13",
20
22
  "wonka": "^6.3.2",
21
- "@pluv/crdt-yjs": "^0.3.8",
22
- "@pluv/types": "^0.2.0"
23
+ "@pluv/crdt-yjs": "^0.4.0",
24
+ "@pluv/types": "^0.2.1"
23
25
  },
24
26
  "peerDependencies": {
25
27
  "yjs": "^13.0.0"
26
28
  },
27
29
  "devDependencies": {
28
- "tsup": "^6.7.0",
29
- "typescript": "^5.0.4",
30
- "yjs": "^13.6.1",
31
- "@pluv/tsconfig": "^0.1.6",
32
- "eslint-config-pluv": "^0.1.12"
30
+ "tsup": "^7.0.0",
31
+ "typescript": "^5.1.3",
32
+ "yjs": "^13.6.4",
33
+ "@pluv/tsconfig": "^0.1.7",
34
+ "eslint-config-pluv": "^0.1.13"
33
35
  },
34
36
  "scripts": {
35
37
  "build": "tsup src/index.ts --format esm,cjs --dts",
@@ -0,0 +1,21 @@
1
+ import { MaybePromise } from "@pluv/types";
2
+
3
+ export abstract class AbstractStorageStore {
4
+ public room: string;
5
+
6
+ constructor(room: string) {
7
+ this.room = room;
8
+ }
9
+
10
+ public abstract addUpdate(update: string): Promise<void>;
11
+
12
+ public abstract destroy(): Promise<void>;
13
+
14
+ public abstract flatten(encodedState: string): Promise<void>;
15
+
16
+ public abstract getShouldFlatten(): Promise<boolean>;
17
+
18
+ public abstract getUpdates(start?: number): Promise<readonly string[]>;
19
+
20
+ public abstract initialize(): Promise<void>;
21
+ }
@@ -9,8 +9,15 @@ export interface CrdtManagerOptions<
9
9
  initialStorage?: () => TStorage;
10
10
  }
11
11
 
12
+ interface CrdtManagerInitializeParams {
13
+ onInitialized?: () => void;
14
+ origin?: any;
15
+ update: string | readonly string[];
16
+ }
17
+
12
18
  export class CrdtManager<TStorage extends Record<string, AbstractType<any>>> {
13
19
  public doc: YjsDoc<TStorage>;
20
+ public initialized: boolean = false;
14
21
 
15
22
  constructor(options: CrdtManagerOptions<TStorage> = {}) {
16
23
  const { encodedState, initialStorage } = options;
@@ -30,6 +37,23 @@ export class CrdtManager<TStorage extends Record<string, AbstractType<any>>> {
30
37
  this.doc = doc<TStorage>(initialStorage?.());
31
38
  }
32
39
 
40
+ /**
41
+ * !HACK
42
+ * @description If this is called before the doc is initialized. Omit the
43
+ * update. This unfortunately means updates can be dropped entirely
44
+ * @date June 13, 2023
45
+ */
46
+ public applyUpdate(update: string | readonly string[], origin?: any): this {
47
+ if (!this.initialized) return this;
48
+
49
+ const updates: readonly string[] =
50
+ typeof update === "string" ? [update] : update;
51
+
52
+ this._applyDocUpdates(this.doc, updates, origin);
53
+
54
+ return this;
55
+ }
56
+
33
57
  public destroy(): void {
34
58
  this.doc.destroy();
35
59
  }
@@ -37,4 +61,44 @@ export class CrdtManager<TStorage extends Record<string, AbstractType<any>>> {
37
61
  public get<TKey extends keyof TStorage>(key: TKey): TStorage[TKey] {
38
62
  return this.doc.get(key);
39
63
  }
64
+
65
+ public initialize(params: CrdtManagerInitializeParams): this {
66
+ const { onInitialized, origin, update } = params;
67
+
68
+ const updates = typeof update === "string" ? [update] : update;
69
+
70
+ if (!updates.length) return this;
71
+
72
+ if (this.initialized) {
73
+ this._applyDocUpdates(this.doc, updates, origin);
74
+
75
+ return this;
76
+ }
77
+
78
+ const _doc = this._applyDocUpdates(doc<TStorage>(), updates, origin);
79
+
80
+ this.initialized = true;
81
+
82
+ if (!!Object.keys(_doc.toJSON()).length) {
83
+ this.doc = _doc;
84
+ } else {
85
+ _doc.destroy();
86
+ }
87
+
88
+ onInitialized?.();
89
+
90
+ return this;
91
+ }
92
+
93
+ private _applyDocUpdates(
94
+ yjsDoc: YjsDoc<TStorage>,
95
+ updates: readonly string[],
96
+ origin?: string
97
+ ): YjsDoc<TStorage> {
98
+ yjsDoc.transact(() => {
99
+ updates.reduce((acc, _update) => acc.applyUpdate(_update), yjsDoc);
100
+ }, origin);
101
+
102
+ return yjsDoc;
103
+ }
40
104
  }
package/src/PluvClient.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { IOLike, JsonObject } from "@pluv/types";
2
2
  import { AbstractType } from "yjs";
3
- import type {
3
+ import {
4
4
  AuthEndpoint,
5
- RoomEndpoints,
5
+ PluvRoom,
6
6
  PluvRoomOptions,
7
- WsEndpoint,
8
7
  RoomConfig,
8
+ RoomEndpoints,
9
+ WsEndpoint,
9
10
  } from "./PluvRoom";
10
- import { PluvRoom } from "./PluvRoom";
11
11
 
12
12
  export type PluvClientOptions<TIO extends IOLike> = RoomEndpoints<TIO> & {
13
13
  debug?: boolean;
@@ -40,6 +40,7 @@ export class PluvClient<TIO extends IOLike = IOLike> {
40
40
  if (oldRoom) return oldRoom;
41
41
 
42
42
  const newRoom = new PluvRoom<TIO, TPresence, TStorage>(room, {
43
+ addons: options.addons,
43
44
  authEndpoint: this._authEndpoint,
44
45
  debug: options.debug,
45
46
  initialPresence: options.initialPresence,
@@ -90,12 +91,14 @@ export class PluvClient<TIO extends IOLike = IOLike> {
90
91
  return Array.from(this._rooms.values());
91
92
  };
92
93
 
93
- public leave = (room: string | PluvRoom<TIO, any, any>): void => {
94
+ public leave = async (
95
+ room: string | PluvRoom<TIO, any, any>
96
+ ): Promise<void> => {
94
97
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
95
98
 
96
99
  if (!toLeave) return;
97
100
 
98
- toLeave.disconnect();
101
+ await toLeave.disconnect();
99
102
 
100
103
  this._rooms.delete(toLeave.id);
101
104