@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.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
  };
@@ -306,8 +402,7 @@ var PluvRoom = class extends AbstractRoom {
306
402
  return this._eventNotifier.subscribe(event, callback);
307
403
  };
308
404
  this.getMyPresence = () => {
309
- var _a, _b;
310
- return (_b = (_a = this._usersManager.myself) == null ? void 0 : _a.presence) != null ? _b : null;
405
+ return this._usersManager.myPresence;
311
406
  };
312
407
  this.getMyself = () => {
313
408
  return this._usersManager.myself;
@@ -343,6 +438,27 @@ var PluvRoom = class extends AbstractRoom {
343
438
  { presence }
344
439
  );
345
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;
346
462
  this._debug = debug;
347
463
  this._endpoints = { authEndpoint, wsEndpoint };
348
464
  this._listeners = {
@@ -359,8 +475,7 @@ var PluvRoom = class extends AbstractRoom {
359
475
  initialPresence,
360
476
  presence
361
477
  });
362
- if (initialStorage)
363
- this._initialStorage = initialStorage;
478
+ this._crdtManager = new CrdtManager({ initialStorage });
364
479
  }
365
480
  get webSocket() {
366
481
  return this._state.webSocket;
@@ -389,19 +504,17 @@ var PluvRoom = class extends AbstractRoom {
389
504
  oldState.connection.state = "Connecting" /* Connecting */;
390
505
  return oldState;
391
506
  });
507
+ yield this._storageStore.initialize();
508
+ yield this._applyStorageStore();
392
509
  const url = new URL(this._getWsEndpoint(this.id));
393
510
  let authToken = null;
394
- try {
395
- authToken = yield this._getAuthorization(this.id);
396
- } catch (err) {
397
- this._onAuthorizationFail(err);
398
- return;
399
- }
400
- authToken && url.searchParams.set("token", encodeURIComponent(authToken));
401
511
  let webSocket;
402
512
  try {
513
+ authToken = yield this._getAuthorization(this.id);
514
+ authToken && url.searchParams.set("token", encodeURIComponent(authToken));
403
515
  webSocket = new WebSocket(url.toString());
404
516
  } catch (err) {
517
+ yield this._storageStore.destroy();
405
518
  this._onAuthorizationFail(err);
406
519
  return;
407
520
  }
@@ -415,27 +528,24 @@ var PluvRoom = class extends AbstractRoom {
415
528
  });
416
529
  }
417
530
  disconnect() {
418
- var _a;
419
- if (!this._endpoints.wsEndpoint) {
420
- throw new Error("Must provide an wsEndpoint.");
421
- }
422
- if (!this._state.webSocket)
423
- return;
424
- const canDisconnect = [
425
- "Connecting" /* Connecting */,
426
- "Open" /* Open */
427
- ].some((state) => this._state.connection.state === state);
428
- if (!canDisconnect)
429
- return;
430
- this._clearInterval(this._intervals.heartbeat);
431
- this._closeWs();
432
- (_a = this._crdtManager) == null ? void 0 : _a.destroy();
433
- this._stateNotifier.subjects["my-presence"].next(null);
434
- this._updateState((oldState) => {
435
- oldState.authorization.token = null;
436
- oldState.connection.state = "Closed" /* Closed */;
437
- oldState.webSocket = null;
438
- 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);
439
549
  });
440
550
  }
