@pluv/client 1.0.0 → 1.0.2

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
@@ -95,9 +95,9 @@ import { noop } from "@pluv/crdt";
95
95
  var CrdtManager = class {
96
96
  constructor(options) {
97
97
  this.initialized = false;
98
- const { encodedState, initialStorage = noop.doc({}) } = options;
98
+ const { initialStorage = noop.doc({}) } = options;
99
99
  this._docFactory = initialStorage;
100
- this.doc = encodedState ? initialStorage.getFresh().applyEncodedState({ update: encodedState }) : initialStorage.getFresh();
100
+ this.doc = initialStorage.getEmpty();
101
101
  }
102
102
  /**
103
103
  * !HACK
@@ -105,40 +105,50 @@ var CrdtManager = class {
105
105
  * update. This unfortunately means updates can be dropped entirely
106
106
  * @date June 13, 2023
107
107
  */
108
- applyUpdate(update, origin) {
108
+ applyUpdate(params) {
109
+ const { origin, update } = params;
109
110
  if (!this.initialized) return this;
110
111
  const updates = typeof update === "string" ? [update] : update;
111
112
  this._applyDocUpdates(this.doc, updates, origin);
112
113
  return this;
113
114
  }
114
115
  destroy() {
116
+ this.initialized = false;
115
117
  this.doc.destroy();
118
+ this.doc = this._docFactory.getEmpty();
116
119
  }
117
120
  get(key) {
118
121
  return this.doc.get(key);
119
122
  }
123
+ getInitialState() {
124
+ return this._docFactory.getInitialized().getEncodedState();
125
+ }
120
126
  initialize(params) {
121
- const { onInitialized, origin, update } = params;
122
- const updates = typeof update === "string" ? [update] : update;
123
- if (!updates.length) return this;
127
+ const { origin, update } = params;
124
128
  if (this.initialized) {
125
- this.doc = this._applyDocUpdates(this.doc, updates, origin).track();
129
+ console.warn("Attempted to initialize storage multiple times");
126
130
  return this;
127
131
  }
128
- const onEmpty = this._applyDocUpdates(this._docFactory.getEmpty(), updates, origin);
129
- if (!onEmpty.isEmpty()) {
130
- onEmpty.destroy();
131
- this.doc = this._applyDocUpdates(this._docFactory.getFresh(), updates, origin).track();
132
- this.initialized = true;
133
- onInitialized == null ? void 0 : onInitialized(this.doc.getEncodedState());
132
+ if (!this.doc.isEmpty()) {
133
+ console.warn("Attempted to initialize over a non-empty storage");
134
134
  return this;
135
135
  }
136
- onEmpty.destroy();
137
- this.doc = this._docFactory.getInitialized().track();
136
+ const updates = typeof update === "string" ? [update] : update;
137
+ if (!updates.length) return this;
138
+ const initialized = this._docFactory.getInitialized();
139
+ const reference = initialized.get();
140
+ this.doc = this._applyDocUpdates(
141
+ this._docFactory.getEmpty(),
142
+ updates,
143
+ origin
144
+ ).rebuildStorage(reference);
138
145
  this.initialized = true;
139
- onInitialized == null ? void 0 : onInitialized(this.doc.getEncodedState());
146
+ initialized.destroy();
140
147
  return this;
141
148
  }
149
+ resolveEncodedState(updates) {
150
+ return this._docFactory.resolveEncodedState(updates);
151
+ }
142
152
  _applyDocUpdates(doc, updates, origin) {
143
153
  doc.transact(() => {
144
154
  doc.batchApplyEncodedState({ origin, updates });
@@ -233,7 +243,10 @@ var PluvRouter = class _PluvRouter {
233
243
  this._defs = { events };
234
244
  }
235
245
  static merge(...routers) {
236
- const events = Object.assign(/* @__PURE__ */ Object.create(null), ...routers.map((router) => router._defs.events));
246
+ const events = Object.assign(
247
+ /* @__PURE__ */ Object.create(null),
248
+ ...routers.map((router) => router._defs.events)
249
+ );
237
250
  if (Object.keys(events).some((key) => key.startsWith("$"))) {
238
251
  throw new Error('Procedures may not start with "$"');
239
252
  }
@@ -386,7 +399,11 @@ var UsersManager = class {
386
399
  const userInfo = (_a = this._others.get(clientId)) != null ? _a : null;
387
400
  if (!userInfo) return null;
388
401
  const remaining = (_c = (_b = this._getConnectionIds(clientId)) == null ? void 0 : _b.size) != null ? _c : 0;
389
- const result = { clientId, remaining, data: userInfo };
402
+ const result = {
403
+ clientId,
404
+ remaining,
405
+ data: userInfo
406
+ };
390
407
  if (!remaining) this._others.delete(clientId);
391
408
  return result;
392
409
  }
@@ -423,7 +440,10 @@ var UsersManager = class {
423
440
  const other = this._others.get(clientId);
424
441
  if (!other) return null;
425
442
  const cleanedPatch = pickBy(patch != null ? patch : {}, (value) => typeof value !== "undefined");
426
- const cleanedPresence = pickBy((_a = other.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
443
+ const cleanedPresence = pickBy(
444
+ (_a = other.presence) != null ? _a : {},
445
+ (value) => typeof value !== "undefined"
446
+ );
427
447
  const presence = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
428
448
  const validated = this._presence ? this._presence.parse(presence) : presence;
429
449
  this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence: validated }));
@@ -592,10 +612,7 @@ var PluvRoom = class extends AbstractRoom {
592
612
  room: this.id,
593
613
  user: myself
594
614
  };
595
- const output = yield procedure.config.broadcast(
596
- parsed,
597
- context
598
- );
615
+ const output = yield procedure.config.broadcast(parsed, context);
599
616
  Object.entries(output).forEach(([_type, _data]) => {
600
617
  this._sendMessage({ data: _data, type: _type });
601
618
  });
@@ -691,7 +708,7 @@ var PluvRoom = class extends AbstractRoom {
691
708
  const shouldFlatten = yield this._storageStore.getShouldFlatten();
692
709
  if (!shouldFlatten) return;
693
710
  const updates = yield this._storageStore.getUpdates();
694
- this._crdtManager.applyUpdate(updates, this);
711
+ this._crdtManager.applyUpdate({ update: updates, origin: this });
695
712
  const encodedState = this._crdtManager.doc.getEncodedState();
696
713
  yield this._storageStore.flatten(encodedState);
697
714
  }),
@@ -718,7 +735,11 @@ var PluvRoom = class extends AbstractRoom {
718
735
  }
719
736
  };
720
737
  this._router = router != null ? router : new PluvRouter({});
721
- this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
738
+ this._usersManager = new UsersManager({
739
+ initialPresence,
740
+ limits: this._limits,
741
+ presence
742
+ });
722
743
  this._crdtManager = new CrdtManager({ initialStorage });
723
744
  }
724
745
  get storageLoaded() {
@@ -733,9 +754,11 @@ var PluvRoom = class extends AbstractRoom {
733
754
  const params = (_a = args[0]) != null ? _a : {};
734
755
  const metadata = params.metadata;
735
756
  this._setMetadata(metadata);
736
- const canConnect = [ConnectionState.Closed, ConnectionState.Unavailable, ConnectionState.Untouched].some(
737
- (state) => this._state.connection.state === state
738
- );
757
+ const canConnect = [
758
+ ConnectionState.Closed,
759
+ ConnectionState.Unavailable,
760
+ ConnectionState.Untouched
761
+ ].some((state) => this._state.connection.state === state);
739
762
  if (!canConnect) return;
740
763
  this._clearTimeout(this._timeouts.reconnect);
741
764
  this._updateState((oldState) => {
@@ -743,7 +766,6 @@ var PluvRoom = class extends AbstractRoom {
743
766
  return oldState;
744
767
  });
745
768
  yield this._storageStore.initialize();
746
- yield this._applyStorageStore();
747
769
  const wsEndpoint = this._getWsEndpoint(this.id, params);
748
770
  const url = new URL(wsEndpoint);
749
771
  let authToken = null;
@@ -808,20 +830,6 @@ var PluvRoom = class extends AbstractRoom {
808
830
  this._flattenStorageStore();
809
831
  });
810
832
  }
811
- _applyStorageStore() {
812
- return __async(this, null, function* () {
813
- const updates = yield this._storageStore.getUpdates();
814
- this._crdtManager.initialize({
815
- onInitialized: () => {
816
- this._stateNotifier.subjects["storage-loaded"].next(true);
817
- this._observeCrdt();
818
- },
819
- origin: ORIGIN_INITIALIZED,
820
- update: updates
821
- });
822
- this._emitSharedTypes();
823
- });
824
- }
825
833
  _attachWindowListeners() {
826
834
  if (typeof window === "undefined") return;
827
835
  if (typeof document === "undefined") return;
@@ -1012,32 +1020,35 @@ var PluvRoom = class extends AbstractRoom {
1012
1020
  this._stateNotifier.subjects.others.next(others);
1013
1021
  }
1014
1022
  _handleRegisteredMessage(message) {
1015
- var _a, _b, _c;
1016
- const { connectionId } = message;
1017
- const user = message.user;
1018
- if (!connectionId) return;
1019
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1020
- const data = message.data;
1021
- this._updateState((oldState) => {
1022
- oldState.connection.count += 1;
1023
- oldState.connection.id = connectionId;
1024
- oldState.authorization.user = user != null ? user : null;
1025
- return oldState;
1026
- });
1027
- const userInfo = {
1028
- connectionId,
1029
- presence: (_a = data.presence) != null ? _a : void 0,
1030
- user
1031
- };
1032
- this._usersManager.setMyself(userInfo);
1033
- const presence = this._usersManager.myPresence;
1034
- const myself = (_b = this._usersManager.myself) != null ? _b : null;
1035
- this._stateNotifier.subjects["my-presence"].next(presence);
1036
- this._stateNotifier.subjects.myself.next(myself);
1037
- const update = this._state.connection.count > 1 ? (_c = this._crdtManager.doc.getEncodedState()) != null ? _c : null : null;
1038
- this._sendMessage({
1039
- type: "$initializeSession",
1040
- data: { presence, update }
1023
+ return __async(this, null, function* () {
1024
+ var _a, _b, _c;
1025
+ const { connectionId } = message;
1026
+ const user = message.user;
1027
+ if (!connectionId) return;
1028
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1029
+ const data = message.data;
1030
+ const state = data.state;
1031
+ this._updateState((oldState) => {
1032
+ oldState.connection.count += 1;
1033
+ oldState.connection.id = connectionId;
1034
+ oldState.authorization.user = user != null ? user : null;
1035
+ return oldState;
1036
+ });
1037
+ const userInfo = {
1038
+ connectionId,
1039
+ presence: (_a = data.presence) != null ? _a : void 0,
1040
+ user
1041
+ };
1042
+ this._usersManager.setMyself(userInfo);
1043
+ const presence = this._usersManager.myPresence;
1044
+ const myself = (_b = this._usersManager.myself) != null ? _b : null;
1045
+ this._stateNotifier.subjects["my-presence"].next(presence);
1046
+ this._stateNotifier.subjects.myself.next(myself);
1047
+ const update = !state ? (_c = this._crdtManager.resolveEncodedState(yield this._storageStore.getUpdates())) != null ? _c : this._crdtManager.getInitialState() : this._crdtManager.getInitialState();
1048
+ this._sendMessage({
1049
+ type: "$initializeSession",
1050
+ data: { presence, update }
1051
+ });
1041
1052
  });
1042
1053
  }
1043
1054
  _handleStorageReceivedMessage(message) {
@@ -1046,23 +1057,34 @@ var PluvRoom = class extends AbstractRoom {
1046
1057
  if (!connectionId) return;
1047
1058
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1048
1059
  const data = message.data;
1049
- this._crdtManager.initialize({
1050
- onInitialized: (update2) => __async(this, null, function* () {
1051
- this._addToStorageStore(update2);
1052
- this._emitSharedTypes();
1053
- this._observeCrdt();
1054
- this._stateNotifier.subjects["storage-loaded"].next(true);
1055
- }),
1056
- origin: ORIGIN_INITIALIZED,
1057
- update: data.state
1058
- });
1060
+ const changeKind = data.changeKind;
1061
+ const state = data.state;
1062
+ const updates = yield this._storageStore.getUpdates();
1063
+ const origin = ORIGIN_INITIALIZED;
1064
+ const updateStorage = (update) => {
1065
+ return this._crdtManager.initialized ? this._crdtManager.applyUpdate({ update, origin }) : this._crdtManager.initialize({ update, origin });
1066
+ };
1067
+ if (changeKind === "empty") {
1068
+ this._crdtManager.destroy();
1069
+ this._crdtManager.initialize({ origin, update: updates });
1070
+ } else if (changeKind === "initialized") {
1071
+ if (!!updates.length) updateStorage(updates);
1072
+ else {
1073
+ const encodedState2 = updateStorage(state).doc.getEncodedState();
1074
+ this._addToStorageStore(encodedState2);
1075
+ }
1076
+ } else {
1077
+ const encodedState2 = updateStorage(state).doc.getEncodedState();
1078
+ if (!!updates.length) this._crdtManager.applyUpdate({ update: updates, origin });
1079
+ yield this._addToStorageStore(encodedState2);
1080
+ }
1059
1081
  const encodedState = this._crdtManager.doc.getEncodedState();
1060
- yield this._storageStore.addUpdate(encodedState);
1061
1082
  this._emitSharedTypes();
1062
- const update = this._crdtManager.doc.getEncodedState();
1083
+ this._observeCrdt();
1084
+ this._stateNotifier.subjects["storage-loaded"].next(true);
1063
1085
  this._sendMessage({
1064
1086
  type: "$updateStorage",
1065
- data: { origin: ORIGIN_INITIALIZED, update }
1087
+ data: { origin, update: encodedState }
1066
1088
  });
1067
1089
  });
1068
1090
  }
@@ -1071,7 +1093,6 @@ var PluvRoom = class extends AbstractRoom {
1071
1093
  if (!connectionId) return;
1072
1094
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1073
1095
  const data = message.data;
1074
- if (!this._crdtManager) return;
1075
1096
  this._crdtManager.doc.applyEncodedState({
1076
1097
  origin: ORIGIN_STORAGE_UPDATED,
1077
1098
  update: data.state
@@ -1079,7 +1100,6 @@ var PluvRoom = class extends AbstractRoom {
1079
1100
  const sharedTypes = this._crdtManager.doc.get();
1080
1101
  const storageRoot = Object.keys(sharedTypes).reduce(
1081
1102
  (acc, prop) => {
1082
- if (!this._crdtManager) return acc;
1083
1103
  const serialized = this._crdtManager.doc.toJson(prop);
1084
1104
  this._crdtNotifier.subject(prop).next(serialized);
1085
1105
  return __spreadProps(__spreadValues({}, acc), { [prop]: serialized });
@@ -1126,7 +1146,10 @@ var PluvRoom = class extends AbstractRoom {
1126
1146
  }
1127
1147
  _heartbeat() {
1128
1148
  this._clearTimeout(this._timeouts.pong);
1129
- this._timeouts.pong = setTimeout(this._reconnect.bind(this), PONG_TIMEOUT_MS);
1149
+ this._timeouts.pong = setTimeout(
1150
+ this._reconnect.bind(this),
1151
+ PONG_TIMEOUT_MS
1152
+ );
1130
1153
  this._sendMessage('{"type":"$ping","data":{}}');
1131
1154
  }
1132
1155
  _logDebug(...data) {
@@ -1137,15 +1160,14 @@ var PluvRoom = class extends AbstractRoom {
1137
1160
  _observeCrdt() {
1138
1161
  var _a, _b;
1139
1162
  (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
1140
- if (!this._crdtManager) return;
1141
1163
  const unsubscribe = this._crdtManager.doc.subscribe((event) => {
1142
1164
  var _a2;
1143
1165
  const origin = (_a2 = event.origin) != null ? _a2 : null;
1144
- this._addToStorageStore(event.update);
1145
1166
  this._emitSharedTypes();
1146
1167
  if (origin === ORIGIN_INITIALIZED || origin === ORIGIN_STORAGE_UPDATED) {
1147
1168
  return;
1148
1169
  }
1170
+ this._addToStorageStore(event.update);
1149
1171
  if (!this._state.webSocket) return;
1150
1172
  if (!this._state.connection.id) return;
1151
1173
  if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
@@ -1267,7 +1289,10 @@ var PluvRoom = class extends AbstractRoom {
1267
1289
  return oldState;
1268
1290
  });
1269
1291
  this._clearInterval(this._intervals.heartbeat);
1270
- this._intervals.heartbeat = setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1292
+ this._intervals.heartbeat = setInterval(
1293
+ this._heartbeat.bind(this),
1294
+ HEARTBEAT_INTERVAL_MS
1295
+ );
1271
1296
  }
1272
1297
  _onNavigatorOffline() {
1273
1298
  return __async(this, null, function* () {
@@ -1322,8 +1347,14 @@ var PluvRoom = class extends AbstractRoom {
1322
1347
  _pollReconnect() {
1323
1348
  this._clearTimeout(this._timeouts.reconnect);
1324
1349
  const timeoutMs = typeof this._reconnectTimeoutMs === "number" ? this._reconnectTimeoutMs : this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
1325
- const clampedMs = Math.max(Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs), MAX_RECONNECT_TIMEOUT_MS);
1326
- this._timeouts.reconnect = setTimeout(this._reconnect.bind(this), clampedMs);
1350
+ const clampedMs = Math.max(
1351
+ Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs),
1352
+ MAX_RECONNECT_TIMEOUT_MS
1353
+ );
1354
+ this._timeouts.reconnect = setTimeout(
1355
+ this._reconnect.bind(this),
1356
+ clampedMs
1357
+ );
1327
1358
  }
1328
1359
  _reconnect() {
1329
1360
  return __async(this, null, function* () {
@@ -1522,10 +1553,7 @@ var MockedRoom = class extends AbstractRoom {
1522
1553
  room: this.id,
1523
1554
  user: myself
1524
1555
  };
1525
- const output = yield procedure.config.broadcast(
1526
- parsed,
1527
- context
1528
- );
1556
+ const output = yield procedure.config.broadcast(parsed, context);
1529
1557
  Object.entries(output).forEach(([_type, _data]) => {
1530
1558
  this._simulateEvent(_type, _data);
1531
1559
  });
@@ -1616,7 +1644,11 @@ var MockedRoom = class extends AbstractRoom {
1616
1644
  presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1617
1645
  }, limits);
1618
1646
  this._router = router != null ? router : new PluvRouter({});
1619
- this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
1647
+ this._usersManager = new UsersManager({
1648
+ initialPresence,
1649
+ limits: this._limits,
1650
+ presence
1651
+ });
1620
1652
  this._crdtManager = new CrdtManager({
1621
1653
  initialStorage
1622
1654
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "@pluv/io frontend client",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -18,15 +18,15 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "wonka": "^6.3.5",
21
- "@pluv/crdt": "^1.0.0",
22
- "@pluv/types": "^1.0.0"
21
+ "@pluv/crdt": "^1.0.2",
22
+ "@pluv/types": "^1.0.2"
23
23
  },
24
24
  "devDependencies": {
25
25
  "eslint": "^9.25.1",
26
26
  "tsup": "^8.4.0",
27
27
  "typescript": "^5.8.3",
28
- "@pluv/tsconfig": "^1.0.0",
29
- "eslint-config-pluv": "^1.0.0"
28
+ "@pluv/tsconfig": "^1.0.2",
29
+ "eslint-config-pluv": "^1.0.2"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm,cjs --dts",
@@ -49,7 +49,9 @@ export abstract class AbstractRoom<
49
49
  public abstract getStorage<TKey extends keyof TStorage>(type: TKey): TStorage[TKey] | null;
50
50
 
51
51
  public abstract getStorageJson(): InferCrdtJson<TStorage> | null;
52
- public abstract getStorageJson<TKey extends keyof TStorage>(type: TKey): InferCrdtJson<TStorage[TKey]> | null;
52
+ public abstract getStorageJson<TKey extends keyof TStorage>(
53
+ type: TKey,
54
+ ): InferCrdtJson<TStorage[TKey]> | null;
53
55
 
54
56
  public abstract other(
55
57
  connectionId: string,
@@ -2,15 +2,18 @@ import type { AbstractCrdtDoc, AbstractCrdtDocFactory, CrdtType } from "@pluv/cr
2
2
  import { noop } from "@pluv/crdt";
3
3
 
4
4
  export type CrdtManagerOptions<TStorage extends Record<string, CrdtType<any, any>> = {}> = {
5
- encodedState?: string | Uint8Array | null;
6
5
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
7
6
  };
8
7
 
9
- type CrdtManagerInitializeParams = {
10
- onInitialized?: (state: string) => void;
8
+ interface CrdtManagerInitializeParams {
11
9
  origin?: any;
12
10
  update: string | readonly string[];
13
- };
11
+ }
12
+
13
+ interface ApplyUpdateParams {
14
+ origin?: any;
15
+ update: string | readonly string[];
16
+ }
14
17
 
15
18
  export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
16
19
  public doc: AbstractCrdtDoc<TStorage>;
@@ -19,13 +22,10 @@ export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
19
22
  private readonly _docFactory: AbstractCrdtDocFactory<TStorage>;
20
23
 
21
24
  constructor(options: CrdtManagerOptions<TStorage>) {
22
- const { encodedState, initialStorage = noop.doc({}) as AbstractCrdtDocFactory<TStorage> } = options;
25
+ const { initialStorage = noop.doc({}) as AbstractCrdtDocFactory<TStorage> } = options;
23
26
 
24
27
  this._docFactory = initialStorage;
25
-
26
- this.doc = encodedState
27
- ? initialStorage.getFresh().applyEncodedState({ update: encodedState })
28
- : initialStorage.getFresh();
28
+ this.doc = initialStorage.getEmpty();
29
29
  }
30
30
 
31
31
  /**
@@ -34,7 +34,9 @@ export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
34
34
  * update. This unfortunately means updates can be dropped entirely
35
35
  * @date June 13, 2023
36
36
  */
37
- public applyUpdate(update: string | readonly string[], origin?: any): this {
37
+ public applyUpdate(params: ApplyUpdateParams): this {
38
+ const { origin, update } = params;
39
+
38
40
  if (!this.initialized) return this;
39
41
 
40
42
  const updates: readonly string[] = typeof update === "string" ? [update] : update;
@@ -45,48 +47,68 @@ export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
45
47
  }
46
48
 
47
49
  public destroy(): void {
50
+ this.initialized = false;
48
51
  this.doc.destroy();
52
+ this.doc = this._docFactory.getEmpty();
49
53
  }
50
54
 
51
55
  public get<TKey extends keyof TStorage>(key: TKey): TStorage[TKey] {
52
56
  return this.doc.get(key);
53
57
  }
54
58
 
55
- public initialize(params: CrdtManagerInitializeParams): this {
56
- const { onInitialized, origin, update } = params;
57
-
58
- const updates = typeof update === "string" ? [update] : update;
59
-
60
- if (!updates.length) return this;
59
+ public getInitialState(): string {
60
+ return this._docFactory.getInitialized().getEncodedState();
61
+ }
61
62
 
63
+ public initialize(params: CrdtManagerInitializeParams): this {
64
+ const { origin, update } = params;
65
+
66
+ /**
67
+ * !HACK
68
+ * @description If the doc was already initialized, we don't want to re-initialize
69
+ * the doc and overwrite what's already there. Skip the initialization and log a
70
+ * warning.
71
+ * @date May 7, 2025
72
+ */
62
73
  if (this.initialized) {
63
- this.doc = this._applyDocUpdates(this.doc, updates, origin).track();
64
-
74
+ console.warn("Attempted to initialize storage multiple times");
65
75
  return this;
66
76
  }
67
77
 
68
- const onEmpty = this._applyDocUpdates(this._docFactory.getEmpty(), updates, origin);
69
-
70
- if (!onEmpty.isEmpty()) {
71
- onEmpty.destroy();
78
+ /**
79
+ * !HACK
80
+ * @description If the doc is already populated, somehow we're attempting to overwrite
81
+ * a doc that was presumably initialized but not identified as initialized. Skip the
82
+ * initialization and log a warning.
83
+ * @date May 7, 2025
84
+ */
85
+ if (!this.doc.isEmpty()) {
86
+ console.warn("Attempted to initialize over a non-empty storage");
87
+ return this;
88
+ }
72
89
 
73
- this.doc = this._applyDocUpdates(this._docFactory.getFresh(), updates, origin).track();
74
- this.initialized = true;
90
+ const updates = typeof update === "string" ? [update] : update;
75
91
 
76
- onInitialized?.(this.doc.getEncodedState());
92
+ if (!updates.length) return this;
77
93
 
78
- return this;
79
- }
94
+ const initialized = this._docFactory.getInitialized();
95
+ const reference = initialized.get();
80
96
 
81
- onEmpty.destroy();
82
- this.doc = this._docFactory.getInitialized().track();
97
+ this.doc = this._applyDocUpdates(
98
+ this._docFactory.getEmpty(),
99
+ updates,
100
+ origin,
101
+ ).rebuildStorage(reference);
83
102
  this.initialized = true;
84
-
85
- onInitialized?.(this.doc.getEncodedState());
103
+ initialized.destroy();
86
104
 
87
105
  return this;
88
106
  }
89
107
 
108
+ public resolveEncodedState(updates: string | string[] | readonly string[]): string | null {
109
+ return this._docFactory.resolveEncodedState(updates);
110
+ }
111
+
90
112
  private _applyDocUpdates(
91
113
  doc: AbstractCrdtDoc<TStorage>,
92
114
  updates: readonly string[],
@@ -7,7 +7,10 @@ export class CrdtNotifier<TStorage extends Record<string, CrdtType<any, any>>> {
7
7
  [P in keyof TStorage]: InferCrdtJson<TStorage[P]>;
8
8
  }>();
9
9
 
10
- public subjects = new Map<keyof TStorage, Subject<InferCrdtJson<TStorage[keyof TStorage]>> | null>();
10
+ public subjects = new Map<
11
+ keyof TStorage,
12
+ Subject<InferCrdtJson<TStorage[keyof TStorage]>> | null
13
+ >();
11
14
 
12
15
  public subject<TKey extends keyof TStorage>(key: TKey): Subject<InferCrdtJson<TStorage[TKey]>> {
13
16
  const subject = this.subjects.get(key);
@@ -2,14 +2,20 @@ import type { Id, InferIOOutput, IOEventMessage, IOLike } from "@pluv/types";
2
2
  import type { Subject } from "wonka";
3
3
  import { makeSubject, subscribe } from "wonka";
4
4
 
5
- export type EventNotifierSubscriptionCallback<TIO extends IOLike<any, any>, TEvent extends keyof InferIOOutput<TIO>> = (
6
- value: Id<IOEventMessage<TIO, TEvent>>,
7
- ) => void;
5
+ export type EventNotifierSubscriptionCallback<
6
+ TIO extends IOLike<any, any>,
7
+ TEvent extends keyof InferIOOutput<TIO>,
8
+ > = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
8
9
 
9
10
  export class EventNotifier<TIO extends IOLike> {
10
- public subjects = new Map<keyof InferIOOutput<TIO>, Subject<Id<IOEventMessage<TIO, keyof InferIOOutput<TIO>>>>>();
11
+ public subjects = new Map<
12
+ keyof InferIOOutput<TIO>,
13
+ Subject<Id<IOEventMessage<TIO, keyof InferIOOutput<TIO>>>>
14
+ >();
11
15
 
12
- public subject<TEvent extends keyof InferIOOutput<TIO>>(name: TEvent): Subject<Id<IOEventMessage<TIO, TEvent>>> {
16
+ public subject<TEvent extends keyof InferIOOutput<TIO>>(
17
+ name: TEvent,
18
+ ): Subject<Id<IOEventMessage<TIO, TEvent>>> {
13
19
  const subject = this.subjects.get(name);
14
20
 
15
21
  if (subject) return subject;