@telorun/run 0.1.3 → 0.2.1
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/CHANGELOG.md +82 -0
- package/README.md +3 -3
- package/dist/sequence.d.ts +11 -2
- package/dist/sequence.js +25 -5
- package/package.json +2 -2
- package/src/sequence.ts +57 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# @telorun/run
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [3c4ac58]
|
|
8
|
+
- @telorun/sdk@0.3.2
|
|
9
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 353d7e5: feat: invocable errors — structured error channel end-to-end
|
|
15
|
+
|
|
16
|
+
Invocables and runnables now have a first-class structured-error channel for domain failures (`InvokeError`), distinct from operational failures (plain `Error` / `RuntimeError`). Route handlers branch on named codes via `catches:`; sequences catch with `error.code` / `error.message` / `error.data` / `error.step` context.
|
|
17
|
+
|
|
18
|
+
**SDK** (`@telorun/sdk`)
|
|
19
|
+
|
|
20
|
+
- New `InvokeError` class + `isInvokeError` guard. Symbol-based discrimination (`Symbol.for("telo.InvokeError")`) is dual-realm-safe across pnpm hoist splits, registry modules, and future sandbox isolation.
|
|
21
|
+
- `ResourceDefinition.throws`: declared-throw contract (`codes` map, `inherit: true`, `passthrough: true`).
|
|
22
|
+
- `ResourceContext` / `EvaluationContext` gain `invokeResolved(kind, name, instance, inputs)` for callers that already hold a resolved instance.
|
|
23
|
+
|
|
24
|
+
**Kernel** (`@telorun/kernel`)
|
|
25
|
+
|
|
26
|
+
- Single emission point for invoke-level events: `Invoked` / `InvokeRejected` / `InvokeFailed` / `InvokeRejected.Undeclared`. All call paths (direct invoke, sequence scope path, HTTP route handler) route through the same wrapper.
|
|
27
|
+
- `Telo.Definition.throws:` schema with per-capability restrictions (rule 8: only on Invocable / Runnable).
|
|
28
|
+
- `resolveChildren` now auto-registers bare-kind inline refs when a resource name is supplied without an explicit name on the ref — lets stateless invocables like `Run.Throw` be used inline via `invoke: {kind: Run.Throw}`.
|
|
29
|
+
|
|
30
|
+
**Analyzer** (`@telorun/analyzer`)
|
|
31
|
+
|
|
32
|
+
- New dataflow resolver (`resolve-throws-union.ts`) for `inherit: true` / `passthrough: true` declarations. Walks `x-telo-step-context` arrays generically, applies `try`/`catch` subtraction, detects cycles, memoises per manifest.
|
|
33
|
+
- New coverage validator (`validate-throws-coverage.ts`) — rules 1/2/4/7 for `catches:` lists. Coverage-proving CEL parser recognises `error.code == 'X'`, disjunctions, and `error.code in [...]`. Typed `error.data.<field>` access against per-code `data:` schemas, with intersection narrowing for disjunctive `when:` clauses.
|
|
34
|
+
- New error codes: `UNDECLARED_THROW_CODE`, `UNCOVERED_THROW_CODE`, `UNBOUNDED_UNION_NEEDS_CATCHALL`, `CATCHALL_NOT_LAST`, `INHERIT_WITHOUT_STEP_CONTEXT`.
|
|
35
|
+
|
|
36
|
+
**Run module** (`@telorun/run`)
|
|
37
|
+
|
|
38
|
+
- `Run.Sequence` declares `throws: { inherit: true }`. Its effective union is resolved from step invocables at analysis time.
|
|
39
|
+
- New `Run.Throw` invocable: takes `{code, message, data?}` and throws `InvokeError`. Declared with `throws: { passthrough: true }`; the analyzer resolves constant / `error.code`-inside-catch forms at each call site.
|
|
40
|
+
- Sequence `try`/`catch` `error` context gains `data?: unknown` and now branches on `isInvokeError`.
|
|
41
|
+
|
|
42
|
+
**HTTP server module** (`@telorun/http-server`) — **breaking**
|
|
43
|
+
|
|
44
|
+
- Route-level `response:` is replaced by two channel lists: `returns:` (how to render handler results) and `catches:` (how to render `InvokeError` throws). Applies to both `Http.Api` routes and `Http.Server.notFoundHandler`.
|
|
45
|
+
- Plain `Error` / `RuntimeError` throws skip `catches:` and fall through to Fastify's default 5xx renderer — operational vs. domain failures are now distinct on the wire.
|
|
46
|
+
- `catches:` entries reject `mode: stream` at schema validation (structured errors always render as JSON).
|
|
47
|
+
- Unmatched `returns:` dispatch now throws (surfaces via Fastify's error handler) instead of rendering a silent 500.
|
|
48
|
+
- Every `response:` occurrence across the repo (apps, benchmarks, examples, tests) migrated to `returns:` — no manifest carries the old shape.
|
|
49
|
+
|
|
50
|
+
See `sdk/nodejs/plans/invocable-errors.md` for the full design and rollout phasing.
|
|
51
|
+
|
|
52
|
+
- 31d721e: feat: bearer-token auth for the Telo module registry publish endpoint
|
|
53
|
+
|
|
54
|
+
The registry's `PUT /{namespace}/{name}/{version}` now requires an `Authorization: Bearer <token>` header. Reads stay anonymous. Tokens are provisioned declaratively at boot via `TELO_PUBLISH_TOKEN` and stored as SHA-256 hashes in a `tokens` table joined to `users` and `namespaces`.
|
|
55
|
+
|
|
56
|
+
**Analyzer** (`@telorun/analyzer`) — **breaking for direct API consumers**
|
|
57
|
+
|
|
58
|
+
- `StaticAnalyzer` and `Loader` now accept an optional `{ celHandlers }` in their constructors. Analyzer-only callers (VS Code extension, Docusaurus preview, CLI `check`/`publish`) can omit it and get throwing stubs. Runtime callers (kernel) must supply real handlers.
|
|
59
|
+
- The module-level `celEnvironment` singleton is removed — `precompile.ts` now takes the `Environment` as a parameter.
|
|
60
|
+
- New CEL stdlib function: `sha256(string): string`. Always registered with the correct signature so `env.check()` type-checks; behaviour depends on the supplied handler.
|
|
61
|
+
- The throws-union resolver recognises the new `throw:` step shape (see Run module) and resolves its code at the call site using the same rules as passthrough invocables (literal / `${{ 'LIT' }}` / `${{ error.code }}` in catch).
|
|
62
|
+
- CEL type-check failures now surface as diagnostics. Previously the analyzer only reported schema/type mismatches on valid expressions; `env.check(...)` returning `{ valid: false }` (wrong method, wrong operand types, wrong overload — e.g. `s.slice(7)` on a dyn) was silently dropped. Now surfaces as `SCHEMA_VIOLATION` with a `CEL type error:` message.
|
|
63
|
+
|
|
64
|
+
**Kernel** (`@telorun/kernel`)
|
|
65
|
+
|
|
66
|
+
- Constructs `StaticAnalyzer` and `Loader` with a `node:crypto`-backed `sha256` handler, so CEL templates invoking `sha256()` evaluate at runtime.
|
|
67
|
+
|
|
68
|
+
**Run module** (`@telorun/run`) — **breaking**
|
|
69
|
+
|
|
70
|
+
- `Run.Sequence` gains a first-class `throw:` step variant: `- name: X; throw: { code, message?, data? }` — throws `InvokeError` directly from inside the sequence. Works inside `catch:` blocks via `code: "${{ error.code }}"` for re-raise. A malformed `throw.code` (non-string or empty after expansion) is itself reported as `InvokeError("INVALID_THROW_STEP", …)` rather than a plain Error, so the failure stays in the structured-error channel and a surrounding `catches:` can map it.
|
|
71
|
+
- The `Run.Throw` invocable is removed. Existing `invoke: { kind: Run.Throw }` call sites must migrate to `throw:` steps. The separate kind was redundant with the new step form, and the `throw:` step expresses the intent more directly inside sequences.
|
|
72
|
+
- **Event-stream change:** `throw:` steps do **not** emit a scoped `<Kind>.<name>.InvokeRejected` event the way `Run.Throw` did. The error is thrown from inside the sequence's own `invoke()`, so the enclosing kind's event is what fires (e.g. `Run.Sequence.<handlerName>.InvokeRejected` — or nothing, when an enclosing `try` absorbs the throw). Downstream observers that filtered on `Run.Throw.*.InvokeRejected` must switch filters.
|
|
73
|
+
|
|
74
|
+
**CLI** (`@telorun/cli`)
|
|
75
|
+
|
|
76
|
+
- `telo publish` reads `TELO_REGISTRY_TOKEN` and sends it as `Authorization: Bearer <token>`. Without the env var, publishes to auth-gated registries fail with 401.
|
|
77
|
+
|
|
78
|
+
See `apps/registry/plans/registry-auth.md` for the full plan.
|
|
79
|
+
|
|
80
|
+
### Patch Changes
|
|
81
|
+
|
|
82
|
+
- Updated dependencies [353d7e5]
|
|
83
|
+
- @telorun/sdk@0.3.0
|
|
84
|
+
|
|
3
85
|
## 0.1.3
|
|
4
86
|
|
|
5
87
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Manifests also support directives for dynamic generation: `$let`, `$if`, `$for`,
|
|
|
34
34
|
Here is an example Telo application that defines a simple HTTP API:
|
|
35
35
|
|
|
36
36
|
```yaml
|
|
37
|
-
kind:
|
|
37
|
+
kind: Telo.Application
|
|
38
38
|
metadata:
|
|
39
39
|
name: feedback
|
|
40
40
|
version: 1.0.0
|
|
@@ -45,12 +45,12 @@ targets:
|
|
|
45
45
|
- Migrations
|
|
46
46
|
- Server
|
|
47
47
|
---
|
|
48
|
-
kind:
|
|
48
|
+
kind: Telo.Import
|
|
49
49
|
metadata:
|
|
50
50
|
name: Http
|
|
51
51
|
source: ../modules/http-server
|
|
52
52
|
---
|
|
53
|
-
kind:
|
|
53
|
+
kind: Telo.Import
|
|
54
54
|
metadata:
|
|
55
55
|
name: Sql
|
|
56
56
|
source: ../modules/sql
|
package/dist/sequence.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Invocable, type KindRef, type ResourceContext, type ScopeHandle } from "@telorun/sdk";
|
|
2
2
|
interface RetryPolicy {
|
|
3
3
|
attempts?: number;
|
|
4
4
|
delay?: string;
|
|
@@ -38,7 +38,15 @@ interface TryStep {
|
|
|
38
38
|
catch?: Step[];
|
|
39
39
|
finally?: Step[];
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
interface ThrowStep {
|
|
42
|
+
name: string;
|
|
43
|
+
throw: {
|
|
44
|
+
code: string;
|
|
45
|
+
message?: string;
|
|
46
|
+
data?: unknown;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
type Step = InvokeStep | IfStep | WhileStep | SwitchStep | TryStep | ThrowStep;
|
|
42
50
|
interface RunSequenceManifest {
|
|
43
51
|
metadata: Record<string, string | number | boolean>;
|
|
44
52
|
with?: ScopeHandle;
|
|
@@ -64,6 +72,7 @@ declare class RunSequence {
|
|
|
64
72
|
private executeIfStep;
|
|
65
73
|
private executeWhileStep;
|
|
66
74
|
private executeSwitchStep;
|
|
75
|
+
private executeThrowStep;
|
|
67
76
|
private executeTryStep;
|
|
68
77
|
}
|
|
69
78
|
export declare function register(): void;
|
package/dist/sequence.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InvokeError, isInvokeError, } from "@telorun/sdk";
|
|
1
2
|
function isInvokeStep(step) {
|
|
2
3
|
return "invoke" in step;
|
|
3
4
|
}
|
|
@@ -13,6 +14,9 @@ function isSwitchStep(step) {
|
|
|
13
14
|
function isTryStep(step) {
|
|
14
15
|
return "try" in step;
|
|
15
16
|
}
|
|
17
|
+
function isThrowStep(step) {
|
|
18
|
+
return "throw" in step;
|
|
19
|
+
}
|
|
16
20
|
class RunSequence {
|
|
17
21
|
ctx;
|
|
18
22
|
resource;
|
|
@@ -120,6 +124,8 @@ class RunSequence {
|
|
|
120
124
|
await this.executeSwitchStep(step, steps, scope, extraCtx);
|
|
121
125
|
else if (isTryStep(step))
|
|
122
126
|
await this.executeTryStep(step, steps, scope, extraCtx);
|
|
127
|
+
else if (isThrowStep(step))
|
|
128
|
+
this.executeThrowStep(step, steps, extraCtx);
|
|
123
129
|
else
|
|
124
130
|
throw new Error(`Step "${step.name}" has no recognized type key`);
|
|
125
131
|
}
|
|
@@ -136,7 +142,8 @@ class RunSequence {
|
|
|
136
142
|
else {
|
|
137
143
|
const ref = raw;
|
|
138
144
|
if (scope) {
|
|
139
|
-
|
|
145
|
+
const instance = scope.getInstance(ref.name);
|
|
146
|
+
result = await this.ctx.invokeResolved(ref.kind, ref.name, instance, inputs);
|
|
140
147
|
}
|
|
141
148
|
else {
|
|
142
149
|
result = await this.ctx.invoke(ref.kind, ref.name, inputs, { retry: step.retry });
|
|
@@ -178,6 +185,19 @@ class RunSequence {
|
|
|
178
185
|
throw new Error(`Switch step "${step.name}": no matching case for "${key}" and no default`);
|
|
179
186
|
}
|
|
180
187
|
}
|
|
188
|
+
executeThrowStep(step, steps, extraCtx) {
|
|
189
|
+
const cel = { steps, ...extraCtx };
|
|
190
|
+
const expanded = this.ctx.expandValue(step.throw, cel);
|
|
191
|
+
const code = expanded?.code;
|
|
192
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
193
|
+
// Structured error (not plain Error) so the failure stays in the InvokeError
|
|
194
|
+
// channel and a route's `catches:` list can still map it. The alternative —
|
|
195
|
+
// a plain Error — would skip catches: entirely and fall through to a 500.
|
|
196
|
+
throw new InvokeError("INVALID_THROW_STEP", `Run.Sequence step "${step.name}": throw.code is required and must resolve to a non-empty string`, { step: step.name, code });
|
|
197
|
+
}
|
|
198
|
+
const message = typeof expanded.message === "string" ? expanded.message : code;
|
|
199
|
+
throw new InvokeError(code, message, expanded.data);
|
|
200
|
+
}
|
|
181
201
|
async executeTryStep(step, steps, scope, extraCtx) {
|
|
182
202
|
if (step.when !== undefined && !this.ctx.expandValue(step.when, { steps, ...extraCtx }))
|
|
183
203
|
return;
|
|
@@ -225,11 +245,11 @@ class RunSequence {
|
|
|
225
245
|
}
|
|
226
246
|
}
|
|
227
247
|
function toSequenceError(err, stepName) {
|
|
248
|
+
if (isInvokeError(err)) {
|
|
249
|
+
return { message: err.message, code: err.code, data: err.data, step: stepName };
|
|
250
|
+
}
|
|
228
251
|
const message = err instanceof Error ? err.message : String(err);
|
|
229
|
-
|
|
230
|
-
? err.code
|
|
231
|
-
: null;
|
|
232
|
-
return { message, code, step: stepName };
|
|
252
|
+
return { message, code: null, data: undefined, step: stepName };
|
|
233
253
|
}
|
|
234
254
|
export function register() { }
|
|
235
255
|
export async function create(resource, ctx) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/run",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Telo Run module - Sequence execution for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@telorun/sdk": "0.2
|
|
32
|
+
"@telorun/sdk": "0.3.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^20.0.0",
|
package/src/sequence.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
InvokeError,
|
|
3
|
+
isInvokeError,
|
|
4
|
+
type Invocable,
|
|
5
|
+
type KindRef,
|
|
6
|
+
type ResourceContext,
|
|
7
|
+
type ResourceInstance,
|
|
8
|
+
type ScopeContext,
|
|
9
|
+
type ScopeHandle,
|
|
10
|
+
} from "@telorun/sdk";
|
|
2
11
|
|
|
3
12
|
interface RetryPolicy {
|
|
4
13
|
attempts?: number;
|
|
@@ -45,7 +54,16 @@ interface TryStep {
|
|
|
45
54
|
finally?: Step[];
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
interface ThrowStep {
|
|
58
|
+
name: string;
|
|
59
|
+
throw: {
|
|
60
|
+
code: string;
|
|
61
|
+
message?: string;
|
|
62
|
+
data?: unknown;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Step = InvokeStep | IfStep | WhileStep | SwitchStep | TryStep | ThrowStep;
|
|
49
67
|
|
|
50
68
|
interface RunSequenceManifest {
|
|
51
69
|
metadata: Record<string, string | number | boolean>;
|
|
@@ -59,6 +77,7 @@ interface RunSequenceManifest {
|
|
|
59
77
|
interface SequenceError {
|
|
60
78
|
message: string;
|
|
61
79
|
code: string | null;
|
|
80
|
+
data?: unknown;
|
|
62
81
|
step: string;
|
|
63
82
|
}
|
|
64
83
|
|
|
@@ -77,6 +96,9 @@ function isSwitchStep(step: Step): step is SwitchStep {
|
|
|
77
96
|
function isTryStep(step: Step): step is TryStep {
|
|
78
97
|
return "try" in step;
|
|
79
98
|
}
|
|
99
|
+
function isThrowStep(step: Step): step is ThrowStep {
|
|
100
|
+
return "throw" in step;
|
|
101
|
+
}
|
|
80
102
|
|
|
81
103
|
class RunSequence {
|
|
82
104
|
constructor(
|
|
@@ -196,6 +218,7 @@ class RunSequence {
|
|
|
196
218
|
else if (isWhileStep(step)) await this.executeWhileStep(step, steps, scope, extraCtx);
|
|
197
219
|
else if (isSwitchStep(step)) await this.executeSwitchStep(step, steps, scope, extraCtx);
|
|
198
220
|
else if (isTryStep(step)) await this.executeTryStep(step, steps, scope, extraCtx);
|
|
221
|
+
else if (isThrowStep(step)) this.executeThrowStep(step, steps, extraCtx);
|
|
199
222
|
else throw new Error(`Step "${(step as Step).name}" has no recognized type key`);
|
|
200
223
|
}
|
|
201
224
|
|
|
@@ -217,7 +240,8 @@ class RunSequence {
|
|
|
217
240
|
} else {
|
|
218
241
|
const ref = raw as KindRef<Invocable>;
|
|
219
242
|
if (scope) {
|
|
220
|
-
|
|
243
|
+
const instance = scope.getInstance(ref.name) as unknown as ResourceInstance;
|
|
244
|
+
result = await this.ctx.invokeResolved(ref.kind, ref.name, instance, inputs);
|
|
221
245
|
} else {
|
|
222
246
|
result = await this.ctx.invoke(ref.kind, ref.name, inputs, { retry: step.retry });
|
|
223
247
|
}
|
|
@@ -278,6 +302,32 @@ class RunSequence {
|
|
|
278
302
|
}
|
|
279
303
|
}
|
|
280
304
|
|
|
305
|
+
private executeThrowStep(
|
|
306
|
+
step: ThrowStep,
|
|
307
|
+
steps: Record<string, unknown>,
|
|
308
|
+
extraCtx: Record<string, unknown>,
|
|
309
|
+
): never {
|
|
310
|
+
const cel = { steps, ...extraCtx };
|
|
311
|
+
const expanded = this.ctx.expandValue(step.throw, cel) as {
|
|
312
|
+
code: unknown;
|
|
313
|
+
message?: unknown;
|
|
314
|
+
data?: unknown;
|
|
315
|
+
};
|
|
316
|
+
const code = expanded?.code;
|
|
317
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
318
|
+
// Structured error (not plain Error) so the failure stays in the InvokeError
|
|
319
|
+
// channel and a route's `catches:` list can still map it. The alternative —
|
|
320
|
+
// a plain Error — would skip catches: entirely and fall through to a 500.
|
|
321
|
+
throw new InvokeError(
|
|
322
|
+
"INVALID_THROW_STEP",
|
|
323
|
+
`Run.Sequence step "${step.name}": throw.code is required and must resolve to a non-empty string`,
|
|
324
|
+
{ step: step.name, code },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const message = typeof expanded.message === "string" ? expanded.message : code;
|
|
328
|
+
throw new InvokeError(code, message, expanded.data);
|
|
329
|
+
}
|
|
330
|
+
|
|
281
331
|
private async executeTryStep(
|
|
282
332
|
step: TryStep,
|
|
283
333
|
steps: Record<string, unknown>,
|
|
@@ -329,12 +379,11 @@ class RunSequence {
|
|
|
329
379
|
}
|
|
330
380
|
|
|
331
381
|
function toSequenceError(err: unknown, stepName: string): SequenceError {
|
|
382
|
+
if (isInvokeError(err)) {
|
|
383
|
+
return { message: err.message, code: err.code, data: err.data, step: stepName };
|
|
384
|
+
}
|
|
332
385
|
const message = err instanceof Error ? err.message : String(err);
|
|
333
|
-
|
|
334
|
-
err instanceof Error && (err as Error & { code?: string }).code != null
|
|
335
|
-
? (err as Error & { code?: string }).code!
|
|
336
|
-
: null;
|
|
337
|
-
return { message, code, step: stepName };
|
|
386
|
+
return { message, code: null, data: undefined, step: stepName };
|
|
338
387
|
}
|
|
339
388
|
|
|
340
389
|
export function register(): void {}
|