adispatchd 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/api/errors.d.ts +27 -0
- package/dist/api/errors.js +80 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +11 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/logs.d.ts +30 -0
- package/dist/api/logs.js +88 -0
- package/dist/api/logs.js.map +1 -0
- package/dist/api/serialize.d.ts +65 -0
- package/dist/api/serialize.js +135 -0
- package/dist/api/serialize.js.map +1 -0
- package/dist/api/server.d.ts +89 -0
- package/dist/api/server.js +751 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/sse.d.ts +90 -0
- package/dist/api/sse.js +225 -0
- package/dist/api/sse.js.map +1 -0
- package/dist/app.d.ts +173 -0
- package/dist/app.js +488 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/config-validate.d.ts +6 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/config-validate.js.map +1 -0
- package/dist/cli/control.d.ts +31 -0
- package/dist/cli/control.js +186 -0
- package/dist/cli/control.js.map +1 -0
- package/dist/cli/http-client.d.ts +57 -0
- package/dist/cli/http-client.js +174 -0
- package/dist/cli/http-client.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-templates.d.ts +20 -0
- package/dist/cli/init-templates.js +275 -0
- package/dist/cli/init-templates.js.map +1 -0
- package/dist/cli/init.d.ts +16 -0
- package/dist/cli/init.js +107 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +25 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/program.d.ts +102 -0
- package/dist/cli/program.js +133 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/service.d.ts +27 -0
- package/dist/cli/service.js +252 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/start.d.ts +22 -0
- package/dist/cli/start.js +133 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/workflow-dry-run.d.ts +13 -0
- package/dist/cli/workflow-dry-run.js +144 -0
- package/dist/cli/workflow-dry-run.js.map +1 -0
- package/dist/config/field-catalog.d.ts +33 -0
- package/dist/config/field-catalog.js +172 -0
- package/dist/config/field-catalog.js.map +1 -0
- package/dist/config/filter-schema.d.ts +15 -0
- package/dist/config/filter-schema.js +110 -0
- package/dist/config/filter-schema.js.map +1 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.js +16 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/issues.d.ts +27 -0
- package/dist/config/issues.js +37 -0
- package/dist/config/issues.js.map +1 -0
- package/dist/config/loader.d.ts +32 -0
- package/dist/config/loader.js +250 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/manager.d.ts +153 -0
- package/dist/config/manager.js +276 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/model.d.ts +334 -0
- package/dist/config/model.js +2 -0
- package/dist/config/model.js.map +1 -0
- package/dist/config/normalize.d.ts +20 -0
- package/dist/config/normalize.js +200 -0
- package/dist/config/normalize.js.map +1 -0
- package/dist/config/parameters.d.ts +37 -0
- package/dist/config/parameters.js +71 -0
- package/dist/config/parameters.js.map +1 -0
- package/dist/config/public.d.ts +6 -0
- package/dist/config/public.js +18 -0
- package/dist/config/public.js.map +1 -0
- package/dist/config/schemas.d.ts +538 -0
- package/dist/config/schemas.js +321 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/snapshot.d.ts +38 -0
- package/dist/config/snapshot.js +57 -0
- package/dist/config/snapshot.js.map +1 -0
- package/dist/config/validate.d.ts +33 -0
- package/dist/config/validate.js +346 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/core/clock.d.ts +59 -0
- package/dist/core/clock.js +133 -0
- package/dist/core/clock.js.map +1 -0
- package/dist/core/duration.d.ts +19 -0
- package/dist/core/duration.js +93 -0
- package/dist/core/duration.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.js +72 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/ids.d.ts +57 -0
- package/dist/core/ids.js +88 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +100 -0
- package/dist/core/logger.js +207 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paths.d.ts +84 -0
- package/dist/core/paths.js +138 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/template.d.ts +136 -0
- package/dist/core/template.js +261 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.d.ts +544 -0
- package/dist/core/types.js +172 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/worker-protocol.d.ts +102 -0
- package/dist/core/worker-protocol.js +75 -0
- package/dist/core/worker-protocol.js.map +1 -0
- package/dist/executors/classify.d.ts +25 -0
- package/dist/executors/classify.js +54 -0
- package/dist/executors/classify.js.map +1 -0
- package/dist/executors/dispatcher.d.ts +35 -0
- package/dist/executors/dispatcher.js +68 -0
- package/dist/executors/dispatcher.js.map +1 -0
- package/dist/executors/env.d.ts +43 -0
- package/dist/executors/env.js +66 -0
- package/dist/executors/env.js.map +1 -0
- package/dist/executors/event-parser.d.ts +41 -0
- package/dist/executors/event-parser.js +106 -0
- package/dist/executors/event-parser.js.map +1 -0
- package/dist/executors/index.d.ts +12 -0
- package/dist/executors/index.js +13 -0
- package/dist/executors/index.js.map +1 -0
- package/dist/executors/log-capture.d.ts +57 -0
- package/dist/executors/log-capture.js +155 -0
- package/dist/executors/log-capture.js.map +1 -0
- package/dist/executors/process-identity.d.ts +124 -0
- package/dist/executors/process-identity.js +283 -0
- package/dist/executors/process-identity.js.map +1 -0
- package/dist/executors/shell-executor.d.ts +35 -0
- package/dist/executors/shell-executor.js +478 -0
- package/dist/executors/shell-executor.js.map +1 -0
- package/dist/executors/stream-redactor.d.ts +40 -0
- package/dist/executors/stream-redactor.js +89 -0
- package/dist/executors/stream-redactor.js.map +1 -0
- package/dist/executors/workspace.d.ts +10 -0
- package/dist/executors/workspace.js +47 -0
- package/dist/executors/workspace.js.map +1 -0
- package/dist/filters/evaluate.d.ts +77 -0
- package/dist/filters/evaluate.js +187 -0
- package/dist/filters/evaluate.js.map +1 -0
- package/dist/filters/fields.d.ts +35 -0
- package/dist/filters/fields.js +75 -0
- package/dist/filters/fields.js.map +1 -0
- package/dist/filters/index.d.ts +11 -0
- package/dist/filters/index.js +12 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/filters/iso-date.d.ts +10 -0
- package/dist/filters/iso-date.js +42 -0
- package/dist/filters/iso-date.js.map +1 -0
- package/dist/filters/predicate.d.ts +24 -0
- package/dist/filters/predicate.js +178 -0
- package/dist/filters/predicate.js.map +1 -0
- package/dist/filters/priority.d.ts +51 -0
- package/dist/filters/priority.js +91 -0
- package/dist/filters/priority.js.map +1 -0
- package/dist/filters/pushdown.d.ts +12 -0
- package/dist/filters/pushdown.js +52 -0
- package/dist/filters/pushdown.js.map +1 -0
- package/dist/filters/shell-check.d.ts +110 -0
- package/dist/filters/shell-check.js +304 -0
- package/dist/filters/shell-check.js.map +1 -0
- package/dist/identities/github-app.d.ts +55 -0
- package/dist/identities/github-app.js +160 -0
- package/dist/identities/github-app.js.map +1 -0
- package/dist/identities/index.d.ts +6 -0
- package/dist/identities/index.js +7 -0
- package/dist/identities/index.js.map +1 -0
- package/dist/identities/jwt.d.ts +24 -0
- package/dist/identities/jwt.js +56 -0
- package/dist/identities/jwt.js.map +1 -0
- package/dist/lifecycle/context.d.ts +106 -0
- package/dist/lifecycle/context.js +170 -0
- package/dist/lifecycle/context.js.map +1 -0
- package/dist/lifecycle/engine.d.ts +185 -0
- package/dist/lifecycle/engine.js +551 -0
- package/dist/lifecycle/engine.js.map +1 -0
- package/dist/lifecycle/errors.d.ts +48 -0
- package/dist/lifecycle/errors.js +71 -0
- package/dist/lifecycle/errors.js.map +1 -0
- package/dist/lifecycle/index.d.ts +9 -0
- package/dist/lifecycle/index.js +9 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/performers.d.ts +66 -0
- package/dist/lifecycle/performers.js +179 -0
- package/dist/lifecycle/performers.js.map +1 -0
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +115 -0
- package/dist/notifications/service.js +364 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/persistence/actions.d.ts +101 -0
- package/dist/persistence/actions.js +196 -0
- package/dist/persistence/actions.js.map +1 -0
- package/dist/persistence/blocks.d.ts +85 -0
- package/dist/persistence/blocks.js +140 -0
- package/dist/persistence/blocks.js.map +1 -0
- package/dist/persistence/cron-firings.d.ts +55 -0
- package/dist/persistence/cron-firings.js +70 -0
- package/dist/persistence/cron-firings.js.map +1 -0
- package/dist/persistence/database.d.ts +60 -0
- package/dist/persistence/database.js +124 -0
- package/dist/persistence/database.js.map +1 -0
- package/dist/persistence/errors.d.ts +39 -0
- package/dist/persistence/errors.js +67 -0
- package/dist/persistence/errors.js.map +1 -0
- package/dist/persistence/events.d.ts +72 -0
- package/dist/persistence/events.js +83 -0
- package/dist/persistence/events.js.map +1 -0
- package/dist/persistence/index.d.ts +18 -0
- package/dist/persistence/index.js +19 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/instance-lock.d.ts +26 -0
- package/dist/persistence/instance-lock.js +117 -0
- package/dist/persistence/instance-lock.js.map +1 -0
- package/dist/persistence/lock-keys.d.ts +16 -0
- package/dist/persistence/lock-keys.js +41 -0
- package/dist/persistence/lock-keys.js.map +1 -0
- package/dist/persistence/notifications.d.ts +113 -0
- package/dist/persistence/notifications.js +213 -0
- package/dist/persistence/notifications.js.map +1 -0
- package/dist/persistence/persistence.d.ts +46 -0
- package/dist/persistence/persistence.js +42 -0
- package/dist/persistence/persistence.js.map +1 -0
- package/dist/persistence/retention.d.ts +46 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/retention.js.map +1 -0
- package/dist/persistence/runs.d.ts +215 -0
- package/dist/persistence/runs.js +417 -0
- package/dist/persistence/runs.js.map +1 -0
- package/dist/persistence/schema.d.ts +16 -0
- package/dist/persistence/schema.js +183 -0
- package/dist/persistence/schema.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +49 -0
- package/dist/persistence/sqlite.js +131 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/runs/controller.d.ts +307 -0
- package/dist/runs/controller.js +1078 -0
- package/dist/runs/controller.js.map +1 -0
- package/dist/runs/errors.d.ts +20 -0
- package/dist/runs/errors.js +26 -0
- package/dist/runs/errors.js.map +1 -0
- package/dist/runs/event-bus.d.ts +106 -0
- package/dist/runs/event-bus.js +105 -0
- package/dist/runs/event-bus.js.map +1 -0
- package/dist/runs/execution.d.ts +39 -0
- package/dist/runs/execution.js +146 -0
- package/dist/runs/execution.js.map +1 -0
- package/dist/runs/index.d.ts +12 -0
- package/dist/runs/index.js +13 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/retention.d.ts +64 -0
- package/dist/runs/retention.js +232 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/workspace.d.ts +20 -0
- package/dist/runs/workspace.js +51 -0
- package/dist/runs/workspace.js.map +1 -0
- package/dist/scheduler/checks.d.ts +41 -0
- package/dist/scheduler/checks.js +111 -0
- package/dist/scheduler/checks.js.map +1 -0
- package/dist/scheduler/cron.d.ts +32 -0
- package/dist/scheduler/cron.js +83 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/dry-run.d.ts +42 -0
- package/dist/scheduler/dry-run.js +167 -0
- package/dist/scheduler/dry-run.js.map +1 -0
- package/dist/scheduler/errors.d.ts +22 -0
- package/dist/scheduler/errors.js +22 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/format.d.ts +3 -0
- package/dist/scheduler/format.js +73 -0
- package/dist/scheduler/format.js.map +1 -0
- package/dist/scheduler/index.d.ts +13 -0
- package/dist/scheduler/index.js +14 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/planner.d.ts +88 -0
- package/dist/scheduler/planner.js +255 -0
- package/dist/scheduler/planner.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +160 -0
- package/dist/scheduler/scheduler.js +680 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/state.d.ts +26 -0
- package/dist/scheduler/state.js +25 -0
- package/dist/scheduler/state.js.map +1 -0
- package/dist/scheduler/types.d.ts +197 -0
- package/dist/scheduler/types.js +3 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service/index.d.ts +6 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/install.d.ts +93 -0
- package/dist/service/install.js +250 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/unit.d.ts +50 -0
- package/dist/service/unit.js +171 -0
- package/dist/service/unit.js.map +1 -0
- package/dist/sources/github/adapter.d.ts +99 -0
- package/dist/sources/github/adapter.js +336 -0
- package/dist/sources/github/adapter.js.map +1 -0
- package/dist/sources/github/fields.d.ts +8 -0
- package/dist/sources/github/fields.js +72 -0
- package/dist/sources/github/fields.js.map +1 -0
- package/dist/sources/github/http.d.ts +100 -0
- package/dist/sources/github/http.js +288 -0
- package/dist/sources/github/http.js.map +1 -0
- package/dist/sources/github/index.d.ts +6 -0
- package/dist/sources/github/index.js +7 -0
- package/dist/sources/github/index.js.map +1 -0
- package/dist/sources/github/pushdown.d.ts +14 -0
- package/dist/sources/github/pushdown.js +47 -0
- package/dist/sources/github/pushdown.js.map +1 -0
- package/dist/sources/github/refs.d.ts +17 -0
- package/dist/sources/github/refs.js +61 -0
- package/dist/sources/github/refs.js.map +1 -0
- package/dist/sources/index.d.ts +6 -0
- package/dist/sources/index.js +7 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +28 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/public/app.css +239 -0
- package/dist/ui/public/favicon.svg +1 -0
- package/dist/ui/public/index.html +27 -0
- package/dist/ui/public/js/client/api.js +68 -0
- package/dist/ui/public/js/client/app.js +146 -0
- package/dist/ui/public/js/client/dom.js +34 -0
- package/dist/ui/public/js/client/page.js +9 -0
- package/dist/ui/public/js/client/pages/actions.js +35 -0
- package/dist/ui/public/js/client/pages/blocked.js +58 -0
- package/dist/ui/public/js/client/pages/config.js +126 -0
- package/dist/ui/public/js/client/pages/dashboard.js +175 -0
- package/dist/ui/public/js/client/pages/run.js +367 -0
- package/dist/ui/public/js/client/pages/runs.js +72 -0
- package/dist/ui/public/js/client/pages/views.js +80 -0
- package/dist/ui/public/js/client/pages/workflows.js +291 -0
- package/dist/ui/public/js/client/types.js +1 -0
- package/dist/ui/public/js/client/ui.js +152 -0
- package/dist/ui/public/js/shared/dom.js +77 -0
- package/dist/ui/public/js/shared/events.js +88 -0
- package/dist/ui/public/js/shared/format.js +213 -0
- package/dist/ui/public/js/shared/logs.js +136 -0
- package/dist/ui/public/js/shared/params.js +90 -0
- package/dist/ui/public/js/shared/route.js +36 -0
- package/dist/ui/public/js/shared/sort.js +70 -0
- package/dist/ui/public/js/shared/sse.js +193 -0
- package/dist/ui/public/js/shared/throttle.js +65 -0
- package/dist/views/index.d.ts +6 -0
- package/dist/views/index.js +7 -0
- package/dist/views/index.js.map +1 -0
- package/dist/views/service.d.ts +131 -0
- package/dist/views/service.js +264 -0
- package/dist/views/service.js.map +1 -0
- package/docs/api.md +239 -0
- package/docs/configuration-reference.md +1785 -0
- package/docs/cron-semantics.md +96 -0
- package/docs/manual-tests/github-fixture/config.yaml +30 -0
- package/docs/manual-tests/github-fixture/helpers.sh +93 -0
- package/docs/manual-tests/github-fixture/identities.yaml +18 -0
- package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
- package/docs/manual-tests/github-fixture/sources.yaml +13 -0
- package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
- package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
- package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
- package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
- package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
- package/docs/manual-tests/github.md +1308 -0
- package/docs/manual-tests/process-supervision.md +64 -0
- package/docs/manual-tests/systemd.md +68 -0
- package/docs/operations.md +271 -0
- package/docs/worker-protocol.md +145 -0
- package/examples/config/config.yaml +35 -0
- package/examples/config/identities.yaml +18 -0
- package/examples/config/sources.yaml +13 -0
- package/examples/config/views/needs-maintainer.yaml +15 -0
- package/examples/config/workflows/coder-project-a.yaml +86 -0
- package/examples/config/workflows/deploy.yaml +15 -0
- package/examples/config/workflows/nightly-maintenance.yaml +13 -0
- package/package.json +75 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { InvalidTransitionError, NotFoundError } from './errors.js';
|
|
2
|
+
import { all, fromJson, get, run, str, toJson, transaction } from './sqlite.js';
|
|
3
|
+
/** Allowed action status changes. */
|
|
4
|
+
export const ACTION_STATUS_TRANSITIONS = {
|
|
5
|
+
pending: ['running', 'succeeded', 'failed', 'skipped'],
|
|
6
|
+
// `running → pending`: recovery resets an interrupted attempt before retrying.
|
|
7
|
+
running: ['succeeded', 'failed', 'pending'],
|
|
8
|
+
// `failed → running|pending`: (manual) retry; `failed → skipped`: operator skips it.
|
|
9
|
+
failed: ['running', 'pending', 'skipped'],
|
|
10
|
+
succeeded: [],
|
|
11
|
+
skipped: [],
|
|
12
|
+
};
|
|
13
|
+
/** Repository for the action journal. */
|
|
14
|
+
export class ActionRepository {
|
|
15
|
+
db;
|
|
16
|
+
clock;
|
|
17
|
+
constructor(db, clock) {
|
|
18
|
+
this.db = db;
|
|
19
|
+
this.clock = clock;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create the journal for all actions of a hook (`pending`, ordinal 0..n-1).
|
|
23
|
+
* Idempotent: if a journal for `(runId, hook)` already exists it is returned
|
|
24
|
+
* unchanged (resume after restart).
|
|
25
|
+
*/
|
|
26
|
+
createJournal(runId, hook, actions) {
|
|
27
|
+
return transaction(this.db, () => {
|
|
28
|
+
const existing = this.listForHook(runId, hook);
|
|
29
|
+
if (existing.length > 0)
|
|
30
|
+
return existing;
|
|
31
|
+
if (!get(this.db, 'SELECT 1 FROM runs WHERE id = ?', [runId]))
|
|
32
|
+
throw new NotFoundError('run', runId);
|
|
33
|
+
const now = this.clock.now().toISOString();
|
|
34
|
+
actions.forEach((action, ordinal) => {
|
|
35
|
+
run(this.db, `INSERT INTO run_actions (run_id, hook, ordinal, action_type, action_name, action_config_json, status, created_at, updated_at)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)`, [runId, hook, ordinal, action.type, action.name ?? null, JSON.stringify(action.config ?? null), now, now]);
|
|
37
|
+
});
|
|
38
|
+
return this.listForHook(runId, hook);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Action by ID or `null`. */
|
|
42
|
+
get(id) {
|
|
43
|
+
const row = get(this.db, 'SELECT * FROM run_actions WHERE id = ?', [id]);
|
|
44
|
+
return row ? toAction(row) : null;
|
|
45
|
+
}
|
|
46
|
+
/** Action by ID; throws {@link NotFoundError}. */
|
|
47
|
+
require(id) {
|
|
48
|
+
const a = this.get(id);
|
|
49
|
+
if (!a)
|
|
50
|
+
throw new NotFoundError('action', id);
|
|
51
|
+
return a;
|
|
52
|
+
}
|
|
53
|
+
/** All actions of a run, by hook creation then ordinal. */
|
|
54
|
+
listForRun(runId) {
|
|
55
|
+
return all(this.db, 'SELECT * FROM run_actions WHERE run_id = ? ORDER BY id ASC', [runId]).map(toAction);
|
|
56
|
+
}
|
|
57
|
+
/** Actions of one hook in ordinal order. */
|
|
58
|
+
listForHook(runId, hook) {
|
|
59
|
+
return all(this.db, 'SELECT * FROM run_actions WHERE run_id = ? AND hook = ? ORDER BY ordinal ASC', [runId, hook]).map(toAction);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Change an action's status (validated against {@link ACTION_STATUS_TRANSITIONS}).
|
|
63
|
+
* `externalKey`/`error` are stored when given.
|
|
64
|
+
*/
|
|
65
|
+
setStatus(id, status, options = {}) {
|
|
66
|
+
return transaction(this.db, () => {
|
|
67
|
+
const current = this.require(id);
|
|
68
|
+
if (options.expectedFrom !== undefined && current.status !== options.expectedFrom) {
|
|
69
|
+
throw new InvalidTransitionError(`Action ${id} is '${current.status}', expected '${options.expectedFrom}'`, {
|
|
70
|
+
actionId: id,
|
|
71
|
+
from: current.status,
|
|
72
|
+
to: status,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (current.status !== status && !ACTION_STATUS_TRANSITIONS[current.status].includes(status)) {
|
|
76
|
+
throw new InvalidTransitionError(`Invalid action transition '${current.status}' → '${status}'`, {
|
|
77
|
+
actionId: id,
|
|
78
|
+
from: current.status,
|
|
79
|
+
to: status,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const sets = ['status = ?', 'updated_at = ?'];
|
|
83
|
+
const params = [status, this.clock.now().toISOString()];
|
|
84
|
+
if (options.externalKey !== undefined) {
|
|
85
|
+
sets.push('external_key = ?');
|
|
86
|
+
params.push(options.externalKey);
|
|
87
|
+
}
|
|
88
|
+
if (options.error !== undefined) {
|
|
89
|
+
sets.push('last_error_json = ?');
|
|
90
|
+
params.push(toJson(options.error ?? undefined));
|
|
91
|
+
}
|
|
92
|
+
run(this.db, `UPDATE run_actions SET ${sets.join(', ')} WHERE id = ?`, [...params, id]);
|
|
93
|
+
return this.require(id);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Start a new attempt: inserts `action_attempts` row (attempt = previous + 1),
|
|
98
|
+
* increments `attempts` and sets the action `running`.
|
|
99
|
+
*/
|
|
100
|
+
startAttempt(actionId) {
|
|
101
|
+
return transaction(this.db, () => {
|
|
102
|
+
const action = this.require(actionId);
|
|
103
|
+
if (action.status !== 'running')
|
|
104
|
+
this.setStatus(actionId, 'running');
|
|
105
|
+
const next = Number(get(this.db, 'SELECT COALESCE(MAX(attempt), 0) + 1 AS n FROM action_attempts WHERE run_action_id = ?', [actionId])?.n ?? 1);
|
|
106
|
+
const now = this.clock.now().toISOString();
|
|
107
|
+
const res = run(this.db, 'INSERT INTO action_attempts (run_action_id, attempt, started_at) VALUES (?, ?, ?)', [
|
|
108
|
+
actionId,
|
|
109
|
+
next,
|
|
110
|
+
now,
|
|
111
|
+
]);
|
|
112
|
+
run(this.db, 'UPDATE run_actions SET attempts = ?, updated_at = ? WHERE id = ?', [next, now, actionId]);
|
|
113
|
+
return this.requireAttempt(res.lastInsertRowid);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Record the end of an attempt. Does not change the action status – the
|
|
118
|
+
* lifecycle engine decides (retry → stays `running`/`pending`, give up → `failed`).
|
|
119
|
+
*/
|
|
120
|
+
finishAttempt(attemptId, outcome, options = {}) {
|
|
121
|
+
return transaction(this.db, () => {
|
|
122
|
+
const attempt = this.requireAttempt(attemptId);
|
|
123
|
+
const now = this.clock.now().toISOString();
|
|
124
|
+
run(this.db, 'UPDATE action_attempts SET finished_at = ?, outcome = ?, error_json = ?, external_key = ? WHERE id = ?', [
|
|
125
|
+
now,
|
|
126
|
+
outcome,
|
|
127
|
+
toJson(options.error ?? undefined),
|
|
128
|
+
options.externalKey ?? null,
|
|
129
|
+
attemptId,
|
|
130
|
+
]);
|
|
131
|
+
if (options.error !== undefined) {
|
|
132
|
+
run(this.db, 'UPDATE run_actions SET last_error_json = ?, updated_at = ? WHERE id = ?', [
|
|
133
|
+
toJson(options.error),
|
|
134
|
+
now,
|
|
135
|
+
attempt.actionId,
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
return this.requireAttempt(attemptId);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Attempts of an action in order. */
|
|
142
|
+
listAttempts(actionId) {
|
|
143
|
+
return all(this.db, 'SELECT * FROM action_attempts WHERE run_action_id = ? ORDER BY attempt ASC', [actionId]).map(toAttempt);
|
|
144
|
+
}
|
|
145
|
+
/** All `failed` actions with run context, newest first (GUI / retry). */
|
|
146
|
+
listFailed() {
|
|
147
|
+
return all(this.db, `SELECT a.*, r.workflow_id AS r_workflow_id, r.source_item_key AS r_item, r.source_url AS r_url
|
|
148
|
+
FROM run_actions a JOIN runs r ON r.id = a.run_id
|
|
149
|
+
WHERE a.status = 'failed' ORDER BY a.updated_at DESC, a.id DESC`).map((row) => ({
|
|
150
|
+
...toAction(row),
|
|
151
|
+
workflowId: String(row.r_workflow_id),
|
|
152
|
+
sourceItemKey: str(row.r_item),
|
|
153
|
+
sourceUrl: str(row.r_url),
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
/** IDs of runs with `pending` or `running` actions (recovery: resume chains). */
|
|
157
|
+
runIdsWithIncompleteActions() {
|
|
158
|
+
return all(this.db, `SELECT DISTINCT run_id FROM run_actions WHERE status IN ('pending', 'running') ORDER BY run_id ASC`).map((r) => String(r.run_id));
|
|
159
|
+
}
|
|
160
|
+
requireAttempt(id) {
|
|
161
|
+
const row = get(this.db, 'SELECT * FROM action_attempts WHERE id = ?', [id]);
|
|
162
|
+
if (!row)
|
|
163
|
+
throw new NotFoundError('action_attempt', id);
|
|
164
|
+
return toAttempt(row);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function toAction(row) {
|
|
168
|
+
return {
|
|
169
|
+
id: Number(row.id),
|
|
170
|
+
runId: String(row.run_id),
|
|
171
|
+
hook: row.hook,
|
|
172
|
+
ordinal: Number(row.ordinal),
|
|
173
|
+
type: String(row.action_type),
|
|
174
|
+
name: str(row.action_name),
|
|
175
|
+
config: fromJson(row.action_config_json),
|
|
176
|
+
status: row.status,
|
|
177
|
+
externalKey: str(row.external_key),
|
|
178
|
+
attempts: Number(row.attempts),
|
|
179
|
+
lastError: fromJson(row.last_error_json),
|
|
180
|
+
createdAt: String(row.created_at),
|
|
181
|
+
updatedAt: String(row.updated_at),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function toAttempt(row) {
|
|
185
|
+
return {
|
|
186
|
+
id: Number(row.id),
|
|
187
|
+
actionId: Number(row.run_action_id),
|
|
188
|
+
attempt: Number(row.attempt),
|
|
189
|
+
startedAt: String(row.started_at),
|
|
190
|
+
finishedAt: str(row.finished_at),
|
|
191
|
+
outcome: str(row.outcome) ?? null,
|
|
192
|
+
error: fromJson(row.error_json),
|
|
193
|
+
externalKey: str(row.external_key),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/persistence/actions.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAY,MAAM,aAAa,CAAC;AAE1F,qCAAqC;AACrC,MAAM,CAAC,MAAM,yBAAyB,GAA4D;IAChG,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,CAAC;IACtD,+EAA+E;IAC/E,OAAO,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,SAAS,CAAC;IAC3C,qFAAqF;IACrF,MAAM,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC;IACzC,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;CACZ,CAAC;AAkDF,yCAAyC;AACzC,MAAM,OAAO,gBAAgB;IAER,EAAE;IACF,KAAK;IAFxB,YACmB,EAAgB,EAChB,KAAY;kBADZ,EAAE;qBACF,KAAK;IACrB,CAAC;IAEJ;;;;OAIG;IACH,aAAa,CAAC,KAAa,EAAE,IAAc,EAAE,OAAsC;QACjF,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE;YAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC/C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,QAAQ,CAAC;YACzC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,iCAAiC,EAAE,CAAC,KAAK,CAAC,CAAC;gBAAE,MAAM,IAAI,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACrG,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;gBAClC,GAAG,CACD,IAAI,CAAC,EAAE,EACP;sDAC4C,EAC5C,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAC1G,CAAC;YACJ,CAAC,CAAC,CAAC;YACH,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,8BAA8B;IAC9B,GAAG,CAAC,EAAU;QACZ,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,wCAAwC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACzE,OAAO,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpC,CAAC;IAED,kDAAkD;IAClD,OAAO,CAAC,EAAU;QAChB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvB,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC9C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,2DAA2D;IAC3D,UAAU,CAAC,KAAa;QACtB,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,4DAA4D,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC3G,CAAC;IAED,4CAA4C;IAC5C,WAAW,CAAC,KAAa,EAAE,IAAc;QACvC,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,8EAA8E,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CACpH,QAAQ,CACT,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,SAAS,CACP,EAAU,EACV,MAAoB,EACpB,OAAO,GAAkF,EAAE;QAE3F,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE;YAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACjC,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC;gBAClF,MAAM,IAAI,sBAAsB,CAAC,UAAU,EAAE,QAAQ,OAAO,CAAC,MAAM,gBAAgB,OAAO,CAAC,YAAY,GAAG,EAAE;oBAC1G,QAAQ,EAAE,EAAE;oBACZ,IAAI,EAAE,OAAO,CAAC,MAAM;oBACpB,EAAE,EAAE,MAAM;iBACX,CAAC,CAAC;YACL,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC7F,MAAM,IAAI,sBAAsB,CAAC,8BAA8B,OAAO,CAAC,MAAM,QAAQ,MAAM,GAAG,EAAE;oBAC9F,QAAQ,EAAE,EAAE;oBACZ,IAAI,EAAE,OAAO,CAAC,MAAM;oBACpB,EAAE,EAAE,MAAM;iBACX,CAAC,CAAC;YACL,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAAC;YAC9C,MAAM,MAAM,GAA+B,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YACpF,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;gBACtC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;gBAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACnC,CAAC;YACD,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAChC,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;gBACjC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC;YAClD,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,0BAA0B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;YACxF,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,QAAgB;QAC3B,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YACrE,MAAM,IAAI,GAAG,MAAM,CACjB,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,wFAAwF,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAC3H,CAAC;YACF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,mFAAmF,EAAE;gBAC5G,QAAQ;gBACR,IAAI;gBACJ,GAAG;aACJ,CAAC,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,kEAAkE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;YACxG,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,aAAa,CACX,SAAiB,EACjB,OAA+B,EAC/B,OAAO,GAAqD,EAAE;QAE9D,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE;YAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;YAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,wGAAwG,EAAE;gBACrH,GAAG;gBACH,OAAO;gBACP,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,SAAS,CAAC;gBAClC,OAAO,CAAC,WAAW,IAAI,IAAI;gBAC3B,SAAS;aACV,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAChC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,yEAAyE,EAAE;oBACtF,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;oBACrB,GAAG;oBACH,OAAO,CAAC,QAAQ;iBACjB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,sCAAsC;IACtC,YAAY,CAAC,QAAgB;QAC3B,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,4EAA4E,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/H,CAAC;IAED,yEAAyE;IACzE,UAAU;QACR,OAAO,GAAG,CACR,IAAI,CAAC,EAAE,EACP;;uEAEiE,CAClE,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACd,GAAG,QAAQ,CAAC,GAAG,CAAC;YAChB,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;YACrC,aAAa,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;YAC9B,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC;SAC1B,CAAC,CAAC,CAAC;IACN,CAAC;IAED,iFAAiF;IACjF,2BAA2B;QACzB,OAAO,GAAG,CACR,IAAI,CAAC,EAAE,EACP,oGAAoG,CACrG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACjC,CAAC;IAEO,cAAc,CAAC,EAAU;QAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,4CAA4C,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,aAAa,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;QACxD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,GAAQ;IACxB,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QACzB,IAAI,EAAE,GAAG,CAAC,IAAgB;QAC1B,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;QAC5B,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;QAC7B,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC;QAC1B,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,kBAAkB,CAAC;QACxC,MAAM,EAAE,GAAG,CAAC,MAAsB;QAClC,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;QAClC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC9B,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC;QACxC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,GAAQ;IACzB,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;QAC5B,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QACjC,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC;QAChC,OAAO,EAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAmC,IAAI,IAAI;QACpE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;QAC/B,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;KACnC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry blocks and technical-failure counters (Spec §11).
|
|
3
|
+
*
|
|
4
|
+
* Counter design: an explicit `failure_counters` row per
|
|
5
|
+
* `(workflow_id, scope_key)` (scope = source item key, `''` = workflow scope
|
|
6
|
+
* for sourceless runs) holding `consecutive` and the counting-period
|
|
7
|
+
* `generation`. It is updated exclusively in the same transaction as a run's
|
|
8
|
+
* transition into `finalizing` (see `RunRepository.beginFinalization`), which
|
|
9
|
+
* happens at most once per run – so counting is exactly-once also across
|
|
10
|
+
* crashes/recovery. Technical failures (`crashed|start_error|timed_out`)
|
|
11
|
+
* increment; `completed`+`success` resets to 0 and starts a new counting
|
|
12
|
+
* period (`generation + 1`); business `failure` and `cancelled` leave it
|
|
13
|
+
* unchanged. Clearing a block likewise resets to 0 and increments
|
|
14
|
+
* `generation`; old runs and blocks stay untouched.
|
|
15
|
+
*/
|
|
16
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
17
|
+
import type { Clock } from '../core/index.js';
|
|
18
|
+
/** A persisted retry block. `itemKey === null` = workflow-scoped block. */
|
|
19
|
+
export interface BlockRecord {
|
|
20
|
+
id: number;
|
|
21
|
+
workflowId: string;
|
|
22
|
+
itemKey: string | null;
|
|
23
|
+
reason: string;
|
|
24
|
+
/** Run whose failure created the block, if any. */
|
|
25
|
+
runId: string | null;
|
|
26
|
+
/** Consecutive technical failures when the block was created. */
|
|
27
|
+
failureCount: number;
|
|
28
|
+
/** Counting period the block belongs to. */
|
|
29
|
+
generation: number;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
clearedAt: string | null;
|
|
32
|
+
clearedBy: string | null;
|
|
33
|
+
}
|
|
34
|
+
/** Input for {@link BlockRepository.create}. */
|
|
35
|
+
export interface CreateBlockInput {
|
|
36
|
+
workflowId: string;
|
|
37
|
+
itemKey: string | null;
|
|
38
|
+
reason: string;
|
|
39
|
+
runId?: string | null;
|
|
40
|
+
failureCount?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Current counter state of one workflow/scope. */
|
|
43
|
+
export interface FailureCounterState {
|
|
44
|
+
consecutive: number;
|
|
45
|
+
generation: number;
|
|
46
|
+
}
|
|
47
|
+
/** Repository for `blocks` and `failure_counters`. */
|
|
48
|
+
export declare class BlockRepository {
|
|
49
|
+
private readonly db;
|
|
50
|
+
private readonly clock;
|
|
51
|
+
constructor(db: DatabaseSync, clock: Clock);
|
|
52
|
+
/** Create an active block. Throws if an active block already exists for the key. */
|
|
53
|
+
create(input: CreateBlockInput): BlockRecord;
|
|
54
|
+
/**
|
|
55
|
+
* Clear (remove) a block: sets `cleared_at`, resets the counter and starts
|
|
56
|
+
* a new counting period (`generation + 1`). Idempotent for already cleared blocks.
|
|
57
|
+
*/
|
|
58
|
+
clear(id: number, clearedBy?: string | null): BlockRecord;
|
|
59
|
+
/** Block by ID or `null`. */
|
|
60
|
+
get(id: number): BlockRecord | null;
|
|
61
|
+
/** Active block for `(workflowId, itemKey)` (`itemKey = null` → workflow scope), or `null`. */
|
|
62
|
+
activeFor(workflowId: string, itemKey: string | null): BlockRecord | null;
|
|
63
|
+
/** True if an active block exists for `(workflowId, itemKey)`. */
|
|
64
|
+
isBlocked(workflowId: string, itemKey: string | null): boolean;
|
|
65
|
+
/** All active blocks, newest first. */
|
|
66
|
+
listActive(): BlockRecord[];
|
|
67
|
+
/** Block history of a workflow (optionally one scope), newest first. */
|
|
68
|
+
listForWorkflow(workflowId: string, options?: {
|
|
69
|
+
itemKey?: string | null;
|
|
70
|
+
limit?: number;
|
|
71
|
+
}): BlockRecord[];
|
|
72
|
+
/** IDs of runs whose technical failures were counted in the block's period (for the GUI). */
|
|
73
|
+
affectedRunIds(blockId: number): string[];
|
|
74
|
+
/** Consecutive technical failures in the current counting period. */
|
|
75
|
+
consecutiveTechnicalFailures(workflowId: string, itemKey: string | null): number;
|
|
76
|
+
/** Counter state (`{0, 0}` if never touched). */
|
|
77
|
+
counter(workflowId: string, itemKey: string | null): FailureCounterState;
|
|
78
|
+
/**
|
|
79
|
+
* Internal: apply a run outcome to the counter. Must be called inside the
|
|
80
|
+
* transaction that moves the run into `finalizing`.
|
|
81
|
+
*/
|
|
82
|
+
applyOutcome(workflowId: string, itemKey: string | null, effect: 'increment' | 'reset' | 'none'): FailureCounterState;
|
|
83
|
+
private ensureCounter;
|
|
84
|
+
private require;
|
|
85
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { BlockExistsError, NotFoundError } from './errors.js';
|
|
2
|
+
import { blockScopeKey } from './lock-keys.js';
|
|
3
|
+
import { all, get, run, str, transaction } from './sqlite.js';
|
|
4
|
+
/** Repository for `blocks` and `failure_counters`. */
|
|
5
|
+
export class BlockRepository {
|
|
6
|
+
db;
|
|
7
|
+
clock;
|
|
8
|
+
constructor(db, clock) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.clock = clock;
|
|
11
|
+
}
|
|
12
|
+
/** Create an active block. Throws if an active block already exists for the key. */
|
|
13
|
+
create(input) {
|
|
14
|
+
return transaction(this.db, () => {
|
|
15
|
+
const existing = this.activeFor(input.workflowId, input.itemKey);
|
|
16
|
+
if (existing) {
|
|
17
|
+
throw new BlockExistsError(existing.id);
|
|
18
|
+
}
|
|
19
|
+
const { generation } = this.counter(input.workflowId, input.itemKey);
|
|
20
|
+
const res = run(this.db, `INSERT INTO blocks (workflow_id, source_item_key, reason, run_id, failure_count, generation, created_at)
|
|
21
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
22
|
+
input.workflowId,
|
|
23
|
+
input.itemKey,
|
|
24
|
+
input.reason,
|
|
25
|
+
input.runId ?? null,
|
|
26
|
+
input.failureCount ?? 0,
|
|
27
|
+
generation,
|
|
28
|
+
this.clock.now().toISOString(),
|
|
29
|
+
]);
|
|
30
|
+
return this.require(res.lastInsertRowid);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Clear (remove) a block: sets `cleared_at`, resets the counter and starts
|
|
35
|
+
* a new counting period (`generation + 1`). Idempotent for already cleared blocks.
|
|
36
|
+
*/
|
|
37
|
+
clear(id, clearedBy = null) {
|
|
38
|
+
return transaction(this.db, () => {
|
|
39
|
+
const block = this.require(id);
|
|
40
|
+
if (block.clearedAt !== null)
|
|
41
|
+
return block;
|
|
42
|
+
const now = this.clock.now().toISOString();
|
|
43
|
+
run(this.db, 'UPDATE blocks SET cleared_at = ?, cleared_by = ? WHERE id = ?', [now, clearedBy, id]);
|
|
44
|
+
this.ensureCounter(block.workflowId, block.itemKey);
|
|
45
|
+
run(this.db, `UPDATE failure_counters SET consecutive = 0, generation = generation + 1, updated_at = ?
|
|
46
|
+
WHERE workflow_id = ? AND scope_key = ?`, [now, block.workflowId, blockScopeKey(block.itemKey)]);
|
|
47
|
+
return this.require(id);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Block by ID or `null`. */
|
|
51
|
+
get(id) {
|
|
52
|
+
const row = get(this.db, 'SELECT * FROM blocks WHERE id = ?', [id]);
|
|
53
|
+
return row ? toBlock(row) : null;
|
|
54
|
+
}
|
|
55
|
+
/** Active block for `(workflowId, itemKey)` (`itemKey = null` → workflow scope), or `null`. */
|
|
56
|
+
activeFor(workflowId, itemKey) {
|
|
57
|
+
const row = get(this.db, `SELECT * FROM blocks WHERE workflow_id = ? AND COALESCE(source_item_key, '') = ? AND cleared_at IS NULL`, [workflowId, blockScopeKey(itemKey)]);
|
|
58
|
+
return row ? toBlock(row) : null;
|
|
59
|
+
}
|
|
60
|
+
/** True if an active block exists for `(workflowId, itemKey)`. */
|
|
61
|
+
isBlocked(workflowId, itemKey) {
|
|
62
|
+
return this.activeFor(workflowId, itemKey) !== null;
|
|
63
|
+
}
|
|
64
|
+
/** All active blocks, newest first. */
|
|
65
|
+
listActive() {
|
|
66
|
+
return all(this.db, 'SELECT * FROM blocks WHERE cleared_at IS NULL ORDER BY created_at DESC, id DESC').map(toBlock);
|
|
67
|
+
}
|
|
68
|
+
/** Block history of a workflow (optionally one scope), newest first. */
|
|
69
|
+
listForWorkflow(workflowId, options = {}) {
|
|
70
|
+
if (options.itemKey !== undefined) {
|
|
71
|
+
return all(this.db, `SELECT * FROM blocks WHERE workflow_id = ? AND COALESCE(source_item_key, '') = ?
|
|
72
|
+
ORDER BY created_at DESC, id DESC LIMIT ?`, [workflowId, blockScopeKey(options.itemKey), options.limit ?? 100]).map(toBlock);
|
|
73
|
+
}
|
|
74
|
+
return all(this.db, 'SELECT * FROM blocks WHERE workflow_id = ? ORDER BY created_at DESC, id DESC LIMIT ?', [
|
|
75
|
+
workflowId,
|
|
76
|
+
options.limit ?? 100,
|
|
77
|
+
]).map(toBlock);
|
|
78
|
+
}
|
|
79
|
+
/** IDs of runs whose technical failures were counted in the block's period (for the GUI). */
|
|
80
|
+
affectedRunIds(blockId) {
|
|
81
|
+
const block = this.require(blockId);
|
|
82
|
+
return all(this.db, `SELECT id FROM runs WHERE workflow_id = ? AND COALESCE(source_item_key, '') = ?
|
|
83
|
+
AND block_generation = ? AND counted_technical_failure = 1
|
|
84
|
+
ORDER BY created_at ASC, id ASC`, [block.workflowId, blockScopeKey(block.itemKey), block.generation]).map((r) => String(r.id));
|
|
85
|
+
}
|
|
86
|
+
/** Consecutive technical failures in the current counting period. */
|
|
87
|
+
consecutiveTechnicalFailures(workflowId, itemKey) {
|
|
88
|
+
return this.counter(workflowId, itemKey).consecutive;
|
|
89
|
+
}
|
|
90
|
+
/** Counter state (`{0, 0}` if never touched). */
|
|
91
|
+
counter(workflowId, itemKey) {
|
|
92
|
+
const row = get(this.db, 'SELECT consecutive, generation FROM failure_counters WHERE workflow_id = ? AND scope_key = ?', [
|
|
93
|
+
workflowId,
|
|
94
|
+
blockScopeKey(itemKey),
|
|
95
|
+
]);
|
|
96
|
+
return { consecutive: Number(row?.consecutive ?? 0), generation: Number(row?.generation ?? 0) };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Internal: apply a run outcome to the counter. Must be called inside the
|
|
100
|
+
* transaction that moves the run into `finalizing`.
|
|
101
|
+
*/
|
|
102
|
+
applyOutcome(workflowId, itemKey, effect) {
|
|
103
|
+
this.ensureCounter(workflowId, itemKey);
|
|
104
|
+
const now = this.clock.now().toISOString();
|
|
105
|
+
if (effect === 'increment') {
|
|
106
|
+
run(this.db, 'UPDATE failure_counters SET consecutive = consecutive + 1, updated_at = ? WHERE workflow_id = ? AND scope_key = ?', [now, workflowId, blockScopeKey(itemKey)]);
|
|
107
|
+
}
|
|
108
|
+
else if (effect === 'reset') {
|
|
109
|
+
// A success ends the counting period: next failures belong to a new generation,
|
|
110
|
+
// so `affectedRunIds` of a later block never includes failures before the success.
|
|
111
|
+
run(this.db, 'UPDATE failure_counters SET consecutive = 0, generation = generation + 1, updated_at = ? WHERE workflow_id = ? AND scope_key = ?', [now, workflowId, blockScopeKey(itemKey)]);
|
|
112
|
+
}
|
|
113
|
+
return this.counter(workflowId, itemKey);
|
|
114
|
+
}
|
|
115
|
+
ensureCounter(workflowId, itemKey) {
|
|
116
|
+
run(this.db, `INSERT INTO failure_counters (workflow_id, scope_key, consecutive, generation, updated_at)
|
|
117
|
+
VALUES (?, ?, 0, 0, ?) ON CONFLICT (workflow_id, scope_key) DO NOTHING`, [workflowId, blockScopeKey(itemKey), this.clock.now().toISOString()]);
|
|
118
|
+
}
|
|
119
|
+
require(id) {
|
|
120
|
+
const block = this.get(id);
|
|
121
|
+
if (!block)
|
|
122
|
+
throw new NotFoundError('block', id);
|
|
123
|
+
return block;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function toBlock(row) {
|
|
127
|
+
return {
|
|
128
|
+
id: Number(row.id),
|
|
129
|
+
workflowId: String(row.workflow_id),
|
|
130
|
+
itemKey: str(row.source_item_key),
|
|
131
|
+
reason: String(row.reason),
|
|
132
|
+
runId: str(row.run_id),
|
|
133
|
+
failureCount: Number(row.failure_count),
|
|
134
|
+
generation: Number(row.generation),
|
|
135
|
+
createdAt: String(row.created_at),
|
|
136
|
+
clearedAt: str(row.cleared_at),
|
|
137
|
+
clearedBy: str(row.cleared_by),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=blocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.js","sourceRoot":"","sources":["../../src/persistence/blocks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron idempotency and diagnostics (Spec §5.5, §7): at most one firing
|
|
3
|
+
* record per `(workflow_id, scheduled_for)`.
|
|
4
|
+
*/
|
|
5
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import type { Clock } from '../core/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Decision for a cron slot, e.g. `started`, `skipped_missed`, `skipped_limit`,
|
|
9
|
+
* `skipped_condition`, `skipped_blocked`, `skipped_lock` (WI-09 owns the vocabulary).
|
|
10
|
+
*/
|
|
11
|
+
export type CronDecision = string;
|
|
12
|
+
/** A persisted cron firing. */
|
|
13
|
+
export interface CronFiringRecord {
|
|
14
|
+
workflowId: string;
|
|
15
|
+
/** Cron slot, ISO-8601 UTC. */
|
|
16
|
+
scheduledFor: string;
|
|
17
|
+
runId: string | null;
|
|
18
|
+
decision: CronDecision;
|
|
19
|
+
reason: string | null;
|
|
20
|
+
recordedAt: string;
|
|
21
|
+
}
|
|
22
|
+
/** Input for {@link CronFiringRepository.record}. */
|
|
23
|
+
export interface RecordCronFiringInput {
|
|
24
|
+
workflowId: string;
|
|
25
|
+
scheduledFor: Date | string;
|
|
26
|
+
decision: CronDecision;
|
|
27
|
+
runId?: string | null;
|
|
28
|
+
reason?: string | null;
|
|
29
|
+
}
|
|
30
|
+
/** Repository for `cron_firings`. */
|
|
31
|
+
export declare class CronFiringRepository {
|
|
32
|
+
private readonly db;
|
|
33
|
+
private readonly clock;
|
|
34
|
+
constructor(db: DatabaseSync, clock: Clock);
|
|
35
|
+
/**
|
|
36
|
+
* Record a firing if none exists for the slot. Returns `inserted: false`
|
|
37
|
+
* and the existing record if the slot was already handled (no duplicate run).
|
|
38
|
+
*/
|
|
39
|
+
record(input: RecordCronFiringInput): {
|
|
40
|
+
inserted: boolean;
|
|
41
|
+
firing: CronFiringRecord;
|
|
42
|
+
};
|
|
43
|
+
/** Update decision/run of an existing slot (e.g. `started` + run ID after reservation). */
|
|
44
|
+
update(workflowId: string, scheduledFor: Date | string, change: {
|
|
45
|
+
decision?: CronDecision;
|
|
46
|
+
runId?: string | null;
|
|
47
|
+
reason?: string | null;
|
|
48
|
+
}): CronFiringRecord;
|
|
49
|
+
/** Firing for a slot or `null`. */
|
|
50
|
+
get(workflowId: string, scheduledFor: Date | string): CronFiringRecord | null;
|
|
51
|
+
/** Latest firing (by slot) of a workflow or `null`. */
|
|
52
|
+
latest(workflowId: string): CronFiringRecord | null;
|
|
53
|
+
/** Firings of a workflow, newest slot first. */
|
|
54
|
+
listForWorkflow(workflowId: string, limit?: number): CronFiringRecord[];
|
|
55
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NotFoundError } from './errors.js';
|
|
2
|
+
import { all, get, iso, run, str } from './sqlite.js';
|
|
3
|
+
/** Repository for `cron_firings`. */
|
|
4
|
+
export class CronFiringRepository {
|
|
5
|
+
db;
|
|
6
|
+
clock;
|
|
7
|
+
constructor(db, clock) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this.clock = clock;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Record a firing if none exists for the slot. Returns `inserted: false`
|
|
13
|
+
* and the existing record if the slot was already handled (no duplicate run).
|
|
14
|
+
*/
|
|
15
|
+
record(input) {
|
|
16
|
+
const slot = iso(input.scheduledFor);
|
|
17
|
+
const res = run(this.db, `INSERT INTO cron_firings (workflow_id, scheduled_for, run_id, decision, reason, recorded_at)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (workflow_id, scheduled_for) DO NOTHING`, [input.workflowId, slot, input.runId ?? null, input.decision, input.reason ?? null, this.clock.now().toISOString()]);
|
|
19
|
+
const firing = this.get(input.workflowId, slot);
|
|
20
|
+
if (!firing)
|
|
21
|
+
throw new NotFoundError('cron_firing', `${input.workflowId}@${slot}`);
|
|
22
|
+
return { inserted: res.changes === 1, firing };
|
|
23
|
+
}
|
|
24
|
+
/** Update decision/run of an existing slot (e.g. `started` + run ID after reservation). */
|
|
25
|
+
update(workflowId, scheduledFor, change) {
|
|
26
|
+
const slot = iso(scheduledFor);
|
|
27
|
+
const current = this.get(workflowId, slot);
|
|
28
|
+
if (!current)
|
|
29
|
+
throw new NotFoundError('cron_firing', `${workflowId}@${slot}`);
|
|
30
|
+
run(this.db, 'UPDATE cron_firings SET decision = ?, run_id = ?, reason = ? WHERE workflow_id = ? AND scheduled_for = ?', [
|
|
31
|
+
change.decision ?? current.decision,
|
|
32
|
+
change.runId !== undefined ? change.runId : current.runId,
|
|
33
|
+
change.reason !== undefined ? change.reason : current.reason,
|
|
34
|
+
workflowId,
|
|
35
|
+
slot,
|
|
36
|
+
]);
|
|
37
|
+
return this.get(workflowId, slot);
|
|
38
|
+
}
|
|
39
|
+
/** Firing for a slot or `null`. */
|
|
40
|
+
get(workflowId, scheduledFor) {
|
|
41
|
+
const row = get(this.db, 'SELECT * FROM cron_firings WHERE workflow_id = ? AND scheduled_for = ?', [
|
|
42
|
+
workflowId,
|
|
43
|
+
iso(scheduledFor),
|
|
44
|
+
]);
|
|
45
|
+
return row ? toFiring(row) : null;
|
|
46
|
+
}
|
|
47
|
+
/** Latest firing (by slot) of a workflow or `null`. */
|
|
48
|
+
latest(workflowId) {
|
|
49
|
+
const row = get(this.db, 'SELECT * FROM cron_firings WHERE workflow_id = ? ORDER BY scheduled_for DESC LIMIT 1', [workflowId]);
|
|
50
|
+
return row ? toFiring(row) : null;
|
|
51
|
+
}
|
|
52
|
+
/** Firings of a workflow, newest slot first. */
|
|
53
|
+
listForWorkflow(workflowId, limit = 100) {
|
|
54
|
+
return all(this.db, 'SELECT * FROM cron_firings WHERE workflow_id = ? ORDER BY scheduled_for DESC LIMIT ?', [
|
|
55
|
+
workflowId,
|
|
56
|
+
limit,
|
|
57
|
+
]).map(toFiring);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function toFiring(row) {
|
|
61
|
+
return {
|
|
62
|
+
workflowId: String(row.workflow_id),
|
|
63
|
+
scheduledFor: String(row.scheduled_for),
|
|
64
|
+
runId: str(row.run_id),
|
|
65
|
+
decision: String(row.decision),
|
|
66
|
+
reason: str(row.reason),
|
|
67
|
+
recordedAt: String(row.recorded_at),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=cron-firings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-firings.js","sourceRoot":"","sources":["../../src/persistence/cron-firings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { type Clock, type Logger } from '../core/index.js';
|
|
3
|
+
import { type Migration } from './schema.js';
|
|
4
|
+
/** Default SQLite busy timeout (ms). */
|
|
5
|
+
export declare const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
6
|
+
/** Options for {@link openDatabase}. */
|
|
7
|
+
export interface OpenDatabaseOptions {
|
|
8
|
+
clock?: Clock;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
/** Migrations to apply (default: built-in {@link MIGRATIONS}; tests may append artificial ones). */
|
|
11
|
+
migrations?: readonly Migration[];
|
|
12
|
+
busyTimeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Outcome of applying migrations. */
|
|
15
|
+
export interface MigrationResult {
|
|
16
|
+
/** Schema version before opening (0 = empty database). */
|
|
17
|
+
fromVersion: number;
|
|
18
|
+
/** Schema version after migrating. */
|
|
19
|
+
toVersion: number;
|
|
20
|
+
/** Versions applied during this open. */
|
|
21
|
+
applied: number[];
|
|
22
|
+
/** Backup written before migrating, if any. */
|
|
23
|
+
backupPath: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** An opened, migrated database. */
|
|
26
|
+
export interface OpenedDatabase {
|
|
27
|
+
db: DatabaseSync;
|
|
28
|
+
/** Absolute path of the database file. */
|
|
29
|
+
path: string;
|
|
30
|
+
migration: MigrationResult;
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Open (creating if needed) `<dataDir>/adispatchd.sqlite`, configure it
|
|
35
|
+
* (foreign keys, WAL, busy timeout) and apply pending migrations. If the
|
|
36
|
+
* database already existed and migrations are pending, it is first backed up
|
|
37
|
+
* to `<dataDir>/backups/adispatchd-<version>-<ts>.sqlite`.
|
|
38
|
+
*
|
|
39
|
+
* The caller should hold the data-dir instance lock ({@link acquireInstanceLock}) first.
|
|
40
|
+
*/
|
|
41
|
+
export declare function openDatabase(dataDir: string, options?: OpenDatabaseOptions): OpenedDatabase;
|
|
42
|
+
/** Apply connection pragmas. */
|
|
43
|
+
export declare function configureConnection(db: DatabaseSync, busyTimeoutMs?: number): void;
|
|
44
|
+
/** Options for {@link migrate}. */
|
|
45
|
+
export interface MigrateOptions {
|
|
46
|
+
migrations: readonly Migration[];
|
|
47
|
+
/** Directory for the pre-migration backup; `null` = no backup (fresh database). */
|
|
48
|
+
backupDir: string | null;
|
|
49
|
+
clock: Clock;
|
|
50
|
+
logger: Logger;
|
|
51
|
+
}
|
|
52
|
+
/** Current schema version (0 if `schema_migrations` does not exist yet). */
|
|
53
|
+
export declare function schemaVersion(db: DatabaseSync): number;
|
|
54
|
+
/** Apply all pending migrations, each in its own transaction. Idempotent. */
|
|
55
|
+
export declare function migrate(db: DatabaseSync, options: MigrateOptions): MigrationResult;
|
|
56
|
+
/**
|
|
57
|
+
* Write a consistent copy of the database (`VACUUM INTO`, safe in WAL mode)
|
|
58
|
+
* to `<backupDir>/adispatchd-<version>-<ts>.sqlite`. Returns its path.
|
|
59
|
+
*/
|
|
60
|
+
export declare function backupDatabase(db: DatabaseSync, backupDir: string, version: number, clock: Clock): string;
|