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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/coValueCore/coValueCore.d.ts +9 -0
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +21 -0
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/account.js +10 -10
- package/dist/coValues/account.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +7 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +4 -1
- package/dist/exports.js.map +1 -1
- package/dist/ids.d.ts +1 -1
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js.map +1 -1
- package/dist/knownState.d.ts +5 -0
- package/dist/knownState.d.ts.map +1 -1
- package/dist/knownState.js +15 -0
- package/dist/knownState.js.map +1 -1
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +8 -2
- package/dist/localNode.js.map +1 -1
- package/dist/queue/IncomingMessagesQueue.d.ts +6 -7
- package/dist/queue/IncomingMessagesQueue.d.ts.map +1 -1
- package/dist/queue/IncomingMessagesQueue.js +7 -30
- package/dist/queue/IncomingMessagesQueue.js.map +1 -1
- package/dist/queue/LinkedList.d.ts +1 -1
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/StorageStreamingQueue.d.ts +43 -0
- package/dist/queue/StorageStreamingQueue.d.ts.map +1 -0
- package/dist/queue/StorageStreamingQueue.js +70 -0
- package/dist/queue/StorageStreamingQueue.js.map +1 -0
- package/dist/storage/knownState.d.ts +5 -0
- package/dist/storage/knownState.d.ts.map +1 -1
- package/dist/storage/knownState.js +11 -0
- package/dist/storage/knownState.js.map +1 -1
- package/dist/storage/sqlite/client.d.ts +2 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +18 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +2 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +20 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +2 -0
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +40 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +9 -2
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +71 -44
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +20 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +34 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +185 -46
- package/dist/sync.js.map +1 -1
- package/dist/tests/IncomingMessagesQueue.test.js +4 -150
- package/dist/tests/IncomingMessagesQueue.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +91 -0
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +91 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/StorageStreamingQueue.test.d.ts +2 -0
- package/dist/tests/StorageStreamingQueue.test.d.ts.map +1 -0
- package/dist/tests/StorageStreamingQueue.test.js +213 -0
- package/dist/tests/StorageStreamingQueue.test.js.map +1 -0
- package/dist/tests/SyncManager.processQueues.test.d.ts +2 -0
- package/dist/tests/SyncManager.processQueues.test.d.ts.map +1 -0
- package/dist/tests/SyncManager.processQueues.test.js +208 -0
- package/dist/tests/SyncManager.processQueues.test.js.map +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
- package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
- package/dist/tests/knownState.lazyLoading.test.js +166 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
- package/dist/tests/messagesTestUtils.d.ts +5 -2
- package/dist/tests/messagesTestUtils.d.ts.map +1 -1
- package/dist/tests/messagesTestUtils.js +4 -0
- package/dist/tests/messagesTestUtils.js.map +1 -1
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +4 -0
- package/dist/tests/setup.js.map +1 -0
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +388 -0
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +23 -23
- package/dist/tests/sync.storage.test.js +176 -20
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.test.js +1 -1
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/testStorage.js +36 -0
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +16 -4
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +2 -2
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/coValueCore/coValueCore.ts +26 -0
- package/src/coValues/account.ts +12 -14
- package/src/config.ts +13 -0
- package/src/exports.ts +6 -0
- package/src/ids.ts +1 -1
- package/src/knownState.ts +24 -0
- package/src/localNode.ts +9 -2
- package/src/queue/IncomingMessagesQueue.ts +7 -39
- package/src/queue/LinkedList.ts +1 -1
- package/src/queue/StorageStreamingQueue.ts +96 -0
- package/src/storage/knownState.ts +12 -0
- package/src/storage/sqlite/client.ts +31 -0
- package/src/storage/sqliteAsync/client.ts +35 -0
- package/src/storage/storageAsync.ts +51 -0
- package/src/storage/storageSync.ts +121 -55
- package/src/storage/types.ts +29 -0
- package/src/sync.ts +210 -47
- package/src/tests/IncomingMessagesQueue.test.ts +4 -206
- package/src/tests/StorageApiAsync.test.ts +136 -0
- package/src/tests/StorageApiSync.test.ts +132 -0
- package/src/tests/StorageStreamingQueue.test.ts +276 -0
- package/src/tests/SyncManager.processQueues.test.ts +287 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +3 -0
- package/src/tests/knownState.lazyLoading.test.ts +217 -0
- package/src/tests/messagesTestUtils.ts +10 -3
- package/src/tests/setup.ts +4 -0
- package/src/tests/sync.garbageCollection.test.ts +1 -3
- package/src/tests/sync.load.test.ts +483 -1
- package/src/tests/sync.mesh.test.ts +23 -23
- package/src/tests/sync.storage.test.ts +224 -32
- package/src/tests/sync.test.ts +1 -9
- package/src/tests/testStorage.ts +38 -0
- package/src/tests/testUtils.ts +16 -4
- 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
|
-
|
|
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.
|
|
438
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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,
|
|
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
|
});
|