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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +20 -0
- package/dist/PeerState.d.ts +2 -2
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/PeerState.js +3 -3
- package/dist/PeerState.js.map +1 -1
- package/dist/StorageReconciliationAckTracker.d.ts +14 -0
- package/dist/StorageReconciliationAckTracker.d.ts.map +1 -0
- package/dist/StorageReconciliationAckTracker.js +72 -0
- package/dist/StorageReconciliationAckTracker.js.map +1 -0
- package/dist/SyncStateManager.js +2 -2
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +2 -1
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +43 -10
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/coList.d.ts +2 -0
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +28 -0
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/group.d.ts +4 -1
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +15 -1
- package/dist/coValues/group.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +9 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +5 -1
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +7 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +13 -5
- package/dist/localNode.js.map +1 -1
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/queue/LinkedList.d.ts +2 -0
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js +7 -0
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/OutgoingLoadQueue.d.ts +4 -1
- package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -1
- package/dist/queue/OutgoingLoadQueue.js +41 -13
- package/dist/queue/OutgoingLoadQueue.js.map +1 -1
- package/dist/queue/PriorityBasedMessageQueue.d.ts +1 -0
- package/dist/queue/PriorityBasedMessageQueue.d.ts.map +1 -1
- package/dist/queue/PriorityBasedMessageQueue.js +11 -1
- package/dist/queue/PriorityBasedMessageQueue.js.map +1 -1
- package/dist/storage/knownState.d.ts +2 -0
- package/dist/storage/knownState.d.ts.map +1 -1
- package/dist/storage/knownState.js +11 -0
- package/dist/storage/knownState.js.map +1 -1
- package/dist/storage/sqlite/client.d.ts +10 -1
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +84 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
- package/dist/storage/sqlite/sqliteMigrations.js +11 -0
- package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +10 -1
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +86 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +9 -2
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +19 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +9 -2
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +20 -13
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +64 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/sync.d.ts +44 -2
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +268 -44
- package/dist/sync.js.map +1 -1
- package/dist/tests/OutgoingLoadQueue.test.js +137 -39
- package/dist/tests/OutgoingLoadQueue.test.js.map +1 -1
- package/dist/tests/SQLiteClientAsync.test.js +1 -1
- package/dist/tests/SQLiteClientAsync.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +138 -0
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +154 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/StorageReconciliationAckTracker.test.d.ts +2 -0
- package/dist/tests/StorageReconciliationAckTracker.test.d.ts.map +1 -0
- package/dist/tests/StorageReconciliationAckTracker.test.js +74 -0
- package/dist/tests/StorageReconciliationAckTracker.test.js.map +1 -0
- package/dist/tests/SyncStateManager.test.js +18 -0
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coList.test.js +112 -1
- package/dist/tests/coList.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +36 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/group.test.js +44 -0
- package/dist/tests/group.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +6 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
- package/dist/tests/messagesTestUtils.d.ts.map +1 -1
- package/dist/tests/messagesTestUtils.js +4 -0
- package/dist/tests/messagesTestUtils.js.map +1 -1
- package/dist/tests/sync.concurrentLoad.test.js +333 -1
- package/dist/tests/sync.concurrentLoad.test.js.map +1 -1
- package/dist/tests/sync.garbageCollection.test.js +4 -0
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +19 -0
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +1 -0
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.multipleServers.test.js +41 -3
- package/dist/tests/sync.multipleServers.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +2 -0
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +1 -0
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.storageReconciliation.test.d.ts +2 -0
- package/dist/tests/sync.storageReconciliation.test.d.ts.map +1 -0
- package/dist/tests/sync.storageReconciliation.test.js +501 -0
- package/dist/tests/sync.storageReconciliation.test.js.map +1 -0
- package/dist/tests/testUtils.d.ts +1 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +3 -2
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/PeerState.ts +10 -3
- package/src/StorageReconciliationAckTracker.ts +83 -0
- package/src/SyncStateManager.ts +3 -3
- package/src/coValueCore/coValueCore.ts +47 -16
- package/src/coValues/coList.ts +23 -0
- package/src/coValues/group.ts +18 -0
- package/src/config.ts +18 -0
- package/src/exports.ts +8 -0
- package/src/localNode.ts +18 -0
- package/src/permissions.ts +1 -1
- package/src/queue/LinkedList.ts +10 -0
- package/src/queue/OutgoingLoadQueue.ts +57 -15
- package/src/queue/PriorityBasedMessageQueue.ts +15 -1
- package/src/storage/knownState.ts +14 -0
- package/src/storage/sqlite/client.ts +128 -0
- package/src/storage/sqlite/sqliteMigrations.ts +11 -0
- package/src/storage/sqliteAsync/client.ts +139 -0
- package/src/storage/storageAsync.ts +37 -0
- package/src/storage/storageSync.ts +41 -16
- package/src/storage/types.ts +110 -0
- package/src/sync.ts +311 -14
- package/src/tests/OutgoingLoadQueue.test.ts +226 -59
- package/src/tests/SQLiteClientAsync.test.ts +1 -1
- package/src/tests/StorageApiAsync.test.ts +161 -1
- package/src/tests/StorageApiSync.test.ts +176 -0
- package/src/tests/StorageReconciliationAckTracker.test.ts +99 -0
- package/src/tests/SyncStateManager.test.ts +25 -0
- package/src/tests/coList.test.ts +138 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +72 -1
- package/src/tests/group.test.ts +87 -0
- package/src/tests/knownState.lazyLoading.test.ts +36 -1
- package/src/tests/messagesTestUtils.ts +4 -0
- package/src/tests/sync.concurrentLoad.test.ts +491 -0
- package/src/tests/sync.garbageCollection.test.ts +4 -0
- package/src/tests/sync.load.test.ts +26 -0
- package/src/tests/sync.mesh.test.ts +1 -0
- package/src/tests/sync.multipleServers.test.ts +60 -2
- package/src/tests/sync.storage.test.ts +2 -0
- package/src/tests/sync.storageAsync.test.ts +1 -0
- package/src/tests/sync.storageReconciliation.test.ts +697 -0
- 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
|
+
}
|