@auto-engineer/pipeline 0.0.1 → 0.15.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/CHANGELOG.md +26 -0
- package/README.md +279 -0
- package/dist/src/builder/define.d.ts +6 -2
- package/dist/src/builder/define.d.ts.map +1 -1
- package/dist/src/builder/define.js +17 -7
- package/dist/src/builder/define.js.map +1 -1
- package/dist/src/builder/define.specs.js +3 -3
- package/dist/src/builder/define.specs.js.map +1 -1
- package/dist/src/core/descriptors.d.ts +6 -2
- package/dist/src/core/descriptors.d.ts.map +1 -1
- package/dist/src/graph/filter-graph.d.ts +3 -0
- package/dist/src/graph/filter-graph.d.ts.map +1 -0
- package/dist/src/graph/filter-graph.js +80 -0
- package/dist/src/graph/filter-graph.js.map +1 -0
- package/dist/src/graph/filter-graph.specs.d.ts +2 -0
- package/dist/src/graph/filter-graph.specs.d.ts.map +1 -0
- package/dist/src/graph/filter-graph.specs.js +204 -0
- package/dist/src/graph/filter-graph.specs.js.map +1 -0
- package/dist/src/graph/types.d.ts +8 -0
- package/dist/src/graph/types.d.ts.map +1 -1
- 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/projections/await-tracker-projection.d.ts +31 -0
- package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
- package/dist/src/projections/await-tracker-projection.js +35 -0
- package/dist/src/projections/await-tracker-projection.js.map +1 -0
- package/dist/src/projections/index.d.ts +4 -0
- package/dist/src/projections/index.d.ts.map +1 -0
- package/dist/src/projections/index.js +4 -0
- package/dist/src/projections/index.js.map +1 -0
- package/dist/src/projections/item-status-projection.d.ts +22 -0
- package/dist/src/projections/item-status-projection.d.ts.map +1 -0
- package/dist/src/projections/item-status-projection.js +11 -0
- package/dist/src/projections/item-status-projection.js.map +1 -0
- package/dist/src/projections/item-status-projection.specs.d.ts +2 -0
- package/dist/src/projections/item-status-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/item-status-projection.specs.js +119 -0
- package/dist/src/projections/item-status-projection.specs.js.map +1 -0
- package/dist/src/projections/latest-run-projection.d.ts +15 -0
- package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
- package/dist/src/projections/latest-run-projection.js +7 -0
- package/dist/src/projections/latest-run-projection.js.map +1 -0
- package/dist/src/projections/latest-run-projection.specs.d.ts +2 -0
- package/dist/src/projections/latest-run-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/latest-run-projection.specs.js +33 -0
- package/dist/src/projections/latest-run-projection.specs.js.map +1 -0
- package/dist/src/projections/message-log-projection.d.ts +51 -0
- package/dist/src/projections/message-log-projection.d.ts.map +1 -0
- package/dist/src/projections/message-log-projection.js +51 -0
- package/dist/src/projections/message-log-projection.js.map +1 -0
- package/dist/src/projections/message-log-projection.specs.d.ts +2 -0
- package/dist/src/projections/message-log-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/message-log-projection.specs.js +101 -0
- package/dist/src/projections/message-log-projection.specs.js.map +1 -0
- package/dist/src/projections/node-status-projection.d.ts +23 -0
- package/dist/src/projections/node-status-projection.d.ts.map +1 -0
- package/dist/src/projections/node-status-projection.js +10 -0
- package/dist/src/projections/node-status-projection.js.map +1 -0
- package/dist/src/projections/node-status-projection.specs.d.ts +2 -0
- package/dist/src/projections/node-status-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/node-status-projection.specs.js +116 -0
- package/dist/src/projections/node-status-projection.specs.js.map +1 -0
- package/dist/src/projections/phased-execution-projection.d.ts +77 -0
- package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
- package/dist/src/projections/phased-execution-projection.js +54 -0
- package/dist/src/projections/phased-execution-projection.js.map +1 -0
- package/dist/src/projections/phased-execution-projection.specs.d.ts +2 -0
- package/dist/src/projections/phased-execution-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/phased-execution-projection.specs.js +171 -0
- package/dist/src/projections/phased-execution-projection.specs.js.map +1 -0
- package/dist/src/projections/settled-instance-projection.d.ts +67 -0
- package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
- package/dist/src/projections/settled-instance-projection.js +66 -0
- package/dist/src/projections/settled-instance-projection.js.map +1 -0
- package/dist/src/projections/settled-instance-projection.specs.d.ts +2 -0
- package/dist/src/projections/settled-instance-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/settled-instance-projection.specs.js +217 -0
- package/dist/src/projections/settled-instance-projection.specs.js.map +1 -0
- package/dist/src/projections/stats-projection.d.ts +9 -0
- package/dist/src/projections/stats-projection.d.ts.map +1 -0
- package/dist/src/projections/stats-projection.js +16 -0
- package/dist/src/projections/stats-projection.js.map +1 -0
- package/dist/src/projections/stats-projection.specs.d.ts +2 -0
- package/dist/src/projections/stats-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/stats-projection.specs.js +91 -0
- package/dist/src/projections/stats-projection.specs.js.map +1 -0
- package/dist/src/runtime/await-tracker.d.ts +17 -7
- package/dist/src/runtime/await-tracker.d.ts.map +1 -1
- package/dist/src/runtime/await-tracker.js +32 -29
- package/dist/src/runtime/await-tracker.js.map +1 -1
- package/dist/src/runtime/await-tracker.specs.js +56 -38
- package/dist/src/runtime/await-tracker.specs.js.map +1 -1
- package/dist/src/runtime/context.d.ts +1 -1
- package/dist/src/runtime/context.d.ts.map +1 -1
- package/dist/src/runtime/event-command-map.d.ts +3 -3
- package/dist/src/runtime/event-command-map.d.ts.map +1 -1
- package/dist/src/runtime/event-command-map.js +6 -2
- package/dist/src/runtime/event-command-map.js.map +1 -1
- package/dist/src/runtime/phased-executor.d.ts +15 -9
- package/dist/src/runtime/phased-executor.d.ts.map +1 -1
- package/dist/src/runtime/phased-executor.js +126 -104
- package/dist/src/runtime/phased-executor.js.map +1 -1
- package/dist/src/runtime/phased-executor.specs.js +243 -81
- package/dist/src/runtime/phased-executor.specs.js.map +1 -1
- package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
- package/dist/src/runtime/pipeline-runtime.js +2 -2
- package/dist/src/runtime/pipeline-runtime.js.map +1 -1
- package/dist/src/runtime/pipeline-runtime.specs.js +35 -0
- package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -1
- package/dist/src/runtime/settled-tracker.d.ts +12 -9
- package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
- package/dist/src/runtime/settled-tracker.js +92 -77
- package/dist/src/runtime/settled-tracker.js.map +1 -1
- package/dist/src/runtime/settled-tracker.specs.js +568 -118
- package/dist/src/runtime/settled-tracker.specs.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts +31 -9
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.e2e.specs.js +2 -10
- package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -1
- package/dist/src/server/pipeline-server.js +418 -134
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/server/pipeline-server.specs.js +777 -32
- package/dist/src/server/pipeline-server.specs.js.map +1 -1
- package/dist/src/server/sse-manager.specs.js +55 -35
- package/dist/src/server/sse-manager.specs.js.map +1 -1
- package/dist/src/store/index.d.ts +3 -0
- package/dist/src/store/index.d.ts.map +1 -0
- package/dist/src/store/index.js +3 -0
- package/dist/src/store/index.js.map +1 -0
- package/dist/src/store/pipeline-event-store.d.ts +10 -0
- package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
- package/dist/src/store/pipeline-event-store.js +112 -0
- package/dist/src/store/pipeline-event-store.js.map +1 -0
- package/dist/src/store/pipeline-event-store.specs.d.ts +2 -0
- package/dist/src/store/pipeline-event-store.specs.d.ts.map +1 -0
- package/dist/src/store/pipeline-event-store.specs.js +287 -0
- package/dist/src/store/pipeline-event-store.specs.js.map +1 -0
- package/dist/src/store/pipeline-read-model.d.ts +49 -0
- package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
- package/dist/src/store/pipeline-read-model.js +157 -0
- package/dist/src/store/pipeline-read-model.js.map +1 -0
- package/dist/src/store/pipeline-read-model.specs.d.ts +2 -0
- package/dist/src/store/pipeline-read-model.specs.d.ts.map +1 -0
- package/dist/src/store/pipeline-read-model.specs.js +830 -0
- package/dist/src/store/pipeline-read-model.specs.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
- package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
- package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
- package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +960 -0
- package/package.json +7 -3
- package/src/builder/define.specs.ts +3 -3
- package/src/builder/define.ts +24 -11
- package/src/core/descriptors.ts +7 -2
- package/src/graph/filter-graph.specs.ts +241 -0
- package/src/graph/filter-graph.ts +111 -0
- package/src/graph/types.ts +10 -0
- package/src/index.ts +1 -2
- package/src/projections/await-tracker-projection.ts +68 -0
- package/src/projections/index.ts +11 -0
- package/src/projections/item-status-projection.specs.ts +130 -0
- package/src/projections/item-status-projection.ts +32 -0
- package/src/projections/latest-run-projection.specs.ts +38 -0
- package/src/projections/latest-run-projection.ts +20 -0
- package/src/projections/message-log-projection.specs.ts +118 -0
- package/src/projections/message-log-projection.ts +113 -0
- package/src/projections/node-status-projection.specs.ts +127 -0
- package/src/projections/node-status-projection.ts +33 -0
- package/src/projections/phased-execution-projection.specs.ts +202 -0
- package/src/projections/phased-execution-projection.ts +146 -0
- package/src/projections/settled-instance-projection.specs.ts +249 -0
- package/src/projections/settled-instance-projection.ts +160 -0
- package/src/projections/stats-projection.specs.ts +105 -0
- package/src/projections/stats-projection.ts +26 -0
- package/src/runtime/await-tracker.specs.ts +57 -34
- package/src/runtime/await-tracker.ts +43 -31
- package/src/runtime/context.ts +1 -1
- package/src/runtime/event-command-map.ts +11 -4
- package/src/runtime/phased-executor.specs.ts +357 -81
- package/src/runtime/phased-executor.ts +142 -126
- package/src/runtime/pipeline-runtime.specs.ts +42 -0
- package/src/runtime/pipeline-runtime.ts +6 -4
- package/src/runtime/settled-tracker.specs.ts +716 -120
- package/src/runtime/settled-tracker.ts +104 -98
- package/src/server/pipeline-server.e2e.specs.ts +10 -16
- package/src/server/pipeline-server.specs.ts +964 -49
- package/src/server/pipeline-server.ts +522 -156
- package/src/server/sse-manager.specs.ts +67 -36
- package/src/store/index.ts +2 -0
- package/src/store/pipeline-event-store.specs.ts +309 -0
- package/src/store/pipeline-event-store.ts +156 -0
- package/src/store/pipeline-read-model.specs.ts +967 -0
- package/src/store/pipeline-read-model.ts +223 -0
- package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
- package/src/testing/fixtures/kanban.pipeline.ts +2 -2
- package/claude.md +0 -160
- package/docs/testing-analysis.md +0 -395
- package/pomodoro-plan.md +0 -651
|
@@ -6,31 +6,35 @@ import {
|
|
|
6
6
|
type Event,
|
|
7
7
|
type MessageBus,
|
|
8
8
|
} from '@auto-engineer/message-bus';
|
|
9
|
-
import { type ILocalMessageStore, MemoryMessageStore } from '@auto-engineer/message-store';
|
|
10
9
|
import cors from 'cors';
|
|
11
10
|
import express from 'express';
|
|
12
11
|
import getPort from 'get-port';
|
|
13
12
|
import { nanoid } from 'nanoid';
|
|
14
13
|
import type { Pipeline } from '../builder/define';
|
|
15
|
-
import
|
|
14
|
+
import { filterGraph } from '../graph/filter-graph';
|
|
15
|
+
import type { FilterOptions, GraphIR, GraphNode, NodeStatus, NodeType } from '../graph/types';
|
|
16
16
|
import type { PipelineContext } from '../runtime/context';
|
|
17
|
+
import type { EventDefinition } from '../runtime/event-command-map';
|
|
17
18
|
import { EventCommandMapper } from '../runtime/event-command-map';
|
|
18
19
|
import { PhasedExecutor } from '../runtime/phased-executor';
|
|
19
20
|
import { PipelineRuntime } from '../runtime/pipeline-runtime';
|
|
20
21
|
import { SettledTracker } from '../runtime/settled-tracker';
|
|
22
|
+
import { createPipelineEventStore, type PipelineEventStoreContext } from '../store/pipeline-event-store';
|
|
21
23
|
import { SSEManager } from './sse-manager';
|
|
22
24
|
|
|
25
|
+
export type { EventDefinition };
|
|
26
|
+
|
|
23
27
|
export interface CommandHandlerWithMetadata extends CommandHandler {
|
|
24
28
|
alias?: string;
|
|
25
29
|
description?: string;
|
|
30
|
+
displayName?: string;
|
|
26
31
|
fields?: Record<string, unknown>;
|
|
27
32
|
examples?: unknown[];
|
|
28
|
-
events?:
|
|
33
|
+
events?: EventDefinition[];
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export interface PipelineServerConfig {
|
|
32
37
|
port: number;
|
|
33
|
-
messageStore?: ILocalMessageStore;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
interface EventWithCorrelation extends Event {
|
|
@@ -41,17 +45,17 @@ export class PipelineServer {
|
|
|
41
45
|
private app: express.Application;
|
|
42
46
|
private httpServer: HttpServer;
|
|
43
47
|
private messageBus: MessageBus;
|
|
44
|
-
private messageStore: ILocalMessageStore;
|
|
45
48
|
private readonly commandHandlers: Map<string, CommandHandlerWithMetadata> = new Map();
|
|
46
49
|
private readonly pipelines: Map<string, Pipeline> = new Map();
|
|
47
50
|
private readonly runtimes: Map<string, PipelineRuntime> = new Map();
|
|
48
51
|
private actualPort: number;
|
|
49
52
|
private readonly requestedPort: number;
|
|
50
|
-
private currentSessionId?: string;
|
|
51
53
|
private readonly settledTracker: SettledTracker;
|
|
52
54
|
private readonly eventCommandMapper: EventCommandMapper;
|
|
53
55
|
private readonly phasedExecutor: PhasedExecutor;
|
|
54
56
|
private readonly sseManager: SSEManager;
|
|
57
|
+
private readonly eventStoreContext: PipelineEventStoreContext;
|
|
58
|
+
private readonly itemKeyExtractors = new Map<string, (data: unknown) => string | undefined>();
|
|
55
59
|
|
|
56
60
|
constructor(config: PipelineServerConfig) {
|
|
57
61
|
this.requestedPort = config.port;
|
|
@@ -61,20 +65,32 @@ export class PipelineServer {
|
|
|
61
65
|
this.app.use(express.json());
|
|
62
66
|
this.httpServer = createServer(this.app);
|
|
63
67
|
this.messageBus = createMessageBus();
|
|
64
|
-
this.
|
|
68
|
+
this.eventStoreContext = createPipelineEventStore();
|
|
65
69
|
this.eventCommandMapper = new EventCommandMapper([]);
|
|
66
70
|
this.settledTracker = new SettledTracker({
|
|
71
|
+
readModel: this.eventStoreContext.readModel,
|
|
67
72
|
onDispatch: (commandType, data, correlationId) => {
|
|
68
73
|
void this.dispatchFromSettled(commandType, data, correlationId);
|
|
69
74
|
},
|
|
75
|
+
onEventEmit: async (event) => {
|
|
76
|
+
const correlationId = event.data.correlationId;
|
|
77
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [event]);
|
|
78
|
+
},
|
|
70
79
|
});
|
|
71
80
|
this.phasedExecutor = new PhasedExecutor({
|
|
81
|
+
readModel: this.eventStoreContext.readModel,
|
|
72
82
|
onDispatch: (commandType, data, correlationId) => {
|
|
73
83
|
void this.dispatchFromSettled(commandType, data, correlationId);
|
|
74
84
|
},
|
|
75
85
|
onComplete: (event, correlationId) => {
|
|
76
86
|
void this.handlePhasedComplete(event, correlationId);
|
|
77
87
|
},
|
|
88
|
+
onEventEmit: async (event) => {
|
|
89
|
+
const data = event.data as Record<string, unknown>;
|
|
90
|
+
const correlationId =
|
|
91
|
+
(data.correlationId as string) ?? (data.executionId as string)?.split('-')[1] ?? 'default';
|
|
92
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [event]);
|
|
93
|
+
},
|
|
78
94
|
});
|
|
79
95
|
this.sseManager = new SSEManager();
|
|
80
96
|
this.setupRoutes();
|
|
@@ -96,6 +112,10 @@ export class PipelineServer {
|
|
|
96
112
|
return Array.from(this.commandHandlers.keys());
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
registerItemKeyExtractor(commandType: string, extractor: (data: unknown) => string | undefined): void {
|
|
116
|
+
this.itemKeyExtractors.set(commandType, extractor);
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
registerPipeline(pipeline: Pipeline): void {
|
|
100
120
|
this.pipelines.set(pipeline.descriptor.name, pipeline);
|
|
101
121
|
this.runtimes.set(pipeline.descriptor.name, new PipelineRuntime(pipeline.descriptor));
|
|
@@ -119,8 +139,6 @@ export class PipelineServer {
|
|
|
119
139
|
this.actualPort = await getPort();
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
this.currentSessionId = await this.messageStore.createSession();
|
|
123
|
-
|
|
124
142
|
await new Promise<void>((resolve) => {
|
|
125
143
|
this.httpServer.listen(this.actualPort, () => {
|
|
126
144
|
resolve();
|
|
@@ -130,9 +148,6 @@ export class PipelineServer {
|
|
|
130
148
|
|
|
131
149
|
async stop(): Promise<void> {
|
|
132
150
|
this.sseManager.closeAll();
|
|
133
|
-
if (this.currentSessionId !== undefined) {
|
|
134
|
-
await this.messageStore.endSession(this.currentSessionId);
|
|
135
|
-
}
|
|
136
151
|
await new Promise<void>((resolve) => {
|
|
137
152
|
this.httpServer.close(() => resolve());
|
|
138
153
|
});
|
|
@@ -176,27 +191,37 @@ export class PipelineServer {
|
|
|
176
191
|
});
|
|
177
192
|
});
|
|
178
193
|
|
|
179
|
-
this.app.get('/pipeline', (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
this.app.get('/pipeline', (req, res) => {
|
|
195
|
+
void (async () => {
|
|
196
|
+
const commandToEvents = this.buildCommandToEvents();
|
|
197
|
+
const rawGraph = this.buildCombinedGraph();
|
|
198
|
+
const pipelineEvents = this.extractPipelineEvents(rawGraph, commandToEvents);
|
|
199
|
+
const graphWithEvents = this.addCommandEventEdgesToGraph(rawGraph, commandToEvents, pipelineEvents);
|
|
200
|
+
const graphWithEnrichedEvents = this.enrichEventLabels(graphWithEvents);
|
|
201
|
+
const completeGraph = this.markBackLinks(graphWithEnrichedEvents);
|
|
202
|
+
const filterOptions = this.parseFilterOptions(req.query);
|
|
203
|
+
const filteredGraph = filterGraph(completeGraph, filterOptions);
|
|
204
|
+
const correlationId = req.query.correlationId as string | undefined;
|
|
205
|
+
const graphWithStatus = await this.addStatusToCommandNodes(filteredGraph, correlationId);
|
|
206
|
+
|
|
207
|
+
const latestRun = await this.eventStoreContext.readModel.getLatestCorrelationId();
|
|
208
|
+
res.json({
|
|
209
|
+
nodes: graphWithStatus.nodes,
|
|
210
|
+
edges: graphWithStatus.edges,
|
|
211
|
+
latestRun,
|
|
212
|
+
});
|
|
213
|
+
})();
|
|
191
214
|
});
|
|
192
215
|
|
|
193
|
-
this.app.get('/pipeline/mermaid', (
|
|
194
|
-
const
|
|
216
|
+
this.app.get('/pipeline/mermaid', (req, res) => {
|
|
217
|
+
const filterOptions = this.parseFilterOptions(req.query);
|
|
218
|
+
const mermaid = this.buildMermaidDiagram(filterOptions);
|
|
195
219
|
res.type('text/plain').send(mermaid);
|
|
196
220
|
});
|
|
197
221
|
|
|
198
|
-
this.app.get('/pipeline/diagram', (
|
|
199
|
-
const
|
|
222
|
+
this.app.get('/pipeline/diagram', (req, res) => {
|
|
223
|
+
const filterOptions = this.parseFilterOptions(req.query);
|
|
224
|
+
const mermaidDefinition = this.buildMermaidDiagram(filterOptions);
|
|
200
225
|
const html = this.buildDiagramHtml(mermaidDefinition);
|
|
201
226
|
res.type('text/html').send(html);
|
|
202
227
|
});
|
|
@@ -221,13 +246,14 @@ export class PipelineServer {
|
|
|
221
246
|
correlationId,
|
|
222
247
|
};
|
|
223
248
|
|
|
224
|
-
await this.
|
|
249
|
+
await this.emitCommandDispatched(correlationId, requestId, commandWithIds.type, commandWithIds.data);
|
|
225
250
|
|
|
226
251
|
void this.processCommand(commandWithIds);
|
|
227
252
|
|
|
228
253
|
res.json({
|
|
229
254
|
status: 'ack',
|
|
230
255
|
commandId: commandWithIds.requestId,
|
|
256
|
+
correlationId: commandWithIds.correlationId,
|
|
231
257
|
timestamp: new Date().toISOString(),
|
|
232
258
|
});
|
|
233
259
|
})();
|
|
@@ -235,11 +261,17 @@ export class PipelineServer {
|
|
|
235
261
|
|
|
236
262
|
this.app.get('/messages', (_req, res) => {
|
|
237
263
|
void (async () => {
|
|
238
|
-
const messages = await this.
|
|
239
|
-
const serialized = messages.map((m) => ({
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
264
|
+
const messages = await this.eventStoreContext.readModel.getMessages();
|
|
265
|
+
const serialized = messages.map((m, index) => ({
|
|
266
|
+
message: {
|
|
267
|
+
type: m.messageName,
|
|
268
|
+
data: m.messageData,
|
|
269
|
+
correlationId: m.correlationId,
|
|
270
|
+
requestId: m.requestId,
|
|
271
|
+
},
|
|
272
|
+
messageType: m.messageType,
|
|
273
|
+
revision: String(index),
|
|
274
|
+
position: String(index),
|
|
243
275
|
}));
|
|
244
276
|
res.json(serialized);
|
|
245
277
|
})();
|
|
@@ -247,18 +279,11 @@ export class PipelineServer {
|
|
|
247
279
|
|
|
248
280
|
this.app.get('/stats', (_req, res) => {
|
|
249
281
|
void (async () => {
|
|
250
|
-
const stats = await this.
|
|
282
|
+
const stats = await this.eventStoreContext.readModel.getStats();
|
|
251
283
|
res.json(stats);
|
|
252
284
|
})();
|
|
253
285
|
});
|
|
254
286
|
|
|
255
|
-
this.app.get('/sessions', (_req, res) => {
|
|
256
|
-
void (async () => {
|
|
257
|
-
const sessions = await this.messageStore.getSessions();
|
|
258
|
-
res.json(sessions);
|
|
259
|
-
})();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
287
|
this.app.get('/events', (req, res) => {
|
|
263
288
|
const clientId = `sse-${nanoid()}`;
|
|
264
289
|
const correlationIdFilter = req.query.correlationId as string | undefined;
|
|
@@ -281,47 +306,330 @@ export class PipelineServer {
|
|
|
281
306
|
combinedGraph.edges.push(...graph.edges);
|
|
282
307
|
}
|
|
283
308
|
|
|
284
|
-
return combinedGraph;
|
|
309
|
+
return this.enrichCommandLabels(combinedGraph);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private enrichCommandLabels(graph: GraphIR): GraphIR {
|
|
313
|
+
return {
|
|
314
|
+
nodes: graph.nodes.map((node) => {
|
|
315
|
+
if (node.type !== 'command') {
|
|
316
|
+
return node;
|
|
317
|
+
}
|
|
318
|
+
const handler = this.commandHandlers.get(node.label);
|
|
319
|
+
if (handler?.displayName === undefined) {
|
|
320
|
+
return node;
|
|
321
|
+
}
|
|
322
|
+
return { ...node, label: handler.displayName };
|
|
323
|
+
}),
|
|
324
|
+
edges: graph.edges,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private enrichEventLabels(graph: GraphIR): GraphIR {
|
|
329
|
+
const eventDisplayNames = this.buildEventDisplayNames();
|
|
330
|
+
return {
|
|
331
|
+
nodes: graph.nodes.map((node) => {
|
|
332
|
+
if (node.type !== 'event') {
|
|
333
|
+
return node;
|
|
334
|
+
}
|
|
335
|
+
const displayName = eventDisplayNames.get(node.label);
|
|
336
|
+
if (displayName === undefined) {
|
|
337
|
+
return node;
|
|
338
|
+
}
|
|
339
|
+
return { ...node, label: displayName };
|
|
340
|
+
}),
|
|
341
|
+
edges: graph.edges,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async addStatusToCommandNodes(graph: GraphIR, correlationId?: string): Promise<GraphIR> {
|
|
346
|
+
const nodesWithStatus = await Promise.all(
|
|
347
|
+
graph.nodes.map(async (node) => {
|
|
348
|
+
if (node.type === 'command') {
|
|
349
|
+
return this.addStatusToCommandNode(node, correlationId);
|
|
350
|
+
}
|
|
351
|
+
if (node.type === 'settled') {
|
|
352
|
+
return this.addStatusToSettledNode(node, correlationId);
|
|
353
|
+
}
|
|
354
|
+
return node;
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
nodes: nodesWithStatus,
|
|
359
|
+
edges: graph.edges,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async addStatusToCommandNode(node: GraphNode, correlationId?: string): Promise<GraphNode> {
|
|
364
|
+
const commandName = node.id.replace(/^cmd:/, '');
|
|
365
|
+
if (correlationId === undefined) {
|
|
366
|
+
return { ...node, status: 'idle' as NodeStatus, pendingCount: 0, endedCount: 0 };
|
|
367
|
+
}
|
|
368
|
+
const stats = await this.computeCommandStats(correlationId, commandName);
|
|
369
|
+
return {
|
|
370
|
+
...node,
|
|
371
|
+
status: stats.aggregateStatus,
|
|
372
|
+
pendingCount: stats.pendingCount,
|
|
373
|
+
endedCount: stats.endedCount,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private async addStatusToSettledNode(node: GraphNode, correlationId?: string): Promise<GraphNode> {
|
|
378
|
+
if (correlationId === undefined) {
|
|
379
|
+
return { ...node, status: 'idle' as NodeStatus, pendingCount: 0, endedCount: 0 };
|
|
380
|
+
}
|
|
381
|
+
const commandTypes = node.id.replace(/^settled:/, '');
|
|
382
|
+
const templateId = `template-${commandTypes}`;
|
|
383
|
+
const stats = await this.eventStoreContext.readModel.computeSettledStats(correlationId, templateId);
|
|
384
|
+
return {
|
|
385
|
+
...node,
|
|
386
|
+
status: stats.status,
|
|
387
|
+
pendingCount: stats.pendingCount,
|
|
388
|
+
endedCount: stats.endedCount,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async emitItemStatusChanged(
|
|
393
|
+
correlationId: string,
|
|
394
|
+
commandType: string,
|
|
395
|
+
itemKey: string,
|
|
396
|
+
requestId: string,
|
|
397
|
+
status: 'running' | 'success' | 'error',
|
|
398
|
+
attemptCount: number,
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
401
|
+
{
|
|
402
|
+
type: 'ItemStatusChanged',
|
|
403
|
+
data: {
|
|
404
|
+
correlationId,
|
|
405
|
+
commandType,
|
|
406
|
+
itemKey,
|
|
407
|
+
requestId,
|
|
408
|
+
status,
|
|
409
|
+
attemptCount,
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async emitNodeStatusChanged(
|
|
416
|
+
correlationId: string,
|
|
417
|
+
commandName: string,
|
|
418
|
+
status: NodeStatus,
|
|
419
|
+
previousStatus: NodeStatus,
|
|
420
|
+
): Promise<void> {
|
|
421
|
+
const stats = await this.computeCommandStats(correlationId, commandName);
|
|
422
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
423
|
+
{
|
|
424
|
+
type: 'NodeStatusChanged',
|
|
425
|
+
data: {
|
|
426
|
+
correlationId,
|
|
427
|
+
commandName,
|
|
428
|
+
nodeId: `cmd:${commandName}`,
|
|
429
|
+
status,
|
|
430
|
+
previousStatus,
|
|
431
|
+
pendingCount: stats.pendingCount,
|
|
432
|
+
endedCount: stats.endedCount,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async emitCommandDispatched(
|
|
439
|
+
correlationId: string,
|
|
440
|
+
requestId: string,
|
|
441
|
+
commandType: string,
|
|
442
|
+
commandData: Record<string, unknown>,
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
445
|
+
{
|
|
446
|
+
type: 'CommandDispatched',
|
|
447
|
+
data: {
|
|
448
|
+
correlationId,
|
|
449
|
+
requestId,
|
|
450
|
+
commandType,
|
|
451
|
+
commandData,
|
|
452
|
+
timestamp: new Date(),
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private async emitDomainEventEmitted(
|
|
459
|
+
correlationId: string,
|
|
460
|
+
requestId: string,
|
|
461
|
+
eventType: string,
|
|
462
|
+
eventData: Record<string, unknown>,
|
|
463
|
+
): Promise<void> {
|
|
464
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
465
|
+
{
|
|
466
|
+
type: 'DomainEventEmitted',
|
|
467
|
+
data: {
|
|
468
|
+
correlationId,
|
|
469
|
+
requestId,
|
|
470
|
+
eventType,
|
|
471
|
+
eventData,
|
|
472
|
+
timestamp: new Date(),
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private async emitPipelineRunStarted(correlationId: string, triggerCommand: string): Promise<void> {
|
|
479
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
480
|
+
{
|
|
481
|
+
type: 'PipelineRunStarted',
|
|
482
|
+
data: {
|
|
483
|
+
correlationId,
|
|
484
|
+
triggerCommand,
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
]);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private async updateNodeStatus(correlationId: string, commandName: string, status: NodeStatus): Promise<void> {
|
|
491
|
+
const existing = await this.eventStoreContext.readModel.getNodeStatus(correlationId, commandName);
|
|
492
|
+
const previousStatus: NodeStatus = existing?.status ?? 'idle';
|
|
493
|
+
await this.emitNodeStatusChanged(correlationId, commandName, status, previousStatus);
|
|
494
|
+
await this.broadcastNodeStatusChanged(correlationId, commandName, status, previousStatus);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async broadcastNodeStatusChanged(
|
|
498
|
+
correlationId: string,
|
|
499
|
+
commandName: string,
|
|
500
|
+
status: NodeStatus,
|
|
501
|
+
previousStatus: NodeStatus,
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
const stats = await this.computeCommandStats(correlationId, commandName);
|
|
504
|
+
const event: Event & { correlationId: string } = {
|
|
505
|
+
type: 'NodeStatusChanged',
|
|
506
|
+
data: {
|
|
507
|
+
nodeId: `cmd:${commandName}`,
|
|
508
|
+
status,
|
|
509
|
+
previousStatus,
|
|
510
|
+
pendingCount: stats.pendingCount,
|
|
511
|
+
endedCount: stats.endedCount,
|
|
512
|
+
},
|
|
513
|
+
correlationId,
|
|
514
|
+
};
|
|
515
|
+
this.sseManager.broadcast(event);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private async broadcastPipelineRunStarted(correlationId: string, triggerCommand: string): Promise<void> {
|
|
519
|
+
const event: Event & { correlationId: string } = {
|
|
520
|
+
type: 'PipelineRunStarted',
|
|
521
|
+
data: { correlationId, triggerCommand },
|
|
522
|
+
correlationId,
|
|
523
|
+
};
|
|
524
|
+
this.sseManager.broadcast(event);
|
|
525
|
+
await this.emitPipelineRunStarted(correlationId, triggerCommand);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private extractItemKey(commandType: string, data: unknown, requestId: string): string {
|
|
529
|
+
const extractor = this.itemKeyExtractors.get(commandType);
|
|
530
|
+
if (extractor !== undefined) {
|
|
531
|
+
const key = extractor(data);
|
|
532
|
+
if (key !== undefined) return key;
|
|
533
|
+
}
|
|
534
|
+
return requestId;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private async getOrCreateItemStatus(
|
|
538
|
+
correlationId: string,
|
|
539
|
+
commandType: string,
|
|
540
|
+
itemKey: string,
|
|
541
|
+
requestId: string,
|
|
542
|
+
): Promise<{ attemptCount: number }> {
|
|
543
|
+
const existing = await this.eventStoreContext.readModel.getItemStatus(correlationId, commandType, itemKey);
|
|
544
|
+
const attemptCount = (existing?.attemptCount ?? 0) + 1;
|
|
545
|
+
|
|
546
|
+
await this.emitItemStatusChanged(correlationId, commandType, itemKey, requestId, 'running', attemptCount);
|
|
547
|
+
|
|
548
|
+
return { attemptCount };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private async updateItemStatus(
|
|
552
|
+
correlationId: string,
|
|
553
|
+
commandType: string,
|
|
554
|
+
itemKey: string,
|
|
555
|
+
status: 'running' | 'success' | 'error',
|
|
556
|
+
): Promise<void> {
|
|
557
|
+
const existing = await this.eventStoreContext.readModel.getItemStatus(correlationId, commandType, itemKey);
|
|
558
|
+
if (existing !== null) {
|
|
559
|
+
await this.emitItemStatusChanged(
|
|
560
|
+
correlationId,
|
|
561
|
+
commandType,
|
|
562
|
+
itemKey,
|
|
563
|
+
existing.currentRequestId,
|
|
564
|
+
status,
|
|
565
|
+
existing.attemptCount,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private async computeCommandStats(
|
|
571
|
+
correlationId: string,
|
|
572
|
+
commandType: string,
|
|
573
|
+
): Promise<{ pendingCount: number; endedCount: number; aggregateStatus: NodeStatus }> {
|
|
574
|
+
return this.eventStoreContext.readModel.computeCommandStats(correlationId, commandType);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private getEventName(event: EventDefinition): string {
|
|
578
|
+
return typeof event === 'string' ? event : event.name;
|
|
285
579
|
}
|
|
286
580
|
|
|
287
581
|
private buildCommandToEvents(): Record<string, string[]> {
|
|
288
582
|
const commandToEvents: Record<string, string[]> = {};
|
|
289
583
|
for (const [name, handler] of this.commandHandlers.entries()) {
|
|
290
584
|
if (handler.events !== undefined && Array.isArray(handler.events)) {
|
|
291
|
-
commandToEvents[name] = handler.events;
|
|
585
|
+
commandToEvents[name] = handler.events.map((e) => this.getEventName(e));
|
|
292
586
|
}
|
|
293
587
|
}
|
|
294
588
|
return commandToEvents;
|
|
295
589
|
}
|
|
296
590
|
|
|
297
|
-
private
|
|
298
|
-
const
|
|
299
|
-
for (const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
591
|
+
private buildEventDisplayNames(): Map<string, string> {
|
|
592
|
+
const eventDisplayNames = new Map<string, string>();
|
|
593
|
+
for (const handler of this.commandHandlers.values()) {
|
|
594
|
+
if (handler.events === undefined) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
for (const event of handler.events) {
|
|
598
|
+
if (typeof event !== 'string' && event.displayName !== undefined) {
|
|
599
|
+
eventDisplayNames.set(event.name, event.displayName);
|
|
305
600
|
}
|
|
306
601
|
}
|
|
307
602
|
}
|
|
308
|
-
return
|
|
603
|
+
return eventDisplayNames;
|
|
309
604
|
}
|
|
310
605
|
|
|
311
|
-
private
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
606
|
+
private parseFilterOptions(query: Record<string, unknown>): FilterOptions {
|
|
607
|
+
const excludeTypesParam = query.excludeTypes;
|
|
608
|
+
const maintainEdgesParam = query.maintainEdges;
|
|
609
|
+
|
|
610
|
+
const excludeTypes: NodeType[] = [];
|
|
611
|
+
if (typeof excludeTypesParam === 'string' && excludeTypesParam.length > 0) {
|
|
612
|
+
const types = excludeTypesParam.split(',');
|
|
613
|
+
for (const t of types) {
|
|
614
|
+
if (t === 'event' || t === 'command' || t === 'settled') {
|
|
615
|
+
excludeTypes.push(t);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const maintainEdges = maintainEdgesParam === 'true';
|
|
621
|
+
|
|
622
|
+
return { excludeTypes, maintainEdges };
|
|
319
623
|
}
|
|
320
624
|
|
|
321
|
-
private buildMermaidDiagram(): string {
|
|
322
|
-
const graph = this.buildCombinedGraph();
|
|
625
|
+
private buildMermaidDiagram(filterOptions?: FilterOptions): string {
|
|
323
626
|
const commandToEvents = this.buildCommandToEvents();
|
|
324
|
-
const
|
|
627
|
+
const rawGraph = this.buildCombinedGraph();
|
|
628
|
+
const pipelineEvents = this.extractPipelineEvents(rawGraph, commandToEvents);
|
|
629
|
+
const graphWithEvents = this.addCommandEventEdgesToGraph(rawGraph, commandToEvents, pipelineEvents);
|
|
630
|
+
const graphWithEnrichedEvents = this.enrichEventLabels(graphWithEvents);
|
|
631
|
+
const completeGraph = this.markBackLinks(graphWithEnrichedEvents);
|
|
632
|
+
const graph = filterOptions ? filterGraph(completeGraph, filterOptions) : completeGraph;
|
|
325
633
|
const lines: string[] = ['flowchart LR'];
|
|
326
634
|
|
|
327
635
|
const eventNodes = new Set<string>();
|
|
@@ -330,15 +638,101 @@ export class PipelineServer {
|
|
|
330
638
|
const edgeContext = { index: 0, backLinkIndices: [] as number[] };
|
|
331
639
|
|
|
332
640
|
this.addGraphNodesToMermaid(graph, lines, eventNodes, commandNodes, settledNodes);
|
|
333
|
-
|
|
334
|
-
this.addCommandEventNodesToMermaid(commandToEvents, pipelineCommands, pipelineEvents, lines, eventNodes);
|
|
335
|
-
this.addGraphEdgesToMermaid(graph, commandToEvents, lines, edgeContext);
|
|
336
|
-
this.addCommandEventEdgesToMermaid(commandToEvents, pipelineCommands, pipelineEvents, lines, edgeContext);
|
|
641
|
+
this.addGraphEdgesToMermaid(graph, lines, edgeContext);
|
|
337
642
|
this.addMermaidStyles(lines, eventNodes, commandNodes, settledNodes, edgeContext.backLinkIndices);
|
|
338
643
|
|
|
339
644
|
return lines.join('\n');
|
|
340
645
|
}
|
|
341
646
|
|
|
647
|
+
private addCommandEventEdgesToGraph(
|
|
648
|
+
graph: GraphIR,
|
|
649
|
+
commandToEvents: Record<string, string[]>,
|
|
650
|
+
pipelineEvents: Set<string>,
|
|
651
|
+
): GraphIR {
|
|
652
|
+
const commandNodes = new Set(graph.nodes.filter((n) => n.type === 'command').map((n) => n.id.replace('cmd:', '')));
|
|
653
|
+
const existingEventIds = new Set(graph.nodes.filter((n) => n.type === 'event').map((n) => n.id));
|
|
654
|
+
const newNodes = [...graph.nodes];
|
|
655
|
+
const newEdges = [...graph.edges];
|
|
656
|
+
|
|
657
|
+
for (const [commandName, events] of Object.entries(commandToEvents)) {
|
|
658
|
+
if (!commandNodes.has(commandName)) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
for (const eventName of events) {
|
|
662
|
+
if (!pipelineEvents.has(eventName)) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const eventId = `evt:${eventName}`;
|
|
666
|
+
if (!existingEventIds.has(eventId)) {
|
|
667
|
+
newNodes.push({ id: eventId, type: 'event', label: eventName });
|
|
668
|
+
existingEventIds.add(eventId);
|
|
669
|
+
}
|
|
670
|
+
newEdges.push({ from: `cmd:${commandName}`, to: eventId });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return { nodes: newNodes, edges: newEdges };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private markBackLinks(graph: GraphIR): GraphIR {
|
|
678
|
+
const outgoingEdgesWithBackLink = new Map<string, Array<{ to: string; isBackLink: boolean }>>();
|
|
679
|
+
for (const edge of graph.edges) {
|
|
680
|
+
const existing = outgoingEdgesWithBackLink.get(edge.from) ?? [];
|
|
681
|
+
existing.push({ to: edge.to, isBackLink: edge.backLink === true });
|
|
682
|
+
outgoingEdgesWithBackLink.set(edge.from, existing);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const markedEdges = graph.edges.map((edge) => {
|
|
686
|
+
if (edge.backLink === true) {
|
|
687
|
+
return edge;
|
|
688
|
+
}
|
|
689
|
+
if (edge.from.startsWith('evt:') && edge.to.startsWith('cmd:')) {
|
|
690
|
+
const createsBackLink = this.hasPathToExcludingBackLinks(edge.to, edge.from, outgoingEdgesWithBackLink);
|
|
691
|
+
if (createsBackLink) {
|
|
692
|
+
return { ...edge, backLink: true };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return edge;
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
return { nodes: graph.nodes, edges: markedEdges };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private hasPathToExcludingBackLinks(
|
|
702
|
+
from: string,
|
|
703
|
+
target: string,
|
|
704
|
+
outgoingEdges: Map<string, Array<{ to: string; isBackLink: boolean }>>,
|
|
705
|
+
): boolean {
|
|
706
|
+
const visited = new Set<string>();
|
|
707
|
+
const queue = [from];
|
|
708
|
+
|
|
709
|
+
while (queue.length > 0) {
|
|
710
|
+
const current = queue.shift();
|
|
711
|
+
if (current === undefined) {
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
if (current === target) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
if (visited.has(current)) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
visited.add(current);
|
|
721
|
+
|
|
722
|
+
const neighbors = outgoingEdges.get(current) ?? [];
|
|
723
|
+
for (const neighbor of neighbors) {
|
|
724
|
+
if (neighbor.isBackLink) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (!visited.has(neighbor.to)) {
|
|
728
|
+
queue.push(neighbor.to);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
|
|
342
736
|
private extractPipelineEvents(graph: GraphIR, commandToEvents: Record<string, string[]>): Set<string> {
|
|
343
737
|
const pipelineEvents = new Set<string>();
|
|
344
738
|
|
|
@@ -374,11 +768,11 @@ export class PipelineServer {
|
|
|
374
768
|
const eventName = node.id.replace('evt:', '');
|
|
375
769
|
const safeId = `evt_${eventName}`;
|
|
376
770
|
eventNodes.add(safeId);
|
|
377
|
-
lines.push(` ${safeId}([${
|
|
771
|
+
lines.push(` ${safeId}([${node.label}])`);
|
|
378
772
|
} else if (node.id.startsWith('cmd:')) {
|
|
379
773
|
const commandName = node.id.replace('cmd:', '');
|
|
380
774
|
commandNodes.add(commandName);
|
|
381
|
-
lines.push(` ${commandName}[${
|
|
775
|
+
lines.push(` ${commandName}[${node.label}]`);
|
|
382
776
|
} else if (node.id.startsWith('settled:')) {
|
|
383
777
|
const commandTypes = node.id.replace('settled:', '').split(',');
|
|
384
778
|
const safeId = `settled_${commandTypes.join('_')}`;
|
|
@@ -388,55 +782,19 @@ export class PipelineServer {
|
|
|
388
782
|
}
|
|
389
783
|
}
|
|
390
784
|
|
|
391
|
-
private addCommandEventNodesToMermaid(
|
|
392
|
-
commandToEvents: Record<string, string[]>,
|
|
393
|
-
pipelineCommands: Set<string>,
|
|
394
|
-
pipelineEvents: Set<string>,
|
|
395
|
-
lines: string[],
|
|
396
|
-
eventNodes: Set<string>,
|
|
397
|
-
): void {
|
|
398
|
-
for (const [commandName, events] of Object.entries(commandToEvents)) {
|
|
399
|
-
if (!pipelineCommands.has(commandName)) {
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
for (const eventName of events) {
|
|
403
|
-
if (!pipelineEvents.has(eventName)) {
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
const safeEventId = `evt_${eventName}`;
|
|
407
|
-
if (!eventNodes.has(safeEventId)) {
|
|
408
|
-
eventNodes.add(safeEventId);
|
|
409
|
-
lines.push(` ${safeEventId}([${eventName}])`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
785
|
private addGraphEdgesToMermaid(
|
|
416
786
|
graph: GraphIR,
|
|
417
|
-
commandToEvents: Record<string, string[]>,
|
|
418
787
|
lines: string[],
|
|
419
788
|
edgeContext: { index: number; backLinkIndices: number[] },
|
|
420
789
|
): void {
|
|
421
790
|
for (const edge of graph.edges) {
|
|
422
|
-
if (edge.from.startsWith('cmd:') && edge.to.startsWith('settled:')) {
|
|
423
|
-
const commandType = edge.from.replace('cmd:', '');
|
|
424
|
-
const commandTypes = edge.to.replace('settled:', '').split(',');
|
|
425
|
-
const settledId = `settled_${commandTypes.join('_')}`;
|
|
426
|
-
const events = commandToEvents[commandType];
|
|
427
|
-
if (events !== undefined) {
|
|
428
|
-
for (const eventName of events) {
|
|
429
|
-
lines.push(` evt_${eventName} --> ${settledId}`);
|
|
430
|
-
edgeContext.index++;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
791
|
const from = this.normalizeNodeId(edge.from);
|
|
436
792
|
const to = this.normalizeNodeId(edge.to);
|
|
437
|
-
lines.push(` ${from} --> ${to}`);
|
|
438
793
|
if (edge.backLink === true) {
|
|
794
|
+
lines.push(` ${from} -.->|retry| ${to}`);
|
|
439
795
|
edgeContext.backLinkIndices.push(edgeContext.index);
|
|
796
|
+
} else {
|
|
797
|
+
lines.push(` ${from} --> ${to}`);
|
|
440
798
|
}
|
|
441
799
|
edgeContext.index++;
|
|
442
800
|
}
|
|
@@ -453,27 +811,6 @@ export class PipelineServer {
|
|
|
453
811
|
return `settled_${commandTypes.join('_')}`;
|
|
454
812
|
}
|
|
455
813
|
|
|
456
|
-
private addCommandEventEdgesToMermaid(
|
|
457
|
-
commandToEvents: Record<string, string[]>,
|
|
458
|
-
pipelineCommands: Set<string>,
|
|
459
|
-
pipelineEvents: Set<string>,
|
|
460
|
-
lines: string[],
|
|
461
|
-
edgeContext: { index: number; backLinkIndices: number[] },
|
|
462
|
-
): void {
|
|
463
|
-
for (const [commandName, events] of Object.entries(commandToEvents)) {
|
|
464
|
-
if (!pipelineCommands.has(commandName)) {
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
for (const eventName of events) {
|
|
468
|
-
if (!pipelineEvents.has(eventName)) {
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
lines.push(` ${commandName} --> evt_${eventName}`);
|
|
472
|
-
edgeContext.index++;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
814
|
private addMermaidStyles(
|
|
478
815
|
lines: string[],
|
|
479
816
|
eventNodes: Set<string>,
|
|
@@ -511,97 +848,126 @@ export class PipelineServer {
|
|
|
511
848
|
const handler = this.commandHandlers.get(command.type);
|
|
512
849
|
if (!handler) return;
|
|
513
850
|
|
|
514
|
-
this.
|
|
851
|
+
const isNewCorrelationId = !(await this.eventStoreContext.readModel.hasCorrelation(command.correlationId));
|
|
852
|
+
if (isNewCorrelationId) {
|
|
853
|
+
await this.broadcastPipelineRunStarted(command.correlationId, command.type);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const itemKey = this.extractItemKey(command.type, command.data, command.requestId);
|
|
857
|
+
await this.getOrCreateItemStatus(command.correlationId, command.type, itemKey, command.requestId);
|
|
858
|
+
|
|
859
|
+
await this.updateNodeStatus(command.correlationId, command.type, 'running');
|
|
860
|
+
await this.settledTracker.onCommandStarted(command);
|
|
515
861
|
|
|
516
862
|
const resultEvent = await handler.handle(command);
|
|
517
863
|
const events = Array.isArray(resultEvent) ? resultEvent : [resultEvent];
|
|
518
864
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
865
|
+
const finalStatus = this.getStatusFromEvents(events);
|
|
866
|
+
const itemFinalStatus = finalStatus === 'idle' ? 'success' : finalStatus;
|
|
867
|
+
await this.updateItemStatus(command.correlationId, command.type, itemKey, itemFinalStatus);
|
|
868
|
+
await this.updateNodeStatus(command.correlationId, command.type, finalStatus);
|
|
869
|
+
|
|
870
|
+
const eventsWithIds: EventWithCorrelation[] = events.map((event) => ({
|
|
871
|
+
...event,
|
|
872
|
+
correlationId: command.correlationId,
|
|
873
|
+
requestId: command.requestId,
|
|
874
|
+
}));
|
|
525
875
|
|
|
526
|
-
|
|
876
|
+
await Promise.all(
|
|
877
|
+
eventsWithIds.map((e) =>
|
|
878
|
+
this.emitDomainEventEmitted(e.correlationId, command.requestId, e.type, e.data as Record<string, unknown>),
|
|
879
|
+
),
|
|
880
|
+
);
|
|
527
881
|
|
|
882
|
+
for (const eventWithIds of eventsWithIds) {
|
|
528
883
|
this.sseManager.broadcast(eventWithIds);
|
|
529
884
|
|
|
530
|
-
const sourceCommand = this.eventCommandMapper.getSourceCommand(
|
|
885
|
+
const sourceCommand = this.eventCommandMapper.getSourceCommand(eventWithIds.type);
|
|
531
886
|
if (sourceCommand !== undefined) {
|
|
532
|
-
this.settledTracker.onEventReceived(eventWithIds, sourceCommand);
|
|
887
|
+
await this.settledTracker.onEventReceived(eventWithIds, sourceCommand);
|
|
533
888
|
}
|
|
534
889
|
|
|
535
|
-
this.routeEventToPhasedExecutor(eventWithIds);
|
|
890
|
+
await this.routeEventToPhasedExecutor(eventWithIds);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
await Promise.all(eventsWithIds.map((e) => this.routeEventToPipelines(e)));
|
|
894
|
+
}
|
|
536
895
|
|
|
537
|
-
|
|
896
|
+
private getStatusFromEvents(events: Event[]): NodeStatus {
|
|
897
|
+
for (const event of events) {
|
|
898
|
+
if (event.type.includes('Failed')) {
|
|
899
|
+
return 'error';
|
|
900
|
+
}
|
|
538
901
|
}
|
|
902
|
+
return 'success';
|
|
539
903
|
}
|
|
540
904
|
|
|
541
905
|
private async dispatchFromSettled(commandType: string, data: unknown, correlationId: string): Promise<void> {
|
|
906
|
+
const requestId = `req-${nanoid()}`;
|
|
542
907
|
const command: Command & { correlationId: string; requestId: string } = {
|
|
543
908
|
type: commandType,
|
|
544
909
|
data: data as Record<string, unknown>,
|
|
545
910
|
correlationId,
|
|
546
|
-
requestId
|
|
911
|
+
requestId,
|
|
547
912
|
};
|
|
548
|
-
await this.
|
|
913
|
+
await this.emitCommandDispatched(correlationId, requestId, commandType, data as Record<string, unknown>);
|
|
549
914
|
await this.processCommand(command);
|
|
550
915
|
}
|
|
551
916
|
|
|
552
917
|
private async handlePhasedComplete(event: Event, correlationId: string): Promise<void> {
|
|
918
|
+
const requestId = `req-${nanoid()}`;
|
|
553
919
|
const eventWithIds: EventWithCorrelation = {
|
|
554
920
|
...event,
|
|
555
921
|
correlationId,
|
|
556
922
|
};
|
|
557
|
-
await this.
|
|
923
|
+
await this.emitDomainEventEmitted(correlationId, requestId, event.type, event.data as Record<string, unknown>);
|
|
558
924
|
this.sseManager.broadcast(eventWithIds);
|
|
559
925
|
await this.routeEventToPipelines(eventWithIds);
|
|
560
926
|
}
|
|
561
927
|
|
|
562
928
|
private async routeEventToPipelines(event: EventWithCorrelation): Promise<void> {
|
|
563
929
|
const ctx = this.createContext(event.correlationId);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await runtime.handleEvent(event, ctx);
|
|
567
|
-
}
|
|
930
|
+
const runtimes = Array.from(this.runtimes.values());
|
|
931
|
+
await Promise.all(runtimes.map((runtime) => runtime.handleEvent(event, ctx)));
|
|
568
932
|
}
|
|
569
933
|
|
|
570
934
|
private createContext(correlationId: string): PipelineContext {
|
|
571
935
|
return {
|
|
572
936
|
correlationId,
|
|
573
937
|
emit: async (type: string, data: unknown) => {
|
|
938
|
+
const requestId = `req-${nanoid()}`;
|
|
574
939
|
const event: EventWithCorrelation = {
|
|
575
940
|
type,
|
|
576
941
|
data: data as Record<string, unknown>,
|
|
577
942
|
correlationId,
|
|
578
943
|
};
|
|
579
|
-
await this.
|
|
944
|
+
await this.emitDomainEventEmitted(correlationId, requestId, type, data as Record<string, unknown>);
|
|
580
945
|
this.sseManager.broadcast(event);
|
|
581
946
|
await this.routeEventToPipelines(event);
|
|
582
947
|
},
|
|
583
948
|
sendCommand: async (type: string, data: unknown) => {
|
|
949
|
+
const requestId = `req-${nanoid()}`;
|
|
584
950
|
const command: Command & { correlationId: string; requestId: string } = {
|
|
585
951
|
type,
|
|
586
952
|
data: data as Record<string, unknown>,
|
|
587
953
|
correlationId,
|
|
588
|
-
requestId
|
|
954
|
+
requestId,
|
|
589
955
|
};
|
|
590
|
-
await this.
|
|
956
|
+
await this.emitCommandDispatched(correlationId, requestId, type, data as Record<string, unknown>);
|
|
591
957
|
await this.processCommand(command);
|
|
592
958
|
},
|
|
593
|
-
startPhased: (handler, event) => {
|
|
594
|
-
this.phasedExecutor.startPhased(handler, event, correlationId);
|
|
959
|
+
startPhased: async (handler, event) => {
|
|
960
|
+
await this.phasedExecutor.startPhased(handler, event, correlationId);
|
|
595
961
|
},
|
|
596
962
|
};
|
|
597
963
|
}
|
|
598
964
|
|
|
599
|
-
private routeEventToPhasedExecutor(event: EventWithCorrelation): void {
|
|
965
|
+
private async routeEventToPhasedExecutor(event: EventWithCorrelation): Promise<void> {
|
|
600
966
|
for (const pipeline of this.pipelines.values()) {
|
|
601
967
|
for (const handler of pipeline.descriptor.handlers) {
|
|
602
968
|
if (handler.type === 'foreach-phased') {
|
|
603
969
|
const itemKey = handler.completion.itemKey(event);
|
|
604
|
-
this.phasedExecutor.onEventReceived(event, itemKey);
|
|
970
|
+
await this.phasedExecutor.onEventReceived(event, itemKey);
|
|
605
971
|
}
|
|
606
972
|
}
|
|
607
973
|
}
|