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.
Files changed (159) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
  4. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
  5. package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
  6. package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
  7. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
  8. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
  9. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
  10. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
  11. package/dist/CojsonMessageChannel/index.d.ts +3 -0
  12. package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
  13. package/dist/CojsonMessageChannel/index.js +2 -0
  14. package/dist/CojsonMessageChannel/index.js.map +1 -0
  15. package/dist/CojsonMessageChannel/types.d.ts +149 -0
  16. package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
  17. package/dist/CojsonMessageChannel/types.js +36 -0
  18. package/dist/CojsonMessageChannel/types.js.map +1 -0
  19. package/dist/GarbageCollector.d.ts +4 -2
  20. package/dist/GarbageCollector.d.ts.map +1 -1
  21. package/dist/GarbageCollector.js +5 -3
  22. package/dist/GarbageCollector.js.map +1 -1
  23. package/dist/SyncStateManager.d.ts +3 -3
  24. package/dist/SyncStateManager.d.ts.map +1 -1
  25. package/dist/SyncStateManager.js +4 -4
  26. package/dist/SyncStateManager.js.map +1 -1
  27. package/dist/coValueCore/coValueCore.d.ts +28 -1
  28. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  29. package/dist/coValueCore/coValueCore.js +50 -5
  30. package/dist/coValueCore/coValueCore.js.map +1 -1
  31. package/dist/coValues/account.d.ts.map +1 -1
  32. package/dist/coValues/account.js +10 -10
  33. package/dist/coValues/account.js.map +1 -1
  34. package/dist/exports.d.ts +1 -0
  35. package/dist/exports.d.ts.map +1 -1
  36. package/dist/exports.js +1 -0
  37. package/dist/exports.js.map +1 -1
  38. package/dist/ids.d.ts +1 -1
  39. package/dist/ids.d.ts.map +1 -1
  40. package/dist/ids.js.map +1 -1
  41. package/dist/knownState.d.ts +5 -0
  42. package/dist/knownState.d.ts.map +1 -1
  43. package/dist/knownState.js +15 -0
  44. package/dist/knownState.js.map +1 -1
  45. package/dist/localNode.d.ts +1 -3
  46. package/dist/localNode.d.ts.map +1 -1
  47. package/dist/localNode.js +11 -4
  48. package/dist/localNode.js.map +1 -1
  49. package/dist/storage/knownState.d.ts +5 -0
  50. package/dist/storage/knownState.d.ts.map +1 -1
  51. package/dist/storage/knownState.js +11 -0
  52. package/dist/storage/knownState.js.map +1 -1
  53. package/dist/storage/sqlite/client.d.ts +2 -0
  54. package/dist/storage/sqlite/client.d.ts.map +1 -1
  55. package/dist/storage/sqlite/client.js +18 -0
  56. package/dist/storage/sqlite/client.js.map +1 -1
  57. package/dist/storage/sqliteAsync/client.d.ts +2 -0
  58. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  59. package/dist/storage/sqliteAsync/client.js +20 -0
  60. package/dist/storage/sqliteAsync/client.js.map +1 -1
  61. package/dist/storage/storageAsync.d.ts +10 -3
  62. package/dist/storage/storageAsync.d.ts.map +1 -1
  63. package/dist/storage/storageAsync.js +52 -3
  64. package/dist/storage/storageAsync.js.map +1 -1
  65. package/dist/storage/storageSync.d.ts +9 -3
  66. package/dist/storage/storageSync.d.ts.map +1 -1
  67. package/dist/storage/storageSync.js +27 -3
  68. package/dist/storage/storageSync.js.map +1 -1
  69. package/dist/storage/types.d.ts +23 -0
  70. package/dist/storage/types.d.ts.map +1 -1
  71. package/dist/sync.d.ts +23 -0
  72. package/dist/sync.d.ts.map +1 -1
  73. package/dist/sync.js +136 -45
  74. package/dist/sync.js.map +1 -1
  75. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  76. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  77. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  78. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  79. package/dist/tests/GarbageCollector.test.js +87 -13
  80. package/dist/tests/GarbageCollector.test.js.map +1 -1
  81. package/dist/tests/StorageApiAsync.test.js +124 -1
  82. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  83. package/dist/tests/StorageApiSync.test.js +123 -0
  84. package/dist/tests/StorageApiSync.test.js.map +1 -1
  85. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  86. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  87. package/dist/tests/SyncStateManager.test.js +1 -1
  88. package/dist/tests/SyncStateManager.test.js.map +1 -1
  89. package/dist/tests/coPlainText.test.js +1 -1
  90. package/dist/tests/coPlainText.test.js.map +1 -1
  91. package/dist/tests/coValueCore.loadFromStorage.test.js +2 -0
  92. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  93. package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
  94. package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
  95. package/dist/tests/knownState.lazyLoading.test.js +167 -0
  96. package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
  97. package/dist/tests/messagesTestUtils.d.ts +5 -2
  98. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  99. package/dist/tests/messagesTestUtils.js +4 -0
  100. package/dist/tests/messagesTestUtils.js.map +1 -1
  101. package/dist/tests/sync.garbageCollection.test.js +56 -32
  102. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  103. package/dist/tests/sync.load.test.js +387 -1
  104. package/dist/tests/sync.load.test.js.map +1 -1
  105. package/dist/tests/sync.mesh.test.js +5 -5
  106. package/dist/tests/sync.mesh.test.js.map +1 -1
  107. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  108. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  109. package/dist/tests/sync.storage.test.js +9 -9
  110. package/dist/tests/sync.storage.test.js.map +1 -1
  111. package/dist/tests/sync.storageAsync.test.js +7 -7
  112. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  113. package/dist/tests/sync.tracking.test.js +35 -4
  114. package/dist/tests/sync.tracking.test.js.map +1 -1
  115. package/dist/tests/testStorage.js +38 -2
  116. package/dist/tests/testStorage.js.map +1 -1
  117. package/dist/tests/testUtils.d.ts +38 -4
  118. package/dist/tests/testUtils.d.ts.map +1 -1
  119. package/dist/tests/testUtils.js +68 -7
  120. package/dist/tests/testUtils.js.map +1 -1
  121. package/package.json +4 -4
  122. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  123. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  124. package/src/CojsonMessageChannel/index.ts +9 -0
  125. package/src/CojsonMessageChannel/types.ts +200 -0
  126. package/src/GarbageCollector.ts +5 -5
  127. package/src/SyncStateManager.ts +6 -6
  128. package/src/coValueCore/coValueCore.ts +56 -7
  129. package/src/coValues/account.ts +12 -14
  130. package/src/exports.ts +1 -0
  131. package/src/ids.ts +1 -1
  132. package/src/knownState.ts +24 -0
  133. package/src/localNode.ts +12 -7
  134. package/src/storage/knownState.ts +12 -0
  135. package/src/storage/sqlite/client.ts +31 -0
  136. package/src/storage/sqliteAsync/client.ts +35 -0
  137. package/src/storage/storageAsync.ts +66 -4
  138. package/src/storage/storageSync.ts +37 -4
  139. package/src/storage/types.ts +32 -0
  140. package/src/sync.ts +159 -46
  141. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  142. package/src/tests/GarbageCollector.test.ts +114 -13
  143. package/src/tests/StorageApiAsync.test.ts +186 -1
  144. package/src/tests/StorageApiSync.test.ts +181 -0
  145. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  146. package/src/tests/SyncStateManager.test.ts +1 -1
  147. package/src/tests/coPlainText.test.ts +1 -1
  148. package/src/tests/coValueCore.loadFromStorage.test.ts +5 -0
  149. package/src/tests/knownState.lazyLoading.test.ts +219 -0
  150. package/src/tests/messagesTestUtils.ts +10 -3
  151. package/src/tests/sync.garbageCollection.test.ts +69 -36
  152. package/src/tests/sync.load.test.ts +482 -2
  153. package/src/tests/sync.mesh.test.ts +5 -5
  154. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  155. package/src/tests/sync.storage.test.ts +9 -9
  156. package/src/tests/sync.storageAsync.test.ts +7 -7
  157. package/src/tests/sync.tracking.test.ts +54 -4
  158. package/src/tests/testStorage.ts +40 -2
  159. package/src/tests/testUtils.ts +99 -8
