@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @pluv/client@1.0.0 build /home/runner/work/pluv/pluv/packages/client
2
+ > @pluv/client@1.0.2 build /home/runner/work/pluv/pluv/packages/client
3
3
  > tsup src/index.ts --format esm,cjs --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -8,11 +8,11 @@
8
8
  CLI Target: es6
9
9
  ESM Build start
10
10
  CJS Build start
11
- CJS dist/index.js 58.23 KB
12
- CJS ⚡️ Build success in 110ms
13
- ESM dist/index.mjs 56.91 KB
14
- ESM ⚡️ Build success in 114ms
11
+ ESM dist/index.mjs 57.31 KB
12
+ ESM ⚡️ Build success in 126ms
13
+ CJS dist/index.js 58.63 KB
14
+ CJS ⚡️ Build success in 126ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3353ms
17
- DTS dist/index.d.mts 25.38 KB
18
- DTS dist/index.d.ts 25.38 KB
16
+ DTS ⚡️ Build success in 3638ms
17
+ DTS dist/index.d.mts 25.31 KB
18
+ DTS dist/index.d.ts 25.31 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @pluv/client
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d87159c: Restored `@pluv/addon-indexeddb` functionality, and fixed `@pluv/addon-indexeddb` sometimes duplicating storage operations based on `initialStorage` values.
8
+ - @pluv/crdt@1.0.2
9
+ - @pluv/types@1.0.2
10
+
11
+ ## 1.0.1
12
+
13
+ ### Patch Changes
14
+
15
+ - c583be9: Fixed storage race condition that occurred when 2 users joined a room near simultaneously that would cause users to have mismatched storage states from the server (and thereby preventing updates from properly registering) until the user reconnected.
16
+ - @pluv/crdt@1.0.1
17
+ - @pluv/types@1.0.1
18
+
3
19
  ## 1.0.0
4
20
 
5
21
  ### Major Changes
package/dist/index.d.mts CHANGED
@@ -48,7 +48,6 @@ declare abstract class AbstractStorageStore {
48
48
  }
49
49
 
50
50
  type CrdtManagerOptions<TStorage extends Record<string, CrdtType<any, any>> = {}> = {
51
- encodedState?: string | Uint8Array | null;
52
51
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
53
52
  };
54
53
 
@@ -189,7 +188,6 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
189
188
  undo: () => void;
190
189
  updateMyPresence: (presence: UpdateMyPresenceAction<TPresence>) => void;
191
190
  private _addToStorageStore;
192
- private _applyStorageStore;
193
191
  private _attachWindowListeners;
194
192
  private _attachWsListeners;
195
193
  private _clearInterval;
package/dist/index.d.ts CHANGED
@@ -48,7 +48,6 @@ declare abstract class AbstractStorageStore {
48
48
  }
49
49
 
50
50
  type CrdtManagerOptions<TStorage extends Record<string, CrdtType<any, any>> = {}> = {
51
- encodedState?: string | Uint8Array | null;
52
51
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
53
52
  };
54
53
 
@@ -189,7 +188,6 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
189
188
  undo: () => void;
190
189
  updateMyPresence: (presence: UpdateMyPresenceAction<TPresence>) => void;
191
190
  private _addToStorageStore;
192
- private _applyStorageStore;
193
191
  private _attachWindowListeners;
194
192
  private _attachWsListeners;
195
193
  private _clearInterval;
