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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +29 -0
- package/dist/OngoingStorageReconciliationTracker.d.ts +16 -0
- package/dist/OngoingStorageReconciliationTracker.d.ts.map +1 -0
- package/dist/OngoingStorageReconciliationTracker.js +75 -0
- package/dist/OngoingStorageReconciliationTracker.js.map +1 -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 +53 -2
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +300 -44
- package/dist/sync.js.map +1 -1
- package/dist/tests/OngoingStorageReconciliationTracker.test.d.ts +2 -0
- package/dist/tests/OngoingStorageReconciliationTracker.test.d.ts.map +1 -0
- package/dist/tests/OngoingStorageReconciliationTracker.test.js +60 -0
- package/dist/tests/OngoingStorageReconciliationTracker.test.js.map +1 -0
- 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 +502 -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/OngoingStorageReconciliationTracker.ts +97 -0
- 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 +359 -14
- package/src/tests/OngoingStorageReconciliationTracker.test.ts +85 -0
- 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 +696 -0
- package/src/tests/testUtils.ts +10 -1
package/src/sync.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { base58 } from "@scure/base";
|
|
1
2
|
import { md5 } from "@noble/hashes/legacy";
|
|
2
3
|
import { Histogram, ValueType, metrics } from "@opentelemetry/api";
|
|
3
4
|
import { PeerState } from "./PeerState.js";
|
|
4
5
|
import { SyncStateManager } from "./SyncStateManager.js";
|
|
5
6
|
import { UnsyncedCoValuesTracker } from "./UnsyncedCoValuesTracker.js";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
STORAGE_RECONCILIATION_CONFIG,
|
|
9
|
+
SYNC_SCHEDULER_CONFIG,
|
|
10
|
+
} from "./config.js";
|
|
7
11
|
import {
|
|
8
12
|
getContenDebugInfo,
|
|
9
13
|
getNewTransactionsFromContentMessage,
|
|
@@ -21,6 +25,8 @@ import { CoValuePriority } from "./priority.js";
|
|
|
21
25
|
import { IncomingMessagesQueue } from "./queue/IncomingMessagesQueue.js";
|
|
22
26
|
import { LocalTransactionsSyncQueue } from "./queue/LocalTransactionsSyncQueue.js";
|
|
23
27
|
import type { StorageStreamingQueue } from "./queue/StorageStreamingQueue.js";
|
|
28
|
+
import { OngoingStorageReconciliationTracker } from "./OngoingStorageReconciliationTracker.js";
|
|
29
|
+
import { StorageReconciliationServerAckTracker } from "./StorageReconciliationAckTracker.js";
|
|
24
30
|
import {
|
|
25
31
|
CoValueKnownState,
|
|
26
32
|
knownStateFrom,
|
|
@@ -33,7 +39,9 @@ export type SyncMessage =
|
|
|
33
39
|
| LoadMessage
|
|
34
40
|
| KnownStateMessage
|
|
35
41
|
| NewContentMessage
|
|
36
|
-
| DoneMessage
|
|
42
|
+
| DoneMessage
|
|
43
|
+
| ReconcileMessage
|
|
44
|
+
| ReconcileAckMessage;
|
|
37
45
|
|
|
38
46
|
export type LoadMessage = {
|
|
39
47
|
action: "load";
|
|
@@ -68,6 +76,19 @@ export type DoneMessage = {
|
|
|
68
76
|
id: RawCoID;
|
|
69
77
|
};
|
|
70
78
|
|
|
79
|
+
export type ReconcileBatchID = string;
|
|
80
|
+
|
|
81
|
+
export type ReconcileMessage = {
|
|
82
|
+
action: "reconcile";
|
|
83
|
+
id: ReconcileBatchID;
|
|
84
|
+
values: [coValue: RawCoID, sessionsHash: string][];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ReconcileAckMessage = {
|
|
88
|
+
action: "reconcile-ack";
|
|
89
|
+
id: ReconcileBatchID;
|
|
90
|
+
};
|
|
91
|
+
|
|
71
92
|
/**
|
|
72
93
|
* Determines when network sync is enabled.
|
|
73
94
|
* - "always": sync is enabled for both Anonymous Authentication and Authenticated Account
|
|
@@ -111,9 +132,29 @@ export type ServerPeerSelector = (
|
|
|
111
132
|
serverPeers: PeerState[],
|
|
112
133
|
) => PeerState[];
|
|
113
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Manages the sync of coValues between peers.
|
|
137
|
+
* It is responsible for sending, receiving and processing sync messages.
|
|
138
|
+
* For more details on how the sync protocol works, see the sync protocol documentation:
|
|
139
|
+
* {@link docs/sync-protocol.md}
|
|
140
|
+
*/
|
|
114
141
|
export class SyncManager {
|
|
115
142
|
peers: { [key: PeerID]: PeerState } = {};
|
|
116
143
|
local: LocalNode;
|
|
144
|
+
/**
|
|
145
|
+
* Tracks pending reconcile acks from the server.
|
|
146
|
+
*/
|
|
147
|
+
private reconciliationAckTracker =
|
|
148
|
+
new StorageReconciliationServerAckTracker();
|
|
149
|
+
/**
|
|
150
|
+
* Tracks ongoing storage reconciliation batches in a server.
|
|
151
|
+
*/
|
|
152
|
+
private ongoingStorageReconciliationTracker =
|
|
153
|
+
new OngoingStorageReconciliationTracker();
|
|
154
|
+
|
|
155
|
+
get pendingReconciliationAck(): Map<string, number> {
|
|
156
|
+
return this.reconciliationAckTracker.pendingReconciliationAck;
|
|
157
|
+
}
|
|
117
158
|
|
|
118
159
|
// When true, transactions will not be verified.
|
|
119
160
|
// This is useful when syncing only for storage purposes, with the expectation that
|
|
@@ -127,6 +168,8 @@ export class SyncManager {
|
|
|
127
168
|
this._ignoreUnknownCoValuesFromServers = true;
|
|
128
169
|
}
|
|
129
170
|
|
|
171
|
+
fullStorageReconciliationEnabled = false;
|
|
172
|
+
|
|
130
173
|
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
|
|
131
174
|
description: "Amount of connected peers",
|
|
132
175
|
valueType: ValueType.INT,
|
|
@@ -179,6 +222,15 @@ export class SyncManager {
|
|
|
179
222
|
}
|
|
180
223
|
|
|
181
224
|
handleSyncMessage(msg: SyncMessage, peer: PeerState) {
|
|
225
|
+
if (msg.action === "reconcile") {
|
|
226
|
+
this.handleReconcile(msg, peer);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (msg.action === "reconcile-ack") {
|
|
230
|
+
this.handleReconcileAck(msg, peer);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
182
234
|
if (!isRawCoID(msg.id)) {
|
|
183
235
|
const errorType = msg.id ? "invalid" : "undefined";
|
|
184
236
|
logger.warn(`Received sync message with ${errorType} id`, {
|
|
@@ -233,7 +285,20 @@ export class SyncManager {
|
|
|
233
285
|
}
|
|
234
286
|
}
|
|
235
287
|
|
|
236
|
-
sendNewContent(
|
|
288
|
+
sendNewContent(
|
|
289
|
+
id: RawCoID,
|
|
290
|
+
peer: PeerState,
|
|
291
|
+
forceKnownReplyOnNoDelta: boolean = false,
|
|
292
|
+
) {
|
|
293
|
+
this.#sendNewContent(id, peer, new Set(), forceKnownReplyOnNoDelta);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#sendNewContent(
|
|
297
|
+
id: RawCoID,
|
|
298
|
+
peer: PeerState,
|
|
299
|
+
seen: Set<RawCoID>,
|
|
300
|
+
forceKnownReplyOnNoDelta: boolean,
|
|
301
|
+
) {
|
|
237
302
|
if (seen.has(id)) {
|
|
238
303
|
return;
|
|
239
304
|
}
|
|
@@ -249,7 +314,7 @@ export class SyncManager {
|
|
|
249
314
|
const includeDependencies = peer.role !== "server";
|
|
250
315
|
if (includeDependencies) {
|
|
251
316
|
for (const dependency of coValue.getDependedOnCoValues()) {
|
|
252
|
-
this
|
|
317
|
+
this.#sendNewContent(dependency, peer, seen, false);
|
|
253
318
|
}
|
|
254
319
|
}
|
|
255
320
|
|
|
@@ -263,7 +328,7 @@ export class SyncManager {
|
|
|
263
328
|
}
|
|
264
329
|
|
|
265
330
|
peer.combineOptimisticWith(id, coValue.knownState());
|
|
266
|
-
} else if (!peer.toldKnownState.has(id)) {
|
|
331
|
+
} else if (forceKnownReplyOnNoDelta || !peer.toldKnownState.has(id)) {
|
|
267
332
|
if (coValue.isDeleted) {
|
|
268
333
|
// This way we make the peer believe that we've always ingested all the content they sent, even though we skipped it because the coValue is deleted
|
|
269
334
|
this.trySendToPeer(
|
|
@@ -281,15 +346,172 @@ export class SyncManager {
|
|
|
281
346
|
peer.trackToldKnownState(id);
|
|
282
347
|
}
|
|
283
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Reconciles all in-memory CoValues with all persistent server peers
|
|
351
|
+
*/
|
|
284
352
|
reconcileServerPeers() {
|
|
285
353
|
const serverPeers = Object.values(this.peers).filter(
|
|
286
|
-
|
|
354
|
+
isPersistentServerPeer,
|
|
287
355
|
);
|
|
288
356
|
for (const peer of serverPeers) {
|
|
289
357
|
this.startPeerReconciliation(peer);
|
|
290
358
|
}
|
|
291
359
|
}
|
|
292
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Ensures all CoValues in storage are synced to the given server peer.
|
|
363
|
+
* Sends "reconcile" message(s) with [coValueId, sessionsHash] for each CoValue.
|
|
364
|
+
* Server responds with "known" only where it is missing the CoValue or has different sessions,
|
|
365
|
+
* so that client can send missing content.
|
|
366
|
+
* Processes CoValues in batches of RECONCILIATION_BATCH_SIZE.
|
|
367
|
+
* @param peer - The server peer to reconcile with.
|
|
368
|
+
* @param initialOffset - Offset to start from (for resuming after interrupt). Default 0.
|
|
369
|
+
* @param onComplete - Called when reconciliation is fully complete (all batches sent and acked).
|
|
370
|
+
*/
|
|
371
|
+
startStorageReconciliation(
|
|
372
|
+
peer: PeerState,
|
|
373
|
+
initialOffset?: number,
|
|
374
|
+
onComplete?: () => void,
|
|
375
|
+
): void {
|
|
376
|
+
if (!this.local.storage) return;
|
|
377
|
+
if (!isPersistentServerPeer(peer)) return;
|
|
378
|
+
|
|
379
|
+
const startOffset = initialOffset ?? 0;
|
|
380
|
+
const batchSize = STORAGE_RECONCILIATION_CONFIG.BATCH_SIZE;
|
|
381
|
+
|
|
382
|
+
const storage = this.local.storage;
|
|
383
|
+
|
|
384
|
+
storage.getCoValueCount((totalCoValueCount) => {
|
|
385
|
+
const sendReconcileMessage = (
|
|
386
|
+
batchId: string,
|
|
387
|
+
entries: [RawCoID, string][],
|
|
388
|
+
offset: number,
|
|
389
|
+
) => {
|
|
390
|
+
if (entries.length === 0) return;
|
|
391
|
+
|
|
392
|
+
this.reconciliationAckTracker.trackBatch(
|
|
393
|
+
batchId,
|
|
394
|
+
peer.id,
|
|
395
|
+
offset + batchSize,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
this.trySendToPeer(peer, {
|
|
399
|
+
action: "reconcile",
|
|
400
|
+
id: batchId,
|
|
401
|
+
values: entries,
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const triggerNextBatch = (lastBatchLength: number, offset: number) => {
|
|
406
|
+
// This value becomes false when the last covalueid batch picked from the storage
|
|
407
|
+
// is smaller than the batch size.
|
|
408
|
+
if (lastBatchLength === batchSize) {
|
|
409
|
+
logger.info("Reconciled CoValues in storage", {
|
|
410
|
+
peerId: peer.id,
|
|
411
|
+
completed: offset + batchSize,
|
|
412
|
+
total: totalCoValueCount,
|
|
413
|
+
});
|
|
414
|
+
processStorageBatch(offset + batchSize);
|
|
415
|
+
} else {
|
|
416
|
+
// Note: `completed` can be higher than `total` if CoValues were added
|
|
417
|
+
// after the reconciliation started
|
|
418
|
+
logger.info("Storage reconciliation complete", {
|
|
419
|
+
peerId: peer.id,
|
|
420
|
+
startOffset,
|
|
421
|
+
completed: offset + lastBatchLength,
|
|
422
|
+
total: totalCoValueCount,
|
|
423
|
+
});
|
|
424
|
+
onComplete?.();
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const processStorageBatch = (offset: number) => {
|
|
429
|
+
storage.getCoValueIDs(batchSize, offset, (batch) => {
|
|
430
|
+
this.buildStorageReconciliationEntries(batch, (entries) => {
|
|
431
|
+
if (entries.length === 0) {
|
|
432
|
+
triggerNextBatch(batch.length, offset);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const batchId = base58.encode(this.local.crypto.randomBytes(12));
|
|
437
|
+
sendReconcileMessage(batchId, entries, offset);
|
|
438
|
+
|
|
439
|
+
this.reconciliationAckTracker.waitForAck(batchId, peer, () => {
|
|
440
|
+
triggerNextBatch(batch.length, offset);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
logger.info("Starting storage reconciliation", {
|
|
447
|
+
peerId: peer.id,
|
|
448
|
+
startOffset,
|
|
449
|
+
total: totalCoValueCount,
|
|
450
|
+
});
|
|
451
|
+
processStorageBatch(startOffset);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private buildStorageReconciliationEntries(
|
|
456
|
+
batch: { id: RawCoID }[],
|
|
457
|
+
callback: (entries: [RawCoID, string][]) => void,
|
|
458
|
+
): void {
|
|
459
|
+
const storage = this.local.storage;
|
|
460
|
+
|
|
461
|
+
if (!storage) {
|
|
462
|
+
callback([]);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const pending = batch.filter(({ id }) => !this.local.isCoValueInMemory(id));
|
|
467
|
+
|
|
468
|
+
if (pending.length === 0) {
|
|
469
|
+
callback([]);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let done = 0;
|
|
474
|
+
const entries: [RawCoID, string][] = [];
|
|
475
|
+
|
|
476
|
+
for (const coValue of pending) {
|
|
477
|
+
storage.loadKnownState(coValue.id, (storageKnownState) => {
|
|
478
|
+
if (storageKnownState) {
|
|
479
|
+
entries.push([
|
|
480
|
+
coValue.id,
|
|
481
|
+
this.hashKnownStateSessions(storageKnownState.sessions),
|
|
482
|
+
]);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
done += 1;
|
|
486
|
+
if (done === pending.length) {
|
|
487
|
+
callback(entries);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private maybeStartStorageReconciliationForPeer(peer: PeerState): void {
|
|
494
|
+
if (!this.fullStorageReconciliationEnabled) return;
|
|
495
|
+
if (!this.local.storage) return;
|
|
496
|
+
|
|
497
|
+
const sessionId = this.local.currentSessionID;
|
|
498
|
+
this.local.storage.tryAcquireStorageReconciliationLock(
|
|
499
|
+
sessionId,
|
|
500
|
+
peer.id,
|
|
501
|
+
(result) => {
|
|
502
|
+
if (!result.acquired) return;
|
|
503
|
+
|
|
504
|
+
const lastProcessedOffset = result.lastProcessedOffset;
|
|
505
|
+
this.startStorageReconciliation(peer, lastProcessedOffset, () => {
|
|
506
|
+
this.local.storage?.releaseStorageReconciliationLock(
|
|
507
|
+
sessionId,
|
|
508
|
+
peer.id,
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
293
515
|
async resumeUnsyncedCoValues(): Promise<void> {
|
|
294
516
|
if (!this.local.storage) {
|
|
295
517
|
// No storage available, skip resumption
|
|
@@ -364,12 +586,19 @@ export class SyncManager {
|
|
|
364
586
|
});
|
|
365
587
|
}
|
|
366
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Reconciles all in-memory CoValues with the given peer.
|
|
591
|
+
* Creates a subscription for each CoValue that is not already subscribed to.
|
|
592
|
+
*/
|
|
367
593
|
startPeerReconciliation(peer: PeerState) {
|
|
368
594
|
if (isPersistentServerPeer(peer)) {
|
|
369
595
|
// Resume syncing unsynced CoValues asynchronously
|
|
370
596
|
this.resumeUnsyncedCoValues().catch((error) => {
|
|
371
597
|
logger.warn("Failed to resume unsynced CoValues:", error);
|
|
372
598
|
});
|
|
599
|
+
|
|
600
|
+
// Try to run full storage reconciliation for this peer (scheduled per peer, every 30 days)
|
|
601
|
+
this.maybeStartStorageReconciliationForPeer(peer);
|
|
373
602
|
}
|
|
374
603
|
|
|
375
604
|
const coValuesOrderedByDependency: CoValueCore[] = [];
|
|
@@ -549,6 +778,7 @@ export class SyncManager {
|
|
|
549
778
|
|
|
550
779
|
peerState.addCloseListener(() => {
|
|
551
780
|
unsubscribeFromKnownStatesUpdates();
|
|
781
|
+
this.ongoingStorageReconciliationTracker.clearPeer(peer.id);
|
|
552
782
|
this.peersCounter.add(-1, { role: peer.role });
|
|
553
783
|
|
|
554
784
|
if (!peer.persistent && this.peers[peer.id] === peerState) {
|
|
@@ -592,7 +822,7 @@ export class SyncManager {
|
|
|
592
822
|
|
|
593
823
|
// Fast path: CoValue is already in memory
|
|
594
824
|
if (coValue.isAvailable()) {
|
|
595
|
-
this.sendNewContent(msg.id, peer);
|
|
825
|
+
this.sendNewContent(msg.id, peer, true);
|
|
596
826
|
return;
|
|
597
827
|
}
|
|
598
828
|
|
|
@@ -608,7 +838,7 @@ export class SyncManager {
|
|
|
608
838
|
coValue.getKnownStateFromStorage((storageKnownState) => {
|
|
609
839
|
// Race condition: CoValue might have been loaded while we were waiting for storage
|
|
610
840
|
if (coValue.isAvailable()) {
|
|
611
|
-
this.sendNewContent(msg.id, peer);
|
|
841
|
+
this.sendNewContent(msg.id, peer, true);
|
|
612
842
|
return;
|
|
613
843
|
}
|
|
614
844
|
|
|
@@ -631,7 +861,7 @@ export class SyncManager {
|
|
|
631
861
|
// Even though we responded with KNOWN (client has everything), we need
|
|
632
862
|
// to establish a subscription so that updates from core flow to us.
|
|
633
863
|
const serverPeers = this.getServerPeers(msg.id, peer.id);
|
|
634
|
-
coValue.loadFromPeers(serverPeers);
|
|
864
|
+
coValue.loadFromPeers(serverPeers, "low-priority");
|
|
635
865
|
|
|
636
866
|
return;
|
|
637
867
|
}
|
|
@@ -652,7 +882,7 @@ export class SyncManager {
|
|
|
652
882
|
) {
|
|
653
883
|
coValue.loadFromStorage((found) => {
|
|
654
884
|
if (found && coValue.isAvailable()) {
|
|
655
|
-
this.sendNewContent(id, peer);
|
|
885
|
+
this.sendNewContent(id, peer, true);
|
|
656
886
|
} else {
|
|
657
887
|
this.loadFromPeersAndRespond(id, peer, coValue);
|
|
658
888
|
}
|
|
@@ -668,7 +898,7 @@ export class SyncManager {
|
|
|
668
898
|
coValue: CoValueCore,
|
|
669
899
|
) {
|
|
670
900
|
const peers = this.getServerPeers(id, peer.id);
|
|
671
|
-
coValue.loadFromPeers(peers);
|
|
901
|
+
coValue.loadFromPeers(peers, "immediate");
|
|
672
902
|
|
|
673
903
|
const handleLoadResult = () => {
|
|
674
904
|
if (coValue.isAvailable()) {
|
|
@@ -734,9 +964,107 @@ export class SyncManager {
|
|
|
734
964
|
|
|
735
965
|
if (coValue.isAvailable()) {
|
|
736
966
|
this.sendNewContent(msg.id, peer);
|
|
967
|
+
} else if (coValue.isKnownStateAvailable()) {
|
|
968
|
+
// Validate if content is missing before loading it from storage
|
|
969
|
+
if (!this.syncState.isSynced(peer, msg.id)) {
|
|
970
|
+
this.local.loadCoValueCore(msg.id).then(() => {
|
|
971
|
+
this.sendNewContent(msg.id, peer);
|
|
972
|
+
});
|
|
973
|
+
}
|
|
737
974
|
}
|
|
738
975
|
|
|
739
|
-
peer.trackLoadRequestComplete(coValue);
|
|
976
|
+
peer.trackLoadRequestComplete(coValue, "known");
|
|
977
|
+
this.maybeMarkCoValueAsReconciled(peer, msg.id);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
handleReconcile(msg: ReconcileMessage, peer: PeerState): void {
|
|
981
|
+
let remaining = msg.values.length;
|
|
982
|
+
if (remaining === 0) {
|
|
983
|
+
this.trySendToPeer(peer, { action: "reconcile-ack", id: msg.id });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const pending = new Set<RawCoID>();
|
|
988
|
+
const processEntryDone = () => {
|
|
989
|
+
remaining -= 1;
|
|
990
|
+
|
|
991
|
+
if (remaining !== 0) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (pending.size === 0) {
|
|
996
|
+
this.trySendToPeer(peer, { action: "reconcile-ack", id: msg.id });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
this.ongoingStorageReconciliationTracker.trackBatch(
|
|
1001
|
+
peer.id,
|
|
1002
|
+
msg.id,
|
|
1003
|
+
pending,
|
|
1004
|
+
);
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
for (const [coValueId, clientSessionsHash] of msg.values) {
|
|
1008
|
+
// Avoid creating a new coValue object if it's not already in memory
|
|
1009
|
+
const inMemoryCoValue = this.local.isCoValueInMemory(coValueId)
|
|
1010
|
+
? this.local.getCoValue(coValueId)
|
|
1011
|
+
: undefined;
|
|
1012
|
+
if (inMemoryCoValue?.isErroredInPeer(peer.id)) {
|
|
1013
|
+
processEntryDone();
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const maybeSendLoadRequest = (
|
|
1018
|
+
knownState: CoValueKnownState | undefined,
|
|
1019
|
+
) => {
|
|
1020
|
+
if (!knownState) {
|
|
1021
|
+
pending.add(coValueId);
|
|
1022
|
+
peer.trackToldKnownState(coValueId);
|
|
1023
|
+
this.trySendToPeer(peer, {
|
|
1024
|
+
action: "load",
|
|
1025
|
+
id: coValueId,
|
|
1026
|
+
header: false,
|
|
1027
|
+
sessions: {},
|
|
1028
|
+
});
|
|
1029
|
+
} else {
|
|
1030
|
+
const serverSessionsHash = this.hashKnownStateSessions(
|
|
1031
|
+
knownState.sessions,
|
|
1032
|
+
);
|
|
1033
|
+
if (serverSessionsHash !== clientSessionsHash) {
|
|
1034
|
+
pending.add(coValueId);
|
|
1035
|
+
peer.trackToldKnownState(coValueId);
|
|
1036
|
+
this.trySendToPeer(peer, { action: "load", ...knownState });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
processEntryDone();
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
if (
|
|
1043
|
+
inMemoryCoValue?.isAvailable() ||
|
|
1044
|
+
inMemoryCoValue?.loadingState === "onlyKnownState"
|
|
1045
|
+
) {
|
|
1046
|
+
maybeSendLoadRequest(inMemoryCoValue.knownState());
|
|
1047
|
+
} else {
|
|
1048
|
+
this.local.storage
|
|
1049
|
+
? this.local.storage.loadKnownState(coValueId, maybeSendLoadRequest)
|
|
1050
|
+
: maybeSendLoadRequest(undefined);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
handleReconcileAck(msg: ReconcileAckMessage, peer: PeerState): void {
|
|
1056
|
+
const nextOffset = this.reconciliationAckTracker.handleAck(msg.id, peer.id);
|
|
1057
|
+
if (nextOffset !== undefined) {
|
|
1058
|
+
this.local.storage?.renewStorageReconciliationLock(
|
|
1059
|
+
this.local.currentSessionID,
|
|
1060
|
+
peer.id,
|
|
1061
|
+
nextOffset,
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private hashKnownStateSessions(sessions: KnownStateSessions): string {
|
|
1067
|
+
return this.local.crypto.shortHash(sessions);
|
|
740
1068
|
}
|
|
741
1069
|
|
|
742
1070
|
recordTransactionsSize(newTransactions: Transaction[], source: string) {
|
|
@@ -1040,7 +1368,10 @@ export class SyncManager {
|
|
|
1040
1368
|
}
|
|
1041
1369
|
}
|
|
1042
1370
|
|
|
1043
|
-
peer?.trackLoadRequestComplete(coValue);
|
|
1371
|
+
peer?.trackLoadRequestComplete(coValue, "content");
|
|
1372
|
+
if (peer && !coValue.isStreaming()) {
|
|
1373
|
+
this.maybeMarkCoValueAsReconciled(peer, msg.id);
|
|
1374
|
+
}
|
|
1044
1375
|
|
|
1045
1376
|
for (const peer of this.getPeers(coValue.id)) {
|
|
1046
1377
|
/**
|
|
@@ -1056,7 +1387,7 @@ export class SyncManager {
|
|
|
1056
1387
|
if (peer.isCoValueSubscribedToPeer(coValue.id)) {
|
|
1057
1388
|
this.sendNewContent(coValue.id, peer);
|
|
1058
1389
|
} else if (peer.role === "server") {
|
|
1059
|
-
peer.sendLoadRequest(coValue);
|
|
1390
|
+
peer.sendLoadRequest(coValue, "low-priority");
|
|
1060
1391
|
}
|
|
1061
1392
|
}
|
|
1062
1393
|
}
|
|
@@ -1067,6 +1398,20 @@ export class SyncManager {
|
|
|
1067
1398
|
return this.sendNewContent(msg.id, peer);
|
|
1068
1399
|
}
|
|
1069
1400
|
|
|
1401
|
+
private maybeMarkCoValueAsReconciled(peer: PeerState, coValueId: RawCoID) {
|
|
1402
|
+
const completedBatchIds =
|
|
1403
|
+
this.ongoingStorageReconciliationTracker.markItemComplete(
|
|
1404
|
+
peer.id,
|
|
1405
|
+
coValueId,
|
|
1406
|
+
);
|
|
1407
|
+
for (const batchId of completedBatchIds) {
|
|
1408
|
+
this.trySendToPeer(peer, {
|
|
1409
|
+
action: "reconcile-ack",
|
|
1410
|
+
id: batchId,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1070
1415
|
private syncQueue = new LocalTransactionsSyncQueue((content) =>
|
|
1071
1416
|
this.syncContent(content),
|
|
1072
1417
|
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { OngoingStorageReconciliationTracker } from "../OngoingStorageReconciliationTracker.js";
|
|
3
|
+
import { RawCoID } from "../ids.js";
|
|
4
|
+
|
|
5
|
+
function coID(value: string): RawCoID {
|
|
6
|
+
return value as RawCoID;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("OngoingStorageReconciliationTracker", () => {
|
|
10
|
+
test("does not track empty batches", () => {
|
|
11
|
+
const tracker = new OngoingStorageReconciliationTracker();
|
|
12
|
+
|
|
13
|
+
tracker.trackBatch("peer-1", "batch-1", new Set());
|
|
14
|
+
|
|
15
|
+
// @ts-expect-error - reconcileBatches is private
|
|
16
|
+
expect(tracker.reconcileBatches.size).toEqual(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("completes a batch only when all its covalues are completed", () => {
|
|
20
|
+
const tracker = new OngoingStorageReconciliationTracker();
|
|
21
|
+
|
|
22
|
+
tracker.trackBatch(
|
|
23
|
+
"peer-1",
|
|
24
|
+
"batch-1",
|
|
25
|
+
new Set([coID("co_A"), coID("co_B")]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(tracker.markItemComplete("peer-1", coID("co_A"))).toEqual([]);
|
|
29
|
+
expect(tracker.markItemComplete("peer-1", coID("co_B"))).toEqual([
|
|
30
|
+
"batch-1",
|
|
31
|
+
]);
|
|
32
|
+
// @ts-expect-error - reconcileBatches is private
|
|
33
|
+
expect(tracker.reconcileBatches.size).toEqual(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns all completed batch ids for a covalue that is in multiple batches", () => {
|
|
37
|
+
const tracker = new OngoingStorageReconciliationTracker();
|
|
38
|
+
|
|
39
|
+
tracker.trackBatch("peer-1", "batch-1", new Set([coID("co_A")]));
|
|
40
|
+
tracker.trackBatch(
|
|
41
|
+
"peer-1",
|
|
42
|
+
"batch-2",
|
|
43
|
+
new Set([coID("co_A"), coID("co_B")]),
|
|
44
|
+
);
|
|
45
|
+
tracker.trackBatch("peer-1", "batch-3", new Set([coID("co_B")]));
|
|
46
|
+
|
|
47
|
+
expect(tracker.markItemComplete("peer-1", coID("co_A"))).toEqual([
|
|
48
|
+
"batch-1",
|
|
49
|
+
]);
|
|
50
|
+
expect(tracker.markItemComplete("peer-1", coID("co_B"))).toEqual([
|
|
51
|
+
"batch-2",
|
|
52
|
+
"batch-3",
|
|
53
|
+
]);
|
|
54
|
+
// @ts-expect-error - reconcileBatches is private
|
|
55
|
+
expect(tracker.reconcileBatches.size).toEqual(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("isolates state by peer", () => {
|
|
59
|
+
const tracker = new OngoingStorageReconciliationTracker();
|
|
60
|
+
|
|
61
|
+
tracker.trackBatch("peer-1", "batch-A", new Set([coID("co_A")]));
|
|
62
|
+
tracker.trackBatch("peer-2", "batch-B", new Set([coID("co_A")]));
|
|
63
|
+
|
|
64
|
+
expect(tracker.markItemComplete("peer-1", coID("co_A"))).toEqual([
|
|
65
|
+
"batch-A",
|
|
66
|
+
]);
|
|
67
|
+
expect(tracker.markItemComplete("peer-2", coID("co_A"))).toEqual([
|
|
68
|
+
"batch-B",
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("clearPeer removes only that peer state", () => {
|
|
73
|
+
const tracker = new OngoingStorageReconciliationTracker();
|
|
74
|
+
|
|
75
|
+
tracker.trackBatch("peer-1", "batch-A", new Set([coID("co_A")]));
|
|
76
|
+
tracker.trackBatch("peer-2", "batch-B", new Set([coID("co_B")]));
|
|
77
|
+
|
|
78
|
+
tracker.clearPeer("peer-1");
|
|
79
|
+
|
|
80
|
+
expect(tracker.markItemComplete("peer-1", coID("co_A"))).toEqual([]);
|
|
81
|
+
expect(tracker.markItemComplete("peer-2", coID("co_B"))).toEqual([
|
|
82
|
+
"batch-B",
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
});
|