@auto-engineer/pipeline 0.0.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +279 -0
  4. package/dist/src/builder/define.d.ts +6 -2
  5. package/dist/src/builder/define.d.ts.map +1 -1
  6. package/dist/src/builder/define.js +17 -7
  7. package/dist/src/builder/define.js.map +1 -1
  8. package/dist/src/builder/define.specs.js +3 -3
  9. package/dist/src/builder/define.specs.js.map +1 -1
  10. package/dist/src/core/descriptors.d.ts +6 -2
  11. package/dist/src/core/descriptors.d.ts.map +1 -1
  12. package/dist/src/graph/filter-graph.d.ts +3 -0
  13. package/dist/src/graph/filter-graph.d.ts.map +1 -0
  14. package/dist/src/graph/filter-graph.js +80 -0
  15. package/dist/src/graph/filter-graph.js.map +1 -0
  16. package/dist/src/graph/filter-graph.specs.d.ts +2 -0
  17. package/dist/src/graph/filter-graph.specs.d.ts.map +1 -0
  18. package/dist/src/graph/filter-graph.specs.js +204 -0
  19. package/dist/src/graph/filter-graph.specs.js.map +1 -0
  20. package/dist/src/graph/types.d.ts +8 -0
  21. package/dist/src/graph/types.d.ts.map +1 -1
  22. package/dist/src/index.d.ts +1 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/projections/await-tracker-projection.d.ts +31 -0
  26. package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
  27. package/dist/src/projections/await-tracker-projection.js +35 -0
  28. package/dist/src/projections/await-tracker-projection.js.map +1 -0
  29. package/dist/src/projections/index.d.ts +4 -0
  30. package/dist/src/projections/index.d.ts.map +1 -0
  31. package/dist/src/projections/index.js +4 -0
  32. package/dist/src/projections/index.js.map +1 -0
  33. package/dist/src/projections/item-status-projection.d.ts +22 -0
  34. package/dist/src/projections/item-status-projection.d.ts.map +1 -0
  35. package/dist/src/projections/item-status-projection.js +11 -0
  36. package/dist/src/projections/item-status-projection.js.map +1 -0
  37. package/dist/src/projections/item-status-projection.specs.d.ts +2 -0
  38. package/dist/src/projections/item-status-projection.specs.d.ts.map +1 -0
  39. package/dist/src/projections/item-status-projection.specs.js +119 -0
  40. package/dist/src/projections/item-status-projection.specs.js.map +1 -0
  41. package/dist/src/projections/latest-run-projection.d.ts +15 -0
  42. package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
  43. package/dist/src/projections/latest-run-projection.js +7 -0
  44. package/dist/src/projections/latest-run-projection.js.map +1 -0
  45. package/dist/src/projections/latest-run-projection.specs.d.ts +2 -0
  46. package/dist/src/projections/latest-run-projection.specs.d.ts.map +1 -0
  47. package/dist/src/projections/latest-run-projection.specs.js +33 -0
  48. package/dist/src/projections/latest-run-projection.specs.js.map +1 -0
  49. package/dist/src/projections/message-log-projection.d.ts +51 -0
  50. package/dist/src/projections/message-log-projection.d.ts.map +1 -0
  51. package/dist/src/projections/message-log-projection.js +51 -0
  52. package/dist/src/projections/message-log-projection.js.map +1 -0
  53. package/dist/src/projections/message-log-projection.specs.d.ts +2 -0
  54. package/dist/src/projections/message-log-projection.specs.d.ts.map +1 -0
  55. package/dist/src/projections/message-log-projection.specs.js +101 -0
  56. package/dist/src/projections/message-log-projection.specs.js.map +1 -0
  57. package/dist/src/projections/node-status-projection.d.ts +23 -0
  58. package/dist/src/projections/node-status-projection.d.ts.map +1 -0
  59. package/dist/src/projections/node-status-projection.js +10 -0
  60. package/dist/src/projections/node-status-projection.js.map +1 -0
  61. package/dist/src/projections/node-status-projection.specs.d.ts +2 -0
  62. package/dist/src/projections/node-status-projection.specs.d.ts.map +1 -0
  63. package/dist/src/projections/node-status-projection.specs.js +116 -0
  64. package/dist/src/projections/node-status-projection.specs.js.map +1 -0
  65. package/dist/src/projections/phased-execution-projection.d.ts +77 -0
  66. package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
  67. package/dist/src/projections/phased-execution-projection.js +54 -0
  68. package/dist/src/projections/phased-execution-projection.js.map +1 -0
  69. package/dist/src/projections/phased-execution-projection.specs.d.ts +2 -0
  70. package/dist/src/projections/phased-execution-projection.specs.d.ts.map +1 -0
  71. package/dist/src/projections/phased-execution-projection.specs.js +171 -0
  72. package/dist/src/projections/phased-execution-projection.specs.js.map +1 -0
  73. package/dist/src/projections/settled-instance-projection.d.ts +67 -0
  74. package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
  75. package/dist/src/projections/settled-instance-projection.js +66 -0
  76. package/dist/src/projections/settled-instance-projection.js.map +1 -0
  77. package/dist/src/projections/settled-instance-projection.specs.d.ts +2 -0
  78. package/dist/src/projections/settled-instance-projection.specs.d.ts.map +1 -0
  79. package/dist/src/projections/settled-instance-projection.specs.js +217 -0
  80. package/dist/src/projections/settled-instance-projection.specs.js.map +1 -0
  81. package/dist/src/projections/stats-projection.d.ts +9 -0
  82. package/dist/src/projections/stats-projection.d.ts.map +1 -0
  83. package/dist/src/projections/stats-projection.js +16 -0
  84. package/dist/src/projections/stats-projection.js.map +1 -0
  85. package/dist/src/projections/stats-projection.specs.d.ts +2 -0
  86. package/dist/src/projections/stats-projection.specs.d.ts.map +1 -0
  87. package/dist/src/projections/stats-projection.specs.js +91 -0
  88. package/dist/src/projections/stats-projection.specs.js.map +1 -0
  89. package/dist/src/runtime/await-tracker.d.ts +17 -7
  90. package/dist/src/runtime/await-tracker.d.ts.map +1 -1
  91. package/dist/src/runtime/await-tracker.js +32 -29
  92. package/dist/src/runtime/await-tracker.js.map +1 -1
  93. package/dist/src/runtime/await-tracker.specs.js +56 -38
  94. package/dist/src/runtime/await-tracker.specs.js.map +1 -1
  95. package/dist/src/runtime/context.d.ts +1 -1
  96. package/dist/src/runtime/context.d.ts.map +1 -1
  97. package/dist/src/runtime/event-command-map.d.ts +3 -3
  98. package/dist/src/runtime/event-command-map.d.ts.map +1 -1
  99. package/dist/src/runtime/event-command-map.js +6 -2
  100. package/dist/src/runtime/event-command-map.js.map +1 -1
  101. package/dist/src/runtime/phased-executor.d.ts +15 -9
  102. package/dist/src/runtime/phased-executor.d.ts.map +1 -1
  103. package/dist/src/runtime/phased-executor.js +126 -104
  104. package/dist/src/runtime/phased-executor.js.map +1 -1
  105. package/dist/src/runtime/phased-executor.specs.js +243 -81
  106. package/dist/src/runtime/phased-executor.specs.js.map +1 -1
  107. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
  108. package/dist/src/runtime/pipeline-runtime.js +2 -2
  109. package/dist/src/runtime/pipeline-runtime.js.map +1 -1
  110. package/dist/src/runtime/pipeline-runtime.specs.js +35 -0
  111. package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -1
  112. package/dist/src/runtime/settled-tracker.d.ts +12 -9
  113. package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
  114. package/dist/src/runtime/settled-tracker.js +92 -77
  115. package/dist/src/runtime/settled-tracker.js.map +1 -1
  116. package/dist/src/runtime/settled-tracker.specs.js +568 -118
  117. package/dist/src/runtime/settled-tracker.specs.js.map +1 -1
  118. package/dist/src/server/pipeline-server.d.ts +31 -9
  119. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  120. package/dist/src/server/pipeline-server.e2e.specs.js +2 -10
  121. package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -1
  122. package/dist/src/server/pipeline-server.js +418 -134
  123. package/dist/src/server/pipeline-server.js.map +1 -1
  124. package/dist/src/server/pipeline-server.specs.js +777 -32
  125. package/dist/src/server/pipeline-server.specs.js.map +1 -1
  126. package/dist/src/server/sse-manager.specs.js +55 -35
  127. package/dist/src/server/sse-manager.specs.js.map +1 -1
  128. package/dist/src/store/index.d.ts +3 -0
  129. package/dist/src/store/index.d.ts.map +1 -0
  130. package/dist/src/store/index.js +3 -0
  131. package/dist/src/store/index.js.map +1 -0
  132. package/dist/src/store/pipeline-event-store.d.ts +10 -0
  133. package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
  134. package/dist/src/store/pipeline-event-store.js +112 -0
  135. package/dist/src/store/pipeline-event-store.js.map +1 -0
  136. package/dist/src/store/pipeline-event-store.specs.d.ts +2 -0
  137. package/dist/src/store/pipeline-event-store.specs.d.ts.map +1 -0
  138. package/dist/src/store/pipeline-event-store.specs.js +287 -0
  139. package/dist/src/store/pipeline-event-store.specs.js.map +1 -0
  140. package/dist/src/store/pipeline-read-model.d.ts +49 -0
  141. package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
  142. package/dist/src/store/pipeline-read-model.js +157 -0
  143. package/dist/src/store/pipeline-read-model.js.map +1 -0
  144. package/dist/src/store/pipeline-read-model.specs.d.ts +2 -0
  145. package/dist/src/store/pipeline-read-model.specs.d.ts.map +1 -0
  146. package/dist/src/store/pipeline-read-model.specs.js +830 -0
  147. package/dist/src/store/pipeline-read-model.specs.js.map +1 -0
  148. package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
  149. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
  150. package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
  151. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
  152. package/dist/tsconfig.tsbuildinfo +1 -1
  153. package/ketchup-plan.md +960 -0
  154. package/package.json +7 -3
  155. package/src/builder/define.specs.ts +3 -3
  156. package/src/builder/define.ts +24 -11
  157. package/src/core/descriptors.ts +7 -2
  158. package/src/graph/filter-graph.specs.ts +241 -0
  159. package/src/graph/filter-graph.ts +111 -0
  160. package/src/graph/types.ts +10 -0
  161. package/src/index.ts +1 -2
  162. package/src/projections/await-tracker-projection.ts +68 -0
  163. package/src/projections/index.ts +11 -0
  164. package/src/projections/item-status-projection.specs.ts +130 -0
  165. package/src/projections/item-status-projection.ts +32 -0
  166. package/src/projections/latest-run-projection.specs.ts +38 -0
  167. package/src/projections/latest-run-projection.ts +20 -0
  168. package/src/projections/message-log-projection.specs.ts +118 -0
  169. package/src/projections/message-log-projection.ts +113 -0
  170. package/src/projections/node-status-projection.specs.ts +127 -0
  171. package/src/projections/node-status-projection.ts +33 -0
  172. package/src/projections/phased-execution-projection.specs.ts +202 -0
  173. package/src/projections/phased-execution-projection.ts +146 -0
  174. package/src/projections/settled-instance-projection.specs.ts +249 -0
  175. package/src/projections/settled-instance-projection.ts +160 -0
  176. package/src/projections/stats-projection.specs.ts +105 -0
  177. package/src/projections/stats-projection.ts +26 -0
  178. package/src/runtime/await-tracker.specs.ts +57 -34
  179. package/src/runtime/await-tracker.ts +43 -31
  180. package/src/runtime/context.ts +1 -1
  181. package/src/runtime/event-command-map.ts +11 -4
  182. package/src/runtime/phased-executor.specs.ts +357 -81
  183. package/src/runtime/phased-executor.ts +142 -126
  184. package/src/runtime/pipeline-runtime.specs.ts +42 -0
  185. package/src/runtime/pipeline-runtime.ts +6 -4
  186. package/src/runtime/settled-tracker.specs.ts +716 -120
  187. package/src/runtime/settled-tracker.ts +104 -98
  188. package/src/server/pipeline-server.e2e.specs.ts +10 -16
  189. package/src/server/pipeline-server.specs.ts +964 -49
  190. package/src/server/pipeline-server.ts +522 -156
  191. package/src/server/sse-manager.specs.ts +67 -36
  192. package/src/store/index.ts +2 -0
  193. package/src/store/pipeline-event-store.specs.ts +309 -0
  194. package/src/store/pipeline-event-store.ts +156 -0
  195. package/src/store/pipeline-read-model.specs.ts +967 -0
  196. package/src/store/pipeline-read-model.ts +223 -0
  197. package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
  198. package/src/testing/fixtures/kanban.pipeline.ts +2 -2
  199. package/claude.md +0 -160
  200. package/docs/testing-analysis.md +0 -395
  201. package/pomodoro-plan.md +0 -651
