cojson 0.19.21 → 0.19.22

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 (124) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
  4. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
  5. package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
  6. package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
  7. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
  8. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
  9. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
  10. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
  11. package/dist/CojsonMessageChannel/index.d.ts +3 -0
  12. package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
  13. package/dist/CojsonMessageChannel/index.js +2 -0
  14. package/dist/CojsonMessageChannel/index.js.map +1 -0
  15. package/dist/CojsonMessageChannel/types.d.ts +149 -0
  16. package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
  17. package/dist/CojsonMessageChannel/types.js +36 -0
  18. package/dist/CojsonMessageChannel/types.js.map +1 -0
  19. package/dist/GarbageCollector.d.ts +4 -2
  20. package/dist/GarbageCollector.d.ts.map +1 -1
  21. package/dist/GarbageCollector.js +5 -3
  22. package/dist/GarbageCollector.js.map +1 -1
  23. package/dist/SyncStateManager.d.ts +3 -3
  24. package/dist/SyncStateManager.d.ts.map +1 -1
  25. package/dist/SyncStateManager.js +4 -4
  26. package/dist/SyncStateManager.js.map +1 -1
  27. package/dist/coValueCore/coValueCore.d.ts +19 -1
  28. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  29. package/dist/coValueCore/coValueCore.js +29 -5
  30. package/dist/coValueCore/coValueCore.js.map +1 -1
  31. package/dist/exports.d.ts +1 -0
  32. package/dist/exports.d.ts.map +1 -1
  33. package/dist/exports.js +1 -0
  34. package/dist/exports.js.map +1 -1
  35. package/dist/localNode.d.ts +1 -3
  36. package/dist/localNode.d.ts.map +1 -1
  37. package/dist/localNode.js +3 -2
  38. package/dist/localNode.js.map +1 -1
  39. package/dist/storage/storageAsync.d.ts +8 -3
  40. package/dist/storage/storageAsync.d.ts.map +1 -1
  41. package/dist/storage/storageAsync.js +12 -3
  42. package/dist/storage/storageAsync.js.map +1 -1
  43. package/dist/storage/storageSync.d.ts +8 -3
  44. package/dist/storage/storageSync.d.ts.map +1 -1
  45. package/dist/storage/storageSync.js +12 -3
  46. package/dist/storage/storageSync.js.map +1 -1
  47. package/dist/storage/types.d.ts +5 -0
  48. package/dist/storage/types.d.ts.map +1 -1
  49. package/dist/sync.d.ts +6 -0
  50. package/dist/sync.d.ts.map +1 -1
  51. package/dist/sync.js +25 -4
  52. package/dist/sync.js.map +1 -1
  53. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  54. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  55. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  56. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  57. package/dist/tests/GarbageCollector.test.js +87 -13
  58. package/dist/tests/GarbageCollector.test.js.map +1 -1
  59. package/dist/tests/StorageApiAsync.test.js +33 -1
  60. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  61. package/dist/tests/StorageApiSync.test.js +32 -0
  62. package/dist/tests/StorageApiSync.test.js.map +1 -1
  63. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  64. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  65. package/dist/tests/SyncStateManager.test.js +1 -1
  66. package/dist/tests/SyncStateManager.test.js.map +1 -1
  67. package/dist/tests/coPlainText.test.js +1 -1
  68. package/dist/tests/coPlainText.test.js.map +1 -1
  69. package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
  70. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  71. package/dist/tests/knownState.lazyLoading.test.js +1 -0
  72. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  73. package/dist/tests/sync.garbageCollection.test.js +56 -32
  74. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  75. package/dist/tests/sync.load.test.js +3 -5
  76. package/dist/tests/sync.load.test.js.map +1 -1
  77. package/dist/tests/sync.mesh.test.js +1 -1
  78. package/dist/tests/sync.mesh.test.js.map +1 -1
  79. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  80. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  81. package/dist/tests/sync.storage.test.js +9 -9
  82. package/dist/tests/sync.storage.test.js.map +1 -1
  83. package/dist/tests/sync.storageAsync.test.js +7 -7
  84. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  85. package/dist/tests/sync.tracking.test.js +35 -4
  86. package/dist/tests/sync.tracking.test.js.map +1 -1
  87. package/dist/tests/testStorage.js +2 -2
  88. package/dist/tests/testStorage.js.map +1 -1
  89. package/dist/tests/testUtils.d.ts +24 -2
  90. package/dist/tests/testUtils.d.ts.map +1 -1
  91. package/dist/tests/testUtils.js +68 -7
  92. package/dist/tests/testUtils.js.map +1 -1
  93. package/package.json +4 -4
  94. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  95. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  96. package/src/CojsonMessageChannel/index.ts +9 -0
  97. package/src/CojsonMessageChannel/types.ts +200 -0
  98. package/src/GarbageCollector.ts +5 -5
  99. package/src/SyncStateManager.ts +6 -6
  100. package/src/coValueCore/coValueCore.ts +30 -7
  101. package/src/exports.ts +1 -0
  102. package/src/localNode.ts +3 -5
  103. package/src/storage/storageAsync.ts +15 -4
  104. package/src/storage/storageSync.ts +15 -4
  105. package/src/storage/types.ts +6 -0
  106. package/src/sync.ts +33 -4
  107. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  108. package/src/tests/GarbageCollector.test.ts +114 -13
  109. package/src/tests/StorageApiAsync.test.ts +50 -1
  110. package/src/tests/StorageApiSync.test.ts +49 -0
  111. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  112. package/src/tests/SyncStateManager.test.ts +1 -1
  113. package/src/tests/coPlainText.test.ts +1 -1
  114. package/src/tests/coValueCore.loadFromStorage.test.ts +2 -0
  115. package/src/tests/knownState.lazyLoading.test.ts +2 -0
  116. package/src/tests/sync.garbageCollection.test.ts +69 -36
  117. package/src/tests/sync.load.test.ts +3 -5
  118. package/src/tests/sync.mesh.test.ts +1 -1
  119. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  120. package/src/tests/sync.storage.test.ts +9 -9
  121. package/src/tests/sync.storageAsync.test.ts +7 -7
  122. package/src/tests/sync.tracking.test.ts +54 -4
  123. package/src/tests/testStorage.ts +2 -2
  124. package/src/tests/testUtils.ts +85 -6
