reqon-dsl 0.2.0 → 0.4.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 (450) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +45 -3
  3. package/dist/ast/nodes.d.ts +91 -4
  4. package/dist/ast/nodes.js +14 -0
  5. package/dist/auth/circuit-breaker.d.ts +11 -0
  6. package/dist/auth/circuit-breaker.js +90 -18
  7. package/dist/auth/credentials.d.ts +6 -1
  8. package/dist/auth/credentials.js +12 -4
  9. package/dist/auth/oauth2-provider.js +13 -3
  10. package/dist/auth/rate-limiter.d.ts +12 -1
  11. package/dist/auth/rate-limiter.js +39 -26
  12. package/dist/auth/token-store.js +8 -1
  13. package/dist/cli.d.ts +24 -1
  14. package/dist/cli.js +149 -10
  15. package/dist/config/constants.d.ts +152 -0
  16. package/dist/config/constants.js +139 -0
  17. package/dist/config/index.d.ts +4 -0
  18. package/dist/config/index.js +4 -0
  19. package/dist/control/index.d.ts +2 -0
  20. package/dist/control/index.js +1 -0
  21. package/dist/control/server.d.ts +105 -0
  22. package/dist/control/server.js +315 -0
  23. package/dist/control/types.d.ts +61 -0
  24. package/dist/control/types.js +7 -0
  25. package/dist/debug/cli-debugger.d.ts +17 -0
  26. package/dist/debug/cli-debugger.js +185 -0
  27. package/dist/debug/controller.d.ts +94 -0
  28. package/dist/debug/controller.js +45 -0
  29. package/dist/debug/index.d.ts +6 -0
  30. package/dist/debug/index.js +5 -0
  31. package/dist/errors/index.d.ts +67 -0
  32. package/dist/errors/index.js +89 -1
  33. package/dist/execution/index.d.ts +1 -1
  34. package/dist/execution/state.d.ts +24 -0
  35. package/dist/execution/store.js +2 -2
  36. package/dist/execution-log/events.d.ts +125 -0
  37. package/dist/execution-log/events.js +17 -0
  38. package/dist/execution-log/fold.d.ts +38 -0
  39. package/dist/execution-log/fold.js +54 -0
  40. package/dist/execution-log/index.d.ts +18 -0
  41. package/dist/execution-log/index.js +6 -0
  42. package/dist/execution-log/postgres-store.d.ts +36 -0
  43. package/dist/execution-log/postgres-store.js +108 -0
  44. package/dist/execution-log/resume.d.ts +11 -0
  45. package/dist/execution-log/resume.js +5 -0
  46. package/dist/execution-log/sqlite-store.d.ts +16 -0
  47. package/dist/execution-log/sqlite-store.js +101 -0
  48. package/dist/execution-log/store.d.ts +72 -0
  49. package/dist/execution-log/store.js +182 -0
  50. package/dist/index.d.ts +23 -2
  51. package/dist/index.js +35 -3
  52. package/dist/interpreter/context.d.ts +29 -0
  53. package/dist/interpreter/context.js +18 -0
  54. package/dist/interpreter/evaluator.d.ts +63 -1
  55. package/dist/interpreter/evaluator.js +219 -42
  56. package/dist/interpreter/executor.d.ts +132 -14
  57. package/dist/interpreter/executor.js +883 -178
  58. package/dist/interpreter/fetch-handler.d.ts +48 -1
  59. package/dist/interpreter/fetch-handler.js +216 -38
  60. package/dist/interpreter/http.d.ts +34 -0
  61. package/dist/interpreter/http.js +203 -28
  62. package/dist/interpreter/index.d.ts +5 -3
  63. package/dist/interpreter/index.js +4 -2
  64. package/dist/interpreter/pagination.d.ts +12 -3
  65. package/dist/interpreter/pagination.js +102 -32
  66. package/dist/interpreter/signals.d.ts +8 -0
  67. package/dist/interpreter/signals.js +12 -0
  68. package/dist/interpreter/source-manager.d.ts +75 -0
  69. package/dist/interpreter/source-manager.js +157 -0
  70. package/dist/interpreter/step-handlers/apply-handler.d.ts +29 -0
  71. package/dist/interpreter/step-handlers/apply-handler.js +79 -0
  72. package/dist/interpreter/step-handlers/for-handler.d.ts +16 -0
  73. package/dist/interpreter/step-handlers/for-handler.js +89 -7
  74. package/dist/interpreter/step-handlers/index.d.ts +4 -2
  75. package/dist/interpreter/step-handlers/index.js +4 -2
  76. package/dist/interpreter/step-handlers/match-handler.d.ts +9 -0
  77. package/dist/interpreter/step-handlers/match-handler.js +47 -17
  78. package/dist/interpreter/step-handlers/pause-handler.d.ts +52 -0
  79. package/dist/interpreter/step-handlers/pause-handler.js +87 -0
  80. package/dist/interpreter/step-handlers/store-handler.d.ts +17 -1
  81. package/dist/interpreter/step-handlers/store-handler.js +61 -20
  82. package/dist/interpreter/step-handlers/types.d.ts +3 -0
  83. package/dist/interpreter/step-handlers/validate-handler.d.ts +2 -1
  84. package/dist/interpreter/step-handlers/validate-handler.js +7 -2
  85. package/dist/interpreter/step-handlers/webhook-handler.d.ts +4 -0
  86. package/dist/interpreter/step-handlers/webhook-handler.js +31 -5
  87. package/dist/interpreter/store-manager.d.ts +46 -0
  88. package/dist/interpreter/store-manager.js +70 -0
  89. package/dist/lexer/index.d.ts +11 -4
  90. package/dist/lexer/index.js +11 -4
  91. package/dist/lexer/tokens.d.ts +17 -1
  92. package/dist/lexer/tokens.js +36 -0
  93. package/dist/loader/index.js +5 -8
  94. package/dist/mcp/index.d.ts +11 -0
  95. package/dist/mcp/index.js +11 -0
  96. package/dist/mcp/sandbox.d.ts +41 -0
  97. package/dist/mcp/sandbox.js +76 -0
  98. package/dist/mcp/server.d.ts +17 -0
  99. package/dist/mcp/server.js +504 -0
  100. package/dist/oas/index.d.ts +2 -0
  101. package/dist/oas/index.js +1 -0
  102. package/dist/oas/loader.d.ts +13 -1
  103. package/dist/oas/loader.js +25 -3
  104. package/dist/oas/mock-generator.d.ts +12 -0
  105. package/dist/oas/mock-generator.js +196 -0
  106. package/dist/oas/validator.js +45 -5
  107. package/dist/observability/events.d.ts +248 -0
  108. package/dist/observability/events.js +85 -0
  109. package/dist/observability/index.d.ts +15 -0
  110. package/dist/observability/index.js +12 -0
  111. package/dist/observability/logger.d.ts +106 -0
  112. package/dist/observability/logger.js +266 -0
  113. package/dist/observability/otel.d.ts +143 -0
  114. package/dist/observability/otel.js +421 -0
  115. package/dist/parser/action-parser.d.ts +105 -0
  116. package/dist/parser/action-parser.js +645 -0
  117. package/dist/parser/base.d.ts +7 -0
  118. package/dist/parser/base.js +11 -0
  119. package/dist/parser/expressions.d.ts +14 -0
  120. package/dist/parser/expressions.js +89 -6
  121. package/dist/parser/fetch-parser.d.ts +27 -0
  122. package/dist/parser/fetch-parser.js +280 -0
  123. package/dist/parser/index.d.ts +17 -0
  124. package/dist/parser/index.js +17 -0
  125. package/dist/parser/parser.d.ts +44 -46
  126. package/dist/parser/parser.js +122 -1070
  127. package/dist/parser/pipeline-parser.d.ts +12 -0
  128. package/dist/parser/pipeline-parser.js +52 -0
  129. package/dist/parser/schedule-parser.d.ts +7 -0
  130. package/dist/parser/schedule-parser.js +137 -0
  131. package/dist/parser/source-parser.d.ts +9 -0
  132. package/dist/parser/source-parser.js +151 -0
  133. package/dist/pause/index.d.ts +15 -0
  134. package/dist/pause/index.js +12 -0
  135. package/dist/pause/log-store.d.ts +33 -0
  136. package/dist/pause/log-store.js +98 -0
  137. package/dist/pause/manager.d.ts +130 -0
  138. package/dist/pause/manager.js +294 -0
  139. package/dist/pause/state.d.ts +93 -0
  140. package/dist/pause/state.js +103 -0
  141. package/dist/pause/store.d.ts +61 -0
  142. package/dist/pause/store.js +158 -0
  143. package/dist/plugin.d.ts +9 -12
  144. package/dist/plugin.js +10 -13
  145. package/dist/scheduler/cron-parser.d.ts +10 -3
  146. package/dist/scheduler/cron-parser.js +227 -48
  147. package/dist/scheduler/scheduler.js +56 -22
  148. package/dist/stores/factory.d.ts +7 -1
  149. package/dist/stores/factory.js +14 -3
  150. package/dist/stores/file.d.ts +26 -0
  151. package/dist/stores/file.js +67 -21
  152. package/dist/stores/index.d.ts +16 -1
  153. package/dist/stores/index.js +16 -1
  154. package/dist/stores/memory.d.ts +4 -0
  155. package/dist/stores/memory.js +8 -6
  156. package/dist/stores/postgrest.d.ts +28 -0
  157. package/dist/stores/postgrest.js +84 -37
  158. package/dist/stores/types.d.ts +17 -0
  159. package/dist/stores/types.js +12 -0
  160. package/dist/sync/index.d.ts +3 -2
  161. package/dist/sync/index.js +2 -1
  162. package/dist/sync/log-store.d.ts +30 -0
  163. package/dist/sync/log-store.js +45 -0
  164. package/dist/sync/store.js +1 -1
  165. package/dist/trace/index.d.ts +18 -0
  166. package/dist/trace/index.js +13 -0
  167. package/dist/trace/log-view.d.ts +57 -0
  168. package/dist/trace/log-view.js +76 -0
  169. package/dist/trace/recorder.d.ts +75 -0
  170. package/dist/trace/recorder.js +157 -0
  171. package/dist/trace/replay.d.ts +132 -0
  172. package/dist/trace/replay.js +264 -0
  173. package/dist/trace/state.d.ts +102 -0
  174. package/dist/trace/state.js +86 -0
  175. package/dist/trace/store.d.ts +75 -0
  176. package/dist/trace/store.js +250 -0
  177. package/dist/utils/deep-merge.d.ts +10 -0
  178. package/dist/utils/deep-merge.js +23 -0
  179. package/dist/utils/file.d.ts +13 -4
  180. package/dist/utils/file.js +70 -12
  181. package/dist/utils/index.d.ts +2 -1
  182. package/dist/utils/index.js +2 -1
  183. package/dist/utils/long-timeout.d.ts +19 -0
  184. package/dist/utils/long-timeout.js +33 -0
  185. package/dist/utils/path.d.ts +22 -1
  186. package/dist/utils/path.js +46 -1
  187. package/dist/utils/redact.d.ts +22 -0
  188. package/dist/utils/redact.js +42 -0
  189. package/dist/utils/type-guards.d.ts +58 -0
  190. package/dist/utils/type-guards.js +92 -0
  191. package/dist/webhook/server.d.ts +9 -0
  192. package/dist/webhook/server.js +122 -36
  193. package/dist/webhook/types.d.ts +9 -1
  194. package/package.json +76 -9
  195. package/.claude/settings.local.json +0 -31
  196. package/.claude/skills/api-integration.md +0 -125
  197. package/.claude/skills/database-schema.md +0 -51
  198. package/.claude/skills/dsl-design.md +0 -80
  199. package/.claude/skills/property-testing.md +0 -143
  200. package/.claude/skills/reqon/SKILL.md +0 -44
  201. package/.claude/skills/reqon/references/examples.md +0 -206
  202. package/.claude/skills/reqon/references/syntax.md +0 -263
  203. package/.claude/skills/vscode-extension.md +0 -113
  204. package/.github/dependabot.yml +0 -32
  205. package/.github/pull_request_template.md +0 -21
  206. package/.github/workflows/ci.yml +0 -174
  207. package/.github/workflows/release.yml +0 -73
  208. package/CLAUDE.md +0 -72
  209. package/CONTRIBUTING.md +0 -161
  210. package/TODO.md +0 -51
  211. package/dist/auth/auth.test.d.ts +0 -1
  212. package/dist/auth/auth.test.js +0 -255
  213. package/dist/errors/errors.test.d.ts +0 -1
  214. package/dist/errors/errors.test.js +0 -165
  215. package/dist/execution/execution.test.d.ts +0 -1
  216. package/dist/execution/execution.test.js +0 -246
  217. package/dist/integration.test.d.ts +0 -1
  218. package/dist/integration.test.js +0 -168
  219. package/dist/interpreter/evaluator.test.d.ts +0 -1
  220. package/dist/interpreter/evaluator.test.js +0 -512
  221. package/dist/interpreter/http.test.d.ts +0 -1
  222. package/dist/interpreter/http.test.js +0 -299
  223. package/dist/interpreter/progress.test.d.ts +0 -1
  224. package/dist/interpreter/progress.test.js +0 -216
  225. package/dist/interpreter/schema-matcher.test.d.ts +0 -1
  226. package/dist/interpreter/schema-matcher.test.js +0 -122
  227. package/dist/lexer/lexer.d.ts +0 -24
  228. package/dist/lexer/lexer.js +0 -264
  229. package/dist/lexer/lexer.test.d.ts +0 -1
  230. package/dist/lexer/lexer.test.js +0 -259
  231. package/dist/loader/loader.test.d.ts +0 -1
  232. package/dist/loader/loader.test.js +0 -287
  233. package/dist/oas/oas.test.d.ts +0 -1
  234. package/dist/oas/oas.test.js +0 -218
  235. package/dist/parser/expressions.test.d.ts +0 -1
  236. package/dist/parser/expressions.test.js +0 -378
  237. package/dist/parser/match.test.d.ts +0 -1
  238. package/dist/parser/match.test.js +0 -254
  239. package/dist/parser/parser.test.d.ts +0 -1
  240. package/dist/parser/parser.test.js +0 -333
  241. package/dist/parser/schedule.test.d.ts +0 -1
  242. package/dist/parser/schedule.test.js +0 -241
  243. package/dist/scheduler/cron-parser.test.d.ts +0 -1
  244. package/dist/scheduler/cron-parser.test.js +0 -188
  245. package/dist/stores/file.test.d.ts +0 -1
  246. package/dist/stores/file.test.js +0 -165
  247. package/dist/stores/memory.test.d.ts +0 -1
  248. package/dist/stores/memory.test.js +0 -157
  249. package/dist/stores/stores.test.d.ts +0 -1
  250. package/dist/stores/stores.test.js +0 -158
  251. package/dist/sync/sync.test.d.ts +0 -1
  252. package/dist/sync/sync.test.js +0 -221
  253. package/docusaurus/README.md +0 -41
  254. package/docusaurus/docs/advanced/execution-state.md +0 -283
  255. package/docusaurus/docs/advanced/extending-reqon.md +0 -388
  256. package/docusaurus/docs/advanced/multi-file-missions.md +0 -250
  257. package/docusaurus/docs/advanced/parallel-execution.md +0 -353
  258. package/docusaurus/docs/api-reference.md +0 -443
  259. package/docusaurus/docs/authentication/api-key.md +0 -339
  260. package/docusaurus/docs/authentication/basic.md +0 -276
  261. package/docusaurus/docs/authentication/bearer.md +0 -282
  262. package/docusaurus/docs/authentication/oauth2.md +0 -317
  263. package/docusaurus/docs/authentication/overview.md +0 -251
  264. package/docusaurus/docs/cli.md +0 -229
  265. package/docusaurus/docs/core-concepts/actions.md +0 -286
  266. package/docusaurus/docs/core-concepts/missions.md +0 -264
  267. package/docusaurus/docs/core-concepts/schemas.md +0 -353
  268. package/docusaurus/docs/core-concepts/sources.md +0 -339
  269. package/docusaurus/docs/core-concepts/stores.md +0 -332
  270. package/docusaurus/docs/dsl-syntax/expressions.md +0 -361
  271. package/docusaurus/docs/dsl-syntax/fetch.md +0 -293
  272. package/docusaurus/docs/dsl-syntax/for-loops.md +0 -324
  273. package/docusaurus/docs/dsl-syntax/map.md +0 -345
  274. package/docusaurus/docs/dsl-syntax/match.md +0 -387
  275. package/docusaurus/docs/dsl-syntax/pipelines.md +0 -397
  276. package/docusaurus/docs/dsl-syntax/validate.md +0 -401
  277. package/docusaurus/docs/error-handling/dead-letter-queues.md +0 -399
  278. package/docusaurus/docs/error-handling/flow-control.md +0 -337
  279. package/docusaurus/docs/error-handling/retry-strategies.md +0 -368
  280. package/docusaurus/docs/examples.md +0 -488
  281. package/docusaurus/docs/getting-started.md +0 -256
  282. package/docusaurus/docs/http/circuit-breaker.md +0 -401
  283. package/docusaurus/docs/http/incremental-sync.md +0 -394
  284. package/docusaurus/docs/http/pagination.md +0 -361
  285. package/docusaurus/docs/http/rate-limiting.md +0 -383
  286. package/docusaurus/docs/http/requests.md +0 -328
  287. package/docusaurus/docs/http/retry.md +0 -402
  288. package/docusaurus/docs/intro.md +0 -90
  289. package/docusaurus/docs/openapi/loading-specs.md +0 -305
  290. package/docusaurus/docs/openapi/operation-calls.md +0 -314
  291. package/docusaurus/docs/openapi/overview.md +0 -212
  292. package/docusaurus/docs/openapi/response-validation.md +0 -344
  293. package/docusaurus/docs/scheduling/cron.md +0 -305
  294. package/docusaurus/docs/scheduling/daemon-mode.md +0 -317
  295. package/docusaurus/docs/scheduling/intervals.md +0 -289
  296. package/docusaurus/docs/scheduling/overview.md +0 -231
  297. package/docusaurus/docs/stores/custom-adapters.md +0 -376
  298. package/docusaurus/docs/stores/file.md +0 -236
  299. package/docusaurus/docs/stores/memory.md +0 -193
  300. package/docusaurus/docs/stores/overview.md +0 -274
  301. package/docusaurus/docs/stores/postgrest.md +0 -316
  302. package/docusaurus/docusaurus.config.ts +0 -148
  303. package/docusaurus/package-lock.json +0 -18029
  304. package/docusaurus/package.json +0 -47
  305. package/docusaurus/sidebars.ts +0 -155
  306. package/docusaurus/src/components/HomepageFeatures/index.tsx +0 -105
  307. package/docusaurus/src/components/HomepageFeatures/styles.module.css +0 -12
  308. package/docusaurus/src/css/custom.css +0 -169
  309. package/docusaurus/src/pages/index.module.css +0 -48
  310. package/docusaurus/src/pages/index.tsx +0 -110
  311. package/docusaurus/src/pages/markdown-page.md +0 -7
  312. package/docusaurus/static/.nojekyll +0 -0
  313. package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
  314. package/docusaurus/static/img/docusaurus.png +0 -0
  315. package/docusaurus/static/img/favicon.ico +0 -0
  316. package/docusaurus/static/img/logo.svg +0 -10
  317. package/docusaurus/static/img/undraw_docusaurus_mountain.svg +0 -171
  318. package/docusaurus/static/img/undraw_docusaurus_react.svg +0 -170
  319. package/docusaurus/static/img/undraw_docusaurus_tree.svg +0 -40
  320. package/docusaurus/tsconfig.json +0 -8
  321. package/examples/README.md +0 -112
  322. package/examples/error-handling/README.md +0 -150
  323. package/examples/error-handling/payment-processor.vague +0 -287
  324. package/examples/github-sync/README.md +0 -74
  325. package/examples/github-sync/fetch-issues.vague +0 -47
  326. package/examples/github-sync/fetch-prs.vague +0 -40
  327. package/examples/github-sync/mission.vague +0 -101
  328. package/examples/github-sync/normalize.vague +0 -70
  329. package/examples/jsonplaceholder/README.md +0 -28
  330. package/examples/jsonplaceholder/posts.vague +0 -48
  331. package/examples/petstore/README.md +0 -35
  332. package/examples/petstore/openapi.yaml +0 -97
  333. package/examples/petstore/sync.vague +0 -52
  334. package/examples/temporal-comparison/README.md +0 -297
  335. package/examples/temporal-comparison/reconciliation.vague +0 -355
  336. package/examples/temporal-comparison/temporal/activities/index.ts +0 -8
  337. package/examples/temporal-comparison/temporal/activities/shipstation.ts +0 -225
  338. package/examples/temporal-comparison/temporal/activities/shopify.ts +0 -257
  339. package/examples/temporal-comparison/temporal/activities/storage.ts +0 -198
  340. package/examples/temporal-comparison/temporal/activities/stripe.ts +0 -169
  341. package/examples/temporal-comparison/temporal/activities/validation.ts +0 -205
  342. package/examples/temporal-comparison/temporal/client/schedule.ts +0 -218
  343. package/examples/temporal-comparison/temporal/config/retry.ts +0 -63
  344. package/examples/temporal-comparison/temporal/types/index.ts +0 -129
  345. package/examples/temporal-comparison/temporal/workers/main.ts +0 -130
  346. package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +0 -262
  347. package/examples/xero/README.md +0 -88
  348. package/examples/xero/invoices.vague +0 -189
  349. package/src/api-integration.test.ts +0 -954
  350. package/src/ast/index.ts +0 -1
  351. package/src/ast/nodes.ts +0 -310
  352. package/src/auth/auth.test.ts +0 -326
  353. package/src/auth/circuit-breaker.test.ts +0 -390
  354. package/src/auth/circuit-breaker.ts +0 -379
  355. package/src/auth/credentials.test.ts +0 -273
  356. package/src/auth/credentials.ts +0 -246
  357. package/src/auth/index.ts +0 -40
  358. package/src/auth/oauth2-provider.ts +0 -177
  359. package/src/auth/rate-limiter.ts +0 -459
  360. package/src/auth/token-store.ts +0 -177
  361. package/src/auth/types.ts +0 -159
  362. package/src/benchmark/e2e.bench.ts +0 -288
  363. package/src/benchmark/evaluator.bench.ts +0 -331
  364. package/src/benchmark/fixtures.ts +0 -295
  365. package/src/benchmark/index.ts +0 -108
  366. package/src/benchmark/lexer.bench.ts +0 -69
  367. package/src/benchmark/parser.bench.ts +0 -103
  368. package/src/benchmark/resilience.bench.ts +0 -193
  369. package/src/benchmark/store.bench.ts +0 -147
  370. package/src/benchmark/utils.ts +0 -230
  371. package/src/cli.ts +0 -313
  372. package/src/errors/errors.test.ts +0 -234
  373. package/src/errors/index.ts +0 -223
  374. package/src/execution/execution.test.ts +0 -307
  375. package/src/execution/index.ts +0 -21
  376. package/src/execution/state.ts +0 -207
  377. package/src/execution/store.ts +0 -188
  378. package/src/index.ts +0 -169
  379. package/src/integration.test.ts +0 -192
  380. package/src/interpreter/context.ts +0 -57
  381. package/src/interpreter/evaluator.test.ts +0 -796
  382. package/src/interpreter/evaluator.ts +0 -245
  383. package/src/interpreter/executor.ts +0 -946
  384. package/src/interpreter/fetch-handler.ts +0 -302
  385. package/src/interpreter/http.test.ts +0 -423
  386. package/src/interpreter/http.ts +0 -308
  387. package/src/interpreter/index.ts +0 -32
  388. package/src/interpreter/pagination.ts +0 -207
  389. package/src/interpreter/progress.test.ts +0 -276
  390. package/src/interpreter/schema-matcher.test.ts +0 -160
  391. package/src/interpreter/schema-matcher.ts +0 -168
  392. package/src/interpreter/signals.ts +0 -73
  393. package/src/interpreter/step-handlers/for-handler.ts +0 -65
  394. package/src/interpreter/step-handlers/index.ts +0 -17
  395. package/src/interpreter/step-handlers/map-handler.ts +0 -24
  396. package/src/interpreter/step-handlers/match-handler.ts +0 -101
  397. package/src/interpreter/step-handlers/store-handler.ts +0 -78
  398. package/src/interpreter/step-handlers/types.ts +0 -17
  399. package/src/interpreter/step-handlers/validate-handler.ts +0 -30
  400. package/src/interpreter/step-handlers/webhook-handler.ts +0 -142
  401. package/src/lexer/index.ts +0 -18
  402. package/src/lexer/lexer.test.ts +0 -316
  403. package/src/lexer/tokens.ts +0 -179
  404. package/src/loader/index.ts +0 -288
  405. package/src/loader/loader.test.ts +0 -360
  406. package/src/oas/index.ts +0 -4
  407. package/src/oas/loader.ts +0 -126
  408. package/src/oas/oas.test.ts +0 -254
  409. package/src/oas/validator.ts +0 -299
  410. package/src/parser/base.ts +0 -124
  411. package/src/parser/expressions.test.ts +0 -525
  412. package/src/parser/expressions.ts +0 -314
  413. package/src/parser/index.ts +0 -3
  414. package/src/parser/match.test.ts +0 -296
  415. package/src/parser/parser.test.ts +0 -739
  416. package/src/parser/parser.ts +0 -1469
  417. package/src/parser/schedule.test.ts +0 -287
  418. package/src/parser/webhook.test.ts +0 -248
  419. package/src/plugin.ts +0 -83
  420. package/src/scheduler/cron-parser.test.ts +0 -236
  421. package/src/scheduler/cron-parser.ts +0 -236
  422. package/src/scheduler/index.ts +0 -10
  423. package/src/scheduler/scheduler.ts +0 -443
  424. package/src/scheduler/types.ts +0 -71
  425. package/src/stores/factory.ts +0 -104
  426. package/src/stores/file.test.ts +0 -276
  427. package/src/stores/file.ts +0 -211
  428. package/src/stores/index.ts +0 -6
  429. package/src/stores/memory.test.ts +0 -238
  430. package/src/stores/memory.ts +0 -63
  431. package/src/stores/postgrest.test.ts +0 -488
  432. package/src/stores/postgrest.ts +0 -263
  433. package/src/stores/stores.test.ts +0 -197
  434. package/src/stores/types.ts +0 -58
  435. package/src/sync/index.ts +0 -16
  436. package/src/sync/state.ts +0 -126
  437. package/src/sync/store.ts +0 -139
  438. package/src/sync/sync.test.ts +0 -271
  439. package/src/utils/async.ts +0 -10
  440. package/src/utils/file.ts +0 -106
  441. package/src/utils/index.ts +0 -14
  442. package/src/utils/logger.ts +0 -53
  443. package/src/utils/path.ts +0 -47
  444. package/src/webhook/index.ts +0 -15
  445. package/src/webhook/server.test.ts +0 -253
  446. package/src/webhook/server.ts +0 -389
  447. package/src/webhook/store.ts +0 -239
  448. package/src/webhook/types.ts +0 -93
  449. package/tsconfig.json +0 -17
  450. package/vitest.config.ts +0 -39
