cojson 0.19.19 → 0.19.21

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 (140) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/coValueCore/coValueCore.d.ts +9 -0
  3. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  4. package/dist/coValueCore/coValueCore.js +21 -0
  5. package/dist/coValueCore/coValueCore.js.map +1 -1
  6. package/dist/coValues/account.d.ts.map +1 -1
  7. package/dist/coValues/account.js +10 -10
  8. package/dist/coValues/account.js.map +1 -1
  9. package/dist/config.d.ts +6 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +10 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/exports.d.ts +7 -1
  14. package/dist/exports.d.ts.map +1 -1
  15. package/dist/exports.js +4 -1
  16. package/dist/exports.js.map +1 -1
  17. package/dist/ids.d.ts +1 -1
  18. package/dist/ids.d.ts.map +1 -1
  19. package/dist/ids.js.map +1 -1
  20. package/dist/knownState.d.ts +5 -0
  21. package/dist/knownState.d.ts.map +1 -1
  22. package/dist/knownState.js +15 -0
  23. package/dist/knownState.js.map +1 -1
  24. package/dist/localNode.d.ts.map +1 -1
  25. package/dist/localNode.js +8 -2
  26. package/dist/localNode.js.map +1 -1
  27. package/dist/queue/IncomingMessagesQueue.d.ts +6 -7
  28. package/dist/queue/IncomingMessagesQueue.d.ts.map +1 -1
  29. package/dist/queue/IncomingMessagesQueue.js +7 -30
  30. package/dist/queue/IncomingMessagesQueue.js.map +1 -1
  31. package/dist/queue/LinkedList.d.ts +1 -1
  32. package/dist/queue/LinkedList.d.ts.map +1 -1
  33. package/dist/queue/LinkedList.js.map +1 -1
  34. package/dist/queue/StorageStreamingQueue.d.ts +43 -0
  35. package/dist/queue/StorageStreamingQueue.d.ts.map +1 -0
  36. package/dist/queue/StorageStreamingQueue.js +70 -0
  37. package/dist/queue/StorageStreamingQueue.js.map +1 -0
  38. package/dist/storage/knownState.d.ts +5 -0
  39. package/dist/storage/knownState.d.ts.map +1 -1
  40. package/dist/storage/knownState.js +11 -0
  41. package/dist/storage/knownState.js.map +1 -1
  42. package/dist/storage/sqlite/client.d.ts +2 -0
  43. package/dist/storage/sqlite/client.d.ts.map +1 -1
  44. package/dist/storage/sqlite/client.js +18 -0
  45. package/dist/storage/sqlite/client.js.map +1 -1
  46. package/dist/storage/sqliteAsync/client.d.ts +2 -0
  47. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  48. package/dist/storage/sqliteAsync/client.js +20 -0
  49. package/dist/storage/sqliteAsync/client.js.map +1 -1
  50. package/dist/storage/storageAsync.d.ts +2 -0
  51. package/dist/storage/storageAsync.d.ts.map +1 -1
  52. package/dist/storage/storageAsync.js +40 -0
  53. package/dist/storage/storageAsync.js.map +1 -1
  54. package/dist/storage/storageSync.d.ts +9 -2
  55. package/dist/storage/storageSync.d.ts.map +1 -1
  56. package/dist/storage/storageSync.js +71 -44
  57. package/dist/storage/storageSync.js.map +1 -1
  58. package/dist/storage/types.d.ts +20 -0
  59. package/dist/storage/types.d.ts.map +1 -1
  60. package/dist/sync.d.ts +34 -0
  61. package/dist/sync.d.ts.map +1 -1
  62. package/dist/sync.js +185 -46
  63. package/dist/sync.js.map +1 -1
  64. package/dist/tests/IncomingMessagesQueue.test.js +4 -150
  65. package/dist/tests/IncomingMessagesQueue.test.js.map +1 -1
  66. package/dist/tests/StorageApiAsync.test.js +91 -0
  67. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  68. package/dist/tests/StorageApiSync.test.js +91 -0
  69. package/dist/tests/StorageApiSync.test.js.map +1 -1
  70. package/dist/tests/StorageStreamingQueue.test.d.ts +2 -0
  71. package/dist/tests/StorageStreamingQueue.test.d.ts.map +1 -0
  72. package/dist/tests/StorageStreamingQueue.test.js +213 -0
  73. package/dist/tests/StorageStreamingQueue.test.js.map +1 -0
  74. package/dist/tests/SyncManager.processQueues.test.d.ts +2 -0
  75. package/dist/tests/SyncManager.processQueues.test.d.ts.map +1 -0
  76. package/dist/tests/SyncManager.processQueues.test.js +208 -0
  77. package/dist/tests/SyncManager.processQueues.test.js.map +1 -0
  78. package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
  79. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  80. package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
  81. package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
  82. package/dist/tests/knownState.lazyLoading.test.js +166 -0
  83. package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
  84. package/dist/tests/messagesTestUtils.d.ts +5 -2
  85. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  86. package/dist/tests/messagesTestUtils.js +4 -0
  87. package/dist/tests/messagesTestUtils.js.map +1 -1
  88. package/dist/tests/setup.d.ts +2 -0
  89. package/dist/tests/setup.d.ts.map +1 -0
  90. package/dist/tests/setup.js +4 -0
  91. package/dist/tests/setup.js.map +1 -0
  92. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  93. package/dist/tests/sync.load.test.js +388 -0
  94. package/dist/tests/sync.load.test.js.map +1 -1
  95. package/dist/tests/sync.mesh.test.js +23 -23
  96. package/dist/tests/sync.storage.test.js +176 -20
  97. package/dist/tests/sync.storage.test.js.map +1 -1
  98. package/dist/tests/sync.test.js +1 -1
  99. package/dist/tests/sync.test.js.map +1 -1
  100. package/dist/tests/testStorage.js +36 -0
  101. package/dist/tests/testStorage.js.map +1 -1
  102. package/dist/tests/testUtils.d.ts +16 -4
  103. package/dist/tests/testUtils.d.ts.map +1 -1
  104. package/dist/tests/testUtils.js +2 -2
  105. package/dist/tests/testUtils.js.map +1 -1
  106. package/package.json +4 -4
  107. package/src/coValueCore/coValueCore.ts +26 -0
  108. package/src/coValues/account.ts +12 -14
  109. package/src/config.ts +13 -0
  110. package/src/exports.ts +6 -0
  111. package/src/ids.ts +1 -1
  112. package/src/knownState.ts +24 -0
  113. package/src/localNode.ts +9 -2
  114. package/src/queue/IncomingMessagesQueue.ts +7 -39
  115. package/src/queue/LinkedList.ts +1 -1
  116. package/src/queue/StorageStreamingQueue.ts +96 -0
  117. package/src/storage/knownState.ts +12 -0
  118. package/src/storage/sqlite/client.ts +31 -0
  119. package/src/storage/sqliteAsync/client.ts +35 -0
  120. package/src/storage/storageAsync.ts +51 -0
  121. package/src/storage/storageSync.ts +121 -55
  122. package/src/storage/types.ts +29 -0
  123. package/src/sync.ts +210 -47
  124. package/src/tests/IncomingMessagesQueue.test.ts +4 -206
  125. package/src/tests/StorageApiAsync.test.ts +136 -0
  126. package/src/tests/StorageApiSync.test.ts +132 -0
  127. package/src/tests/StorageStreamingQueue.test.ts +276 -0
  128. package/src/tests/SyncManager.processQueues.test.ts +287 -0
  129. package/src/tests/coValueCore.loadFromStorage.test.ts +3 -0
  130. package/src/tests/knownState.lazyLoading.test.ts +217 -0
  131. package/src/tests/messagesTestUtils.ts +10 -3
  132. package/src/tests/setup.ts +4 -0
  133. package/src/tests/sync.garbageCollection.test.ts +1 -3
  134. package/src/tests/sync.load.test.ts +483 -1
  135. package/src/tests/sync.mesh.test.ts +23 -23
  136. package/src/tests/sync.storage.test.ts +224 -32
  137. package/src/tests/sync.test.ts +1 -9
  138. package/src/tests/testStorage.ts +38 -0
  139. package/src/tests/testUtils.ts +16 -4
  140. package/vitest.config.ts +1 -0
