@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
|
@@ -49,14 +49,8 @@ describe('SSEManager', () => {
|
|
|
49
49
|
manager = new SSEManager();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
describe('client
|
|
53
|
-
it('should
|
|
54
|
-
const res = createMockResponse();
|
|
55
|
-
manager.addClient('c1', res);
|
|
56
|
-
expect(manager.clientCount).toBe(1);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should set correct SSE headers', () => {
|
|
52
|
+
describe('client connection', () => {
|
|
53
|
+
it('should set correct SSE headers when client connects', () => {
|
|
60
54
|
const res = createMockResponse();
|
|
61
55
|
manager.addClient('c1', res);
|
|
62
56
|
expect(res.writeHeadMock).toHaveBeenCalledWith(200, {
|
|
@@ -73,23 +67,43 @@ describe('SSEManager', () => {
|
|
|
73
67
|
expect(res.written[0]).toBe(':\n\n');
|
|
74
68
|
});
|
|
75
69
|
|
|
76
|
-
it('should
|
|
70
|
+
it('should receive broadcasts after connecting', () => {
|
|
71
|
+
const res = createMockResponse();
|
|
72
|
+
manager.addClient('c1', res);
|
|
73
|
+
|
|
74
|
+
const event: Event = { type: 'TestEvent', data: { foo: 'bar' } };
|
|
75
|
+
manager.broadcast(event);
|
|
76
|
+
|
|
77
|
+
expect(res.written).toContainEqual(`data: ${JSON.stringify(event)}\n\n`);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('client removal', () => {
|
|
82
|
+
it('should not receive broadcasts after explicit removal', () => {
|
|
77
83
|
const res = createMockResponse();
|
|
78
84
|
manager.addClient('c1', res);
|
|
79
85
|
manager.removeClient('c1');
|
|
80
|
-
|
|
86
|
+
|
|
87
|
+
const event: Event = { type: 'TestEvent', data: { foo: 'bar' } };
|
|
88
|
+
manager.broadcast(event);
|
|
89
|
+
|
|
90
|
+
expect(res.written).toHaveLength(1);
|
|
81
91
|
});
|
|
82
92
|
|
|
83
|
-
it('should
|
|
93
|
+
it('should not receive broadcasts after connection close', () => {
|
|
84
94
|
const res = createMockResponse();
|
|
85
95
|
manager.addClient('c1', res);
|
|
86
96
|
res.triggerClose();
|
|
87
|
-
|
|
97
|
+
|
|
98
|
+
const event: Event = { type: 'TestEvent', data: { foo: 'bar' } };
|
|
99
|
+
manager.broadcast(event);
|
|
100
|
+
|
|
101
|
+
expect(res.written).toHaveLength(1);
|
|
88
102
|
});
|
|
89
103
|
});
|
|
90
104
|
|
|
91
105
|
describe('broadcasting', () => {
|
|
92
|
-
it('should broadcast event to all clients', () => {
|
|
106
|
+
it('should broadcast event to all connected clients', () => {
|
|
93
107
|
const res1 = createMockResponse();
|
|
94
108
|
const res2 = createMockResponse();
|
|
95
109
|
|
|
@@ -103,21 +117,21 @@ describe('SSEManager', () => {
|
|
|
103
117
|
expect(res2.written).toContainEqual(`data: ${JSON.stringify(event)}\n\n`);
|
|
104
118
|
});
|
|
105
119
|
|
|
106
|
-
it('should filter by correlationId when
|
|
107
|
-
const
|
|
108
|
-
const
|
|
120
|
+
it('should filter events by correlationId when client subscribes to specific correlation', () => {
|
|
121
|
+
const filteredRes = createMockResponse();
|
|
122
|
+
const unfilteredRes = createMockResponse();
|
|
109
123
|
|
|
110
|
-
manager.addClient('c1',
|
|
111
|
-
manager.addClient('c2',
|
|
124
|
+
manager.addClient('c1', filteredRes, 'workflow-123');
|
|
125
|
+
manager.addClient('c2', unfilteredRes);
|
|
112
126
|
|
|
113
127
|
const event: Event = { type: 'TestEvent', correlationId: 'workflow-456', data: {} };
|
|
114
128
|
manager.broadcast(event);
|
|
115
129
|
|
|
116
|
-
expect(
|
|
117
|
-
expect(
|
|
130
|
+
expect(filteredRes.written).toHaveLength(1);
|
|
131
|
+
expect(unfilteredRes.written).toHaveLength(2);
|
|
118
132
|
});
|
|
119
133
|
|
|
120
|
-
it('should send to filtered client when correlationId matches', () => {
|
|
134
|
+
it('should send event to filtered client when correlationId matches', () => {
|
|
121
135
|
const res = createMockResponse();
|
|
122
136
|
manager.addClient('c1', res, 'workflow-123');
|
|
123
137
|
|
|
@@ -127,19 +141,19 @@ describe('SSEManager', () => {
|
|
|
127
141
|
expect(res.written).toContainEqual(`data: ${JSON.stringify(event)}\n\n`);
|
|
128
142
|
});
|
|
129
143
|
|
|
130
|
-
it('should not send to filtered client when correlationId does not match', () => {
|
|
144
|
+
it('should not send event to filtered client when correlationId does not match', () => {
|
|
131
145
|
const res = createMockResponse();
|
|
132
146
|
manager.addClient('c1', res, 'workflow-123');
|
|
133
147
|
|
|
134
148
|
const event: Event = { type: 'TestEvent', correlationId: 'workflow-456', data: {} };
|
|
135
149
|
manager.broadcast(event);
|
|
136
150
|
|
|
137
|
-
expect(res.written
|
|
151
|
+
expect(res.written).toHaveLength(1);
|
|
138
152
|
});
|
|
139
153
|
});
|
|
140
154
|
|
|
141
155
|
describe('broadcast error handling', () => {
|
|
142
|
-
it('should not throw when client
|
|
156
|
+
it('should not throw when client write fails', () => {
|
|
143
157
|
const res = createMockResponse();
|
|
144
158
|
manager.addClient('c1', res);
|
|
145
159
|
|
|
@@ -152,21 +166,28 @@ describe('SSEManager', () => {
|
|
|
152
166
|
expect(() => manager.broadcast(event)).not.toThrow();
|
|
153
167
|
});
|
|
154
168
|
|
|
155
|
-
it('should
|
|
169
|
+
it('should stop sending to failed client on subsequent broadcasts', () => {
|
|
156
170
|
const res = createMockResponse();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
let writeCallCount = 0;
|
|
172
|
+
vi.mocked(res.write).mockImplementation((data: string) => {
|
|
173
|
+
writeCallCount++;
|
|
174
|
+
if (writeCallCount > 2) {
|
|
160
175
|
throw new Error('Connection reset');
|
|
161
|
-
}
|
|
176
|
+
}
|
|
177
|
+
res.written.push(data);
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
162
180
|
|
|
163
181
|
manager.addClient('c1', res);
|
|
164
|
-
expect(manager.clientCount).toBe(1);
|
|
165
182
|
|
|
166
|
-
const
|
|
167
|
-
manager.broadcast(
|
|
183
|
+
const event1: Event = { type: 'TestEvent1', data: {} };
|
|
184
|
+
manager.broadcast(event1);
|
|
185
|
+
|
|
186
|
+
const event2: Event = { type: 'TestEvent2', data: {} };
|
|
187
|
+
manager.broadcast(event2);
|
|
168
188
|
|
|
169
|
-
|
|
189
|
+
const eventMessages = res.written.filter((w) => w.startsWith('data:'));
|
|
190
|
+
expect(eventMessages).toHaveLength(1);
|
|
170
191
|
});
|
|
171
192
|
|
|
172
193
|
it('should continue broadcasting to other clients after one fails', () => {
|
|
@@ -186,12 +207,11 @@ describe('SSEManager', () => {
|
|
|
186
207
|
manager.broadcast(event);
|
|
187
208
|
|
|
188
209
|
expect(workingRes.written).toContainEqual(`data: ${JSON.stringify(event)}\n\n`);
|
|
189
|
-
expect(manager.clientCount).toBe(1);
|
|
190
210
|
});
|
|
191
211
|
});
|
|
192
212
|
|
|
193
213
|
describe('closeAll', () => {
|
|
194
|
-
it('should
|
|
214
|
+
it('should end all client connections', () => {
|
|
195
215
|
const res1 = createMockResponse();
|
|
196
216
|
const res2 = createMockResponse();
|
|
197
217
|
|
|
@@ -200,9 +220,20 @@ describe('SSEManager', () => {
|
|
|
200
220
|
|
|
201
221
|
manager.closeAll();
|
|
202
222
|
|
|
203
|
-
expect(manager.clientCount).toBe(0);
|
|
204
223
|
expect(res1.endMock).toHaveBeenCalled();
|
|
205
224
|
expect(res2.endMock).toHaveBeenCalled();
|
|
206
225
|
});
|
|
226
|
+
|
|
227
|
+
it('should not send broadcasts after closeAll', () => {
|
|
228
|
+
const res = createMockResponse();
|
|
229
|
+
manager.addClient('c1', res);
|
|
230
|
+
|
|
231
|
+
manager.closeAll();
|
|
232
|
+
|
|
233
|
+
const event: Event = { type: 'TestEvent', data: {} };
|
|
234
|
+
manager.broadcast(event);
|
|
235
|
+
|
|
236
|
+
expect(res.written).toHaveLength(1);
|
|
237
|
+
});
|
|
207
238
|
});
|
|
208
239
|
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ItemStatusDocument } from '../projections/item-status-projection';
|
|
3
|
+
import type { NodeStatusDocument } from '../projections/node-status-projection';
|
|
4
|
+
import type { PhasedExecutionDocument } from '../projections/phased-execution-projection';
|
|
5
|
+
import type { SettledInstanceDocument } from '../projections/settled-instance-projection';
|
|
6
|
+
import { createPipelineEventStore } from './pipeline-event-store';
|
|
7
|
+
|
|
8
|
+
describe('PipelineEventStore', () => {
|
|
9
|
+
describe('appendToStream', () => {
|
|
10
|
+
it('should append events and update projections', async () => {
|
|
11
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
12
|
+
try {
|
|
13
|
+
await eventStore.appendToStream('pipeline-c1', [
|
|
14
|
+
{
|
|
15
|
+
type: 'ItemStatusChanged',
|
|
16
|
+
data: {
|
|
17
|
+
correlationId: 'c1',
|
|
18
|
+
commandType: 'Cmd',
|
|
19
|
+
itemKey: 'a',
|
|
20
|
+
requestId: 'r1',
|
|
21
|
+
status: 'running',
|
|
22
|
+
attemptCount: 1,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const collection = database.collection<ItemStatusDocument & { _id: string }>('ItemStatus');
|
|
28
|
+
const items = await collection.find();
|
|
29
|
+
|
|
30
|
+
expect(items.length).toBe(1);
|
|
31
|
+
expect(items[0]?.status).toBe('running');
|
|
32
|
+
} finally {
|
|
33
|
+
await close();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should project NodeStatusChanged events', async () => {
|
|
38
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
39
|
+
try {
|
|
40
|
+
await eventStore.appendToStream('pipeline-c1', [
|
|
41
|
+
{
|
|
42
|
+
type: 'NodeStatusChanged',
|
|
43
|
+
data: {
|
|
44
|
+
correlationId: 'c1',
|
|
45
|
+
commandName: 'Cmd',
|
|
46
|
+
nodeId: 'node-1',
|
|
47
|
+
status: 'running',
|
|
48
|
+
previousStatus: 'idle',
|
|
49
|
+
pendingCount: 1,
|
|
50
|
+
endedCount: 0,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const collection = database.collection<NodeStatusDocument & { _id: string }>('NodeStatus');
|
|
56
|
+
const nodes = await collection.find();
|
|
57
|
+
|
|
58
|
+
expect(nodes.length).toBe(1);
|
|
59
|
+
expect(nodes[0]?.status).toBe('running');
|
|
60
|
+
} finally {
|
|
61
|
+
await close();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should update existing projection documents', async () => {
|
|
66
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
67
|
+
try {
|
|
68
|
+
await eventStore.appendToStream('pipeline-c1', [
|
|
69
|
+
{
|
|
70
|
+
type: 'ItemStatusChanged',
|
|
71
|
+
data: {
|
|
72
|
+
correlationId: 'c1',
|
|
73
|
+
commandType: 'Cmd',
|
|
74
|
+
itemKey: 'a',
|
|
75
|
+
requestId: 'r1',
|
|
76
|
+
status: 'running',
|
|
77
|
+
attemptCount: 1,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
await eventStore.appendToStream('pipeline-c1', [
|
|
83
|
+
{
|
|
84
|
+
type: 'ItemStatusChanged',
|
|
85
|
+
data: {
|
|
86
|
+
correlationId: 'c1',
|
|
87
|
+
commandType: 'Cmd',
|
|
88
|
+
itemKey: 'a',
|
|
89
|
+
requestId: 'r1',
|
|
90
|
+
status: 'success',
|
|
91
|
+
attemptCount: 1,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const collection = database.collection<ItemStatusDocument & { _id: string }>('ItemStatus');
|
|
97
|
+
const items = await collection.find();
|
|
98
|
+
|
|
99
|
+
expect(items.length).toBe(1);
|
|
100
|
+
expect(items[0]?.status).toBe('success');
|
|
101
|
+
} finally {
|
|
102
|
+
await close();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('readModel integration', () => {
|
|
108
|
+
it('should provide working read model queries', async () => {
|
|
109
|
+
const { eventStore, readModel, close } = createPipelineEventStore();
|
|
110
|
+
try {
|
|
111
|
+
await eventStore.appendToStream('pipeline-c1', [
|
|
112
|
+
{
|
|
113
|
+
type: 'ItemStatusChanged',
|
|
114
|
+
data: {
|
|
115
|
+
correlationId: 'c1',
|
|
116
|
+
commandType: 'Cmd',
|
|
117
|
+
itemKey: 'a',
|
|
118
|
+
requestId: 'r1',
|
|
119
|
+
status: 'running',
|
|
120
|
+
attemptCount: 1,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'ItemStatusChanged',
|
|
125
|
+
data: {
|
|
126
|
+
correlationId: 'c1',
|
|
127
|
+
commandType: 'Cmd',
|
|
128
|
+
itemKey: 'b',
|
|
129
|
+
requestId: 'r2',
|
|
130
|
+
status: 'success',
|
|
131
|
+
attemptCount: 1,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const stats = await readModel.computeCommandStats('c1', 'Cmd');
|
|
137
|
+
|
|
138
|
+
expect(stats).toEqual({
|
|
139
|
+
pendingCount: 1,
|
|
140
|
+
endedCount: 1,
|
|
141
|
+
aggregateStatus: 'running',
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
await close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should detect correlation via read model', async () => {
|
|
149
|
+
const { eventStore, readModel, close } = createPipelineEventStore();
|
|
150
|
+
try {
|
|
151
|
+
expect(await readModel.hasCorrelation('c1')).toBe(false);
|
|
152
|
+
|
|
153
|
+
await eventStore.appendToStream('pipeline-c1', [
|
|
154
|
+
{
|
|
155
|
+
type: 'NodeStatusChanged',
|
|
156
|
+
data: {
|
|
157
|
+
correlationId: 'c1',
|
|
158
|
+
commandName: 'Cmd',
|
|
159
|
+
nodeId: 'node-1',
|
|
160
|
+
status: 'running',
|
|
161
|
+
previousStatus: 'idle',
|
|
162
|
+
pendingCount: 1,
|
|
163
|
+
endedCount: 0,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
expect(await readModel.hasCorrelation('c1')).toBe(true);
|
|
169
|
+
} finally {
|
|
170
|
+
await close();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('settled instance projection', () => {
|
|
176
|
+
it('should project SettledInstanceCreated events', async () => {
|
|
177
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
178
|
+
try {
|
|
179
|
+
await eventStore.appendToStream('settled-c1', [
|
|
180
|
+
{
|
|
181
|
+
type: 'SettledInstanceCreated',
|
|
182
|
+
data: {
|
|
183
|
+
templateId: 'template-CmdA,CmdB',
|
|
184
|
+
correlationId: 'c1',
|
|
185
|
+
commandTypes: ['CmdA', 'CmdB'],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const collection = database.collection<SettledInstanceDocument & { _id: string }>('SettledInstance');
|
|
191
|
+
const instances = await collection.find();
|
|
192
|
+
|
|
193
|
+
expect(instances.length).toBe(1);
|
|
194
|
+
expect(instances[0]?.status).toBe('active');
|
|
195
|
+
expect(instances[0]?.commandTrackers).toHaveLength(2);
|
|
196
|
+
} finally {
|
|
197
|
+
await close();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should update settled instance through lifecycle events', async () => {
|
|
202
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
203
|
+
try {
|
|
204
|
+
await eventStore.appendToStream('settled-c1', [
|
|
205
|
+
{
|
|
206
|
+
type: 'SettledInstanceCreated',
|
|
207
|
+
data: {
|
|
208
|
+
templateId: 'template-CmdA',
|
|
209
|
+
correlationId: 'c1',
|
|
210
|
+
commandTypes: ['CmdA'],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
type: 'SettledCommandStarted',
|
|
215
|
+
data: {
|
|
216
|
+
templateId: 'template-CmdA',
|
|
217
|
+
correlationId: 'c1',
|
|
218
|
+
commandType: 'CmdA',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const collection = database.collection<SettledInstanceDocument & { _id: string }>('SettledInstance');
|
|
224
|
+
const instances = await collection.find();
|
|
225
|
+
|
|
226
|
+
expect(instances.length).toBe(1);
|
|
227
|
+
expect(instances[0]?.commandTrackers[0]?.hasStarted).toBe(true);
|
|
228
|
+
expect(instances[0]?.commandTrackers[0]?.hasCompleted).toBe(false);
|
|
229
|
+
} finally {
|
|
230
|
+
await close();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('phased execution projection', () => {
|
|
236
|
+
it('should project PhasedExecutionStarted events', async () => {
|
|
237
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
238
|
+
try {
|
|
239
|
+
await eventStore.appendToStream('phased-c1', [
|
|
240
|
+
{
|
|
241
|
+
type: 'PhasedExecutionStarted',
|
|
242
|
+
data: {
|
|
243
|
+
executionId: 'exec-1',
|
|
244
|
+
correlationId: 'c1',
|
|
245
|
+
handlerId: 'handler-1',
|
|
246
|
+
triggerEvent: { type: 'TestEvent', correlationId: 'c1', data: {} },
|
|
247
|
+
items: [{ key: 'a', phase: 'prepare', dispatched: false, completed: false }],
|
|
248
|
+
phases: ['prepare', 'execute'],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
const collection = database.collection<PhasedExecutionDocument & { _id: string }>('PhasedExecution');
|
|
254
|
+
const executions = await collection.find();
|
|
255
|
+
|
|
256
|
+
expect(executions.length).toBe(1);
|
|
257
|
+
expect(executions[0]?.status).toBe('active');
|
|
258
|
+
expect(executions[0]?.items).toHaveLength(1);
|
|
259
|
+
} finally {
|
|
260
|
+
await close();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should update phased execution through lifecycle events', async () => {
|
|
265
|
+
const { eventStore, database, close } = createPipelineEventStore();
|
|
266
|
+
try {
|
|
267
|
+
await eventStore.appendToStream('phased-c1', [
|
|
268
|
+
{
|
|
269
|
+
type: 'PhasedExecutionStarted',
|
|
270
|
+
data: {
|
|
271
|
+
executionId: 'exec-1',
|
|
272
|
+
correlationId: 'c1',
|
|
273
|
+
handlerId: 'handler-1',
|
|
274
|
+
triggerEvent: { type: 'TestEvent', correlationId: 'c1', data: {} },
|
|
275
|
+
items: [{ key: 'a', phase: 'prepare', dispatched: false, completed: false }],
|
|
276
|
+
phases: ['prepare'],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
type: 'PhasedItemDispatched',
|
|
281
|
+
data: { executionId: 'exec-1', itemKey: 'a', phase: 'prepare' },
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
type: 'PhasedItemCompleted',
|
|
285
|
+
data: {
|
|
286
|
+
executionId: 'exec-1',
|
|
287
|
+
itemKey: 'a',
|
|
288
|
+
resultEvent: { type: 'ItemDone', correlationId: 'c1', data: {} },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: 'PhasedExecutionCompleted',
|
|
293
|
+
data: { executionId: 'exec-1', success: true, results: ['a'] },
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
const collection = database.collection<PhasedExecutionDocument & { _id: string }>('PhasedExecution');
|
|
298
|
+
const executions = await collection.find();
|
|
299
|
+
|
|
300
|
+
expect(executions.length).toBe(1);
|
|
301
|
+
expect(executions[0]?.status).toBe('completed');
|
|
302
|
+
expect(executions[0]?.items[0]?.dispatched).toBe(true);
|
|
303
|
+
expect(executions[0]?.items[0]?.completed).toBe(true);
|
|
304
|
+
} finally {
|
|
305
|
+
await close();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getInMemoryDatabase,
|
|
3
|
+
getInMemoryEventStore,
|
|
4
|
+
type InMemoryDatabase,
|
|
5
|
+
type InMemoryEventStore,
|
|
6
|
+
type InMemoryReadEventMetadata,
|
|
7
|
+
inlineProjections,
|
|
8
|
+
inMemorySingleStreamProjection,
|
|
9
|
+
} from '@event-driven-io/emmett';
|
|
10
|
+
import type { AwaitEvent, AwaitTrackerDocument } from '../projections/await-tracker-projection';
|
|
11
|
+
import { evolve as evolveAwaitTracker } from '../projections/await-tracker-projection';
|
|
12
|
+
import type { ItemStatusChangedEvent, ItemStatusDocument } from '../projections/item-status-projection';
|
|
13
|
+
import { evolve as evolveItemStatus } from '../projections/item-status-projection';
|
|
14
|
+
import type { LatestRunDocument } from '../projections/latest-run-projection';
|
|
15
|
+
import { evolve as evolveLatestRun } from '../projections/latest-run-projection';
|
|
16
|
+
import type { MessageLogDocument, MessageLogEvent } from '../projections/message-log-projection';
|
|
17
|
+
import { evolve as evolveMessageLog } from '../projections/message-log-projection';
|
|
18
|
+
import type { NodeStatusChangedEvent, NodeStatusDocument } from '../projections/node-status-projection';
|
|
19
|
+
import { evolve as evolveNodeStatus } from '../projections/node-status-projection';
|
|
20
|
+
import type { PhasedExecutionDocument, PhasedExecutionEvent } from '../projections/phased-execution-projection';
|
|
21
|
+
import { evolve as evolvePhasedExecution } from '../projections/phased-execution-projection';
|
|
22
|
+
import type { SettledEvent, SettledInstanceDocument } from '../projections/settled-instance-projection';
|
|
23
|
+
import { evolve as evolveSettledInstance } from '../projections/settled-instance-projection';
|
|
24
|
+
import type { StatsDocument } from '../projections/stats-projection';
|
|
25
|
+
import { evolve as evolveStats } from '../projections/stats-projection';
|
|
26
|
+
import { PipelineReadModel } from './pipeline-read-model';
|
|
27
|
+
|
|
28
|
+
interface PipelineRunStartedEvent {
|
|
29
|
+
type: 'PipelineRunStarted';
|
|
30
|
+
data: {
|
|
31
|
+
correlationId: string;
|
|
32
|
+
triggerCommand: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createProjections() {
|
|
37
|
+
const itemStatusProjection = inMemorySingleStreamProjection<ItemStatusDocument, ItemStatusChangedEvent>({
|
|
38
|
+
collectionName: 'ItemStatus',
|
|
39
|
+
canHandle: ['ItemStatusChanged'],
|
|
40
|
+
getDocumentId: (event) => `${event.data.correlationId}-${event.data.commandType}-${event.data.itemKey}`,
|
|
41
|
+
evolve: (document: ItemStatusDocument | null, event: ItemStatusChangedEvent) => evolveItemStatus(document, event),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const nodeStatusProjection = inMemorySingleStreamProjection<NodeStatusDocument, NodeStatusChangedEvent>({
|
|
45
|
+
collectionName: 'NodeStatus',
|
|
46
|
+
canHandle: ['NodeStatusChanged'],
|
|
47
|
+
getDocumentId: (event) => `${event.data.correlationId}-${event.data.commandName}`,
|
|
48
|
+
evolve: (document: NodeStatusDocument | null, event: NodeStatusChangedEvent) => evolveNodeStatus(document, event),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const latestRunProjection = inMemorySingleStreamProjection<LatestRunDocument, PipelineRunStartedEvent>({
|
|
52
|
+
collectionName: 'LatestRun',
|
|
53
|
+
canHandle: ['PipelineRunStarted'],
|
|
54
|
+
getDocumentId: () => 'singleton',
|
|
55
|
+
evolve: (document: LatestRunDocument | null, event: PipelineRunStartedEvent) => evolveLatestRun(document, event),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const messageLogProjection = inMemorySingleStreamProjection<MessageLogDocument, MessageLogEvent>({
|
|
59
|
+
collectionName: 'MessageLog',
|
|
60
|
+
canHandle: ['CommandDispatched', 'DomainEventEmitted', 'PipelineRunStarted', 'NodeStatusChanged'],
|
|
61
|
+
getDocumentId: (event) => {
|
|
62
|
+
if (event.type === 'PipelineRunStarted') {
|
|
63
|
+
return `prs-${event.data.correlationId}`;
|
|
64
|
+
}
|
|
65
|
+
if (event.type === 'NodeStatusChanged') {
|
|
66
|
+
return `nsc-${event.data.correlationId}-${event.data.commandName}-${event.data.status}`;
|
|
67
|
+
}
|
|
68
|
+
if (event.type === 'DomainEventEmitted') {
|
|
69
|
+
return `dee-${event.data.requestId}-${event.data.eventType}`;
|
|
70
|
+
}
|
|
71
|
+
return `cmd-${event.data.requestId}`;
|
|
72
|
+
},
|
|
73
|
+
evolve: (document: MessageLogDocument | null, event: MessageLogEvent) => evolveMessageLog(document, event),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const statsProjection = inMemorySingleStreamProjection<StatsDocument, MessageLogEvent>({
|
|
77
|
+
collectionName: 'Stats',
|
|
78
|
+
canHandle: ['CommandDispatched', 'DomainEventEmitted'],
|
|
79
|
+
getDocumentId: () => 'global',
|
|
80
|
+
evolve: (document: StatsDocument | null, event: MessageLogEvent) => evolveStats(document, event),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const settledInstanceProjection = inMemorySingleStreamProjection<SettledInstanceDocument, SettledEvent>({
|
|
84
|
+
collectionName: 'SettledInstance',
|
|
85
|
+
canHandle: [
|
|
86
|
+
'SettledInstanceCreated',
|
|
87
|
+
'SettledCommandStarted',
|
|
88
|
+
'SettledEventReceived',
|
|
89
|
+
'SettledHandlerFired',
|
|
90
|
+
'SettledInstanceReset',
|
|
91
|
+
'SettledInstanceCleaned',
|
|
92
|
+
],
|
|
93
|
+
getDocumentId: (event) => `${event.data.templateId}-${event.data.correlationId}`,
|
|
94
|
+
evolve: (document: SettledInstanceDocument | null, event: SettledEvent) => evolveSettledInstance(document, event),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const phasedExecutionProjection = inMemorySingleStreamProjection<PhasedExecutionDocument, PhasedExecutionEvent>({
|
|
98
|
+
collectionName: 'PhasedExecution',
|
|
99
|
+
canHandle: [
|
|
100
|
+
'PhasedExecutionStarted',
|
|
101
|
+
'PhasedItemDispatched',
|
|
102
|
+
'PhasedItemCompleted',
|
|
103
|
+
'PhasedItemFailed',
|
|
104
|
+
'PhasedPhaseAdvanced',
|
|
105
|
+
'PhasedExecutionCompleted',
|
|
106
|
+
],
|
|
107
|
+
getDocumentId: (event) => event.data.executionId,
|
|
108
|
+
evolve: (document: PhasedExecutionDocument | null, event: PhasedExecutionEvent) =>
|
|
109
|
+
evolvePhasedExecution(document, event),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const awaitTrackerProjection = inMemorySingleStreamProjection<AwaitTrackerDocument, AwaitEvent>({
|
|
113
|
+
collectionName: 'AwaitTracker',
|
|
114
|
+
canHandle: ['AwaitStarted', 'AwaitItemCompleted', 'AwaitCompleted'],
|
|
115
|
+
getDocumentId: (event) => event.data.correlationId,
|
|
116
|
+
evolve: (document: AwaitTrackerDocument | null, event: AwaitEvent) => evolveAwaitTracker(document, event),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return inlineProjections<InMemoryReadEventMetadata>([
|
|
120
|
+
itemStatusProjection,
|
|
121
|
+
nodeStatusProjection,
|
|
122
|
+
latestRunProjection,
|
|
123
|
+
messageLogProjection,
|
|
124
|
+
statsProjection,
|
|
125
|
+
settledInstanceProjection,
|
|
126
|
+
phasedExecutionProjection,
|
|
127
|
+
awaitTrackerProjection,
|
|
128
|
+
] as Parameters<typeof inlineProjections<InMemoryReadEventMetadata>>[0]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface PipelineEventStoreContext {
|
|
132
|
+
eventStore: InMemoryEventStore;
|
|
133
|
+
database: InMemoryDatabase;
|
|
134
|
+
readModel: PipelineReadModel;
|
|
135
|
+
close: () => Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createPipelineEventStore(): PipelineEventStoreContext {
|
|
139
|
+
const database = getInMemoryDatabase();
|
|
140
|
+
const eventStore = getInMemoryEventStore({
|
|
141
|
+
database,
|
|
142
|
+
projections: createProjections() as Parameters<typeof getInMemoryEventStore>[0] extends { projections?: infer P }
|
|
143
|
+
? P
|
|
144
|
+
: never,
|
|
145
|
+
});
|
|
146
|
+
const readModel = new PipelineReadModel(database);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
eventStore,
|
|
150
|
+
database,
|
|
151
|
+
readModel,
|
|
152
|
+
close: async () => {
|
|
153
|
+
// No-op for in-memory store
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|