cojson 0.16.5 → 0.16.7

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 (121) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -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/coValueContentMessage.d.ts +1 -0
  9. package/dist/coValueContentMessage.d.ts.map +1 -1
  10. package/dist/coValueContentMessage.js +11 -3
  11. package/dist/coValueContentMessage.js.map +1 -1
  12. package/dist/coValueCore/coValueCore.d.ts +2 -1
  13. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  14. package/dist/coValueCore/coValueCore.js +15 -0
  15. package/dist/coValueCore/coValueCore.js.map +1 -1
  16. package/dist/coValueCore/utils.d.ts.map +1 -1
  17. package/dist/coValueCore/utils.js.map +1 -1
  18. package/dist/coValueCore/verifiedState.d.ts +1 -0
  19. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  20. package/dist/coValueCore/verifiedState.js.map +1 -1
  21. package/dist/coValues/coMap.d.ts +3 -3
  22. package/dist/coValues/coPlainText.d.ts +1 -0
  23. package/dist/coValues/coPlainText.d.ts.map +1 -1
  24. package/dist/coValues/coPlainText.js +27 -8
  25. package/dist/coValues/coPlainText.js.map +1 -1
  26. package/dist/coValues/coStream.d.ts +2 -2
  27. package/dist/coValues/group.d.ts +1 -1
  28. package/dist/config.d.ts +10 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +16 -1
  31. package/dist/config.js.map +1 -1
  32. package/dist/exports.d.ts +11 -4
  33. package/dist/exports.d.ts.map +1 -1
  34. package/dist/exports.js +8 -3
  35. package/dist/exports.js.map +1 -1
  36. package/dist/localNode.d.ts +3 -0
  37. package/dist/localNode.d.ts.map +1 -1
  38. package/dist/localNode.js +11 -0
  39. package/dist/localNode.js.map +1 -1
  40. package/dist/permissions.d.ts.map +1 -1
  41. package/dist/permissions.js +3 -1
  42. package/dist/permissions.js.map +1 -1
  43. package/dist/queue/LocalTransactionsSyncQueue.js +1 -1
  44. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  45. package/dist/queue/StoreQueue.d.ts +7 -1
  46. package/dist/queue/StoreQueue.d.ts.map +1 -1
  47. package/dist/queue/StoreQueue.js +35 -13
  48. package/dist/queue/StoreQueue.js.map +1 -1
  49. package/dist/storage/sqlite/client.d.ts +4 -4
  50. package/dist/storage/sqlite/client.d.ts.map +1 -1
  51. package/dist/storage/sqlite/client.js +13 -4
  52. package/dist/storage/sqlite/client.js.map +1 -1
  53. package/dist/storage/sqliteAsync/client.d.ts +3 -3
  54. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  55. package/dist/storage/sqliteAsync/client.js +12 -3
  56. package/dist/storage/sqliteAsync/client.js.map +1 -1
  57. package/dist/storage/storageAsync.d.ts.map +1 -1
  58. package/dist/storage/storageAsync.js +2 -7
  59. package/dist/storage/storageAsync.js.map +1 -1
  60. package/dist/storage/storageSync.d.ts.map +1 -1
  61. package/dist/storage/storageSync.js +2 -7
  62. package/dist/storage/storageSync.js.map +1 -1
  63. package/dist/storage/types.d.ts +2 -2
  64. package/dist/storage/types.d.ts.map +1 -1
  65. package/dist/sync.d.ts.map +1 -1
  66. package/dist/sync.js +17 -3
  67. package/dist/sync.js.map +1 -1
  68. package/dist/tests/GarbageCollector.test.d.ts +2 -0
  69. package/dist/tests/GarbageCollector.test.d.ts.map +1 -0
  70. package/dist/tests/GarbageCollector.test.js +85 -0
  71. package/dist/tests/GarbageCollector.test.js.map +1 -0
  72. package/dist/tests/coPlainText.test.js +142 -4
  73. package/dist/tests/coPlainText.test.js.map +1 -1
  74. package/dist/tests/coStream.test.js +3 -3
  75. package/dist/tests/coStream.test.js.map +1 -1
  76. package/dist/tests/sync.garbageCollection.test.d.ts +2 -0
  77. package/dist/tests/sync.garbageCollection.test.d.ts.map +1 -0
  78. package/dist/tests/sync.garbageCollection.test.js +133 -0
  79. package/dist/tests/sync.garbageCollection.test.js.map +1 -0
  80. package/dist/tests/sync.mesh.test.js +48 -34
  81. package/dist/tests/sync.mesh.test.js.map +1 -1
  82. package/dist/tests/sync.storage.test.js +31 -21
  83. package/dist/tests/sync.storage.test.js.map +1 -1
  84. package/dist/tests/sync.storageAsync.test.js +76 -29
  85. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  86. package/dist/tests/testStorage.d.ts +1 -0
  87. package/dist/tests/testStorage.d.ts.map +1 -1
  88. package/dist/tests/testStorage.js +1 -1
  89. package/dist/tests/testStorage.js.map +1 -1
  90. package/dist/tests/testUtils.d.ts +1 -0
  91. package/dist/tests/testUtils.d.ts.map +1 -1
  92. package/dist/tests/testUtils.js +1 -0
  93. package/dist/tests/testUtils.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/GarbageCollector.ts +48 -0
  96. package/src/coValueContentMessage.ts +16 -3
  97. package/src/coValueCore/coValueCore.ts +27 -10
  98. package/src/coValueCore/utils.ts +1 -0
  99. package/src/coValueCore/verifiedState.ts +1 -0
  100. package/src/coValues/coPlainText.ts +40 -8
  101. package/src/config.ts +20 -1
  102. package/src/exports.ts +13 -5
  103. package/src/localNode.ts +15 -1
  104. package/src/permissions.ts +3 -1
  105. package/src/queue/LocalTransactionsSyncQueue.ts +1 -1
  106. package/src/queue/StoreQueue.ts +45 -12
  107. package/src/storage/sqlite/client.ts +24 -10
  108. package/src/storage/sqliteAsync/client.ts +26 -5
  109. package/src/storage/storageAsync.ts +5 -9
  110. package/src/storage/storageSync.ts +2 -9
  111. package/src/storage/types.ts +7 -4
  112. package/src/sync.ts +19 -3
  113. package/src/tests/GarbageCollector.test.ts +127 -0
  114. package/src/tests/coPlainText.test.ts +176 -4
  115. package/src/tests/coStream.test.ts +7 -3
  116. package/src/tests/sync.garbageCollection.test.ts +178 -0
  117. package/src/tests/sync.mesh.test.ts +49 -34
  118. package/src/tests/sync.storage.test.ts +31 -21
  119. package/src/tests/sync.storageAsync.test.ts +81 -29
  120. package/src/tests/testStorage.ts +11 -3
  121. package/src/tests/testUtils.ts +4 -1
