@auto-engineer/pipeline 0.14.0 → 0.16.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 (316) hide show
  1. package/.turbo/turbo-build.log +5 -6
  2. package/CHANGELOG.md +24 -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/core/descriptors.d.ts +6 -2
  9. package/dist/src/core/descriptors.d.ts.map +1 -1
  10. package/dist/src/graph/filter-graph.d.ts +3 -0
  11. package/dist/src/graph/filter-graph.d.ts.map +1 -0
  12. package/dist/src/graph/filter-graph.js +80 -0
  13. package/dist/src/graph/filter-graph.js.map +1 -0
  14. package/dist/src/graph/types.d.ts +8 -0
  15. package/dist/src/graph/types.d.ts.map +1 -1
  16. package/dist/src/index.d.ts +1 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/projections/await-tracker-projection.d.ts +31 -0
  20. package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
  21. package/dist/src/projections/await-tracker-projection.js +35 -0
  22. package/dist/src/projections/await-tracker-projection.js.map +1 -0
  23. package/dist/src/projections/index.d.ts +4 -0
  24. package/dist/src/projections/index.d.ts.map +1 -0
  25. package/dist/src/projections/index.js +4 -0
  26. package/dist/src/projections/index.js.map +1 -0
  27. package/dist/src/projections/item-status-projection.d.ts +22 -0
  28. package/dist/src/projections/item-status-projection.d.ts.map +1 -0
  29. package/dist/src/projections/item-status-projection.js +11 -0
  30. package/dist/src/projections/item-status-projection.js.map +1 -0
  31. package/dist/src/projections/latest-run-projection.d.ts +15 -0
  32. package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
  33. package/dist/src/projections/latest-run-projection.js +7 -0
  34. package/dist/src/projections/latest-run-projection.js.map +1 -0
  35. package/dist/src/projections/message-log-projection.d.ts +51 -0
  36. package/dist/src/projections/message-log-projection.d.ts.map +1 -0
  37. package/dist/src/projections/message-log-projection.js +51 -0
  38. package/dist/src/projections/message-log-projection.js.map +1 -0
  39. package/dist/src/projections/node-status-projection.d.ts +23 -0
  40. package/dist/src/projections/node-status-projection.d.ts.map +1 -0
  41. package/dist/src/projections/node-status-projection.js +10 -0
  42. package/dist/src/projections/node-status-projection.js.map +1 -0
  43. package/dist/src/projections/phased-execution-projection.d.ts +77 -0
  44. package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
  45. package/dist/src/projections/phased-execution-projection.js +54 -0
  46. package/dist/src/projections/phased-execution-projection.js.map +1 -0
  47. package/dist/src/projections/settled-instance-projection.d.ts +67 -0
  48. package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
  49. package/dist/src/projections/settled-instance-projection.js +66 -0
  50. package/dist/src/projections/settled-instance-projection.js.map +1 -0
  51. package/dist/src/projections/stats-projection.d.ts +9 -0
  52. package/dist/src/projections/stats-projection.d.ts.map +1 -0
  53. package/dist/src/projections/stats-projection.js +16 -0
  54. package/dist/src/projections/stats-projection.js.map +1 -0
  55. package/dist/src/runtime/await-tracker.d.ts +17 -7
  56. package/dist/src/runtime/await-tracker.d.ts.map +1 -1
  57. package/dist/src/runtime/await-tracker.js +32 -29
  58. package/dist/src/runtime/await-tracker.js.map +1 -1
  59. package/dist/src/runtime/context.d.ts +1 -1
  60. package/dist/src/runtime/context.d.ts.map +1 -1
  61. package/dist/src/runtime/event-command-map.d.ts +3 -3
  62. package/dist/src/runtime/event-command-map.d.ts.map +1 -1
  63. package/dist/src/runtime/event-command-map.js +6 -2
  64. package/dist/src/runtime/event-command-map.js.map +1 -1
  65. package/dist/src/runtime/phased-executor.d.ts +14 -9
  66. package/dist/src/runtime/phased-executor.d.ts.map +1 -1
  67. package/dist/src/runtime/phased-executor.js +113 -105
  68. package/dist/src/runtime/phased-executor.js.map +1 -1
  69. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
  70. package/dist/src/runtime/pipeline-runtime.js +2 -2
  71. package/dist/src/runtime/pipeline-runtime.js.map +1 -1
  72. package/dist/src/runtime/settled-tracker.d.ts +12 -10
  73. package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
  74. package/dist/src/runtime/settled-tracker.js +89 -80
  75. package/dist/src/runtime/settled-tracker.js.map +1 -1
  76. package/dist/src/server/pipeline-server.d.ts +31 -9
  77. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  78. package/dist/src/server/pipeline-server.js +424 -123
  79. package/dist/src/server/pipeline-server.js.map +1 -1
  80. package/dist/src/server/sse-manager.d.ts +0 -1
  81. package/dist/src/server/sse-manager.d.ts.map +1 -1
  82. package/dist/src/server/sse-manager.js +0 -3
  83. package/dist/src/server/sse-manager.js.map +1 -1
  84. package/dist/src/store/index.d.ts +3 -0
  85. package/dist/src/store/index.d.ts.map +1 -0
  86. package/dist/src/store/index.js +3 -0
  87. package/dist/src/store/index.js.map +1 -0
  88. package/dist/src/store/pipeline-event-store.d.ts +10 -0
  89. package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
  90. package/dist/src/store/pipeline-event-store.js +112 -0
  91. package/dist/src/store/pipeline-event-store.js.map +1 -0
  92. package/dist/src/store/pipeline-read-model.d.ts +49 -0
  93. package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
  94. package/dist/src/store/pipeline-read-model.js +156 -0
  95. package/dist/src/store/pipeline-read-model.js.map +1 -0
  96. package/dist/src/store/sqlite-pipeline-event-store.d.ts +14 -0
  97. package/dist/src/store/sqlite-pipeline-event-store.d.ts.map +1 -0
  98. package/dist/src/store/sqlite-pipeline-event-store.js +20 -0
  99. package/dist/src/store/sqlite-pipeline-event-store.js.map +1 -0
  100. package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
  101. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
  102. package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
  103. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
  104. package/dist/tsconfig.tsbuildinfo +1 -1
  105. package/ketchup-plan.md +1216 -0
  106. package/package.json +6 -4
  107. package/src/builder/define.specs.ts +5 -4
  108. package/src/builder/define.ts +24 -11
  109. package/src/config/pipeline-config.specs.ts +32 -0
  110. package/src/core/descriptors.ts +7 -2
  111. package/src/graph/filter-graph.specs.ts +267 -0
  112. package/src/graph/filter-graph.ts +111 -0
  113. package/src/graph/types.specs.ts +0 -14
  114. package/src/graph/types.ts +10 -0
  115. package/src/index.ts +1 -0
  116. package/src/projections/await-tracker-projection.specs.ts +24 -0
  117. package/src/projections/await-tracker-projection.ts +68 -0
  118. package/src/projections/index.ts +11 -0
  119. package/src/projections/item-status-projection.specs.ts +130 -0
  120. package/src/projections/item-status-projection.ts +32 -0
  121. package/src/projections/latest-run-projection.ts +20 -0
  122. package/src/projections/message-log-projection.ts +113 -0
  123. package/src/projections/node-status-projection.ts +33 -0
  124. package/src/projections/phased-execution-projection.specs.ts +202 -0
  125. package/src/projections/phased-execution-projection.ts +146 -0
  126. package/src/projections/settled-instance-projection.specs.ts +296 -0
  127. package/src/projections/settled-instance-projection.ts +160 -0
  128. package/src/projections/stats-projection.ts +26 -0
  129. package/src/runtime/await-tracker.specs.ts +57 -34
  130. package/src/runtime/await-tracker.ts +43 -31
  131. package/src/runtime/context.ts +1 -1
  132. package/src/runtime/event-command-map.ts +11 -4
  133. package/src/runtime/phased-executor.specs.ts +357 -81
  134. package/src/runtime/phased-executor.ts +134 -128
  135. package/src/runtime/pipeline-runtime.specs.ts +65 -0
  136. package/src/runtime/pipeline-runtime.ts +6 -4
  137. package/src/runtime/settled-tracker.specs.ts +716 -120
  138. package/src/runtime/settled-tracker.ts +100 -102
  139. package/src/server/pipeline-server.e2e.specs.ts +10 -16
  140. package/src/server/pipeline-server.specs.ts +1441 -211
  141. package/src/server/pipeline-server.ts +535 -144
  142. package/src/server/sse-manager.specs.ts +67 -36
  143. package/src/server/sse-manager.ts +0 -4
  144. package/src/store/index.ts +2 -0
  145. package/src/store/pipeline-event-store.specs.ts +357 -0
  146. package/src/store/pipeline-event-store.ts +156 -0
  147. package/src/store/pipeline-read-model.specs.ts +1170 -0
  148. package/src/store/pipeline-read-model.ts +223 -0
  149. package/src/store/sqlite-pipeline-event-store.specs.ts +13 -0
  150. package/src/store/sqlite-pipeline-event-store.ts +36 -0
  151. package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
  152. package/src/testing/fixtures/kanban.pipeline.ts +2 -2
  153. package/tsconfig.json +1 -1
  154. package/vitest.config.ts +1 -8
  155. package/claude.md +0 -160
  156. package/dist/src/__tests__/e2e/helpers.d.ts +0 -48
  157. package/dist/src/__tests__/e2e/helpers.d.ts.map +0 -1
  158. package/dist/src/__tests__/e2e/helpers.js +0 -253
  159. package/dist/src/__tests__/e2e/helpers.js.map +0 -1
  160. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.d.ts +0 -2
  161. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.d.ts.map +0 -1
  162. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.js +0 -195
  163. package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.js.map +0 -1
  164. package/dist/src/__tests__/e2e/types.d.ts +0 -107
  165. package/dist/src/__tests__/e2e/types.d.ts.map +0 -1
  166. package/dist/src/__tests__/e2e/types.js +0 -2
  167. package/dist/src/__tests__/e2e/types.js.map +0 -1
  168. package/dist/src/builder/define.specs.d.ts +0 -2
  169. package/dist/src/builder/define.specs.d.ts.map +0 -1
  170. package/dist/src/builder/define.specs.js +0 -435
  171. package/dist/src/builder/define.specs.js.map +0 -1
  172. package/dist/src/core/descriptors.specs.d.ts +0 -2
  173. package/dist/src/core/descriptors.specs.d.ts.map +0 -1
  174. package/dist/src/core/descriptors.specs.js +0 -24
  175. package/dist/src/core/descriptors.specs.js.map +0 -1
  176. package/dist/src/core/types.specs.d.ts +0 -2
  177. package/dist/src/core/types.specs.d.ts.map +0 -1
  178. package/dist/src/core/types.specs.js +0 -40
  179. package/dist/src/core/types.specs.js.map +0 -1
  180. package/dist/src/file-syncer/crypto/jwe-encryptor.d.ts +0 -15
  181. package/dist/src/file-syncer/crypto/jwe-encryptor.d.ts.map +0 -1
  182. package/dist/src/file-syncer/crypto/jwe-encryptor.js +0 -64
  183. package/dist/src/file-syncer/crypto/jwe-encryptor.js.map +0 -1
  184. package/dist/src/file-syncer/crypto/provider-resolver.d.ts +0 -24
  185. package/dist/src/file-syncer/crypto/provider-resolver.d.ts.map +0 -1
  186. package/dist/src/file-syncer/crypto/provider-resolver.js +0 -71
  187. package/dist/src/file-syncer/crypto/provider-resolver.js.map +0 -1
  188. package/dist/src/file-syncer/discovery/bareImports.d.ts +0 -3
  189. package/dist/src/file-syncer/discovery/bareImports.d.ts.map +0 -1
  190. package/dist/src/file-syncer/discovery/bareImports.js +0 -36
  191. package/dist/src/file-syncer/discovery/bareImports.js.map +0 -1
  192. package/dist/src/file-syncer/discovery/dts.d.ts +0 -8
  193. package/dist/src/file-syncer/discovery/dts.d.ts.map +0 -1
  194. package/dist/src/file-syncer/discovery/dts.js +0 -99
  195. package/dist/src/file-syncer/discovery/dts.js.map +0 -1
  196. package/dist/src/file-syncer/index.d.ts +0 -46
  197. package/dist/src/file-syncer/index.d.ts.map +0 -1
  198. package/dist/src/file-syncer/index.js +0 -392
  199. package/dist/src/file-syncer/index.js.map +0 -1
  200. package/dist/src/file-syncer/sync/resolveSyncFileSet.d.ts +0 -7
  201. package/dist/src/file-syncer/sync/resolveSyncFileSet.d.ts.map +0 -1
  202. package/dist/src/file-syncer/sync/resolveSyncFileSet.js +0 -86
  203. package/dist/src/file-syncer/sync/resolveSyncFileSet.js.map +0 -1
  204. package/dist/src/file-syncer/types/wire.d.ts +0 -14
  205. package/dist/src/file-syncer/types/wire.d.ts.map +0 -1
  206. package/dist/src/file-syncer/types/wire.js +0 -2
  207. package/dist/src/file-syncer/types/wire.js.map +0 -1
  208. package/dist/src/file-syncer/utils/hash.d.ts +0 -5
  209. package/dist/src/file-syncer/utils/hash.d.ts.map +0 -1
  210. package/dist/src/file-syncer/utils/hash.js +0 -19
  211. package/dist/src/file-syncer/utils/hash.js.map +0 -1
  212. package/dist/src/file-syncer/utils/path.d.ts +0 -13
  213. package/dist/src/file-syncer/utils/path.d.ts.map +0 -1
  214. package/dist/src/file-syncer/utils/path.js +0 -74
  215. package/dist/src/file-syncer/utils/path.js.map +0 -1
  216. package/dist/src/graph/types.specs.d.ts +0 -2
  217. package/dist/src/graph/types.specs.d.ts.map +0 -1
  218. package/dist/src/graph/types.specs.js +0 -148
  219. package/dist/src/graph/types.specs.js.map +0 -1
  220. package/dist/src/logging/event-logger.specs.d.ts +0 -2
  221. package/dist/src/logging/event-logger.specs.d.ts.map +0 -1
  222. package/dist/src/logging/event-logger.specs.js +0 -81
  223. package/dist/src/logging/event-logger.specs.js.map +0 -1
  224. package/dist/src/plugins/handler-adapter.specs.d.ts +0 -2
  225. package/dist/src/plugins/handler-adapter.specs.d.ts.map +0 -1
  226. package/dist/src/plugins/handler-adapter.specs.js +0 -129
  227. package/dist/src/plugins/handler-adapter.specs.js.map +0 -1
  228. package/dist/src/plugins/plugin-loader.specs.d.ts +0 -2
  229. package/dist/src/plugins/plugin-loader.specs.d.ts.map +0 -1
  230. package/dist/src/plugins/plugin-loader.specs.js +0 -246
  231. package/dist/src/plugins/plugin-loader.specs.js.map +0 -1
  232. package/dist/src/runtime/await-tracker.specs.d.ts +0 -2
  233. package/dist/src/runtime/await-tracker.specs.d.ts.map +0 -1
  234. package/dist/src/runtime/await-tracker.specs.js +0 -46
  235. package/dist/src/runtime/await-tracker.specs.js.map +0 -1
  236. package/dist/src/runtime/context.specs.d.ts +0 -2
  237. package/dist/src/runtime/context.specs.d.ts.map +0 -1
  238. package/dist/src/runtime/context.specs.js +0 -26
  239. package/dist/src/runtime/context.specs.js.map +0 -1
  240. package/dist/src/runtime/event-command-map.specs.d.ts +0 -2
  241. package/dist/src/runtime/event-command-map.specs.d.ts.map +0 -1
  242. package/dist/src/runtime/event-command-map.specs.js +0 -108
  243. package/dist/src/runtime/event-command-map.specs.js.map +0 -1
  244. package/dist/src/runtime/phased-executor.specs.d.ts +0 -2
  245. package/dist/src/runtime/phased-executor.specs.d.ts.map +0 -1
  246. package/dist/src/runtime/phased-executor.specs.js +0 -256
  247. package/dist/src/runtime/phased-executor.specs.js.map +0 -1
  248. package/dist/src/runtime/pipeline-runtime.specs.d.ts +0 -2
  249. package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +0 -1
  250. package/dist/src/runtime/pipeline-runtime.specs.js +0 -192
  251. package/dist/src/runtime/pipeline-runtime.specs.js.map +0 -1
  252. package/dist/src/runtime/settled-tracker.specs.d.ts +0 -2
  253. package/dist/src/runtime/settled-tracker.specs.d.ts.map +0 -1
  254. package/dist/src/runtime/settled-tracker.specs.js +0 -361
  255. package/dist/src/runtime/settled-tracker.specs.js.map +0 -1
  256. package/dist/src/server/full-orchestration.e2e.specs.d.ts +0 -2
  257. package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +0 -1
  258. package/dist/src/server/full-orchestration.e2e.specs.js +0 -561
  259. package/dist/src/server/full-orchestration.e2e.specs.js.map +0 -1
  260. package/dist/src/server/pipeline-server.e2e.specs.d.ts +0 -2
  261. package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +0 -1
  262. package/dist/src/server/pipeline-server.e2e.specs.js +0 -381
  263. package/dist/src/server/pipeline-server.e2e.specs.js.map +0 -1
  264. package/dist/src/server/pipeline-server.specs.d.ts +0 -2
  265. package/dist/src/server/pipeline-server.specs.d.ts.map +0 -1
  266. package/dist/src/server/pipeline-server.specs.js +0 -662
  267. package/dist/src/server/pipeline-server.specs.js.map +0 -1
  268. package/dist/src/server/sse-manager.specs.d.ts +0 -2
  269. package/dist/src/server/sse-manager.specs.d.ts.map +0 -1
  270. package/dist/src/server/sse-manager.specs.js +0 -158
  271. package/dist/src/server/sse-manager.specs.js.map +0 -1
  272. package/dist/src/testing/event-capture.specs.d.ts +0 -2
  273. package/dist/src/testing/event-capture.specs.d.ts.map +0 -1
  274. package/dist/src/testing/event-capture.specs.js +0 -114
  275. package/dist/src/testing/event-capture.specs.js.map +0 -1
  276. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +0 -2
  277. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +0 -1
  278. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +0 -263
  279. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +0 -1
  280. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +0 -2
  281. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +0 -1
  282. package/dist/src/testing/fixtures/kanban.pipeline.specs.js +0 -29
  283. package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +0 -1
  284. package/dist/src/testing/kanban-todo.e2e.specs.d.ts +0 -2
  285. package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +0 -1
  286. package/dist/src/testing/kanban-todo.e2e.specs.js +0 -160
  287. package/dist/src/testing/kanban-todo.e2e.specs.js.map +0 -1
  288. package/dist/src/testing/mock-handlers.specs.d.ts +0 -2
  289. package/dist/src/testing/mock-handlers.specs.d.ts.map +0 -1
  290. package/dist/src/testing/mock-handlers.specs.js +0 -193
  291. package/dist/src/testing/mock-handlers.specs.js.map +0 -1
  292. package/dist/src/testing/real-execution.e2e.specs.d.ts +0 -2
  293. package/dist/src/testing/real-execution.e2e.specs.d.ts.map +0 -1
  294. package/dist/src/testing/real-execution.e2e.specs.js +0 -140
  295. package/dist/src/testing/real-execution.e2e.specs.js.map +0 -1
  296. package/dist/src/testing/real-plugin.e2e.specs.d.ts +0 -2
  297. package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +0 -1
  298. package/dist/src/testing/real-plugin.e2e.specs.js +0 -65
  299. package/dist/src/testing/real-plugin.e2e.specs.js.map +0 -1
  300. package/dist/src/testing/server-startup.e2e.specs.d.ts +0 -2
  301. package/dist/src/testing/server-startup.e2e.specs.d.ts.map +0 -1
  302. package/dist/src/testing/server-startup.e2e.specs.js +0 -104
  303. package/dist/src/testing/server-startup.e2e.specs.js.map +0 -1
  304. package/dist/src/testing/snapshot-compare.specs.d.ts +0 -2
  305. package/dist/src/testing/snapshot-compare.specs.d.ts.map +0 -1
  306. package/dist/src/testing/snapshot-compare.specs.js +0 -112
  307. package/dist/src/testing/snapshot-compare.specs.js.map +0 -1
  308. package/dist/src/testing/snapshot-sanitize.specs.d.ts +0 -2
  309. package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +0 -1
  310. package/dist/src/testing/snapshot-sanitize.specs.js +0 -104
  311. package/dist/src/testing/snapshot-sanitize.specs.js.map +0 -1
  312. package/docs/testing-analysis.md +0 -395
  313. package/pomodoro-plan.md +0 -651
  314. package/src/core/descriptors.specs.ts +0 -28
  315. package/src/core/types.specs.ts +0 -44
  316. package/src/runtime/context.specs.ts +0 -28
