cojson 0.14.25 → 0.14.27

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/coValueCore/coValueCore.d.ts +3 -3
  4. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  5. package/dist/coValueCore/coValueCore.js +25 -30
  6. package/dist/coValueCore/coValueCore.js.map +1 -1
  7. package/dist/coValueCore/utils.d.ts +4 -0
  8. package/dist/coValueCore/utils.d.ts.map +1 -0
  9. package/dist/coValueCore/utils.js +48 -0
  10. package/dist/coValueCore/utils.js.map +1 -0
  11. package/dist/exports.d.ts +2 -0
  12. package/dist/exports.d.ts.map +1 -1
  13. package/dist/exports.js +2 -0
  14. package/dist/exports.js.map +1 -1
  15. package/dist/sync.d.ts.map +1 -1
  16. package/dist/sync.js +48 -39
  17. package/dist/sync.js.map +1 -1
  18. package/dist/tests/coValueCoreLoadingState.test.js +2 -2
  19. package/dist/tests/sync.load.test.js +256 -2
  20. package/dist/tests/sync.load.test.js.map +1 -1
  21. package/dist/tests/sync.mesh.test.js +2 -2
  22. package/dist/tests/sync.mesh.test.js.map +1 -1
  23. package/dist/tests/sync.peerReconciliation.test.js +6 -7
  24. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  25. package/dist/tests/sync.test.js +1 -1
  26. package/dist/tests/sync.test.js.map +1 -1
  27. package/dist/tests/testUtils.d.ts +4 -1
  28. package/dist/tests/testUtils.d.ts.map +1 -1
  29. package/dist/tests/testUtils.js +6 -2
  30. package/dist/tests/testUtils.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/coValueCore/coValueCore.ts +33 -41
  33. package/src/coValueCore/utils.ts +64 -0
  34. package/src/exports.ts +2 -0
  35. package/src/sync.ts +63 -40
  36. package/src/tests/coValueCoreLoadingState.test.ts +2 -2
  37. package/src/tests/sync.load.test.ts +338 -2
  38. package/src/tests/sync.mesh.test.ts +2 -2
  39. package/src/tests/sync.peerReconciliation.test.ts +8 -8
  40. package/src/tests/sync.test.ts +1 -1
  41. package/src/tests/testUtils.ts +11 -1
@@ -195,7 +195,7 @@ describe("CoValueCore loading state", () => {
195
195
  role: "storage",
196
196
  },
197
197
  async () => {
198
- state.markAvailable({} as CoValueHeader, "peer1");
198
+ state.provideHeader({} as CoValueHeader, "peer1");
199
199
  },
200
200
  );
