cojson 0.20.9 → 0.20.10

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 (169) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/PeerState.d.ts +2 -2
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +3 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/StorageReconciliationAckTracker.d.ts +14 -0
  8. package/dist/StorageReconciliationAckTracker.d.ts.map +1 -0
  9. package/dist/StorageReconciliationAckTracker.js +72 -0
  10. package/dist/StorageReconciliationAckTracker.js.map +1 -0
  11. package/dist/SyncStateManager.js +2 -2
  12. package/dist/SyncStateManager.js.map +1 -1
  13. package/dist/coValueCore/coValueCore.d.ts +2 -1
  14. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  15. package/dist/coValueCore/coValueCore.js +43 -10
  16. package/dist/coValueCore/coValueCore.js.map +1 -1
  17. package/dist/coValues/coList.d.ts +2 -0
  18. package/dist/coValues/coList.d.ts.map +1 -1
  19. package/dist/coValues/coList.js +28 -0
  20. package/dist/coValues/coList.js.map +1 -1
  21. package/dist/coValues/group.d.ts +4 -1
  22. package/dist/coValues/group.d.ts.map +1 -1
  23. package/dist/coValues/group.js +15 -1
  24. package/dist/coValues/group.js.map +1 -1
  25. package/dist/config.d.ts +8 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +14 -0
  28. package/dist/config.js.map +1 -1
  29. package/dist/exports.d.ts +9 -1
  30. package/dist/exports.d.ts.map +1 -1
  31. package/dist/exports.js +5 -1
  32. package/dist/exports.js.map +1 -1
  33. package/dist/localNode.d.ts +7 -3
  34. package/dist/localNode.d.ts.map +1 -1
  35. package/dist/localNode.js +13 -5
  36. package/dist/localNode.js.map +1 -1
  37. package/dist/permissions.d.ts +1 -0
  38. package/dist/permissions.d.ts.map +1 -1
  39. package/dist/queue/LinkedList.d.ts +2 -0
  40. package/dist/queue/LinkedList.d.ts.map +1 -1
  41. package/dist/queue/LinkedList.js +7 -0
  42. package/dist/queue/LinkedList.js.map +1 -1
  43. package/dist/queue/OutgoingLoadQueue.d.ts +4 -1
  44. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -1
  45. package/dist/queue/OutgoingLoadQueue.js +41 -13
  46. package/dist/queue/OutgoingLoadQueue.js.map +1 -1
  47. package/dist/queue/PriorityBasedMessageQueue.d.ts +1 -0
  48. package/dist/queue/PriorityBasedMessageQueue.d.ts.map +1 -1
  49. package/dist/queue/PriorityBasedMessageQueue.js +11 -1
  50. package/dist/queue/PriorityBasedMessageQueue.js.map +1 -1
  51. package/dist/storage/knownState.d.ts +2 -0
  52. package/dist/storage/knownState.d.ts.map +1 -1
  53. package/dist/storage/knownState.js +11 -0
  54. package/dist/storage/knownState.js.map +1 -1
  55. package/dist/storage/sqlite/client.d.ts +10 -1
  56. package/dist/storage/sqlite/client.d.ts.map +1 -1
  57. package/dist/storage/sqlite/client.js +84 -0
  58. package/dist/storage/sqlite/client.js.map +1 -1
  59. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  60. package/dist/storage/sqlite/sqliteMigrations.js +11 -0
  61. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  62. package/dist/storage/sqliteAsync/client.d.ts +10 -1
  63. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  64. package/dist/storage/sqliteAsync/client.js +86 -0
  65. package/dist/storage/sqliteAsync/client.js.map +1 -1
  66. package/dist/storage/storageAsync.d.ts +9 -2
  67. package/dist/storage/storageAsync.d.ts.map +1 -1
  68. package/dist/storage/storageAsync.js +19 -0
  69. package/dist/storage/storageAsync.js.map +1 -1
  70. package/dist/storage/storageSync.d.ts +9 -2
  71. package/dist/storage/storageSync.d.ts.map +1 -1
  72. package/dist/storage/storageSync.js +20 -13
  73. package/dist/storage/storageSync.js.map +1 -1
  74. package/dist/storage/types.d.ts +64 -0
  75. package/dist/storage/types.d.ts.map +1 -1
  76. package/dist/storage/types.js.map +1 -1
  77. package/dist/sync.d.ts +44 -2
  78. package/dist/sync.d.ts.map +1 -1
  79. package/dist/sync.js +268 -44
  80. package/dist/sync.js.map +1 -1
  81. package/dist/tests/OutgoingLoadQueue.test.js +137 -39
  82. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -1
  83. package/dist/tests/SQLiteClientAsync.test.js +1 -1
  84. package/dist/tests/SQLiteClientAsync.test.js.map +1 -1
  85. package/dist/tests/StorageApiAsync.test.js +138 -0
  86. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  87. package/dist/tests/StorageApiSync.test.js +154 -0
  88. package/dist/tests/StorageApiSync.test.js.map +1 -1
  89. package/dist/tests/StorageReconciliationAckTracker.test.d.ts +2 -0
  90. package/dist/tests/StorageReconciliationAckTracker.test.d.ts.map +1 -0
  91. package/dist/tests/StorageReconciliationAckTracker.test.js +74 -0
  92. package/dist/tests/StorageReconciliationAckTracker.test.js.map +1 -0
  93. package/dist/tests/SyncStateManager.test.js +18 -0
  94. package/dist/tests/SyncStateManager.test.js.map +1 -1
  95. package/dist/tests/coList.test.js +112 -1
  96. package/dist/tests/coList.test.js.map +1 -1
  97. package/dist/tests/coValueCore.loadFromStorage.test.js +36 -0
  98. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  99. package/dist/tests/group.test.js +44 -0
  100. package/dist/tests/group.test.js.map +1 -1
  101. package/dist/tests/knownState.lazyLoading.test.js +6 -0
  102. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  103. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  104. package/dist/tests/messagesTestUtils.js +4 -0
  105. package/dist/tests/messagesTestUtils.js.map +1 -1
  106. package/dist/tests/sync.concurrentLoad.test.js +333 -1
  107. package/dist/tests/sync.concurrentLoad.test.js.map +1 -1
  108. package/dist/tests/sync.garbageCollection.test.js +4 -0
  109. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  110. package/dist/tests/sync.load.test.js +19 -0
  111. package/dist/tests/sync.load.test.js.map +1 -1
  112. package/dist/tests/sync.mesh.test.js +1 -0
  113. package/dist/tests/sync.mesh.test.js.map +1 -1
  114. package/dist/tests/sync.multipleServers.test.js +41 -3
  115. package/dist/tests/sync.multipleServers.test.js.map +1 -1
  116. package/dist/tests/sync.storage.test.js +2 -0
  117. package/dist/tests/sync.storage.test.js.map +1 -1
  118. package/dist/tests/sync.storageAsync.test.js +1 -0
  119. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  120. package/dist/tests/sync.storageReconciliation.test.d.ts +2 -0
  121. package/dist/tests/sync.storageReconciliation.test.d.ts.map +1 -0
  122. package/dist/tests/sync.storageReconciliation.test.js +501 -0
  123. package/dist/tests/sync.storageReconciliation.test.js.map +1 -0
  124. package/dist/tests/testUtils.d.ts +1 -0
  125. package/dist/tests/testUtils.d.ts.map +1 -1
  126. package/dist/tests/testUtils.js +3 -2
  127. package/dist/tests/testUtils.js.map +1 -1
  128. package/package.json +4 -4
  129. package/src/PeerState.ts +10 -3
  130. package/src/StorageReconciliationAckTracker.ts +83 -0
  131. package/src/SyncStateManager.ts +3 -3
  132. package/src/coValueCore/coValueCore.ts +47 -16
  133. package/src/coValues/coList.ts +23 -0
  134. package/src/coValues/group.ts +18 -0
  135. package/src/config.ts +18 -0
  136. package/src/exports.ts +8 -0
  137. package/src/localNode.ts +18 -0
  138. package/src/permissions.ts +1 -1
  139. package/src/queue/LinkedList.ts +10 -0
  140. package/src/queue/OutgoingLoadQueue.ts +57 -15
  141. package/src/queue/PriorityBasedMessageQueue.ts +15 -1
  142. package/src/storage/knownState.ts +14 -0
  143. package/src/storage/sqlite/client.ts +128 -0
  144. package/src/storage/sqlite/sqliteMigrations.ts +11 -0
  145. package/src/storage/sqliteAsync/client.ts +139 -0
  146. package/src/storage/storageAsync.ts +37 -0
  147. package/src/storage/storageSync.ts +41 -16
  148. package/src/storage/types.ts +110 -0
  149. package/src/sync.ts +311 -14
  150. package/src/tests/OutgoingLoadQueue.test.ts +226 -59
  151. package/src/tests/SQLiteClientAsync.test.ts +1 -1
  152. package/src/tests/StorageApiAsync.test.ts +161 -1
  153. package/src/tests/StorageApiSync.test.ts +176 -0
  154. package/src/tests/StorageReconciliationAckTracker.test.ts +99 -0
  155. package/src/tests/SyncStateManager.test.ts +25 -0
  156. package/src/tests/coList.test.ts +138 -0
  157. package/src/tests/coValueCore.loadFromStorage.test.ts +72 -1
  158. package/src/tests/group.test.ts +87 -0
  159. package/src/tests/knownState.lazyLoading.test.ts +36 -1
  160. package/src/tests/messagesTestUtils.ts +4 -0
  161. package/src/tests/sync.concurrentLoad.test.ts +491 -0
  162. package/src/tests/sync.garbageCollection.test.ts +4 -0
  163. package/src/tests/sync.load.test.ts +26 -0
  164. package/src/tests/sync.mesh.test.ts +1 -0
  165. package/src/tests/sync.multipleServers.test.ts +60 -2
  166. package/src/tests/sync.storage.test.ts +2 -0
  167. package/src/tests/sync.storageAsync.test.ts +1 -0
  168. package/src/tests/sync.storageReconciliation.test.ts +697 -0
  169. package/src/tests/testUtils.ts +10 -1
