cojson 0.16.4 → 0.16.6

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 (150) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/dist/GarbageCollector.d.ts +12 -0
  4. package/dist/GarbageCollector.d.ts.map +1 -0
  5. package/dist/GarbageCollector.js +37 -0
  6. package/dist/GarbageCollector.js.map +1 -0
  7. package/dist/coValue.d.ts +1 -1
  8. package/dist/coValue.d.ts.map +1 -1
  9. package/dist/coValue.js.map +1 -1
  10. package/dist/coValueContentMessage.d.ts +1 -0
  11. package/dist/coValueContentMessage.d.ts.map +1 -1
  12. package/dist/coValueContentMessage.js +11 -3
  13. package/dist/coValueContentMessage.js.map +1 -1
  14. package/dist/coValueCore/coValueCore.d.ts +8 -11
  15. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  16. package/dist/coValueCore/coValueCore.js +30 -122
  17. package/dist/coValueCore/coValueCore.js.map +1 -1
  18. package/dist/coValueCore/utils.d.ts.map +1 -1
  19. package/dist/coValueCore/utils.js.map +1 -1
  20. package/dist/coValueCore/verifiedState.d.ts +1 -0
  21. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coMap.d.ts +3 -3
  24. package/dist/coValues/coPlainText.d.ts +1 -0
  25. package/dist/coValues/coPlainText.d.ts.map +1 -1
  26. package/dist/coValues/coPlainText.js +27 -8
  27. package/dist/coValues/coPlainText.js.map +1 -1
  28. package/dist/coValues/coStream.d.ts +2 -2
  29. package/dist/coValues/group.d.ts +19 -11
  30. package/dist/coValues/group.d.ts.map +1 -1
  31. package/dist/coValues/group.js +221 -59
  32. package/dist/coValues/group.js.map +1 -1
  33. package/dist/config.d.ts +10 -1
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +16 -1
  36. package/dist/config.js.map +1 -1
  37. package/dist/exports.d.ts +11 -4
  38. package/dist/exports.d.ts.map +1 -1
  39. package/dist/exports.js +8 -3
  40. package/dist/exports.js.map +1 -1
  41. package/dist/ids.d.ts +3 -3
  42. package/dist/ids.d.ts.map +1 -1
  43. package/dist/ids.js.map +1 -1
  44. package/dist/localNode.d.ts +8 -5
  45. package/dist/localNode.d.ts.map +1 -1
  46. package/dist/localNode.js +11 -0
  47. package/dist/localNode.js.map +1 -1
  48. package/dist/queue/LocalTransactionsSyncQueue.js +1 -1
  49. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  50. package/dist/queue/StoreQueue.d.ts +7 -1
  51. package/dist/queue/StoreQueue.d.ts.map +1 -1
  52. package/dist/queue/StoreQueue.js +35 -13
  53. package/dist/queue/StoreQueue.js.map +1 -1
  54. package/dist/storage/sqlite/client.d.ts +4 -4
  55. package/dist/storage/sqlite/client.d.ts.map +1 -1
  56. package/dist/storage/sqlite/client.js +13 -4
  57. package/dist/storage/sqlite/client.js.map +1 -1
  58. package/dist/storage/sqliteAsync/client.d.ts +3 -3
  59. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  60. package/dist/storage/sqliteAsync/client.js +12 -3
  61. package/dist/storage/sqliteAsync/client.js.map +1 -1
  62. package/dist/storage/storageAsync.d.ts.map +1 -1
  63. package/dist/storage/storageAsync.js +2 -7
  64. package/dist/storage/storageAsync.js.map +1 -1
  65. package/dist/storage/storageSync.d.ts.map +1 -1
  66. package/dist/storage/storageSync.js +2 -7
  67. package/dist/storage/storageSync.js.map +1 -1
  68. package/dist/storage/types.d.ts +2 -2
  69. package/dist/storage/types.d.ts.map +1 -1
  70. package/dist/sync.d.ts.map +1 -1
  71. package/dist/sync.js +17 -3
  72. package/dist/sync.js.map +1 -1
  73. package/dist/tests/GarbageCollector.test.d.ts +2 -0
  74. package/dist/tests/GarbageCollector.test.d.ts.map +1 -0
  75. package/dist/tests/GarbageCollector.test.js +85 -0
  76. package/dist/tests/GarbageCollector.test.js.map +1 -0
  77. package/dist/tests/coPlainText.test.js +142 -4
  78. package/dist/tests/coPlainText.test.js.map +1 -1
  79. package/dist/tests/coStream.test.js +3 -3
  80. package/dist/tests/coStream.test.js.map +1 -1
  81. package/dist/tests/group.inheritance.test.js +195 -0
  82. package/dist/tests/group.inheritance.test.js.map +1 -1
  83. package/dist/tests/group.removeMember.test.js +152 -1
  84. package/dist/tests/group.removeMember.test.js.map +1 -1
  85. package/dist/tests/group.roleOf.test.js +2 -2
  86. package/dist/tests/group.roleOf.test.js.map +1 -1
  87. package/dist/tests/group.test.js +81 -3
  88. package/dist/tests/group.test.js.map +1 -1
  89. package/dist/tests/sync.garbageCollection.test.d.ts +2 -0
  90. package/dist/tests/sync.garbageCollection.test.d.ts.map +1 -0
  91. package/dist/tests/sync.garbageCollection.test.js +133 -0
  92. package/dist/tests/sync.garbageCollection.test.js.map +1 -0
  93. package/dist/tests/sync.load.test.js +6 -3
  94. package/dist/tests/sync.load.test.js.map +1 -1
  95. package/dist/tests/sync.mesh.test.js +48 -34
  96. package/dist/tests/sync.mesh.test.js.map +1 -1
  97. package/dist/tests/sync.storage.test.js +31 -21
  98. package/dist/tests/sync.storage.test.js.map +1 -1
  99. package/dist/tests/sync.storageAsync.test.js +76 -29
  100. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  101. package/dist/tests/testStorage.d.ts +1 -0
  102. package/dist/tests/testStorage.d.ts.map +1 -1
  103. package/dist/tests/testStorage.js +1 -1
  104. package/dist/tests/testStorage.js.map +1 -1
  105. package/dist/tests/testUtils.d.ts +2 -0
  106. package/dist/tests/testUtils.d.ts.map +1 -1
  107. package/dist/tests/testUtils.js +6 -0
  108. package/dist/tests/testUtils.js.map +1 -1
  109. package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts +2 -2
  110. package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts.map +1 -1
  111. package/dist/typeUtils/expectGroup.d.ts.map +1 -1
  112. package/dist/typeUtils/expectGroup.js +6 -5
  113. package/dist/typeUtils/expectGroup.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/GarbageCollector.ts +48 -0
  116. package/src/coValue.ts +1 -4
  117. package/src/coValueContentMessage.ts +16 -3
  118. package/src/coValueCore/coValueCore.ts +50 -198
  119. package/src/coValueCore/utils.ts +1 -0
  120. package/src/coValueCore/verifiedState.ts +1 -0
  121. package/src/coValues/coPlainText.ts +40 -8
  122. package/src/coValues/group.ts +310 -91
  123. package/src/config.ts +20 -1
  124. package/src/exports.ts +13 -5
  125. package/src/ids.ts +3 -3
  126. package/src/localNode.ts +22 -8
  127. package/src/queue/LocalTransactionsSyncQueue.ts +1 -1
  128. package/src/queue/StoreQueue.ts +45 -12
  129. package/src/storage/sqlite/client.ts +24 -10
  130. package/src/storage/sqliteAsync/client.ts +26 -5
  131. package/src/storage/storageAsync.ts +5 -9
  132. package/src/storage/storageSync.ts +2 -9
  133. package/src/storage/types.ts +7 -4
  134. package/src/sync.ts +19 -3
  135. package/src/tests/GarbageCollector.test.ts +127 -0
  136. package/src/tests/coPlainText.test.ts +176 -4
  137. package/src/tests/coStream.test.ts +7 -3
  138. package/src/tests/group.inheritance.test.ts +279 -0
  139. package/src/tests/group.removeMember.test.ts +244 -1
  140. package/src/tests/group.roleOf.test.ts +2 -2
  141. package/src/tests/group.test.ts +105 -5
  142. package/src/tests/sync.garbageCollection.test.ts +178 -0
  143. package/src/tests/sync.load.test.ts +6 -3
  144. package/src/tests/sync.mesh.test.ts +49 -34
  145. package/src/tests/sync.storage.test.ts +31 -21
  146. package/src/tests/sync.storageAsync.test.ts +81 -29
  147. package/src/tests/testStorage.ts +11 -3
  148. package/src/tests/testUtils.ts +9 -1
  149. package/src/typeUtils/accountOrAgentIDfromSessionID.ts +2 -2
  150. package/src/typeUtils/expectGroup.ts +8 -5