@@ -50,6 +50,47 @@ describe("sync after the garbage collector has run", () => {
50
50
  const mapOnClient = await loadCoValueOrFail(client.node, map.id);
51
51
  expect(mapOnClient.get("hello")).toEqual("world");
52
52
 
53
+ expect(
54
+ SyncMessagesLog.getMessages({
55
+ Group: group.core,
56
+ Map: map.core,
57
+ }),
58
+ ).toMatchInlineSnapshot(`
59
+ [
60
+ "client -> server | LOAD Map sessions: empty",
61
+ "server -> storage | LOAD Map sessions: empty",
62
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
63
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
64
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
65
+ "client -> server | KNOWN Group sessions: header/3",
66
+ "client -> server | KNOWN Map sessions: header/1",
67
+ ]
68
+ `);
69
+ });
70
+
71
+ test("loading a coValue from the sync server that was removed by the garbage collector along with its owner", async () => {
72
+ const client = setupTestNode();
73
+
74
+ client.connectToSyncServer();
75
+
76
+ const group = jazzCloud.node.createGroup();
77
+ const map = group.createMap();
78
+ map.set("hello", "world", "trusting");
79
+
80
+ await map.core.waitForSync();
81
+
82
+ // force the garbage collector to run twice to remove the map and its group
83
+ jazzCloud.node.garbageCollector?.collect();
84
+ jazzCloud.node.garbageCollector?.collect();
85
+
86
+ expect(jazzCloud.node.getCoValue(group.id).isAvailable()).toBe(false);
87
+ expect(jazzCloud.node.getCoValue(map.id).isAvailable()).toBe(false);
88
+
89
+ SyncMessagesLog.clear();
90
+
91
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
92
+ expect(mapOnClient.get("hello")).toEqual("world");
93
+
53
94
  expect(
54
95
  SyncMessagesLog.getMessages({
55
96
  Group: group.core,
@@ -103,7 +144,6 @@ describe("sync after the garbage collector has run", () => {
103
144
  [
104
145
  "client -> server | CONTENT Map header: false new: After: 0 New: 1",
105
146
  "server -> storage | LOAD Map sessions: empty",
106
- "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
107
147
  "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
108
148
  "server -> client | KNOWN Map sessions: header/2",
109
149
  "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
@@ -112,41 +152,30 @@ describe("sync after the garbage collector has run", () => {
112
152
  });
113
153
 
114
154
  test("syncing a coValue that was removed by the garbage collector", async () => {
115
- const edge = setupTestNode();
116
- edge.addStorage({
117
- ourName: "edge",
118
- });
119
- edge.connectToSyncServer({
120
- syncServer: jazzCloud.node,
121
- syncServerName: "server",
122
- ourName: "edge",
123
- });
124
- edge.node.enableGarbageCollector();
125
155
  const client = setupTestNode();
126
-
127
- client.connectToSyncServer({
128
- syncServer: edge.node,
129
- syncServerName: "edge",
156
+ client.addStorage({
157
+ ourName: "client",
130
158
  });
159
+ client.node.enableGarbageCollector();
131
160
 
132
- const group = edge.node.createGroup();
133
- group.addMember("everyone", "writer");
134
-
135
- await group.core.waitForSync();
136
-
161
+ const group = client.node.createGroup();
137
162
  const map = group.createMap();
138
-
139
163
  map.set("hello", "updated", "trusting");
140
164
 
141
165
  // force the garbage collector to run before the transaction is synced
142
- edge.node.garbageCollector?.collect();
143
- expect(edge.node.getCoValue(map.id).isAvailable()).toBe(false);
166
+ client.node.garbageCollector?.collect();
167
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
144
168
 
145
169
  SyncMessagesLog.clear();
146
170
 
171
+ client.connectToSyncServer();
172
+
173
+ // Wait for unsynced coValues to be resumed and synced after connecting to server
174
+ await client.node.syncManager.waitForAllCoValuesSync();
175
+
147
176
  // The storage should work even after the coValue is unmounted, so the load should be successful
148
- const mapOnClient = await loadCoValueOrFail(client.node, map.id);
149
- expect(mapOnClient.get("hello")).toEqual("updated");
177
+ const mapOnServer = await loadCoValueOrFail(jazzCloud.node, map.id);
178
+ expect(mapOnServer.get("hello")).toEqual("updated");
150
179
 
151
180
  expect(
152
181
  SyncMessagesLog.getMessages({
@@ -155,18 +184,22 @@ describe("sync after the garbage collector has run", () => {
155
184
  }),
156
185
  ).toMatchInlineSnapshot(`
157
186
  [
158
- "client -> edge | LOAD Map sessions: empty",
159
- "edge -> storage | CONTENT Map header: true new: After: 0 New: 1",
160
- "edge -> server | CONTENT Map header: true new: After: 0 New: 1",
161
- "edge -> storage | LOAD Map sessions: empty",
162
- "storage -> edge | CONTENT Group header: true new: After: 0 New: 5",
163
- "storage -> edge | CONTENT Map header: true new: After: 0 New: 1",
164
- "edge -> client | CONTENT Group header: true new: After: 0 New: 5",
165
- "edge -> client | CONTENT Map header: true new: After: 0 New: 1",
166
- "server -> edge | KNOWN Map sessions: header/1",
187
+ "client -> server | LOAD Map sessions: empty",
188
+ "client -> server | LOAD Group sessions: header/3",
189
+ "client -> storage | CONTENT Group header: true new: After: 0 New: 3",
190
+ "client -> server | CONTENT Group header: true new: After: 0 New: 3",
191
+ "client -> storage | CONTENT Map header: true new: After: 0 New: 1",
192
+ "client -> server | CONTENT Map header: true new: After: 0 New: 1",
193
+ "server -> storage | LOAD Map sessions: empty",
194
+ "storage -> server | KNOWN Map sessions: empty",
195
+ "server -> client | KNOWN Map sessions: empty",
196
+ "server -> storage | GET_KNOWN_STATE Group",
197
+ "storage -> server | GET_KNOWN_STATE_RESULT Group sessions: empty",
198
+ "server -> client | KNOWN Group sessions: empty",
199
+ "server -> client | KNOWN Group sessions: header/3",
200
+ "server -> storage | CONTENT Group header: true new: After: 0 New: 3",
201
+ "server -> client | KNOWN Map sessions: header/1",
167
202
  "server -> storage | CONTENT Map header: true new: After: 0 New: 1",
168
- "client -> edge | KNOWN Group sessions: header/5",
169
- "client -> edge | KNOWN Map sessions: header/1",
170
203
  ]
171
204
  `);
172
205
  });
@@ -514,7 +514,7 @@ describe("loading coValues from server", () => {
514
514
  });
515
515
 
516
516
  // Makes the CoValues unavailable on the server
517
- jazzCloud.restart();
517
+ await jazzCloud.restart();
518
518
 
519
519
  const client = setupTestNode({
520
520
  connected: true,
@@ -1476,7 +1476,7 @@ describe("lazy storage load optimization", () => {
1476
1476
 
1477
1477
  // Restart the server to clear memory (keeping storage)
1478
1478
  // Now the server has no CoValues in memory, only in storage
1479
- jazzCloud.restart();
1479
+ await jazzCloud.restart();
1480
1480
  jazzCloud.node.setStorage(storage);
1481
1481
 
1482
1482
  SyncMessagesLog.clear();
@@ -1519,7 +1519,7 @@ describe("lazy storage load optimization", () => {
1519
1519
  await map.core.waitForSync();
1520
1520
 
1521
1521
  // Restart the server to clear memory (keeping storage)
1522
- jazzCloud.restart();
1522
+ await jazzCloud.restart();
1523
1523
  jazzCloud.node.setStorage(storage);
1524
1524
 
1525
1525
  SyncMessagesLog.clear();
@@ -1643,7 +1643,6 @@ describe("lazy storage load optimization", () => {
1643
1643
  [
1644
1644
  "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1645
1645
  "server -> storage | LOAD Map sessions: empty",
1646
- "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
1647
1646
  "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
1648
1647
  "server -> client | KNOWN Map sessions: header/2",
1649
1648
  "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
@@ -1702,7 +1701,6 @@ describe("lazy storage load optimization", () => {
1702
1701
  [
1703
1702
  "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1704
1703
  "server -> storage | LOAD Map sessions: empty",
1705
- "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
1706
1704
  "storage -> server | CONTENT Map header: true new: After: 0 New: 73 expectContentUntil: header/201",
1707
1705
  "server -> client | KNOWN Map sessions: header/74",
1708
1706
  "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
@@ -506,7 +506,7 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
506
506
  ]
507
507
  `);
508
508
 
509
- edge.restart();
509
+ await edge.restart();
510
510
 
511
511
  edge.connectToSyncServer({
512
512
  syncServerName: "core",
@@ -167,7 +167,7 @@ describe("peer reconciliation", () => {
167
167
 
168
168
  await map.core.waitForSync();
169
169
 
170
- jazzCloud.restart();
170
+ await jazzCloud.restart();
171
171
  SyncMessagesLog.clear();
172
172
  client.connectToSyncServer();
173
173
 
@@ -222,7 +222,7 @@ describe("peer reconciliation", () => {
222
222
 
223
223
  await map.core.waitForSync();
224
224
 
225
- jazzCloud.restart();
225
+ await jazzCloud.restart();
226
226
  SyncMessagesLog.clear();
227
227
  client.connectToSyncServer();
228
228
 
@@ -305,7 +305,7 @@ describe("peer reconciliation", () => {
305
305
 
306
306
  await map.core.waitForSync();
307
307
 
308
- jazzCloud.restart();
308
+ await jazzCloud.restart();
309
309
 
310
310
  SyncMessagesLog.clear();
311
311
  client.connectToSyncServer();
@@ -83,7 +83,7 @@ describe("client with storage syncs with server", () => {
83
83
 
84
84
  await loadCoValueOrFail(client.node, map.id);
85
85
 
86
- client.restart();
86
+ await client.restart();
87
87
 
88
88
  client.connectToSyncServer();
89
89
  client.addStorage({
@@ -167,7 +167,7 @@ describe("client with storage syncs with server", () => {
167
167
 
168
168
  await map.core.waitForSync();
169
169
 
170
- client.restart();
170
+ await client.restart();
171
171
 
172
172
  client.addStorage({
173
173
  storage,
@@ -217,7 +217,7 @@ describe("client with storage syncs with server", () => {
217
217
  branch.set("branchKey", "branchValue");
218
218
  await branch.core.waitForSync();
219
219
 
220
- client.restart();
220
+ await client.restart();
221
221
  client.addStorage({
222
222
  storage,
223
223
  });
@@ -388,7 +388,7 @@ describe("client syncs with a server with storage", () => {
388
388
 
389
389
  SyncMessagesLog.clear();
390
390
 
391
- client.restart();
391
+ await client.restart();
392
392
 
393
393
  client.connectToSyncServer({
394
394
  ourName: "client",
@@ -457,7 +457,7 @@ describe("client syncs with a server with storage", () => {
457
457
 
458
458
  expect(correctionSpy).not.toHaveBeenCalled();
459
459
 
460
- client.restart();
460
+ await client.restart();
461
461
 
462
462
  client.connectToSyncServer({
463
463
  ourName: "client",
@@ -771,7 +771,7 @@ describe("client syncs with a server with storage", () => {
771
771
 
772
772
  SyncMessagesLog.clear();
773
773
 
774
- syncServer.restart();
774
+ await syncServer.restart();
775
775
  syncServer.addStorage({
776
776
  ourName: "syncServer",
777
777
  storage,
@@ -848,7 +848,7 @@ describe("client syncs with a server with storage", () => {
848
848
  ]);
849
849
 
850
850
  // Restart to load from storage
851
- client.restart();
851
+ await client.restart();
852
852
  client.addStorage({ storage });
853
853
 
854
854
  // Load all maps concurrently from storage
@@ -892,7 +892,7 @@ describe("client syncs with a server with storage", () => {
892
892
  SyncMessagesLog.clear();
893
893
 
894
894
  // Restart client with storage
895
- client.restart();
895
+ await client.restart();
896
896
  client.connectToSyncServer();
897
897
  client.addStorage({ storage });
898
898
 
@@ -985,7 +985,7 @@ describe("client syncs with a server with storage", () => {
985
985
 
986
986
  SyncMessagesLog.clear();
987
987
 
988
- syncServer.restart();
988
+ await syncServer.restart();
989
989
  syncServer.addStorage({
990
990
  ourName: "syncServer",
991
991
  storage,
@@ -72,7 +72,7 @@ describe("client with storage syncs with server", () => {
72
72
  const firstLoad = await loadCoValueOrFail(client.node, map.id);
73
73
  await firstLoad.core.waitForSync(); // Need to wait for sync with storage
74
74
 
75
- client.restart();
75
+ await client.restart();
76
76
 
77
77
  client.connectToSyncServer();
78
78
  client.addStorage({
@@ -311,7 +311,7 @@ describe("client syncs with a server with storage", () => {
311
311
 
312
312
  SyncMessagesLog.clear();
313
313
 
314
- client.restart();
314
+ await client.restart();
315
315
 
316
316
  client.connectToSyncServer({
317
317
  ourName: "client",
@@ -406,7 +406,7 @@ describe("client syncs with a server with storage", () => {
406
406
  const largeMapContent =
407
407
  largeMap.core.newContentSince(undefined)?.slice(0, 4) ?? [];
408
408
 
409
- client.restart();
409
+ await client.restart();
410
410
 
411
411
  const newSyncServer = setupTestNode({
412
412
  isSyncServer: true,
@@ -518,7 +518,7 @@ describe("client syncs with a server with storage", () => {
518
518
 
519
519
  expect(correctionSpy).not.toHaveBeenCalled();
520
520
 
521
- client.restart();
521
+ await client.restart();
522
522
 
523
523
  client.connectToSyncServer({
524
524
  ourName: "client",
@@ -566,14 +566,14 @@ describe("client syncs with a server with storage", () => {
566
566
 
567
567
  await largeMap.core.waitForSync();
568
568
 
569
- server.restart();
569
+ await server.restart();
570
570
 
571
571
  server.addStorage({
572
572
  ourName: "server",
573
573
  storage: serverStorage,
574
574
  });
575
575
 
576
- client.restart();
576
+ await client.restart();
577
577
 
578
578
  client.connectToSyncServer({
579
579
  ourName: "client",
@@ -703,7 +703,7 @@ describe("client syncs with a server with storage", () => {
703
703
 
704
704
  SyncMessagesLog.clear();
705
705
 
706
- syncServer.restart();
706
+ await syncServer.restart();
707
707
  syncServer.addStorage({
708
708
  ourName: "syncServer",
709
709
  storage,
@@ -73,6 +73,56 @@ describe("coValue sync state tracking", () => {
73
73
  expect(unsyncedTracker.has(map.id)).toBe(true);
74
74
  });
75
75
 
76
+ test("coValue is marked as synced after connecting to a server peer", async () => {
77
+ const client = setupTestNode({ connected: false });
78
+
79
+ const group = client.node.createGroup();
80
+ const map = group.createMap();
81
+ map.set("key", "value");
82
+
83
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
84
+
85
+ const unsyncedTracker = client.node.syncManager.unsyncedTracker;
86
+ expect(unsyncedTracker.has(map.id)).toBe(true);
87
+
88
+ client.connectToSyncServer();
89
+
90
+ const serverPeer =
91
+ client.node.syncManager.peers[jazzCloud.node.currentSessionID]!;
92
+ await waitFor(() =>
93
+ client.node.syncManager.syncState.isSynced(serverPeer, map.id),
94
+ );
95
+ expect(unsyncedTracker.has(map.id)).toBe(false);
96
+ });
97
+
98
+ test("coValue is NOT marked as synced after uploading it to a client peer", async () => {
99
+ const client = setupTestNode({ connected: false });
100
+
101
+ const group = client.node.createGroup();
102
+ const map = group.createMap();
103
+ map.set("key", "value");
104
+
105
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
106
+
107
+ const unsyncedTracker = client.node.syncManager.unsyncedTracker;
108
+ expect(unsyncedTracker.has(map.id)).toBe(true);
109
+
110
+ const anotherClient = setupTestNode({ connected: false });
111
+ anotherClient.connectToSyncServer({
112
+ syncServer: client.node,
113
+ });
114
+
115
+ // Load the coValue from the client to trigger sync between the server and the client
116
+ await anotherClient.node.loadCoValueCore(map.id);
117
+
118
+ const clientPeer =
119
+ client.node.syncManager.peers[anotherClient.node.currentSessionID]!;
120
+ await waitFor(() =>
121
+ client.node.syncManager.syncState.isSynced(clientPeer, map.id),
122
+ );
123
+ expect(unsyncedTracker.has(map.id)).toBe(true);
124
+ });
125
+
76
126
  test("only tracks sync state for persistent servers peers", async () => {
77
127
  const { node: client, connectToSyncServer } = setupTestNode({
78
128
  connected: true,
@@ -262,7 +312,7 @@ describe("sync resumption", () => {
262
312
  expect(unsyncedTracker.has(map.id)).toBe(true);
263
313
  expect(await getUnsyncedCoValueIDsFromStorage()).toHaveLength(2);
264
314
 
265
- client.restart();
315
+ await client.restart();
266
316
  client.addStorage({ storage });
267
317
  const { peerState: serverPeerState } = client.connectToSyncServer();
268
318
 
@@ -300,7 +350,7 @@ describe("sync resumption", () => {
300
350
  }
301
351
  expect(await getUnsyncedCoValueIDsFromStorage()).toHaveLength(101);
302
352
 
303
- client.restart();
353
+ await client.restart();
304
354
  client.addStorage({ storage });
305
355
  const { peerState: serverPeerState } = client.connectToSyncServer();
306
356
 
@@ -339,7 +389,7 @@ describe("sync resumption", () => {
339
389
 
340
390
  expect(await getUnsyncedCoValueIDsFromStorage()).toHaveLength(2);
341
391
 
342
- client.restart();
392
+ await client.restart();
343
393
  client.addStorage({ storage });
344
394
  const newSyncServer = setupTestNode({ isSyncServer: true });
345
395
  const { peerState: newServerPeerState } = client.connectToSyncServer({
@@ -377,7 +427,7 @@ describe("sync resumption", () => {
377
427
  expect(unsyncedCoValueIDs).toContain(map.id);
378
428
  expect(unsyncedCoValueIDs).toContain(group.id);
379
429
 
380
- client.restart();
430
+ await client.restart();
381
431
  client.addStorage({ storage });
382
432
  const newPeer = setupTestNode({ isSyncServer: true });
383
433
  client.connectToSyncServer({
@@ -104,8 +104,8 @@ export async function createAsyncStorage({
104
104
  new LibSQLSqliteAsyncDriver(getDbPath(filename)),
105
105
  );
106
106
 
107
- onTestFinished(() => {
108
- storage.close();
107
+ onTestFinished(async () => {
108
+ await storage.close();
109
109
  });
110
110
 
111
111
  trackStorageMessages(storage, nodeName, storageName);
@@ -13,6 +13,8 @@ import {
13
13
  AnyRawCoValue,
14
14
  type CoID,
15
15
  type CoValueCore,
16
+ MessageChannelLike,
17
+ MessagePortLike,
16
18
  type RawAccount,
17
19
  RawAccountID,
18
20
  RawCoMap,
@@ -187,8 +189,8 @@ export function newGroupHighLevel() {
187
189
 
188
190
  const group = node.createGroup();
189
191
 
190
- onTestFinished(() => {
191
- node.gracefulShutdown();
192
+ onTestFinished(async () => {
193
+ await node.gracefulShutdown();
192
194
  });
193
195
  return { admin, node, group };
194
196
  }
@@ -517,8 +519,8 @@ export function setupTestNode(
517
519
  connectToSyncServer();
518
520
  }
519
521
 
520
- onTestFinished(() => {
521
- node.gracefulShutdown();
522
+ onTestFinished(async () => {
523
+ await node.gracefulShutdown();
522
524
  });
523
525
 
524
526
  const ctx = {
@@ -526,8 +528,8 @@ export function setupTestNode(
526
528
  connectToSyncServer,
527
529
  addStorage,
528
530
  addAsyncStorage,
529
- restart: () => {
530
- node.gracefulShutdown();
531
+ restart: async () => {
532
+ await node.gracefulShutdown();
531
533
  ctx.node = node = new LocalNode(
532
534
  admin.agentSecret,
533
535
  session,
@@ -811,3 +813,80 @@ export function fillCoMapWithLargeData(map: RawCoMap) {
811
813
 
812
814
  return map;
813
815
  }
816
+
817
+ // ============================================================================
818
+ // MessageChannel Test Helpers
819
+ // ============================================================================
820
+
821
+ /**
822
+ * Type guard to check if a message is a SyncMessage.
823
+ */
824
+ export function isSyncMessage(msg: unknown): msg is SyncMessage {
825
+ return (
826
+ typeof msg === "object" &&
827
+ msg !== null &&
828
+ "action" in msg &&
829
+ typeof (msg as { action: unknown }).action === "string"
830
+ );
831
+ }
832
+
833
+ /**
834
+ * Creates a MessageChannel that logs all sync messages exchanged between ports.
835
+ * Similar to connectedPeersWithMessagesTracking but for MessageChannel.
836
+ */
837
+ export function createTrackedMessageChannel(opts: {
838
+ port1Name?: string;
839
+ port2Name?: string;
840
+ }) {
841
+ const { port1, port2 } = new MessageChannel();
842
+ const port1Name = opts.port1Name ?? "port1";
843
+ const port2Name = opts.port2Name ?? "port2";
844
+
845
+ // Wrap port1.postMessage to log messages
846
+ const originalPort1PostMessage = port1.postMessage.bind(port1);
847
+ port1.postMessage = (message, transfer) => {
848
+ if (isSyncMessage(message)) {
849
+ SyncMessagesLog.add({
850
+ from: port1Name,
851
+ to: port2Name,
852
+ msg: message,
853
+ });
854
+ }
855
+
856
+ originalPort1PostMessage(message, transfer);
857
+ };
858
+
859
+ // Wrap port2.postMessage to log messages
860
+ const originalPort2PostMessage = port2.postMessage.bind(port2);
861
+ port2.postMessage = (message, transfer) => {
862
+ if (isSyncMessage(message)) {
863
+ SyncMessagesLog.add({
864
+ from: port2Name,
865
+ to: port1Name,
866
+ msg: message,
867
+ });
868
+ }
869
+
870
+ originalPort2PostMessage(message, transfer);
871
+ };
872
+
873
+ return { port1, port2 };
874
+ }
875
+
876
+ /**
877
+ * Creates a mock worker target that simulates receiving a port
878
+ * and calling a callback with the received port (simulating a connection handshake).
879
+ */
880
+ export function createMockWorkerWithAccept(
881
+ onPortReceived: (port: MessagePortLike) => Promise<void>,
882
+ ) {
883
+ return {
884
+ postMessage: vi.fn().mockImplementation((data, transfer) => {
885
+ if (data?.type === "jazz:port" && transfer?.[0]) {
886
+ const port = transfer[0] as MessagePortLike;
887
+ // Simulate the worker receiving the port and calling accept
888
+ onPortReceived(port);
889
+ }
890
+ }),
891
+ };
892
+ }