@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { Event } from '@auto-engineer/message-bus';
|
|
2
|
+
|
|
3
|
+
interface CommandTracker {
|
|
4
|
+
commandType: string;
|
|
5
|
+
hasStarted: boolean;
|
|
6
|
+
hasCompleted: boolean;
|
|
7
|
+
events: Event[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SettledInstanceDocument {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
instanceId: string;
|
|
13
|
+
templateId: string;
|
|
14
|
+
correlationId: string;
|
|
15
|
+
commandTrackers: CommandTracker[];
|
|
16
|
+
status: 'active' | 'fired' | 'cleaned';
|
|
17
|
+
firedCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SettledInstanceCreatedEvent {
|
|
21
|
+
type: 'SettledInstanceCreated';
|
|
22
|
+
data: {
|
|
23
|
+
templateId: string;
|
|
24
|
+
correlationId: string;
|
|
25
|
+
commandTypes: readonly string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SettledCommandStartedEvent {
|
|
30
|
+
type: 'SettledCommandStarted';
|
|
31
|
+
data: {
|
|
32
|
+
templateId: string;
|
|
33
|
+
correlationId: string;
|
|
34
|
+
commandType: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SettledEventReceivedEvent {
|
|
39
|
+
type: 'SettledEventReceived';
|
|
40
|
+
data: {
|
|
41
|
+
templateId: string;
|
|
42
|
+
correlationId: string;
|
|
43
|
+
commandType: string;
|
|
44
|
+
event: Event;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SettledHandlerFiredEvent {
|
|
49
|
+
type: 'SettledHandlerFired';
|
|
50
|
+
data: {
|
|
51
|
+
templateId: string;
|
|
52
|
+
correlationId: string;
|
|
53
|
+
persist: boolean;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SettledInstanceResetEvent {
|
|
58
|
+
type: 'SettledInstanceReset';
|
|
59
|
+
data: {
|
|
60
|
+
templateId: string;
|
|
61
|
+
correlationId: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SettledInstanceCleanedEvent {
|
|
66
|
+
type: 'SettledInstanceCleaned';
|
|
67
|
+
data: {
|
|
68
|
+
templateId: string;
|
|
69
|
+
correlationId: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SettledEvent =
|
|
74
|
+
| SettledInstanceCreatedEvent
|
|
75
|
+
| SettledCommandStartedEvent
|
|
76
|
+
| SettledEventReceivedEvent
|
|
77
|
+
| SettledHandlerFiredEvent
|
|
78
|
+
| SettledInstanceResetEvent
|
|
79
|
+
| SettledInstanceCleanedEvent;
|
|
80
|
+
|
|
81
|
+
function assertDocument(
|
|
82
|
+
document: SettledInstanceDocument | null,
|
|
83
|
+
eventType: string,
|
|
84
|
+
): asserts document is SettledInstanceDocument {
|
|
85
|
+
if (document === null) {
|
|
86
|
+
throw new Error(`Cannot apply ${eventType} to null document`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function evolveCommandStarted(document: SettledInstanceDocument, commandType: string): SettledInstanceDocument {
|
|
91
|
+
return {
|
|
92
|
+
...document,
|
|
93
|
+
commandTrackers: document.commandTrackers.map((tracker) =>
|
|
94
|
+
tracker.commandType === commandType ? { ...tracker, hasStarted: true } : tracker,
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function evolveEventReceived(
|
|
100
|
+
document: SettledInstanceDocument,
|
|
101
|
+
commandType: string,
|
|
102
|
+
domainEvent: Event,
|
|
103
|
+
): SettledInstanceDocument {
|
|
104
|
+
return {
|
|
105
|
+
...document,
|
|
106
|
+
commandTrackers: document.commandTrackers.map((tracker) =>
|
|
107
|
+
tracker.commandType === commandType
|
|
108
|
+
? { ...tracker, hasCompleted: true, events: [...tracker.events, domainEvent] }
|
|
109
|
+
: tracker,
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function evolveReset(document: SettledInstanceDocument): SettledInstanceDocument {
|
|
115
|
+
return {
|
|
116
|
+
...document,
|
|
117
|
+
status: 'active',
|
|
118
|
+
commandTrackers: document.commandTrackers.map((tracker) => ({
|
|
119
|
+
...tracker,
|
|
120
|
+
hasCompleted: false,
|
|
121
|
+
events: [],
|
|
122
|
+
})),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function evolve(document: SettledInstanceDocument | null, event: SettledEvent): SettledInstanceDocument {
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
case 'SettledInstanceCreated': {
|
|
129
|
+
const { templateId, correlationId, commandTypes } = event.data;
|
|
130
|
+
return {
|
|
131
|
+
instanceId: `${templateId}-${correlationId}`,
|
|
132
|
+
templateId,
|
|
133
|
+
correlationId,
|
|
134
|
+
commandTrackers: commandTypes.map((commandType) => ({
|
|
135
|
+
commandType,
|
|
136
|
+
hasStarted: false,
|
|
137
|
+
hasCompleted: false,
|
|
138
|
+
events: [],
|
|
139
|
+
})),
|
|
140
|
+
status: 'active',
|
|
141
|
+
firedCount: 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
case 'SettledCommandStarted':
|
|
145
|
+
assertDocument(document, event.type);
|
|
146
|
+
return evolveCommandStarted(document, event.data.commandType);
|
|
147
|
+
case 'SettledEventReceived':
|
|
148
|
+
assertDocument(document, event.type);
|
|
149
|
+
return evolveEventReceived(document, event.data.commandType, event.data.event);
|
|
150
|
+
case 'SettledHandlerFired':
|
|
151
|
+
assertDocument(document, event.type);
|
|
152
|
+
return { ...document, status: 'fired', firedCount: document.firedCount + 1 };
|
|
153
|
+
case 'SettledInstanceReset':
|
|
154
|
+
assertDocument(document, event.type);
|
|
155
|
+
return evolveReset(document);
|
|
156
|
+
case 'SettledInstanceCleaned':
|
|
157
|
+
assertDocument(document, event.type);
|
|
158
|
+
return { ...document, status: 'cleaned' };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { MessageLogEvent } from './message-log-projection';
|
|
3
|
+
import { evolve, type StatsDocument } from './stats-projection';
|
|
4
|
+
|
|
5
|
+
describe('StatsProjection', () => {
|
|
6
|
+
describe('CommandDispatched', () => {
|
|
7
|
+
it('increments totalMessages and totalCommands for first command', () => {
|
|
8
|
+
const event: MessageLogEvent = {
|
|
9
|
+
type: 'CommandDispatched',
|
|
10
|
+
data: {
|
|
11
|
+
correlationId: 'c1',
|
|
12
|
+
requestId: 'r1',
|
|
13
|
+
commandType: 'CreateUser',
|
|
14
|
+
commandData: { name: 'Alice' },
|
|
15
|
+
timestamp: new Date('2025-01-01T00:00:00Z'),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = evolve(null, event);
|
|
20
|
+
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
totalMessages: 1,
|
|
23
|
+
totalCommands: 1,
|
|
24
|
+
totalEvents: 0,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('accumulates commands over time', () => {
|
|
29
|
+
const existing: StatsDocument = {
|
|
30
|
+
totalMessages: 5,
|
|
31
|
+
totalCommands: 3,
|
|
32
|
+
totalEvents: 2,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const event: MessageLogEvent = {
|
|
36
|
+
type: 'CommandDispatched',
|
|
37
|
+
data: {
|
|
38
|
+
correlationId: 'c1',
|
|
39
|
+
requestId: 'r2',
|
|
40
|
+
commandType: 'UpdateUser',
|
|
41
|
+
commandData: { name: 'Bob' },
|
|
42
|
+
timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = evolve(existing, event);
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
totalMessages: 6,
|
|
50
|
+
totalCommands: 4,
|
|
51
|
+
totalEvents: 2,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('DomainEventEmitted', () => {
|
|
57
|
+
it('increments totalMessages and totalEvents for first event', () => {
|
|
58
|
+
const event: MessageLogEvent = {
|
|
59
|
+
type: 'DomainEventEmitted',
|
|
60
|
+
data: {
|
|
61
|
+
correlationId: 'c1',
|
|
62
|
+
requestId: 'r1',
|
|
63
|
+
eventType: 'UserCreated',
|
|
64
|
+
eventData: { userId: '123' },
|
|
65
|
+
timestamp: new Date('2025-01-01T00:00:00Z'),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = evolve(null, event);
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
totalMessages: 1,
|
|
73
|
+
totalCommands: 0,
|
|
74
|
+
totalEvents: 1,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('accumulates events over time', () => {
|
|
79
|
+
const existing: StatsDocument = {
|
|
80
|
+
totalMessages: 5,
|
|
81
|
+
totalCommands: 3,
|
|
82
|
+
totalEvents: 2,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const event: MessageLogEvent = {
|
|
86
|
+
type: 'DomainEventEmitted',
|
|
87
|
+
data: {
|
|
88
|
+
correlationId: 'c1',
|
|
89
|
+
requestId: 'r2',
|
|
90
|
+
eventType: 'UserUpdated',
|
|
91
|
+
eventData: { userId: '123' },
|
|
92
|
+
timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = evolve(existing, event);
|
|
97
|
+
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
totalMessages: 6,
|
|
100
|
+
totalCommands: 3,
|
|
101
|
+
totalEvents: 3,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MessageLogEvent } from './message-log-projection';
|
|
2
|
+
|
|
3
|
+
export interface StatsDocument {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
totalMessages: number;
|
|
6
|
+
totalCommands: number;
|
|
7
|
+
totalEvents: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function evolve(document: StatsDocument | null, event: MessageLogEvent): StatsDocument {
|
|
11
|
+
const current = document ?? { totalMessages: 0, totalCommands: 0, totalEvents: 0 };
|
|
12
|
+
|
|
13
|
+
if (event.type === 'CommandDispatched') {
|
|
14
|
+
return {
|
|
15
|
+
totalMessages: current.totalMessages + 1,
|
|
16
|
+
totalCommands: current.totalCommands + 1,
|
|
17
|
+
totalEvents: current.totalEvents,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
totalMessages: current.totalMessages + 1,
|
|
23
|
+
totalCommands: current.totalCommands,
|
|
24
|
+
totalEvents: current.totalEvents + 1,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,52 +1,75 @@
|
|
|
1
|
+
import type { AwaitEvent } from '../projections/await-tracker-projection';
|
|
2
|
+
import { createPipelineEventStore, type PipelineEventStoreContext } from '../store/pipeline-event-store';
|
|
1
3
|
import { AwaitTracker } from './await-tracker';
|
|
2
4
|
|
|
5
|
+
function createESTracker(ctx: PipelineEventStoreContext): AwaitTracker {
|
|
6
|
+
return new AwaitTracker({
|
|
7
|
+
readModel: ctx.readModel,
|
|
8
|
+
onEventEmit: async (event: AwaitEvent) => {
|
|
9
|
+
await ctx.eventStore.appendToStream(`await-${event.data.correlationId}`, [
|
|
10
|
+
{ type: event.type, data: event.data },
|
|
11
|
+
]);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
describe('AwaitTracker', () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
let ctx: PipelineEventStoreContext;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
ctx = createPipelineEventStore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await ctx.close();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should track pending keys', async () => {
|
|
28
|
+
const tracker = createESTracker(ctx);
|
|
29
|
+
await tracker.startAwaiting('corr-1', ['a', 'b']);
|
|
30
|
+
expect(await tracker.isPending('corr-1')).toBe(true);
|
|
31
|
+
expect(await tracker.getPendingKeys('corr-1')).toEqual(['a', 'b']);
|
|
9
32
|
});
|
|
10
33
|
|
|
11
|
-
it('should detect completion', () => {
|
|
12
|
-
const tracker =
|
|
13
|
-
tracker.startAwaiting('c', ['a', 'b']);
|
|
14
|
-
tracker.markComplete('c', 'a', { result: 1 });
|
|
15
|
-
expect(tracker.isComplete('c')).toBe(false);
|
|
16
|
-
tracker.markComplete('c', 'b', { result: 2 });
|
|
17
|
-
expect(tracker.isComplete('c')).toBe(true);
|
|
34
|
+
it('should detect completion', async () => {
|
|
35
|
+
const tracker = createESTracker(ctx);
|
|
36
|
+
await tracker.startAwaiting('c', ['a', 'b']);
|
|
37
|
+
await tracker.markComplete('c', 'a', { result: 1 });
|
|
38
|
+
expect(await tracker.isComplete('c')).toBe(false);
|
|
39
|
+
await tracker.markComplete('c', 'b', { result: 2 });
|
|
40
|
+
expect(await tracker.isComplete('c')).toBe(true);
|
|
18
41
|
});
|
|
19
42
|
|
|
20
|
-
it('should return false for unknown correlationId', () => {
|
|
21
|
-
const tracker =
|
|
22
|
-
expect(tracker.isPending('unknown')).toBe(false);
|
|
23
|
-
expect(tracker.isComplete('unknown')).toBe(false);
|
|
43
|
+
it('should return false for unknown correlationId', async () => {
|
|
44
|
+
const tracker = createESTracker(ctx);
|
|
45
|
+
expect(await tracker.isPending('unknown')).toBe(false);
|
|
46
|
+
expect(await tracker.isComplete('unknown')).toBe(false);
|
|
24
47
|
});
|
|
25
48
|
|
|
26
|
-
it('should return empty array for unknown correlationId keys', () => {
|
|
27
|
-
const tracker =
|
|
28
|
-
expect(tracker.getPendingKeys('unknown')).toEqual([]);
|
|
49
|
+
it('should return empty array for unknown correlationId keys', async () => {
|
|
50
|
+
const tracker = createESTracker(ctx);
|
|
51
|
+
expect(await tracker.getPendingKeys('unknown')).toEqual([]);
|
|
29
52
|
});
|
|
30
53
|
|
|
31
|
-
it('should collect results when all keys complete', () => {
|
|
32
|
-
const tracker =
|
|
33
|
-
tracker.startAwaiting('c', ['x', 'y']);
|
|
34
|
-
tracker.markComplete('c', 'x', { val: 1 });
|
|
35
|
-
tracker.markComplete('c', 'y', { val: 2 });
|
|
36
|
-
const results = tracker.getResults('c');
|
|
54
|
+
it('should collect results when all keys complete', async () => {
|
|
55
|
+
const tracker = createESTracker(ctx);
|
|
56
|
+
await tracker.startAwaiting('c', ['x', 'y']);
|
|
57
|
+
await tracker.markComplete('c', 'x', { val: 1 });
|
|
58
|
+
await tracker.markComplete('c', 'y', { val: 2 });
|
|
59
|
+
const results = await tracker.getResults('c');
|
|
37
60
|
expect(results).toEqual({ x: { val: 1 }, y: { val: 2 } });
|
|
38
61
|
});
|
|
39
62
|
|
|
40
|
-
it('should clear tracking after getting results', () => {
|
|
41
|
-
const tracker =
|
|
42
|
-
tracker.startAwaiting('c', ['a']);
|
|
43
|
-
tracker.markComplete('c', 'a', {});
|
|
44
|
-
tracker.getResults('c');
|
|
45
|
-
expect(tracker.isPending('c')).toBe(false);
|
|
63
|
+
it('should clear tracking after getting results', async () => {
|
|
64
|
+
const tracker = createESTracker(ctx);
|
|
65
|
+
await tracker.startAwaiting('c', ['a']);
|
|
66
|
+
await tracker.markComplete('c', 'a', {});
|
|
67
|
+
await tracker.getResults('c');
|
|
68
|
+
expect(await tracker.isPending('c')).toBe(false);
|
|
46
69
|
});
|
|
47
70
|
|
|
48
|
-
it('should return empty object for unknown correlationId results', () => {
|
|
49
|
-
const tracker =
|
|
50
|
-
expect(tracker.getResults('unknown')).toEqual({});
|
|
71
|
+
it('should return empty object for unknown correlationId results', async () => {
|
|
72
|
+
const tracker = createESTracker(ctx);
|
|
73
|
+
expect(await tracker.getResults('unknown')).toEqual({});
|
|
51
74
|
});
|
|
52
75
|
});
|
|
@@ -1,50 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { AwaitEvent } from '../projections/await-tracker-projection';
|
|
2
|
+
import type { PipelineReadModel } from '../store/pipeline-read-model';
|
|
3
|
+
|
|
4
|
+
interface AwaitTrackerOptions {
|
|
5
|
+
readModel: PipelineReadModel;
|
|
6
|
+
onEventEmit: (event: AwaitEvent) => void | Promise<void>;
|
|
4
7
|
}
|
|
5
8
|
|
|
6
9
|
export class AwaitTracker {
|
|
7
|
-
private readonly
|
|
10
|
+
private readonly readModel: PipelineReadModel;
|
|
11
|
+
private readonly onEventEmit: (event: AwaitEvent) => void | Promise<void>;
|
|
12
|
+
|
|
13
|
+
constructor(options: AwaitTrackerOptions) {
|
|
14
|
+
this.readModel = options.readModel;
|
|
15
|
+
this.onEventEmit = options.onEventEmit;
|
|
16
|
+
}
|
|
8
17
|
|
|
9
|
-
startAwaiting(correlationId: string, keys: string[]): void {
|
|
10
|
-
this.
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
async startAwaiting(correlationId: string, keys: string[]): Promise<void> {
|
|
19
|
+
await this.emitEvent({
|
|
20
|
+
type: 'AwaitStarted',
|
|
21
|
+
data: { correlationId, keys },
|
|
13
22
|
});
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
isPending(correlationId: string): boolean {
|
|
17
|
-
|
|
25
|
+
async isPending(correlationId: string): Promise<boolean> {
|
|
26
|
+
const state = await this.readModel.getAwaitState(correlationId);
|
|
27
|
+
return state !== null;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
getPendingKeys(correlationId: string): string[] {
|
|
21
|
-
const
|
|
22
|
-
return
|
|
30
|
+
async getPendingKeys(correlationId: string): Promise<string[]> {
|
|
31
|
+
const state = await this.readModel.getAwaitState(correlationId);
|
|
32
|
+
return state ? state.pendingKeys : [];
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
markComplete(correlationId: string, key: string, result: unknown): void {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
35
|
+
async markComplete(correlationId: string, key: string, result: unknown): Promise<void> {
|
|
36
|
+
await this.emitEvent({
|
|
37
|
+
type: 'AwaitItemCompleted',
|
|
38
|
+
data: { correlationId, key, result },
|
|
39
|
+
});
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
isComplete(correlationId: string): boolean {
|
|
34
|
-
const
|
|
35
|
-
return
|
|
42
|
+
async isComplete(correlationId: string): Promise<boolean> {
|
|
43
|
+
const state = await this.readModel.getAwaitState(correlationId);
|
|
44
|
+
return state !== null && state.pendingKeys.length === 0;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
getResults(correlationId: string): Record<string, unknown
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
47
|
+
async getResults(correlationId: string): Promise<Record<string, unknown>> {
|
|
48
|
+
const state = await this.readModel.getAwaitState(correlationId);
|
|
49
|
+
if (state === null) {
|
|
41
50
|
return {};
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
await this.emitEvent({
|
|
53
|
+
type: 'AwaitCompleted',
|
|
54
|
+
data: { correlationId },
|
|
55
|
+
});
|
|
56
|
+
return state.results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async emitEvent(event: AwaitEvent): Promise<void> {
|
|
60
|
+
await this.onEventEmit(event);
|
|
49
61
|
}
|
|
50
62
|
}
|
package/src/runtime/context.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface PipelineContext {
|
|
|
5
5
|
emit: (type: string, data: unknown) => Promise<void>;
|
|
6
6
|
sendCommand: (type: string, data: unknown) => Promise<void>;
|
|
7
7
|
correlationId: string;
|
|
8
|
-
startPhased?: (handler: ForEachPhasedDescriptor, event: Event) => void
|
|
8
|
+
startPhased?: (handler: ForEachPhasedDescriptor, event: Event) => Promise<void>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface RuntimeConfig {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type { CommandHandler } from '@auto-engineer/message-bus';
|
|
1
|
+
import type { CommandHandler, EventDefinition } from '@auto-engineer/message-bus';
|
|
2
|
+
|
|
3
|
+
export type { EventDefinition };
|
|
2
4
|
|
|
3
5
|
interface CommandHandlerWithEvents extends CommandHandler {
|
|
4
|
-
events?: readonly
|
|
6
|
+
events?: readonly EventDefinition[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getEventName(event: EventDefinition): string {
|
|
10
|
+
return typeof event === 'string' ? event : event.name;
|
|
5
11
|
}
|
|
6
12
|
|
|
7
13
|
export class EventCommandMapper {
|
|
@@ -17,11 +23,12 @@ export class EventCommandMapper {
|
|
|
17
23
|
addHandler(handler: CommandHandlerWithEvents): void {
|
|
18
24
|
const events = handler.events ?? [];
|
|
19
25
|
|
|
20
|
-
for (const
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
const eventType = getEventName(event);
|
|
21
28
|
this.eventToCommand.set(eventType, handler.name);
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
this.commandToEvents.set(handler.name,
|
|
31
|
+
this.commandToEvents.set(handler.name, events.map(getEventName));
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
getSourceCommand(eventType: string): string | undefined {
|