@telorun/sdk 0.75.0 → 0.79.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/cancellation.d.ts +50 -1
- package/dist/cancellation.d.ts.map +1 -1
- package/dist/contract-errors.d.ts +8 -1
- package/dist/contract-errors.d.ts.map +1 -1
- package/dist/contract-errors.js +8 -0
- package/dist/durable-run.d.ts +310 -0
- package/dist/durable-run.d.ts.map +1 -0
- package/dist/durable-run.js +223 -0
- package/dist/durable-suspension.d.ts +143 -0
- package/dist/durable-suspension.d.ts.map +1 -0
- package/dist/durable-suspension.js +153 -0
- package/dist/durable-target-encoding.d.ts +49 -0
- package/dist/durable-target-encoding.d.ts.map +1 -0
- package/dist/durable-target-encoding.js +121 -0
- package/dist/duration.d.ts +1 -1
- package/dist/duration.js +5 -5
- package/dist/evaluation-context.d.ts +16 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/invoke-step.d.ts +86 -1
- package/dist/invoke-step.d.ts.map +1 -1
- package/dist/invoke-step.js +261 -18
- package/dist/resource-context.d.ts +23 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-instance.d.ts +21 -1
- package/dist/resource-instance.d.ts.map +1 -1
- package/dist/resource-instance.js +6 -2
- package/dist/step-engine.d.ts +170 -0
- package/dist/step-engine.d.ts.map +1 -0
- package/dist/step-engine.js +365 -0
- package/dist/type-schema-ref.d.ts.map +1 -1
- package/dist/type-schema-ref.js +18 -0
- package/dist/zone-attribute.d.ts +101 -0
- package/dist/zone-attribute.d.ts.map +1 -0
- package/dist/zone-attribute.js +130 -0
- package/dist/zone-attributes/entries/atomic.json +7 -0
- package/dist/zone-attributes/entries/idempotent.json +6 -0
- package/dist/zone-attributes/entries/index.d.ts +3 -0
- package/dist/zone-attributes/entries/index.d.ts.map +1 -0
- package/dist/zone-attributes/entries/index.js +13 -0
- package/dist/zone-attributes/entries/no-suspend.json +6 -0
- package/dist/zone-attributes/entries/replayed.json +6 -0
- package/package.json +1 -1
- package/src/cancellation.ts +50 -1
- package/src/contract-errors.ts +9 -0
- package/src/durable-run.ts +450 -0
- package/src/durable-suspension.ts +188 -0
- package/src/durable-target-encoding.ts +181 -0
- package/src/duration.ts +5 -5
- package/src/evaluation-context.ts +17 -0
- package/src/index.ts +5 -1
- package/src/invoke-step.ts +378 -24
- package/src/resource-context.ts +23 -0
- package/src/resource-instance.ts +32 -2
- package/src/step-engine.ts +627 -0
- package/src/type-schema-ref.ts +16 -0
- package/src/zone-attribute.ts +208 -0
- package/src/zone-attributes/entries/atomic.json +7 -0
- package/src/zone-attributes/entries/idempotent.json +6 -0
- package/src/zone-attributes/entries/index.ts +14 -0
- package/src/zone-attributes/entries/no-suspend.json +6 -0
- package/src/zone-attributes/entries/replayed.json +6 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suspension — the signal a parking kind raises, and the latch that catches a
|
|
3
|
+
* swallowed one. Slice 4 of `kernel/specs/durable-execution.md`.
|
|
4
|
+
*
|
|
5
|
+
* **Suspension is a distinct signal, not an error.** It unwinds the stack to the
|
|
6
|
+
* workflow boundary, so a `try:` step must not catch it, a composer's
|
|
7
|
+
* `catches:` must not map it, and a retry policy must not re-attempt it. Those
|
|
8
|
+
* three sites rethrow on {@link isSuspension} before they convert anything.
|
|
9
|
+
*
|
|
10
|
+
* **But naming the known swallowers is not a defence.** The signal passes
|
|
11
|
+
* through every controller between the parking kind and the workflow — HTTP
|
|
12
|
+
* handlers, agent tool loops, a cache view, and any third-party controller with
|
|
13
|
+
* a `catch (e)` in it. A swallowed suspension silently converts a park into a
|
|
14
|
+
* completed step and duplicates every effect after it, and a pure-conduit kernel
|
|
15
|
+
* cannot see that happen. Enumerating the swallowers is unbounded and would go
|
|
16
|
+
* stale on the next module.
|
|
17
|
+
*
|
|
18
|
+
* So it is **latched, not just thrown**: {@link parkRun} records the signal
|
|
19
|
+
* against the run handle at the moment it raises, and the workflow kind treats
|
|
20
|
+
* *an invocation that returned normally while a suspension is latched* as a hard
|
|
21
|
+
* error ({@link assertNotSwallowed}). Detection is O(1), needs no cooperation
|
|
22
|
+
* from the swallower, and turns an unbounded-surface silent corruption into one
|
|
23
|
+
* loud failure at the boundary that owns the run.
|
|
24
|
+
*
|
|
25
|
+
* **The latch lives here rather than on the backend's handle**, and the
|
|
26
|
+
* distinction is what makes it a guarantee: a backend that had to set it could
|
|
27
|
+
* forget to, and the failure of forgetting is the silent corruption this exists
|
|
28
|
+
* to catch. It is a `WeakMap` in the SDK — the one package with a single scope
|
|
29
|
+
* per process (`REALM_COLLAPSE_NAMES` symlinks it onto the kernel's copy), so
|
|
30
|
+
* the payload rule's "provider-private state hangs off an injected instance"
|
|
31
|
+
* does not bite: there is exactly one map however many controller bundles are
|
|
32
|
+
* loaded.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { InvokeContext } from "./cancellation.js";
|
|
36
|
+
import type { DurableRunHandle } from "./durable-run.js";
|
|
37
|
+
import { InvokeError } from "./invoke-error.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The backoff above which a retry PARKS instead of sleeping.
|
|
41
|
+
*
|
|
42
|
+
* A threshold is unavoidable and so is stating where it came from. Below it,
|
|
43
|
+
* sleeping in process is cheaper than a park: a park is a journal write, a
|
|
44
|
+
* process exit and a poller pass, and paying that to save a few seconds of an
|
|
45
|
+
* idle timer is worse on both counts. Above it, holding a process open to wait
|
|
46
|
+
* is precisely what durability exists to stop — and the plan's own retry
|
|
47
|
+
* example (`delay: 10s`, three attempts) sits deliberately below it, while a
|
|
48
|
+
* policy backing off to minutes sits above.
|
|
49
|
+
*
|
|
50
|
+
* Shared with the analyzer, which decides statically whether a step's declared
|
|
51
|
+
* policy COULD suspend, so the check and the behaviour cannot disagree about
|
|
52
|
+
* where the line is.
|
|
53
|
+
*/
|
|
54
|
+
export const SUSPENDING_BACKOFF_MS = 30_000;
|
|
55
|
+
|
|
56
|
+
/** The code every suspension carries, so a runtime that only sees a shape can
|
|
57
|
+
* still recognise one. */
|
|
58
|
+
export const ERR_DURABLE_SUSPENDED = "ERR_DURABLE_SUSPENDED";
|
|
59
|
+
|
|
60
|
+
/** Where a parked run is waiting for. Exactly one half is meaningful to a
|
|
61
|
+
* backend at a time, but both may be present: an await with a deadline parks on
|
|
62
|
+
* its token AND is due at a time. */
|
|
63
|
+
export interface ParkUntil {
|
|
64
|
+
/** Epoch milliseconds at which the run becomes due. */
|
|
65
|
+
readonly at?: number;
|
|
66
|
+
/** The token a delivery must carry to wake this run. */
|
|
67
|
+
readonly token?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The signal itself.
|
|
72
|
+
*
|
|
73
|
+
* An `Error` so it unwinds, and deliberately **not** an {@link InvokeError}: an
|
|
74
|
+
* `InvokeError` is the channel a `catches:` list maps by code, and a suspension
|
|
75
|
+
* that could be named there would be catchable by configuration — which is the
|
|
76
|
+
* corruption, spelled as a feature.
|
|
77
|
+
*/
|
|
78
|
+
export class DurableSuspension extends Error {
|
|
79
|
+
readonly code = ERR_DURABLE_SUSPENDED;
|
|
80
|
+
|
|
81
|
+
constructor(
|
|
82
|
+
/** The run that parked. */
|
|
83
|
+
readonly runId: string,
|
|
84
|
+
/** The step path it parked at — the journal key its park is recorded under. */
|
|
85
|
+
readonly path: string,
|
|
86
|
+
/** The parking resource's `metadata.name`, so the diagnostic names what
|
|
87
|
+
* waited rather than only where. */
|
|
88
|
+
readonly resource: string,
|
|
89
|
+
readonly until: ParkUntil,
|
|
90
|
+
) {
|
|
91
|
+
super(
|
|
92
|
+
`Run '${runId}' parked at '${path}' (${resource}). This is a suspension signal, not a ` +
|
|
93
|
+
`failure: it must reach the workflow that owns the run.`,
|
|
94
|
+
);
|
|
95
|
+
this.name = "DurableSuspension";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Is this the suspension signal? Structural rather than `instanceof`, for the
|
|
100
|
+
* same reason every other cross-boundary test here is: the SDK is one scope per
|
|
101
|
+
* process today, and a runtime that threads a handle it owns should still be
|
|
102
|
+
* recognised. */
|
|
103
|
+
export function isSuspension(err: unknown): err is DurableSuspension {
|
|
104
|
+
return (
|
|
105
|
+
typeof err === "object" &&
|
|
106
|
+
err !== null &&
|
|
107
|
+
(err as { code?: unknown }).code === ERR_DURABLE_SUSPENDED
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const LATCHED = new WeakMap<DurableRunHandle, DurableSuspension>();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Park the run — the one way a parking kind suspends.
|
|
115
|
+
*
|
|
116
|
+
* Latches first, then asks the backend to record the park, then throws. Each of
|
|
117
|
+
* the three is load-bearing. Latching FIRST means a backend whose `park()`
|
|
118
|
+
* itself throws something else still leaves the evidence behind. Throwing here
|
|
119
|
+
* rather than trusting `park()`'s `Promise<never>` means a backend that returns
|
|
120
|
+
* — the one bug that would convert a wait into a completed step — is caught by
|
|
121
|
+
* the seam instead of by whatever ran next.
|
|
122
|
+
*/
|
|
123
|
+
export async function parkRun(
|
|
124
|
+
handle: DurableRunHandle,
|
|
125
|
+
where: { readonly path: string; readonly resource: string },
|
|
126
|
+
until: ParkUntil,
|
|
127
|
+
): Promise<never> {
|
|
128
|
+
const signal = new DurableSuspension(handle.runId, where.path, where.resource, until);
|
|
129
|
+
LATCHED.set(handle, signal);
|
|
130
|
+
await handle.park(where, until);
|
|
131
|
+
throw signal;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The suspension latched against this run, if one was raised during this
|
|
135
|
+
* execution. */
|
|
136
|
+
export function latchedSuspension(handle: DurableRunHandle): DurableSuspension | undefined {
|
|
137
|
+
return LATCHED.get(handle);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The workflow boundary's check: a body that returned normally while a
|
|
142
|
+
* suspension is latched swallowed one.
|
|
143
|
+
*
|
|
144
|
+
* Raised rather than repaired, because there is nothing to repair: every effect
|
|
145
|
+
* after the swallow already ran, un-parked and un-recorded, and the run's own
|
|
146
|
+
* record would say it completed. What is actionable is the pair of names — the
|
|
147
|
+
* resource that parked and the step path — since the swallower is somewhere
|
|
148
|
+
* between them.
|
|
149
|
+
*/
|
|
150
|
+
export function assertNotSwallowed(handle: DurableRunHandle): void {
|
|
151
|
+
const signal = LATCHED.get(handle);
|
|
152
|
+
if (!signal) return;
|
|
153
|
+
throw new InvokeError(
|
|
154
|
+
"ERR_DURABLE_SUSPENSION_SWALLOWED",
|
|
155
|
+
`Run '${signal.runId}': '${signal.resource}' parked the run at step '${signal.path}', but ` +
|
|
156
|
+
`the body returned normally — something between them caught the suspension signal and ` +
|
|
157
|
+
`continued. Every step after the park has now run outside the journal, and the run would ` +
|
|
158
|
+
`have been recorded as completed. A controller in that path has a 'catch' that swallows ` +
|
|
159
|
+
`unknown errors; it must rethrow anything it did not recognise.`,
|
|
160
|
+
{ run: signal.runId, path: signal.path, resource: signal.resource },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Refuse to park inside a zone that forbids it.
|
|
166
|
+
*
|
|
167
|
+
* The runtime half of `noSuspend` — one rule rather than an enumeration of
|
|
168
|
+
* forbidden kinds, so a parking kind added later is covered without touching
|
|
169
|
+
* anything here. The zone's own declared sentence is printed verbatim: it is the
|
|
170
|
+
* author's statement of what is being held open, and nothing generated says it
|
|
171
|
+
* better.
|
|
172
|
+
*/
|
|
173
|
+
export function assertMaySuspend(
|
|
174
|
+
ctx: { zoneAttributes?(ctx?: InvokeContext): readonly { kind: string; attributes: { noSuspend?: string } }[] },
|
|
175
|
+
invokeCtx: InvokeContext | undefined,
|
|
176
|
+
where: { readonly resource: string },
|
|
177
|
+
): void {
|
|
178
|
+
for (const zone of ctx.zoneAttributes?.(invokeCtx) ?? []) {
|
|
179
|
+
const reason = zone.attributes.noSuspend;
|
|
180
|
+
if (!reason) continue;
|
|
181
|
+
throw new InvokeError(
|
|
182
|
+
"ERR_DURABLE_SUSPEND_FORBIDDEN",
|
|
183
|
+
`'${where.resource}' would park the run, but it is inside a ${zone.kind} zone that ` +
|
|
184
|
+
`cannot be held open across a suspension: ${reason}`,
|
|
185
|
+
{ resource: where.resource, zone: zone.kind, reason },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire form of a {@link DurableTarget} — spec §5.3.
|
|
3
|
+
*
|
|
4
|
+
* Written in the slice where something first sends one across a process
|
|
5
|
+
* boundary, and not before: a normative format frozen with no consumer is the
|
|
6
|
+
* failure the sequencing rule exists to prevent. What crosses first is a child
|
|
7
|
+
* kernel, which is a real boundary — it shares no instance graph with its
|
|
8
|
+
* parent, so nothing about a target can survive by accident.
|
|
9
|
+
*
|
|
10
|
+
* **JSON, not a URI.** A journal entry already carries its target as JSON, and a
|
|
11
|
+
* second serialization vocabulary for one value is a second thing to keep
|
|
12
|
+
* agreeing. What this adds over `JSON.stringify` is what a FORMAT has to add:
|
|
13
|
+
* a canonical key order, so two runtimes producing the same identity produce the
|
|
14
|
+
* same bytes and an equality check needs no parser; a version tag, so a later
|
|
15
|
+
* form is refused rather than misread; and a stated set of required fields per
|
|
16
|
+
* form, so an incomplete identity is refused at the encoder rather than
|
|
17
|
+
* resolving to the wrong resource at the far end.
|
|
18
|
+
*
|
|
19
|
+
* **Three forms, discriminated by shape rather than by a tag.** Which one a
|
|
20
|
+
* target is follows from which fields it carries — `scope` makes it scoped,
|
|
21
|
+
* `pointer` makes it inline, neither makes it module-level — and the encoder
|
|
22
|
+
* refuses a value carrying both, since that is not a fourth form but a
|
|
23
|
+
* contradiction.
|
|
24
|
+
*/
|
|
25
|
+
import type { DurableTarget } from "./durable-run.js";
|
|
26
|
+
import { InvokeError } from "./invoke-error.js";
|
|
27
|
+
|
|
28
|
+
/** Bumped only for a change a previous reader would MISREAD. A new optional
|
|
29
|
+
* field a reader can ignore is not one; a change to what an existing field
|
|
30
|
+
* means is. */
|
|
31
|
+
export const DURABLE_TARGET_ENCODING_VERSION = 1;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Canonical key order: `v`, `kind`, `name`, `module`, `pointer`, `scope`, and
|
|
35
|
+
* inside `scope`, `owner`, `site`, `stepPath`.
|
|
36
|
+
*
|
|
37
|
+
* Fixed rather than left to whichever code path built the target, because two
|
|
38
|
+
* paths producing the same identity must produce the same bytes — that is what
|
|
39
|
+
* lets a recipient compare, log and key on the encoded form without decoding it
|
|
40
|
+
* first. Enforced by BUILDING the payload in this order and letting
|
|
41
|
+
* `JSON.stringify` follow insertion order, not by passing these as its replacer
|
|
42
|
+
* array: a replacer array filters keys at EVERY depth, so the nested scope keys
|
|
43
|
+
* are absent from the top-level list and a scoped target would encode with an
|
|
44
|
+
* empty `scope` — the identity's whole distinguishing half, dropped silently.
|
|
45
|
+
*/
|
|
46
|
+
const SCOPE_KEY_ORDER = ["owner", "site", "stepPath"] as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Encode a target for transport.
|
|
50
|
+
*
|
|
51
|
+
* Refuses rather than guesses. An identity missing what its form requires would
|
|
52
|
+
* decode at the far end into a resource that is merely *similar*, and a step
|
|
53
|
+
* executed against the wrong resource is the one failure durable execution must
|
|
54
|
+
* never produce quietly — so an incomplete target is an error at the sender,
|
|
55
|
+
* where the manifest that produced it is still in reach.
|
|
56
|
+
*/
|
|
57
|
+
export function encodeDurableTarget(target: DurableTarget): string {
|
|
58
|
+
if (!target.kind || !target.name) {
|
|
59
|
+
throw new InvokeError(
|
|
60
|
+
"ERR_DURABLE_TARGET_UNENCODABLE",
|
|
61
|
+
`A step target must carry both a kind and a name to cross a process boundary; ` +
|
|
62
|
+
`got kind='${target.kind ?? ""}' name='${target.name ?? ""}'.`,
|
|
63
|
+
{ target },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (target.pointer !== undefined && (target.scoped || target.scope !== undefined)) {
|
|
67
|
+
throw new InvokeError(
|
|
68
|
+
"ERR_DURABLE_TARGET_UNENCODABLE",
|
|
69
|
+
`A step target is declared one way: at module level, inside a 'with:' scope, or ` +
|
|
70
|
+
`inline. This one carries both a scope and a pointer, which describes no ` +
|
|
71
|
+
`declaration site.`,
|
|
72
|
+
{ target },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
// The module is what disambiguates two libraries that each declare a `store`,
|
|
76
|
+
// and a recipient resolving without it would pick whichever it saw first. It
|
|
77
|
+
// is required for every form that leaves the process, and optional only on the
|
|
78
|
+
// in-process interface, where an entry written before it was recorded must
|
|
79
|
+
// still replay.
|
|
80
|
+
if (!target.module) {
|
|
81
|
+
throw new InvokeError(
|
|
82
|
+
"ERR_DURABLE_TARGET_UNENCODABLE",
|
|
83
|
+
`Step target ${target.kind} '${target.name}' carries no declaring module, so a ` +
|
|
84
|
+
`recipient could not tell it from a same-named resource in another module.`,
|
|
85
|
+
{ target },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
// A SCOPED target must carry its tuple, and the check is on `scoped` rather
|
|
89
|
+
// than on the presence of `scope` — otherwise a scoped target whose tuple
|
|
90
|
+
// could not be derived would pass here as module-level, which is precisely the
|
|
91
|
+
// misresolution this refusal exists to prevent: at the far end that identity
|
|
92
|
+
// names a DIFFERENT resource that merely shares the name.
|
|
93
|
+
if (target.scoped || target.scope !== undefined) {
|
|
94
|
+
for (const key of SCOPE_KEY_ORDER) {
|
|
95
|
+
if (target.scope?.[key]) continue;
|
|
96
|
+
throw new InvokeError(
|
|
97
|
+
"ERR_DURABLE_TARGET_UNENCODABLE",
|
|
98
|
+
`Step target ${target.kind} '${target.name}' is declared inside a 'with:' scope ` +
|
|
99
|
+
`but records no ${key}. A scoped instance is distinguished by its scope RUN, so ` +
|
|
100
|
+
`an identity missing that names every run of the scope at once.`,
|
|
101
|
+
{ target },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const payload: Record<string, unknown> = {
|
|
106
|
+
v: DURABLE_TARGET_ENCODING_VERSION,
|
|
107
|
+
kind: target.kind,
|
|
108
|
+
name: target.name,
|
|
109
|
+
module: target.module,
|
|
110
|
+
...(target.pointer === undefined ? {} : { pointer: target.pointer }),
|
|
111
|
+
...(target.scope === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: {
|
|
114
|
+
scope: {
|
|
115
|
+
owner: target.scope.owner,
|
|
116
|
+
site: target.scope.site,
|
|
117
|
+
stepPath: target.scope.stepPath,
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
return JSON.stringify(payload);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Decode a target received from another process.
|
|
126
|
+
*
|
|
127
|
+
* A version this reader does not know is REFUSED, never read as far as it
|
|
128
|
+
* understands: the fields it recognizes may mean something else in a form it has
|
|
129
|
+
* never seen, and resolving anyway is how a step gets executed against a
|
|
130
|
+
* resource nobody named.
|
|
131
|
+
*/
|
|
132
|
+
export function decodeDurableTarget(encoded: string): DurableTarget {
|
|
133
|
+
let parsed: unknown;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(encoded);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new InvokeError(
|
|
138
|
+
"ERR_DURABLE_TARGET_UNDECODABLE",
|
|
139
|
+
`A step target arrived that is not valid JSON: ${(err as Error).message}`,
|
|
140
|
+
{ encoded },
|
|
141
|
+
{ cause: err },
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
145
|
+
throw new InvokeError(
|
|
146
|
+
"ERR_DURABLE_TARGET_UNDECODABLE",
|
|
147
|
+
`A step target arrived that is not an object.`,
|
|
148
|
+
{ encoded },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const raw = parsed as Record<string, unknown>;
|
|
152
|
+
if (raw.v !== DURABLE_TARGET_ENCODING_VERSION) {
|
|
153
|
+
throw new InvokeError(
|
|
154
|
+
"ERR_DURABLE_TARGET_UNDECODABLE",
|
|
155
|
+
`A step target arrived in encoding version ${String(raw.v)}, and this runtime reads ` +
|
|
156
|
+
`version ${DURABLE_TARGET_ENCODING_VERSION}. Reading the fields it recognizes would ` +
|
|
157
|
+
`risk resolving a resource nobody named.`,
|
|
158
|
+
{ encoded },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (typeof raw.kind !== "string" || typeof raw.name !== "string" || typeof raw.module !== "string") {
|
|
162
|
+
throw new InvokeError(
|
|
163
|
+
"ERR_DURABLE_TARGET_UNDECODABLE",
|
|
164
|
+
`A step target arrived without a kind, a name or a module.`,
|
|
165
|
+
{ encoded },
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const scope = raw.scope as Record<string, unknown> | undefined;
|
|
169
|
+
return {
|
|
170
|
+
kind: raw.kind,
|
|
171
|
+
name: raw.name,
|
|
172
|
+
module: raw.module,
|
|
173
|
+
...(typeof raw.pointer === "string" ? { pointer: raw.pointer } : {}),
|
|
174
|
+
...(scope &&
|
|
175
|
+
typeof scope.owner === "string" &&
|
|
176
|
+
typeof scope.site === "string" &&
|
|
177
|
+
typeof scope.stepPath === "string"
|
|
178
|
+
? { scope: { owner: scope.owner, site: scope.site, stepPath: scope.stepPath } }
|
|
179
|
+
: {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/duration.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Duration parsing — the shared time helper for controllers. Telo writes human
|
|
3
|
-
* durations (`250ms`, `2s`, `1.5m`, `1h`) in manifests for timers, TTLs,
|
|
3
|
+
* durations (`250ms`, `2s`, `1.5m`, `1h`, `30d`) in manifests for timers, TTLs,
|
|
4
4
|
* windows, connect timeouts, backoff, and so on; this is the single place that
|
|
5
5
|
* turns one into a millisecond count. Consumers own their own error UX:
|
|
6
6
|
* `tryParseDurationMs` returns `null` on a bad string so a caller can throw its
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { RuntimeError } from "./types.js";
|
|
12
12
|
|
|
13
|
-
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
|
|
14
|
-
const UNIT_MS = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 } as const;
|
|
13
|
+
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
14
|
+
const UNIT_MS = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 } as const;
|
|
15
15
|
|
|
16
16
|
/** Parse a duration string to milliseconds, or `null` if it is not a valid
|
|
17
17
|
* duration. Lets the caller own the error message and error type. */
|
|
@@ -21,7 +21,7 @@ export function tryParseDurationMs(value: string): number | null {
|
|
|
21
21
|
return Number(match[1]) * UNIT_MS[match[2] as keyof typeof UNIT_MS];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Parse a duration string (`300s`, `5m`, `1h`, `50ms`) to milliseconds. An
|
|
24
|
+
/** Parse a duration string (`300s`, `5m`, `1h`, `30d`, `50ms`) to milliseconds. An
|
|
25
25
|
* `undefined` value yields `fallback` (default `0`) for optional fields; an
|
|
26
26
|
* invalid string throws a plain `Error`. */
|
|
27
27
|
export function parseDurationMs(value: string | undefined, fallback = 0): number {
|
|
@@ -30,7 +30,7 @@ export function parseDurationMs(value: string | undefined, fallback = 0): number
|
|
|
30
30
|
if (ms === null) {
|
|
31
31
|
throw new RuntimeError(
|
|
32
32
|
"ERR_INVALID_VALUE",
|
|
33
|
-
`Invalid duration ${JSON.stringify(value)}; use a number with a unit, e.g. "250ms", "2s", "1.5m", "1h".`,
|
|
33
|
+
`Invalid duration ${JSON.stringify(value)}; use a number with a unit, e.g. "250ms", "2s", "1.5m", "1h", "30d".`,
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
return ms;
|
|
@@ -115,6 +115,23 @@ export interface EvaluationContext {
|
|
|
115
115
|
|
|
116
116
|
preInitHook?: PreInitHook;
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The manifest a name was DECLARED with, resolved scope-local first and then
|
|
120
|
+
* up the enclosing chain; `alias` routes into that import's exported
|
|
121
|
+
* instances. Undefined when the name resolves to nothing.
|
|
122
|
+
*
|
|
123
|
+
* A DECLARATION, not an instance — nothing here yields one. It is what a
|
|
124
|
+
* contract typed from a referenced declaration reads
|
|
125
|
+
* (`x-telo-schema-projection-from`), which the pending-resource queue cannot
|
|
126
|
+
* answer because it has drained by the time a contract is bound.
|
|
127
|
+
*
|
|
128
|
+
* Optional, like every other kernel-supplied hook on this interface
|
|
129
|
+
* (`getDefinition?`, `preInitHook?`) and for the same reason: a third-party
|
|
130
|
+
* implementation must keep compiling. Absent reads as "no declaration in
|
|
131
|
+
* scope", which leaves a projected slot as its author wrote it.
|
|
132
|
+
*/
|
|
133
|
+
resolveDeclaredManifest?(name: string, alias?: string): ResourceManifest | undefined;
|
|
134
|
+
|
|
118
135
|
/** Looks up a registered resource definition by fully-qualified kind.
|
|
119
136
|
* Set by the kernel; used for declared-throw-union checks. */
|
|
120
137
|
getDefinition?: (kind: string) => ResourceDefinition | undefined;
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./capabilities/invokable.js";
|
|
|
5
5
|
export * from "./ref.js";
|
|
6
6
|
export * from "./type-schema-ref.js";
|
|
7
7
|
export * from "./invoke-step.js";
|
|
8
|
+
export * from "./step-engine.js";
|
|
8
9
|
export * from "./dispatch-invoke-ref.js";
|
|
9
10
|
export * from "./capabilities/provider.js";
|
|
10
11
|
export * from "./capabilities/runnable.js";
|
|
@@ -34,5 +35,8 @@ export * from "./runtime-event.js";
|
|
|
34
35
|
export * from "./runtime-resource.js";
|
|
35
36
|
export * from "./stream.js";
|
|
36
37
|
export * from "./types.js";
|
|
38
|
+
export * from "./durable-run.js";
|
|
39
|
+
export * from "./durable-suspension.js";
|
|
40
|
+
export * from "./durable-target-encoding.js";
|
|
37
41
|
export * from "./value-type.js";
|
|
38
|
-
|
|
42
|
+
export * from "./zone-attribute.js";
|