cojson 0.19.22 → 0.20.0
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 +54 -0
- package/dist/coValueContentMessage.d.ts +0 -2
- package/dist/coValueContentMessage.d.ts.map +1 -1
- package/dist/coValueContentMessage.js +0 -8
- package/dist/coValueContentMessage.js.map +1 -1
- package/dist/coValueCore/SessionMap.d.ts +4 -2
- package/dist/coValueCore/SessionMap.d.ts.map +1 -1
- package/dist/coValueCore/SessionMap.js +30 -0
- package/dist/coValueCore/SessionMap.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +67 -3
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +289 -12
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +6 -1
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +9 -0
- package/dist/coValueCore/verifiedState.js.map +1 -1
- package/dist/coValues/coList.d.ts +3 -2
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +3 -6
- package/dist/coValues/group.js.map +1 -1
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -8
- package/dist/config.js.map +1 -1
- package/dist/crypto/NapiCrypto.d.ts +1 -2
- package/dist/crypto/NapiCrypto.d.ts.map +1 -1
- package/dist/crypto/NapiCrypto.js +19 -4
- package/dist/crypto/NapiCrypto.js.map +1 -1
- package/dist/crypto/RNCrypto.d.ts.map +1 -1
- package/dist/crypto/RNCrypto.js +19 -4
- package/dist/crypto/RNCrypto.js.map +1 -1
- package/dist/crypto/WasmCrypto.d.ts +11 -4
- package/dist/crypto/WasmCrypto.d.ts.map +1 -1
- package/dist/crypto/WasmCrypto.js +52 -10
- package/dist/crypto/WasmCrypto.js.map +1 -1
- package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
- package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
- package/dist/crypto/WasmCryptoEdge.js +4 -1
- package/dist/crypto/WasmCryptoEdge.js.map +1 -1
- package/dist/crypto/crypto.d.ts +3 -3
- package/dist/crypto/crypto.d.ts.map +1 -1
- package/dist/crypto/crypto.js +6 -1
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/exports.d.ts +2 -2
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +2 -1
- package/dist/exports.js.map +1 -1
- package/dist/ids.d.ts +4 -1
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +4 -0
- package/dist/ids.js.map +1 -1
- package/dist/knownState.d.ts +2 -0
- package/dist/knownState.d.ts.map +1 -1
- package/dist/localNode.d.ts +12 -0
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +14 -0
- package/dist/localNode.js.map +1 -1
- package/dist/platformUtils.d.ts +3 -0
- package/dist/platformUtils.d.ts.map +1 -0
- package/dist/platformUtils.js +24 -0
- package/dist/platformUtils.js.map +1 -0
- package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
- package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
- package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
- package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
- package/dist/storage/sqlite/client.d.ts +3 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +44 -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 +7 -0
- package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +3 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +42 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +7 -0
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +48 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +6 -0
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +42 -0
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +59 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +12 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +44 -11
- package/dist/sync.js.map +1 -1
- package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
- package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
- package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
- package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
- package/dist/tests/GarbageCollector.test.js +5 -6
- package/dist/tests/GarbageCollector.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +484 -152
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +505 -136
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/WasmCrypto.test.js +6 -3
- package/dist/tests/WasmCrypto.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/coValueCore.test.js +34 -13
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/coreWasm.test.js +127 -4
- package/dist/tests/coreWasm.test.js.map +1 -1
- package/dist/tests/crypto.test.js +89 -93
- package/dist/tests/crypto.test.js.map +1 -1
- package/dist/tests/deleteCoValue.test.d.ts +2 -0
- package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
- package/dist/tests/deleteCoValue.test.js +313 -0
- package/dist/tests/deleteCoValue.test.js.map +1 -0
- package/dist/tests/group.removeMember.test.js +18 -30
- package/dist/tests/group.removeMember.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +3 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
- package/dist/tests/sync.deleted.test.d.ts +2 -0
- package/dist/tests/sync.deleted.test.d.ts.map +1 -0
- package/dist/tests/sync.deleted.test.js +214 -0
- package/dist/tests/sync.deleted.test.js.map +1 -0
- package/dist/tests/sync.mesh.test.js +3 -2
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +3 -2
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.test.js +3 -2
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/testStorage.d.ts +3 -0
- package/dist/tests/testStorage.d.ts.map +1 -1
- package/dist/tests/testStorage.js +14 -0
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +6 -3
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +17 -3
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +6 -16
- package/src/coValueContentMessage.ts +0 -14
- package/src/coValueCore/SessionMap.ts +43 -1
- package/src/coValueCore/coValueCore.ts +400 -8
- package/src/coValueCore/verifiedState.ts +26 -3
- package/src/coValues/coList.ts +5 -3
- package/src/coValues/group.ts +5 -6
- package/src/config.ts +0 -9
- package/src/crypto/NapiCrypto.ts +29 -13
- package/src/crypto/RNCrypto.ts +29 -11
- package/src/crypto/WasmCrypto.ts +67 -20
- package/src/crypto/WasmCryptoEdge.ts +5 -1
- package/src/crypto/crypto.ts +16 -4
- package/src/exports.ts +2 -0
- package/src/ids.ts +11 -1
- package/src/localNode.ts +15 -0
- package/src/platformUtils.ts +26 -0
- package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
- package/src/storage/sqlite/client.ts +77 -0
- package/src/storage/sqlite/sqliteMigrations.ts +7 -0
- package/src/storage/sqliteAsync/client.ts +75 -0
- package/src/storage/storageAsync.ts +62 -0
- package/src/storage/storageSync.ts +58 -0
- package/src/storage/types.ts +69 -0
- package/src/sync.ts +51 -11
- package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
- package/src/tests/GarbageCollector.test.ts +6 -10
- package/src/tests/StorageApiAsync.test.ts +572 -162
- package/src/tests/StorageApiSync.test.ts +580 -143
- package/src/tests/WasmCrypto.test.ts +8 -3
- package/src/tests/coValueCore.loadFromStorage.test.ts +6 -0
- package/src/tests/coValueCore.test.ts +49 -14
- package/src/tests/coreWasm.test.ts +319 -10
- package/src/tests/crypto.test.ts +141 -150
- package/src/tests/deleteCoValue.test.ts +528 -0
- package/src/tests/group.removeMember.test.ts +35 -35
- package/src/tests/knownState.lazyLoading.test.ts +6 -0
- package/src/tests/sync.deleted.test.ts +294 -0
- package/src/tests/sync.mesh.test.ts +5 -2
- package/src/tests/sync.storage.test.ts +5 -2
- package/src/tests/sync.test.ts +5 -2
- package/src/tests/testStorage.ts +28 -1
- package/src/tests/testUtils.ts +28 -9
- package/dist/crypto/PureJSCrypto.d.ts +0 -77
- package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
- package/dist/crypto/PureJSCrypto.js +0 -236
- package/dist/crypto/PureJSCrypto.js.map +0 -1
- package/dist/tests/PureJSCrypto.test.d.ts +0 -2
- package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
- package/dist/tests/PureJSCrypto.test.js +0 -145
- package/dist/tests/PureJSCrypto.test.js.map +0 -1
- package/src/crypto/PureJSCrypto.ts +0 -429
- package/src/tests/PureJSCrypto.test.ts +0 -217
|
@@ -40,6 +40,13 @@ export const migrations: Record<number, string[]> = {
|
|
|
40
40
|
);`,
|
|
41
41
|
"CREATE INDEX IF NOT EXISTS idx_unsynced_covalues_co_value_id ON unsynced_covalues(co_value_id);",
|
|
42
42
|
],
|
|
43
|
+
5: [
|
|
44
|
+
`CREATE TABLE IF NOT EXISTS deletedCoValues (
|
|
45
|
+
coValueID TEXT PRIMARY KEY,
|
|
46
|
+
status INTEGER NOT NULL DEFAULT 0
|
|
47
|
+
) WITHOUT ROWID;`,
|
|
48
|
+
"CREATE INDEX IF NOT EXISTS deletedCoValuesByStatus ON deletedCoValues (status);",
|
|
49
|
+
],
|
|
43
50
|
};
|
|
44
51
|
|
|
45
52
|
type Migration = {
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
StoredSessionRow,
|
|
16
16
|
TransactionRow,
|
|
17
17
|
} from "../types.js";
|
|
18
|
+
import { DeletedCoValueDeletionStatus } from "../types.js";
|
|
18
19
|
import type { SQLiteDatabaseDriverAsync } from "./types.js";
|
|
19
20
|
import type { PeerID } from "../../sync.js";
|
|
20
21
|
|
|
@@ -29,6 +30,10 @@ export type RawTransactionRow = {
|
|
|
29
30
|
tx: string;
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
type DeletedCoValueQueueRow = {
|
|
34
|
+
id: RawCoID;
|
|
35
|
+
};
|
|
36
|
+
|
|
32
37
|
export function getErrorMessage(error: unknown) {
|
|
33
38
|
return error instanceof Error ? error.message : "Unknown error";
|
|
34
39
|
}
|
|
@@ -146,6 +151,76 @@ export class SQLiteClientAsync
|
|
|
146
151
|
return result.rowID;
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
async markCoValueAsDeleted(id: RawCoID) {
|
|
155
|
+
// Work queue entry. Table only stores the coValueID.
|
|
156
|
+
// Idempotent by design.
|
|
157
|
+
await this.db.run(
|
|
158
|
+
`INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
|
|
159
|
+
[id],
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async eraseCoValueButKeepTombstone(coValueId: RawCoID) {
|
|
164
|
+
const coValueRow = await this.db.get<RawCoValueRow & { rowID: number }>(
|
|
165
|
+
"SELECT * FROM coValues WHERE id = ?",
|
|
166
|
+
[coValueId],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!coValueRow) {
|
|
170
|
+
logger.warn(`CoValue ${coValueId} not found, skipping deletion`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await this.transaction(async () => {
|
|
175
|
+
await this.db.run(
|
|
176
|
+
`DELETE FROM transactions
|
|
177
|
+
WHERE ses IN (
|
|
178
|
+
SELECT rowID FROM sessions
|
|
179
|
+
WHERE coValue = ?
|
|
180
|
+
AND sessionID NOT LIKE '%$'
|
|
181
|
+
)`,
|
|
182
|
+
[coValueRow.rowID],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await this.db.run(
|
|
186
|
+
`DELETE FROM signatureAfter
|
|
187
|
+
WHERE ses IN (
|
|
188
|
+
SELECT rowID FROM sessions
|
|
189
|
+
WHERE coValue = ?
|
|
190
|
+
AND sessionID NOT LIKE '%$'
|
|
191
|
+
)`,
|
|
192
|
+
[coValueRow.rowID],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await this.db.run(
|
|
196
|
+
`DELETE FROM sessions
|
|
197
|
+
WHERE coValue = ?
|
|
198
|
+
AND sessionID NOT LIKE '%$'`,
|
|
199
|
+
[coValueRow.rowID],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await this.db.run(
|
|
203
|
+
`INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
|
|
204
|
+
ON CONFLICT(coValueID) DO UPDATE SET status=?`,
|
|
205
|
+
[
|
|
206
|
+
coValueId,
|
|
207
|
+
DeletedCoValueDeletionStatus.Done,
|
|
208
|
+
DeletedCoValueDeletionStatus.Done,
|
|
209
|
+
],
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getAllCoValuesWaitingForDelete(): Promise<RawCoID[]> {
|
|
215
|
+
const rows = await this.db.query<DeletedCoValueQueueRow>(
|
|
216
|
+
`SELECT coValueID as id
|
|
217
|
+
FROM deletedCoValues
|
|
218
|
+
WHERE status = ?`,
|
|
219
|
+
[DeletedCoValueDeletionStatus.Pending],
|
|
220
|
+
);
|
|
221
|
+
return rows.map((r) => r.id);
|
|
222
|
+
}
|
|
223
|
+
|
|
149
224
|
async addSessionUpdate({
|
|
150
225
|
sessionUpdate,
|
|
151
226
|
}: {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
setSessionCounter,
|
|
18
18
|
} from "../knownState.js";
|
|
19
19
|
import { StorageKnownState } from "./knownState.js";
|
|
20
|
+
import { DeletedCoValuesEraserScheduler } from "./DeletedCoValuesEraserScheduler.js";
|
|
20
21
|
import {
|
|
21
22
|
collectNewTxs,
|
|
22
23
|
getDependedOnCoValues,
|
|
@@ -30,10 +31,15 @@ import type {
|
|
|
30
31
|
StoredCoValueRow,
|
|
31
32
|
StoredSessionRow,
|
|
32
33
|
} from "./types.js";
|
|
34
|
+
import { isDeleteSessionID } from "../ids.js";
|
|
33
35
|
|
|
34
36
|
export class StorageApiAsync implements StorageAPI {
|
|
35
37
|
private readonly dbClient: DBClientInterfaceAsync;
|
|
36
38
|
|
|
39
|
+
private deletedCoValuesEraserScheduler:
|
|
40
|
+
| DeletedCoValuesEraserScheduler
|
|
41
|
+
| undefined;
|
|
42
|
+
private eraserController: AbortController | undefined;
|
|
37
43
|
/**
|
|
38
44
|
* Keeps track of CoValues that are in memory, to avoid reloading them from storage
|
|
39
45
|
* when it isn't necessary
|
|
@@ -114,6 +120,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
114
120
|
callback: (data: NewContentMessage) => void,
|
|
115
121
|
done: (found: boolean) => void,
|
|
116
122
|
) {
|
|
123
|
+
this.interruptEraser("load");
|
|
117
124
|
const coValueRow = await this.dbClient.getCoValue(id);
|
|
118
125
|
|
|
119
126
|
if (!coValueRow) {
|
|
@@ -267,10 +274,35 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
267
274
|
this.storeQueue.push(msg, correctionCallback);
|
|
268
275
|
|
|
269
276
|
this.storeQueue.processQueue(async (data, correctionCallback) => {
|
|
277
|
+
this.interruptEraser("store");
|
|
270
278
|
return this.storeSingle(data, correctionCallback);
|
|
271
279
|
});
|
|
272
280
|
}
|
|
273
281
|
|
|
282
|
+
private interruptEraser(reason: string) {
|
|
283
|
+
// Cooperative cancellation: a DB transaction already in progress will complete,
|
|
284
|
+
// but the eraser loop will stop starting further work at its next abort check.
|
|
285
|
+
if (this.eraserController) {
|
|
286
|
+
this.eraserController.abort(reason);
|
|
287
|
+
this.eraserController = undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async eraseAllDeletedCoValues() {
|
|
292
|
+
const ids = await this.dbClient.getAllCoValuesWaitingForDelete();
|
|
293
|
+
|
|
294
|
+
this.eraserController = new AbortController();
|
|
295
|
+
const signal = this.eraserController.signal;
|
|
296
|
+
|
|
297
|
+
for (const id of ids) {
|
|
298
|
+
if (signal.aborted) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await this.dbClient.eraseCoValueButKeepTombstone(id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
274
306
|
/**
|
|
275
307
|
* This function is called when the storage lacks the information required to store the incoming content.
|
|
276
308
|
*
|
|
@@ -313,6 +345,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
313
345
|
msg: NewContentMessage,
|
|
314
346
|
correctionCallback: CorrectionCallback,
|
|
315
347
|
): Promise<boolean> {
|
|
348
|
+
this.interruptEraser("store");
|
|
316
349
|
if (this.storeQueue.closed) {
|
|
317
350
|
return false;
|
|
318
351
|
}
|
|
@@ -342,6 +375,10 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
342
375
|
sessionID,
|
|
343
376
|
);
|
|
344
377
|
|
|
378
|
+
if (this.deletedValues.has(id) && isDeleteSessionID(sessionID)) {
|
|
379
|
+
await tx.markCoValueAsDeleted(id);
|
|
380
|
+
}
|
|
381
|
+
|
|
345
382
|
if (sessionRow) {
|
|
346
383
|
setSessionCounter(
|
|
347
384
|
knownState.sessions,
|
|
@@ -445,6 +482,30 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
445
482
|
return newLastIdx;
|
|
446
483
|
}
|
|
447
484
|
|
|
485
|
+
deletedValues = new Set<RawCoID>();
|
|
486
|
+
|
|
487
|
+
markDeleteAsValid(id: RawCoID) {
|
|
488
|
+
this.deletedValues.add(id);
|
|
489
|
+
|
|
490
|
+
if (this.deletedCoValuesEraserScheduler) {
|
|
491
|
+
this.deletedCoValuesEraserScheduler.onEnqueueDeletedCoValue();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
enableDeletedCoValuesErasure() {
|
|
496
|
+
if (this.deletedCoValuesEraserScheduler) return;
|
|
497
|
+
|
|
498
|
+
this.deletedCoValuesEraserScheduler = new DeletedCoValuesEraserScheduler({
|
|
499
|
+
run: async () => {
|
|
500
|
+
// Async storage: no max-time budgeting; drain to completion when scheduled.
|
|
501
|
+
await this.eraseAllDeletedCoValues();
|
|
502
|
+
const remaining = await this.dbClient.getAllCoValuesWaitingForDelete();
|
|
503
|
+
return { hasMore: remaining.length > 0 };
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
this.deletedCoValuesEraserScheduler.scheduleStartupDrain();
|
|
507
|
+
}
|
|
508
|
+
|
|
448
509
|
waitForSync(id: string, coValue: CoValueCore) {
|
|
449
510
|
return this.knownStates.waitForSync(id, coValue);
|
|
450
511
|
}
|
|
@@ -471,6 +532,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
471
532
|
}
|
|
472
533
|
|
|
473
534
|
close() {
|
|
535
|
+
this.deletedCoValuesEraserScheduler?.dispose();
|
|
474
536
|
this.inMemoryCoValues.clear();
|
|
475
537
|
return this.storeQueue.close();
|
|
476
538
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
emptyKnownState,
|
|
17
17
|
setSessionCounter,
|
|
18
18
|
} from "../knownState.js";
|
|
19
|
+
import { isDeleteSessionID } from "../ids.js";
|
|
19
20
|
import {
|
|
20
21
|
collectNewTxs,
|
|
21
22
|
getDependedOnCoValues,
|
|
@@ -29,14 +30,21 @@ import type {
|
|
|
29
30
|
StoredCoValueRow,
|
|
30
31
|
StoredSessionRow,
|
|
31
32
|
} from "./types.js";
|
|
33
|
+
import { DeletedCoValuesEraserScheduler } from "./DeletedCoValuesEraserScheduler.js";
|
|
32
34
|
import {
|
|
33
35
|
ContentCallback,
|
|
34
36
|
StorageStreamingQueue,
|
|
35
37
|
} from "../queue/StorageStreamingQueue.js";
|
|
36
38
|
import { getPriorityFromHeader } from "../priority.js";
|
|
37
39
|
|
|
40
|
+
const MAX_DELETE_SCHEDULE_DURATION_MS = 100;
|
|
41
|
+
|
|
38
42
|
export class StorageApiSync implements StorageAPI {
|
|
39
43
|
private readonly dbClient: DBClientInterfaceSync;
|
|
44
|
+
|
|
45
|
+
private deletedCoValuesEraserScheduler:
|
|
46
|
+
| DeletedCoValuesEraserScheduler
|
|
47
|
+
| undefined;
|
|
40
48
|
/**
|
|
41
49
|
* Keeps track of CoValues that are in memory, to avoid reloading them from storage
|
|
42
50
|
* when it isn't necessary
|
|
@@ -329,6 +337,10 @@ export class StorageApiSync implements StorageAPI {
|
|
|
329
337
|
|
|
330
338
|
for (const sessionID of Object.keys(msg.new) as SessionID[]) {
|
|
331
339
|
this.dbClient.transaction((tx) => {
|
|
340
|
+
if (this.deletedValues.has(id) && isDeleteSessionID(sessionID)) {
|
|
341
|
+
tx.markCoValueAsDeleted(id);
|
|
342
|
+
}
|
|
343
|
+
|
|
332
344
|
const sessionRow = tx.getSingleCoValueSession(
|
|
333
345
|
storedCoValueRowID,
|
|
334
346
|
sessionID,
|
|
@@ -433,6 +445,51 @@ export class StorageApiSync implements StorageAPI {
|
|
|
433
445
|
return newLastIdx;
|
|
434
446
|
}
|
|
435
447
|
|
|
448
|
+
deletedValues = new Set<RawCoID>();
|
|
449
|
+
|
|
450
|
+
markDeleteAsValid(id: RawCoID) {
|
|
451
|
+
this.deletedValues.add(id);
|
|
452
|
+
|
|
453
|
+
if (this.deletedCoValuesEraserScheduler) {
|
|
454
|
+
this.deletedCoValuesEraserScheduler.onEnqueueDeletedCoValue();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async eraseAllDeletedCoValues(): Promise<void> {
|
|
459
|
+
const ids = this.dbClient.getAllCoValuesWaitingForDelete();
|
|
460
|
+
|
|
461
|
+
for (const id of ids) {
|
|
462
|
+
this.dbClient.eraseCoValueButKeepTombstone(id);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
enableDeletedCoValuesErasure() {
|
|
467
|
+
if (this.deletedCoValuesEraserScheduler) return;
|
|
468
|
+
this.deletedCoValuesEraserScheduler = new DeletedCoValuesEraserScheduler({
|
|
469
|
+
run: async () =>
|
|
470
|
+
this.eraseDeletedCoValuesOnceBudgeted(MAX_DELETE_SCHEDULE_DURATION_MS),
|
|
471
|
+
});
|
|
472
|
+
this.deletedCoValuesEraserScheduler.scheduleStartupDrain();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private eraseDeletedCoValuesOnceBudgeted(budgetMs?: number) {
|
|
476
|
+
const startedAt = Date.now();
|
|
477
|
+
const ids = this.dbClient.getAllCoValuesWaitingForDelete();
|
|
478
|
+
|
|
479
|
+
for (const id of ids) {
|
|
480
|
+
// Strict time budget for sync storage to avoid blocking.
|
|
481
|
+
if (budgetMs && Date.now() - startedAt >= budgetMs) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.dbClient.eraseCoValueButKeepTombstone(id);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
hasMore: this.dbClient.getAllCoValuesWaitingForDelete().length > 0,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
436
493
|
waitForSync(id: string, coValue: CoValueCore) {
|
|
437
494
|
return this.knownStates.waitForSync(id, coValue);
|
|
438
495
|
}
|
|
@@ -461,6 +518,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
461
518
|
}
|
|
462
519
|
|
|
463
520
|
close() {
|
|
521
|
+
this.deletedCoValuesEraserScheduler?.dispose();
|
|
464
522
|
this.inMemoryCoValues.clear();
|
|
465
523
|
return undefined;
|
|
466
524
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -13,12 +13,43 @@ export type CorrectionCallback = (
|
|
|
13
13
|
correction: CoValueKnownState,
|
|
14
14
|
) => NewContentMessage[] | undefined;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Deletion work queue status for `deletedCoValues` (SQLite).
|
|
18
|
+
*
|
|
19
|
+
* Stored as an INTEGER in SQLite:
|
|
20
|
+
* - 0 = pending
|
|
21
|
+
* - 1 = done
|
|
22
|
+
*/
|
|
23
|
+
export enum DeletedCoValueDeletionStatus {
|
|
24
|
+
Pending = 0,
|
|
25
|
+
Done = 1,
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
/**
|
|
17
29
|
* The StorageAPI is the interface that the StorageSync and StorageAsync classes implement.
|
|
18
30
|
*
|
|
19
31
|
* It uses callbacks instead of promises to have no overhead when using the StorageSync and less overhead when using the StorageAsync.
|
|
20
32
|
*/
|
|
21
33
|
export interface StorageAPI {
|
|
34
|
+
/**
|
|
35
|
+
* Flags that the coValue delete is valid.
|
|
36
|
+
*
|
|
37
|
+
* When the delete tx is stored, the storage will mark the coValue as deleted.
|
|
38
|
+
*/
|
|
39
|
+
markDeleteAsValid(id: RawCoID): void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enable the background erasure scheduler that drains the `deletedCoValues` work queue.
|
|
43
|
+
* This is intentionally opt-in and should be activated by `LocalNode`.
|
|
44
|
+
*/
|
|
45
|
+
enableDeletedCoValuesErasure(): void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Batch physical deletion for coValues queued in `deletedCoValues` with status `Pending`.
|
|
49
|
+
* Must preserve tombstones (header + delete session(s) + their tx/signatures).
|
|
50
|
+
*/
|
|
51
|
+
eraseAllDeletedCoValues(): Promise<void>;
|
|
52
|
+
|
|
22
53
|
load(
|
|
23
54
|
id: string,
|
|
24
55
|
// This callback is fired when data is found, might be called multiple times if the content requires streaming (e.g when loading files)
|
|
@@ -111,6 +142,13 @@ export interface DBTransactionInterfaceAsync {
|
|
|
111
142
|
sessionID: SessionID,
|
|
112
143
|
): Promise<StoredSessionRow | undefined>;
|
|
113
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Persist a "deleted coValue" marker in storage (work queue entry).
|
|
147
|
+
* This is an enqueue signal: implementations should set status to `Pending`.
|
|
148
|
+
* This is expected to be idempotent (safe to call repeatedly).
|
|
149
|
+
*/
|
|
150
|
+
markCoValueAsDeleted(id: RawCoID): Promise<unknown>;
|
|
151
|
+
|
|
114
152
|
addSessionUpdate({
|
|
115
153
|
sessionUpdate,
|
|
116
154
|
sessionRow,
|
|
@@ -146,6 +184,11 @@ export interface DBClientInterfaceAsync {
|
|
|
146
184
|
header?: CoValueHeader,
|
|
147
185
|
): Promise<number | undefined>;
|
|
148
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Enumerate all coValue IDs currently pending in the "deleted coValues" work queue.
|
|
189
|
+
*/
|
|
190
|
+
getAllCoValuesWaitingForDelete(): Promise<RawCoID[]>;
|
|
191
|
+
|
|
149
192
|
getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]>;
|
|
150
193
|
|
|
151
194
|
getNewTransactionInSession(
|
|
@@ -171,6 +214,13 @@ export interface DBClientInterfaceAsync {
|
|
|
171
214
|
|
|
172
215
|
stopTrackingSyncState(id: RawCoID): Promise<void>;
|
|
173
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Physical deletion primitive: erase all persisted history for a deleted coValue,
|
|
219
|
+
* while preserving the tombstone (header + delete session(s)).
|
|
220
|
+
* Must run inside a single storage transaction.
|
|
221
|
+
*/
|
|
222
|
+
eraseCoValueButKeepTombstone(coValueID: RawCoID): Promise<unknown>;
|
|
223
|
+
|
|
174
224
|
/**
|
|
175
225
|
* Get the knownState for a CoValue without loading transactions.
|
|
176
226
|
* Returns undefined if the CoValue doesn't exist.
|
|
@@ -186,6 +236,13 @@ export interface DBTransactionInterfaceSync {
|
|
|
186
236
|
sessionID: SessionID,
|
|
187
237
|
): StoredSessionRow | undefined;
|
|
188
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Persist a "deleted coValue" marker in storage (work queue entry).
|
|
241
|
+
* This is an enqueue signal: implementations should set status to `"pending"`.
|
|
242
|
+
* This is expected to be idempotent (safe to call repeatedly).
|
|
243
|
+
*/
|
|
244
|
+
markCoValueAsDeleted(id: RawCoID): unknown;
|
|
245
|
+
|
|
189
246
|
addSessionUpdate({
|
|
190
247
|
sessionUpdate,
|
|
191
248
|
sessionRow,
|
|
@@ -216,6 +273,11 @@ export interface DBClientInterfaceSync {
|
|
|
216
273
|
|
|
217
274
|
upsertCoValue(id: string, header?: CoValueHeader): number | undefined;
|
|
218
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Enumerate all coValue IDs currently pending in the "deleted coValues" work queue.
|
|
278
|
+
*/
|
|
279
|
+
getAllCoValuesWaitingForDelete(): RawCoID[];
|
|
280
|
+
|
|
219
281
|
getCoValueSessions(coValueRowId: number): StoredSessionRow[];
|
|
220
282
|
|
|
221
283
|
getNewTransactionInSession(
|
|
@@ -239,6 +301,13 @@ export interface DBClientInterfaceSync {
|
|
|
239
301
|
|
|
240
302
|
stopTrackingSyncState(id: RawCoID): void;
|
|
241
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Physical deletion primitive: erase all persisted history for a deleted coValue,
|
|
306
|
+
* while preserving the tombstone (header + delete session(s)).
|
|
307
|
+
* Must run inside a single storage transaction.
|
|
308
|
+
*/
|
|
309
|
+
eraseCoValueButKeepTombstone(coValueID: RawCoID): unknown;
|
|
310
|
+
|
|
242
311
|
/**
|
|
243
312
|
* Get the knownState for a CoValue without loading transactions.
|
|
244
313
|
* Returns undefined if the CoValue doesn't exist.
|
package/src/sync.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { CoValueCore } from "./coValueCore/coValueCore.js";
|
|
15
15
|
import { CoValueHeader, Transaction } from "./coValueCore/verifiedState.js";
|
|
16
16
|
import { Signature } from "./crypto/crypto.js";
|
|
17
|
-
import { RawCoID, SessionID, isRawCoID } from "./ids.js";
|
|
17
|
+
import { isDeleteSessionID, RawCoID, SessionID, isRawCoID } from "./ids.js";
|
|
18
18
|
import { LocalNode } from "./localNode.js";
|
|
19
19
|
import { logger } from "./logger.js";
|
|
20
20
|
import { CoValuePriority } from "./priority.js";
|
|
@@ -213,7 +213,6 @@ export class SyncManager {
|
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
// TODO: validate
|
|
217
216
|
switch (msg.action) {
|
|
218
217
|
case "load":
|
|
219
218
|
return this.handleLoad(msg, peer);
|
|
@@ -265,10 +264,18 @@ export class SyncManager {
|
|
|
265
264
|
|
|
266
265
|
peer.combineOptimisticWith(id, coValue.knownState());
|
|
267
266
|
} else if (!peer.toldKnownState.has(id)) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
if (coValue.isDeleted) {
|
|
268
|
+
// This way we make the peer believe that we've always ingested all the content they sent, even though we skipped it because the coValue is deleted
|
|
269
|
+
this.trySendToPeer(
|
|
270
|
+
peer,
|
|
271
|
+
coValue.stopSyncingKnownStateMessage(peer.getKnownState(id)),
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
this.trySendToPeer(peer, {
|
|
275
|
+
action: "known",
|
|
276
|
+
...coValue.knownStateWithStreaming(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
peer.trackToldKnownState(id);
|
|
@@ -875,6 +882,8 @@ export class SyncManager {
|
|
|
875
882
|
new: {},
|
|
876
883
|
};
|
|
877
884
|
|
|
885
|
+
let wasAlreadyDeleted = coValue.isDeleted;
|
|
886
|
+
|
|
878
887
|
/**
|
|
879
888
|
* The coValue is in memory, load the transactions from the content message
|
|
880
889
|
*/
|
|
@@ -882,6 +891,10 @@ export class SyncManager {
|
|
|
882
891
|
sessionID,
|
|
883
892
|
newContentForSession,
|
|
884
893
|
] of getSessionEntriesFromContentMessage(msg)) {
|
|
894
|
+
if (wasAlreadyDeleted && !isDeleteSessionID(sessionID)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
|
|
885
898
|
const newTransactions = getNewTransactionsFromContentMessage(
|
|
886
899
|
newContentForSession,
|
|
887
900
|
coValue.knownState(),
|
|
@@ -936,12 +949,25 @@ export class SyncManager {
|
|
|
936
949
|
this.recordTransactionsSize(newTransactions, sourceRole);
|
|
937
950
|
}
|
|
938
951
|
|
|
952
|
+
// We reset the new content for the deleted coValue
|
|
953
|
+
// because we want to store only the delete session/transaction
|
|
954
|
+
if (!wasAlreadyDeleted && coValue.isDeleted) {
|
|
955
|
+
wasAlreadyDeleted = true;
|
|
956
|
+
validNewContent.new = {};
|
|
957
|
+
}
|
|
958
|
+
|
|
939
959
|
// The new content for this session has been verified, so we can store it
|
|
940
960
|
validNewContent.new[sessionID] = newContentForSession;
|
|
941
961
|
}
|
|
942
962
|
|
|
943
963
|
if (peer) {
|
|
944
|
-
|
|
964
|
+
if (coValue.isDeleted) {
|
|
965
|
+
// In case of deleted coValues, we combine the known state with the content message
|
|
966
|
+
// to avoid that clients that don't support deleted coValues try to sync their own content indefinitely
|
|
967
|
+
peer.combineWith(msg.id, knownStateFromContent(msg));
|
|
968
|
+
} else {
|
|
969
|
+
peer.combineWith(msg.id, knownStateFromContent(validNewContent));
|
|
970
|
+
}
|
|
945
971
|
}
|
|
946
972
|
|
|
947
973
|
/**
|
|
@@ -973,10 +999,18 @@ export class SyncManager {
|
|
|
973
999
|
* This way the sender knows that the content has been received and applied
|
|
974
1000
|
* and can update their peer's knownState accordingly.
|
|
975
1001
|
*/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1002
|
+
if (coValue.isDeleted) {
|
|
1003
|
+
// This way we make the peer believe that we've ingested all the content, even though we skipped it because the coValue is deleted
|
|
1004
|
+
this.trySendToPeer(
|
|
1005
|
+
peer,
|
|
1006
|
+
coValue.stopSyncingKnownStateMessage(peer.getKnownState(msg.id)),
|
|
1007
|
+
);
|
|
1008
|
+
} else {
|
|
1009
|
+
this.trySendToPeer(peer, {
|
|
1010
|
+
action: "known",
|
|
1011
|
+
...coValue.knownState(),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
980
1014
|
peer.trackToldKnownState(msg.id);
|
|
981
1015
|
}
|
|
982
1016
|
|
|
@@ -1124,6 +1158,12 @@ export class SyncManager {
|
|
|
1124
1158
|
|
|
1125
1159
|
const value = this.local.getCoValue(content.id);
|
|
1126
1160
|
|
|
1161
|
+
if (value.isDeleted) {
|
|
1162
|
+
// This doesn't persist the delete flag, it only signals the storage
|
|
1163
|
+
// API that the delete transaction is valid
|
|
1164
|
+
storage.markDeleteAsValid(value.id);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1127
1167
|
// Try to store the content as-is for performance
|
|
1128
1168
|
// In case that some transactions are missing, a correction will be requested, but it's an edge case
|
|
1129
1169
|
storage.store(content, (correction) => {
|