cojson 0.19.20 → 0.19.22
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 +13 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
- package/dist/CojsonMessageChannel/index.d.ts +3 -0
- package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/index.js +2 -0
- package/dist/CojsonMessageChannel/index.js.map +1 -0
- package/dist/CojsonMessageChannel/types.d.ts +149 -0
- package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/types.js +36 -0
- package/dist/CojsonMessageChannel/types.js.map +1 -0
- package/dist/GarbageCollector.d.ts +4 -2
- package/dist/GarbageCollector.d.ts.map +1 -1
- package/dist/GarbageCollector.js +5 -3
- package/dist/GarbageCollector.js.map +1 -1
- package/dist/SyncStateManager.d.ts +3 -3
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +4 -4
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +28 -1
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +50 -5
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/account.js +10 -10
- package/dist/coValues/account.js.map +1 -1
- package/dist/exports.d.ts +1 -0
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +1 -0
- package/dist/exports.js.map +1 -1
- package/dist/ids.d.ts +1 -1
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js.map +1 -1
- package/dist/knownState.d.ts +5 -0
- package/dist/knownState.d.ts.map +1 -1
- package/dist/knownState.js +15 -0
- package/dist/knownState.js.map +1 -1
- package/dist/localNode.d.ts +1 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +11 -4
- package/dist/localNode.js.map +1 -1
- package/dist/storage/knownState.d.ts +5 -0
- package/dist/storage/knownState.d.ts.map +1 -1
- package/dist/storage/knownState.js +11 -0
- package/dist/storage/knownState.js.map +1 -1
- package/dist/storage/sqlite/client.d.ts +2 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +18 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +2 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +20 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +10 -3
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +52 -3
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +9 -3
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +27 -3
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +23 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +23 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +136 -45
- package/dist/sync.js.map +1 -1
- package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
- package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
- package/dist/tests/CojsonMessageChannel.test.js +236 -0
- package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
- package/dist/tests/GarbageCollector.test.js +87 -13
- package/dist/tests/GarbageCollector.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +124 -1
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +123 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/SyncManager.processQueues.test.js +1 -1
- package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
- package/dist/tests/SyncStateManager.test.js +1 -1
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coPlainText.test.js +1 -1
- package/dist/tests/coPlainText.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +2 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
- package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
- package/dist/tests/knownState.lazyLoading.test.js +167 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
- package/dist/tests/messagesTestUtils.d.ts +5 -2
- package/dist/tests/messagesTestUtils.d.ts.map +1 -1
- package/dist/tests/messagesTestUtils.js +4 -0
- package/dist/tests/messagesTestUtils.js.map +1 -1
- package/dist/tests/sync.garbageCollection.test.js +56 -32
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +387 -1
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +5 -5
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +3 -3
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +9 -9
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +7 -7
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.tracking.test.js +35 -4
- package/dist/tests/sync.tracking.test.js.map +1 -1
- package/dist/tests/testStorage.js +38 -2
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +38 -4
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +68 -7
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
- package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
- package/src/CojsonMessageChannel/index.ts +9 -0
- package/src/CojsonMessageChannel/types.ts +200 -0
- package/src/GarbageCollector.ts +5 -5
- package/src/SyncStateManager.ts +6 -6
- package/src/coValueCore/coValueCore.ts +56 -7
- package/src/coValues/account.ts +12 -14
- package/src/exports.ts +1 -0
- package/src/ids.ts +1 -1
- package/src/knownState.ts +24 -0
- package/src/localNode.ts +12 -7
- package/src/storage/knownState.ts +12 -0
- package/src/storage/sqlite/client.ts +31 -0
- package/src/storage/sqliteAsync/client.ts +35 -0
- package/src/storage/storageAsync.ts +66 -4
- package/src/storage/storageSync.ts +37 -4
- package/src/storage/types.ts +32 -0
- package/src/sync.ts +159 -46
- package/src/tests/CojsonMessageChannel.test.ts +306 -0
- package/src/tests/GarbageCollector.test.ts +114 -13
- package/src/tests/StorageApiAsync.test.ts +186 -1
- package/src/tests/StorageApiSync.test.ts +181 -0
- package/src/tests/SyncManager.processQueues.test.ts +1 -1
- package/src/tests/SyncStateManager.test.ts +1 -1
- package/src/tests/coPlainText.test.ts +1 -1
- package/src/tests/coValueCore.loadFromStorage.test.ts +5 -0
- package/src/tests/knownState.lazyLoading.test.ts +219 -0
- package/src/tests/messagesTestUtils.ts +10 -3
- package/src/tests/sync.garbageCollection.test.ts +69 -36
- package/src/tests/sync.load.test.ts +482 -2
- package/src/tests/sync.mesh.test.ts +5 -5
- package/src/tests/sync.peerReconciliation.test.ts +3 -3
- package/src/tests/sync.storage.test.ts +9 -9
- package/src/tests/sync.storageAsync.test.ts +7 -7
- package/src/tests/sync.tracking.test.ts +54 -4
- package/src/tests/testStorage.ts +40 -2
- package/src/tests/testUtils.ts +99 -8
|
@@ -373,16 +373,26 @@ export class CoValueCore {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
376
|
+
/**
|
|
377
|
+
* Removes the CoValue from memory.
|
|
378
|
+
*
|
|
379
|
+
* @returns true if the coValue was successfully unmounted, false otherwise
|
|
380
|
+
*/
|
|
381
|
+
unmount(): boolean {
|
|
382
|
+
if (this.listeners.size > 0) {
|
|
383
|
+
// The coValue is still in use
|
|
381
384
|
return false;
|
|
382
385
|
}
|
|
383
386
|
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
for (const dependant of this.dependant) {
|
|
388
|
+
if (this.node.hasCoValue(dependant)) {
|
|
389
|
+
// Another in-memory coValue depends on this coValue
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!this.node.syncManager.isSyncedToServerPeers(this.id)) {
|
|
395
|
+
return false;
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
this.counter.add(-1, { state: this.loadingState });
|
|
@@ -1193,6 +1203,15 @@ export class CoValueCore {
|
|
|
1193
1203
|
return matchingTransactions;
|
|
1194
1204
|
}
|
|
1195
1205
|
|
|
1206
|
+
/**
|
|
1207
|
+
* The CoValues that this CoValue depends on.
|
|
1208
|
+
* We currently track dependencies for:
|
|
1209
|
+
* - Ownership (a CoValue depends on its account/group owner)
|
|
1210
|
+
* - Group membership (a group depends on its direct account/group members)
|
|
1211
|
+
* - Sessions (a CoValue depends on Accounts that made changes to it)
|
|
1212
|
+
* - Branches (a branched CoValue depends on its branch source)
|
|
1213
|
+
* See {@link dependant} for the CoValues that depend on this CoValue.
|
|
1214
|
+
*/
|
|
1196
1215
|
dependencies: Set<RawCoID> = new Set();
|
|
1197
1216
|
incompleteDependencies: Set<RawCoID> = new Set();
|
|
1198
1217
|
private addDependency(dependency: RawCoID) {
|
|
@@ -1238,6 +1257,10 @@ export class CoValueCore {
|
|
|
1238
1257
|
}
|
|
1239
1258
|
}
|
|
1240
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* The CoValues that depend on this CoValue.
|
|
1262
|
+
* This is the inverse relationship of {@link dependencies}.
|
|
1263
|
+
*/
|
|
1241
1264
|
dependant: Set<RawCoID> = new Set();
|
|
1242
1265
|
private addDependant(dependant: RawCoID) {
|
|
1243
1266
|
this.dependant.add(dependant);
|
|
@@ -1506,6 +1529,32 @@ export class CoValueCore {
|
|
|
1506
1529
|
);
|
|
1507
1530
|
}
|
|
1508
1531
|
|
|
1532
|
+
/**
|
|
1533
|
+
* Lazily load only the knownState from storage without loading full transaction data.
|
|
1534
|
+
* This is useful for checking if a peer needs new content before committing to a full load.
|
|
1535
|
+
*
|
|
1536
|
+
* Caching and deduplication are handled at the storage layer.
|
|
1537
|
+
*
|
|
1538
|
+
* @param done - Callback with the storage knownState, or undefined if not found in storage
|
|
1539
|
+
*/
|
|
1540
|
+
getKnownStateFromStorage(
|
|
1541
|
+
done: (knownState: CoValueKnownState | undefined) => void,
|
|
1542
|
+
) {
|
|
1543
|
+
if (!this.node.storage) {
|
|
1544
|
+
done(undefined);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// If already available in memory, return the current knownState
|
|
1549
|
+
if (this.isAvailable()) {
|
|
1550
|
+
done(this.knownState());
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Delegate to storage - caching is handled at storage level
|
|
1555
|
+
this.node.storage.loadKnownState(this.id, done);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1509
1558
|
loadFromPeers(peers: PeerState[]) {
|
|
1510
1559
|
if (peers.length === 0) {
|
|
1511
1560
|
return;
|
package/src/coValues/account.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
SignerID,
|
|
9
9
|
SignerSecret,
|
|
10
10
|
} from "../crypto/crypto.js";
|
|
11
|
-
import { AgentID } from "../ids.js";
|
|
11
|
+
import { AgentID, isAgentID } from "../ids.js";
|
|
12
12
|
import { JsonObject } from "../jsonValue.js";
|
|
13
13
|
import { LocalNode } from "../localNode.js";
|
|
14
14
|
import { logger } from "../logger.js";
|
|
@@ -43,25 +43,23 @@ export class RawAccount<
|
|
|
43
43
|
_cachedCurrentAgentID: AgentID | undefined;
|
|
44
44
|
|
|
45
45
|
currentAgentID(): AgentID {
|
|
46
|
-
if (this._cachedCurrentAgentID)
|
|
47
|
-
|
|
46
|
+
if (this._cachedCurrentAgentID) return this._cachedCurrentAgentID;
|
|
47
|
+
|
|
48
|
+
const header = this.core.verified.header;
|
|
49
|
+
|
|
50
|
+
if (header.ruleset.type !== "group") {
|
|
51
|
+
throw new Error("You can't get an agent id from a non-group value");
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
const
|
|
51
|
-
.filter((k): k is AgentID => k.startsWith("sealer_"))
|
|
52
|
-
.sort(
|
|
53
|
-
(a, b) =>
|
|
54
|
-
(this.lastEditAt(a)?.at.getTime() || 0) -
|
|
55
|
-
(this.lastEditAt(b)?.at.getTime() || 0),
|
|
56
|
-
);
|
|
54
|
+
const initialAdmin = header.ruleset.initialAdmin;
|
|
57
55
|
|
|
58
|
-
if (
|
|
59
|
-
|
|
56
|
+
if (!isAgentID(initialAdmin)) {
|
|
57
|
+
throw new Error("You can read agent ids only from account values");
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
this._cachedCurrentAgentID =
|
|
60
|
+
this._cachedCurrentAgentID = initialAdmin;
|
|
63
61
|
|
|
64
|
-
return
|
|
62
|
+
return initialAdmin;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
override createInvite(_: AccountRole): InviteSecret {
|
package/src/exports.ts
CHANGED
package/src/ids.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type TransactionID = {
|
|
|
28
28
|
|
|
29
29
|
export type AgentID = `sealer_z${string}/signer_z${string}`;
|
|
30
30
|
|
|
31
|
-
export function isAgentID(id:
|
|
31
|
+
export function isAgentID(id: unknown): id is AgentID {
|
|
32
32
|
return (
|
|
33
33
|
typeof id === "string" &&
|
|
34
34
|
id.startsWith("sealer_") &&
|
package/src/knownState.ts
CHANGED
|
@@ -140,6 +140,30 @@ export function isKnownStateSubsetOf(
|
|
|
140
140
|
return true;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Check if the peer already has all the content from storage.
|
|
145
|
+
* Returns true if the peer has at least as many transactions as storage for all sessions.
|
|
146
|
+
*/
|
|
147
|
+
export function peerHasAllContent(
|
|
148
|
+
storageKnownState: CoValueKnownState,
|
|
149
|
+
peerKnownState: CoValueKnownState | undefined,
|
|
150
|
+
): boolean {
|
|
151
|
+
if (!peerKnownState) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if peer has the header
|
|
156
|
+
if (!peerKnownState.header && storageKnownState.header) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check all sessions - peer must have at least as many transactions as storage
|
|
161
|
+
return isKnownStateSubsetOf(
|
|
162
|
+
storageKnownState.sessions,
|
|
163
|
+
peerKnownState.sessions,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
/**
|
|
144
168
|
* Returns the record with the sessions that need to be sent to the target
|
|
145
169
|
*/
|
package/src/localNode.ts
CHANGED
|
@@ -82,15 +82,12 @@ export class LocalNode {
|
|
|
82
82
|
this.crypto = crypto;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
enableGarbageCollector(
|
|
85
|
+
enableGarbageCollector() {
|
|
86
86
|
if (this.garbageCollector) {
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
this.garbageCollector = new GarbageCollector(
|
|
91
|
-
this.coValues,
|
|
92
|
-
opts?.garbageCollectGroups ?? false,
|
|
93
|
-
);
|
|
90
|
+
this.garbageCollector = new GarbageCollector(this.coValues);
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
setStorage(storage: StorageAPI) {
|
|
@@ -133,6 +130,7 @@ export class LocalNode {
|
|
|
133
130
|
|
|
134
131
|
internalDeleteCoValue(id: RawCoID) {
|
|
135
132
|
this.coValues.delete(id);
|
|
133
|
+
this.storage?.onCoValueUnmounted(id);
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
getCurrentAccountOrAgentID(): RawAccountID | AgentID {
|
|
@@ -736,9 +734,16 @@ export class LocalNode {
|
|
|
736
734
|
};
|
|
737
735
|
}
|
|
738
736
|
|
|
739
|
-
const
|
|
737
|
+
const agentId = coValue.verified.header.ruleset.initialAdmin;
|
|
738
|
+
|
|
739
|
+
if (!isAgentID(agentId)) {
|
|
740
|
+
return {
|
|
741
|
+
value: undefined,
|
|
742
|
+
error: new Error(`Unexpectedly not account: ${expectation}`),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
740
745
|
|
|
741
|
-
return { value:
|
|
746
|
+
return { value: agentId, error: undefined };
|
|
742
747
|
}
|
|
743
748
|
|
|
744
749
|
createGroup(
|
|
@@ -25,6 +25,18 @@ export class StorageKnownState {
|
|
|
25
25
|
return knownState;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Get a cached knownState if it exists and has a header.
|
|
30
|
+
* Unlike getKnownState, this doesn't create an empty state if one doesn't exist.
|
|
31
|
+
*/
|
|
32
|
+
getCachedKnownState(id: string): CoValueKnownState | undefined {
|
|
33
|
+
const knownState = this.knownStates.get(id);
|
|
34
|
+
if (knownState?.header) {
|
|
35
|
+
return knownState;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
setKnownState(id: string, knownState: CoValueKnownState) {
|
|
29
41
|
this.knownStates.set(id, knownState);
|
|
30
42
|
}
|
|
@@ -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 { CoValueKnownState } from "../../knownState.js";
|
|
7
8
|
import type { PeerID } from "../../sync.js";
|
|
8
9
|
import { logger } from "../../logger.js";
|
|
9
10
|
import type {
|
|
@@ -224,4 +225,34 @@ export class SQLiteClient
|
|
|
224
225
|
stopTrackingSyncState(id: RawCoID): void {
|
|
225
226
|
this.db.run("DELETE FROM unsynced_covalues WHERE co_value_id = ?", [id]);
|
|
226
227
|
}
|
|
228
|
+
|
|
229
|
+
getCoValueKnownState(coValueId: string): CoValueKnownState | undefined {
|
|
230
|
+
// First check if the CoValue exists
|
|
231
|
+
const coValueRow = this.db.get<{ rowID: number }>(
|
|
232
|
+
"SELECT rowID FROM coValues WHERE id = ?",
|
|
233
|
+
[coValueId],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (!coValueRow) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Get all session counters without loading transactions
|
|
241
|
+
const sessions = this.db.query<{ sessionID: SessionID; lastIdx: number }>(
|
|
242
|
+
"SELECT sessionID, lastIdx FROM sessions WHERE coValue = ?",
|
|
243
|
+
[coValueRow.rowID],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const knownState: CoValueKnownState = {
|
|
247
|
+
id: coValueId as RawCoID,
|
|
248
|
+
header: true,
|
|
249
|
+
sessions: {},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
for (const session of sessions) {
|
|
253
|
+
knownState.sessions[session.sessionID] = session.lastIdx;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return knownState;
|
|
257
|
+
}
|
|
227
258
|
}
|
|
@@ -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 { CoValueKnownState } from "../../knownState.js";
|
|
7
8
|
import { logger } from "../../logger.js";
|
|
8
9
|
import type {
|
|
9
10
|
DBClientInterfaceAsync,
|
|
@@ -236,4 +237,38 @@ export class SQLiteClientAsync
|
|
|
236
237
|
id,
|
|
237
238
|
]);
|
|
238
239
|
}
|
|
240
|
+
|
|
241
|
+
async getCoValueKnownState(
|
|
242
|
+
coValueId: string,
|
|
243
|
+
): Promise<CoValueKnownState | undefined> {
|
|
244
|
+
// First check if the CoValue exists
|
|
245
|
+
const coValueRow = await this.db.get<{ rowID: number }>(
|
|
246
|
+
"SELECT rowID FROM coValues WHERE id = ?",
|
|
247
|
+
[coValueId],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!coValueRow) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Get all session counters without loading transactions
|
|
255
|
+
const sessions = await this.db.query<{
|
|
256
|
+
sessionID: SessionID;
|
|
257
|
+
lastIdx: number;
|
|
258
|
+
}>("SELECT sessionID, lastIdx FROM sessions WHERE coValue = ?", [
|
|
259
|
+
coValueRow.rowID,
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const knownState: CoValueKnownState = {
|
|
263
|
+
id: coValueId as RawCoID,
|
|
264
|
+
header: true,
|
|
265
|
+
sessions: {},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
for (const session of sessions) {
|
|
269
|
+
knownState.sessions[session.sessionID] = session.lastIdx;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return knownState;
|
|
273
|
+
}
|
|
239
274
|
}
|
|
@@ -34,7 +34,17 @@ import type {
|
|
|
34
34
|
export class StorageApiAsync implements StorageAPI {
|
|
35
35
|
private readonly dbClient: DBClientInterfaceAsync;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Keeps track of CoValues that are in memory, to avoid reloading them from storage
|
|
39
|
+
* when it isn't necessary
|
|
40
|
+
*/
|
|
41
|
+
private inMemoryCoValues = new Set<RawCoID>();
|
|
42
|
+
|
|
43
|
+
// Track pending loads to deduplicate concurrent requests
|
|
44
|
+
private pendingKnownStateLoads = new Map<
|
|
45
|
+
string,
|
|
46
|
+
Promise<CoValueKnownState | undefined>
|
|
47
|
+
>();
|
|
38
48
|
|
|
39
49
|
constructor(dbClient: DBClientInterfaceAsync) {
|
|
40
50
|
this.dbClient = dbClient;
|
|
@@ -46,6 +56,51 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
46
56
|
return this.knownStates.getKnownState(id);
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
loadKnownState(
|
|
60
|
+
id: string,
|
|
61
|
+
callback: (knownState: CoValueKnownState | undefined) => void,
|
|
62
|
+
): void {
|
|
63
|
+
// Check in-memory cache first
|
|
64
|
+
const cached = this.knownStates.getCachedKnownState(id);
|
|
65
|
+
if (cached) {
|
|
66
|
+
callback(cached);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if there's already a pending load for this ID (deduplication)
|
|
71
|
+
const pending = this.pendingKnownStateLoads.get(id);
|
|
72
|
+
if (pending) {
|
|
73
|
+
// Ensure callback is always called, even if pending fails unexpectedly
|
|
74
|
+
pending.then(callback, () => callback(undefined));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Start new load and track it for deduplication
|
|
79
|
+
const loadPromise = this.dbClient
|
|
80
|
+
.getCoValueKnownState(id)
|
|
81
|
+
.then((knownState) => {
|
|
82
|
+
if (knownState) {
|
|
83
|
+
// Cache for future use
|
|
84
|
+
this.knownStates.setKnownState(id, knownState);
|
|
85
|
+
}
|
|
86
|
+
return knownState;
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
// Error handling contract:
|
|
90
|
+
// - Log warning
|
|
91
|
+
// - Behave like "not found" so callers can fall back (full load / load from peers)
|
|
92
|
+
logger.warn("Failed to load knownState from storage", { id, err });
|
|
93
|
+
return undefined;
|
|
94
|
+
})
|
|
95
|
+
.finally(() => {
|
|
96
|
+
// Remove from pending map after completion (success or failure)
|
|
97
|
+
this.pendingKnownStateLoads.delete(id);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.pendingKnownStateLoads.set(id, loadPromise);
|
|
101
|
+
loadPromise.then(callback);
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
async load(
|
|
50
105
|
id: string,
|
|
51
106
|
callback: (data: NewContentMessage) => void,
|
|
@@ -102,7 +157,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
102
157
|
);
|
|
103
158
|
}
|
|
104
159
|
|
|
105
|
-
this.
|
|
160
|
+
this.inMemoryCoValues.add(coValueRow.id);
|
|
106
161
|
|
|
107
162
|
let contentMessage = createContentMessage(coValueRow.id, coValueRow.header);
|
|
108
163
|
|
|
@@ -173,7 +228,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
173
228
|
done?.(true);
|
|
174
229
|
}
|
|
175
230
|
|
|
176
|
-
async pushContentWithDependencies(
|
|
231
|
+
private async pushContentWithDependencies(
|
|
177
232
|
coValueRow: StoredCoValueRow,
|
|
178
233
|
contentMessage: NewContentMessage,
|
|
179
234
|
pushCallback: (data: NewContentMessage) => void,
|
|
@@ -186,7 +241,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
186
241
|
const promises = [];
|
|
187
242
|
|
|
188
243
|
for (const dependedOnCoValue of dependedOnCoValuesList) {
|
|
189
|
-
if (this.
|
|
244
|
+
if (this.inMemoryCoValues.has(dependedOnCoValue)) {
|
|
190
245
|
continue;
|
|
191
246
|
}
|
|
192
247
|
|
|
@@ -313,6 +368,8 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
313
368
|
});
|
|
314
369
|
}
|
|
315
370
|
|
|
371
|
+
this.inMemoryCoValues.add(id);
|
|
372
|
+
|
|
316
373
|
this.knownStates.handleUpdate(id, knownState);
|
|
317
374
|
|
|
318
375
|
if (invalidAssumptions) {
|
|
@@ -409,7 +466,12 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
409
466
|
this.dbClient.stopTrackingSyncState(id);
|
|
410
467
|
}
|
|
411
468
|
|
|
469
|
+
onCoValueUnmounted(id: RawCoID): void {
|
|
470
|
+
this.inMemoryCoValues.delete(id);
|
|
471
|
+
}
|
|
472
|
+
|
|
412
473
|
close() {
|
|
474
|
+
this.inMemoryCoValues.clear();
|
|
413
475
|
return this.storeQueue.close();
|
|
414
476
|
}
|
|
415
477
|
}
|
|
@@ -37,7 +37,11 @@ import { getPriorityFromHeader } from "../priority.js";
|
|
|
37
37
|
|
|
38
38
|
export class StorageApiSync implements StorageAPI {
|
|
39
39
|
private readonly dbClient: DBClientInterfaceSync;
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Keeps track of CoValues that are in memory, to avoid reloading them from storage
|
|
42
|
+
* when it isn't necessary
|
|
43
|
+
*/
|
|
44
|
+
private inMemoryCoValues = new Set<RawCoID>();
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Queue for streaming content that will be pulled by SyncManager.
|
|
@@ -56,6 +60,28 @@ export class StorageApiSync implements StorageAPI {
|
|
|
56
60
|
return this.knownStates.getKnownState(id);
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
loadKnownState(
|
|
64
|
+
id: string,
|
|
65
|
+
callback: (knownState: CoValueKnownState | undefined) => void,
|
|
66
|
+
): void {
|
|
67
|
+
// Check in-memory cache first
|
|
68
|
+
const cached = this.knownStates.getCachedKnownState(id);
|
|
69
|
+
if (cached) {
|
|
70
|
+
callback(cached);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load from database
|
|
75
|
+
const knownState = this.dbClient.getCoValueKnownState(id);
|
|
76
|
+
|
|
77
|
+
if (knownState) {
|
|
78
|
+
// Cache for future use
|
|
79
|
+
this.knownStates.setKnownState(id, knownState);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
callback(knownState);
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
async load(
|
|
60
86
|
id: string,
|
|
61
87
|
callback: (data: NewContentMessage) => void,
|
|
@@ -116,7 +142,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
116
142
|
);
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
this.
|
|
145
|
+
this.inMemoryCoValues.add(coValueRow.id);
|
|
120
146
|
|
|
121
147
|
const priority = getPriorityFromHeader(coValueRow.header);
|
|
122
148
|
const contentMessage = createContentMessage(
|
|
@@ -222,7 +248,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
222
248
|
});
|
|
223
249
|
}
|
|
224
250
|
|
|
225
|
-
async pushContentWithDependencies(
|
|
251
|
+
private async pushContentWithDependencies(
|
|
226
252
|
coValueRow: StoredCoValueRow,
|
|
227
253
|
contentMessage: NewContentMessage,
|
|
228
254
|
pushCallback: (data: NewContentMessage) => void,
|
|
@@ -233,7 +259,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
233
259
|
);
|
|
234
260
|
|
|
235
261
|
for (const dependedOnCoValue of dependedOnCoValuesList) {
|
|
236
|
-
if (this.
|
|
262
|
+
if (this.inMemoryCoValues.has(dependedOnCoValue)) {
|
|
237
263
|
continue;
|
|
238
264
|
}
|
|
239
265
|
|
|
@@ -331,6 +357,8 @@ export class StorageApiSync implements StorageAPI {
|
|
|
331
357
|
});
|
|
332
358
|
}
|
|
333
359
|
|
|
360
|
+
this.inMemoryCoValues.add(id);
|
|
361
|
+
|
|
334
362
|
this.knownStates.handleUpdate(id, knownState);
|
|
335
363
|
|
|
336
364
|
if (invalidAssumptions) {
|
|
@@ -428,7 +456,12 @@ export class StorageApiSync implements StorageAPI {
|
|
|
428
456
|
this.dbClient.stopTrackingSyncState(id);
|
|
429
457
|
}
|
|
430
458
|
|
|
459
|
+
onCoValueUnmounted(id: RawCoID): void {
|
|
460
|
+
this.inMemoryCoValues.delete(id);
|
|
461
|
+
}
|
|
462
|
+
|
|
431
463
|
close() {
|
|
464
|
+
this.inMemoryCoValues.clear();
|
|
432
465
|
return undefined;
|
|
433
466
|
}
|
|
434
467
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -55,6 +55,24 @@ export interface StorageAPI {
|
|
|
55
55
|
*/
|
|
56
56
|
stopTrackingSyncState(id: RawCoID): void;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Load only the knownState (header presence + session counters) for a CoValue.
|
|
60
|
+
* This is more efficient than load() when we only need to check if a peer needs new content.
|
|
61
|
+
*
|
|
62
|
+
* @param id - The CoValue ID
|
|
63
|
+
* @param callback - Called with the knownState, or undefined if CoValue not found
|
|
64
|
+
*/
|
|
65
|
+
loadKnownState(
|
|
66
|
+
id: string,
|
|
67
|
+
callback: (knownState: CoValueKnownState | undefined) => void,
|
|
68
|
+
): void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Called when a CoValue is unmounted from memory.
|
|
72
|
+
* Used to clean up the metadata associated with that CoValue.
|
|
73
|
+
*/
|
|
74
|
+
onCoValueUnmounted(id: RawCoID): void;
|
|
75
|
+
|
|
58
76
|
close(): Promise<unknown> | undefined;
|
|
59
77
|
}
|
|
60
78
|
|
|
@@ -152,6 +170,14 @@ export interface DBClientInterfaceAsync {
|
|
|
152
170
|
getUnsyncedCoValueIDs(): Promise<RawCoID[]>;
|
|
153
171
|
|
|
154
172
|
stopTrackingSyncState(id: RawCoID): Promise<void>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the knownState for a CoValue without loading transactions.
|
|
176
|
+
* Returns undefined if the CoValue doesn't exist.
|
|
177
|
+
*/
|
|
178
|
+
getCoValueKnownState(
|
|
179
|
+
coValueId: string,
|
|
180
|
+
): Promise<CoValueKnownState | undefined>;
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
export interface DBTransactionInterfaceSync {
|
|
@@ -212,4 +238,10 @@ export interface DBClientInterfaceSync {
|
|
|
212
238
|
getUnsyncedCoValueIDs(): RawCoID[];
|
|
213
239
|
|
|
214
240
|
stopTrackingSyncState(id: RawCoID): void;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get the knownState for a CoValue without loading transactions.
|
|
244
|
+
* Returns undefined if the CoValue doesn't exist.
|
|
245
|
+
*/
|
|
246
|
+
getCoValueKnownState(coValueId: string): CoValueKnownState | undefined;
|
|
215
247
|
}
|