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
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,7 @@ 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 { StorageReconciliationAckTracker } from "./StorageReconciliationAckTracker.js";
|
|
24
29
|
import {
|
|
25
30
|
CoValueKnownState,
|
|
26
31
|
knownStateFrom,
|
|
@@ -33,7 +38,9 @@ export type SyncMessage =
|
|
|
33
38
|
| LoadMessage
|
|
34
39
|
| KnownStateMessage
|
|
35
40
|
| NewContentMessage
|
|
36
|
-
| DoneMessage
|
|
41
|
+
| DoneMessage
|
|
42
|
+
| ReconcileMessage
|
|
43
|
+
| ReconcileAckMessage;
|
|
37
44
|
|
|
38
45
|
export type LoadMessage = {
|
|
39
46
|
action: "load";
|
|
@@ -68,6 +75,17 @@ export type DoneMessage = {
|
|
|
68
75
|
id: RawCoID;
|
|
69
76
|
};
|
|
70
77
|
|
|
78
|
+
export type ReconcileMessage = {
|
|
79
|
+
action: "reconcile";
|
|
80
|
+
id: string;
|
|
81
|
+
values: [coValue: RawCoID, sessionsHash: string][];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ReconcileAckMessage = {
|
|
85
|
+
action: "reconcile-ack";
|
|
86
|
+
id: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
71
89
|
/**
|
|
72
90
|
* Determines when network sync is enabled.
|
|
73
91
|
* - "always": sync is enabled for both Anonymous Authentication and Authenticated Account
|
|
@@ -111,9 +129,20 @@ export type ServerPeerSelector = (
|
|
|
111
129
|
serverPeers: PeerState[],
|
|
112
130
|
) => PeerState[];
|
|
113
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Manages the sync of coValues between peers.
|
|
134
|
+
* It is responsible for sending, receiving and processing sync messages.
|
|
135
|
+
* For more details on how the sync protocol works, see the sync protocol documentation:
|
|
136
|
+
* {@link docs/sync-protocol.md}
|
|
137
|
+
*/
|
|
114
138
|
export class SyncManager {
|
|
115
139
|
peers: { [key: PeerID]: PeerState } = {};
|
|
116
140
|
local: LocalNode;
|
|
141
|
+
private reconciliationAckTracker = new StorageReconciliationAckTracker();
|
|
142
|
+
|
|
143
|
+
get pendingReconciliationAck(): Map<string, number> {
|
|
144
|
+
return this.reconciliationAckTracker.pendingReconciliationAck;
|
|
145
|
+
}
|
|
117
146
|
|
|
118
147
|
// When true, transactions will not be verified.
|
|
119
148
|
// This is useful when syncing only for storage purposes, with the expectation that
|
|
@@ -127,6 +156,8 @@ export class SyncManager {
|
|
|
127
156
|
this._ignoreUnknownCoValuesFromServers = true;
|
|
128
157
|
}
|
|
129
158
|
|
|
159
|
+
fullStorageReconciliationEnabled = false;
|
|
160
|
+
|
|
130
161
|
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
|
|
131
162
|
description: "Amount of connected peers",
|
|
132
163
|
valueType: ValueType.INT,
|
|
@@ -179,6 +210,15 @@ export class SyncManager {
|
|
|
179
210
|
}
|
|
180
211
|
|
|
181
212
|
handleSyncMessage(msg: SyncMessage, peer: PeerState) {
|
|
213
|
+
if (msg.action === "reconcile") {
|
|
214
|
+
this.handleReconcile(msg, peer);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (msg.action === "reconcile-ack") {
|
|
218
|
+
this.handleReconcileAck(msg, peer);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
182
222
|
if (!isRawCoID(msg.id)) {
|
|
183
223
|
const errorType = msg.id ? "invalid" : "undefined";
|
|
184
224
|
logger.warn(`Received sync message with ${errorType} id`, {
|
|
@@ -233,7 +273,20 @@ export class SyncManager {
|
|
|
233
273
|
}
|
|
234
274
|
}
|
|
235
275
|
|
|
236
|
-
sendNewContent(
|
|
276
|
+
sendNewContent(
|
|
277
|
+
id: RawCoID,
|
|
278
|
+
peer: PeerState,
|
|
279
|
+
forceKnownReplyOnNoDelta: boolean = false,
|
|
280
|
+
) {
|
|
281
|
+
this.#sendNewContent(id, peer, new Set(), forceKnownReplyOnNoDelta);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#sendNewContent(
|
|
285
|
+
id: RawCoID,
|
|
286
|
+
peer: PeerState,
|
|
287
|
+
seen: Set<RawCoID>,
|
|
288
|
+
forceKnownReplyOnNoDelta: boolean,
|
|
289
|
+
) {
|
|
237
290
|
if (seen.has(id)) {
|
|
238
291
|
return;
|
|
239
292
|
}
|
|
@@ -249,7 +302,7 @@ export class SyncManager {
|
|
|
249
302
|
const includeDependencies = peer.role !== "server";
|
|
250
303
|
if (includeDependencies) {
|
|
251
304
|
for (const dependency of coValue.getDependedOnCoValues()) {
|
|
252
|
-
this
|
|
305
|
+
this.#sendNewContent(dependency, peer, seen, false);
|
|
253
306
|
}
|
|
254
307
|
}
|
|
255
308
|
|
|
@@ -263,7 +316,7 @@ export class SyncManager {
|
|
|
263
316
|
}
|
|
264
317
|
|
|
265
318
|
peer.combineOptimisticWith(id, coValue.knownState());
|
|
266
|
-
} else if (!peer.toldKnownState.has(id)) {
|
|
319
|
+
} else if (forceKnownReplyOnNoDelta || !peer.toldKnownState.has(id)) {
|
|
267
320
|
if (coValue.isDeleted) {
|
|
268
321
|
// 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
322
|
this.trySendToPeer(
|
|
@@ -281,15 +334,172 @@ export class SyncManager {
|
|
|
281
334
|
peer.trackToldKnownState(id);
|
|
282
335
|
}
|
|
283
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Reconciles all in-memory CoValues with all persistent server peers
|
|
339
|
+
*/
|
|
284
340
|
reconcileServerPeers() {
|
|
285
341
|
const serverPeers = Object.values(this.peers).filter(
|
|
286
|
-
|
|
342
|
+
isPersistentServerPeer,
|
|
287
343
|
);
|
|
288
344
|
for (const peer of serverPeers) {
|
|
289
345
|
this.startPeerReconciliation(peer);
|
|
290
346
|
}
|
|
291
347
|
}
|
|
292
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Ensures all CoValues in storage are synced to the given server peer.
|
|
351
|
+
* Sends "reconcile" message(s) with [coValueId, sessionsHash] for each CoValue.
|
|
352
|
+
* Server responds with "known" only where it is missing the CoValue or has different sessions,
|
|
353
|
+
* so that client can send missing content.
|
|
354
|
+
* Processes CoValues in batches of RECONCILIATION_BATCH_SIZE.
|
|
355
|
+
* @param peer - The server peer to reconcile with.
|
|
356
|
+
* @param initialOffset - Offset to start from (for resuming after interrupt). Default 0.
|
|
357
|
+
* @param onComplete - Called when reconciliation is fully complete (all batches sent and acked).
|
|
358
|
+
*/
|
|
359
|
+
startStorageReconciliation(
|
|
360
|
+
peer: PeerState,
|
|
361
|
+
initialOffset?: number,
|
|
362
|
+
onComplete?: () => void,
|
|
363
|
+
): void {
|
|
364
|
+
if (!this.local.storage) return;
|
|
365
|
+
if (!isPersistentServerPeer(peer)) return;
|
|
366
|
+
|
|
367
|
+
const startOffset = initialOffset ?? 0;
|
|
368
|
+
const batchSize = STORAGE_RECONCILIATION_CONFIG.BATCH_SIZE;
|
|
369
|
+
|
|
370
|
+
const storage = this.local.storage;
|
|
371
|
+
|
|
372
|
+
storage.getCoValueCount((totalCoValueCount) => {
|
|
373
|
+
const sendReconcileMessage = (
|
|
374
|
+
batchId: string,
|
|
375
|
+
entries: [RawCoID, string][],
|
|
376
|
+
offset: number,
|
|
377
|
+
) => {
|
|
378
|
+
if (entries.length === 0) return;
|
|
379
|
+
|
|
380
|
+
this.reconciliationAckTracker.trackBatch(
|
|
381
|
+
batchId,
|
|
382
|
+
peer.id,
|
|
383
|
+
offset + batchSize,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
this.trySendToPeer(peer, {
|
|
387
|
+
action: "reconcile",
|
|
388
|
+
id: batchId,
|
|
389
|
+
values: entries,
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const triggerNextBatch = (lastBatchLength: number, offset: number) => {
|
|
394
|
+
// This value becomes false when the last covalueid batch picked from the storage
|
|
395
|
+
// is smaller than the batch size.
|
|
396
|
+
if (lastBatchLength === batchSize) {
|
|
397
|
+
logger.info("Reconciled CoValues in storage", {
|
|
398
|
+
peerId: peer.id,
|
|
399
|
+
completed: offset + batchSize,
|
|
400
|
+
total: totalCoValueCount,
|
|
401
|
+
});
|
|
402
|
+
processStorageBatch(offset + batchSize);
|
|
403
|
+
} else {
|
|
404
|
+
// Note: `completed` can be higher than `total` if CoValues were added
|
|
405
|
+
// after the reconciliation started
|
|
406
|
+
logger.info("Storage reconciliation complete", {
|
|
407
|
+
peerId: peer.id,
|
|
408
|
+
startOffset,
|
|
409
|
+
completed: offset + lastBatchLength,
|
|
410
|
+
total: totalCoValueCount,
|
|
411
|
+
});
|
|
412
|
+
onComplete?.();
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const processStorageBatch = (offset: number) => {
|
|
417
|
+
storage.getCoValueIDs(batchSize, offset, (batch) => {
|
|
418
|
+
this.buildStorageReconciliationEntries(batch, (entries) => {
|
|
419
|
+
if (entries.length === 0) {
|
|
420
|
+
triggerNextBatch(batch.length, offset);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const batchId = base58.encode(this.local.crypto.randomBytes(12));
|
|
425
|
+
sendReconcileMessage(batchId, entries, offset);
|
|
426
|
+
|
|
427
|
+
this.reconciliationAckTracker.waitForAck(batchId, peer, () => {
|
|
428
|
+
triggerNextBatch(batch.length, offset);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
logger.info("Starting storage reconciliation", {
|
|
435
|
+
peerId: peer.id,
|
|
436
|
+
startOffset,
|
|
437
|
+
total: totalCoValueCount,
|
|
438
|
+
});
|
|
439
|
+
processStorageBatch(startOffset);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private buildStorageReconciliationEntries(
|
|
444
|
+
batch: { id: RawCoID }[],
|
|
445
|
+
callback: (entries: [RawCoID, string][]) => void,
|
|
446
|
+
): void {
|
|
447
|
+
const storage = this.local.storage;
|
|
448
|
+
|
|
449
|
+
if (!storage) {
|
|
450
|
+
callback([]);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const pending = batch.filter(({ id }) => !this.local.isCoValueInMemory(id));
|
|
455
|
+
|
|
456
|
+
if (pending.length === 0) {
|
|
457
|
+
callback([]);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let done = 0;
|
|
462
|
+
const entries: [RawCoID, string][] = [];
|
|
463
|
+
|
|
464
|
+
for (const coValue of pending) {
|
|
465
|
+
storage.loadKnownState(coValue.id, (storageKnownState) => {
|
|
466
|
+
if (storageKnownState) {
|
|
467
|
+
entries.push([
|
|
468
|
+
coValue.id,
|
|
469
|
+
this.hashKnownStateSessions(storageKnownState.sessions),
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
done += 1;
|
|
474
|
+
if (done === pending.length) {
|
|
475
|
+
callback(entries);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private maybeStartStorageReconciliationForPeer(peer: PeerState): void {
|
|
482
|
+
if (!this.fullStorageReconciliationEnabled) return;
|
|
483
|
+
if (!this.local.storage) return;
|
|
484
|
+
|
|
485
|
+
const sessionId = this.local.currentSessionID;
|
|
486
|
+
this.local.storage.tryAcquireStorageReconciliationLock(
|
|
487
|
+
sessionId,
|
|
488
|
+
peer.id,
|
|
489
|
+
(result) => {
|
|
490
|
+
if (!result.acquired) return;
|
|
491
|
+
|
|
492
|
+
const lastProcessedOffset = result.lastProcessedOffset;
|
|
493
|
+
this.startStorageReconciliation(peer, lastProcessedOffset, () => {
|
|
494
|
+
this.local.storage?.releaseStorageReconciliationLock(
|
|
495
|
+
sessionId,
|
|
496
|
+
peer.id,
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
293
503
|
async resumeUnsyncedCoValues(): Promise<void> {
|
|
294
504
|
if (!this.local.storage) {
|
|
295
505
|
// No storage available, skip resumption
|
|
@@ -364,12 +574,19 @@ export class SyncManager {
|
|
|
364
574
|
});
|
|
365
575
|
}
|
|
366
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Reconciles all in-memory CoValues with the given peer.
|
|
579
|
+
* Creates a subscription for each CoValue that is not already subscribed to.
|
|
580
|
+
*/
|
|
367
581
|
startPeerReconciliation(peer: PeerState) {
|
|
368
582
|
if (isPersistentServerPeer(peer)) {
|
|
369
583
|
// Resume syncing unsynced CoValues asynchronously
|
|
370
584
|
this.resumeUnsyncedCoValues().catch((error) => {
|
|
371
585
|
logger.warn("Failed to resume unsynced CoValues:", error);
|
|
372
586
|
});
|
|
587
|
+
|
|
588
|
+
// Try to run full storage reconciliation for this peer (scheduled per peer, every 30 days)
|
|
589
|
+
this.maybeStartStorageReconciliationForPeer(peer);
|
|
373
590
|
}
|
|
374
591
|
|
|
375
592
|
const coValuesOrderedByDependency: CoValueCore[] = [];
|
|
@@ -592,7 +809,7 @@ export class SyncManager {
|
|
|
592
809
|
|
|
593
810
|
// Fast path: CoValue is already in memory
|
|
594
811
|
if (coValue.isAvailable()) {
|
|
595
|
-
this.sendNewContent(msg.id, peer);
|
|
812
|
+
this.sendNewContent(msg.id, peer, true);
|
|
596
813
|
return;
|
|
597
814
|
}
|
|
598
815
|
|
|
@@ -608,7 +825,7 @@ export class SyncManager {
|
|
|
608
825
|
coValue.getKnownStateFromStorage((storageKnownState) => {
|
|
609
826
|
// Race condition: CoValue might have been loaded while we were waiting for storage
|
|
610
827
|
if (coValue.isAvailable()) {
|
|
611
|
-
this.sendNewContent(msg.id, peer);
|
|
828
|
+
this.sendNewContent(msg.id, peer, true);
|
|
612
829
|
return;
|
|
613
830
|
}
|
|
614
831
|
|
|
@@ -631,7 +848,7 @@ export class SyncManager {
|
|
|
631
848
|
// Even though we responded with KNOWN (client has everything), we need
|
|
632
849
|
// to establish a subscription so that updates from core flow to us.
|
|
633
850
|
const serverPeers = this.getServerPeers(msg.id, peer.id);
|
|
634
|
-
coValue.loadFromPeers(serverPeers);
|
|
851
|
+
coValue.loadFromPeers(serverPeers, "low-priority");
|
|
635
852
|
|
|
636
853
|
return;
|
|
637
854
|
}
|
|
@@ -652,7 +869,7 @@ export class SyncManager {
|
|
|
652
869
|
) {
|
|
653
870
|
coValue.loadFromStorage((found) => {
|
|
654
871
|
if (found && coValue.isAvailable()) {
|
|
655
|
-
this.sendNewContent(id, peer);
|
|
872
|
+
this.sendNewContent(id, peer, true);
|
|
656
873
|
} else {
|
|
657
874
|
this.loadFromPeersAndRespond(id, peer, coValue);
|
|
658
875
|
}
|
|
@@ -668,7 +885,7 @@ export class SyncManager {
|
|
|
668
885
|
coValue: CoValueCore,
|
|
669
886
|
) {
|
|
670
887
|
const peers = this.getServerPeers(id, peer.id);
|
|
671
|
-
coValue.loadFromPeers(peers);
|
|
888
|
+
coValue.loadFromPeers(peers, "immediate");
|
|
672
889
|
|
|
673
890
|
const handleLoadResult = () => {
|
|
674
891
|
if (coValue.isAvailable()) {
|
|
@@ -734,9 +951,89 @@ export class SyncManager {
|
|
|
734
951
|
|
|
735
952
|
if (coValue.isAvailable()) {
|
|
736
953
|
this.sendNewContent(msg.id, peer);
|
|
954
|
+
} else if (coValue.isKnownStateAvailable()) {
|
|
955
|
+
// Validate if content is missing before loading it from storage
|
|
956
|
+
if (!this.syncState.isSynced(peer, msg.id)) {
|
|
957
|
+
this.local.loadCoValueCore(msg.id).then(() => {
|
|
958
|
+
this.sendNewContent(msg.id, peer);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
737
961
|
}
|
|
738
962
|
|
|
739
|
-
peer.trackLoadRequestComplete(coValue);
|
|
963
|
+
peer.trackLoadRequestComplete(coValue, "known");
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
handleReconcile(msg: ReconcileMessage, peer: PeerState): void {
|
|
967
|
+
let pending = msg.values.length;
|
|
968
|
+
const sendAckWhenDone = () => {
|
|
969
|
+
if (--pending === 0) {
|
|
970
|
+
this.trySendToPeer(peer, { action: "reconcile-ack", id: msg.id });
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
for (const [coValueId, clientSessionsHash] of msg.values) {
|
|
975
|
+
// Avoid creating a new coValue object if it's not already in memory
|
|
976
|
+
const inMemoryCoValue = this.local.isCoValueInMemory(coValueId)
|
|
977
|
+
? this.local.getCoValue(coValueId)
|
|
978
|
+
: undefined;
|
|
979
|
+
if (inMemoryCoValue?.isErroredInPeer(peer.id)) {
|
|
980
|
+
sendAckWhenDone();
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const maybeSendLoadRequest = (
|
|
985
|
+
knownState: CoValueKnownState | undefined,
|
|
986
|
+
) => {
|
|
987
|
+
if (!knownState) {
|
|
988
|
+
peer.trackToldKnownState(coValueId);
|
|
989
|
+
this.trySendToPeer(peer, {
|
|
990
|
+
action: "load",
|
|
991
|
+
id: coValueId,
|
|
992
|
+
header: false,
|
|
993
|
+
sessions: {},
|
|
994
|
+
});
|
|
995
|
+
} else {
|
|
996
|
+
const serverSessionsHash = this.hashKnownStateSessions(
|
|
997
|
+
knownState.sessions,
|
|
998
|
+
);
|
|
999
|
+
if (serverSessionsHash !== clientSessionsHash) {
|
|
1000
|
+
peer.trackToldKnownState(coValueId);
|
|
1001
|
+
this.trySendToPeer(peer, { action: "load", ...knownState });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
sendAckWhenDone();
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
if (
|
|
1008
|
+
inMemoryCoValue?.isAvailable() ||
|
|
1009
|
+
inMemoryCoValue?.loadingState === "onlyKnownState"
|
|
1010
|
+
) {
|
|
1011
|
+
maybeSendLoadRequest(inMemoryCoValue.knownState());
|
|
1012
|
+
} else {
|
|
1013
|
+
this.local.storage
|
|
1014
|
+
? this.local.storage.loadKnownState(coValueId, maybeSendLoadRequest)
|
|
1015
|
+
: maybeSendLoadRequest(undefined);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (msg.values.length === 0) {
|
|
1020
|
+
this.trySendToPeer(peer, { action: "reconcile-ack", id: msg.id });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
handleReconcileAck(msg: ReconcileAckMessage, peer: PeerState): void {
|
|
1025
|
+
const nextOffset = this.reconciliationAckTracker.handleAck(msg.id, peer.id);
|
|
1026
|
+
if (nextOffset !== undefined) {
|
|
1027
|
+
this.local.storage?.renewStorageReconciliationLock(
|
|
1028
|
+
this.local.currentSessionID,
|
|
1029
|
+
peer.id,
|
|
1030
|
+
nextOffset,
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private hashKnownStateSessions(sessions: KnownStateSessions): string {
|
|
1036
|
+
return this.local.crypto.shortHash(sessions);
|
|
740
1037
|
}
|
|
741
1038
|
|
|
742
1039
|
recordTransactionsSize(newTransactions: Transaction[], source: string) {
|
|
@@ -1040,7 +1337,7 @@ export class SyncManager {
|
|
|
1040
1337
|
}
|
|
1041
1338
|
}
|
|
1042
1339
|
|
|
1043
|
-
peer?.trackLoadRequestComplete(coValue);
|
|
1340
|
+
peer?.trackLoadRequestComplete(coValue, "content");
|
|
1044
1341
|
|
|
1045
1342
|
for (const peer of this.getPeers(coValue.id)) {
|
|
1046
1343
|
/**
|
|
@@ -1056,7 +1353,7 @@ export class SyncManager {
|
|
|
1056
1353
|
if (peer.isCoValueSubscribedToPeer(coValue.id)) {
|
|
1057
1354
|
this.sendNewContent(coValue.id, peer);
|
|
1058
1355
|
} else if (peer.role === "server") {
|
|
1059
|
-
peer.sendLoadRequest(coValue);
|
|
1356
|
+
peer.sendLoadRequest(coValue, "low-priority");
|
|
1060
1357
|
}
|
|
1061
1358
|
}
|
|
1062
1359
|
}
|