@telorun/sdk 0.73.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/bigint-json.d.ts +21 -0
- package/dist/bigint-json.d.ts.map +1 -1
- package/dist/bigint-json.js +28 -0
- 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/dist/value-type.d.ts +17 -0
- package/dist/value-type.d.ts.map +1 -1
- package/dist/value-type.js +36 -5
- package/dist/value-types/entries/telo-stream.json +1 -0
- package/package.json +6 -3
- package/src/bigint-json.ts +27 -0
- package/src/invoke-step.ts +213 -8
- package/src/resource-context.ts +9 -6
- package/src/value-type.ts +42 -5
- package/src/value-types/entries/telo-stream.json +1 -0
package/dist/bigint-json.d.ts
CHANGED
|
@@ -39,4 +39,25 @@ export declare function isBigIntJsonEnabled(): boolean;
|
|
|
39
39
|
* Everything that wants exact digits on the wire needs no replacer at all.
|
|
40
40
|
*/
|
|
41
41
|
export declare function bigIntAt(holder: unknown, key: string): bigint | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* A declared-integer input read as a JS number, whichever representation the
|
|
44
|
+
* call site produced.
|
|
45
|
+
*
|
|
46
|
+
* A CEL integer is an int64 — a BigInt — and the kernel normalizes a declared
|
|
47
|
+
* `type: integer` OUTPUT to that form, so one resource's result reaching another
|
|
48
|
+
* resource's input arrives as a BigInt while a YAML literal at the same slot
|
|
49
|
+
* arrives as a plain number. A controller that reads such an input with
|
|
50
|
+
* `Number.isInteger(...)` or plain arithmetic therefore works for one call site
|
|
51
|
+
* and throws `Cannot mix BigInt and other types` for the other. Inputs are
|
|
52
|
+
* deliberately NOT normalized (that would change the authoring surface of every
|
|
53
|
+
* module rather than repair a false declaration), so this is how a controller
|
|
54
|
+
* reads one.
|
|
55
|
+
*
|
|
56
|
+
* Returns `undefined` for anything that is not an integer in either
|
|
57
|
+
* representation — including a BigInt too large for a double, since silently
|
|
58
|
+
* rounding it would be the precision loss int64 support exists to remove — so a
|
|
59
|
+
* caller's own "must be a non-negative integer" check still rejects what it
|
|
60
|
+
* should.
|
|
61
|
+
*/
|
|
62
|
+
export declare function integerInput(value: unknown): number | undefined;
|
|
42
63
|
//# sourceMappingURL=bigint-json.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bigint-json.d.ts","sourceRoot":"","sources":["../src/bigint-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;uCAGuC;AACvC,eAAO,MAAM,yBAAyB,QAAgB,CAAC;AAEvD,oFAAoF;AACpF,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE"}
|
|
1
|
+
{"version":3,"file":"bigint-json.d.ts","sourceRoot":"","sources":["../src/bigint-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;uCAGuC;AACvC,eAAO,MAAM,yBAAyB,QAAgB,CAAC;AAEvD,oFAAoF;AACpF,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAK/D"}
|
package/dist/bigint-json.js
CHANGED
|
@@ -45,3 +45,31 @@ export function bigIntAt(holder, key) {
|
|
|
45
45
|
const source = holder?.[key];
|
|
46
46
|
return typeof source === "bigint" ? source : undefined;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A declared-integer input read as a JS number, whichever representation the
|
|
50
|
+
* call site produced.
|
|
51
|
+
*
|
|
52
|
+
* A CEL integer is an int64 — a BigInt — and the kernel normalizes a declared
|
|
53
|
+
* `type: integer` OUTPUT to that form, so one resource's result reaching another
|
|
54
|
+
* resource's input arrives as a BigInt while a YAML literal at the same slot
|
|
55
|
+
* arrives as a plain number. A controller that reads such an input with
|
|
56
|
+
* `Number.isInteger(...)` or plain arithmetic therefore works for one call site
|
|
57
|
+
* and throws `Cannot mix BigInt and other types` for the other. Inputs are
|
|
58
|
+
* deliberately NOT normalized (that would change the authoring surface of every
|
|
59
|
+
* module rather than repair a false declaration), so this is how a controller
|
|
60
|
+
* reads one.
|
|
61
|
+
*
|
|
62
|
+
* Returns `undefined` for anything that is not an integer in either
|
|
63
|
+
* representation — including a BigInt too large for a double, since silently
|
|
64
|
+
* rounding it would be the precision loss int64 support exists to remove — so a
|
|
65
|
+
* caller's own "must be a non-negative integer" check still rejects what it
|
|
66
|
+
* should.
|
|
67
|
+
*/
|
|
68
|
+
export function integerInput(value) {
|
|
69
|
+
if (typeof value === "number")
|
|
70
|
+
return Number.isInteger(value) ? value : undefined;
|
|
71
|
+
if (typeof value !== "bigint")
|
|
72
|
+
return undefined;
|
|
73
|
+
const asNumber = Number(value);
|
|
74
|
+
return Number.isSafeInteger(asNumber) ? asNumber : undefined;
|
|
75
|
+
}
|
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/dist/value-type.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export type ValueTypeRepresentation = "json" | "instance";
|
|
|
38
38
|
* second parameter can be added without a migration. */
|
|
39
39
|
export interface ValueTypeParameter {
|
|
40
40
|
readonly name: string;
|
|
41
|
+
/** This parameter's argument is what ITERATING a value of the type yields.
|
|
42
|
+
* Declared here so "what is the element of this collection" is answered by
|
|
43
|
+
* the vocabulary rather than by a consumer that knows one type's name — the
|
|
44
|
+
* same reason `live` is a field and not a check against `Telo.Stream`. At
|
|
45
|
+
* most one parameter per entry may carry it. */
|
|
46
|
+
readonly element?: boolean;
|
|
41
47
|
readonly description?: string;
|
|
42
48
|
}
|
|
43
49
|
/** One value type, exactly as its entry file declares it. */
|
|
@@ -124,6 +130,17 @@ export declare function isLiveSlot(schema: unknown): boolean;
|
|
|
124
130
|
/** True when the node declares a type represented as a runtime instance —
|
|
125
131
|
* the values no manifest literal can ever be. */
|
|
126
132
|
export declare function isInstanceSlot(schema: unknown): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* The schema of what iterating a value at this slot yields, or undefined when
|
|
135
|
+
* the slot declares no value type, or one with no element parameter.
|
|
136
|
+
*
|
|
137
|
+
* The whole point of reading it from the entry is that no consumer names a type:
|
|
138
|
+
* a future iterable value type is covered by declaring `element` on its own
|
|
139
|
+
* parameter, with nothing to change here or in the analyzer. An element
|
|
140
|
+
* parameter left unsupplied means *any*, exactly as every other omitted argument
|
|
141
|
+
* does, so an unparameterized use degrades to permissive rather than to nothing.
|
|
142
|
+
*/
|
|
143
|
+
export declare function elementSchemaOf(schema: unknown): unknown | undefined;
|
|
127
144
|
/** The binding row for a schema node's declared type, or undefined when it
|
|
128
145
|
* declares none / declares a `json` one. */
|
|
129
146
|
export declare function bindingOf(schema: unknown): ValueTypeBinding | undefined;
|
package/dist/value-type.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-type.d.ts","sourceRoot":"","sources":["../src/value-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAEzC;;;;;4EAK4E;AAC5E,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D;;;yDAGyD;AACzD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;iFAC6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;yCAEyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B;oEACgE;IAChE,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;qDAGiD;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;CACtC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAG1E,CAAC;
|
|
1
|
+
{"version":3,"file":"value-type.d.ts","sourceRoot":"","sources":["../src/value-type.ts"],"names":[],"mappings":"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"}
|
package/dist/value-type.js
CHANGED
|
@@ -80,20 +80,34 @@ function readParameters(file, raw) {
|
|
|
80
80
|
return [];
|
|
81
81
|
if (!Array.isArray(raw))
|
|
82
82
|
throw new ValueTypeEntryError(file, "'parameters' must be a sequence");
|
|
83
|
-
|
|
83
|
+
const params = raw.map((entry, i) => {
|
|
84
84
|
if (!isPlainObject(entry)) {
|
|
85
85
|
throw new ValueTypeEntryError(file, `parameters[${i}] must be a mapping`);
|
|
86
86
|
}
|
|
87
87
|
for (const key of Object.keys(entry)) {
|
|
88
|
-
if (key !== "name" && key !== "description") {
|
|
88
|
+
if (key !== "name" && key !== "description" && key !== "element") {
|
|
89
89
|
throw new ValueTypeEntryError(file, `parameters[${i}] has no key '${key}'`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
if (entry.element !== undefined && typeof entry.element !== "boolean") {
|
|
93
|
+
throw new ValueTypeEntryError(file, `parameters[${i}].element must be a boolean when present`);
|
|
94
|
+
}
|
|
92
95
|
const name = requireString(file, entry, "name");
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
return {
|
|
97
|
+
name,
|
|
98
|
+
...(entry.element === true ? { element: true } : {}),
|
|
99
|
+
...(entry.description === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { description: requireString(file, entry, "description") }),
|
|
102
|
+
};
|
|
96
103
|
});
|
|
104
|
+
// Two element parameters would make "the element of this value" ambiguous, and
|
|
105
|
+
// the reader is the only place that can refuse it — every consumer takes the
|
|
106
|
+
// first match and would silently pick one.
|
|
107
|
+
if (params.filter((p) => p.element).length > 1) {
|
|
108
|
+
throw new ValueTypeEntryError(file, "at most one parameter may declare 'element'");
|
|
109
|
+
}
|
|
110
|
+
return params;
|
|
97
111
|
}
|
|
98
112
|
/**
|
|
99
113
|
* Read one entry file's parsed data.
|
|
@@ -242,6 +256,23 @@ export function isLiveSlot(schema) {
|
|
|
242
256
|
export function isInstanceSlot(schema) {
|
|
243
257
|
return valueTypeOf(schema)?.representation === "instance";
|
|
244
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The schema of what iterating a value at this slot yields, or undefined when
|
|
261
|
+
* the slot declares no value type, or one with no element parameter.
|
|
262
|
+
*
|
|
263
|
+
* The whole point of reading it from the entry is that no consumer names a type:
|
|
264
|
+
* a future iterable value type is covered by declaring `element` on its own
|
|
265
|
+
* parameter, with nothing to change here or in the analyzer. An element
|
|
266
|
+
* parameter left unsupplied means *any*, exactly as every other omitted argument
|
|
267
|
+
* does, so an unparameterized use degrades to permissive rather than to nothing.
|
|
268
|
+
*/
|
|
269
|
+
export function elementSchemaOf(schema) {
|
|
270
|
+
const slot = readValueTypeSlot(schema);
|
|
271
|
+
const parameter = slot?.entry?.parameters.find((p) => p.element);
|
|
272
|
+
if (!parameter)
|
|
273
|
+
return undefined;
|
|
274
|
+
return slot.args[parameter.name] ?? {};
|
|
275
|
+
}
|
|
245
276
|
/** The binding row for a schema node's declared type, or undefined when it
|
|
246
277
|
* declares none / declares a `json` one. */
|
|
247
278
|
export function bindingOf(schema) {
|
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/bigint-json.ts
CHANGED
|
@@ -49,3 +49,30 @@ export function bigIntAt(holder: unknown, key: string): bigint | undefined {
|
|
|
49
49
|
const source = (holder as Record<string, unknown> | null | undefined)?.[key];
|
|
50
50
|
return typeof source === "bigint" ? source : undefined;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A declared-integer input read as a JS number, whichever representation the
|
|
55
|
+
* call site produced.
|
|
56
|
+
*
|
|
57
|
+
* A CEL integer is an int64 — a BigInt — and the kernel normalizes a declared
|
|
58
|
+
* `type: integer` OUTPUT to that form, so one resource's result reaching another
|
|
59
|
+
* resource's input arrives as a BigInt while a YAML literal at the same slot
|
|
60
|
+
* arrives as a plain number. A controller that reads such an input with
|
|
61
|
+
* `Number.isInteger(...)` or plain arithmetic therefore works for one call site
|
|
62
|
+
* and throws `Cannot mix BigInt and other types` for the other. Inputs are
|
|
63
|
+
* deliberately NOT normalized (that would change the authoring surface of every
|
|
64
|
+
* module rather than repair a false declaration), so this is how a controller
|
|
65
|
+
* reads one.
|
|
66
|
+
*
|
|
67
|
+
* Returns `undefined` for anything that is not an integer in either
|
|
68
|
+
* representation — including a BigInt too large for a double, since silently
|
|
69
|
+
* rounding it would be the precision loss int64 support exists to remove — so a
|
|
70
|
+
* caller's own "must be a non-negative integer" check still rejects what it
|
|
71
|
+
* should.
|
|
72
|
+
*/
|
|
73
|
+
export function integerInput(value: unknown): number | undefined {
|
|
74
|
+
if (typeof value === "number") return Number.isInteger(value) ? value : undefined;
|
|
75
|
+
if (typeof value !== "bigint") return undefined;
|
|
76
|
+
const asNumber = Number(value);
|
|
77
|
+
return Number.isSafeInteger(asNumber) ? asNumber : undefined;
|
|
78
|
+
}
|
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 {
|
package/src/value-type.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type ValueTypeRepresentation = "json" | "instance";
|
|
|
44
44
|
* second parameter can be added without a migration. */
|
|
45
45
|
export interface ValueTypeParameter {
|
|
46
46
|
readonly name: string;
|
|
47
|
+
/** This parameter's argument is what ITERATING a value of the type yields.
|
|
48
|
+
* Declared here so "what is the element of this collection" is answered by
|
|
49
|
+
* the vocabulary rather than by a consumer that knows one type's name — the
|
|
50
|
+
* same reason `live` is a field and not a check against `Telo.Stream`. At
|
|
51
|
+
* most one parameter per entry may carry it. */
|
|
52
|
+
readonly element?: boolean;
|
|
47
53
|
readonly description?: string;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -136,20 +142,34 @@ function requireString(file: string, node: Record<string, unknown>, key: string)
|
|
|
136
142
|
function readParameters(file: string, raw: unknown): ValueTypeParameter[] {
|
|
137
143
|
if (raw === undefined) return [];
|
|
138
144
|
if (!Array.isArray(raw)) throw new ValueTypeEntryError(file, "'parameters' must be a sequence");
|
|
139
|
-
|
|
145
|
+
const params = raw.map((entry, i) => {
|
|
140
146
|
if (!isPlainObject(entry)) {
|
|
141
147
|
throw new ValueTypeEntryError(file, `parameters[${i}] must be a mapping`);
|
|
142
148
|
}
|
|
143
149
|
for (const key of Object.keys(entry)) {
|
|
144
|
-
if (key !== "name" && key !== "description") {
|
|
150
|
+
if (key !== "name" && key !== "description" && key !== "element") {
|
|
145
151
|
throw new ValueTypeEntryError(file, `parameters[${i}] has no key '${key}'`);
|
|
146
152
|
}
|
|
147
153
|
}
|
|
154
|
+
if (entry.element !== undefined && typeof entry.element !== "boolean") {
|
|
155
|
+
throw new ValueTypeEntryError(file, `parameters[${i}].element must be a boolean when present`);
|
|
156
|
+
}
|
|
148
157
|
const name = requireString(file, entry, "name");
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
return {
|
|
159
|
+
name,
|
|
160
|
+
...(entry.element === true ? { element: true as const } : {}),
|
|
161
|
+
...(entry.description === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { description: requireString(file, entry, "description") }),
|
|
164
|
+
};
|
|
152
165
|
});
|
|
166
|
+
// Two element parameters would make "the element of this value" ambiguous, and
|
|
167
|
+
// the reader is the only place that can refuse it — every consumer takes the
|
|
168
|
+
// first match and would silently pick one.
|
|
169
|
+
if (params.filter((p) => p.element).length > 1) {
|
|
170
|
+
throw new ValueTypeEntryError(file, "at most one parameter may declare 'element'");
|
|
171
|
+
}
|
|
172
|
+
return params;
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
/**
|
|
@@ -334,6 +354,23 @@ export function isInstanceSlot(schema: unknown): boolean {
|
|
|
334
354
|
return valueTypeOf(schema)?.representation === "instance";
|
|
335
355
|
}
|
|
336
356
|
|
|
357
|
+
/**
|
|
358
|
+
* The schema of what iterating a value at this slot yields, or undefined when
|
|
359
|
+
* the slot declares no value type, or one with no element parameter.
|
|
360
|
+
*
|
|
361
|
+
* The whole point of reading it from the entry is that no consumer names a type:
|
|
362
|
+
* a future iterable value type is covered by declaring `element` on its own
|
|
363
|
+
* parameter, with nothing to change here or in the analyzer. An element
|
|
364
|
+
* parameter left unsupplied means *any*, exactly as every other omitted argument
|
|
365
|
+
* does, so an unparameterized use degrades to permissive rather than to nothing.
|
|
366
|
+
*/
|
|
367
|
+
export function elementSchemaOf(schema: unknown): unknown | undefined {
|
|
368
|
+
const slot = readValueTypeSlot(schema);
|
|
369
|
+
const parameter = slot?.entry?.parameters.find((p) => p.element);
|
|
370
|
+
if (!parameter) return undefined;
|
|
371
|
+
return slot!.args[parameter.name] ?? {};
|
|
372
|
+
}
|
|
373
|
+
|
|
337
374
|
/** The binding row for a schema node's declared type, or undefined when it
|
|
338
375
|
* declares none / declares a `json` one. */
|
|
339
376
|
export function bindingOf(schema: unknown): ValueTypeBinding | undefined {
|