cojson 0.20.9 → 0.20.11

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 (179) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/dist/OngoingStorageReconciliationTracker.d.ts +16 -0
  4. package/dist/OngoingStorageReconciliationTracker.d.ts.map +1 -0
  5. package/dist/OngoingStorageReconciliationTracker.js +75 -0
  6. package/dist/OngoingStorageReconciliationTracker.js.map +1 -0
  7. package/dist/PeerState.d.ts +2 -2
  8. package/dist/PeerState.d.ts.map +1 -1
  9. package/dist/PeerState.js +3 -3
  10. package/dist/PeerState.js.map +1 -1
  11. package/dist/StorageReconciliationAckTracker.d.ts +14 -0
  12. package/dist/StorageReconciliationAckTracker.d.ts.map +1 -0
  13. package/dist/StorageReconciliationAckTracker.js +72 -0
  14. package/dist/StorageReconciliationAckTracker.js.map +1 -0
  15. package/dist/SyncStateManager.js +2 -2
  16. package/dist/SyncStateManager.js.map +1 -1
  17. package/dist/coValueCore/coValueCore.d.ts +2 -1
  18. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  19. package/dist/coValueCore/coValueCore.js +43 -10
  20. package/dist/coValueCore/coValueCore.js.map +1 -1
  21. package/dist/coValues/coList.d.ts +2 -0
  22. package/dist/coValues/coList.d.ts.map +1 -1
  23. package/dist/coValues/coList.js +28 -0
  24. package/dist/coValues/coList.js.map +1 -1
  25. package/dist/coValues/group.d.ts +4 -1
  26. package/dist/coValues/group.d.ts.map +1 -1
  27. package/dist/coValues/group.js +15 -1
  28. package/dist/coValues/group.js.map +1 -1
  29. package/dist/config.d.ts +8 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +14 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/exports.d.ts +9 -1
  34. package/dist/exports.d.ts.map +1 -1
  35. package/dist/exports.js +5 -1
  36. package/dist/exports.js.map +1 -1
  37. package/dist/localNode.d.ts +7 -3
  38. package/dist/localNode.d.ts.map +1 -1
  39. package/dist/localNode.js +13 -5
  40. package/dist/localNode.js.map +1 -1
  41. package/dist/permissions.d.ts +1 -0
  42. package/dist/permissions.d.ts.map +1 -1
  43. package/dist/queue/LinkedList.d.ts +2 -0
  44. package/dist/queue/LinkedList.d.ts.map +1 -1
  45. package/dist/queue/LinkedList.js +7 -0
  46. package/dist/queue/LinkedList.js.map +1 -1
  47. package/dist/queue/OutgoingLoadQueue.d.ts +4 -1
  48. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -1
  49. package/dist/queue/OutgoingLoadQueue.js +41 -13
  50. package/dist/queue/OutgoingLoadQueue.js.map +1 -1
  51. package/dist/queue/PriorityBasedMessageQueue.d.ts +1 -0
  52. package/dist/queue/PriorityBasedMessageQueue.d.ts.map +1 -1
  53. package/dist/queue/PriorityBasedMessageQueue.js +11 -1
  54. package/dist/queue/PriorityBasedMessageQueue.js.map +1 -1
  55. package/dist/storage/knownState.d.ts +2 -0
  56. package/dist/storage/knownState.d.ts.map +1 -1
  57. package/dist/storage/knownState.js +11 -0
  58. package/dist/storage/knownState.js.map +1 -1
  59. package/dist/storage/sqlite/client.d.ts +10 -1
  60. package/dist/storage/sqlite/client.d.ts.map +1 -1
  61. package/dist/storage/sqlite/client.js +84 -0
  62. package/dist/storage/sqlite/client.js.map +1 -1
  63. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  64. package/dist/storage/sqlite/sqliteMigrations.js +11 -0
  65. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  66. package/dist/storage/sqliteAsync/client.d.ts +10 -1
  67. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  68. package/dist/storage/sqliteAsync/client.js +86 -0
  69. package/dist/storage/sqliteAsync/client.js.map +1 -1
  70. package/dist/storage/storageAsync.d.ts +9 -2
  71. package/dist/storage/storageAsync.d.ts.map +1 -1
  72. package/dist/storage/storageAsync.js +19 -0
  73. package/dist/storage/storageAsync.js.map +1 -1
  74. package/dist/storage/storageSync.d.ts +9 -2
  75. package/dist/storage/storageSync.d.ts.map +1 -1
  76. package/dist/storage/storageSync.js +20 -13
  77. package/dist/storage/storageSync.js.map +1 -1
  78. package/dist/storage/types.d.ts +64 -0
  79. package/dist/storage/types.d.ts.map +1 -1
  80. package/dist/storage/types.js.map +1 -1
  81. package/dist/sync.d.ts +53 -2
  82. package/dist/sync.d.ts.map +1 -1
  83. package/dist/sync.js +300 -44
  84. package/dist/sync.js.map +1 -1
  85. package/dist/tests/OngoingStorageReconciliationTracker.test.d.ts +2 -0
  86. package/dist/tests/OngoingStorageReconciliationTracker.test.d.ts.map +1 -0
  87. package/dist/tests/OngoingStorageReconciliationTracker.test.js +60 -0
  88. package/dist/tests/OngoingStorageReconciliationTracker.test.js.map +1 -0
  89. package/dist/tests/OutgoingLoadQueue.test.js +137 -39
  90. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -1
  91. package/dist/tests/SQLiteClientAsync.test.js +1 -1
  92. package/dist/tests/SQLiteClientAsync.test.js.map +1 -1
  93. package/dist/tests/StorageApiAsync.test.js +138 -0
  94. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  95. package/dist/tests/StorageApiSync.test.js +154 -0
  96. package/dist/tests/StorageApiSync.test.js.map +1 -1
  97. package/dist/tests/StorageReconciliationAckTracker.test.d.ts +2 -0
  98. package/dist/tests/StorageReconciliationAckTracker.test.d.ts.map +1 -0
  99. package/dist/tests/StorageReconciliationAckTracker.test.js +74 -0
  100. package/dist/tests/StorageReconciliationAckTracker.test.js.map +1 -0
  101. package/dist/tests/SyncStateManager.test.js +18 -0
  102. package/dist/tests/SyncStateManager.test.js.map +1 -1
  103. package/dist/tests/coList.test.js +112 -1
  104. package/dist/tests/coList.test.js.map +1 -1
  105. package/dist/tests/coValueCore.loadFromStorage.test.js +36 -0
  106. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  107. package/dist/tests/group.test.js +44 -0
  108. package/dist/tests/group.test.js.map +1 -1
  109. package/dist/tests/knownState.lazyLoading.test.js +6 -0
  110. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  111. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  112. package/dist/tests/messagesTestUtils.js +4 -0
  113. package/dist/tests/messagesTestUtils.js.map +1 -1
  114. package/dist/tests/sync.concurrentLoad.test.js +333 -1
  115. package/dist/tests/sync.concurrentLoad.test.js.map +1 -1
  116. package/dist/tests/sync.garbageCollection.test.js +4 -0
  117. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  118. package/dist/tests/sync.load.test.js +19 -0
  119. package/dist/tests/sync.load.test.js.map +1 -1
  120. package/dist/tests/sync.mesh.test.js +1 -0
  121. package/dist/tests/sync.mesh.test.js.map +1 -1
  122. package/dist/tests/sync.multipleServers.test.js +41 -3
  123. package/dist/tests/sync.multipleServers.test.js.map +1 -1
  124. package/dist/tests/sync.storage.test.js +2 -0
  125. package/dist/tests/sync.storage.test.js.map +1 -1
  126. package/dist/tests/sync.storageAsync.test.js +1 -0
  127. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  128. package/dist/tests/sync.storageReconciliation.test.d.ts +2 -0
  129. package/dist/tests/sync.storageReconciliation.test.d.ts.map +1 -0
  130. package/dist/tests/sync.storageReconciliation.test.js +502 -0
  131. package/dist/tests/sync.storageReconciliation.test.js.map +1 -0
  132. package/dist/tests/testUtils.d.ts +1 -0
  133. package/dist/tests/testUtils.d.ts.map +1 -1
  134. package/dist/tests/testUtils.js +3 -2
  135. package/dist/tests/testUtils.js.map +1 -1
  136. package/package.json +4 -4
  137. package/src/OngoingStorageReconciliationTracker.ts +97 -0
  138. package/src/PeerState.ts +10 -3
  139. package/src/StorageReconciliationAckTracker.ts +83 -0
  140. package/src/SyncStateManager.ts +3 -3
  141. package/src/coValueCore/coValueCore.ts +47 -16
  142. package/src/coValues/coList.ts +23 -0
  143. package/src/coValues/group.ts +18 -0
  144. package/src/config.ts +18 -0
  145. package/src/exports.ts +8 -0
  146. package/src/localNode.ts +18 -0
  147. package/src/permissions.ts +1 -1
  148. package/src/queue/LinkedList.ts +10 -0
  149. package/src/queue/OutgoingLoadQueue.ts +57 -15
  150. package/src/queue/PriorityBasedMessageQueue.ts +15 -1
  151. package/src/storage/knownState.ts +14 -0
  152. package/src/storage/sqlite/client.ts +128 -0
  153. package/src/storage/sqlite/sqliteMigrations.ts +11 -0
  154. package/src/storage/sqliteAsync/client.ts +139 -0
  155. package/src/storage/storageAsync.ts +37 -0
  156. package/src/storage/storageSync.ts +41 -16
  157. package/src/storage/types.ts +110 -0
  158. package/src/sync.ts +359 -14
  159. package/src/tests/OngoingStorageReconciliationTracker.test.ts +85 -0
  160. package/src/tests/OutgoingLoadQueue.test.ts +226 -59
  161. package/src/tests/SQLiteClientAsync.test.ts +1 -1
  162. package/src/tests/StorageApiAsync.test.ts +161 -1
  163. package/src/tests/StorageApiSync.test.ts +176 -0
  164. package/src/tests/StorageReconciliationAckTracker.test.ts +99 -0
  165. package/src/tests/SyncStateManager.test.ts +25 -0
  166. package/src/tests/coList.test.ts +138 -0
  167. package/src/tests/coValueCore.loadFromStorage.test.ts +72 -1
  168. package/src/tests/group.test.ts +87 -0
  169. package/src/tests/knownState.lazyLoading.test.ts +36 -1
  170. package/src/tests/messagesTestUtils.ts +4 -0
  171. package/src/tests/sync.concurrentLoad.test.ts +491 -0
  172. package/src/tests/sync.garbageCollection.test.ts +4 -0
  173. package/src/tests/sync.load.test.ts +26 -0
  174. package/src/tests/sync.mesh.test.ts +1 -0
  175. package/src/tests/sync.multipleServers.test.ts +60 -2
  176. package/src/tests/sync.storage.test.ts +2 -0
  177. package/src/tests/sync.storageAsync.test.ts +1 -0
  178. package/src/tests/sync.storageReconciliation.test.ts +696 -0
  179. package/src/tests/testUtils.ts +10 -1
