cojson 0.20.9 → 0.20.10

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 (169) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/PeerState.d.ts +2 -2
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +3 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/StorageReconciliationAckTracker.d.ts +14 -0
  8. package/dist/StorageReconciliationAckTracker.d.ts.map +1 -0
  9. package/dist/StorageReconciliationAckTracker.js +72 -0
  10. package/dist/StorageReconciliationAckTracker.js.map +1 -0
  11. package/dist/SyncStateManager.js +2 -2
  12. package/dist/SyncStateManager.js.map +1 -1
  13. package/dist/coValueCore/coValueCore.d.ts +2 -1
  14. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  15. package/dist/coValueCore/coValueCore.js +43 -10
  16. package/dist/coValueCore/coValueCore.js.map +1 -1
  17. package/dist/coValues/coList.d.ts +2 -0
  18. package/dist/coValues/coList.d.ts.map +1 -1
  19. package/dist/coValues/coList.js +28 -0
  20. package/dist/coValues/coList.js.map +1 -1
  21. package/dist/coValues/group.d.ts +4 -1
  22. package/dist/coValues/group.d.ts.map +1 -1
  23. package/dist/coValues/group.js +15 -1
  24. package/dist/coValues/group.js.map +1 -1
  25. package/dist/config.d.ts +8 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +14 -0
  28. package/dist/config.js.map +1 -1
  29. package/dist/exports.d.ts +9 -1
  30. package/dist/exports.d.ts.map +1 -1
  31. package/dist/exports.js +5 -1
  32. package/dist/exports.js.map +1 -1
  33. package/dist/localNode.d.ts +7 -3
  34. package/dist/localNode.d.ts.map +1 -1
  35. package/dist/localNode.js +13 -5
  36. package/dist/localNode.js.map +1 -1
  37. package/dist/permissions.d.ts +1 -0
  38. package/dist/permissions.d.ts.map +1 -1
  39. package/dist/queue/LinkedList.d.ts +2 -0
  40. package/dist/queue/LinkedList.d.ts.map +1 -1
  41. package/dist/queue/LinkedList.js +7 -0
  42. package/dist/queue/LinkedList.js.map +1 -1
  43. package/dist/queue/OutgoingLoadQueue.d.ts +4 -1
  44. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -1
  45. package/dist/queue/OutgoingLoadQueue.js +41 -13
  46. package/dist/queue/OutgoingLoadQueue.js.map +1 -1
  47. package/dist/queue/PriorityBasedMessageQueue.d.ts +1 -0
  48. package/dist/queue/PriorityBasedMessageQueue.d.ts.map +1 -1
  49. package/dist/queue/PriorityBasedMessageQueue.js +11 -1
  50. package/dist/queue/PriorityBasedMessageQueue.js.map +1 -1
  51. package/dist/storage/knownState.d.ts +2 -0
  52. package/dist/storage/knownState.d.ts.map +1 -1
  53. package/dist/storage/knownState.js +11 -0
  54. package/dist/storage/knownState.js.map +1 -1
  55. package/dist/storage/sqlite/client.d.ts +10 -1
  56. package/dist/storage/sqlite/client.d.ts.map +1 -1
  57. package/dist/storage/sqlite/client.js +84 -0
  58. package/dist/storage/sqlite/client.js.map +1 -1
  59. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  60. package/dist/storage/sqlite/sqliteMigrations.js +11 -0
  61. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  62. package/dist/storage/sqliteAsync/client.d.ts +10 -1
  63. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  64. package/dist/storage/sqliteAsync/client.js +86 -0
  65. package/dist/storage/sqliteAsync/client.js.map +1 -1
  66. package/dist/storage/storageAsync.d.ts +9 -2
  67. package/dist/storage/storageAsync.d.ts.map +1 -1
  68. package/dist/storage/storageAsync.js +19 -0
  69. package/dist/storage/storageAsync.js.map +1 -1
  70. package/dist/storage/storageSync.d.ts +9 -2
  71. package/dist/storage/storageSync.d.ts.map +1 -1
  72. package/dist/storage/storageSync.js +20 -13
  73. package/dist/storage/storageSync.js.map +1 -1
  74. package/dist/storage/types.d.ts +64 -0
  75. package/dist/storage/types.d.ts.map +1 -1
  76. package/dist/storage/types.js.map +1 -1
  77. package/dist/sync.d.ts +44 -2
  78. package/dist/sync.d.ts.map +1 -1
  79. package/dist/sync.js +268 -44
  80. package/dist/sync.js.map +1 -1
  81. package/dist/tests/OutgoingLoadQueue.test.js +137 -39
  82. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -1
  83. package/dist/tests/SQLiteClientAsync.test.js +1 -1
  84. package/dist/tests/SQLiteClientAsync.test.js.map +1 -1
  85. package/dist/tests/StorageApiAsync.test.js +138 -0
  86. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  87. package/dist/tests/StorageApiSync.test.js +154 -0
  88. package/dist/tests/StorageApiSync.test.js.map +1 -1
  89. package/dist/tests/StorageReconciliationAckTracker.test.d.ts +2 -0
  90. package/dist/tests/StorageReconciliationAckTracker.test.d.ts.map +1 -0
  91. package/dist/tests/StorageReconciliationAckTracker.test.js +74 -0
  92. package/dist/tests/StorageReconciliationAckTracker.test.js.map +1 -0
  93. package/dist/tests/SyncStateManager.test.js +18 -0
  94. package/dist/tests/SyncStateManager.test.js.map +1 -1
  95. package/dist/tests/coList.test.js +112 -1
  96. package/dist/tests/coList.test.js.map +1 -1
  97. package/dist/tests/coValueCore.loadFromStorage.test.js +36 -0
  98. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  99. package/dist/tests/group.test.js +44 -0
  100. package/dist/tests/group.test.js.map +1 -1
  101. package/dist/tests/knownState.lazyLoading.test.js +6 -0
  102. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  103. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  104. package/dist/tests/messagesTestUtils.js +4 -0
  105. package/dist/tests/messagesTestUtils.js.map +1 -1
  106. package/dist/tests/sync.concurrentLoad.test.js +333 -1
  107. package/dist/tests/sync.concurrentLoad.test.js.map +1 -1
  108. package/dist/tests/sync.garbageCollection.test.js +4 -0
  109. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  110. package/dist/tests/sync.load.test.js +19 -0
  111. package/dist/tests/sync.load.test.js.map +1 -1
  112. package/dist/tests/sync.mesh.test.js +1 -0
  113. package/dist/tests/sync.mesh.test.js.map +1 -1
  114. package/dist/tests/sync.multipleServers.test.js +41 -3
  115. package/dist/tests/sync.multipleServers.test.js.map +1 -1
  116. package/dist/tests/sync.storage.test.js +2 -0
  117. package/dist/tests/sync.storage.test.js.map +1 -1
  118. package/dist/tests/sync.storageAsync.test.js +1 -0
  119. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  120. package/dist/tests/sync.storageReconciliation.test.d.ts +2 -0
  121. package/dist/tests/sync.storageReconciliation.test.d.ts.map +1 -0
  122. package/dist/tests/sync.storageReconciliation.test.js +501 -0
  123. package/dist/tests/sync.storageReconciliation.test.js.map +1 -0
  124. package/dist/tests/testUtils.d.ts +1 -0
  125. package/dist/tests/testUtils.d.ts.map +1 -1
  126. package/dist/tests/testUtils.js +3 -2
  127. package/dist/tests/testUtils.js.map +1 -1
  128. package/package.json +4 -4
  129. package/src/PeerState.ts +10 -3
  130. package/src/StorageReconciliationAckTracker.ts +83 -0
  131. package/src/SyncStateManager.ts +3 -3
  132. package/src/coValueCore/coValueCore.ts +47 -16
  133. package/src/coValues/coList.ts +23 -0
  134. package/src/coValues/group.ts +18 -0
  135. package/src/config.ts +18 -0
  136. package/src/exports.ts +8 -0
  137. package/src/localNode.ts +18 -0
  138. package/src/permissions.ts +1 -1
  139. package/src/queue/LinkedList.ts +10 -0
  140. package/src/queue/OutgoingLoadQueue.ts +57 -15
  141. package/src/queue/PriorityBasedMessageQueue.ts +15 -1
  142. package/src/storage/knownState.ts +14 -0
  143. package/src/storage/sqlite/client.ts +128 -0
  144. package/src/storage/sqlite/sqliteMigrations.ts +11 -0
  145. package/src/storage/sqliteAsync/client.ts +139 -0
  146. package/src/storage/storageAsync.ts +37 -0
  147. package/src/storage/storageSync.ts +41 -16
  148. package/src/storage/types.ts +110 -0
  149. package/src/sync.ts +311 -14
  150. package/src/tests/OutgoingLoadQueue.test.ts +226 -59
  151. package/src/tests/SQLiteClientAsync.test.ts +1 -1
  152. package/src/tests/StorageApiAsync.test.ts +161 -1
  153. package/src/tests/StorageApiSync.test.ts +176 -0
  154. package/src/tests/StorageReconciliationAckTracker.test.ts +99 -0
  155. package/src/tests/SyncStateManager.test.ts +25 -0
  156. package/src/tests/coList.test.ts +138 -0
  157. package/src/tests/coValueCore.loadFromStorage.test.ts +72 -1
  158. package/src/tests/group.test.ts +87 -0
  159. package/src/tests/knownState.lazyLoading.test.ts +36 -1
  160. package/src/tests/messagesTestUtils.ts +4 -0
  161. package/src/tests/sync.concurrentLoad.test.ts +491 -0
  162. package/src/tests/sync.garbageCollection.test.ts +4 -0
  163. package/src/tests/sync.load.test.ts +26 -0
  164. package/src/tests/sync.mesh.test.ts +1 -0
  165. package/src/tests/sync.multipleServers.test.ts +60 -2
  166. package/src/tests/sync.storage.test.ts +2 -0
  167. package/src/tests/sync.storageAsync.test.ts +1 -0
  168. package/src/tests/sync.storageReconciliation.test.ts +697 -0
  169. package/src/tests/testUtils.ts +10 -1
