cojson 0.20.9 → 0.20.11

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