cojson 0.19.18 → 0.19.19
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/exports.d.ts +4 -2
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +2 -0
- 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/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 +9 -2
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +11 -0
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +33 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +21 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +107 -2
- package/dist/sync.js.map +1 -1
- 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/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 +2 -1
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +2 -2
- 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/exports.ts +4 -1
- package/src/localNode.ts +15 -3
- 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 +20 -1
- package/src/storage/types.ts +39 -0
- package/src/sync.ts +151 -3
- package/src/tests/SyncStateManager.test.ts +3 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +11 -0
- package/src/tests/sync.tracking.test.ts +396 -0
- package/src/tests/testUtils.ts +9 -3
|
@@ -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/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 {
|
|
@@ -125,6 +126,7 @@ export const cojsonInternals = {
|
|
|
125
126
|
setCoValueLoadingRetryDelay,
|
|
126
127
|
setCoValueLoadingMaxRetries,
|
|
127
128
|
setCoValueLoadingTimeout,
|
|
129
|
+
setSyncStateTrackingBatchDelay,
|
|
128
130
|
ConnectedPeerChannel,
|
|
129
131
|
textEncoder,
|
|
130
132
|
textDecoder,
|
|
@@ -193,6 +195,7 @@ export type {
|
|
|
193
195
|
AccountRole,
|
|
194
196
|
AvailableCoValueCore,
|
|
195
197
|
PeerState,
|
|
198
|
+
SyncWhen,
|
|
196
199
|
CoValueHeader,
|
|
197
200
|
};
|
|
198
201
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -31,6 +31,15 @@ export const migrations: Record<number, string[]> = {
|
|
|
31
31
|
) WITHOUT ROWID;`,
|
|
32
32
|
"ALTER TABLE sessions ADD COLUMN bytesSinceLastSignature INTEGER;",
|
|
33
33
|
],
|
|
34
|
+
4: [
|
|
35
|
+
`CREATE TABLE IF NOT EXISTS unsynced_covalues (
|
|
36
|
+
rowID INTEGER PRIMARY KEY,
|
|
37
|
+
co_value_id TEXT NOT NULL,
|
|
38
|
+
peer_id TEXT NOT NULL,
|
|
39
|
+
UNIQUE (co_value_id, peer_id)
|
|
40
|
+
);`,
|
|
41
|
+
"CREATE INDEX IF NOT EXISTS idx_unsynced_covalues_co_value_id ON unsynced_covalues(co_value_id);",
|
|
42
|
+
],
|
|
34
43
|
};
|
|
35
44
|
|
|
36
45
|
type Migration = {
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
TransactionRow,
|
|
16
16
|
} from "../types.js";
|
|
17
17
|
import type { SQLiteDatabaseDriverAsync } from "./types.js";
|
|
18
|
+
import type { PeerID } from "../../sync.js";
|
|
18
19
|
|
|
19
20
|
export type RawCoValueRow = {
|
|
20
21
|
id: RawCoID;
|
|
@@ -201,4 +202,38 @@ export class SQLiteClientAsync
|
|
|
201
202
|
) {
|
|
202
203
|
return this.db.transaction(() => operationsCallback(this));
|
|
203
204
|
}
|
|
205
|
+
|
|
206
|
+
async getUnsyncedCoValueIDs(): Promise<RawCoID[]> {
|
|
207
|
+
const rows = await this.db.query<{ co_value_id: RawCoID }>(
|
|
208
|
+
"SELECT DISTINCT co_value_id FROM unsynced_covalues",
|
|
209
|
+
[],
|
|
210
|
+
);
|
|
211
|
+
return rows.map((row) => row.co_value_id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async trackCoValuesSyncState(
|
|
215
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
await Promise.all(
|
|
218
|
+
updates.map(async (update) => {
|
|
219
|
+
if (update.synced) {
|
|
220
|
+
await this.db.run(
|
|
221
|
+
"DELETE FROM unsynced_covalues WHERE co_value_id = ? AND peer_id = ?",
|
|
222
|
+
[update.id, update.peerId],
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
await this.db.run(
|
|
226
|
+
"INSERT OR REPLACE INTO unsynced_covalues (co_value_id, peer_id) VALUES (?, ?)",
|
|
227
|
+
[update.id, update.peerId],
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async stopTrackingSyncState(id: RawCoID): Promise<void> {
|
|
235
|
+
await this.db.run("DELETE FROM unsynced_covalues WHERE co_value_id = ?", [
|
|
236
|
+
id,
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
204
239
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
logger,
|
|
11
11
|
} from "../exports.js";
|
|
12
12
|
import { StoreQueue } from "../queue/StoreQueue.js";
|
|
13
|
-
import { NewContentMessage } from "../sync.js";
|
|
13
|
+
import { NewContentMessage, type PeerID } from "../sync.js";
|
|
14
14
|
import {
|
|
15
15
|
CoValueKnownState,
|
|
16
16
|
emptyKnownState,
|
|
@@ -392,6 +392,23 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
392
392
|
return this.knownStates.waitForSync(id, coValue);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
trackCoValuesSyncState(
|
|
396
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
397
|
+
done?: () => void,
|
|
398
|
+
): void {
|
|
399
|
+
this.dbClient.trackCoValuesSyncState(updates).then(() => done?.());
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getUnsyncedCoValueIDs(
|
|
403
|
+
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
404
|
+
): void {
|
|
405
|
+
this.dbClient.getUnsyncedCoValueIDs().then(callback);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
stopTrackingSyncState(id: RawCoID): void {
|
|
409
|
+
this.dbClient.stopTrackingSyncState(id);
|
|
410
|
+
}
|
|
411
|
+
|
|
395
412
|
close() {
|
|
396
413
|
return this.storeQueue.close();
|
|
397
414
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type StorageAPI,
|
|
11
11
|
logger,
|
|
12
12
|
} from "../exports.js";
|
|
13
|
-
import { NewContentMessage } from "../sync.js";
|
|
13
|
+
import { NewContentMessage, type PeerID } from "../sync.js";
|
|
14
14
|
import { StorageKnownState } from "./knownState.js";
|
|
15
15
|
import {
|
|
16
16
|
CoValueKnownState,
|
|
@@ -365,6 +365,25 @@ export class StorageApiSync implements StorageAPI {
|
|
|
365
365
|
return this.knownStates.waitForSync(id, coValue);
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
trackCoValuesSyncState(
|
|
369
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
370
|
+
done?: () => void,
|
|
371
|
+
): void {
|
|
372
|
+
this.dbClient.trackCoValuesSyncState(updates);
|
|
373
|
+
done?.();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
getUnsyncedCoValueIDs(
|
|
377
|
+
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
378
|
+
): void {
|
|
379
|
+
const ids = this.dbClient.getUnsyncedCoValueIDs();
|
|
380
|
+
callback(ids);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
stopTrackingSyncState(id: RawCoID): void {
|
|
384
|
+
this.dbClient.stopTrackingSyncState(id);
|
|
385
|
+
}
|
|
386
|
+
|
|
368
387
|
close() {
|
|
369
388
|
return undefined;
|
|
370
389
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import { Signature } from "../crypto/crypto.js";
|
|
6
6
|
import type { CoValueCore, RawCoID, SessionID } from "../exports.js";
|
|
7
7
|
import { NewContentMessage } from "../sync.js";
|
|
8
|
+
import type { PeerID } from "../sync.js";
|
|
8
9
|
import { CoValueKnownState } from "../knownState.js";
|
|
9
10
|
|
|
10
11
|
export type CorrectionCallback = (
|
|
@@ -29,6 +30,28 @@ export interface StorageAPI {
|
|
|
29
30
|
|
|
30
31
|
waitForSync(id: string, coValue: CoValueCore): Promise<void>;
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Track multiple sync status updates.
|
|
35
|
+
* Does not guarantee the updates will be applied in order, so only one
|
|
36
|
+
* update per CoValue ID + Peer ID combination should be tracked at a time.
|
|
37
|
+
*/
|
|
38
|
+
trackCoValuesSyncState(
|
|
39
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
40
|
+
done?: () => void,
|
|
41
|
+
): void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get all CoValue IDs that have at least one unsynced peer.
|
|
45
|
+
*/
|
|
46
|
+
getUnsyncedCoValueIDs(
|
|
47
|
+
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
48
|
+
): void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Stop tracking sync status for a CoValue (remove all peer entries).
|
|
52
|
+
*/
|
|
53
|
+
stopTrackingSyncState(id: RawCoID): void;
|
|
54
|
+
|
|
32
55
|
close(): Promise<unknown> | undefined;
|
|
33
56
|
}
|
|
34
57
|
|
|
@@ -118,6 +141,14 @@ export interface DBClientInterfaceAsync {
|
|
|
118
141
|
transaction(
|
|
119
142
|
callback: (tx: DBTransactionInterfaceAsync) => Promise<unknown>,
|
|
120
143
|
): Promise<unknown>;
|
|
144
|
+
|
|
145
|
+
trackCoValuesSyncState(
|
|
146
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
147
|
+
): Promise<void>;
|
|
148
|
+
|
|
149
|
+
getUnsyncedCoValueIDs(): Promise<RawCoID[]>;
|
|
150
|
+
|
|
151
|
+
stopTrackingSyncState(id: RawCoID): Promise<void>;
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
export interface DBTransactionInterfaceSync {
|
|
@@ -170,4 +201,12 @@ export interface DBClientInterfaceSync {
|
|
|
170
201
|
): Pick<SignatureAfterRow, "idx" | "signature">[];
|
|
171
202
|
|
|
172
203
|
transaction(callback: (tx: DBTransactionInterfaceSync) => unknown): unknown;
|
|
204
|
+
|
|
205
|
+
trackCoValuesSyncState(
|
|
206
|
+
updates: { id: RawCoID; peerId: PeerID; synced: boolean }[],
|
|
207
|
+
): void;
|
|
208
|
+
|
|
209
|
+
getUnsyncedCoValueIDs(): RawCoID[];
|
|
210
|
+
|
|
211
|
+
stopTrackingSyncState(id: RawCoID): void;
|
|
173
212
|
}
|