@telorun/sdk 0.77.0 → 0.80.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 +37 -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/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 +37 -0
- package/src/resource-instance.ts +32 -2
- package/src/step-engine.ts +627 -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,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";
|