@@ -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
- it('should track pending keys', () => {
5
- const tracker = new AwaitTracker();
6
- tracker.startAwaiting('corr-1', ['a', 'b']);
7
- expect(tracker.isPending('corr-1')).toBe(true);
8
- expect(tracker.getPendingKeys('corr-1')).toEqual(['a', 'b']);
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 = new AwaitTracker();
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 = new AwaitTracker();
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 = new AwaitTracker();
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 = new AwaitTracker();
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 = new AwaitTracker();
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 = new AwaitTracker();
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
- interface AwaitState {
2
- pendingKeys: Set<string>;
3
- results: Map<string, unknown>;
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 state = new Map<string, AwaitState>();
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.state.set(correlationId, {
11
- pendingKeys: new Set(keys),
12
- results: new Map(),
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
- return this.state.has(correlationId);
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 awaitState = this.state.get(correlationId);
22
- return awaitState ? Array.from(awaitState.pendingKeys) : [];
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
- const awaitState = this.state.get(correlationId);
27
- if (awaitState) {
28
- awaitState.pendingKeys.delete(key);
29
- awaitState.results.set(key, result);
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 awaitState = this.state.get(correlationId);
35
- return awaitState ? awaitState.pendingKeys.size === 0 : false;
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 awaitState = this.state.get(correlationId);
40
- if (!awaitState) {
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
- const results: Record<string, unknown> = {};
44
- for (const [key, value] of awaitState.results) {
45
- results[key] = value;
46
- }
47
- this.state.delete(correlationId);
48
- return results;
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
  }
@@ -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 string[];
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 eventType of events) {
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, [...events]);
31
+ this.commandToEvents.set(handler.name, events.map(getEventName));
25
32
  }
26
33
 
27
34
  getSourceCommand(eventType: string): string | undefined {