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
package/src/sync.ts CHANGED
@@ -3,6 +3,7 @@ import { Histogram, ValueType, metrics } from "@opentelemetry/api";
3
3
  import { PeerState } from "./PeerState.js";
4
4
  import { SyncStateManager } from "./SyncStateManager.js";
5
5
  import { UnsyncedCoValuesTracker } from "./UnsyncedCoValuesTracker.js";
6
+ import { SYNC_SCHEDULER_CONFIG } from "./config.js";
6
7
  import {
7
8
  getContenDebugInfo,
8
9
  getNewTransactionsFromContentMessage,
@@ -19,10 +20,12 @@ import { logger } from "./logger.js";
19
20
  import { CoValuePriority } from "./priority.js";
20
21
  import { IncomingMessagesQueue } from "./queue/IncomingMessagesQueue.js";
21
22
  import { LocalTransactionsSyncQueue } from "./queue/LocalTransactionsSyncQueue.js";
23
+ import type { StorageStreamingQueue } from "./queue/StorageStreamingQueue.js";
22
24
  import {
23
25
  CoValueKnownState,
24
26
  knownStateFrom,
25
27
  KnownStateSessions,
28
+ peerHasAllContent,
26
29
  } from "./knownState.js";
27
30
  import { StorageAPI } from "./storage/index.js";
28
31
 
@@ -426,17 +429,87 @@ export class SyncManager {
426
429
  }
427
430
  }
428
431
 
429
- messagesQueue = new IncomingMessagesQueue();
432
+ messagesQueue = new IncomingMessagesQueue(() => this.processQueues());
433
+ private processing = false;
434
+
430
435
  pushMessage(incoming: SyncMessage, peer: PeerState) {
431
436
  this.messagesQueue.push(incoming, peer);
437
+ }
432
438
 
