@strav/durable 0.4.31 → 1.0.0-alpha.8
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/package.json +18 -37
- package/src/define_durable.ts +29 -0
- package/src/durable_advance_job.ts +38 -0
- package/src/durable_compensate_job.ts +33 -0
- package/src/durable_error.ts +38 -0
- package/src/durable_provider.ts +91 -0
- package/src/durable_runner.ts +395 -0
- package/src/durable_workflow.ts +97 -0
- package/src/index.ts +25 -26
- package/src/journal_schema.ts +53 -0
- package/src/runs_schema.ts +38 -0
- package/src/types.ts +58 -198
- package/src/workflow_registry.ts +49 -0
- package/CHANGELOG.md +0 -26
- package/src/builder.ts +0 -158
- package/src/config.ts +0 -36
- package/src/durable.ts +0 -268
- package/src/engine/advance_handler.ts +0 -154
- package/src/engine/compensate_handler.ts +0 -70
- package/src/engine/compensation_driver.ts +0 -61
- package/src/engine/context.ts +0 -36
- package/src/engine/enqueue.ts +0 -62
- package/src/engine/finalize.ts +0 -111
- package/src/engine/index.ts +0 -20
- package/src/engine/run_store.ts +0 -42
- package/src/engine/step_driver.ts +0 -291
- package/src/engine/suspended_run.ts +0 -24
- package/src/errors.ts +0 -21
- package/src/helpers.ts +0 -16
- package/src/models/index.ts +0 -3
- package/src/models/journal.ts +0 -54
- package/src/models/run_machine.ts +0 -39
- package/src/models/workflow_run.ts +0 -36
- package/src/providers/durable_provider.ts +0 -31
- package/src/providers/index.ts +0 -2
- package/src/registry.ts +0 -35
- package/src/schema.ts +0 -70
- package/src/util.ts +0 -25
- package/tsconfig.json +0 -5
package/src/types.ts
CHANGED
|
@@ -1,216 +1,76 @@
|
|
|
1
|
-
import type { WorkflowContext } from '@strav/workflow'
|
|
2
|
-
|
|
3
|
-
// ── Run + journal status ────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
/** Lifecycle status of a durable workflow run. */
|
|
6
|
-
export type RunStatus =
|
|
7
|
-
| 'pending'
|
|
8
|
-
| 'running'
|
|
9
|
-
| 'suspended'
|
|
10
|
-
| 'compensating'
|
|
11
|
-
| 'completed'
|
|
12
|
-
| 'failed'
|
|
13
|
-
| 'canceled'
|
|
14
|
-
|
|
15
|
-
/** Status of a single journal entry. */
|
|
16
|
-
export type JournalStatus = 'completed' | 'failed'
|
|
17
|
-
|
|
18
|
-
// ── Durable context ─────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
1
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
2
|
+
* Public types for durable execution.
|
|
3
|
+
*
|
|
4
|
+
* A durable workflow is a *named*, registered definition: handlers are
|
|
5
|
+
* keyed by step name so the runner can re-enter them across processes
|
|
6
|
+
* after a crash. Apps don't pass closures into `start()` — they pass
|
|
7
|
+
* a workflow name + input.
|
|
8
|
+
*
|
|
9
|
+
* `DurableContext` is what each step handler receives. `results` is the
|
|
10
|
+
* accumulated typed return of every prior step; `runId` lets apps log
|
|
11
|
+
* + correlate with the durable record; `attempt` is 1-based and lets
|
|
12
|
+
* apps switch behavior on retry (e.g. tighten timeouts).
|
|
13
|
+
*
|
|
14
|
+
* `RunSnapshot` is what `DurableRunner.find(runId)` returns — the
|
|
15
|
+
* shape app code uses to poll a run from outside (UI status pages, the
|
|
16
|
+
* `durable:status` CLI command in a later slice).
|
|
24
17
|
*/
|
|
25
|
-
export interface DurableContext extends WorkflowContext {
|
|
26
|
-
/** The durable run id — stable across crashes/restarts. */
|
|
27
|
-
runId: number
|
|
28
|
-
/** The current retry attempt for the executing step (1-based). */
|
|
29
|
-
attempt: number
|
|
30
|
-
/** The name of the step currently executing. */
|
|
31
|
-
stepName: string
|
|
32
|
-
/** Read a completed step (or signal) result by name. */
|
|
33
|
-
signal<T = unknown>(name: string): T | undefined
|
|
34
|
-
/**
|
|
35
|
-
* The payload passed to the most recent `Durable.resume()` for this run.
|
|
36
|
-
* Used by a brain-agent step to resume a journaled `SuspendedRun`.
|
|
37
|
-
*/
|
|
38
|
-
resumeData<T = unknown>(): T | undefined
|
|
39
|
-
}
|
|
40
18
|
|
|
41
|
-
|
|
19
|
+
export type RunStatus = 'pending' | 'running' | 'compensating' | 'completed' | 'failed'
|
|
42
20
|
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
export interface DurableContext {
|
|
22
|
+
/** Workflow input — the object passed to `DurableRunner.start(name, input)`. */
|
|
23
|
+
readonly input: Record<string, unknown>
|
|
24
|
+
/** Results from every prior step, keyed by step name. */
|
|
25
|
+
readonly results: Record<string, unknown>
|
|
26
|
+
/** Durable run id (the row PK). Useful for logging / correlation. */
|
|
27
|
+
readonly runId: string
|
|
28
|
+
/** 1-based retry counter for this step. `1` on first run. */
|
|
29
|
+
readonly attempt: number
|
|
30
|
+
}
|
|
45
31
|
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
export interface RunSnapshot {
|
|
33
|
+
id: string
|
|
34
|
+
workflowName: string
|
|
35
|
+
status: RunStatus
|
|
36
|
+
input: Record<string, unknown>
|
|
37
|
+
results: Record<string, unknown>
|
|
38
|
+
currentStep: number
|
|
39
|
+
result: Record<string, unknown> | null
|
|
40
|
+
error: string | null
|
|
41
|
+
createdAt: Date
|
|
42
|
+
updatedAt: Date
|
|
43
|
+
}
|
|
48
44
|
|
|
49
|
-
/**
|
|
50
|
-
export type
|
|
45
|
+
/** Step handler — receives the durable context, returns the step's result. */
|
|
46
|
+
export type DurableStepHandler = (ctx: DurableContext) => Promise<unknown>
|
|
51
47
|
|
|
52
|
-
/**
|
|
48
|
+
/** Saga rollback handler. Same context shape; return ignored. */
|
|
53
49
|
export type DurableCompensator = (ctx: DurableContext) => Promise<void>
|
|
54
50
|
|
|
55
|
-
/** A single entry in a parallel step. */
|
|
56
|
-
export interface DurableParallelEntry {
|
|
57
|
-
name: string
|
|
58
|
-
handler: DurableStepHandler
|
|
59
|
-
compensate?: DurableCompensator
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ── Step options ────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
51
|
export interface DurableStepOptions {
|
|
65
|
-
/** Saga compensation — run in reverse order if a later step fails. */
|
|
66
52
|
compensate?: DurableCompensator
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
until?: (result: unknown, iteration: number) => boolean
|
|
76
|
-
feedback?: (result: unknown) => unknown
|
|
77
|
-
mapInput?: (ctx: DurableContext) => unknown
|
|
78
|
-
maxRetries?: number
|
|
79
|
-
retryBackoff?: 'exponential' | 'linear'
|
|
53
|
+
/** Hard cap on attempts. Default `3`. Includes the first attempt. */
|
|
54
|
+
maxAttempts?: number
|
|
55
|
+
/**
|
|
56
|
+
* Backoff function — input is the attempt number that just failed
|
|
57
|
+
* (1-based), output is the delay in seconds before the next attempt.
|
|
58
|
+
* Default: exponential `2 ** attempt` capped at 60s.
|
|
59
|
+
*/
|
|
60
|
+
backoff?: (failedAttempt: number) => number
|
|
80
61
|
}
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Internal step record. Apps don't construct this directly — the
|
|
65
|
+
* `DurableWorkflow` builder pushes one per `.step()` call. Exported
|
|
66
|
+
* so tests and introspection tools can read the plan from
|
|
67
|
+
* `workflow.steps`.
|
|
68
|
+
*/
|
|
69
|
+
export interface DurableStep {
|
|
85
70
|
type: 'step'
|
|
86
71
|
name: string
|
|
87
72
|
handler: DurableStepHandler
|
|
88
73
|
compensate?: DurableCompensator
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface ParallelStep {
|
|
94
|
-
type: 'parallel'
|
|
95
|
-
name: string
|
|
96
|
-
entries: DurableParallelEntry[]
|
|
97
|
-
maxRetries: number
|
|
98
|
-
retryBackoff: 'exponential' | 'linear'
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface RouteStep {
|
|
102
|
-
type: 'route'
|
|
103
|
-
name: string
|
|
104
|
-
resolver: DurableRouteResolver
|
|
105
|
-
branches: Record<string, DurableStepHandler>
|
|
106
|
-
maxRetries: number
|
|
107
|
-
retryBackoff: 'exponential' | 'linear'
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface LoopStep {
|
|
111
|
-
type: 'loop'
|
|
112
|
-
name: string
|
|
113
|
-
handler: DurableLoopHandler
|
|
114
|
-
maxIterations: number
|
|
115
|
-
until?: (result: unknown, iteration: number) => boolean
|
|
116
|
-
feedback?: (result: unknown) => unknown
|
|
117
|
-
mapInput?: (ctx: DurableContext) => unknown
|
|
118
|
-
maxRetries: number
|
|
119
|
-
retryBackoff: 'exponential' | 'linear'
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Durable timer — suspends the run and resumes after `duration`. */
|
|
123
|
-
export interface SleepStep {
|
|
124
|
-
type: 'sleep'
|
|
125
|
-
name: string
|
|
126
|
-
duration: number | Date
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Human-in-the-loop pause — suspends until `Durable.resume()` delivers the signal. */
|
|
130
|
-
export interface SignalStep {
|
|
131
|
-
type: 'signal'
|
|
132
|
-
name: string
|
|
133
|
-
signal: string
|
|
134
|
-
timeout?: number
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Spawn an independently durable child workflow and wait for it. */
|
|
138
|
-
export interface ChildStep {
|
|
139
|
-
type: 'child'
|
|
140
|
-
name: string
|
|
141
|
-
childName: string
|
|
142
|
-
mapInput?: (ctx: DurableContext) => Record<string, unknown>
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export type DurableStep =
|
|
146
|
-
| SequentialStep
|
|
147
|
-
| ParallelStep
|
|
148
|
-
| RouteStep
|
|
149
|
-
| LoopStep
|
|
150
|
-
| SleepStep
|
|
151
|
-
| SignalStep
|
|
152
|
-
| ChildStep
|
|
153
|
-
|
|
154
|
-
// ── Public result shapes ────────────────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
export interface StartResult {
|
|
157
|
-
runId: number
|
|
158
|
-
status: RunStatus
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface ResumeResult {
|
|
162
|
-
/** Whether the signal was accepted (run suspended on a matching signal). */
|
|
163
|
-
accepted: boolean
|
|
164
|
-
status: RunStatus
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export interface RunStatusSnapshot {
|
|
168
|
-
runId: number
|
|
169
|
-
workflowName: string
|
|
170
|
-
status: RunStatus
|
|
171
|
-
currentStep: number
|
|
172
|
-
totalSteps: number
|
|
173
|
-
awaitingSignal: string | null
|
|
174
|
-
wakeAt: string | null
|
|
175
|
-
results: Record<string, unknown>
|
|
176
|
-
error: string | null
|
|
177
|
-
parentRunId: number | null
|
|
178
|
-
children: RunStatusSnapshot[]
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── Internal records ────────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
/** A hydrated `_strav_workflow_runs` row (snake_case columns → camelCase). */
|
|
184
|
-
export interface RunRow {
|
|
185
|
-
id: number
|
|
186
|
-
workflowName: string
|
|
187
|
-
input: Record<string, unknown>
|
|
188
|
-
status: RunStatus
|
|
189
|
-
state: Record<string, unknown>
|
|
190
|
-
currentStep: number
|
|
191
|
-
compensationCursor: number | null
|
|
192
|
-
parentRunId: number | null
|
|
193
|
-
parentStepId: string | null
|
|
194
|
-
awaitingSignal: string | null
|
|
195
|
-
wakeAt: Date | null
|
|
196
|
-
error: string | null
|
|
197
|
-
result: Record<string, unknown> | null
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** A hydrated `_strav_workflow_journal` row. */
|
|
201
|
-
export interface JournalRecord {
|
|
202
|
-
stepId: string
|
|
203
|
-
status: JournalStatus
|
|
204
|
-
result: unknown
|
|
205
|
-
error: string | null
|
|
206
|
-
attempt: number
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** A pending journal write produced by the step driver. */
|
|
210
|
-
export interface JournalWrite {
|
|
211
|
-
stepId: string
|
|
212
|
-
status: JournalStatus
|
|
213
|
-
result?: unknown
|
|
214
|
-
error?: string
|
|
215
|
-
attempt: number
|
|
74
|
+
maxAttempts: number
|
|
75
|
+
backoff: (failedAttempt: number) => number
|
|
216
76
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkflowRegistry` — name → `DurableWorkflow` lookup used by the
|
|
3
|
+
* advance / compensate handlers.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `JobRegistry` from `@strav/queue`: explicit registration on
|
|
6
|
+
* an instance, no module-singleton state. Each app builds one
|
|
7
|
+
* registry, registers its workflows, and hands it to the runner.
|
|
8
|
+
*
|
|
9
|
+
* Duplicate-name registration throws — the runner journals steps by
|
|
10
|
+
* (run, step name), so silently shadowing a registered workflow with
|
|
11
|
+
* a different shape would corrupt replays.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { DurableError, WorkflowNotRegisteredError } from './durable_error.ts'
|
|
15
|
+
import type { DurableWorkflow } from './durable_workflow.ts'
|
|
16
|
+
|
|
17
|
+
export class WorkflowRegistry {
|
|
18
|
+
private readonly workflows = new Map<string, DurableWorkflow>()
|
|
19
|
+
|
|
20
|
+
register(workflow: DurableWorkflow): this {
|
|
21
|
+
if (this.workflows.has(workflow.name)) {
|
|
22
|
+
throw new DurableError(
|
|
23
|
+
`WorkflowRegistry: workflow "${workflow.name}" is already registered. Refusing to shadow — re-name the workflow or restart the registry.`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
this.workflows.set(workflow.name, workflow)
|
|
27
|
+
return this
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
registerAll(workflows: readonly DurableWorkflow[]): this {
|
|
31
|
+
for (const wf of workflows) this.register(wf)
|
|
32
|
+
return this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Look up a workflow by name. Throws when missing. */
|
|
36
|
+
get(name: string): DurableWorkflow {
|
|
37
|
+
const wf = this.workflows.get(name)
|
|
38
|
+
if (!wf) throw new WorkflowNotRegisteredError(name, [...this.workflows.keys()])
|
|
39
|
+
return wf
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
has(name: string): boolean {
|
|
43
|
+
return this.workflows.has(name)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
names(): string[] {
|
|
47
|
+
return [...this.workflows.keys()]
|
|
48
|
+
}
|
|
49
|
+
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# @strav/durable
|
|
2
|
-
|
|
3
|
-
## 0.4.26
|
|
4
|
-
|
|
5
|
-
Initial release — durable, crash-resumable workflow execution.
|
|
6
|
-
|
|
7
|
-
- `durable(name)` builder mirroring the `@strav/workflow` authoring API
|
|
8
|
-
(`.step` / `.parallel` / `.route` / `.loop` + `compensate`), plus durable-only
|
|
9
|
-
step types `.sleep`, `.waitForSignal`, and `.childWorkflow`.
|
|
10
|
-
- Explicit-journal execution model (DBOS/Inngest style, no determinism sandbox):
|
|
11
|
-
every step is checkpointed to `_strav_workflow_journal`; `UNIQUE (run_id, step_id)`
|
|
12
|
-
makes redelivery idempotent.
|
|
13
|
-
- Queue-driven progression — one top-level step = one `durable:advance`
|
|
14
|
-
`@strav/queue` job; the journal write + run-row update + next-job enqueue
|
|
15
|
-
commit in a single transaction, so the engine inherits crash-safety from the
|
|
16
|
-
queue. A workflow killed mid-execution resumes from the first incomplete step.
|
|
17
|
-
- Suspend/resume: `waitForSignal` parks a run for an unbounded time holding no
|
|
18
|
-
process; `Durable.resume()` delivers the signal.
|
|
19
|
-
- Durable timers via `.sleep` (survives process restarts).
|
|
20
|
-
- Independently durable, parent-linked child workflows.
|
|
21
|
-
- Journaled saga compensation — rollback is itself crash-safe.
|
|
22
|
-
- Composes with `@strav/brain` — a step returning a `SuspendedRun` suspends the
|
|
23
|
-
run; resume re-enters the step (duck-typed, no `@strav/brain` dependency).
|
|
24
|
-
- `WorkflowRun` — a `stateful()` ORM model over the run record, with a
|
|
25
|
-
`@strav/machine` run-status lifecycle.
|
|
26
|
-
- `Durable.start` / `resume` / `status` / `list` / `cancel` / `recover`.
|
package/src/builder.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { DurableError } from './errors.ts'
|
|
2
|
-
import type {
|
|
3
|
-
DurableStep,
|
|
4
|
-
DurableStepHandler,
|
|
5
|
-
DurableLoopHandler,
|
|
6
|
-
DurableRouteResolver,
|
|
7
|
-
DurableParallelEntry,
|
|
8
|
-
DurableStepOptions,
|
|
9
|
-
DurableLoopOptions,
|
|
10
|
-
} from './types.ts'
|
|
11
|
-
|
|
12
|
-
const DEFAULT_MAX_RETRIES = 3
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Durable workflow builder.
|
|
16
|
-
*
|
|
17
|
-
* Mirrors the `@strav/workflow` authoring API (`.step` / `.parallel` /
|
|
18
|
-
* `.route` / `.loop` + `compensate`) so plain workflow code is copy-paste
|
|
19
|
-
* portable, and adds durable-only step types: `.sleep`, `.waitForSignal`,
|
|
20
|
-
* and `.childWorkflow`.
|
|
21
|
-
*
|
|
22
|
-
* Built workflows are flat ordered step lists — there is no re-runnable
|
|
23
|
-
* function body, so durability needs no determinism sandbox.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* durable('milestone')
|
|
27
|
-
* .step('discover', async (ctx) => discover(ctx.input))
|
|
28
|
-
* .parallel('build', [
|
|
29
|
-
* { name: 'api', handler: async (ctx) => buildApi(ctx) },
|
|
30
|
-
* { name: 'ui', handler: async (ctx) => buildUi(ctx) },
|
|
31
|
-
* ])
|
|
32
|
-
* .waitForSignal('signoff', 'founder-signoff')
|
|
33
|
-
* .step('ship', async (ctx) => ship(ctx))
|
|
34
|
-
*/
|
|
35
|
-
export class DurableWorkflow {
|
|
36
|
-
readonly name: string
|
|
37
|
-
private readonly _steps: DurableStep[] = []
|
|
38
|
-
private readonly _names = new Set<string>()
|
|
39
|
-
|
|
40
|
-
constructor(name: string) {
|
|
41
|
-
this.name = name
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The ordered, immutable step list. */
|
|
45
|
-
get steps(): readonly DurableStep[] {
|
|
46
|
-
return this._steps
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private claim(name: string): void {
|
|
50
|
-
if (this._names.has(name)) {
|
|
51
|
-
throw new DurableError(
|
|
52
|
-
`Duplicate step name "${name}" in durable workflow "${this.name}".`
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
this._names.add(name)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Add a sequential step. Its result is stored in `ctx.results[name]`. */
|
|
59
|
-
step(name: string, handler: DurableStepHandler, options?: DurableStepOptions): this {
|
|
60
|
-
this.claim(name)
|
|
61
|
-
this._steps.push({
|
|
62
|
-
type: 'step',
|
|
63
|
-
name,
|
|
64
|
-
handler,
|
|
65
|
-
compensate: options?.compensate,
|
|
66
|
-
maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
67
|
-
retryBackoff: options?.retryBackoff ?? 'exponential',
|
|
68
|
-
})
|
|
69
|
-
return this
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Run multiple handlers in parallel; each result is stored under its entry name. */
|
|
73
|
-
parallel(
|
|
74
|
-
name: string,
|
|
75
|
-
entries: DurableParallelEntry[],
|
|
76
|
-
options?: Pick<DurableStepOptions, 'maxRetries' | 'retryBackoff'>
|
|
77
|
-
): this {
|
|
78
|
-
this.claim(name)
|
|
79
|
-
if (entries.length === 0) {
|
|
80
|
-
throw new DurableError(`Parallel step "${name}" must have at least one entry.`)
|
|
81
|
-
}
|
|
82
|
-
this._steps.push({
|
|
83
|
-
type: 'parallel',
|
|
84
|
-
name,
|
|
85
|
-
entries,
|
|
86
|
-
maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
87
|
-
retryBackoff: options?.retryBackoff ?? 'exponential',
|
|
88
|
-
})
|
|
89
|
-
return this
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Route to a branch based on a resolver's return value. */
|
|
93
|
-
route(
|
|
94
|
-
name: string,
|
|
95
|
-
resolver: DurableRouteResolver,
|
|
96
|
-
branches: Record<string, DurableStepHandler>,
|
|
97
|
-
options?: Pick<DurableStepOptions, 'maxRetries' | 'retryBackoff'>
|
|
98
|
-
): this {
|
|
99
|
-
this.claim(name)
|
|
100
|
-
this._steps.push({
|
|
101
|
-
type: 'route',
|
|
102
|
-
name,
|
|
103
|
-
resolver,
|
|
104
|
-
branches,
|
|
105
|
-
maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
106
|
-
retryBackoff: options?.retryBackoff ?? 'exponential',
|
|
107
|
-
})
|
|
108
|
-
return this
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Run a handler in a loop until a condition is met or max iterations reached. */
|
|
112
|
-
loop(name: string, handler: DurableLoopHandler, options: DurableLoopOptions): this {
|
|
113
|
-
this.claim(name)
|
|
114
|
-
this._steps.push({
|
|
115
|
-
type: 'loop',
|
|
116
|
-
name,
|
|
117
|
-
handler,
|
|
118
|
-
maxIterations: options.maxIterations,
|
|
119
|
-
until: options.until,
|
|
120
|
-
feedback: options.feedback,
|
|
121
|
-
mapInput: options.mapInput,
|
|
122
|
-
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
123
|
-
retryBackoff: options.retryBackoff ?? 'exponential',
|
|
124
|
-
})
|
|
125
|
-
return this
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Durable timer — suspend the run and resume after `duration` (ms or a Date). */
|
|
129
|
-
sleep(name: string, duration: number | Date): this {
|
|
130
|
-
this.claim(name)
|
|
131
|
-
this._steps.push({ type: 'sleep', name, duration })
|
|
132
|
-
return this
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Suspend the run until `Durable.resume(runId, signal, data)` is called.
|
|
137
|
-
* Holds no process while suspended — resumes exactly, even days later.
|
|
138
|
-
*/
|
|
139
|
-
waitForSignal(name: string, signal: string, options?: { timeout?: number }): this {
|
|
140
|
-
this.claim(name)
|
|
141
|
-
this._steps.push({ type: 'signal', name, signal, timeout: options?.timeout })
|
|
142
|
-
return this
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Spawn an independently durable child workflow and wait for it to finish.
|
|
147
|
-
* The child result is stored in `ctx.results[name]`.
|
|
148
|
-
*/
|
|
149
|
-
childWorkflow(
|
|
150
|
-
name: string,
|
|
151
|
-
childName: string,
|
|
152
|
-
mapInput?: (ctx: import('./types.ts').DurableContext) => Record<string, unknown>
|
|
153
|
-
): this {
|
|
154
|
-
this.claim(name)
|
|
155
|
-
this._steps.push({ type: 'child', name, childName, mapInput })
|
|
156
|
-
return this
|
|
157
|
-
}
|
|
158
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/** Engine-wide configuration for `@strav/durable`. */
|
|
2
|
-
export interface DurableConfig {
|
|
3
|
-
/**
|
|
4
|
-
* The `@strav/queue` queue name durable jobs are dispatched on. Run a
|
|
5
|
-
* `Worker({ queue })` on this name to process durable workflows. Keeping
|
|
6
|
-
* it separate from app jobs lets durable work be scaled independently.
|
|
7
|
-
*/
|
|
8
|
-
queue: string
|
|
9
|
-
/**
|
|
10
|
-
* Per-job timeout (ms) for `durable:advance` / `durable:compensate` jobs.
|
|
11
|
-
* Must comfortably exceed the slowest single step (e.g. an LLM call).
|
|
12
|
-
*/
|
|
13
|
-
jobTimeout: number
|
|
14
|
-
/**
|
|
15
|
-
* Queue-level max attempts for durable jobs. This insures against *process
|
|
16
|
-
* crashes* only — application-level step retries are handled by the engine
|
|
17
|
-
* via the journal `attempt` count, independently of this.
|
|
18
|
-
*/
|
|
19
|
-
maxAttempts: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const config: DurableConfig = {
|
|
23
|
-
queue: 'durable',
|
|
24
|
-
jobTimeout: 600_000,
|
|
25
|
-
maxAttempts: 5,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Read the current durable engine configuration. */
|
|
29
|
-
export function getConfig(): DurableConfig {
|
|
30
|
-
return config
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Override durable engine configuration. Call before booting workers. */
|
|
34
|
-
export function configureDurable(patch: Partial<DurableConfig>): void {
|
|
35
|
-
Object.assign(config, patch)
|
|
36
|
-
}
|