@@ -1,50 +1,104 @@
1
+ /**
2
+ * ---
3
+ * purpose: Mission executor - orchestrates pipeline execution
4
+ * inputs:
5
+ * - ReqonProgram - parsed AST
6
+ * - ExecutorConfig - auth, stores, callbacks, debug settings
7
+ * outputs:
8
+ * - ExecutionResult - success/errors, stores, duration
9
+ * related:
10
+ * - ./context.ts - execution state (variables, stores, sources)
11
+ * - ./evaluator.ts - expression evaluation
12
+ * - ./fetch-handler.ts - HTTP requests
13
+ * - ./step-handlers/ - individual step type handlers
14
+ * - ./source-manager.ts - auth provider management
15
+ * ---
16
+ */
1
17
  import { isParallelStage } from '../ast/nodes.js';
2
- import { createContext, setVariable } from './context.js';
18
+ import { createContext, childContext, setVariable } from './context.js';
19
+ import { createHash } from 'node:crypto';
3
20
  import { evaluate } from './evaluator.js';
4
- import { HttpClient, BearerAuthProvider, OAuth2AuthProvider } from './http.js';
5
- import { createStore, resolveStoreType } from '../stores/index.js';
6
- import { loadOAS } from '../oas/index.js';
21
+ import { SourceManager } from './source-manager.js';
22
+ import { StoreManager } from './store-manager.js';
7
23
  import { AdaptiveRateLimiter } from '../auth/rate-limiter.js';