441
551
  getConnection() {
@@ -443,6 +553,25 @@ var PluvRoom = class extends AbstractRoom {
443
553
  JSON.parse(JSON.stringify(this._state.connection))
444
554
  );
445
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
+ }
446
575
  _attachWindowListeners() {
447
576
  if (typeof window === "undefined")
448
577
  return;
@@ -515,7 +644,6 @@ var PluvRoom = class extends AbstractRoom {
515
644
  if (canClose)
516
645
  this._state.webSocket.close();
517
646
  this._detachWsListeners();
518
- this._stateNotifier.subjects["myself"].next(null);
519
647
  }
520
648
  _detachWindowListeners() {
521
649
  if (!this._windowListeners)
@@ -555,23 +683,11 @@ var PluvRoom = class extends AbstractRoom {
555
683
  );
556
684
  this._wsListeners = null;
557
685
  }
558
- _getAuthorization(room) {
559
- return __async(this, null, function* () {
560
- const fetchOptions = this._getAuthFetchOptions(room);
561
- if (!fetchOptions)
562
- return null;
563
- const { url, options } = fetchOptions;
564
- const res = yield fetch(url, options);
565
- if (!res.ok || res.status !== 200) {
566
- throw new Error("Room is unauthorized");
567
- }
568
- try {
569
- return yield res.text().then((text2) => text2.trim());
570
- } catch (err) {
571
- throw new Error(
572
- err instanceof Error ? err.message : "Room is unauthorized"
573
- );
574
- }
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);
575
691
  });
576
692
  }
577
693
  _getAuthFetchOptions(room) {
@@ -592,6 +708,25 @@ var PluvRoom = class extends AbstractRoom {
592
708
  const result = this._endpoints.authEndpoint(room);
593
709
  return typeof result === "string" ? { url: result, options: {} } : result;
594
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
+ }
595
730
  _getWsEndpoint(room) {
596
731
  switch (typeof this._endpoints.wsEndpoint) {
597
732
  case "undefined":
@@ -687,28 +822,28 @@ var PluvRoom = class extends AbstractRoom {
687
822
  });
688
823
  }
689
824
  _handleStorageReceivedMessage(message) {
690
- var _a;
691
- const { connectionId } = message;
692
- if (!connectionId)
693
- return;
694
- if (!this._state.webSocket)
695
- throw new Error("Could not find WebSocket");
696
- const data = message.data;
697
- this._crdtManager = new CrdtManager({
698
- encodedState: data.state,
699
- initialStorage: (_a = this._initialStorage) != null ? _a : void 0
700
- });
701
- this._observeCrdt();
702
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
703
- Object.keys(sharedTypes).forEach((key) => {
704
- const sharedType = sharedTypes[key];
705
- const serialized = sharedType.toJSON();
706
- this._crdtNotifier.subject(key).next(serialized);
707
- });
708
- const update = this._crdtManager.doc.encodeStateAsUpdate();
709
- this._sendMessage({
710
- type: "$UPDATE_STORAGE",
711
- 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
+ });
712
847
  });
713
848
  }