@@ -0,0 +1,697 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import {
3
+ cojsonInternals,
4
+ LocalNode,
5
+ RawCoMap,
6
+ SessionID,
7
+ StorageReconciliationAcquireResult,
8
+ } from "../exports";
9
+ import {
10
+ SyncMessagesLog,
11
+ TEST_NODE_CONFIG,
12
+ setupTestNode,
13
+ waitFor,
14
+ } from "./testUtils";
15
+ import {
16
+ GARBAGE_COLLECTOR_CONFIG,
17
+ setGarbageCollectorMaxAge,
18
+ setStorageReconciliationBatchSize,
19
+ setStorageReconciliationInterval,
20
+ setStorageReconciliationLockTTL,
21
+ STORAGE_RECONCILIATION_CONFIG,
22
+ } from "../config";
23
+
24
+ // We want to simulate a real world communication that happens asynchronously
25
+ TEST_NODE_CONFIG.withAsyncPeers = true;
26
+
27
+ let jazzCloud: ReturnType<typeof setupTestNode>;
28
+ const originalBatchSize = STORAGE_RECONCILIATION_CONFIG.BATCH_SIZE;
29
+ const originalLockTTL = STORAGE_RECONCILIATION_CONFIG.LOCK_TTL_MS;
30
+ const originalInterval =
31
+ STORAGE_RECONCILIATION_CONFIG.RECONCILIATION_INTERVAL_MS;
32
+
33
+ beforeEach(async () => {
34
+ SyncMessagesLog.clear();
35
+ jazzCloud = setupTestNode({ isSyncServer: true });
36
+ setStorageReconciliationBatchSize(originalBatchSize);
37
+ setStorageReconciliationLockTTL(originalLockTTL);
38
+ setStorageReconciliationInterval(originalInterval);
39
+ });
40
+
41
+ describe("full storage reconciliation", () => {
42
+ test("startStorageReconciliation sends 'reconcile' message, server responds with 'known' messages for missing CoValues", async () => {
43
+ const client = setupTestNode();
44
+ const { storage } = client.addStorage();
45
+
46
+ const group = client.node.createGroup();
47
+ const map = group.createMap();
48
+ map.set("hello", "world", "trusting");
49
+
50
+ await map.core.waitForSync();
51
+
52
+ const anotherClient = setupTestNode();
53
+ anotherClient.addStorage({ storage });
54
+ anotherClient.connectToSyncServer({
55
+ persistent: true,
56
+ skipReconciliation: true,
57
+ });
58
+
59
+ SyncMessagesLog.clear();
60
+
61
+ const serverPeer = Object.values(anotherClient.node.syncManager.peers).find(
62
+ (p) => p.role === "server" && p.persistent,
63
+ )!;
64
+ anotherClient.node.syncManager.startStorageReconciliation(serverPeer);
65
+
66
+ await waitForStorageReconciliationBatchAck(anotherClient.node);
67
+
68
+ const messages = SyncMessagesLog.getMessages({
69
+ Group: group.core,
70
+ Map: map.core,
71
+ });
72
+ expect(messages).toMatchInlineSnapshot(`
73
+ [
74
+ "client -> storage | GET_KNOWN_STATE Group",
75
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
76
+ "client -> storage | GET_KNOWN_STATE Map",
77
+ "storage -> client | GET_KNOWN_STATE_RESULT Map sessions: header/1",
78
+ "client -> server | RECONCILE",
79
+ "server -> client | LOAD Group sessions: empty",
80
+ "server -> client | LOAD Map sessions: empty",
81
+ "server -> client | RECONCILE_ACK",
82
+ "client -> storage | LOAD Group sessions: empty",
83
+ "storage -> client | CONTENT Group header: true new: After: 0 New: 4",
84
+ "client -> server | CONTENT Group header: true new: After: 0 New: 4",
85
+ "client -> server | KNOWN Group sessions: header/4",
86
+ "client -> storage | LOAD Map sessions: empty",
87
+ "storage -> client | CONTENT Map header: true new: After: 0 New: 1",
88
+ "client -> server | CONTENT Map header: true new: After: 0 New: 1",
89
+ "client -> server | KNOWN Map sessions: header/1",
90
+ "server -> client | KNOWN Group sessions: header/4",
91
+ "server -> client | KNOWN Map sessions: header/1",
92
+ ]
93
+ `);
94
+ });
95
+
96
+ test("startStorageReconciliation sends 'reconcile' message, server responds with 'known' messages for outdated CoValues", async () => {
97
+ const client = setupTestNode();
98
+ const { storage } = client.addStorage();
99
+ client.connectToSyncServer({ persistent: true });
100
+
101
+ const group = client.node.createGroup();
102
+ const map = group.createMap();
103
+ map.set("hello", "world", "trusting");
104
+
105
+ await map.core.waitForSync();
106
+
107
+ map.set("hello", "world2", "trusting");
108
+
109
+ // Restart the client before the latest change is synced to the sync server
110
+ await client.restart();
111
+ client.addStorage({ storage });
112
+ client.connectToSyncServer({ persistent: true, skipReconciliation: true });
113
+
114
+ SyncMessagesLog.clear();
115
+
116
+ const serverPeer = Object.values(client.node.syncManager.peers).find(
117
+ (p) => p.role === "server" && p.persistent,
118
+ )!;
119
+ client.node.syncManager.startStorageReconciliation(serverPeer);
120
+
121
+ await waitForStorageReconciliationBatchAck(client.node);
122
+
123
+ const messages = SyncMessagesLog.getMessages({
124
+ Group: group.core,
125
+ Map: map.core,
126
+ });
127
+ expect(messages).toMatchInlineSnapshot(`
128
+ [
129
+ "client -> storage | GET_KNOWN_STATE Group",
130
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
131
+ "client -> storage | GET_KNOWN_STATE Map",
132
+ "storage -> client | GET_KNOWN_STATE_RESULT Map sessions: header/2",
133
+ "client -> server | RECONCILE",
134
+ "server -> client | LOAD Map sessions: header/1",
135
+ "server -> client | RECONCILE_ACK",
136
+ "client -> storage | GET_KNOWN_STATE Map",
137
+ "storage -> client | GET_KNOWN_STATE_RESULT Map sessions: header/2",
138
+ "client -> storage | LOAD Map sessions: empty",
139
+ "storage -> client | CONTENT Group header: true new: After: 0 New: 4",
140
+ "client -> server | LOAD Group sessions: header/4",
141
+ "storage -> client | CONTENT Map header: true new: After: 0 New: 2",
142
+ "client -> server | CONTENT Map header: false new: After: 1 New: 1",
143
+ "client -> server | KNOWN Map sessions: header/2",
144
+ "server -> client | KNOWN Group sessions: header/4",
145
+ "server -> client | KNOWN Map sessions: header/2",
146
+ ]
147
+ `);
148
+ });
149
+
150
+ test("pendingReconciliationAck is cleared when 'reconcile-ack' is received", async () => {
151
+ const client = setupTestNode();
152
+ const { storage } = client.addStorage();
153
+
154
+ const group = client.node.createGroup();
155
+ const map = group.createMap();
156
+ map.set("hello", "world", "trusting");
157
+
158
+ await map.core.waitForSync();
159
+
160
+ const anotherClient = setupTestNode();
161
+ anotherClient.addStorage({ storage });
162
+ anotherClient.connectToSyncServer({
163
+ persistent: true,
164
+ skipReconciliation: true,
165
+ });
166
+
167
+ const serverPeer = Object.values(anotherClient.node.syncManager.peers).find(
168
+ (p) => p.role === "server" && p.persistent,
169
+ )!;
170
+ anotherClient.node.syncManager.startStorageReconciliation(serverPeer);
171
+
172
+ expect(
173
+ anotherClient.node.syncManager.pendingReconciliationAck.size,
174
+ ).toBeGreaterThan(0);
175
+
176
+ await waitFor(
177
+ () => anotherClient.node.syncManager.pendingReconciliationAck.size === 0,
178
+ );
179
+ });
180
+
181
+ test("in-memory CoValues are not reconciled", async () => {
182
+ const client = setupTestNode();
183
+ const { storage } = client.addStorage();
184
+
185
+ const group = client.node.createGroup();
186
+ const map = group.createMap();
187
+ map.set("hello", "world", "trusting");
188
+
189
+ await map.core.waitForSync();
190
+
191
+ const anotherClient = setupTestNode();
192
+ anotherClient.addStorage({ storage });
193
+ anotherClient.connectToSyncServer({
194
+ persistent: true,
195
+ skipReconciliation: true,
196
+ });
197
+
198
+ const group2 = anotherClient.node.createGroup();
199
+ const map2 = group2.createMap();
200
+ map2.set("hello2", "world2", "trusting");
201
+
202
+ await map2.core.waitForSync();
203
+
204
+ SyncMessagesLog.clear();
205
+
206
+ const serverPeer = Object.values(anotherClient.node.syncManager.peers).find(
207
+ (p) => p.role === "server" && p.persistent,
208
+ )!;
209
+ anotherClient.node.syncManager.startStorageReconciliation(serverPeer);
210
+
211
+ await waitForStorageReconciliationBatchAck(anotherClient.node);
212
+
213
+ const messages = SyncMessagesLog.getMessages({
214
+ Group: group.core,
215
+ Map: map.core,
216
+ });
217
+ // In-memory CoValues are skipped
218
+ expect(messages).toMatchInlineSnapshot(`
219
+ [
220
+ "client -> storage | GET_KNOWN_STATE Group",
221
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
222
+ "client -> storage | GET_KNOWN_STATE Map",
223
+ "storage -> client | GET_KNOWN_STATE_RESULT Map sessions: header/1",
224
+ "client -> server | RECONCILE",
225
+ "server -> client | LOAD Group sessions: empty",
226
+ "server -> client | LOAD Map sessions: empty",
227
+ "server -> client | RECONCILE_ACK",
228
+ "client -> storage | LOAD Group sessions: empty",
229
+ "storage -> client | CONTENT Group header: true new: After: 0 New: 4",
230
+ "client -> server | CONTENT Group header: true new: After: 0 New: 4",
231
+ "client -> server | KNOWN Group sessions: header/4",
232
+ "client -> storage | LOAD Map sessions: empty",
233
+ "storage -> client | CONTENT Map header: true new: After: 0 New: 1",
234
+ "client -> server | CONTENT Map header: true new: After: 0 New: 1",
235
+ "client -> server | KNOWN Map sessions: header/1",
236
+ "server -> client | KNOWN Group sessions: header/4",
237
+ "server -> client | KNOWN Map sessions: header/1",
238
+ ]
239
+ `);
240
+ });
241
+
242
+ test("'reconcile' message is not sent if there are no CoValues to reconcile", async () => {
243
+ const client = setupTestNode({ connected: true });
244
+ client.addStorage();
245
+
246
+ const group = client.node.createGroup();
247
+ const map = group.createMap();
248
+ map.set("hello", "world", "trusting");
249
+
250
+ await map.core.waitForSync();
251
+
252
+ SyncMessagesLog.clear();
253
+
254
+ // CoValue is in memory, so it will be skipped
255
+ const serverPeer = Object.values(client.node.syncManager.peers).find(
256
+ (p) => p.role === "server" && p.persistent,
257
+ )!;
258
+ client.node.syncManager.startStorageReconciliation(serverPeer);
259
+
260
+ // Wait for reconciliation to complete
261
+ await new Promise((resolve) => setTimeout(resolve, 100));
262
+
263
+ expect(client.node.syncManager.pendingReconciliationAck.size).toEqual(0);
264
+ const messages = SyncMessagesLog.getMessages({
265
+ Group: group.core,
266
+ Map: map.core,
267
+ });
268
+ expect(messages).toMatchInlineSnapshot(`[]`);
269
+ });
270
+
271
+ test("sends reconcile messages for each batch, waits for reconcile-ack, then sends next batch", async () => {
272
+ setStorageReconciliationBatchSize(2);
273
+
274
+ const client = setupTestNode();
275
+ client.connectToSyncServer({ persistent: true });
276
+ const { storage } = client.addStorage();
277
+
278
+ const group = client.node.createGroup();
279
+ const maps: RawCoMap[] = [];
280
+ for (let i = 0; i < 4; i++) {
281
+ const m = group.createMap();
282
+ m.set("i", i, "trusting");
283
+ maps.push(m);
284
+ }
285
+
286
+ await Promise.all(maps.map((m) => m.core.waitForSync()));
287
+
288
+ SyncMessagesLog.clear();
289
+
290
+ const anotherClient = setupTestNode();
291
+ anotherClient.connectToSyncServer({ persistent: true });
292
+ anotherClient.addStorage({ storage });
293
+
294
+ const serverPeer = Object.values(anotherClient.node.syncManager.peers).find(
295
+ (p) => p.role === "server" && p.persistent,
296
+ )!;
297
+ await new Promise<void>((resolve) =>
298
+ anotherClient.node.syncManager.startStorageReconciliation(
299
+ serverPeer,
300
+ 0,
301
+ resolve,
302
+ ),
303
+ );
304
+
305
+ const coValueMapping = Object.fromEntries([
306
+ ["Group", group.core],
307
+ ...maps.map((m, i) => [`Map${i}`, m.core]),
308
+ ]);
309
+ const messages = SyncMessagesLog.getMessages(coValueMapping);
310
+ expect(messages).toMatchInlineSnapshot(`
311
+ [
312
+ "client -> storage | GET_KNOWN_STATE Group",
313
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
314
+ "client -> storage | GET_KNOWN_STATE Map0",
315
+ "storage -> client | GET_KNOWN_STATE_RESULT Map0 sessions: header/1",
316
+ "client -> server | RECONCILE",
317
+ "server -> client | RECONCILE_ACK",
318
+ "client -> storage | GET_KNOWN_STATE Map1",
319
+ "storage -> client | GET_KNOWN_STATE_RESULT Map1 sessions: header/1",
320
+ "client -> storage | GET_KNOWN_STATE Map2",
321
+ "storage -> client | GET_KNOWN_STATE_RESULT Map2 sessions: header/1",
322
+ "client -> server | RECONCILE",
323
+ "server -> client | RECONCILE_ACK",
324
+ "client -> storage | GET_KNOWN_STATE Map3",
325
+ "storage -> client | GET_KNOWN_STATE_RESULT Map3 sessions: header/1",
326
+ "client -> server | RECONCILE",
327
+ "server -> client | RECONCILE_ACK",
328
+ ]
329
+ `);
330
+ });
331
+
332
+ test("aborts reconciliation when peer disconnects during wait for reconcile-ack", async () => {
333
+ const client = setupTestNode();
334
+ const { storage } = client.addStorage();
335
+
336
+ const group = client.node.createGroup();
337
+ const map = group.createMap();
338
+ map.set("hello", "world", "trusting");
339
+
340
+ await map.core.waitForSync();
341
+
342
+ const anotherClient = setupTestNode();
343
+ anotherClient.addStorage({ storage });
344
+ anotherClient.connectToSyncServer({
345
+ persistent: true,
346
+ skipReconciliation: true,
347
+ });
348
+
349
+ const serverPeer = Object.values(anotherClient.node.syncManager.peers).find(
350
+ (p) => p.role === "server" && p.persistent,
351
+ )!;
352
+ let reconciliationFinished = false;
353
+ anotherClient.node.syncManager.startStorageReconciliation(
354
+ serverPeer,
355
+ 0,
356
+ () => {
357
+ reconciliationFinished = true;
358
+ },
359
+ );
360
+
361
+ // Prevent "reconcile" message from being processed so that client stays in "waiting" state
362
+ const { promise, resolve } = Promise.withResolvers<void>();
363
+ const syncManager = jazzCloud.node.syncManager;
364
+ syncManager.handleReconcile = () => {
365
+ resolve();
366
+ };
367
+ await promise;
368
+
369
+ anotherClient.disconnect();
370
+
371
+ // Reconciliation should abort (peer closed) without hanging
372
+ // and clear the pending reconciliation ack
373
+ await waitFor(
374
+ () => anotherClient.node.syncManager.pendingReconciliationAck.size === 0,
375
+ );
376
+
377
+ // onComplete should NOT have been called (we aborted, did not complete)
378
+ expect(reconciliationFinished).toBe(false);
379
+ });
380
+
381
+ describe("scheduling", () => {
382
+ test("full storage reconciliation is not run if not enabled", async () => {
383
+ const client = setupTestNode();
384
+ const { storage } = client.addStorage();
385
+ client.connectToSyncServer({ persistent: true });
386
+
387
+ const group = client.node.createGroup();
388
+ const map = group.createMap();
389
+ map.set("hello", "world", "trusting");
390
+
391
+ await map.core.waitForSync();
392
+ SyncMessagesLog.clear();
393
+
394
+ const anotherClient = setupTestNode();
395
+ anotherClient.addStorage({ storage });
396
+ anotherClient.connectToSyncServer({ persistent: true });
397
+
398
+ await new Promise((resolve) => setTimeout(resolve, 100));
399
+
400
+ const messages = SyncMessagesLog.getMessages({
401
+ Group: group.core,
402
+ Map: map.core,
403
+ });
404
+ expect(messages).toMatchInlineSnapshot(`[]`);
405
+ });
406
+
407
+ test("full storage reconciliation is run when adding a new persistent server peer", async () => {
408
+ const client = setupTestNode({ enableFullStorageReconciliation: true });
409
+ const { storage } = client.addStorage();
410
+
411
+ const group = client.node.createGroup();
412
+ const map = group.createMap();
413
+ map.set("hello", "world", "trusting");
414
+
415
+ await map.core.waitForSync();
416
+
417
+ const anotherClient = setupTestNode({
418
+ enableFullStorageReconciliation: true,
419
+ });
420
+ anotherClient.addStorage({ storage });
421
+
422
+ SyncMessagesLog.clear();
423
+
424
+ // Connecting to the sync server will trigger a full storage reconciliation
425
+ anotherClient.connectToSyncServer({
426
+ persistent: true,
427
+ });
428
+
429
+ await waitForStorageReconciliationBatchAck(anotherClient.node);
430
+
431
+ const messages = SyncMessagesLog.getMessages({
432
+ Group: group.core,
433
+ Map: map.core,
434
+ });
435
+ expect(messages).toMatchInlineSnapshot(`
436
+ [
437
+ "client -> storage | GET_KNOWN_STATE Group",
438
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
439
+ "client -> storage | GET_KNOWN_STATE Map",
440
+ "storage -> client | GET_KNOWN_STATE_RESULT Map sessions: header/1",
441
+ "client -> server | RECONCILE",
442
+ "server -> client | LOAD Group sessions: empty",
443
+ "server -> client | LOAD Map sessions: empty",
444
+ "server -> client | RECONCILE_ACK",
445
+ "client -> storage | LOAD Group sessions: empty",
446
+ "storage -> client | CONTENT Group header: true new: After: 0 New: 4",
447
+ "client -> server | CONTENT Group header: true new: After: 0 New: 4",
448
+ "client -> server | KNOWN Group sessions: header/4",
449
+ "client -> storage | LOAD Map sessions: empty",
450
+ "storage -> client | CONTENT Map header: true new: After: 0 New: 1",
451
+ "client -> server | CONTENT Map header: true new: After: 0 New: 1",
452
+ "client -> server | KNOWN Map sessions: header/1",
453
+ "server -> client | KNOWN Group sessions: header/4",
454
+ "server -> client | KNOWN Map sessions: header/1",
455
+ ]
456
+ `);
457
+ });
458
+
459
+ test("reconciliation is not run again until the reconciliation interval passed", async () => {
460
+ const client = setupTestNode({ enableFullStorageReconciliation: true });
461
+ const { storage } = client.addStorage();
462
+ // Connecting to the sync server triggers full storage reconciliation
463
+ const { peer } = client.connectToSyncServer({ persistent: true });
464
+
465
+ const group = client.node.createGroup();
466
+ await group.core.waitForSync();
467
+
468
+ const storageReconciliationLock =
469
+ await new Promise<StorageReconciliationAcquireResult>((resolve) =>
470
+ storage.tryAcquireStorageReconciliationLock(
471
+ client.node.currentSessionID,
472
+ peer.id,
473
+ resolve,
474
+ ),
475
+ );
476
+ expect(storageReconciliationLock.acquired).toBe(false);
477
+ if (!storageReconciliationLock.acquired) {
478
+ expect(storageReconciliationLock.reason).toBe("not_due");
479
+ }
480
+
481
+ const anotherClient = setupTestNode({
482
+ enableFullStorageReconciliation: true,
483
+ });
484
+ anotherClient.addStorage({ storage });
485
+
486
+ SyncMessagesLog.clear();
487
+
488
+ // Since the previous storage reconciliation was run, no other will be run for 30 days
489
+ anotherClient.connectToSyncServer({ persistent: true });
490
+
491
+ await new Promise((resolve) => setTimeout(resolve, 100));
492
+
493
+ const messages = SyncMessagesLog.getMessages({
494
+ Group: group.core,
495
+ });
496
+ expect(messages).toMatchInlineSnapshot(`[]`);
497
+ });
498
+
499
+ test("reconciliation is run for the same peer after reconciliation interval passes", async () => {
500
+ cojsonInternals.setStorageReconciliationInterval(100);
501
+
502
+ const client = setupTestNode({ enableFullStorageReconciliation: true });
503
+ const { storage } = client.addStorage();
504
+ // Connecting to the sync server triggers full storage reconciliation
505
+ client.connectToSyncServer({ persistent: true });
506
+
507
+ const group = client.node.createGroup();
508
+ await group.core.waitForSync();
509
+
510
+ // Wait for the next reconciliation window to start
511
+ await new Promise((resolve) => setTimeout(resolve, 200));
512
+
513
+ const anotherClient = setupTestNode({
514
+ enableFullStorageReconciliation: true,
515
+ });
516
+ anotherClient.addStorage({ storage });
517
+
518
+ SyncMessagesLog.clear();
519
+
520
+ // Runs storage reconciliation again
521
+ anotherClient.connectToSyncServer({ persistent: true });
522
+ await waitForStorageReconciliationBatchAck(anotherClient.node);
523
+
524
+ const messages = SyncMessagesLog.getMessages({
525
+ Group: group.core,
526
+ });
527
+ expect(messages).toMatchInlineSnapshot(`
528
+ [
529
+ "client -> storage | GET_KNOWN_STATE Group",
530
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
531
+ "client -> server | RECONCILE",
532
+ "server -> client | RECONCILE_ACK",
533
+ ]
534
+ `);
535
+ });
536
+
537
+ test("if reconciliation is interrupted, it is not run again until the lock TTL expires", async () => {
538
+ cojsonInternals.setStorageReconciliationInterval(0);
539
+ cojsonInternals.setStorageReconciliationLockTTL(100);
540
+
541
+ let client = setupTestNode({ enableFullStorageReconciliation: true });
542
+ const { storage } = client.addStorage();
543
+ client.connectToSyncServer({ persistent: true });
544
+
545
+ const group = client.node.createGroup();
546
+ await group.core.waitForSync();
547
+ await client.node.gracefulShutdown();
548
+
549
+ client = setupTestNode({ enableFullStorageReconciliation: true });
550
+ client.addStorage({ storage });
551
+
552
+ // Connecting to the sync server triggers full storage reconciliation
553
+ const { peer } = client.connectToSyncServer({ persistent: true });
554
+
555
+ // Kill the node before the reconciliation completes
556
+ await client.node.gracefulShutdown();
557
+
558
+ // Try to acquire the lock in another session, fails because the lock is held by the previous node
559
+ const storageReconciliationLock =
560
+ await new Promise<StorageReconciliationAcquireResult>((resolve) =>
561
+ storage.tryAcquireStorageReconciliationLock(
562
+ "another-session-id" as SessionID,
563
+ peer.id,
564
+ resolve,
565
+ ),
566
+ );
567
+ expect(storageReconciliationLock.acquired).toBe(false);
568
+ if (!storageReconciliationLock.acquired) {
569
+ expect(storageReconciliationLock.reason).toBe("lock_held");
570
+ }
571
+
572
+ // Wait for the lock to expire
573
+ await new Promise((resolve) => setTimeout(resolve, 200));
574
+
575
+ client = setupTestNode({ enableFullStorageReconciliation: true });
576
+ client.addStorage({ storage });
577
+
578
+ SyncMessagesLog.clear();
579
+
580
+ // Runs storage reconciliation again
581
+ client.connectToSyncServer({ persistent: true });
582
+
583
+ await new Promise((resolve) => setTimeout(resolve, 100));
584
+
585
+ const messages = SyncMessagesLog.getMessages({
586
+ Group: group.core,
587
+ });
588
+ expect(messages).toMatchInlineSnapshot(`
589
+ [
590
+ "client -> storage | GET_KNOWN_STATE Group",
591
+ "storage -> client | GET_KNOWN_STATE_RESULT Group sessions: header/4",
592
+ "client -> server | RECONCILE",
593
+ "server -> client | RECONCILE_ACK",
594
+ ]
595
+ `);
596
+ });
597
+
598
+ test("after interrupted run, next acquire returns lastProcessedOffset and reconciliation resumes from that offset", async () => {
599
+ setStorageReconciliationBatchSize(1);
600
+ setStorageReconciliationLockTTL(100);
601
+ setStorageReconciliationInterval(200);
602
+
603
+ const client = setupTestNode({ enableFullStorageReconciliation: true });
604
+ const { storage } = client.addStorage();
605
+ const { peer } = client.connectToSyncServer({ persistent: true });
606
+
607
+ const group = client.node.createGroup();
608
+ const map = group.createMap();
609
+ map.set("x", "y", "trusting");
610
+ await map.core.waitForSync();
611
+ await client.node.gracefulShutdown();
612
+
613
+ // Wait for the reconciliation interval to pass
614
+ await new Promise((resolve) => setTimeout(resolve, 200));
615
+
616
+ SyncMessagesLog.clear();
617
+ const anotherClient = setupTestNode({
618
+ enableFullStorageReconciliation: true,
619
+ });
620
+ anotherClient.addStorage({ storage });
621
+ anotherClient.connectToSyncServer({ persistent: true });
622
+
623
+ const { promise, resolve } = Promise.withResolvers<void>();
624
+ const syncManager = anotherClient.node.syncManager;
625
+ const originalHandler = syncManager.handleReconcileAck.bind(syncManager);
626
+ let processReconciliationAcks = true;
627
+ syncManager.handleReconcileAck = (msg, peer) => {
628
+ if (processReconciliationAcks) {
629
+ originalHandler(msg, peer);
630
+ processReconciliationAcks = false;
631
+ resolve();
632
+ }
633
+ };
634
+ await promise;
635
+ await anotherClient.node.gracefulShutdown();
636
+
637
+ // No need to wait for the lock to expire, since it's held by the same session
638
+ const acquireResult =
639
+ await new Promise<StorageReconciliationAcquireResult>((resolve) =>
640
+ storage.tryAcquireStorageReconciliationLock(
641
+ anotherClient.node.currentSessionID,
642
+ peer.id,
643
+ resolve,
644
+ ),
645
+ );
646
+ expect(acquireResult.acquired).toBe(true);
647
+ if (acquireResult.acquired) {
648
+ expect(acquireResult.lastProcessedOffset).toBe(1);
649
+ }
650
+ });
651
+
652
+ test("after successful completion, next due run starts from the beginning", async () => {
653
+ setStorageReconciliationInterval(100);
654
+
655
+ const client = setupTestNode({ enableFullStorageReconciliation: true });
656
+ const { storage } = client.addStorage();
657
+ client.connectToSyncServer({ persistent: true });
658
+
659
+ const group = client.node.createGroup();
660
+ await group.core.waitForSync();
661
+ await client.node.gracefulShutdown();
662
+
663
+ await new Promise((resolve) => setTimeout(resolve, 500));
664
+
665
+ const anotherClient = setupTestNode({
666
+ enableFullStorageReconciliation: true,
667
+ });
668
+ anotherClient.addStorage({ storage });
669
+ const { peer } = anotherClient.connectToSyncServer({
670
+ persistent: true,
671
+ });
672
+ await waitForStorageReconciliationBatchAck(anotherClient.node);
673
+
674
+ await new Promise((resolve) => setTimeout(resolve, 500));
675
+
676
+ const acquireResult =
677
+ await new Promise<StorageReconciliationAcquireResult>((resolve) =>
678
+ storage.tryAcquireStorageReconciliationLock(
679
+ anotherClient.node.currentSessionID,
680
+ peer.id,
681
+ resolve,
682
+ ),
683
+ );
684
+ expect(acquireResult.acquired).toBe(true);
685
+ if (acquireResult.acquired) {
686
+ expect(acquireResult.lastProcessedOffset).toBe(0);
687
+ }
688
+ });
689
+ });
690
+ });
691
+
692
+ function waitForStorageReconciliationBatchAck(node: LocalNode): Promise<void> {
693
+ const pendingReconciliationAck = node.syncManager.pendingReconciliationAck;
694
+ expect(pendingReconciliationAck.size).toBeGreaterThan(0);
695
+
696
+ return waitFor(() => pendingReconciliationAck.size === 0);
697
+ }