@bluelibs/runner-dev 5.3.0 → 6.0.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/AI.md +25 -3
- package/README.md +190 -55
- package/dist/cli/generators/artifact.js +2 -14
- package/dist/cli/generators/artifact.js.map +1 -1
- package/dist/cli/generators/common.d.ts +1 -0
- package/dist/cli/generators/common.js +22 -0
- package/dist/cli/generators/common.js.map +1 -1
- package/dist/cli/generators/printNewHelp.js +2 -2
- package/dist/cli/generators/printNewHelp.js.map +1 -1
- package/dist/cli/generators/scaffold/templates/package.json.d.ts +2 -2
- package/dist/cli/generators/scaffold/templates/package.json.js +2 -2
- package/dist/cli/generators/scaffold/templates/src/main.ts.js +7 -9
- package/dist/cli/generators/scaffold/templates/src/main.ts.js.map +1 -1
- package/dist/cli/generators/scaffold.js +1 -135
- package/dist/cli/generators/scaffold.js.map +1 -1
- package/dist/cli/generators/templates.js +64 -63
- package/dist/cli/generators/templates.js.map +1 -1
- package/dist/generated/resolvers-types.d.ts +376 -144
- package/dist/index.d.ts +39 -43
- package/dist/resources/cli.config.resource.d.ts +1 -1
- package/dist/resources/cli.config.resource.js +2 -2
- package/dist/resources/cli.config.resource.js.map +1 -1
- package/dist/resources/coverage.resource.d.ts +2 -2
- package/dist/resources/coverage.resource.js +3 -3
- package/dist/resources/coverage.resource.js.map +1 -1
- package/dist/resources/dev.resource.d.ts +1 -1
- package/dist/resources/dev.resource.js +2 -2
- package/dist/resources/dev.resource.js.map +1 -1
- package/dist/resources/docs.generator.resource.d.ts +4 -4
- package/dist/resources/docs.generator.resource.js +2 -2
- package/dist/resources/docs.generator.resource.js.map +1 -1
- package/dist/resources/graphql-accumulator.resource.d.ts +2 -2
- package/dist/resources/graphql-accumulator.resource.js +6 -3
- package/dist/resources/graphql-accumulator.resource.js.map +1 -1
- package/dist/resources/graphql.cli.resource.d.ts +1 -1
- package/dist/resources/graphql.cli.resource.js +2 -2
- package/dist/resources/graphql.cli.resource.js.map +1 -1
- package/dist/resources/graphql.query.cli.task.d.ts +14 -16
- package/dist/resources/graphql.query.cli.task.js +3 -3
- package/dist/resources/graphql.query.cli.task.js.map +1 -1
- package/dist/resources/graphql.query.task.d.ts +18 -20
- package/dist/resources/graphql.query.task.js +4 -4
- package/dist/resources/graphql.query.task.js.map +1 -1
- package/dist/resources/http.tag.d.ts +1 -1
- package/dist/resources/http.tag.js +2 -2
- package/dist/resources/http.tag.js.map +1 -1
- package/dist/resources/introspector.cli.resource.d.ts +2 -2
- package/dist/resources/introspector.cli.resource.js +14 -6
- package/dist/resources/introspector.cli.resource.js.map +1 -1
- package/dist/resources/introspector.resource.d.ts +3 -3
- package/dist/resources/introspector.resource.js +4 -5
- package/dist/resources/introspector.resource.js.map +1 -1
- package/dist/resources/live.resource.d.ts +4 -6
- package/dist/resources/live.resource.js +38 -25
- package/dist/resources/live.resource.js.map +1 -1
- package/dist/resources/models/Introspector.d.ts +28 -14
- package/dist/resources/models/Introspector.js +334 -161
- package/dist/resources/models/Introspector.js.map +1 -1
- package/dist/resources/models/durable.runtime.js +36 -10
- package/dist/resources/models/durable.runtime.js.map +1 -1
- package/dist/resources/models/durable.tools.d.ts +1 -1
- package/dist/resources/models/durable.tools.js +6 -3
- package/dist/resources/models/durable.tools.js.map +1 -1
- package/dist/resources/models/initializeFromStore.js +54 -21
- package/dist/resources/models/initializeFromStore.js.map +1 -1
- package/dist/resources/models/initializeFromStore.utils.d.ts +7 -6
- package/dist/resources/models/initializeFromStore.utils.js +302 -25
- package/dist/resources/models/initializeFromStore.utils.js.map +1 -1
- package/dist/resources/models/introspector.tools.js +18 -6
- package/dist/resources/models/introspector.tools.js.map +1 -1
- package/dist/resources/routeHandlers/getDocsData.d.ts +4 -0
- package/dist/resources/routeHandlers/getDocsData.js +28 -0
- package/dist/resources/routeHandlers/getDocsData.js.map +1 -1
- package/dist/resources/routeHandlers/registerHttpRoutes.hook.d.ts +26 -25
- package/dist/resources/routeHandlers/registerHttpRoutes.hook.js +10 -9
- package/dist/resources/routeHandlers/registerHttpRoutes.hook.js.map +1 -1
- package/dist/resources/server.resource.d.ts +20 -22
- package/dist/resources/server.resource.js +6 -6
- package/dist/resources/server.resource.js.map +1 -1
- package/dist/resources/swap.cli.resource.d.ts +4 -4
- package/dist/resources/swap.cli.resource.js +2 -2
- package/dist/resources/swap.cli.resource.js.map +1 -1
- package/dist/resources/swap.resource.d.ts +7 -7
- package/dist/resources/swap.resource.js +188 -38
- package/dist/resources/swap.resource.js.map +1 -1
- package/dist/resources/swap.tools.d.ts +3 -2
- package/dist/resources/swap.tools.js +27 -27
- package/dist/resources/swap.tools.js.map +1 -1
- package/dist/resources/telemetry.resource.d.ts +1 -1
- package/dist/resources/telemetry.resource.js +46 -43
- package/dist/resources/telemetry.resource.js.map +1 -1
- package/dist/runner-compat.d.ts +85 -0
- package/dist/runner-compat.js +178 -0
- package/dist/runner-compat.js.map +1 -0
- package/dist/runner-node-compat.d.ts +2 -0
- package/dist/runner-node-compat.js +28 -0
- package/dist/runner-node-compat.js.map +1 -0
- package/dist/schema/index.js +4 -8
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/model.d.ts +80 -23
- package/dist/schema/model.js.map +1 -1
- package/dist/schema/query.js +2 -1
- package/dist/schema/query.js.map +1 -1
- package/dist/schema/types/AllType.js +6 -3
- package/dist/schema/types/AllType.js.map +1 -1
- package/dist/schema/types/BaseElementCommon.js +2 -2
- package/dist/schema/types/ErrorType.js +1 -1
- package/dist/schema/types/ErrorType.js.map +1 -1
- package/dist/schema/types/EventType.js +19 -2
- package/dist/schema/types/EventType.js.map +1 -1
- package/dist/schema/types/LaneSummaryTypes.d.ts +3 -0
- package/dist/schema/types/LaneSummaryTypes.js +19 -0
- package/dist/schema/types/LaneSummaryTypes.js.map +1 -0
- package/dist/schema/types/LiveType.js +67 -0
- package/dist/schema/types/LiveType.js.map +1 -1
- package/dist/schema/types/ResourceType.js +100 -19
- package/dist/schema/types/ResourceType.js.map +1 -1
- package/dist/schema/types/RunOptionsType.js +41 -5
- package/dist/schema/types/RunOptionsType.js.map +1 -1
- package/dist/schema/types/TagType.js +35 -4
- package/dist/schema/types/TagType.js.map +1 -1
- package/dist/schema/types/TaskType.js +5 -0
- package/dist/schema/types/TaskType.js.map +1 -1
- package/dist/schema/types/index.d.ts +2 -2
- package/dist/schema/types/index.js +6 -7
- package/dist/schema/types/index.js.map +1 -1
- package/dist/schema/types/middleware/common.d.ts +3 -2
- package/dist/schema/types/middleware/common.js +19 -13
- package/dist/schema/types/middleware/common.js.map +1 -1
- package/dist/ui/.vite/manifest.json +2 -2
- package/dist/ui/assets/docs-Btkv97Ls.js +302 -0
- package/dist/ui/assets/docs-Btkv97Ls.js.map +1 -0
- package/dist/ui/assets/docs-CipvKUxZ.css +1 -0
- package/dist/utils/lane-resources.d.ts +55 -0
- package/dist/utils/lane-resources.js +143 -0
- package/dist/utils/lane-resources.js.map +1 -0
- package/dist/utils/zod.js +36 -3
- package/dist/utils/zod.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/readmes/runner-AI.md +740 -0
- package/readmes/runner-durable-workflows.md +2247 -0
- package/readmes/runner-full-guide.md +5869 -0
- package/readmes/runner-remote-lanes.md +909 -0
- package/dist/ui/assets/docs-BhRuaJ5l.css +0 -1
- package/dist/ui/assets/docs-H4oDZj7p.js +0 -302
- package/dist/ui/assets/docs-H4oDZj7p.js.map +0 -1
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
# BlueLibs Runner: AI Field Guide
|
|
2
|
+
|
|
3
|
+
Runner is a strongly typed application composition framework built around explicit contracts. Think of it as a graph of definitions: resources model long-lived services and lifecycle, tasks model business actions, events and hooks model decoupled reactions, middleware models cross-cutting behavior, and tags model discovery and policy. The point is not "just run some code", but to declare what exists, what depends on what, what gets validated, and how the whole system starts, runs, pauses, and shuts down.
|
|
4
|
+
|
|
5
|
+
It treats architecture as runtime-enforced structure rather than team convention. Dependency injection is explicit, validation is first-class, isolation boundaries are part of the model, and lifecycle phases are deliberate. So instead of building an app out of loosely connected modules, you build a constrained execution graph where contracts, composition, and operational behavior are visible and testable from the start.
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Mental Model
|
|
10
|
+
|
|
11
|
+
- `resource`: a singleton with lifecycle (`init`, `ready`, `cooldown`, `dispose`)
|
|
12
|
+
- `task`: a typed business action with DI, middleware, and validation
|
|
13
|
+
- `event`: a typed signal
|
|
14
|
+
- `hook`: a listener for an event
|
|
15
|
+
- `middleware`: a wrapper around a task or resource
|
|
16
|
+
- `tag`: metadata you can attach and query later
|
|
17
|
+
- `error`: a typed Runner error helper
|
|
18
|
+
- `run(app)`: bootstraps the graph and returns the runtime API
|
|
19
|
+
|
|
20
|
+
Prefer the flat globals for built-ins, exported by runner:
|
|
21
|
+
|
|
22
|
+
- `resources.*`
|
|
23
|
+
- `events.*`
|
|
24
|
+
- `tags.*`
|
|
25
|
+
- `middleware.*`
|
|
26
|
+
- `debug.levels`
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { resources, r, run } from "@bluelibs/runner";
|
|
32
|
+
|
|
33
|
+
const userCreated = r
|
|
34
|
+
.event<{ id: string; email: string }>("userCreated")
|
|
35
|
+
.build();
|
|
36
|
+
|
|
37
|
+
const userStore = r
|
|
38
|
+
.resource("userStore")
|
|
39
|
+
.init(async () => new Map<string, { id: string; email: string }>())
|
|
40
|
+
.build();
|
|
41
|
+
|
|
42
|
+
const createUser = r
|
|
43
|
+
.task<{ email: string }>("createUser")
|
|
44
|
+
.dependencies({
|
|
45
|
+
userCreated,
|
|
46
|
+
userStore,
|
|
47
|
+
logger: resources.logger,
|
|
48
|
+
})
|
|
49
|
+
.run(async (input, deps) => {
|
|
50
|
+
const user = { id: "user-1", email: input.email };
|
|
51
|
+
|
|
52
|
+
deps.userStore.set(user.id, user);
|
|
53
|
+
await deps.logger.info(`Created user ${user.email}`);
|
|
54
|
+
await deps.userCreated(user);
|
|
55
|
+
|
|
56
|
+
return user;
|
|
57
|
+
})
|
|
58
|
+
.build();
|
|
59
|
+
|
|
60
|
+
const sendWelcomeEmail = r
|
|
61
|
+
.hook("sendWelcomeEmail")
|
|
62
|
+
.on(userCreated)
|
|
63
|
+
.run(async (event) => {
|
|
64
|
+
console.log(`Welcome ${event.data.email}`);
|
|
65
|
+
})
|
|
66
|
+
.build();
|
|
67
|
+
|
|
68
|
+
const app = r
|
|
69
|
+
.resource("app")
|
|
70
|
+
.register([userStore, createUser, sendWelcomeEmail])
|
|
71
|
+
.build();
|
|
72
|
+
|
|
73
|
+
const runtime = await run(app);
|
|
74
|
+
|
|
75
|
+
await runtime.runTask(createUser, { email: "ada@example.com" });
|
|
76
|
+
await runtime.dispose();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Core Builder Rules and IDs
|
|
80
|
+
|
|
81
|
+
- Fluent builders chain methods and end with `.build()`.
|
|
82
|
+
- Configurable built definitions expose `.with(config)`.
|
|
83
|
+
- `r.task<Input>(id)` and `r.resource<Config>(id)` seed typing before explicit schemas.
|
|
84
|
+
- User-specified definition ids are local ids and cannot contain `.`. Use `send-email`, not `app.tasks.sendEmail`.
|
|
85
|
+
- Dotted `runner.*` and `system.*` ids are reserved for framework-owned internals.
|
|
86
|
+
- `.schema()` is the unified alias:
|
|
87
|
+
- task -> input schema
|
|
88
|
+
- resource -> config schema
|
|
89
|
+
- event -> payload schema
|
|
90
|
+
- error -> data schema
|
|
91
|
+
- Explicit builder methods still exist when you want readability:
|
|
92
|
+
- `.inputSchema(...)`
|
|
93
|
+
- `.configSchema(...)`
|
|
94
|
+
- `.payloadSchema(...)`
|
|
95
|
+
- `.dataSchema(...)`
|
|
96
|
+
- Tasks use `.resultSchema()` for output validation.
|
|
97
|
+
- Schema resolution prefers `parse(input)` when present; otherwise Runner falls back to pattern validation (`check(...)`).
|
|
98
|
+
- Builder schema slots accept plain Match patterns, compiled Match schemas, decorator-backed classes, or any schema object exposing `parse(...)`.
|
|
99
|
+
- For the strongest TypeScript inference in docs and user code, prefer `Match.compile(...)`, decorator-backed classes, or explicit builder generics such as `r.event<T>()`.
|
|
100
|
+
- List builders append by default. Pass `{ override: true }` to replace.
|
|
101
|
+
- `.meta({ ... })` is available across builders for docs and tooling.
|
|
102
|
+
- Builder order is enforced. After terminal methods like `.run()` or `.init()`, mutation surfaces are intentionally reduced.
|
|
103
|
+
- Prefer local names in definitions such as `task("createUser")`.
|
|
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:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Match, r } from "@bluelibs/runner";
|
|
120
|
+
|
|
121
|
+
const userInput = Match.compile({
|
|
122
|
+
email: Match.Email,
|
|
123
|
+
age: Match.Optional(Match.Integer),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const appConfig = Match.compile({
|
|
127
|
+
env: Match.OneOf("dev", "test", "prod"),
|
|
128
|
+
featureFlags: Match.Optional(Match.MapOf(Boolean)),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const createUser = r
|
|
132
|
+
.task("createUser")
|
|
133
|
+
.inputSchema(userInput) // same as .schema(userInput)
|
|
134
|
+
.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
|
+
.build();
|
|
143
|
+
|
|
144
|
+
const userCreated = r
|
|
145
|
+
.event("userCreated")
|
|
146
|
+
.payloadSchema(
|
|
147
|
+
Match.compile({ id: Match.NonEmptyString, email: Match.Email }),
|
|
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
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Runtime and Lifecycle
|
|
159
|
+
|
|
160
|
+
- `run(app, options?)` wires dependencies, initializes resources, emits lifecycle events, and returns the runtime API.
|
|
161
|
+
- The returned runtime exposes `runOptions`, the normalized effective `run(...)` options for that container.
|
|
162
|
+
- Main runtime helpers:
|
|
163
|
+
- `runTask`
|
|
164
|
+
- `emitEvent`
|
|
165
|
+
- `getResourceValue`
|
|
166
|
+
- `getLazyResourceValue`
|
|
167
|
+
- `getResourceConfig`
|
|
168
|
+
- `getHealth`
|
|
169
|
+
- `dispose`
|
|
170
|
+
- Lifecycle-shaping run options:
|
|
171
|
+
- `dryRun: true`: validate the graph without running `init()` / `ready()` or starting ingress.
|
|
172
|
+
- `lazy: true`: keep startup-unused resources asleep until `getLazyResourceValue(...)` wakes them; their `ready()` runs when they initialize.
|
|
173
|
+
- `lifecycleMode: "parallel"`: keep dependency ordering, but allow same-wave `init`, `ready`, `cooldown`, and `dispose` to run in parallel.
|
|
174
|
+
- `shutdownHooks: true`: install `SIGINT` / `SIGTERM` graceful shutdown hooks; signals during bootstrap cancel startup and roll back initialized resources.
|
|
175
|
+
- `dispose: { totalBudgetMs, drainingBudgetMs, cooldownWindowMs }`: control shutdown budget, drain wait, and the short post-`cooldown()` admissions window.
|
|
176
|
+
- `errorBoundary: true`: install process-level unhandled error capture and route it through `onUnhandledError`.
|
|
177
|
+
- `executionContext: true | { ... }`: enable correlation ids, causal-chain tracking, and cycle detection for task/event execution.
|
|
178
|
+
- `mode: "dev" | "prod" | "test"`: override environment-based mode detection.
|
|
179
|
+
- `debug` and `logs` tune observability; they do not change lifecycle semantics.
|
|
180
|
+
- For the full option table, see the `run() and RunOptions` section in [FULL_GUIDE.md](./FULL_GUIDE.md).
|
|
181
|
+
- Use `run(app, { debug: "verbose" })` for structured debug output.
|
|
182
|
+
- Use `run(app, { logs: { printThreshold: null } })` to silence console output.
|
|
183
|
+
- `runtime.pause()` is a synchronous, idempotent admission switch.
|
|
184
|
+
It stops new runtime-origin task and event admissions immediately, while already-running work can finish.
|
|
185
|
+
- `runtime.state` is `"running" | "paused"`.
|
|
186
|
+
- `runtime.resume()` reopens admissions immediately.
|
|
187
|
+
- `runtime.recoverWhen({ everyMs, check })` registers paused-state recovery conditions; Runner auto-resumes only after all active conditions for the current pause episode pass.
|
|
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 }`.
|
|
190
|
+
|
|
191
|
+
Lifecycle:
|
|
192
|
+
|
|
193
|
+
- Startup order:
|
|
194
|
+
- wire dependencies
|
|
195
|
+
- `init` resources
|
|
196
|
+
- lock runtime mutation surfaces
|
|
197
|
+
- run `ready()` in dependency order
|
|
198
|
+
- emit `events.ready`
|
|
199
|
+
- Shutdown order:
|
|
200
|
+
- enter `coolingDown`
|
|
201
|
+
- run `cooldown()` in reverse dependency order
|
|
202
|
+
- keep admissions open during `dispose.cooldownWindowMs`
|
|
203
|
+
- enter `disposing`
|
|
204
|
+
- emit `events.disposing`
|
|
205
|
+
- drain in-flight work
|
|
206
|
+
- emit `events.drained`
|
|
207
|
+
- run `dispose()` in reverse dependency order
|
|
208
|
+
|
|
209
|
+
## Resources
|
|
210
|
+
|
|
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.
|
|
213
|
+
|
|
214
|
+
- 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
|
+
- `init(config, deps, context)` creates the value.
|
|
218
|
+
- `ready(value, config, deps, context)` starts ingress after startup lock and runs after dependencies are all initialized.
|
|
219
|
+
- `cooldown(value, config, deps, context)` stops ingress quickly at shutdown start and runs during `coolingDown`, before `disposing` begins. Task runs and event emissions stay open during `coolingDown`, and if `dispose.cooldownWindowMs` is greater than `0` Runner keeps that broader admission policy open for the extra bounded window after cooldown completes. At the default `0`, Runner skips that wait. Once `disposing` begins, fresh admissions narrow to the cooling resource itself, any additional resource definitions returned from `cooldown()`, and in-flight continuations.
|
|
220
|
+
- `dispose(value, config, deps, context)` performs final teardown after drain and runs in reverse dependency order.
|
|
221
|
+
- `health(value, config, deps, context)` is an optional async probe used by `resources.health.getHealth(...)` and `runtime.getHealth(...)`.
|
|
222
|
+
Return `{ status: "healthy" | "degraded" | "unhealthy", message?, details? }`.
|
|
223
|
+
- Config-only resources can omit `.init()` — their resolved value is `undefined`; they are used purely for configuration access and registration.
|
|
224
|
+
- `r.resource(id, { gateway: true })` prevents the resource from adding its own namespace segment.
|
|
225
|
+
- Gateway resources cannot be passed directly to `run(...)`; wrap them in a non-gateway root resource first.
|
|
226
|
+
- If you register something, you are a non-leaf resource.
|
|
227
|
+
- 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
|
+
|
|
231
|
+
Use the lifecycle intentionally:
|
|
232
|
+
|
|
233
|
+
- `ready()` for starting HTTP listeners, consumers, schedulers, and similar ingress
|
|
234
|
+
- `cooldown()` for stopping new work immediately
|
|
235
|
+
- `dispose()` for final cleanup
|
|
236
|
+
|
|
237
|
+
Health reporting:
|
|
238
|
+
|
|
239
|
+
- Only resources that define `health()` participate.
|
|
240
|
+
- `resources.health` is the built-in health reporter resource from the exported `resources` namespace.
|
|
241
|
+
- Prefer `resources.health.getHealth()` inside resources; keep `runtime.getHealth()` for operator/runtime callers.
|
|
242
|
+
- Health checks are available only after `run(...)` resolves and before disposal starts.
|
|
243
|
+
- Calling `getHealth()` during disposal or after `dispose()` starts is invalid; treat health APIs as unavailable once shutdown begins.
|
|
244
|
+
- Startup-unused lazy resources stay asleep and are skipped; requested resources without `health()` are ignored.
|
|
245
|
+
- Result shape is `{ totals, report, find(...) }`, with counts for `healthy`, `degraded`, and `unhealthy`.
|
|
246
|
+
- `report` entries look like `{ id, initialized, status, message?, details? }`, where `id` is the canonical global runtime id.
|
|
247
|
+
- Use `report.find(resourceOrId).status` when you want one specific resource entry.
|
|
248
|
+
It returns the entry or throws if that resource is not present in the report.
|
|
249
|
+
- If `health()` throws, Runner records that resource as `unhealthy` and places the normalized error on `details`.
|
|
250
|
+
- When health indicates temporary pressure or outage, prefer `runtime.pause()` over shutdown.
|
|
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.
|
|
254
|
+
|
|
255
|
+
Do not use `cooldown()` as a general teardown phase for support resources like databases. Use `cooldown()` to stop accepting new external work; use `dispose()` for final teardown.
|
|
256
|
+
|
|
257
|
+
## Tasks
|
|
258
|
+
|
|
259
|
+
Tasks are your main business actions.
|
|
260
|
+
|
|
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
|
+
- Tasks are async functions with DI, middleware, validation, and typed output.
|
|
264
|
+
- Dependency maps are fail-fast validated. If `dependencies` is a function, it must resolve to an object map.
|
|
265
|
+
- Optional dependencies are explicit: `someResource.optional()`.
|
|
266
|
+
- `.throws([...])` declares error contracts for docs and tooling.
|
|
267
|
+
- Task `.run(input, deps, context)` receives three arguments:
|
|
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
|
|
273
|
+
|
|
274
|
+
Example showing all three parameters:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const sendEmail = r
|
|
278
|
+
.task<{ to: string; body: string }>("sendEmail")
|
|
279
|
+
.dependencies({ logger: resources.logger })
|
|
280
|
+
.run(async (input, { logger }, context) => {
|
|
281
|
+
// context.journal stores execution-local state accessible by middleware too
|
|
282
|
+
context.journal.set(auditKey, { startedAt: Date.now() });
|
|
283
|
+
await logger.info(`Sending email to ${input.to}`);
|
|
284
|
+
return { delivered: true };
|
|
285
|
+
})
|
|
286
|
+
.build();
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### ExecutionJournal
|
|
290
|
+
|
|
291
|
+
`ExecutionJournal` is typed state scoped to a single task execution.
|
|
292
|
+
|
|
293
|
+
- Use it when middleware and tasks need to share execution-local state.
|
|
294
|
+
- `journal.set(key, value)` fails if the key already exists.
|
|
295
|
+
- Pass `{ override: true }` when replacement is intentional.
|
|
296
|
+
- Create custom keys with `journal.createKey<T>(id)`.
|
|
297
|
+
- Task context includes `journal` and `source`.
|
|
298
|
+
|
|
299
|
+
## Events and Hooks
|
|
300
|
+
|
|
301
|
+
Events decouple producers from listeners. Hooks subscribe with `.on(event)` or `.on(onAnyOf(...))`; passing arrays directly is invalid.
|
|
302
|
+
|
|
303
|
+
Key rules:
|
|
304
|
+
|
|
305
|
+
- `.order(priority)` controls execution order. Lower numbers run first.
|
|
306
|
+
- `event.stopPropagation()` prevents downstream hooks from running.
|
|
307
|
+
- `.on("*")` listens to all visible events except those tagged with `tags.excludeFromGlobalHooks`.
|
|
308
|
+
- `.parallel(true)` allows concurrent same-priority listeners.
|
|
309
|
+
- `.transactional(true)` makes listeners reversible; each executed hook must return an async undo closure.
|
|
310
|
+
- Transactional constraints are fail-fast:
|
|
311
|
+
- `transactional + parallel` is invalid.
|
|
312
|
+
- `transactional + tags.eventLane` is invalid.
|
|
313
|
+
|
|
314
|
+
Emitters accept controls via `await event(payload, options?)`:
|
|
315
|
+
|
|
316
|
+
- `failureMode`: `"fail-fast"` (default, aborts on first hook error) or `"aggregate"` (runs all hooks, collects errors).
|
|
317
|
+
- `throwOnError`: `true` (default). When `false` with `report: true`, lets calling code handle failures gracefully.
|
|
318
|
+
- `report: true`: returns `{ totalListeners, attemptedListeners, skippedListeners, succeededListeners, failedListeners, propagationStopped, errors }`. (Note: for transactional events, fail-fast rollback is enforced regardless of mode).
|
|
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.
|
|
320
|
+
|
|
321
|
+
Transactional hook example:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const orderPlaced = r
|
|
325
|
+
.event<{ orderId: string }>("orderPlaced")
|
|
326
|
+
.transactional()
|
|
327
|
+
.build();
|
|
328
|
+
|
|
329
|
+
const reserveInventory = r
|
|
330
|
+
.hook("reserveInventory")
|
|
331
|
+
.on(orderPlaced)
|
|
332
|
+
.run(async (event) => {
|
|
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();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Middleware
|
|
341
|
+
|
|
342
|
+
Middleware wraps tasks or resources.
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
const audit = r.middleware
|
|
346
|
+
.task("audit")
|
|
347
|
+
.dependencies({ logger: resources.logger })
|
|
348
|
+
.run(async ({ task, next }, { logger }) => {
|
|
349
|
+
await logger.info(`-> ${task.definition.id}`);
|
|
350
|
+
const result = await next(task.input);
|
|
351
|
+
await logger.info(`<- ${task.definition.id}`);
|
|
352
|
+
return result;
|
|
353
|
+
})
|
|
354
|
+
.build();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Key rules:
|
|
358
|
+
|
|
359
|
+
- Create task middleware with `r.middleware.task(id)`.
|
|
360
|
+
- Create resource middleware with `r.middleware.resource(id)`.
|
|
361
|
+
- Attach middleware with `.middleware([...])`.
|
|
362
|
+
- First listed middleware is the outermost wrapper.
|
|
363
|
+
- Runner validates the target:
|
|
364
|
+
- task middleware can attach only to tasks or `subtree.tasks.middleware`
|
|
365
|
+
- resource middleware can attach only to resources or `subtree.resources.middleware`
|
|
366
|
+
- Owner-scoped auto-application is available through `resource.subtree({ tasks/resources: { middleware: [...] } })`.
|
|
367
|
+
- Contract middleware can constrain task input and output types.
|
|
368
|
+
- Built-in middleware covers common reliability concerns such as retry, cache, timeout, fallback, circuit breaker, rate limit, debounce, and concurrency.
|
|
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.
|
|
371
|
+
|
|
372
|
+
Task vs resource middleware:
|
|
373
|
+
|
|
374
|
+
- Task middleware wraps task execution.
|
|
375
|
+
- Resource middleware wraps resource initialization and resource value resolution.
|
|
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.
|
|
380
|
+
- Canonical ids differ:
|
|
381
|
+
- task middleware -> `app.middleware.task.name`
|
|
382
|
+
- resource middleware -> `app.middleware.resource.name`
|
|
383
|
+
|
|
384
|
+
### Global Interception
|
|
385
|
+
|
|
386
|
+
`eventManager.intercept(fn)`, `middlewareManager.intercept("task"|"resource", fn)`, `taskRunner.intercept(fn, options?)` wraps **all** task executions globally — the outermost layer. Use for cross-cutting concerns. Must be called inside a resource's `init()`.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
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
|
+
```
|
|
399
|
+
|
|
400
|
+
### Built-in Resilience Middleware
|
|
401
|
+
|
|
402
|
+
Runner ships with these resilience-focused built-ins.
|
|
403
|
+
|
|
404
|
+
| Middleware | Config | Notes |
|
|
405
|
+
| -------------- | ----------------------------------------- | ------------------------------------------------------------- |
|
|
406
|
+
| cache | `{ ttl, max, ttlAutopurge, keyBuilder }` | requires `resources.cache`; Node exposes `redisCacheProvider` |
|
|
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 |
|
|
415
|
+
|
|
416
|
+
Resource: `middleware.resource.retry`, `middleware.resource.timeout` (same semantics).
|
|
417
|
+
Non-resilience: `middleware.task.requireContext.with({ context })` — enforces async context.
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// Patterns: Order matters (outermost first)
|
|
421
|
+
r.task("cached").middleware([middleware.task.cache.with({ ttl: 60_000 })]).run(...).build();
|
|
422
|
+
r.task("fallback-retry").middleware([middleware.task.fallback.with({fallback:"default"}), middleware.task.retry.with({retries:3})]).run(...).build();
|
|
423
|
+
r.task("ratelimit-concurrency").middleware([middleware.task.rateLimit.with({windowMs:60_000,max:10}), middleware.task.concurrency.with({limit:5})]).run(...).build();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Order:** fallback (outermost) → timeout (inside retry if per-attempt budgets needed) → others.
|
|
427
|
+
**Use:** rate-limit for admission, concurrency for in-flight, circuit-breaker for fail-fast, cache for idempotent reads, debounce/throttle for bursty calls.
|
|
428
|
+
|
|
429
|
+
Built-in journal keys exist for middleware introspection:
|
|
430
|
+
|
|
431
|
+
- `middleware.task.cache.journalKeys.hit`
|
|
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`
|
|
437
|
+
|
|
438
|
+
## Data Contracts
|
|
439
|
+
|
|
440
|
+
### Validation
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { check, Match } from "@bluelibs/runner";
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- `check(value, pattern)` is the low-level runtime validator.
|
|
447
|
+
- `Match.compile(pattern)` creates reusable schemas with `.parse()`, `.test()`, and JSON-Schema export.
|
|
448
|
+
- Constructors act as matchers: `String`, `Number`, `Boolean`.
|
|
449
|
+
- Common `Match.*` helpers include `NonEmptyString`, `Email`, `Integer`, `UUID`, `URL`, `Optional()`, `OneOf()`, `ObjectIncluding()`, `MapOf()`, `ArrayOf()`, `Lazy()`, and `Where()`.
|
|
450
|
+
- Plain objects are strict by default, so `check(value, { name: String })` rejects unknown keys.
|
|
451
|
+
- `@Match.Schema({ base: BaseClass })` allows subclassing without TypeScript `extends`.
|
|
452
|
+
- Builder slots accept the same schema sources everywhere: task input/output, config, payload, tag config, and error data.
|
|
453
|
+
|
|
454
|
+
### Errors
|
|
455
|
+
|
|
456
|
+
- `r.error(...)` defines typed Runner errors.
|
|
457
|
+
- Helpers expose `new`, `create`, `throw`, and `is`.
|
|
458
|
+
- `.is(err, partialData?)` checks error lineage and an optional data subset.
|
|
459
|
+
- `.httpCode()` and `.remediation()` enrich errors for transport and operator feedback.
|
|
460
|
+
- `r.error.is(err)` checks whether a value is any Runner error.
|
|
461
|
+
|
|
462
|
+
### Serialization
|
|
463
|
+
|
|
464
|
+
- The built-in serializer round-trips common non-JSON shapes such as `Date` and `RegExp`.
|
|
465
|
+
- Register custom types through `resources.serializer`.
|
|
466
|
+
- Use `serializer.parse(payload, { schema })` when you want deserialization and validation in one step.
|
|
467
|
+
- `@Serializer.Field({ from, deserialize, serialize })` composes with `@Match.Field(...)` on `@Match.Schema()` classes for explicit DTOs.
|
|
468
|
+
|
|
469
|
+
## Testing
|
|
470
|
+
|
|
471
|
+
- In unit tests, build the smallest root resource that expresses the contract you care about.
|
|
472
|
+
- Run it with `await run(app)`.
|
|
473
|
+
- Assert through `runTask`, `emitEvent`, `getResourceValue`, or `getResourceConfig`.
|
|
474
|
+
- `r.override(base, fn)` is the standard way to swap behavior in tests while preserving ids.
|
|
475
|
+
|
|
476
|
+
## Composition Boundaries
|
|
477
|
+
|
|
478
|
+
Runner treats composition boundaries as first-class.
|
|
479
|
+
|
|
480
|
+
### Isolation
|
|
481
|
+
|
|
482
|
+
- Think of `.isolate(...)` as two controls on one boundary:
|
|
483
|
+
- `exports`: what this subtree exposes outward
|
|
484
|
+
- `deny` / `only` / `whitelist`: what consumers in this subtree may wire to across boundaries
|
|
485
|
+
- `exports: []` or `exports: "none"` makes the subtree private. Export entries must be explicit Runner definition or resource references.
|
|
486
|
+
- Runtime operator APIs such as `runTask`, `emitEvent`, and `getResourceValue` are gated only by the root resource's `isolate.exports` surface.
|
|
487
|
+
- `.isolate((config) => ({ ... }))` resolves once per configured resource instance.
|
|
488
|
+
|
|
489
|
+
Selector model:
|
|
490
|
+
|
|
491
|
+
- direct ref: one concrete definition/resource/tag
|
|
492
|
+
- `subtreeOf(resource, { types? })`: everything owned by that resource subtree
|
|
493
|
+
- `scope(target, channels?)`: apply the rule only to selected channels: `dependencies`, `listening`, `tagging`, `middleware`
|
|
494
|
+
- string selectors are valid only inside `scope(...)`
|
|
495
|
+
- `scope("*")`: everything
|
|
496
|
+
- `scope("system.*")`: all registered canonical ids matching that segment wildcard
|
|
497
|
+
- `scope("app.resources.*")`: one dotted segment per `*`
|
|
498
|
+
- `subtreeOf(resource)` is ownership-based, not string-prefix-based
|
|
499
|
+
|
|
500
|
+
Rule model:
|
|
501
|
+
|
|
502
|
+
- `deny`: block matching cross-boundary targets
|
|
503
|
+
- `only`: allow only matching cross-boundary targets
|
|
504
|
+
- `whitelist`: per-boundary consumer -> target carve-out; it relaxes this boundary's `deny` / `only`, but does not override ancestor restrictions or make private exports public
|
|
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: [...] })`
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
.isolate({
|
|
512
|
+
deny: [subtreeOf(adminResource), scope([internalEvent], { listening: false })],
|
|
513
|
+
whitelist: [{ for: [healthTask], targets: [resources.health] }],
|
|
514
|
+
})
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Examples:
|
|
518
|
+
|
|
519
|
+
- Hide everything except one task from the outside:
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
.isolate({
|
|
523
|
+
exports: [createInvoice],
|
|
524
|
+
})
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- Block all `system.*` dependencies for this subtree except `runnerDev`:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
.isolate({
|
|
531
|
+
deny: [scope("system.*", { dependencies: true })],
|
|
532
|
+
whitelist: [
|
|
533
|
+
{
|
|
534
|
+
for: [scope(subtreeOf(runnerDev), { dependencies: true })],
|
|
535
|
+
targets: [scope("system.*", { dependencies: true })],
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
})
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- Allow only tasks owned by another subtree:
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
.isolate({
|
|
545
|
+
only: [subtreeOf(agentResource, { types: ["task"] })],
|
|
546
|
+
})
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Subtrees
|
|
550
|
+
|
|
551
|
+
- `.subtree(policy)` and `.subtree((config) => policy)` can auto-attach middleware to nested tasks/resources.
|
|
552
|
+
- Subtrees can validate contained definitions.
|
|
553
|
+
- `subtree.validate` is generic for compiled subtree definitions and can be one function or an array.
|
|
554
|
+
- Typed validation is also available on `tasks`, `resources`, `hooks`, `events`, `tags`, `taskMiddleware`, and `resourceMiddleware`.
|
|
555
|
+
- Generic and typed validators both run when they match the same compiled definition.
|
|
556
|
+
- Validators receive only the compiled definition. Use `subtree((config) => ({ ... }))` when the policy depends on resource config.
|
|
557
|
+
- Use exported guards such as `isTask(...)` and `isResource(...)` inside `subtree.validate(...)` for cross-type checks.
|
|
558
|
+
- Validators are return-based:
|
|
559
|
+
- return `SubtreeViolation[]` for normal policy failures
|
|
560
|
+
- do not throw for expected validation failures
|
|
561
|
+
|
|
562
|
+
### Forks and Overrides
|
|
563
|
+
|
|
564
|
+
- `resource.fork(newId)` clones a leaf resource definition under a new id.
|
|
565
|
+
- Forks clone identity, not structure.
|
|
566
|
+
- If a resource declares `.register(...)`, it is non-leaf and `.fork()` is invalid.
|
|
567
|
+
- Use `.fork(...)` when you need another instance of a leaf resource.
|
|
568
|
+
- `.fork()` is not supported for gateway resources.
|
|
569
|
+
- `.fork()` returns a built resource. You do not call `.build()` again.
|
|
570
|
+
- Compose a distinct parent resource when you need a structural variant of a non-leaf resource.
|
|
571
|
+
- Durable support is registered via `resources.durable`, while concrete durable backends use normal forks such as `resources.memoryWorkflow.fork("app-durable")`.
|
|
572
|
+
- Use `r.override(base, fn)` when you need to replace behavior while preserving the original id.
|
|
573
|
+
- `.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. Child resources cannot replace parent-owned or sibling-owned definitions.
|
|
575
|
+
- Override targets must already exist in the graph.
|
|
576
|
+
|
|
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
|
+
## Tags and Scheduling
|
|
584
|
+
|
|
585
|
+
Tags are Runner's typed discovery system. They attach metadata to definitions, can influence framework behavior, and can also be consumed as dependencies to discover matching definitions at runtime.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { Match, r } from "@bluelibs/runner";
|
|
589
|
+
|
|
590
|
+
const httpRoute = r
|
|
591
|
+
.tag("httpRoute")
|
|
592
|
+
.for(["tasks"])
|
|
593
|
+
.configSchema(
|
|
594
|
+
Match.compile({
|
|
595
|
+
method: Match.OneOf("GET", "POST"),
|
|
596
|
+
path: Match.NonEmptyString,
|
|
597
|
+
}),
|
|
598
|
+
)
|
|
599
|
+
.build();
|
|
600
|
+
|
|
601
|
+
const getHealth = r
|
|
602
|
+
.task("getHealth")
|
|
603
|
+
.tags([httpRoute.with({ method: "GET", path: "/health" })])
|
|
604
|
+
.run(async () => ({ ok: true }))
|
|
605
|
+
.build();
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Key rules:
|
|
609
|
+
|
|
610
|
+
- Depending on a tag injects a typed accessor over matching definitions.
|
|
611
|
+
- `.for([...])` restricts which definition kinds can receive the tag.
|
|
612
|
+
- Tag configs are typed and validated like any other config surface, so `.configSchema(...)` accepts Match patterns, `Match.compile(...)`, class schemas, or any `parse(...)` schema.
|
|
613
|
+
- Contract tags shape task or resource typing without changing runtime behavior.
|
|
614
|
+
- 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 debug config.
|
|
616
|
+
- Tasks can opt into runtime health gating with `tags.failWhenUnhealthy.with([db, cache])`.
|
|
617
|
+
It blocks only when one of those resources reports `unhealthy`; `degraded` still runs, bootstrap-time task calls are not gated, and sleeping lazy resources stay skipped.
|
|
618
|
+
- Tags are often the cleanest way to implement auto-discovery such as HTTP route registration, cron scheduling, cache warmers, or internal policies without manual registries.
|
|
619
|
+
|
|
620
|
+
Cron:
|
|
621
|
+
|
|
622
|
+
- `tags.cron` schedules tasks with cron expressions.
|
|
623
|
+
- Attach it on the task with `tags.cron.with({ expression: "* * * * *" })`; for example: `.tags([tags.cron.with({ expression: "0 9 * * *", immediate: true, ... })])`.
|
|
624
|
+
- Cron runs only when `resources.cron` is registered.
|
|
625
|
+
- One cron tag per task is supported.
|
|
626
|
+
- If `resources.cron` is not registered, cron tags remain metadata only.
|
|
627
|
+
|
|
628
|
+
## Execution Context and Request Tracing
|
|
629
|
+
|
|
630
|
+
> `ExecutionContext`: auto-managed bookkeeping (`correlationId`, `depth`, cycle detection). Different from `AsyncContext` (user-owned state).
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// Enable globally. Top-level runtime task/event calls now get a correlation id automatically.
|
|
634
|
+
const runtime = await run(app, { executionContext: true }); // or { cycleDetection: false }
|
|
635
|
+
|
|
636
|
+
await runtime.runTask(handleRequest, input);
|
|
637
|
+
await runtime.emitEvent(userSeen, payload);
|
|
638
|
+
|
|
639
|
+
// Use inside tasks/hooks/interceptors
|
|
640
|
+
const myTask = r
|
|
641
|
+
.task("myTask")
|
|
642
|
+
.run(async () => {
|
|
643
|
+
const { correlationId, depth, frames } = asyncContexts.execution.use();
|
|
644
|
+
})
|
|
645
|
+
.build();
|
|
646
|
+
|
|
647
|
+
// Optional: seed your own correlation id at an external boundary
|
|
648
|
+
await asyncContexts.execution.provide(
|
|
649
|
+
{ correlationId: req.headers["x-id"] },
|
|
650
|
+
() => runtime.runTask(handleRequest, input),
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// Optional: capture the exact execution tree during testing/tracing
|
|
654
|
+
const { result, recording } = await asyncContexts.execution.record(() =>
|
|
655
|
+
runtime.runTask(myTask, input),
|
|
656
|
+
);
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`executionContext: true` already creates execution context for top-level runtime task runs and event emissions. You do not need `provide()` just to enable propagation.
|
|
660
|
+
|
|
661
|
+
Use `provide()` when you want to seed or override the correlation id from an external boundary such as HTTP, RPC, or a queue consumer.
|
|
662
|
+
|
|
663
|
+
Use `record()` when you want the execution tree back for assertions, tracing, or debugging.
|
|
664
|
+
|
|
665
|
+
Cycle protection comes in layers:
|
|
666
|
+
|
|
667
|
+
- declared `.dependencies(...)` cycles fail during bootstrap graph validation (it is middleware-aware too)
|
|
668
|
+
- declared hook-driven event bounce graphs fail during bootstrap event-emission validation
|
|
669
|
+
- dynamic runtime loops such as `task -> event -> hook -> task` need `executionContext.cycleDetection` enabled to be stopped at execution time
|
|
670
|
+
|
|
671
|
+
`executionContext` is Node-only in practice because it requires `AsyncLocalStorage`.
|
|
672
|
+
|
|
673
|
+
## Async Context
|
|
674
|
+
|
|
675
|
+
Defines serializable request-local state scoped to an async execution tree (requires `AsyncLocalStorage`; Node-only in practice).
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
import { r } from "@bluelibs/runner";
|
|
679
|
+
|
|
680
|
+
const tenantCtx = r.asyncContext<string>("tenantId");
|
|
681
|
+
|
|
682
|
+
// Provide at the request boundary
|
|
683
|
+
await tenantCtx.provide("acme-corp", () =>
|
|
684
|
+
runtime.runTask(handleRequest, input),
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// Consume anywhere downstream in the same async tree
|
|
688
|
+
const myTask = r
|
|
689
|
+
.task("myTask")
|
|
690
|
+
.run(async () => {
|
|
691
|
+
const tenantId = tenantCtx.use(); // "acme-corp"
|
|
692
|
+
})
|
|
693
|
+
.build();
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Contexts can be injected as dependencies or enforced by middleware via `middleware.task.requireContext.with({ context: tenantCtx })`. Custom `serialize` / `parse` support propagation over RPC lanes.
|
|
697
|
+
|
|
698
|
+
## Queue
|
|
699
|
+
|
|
700
|
+
`resources.queue` provides named FIFO queues. Each queue id gets its own isolated instance.
|
|
701
|
+
|
|
702
|
+
`queue.run(id, task)` schedules work sequentially. Each queued task receives `(signal: AbortSignal) => Promise<void>`, and the signal fires during `dispose()` — always respect it to avoid hanging shutdown:
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
await queue.run("uploads", async (signal) => {
|
|
706
|
+
if (signal.aborted) return;
|
|
707
|
+
await processFile(file, signal);
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
## Remote Lanes (Node)
|
|
712
|
+
|
|
713
|
+
Event lanes are async fire-and-forget routing for events across Runner instances. RPC lanes are synchronous cross-runner task or event calls.
|
|
714
|
+
|
|
715
|
+
Supported modes: `network`, `transparent`, `local-simulated`. Async-context propagation over RPC lanes is allowlist-based.
|
|
716
|
+
|
|
717
|
+
Full detail: `readmes/REMOTE_LANES_AI.md`, `readmes/REMOTE_LANES.md`
|
|
718
|
+
|
|
719
|
+
## Observability and Project Structure
|
|
720
|
+
|
|
721
|
+
### Observability
|
|
722
|
+
|
|
723
|
+
- `resources.logger` is the built-in structured logger.
|
|
724
|
+
- Loggers support `trace`, `debug`, `info`, `warn`, `error`, and `critical`.
|
|
725
|
+
- `logger.with({ source, additionalContext })` creates contextual child loggers that share the same root listeners and buffering.
|
|
726
|
+
- `logger.onLog(async (log) => { ... })` lets you forward, redact, or collect logs without routing them through the event system.
|
|
727
|
+
- `run(app, { logs: { printThreshold, printStrategy, bufferLogs } })` controls printing and startup buffering.
|
|
728
|
+
- Prefer stable `source` ids and low-cardinality context fields such as `requestId`, `taskId`, or `tenantId`.
|
|
729
|
+
|
|
730
|
+
### Project Structure
|
|
731
|
+
|
|
732
|
+
- Prefer feature-driven folders.
|
|
733
|
+
- Prefer naming by Runner item type:
|
|
734
|
+
- `*.task.ts`
|
|
735
|
+
- `*.resource.ts`
|
|
736
|
+
- `*.event.ts`
|
|
737
|
+
- `*.hook.ts`
|
|
738
|
+
- `*.middleware.ts`
|
|
739
|
+
- `*.tag.ts`
|
|
740
|
+
- `*.error.ts`
|