package/src/exports.ts CHANGED
@@ -85,6 +85,7 @@ import {
85
85
  import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
86
86
  import {
87
87
  CO_VALUE_LOADING_CONFIG,
88
+ STORAGE_RECONCILIATION_CONFIG,
88
89
  TRANSACTION_CONFIG,
89
90
  WEBSOCKET_CONFIG,
90
91
  setCoValueLoadingMaxRetries,
@@ -94,6 +95,9 @@ import {
94
95
  setMaxInFlightLoadsPerPeer,
95
96
  setMaxOutgoingMessagesChunkBytes,
96
97
  setMaxRecommendedTxSize,
98
+ setStorageReconciliationBatchSize,
99
+ setStorageReconciliationLockTTL,
100
+ setStorageReconciliationInterval,
97
101
  } from "./config.js";
98
102
  import { LogLevel, logger } from "./logger.js";
99
103
  import { CO_VALUE_PRIORITY, getPriorityFromHeader } from "./priority.js";
@@ -147,6 +151,10 @@ export const cojsonInternals = {
147
151
  WEBSOCKET_CONFIG,
148
152
  setMaxOutgoingMessagesChunkBytes,
149
153
  setMaxInFlightLoadsPerPeer,
154
+ STORAGE_RECONCILIATION_CONFIG,
155
+ setStorageReconciliationBatchSize,
156
+ setStorageReconciliationLockTTL,
157
+ setStorageReconciliationInterval,
150
158
  };
151
159
 
152
160
  export {
package/src/localNode.ts CHANGED
@@ -77,10 +77,14 @@ export class LocalNode {
77
77
  currentSessionID: SessionID,
78
78
  crypto: CryptoProvider,
79
79
  public readonly syncWhen?: SyncWhen,
80
+ enableFullStorageReconciliation?: boolean,
80
81
  ) {
81
82
  this.agentSecret = agentSecret;
82
83
  this.currentSessionID = currentSessionID;
83
84
  this.crypto = crypto;
85
+ if (enableFullStorageReconciliation) {
86
+ this.syncManager.fullStorageReconciliationEnabled = true;
87
+ }
84
88
  }
85
89
 
86
90
  enableGarbageCollector() {
@@ -133,6 +137,12 @@ export class LocalNode {
133
137
  );
134
138
  }
135
139
 
140
+ isCoValueInMemory(id: RawCoID) {
141
+ const state = this.coValues.get(id)?.loadingState;
142
+
143
+ return Boolean(state);
144
+ }
145
+
136
146
  getCoValue(id: RawCoID) {
137
147
  let entry = this.coValues.get(id);
138
148
 
@@ -256,6 +266,7 @@ export class LocalNode {
256
266
  peers?: Peer[];
257
267
  syncWhen?: SyncWhen;
258
268
  storage?: StorageAPI;
269
+ enableFullStorageReconciliation?: boolean;
259
270
  }): RawAccount {
260
271
  const {
261
272
  crypto,
@@ -274,6 +285,7 @@ export class LocalNode {
274
285
  crypto.newRandomSessionID(accountID as RawAccountID),
275
286
  crypto,
276
287
  syncWhen,
288
+ opts.enableFullStorageReconciliation,
277
289
  );
278
290
 
279
291
  if (opts.storage) {
@@ -320,6 +332,7 @@ export class LocalNode {
320
332
  crypto,
321
333
  initialAgentSecret = crypto.newRandomAgentSecret(),
322
334
  storage,
335
+ enableFullStorageReconciliation,
323
336
  }: {
324
337
  creationProps: { name: string };
325
338
  peers?: Peer[];
@@ -328,6 +341,7 @@ export class LocalNode {
328
341
  crypto: CryptoProvider;
329
342
  initialAgentSecret?: AgentSecret;
330
343
  storage?: StorageAPI;
344
+ enableFullStorageReconciliation?: boolean;
331
345
  }): Promise<{
332
346
  node: LocalNode;
333
347
  accountID: RawAccountID;
@@ -340,6 +354,7 @@ export class LocalNode {
340
354
  peers,
341
355
  syncWhen,
342
356
  storage,
357
+ enableFullStorageReconciliation,
343
358
  });
344
359
  const node = account.core.node;
345
360
 
@@ -385,6 +400,7 @@ export class LocalNode {
385
400
  crypto,
386
401
  migration,
387
402
  storage,
403
+ enableFullStorageReconciliation,
388
404
  }: {
389
405
  accountID: RawAccountID;
390
406
  accountSecret: AgentSecret;
@@ -394,6 +410,7 @@ export class LocalNode {
394
410
  crypto: CryptoProvider;
395
411
  migration?: RawAccountMigration<AccountMeta>;
396
412
  storage?: StorageAPI;
413
+ enableFullStorageReconciliation?: boolean;
397
414
  }): Promise<LocalNode> {
398
415
  try {
399
416
  const expectedAgentID = crypto.getAgentID(accountSecret);
@@ -403,6 +420,7 @@ export class LocalNode {
403
420
  sessionID || crypto.newRandomSessionID(accountID),
404
421
  crypto,
405
422
  syncWhen,
423
+ enableFullStorageReconciliation,
406
424
  );
407
425
 
408
426
  if (storage) {
@@ -22,7 +22,7 @@ import { expectGroup } from "./typeUtils/expectGroup.js";
22
22
 
23
23
  export type PermissionsDef =
24
24
  | { type: "group"; initialAdmin: RawAccountID | AgentID }
25
- | { type: "ownedByGroup"; group: RawCoID }
25
+ | { type: "ownedByGroup"; group: RawCoID; restrictDeletion?: boolean }
26
26
  | { type: "unsafeAllowAll" };
27
27
 
28
28
  export type AccountRole =
@@ -68,6 +68,10 @@ export class LinkedList<T> {
68
68
  return value;
69
69
  }
70
70
 
71
+ trackPushPull() {
72
+ this.meter?.trackPushPull();
73
+ }
74
+
71
75
  /**
72
76
  * Remove a specific node from the list in O(1) time.
73
77
  * The node must be a valid node that was returned by push().
@@ -136,7 +140,13 @@ class QueueMeter {
136
140
  public push() {
137
141
  this.pushCounter.add(1, this.attrs);
138
142
  }
143
+
144
+ public trackPushPull() {
145
+ this.pullCounter.add(1, this.attrs);
146
+ this.pushCounter.add(1, this.attrs);
147
+ }
139
148
  }
149
+
140
150
  export function meteredList<T>(
141
151
  type: "incoming" | "outgoing" | "storage-streaming" | "load-requests-queue",
142
152
  attrs?: Record<string, string | number>,
@@ -1,3 +1,4 @@
1
+ import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
1
2
  import { CO_VALUE_LOADING_CONFIG } from "../config.js";
2
3
  import { CoValueCore } from "../exports.js";
3
4
  import type { RawCoID } from "../ids.js";
@@ -10,6 +11,11 @@ interface PendingLoad {
10
11
  sendCallback: () => void;
11
12
  }
12
13
 
14
+ interface InFlightLoad {
15
+ value: CoValueCore;
16
+ sentAt: number;
17
+ }
18
+
13
19
  /**
14
20
  * Mode for enqueuing load requests:
15
21
  * - "high-priority" (default): high priority, processed in order
@@ -17,6 +23,7 @@ interface PendingLoad {
17
23
  * - "immediate": bypasses the queue entirely, executes immediately
18
24
  */
19
25
  export type LoadMode = "low-priority" | "immediate" | "high-priority";
26
+ export type LoadCompletionSource = "content" | "known";
20
27
 
21
28
  /**
22
29
  * A queue that manages outgoing load requests with throttling.
@@ -28,7 +35,8 @@ export type LoadMode = "low-priority" | "immediate" | "high-priority";
28
35
  * - Manages timeouts for in-flight loads with a single timer
29
36
  */
30
37
  export class OutgoingLoadQueue {
31
- private inFlightLoads: Map<CoValueCore, number> = new Map();
38
+ private inFlightLoads: Map<RawCoID, InFlightLoad> = new Map();
39
+ private inFlightCounter: UpDownCounter;
32
40
  private highPriorityPending: LinkedList<PendingLoad> = meteredList(
33
41
  "load-requests-queue",
34
42
  { priority: "high" },
@@ -49,7 +57,18 @@ export class OutgoingLoadQueue {
49
57
  new Map();
50
58
  private timeoutHandle: ReturnType<typeof setTimeout> | null = null;
51
59
 
52
- constructor(private peerId: PeerID) {}
60
+ constructor(private peerId: PeerID) {
61
+ this.inFlightCounter = metrics
62
+ .getMeter("cojson")
63
+ .createUpDownCounter("jazz.loadqueue.outgoing.inflight", {
64
+ description: "Number of in-flight outgoing load requests",
65
+ unit: "1",
66
+ valueType: ValueType.INT,
67
+ });
68
+
69
+ // Emit an initial 0 value so the series appears immediately.
70
+ this.inFlightCounter.add(0);
71
+ }
53
72
 
54
73
  /**
55
74
  * Check if we can send another load request.
@@ -66,10 +85,20 @@ export class OutgoingLoadQueue {
66
85
  */
67
86
  private trackSent(coValue: CoValueCore): void {
68
87
  const now = performance.now();
69
- this.inFlightLoads.set(coValue, now);
88
+ this.inFlightLoads.set(coValue.id, { value: coValue, sentAt: now });
89
+ this.inFlightCounter.add(1);
70
90
  this.scheduleTimeoutCheck(CO_VALUE_LOADING_CONFIG.TIMEOUT);
71
91
  }
72
92
 
93
+ private untrackInFlight(id: RawCoID): boolean {
94
+ if (!this.inFlightLoads.delete(id)) {
95
+ return false;
96
+ }
97
+
98
+ this.inFlightCounter.add(-1);
99
+ return true;
100
+ }
101
+
73
102
  /**
74
103
  * Schedule a timeout check if not already scheduled.
75
104
  * Uses a single timer to check all in-flight loads.
@@ -92,7 +121,7 @@ export class OutgoingLoadQueue {
92
121
  const now = performance.now();
93
122
 
94
123
  let nextTimeout: number | undefined;
95
- for (const [coValue, sentAt] of this.inFlightLoads.entries()) {
124
+ for (const { value: coValue, sentAt } of this.inFlightLoads.values()) {
96
125
  const timeout = sentAt + CO_VALUE_LOADING_CONFIG.TIMEOUT;
97
126
 
98
127
  if (now >= timeout) {
@@ -101,7 +130,8 @@ export class OutgoingLoadQueue {
101
130
  id: coValue.id,
102
131
  peerId: this.peerId,
103
132
  });
104
- coValue.markNotFoundInPeer(this.peerId);
133
+ // Re-resolve by ID to avoid mutating a stale CoValue instance.
134
+ coValue.node.getCoValue(coValue.id).markNotFoundInPeer(this.peerId);
105
135
  } else if (coValue.isStreaming()) {
106
136
  logger.warn(
107
137
  "Content streaming is taking more than " +
@@ -116,8 +146,9 @@ export class OutgoingLoadQueue {
116
146
  );
117
147
  }
118
148
 
119
- this.inFlightLoads.delete(coValue);
120
- this.processQueue();
149
+ if (this.untrackInFlight(coValue.id)) {
150
+ this.processQueue();
151
+ }
121
152
  } else {
122
153
  nextTimeout = Math.min(nextTimeout ?? Infinity, timeout - now);
123
154
  }
@@ -130,30 +161,37 @@ export class OutgoingLoadQueue {
130
161
  }
131
162
 
132
163
  trackUpdate(coValue: CoValueCore): void {
133
- if (!this.inFlightLoads.has(coValue)) {
164
+ if (!this.inFlightLoads.has(coValue.id)) {
134
165
  return;
135
166
  }
136
167
 
137
168
  // Refresh the timeout for the in-flight load
138
- this.inFlightLoads.set(coValue, performance.now());
169
+ this.inFlightLoads.set(coValue.id, {
170
+ value: coValue,
171
+ sentAt: performance.now(),
172
+ });
139
173
  }
140
174
 
141
175
  /**
142
176
  * Track that a load request has completed.
143
177
  * Triggers processing of pending requests.
144
178
  */
145
- trackComplete(coValue: CoValueCore): void {
146
- if (!this.inFlightLoads.has(coValue)) {
179
+ trackComplete(
180
+ coValue: CoValueCore,
181
+ source: LoadCompletionSource = "content",
182
+ ): void {
183
+ if (!this.inFlightLoads.has(coValue.id)) {
147
184
  return;
148
185
  }
149
186
 
150
- if (coValue.isStreaming()) {
187
+ if (source === "content" && coValue.isStreaming()) {
151
188
  // wait for the next chunk
152
189
  return;
153
190
  }
154
191
 
155
- this.inFlightLoads.delete(coValue);
156
- this.processQueue();
192
+ if (this.untrackInFlight(coValue.id)) {
193
+ this.processQueue();
194
+ }
157
195
  }
158
196
 
159
197
  /**
@@ -170,7 +208,7 @@ export class OutgoingLoadQueue {
170
208
  sendCallback: () => void,
171
209
  mode: LoadMode = "high-priority",
172
210
  ): void {
173
- if (this.inFlightLoads.has(value)) {
211
+ if (this.inFlightLoads.has(value.id)) {
174
212
  return;
175
213
  }
176
214
 
@@ -270,7 +308,11 @@ export class OutgoingLoadQueue {
270
308
  clearTimeout(this.timeoutHandle);
271
309
  this.timeoutHandle = null;
272
310
  }
311
+ const inFlightCount = this.inFlightLoads.size;
273
312
  this.inFlightLoads.clear();
313
+ if (inFlightCount > 0) {
314
+ this.inFlightCounter.add(-inFlightCount);
315
+ }
274
316
 
275
317
  // Drain existing queues to balance push/pull metrics
276
318
  while (this.highPriorityPending.shift()) {}
@@ -32,7 +32,11 @@ export class PriorityBasedMessageQueue {
32
32
  }
33
33
 
34
34
  public push(msg: SyncMessage) {
35
- const priority = "priority" in msg ? msg.priority : this.defaultPriority;
35
+ let priority = "priority" in msg ? msg.priority : this.defaultPriority;
36
+
37
+ if (msg.action === "reconcile") {
38
+ priority = CO_VALUE_PRIORITY.LOW;
39
+ }
36
40
 
37
41
  this.getQueue(priority).push(msg);
38
42
  }
@@ -42,4 +46,14 @@ export class PriorityBasedMessageQueue {
42
46
 
43
47
  return this.queues[priority]?.shift();
44
48
  }
49
+
50
+ public trackPushPull(msg: SyncMessage) {
51
+ let priority = "priority" in msg ? msg.priority : this.defaultPriority;
52
+
53
+ if (msg.action === "reconcile") {
54
+ priority = CO_VALUE_PRIORITY.LOW;
55
+ }
56
+
57
+ this.getQueue(priority).trackPushPull();
58
+ }
45
59
  }
@@ -89,6 +89,20 @@ export class StorageKnownState {
89
89
  requests.add(req);
90
90
  });
91
91
  }
92
+
93
+ deleteKnownState(id: string) {
94
+ this.knownStates.delete(id);
95
+
96
+ for (const request of this.waitForSyncRequests.get(id) || []) {
97
+ request.resolve();
98
+ }
99
+ this.waitForSyncRequests.delete(id);
100
+ }
101
+
102
+ clear() {
103
+ this.knownStates.clear();
104
+ this.waitForSyncRequests.clear();
105
+ }
92
106
  }
93
107
 
94
108
  function isInSync(
@@ -12,12 +12,15 @@ import type {
12
12
  DBTransactionInterfaceSync,
13
13
  SessionRow,
14
14
  SignatureAfterRow,
15
+ StorageReconciliationLockRow,
15
16
  StoredCoValueRow,
16
17
  StoredSessionRow,
17
18
  TransactionRow,
19
+ StorageReconciliationAcquireResult,
18
20
  } from "../types.js";
19
21
  import { DeletedCoValueDeletionStatus } from "../types.js";
20
22
  import type { SQLiteDatabaseDriver } from "./types.js";
23
+ import { STORAGE_RECONCILIATION_CONFIG } from "../../config.js";
21
24
 
22
25
  export type RawCoValueRow = {
23
26
  id: RawCoID;
@@ -268,11 +271,55 @@ export class SQLiteClient
268
271
  );
269
272
  }
270
273
 
274
+ getStorageReconciliationLock(
275
+ key: string,
276
+ ): StorageReconciliationLockRow | undefined {
277
+ return this.db.get<StorageReconciliationLockRow>(
278
+ "SELECT * FROM storageReconciliationLocks WHERE key = ?",
279
+ [key],
280
+ );
281
+ }
282
+
283
+ putStorageReconciliationLock(entry: StorageReconciliationLockRow): void {
284
+ const {
285
+ key,
286
+ holderSessionId,
287
+ acquiredAt,
288
+ releasedAt,
289
+ lastProcessedOffset,
290
+ } = entry;
291
+ this.db.run(
292
+ `INSERT OR REPLACE INTO storageReconciliationLocks (key, holderSessionId, acquiredAt, releasedAt, lastProcessedOffset) VALUES (?, ?, ?, ?, ?)`,
293
+ [
294
+ key,
295
+ holderSessionId,
296
+ acquiredAt,
297
+ releasedAt ?? null,
298
+ lastProcessedOffset,
299
+ ],
300
+ );
301
+ }
302
+
271
303
  transaction(operationsCallback: (tx: DBTransactionInterfaceSync) => unknown) {
272
304
  this.db.transaction(() => operationsCallback(this));
273
305
  return undefined;
274
306
  }
275
307
 
308
+ getCoValueIDs(limit: number, offset: number): { id: RawCoID }[] {
309
+ return this.db.query<{ id: RawCoID }>(
310
+ "SELECT id FROM coValues WHERE rowID > ? ORDER BY rowID LIMIT ?",
311
+ [offset, limit],
312
+ );
313
+ }
314
+
315
+ getCoValueCount(): number {
316
+ const row = this.db.get<{ count: number }>(
317
+ "SELECT COUNT(*) as count FROM coValues",
318
+ [],
319
+ );
320
+ return row?.count ?? 0;
321
+ }
322
+
276
323
  getUnsyncedCoValueIDs(): RawCoID[] {
277
324
  const rows = this.db.query<{ co_value_id: RawCoID }>(
278
325
  "SELECT DISTINCT co_value_id FROM unsynced_covalues",
@@ -303,6 +350,87 @@ export class SQLiteClient
303
350
  this.db.run("DELETE FROM unsynced_covalues WHERE co_value_id = ?", [id]);
304
351
  }
305
352
 
353
+ tryAcquireStorageReconciliationLock(
354
+ sessionId: SessionID,
355
+ peerId: PeerID,
356
+ ): StorageReconciliationAcquireResult {
357
+ let result: StorageReconciliationAcquireResult = {
358
+ acquired: false,
359
+ reason: "not_due",
360
+ };
361
+ this.transaction(() => {
362
+ const now = Date.now();
363
+ const lockKey = `lock#${peerId}`;
364
+ const lockRow = this.getStorageReconciliationLock(lockKey);
365
+ if (
366
+ lockRow?.releasedAt &&
367
+ now - lockRow.releasedAt <
368
+ STORAGE_RECONCILIATION_CONFIG.RECONCILIATION_INTERVAL_MS
369
+ ) {
370
+ result = { acquired: false, reason: "not_due" };
371
+ return;
372
+ }
373
+ const expiresAt = lockRow
374
+ ? lockRow.acquiredAt + STORAGE_RECONCILIATION_CONFIG.LOCK_TTL_MS
375
+ : 0;
376
+ const isLockHeldByOtherSession = lockRow?.holderSessionId !== sessionId;
377
+ if (
378
+ lockRow &&
379
+ !lockRow.releasedAt &&
380
+ expiresAt >= now &&
381
+ isLockHeldByOtherSession
382
+ ) {
383
+ result = { acquired: false, reason: "lock_held" };
384
+ return;
385
+ }
386
+
387
+ const lastProcessedOffset =
388
+ lockRow && !lockRow.releasedAt ? (lockRow.lastProcessedOffset ?? 0) : 0;
389
+ this.putStorageReconciliationLock({
390
+ key: lockKey,
391
+ holderSessionId: sessionId,
392
+ acquiredAt: now,
393
+ lastProcessedOffset,
394
+ });
395
+ result = { acquired: true, lastProcessedOffset };
396
+ });
397
+ return result;
398
+ }
399
+
400
+ renewStorageReconciliationLock(
401
+ sessionId: SessionID,
402
+ peerId: PeerID,
403
+ offset: number,
404
+ ): void {
405
+ const lockKey = `lock#${peerId}`;
406
+ const lockRow = this.getStorageReconciliationLock(lockKey);
407
+ if (
408
+ lockRow &&
409
+ lockRow.holderSessionId === sessionId &&
410
+ !lockRow.releasedAt
411
+ ) {
412
+ this.putStorageReconciliationLock({
413
+ ...lockRow,
414
+ lastProcessedOffset: offset,
415
+ });
416
+ }
417
+ }
418
+
419
+ releaseStorageReconciliationLock(sessionId: SessionID, peerId: PeerID): void {
420
+ this.transaction(() => {
421
+ const lockKey = `lock#${peerId}`;
422
+ const releasedAt = Date.now();
423
+ const lockRow = this.getStorageReconciliationLock(lockKey);
424
+ if (lockRow?.holderSessionId === sessionId) {
425
+ this.putStorageReconciliationLock({
426
+ ...lockRow,
427
+ releasedAt,
428
+ lastProcessedOffset: 0,
429
+ });
430
+ }
431
+ });
432
+ }
433
+
306
434
  getCoValueKnownState(coValueId: string): CoValueKnownState | undefined {
307
435
  // First check if the CoValue exists
308
436
  const coValueRow = this.db.get<{ rowID: number }>(
@@ -47,6 +47,17 @@ export const migrations: Record<number, string[]> = {
47
47
  ) WITHOUT ROWID;`,
48
48
  "CREATE INDEX IF NOT EXISTS deletedCoValuesByStatus ON deletedCoValues (status);",
49
49
  ],
50
+ 6: [
51
+ `CREATE TABLE IF NOT EXISTS storageReconciliationLocks (
52
+ key TEXT PRIMARY KEY,
53
+ holderSessionId TEXT NOT NULL,
54
+ acquiredAt INTEGER NOT NULL,
55
+ expiresAt INTEGER NOT NULL,
56
+ lastProcessedOffset INTEGER NOT NULL DEFAULT 0,
57
+ releasedAt INTEGER
58
+ ) WITHOUT ROWID;`,
59
+ ],
60
+ 7: ["ALTER TABLE storageReconciliationLocks DROP COLUMN expiresAt;"],
50
61
  };
51
62
 
52
63
  type Migration = {