@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zone attributes — what an `x-telo-provides-zone` object form declares about
|
|
3
|
+
* the region a body slot establishes, and the single accessor every surface
|
|
4
|
+
* reads that vocabulary through (the `value-type.ts` precedent, itself the
|
|
5
|
+
* `ref-slot.ts` one).
|
|
6
|
+
*
|
|
7
|
+
* A body slot that CONSTRAINS its contents is a body slot that already
|
|
8
|
+
* ESTABLISHES a zone — a transaction, a lease, an idempotency claim, a durable
|
|
9
|
+
* run are all of them — so the constraints are attributes on the annotation
|
|
10
|
+
* rather than a second annotation family that would have to restate the zone's
|
|
11
|
+
* location, its `extends` resolution and its runtime open call.
|
|
12
|
+
*
|
|
13
|
+
* THE VOCABULARY IS DATA; THE MEANING IS THE CONSUMER'S. Entries live at
|
|
14
|
+
* `sdk/zone-attributes/*.json` (see the README there) and are copied in by the
|
|
15
|
+
* root `prepare`. Both kernels read the identical files, because `noSuspend` is
|
|
16
|
+
* what stops a run parking inside a lease wherever that run executes. An entry
|
|
17
|
+
* declares a name, a value schema and its `requires:` dependencies, and no code:
|
|
18
|
+
* there is nothing per entry to implement.
|
|
19
|
+
*
|
|
20
|
+
* THE SET IS CLOSED. `x-telo-ref`'s `use` is a closed set on the same annotation
|
|
21
|
+
* family and nothing has needed to extend it; capabilities and value types are
|
|
22
|
+
* closed. The argument for openness inverts on inspection — `metadata.categories`
|
|
23
|
+
* is open precisely because NOTHING BRANCHES ON IT, while every zone attribute
|
|
24
|
+
* exists to be branched on and every reader is core. And a third-party attribute
|
|
25
|
+
* could only ever be HALF an attribute: a module cannot contribute an analyzer
|
|
26
|
+
* pass, so it would get a runtime reader here and no static check, while the
|
|
27
|
+
* failure directions that justify validating this vocabulary at all — an unread
|
|
28
|
+
* `noSuspend`, an unread `atomic` — are exactly the ones only a static check
|
|
29
|
+
* catches.
|
|
30
|
+
*
|
|
31
|
+
* THE REGISTRY IS IN THE SDK for the reasons the value-type one is: it is
|
|
32
|
+
* dependency-free and Node-built-in-free (so the browser-side analyzer can read
|
|
33
|
+
* it), and it is the only placement a module controller can reach.
|
|
34
|
+
*/
|
|
35
|
+
import { ZONE_ATTRIBUTE_ENTRY_FILES } from "./zone-attributes/entries/index.js";
|
|
36
|
+
class ZoneAttributeEntryError extends Error {
|
|
37
|
+
constructor(file, detail) {
|
|
38
|
+
super(`Invalid zone-attribute entry '${file}': ${detail}`);
|
|
39
|
+
this.name = "ZoneAttributeEntryError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const ENTRY_KEYS = ["name", "value", "requires", "description", "$comment"];
|
|
43
|
+
function isPlainObject(value) {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function requireString(file, node, key) {
|
|
47
|
+
const value = node[key];
|
|
48
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
49
|
+
throw new ZoneAttributeEntryError(file, `'${key}' must be a non-empty string`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read one entry file's parsed data.
|
|
55
|
+
*
|
|
56
|
+
* Reading is STRICT and the vocabulary is closed at every level, for the reason
|
|
57
|
+
* the value-type reader is: a malformed or typo'd entry's only other outcome is
|
|
58
|
+
* an attribute that quietly is not in the vocabulary — which reads to an author
|
|
59
|
+
* as "unknown name", pointing at their manifest instead of at the entry.
|
|
60
|
+
*/
|
|
61
|
+
export function parseZoneAttributeEntry(file, data) {
|
|
62
|
+
if (!isPlainObject(data))
|
|
63
|
+
throw new ZoneAttributeEntryError(file, "an entry must be a mapping");
|
|
64
|
+
for (const key of Object.keys(data)) {
|
|
65
|
+
if (!ENTRY_KEYS.includes(key)) {
|
|
66
|
+
throw new ZoneAttributeEntryError(file, `an entry has no key '${key}'. Known keys: ${ENTRY_KEYS.join(", ")}.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const name = requireString(file, data, "name");
|
|
70
|
+
// Bare names, checked here rather than left to convention: a qualified one
|
|
71
|
+
// would be a key nothing resolves, and the closed set has nothing to qualify
|
|
72
|
+
// against.
|
|
73
|
+
if (!/^[a-z][A-Za-z0-9]*$/.test(name)) {
|
|
74
|
+
throw new ZoneAttributeEntryError(file, `'name' must be a bare camelCase word — the annotation position already implies ` +
|
|
75
|
+
`the namespace, and a closed set has no second namespace to qualify against`);
|
|
76
|
+
}
|
|
77
|
+
if (!isPlainObject(data.value)) {
|
|
78
|
+
throw new ZoneAttributeEntryError(file, "'value' must be a JSON Schema mapping");
|
|
79
|
+
}
|
|
80
|
+
const requires = data.requires === undefined ? [] : data.requires;
|
|
81
|
+
if (!Array.isArray(requires) || requires.some((r) => typeof r !== "string" || !r)) {
|
|
82
|
+
throw new ZoneAttributeEntryError(file, "'requires' must be a sequence of attribute names");
|
|
83
|
+
}
|
|
84
|
+
if (requires.includes(name)) {
|
|
85
|
+
throw new ZoneAttributeEntryError(file, `'requires' names '${name}' itself`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
name,
|
|
89
|
+
value: data.value,
|
|
90
|
+
requires: requires,
|
|
91
|
+
description: requireString(file, data, "description"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildRegistry() {
|
|
95
|
+
const registry = new Map();
|
|
96
|
+
for (const [file, data] of ZONE_ATTRIBUTE_ENTRY_FILES) {
|
|
97
|
+
const entry = parseZoneAttributeEntry(file, data);
|
|
98
|
+
if (registry.has(entry.name)) {
|
|
99
|
+
throw new ZoneAttributeEntryError(file, `'${entry.name}' is already declared by another entry`);
|
|
100
|
+
}
|
|
101
|
+
registry.set(entry.name, entry);
|
|
102
|
+
}
|
|
103
|
+
// A `requires:` naming an attribute no entry declares would compile to a
|
|
104
|
+
// `dependentRequired` clause nothing can ever satisfy, so every declaration of
|
|
105
|
+
// the depending attribute would be rejected with no way to fix it. Checked
|
|
106
|
+
// after the whole set is read, since entries are order-independent.
|
|
107
|
+
for (const entry of registry.values()) {
|
|
108
|
+
for (const dependency of entry.requires) {
|
|
109
|
+
if (!registry.has(dependency)) {
|
|
110
|
+
throw new ZoneAttributeEntryError(`${entry.name}.json`, `'requires' names '${dependency}', which no entry declares`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Defence in depth against the packaging mistake, whose failure is
|
|
115
|
+
// indistinguishable from an author's typo: every declared attribute becomes an
|
|
116
|
+
// unknown name, reported against manifests that are correct.
|
|
117
|
+
if (registry.size === 0) {
|
|
118
|
+
throw new Error("The zone-attribute vocabulary is empty. `sdk/zone-attributes/*.json` did not reach " +
|
|
119
|
+
"this build — check the file allowlist of whatever packaged it. Continuing would " +
|
|
120
|
+
"report every declared attribute as an unknown name, while enforcing none of them.");
|
|
121
|
+
}
|
|
122
|
+
return registry;
|
|
123
|
+
}
|
|
124
|
+
/** Every declared zone attribute, keyed by its bare name. */
|
|
125
|
+
export const ZONE_ATTRIBUTES = buildRegistry();
|
|
126
|
+
/** The declared names, in entry order — what a diagnostic listing the closed
|
|
127
|
+
* vocabulary prints. */
|
|
128
|
+
export function zoneAttributeNames() {
|
|
129
|
+
return [...ZONE_ATTRIBUTES.keys()];
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Distinct from `idempotent`, and the exactly-once machinery reads the difference literally: per-step journaling is safe inside a transaction PRECISELY because a rollback erases the journal's own entries too. A region that rolls nothing back has no such property, so it declares `idempotent` instead — borrowing this name there would let a journal's attestation relax collapse and record entries for effects that will re-run.",
|
|
3
|
+
"name": "atomic",
|
|
4
|
+
"requires": ["noSuspend"],
|
|
5
|
+
"value": { "type": "string", "minLength": 1 },
|
|
6
|
+
"description": "Effects inside are discarded together on failure, so a consumer recording them individually would record work a rollback erases."
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Worn by a provider that ENFORCES the property (Idempotency.Once, whose claim makes it true) and by one that merely ASSERTS it on the author's word (Durable.Idempotent). Which one an author reaches for is a cost decision, visible in the manifest as a different kind rather than as a flag on one.",
|
|
3
|
+
"name": "idempotent",
|
|
4
|
+
"value": { "type": "string", "minLength": 1 },
|
|
5
|
+
"description": "Re-executing the zone is observably a no-op — the same writes land, or land once. Nothing is discarded, so unlike an atomic region there is no rollback for a consumer's own records to participate in."
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/zone-attributes/entries/index.ts"],"names":[],"mappings":"AAOA,6EAA6E;AAC7E,eAAO,MAAM,0BAA0B,EAAE,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAK5F,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// GENERATED by scripts/copy-zone-attribute-entries.mjs — do not edit, and do not commit.
|
|
2
|
+
// Source: sdk/zone-attributes/*.json (lexically ordered).
|
|
3
|
+
import e0 from "./atomic.json" with { type: "json" };
|
|
4
|
+
import e1 from "./idempotent.json" with { type: "json" };
|
|
5
|
+
import e2 from "./no-suspend.json" with { type: "json" };
|
|
6
|
+
import e3 from "./replayed.json" with { type: "json" };
|
|
7
|
+
/** Every zone-attribute entry file, in the order the registry reads them. */
|
|
8
|
+
export const ZONE_ATTRIBUTE_ENTRY_FILES = [
|
|
9
|
+
["atomic.json", e0],
|
|
10
|
+
["idempotent.json", e1],
|
|
11
|
+
["no-suspend.json", e2],
|
|
12
|
+
["replayed.json", e3],
|
|
13
|
+
];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "The clearest case for the reason-as-value design: one name is worn by a transaction holding a connection, a lease that lapses unrenewed, an idempotency claim, and later a deadline scope whose bound would elapse while parked. Four providers, four different reasons, one attribute — without the prose the pressure would be to invent `connectionHeld`, `leaseHeld` and `deadlineBound` as separate entries.",
|
|
3
|
+
"name": "noSuspend",
|
|
4
|
+
"value": { "type": "string", "minLength": 1 },
|
|
5
|
+
"description": "The zone holds something bounded that cannot outlive the current process — a connection, a lease, a claim — so execution inside it must not park and resume elsewhere."
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "This attribute, and nothing else, is what makes a zone durable to the static checks. Keying them on the bare `x-telo-provides-zone` annotation would apply DURABLE_NONDETERMINISM and its siblings inside every Sql.Transaction in the ecosystem, since transactions, leases and idempotency claims all provide zones. Keying them on a `Durable.Run` kind would instead put a module's kind into analyzer code.",
|
|
3
|
+
"name": "replayed",
|
|
4
|
+
"value": { "type": "string", "minLength": 1 },
|
|
5
|
+
"description": "Execution inside may be re-run from a record of a previous execution, so it must reach the same decisions on every pass and the values it produces must be serializable."
|
|
6
|
+
}
|
package/package.json
CHANGED
package/src/cancellation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InvokeError } from "./invoke-error.js";
|
|
2
|
+
import type { DurableRunHandle } from "./durable-run.js";
|
|
2
3
|
import type { ResourceHandle } from "./resource-instance.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -72,6 +73,49 @@ export interface InvokeContext {
|
|
|
72
73
|
readonly traceId?: string;
|
|
73
74
|
/** Zones open around this invocation, outermost first. Absent = none. */
|
|
74
75
|
readonly zones?: readonly ZoneEntry[];
|
|
76
|
+
/**
|
|
77
|
+
* The durable run this invocation is executing inside, when there is one —
|
|
78
|
+
* the replay seam the step engine journals through
|
|
79
|
+
* (`kernel/specs/durable-execution.md`).
|
|
80
|
+
*
|
|
81
|
+
* **Its own member, not a `ZoneEntry` payload.** The durable zone IS a real
|
|
82
|
+
* zone and rides the stack above, but an entry is three identities *because*
|
|
83
|
+
* that keeps it ABI-serializable and stops any controller reading another
|
|
84
|
+
* module's open state off the stack. A run handle is a live object with
|
|
85
|
+
* methods, and hanging it on the entry would trade that property away for
|
|
86
|
+
* every zone, durable or not. The landed payload rule (provider-private state
|
|
87
|
+
* lives on an instance injected across the boundary) cannot carry it either —
|
|
88
|
+
* a nested `Run.Sequence` holds no durable reference, so a sequence two levels
|
|
89
|
+
* down has no injected instance to read from.
|
|
90
|
+
*
|
|
91
|
+
* The consequence, stated rather than implied: unlike {@link zones}, this
|
|
92
|
+
* member does **not** cross the ABI. The kernel is a pure conduit — it carries
|
|
93
|
+
* the handle and never calls it — so a second runtime threads a handle it
|
|
94
|
+
* owns rather than deserializing this one.
|
|
95
|
+
*
|
|
96
|
+
* Picked up by every nested dispatch, which is what makes nesting work with no
|
|
97
|
+
* per-module effort: a nested sequence, in this module or across an import
|
|
98
|
+
* boundary, journals its steps under the outer step's path, so a crash inside
|
|
99
|
+
* it resumes inside it.
|
|
100
|
+
*/
|
|
101
|
+
readonly durable?: DurableRunHandle;
|
|
102
|
+
/**
|
|
103
|
+
* The journal path of the step whose dispatch led here, when this invocation
|
|
104
|
+
* is inside a durable run.
|
|
105
|
+
*
|
|
106
|
+
* The other half of carriage, and without it nesting is silently wrong rather
|
|
107
|
+
* than merely unsupported: a nested step body that started its own paths at
|
|
108
|
+
* `steps` would record `steps/<name>` for every body in the run, so two nested
|
|
109
|
+
* bodies with a same-named step share one key. First-writer-wins then hands
|
|
110
|
+
* the second the first's RESULT — and when both dispatch the same target there
|
|
111
|
+
* is no mismatch to detect, so the run continues with a value produced for a
|
|
112
|
+
* different step.
|
|
113
|
+
*
|
|
114
|
+
* A nested engine reads this as the base its own step paths hang under, which
|
|
115
|
+
* is what makes "a crash inside a nested body resumes inside it" true. Absent
|
|
116
|
+
* at the top of a run, where the base is `steps`.
|
|
117
|
+
*/
|
|
118
|
+
readonly durablePath?: string;
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
/**
|
|
@@ -87,7 +131,12 @@ export function deriveContext(base: InvokeContext, overrides: Partial<InvokeCont
|
|
|
87
131
|
}
|
|
88
132
|
|
|
89
133
|
/** Terminal status of a span — maps to OpenTelemetry span status. */
|
|
90
|
-
|
|
134
|
+
/** How an invocation ended.
|
|
135
|
+
*
|
|
136
|
+
* `parked` is its own outcome and not a flavour of failure: a suspended
|
|
137
|
+
* invocation neither succeeded nor failed, and recording it as failed would
|
|
138
|
+
* make a trace say the run broke every time it waited. */
|
|
139
|
+
export type SpanOutcome = "ok" | "failed" | "rejected" | "cancelled" | "parked";
|
|
91
140
|
|
|
92
141
|
/** Options for {@link ResourceContext.openSpan}. */
|
|
93
142
|
export interface OpenSpanOptions {
|
package/src/contract-errors.ts
CHANGED
|
@@ -26,10 +26,19 @@ export const ERR_OUTPUT_INVALID = "ERR_OUTPUT_INVALID";
|
|
|
26
26
|
* quietly switch itself off. */
|
|
27
27
|
export const ERR_CONTRACT_UNRESOLVABLE = "ERR_CONTRACT_UNRESOLVABLE";
|
|
28
28
|
|
|
29
|
+
/** A contract slot declaring `x-telo-schema-projection-from` named a declaration
|
|
30
|
+
* that could not be projected. The same failure as {@link
|
|
31
|
+
* ERR_CONTRACT_UNRESOLVABLE} one level down: the slot promises the shape of a
|
|
32
|
+
* referenced declaration, so leaving it unprojected enforces nothing exactly
|
|
33
|
+
* where it claims to enforce something. Its own code because the repair is
|
|
34
|
+
* different — fix the reference, not the type registration. */
|
|
35
|
+
export const ERR_SCHEMA_PROJECTION_UNRESOLVED = "ERR_SCHEMA_PROJECTION_UNRESOLVED";
|
|
36
|
+
|
|
29
37
|
export const AMBIENT_CONTRACT_ERROR_CODES = [
|
|
30
38
|
ERR_INPUT_INVALID,
|
|
31
39
|
ERR_OUTPUT_INVALID,
|
|
32
40
|
ERR_CONTRACT_UNRESOLVABLE,
|
|
41
|
+
ERR_SCHEMA_PROJECTION_UNRESOLVED,
|
|
33
42
|
] as const;
|
|
34
43
|
|
|
35
44
|
export type AmbientContractErrorCode = (typeof AMBIENT_CONTRACT_ERROR_CODES)[number];
|