@bluelibs/runner-dev 6.2.0 → 6.4.0

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