@astrale-os/sdk 0.6.0-beta.37 → 0.6.0-beta.39

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.
Files changed (28) hide show
  1. package/dist/application/runtime/workflows.js +0 -3
  2. package/dist/application/workflow/authoring.d.ts +1 -1
  3. package/dist/application/workflow/define.js +4 -15
  4. package/dist/application/workflow/durable.d.ts +43 -4
  5. package/dist/application/workflow/index.d.ts +1 -1
  6. package/dist/application/workflow/workflow.d.ts +0 -8
  7. package/dist/deployment/address/configuration.d.ts +1 -0
  8. package/dist/deployment/address/configuration.js +2 -1
  9. package/dist/execution/index.d.ts +1 -0
  10. package/dist/execution/index.js +1 -0
  11. package/dist/execution/workflows/engine.d.ts +19 -1
  12. package/dist/execution/workflows/failure.d.ts +40 -5
  13. package/dist/execution/workflows/failure.js +123 -6
  14. package/dist/execution/workflows/index.d.ts +2 -2
  15. package/dist/execution/workflows/index.js +1 -1
  16. package/dist/execution/workflows/options.d.ts +36 -9
  17. package/dist/execution/workflows/options.js +82 -19
  18. package/dist/execution/workflows/receiver.js +2 -0
  19. package/dist/execution/workflows/run.js +69 -29
  20. package/dist/testing/durable/engine.d.ts +16 -6
  21. package/dist/testing/durable/engine.js +67 -15
  22. package/dist/tooling/linter/implementations/source/dependencies.js +1 -73
  23. package/dist/tooling/linter/implementations/source/shared.d.ts +7 -0
  24. package/dist/tooling/linter/implementations/source/shared.js +92 -13
  25. package/dist/tooling/linter/implementations/source/workflows.js +426 -3
  26. package/dist/tooling/linter/policy/generated.js +2 -2
  27. package/dist/tooling/linter/requirements/registry.js +3 -1
  28. package/package.json +6 -6
@@ -20,9 +20,6 @@ export function admitWorkflows(callables, input) {
20
20
  throw new TypeError(`Runtime Workflow ${definition.address} must have value output.`);
21
21
  }
22
22
  const durable = callable.execution?.durable === true;
