@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.
Files changed (148) hide show
  1. package/AI.md +25 -3
  2. package/README.md +190 -55
  3. package/dist/cli/generators/artifact.js +2 -14
  4. package/dist/cli/generators/artifact.js.map +1 -1
  5. package/dist/cli/generators/common.d.ts +1 -0
  6. package/dist/cli/generators/common.js +22 -0
  7. package/dist/cli/generators/common.js.map +1 -1
  8. package/dist/cli/generators/printNewHelp.js +2 -2
  9. package/dist/cli/generators/printNewHelp.js.map +1 -1
  10. package/dist/cli/generators/scaffold/templates/package.json.d.ts +2 -2
  11. package/dist/cli/generators/scaffold/templates/package.json.js +2 -2
  12. package/dist/cli/generators/scaffold/templates/src/main.ts.js +7 -9
  13. package/dist/cli/generators/scaffold/templates/src/main.ts.js.map +1 -1
  14. package/dist/cli/generators/scaffold.js +1 -135
  15. package/dist/cli/generators/scaffold.js.map +1 -1
  16. package/dist/cli/generators/templates.js +64 -63
  17. package/dist/cli/generators/templates.js.map +1 -1
  18. package/dist/generated/resolvers-types.d.ts +376 -144
  19. package/dist/index.d.ts +39 -43
  20. package/dist/resources/cli.config.resource.d.ts +1 -1
  21. package/dist/resources/cli.config.resource.js +2 -2
  22. package/dist/resources/cli.config.resource.js.map +1 -1
  23. package/dist/resources/coverage.resource.d.ts +2 -2
  24. package/dist/resources/coverage.resource.js +3 -3
  25. package/dist/resources/coverage.resource.js.map +1 -1
  26. package/dist/resources/dev.resource.d.ts +1 -1
  27. package/dist/resources/dev.resource.js +2 -2
  28. package/dist/resources/dev.resource.js.map +1 -1
  29. package/dist/resources/docs.generator.resource.d.ts +4 -4
  30. package/dist/resources/docs.generator.resource.js +2 -2
  31. package/dist/resources/docs.generator.resource.js.map +1 -1
  32. package/dist/resources/graphql-accumulator.resource.d.ts +2 -2
  33. package/dist/resources/graphql-accumulator.resource.js +6 -3
  34. package/dist/resources/graphql-accumulator.resource.js.map +1 -1
  35. package/dist/resources/graphql.cli.resource.d.ts +1 -1
  36. package/dist/resources/graphql.cli.resource.js +2 -2
  37. package/dist/resources/graphql.cli.resource.js.map +1 -1
  38. package/dist/resources/graphql.query.cli.task.d.ts +14 -16
  39. package/dist/resources/graphql.query.cli.task.js +3 -3
  40. package/dist/resources/graphql.query.cli.task.js.map +1 -1
  41. package/dist/resources/graphql.query.task.d.ts +18 -20
  42. package/dist/resources/graphql.query.task.js +4 -4
  43. package/dist/resources/graphql.query.task.js.map +1 -1
  44. package/dist/resources/http.tag.d.ts +1 -1
  45. package/dist/resources/http.tag.js +2 -2
  46. package/dist/resources/http.tag.js.map +1 -1
  47. package/dist/resources/introspector.cli.resource.d.ts +2 -2
  48. package/dist/resources/introspector.cli.resource.js +14 -6
  49. package/dist/resources/introspector.cli.resource.js.map +1 -1
  50. package/dist/resources/introspector.resource.d.ts +3 -3
  51. package/dist/resources/introspector.resource.js +4 -5
  52. package/dist/resources/introspector.resource.js.map +1 -1
  53. package/dist/resources/live.resource.d.ts +4 -6
  54. package/dist/resources/live.resource.js +38 -25
  55. package/dist/resources/live.resource.js.map +1 -1
  56. package/dist/resources/models/Introspector.d.ts +28 -14
  57. package/dist/resources/models/Introspector.js +334 -161
  58. package/dist/resources/models/Introspector.js.map +1 -1
  59. package/dist/resources/models/durable.runtime.js +36 -10
  60. package/dist/resources/models/durable.runtime.js.map +1 -1
  61. package/dist/resources/models/durable.tools.d.ts +1 -1
  62. package/dist/resources/models/durable.tools.js +6 -3
  63. package/dist/resources/models/durable.tools.js.map +1 -1
  64. package/dist/resources/models/initializeFromStore.js +54 -21
  65. package/dist/resources/models/initializeFromStore.js.map +1 -1
  66. package/dist/resources/models/initializeFromStore.utils.d.ts +7 -6
  67. package/dist/resources/models/initializeFromStore.utils.js +302 -25
  68. package/dist/resources/models/initializeFromStore.utils.js.map +1 -1
  69. package/dist/resources/models/introspector.tools.js +18 -6
  70. package/dist/resources/models/introspector.tools.js.map +1 -1
  71. package/dist/resources/routeHandlers/getDocsData.d.ts +4 -0
  72. package/dist/resources/routeHandlers/getDocsData.js +28 -0
  73. package/dist/resources/routeHandlers/getDocsData.js.map +1 -1
  74. package/dist/resources/routeHandlers/registerHttpRoutes.hook.d.ts +26 -25
  75. package/dist/resources/routeHandlers/registerHttpRoutes.hook.js +10 -9
  76. package/dist/resources/routeHandlers/registerHttpRoutes.hook.js.map +1 -1
  77. package/dist/resources/server.resource.d.ts +20 -22
  78. package/dist/resources/server.resource.js +6 -6
  79. package/dist/resources/server.resource.js.map +1 -1
  80. package/dist/resources/swap.cli.resource.d.ts +4 -4
  81. package/dist/resources/swap.cli.resource.js +2 -2
  82. package/dist/resources/swap.cli.resource.js.map +1 -1
  83. package/dist/resources/swap.resource.d.ts +7 -7
  84. package/dist/resources/swap.resource.js +188 -38
  85. package/dist/resources/swap.resource.js.map +1 -1
  86. package/dist/resources/swap.tools.d.ts +3 -2
  87. package/dist/resources/swap.tools.js +27 -27
  88. package/dist/resources/swap.tools.js.map +1 -1
  89. package/dist/resources/telemetry.resource.d.ts +1 -1
  90. package/dist/resources/telemetry.resource.js +46 -43
  91. package/dist/resources/telemetry.resource.js.map +1 -1
  92. package/dist/runner-compat.d.ts +85 -0
  93. package/dist/runner-compat.js +178 -0
  94. package/dist/runner-compat.js.map +1 -0
  95. package/dist/runner-node-compat.d.ts +2 -0
  96. package/dist/runner-node-compat.js +28 -0
  97. package/dist/runner-node-compat.js.map +1 -0
  98. package/dist/schema/index.js +4 -8
  99. package/dist/schema/index.js.map +1 -1
  100. package/dist/schema/model.d.ts +80 -23
  101. package/dist/schema/model.js.map +1 -1
  102. package/dist/schema/query.js +2 -1
  103. package/dist/schema/query.js.map +1 -1
  104. package/dist/schema/types/AllType.js +6 -3
  105. package/dist/schema/types/AllType.js.map +1 -1
  106. package/dist/schema/types/BaseElementCommon.js +2 -2
  107. package/dist/schema/types/ErrorType.js +1 -1
  108. package/dist/schema/types/ErrorType.js.map +1 -1
  109. package/dist/schema/types/EventType.js +19 -2
  110. package/dist/schema/types/EventType.js.map +1 -1
  111. package/dist/schema/types/LaneSummaryTypes.d.ts +3 -0
  112. package/dist/schema/types/LaneSummaryTypes.js +19 -0
  113. package/dist/schema/types/LaneSummaryTypes.js.map +1 -0
  114. package/dist/schema/types/LiveType.js +67 -0
  115. package/dist/schema/types/LiveType.js.map +1 -1
  116. package/dist/schema/types/ResourceType.js +100 -19
  117. package/dist/schema/types/ResourceType.js.map +1 -1
  118. package/dist/schema/types/RunOptionsType.js +41 -5
  119. package/dist/schema/types/RunOptionsType.js.map +1 -1
  120. package/dist/schema/types/TagType.js +35 -4
  121. package/dist/schema/types/TagType.js.map +1 -1
  122. package/dist/schema/types/TaskType.js +5 -0
  123. package/dist/schema/types/TaskType.js.map +1 -1
  124. package/dist/schema/types/index.d.ts +2 -2
  125. package/dist/schema/types/index.js +6 -7
  126. package/dist/schema/types/index.js.map +1 -1
  127. package/dist/schema/types/middleware/common.d.ts +3 -2
  128. package/dist/schema/types/middleware/common.js +19 -13
  129. package/dist/schema/types/middleware/common.js.map +1 -1
  130. package/dist/ui/.vite/manifest.json +2 -2
  131. package/dist/ui/assets/docs-Btkv97Ls.js +302 -0
  132. package/dist/ui/assets/docs-Btkv97Ls.js.map +1 -0
  133. package/dist/ui/assets/docs-CipvKUxZ.css +1 -0
  134. package/dist/utils/lane-resources.d.ts +55 -0
  135. package/dist/utils/lane-resources.js +143 -0
  136. package/dist/utils/lane-resources.js.map +1 -0
  137. package/dist/utils/zod.js +36 -3
  138. package/dist/utils/zod.js.map +1 -1
  139. package/dist/version.d.ts +1 -1
  140. package/dist/version.js +1 -1
  141. package/package.json +4 -4
  142. package/readmes/runner-AI.md +740 -0
  143. package/readmes/runner-durable-workflows.md +2247 -0
  144. package/readmes/runner-full-guide.md +5869 -0
  145. package/readmes/runner-remote-lanes.md +909 -0
  146. package/dist/ui/assets/docs-BhRuaJ5l.css +0 -1
  147. package/dist/ui/assets/docs-H4oDZj7p.js +0 -302
  148. 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`