8
24
  import { CircuitBreaker } from '../auth/circuit-breaker.js';
9
25
  import { createExecutionState, findResumePoint, FileExecutionStore, } from '../execution/index.js';
10
- import { FileSyncStore, } from '../sync/index.js';
26
+ import { FileSyncStore, LogBackedSyncStore } from '../sync/index.js';
11
27
  import { FetchHandler } from './fetch-handler.js';
12
- import { ForHandler, MapHandler, ValidateHandler, StoreHandler, MatchHandler, WebhookHandler, SkipSignal, RetrySignal, JumpSignal, QueueSignal, } from './step-handlers/index.js';
28
+ import { ForHandler, MapHandler, ValidateHandler, StoreHandler, MatchHandler, ApplyHandler, WebhookHandler, PauseHandler, SkipSignal, AbortError, RetrySignal, JumpSignal, QueueSignal, } from './step-handlers/index.js';
29
+ import { createStructuredLogger } from '../observability/index.js';
30
+ import { PauseSignal } from './signals.js';
31
+ import { FileTraceStore, createTraceRecorder, } from '../trace/index.js';
32
+ import { FilePauseStore, LogBackedPauseStore, createPauseManager, } from '../pause/index.js';
33
+ import { sleep } from '../utils/async.js';
34
+ import { redactNamedValue } from '../utils/redact.js';
35
+ import { effectId, loadState } from '../execution-log/index.js';
36
+ import { generateExecutionId } from '../execution/index.js';
37
+ /** Max parallel-stage actions running concurrently (bounds fan-out). */
38
+ const MAX_PARALLEL_ACTIONS = 8;
13
39
  export class MissionExecutor {
14
40
  config;
15
41
  ctx;
16
42
  errors = [];
17
43
  actionsRun = [];
18
- oasSources = new Map();
19
- sourceConfigs = new Map();
44
+ /** Monotonic key generator for queued values lacking an id. */
45
+ queueCounter = 0;
46
+ transforms = new Map();
20
47
  rateLimiter;
21
48
  circuitBreaker;
49
+ sourceManager;
50
+ storeManager;
22
51
  executionStore;
23
52
  executionState;
24
53
  syncStore;
25
54
  missionName;
55
+ eventEmitter;
56
+ logger;
57
+ executionLog;
58
+ /** Stable id used for the execution event log (independent of persistState). */
59
+ logExecutionId;
60
+ /** Effect ids already applied (from the log) — replay skips these. */
61
+ appliedEffects = new Set();
62
+ /** Backfill page progress per step id (from the log) — resumes pagination. */
63
+ pageProgress = new Map();
64
+ /** The pause being resumed on this run — its step replays past, not into, a pause. */
65
+ resumingPause;
66
+ debugController;
67
+ traceRecorder;
68
+ traceStore;
69
+ pauseManager;
70
+ pauseStore;
71
+ currentStageIndex = 0;
72
+ currentPauseId;
26
73
  constructor(config = {}) {
27
74
  this.config = config;
28
75
  this.ctx = createContext();
29
76
  this.rateLimiter = new AdaptiveRateLimiter();
30
77
  this.circuitBreaker = new CircuitBreaker();
78
+ // Initialize managers (logger set after verbose callbacks configured)
79
+ this.sourceManager = new SourceManager({ auth: config.auth, missionDir: config.missionDir }, { rateLimiter: this.rateLimiter, circuitBreaker: this.circuitBreaker });
80
+ this.storeManager = new StoreManager({
81
+ customStores: config.stores,
82
+ developmentMode: config.developmentMode,
83
+ dataDir: config.dataDir,
84
+ });
31
85
  // Set up rate limit callbacks with default logging if verbose
32
86
  const callbacks = config.rateLimitCallbacks ?? {};
33
87
  if (config.verbose && !callbacks.onRateLimited) {
34
88
  callbacks.onRateLimited = (event) => {
35
- console.log(`[Reqon] Rate limited on ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
89
+ this.log(`Rate limited on ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
36
90
  `waiting ${event.waitSeconds}s (strategy: ${event.strategy})`);
37
91
  };
38
92
  }
39
93
  if (config.verbose && !callbacks.onResumed) {
40
94
  callbacks.onResumed = (event) => {
41
- console.log(`[Reqon] Rate limit cleared for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} ` +
95
+ this.log(`Rate limit cleared for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} ` +
42
96
  `(waited ${event.waitedSeconds}s)`);
43
97
  };
44
98
  }
45
99
  if (config.verbose && !callbacks.onWaiting) {
46
100
  callbacks.onWaiting = (event) => {
47
- console.log(`[Reqon] Still waiting for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
101
+ this.log(`Still waiting for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
48
102
  `${event.waitSeconds}s remaining (elapsed: ${event.elapsedSeconds}s)`);
49
103
  };
50
104
  }
@@ -53,33 +107,78 @@ export class MissionExecutor {
53
107
  const cbCallbacks = config.circuitBreakerCallbacks ?? {};
54
108
  if (config.verbose && !cbCallbacks.onOpen) {
55
109
  cbCallbacks.onOpen = (event) => {
56
- console.log(`[Reqon] Circuit breaker OPEN for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
110
+ this.log(`Circuit breaker OPEN for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
57
111
  `${event.failures} failures (${event.reason ?? 'threshold exceeded'})`);
58
112
  };
59
113
  }
60
114
  if (config.verbose && !cbCallbacks.onHalfOpen) {
61
115
  cbCallbacks.onHalfOpen = (event) => {
62
- console.log(`[Reqon] Circuit breaker HALF-OPEN for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
116
+ this.log(`Circuit breaker HALF-OPEN for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
63
117
  `testing recovery`);
64
118
  };
65
119
  }
66
120
  if (config.verbose && !cbCallbacks.onClose) {
67
121
  cbCallbacks.onClose = (event) => {
68
- console.log(`[Reqon] Circuit breaker CLOSED for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
122
+ this.log(`Circuit breaker CLOSED for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
69
123
  `recovery successful`);
70
124
  };
71
125
  }
72
126
  if (config.verbose && !cbCallbacks.onRejected) {
73
127
  cbCallbacks.onRejected = (event) => {
74
- console.log(`[Reqon] Request REJECTED by circuit breaker for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
128
+ this.log(`Request REJECTED by circuit breaker for ${event.source}${event.endpoint ? `:${event.endpoint}` : ''} - ` +
75
129
  `retry in ${Math.ceil(event.nextAttemptIn / 1000)}s`);
76
130
  };
77
131
  }
78
132
  this.circuitBreaker.setCallbacks(cbCallbacks);
79
133
  // Initialize execution store if persistence enabled
80
134
  if (config.persistState) {
81
- this.executionStore = config.executionStore ?? new FileExecutionStore(`${config.dataDir ?? '.reqon-data'}/executions`);
135
+ this.executionStore =
136
+ config.executionStore ??
137
+ new FileExecutionStore(`${config.dataDir ?? '.reqon-data'}/executions`);
138
+ }
139
+ // Initialize event emitter if provided
140
+ this.eventEmitter = config.eventEmitter;
141
+ this.executionLog = config.executionLog;
142
+ // Initialize logger if verbose or provided
143
+ if (config.logger) {
144
+ this.logger = config.logger;
145
+ }
146
+ else if (config.verbose) {
147
+ this.logger = createStructuredLogger({
148
+ prefix: 'Reqon',
149
+ level: 'debug',
150
+ context: {},
151
+ });
82
152
  }
153
+ // Update managers with log function now that logger is configured
154
+ this.sourceManager = new SourceManager({ auth: config.auth, missionDir: config.missionDir, log: (msg) => this.log(msg) }, { rateLimiter: this.rateLimiter, circuitBreaker: this.circuitBreaker });
155
+ this.storeManager = new StoreManager({
156
+ customStores: config.stores,
157
+ developmentMode: config.developmentMode,
158
+ dataDir: config.dataDir,
159
+ log: (msg) => this.log(msg),
160
+ });
161
+ // Initialize debug controller if provided
162
+ this.debugController = config.debugController;
163
+ // Initialize trace store
164
+ this.traceStore =
165
+ config.traceStore ?? new FileTraceStore(`${config.dataDir ?? '.reqon-data'}/traces`);
166
+ // Initialize pause store and manager. In durable mode the execution log is
167
+ // the single source of truth — pause state (deadline, triggers, checkpoint)
168
+ // is recorded as pause events and folded back, rather than kept in a
169
+ // separate pause file.
170
+ this.pauseStore =
171
+ config.pauseStore ??
172
+ (config.executionLog
173
+ ? new LogBackedPauseStore(config.executionLog)
174
+ : new FilePauseStore(`${config.dataDir ?? '.reqon-data'}/pauses`));
175
+ this.pauseManager =
176
+ config.pauseManager ??
177
+ createPauseManager({
178
+ store: this.pauseStore,
179
+ webhookServer: config.webhookServer,
180
+ log: (msg) => this.log(msg),
181
+ });
83
182
  }
84
183
  async execute(program) {
85
184
  const startTime = Date.now();
@@ -96,6 +195,54 @@ export class MissionExecutor {
96
195
  }
97
196
  // Initialize or resume execution state
98
197
  await this.initializeExecutionState(mission);
198
+ // Establish a stable id for the execution log. On resume we reuse the prior
199
+ // id (so replay reads the same log); otherwise a fresh id.
200
+ this.logExecutionId =
201
+ this.executionState?.id ?? this.config.resumeFrom ?? generateExecutionId();
202
+ // Load already-applied effects from the log so replay skips them, and note
203
+ // a pending pause so we can record its resumption below.
204
+ let pendingPauseId;
205
+ let alreadyResumedPauseId;
206
+ if (this.executionLog) {
207
+ const prior = await loadState(this.executionLog, this.logExecutionId);
208
+ this.appliedEffects = new Set(prior.appliedEffects);
209
+ this.pageProgress = prior.pageProgress;
210
+ pendingPauseId = prior.pendingPauseId;
211
+ // A resume trigger may have recorded `pause.resumed` already; if the run
212
+ // didn't finish, we still need to replay past that pause.
213
+ alreadyResumedPauseId = prior.pendingPauseId ? undefined : prior.resumedPauseId;
214
+ }
215
+ await this.logEvent({ type: 'mission.started', mission: mission.name });
216
+ // If the prior log ended paused, this run is resuming that pause. Record it
217
+ // so the log's folded status leaves 'paused' before replay continues, and
218
+ // load the pause's checkpoint so the replayed pause step resumes past it
219
+ // (restoring captured state) rather than pausing again.
220
+ if (pendingPauseId) {
221
+ await this.logEvent({
222
+ type: 'pause.resumed',
223
+ pauseId: pendingPauseId,
224
+ resumedBy: this.config.resumeFrom ? 'resume' : 'replay',
225
+ });
226
+ this.resumingPause = await this.loadResumingPause(pendingPauseId);
227
+ }
228
+ else if (alreadyResumedPauseId) {
229
+ // The pause was already marked resumed out of band (by a webhook/timeout
230
+ // trigger) but the run didn't continue past it. Replay past it now without
231
+ // recording a second `pause.resumed` — otherwise the step re-pauses forever.
232
+ this.resumingPause = await this.loadResumingPause(alreadyResumedPauseId);
233
+ }
234
+ // Initialize trace recorder if tracing is enabled
235
+ if (mission.trace && this.traceStore && this.executionState) {
236
+ this.traceRecorder = createTraceRecorder({
237
+ executionId: this.executionState.id,
238
+ mission: mission.name,
239
+ mode: mission.trace.mode,
240
+ store: this.traceStore,
241
+ metadata: this.config.metadata,
242
+ streaming: true, // Stream snapshots as they happen
243
+ });
244
+ this.log(`Tracing enabled (mode: ${mission.trace.mode})`);
245
+ }
99
246
  try {
100
247
  await this.executeMission(mission);
101
248
  // Mark execution as completed
@@ -105,24 +252,44 @@ export class MissionExecutor {
105
252
  this.executionState.duration = Date.now() - startTime;
106
253
  await this.saveExecutionState();
107
254
  }
255
+ await this.logEvent({ type: 'mission.completed' });
108
256
  }
109
257
  catch (error) {
110
- this.errors.push({
111
- action: 'mission',
112
- step: 'execute',
113
- message: error.message,
114
- details: error,
115
- });
116
- // Mark execution as failed
117
- if (this.executionState) {
118
- this.executionState.status = 'failed';
119
- this.executionState.completedAt = new Date();
120
- this.executionState.duration = Date.now() - startTime;
121
- await this.saveExecutionState();
258
+ // PauseSignal is not an error - execution was intentionally paused
259
+ if (error instanceof PauseSignal) {
260
+ this.log('Execution paused');
261
+ this.currentPauseId = error.pauseId;
262
+ // State is already set to 'paused' in checkPause() or pause handler
263
+ // Don't record as error, just let execution end.
264
+ //
265
+ // A LogBackedPauseStore already appended pause.created (with the full
266
+ // pause payload) when the pause manager saved it, so only emit the bare
267
+ // event here when the pause store is *not* the log itself — otherwise
268
+ // we'd record the pause twice.
269
+ if (error.pauseId && !(this.pauseStore instanceof LogBackedPauseStore)) {
270
+ await this.logEvent({ type: 'pause.created', pauseId: error.pauseId });
271
+ }
272
+ }
273
+ else {
274
+ this.errors.push({
275
+ action: 'mission',
276
+ step: 'execute',
277
+ message: error.message,
278
+ details: error,
279
+ });
280
+ // Mark execution as failed
281
+ if (this.executionState) {
282
+ this.executionState.status = 'failed';
283
+ this.executionState.completedAt = new Date();
284
+ this.executionState.duration = Date.now() - startTime;
285
+ await this.saveExecutionState();
286
+ }
287
+ await this.logEvent({ type: 'mission.failed', error: error.message });
122
288
  }
123
289
  }
124
290
  const duration = Date.now() - startTime;
125
- const success = this.errors.length === 0;
291
+ const isPaused = this.executionState?.status === 'paused';
292
+ const success = this.errors.length === 0 && !isPaused;
126
293
  // Emit onExecutionComplete callback - count stages in a single pass
127
294
  const stageCounts = this.executionState?.stages.reduce((acc, s) => {
128
295
  if (s.status === 'completed')
@@ -142,14 +309,44 @@ export class MissionExecutor {
142
309
  stagesFailed,
143
310
  errors: this.errors,
144
311
  });
312
+ // Emit mission.complete, mission.paused, or mission.failed event
313
+ if (isPaused) {
314
+ this.eventEmitter?.emit('mission.paused', {
315
+ stagesCompleted,
316
+ executionId: this.executionState?.id,
317
+ });
318
+ }
319
+ else if (success) {
320
+ this.eventEmitter?.emit('mission.complete', {
321
+ success: true,
322
+ stagesCompleted,
323
+ stagesFailed,
324
+ stagesSkipped: this.executionState?.stages.filter((s) => s.status === 'skipped').length ?? 0,
325
+ errorCount: this.errors.length,
326
+ });
327
+ }
328
+ else {
329
+ const failedStage = this.executionState?.stages.find((s) => s.status === 'failed');
330
+ this.eventEmitter?.emit('mission.failed', {
331
+ error: this.errors[0]?.message ?? 'Unknown error',
332
+ failedStage: failedStage?.action,
333
+ stagesCompleted,
334
+ });
335
+ }
336
+ // Finalize trace if enabled
337
+ if (this.traceRecorder) {
338
+ await this.traceRecorder.finalize(success);
339
+ }
145
340
  return {
146
341
  success,
147
342
  duration,
148
343
  actionsRun: this.actionsRun,
149
344
  errors: this.errors,
150
345
  stores: this.ctx.stores,
151
- executionId: this.executionState?.id,
346
+ executionId: this.executionState?.id ?? this.logExecutionId,
152
347
  state: this.executionState,
348
+ traceId: this.traceRecorder ? this.executionState?.id : undefined,
349
+ pauseId: this.currentPauseId,
153
350
  };
154
351
  }
155
352
  async initializeExecutionState(mission) {
@@ -190,6 +387,13 @@ export class MissionExecutor {
190
387
  isResume,
191
388
  metadata: this.config.metadata,
192
389
  });
390
+ // Emit mission.start event
391
+ this.eventEmitter?.emit('mission.start', {
392
+ stageCount: mission.pipeline.stages.length,
393
+ isResume,
394
+ resumeFromStage: isResume ? findResumePoint(this.executionState) : undefined,
395
+ metadata: this.config.metadata,
396
+ });
193
397
  }
194
398
  async saveExecutionState() {
195
399
  if (this.executionStore && this.executionState) {
@@ -226,21 +430,28 @@ export class MissionExecutor {
226
430
  async executeMission(mission) {
227
431
  this.log(`Executing mission: ${mission.name}`);
228
432
  this.missionName = mission.name;
229
- // Initialize sync store
230
- this.syncStore = this.config.syncStore ?? new FileSyncStore(mission.name, `${this.config.dataDir ?? '.reqon-data'}/sync`);
231
- // Initialize sources (HTTP clients)
232
- for (const source of mission.sources) {
233
- await this.initializeSource(source);
234
- }
235
- // Initialize stores
236
- for (const store of mission.stores) {
237
- await this.initializeStore(store);
238
- }
433
+ // Initialize sync store. In durable mode the execution log is the single
434
+ // source of truth, so sync checkpoints are read back as a view over the log
435
+ // rather than from a separate sync file.
436
+ this.syncStore =
437
+ this.config.syncStore ??
438
+ (this.executionLog
439
+ ? new LogBackedSyncStore(this.executionLog, mission.name)
440
+ : new FileSyncStore(mission.name, `${this.config.dataDir ?? '.reqon-data'}/sync`));
441
+ // Initialize sources using SourceManager
442
+ await this.sourceManager.initializeSources(mission.sources, this.ctx);
443
+ // Initialize stores using StoreManager
444
+ await this.storeManager.initializeStores(mission.stores, this.ctx);
239
445
  // Initialize schemas (for match step schema matching)
240
446
  for (const schema of mission.schemas) {
241
447
  this.ctx.schemas.set(schema.name, schema);
242
448
  this.log(`Registered schema: ${schema.name}`);
243
449
  }
450
+ // Initialize transforms
451
+ for (const transform of mission.transforms) {
452
+ this.transforms.set(transform.name, transform);
453
+ this.log(`Registered transform: ${transform.name}`);
454
+ }
244
455
  // Build action lookup
245
456
  const actions = new Map();
246
457
  for (const action of mission.actions) {
@@ -256,6 +467,8 @@ export class MissionExecutor {
256
467
  // Execute pipeline
257
468
  for (let i = 0; i < mission.pipeline.stages.length; i++) {
258
469
  const stage = mission.pipeline.stages[i];
470
+ // Check for pause request at safe point (between stages)
471
+ await this.checkPause();
259
472
  // Skip already completed stages when resuming
260
473
  if (i < resumeIndex) {
261
474
  this.log(`Skipping ${this.getStageName(stage)} (already completed)`);
@@ -268,16 +481,37 @@ export class MissionExecutor {
268
481
  this.log(`Skipping ${this.getStageName(stage)} (condition not met)`);
269
482
  this.updateStageState(i, { status: 'skipped' });
270
483
  await this.saveExecutionState();
484
+ this.updateControlServerState();
271
485
  continue;
272
486
  }
273
487
  }
488
+ // Track current stage index for pause handler
489
+ this.currentStageIndex = i;
274
490
  // Execute stage (parallel or sequential)
275
- if (isParallelStage(stage)) {
276
- await this.executeParallelStage(i, stage, actions, mission);
491
+ try {
492
+ if (isParallelStage(stage)) {
493
+ await this.executeParallelStage(i, stage, actions, mission);
494
+ }
495
+ else if (stage.action) {
496
+ await this.executeSequentialStage(i, stage.action, actions, mission);
497
+ }
277
498
  }
278
- else if (stage.action) {
279
- await this.executeSequentialStage(i, stage.action, actions, mission);
499
+ catch (error) {
500
+ // A jump directive redirects the pipeline to a named action's stage.
501
+ if (error instanceof JumpSignal) {
502
+ const targetIndex = mission.pipeline.stages.findIndex((s) => !isParallelStage(s) && s.action === error.action);
503
+ if (targetIndex === -1) {
504
+ throw new Error(`Jump target action not found in pipeline: ${error.action}`);
505
+ }
506
+ this.log(`Jump to action '${error.action}' (stage ${targetIndex})`);
507
+ i = targetIndex - 1; // loop's i++ lands on the target stage
508
+ this.updateControlServerState();
509
+ continue;
510
+ }
511
+ throw error;
280
512
  }
513
+ // Update control server with latest state after each stage
514
+ this.updateControlServerState();
281
515
  }
282
516
  }
283
517
  getStageName(stage) {
@@ -303,6 +537,13 @@ export class MissionExecutor {
303
537
  stageName: actionName,
304
538
  totalStages: mission.pipeline.stages.length,
305
539
  });
540
+ // Emit stage.start event
541
+ this.eventEmitter?.emit('stage.start', {
542
+ stageIndex,
543
+ stageName: actionName,
544
+ totalStages: mission.pipeline.stages.length,
545
+ isParallel: false,
546
+ });
306
547
  try {
307
548
  await this.executeAction(action);
308
549
  this.actionsRun.push(action.name);
@@ -319,8 +560,19 @@ export class MissionExecutor {
319
560
  success: true,
320
561
  duration: Date.now() - stageStartTime,
321
562
  });
563
+ // Emit stage.complete event
564
+ this.eventEmitter?.emit('stage.complete', {
565
+ stageIndex,
566
+ stageName: actionName,
567
+ success: true,
568
+ });
322
569
  }
323
570
  catch (error) {
571
+ // A jump directive is flow control, not a stage failure — let it bubble
572
+ // to the mission loop without polluting stage state.
573
+ if (error instanceof JumpSignal) {
574
+ throw error;
575
+ }
324
576
  // Mark stage as failed
325
577
  this.updateStageState(stageIndex, {
326
578
  status: 'failed',
@@ -338,9 +590,49 @@ export class MissionExecutor {
338
590
  duration: Date.now() - stageStartTime,
339
591
  error: error.message,
340
592
  });
593
+ // Emit stage.complete event (failure)
594
+ this.eventEmitter?.emit('stage.complete', {
595
+ stageIndex,
596
+ stageName: actionName,
597
+ success: false,
598
+ error: error.message,
599
+ });
341
600
  throw error; // Re-throw to stop execution
342
601
  }
343
602
  }
603
+ /**
604
+ * Run all settled-style tasks with a bounded number in flight at once,
605
+ * preserving result order. Caps fan-out so a wide `run [...]` can't open an
606
+ * unbounded number of concurrent HTTP/store operations.
607
+ */
608
+ async settleWithLimit(tasks, limit) {
609
+ const results = new Array(tasks.length);
610
+ let next = 0;
611
+ const worker = async () => {
612
+ for (let i = next++; i < tasks.length; i = next++) {
613
+ try {
614
+ results[i] = { status: 'fulfilled', value: await tasks[i]() };
615
+ }
616
+ catch (reason) {
617
+ results[i] = { status: 'rejected', reason };
618
+ }
619
+ }
620
+ };
621
+ const workerCount = Math.min(Math.max(1, limit), tasks.length);
622
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
623
+ return results;
624
+ }
625
+ /**
626
+ * Execute a `run [A, B, ...]` stage.
627
+ *
628
+ * Failure semantics are **complete-then-fail**: every branch runs to
629
+ * completion (bounded by MAX_PARALLEL_ACTIONS in flight), then the stage
630
+ * fails if any branch failed. There is no cancellation of siblings and no
631
+ * rollback — a branch that committed store writes keeps them even if another
632
+ * branch failed. Each branch gets its own action scope (step counter +
633
+ * checkpoints); stores/sources/schemas are shared, so parallel branches that
634
+ * write the same key get last-writer-wins and should target disjoint keys.
635
+ */
344
636
  async executeParallelStage(stageIndex, stage, actions, mission) {
345
637
  const actionNames = stage.actions;
346
638
  const stageName = `[${actionNames.join(', ')}]`;
@@ -365,10 +657,19 @@ export class MissionExecutor {
365
657
  stageName,
366
658
  totalStages: mission.pipeline.stages.length,
367
659
  });
660
+ // Emit stage.start event (parallel)
661
+ this.eventEmitter?.emit('stage.start', {
662
+ stageIndex,
663
+ stageName,
664
+ totalStages: mission.pipeline.stages.length,
665
+ isParallel: true,
666
+ parallelActions: actionNames,
667
+ });
368
668
  this.log(`Executing parallel stage: ${stageName}`);
369
669
  try {
370
- // Execute all actions in parallel
371
- const results = await Promise.allSettled(actionDefs.map(action => this.executeAction(action)));
670
+ // Execute all actions in parallel, bounded to MAX_PARALLEL_ACTIONS in
671
+ // flight. allSettled semantics: every started branch runs to completion.
672
+ const results = await this.settleWithLimit(actionDefs.map((action) => () => this.executeAction(action)), MAX_PARALLEL_ACTIONS);
372
673
  // Check for failures
373
674
  const failures = [];
374
675
  for (let i = 0; i < results.length; i++) {
@@ -381,7 +682,7 @@ export class MissionExecutor {
381
682
  }
382
683
  }
383
684
  if (failures.length > 0) {
384
- const errorMsg = failures.map(f => `${f.name}: ${f.error.message}`).join('; ');
685
+ const errorMsg = failures.map((f) => `${f.name}: ${f.error.message}`).join('; ');
385
686
  throw new Error(`Parallel stage failed: ${errorMsg}`);
386
687
  }
387
688
  // Mark stage as completed
@@ -397,6 +698,12 @@ export class MissionExecutor {
397
698
  success: true,
398
699
  duration: Date.now() - stageStartTime,
399
700
  });
701
+ // Emit stage.complete event (success)
702
+ this.eventEmitter?.emit('stage.complete', {
703
+ stageIndex,
704
+ stageName,
705
+ success: true,
706
+ });
400
707
  }
401
708
  catch (error) {
402
709
  // Mark stage as failed
@@ -416,154 +723,219 @@ export class MissionExecutor {
416
723
  duration: Date.now() - stageStartTime,
417
724
  error: error.message,
418
725
  });
726
+ // Emit stage.complete event (failure)
727
+ this.eventEmitter?.emit('stage.complete', {
728
+ stageIndex,
729
+ stageName,
730
+ success: false,
731
+ error: error.message,
732
+ });
419
733
  throw error; // Re-throw to stop execution
420
734
  }
421
735
  }
422
- async initializeSource(source) {
423
- // Store source config for later reference
424
- this.sourceConfigs.set(source.name, source);
425
- const authConfig = this.config.auth?.[source.name];
426
- let authProvider;
427
- if (authConfig) {
428
- if (authConfig.type === 'bearer' && authConfig.token) {
429
- authProvider = new BearerAuthProvider(authConfig.token);
430
- }
431
- else if (authConfig.type === 'oauth2' && authConfig.accessToken) {
432
- authProvider = new OAuth2AuthProvider({
433
- accessToken: authConfig.accessToken,
434
- refreshToken: authConfig.refreshToken,
435
- tokenEndpoint: authConfig.tokenEndpoint,
436
- clientId: authConfig.clientId,
437
- clientSecret: authConfig.clientSecret,
438
- });
439
- }
440
- }
441
- // If source has OAS spec, load it
442
- let baseUrl = source.config.base;
443
- if (source.specPath) {
736
+ async executeAction(action) {
737
+ this.log(`Executing action: ${action.name}`);
738
+ // Flow-control directives surface as thrown signals from a step (typically
739
+ // a `match` arm). Handle them at the action boundary: skip stops the rest
740
+ // of the action, queue stashes a value and stops, retry re-runs the whole
741
+ // action with backoff. Jump/Pause propagate to the mission loop.
742
+ const MAX_RETRY_FALLBACK = 3;
743
+ let attempt = 0;
744
+ for (;;) {
745
+ // Create a child context for this action with its own response scope and
746
+ // its own action scope (step counter + deferred checkpoints). Each attempt
747
+ // gets a fresh scope, so a retry's fetch doesn't double-record and so
748
+ // parallel actions never share a counter or checkpoint list.
749
+ const actionCtx = childContext(this.ctx);
750
+ actionCtx.actionScope = { stepIndex: 0, attempt, pendingCheckpoints: [] };
444
751
  try {
445
- const oasSource = await loadOAS(source.specPath);
446
- this.oasSources.set(source.name, oasSource);
447
- // Use base URL from OAS if not explicitly provided
448
- if (!baseUrl) {
449
- baseUrl = oasSource.baseUrl;
752
+ for (const step of action.steps) {
753
+ await this.executeStep(step, action.name, actionCtx);
450
754
  }
451
- this.log(`Loaded OAS spec for ${source.name}: ${oasSource.operations.size} operations`);
755
+ // Flush checkpoints for fetches that completed without a later store.
756
+ await this.flushPendingCheckpoints(actionCtx);
757
+ return;
452
758
  }
453
759
  catch (error) {
454
- throw new Error(`Failed to load OAS spec for ${source.name}: ${error.message}`);
760
+ if (error instanceof SkipSignal) {
761
+ this.log(`Action ${action.name}: skip — remaining steps skipped`);
762
+ await this.flushPendingCheckpoints(actionCtx);
763
+ return;
764
+ }
765
+ if (error instanceof QueueSignal) {
766
+ await this.handleQueue(error);
767
+ await this.flushPendingCheckpoints(actionCtx);
768
+ return;
769
+ }
770
+ if (error instanceof RetrySignal) {
771
+ const maxAttempts = error.backoff?.maxAttempts ?? MAX_RETRY_FALLBACK;
772
+ attempt++;
773
+ if (attempt >= maxAttempts) {
774
+ throw new Error(`Action ${action.name} exhausted ${maxAttempts} retry attempt(s)`);
775
+ }
776
+ const delay = this.computeRetryDelay(error.backoff, attempt);
777
+ this.log(`Action ${action.name}: retry ${attempt}/${maxAttempts} in ${delay}ms`);
778
+ if (delay > 0)
779
+ await sleep(delay);
780
+ continue;
781
+ }
782
+ // JumpSignal, PauseSignal, and real errors propagate. The action's
783
+ // checkpoints live on actionCtx and are simply discarded (never flushed)
784
+ // since the data was not durably stored.
785
+ throw error;
455
786
  }
456
787
  }
457
- if (!baseUrl) {
458
- throw new Error(`Source ${source.name} has no base URL (provide 'base' or OAS spec with servers)`);
788
+ }
789
+ /** Compute a retry backoff delay from a RetrySignal's backoff config. */
790
+ computeRetryDelay(backoff, attempt) {
791
+ const initial = backoff?.initialDelay ?? 0;
792
+ switch (backoff?.backoff) {
793
+ case 'exponential':
794
+ return initial * Math.pow(2, attempt - 1);
795
+ case 'linear':
796
+ return initial * attempt;
797
+ default:
798
+ return initial;
459
799
  }
460
- // Configure rate limiter for this source
461
- if (source.config.rateLimit) {
462
- this.rateLimiter.configure(source.name, {
463
- strategy: source.config.rateLimit.strategy,
464
- maxWait: source.config.rateLimit.maxWait,
465
- fallbackRpm: source.config.rateLimit.fallbackRpm,
466
- });
467
- this.log(`Rate limit config for ${source.name}: strategy=${source.config.rateLimit.strategy ?? 'pause'}, ` +
468
- `maxWait=${source.config.rateLimit.maxWait ?? 300}s`);
469
- }
470
- // Configure circuit breaker for this source
471
- if (source.config.circuitBreaker) {
472
- this.circuitBreaker.configure(source.name, {
473
- failureThreshold: source.config.circuitBreaker.failureThreshold,
474
- // Convert seconds to milliseconds for the circuit breaker
475
- resetTimeout: source.config.circuitBreaker.resetTimeout
476
- ? source.config.circuitBreaker.resetTimeout * 1000
477
- : undefined,
478
- successThreshold: source.config.circuitBreaker.successThreshold,
479
- failureWindow: source.config.circuitBreaker.failureWindow
480
- ? source.config.circuitBreaker.failureWindow * 1000
481
- : undefined,
482
- });
483
- this.log(`Circuit breaker config for ${source.name}: ` +
484
- `failureThreshold=${source.config.circuitBreaker.failureThreshold ?? 5}, ` +
485
- `resetTimeout=${source.config.circuitBreaker.resetTimeout ?? 30}s`);
486
- }
487
- const client = new HttpClient({
488
- baseUrl,
489
- auth: authProvider,
490
- rateLimiter: this.rateLimiter,
491
- circuitBreaker: this.circuitBreaker,
492
- sourceName: source.name,
493
- });
494
- this.ctx.sources.set(source.name, client);
495
- this.log(`Initialized source: ${source.name}`);
496
- }
497
- async initializeStore(store) {
498
- // Check for custom store adapter
499
- if (this.config.stores?.[store.name]) {
500
- this.ctx.stores.set(store.name, this.config.stores[store.name]);
501
- this.log(`Initialized store: ${store.name} (custom adapter)`);
800
+ }
801
+ /** Push a queued value to its target store (queue directive). */
802
+ async handleQueue(signal) {
803
+ const target = signal.target;
804
+ if (!target) {
805
+ this.log('Queue directive without target — value discarded');
502
806
  return;
503
807
  }
504
- // Use store factory to create appropriate adapter
505
- const developmentMode = this.config.developmentMode ?? true;
506
- const storeType = resolveStoreType(store.storeType, developmentMode);
507
- const adapter = createStore({
508
- type: storeType,
509
- name: store.target,
510
- baseDir: this.config.dataDir,
511
- });
512
- this.ctx.stores.set(store.name, adapter);
513
- this.log(`Initialized store: ${store.name} (${storeType}${storeType !== store.storeType ? ` <- ${store.storeType}` : ''})`);
514
- }
515
- async executeAction(action) {
516
- this.log(`Executing action: ${action.name}`);
517
- for (const step of action.steps) {
518
- await this.executeStep(step, action.name);
808
+ const store = this.ctx.stores.get(target);
809
+ if (!store) {
810
+ throw new Error(`Queue target store not found: ${target}`);
519
811
  }
812
+ const value = signal.value;
813
+ const record = value && typeof value === 'object' && !Array.isArray(value)
814
+ ? value
815
+ : { value };
816
+ const key = typeof record.id === 'string' || typeof record.id === 'number'
817
+ ? String(record.id)
818
+ : `queued-${this.queueCounter++}`;
819
+ await store.set(key, record);
820
+ this.log(`Queued value to store '${target}' (key=${key})`);
520
821
  }
521
822
  async executeStep(step, actionName, ctx) {
522
823
  // Use provided context or default to this.ctx
824
+ // NOTE: ctx is used for action-scoped operations (response, variables)
825
+ // this.ctx is still used for mission-level resources (stores, sources)
523
826
  const execCtx = ctx ?? this.ctx;
524
- const originalCtx = this.ctx;
525
- // Temporarily use the provided context
526
- if (ctx) {
527
- this.ctx = ctx;
827
+ // Track step index per-action so parallel actions don't share a counter.
828
+ const scope = this.scopeFor(execCtx);
829
+ const currentStepIndex = scope.stepIndex++;
830
+ const stepType = this.getStepType(step.type);
831
+ // Stable step identity for the execution log: action + per-action index.
832
+ const stepId = `${actionName}#${currentStepIndex}`;
833
+ // Emit step.start event
834
+ this.eventEmitter?.emit('step.start', {
835
+ actionName,
836
+ stepIndex: currentStepIndex,
837
+ stepType,
838
+ });
839
+ // Append step.started to the durable execution log.
840
+ await this.logEvent({
841
+ type: 'step.started',
842
+ stepId,
843
+ action: actionName,
844
+ stepType,
845
+ attempt: scope.attempt,
846
+ });
847
+ const stepStartTime = Date.now();
848
+ // Record trace snapshot before step
849
+ if (this.traceRecorder) {
850
+ await this.traceRecorder.recordBeforeStep(actionName, currentStepIndex, stepType, execCtx);
851
+ }
852
+ // Debug pause point - before executing step
853
+ if (this.debugController) {
854
+ const location = {
855
+ action: actionName,
856
+ stepIndex: currentStepIndex,
857
+ stepType,
858
+ };
859
+ if (this.debugController.shouldPause(location)) {
860
+ const snapshot = this.captureDebugSnapshot(actionName, currentStepIndex, stepType, { type: 'step' }, execCtx);
861
+ const command = await this.debugController.pause(snapshot);
862
+ this.handleDebugCommand(command);
863
+ }
528
864
  }
529
865
  try {
530
866
  switch (step.type) {
531
867
  case 'FetchStep':
532
- await this.executeFetch(step);
868
+ await this.executeFetch(step, execCtx, stepId);
533
869
  break;
534
870
  case 'ForStep':
535
- await this.executeFor(step, actionName);
871
+ await this.executeFor(step, actionName, execCtx);
536
872
  break;
537
873
  case 'MapStep':
538
- await this.executeMap(step);
874
+ await this.executeMap(step, execCtx);
539
875
  break;
540
876
  case 'ValidateStep':
541
- await this.executeValidate(step);
877
+ await this.executeValidate(step, execCtx);
542
878
  break;
543
879
  case 'StoreStep':
544
- await this.executeStore(step);
880
+ await this.executeStore(step, execCtx, stepId);
545
881
  break;
546
882
  case 'MatchStep':
547
- await this.executeMatch(step, actionName);
883
+ await this.executeMatch(step, actionName, execCtx);
548
884
  break;
549
885
  case 'LetStep':
550
- await this.executeLet(step);
886
+ await this.executeLet(step, execCtx);
887
+ break;
888
+ case 'ApplyStep':
889
+ await this.executeApply(step, execCtx);
551
890
  break;
552
891
  case 'WebhookStep':
553
- await this.executeWebhook(step);
892
+ await this.executeWebhook(step, execCtx);
893
+ break;
894
+ case 'PauseStep':
895
+ await this.executePause(step, actionName, currentStepIndex, execCtx);
554
896
  break;
555
897
  default:
556
898
  throw new Error(`Unknown step type: ${step.type}`);
557
899
  }
900
+ const stepDuration = Date.now() - stepStartTime;
901
+ // Record trace snapshot after step
902
+ if (this.traceRecorder) {
903
+ await this.traceRecorder.recordAfterStep(actionName, currentStepIndex, stepType, execCtx, stepDuration);
904
+ }
905
+ // Emit step.complete event (success)
906
+ this.eventEmitter?.emit('step.complete', {
907
+ actionName,
908
+ stepIndex: currentStepIndex,
909
+ stepType,
910
+ success: true,
911
+ });
912
+ // Append step.completed to the durable execution log.
913
+ await this.logEvent({ type: 'step.completed', stepId, attempt: scope.attempt });
558
914
  }
559
915
  catch (error) {
560
916
  // Re-throw flow control signals without recording as errors
561
917
  if (error instanceof SkipSignal ||
562
918
  error instanceof RetrySignal ||
563
919
  error instanceof JumpSignal ||
564
- error instanceof QueueSignal) {
920
+ error instanceof QueueSignal ||
921
+ error instanceof PauseSignal) {
922
+ // Emit step.complete for flow control (not an error)
923
+ this.eventEmitter?.emit('step.complete', {
924
+ actionName,
925
+ stepIndex: currentStepIndex,
926
+ stepType,
927
+ success: true, // Flow control is not a failure
928
+ });
565
929
  throw error;
566
930
  }
931
+ // Emit step.complete event (failure)
932
+ this.eventEmitter?.emit('step.complete', {
933
+ actionName,
934
+ stepIndex: currentStepIndex,
935
+ stepType,
936
+ success: false,
937
+ error: error.message,
938
+ });
567
939
  // AbortError is a controlled abort, still record it
568
940
  this.errors.push({
569
941
  action: actionName,
@@ -573,91 +945,424 @@ export class MissionExecutor {
573
945
  });
574
946
  throw error;
575
947
  }
576
- finally {
577
- // Restore original context
578
- if (ctx) {
579
- this.ctx = originalCtx;
580
- }
581
- }
582
948
  }
583
- async executeFetch(step) {
949
+ async executeFetch(step, ctx, stepId) {
584
950
  const fetchHandler = new FetchHandler({
585
- ctx: this.ctx,
586
- oasSources: this.oasSources,
587
- sourceConfigs: this.sourceConfigs,
951
+ ctx,
952
+ oasSources: this.sourceManager.getAllOASSources(),
953
+ sourceConfigs: this.sourceManager.getAllSourceConfigs(),
588
954
  syncStore: this.syncStore,
589
955
  missionName: this.missionName,
590
956
  executionId: this.executionState?.id,
591
957
  dryRun: this.config.dryRun,
592
958
  log: (msg) => this.log(msg),
959
+ emit: this.eventEmitter
960
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
961
+ : undefined,
962
+ // Durable mode: mutating fetches carry a stable Idempotency-Key.
963
+ idempotency: this.executionLog && this.logExecutionId && stepId
964
+ ? { executionId: this.logExecutionId, stepId }
965
+ : undefined,
966
+ // Resumable backfill: seed pagination from the log and record each page.
967
+ pagination: step.backfill && this.executionLog && this.logExecutionId && stepId
968
+ ? {
969
+ resume: this.pageProgress.get(stepId),
970
+ maxItemsPerRun: this.config.backfillMaxItemsPerRun,
971
+ onPage: async (progress) => {
972
+ await this.logEvent({
973
+ type: 'page.completed',
974
+ stepId,
975
+ page: progress.page,
976
+ cursor: progress.cursor,
977
+ recordCount: progress.recordCount,
978
+ done: progress.done,
979
+ });
980
+ },
981
+ }
982
+ : undefined,
593
983
  });
984
+ // Capture when the request began; used as the checkpoint fallback time so
985
+ // a sync without an explicit update field never advances past records
986
+ // written during the fetch.
987
+ const fetchStartedAt = new Date();
594
988
  const result = await fetchHandler.execute(step);
595
- this.ctx.response = result.data;
596
- // Update sync checkpoint after successful fetch
989
+ ctx.response = result.data;
990
+ // Defer the sync checkpoint until the fetched data is durably stored.
597
991
  if (result.checkpointKey && this.syncStore) {
598
- await fetchHandler.recordCheckpoint(result.checkpointKey, step, result.data);
992
+ const key = result.checkpointKey;
993
+ const data = result.data;
994
+ this.scopeFor(ctx).pendingCheckpoints.push(async () => {
995
+ const syncedAt = await fetchHandler.recordCheckpoint(key, step, data, fetchStartedAt);
996
+ if (syncedAt) {
997
+ await this.logEvent({
998
+ type: 'checkpoint.advanced',
999
+ key,
1000
+ syncedAt: syncedAt.toISOString(),
1001
+ recordCount: Array.isArray(data) ? data.length : undefined,
1002
+ mission: this.missionName,
1003
+ });
1004
+ }
1005
+ });
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Per-action mutable scope (step counter + deferred checkpoints). Lazily
1010
+ * created so a step run with the bare mission context still works; normally
1011
+ * executeAction installs a fresh scope that nested scopes inherit.
1012
+ */
1013
+ scopeFor(ctx) {
1014
+ if (!ctx.actionScope) {
1015
+ ctx.actionScope = { stepIndex: 0, attempt: 0, pendingCheckpoints: [] };
599
1016
  }
1017
+ return ctx.actionScope;
600
1018
  }
601
- async executeFor(step, actionName) {
1019
+ /**
1020
+ * Append an event to the execution log. No-op (zero cost) when no log is
1021
+ * configured. The executionId is supplied from this run's stable log id.
1022
+ */
1023
+ async logEvent(event) {
1024
+ if (!this.executionLog || !this.logExecutionId)
1025
+ return;
1026
+ await this.executionLog.append({
1027
+ ...event,
1028
+ executionId: this.logExecutionId,
1029
+ });
1030
+ }
1031
+ /** Flush deferred sync checkpoints (called after a successful store / action). */
1032
+ async flushPendingCheckpoints(ctx) {
1033
+ const scope = this.scopeFor(ctx);
1034
+ const pending = scope.pendingCheckpoints;
1035
+ scope.pendingCheckpoints = [];
1036
+ for (const record of pending) {
1037
+ await record();
1038
+ }
1039
+ }
1040
+ async executeFor(step, actionName, ctx) {
602
1041
  const handler = new ForHandler({
603
- ctx: this.ctx,
1042
+ ctx,
604
1043
  log: (msg) => this.log(msg),
1044
+ emit: this.eventEmitter
1045
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1046
+ : undefined,
605
1047
  executeStep: (s, a, c) => this.executeStep(s, a, c),
606
1048
  actionName,
1049
+ debugController: this.debugController,
1050
+ captureDebugSnapshot: this.debugController
1051
+ ? (action, stepIndex, stepType, pauseReason, ctx) => this.captureDebugSnapshot(action, stepIndex, stepType, pauseReason, ctx)
1052
+ : undefined,
1053
+ handleDebugCommand: this.debugController
1054
+ ? (cmd) => this.handleDebugCommand(cmd)
1055
+ : undefined,
1056
+ checkPause: this.config.controlServer ? () => this.checkPause() : undefined,
1057
+ handleQueue: (signal) => this.handleQueue(signal),
607
1058
  });
608
1059
  await handler.execute(step);
609
1060
  }
610
- async executeMap(step) {
1061
+ async executeMap(step, ctx) {
611
1062
  const handler = new MapHandler({
612
- ctx: this.ctx,
1063
+ ctx,
613
1064
  log: (msg) => this.log(msg),
1065
+ emit: this.eventEmitter
1066
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1067
+ : undefined,
614
1068
  });
615
1069
  await handler.execute(step);
616
1070
  }
617
- async executeValidate(step) {
1071
+ async executeValidate(step, ctx) {
618
1072
  const handler = new ValidateHandler({
619
- ctx: this.ctx,
1073
+ ctx,
620
1074
  log: (msg) => this.log(msg),
1075
+ emit: this.eventEmitter
1076
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1077
+ : undefined,
621
1078
  });
622
1079
  await handler.execute(step);
623
1080
  }
624
- async executeStore(step) {
1081
+ async executeStore(step, ctx, stepId) {
1082
+ // Dry runs use synthetic fetch data that has no real keys; persisting it
1083
+ // would both write garbage and trip key validation. Skip the write but
1084
+ // still advance checkpoints so the dry run exercises the sync path.
1085
+ if (this.config.dryRun) {
1086
+ this.log(`[dry run] skipping store to ${step.target}`);
1087
+ await this.flushPendingCheckpoints(ctx);
1088
+ return;
1089
+ }
1090
+ // Step-level effect identity (attempt-independent): a store effect already
1091
+ // applied in the log — whether by a prior run we are resuming or an earlier
1092
+ // action attempt — must not be re-applied. This is the exactly-once-on-replay
1093
+ // guarantee for store writes.
1094
+ //
1095
+ // The identity is keyed on the *content* being written, not just the target.
1096
+ // A resumable backfill re-runs the same step (same stepId) once per run, each
1097
+ // run storing a different page; keying on target alone made every run after
1098
+ // the first collide on one id and skip the write, silently dropping every
1099
+ // page but the first. Hashing the resolved payload keeps re-storing the same
1100
+ // data idempotent (the upsert is a no-op anyway) while a different page is a
1101
+ // distinct effect that applies.
1102
+ const discriminator = `${step.target}::${this.storeContentHash(step, ctx)}`;
1103
+ const fx = stepId && this.logExecutionId
1104
+ ? effectId(this.logExecutionId, stepId, 0, 'store', discriminator)
1105
+ : undefined;
1106
+ if (fx && this.appliedEffects.has(fx)) {
1107
+ this.log(`Skipping already-applied store to ${step.target} (resume)`);
1108
+ await this.flushPendingCheckpoints(ctx);
1109
+ return;
1110
+ }
625
1111
  const handler = new StoreHandler({
626
- ctx: this.ctx,
1112
+ ctx,
627
1113
  log: (msg) => this.log(msg),
1114
+ emit: this.eventEmitter
1115
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1116
+ : undefined,
628
1117
  });
629
1118
  await handler.execute(step);
1119
+ // Record the effect as applied so replay/retry skips it.
1120
+ if (fx) {
1121
+ this.appliedEffects.add(fx);
1122
+ await this.logEvent({
1123
+ type: 'effect.applied',
1124
+ stepId: stepId,
1125
+ attempt: 0,
1126
+ effectType: 'store',
1127
+ effectId: fx,
1128
+ });
1129
+ }
1130
+ // Data is now durably stored — safe to advance any pending sync checkpoint.
1131
+ await this.flushPendingCheckpoints(ctx);
630
1132
  }
631
- async executeMatch(step, actionName) {
1133
+ /**
1134
+ * A stable hash of the payload a store step will write, used to make the
1135
+ * store-effect identity content-aware. Resolving the source is a pure read of
1136
+ * the context (the same value the handler stores); on any evaluation failure we
1137
+ * fall back to a constant so behaviour matches the old target-only identity
1138
+ * rather than throwing inside the dedup path.
1139
+ */
1140
+ /** Load a pause's checkpoint into the shape the pause step uses to resume. */
1141
+ async loadResumingPause(pauseId) {
1142
+ const resumed = await this.pauseStore?.load(pauseId);
1143
+ return resumed
1144
+ ? { pauseId, checkpoint: resumed.checkpoint, payload: resumed.webhookPayload }
1145
+ : undefined;
1146
+ }
1147
+ storeContentHash(step, ctx) {
1148
+ try {
1149
+ const source = evaluate(step.source, ctx);
1150
+ return createHash('sha1')
1151
+ .update(JSON.stringify(source) ?? 'null')
1152
+ .digest('hex');
1153
+ }
1154
+ catch {
1155
+ return 'unhashable';
1156
+ }
1157
+ }
1158
+ async executeMatch(step, actionName, ctx) {
632
1159
  const handler = new MatchHandler({
633
- ctx: this.ctx,
1160
+ ctx,
634
1161
  log: (msg) => this.log(msg),
1162
+ emit: this.eventEmitter
1163
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1164
+ : undefined,
635
1165
  executeStep: (s, a, c) => this.executeStep(s, a, c),
636
1166
  actionName,
1167
+ debugController: this.debugController,
1168
+ captureDebugSnapshot: this.debugController
1169
+ ? (action, stepIndex, stepType, pauseReason, execCtx) => this.captureDebugSnapshot(action, stepIndex, stepType, pauseReason, execCtx)
1170
+ : undefined,
1171
+ handleDebugCommand: this.debugController
1172
+ ? (cmd) => this.handleDebugCommand(cmd)
1173
+ : undefined,
637
1174
  });
638
1175
  await handler.execute(step);
639
1176
  // Flow control signals (SkipSignal, RetrySignal, etc.) will propagate up
640
1177
  }
641
- async executeLet(step) {
642
- const value = evaluate(step.value, this.ctx);
643
- setVariable(this.ctx, step.name, value);
644
- this.log(`Set variable '${step.name}' = ${JSON.stringify(value)}`);
1178
+ async executeLet(step, ctx) {
1179
+ const value = evaluate(step.value, ctx);
1180
+ setVariable(ctx, step.name, value);
1181
+ // Redact before logging: the value (or a nested field) may be a secret.
1182
+ this.log(`Set variable '${step.name}' = ${JSON.stringify(redactNamedValue(step.name, value))}`);
1183
+ }
1184
+ async executeApply(step, ctx) {
1185
+ const transform = this.transforms.get(step.transform);
1186
+ if (!transform) {
1187
+ throw new Error(`Transform '${step.transform}' not found`);
1188
+ }
1189
+ const handler = new ApplyHandler({
1190
+ ctx,
1191
+ log: (msg) => this.log(msg),
1192
+ transform,
1193
+ });
1194
+ await handler.execute(step);
645
1195
  }
646
- async executeWebhook(step) {
1196
+ async executeWebhook(step, ctx) {
647
1197
  if (!this.config.webhookServer) {
648
1198
  throw new Error('Webhook server not configured. Use --webhook flag or configure webhookServer in executor config.');
649
1199
  }
650
1200
  const handler = new WebhookHandler({
651
- ctx: this.ctx,
1201
+ ctx,
652
1202
  webhookServer: this.config.webhookServer,
653
1203
  executionId: this.executionState?.id ?? 'ephemeral',
654
1204
  log: (msg) => this.log(msg),
1205
+ emit: this.eventEmitter
1206
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1207
+ : undefined,
655
1208
  });
656
1209
  await handler.execute(step);
657
1210
  }
1211
+ async executePause(step, actionName, stepIndex, ctx) {
1212
+ if (!this.pauseManager) {
1213
+ throw new Error('Pause manager not configured');
1214
+ }
1215
+ // Resuming this very pause: don't pause again. Restore the captured
1216
+ // checkpoint (variables + response, plus any webhook payload) and fall
1217
+ // through so the steps after the pause run to completion.
1218
+ const resuming = this.resumingPause;
1219
+ if (resuming &&
1220
+ resuming.checkpoint.action === actionName &&
1221
+ resuming.checkpoint.stepIndex === stepIndex + 1) {
1222
+ this.resumingPause = undefined;
1223
+ for (const [key, value] of Object.entries(resuming.checkpoint.variables ?? {})) {
1224
+ ctx.variables.set(key, value);
1225
+ }
1226
+ ctx.response = resuming.payload ?? resuming.checkpoint.response;
1227
+ this.log(`Resuming past pause ${resuming.pauseId}`);
1228
+ return;
1229
+ }
1230
+ // Mark execution state as paused before creating pause
1231
+ if (this.executionState) {
1232
+ this.executionState.status = 'paused';
1233
+ await this.saveExecutionState();
1234
+ }
1235
+ const handler = new PauseHandler({
1236
+ ctx,
1237
+ log: (msg) => this.log(msg),
1238
+ emit: this.eventEmitter
1239
+ ? (type, payload) => this.eventEmitter.emit(type, payload)
1240
+ : undefined,
1241
+ pauseManager: this.pauseManager,
1242
+ // Anchor the pause to the durable log id so its pause.created lands under
1243
+ // the same execution the log replays on resume (executionState may be
1244
+ // absent when running without an execution store).
1245
+ executionId: this.logExecutionId ?? this.executionState?.id ?? 'ephemeral',
1246
+ mission: this.missionName ?? 'unknown',
1247
+ actionName,
1248
+ stageIndex: this.currentStageIndex,
1249
+ stepIndex,
1250
+ });
1251
+ // This will throw PauseSignal
1252
+ await handler.execute(step);
1253
+ }
658
1254
  log(message) {
659
- if (this.config.verbose) {
1255
+ if (this.logger) {
1256
+ this.logger.info(message);
1257
+ }
1258
+ else if (this.config.verbose) {
660
1259
  console.log(`[Reqon] ${message}`);
661
1260
  }
662
1261
  }
1262
+ /**
1263
+ * Check if pause has been requested and handle it
1264
+ * Should be called at safe pause points (between stages, loop iterations)
1265
+ */
1266
+ async checkPause() {
1267
+ if (!this.config.controlServer?.isPauseRequested()) {
1268
+ return;
1269
+ }
1270
+ this.log('Pause requested - saving state and pausing execution');
1271
+ // Save state as paused
1272
+ if (this.executionState) {
1273
+ this.executionState.status = 'paused';
1274
+ await this.saveExecutionState();
1275
+ }
1276
+ // Clear the pause request (it's been handled)
1277
+ this.config.controlServer.clearPauseRequest();
1278
+ // Throw pause signal to stop execution
1279
+ throw new PauseSignal();
1280
+ }
1281
+ /**
1282
+ * Update control server with current state
1283
+ */
1284
+ updateControlServerState() {
1285
+ if (this.config.controlServer && this.executionState) {
1286
+ this.config.controlServer.updateState(this.executionState);
1287
+ }
1288
+ }
1289
+ getStepType(stepType) {
1290
+ const mapping = {
1291
+ FetchStep: 'fetch',
1292
+ ForStep: 'for',
1293
+ MapStep: 'map',
1294
+ ValidateStep: 'validate',
1295
+ StoreStep: 'store',
1296
+ MatchStep: 'match',
1297
+ LetStep: 'let',
1298
+ WebhookStep: 'webhook',
1299
+ PauseStep: 'pause',
1300
+ };
1301
+ return mapping[stepType] ?? 'fetch';
1302
+ }
1303
+ /** Get the event emitter (for external access) */
1304
+ getEventEmitter() {
1305
+ return this.eventEmitter;
1306
+ }
1307
+ /** Get the structured logger (for external access) */
1308
+ getLogger() {
1309
+ return this.logger;
1310
+ }
1311
+ /** Get the debug controller (for external access) */
1312
+ getDebugController() {
1313
+ return this.debugController;
1314
+ }
1315
+ /** Capture current execution state for debugging */
1316
+ captureDebugSnapshot(action, stepIndex, stepType, pauseReason, ctx) {
1317
+ // Collect variables from context chain
1318
+ const variables = {};
1319
+ let current = ctx;
1320
+ while (current) {
1321
+ for (const [key, value] of current.variables) {
1322
+ if (!(key in variables)) {
1323
+ variables[key] = value;
1324
+ }
1325
+ }
1326
+ current = current.parent;
1327
+ }
1328
+ // Collect store info
1329
+ const stores = {};
1330
+ for (const [name, _store] of ctx.stores) {
1331
+ stores[name] = {
1332
+ type: ctx.storeTypes.get(name) ?? 'unknown',
1333
+ count: -1, // Would need async call to get count
1334
+ };
1335
+ }
1336
+ return {
1337
+ mission: this.missionName ?? 'unknown',
1338
+ action,
1339
+ stepIndex,
1340
+ stepType,
1341
+ pauseReason,
1342
+ variables,
1343
+ stores,
1344
+ response: ctx.response,
1345
+ };
1346
+ }
1347
+ /** Handle debug command and update state */
1348
+ handleDebugCommand(cmd) {
1349
+ if (!this.debugController)
1350
+ return;
1351
+ switch (cmd.type) {
1352
+ case 'abort':
1353
+ throw new AbortError('Execution aborted by debugger');
1354
+ case 'continue':
1355
+ this.debugController.mode = 'run';
1356
+ break;
1357
+ case 'step':
1358
+ this.debugController.mode = 'step';
1359
+ break;
1360
+ case 'step-into':
1361
+ this.debugController.mode = 'step-into';
1362
+ break;
1363
+ case 'step-over':
1364
+ this.debugController.mode = 'step-over';
1365
+ break;
1366
+ }
1367
+ }
663
1368
  }