@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,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";