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/exports.ts
CHANGED
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
|
|
86
86
|
import {
|
|
87
87
|
CO_VALUE_LOADING_CONFIG,
|
|
88
|
+
STORAGE_RECONCILIATION_CONFIG,
|
|
88
89
|
TRANSACTION_CONFIG,
|
|
89
90
|
WEBSOCKET_CONFIG,
|
|
90
91
|
setCoValueLoadingMaxRetries,
|
|
@@ -94,6 +95,9 @@ import {
|
|
|
94
95
|
setMaxInFlightLoadsPerPeer,
|
|
95
96
|
setMaxOutgoingMessagesChunkBytes,
|
|
96
97
|
setMaxRecommendedTxSize,
|
|
98
|
+
setStorageReconciliationBatchSize,
|
|
99
|
+
setStorageReconciliationLockTTL,
|
|
100
|
+
setStorageReconciliationInterval,
|
|
97
101
|
} from "./config.js";
|
|
98
102
|
import { LogLevel, logger } from "./logger.js";
|
|
99
103
|
import { CO_VALUE_PRIORITY, getPriorityFromHeader } from "./priority.js";
|
|
@@ -147,6 +151,10 @@ export const cojsonInternals = {
|
|
|
147
151
|
WEBSOCKET_CONFIG,
|
|
148
152
|
setMaxOutgoingMessagesChunkBytes,
|
|
149
153
|
setMaxInFlightLoadsPerPeer,
|
|
154
|
+
STORAGE_RECONCILIATION_CONFIG,
|
|
155
|
+
setStorageReconciliationBatchSize,
|
|
156
|
+
setStorageReconciliationLockTTL,
|
|
157
|
+
setStorageReconciliationInterval,
|
|
150
158
|
};
|
|
151
159
|
|
|
152
160
|
export {
|
package/src/localNode.ts
CHANGED
|
@@ -77,10 +77,14 @@ export class LocalNode {
|
|
|
77
77
|
currentSessionID: SessionID,
|
|
78
78
|
crypto: CryptoProvider,
|
|
79
79
|
public readonly syncWhen?: SyncWhen,
|
|
80
|
+
enableFullStorageReconciliation?: boolean,
|
|
80
81
|
) {
|
|
81
82
|
this.agentSecret = agentSecret;
|
|
82
83
|
this.currentSessionID = currentSessionID;
|
|
83
84
|
this.crypto = crypto;
|
|
85
|
+
if (enableFullStorageReconciliation) {
|
|
86
|
+
this.syncManager.fullStorageReconciliationEnabled = true;
|
|
87
|
+
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
enableGarbageCollector() {
|
|
@@ -133,6 +137,12 @@ export class LocalNode {
|
|
|
133
137
|
);
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
isCoValueInMemory(id: RawCoID) {
|
|
141
|
+
const state = this.coValues.get(id)?.loadingState;
|
|
142
|
+
|
|
143
|
+
return Boolean(state);
|
|
144
|
+
}
|
|
145
|
+
|
|
136
146
|
getCoValue(id: RawCoID) {
|
|
137
147
|
let entry = this.coValues.get(id);
|
|
138
148
|
|
|
@@ -256,6 +266,7 @@ export class LocalNode {
|
|
|
256
266
|
peers?: Peer[];
|
|
257
267
|
syncWhen?: SyncWhen;
|
|
258
268
|
storage?: StorageAPI;
|
|
269
|
+
enableFullStorageReconciliation?: boolean;
|
|
259
270
|
}): RawAccount {
|
|
260
271
|
const {
|
|
261
272
|
crypto,
|
|
@@ -274,6 +285,7 @@ export class LocalNode {
|
|
|
274
285
|
crypto.newRandomSessionID(accountID as RawAccountID),
|
|
275
286
|
crypto,
|
|
276
287
|
syncWhen,
|
|
288
|
+
opts.enableFullStorageReconciliation,
|
|
277
289
|
);
|
|
278
290
|
|
|
279
291
|
if (opts.storage) {
|
|
@@ -320,6 +332,7 @@ export class LocalNode {
|
|
|
320
332
|
crypto,
|
|
321
333
|
initialAgentSecret = crypto.newRandomAgentSecret(),
|
|
322
334
|
storage,
|
|
335
|
+
enableFullStorageReconciliation,
|
|
323
336
|
}: {
|
|
324
337
|
creationProps: { name: string };
|
|
325
338
|
peers?: Peer[];
|
|
@@ -328,6 +341,7 @@ export class LocalNode {
|
|
|
328
341
|
crypto: CryptoProvider;
|
|
329
342
|
initialAgentSecret?: AgentSecret;
|
|
330
343
|
storage?: StorageAPI;
|
|
344
|
+
enableFullStorageReconciliation?: boolean;
|
|
331
345
|
}): Promise<{
|
|
332
346
|
node: LocalNode;
|
|
333
347
|
accountID: RawAccountID;
|
|
@@ -340,6 +354,7 @@ export class LocalNode {
|
|
|
340
354
|
peers,
|
|
341
355
|
syncWhen,
|
|
342
356
|
storage,
|
|
357
|
+
enableFullStorageReconciliation,
|
|
343
358
|
});
|
|
344
359
|
const node = account.core.node;
|
|
345
360
|
|
|
@@ -385,6 +400,7 @@ export class LocalNode {
|
|
|
385
400
|
crypto,
|
|
386
401
|
migration,
|
|
387
402
|
storage,
|
|
403
|
+
enableFullStorageReconciliation,
|
|
388
404
|
}: {
|
|
389
405
|
accountID: RawAccountID;
|
|
390
406
|
accountSecret: AgentSecret;
|
|
@@ -394,6 +410,7 @@ export class LocalNode {
|
|
|
394
410
|
crypto: CryptoProvider;
|
|
395
411
|
migration?: RawAccountMigration<AccountMeta>;
|
|
396
412
|
storage?: StorageAPI;
|
|
413
|
+
enableFullStorageReconciliation?: boolean;
|
|
397
414
|
}): Promise<LocalNode> {
|
|
398
415
|
try {
|
|
399
416
|
const expectedAgentID = crypto.getAgentID(accountSecret);
|
|
@@ -403,6 +420,7 @@ export class LocalNode {
|
|
|
403
420
|
sessionID || crypto.newRandomSessionID(accountID),
|
|
404
421
|
crypto,
|
|
405
422
|
syncWhen,
|
|
423
|
+
enableFullStorageReconciliation,
|
|
406
424
|
);
|
|
407
425
|
|
|
408
426
|
if (storage) {
|
package/src/permissions.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { expectGroup } from "./typeUtils/expectGroup.js";
|
|
|
22
22
|
|
|
23
23
|
export type PermissionsDef =
|
|
24
24
|
| { type: "group"; initialAdmin: RawAccountID | AgentID }
|
|
25
|
-
| { type: "ownedByGroup"; group: RawCoID }
|
|
25
|
+
| { type: "ownedByGroup"; group: RawCoID; restrictDeletion?: boolean }
|
|
26
26
|
| { type: "unsafeAllowAll" };
|
|
27
27
|
|
|
28
28
|
export type AccountRole =
|
package/src/queue/LinkedList.ts
CHANGED
|
@@ -68,6 +68,10 @@ export class LinkedList<T> {
|
|
|
68
68
|
return value;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
trackPushPull() {
|
|
72
|
+
this.meter?.trackPushPull();
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
/**
|
|
72
76
|
* Remove a specific node from the list in O(1) time.
|
|
73
77
|
* The node must be a valid node that was returned by push().
|
|
@@ -136,7 +140,13 @@ class QueueMeter {
|
|
|
136
140
|
public push() {
|
|
137
141
|
this.pushCounter.add(1, this.attrs);
|
|
138
142
|
}
|
|
143
|
+
|
|
144
|
+
public trackPushPull() {
|
|
145
|
+
this.pullCounter.add(1, this.attrs);
|
|
146
|
+
this.pushCounter.add(1, this.attrs);
|
|
147
|
+
}
|
|
139
148
|
}
|
|
149
|
+
|
|
140
150
|
export function meteredList<T>(
|
|
141
151
|
type: "incoming" | "outgoing" | "storage-streaming" | "load-requests-queue",
|
|
142
152
|
attrs?: Record<string, string | number>,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
|
|
1
2
|
import { CO_VALUE_LOADING_CONFIG } from "../config.js";
|
|
2
3
|
import { CoValueCore } from "../exports.js";
|
|
3
4
|
import type { RawCoID } from "../ids.js";
|
|
@@ -10,6 +11,11 @@ interface PendingLoad {
|
|
|
10
11
|
sendCallback: () => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
interface InFlightLoad {
|
|
15
|
+
value: CoValueCore;
|
|
16
|
+
sentAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Mode for enqueuing load requests:
|
|
15
21
|
* - "high-priority" (default): high priority, processed in order
|
|
@@ -17,6 +23,7 @@ interface PendingLoad {
|
|
|
17
23
|
* - "immediate": bypasses the queue entirely, executes immediately
|
|
18
24
|
*/
|
|
19
25
|
export type LoadMode = "low-priority" | "immediate" | "high-priority";
|
|
26
|
+
export type LoadCompletionSource = "content" | "known";
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* A queue that manages outgoing load requests with throttling.
|
|
@@ -28,7 +35,8 @@ export type LoadMode = "low-priority" | "immediate" | "high-priority";
|
|
|
28
35
|
* - Manages timeouts for in-flight loads with a single timer
|
|
29
36
|
*/
|
|
30
37
|
export class OutgoingLoadQueue {
|
|
31
|
-
private inFlightLoads: Map<
|
|
38
|
+
private inFlightLoads: Map<RawCoID, InFlightLoad> = new Map();
|
|
39
|
+
private inFlightCounter: UpDownCounter;
|
|
32
40
|
private highPriorityPending: LinkedList<PendingLoad> = meteredList(
|
|
33
41
|
"load-requests-queue",
|
|
34
42
|
{ priority: "high" },
|
|
@@ -49,7 +57,18 @@ export class OutgoingLoadQueue {
|
|
|
49
57
|
new Map();
|
|
50
58
|
private timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
51
59
|
|
|
52
|
-
constructor(private peerId: PeerID) {
|
|
60
|
+
constructor(private peerId: PeerID) {
|
|
61
|
+
this.inFlightCounter = metrics
|
|
62
|
+
.getMeter("cojson")
|
|
63
|
+
.createUpDownCounter("jazz.loadqueue.outgoing.inflight", {
|
|
64
|
+
description: "Number of in-flight outgoing load requests",
|
|
65
|
+
unit: "1",
|
|
66
|
+
valueType: ValueType.INT,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Emit an initial 0 value so the series appears immediately.
|
|
70
|
+
this.inFlightCounter.add(0);
|
|
71
|
+
}
|
|
53
72
|
|
|
54
73
|
/**
|
|
55
74
|
* Check if we can send another load request.
|
|
@@ -66,10 +85,20 @@ export class OutgoingLoadQueue {
|
|
|
66
85
|
*/
|
|
67
86
|
private trackSent(coValue: CoValueCore): void {
|
|
68
87
|
const now = performance.now();
|
|
69
|
-
this.inFlightLoads.set(coValue, now);
|
|
88
|
+
this.inFlightLoads.set(coValue.id, { value: coValue, sentAt: now });
|
|
89
|
+
this.inFlightCounter.add(1);
|
|
70
90
|
this.scheduleTimeoutCheck(CO_VALUE_LOADING_CONFIG.TIMEOUT);
|
|
71
91
|
}
|
|
72
92
|
|
|
93
|
+
private untrackInFlight(id: RawCoID): boolean {
|
|
94
|
+
if (!this.inFlightLoads.delete(id)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.inFlightCounter.add(-1);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
/**
|
|
74
103
|
* Schedule a timeout check if not already scheduled.
|
|
75
104
|
* Uses a single timer to check all in-flight loads.
|
|
@@ -92,7 +121,7 @@ export class OutgoingLoadQueue {
|
|
|
92
121
|
const now = performance.now();
|
|
93
122
|
|
|
94
123
|
let nextTimeout: number | undefined;
|
|
95
|
-
for (const
|
|
124
|
+
for (const { value: coValue, sentAt } of this.inFlightLoads.values()) {
|
|
96
125
|
const timeout = sentAt + CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
|
97
126
|
|
|
98
127
|
if (now >= timeout) {
|
|
@@ -101,7 +130,8 @@ export class OutgoingLoadQueue {
|
|
|
101
130
|
id: coValue.id,
|
|
102
131
|
peerId: this.peerId,
|
|
103
132
|
});
|
|
104
|
-
|
|
133
|
+
// Re-resolve by ID to avoid mutating a stale CoValue instance.
|
|
134
|
+
coValue.node.getCoValue(coValue.id).markNotFoundInPeer(this.peerId);
|
|
105
135
|
} else if (coValue.isStreaming()) {
|
|
106
136
|
logger.warn(
|
|
107
137
|
"Content streaming is taking more than " +
|
|
@@ -116,8 +146,9 @@ export class OutgoingLoadQueue {
|
|
|
116
146
|
);
|
|
117
147
|
}
|
|
118
148
|
|
|
119
|
-
this.
|
|
120
|
-
|
|
149
|
+
if (this.untrackInFlight(coValue.id)) {
|
|
150
|
+
this.processQueue();
|
|
151
|
+
}
|
|
121
152
|
} else {
|
|
122
153
|
nextTimeout = Math.min(nextTimeout ?? Infinity, timeout - now);
|
|
123
154
|
}
|
|
@@ -130,30 +161,37 @@ export class OutgoingLoadQueue {
|
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
trackUpdate(coValue: CoValueCore): void {
|
|
133
|
-
if (!this.inFlightLoads.has(coValue)) {
|
|
164
|
+
if (!this.inFlightLoads.has(coValue.id)) {
|
|
134
165
|
return;
|
|
135
166
|
}
|
|
136
167
|
|
|
137
168
|
// Refresh the timeout for the in-flight load
|
|
138
|
-
this.inFlightLoads.set(coValue,
|
|
169
|
+
this.inFlightLoads.set(coValue.id, {
|
|
170
|
+
value: coValue,
|
|
171
|
+
sentAt: performance.now(),
|
|
172
|
+
});
|
|
139
173
|
}
|
|
140
174
|
|
|
141
175
|
/**
|
|
142
176
|
* Track that a load request has completed.
|
|
143
177
|
* Triggers processing of pending requests.
|
|
144
178
|
*/
|
|
145
|
-
trackComplete(
|
|
146
|
-
|
|
179
|
+
trackComplete(
|
|
180
|
+
coValue: CoValueCore,
|
|
181
|
+
source: LoadCompletionSource = "content",
|
|
182
|
+
): void {
|
|
183
|
+
if (!this.inFlightLoads.has(coValue.id)) {
|
|
147
184
|
return;
|
|
148
185
|
}
|
|
149
186
|
|
|
150
|
-
if (coValue.isStreaming()) {
|
|
187
|
+
if (source === "content" && coValue.isStreaming()) {
|
|
151
188
|
// wait for the next chunk
|
|
152
189
|
return;
|
|
153
190
|
}
|
|
154
191
|
|
|
155
|
-
this.
|
|
156
|
-
|
|
192
|
+
if (this.untrackInFlight(coValue.id)) {
|
|
193
|
+
this.processQueue();
|
|
194
|
+
}
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
/**
|
|
@@ -170,7 +208,7 @@ export class OutgoingLoadQueue {
|
|
|
170
208
|
sendCallback: () => void,
|
|
171
209
|
mode: LoadMode = "high-priority",
|
|
172
210
|
): void {
|
|
173
|
-
if (this.inFlightLoads.has(value)) {
|
|
211
|
+
if (this.inFlightLoads.has(value.id)) {
|
|
174
212
|
return;
|
|
175
213
|
}
|
|
176
214
|
|
|
@@ -270,7 +308,11 @@ export class OutgoingLoadQueue {
|
|
|
270
308
|
clearTimeout(this.timeoutHandle);
|
|
271
309
|
this.timeoutHandle = null;
|
|
272
310
|
}
|
|
311
|
+
const inFlightCount = this.inFlightLoads.size;
|
|
273
312
|
this.inFlightLoads.clear();
|
|
313
|
+
if (inFlightCount > 0) {
|
|
314
|
+
this.inFlightCounter.add(-inFlightCount);
|
|
315
|
+
}
|
|
274
316
|
|
|
275
317
|
// Drain existing queues to balance push/pull metrics
|
|
276
318
|
while (this.highPriorityPending.shift()) {}
|
|
@@ -32,7 +32,11 @@ export class PriorityBasedMessageQueue {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
public push(msg: SyncMessage) {
|
|
35
|
-
|
|
35
|
+
let priority = "priority" in msg ? msg.priority : this.defaultPriority;
|
|
36
|
+
|
|
37
|
+
if (msg.action === "reconcile") {
|
|
38
|
+
priority = CO_VALUE_PRIORITY.LOW;
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
this.getQueue(priority).push(msg);
|
|
38
42
|
}
|
|
@@ -42,4 +46,14 @@ export class PriorityBasedMessageQueue {
|
|
|
42
46
|
|
|
43
47
|
return this.queues[priority]?.shift();
|
|
44
48
|
}
|
|
49
|
+
|
|
50
|
+
public trackPushPull(msg: SyncMessage) {
|
|
51
|
+
let priority = "priority" in msg ? msg.priority : this.defaultPriority;
|
|
52
|
+
|
|
53
|
+
if (msg.action === "reconcile") {
|
|
54
|
+
priority = CO_VALUE_PRIORITY.LOW;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.getQueue(priority).trackPushPull();
|
|
58
|
+
}
|
|
45
59
|
}
|
|
@@ -89,6 +89,20 @@ export class StorageKnownState {
|
|
|
89
89
|
requests.add(req);
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
deleteKnownState(id: string) {
|
|
94
|
+
this.knownStates.delete(id);
|
|
95
|
+
|
|
96
|
+
for (const request of this.waitForSyncRequests.get(id) || []) {
|
|
97
|
+
request.resolve();
|
|
98
|
+
}
|
|
99
|
+
this.waitForSyncRequests.delete(id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
clear() {
|
|
103
|
+
this.knownStates.clear();
|
|
104
|
+
this.waitForSyncRequests.clear();
|
|
105
|
+
}
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
function isInSync(
|
|
@@ -12,12 +12,15 @@ import type {
|
|
|
12
12
|
DBTransactionInterfaceSync,
|
|
13
13
|
SessionRow,
|
|
14
14
|
SignatureAfterRow,
|
|
15
|
+
StorageReconciliationLockRow,
|
|
15
16
|
StoredCoValueRow,
|
|
16
17
|
StoredSessionRow,
|
|
17
18
|
TransactionRow,
|
|
19
|
+
StorageReconciliationAcquireResult,
|
|
18
20
|
} from "../types.js";
|
|
19
21
|
import { DeletedCoValueDeletionStatus } from "../types.js";
|
|
20
22
|
import type { SQLiteDatabaseDriver } from "./types.js";
|
|
23
|
+
import { STORAGE_RECONCILIATION_CONFIG } from "../../config.js";
|
|
21
24
|
|
|
22
25
|
export type RawCoValueRow = {
|
|
23
26
|
id: RawCoID;
|
|
@@ -268,11 +271,55 @@ export class SQLiteClient
|
|
|
268
271
|
);
|
|
269
272
|
}
|
|
270
273
|
|
|
274
|
+
getStorageReconciliationLock(
|
|
275
|
+
key: string,
|
|
276
|
+
): StorageReconciliationLockRow | undefined {
|
|
277
|
+
return this.db.get<StorageReconciliationLockRow>(
|
|
278
|
+
"SELECT * FROM storageReconciliationLocks WHERE key = ?",
|
|
279
|
+
[key],
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
putStorageReconciliationLock(entry: StorageReconciliationLockRow): void {
|
|
284
|
+
const {
|
|
285
|
+
key,
|
|
286
|
+
holderSessionId,
|
|
287
|
+
acquiredAt,
|
|
288
|
+
releasedAt,
|
|
289
|
+
lastProcessedOffset,
|
|
290
|
+
} = entry;
|
|
291
|
+
this.db.run(
|
|
292
|
+
`INSERT OR REPLACE INTO storageReconciliationLocks (key, holderSessionId, acquiredAt, releasedAt, lastProcessedOffset) VALUES (?, ?, ?, ?, ?)`,
|
|
293
|
+
[
|
|
294
|
+
key,
|
|
295
|
+
holderSessionId,
|
|
296
|
+
acquiredAt,
|
|
297
|
+
releasedAt ?? null,
|
|
298
|
+
lastProcessedOffset,
|
|
299
|
+
],
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
271
303
|
transaction(operationsCallback: (tx: DBTransactionInterfaceSync) => unknown) {
|
|
272
304
|
this.db.transaction(() => operationsCallback(this));
|
|
273
305
|
return undefined;
|
|
274
306
|
}
|
|
275
307
|
|
|
308
|
+
getCoValueIDs(limit: number, offset: number): { id: RawCoID }[] {
|
|
309
|
+
return this.db.query<{ id: RawCoID }>(
|
|
310
|
+
"SELECT id FROM coValues WHERE rowID > ? ORDER BY rowID LIMIT ?",
|
|
311
|
+
[offset, limit],
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
getCoValueCount(): number {
|
|
316
|
+
const row = this.db.get<{ count: number }>(
|
|
317
|
+
"SELECT COUNT(*) as count FROM coValues",
|
|
318
|
+
[],
|
|
319
|
+
);
|
|
320
|
+
return row?.count ?? 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
276
323
|
getUnsyncedCoValueIDs(): RawCoID[] {
|
|
277
324
|
const rows = this.db.query<{ co_value_id: RawCoID }>(
|
|
278
325
|
"SELECT DISTINCT co_value_id FROM unsynced_covalues",
|
|
@@ -303,6 +350,87 @@ export class SQLiteClient
|
|
|
303
350
|
this.db.run("DELETE FROM unsynced_covalues WHERE co_value_id = ?", [id]);
|
|
304
351
|
}
|
|
305
352
|
|
|
353
|
+
tryAcquireStorageReconciliationLock(
|
|
354
|
+
sessionId: SessionID,
|
|
355
|
+
peerId: PeerID,
|
|
356
|
+
): StorageReconciliationAcquireResult {
|
|
357
|
+
let result: StorageReconciliationAcquireResult = {
|
|
358
|
+
acquired: false,
|
|
359
|
+
reason: "not_due",
|
|
360
|
+
};
|
|
361
|
+
this.transaction(() => {
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const lockKey = `lock#${peerId}`;
|
|
364
|
+
const lockRow = this.getStorageReconciliationLock(lockKey);
|
|
365
|
+
if (
|
|
366
|
+
lockRow?.releasedAt &&
|
|
367
|
+
now - lockRow.releasedAt <
|
|
368
|
+
STORAGE_RECONCILIATION_CONFIG.RECONCILIATION_INTERVAL_MS
|
|
369
|
+
) {
|
|
370
|
+
result = { acquired: false, reason: "not_due" };
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const expiresAt = lockRow
|
|
374
|
+
? lockRow.acquiredAt + STORAGE_RECONCILIATION_CONFIG.LOCK_TTL_MS
|
|
375
|
+
: 0;
|
|
376
|
+
const isLockHeldByOtherSession = lockRow?.holderSessionId !== sessionId;
|
|
377
|
+
if (
|
|
378
|
+
lockRow &&
|
|
379
|
+
!lockRow.releasedAt &&
|
|
380
|
+
expiresAt >= now &&
|
|
381
|
+
isLockHeldByOtherSession
|
|
382
|
+
) {
|
|
383
|
+
result = { acquired: false, reason: "lock_held" };
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const lastProcessedOffset =
|
|
388
|
+
lockRow && !lockRow.releasedAt ? (lockRow.lastProcessedOffset ?? 0) : 0;
|
|
389
|
+
this.putStorageReconciliationLock({
|
|
390
|
+
key: lockKey,
|
|
391
|
+
holderSessionId: sessionId,
|
|
392
|
+
acquiredAt: now,
|
|
393
|
+
lastProcessedOffset,
|
|
394
|
+
});
|
|
395
|
+
result = { acquired: true, lastProcessedOffset };
|
|
396
|
+
});
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
renewStorageReconciliationLock(
|
|
401
|
+
sessionId: SessionID,
|
|
402
|
+
peerId: PeerID,
|
|
403
|
+
offset: number,
|
|
404
|
+
): void {
|
|
405
|
+
const lockKey = `lock#${peerId}`;
|
|
406
|
+
const lockRow = this.getStorageReconciliationLock(lockKey);
|
|
407
|
+
if (
|
|
408
|
+
lockRow &&
|
|
409
|
+
lockRow.holderSessionId === sessionId &&
|
|
410
|
+
!lockRow.releasedAt
|
|
411
|
+
) {
|
|
412
|
+
this.putStorageReconciliationLock({
|
|
413
|
+
...lockRow,
|
|
414
|
+
lastProcessedOffset: offset,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
releaseStorageReconciliationLock(sessionId: SessionID, peerId: PeerID): void {
|
|
420
|
+
this.transaction(() => {
|
|
421
|
+
const lockKey = `lock#${peerId}`;
|
|
422
|
+
const releasedAt = Date.now();
|
|
423
|
+
const lockRow = this.getStorageReconciliationLock(lockKey);
|
|
424
|
+
if (lockRow?.holderSessionId === sessionId) {
|
|
425
|
+
this.putStorageReconciliationLock({
|
|
426
|
+
...lockRow,
|
|
427
|
+
releasedAt,
|
|
428
|
+
lastProcessedOffset: 0,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
306
434
|
getCoValueKnownState(coValueId: string): CoValueKnownState | undefined {
|
|
307
435
|
// First check if the CoValue exists
|
|
308
436
|
const coValueRow = this.db.get<{ rowID: number }>(
|
|
@@ -47,6 +47,17 @@ export const migrations: Record<number, string[]> = {
|
|
|
47
47
|
) WITHOUT ROWID;`,
|
|
48
48
|
"CREATE INDEX IF NOT EXISTS deletedCoValuesByStatus ON deletedCoValues (status);",
|
|
49
49
|
],
|
|
50
|
+
6: [
|
|
51
|
+
`CREATE TABLE IF NOT EXISTS storageReconciliationLocks (
|
|
52
|
+
key TEXT PRIMARY KEY,
|
|
53
|
+
holderSessionId TEXT NOT NULL,
|
|
54
|
+
acquiredAt INTEGER NOT NULL,
|
|
55
|
+
expiresAt INTEGER NOT NULL,
|
|
56
|
+
lastProcessedOffset INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
releasedAt INTEGER
|
|
58
|
+
) WITHOUT ROWID;`,
|
|
59
|
+
],
|
|
60
|
+
7: ["ALTER TABLE storageReconciliationLocks DROP COLUMN expiresAt;"],
|
|
50
61
|
};
|
|
51
62
|
|
|
52
63
|
type Migration = {
|