23
- if (!durable && definition.compat !== undefined) {
24
- throw new TypeError(`Runtime Workflow ${definition.address} declares compat but its callable is not durable.`);
25
- }
26
23
  return Object.freeze({
27
24
  kind: 'workflow',
28
25
  address: definition.address,
@@ -1,5 +1,5 @@
1
1
  export { defineWorkflow } from './define.js';
2
2
  export type { InlineWorkflowContext, WorkflowContext } from './context.js';
3
- export type { DurableCallable, DurableExecution, DurableStep, DurableStepAuthority, DurableWorkflowContext, SignalOutcome, } from './durable.js';
3
+ export type { DurableCallable, DurableExecution, DurableStep, DurableStepAuthority, DurableStepDeclaration, DurableStepId, DurableStepRetry, DurableWorkflowContext, SignalOutcome, } from './durable.js';
4
4
  export type { Workflow, WorkflowAddress, WorkflowDefinition, WorkflowOptions } from './workflow.js';
5
5
  export type { DeclaredErrorDefinition, DeclaredErrorFactory, DeclaredErrorFamily, DeclaredErrorOptions, DeclaredErrors, } from '../error/index.js';
@@ -1,7 +1,6 @@
1
1
  import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
2
2
  import { rememberDeclaredErrors } from '../error/index.js';
3
3
  const admittedWorkflows = new WeakSet();
4
- const COMPAT_TOKEN = /^[A-Za-z0-9._~-]{1,64}$/u;
5
4
  /** Define one multi-step value-output implementation by its SDK Action address. */
6
5
  export function defineWorkflow() {
7
6
  return function define(address, optionsOrRun, possibleRun) {
@@ -10,12 +9,7 @@ export function defineWorkflow() {
10
9
  const run = possibleRun ?? optionsOrRun;
11
10
  if (typeof run !== 'function')
12
11
  throw new TypeError('Workflow run must be a function.');
13
- const workflow = Object.freeze({
14
- kind: 'workflow',
15
- address,
16
- run,
17
- ...(options?.compat === undefined ? {} : { compat: options.compat }),
18
- });
12
+ const workflow = Object.freeze({ kind: 'workflow', address, run });
19
13
  if (options?.errors !== undefined)
20
14
  rememberDeclaredErrors(workflow, { errors: options.errors });
21
15
  admittedWorkflows.add(workflow);
@@ -26,15 +20,10 @@ function acceptOptions(input) {
26
20
  if (input === null ||
27
21
  typeof input !== 'object' ||
28
22
  Array.isArray(input) ||
29
- Reflect.ownKeys(input).some((key) => key !== 'errors' && key !== 'compat')) {
30
- throw new TypeError('Workflow options accept only errors and compat.');
23
+ Reflect.ownKeys(input).some((key) => key !== 'errors')) {
24
+ throw new TypeError('Workflow options accept only errors.');
31
25
  }
32
- const options = input;
33
- if (options.compat !== undefined &&
34
- (typeof options.compat !== 'string' || !COMPAT_TOKEN.test(options.compat))) {
35
- throw new TypeError(`Invalid Workflow compat token: ${JSON.stringify(options.compat)}.`);
36
- }
37
- return options;
26
+ return input;
38
27
  }
39
28
  export function isWorkflow(input) {
40
29
  return input !== null && typeof input === 'object' && admittedWorkflows.has(input);
@@ -61,24 +61,55 @@ export interface DurableStepAuthority<DomainValue extends Domain = Domain, Defin
61
61
  */
62
62
  readonly integrations: ReplayableIntegrationClients<Definitions>;
63
63
  }
64
+ /**
65
+ * How long a failing Step is retried. Its limits, `maxAttempts` and `maxDurationMs`, go together: a
66
+ * Step that declares either replaces both limits of the deployment, and the one it omits does not
67
+ * bound it. The budget is spent at the first limit reached.
68
+ */
69
+ export interface DurableStepRetry {
70
+ /** Attempts, the first one included: 1 to 1,000. */
71
+ readonly maxAttempts?: number;
72
+ /**
73
+ * Time from the first attempt after which a failed attempt is not retried, 1 ms to 365 days; an
74
+ * attempt already scheduled, such as after a long prescribed delay, still runs.
75
+ */
76
+ readonly maxDurationMs?: number;
77
+ /** Delay before the first retry, doubled on each failure: 1 ms to one hour. */
78
+ readonly initialIntervalMs?: number;
79
+ /** Longest delay between two attempts: `initialIntervalMs` to one day. */
80
+ readonly maxIntervalMs?: number;
81
+ }
82
+ /** A Step: its id, and the retry budget and timeout it declares instead of the deployment's. */
83
+ export interface DurableStepDeclaration {
84
+ readonly id: string;
85
+ readonly retry?: DurableStepRetry;
86
+ /** Deadline of one attempt, including its Kernel calls: one second to ten minutes. */
87
+ readonly timeoutMs?: number;
88
+ }
89
+ /**
90
+ * A Step's id alone, under the deployment's retry budget and Step timeout, or a declaration of its
91
+ * own. A Step that spends its budget throws an error `exhausted` recognizes; uncaught, the Execution
92
+ * ends `failed` with code `STEP_EXHAUSTED`.
93
+ */
94
+ export type DurableStepId = string | DurableStepDeclaration;
64
95
  export interface DurableStep<DomainValue extends Domain = Domain, Definitions extends Integrations = Readonly<Record<never, never>>> {
65
96
  /**
66
97
  * Run `execute` once and journal its portable output; a replay returns the journaled output. A
67
98
  * Step may run again when its outcome was not journaled: its Mutations, durable calls and keyed
68
99
  * Integration operations are answered from their keys, and any other effect must be idempotent.
69
100
  */
70
- run<Output>(id: string, execute: (authority: DurableStepAuthority<DomainValue, Definitions>) => Output | Promise<Output>): Promise<Output>;
101
+ run<Output>(step: DurableStepId, execute: (authority: DurableStepAuthority<DomainValue, Definitions>) => Output | Promise<Output>): Promise<Output>;
71
102
  /**
72
103
  * One journaled Query. A replay returns the result of its first run, so a later Step that depends
73
104
  * on it resends the same request whenever it is retried.
74
105
  */
75
- query<Output>(id: string, query: QueryDefinition<DomainValue, void, Output>): Promise<Output>;
76
- query<Input, Output>(id: string, query: QueryDefinition<DomainValue, Input, Output>, input: Input): Promise<Output>;
106
+ query<Output>(step: DurableStepId, query: QueryDefinition<DomainValue, void, Output>): Promise<Output>;
107
+ query<Input, Output>(step: DurableStepId, query: QueryDefinition<DomainValue, Input, Output>, input: Input): Promise<Output>;
77
108
  /**
78
109
  * One journaled Mutation. A Step replayed after its commit receives the result of that commit
79
110
  * from the Kernel's receipt instead of writing again.
80
111
  */
81
- mutate<Input, Output>(id: string, mutation: Mutation<DomainValue, Input, Output>, input: Input): Promise<Output>;
112
+ mutate<Input, Output>(step: DurableStepId, mutation: Mutation<DomainValue, Input, Output>, input: Input): Promise<Output>;
82
113
  }
83
114
  type DurableSelfOf<Callable> = Callable extends ResolvedMethod<unknown, unknown, false> ? {
84
115
  readonly self: NodeId;
@@ -116,6 +147,14 @@ export type DurableWorkflowContext<Callable, DomainValue extends Domain = Domain
116
147
  }): Promise<SignalOutcome<Signal>>;
117
148
  /** Whether `error` is the engine's cooperative cancellation, which the Workflow may compensate. */
118
149
  cancelled(error: unknown): boolean;
150
+ /** Whether `error` is a Step that spent its retry budget, which the Workflow may handle. */
151
+ exhausted(error: unknown): boolean;
152
+ /**
153
+ * The failure of a Step attempt, to throw inside the Step, that the engine attempts again no
154
+ * earlier than `delayMs` (at most one hour), as an HTTP `Retry-After` asks. The attempt counts
155
+ * toward the Step's budget. A Kernel refusal that prescribes a delay is retried this way already.
156
+ */
157
+ retryAfter(delayMs: number, cause?: unknown): Error;
119
158
  /** Wait durably for another Execution to end, by journaled polling of its `result`. */
120
159
  awaitExecution<Output>(execution: ExecutionRef<Output>): Promise<ExecutionOutcome<Output>>;
121
160
  } & DurableSelfOf<Callable>;
@@ -1,6 +1,6 @@
1
1
  export { defineWorkflow, isWorkflow } from './define.js';
2
2
  export type { InlineWorkflowContext, WorkflowContext } from './context.js';
3
- export type { DurableCallable, DurableExecution, DurableStep, DurableStepAuthority, DurableWorkflowContext, SignalOutcome, } from './durable.js';
3
+ export type { DurableCallable, DurableExecution, DurableStep, DurableStepAuthority, DurableStepDeclaration, DurableStepId, DurableStepRetry, DurableWorkflowContext, SignalOutcome, } from './durable.js';
4
4
  export type { CallableAtWorkflowAddress, Workflow, WorkflowAddress, WorkflowCallable, WorkflowDefinition, WorkflowOptions, } from './workflow.js';
5
5
  export * as step from './step/index.js';
6
6
  export * as runner from './runner/index.js';
@@ -13,20 +13,12 @@ declare const WORKFLOW_SCHEMA: unique symbol;
13
13
  /** Options of one Workflow implementation. */
14
14
  export interface WorkflowOptions<out Errors extends DeclaredErrors = DeclaredErrors> {
15
15
  readonly errors?: Errors;
16
- /**
17
- * Durable only. Compatibility token journaled first by every run: keep it while a redeployment
18
- * can replay the journals of running Executions, change it when it cannot, and those runs pause
19
- * until they are resumed on compatible code or cancelled.
20
- */
21
- readonly compat?: string;
22
16
  }
23
17
  /** Sealed multi-step Workflow awaiting canonical callable association by Runtime admission. */
24
18
  export interface WorkflowDefinition<Schema extends schema.DomainSchema = schema.DomainSchema, Address extends WorkflowAddress<Schema> = WorkflowAddress<Schema>, Definitions extends Integrations = Readonly<Record<never, never>>, Errors extends DeclaredErrors = NoDeclaredErrors> {
25
19
  readonly kind: 'workflow';
26
20
  readonly address: Address;
27
21
  readonly run: Workflow<CallableAtWorkflowAddress<Schema, Address>, Definitions, ResolvedDomainOf<Schema>, Errors>;
28
- /** Durable compatibility token; absent means the default token. */
29
- readonly compat?: string;
30
22
  /** Invariant type-only Schema witness; absent from emitted JavaScript. */
31
23
  readonly [WORKFLOW_SCHEMA]: (schema: Schema) => Schema;
32
24
  }
@@ -67,6 +67,7 @@ export interface FrozenDurableOptions {
67
67
  readonly retry?: {
68
68
  readonly initialIntervalMs?: number;
69
69
  readonly maxAttempts?: number;
70
+ readonly maxDurationMs?: number;
70
71
  readonly maxIntervalMs?: number;
71
72
  };
72
73
  readonly stepTimeoutMs?: number;
@@ -195,11 +195,12 @@ function durableOptionsOf(input) {
195
195
  const options = input;
196
196
  let retry;
197
197
  if (Object.hasOwn(options, 'retry')) {
198
- exact(options.retry, [], ['initialIntervalMs', 'maxAttempts', 'maxIntervalMs']);
198
+ exact(options.retry, [], ['initialIntervalMs', 'maxAttempts', 'maxDurationMs', 'maxIntervalMs']);
199
199
  const value = options.retry;
200
200
  retry = Object.freeze({
201
201
  ...optional(value, 'initialIntervalMs'),
202
202
  ...optional(value, 'maxAttempts'),
203
+ ...optional(value, 'maxDurationMs'),
203
204
  ...optional(value, 'maxIntervalMs'),
204
205
  });
205
206
  }
@@ -3,4 +3,5 @@ export type { ExecutionContext, ServedDomain, ServeInput } from './serve.js';
3
3
  export { DEFAULT_MEMORY_CACHE_ENTRIES, memoryCache, type DeploymentCache, type MemoryCacheOptions, } from './cache/index.js';
4
4
  export { createIdentity, maximumExchangeTtl, type ExchangeOptions, type Identity, type IdentityInput, } from './identity/index.js';
5
5
  export type { DurableEngine, DurableServing, DurableServingProvider } from './workflows/index.js';
6
+ export { DURABLE_ENGINE_EXPORTS } from './workflows/options.js';
6
7
  export { isCanonicalDurableRequest } from './workflows/route.js';
@@ -1,4 +1,5 @@
1
1
  export { serve } from './serve.js';
2
2
  export { DEFAULT_MEMORY_CACHE_ENTRIES, memoryCache, } from './cache/index.js';
3
3
  export { createIdentity, maximumExchangeTtl, } from './identity/index.js';
4
+ export { DURABLE_ENGINE_EXPORTS } from './workflows/options.js';
4
5
  export { isCanonicalDurableRequest } from './workflows/route.js';
@@ -15,6 +15,11 @@ export interface DurableEngine {
15
15
  readonly metadata?: Record<string, string>;
16
16
  }) => Error;
17
17
  readonly CancelledError: abstract new (...input: never[]) => Error;
18
+ /** A failure inside `run` that the engine attempts again no earlier than `retryAfter` ms. */
19
+ readonly RetryableError: new (message: string, options?: {
20
+ readonly retryAfter?: number;
21
+ readonly cause?: unknown;
22
+ }) => Error;
18
23
  /** The rejection of `orTimeout` once its duration elapses first. */
19
24
  readonly TimeoutError: abstract new (...input: never[]) => Error;
20
25
  }
@@ -27,6 +32,8 @@ export interface EngineWorkflowDefinition {
27
32
  readonly options: {
28
33
  readonly workflowRetention: number;
29
34
  readonly retryPolicy: EngineRetryPolicy;
35
+ /** How long an attempt may run without progress before the engine aborts it. */
36
+ readonly abortTimeout: number;
30
37
  };
31
38
  }
32
39
  export interface EngineRetryPolicy {
@@ -36,6 +43,17 @@ export interface EngineRetryPolicy {
36
43
  readonly maxInterval: number;
37
44
  readonly exponentiationFactor: number;
38
45
  }
46
+ /**
47
+ * The retry budget of one journaled `run`. Spent, the `run` rejects with a `TerminalError` and the
48
+ * engine attempts it no more; an absent limit does not bound it.
49
+ */
50
+ export interface EngineRunOptions {
51
+ readonly maxRetryAttempts?: number;
52
+ readonly maxRetryDuration?: number;
53
+ readonly initialRetryInterval: number;
54
+ readonly maxRetryInterval: number;
55
+ readonly retryIntervalFactor: number;
56
+ }
39
57
  /**
40
58
  * A promise the engine journals. Its combinators are journaled too, so a replay settles them as the
41
59
  * first run did; racing it against another promise with `Promise.race` would not be.
@@ -46,7 +64,7 @@ export interface EnginePromise<Output> extends PromiseLike<Output> {
46
64
  }
47
65
  /** Journaled operations of one workflow run. */
48
66
  export interface EngineWorkflowContext {
49
- run<Output>(name: string, action: () => Promise<Output>): PromiseLike<Output>;
67
+ run<Output>(name: string, action: () => Promise<Output>, options?: EngineRunOptions): PromiseLike<Output>;
50
68
  sleep(durationMs: number, name?: string): PromiseLike<void>;
51
69
  promise(name: string): {
52
70
  get(): EnginePromise<unknown>;
@@ -1,11 +1,46 @@
1
1
  import type { DurableEngine } from './engine.js';
2
+ /** Failure code of an Execution whose Workflow did not catch a Step that spent its retry budget. */
3
+ export declare const STEP_EXHAUSTED = "STEP_EXHAUSTED";
4
+ /** A Kernel refusal as the wire names it: its protocol code and, when given, its reason code. */
5
+ export interface KernelRefusal {
6
+ readonly code: number;
7
+ readonly reason?: string;
8
+ }
2
9
  /**
3
- * Encode a declared business error of `definition` as a terminal engine failure, or return
4
- * undefined for any other error, which the engine retries.
10
+ * A Step that spent its retry budget. Its message never carries what the last attempt threw; when
11
+ * the Kernel refused that attempt, `refusal` names the refusal by its codes alone.
12
+ */
13
+ export declare class StepExhausted extends Error {
14
+ readonly step: string;
15
+ readonly refusal?: KernelRefusal;
16
+ constructor(step: string, refusal?: KernelRefusal);
17
+ }
18
+ /** The Kernel refusal behind `cause`, itself or one of its causes. */
19
+ export declare function kernelRefusalOf(cause: unknown): KernelRefusal | undefined;
20
+ /**
21
+ * The failure a Step attempt hands the engine: `cause` itself, or, when the Kernel refused the
22
+ * attempt, an error whose message starts with the refusal's codes, so the exhaustion can name it.
23
+ */
24
+ export declare function attemptFailure(cause: unknown): unknown;
25
+ /** A Step attempt's failure that asks the engine to wait `delayMs` before the next attempt. */
26
+ export declare class StepRetryAfter extends Error {
27
+ readonly delayMs: number;
28
+ constructor(delayMs: number, cause?: unknown);
29
+ }
30
+ /**
31
+ * The failure of a Step attempt that waits out a prescribed delay before the next attempt, instead
32
+ * of the Step's backoff: a `retryAfter` failure, or a Kernel refusal that prescribes `retryAfterMs`.
33
+ */
34
+ export declare function delayedRetry(engine: DurableEngine, cause: unknown): Error | undefined;
35
+ /**
36
+ * Encode a declared business error of `definition`, or a Step that spent its retry budget, as a
37
+ * terminal engine failure, or return undefined for any other error, which the engine retries.
5
38
  */
6
39
  export declare function terminalFailure(engine: DurableEngine, cause: unknown, definition: object): Error | undefined;
7
40
  /**
8
- * The declared business error a journaled Step failed with, rebuilt through the definition's own
9
- * factory, so a replay throws what the live run threw.
41
+ * What a journaled Step failed with, as the Workflow sees it: its declared business error, rebuilt
42
+ * through the definition's own factory, or `StepExhausted` once the engine spent its retry budget
43
+ * or the Step threw an exhaustion caught from an earlier Step. A replay throws what the live run
44
+ * threw.
10
45
  */
11
- export declare function replayedFailure(engine: DurableEngine, cause: unknown, definition: object): Error | undefined;
46
+ export declare function replayedFailure(engine: DurableEngine, cause: unknown, definition: object, step: string): Error | undefined;
@@ -1,6 +1,101 @@
1
1
  import { declaredErrorFactory, declaredErrorOf } from '../../application/error/index.js';
2
+ import { ResponseError, retryAfterOf } from '../../platform/client/index.js';
2
3
  /** Metadata key the Kernel reads a declared failure from (`backend/restate` outcome decoding). */
3
4
  const FAILURE = 'astrale.failure';
5
+ /** Failure code of an Execution whose Workflow did not catch a Step that spent its retry budget. */
6
+ export const STEP_EXHAUSTED = 'STEP_EXHAUSTED';
7
+ /** The longest delay a Step attempt may ask for, as the Kernel bounds a `retryAfterMs` hint. */
8
+ const MAXIMUM_RETRY_AFTER_MS = 3_600_000;
9
+ /** How deep a failure's causes are searched for the Kernel refusal behind it. */
10
+ const MAXIMUM_CAUSE_DEPTH = 8;
11
+ const REASON_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
12
+ /**
13
+ * The mark a failed attempt's message carries when the Kernel refused it. The engine journals only
14
+ * that message, and gives back the last one when the budget is spent.
15
+ */
16
+ const REFUSAL_MARK = /\[kernel refusal ([1-9][0-9]{3})(?: ([A-Z][A-Z0-9_]{0,63}))?\] /gu;
17
+ /**
18
+ * A Step that spent its retry budget. Its message never carries what the last attempt threw; when
19
+ * the Kernel refused that attempt, `refusal` names the refusal by its codes alone.
20
+ */
21
+ export class StepExhausted extends Error {
22
+ step;
23
+ refusal;
24
+ constructor(step, refusal) {
25
+ super(`Workflow Step ${step} exhausted its retries.`);
26
+ this.name = 'StepExhausted';
27
+ this.step = step;
28
+ if (refusal !== undefined)
29
+ this.refusal = refusal;
30
+ }
31
+ }
32
+ /** The Kernel refusal behind `cause`, itself or one of its causes. */
33
+ export function kernelRefusalOf(cause) {
34
+ let current = cause;
35
+ for (let depth = 0; depth < MAXIMUM_CAUSE_DEPTH; depth += 1) {
36
+ if (current instanceof ResponseError) {
37
+ const reason = current.reason?.code;
38
+ return Object.freeze({
39
+ code: current.code,
40
+ ...(reason !== undefined && REASON_CODE.test(reason) ? { reason } : {}),
41
+ });
42
+ }
43
+ if (current === null || typeof current !== 'object')
44
+ return undefined;
45
+ current = current.cause;
46
+ }
47
+ return undefined;
48
+ }
49
+ /**
50
+ * The failure a Step attempt hands the engine: `cause` itself, or, when the Kernel refused the
51
+ * attempt, an error whose message starts with the refusal's codes, so the exhaustion can name it.
52
+ */
53
+ export function attemptFailure(cause) {
54
+ const refusal = kernelRefusalOf(cause);
55
+ if (refusal === undefined)
56
+ return cause;
57
+ return new Error(attemptMessage(cause, refusal), { cause });
58
+ }
59
+ function attemptMessage(cause, refusal = kernelRefusalOf(cause)) {
60
+ const message = cause instanceof Error ? cause.message : String(cause);
61
+ if (refusal === undefined)
62
+ return message;
63
+ const reason = refusal.reason === undefined ? '' : ` ${refusal.reason}`;
64
+ return `[kernel refusal ${refusal.code}${reason}] ${message}`;
65
+ }
66
+ /** The refusal the last attempt's message names, if the Kernel refused it. */
67
+ function refusalIn(message) {
68
+ const marks = [...message.matchAll(REFUSAL_MARK)];
69
+ const last = marks.at(-1);
70
+ if (last === undefined)
71
+ return undefined;
72
+ return Object.freeze({
73
+ code: Number(last[1]),
74
+ ...(last[2] === undefined ? {} : { reason: last[2] }),
75
+ });
76
+ }
77
+ /** A Step attempt's failure that asks the engine to wait `delayMs` before the next attempt. */
78
+ export class StepRetryAfter extends Error {
79
+ delayMs;
80
+ constructor(delayMs, cause) {
81
+ if (!Number.isSafeInteger(delayMs) || delayMs < 0 || delayMs > MAXIMUM_RETRY_AFTER_MS) {
82
+ throw new TypeError(`A Step retry delay must be an integer between 0 and ${MAXIMUM_RETRY_AFTER_MS} ms.`);
83
+ }
84
+ super(cause instanceof Error ? cause.message : `Workflow Step asked to retry after ${delayMs} ms.`, cause === undefined ? undefined : { cause });
85
+ this.name = 'StepRetryAfter';
86
+ this.delayMs = delayMs;
87
+ }
88
+ }
89
+ /**
90
+ * The failure of a Step attempt that waits out a prescribed delay before the next attempt, instead
91
+ * of the Step's backoff: a `retryAfter` failure, or a Kernel refusal that prescribes `retryAfterMs`.
92
+ */
93
+ export function delayedRetry(engine, cause) {
94
+ const delayMs = cause instanceof StepRetryAfter ? cause.delayMs : retryAfterOf(cause);
95
+ if (delayMs === undefined)
96
+ return undefined;
97
+ return new engine.RetryableError(attemptMessage(cause), { retryAfter: delayMs, cause });
98
+ }
4
99
  const STATUS = {
5
100
  VALIDATION_ERROR: 422,
6
101
  NOT_FOUND: 404,
@@ -8,10 +103,24 @@ const STATUS = {
8
103
  UNSUPPORTED: 422,
9
104
  };
10
105
  /**
11
- * Encode a declared business error of `definition` as a terminal engine failure, or return
12
- * undefined for any other error, which the engine retries.
106
+ * Encode a declared business error of `definition`, or a Step that spent its retry budget, as a
107
+ * terminal engine failure, or return undefined for any other error, which the engine retries.
13
108
  */
14
109
  export function terminalFailure(engine, cause, definition) {
110
+ if (cause instanceof StepExhausted) {
111
+ return new engine.TerminalError(cause.message, {
112
+ metadata: {
113
+ [FAILURE]: JSON.stringify({
114
+ code: STEP_EXHAUSTED,
115
+ message: cause.message,
116
+ details: {
117
+ step: cause.step,
118
+ ...(cause.refusal === undefined ? {} : { refusal: cause.refusal }),
119
+ },
120
+ }),
121
+ },
122
+ });
123
+ }
15
124
  const declared = declaredErrorOf(cause, definition);
16
125
  if (declared === undefined)
17
126
  return undefined;
@@ -28,16 +137,24 @@ export function terminalFailure(engine, cause, definition) {
28
137
  });
29
138
  }
30
139
  /**
31
- * The declared business error a journaled Step failed with, rebuilt through the definition's own
32
- * factory, so a replay throws what the live run threw.
140
+ * What a journaled Step failed with, as the Workflow sees it: its declared business error, rebuilt
141
+ * through the definition's own factory, or `StepExhausted` once the engine spent its retry budget
142
+ * or the Step threw an exhaustion caught from an earlier Step. A replay throws what the live run
143
+ * threw.
33
144
  */
34
- export function replayedFailure(engine, cause, definition) {
145
+ export function replayedFailure(engine, cause, definition, step) {
35
146
  if (!(cause instanceof engine.TerminalError) || cause instanceof engine.CancelledError) {
36
147
  return undefined;
37
148
  }
38
149
  const encoded = cause.metadata?.[FAILURE];
150
+ // The Step's own code turns every terminal failure into a declared one; the engine's is its budget.
39
151
  if (encoded === undefined)
40
- return undefined;
152
+ return new StepExhausted(step, refusalIn(cause.message));
41
153
  const failure = JSON.parse(encoded);
154
+ // Only a declared business error has a family, so a declared code may be STEP_EXHAUSTED too.
155
+ if (failure.family === undefined && failure.code === STEP_EXHAUSTED) {
156
+ const details = failure.details;
157
+ return new StepExhausted(details.step, details.refusal);
158
+ }
42
159
  return declaredErrorFactory(definition)(failure.code, failure.details);
43
160
  }
@@ -1,4 +1,4 @@
1
- export type { DurableEngine, EnginePromise, EngineRetryPolicy, EngineSharedContext, EngineWorkflowContext, EngineWorkflowDefinition, } from './engine.js';
2
- export { durableServing, type AdmittedDurableServing, type DurableServing, type DurableServingProvider, } from './options.js';
1
+ export type { DurableEngine, EnginePromise, EngineRetryPolicy, EngineRunOptions, EngineSharedContext, EngineWorkflowContext, EngineWorkflowDefinition, } from './engine.js';
2
+ export { DURABLE_ENGINE_EXPORTS, durableServing, type AdmittedDurableServing, type DurableServing, type DurableServingProvider, } from './options.js';
3
3
  export { createDurableReceiver, type DurableReceiver } from './receiver.js';
4
4
  export { isDurableRequest, isCanonicalDurableRequest } from './route.js';
@@ -1,3 +1,3 @@
1
- export { durableServing, } from './options.js';
1
+ export { DURABLE_ENGINE_EXPORTS, durableServing, } from './options.js';
2
2
  export { createDurableReceiver } from './receiver.js';
3
3
  export { isDurableRequest, isCanonicalDurableRequest } from './route.js';
@@ -1,5 +1,6 @@
1
1
  import type { Fetch } from '@astrale-os/kernel-client';
2
- import type { DurableEngine, EngineRetryPolicy } from './engine.js';
2
+ import type { DurableStepRetry } from '../../application/workflow/index.js';
3
+ import type { DurableEngine, EngineRetryPolicy, EngineRunOptions } from './engine.js';
3
4
  /** Serving configuration of the durable Workflows of one deployment. */
4
5
  export interface DurableServing {
5
6
  /** The Restate SDK Fetch build of this platform. */
@@ -12,13 +13,13 @@ export interface DurableServing {
12
13
  * default.
13
14
  */
14
15
  readonly retentionMs?: number;
15
- /** Attempts of a failing run before it pauses for `resume` or `cancel`. The Host caps them. */
16
- readonly retry?: {
17
- readonly maxAttempts?: number;
18
- readonly initialIntervalMs?: number;
19
- readonly maxIntervalMs?: number;
20
- };
21
- /** Deadline of one Step, including its Kernel calls. One minute by default. */
16
+ /**
17
+ * Retry budget of every Step that declares none: 20 attempts from 1 s to 60 s by default. A Step
18
+ * that spends it fails into its Workflow. `maxAttempts` also bounds the attempts of a run that
19
+ * fails outside any Step, which then pauses for `resume` or `cancel`. The Host caps them.
20
+ */
21
+ readonly retry?: DurableStepRetry;
22
+ /** Deadline of one Step attempt that declares none, including its Kernel calls. One minute by default. */
22
23
  readonly stepTimeoutMs?: number;
23
24
  }
24
25
  /** Resolve a deployment's engine only when its durable receiver is first requested. */
@@ -27,8 +28,34 @@ export interface AdmittedDurableServing {
27
28
  readonly engine: DurableEngine;
28
29
  readonly fetch: Fetch;
29
30
  readonly retentionMs: number;
31
+ /** Attempts of a run that fails outside any Step, before the engine pauses it. */
30
32
  readonly retryPolicy: EngineRetryPolicy;
31
- readonly stepTimeoutMs: number;
33
+ /** Retry budget and attempt deadline of a Step that declares none. */
34
+ readonly step: StepServing;
35
+ }
36
+ /** What a Step is held to: its retry budget, as the engine applies it, and its attempt deadline. */
37
+ export interface StepServing {
38
+ readonly retry: EngineRunOptions;
39
+ readonly timeoutMs: number;
32
40
  }
41
+ /**
42
+ * The members of the Restate SDK Fetch entrypoint the durable receiver uses, which a platform's
43
+ * engine module must export.
44
+ */
45
+ export declare const DURABLE_ENGINE_EXPORTS: readonly ["createEndpointHandler", "workflow", "TerminalError", "CancelledError", "RetryableError", "TimeoutError"];
46
+ /**
47
+ * How long the engine lets an attempt run without progress before it aborts it, declared on every
48
+ * Workflow service. Restate's own default is ten minutes, which a ten-minute Step would race: the
49
+ * margin lets a Step's own timeout fail the attempt, and report it, before the engine cuts it.
50
+ */
51
+ export declare const ENGINE_ABORT_TIMEOUT_MS: number;
33
52
  /** Capture one durable serving configuration, failing closed on any gap. */
34
53
  export declare function durableServing(input: DurableServing): AdmittedDurableServing;
54
+ /**
55
+ * What one Step is held to: the retry budget and timeout it declares, in place of `fallback`'s. A
56
+ * declared limit replaces both limits of the fallback; an undeclared interval keeps the fallback's.
57
+ */
58
+ export declare function stepServing(declared: {
59
+ readonly retry?: DurableStepRetry | undefined;
60
+ readonly timeoutMs?: number | undefined;
61
+ }, fallback: StepServing): StepServing;