@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.
Files changed (201) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +279 -0
  4. package/dist/src/builder/define.d.ts +6 -2
  5. package/dist/src/builder/define.d.ts.map +1 -1
  6. package/dist/src/builder/define.js +17 -7
  7. package/dist/src/builder/define.js.map +1 -1
  8. package/dist/src/builder/define.specs.js +3 -3
  9. package/dist/src/builder/define.specs.js.map +1 -1
  10. package/dist/src/core/descriptors.d.ts +6 -2
  11. package/dist/src/core/descriptors.d.ts.map +1 -1
  12. package/dist/src/graph/filter-graph.d.ts +3 -0
  13. package/dist/src/graph/filter-graph.d.ts.map +1 -0
  14. package/dist/src/graph/filter-graph.js +80 -0
  15. package/dist/src/graph/filter-graph.js.map +1 -0
  16. package/dist/src/graph/filter-graph.specs.d.ts +2 -0
  17. package/dist/src/graph/filter-graph.specs.d.ts.map +1 -0
  18. package/dist/src/graph/filter-graph.specs.js +204 -0
  19. package/dist/src/graph/filter-graph.specs.js.map +1 -0
  20. package/dist/src/graph/types.d.ts +8 -0
  21. package/dist/src/graph/types.d.ts.map +1 -1
  22. package/dist/src/index.d.ts +1 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/projections/await-tracker-projection.d.ts +31 -0
  26. package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
  27. package/dist/src/projections/await-tracker-projection.js +35 -0
  28. package/dist/src/projections/await-tracker-projection.js.map +1 -0
  29. package/dist/src/projections/index.d.ts +4 -0
  30. package/dist/src/projections/index.d.ts.map +1 -0
  31. package/dist/src/projections/index.js +4 -0
  32. package/dist/src/projections/index.js.map +1 -0
  33. package/dist/src/projections/item-status-projection.d.ts +22 -0
  34. package/dist/src/projections/item-status-projection.d.ts.map +1 -0
  35. package/dist/src/projections/item-status-projection.js +11 -0
  36. package/dist/src/projections/item-status-projection.js.map +1 -0
  37. package/dist/src/projections/item-status-projection.specs.d.ts +2 -0
  38. package/dist/src/projections/item-status-projection.specs.d.ts.map +1 -0
  39. package/dist/src/projections/item-status-projection.specs.js +119 -0
  40. package/dist/src/projections/item-status-projection.specs.js.map +1 -0
  41. package/dist/src/projections/latest-run-projection.d.ts +15 -0
  42. package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
  43. package/dist/src/projections/latest-run-projection.js +7 -0
  44. package/dist/src/projections/latest-run-projection.js.map +1 -0
  45. package/dist/src/projections/latest-run-projection.specs.d.ts +2 -0
  46. package/dist/src/projections/latest-run-projection.specs.d.ts.map +1 -0
  47. package/dist/src/projections/latest-run-projection.specs.js +33 -0
  48. package/dist/src/projections/latest-run-projection.specs.js.map +1 -0
  49. package/dist/src/projections/message-log-projection.d.ts +51 -0
  50. package/dist/src/projections/message-log-projection.d.ts.map +1 -0
  51. package/dist/src/projections/message-log-projection.js +51 -0
  52. package/dist/src/projections/message-log-projection.js.map +1 -0
  53. package/dist/src/projections/message-log-projection.specs.d.ts +2 -0
  54. package/dist/src/projections/message-log-projection.specs.d.ts.map +1 -0
  55. package/dist/src/projections/message-log-projection.specs.js +101 -0
  56. package/dist/src/projections/message-log-projection.specs.js.map +1 -0
  57. package/dist/src/projections/node-status-projection.d.ts +23 -0
  58. package/dist/src/projections/node-status-projection.d.ts.map +1 -0
  59. package/dist/src/projections/node-status-projection.js +10 -0
  60. package/dist/src/projections/node-status-projection.js.map +1 -0
  61. package/dist/src/projections/node-status-projection.specs.d.ts +2 -0
  62. package/dist/src/projections/node-status-projection.specs.d.ts.map +1 -0
  63. package/dist/src/projections/node-status-projection.specs.js +116 -0
  64. package/dist/src/projections/node-status-projection.specs.js.map +1 -0
  65. package/dist/src/projections/phased-execution-projection.d.ts +77 -0
  66. package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
  67. package/dist/src/projections/phased-execution-projection.js +54 -0
  68. package/dist/src/projections/phased-execution-projection.js.map +1 -0
  69. package/dist/src/projections/phased-execution-projection.specs.d.ts +2 -0
  70. package/dist/src/projections/phased-execution-projection.specs.d.ts.map +1 -0
  71. package/dist/src/projections/phased-execution-projection.specs.js +171 -0
  72. package/dist/src/projections/phased-execution-projection.specs.js.map +1 -0
  73. package/dist/src/projections/settled-instance-projection.d.ts +67 -0
  74. package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
  75. package/dist/src/projections/settled-instance-projection.js +66 -0
  76. package/dist/src/projections/settled-instance-projection.js.map +1 -0
  77. package/dist/src/projections/settled-instance-projection.specs.d.ts +2 -0
  78. package/dist/src/projections/settled-instance-projection.specs.d.ts.map +1 -0
  79. package/dist/src/projections/settled-instance-projection.specs.js +217 -0
  80. package/dist/src/projections/settled-instance-projection.specs.js.map +1 -0
  81. package/dist/src/projections/stats-projection.d.ts +9 -0
  82. package/dist/src/projections/stats-projection.d.ts.map +1 -0
  83. package/dist/src/projections/stats-projection.js +16 -0
  84. package/dist/src/projections/stats-projection.js.map +1 -0
  85. package/dist/src/projections/stats-projection.specs.d.ts +2 -0
  86. package/dist/src/projections/stats-projection.specs.d.ts.map +1 -0
  87. package/dist/src/projections/stats-projection.specs.js +91 -0
  88. package/dist/src/projections/stats-projection.specs.js.map +1 -0
  89. package/dist/src/runtime/await-tracker.d.ts +17 -7
  90. package/dist/src/runtime/await-tracker.d.ts.map +1 -1
  91. package/dist/src/runtime/await-tracker.js +32 -29
  92. package/dist/src/runtime/await-tracker.js.map +1 -1
  93. package/dist/src/runtime/await-tracker.specs.js +56 -38
  94. package/dist/src/runtime/await-tracker.specs.js.map +1 -1
  95. package/dist/src/runtime/context.d.ts +1 -1
  96. package/dist/src/runtime/context.d.ts.map +1 -1
  97. package/dist/src/runtime/event-command-map.d.ts +3 -3
  98. package/dist/src/runtime/event-command-map.d.ts.map +1 -1
  99. package/dist/src/runtime/event-command-map.js +6 -2
  100. package/dist/src/runtime/event-command-map.js.map +1 -1
  101. package/dist/src/runtime/phased-executor.d.ts +15 -9
  102. package/dist/src/runtime/phased-executor.d.ts.map +1 -1
  103. package/dist/src/runtime/phased-executor.js +126 -104
  104. package/dist/src/runtime/phased-executor.js.map +1 -1
  105. package/dist/src/runtime/phased-executor.specs.js +243 -81
  106. package/dist/src/runtime/phased-executor.specs.js.map +1 -1
  107. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
  108. package/dist/src/runtime/pipeline-runtime.js +2 -2
  109. package/dist/src/runtime/pipeline-runtime.js.map +1 -1
  110. package/dist/src/runtime/pipeline-runtime.specs.js +35 -0
  111. package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -1
  112. package/dist/src/runtime/settled-tracker.d.ts +12 -9
  113. package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
  114. package/dist/src/runtime/settled-tracker.js +92 -77
  115. package/dist/src/runtime/settled-tracker.js.map +1 -1
  116. package/dist/src/runtime/settled-tracker.specs.js +568 -118
  117. package/dist/src/runtime/settled-tracker.specs.js.map +1 -1
  118. package/dist/src/server/pipeline-server.d.ts +31 -9
  119. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  120. package/dist/src/server/pipeline-server.e2e.specs.js +2 -10
  121. package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -1
  122. package/dist/src/server/pipeline-server.js +418 -134
  123. package/dist/src/server/pipeline-server.js.map +1 -1
  124. package/dist/src/server/pipeline-server.specs.js +777 -32
  125. package/dist/src/server/pipeline-server.specs.js.map +1 -1
  126. package/dist/src/server/sse-manager.specs.js +55 -35
  127. package/dist/src/server/sse-manager.specs.js.map +1 -1
  128. package/dist/src/store/index.d.ts +3 -0
  129. package/dist/src/store/index.d.ts.map +1 -0
  130. package/dist/src/store/index.js +3 -0
  131. package/dist/src/store/index.js.map +1 -0
  132. package/dist/src/store/pipeline-event-store.d.ts +10 -0
  133. package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
  134. package/dist/src/store/pipeline-event-store.js +112 -0
  135. package/dist/src/store/pipeline-event-store.js.map +1 -0
  136. package/dist/src/store/pipeline-event-store.specs.d.ts +2 -0
  137. package/dist/src/store/pipeline-event-store.specs.d.ts.map +1 -0
  138. package/dist/src/store/pipeline-event-store.specs.js +287 -0
  139. package/dist/src/store/pipeline-event-store.specs.js.map +1 -0
  140. package/dist/src/store/pipeline-read-model.d.ts +49 -0
  141. package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
  142. package/dist/src/store/pipeline-read-model.js +157 -0
  143. package/dist/src/store/pipeline-read-model.js.map +1 -0
  144. package/dist/src/store/pipeline-read-model.specs.d.ts +2 -0
  145. package/dist/src/store/pipeline-read-model.specs.d.ts.map +1 -0
  146. package/dist/src/store/pipeline-read-model.specs.js +830 -0
  147. package/dist/src/store/pipeline-read-model.specs.js.map +1 -0
  148. package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
  149. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
  150. package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
  151. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
  152. package/dist/tsconfig.tsbuildinfo +1 -1
  153. package/ketchup-plan.md +960 -0
  154. package/package.json +7 -3
  155. package/src/builder/define.specs.ts +3 -3
  156. package/src/builder/define.ts +24 -11
  157. package/src/core/descriptors.ts +7 -2
  158. package/src/graph/filter-graph.specs.ts +241 -0
  159. package/src/graph/filter-graph.ts +111 -0
  160. package/src/graph/types.ts +10 -0
  161. package/src/index.ts +1 -2
  162. package/src/projections/await-tracker-projection.ts +68 -0
  163. package/src/projections/index.ts +11 -0
  164. package/src/projections/item-status-projection.specs.ts +130 -0
  165. package/src/projections/item-status-projection.ts +32 -0
  166. package/src/projections/latest-run-projection.specs.ts +38 -0
  167. package/src/projections/latest-run-projection.ts +20 -0
  168. package/src/projections/message-log-projection.specs.ts +118 -0
  169. package/src/projections/message-log-projection.ts +113 -0
  170. package/src/projections/node-status-projection.specs.ts +127 -0
  171. package/src/projections/node-status-projection.ts +33 -0
  172. package/src/projections/phased-execution-projection.specs.ts +202 -0
  173. package/src/projections/phased-execution-projection.ts +146 -0
  174. package/src/projections/settled-instance-projection.specs.ts +249 -0
  175. package/src/projections/settled-instance-projection.ts +160 -0
  176. package/src/projections/stats-projection.specs.ts +105 -0
  177. package/src/projections/stats-projection.ts +26 -0
  178. package/src/runtime/await-tracker.specs.ts +57 -34
  179. package/src/runtime/await-tracker.ts +43 -31
  180. package/src/runtime/context.ts +1 -1
  181. package/src/runtime/event-command-map.ts +11 -4
  182. package/src/runtime/phased-executor.specs.ts +357 -81
  183. package/src/runtime/phased-executor.ts +142 -126
  184. package/src/runtime/pipeline-runtime.specs.ts +42 -0
  185. package/src/runtime/pipeline-runtime.ts +6 -4
  186. package/src/runtime/settled-tracker.specs.ts +716 -120
  187. package/src/runtime/settled-tracker.ts +104 -98
  188. package/src/server/pipeline-server.e2e.specs.ts +10 -16
  189. package/src/server/pipeline-server.specs.ts +964 -49
  190. package/src/server/pipeline-server.ts +522 -156
  191. package/src/server/sse-manager.specs.ts +67 -36
  192. package/src/store/index.ts +2 -0
  193. package/src/store/pipeline-event-store.specs.ts +309 -0
  194. package/src/store/pipeline-event-store.ts +156 -0
  195. package/src/store/pipeline-read-model.specs.ts +967 -0
  196. package/src/store/pipeline-read-model.ts +223 -0
  197. package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
  198. package/src/testing/fixtures/kanban.pipeline.ts +2 -2
  199. package/claude.md +0 -160
  200. package/docs/testing-analysis.md +0 -395
  201. 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 type { GraphIR } from '../graph/types';
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?: string[];
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.messageStore = config.messageStore ?? new MemoryMessageStore();
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', (_req, res) => {
180
- const combinedGraph = this.buildCombinedGraph();
181
- const commandToEvents = this.buildCommandToEvents();
182
- const eventToCommand = this.buildEventToCommand();
183
- const pipelineNodes = this.buildPipelineNodes();
184
-
185
- res.json({
186
- nodes: [...combinedGraph.nodes, ...pipelineNodes],
187
- edges: combinedGraph.edges,
188
- commandToEvents,
189
- eventToCommand,
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', (_req, res) => {
194
- const mermaid = this.buildMermaidDiagram();
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', (_req, res) => {
199
- const mermaidDefinition = this.buildMermaidDiagram();
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.messageStore.saveMessage('$all', commandWithIds, undefined, 'command');
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.messageStore.getMessages('$all');
239
- const serialized = messages.map((m) => ({
240
- ...m,
241
- revision: m.revision.toString(),
242
- position: m.position.toString(),
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.messageStore.getStats();
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 buildEventToCommand(): Record<string, string> {
298
- const eventToCommand: Record<string, string> = {};
299
- for (const pipeline of this.pipelines.values()) {
300
- for (const handler of pipeline.descriptor.handlers) {
301
- if (handler.type === 'emit') {
302
- for (const cmd of handler.commands) {
303
- eventToCommand[handler.eventType] = cmd.commandType;
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 eventToCommand;
603
+ return eventDisplayNames;
309
604
  }
310
605
 
311
- private buildPipelineNodes(): Array<{ id: string; name: string; title: string; alias?: string; status: 'None' }> {
312
- return Array.from(this.commandHandlers.entries()).map(([name, handler]) => ({
313
- id: name,
314
- name,
315
- title: handler.description ?? '',
316
- alias: handler.alias,
317
- status: 'None' as const,
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 pipelineEvents = this.extractPipelineEvents(graph, commandToEvents);
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
- const pipelineCommands = new Set(commandNodes);
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}([${eventName}])`);
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}[${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.settledTracker.onCommandStarted(command);
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
- for (const event of events) {
520
- const eventWithIds: EventWithCorrelation = {
521
- ...event,
522
- correlationId: command.correlationId,
523
- requestId: command.requestId,
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
- await this.messageStore.saveMessage('$all', eventWithIds, undefined, 'event');
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(event.type);
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
- await this.routeEventToPipelines(eventWithIds);
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: `req-${nanoid()}`,
911
+ requestId,
547
912
  };
548
- await this.messageStore.saveMessage('$all', command, undefined, 'command');
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.messageStore.saveMessage('$all', eventWithIds, undefined, 'event');
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
- for (const runtime of this.runtimes.values()) {
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.messageStore.saveMessage('$all', event, undefined, 'event');
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: `req-${nanoid()}`,
954
+ requestId,
589
955
  };
590
- await this.messageStore.saveMessage('$all', command, undefined, 'command');
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
  }