@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.
@@ -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 { createTestEngine } from './durable/index.js';
14
- export type { TestEngine, TestEngineOptions, TestExecution, TestJournalEntry, TestStartOptions, TestStepCapabilities, } from './durable/index.js';
13
+ export { createTestWorkflowEngine } from './durable/index.js';
14
+ export type { TestExecution, TestJournalEntry, TestStartOptions, TestStepCapabilities, TestWorkflowEngine, TestWorkflowEngineOptions, } from './durable/index.js';
@@ -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 { createTestEngine } from './durable/index.js';
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`, `awaitSignal` or `awaitExecution`
86
- started while another runs is therefore refused with a `TypeError`, which pauses the run as any
87
- error between Steps does. Independent reads may share one Step, whose Kernel calls are ordinary
88
- calls that `Promise.all` may combine:
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
- `createTestEngine` (see [Testing](#testing)) shows the first two: a refusal path ends
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
- `createTestEngine` from `@astrale-os/sdk/testing` runs a durable Workflow in memory with the
196
- receiver a Host serves; only the engine is simulated. Each Step journals its result, every attempt
197
- replays the journal, timers follow a virtual clock, signals are buffered, a failing Step is retried
198
- within its budget and then fails into the Workflow, a run failing outside any Step is retried until it
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 { createTestEngine } from '@astrale-os/sdk/testing'
201
+ import { createTestWorkflowEngine } from '@astrale-os/sdk/testing'
203
202
 
204
- const engine = createTestEngine()
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. Retries do not wait out a backoff, but the backoff a retry skips, or the
236
- delay its failure asks for, counts toward the Step's `maxDurationMs`. Code should await only Steps and
237
- the context's own operations; the engine counts a run as waiting once it waits on a timer or a signal
238
- and nothing else runs.
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, and the engine attempts the Step again within
260
- its retry budget. A Step that spends its budget throws into the Workflow; uncaught, the Execution
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 delay between two attempts. | `initialIntervalMs` to one day |
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. An interval the Step leaves out is the deployment's. `timeoutMs` bounds one attempt,
303
- its Kernel calls included, from one second to ten minutes: past it, the attempt fails, what it holds
304
- is aborted, and the failure counts toward the budget. Each Workflow service declares to the engine
305
- an abort timeout of fifteen minutes, so a Step's own timeout always ends its attempt before the
306
- engine would cut it.
307
-
308
- A Kernel refusal that prescribes a delay (capacity, draining, maintenance) waits that delay before
309
- the next attempt, instead of the backoff. Throw `retryAfter(ms, cause)` inside a Step for the same
310
- behavior, for instance on an HTTP `429` with `Retry-After`. Both attempts count toward the budget.
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()`. `resume` attempts the run again with
316
- the code served at its URL: it helps only once that code can replay the journal. A run that fails
317
- again before it awaits anything never sees a cooperative cancel; `cancel({ force: true })`, which
318
- the owner Domain holds, ends it without compensation.
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 calls,
334
- not when its run started: a Class, Function or Edge the run uses and a later install
335
- removed or narrowed refuses its calls until the Step spends its budget, with `STEP_EXHAUSTED`.
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` leaves them `paused`; only `cancel({ force: true })` ends them. A durable
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: 20 attempts from 1 s to 60 s. `maxAttempts` also bounds a run failing outside any Step, which then pauses. The Host caps them. |
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.6.0-beta.40",
3
+ "version": "0.6.0-beta.41",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",