@@ -36,6 +36,12 @@ export class StorageApiAsync implements StorageAPI {
36
36
 
37
37
  private loadedCoValues = new Set<RawCoID>();
38
38
 
39
+ // Track pending loads to deduplicate concurrent requests
40
+ private pendingKnownStateLoads = new Map<
41
+ string,
42
+ Promise<CoValueKnownState | undefined>
43
+ >();
44
+
39
45
  constructor(dbClient: DBClientInterfaceAsync) {
40
46
  this.dbClient = dbClient;
41
47
  }
@@ -46,6 +52,51 @@ export class StorageApiAsync implements StorageAPI {
46
52
  return this.knownStates.getKnownState(id);
47
53
  }
48
54
 
55
+ loadKnownState(
56
+ id: string,
57
+ callback: (knownState: CoValueKnownState | undefined) => void,
58
+ ): void {
59
+ // Check in-memory cache first
60
+ const cached = this.knownStates.getCachedKnownState(id);
61
+ if (cached) {
62
+ callback(cached);
63
+ return;
64
+ }
65
+
66
+ // Check if there's already a pending load for this ID (deduplication)
67
+ const pending = this.pendingKnownStateLoads.get(id);
68
+ if (pending) {
69
+ // Ensure callback is always called, even if pending fails unexpectedly
70
+ pending.then(callback, () => callback(undefined));
71
+ return;
72
+ }
73
+
74
+ // Start new load and track it for deduplication
75
+ const loadPromise = this.dbClient
76
+ .getCoValueKnownState(id)
77
+ .then((knownState) => {
78
+ if (knownState) {
79
+ // Cache for future use
80
+ this.knownStates.setKnownState(id, knownState);
81
+ }
82
+ return knownState;
83
+ })
84
+ .catch((err) => {
85
+ // Error handling contract:
86
+ // - Log warning
87
+ // - Behave like "not found" so callers can fall back (full load / load from peers)
88
+ logger.warn("Failed to load knownState from storage", { id, err });
89
+ return undefined;
90
+ })
91
+ .finally(() => {
92
+ // Remove from pending map after completion (success or failure)
93
+ this.pendingKnownStateLoads.delete(id);
94
+ });
95
+
96
+ this.pendingKnownStateLoads.set(id, loadPromise);
97
+ loadPromise.then(callback);
98
+ }
99
+
49
100
  async load(
50
101
  id: string,
51
102
  callback: (data: NewContentMessage) => void,
@@ -1,4 +1,3 @@
1
- import { UpDownCounter, metrics } from "@opentelemetry/api";
2
1
  import {
3
2
  createContentMessage,
4
3
  exceedsRecommendedSize,
@@ -30,22 +29,25 @@ import type {
30
29
  StoredCoValueRow,
31
30
  StoredSessionRow,
32
31
  } from "./types.js";
32
+ import {
33
+ ContentCallback,
34
+ StorageStreamingQueue,
35
+ } from "../queue/StorageStreamingQueue.js";
36
+ import { getPriorityFromHeader } from "../priority.js";
33
37
 
34
38
  export class StorageApiSync implements StorageAPI {
35
- private streamingCounter: UpDownCounter;
36
-
37
39
  private readonly dbClient: DBClientInterfaceSync;
38
40
  private loadedCoValues = new Set<RawCoID>();
39
41
 
42
+ /**
43
+ * Queue for streaming content that will be pulled by SyncManager.
44
+ * Only used when content requires streaming (multiple chunks).
45
+ */
46
+ readonly streamingQueue: StorageStreamingQueue;
47
+
40
48
  constructor(dbClient: DBClientInterfaceSync) {
41
49
  this.dbClient = dbClient;
42
- this.streamingCounter = metrics
43
- .getMeter("cojson")
44
- .createUpDownCounter(`jazz.storage.streaming`, {
45
- description: "Number of streaming coValues",
46
- unit: "1",
47
- });
48
- this.streamingCounter.add(0);
50
+ this.streamingQueue = new StorageStreamingQueue();
49
51
  }
50
52
 
51
53
  knownStates = new StorageKnownState();
@@ -54,6 +56,28 @@ export class StorageApiSync implements StorageAPI {
54
56
  return this.knownStates.getKnownState(id);
55
57
  }
56
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
+ // Load from database
71
+ const knownState = this.dbClient.getCoValueKnownState(id);
72
+
73
+ if (knownState) {
74
+ // Cache for future use
75
+ this.knownStates.setKnownState(id, knownState);
76
+ }
77
+
78
+ callback(knownState);
79
+ }
80
+
57
81
  async load(
58
82
  id: string,
59
83
  callback: (data: NewContentMessage) => void,
@@ -62,7 +86,7 @@ export class StorageApiSync implements StorageAPI {
62
86
  await this.loadCoValue(id, callback, done);
63
87
  }
64
88
 
65
- async loadCoValue(
89
+ loadCoValue(
66
90
  id: string,
67
91
  callback: (data: NewContentMessage) => void,
68
92
  done?: (found: boolean) => void,
@@ -89,8 +113,18 @@ export class StorageApiSync implements StorageAPI {
89
113
 
90
114
  if (signatures.length > 0) {
91
115
  contentStreaming = true;
92
- signaturesBySession.set(sessionRow.sessionID, signatures);
93
116
  }
117
+
118
+ const lastSignature = signatures[signatures.length - 1];
119
+
120
+ if (lastSignature?.signature !== sessionRow.lastSignature) {
121
+ signatures.push({
122
+ idx: sessionRow.lastIdx,
123
+ signature: sessionRow.lastSignature,
124
+ });
125
+ }
126
+
127
+ signaturesBySession.set(sessionRow.sessionID, signatures);
94
128
  }
95
129
 
96
130
  const knownState = this.knownStates.getKnownState(coValueRow.id);
@@ -106,78 +140,110 @@ export class StorageApiSync implements StorageAPI {
106
140
 
107
141
  this.loadedCoValues.add(coValueRow.id);
108
142
 
109
- let contentMessage = createContentMessage(coValueRow.id, coValueRow.header);
143
+ const priority = getPriorityFromHeader(coValueRow.header);
144
+ const contentMessage = createContentMessage(
145
+ coValueRow.id,
146
+ coValueRow.header,
147
+ );
110
148
 
111
149
  if (contentStreaming) {
112
- this.streamingCounter.add(1);
113
150
  contentMessage.expectContentUntil = knownState.sessions;
114
151
  }
115
152
 
153
+ const streamingQueue: ContentCallback[] = [];
154
+
116
155
  for (const sessionRow of allCoValueSessions) {
117
- const signatures = signaturesBySession.get(sessionRow.sessionID) || [];
156
+ const signatures = signaturesBySession.get(sessionRow.sessionID);
118
157
 
119
- let idx = 0;
158
+ if (!signatures) {
159
+ throw new Error("Signatures not found for session");
160
+ }
120
161
 
121
- const lastSignature = signatures[signatures.length - 1];
162
+ const firstSignature = signatures[0];
122
163
 
123
- if (lastSignature?.signature !== sessionRow.lastSignature) {
124
- signatures.push({
125
- idx: sessionRow.lastIdx,
126
- signature: sessionRow.lastSignature,
127
- });
164
+ if (!firstSignature) {
165
+ continue;
128
166
  }
129
167
 
130
- for (const signature of signatures) {
131
- const newTxsInSession = this.dbClient.getNewTransactionInSession(
132
- sessionRow.rowID,
133
- idx,
134
- signature.idx,
135
- );
168
+ this.loadSessionTransactions(
169
+ contentMessage,
170
+ sessionRow,
171
+ 0,
172
+ firstSignature,
173
+ );
136
174
 
137
- collectNewTxs({
138
- newTxsInSession,
139
- contentMessage,
140
- sessionRow,
141
- firstNewTxIdx: idx,
142
- signature: signature.signature,
143
- });
175
+ for (let i = 1; i < signatures.length; i++) {
176
+ const prevSignature = signatures[i - 1];
144
177
 
145
- idx = signature.idx + 1;
178
+ if (!prevSignature) {
179
+ throw new Error("Previous signature is nullish");
180
+ }
146
181
 
147
- if (signatures.length > 1) {
148
- this.pushContentWithDependencies(
149
- coValueRow,
150
- contentMessage,
151
- callback,
152
- );
153
- contentMessage = createContentMessage(
182
+ streamingQueue.push(() => {
183
+ const contentMessage = createContentMessage(
154
184
  coValueRow.id,
155
185
  coValueRow.header,
156
186
  );
157
187
 
158
- // Introduce a delay to not block the main thread
159
- // for the entire content processing
160
- await new Promise((resolve) => setTimeout(resolve));
161
- }
188
+ const signature = signatures[i];
189
+ if (!signature) throw new Error("Signature item is nullish");
190
+
191
+ this.loadSessionTransactions(
192
+ contentMessage,
193
+ sessionRow,
194
+ prevSignature.idx + 1,
195
+ signature,
196
+ );
197
+
198
+ if (Object.keys(contentMessage.new).length > 0) {
199
+ this.pushContentWithDependencies(
200
+ coValueRow,
201
+ contentMessage,
202
+ callback,
203
+ );
204
+ }
205
+ });
162
206
  }
163
207
  }
164
208
 
165
- const hasNewContent = Object.keys(contentMessage.new).length > 0;
209
+ // Send the first chunk
210
+ this.pushContentWithDependencies(coValueRow, contentMessage, callback);
211
+ this.knownStates.handleUpdate(coValueRow.id, knownState);
166
212
 
167
- // If there is no new content but steaming is not active, it's the case for a coValue with the header but no transactions
168
- // For streaming the push has already been done in the loop above
169
- if (hasNewContent || !contentStreaming) {
170
- this.pushContentWithDependencies(coValueRow, contentMessage, callback);
213
+ // All priorities go through the queue (HIGH > MEDIUM > LOW)
214
+ for (const pushStreamingContent of streamingQueue) {
215
+ this.streamingQueue.push(pushStreamingContent, priority);
171
216
  }
172
217
 
173
- if (contentStreaming) {
174
- this.streamingCounter.add(-1);
218
+ // Trigger the queue to process the entries
219
+ if (streamingQueue.length > 0) {
220
+ this.streamingQueue.emit();
175
221
  }
176
222
 
177
- this.knownStates.handleUpdate(coValueRow.id, knownState);
178
223
  done?.(true);
179
224
  }
180
225
 
226
+ private loadSessionTransactions(
227
+ contentMessage: NewContentMessage,
228
+ sessionRow: StoredSessionRow,
229
+ idx: number,
230
+ signature: Pick<SignatureAfterRow, "idx" | "signature">,
231
+ ) {
232
+ const newTxsInSession = this.dbClient.getNewTransactionInSession(
233
+ sessionRow.rowID,
234
+ idx,
235
+ signature.idx,
236
+ );
237
+
238
+ collectNewTxs({
239
+ newTxsInSession,
240
+ contentMessage,
241
+ sessionRow,
242
+ firstNewTxIdx: idx,
243
+ signature: signature.signature,
244
+ });
245
+ }
246
+
181
247
  async pushContentWithDependencies(
182
248
  coValueRow: StoredCoValueRow,
183
249
  contentMessage: NewContentMessage,
@@ -7,6 +7,7 @@ import type { CoValueCore, RawCoID, SessionID } from "../exports.js";
7
7
  import { NewContentMessage } from "../sync.js";
8
8
  import type { PeerID } from "../sync.js";
9
9
  import { CoValueKnownState } from "../knownState.js";
10
+ import { StorageStreamingQueue } from "../queue/StorageStreamingQueue.js";
10
11
 
11
12
  export type CorrectionCallback = (
12
13
  correction: CoValueKnownState,
@@ -26,6 +27,8 @@ export interface StorageAPI {
26
27
  ): void;
27
28
  store(data: NewContentMessage, handleCorrection: CorrectionCallback): void;
28
29
 
30
+ streamingQueue?: StorageStreamingQueue;
31
+
29
32
  getKnownState(id: string): CoValueKnownState;
30
33
 
31
34
  waitForSync(id: string, coValue: CoValueCore): Promise<void>;
@@ -52,6 +55,18 @@ export interface StorageAPI {
52
55
  */
53
56
  stopTrackingSyncState(id: RawCoID): void;
54
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
+
55
70
  close(): Promise<unknown> | undefined;
56
71
  }
57
72
 
@@ -149,6 +164,14 @@ export interface DBClientInterfaceAsync {
149
164
  getUnsyncedCoValueIDs(): Promise<RawCoID[]>;
150
165
 
151
166
  stopTrackingSyncState(id: RawCoID): Promise<void>;
167
+
168
+ /**
169
+ * Get the knownState for a CoValue without loading transactions.
170
+ * Returns undefined if the CoValue doesn't exist.
171
+ */
172
+ getCoValueKnownState(
173
+ coValueId: string,
174
+ ): Promise<CoValueKnownState | undefined>;
152
175
  }
153
176
 
154
177
  export interface DBTransactionInterfaceSync {
@@ -209,4 +232,10 @@ export interface DBClientInterfaceSync {
209
232
  getUnsyncedCoValueIDs(): RawCoID[];
210
233
 
211
234
  stopTrackingSyncState(id: RawCoID): void;
235
+
236
+ /**
237
+ * Get the knownState for a CoValue without loading transactions.
238
+ * Returns undefined if the CoValue doesn't exist.
239
+ */
240
+ getCoValueKnownState(coValueId: string): CoValueKnownState | undefined;
212
241
  }