@pluv/client 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { makeSubject, subscribe } from "wonka";
4
4
 
5
5
  export type EventNotifierSubscriptionCallback<
6
6
  TIO extends IOLike,
7
- TEvent extends keyof InferIOOutput<TIO>
7
+ TEvent extends keyof InferIOOutput<TIO>,
8
8
  > = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
9
9
 
10
10
  export class EventNotifier<TIO extends IOLike> {
@@ -14,7 +14,7 @@ export class EventNotifier<TIO extends IOLike> {
14
14
  >();
15
15
 
16
16
  public subject<TEvent extends keyof InferIOOutput<TIO>>(
17
- name: TEvent
17
+ name: TEvent,
18
18
  ): Subject<Id<IOEventMessage<TIO, TEvent>>> {
19
19
  const subject = this.subjects.get(name);
20
20
 
@@ -29,7 +29,7 @@ export class EventNotifier<TIO extends IOLike> {
29
29
 
30
30
  public subscribe<TEvent extends keyof InferIOOutput<TIO>>(
31
31
  name: TEvent,
32
- callback: EventNotifierSubscriptionCallback<TIO, TEvent>
32
+ callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
33
33
  ): () => void {
34
34
  const subject = this.subject(name);
35
35
 
package/src/MockedRoom.ts CHANGED
@@ -31,14 +31,14 @@ import { UsersManager } from "./UsersManager";
31
31
 
32
32
  export type MockedRoomEvents<TIO extends IOLike> = Partial<{
33
33
  [P in keyof InferIOInput<TIO>]: (
34
- data: Id<InferIOInput<TIO>[P]>
34
+ data: Id<InferIOInput<TIO>[P]>,
35
35
  ) => Partial<InferIOOutput<TIO>>;
36
36
  }>;
37
37
 
38
38
  export type MockedRoomConfig<
39
39
  TIO extends IOLike,
40
40
  TPresence extends JsonObject = {},
41
- TStorage extends Record<string, AbstractType<any>> = {}
41
+ TStorage extends Record<string, AbstractType<any>> = {},
42
42
  > = { events?: MockedRoomEvents<TIO> } & Omit<
43
43
  CrdtManagerOptions<TStorage>,
44
44
  "encodedState"
@@ -48,9 +48,9 @@ export type MockedRoomConfig<
48
48
  export class MockedRoom<
49
49
  TIO extends IOLike,
50
50
  TPresence extends JsonObject = {},
51
- TStorage extends Record<string, AbstractType<any>> = {}
51
+ TStorage extends Record<string, AbstractType<any>> = {},
52
52
  > extends AbstractRoom<TIO, TPresence, TStorage> {
53
- private _crdtManager: CrdtManager<TStorage> | null = null;
53
+ private _crdtManager: CrdtManager<TStorage>;
54
54
  private _crdtNotifier = new CrdtNotifier<TStorage>();
55
55
  private _eventNotifier = new EventNotifier<TIO>();
56
56
  private _events?: MockedRoomEvents<TIO>;
@@ -75,7 +75,7 @@ export class MockedRoom<
75
75
 
76
76
  constructor(
77
77
  room: string,
78
- options: MockedRoomConfig<TIO, TPresence, TStorage>
78
+ options: MockedRoomConfig<TIO, TPresence, TStorage>,
79
79
  ) {
80
80
  const { events, initialPresence, initialStorage, presence } = options;
81
81
 
@@ -97,7 +97,7 @@ export class MockedRoom<
97
97
 
98
98
  public broadcast<TEvent extends keyof InferIOInput<TIO>>(
99
99
  event: TEvent,
100
- data: Id<InferIOInput<TIO>[TEvent]>
100
+ data: Id<InferIOInput<TIO>[TEvent]>,
101
101
  ): void {
102
102
  if (!this._events) return;
103
103
 
@@ -117,9 +117,17 @@ export class MockedRoom<
117
117
  });
118
118
  }
119
119
 
120
+ public canRedo = (): boolean => {
121
+ return !!this._crdtManager.doc.canRedo();
122
+ };
123
+
124
+ public canUndo = (): boolean => {
125
+ return !!this._crdtManager.doc.canUndo();
126
+ };
127
+
120
128
  public event = <TEvent extends keyof InferIOOutput<TIO>>(
121
129
  event: TEvent,
122
- callback: EventNotifierSubscriptionCallback<TIO, TEvent>
130
+ callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
123
131
  ): (() => void) => {
124
132
  return this._eventNotifier.subscribe(event, callback);
125
133
  };
@@ -127,7 +135,7 @@ export class MockedRoom<
127
135
  public getConnection(): WebSocketConnection {
128
136
  // Create a read-only clone of the connection state
129
137
  return Object.freeze(
130
- JSON.parse(JSON.stringify(this._state.connection))
138
+ JSON.parse(JSON.stringify(this._state.connection)),
131
139
  );
132
140
  }
133
141
 
@@ -140,7 +148,7 @@ export class MockedRoom<
140
148
  };
141
149
 
142
150
  public getOther = (
143
- connectionId: string
151
+ connectionId: string,
144
152
  ): Id<UserInfo<TIO, TPresence>> | null => {
145
153
  return this._usersManager.getOther(connectionId);
146
154
  };
@@ -150,34 +158,78 @@ export class MockedRoom<
150
158
  };
151
159
 
152
160
  public getStorage = <TKey extends keyof TStorage>(
153
- key: TKey
154
- ): TStorage[TKey] | null => {
155
- return this._crdtManager?.get(key) ?? null;
161
+ key: TKey,
162
+ ): TStorage[TKey] => {
163
+ const sharedType = this._crdtManager.get(key);
164
+
165
+ if (typeof sharedType === "undefined") {
166
+ throw new Error(`Could not find storege: ${key.toString()}`);
167
+ }
168
+
169
+ return sharedType;
156
170
  };
157
171
 
158
172
  public other = (
159
173
  connectionId: string,
160
- callback: OtherNotifierSubscriptionCallback<TIO>
174
+ callback: OtherNotifierSubscriptionCallback<TIO>,
161
175
  ): (() => void) => {
162
176
  return this._otherNotifier.subscribe(connectionId, callback);
163
177
  };
164
178
 
179
+ public redo = (): void => {
180
+ this._crdtManager.doc.redo();
181
+ };
182
+
165
183
  public storage = <TKey extends keyof TStorage>(
166
184
  key: TKey,
167
- fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void
185
+ fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void,
168
186
  ): (() => void) => {
169
187
  return this._crdtNotifier.subscribe(key, fn);
170
188
  };
171
189
 
190
+ public storageRoot = (
191
+ fn: (value: {
192
+ [P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
193
+ }) => void,
194
+ ): (() => void) => {
195
+ return this._crdtNotifier.subcribeRoot(fn);
196
+ };
197
+
172
198
  public subscribe = <
173
- TSubject extends keyof StateNotifierSubjects<TIO, TPresence>
199
+ TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
174
200
  >(
175
201
  name: TSubject,
176
- callback: SubscriptionCallback<TIO, TPresence, TSubject>
202
+ callback: SubscriptionCallback<TIO, TPresence, TSubject>,
177
203
  ): (() => void) => {
178
204
  return this._stateNotifier.subscribe(name, callback);
179
205
  };
180
206
 
207
+ public transact = (
208
+ fn: (storage: TStorage) => void,
209
+ origin?: string,
210
+ ): void => {
211
+ const _origin = origin ?? this._state.connection.id;
212
+ const crdtManager = this._crdtManager;
213
+
214
+ /**
215
+ * !HACK
216
+ * @description Don't transact anything if there is no origin, because
217
+ * that means the user isn't connected yet. This will mean events are
218
+ * lost unfortunately.
219
+ * @date September 23, 2023
220
+ */
221
+ if (typeof _origin !== "string") return;
222
+ if (!crdtManager) return;
223
+
224
+ crdtManager.doc.transact(() => {
225
+ fn(crdtManager.doc.storage);
226
+ }, _origin);
227
+ };
228
+
229
+ public undo = (): void => {
230
+ this._crdtManager.doc.undo();
231
+ };
232
+
181
233
  public updateMyPresence = (presence: Partial<TPresence>): void => {
182
234
  this._usersManager.updateMyPresence(presence);
183
235
 
@@ -202,14 +254,25 @@ export class MockedRoom<
202
254
 
203
255
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
204
256
 
205
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
206
- if (!this._crdtManager) return;
257
+ const storageRoot = Object.entries(sharedTypes).reduce(
258
+ (acc, [prop, sharedType]) => {
259
+ if (!this._crdtManager) return acc;
260
+
261
+ const serialized = sharedType.toJSON();
262
+
263
+ this._crdtNotifier.subject(prop).next(serialized);
207
264
 
208
- const serialized = sharedType.toJSON();
265
+ return { ...acc, [prop]: serialized };
266
+ },
267
+ {} as {
268
+ [P in keyof TStorage]: InferYjsSharedTypeJson<
269
+ TStorage[P]
270
+ >;
271
+ },
272
+ );
209
273
 
210
- this._crdtNotifier.subject(prop).next(serialized);
211
- });
212
- }
274
+ this._crdtNotifier.rootSubject.next(storageRoot);
275
+ },
213
276
  );