package/dist/index.js CHANGED
@@ -126,9 +126,9 @@ var import_crdt = require("@pluv/crdt");
126
126
  var CrdtManager = class {
127
127
  constructor(options) {
128
128
  this.initialized = false;
129
- const { encodedState, initialStorage = import_crdt.noop.doc({}) } = options;
129
+ const { initialStorage = import_crdt.noop.doc({}) } = options;
130
130
  this._docFactory = initialStorage;
131
- this.doc = encodedState ? initialStorage.getFresh().applyEncodedState({ update: encodedState }) : initialStorage.getFresh();
131
+ this.doc = initialStorage.getEmpty();
132
132
  }
133
133
  /**
134
134
  * !HACK
@@ -136,40 +136,50 @@ var CrdtManager = class {
136
136
  * update. This unfortunately means updates can be dropped entirely
137
137
  * @date June 13, 2023
138
138
  */
139
- applyUpdate(update, origin) {
139
+ applyUpdate(params) {
140
+ const { origin, update } = params;
140
141
  if (!this.initialized) return this;
141
142
  const updates = typeof update === "string" ? [update] : update;
142
143
  this._applyDocUpdates(this.doc, updates, origin);
143
144
  return this;
144
145
  }
145
146
  destroy() {
147
+ this.initialized = false;
146
148
  this.doc.destroy();
149
+ this.doc = this._docFactory.getEmpty();
147
150
  }
148
151
  get(key) {
149
152
  return this.doc.get(key);
150
153
  }
154
+ getInitialState() {
155
+ return this._docFactory.getInitialized().getEncodedState();
156
+ }
151
157
  initialize(params) {
152
- const { onInitialized, origin, update } = params;
153
- const updates = typeof update === "string" ? [update] : update;
154
- if (!updates.length) return this;
158
+ const { origin, update } = params;
155
159
  if (this.initialized) {
156
- this.doc = this._applyDocUpdates(this.doc, updates, origin).track();
160
+ console.warn("Attempted to initialize storage multiple times");
157
161
  return this;
158
162
  }
159
- const onEmpty = this._applyDocUpdates(this._docFactory.getEmpty(), updates, origin);
160
- if (!onEmpty.isEmpty()) {
161
- onEmpty.destroy();
162
- this.doc = this._applyDocUpdates(this._docFactory.getFresh(), updates, origin).track();
163
- this.initialized = true;
164
- onInitialized == null ? void 0 : onInitialized(this.doc.getEncodedState());
163
+ if (!this.doc.isEmpty()) {
164
+ console.warn("Attempted to initialize over a non-empty storage");
165
165
  return this;
166
166
  }
167
- onEmpty.destroy();
168
- this.doc = this._docFactory.getInitialized().track();
167
+ const updates = typeof update === "string" ? [update] : update;
168
+ if (!updates.length) return this;
169
+ const initialized = this._docFactory.getInitialized();
170
+ const reference = initialized.get();
171
+ this.doc = this._applyDocUpdates(
172
+ this._docFactory.getEmpty(),
173
+ updates,
174
+ origin
175
+ ).rebuildStorage(reference);
169
176
  this.initialized = true;
170
- onInitialized == null ? void 0 : onInitialized(this.doc.getEncodedState());
177
+ initialized.destroy();
171
178
  return this;
172
179
  }
180
+ resolveEncodedState(updates) {
181
+ return this._docFactory.resolveEncodedState(updates);
182
+ }
173
183
  _applyDocUpdates(doc, updates, origin) {
174
184
  doc.transact(() => {
175
185
  doc.batchApplyEncodedState({ origin, updates });
@@ -264,7 +274,10 @@ var PluvRouter = class _PluvRouter {
264
274
  this._defs = { events };
265
275
  }
266
276
  static merge(...routers) {
267
- const events = Object.assign(/* @__PURE__ */ Object.create(null), ...routers.map((router) => router._defs.events));
277
+ const events = Object.assign(
278
+ /* @__PURE__ */ Object.create(null),
279
+ ...routers.map((router) => router._defs.events)
280
+ );
268
281
  if (Object.keys(events).some((key) => key.startsWith("$"))) {
269
282
  throw new Error('Procedures may not start with "$"');
270
283
  }
@@ -417,7 +430,11 @@ var UsersManager = class {
417
430
  const userInfo = (_a = this._others.get(clientId)) != null ? _a : null;
418
431
  if (!userInfo) return null;
419
432
  const remaining = (_c = (_b = this._getConnectionIds(clientId)) == null ? void 0 : _b.size) != null ? _c : 0;
420
- const result = { clientId, remaining, data: userInfo };
433
+ const result = {
434
+ clientId,
435
+ remaining,
436
+ data: userInfo
437
+ };
421
438
  if (!remaining) this._others.delete(clientId);
422
439
  return result;
423
440
  }
@@ -454,7 +471,10 @@ var UsersManager = class {
454
471
  const other = this._others.get(clientId);
455
472
  if (!other) return null;
456
473
  const cleanedPatch = pickBy(patch != null ? patch : {}, (value) => typeof value !== "undefined");
457
- const cleanedPresence = pickBy((_a = other.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
474
+ const cleanedPresence = pickBy(
475
+ (_a = other.presence) != null ? _a : {},
476
+ (value) => typeof value !== "undefined"
477
+ );
458
478
  const presence = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
459
479
  const validated = this._presence ? this._presence.parse(presence) : presence;
460
480
  this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence: validated }));
@@ -623,10 +643,7 @@ var PluvRoom = class extends AbstractRoom {
623
643
  room: this.id,
624
644
  user: myself
625
645
  };
626
- const output = yield procedure.config.broadcast(
627
- parsed,
628
- context
629
- );
646
+ const output = yield procedure.config.broadcast(parsed, context);
630
647
  Object.entries(output).forEach(([_type, _data]) => {
631
648
  this._sendMessage({ data: _data, type: _type });
632
649
  });
@@ -722,7 +739,7 @@ var PluvRoom = class extends AbstractRoom {
722
739
  const shouldFlatten = yield this._storageStore.getShouldFlatten();
723
740
  if (!shouldFlatten) return;
724
741
  const updates = yield this._storageStore.getUpdates();
725
- this._crdtManager.applyUpdate(updates, this);
742
+ this._crdtManager.applyUpdate({ update: updates, origin: this });
726
743
  const encodedState = this._crdtManager.doc.getEncodedState();
727
744
  yield this._storageStore.flatten(encodedState);
728
745
  }),
@@ -749,7 +766,11 @@ var PluvRoom = class extends AbstractRoom {
749
766
  }
750
767
  };
751
768
  this._router = router != null ? router : new PluvRouter({});
752
- this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
769
+ this._usersManager = new UsersManager({
770
+ initialPresence,
771
+ limits: this._limits,
772
+ presence
773
+ });
753
774
  this._crdtManager = new CrdtManager({ initialStorage });
754
775
  }
755
776
  get storageLoaded() {
@@ -764,9 +785,11 @@ var PluvRoom = class extends AbstractRoom {
764
785
  const params = (_a = args[0]) != null ? _a : {};
765
786
  const metadata = params.metadata;
766
787
  this._setMetadata(metadata);
767
- const canConnect = [ConnectionState.Closed, ConnectionState.Unavailable, ConnectionState.Untouched].some(
768
- (state) => this._state.connection.state === state
769
- );
788
+ const canConnect = [
789
+ ConnectionState.Closed,
790
+ ConnectionState.Unavailable,
791
+ ConnectionState.Untouched
792
+ ].some((state) => this._state.connection.state === state);
770
793
  if (!canConnect) return;
771
794
  this._clearTimeout(this._timeouts.reconnect);
772
795
  this._updateState((oldState) => {
@@ -774,7 +797,6 @@ var PluvRoom = class extends AbstractRoom {
774
797
  return oldState;
775
798
  });
776
799
  yield this._storageStore.initialize();
777
- yield this._applyStorageStore();
778
800
  const wsEndpoint = this._getWsEndpoint(this.id, params);
779
801
  const url = new URL(wsEndpoint);
780
802
  let authToken = null;
@@ -839,20 +861,6 @@ var PluvRoom = class extends AbstractRoom {
839
861
  this._flattenStorageStore();
840
862
  });
841
863
  }
842
- _applyStorageStore() {
843
- return __async(this, null, function* () {
844
- const updates = yield this._storageStore.getUpdates();
845
- this._crdtManager.initialize({
846
- onInitialized: () => {
847
- this._stateNotifier.subjects["storage-loaded"].next(true);
848
- this._observeCrdt();
849
- },
850
- origin: ORIGIN_INITIALIZED,
851
- update: updates
852
- });
853
- this._emitSharedTypes();
854
- });
855
- }
856
864
  _attachWindowListeners() {
857
865
  if (typeof window === "undefined") return;
858
866
  if (typeof document === "undefined") return;
@@ -1043,32 +1051,35 @@ var PluvRoom = class extends AbstractRoom {
1043
1051
  this._stateNotifier.subjects.others.next(others);
1044
1052
  }
1045
1053
  _handleRegisteredMessage(message) {
1046
- var _a, _b, _c;
1047
- const { connectionId } = message;
1048
- const user = message.user;
1049
- if (!connectionId) return;
1050
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1051
- const data = message.data;
1052
- this._updateState((oldState) => {
1053
- oldState.connection.count += 1;
1054
- oldState.connection.id = connectionId;
1055
- oldState.authorization.user = user != null ? user : null;
1056
- return oldState;
1057
- });
1058
- const userInfo = {
1059
- connectionId,
1060
- presence: (_a = data.presence) != null ? _a : void 0,
1061
- user
1062
- };
1063
- this._usersManager.setMyself(userInfo);
1064
- const presence = this._usersManager.myPresence;
1065
- const myself = (_b = this._usersManager.myself) != null ? _b : null;
1066
- this._stateNotifier.subjects["my-presence"].next(presence);
1067
- this._stateNotifier.subjects.myself.next(myself);
1068
- const update = this._state.connection.count > 1 ? (_c = this._crdtManager.doc.getEncodedState()) != null ? _c : null : null;
1069
- this._sendMessage({
1070
- type: "$initializeSession",
1071
- data: { presence, update }
1054
+ return __async(this, null, function* () {
1055
+ var _a, _b, _c;
1056
+ const { connectionId } = message;
1057
+ const user = message.user;
1058
+ if (!connectionId) return;
1059
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1060
+ const data = message.data;
1061
+ const state = data.state;
1062
+ this._updateState((oldState) => {
1063
+ oldState.connection.count += 1;
1064
+ oldState.connection.id = connectionId;
1065
+ oldState.authorization.user = user != null ? user : null;
1066
+ return oldState;
1067
+ });
1068
+ const userInfo = {
1069
+ connectionId,
1070
+ presence: (_a = data.presence) != null ? _a : void 0,
1071
+ user
1072
+ };
1073
+ this._usersManager.setMyself(userInfo);
1074
+ const presence = this._usersManager.myPresence;
1075
+ const myself = (_b = this._usersManager.myself) != null ? _b : null;
1076
+ this._stateNotifier.subjects["my-presence"].next(presence);
1077
+ this._stateNotifier.subjects.myself.next(myself);
1078
+ const update = !state ? (_c = this._crdtManager.resolveEncodedState(yield this._storageStore.getUpdates())) != null ? _c : this._crdtManager.getInitialState() : this._crdtManager.getInitialState();
1079
+ this._sendMessage({
1080
+ type: "$initializeSession",
1081
+ data: { presence, update }
1082
+ });
1072
1083
  });
1073
1084
  }
1074
1085
  _handleStorageReceivedMessage(message) {
@@ -1077,23 +1088,34 @@ var PluvRoom = class extends AbstractRoom {
1077
1088
  if (!connectionId) return;
1078
1089
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1079
1090
  const data = message.data;
1080
- this._crdtManager.initialize({
1081
- onInitialized: (update2) => __async(this, null, function* () {
1082
- this._addToStorageStore(update2);
1083
- this._emitSharedTypes();
1084
- this._observeCrdt();
1085
- this._stateNotifier.subjects["storage-loaded"].next(true);
1086
- }),
1087
- origin: ORIGIN_INITIALIZED,
1088
- update: data.state
1089
- });
1091
+ const changeKind = data.changeKind;
1092
+ const state = data.state;
1093
+ const updates = yield this._storageStore.getUpdates();
1094
+ const origin = ORIGIN_INITIALIZED;
1095
+ const updateStorage = (update) => {
1096
+ return this._crdtManager.initialized ? this._crdtManager.applyUpdate({ update, origin }) : this._crdtManager.initialize({ update, origin });
1097
+ };
1098
+ if (changeKind === "empty") {
1099
+ this._crdtManager.destroy();
1100
+ this._crdtManager.initialize({ origin, update: updates });
1101
+ } else if (changeKind === "initialized") {
1102
+ if (!!updates.length) updateStorage(updates);
1103
+ else {
1104
+ const encodedState2 = updateStorage(state).doc.getEncodedState();
1105
+ this._addToStorageStore(encodedState2);
1106
+ }
1107
+ } else {
1108
+ const encodedState2 = updateStorage(state).doc.getEncodedState();
1109
+ if (!!updates.length) this._crdtManager.applyUpdate({ update: updates, origin });
1110
+ yield this._addToStorageStore(encodedState2);
1111
+ }
1090
1112
  const encodedState = this._crdtManager.doc.getEncodedState();
1091
- yield this._storageStore.addUpdate(encodedState);
1092
1113
  this._emitSharedTypes();
1093
- const update = this._crdtManager.doc.getEncodedState();
1114
+ this._observeCrdt();
1115
+ this._stateNotifier.subjects["storage-loaded"].next(true);
1094
1116
  this._sendMessage({
1095
1117
  type: "$updateStorage",
1096
- data: { origin: ORIGIN_INITIALIZED, update }
1118
+ data: { origin, update: encodedState }
1097
1119
  });
1098
1120
  });
1099
1121
  }
@@ -1102,7 +1124,6 @@ var PluvRoom = class extends AbstractRoom {
1102
1124
  if (!connectionId) return;
1103
1125
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1104
1126
  const data = message.data;
1105
- if (!this._crdtManager) return;
1106
1127
  this._crdtManager.doc.applyEncodedState({
1107
1128
  origin: ORIGIN_STORAGE_UPDATED,
1108
1129
  update: data.state
@@ -1110,7 +1131,6 @@ var PluvRoom = class extends AbstractRoom {
1110
1131
  const sharedTypes = this._crdtManager.doc.get();
1111
1132
  const storageRoot = Object.keys(sharedTypes).reduce(
1112
1133
  (acc, prop) => {
1113
- if (!this._crdtManager) return acc;
1114
1134
  const serialized = this._crdtManager.doc.toJson(prop);
1115
1135
  this._crdtNotifier.subject(prop).next(serialized);
1116
1136
  return __spreadProps(__spreadValues({}, acc), { [prop]: serialized });
@@ -1157,7 +1177,10 @@ var PluvRoom = class extends AbstractRoom {
1157
1177
  }
1158
1178
  _heartbeat() {
1159
1179
  this._clearTimeout(this._timeouts.pong);
1160
- this._timeouts.pong = setTimeout(this._reconnect.bind(this), PONG_TIMEOUT_MS);
1180
+ this._timeouts.pong = setTimeout(
1181
+ this._reconnect.bind(this),
1182
+ PONG_TIMEOUT_MS
1183
+ );
1161
1184
  this._sendMessage('{"type":"$ping","data":{}}');
1162
1185
  }
1163
1186
  _logDebug(...data) {
@@ -1168,15 +1191,14 @@ var PluvRoom = class extends AbstractRoom {
1168
1191
  _observeCrdt() {
1169
1192
  var _a, _b;
1170
1193
  (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
1171
- if (!this._crdtManager) return;
1172
1194
  const unsubscribe = this._crdtManager.doc.subscribe((event) => {
1173
1195
  var _a2;
1174
1196
  const origin = (_a2 = event.origin) != null ? _a2 : null;
1175
- this._addToStorageStore(event.update);
1176
1197
  this._emitSharedTypes();
1177
1198
  if (origin === ORIGIN_INITIALIZED || origin === ORIGIN_STORAGE_UPDATED) {
1178
1199
  return;
1179
1200
  }
1201
+ this._addToStorageStore(event.update);
1180
1202
  if (!this._state.webSocket) return;
1181
1203
  if (!this._state.connection.id) return;
1182
1204
  if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
@@ -1298,7 +1320,10 @@ var PluvRoom = class extends AbstractRoom {
1298
1320
  return oldState;
1299
1321
  });
1300
1322
  this._clearInterval(this._intervals.heartbeat);
1301
- this._intervals.heartbeat = setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1323
+ this._intervals.heartbeat = setInterval(
1324
+ this._heartbeat.bind(this),
1325
+ HEARTBEAT_INTERVAL_MS
1326
+ );
1302
1327
  }
1303
1328
  _onNavigatorOffline() {
1304
1329
  return __async(this, null, function* () {
@@ -1353,8 +1378,14 @@ var PluvRoom = class extends AbstractRoom {
1353
1378
  _pollReconnect() {
1354
1379
  this._clearTimeout(this._timeouts.reconnect);
1355
1380
  const timeoutMs = typeof this._reconnectTimeoutMs === "number" ? this._reconnectTimeoutMs : this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
1356
- const clampedMs = Math.max(Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs), MAX_RECONNECT_TIMEOUT_MS);
1357
- this._timeouts.reconnect = setTimeout(this._reconnect.bind(this), clampedMs);
1381
+ const clampedMs = Math.max(
1382
+ Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs),
1383
+ MAX_RECONNECT_TIMEOUT_MS
1384
+ );
1385
+ this._timeouts.reconnect = setTimeout(
1386
+ this._reconnect.bind(this),
1387
+ clampedMs
1388
+ );
1358
1389
  }
1359
1390
  _reconnect() {
1360
1391
  return __async(this, null, function* () {
@@ -1553,10 +1584,7 @@ var MockedRoom = class extends AbstractRoom {
1553
1584
  room: this.id,
1554
1585
  user: myself
1555
1586
  };
1556
- const output = yield procedure.config.broadcast(
1557
- parsed,
1558
- context
1559
- );
1587
+ const output = yield procedure.config.broadcast(parsed, context);
1560
1588
  Object.entries(output).forEach(([_type, _data]) => {
1561
1589
  this._simulateEvent(_type, _data);
1562
1590
  });
@@ -1647,7 +1675,11 @@ var MockedRoom = class extends AbstractRoom {
1647
1675
  presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1648
1676
  }, limits);
1649
1677
  this._router = router != null ? router : new PluvRouter({});
1650
- this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
1678
+ this._usersManager = new UsersManager({
1679
+ initialPresence,
1680
+ limits: this._limits,
1681
+ presence
1682
+ });
1651
1683
  this._crdtManager = new CrdtManager({
1652
1684
  initialStorage
1653
1685
  });