@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.
Files changed (61) hide show
  1. package/dist/cancellation.d.ts +50 -1
  2. package/dist/cancellation.d.ts.map +1 -1
  3. package/dist/contract-errors.d.ts +8 -1
  4. package/dist/contract-errors.d.ts.map +1 -1
  5. package/dist/contract-errors.js +8 -0
  6. package/dist/durable-run.d.ts +310 -0
  7. package/dist/durable-run.d.ts.map +1 -0
  8. package/dist/durable-run.js +223 -0
  9. package/dist/durable-suspension.d.ts +143 -0
  10. package/dist/durable-suspension.d.ts.map +1 -0
  11. package/dist/durable-suspension.js +153 -0
  12. package/dist/durable-target-encoding.d.ts +49 -0
  13. package/dist/durable-target-encoding.d.ts.map +1 -0
  14. package/dist/durable-target-encoding.js +121 -0
  15. package/dist/duration.d.ts +1 -1
  16. package/dist/duration.js +5 -5
  17. package/dist/evaluation-context.d.ts +16 -0
  18. package/dist/evaluation-context.d.ts.map +1 -1
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -0
  22. package/dist/invoke-step.d.ts +86 -1
  23. package/dist/invoke-step.d.ts.map +1 -1
  24. package/dist/invoke-step.js +261 -18
  25. package/dist/resource-context.d.ts +37 -0
  26. package/dist/resource-context.d.ts.map +1 -1
  27. package/dist/resource-instance.d.ts +21 -1
  28. package/dist/resource-instance.d.ts.map +1 -1
  29. package/dist/resource-instance.js +6 -2
  30. package/dist/step-engine.d.ts +170 -0
  31. package/dist/step-engine.d.ts.map +1 -0
  32. package/dist/step-engine.js +365 -0
  33. package/dist/zone-attribute.d.ts +101 -0
  34. package/dist/zone-attribute.d.ts.map +1 -0
  35. package/dist/zone-attribute.js +130 -0
  36. package/dist/zone-attributes/entries/atomic.json +7 -0
  37. package/dist/zone-attributes/entries/idempotent.json +6 -0
  38. package/dist/zone-attributes/entries/index.d.ts +3 -0
  39. package/dist/zone-attributes/entries/index.d.ts.map +1 -0
  40. package/dist/zone-attributes/entries/index.js +13 -0
  41. package/dist/zone-attributes/entries/no-suspend.json +6 -0
  42. package/dist/zone-attributes/entries/replayed.json +6 -0
  43. package/package.json +1 -1
  44. package/src/cancellation.ts +50 -1
  45. package/src/contract-errors.ts +9 -0
  46. package/src/durable-run.ts +450 -0
  47. package/src/durable-suspension.ts +188 -0
  48. package/src/durable-target-encoding.ts +181 -0
  49. package/src/duration.ts +5 -5
  50. package/src/evaluation-context.ts +17 -0
  51. package/src/index.ts +5 -1
  52. package/src/invoke-step.ts +378 -24
  53. package/src/resource-context.ts +37 -0
  54. package/src/resource-instance.ts +32 -2
  55. package/src/step-engine.ts +627 -0
  56. package/src/zone-attribute.ts +208 -0
  57. package/src/zone-attributes/entries/atomic.json +7 -0
  58. package/src/zone-attributes/entries/idempotent.json +6 -0
  59. package/src/zone-attributes/entries/index.ts +14 -0
  60. package/src/zone-attributes/entries/no-suspend.json +6 -0
  61. 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,3 @@
1
+ /** Every zone-attribute entry file, in the order the registry reads them. */
2
+ export declare const ZONE_ATTRIBUTE_ENTRY_FILES: ReadonlyArray<readonly [file: string, data: unknown]>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/sdk",
3
- "version": "0.77.0",
3
+ "version": "0.80.0",
4
4
  "description": "Telo SDK - Public API for Telo module authors.",
5
5
  "keywords": [
6
6
  "telo",
@@ -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
- export type SpanOutcome = "ok" | "failed" | "rejected" | "cancelled";
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 {
@@ -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];