714
849
  _handleStorageUpdatedMessage(message) {
@@ -722,14 +857,11 @@ var PluvRoom = class extends AbstractRoom {
722
857
  return;
723
858
  this._crdtManager.doc.applyUpdate(data.state, "$STORAGE_UPDATED");
724
859
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
725
- Object.keys(sharedTypes).forEach((key) => {
860
+ Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
726
861
  if (!this._crdtManager)
727
862
  return;
728
- const sharedType = sharedTypes[key];
729
- if (!sharedType)
730
- return;
731
863
  const serialized = sharedType.toJSON();
732
- this._crdtNotifier.subject(key).next(serialized);
864
+ this._crdtNotifier.subject(prop).next(serialized);
733
865
  });
734
866
  }
735
867
  _handleUserJoinedMessage(message) {
@@ -777,7 +909,9 @@ var PluvRoom = class extends AbstractRoom {
777
909
  const unsubscribe = this._crdtManager.doc.subscribe(
778
910
  (update, _origin) => {
779
911
  const origin = _origin != null ? _origin : null;
780
- if (!this._crdtManager)
912
+ this._addToStorageStore(update);
913
+ this._emitSharedTypes();
914
+ if (origin === Y_ORIGIN_INITIALIZED || origin === this)
781
915
  return;
782
916
  if (!this._state.webSocket)
783
917
  return;
@@ -787,13 +921,6 @@ var PluvRoom = class extends AbstractRoom {
787
921
  return;
788
922
  if (origin === "$STORAGE_UPDATED")
789
923
  return;
790
- const sharedTypes = this._crdtManager.doc.getSharedTypes();
791
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
792
- if (!this._crdtManager)
793
- return;
794
- const serialized = sharedType.toJSON();
795
- this._crdtNotifier.subject(prop).next(serialized);
796
- });
797
924
  this._sendMessage({
798
925
  type: "$UPDATE_STORAGE",
799
926
  data: { origin, update }
@@ -818,14 +945,18 @@ var PluvRoom = class extends AbstractRoom {
818
945
  * @date August 19, 2022
819
946
  */
820
947
  _onClose(event) {
821
- this._logDebug("WebSocket closed");
822
- !!event.reason && this._logDebug(event.reason);
823
- this._clearInterval(this._intervals.heartbeat);
824
- this._clearTimeout(this._timeouts.pong);
825
- this._updateState((oldState) => {
826
- oldState.connection.state = "Closed" /* Closed */;
827
- oldState.webSocket = null;
828
- 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
+ });
829
960
  });
830
961
  }
831
962
  /**
@@ -998,6 +1129,7 @@ var PluvClient = class {
998
1129
  if (oldRoom)
999
1130
  return oldRoom;
1000
1131
  const newRoom = new PluvRoom(room, {
1132
+ addons: options.addons,
1001
1133
  authEndpoint: this._authEndpoint,
1002
1134
  debug: options.debug,
1003
1135
  initialPresence: options.initialPresence,
@@ -1026,14 +1158,14 @@ var PluvClient = class {
1026
1158
  this.getRooms = () => {
1027
1159
  return Array.from(this._rooms.values());
1028
1160
  };
1029
- this.leave = (room) => {
1161
+ this.leave = (room) => __async(this, null, function* () {
1030
1162
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
1031
1163
  if (!toLeave)
1032
1164
  return;
1033
- toLeave.disconnect();
1165
+ yield toLeave.disconnect();
1034
1166
  this._rooms.delete(toLeave.id);
1035
1167
  this._logDebug(`Left and deleted room: ${room}`);
1036
- };
1168
+ });
1037
1169
  const { authEndpoint, debug = false, wsEndpoint } = options;
1038
1170
  this._authEndpoint = authEndpoint;
1039
1171
  this._debug = debug;
@@ -1081,8 +1213,7 @@ var MockedRoom = class extends AbstractRoom {
1081
1213
  return this._eventNotifier.subscribe(event, callback);
1082
1214
  };
1083
1215
  this.getMyPresence = () => {
1084
- var _a, _b;
1085
- return (_b = (_a = this._usersManager.myself) == null ? void 0 : _a.presence) != null ? _b : null;
1216
+ return this._usersManager.myPresence;
1086
1217
  };
1087
1218
  this.getMyself = () => {
1088
1219
  return this._usersManager.myself;
@@ -1180,6 +1311,7 @@ __export(y_exports, {
1180
1311
  import { array, map, text, object } from "@pluv/crdt-yjs";
1181
1312
  export {
1182
1313
  AbstractRoom,
1314
+ AbstractStorageStore,
1183
1315
  ConnectionState,
1184
1316
  MockedRoom,
1185
1317
  PluvClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.8.0",
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",
@@ -38,7 +38,7 @@ export abstract class AbstractRoom<
38
38
 
39
39
  public abstract getConnection(): WebSocketConnection;
40
40
 
41
- public abstract getMyPresence(): TPresence | null;
41
+ public abstract getMyPresence(): TPresence;
42
42
 
43
43
  public abstract getMyself(): Id<UserInfo<TIO, TPresence>> | null;
44
44
 
@@ -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/MockedRoom.ts CHANGED
@@ -131,8 +131,8 @@ export class MockedRoom<
131
131
  );
132
132
  }
133
133
 
134
- public getMyPresence = (): TPresence | null => {
135
- return this._usersManager.myself?.presence ?? null;
134
+ public getMyPresence = (): TPresence => {
135
+ return this._usersManager.myPresence;
136
136
  };
137
137
 
138
138
  public getMyself = (): Id<UserInfo<TIO, TPresence>> | null => {