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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pause Store - Persistence for pause states
|
|
3
|
+
*
|
|
4
|
+
* Stores pause state for resource-free long pauses,
|
|
5
|
+
* enabling resumption after extended periods.
|
|
6
|
+
*/
|
|
7
|
+
import { safeJoin } from '../utils/path.js';
|
|
8
|
+
import { ensureDirectory, writeJsonFile, readJsonFile, listFiles, deleteFile, restoreDates, } from '../utils/file.js';
|
|
9
|
+
/**
|
|
10
|
+
* File-based pause store
|
|
11
|
+
*/
|
|
12
|
+
export class FilePauseStore {
|
|
13
|
+
baseDir;
|
|
14
|
+
initialized;
|
|
15
|
+
constructor(baseDir = '.reqon-data/pauses') {
|
|
16
|
+
this.baseDir = baseDir;
|
|
17
|
+
this.initialized = ensureDirectory(this.baseDir);
|
|
18
|
+
}
|
|
19
|
+
getFilePath(id) {
|
|
20
|
+
return safeJoin(this.baseDir, `${id}.json`);
|
|
21
|
+
}
|
|
22
|
+
deserialize(parsed) {
|
|
23
|
+
restoreDates(parsed, ['pausedAt', 'expiresAt', 'resumedAt']);
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
async save(pause) {
|
|
27
|
+
await this.initialized;
|
|
28
|
+
// Pause files hold the captured variables needed to resume (so they can't
|
|
29
|
+
// be redacted); write owner-only (0o600) so they aren't world-readable.
|
|
30
|
+
await writeJsonFile(this.getFilePath(pause.id), pause, true, 0o600);
|
|
31
|
+
}
|
|
32
|
+
async load(id) {
|
|
33
|
+
await this.initialized;
|
|
34
|
+
const parsed = await readJsonFile(this.getFilePath(id));
|
|
35
|
+
return parsed ? this.deserialize(parsed) : null;
|
|
36
|
+
}
|
|
37
|
+
async loadByExecution(executionId) {
|
|
38
|
+
const all = await this.listActive();
|
|
39
|
+
return all.find((p) => p.executionId === executionId) ?? null;
|
|
40
|
+
}
|
|
41
|
+
async listActive() {
|
|
42
|
+
await this.initialized;
|
|
43
|
+
const files = await listFiles(this.baseDir, '.json');
|
|
44
|
+
const pauses = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const parsed = await readJsonFile(file);
|
|
47
|
+
if (parsed) {
|
|
48
|
+
const pause = this.deserialize(parsed);
|
|
49
|
+
if (pause.status === 'waiting') {
|
|
50
|
+
pauses.push(pause);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return pauses.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());
|
|
55
|
+
}
|
|
56
|
+
async listByMission(mission) {
|
|
57
|
+
await this.initialized;
|
|
58
|
+
const files = await listFiles(this.baseDir, '.json');
|
|
59
|
+
const pauses = [];
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const parsed = await readJsonFile(file);
|
|
62
|
+
if (parsed) {
|
|
63
|
+
const pause = this.deserialize(parsed);
|
|
64
|
+
if (pause.mission === mission) {
|
|
65
|
+
pauses.push(pause);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return pauses.sort((a, b) => b.pausedAt.getTime() - a.pausedAt.getTime());
|
|
70
|
+
}
|
|
71
|
+
async delete(id) {
|
|
72
|
+
await this.initialized;
|
|
73
|
+
await deleteFile(this.getFilePath(id));
|
|
74
|
+
}
|
|
75
|
+
async findExpired() {
|
|
76
|
+
const active = await this.listActive();
|
|
77
|
+
const now = new Date();
|
|
78
|
+
return active.filter((p) => p.expiresAt <= now);
|
|
79
|
+
}
|
|
80
|
+
async update(id, updates) {
|
|
81
|
+
const pause = await this.load(id);
|
|
82
|
+
if (!pause) {
|
|
83
|
+
throw new Error(`Pause not found: ${id}`);
|
|
84
|
+
}
|
|
85
|
+
const updated = { ...pause, ...updates };
|
|
86
|
+
await this.save(updated);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* In-memory pause store (for testing)
|
|
91
|
+
*/
|
|
92
|
+
export class MemoryPauseStore {
|
|
93
|
+
pauses = new Map();
|
|
94
|
+
async save(pause) {
|
|
95
|
+
this.pauses.set(pause.id, JSON.parse(JSON.stringify(pause)));
|
|
96
|
+
}
|
|
97
|
+
async load(id) {
|
|
98
|
+
const pause = this.pauses.get(id);
|
|
99
|
+
if (!pause)
|
|
100
|
+
return null;
|
|
101
|
+
const restored = JSON.parse(JSON.stringify(pause));
|
|
102
|
+
restored.pausedAt = new Date(restored.pausedAt);
|
|
103
|
+
restored.expiresAt = new Date(restored.expiresAt);
|
|
104
|
+
if (restored.resumedAt) {
|
|
105
|
+
restored.resumedAt = new Date(restored.resumedAt);
|
|
106
|
+
}
|
|
107
|
+
return restored;
|
|
108
|
+
}
|
|
109
|
+
async loadByExecution(executionId) {
|
|
110
|
+
for (const pause of this.pauses.values()) {
|
|
111
|
+
if (pause.executionId === executionId && pause.status === 'waiting') {
|
|
112
|
+
return this.load(pause.id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
async listActive() {
|
|
118
|
+
const pauses = [];
|
|
119
|
+
for (const pause of this.pauses.values()) {
|
|
120
|
+
if (pause.status === 'waiting') {
|
|
121
|
+
const loaded = await this.load(pause.id);
|
|
122
|
+
if (loaded)
|
|
123
|
+
pauses.push(loaded);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return pauses.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());
|
|
127
|
+
}
|
|
128
|
+
async listByMission(mission) {
|
|
129
|
+
const pauses = [];
|
|
130
|
+
for (const pause of this.pauses.values()) {
|
|
131
|
+
if (pause.mission === mission) {
|
|
132
|
+
const loaded = await this.load(pause.id);
|
|
133
|
+
if (loaded)
|
|
134
|
+
pauses.push(loaded);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return pauses.sort((a, b) => b.pausedAt.getTime() - a.pausedAt.getTime());
|
|
138
|
+
}
|
|
139
|
+
async delete(id) {
|
|
140
|
+
this.pauses.delete(id);
|
|
141
|
+
}
|
|
142
|
+
async findExpired() {
|
|
143
|
+
const active = await this.listActive();
|
|
144
|
+
const now = new Date();
|
|
145
|
+
return active.filter((p) => p.expiresAt <= now);
|
|
146
|
+
}
|
|
147
|
+
async update(id, updates) {
|
|
148
|
+
const pause = await this.load(id);
|
|
149
|
+
if (!pause) {
|
|
150
|
+
throw new Error(`Pause not found: ${id}`);
|
|
151
|
+
}
|
|
152
|
+
const updated = { ...pause, ...updates };
|
|
153
|
+
await this.save(updated);
|
|
154
|
+
}
|
|
155
|
+
clear() {
|
|
156
|
+
this.pauses.clear();
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Or simply import the plugin module to auto-register:
|
|
13
|
-
* import 'reqon/plugin';
|
|
2
|
+
* ---
|
|
3
|
+
* purpose: Vague plugin - registers Reqon keywords with Vague's lexer
|
|
4
|
+
* exports:
|
|
5
|
+
* - reqonPlugin - plugin object for manual registration
|
|
6
|
+
* - registerReqonPlugin, unregisterReqonPlugin - registration helpers
|
|
7
|
+
* related:
|
|
8
|
+
* - ./lexer/tokens.ts - REQON_KEYWORDS map
|
|
9
|
+
* - vague-lang - parent DSL framework
|
|
10
|
+
* ---
|
|
14
11
|
*/
|
|
15
12
|
import { type VaguePlugin } from 'vague-lang';
|
|
16
13
|
/**
|
package/dist/plugin.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Or simply import the plugin module to auto-register:
|
|
13
|
-
* import 'reqon/plugin';
|
|
2
|
+
* ---
|
|
3
|
+
* purpose: Vague plugin - registers Reqon keywords with Vague's lexer
|
|
4
|
+
* exports:
|
|
5
|
+
* - reqonPlugin - plugin object for manual registration
|
|
6
|
+
* - registerReqonPlugin, unregisterReqonPlugin - registration helpers
|
|
7
|
+
* related:
|
|
8
|
+
* - ./lexer/tokens.ts - REQON_KEYWORDS map
|
|
9
|
+
* - vague-lang - parent DSL framework
|
|
10
|
+
* ---
|
|
14
11
|
*/
|
|
15
|
-
import { registerPlugin, unregisterPlugin
|
|
12
|
+
import { registerPlugin, unregisterPlugin } from 'vague-lang';
|
|
16
13
|
import { REQON_KEYWORDS } from './lexer/tokens.js';
|
|
17
14
|
/**
|
|
18
15
|
* Convert REQON_KEYWORDS map to PluginKeyword array for Vague plugin system.
|
|
@@ -3,7 +3,7 @@ import type { ScheduleDefinition, IntervalSchedule } from '../ast/nodes.js';
|
|
|
3
3
|
* Parse a cron expression and calculate the next run time
|
|
4
4
|
*
|
|
5
5
|
* Cron format: "minute hour day-of-month month day-of-week"
|
|
6
|
-
* Supports: numbers, ranges (1-5), steps (
|
|
6
|
+
* Supports: numbers, ranges (1-5), steps (* /5), lists (1,3,5), and wildcards (*)
|
|
7
7
|
*/
|
|
8
8
|
export declare function parseCronExpression(expression: string): CronSchedule;
|
|
9
9
|
interface CronSchedule {
|
|
@@ -12,11 +12,18 @@ interface CronSchedule {
|
|
|
12
12
|
dayOfMonth: number[];
|
|
13
13
|
month: number[];
|
|
14
14
|
dayOfWeek: number[];
|
|
15
|
+
/** True when day-of-month is not `*` (used for POSIX OR matching). */
|
|
16
|
+
dayOfMonthRestricted: boolean;
|
|
17
|
+
/** True when day-of-week is not `*` (used for POSIX OR matching). */
|
|
18
|
+
dayOfWeekRestricted: boolean;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
|
-
* Calculate the next run time for a cron schedule
|
|
21
|
+
* Calculate the next run time for a cron schedule, evaluated against
|
|
22
|
+
* wall-clock time in `timeZone` (default UTC, so DST never shifts the result).
|
|
23
|
+
* Day-of-month and day-of-week follow POSIX: when both are restricted the
|
|
24
|
+
* match is their union (OR), not their intersection.
|
|
18
25
|
*/
|
|
19
|
-
export declare function getNextCronRun(schedule: CronSchedule, after?: Date): Date;
|
|
26
|
+
export declare function getNextCronRun(schedule: CronSchedule, after?: Date, timeZone?: string): Date;
|
|
20
27
|
/**
|
|
21
28
|
* Convert interval schedule to milliseconds
|
|
22
29
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Parse a cron expression and calculate the next run time
|
|
3
3
|
*
|
|
4
4
|
* Cron format: "minute hour day-of-month month day-of-week"
|
|
5
|
-
* Supports: numbers, ranges (1-5), steps (
|
|
5
|
+
* Supports: numbers, ranges (1-5), steps (* /5), lists (1,3,5), and wildcards (*)
|
|
6
6
|
*/
|
|
7
7
|
export function parseCronExpression(expression) {
|
|
8
8
|
const parts = expression.trim().split(/\s+/);
|
|
@@ -14,11 +14,29 @@ export function parseCronExpression(expression) {
|
|
|
14
14
|
hour: parseField(parts[1], 0, 23),
|
|
15
15
|
dayOfMonth: parseField(parts[2], 1, 31),
|
|
16
16
|
month: parseField(parts[3], 1, 12),
|
|
17
|
-
dayOfWeek: parseField(parts[4], 0, 6), // 0 = Sunday
|
|
17
|
+
dayOfWeek: parseField(parts[4], 0, 6, true), // 0 = Sunday; 7 also = Sunday
|
|
18
|
+
// POSIX day matching keys off whether each field is a literal wildcard.
|
|
19
|
+
dayOfMonthRestricted: parts[2] !== '*',
|
|
20
|
+
dayOfWeekRestricted: parts[4] !== '*',
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Parse one cron field into the explicit set of integers it matches.
|
|
25
|
+
*
|
|
26
|
+
* Validation is strict and fails fast (rather than silently yielding an empty
|
|
27
|
+
* set or, worse, hanging): steps must be integers >= 1, bounds must be numeric,
|
|
28
|
+
* and ranges must not run backwards. When `allowSundaySeven` is set (day-of-week
|
|
29
|
+
* only) a literal 7 is accepted and normalised to 0 (Sunday).
|
|
30
|
+
*/
|
|
31
|
+
function parseField(field, min, max, allowSundaySeven = false) {
|
|
21
32
|
const values = new Set();
|
|
33
|
+
const toInt = (token, label) => {
|
|
34
|
+
const n = parseInt(token, 10);
|
|
35
|
+
if (!Number.isInteger(n) || !/^[+-]?\d+$/.test(token.trim())) {
|
|
36
|
+
throw new Error(`Invalid cron ${label} '${token}' in '${field}': expected an integer`);
|
|
37
|
+
}
|
|
38
|
+
return n;
|
|
39
|
+
};
|
|
22
40
|
for (const part of field.split(',')) {
|
|
23
41
|
if (part === '*') {
|
|
24
42
|
// All values
|
|
@@ -30,88 +48,244 @@ function parseField(field, min, max) {
|
|
|
30
48
|
// Step values (e.g., */5 or 1-10/2)
|
|
31
49
|
const [range, stepStr] = part.split('/');
|
|
32
50
|
const step = parseInt(stepStr, 10);
|
|
51
|
+
if (!Number.isInteger(step) || step < 1) {
|
|
52
|
+
throw new Error(`Invalid cron step '${stepStr}' in '${part}': step must be an integer >= 1`);
|
|
53
|
+
}
|
|
33
54
|
let start = min;
|
|
34
55
|
let end = max;
|
|
35
56
|
if (range !== '*') {
|
|
36
57
|
if (range.includes('-')) {
|
|
37
|
-
const [rangeStart, rangeEnd] = range.split('-')
|
|
38
|
-
start = rangeStart;
|
|
39
|
-
end = rangeEnd;
|
|
58
|
+
const [rangeStart, rangeEnd] = range.split('-');
|
|
59
|
+
start = toInt(rangeStart, 'range start');
|
|
60
|
+
end = toInt(rangeEnd, 'range end');
|
|
40
61
|
}
|
|
41
62
|
else {
|
|
42
|
-
start =
|
|
63
|
+
start = toInt(range, 'range start');
|
|
43
64
|
}
|
|
44
65
|
}
|
|
66
|
+
if (end < start) {
|
|
67
|
+
throw new Error(`Invalid cron range '${range}' in '${part}': ${start} is after ${end}`);
|
|
68
|
+
}
|
|
45
69
|
for (let i = start; i <= end; i += step) {
|
|
46
70
|
values.add(i);
|
|
47
71
|
}
|
|
48
72
|
}
|
|
49
73
|
else if (part.includes('-')) {
|
|
50
74
|
// Range (e.g., 1-5)
|
|
51
|
-
const [
|
|
75
|
+
const [startStr, endStr] = part.split('-');
|
|
76
|
+
const start = toInt(startStr, 'range start');
|
|
77
|
+
const end = toInt(endStr, 'range end');
|
|
78
|
+
if (end < start) {
|
|
79
|
+
throw new Error(`Invalid cron range '${part}': ${start} is after ${end}`);
|
|
80
|
+
}
|
|
52
81
|
for (let i = start; i <= end; i++) {
|
|
53
82
|
values.add(i);
|
|
54
83
|
}
|
|
55
84
|
}
|
|
56
85
|
else {
|
|
57
86
|
// Single value
|
|
58
|
-
values.add(
|
|
87
|
+
values.add(toInt(part, 'value'));
|
|
59
88
|
}
|
|
60
89
|
}
|
|
61
|
-
//
|
|
90
|
+
// Day-of-week 7 is an alias for Sunday (0) in standard cron.
|
|
91
|
+
const normalized = new Set();
|
|
62
92
|
for (const value of values) {
|
|
63
|
-
|
|
93
|
+
normalized.add(allowSundaySeven && value === 7 ? 0 : value);
|
|
94
|
+
}
|
|
95
|
+
// Validate all values are in range (NaN is rejected by `toInt` above, but
|
|
96
|
+
// guard explicitly so an out-of-range NaN can never slip through).
|
|
97
|
+
for (const value of normalized) {
|
|
98
|
+
if (Number.isNaN(value) || value < min || value > max) {
|
|
64
99
|
throw new Error(`Cron field value ${value} out of range [${min}, ${max}]`);
|
|
65
100
|
}
|
|
66
101
|
}
|
|
67
|
-
return Array.from(
|
|
102
|
+
return Array.from(normalized).sort((a, b) => a - b);
|
|
103
|
+
}
|
|
104
|
+
/** Number of days in a given (1-based) month, accounting for leap years. */
|
|
105
|
+
function daysInMonth(year, month) {
|
|
106
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
107
|
+
}
|
|
108
|
+
/** The day-of-week (0 = Sunday) for a calendar date — timezone-independent. */
|
|
109
|
+
function weekdayOf(year, month, day) {
|
|
110
|
+
return new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
|
111
|
+
}
|
|
112
|
+
/** Read the wall-clock fields of an instant in `timeZone`. */
|
|
113
|
+
function partsInZone(instant, timeZone) {
|
|
114
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
115
|
+
timeZone,
|
|
116
|
+
hourCycle: 'h23',
|
|
117
|
+
year: 'numeric',
|
|
118
|
+
month: '2-digit',
|
|
119
|
+
day: '2-digit',
|
|
120
|
+
hour: '2-digit',
|
|
121
|
+
minute: '2-digit',
|
|
122
|
+
second: '2-digit',
|
|
123
|
+
});
|
|
124
|
+
const map = {};
|
|
125
|
+
for (const part of dtf.formatToParts(new Date(instant))) {
|
|
126
|
+
if (part.type !== 'literal')
|
|
127
|
+
map[part.type] = parseInt(part.value, 10);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
year: map.year,
|
|
131
|
+
month: map.month,
|
|
132
|
+
day: map.day,
|
|
133
|
+
hour: map.hour === 24 ? 0 : map.hour,
|
|
134
|
+
minute: map.minute,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Offset (ms) such that wall-clock-in-zone = instant + offset, at `instant`. */
|
|
138
|
+
function zoneOffsetMs(instant, timeZone) {
|
|
139
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
140
|
+
timeZone,
|
|
141
|
+
hourCycle: 'h23',
|
|
142
|
+
year: 'numeric',
|
|
143
|
+
month: '2-digit',
|
|
144
|
+
day: '2-digit',
|
|
145
|
+
hour: '2-digit',
|
|
146
|
+
minute: '2-digit',
|
|
147
|
+
second: '2-digit',
|
|
148
|
+
});
|
|
149
|
+
const m = {};
|
|
150
|
+
for (const part of dtf.formatToParts(new Date(instant))) {
|
|
151
|
+
if (part.type !== 'literal')
|
|
152
|
+
m[part.type] = parseInt(part.value, 10);
|
|
153
|
+
}
|
|
154
|
+
const hour = m.hour === 24 ? 0 : m.hour;
|
|
155
|
+
return Date.UTC(m.year, m.month - 1, m.day, hour, m.minute, m.second) - instant;
|
|
68
156
|
}
|
|
69
157
|
/**
|
|
70
|
-
*
|
|
158
|
+
* Convert a wall-clock time in `timeZone` to the corresponding UTC instant.
|
|
159
|
+
* Around DST transitions the wall time may be ambiguous or nonexistent; this
|
|
160
|
+
* resolves to a deterministic nearby instant.
|
|
71
161
|
*/
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
162
|
+
function wallToInstant(wc, timeZone) {
|
|
163
|
+
const asUTC = Date.UTC(wc.year, wc.month - 1, wc.day, wc.hour, wc.minute);
|
|
164
|
+
const offset = zoneOffsetMs(asUTC, timeZone);
|
|
165
|
+
let instant = asUTC - offset;
|
|
166
|
+
const offset2 = zoneOffsetMs(instant, timeZone);
|
|
167
|
+
if (offset2 !== offset)
|
|
168
|
+
instant = asUTC - offset2;
|
|
169
|
+
return instant;
|
|
170
|
+
}
|
|
171
|
+
/** Advance a wall-clock by one minute, rolling over fields as needed. */
|
|
172
|
+
function addMinute(wc) {
|
|
173
|
+
let { year, month, day, hour, minute } = wc;
|
|
174
|
+
minute += 1;
|
|
175
|
+
if (minute > 59) {
|
|
176
|
+
minute = 0;
|
|
177
|
+
hour += 1;
|
|
178
|
+
if (hour > 23) {
|
|
179
|
+
hour = 0;
|
|
180
|
+
day += 1;
|
|
181
|
+
if (day > daysInMonth(year, month)) {
|
|
182
|
+
day = 1;
|
|
183
|
+
month += 1;
|
|
184
|
+
if (month > 12) {
|
|
185
|
+
month = 1;
|
|
186
|
+
year += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { year, month, day, hour, minute };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Calculate the next run time for a cron schedule, evaluated against
|
|
195
|
+
* wall-clock time in `timeZone` (default UTC, so DST never shifts the result).
|
|
196
|
+
* Day-of-month and day-of-week follow POSIX: when both are restricted the
|
|
197
|
+
* match is their union (OR), not their intersection.
|
|
198
|
+
*/
|
|
199
|
+
export function getNextCronRun(schedule, after = new Date(), timeZone = 'UTC') {
|
|
200
|
+
// Fail fast on combinations that can never match (e.g. Feb 31) instead of
|
|
201
|
+
// grinding through a full multi-year minute-by-minute scan before throwing.
|
|
202
|
+
assertReachableDayMonth(schedule);
|
|
203
|
+
// Start from the first whole minute strictly after `after`.
|
|
204
|
+
const wc = addMinute(partsInZone(after.getTime(), timeZone));
|
|
77
205
|
const maxIterations = 4 * 366 * 24 * 60;
|
|
78
206
|
for (let i = 0; i < maxIterations; i++) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
207
|
+
if (!schedule.month.includes(wc.month)) {
|
|
208
|
+
wc.month += 1;
|
|
209
|
+
if (wc.month > 12) {
|
|
210
|
+
wc.month = 1;
|
|
211
|
+
wc.year += 1;
|
|
212
|
+
}
|
|
213
|
+
wc.day = 1;
|
|
214
|
+
wc.hour = 0;
|
|
215
|
+
wc.minute = 0;
|
|
85
216
|
continue;
|
|
86
217
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
218
|
+
if (wc.day > daysInMonth(wc.year, wc.month)) {
|
|
219
|
+
wc.month += 1;
|
|
220
|
+
if (wc.month > 12) {
|
|
221
|
+
wc.month = 1;
|
|
222
|
+
wc.year += 1;
|
|
223
|
+
}
|
|
224
|
+
wc.day = 1;
|
|
225
|
+
wc.hour = 0;
|
|
226
|
+
wc.minute = 0;
|
|
91
227
|
continue;
|
|
92
228
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
229
|
+
if (!matchesDay(schedule, wc)) {
|
|
230
|
+
wc.day += 1;
|
|
231
|
+
wc.hour = 0;
|
|
232
|
+
wc.minute = 0;
|
|
97
233
|
continue;
|
|
98
234
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
235
|
+
if (!schedule.hour.includes(wc.hour)) {
|
|
236
|
+
wc.hour += 1;
|
|
237
|
+
wc.minute = 0;
|
|
238
|
+
if (wc.hour > 23) {
|
|
239
|
+
wc.hour = 0;
|
|
240
|
+
wc.day += 1;
|
|
241
|
+
}
|
|
103
242
|
continue;
|
|
104
243
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
244
|
+
if (!schedule.minute.includes(wc.minute)) {
|
|
245
|
+
wc.minute += 1;
|
|
246
|
+
if (wc.minute > 59) {
|
|
247
|
+
wc.minute = 0;
|
|
248
|
+
wc.hour += 1;
|
|
249
|
+
if (wc.hour > 23) {
|
|
250
|
+
wc.hour = 0;
|
|
251
|
+
wc.day += 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
108
254
|
continue;
|
|
109
255
|
}
|
|
110
|
-
|
|
111
|
-
return next;
|
|
256
|
+
return new Date(wallToInstant(wc, timeZone));
|
|
112
257
|
}
|
|
113
258
|
throw new Error('Could not find next cron run time within 4 years');
|
|
114
259
|
}
|
|
260
|
+
/** Maximum days any given (1-based) month can have, allowing for leap Februarys. */
|
|
261
|
+
const MAX_DAYS_IN_MONTH = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
262
|
+
/**
|
|
263
|
+
* Throw if a restricted day-of-month can never occur in any allowed month.
|
|
264
|
+
* Only applies when day-of-week is a wildcard: with a restricted weekday the
|
|
265
|
+
* POSIX OR means a matching day still occurs every month, so it is reachable.
|
|
266
|
+
*/
|
|
267
|
+
function assertReachableDayMonth(schedule) {
|
|
268
|
+
if (!schedule.dayOfMonthRestricted || schedule.dayOfWeekRestricted)
|
|
269
|
+
return;
|
|
270
|
+
const reachable = schedule.month.some((m) => schedule.dayOfMonth.some((d) => d <= MAX_DAYS_IN_MONTH[m]));
|
|
271
|
+
if (!reachable) {
|
|
272
|
+
throw new Error(`Cron schedule can never match: day-of-month [${schedule.dayOfMonth.join(',')}] never occurs in month(s) [${schedule.month.join(',')}]`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** POSIX day matching: union of day-of-month and day-of-week when both set. */
|
|
276
|
+
function matchesDay(schedule, wc) {
|
|
277
|
+
const domMatch = schedule.dayOfMonth.includes(wc.day);
|
|
278
|
+
const dowMatch = schedule.dayOfWeek.includes(weekdayOf(wc.year, wc.month, wc.day));
|
|
279
|
+
const domR = schedule.dayOfMonthRestricted;
|
|
280
|
+
const dowR = schedule.dayOfWeekRestricted;
|
|
281
|
+
if (domR && dowR)
|
|
282
|
+
return domMatch || dowMatch;
|
|
283
|
+
if (domR)
|
|
284
|
+
return domMatch;
|
|
285
|
+
if (dowR)
|
|
286
|
+
return dowMatch;
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
115
289
|
/**
|
|
116
290
|
* Convert interval schedule to milliseconds
|
|
117
291
|
*/
|
|
@@ -142,7 +316,7 @@ export function getNextRunTime(schedule, after = new Date()) {
|
|
|
142
316
|
throw new Error('Cron schedule missing cron expression');
|
|
143
317
|
}
|
|
144
318
|
const cronSchedule = parseCronExpression(schedule.cronExpression);
|
|
145
|
-
return getNextCronRun(cronSchedule, after);
|
|
319
|
+
return getNextCronRun(cronSchedule, after, schedule.timezone ?? 'UTC');
|
|
146
320
|
}
|
|
147
321
|
case 'once': {
|
|
148
322
|
if (!schedule.runAt) {
|
|
@@ -178,19 +352,24 @@ export function shouldRunNow(schedule, lastRun, checkIntervalMs = 1000) {
|
|
|
178
352
|
if (!schedule.cronExpression)
|
|
179
353
|
return false;
|
|
180
354
|
const cronSchedule = parseCronExpression(schedule.cronExpression);
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
355
|
+
// Drift-free: the job is due once its next scheduled time after the last
|
|
356
|
+
// run has arrived. A late or slow poll never silently skips a tick — the
|
|
357
|
+
// overshot run still satisfies `nextRun <= now`, instead of needing `now`
|
|
358
|
+
// to land within a 1s window of the scheduled time.
|
|
359
|
+
const baseline = lastRun ?? new Date(now.getTime() - checkIntervalMs);
|
|
360
|
+
const nextRun = getNextCronRun(cronSchedule, baseline, schedule.timezone ?? 'UTC');
|
|
361
|
+
return nextRun.getTime() <= now.getTime();
|
|
185
362
|
}
|
|
186
363
|
case 'once': {
|
|
187
364
|
if (!schedule.runAt)
|
|
188
365
|
return false;
|
|
189
366
|
if (lastRun)
|
|
190
367
|
return false; // Already ran
|
|
368
|
+
// Fire as soon as the scheduled instant has arrived. A missed tick must
|
|
369
|
+
// not strand the job: once runAt is in the past (and it has never run),
|
|
370
|
+
// the next check still catches it, rather than only a ±checkInterval window.
|
|
191
371
|
const runAt = new Date(schedule.runAt);
|
|
192
|
-
|
|
193
|
-
return diff <= checkIntervalMs;
|
|
372
|
+
return now.getTime() >= runAt.getTime();
|
|
194
373
|
}
|
|
195
374
|
default:
|
|
196
375
|
return false;
|