@@ -1,12 +1,15 @@
1
1
  import { beforeEach, describe, expect, test } from "vitest";
2
+ import { setCoValueLoadingRetryDelay } from "../config.js";
2
3
  import {
3
4
  SyncMessagesLog,
4
- TEST_NODE_CONFIG,
5
+ blockMessageTypeOnOutgoingPeer,
5
6
  loadCoValueOrFail,
6
7
  setupTestAccount,
7
8
  setupTestNode,
8
9
  } from "./testUtils.js";
9
10
 
11
+ setCoValueLoadingRetryDelay(10);
12
+
10
13
  let jazzCloud: ReturnType<typeof setupTestNode>;
11
14
 
12
15
  beforeEach(async () => {
@@ -15,6 +18,65 @@ beforeEach(async () => {
15
18
  });
16
19
 
17
20
  describe("Group.removeMember", () => {
21
+ test("revoking a member access should not affect everyone access", async () => {
22
+ const admin = await setupTestAccount({
23
+ connected: true,
24
+ });
25
+
26
+ const alice = await setupTestAccount({
27
+ connected: true,
28
+ });
29
+
30
+ const group = admin.node.createGroup();
31
+ group.addMember("everyone", "writer");
32
+
33
+ const aliceOnAdminNode = await loadCoValueOrFail(
34
+ admin.node,
35
+ alice.accountID,
36
+ );
37
+ group.addMember(aliceOnAdminNode, "writer");
38
+ group.removeMember(aliceOnAdminNode);
39
+
40
+ const groupOnAliceNode = await loadCoValueOrFail(alice.node, group.id);
41
+ expect(groupOnAliceNode.myRole()).toEqual("writer");
42
+
43
+ const map = groupOnAliceNode.createMap();
44
+
45
+ map.set("test", "test");
46
+ expect(map.get("test")).toEqual("test");
47
+ });
48
+
49
+ test("revoking a member access should not affect everyone access when everyone access is gained through a group extension", async () => {
50
+ const admin = await setupTestAccount({
51
+ connected: true,
52
+ });
53
+
54
+ const alice = await setupTestAccount({
55
+ connected: true,
56
+ });
57
+
58
+ const parentGroup = admin.node.createGroup();
59
+ const group = admin.node.createGroup();
60
+ parentGroup.addMember("everyone", "reader");
61
+ group.extend(parentGroup);
62
+
63
+ const aliceOnAdminNode = await loadCoValueOrFail(
64
+ admin.node,
65
+ alice.accountID,
66
+ );
67
+ group.addMember(aliceOnAdminNode, "writer");
68
+ group.removeMember(aliceOnAdminNode);
69
+
70
+ const map = group.createMap();
71
+ map.set("test", "test");
72
+
73
+ const groupOnAliceNode = await loadCoValueOrFail(alice.node, group.id);
74
+ expect(groupOnAliceNode.myRole()).toEqual("reader");
75
+
76
+ const mapOnAliceNode = await loadCoValueOrFail(alice.node, map.id);
77
+ expect(mapOnAliceNode.get("test")).toEqual("test");
78
+ });
79
+
18
80
  test("a reader member should be able to revoke themselves", async () => {
19
81
  const admin = await setupTestAccount({
20
82
  connected: true,
@@ -294,4 +356,185 @@ describe("Group.removeMember", () => {
294
356
  undefined,
295
357
  );
296
358
  });
359
+
360
+ test("removing a member should rotate the readKey on available child groups", async () => {
361
+ const admin = await setupTestAccount({
362
+ connected: true,
363
+ });
364
+
365
+ const alice = await setupTestAccount({
366
+ connected: true,
367
+ });
368
+
369
+ const aliceOnAdminNode = await loadCoValueOrFail(
370
+ admin.node,
371
+ alice.accountID,
372
+ );
373
+
374
+ const group = admin.node.createGroup();
375
+ const childGroup = admin.node.createGroup();
376
+ group.addMember(aliceOnAdminNode, "reader");
377
+
378
+ childGroup.extend(group);
379
+
380
+ group.removeMember(aliceOnAdminNode);
381
+
382
+ const map = childGroup.createMap();
383
+ map.set("test", "Not readable by alice");
384
+
385
+ await map.core.waitForSync();
386
+
387
+ const mapOnAliceNode = await loadCoValueOrFail(alice.node, map.id);
388
+ expect(mapOnAliceNode.get("test")).toBeUndefined();
389
+ });
390
+
391
+ test("removing a member should rotate the readKey on unloaded child groups", async () => {
392
+ const admin = await setupTestAccount({
393
+ connected: true,
394
+ });
395
+
396
+ const bob = await setupTestAccount({
397
+ connected: true,
398
+ });
399
+
400
+ const alice = await setupTestAccount({
401
+ connected: true,
402
+ });
403
+
404
+ const bobOnAdminNode = await loadCoValueOrFail(admin.node, bob.accountID);
405
+
406
+ const aliceOnAdminNode = await loadCoValueOrFail(
407
+ admin.node,
408
+ alice.accountID,
409
+ );
410
+
411
+ const group = admin.node.createGroup();
412
+
413
+ const childGroup = bob.node.createGroup();
414
+ group.addMember(bobOnAdminNode, "reader");
415
+ group.addMember(aliceOnAdminNode, "reader");
416
+
417
+ const groupOnBobNode = await loadCoValueOrFail(bob.node, group.id);
418
+
419
+ childGroup.extend(groupOnBobNode);
420
+
421
+ await childGroup.core.waitForSync();
422
+
423
+ group.removeMember(aliceOnAdminNode);
424
+
425
+ // Rotating the child group keys is async when the child group is not loaded
426
+ await admin.node.getCoValue(childGroup.id).waitForAvailableOrUnavailable();
427
+ await admin.node.syncManager.waitForAllCoValuesSync();
428
+
429
+ const map = childGroup.createMap();
430
+ map.set("test", "Not readable by alice");
431
+
432
+ await map.core.waitForSync();
433
+
434
+ const mapOnAliceNode = await loadCoValueOrFail(alice.node, map.id);
435
+ expect(mapOnAliceNode.get("test")).toBeUndefined();
436
+ });
437
+
438
+ test("removing a member should work even if there are partially available child groups", async () => {
439
+ const admin = await setupTestAccount({
440
+ connected: true,
441
+ });
442
+
443
+ const bob = await setupTestAccount();
444
+ const { peer } = bob.connectToSyncServer();
445
+
446
+ const alice = await setupTestAccount({
447
+ connected: true,
448
+ });
449
+
450
+ const bobOnAdminNode = await loadCoValueOrFail(admin.node, bob.accountID);
451
+
452
+ const aliceOnAdminNode = await loadCoValueOrFail(
453
+ admin.node,
454
+ alice.accountID,
455
+ );
456
+
457
+ const group = admin.node.createGroup();
458
+ const childGroup = bob.node.createGroup();
459
+
460
+ group.addMember(bobOnAdminNode, "reader");
461
+ group.addMember(aliceOnAdminNode, "reader");
462
+
463
+ await group.core.waitForSync();
464
+
465
+ blockMessageTypeOnOutgoingPeer(peer, "content", {
466
+ id: childGroup.id,
467
+ });
468
+
469
+ const groupOnBobNode = await loadCoValueOrFail(bob.node, group.id);
470
+
471
+ childGroup.extend(groupOnBobNode);
472
+
473
+ await groupOnBobNode.core.waitForSync();
474
+
475
+ group.removeMember(aliceOnAdminNode);
476
+
477
+ await admin.node.syncManager.waitForAllCoValuesSync();
478
+
479
+ const map = group.createMap();
480
+ map.set("test", "Not readable by alice");
481
+
482
+ await map.core.waitForSync();
483
+
484
+ const mapOnAliceNode = await loadCoValueOrFail(alice.node, map.id);
485
+ expect(mapOnAliceNode.get("test")).toBeUndefined();
486
+ });
487
+
488
+ test("removing a member should work even if there are unavailable child groups", async () => {
489
+ const admin = await setupTestAccount({
490
+ connected: true,
491
+ });
492
+
493
+ const { peerOnServer } = admin.connectToSyncServer();
494
+
495
+ const bob = await setupTestAccount({
496
+ connected: true,
497
+ });
498
+
499
+ const alice = await setupTestAccount({
500
+ connected: true,
501
+ });
502
+
503
+ const bobOnAdminNode = await loadCoValueOrFail(admin.node, bob.accountID);
504
+
505
+ const aliceOnAdminNode = await loadCoValueOrFail(
506
+ admin.node,
507
+ alice.accountID,
508
+ );
509
+
510
+ const group = admin.node.createGroup();
511
+ const childGroup = bob.node.createGroup();
512
+
513
+ blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
514
+ id: childGroup.id,
515
+ });
516
+
517
+ group.addMember(bobOnAdminNode, "reader");
518
+ group.addMember(aliceOnAdminNode, "reader");
519
+
520
+ await group.core.waitForSync();
521
+
522
+ const groupOnBobNode = await loadCoValueOrFail(bob.node, group.id);
523
+
524
+ childGroup.extend(groupOnBobNode);
525
+
526
+ await groupOnBobNode.core.waitForSync();
527
+
528
+ group.removeMember(aliceOnAdminNode);
529
+
530
+ await group.core.waitForSync();
531
+
532
+ const map = group.createMap();
533
+ map.set("test", "Not readable by alice");
534
+
535
+ await map.core.waitForSync();
536
+
537
+ const mapOnAliceNode = await loadCoValueOrFail(alice.node, map.id);
538
+ expect(mapOnAliceNode.get("test")).toBeUndefined();
539
+ });
297
540
  });
@@ -33,7 +33,7 @@ describe("roleOf", () => {
33
33
  const [agent2] = randomAgentAndSessionID();
34
34
 
35
35
  group.addMember(agent2, "writer");
36
- group.removeMemberInternal(agent2);
36
+ group.removeMember(agent2);
37
37
  expect(group.roleOfInternal(agent2.id)).toEqual(undefined);
38
38
  });
39
39
 
@@ -63,7 +63,7 @@ describe("roleOf", () => {
63
63
 
64
64
  group.addMemberInternal("everyone", "reader");
65
65
  group.addMember(agent2, "writer");
66
- group.removeMemberInternal("everyone");
66
+ group.removeMember("everyone");
67
67
  expect(group.roleOfInternal(agent2.id)).toEqual("writer");
68
68
  expect(group.roleOfInternal("123" as RawAccountID)).toEqual(undefined);
69
69
  });
@@ -3,18 +3,27 @@ import { RawCoList } from "../coValues/coList.js";
3
3
  import { RawCoMap } from "../coValues/coMap.js";
4
4
  import { RawCoStream } from "../coValues/coStream.js";
5
5
  import { RawBinaryCoStream } from "../coValues/coStream.js";
6
- import { WasmCrypto } from "../crypto/WasmCrypto.js";
7
- import { RawAccountID } from "../exports.js";
6
+ import type { RawCoValue, RawGroup } from "../exports.js";
7
+ import type { NewContentMessage } from "../sync.js";
8
8
  import {
9
9
  createThreeConnectedNodes,
10
10
  createTwoConnectedNodes,
11
11
  loadCoValueOrFail,
12
12
  nodeWithRandomAgentAndSessionID,
13
- randomAgentAndSessionID,
14
- waitFor,
13
+ setupTestNode,
15
14
  } from "./testUtils.js";
16
15
 
17
- const Crypto = await WasmCrypto.create();
16
+ function expectGroup(content: RawCoValue): RawGroup {
17
+ if (content.type !== "comap") {
18
+ throw new Error("Expected group");
19
+ }
20
+
21
+ if (content.core.verified.header.ruleset.type !== "group") {
22
+ throw new Error("Expected group ruleset in group");
23
+ }
24
+
25
+ return content as RawGroup;
26
+ }
18
27
 
19
28
  test("Can create a RawCoMap in a group", () => {
20
29
  const node = nodeWithRandomAgentAndSessionID();
@@ -307,6 +316,97 @@ test("Invites should have access to the new keys", async () => {
307
316
  expect(mapOnNode2.get("test")).toEqual("Written from node1");
308
317
  });
309
318
 
319
+ test("Should heal the missing key_for_everyone", async () => {
320
+ const client = setupTestNode({
321
+ secret:
322
+ "sealerSecret_zBTPp7U58Fzq9o7EvJpu4KEziepi8QVf2Xaxuy5xmmXFx/signerSecret_z62DuviZdXCjz4EZWofvr9vaLYFXDeTaC9KWhoQiQjzKk",
323
+ });
324
+
325
+ const brokenGroupContent = {
326
+ action: "content",
327
+ id: "co_zW7F36Nnop9A7Er4gUzBcUXnZCK",
328
+ header: {
329
+ type: "comap",
330
+ ruleset: {
331
+ type: "group",
332
+ initialAdmin:
333
+ "sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv",
334
+ },
335
+ meta: null,
336
+ createdAt: "2025-08-06T10:14:39.617Z",
337
+ uniqueness: "z3LJjnuPiPJaf5Qb9A",
338
+ },
339
+ priority: 0,
340
+ new: {
341
+ "sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv_session_zYLsz2CiW9pW":
342
+ {
343
+ after: 0,
344
+ newTransactions: [
345
+ {
346
+ privacy: "trusting",
347
+ madeAt: 1754475279619,
348
+ changes:
349
+ '[{"key":"sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"admin"}]',
350
+ },
351
+ {
352
+ privacy: "trusting",
353
+ madeAt: 1754475279621,
354
+ changes:
355
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"sealed_UCg4UkytXF-W8PaIvaDffO3pZ3d9hdXUuNkQQEikPTAuOD9us92Pqb5Vgu7lx1Fpb0X8V5BJ2yxz6_D5WOzK3qjWBSsc7J1xDJA=="}]',
356
+ },
357
+ {
358
+ privacy: "trusting",
359
+ madeAt: 1754475279621,
360
+ changes:
361
+ '[{"key":"readKey","op":"set","value":"key_z5CVahfMkEWPj1B3zH"}]',
362
+ },
363
+ {
364
+ privacy: "trusting",
365
+ madeAt: 1754475279622,
366
+ changes: '[{"key":"everyone","op":"set","value":"reader"}]',
367
+ },
368
+ {
369
+ privacy: "trusting",
370
+ madeAt: 1754475279623,
371
+ changes:
372
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_everyone","op":"set","value":"keySecret_z9U9gzkahQXCxDoSw7isiUnbobXwuLdcSkL9Ci6ZEEkaL"}]',
373
+ },
374
+ {
375
+ privacy: "trusting",
376
+ madeAt: 1754475279623,
377
+ changes:
378
+ '[{"key":"key_z4Fi7hZNBx7XoVAKkP_for_sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"sealed_UuCBBfZkTnRTrGraqWWlzm9JE-VFduhsfu7WaZjpCbJYOTXpPhSNOnzGeS8qVuIsG6dORbME22lc5ltLxPjRqofQdDCNGQehCeQ=="}]',
379
+ },
380
+ {
381
+ privacy: "trusting",
382
+ madeAt: 1754475279624,
383
+ changes:
384
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_key_z4Fi7hZNBx7XoVAKkP","op":"set","value":"encrypted_USTrBuobwTCORwy5yHxy4sFZ7swfrafP6k5ZwcTf76f0MBu9Ie-JmsX3mNXad4mluI47gvGXzi8I_"}]',
385
+ },
386
+ {
387
+ privacy: "trusting",
388
+ madeAt: 1754475279624,
389
+ changes:
390
+ '[{"key":"readKey","op":"set","value":"key_z4Fi7hZNBx7XoVAKkP"}]',
391
+ },
392
+ ],
393
+ lastSignature:
394
+ "signature_z3tsE7U1JaeNeUmZ4EY3Xq5uQ9jq9jDi6Rkhdt7T7b7z4NCnpMgB4bo8TwLXYVCrRdBm6PoyyPdK8fYFzHJUh5EzA",
395
+ },
396
+ },
397
+ } as unknown as NewContentMessage;
398
+
399
+ client.node.syncManager.handleNewContent(brokenGroupContent, "import");
400
+
401
+ const group = expectGroup(
402
+ client.node.getCoValue(brokenGroupContent.id).getCurrentContent(),
403
+ );
404
+
405
+ expect(group.get(`${group.get("readKey")!}_for_everyone`)).toBe(
406
+ group.core.getCurrentReadKey()?.secret,
407
+ );
408
+ });
409
+
310
410
  describe("writeOnly", () => {
311
411
  test("Admins can invite writeOnly members", async () => {
312
412
  const { node1, node2 } = await createTwoConnectedNodes("server", "server");
@@ -0,0 +1,178 @@
1
+ import { assert, beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import { setGarbageCollectorMaxAge } from "../config";
4
+ import { emptyKnownState } from "../exports";
5
+ import {
6
+ SyncMessagesLog,
7
+ TEST_NODE_CONFIG,
8
+ loadCoValueOrFail,
9
+ setupTestNode,
10
+ waitFor,
11
+ } from "./testUtils";
12
+
13
+ // We want to simulate a real world communication that happens asynchronously
14
+ TEST_NODE_CONFIG.withAsyncPeers = true;
15
+
16
+ beforeEach(() => {
17
+ // We want to test what happens when the garbage collector kicks in and removes a coValue
18
+ // We set the max age to -1 to make it remove everything
19
+ setGarbageCollectorMaxAge(-1);
20
+ });
21
+
22
+ describe("sync after the garbage collector has run", () => {
23
+ let jazzCloud: ReturnType<typeof setupTestNode>;
24
+
25
+ beforeEach(async () => {
26
+ SyncMessagesLog.clear();
27
+ jazzCloud = setupTestNode({
28
+ isSyncServer: true,
29
+ });
30
+ jazzCloud.addStorage({
31
+ ourName: "server",
32
+ });
33
+ jazzCloud.node.enableGarbageCollector();
34
+ });
35
+
36
+ test("loading a coValue from the sync server that was removed by the garbage collector", async () => {
37
+ const client = setupTestNode();
38
+
39
+ client.connectToSyncServer();
40
+
41
+ const group = jazzCloud.node.createGroup();
42
+ const map = group.createMap();
43
+ map.set("hello", "world", "trusting");
44
+
45
+ await map.core.waitForSync();
46
+
47
+ // force the garbage collector to run
48
+ jazzCloud.node.garbageCollector?.collect();
49
+
50
+ SyncMessagesLog.clear();
51
+
52
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
53
+ expect(mapOnClient.get("hello")).toEqual("world");
54
+
55
+ expect(
56
+ SyncMessagesLog.getMessages({
57
+ Group: group.core,
58
+ Map: map.core,
59
+ }),
60
+ ).toMatchInlineSnapshot(`
61
+ [
62
+ "client -> server | LOAD Map sessions: empty",
63
+ "server -> storage | LOAD Map sessions: empty",
64
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 3",
65
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
66
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
67
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
68
+ "client -> server | KNOWN Group sessions: header/3",
69
+ "client -> server | KNOWN Map sessions: header/1",
70
+ ]
71
+ `);
72
+ });
73
+
74
+ test("updating a coValue that was removed by the garbage collector", async () => {
75
+ const client = setupTestNode();
76
+
77
+ client.connectToSyncServer();
78
+
79
+ const group = jazzCloud.node.createGroup();
80
+ group.addMember("everyone", "writer");
81
+ const map = group.createMap();
82
+ map.set("hello", "world", "trusting");
83
+
84
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
85
+ expect(mapOnClient.get("hello")).toEqual("world");
86
+
87
+ // force the garbage collector to run
88
+ jazzCloud.node.garbageCollector?.collect();
89
+ SyncMessagesLog.clear();
90
+
91
+ mapOnClient.set("hello", "updated", "trusting");
92
+
93
+ await mapOnClient.core.waitForSync();
94
+
95
+ const mapOnServer = await loadCoValueOrFail(jazzCloud.node, map.id);
96
+
97
+ expect(mapOnServer.get("hello")).toEqual("updated");
98
+
99
+ expect(
100
+ SyncMessagesLog.getMessages({
101
+ Group: group.core,
102
+ Map: map.core,
103
+ }),
104
+ ).toMatchInlineSnapshot(`
105
+ [
106
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
107
+ "server -> storage | LOAD Map sessions: empty",
108
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
109
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
110
+ "server -> client | KNOWN Map sessions: header/2",
111
+ "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
112
+ ]
113
+ `);
114
+ });
115
+
116
+ test("syncing a coValue that was removed by the garbage collector", async () => {
117
+ const edge = setupTestNode();
118
+ edge.addStorage({
119
+ ourName: "edge",
120
+ });
121
+ edge.connectToSyncServer({
122
+ syncServer: jazzCloud.node,
123
+ syncServerName: "server",
124
+ ourName: "edge",
125
+ });
126
+ edge.node.enableGarbageCollector();
127
+ const client = setupTestNode();
128
+
129
+ client.connectToSyncServer({
130
+ syncServer: edge.node,
131
+ syncServerName: "edge",
132
+ });
133
+
134
+ const group = edge.node.createGroup();
135
+ group.addMember("everyone", "writer");
136
+
137
+ await group.core.waitForSync();
138
+
139
+ const map = group.createMap();
140
+
141
+ map.set("hello", "updated", "trusting");
142
+
143
+ // force the garbage collector to run before the transaction is synced
144
+ edge.node.garbageCollector?.collect();
145
+ expect(edge.node.getCoValue(map.id).isAvailable()).toBe(false);
146
+
147
+ SyncMessagesLog.clear();
148
+
149
+ // The storage should work even after the coValue is unmounted, so the load should be successful
150
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
151
+ expect(mapOnClient.get("hello")).toEqual("updated");
152
+
153
+ expect(
154
+ SyncMessagesLog.getMessages({
155
+ Group: group.core,
156
+ Map: map.core,
157
+ }),
158
+ ).toMatchInlineSnapshot(`
159
+ [
160
+ "client -> edge | LOAD Map sessions: empty",
161
+ "edge -> storage | CONTENT Map header: true new: After: 0 New: 1",
162
+ "edge -> server | CONTENT Map header: true new: After: 0 New: 1",
163
+ "edge -> storage | LOAD Map sessions: empty",
164
+ "storage -> edge | CONTENT Group header: true new: After: 0 New: 5",
165
+ "storage -> edge | CONTENT Map header: true new: After: 0 New: 1",
166
+ "edge -> server | CONTENT Map header: true new: ",
167
+ "edge -> client | CONTENT Group header: true new: After: 0 New: 5",
168
+ "edge -> client | CONTENT Map header: true new: After: 0 New: 1",
169
+ "server -> edge | KNOWN Map sessions: header/1",
170
+ "server -> storage | CONTENT Map header: true new: After: 0 New: 1",
171
+ "server -> edge | KNOWN Map sessions: header/1",
172
+ "server -> storage | CONTENT Map header: true new: ",
173
+ "client -> edge | KNOWN Group sessions: header/5",
174
+ "client -> edge | KNOWN Map sessions: header/1",
175
+ ]
176
+ `);
177
+ });
178
+ });
@@ -590,11 +590,14 @@ describe("loading coValues from server", () => {
590
590
  "client -> server | LOAD Map sessions: empty",
591
591
  "server -> client | CONTENT Group header: true new: After: 0 New: 5",
592
592
  "client -> server | KNOWN Group sessions: header/5",
593
- "client -> server | LOAD Map sessions: empty",
594
- "server -> client | KNOWN Group sessions: header/5",
593
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
594
+ "client -> server | KNOWN Group sessions: header/5",
595
595
  "server -> client | CONTENT Map header: true new: After: 0 New: 1",
596
- "client -> server | LOAD Group sessions: header/5",
597
596
  "client -> server | KNOWN Map sessions: header/1",
597
+ "client -> server | LOAD Group sessions: header/5",
598
+ "server -> client | KNOWN Group sessions: header/5",
599
+ "client -> server | LOAD Map sessions: header/1",
600
+ "server -> client | KNOWN Map sessions: header/1",
598
601
  ]
599
602
  `);
600
603
  });