@bluelibs/runner-dev 6.0.1 → 6.2.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/AI.md +20 -1
- package/README.md +25 -4
- package/dist/cli/generators/scaffold/templates/README.md.js +11 -2
- package/dist/cli/generators/scaffold/templates/README.md.js.map +1 -1
- package/dist/cli/generators/scaffold/templates/index.d.ts +0 -2
- package/dist/cli/generators/scaffold/templates/index.js +1 -5
- package/dist/cli/generators/scaffold/templates/index.js.map +1 -1
- package/dist/cli/generators/scaffold/templates/package.json.d.ts +6 -8
- package/dist/cli/generators/scaffold/templates/package.json.js +6 -8
- package/dist/cli/generators/scaffold/templates/package.json.js.map +1 -1
- package/dist/cli/generators/scaffold/templates/src/main.test.ts.js +3 -1
- package/dist/cli/generators/scaffold/templates/src/main.test.ts.js.map +1 -1
- package/dist/cli/generators/scaffold/templates/src/main.ts.js +1 -1
- package/dist/cli/generators/scaffold.js +0 -2
- package/dist/cli/generators/scaffold.js.map +1 -1
- package/dist/cli/init.js +1 -2
- package/dist/cli/init.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/generated/resolvers-types.d.ts +13 -13
- package/dist/index.d.ts +1 -1
- package/dist/resources/cli.config.resource.js +1 -1
- package/dist/resources/cli.config.resource.js.map +1 -1
- package/dist/resources/coverage.resource.js +1 -1
- package/dist/resources/coverage.resource.js.map +1 -1
- package/dist/resources/dev.resource.d.ts +1 -1
- package/dist/resources/dev.resource.js +1 -1
- package/dist/resources/dev.resource.js.map +1 -1
- package/dist/resources/docs.generator.resource.js +1 -1
- package/dist/resources/docs.generator.resource.js.map +1 -1
- package/dist/resources/graphql-accumulator.resource.js +1 -1
- package/dist/resources/graphql-accumulator.resource.js.map +1 -1
- package/dist/resources/graphql.cli.resource.js +1 -1
- package/dist/resources/graphql.cli.resource.js.map +1 -1
- package/dist/resources/graphql.query.cli.task.d.ts +2 -4
- package/dist/resources/graphql.query.cli.task.js +1 -1
- package/dist/resources/graphql.query.cli.task.js.map +1 -1
- package/dist/resources/graphql.query.task.d.ts +2 -4
- package/dist/resources/graphql.query.task.js +1 -1
- package/dist/resources/graphql.query.task.js.map +1 -1
- package/dist/resources/http.tag.js +1 -1
- package/dist/resources/http.tag.js.map +1 -1
- package/dist/resources/introspector.cli.resource.js +1 -1
- package/dist/resources/introspector.cli.resource.js.map +1 -1
- package/dist/resources/introspector.resource.js +1 -1
- package/dist/resources/introspector.resource.js.map +1 -1
- package/dist/resources/live.resource.js +3 -3
- package/dist/resources/live.resource.js.map +1 -1
- package/dist/resources/models/Introspector.d.ts +4 -4
- package/dist/resources/models/Introspector.js +17 -11
- package/dist/resources/models/Introspector.js.map +1 -1
- package/dist/resources/models/durable.tools.js +3 -1
- package/dist/resources/models/durable.tools.js.map +1 -1
- package/dist/resources/models/initializeFromStore.utils.js +92 -29
- package/dist/resources/models/initializeFromStore.utils.js.map +1 -1
- package/dist/resources/models/introspector.tools.js +15 -15
- package/dist/resources/models/introspector.tools.js.map +1 -1
- package/dist/resources/routeHandlers/registerHttpRoutes.hook.js +1 -1
- package/dist/resources/routeHandlers/registerHttpRoutes.hook.js.map +1 -1
- package/dist/resources/server.resource.js +1 -1
- package/dist/resources/server.resource.js.map +1 -1
- package/dist/resources/swap.cli.resource.js +1 -1
- package/dist/resources/swap.cli.resource.js.map +1 -1
- package/dist/resources/swap.resource.js +1 -1
- package/dist/resources/swap.resource.js.map +1 -1
- package/dist/resources/telemetry.resource.js +27 -6
- package/dist/resources/telemetry.resource.js.map +1 -1
- package/dist/runtime/symbolMetadata.d.ts +1 -0
- package/dist/{architect/core/interfaces.js → runtime/symbolMetadata.js} +1 -1
- package/dist/runtime/symbolMetadata.js.map +1 -0
- package/dist/schema/query.js +3 -1
- package/dist/schema/query.js.map +1 -1
- package/dist/schema/types/AsyncContextType.js +21 -17
- package/dist/schema/types/AsyncContextType.js.map +1 -1
- package/dist/ui/.vite/manifest.json +2 -2
- package/dist/ui/assets/{docs-CipvKUxZ.css → docs-CWJO6emS.css} +1 -1
- package/dist/ui/assets/{docs-Btkv97Ls.js → docs-Zej7hPlV.js} +52 -52
- package/dist/ui/assets/{docs-Btkv97Ls.js.map → docs-Zej7hPlV.js.map} +1 -1
- package/dist/utils/lane-resources.d.ts +1 -1
- package/dist/utils/lane-resources.js +10 -2
- package/dist/utils/lane-resources.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -9
- package/readmes/runner-AI.md +544 -367
- package/readmes/runner-durable-workflows.md +2 -2
- package/readmes/runner-full-guide.md +3620 -3479
- package/readmes/runner-remote-lanes.md +15 -14
- package/dist/app/tasks/create-user.task.d.ts +0 -5
- package/dist/app/tasks/create-user.task.js +0 -20
- package/dist/app/tasks/create-user.task.js.map +0 -1
- package/dist/app/tasks/index.d.ts +0 -1
- package/dist/app/tasks/index.js +0 -18
- package/dist/app/tasks/index.js.map +0 -1
- package/dist/architect/core/errors.d.ts +0 -39
- package/dist/architect/core/errors.js +0 -143
- package/dist/architect/core/errors.js.map +0 -1
- package/dist/architect/core/index.d.ts +0 -3
- package/dist/architect/core/index.js +0 -21
- package/dist/architect/core/index.js.map +0 -1
- package/dist/architect/core/interfaces.d.ts +0 -158
- package/dist/architect/core/interfaces.js.map +0 -1
- package/dist/architect/core/types.d.ts +0 -544
- package/dist/architect/core/types.js +0 -49
- package/dist/architect/core/types.js.map +0 -1
- package/dist/architect/execution/executor.d.ts +0 -23
- package/dist/architect/execution/executor.js +0 -476
- package/dist/architect/execution/executor.js.map +0 -1
- package/dist/architect/execution/index.d.ts +0 -1
- package/dist/architect/execution/index.js +0 -19
- package/dist/architect/execution/index.js.map +0 -1
- package/dist/architect/executor.d.ts +0 -7
- package/dist/architect/executor.js +0 -150
- package/dist/architect/executor.js.map +0 -1
- package/dist/architect/index.d.ts +0 -45
- package/dist/architect/index.js +0 -76
- package/dist/architect/index.js.map +0 -1
- package/dist/architect/llmClient.d.ts +0 -10
- package/dist/architect/llmClient.js +0 -33
- package/dist/architect/llmClient.js.map +0 -1
- package/dist/architect/models/base.d.ts +0 -16
- package/dist/architect/models/base.js +0 -68
- package/dist/architect/models/base.js.map +0 -1
- package/dist/architect/models/factory.d.ts +0 -16
- package/dist/architect/models/factory.js +0 -73
- package/dist/architect/models/factory.js.map +0 -1
- package/dist/architect/models/index.d.ts +0 -3
- package/dist/architect/models/index.js +0 -21
- package/dist/architect/models/index.js.map +0 -1
- package/dist/architect/models/openai.d.ts +0 -7
- package/dist/architect/models/openai.js +0 -71
- package/dist/architect/models/openai.js.map +0 -1
- package/dist/architect/planner.d.ts +0 -9
- package/dist/architect/planner.js +0 -42
- package/dist/architect/planner.js.map +0 -1
- package/dist/architect/planning/index.d.ts +0 -4
- package/dist/architect/planning/index.js +0 -22
- package/dist/architect/planning/index.js.map +0 -1
- package/dist/architect/planning/optimizer.d.ts +0 -14
- package/dist/architect/planning/optimizer.js +0 -275
- package/dist/architect/planning/optimizer.js.map +0 -1
- package/dist/architect/planning/planner.d.ts +0 -15
- package/dist/architect/planning/planner.js +0 -124
- package/dist/architect/planning/planner.js.map +0 -1
- package/dist/architect/planning/prompts.d.ts +0 -6
- package/dist/architect/planning/prompts.js +0 -111
- package/dist/architect/planning/prompts.js.map +0 -1
- package/dist/architect/planning/validator.d.ts +0 -16
- package/dist/architect/planning/validator.js +0 -331
- package/dist/architect/planning/validator.js.map +0 -1
- package/dist/architect/prompt.d.ts +0 -1
- package/dist/architect/prompt.js +0 -13
- package/dist/architect/prompt.js.map +0 -1
- package/dist/architect/types.d.ts +0 -4
- package/dist/architect/types.js +0 -24
- package/dist/architect/types.js.map +0 -1
- package/dist/cli/generators/scaffold/templates/jest.config.cjs.d.ts +0 -1
- package/dist/cli/generators/scaffold/templates/jest.config.cjs.js +0 -24
- package/dist/cli/generators/scaffold/templates/jest.config.cjs.js.map +0 -1
- package/dist/cli/generators/scaffold/templates/tsconfig.jest.json.d.ts +0 -11
- package/dist/cli/generators/scaffold/templates/tsconfig.jest.json.js +0 -17
- package/dist/cli/generators/scaffold/templates/tsconfig.jest.json.js.map +0 -1
- package/dist/client/documentation.d.ts +0 -8
- package/dist/client/documentation.js +0 -144
- package/dist/client/documentation.js.map +0 -1
- package/dist/components/Documentation/Documentation.d.ts +0 -8
- package/dist/components/Documentation/Documentation.js +0 -283
- package/dist/components/Documentation/Documentation.js.map +0 -1
- package/dist/components/Documentation/components/DiagnosticsPanel.d.ts +0 -7
- package/dist/components/Documentation/components/DiagnosticsPanel.js +0 -189
- package/dist/components/Documentation/components/DiagnosticsPanel.js.map +0 -1
- package/dist/components/Documentation/components/EventCard.d.ts +0 -8
- package/dist/components/Documentation/components/EventCard.js +0 -290
- package/dist/components/Documentation/components/EventCard.js.map +0 -1
- package/dist/components/Documentation/components/HookCard.d.ts +0 -8
- package/dist/components/Documentation/components/HookCard.js +0 -282
- package/dist/components/Documentation/components/HookCard.js.map +0 -1
- package/dist/components/Documentation/components/MiddlewareCard.d.ts +0 -8
- package/dist/components/Documentation/components/MiddlewareCard.js +0 -314
- package/dist/components/Documentation/components/MiddlewareCard.js.map +0 -1
- package/dist/components/Documentation/components/ResourceCard.d.ts +0 -8
- package/dist/components/Documentation/components/ResourceCard.js +0 -228
- package/dist/components/Documentation/components/ResourceCard.js.map +0 -1
- package/dist/components/Documentation/components/Sidebar.d.ts +0 -13
- package/dist/components/Documentation/components/Sidebar.js +0 -165
- package/dist/components/Documentation/components/Sidebar.js.map +0 -1
- package/dist/components/Documentation/components/TagCard.d.ts +0 -7
- package/dist/components/Documentation/components/TagCard.js +0 -75
- package/dist/components/Documentation/components/TagCard.js.map +0 -1
- package/dist/components/Documentation/components/TaskCard.d.ts +0 -8
- package/dist/components/Documentation/components/TaskCard.js +0 -196
- package/dist/components/Documentation/components/TaskCard.js.map +0 -1
- package/dist/components/Documentation/index.d.ts +0 -2
- package/dist/components/Documentation/index.js +0 -6
- package/dist/components/Documentation/index.js.map +0 -1
- package/dist/components/Documentation/utils/formatting.d.ts +0 -8
- package/dist/components/Documentation/utils/formatting.js +0 -84
- package/dist/components/Documentation/utils/formatting.js.map +0 -1
- package/dist/components/ExampleComponent.d.ts +0 -10
- package/dist/components/ExampleComponent.js +0 -89
- package/dist/components/ExampleComponent.js.map +0 -1
- package/dist/mcp/z3.d.ts +0 -1
- package/dist/mcp/z3.js +0 -9
- package/dist/mcp/z3.js.map +0 -1
- package/dist/project-writer/AIModel.d.ts +0 -29
- package/dist/project-writer/AIModel.js +0 -48
- package/dist/project-writer/AIModel.js.map +0 -1
- package/dist/resources/docs.route.d.ts +0 -23
- package/dist/resources/docs.route.js +0 -73
- package/dist/resources/docs.route.js.map +0 -1
- package/dist/resources/durable.workflow.tag.d.ts +0 -2
- package/dist/resources/durable.workflow.tag.js +0 -28
- package/dist/resources/durable.workflow.tag.js.map +0 -1
- package/dist/resources/getFileContents.task.d.ts +0 -17
- package/dist/resources/getFileContents.task.js +0 -44
- package/dist/resources/getFileContents.task.js.map +0 -1
- package/dist/resources/introspector.tools.d.ts +0 -47
- package/dist/resources/introspector.tools.js +0 -505
- package/dist/resources/introspector.tools.js.map +0 -1
- package/dist/resources/models/extractTunnelInfo.d.ts +0 -8
- package/dist/resources/models/extractTunnelInfo.js +0 -85
- package/dist/resources/models/extractTunnelInfo.js.map +0 -1
- package/dist/resources/models/tunnel.tools.d.ts +0 -3
- package/dist/resources/models/tunnel.tools.js +0 -35
- package/dist/resources/models/tunnel.tools.js.map +0 -1
- package/dist/runner-compat.d.ts +0 -85
- package/dist/runner-compat.js +0 -178
- package/dist/runner-compat.js.map +0 -1
- package/dist/runner-node-compat.d.ts +0 -2
- package/dist/runner-node-compat.js +0 -28
- package/dist/runner-node-compat.js.map +0 -1
- package/dist/schema/types/TunnelInfoType.d.ts +0 -5
- package/dist/schema/types/TunnelInfoType.js +0 -86
- package/dist/schema/types/TunnelInfoType.js.map +0 -1
- package/dist/tasks/create-user.d.ts +0 -5
- package/dist/tasks/create-user.js +0 -20
- package/dist/tasks/create-user.js.map +0 -1
- package/dist/tasks/index.d.ts +0 -1
- package/dist/tasks/index.js +0 -18
- package/dist/tasks/index.js.map +0 -1
- package/dist/ui-test/index.html +0 -1
package/readmes/runner-AI.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# BlueLibs Runner: AI Field Guide
|
|
2
2
|
|
|
3
|
-
Runner is a strongly typed application composition framework built around explicit contracts.
|
|
3
|
+
Runner is a strongly typed application composition framework built around explicit contracts.
|
|
4
|
+
You declare a graph of resources, tasks, events, hooks, middleware, tags, and errors, then `run(app)` turns that graph into a constrained runtime with validation, lifecycle, isolation, and observability built in.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**Reading order for agents:** start with the Mental Model below, then Quick Start, then the section matching your task. For full documentation, see the [FULL_GUIDE.md](./FULL_GUIDE.md). For Node-specific features (Async Context, Durable Workflows, Remote Lanes), see the dedicated readmes linked at the end.
|
|
6
|
+
Think "architecture as runtime-enforced structure", not "some modules that hopefully cooperate".
|
|
8
7
|
|
|
9
8
|
## Mental Model
|
|
10
9
|
|
|
11
|
-
- `resource`:
|
|
12
|
-
- `task`:
|
|
13
|
-
- `event`:
|
|
14
|
-
- `hook`:
|
|
15
|
-
- `middleware`:
|
|
16
|
-
- `tag`: metadata
|
|
17
|
-
- `error`:
|
|
10
|
+
- `resource`: singleton with lifecycle (`init`, `ready`, `cooldown`, `dispose`)
|
|
11
|
+
- `task`: typed business action with DI, middleware, and validation
|
|
12
|
+
- `event`: typed signal
|
|
13
|
+
- `hook`: reaction subscribed to an event
|
|
14
|
+
- `middleware`: cross-cutting wrapper around a task or resource
|
|
15
|
+
- `tag`: typed metadata for discovery and policy
|
|
16
|
+
- `error`: typed Runner error helper
|
|
18
17
|
- `run(app)`: bootstraps the graph and returns the runtime API
|
|
19
18
|
|
|
20
|
-
Prefer the flat globals
|
|
19
|
+
Prefer the built-in flat globals exported by Runner:
|
|
21
20
|
|
|
22
21
|
- `resources.*`
|
|
23
22
|
- `events.*`
|
|
24
23
|
- `tags.*`
|
|
24
|
+
- `errors.*`
|
|
25
25
|
- `middleware.*`
|
|
26
26
|
- `debug.levels`
|
|
27
27
|
|
|
@@ -76,44 +76,48 @@ await runtime.runTask(createUser, { email: "ada@example.com" });
|
|
|
76
76
|
await runtime.dispose();
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
## Core
|
|
79
|
+
## Core Rules
|
|
80
80
|
|
|
81
81
|
- Fluent builders chain methods and end with `.build()`.
|
|
82
82
|
- Configurable built definitions expose `.with(config)`.
|
|
83
|
-
- `r.task<Input>(id)` and `r.resource<Config>(id)` seed typing before explicit schemas.
|
|
84
|
-
- User
|
|
85
|
-
|
|
83
|
+
- `r.task<Input>(id)` and `r.resource<Config>(id)` can seed typing before explicit schemas.
|
|
84
|
+
- User ids are local ids. They cannot contain `.`.
|
|
85
|
+
Use `send-email`, not `app.tasks.sendEmail`.
|
|
86
|
+
- Reserved local ids fail fast:
|
|
87
|
+
- `tasks`
|
|
88
|
+
- `resources`
|
|
89
|
+
- `events`
|
|
90
|
+
- `hooks`
|
|
91
|
+
- `tags`
|
|
92
|
+
- `errors`
|
|
93
|
+
- `asyncContexts`
|
|
94
|
+
- Ids cannot start or end with `.`, and cannot contain `..`.
|
|
95
|
+
- Builder order is enforced. After terminal methods such as `.run()` or `.init()`, mutation surfaces intentionally narrow.
|
|
96
|
+
- List builders append by default. Pass `{ override: true }` to replace.
|
|
97
|
+
- `.meta({ ... })` is available across builders for docs and tooling.
|
|
98
|
+
- Prefer local ids such as `task("createUser")`. Runner composes canonical ids from the owner subtree at runtime.
|
|
99
|
+
- Runtime and store internals always expose canonical ids.
|
|
100
|
+
|
|
101
|
+
### Schemas
|
|
102
|
+
|
|
86
103
|
- `.schema()` is the unified alias:
|
|
87
104
|
- task -> input schema
|
|
88
105
|
- resource -> config schema
|
|
89
106
|
- event -> payload schema
|
|
90
107
|
- error -> data schema
|
|
91
|
-
- Explicit
|
|
108
|
+
- Explicit aliases still exist when they read better:
|
|
92
109
|
- `.inputSchema(...)`
|
|
93
110
|
- `.configSchema(...)`
|
|
94
111
|
- `.payloadSchema(...)`
|
|
95
112
|
- `.dataSchema(...)`
|
|
96
113
|
- Tasks use `.resultSchema()` for output validation.
|
|
97
|
-
- Schema
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- Prefer
|
|
104
|
-
- Runner composes canonical ids from the owner subtree at runtime.
|
|
105
|
-
- Runtime and store internals always expose canonical ids.
|
|
106
|
-
- Reserved local names fail fast:
|
|
107
|
-
- `tasks`
|
|
108
|
-
- `resources`
|
|
109
|
-
- `events`
|
|
110
|
-
- `hooks`
|
|
111
|
-
- `tags`
|
|
112
|
-
- `errors`
|
|
113
|
-
- `asyncContexts`
|
|
114
|
-
- Ids cannot start or end with `.`, and cannot contain `..`.
|
|
115
|
-
|
|
116
|
-
Schema quick guide:
|
|
114
|
+
- Schema slots accept:
|
|
115
|
+
- raw Match patterns
|
|
116
|
+
- compiled Match schemas
|
|
117
|
+
- decorator-backed classes
|
|
118
|
+
- any schema object exposing `parse(...)`
|
|
119
|
+
- Schema resolution prefers `parse(input)` when present. Otherwise Runner compiles raw Match patterns once and reuses the compiled schema.
|
|
120
|
+
- Prefer `Match.compile(...)` when you want to reuse a schema value yourself or access `.pattern`, `.parse()`, `.test()`, and `.toJSONSchema()` directly.
|
|
117
121
|
|
|
118
122
|
```ts
|
|
119
123
|
import { Match, r } from "@bluelibs/runner";
|
|
@@ -123,182 +127,222 @@ const userInput = Match.compile({
|
|
|
123
127
|
age: Match.Optional(Match.Integer),
|
|
124
128
|
});
|
|
125
129
|
|
|
126
|
-
const appConfig = Match.compile({
|
|
127
|
-
env: Match.OneOf("dev", "test", "prod"),
|
|
128
|
-
featureFlags: Match.Optional(Match.MapOf(Boolean)),
|
|
129
|
-
});
|
|
130
|
-
|
|
131
130
|
const createUser = r
|
|
132
131
|
.task("createUser")
|
|
133
|
-
.inputSchema(userInput)
|
|
132
|
+
.inputSchema(userInput)
|
|
133
|
+
.resultSchema({
|
|
134
|
+
id: Match.NonEmptyString,
|
|
135
|
+
email: Match.Email,
|
|
136
|
+
})
|
|
134
137
|
.run(async (input) => ({ id: "u1", ...input }))
|
|
135
|
-
.resultSchema({ id: Match.NonEmptyString, email: Match.Email })
|
|
136
|
-
.build();
|
|
137
|
-
|
|
138
|
-
const app = r
|
|
139
|
-
.resource("app")
|
|
140
|
-
.configSchema(appConfig) // same as .schema(appConfig)
|
|
141
|
-
.register([createUser])
|
|
142
138
|
.build();
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
.build();
|
|
150
|
-
|
|
151
|
-
// Compiled Match schemas expose:
|
|
152
|
-
userInput.pattern; // original Match pattern
|
|
153
|
-
userInput.parse({ email: "ada@example.com" }); // validate + return typed value
|
|
154
|
-
userInput.test({ email: "ada@example.com" }); // boolean type guard
|
|
155
|
-
userInput.toJSONSchema(); // machine-readable contract for tooling
|
|
140
|
+
userInput.pattern;
|
|
141
|
+
userInput.parse({ email: "ada@example.com" });
|
|
142
|
+
userInput.test({ email: "ada@example.com" });
|
|
143
|
+
userInput.toJSONSchema();
|
|
156
144
|
```
|
|
157
145
|
|
|
158
146
|
## Runtime and Lifecycle
|
|
159
147
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
- `
|
|
184
|
-
|
|
185
|
-
- `
|
|
186
|
-
- `
|
|
187
|
-
- `
|
|
188
|
-
- `executionContext: true | { createCorrelationId?, cycleDetection? }` enables correlation tracking and execution tree recording (Node-only; requires `AsyncLocalStorage`). See "Execution Context and Request Tracing" below.
|
|
189
|
-
- `asyncContexts.execution.use()` returns the current branch snapshot: `{ correlationId, startedAt, depth, currentFrame, frames }`.
|
|
148
|
+
`run(app, options?)` wires dependencies, initializes resources, emits lifecycle events, and returns the runtime API.
|
|
149
|
+
|
|
150
|
+
Main runtime helpers:
|
|
151
|
+
|
|
152
|
+
- `runTask`
|
|
153
|
+
- `emitEvent`
|
|
154
|
+
- `getResourceValue`
|
|
155
|
+
- `getLazyResourceValue`
|
|
156
|
+
- `getResourceConfig`
|
|
157
|
+
- `getHealth`
|
|
158
|
+
- `dispose`
|
|
159
|
+
|
|
160
|
+
The returned runtime also exposes:
|
|
161
|
+
|
|
162
|
+
- `runOptions`: the normalized effective `run(...)` options
|
|
163
|
+
- `mode`: `"dev" | "prod" | "test"`
|
|
164
|
+
- `state`: `"running" | "paused"`
|
|
165
|
+
|
|
166
|
+
Important run options:
|
|
167
|
+
|
|
168
|
+
- `dryRun: true`: validate the graph without running `init()` / `ready()` or starting ingress
|
|
169
|
+
- `lazy: true`: keep startup-unused resources asleep until `getLazyResourceValue(...)` wakes them, then run their `ready()` when they initialize
|
|
170
|
+
- `lifecycleMode: "parallel"`: preserve dependency ordering, but run same-wave lifecycle hooks in parallel
|
|
171
|
+
- `shutdownHooks: true`: install graceful `SIGINT` / `SIGTERM` hooks; signals during bootstrap cancel startup and roll back initialized resources
|
|
172
|
+
- `dispose: { totalBudgetMs, drainingBudgetMs, cooldownWindowMs }`: control bounded shutdown timing
|
|
173
|
+
- `errorBoundary: true`: install process-level unhandled error capture and route it through `onUnhandledError`
|
|
174
|
+
- `executionContext: true | { ... }`: enable correlation ids and inherited execution signals, with optional frame tracking and cycle detection
|
|
175
|
+
- `mode: "dev" | "prod" | "test"`: override environment-based mode detection
|
|
190
176
|
|
|
191
|
-
|
|
177
|
+
Observability options do not change lifecycle semantics:
|
|
192
178
|
|
|
193
|
-
-
|
|
179
|
+
- `debug`
|
|
180
|
+
- `logs`
|
|
181
|
+
|
|
182
|
+
Useful examples:
|
|
183
|
+
|
|
184
|
+
- `run(app, { debug: "verbose" })` for structured debug output
|
|
185
|
+
- `run(app, { logs: { printThreshold: null } })` to silence console printing
|
|
186
|
+
|
|
187
|
+
Lifecycle order:
|
|
188
|
+
|
|
189
|
+
- Startup:
|
|
194
190
|
- wire dependencies
|
|
195
|
-
- `init`
|
|
191
|
+
- run `init()` in dependency order
|
|
196
192
|
- lock runtime mutation surfaces
|
|
197
193
|
- run `ready()` in dependency order
|
|
198
194
|
- emit `events.ready`
|
|
199
|
-
- Shutdown
|
|
195
|
+
- Shutdown:
|
|
200
196
|
- enter `coolingDown`
|
|
201
197
|
- run `cooldown()` in reverse dependency order
|
|
202
|
-
- keep admissions open
|
|
198
|
+
- optionally keep broader admissions open for `dispose.cooldownWindowMs`
|
|
203
199
|
- enter `disposing`
|
|
204
200
|
- emit `events.disposing`
|
|
205
|
-
- drain in-flight work
|
|
201
|
+
- drain in-flight work within the remaining shutdown budget
|
|
206
202
|
- emit `events.drained`
|
|
207
203
|
- run `dispose()` in reverse dependency order
|
|
208
204
|
|
|
205
|
+
Pause and recovery:
|
|
206
|
+
|
|
207
|
+
- `runtime.pause()` is synchronous and idempotent. It stops new runtime-origin task and event admissions immediately.
|
|
208
|
+
- `runtime.resume()` reopens admissions immediately.
|
|
209
|
+
- `runtime.recoverWhen({ everyMs, check })` registers paused-state recovery conditions. Runner auto-resumes only after all active conditions for the current pause episode pass.
|
|
210
|
+
|
|
211
|
+
Mode access:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const runtime = await run(app);
|
|
215
|
+
|
|
216
|
+
runtime.mode; // "dev" | "prod" | "test"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Inside resources, prefer the narrow DI value:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const app = r
|
|
223
|
+
.resource("app")
|
|
224
|
+
.dependencies({ mode: resources.mode })
|
|
225
|
+
.init(async (_config, { mode }) => {
|
|
226
|
+
if (mode === "test") {
|
|
227
|
+
// install test-only behavior
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return "ready";
|
|
231
|
+
})
|
|
232
|
+
.build();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Serverless / AWS Lambda
|
|
236
|
+
|
|
237
|
+
- Treat the Lambda handler as a thin ingress adapter: parse the API Gateway event, provide request async context, then call `runtime.runTask(...)`.
|
|
238
|
+
- Cache the `run(app, { shutdownHooks: false })` promise across warm invocations so cold-start bootstrap happens once per container.
|
|
239
|
+
- Prefer task input schemas for business validation. Keep the handler focused on HTTP adaptation and error mapping.
|
|
240
|
+
- Require request-local business state with `r.asyncContext(...).require()` so missing context fails fast.
|
|
241
|
+
- Use an explicit `disposeRunner()` helper only in tests, local scripts, or environments where you truly control teardown.
|
|
242
|
+
- See `examples/aws-lambda-quickstart` for examples.
|
|
243
|
+
|
|
209
244
|
## Resources
|
|
210
245
|
|
|
211
|
-
Resources model shared services and state.
|
|
212
|
-
They are Runner's main composition and ownership unit: a resource can register child definitions, expose a value, enforce boundaries, and define lifecycle behavior.
|
|
246
|
+
Resources model shared services and state. They are Runner's primary composition and ownership unit.
|
|
213
247
|
|
|
214
248
|
- Start most apps with `const runtime = await run(appResource)`.
|
|
215
|
-
- The runtime then gives you `runTask(...)`, `emitEvent(...)`, `getResourceValue(...)`, `getLazyResourceValue(...)`, `getResourceConfig(...)`, `getHealth(...)`, `pause()`, `resume()`, `recoverWhen(...)`, and `dispose()`.
|
|
216
|
-
|
|
217
249
|
- `init(config, deps, context)` creates the value.
|
|
218
|
-
- `ready(value, config, deps, context)` starts ingress after startup lock
|
|
219
|
-
- `cooldown(value, config, deps, context)` stops
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
- `ready(value, config, deps, context)` starts ingress after startup lock.
|
|
251
|
+
- `cooldown(value, config, deps, context)` stops accepting new external work quickly at shutdown start.
|
|
252
|
+
Runner fully awaits it before narrowing admissions, and its time still counts against the remaining `dispose.totalBudgetMs` budget.
|
|
253
|
+
During `coolingDown`, task runs and event emissions stay open; if `dispose.cooldownWindowMs > 0`, Runner keeps that broader admission policy open for the extra bounded window after `cooldown()` completes.
|
|
254
|
+
Once `disposing` begins, fresh admissions narrow to the cooling resource itself, any additional resource definitions returned from `cooldown()`, and in-flight continuations.
|
|
255
|
+
- `dispose(value, config, deps, context)` performs final teardown after drain.
|
|
256
|
+
- `health(value, config, deps, context)` is an optional probe used by `resources.health.getHealth(...)` and `runtime.getHealth(...)`.
|
|
222
257
|
Return `{ status: "healthy" | "degraded" | "unhealthy", message?, details? }`.
|
|
223
|
-
- Config-only resources can omit `.init()
|
|
224
|
-
-
|
|
225
|
-
- Gateway resources cannot be passed directly to `run(...)`; wrap them in a non-gateway root resource first.
|
|
258
|
+
- Config-only resources can omit `.init()`. Their resolved value is `undefined`.
|
|
259
|
+
- `.context(() => initialContext)` can hold mutable resource-local state shared across lifecycle phases.
|
|
226
260
|
- If you register something, you are a non-leaf resource.
|
|
227
261
|
- Non-leaf resources cannot be forked.
|
|
228
|
-
- Gateway resources cannot be forked with `.fork()` because multiple gateway instances would compile the same child canonical ids.
|
|
229
|
-
- `.context(() => initialContext)` can hold mutable resource-local state used across lifecycle phases.
|
|
230
262
|
|
|
231
263
|
Use the lifecycle intentionally:
|
|
232
264
|
|
|
233
|
-
- `ready()` for
|
|
265
|
+
- `ready()` for HTTP listeners, consumers, schedulers, and other ingress
|
|
234
266
|
- `cooldown()` for stopping new work immediately
|
|
235
267
|
- `dispose()` for final cleanup
|
|
236
268
|
|
|
269
|
+
Do not use `cooldown()` as a generic teardown phase for support resources such as databases.
|
|
270
|
+
Use it to stop new work; use `dispose()` for final cleanup.
|
|
271
|
+
|
|
272
|
+
Ownership and ids:
|
|
273
|
+
|
|
274
|
+
- User resources contribute their own ownership segment to canonical ids.
|
|
275
|
+
- The app resource passed to `run(...)` is a normal resource, so direct registrations compile under `app.*`.
|
|
276
|
+
- Child resources continue that chain, for example `app.billing.tasks.createInvoice`.
|
|
277
|
+
- Only the internal synthetic framework root is invisible to user-facing ids.
|
|
278
|
+
- `runtime-framework-root` is reserved and cannot be used as a user resource id.
|
|
279
|
+
|
|
280
|
+
Lazy resources:
|
|
281
|
+
|
|
282
|
+
- `getLazyResourceValue(...)` is valid only before shutdown starts.
|
|
283
|
+
- Once the runtime enters `coolingDown` or later, startup-unused resources stay asleep and wakeup attempts fail fast.
|
|
284
|
+
|
|
237
285
|
Health reporting:
|
|
238
286
|
|
|
239
287
|
- Only resources that define `health()` participate.
|
|
240
|
-
- `resources.health` is the built-in health reporter resource
|
|
241
|
-
- Prefer `resources.health.getHealth()` inside resources; keep `runtime.getHealth()` for operator
|
|
242
|
-
- Health
|
|
243
|
-
- Calling `getHealth()` during disposal or after `dispose()` starts is invalid
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
|
|
288
|
+
- `resources.health` is the built-in health reporter resource.
|
|
289
|
+
- Prefer `resources.health.getHealth()` inside resources; keep `runtime.getHealth()` for operator callers.
|
|
290
|
+
- Health APIs are valid only after `run(...)` resolves and before disposal starts.
|
|
291
|
+
- Calling `getHealth()` during disposal or after `dispose()` starts is invalid.
|
|
292
|
+
- Sleeping lazy resources are skipped.
|
|
293
|
+
- Requested resources without `health()` are ignored.
|
|
294
|
+
- Health results expose `{ totals, report, find(...) }`.
|
|
295
|
+
- Report entries look like `{ id, initialized, status, message?, details? }`.
|
|
296
|
+
- `report.find(resourceOrId)` returns that resource entry or throws if it is not present.
|
|
249
297
|
- If `health()` throws, Runner records that resource as `unhealthy` and places the normalized error on `details`.
|
|
250
|
-
- When health
|
|
251
|
-
It simply stops new runtime-origin and resource-origin task runs and event emissions while already-running work continues.
|
|
252
|
-
- `runtime.recoverWhen({ everyMs, check })` belongs on that paused path.
|
|
253
|
-
Register it after `pause()` when you want Runner to poll a recovery condition and auto-resume once the current incident is cleared.
|
|
298
|
+
- When health shows temporary pressure or outage, prefer `runtime.pause()` and `runtime.recoverWhen(...)` over shutdown.
|
|
254
299
|
|
|
255
|
-
|
|
300
|
+
Dynamic registration callbacks receive the resolved mode:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const app = r
|
|
304
|
+
.resource<{ enableDevTools: boolean }>("app")
|
|
305
|
+
.register((config, mode) => [
|
|
306
|
+
...(config.enableDevTools && mode === "dev" ? [devToolsResource] : []),
|
|
307
|
+
])
|
|
308
|
+
.build();
|
|
309
|
+
```
|
|
256
310
|
|
|
257
311
|
## Tasks
|
|
258
312
|
|
|
259
|
-
Tasks are
|
|
313
|
+
Tasks are the main business actions in Runner.
|
|
260
314
|
|
|
261
|
-
- For lifecycle-owned timers, depend on `resources.timers` inside a task or resource.
|
|
262
|
-
`timers.setTimeout()` and `timers.setInterval()` are available during `init()`, stop accepting new timers once `cooldown()` starts, and clear pending timers during `dispose()`.
|
|
263
315
|
- Tasks are async functions with DI, middleware, validation, and typed output.
|
|
264
316
|
- Dependency maps are fail-fast validated. If `dependencies` is a function, it must resolve to an object map.
|
|
265
317
|
- Optional dependencies are explicit: `someResource.optional()`.
|
|
266
|
-
- `.throws([...])` declares error contracts for docs and tooling.
|
|
267
|
-
- Task `.run(input, deps, context)` receives
|
|
268
|
-
- `input`: the validated task input
|
|
269
|
-
- `deps`: the resolved dependency map
|
|
270
|
-
- `context`: auto-injected execution context (always the third arg — never part of `deps`)
|
|
271
|
-
- `context.journal`: per-task typed state shared with middleware
|
|
272
|
-
- `context.source`: `{ kind, id }` — canonical id of the running task
|
|
318
|
+
- `.throws([...])` declares error contracts for docs and tooling. It accepts Runner error helpers only and is declarative metadata, not runtime enforcement.
|
|
319
|
+
- Task `.run(input, deps, context)` always receives execution context as the third argument, never inside `deps`.
|
|
273
320
|
|
|
274
|
-
|
|
321
|
+
Task context includes:
|
|
275
322
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
})
|
|
286
|
-
.build();
|
|
287
|
-
```
|
|
323
|
+
- `journal`: typed per-execution state shared with middleware
|
|
324
|
+
- `source`: `{ kind, id }`, the canonical runtime source of the running task
|
|
325
|
+
- `signal`: the cooperative cancellation signal when execution context or boundary cancellation is active
|
|
326
|
+
|
|
327
|
+
For lifecycle-owned timers, prefer `resources.timers` inside a task or resource:
|
|
328
|
+
|
|
329
|
+
- `timers.setTimeout()` and `timers.setInterval()` are available during `init()`
|
|
330
|
+
- they stop accepting new timers once `cooldown()` starts
|
|
331
|
+
- pending timers are cleared during `dispose()`
|
|
288
332
|
|
|
289
333
|
### ExecutionJournal
|
|
290
334
|
|
|
291
|
-
`ExecutionJournal` is typed state scoped to
|
|
335
|
+
`ExecutionJournal` is typed state scoped to one task execution designed for middleware comms.
|
|
292
336
|
|
|
293
337
|
- Use it when middleware and tasks need to share execution-local state.
|
|
294
338
|
- `journal.set(key, value)` fails if the key already exists.
|
|
295
339
|
- Pass `{ override: true }` when replacement is intentional.
|
|
296
340
|
- Create custom keys with `journal.createKey<T>(id)`.
|
|
297
|
-
- Task context includes `journal` and `source`.
|
|
298
341
|
|
|
299
342
|
## Events and Hooks
|
|
300
343
|
|
|
301
|
-
Events decouple producers from listeners. Hooks subscribe with `.on(event)` or `.on(onAnyOf(...))
|
|
344
|
+
Events decouple producers from listeners. Hooks subscribe with `.on(event)` or `.on(onAnyOf(...))`.
|
|
345
|
+
Passing arrays directly is invalid.
|
|
302
346
|
|
|
303
347
|
Key rules:
|
|
304
348
|
|
|
@@ -306,37 +350,36 @@ Key rules:
|
|
|
306
350
|
- `event.stopPropagation()` prevents downstream hooks from running.
|
|
307
351
|
- `.on("*")` listens to all visible events except those tagged with `tags.excludeFromGlobalHooks`.
|
|
308
352
|
- `.parallel(true)` allows concurrent same-priority listeners.
|
|
309
|
-
- `.transactional(true)` makes listeners reversible
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
- `.transactional(true)` makes listeners reversible. Each executed hook must return an async undo closure.
|
|
354
|
+
|
|
355
|
+
Transactional constraints fail fast:
|
|
356
|
+
|
|
357
|
+
- `transactional + parallel` is invalid
|
|
358
|
+
- `transactional + tags.eventLane` is invalid
|
|
313
359
|
|
|
314
360
|
Emitters accept controls via `await event(payload, options?)`:
|
|
315
361
|
|
|
316
|
-
- `failureMode`: `"fail-fast"`
|
|
317
|
-
- `throwOnError`: `true`
|
|
318
|
-
- `report: true`:
|
|
319
|
-
- If rollback handlers fail, Runner continues the remaining rollbacks and throws a transactional rollback failure that preserves the original trigger failure as the cause.
|
|
362
|
+
- `failureMode`: `"fail-fast"` or `"aggregate"`
|
|
363
|
+
- `throwOnError`: `true` by default
|
|
364
|
+
- `report: true`: return an execution report instead of relying only on exceptions
|
|
320
365
|
|
|
321
|
-
|
|
366
|
+
`report: true` returns:
|
|
322
367
|
|
|
323
368
|
```ts
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// Transactional: `run(async (event) => { /* do work */ return async () => { /* rollback */ } })`
|
|
334
|
-
await inventory.reserve(event.data.orderId);
|
|
335
|
-
return async () => await inventory.release(event.data.orderId);
|
|
336
|
-
})
|
|
337
|
-
.build();
|
|
369
|
+
{
|
|
370
|
+
totalListeners,
|
|
371
|
+
attemptedListeners,
|
|
372
|
+
skippedListeners,
|
|
373
|
+
succeededListeners,
|
|
374
|
+
failedListeners,
|
|
375
|
+
propagationStopped,
|
|
376
|
+
errors,
|
|
377
|
+
}
|
|
338
378
|
```
|
|
339
379
|
|
|
380
|
+
For transactional events, fail-fast rollback is always enforced regardless of reporting mode.
|
|
381
|
+
If rollback handlers fail, Runner continues remaining rollbacks and throws a rollback failure that preserves the original trigger failure as the cause.
|
|
382
|
+
|
|
340
383
|
## Middleware
|
|
341
384
|
|
|
342
385
|
Middleware wraps tasks or resources.
|
|
@@ -354,110 +397,171 @@ const audit = r.middleware
|
|
|
354
397
|
.build();
|
|
355
398
|
```
|
|
356
399
|
|
|
357
|
-
|
|
400
|
+
Core rules:
|
|
358
401
|
|
|
359
402
|
- Create task middleware with `r.middleware.task(id)`.
|
|
360
403
|
- Create resource middleware with `r.middleware.resource(id)`.
|
|
361
404
|
- Attach middleware with `.middleware([...])`.
|
|
362
405
|
- First listed middleware is the outermost wrapper.
|
|
363
|
-
- Runner validates
|
|
364
|
-
- task middleware
|
|
365
|
-
- resource middleware
|
|
406
|
+
- Runner validates targets:
|
|
407
|
+
- task middleware attaches only to tasks or `subtree.tasks.middleware`
|
|
408
|
+
- resource middleware attaches only to resources or `subtree.resources.middleware`
|
|
366
409
|
- Owner-scoped auto-application is available through `resource.subtree({ tasks/resources: { middleware: [...] } })`.
|
|
367
410
|
- Contract middleware can constrain task input and output types.
|
|
368
|
-
-
|
|
369
|
-
- `taskRunner.intercept(...)` can wrap task executions globally at runtime.
|
|
370
|
-
- When a runtime predicate must match one specific task/event/resource definition, prefer `isSameDefinition(candidate, definitionRef)` over comparing public ids directly.
|
|
411
|
+
- When a runtime predicate must match one exact definition, prefer `isSameDefinition(candidate, definitionRef)` over comparing public ids directly.
|
|
371
412
|
|
|
372
413
|
Task vs resource middleware:
|
|
373
414
|
|
|
374
|
-
- Task middleware wraps task execution.
|
|
375
|
-
- Resource middleware wraps resource initialization and
|
|
376
|
-
- Task middleware receives execution input shaped around `{ task, next, journal }`.
|
|
377
|
-
- Resource middleware receives execution input shaped around `{ resource, next }`.
|
|
378
|
-
- Task middleware is where you usually apply auth, retry, cache, rate limit, fallback, tracing, and request-scoped policies.
|
|
379
|
-
- Resource middleware is where you usually apply retry or timeout around expensive startup or resource creation.
|
|
415
|
+
- Task middleware wraps task execution and usually handles auth, retry, cache, tracing, rate limits, fallbacks, or request policies.
|
|
416
|
+
- Resource middleware wraps resource initialization and value resolution and usually handles startup retry or timeout.
|
|
380
417
|
- Canonical ids differ:
|
|
381
418
|
- task middleware -> `app.middleware.task.name`
|
|
382
419
|
- resource middleware -> `app.middleware.resource.name`
|
|
383
420
|
|
|
384
|
-
|
|
421
|
+
Global interception is also available through:
|
|
385
422
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const installer = r
|
|
390
|
-
.resource("installer")
|
|
391
|
-
.dependencies({ taskRunner: resources.taskRunner })
|
|
392
|
-
.init(async (_, { taskRunner }) => {
|
|
393
|
-
taskRunner.intercept(async (next, input) => next(input), {
|
|
394
|
-
when: (def) => isSameDefinition(def, myTask),
|
|
395
|
-
});
|
|
396
|
-
})
|
|
397
|
-
.build();
|
|
398
|
-
```
|
|
423
|
+
- `taskRunner.intercept(...)`
|
|
424
|
+
- `eventManager.intercept(...)`
|
|
425
|
+
- `middlewareManager.intercept("task" | "resource", ...)`
|
|
399
426
|
|
|
400
|
-
|
|
427
|
+
Install those inside a resource `init()`.
|
|
401
428
|
|
|
402
|
-
|
|
429
|
+
Built-in resilience middleware:
|
|
403
430
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
| concurrency | `{ limit, key?, semaphore? }` | limits executions; share concurrency logic via `semaphore` |
|
|
408
|
-
| circuitBreaker | `{ failureThreshold, resetTimeout }` | opens after failures, fails fast until recovery |
|
|
409
|
-
| debounce | `{ ms }` | runs only after inactivity |
|
|
410
|
-
| throttle | `{ ms }` | max once per `ms` |
|
|
411
|
-
| fallback | `{ fallback }` | static value, function, or task fallback |
|
|
412
|
-
| rateLimit | `{ windowMs, max }` | fixed-window limit per instance |
|
|
413
|
-
| retry | `{ retries, stopRetryIf, delayStrategy }` | transient failures with configurable logic |
|
|
414
|
-
| timeout | `{ ttl }` | aborts long-running executions via AbortController |
|
|
431
|
+
- task: `cache`, `concurrency`, `circuitBreaker`, `debounce`, `throttle`, `fallback`, `rateLimit`, `retry`, `timeout`
|
|
432
|
+
- resource: `retry`, `timeout`
|
|
433
|
+
- non-resilience helper: `middleware.task.requireContext.with({ context })`
|
|
415
434
|
|
|
416
|
-
|
|
417
|
-
Non-resilience: `middleware.task.requireContext.with({ context })` — enforces async context.
|
|
435
|
+
Important config surfaces:
|
|
418
436
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
437
|
+
- `cache.with({ ttl, max, ttlAutopurge, keyBuilder })`
|
|
438
|
+
- `concurrency.with({ limit, key?, semaphore? })`
|
|
439
|
+
- `circuitBreaker.with({ failureThreshold, resetTimeout })`
|
|
440
|
+
- `debounce.with({ ms, keyBuilder? })`
|
|
441
|
+
- `throttle.with({ ms, keyBuilder? })`
|
|
442
|
+
- `fallback.with({ fallback })`
|
|
443
|
+
- `rateLimit.with({ windowMs, max, keyBuilder? })`
|
|
444
|
+
- `retry.with({ retries, stopRetryIf, delayStrategy })`
|
|
445
|
+
- `timeout.with({ ttl })`
|
|
425
446
|
|
|
426
|
-
|
|
427
|
-
**Use:** rate-limit for admission, concurrency for in-flight, circuit-breaker for fail-fast, cache for idempotent reads, debounce/throttle for bursty calls.
|
|
447
|
+
Operational notes:
|
|
428
448
|
|
|
429
|
-
|
|
449
|
+
- Register `resources.cache` in a parent resource before using task cache middleware.
|
|
450
|
+
- Order matters. Common pattern: `fallback` outermost, `timeout` inside `retry` when you want per-attempt budgets.
|
|
451
|
+
- Use `rateLimit` for quotas, `concurrency` for in-flight limits, `circuitBreaker` for fail-fast protection, `cache` for idempotent reads, and `debounce` / `throttle` for burst shaping.
|
|
452
|
+
- `rateLimit`, `debounce`, and `throttle` default to `taskId` partitioning. Pass `keyBuilder(taskId, input)` to partition by user, tenant, request context, or similar keys.
|
|
453
|
+
- When `tenantScope` is active, Runner prefixes internal middleware keys with `<tenantId>:`.
|
|
454
|
+
- Resource `retry` and `timeout` use the same semantics on `middleware.resource.*`.
|
|
430
455
|
|
|
431
|
-
-
|
|
432
|
-
- `middleware.task.retry.journalKeys.attempt` / `.lastError`
|
|
433
|
-
- `middleware.task.circuitBreaker.journalKeys.state` / `.failures`
|
|
434
|
-
- `middleware.task.rateLimit.journalKeys.remaining` / `.resetTime` / `.limit`
|
|
435
|
-
- `middleware.task.fallback.journalKeys.active` / `.error`
|
|
436
|
-
- `middleware.task.timeout.journalKeys.abortController`
|
|
456
|
+
Built-in journal keys exist for middleware introspection, for example cache hits, retry attempts, circuit-breaker state, and timeout abort controllers.
|
|
437
457
|
|
|
438
458
|
## Data Contracts
|
|
439
459
|
|
|
440
460
|
### Validation
|
|
441
461
|
|
|
442
462
|
```ts
|
|
443
|
-
import {
|
|
463
|
+
import { Match, check } from "@bluelibs/runner";
|
|
444
464
|
```
|
|
445
465
|
|
|
446
|
-
|
|
447
|
-
|
|
466
|
+
Core primitives:
|
|
467
|
+
|
|
468
|
+
- `check(value, pattern)` validates at runtime and returns the same value reference on success.
|
|
469
|
+
- `Match.compile(pattern)` creates reusable schemas with `.parse()`, `.test()`, and `.toJSONSchema()`.
|
|
470
|
+
- Match-native helpers and built-in tokens expose the same `.parse()`, `.test()`, and `.toJSONSchema()` surface directly.
|
|
471
|
+
- `type Output = Match.infer<typeof schema>` is the ergonomic type inference alias.
|
|
472
|
+
- Schema slots consume parse results, so class-backed schemas hydrate by default when used in `.inputSchema(...)`, `.configSchema(...)`, or `.payloadSchema(...)`.
|
|
473
|
+
|
|
474
|
+
Important rules:
|
|
475
|
+
|
|
476
|
+
- Hydration happens on `parse(...)`, not on `check(...)`.
|
|
477
|
+
- Class-schema hydration uses prototype assignment and does not call constructors during parse.
|
|
478
|
+
- Plain objects are strict by default.
|
|
479
|
+
- Prefer a plain object for the normal strict case, `Match.ObjectStrict(...)` when you want that strictness to be explicit, and `Match.ObjectIncluding(...)` when extra keys are allowed.
|
|
448
480
|
- Constructors act as matchers: `String`, `Number`, `Boolean`.
|
|
449
|
-
-
|
|
450
|
-
|
|
481
|
+
- Compiled schemas do not expose `.extend()`. Compose `compiled.pattern` into a new pattern and compile again.
|
|
482
|
+
|
|
483
|
+
Custom schema and pattern notes:
|
|
484
|
+
|
|
485
|
+
- The supported way to create reusable custom patterns is to compose Match-native helpers into named constants.
|
|
486
|
+
- `CheckSchemaLike<T>` is the minimal top-level custom schema contract: implement `parse(input): T`, and optionally `toJSONSchema()`.
|
|
487
|
+
- `CheckSchemaLike` works for schema slots and `check(...)`. It is not a public nested Match-pattern extension point.
|
|
488
|
+
- In a custom `CheckSchemaLike`, a normal thrown error or `errors.genericError` is the normal fit for validation failures.
|
|
489
|
+
Use `errors.matchError.new({ path: "$", failures: [...] })` only when you intentionally want Match-style failure metadata at the top level.
|
|
490
|
+
|
|
491
|
+
Common helpers include:
|
|
492
|
+
|
|
493
|
+
- `NonEmptyString`
|
|
494
|
+
- `Email`
|
|
495
|
+
- `Integer`
|
|
496
|
+
- `UUID`
|
|
497
|
+
- `URL`
|
|
498
|
+
- `Range({ min?, max?, inclusive?, integer? })`
|
|
499
|
+
- `Optional(...)`
|
|
500
|
+
- `OneOf(...)`
|
|
501
|
+
- `ObjectIncluding(...)`
|
|
502
|
+
- `MapOf(...)`
|
|
503
|
+
- `ArrayOf(...)`
|
|
504
|
+
- `Lazy(...)`
|
|
505
|
+
- `Where(...)`
|
|
506
|
+
- `WithMessage(...)`
|
|
507
|
+
|
|
508
|
+
Decorator-backed schemas:
|
|
509
|
+
|
|
451
510
|
- `@Match.Schema({ base: BaseClass })` allows subclassing without TypeScript `extends`.
|
|
452
|
-
-
|
|
511
|
+
- `@Match.Schema({ exact, schemaId, errorPolicy })` controls strictness, schema identity, and default aggregation policy.
|
|
512
|
+
- Default decorator exports target standard ES decorators.
|
|
513
|
+
- For legacy `experimentalDecorators`, import `Match` and `Serializer` from `@bluelibs/runner/decorators/legacy`.
|
|
514
|
+
- Runner decorators do not require `emitDecoratorMetadata` or `reflect-metadata`.
|
|
515
|
+
- The default package initializes `Symbol.metadata` when missing, without replacing a native implementation.
|
|
516
|
+
|
|
517
|
+
Recursion and custom predicates:
|
|
518
|
+
|
|
519
|
+
- Use `Match.fromSchema(() => User)` for self-referencing or forward class-schema links.
|
|
520
|
+
- Use `Match.Lazy(() => pattern)` for recursive plain Match patterns.
|
|
521
|
+
- Use `Match.Where(...)` for runtime-only predicates or type guards.
|
|
522
|
+
- Prefer built-ins, `RegExp`, or object patterns when JSON Schema export needs to stay precise.
|
|
523
|
+
- `Match.Range({ min?, max?, inclusive?, integer? })` defaults to inclusive bounds; `inclusive: false` makes both bounds exclusive, and `integer: true` restricts the range to integers.
|
|
524
|
+
- Example: `Match.Range({ min: 5, max: 10, integer: true })`.
|
|
525
|
+
- `Match.Where(...)` receives the immediate parent when matching compound values.
|
|
526
|
+
- `Match.Where(..., messageOrFormatter)` is shorthand for `Match.WithMessage(Match.Where(...), messageOrFormatter)`.
|
|
527
|
+
|
|
528
|
+
Validation errors:
|
|
529
|
+
|
|
530
|
+
- Validation failures throw `errors.matchError`.
|
|
531
|
+
- The thrown error exposes `.path` and flat `.failures`.
|
|
532
|
+
- `Match.WithMessage(...)` customizes the error headline.
|
|
533
|
+
- `messageOrFormatter` can be a string, `{ message, code?, params? }`, or a callback.
|
|
534
|
+
- In callback form, `ctx` is `{ value, error, path, pattern, parent? }`.
|
|
535
|
+
- When `{ code, params }` is provided, Runner copies that metadata onto owned `failures[]` entries while keeping each leaf failure's raw `message` intact.
|
|
536
|
+
- Use `check(value, pattern, { errorPolicy: "all" })`, `Match.WithErrorPolicy(pattern, "all")`, or `@Match.Schema({ errorPolicy: "all" })` when you want aggregate failures.
|
|
453
537
|
|
|
454
538
|
### Errors
|
|
455
539
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
540
|
+
Typed errors are declared once and usually registered + injected via DI, but the built helper also works locally outside `run(...)`.
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
const userNotFound = r
|
|
544
|
+
.error<{ userId: string }>("userNotFound")
|
|
545
|
+
.httpCode(404)
|
|
546
|
+
.format((d) => `User '${d.userId}' not found`)
|
|
547
|
+
.remediation((d) => `Verify user '${d.userId}' exists first.`)
|
|
548
|
+
.build();
|
|
549
|
+
|
|
550
|
+
userNotFound.throw({ userId: "u1" });
|
|
551
|
+
userNotFound.new({ userId: "u1" });
|
|
552
|
+
userNotFound.is(err);
|
|
553
|
+
userNotFound.is(err, { severity: "high" });
|
|
554
|
+
r.error.is(err);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Important rules:
|
|
558
|
+
|
|
559
|
+
- `IRunnerError` exposes `.id`, `.data`, `.message`, `.httpCode`, and `.remediation`.
|
|
560
|
+
- `.dataSchema(...)` validates error data at throw time.
|
|
561
|
+
- `.throws([...])` on tasks, resources, hooks, and middleware accepts Runner error helpers only and remains declarative metadata.
|
|
562
|
+
- `.new()` / `.throw()` / `.is()` work even when the helper is used outside the Runner graph.
|
|
563
|
+
- Register the error when you want DI, discovery, or app definitions to depend on it.
|
|
564
|
+
- `errors.genericError` is the built-in fallback for ad-hoc message-only errors. Prefer domain-specific helpers when the contract is stable.
|
|
461
565
|
|
|
462
566
|
### Serialization
|
|
463
567
|
|
|
@@ -465,6 +569,7 @@ import { check, Match } from "@bluelibs/runner";
|
|
|
465
569
|
- Register custom types through `resources.serializer`.
|
|
466
570
|
- Use `serializer.parse(payload, { schema })` when you want deserialization and validation in one step.
|
|
467
571
|
- `@Serializer.Field({ from, deserialize, serialize })` composes with `@Match.Field(...)` on `@Match.Schema()` classes for explicit DTOs.
|
|
572
|
+
- For legacy decorators, import `Serializer` from `@bluelibs/runner/decorators/legacy`.
|
|
468
573
|
|
|
469
574
|
## Testing
|
|
470
575
|
|
|
@@ -472,62 +577,57 @@ import { check, Match } from "@bluelibs/runner";
|
|
|
472
577
|
- Run it with `await run(app)`.
|
|
473
578
|
- Assert through `runTask`, `emitEvent`, `getResourceValue`, or `getResourceConfig`.
|
|
474
579
|
- `r.override(base, fn)` is the standard way to swap behavior in tests while preserving ids.
|
|
580
|
+
- Duplicate override targets are allowed only in resolved `test` mode.
|
|
581
|
+
The outermost declaring resource wins, and same-resource duplicates use the last declaration.
|
|
475
582
|
|
|
476
583
|
## Composition Boundaries
|
|
477
584
|
|
|
585
|
+
### Isolation
|
|
586
|
+
|
|
478
587
|
Runner treats composition boundaries as first-class.
|
|
479
588
|
|
|
480
|
-
|
|
589
|
+
Think of `.isolate(...)` as two controls on one boundary:
|
|
590
|
+
|
|
591
|
+
- `exports`: what the subtree exposes outward
|
|
592
|
+
- `deny` / `only` / `whitelist`: what consumers in the subtree may wire to across boundaries
|
|
481
593
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
-
|
|
594
|
+
Important rules:
|
|
595
|
+
|
|
596
|
+
- `exports: []` or `exports: "none"` makes the subtree private.
|
|
597
|
+
- Export entries must be explicit Runner definition or resource references.
|
|
486
598
|
- Runtime operator APIs such as `runTask`, `emitEvent`, and `getResourceValue` are gated only by the root resource's `isolate.exports` surface.
|
|
487
599
|
- `.isolate((config) => ({ ... }))` resolves once per configured resource instance.
|
|
488
600
|
|
|
489
601
|
Selector model:
|
|
490
602
|
|
|
491
|
-
- direct ref: one concrete definition
|
|
603
|
+
- direct ref: one concrete definition, resource, or tag
|
|
492
604
|
- `subtreeOf(resource, { types? })`: everything owned by that resource subtree
|
|
493
|
-
- `scope(target, channels?)`:
|
|
605
|
+
- `scope(target, channels?)`: limit matching to selected channels such as `dependencies`, `listening`, `tagging`, or `middleware`
|
|
494
606
|
- string selectors are valid only inside `scope(...)`
|
|
495
|
-
- `scope("*")
|
|
496
|
-
- `scope("system.*")
|
|
497
|
-
- `scope("app.resources.*")
|
|
607
|
+
- `scope("*")`
|
|
608
|
+
- `scope("system.*")`
|
|
609
|
+
- `scope("app.resources.*")`
|
|
498
610
|
- `subtreeOf(resource)` is ownership-based, not string-prefix-based
|
|
499
611
|
|
|
500
612
|
Rule model:
|
|
501
613
|
|
|
502
614
|
- `deny`: block matching cross-boundary targets
|
|
503
615
|
- `only`: allow only matching cross-boundary targets
|
|
504
|
-
- `whitelist`:
|
|
505
|
-
- `whitelist.for` and `whitelist.targets` accept the same selector forms as `deny` and `only`
|
|
506
|
-
- unknown targets or selectors that resolve to nothing fail fast at bootstrap
|
|
507
|
-
- violations fail during bootstrap wiring, not first runtime use
|
|
508
|
-
- legacy resource-level `exports` and fluent `.exports(...)` were removed in 6.x; use `isolate: { exports: [...] }` or `.isolate({ exports: [...] })`
|
|
616
|
+
- `whitelist`: carve out exceptions for this boundary only
|
|
509
617
|
|
|
510
|
-
|
|
511
|
-
.isolate({
|
|
512
|
-
deny: [subtreeOf(adminResource), scope([internalEvent], { listening: false })],
|
|
513
|
-
whitelist: [{ for: [healthTask], targets: [resources.health] }],
|
|
514
|
-
})
|
|
515
|
-
```
|
|
618
|
+
More isolation rules:
|
|
516
619
|
|
|
517
|
-
|
|
620
|
+
- `whitelist` does not override ancestor restrictions or make private exports public.
|
|
621
|
+
- `whitelist.for` and `whitelist.targets` accept the same selector forms as `deny` and `only`.
|
|
622
|
+
- Unknown selectors or targets that resolve to nothing fail fast at bootstrap.
|
|
623
|
+
- Violations fail during bootstrap wiring, not first runtime use.
|
|
624
|
+
- Legacy resource-level `exports` and fluent `.exports(...)` were removed in 6.x. Use `isolate: { exports: [...] }` or `.isolate({ exports: [...] })`.
|
|
518
625
|
|
|
519
|
-
|
|
626
|
+
Example:
|
|
520
627
|
|
|
521
628
|
```ts
|
|
522
629
|
.isolate({
|
|
523
630
|
exports: [createInvoice],
|
|
524
|
-
})
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
- Block all `system.*` dependencies for this subtree except `runnerDev`:
|
|
528
|
-
|
|
529
|
-
```ts
|
|
530
|
-
.isolate({
|
|
531
631
|
deny: [scope("system.*", { dependencies: true })],
|
|
532
632
|
whitelist: [
|
|
533
633
|
{
|
|
@@ -538,51 +638,48 @@ Examples:
|
|
|
538
638
|
})
|
|
539
639
|
```
|
|
540
640
|
|
|
541
|
-
|
|
641
|
+
Other common patterns:
|
|
542
642
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
only: [subtreeOf(agentResource, { types: ["task"] })],
|
|
546
|
-
})
|
|
547
|
-
```
|
|
643
|
+
- Channel-specific boundaries, for example `scope([internalEvent], { listening: false })`
|
|
644
|
+
- Task-only allowlists, for example `only: [subtreeOf(agentResource, { types: ["task"] })]`
|
|
548
645
|
|
|
549
646
|
### Subtrees
|
|
550
647
|
|
|
551
|
-
- `.subtree(policy)
|
|
648
|
+
- `.subtree(policy)`, `.subtree([policyA, policyB])`, and `.subtree((config) => policy | policy[])` can auto-attach middleware to nested tasks or resources.
|
|
649
|
+
- If subtree middleware and local middleware resolve to the same middleware id on one target, Runner fails fast.
|
|
552
650
|
- Subtrees can validate contained definitions.
|
|
553
651
|
- `subtree.validate` is generic for compiled subtree definitions and can be one function or an array.
|
|
554
652
|
- Typed validation is also available on `tasks`, `resources`, `hooks`, `events`, `tags`, `taskMiddleware`, and `resourceMiddleware`.
|
|
555
653
|
- Generic and typed validators both run when they match the same compiled definition.
|
|
556
|
-
-
|
|
557
|
-
-
|
|
558
|
-
- Validators are return-based:
|
|
559
|
-
- return `SubtreeViolation[]` for normal policy failures
|
|
560
|
-
- do not throw for expected validation failures
|
|
654
|
+
- Use the function form when subtree policy depends on resource config.
|
|
655
|
+
- Validators receive the compiled definition and should return `SubtreeViolation[]` for expected policy failures rather than throwing.
|
|
561
656
|
|
|
562
657
|
### Forks and Overrides
|
|
563
658
|
|
|
564
659
|
- `resource.fork(newId)` clones a leaf resource definition under a new id.
|
|
565
660
|
- Forks clone identity, not structure.
|
|
566
|
-
-
|
|
567
|
-
-
|
|
568
|
-
- `.fork()` is not supported for gateway resources.
|
|
569
|
-
- `.fork()` returns a built resource. You do not call `.build()` again.
|
|
661
|
+
- Non-leaf resources cannot be forked.
|
|
662
|
+
- `.fork()` returns a built resource. Do not call `.build()` again.
|
|
570
663
|
- Compose a distinct parent resource when you need a structural variant of a non-leaf resource.
|
|
571
664
|
- Durable support is registered via `resources.durable`, while concrete durable backends use normal forks such as `resources.memoryWorkflow.fork("app-durable")`.
|
|
665
|
+
|
|
666
|
+
Overrides:
|
|
667
|
+
|
|
572
668
|
- Use `r.override(base, fn)` when you need to replace behavior while preserving the original id.
|
|
669
|
+
- For resources only, `r.override(resource, { context, init, ready, cooldown, dispose })` is also supported.
|
|
670
|
+
- Resource object-form overrides inherit unspecified lifecycle hooks from the base resource and may add stages the base resource did not define.
|
|
671
|
+
- Overriding resource `context` changes the private lifecycle-state contract shared across resource hooks.
|
|
573
672
|
- `.overrides([...])` applies override definitions during bootstrap.
|
|
574
|
-
- Override direction is downstream-only: declare overrides from the resource that owns the target subtree or from one of its ancestors.
|
|
673
|
+
- Override direction is downstream-only: declare overrides from the resource that owns the target subtree or from one of its ancestors.
|
|
674
|
+
- Child resources cannot replace parent-owned or sibling-owned definitions.
|
|
675
|
+
- Outside `test` mode, duplicate override targets fail fast.
|
|
676
|
+
In `test`, the outermost declaring resource wins and same-resource duplicates use the last declaration.
|
|
575
677
|
- Override targets must already exist in the graph.
|
|
576
678
|
|
|
577
|
-
Fork quick guide:
|
|
578
|
-
|
|
579
|
-
- `fork("new-id")`: same leaf resource behavior, new id
|
|
580
|
-
- non-leaf resource variant: compose a new parent resource and register the desired children explicitly
|
|
581
|
-
- durable workflow variant: register `resources.durable` and fork a backend such as `resources.memoryWorkflow.fork("app-durable")`
|
|
582
|
-
|
|
583
679
|
## Tags and Scheduling
|
|
584
680
|
|
|
585
|
-
Tags are Runner's typed discovery system.
|
|
681
|
+
Tags are Runner's typed discovery system.
|
|
682
|
+
They attach metadata to definitions, can affect framework behavior, and can be injected as typed accessors over matching definitions.
|
|
586
683
|
|
|
587
684
|
```ts
|
|
588
685
|
import { Match, r } from "@bluelibs/runner";
|
|
@@ -590,12 +687,10 @@ import { Match, r } from "@bluelibs/runner";
|
|
|
590
687
|
const httpRoute = r
|
|
591
688
|
.tag("httpRoute")
|
|
592
689
|
.for(["tasks"])
|
|
593
|
-
.configSchema(
|
|
594
|
-
Match.
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}),
|
|
598
|
-
)
|
|
690
|
+
.configSchema({
|
|
691
|
+
method: Match.OneOf("GET", "POST"),
|
|
692
|
+
path: Match.NonEmptyString,
|
|
693
|
+
})
|
|
599
694
|
.build();
|
|
600
695
|
|
|
601
696
|
const getHealth = r
|
|
@@ -609,132 +704,214 @@ Key rules:
|
|
|
609
704
|
|
|
610
705
|
- Depending on a tag injects a typed accessor over matching definitions.
|
|
611
706
|
- `.for([...])` restricts which definition kinds can receive the tag.
|
|
612
|
-
- Tag
|
|
613
|
-
- Contract tags shape task or resource typing without changing runtime behavior.
|
|
707
|
+
- Tag config schemas accept the same schema types as other config surfaces.
|
|
708
|
+
- Contract tags can shape task or resource typing without changing runtime behavior.
|
|
614
709
|
- Built-in tags such as `tags.system`, `tags.debug`, and `tags.excludeFromGlobalHooks` affect framework behavior.
|
|
615
|
-
- `tags.debug` supports preset levels or fine-grained per-component
|
|
616
|
-
-
|
|
617
|
-
|
|
618
|
-
- Tags are often the cleanest way to implement
|
|
710
|
+
- `tags.debug` supports preset levels or fine-grained per-component config.
|
|
711
|
+
- `tags.failWhenUnhealthy.with([db, cache])` blocks task execution only when one of those resources reports `unhealthy`.
|
|
712
|
+
`degraded` still runs, bootstrap-time task calls are not gated, and sleeping lazy resources stay skipped.
|
|
713
|
+
- Tags are often the cleanest way to implement route discovery, cron scheduling, cache warmers, or internal policies without manual registries.
|
|
619
714
|
|
|
620
715
|
Cron:
|
|
621
716
|
|
|
622
717
|
- `tags.cron` schedules tasks with cron expressions.
|
|
623
|
-
- Attach it
|
|
718
|
+
- Attach it with `tags.cron.with({ expression: "* * * * *" })`.
|
|
624
719
|
- Cron runs only when `resources.cron` is registered.
|
|
625
720
|
- One cron tag per task is supported.
|
|
626
|
-
-
|
|
721
|
+
- Without `resources.cron`, cron tags remain metadata only.
|
|
722
|
+
|
|
723
|
+
## Context
|
|
724
|
+
|
|
725
|
+
Runner has two different async-context surfaces:
|
|
627
726
|
|
|
628
|
-
|
|
727
|
+
- `executionContext`: Runner-managed metadata such as `correlationId`, cancellation `signal`, and optional frame tracing
|
|
728
|
+
- `r.asyncContext(...)`: user-owned business state such as tenant, auth, locale, or request metadata
|
|
629
729
|
|
|
630
|
-
|
|
730
|
+
Do not treat them as the same feature just because they use the same async-local machinery under the hood.
|
|
731
|
+
|
|
732
|
+
### Execution Context
|
|
733
|
+
|
|
734
|
+
Use execution context when you want correlation ids, inherited execution signals, frame tracing, or runtime cycle detection.
|
|
631
735
|
|
|
632
736
|
```ts
|
|
633
|
-
|
|
634
|
-
const runtime = await run(app, { executionContext: true }); // or { cycleDetection: false }
|
|
737
|
+
const runtime = await run(app, { executionContext: true });
|
|
635
738
|
|
|
636
|
-
await
|
|
637
|
-
|
|
739
|
+
const fastRuntime = await run(app, {
|
|
740
|
+
executionContext: { frames: "off", cycleDetection: false },
|
|
741
|
+
});
|
|
638
742
|
|
|
639
|
-
// Use inside tasks/hooks/interceptors
|
|
640
743
|
const myTask = r
|
|
641
744
|
.task("myTask")
|
|
642
745
|
.run(async () => {
|
|
643
|
-
const
|
|
746
|
+
const execution = asyncContexts.execution.use();
|
|
747
|
+
const { correlationId, signal } = execution;
|
|
748
|
+
|
|
749
|
+
if (execution.framesMode === "full") {
|
|
750
|
+
execution.currentFrame.kind;
|
|
751
|
+
execution.frames;
|
|
752
|
+
}
|
|
644
753
|
})
|
|
645
754
|
.build();
|
|
755
|
+
```
|
|
646
756
|
|
|
647
|
-
|
|
648
|
-
await asyncContexts.execution.provide(
|
|
649
|
-
{ correlationId: req.headers["x-id"] },
|
|
650
|
-
() => runtime.runTask(handleRequest, input),
|
|
651
|
-
);
|
|
757
|
+
Important rules:
|
|
652
758
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
657
|
-
|
|
759
|
+
- `executionContext: true` enables full tracing.
|
|
760
|
+
- `executionContext: { frames: "off", cycleDetection: false }` keeps cheap signal inheritance and correlation ids without full frame bookkeeping.
|
|
761
|
+
- Top-level runtime task runs and event emissions automatically create execution context when enabled.
|
|
762
|
+
- You do not need `provide()` just to enable propagation.
|
|
763
|
+
- `asyncContexts.execution.provide(...)` seeds external metadata such as correlation ids or signals at an ingress boundary.
|
|
764
|
+
- `asyncContexts.execution.record(...)` captures the execution tree for assertions, tracing, or debugging.
|
|
765
|
+
- `record()` temporarily promotes lightweight execution context to full frame tracking for the recorded callback.
|
|
766
|
+
- `provide()` and `record()` do not create cancellation on their own. They only propagate a signal you already provide.
|
|
767
|
+
- `asyncContexts.execution` is for Runner metadata, not arbitrary business state.
|
|
658
768
|
|
|
659
|
-
|
|
769
|
+
Execution signal model:
|
|
660
770
|
|
|
661
|
-
|
|
771
|
+
- Pass a signal explicitly at the boundary with `runTask(..., { signal })` or `emit(..., { signal })`.
|
|
772
|
+
- Once execution context is enabled, nested calls can inherit that ambient execution signal automatically.
|
|
773
|
+
- The first signal attached to the execution tree becomes the ambient execution signal.
|
|
774
|
+
- Explicit nested signals stay local to that child call and do not rewrite the ambient signal for deeper propagation.
|
|
662
775
|
|
|
663
|
-
|
|
776
|
+
Cancellation surfaces:
|
|
777
|
+
|
|
778
|
+
- Tasks read `context.signal`.
|
|
779
|
+
- Hooks read `event.signal`.
|
|
780
|
+
- Injected event emitters accept `emit(payload, { signal })`.
|
|
781
|
+
- Low-level event-manager APIs accept merged call options such as `{ source, signal, report }`.
|
|
782
|
+
- RPC lane calls forward the active task or event signal automatically.
|
|
783
|
+
- Timeout middleware uses the same cooperative cancellation path.
|
|
784
|
+
- `middleware.task.timeout.journalKeys.abortController` remains available for middleware coordination and compatibility.
|
|
785
|
+
- If no cancellation source exists, `context.signal` and `event.signal` stay `undefined` rather than using a shared fake signal.
|
|
664
786
|
|
|
665
787
|
Cycle protection comes in layers:
|
|
666
788
|
|
|
667
|
-
- declared `.dependencies(...)` cycles fail
|
|
668
|
-
- declared hook-driven event bounce graphs fail
|
|
669
|
-
- dynamic runtime loops such as `task -> event -> hook -> task` need
|
|
789
|
+
- declared `.dependencies(...)` cycles fail at bootstrap, including middleware-aware graph validation
|
|
790
|
+
- declared hook-driven event bounce graphs fail at bootstrap event-emission validation
|
|
791
|
+
- dynamic runtime loops such as `task -> event -> hook -> task` need full execution-context frame tracking with cycle detection enabled
|
|
792
|
+
|
|
793
|
+
Platform note:
|
|
670
794
|
|
|
671
|
-
|
|
795
|
+
- Execution context requires `AsyncLocalStorage`.
|
|
796
|
+
- On runtimes without it, `run(..., { executionContext: ... })` fails fast with a typed context error.
|
|
797
|
+
- Direct calls to `asyncContexts.execution.provide()` or `.record()` throw a typed context error if async-local storage is unavailable.
|
|
672
798
|
|
|
673
|
-
|
|
799
|
+
### Async Context
|
|
674
800
|
|
|
675
|
-
|
|
801
|
+
Use `r.asyncContext(...)` for request-local business state.
|
|
676
802
|
|
|
677
803
|
```ts
|
|
678
804
|
import { r } from "@bluelibs/runner";
|
|
679
805
|
|
|
680
806
|
const tenantCtx = r.asyncContext<string>("tenantId");
|
|
681
807
|
|
|
682
|
-
// Provide at the request boundary
|
|
683
808
|
await tenantCtx.provide("acme-corp", () =>
|
|
684
809
|
runtime.runTask(handleRequest, input),
|
|
685
810
|
);
|
|
686
811
|
|
|
687
|
-
// Consume anywhere downstream in the same async tree
|
|
688
812
|
const myTask = r
|
|
689
813
|
.task("myTask")
|
|
690
814
|
.run(async () => {
|
|
691
|
-
const tenantId = tenantCtx.use();
|
|
815
|
+
const tenantId = tenantCtx.use();
|
|
692
816
|
})
|
|
693
817
|
.build();
|
|
694
818
|
```
|
|
695
819
|
|
|
696
|
-
|
|
820
|
+
Key rules:
|
|
821
|
+
|
|
822
|
+
- Async context defines serializable business state scoped to one async execution tree.
|
|
823
|
+
- Contexts can be injected as dependencies.
|
|
824
|
+
- `middleware.task.requireContext.with({ context })` enforces that required context exists.
|
|
825
|
+
- Custom `serialize` / `parse` support propagation over RPC lanes.
|
|
826
|
+
- Async context also requires `AsyncLocalStorage` for propagation.
|
|
827
|
+
|
|
828
|
+
### Multi-Tenant Systems
|
|
829
|
+
|
|
830
|
+
Runner's official same-runtime multi-tenant pattern uses `asyncContexts.tenant`.
|
|
831
|
+
|
|
832
|
+
- `tenant.use()` returns `{ tenantId: string }` and throws when missing.
|
|
833
|
+
- `tenant.tryUse()` returns the tenant value or `undefined`.
|
|
834
|
+
- `tenant.has()` is the safe boolean check.
|
|
835
|
+
- `tenant.require()` enforces tenant presence.
|
|
836
|
+
- Augment `TenantContextValue` when your app needs extra tenant metadata.
|
|
837
|
+
- Provide tenant identity at ingress with `tenant.provide({ tenantId }, fn)`.
|
|
838
|
+
|
|
839
|
+
Tenant-sensitive middleware such as `cache`, `rateLimit`, `debounce`, `throttle`, and `concurrency` default to `tenantScope: "auto"`:
|
|
840
|
+
|
|
841
|
+
- `"auto"`: partition by tenant when tenant context exists, otherwise use shared space
|
|
842
|
+
- `"required"`: fail fast when tenant context is missing
|
|
843
|
+
- `"off"`: always use the shared non-tenant space
|
|
844
|
+
|
|
845
|
+
Use `"off"` only when cross-tenant sharing is intentional, such as a truly global cache or semaphore namespace.
|
|
846
|
+
|
|
847
|
+
Platform note:
|
|
848
|
+
|
|
849
|
+
- Tenant propagation also depends on `AsyncLocalStorage`.
|
|
850
|
+
- On runtimes without it, `tenant.provide()` still runs the callback but does not propagate tenant state, so prefer safe accessors in multi-platform code.
|
|
697
851
|
|
|
698
852
|
## Queue
|
|
699
853
|
|
|
700
854
|
`resources.queue` provides named FIFO queues. Each queue id gets its own isolated instance.
|
|
701
855
|
|
|
702
|
-
`queue.run(id, task)` schedules work sequentially
|
|
856
|
+
- `queue.run(id, task)` schedules work sequentially for that queue id.
|
|
857
|
+
- Each queued task receives `(signal: AbortSignal) => Promise<void>`.
|
|
858
|
+
- `queue.dispose()` drains queued work without aborting the active task.
|
|
859
|
+
- `queue.dispose({ cancel: true })` is teardown mode: abort the active task cooperatively and reject queued-but-not-started work.
|
|
860
|
+
- `resources.queue` uses `queue.dispose({ cancel: true })` during runtime teardown and awaits every queue before the resource is considered disposed.
|
|
703
861
|
|
|
704
|
-
|
|
705
|
-
await queue.run("uploads", async (signal) => {
|
|
706
|
-
if (signal.aborted) return;
|
|
707
|
-
await processFile(file, signal);
|
|
708
|
-
});
|
|
709
|
-
```
|
|
862
|
+
Always respect the signal in tasks that may be cancelled.
|
|
710
863
|
|
|
711
864
|
## Remote Lanes (Node)
|
|
712
865
|
|
|
713
|
-
Event lanes are async fire-and-forget routing for events across Runner instances.
|
|
866
|
+
Event lanes are async fire-and-forget routing for events across Runner instances.
|
|
867
|
+
RPC lanes are synchronous cross-runner task or event calls.
|
|
868
|
+
|
|
869
|
+
Supported modes:
|
|
714
870
|
|
|
715
|
-
|
|
871
|
+
- `network`
|
|
872
|
+
- `transparent`
|
|
873
|
+
- `local-simulated`
|
|
716
874
|
|
|
717
|
-
|
|
875
|
+
Async-context propagation over RPC lanes and event lanes is lane-allowlisted by default.
|
|
718
876
|
|
|
719
|
-
|
|
877
|
+
See:
|
|
720
878
|
|
|
721
|
-
|
|
879
|
+
- [REMOTE_LANES_AI.md](./REMOTE_LANES_AI.md)
|
|
880
|
+
- [REMOTE_LANES.md](./REMOTE_LANES.md)
|
|
881
|
+
|
|
882
|
+
## Observability
|
|
722
883
|
|
|
723
884
|
- `resources.logger` is the built-in structured logger.
|
|
724
885
|
- Loggers support `trace`, `debug`, `info`, `warn`, `error`, and `critical`.
|
|
725
|
-
- `logger.with({ source, additionalContext })` creates
|
|
726
|
-
- `logger.onLog(async (log) => { ... })` lets you forward, redact, or collect logs without routing
|
|
886
|
+
- `logger.with({ source, additionalContext })` creates child loggers that share root listeners and buffering.
|
|
887
|
+
- `logger.onLog(async (log) => { ... })` lets you forward, redact, or collect logs without routing through the event system.
|
|
888
|
+
- To log an error with its stack trace, pass the actual `Error` object in the `error` field:
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
try {
|
|
892
|
+
await processPayment(order);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
await logger.error("Payment processing failed", {
|
|
895
|
+
// to preserve stacktrace:
|
|
896
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
897
|
+
data: { orderId: order.id, amount: order.total },
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
- Runner extracts `error.name`, `error.message`, and `error.stack` into the structured log entry.
|
|
727
903
|
- `run(app, { logs: { printThreshold, printStrategy, bufferLogs } })` controls printing and startup buffering.
|
|
728
904
|
- Prefer stable `source` ids and low-cardinality context fields such as `requestId`, `taskId`, or `tenantId`.
|
|
729
905
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
906
|
+
## Project Structure
|
|
907
|
+
|
|
908
|
+
Prefer feature-driven folders and naming by Runner item type:
|
|
909
|
+
|
|
910
|
+
- `*.task.ts`
|
|
911
|
+
- `*.resource.ts`
|
|
912
|
+
- `*.event.ts`
|
|
913
|
+
- `*.hook.ts`
|
|
914
|
+
- `*.task-middleware.ts`
|
|
915
|
+
- `*.resource-middleware.ts`
|
|
916
|
+
- `*.tag.ts`
|
|
917
|
+
- `*.error.ts`
|