433
- if (this.messagesQueue.processing) {
439
+ /**
440
+ * Get the storage streaming queue if available.
441
+ * Returns undefined if storage doesn't have a streaming queue.
442
+ */
443
+ private getStorageStreamingQueue(): StorageStreamingQueue | undefined {
444
+ const storage = this.local.storage;
445
+ if (storage && "streamingQueue" in storage) {
446
+ return storage.streamingQueue as StorageStreamingQueue;
447
+ }
448
+ return undefined;
449
+ }
450
+
451
+ /**
452
+ * Unified queue processing that coordinates both incoming messages
453
+ * and storage streaming entries.
454
+ *
455
+ * Processes items from both queues with priority ordering:
456
+ * - Incoming messages are processed via round-robin across peers
457
+ * - Storage streaming entries are processed by priority (MEDIUM before LOW)
458
+ *
459
+ * Implements time budget scheduling to avoid blocking the main thread.
460
+ */
461
+ private async processQueues() {
462
+ if (this.processing) {
434
463
  return;
435
464
  }
436
465
 
437
- this.messagesQueue.processQueue((msg, peer) => {
438
- this.handleSyncMessage(msg, peer);
439
- });
466
+ this.processing = true;
467
+ let lastTimer = performance.now();
468
+
469
+ const streamingQueue = this.getStorageStreamingQueue();
470
+
471
+ while (true) {
472
+ // First, try to pull from incoming messages queue
473
+ const messageEntry = this.messagesQueue.pull();
474
+ if (messageEntry) {
475
+ try {
476
+ this.handleSyncMessage(messageEntry.msg, messageEntry.peer);
477
+ } catch (err) {
478
+ logger.error("Error processing message", { err });
479
+ }
480
+ }
481
+
482
+ // Then, try to pull from storage streaming queue
483
+ const pushStreamingContent = streamingQueue?.pull();
484
+ if (pushStreamingContent) {
485
+ try {
486
+ // Invoke the pushContent callback to stream the content
487
+ pushStreamingContent();
488
+ } catch (err) {
489
+ logger.error("Error processing storage streaming entry", {
490
+ err,
491
+ });
492
+ }
493
+ }
494
+
495
+ // If both queues are empty, we're done
496
+ if (!messageEntry && !pushStreamingContent) {
497
+ break;
498
+ }
499
+
500
+ // Check if we have blocked the main thread for too long
501
+ // and if so, yield to the event loop
502
+ const currentTimer = performance.now();
503
+ if (
504
+ currentTimer - lastTimer >
505
+ SYNC_SCHEDULER_CONFIG.INCOMING_MESSAGES_TIME_BUDGET
506
+ ) {
507
+ await new Promise<void>((resolve) => setTimeout(resolve));
508
+ lastTimer = performance.now();
509
+ }
510
+ }
511
+
512
+ this.processing = false;
440
513
  }
441
514
 
442
515
  addPeer(peer: Peer, skipReconciliation: boolean = false) {
@@ -511,33 +584,125 @@ export class SyncManager {
511
584
  peer.setKnownState(msg.id, knownStateFrom(msg));
512
585
  const coValue = this.local.getCoValue(msg.id);
513
586
 
587
+ // Fast path: CoValue is already in memory
514
588
  if (coValue.isAvailable()) {
515
589
  this.sendNewContent(msg.id, peer);
516
590
  return;
517
591
  }
518
592
 
519
- const peers = this.getServerPeers(msg.id, peer.id);
593
+ const peerKnownState = peer.getOptimisticKnownState(msg.id);
594
+
595
+ // Fast path: Peer has no content at all - skip lazy load check, just load directly
596
+ if (!peerKnownState?.header) {
597
+ this.loadFromStorageAndRespond(msg.id, peer, coValue);
598
+ return;
599
+ }
600
+
601
+ // Check storage knownState before doing full load (lazy load optimization)
602
+ coValue.getKnownStateFromStorage((storageKnownState) => {
603
+ // Race condition: CoValue might have been loaded while we were waiting for storage
604
+ if (coValue.isAvailable()) {
605
+ this.sendNewContent(msg.id, peer);
606
+ return;
607
+ }
608
+
609
+ if (!storageKnownState) {
610
+ // Not in storage, try loading from peers
611
+ this.loadFromPeersAndRespond(msg.id, peer, coValue);
612
+ return;
613
+ }
614
+
615
+ // Check if peer already has all content
616
+ if (peerHasAllContent(storageKnownState, peerKnownState)) {
617
+ // Peer already has everything - reply with known message, no full load needed
618
+ peer.trackToldKnownState(msg.id);
619
+ this.trySendToPeer(peer, {
620
+ action: "known",
621
+ ...storageKnownState,
622
+ });
623
+ return;
624
+ }
520
625
 
521
- coValue.load(peers);
626
+ // Peer needs content - do full load from storage
627
+ this.loadFromStorageAndRespond(msg.id, peer, coValue);
628
+ });
629
+ }
630
+
631
+ /**
632
+ * Helper to load from storage and respond appropriately.
633
+ * Falls back to peers if not found in storage.
634
+ */
635
+ private loadFromStorageAndRespond(
636
+ id: RawCoID,
637
+ peer: PeerState,
638
+ coValue: CoValueCore,
639
+ ) {
640
+ coValue.loadFromStorage((found) => {
641
+ if (found && coValue.isAvailable()) {
642
+ this.sendNewContent(id, peer);
643
+ } else {
644
+ this.loadFromPeersAndRespond(id, peer, coValue);
645
+ }
646
+ });
647
+ }
648
+
649
+ /**
650
+ * Helper to load from peers and respond appropriately.
651
+ */
652
+ private loadFromPeersAndRespond(
653
+ id: RawCoID,
654
+ peer: PeerState,
655
+ coValue: CoValueCore,
656
+ ) {
657
+ const peers = this.getServerPeers(id, peer.id);
658
+ coValue.loadFromPeers(peers);
522
659
 
523
660
  const handleLoadResult = () => {
524
661
  if (coValue.isAvailable()) {
662
+ this.sendNewContent(id, peer);
525
663
  return;
526
664
  }
665
+ this.handleLoadNotFound(id, peer);
666
+ };
527
667
 
528
- peer.trackToldKnownState(msg.id);
668
+ if (peers.length > 0) {
669
+ coValue.waitForAvailableOrUnavailable().then(handleLoadResult);
670
+ } else {
671
+ handleLoadResult();
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Handle case when CoValue is not found.
677
+ */
678
+ private handleLoadNotFound(id: RawCoID, peer: PeerState) {
679
+ peer.trackToldKnownState(id);
680
+ this.trySendToPeer(peer, {
681
+ action: "known",
682
+ id,
683
+ header: false,
684
+ sessions: {},
685
+ });
686
+ }
687
+
688
+ /**
689
+ * Request full content from a peer when we don't have the CoValue.
690
+ */
691
+ private requestFullContent(id: RawCoID, peer: PeerState | undefined) {
692
+ if (peer) {
529
693
  this.trySendToPeer(peer, {
530
694
  action: "known",
531
- id: msg.id,
695
+ isCorrection: true,
696
+ id,
532
697
  header: false,
533
698
  sessions: {},
534
699
  });
535
- };
536
-
537
- if (peers.length > 0 || this.local.storage) {
538
- coValue.waitForAvailableOrUnavailable().then(handleLoadResult);
539
700
  } else {
540
- handleLoadResult();
701
+ // The wrong assumption has been made by storage or import, we don't have a recovery mechanism
702
+ // Should never happen
703
+ logger.error("Received new content with no header on a missing CoValue", {
704
+ id,
705
+ });
541
706
  }
542
707
  }
543
708
 
@@ -622,46 +787,37 @@ export class SyncManager {
622
787
  */
623
788
  if (!coValue.hasVerifiedContent()) {
624
789
  /**
625
- * The peer has assumed we already have the CoValue
790
+ * The peer/import has assumed we already have the CoValue
626
791
  */
627
792
  if (!msg.header) {
628
- // We check if the covalue was in memory and has been garbage collected
629
- // In that case we should have it tracked in the storage
630
- const storageKnownState = this.local.storage?.getKnownState(msg.id);
631
-
632
- if (storageKnownState?.header) {
633
- // If the CoValue has been garbage collected, we load it from the storage before handling the new content
634
- coValue.loadFromStorage((found) => {
635
- if (found) {
636
- this.handleNewContent(msg, from);
637
- } else {
638
- logger.error("Known CoValue not found in storage", {
639
- id: msg.id,
640
- });
641
- }
642
- });
643
- return;
644
- }
645
-
646
- // The peer assumption is not correct, so we ask for the full CoValue
647
- if (peer) {
648
- this.trySendToPeer(peer, {
649
- action: "known",
650
- isCorrection: true,
651
- id: msg.id,
652
- header: false,
653
- sessions: {},
654
- });
655
- } else {
656
- // The wrong assumption has been made by storage or import, we don't have a recovery mechanism
657
- // Should never happen
658
- logger.error(
659
- "Received new content with no header on a missing CoValue",
793
+ // Content from storage without header - this can happen if:
794
+ // 1. Storage is streaming a large CoValue in chunks
795
+ // 2. Server is under heavy load, so a chunk isn't processed for a long time
796
+ // 3. GC cleanup unmounts the CoValue while streaming is in progress
797
+ // 4. The chunk is finally processed, but the CoValue is no longer available
798
+ // TODO: Fix this by either not unmounting CoValues with active streaming,
799
+ // or by cleaning up the streaming queue on unmount
800
+ if (from === "storage") {
801
+ logger.warn(
802
+ "Received content from storage without header - CoValue may have been garbage collected mid-stream",
660
803
  {
661
804
  id: msg.id,
805
+ from,
662
806
  },
663
807
  );
808
+ return;
664
809
  }
810
+
811
+ // Try to load from storage - the CoValue might have been garbage collected from memory
812
+ coValue.loadFromStorage((found) => {
813
+ if (found) {
814
+ // CoValue was in storage, process the new content
815
+ this.handleNewContent(msg, from);
816
+ } else {
817
+ // CoValue not in storage, ask peer for full content
818
+ this.requestFullContent(msg.id, peer);
819
+ }
820
+ });
665
821
  return;
666
822
  }
667
823
 
@@ -1053,6 +1209,13 @@ export class SyncManager {
1053
1209
 
1054
1210
  setStorage(storage: StorageAPI) {
1055
1211
  this.unsyncedTracker.setStorage(storage);
1212
+
1213
+ const storageStreamingQueue = this.getStorageStreamingQueue();
1214
+ if (storageStreamingQueue) {
1215
+ storageStreamingQueue.setListener(() => {
1216
+ this.processQueues();
1217
+ });
1218
+ }
1056
1219
  }
1057
1220
 
1058
1221
  removeStorage() {
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
2
  import { PeerState } from "../PeerState.js";
3
3
  import { IncomingMessagesQueue } from "../queue/IncomingMessagesQueue.js";
4
4
  import { ConnectedPeerChannel } from "../streamUtils.js";
@@ -8,13 +8,6 @@ import {
8
8
  tearDownTestMetricReader,
9
9
  } from "./testUtils.js";
10
10
 
11
- // Mock performance.now for consistent timing tests
12
- let mockPerformanceNow = vi.spyOn(performance, "now");
13
-
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- });
17
-
18
11
  function createMockPeer(id: string): Peer {
19
12
  return {
20
13
  id,
@@ -60,18 +53,15 @@ function createMockSyncMessage(
60
53
  }
61
54
 
62
55
  function setup() {
56
+ const processQueues = vi.fn();
63
57
  const metricReader = createTestMetricReader();
64
- const queue = new IncomingMessagesQueue();
58
+ const queue = new IncomingMessagesQueue(processQueues);
65
59
  const peer1 = createMockPeerState("peer1");
66
60
  const peer2 = createMockPeerState("peer2");
67
61
 
68
- return { queue, peer1, peer2, metricReader };
62
+ return { queue, peer1, peer2, metricReader, processQueues };
69
63
  }
70
64
 
71
- beforeEach(() => {
72
- mockPerformanceNow.mockReturnValue(0);
73
- });
74
-
75
65
  afterEach(() => {
76
66
  tearDownTestMetricReader();
77
67
  });
@@ -82,7 +72,6 @@ describe("IncomingMessagesQueue", () => {
82
72
  const { queue } = setup();
83
73
  expect(queue["queues"]).toEqual([]);
84
74
  expect(queue.currentQueue).toBe(0);
85
- expect(queue.processing).toBe(false);
86
75
  });
87
76
  });
88
77
 
@@ -217,142 +206,6 @@ describe("IncomingMessagesQueue", () => {
217
206
  });
218
207
  });
219
208
 
220
- describe("processQueue", () => {
221
- test("should process all messages in queue", async () => {
222
- const { queue, peer1, peer2 } = setup();
223
- const msg1 = createMockSyncMessage("test1");
224
- const msg2 = createMockSyncMessage("test2");
225
- const msg3 = createMockSyncMessage("test3");
226
-
227
- queue.push(msg1, peer1);
228
- queue.push(msg2, peer1);
229
- queue.push(msg3, peer2);
230
-
231
- const processedMessages: Array<{ msg: SyncMessage; peer: PeerState }> =
232
- [];
233
-
234
- await queue.processQueue((msg, peer) => {
235
- processedMessages.push({ msg, peer });
236
- });
237
-
238
- expect(processedMessages).toEqual([
239
- { msg: msg1, peer: peer1 },
240
- { msg: msg3, peer: peer2 },
241
- { msg: msg2, peer: peer1 },
242
- ]);
243
- expect(queue.processing).toBe(false);
244
- });
245
-
246
- test("should set processing flag during execution", async () => {
247
- const { queue, peer1 } = setup();
248
- const msg = createMockSyncMessage("test");
249
- queue.push(msg, peer1);
250
-
251
- let processingFlagDuringExecution = false;
252
- const processingPromise = queue.processQueue(() => {
253
- processingFlagDuringExecution = queue.processing;
254
- });
255
-
256
- await processingPromise;
257
- expect(processingFlagDuringExecution).toBe(true);
258
- expect(queue.processing).toBe(false);
259
- });
260
-
261
- test("should handle empty queue", async () => {
262
- const { queue } = setup();
263
- const callback = vi.fn();
264
-
265
- await queue.processQueue(callback);
266
-
267
- expect(callback).not.toHaveBeenCalled();
268
- expect(queue.processing).toBe(false);
269
- });
270
-
271
- test("should yield to event loop when processing takes too long", async () => {
272
- const { queue, peer1 } = setup();
273
- const msg1 = createMockSyncMessage("test1");
274
- const msg2 = createMockSyncMessage("test2");
275
-
276
- queue.push(msg1, peer1);
277
- queue.push(msg2, peer1);
278
-
279
- // Mock timing to simulate long processing
280
- mockPerformanceNow
281
- .mockReturnValueOnce(0) // Initial time
282
- .mockReturnValueOnce(60); // After first message (60ms > 50ms threshold)
283
-
284
- const setTimeoutSpy = vi.spyOn(global, "setTimeout");
285
-
286
- await queue.processQueue(() => {
287
- // Simulate some processing time
288
- });
289
-
290
- expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 0);
291
- });
292
-
293
- test("should not yield to event loop when processing is fast", async () => {
294
- const { queue, peer1 } = setup();
295
- const msg = createMockSyncMessage("test");
296
- queue.push(msg, peer1);
297
-
298
- // Mock timing to simulate fast processing
299
- mockPerformanceNow
300
- .mockReturnValueOnce(0) // Initial time
301
- .mockReturnValueOnce(30); // After message (30ms < 50ms threshold)
302
-
303
- const setTimeoutSpy = vi.spyOn(global, "setTimeout");
304
-
305
- await queue.processQueue(() => {
306
- // Simulate some processing time
307
- });
308
-
309
- expect(setTimeoutSpy).not.toHaveBeenCalled();
310
- });
311
-
312
- test("should handle callback errors gracefully", async () => {
313
- const { queue, peer1 } = setup();
314
- const msg = createMockSyncMessage("test");
315
- queue.push(msg, peer1);
316
-
317
- const error = new Error("Callback error");
318
-
319
- await queue.processQueue(() => {
320
- throw error;
321
- });
322
-
323
- // The processing flag should be reset even when an error occurs
324
- expect(queue.processing).toBe(false);
325
- });
326
-
327
- test("should process messages in correct round-robin order", async () => {
328
- const { queue, peer1, peer2 } = setup();
329
- const msg1 = createMockSyncMessage("test1");
330
- const msg2 = createMockSyncMessage("test2");
331
- const msg3 = createMockSyncMessage("test3");
332
- const msg4 = createMockSyncMessage("test4");
333
-
334
- queue.push(msg1, peer1);
335
- queue.push(msg2, peer1);
336
- queue.push(msg3, peer2);
337
- queue.push(msg4, peer2);
338
-
339
- const processedMessages: Array<{ msg: SyncMessage; peer: PeerState }> =
340
- [];
341
-
342
- await queue.processQueue((msg, peer) => {
343
- processedMessages.push({ msg, peer });
344
- });
345
-
346
- // Should process in round-robin: peer1, peer2, peer1, peer2
347
- expect(processedMessages).toEqual([
348
- { msg: msg1, peer: peer1 },
349
- { msg: msg3, peer: peer2 },
350
- { msg: msg2, peer: peer1 },
351
- { msg: msg4, peer: peer2 },
352
- ]);
353
- });
354
- });
355
-
356
209
  describe("edge cases", () => {
357
210
  test("should handle peer with multiple messages correctly", () => {
358
211
  const { queue, peer1 } = setup();
@@ -411,35 +264,6 @@ describe("IncomingMessagesQueue", () => {
411
264
  });
412
265
  });
413
266
 
414
- describe("concurrent operations", () => {
415
- test("should prevent multiple concurrent processQueue calls", async () => {
416
- const { queue, peer1 } = setup();
417
- const msg = createMockSyncMessage("test");
418
- queue.push(msg, peer1);
419
-
420
- const firstProcessSpy = vi.fn();
421
-
422
- const firstProcess = queue.processQueue((msg, peer) => {
423
- firstProcessSpy(msg, peer);
424
- });
425
-
426
- const secondProcessSpy = vi.fn();
427
-
428
- // Second process should not interfere
429
- const secondProcess = queue.processQueue(() => {
430
- secondProcessSpy();
431
- });
432
-
433
- await firstProcess;
434
- await secondProcess;
435
-
436
- expect(firstProcessSpy).toHaveBeenCalled();
437
- expect(secondProcessSpy).not.toHaveBeenCalled();
438
-
439
- expect(queue.processing).toBe(false);
440
- });
441
- });
442
-
443
267
  describe("metrics", () => {
444
268
  test("should increment push counter when pushing messages", async () => {
445
269
  const { queue, peer1, metricReader } = setup();
@@ -594,31 +418,5 @@ describe("IncomingMessagesQueue", () => {
594
418
  expect(clientPullValue).toBe(0);
595
419
  expect(serverPullValue).toBe(0);
596
420
  });
597
-
598
- test("should track metrics during processQueue execution", async () => {
599
- const { queue, peer1, metricReader } = setup();
600
- const msg1 = createMockSyncMessage("test1");
601
- const msg2 = createMockSyncMessage("test2");
602
-
603
- queue.push(msg1, peer1);
604
- queue.push(msg2, peer1);
605
-
606
- await queue.processQueue(() => {
607
- // Process messages
608
- });
609
-
610
- const pushValue = await metricReader.getMetricValue(
611
- "jazz.messagequeue.incoming.pushed",
612
- { peerRole: "client" },
613
- );
614
-
615
- const pullValue = await metricReader.getMetricValue(
616
- "jazz.messagequeue.incoming.pulled",
617
- { peerRole: "client" },
618
- );
619
-
620
- expect(pushValue).toBe(2);
621
- expect(pullValue).toBe(2);
622
- });
623
421
  });
624
422
  });