@liveblocks/yjs 3.2.0 → 3.3.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { OpaqueRoom, IYjsProvider, YjsSyncStatus } from '@liveblocks/core';
1
+ import { OpaqueRoom, YjsSyncStatus, IYjsProvider } from '@liveblocks/core';
2
2
  import { Observable } from 'lib0/observable';
3
3
  import * as Y from 'yjs';
4
4
  import { PermanentUserData, Doc } from 'yjs';
@@ -38,6 +38,11 @@ declare class yDocHandler extends Observable<unknown> {
38
38
  private updateRoomDoc;
39
39
  private fetchRoomDoc;
40
40
  private useV2Encoding;
41
+ private localSnapshotHashΣ;
42
+ private remoteSnapshotHashΣ;
43
+ private debounceTimer;
44
+ private static readonly DEBOUNCE_INTERVAL_MS;
45
+ private isLocalAndRemoteSnapshotEqualΣ;
41
46
  constructor({ doc, isRoot, updateDoc, fetchDoc, useV2Encoding, }: {
42
47
  doc: Y.Doc;
43
48
  isRoot: boolean;
@@ -45,16 +50,19 @@ declare class yDocHandler extends Observable<unknown> {
45
50
  fetchDoc: (vector: string, guid?: string) => void;
46
51
  useV2Encoding: boolean;
47
52
  });
48
- handleServerUpdate: ({ update, stateVector, readOnly, v2, }: {
53
+ handleServerUpdate: ({ update, stateVector, readOnly, v2, remoteSnapshotHash, }: {
49
54
  update: Uint8Array;
50
55
  stateVector: string | null;
51
56
  readOnly: boolean;
52
57
  v2?: boolean;
58
+ remoteSnapshotHash: string;
53
59
  }) => void;
54
60
  syncDoc: () => void;
55
61
  get synced(): boolean;
56
62
  set synced(state: boolean);
63
+ private debounced_updateLocalSnapshot;
57
64
  private updateHandler;
65
+ experimental_getSyncStatus(): YjsSyncStatus;
58
66
  destroy(): void;
59
67
  }
60
68
 
@@ -73,13 +81,12 @@ declare class LiveblocksYjsProvider extends Observable<unknown> implements IYjsP
73
81
  private readonly unsubscribers;
74
82
  readonly awareness: Awareness;
75
83
  readonly rootDocHandler: yDocHandler;
76
- readonly subdocHandlers: Map<string, yDocHandler>;
84
+ private readonly subdocHandlersΣ;
85
+ private readonly syncStatusΣ;
77
86
  readonly permanentUserData?: PermanentUserData;
78
- private pending;
79
87
  constructor(room: OpaqueRoom, doc: Doc, options?: ProviderOptions);
80
88
  private setupOfflineSupport;
81
89
  private handleSubdocs;
82
- private getUniqueUpdateId;
83
90
  private updateDoc;
84
91
  private fetchDoc;
85
92
  private createSubdocHandler;
@@ -95,6 +102,8 @@ declare class LiveblocksYjsProvider extends Observable<unknown> implements IYjsP
95
102
  getYDoc(): Doc;
96
103
  disconnect(): void;
97
104
  connect(): void;
105
+ get subdocHandlers(): Map<string, yDocHandler>;
106
+ set subdocHandlers(value: Map<string, yDocHandler>);
98
107
  }
99
108
 
100
109
  /**
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { OpaqueRoom, IYjsProvider, YjsSyncStatus } from '@liveblocks/core';
1
+ import { OpaqueRoom, YjsSyncStatus, IYjsProvider } from '@liveblocks/core';
2
2
  import { Observable } from 'lib0/observable';
3
3
  import * as Y from 'yjs';
4
4
  import { PermanentUserData, Doc } from 'yjs';
@@ -38,6 +38,11 @@ declare class yDocHandler extends Observable<unknown> {
38
38
  private updateRoomDoc;
39
39
  private fetchRoomDoc;
40
40
  private useV2Encoding;
41
+ private localSnapshotHashΣ;
42
+ private remoteSnapshotHashΣ;
43
+ private debounceTimer;
44
+ private static readonly DEBOUNCE_INTERVAL_MS;
45
+ private isLocalAndRemoteSnapshotEqualΣ;
41
46
  constructor({ doc, isRoot, updateDoc, fetchDoc, useV2Encoding, }: {
42
47
  doc: Y.Doc;
43
48
  isRoot: boolean;
@@ -45,16 +50,19 @@ declare class yDocHandler extends Observable<unknown> {
45
50
  fetchDoc: (vector: string, guid?: string) => void;
46
51
  useV2Encoding: boolean;
47
52
  });
48
- handleServerUpdate: ({ update, stateVector, readOnly, v2, }: {
53
+ handleServerUpdate: ({ update, stateVector, readOnly, v2, remoteSnapshotHash, }: {
49
54
  update: Uint8Array;
50
55
  stateVector: string | null;
51
56
  readOnly: boolean;
52
57
  v2?: boolean;
58
+ remoteSnapshotHash: string;
53
59
  }) => void;
54
60
  syncDoc: () => void;
55
61
  get synced(): boolean;
56
62
  set synced(state: boolean);
63
+ private debounced_updateLocalSnapshot;
57
64
  private updateHandler;
65
+ experimental_getSyncStatus(): YjsSyncStatus;
58
66
  destroy(): void;
59
67
  }
60
68
 
@@ -73,13 +81,12 @@ declare class LiveblocksYjsProvider extends Observable<unknown> implements IYjsP
73
81
  private readonly unsubscribers;
74
82
  readonly awareness: Awareness;
75
83
  readonly rootDocHandler: yDocHandler;
76
- readonly subdocHandlers: Map<string, yDocHandler>;
84
+ private readonly subdocHandlersΣ;
85
+ private readonly syncStatusΣ;
77
86
  readonly permanentUserData?: PermanentUserData;
78
- private pending;
79
87
  constructor(room: OpaqueRoom, doc: Doc, options?: ProviderOptions);
80
88
  private setupOfflineSupport;
81
89
  private handleSubdocs;
82
- private getUniqueUpdateId;
83
90
  private updateDoc;
84
91
  private fetchDoc;
85
92
  private createSubdocHandler;
@@ -95,6 +102,8 @@ declare class LiveblocksYjsProvider extends Observable<unknown> implements IYjsP
95
102
  getYDoc(): Doc;
96
103
  disconnect(): void;
97
104
  connect(): void;
105
+ get subdocHandlers(): Map<string, yDocHandler>;
106
+ set subdocHandlers(value: Map<string, yDocHandler>);
98
107
  }
99
108
 
100
109
  /**
package/dist/index.js CHANGED
@@ -3,11 +3,14 @@ import { detectDupes } from "@liveblocks/core";
3
3
 
4
4
  // src/version.ts
5
5
  var PKG_NAME = "@liveblocks/yjs";
6
- var PKG_VERSION = "3.2.0";
6
+ var PKG_VERSION = "3.3.0";
7
7
  var PKG_FORMAT = "esm";
8
8
 
9
9
  // src/provider.ts
10
- import { ClientMsgCode, kInternal } from "@liveblocks/core";
10
+ import {
11
+ DerivedSignal as DerivedSignal2
12
+ } from "@liveblocks/core";
13
+ import { ClientMsgCode, kInternal, MutableSignal } from "@liveblocks/core";
11
14
  import { Base64 as Base642 } from "js-base64";
12
15
 
13
16
  // ../../node_modules/lib0/map.js
@@ -82,7 +85,7 @@ var Observable = class {
82
85
 
83
86
  // src/provider.ts
84
87
  import { IndexeddbPersistence as IndexeddbPersistence2 } from "y-indexeddb";
85
- import { parseUpdateMeta, PermanentUserData } from "yjs";
88
+ import { PermanentUserData } from "yjs";
86
89
 
87
90
  // src/awareness.ts
88
91
  var Y_PRESENCE_KEY = "__yjs";
@@ -215,16 +218,26 @@ var Awareness = class extends Observable {
215
218
  };
216
219
 
217
220
  // src/doc.ts
221
+ import {
222
+ DerivedSignal,
223
+ Signal
224
+ } from "@liveblocks/core";
225
+ import { sha256 } from "@noble/hashes/sha2";
218
226
  import { Base64 } from "js-base64";
219
227
  import { IndexeddbPersistence } from "y-indexeddb";
220
228
  import * as Y from "yjs";
221
- var yDocHandler = class extends Observable {
229
+ var yDocHandler = class _yDocHandler extends Observable {
222
230
  unsubscribers = [];
223
231
  _synced = false;
224
232
  doc;
225
233
  updateRoomDoc;
226
234
  fetchRoomDoc;
227
235
  useV2Encoding;
236
+ localSnapshotHash\u03A3;
237
+ remoteSnapshotHash\u03A3;
238
+ debounceTimer = null;
239
+ static DEBOUNCE_INTERVAL_MS = 200;
240
+ isLocalAndRemoteSnapshotEqual\u03A3;
228
241
  constructor({
229
242
  doc,
230
243
  isRoot,
@@ -243,12 +256,27 @@ var yDocHandler = class extends Observable {
243
256
  fetchDoc(vector, isRoot ? void 0 : this.doc.guid);
244
257
  };
245
258
  this.syncDoc();
259
+ const encodedSnapshot = this.useV2Encoding ? Y.encodeSnapshotV2(Y.snapshot(this.doc)) : Y.encodeSnapshot(Y.snapshot(this.doc));
260
+ this.localSnapshotHash\u03A3 = new Signal(
261
+ Base64.fromUint8Array(sha256(encodedSnapshot))
262
+ );
263
+ this.remoteSnapshotHash\u03A3 = new Signal(null);
264
+ this.isLocalAndRemoteSnapshotEqual\u03A3 = DerivedSignal.from(() => {
265
+ const remoteSnapshotHash = this.remoteSnapshotHash\u03A3.get();
266
+ if (remoteSnapshotHash === null) return false;
267
+ const localSnapshotHash = this.localSnapshotHash\u03A3.get();
268
+ if (localSnapshotHash !== remoteSnapshotHash) {
269
+ return false;
270
+ }
271
+ return true;
272
+ });
246
273
  }
247
274
  handleServerUpdate = ({
248
275
  update,
249
276
  stateVector,
250
277
  readOnly,
251
- v2
278
+ v2,
279
+ remoteSnapshotHash
252
280
  }) => {
253
281
  const applyUpdate2 = v2 ? Y.applyUpdateV2 : Y.applyUpdate;
254
282
  applyUpdate2(this.doc, update, "backend");
@@ -267,6 +295,7 @@ var yDocHandler = class extends Observable {
267
295
  }
268
296
  this.synced = true;
269
297
  }
298
+ this.remoteSnapshotHash\u03A3.set(remoteSnapshotHash);
270
299
  };
271
300
  syncDoc = () => {
272
301
  this.synced = false;
@@ -284,13 +313,35 @@ var yDocHandler = class extends Observable {
284
313
  this.emit("sync", [state]);
285
314
  }
286
315
  }
316
+ debounced_updateLocalSnapshot() {
317
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
318
+ this.debounceTimer = setTimeout(() => {
319
+ const encodedSnapshot = this.useV2Encoding ? Y.encodeSnapshotV2(Y.snapshot(this.doc)) : Y.encodeSnapshot(Y.snapshot(this.doc));
320
+ this.localSnapshotHash\u03A3.set(
321
+ Base64.fromUint8Array(sha256(encodedSnapshot))
322
+ );
323
+ this.debounceTimer = null;
324
+ }, _yDocHandler.DEBOUNCE_INTERVAL_MS);
325
+ }
287
326
  updateHandler = (update, origin) => {
327
+ this.debounced_updateLocalSnapshot();
288
328
  const isFromLocal = origin instanceof IndexeddbPersistence;
289
329
  if (origin !== "backend" && !isFromLocal) {
290
330
  this.updateRoomDoc(update);
291
331
  }
292
332
  };
333
+ experimental_getSyncStatus() {
334
+ const remoteSnapshotHash = this.remoteSnapshotHash\u03A3.get();
335
+ if (remoteSnapshotHash === null) {
336
+ return "loading";
337
+ }
338
+ if (!this.isLocalAndRemoteSnapshotEqual\u03A3.get()) {
339
+ return "synchronizing";
340
+ }
341
+ return "synchronized";
342
+ }
293
343
  destroy() {
344
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
294
345
  this.doc.off("update", this.updateHandler);
295
346
  this.unsubscribers.forEach((unsub) => unsub());
296
347
  this._observers = /* @__PURE__ */ new Map();
@@ -308,9 +359,9 @@ var LiveblocksYjsProvider = class extends Observable {
308
359
  unsubscribers = [];
309
360
  awareness;
310
361
  rootDocHandler;
311
- subdocHandlers = /* @__PURE__ */ new Map();
362
+ subdocHandlers\u03A3 = new MutableSignal(/* @__PURE__ */ new Map());
363
+ syncStatus\u03A3;
312
364
  permanentUserData;
313
- pending = [];
314
365
  constructor(room, doc, options = {}) {
315
366
  super();
316
367
  this.rootDoc = doc;
@@ -335,7 +386,6 @@ var LiveblocksYjsProvider = class extends Observable {
335
386
  } else {
336
387
  this.rootDocHandler.synced = false;
337
388
  }
338
- this.emit("status", [this.getStatus()]);
339
389
  })
340
390
  );
341
391
  this.unsubscribers.push(
@@ -344,32 +394,32 @@ var LiveblocksYjsProvider = class extends Observable {
344
394
  if (type === ClientMsgCode.UPDATE_YDOC) {
345
395
  return;
346
396
  }
347
- const { stateVector, update: updateStr, guid, v2 } = message;
397
+ const {
398
+ stateVector,
399
+ update: updateStr,
400
+ guid,
401
+ v2,
402
+ remoteSnapshotHash
403
+ } = message;
348
404
  const canWrite = this.room.getSelf()?.canWrite ?? true;
349
405
  const update = Base642.toUint8Array(updateStr);
350
- const updateId = this.getUniqueUpdateId(update);
351
- this.pending = this.pending.filter((pendingUpdate) => {
352
- if (pendingUpdate === updateId) {
353
- return false;
354
- }
355
- return true;
356
- });
357
406
  if (guid !== void 0) {
358
- this.subdocHandlers.get(guid)?.handleServerUpdate({
407
+ this.subdocHandlers\u03A3.get().get(guid)?.handleServerUpdate({
359
408
  update,
360
409
  stateVector,
361
410
  readOnly: !canWrite,
362
- v2
411
+ v2,
412
+ remoteSnapshotHash
363
413
  });
364
414
  } else {
365
415
  this.rootDocHandler.handleServerUpdate({
366
416
  update,
367
417
  stateVector,
368
418
  readOnly: !canWrite,
369
- v2
419
+ v2,
420
+ remoteSnapshotHash
370
421
  });
371
422
  }
372
- this.emit("status", [this.getStatus()]);
373
423
  })
374
424
  );
375
425
  if (options.offlineSupport_experimental) {
@@ -377,15 +427,33 @@ var LiveblocksYjsProvider = class extends Observable {
377
427
  }
378
428
  this.rootDocHandler.on("synced", () => {
379
429
  const state = this.rootDocHandler.synced;
380
- for (const [_, handler] of this.subdocHandlers) {
430
+ for (const [_, handler] of this.subdocHandlers\u03A3.get()) {
381
431
  handler.syncDoc();
382
432
  }
383
433
  this.emit("synced", [state]);
384
434
  this.emit("sync", [state]);
385
- this.emit("status", [this.getStatus()]);
386
435
  });
387
436
  this.rootDoc.on("subdocs", this.handleSubdocs);
388
437
  this.syncDoc();
438
+ this.syncStatus\u03A3 = DerivedSignal2.from(() => {
439
+ const rootDocumentStatus = this.rootDocHandler.experimental_getSyncStatus();
440
+ if (rootDocumentStatus === "loading" || rootDocumentStatus === "synchronizing") {
441
+ return rootDocumentStatus;
442
+ }
443
+ const subdocumentStatuses = Array.from(
444
+ this.subdocHandlers\u03A3.get().values()
445
+ ).map((handler) => handler.experimental_getSyncStatus());
446
+ if (subdocumentStatuses.some((state) => state !== "synchronized")) {
447
+ return "synchronizing";
448
+ }
449
+ return "synchronized";
450
+ });
451
+ this.emit("status", [this.getStatus()]);
452
+ this.unsubscribers.push(
453
+ this.syncStatus\u03A3.subscribe(() => {
454
+ this.emit("status", [this.getStatus()]);
455
+ })
456
+ );
389
457
  }
390
458
  setupOfflineSupport = () => {
391
459
  this.indexeddbProvider = new IndexeddbPersistence2(
@@ -406,43 +474,38 @@ var LiveblocksYjsProvider = class extends Observable {
406
474
  added
407
475
  }) => {
408
476
  loaded.forEach(this.createSubdocHandler);
477
+ const subdocHandlers = this.subdocHandlers\u03A3.get();
409
478
  if (this.options.autoloadSubdocs) {
410
479
  for (const subdoc of added) {
411
- if (!this.subdocHandlers.has(subdoc.guid)) {
480
+ if (!subdocHandlers.has(subdoc.guid)) {
412
481
  subdoc.load();
413
482
  }
414
483
  }
415
484
  }
416
485
  for (const subdoc of removed) {
417
- if (this.subdocHandlers.has(subdoc.guid)) {
418
- this.subdocHandlers.get(subdoc.guid)?.destroy();
419
- this.subdocHandlers.delete(subdoc.guid);
486
+ if (subdocHandlers.has(subdoc.guid)) {
487
+ subdocHandlers.get(subdoc.guid)?.destroy();
488
+ subdocHandlers.delete(subdoc.guid);
420
489
  }
421
490
  }
422
491
  };
423
- getUniqueUpdateId = (update) => {
424
- const clock = parseUpdateMeta(update).to.get(this.rootDoc.clientID) ?? "-1";
425
- return this.rootDoc.clientID + ":" + clock;
426
- };
427
492
  updateDoc = (update, guid) => {
428
493
  const canWrite = this.room.getSelf()?.canWrite ?? true;
429
494
  if (canWrite && !this.isPaused) {
430
- const updateId = this.getUniqueUpdateId(update);
431
- this.pending.push(updateId);
432
495
  this.room.updateYDoc(
433
496
  Base642.fromUint8Array(update),
434
497
  guid,
435
498
  this.useV2Encoding
436
499
  );
437
- this.emit("status", [this.getStatus()]);
438
500
  }
439
501
  };
440
502
  fetchDoc = (vector, guid) => {
441
503
  this.room.fetchYDoc(vector, guid, this.useV2Encoding);
442
504
  };
443
505
  createSubdocHandler = (subdoc) => {
444
- if (this.subdocHandlers.has(subdoc.guid)) {
445
- this.subdocHandlers.get(subdoc.guid)?.syncDoc();
506
+ const subdocHandlers = this.subdocHandlers\u03A3.get();
507
+ if (subdocHandlers.has(subdoc.guid)) {
508
+ subdocHandlers.get(subdoc.guid)?.syncDoc();
446
509
  return;
447
510
  }
448
511
  const handler = new yDocHandler({
@@ -452,7 +515,7 @@ var LiveblocksYjsProvider = class extends Observable {
452
515
  fetchDoc: this.fetchDoc,
453
516
  useV2Encoding: this.options.useV2Encoding_experimental ?? false
454
517
  });
455
- this.subdocHandlers.set(subdoc.guid, handler);
518
+ subdocHandlers.set(subdoc.guid, handler);
456
519
  };
457
520
  // attempt to load a subdoc of a given guid
458
521
  loadSubdoc = (guid) => {
@@ -466,7 +529,7 @@ var LiveblocksYjsProvider = class extends Observable {
466
529
  };
467
530
  syncDoc = () => {
468
531
  this.rootDocHandler.syncDoc();
469
- for (const [_, handler] of this.subdocHandlers) {
532
+ for (const [_, handler] of this.subdocHandlers\u03A3.get()) {
470
533
  handler.syncDoc();
471
534
  }
472
535
  };
@@ -490,20 +553,17 @@ var LiveblocksYjsProvider = class extends Observable {
490
553
  this.rootDocHandler.syncDoc();
491
554
  }
492
555
  getStatus() {
493
- if (!this.synced) {
494
- return "loading";
495
- }
496
- return this.pending.length === 0 ? "synchronized" : "synchronizing";
556
+ return this.syncStatus\u03A3.get();
497
557
  }
498
558
  destroy() {
499
559
  this.unsubscribers.forEach((unsub) => unsub());
500
560
  this.awareness.destroy();
501
561
  this.rootDocHandler.destroy();
502
562
  this._observers = /* @__PURE__ */ new Map();
503
- for (const [_, handler] of this.subdocHandlers) {
563
+ for (const [_, handler] of this.subdocHandlers\u03A3.get()) {
504
564
  handler.destroy();
505
565
  }
506
- this.subdocHandlers.clear();
566
+ this.subdocHandlers\u03A3.get().clear();
507
567
  super.destroy();
508
568
  }
509
569
  async clearOfflineData() {
@@ -518,6 +578,17 @@ var LiveblocksYjsProvider = class extends Observable {
518
578
  }
519
579
  connect() {
520
580
  }
581
+ get subdocHandlers() {
582
+ return this.subdocHandlers\u03A3.get();
583
+ }
584
+ set subdocHandlers(value) {
585
+ this.subdocHandlers\u03A3.mutate((map) => {
586
+ map.clear();
587
+ for (const [key, handler] of value) {
588
+ map.set(key, handler);
589
+ }
590
+ });
591
+ }
521
592
  };
522
593
 
523
594
  // src/providerContext.ts