@@ -65,6 +65,11 @@ export interface DBClientInterfaceAsync {
65
65
  coValueId: string,
66
66
  ): Promise<StoredCoValueRow | undefined> | undefined;
67
67
 
68
+ upsertCoValue(
69
+ id: string,
70
+ header?: CoValueHeader,
71
+ ): Promise<number | undefined>;
72
+
68
73
  getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]>;
69
74
 
70
75
  getSingleCoValueSession(
@@ -83,8 +88,6 @@ export interface DBClientInterfaceAsync {
83
88
  firstNewTxIdx: number,
84
89
  ): Promise<SignatureAfterRow[]>;
85
90
 
86
- addCoValue(msg: NewContentMessage): Promise<number>;
87
-
88
91
  addSessionUpdate({
89
92
  sessionUpdate,
90
93
  sessionRow,
@@ -115,6 +118,8 @@ export interface DBClientInterfaceAsync {
115
118
  export interface DBClientInterfaceSync {
116
119
  getCoValue(coValueId: string): StoredCoValueRow | undefined;
117
120
 
121
+ upsertCoValue(id: string, header?: CoValueHeader): number | undefined;
122
+
118
123
  getCoValueSessions(coValueRowId: number): StoredSessionRow[];
119
124
 
120
125
  getSingleCoValueSession(
@@ -133,8 +138,6 @@ export interface DBClientInterfaceSync {
133
138
  firstNewTxIdx: number,
134
139
  ): Pick<SignatureAfterRow, "idx" | "signature">[];
135
140
 
136
- addCoValue(msg: NewContentMessage): number;
137
-
138
141
  addSessionUpdate({
139
142
  sessionUpdate,
140
143
  sessionRow,
package/src/sync.ts CHANGED
@@ -461,6 +461,22 @@ export class SyncManager {
461
461
 
462
462
  if (!coValue.hasVerifiedContent()) {
463
463
  if (!msg.header) {
464
+ const storageKnownState = this.local.storage?.getKnownState(msg.id);
465
+
466
+ if (storageKnownState?.header) {
467
+ // If the CoValue has been garbage collected, we load it from the storage before handling the new content
468
+ coValue.loadFromStorage((found) => {
469
+ if (found) {
470
+ this.handleNewContent(msg, from);
471
+ } else {
472
+ logger.error("Known CoValue not found in storage", {
473
+ id: msg.id,
474
+ });
475
+ }
476
+ });
477
+ return;
478
+ }
479
+
464
480
  if (peer) {
465
481
  this.trySendToPeer(peer, {
466
482
  action: "known",
@@ -782,12 +798,12 @@ export class SyncManager {
782
798
 
783
799
  if (!storage) return;
784
800
 
801
+ const value = this.local.getCoValue(content.id);
802
+
785
803
  // Try to store the content as-is for performance
786
804
  // In case that some transactions are missing, a correction will be requested, but it's an edge case
787
805
  storage.store(content, (correction) => {
788
- return this.local
789
- .getCoValue(content.id)
790
- .verified?.newContentSince(correction);
806
+ return value.verified?.newContentSince(correction);
791
807
  });
792
808
  }
793
809
 
@@ -0,0 +1,127 @@
1
+ import { assert, beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import { setGarbageCollectorMaxAge } from "../config";
4
+ import { TEST_NODE_CONFIG, setupTestAccount, setupTestNode } from "./testUtils";
5
+
6
+ // We want to simulate a real world communication that happens asynchronously
7
+ TEST_NODE_CONFIG.withAsyncPeers = true;
8
+
9
+ beforeEach(() => {
10
+ // We want to test what happens when the garbage collector kicks in and removes a coValue
11
+ // We set the max age to -1 to make it remove everything
12
+ setGarbageCollectorMaxAge(-1);
13
+ });
14
+
15
+ describe("garbage collector", () => {
16
+ test("coValues are garbage collected when maxAge is reached", async () => {
17
+ const client = setupTestNode();
18
+
19
+ client.addStorage({
20
+ ourName: "client",
21
+ });
22
+ client.node.enableGarbageCollector();
23
+
24
+ const group = client.node.createGroup();
25
+ const map = group.createMap();
26
+ map.set("hello", "world", "trusting");
27
+
28
+ await new Promise((resolve) => setTimeout(resolve, 10));
29
+
30
+ client.node.garbageCollector?.collect();
31
+
32
+ const coValue = client.node.getCoValue(map.id);
33
+
34
+ expect(coValue.isAvailable()).toBe(false);
35
+ });
36
+
37
+ test("coValues are not garbage collected if they have listeners", async () => {
38
+ const client = setupTestNode();
39
+
40
+ client.addStorage({
41
+ ourName: "client",
42
+ });
43
+ client.node.enableGarbageCollector();
44
+
45
+ const group = client.node.createGroup();
46
+ const map = group.createMap();
47
+ map.set("hello", "world", "trusting");
48
+
49
+ // Add a listener to the map
50
+ const unsubscribe = map.subscribe(() => {
51
+ // This listener keeps the coValue alive
52
+ });
53
+
54
+ await new Promise((resolve) => setTimeout(resolve, 10));
55
+
56
+ client.node.garbageCollector?.collect();
57
+
58
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
59
+
60
+ // Clean up the listener
61
+ unsubscribe();
62
+
63
+ // The coValue should be collected after the listener is removed
64
+ client.node.garbageCollector?.collect();
65
+
66
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
67
+ });
68
+
69
+ test("coValues are not garbage collected if they are a group or account", async () => {
70
+ const client = await setupTestAccount();
71
+
72
+ client.addStorage({
73
+ ourName: "client",
74
+ });
75
+ client.node.enableGarbageCollector();
76
+
77
+ const group = client.node.createGroup();
78
+
79
+ await new Promise((resolve) => setTimeout(resolve, 10));
80
+
81
+ client.node.garbageCollector?.collect();
82
+
83
+ expect(client.node.getCoValue(group.id).isAvailable()).toBe(true);
84
+ expect(client.node.getCoValue(client.accountID).isAvailable()).toBe(true);
85
+ });
86
+
87
+ test("coValues are not garbage collected if the maxAge is not reached", async () => {
88
+ setGarbageCollectorMaxAge(1000);
89
+
90
+ const client = setupTestNode();
91
+
92
+ client.addStorage({
93
+ ourName: "client",
94
+ });
95
+ client.node.enableGarbageCollector();
96
+
97
+ const garbageCollector = client.node.garbageCollector;
98
+
99
+ assert(garbageCollector);
100
+
101
+ const getCurrentTime = vi.spyOn(garbageCollector, "getCurrentTime");
102
+
103
+ getCurrentTime.mockReturnValue(1);
104
+
105
+ const group = client.node.createGroup();
106
+ const map1 = group.createMap();
107
+ const map2 = group.createMap();
108
+
109
+ await new Promise((resolve) => setTimeout(resolve, 10));
110
+
111
+ map1.set("hello", "world", "trusting");
112
+
113
+ getCurrentTime.mockReturnValue(2000);
114
+
115
+ await new Promise((resolve) => setTimeout(resolve, 10));
116
+
117
+ garbageCollector.collect();
118
+
119
+ const coValue = client.node.getCoValue(map1.id);
120
+
121
+ expect(coValue.isAvailable()).toBe(true);
122
+
123
+ const coValue2 = client.node.getCoValue(map2.id);
124
+
125
+ expect(coValue2.isAvailable()).toBe(false);
126
+ });
127
+ });
@@ -1,10 +1,21 @@
1
- import { afterEach, expect, test, vi } from "vitest";
1
+ import { afterEach, beforeEach, expect, test, vi } from "vitest";
2
2
  import { expectPlainText } from "../coValue.js";
3
+ import { setMaxRecommendedTxSize } from "../config.js";
3
4
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
4
- import { nodeWithRandomAgentAndSessionID } from "./testUtils.js";
5
+ import {
6
+ SyncMessagesLog,
7
+ loadCoValueOrFail,
8
+ nodeWithRandomAgentAndSessionID,
9
+ setupTestNode,
10
+ } from "./testUtils.js";
5
11
 
6
12
  const Crypto = await WasmCrypto.create();
7
13
 
14
+ beforeEach(() => {
15
+ setMaxRecommendedTxSize(100 * 1024);
16
+ SyncMessagesLog.clear();
17
+ });
18
+
8
19
  afterEach(() => void vi.unstubAllGlobals());
9
20
 
10
21
  test("Empty CoPlainText works", () => {
@@ -185,8 +196,8 @@ test("insertBefore and insertAfter work as expected", () => {
185
196
  expect(content.toString()).toEqual("hey");
186
197
 
187
198
  // Insert '!' at start
188
- content.insertBefore(0, "!", "trusting"); // "!hey"
189
- expect(content.toString()).toEqual("!hey");
199
+ content.insertBefore(0, "!?", "trusting"); // "!?hey"
200
+ expect(content.toString()).toEqual("!?hey");
190
201
  });
191
202
 
192
203
  test("Can delete a single grapheme", () => {
@@ -286,3 +297,164 @@ test("Splits into and from grapheme string arrays", () => {
286
297
  const text = content.fromGraphemes(graphemes);
287
298
  expect(text).toEqual("👋 안녕!");
288
299
  });
300
+
301
+ test("chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX_SIZE", async () => {
302
+ setMaxRecommendedTxSize(5);
303
+
304
+ const client = setupTestNode();
305
+ const { storage } = client.addStorage();
306
+
307
+ const coValue = client.node.createCoValue({
308
+ type: "coplaintext",
309
+ ruleset: { type: "unsafeAllowAll" },
310
+ meta: null,
311
+ ...Crypto.createdNowUnique(),
312
+ });
313
+
314
+ const content = expectPlainText(coValue.getCurrentContent());
315
+
316
+ content.insertAfter(
317
+ content.entries().length,
318
+ "I'm writing you to test that coplaintext",
319
+ "trusting",
320
+ );
321
+ content.insertAfter(
322
+ content.entries().length,
323
+ " chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX_SIZE.",
324
+ "trusting",
325
+ );
326
+ content.insertAfter(
327
+ content.entries().length,
328
+ "This is required because when a user paste 1Mb of text, we can split it in multiple websocket messages.",
329
+ "trusting",
330
+ );
331
+
332
+ content.insertBefore(0, "Dear reader,\n", "trusting");
333
+
334
+ expect(content.toString()).toMatchInlineSnapshot(
335
+ `"Dear reader,\nI'm writing you to test that coplaintext chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX_SIZE.This is required because when a user paste 1Mb of text, we can split it in multiple websocket messages."`,
336
+ );
337
+
338
+ await coValue.waitForSync();
339
+
340
+ client.restart();
341
+ client.addStorage({
342
+ storage,
343
+ });
344
+
345
+ const loaded = await loadCoValueOrFail(client.node, content.id);
346
+ await loaded.core.waitForSync();
347
+
348
+ expect(loaded.toString()).toEqual(content.toString());
349
+
350
+ expect(
351
+ SyncMessagesLog.getMessages({
352
+ CoPlainText: coValue,
353
+ }),
354
+ ).toMatchInlineSnapshot(`
355
+ [
356
+ "client -> storage | CONTENT CoPlainText header: true new: ",
357
+ "client -> storage | CONTENT CoPlainText header: false new: After: 0 New: 1",
358
+ "client -> storage | CONTENT CoPlainText header: false new: After: 1 New: 1",
359
+ "client -> storage | CONTENT CoPlainText header: false new: After: 2 New: 1",
360
+ "client -> storage | CONTENT CoPlainText header: false new: After: 3 New: 1",
361
+ "client -> storage | CONTENT CoPlainText header: false new: After: 4 New: 1",
362
+ "client -> storage | CONTENT CoPlainText header: false new: After: 5 New: 1",
363
+ "client -> storage | CONTENT CoPlainText header: false new: After: 6 New: 1",
364
+ "client -> storage | CONTENT CoPlainText header: false new: After: 7 New: 1",
365
+ "client -> storage | CONTENT CoPlainText header: false new: After: 8 New: 1",
366
+ "client -> storage | CONTENT CoPlainText header: false new: After: 9 New: 1",
367
+ "client -> storage | CONTENT CoPlainText header: false new: After: 10 New: 1",
368
+ "client -> storage | CONTENT CoPlainText header: false new: After: 11 New: 1",
369
+ "client -> storage | CONTENT CoPlainText header: false new: After: 12 New: 1",
370
+ "client -> storage | CONTENT CoPlainText header: false new: After: 13 New: 1",
371
+ "client -> storage | CONTENT CoPlainText header: false new: After: 14 New: 1",
372
+ "client -> storage | CONTENT CoPlainText header: false new: After: 15 New: 1",
373
+ "client -> storage | CONTENT CoPlainText header: false new: After: 16 New: 1",
374
+ "client -> storage | CONTENT CoPlainText header: false new: After: 17 New: 1",
375
+ "client -> storage | CONTENT CoPlainText header: false new: After: 18 New: 1",
376
+ "client -> storage | CONTENT CoPlainText header: false new: After: 19 New: 1",
377
+ "client -> storage | CONTENT CoPlainText header: false new: After: 20 New: 1",
378
+ "client -> storage | CONTENT CoPlainText header: false new: After: 21 New: 1",
379
+ "client -> storage | CONTENT CoPlainText header: false new: After: 22 New: 1",
380
+ "client -> storage | CONTENT CoPlainText header: false new: After: 23 New: 1",
381
+ "client -> storage | CONTENT CoPlainText header: false new: After: 24 New: 1",
382
+ "client -> storage | CONTENT CoPlainText header: false new: After: 25 New: 1",
383
+ "client -> storage | CONTENT CoPlainText header: false new: After: 26 New: 1",
384
+ "client -> storage | CONTENT CoPlainText header: false new: After: 27 New: 1",
385
+ "client -> storage | CONTENT CoPlainText header: false new: After: 28 New: 1",
386
+ "client -> storage | CONTENT CoPlainText header: false new: After: 29 New: 1",
387
+ "client -> storage | CONTENT CoPlainText header: false new: After: 30 New: 1",
388
+ "client -> storage | CONTENT CoPlainText header: false new: After: 31 New: 1",
389
+ "client -> storage | CONTENT CoPlainText header: false new: After: 32 New: 1",
390
+ "client -> storage | CONTENT CoPlainText header: false new: After: 33 New: 1",
391
+ "client -> storage | CONTENT CoPlainText header: false new: After: 34 New: 1",
392
+ "client -> storage | CONTENT CoPlainText header: false new: After: 35 New: 1",
393
+ "client -> storage | CONTENT CoPlainText header: false new: After: 36 New: 1",
394
+ "client -> storage | CONTENT CoPlainText header: false new: After: 37 New: 1",
395
+ "client -> storage | CONTENT CoPlainText header: false new: After: 38 New: 1",
396
+ "client -> storage | CONTENT CoPlainText header: false new: After: 39 New: 1",
397
+ "client -> storage | CONTENT CoPlainText header: false new: After: 40 New: 1",
398
+ "client -> storage | CONTENT CoPlainText header: false new: After: 41 New: 1",
399
+ "client -> storage | CONTENT CoPlainText header: false new: After: 42 New: 1",
400
+ "client -> storage | CONTENT CoPlainText header: false new: After: 43 New: 1",
401
+ "client -> storage | CONTENT CoPlainText header: false new: After: 44 New: 1",
402
+ "client -> storage | CONTENT CoPlainText header: false new: After: 45 New: 1",
403
+ "client -> storage | CONTENT CoPlainText header: false new: After: 46 New: 1",
404
+ "client -> storage | CONTENT CoPlainText header: false new: After: 47 New: 1",
405
+ "client -> storage | CONTENT CoPlainText header: false new: After: 48 New: 1",
406
+ "client -> storage | CONTENT CoPlainText header: false new: After: 49 New: 1",
407
+ "client -> storage | LOAD CoPlainText sessions: empty",
408
+ "storage -> client | CONTENT CoPlainText header: true new: After: 0 New: 1 expectContentUntil: header/50",
409
+ "storage -> client | CONTENT CoPlainText header: true new: After: 1 New: 1",
410
+ "storage -> client | CONTENT CoPlainText header: true new: After: 2 New: 1",
411
+ "storage -> client | CONTENT CoPlainText header: true new: After: 3 New: 1",
412
+ "storage -> client | CONTENT CoPlainText header: true new: After: 4 New: 1",
413
+ "storage -> client | CONTENT CoPlainText header: true new: After: 5 New: 1",
414
+ "storage -> client | CONTENT CoPlainText header: true new: After: 6 New: 1",
415
+ "storage -> client | CONTENT CoPlainText header: true new: After: 7 New: 1",
416
+ "storage -> client | CONTENT CoPlainText header: true new: After: 8 New: 1",
417
+ "storage -> client | CONTENT CoPlainText header: true new: After: 9 New: 1",
418
+ "storage -> client | CONTENT CoPlainText header: true new: After: 10 New: 1",
419
+ "storage -> client | CONTENT CoPlainText header: true new: After: 11 New: 1",
420
+ "storage -> client | CONTENT CoPlainText header: true new: After: 12 New: 1",
421
+ "storage -> client | CONTENT CoPlainText header: true new: After: 13 New: 1",
422
+ "storage -> client | CONTENT CoPlainText header: true new: After: 14 New: 1",
423
+ "storage -> client | CONTENT CoPlainText header: true new: After: 15 New: 1",
424
+ "storage -> client | CONTENT CoPlainText header: true new: After: 16 New: 1",
425
+ "storage -> client | CONTENT CoPlainText header: true new: After: 17 New: 1",
426
+ "storage -> client | CONTENT CoPlainText header: true new: After: 18 New: 1",
427
+ "storage -> client | CONTENT CoPlainText header: true new: After: 19 New: 1",
428
+ "storage -> client | CONTENT CoPlainText header: true new: After: 20 New: 1",
429
+ "storage -> client | CONTENT CoPlainText header: true new: After: 21 New: 1",
430
+ "storage -> client | CONTENT CoPlainText header: true new: After: 22 New: 1",
431
+ "storage -> client | CONTENT CoPlainText header: true new: After: 23 New: 1",
432
+ "storage -> client | CONTENT CoPlainText header: true new: After: 24 New: 1",
433
+ "storage -> client | CONTENT CoPlainText header: true new: After: 25 New: 1",
434
+ "storage -> client | CONTENT CoPlainText header: true new: After: 26 New: 1",
435
+ "storage -> client | CONTENT CoPlainText header: true new: After: 27 New: 1",
436
+ "storage -> client | CONTENT CoPlainText header: true new: After: 28 New: 1",
437
+ "storage -> client | CONTENT CoPlainText header: true new: After: 29 New: 1",
438
+ "storage -> client | CONTENT CoPlainText header: true new: After: 30 New: 1",
439
+ "storage -> client | CONTENT CoPlainText header: true new: After: 31 New: 1",
440
+ "storage -> client | CONTENT CoPlainText header: true new: After: 32 New: 1",
441
+ "storage -> client | CONTENT CoPlainText header: true new: After: 33 New: 1",
442
+ "storage -> client | CONTENT CoPlainText header: true new: After: 34 New: 1",
443
+ "storage -> client | CONTENT CoPlainText header: true new: After: 35 New: 1",
444
+ "storage -> client | CONTENT CoPlainText header: true new: After: 36 New: 1",
445
+ "storage -> client | CONTENT CoPlainText header: true new: After: 37 New: 1",
446
+ "storage -> client | CONTENT CoPlainText header: true new: After: 38 New: 1",
447
+ "storage -> client | CONTENT CoPlainText header: true new: After: 39 New: 1",
448
+ "storage -> client | CONTENT CoPlainText header: true new: After: 40 New: 1",
449
+ "storage -> client | CONTENT CoPlainText header: true new: After: 41 New: 1",
450
+ "storage -> client | CONTENT CoPlainText header: true new: After: 42 New: 1",
451
+ "storage -> client | CONTENT CoPlainText header: true new: After: 43 New: 1",
452
+ "storage -> client | CONTENT CoPlainText header: true new: After: 44 New: 1",
453
+ "storage -> client | CONTENT CoPlainText header: true new: After: 45 New: 1",
454
+ "storage -> client | CONTENT CoPlainText header: true new: After: 46 New: 1",
455
+ "storage -> client | CONTENT CoPlainText header: true new: After: 47 New: 1",
456
+ "storage -> client | CONTENT CoPlainText header: true new: After: 48 New: 1",
457
+ "storage -> client | CONTENT CoPlainText header: true new: After: 49 New: 1",
458
+ ]
459
+ `);
460
+ });
@@ -6,7 +6,7 @@ import {
6
6
  RawBinaryCoStream,
7
7
  RawCoStreamView,
8
8
  } from "../coValues/coStream.js";
9
- import { MAX_RECOMMENDED_TX_SIZE } from "../config.js";
9
+ import { TRANSACTION_CONFIG } from "../config.js";
10
10
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
11
11
  import { SessionID } from "../ids.js";
12
12
  import {
@@ -153,7 +153,9 @@ test("When adding large transactions (small fraction of MAX_RECOMMENDED_TX_SIZE)
153
153
  );
154
154
 
155
155
  for (let i = 0; i < 10; i++) {
156
- const chunk = new Uint8Array(MAX_RECOMMENDED_TX_SIZE / 3 + 100);
156
+ const chunk = new Uint8Array(
157
+ TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE / 3 + 100,
158
+ );
157
159
 
158
160
  content.pushBinaryStreamChunk(chunk, "trusting");
159
161
  }
@@ -226,7 +228,9 @@ test("When adding large transactions (bigger than MAX_RECOMMENDED_TX_SIZE), we s
226
228
  "trusting",
227
229
  );
228
230
 
229
- const chunk = new Uint8Array(MAX_RECOMMENDED_TX_SIZE + 100);
231
+ const chunk = new Uint8Array(
232
+ TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE + 100,
233
+ );
230
234
 
231
235
  for (let i = 0; i < 3; i++) {
232
236
  content.pushBinaryStreamChunk(chunk, "trusting");
@@ -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
+ });