cojson 0.20.8 → 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 +28 -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
|
@@ -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 = {
|
|
@@ -11,13 +11,16 @@ import type {
|
|
|
11
11
|
DBTransactionInterfaceAsync,
|
|
12
12
|
SessionRow,
|
|
13
13
|
SignatureAfterRow,
|
|
14
|
+
StorageReconciliationLockRow,
|
|
14
15
|
StoredCoValueRow,
|
|
15
16
|
StoredSessionRow,
|
|
16
17
|
TransactionRow,
|
|
18
|
+
StorageReconciliationAcquireResult,
|
|
17
19
|
} from "../types.js";
|
|
18
20
|
import { DeletedCoValueDeletionStatus } from "../types.js";
|
|
19
21
|
import type { SQLiteDatabaseDriverAsync } from "./types.js";
|
|
20
22
|
import type { PeerID } from "../../sync.js";
|
|
23
|
+
import { STORAGE_RECONCILIATION_CONFIG } from "../../config.js";
|
|
21
24
|
|
|
22
25
|
export type RawCoValueRow = {
|
|
23
26
|
id: RawCoID;
|
|
@@ -153,6 +156,37 @@ export class SQLiteTransactionAsync implements DBTransactionInterfaceAsync {
|
|
|
153
156
|
],
|
|
154
157
|
);
|
|
155
158
|
}
|
|
159
|
+
|
|
160
|
+
async getStorageReconciliationLock(
|
|
161
|
+
key: string,
|
|
162
|
+
): Promise<StorageReconciliationLockRow | undefined> {
|
|
163
|
+
return this.tx.get<StorageReconciliationLockRow>(
|
|
164
|
+
"SELECT * FROM storageReconciliationLocks WHERE key = ?",
|
|
165
|
+
[key],
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async putStorageReconciliationLock(
|
|
170
|
+
entry: StorageReconciliationLockRow,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
const {
|
|
173
|
+
key,
|
|
174
|
+
holderSessionId,
|
|
175
|
+
acquiredAt,
|
|
176
|
+
releasedAt,
|
|
177
|
+
lastProcessedOffset,
|
|
178
|
+
} = entry;
|
|
179
|
+
await this.tx.run(
|
|
180
|
+
`INSERT OR REPLACE INTO storageReconciliationLocks (key, holderSessionId, acquiredAt, releasedAt, lastProcessedOffset) VALUES (?, ?, ?, ?, ?)`,
|
|
181
|
+
[
|
|
182
|
+
key,
|
|
183
|
+
holderSessionId,
|
|
184
|
+
acquiredAt,
|
|
185
|
+
releasedAt ?? null,
|
|
186
|
+
lastProcessedOffset,
|
|
187
|
+
],
|
|
188
|
+
);
|
|
189
|
+
}
|
|
156
190
|
}
|
|
157
191
|
|
|
158
192
|
export class SQLiteClientAsync implements DBClientInterfaceAsync {
|
|
@@ -334,6 +368,111 @@ export class SQLiteClientAsync implements DBClientInterfaceAsync {
|
|
|
334
368
|
]);
|
|
335
369
|
}
|
|
336
370
|
|
|
371
|
+
async getCoValueIDs(
|
|
372
|
+
limit: number,
|
|
373
|
+
offset: number,
|
|
374
|
+
): Promise<{ id: RawCoID }[]> {
|
|
375
|
+
return this.db.query<{ id: RawCoID }>(
|
|
376
|
+
"SELECT id FROM coValues WHERE rowID > ? ORDER BY rowID LIMIT ?",
|
|
377
|
+
[offset, limit],
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async getCoValueCount(): Promise<number> {
|
|
382
|
+
const row = await this.db.get<{ count: number }>(
|
|
383
|
+
"SELECT COUNT(*) as count FROM coValues",
|
|
384
|
+
[],
|
|
385
|
+
);
|
|
386
|
+
return row?.count ?? 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async tryAcquireStorageReconciliationLock(
|
|
390
|
+
sessionId: SessionID,
|
|
391
|
+
peerId: PeerID,
|
|
392
|
+
): Promise<StorageReconciliationAcquireResult> {
|
|
393
|
+
let result: StorageReconciliationAcquireResult = {
|
|
394
|
+
acquired: false,
|
|
395
|
+
reason: "not_due",
|
|
396
|
+
};
|
|
397
|
+
await this.transaction(async (tx) => {
|
|
398
|
+
const now = Date.now();
|
|
399
|
+
const lockKey = `lock#${peerId}`;
|
|
400
|
+
|
|
401
|
+
const lockRow = await tx.getStorageReconciliationLock(lockKey);
|
|
402
|
+
if (
|
|
403
|
+
lockRow?.releasedAt &&
|
|
404
|
+
now - lockRow.releasedAt <
|
|
405
|
+
STORAGE_RECONCILIATION_CONFIG.RECONCILIATION_INTERVAL_MS
|
|
406
|
+
) {
|
|
407
|
+
result = { acquired: false, reason: "not_due" };
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const expiresAt = lockRow
|
|
411
|
+
? lockRow.acquiredAt + STORAGE_RECONCILIATION_CONFIG.LOCK_TTL_MS
|
|
412
|
+
: 0;
|
|
413
|
+
const isLockHeldByOtherSession = lockRow?.holderSessionId !== sessionId;
|
|
414
|
+
if (
|
|
415
|
+
lockRow &&
|
|
416
|
+
!lockRow.releasedAt &&
|
|
417
|
+
expiresAt >= now &&
|
|
418
|
+
isLockHeldByOtherSession
|
|
419
|
+
) {
|
|
420
|
+
result = { acquired: false, reason: "lock_held" };
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const lastProcessedOffset =
|
|
425
|
+
lockRow && !lockRow.releasedAt ? (lockRow.lastProcessedOffset ?? 0) : 0;
|
|
426
|
+
await tx.putStorageReconciliationLock({
|
|
427
|
+
key: lockKey,
|
|
428
|
+
holderSessionId: sessionId,
|
|
429
|
+
acquiredAt: now,
|
|
430
|
+
lastProcessedOffset,
|
|
431
|
+
});
|
|
432
|
+
result = { acquired: true, lastProcessedOffset };
|
|
433
|
+
});
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async renewStorageReconciliationLock(
|
|
438
|
+
sessionId: SessionID,
|
|
439
|
+
peerId: PeerID,
|
|
440
|
+
offset: number,
|
|
441
|
+
): Promise<void> {
|
|
442
|
+
await this.transaction(async (tx) => {
|
|
443
|
+
const lockKey = `lock#${peerId}`;
|
|
444
|
+
const lockRow = await tx.getStorageReconciliationLock(lockKey);
|
|
445
|
+
if (
|
|
446
|
+
lockRow &&
|
|
447
|
+
lockRow.holderSessionId === sessionId &&
|
|
448
|
+
!lockRow.releasedAt
|
|
449
|
+
) {
|
|
450
|
+
await tx.putStorageReconciliationLock({
|
|
451
|
+
...lockRow,
|
|
452
|
+
lastProcessedOffset: offset,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async releaseStorageReconciliationLock(
|
|
459
|
+
sessionId: SessionID,
|
|
460
|
+
peerId: PeerID,
|
|
461
|
+
): Promise<void> {
|
|
462
|
+
await this.transaction(async (tx) => {
|
|
463
|
+
const lockKey = `lock#${peerId}`;
|
|
464
|
+
const releasedAt = Date.now();
|
|
465
|
+
const lockRow = await tx.getStorageReconciliationLock(lockKey);
|
|
466
|
+
if (lockRow && lockRow.holderSessionId === sessionId) {
|
|
467
|
+
await tx.putStorageReconciliationLock({
|
|
468
|
+
...lockRow,
|
|
469
|
+
releasedAt,
|
|
470
|
+
lastProcessedOffset: 0,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
337
476
|
async getCoValueKnownState(
|
|
338
477
|
coValueId: string,
|
|
339
478
|
): Promise<CoValueKnownState | undefined> {
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
SignatureAfterRow,
|
|
31
31
|
StoredCoValueRow,
|
|
32
32
|
StoredSessionRow,
|
|
33
|
+
StorageReconciliationAcquireResult,
|
|
33
34
|
} from "./types.js";
|
|
34
35
|
import { isDeleteSessionID } from "../ids.js";
|
|
35
36
|
|
|
@@ -517,6 +518,40 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
517
518
|
this.dbClient.trackCoValuesSyncState(updates).then(() => done?.());
|
|
518
519
|
}
|
|
519
520
|
|
|
521
|
+
getCoValueIDs(
|
|
522
|
+
limit: number,
|
|
523
|
+
offset: number,
|
|
524
|
+
callback: (batch: { id: RawCoID }[]) => void,
|
|
525
|
+
): void {
|
|
526
|
+
this.dbClient.getCoValueIDs(limit, offset).then(callback);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
getCoValueCount(callback: (count: number) => void): void {
|
|
530
|
+
this.dbClient.getCoValueCount().then(callback);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
tryAcquireStorageReconciliationLock(
|
|
534
|
+
sessionId: SessionID,
|
|
535
|
+
peerId: PeerID,
|
|
536
|
+
callback: (result: StorageReconciliationAcquireResult) => void,
|
|
537
|
+
): void {
|
|
538
|
+
this.dbClient
|
|
539
|
+
.tryAcquireStorageReconciliationLock(sessionId, peerId)
|
|
540
|
+
.then(callback);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
renewStorageReconciliationLock(
|
|
544
|
+
sessionId: SessionID,
|
|
545
|
+
peerId: PeerID,
|
|
546
|
+
offset: number,
|
|
547
|
+
): void {
|
|
548
|
+
this.dbClient.renewStorageReconciliationLock(sessionId, peerId, offset);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
releaseStorageReconciliationLock(sessionId: SessionID, peerId: PeerID): void {
|
|
552
|
+
this.dbClient.releaseStorageReconciliationLock(sessionId, peerId);
|
|
553
|
+
}
|
|
554
|
+
|
|
520
555
|
getUnsyncedCoValueIDs(
|
|
521
556
|
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
522
557
|
): void {
|
|
@@ -529,11 +564,13 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
529
564
|
|
|
530
565
|
onCoValueUnmounted(id: RawCoID): void {
|
|
531
566
|
this.inMemoryCoValues.delete(id);
|
|
567
|
+
this.knownStates.deleteKnownState(id);
|
|
532
568
|
}
|
|
533
569
|
|
|
534
570
|
close() {
|
|
535
571
|
this.deletedCoValuesEraserScheduler?.dispose();
|
|
536
572
|
this.inMemoryCoValues.clear();
|
|
573
|
+
this.knownStates.clear();
|
|
537
574
|
return this.storeQueue.close();
|
|
538
575
|
}
|
|
539
576
|
}
|