214
277
 
215
278
  this._subscriptions.observeCrdt = unsubscribe;
@@ -4,14 +4,14 @@ import { makeSubject, subscribe } from "wonka";
4
4
  import { UserInfo } from "./types";
5
5
 
6
6
  export type OtherNotifierSubscriptionCallback<TIO extends IOLike> = (
7
- value: Id<UserInfo<TIO>> | null
7
+ value: Id<UserInfo<TIO>> | null,
8
8
  ) => void;
9
9
 
10
10
  export class OtherNotifier<TIO extends IOLike> {
11
11
  public subjects = new Map<string, Subject<Id<UserInfo<TIO>> | null>>();
12
12
 
13
13
  public subject<TEvent extends string>(
14
- name: TEvent
14
+ name: TEvent,
15
15
  ): Subject<Id<UserInfo<TIO>> | null> {
16
16
  const subject = this.subjects.get(name);
17
17
 
@@ -26,7 +26,7 @@ export class OtherNotifier<TIO extends IOLike> {
26
26
 
27
27
  public subscribe<TEvent extends string>(
28
28
  name: TEvent,
29
- callback: OtherNotifierSubscriptionCallback<TIO>
29
+ callback: OtherNotifierSubscriptionCallback<TIO>,
30
30
  ): () => void {
31
31
  const source = this.subject(name).source;
32
32
 
package/src/PluvClient.ts CHANGED
@@ -30,10 +30,10 @@ export class PluvClient<TIO extends IOLike = IOLike> {
30
30
 
31
31
  public createRoom = <
32
32
  TPresence extends JsonObject = {},
33
- TStorage extends Record<string, AbstractType<any>> = {}
33
+ TStorage extends Record<string, AbstractType<any>> = {},
34
34
  >(
35
35
  room: string,
36
- options: PluvRoomOptions<TIO, TPresence, TStorage> = {}
36
+ options: PluvRoomOptions<TIO, TPresence, TStorage> = {},
37
37
  ): PluvRoom<TIO, TPresence, TStorage> => {
38
38
  const oldRoom = this.getRoom<TPresence, TStorage>(room);
39
39
 
@@ -57,7 +57,7 @@ export class PluvClient<TIO extends IOLike = IOLike> {
57
57
  };
58
58
 
59
59
  public enter = async (
60
- room: string | PluvRoom<TIO, any, any>
60
+ room: string | PluvRoom<TIO, any, any>,
61
61
  ): Promise<PluvRoom<TIO, JsonObject, any>> => {
62
62
  const toEnter = typeof room === "string" ? this.getRoom(room) : room;
63
63
 
@@ -76,9 +76,9 @@ export class PluvClient<TIO extends IOLike = IOLike> {
76
76
 
77
77
  public getRoom = <
78
78
  TPresence extends JsonObject = {},
79
- TStorage extends Record<string, AbstractType<any>> = {}
79
+ TStorage extends Record<string, AbstractType<any>> = {},
80
80
  >(
81
- room: string
81
+ room: string,
82
82
  ): PluvRoom<TIO, TPresence, TStorage> | null => {
83
83
  const found = this._rooms.get(room) as
84
84
  | PluvRoom<TIO, TPresence, TStorage>
@@ -92,7 +92,7 @@ export class PluvClient<TIO extends IOLike = IOLike> {
92
92
  };
93
93
 
94
94
  public leave = async (
95
- room: string | PluvRoom<TIO, any, any>
95
+ room: string | PluvRoom<TIO, any, any>,
96
96
  ): Promise<void> => {
97
97
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
98
98
 
package/src/PluvRoom.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { InferYjsSharedTypeJson } from "@pluv/crdt-yjs";
1
+ import { InferYjsSharedTypeJson, TrackOriginOptions } from "@pluv/crdt-yjs";
2
2
  import type {
3
3
  BaseIOEventRecord,
4
4
  EventMessage,
@@ -44,7 +44,8 @@ const ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1_000;
44
44
  const HEARTBEAT_INTERVAL_MS = 10_000;
45
45
  const PONG_TIMEOUT_MS = 2_000;
46
46
  const RECONNECT_TIMEOUT_MS = 30_000;
47
- const Y_ORIGIN_INITIALIZED = "$INITIALIZED";
47
+ const ORIGIN_INITIALIZED = "$INITIALIZED";
48
+ const ORIGIN_STORAGE_UPDATED = "$STORAGE_UPDATED";
48
49
 
49
50
  export const DEFAULT_PLUV_CLIENT_ADDON = <
50
51
  TIO extends IOLike = IOLike,
@@ -143,7 +144,9 @@ export type RoomConfig<
143
144
  TIO extends IOLike,
144
145
  TPresence extends JsonObject = {},
145
146
  TStorage extends Record<string, AbstractType<any>> = {},
146
- > = RoomEndpoints<TIO> & PluvRoomOptions<TIO, TPresence, TStorage>;
147
+ > = RoomEndpoints<TIO> &
148
+ PluvRoomOptions<TIO, TPresence, TStorage> &
149
+ TrackOriginOptions;
147
150
 
148
151
  export class PluvRoom<
149
152
  TIO extends IOLike,
@@ -152,6 +155,7 @@ export class PluvRoom<
152
155
  > extends AbstractRoom<TIO, TPresence, TStorage> {
153
156
  readonly _endpoints: RoomEndpoints<TIO>;
154
157
 
158
+ private _captureTimeout: number | null = null;
155
159
  private _crdtManager: CrdtManager<TStorage>;
156
160
  private _crdtNotifier = new CrdtNotifier<TStorage>();
157
161
  private _debug: boolean | PluvRoomDebug<TIO>;
@@ -182,6 +186,7 @@ export class PluvRoom<
182
186
  pong: null,
183
187
  reconnect: null,
184
188
  };
189
+ private _trackedOrigins: readonly string[] | null = null;
185
190
  private _windowListeners: WindowListeners | null = null;
186
191
  private _wsListeners: WebSocketListeners | null = null;
187
192
  private _usersManager: UsersManager<TIO, TPresence>;
@@ -190,11 +195,13 @@ export class PluvRoom<
190
195
  const {
191
196
  addons = [],
192
197
  authEndpoint,
198
+ captureTimeout,
193
199
  debug = false,
194
200
  initialPresence,
195
201
  initialStorage,
196
202
  onAuthorizationFail,
197
203
  presence,
204
+ trackedOrigins,
198
205
  wsEndpoint,
199
206
  } = options;
200
207
 
@@ -230,7 +237,14 @@ export class PluvRoom<
230
237
  presence,
231
238
  });
232
239
 
233
- this._crdtManager = new CrdtManager<TStorage>({ initialStorage });
240
+ this._crdtManager = new CrdtManager<TStorage>({
241
+ captureTimeout,
242
+ initialStorage,
243
+ trackedOrigins,
244
+ });
245
+
246
+ this._captureTimeout = captureTimeout ?? null;
247
+ this._trackedOrigins = trackedOrigins ?? null;
234
248
  }
235
249
 
236
250
  public get webSocket(): WebSocket | null {
@@ -249,6 +263,14 @@ export class PluvRoom<
249
263
  this._sendMessage({ data, type });
250
264
  }
251
265
 
266
+ public canRedo = (): boolean => {
267
+ return !!this._crdtManager.doc.canRedo();
268
+ };
269
+
270
+ public canUndo = (): boolean => {
271
+ return !!this._crdtManager.doc.canUndo();
272
+ };
273
+
252
274
  public async connect(): Promise<void> {
253
275
  if (!this._endpoints.wsEndpoint) {
254
276
  throw new Error("Must provide an wsEndpoint.");
@@ -272,8 +294,6 @@ export class PluvRoom<
272
294
  await this._storageStore.initialize();
273
295
  await this._applyStorageStore();
274
296
 
275
- console.log(this._getWsEndpoint(this.id));
276
-
277
297
  const url = new URL(this._getWsEndpoint(this.id));
278
298
 
279
299
  let authToken: string | null = null;
@@ -325,7 +345,7 @@ export class PluvRoom<
325
345
 
326
346
  await this._storageStore.destroy();
327
347
 
328
- this._crdtManager?.destroy();
348
+ this._crdtManager.destroy();
329
349
 
330
350
  this._stateNotifier.subjects["my-presence"].next(null);
331
351
  }
@@ -364,8 +384,14 @@ export class PluvRoom<
364
384
 
365
385
  public getStorage = <TKey extends keyof TStorage>(
366
386
  key: TKey,
367
- ): TStorage[TKey] | null => {
368
- return this._crdtManager?.get(key) ?? null;
387
+ ): TStorage[TKey] => {
388
+ const sharedType = this._crdtManager.get(key);
389
+
390
+ if (typeof sharedType === "undefined") {
391
+ throw new Error(`Could not find storege: ${key.toString()}`);
392
+ }
393
+
394
+ return sharedType;
369
395
  };
370
396
 
371
397
  public other = (
@@ -375,6 +401,10 @@ export class PluvRoom<
375
401
  return this._otherNotifier.subscribe(connectionId, callback);
376
402
  };
377
403
 
404
+ public redo = (): void => {
405
+ this._crdtManager.doc.redo();
406
+ };
407
+
378
408
  public storage = <TKey extends keyof TStorage>(
379
409
  key: TKey,
380
410
  fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void,
@@ -382,6 +412,14 @@ export class PluvRoom<
382
412
  return this._crdtNotifier.subscribe(key, fn);
383
413
  };
384
414
 
415
+ public storageRoot = (
416
+ fn: (value: {
417
+ [P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
418
+ }) => void,
419
+ ): (() => void) => {
420
+ return this._crdtNotifier.subcribeRoot(fn);
421
+ };
422
+
385
423
  public subscribe = <
386
424
  TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
387
425
  >(
@@ -391,6 +429,30 @@ export class PluvRoom<
391
429
  return this._stateNotifier.subscribe(name, callback);
392
430
  };
393
431
 
432
+ public transact = (
433
+ fn: (storage: TStorage) => void,
434
+ origin?: string,
435
+ ): void => {
436
+ const _origin = origin ?? this._state.connection.id;
437
+
438
+ /**
439
+ * !HACK
440
+ * @description Don't transact anything if there is no origin, because
441
+ * that means the user isn't connected yet. This will mean events are
442
+ * lost unfortunately.
443
+ * @date September 23, 2023
444
+ */
445
+ if (typeof _origin !== "string") return;
446
+
447
+ this._crdtManager.doc.transact(() => {
448
+ fn(this._crdtManager.doc.storage);
449
+ }, _origin);
450
+ };
451
+
452
+ public undo = (): void => {
453
+ this._crdtManager.doc.undo();
454
+ };
455
+
394
456
  public updateMyPresence = (presence: Partial<TPresence>): void => {
395
457
  this._usersManager.updateMyPresence(presence);
396
458
 
@@ -414,12 +476,15 @@ export class PluvRoom<
414
476
 
415
477
  private async _applyStorageStore(): Promise<void> {
416
478
  const updates = await this._storageStore.getUpdates();
479
+ const trackedOrigins = this._getTrackedOrigins();
417
480
 
418
481
  this._crdtManager.initialize({
482
+ captureTimeout: this._captureTimeout ?? undefined,
419
483
  onInitialized: () => {
420
484
  this._observeCrdt();
421
485
  },
422
- origin: Y_ORIGIN_INITIALIZED,
486
+ origin: ORIGIN_INITIALIZED,
487
+ trackedOrigins: trackedOrigins ?? undefined,
423
488
  update: updates,
424
489
  });
425
490
 
@@ -548,11 +613,20 @@ export class PluvRoom<
548
613
  private _emitSharedTypes(): void {
549
614
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
550
615
 
551
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
552
- const serialized = sharedType.toJSON();
616
+ const storageRoot = Object.entries(sharedTypes).reduce(
617
+ (acc, [prop, sharedType]) => {
618
+ const serialized = sharedType.toJSON();
553
619
 
554
- this._crdtNotifier.subject(prop).next(serialized);
555
- });
620
+ this._crdtNotifier.subject(prop).next(serialized);
621
+
622
+ return { ...acc, [prop]: serialized };
623
+ },
624
+ {} as {
625
+ [P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
626
+ },
627
+ );
628
+
629
+ this._crdtNotifier.rootSubject.next(storageRoot);
556
630
  }
557
631
 
558
632
  private _flattenStorageStore = debounce(
@@ -630,6 +704,14 @@ export class PluvRoom<
630
704
  }
631
705
  }
632
706
 
707
+ private _getTrackedOrigins(): readonly string[] | null {
708
+ const connectionId = this._state.connection.id;
709
+
710
+ return !connectionId
711
+ ? this._trackedOrigins
712
+ : [connectionId, ...(this._trackedOrigins ?? [])];
713
+ }
714
+
633
715
  private _getWsEndpoint(room: string): string {
634
716
  switch (typeof this._endpoints.wsEndpoint) {
635
717
  case "undefined":
@@ -750,7 +832,7 @@ export class PluvRoom<
750
832
 
751
833
  const update =
752
834
  this._state.connection.count > 1
753
- ? this._crdtManager?.doc.encodeStateAsUpdate() ?? null
835
+ ? this._crdtManager.doc.encodeStateAsUpdate() ?? null
754
836
  : null;
755
837
 
756
838
  this._sendMessage({
@@ -772,11 +854,15 @@ export class PluvRoom<
772
854
  InferIOAuthorize<TIO>
773
855
  >["$STORAGE_RECEIVED"];
774
856
 
857
+ const trackedOrigins = this._getTrackedOrigins();
858
+
775
859
  this._crdtManager.initialize({
860
+ captureTimeout: this._captureTimeout ?? undefined,
776
861
  onInitialized: async () => {
777
862
  this._observeCrdt();
778
863
  },
779
- origin: Y_ORIGIN_INITIALIZED,
864
+ origin: ORIGIN_INITIALIZED,
865
+ trackedOrigins: trackedOrigins ?? undefined,
780
866
  update: data.state,
781
867
  });
782
868
 
@@ -790,7 +876,7 @@ export class PluvRoom<
790
876
 
791
877
  this._sendMessage({
792
878
  type: "$UPDATE_STORAGE",
793
- data: { origin: Y_ORIGIN_INITIALIZED, update },
879
+ data: { origin: ORIGIN_INITIALIZED, update },
794
880
  });
795
881
  }
796
882
 
@@ -803,21 +889,30 @@ export class PluvRoom<
803
889
 
804
890
  const data = message.data as BaseIOEventRecord<
805
891
  InferIOAuthorize<TIO>
806
- >["$STORAGE_UPDATED"];
892
+ >[typeof ORIGIN_STORAGE_UPDATED];
807
893
 
808
894
  if (!this._crdtManager) return;
809
895
 
810
- this._crdtManager.doc.applyUpdate(data.state, "$STORAGE_UPDATED");
896
+ this._crdtManager.doc.applyUpdate(data.state, ORIGIN_STORAGE_UPDATED);
811
897
 
812
898
  const sharedTypes = this._crdtManager.doc.getSharedTypes();
813
899
 
814
- Object.entries(sharedTypes).forEach(([prop, sharedType]) => {
815
- if (!this._crdtManager) return;
900
+ const storageRoot = Object.entries(sharedTypes).reduce(
901
+ (acc, [prop, sharedType]) => {
902
+ if (!this._crdtManager) return acc;
816
903
 
817
- const serialized = sharedType.toJSON();
904
+ const serialized = sharedType.toJSON();
818
905
 
819
- this._crdtNotifier.subject(prop).next(serialized);
820
- });
906
+ this._crdtNotifier.subject(prop).next(serialized);
907
+
908
+ return { ...acc, [prop]: serialized };
909
+ },
910
+ {} as {
911
+ [P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
912
+ },
913
+ );
914
+
915
+ this._crdtNotifier.rootSubject.next(storageRoot);
821
916
  }
822
917
 
823
918
  private _handleUserJoinedMessage(message: IOEventMessage<TIO>): void {
@@ -880,13 +975,13 @@ export class PluvRoom<
880
975
  this._addToStorageStore(update);
881
976
  this._emitSharedTypes();
882
977
 
883
- if (origin === Y_ORIGIN_INITIALIZED || origin === this) return;
978
+ if (origin === ORIGIN_INITIALIZED || origin === this) return;
884
979
 
885
980
  if (!this._state.webSocket) return;
886
981
  if (!this._state.connection.id) return;
887
982
  if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
888
983
 
889
- if (origin === "$STORAGE_UPDATED") return;
984
+ if (origin === ORIGIN_STORAGE_UPDATED) return;
890
985
 
891
986
  this._sendMessage({
892
987
  type: "$UPDATE_STORAGE",
@@ -5,7 +5,7 @@ import type { UserInfo, WebSocketState } from "./types";
5
5
 
6
6
  export interface StateNotifierSubjects<
7
7
  TIO extends IOLike,
8
- TPresence extends JsonObject
8
+ TPresence extends JsonObject,
9
9
  > {
10
10
  connection: Subject<Id<WebSocketState<TIO>>>;
11
11
  "my-presence": Subject<TPresence | null>;
@@ -16,7 +16,7 @@ export interface StateNotifierSubjects<
16
16
  type InferSubjectValue<
17
17
  TIO extends IOLike,
18
18
  TPresence extends JsonObject,
19
- TSubject extends keyof StateNotifierSubjects<TIO, TPresence>
19
+ TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
20
20
  > = StateNotifierSubjects<TIO, TPresence>[TSubject] extends Subject<
21
21
  infer IValue
22
22
  >
@@ -26,12 +26,12 @@ type InferSubjectValue<
26
26
  export type SubscriptionCallback<
27
27
  TIO extends IOLike,
28
28
  TPresence extends JsonObject,
29
- TSubject extends keyof StateNotifierSubjects<TIO, TPresence>
29
+ TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
30
30
  > = (value: InferSubjectValue<TIO, TPresence, TSubject>) => void;
31
31
 
32
32
  export class StateNotifier<
33
33
  TIO extends IOLike,
34
- TPresence extends JsonObject = {}
34
+ TPresence extends JsonObject = {},
35
35
  > {
36
36
  public subjects: StateNotifierSubjects<TIO, TPresence> = {
37
37
  connection: makeSubject<Id<WebSocketState<TIO>>>(),
@@ -41,10 +41,10 @@ export class StateNotifier<
41
41
  };
42
42
 
43
43
  public subscribe<
44
- TSubject extends keyof StateNotifierSubjects<TIO, TPresence>
44
+ TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
45
45
  >(
46
46
  name: TSubject,
47
- callback: SubscriptionCallback<TIO, TPresence, TSubject>
47
+ callback: SubscriptionCallback<TIO, TPresence, TSubject>,
48
48
  ): () => void {
49
49
  const source = this.subjects[name]?.source as any;
50
50