@@ -373,16 +373,26 @@ export class CoValueCore {
373
373
  }
374
374
  }
375
375
 
376
- unmount(garbageCollectGroups = false) {
377
- if (
378
- !garbageCollectGroups &&
379
- this.verified?.header.ruleset.type === "group"
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
- if (this.listeners.size > 0) {
385
- return false; // The coValue is still in use
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;
@@ -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
- return this._cachedCurrentAgentID;
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 agents = this.keys()
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 (agents.length !== 1) {
59
- logger.warn("Account has " + agents.length + " agents", { id: this.id });
56
+ if (!isAgentID(initialAdmin)) {
57
+ throw new Error("You can read agent ids only from account values");
60
58
  }
61
59
 
62
- this._cachedCurrentAgentID = agents[0];
60
+ this._cachedCurrentAgentID = initialAdmin;
63
61
 
64
- return agents[0]!;
62
+ return initialAdmin;
65
63
  }
66
64
 
67
65
  override createInvite(_: AccountRole): InviteSecret {
package/src/exports.ts CHANGED
@@ -206,6 +206,7 @@ export type {
206
206
  };
207
207
 
208
208
  export * from "./storage/index.js";
209
+ export * from "./CojsonMessageChannel/index.js";
209
210
 
210
211
  // biome-ignore format: off
211
212
  // eslint-disable-next-line @typescript-eslint/no-namespace
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: string): id is AgentID {
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(opts?: { garbageCollectGroups?: boolean }) {
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 account = coValue.getCurrentContent() as RawAccount;
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: account.currentAgentID(), error: undefined };
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
- private loadedCoValues = new Set<RawCoID>();
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.loadedCoValues.add(coValueRow.id);
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.loadedCoValues.has(dependedOnCoValue)) {
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
- private loadedCoValues = new Set<RawCoID>();
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.loadedCoValues.add(coValueRow.id);
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.loadedCoValues.has(dependedOnCoValue)) {
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
  }
@@ -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
  }