201
201
  const peer2 = createMockPeerState(
@@ -248,7 +248,7 @@ describe("CoValueCore loading state", () => {
248
248
  role: "server",
249
249
  },
250
250
  async () => {
251
- state.markAvailable({} as CoValueHeader, "peer2");
251
+ state.provideHeader({} as CoValueHeader, "peer2");
252
252
  },
253
253
  );
254
254
 
@@ -1,16 +1,22 @@
1
- import { beforeEach, describe, expect, test } from "vitest";
1
+ import { beforeEach, describe, expect, onTestFinished, test, vi } from "vitest";
2
2
 
3
3
  import { expectMap } from "../coValue";
4
- import { toSimplifiedMessages } from "./messagesTestUtils";
4
+ import { CO_VALUE_LOADING_CONFIG } from "../coValueCore/coValueCore";
5
+ import { RawCoMap } from "../exports";
5
6
  import {
6
7
  SyncMessagesLog,
8
+ blockMessageTypeOnOutgoingPeer,
7
9
  loadCoValueOrFail,
10
+ setupTestAccount,
8
11
  setupTestNode,
9
12
  waitFor,
10
13
  } from "./testUtils";
11
14
 
12
15
  let jazzCloud = setupTestNode({ isSyncServer: true });
13
16
 
17
+ // Set a short timeout to make the tests on unavailable complete faster
18
+ CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 100;
19
+
14
20
  beforeEach(async () => {
15
21
  SyncMessagesLog.clear();
16
22
  jazzCloud = setupTestNode({ isSyncServer: true });
@@ -406,4 +412,334 @@ describe("loading coValues from server", () => {
406
412
  ]
407
413
  `);
408
414
  });
415
+
416
+ test("coValue with a delayed group loading", async () => {
417
+ const client = setupTestNode();
418
+
419
+ const { peerOnServer } = client.connectToSyncServer();
420
+
421
+ const group = jazzCloud.node.createGroup();
422
+ const parentGroup = jazzCloud.node.createGroup();
423
+ parentGroup.addMember("everyone", "reader");
424
+
425
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
426
+ id: group.id,
427
+ once: true,
428
+ });
429
+
430
+ group.extend(parentGroup);
431
+
432
+ const map = group.createMap();
433
+ map.set("hello", "world");
434
+
435
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
436
+ expect(mapOnClient.get("hello")).toEqual("world");
437
+
438
+ expect(
439
+ SyncMessagesLog.getMessages({
440
+ ParentGroup: parentGroup.core,
441
+ Group: group.core,
442
+ Map: map.core,
443
+ }),
444
+ ).toMatchInlineSnapshot(`
445
+ [
446
+ "client -> server | LOAD Map sessions: empty",
447
+ "server -> client | CONTENT ParentGroup header: true new: After: 0 New: 6",
448
+ "client -> server | KNOWN ParentGroup sessions: header/6",
449
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
450
+ "client -> server | LOAD Group sessions: empty",
451
+ "client -> server | KNOWN Map sessions: empty",
452
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
453
+ "client -> server | KNOWN Group sessions: header/5",
454
+ ]
455
+ `);
456
+
457
+ blocker.unblock();
458
+ });
459
+
460
+ test("coValue with a delayed parent group loading", async () => {
461
+ const client = setupTestNode();
462
+
463
+ const { peerOnServer } = client.connectToSyncServer();
464
+
465
+ const group = jazzCloud.node.createGroup();
466
+ const parentGroup = jazzCloud.node.createGroup();
467
+ parentGroup.addMember("everyone", "reader");
468
+
469
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
470
+ id: parentGroup.id,
471
+ once: true,
472
+ });
473
+
474
+ group.extend(parentGroup);
475
+
476
+ const map = group.createMap();
477
+ map.set("hello", "world");
478
+
479
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
480
+ expect(mapOnClient.get("hello")).toEqual("world");
481
+
482
+ expect(
483
+ SyncMessagesLog.getMessages({
484
+ ParentGroup: parentGroup.core,
485
+ Group: group.core,
486
+ Map: map.core,
487
+ }),
488
+ ).toMatchInlineSnapshot(`
489
+ [
490
+ "client -> server | LOAD Map sessions: empty",
491
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
492
+ "client -> server | LOAD ParentGroup sessions: empty",
493
+ "client -> server | KNOWN Group sessions: empty",
494
+ "server -> client | CONTENT ParentGroup header: true new: After: 0 New: 6",
495
+ "client -> server | KNOWN ParentGroup sessions: header/6",
496
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
497
+ "client -> server | KNOWN Map sessions: header/1",
498
+ ]
499
+ `);
500
+
501
+ blocker.unblock();
502
+ });
503
+
504
+ test("coValue with a delayed account loading (block once)", async () => {
505
+ const client = setupTestNode();
506
+ const syncServer = await setupTestAccount({ isSyncServer: true });
507
+
508
+ const { peerOnServer } = client.connectToSyncServer({
509
+ syncServer: syncServer.node,
510
+ });
511
+
512
+ const group = syncServer.node.createGroup();
513
+ group.addMember("everyone", "writer");
514
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
515
+ id: syncServer.accountID,
516
+ once: true,
517
+ });
518
+
519
+ const account = syncServer.node.expectCurrentAccount(syncServer.accountID);
520
+
521
+ const map = group.createMap();
522
+ map.set("hello", "world");
523
+
524
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
525
+ expect(mapOnClient.get("hello")).toEqual("world");
526
+
527
+ // ParentGroup sent twice, once because the server pushed it and another time because the client requested the missing dependency
528
+ expect(
529
+ SyncMessagesLog.getMessages({
530
+ Account: account.core,
531
+ Group: group.core,
532
+ Map: map.core,
533
+ }),
534
+ ).toMatchInlineSnapshot(`
535
+ [
536
+ "client -> server | LOAD Map sessions: empty",
537
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
538
+ "client -> server | LOAD Account sessions: empty",
539
+ "client -> server | KNOWN Group sessions: empty",
540
+ "server -> client | CONTENT Account header: true new: After: 0 New: 4",
541
+ "client -> server | KNOWN Account sessions: header/4",
542
+ "client -> server | KNOWN Group sessions: header/5",
543
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
544
+ "client -> server | KNOWN Map sessions: header/1",
545
+ ]
546
+ `);
547
+
548
+ blocker.unblock();
549
+ });
550
+
551
+ test("coValue with a delayed account loading related to an update (block once)", async () => {
552
+ const client = setupTestNode();
553
+ const user = await setupTestAccount({
554
+ connected: true,
555
+ });
556
+
557
+ const { peerOnServer } = client.connectToSyncServer();
558
+
559
+ const account = user.node.expectCurrentAccount(user.accountID);
560
+ await user.node.syncManager.waitForAllCoValuesSync();
561
+
562
+ SyncMessagesLog.clear();
563
+
564
+ const group = jazzCloud.node.createGroup();
565
+ group.addMember("everyone", "writer");
566
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
567
+ id: user.accountID,
568
+ once: true,
569
+ });
570
+
571
+ const map = group.createMap();
572
+ map.set("hello", "world");
573
+
574
+ const mapOnUser = await loadCoValueOrFail(user.node, map.id);
575
+ mapOnUser.set("user", true);
576
+
577
+ await mapOnUser.core.waitForSync();
578
+
579
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
580
+ expect(mapOnClient.get("user")).toEqual(true);
581
+
582
+ // ParentGroup sent twice, once because the server pushed it and another time because the client requested the missing dependency
583
+ expect(
584
+ SyncMessagesLog.getMessages({
585
+ Account: account.core,
586
+ Group: group.core,
587
+ Map: map.core,
588
+ }),
589
+ ).toMatchInlineSnapshot(`
590
+ [
591
+ "client -> server | LOAD Map sessions: empty",
592
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
593
+ "client -> server | KNOWN Group sessions: header/5",
594
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
595
+ "client -> server | KNOWN Map sessions: header/1",
596
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
597
+ "server -> client | KNOWN Map sessions: header/2",
598
+ "client -> server | LOAD Map sessions: empty",
599
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
600
+ "client -> server | KNOWN Group sessions: header/5",
601
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
602
+ "client -> server | LOAD Account sessions: empty",
603
+ "client -> server | KNOWN Map sessions: empty",
604
+ "server -> client | CONTENT Account header: true new: After: 0 New: 4",
605
+ "client -> server | KNOWN Account sessions: header/4",
606
+ "client -> server | KNOWN Map sessions: header/2",
607
+ ]
608
+ `);
609
+
610
+ blocker.unblock();
611
+ });
612
+
613
+ test("coValue with a delayed account loading (block for 100ms)", async () => {
614
+ const client = setupTestNode();
615
+ const syncServer = await setupTestAccount({ isSyncServer: true });
616
+
617
+ const { peerOnServer } = client.connectToSyncServer({
618
+ syncServer: syncServer.node,
619
+ });
620
+
621
+ const group = syncServer.node.createGroup();
622
+ group.addMember("everyone", "writer");
623
+
624
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
625
+ id: syncServer.accountID,
626
+ });
627
+
628
+ const account = syncServer.node.expectCurrentAccount(syncServer.accountID);
629
+
630
+ const map = group.createMap();
631
+ map.set("hello", "world");
632
+
633
+ const core = client.node.getCoValue(map.id);
634
+ const promise = client.node.loadCoValueCore(map.id);
635
+
636
+ const spy = vi.fn();
637
+
638
+ core.subscribe(spy);
639
+ spy.mockClear(); // Reset the first call
640
+
641
+ await new Promise((resolve) => setTimeout(resolve, 100));
642
+
643
+ blocker.sendBlockedMessages();
644
+ blocker.unblock();
645
+
646
+ await promise;
647
+
648
+ expect(spy).toHaveBeenCalled();
649
+ expect(core.isAvailable()).toBe(true);
650
+
651
+ const mapOnClient = expectMap(core.getCurrentContent());
652
+ expect(mapOnClient.get("hello")).toEqual("world");
653
+
654
+ // Account sent twice, once because the server pushed it and another time because the client requested the missing dependency
655
+ expect(
656
+ SyncMessagesLog.getMessages({
657
+ Account: account.core,
658
+ Group: group.core,
659
+ Map: map.core,
660
+ }),
661
+ ).toMatchInlineSnapshot(`
662
+ [
663
+ "client -> server | LOAD Map sessions: empty",
664
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
665
+ "client -> server | LOAD Account sessions: empty",
666
+ "client -> server | KNOWN Group sessions: empty",
667
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
668
+ "client -> server | KNOWN Map sessions: empty",
669
+ "server -> client | CONTENT Account header: true new: After: 0 New: 4",
670
+ "client -> server | KNOWN Account sessions: header/4",
671
+ "server -> client | CONTENT Account header: true new: After: 0 New: 4",
672
+ "client -> server | KNOWN Group sessions: header/5",
673
+ ]
674
+ `);
675
+ });
676
+
677
+ test("coValue with a delayed account loading with no group (block for 100ms)", async () => {
678
+ const client = setupTestNode();
679
+ const syncServer = await setupTestAccount({ isSyncServer: true });
680
+
681
+ const { peerOnServer } = client.connectToSyncServer({
682
+ syncServer: syncServer.node,
683
+ });
684
+
685
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
686
+ id: syncServer.accountID,
687
+ });
688
+
689
+ const account = syncServer.node.expectCurrentAccount(syncServer.accountID);
690
+
691
+ const map = syncServer.node
692
+ .createCoValue({
693
+ type: "comap",
694
+ ruleset: {
695
+ type: "ownedByGroup",
696
+ group: syncServer.accountID,
697
+ },
698
+ meta: null,
699
+ ...syncServer.node.crypto.createdNowUnique(),
700
+ })
701
+ .getCurrentContent() as RawCoMap;
702
+
703
+ map.set("hello", "world", "trusting");
704
+
705
+ const core = client.node.getCoValue(map.id);
706
+ const promise = client.node.loadCoValueCore(map.id);
707
+
708
+ const spy = vi.fn();
709
+
710
+ core.subscribe(spy);
711
+ spy.mockClear(); // Reset the first call
712
+
713
+ await new Promise((resolve) => setTimeout(resolve, 100));
714
+
715
+ blocker.sendBlockedMessages();
716
+ blocker.unblock();
717
+
718
+ await promise;
719
+
720
+ expect(spy).toHaveBeenCalled();
721
+ expect(core.isAvailable()).toBe(true);
722
+
723
+ const mapOnClient = expectMap(core.getCurrentContent());
724
+ expect(mapOnClient.get("hello")).toEqual("world");
725
+
726
+ // Account sent twice, once because the server pushed it and another time because the client requested the missing dependency
727
+ expect(
728
+ SyncMessagesLog.getMessages({
729
+ Account: account.core,
730
+ Map: map.core,
731
+ }),
732
+ ).toMatchInlineSnapshot(`
733
+ [
734
+ "client -> server | LOAD Map sessions: empty",
735
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
736
+ "client -> server | LOAD Account sessions: empty",
737
+ "client -> server | KNOWN Map sessions: empty",
738
+ "server -> client | CONTENT Account header: true new: After: 0 New: 4",
739
+ "client -> server | KNOWN Account sessions: header/4",
740
+ "server -> client | CONTENT Account header: true new: After: 0 New: 4",
741
+ "client -> server | KNOWN Map sessions: header/1",
742
+ ]
743
+ `);
744
+ });
409
745
  });
@@ -380,7 +380,7 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
380
380
  map.set("hello", "updated", "trusting");
381
381
 
382
382
  // Block the content message from the core peer to simulate the delay on response
383
- blockMessageTypeOnOutgoingPeer(peerOnServer, "content");
383
+ blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
384
384
 
385
385
  const mapOnClient = await loadCoValueOrFail(client.node, map.id);
386
386
 
@@ -431,7 +431,7 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
431
431
  },
432
432
  });
433
433
 
434
- blockMessageTypeOnOutgoingPeer(peerToCoreServer, "load");
434
+ blockMessageTypeOnOutgoingPeer(peerToCoreServer, "load", {});
435
435
 
436
436
  client.node.syncManager.addPeer(peer1);
437
437
  anotherServer.node.syncManager.addPeer(peer2);
@@ -1,9 +1,10 @@
1
1
  import { assert, beforeEach, describe, expect, test } from "vitest";
2
2
  import { expectMap } from "../coValue";
3
+ import { CO_VALUE_LOADING_CONFIG } from "../coValueCore/coValueCore";
3
4
  import { WasmCrypto } from "../crypto/WasmCrypto";
5
+ import { RawCoMap } from "../exports";
4
6
  import {
5
7
  SyncMessagesLog,
6
- loadCoValueOrFail,
7
8
  setupTestAccount,
8
9
  setupTestNode,
9
10
  waitFor,
@@ -195,10 +196,8 @@ describe("peer reconciliation", () => {
195
196
  "client -> server | CONTENT Map header: true new: After: 0 New: 2",
196
197
  "server -> client | LOAD Group sessions: empty",
197
198
  "client -> server | CONTENT Group header: true new: After: 0 New: 3",
198
- "server -> client | KNOWN CORRECTION Map sessions: empty",
199
- "client -> server | CONTENT Map header: true new: After: 0 New: 2",
199
+ "server -> client | KNOWN Map sessions: empty",
200
200
  "server -> client | KNOWN Group sessions: header/3",
201
- "server -> client | KNOWN Map sessions: header/2",
202
201
  "client -> server | LOAD Group sessions: header/3",
203
202
  "server -> client | KNOWN Group sessions: header/3",
204
203
  "client -> server | LOAD Map sessions: header/2",
@@ -232,7 +231,9 @@ describe("peer reconciliation", () => {
232
231
  await waitFor(() => {
233
232
  const mapOnSyncServer = jazzCloud.node.getCoValue(map.id);
234
233
 
235
- expect(mapOnSyncServer.loadingState).toBe("available");
234
+ expect(mapOnSyncServer.isAvailable()).toBe(true);
235
+ const content = mapOnSyncServer.getCurrentContent() as RawCoMap;
236
+ expect(content.get("hello")).toBe("updated");
236
237
  });
237
238
 
238
239
  expect(
@@ -263,11 +264,10 @@ describe("peer reconciliation", () => {
263
264
  "client -> server | CONTENT Account header: true new: After: 0 New: 4",
264
265
  "server -> client | LOAD Group sessions: empty",
265
266
  "client -> server | CONTENT Group header: true new: After: 0 New: 3",
266
- "server -> client | KNOWN CORRECTION Map sessions: empty",
267
- "client -> server | CONTENT Map header: true new: After: 0 New: 2",
267
+ "server -> client | KNOWN Map sessions: empty",
268
268
  "server -> client | KNOWN Account sessions: header/4",
269
+ "server -> client | KNOWN Map sessions: empty",
269
270
  "server -> client | KNOWN Group sessions: header/3",
270
- "server -> client | KNOWN Map sessions: header/2",
271
271
  "client -> server | LOAD Account sessions: header/4",
272
272
  "server -> client | KNOWN Account sessions: header/4",
273
273
  "client -> server | LOAD ProfileGroup sessions: header/5",
@@ -601,7 +601,7 @@ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
601
601
  // optimisticKnownStates is updated when the content messages are sent,
602
602
  // while knownStates is only updated when we receive the "known" messages
603
603
  // that are acknowledging the receipt of the content messages
604
- const outgoing = blockMessageTypeOnOutgoingPeer(peer, "content");
604
+ const outgoing = blockMessageTypeOnOutgoingPeer(peer, "content", {});
605
605
 
606
606
  map.set("key2", "value2", "trusting");
607
607
 
@@ -287,15 +287,25 @@ export async function loadCoValueOrFail<V extends RawCoValue>(
287
287
  export function blockMessageTypeOnOutgoingPeer(
288
288
  peer: Peer,
289
289
  messageType: SyncMessage["action"],
290
+ opts: {
291
+ id?: string;
292
+ once?: boolean;
293
+ },
290
294
  ) {
291
295
  const push = peer.outgoing.push;
292
296
  const pushSpy = vi.spyOn(peer.outgoing, "push");
293
297
 
294
298
  const blockedMessages: SyncMessage[] = [];
299
+ const blockedIds = new Set<string>();
295
300
 
296
301
  pushSpy.mockImplementation(async (msg) => {
297
- if (msg.action === messageType) {
302
+ if (
303
+ msg.action === messageType &&
304
+ (!opts.id || msg.id === opts.id) &&
305
+ (!opts.once || !blockedIds.has(msg.id))
306
+ ) {
298
307
  blockedMessages.push(msg);
308
+ blockedIds.add(msg.id);
299
309
  return Promise.resolve();
300
310
  }
301
311