package/src/sync.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import { base58 } from "@scure/base";
1
2
  import { md5 } from "@noble/hashes/legacy";
2
3
  import { Histogram, ValueType, metrics } from "@opentelemetry/api";
3
4
  import { PeerState } from "./PeerState.js";
4
5
  import { SyncStateManager } from "./SyncStateManager.js";
5
6
  import { UnsyncedCoValuesTracker } from "./UnsyncedCoValuesTracker.js";
6
- import { SYNC_SCHEDULER_CONFIG } from "./config.js";
7
+ import {
8
+ STORAGE_RECONCILIATION_CONFIG,
9
+ SYNC_SCHEDULER_CONFIG,
10
+ } from "./config.js";
7
11
  import {
8
12
  getContenDebugInfo,
9
13
  getNewTransactionsFromContentMessage,
@@ -21,6 +25,7 @@ import { CoValuePriority } from "./priority.js";
21
25
  import { IncomingMessagesQueue } from "./queue/IncomingMessagesQueue.js";
22
26
  import { LocalTransactionsSyncQueue } from "./queue/LocalTransactionsSyncQueue.js";
23
27
  import type { StorageStreamingQueue } from "./queue/StorageStreamingQueue.js";
28
+ import { StorageReconciliationAckTracker } from "./StorageReconciliationAckTracker.js";
24
29
  import {
25
30
  CoValueKnownState,
26
31
  knownStateFrom,
@@ -33,7 +38,9 @@ export type SyncMessage =
33
38
  | LoadMessage
34
39
  | KnownStateMessage
35
40
  | NewContentMessage
36
- | DoneMessage;
41
+ | DoneMessage
42
+ | ReconcileMessage
43
+ | ReconcileAckMessage;
37
44
 
38
45
  export type LoadMessage = {
39
46
  action: "load";
@@ -68,6 +75,17 @@ export type DoneMessage = {
68
75
  id: RawCoID;
69
76
  };
70
77
 
78
+ export type ReconcileMessage = {
79
+ action: "reconcile";
80
+ id: string;
81
+ values: [coValue: RawCoID, sessionsHash: string][];
82
+ };
83
+
84
+ export type ReconcileAckMessage = {
85
+ action: "reconcile-ack";
86
+ id: string;
87
+ };
88
+
71
89
  /**
72
90
  * Determines when network sync is enabled.
73
91
  * - "always": sync is enabled for both Anonymous Authentication and Authenticated Account
@@ -111,9 +129,20 @@ export type ServerPeerSelector = (
111
129
  serverPeers: PeerState[],
112
130
  ) => PeerState[];
113
131
 
132
+ /**
133
+ * Manages the sync of coValues between peers.
134
+ * It is responsible for sending, receiving and processing sync messages.
135
+ * For more details on how the sync protocol works, see the sync protocol documentation:
136
+ * {@link docs/sync-protocol.md}
137
+ */
114
138
  export class SyncManager {
115
139
  peers: { [key: PeerID]: PeerState } = {};
116
140
  local: LocalNode;
141
+ private reconciliationAckTracker = new StorageReconciliationAckTracker();
142
+
143
+ get pendingReconciliationAck(): Map<string, number> {
144
+ return this.reconciliationAckTracker.pendingReconciliationAck;
145
+ }
117
146
 
118
147
  // When true, transactions will not be verified.
119
148
  // This is useful when syncing only for storage purposes, with the expectation that
@@ -127,6 +156,8 @@ export class SyncManager {
127
156
  this._ignoreUnknownCoValuesFromServers = true;
128
157
  }
129
158
 
159
+ fullStorageReconciliationEnabled = false;
160
+
130
161
  peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
131
162
  description: "Amount of connected peers",
132
163
  valueType: ValueType.INT,
@@ -179,6 +210,15 @@ export class SyncManager {
179
210
  }
180
211
 
181
212
  handleSyncMessage(msg: SyncMessage, peer: PeerState) {
213
+ if (msg.action === "reconcile") {
214
+ this.handleReconcile(msg, peer);
215
+ return;
216
+ }
217
+ if (msg.action === "reconcile-ack") {
218
+ this.handleReconcileAck(msg, peer);
219
+ return;
220
+ }
221
+
182
222
  if (!isRawCoID(msg.id)) {
183
223
  const errorType = msg.id ? "invalid" : "undefined";
184
224
  logger.warn(`Received sync message with ${errorType} id`, {
@@ -233,7 +273,20 @@ export class SyncManager {
233
273
  }
234
274
  }
235
275
 
236
- sendNewContent(id: RawCoID, peer: PeerState, seen: Set<RawCoID> = new Set()) {
276
+ sendNewContent(
277
+ id: RawCoID,
278
+ peer: PeerState,
279
+ forceKnownReplyOnNoDelta: boolean = false,
280
+ ) {
281
+ this.#sendNewContent(id, peer, new Set(), forceKnownReplyOnNoDelta);
282
+ }
283
+
284
+ #sendNewContent(
285
+ id: RawCoID,
286
+ peer: PeerState,
287
+ seen: Set<RawCoID>,
288
+ forceKnownReplyOnNoDelta: boolean,
289
+ ) {
237
290
  if (seen.has(id)) {
238
291
  return;
239
292
  }
@@ -249,7 +302,7 @@ export class SyncManager {
249
302
  const includeDependencies = peer.role !== "server";
250
303
  if (includeDependencies) {
251
304
  for (const dependency of coValue.getDependedOnCoValues()) {
252
- this.sendNewContent(dependency, peer, seen);
305
+ this.#sendNewContent(dependency, peer, seen, false);
253
306
  }
254
307
  }
255
308
 
@@ -263,7 +316,7 @@ export class SyncManager {
263
316
  }
264
317
 
265
318
  peer.combineOptimisticWith(id, coValue.knownState());
266
- } else if (!peer.toldKnownState.has(id)) {
319
+ } else if (forceKnownReplyOnNoDelta || !peer.toldKnownState.has(id)) {
267
320
  if (coValue.isDeleted) {
268
321
  // This way we make the peer believe that we've always ingested all the content they sent, even though we skipped it because the coValue is deleted
269
322
  this.trySendToPeer(
@@ -281,15 +334,172 @@ export class SyncManager {
281
334
  peer.trackToldKnownState(id);
282
335
  }
283
336
 
337
+ /**
338
+ * Reconciles all in-memory CoValues with all persistent server peers
339
+ */
284
340
  reconcileServerPeers() {
285
341
  const serverPeers = Object.values(this.peers).filter(
286
- (peer) => peer.role === "server",
342
+ isPersistentServerPeer,
287
343
  );
288
344
  for (const peer of serverPeers) {
289
345
  this.startPeerReconciliation(peer);
290
346
  }
291
347
  }
292
348
 
349
+ /**
350
+ * Ensures all CoValues in storage are synced to the given server peer.
351
+ * Sends "reconcile" message(s) with [coValueId, sessionsHash] for each CoValue.
352
+ * Server responds with "known" only where it is missing the CoValue or has different sessions,
353
+ * so that client can send missing content.
354
+ * Processes CoValues in batches of RECONCILIATION_BATCH_SIZE.
355
+ * @param peer - The server peer to reconcile with.
356
+ * @param initialOffset - Offset to start from (for resuming after interrupt). Default 0.
357
+ * @param onComplete - Called when reconciliation is fully complete (all batches sent and acked).
358
+ */
359
+ startStorageReconciliation(
360
+ peer: PeerState,
361
+ initialOffset?: number,
362
+ onComplete?: () => void,
363
+ ): void {
364
+ if (!this.local.storage) return;
365
+ if (!isPersistentServerPeer(peer)) return;
366
+
367
+ const startOffset = initialOffset ?? 0;
368
+ const batchSize = STORAGE_RECONCILIATION_CONFIG.BATCH_SIZE;
369
+
370
+ const storage = this.local.storage;
371
+
372
+ storage.getCoValueCount((totalCoValueCount) => {
373
+ const sendReconcileMessage = (
374
+ batchId: string,
375
+ entries: [RawCoID, string][],
376
+ offset: number,
377
+ ) => {
378
+ if (entries.length === 0) return;
379
+
380
+ this.reconciliationAckTracker.trackBatch(
381
+ batchId,
382
+ peer.id,
383
+ offset + batchSize,
384
+ );
385
+
386
+ this.trySendToPeer(peer, {
387
+ action: "reconcile",
388
+ id: batchId,
389
+ values: entries,
390
+ });
391
+ };
392
+
393
+ const triggerNextBatch = (lastBatchLength: number, offset: number) => {
394
+ // This value becomes false when the last covalueid batch picked from the storage
395
+ // is smaller than the batch size.
396
+ if (lastBatchLength === batchSize) {
397
+ logger.info("Reconciled CoValues in storage", {
398
+ peerId: peer.id,
399
+ completed: offset + batchSize,
400
+ total: totalCoValueCount,
401
+ });
402
+ processStorageBatch(offset + batchSize);
403
+ } else {
404
+ // Note: `completed` can be higher than `total` if CoValues were added
405
+ // after the reconciliation started
406
+ logger.info("Storage reconciliation complete", {
407
+ peerId: peer.id,
408
+ startOffset,
409
+ completed: offset + lastBatchLength,
410
+ total: totalCoValueCount,
411
+ });
412
+ onComplete?.();
413
+ }
414
+ };
415
+
416
+ const processStorageBatch = (offset: number) => {
417
+ storage.getCoValueIDs(batchSize, offset, (batch) => {
418
+ this.buildStorageReconciliationEntries(batch, (entries) => {
419
+ if (entries.length === 0) {
420
+ triggerNextBatch(batch.length, offset);
421
+ return;
422
+ }
423
+
424
+ const batchId = base58.encode(this.local.crypto.randomBytes(12));
425
+ sendReconcileMessage(batchId, entries, offset);
426
+
427
+ this.reconciliationAckTracker.waitForAck(batchId, peer, () => {
428
+ triggerNextBatch(batch.length, offset);
429
+ });
430
+ });
431
+ });
432
+ };
433
+
434
+ logger.info("Starting storage reconciliation", {
435
+ peerId: peer.id,
436
+ startOffset,
437
+ total: totalCoValueCount,
438
+ });
439
+ processStorageBatch(startOffset);
440
+ });
441
+ }
442
+
443
+ private buildStorageReconciliationEntries(
444
+ batch: { id: RawCoID }[],
445
+ callback: (entries: [RawCoID, string][]) => void,
446
+ ): void {
447
+ const storage = this.local.storage;
448
+
449
+ if (!storage) {
450
+ callback([]);
451
+ return;
452
+ }
453
+
454
+ const pending = batch.filter(({ id }) => !this.local.isCoValueInMemory(id));
455
+
456
+ if (pending.length === 0) {
457
+ callback([]);
458
+ return;
459
+ }
460
+
461
+ let done = 0;
462
+ const entries: [RawCoID, string][] = [];
463
+
464
+ for (const coValue of pending) {
465
+ storage.loadKnownState(coValue.id, (storageKnownState) => {
466
+ if (storageKnownState) {
467
+ entries.push([
468
+ coValue.id,
469
+ this.hashKnownStateSessions(storageKnownState.sessions),
470
+ ]);
471
+ }
472
+
473
+ done += 1;
474
+ if (done === pending.length) {
475
+ callback(entries);
476
+ }
477
+ });
478
+ }
479
+ }
480
+
481
+ private maybeStartStorageReconciliationForPeer(peer: PeerState): void {
482
+ if (!this.fullStorageReconciliationEnabled) return;
483
+ if (!this.local.storage) return;
484
+
485
+ const sessionId = this.local.currentSessionID;
486
+ this.local.storage.tryAcquireStorageReconciliationLock(
487
+ sessionId,
488
+ peer.id,
489
+ (result) => {
490
+ if (!result.acquired) return;
491
+
492
+ const lastProcessedOffset = result.lastProcessedOffset;
493
+ this.startStorageReconciliation(peer, lastProcessedOffset, () => {
494
+ this.local.storage?.releaseStorageReconciliationLock(
495
+ sessionId,
496
+ peer.id,
497
+ );
498
+ });
499
+ },
500
+ );
501
+ }
502
+
293
503
  async resumeUnsyncedCoValues(): Promise<void> {
294
504
  if (!this.local.storage) {
295
505
  // No storage available, skip resumption
@@ -364,12 +574,19 @@ export class SyncManager {
364
574
  });
365
575
  }
366
576
 
577
+ /**
578
+ * Reconciles all in-memory CoValues with the given peer.
579
+ * Creates a subscription for each CoValue that is not already subscribed to.
580
+ */
367
581
  startPeerReconciliation(peer: PeerState) {
368
582
  if (isPersistentServerPeer(peer)) {
369
583
  // Resume syncing unsynced CoValues asynchronously
370
584
  this.resumeUnsyncedCoValues().catch((error) => {
371
585
  logger.warn("Failed to resume unsynced CoValues:", error);
372
586
  });
587
+
588
+ // Try to run full storage reconciliation for this peer (scheduled per peer, every 30 days)
589
+ this.maybeStartStorageReconciliationForPeer(peer);
373
590
  }
374
591
 
375
592
  const coValuesOrderedByDependency: CoValueCore[] = [];
@@ -592,7 +809,7 @@ export class SyncManager {
592
809
 
593
810
  // Fast path: CoValue is already in memory
594
811
  if (coValue.isAvailable()) {
595
- this.sendNewContent(msg.id, peer);
812
+ this.sendNewContent(msg.id, peer, true);
596
813
  return;
597
814
  }
598
815
 
@@ -608,7 +825,7 @@ export class SyncManager {
608
825
  coValue.getKnownStateFromStorage((storageKnownState) => {
609
826
  // Race condition: CoValue might have been loaded while we were waiting for storage
610
827
  if (coValue.isAvailable()) {
611
- this.sendNewContent(msg.id, peer);
828
+ this.sendNewContent(msg.id, peer, true);
612
829
  return;
613
830
  }
614
831
 
@@ -631,7 +848,7 @@ export class SyncManager {
631
848
  // Even though we responded with KNOWN (client has everything), we need
632
849
  // to establish a subscription so that updates from core flow to us.
633
850
  const serverPeers = this.getServerPeers(msg.id, peer.id);
634
- coValue.loadFromPeers(serverPeers);
851
+ coValue.loadFromPeers(serverPeers, "low-priority");
635
852
 
636
853
  return;
637
854
  }
@@ -652,7 +869,7 @@ export class SyncManager {
652
869
  ) {
653
870
  coValue.loadFromStorage((found) => {
654
871
  if (found && coValue.isAvailable()) {
655
- this.sendNewContent(id, peer);
872
+ this.sendNewContent(id, peer, true);
656
873
  } else {
657
874
  this.loadFromPeersAndRespond(id, peer, coValue);
658
875
  }
@@ -668,7 +885,7 @@ export class SyncManager {
668
885
  coValue: CoValueCore,
669
886
  ) {
670
887
  const peers = this.getServerPeers(id, peer.id);
671
- coValue.loadFromPeers(peers);
888
+ coValue.loadFromPeers(peers, "immediate");
672
889
 
673
890
  const handleLoadResult = () => {
674
891
  if (coValue.isAvailable()) {
@@ -734,9 +951,89 @@ export class SyncManager {
734
951
 
735
952
  if (coValue.isAvailable()) {
736
953
  this.sendNewContent(msg.id, peer);
954
+ } else if (coValue.isKnownStateAvailable()) {
955
+ // Validate if content is missing before loading it from storage
956
+ if (!this.syncState.isSynced(peer, msg.id)) {
957
+ this.local.loadCoValueCore(msg.id).then(() => {
958
+ this.sendNewContent(msg.id, peer);
959
+ });
960
+ }
737
961
  }
738
962
 
739
- peer.trackLoadRequestComplete(coValue);
963
+ peer.trackLoadRequestComplete(coValue, "known");
964
+ }
965
+
966
+ handleReconcile(msg: ReconcileMessage, peer: PeerState): void {
967
+ let pending = msg.values.length;
968
+ const sendAckWhenDone = () => {
969
+ if (--pending === 0) {
970
+ this.trySendToPeer(peer, { action: "reconcile-ack", id: msg.id });
971
+ }
972
+ };
973
+
974
+ for (const [coValueId, clientSessionsHash] of msg.values) {
975
+ // Avoid creating a new coValue object if it's not already in memory
976
+ const inMemoryCoValue = this.local.isCoValueInMemory(coValueId)
977
+ ? this.local.getCoValue(coValueId)
978
+ : undefined;
979
+ if (inMemoryCoValue?.isErroredInPeer(peer.id)) {
980
+ sendAckWhenDone();
981
+ continue;
982
+ }
983
+
984
+ const maybeSendLoadRequest = (
985
+ knownState: CoValueKnownState | undefined,
986
+ ) => {
987
+ if (!knownState) {
988
+ peer.trackToldKnownState(coValueId);
989
+ this.trySendToPeer(peer, {
990
+ action: "load",
991
+ id: coValueId,
992
+ header: false,
993
+ sessions: {},
994
+ });
995
+ } else {
996
+ const serverSessionsHash = this.hashKnownStateSessions(
997
+ knownState.sessions,
998
+ );
999
+ if (serverSessionsHash !== clientSessionsHash) {
1000
+ peer.trackToldKnownState(coValueId);
1001
+ this.trySendToPeer(peer, { action: "load", ...knownState });
1002
+ }
1003
+ }
1004
+ sendAckWhenDone();
1005
+ };
1006
+
1007
+ if (
1008
+ inMemoryCoValue?.isAvailable() ||
1009
+ inMemoryCoValue?.loadingState === "onlyKnownState"
1010
+ ) {
1011
+ maybeSendLoadRequest(inMemoryCoValue.knownState());
1012
+ } else {
1013
+ this.local.storage
1014
+ ? this.local.storage.loadKnownState(coValueId, maybeSendLoadRequest)
1015
+ : maybeSendLoadRequest(undefined);
1016
+ }
1017
+ }
1018
+
1019
+ if (msg.values.length === 0) {
1020
+ this.trySendToPeer(peer, { action: "reconcile-ack", id: msg.id });
1021
+ }
1022
+ }
1023
+
1024
+ handleReconcileAck(msg: ReconcileAckMessage, peer: PeerState): void {
1025
+ const nextOffset = this.reconciliationAckTracker.handleAck(msg.id, peer.id);
1026
+ if (nextOffset !== undefined) {
1027
+ this.local.storage?.renewStorageReconciliationLock(
1028
+ this.local.currentSessionID,
1029
+ peer.id,
1030
+ nextOffset,
1031
+ );
1032
+ }
1033
+ }
1034
+
1035
+ private hashKnownStateSessions(sessions: KnownStateSessions): string {
1036
+ return this.local.crypto.shortHash(sessions);
740
1037
  }
741
1038
 
742
1039
  recordTransactionsSize(newTransactions: Transaction[], source: string) {
@@ -1040,7 +1337,7 @@ export class SyncManager {
1040
1337
  }
1041
1338
  }
1042
1339
 
1043
- peer?.trackLoadRequestComplete(coValue);
1340
+ peer?.trackLoadRequestComplete(coValue, "content");
1044
1341
 
1045
1342
  for (const peer of this.getPeers(coValue.id)) {
1046
1343
  /**
@@ -1056,7 +1353,7 @@ export class SyncManager {
1056
1353
  if (peer.isCoValueSubscribedToPeer(coValue.id)) {
1057
1354
  this.sendNewContent(coValue.id, peer);
1058
1355
  } else if (peer.role === "server") {
1059
- peer.sendLoadRequest(coValue);
1356
+ peer.sendLoadRequest(coValue, "low-priority");
1060
1357
  }
1061
1358
  }
1062
1359
  }