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.
- package/CHANGELOG.md +7 -0
- package/README.md +45 -3
- package/dist/ast/nodes.d.ts +91 -4
- package/dist/ast/nodes.js +14 -0
- package/dist/auth/circuit-breaker.d.ts +11 -0
- package/dist/auth/circuit-breaker.js +90 -18
- package/dist/auth/credentials.d.ts +6 -1
- package/dist/auth/credentials.js +12 -4
- package/dist/auth/oauth2-provider.js +13 -3
- package/dist/auth/rate-limiter.d.ts +12 -1
- package/dist/auth/rate-limiter.js +39 -26
- package/dist/auth/token-store.js +8 -1
- package/dist/cli.d.ts +24 -1
- package/dist/cli.js +149 -10
- package/dist/config/constants.d.ts +152 -0
- package/dist/config/constants.js +139 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +4 -0
- package/dist/control/index.d.ts +2 -0
- package/dist/control/index.js +1 -0
- package/dist/control/server.d.ts +105 -0
- package/dist/control/server.js +315 -0
- package/dist/control/types.d.ts +61 -0
- package/dist/control/types.js +7 -0
- package/dist/debug/cli-debugger.d.ts +17 -0
- package/dist/debug/cli-debugger.js +185 -0
- package/dist/debug/controller.d.ts +94 -0
- package/dist/debug/controller.js +45 -0
- package/dist/debug/index.d.ts +6 -0
- package/dist/debug/index.js +5 -0
- package/dist/errors/index.d.ts +67 -0
- package/dist/errors/index.js +89 -1
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/state.d.ts +24 -0
- package/dist/execution/store.js +2 -2
- package/dist/execution-log/events.d.ts +125 -0
- package/dist/execution-log/events.js +17 -0
- package/dist/execution-log/fold.d.ts +38 -0
- package/dist/execution-log/fold.js +54 -0
- package/dist/execution-log/index.d.ts +18 -0
- package/dist/execution-log/index.js +6 -0
- package/dist/execution-log/postgres-store.d.ts +36 -0
- package/dist/execution-log/postgres-store.js +108 -0
- package/dist/execution-log/resume.d.ts +11 -0
- package/dist/execution-log/resume.js +5 -0
- package/dist/execution-log/sqlite-store.d.ts +16 -0
- package/dist/execution-log/sqlite-store.js +101 -0
- package/dist/execution-log/store.d.ts +72 -0
- package/dist/execution-log/store.js +182 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +35 -3
- package/dist/interpreter/context.d.ts +29 -0
- package/dist/interpreter/context.js +18 -0
- package/dist/interpreter/evaluator.d.ts +63 -1
- package/dist/interpreter/evaluator.js +219 -42
- package/dist/interpreter/executor.d.ts +132 -14
- package/dist/interpreter/executor.js +883 -178
- package/dist/interpreter/fetch-handler.d.ts +48 -1
- package/dist/interpreter/fetch-handler.js +216 -38
- package/dist/interpreter/http.d.ts +34 -0
- package/dist/interpreter/http.js +203 -28
- package/dist/interpreter/index.d.ts +5 -3
- package/dist/interpreter/index.js +4 -2
- package/dist/interpreter/pagination.d.ts +12 -3
- package/dist/interpreter/pagination.js +102 -32
- package/dist/interpreter/signals.d.ts +8 -0
- package/dist/interpreter/signals.js +12 -0
- package/dist/interpreter/source-manager.d.ts +75 -0
- package/dist/interpreter/source-manager.js +157 -0
- package/dist/interpreter/step-handlers/apply-handler.d.ts +29 -0
- package/dist/interpreter/step-handlers/apply-handler.js +79 -0
- package/dist/interpreter/step-handlers/for-handler.d.ts +16 -0
- package/dist/interpreter/step-handlers/for-handler.js +89 -7
- package/dist/interpreter/step-handlers/index.d.ts +4 -2
- package/dist/interpreter/step-handlers/index.js +4 -2
- package/dist/interpreter/step-handlers/match-handler.d.ts +9 -0
- package/dist/interpreter/step-handlers/match-handler.js +47 -17
- package/dist/interpreter/step-handlers/pause-handler.d.ts +52 -0
- package/dist/interpreter/step-handlers/pause-handler.js +87 -0
- package/dist/interpreter/step-handlers/store-handler.d.ts +17 -1
- package/dist/interpreter/step-handlers/store-handler.js +61 -20
- package/dist/interpreter/step-handlers/types.d.ts +3 -0
- package/dist/interpreter/step-handlers/validate-handler.d.ts +2 -1
- package/dist/interpreter/step-handlers/validate-handler.js +7 -2
- package/dist/interpreter/step-handlers/webhook-handler.d.ts +4 -0
- package/dist/interpreter/step-handlers/webhook-handler.js +31 -5
- package/dist/interpreter/store-manager.d.ts +46 -0
- package/dist/interpreter/store-manager.js +70 -0
- package/dist/lexer/index.d.ts +11 -4
- package/dist/lexer/index.js +11 -4
- package/dist/lexer/tokens.d.ts +17 -1
- package/dist/lexer/tokens.js +36 -0
- package/dist/loader/index.js +5 -8
- package/dist/mcp/index.d.ts +11 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/sandbox.d.ts +41 -0
- package/dist/mcp/sandbox.js +76 -0
- package/dist/mcp/server.d.ts +17 -0
- package/dist/mcp/server.js +504 -0
- package/dist/oas/index.d.ts +2 -0
- package/dist/oas/index.js +1 -0
- package/dist/oas/loader.d.ts +13 -1
- package/dist/oas/loader.js +25 -3
- package/dist/oas/mock-generator.d.ts +12 -0
- package/dist/oas/mock-generator.js +196 -0
- package/dist/oas/validator.js +45 -5
- package/dist/observability/events.d.ts +248 -0
- package/dist/observability/events.js +85 -0
- package/dist/observability/index.d.ts +15 -0
- package/dist/observability/index.js +12 -0
- package/dist/observability/logger.d.ts +106 -0
- package/dist/observability/logger.js +266 -0
- package/dist/observability/otel.d.ts +143 -0
- package/dist/observability/otel.js +421 -0
- package/dist/parser/action-parser.d.ts +105 -0
- package/dist/parser/action-parser.js +645 -0
- package/dist/parser/base.d.ts +7 -0
- package/dist/parser/base.js +11 -0
- package/dist/parser/expressions.d.ts +14 -0
- package/dist/parser/expressions.js +89 -6
- package/dist/parser/fetch-parser.d.ts +27 -0
- package/dist/parser/fetch-parser.js +280 -0
- package/dist/parser/index.d.ts +17 -0
- package/dist/parser/index.js +17 -0
- package/dist/parser/parser.d.ts +44 -46
- package/dist/parser/parser.js +122 -1070
- package/dist/parser/pipeline-parser.d.ts +12 -0
- package/dist/parser/pipeline-parser.js +52 -0
- package/dist/parser/schedule-parser.d.ts +7 -0
- package/dist/parser/schedule-parser.js +137 -0
- package/dist/parser/source-parser.d.ts +9 -0
- package/dist/parser/source-parser.js +151 -0
- package/dist/pause/index.d.ts +15 -0
- package/dist/pause/index.js +12 -0
- package/dist/pause/log-store.d.ts +33 -0
- package/dist/pause/log-store.js +98 -0
- package/dist/pause/manager.d.ts +130 -0
- package/dist/pause/manager.js +294 -0
- package/dist/pause/state.d.ts +93 -0
- package/dist/pause/state.js +103 -0
- package/dist/pause/store.d.ts +61 -0
- package/dist/pause/store.js +158 -0
- package/dist/plugin.d.ts +9 -12
- package/dist/plugin.js +10 -13
- package/dist/scheduler/cron-parser.d.ts +10 -3
- package/dist/scheduler/cron-parser.js +227 -48
- package/dist/scheduler/scheduler.js +56 -22
- package/dist/stores/factory.d.ts +7 -1
- package/dist/stores/factory.js +14 -3
- package/dist/stores/file.d.ts +26 -0
- package/dist/stores/file.js +67 -21
- package/dist/stores/index.d.ts +16 -1
- package/dist/stores/index.js +16 -1
- package/dist/stores/memory.d.ts +4 -0
- package/dist/stores/memory.js +8 -6
- package/dist/stores/postgrest.d.ts +28 -0
- package/dist/stores/postgrest.js +84 -37
- package/dist/stores/types.d.ts +17 -0
- package/dist/stores/types.js +12 -0
- package/dist/sync/index.d.ts +3 -2
- package/dist/sync/index.js +2 -1
- package/dist/sync/log-store.d.ts +30 -0
- package/dist/sync/log-store.js +45 -0
- package/dist/sync/store.js +1 -1
- package/dist/trace/index.d.ts +18 -0
- package/dist/trace/index.js +13 -0
- package/dist/trace/log-view.d.ts +57 -0
- package/dist/trace/log-view.js +76 -0
- package/dist/trace/recorder.d.ts +75 -0
- package/dist/trace/recorder.js +157 -0
- package/dist/trace/replay.d.ts +132 -0
- package/dist/trace/replay.js +264 -0
- package/dist/trace/state.d.ts +102 -0
- package/dist/trace/state.js +86 -0
- package/dist/trace/store.d.ts +75 -0
- package/dist/trace/store.js +250 -0
- package/dist/utils/deep-merge.d.ts +10 -0
- package/dist/utils/deep-merge.js +23 -0
- package/dist/utils/file.d.ts +13 -4
- package/dist/utils/file.js +70 -12
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/long-timeout.d.ts +19 -0
- package/dist/utils/long-timeout.js +33 -0
- package/dist/utils/path.d.ts +22 -1
- package/dist/utils/path.js +46 -1
- package/dist/utils/redact.d.ts +22 -0
- package/dist/utils/redact.js +42 -0
- package/dist/utils/type-guards.d.ts +58 -0
- package/dist/utils/type-guards.js +92 -0
- package/dist/webhook/server.d.ts +9 -0
- package/dist/webhook/server.js +122 -36
- package/dist/webhook/types.d.ts +9 -1
- package/package.json +76 -9
- package/.claude/settings.local.json +0 -31
- package/.claude/skills/api-integration.md +0 -125
- package/.claude/skills/database-schema.md +0 -51
- package/.claude/skills/dsl-design.md +0 -80
- package/.claude/skills/property-testing.md +0 -143
- package/.claude/skills/reqon/SKILL.md +0 -44
- package/.claude/skills/reqon/references/examples.md +0 -206
- package/.claude/skills/reqon/references/syntax.md +0 -263
- package/.claude/skills/vscode-extension.md +0 -113
- package/.github/dependabot.yml +0 -32
- package/.github/pull_request_template.md +0 -21
- package/.github/workflows/ci.yml +0 -174
- package/.github/workflows/release.yml +0 -73
- package/CLAUDE.md +0 -72
- package/CONTRIBUTING.md +0 -161
- package/TODO.md +0 -51
- package/dist/auth/auth.test.d.ts +0 -1
- package/dist/auth/auth.test.js +0 -255
- package/dist/errors/errors.test.d.ts +0 -1
- package/dist/errors/errors.test.js +0 -165
- package/dist/execution/execution.test.d.ts +0 -1
- package/dist/execution/execution.test.js +0 -246
- package/dist/integration.test.d.ts +0 -1
- package/dist/integration.test.js +0 -168
- package/dist/interpreter/evaluator.test.d.ts +0 -1
- package/dist/interpreter/evaluator.test.js +0 -512
- package/dist/interpreter/http.test.d.ts +0 -1
- package/dist/interpreter/http.test.js +0 -299
- package/dist/interpreter/progress.test.d.ts +0 -1
- package/dist/interpreter/progress.test.js +0 -216
- package/dist/interpreter/schema-matcher.test.d.ts +0 -1
- package/dist/interpreter/schema-matcher.test.js +0 -122
- package/dist/lexer/lexer.d.ts +0 -24
- package/dist/lexer/lexer.js +0 -264
- package/dist/lexer/lexer.test.d.ts +0 -1
- package/dist/lexer/lexer.test.js +0 -259
- package/dist/loader/loader.test.d.ts +0 -1
- package/dist/loader/loader.test.js +0 -287
- package/dist/oas/oas.test.d.ts +0 -1
- package/dist/oas/oas.test.js +0 -218
- package/dist/parser/expressions.test.d.ts +0 -1
- package/dist/parser/expressions.test.js +0 -378
- package/dist/parser/match.test.d.ts +0 -1
- package/dist/parser/match.test.js +0 -254
- package/dist/parser/parser.test.d.ts +0 -1
- package/dist/parser/parser.test.js +0 -333
- package/dist/parser/schedule.test.d.ts +0 -1
- package/dist/parser/schedule.test.js +0 -241
- package/dist/scheduler/cron-parser.test.d.ts +0 -1
- package/dist/scheduler/cron-parser.test.js +0 -188
- package/dist/stores/file.test.d.ts +0 -1
- package/dist/stores/file.test.js +0 -165
- package/dist/stores/memory.test.d.ts +0 -1
- package/dist/stores/memory.test.js +0 -157
- package/dist/stores/stores.test.d.ts +0 -1
- package/dist/stores/stores.test.js +0 -158
- package/dist/sync/sync.test.d.ts +0 -1
- package/dist/sync/sync.test.js +0 -221
- package/docusaurus/README.md +0 -41
- package/docusaurus/docs/advanced/execution-state.md +0 -283
- package/docusaurus/docs/advanced/extending-reqon.md +0 -388
- package/docusaurus/docs/advanced/multi-file-missions.md +0 -250
- package/docusaurus/docs/advanced/parallel-execution.md +0 -353
- package/docusaurus/docs/api-reference.md +0 -443
- package/docusaurus/docs/authentication/api-key.md +0 -339
- package/docusaurus/docs/authentication/basic.md +0 -276
- package/docusaurus/docs/authentication/bearer.md +0 -282
- package/docusaurus/docs/authentication/oauth2.md +0 -317
- package/docusaurus/docs/authentication/overview.md +0 -251
- package/docusaurus/docs/cli.md +0 -229
- package/docusaurus/docs/core-concepts/actions.md +0 -286
- package/docusaurus/docs/core-concepts/missions.md +0 -264
- package/docusaurus/docs/core-concepts/schemas.md +0 -353
- package/docusaurus/docs/core-concepts/sources.md +0 -339
- package/docusaurus/docs/core-concepts/stores.md +0 -332
- package/docusaurus/docs/dsl-syntax/expressions.md +0 -361
- package/docusaurus/docs/dsl-syntax/fetch.md +0 -293
- package/docusaurus/docs/dsl-syntax/for-loops.md +0 -324
- package/docusaurus/docs/dsl-syntax/map.md +0 -345
- package/docusaurus/docs/dsl-syntax/match.md +0 -387
- package/docusaurus/docs/dsl-syntax/pipelines.md +0 -397
- package/docusaurus/docs/dsl-syntax/validate.md +0 -401
- package/docusaurus/docs/error-handling/dead-letter-queues.md +0 -399
- package/docusaurus/docs/error-handling/flow-control.md +0 -337
- package/docusaurus/docs/error-handling/retry-strategies.md +0 -368
- package/docusaurus/docs/examples.md +0 -488
- package/docusaurus/docs/getting-started.md +0 -256
- package/docusaurus/docs/http/circuit-breaker.md +0 -401
- package/docusaurus/docs/http/incremental-sync.md +0 -394
- package/docusaurus/docs/http/pagination.md +0 -361
- package/docusaurus/docs/http/rate-limiting.md +0 -383
- package/docusaurus/docs/http/requests.md +0 -328
- package/docusaurus/docs/http/retry.md +0 -402
- package/docusaurus/docs/intro.md +0 -90
- package/docusaurus/docs/openapi/loading-specs.md +0 -305
- package/docusaurus/docs/openapi/operation-calls.md +0 -314
- package/docusaurus/docs/openapi/overview.md +0 -212
- package/docusaurus/docs/openapi/response-validation.md +0 -344
- package/docusaurus/docs/scheduling/cron.md +0 -305
- package/docusaurus/docs/scheduling/daemon-mode.md +0 -317
- package/docusaurus/docs/scheduling/intervals.md +0 -289
- package/docusaurus/docs/scheduling/overview.md +0 -231
- package/docusaurus/docs/stores/custom-adapters.md +0 -376
- package/docusaurus/docs/stores/file.md +0 -236
- package/docusaurus/docs/stores/memory.md +0 -193
- package/docusaurus/docs/stores/overview.md +0 -274
- package/docusaurus/docs/stores/postgrest.md +0 -316
- package/docusaurus/docusaurus.config.ts +0 -148
- package/docusaurus/package-lock.json +0 -18029
- package/docusaurus/package.json +0 -47
- package/docusaurus/sidebars.ts +0 -155
- package/docusaurus/src/components/HomepageFeatures/index.tsx +0 -105
- package/docusaurus/src/components/HomepageFeatures/styles.module.css +0 -12
- package/docusaurus/src/css/custom.css +0 -169
- package/docusaurus/src/pages/index.module.css +0 -48
- package/docusaurus/src/pages/index.tsx +0 -110
- package/docusaurus/src/pages/markdown-page.md +0 -7
- package/docusaurus/static/.nojekyll +0 -0
- package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
- package/docusaurus/static/img/docusaurus.png +0 -0
- package/docusaurus/static/img/favicon.ico +0 -0
- package/docusaurus/static/img/logo.svg +0 -10
- package/docusaurus/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docusaurus/static/img/undraw_docusaurus_react.svg +0 -170
- package/docusaurus/static/img/undraw_docusaurus_tree.svg +0 -40
- package/docusaurus/tsconfig.json +0 -8
- package/examples/README.md +0 -112
- package/examples/error-handling/README.md +0 -150
- package/examples/error-handling/payment-processor.vague +0 -287
- package/examples/github-sync/README.md +0 -74
- package/examples/github-sync/fetch-issues.vague +0 -47
- package/examples/github-sync/fetch-prs.vague +0 -40
- package/examples/github-sync/mission.vague +0 -101
- package/examples/github-sync/normalize.vague +0 -70
- package/examples/jsonplaceholder/README.md +0 -28
- package/examples/jsonplaceholder/posts.vague +0 -48
- package/examples/petstore/README.md +0 -35
- package/examples/petstore/openapi.yaml +0 -97
- package/examples/petstore/sync.vague +0 -52
- package/examples/temporal-comparison/README.md +0 -297
- package/examples/temporal-comparison/reconciliation.vague +0 -355
- package/examples/temporal-comparison/temporal/activities/index.ts +0 -8
- package/examples/temporal-comparison/temporal/activities/shipstation.ts +0 -225
- package/examples/temporal-comparison/temporal/activities/shopify.ts +0 -257
- package/examples/temporal-comparison/temporal/activities/storage.ts +0 -198
- package/examples/temporal-comparison/temporal/activities/stripe.ts +0 -169
- package/examples/temporal-comparison/temporal/activities/validation.ts +0 -205
- package/examples/temporal-comparison/temporal/client/schedule.ts +0 -218
- package/examples/temporal-comparison/temporal/config/retry.ts +0 -63
- package/examples/temporal-comparison/temporal/types/index.ts +0 -129
- package/examples/temporal-comparison/temporal/workers/main.ts +0 -130
- package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +0 -262
- package/examples/xero/README.md +0 -88
- package/examples/xero/invoices.vague +0 -189
- package/src/api-integration.test.ts +0 -954
- package/src/ast/index.ts +0 -1
- package/src/ast/nodes.ts +0 -310
- package/src/auth/auth.test.ts +0 -326
- package/src/auth/circuit-breaker.test.ts +0 -390
- package/src/auth/circuit-breaker.ts +0 -379
- package/src/auth/credentials.test.ts +0 -273
- package/src/auth/credentials.ts +0 -246
- package/src/auth/index.ts +0 -40
- package/src/auth/oauth2-provider.ts +0 -177
- package/src/auth/rate-limiter.ts +0 -459
- package/src/auth/token-store.ts +0 -177
- package/src/auth/types.ts +0 -159
- package/src/benchmark/e2e.bench.ts +0 -288
- package/src/benchmark/evaluator.bench.ts +0 -331
- package/src/benchmark/fixtures.ts +0 -295
- package/src/benchmark/index.ts +0 -108
- package/src/benchmark/lexer.bench.ts +0 -69
- package/src/benchmark/parser.bench.ts +0 -103
- package/src/benchmark/resilience.bench.ts +0 -193
- package/src/benchmark/store.bench.ts +0 -147
- package/src/benchmark/utils.ts +0 -230
- package/src/cli.ts +0 -313
- package/src/errors/errors.test.ts +0 -234
- package/src/errors/index.ts +0 -223
- package/src/execution/execution.test.ts +0 -307
- package/src/execution/index.ts +0 -21
- package/src/execution/state.ts +0 -207
- package/src/execution/store.ts +0 -188
- package/src/index.ts +0 -169
- package/src/integration.test.ts +0 -192
- package/src/interpreter/context.ts +0 -57
- package/src/interpreter/evaluator.test.ts +0 -796
- package/src/interpreter/evaluator.ts +0 -245
- package/src/interpreter/executor.ts +0 -946
- package/src/interpreter/fetch-handler.ts +0 -302
- package/src/interpreter/http.test.ts +0 -423
- package/src/interpreter/http.ts +0 -308
- package/src/interpreter/index.ts +0 -32
- package/src/interpreter/pagination.ts +0 -207
- package/src/interpreter/progress.test.ts +0 -276
- package/src/interpreter/schema-matcher.test.ts +0 -160
- package/src/interpreter/schema-matcher.ts +0 -168
- package/src/interpreter/signals.ts +0 -73
- package/src/interpreter/step-handlers/for-handler.ts +0 -65
- package/src/interpreter/step-handlers/index.ts +0 -17
- package/src/interpreter/step-handlers/map-handler.ts +0 -24
- package/src/interpreter/step-handlers/match-handler.ts +0 -101
- package/src/interpreter/step-handlers/store-handler.ts +0 -78
- package/src/interpreter/step-handlers/types.ts +0 -17
- package/src/interpreter/step-handlers/validate-handler.ts +0 -30
- package/src/interpreter/step-handlers/webhook-handler.ts +0 -142
- package/src/lexer/index.ts +0 -18
- package/src/lexer/lexer.test.ts +0 -316
- package/src/lexer/tokens.ts +0 -179
- package/src/loader/index.ts +0 -288
- package/src/loader/loader.test.ts +0 -360
- package/src/oas/index.ts +0 -4
- package/src/oas/loader.ts +0 -126
- package/src/oas/oas.test.ts +0 -254
- package/src/oas/validator.ts +0 -299
- package/src/parser/base.ts +0 -124
- package/src/parser/expressions.test.ts +0 -525
- package/src/parser/expressions.ts +0 -314
- package/src/parser/index.ts +0 -3
- package/src/parser/match.test.ts +0 -296
- package/src/parser/parser.test.ts +0 -739
- package/src/parser/parser.ts +0 -1469
- package/src/parser/schedule.test.ts +0 -287
- package/src/parser/webhook.test.ts +0 -248
- package/src/plugin.ts +0 -83
- package/src/scheduler/cron-parser.test.ts +0 -236
- package/src/scheduler/cron-parser.ts +0 -236
- package/src/scheduler/index.ts +0 -10
- package/src/scheduler/scheduler.ts +0 -443
- package/src/scheduler/types.ts +0 -71
- package/src/stores/factory.ts +0 -104
- package/src/stores/file.test.ts +0 -276
- package/src/stores/file.ts +0 -211
- package/src/stores/index.ts +0 -6
- package/src/stores/memory.test.ts +0 -238
- package/src/stores/memory.ts +0 -63
- package/src/stores/postgrest.test.ts +0 -488
- package/src/stores/postgrest.ts +0 -263
- package/src/stores/stores.test.ts +0 -197
- package/src/stores/types.ts +0 -58
- package/src/sync/index.ts +0 -16
- package/src/sync/state.ts +0 -126
- package/src/sync/store.ts +0 -139
- package/src/sync/sync.test.ts +0 -271
- package/src/utils/async.ts +0 -10
- package/src/utils/file.ts +0 -106
- package/src/utils/index.ts +0 -14
- package/src/utils/logger.ts +0 -53
- package/src/utils/path.ts +0 -47
- package/src/webhook/index.ts +0 -15
- package/src/webhook/server.test.ts +0 -253
- package/src/webhook/server.ts +0 -389
- package/src/webhook/store.ts +0 -239
- package/src/webhook/types.ts +0 -93
- package/tsconfig.json +0 -17
- 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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
586
|
-
oasSources: this.
|
|
587
|
-
sourceConfigs: this.
|
|
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
|
-
|
|
596
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
643
|
-
setVariable(
|
|
644
|
-
|
|
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
|
|
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.
|
|
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
|
}
|