@auto-engineer/pipeline 1.93.0 → 1.96.0
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/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +108 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +32 -0
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/store/pipeline-read-model.d.ts +18 -0
- package/dist/src/store/pipeline-read-model.d.ts.map +1 -1
- package/dist/src/store/pipeline-read-model.js +51 -0
- package/dist/src/store/pipeline-read-model.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/server/pipeline-server.specs.ts +70 -0
- package/src/server/pipeline-server.ts +34 -0
- package/src/store/pipeline-read-model.specs.ts +361 -0
- package/src/store/pipeline-read-model.ts +55 -0
package/package.json
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"get-port": "^7.1.0",
|
|
14
14
|
"jose": "^5.9.6",
|
|
15
15
|
"nanoid": "^5.0.0",
|
|
16
|
-
"@auto-engineer/file-store": "1.
|
|
17
|
-
"@auto-engineer/message-bus": "1.
|
|
16
|
+
"@auto-engineer/file-store": "1.96.0",
|
|
17
|
+
"@auto-engineer/message-bus": "1.96.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/cors": "^2.8.17",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
|
-
"version": "1.
|
|
26
|
+
"version": "1.96.0",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
|
|
29
29
|
"test": "vitest run --reporter=dot",
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type { ConcurrencyConfig } from './server/command-gate';
|
|
|
47
47
|
export type { CommandHandlerWithMetadata, PipelineServerConfig } from './server/pipeline-server';
|
|
48
48
|
export { PipelineServer } from './server/pipeline-server';
|
|
49
49
|
export { SSEManager } from './server/sse-manager';
|
|
50
|
+
export type { RunStats } from './store/pipeline-read-model';
|
|
50
51
|
export type { SnapshotDiff, SnapshotResult } from './testing/snapshot-compare';
|
|
51
52
|
export {
|
|
52
53
|
compareEventSequence,
|
|
@@ -59,6 +59,13 @@ interface StatsResponse {
|
|
|
59
59
|
totalMessages: number;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
interface RunStatsResponse {
|
|
63
|
+
pipelineStatus: string;
|
|
64
|
+
correlationId: string;
|
|
65
|
+
items: { total: number; running: number; success: number; error: number; retried: number };
|
|
66
|
+
nodes: { total: number; running: number; success: number; error: number };
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
async function fetchAs<T>(url: string, options?: RequestInit): Promise<T> {
|
|
63
70
|
const res = await fetch(url, options);
|
|
64
71
|
return res.json() as Promise<T>;
|
|
@@ -948,6 +955,69 @@ describe('PipelineServer', () => {
|
|
|
948
955
|
});
|
|
949
956
|
});
|
|
950
957
|
|
|
958
|
+
describe('GET /run-stats', () => {
|
|
959
|
+
it('should return idle status when no activity exists', async () => {
|
|
960
|
+
const server = new PipelineServer({ port: 0 });
|
|
961
|
+
await server.start();
|
|
962
|
+
|
|
963
|
+
const data = await fetchAs<RunStatsResponse>(`http://localhost:${server.port}/run-stats`);
|
|
964
|
+
|
|
965
|
+
expect(data.pipelineStatus).toBe('idle');
|
|
966
|
+
expect(data.correlationId).toBeDefined();
|
|
967
|
+
expect(data.items).toEqual({ total: 0, running: 0, success: 0, error: 0, retried: 0 });
|
|
968
|
+
expect(data.nodes).toEqual({ total: 0, running: 0, success: 0, error: 0 });
|
|
969
|
+
await server.stop();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('should return active status when pipeline is processing', async () => {
|
|
973
|
+
const handler = {
|
|
974
|
+
name: 'SlowCmd',
|
|
975
|
+
handle: async () => {
|
|
976
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
977
|
+
return { type: 'SlowDone', data: {} };
|
|
978
|
+
},
|
|
979
|
+
};
|
|
980
|
+
const server = new PipelineServer({ port: 0 });
|
|
981
|
+
server.registerCommandHandlers([handler]);
|
|
982
|
+
await server.start();
|
|
983
|
+
|
|
984
|
+
void fetch(`http://localhost:${server.port}/command`, {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: { 'Content-Type': 'application/json' },
|
|
987
|
+
body: JSON.stringify({ type: 'SlowCmd', data: {} }),
|
|
988
|
+
});
|
|
989
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
990
|
+
|
|
991
|
+
const data = await fetchAs<RunStatsResponse>(`http://localhost:${server.port}/run-stats`);
|
|
992
|
+
|
|
993
|
+
expect(data.pipelineStatus).toBe('active');
|
|
994
|
+
await server.stop();
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('should return completed status after pipeline finishes', async () => {
|
|
998
|
+
const handler = {
|
|
999
|
+
name: 'QuickCmd',
|
|
1000
|
+
handle: async () => ({ type: 'QuickDone', data: {} }),
|
|
1001
|
+
};
|
|
1002
|
+
const server = new PipelineServer({ port: 0 });
|
|
1003
|
+
server.registerCommandHandlers([handler]);
|
|
1004
|
+
await server.start();
|
|
1005
|
+
|
|
1006
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1007
|
+
method: 'POST',
|
|
1008
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1009
|
+
body: JSON.stringify({ type: 'QuickCmd', data: {} }),
|
|
1010
|
+
});
|
|
1011
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1012
|
+
|
|
1013
|
+
const data = await fetchAs<RunStatsResponse>(`http://localhost:${server.port}/run-stats`);
|
|
1014
|
+
|
|
1015
|
+
expect(data.pipelineStatus).toBe('completed');
|
|
1016
|
+
expect(data.items.total).toBeGreaterThanOrEqual(1);
|
|
1017
|
+
await server.stop();
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
951
1021
|
describe('event routing', () => {
|
|
952
1022
|
it('should route events through pipeline', async () => {
|
|
953
1023
|
const handler = {
|
|
@@ -395,6 +395,40 @@ export class PipelineServer {
|
|
|
395
395
|
})();
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
+
this.app.get('/run-stats', (req, res) => {
|
|
399
|
+
void (async () => {
|
|
400
|
+
const correlationId = (req.query.correlationId as string) || this.currentSessionId;
|
|
401
|
+
if (!correlationId) {
|
|
402
|
+
res.json({
|
|
403
|
+
pipelineStatus: 'idle',
|
|
404
|
+
correlationId: '',
|
|
405
|
+
items: { total: 0, running: 0, success: 0, error: 0, retried: 0 },
|
|
406
|
+
nodes: { total: 0, running: 0, success: 0, error: 0 },
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const runStats = await this.eventStoreContext.readModel.getRunStats(correlationId);
|
|
412
|
+
const hasActivity = runStats.items.total > 0 || runStats.nodes.total > 0;
|
|
413
|
+
const isQuiescent = this.quiescenceTracker.isQuiescent();
|
|
414
|
+
|
|
415
|
+
let pipelineStatus: 'idle' | 'active' | 'completed';
|
|
416
|
+
if (!isQuiescent) {
|
|
417
|
+
pipelineStatus = 'active';
|
|
418
|
+
} else if (hasActivity) {
|
|
419
|
+
pipelineStatus = 'completed';
|
|
420
|
+
} else {
|
|
421
|
+
pipelineStatus = 'idle';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
res.json({
|
|
425
|
+
pipelineStatus,
|
|
426
|
+
correlationId,
|
|
427
|
+
...runStats,
|
|
428
|
+
});
|
|
429
|
+
})();
|
|
430
|
+
});
|
|
431
|
+
|
|
398
432
|
this.app.get('/events', (req, res) => {
|
|
399
433
|
const clientId = `sse-${nanoid()}`;
|
|
400
434
|
const correlationIdFilter = req.query.correlationId as string | undefined;
|
|
@@ -571,6 +571,367 @@ describe('PipelineReadModel', () => {
|
|
|
571
571
|
});
|
|
572
572
|
});
|
|
573
573
|
|
|
574
|
+
describe('getAllItemStatuses', () => {
|
|
575
|
+
it('should return empty array when no items exist', async () => {
|
|
576
|
+
const result = await readModel.getAllItemStatuses('c1');
|
|
577
|
+
|
|
578
|
+
expect(result).toEqual([]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should return all items for correlationId', async () => {
|
|
582
|
+
const collection = database.collection<WithId<ItemStatusDocument>>('ItemStatus');
|
|
583
|
+
await collection.insertOne({
|
|
584
|
+
_id: 'c1-Cmd-a',
|
|
585
|
+
correlationId: 'c1',
|
|
586
|
+
commandType: 'Cmd',
|
|
587
|
+
itemKey: 'a',
|
|
588
|
+
currentRequestId: 'r1',
|
|
589
|
+
status: 'running',
|
|
590
|
+
attemptCount: 1,
|
|
591
|
+
});
|
|
592
|
+
await collection.insertOne({
|
|
593
|
+
_id: 'c1-Cmd-b',
|
|
594
|
+
correlationId: 'c1',
|
|
595
|
+
commandType: 'Cmd',
|
|
596
|
+
itemKey: 'b',
|
|
597
|
+
currentRequestId: 'r2',
|
|
598
|
+
status: 'success',
|
|
599
|
+
attemptCount: 1,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const result = await readModel.getAllItemStatuses('c1');
|
|
603
|
+
|
|
604
|
+
expect(result).toEqual([
|
|
605
|
+
{
|
|
606
|
+
correlationId: 'c1',
|
|
607
|
+
commandType: 'Cmd',
|
|
608
|
+
itemKey: 'a',
|
|
609
|
+
currentRequestId: 'r1',
|
|
610
|
+
status: 'running',
|
|
611
|
+
attemptCount: 1,
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
correlationId: 'c1',
|
|
615
|
+
commandType: 'Cmd',
|
|
616
|
+
itemKey: 'b',
|
|
617
|
+
currentRequestId: 'r2',
|
|
618
|
+
status: 'success',
|
|
619
|
+
attemptCount: 1,
|
|
620
|
+
},
|
|
621
|
+
]);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should filter by correlationId', async () => {
|
|
625
|
+
const collection = database.collection<WithId<ItemStatusDocument>>('ItemStatus');
|
|
626
|
+
await collection.insertOne({
|
|
627
|
+
_id: 'c1-Cmd-a',
|
|
628
|
+
correlationId: 'c1',
|
|
629
|
+
commandType: 'Cmd',
|
|
630
|
+
itemKey: 'a',
|
|
631
|
+
currentRequestId: 'r1',
|
|
632
|
+
status: 'running',
|
|
633
|
+
attemptCount: 1,
|
|
634
|
+
});
|
|
635
|
+
await collection.insertOne({
|
|
636
|
+
_id: 'c2-Cmd-a',
|
|
637
|
+
correlationId: 'c2',
|
|
638
|
+
commandType: 'Cmd',
|
|
639
|
+
itemKey: 'a',
|
|
640
|
+
currentRequestId: 'r2',
|
|
641
|
+
status: 'success',
|
|
642
|
+
attemptCount: 1,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const result = await readModel.getAllItemStatuses('c1');
|
|
646
|
+
|
|
647
|
+
expect(result).toEqual([
|
|
648
|
+
{
|
|
649
|
+
correlationId: 'c1',
|
|
650
|
+
commandType: 'Cmd',
|
|
651
|
+
itemKey: 'a',
|
|
652
|
+
currentRequestId: 'r1',
|
|
653
|
+
status: 'running',
|
|
654
|
+
attemptCount: 1,
|
|
655
|
+
},
|
|
656
|
+
]);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
describe('getAllNodeStatuses', () => {
|
|
661
|
+
it('should return empty array when no nodes exist', async () => {
|
|
662
|
+
const result = await readModel.getAllNodeStatuses('c1');
|
|
663
|
+
|
|
664
|
+
expect(result).toEqual([]);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should return all nodes for correlationId', async () => {
|
|
668
|
+
const collection = database.collection<WithId<NodeStatusDocument>>('NodeStatus');
|
|
669
|
+
await collection.insertOne({
|
|
670
|
+
_id: 'c1-CmdA',
|
|
671
|
+
correlationId: 'c1',
|
|
672
|
+
commandName: 'CmdA',
|
|
673
|
+
status: 'running',
|
|
674
|
+
pendingCount: 1,
|
|
675
|
+
endedCount: 0,
|
|
676
|
+
});
|
|
677
|
+
await collection.insertOne({
|
|
678
|
+
_id: 'c1-CmdB',
|
|
679
|
+
correlationId: 'c1',
|
|
680
|
+
commandName: 'CmdB',
|
|
681
|
+
status: 'success',
|
|
682
|
+
pendingCount: 0,
|
|
683
|
+
endedCount: 2,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const result = await readModel.getAllNodeStatuses('c1');
|
|
687
|
+
|
|
688
|
+
expect(result).toEqual([
|
|
689
|
+
{
|
|
690
|
+
correlationId: 'c1',
|
|
691
|
+
commandName: 'CmdA',
|
|
692
|
+
status: 'running',
|
|
693
|
+
pendingCount: 1,
|
|
694
|
+
endedCount: 0,
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
correlationId: 'c1',
|
|
698
|
+
commandName: 'CmdB',
|
|
699
|
+
status: 'success',
|
|
700
|
+
pendingCount: 0,
|
|
701
|
+
endedCount: 2,
|
|
702
|
+
},
|
|
703
|
+
]);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('should filter by correlationId', async () => {
|
|
707
|
+
const collection = database.collection<WithId<NodeStatusDocument>>('NodeStatus');
|
|
708
|
+
await collection.insertOne({
|
|
709
|
+
_id: 'c1-Cmd',
|
|
710
|
+
correlationId: 'c1',
|
|
711
|
+
commandName: 'Cmd',
|
|
712
|
+
status: 'running',
|
|
713
|
+
pendingCount: 1,
|
|
714
|
+
endedCount: 0,
|
|
715
|
+
});
|
|
716
|
+
await collection.insertOne({
|
|
717
|
+
_id: 'c2-Cmd',
|
|
718
|
+
correlationId: 'c2',
|
|
719
|
+
commandName: 'Cmd',
|
|
720
|
+
status: 'success',
|
|
721
|
+
pendingCount: 0,
|
|
722
|
+
endedCount: 1,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const result = await readModel.getAllNodeStatuses('c1');
|
|
726
|
+
|
|
727
|
+
expect(result).toEqual([
|
|
728
|
+
{
|
|
729
|
+
correlationId: 'c1',
|
|
730
|
+
commandName: 'Cmd',
|
|
731
|
+
status: 'running',
|
|
732
|
+
pendingCount: 1,
|
|
733
|
+
endedCount: 0,
|
|
734
|
+
},
|
|
735
|
+
]);
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
describe('getRunStats', () => {
|
|
740
|
+
it('should return zero counts when no data exists', async () => {
|
|
741
|
+
const result = await readModel.getRunStats('c1');
|
|
742
|
+
|
|
743
|
+
expect(result).toEqual({
|
|
744
|
+
items: { total: 0, running: 0, success: 0, error: 0, retried: 0 },
|
|
745
|
+
nodes: { total: 0, running: 0, success: 0, error: 0 },
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('should count items by status', async () => {
|
|
750
|
+
const collection = database.collection<WithId<ItemStatusDocument>>('ItemStatus');
|
|
751
|
+
await collection.insertOne({
|
|
752
|
+
_id: 'c1-Cmd-a',
|
|
753
|
+
correlationId: 'c1',
|
|
754
|
+
commandType: 'Cmd',
|
|
755
|
+
itemKey: 'a',
|
|
756
|
+
currentRequestId: 'r1',
|
|
757
|
+
status: 'running',
|
|
758
|
+
attemptCount: 1,
|
|
759
|
+
});
|
|
760
|
+
await collection.insertOne({
|
|
761
|
+
_id: 'c1-Cmd-b',
|
|
762
|
+
correlationId: 'c1',
|
|
763
|
+
commandType: 'Cmd',
|
|
764
|
+
itemKey: 'b',
|
|
765
|
+
currentRequestId: 'r2',
|
|
766
|
+
status: 'success',
|
|
767
|
+
attemptCount: 1,
|
|
768
|
+
});
|
|
769
|
+
await collection.insertOne({
|
|
770
|
+
_id: 'c1-Cmd-c',
|
|
771
|
+
correlationId: 'c1',
|
|
772
|
+
commandType: 'Cmd',
|
|
773
|
+
itemKey: 'c',
|
|
774
|
+
currentRequestId: 'r3',
|
|
775
|
+
status: 'error',
|
|
776
|
+
attemptCount: 1,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const result = await readModel.getRunStats('c1');
|
|
780
|
+
|
|
781
|
+
expect(result.items).toEqual({
|
|
782
|
+
total: 3,
|
|
783
|
+
running: 1,
|
|
784
|
+
success: 1,
|
|
785
|
+
error: 1,
|
|
786
|
+
retried: 0,
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('should count retried items with attemptCount > 1', async () => {
|
|
791
|
+
const collection = database.collection<WithId<ItemStatusDocument>>('ItemStatus');
|
|
792
|
+
await collection.insertOne({
|
|
793
|
+
_id: 'c1-Cmd-a',
|
|
794
|
+
correlationId: 'c1',
|
|
795
|
+
commandType: 'Cmd',
|
|
796
|
+
itemKey: 'a',
|
|
797
|
+
currentRequestId: 'r1',
|
|
798
|
+
status: 'success',
|
|
799
|
+
attemptCount: 2,
|
|
800
|
+
});
|
|
801
|
+
await collection.insertOne({
|
|
802
|
+
_id: 'c1-Cmd-b',
|
|
803
|
+
correlationId: 'c1',
|
|
804
|
+
commandType: 'Cmd',
|
|
805
|
+
itemKey: 'b',
|
|
806
|
+
currentRequestId: 'r2',
|
|
807
|
+
status: 'success',
|
|
808
|
+
attemptCount: 1,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const result = await readModel.getRunStats('c1');
|
|
812
|
+
|
|
813
|
+
expect(result.items).toEqual({
|
|
814
|
+
total: 2,
|
|
815
|
+
running: 0,
|
|
816
|
+
success: 2,
|
|
817
|
+
error: 0,
|
|
818
|
+
retried: 1,
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('should count nodes by status', async () => {
|
|
823
|
+
const collection = database.collection<WithId<NodeStatusDocument>>('NodeStatus');
|
|
824
|
+
await collection.insertOne({
|
|
825
|
+
_id: 'c1-CmdA',
|
|
826
|
+
correlationId: 'c1',
|
|
827
|
+
commandName: 'CmdA',
|
|
828
|
+
status: 'running',
|
|
829
|
+
pendingCount: 1,
|
|
830
|
+
endedCount: 0,
|
|
831
|
+
});
|
|
832
|
+
await collection.insertOne({
|
|
833
|
+
_id: 'c1-CmdB',
|
|
834
|
+
correlationId: 'c1',
|
|
835
|
+
commandName: 'CmdB',
|
|
836
|
+
status: 'success',
|
|
837
|
+
pendingCount: 0,
|
|
838
|
+
endedCount: 2,
|
|
839
|
+
});
|
|
840
|
+
await collection.insertOne({
|
|
841
|
+
_id: 'c1-CmdC',
|
|
842
|
+
correlationId: 'c1',
|
|
843
|
+
commandName: 'CmdC',
|
|
844
|
+
status: 'error',
|
|
845
|
+
pendingCount: 0,
|
|
846
|
+
endedCount: 1,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
const result = await readModel.getRunStats('c1');
|
|
850
|
+
|
|
851
|
+
expect(result.nodes).toEqual({
|
|
852
|
+
total: 3,
|
|
853
|
+
running: 1,
|
|
854
|
+
success: 1,
|
|
855
|
+
error: 1,
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('should filter by correlationId', async () => {
|
|
860
|
+
const itemCollection = database.collection<WithId<ItemStatusDocument>>('ItemStatus');
|
|
861
|
+
await itemCollection.insertOne({
|
|
862
|
+
_id: 'c1-Cmd-a',
|
|
863
|
+
correlationId: 'c1',
|
|
864
|
+
commandType: 'Cmd',
|
|
865
|
+
itemKey: 'a',
|
|
866
|
+
currentRequestId: 'r1',
|
|
867
|
+
status: 'running',
|
|
868
|
+
attemptCount: 1,
|
|
869
|
+
});
|
|
870
|
+
await itemCollection.insertOne({
|
|
871
|
+
_id: 'c2-Cmd-a',
|
|
872
|
+
correlationId: 'c2',
|
|
873
|
+
commandType: 'Cmd',
|
|
874
|
+
itemKey: 'a',
|
|
875
|
+
currentRequestId: 'r2',
|
|
876
|
+
status: 'success',
|
|
877
|
+
attemptCount: 1,
|
|
878
|
+
});
|
|
879
|
+
const nodeCollection = database.collection<WithId<NodeStatusDocument>>('NodeStatus');
|
|
880
|
+
await nodeCollection.insertOne({
|
|
881
|
+
_id: 'c1-Cmd',
|
|
882
|
+
correlationId: 'c1',
|
|
883
|
+
commandName: 'Cmd',
|
|
884
|
+
status: 'running',
|
|
885
|
+
pendingCount: 1,
|
|
886
|
+
endedCount: 0,
|
|
887
|
+
});
|
|
888
|
+
await nodeCollection.insertOne({
|
|
889
|
+
_id: 'c2-Cmd',
|
|
890
|
+
correlationId: 'c2',
|
|
891
|
+
commandName: 'Cmd',
|
|
892
|
+
status: 'success',
|
|
893
|
+
pendingCount: 0,
|
|
894
|
+
endedCount: 1,
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const result = await readModel.getRunStats('c1');
|
|
898
|
+
|
|
899
|
+
expect(result).toEqual({
|
|
900
|
+
items: { total: 1, running: 1, success: 0, error: 0, retried: 0 },
|
|
901
|
+
nodes: { total: 1, running: 1, success: 0, error: 0 },
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('should count idle nodes', async () => {
|
|
906
|
+
const collection = database.collection<WithId<NodeStatusDocument>>('NodeStatus');
|
|
907
|
+
await collection.insertOne({
|
|
908
|
+
_id: 'c1-CmdA',
|
|
909
|
+
correlationId: 'c1',
|
|
910
|
+
commandName: 'CmdA',
|
|
911
|
+
status: 'idle',
|
|
912
|
+
pendingCount: 0,
|
|
913
|
+
endedCount: 0,
|
|
914
|
+
});
|
|
915
|
+
await collection.insertOne({
|
|
916
|
+
_id: 'c1-CmdB',
|
|
917
|
+
correlationId: 'c1',
|
|
918
|
+
commandName: 'CmdB',
|
|
919
|
+
status: 'running',
|
|
920
|
+
pendingCount: 1,
|
|
921
|
+
endedCount: 0,
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const result = await readModel.getRunStats('c1');
|
|
925
|
+
|
|
926
|
+
expect(result.nodes).toEqual({
|
|
927
|
+
total: 2,
|
|
928
|
+
running: 1,
|
|
929
|
+
success: 0,
|
|
930
|
+
error: 0,
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
|
|
574
935
|
describe('getStats', () => {
|
|
575
936
|
it('should return zero stats when no messages exist', async () => {
|
|
576
937
|
const result = await readModel.getStats();
|
|
@@ -13,6 +13,11 @@ export interface CommandStats {
|
|
|
13
13
|
aggregateStatus: NodeStatus;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface RunStats {
|
|
17
|
+
items: { total: number; running: number; success: number; error: number; retried: number };
|
|
18
|
+
nodes: { total: number; running: number; success: number; error: number };
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export interface MessageStats {
|
|
17
22
|
totalMessages: number;
|
|
18
23
|
totalCommands: number;
|
|
@@ -115,6 +120,56 @@ export class PipelineReadModel {
|
|
|
115
120
|
};
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
async getAllItemStatuses(correlationId: string): Promise<ItemStatusDocument[]> {
|
|
124
|
+
const items = await this.itemStatusCollection.find((doc) => doc.correlationId === correlationId);
|
|
125
|
+
return items.map((item) => ({
|
|
126
|
+
correlationId: item.correlationId,
|
|
127
|
+
commandType: item.commandType,
|
|
128
|
+
itemKey: item.itemKey,
|
|
129
|
+
currentRequestId: item.currentRequestId,
|
|
130
|
+
status: item.status,
|
|
131
|
+
attemptCount: item.attemptCount,
|
|
132
|
+
startedAt: item.startedAt,
|
|
133
|
+
endedAt: item.endedAt,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getAllNodeStatuses(correlationId: string): Promise<NodeStatusDocument[]> {
|
|
138
|
+
const nodes = await this.nodeStatusCollection.find((doc) => doc.correlationId === correlationId);
|
|
139
|
+
return nodes.map((node) => ({
|
|
140
|
+
correlationId: node.correlationId,
|
|
141
|
+
commandName: node.commandName,
|
|
142
|
+
status: node.status,
|
|
143
|
+
pendingCount: node.pendingCount,
|
|
144
|
+
endedCount: node.endedCount,
|
|
145
|
+
lastDurationMs: node.lastDurationMs,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getRunStats(correlationId: string): Promise<RunStats> {
|
|
150
|
+
const items = await this.itemStatusCollection.find((doc) => doc.correlationId === correlationId);
|
|
151
|
+
const nodes = await this.nodeStatusCollection.find((doc) => doc.correlationId === correlationId);
|
|
152
|
+
|
|
153
|
+
const itemStats = { total: 0, running: 0, success: 0, error: 0, retried: 0 };
|
|
154
|
+
for (const item of items) {
|
|
155
|
+
itemStats.total++;
|
|
156
|
+
if (item.status === 'running') itemStats.running++;
|
|
157
|
+
else if (item.status === 'success') itemStats.success++;
|
|
158
|
+
else if (item.status === 'error') itemStats.error++;
|
|
159
|
+
if (item.attemptCount > 1) itemStats.retried++;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const nodeStats = { total: 0, running: 0, success: 0, error: 0 };
|
|
163
|
+
for (const node of nodes) {
|
|
164
|
+
nodeStats.total++;
|
|
165
|
+
if (node.status === 'running') nodeStats.running++;
|
|
166
|
+
else if (node.status === 'success') nodeStats.success++;
|
|
167
|
+
else if (node.status === 'error') nodeStats.error++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { items: itemStats, nodes: nodeStats };
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
async getMessages(correlationId?: string): Promise<MessageLogDocument[]> {
|
|
119
174
|
if (correlationId) {
|
|
120
175
|
return this.messageLogCollection.find((doc) => doc.correlationId === correlationId);
|