@bluelibs/runner-dev 6.0.1 → 6.2.0

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