@@ -1,119 +1,111 @@
1
1
  import type { Event } from '@auto-engineer/message-bus';
2
2
  import type { ForEachPhasedDescriptor } from '../core/descriptors';
3
-
4
- interface ItemTracker {
5
- key: string;
6
- phase: string;
7
- dispatched: boolean;
8
- completed: boolean;
9
- }
10
-
11
- interface PhasedSession {
12
- correlationId: string;
13
- handler: ForEachPhasedDescriptor;
14
- triggerEvent: Event;
15
- items: Map<string, ItemTracker>;
16
- phases: readonly string[];
17
- currentPhaseIndex: number;
18
- pendingInPhase: Set<string>;
19
- failedItems: Map<string, unknown>;
20
- }
3
+ import type { PhasedExecutionDocument, PhasedExecutionEvent } from '../projections/phased-execution-projection';
4
+ import type { PipelineReadModel } from '../store/pipeline-read-model';
21
5
 
22
6
  interface PhasedExecutorOptions {
7
+ readModel: PipelineReadModel;
23
8
  onDispatch: (commandType: string, data: unknown, correlationId: string) => void;
24
9
  onComplete: (event: Event, correlationId: string) => void;
10
+ onEventEmit?: (event: PhasedExecutionEvent) => void | Promise<void>;
25
11
  }
