@telorun/sdk 0.74.0 → 0.75.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/dist/invoke-step.d.ts +51 -5
- package/dist/invoke-step.d.ts.map +1 -1
- package/dist/invoke-step.js +124 -2
- package/dist/resource-context.d.ts +9 -6
- package/dist/resource-context.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/invoke-step.ts +213 -8
- package/src/resource-context.ts +9 -6
package/dist/invoke-step.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import type { Invocable } from "./capabilities/invokable.js";
|
|
2
|
+
import { type InvokeContext } from "./cancellation.js";
|
|
2
3
|
import type { KindRef, ScopeContext } from "./ref.js";
|
|
3
4
|
import { type ResourceInstance } from "./resource-instance.js";
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Retry policy for a single invoke step.
|
|
7
|
+
*
|
|
8
|
+
* The field names are `Http.Request`'s, deliberately: two spellings of one
|
|
9
|
+
* concern in one standard library is how an author learns that backoff means
|
|
10
|
+
* something different depending on where it is written. `delay` is the older
|
|
11
|
+
* duration-string spelling, kept because published manifests carry it, and read
|
|
12
|
+
* as `initialDelay` when that is absent.
|
|
13
|
+
*
|
|
14
|
+
* Consumed HERE, in the leaf, rather than passed to `ctx.invoke`. The leaf has
|
|
15
|
+
* four dispatch branches and only one of them went through `ctx.invoke`, so a
|
|
16
|
+
* policy handed downstream was silently ignored for a pre-injected `!ref` — the
|
|
17
|
+
* dominant shape — and no kernel path ever read it. Owning it at the one place
|
|
18
|
+
* every branch passes through is what makes the field mean anything at all.
|
|
19
|
+
*/
|
|
5
20
|
export interface InvokeStepRetry {
|
|
21
|
+
/** Re-attempts after the first try. 0 (or absent) disables retrying. */
|
|
6
22
|
attempts?: number;
|
|
23
|
+
/** Milliseconds before the first re-attempt. */
|
|
24
|
+
initialDelay?: number;
|
|
25
|
+
/** Multiplier applied to the delay after each re-attempt. */
|
|
26
|
+
factor?: number;
|
|
27
|
+
/** Ceiling on the delay between re-attempts, in milliseconds. */
|
|
28
|
+
maxDelay?: number;
|
|
29
|
+
/** `full` picks each delay uniformly from [0, delay], decorrelating a fleet
|
|
30
|
+
* that failed together. */
|
|
31
|
+
jitter?: "none" | "full";
|
|
32
|
+
/** DEPRECATED duration string (`"250ms"`, `"1s"`) — read as `initialDelay`. */
|
|
7
33
|
delay?: string;
|
|
8
34
|
}
|
|
9
35
|
/**
|
|
@@ -19,15 +45,17 @@ export interface InvokeStep {
|
|
|
19
45
|
retry?: InvokeStepRetry;
|
|
20
46
|
}
|
|
21
47
|
/** An inline flat invoke step on an Application's `targets`. Same as an
|
|
22
|
-
*
|
|
23
|
-
* plumbing
|
|
24
|
-
*
|
|
25
|
-
*
|
|
48
|
+
* {@link InvokeStep} but `name` is optional — it is only needed for
|
|
49
|
+
* `steps.<name>.result` plumbing, and the boot runner synthesizes one when
|
|
50
|
+
* omitted. Everything else a dispatch site carries applies here, `retry`
|
|
51
|
+
* included: both are the same kernel-owned shape, and the schema half now says
|
|
52
|
+
* so too. Control flow (`if`/`while`/`switch`/`try`) is still Run's. */
|
|
26
53
|
export interface InlineInvokeTarget {
|
|
27
54
|
name?: string;
|
|
28
55
|
when?: string;
|
|
29
56
|
invoke: KindRef<Invocable> | Invocable;
|
|
30
57
|
inputs?: Record<string, unknown>;
|
|
58
|
+
retry?: InvokeStepRetry;
|
|
31
59
|
}
|
|
32
60
|
/** A single Application `targets` entry. The kernel boot runner dispatches by
|
|
33
61
|
* shape: a bare string or resolved `{ kind, name }` runs a Runnable/Service;
|
|
@@ -67,6 +95,24 @@ export interface InvokeStepState {
|
|
|
67
95
|
steps: Record<string, unknown>;
|
|
68
96
|
cel?: Record<string, unknown>;
|
|
69
97
|
scope?: ScopeContext;
|
|
98
|
+
/**
|
|
99
|
+
* The invocation this step runs inside, forwarded from the composer's own
|
|
100
|
+
* `invoke(inputs, ctx)`.
|
|
101
|
+
*
|
|
102
|
+
* Needed for the WAIT, not for the dispatch. The kernel refuses a dispatch
|
|
103
|
+
* reached after the tree was cancelled, so every step boundary is already a
|
|
104
|
+
* cancellation point through the ambient context — but a backoff between two
|
|
105
|
+
* attempts is time spent inside this leaf, where the kernel's gate cannot see
|
|
106
|
+
* it and the ambient store is deliberately not on the SDK surface (it is one
|
|
107
|
+
* runtime's mechanism; a second-language leaf has no `AsyncLocalStorage`).
|
|
108
|
+
* Passing it explicitly is what makes the wait interruptible in any runtime.
|
|
109
|
+
*
|
|
110
|
+
* Present at boot too: the boot runner forwards the kernel's boot cancellation,
|
|
111
|
+
* which the CLI's SIGINT handler trips, so Ctrl-C ends a target parked in a
|
|
112
|
+
* backoff. Absent only for a caller that assembled a step in code and had no
|
|
113
|
+
* invocation to forward.
|
|
114
|
+
*/
|
|
115
|
+
invokeCtx?: InvokeContext;
|
|
70
116
|
}
|
|
71
117
|
/**
|
|
72
118
|
* Execute one invoke step: evaluate the `when` guard, expand `inputs`, resolve
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoke-step.d.ts","sourceRoot":"","sources":["../src/invoke-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/E
|
|
1
|
+
{"version":3,"file":"invoke-step.d.ts","sourceRoot":"","sources":["../src/invoke-step.ts"],"names":[],"mappings":"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"}
|
package/dist/invoke-step.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ERR_INVOKE_CANCELLED, isCancellationError, } from "./cancellation.js";
|
|
2
|
+
import { isAmbientContractErrorCode } from "./contract-errors.js";
|
|
3
|
+
import { tryParseDurationMs } from "./duration.js";
|
|
4
|
+
import { InvokeError } from "./invoke-error.js";
|
|
1
5
|
import { getRefIdentity } from "./resource-instance.js";
|
|
2
6
|
/**
|
|
3
7
|
* Execute one invoke step: evaluate the `when` guard, expand `inputs`, resolve
|
|
@@ -11,6 +15,124 @@ export async function executeInvokeStep(step, ctx, state) {
|
|
|
11
15
|
return;
|
|
12
16
|
const inputs = ctx.expandValue(step.inputs ?? {}, cel);
|
|
13
17
|
const raw = step.invoke;
|
|
18
|
+
const result = await withStepRetry(step, state.invokeCtx, () => dispatch(raw, inputs, ctx, state));
|
|
19
|
+
state.steps[step.name] = { result };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Re-attempt a step's dispatch while its policy allows.
|
|
23
|
+
*
|
|
24
|
+
* Retries a DOMAIN failure and nothing else. There is no status to classify at
|
|
25
|
+
* this level, so the classification cannot be positive — what there is instead is
|
|
26
|
+
* an explicit author instruction, since a step carries `retry:` only because
|
|
27
|
+
* someone wrote it. So the rule is stated as exclusions, and both of them are
|
|
28
|
+
* decidable without judgement:
|
|
29
|
+
*
|
|
30
|
+
* - **Cancellation.** The invocation has been asked to stop; re-issuing it
|
|
31
|
+
* ignores that.
|
|
32
|
+
* - **A contract violation**, and a **resolution failure** — the target does not
|
|
33
|
+
* exist, or cannot be invoked. {@link isAmbientContractErrorCode} names the
|
|
34
|
+
* first set; {@link UNRETRYABLE_CODES} adds the second. Both are the KERNEL's
|
|
35
|
+
* verdict on the shape of the call rather than on the work, so they are a
|
|
36
|
+
* property of the manifest and every re-attempt fails identically — a budget
|
|
37
|
+
* spent on one is dead time between a typo and the diagnostic that names it,
|
|
38
|
+
* up to `attempts × maxDelay`. Nothing about a misspelled resource name gets
|
|
39
|
+
* truer after eight seconds of backoff.
|
|
40
|
+
*
|
|
41
|
+
* The WAIT between attempts is cancellable, for the first reason above. Every
|
|
42
|
+
* other point in a sequence already is — the kernel refuses a dispatch reached
|
|
43
|
+
* after the tree was cancelled — so a backoff is the one interval where a
|
|
44
|
+
* cancelled run would otherwise stay parked, for up to `attempts × maxDelay`.
|
|
45
|
+
*
|
|
46
|
+
* The defaults come from the schema (`Run` steps declare `default:` on every
|
|
47
|
+
* field, as `Http.Request.retry` does), so the `??` fallbacks here are the floor
|
|
48
|
+
* for a caller that assembled a policy in code rather than from a manifest — not
|
|
49
|
+
* a second, competing statement of what a default is.
|
|
50
|
+
*/
|
|
51
|
+
async function withStepRetry(step, invokeCtx, dispatch) {
|
|
52
|
+
const policy = step.retry;
|
|
53
|
+
const attempts = policy?.attempts ?? 0;
|
|
54
|
+
if (!policy || attempts <= 0)
|
|
55
|
+
return dispatch();
|
|
56
|
+
const initial = policy.initialDelay ?? parseDuration(policy.delay) ?? 250;
|
|
57
|
+
const factor = policy.factor ?? 2;
|
|
58
|
+
const maxDelay = policy.maxDelay ?? 32_000;
|
|
59
|
+
const jitter = policy.jitter ?? "full";
|
|
60
|
+
for (let resend = 0;; resend++) {
|
|
61
|
+
try {
|
|
62
|
+
return await dispatch();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (resend >= attempts || !isRetryable(err))
|
|
66
|
+
throw err;
|
|
67
|
+
const backoff = Math.min(maxDelay, initial * Math.pow(factor, resend));
|
|
68
|
+
await waitBeforeResend(jitter === "full" ? Math.random() * backoff : backoff, invokeCtx?.cancellation, step, err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Kernel verdicts on the CALL rather than on the work, beyond the ambient
|
|
73
|
+
* contract set. A dispatch that cannot resolve its target is a manifest defect;
|
|
74
|
+
* re-issuing it re-resolves the same name against the same registry. */
|
|
75
|
+
const UNRETRYABLE_CODES = new Set(["ERR_RESOURCE_NOT_FOUND", "ERR_RESOURCE_NOT_INVOKABLE"]);
|
|
76
|
+
function isRetryable(err) {
|
|
77
|
+
if (isCancellationError(err))
|
|
78
|
+
return false;
|
|
79
|
+
const code = err?.code;
|
|
80
|
+
if (typeof code !== "string")
|
|
81
|
+
return true;
|
|
82
|
+
return !isAmbientContractErrorCode(code) && !UNRETRYABLE_CODES.has(code);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Wait out the backoff, or give up the moment the invocation is cancelled.
|
|
86
|
+
*
|
|
87
|
+
* The failure that CAUSED the wait rides in the cancellation's `data`. Without
|
|
88
|
+
* it, cancelling mid-backoff would report only that the run was cancelled and
|
|
89
|
+
* the attempt's actual error — the thing the author is retrying because of —
|
|
90
|
+
* would be gone, which is exactly the swallowing a retry loop is prone to.
|
|
91
|
+
*
|
|
92
|
+
* `onCancelled` fires synchronously when the token is already cancelled, so an
|
|
93
|
+
* already-cancelled run clears the timer and rejects without waiting a tick.
|
|
94
|
+
*/
|
|
95
|
+
function waitBeforeResend(ms, token, step, pending) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
unsubscribe?.();
|
|
99
|
+
resolve();
|
|
100
|
+
}, ms);
|
|
101
|
+
const unsubscribe = token?.onCancelled((reason) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
// Released on BOTH paths. The listener is the one thing this holds, and a
|
|
104
|
+
// subscription outliving the wait it belongs to is a leak per re-attempt.
|
|
105
|
+
unsubscribe?.();
|
|
106
|
+
reject(new InvokeError(ERR_INVOKE_CANCELLED, `Step "${step.name}": cancelled while waiting to re-attempt` +
|
|
107
|
+
`${reason ? ` (${reason})` : ""}.`, { step: step.name, pendingFailure: describeFailure(pending) }));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function describeFailure(err) {
|
|
112
|
+
const code = err?.code;
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
return { ...(typeof code === "string" ? { code } : {}), message };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The deprecated `delay` duration string, in milliseconds.
|
|
118
|
+
*
|
|
119
|
+
* Delegates to the SDK's one duration grammar rather than restating it: a second
|
|
120
|
+
* spelling here would accept strings `telo check`'s `pattern` rejects, and the
|
|
121
|
+
* two would drift. A malformed value THROWS — falling back to the default would
|
|
122
|
+
* swallow a typo into a silently different backoff, and the schema's `pattern`
|
|
123
|
+
* means anything reaching this already failed static analysis.
|
|
124
|
+
*/
|
|
125
|
+
function parseDuration(value) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const ms = tryParseDurationMs(value);
|
|
129
|
+
if (ms === null) {
|
|
130
|
+
throw new InvokeError("ERR_INVALID_VALUE", `Step retry: invalid 'delay' ${JSON.stringify(value)}; use a number with a unit, e.g. ` +
|
|
131
|
+
`"250ms", "2s", "1.5m", "1h" — or the preferred 'initialDelay', in milliseconds.`);
|
|
132
|
+
}
|
|
133
|
+
return ms;
|
|
134
|
+
}
|
|
135
|
+
async function dispatch(raw, inputs, ctx, state) {
|
|
14
136
|
let result;
|
|
15
137
|
if (raw && typeof raw.invoke === "function") {
|
|
16
138
|
// A pre-injected live instance (a `!ref` resolved at Phase 5). Route it
|
|
@@ -39,8 +161,8 @@ export async function executeInvokeStep(step, ctx, state) {
|
|
|
39
161
|
result = await ctx.invokeResolved(ref.kind, ref.name, instance, inputs);
|
|
40
162
|
}
|
|
41
163
|
else {
|
|
42
|
-
result = await ctx.invoke(ref.kind, ref.name, inputs
|
|
164
|
+
result = await ctx.invoke(ref.kind, ref.name, inputs);
|
|
43
165
|
}
|
|
44
166
|
}
|
|
45
|
-
|
|
167
|
+
return result;
|
|
46
168
|
}
|
|
@@ -46,16 +46,19 @@ export type ParsedArgs = Partial<Record<string, string | boolean | string[]>> &
|
|
|
46
46
|
/**
|
|
47
47
|
* Per-call options for a by-name dispatch.
|
|
48
48
|
*
|
|
49
|
-
* A bag rather than a positional context parameter because
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* A bag rather than a positional context parameter, because `ResourceContext`
|
|
50
|
+
* satisfies `InvokeStepContext` structurally: a positional fourth argument here
|
|
51
|
+
* is reachable from the step leaf's own `invoke` call, so the two surfaces would
|
|
52
|
+
* have to agree on its meaning forever. A named key cannot be filled by accident.
|
|
53
|
+
*
|
|
54
|
+
* `retry` USED to live here and was read by nobody — the step leaf handed it down
|
|
55
|
+
* on one of its four dispatch branches and no kernel path looked at it. It now
|
|
56
|
+
* belongs to the leaf, which is the one place every branch passes through; a key
|
|
57
|
+
* kept here would be a second, inert way to ask for the same thing.
|
|
53
58
|
*/
|
|
54
59
|
export interface InvokeByNameOptions {
|
|
55
60
|
/** Seeds the invocation context, replacing the ambient. */
|
|
56
61
|
ctx?: InvokeContext;
|
|
57
|
-
/** Retry policy, read by `executeInvokeStep`. */
|
|
58
|
-
retry?: unknown;
|
|
59
62
|
}
|
|
60
63
|
export interface ResourceContext extends ControllerContext {
|
|
61
64
|
readonly args: ParsedArgs;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-context.d.ts","sourceRoot":"","sources":["../src/resource-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,QAAQ,EACR,eAAe,EACf,SAAS,EACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B;gFAC4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;4EAGwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;4CAIwC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO;IAIP,QAAQ;CAGT;AAED,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC,CAAC,GAAG;IAAE,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEhG
|
|
1
|
+
{"version":3,"file":"resource-context.d.ts","sourceRoot":"","sources":["../src/resource-context.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "Telo SDK - Public API for Telo module authors.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -36,9 +36,12 @@
|
|
|
36
36
|
],
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20.0.0",
|
|
39
|
-
"typescript": "^5.0.0"
|
|
39
|
+
"typescript": "^5.0.0",
|
|
40
|
+
"vitest": "^2.1.8"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
|
-
"build": "tsc -p tsconfig.lib.json"
|
|
43
|
+
"build": "tsc -p tsconfig.lib.json",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/src/invoke-step.ts
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
import type { Invocable } from "./capabilities/invokable.js";
|
|
2
|
+
import {
|
|
3
|
+
type CancellationToken,
|
|
4
|
+
ERR_INVOKE_CANCELLED,
|
|
5
|
+
type InvokeContext,
|
|
6
|
+
isCancellationError,
|
|
7
|
+
} from "./cancellation.js";
|
|
8
|
+
import { isAmbientContractErrorCode } from "./contract-errors.js";
|
|
9
|
+
import { tryParseDurationMs } from "./duration.js";
|
|
10
|
+
import { InvokeError } from "./invoke-error.js";
|
|
2
11
|
import type { KindRef, ScopeContext } from "./ref.js";
|
|
3
12
|
import { getRefIdentity, type ResourceInstance } from "./resource-instance.js";
|
|
4
13
|
|
|
5
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Retry policy for a single invoke step.
|
|
16
|
+
*
|
|
17
|
+
* The field names are `Http.Request`'s, deliberately: two spellings of one
|
|
18
|
+
* concern in one standard library is how an author learns that backoff means
|
|
19
|
+
* something different depending on where it is written. `delay` is the older
|
|
20
|
+
* duration-string spelling, kept because published manifests carry it, and read
|
|
21
|
+
* as `initialDelay` when that is absent.
|
|
22
|
+
*
|
|
23
|
+
* Consumed HERE, in the leaf, rather than passed to `ctx.invoke`. The leaf has
|
|
24
|
+
* four dispatch branches and only one of them went through `ctx.invoke`, so a
|
|
25
|
+
* policy handed downstream was silently ignored for a pre-injected `!ref` — the
|
|
26
|
+
* dominant shape — and no kernel path ever read it. Owning it at the one place
|
|
27
|
+
* every branch passes through is what makes the field mean anything at all.
|
|
28
|
+
*/
|
|
6
29
|
export interface InvokeStepRetry {
|
|
30
|
+
/** Re-attempts after the first try. 0 (or absent) disables retrying. */
|
|
7
31
|
attempts?: number;
|
|
32
|
+
/** Milliseconds before the first re-attempt. */
|
|
33
|
+
initialDelay?: number;
|
|
34
|
+
/** Multiplier applied to the delay after each re-attempt. */
|
|
35
|
+
factor?: number;
|
|
36
|
+
/** Ceiling on the delay between re-attempts, in milliseconds. */
|
|
37
|
+
maxDelay?: number;
|
|
38
|
+
/** `full` picks each delay uniformly from [0, delay], decorrelating a fleet
|
|
39
|
+
* that failed together. */
|
|
40
|
+
jitter?: "none" | "full";
|
|
41
|
+
/** DEPRECATED duration string (`"250ms"`, `"1s"`) — read as `initialDelay`. */
|
|
8
42
|
delay?: string;
|
|
9
43
|
}
|
|
10
44
|
|
|
@@ -22,15 +56,17 @@ export interface InvokeStep {
|
|
|
22
56
|
}
|
|
23
57
|
|
|
24
58
|
/** An inline flat invoke step on an Application's `targets`. Same as an
|
|
25
|
-
*
|
|
26
|
-
* plumbing
|
|
27
|
-
*
|
|
28
|
-
*
|
|
59
|
+
* {@link InvokeStep} but `name` is optional — it is only needed for
|
|
60
|
+
* `steps.<name>.result` plumbing, and the boot runner synthesizes one when
|
|
61
|
+
* omitted. Everything else a dispatch site carries applies here, `retry`
|
|
62
|
+
* included: both are the same kernel-owned shape, and the schema half now says
|
|
63
|
+
* so too. Control flow (`if`/`while`/`switch`/`try`) is still Run's. */
|
|
29
64
|
export interface InlineInvokeTarget {
|
|
30
65
|
name?: string;
|
|
31
66
|
when?: string;
|
|
32
67
|
invoke: KindRef<Invocable> | Invocable;
|
|
33
68
|
inputs?: Record<string, unknown>;
|
|
69
|
+
retry?: InvokeStepRetry;
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
/** A single Application `targets` entry. The kernel boot runner dispatches by
|
|
@@ -73,6 +109,24 @@ export interface InvokeStepState {
|
|
|
73
109
|
steps: Record<string, unknown>;
|
|
74
110
|
cel?: Record<string, unknown>;
|
|
75
111
|
scope?: ScopeContext;
|
|
112
|
+
/**
|
|
113
|
+
* The invocation this step runs inside, forwarded from the composer's own
|
|
114
|
+
* `invoke(inputs, ctx)`.
|
|
115
|
+
*
|
|
116
|
+
* Needed for the WAIT, not for the dispatch. The kernel refuses a dispatch
|
|
117
|
+
* reached after the tree was cancelled, so every step boundary is already a
|
|
118
|
+
* cancellation point through the ambient context — but a backoff between two
|
|
119
|
+
* attempts is time spent inside this leaf, where the kernel's gate cannot see
|
|
120
|
+
* it and the ambient store is deliberately not on the SDK surface (it is one
|
|
121
|
+
* runtime's mechanism; a second-language leaf has no `AsyncLocalStorage`).
|
|
122
|
+
* Passing it explicitly is what makes the wait interruptible in any runtime.
|
|
123
|
+
*
|
|
124
|
+
* Present at boot too: the boot runner forwards the kernel's boot cancellation,
|
|
125
|
+
* which the CLI's SIGINT handler trips, so Ctrl-C ends a target parked in a
|
|
126
|
+
* backoff. Absent only for a caller that assembled a step in code and had no
|
|
127
|
+
* invocation to forward.
|
|
128
|
+
*/
|
|
129
|
+
invokeCtx?: InvokeContext;
|
|
76
130
|
}
|
|
77
131
|
|
|
78
132
|
/**
|
|
@@ -91,6 +145,158 @@ export async function executeInvokeStep(
|
|
|
91
145
|
|
|
92
146
|
const inputs = ctx.expandValue(step.inputs ?? {}, cel) as Record<string, unknown>;
|
|
93
147
|
const raw = step.invoke as unknown;
|
|
148
|
+
const result = await withStepRetry(step, state.invokeCtx, () =>
|
|
149
|
+
dispatch(raw, inputs, ctx, state),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
state.steps[step.name] = { result };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Re-attempt a step's dispatch while its policy allows.
|
|
157
|
+
*
|
|
158
|
+
* Retries a DOMAIN failure and nothing else. There is no status to classify at
|
|
159
|
+
* this level, so the classification cannot be positive — what there is instead is
|
|
160
|
+
* an explicit author instruction, since a step carries `retry:` only because
|
|
161
|
+
* someone wrote it. So the rule is stated as exclusions, and both of them are
|
|
162
|
+
* decidable without judgement:
|
|
163
|
+
*
|
|
164
|
+
* - **Cancellation.** The invocation has been asked to stop; re-issuing it
|
|
165
|
+
* ignores that.
|
|
166
|
+
* - **A contract violation**, and a **resolution failure** — the target does not
|
|
167
|
+
* exist, or cannot be invoked. {@link isAmbientContractErrorCode} names the
|
|
168
|
+
* first set; {@link UNRETRYABLE_CODES} adds the second. Both are the KERNEL's
|
|
169
|
+
* verdict on the shape of the call rather than on the work, so they are a
|
|
170
|
+
* property of the manifest and every re-attempt fails identically — a budget
|
|
171
|
+
* spent on one is dead time between a typo and the diagnostic that names it,
|
|
172
|
+
* up to `attempts × maxDelay`. Nothing about a misspelled resource name gets
|
|
173
|
+
* truer after eight seconds of backoff.
|
|
174
|
+
*
|
|
175
|
+
* The WAIT between attempts is cancellable, for the first reason above. Every
|
|
176
|
+
* other point in a sequence already is — the kernel refuses a dispatch reached
|
|
177
|
+
* after the tree was cancelled — so a backoff is the one interval where a
|
|
178
|
+
* cancelled run would otherwise stay parked, for up to `attempts × maxDelay`.
|
|
179
|
+
*
|
|
180
|
+
* The defaults come from the schema (`Run` steps declare `default:` on every
|
|
181
|
+
* field, as `Http.Request.retry` does), so the `??` fallbacks here are the floor
|
|
182
|
+
* for a caller that assembled a policy in code rather than from a manifest — not
|
|
183
|
+
* a second, competing statement of what a default is.
|
|
184
|
+
*/
|
|
185
|
+
async function withStepRetry<T>(
|
|
186
|
+
step: InvokeStep,
|
|
187
|
+
invokeCtx: InvokeContext | undefined,
|
|
188
|
+
dispatch: () => Promise<T>,
|
|
189
|
+
): Promise<T> {
|
|
190
|
+
const policy = step.retry;
|
|
191
|
+
const attempts = policy?.attempts ?? 0;
|
|
192
|
+
if (!policy || attempts <= 0) return dispatch();
|
|
193
|
+
|
|
194
|
+
const initial = policy.initialDelay ?? parseDuration(policy.delay) ?? 250;
|
|
195
|
+
const factor = policy.factor ?? 2;
|
|
196
|
+
const maxDelay = policy.maxDelay ?? 32_000;
|
|
197
|
+
const jitter = policy.jitter ?? "full";
|
|
198
|
+
|
|
199
|
+
for (let resend = 0; ; resend++) {
|
|
200
|
+
try {
|
|
201
|
+
return await dispatch();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (resend >= attempts || !isRetryable(err)) throw err;
|
|
204
|
+
const backoff = Math.min(maxDelay, initial * Math.pow(factor, resend));
|
|
205
|
+
await waitBeforeResend(
|
|
206
|
+
jitter === "full" ? Math.random() * backoff : backoff,
|
|
207
|
+
invokeCtx?.cancellation,
|
|
208
|
+
step,
|
|
209
|
+
err,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Kernel verdicts on the CALL rather than on the work, beyond the ambient
|
|
216
|
+
* contract set. A dispatch that cannot resolve its target is a manifest defect;
|
|
217
|
+
* re-issuing it re-resolves the same name against the same registry. */
|
|
218
|
+
const UNRETRYABLE_CODES = new Set(["ERR_RESOURCE_NOT_FOUND", "ERR_RESOURCE_NOT_INVOKABLE"]);
|
|
219
|
+
|
|
220
|
+
function isRetryable(err: unknown): boolean {
|
|
221
|
+
if (isCancellationError(err)) return false;
|
|
222
|
+
const code = (err as { code?: unknown } | null | undefined)?.code;
|
|
223
|
+
if (typeof code !== "string") return true;
|
|
224
|
+
return !isAmbientContractErrorCode(code) && !UNRETRYABLE_CODES.has(code);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Wait out the backoff, or give up the moment the invocation is cancelled.
|
|
229
|
+
*
|
|
230
|
+
* The failure that CAUSED the wait rides in the cancellation's `data`. Without
|
|
231
|
+
* it, cancelling mid-backoff would report only that the run was cancelled and
|
|
232
|
+
* the attempt's actual error — the thing the author is retrying because of —
|
|
233
|
+
* would be gone, which is exactly the swallowing a retry loop is prone to.
|
|
234
|
+
*
|
|
235
|
+
* `onCancelled` fires synchronously when the token is already cancelled, so an
|
|
236
|
+
* already-cancelled run clears the timer and rejects without waiting a tick.
|
|
237
|
+
*/
|
|
238
|
+
function waitBeforeResend(
|
|
239
|
+
ms: number,
|
|
240
|
+
token: CancellationToken | undefined,
|
|
241
|
+
step: InvokeStep,
|
|
242
|
+
pending: unknown,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
return new Promise<void>((resolve, reject) => {
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
unsubscribe?.();
|
|
247
|
+
resolve();
|
|
248
|
+
}, ms);
|
|
249
|
+
const unsubscribe = token?.onCancelled((reason) => {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
// Released on BOTH paths. The listener is the one thing this holds, and a
|
|
252
|
+
// subscription outliving the wait it belongs to is a leak per re-attempt.
|
|
253
|
+
unsubscribe?.();
|
|
254
|
+
reject(
|
|
255
|
+
new InvokeError(
|
|
256
|
+
ERR_INVOKE_CANCELLED,
|
|
257
|
+
`Step "${step.name}": cancelled while waiting to re-attempt` +
|
|
258
|
+
`${reason ? ` (${reason})` : ""}.`,
|
|
259
|
+
{ step: step.name, pendingFailure: describeFailure(pending) },
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function describeFailure(err: unknown): { code?: string; message: string } {
|
|
267
|
+
const code = (err as { code?: unknown } | null | undefined)?.code;
|
|
268
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
269
|
+
return { ...(typeof code === "string" ? { code } : {}), message };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The deprecated `delay` duration string, in milliseconds.
|
|
274
|
+
*
|
|
275
|
+
* Delegates to the SDK's one duration grammar rather than restating it: a second
|
|
276
|
+
* spelling here would accept strings `telo check`'s `pattern` rejects, and the
|
|
277
|
+
* two would drift. A malformed value THROWS — falling back to the default would
|
|
278
|
+
* swallow a typo into a silently different backoff, and the schema's `pattern`
|
|
279
|
+
* means anything reaching this already failed static analysis.
|
|
280
|
+
*/
|
|
281
|
+
function parseDuration(value: string | undefined): number | undefined {
|
|
282
|
+
if (value === undefined) return undefined;
|
|
283
|
+
const ms = tryParseDurationMs(value);
|
|
284
|
+
if (ms === null) {
|
|
285
|
+
throw new InvokeError(
|
|
286
|
+
"ERR_INVALID_VALUE",
|
|
287
|
+
`Step retry: invalid 'delay' ${JSON.stringify(value)}; use a number with a unit, e.g. ` +
|
|
288
|
+
`"250ms", "2s", "1.5m", "1h" — or the preferred 'initialDelay', in milliseconds.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return ms;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function dispatch(
|
|
295
|
+
raw: unknown,
|
|
296
|
+
inputs: Record<string, unknown>,
|
|
297
|
+
ctx: InvokeStepContext,
|
|
298
|
+
state: InvokeStepState,
|
|
299
|
+
): Promise<unknown> {
|
|
94
300
|
let result: unknown;
|
|
95
301
|
|
|
96
302
|
if (raw && typeof (raw as Invocable).invoke === "function") {
|
|
@@ -119,9 +325,8 @@ export async function executeInvokeStep(
|
|
|
119
325
|
const instance = state.scope.getInstance(ref.name) as unknown as ResourceInstance;
|
|
120
326
|
result = await ctx.invokeResolved(ref.kind, ref.name, instance, inputs);
|
|
121
327
|
} else {
|
|
122
|
-
result = await ctx.invoke(ref.kind, ref.name, inputs
|
|
328
|
+
result = await ctx.invoke(ref.kind, ref.name, inputs);
|
|
123
329
|
}
|
|
124
330
|
}
|
|
125
|
-
|
|
126
|
-
state.steps[step.name] = { result };
|
|
331
|
+
return result;
|
|
127
332
|
}
|
package/src/resource-context.ts
CHANGED
|
@@ -61,16 +61,19 @@ export type ParsedArgs = Partial<Record<string, string | boolean | string[]>> &
|
|
|
61
61
|
/**
|
|
62
62
|
* Per-call options for a by-name dispatch.
|
|
63
63
|
*
|
|
64
|
-
* A bag rather than a positional context parameter because
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
64
|
+
* A bag rather than a positional context parameter, because `ResourceContext`
|
|
65
|
+
* satisfies `InvokeStepContext` structurally: a positional fourth argument here
|
|
66
|
+
* is reachable from the step leaf's own `invoke` call, so the two surfaces would
|
|
67
|
+
* have to agree on its meaning forever. A named key cannot be filled by accident.
|
|
68
|
+
*
|
|
69
|
+
* `retry` USED to live here and was read by nobody — the step leaf handed it down
|
|
70
|
+
* on one of its four dispatch branches and no kernel path looked at it. It now
|
|
71
|
+
* belongs to the leaf, which is the one place every branch passes through; a key
|
|
72
|
+
* kept here would be a second, inert way to ask for the same thing.
|
|
68
73
|
*/
|
|
69
74
|
export interface InvokeByNameOptions {
|
|
70
75
|
/** Seeds the invocation context, replacing the ambient. */
|
|
71
76
|
ctx?: InvokeContext;
|
|
72
|
-
/** Retry policy, read by `executeInvokeStep`. */
|
|
73
|
-
retry?: unknown;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export interface ResourceContext extends ControllerContext {
|