cojson 0.19.18 → 0.19.20
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 +9 -0
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/SyncStateManager.d.ts +5 -2
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +49 -12
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/UnsyncedCoValuesTracker.d.ts +81 -0
- package/dist/UnsyncedCoValuesTracker.d.ts.map +1 -0
- package/dist/UnsyncedCoValuesTracker.js +209 -0
- package/dist/UnsyncedCoValuesTracker.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +11 -3
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +6 -1
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +9 -5
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +12 -8
- package/dist/localNode.js.map +1 -1
- package/dist/queue/IncomingMessagesQueue.d.ts +6 -7
- package/dist/queue/IncomingMessagesQueue.d.ts.map +1 -1
- package/dist/queue/IncomingMessagesQueue.js +7 -30
- package/dist/queue/IncomingMessagesQueue.js.map +1 -1
- package/dist/queue/LinkedList.d.ts +1 -1
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/StorageStreamingQueue.d.ts +43 -0
- package/dist/queue/StorageStreamingQueue.d.ts.map +1 -0
- package/dist/queue/StorageStreamingQueue.js +70 -0
- package/dist/queue/StorageStreamingQueue.js.map +1 -0
- package/dist/storage/knownState.d.ts +1 -1
- package/dist/storage/knownState.js +4 -4
- package/dist/storage/sqlite/client.d.ts +8 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +17 -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 +9 -0
- package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +8 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +19 -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 +9 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +17 -4
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +67 -44
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +35 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +38 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +181 -7
- package/dist/sync.js.map +1 -1
- package/dist/tests/IncomingMessagesQueue.test.js +4 -150
- package/dist/tests/IncomingMessagesQueue.test.js.map +1 -1
- package/dist/tests/StorageStreamingQueue.test.d.ts +2 -0
- package/dist/tests/StorageStreamingQueue.test.d.ts.map +1 -0
- package/dist/tests/StorageStreamingQueue.test.js +213 -0
- package/dist/tests/StorageStreamingQueue.test.js.map +1 -0
- package/dist/tests/SyncManager.processQueues.test.d.ts +2 -0
- package/dist/tests/SyncManager.processQueues.test.d.ts.map +1 -0
- package/dist/tests/SyncManager.processQueues.test.js +208 -0
- package/dist/tests/SyncManager.processQueues.test.js.map +1 -0
- package/dist/tests/SyncStateManager.test.js +3 -3
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +4 -0
- package/dist/tests/setup.js.map +1 -0
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +19 -19
- package/dist/tests/sync.storage.test.js +176 -20
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.test.js +1 -1
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/sync.tracking.test.d.ts +2 -0
- package/dist/tests/sync.tracking.test.d.ts.map +1 -0
- package/dist/tests/sync.tracking.test.js +261 -0
- package/dist/tests/sync.tracking.test.js.map +1 -0
- package/dist/tests/testUtils.d.ts +4 -3
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +4 -4
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/PeerState.ts +2 -2
- package/src/SyncStateManager.ts +63 -12
- package/src/UnsyncedCoValuesTracker.ts +272 -0
- package/src/config.ts +13 -0
- package/src/exports.ts +10 -1
- package/src/localNode.ts +15 -3
- package/src/queue/IncomingMessagesQueue.ts +7 -39
- package/src/queue/LinkedList.ts +1 -1
- package/src/queue/StorageStreamingQueue.ts +96 -0
- package/src/storage/knownState.ts +4 -4
- package/src/storage/sqlite/client.ts +31 -0
- package/src/storage/sqlite/sqliteMigrations.ts +9 -0
- package/src/storage/sqliteAsync/client.ts +35 -0
- package/src/storage/storageAsync.ts +18 -1
- package/src/storage/storageSync.ts +119 -56
- package/src/storage/types.ts +42 -0
- package/src/sync.ts +235 -8
- package/src/tests/IncomingMessagesQueue.test.ts +4 -206
- package/src/tests/StorageStreamingQueue.test.ts +276 -0
- package/src/tests/SyncManager.processQueues.test.ts +287 -0
- package/src/tests/SyncStateManager.test.ts +3 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +11 -0
- package/src/tests/setup.ts +4 -0
- package/src/tests/sync.garbageCollection.test.ts +1 -3
- package/src/tests/sync.mesh.test.ts +19 -19
- package/src/tests/sync.storage.test.ts +224 -32
- package/src/tests/sync.test.ts +1 -9
- package/src/tests/sync.tracking.test.ts +396 -0
- package/src/tests/testUtils.ts +11 -5
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { RawCoID } from "./ids.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
import type { PeerID } from "./sync.js";
|
|
4
|
+
import type { StorageAPI } from "./storage/types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Used to track a CoValue that hasn't been synced to any peer,
|
|
8
|
+
* because none is currently connected.
|
|
9
|
+
*/
|
|
10
|
+
const ANY_PEER_ID: PeerID = "any";
|
|
11
|
+
|
|
12
|
+
// Flush pending updates to storage after 200ms
|
|
13
|
+
let BATCH_DELAY_MS = 200;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set the delay for flushing pending sync state updates to storage.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function setSyncStateTrackingBatchDelay(delay: number): void {
|
|
20
|
+
BATCH_DELAY_MS = delay;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PendingUpdate = {
|
|
24
|
+
id: RawCoID;
|
|
25
|
+
peerId: PeerID;
|
|
26
|
+
synced: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tracks CoValues that have unsynced changes to specific peers.
|
|
31
|
+
* Maintains an in-memory map and periodically persists to storage.
|
|
32
|
+
*/
|
|
33
|
+
export class UnsyncedCoValuesTracker {
|
|
34
|
+
private unsynced: Map<RawCoID, Set<PeerID>> = new Map();
|
|
35
|
+
private coValueListeners: Map<RawCoID, Set<(synced: boolean) => void>> =
|
|
36
|
+
new Map();
|
|
37
|
+
// Listeners for global "all synced" status changes
|
|
38
|
+
private globalListeners: Set<(synced: boolean) => void> = new Set();
|
|
39
|
+
|
|
40
|
+
// Pending updates to be persisted
|
|
41
|
+
private pendingUpdates: PendingUpdate[] = [];
|
|
42
|
+
private flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
43
|
+
|
|
44
|
+
private storage?: StorageAPI;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add a CoValue as unsynced to a specific peer.
|
|
48
|
+
* Triggers persistence if storage is available.
|
|
49
|
+
* @returns true if the CoValue was already tracked, false otherwise.
|
|
50
|
+
*/
|
|
51
|
+
add(id: RawCoID, peerId: PeerID = ANY_PEER_ID): boolean {
|
|
52
|
+
if (!this.unsynced.has(id)) {
|
|
53
|
+
this.unsynced.set(id, new Set());
|
|
54
|
+
}
|
|
55
|
+
const peerSet = this.unsynced.get(id)!;
|
|
56
|
+
|
|
57
|
+
const alreadyTracked = peerSet.has(peerId);
|
|
58
|
+
if (!alreadyTracked) {
|
|
59
|
+
// Only update if this is a new peer
|
|
60
|
+
peerSet.add(peerId);
|
|
61
|
+
|
|
62
|
+
this.schedulePersist(id, peerId, false);
|
|
63
|
+
|
|
64
|
+
this.notifyCoValueListeners(id, false);
|
|
65
|
+
this.notifyGlobalListeners(false);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return alreadyTracked;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove a CoValue from being unsynced to a specific peer.
|
|
73
|
+
* Triggers persistence if storage is available.
|
|
74
|
+
*/
|
|
75
|
+
remove(id: RawCoID, peerId: PeerID = ANY_PEER_ID): void {
|
|
76
|
+
const peerSet = this.unsynced.get(id);
|
|
77
|
+
if (!peerSet || !peerSet.has(peerId)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
peerSet.delete(peerId);
|
|
82
|
+
|
|
83
|
+
// If no more unsynced peers for this CoValue, remove the entry
|
|
84
|
+
if (peerSet.size === 0) {
|
|
85
|
+
this.unsynced.delete(id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.schedulePersist(id, peerId, true);
|
|
89
|
+
|
|
90
|
+
const isSynced = !this.unsynced.has(id);
|
|
91
|
+
this.notifyCoValueListeners(id, isSynced);
|
|
92
|
+
this.notifyGlobalListeners(this.isAllSynced());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove all tracking for a CoValue (all peers).
|
|
97
|
+
* Triggers persistence if storage is available.
|
|
98
|
+
*/
|
|
99
|
+
removeAll(id: RawCoID): void {
|
|
100
|
+
const peerSet = this.unsynced.get(id);
|
|
101
|
+
if (!peerSet) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remove all peers for this CoValue
|
|
106
|
+
const peersToRemove = Array.from(peerSet);
|
|
107
|
+
for (const peerId of peersToRemove) {
|
|
108
|
+
this.remove(id, peerId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
forcePersist(): Promise<void> | undefined {
|
|
113
|
+
return this.flush();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private schedulePersist(id: RawCoID, peerId: PeerID, synced: boolean): void {
|
|
117
|
+
const storage = this.storage;
|
|
118
|
+
if (!storage) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.pendingUpdates.push({ id, peerId, synced });
|
|
123
|
+
if (!this.flushTimer) {
|
|
124
|
+
this.flushTimer = setTimeout(() => {
|
|
125
|
+
this.flush();
|
|
126
|
+
}, BATCH_DELAY_MS);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Flush all pending persistence updates in a batch
|
|
132
|
+
*/
|
|
133
|
+
private flush(): Promise<void> | undefined {
|
|
134
|
+
if (this.flushTimer) {
|
|
135
|
+
clearTimeout(this.flushTimer);
|
|
136
|
+
this.flushTimer = undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.pendingUpdates.length === 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const storage = this.storage;
|
|
144
|
+
if (!storage) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const filteredUpdates = this.simplifyPendingUpdates(this.pendingUpdates);
|
|
149
|
+
this.pendingUpdates = [];
|
|
150
|
+
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
try {
|
|
153
|
+
storage.trackCoValuesSyncState(filteredUpdates, () => resolve());
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.warn("Failed to persist batched unsynced CoValue tracking", {
|
|
156
|
+
err: error,
|
|
157
|
+
});
|
|
158
|
+
resolve();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all CoValue IDs that have at least one unsynced peer.
|
|
165
|
+
*/
|
|
166
|
+
getAll(): RawCoID[] {
|
|
167
|
+
return Array.from(this.unsynced.keys());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if all CoValues are synced
|
|
172
|
+
*/
|
|
173
|
+
isAllSynced(): boolean {
|
|
174
|
+
return this.unsynced.size === 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if a specific CoValue is tracked as unsynced.
|
|
179
|
+
*/
|
|
180
|
+
has(id: RawCoID): boolean {
|
|
181
|
+
return this.unsynced.has(id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Subscribe to changes in whether a specific CoValue is synced.
|
|
186
|
+
* The listener is called immediately with the current state.
|
|
187
|
+
* @returns Unsubscribe function
|
|
188
|
+
*/
|
|
189
|
+
subscribe(id: RawCoID, listener: (synced: boolean) => void): () => void;
|
|
190
|
+
/**
|
|
191
|
+
* Subscribe to changes in whether all CoValues are synced.
|
|
192
|
+
* The listener is called immediately with the current state.
|
|
193
|
+
* @returns Unsubscribe function
|
|
194
|
+
*/
|
|
195
|
+
subscribe(listener: (synced: boolean) => void): () => void;
|
|
196
|
+
subscribe(
|
|
197
|
+
idOrListener: RawCoID | ((synced: boolean) => void),
|
|
198
|
+
listener?: (synced: boolean) => void,
|
|
199
|
+
): () => void {
|
|
200
|
+
if (typeof idOrListener === "string" && listener) {
|
|
201
|
+
const id = idOrListener;
|
|
202
|
+
if (!this.coValueListeners.has(id)) {
|
|
203
|
+
this.coValueListeners.set(id, new Set());
|
|
204
|
+
}
|
|
205
|
+
this.coValueListeners.get(id)!.add(listener);
|
|
206
|
+
|
|
207
|
+
// Call immediately with current state
|
|
208
|
+
const isSynced = !this.unsynced.has(id);
|
|
209
|
+
listener(isSynced);
|
|
210
|
+
|
|
211
|
+
return () => {
|
|
212
|
+
const listeners = this.coValueListeners.get(id);
|
|
213
|
+
if (listeners) {
|
|
214
|
+
listeners.delete(listener);
|
|
215
|
+
if (listeners.size === 0) {
|
|
216
|
+
this.coValueListeners.delete(id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const globalListener = idOrListener as (synced: boolean) => void;
|
|
223
|
+
this.globalListeners.add(globalListener);
|
|
224
|
+
|
|
225
|
+
// Call immediately with current state
|
|
226
|
+
globalListener(this.isAllSynced());
|
|
227
|
+
|
|
228
|
+
return () => {
|
|
229
|
+
this.globalListeners.delete(globalListener);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setStorage(storage: StorageAPI) {
|
|
234
|
+
this.storage = storage;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
removeStorage() {
|
|
238
|
+
this.storage = undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Notify all listeners for a specific CoValue about sync status change.
|
|
243
|
+
*/
|
|
244
|
+
private notifyCoValueListeners(id: RawCoID, synced: boolean): void {
|
|
245
|
+
const listeners = this.coValueListeners.get(id);
|
|
246
|
+
if (listeners) {
|
|
247
|
+
for (const listener of listeners) {
|
|
248
|
+
listener(synced);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Notify all global listeners about "all synced" status change.
|
|
255
|
+
*/
|
|
256
|
+
private notifyGlobalListeners(allSynced: boolean): void {
|
|
257
|
+
for (const listener of this.globalListeners) {
|
|
258
|
+
listener(allSynced);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Keep only the last update for each (id, peerId) combination
|
|
264
|
+
*/
|
|
265
|
+
private simplifyPendingUpdates(updates: PendingUpdate[]): PendingUpdate[] {
|
|
266
|
+
const latestUpdates = new Map<string, PendingUpdate>();
|
|
267
|
+
for (const update of updates) {
|
|
268
|
+
latestUpdates.set(`${update.id}|${update.peerId}`, update);
|
|
269
|
+
}
|
|
270
|
+
return Array.from(latestUpdates.values());
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -60,3 +60,16 @@ export function setGarbageCollectorMaxAge(maxAge: number) {
|
|
|
60
60
|
export function setGarbageCollectorInterval(interval: number) {
|
|
61
61
|
GARBAGE_COLLECTOR_CONFIG.INTERVAL = interval;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
export const WEBSOCKET_CONFIG = {
|
|
65
|
+
MAX_OUTGOING_MESSAGES_CHUNK_BYTES: 25_000,
|
|
66
|
+
OUTGOING_MESSAGES_CHUNK_DELAY: 5,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function setMaxOutgoingMessagesChunkBytes(bytes: number) {
|
|
70
|
+
WEBSOCKET_CONFIG.MAX_OUTGOING_MESSAGES_CHUNK_BYTES = bytes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setOutgoingMessagesChunkDelay(delay: number) {
|
|
74
|
+
WEBSOCKET_CONFIG.OUTGOING_MESSAGES_CHUNK_DELAY = delay;
|
|
75
|
+
}
|
package/src/exports.ts
CHANGED
|
@@ -65,12 +65,13 @@ import type { AgentID, RawCoID, SessionID } from "./ids.js";
|
|
|
65
65
|
import type { JsonObject, JsonValue } from "./jsonValue.js";
|
|
66
66
|
import type * as Media from "./media.js";
|
|
67
67
|
import { isAccountRole } from "./permissions.js";
|
|
68
|
-
import type { Peer, SyncMessage } from "./sync.js";
|
|
68
|
+
import type { Peer, SyncMessage, SyncWhen } from "./sync.js";
|
|
69
69
|
import {
|
|
70
70
|
DisconnectedError,
|
|
71
71
|
SyncManager,
|
|
72
72
|
hwrServerPeerSelector,
|
|
73
73
|
} from "./sync.js";
|
|
74
|
+
import { setSyncStateTrackingBatchDelay } from "./UnsyncedCoValuesTracker.js";
|
|
74
75
|
import { emptyKnownState } from "./knownState.js";
|
|
75
76
|
|
|
76
77
|
import {
|
|
@@ -81,10 +82,13 @@ import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
|
|
|
81
82
|
import {
|
|
82
83
|
CO_VALUE_LOADING_CONFIG,
|
|
83
84
|
TRANSACTION_CONFIG,
|
|
85
|
+
WEBSOCKET_CONFIG,
|
|
84
86
|
setCoValueLoadingMaxRetries,
|
|
85
87
|
setCoValueLoadingRetryDelay,
|
|
86
88
|
setCoValueLoadingTimeout,
|
|
87
89
|
setIncomingMessagesTimeBudget,
|
|
90
|
+
setMaxOutgoingMessagesChunkBytes,
|
|
91
|
+
setOutgoingMessagesChunkDelay,
|
|
88
92
|
setMaxRecommendedTxSize,
|
|
89
93
|
} from "./config.js";
|
|
90
94
|
import { LogLevel, logger } from "./logger.js";
|
|
@@ -125,6 +129,7 @@ export const cojsonInternals = {
|
|
|
125
129
|
setCoValueLoadingRetryDelay,
|
|
126
130
|
setCoValueLoadingMaxRetries,
|
|
127
131
|
setCoValueLoadingTimeout,
|
|
132
|
+
setSyncStateTrackingBatchDelay,
|
|
128
133
|
ConnectedPeerChannel,
|
|
129
134
|
textEncoder,
|
|
130
135
|
textDecoder,
|
|
@@ -133,6 +138,9 @@ export const cojsonInternals = {
|
|
|
133
138
|
TRANSACTION_CONFIG,
|
|
134
139
|
setMaxRecommendedTxSize,
|
|
135
140
|
canBeBranched,
|
|
141
|
+
WEBSOCKET_CONFIG,
|
|
142
|
+
setMaxOutgoingMessagesChunkBytes,
|
|
143
|
+
setOutgoingMessagesChunkDelay,
|
|
136
144
|
};
|
|
137
145
|
|
|
138
146
|
export {
|
|
@@ -193,6 +201,7 @@ export type {
|
|
|
193
201
|
AccountRole,
|
|
194
202
|
AvailableCoValueCore,
|
|
195
203
|
PeerState,
|
|
204
|
+
SyncWhen,
|
|
196
205
|
CoValueHeader,
|
|
197
206
|
};
|
|
198
207
|
|
package/src/localNode.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { AgentSecret, CryptoProvider } from "./crypto/crypto.js";
|
|
|
34
34
|
import { AgentID, RawCoID, SessionID, isAgentID, isRawCoID } from "./ids.js";
|
|
35
35
|
import { logger } from "./logger.js";
|
|
36
36
|
import { StorageAPI } from "./storage/index.js";
|
|
37
|
-
import { Peer, PeerID, SyncManager } from "./sync.js";
|
|
37
|
+
import { Peer, PeerID, SyncManager, type SyncWhen } from "./sync.js";
|
|
38
38
|
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
|
|
39
39
|
import { expectGroup } from "./typeUtils/expectGroup.js";
|
|
40
40
|
import { canBeBranched } from "./coValueCore/branching.js";
|
|
@@ -75,6 +75,7 @@ export class LocalNode {
|
|
|
75
75
|
agentSecret: AgentSecret,
|
|
76
76
|
currentSessionID: SessionID,
|
|
77
77
|
crypto: CryptoProvider,
|
|
78
|
+
public readonly syncWhen?: SyncWhen,
|
|
78
79
|
) {
|
|
79
80
|
this.agentSecret = agentSecret;
|
|
80
81
|
this.currentSessionID = currentSessionID;
|
|
@@ -94,11 +95,13 @@ export class LocalNode {
|
|
|
94
95
|
|
|
95
96
|
setStorage(storage: StorageAPI) {
|
|
96
97
|
this.storage = storage;
|
|
98
|
+
this.syncManager.setStorage(storage);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
removeStorage() {
|
|
100
102
|
this.storage?.close();
|
|
101
103
|
this.storage = undefined;
|
|
104
|
+
this.syncManager.removeStorage();
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
hasCoValue(id: RawCoID) {
|
|
@@ -178,12 +181,14 @@ export class LocalNode {
|
|
|
178
181
|
crypto: CryptoProvider;
|
|
179
182
|
initialAgentSecret?: AgentSecret;
|
|
180
183
|
peers?: Peer[];
|
|
184
|
+
syncWhen?: SyncWhen;
|
|
181
185
|
storage?: StorageAPI;
|
|
182
186
|
}): RawAccount {
|
|
183
187
|
const {
|
|
184
188
|
crypto,
|
|
185
189
|
initialAgentSecret = crypto.newRandomAgentSecret(),
|
|
186
190
|
peers = [],
|
|
191
|
+
syncWhen,
|
|
187
192
|
} = opts;
|
|
188
193
|
const accountHeader = accountHeaderForInitialAgentSecret(
|
|
189
194
|
initialAgentSecret,
|
|
@@ -195,6 +200,7 @@ export class LocalNode {
|
|
|
195
200
|
initialAgentSecret,
|
|
196
201
|
crypto.newRandomSessionID(accountID as RawAccountID),
|
|
197
202
|
crypto,
|
|
203
|
+
syncWhen,
|
|
198
204
|
);
|
|
199
205
|
|
|
200
206
|
if (opts.storage) {
|
|
@@ -236,6 +242,7 @@ export class LocalNode {
|
|
|
236
242
|
static async withNewlyCreatedAccount({
|
|
237
243
|
creationProps,
|
|
238
244
|
peers,
|
|
245
|
+
syncWhen,
|
|
239
246
|
migration,
|
|
240
247
|
crypto,
|
|
241
248
|
initialAgentSecret = crypto.newRandomAgentSecret(),
|
|
@@ -243,6 +250,7 @@ export class LocalNode {
|
|
|
243
250
|
}: {
|
|
244
251
|
creationProps: { name: string };
|
|
245
252
|
peers?: Peer[];
|
|
253
|
+
syncWhen?: SyncWhen;
|
|
246
254
|
migration?: RawAccountMigration<AccountMeta>;
|
|
247
255
|
crypto: CryptoProvider;
|
|
248
256
|
initialAgentSecret?: AgentSecret;
|
|
@@ -257,6 +265,7 @@ export class LocalNode {
|
|
|
257
265
|
crypto,
|
|
258
266
|
initialAgentSecret,
|
|
259
267
|
peers,
|
|
268
|
+
syncWhen,
|
|
260
269
|
storage,
|
|
261
270
|
});
|
|
262
271
|
const node = account.core.node;
|
|
@@ -299,6 +308,7 @@ export class LocalNode {
|
|
|
299
308
|
accountSecret,
|
|
300
309
|
sessionID,
|
|
301
310
|
peers,
|
|
311
|
+
syncWhen,
|
|
302
312
|
crypto,
|
|
303
313
|
migration,
|
|
304
314
|
storage,
|
|
@@ -307,6 +317,7 @@ export class LocalNode {
|
|
|
307
317
|
accountSecret: AgentSecret;
|
|
308
318
|
sessionID: SessionID | undefined;
|
|
309
319
|
peers: Peer[];
|
|
320
|
+
syncWhen?: SyncWhen;
|
|
310
321
|
crypto: CryptoProvider;
|
|
311
322
|
migration?: RawAccountMigration<AccountMeta>;
|
|
312
323
|
storage?: StorageAPI;
|
|
@@ -318,6 +329,7 @@ export class LocalNode {
|
|
|
318
329
|
accountSecret,
|
|
319
330
|
sessionID || crypto.newRandomSessionID(accountID),
|
|
320
331
|
crypto,
|
|
332
|
+
syncWhen,
|
|
321
333
|
);
|
|
322
334
|
|
|
323
335
|
if (storage) {
|
|
@@ -817,9 +829,9 @@ export class LocalNode {
|
|
|
817
829
|
*
|
|
818
830
|
* @returns Promise of the current pending store operation, if any.
|
|
819
831
|
*/
|
|
820
|
-
gracefulShutdown(): Promise<unknown>
|
|
821
|
-
this.syncManager.gracefulShutdown();
|
|
832
|
+
async gracefulShutdown(): Promise<unknown> {
|
|
822
833
|
this.garbageCollector?.stop();
|
|
834
|
+
await this.syncManager.gracefulShutdown();
|
|
823
835
|
return this.storage?.close();
|
|
824
836
|
}
|
|
825
837
|
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { Counter, ValueType, metrics } from "@opentelemetry/api";
|
|
2
2
|
import type { PeerState } from "../PeerState.js";
|
|
3
|
-
import { SYNC_SCHEDULER_CONFIG } from "../config.js";
|
|
4
|
-
import { logger } from "../logger.js";
|
|
5
3
|
import type { SyncMessage } from "../sync.js";
|
|
6
4
|
import { LinkedList } from "./LinkedList.js";
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
|
-
* A queue that
|
|
7
|
+
* A queue that manages incoming sync messages across different peers using a round-robin approach.
|
|
10
8
|
*
|
|
11
9
|
* This class manages incoming sync messages from multiple peers, ensuring fair processing
|
|
12
|
-
* by cycling through each peer's message queue in a round-robin fashion.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* by cycling through each peer's message queue in a round-robin fashion.
|
|
11
|
+
*
|
|
12
|
+
* Queue processing and scheduling is handled by SyncManager.processQueues().
|
|
15
13
|
*/
|
|
16
14
|
export class IncomingMessagesQueue {
|
|
17
15
|
private pullCounter: Counter;
|
|
@@ -21,7 +19,7 @@ export class IncomingMessagesQueue {
|
|
|
21
19
|
peerToQueue: WeakMap<PeerState, LinkedList<SyncMessage>>;
|
|
22
20
|
currentQueue = 0;
|
|
23
21
|
|
|
24
|
-
constructor() {
|
|
22
|
+
constructor(private processQueues: () => void) {
|
|
25
23
|
this.pullCounter = metrics
|
|
26
24
|
.getMeter("cojson")
|
|
27
25
|
.createCounter(`jazz.messagequeue.incoming.pulled`, {
|
|
@@ -74,6 +72,8 @@ export class IncomingMessagesQueue {
|
|
|
74
72
|
this.pushCounter.add(1, {
|
|
75
73
|
peerRole: peer.role,
|
|
76
74
|
});
|
|
75
|
+
|
|
76
|
+
this.processQueues();
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
public pull() {
|
|
@@ -107,36 +107,4 @@ export class IncomingMessagesQueue {
|
|
|
107
107
|
|
|
108
108
|
return undefined;
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
processing = false;
|
|
112
|
-
|
|
113
|
-
async processQueue(callback: (msg: SyncMessage, peer: PeerState) => void) {
|
|
114
|
-
this.processing = true;
|
|
115
|
-
|
|
116
|
-
let entry: { msg: SyncMessage; peer: PeerState } | undefined;
|
|
117
|
-
let lastTimer = performance.now();
|
|
118
|
-
|
|
119
|
-
while ((entry = this.pull())) {
|
|
120
|
-
const { msg, peer } = entry;
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
callback(msg, peer);
|
|
124
|
-
} catch (err) {
|
|
125
|
-
logger.error("Error processing message", { err });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const currentTimer = performance.now();
|
|
129
|
-
|
|
130
|
-
// We check if we have blocked the main thread for too long
|
|
131
|
-
// and if so, we schedule a timer task to yield to the event loop
|
|
132
|
-
if (
|
|
133
|
-
currentTimer - lastTimer >
|
|
134
|
-
SYNC_SCHEDULER_CONFIG.INCOMING_MESSAGES_TIME_BUDGET
|
|
135
|
-
) {
|
|
136
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
this.processing = false;
|
|
141
|
-
}
|
|
142
110
|
}
|
package/src/queue/LinkedList.ts
CHANGED
|
@@ -108,7 +108,7 @@ class QueueMeter {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
export function meteredList<T>(
|
|
111
|
-
type: "incoming" | "outgoing",
|
|
111
|
+
type: "incoming" | "outgoing" | "storage-streaming",
|
|
112
112
|
attrs?: Record<string, string | number>,
|
|
113
113
|
) {
|
|
114
114
|
return new LinkedList<T>(new QueueMeter("jazz.messagequeue." + type, attrs));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { CO_VALUE_PRIORITY, type CoValuePriority } from "../priority.js";
|
|
2
|
+
import { LinkedList, meteredList } from "./LinkedList.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A callback that pushes content when invoked.
|
|
6
|
+
* Content is only fetched from the database when this callback is called.
|
|
7
|
+
*/
|
|
8
|
+
export type ContentCallback = () => void;
|
|
9
|
+
|
|
10
|
+
// All priorities use the queue, processed in order: HIGH > MEDIUM > LOW
|
|
11
|
+
const PRIORITY_TO_QUEUE_INDEX = {
|
|
12
|
+
[CO_VALUE_PRIORITY.HIGH]: 0,
|
|
13
|
+
[CO_VALUE_PRIORITY.MEDIUM]: 1,
|
|
14
|
+
[CO_VALUE_PRIORITY.LOW]: 2,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
type StreamingQueueTuple = [
|
|
18
|
+
high: LinkedList<ContentCallback>,
|
|
19
|
+
medium: LinkedList<ContentCallback>,
|
|
20
|
+
low: LinkedList<ContentCallback>,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A priority-based queue for storage content streaming.
|
|
25
|
+
*
|
|
26
|
+
* This queue manages content streaming for all priority levels (HIGH, MEDIUM, LOW).
|
|
27
|
+
* Content is processed in priority order: HIGH first, then MEDIUM, then LOW.
|
|
28
|
+
*
|
|
29
|
+
* Key features:
|
|
30
|
+
* - Stores callbacks to get content (lazy evaluation) rather than content itself
|
|
31
|
+
* - Priority-based ordering: HIGH > MEDIUM > LOW
|
|
32
|
+
*/
|
|
33
|
+
export class StorageStreamingQueue {
|
|
34
|
+
private queues: StreamingQueueTuple;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
this.queues = [
|
|
38
|
+
meteredList("storage-streaming", { priority: CO_VALUE_PRIORITY.HIGH }),
|
|
39
|
+
meteredList("storage-streaming", { priority: CO_VALUE_PRIORITY.MEDIUM }),
|
|
40
|
+
meteredList("storage-streaming", { priority: CO_VALUE_PRIORITY.LOW }),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private getQueue(priority: CoValuePriority) {
|
|
45
|
+
return this.queues[PRIORITY_TO_QUEUE_INDEX[priority]];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Push a content callback to the queue with explicit priority.
|
|
50
|
+
* The callback will be invoked when the entry is pulled and processed.
|
|
51
|
+
*
|
|
52
|
+
* @param entry - Callback that pushes content when invoked
|
|
53
|
+
* @param priority - Priority for this entry (HIGH, MEDIUM, or LOW)
|
|
54
|
+
*/
|
|
55
|
+
public push(entry: ContentCallback, priority: CoValuePriority): void {
|
|
56
|
+
this.getQueue(priority).push(entry);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pull the next entry from the queue.
|
|
61
|
+
* Returns undefined if no entries are available.
|
|
62
|
+
* Priority order: HIGH > MEDIUM > LOW
|
|
63
|
+
*/
|
|
64
|
+
public pull(): ContentCallback | undefined {
|
|
65
|
+
// Find the first non-empty queue (HIGH > MEDIUM > LOW)
|
|
66
|
+
const queueIndex = this.queues.findIndex((queue) => queue.length > 0);
|
|
67
|
+
|
|
68
|
+
if (queueIndex === -1) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const entry = this.queues[queueIndex]?.shift();
|
|
73
|
+
|
|
74
|
+
if (!entry) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if the queue is empty (no pending entries).
|
|
83
|
+
*/
|
|
84
|
+
public isEmpty(): boolean {
|
|
85
|
+
return this.queues.every((queue) => queue.length === 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private listener: (() => void) | undefined;
|
|
89
|
+
setListener(listener: () => void): void {
|
|
90
|
+
this.listener = listener;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
emit(): void {
|
|
94
|
+
this.listener?.();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
* and provides the API to wait for the data to be fully stored.
|
|
12
12
|
*/
|
|
13
13
|
export class StorageKnownState {
|
|
14
|
-
|
|
14
|
+
knownStates = new Map<string, CoValueKnownState>();
|
|
15
15
|
|
|
16
16
|
getKnownState(id: string): CoValueKnownState {
|
|
17
|
-
const knownState = this.
|
|
17
|
+
const knownState = this.knownStates.get(id);
|
|
18
18
|
|
|
19
19
|
if (!knownState) {
|
|
20
20
|
const empty = emptyKnownState(id as RawCoID);
|
|
21
|
-
this.
|
|
21
|
+
this.knownStates.set(id, empty);
|
|
22
22
|
return empty;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ export class StorageKnownState {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
setKnownState(id: string, knownState: CoValueKnownState) {
|
|
29
|
-
this.
|
|
29
|
+
this.knownStates.set(id, knownState);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
handleUpdate(id: string, knownState: CoValueKnownState) {
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from "../../coValueCore/verifiedState.js";
|
|
5
5
|
import type { Signature } from "../../crypto/crypto.js";
|
|
6
6
|
import type { RawCoID, SessionID } from "../../exports.js";
|
|
7
|
+
import type { PeerID } from "../../sync.js";
|
|
7
8
|
import { logger } from "../../logger.js";
|
|
8
9
|
import type {
|
|
9
10
|
DBClientInterfaceSync,
|
|
@@ -193,4 +194,34 @@ export class SQLiteClient
|
|
|
193
194
|
this.db.transaction(() => operationsCallback(this));
|
|
194
195
|
return undefined;
|
|
195
196
|
}
|
|
197
|
+
|
|
198
|
+
getUnsyncedCoValueIDs(): RawCoID[] {
|
|
199
|
+
const rows = this.db.query<{ co_value_id: RawCoID }>(
|
|
200
|
+
"SELECT DISTINCT co_value_id FROM unsynced_covalues",
|
|
201
|
+
[],
|
|
202
|
+
) as { co_value_id: RawCoID }[];
|
|
203
|
+
return rows.map((row) => row.co_value_id);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
trackCoValuesSyncState(
|
|
207
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
208
|
+
): void {
|
|
209
|
+
for (const update of updates) {
|
|
210
|
+
if (update.synced) {
|
|
211
|
+
this.db.run(
|
|
212
|
+
"DELETE FROM unsynced_covalues WHERE co_value_id = ? AND peer_id = ?",
|
|
213
|
+
[update.id, update.peerId],
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
this.db.run(
|
|
217
|
+
"INSERT OR REPLACE INTO unsynced_covalues (co_value_id, peer_id) VALUES (?, ?)",
|
|
218
|
+
[update.id, update.peerId],
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
stopTrackingSyncState(id: RawCoID): void {
|
|
225
|
+
this.db.run("DELETE FROM unsynced_covalues WHERE co_value_id = ?", [id]);
|
|
226
|
+
}
|
|
196
227
|
}
|