26
12
 
27
13
  export class PhasedExecutor {
28
- private sessions = new Map<string, PhasedSession>();
29
- private keyToSession = new Map<string, string>();
14
+ private handlerRegistry = new Map<string, ForEachPhasedDescriptor>();
15
+ private readonly readModel: PipelineReadModel;
30
16
  private readonly onDispatch: (commandType: string, data: unknown, correlationId: string) => void;
31
17
  private readonly onComplete: (event: Event, correlationId: string) => void;
18
+ private readonly onEventEmit?: (event: PhasedExecutionEvent) => void | Promise<void>;
32
19
 
33
20
  constructor(options: PhasedExecutorOptions) {
21
+ this.readModel = options.readModel;
34
22
  this.onDispatch = options.onDispatch;
35
23
  this.onComplete = options.onComplete;
24
+ this.onEventEmit = options.onEventEmit;
25
+ }
26
+
27
+ registerHandler(handler: ForEachPhasedDescriptor): void {
28
+ const handlerId = this.generateHandlerId(handler);
29
+ this.handlerRegistry.set(handlerId, handler);
36
30
  }
37
31
 
38
- startPhased(handler: ForEachPhasedDescriptor, event: Event, correlationId: string): void {
32
+ async startPhased(handler: ForEachPhasedDescriptor, event: Event, correlationId: string): Promise<void> {
39
33
  const items = handler.itemsSelector(event);
40
- const itemTrackers = new Map<string, ItemTracker>();
34
+ const itemData: Array<{ key: string; phase: string; dispatched: boolean; completed: boolean }> = [];
41
35
 
42
36
  for (const item of items) {
43
37
  const key = handler.completion.itemKey({ type: event.type, data: item as Record<string, unknown> });
44
38
  const phase = handler.classifier(item);
45
- itemTrackers.set(key, {
46
- key,
47
- phase,
48
- dispatched: false,
49
- completed: false,
50
- });
39
+ itemData.push({ key, phase, dispatched: false, completed: false });
51
40
  }
52
41
 
53
- const session: PhasedSession = {
54
- correlationId,
55
- handler,
56
- triggerEvent: event,
57
- items: itemTrackers,
58
- phases: handler.phases,
59
- currentPhaseIndex: 0,
60
- pendingInPhase: new Set(),
61
- failedItems: new Map(),
62
- };
63
-
64
- const sessionId = this.generateSessionId(correlationId, handler);
65
- this.sessions.set(sessionId, session);
66
-
67
- for (const key of itemTrackers.keys()) {
68
- this.keyToSession.set(this.keyWithCorrelation(key, correlationId), sessionId);
69
- }
42
+ const executionId = this.generateSessionId(correlationId, handler);
43
+ const handlerId = this.generateHandlerId(handler);
44
+
45
+ await this.emitEvent({
46
+ type: 'PhasedExecutionStarted',
47
+ data: {
48
+ executionId,
49
+ correlationId,
50
+ handlerId,
51
+ triggerEvent: event,
52
+ items: itemData,
53
+ phases: handler.phases,
54
+ },
55
+ });
70
56
 
71
- this.dispatchCurrentPhase(sessionId);
57
+ await this.dispatchCurrentPhase(executionId, handler);
72
58
  }
73
59
 
74
- onEventReceived(event: Event, itemKey: string): void {
60
+ async onEventReceived(event: Event, itemKey: string): Promise<void> {
75
61
  const correlationId = event.correlationId;
76
62
  if (correlationId === undefined || correlationId === '') return;
77
63
 
78
- const lookupKey = this.keyWithCorrelation(itemKey, correlationId);
79
- const sessionId = this.keyToSession.get(lookupKey);
80
- if (sessionId === undefined) return;
64
+ const executions = await this.readModel.getActivePhasedExecutions(correlationId);
65
+ const execution = executions.find((ex) => ex.items.some((item) => item.key === itemKey));
66
+
67
+ if (execution === undefined) return;
81
68
 
82
- const session = this.sessions.get(sessionId)!;
83
- const tracker = session.items.get(itemKey);
84
- if (tracker === undefined || tracker.completed) return;
69
+ const itemDoc = execution.items.find((i) => i.key === itemKey);
70
+ if (itemDoc === undefined || itemDoc.completed) return;
85
71
 
86
- tracker.completed = true;
87
- session.pendingInPhase.delete(itemKey);
72
+ const handler = this.handlerRegistry.get(execution.handlerId)!;
88
73
 
89
- if (session.handler.stopOnFailure && this.isFailureEvent(event, session.handler)) {
90
- session.failedItems.set(itemKey, event);
91
- this.handleFailure(sessionId, session);
74
+ if (handler.stopOnFailure && this.isFailureEvent(event, handler)) {
75
+ await this.emitEvent({
76
+ type: 'PhasedItemFailed',
77
+ data: { executionId: execution.executionId, itemKey, error: event },
78
+ });
79
+ await this.handleFailure(execution.executionId, handler);
92
80
  return;
93
81
  }
94
82
 
95
- if (session.pendingInPhase.size === 0) {
96
- this.advanceToNextPhase(sessionId, session);
97
- }
98
- }
83
+ await this.emitEvent({
84
+ type: 'PhasedItemCompleted',
85
+ data: { executionId: execution.executionId, itemKey, resultEvent: event },
86
+ });
99
87
 
100
- getActiveSessionCount(): number {
101
- return this.sessions.size;
102
- }
88
+ const updatedExecution = (await this.readModel.getPhasedExecution(execution.executionId))!;
103
89
 
104
- isPhaseComplete(correlationId: string, phase: string): boolean {
105
- for (const session of this.sessions.values()) {
106
- if (session.correlationId !== correlationId) continue;
90
+ const pendingCount = this.countPendingInPhase(updatedExecution);
91
+ if (pendingCount === 0) {
92
+ await this.advanceToNextPhase(execution.executionId, handler);
93
+ }
94
+ }
107
95
 
108
- const phaseIndex = session.phases.indexOf(phase);
96
+ async isPhaseComplete(correlationId: string, phase: string): Promise<boolean> {
97
+ const executions = await this.readModel.getActivePhasedExecutions(correlationId);
98
+ for (const execution of executions) {
99
+ const phaseIndex = execution.phases.indexOf(phase);
109
100
  if (phaseIndex === -1) continue;
110
101
 
111
- if (session.currentPhaseIndex > phaseIndex) {
102
+ if (execution.currentPhaseIndex > phaseIndex) {
112
103
  return true;
113
104
  }
114
105
 
115
- if (session.currentPhaseIndex === phaseIndex) {
116
- return session.pendingInPhase.size === 0;
106
+ if (execution.currentPhaseIndex === phaseIndex) {
107
+ const pendingCount = this.countPendingInPhase(execution);
108
+ return pendingCount === 0;
117
109
  }
118
110
 
119
111
  return false;
@@ -121,89 +113,99 @@ export class PhasedExecutor {
121
113
  return false;
122
114
  }
123
115
 
124
- private dispatchCurrentPhase(sessionId: string): void {
125
- const session = this.sessions.get(sessionId)!;
126
-
127
- if (session.currentPhaseIndex >= session.phases.length) {
128
- this.completeSession(sessionId, session, true);
129
- return;
116
+ private countPendingInPhase(execution: PhasedExecutionDocument): number {
117
+ let pending = 0;
118
+ const currentPhase = execution.phases[execution.currentPhaseIndex];
119
+ for (const item of execution.items) {
120
+ if (item.phase === currentPhase && item.dispatched && !item.completed) {
121
+ pending++;
122
+ }
130
123
  }
124
+ return pending;
125
+ }
131
126
 
132
- const currentPhase = session.phases[session.currentPhaseIndex];
133
- const itemsToDispatch: ItemTracker[] = [];
127
+ private async dispatchCurrentPhase(executionId: string, handler: ForEachPhasedDescriptor): Promise<void> {
128
+ const execution = (await this.readModel.getPhasedExecution(executionId))!;
134
129
 
135
- for (const tracker of session.items.values()) {
136
- if (tracker.phase === currentPhase && !tracker.dispatched) {
137
- itemsToDispatch.push(tracker);
138
- }
130
+ if (execution.currentPhaseIndex >= execution.phases.length) {
131
+ await this.completeSession(executionId, handler, true);
132
+ return;
139
133
  }
140
134
 
135
+ const currentPhase = execution.phases[execution.currentPhaseIndex];
136
+ const itemsToDispatch = execution.items.filter((i) => i.phase === currentPhase && !i.dispatched);
137
+
141
138
  if (itemsToDispatch.length === 0) {
142
- this.advanceToNextPhase(sessionId, session);
139
+ await this.advanceToNextPhase(executionId, handler);
143
140
  return;
144
141
  }
145
142
 
146
- for (const tracker of itemsToDispatch) {
147
- tracker.dispatched = true;
148
- session.pendingInPhase.add(tracker.key);
143
+ for (const item of itemsToDispatch) {
144
+ await this.emitEvent({
145
+ type: 'PhasedItemDispatched',
146
+ data: { executionId, itemKey: item.key, phase: item.phase },
147
+ });
149
148
 
150
- const item = this.findItemByKey(session, tracker.key);
151
- if (item !== undefined) {
152
- const command = session.handler.emitFactory(item, tracker.phase, session.triggerEvent);
153
- this.onDispatch(command.commandType, command.data, session.correlationId);
149
+ const originalItem = this.findItemByKey(execution.triggerEvent, handler, item.key);
150
+ if (originalItem !== undefined) {
151
+ const command = handler.emitFactory(originalItem, item.phase, execution.triggerEvent);
152
+ this.onDispatch(command.commandType, command.data, execution.correlationId);
154
153
  }
155
154
  }
156
155
  }
157
156
 
158
- private advanceToNextPhase(sessionId: string, session: PhasedSession): void {
159
- session.currentPhaseIndex++;
160
- this.dispatchCurrentPhase(sessionId);
157
+ private async advanceToNextPhase(executionId: string, handler: ForEachPhasedDescriptor): Promise<void> {
158
+ const execution = (await this.readModel.getPhasedExecution(executionId))!;
159
+
160
+ const fromPhase = execution.currentPhaseIndex;
161
+ const toPhase = fromPhase + 1;
162
+
163
+ await this.emitEvent({
164
+ type: 'PhasedPhaseAdvanced',
165
+ data: { executionId, fromPhase, toPhase },
166
+ });
167
+
168
+ await this.dispatchCurrentPhase(executionId, handler);
161
169
  }
162
170
 
163
- private handleFailure(sessionId: string, session: PhasedSession): void {
164
- this.completeSession(sessionId, session, false);
171
+ private async handleFailure(executionId: string, handler: ForEachPhasedDescriptor): Promise<void> {
172
+ await this.completeSession(executionId, handler, false);
165
173
  }
166
174
 
167
- private completeSession(sessionId: string, session: PhasedSession, success: boolean): void {
168
- const eventType = success ? session.handler.completion.successEvent : session.handler.completion.failureEvent;
175
+ private async completeSession(
176
+ executionId: string,
177
+ handler: ForEachPhasedDescriptor,
178
+ success: boolean,
179
+ ): Promise<void> {
180
+ const execution = (await this.readModel.getPhasedExecution(executionId))!;
169
181
 
170
- const results = this.collectResults(session);
182
+ const eventDescriptor = success ? handler.completion.successEvent : handler.completion.failureEvent;
183
+ const eventType = eventDescriptor.name;
184
+
185
+ const results = execution.items.filter((i) => i.completed).map((i) => i.key);
171
186
  const eventData = success
172
- ? { results, itemCount: session.items.size }
173
- : { failures: Array.from(session.failedItems.entries()).map(([k, v]) => ({ key: k, error: v })) };
187
+ ? { results, itemCount: execution.items.length }
188
+ : { failures: execution.failedItems.map((f) => ({ key: f.key, error: f.error })) };
174
189
 
175
190
  const completionEvent: Event = {
176
191
  type: eventType,
177
- correlationId: session.correlationId,
192
+ correlationId: execution.correlationId,
178
193
  data: eventData,
179
194
  };
180
195
 
181
- this.onComplete(completionEvent, session.correlationId);
182
- this.cleanupSession(sessionId, session);
183
- }
184
-
185
- private collectResults(session: PhasedSession): string[] {
186
- const completed: string[] = [];
187
- for (const tracker of session.items.values()) {
188
- if (tracker.completed) {
189
- completed.push(tracker.key);
190
- }
191
- }
192
- return completed;
193
- }
196
+ await this.emitEvent({
197
+ type: 'PhasedExecutionCompleted',
198
+ data: { executionId, success, results },
199
+ });
194
200
 
195
- private cleanupSession(sessionId: string, session: PhasedSession): void {
196
- for (const key of session.items.keys()) {
197
- this.keyToSession.delete(this.keyWithCorrelation(key, session.correlationId));
198
- }
199
- this.sessions.delete(sessionId);
201
+ this.onComplete(completionEvent, execution.correlationId);
200
202
  }
201
203
 
202
- private findItemByKey(session: PhasedSession, key: string): unknown {
203
- const items = session.handler.itemsSelector(session.triggerEvent);
204
+ private findItemByKey(triggerEvent: Event, handler: ForEachPhasedDescriptor, key: string): unknown {
205
+ const items = handler.itemsSelector(triggerEvent);
204
206
  return items.find((item) => {
205
- const itemKey = session.handler.completion.itemKey({
206
- type: session.triggerEvent.type,
207
+ const itemKey = handler.completion.itemKey({
208
+ type: triggerEvent.type,
207
209
  data: item as Record<string, unknown>,
208
210
  });
209
211
  return itemKey === key;
@@ -211,14 +213,18 @@ export class PhasedExecutor {
211
213
  }
212
214
 
213
215
  private isFailureEvent(event: Event, handler: ForEachPhasedDescriptor): boolean {
214
- return event.type === handler.completion.failureEvent;
216
+ return event.type === handler.completion.failureEvent.name;
215
217
  }
216
218
 
217
219
  private generateSessionId(correlationId: string, handler: ForEachPhasedDescriptor): string {
218
220
  return `phased-${correlationId}-${handler.eventType}`;
219
221
  }
220
222
 
221
- private keyWithCorrelation(key: string, correlationId: string): string {
222
- return `${correlationId}::${key}`;
223
+ private generateHandlerId(handler: ForEachPhasedDescriptor): string {
224
+ return `phased-handler-${handler.eventType}`;
225
+ }
226
+
227
+ private async emitEvent(event: PhasedExecutionEvent): Promise<void> {
228
+ await this.onEventEmit?.(event);
223
229
  }
224
230
  }
@@ -211,4 +211,69 @@ describe('PipelineRuntime', () => {
211
211
  );
212
212
  expect(sent).toEqual(['2', '3', '1']);
213
213
  });
214
+
215
+ it('should use startPhased when provided for foreach-phased', async () => {
216
+ let phasedCalled = false;
217
+ const ctx = {
218
+ sendCommand: async () => {},
219
+ emit: async () => {},
220
+ correlationId: 'test',
221
+ startPhased: () => {
222
+ phasedCalled = true;
223
+ },
224
+ };
225
+ type Item = { id: string; p: 'high' | 'low' };
226
+ const pipeline = define('test')
227
+ .on('Items')
228
+ .forEach((e: { data: { items: Item[] } }) => e.data.items)
229
+ .groupInto(['high', 'low'], (i: Item) => i.p)
230
+ .process('Cmd', (i: Item) => ({ id: i.id }))
231
+ .onComplete({ success: 'Done', failure: 'Fail', itemKey: () => '' })
232
+ .build();
233
+ const runtime = new PipelineRuntime(pipeline.descriptor);
234
+ await runtime.handleEvent({ type: 'Items', data: { items: [{ id: '1', p: 'low' }] } }, ctx);
235
+ expect(phasedCalled).toBe(true);
236
+ });
237
+
238
+ it('should dispatch multiple emit commands in parallel, not sequentially', async () => {
239
+ const callOrder: string[] = [];
240
+ let resolveA: () => void;
241
+ let resolveB: () => void;
242
+ const promiseA = new Promise<void>((r) => {
243
+ resolveA = r;
244
+ });
245
+ const promiseB = new Promise<void>((r) => {
246
+ resolveB = r;
247
+ });
248
+
249
+ const ctx = {
250
+ sendCommand: async (type: string) => {
251
+ callOrder.push(`${type}:start`);
252
+ if (type === 'CmdA') {
253
+ await promiseA;
254
+ } else if (type === 'CmdB') {
255
+ await promiseB;
256
+ }
257
+ callOrder.push(`${type}:end`);
258
+ },
259
+ emit: async () => {},
260
+ correlationId: 'test',
261
+ };
262
+
263
+ const pipeline = define('test').on('Start').emit('CmdA', {}).emit('CmdB', {}).build();
264
+ const runtime = new PipelineRuntime(pipeline.descriptor);
265
+
266
+ const handlePromise = runtime.handleEvent({ type: 'Start', data: {} }, ctx);
267
+
268
+ await new Promise((r) => setTimeout(r, 10));
269
+
270
+ expect(callOrder).toContain('CmdA:start');
271
+ expect(callOrder).toContain('CmdB:start');
272
+
273
+ resolveB!();
274
+ resolveA!();
275
+ await handlePromise;
276
+
277
+ expect(callOrder).toEqual(['CmdA:start', 'CmdB:start', 'CmdB:end', 'CmdA:end']);
278
+ });
214
279
  });
@@ -53,10 +53,12 @@ export class PipelineRuntime {
53
53
  }
54
54
 
55
55
  private async executeEmitHandler(handler: EmitHandlerDescriptor, event: Event, ctx: PipelineContext): Promise<void> {
56
- for (const command of handler.commands) {
57
- const data = typeof command.data === 'function' ? command.data(event) : command.data;
58
- await ctx.sendCommand(command.commandType, data);
59
- }
56
+ await Promise.all(
57
+ handler.commands.map(async (command) => {
58
+ const data = typeof command.data === 'function' ? command.data(event) : command.data;
59
+ await ctx.sendCommand(command.commandType, data);
60
+ }),
61
+ );
60
62
  }
61
63
 
62
64
  private async executeCustomHandler(