@auto-engineer/pipeline 1.74.0 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +54 -0
- package/dist/src/projections/item-status-projection.d.ts +4 -1
- package/dist/src/projections/item-status-projection.d.ts.map +1 -1
- package/dist/src/projections/item-status-projection.js +10 -2
- package/dist/src/projections/item-status-projection.js.map +1 -1
- package/dist/src/projections/message-log-projection.d.ts +1 -0
- package/dist/src/projections/message-log-projection.d.ts.map +1 -1
- package/dist/src/projections/message-log-projection.js +1 -0
- package/dist/src/projections/message-log-projection.js.map +1 -1
- package/dist/src/projections/node-status-projection.d.ts +3 -1
- package/dist/src/projections/node-status-projection.d.ts.map +1 -1
- package/dist/src/projections/node-status-projection.js +2 -1
- package/dist/src/projections/node-status-projection.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +17 -6
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/store/pipeline-read-model.d.ts.map +1 -1
- package/dist/src/store/pipeline-read-model.js +3 -0
- package/dist/src/store/pipeline-read-model.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/projections/item-status-projection.specs.ts +83 -0
- package/src/projections/item-status-projection.ts +14 -2
- package/src/projections/message-log-projection.ts +2 -0
- package/src/projections/node-status-projection.specs.ts +108 -0
- package/src/projections/node-status-projection.ts +4 -1
- package/src/server/pipeline-server.specs.ts +76 -0
- package/src/server/pipeline-server.ts +31 -5
- package/src/store/pipeline-read-model.ts +3 -0
package/package.json
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"get-port": "^7.1.0",
|
|
14
14
|
"jose": "^5.9.6",
|
|
15
15
|
"nanoid": "^5.0.0",
|
|
16
|
-
"@auto-engineer/file-store": "1.
|
|
17
|
-
"@auto-engineer/message-bus": "1.
|
|
16
|
+
"@auto-engineer/file-store": "1.76.0",
|
|
17
|
+
"@auto-engineer/message-bus": "1.76.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/cors": "^2.8.17",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
|
-
"version": "1.
|
|
26
|
+
"version": "1.76.0",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
|
|
29
29
|
"test": "vitest run --reporter=dot",
|
|
@@ -126,5 +126,88 @@ describe('ItemStatusProjection', () => {
|
|
|
126
126
|
attemptCount: 2,
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it('sets startedAt when status becomes running', () => {
|
|
131
|
+
const now = new Date('2025-01-01T00:00:00Z');
|
|
132
|
+
const event: ItemStatusChangedEvent = {
|
|
133
|
+
type: 'ItemStatusChanged',
|
|
134
|
+
data: {
|
|
135
|
+
correlationId: 'c1',
|
|
136
|
+
commandType: 'ProcessItem',
|
|
137
|
+
itemKey: 'item-1',
|
|
138
|
+
requestId: 'req-1',
|
|
139
|
+
status: 'running',
|
|
140
|
+
attemptCount: 1,
|
|
141
|
+
timestamp: now.toISOString(),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = evolve(null, event);
|
|
146
|
+
|
|
147
|
+
expect(result.startedAt).toEqual(now.toISOString());
|
|
148
|
+
expect(result.endedAt).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('sets endedAt when status becomes success', () => {
|
|
152
|
+
const startTime = new Date('2025-01-01T00:00:00Z');
|
|
153
|
+
const endTime = new Date('2025-01-01T00:00:05Z');
|
|
154
|
+
const existing: ItemStatusDocument = {
|
|
155
|
+
correlationId: 'c1',
|
|
156
|
+
commandType: 'ProcessItem',
|
|
157
|
+
itemKey: 'item-1',
|
|
158
|
+
currentRequestId: 'req-1',
|
|
159
|
+
status: 'running',
|
|
160
|
+
attemptCount: 1,
|
|
161
|
+
startedAt: startTime.toISOString(),
|
|
162
|
+
};
|
|
163
|
+
const event: ItemStatusChangedEvent = {
|
|
164
|
+
type: 'ItemStatusChanged',
|
|
165
|
+
data: {
|
|
166
|
+
correlationId: 'c1',
|
|
167
|
+
commandType: 'ProcessItem',
|
|
168
|
+
itemKey: 'item-1',
|
|
169
|
+
requestId: 'req-1',
|
|
170
|
+
status: 'success',
|
|
171
|
+
attemptCount: 1,
|
|
172
|
+
timestamp: endTime.toISOString(),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = evolve(existing, event);
|
|
177
|
+
|
|
178
|
+
expect(result.startedAt).toEqual(startTime.toISOString());
|
|
179
|
+
expect(result.endedAt).toEqual(endTime.toISOString());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('sets endedAt when status becomes error', () => {
|
|
183
|
+
const startTime = new Date('2025-01-01T00:00:00Z');
|
|
184
|
+
const endTime = new Date('2025-01-01T00:00:03Z');
|
|
185
|
+
const existing: ItemStatusDocument = {
|
|
186
|
+
correlationId: 'c1',
|
|
187
|
+
commandType: 'ProcessItem',
|
|
188
|
+
itemKey: 'item-1',
|
|
189
|
+
currentRequestId: 'req-1',
|
|
190
|
+
status: 'running',
|
|
191
|
+
attemptCount: 1,
|
|
192
|
+
startedAt: startTime.toISOString(),
|
|
193
|
+
};
|
|
194
|
+
const event: ItemStatusChangedEvent = {
|
|
195
|
+
type: 'ItemStatusChanged',
|
|
196
|
+
data: {
|
|
197
|
+
correlationId: 'c1',
|
|
198
|
+
commandType: 'ProcessItem',
|
|
199
|
+
itemKey: 'item-1',
|
|
200
|
+
requestId: 'req-1',
|
|
201
|
+
status: 'error',
|
|
202
|
+
attemptCount: 1,
|
|
203
|
+
timestamp: endTime.toISOString(),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = evolve(existing, event);
|
|
208
|
+
|
|
209
|
+
expect(result.startedAt).toEqual(startTime.toISOString());
|
|
210
|
+
expect(result.endedAt).toEqual(endTime.toISOString());
|
|
211
|
+
});
|
|
129
212
|
});
|
|
130
213
|
});
|
|
@@ -6,6 +6,8 @@ export interface ItemStatusDocument {
|
|
|
6
6
|
currentRequestId: string;
|
|
7
7
|
status: 'running' | 'success' | 'error';
|
|
8
8
|
attemptCount: number;
|
|
9
|
+
startedAt?: string;
|
|
10
|
+
endedAt?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface ItemStatusChangedEvent {
|
|
@@ -17,11 +19,12 @@ export interface ItemStatusChangedEvent {
|
|
|
17
19
|
requestId: string;
|
|
18
20
|
status: 'running' | 'success' | 'error';
|
|
19
21
|
attemptCount: number;
|
|
22
|
+
timestamp?: string;
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
export function evolve(
|
|
24
|
-
|
|
26
|
+
export function evolve(document: ItemStatusDocument | null, event: ItemStatusChangedEvent): ItemStatusDocument {
|
|
27
|
+
const base: ItemStatusDocument = {
|
|
25
28
|
correlationId: event.data.correlationId,
|
|
26
29
|
commandType: event.data.commandType,
|
|
27
30
|
itemKey: event.data.itemKey,
|
|
@@ -29,4 +32,13 @@ export function evolve(_document: ItemStatusDocument | null, event: ItemStatusCh
|
|
|
29
32
|
status: event.data.status,
|
|
30
33
|
attemptCount: event.data.attemptCount,
|
|
31
34
|
};
|
|
35
|
+
|
|
36
|
+
if (event.data.status === 'running') {
|
|
37
|
+
base.startedAt = event.data.timestamp;
|
|
38
|
+
} else {
|
|
39
|
+
base.startedAt = document?.startedAt;
|
|
40
|
+
base.endedAt = event.data.timestamp;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return base;
|
|
32
44
|
}
|
|
@@ -50,6 +50,7 @@ export interface NodeStatusChangedLogEvent {
|
|
|
50
50
|
previousStatus: NodeStatus;
|
|
51
51
|
pendingCount: number;
|
|
52
52
|
endedCount: number;
|
|
53
|
+
lastDurationMs?: number;
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -107,6 +108,7 @@ export function evolve(_document: MessageLogDocument | null, event: MessageLogEv
|
|
|
107
108
|
previousStatus: event.data.previousStatus,
|
|
108
109
|
pendingCount: event.data.pendingCount,
|
|
109
110
|
endedCount: event.data.endedCount,
|
|
111
|
+
lastDurationMs: event.data.lastDurationMs,
|
|
110
112
|
},
|
|
111
113
|
timestamp: new Date(),
|
|
112
114
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { NodeStatus } from '../graph/types';
|
|
3
|
+
import { evolve, type NodeStatusChangedEvent, type NodeStatusDocument } from './node-status-projection';
|
|
4
|
+
|
|
5
|
+
describe('NodeStatusProjection', () => {
|
|
6
|
+
describe('evolve', () => {
|
|
7
|
+
it('creates node document from event', () => {
|
|
8
|
+
const event: NodeStatusChangedEvent = {
|
|
9
|
+
type: 'NodeStatusChanged',
|
|
10
|
+
data: {
|
|
11
|
+
correlationId: 'c1',
|
|
12
|
+
commandName: 'RunCmd',
|
|
13
|
+
nodeId: 'cmd:RunCmd',
|
|
14
|
+
status: 'running' as NodeStatus,
|
|
15
|
+
previousStatus: 'idle' as NodeStatus,
|
|
16
|
+
pendingCount: 1,
|
|
17
|
+
endedCount: 0,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const result = evolve(null, event);
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
correlationId: 'c1',
|
|
25
|
+
commandName: 'RunCmd',
|
|
26
|
+
status: 'running',
|
|
27
|
+
pendingCount: 1,
|
|
28
|
+
endedCount: 0,
|
|
29
|
+
lastDurationMs: undefined,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('stores lastDurationMs when provided in event', () => {
|
|
34
|
+
const event: NodeStatusChangedEvent = {
|
|
35
|
+
type: 'NodeStatusChanged',
|
|
36
|
+
data: {
|
|
37
|
+
correlationId: 'c1',
|
|
38
|
+
commandName: 'RunCmd',
|
|
39
|
+
nodeId: 'cmd:RunCmd',
|
|
40
|
+
status: 'success' as NodeStatus,
|
|
41
|
+
previousStatus: 'running' as NodeStatus,
|
|
42
|
+
pendingCount: 0,
|
|
43
|
+
endedCount: 1,
|
|
44
|
+
lastDurationMs: 5000,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = evolve(null, event);
|
|
49
|
+
|
|
50
|
+
expect(result.lastDurationMs).toBe(5000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('preserves lastDurationMs from existing document when event has none', () => {
|
|
54
|
+
const existing: NodeStatusDocument = {
|
|
55
|
+
correlationId: 'c1',
|
|
56
|
+
commandName: 'RunCmd',
|
|
57
|
+
status: 'success' as NodeStatus,
|
|
58
|
+
pendingCount: 0,
|
|
59
|
+
endedCount: 1,
|
|
60
|
+
lastDurationMs: 3000,
|
|
61
|
+
};
|
|
62
|
+
const event: NodeStatusChangedEvent = {
|
|
63
|
+
type: 'NodeStatusChanged',
|
|
64
|
+
data: {
|
|
65
|
+
correlationId: 'c1',
|
|
66
|
+
commandName: 'RunCmd',
|
|
67
|
+
nodeId: 'cmd:RunCmd',
|
|
68
|
+
status: 'running' as NodeStatus,
|
|
69
|
+
previousStatus: 'success' as NodeStatus,
|
|
70
|
+
pendingCount: 1,
|
|
71
|
+
endedCount: 1,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const result = evolve(existing, event);
|
|
76
|
+
|
|
77
|
+
expect(result.lastDurationMs).toBe(3000);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('overwrites lastDurationMs when event provides new value', () => {
|
|
81
|
+
const existing: NodeStatusDocument = {
|
|
82
|
+
correlationId: 'c1',
|
|
83
|
+
commandName: 'RunCmd',
|
|
84
|
+
status: 'success' as NodeStatus,
|
|
85
|
+
pendingCount: 0,
|
|
86
|
+
endedCount: 1,
|
|
87
|
+
lastDurationMs: 3000,
|
|
88
|
+
};
|
|
89
|
+
const event: NodeStatusChangedEvent = {
|
|
90
|
+
type: 'NodeStatusChanged',
|
|
91
|
+
data: {
|
|
92
|
+
correlationId: 'c1',
|
|
93
|
+
commandName: 'RunCmd',
|
|
94
|
+
nodeId: 'cmd:RunCmd',
|
|
95
|
+
status: 'success' as NodeStatus,
|
|
96
|
+
previousStatus: 'running' as NodeStatus,
|
|
97
|
+
pendingCount: 0,
|
|
98
|
+
endedCount: 2,
|
|
99
|
+
lastDurationMs: 7000,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const result = evolve(existing, event);
|
|
104
|
+
|
|
105
|
+
expect(result.lastDurationMs).toBe(7000);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -7,6 +7,7 @@ export interface NodeStatusDocument {
|
|
|
7
7
|
status: NodeStatus;
|
|
8
8
|
pendingCount: number;
|
|
9
9
|
endedCount: number;
|
|
10
|
+
lastDurationMs?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface NodeStatusChangedEvent {
|
|
@@ -19,15 +20,17 @@ export interface NodeStatusChangedEvent {
|
|
|
19
20
|
previousStatus: NodeStatus;
|
|
20
21
|
pendingCount: number;
|
|
21
22
|
endedCount: number;
|
|
23
|
+
lastDurationMs?: number;
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
export function evolve(
|
|
27
|
+
export function evolve(document: NodeStatusDocument | null, event: NodeStatusChangedEvent): NodeStatusDocument {
|
|
26
28
|
return {
|
|
27
29
|
correlationId: event.data.correlationId,
|
|
28
30
|
commandName: event.data.commandName,
|
|
29
31
|
status: event.data.status,
|
|
30
32
|
pendingCount: event.data.pendingCount,
|
|
31
33
|
endedCount: event.data.endedCount,
|
|
34
|
+
lastDurationMs: event.data.lastDurationMs ?? document?.lastDurationMs,
|
|
32
35
|
};
|
|
33
36
|
}
|
|
@@ -33,6 +33,7 @@ interface GraphNode {
|
|
|
33
33
|
status?: string;
|
|
34
34
|
pendingCount?: number;
|
|
35
35
|
endedCount?: number;
|
|
36
|
+
lastDurationMs?: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
interface PipelineResponse {
|
|
@@ -1852,6 +1853,81 @@ describe('PipelineServer', () => {
|
|
|
1852
1853
|
await server.stop();
|
|
1853
1854
|
});
|
|
1854
1855
|
|
|
1856
|
+
it('should include lastDurationMs in pipeline graph after command completes', async () => {
|
|
1857
|
+
const handler = {
|
|
1858
|
+
name: 'DurationCmd',
|
|
1859
|
+
events: ['DurationDone'],
|
|
1860
|
+
handle: async () => {
|
|
1861
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1862
|
+
return { type: 'DurationDone', data: {} };
|
|
1863
|
+
},
|
|
1864
|
+
};
|
|
1865
|
+
const pipeline = define('test').on('Trigger').emit('DurationCmd', {}).build();
|
|
1866
|
+
const server = new PipelineServer({ port: 0 });
|
|
1867
|
+
server.registerCommandHandlers([handler]);
|
|
1868
|
+
server.registerPipeline(pipeline);
|
|
1869
|
+
server.registerItemKeyExtractor('DurationCmd', (d) => (d as { id?: string }).id);
|
|
1870
|
+
await server.start();
|
|
1871
|
+
|
|
1872
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1873
|
+
method: 'POST',
|
|
1874
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1875
|
+
body: JSON.stringify({ type: 'DurationCmd', data: { id: 'item-1' } }),
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1879
|
+
|
|
1880
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1881
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:DurationCmd');
|
|
1882
|
+
expect(cmdNode).toBeDefined();
|
|
1883
|
+
expect(cmdNode?.lastDurationMs).toBeTypeOf('number');
|
|
1884
|
+
expect(cmdNode?.lastDurationMs).toBeGreaterThanOrEqual(0);
|
|
1885
|
+
|
|
1886
|
+
await server.stop();
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
it('should include lastDurationMs in NodeStatusChanged events on completion', async () => {
|
|
1890
|
+
const handler = {
|
|
1891
|
+
name: 'DurationEvtCmd',
|
|
1892
|
+
events: ['DurationEvtDone'],
|
|
1893
|
+
handle: async () => {
|
|
1894
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1895
|
+
return { type: 'DurationEvtDone', data: {} };
|
|
1896
|
+
},
|
|
1897
|
+
};
|
|
1898
|
+
const pipeline = define('test').on('Trigger').emit('DurationEvtCmd', {}).build();
|
|
1899
|
+
const server = new PipelineServer({ port: 0 });
|
|
1900
|
+
server.registerCommandHandlers([handler]);
|
|
1901
|
+
server.registerPipeline(pipeline);
|
|
1902
|
+
server.registerItemKeyExtractor('DurationEvtCmd', (d) => (d as { id?: string }).id);
|
|
1903
|
+
await server.start();
|
|
1904
|
+
|
|
1905
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1906
|
+
method: 'POST',
|
|
1907
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1908
|
+
body: JSON.stringify({ type: 'DurationEvtCmd', data: { id: 'item-1' } }),
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1912
|
+
|
|
1913
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
1914
|
+
type NodeStatusChangedMessage = {
|
|
1915
|
+
type: string;
|
|
1916
|
+
data?: {
|
|
1917
|
+
status?: string;
|
|
1918
|
+
lastDurationMs?: number;
|
|
1919
|
+
};
|
|
1920
|
+
};
|
|
1921
|
+
const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
|
|
1922
|
+
const successEvent = nodeStatusChanged.find(
|
|
1923
|
+
(m) => (m.message as NodeStatusChangedMessage).data?.status === 'success',
|
|
1924
|
+
);
|
|
1925
|
+
expect(successEvent).toBeDefined();
|
|
1926
|
+
expect((successEvent?.message as NodeStatusChangedMessage).data?.lastDurationMs).toBeTypeOf('number');
|
|
1927
|
+
|
|
1928
|
+
await server.stop();
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1855
1931
|
it('should use requestId as fallback when no itemKey extractor is registered', async () => {
|
|
1856
1932
|
const handler = {
|
|
1857
1933
|
name: 'NoExtractorCmd',
|
|
@@ -46,6 +46,10 @@ interface EventWithCorrelation extends Event {
|
|
|
46
46
|
correlationId: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
interface GraphNodeWithDuration extends GraphNode {
|
|
50
|
+
lastDurationMs?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
export class PipelineServer {
|
|
50
54
|
private app: express.Application;
|
|
51
55
|
private httpServer: HttpServer;
|
|
@@ -370,6 +374,7 @@ export class PipelineServer {
|
|
|
370
374
|
messageType: m.messageType,
|
|
371
375
|
revision: String(index),
|
|
372
376
|
position: String(index),
|
|
377
|
+
timestamp: m.timestamp,
|
|
373
378
|
}));
|
|
374
379
|
res.json(serialized);
|
|
375
380
|
})();
|
|
@@ -494,17 +499,19 @@ export class PipelineServer {
|
|
|
494
499
|
};
|
|
495
500
|
}
|
|
496
501
|
|
|
497
|
-
private async addStatusToCommandNode(node: GraphNode, correlationId?: string): Promise<
|
|
502
|
+
private async addStatusToCommandNode(node: GraphNode, correlationId?: string): Promise<GraphNodeWithDuration> {
|
|
498
503
|
const commandName = node.id.replace(/^cmd:/, '');
|
|
499
504
|
if (correlationId === undefined) {
|
|
500
505
|
return { ...node, status: 'idle' as NodeStatus, pendingCount: 0, endedCount: 0 };
|
|
501
506
|
}
|
|
502
507
|
const stats = await this.computeCommandStats(correlationId, commandName);
|
|
508
|
+
const nodeStatus = await this.eventStoreContext.readModel.getNodeStatus(correlationId, commandName);
|
|
503
509
|
return {
|
|
504
510
|
...node,
|
|
505
511
|
status: stats.aggregateStatus,
|
|
506
512
|
pendingCount: stats.pendingCount,
|
|
507
513
|
endedCount: stats.endedCount,
|
|
514
|
+
lastDurationMs: nodeStatus?.lastDurationMs,
|
|
508
515
|
};
|
|
509
516
|
}
|
|
510
517
|
|
|
@@ -541,6 +548,7 @@ export class PipelineServer {
|
|
|
541
548
|
requestId,
|
|
542
549
|
status,
|
|
543
550
|
attemptCount,
|
|
551
|
+
timestamp: new Date().toISOString(),
|
|
544
552
|
},
|
|
545
553
|
},
|
|
546
554
|
]);
|
|
@@ -551,6 +559,7 @@ export class PipelineServer {
|
|
|
551
559
|
commandName: string,
|
|
552
560
|
status: NodeStatus,
|
|
553
561
|
previousStatus: NodeStatus,
|
|
562
|
+
lastDurationMs?: number,
|
|
554
563
|
): Promise<void> {
|
|
555
564
|
const stats = await this.computeCommandStats(correlationId, commandName);
|
|
556
565
|
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
@@ -564,6 +573,7 @@ export class PipelineServer {
|
|
|
564
573
|
previousStatus,
|
|
565
574
|
pendingCount: stats.pendingCount,
|
|
566
575
|
endedCount: stats.endedCount,
|
|
576
|
+
lastDurationMs,
|
|
567
577
|
},
|
|
568
578
|
},
|
|
569
579
|
]);
|
|
@@ -621,11 +631,16 @@ export class PipelineServer {
|
|
|
621
631
|
]);
|
|
622
632
|
}
|
|
623
633
|
|
|
624
|
-
private async updateNodeStatus(
|
|
634
|
+
private async updateNodeStatus(
|
|
635
|
+
correlationId: string,
|
|
636
|
+
commandName: string,
|
|
637
|
+
status: NodeStatus,
|
|
638
|
+
lastDurationMs?: number,
|
|
639
|
+
): Promise<void> {
|
|
625
640
|
const existing = await this.eventStoreContext.readModel.getNodeStatus(correlationId, commandName);
|
|
626
641
|
const previousStatus: NodeStatus = existing?.status ?? 'idle';
|
|
627
|
-
await this.emitNodeStatusChanged(correlationId, commandName, status, previousStatus);
|
|
628
|
-
await this.broadcastNodeStatusChanged(correlationId, commandName, status, previousStatus);
|
|
642
|
+
await this.emitNodeStatusChanged(correlationId, commandName, status, previousStatus, lastDurationMs);
|
|
643
|
+
await this.broadcastNodeStatusChanged(correlationId, commandName, status, previousStatus, lastDurationMs);
|
|
629
644
|
}
|
|
630
645
|
|
|
631
646
|
private async broadcastNodeStatusChanged(
|
|
@@ -633,6 +648,7 @@ export class PipelineServer {
|
|
|
633
648
|
commandName: string,
|
|
634
649
|
status: NodeStatus,
|
|
635
650
|
previousStatus: NodeStatus,
|
|
651
|
+
lastDurationMs?: number,
|
|
636
652
|
): Promise<void> {
|
|
637
653
|
const stats = await this.computeCommandStats(correlationId, commandName);
|
|
638
654
|
const event: Event & { correlationId: string } = {
|
|
@@ -643,6 +659,7 @@ export class PipelineServer {
|
|
|
643
659
|
previousStatus,
|
|
644
660
|
pendingCount: stats.pendingCount,
|
|
645
661
|
endedCount: stats.endedCount,
|
|
662
|
+
lastDurationMs,
|
|
646
663
|
},
|
|
647
664
|
correlationId,
|
|
648
665
|
};
|
|
@@ -1022,7 +1039,16 @@ export class PipelineServer {
|
|
|
1022
1039
|
|
|
1023
1040
|
const finalStatus = this.getStatusFromEvents(events);
|
|
1024
1041
|
await this.updateItemStatus(this.currentSessionId, command.type, itemKey, finalStatus);
|
|
1025
|
-
await this.
|
|
1042
|
+
const completedItem = await this.eventStoreContext.readModel.getItemStatus(
|
|
1043
|
+
this.currentSessionId,
|
|
1044
|
+
command.type,
|
|
1045
|
+
itemKey,
|
|
1046
|
+
);
|
|
1047
|
+
let durationMs: number | undefined;
|
|
1048
|
+
if (completedItem?.startedAt && completedItem?.endedAt) {
|
|
1049
|
+
durationMs = new Date(completedItem.endedAt).getTime() - new Date(completedItem.startedAt).getTime();
|
|
1050
|
+
}
|
|
1051
|
+
await this.updateNodeStatus(this.currentSessionId, command.type, finalStatus, durationMs);
|
|
1026
1052
|
|
|
1027
1053
|
const eventsWithIds: EventWithCorrelation[] = events.map((event) => ({
|
|
1028
1054
|
...event,
|
|
@@ -91,6 +91,7 @@ export class PipelineReadModel {
|
|
|
91
91
|
status: node.status,
|
|
92
92
|
pendingCount: node.pendingCount,
|
|
93
93
|
endedCount: node.endedCount,
|
|
94
|
+
lastDurationMs: node.lastDurationMs,
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -109,6 +110,8 @@ export class PipelineReadModel {
|
|
|
109
110
|
currentRequestId: item.currentRequestId,
|
|
110
111
|
status: item.status,
|
|
111
112
|
attemptCount: item.attemptCount,
|
|
113
|
+
startedAt: item.startedAt,
|
|
114
|
+
endedAt: item.endedAt,
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
117
|
|