@astrale-os/sdk 0.6.0-beta.40 → 0.6.0-beta.41
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/dist/application/workflow/durable.d.ts +25 -13
- package/dist/execution/workflows/attempts.d.ts +87 -0
- package/dist/execution/workflows/attempts.js +205 -0
- package/dist/execution/workflows/engine.d.ts +6 -12
- package/dist/execution/workflows/failure.d.ts +6 -13
- package/dist/execution/workflows/failure.js +9 -26
- package/dist/execution/workflows/index.d.ts +1 -1
- package/dist/execution/workflows/options.d.ts +21 -6
- package/dist/execution/workflows/options.js +28 -18
- package/dist/execution/workflows/run.d.ts +2 -0
- package/dist/execution/workflows/run.js +120 -76
- package/dist/execution/workflows/steps.d.ts +7 -1
- package/dist/execution/workflows/steps.js +32 -6
- package/dist/testing/durable/engine.d.ts +18 -11
- package/dist/testing/durable/engine.js +43 -38
- package/dist/testing/durable/index.d.ts +2 -2
- package/dist/testing/durable/index.js +1 -1
- package/dist/testing/durable/steps.js +9 -4
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +1 -1
- package/dist/tooling/linter/implementations/source/workflows.js +0 -1
- package/docs/durable-workflows.md +69 -42
- package/package.json +1 -1
package/dist/testing/index.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export { dataset, isDatasetReference } from './reference.js';
|
|
|
10
10
|
export type { DatasetReference } from './reference.js';
|
|
11
11
|
export { isTests, tests } from './tests.js';
|
|
12
12
|
export type { Tests, TestsInput } from './tests.js';
|
|
13
|
-
export {
|
|
14
|
-
export type {
|
|
13
|
+
export { createTestWorkflowEngine } from './durable/index.js';
|
|
14
|
+
export type { TestExecution, TestJournalEntry, TestStartOptions, TestStepCapabilities, TestWorkflowEngine, TestWorkflowEngineOptions, } from './durable/index.js';
|
package/dist/testing/index.js
CHANGED
|
@@ -6,4 +6,4 @@ export { DATASET_FORMAT, DATASET_VERSION, DatasetAdmissionError, decodeDataset,
|
|
|
6
6
|
export { loadDataset, loadDatasets } from './load.js';
|
|
7
7
|
export { dataset, isDatasetReference } from './reference.js';
|
|
8
8
|
export { isTests, tests } from './tests.js';
|
|
9
|
-
export {
|
|
9
|
+
export { createTestWorkflowEngine } from './durable/index.js';
|
|
@@ -6,7 +6,6 @@ const STEP_METHODS = new Set(['run', 'query', 'mutate']);
|
|
|
6
6
|
// Members only a durable Workflow context has: using one proves the Workflow durable.
|
|
7
7
|
const DURABLE_CONTEXT_MEMBERS = new Set([
|
|
8
8
|
'awaitSignal',
|
|
9
|
-
'awaitExecution',
|
|
10
9
|
'cancelled',
|
|
11
10
|
'exhausted',
|
|
12
11
|
'retryAfter',
|
|
@@ -50,9 +50,8 @@ export const remind = defineWorkflow<typeof schema>()(
|
|
|
50
50
|
| `awaitSignal(name)` | Waits for the value `Execution.signal({ name, value })` delivers once under `name`, sent before or during the wait. |
|
|
51
51
|
| `awaitSignal(name, { timeoutMs })` | Waits at most `timeoutMs`: `{ kind: 'signal', value }` or `{ kind: 'timeout' }`. |
|
|
52
52
|
| `cancelled(error)` | Whether `error` is the engine's cooperative cancellation; catch it to compensate. |
|
|
53
|
-
| `exhausted(error)` | Whether `error` is a Step that spent its retry budget; catch it to take another path. |
|
|
54
|
-
| `retryAfter(ms, cause?)` | The failure to throw inside a Step whose next attempt must wait `ms` (at most one hour), as an HTTP `Retry-After` asks. |
|
|
55
|
-
| `awaitExecution(ref)` | Waits durably for another Execution by journaled polling of its `result`. A poll has no Step budget: while the Kernel cannot answer, the run is retried until it pauses. |
|
|
53
|
+
| `exhausted(error)` | Whether `error` is a Step that spent its retry budget, or that the Kernel refused for good; catch it to take another path. |
|
|
54
|
+
| `retryAfter(ms, cause?)` | The failure to throw inside a Step whose next attempt must wait `ms` (at most one hour), as an HTTP `Retry-After` asks. It spends an attempt. |
|
|
56
55
|
| `error(code, details)` | Declared business errors, as for any Workflow; they become the `failed` outcome. |
|
|
57
56
|
|
|
58
57
|
A durable run acts as its Domain, never as its caller: the Workflow and its Steps have no `caller`,
|
|
@@ -82,10 +81,10 @@ This requires a Kernel that keeps Mutation receipts. An older Kernel refuses the
|
|
|
82
81
|
|
|
83
82
|
Step ids are unique within a run. Steps and waits run one after another: the engine follows one
|
|
84
83
|
awaited operation at a time, and loses track of a run that awaits several together through
|
|
85
|
-
`Promise.all`, `allSettled`, `race` or `any`. A Step, `sleep
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
`Promise.all`, `allSettled`, `race` or `any`. A Step, `sleep` or `awaitSignal` started while
|
|
85
|
+
another runs is therefore refused with a `TypeError`, which pauses the run as any error between
|
|
86
|
+
Steps does. Independent reads may share one Step, whose Kernel calls are ordinary calls that
|
|
87
|
+
`Promise.all` may combine:
|
|
89
88
|
|
|
90
89
|
```ts
|
|
91
90
|
const [invoice, reminders] = await step.run('read-invoice', ({ query }) =>
|
|
@@ -147,7 +146,7 @@ reads the output. Three changes it cannot see:
|
|
|
147
146
|
- A caller that ignores the output, such as a View that refreshes after the call, compiles unchanged
|
|
148
147
|
but refreshes before the Execution ends. Follow the Execution, and refresh when it ends.
|
|
149
148
|
|
|
150
|
-
`
|
|
149
|
+
`createTestWorkflowEngine` (see [Testing](#testing)) shows the first two: a refusal path ends
|
|
151
150
|
`STEP_EXHAUSTED`, with what the last attempt threw in `failure`, and code whose path differs on
|
|
152
151
|
replay fails with `JournalMismatch`.
|
|
153
152
|
|
|
@@ -192,16 +191,16 @@ receipt, even though its precondition no longer holds.
|
|
|
192
191
|
|
|
193
192
|
## Testing
|
|
194
193
|
|
|
195
|
-
`
|
|
196
|
-
receiver a Host serves; only the engine is simulated. Each Step journals its result, every
|
|
197
|
-
replays the journal, timers follow a virtual clock, signals are buffered, a failing Step is
|
|
198
|
-
within its budget and then fails into the Workflow, a run failing outside any Step is
|
|
199
|
-
pauses, and cancellation is cooperative.
|
|
194
|
+
`createTestWorkflowEngine` from `@astrale-os/sdk/testing` runs a durable Workflow in memory with
|
|
195
|
+
the receiver a Host serves; only the engine is simulated. Each Step journals its result, every
|
|
196
|
+
attempt replays the journal, timers follow a virtual clock, signals are buffered, a failing Step is
|
|
197
|
+
retried within its budget and then fails into the Workflow, a run failing outside any Step is
|
|
198
|
+
retried until it pauses, and cancellation is cooperative.
|
|
200
199
|
|
|
201
200
|
```ts
|
|
202
|
-
import {
|
|
201
|
+
import { createTestWorkflowEngine } from '@astrale-os/sdk/testing'
|
|
203
202
|
|
|
204
|
-
const engine =
|
|
203
|
+
const engine = createTestWorkflowEngine()
|
|
205
204
|
const execution = await engine.start(remind, {
|
|
206
205
|
schema: Billing, // the resolved Schema
|
|
207
206
|
input: { note: 'soon' },
|
|
@@ -232,10 +231,12 @@ await execution.replay() // the journal alone reaches the same ou
|
|
|
232
231
|
| `journal`, `attempts` | The recorded entries, each Step with the times it ran, and the attempts of the run. |
|
|
233
232
|
|
|
234
233
|
A Workflow whose code between Steps is not deterministic fails its next attempt with
|
|
235
|
-
`JournalMismatch` and pauses.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
`JournalMismatch` and pauses. A retry after an attempt that spent the Step's budget does not wait
|
|
235
|
+
out its backoff, but the backoff it skips, or the delay its `retryAfter` asks for, counts toward the
|
|
236
|
+
Step's `maxDurationMs`. A wait for the platform, after a Kernel refusal that prescribes a delay or an
|
|
237
|
+
unreachable Kernel, is a timer: the run is `waiting` until `advance` reaches it. Code should await only
|
|
238
|
+
Steps and the context's own operations; the engine counts a run as waiting once it waits on a timer or
|
|
239
|
+
a signal and nothing else runs.
|
|
239
240
|
|
|
240
241
|
## Signals
|
|
241
242
|
|
|
@@ -256,8 +257,22 @@ not settle on the engine.
|
|
|
256
257
|
## Retry budgets and timeouts
|
|
257
258
|
|
|
258
259
|
A declared business error ends the Execution as `failed { family, code, message, details }`, with no
|
|
259
|
-
retry. Any other error a Step throws fails that attempt
|
|
260
|
-
|
|
260
|
+
retry. Any other error a Step throws fails that attempt. What follows depends on what the attempt
|
|
261
|
+
failed on, read from the Kernel answer or the transport failure the error is, or carries as a `cause`:
|
|
262
|
+
|
|
263
|
+
| The attempt failed on | The Step |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| The platform: a Kernel refusal that prescribes a delay (`CAPACITY_EXHAUSTED`, `SERVER_DRAINING`, `INSTANCE_MAINTENANCE`) or a stale snapshot; a Kernel or Release discovery that does not answer (a network failure, HTTP `429`, `502` or `503`) to a request that did not leave, or to a Query or a keyed call; `OUTCOME_UNKNOWN` to a Query or a keyed call | Waits, and is attempted again without spending its budget, for as long as the platform stays unavailable within its `maxDurationMs`. |
|
|
266
|
+
| A request the Kernel never accepts as sent: `PARSE_ERROR`, `INVALID_REQUEST`, `METHOD_NOT_ALLOWED`, `UNSUPPORTED`, an expectation or a callable fingerprint that no longer matches, a stale maintenance session | Fails at once. |
|
|
267
|
+
| Anything else: the Step's code, a Provider, `retryAfter`, a deadline, any other Kernel refusal, an unkeyed call whose outcome is unknown | Spends an attempt, and is attempted again within its budget. |
|
|
268
|
+
|
|
269
|
+
A Query, a Mutation and an `invoke` of a durable callable repeat no effect when sent again, the last
|
|
270
|
+
two by their key, so the Step waits out their unavailability. An unkeyed `invoke` that may have
|
|
271
|
+
reached the Kernel may have run: sending it again is the Step's own risk, and spends an attempt. So
|
|
272
|
+
does any failure of an attempt that sent one, since the next attempt sends it again: an outage never
|
|
273
|
+
repeats an unkeyed `invoke` beyond the Step's budget.
|
|
274
|
+
|
|
275
|
+
A Step that spends its budget, or fails at once, throws into the Workflow; uncaught, the Execution
|
|
261
276
|
ends `failed` with code `STEP_EXHAUSTED`:
|
|
262
277
|
|
|
263
278
|
```ts
|
|
@@ -292,30 +307,39 @@ try {
|
|
|
292
307
|
|
|
293
308
|
| `retry` field | Meaning | Bounds |
|
|
294
309
|
| --- | --- | --- |
|
|
295
|
-
| `maxAttempts` | Attempts, the first one included. | 1 to 1,000 |
|
|
296
|
-
| `maxDurationMs` | Time from the first attempt after which a failed attempt is not retried; an attempt already scheduled still runs. | 1 ms to 365 days |
|
|
310
|
+
| `maxAttempts` | Attempts that may fail on the Step's own account, the first one included. A wait for the platform spends none. | 1 to 1,000 |
|
|
311
|
+
| `maxDurationMs` | Time from the first attempt after which a failed attempt is not retried, whatever it failed on; an attempt already scheduled still runs. The only bound on waiting for the platform. | 1 ms to 365 days |
|
|
297
312
|
| `initialIntervalMs` | Delay before the first retry, doubled on each failure. | 1 ms to one hour |
|
|
298
|
-
| `maxIntervalMs` | Longest
|
|
313
|
+
| `maxIntervalMs` | Longest backoff between two attempts. | `initialIntervalMs` to one day |
|
|
299
314
|
|
|
300
315
|
The limits, `maxAttempts` and `maxDurationMs`, go together: a Step that declares either replaces both
|
|
301
316
|
limits of the deployment, and the one it omits does not bound it. The budget is spent at the first
|
|
302
|
-
limit reached
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
engine would
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
317
|
+
limit reached; a Step without `maxDurationMs` waits for the platform until it answers or the run is
|
|
318
|
+
cancelled. An interval the Step leaves out is the deployment's. `timeoutMs` bounds one attempt, its
|
|
319
|
+
Kernel calls included, from one second to ten minutes: past it, the attempt fails, what it holds is
|
|
320
|
+
aborted, and the failure spends an attempt. Each Workflow service declares to the engine an abort
|
|
321
|
+
timeout of fifteen minutes, so a Step's own timeout always ends its attempt before the engine would
|
|
322
|
+
cut it.
|
|
323
|
+
|
|
324
|
+
An attempt after a failure of the Step's own waits its backoff, which doubles with each such
|
|
325
|
+
failure. An attempt after a platform failure waits the longer of the delay the Kernel prescribes and a
|
|
326
|
+
backoff that doubles with each platform failure since the Step's last own failure. Under the default
|
|
327
|
+
intervals, a Step reaches an unavailable Kernel at most once a minute after its first minute: an hour
|
|
328
|
+
of outage costs about 65 attempts and 260 journal entries, and none of its budget. Throw
|
|
329
|
+
`retryAfter(ms, cause)` inside a Step to wait exactly `ms`, for instance on an HTTP `429` with
|
|
330
|
+
`Retry-After`; that attempt spends the budget, since only the Step knows whether its cause clears.
|
|
311
331
|
|
|
312
332
|
A run that fails outside any Step keeps the engine's pause. An input or receiver the code cannot
|
|
313
333
|
read, an output the Schema refuses, a journal whose Steps the code no longer takes, or an error
|
|
314
334
|
thrown between two Steps fails every attempt; after the deployment's `maxAttempts` the Execution is
|
|
315
|
-
`paused` until `Execution.resume()` or `Execution.cancel()`.
|
|
316
|
-
the
|
|
317
|
-
|
|
318
|
-
the
|
|
335
|
+
`paused` until `Execution.resume()` or `Execution.cancel()`. A deployment the engine cannot reach
|
|
336
|
+
counts the same way, inside a Step or not, since its attempt of the run fails before any Workflow code
|
|
337
|
+
runs: with the default intervals, the run pauses after about fourteen minutes. `resume` attempts the
|
|
338
|
+
run again with the code served at its URL: it helps only once that code answers and can replay the
|
|
339
|
+
journal. A cooperative `cancel` attempts the run again, under the same `maxAttempts`: a run that
|
|
340
|
+
reaches an await sees the cancellation and may compensate (`cancelled(error)`); a run that fails again
|
|
341
|
+
before it awaits anything pauses again, and the Kernel then ends it `cancelled`, without compensation.
|
|
342
|
+
`cancel({ force: true })`, which the owner Domain holds, ends a run at once, without compensation.
|
|
319
343
|
|
|
320
344
|
## Redeployments
|
|
321
345
|
|
|
@@ -330,14 +354,17 @@ refuses a journal whose Steps no longer match, in order and by id, and pauses th
|
|
|
330
354
|
same Steps continues the run. Renaming, reordering or removing a Step that runs have journaled is
|
|
331
355
|
therefore an incompatible change in place, and no option of the Workflow declares it.
|
|
332
356
|
|
|
333
|
-
**Keep the Schema additive while runs are in flight.** A Step calls the Kernel as installed when it
|
|
334
|
-
not when its run started: a Class, Function or Edge the run uses and a later install
|
|
335
|
-
|
|
357
|
+
**Keep the Schema additive while runs are in flight.** A Step calls the Kernel as installed when it
|
|
358
|
+
calls, not when its run started: a Class, Function or Edge the run uses and a later install removed or
|
|
359
|
+
narrowed refuses its calls, and the Step ends `STEP_EXHAUSTED`, at once when the Kernel can never
|
|
360
|
+
accept the request, such as a callable whose fingerprint changed, otherwise once it spends its budget.
|
|
361
|
+
The install itself only delays the Step, which waits out its maintenance without spending its budget.
|
|
336
362
|
Remove them once the runs that use them have ended.
|
|
337
363
|
|
|
338
364
|
**Do not retire a Release, or remove a durable callable, while runs are in flight.** A retired
|
|
339
365
|
Release no longer answers its URL: its runs no longer progress, a signal to them times out, and a
|
|
340
|
-
cooperative `cancel`
|
|
366
|
+
cooperative `cancel` ends them `cancelled`, without compensation, once the engine has spent their
|
|
367
|
+
attempts; `cancel({ force: true })` ends them at once. A durable
|
|
341
368
|
callable a new install no longer declares has its runs cancelled by the Kernel, without
|
|
342
369
|
compensation, within minutes.
|
|
343
370
|
|
|
@@ -387,7 +414,7 @@ loaded engine can still be supplied synchronously.
|
|
|
387
414
|
| Option | Default |
|
|
388
415
|
| --- | --- |
|
|
389
416
|
| `retentionMs` | 7 days; at least the Host minimum (one day by default). |
|
|
390
|
-
| `retry` | The budget of every Step that declares none:
|
|
417
|
+
| `retry` | The budget of every Step that declares none: 10 attempts from 1 s to 60 s, and no duration limit. `maxAttempts` also bounds a run failing outside any Step, which then pauses: 20 attempts when `retry` declares none. The Host caps them. |
|
|
391
418
|
| `stepTimeoutMs` | 60 s per Step attempt that declares no `timeoutMs`, including its Kernel calls; ten minutes at most. |
|
|
392
419
|
|
|
393
420
|
The provider receives the durable `Request`. Its route identifies the HTTPS Kernel whose
|