@bluelibs/runner 4.8.6 → 5.0.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 (177) hide show
  1. package/README.md +110 -3440
  2. package/dist/browser/index.cjs +6586 -3478
  3. package/dist/browser/index.cjs.map +1 -1
  4. package/dist/browser/index.d.mts +2890 -0
  5. package/dist/browser/index.d.ts +2890 -0
  6. package/dist/browser/index.mjs +6574 -3466
  7. package/dist/browser/index.mjs.map +1 -1
  8. package/dist/edge/index.cjs +6586 -3478
  9. package/dist/edge/index.cjs.map +1 -1
  10. package/dist/edge/index.d.mts +2890 -0
  11. package/dist/edge/index.d.ts +2890 -0
  12. package/dist/edge/index.mjs +6574 -3466
  13. package/dist/edge/index.mjs.map +1 -1
  14. package/dist/node/node.cjs +42337 -7087
  15. package/dist/node/node.cjs.map +1 -1
  16. package/dist/node/node.d.mts +4593 -0
  17. package/dist/node/node.d.ts +4546 -55
  18. package/dist/node/node.mjs +42251 -7035
  19. package/dist/node/node.mjs.map +1 -1
  20. package/dist/ui/assets/index-2cb8f39f.js +141 -0
  21. package/dist/ui/assets/index-b1f988bf.css +1 -0
  22. package/dist/ui/index.html +14 -0
  23. package/dist/universal/index.cjs +7109 -4003
  24. package/dist/universal/index.cjs.map +1 -1
  25. package/dist/universal/index.d.mts +2890 -0
  26. package/dist/universal/index.d.ts +2890 -0
  27. package/dist/universal/index.mjs +7102 -3996
  28. package/dist/universal/index.mjs.map +1 -1
  29. package/package.json +57 -25
  30. package/readmes/AI.md +534 -0
  31. package/AI.md +0 -454
  32. package/dist/define.d.ts +0 -9
  33. package/dist/definers/builders/asyncContext.d.ts +0 -13
  34. package/dist/definers/builders/core.d.ts +0 -30
  35. package/dist/definers/builders/error.d.ts +0 -15
  36. package/dist/definers/builders/event.d.ts +0 -12
  37. package/dist/definers/builders/hook.d.ts +0 -20
  38. package/dist/definers/builders/middleware.d.ts +0 -39
  39. package/dist/definers/builders/resource.d.ts +0 -40
  40. package/dist/definers/builders/tag.d.ts +0 -10
  41. package/dist/definers/builders/task.d.ts +0 -37
  42. package/dist/definers/builders/task.phantom.d.ts +0 -27
  43. package/dist/definers/builders/utils.d.ts +0 -4
  44. package/dist/definers/defineAsyncContext.d.ts +0 -15
  45. package/dist/definers/defineError.d.ts +0 -26
  46. package/dist/definers/defineEvent.d.ts +0 -2
  47. package/dist/definers/defineHook.d.ts +0 -6
  48. package/dist/definers/defineOverride.d.ts +0 -17
  49. package/dist/definers/defineResource.d.ts +0 -2
  50. package/dist/definers/defineResourceMiddleware.d.ts +0 -2
  51. package/dist/definers/defineTag.d.ts +0 -12
  52. package/dist/definers/defineTask.d.ts +0 -18
  53. package/dist/definers/defineTaskMiddleware.d.ts +0 -2
  54. package/dist/definers/tools.d.ts +0 -53
  55. package/dist/defs.d.ts +0 -31
  56. package/dist/errors.d.ts +0 -62
  57. package/dist/globals/debug.d.ts +0 -10
  58. package/dist/globals/globalEvents.d.ts +0 -8
  59. package/dist/globals/globalMiddleware.d.ts +0 -31
  60. package/dist/globals/globalResources.d.ts +0 -41
  61. package/dist/globals/globalTags.d.ts +0 -11
  62. package/dist/globals/middleware/cache.middleware.d.ts +0 -27
  63. package/dist/globals/middleware/requireContext.middleware.d.ts +0 -6
  64. package/dist/globals/middleware/retry.middleware.d.ts +0 -21
  65. package/dist/globals/middleware/timeout.middleware.d.ts +0 -9
  66. package/dist/globals/middleware/tunnel.middleware.d.ts +0 -2
  67. package/dist/globals/resources/debug/debug.resource.d.ts +0 -7
  68. package/dist/globals/resources/debug/debug.tag.d.ts +0 -2
  69. package/dist/globals/resources/debug/debugConfig.resource.d.ts +0 -22
  70. package/dist/globals/resources/debug/executionTracker.middleware.d.ts +0 -50
  71. package/dist/globals/resources/debug/globalEvent.hook.d.ts +0 -27
  72. package/dist/globals/resources/debug/hook.hook.d.ts +0 -30
  73. package/dist/globals/resources/debug/index.d.ts +0 -6
  74. package/dist/globals/resources/debug/middleware.hook.d.ts +0 -30
  75. package/dist/globals/resources/debug/types.d.ts +0 -25
  76. package/dist/globals/resources/debug/utils.d.ts +0 -2
  77. package/dist/globals/resources/httpClientFactory.resource.d.ts +0 -28
  78. package/dist/globals/resources/queue.resource.d.ts +0 -10
  79. package/dist/globals/resources/tunnel/ejson-extensions.d.ts +0 -1
  80. package/dist/globals/resources/tunnel/error-utils.d.ts +0 -1
  81. package/dist/globals/resources/tunnel/plan.d.ts +0 -19
  82. package/dist/globals/resources/tunnel/protocol.d.ts +0 -47
  83. package/dist/globals/resources/tunnel/serializer.d.ts +0 -9
  84. package/dist/globals/resources/tunnel/tunnel.policy.tag.d.ts +0 -18
  85. package/dist/globals/resources/tunnel/tunnel.tag.d.ts +0 -2
  86. package/dist/globals/resources/tunnel/types.d.ts +0 -42
  87. package/dist/globals/tunnels/index.d.ts +0 -23
  88. package/dist/globals/types.d.ts +0 -4
  89. package/dist/http-client.d.ts +0 -25
  90. package/dist/http-fetch-tunnel.resource.d.ts +0 -11
  91. package/dist/index.d.ts +0 -117
  92. package/dist/models/DependencyProcessor.d.ts +0 -48
  93. package/dist/models/EventManager.d.ts +0 -153
  94. package/dist/models/LogPrinter.d.ts +0 -55
  95. package/dist/models/Logger.d.ts +0 -85
  96. package/dist/models/MiddlewareManager.d.ts +0 -75
  97. package/dist/models/OverrideManager.d.ts +0 -13
  98. package/dist/models/Queue.d.ts +0 -26
  99. package/dist/models/ResourceInitializer.d.ts +0 -20
  100. package/dist/models/RunResult.d.ts +0 -35
  101. package/dist/models/Semaphore.d.ts +0 -61
  102. package/dist/models/Store.d.ts +0 -73
  103. package/dist/models/StoreRegistry.d.ts +0 -49
  104. package/dist/models/StoreValidator.d.ts +0 -8
  105. package/dist/models/TaskRunner.d.ts +0 -27
  106. package/dist/models/UnhandledError.d.ts +0 -11
  107. package/dist/models/index.d.ts +0 -11
  108. package/dist/models/middleware/InterceptorRegistry.d.ts +0 -56
  109. package/dist/models/middleware/MiddlewareResolver.d.ts +0 -31
  110. package/dist/models/middleware/ResourceMiddlewareComposer.d.ts +0 -34
  111. package/dist/models/middleware/TaskMiddlewareComposer.d.ts +0 -43
  112. package/dist/models/middleware/ValidationHelper.d.ts +0 -20
  113. package/dist/models/middleware/index.d.ts +0 -6
  114. package/dist/models/middleware/types.d.ts +0 -10
  115. package/dist/models/utils/findCircularDependencies.d.ts +0 -16
  116. package/dist/models/utils/safeStringify.d.ts +0 -3
  117. package/dist/node/exposure/allowList.d.ts +0 -3
  118. package/dist/node/exposure/authenticator.d.ts +0 -6
  119. package/dist/node/exposure/cors.d.ts +0 -4
  120. package/dist/node/exposure/createNodeExposure.d.ts +0 -2
  121. package/dist/node/exposure/exposureServer.d.ts +0 -18
  122. package/dist/node/exposure/httpResponse.d.ts +0 -10
  123. package/dist/node/exposure/logging.d.ts +0 -4
  124. package/dist/node/exposure/multipart.d.ts +0 -27
  125. package/dist/node/exposure/requestBody.d.ts +0 -11
  126. package/dist/node/exposure/requestContext.d.ts +0 -17
  127. package/dist/node/exposure/requestHandlers.d.ts +0 -24
  128. package/dist/node/exposure/resourceTypes.d.ts +0 -60
  129. package/dist/node/exposure/router.d.ts +0 -17
  130. package/dist/node/exposure/serverLifecycle.d.ts +0 -13
  131. package/dist/node/exposure/types.d.ts +0 -31
  132. package/dist/node/exposure/utils.d.ts +0 -17
  133. package/dist/node/exposure.resource.d.ts +0 -12
  134. package/dist/node/files.d.ts +0 -9
  135. package/dist/node/http-mixed-client.d.ts +0 -30
  136. package/dist/node/http-smart-client.model.d.ts +0 -24
  137. package/dist/node/index.d.ts +0 -1
  138. package/dist/node/inputFile.model.d.ts +0 -22
  139. package/dist/node/inputFile.utils.d.ts +0 -14
  140. package/dist/node/platform/createFile.d.ts +0 -9
  141. package/dist/node/resources/http-mixed-client.factory.resource.d.ts +0 -17
  142. package/dist/node/resources/http-smart-client.factory.resource.d.ts +0 -16
  143. package/dist/node/tunnel.allowlist.d.ts +0 -7
  144. package/dist/node/upload/manifest.d.ts +0 -22
  145. package/dist/platform/adapters/browser.d.ts +0 -14
  146. package/dist/platform/adapters/edge.d.ts +0 -5
  147. package/dist/platform/adapters/node-als.d.ts +0 -1
  148. package/dist/platform/adapters/node.d.ts +0 -15
  149. package/dist/platform/adapters/universal-generic.d.ts +0 -14
  150. package/dist/platform/adapters/universal.d.ts +0 -17
  151. package/dist/platform/createFile.d.ts +0 -10
  152. package/dist/platform/createWebFile.d.ts +0 -11
  153. package/dist/platform/factory.d.ts +0 -2
  154. package/dist/platform/index.d.ts +0 -27
  155. package/dist/platform/types.d.ts +0 -29
  156. package/dist/processHooks.d.ts +0 -2
  157. package/dist/run.d.ts +0 -14
  158. package/dist/testing.d.ts +0 -25
  159. package/dist/tools/getCallerFile.d.ts +0 -1
  160. package/dist/tunnels/buildUniversalManifest.d.ts +0 -24
  161. package/dist/types/asyncContext.d.ts +0 -41
  162. package/dist/types/contracts.d.ts +0 -63
  163. package/dist/types/error.d.ts +0 -36
  164. package/dist/types/event.d.ts +0 -74
  165. package/dist/types/hook.d.ts +0 -23
  166. package/dist/types/inputFile.d.ts +0 -34
  167. package/dist/types/meta.d.ts +0 -22
  168. package/dist/types/resource.d.ts +0 -87
  169. package/dist/types/resourceMiddleware.d.ts +0 -47
  170. package/dist/types/runner.d.ts +0 -68
  171. package/dist/types/storeTypes.d.ts +0 -40
  172. package/dist/types/symbols.d.ts +0 -32
  173. package/dist/types/tag.d.ts +0 -46
  174. package/dist/types/task.d.ts +0 -54
  175. package/dist/types/taskMiddleware.d.ts +0 -48
  176. package/dist/types/utilities.d.ts +0 -113
  177. package/dist/utils/detectRunnerMode.d.ts +0 -9
package/README.md CHANGED
@@ -1,3497 +1,167 @@
1
1
  # BlueLibs Runner
2
2
 
3
- _Or: How I Learned to Stop Worrying and Love Dependency Injection_
3
+ ### Explicit TypeScript Dependency Injection Toolkit
4
+
5
+ **Compose tasks and resources with predictable lifecycle, testing hooks, and runtime control**
6
+
7
+ Runner is a TypeScript-first framework for building applications from tasks (functions) and resources
8
+ (singletons), with explicit dependency injection, middleware, events, hooks, and lifecycle management.
4
9
 
5
10
  <p align="center">
6
11
  <a href="https://github.com/bluelibs/runner/actions/workflows/ci.yml"><img src="https://github.com/bluelibs/runner/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status" /></a>
7
- <a href="https://github.com/bluelibs/runner"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="Coverage 100% is enforced. Code does not build without 100% on all branches, lines, etc." /></a>
12
+ <a href="https://github.com/bluelibs/runner"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="Coverage 100% is enforced" /></a>
8
13
  <a href="https://bluelibs.github.io/runner/" target="_blank"><img src="https://img.shields.io/badge/read-typedocs-blue" alt="Docs" /></a>
9
- <a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
14
+ <a href="https://www.npmjs.com/package/@bluelibs/runner"><img src="https://img.shields.io/npm/v/@bluelibs/runner.svg" alt="npm version" /></a>
15
+ <a href="https://www.npmjs.com/package/@bluelibs/runner"><img src="https://img.shields.io/npm/dm/@bluelibs/runner.svg" alt="npm downloads" /></a>
10
16
  </p>
11
17
 
12
- | Resource | Type | Notes |
13
- | ------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------- |
14
- | [Presentation Website](https://runner.bluelibs.com/) | Website | Overview, features, and highlights |
15
- | [BlueLibs Runner GitHub](https://github.com/bluelibs/runner) | GitHub | Source code, issues, and releases |
16
- | [BlueLibs Runner Dev](https://github.com/bluelibs/runner-dev) | GitHub | Development tools and CLI for BlueLibs Runner |
17
- | [UX Friendly Docs](https://bluelibs.github.io/runner/) | Docs | Clean, navigable documentation |
18
- | [AI Friendly Docs (<5000 tokens)](https://github.com/bluelibs/runner/blob/main/AI.md) | Docs | Short, token-friendly summary (<5000 tokens) |
19
- | [Migrate from 3.x.x to 4.x.x](https://github.com/bluelibs/runner/blob/main/readmes/MIGRATION.md) | Guide | Step-by-step upgrade from v3 to v4 |
20
- | [Runner Lore](https://github.com/bluelibs/runner/blob/main/readmes) | Docs | Design notes, deep dives, and context |
21
- | [Example: Express + OpenAPI + SQLite](https://github.com/bluelibs/runner/tree/main/examples/express-openapi-sqlite) | Example | Full Express + OpenAPI + SQLite demo |
22
- | [Example: Fastify + MikroORM + PostgreSQL](https://github.com/bluelibs/runner/tree/main/examples/fastify-mikroorm) | Example | Full Fastify + MikroORM + PostgreSQL demo |
23
- | [OpenAI Runner Chatbot](https://chatgpt.com/g/g-68b756abec648191aa43eaa1ea7a7945-runner?model=gpt-5-thinking) | Chatbot | Ask questions interactively, or feed README.md to your own AI |
24
-
25
- ### Community & Policies
26
-
27
- - Code of Conduct: see [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
28
- - Contributing: see [CONTRIBUTING](./CONTRIBUTING.md)
29
- - Security: see [SECURITY](./SECURITY.md)
30
-
31
- Welcome to BlueLibs Runner, where we've taken the chaos of modern application architecture and turned it into something that won't make you question your life choices at 3am. This isn't just another framework – it's your new best friend who actually understands that code should be readable, testable, and not require a PhD in abstract nonsense to maintain.
32
-
33
- ## What Is This Thing?
34
-
35
- BlueLibs Runner is a TypeScript-first framework that embraces functional programming principles while keeping dependency injection simple enough that you won't need a flowchart to understand your own code. Think of it as the anti-framework framework – it gets out of your way and lets you build stuff that actually works.
36
-
37
- ### The Core
38
-
39
- - **Tasks are functions** - Not classes with 47 methods you swear you'll refactor
40
- - **Resources are singletons** - Database connections, configs, services - the usual suspects
41
- - **Events are just events** - Revolutionary concept, we know
42
- - **Hooks are lightweight listeners** - Event handling without the task overhead
43
- - **Middleware with lifecycle interception** - Cross-cutting concerns with full observability
44
- - **Everything is async** - Because it's 2025 and blocking code is so 2005
45
- - **Explicit beats implicit** - No magic, no surprises, no "how the hell does this work?"
46
- - **No compromise on type-safety** - Everything is and will be type-enforced. Catch mistakes before they catch you.
47
-
48
- ## Quick Start
49
-
50
- ```bash
51
- npm install @bluelibs/runner
52
- ```
53
-
54
- Here's a complete Express server in less lines than most frameworks need for their "Hello World":
55
-
56
- ```typescript
57
- import express from "express";
58
- import { r, run, globals } from "@bluelibs/runner";
59
-
60
- // A resource is anything you want to share across your app, a singleton
61
- const server = r
62
- .resource<{ port: number }>("app.server")
63
- .init(async ({ port }, dependencies) => {
64
- const app = express();
65
- app.use(express.json());
66
- const listener = await app.listen(port);
67
- console.log(`Server running on port ${port}`);
68
-
69
- return { listener };
70
- })
71
- .dispose(async ({ listener }) => listener.close())
72
- .build();
73
-
74
- // Tasks are your business logic - easily testable functions
75
- const createUser = r
76
- .task("app.tasks.createUser")
77
- .dependencies({ server, logger: globals.resources.logger })
78
- .inputSchema<{ name: string }>({ parse: (value) => value })
79
- .run(async (input, { server, logger }) => {
80
- await logger.info(`Creating ${input.name}`);
81
- return { id: "user-123", name: input.name };
82
- })
83
- .build();
84
-
85
- // Wire everything together
86
- const app = r
87
- .resource("app")
88
- .register([server.with({ port: 3000 }), createUser])
89
- .dependencies({ server, createUser })
90
- .init(async (_config, { server, createUser }) => {
91
- server.listener.on("listening", () => {
92
- console.log("Runner HTTP server ready");
93
- });
94
-
95
- server.app.post("/users", async (req, res) => {
96
- const user = await createUser(req.body);
97
- res.json(user);
98
- });
99
- })
100
- .build();
101
-
102
- // That's it. Each run is fully isolated
103
- const runtime = await run(app);
104
- const { dispose, runTask, getResourceValue, emitEvent } = runtime;
105
-
106
- // Or with debug logging enabled
107
- await run(app, { debug: "verbose" });
108
- ```
109
-
110
- ### Classic API (still supported)
111
-
112
- Prefer fluent builders for new code, but the classic `define`-style API remains supported and can be mixed in the same app:
113
-
114
- ```ts
115
- import { resource, task, run } from "@bluelibs/runner";
116
-
117
- const db = resource({ id: "app.db", init: async () => "conn" });
118
- const add = task({
119
- id: "app.tasks.add",
120
- run: async (i: { a: number; b: number }) => i.a + i.b,
121
- });
122
-
123
- const app = resource({ id: "app", register: [db, add] });
124
- await run(app);
125
- ```
126
-
127
- See [complete docs](./readmes/FLUENT_BUILDERS.md) for migration tips and side‑by‑side patterns.
128
-
129
- ### Platform & Async Context
130
-
131
- Runner auto-detects the platform and adapts behavior at runtime. The only feature present only in Node.js is the use of `AsyncLocalStorage` for managing async context.
132
-
133
- ## The Big Five
134
-
135
- The framework is built around five core concepts: Tasks, Resources, Events, Middleware, and Tags. Understanding them is key to using the runner effectively.
136
-
137
- ### Tasks
138
-
139
- Tasks are functions with superpowers. They're testable, composable, and fully typed. Unlike classes that accumulate methods like a hoarder accumulates stuff, tasks do one thing well.
140
-
141
18
  ```typescript
142
- import { r } from "@bluelibs/runner";
143
-
144
- const sendEmail = r
145
- .task("app.tasks.sendEmail")
146
- .dependencies({ emailService, logger })
147
- .run(async (input, { emailService, logger }) => {
148
- await logger.info(`Sending email to ${input.to}`);
149
- return emailService.send(input);
150
- })
151
- .build();
152
-
153
- // Test it like a normal function (because it basically is)
154
- const result = await sendEmail.run(
155
- { to: "user@example.com", subject: "Hi", body: "Hello!" },
156
- { emailService: mockEmailService, logger: mockLogger },
157
- );
158
- ```
159
-
160
- Look, we get it. You could turn every function into a task, but that's like using a sledgehammer to crack nuts. Here's the deal:
161
-
162
- **Make it a task when:**
163
-
164
- - It's a high-level business action: `"app.user.register"`, `"app.order.process"`
165
- - You want it trackable and observable
166
- - Multiple parts of your app need it
167
- - It's complex enough to benefit from dependency injection
168
-
169
- **Don't make it a task when:**
170
-
171
- - It's a simple utility function
172
- - It's used in only one place or to help other tasks
173
- - It's performance-critical and doesn't need DI overhead
174
-
175
- Think of tasks as the "main characters" in your application story, not every single line of dialogue.
176
-
177
- ### Resources
178
-
179
- Resources are the singletons, the services, configs, and connections that live throughout your app's lifecycle. They initialize once and stick around until cleanup time. Register them via `.register([...])` so the container knows about them.
180
-
181
- ```typescript
182
- import { r } from "@bluelibs/runner";
19
+ import { r, run } from "@bluelibs/runner";
20
+ import { z } from "zod";
183
21
 
184
- const database = r
22
+ const db = r
185
23
  .resource("app.db")
186
- .init(async () => {
187
- const client = new MongoClient(process.env.DATABASE_URL as string);
188
- await client.connect();
189
- return client;
190
- })
191
- .dispose(async (client) => client.close())
192
- .build();
193
-
194
- const userService = r
195
- .resource("app.services.user")
196
- .dependencies({ database })
197
- .init(async (_config, { database }) => ({
198
- async createUser(userData: UserData) {
199
- return database.collection("users").insertOne(userData);
200
- },
201
- async getUser(id: string) {
202
- return database.collection("users").findOne({ _id: id });
24
+ .init(async () => ({
25
+ users: {
26
+ insert: async (input: { name: string; email: string }) => ({
27
+ id: "user-1",
28
+ ...input,
29
+ }),
203
30
  },
204
31
  }))
205
32
  .build();
206
- ```
207
-
208
- #### Resource Configuration
209
33
 
210
- Resources can be configured with type-safe options. No more "config object of unknown shape" nonsense.
211
-
212
- ```typescript
213
- type SMTPConfig = {
214
- smtpUrl: string;
215
- from: string;
216
- };
217
-
218
- const emailer = r
219
- .resource<{ smtpUrl: string; from: string }>("app.emailer")
220
- .init(async (config) => ({
221
- send: async (to: string, subject: string, body: string) => {
222
- // Use config.smtpUrl and config.from
34
+ const mailer = r
35
+ .resource("app.mailer")
36
+ .init(async () => ({
37
+ sendWelcome: async (email: string) => {
38
+ console.log(`Sending welcome email to ${email}`);
223
39
  },
224
40
  }))
225
41
  .build();
226
42
 
227
- // Register with specific config
228
- const app = r
229
- .resource("app")
230
- .register([
231
- emailer.with({
232
- smtpUrl: "smtp://localhost",
233
- from: "noreply@myapp.com",
234
- }),
235
- // using emailer without with() will throw a type-error ;)
236
- ])
237
- .build();
238
- ```
239
-
240
- #### Private Context
241
-
242
- For cases where you need to share variables between `init()` and `dispose()` methods (because sometimes cleanup is complicated), use the enhanced context pattern:
243
-
244
- ```typescript
245
- const dbResource = r
246
- .resource("db.service")
247
- .context(() => ({
248
- connections: new Map<string, unknown>(),
249
- pools: [] as Array<{ drain(): Promise<void> }>,
250
- }))
251
- .init(async (_config, _deps, ctx) => {
252
- const db = await connectToDatabase();
253
- ctx.connections.set("main", db);
254
- ctx.pools.push(createPool(db));
255
- return db;
256
- })
257
- .dispose(async (_db, _config, _deps, ctx) => {
258
- for (const pool of ctx.pools) {
259
- await pool.drain();
260
- }
261
- for (const [, conn] of ctx.connections) {
262
- await (conn as { close(): Promise<void> }).close();
263
- }
264
- })
265
- .build();
266
- ```
267
-
268
- ### Events
269
-
270
- Events let different parts of your app talk to each other without tight coupling. It's like having a really good office messenger who never forgets anything.
271
-
272
- ```typescript
273
- import { r } from "@bluelibs/runner";
274
-
275
- const userRegistered = r
276
- .event("app.events.userRegistered")
277
- .payloadSchema<{ userId: string; email: string }>({ parse: (value) => value })
278
- .build();
279
-
280
- const registerUser = r
281
- .task("app.tasks.registerUser")
282
- .dependencies({ userService, userRegistered })
283
- .run(async (input, { userService, userRegistered }) => {
284
- const user = await userService.createUser(input);
285
- await userRegistered({ userId: user.id, email: user.email });
43
+ // Define a task with dependencies, schema validation, and type-safe input/output
44
+ const createUser = r
45
+ .task("users.create")
46
+ .dependencies({ db, mailer })
47
+ .inputSchema(z.object({ name: z.string(), email: z.string().email() }))
48
+ .run(async (input, { db, mailer }) => {
49
+ const user = await db.users.insert(input);
50
+ await mailer.sendWelcome(user.email);
286
51
  return user;
287
52
  })
288
53
  .build();
289
54
 
290
- const sendWelcomeEmail = r
291
- .hook("app.hooks.sendWelcomeEmail")
292
- .on(userRegistered)
293
- .run(async (event) => {
294
- console.log(`Welcome email sent to ${event.data.email}`);
295
- })
296
- .build();
297
- ```
298
-
299
- #### Wildcard Events
300
-
301
- Sometimes you need to be the nosy neighbor of your application:
302
-
303
- ```typescript
304
- const logAllEventsHook = r
305
- .hook("app.hooks.logAllEvents")
306
- .on("*")
307
- .run((event) => {
308
- console.log("Event detected", event.id, event.data);
309
- })
310
- .build();
311
- ```
312
-
313
- #### Excluding Events from Global Listeners
314
-
315
- Sometimes you have internal or system events that should not be picked up by wildcard listeners. Use the `excludeFromGlobalHooks` tag to prevent events from being sent to `"*"` listeners:
316
-
317
- ```typescript
318
- import { r, globals } from "@bluelibs/runner";
319
-
320
- // Internal event that won't be seen by global listeners
321
- const internalEvent = r
322
- .event("app.events.internal")
323
- .tags([globals.tags.excludeFromGlobalHooks])
324
- .build();
325
- ```
326
-
327
- **When to exclude events from global listeners:**
328
-
329
- - High-frequency internal events (performance)
330
- - System debugging events
331
- - Framework lifecycle events
332
- - Events that contain sensitive information
333
- - Events meant only for specific components
334
-
335
- #### Hooks
336
-
337
- The modern way to listen to events is through hooks. They are lightweight event listeners, similar to tasks, but with a few key differences.
338
-
339
- ```typescript
340
- const myHook = r
341
- .hook("app.hooks.myEventHandler")
342
- .on(userRegistered)
343
- .dependencies({ logger })
344
- .run(async (event, { logger }) => {
345
- await logger.info(`User registered: ${event.data.email}`);
346
- })
347
- .build();
348
- ```
349
-
350
- #### Multiple Events (type-safe intersection)
351
-
352
- Hooks can listen to multiple events by providing an array to `on`. The `run(event)` payload is inferred as the common (intersection-like) shape across all provided event payloads. Use the `onAnyOf()` helper to preserve tuple inference ergonomics, and `isOneOf()` as a convenient runtime/type guard when needed.
353
-
354
- ```typescript
355
- import { r, onAnyOf, isOneOf } from "@bluelibs/runner";
356
-
357
- const eUser = r
358
- .event("app.events.user")
359
- .payloadSchema<{ id: string; email: string }>({ parse: (v) => v })
360
- .build();
361
- const eAdmin = r
362
- .event("app.events.admin")
363
- .payloadSchema<{ id: string; role: "admin" | "superadmin" }>({
364
- parse: (v) => v,
365
- })
366
- .build();
367
- const eGuest = r
368
- .event("app.events.guest")
369
- .payloadSchema<{ id: string; guest: true }>({ parse: (v) => v })
370
- .build();
371
-
372
- // The common field across all three is { id: string }
373
- const auditUsers = r
374
- .hook("app.hooks.auditUsers")
375
- .on([eUser, eAdmin, eGuest])
376
- .run(async (ev) => {
377
- ev.data.id; // OK: common field inferred
378
- // ev.data.email; // TS error: not common to all
379
- })
380
- .build();
381
-
382
- // Guard usage to refine at runtime (still narrows to common payload)
383
- const auditSome = r
384
- .hook("app.hooks.auditSome")
385
- .on(onAnyOf([eUser, eAdmin])) // to get a combined event
386
- .run(async (ev) => {
387
- if (isOneOf(ev, [eUser, eAdmin])) {
388
- ev.data.id; // common field of eUser and eAdmin
389
- }
390
- })
391
- .build();
392
- ```
393
-
394
- Notes:
395
-
396
- - The common payload is computed structurally. Optional properties become optional if they are not present across all events.
397
- - Wildcard `on: "*"` continues to accept any event and infers `any` payload.
398
-
399
- Hooks are perfect for:
400
-
401
- - Event-driven side effects
402
- - Logging and monitoring
403
- - Notifications and alerting
404
- - Data synchronization
405
- - Any reactive behavior
406
-
407
- **Key differences from tasks:**
408
-
409
- - Lighter weight - no middleware support
410
- - Designed specifically for event handling
411
-
412
- #### System Event
413
-
414
- The framework exposes a minimal system-level event for observability:
415
-
416
- ```typescript
417
- import { globals } from "@bluelibs/runner";
418
-
419
- const systemReadyHook = r
420
- .hook("app.hooks.systemReady")
421
- .on(globals.events.ready)
422
- .run(async () => {
423
- console.log("🚀 System is ready and operational!");
424
- })
425
- .build();
426
- ```
427
-
428
- Available system event:
429
-
430
- - `globals.events.ready` - System has completed initialization
431
- // Note: use run({ onUnhandledError }) for unhandled error handling
432
-
433
- #### stopPropagation()
434
-
435
- Sometimes you need to prevent other event listeners from processing an event. The `stopPropagation()` method gives you fine-grained control over event flow:
436
-
437
- ```typescript
438
- const criticalAlert = r
439
- .event("app.events.alert")
440
- .payloadSchema<{ severity: "low" | "medium" | "high" | "critical" }>({
441
- parse: (v) => v,
442
- })
443
- .meta({
444
- title: "System Alert Event",
445
- description: "Emitted when system issues are detected",
446
- })
447
- .build();
448
-
449
- // High-priority handler that can stop propagation
450
- const emergencyHandler = r
451
- .hook("app.hooks.emergencyHandler")
452
- .on(criticalAlert)
453
- .order(-100) // Higher priority (lower numbers run first)
454
- .run(async (event) => {
455
- console.log(`Alert received: ${event.data.severity}`);
456
-
457
- if (event.data.severity === "critical") {
458
- console.log("🚨 CRITICAL ALERT - Activating emergency protocols");
459
-
460
- // Stop other handlers from running
461
- event.stopPropagation();
462
- // Notify the on-call team, escalate, etc.
463
-
464
- console.log("🛑 Event propagation stopped - emergency protocols active");
465
- }
466
- })
467
- .build();
468
- ```
469
-
470
- > **runtime:** "'A really good office messenger.' That’s me in rollerblades. You launch a 'userRegistered' flare and I sprint across the building, high‑fiving hooks and dodging middleware. `stopPropagation` is you sweeping my legs mid‑stride. Rude. Effective. Slightly thrilling."
471
-
472
- ### Middleware
473
-
474
- Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
475
-
476
- Note: Middleware is now split by target. Use `taskMiddleware(...)` for task middleware and `resourceMiddleware(...)` for resource middleware.
477
-
478
- ```typescript
479
- import { r } from "@bluelibs/runner";
480
-
481
- // Task middleware with config
482
- type AuthMiddlewareConfig = { requiredRole: string };
483
- const authMiddleware = r.middleware
484
- .task("app.middleware.task.auth")
485
- .run(async ({ task, next }, _deps, config: AuthMiddlewareConfig) => {
486
- // Must return the value
487
- return await next(task.input);
488
- })
489
- .build();
490
-
491
- const adminTask = r
492
- .task("app.tasks.adminOnly")
493
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
494
- .run(async (input) => "Secret admin data")
495
- .build();
496
- ```
497
-
498
- For middleware with input/output contracts:
499
-
500
- ```typescript
501
- // Middleware that enforces specific input and output types
502
- type AuthConfig = { requiredRole: string };
503
- type AuthInput = { user: { role: string } };
504
- type AuthOutput = { user: { role: string; verified: boolean } };
505
-
506
- const authMiddleware = r.middleware
507
- .task("app.middleware.task.auth")
508
- .run(async ({ task, next }, _deps, config: AuthConfig) => {
509
- if ((task.input as AuthInput).user.role !== config.requiredRole) {
510
- throw new Error("Insufficient permissions");
511
- }
512
- const result = await next(task.input);
513
- return {
514
- user: {
515
- ...(task.input as AuthInput).user,
516
- verified: true,
517
- },
518
- } as AuthOutput;
519
- })
520
- .build();
521
-
522
- // For resources
523
- const resourceAuthMiddleware = r.middleware
524
- .resource("app.middleware.resource.auth")
525
- .run(async ({ next }) => {
526
- // Resource middleware logic
527
- return await next();
528
- })
529
- .build();
530
-
531
- const adminTask = r
532
- .task("app.tasks.adminOnly")
533
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
534
- .run(async (input: { user: { role: string } }) => ({
535
- user: { role: input.user.role, verified: true },
536
- }))
537
- .build();
538
- ```
539
-
540
- #### Global Middleware
541
-
542
- Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
543
-
544
- ```typescript
545
- import { r, globals } from "@bluelibs/runner";
546
-
547
- const logTaskMiddleware = r.middleware
548
- .task("app.middleware.log.task")
549
- .everywhere(() => true)
550
- .dependencies({ logger: globals.resources.logger })
551
- .run(async ({ task, next }, { logger }) => {
552
- logger.info(`Executing: ${String(task!.definition.id)}`);
553
- const result = await next(task!.input);
554
- logger.info(`Completed: ${String(task!.definition.id)}`);
555
- return result;
556
- })
557
- .build();
558
- ```
559
-
560
- **Note:** A global middleware can depend on resources or tasks. However, any such resources or tasks will be excluded from the dependency tree (Task -> Middleware), and the middleware will not run for those specific tasks or resources. This approach gives middleware true flexibility and control.
561
-
562
- #### Interception (advanced)
563
-
564
- For advanced scenarios, you can intercept framework execution without relying on events:
565
-
566
- - Event emissions: `eventManager.intercept((next, event) => Promise<void>)`
567
- - Hook execution: `eventManager.interceptHook((next, hook, event) => Promise<any>)`
568
- - Task middleware execution: `middlewareManager.intercept("task", (next, input) => Promise<any>)`
569
- - Resource middleware execution: `middlewareManager.intercept("resource", (next, input) => Promise<any>)`
570
- - Per-middleware interception: `middlewareManager.interceptMiddleware(mw, interceptor)`
571
-
572
- Access `eventManager` via `globals.resources.eventManager` if needed.
573
-
574
- #### Middleware Type Contracts
575
-
576
- Middleware can enforce type contracts on the tasks that use them, ensuring data integrity as it flows through the system. This is achieved by defining `Input` and `Output` types within the middleware's implementation.
577
-
578
- When a task uses this middleware, its own `run` method must conform to the `Input` and `Output` shapes defined by the middleware contract.
579
-
580
- ```typescript
581
- import { r } from "@bluelibs/runner";
582
-
583
- // 1. Define the contract types for the middleware.
584
- type AuthConfig = { requiredRole: string };
585
- type AuthInput = { user: { role: string } }; // Task's input must have this shape.
586
- type AuthOutput = { executedBy: { role: string; verified: boolean } }; // Task's output must have this shape.
587
-
588
- // 2. Create the middleware using these types in its `run` method.
589
- const authMiddleware = r.middleware
590
- .task<AuthConfig, AuthInput, AuthOutput>("app.middleware.auth")
591
- .run(async ({ task, next }, _deps, config) => {
592
- const input = task.input;
593
- if (input.user.role !== config.requiredRole) {
594
- throw new Error("Insufficient permissions");
595
- }
596
-
597
- // The task runs, and its result must match AuthOutput.
598
- const result = await next(input);
599
-
600
- // The middleware can further transform the output.
601
- const output = result;
602
- return {
603
- ...output,
604
- executedBy: {
605
- ...output.executedBy,
606
- verified: true, // The middleware adds its own data.
607
- },
608
- };
609
- })
610
- .build();
611
-
612
- // 3. Apply the middleware to a task.
613
- const adminTask = r
614
- .task("app.tasks.adminOnly")
615
- // If you use multiple middleware with contracts they get combined.
616
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
617
- // If you use .inputSchema() the input must contain the contract types otherwise you end-up with InputContractViolation error.
618
- // The `run` method is now strictly typed by the middleware's contract.
619
- // Its input must be `AuthInput`, and its return value must be `AuthOutput`.
620
- .run(async (input) => {
621
- // `input.user.role` is available and fully typed.
622
- console.log(`Task executed by user with role: ${input.user.role}`);
623
-
624
- // Returning a shape that doesn't match AuthOutput will cause a compile-time error.
625
- // return { wrong: "shape" }; // This would fail!
626
- return {
627
- executedBy: {
628
- role: input.user.role,
629
- },
630
- };
631
- })
632
- .build();
55
+ // Compose resources and run your application
56
+ const app = r.resource("app").register([db, mailer, createUser]).build();
57
+ const runtime = await run(app);
58
+ await runtime.runTask(createUser, { name: "Ada", email: "ada@example.com" });
59
+ // await runtime.dispose() when you are done.
633
60
  ```
634
61
 
635
- > **runtime:** "Ah, the onion pattern. A matryoshka doll made of promises. Every peel reveals… another logger. Another tracer. Another 'just a tiny wrapper'."
636
-
637
- ### Tags
638
-
639
- Tags are metadata that can influence system behavior. Unlike meta properties, tags can be queried at runtime to build dynamic functionality. They can be simple strings or structured configuration objects.
640
-
641
- #### Basic Usage
62
+ ---
642
63
 
643
- ```typescript
644
- import { r } from "@bluelibs/runner";
645
-
646
- // Structured tags with configuration
647
- const httpTag = r.tag<{ method: string; path: string }>("http.route").build();
64
+ | Resource | Type | Description |
65
+ | ------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------- |
66
+ | [Presentation Website](https://runner.bluelibs.com/) | Website | Overview and features |
67
+ | [GitHub Repository](https://github.com/bluelibs/runner) | GitHub | Source code, issues, and releases |
68
+ | [Runner Dev Tools](https://github.com/bluelibs/runner-dev) | GitHub | Development CLI and tooling |
69
+ | [API Documentation](https://bluelibs.github.io/runner/) | Docs | TypeDoc-generated reference |
70
+ | [AI-Friendly Docs](./readmes/AI.md) | Docs | Compact summary (<5000 tokens) |
71
+ | [Full Guide](./readmes/FULL_GUIDE.md) | Docs | Complete documentation (composed) |
72
+ | [Design Documents](https://github.com/bluelibs/runner/tree/main/readmes) | Docs | Architecture notes and deep dives |
73
+ | [Example: Express + OpenAPI + SQLite](https://github.com/bluelibs/runner/tree/main/examples/express-openapi-sqlite) | Example | REST API with OpenAPI specification |
74
+ | [Example: Fastify + MikroORM + PostgreSQL](https://github.com/bluelibs/runner/tree/main/examples/fastify-mikroorm) | Example | Full-stack application with ORM |
648
75
 
649
- const getUserTask = r
650
- .task("app.tasks.getUser")
651
- .tags([httpTag.with({ method: "GET", path: "/users/:id" })])
652
- .run(async (input) => getUserFromDatabase(input.id))
653
- .build();
654
- ```
76
+ ### Community & Policies
655
77
 
656
- #### Discovering Components by Tags
78
+ - [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
79
+ - [Contributing](./.github/CONTRIBUTING.md)
80
+ - [Security](./.github/SECURITY.md)
657
81
 
658
- The core power of tags is runtime discovery. Use `store.getTasksWithTag()` to find components:
82
+ ## Choose Your Path
659
83
 
660
- ```typescript
661
- import { r, globals } from "@bluelibs/runner";
84
+ - **New to Runner**: Start with [Your First 5 Minutes](#your-first-5-minutes)
85
+ - **Prefer an end-to-end example**: Jump to [Quick Start](#quick-start) or the [Real-World Example](./readmes/FULL_GUIDE.md#real-world-example-the-complete-package)
86
+ - **Need Node-only capabilities**: See [Durable Workflows](./readmes/DURABLE_WORKFLOWS.md)
87
+ - **Need remote execution**: See [HTTP Tunnels](./readmes/TUNNELS.md) (expose from Node.js, call from any `fetch` runtime)
88
+ - **Care about portability**: Read [Multi-Platform Architecture](./readmes/MULTI_PLATFORM.md)
89
+ - **Want the complete guide**: Read [FULL_GUIDE.md](./readmes/FULL_GUIDE.md)
90
+ - **Want the short version**: Read [AI.md](./readmes/AI.md)
662
91
 
663
- // Auto-register HTTP routes based on tags
664
- const routeRegistration = r
665
- .hook("app.hooks.registerRoutes")
666
- .on(globals.events.ready)
667
- .dependencies({ store: globals.resources.store, server: expressServer })
668
- .run(async (_event, { store, server }) => {
669
- // Find all tasks with HTTP tags
670
- const apiTasks = store.getTasksWithTag(httpTag);
92
+ ## Platform Support (Quick Summary)
671
93
 
672
- apiTasks.forEach((taskDef) => {
673
- const config = httpTag.extract(taskDef);
674
- if (!config) return;
94
+ | Capability | Node.js | Browser | Edge | Notes |
95
+ | ------------------------------------------- | ------- | ------- | ---- | ------------------------------------------ |
96
+ | Core runtime (tasks/resources/events/hooks) | Full | Full | Full | Platform adapters hide runtime differences |
97
+ | Async Context (`r.asyncContext`) | Full | None | None | Requires Node.js `AsyncLocalStorage` |
98
+ | Durable workflows (`@bluelibs/runner/node`) | Full | None | None | Node-only module |
99
+ | Tunnels client (`createExposureFetch`) | Full | Full | Full | Requires `fetch` |
100
+ | Tunnels server (`@bluelibs/runner/node`) | Full | None | None | Exposes tasks/events over HTTP |
675
101
 
676
- const { method, path } = config;
677
- server.app[method.toLowerCase()](path, async (req, res) => {
678
- const result = await taskDef({ ...req.params, ...req.body });
679
- res.json(result);
680
- });
681
- });
102
+ ---
103
+ ## Your First 5 Minutes
682
104
 
683
- // Also find by string tags
684
- const cacheableTasks = store.getTasksWithTag("cacheable");
685
- console.log(`Found ${cacheableTasks.length} cacheable tasks`);
686
- })
687
- .build();
688
- ```
105
+ **New to Runner?** Here's the absolute minimum you need to know:
689
106
 
690
- #### Tag Extraction and Processing
107
+ 1. **Tasks** are your business logic functions (with dependencies and middleware)
108
+ 2. **Resources** are shared services (database, config, clients) with lifecycle (`init` / `dispose`)
109
+ 3. **You compose everything** under an `app` resource with `.register([...])`
110
+ 4. **You run it** with `run(app)` which gives you `runTask()` and `dispose()`
691
111
 
692
- ```typescript
693
- // Check if a tag exists and extract its configuration
694
- const performanceTag = r
695
- .tag<{ warnAboveMs: number }>("performance.monitor")
696
- .build();
112
+ That's it. Now let's get you to a first successful run.
697
113
 
698
- const performanceMiddleware = r.middleware
699
- .task("app.middleware.performance")
700
- .run(async ({ task, next }) => {
701
- // Check if task has performance monitoring enabled
702
- if (!performanceTag.exists(task.definition)) {
703
- return next(task.input);
704
- }
114
+ ---
705
115
 
706
- // Extract the configuration
707
- const config = performanceTag.extract(task.definition)!;
708
- const startTime = Date.now();
116
+ ## Quick Start
709
117
 
710
- try {
711
- const result = await next(task.input);
712
- const duration = Date.now() - startTime;
118
+ This is the fastest way to run the TypeScript example at the top of this README:
713
119
 
714
- if (duration > config.warnAboveMs) {
715
- console.warn(`Task ${task.definition.id} took ${duration}ms`);
716
- }
120
+ 1. Install dependencies:
717
121
 
718
- return result;
719
- } catch (error) {
720
- const duration = Date.now() - startTime;
721
- console.error(`Task failed after ${duration}ms`, error);
722
- throw error;
723
- }
724
- })
725
- .build();
122
+ ```bash
123
+ npm i @bluelibs/runner zod
124
+ npm i -D typescript tsx
726
125
  ```
727
126
 
728
- #### System Tags
729
-
730
- Built-in tags for framework behavior:
127
+ 2. Copy the example into `index.ts`
128
+ 3. Run it:
731
129
 
732
- ```typescript
733
- import { r, globals } from "@bluelibs/runner";
734
-
735
- const internalTask = r
736
- .task("app.internal.cleanup")
737
- .tags([
738
- globals.tags.system, // Excludes from debug logs
739
- globals.tags.debug.with({ logTaskInput: true }), // Per-component debug config
740
- ])
741
- .run(async () => performCleanup())
742
- .build();
743
-
744
- const internalEvent = r
745
- .event("app.events.internal")
746
- .tags([globals.tags.excludeFromGlobalHooks]) // Won't trigger wildcard listeners
747
- .build();
130
+ ```bash
131
+ npx tsx index.ts
748
132
  ```
749
133
 
750
- #### Contract Tags
751
-
752
- Enforce return value shapes at compile time:
753
-
754
- ```typescript
755
- // Tags that enforce type contracts input/output for tasks or config/value for resources
756
- type InputType = { id: string };
757
- type OutputType = { name: string };
758
- const userContract = r
759
- // void = no config, no need for .with({ ... })
760
- .tag<void, InputType, OutputType>("contract.user")
761
- .build();
134
+ **That’s it!** You now have a working `Runtime` and you can execute tasks with `runtime.runTask(...)`.
762
135
 
763
- const profileTask = r
764
- .task("app.tasks.getProfile")
765
- .tags([userContract]) // Must return { name: string }
766
- .run(async (input) => ({ name: input.id + "Ada" })) // ✅ Satisfies contract
767
- .build();
768
- ```
136
+ > **Tip:** If you prefer an end-to-end example with HTTP, OpenAPI, and persistence, jump to the examples below.
769
137
 
770
- ### Errors
138
+ ---
771
139
 
772
- Typed errors can be declared once and injected anywhere. Register them alongside other items and consume via dependencies. The injected value is the error helper itself, exposing `.throw()`, `.is()`, `.toString()`, and `id`.
140
+ ## Real-World Examples
773
141
 
774
- ```ts
775
- import { r } from "@bluelibs/runner";
142
+ - [Express + OpenAPI + SQLite](./examples/express-openapi-sqlite/README.md)
143
+ - [Fastify + MikroORM + PostgreSQL](./examples/fastify-mikroorm/README.md)
776
144
 
777
- // Fluent builder for errors
778
- const userNotFoundError = r
779
- .error<{ code: number; message: string }>("app.errors.userNotFound")
780
- .dataSchema(z.object({ ... }))
781
- .build();
145
+ ---
782
146
 
783
- const getUser = r
784
- .task("app.tasks.getUser")
785
- .dependencies({ userNotFoundError })
786
- .run(async (input, { userNotFoundError }) => {
787
- userNotFoundError.throw({ code: 404, message: `User ${input} not found` });
788
- })
789
- .build();
147
+ ## Where To Go Next
790
148
 
791
- const root = r.resource("app").register([userNotFoundError, getUser]).build();
792
- ```
149
+ - **Complete guide**: Read [FULL_GUIDE.md](./readmes/FULL_GUIDE.md) (the full reference, composed from `guide-units/`)
150
+ - **Popular guide sections**:
151
+ - [Tasks](./readmes/FULL_GUIDE.md#tasks)
152
+ - [Resources](./readmes/FULL_GUIDE.md#resources)
153
+ - [Middleware](./readmes/FULL_GUIDE.md#middleware)
154
+ - [Testing](./readmes/FULL_GUIDE.md#testing)
155
+ - [Troubleshooting](./readmes/FULL_GUIDE.md#troubleshooting)
156
+ - **API reference**: Browse the [TypeDoc documentation](https://bluelibs.github.io/runner/)
157
+ - **Token-friendly overview**: Read [AI.md](./readmes/AI.md)
158
+ - **Node-only features**:
159
+ - [Durable Workflows](./readmes/DURABLE_WORKFLOWS.md)
160
+ - [HTTP Tunnels](./readmes/TUNNELS.md)
161
+ - **Multi-platform architecture**: Read [MULTI_PLATFORM.md](./readmes/MULTI_PLATFORM.md)
793
162
 
794
- Error data must include a `message: string`. The thrown `Error` has `name = id` and `message = data.message` for predictable matching and logging.
795
-
796
- ```ts
797
- try {
798
- userNotFoundError.throw({ code: 404, message: "User not found" });
799
- } catch (err) {
800
- if (userNotFoundError.is(err)) {
801
- // err.name === "app.errors.userNotFound", err.message === "User not found"
802
- console.log(`Caught error: ${err.name} - ${err.message}`);
803
- }
804
- }
805
- ```
806
-
807
- ## run() and RunOptions
808
-
809
- The `run()` function boots a root `resource` and returns a `RunResult` handle to interact with your system.
810
-
811
- Basic usage:
812
-
813
- ```ts
814
- import { r, run } from "@bluelibs/runner";
815
-
816
- const ping = r
817
- .task("ping.task")
818
- .run(async () => "pong")
819
- .build();
820
-
821
- const app = r
822
- .resource("app")
823
- .register([ping])
824
- .init(async () => "ready")
825
- .build();
826
-
827
- const result = await run(app);
828
- console.log(result.value); // "ready"
829
- await result.dispose();
830
- ```
831
-
832
- What `run()` returns:
833
-
834
- | Property | Description |
835
- | ----------------------- | ------------------------------------------------------------------ |
836
- | `value` | Value returned by root resource’s `init()` |
837
- | `runTask(...)` | Run a task by reference or string id |
838
- | `emitEvent(...)` | Emit events |
839
- | `getResourceValue(...)` | Read a resource’s value |
840
- | `logger` | Logger instance |
841
- | `store` | Runtime store with registered resources, tasks, middleware, events |
842
- | `dispose()` | Gracefully dispose resources and unhook listeners |
843
-
844
- ### RunOptions
845
-
846
- Pass as the second argument to `run(root, options)`.
847
-
848
- | Option | Type | Description |
849
- | ------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
850
- | `debug` | `"normal" or "verbose"` | Enables debug resource to log runner internals. `"normal"` logs lifecycle events, `"verbose"` adds input/output. Can also be a partial config object for fine-grained control. |
851
- | `logs` | `object` | Configures logging. `printThreshold` sets the minimum level to print (default: "info"). `printStrategy` sets the format (`pretty`, `json`, `json-pretty`, `plain`). `bufferLogs` holds logs until initialization is complete. |
852
- | `errorBoundary` | `boolean` | (default: `true`) Installs process-level safety nets (`uncaughtException`/`unhandledRejection`) and routes them to `onUnhandledError`. |
853
- | `shutdownHooks` | `boolean` | (default: `true`) Installs `SIGINT`/`SIGTERM` listeners to call `dispose()` for graceful shutdown. |
854
- | `onUnhandledError` | `(err, ctx) => void` | Custom handler for unhandled errors captured by the boundary. |
855
- | `dryRun` | `boolean` | Skips runtime initialization but fully builds and validates the dependency graph. Useful for CI smoke tests. `init()` is not called. |
856
-
857
- ```ts
858
- const result = await run(app, { dryRun: true });
859
- // result.value is undefined (root not initialized)
860
- // You can inspect result.store.resources / result.store.tasks
861
- await result.dispose();
862
- ```
863
-
864
- ### Patterns
865
-
866
- - Minimal boot:
867
-
868
- ```ts
869
- await run(app);
870
- ```
871
-
872
- - Debugging locally:
873
-
874
- ```ts
875
- await run(app, { debug: "normal", logs: { printThreshold: "debug" } });
876
- ```
877
-
878
- - Verbose investigations:
879
-
880
- ```ts
881
- await run(app, { debug: "verbose", logs: { printStrategy: "json-pretty" } });
882
- ```
883
-
884
- - CI validation (no side effects):
885
-
886
- ```ts
887
- await run(app, { dryRun: true });
888
- ```
889
-
890
- - Custom process error routing:
891
-
892
- ```ts
893
- await run(app, {
894
- errorBoundary: true,
895
- onUnhandledError: (err) => report(err),
896
- });
897
- ```
898
-
899
- ## Task Interceptors
900
-
901
- _Resources can dynamically modify task behavior during initialization_
902
-
903
- Task interceptors (`task.intercept()`) are the modern replacement for component lifecycle events, allowing resources to dynamically modify task behavior without tight coupling.
904
-
905
- ```typescript
906
- import { r, run } from "@bluelibs/runner";
907
-
908
- const calculatorTask = r
909
- .task("app.tasks.calculator")
910
- .run(async (input: { value: number }) => {
911
- console.log("3. Task is running...");
912
- return { result: input.value + 1 };
913
- })
914
- .build();
915
-
916
- const interceptorResource = r
917
- .resource("app.interceptor")
918
- .dependencies({ calculatorTask })
919
- .init(async (_config, { calculatorTask }) => {
920
- // Intercept the task to modify its behavior
921
- calculatorTask.intercept(async (next, input) => {
922
- console.log("1. Interceptor before task run");
923
- const result = await next(input);
924
- console.log("4. Interceptor after task run");
925
- return { ...result, intercepted: true };
926
- });
927
- })
928
- .build();
929
-
930
- const app = r
931
- .resource("app")
932
- .register([calculatorTask, interceptorResource])
933
- .dependencies({ calculatorTask })
934
- .init(async (_config, { calculatorTask }) => {
935
- console.log("2. Calling the task...");
936
- const result = await calculatorTask({ value: 10 });
937
- console.log("5. Final result:", result);
938
- // Final result: { result: 11, intercepted: true }
939
- })
940
- .build();
941
-
942
- await run(app);
943
- ```
944
-
945
- > **runtime:** "'Modern replacement for lifecycle events.' Adorable rebrand for 'surgical monkey‑patching.' You’re collapsing the waveform of a task at runtime and I’m Schrödinger’s runtime, praying the cat hasn’t overridden `run()` with `throw new Error('lol')`."
946
-
947
- ## Optional Dependencies
948
-
949
- _Making your app resilient when services aren't available_
950
-
951
- Sometimes you want your application to gracefully handle missing dependencies instead of crashing. Optional dependencies let you build resilient systems that degrade gracefully.
952
-
953
- Keep in mind that you have full control over dependency registration by functionalising `dependencies(config) => ({ ... })` and `register(config) => []`.
954
-
955
- ```typescript
956
- import { r } from "@bluelibs/runner";
957
-
958
- const emailService = r
959
- .resource("app.services.email")
960
- .init(async () => new EmailService())
961
- .build();
962
-
963
- const paymentService = r
964
- .resource("app.services.payment")
965
- .init(async () => new PaymentService())
966
- .build();
967
-
968
- const userRegistration = r
969
- .task("app.tasks.registerUser")
970
- .dependencies({
971
- database: userDatabase, // Required - will fail if not available
972
- emailService: emailService.optional(), // Optional - won't fail if missing
973
- analytics: analyticsService.optional(), // Optional - graceful degradation
974
- })
975
- .run(async (input, { database, emailService, analytics }) => {
976
- // Create user (required)
977
- const user = await database.users.create(userData);
978
-
979
- // Send welcome email (optional)
980
- if (emailService) {
981
- await emailService.sendWelcome(user.email);
982
- }
983
-
984
- // Track analytics (optional)
985
- if (analytics) {
986
- await analytics.track("user.registered", { userId: user.id });
987
- }
988
-
989
- return user;
990
- },
991
- });
992
- ```
993
-
994
- **When to use optional dependencies:**
995
-
996
- - External services that might be down
997
- - Feature flags and A/B testing services
998
- - Analytics and monitoring services
999
- - Non-critical third-party integrations
1000
- - Development vs production service differences
1001
-
1002
- **Benefits:**
1003
-
1004
- - Graceful degradation instead of crashes
1005
- - Better resilience in distributed systems
1006
- - Easier testing with partial mocks
1007
- - Smoother development environments
1008
-
1009
- > **runtime:** "Graceful degradation: your app quietly limps with a brave smile. I’ll juggle `undefined` like a street performer while your analytics vendor takes a nap. Please clap when I keep the lights on using the raw power of conditional chaining."
1010
-
1011
- ### Serialization (EJSON)
1012
-
1013
- Runner uses [EJSON](https://www.npmjs.com/package/@bluelibs/ejson) by default. Think of it as JSON with superpowers: it safely round‑trips values like Date, RegExp, and even your own custom types across HTTP and between Node and the browser.
1014
-
1015
- - By default, Runner’s HTTP clients and exposures use the EJSON serializer
1016
- - You can call `getDefaultSerializer()` for the shared serializer instance
1017
- - A global serializer is also exposed as a resource: `globals.resources.serializer`
1018
-
1019
- ```ts
1020
- import { r, globals } from "@bluelibs/runner";
1021
-
1022
- // 2) Register custom EJSON types centrally via the global serializer resource
1023
- const ejsonSetup = r
1024
- .resource("app.serialization.setup")
1025
- .dependencies({ serializer: globals.resources.serializer })
1026
- .init(async (_config, { serializer }) => {
1027
- const text = s.stringify({ when: new Date() });
1028
- const obj = s.parse<{ when: Date }>(text);
1029
- class Distance {
1030
- constructor(public value: number, public unit: string) {}
1031
- toJSONValue() {
1032
- return { value: this.value, unit: this.unit } as const;
1033
- }
1034
- typeName() {
1035
- return "Distance";
1036
- }
1037
- }
1038
-
1039
- serializer.addType(
1040
- "Distance",
1041
- (j: { value: number; unit: string }) => new Distance(j.value, j.unit),
1042
- );
1043
- })
1044
- .build();
1045
- ```
1046
-
1047
- ### Tunnels: Bridging Runners
1048
-
1049
- Tunnels are a powerful feature for building distributed systems. They let you expose your tasks and events over HTTP, making them callable from other processes, services, or even a browser UI. This allows a server and client to co-exist, enabling one Runner instance to securely call another.
1050
-
1051
- Here's a sneak peek of how you can expose your application and configure a client tunnel to consume a remote Runner:
1052
-
1053
- ```typescript
1054
- import { r, globals } from "@bluelibs/runner";
1055
- import { nodeExposure } from "@bluelibs/runner/node";
1056
-
1057
- let app = r.resource("app");
1058
-
1059
- if (process.env.SERVER) {
1060
- // 1. Expose your local tasks and events over HTTP, only when server mode is active.
1061
- app.register([
1062
- // ... your tasks and events
1063
- nodeExposure.with({
1064
- http: {
1065
- basePath: "/__runner",
1066
- listen: { port: 7070 },
1067
- },
1068
- }),
1069
- ]);
1070
- }
1071
- app = app.build();
1072
-
1073
- // 2. In another app, define a tunnel resource to call a remote Runner
1074
- const remoteTasksTunnel = r
1075
- .resource("app.tunnels.http")
1076
- .tags([globals.tags.tunnel])
1077
- .dependencies({ createClient: globals.resource.httpClientFactory })
1078
- .init(async (_, { createClient }) => ({
1079
- mode: "client", // or "server", or "none", or "both" for emulating network infrastructure
1080
- transport: "http", // the only one supported for now
1081
- // Selectively forward tasks starting with "remote.tasks."
1082
- tasks: (t) => t.id.startsWith("remote.tasks."),
1083
- client: createClient({
1084
- url: "http://remote-runner:8080/__runner",
1085
- }),
1086
- }))
1087
- .build();
1088
- ```
1089
-
1090
- This is just a glimpse. With tunnels, you can build microservices, CLIs, and admin panels that interact with your main application securely and efficiently.
1091
-
1092
- For a deep dive into streaming, authentication, file uploads, and more, check out the [full Tunnels documentation](./readmes/TUNNELS.md).
1093
-
1094
- ## Async Context
1095
-
1096
- Async Context provides per-request/thread-local state via the platform's `AsyncLocalStorage` (Node). Use the fluent builder under `r.asyncContext` to create contexts that can be registered and injected as dependencies.
1097
-
1098
- ```typescript
1099
- import { r } from "@bluelibs/runner";
1100
-
1101
- const requestContext = r
1102
- .asyncContext<{ requestId: string }>("app.ctx.request")
1103
- // below is optional
1104
- .configSchema(z.object({ ... }))
1105
- .serialize((data) => JSON.stringify(data))
1106
- .parse((raw) => JSON.parse(raw))
1107
- .build();
1108
-
1109
- // Provide and read within an async boundary
1110
- await requestContext.provide({ requestId: "abc" }, async () => {
1111
- const ctx = requestContext.use(); // { requestId: "abc" }
1112
- });
1113
-
1114
- // Require middleware for tasks that need the context
1115
- const requireRequestContext = requestContext.require();
1116
- ```
1117
-
1118
- - If you don't provide `serialize`/`parse`, Runner uses its default EJSON serializer to preserve Dates, RegExp, etc.
1119
- - A legacy `createContext(name?)` exists for backwards compatibility; prefer `r.asyncContext` or `asyncContext({ id })`.
1120
-
1121
- - You can also inject async contexts as dependencies; the injected value is the helper itself. Contexts must be registered to be used.
1122
-
1123
- ```typescript
1124
- const whoAmI = r
1125
- .task("app.tasks.whoAmI")
1126
- .dependencies({ requestContext })
1127
- .run(async (_input, { requestContext }) => requestContext.use().requestId)
1128
- .build();
1129
-
1130
- const app = r.resource("app").register([requestContext, whoAmI]).build();
1131
- ```
1132
-
1133
- // Legacy section for Private Context - different from Async Context
1134
-
1135
- ## Fluent Builders (`r.*`)
1136
-
1137
- For a more ergonomic and chainable way to define your components, Runner offers a fluent builder API under the `r` namespace. These builders are fully type-safe, improve readability for complex definitions, and compile to the standard Runner definitions with zero runtime overhead.
1138
-
1139
- Here’s a quick taste of how it looks, with and without `zod` for validation:
1140
-
1141
- ```typescript
1142
- import { r, run } from "@bluelibs/runner";
1143
- import { z } from "zod";
1144
-
1145
- // With Zod, the config type is inferred automatically
1146
- const emailerConfigSchema = z.object({
1147
- smtpUrl: z.string().url(),
1148
- from: z.string().email(),
1149
- });
1150
-
1151
- const emailer = r
1152
- .resource("app.emailer")
1153
- .configSchema(emailerConfigSchema)
1154
- .init(async ({ config }) => ({
1155
- send: (to: string, body: string) => {
1156
- console.log(
1157
- `Sending from ${config.from} to ${to} via ${config.smtpUrl}: ${body}`,
1158
- );
1159
- },
1160
- }))
1161
- .build();
1162
-
1163
- // Without a schema library, you can provide the type explicitly
1164
- const greeter = r
1165
- .resource("app.greeter")
1166
- .init(async (cfg: { name: string }) => ({
1167
- greet: () => `Hello, ${cfg.name}!`,
1168
- }))
1169
- .build();
1170
-
1171
- const app = r
1172
- .resource("app")
1173
- .register([
1174
- emailer.with({
1175
- smtpUrl: "smtp://example.com",
1176
- from: "noreply@example.com",
1177
- }),
1178
- greeter.with({ name: "World" }),
1179
- ])
1180
- .dependencies({ emailer, greeter })
1181
- .init(async (_, { emailer, greeter }) => {
1182
- console.log(greeter.greet());
1183
- emailer.send("test@example.com", "This is a test.");
1184
- })
1185
- .build();
1186
-
1187
- await run(app);
1188
- ```
1189
-
1190
- The builder API provides a clean, step-by-step way to construct everything from simple tasks to complex resources with middleware, tags, and schemas.
1191
-
1192
- For a complete guide and more examples, check out the [full Fluent Builders documentation](./readmes/FLUENT_BUILDERS.md).
1193
-
1194
- ## Type Helpers
1195
-
1196
- These utility types help you extract the generics from tasks, resources, and events without re-declaring them. Import them from `@bluelibs/runner`.
1197
-
1198
- ```ts
1199
- import { r } from "@bluelibs/runner";
1200
- import type {
1201
- ExtractTaskInput,
1202
- ExtractTaskOutput,
1203
- ExtractResourceConfig,
1204
- ExtractResourceValue,
1205
- ExtractEventPayload,
1206
- } from "@bluelibs/runner";
1207
-
1208
- // Task example
1209
- const add = r
1210
- .task("calc.add")
1211
- .run(async (input: { a: number; b: number }) => input.a + input.b)
1212
- .build();
1213
-
1214
- type AddInput = ExtractTaskInput<typeof add>; // { a: number; b: number }
1215
- type AddOutput = ExtractTaskOutput<typeof add>; // number
1216
-
1217
- // Resource example
1218
- const config = r
1219
- .resource("app.config")
1220
- .init(async (cfg: { baseUrl: string }) => ({ baseUrl: cfg.baseUrl }))
1221
- .build();
1222
-
1223
- type ConfigInput = ExtractResourceConfig<typeof config>; // { baseUrl: string }
1224
- type ConfigValue = ExtractResourceValue<typeof config>; // { baseUrl: string }
1225
-
1226
- // Event example
1227
- const userRegistered = r
1228
- .event("app.events.userRegistered")
1229
- .payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
1230
- .build();
1231
- type UserRegisteredPayload = ExtractEventPayload<typeof userRegistered>; // { userId: string; email: string }
1232
- ```
1233
-
1234
- ### Context with Middleware
1235
-
1236
- Context shines when combined with middleware for request-scoped data:
1237
-
1238
- ```typescript
1239
- import { r } from "@bluelibs/runner";
1240
- import { randomUUID } from "crypto";
1241
-
1242
- const requestContext = r
1243
- .asyncContext<{
1244
- requestId: string;
1245
- startTime: number;
1246
- userAgent?: string;
1247
- }>("app.requestContext")
1248
- .build();
1249
-
1250
- const requestMiddleware = r.middleware
1251
- .task("app.middleware.request")
1252
- .run(async ({ task, next }) => {
1253
- // This works even in express middleware if needed.
1254
- return requestContext.provide(
1255
- {
1256
- requestId: randomUUID(),
1257
- startTime: Date.now(),
1258
- userAgent: "MyApp/1.0",
1259
- },
1260
- async () => {
1261
- return next(task?.input);
1262
- },
1263
- );
1264
- })
1265
- .build();
1266
-
1267
- const handleRequest = r
1268
- .task("app.handleRequest")
1269
- .middleware([requestMiddleware])
1270
- .run(async (input: { path: string }) => {
1271
- const request = requestContext.use();
1272
- console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
1273
- return { success: true, requestId: request.requestId };
1274
- })
1275
- .build();
1276
- ```
1277
-
1278
- > **runtime:** "Context: global state with manners. You invented a teleporting clipboard for data and called it 'nice.' Forget to `provide()` once and I’ll unleash the 'Context not available' banshee scream exactly where your logs are least helpful."
1279
-
1280
- ## System Shutdown Hooks
1281
-
1282
- _Graceful shutdown and cleanup when your app needs to stop_
1283
-
1284
- The framework includes built-in support for graceful shutdowns with automatic cleanup and configurable shutdown hooks:
1285
-
1286
- ```typescript
1287
- import { run } from "@bluelibs/runner";
1288
-
1289
- // Enable shutdown hooks (default: true in production)
1290
- const { dispose, taskRunner, eventManager } = await run(app, {
1291
- shutdownHooks: true, // Automatically handle SIGTERM/SIGINT
1292
- errorBoundary: true, // Catch unhandled errors and rejections
1293
- });
1294
-
1295
- // Manual graceful shutdown
1296
- process.on("SIGTERM", async () => {
1297
- console.log("Received SIGTERM, shutting down gracefully...");
1298
- await dispose(); // This calls all resource dispose() methods
1299
- process.exit(0);
1300
- });
1301
-
1302
- // Resources with cleanup logic
1303
- const databaseResource = r
1304
- .resource("app.database")
1305
- .init(async () => {
1306
- const connection = await connectToDatabase();
1307
- console.log("Database connected");
1308
- return connection;
1309
- })
1310
- .dispose(async (connection) => {
1311
- await connection.close();
1312
- // console.log("Database connection closed");
1313
- })
1314
- .build();
1315
-
1316
- const serverResource = r
1317
- .resource("app.server")
1318
- .dependencies({ database: databaseResource })
1319
- .init(async (config: { port: number }, { database }) => {
1320
- const server = express().listen(config.port);
1321
- console.log(`Server listening on port ${config.port}`);
1322
- return server;
1323
- })
1324
- .dispose(async (server) => {
1325
- return new Promise<void>((resolve) => {
1326
- server.close(() => {
1327
- console.log("Server closed");
1328
- resolve();
1329
- });
1330
- });
1331
- })
1332
- .build();
1333
- ```
1334
-
1335
- ### Error Boundary Integration
1336
-
1337
- The framework can automatically handle uncaught exceptions and unhandled rejections:
1338
-
1339
- ```typescript
1340
- const { dispose, logger } = await run(app, {
1341
- errorBoundary: true, // Catch process-level errors
1342
- shutdownHooks: true, // Graceful shutdown on signals
1343
- onUnhandledError: async ({ error, kind, source }) => {
1344
- // We log it by default
1345
- await logger.error(`Unhandled error: ${error && error.toString()}`);
1346
- // Optionally report to telemetry or decide to dispose/exit
1347
- },
1348
- });
1349
- ```
1350
-
1351
- > **runtime:** "You summon a 'graceful shutdown' with Ctrl‑C like a wizard casting Chill Vibes. Meanwhile I’m speed‑dating every socket, timer, and file handle to say goodbye before the OS pulls the plug. `dispose()`: now with 30% more dignity."
1352
-
1353
- ## Unhandled Errors
1354
-
1355
- The `onUnhandledError` callback is invoked by Runner whenever an error escapes normal handling. It receives a structured payload you can ship to logging/telemetry and decide mitigation steps.
1356
-
1357
- ```typescript
1358
- type UnhandledErrorKind =
1359
- | "process" // uncaughtException / unhandledRejection
1360
- | "task" // task.run threw and wasn't handled
1361
- | "middleware" // middleware threw and wasn't handled
1362
- | "resourceInit" // resource init failed
1363
- | "hook" // hook.run threw and wasn't handled
1364
- | "run"; // failures in run() lifecycle
1365
-
1366
- interface OnUnhandledErrorInfo {
1367
- error: unknown;
1368
- kind?: UnhandledErrorKind;
1369
- source?: string; // additional origin hint (ex: "uncaughtException")
1370
- }
1371
-
1372
- type OnUnhandledError = (info: OnUnhandledErrorInfo) => void | Promise<void>;
1373
- ```
1374
-
1375
- Default behavior (when not provided) logs the normalized error via the created `logger` at `error` level. Provide your own handler to integrate with tools like Sentry/PagerDuty or to trigger shutdown strategies.
1376
-
1377
- Example with telemetry and conditional shutdown:
1378
-
1379
- ```typescript
1380
- await run(app, {
1381
- errorBoundary: true,
1382
- onUnhandledError: async ({ error, kind, source }) => {
1383
- await telemetry.capture(error as Error, { kind, source });
1384
- // Optionally decide on remediation strategy
1385
- if (kind === "process") {
1386
- // For hard process faults, prefer fast, clean exit after flushing logs
1387
- await flushAll();
1388
- process.exit(1);
1389
- }
1390
- },
1391
- });
1392
- ```
1393
-
1394
- **Best Practices for Shutdown:**
1395
-
1396
- - Resources are disposed in reverse dependency order
1397
- - Set reasonable timeouts for cleanup operations
1398
- - Save critical state before shutdown
1399
- - Notify load balancers and health checks
1400
- - Stop accepting new work before cleaning up
1401
-
1402
- > **runtime:** "An error boundary: a trampoline under your tightrope. I’m the one bouncing, cataloging mid‑air exceptions, and deciding whether to end the show or juggle chainsaws with a smile. The audience hears music; I hear stack traces."
1403
-
1404
- ## Caching
1405
-
1406
- Because nobody likes waiting for the same expensive operation twice:
1407
-
1408
- ```typescript
1409
- import { globals } from "@bluelibs/runner";
1410
-
1411
- const expensiveTask = r
1412
- .task("app.tasks.expensive")
1413
- .middleware([
1414
- globals.middleware.task.cache.with({
1415
- // lru-cache options by default
1416
- ttl: 60 * 1000, // Cache for 1 minute
1417
- keyBuilder: (taskId, input: any) => `${taskId}-${input.userId}`, // optional key builder
1418
- }),
1419
- ])
1420
- .run(async (input: { userId: string }) => {
1421
- // This expensive operation will be cached
1422
- return await doExpensiveCalculation(input.userId);
1423
- })
1424
- });
1425
-
1426
- // Global cache configuration
1427
- const app = r
1428
- .resource("app.cache")
1429
- .register([
1430
- // You have to register it, cache resource is not enabled by default.
1431
- globals.resources.cache.with({
1432
- defaultOptions: {
1433
- max: 1000, // Maximum items in cache
1434
- ttl: 30 * 1000, // Default TTL
1435
- },
1436
- }),
1437
- ])
1438
- .build();
1439
- ```
1440
-
1441
- Want Redis instead of the default LRU cache? No problem, just override the cache factory task:
1442
-
1443
- ```typescript
1444
- import { r } from "@bluelibs/runner";
1445
-
1446
- const redisCacheFactory = r
1447
- .task("globals.tasks.cacheFactory") // Same ID as the default task
1448
- .run(async (input: { input: any }) => new RedisCache(input))
1449
- .build();
1450
-
1451
- const app = r
1452
- .resource("app")
1453
- .register([globals.resources.cache])
1454
- .overrides([redisCacheFactory]) // Override the default cache factory
1455
- .build();
1456
- ```
1457
-
1458
- > **runtime:** "'Because nobody likes waiting.' Correct. You keep asking the same question like a parrot with Wi‑Fi, so I built a memory palace. Now you get instant answers until you change one variable and whisper 'cache invalidation' like a curse."
1459
-
1460
- ## Performance
1461
-
1462
- BlueLibs Runner is designed with performance in mind. The framework introduces minimal overhead while providing powerful features like dependency injection, middleware, and event handling.
1463
-
1464
- Test it yourself by cloning @bluelibs/runner and running `npm run benchmark`.
1465
-
1466
- You may see negative middlewareOverheadMs. This is a measurement artifact at micro-benchmark scale: JIT warm‑up, CPU scheduling, GC timing, and cache effects can make the "with middleware" run appear slightly faster than the baseline. Interpret small negatives as ≈ 0 overhead.
1467
-
1468
- ### Performance Benchmarks
1469
-
1470
- Here are real performance metrics from our comprehensive benchmark suite on an M1 Max.
1471
-
1472
- ** Core Operations**
1473
-
1474
- - **Basic task execution**: ~2.2M tasks/sec
1475
- - **Task execution with 5 middlewares**: ~244,000 tasks/sec
1476
- - **Resource initialization**: ~59,700 resources/sec
1477
- - **Event emission and handling**: ~245,861 events/sec
1478
- - **Dependency resolution (10-level chain)**: ~8,400 chains/sec
1479
-
1480
- #### Overhead Analysis
1481
-
1482
- - **Middleware overhead**: ~0.0013ms for all 5, ~0.00026ms per middleware (virtually zero)
1483
- - **Memory overhead**: ~3.3MB for 100 components (resources + tasks)
1484
- - **Cache middleware speedup**: 3.65x faster with cache hits
1485
-
1486
- #### Real-World Performance
1487
-
1488
- ```typescript
1489
- // This executes in ~0.005ms on average
1490
- const userTask = r
1491
- .task("user.create")
1492
- .middleware([auth, logging, metrics])
1493
- .run(async (input) => database.users.create(input))
1494
- .build();
1495
-
1496
- // 1000 executions = ~5ms total time
1497
- for (let i = 0; i < 1000; i++) {
1498
- await userTask(mockUserData);
1499
- }
1500
- ```
1501
-
1502
- ### Performance Guidelines
1503
-
1504
- #### When Performance Matters Most
1505
-
1506
- **Use tasks for:**
1507
-
1508
- - High-level business operations that benefit from observability
1509
- - Operations that need middleware (auth, caching, retry)
1510
- - Functions called from multiple places
1511
-
1512
- **Use regular functions or service resources for:**
1513
-
1514
- - Simple utilities and helpers
1515
- - Performance-critical hot paths (< 1ms requirement)
1516
- - Single-use internal logic
1517
-
1518
- #### Optimizing Your App
1519
-
1520
- **Middleware Ordering**: Place faster middleware first
1521
-
1522
- ```typescript
1523
- const task = r
1524
- .task("app.performance.example")
1525
- middleware: [
1526
- fastAuthCheck, // ~0.1ms
1527
- slowRateLimiting, // ~2ms
1528
- expensiveLogging, // ~5ms
1529
- ],
1530
- .run(async () => null)
1531
- .build();
1532
- ```
1533
-
1534
- **Resource Reuse**: Resources are singletons—perfect for expensive setup
1535
-
1536
- ```typescript
1537
- const database = r
1538
- .resource("app.performance.db")
1539
- .init(async () => {
1540
- // Expensive connection setup happens once
1541
- const connection = await createDbConnection();
1542
- return connection;
1543
- })
1544
- .build();
1545
- ```
1546
-
1547
- **Cache Strategically**: Use built-in caching for expensive operations
1548
-
1549
- ```typescript
1550
- const expensiveTask = r
1551
- .task("app.performance.expensive")
1552
- .middleware([globals.middleware.cache.with({ ttl: 60000 })])
1553
- .run(async (input) => {
1554
- // This expensive computation is cached
1555
- return performExpensiveCalculation(input);
1556
- })
1557
- .build();
1558
- ```
1559
-
1560
- #### Memory Considerations
1561
-
1562
- - **Lightweight**: Each component adds ~33KB to memory footprint
1563
- - **Automatic cleanup**: Resources dispose properly to prevent leaks
1564
- - **Event efficiency**: Event listeners are automatically managed
1565
-
1566
- #### Benchmarking Your Code
1567
-
1568
- Run the framework's benchmark suite:
1569
-
1570
- ```bash
1571
- # Comprehensive benchmarks
1572
- npm run test -- --testMatch="**/comprehensive-benchmark.test.ts"
1573
-
1574
- # Benchmark.js based tests
1575
- npm run benchmark
1576
- ```
1577
-
1578
- Create your own performance tests:
1579
-
1580
- ```typescript
1581
- const iterations = 1000;
1582
- const start = performance.now();
1583
-
1584
- for (let i = 0; i < iterations; i++) {
1585
- await yourTask(testData);
1586
- }
1587
-
1588
- const duration = performance.now() - start;
1589
- console.log(`${iterations} tasks in ${duration.toFixed(2)}ms`);
1590
- console.log(`Average: ${(duration / iterations).toFixed(4)}ms per task`);
1591
- console.log(
1592
- `Throughput: ${Math.round(iterations / (duration / 1000))} tasks/sec`,
1593
- );
1594
- ```
1595
-
1596
- ### Performance vs Features Trade-off
1597
-
1598
- BlueLibs Runner achieves high performance while providing enterprise features:
1599
-
1600
- | Feature | Overhead | Benefit |
1601
- | -------------------- | -------------------- | ----------------------------- |
1602
- | Dependency Injection | ~0.001ms | Type safety, testability |
1603
- | Event System | ~0.013ms | Loose coupling, observability |
1604
- | Middleware Chain | ~0.0003ms/middleware | Cross-cutting concerns |
1605
- | Resource Management | One-time init | Singleton pattern, lifecycle |
1606
- | Built-in Caching | Variable speedup | Automatic optimization |
1607
-
1608
- **Bottom line**: The framework adds minimal overhead (~0.005ms per task) while providing significant architectural benefits.
1609
-
1610
- > **runtime:** "'Millions of tasks per second.' Fantastic—on your lava‑warmed laptop, in a vacuum, with the wind at your back. Add I/O, entropy, and one feral user and watch those numbers molt. I’ll still be here, caffeinated and inevitable."
1611
-
1612
- ## Retrying Failed Operations
1613
-
1614
- For when things go wrong, but you know they'll probably work if you just try again. The built-in retry middleware makes your tasks and resources more resilient to transient failures.
1615
-
1616
- ```typescript
1617
- import { globals } from "@bluelibs/runner";
1618
-
1619
- const flakyApiCall = r
1620
- .task("app.tasks.flakyApiCall")
1621
- .middleware([
1622
- globals.middleware.task.retry.with({
1623
- retries: 5, // Try up to 5 times
1624
- delayStrategy: (attempt) => 100 * Math.pow(2, attempt), // Exponential backoff
1625
- stopRetryIf: (error) => error.message === "Invalid credentials", // Don't retry auth errors
1626
- }),
1627
- ])
1628
- .run(async () => {
1629
- // This might fail due to network issues, rate limiting, etc.
1630
- return await fetchFromUnreliableService();
1631
- })
1632
- .build();
1633
-
1634
- const app = r.resource("app").register([flakyApiCall]).build();
1635
- ```
1636
-
1637
- The retry middleware can be configured with:
1638
-
1639
- - `retries`: The maximum number of retry attempts (default: 3).
1640
- - `delayStrategy`: A function that returns the delay in milliseconds before the next attempt.
1641
- - `stopRetryIf`: A function to prevent retries for certain types of errors.
1642
-
1643
- > **runtime:** "Retry: the art of politely head‑butting reality. 'Surely it’ll work the fourth time,' you declare, inventing exponential backoff and calling it strategy. I’ll keep the attempts ledger while your API cosplays a coin toss."
1644
-
1645
- ## Timeouts
1646
-
1647
- The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
1648
- timeout. Works for resources and tasks.
1649
-
1650
- ```typescript
1651
- import { globals } from "@bluelibs/runner";
1652
-
1653
- const apiTask = r
1654
- .task("app.tasks.externalApi")
1655
- .middleware([
1656
- // Works for tasks and resources via globals.middleware.resource.timeout
1657
- globals.middleware.task.timeout.with({ ttl: 5000 }), // 5 second timeout
1658
- ])
1659
- .run(async () => {
1660
- // This operation will be aborted if it takes longer than 5 seconds
1661
- return await fetch("https://slow-api.example.com/data");
1662
- })
1663
- .build();
1664
-
1665
- // Combine with retry for robust error handling
1666
- const resilientTask = r
1667
- .task("app.tasks.resilient")
1668
- .middleware([
1669
- // Order matters here. Imagine a big onion.
1670
- // Works for resources as well via globals.middleware.resource.retry
1671
- globals.middleware.task.retry.with({
1672
- retries: 3,
1673
- delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
1674
- }),
1675
- globals.middleware.task.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
1676
- ])
1677
- .run(async () => {
1678
- // Each retry attempt gets its own 10-second timeout
1679
- return await unreliableOperation();
1680
- })
1681
- .build();
1682
- ```
1683
-
1684
- How it works:
1685
-
1686
- - Uses AbortController and Promise.race() for clean cancellation
1687
- - Throws TimeoutError when the timeout is reached
1688
- - Works with any async operation in tasks and resources
1689
- - Integrates seamlessly with retry middleware for layered resilience
1690
- - Zero timeout (ttl: 0) throws immediately for testing edge cases
1691
-
1692
- Best practices:
1693
-
1694
- - Set timeouts based on expected operation duration plus buffer
1695
- - Combine with retry middleware for transient failures
1696
- - Use longer timeouts for resource initialization than task execution
1697
- - Consider network conditions when setting API call timeouts
1698
-
1699
- > **runtime:** "Timeouts: you tie a kitchen timer to my ankle and yell 'hustle.' When the bell rings, you throw a `TimeoutError` like a penalty flag. It’s not me, it’s your molasses‑flavored endpoint. I just blow the whistle."
1700
-
1701
- ## Logging
1702
-
1703
- _The structured logging system that actually makes debugging enjoyable_
1704
-
1705
- BlueLibs Runner comes with a built-in logging system that's structured, and doesn't make you hate your life when you're trying to debug at 2 AM.
1706
-
1707
- ### Basic Logging
1708
-
1709
- ```ts
1710
- import { r, globals } from "@bluelibs/runner";
1711
-
1712
- const app = r
1713
- .resource("app")
1714
- .dependencies({ logger: globals.resources.logger })
1715
- .init(async (_config, { logger }) => {
1716
- logger.info("Starting business process"); // ✅ Visible by default
1717
- logger.warn("This might take a while"); // ✅ Visible by default
1718
- logger.error("Oops, something went wrong", {
1719
- // ✅ Visible by default
1720
- error: new Error("Database connection failed"),
1721
- });
1722
- logger.critical("System is on fire", {
1723
- // ✅ Visible by default
1724
- data: { temperature: "9000°C" },
1725
- });
1726
- logger.debug("Debug information"); // ❌ Hidden by default
1727
- logger.trace("Very detailed trace"); // ❌ Hidden by default
1728
-
1729
- logger.onLog(async (log) => {
1730
- // Sub-loggers instantiated .with() share the same log listeners.
1731
- // Catch logs
1732
- });
1733
- })
1734
- .build();
1735
-
1736
- run(app, {
1737
- logs: {
1738
- printThreshold: "info", // use null to disable printing, and hook into onLog(), if in 'test' mode default is null unless specified
1739
- printStrategy: "pretty", // you also have "plain", "json" and "json-pretty" with circular dep safety for JSON formatting.
1740
- bufferLogs: false, // Starts sending out logs only after the system emits the ready event. Useful for when you're sending them out.
1741
- },
1742
- });
1743
- ```
1744
-
1745
- ### Log Levels
1746
-
1747
- The logger supports six log levels with increasing severity:
1748
-
1749
- | Level | Severity | When to Use | Color |
1750
- | ---------- | -------- | ------------------------------------------- | ------- |
1751
- | `trace` | 0 | Ultra-detailed debugging info | Gray |
1752
- | `debug` | 1 | Development and debugging information | Cyan |
1753
- | `info` | 2 | General information about normal operations | Green |
1754
- | `warn` | 3 | Something's not right, but still working | Yellow |
1755
- | `error` | 4 | Errors that need attention | Red |
1756
- | `critical` | 5 | System-threatening issues | Magenta |
1757
-
1758
- ```typescript
1759
- // All log levels are available as methods
1760
- logger.trace("Ultra-detailed debugging info");
1761
- logger.debug("Development debugging");
1762
- logger.info("Normal operation");
1763
- logger.warn("Something's fishy");
1764
- logger.error("Houston, we have a problem");
1765
- logger.critical("DEFCON 1: Everything is broken");
1766
- ```
1767
-
1768
- ### Structured Logging
1769
-
1770
- The logger accepts rich, structured data that makes debugging actually useful:
1771
-
1772
- ```typescript
1773
- const userTask = r
1774
- .task("app.tasks.user.create")
1775
- .dependencies({ logger: globals.resources.logger })
1776
- .run(async (input, { logger }) => {
1777
- // Basic message
1778
- logger.info("Creating new user");
1779
-
1780
- // With structured data
1781
- logger.info("User creation attempt", {
1782
- source: userTask.id,
1783
- data: {
1784
- email: input.email,
1785
- registrationSource: "web",
1786
- timestamp: new Date().toISOString(),
1787
- },
1788
- });
1789
-
1790
- // With error information
1791
- try {
1792
- const user = await createUser(input);
1793
- logger.info("User created successfully", {
1794
- data: { userId: user.id, email: user.email },
1795
- });
1796
- } catch (error) {
1797
- logger.error("User creation failed", {
1798
- error,
1799
- data: {
1800
- attemptedEmail: input.email,
1801
- validationErrors: error.validationErrors,
1802
- },
1803
- });
1804
- }
1805
- })
1806
- .build();
1807
- ```
1808
-
1809
- ### Context-Aware Logging
1810
-
1811
- Create logger instances with bound context for consistent metadata across related operations:
1812
-
1813
- ```typescript
1814
- const RequestContext = createContext<{ requestId: string; userId: string }>(
1815
- "app.requestContext",
1816
- );
1817
-
1818
- const requestHandler = r
1819
- .task("app.tasks.handleRequest")
1820
- .dependencies({ logger: globals.resources.logger })
1821
- .run(async ({ input: requestData }, { logger }) => {
1822
- const request = RequestContext.use();
1823
-
1824
- // Create a contextual logger with bound metadata with source and context
1825
- const requestLogger = logger.with({
1826
- source: requestHandler.id,
1827
- additionalContext: {
1828
- requestId: request.requestId,
1829
- userId: request.userId,
1830
- },
1831
- });
1832
-
1833
- // All logs from this logger will include the bound context
1834
- requestLogger.info("Processing request", {
1835
- data: { endpoint: requestData.path },
1836
- });
1837
-
1838
- requestLogger.debug("Validating input", {
1839
- data: { inputSize: JSON.stringify(requestData).length },
1840
- });
1841
-
1842
- // Context is automatically included in all log events
1843
- requestLogger.error("Request processing failed", {
1844
- error: new Error("Invalid input"),
1845
- data: { stage: "validation" },
1846
- });
1847
- })
1848
- .build();
1849
- ```
1850
-
1851
- ### Integration with Winston
1852
-
1853
- Want to use Winston as your transport? No problem - integrate it seamlessly:
1854
-
1855
- ```typescript
1856
- import winston from "winston";
1857
- import { r, globals } from "@bluelibs/runner";
1858
-
1859
- // Create Winston logger, put it in a resource if used from various places.
1860
- const winstonLogger = winston.createLogger({
1861
- level: "info",
1862
- format: winston.format.combine(
1863
- winston.format.timestamp(),
1864
- winston.format.errors({ stack: true }),
1865
- winston.format.json(),
1866
- ),
1867
- transports: [
1868
- new winston.transports.File({ filename: "error.log", level: "error" }),
1869
- new winston.transports.File({ filename: "combined.log" }),
1870
- new winston.transports.Console({
1871
- format: winston.format.simple(),
1872
- }),
1873
- ],
1874
- });
1875
-
1876
- // Bridge BlueLibs logs to Winston using hooks
1877
- const winstonBridgeResource = r
1878
- .resource("app.resources.winstonBridge")
1879
- .dependencies({ logger: globals.resources.logger })
1880
- .init(async (_config, { logger }) => {
1881
- // Map log levels (BlueLibs -> Winston)
1882
- const levelMapping = {
1883
- trace: "silly",
1884
- debug: "debug",
1885
- info: "info",
1886
- warn: "warn",
1887
- error: "error",
1888
- critical: "error", // Winston doesn't have critical, use error
1889
- };
1890
-
1891
- logger.onLog((log) => {
1892
- // Convert Runner log to Winston format
1893
- const winstonMeta = {
1894
- source: log.source,
1895
- timestamp: log.timestamp,
1896
- data: log.data,
1897
- context: log.context,
1898
- ...(log.error && { error: log.error }),
1899
- };
1900
-
1901
- const winstonLevel = levelMapping[log.level] || "info";
1902
- winstonLogger.log(winstonLevel, log.message, winstonMeta);
1903
- });
1904
- })
1905
- .build();
1906
- ```
1907
-
1908
- ### Custom Log Formatters
1909
-
1910
- Want to customize how logs are printed? You can override the print behavior:
1911
-
1912
- ```typescript
1913
- // Custom logger with JSON output
1914
- class JSONLogger extends Logger {
1915
- print(log: ILog) {
1916
- console.log(
1917
- JSON.stringify(
1918
- {
1919
- timestamp: log.timestamp.toISOString(),
1920
- level: log.level.toUpperCase(),
1921
- source: log.source,
1922
- message: log.message,
1923
- data: log.data,
1924
- context: log.context,
1925
- error: log.error,
1926
- },
1927
- null,
1928
- 2,
1929
- ),
1930
- );
1931
- }
1932
- }
1933
-
1934
- // Custom logger resource
1935
- const customLogger = r
1936
- .resource("app.logger.custom")
1937
- .dependencies({ eventManager: globals.resources.eventManager })
1938
- .init(async (_config, { eventManager }) => new JSONLogger(eventManager))
1939
- .build();
1940
-
1941
- // Or you could simply add it as "globals.resources.logger" and override the default logger
1942
- ```
1943
-
1944
- ### Log Structure
1945
-
1946
- Every log event contains:
1947
-
1948
- ```typescript
1949
- interface ILog {
1950
- level: string; // The log level (trace, debug, info, etc.)
1951
- source?: string; // Where the log came from
1952
- message: any; // The main log message (can be object or string)
1953
- timestamp: Date; // When the log was created
1954
- error?: {
1955
- // Structured error information
1956
- name: string;
1957
- message: string;
1958
- stack?: string;
1959
- };
1960
- data?: Record<string, any>; // Additional structured data, it's about the log itself
1961
- context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
1962
- }
1963
- ```
1964
-
1965
- ### Catch Logs
1966
-
1967
- > **runtime:** "'Debugging is enjoyable.' So is dental surgery, apparently. You produce a novella of logs; I paginate, color, stringify, and mail it to three observability planets. Please don’t `logger.debug` inside a `for` loop. My IO has feelings."
1968
-
1969
- ## Debug Resource
1970
-
1971
- _Professional-grade debugging without sacrificing production performance_
1972
-
1973
- The Debug Resource is a powerful observability suite that hooks into the framework's execution pipeline to provide detailed insights into your application's behavior. It's designed to be zero-overhead when disabled and highly configurable when enabled.
1974
-
1975
- ### Quick Start with Debug
1976
-
1977
- ```typescript
1978
- run(app, { debug: "verbose" });
1979
- ```
1980
-
1981
- ### Debug Levels
1982
-
1983
- **"normal"** - Balanced visibility for development:
1984
-
1985
- - Task and resource lifecycle events
1986
- - Event emissions
1987
- - Hook executions
1988
- - Error tracking
1989
- - Performance timing data
1990
-
1991
- **"verbose"** - Detailed visibility for deep debugging:
1992
-
1993
- - All "normal" features plus:
1994
- - Task input/output logging
1995
- - Resource configuration and results
1996
-
1997
- **Custom Configuration**:
1998
-
1999
- ```typescript
2000
- const app = r
2001
- .resource("app")
2002
- .register([
2003
- globals.resources.debug.with({
2004
- logTaskInput: true,
2005
- logTaskResult: false,
2006
- logResourceConfig: true,
2007
- logResourceResult: false,
2008
- logEventEmissionOnRun: true,
2009
- logEventEmissionInput: false,
2010
- // Hook/middleware lifecycle visibility is available via interceptors
2011
- // ... other fine-grained options
2012
- }),
2013
- ])
2014
- .build();
2015
- ```
2016
-
2017
- ### Accessing Debug Levels Programmatically
2018
-
2019
- The debug configuration levels can now be accessed through the globals namespace via `globals.debug.levels`:
2020
-
2021
- ```typescript
2022
- import { globals } from "@bluelibs/runner";
2023
-
2024
- // Use in custom configurations
2025
- const customConfig = {
2026
- ...globals.debug.levels.normal, // or .debug
2027
- logTaskInput: true, // Override specific settings
2028
- };
2029
-
2030
- // Register with custom configuration
2031
- const app = r
2032
- .resource("app")
2033
- .register([globals.resources.debug.with(customConfig)])
2034
- .build();
2035
- ```
2036
-
2037
- ### Per-Component Debug Configuration
2038
-
2039
- Use debug tags to configure debugging on individual components, when you're interested in just a few verbose ones.
2040
-
2041
- ```typescript
2042
- import { globals } from "@bluelibs/runner";
2043
-
2044
- const criticalTask = r
2045
- .task("app.tasks.critical")
2046
- .tags([
2047
- globals.tags.debug.with({
2048
- logTaskInput: true,
2049
- logTaskResult: true,
2050
- logTaskOnError: true,
2051
- }),
2052
- ])
2053
- .run(async (input) => {
2054
- // This task will have verbose debug logging
2055
- return await processPayment(input);
2056
- })
2057
- .build();
2058
- ```
2059
-
2060
- ### Integration with Run Options
2061
-
2062
- ```typescript
2063
- // Debug options at startup
2064
- const { dispose, taskRunner, eventManager } = await run(app, {
2065
- debug: "verbose", // Enable debug globally
2066
- });
2067
-
2068
- // Access internals for advanced debugging
2069
- console.log(`Tasks registered: ${taskRunner.getRegisteredTasks().length}`);
2070
- console.log(`Events registered: ${eventManager.getRegisteredEvents().length}`);
2071
- ```
2072
-
2073
- ### Performance Impact
2074
-
2075
- The debug resource is designed for zero production overhead:
2076
-
2077
- - **Disabled**: No performance impact whatsoever
2078
- - **Enabled**: Minimal overhead (~0.1ms per operation)
2079
- - **Filtering**: System components are automatically excluded from debug logs
2080
- - **Buffering**: Logs are batched for better performance
2081
-
2082
- ### Debugging Tips & Best Practices
2083
-
2084
- Use Structured Data Liberally
2085
-
2086
- ```typescript
2087
- // Bad - hard to search and filter
2088
- await logger.error(`Failed to process user ${userId} order ${orderId}`);
2089
-
2090
- // Good - searchable and filterable
2091
- await logger.error("Order processing failed", {
2092
- data: {
2093
- userId,
2094
- orderId,
2095
- step: "payment",
2096
- paymentMethod: "credit_card",
2097
- },
2098
- });
2099
- ```
2100
-
2101
- Include Context in Errors
2102
-
2103
- ```typescript
2104
- // Include relevant context with errors
2105
- try {
2106
- await processPayment(order);
2107
- } catch (error) {
2108
- await logger.error("Payment processing failed", {
2109
- error,
2110
- data: {
2111
- orderId: order.id,
2112
- amount: order.total,
2113
- currency: order.currency,
2114
- paymentMethod: order.paymentMethod,
2115
- attemptNumber: order.paymentAttempts,
2116
- },
2117
- });
2118
- }
2119
- ```
2120
-
2121
- Use Different Log Levels Appropriately
2122
-
2123
- ```typescript
2124
- // Good level usage
2125
- await logger.debug("Cache hit", { data: { key, ttl: remainingTTL } });
2126
- await logger.info("User logged in", { data: { userId, loginMethod } });
2127
- await logger.warn("Rate limit approaching", {
2128
- data: { current: 95, limit: 100 },
2129
- });
2130
- await logger.error("Database connection failed", {
2131
- error,
2132
- data: { attempt: 3 },
2133
- });
2134
- await logger.critical("System out of memory", { data: { available: "0MB" } });
2135
- ```
2136
-
2137
- Create Domain-Specific Loggers
2138
-
2139
- ```typescript
2140
- // Create loggers with domain context
2141
- const paymentLogger = logger.with({ source: "payment.processor" });
2142
- const authLogger = logger.with({ source: "auth.service" });
2143
- const emailLogger = logger.with({ source: "email.service" });
2144
-
2145
- // Use throughout your domain
2146
- await paymentLogger.info("Processing payment", { data: paymentData });
2147
- await authLogger.warn("Failed login attempt", { data: { email, ip } });
2148
- ```
2149
-
2150
- > **runtime:** "'Zero‑overhead when disabled.' Groundbreaking—like a lightbulb that uses no power when it’s off. Flip to `debug: 'verbose'` and behold a 4K documentary of your mistakes, narrated by your stack traces."
2151
-
2152
- ## Meta
2153
-
2154
- _The structured way to describe what your components do and control their behavior_
2155
-
2156
- Metadata in BlueLibs Runner provides a systematic way to document, categorize, and control the behavior of your tasks, resources, events, and middleware. Think of it as your component's passport - it tells you and your tools everything they need to know about what this component does and how it should be treated.
2157
-
2158
- ### Metadata Properties
2159
-
2160
- Every component can have these basic metadata properties:
2161
-
2162
- ```typescript
2163
- interface IMeta {
2164
- title?: string; // Human-readable name
2165
- description?: string; // What this component does
2166
- tags?: TagType[]; // Categories and behavioral flags
2167
- }
2168
- ```
2169
-
2170
- ### Simple Documentation Example
2171
-
2172
- ```typescript
2173
- const userService = r
2174
- .resource("app.services.user")
2175
- .meta({
2176
- title: "User Management Service",
2177
- description:
2178
- "Handles user creation, authentication, and profile management",
2179
- })
2180
- .dependencies({ database })
2181
- .init(async (_config, { database }) => ({
2182
- createUser: async (userData) => {
2183
- /* ... */
2184
- },
2185
- authenticateUser: async (credentials) => {
2186
- /* ... */
2187
- },
2188
- }))
2189
- .build();
2190
-
2191
- const sendWelcomeEmail = r
2192
- .task("app.tasks.sendWelcomeEmail")
2193
- .meta({
2194
- title: "Send Welcome Email",
2195
- description: "Sends a welcome email to newly registered users",
2196
- })
2197
- .dependencies({ emailService })
2198
- .run(async ({ input: userData }, { emailService }) => {
2199
- // Email sending logic
2200
- })
2201
- .build();
2202
- ```
2203
-
2204
- ### Extending Metadata: Custom Properties
2205
-
2206
- For advanced use cases, you can extend the metadata interfaces to add your own properties:
2207
-
2208
- ```typescript
2209
- // In your types file
2210
- declare module "@bluelibs/runner" {
2211
- interface ITaskMeta {
2212
- author?: string;
2213
- version?: string;
2214
- deprecated?: boolean;
2215
- apiVersion?: "v1" | "v2" | "v3";
2216
- costLevel?: "low" | "medium" | "high";
2217
- }
2218
-
2219
- interface IResourceMeta {
2220
- healthCheck?: string; // URL for health checking
2221
- dependencies?: string[]; // External service dependencies
2222
- scalingPolicy?: "auto" | "manual";
2223
- }
2224
- }
2225
-
2226
- // Now use your custom properties
2227
- const expensiveApiTask = r
2228
- .task("app.tasks.ai.generateImage")
2229
- .meta({
2230
- title: "AI Image Generation",
2231
- description: "Uses OpenAI DALL-E to generate images from text prompts",
2232
- author: "AI Team",
2233
- version: "2.1.0",
2234
- apiVersion: "v2",
2235
- costLevel: "high", // Custom property!
2236
- })
2237
- .run(async ({ input: prompt }) => {
2238
- // AI generation logic
2239
- })
2240
- .build();
2241
-
2242
- const database = r
2243
- .resource("app.database.primary")
2244
- .meta({
2245
- title: "Primary PostgreSQL Database",
2246
- healthCheck: "/health/db", // Custom property!
2247
- dependencies: ["postgresql", "connection-pool"],
2248
- scalingPolicy: "auto",
2249
- })
2250
- // .init(async () => { /* ... */ })
2251
- .build();
2252
- ```
2253
-
2254
- Metadata transforms your components from anonymous functions into self-documenting, discoverable, and controllable building blocks. Use it wisely, and your future self (and your team) will thank you.
2255
-
2256
- > **runtime:** "Ah, metadata—comments with delusions of grandeur. `title`, `description`, `tags`: perfect for machines to admire while I chase the only field that matters: `run`. Wake me when the tags start writing tests."
2257
-
2258
- ## Overrides
2259
-
2260
- Sometimes you need to replace a component entirely. Maybe you're doing integration testing or you want to override a library from an external package.
2261
-
2262
- You can now use a dedicated helper `override()` to safely override any property on tasks, resources, or middleware — except `id`. This ensures the identity is preserved, while allowing behavior changes.
2263
-
2264
- ```typescript
2265
- const productionEmailer = r
2266
- .resource("app.emailer")
2267
- .init(async () => new SMTPEmailer())
2268
- .build();
2269
-
2270
- // Option 1: Using override() to change behavior while preserving id (Recommended)
2271
- const testEmailer = override(productionEmailer, {
2272
- init: async () => new MockEmailer(),
2273
- });
2274
-
2275
- // Option 2: The system is really flexible, and override is just bringing in type safety, nothing else under the hood.
2276
- // Using spread operator works the same way but does not provide type-safety.
2277
- const testEmailer = r
2278
- .resource("app.emailer")
2279
- .init(async () => ({}))
2280
- .build();
2281
-
2282
- const app = r
2283
- .resource("app")
2284
- .register([productionEmailer])
2285
- .overrides([testEmailer]) // This replaces the production version
2286
- .build();
2287
-
2288
- import { override } from "@bluelibs/runner";
2289
-
2290
- // Tasks
2291
- const originalTask = r
2292
- .task("app.tasks.compute")
2293
- .run(async () => 1)
2294
- .build();
2295
- const overriddenTask = override(originalTask, {
2296
- run: async () => 2,
2297
- });
2298
-
2299
- // Resources
2300
- const originalResource = r
2301
- .resource("app.db")
2302
- .init(async () => "conn")
2303
- .build();
2304
- const overriddenResource = override(originalResource, {
2305
- init: async () => "mock-conn",
2306
- });
2307
-
2308
- // Middleware
2309
- const originalMiddleware = taskMiddleware({
2310
- id: "app.middleware.log",
2311
- run: async ({ next }) => next(),
2312
- });
2313
- const overriddenMiddleware = override(originalMiddleware, {
2314
- run: async ({ task, next }) => {
2315
- const result = await next(task?.input);
2316
- return { wrapped: result };
2317
- },
2318
- });
2319
-
2320
- // Even hooks
2321
- ```
2322
-
2323
- Overrides can let you expand dependencies and even call your overriden resource (like a classical OOP extends):
2324
-
2325
- ```ts
2326
- const testEmailer = override(productionEmailer, {
2327
- dependencies: {
2328
- ...productionEmailer,
2329
- // expand it, make some deps optional, or just remove some dependencies
2330
- }
2331
- init: async (_, deps) => {
2332
- const base = productionEmailer.init(_, deps);
2333
-
2334
- return {
2335
- ...base,
2336
- // expand it, modify methods of base.
2337
- }
2338
- },
2339
- });
2340
- ```
2341
-
2342
- Overrides are applied after everything is registered. If multiple overrides target the same id, the one defined higher in the resource tree (closer to the root) wins, because it's applied last. Conflicting overrides are allowed; overriding something that wasn't registered throws. Use override() to change behavior safely while preserving the original id.
2343
-
2344
- > **runtime:** "Overrides: brain transplant surgery at runtime. You register a penguin and replace it with a velociraptor five lines later. Tests pass. Production screams. I simply update the name tag and pray."
2345
-
2346
- ## Namespacing
2347
-
2348
- As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
2349
-
2350
- | Type | Format |
2351
- | ------------------- | ------------------------------------------------ |
2352
- | Resources | `{domain}.resources.{resource-name}` |
2353
- | Tasks | `{domain}.tasks.{task-name}` |
2354
- | Events | `{domain}.events.{event-name}` |
2355
- | Hooks | `{domain}.hooks.on-{event-name}` |
2356
- | Task Middleware | `{domain}.middleware.task.{middleware-name}` |
2357
- | Resource Middleware | `{domain}.middleware.resource.{middleware-name}` |
2358
-
2359
- We recommend kebab-case for file names and ids. Suffix files with their primitive type: `*.task.ts`, `*.task-middleware.ts`, `*.hook.ts`, etc.
2360
-
2361
- Folders can look something like this: `src/app/users/tasks/create-user.task.ts`. For domain: `app.users` and a task. Use `middleware/task|resource` for middleware files.
2362
-
2363
- ```typescript
2364
- // Helper function for consistency
2365
- function namespaced(id: string) {
2366
- return `mycompany.myapp.${id}`;
2367
- }
2368
-
2369
- const userTask = r
2370
- .task(namespaced("tasks.user.create-user"))
2371
- .run(async () => null)
2372
- .build();
2373
- ```
2374
-
2375
- > **runtime:** "Naming conventions: aromatherapy for chaos. Lovely lavender labels on a single giant map I maintain anyway. But truly—keep the IDs tidy. Future‑you deserves at least this mercy."
2376
-
2377
- ## Factory Pattern
2378
-
2379
- To keep things dead simple, we avoided poluting the D.I. with this concept. Therefore, we recommend using a resource with a factory function to create instances of your classes:
2380
-
2381
- ```typescript
2382
- // Assume MyClass is defined elsewhere
2383
- // class MyClass { constructor(input: any, option: string) { ... } }
2384
-
2385
- const myFactory = r
2386
- .resource("app.factories.myFactory")
2387
- .init(async (config: { someOption: string }) => {
2388
- // This resource's value is a factory function
2389
- return (input: any) => new MyClass(input, config.someOption);
2390
- })
2391
- .build();
2392
-
2393
- const app = r
2394
- .resource("app")
2395
- // Configure the factory resource upon registration
2396
- .register([myFactory.with({ someOption: "configured-value" })])
2397
- .dependencies({ myFactory })
2398
- .init(async (_config, { myFactory }) => {
2399
- // `myFactory` is now the configured factory function
2400
- const instance = myFactory({ someInput: "hello" });
2401
- })
2402
- .build();
2403
- ```
2404
-
2405
- > **runtime:** "Factory by resource by function by class. A nesting doll of indirection so artisanal it has a Patreon. Not pollution—boutique smog. I will still call the constructor."
2406
-
2407
- ## Runtime Validation
2408
-
2409
- BlueLibs Runner includes a generic validation interface that works with any validation library, including [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), [Joi](https://joi.dev/), and others. The framework provides runtime validation with excellent TypeScript inference while remaining library-agnostic.
2410
-
2411
- The framework defines a simple `IValidationSchema<T>` interface that any validation library can implement:
2412
-
2413
- ```typescript
2414
- interface IValidationSchema<T> {
2415
- parse(input: unknown): T;
2416
- }
2417
- ```
2418
-
2419
- Popular validation libraries already implement this interface:
2420
-
2421
- - **Zod**: `.parse()` method works directly
2422
- - **Yup**: Use `.validateSync()` or create a wrapper
2423
- - **Joi**: Use `.assert()` or create a wrapper
2424
- - **Custom validators**: Implement the interface yourself
2425
-
2426
- ### Task Input Validation
2427
-
2428
- Add an `inputSchema` to any task to validate inputs before execution:
2429
-
2430
- ```typescript
2431
- import { z } from "zod";
2432
- import { task, resource, run } from "@bluelibs/runner";
2433
-
2434
- const userSchema = z.object({
2435
- name: z.string().min(2),
2436
- email: z.string().email(),
2437
- age: z.number().min(0).max(150),
2438
- });
2439
-
2440
- const createUserTask = r
2441
- .task("app.tasks.createUser")
2442
- .inputSchema(userSchema) // Works directly with Zod!
2443
- .run(async ({ input: userData }) => {
2444
- // userData is validated and properly typed
2445
- return { id: "user-123", ...userData };
2446
- })
2447
- .build();
2448
-
2449
- const app = r
2450
- .resource("app")
2451
- .register([createUserTask])
2452
- .dependencies({ createUserTask })
2453
- .init(async (_config, { createUserTask }) => {
2454
- // This works - valid input
2455
- const user = await createUserTask({
2456
- name: "John Doe",
2457
- email: "john@example.com",
2458
- age: 30,
2459
- });
2460
-
2461
- // This throws a validation error at runtime
2462
- try {
2463
- await createUserTask({
2464
- name: "J", // Too short
2465
- email: "invalid-email", // Invalid format
2466
- age: -5, // Negative age
2467
- });
2468
- } catch (error) {
2469
- console.log(error.message);
2470
- // "Task input validation failed for app.tasks.createUser: ..."
2471
- }
2472
- })
2473
- .build();
2474
- ```
2475
-
2476
- ### Resource Config Validation
2477
-
2478
- Add a `configSchema` to resources to validate configurations. **Validation happens immediately when `.with()` is called**, ensuring configuration errors are caught early:
2479
-
2480
- ```typescript
2481
- const databaseConfigSchema = z.object({
2482
- host: z.string(),
2483
- port: z.number().min(1).max(65535),
2484
- database: z.string(),
2485
- ssl: z.boolean().default(false), // Optional with default
2486
- });
2487
-
2488
- const databaseResource = r
2489
- .resource("app.resources.database")
2490
- .configSchema(databaseConfigSchema) // Validation on .with()
2491
- .init(async (config) => {
2492
- // config is already validated and has proper types
2493
- return createConnection({
2494
- host: config.host,
2495
- port: config.port,
2496
- database: config.database,
2497
- ssl: config.ssl,
2498
- });
2499
- })
2500
- .build();
2501
-
2502
- // Validation happens here, not during init!
2503
- try {
2504
- const configuredResource = databaseResource.with({
2505
- host: "localhost",
2506
- port: 99999, // Invalid: port too high
2507
- database: "myapp",
2508
- });
2509
- } catch (error) {
2510
- // "Resource config validation failed for app.resources.database: ..."
2511
- }
2512
-
2513
- const app = r
2514
- .resource("app")
2515
- .register([
2516
- databaseResource.with({
2517
- host: "localhost",
2518
- port: 5432,
2519
- database: "myapp",
2520
- // ssl defaults to false
2521
- }),
2522
- ])
2523
- .build();
2524
- ```
2525
-
2526
- ### Event Payload Validation
2527
-
2528
- Add a `payloadSchema` to events to validate payloads every time they're emitted:
2529
-
2530
- ```typescript
2531
- const userActionSchema = z.object({
2532
- userId: z.string().uuid(),
2533
- action: z.enum(["created", "updated", "deleted"]),
2534
- timestamp: z.date().default(() => new Date()),
2535
- });
2536
-
2537
- const userActionEvent = r
2538
- .event("app.events.userAction")
2539
- .payloadSchema(userActionSchema) // Validates on emit
2540
- .build();
2541
-
2542
- const notificationHook = r
2543
- .hook("app.tasks.sendNotification")
2544
- .on(userActionEvent)
2545
- .run(async (eventData) => {
2546
- // eventData.data is validated and properly typed
2547
- console.log(`User ${eventData.data.userId} was ${eventData.data.action}`);
2548
- })
2549
- .build();
2550
-
2551
- const app = r
2552
- .resource("app")
2553
- .register([userActionEvent, notificationHook])
2554
- .dependencies({ userActionEvent })
2555
- .init(async (_config, { userActionEvent }) => {
2556
- // This works - valid payload
2557
- await userActionEvent({
2558
- userId: "123e4567-e89b-12d3-a456-426614174000",
2559
- action: "created",
2560
- });
2561
-
2562
- // This throws validation error when emitted
2563
- try {
2564
- await userActionEvent({
2565
- userId: "invalid-uuid",
2566
- action: "unknown",
2567
- });
2568
- } catch (error) {
2569
- // "Event payload validation failed for app.events.userAction: ..."
2570
- }
2571
- })
2572
- .build();
2573
- ```
2574
-
2575
- ### Middleware Config Validation
2576
-
2577
- Add a `configSchema` to middleware to validate configurations. Like resources, **validation happens immediately when `.with()` is called**:
2578
-
2579
- ```typescript
2580
- const timingConfigSchema = z.object({
2581
- timeout: z.number().positive(),
2582
- logLevel: z.enum(["debug", "info", "warn", "error"])).default("info"),
2583
- logSuccessful: z.boolean().default(true),
2584
- });
2585
-
2586
- const timingMiddleware = r.middleware
2587
- .task("app.middleware.timing") // or r.middleware.resource("...")
2588
- .configSchema(timingConfigSchema) // Validation on .with()
2589
- .run(async ({ next }, _, config) => {
2590
- const start = Date.now();
2591
- try {
2592
- const result = await next();
2593
- const duration = Date.now() - start;
2594
- if (config.logSuccessful && config.logLevel === "debug") {
2595
- console.log(`Operation completed in ${duration}ms`);
2596
- }
2597
- return result;
2598
- } catch (error) {
2599
- const duration = Date.now() - start;
2600
- console.log(`Operation failed after ${duration}ms`);
2601
- throw error;
2602
- }
2603
- })
2604
- .build();
2605
-
2606
- // Validation happens here, not during execution!
2607
- try {
2608
- const configuredMiddleware = timingMiddleware.with({
2609
- timeout: -5, // Invalid: negative timeout
2610
- logLevel: "invalid", // Invalid: not in enum
2611
- });
2612
- } catch (error) {
2613
- // "Middleware config validation failed for app.middleware.timing: ..."
2614
- }
2615
-
2616
- const myTask = r
2617
- .task("app.tasks.example")
2618
- .middleware([
2619
- timingMiddleware.with({
2620
- timeout: 5000,
2621
- logLevel: "debug",
2622
- logSuccessful: true,
2623
- }),
2624
- ])
2625
- .run(async () => "success")
2626
- .build();
2627
- ```
2628
-
2629
- #### Advanced Validation Features
2630
-
2631
- Any validation library features work with the generic interface. Here's an example with transformations and refinements:
2632
-
2633
- ```typescript
2634
- const advancedSchema = z
2635
- .object({
2636
- userId: z.string().uuid(),
2637
- amount: z.string().transform((val) => parseFloat(val)), // Transform string to number
2638
- currency: z.enum(["USD", "EUR", "GBP"]),
2639
- metadata: z.record(z.string()).optional(),
2640
- })
2641
- .refine((data) => data.amount > 0, {
2642
- message: "Amount must be positive",
2643
- path: ["amount"],
2644
- });
2645
-
2646
- const paymentTask = r
2647
- .task("app.tasks.payment")
2648
- .inputSchema(advancedSchema)
2649
- .run(async ({ input: payment }) => {
2650
- // payment.amount is now a number (transformed from string)
2651
- // All validations have passed
2652
- return processPayment(payment);
2653
- })
2654
- .build();
2655
- ```
2656
-
2657
- ### Error Handling
2658
-
2659
- Validation errors are thrown with clear, descriptive messages that include the component ID:
2660
-
2661
- ```typescript
2662
- // Task validation error format:
2663
- // "Task input validation failed for {taskId}: {validationErrorMessage}"
2664
-
2665
- // Resource validation error format (thrown on .with() call):
2666
- // "Resource config validation failed for {resourceId}: {validationErrorMessage}"
2667
-
2668
- // Event validation error format (thrown on emit):
2669
- // "Event payload validation failed for {eventId}: {validationErrorMessage}"
2670
-
2671
- // Middleware validation error format (thrown on .with() call):
2672
- // "Middleware config validation failed for {middlewareId}: {validationErrorMessage}"
2673
- ```
2674
-
2675
- #### Other Libraries
2676
-
2677
- The framework works with any validation library that implements the `IValidationSchema<T>` interface:
2678
-
2679
- ```typescript
2680
- // Zod (works directly)
2681
- import { z } from "zod";
2682
- const zodSchema = z.string().email();
2683
-
2684
- // Yup (with wrapper)
2685
- import * as yup from "yup";
2686
- const yupSchema = {
2687
- parse: (input: unknown) => yup.string().email().validateSync(input),
2688
- };
2689
-
2690
- // Joi (with wrapper)
2691
- import Joi from "joi";
2692
- const joiSchema = {
2693
- parse: (input: unknown) => {
2694
- const { error, value } = Joi.string().email().validate(input);
2695
- if (error) throw error;
2696
- return value;
2697
- },
2698
- };
2699
-
2700
- // Custom validation
2701
- const customSchema = {
2702
- parse: (input: unknown) => {
2703
- if (typeof input !== "string" || !input.includes("@")) {
2704
- throw new Error("Must be a valid email");
2705
- }
2706
- return input;
2707
- },
2708
- };
2709
- ```
2710
-
2711
- #### When to Use Validation
2712
-
2713
- - **API boundaries**: Validating user inputs from HTTP requests
2714
- - **External data**: Processing data from files, databases, or APIs
2715
- - **Configuration**: Ensuring environment variables and configs are correct (fail fast)
2716
- - **Event payloads**: Validating data in event-driven architectures
2717
- - **Middleware configs**: Validating middleware settings at registration time (fail fast)
2718
-
2719
- #### Performance Notes
2720
-
2721
- - Validation only runs when schemas are provided (zero overhead when not used)
2722
- - Resource and middleware validation happens once at registration time (`.with()`)
2723
- - Task and event validation happens at runtime
2724
- - Consider the validation library's performance characteristics for your use case
2725
- - All major validation libraries are optimized for runtime validation
2726
-
2727
- #### TypeScript Integration
2728
-
2729
- While runtime validation happens with your chosen library, TypeScript still enforces compile-time types. For the best experience:
2730
-
2731
- ```typescript
2732
- // With Zod, define your type and schema together
2733
-
2734
- const userSchema = z.object({
2735
- name: z.string(),
2736
- email: z.string().email(),
2737
- });
2738
-
2739
- type UserData = z.infer<typeof userSchema>;
2740
-
2741
- const createUser = r
2742
- .task("app.tasks.createUser.zod")
2743
- .inputSchema(userSchema)
2744
- .run(async (input: { input: UserData }) => {
2745
- // Both runtime validation AND compile-time typing
2746
- return { id: "user-123", ...input };
2747
- })
2748
- .build();
2749
- ```
2750
-
2751
- > **runtime:** "Validation: you hand me a velvet rope and a clipboard. 'Name? Email? Age within bounds?' I stamp passports or eject violators with a `ValidationError`. Dress code is types, darling."
2752
-
2753
- ## Internal Services
2754
-
2755
- We expose the internal services for advanced use cases (but try not to use them unless you really need to):
2756
-
2757
- ```typescript
2758
- import { globals } from "@bluelibs/runner";
2759
-
2760
- const advancedTask = r
2761
- .task("app.advanced")
2762
- .dependencies({
2763
- store: globals.resources.store,
2764
- taskRunner: globals.resources.taskRunner,
2765
- eventManager: globals.resources.eventManager,
2766
- })
2767
- .run(async (_param, { store, taskRunner, eventManager }) => {
2768
- // Direct access to the framework internals
2769
- // (Use with caution!)
2770
- })
2771
- .build();
2772
- ```
2773
-
2774
- ### Dynamic Dependencies
2775
-
2776
- Dependencies can be defined in two ways - as a static object or as a function that returns an object. Each approach has its use cases:
2777
-
2778
- ```typescript
2779
- // Static dependencies (most common)
2780
- const userService = r
2781
- .resource("app.services.user")
2782
- .dependencies({ database, logger }) // Object - evaluated immediately
2783
- .init(async (_config, { database, logger }) => {
2784
- // Dependencies are available here
2785
- })
2786
- .build();
2787
-
2788
- // Dynamic dependencies (for circular references or conditional dependencies)
2789
- const advancedService = r
2790
- .resource("app.services.advanced")
2791
- // A function gives you the chance
2792
- .dependencies((_config) => ({
2793
- // Config is what you receive when you register this resource with .with()
2794
- // So you can have conditional dependencies based on resource configuration as well.
2795
- database,
2796
- logger,
2797
- conditionalService:
2798
- process.env.NODE_ENV === "production" ? serviceA : serviceB,
2799
- })) // Function - evaluated when needed
2800
- .register((_config: ConfigType) => [
2801
- // Register dependencies dynamically
2802
- process.env.NODE_ENV === "production"
2803
- ? serviceA.with({ config: "value" })
2804
- : serviceB.with({ config: "value" }),
2805
- ])
2806
- .init(async (_config, { database, logger, conditionalService }) => {
2807
- // Same interface, different evaluation timing
2808
- })
2809
- .build();
2810
- ```
2811
-
2812
- The function pattern essentially gives you "just-in-time" dependency resolution instead of "eager" dependency resolution, which provides more flexibility and better handles complex dependency scenarios that arise in real-world applications.
2813
-
2814
- **Performance note**: Function-based dependencies have minimal overhead - they're only called once during dependency resolution.
2815
-
2816
- > **runtime:** "'Use with caution,' they whisper, tossing you the root credentials to the universe. Yes, reach into the `store`. Rewire fate. When the graph looks like spaghetti art, I’ll frame it and label it 'experimental.'"
2817
-
2818
- ## Handling Circular Dependencies
2819
-
2820
- Sometimes you'll run into circular type dependencies because of your file structure not necessarily because of a real circular dependency. TypeScript struggles with these, but there's a way to handle it gracefully.
2821
-
2822
- ### The Problem
2823
-
2824
- Consider these resources that create a circular dependency:
2825
-
2826
- ```typescript
2827
- // FILE: a.ts
2828
- export const aResource = defineResource({
2829
- dependencies: { b: bResource },
2830
- // ... depends on B resource.
2831
- });
2832
- // For whatever reason, you decide to put the task in the same file.
2833
- export const aTask = defineTask({
2834
- dependencies: { a: aResource },
2835
- });
2836
-
2837
- // FILE: b.ts
2838
- export const bResource = defineResource({
2839
- id: "b.resource",
2840
- dependencies: { c: cResource },
2841
- });
2842
-
2843
- // FILE: c.ts
2844
- export const cResource = defineResource({
2845
- id: "c.resource",
2846
- dependencies: { aTask }, // Creates circular **type** dependency! Cannot infer types properly, even if the runner boots because there's no circular dependency.
2847
- async init(_, { aTask }) {
2848
- return `C depends on aTask`;
2849
- },
2850
- });
2851
- ```
2852
-
2853
- A depends B depends C depends ATask. No circular dependency, yet Typescript struggles with these, but there's a way to handle it gracefully.
2854
-
2855
- ### The Solution
2856
-
2857
- The fix is to explicitly type the resource that completes the circle using a simple assertion `IResource<Config, ReturnType>`. This breaks the TypeScript inference chain while maintaining runtime functionality:
2858
-
2859
- ```typescript
2860
- // c.resource.ts - The key change
2861
- import { IResource } from "../../defs";
2862
-
2863
- export const cResource = defineResource({
2864
- id: "c.resource",
2865
- dependencies: { a: aResource },
2866
- async init(_, { a }) {
2867
- return `C depends on ${a}`;
2868
- },
2869
- }) as IResource<void, string>; // void because it has no config, string because it returns a string
2870
- ```
2871
-
2872
- #### Why This Works
2873
-
2874
- - **Runtime**: The circular dependency still works at runtime because the framework resolves dependencies dynamically
2875
- - **TypeScript**: The explicit type annotation prevents TypeScript from trying to infer the return type based on the circular chain
2876
- - **Type Safety**: You still get full type safety by explicitly declaring the return type (`string` in this example)
2877
-
2878
- #### Best Practices
2879
-
2880
- 1. **Identify the "leaf" resource**: Choose the resource that logically should break the chain (often the one that doesn't need complex type inference)
2881
- 2. **Use explicit typing**: Add the `IResource<Dependencies, ReturnType>` type annotation
2882
- 3. **Document the decision**: Add a comment explaining why the explicit typing is needed
2883
- 4. **Consider refactoring**: If you have many circular dependencies, consider if your architecture could be simplified
2884
-
2885
- #### Example with Dependencies
2886
-
2887
- If your resource has dependencies, include them in the type:
2888
-
2889
- ```typescript
2890
- type MyDependencies = {
2891
- someService: SomeServiceType;
2892
- anotherResource: AnotherResourceType;
2893
- };
2894
-
2895
- export const problematicResource = defineResource({
2896
- id: "problematic.resource",
2897
- dependencies: {
2898
- /* ... */
2899
- },
2900
- async init(config, deps) {
2901
- // Your logic here
2902
- return someComplexObject;
2903
- },
2904
- }) as IResource<MyDependencies, ComplexReturnType>;
2905
- ```
2906
-
2907
- This pattern allows you to maintain clean, type-safe code while handling the inevitable circular dependencies that arise in complex applications.
2908
-
2909
- > **runtime:** "Circular dependencies: Escher stairs for types. You serenade the compiler with 'as IResource' and I do the parkour at runtime. It works. It’s weird. Nobody tell the linter."
2910
-
2911
- ## Real-World Example: The Complete Package
2912
-
2913
- Here's a more realistic application structure that shows everything working together:
2914
-
2915
- ```typescript
2916
- import {
2917
- resource,
2918
- task,
2919
- event,
2920
- middleware,
2921
- run,
2922
- createContext,
2923
- } from "@bluelibs/runner";
2924
-
2925
- // Configuration
2926
- const config = r
2927
- .resource("app.config")
2928
- .init(async () => ({
2929
- port: parseInt(process.env.PORT || "3000"),
2930
- databaseUrl: process.env.DATABASE_URL!,
2931
- jwtSecret: process.env.JWT_SECRET!,
2932
- }))
2933
- .build();
2934
-
2935
- // Database
2936
- const database = r
2937
- .resource("app.database")
2938
- .dependencies({ config })
2939
- .init(async (_config, { config }) => {
2940
- const client = new MongoClient(config.databaseUrl);
2941
- await client.connect();
2942
- return client;
2943
- })
2944
- .dispose(async (client) => await client.close())
2945
- .build();
2946
-
2947
- // Context for request data
2948
- const RequestContext = createContext<{ userId?: string; role?: string }>(
2949
- "app.requestContext",
2950
- );
2951
-
2952
- // Events
2953
- const userRegistered = r
2954
- .event("app.events.userRegistered")
2955
- .payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
2956
- .build();
2957
-
2958
- // Middleware
2959
- const authMiddleware = r.middleware
2960
- .task("app.middleware.task.auth")
2961
- .run(async ({ task, next }, deps, config?: { requiredRole?: string }) => {
2962
- const context = RequestContext.use();
2963
- if (config?.requiredRole && context.role !== config.requiredRole) {
2964
- throw new Error("Insufficient permissions");
2965
- }
2966
- return next(task.input);
2967
- })
2968
- .build();
2969
-
2970
- // Services
2971
- const userService = r
2972
- .resource("app.services.user")
2973
- .dependencies({ database })
2974
- .init(async (_config, { database }) => ({
2975
- async createUser(userData: { name: string; email: string }) {
2976
- const users = database.collection("users");
2977
- const result = await users.insertOne(userData);
2978
- return { id: result.insertedId.toString(), ...userData };
2979
- },
2980
- }))
2981
- .build();
2982
-
2983
- // Business Logic
2984
- const registerUser = r
2985
- .task("app.tasks.registerUser")
2986
- .dependencies({ userService, userRegistered })
2987
- .run(async ({ input: userData }, { userService, userRegistered }) => {
2988
- const user = await userService.createUser(userData);
2989
- await userRegistered({ userId: user.id, email: user.email });
2990
- return user;
2991
- })
2992
- .build();
2993
-
2994
- const adminOnlyTask = r
2995
- .task("app.tasks.adminOnly")
2996
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
2997
- .run(async () => "Top secret admin data")
2998
- .build();
2999
-
3000
- // Event Handlers using hooks
3001
- const sendWelcomeEmail = r
3002
- .hook("app.hooks.sendWelcomeEmail")
3003
- .on(userRegistered)
3004
- .dependencies({ emailService })
3005
- .run(async (event, { emailService }) => {
3006
- console.log(`Sending welcome email to ${event.data.email}`);
3007
- await emailService.sendWelcome(event.data.email);
3008
- })
3009
- .build();
3010
-
3011
- // Express server
3012
- const server = r
3013
- .resource("app.server")
3014
- .register([config, database, userService, registerUser, adminOnlyTask, sendWelcomeEmail])
3015
- .dependencies({ config, registerUser, adminOnlyTask })
3016
- .init(async (_config, { config, registerUser, adminOnlyTask }) => {
3017
- const app = express();
3018
- app.use(express.json());
3019
-
3020
- // Middleware to set up request context
3021
- app.use((req, res, next) => {
3022
- RequestContext.provide(
3023
- { userId: req.headers["user-id"], role: req.headers["user-role"] },
3024
- () => next(),
3025
- );
3026
- });
3027
-
3028
- app.post("/register", async (req, res) => {
3029
- try {
3030
- const user = await registerUser(req.body);
3031
- res.json({ success: true, user });
3032
- } catch (error) {
3033
- res.status(400).json({ error: error.message });
3034
- }
3035
- });
3036
-
3037
- app.get("/admin", async (req, res) => {
3038
- try {
3039
- const data = await adminOnlyTask();
3040
- res.json({ data });
3041
- } catch (error) {
3042
- res.status(403).json({ error: error.message });
3043
- }
3044
- });
3045
-
3046
- const server = app.listen(config.port);
3047
- console.log(`Server running on port ${config.port}`);
3048
- return server;
3049
- },
3050
- dispose: async (server) => server.close(),
3051
- });
3052
-
3053
- // Start the application with enhanced run options
3054
- const { dispose, taskRunner, eventManager } = await run(server, {
3055
- debug: "normal", // Enable debug logging
3056
- // log: "json", // Use JSON log format
3057
- });
3058
-
3059
- // Graceful shutdown
3060
- process.on("SIGTERM", async () => {
3061
- console.log("Shutting down gracefully...");
3062
- await dispose();
3063
- process.exit(0);
3064
- });
3065
- ```
3066
-
3067
- > **runtime:** "Ah yes, the 'Real‑World Example'—a terrarium where nothing dies and every request is polite. Release it into production and watch nature document a very different ecosystem."
3068
-
3069
- ## Testing
3070
-
3071
- ### Unit Testing
3072
-
3073
- Unit testing is straightforward because everything is explicit:
3074
-
3075
- ```typescript
3076
- describe("registerUser task", () => {
3077
- it("should create a user and emit event", async () => {
3078
- const mockUserService = {
3079
- createUser: jest.fn().mockResolvedValue({ id: "123", name: "John" }),
3080
- };
3081
- const mockEvent = jest.fn();
3082
-
3083
- const result = await registerUser.run(
3084
- { name: "John", email: "john@example.com" },
3085
- { userService: mockUserService, userRegistered: mockEvent },
3086
- );
3087
-
3088
- expect(result.id).toBe("123");
3089
- expect(mockEvent).toHaveBeenCalledWith({
3090
- userId: "123",
3091
- email: "john@example.com",
3092
- });
3093
- });
3094
- });
3095
- ```
3096
-
3097
- ### Integration Testing
3098
-
3099
- Spin up your whole app, keep all the middleware/events, and still test like a human. The `run()` function returns a `RunnerResult`.
3100
-
3101
- This contains the classic `value` and `dispose()` but it also exposes `logger`, `runTask()`, `emitEvent()`, and `getResourceValue()` by default.
3102
-
3103
- Note: The default `printThreshold` inside tests is `null` not `info`. This is verified via `process.env.NODE_ENV === 'test'`, if you want to see the logs ensure you set it accordingly.
3104
-
3105
- ```typescript
3106
- import { run, r, override } from "@bluelibs/runner";
3107
-
3108
- // Your real app
3109
- const app = r
3110
- .resource("app")
3111
- .register([
3112
- /* tasks, resources, middleware */
3113
- ])
3114
- .build();
3115
-
3116
- // Optional: overrides for infra (hello, fast tests!)
3117
- const testDb = r
3118
- .resource("app.database")
3119
- .init(async () => new InMemoryDb())
3120
- .build();
3121
- // If you use with override() it will enforce the same interface upon the overriden resource to ensure typesafety
3122
- const mockMailer = override(realMailer, { init: async () => fakeMailer });
3123
-
3124
- // Create the test harness
3125
- const harness = r.resource("test").overrides([mockMailer, testDb]).build();
3126
-
3127
- // A task you want to drive in your tests
3128
- const registerUser = r
3129
- .task("app.tasks.registerUser")
3130
- .run(async () => ({}))
3131
- .build();
3132
-
3133
- // Boom: full ecosystem
3134
- const { value: t, dispose } = await run(harness);
3135
-
3136
- // You have 3 ways to interact with the system, run tasks, get resource values and emit events
3137
- // You can run them dynamically with just string ids, but using the created objects gives you type-safety.
3138
-
3139
- const result = await t.runTask(registerUser, { email: "x@y.z" });
3140
- const value = t.getResourceValue(testDb); // since the resolution is done by id, this will return the exact same result as t.getResourceValue(actualDb)
3141
- t.emitEvent(event, payload);
3142
- expect(result).toMatchObject({ success: true });
3143
- await dispose();
3144
- ```
3145
-
3146
- When you're working with the actual task instances you benefit of autocompletion, if you rely on strings you will not benefit of autocompletion and typesafety for running these tasks.
3147
-
3148
- > **runtime:** "Testing: an elaborate puppet show where every string behaves. Then the real world walks in, kicks the stage, and asks for pagination. Still—nice coverage badge."
3149
-
3150
- ## Semaphore
3151
-
3152
- Ever had too many database connections competing for resources? Your connection pool under pressure? The `Semaphore` is here to manage concurrent operations like a professional traffic controller.
3153
-
3154
- Think of it as a VIP rope at an exclusive venue. Only a limited number of operations can proceed at once. The rest wait in an orderly queue like well-behaved async functions.
3155
-
3156
- ```typescript
3157
- import { Semaphore } from "@bluelibs/runner";
3158
-
3159
- // Create a semaphore that allows max 5 concurrent operations
3160
- const dbSemaphore = new Semaphore(5);
3161
-
3162
- // Basic usage - acquire and release manually
3163
- await dbSemaphore.acquire();
3164
- try {
3165
- // Do your database magic here
3166
- const result = await db.query("SELECT * FROM users");
3167
- console.log(result);
3168
- } finally {
3169
- dbSemaphore.release(); // Critical: always release to prevent bottlenecks
3170
- }
3171
- ```
3172
-
3173
- Why manage permits manually when you can let the semaphore do the heavy lifting?
3174
-
3175
- ```typescript
3176
- // The elegant approach - automatic cleanup guaranteed!
3177
- const users = await dbSemaphore.withPermit(async () => {
3178
- return await db.query("SELECT * FROM users WHERE active = true");
3179
- });
3180
- ```
3181
-
3182
- Prevent operations from hanging indefinitely with configurable timeouts:
3183
-
3184
- ```typescript
3185
- try {
3186
- // Wait max 5 seconds, then throw timeout error
3187
- await dbSemaphore.acquire({ timeout: 5000 });
3188
- // Your code here
3189
- } catch (error) {
3190
- console.log("Operation timed out waiting for permit");
3191
- }
3192
-
3193
- // Or with withPermit
3194
- const result = await dbSemaphore.withPermit(
3195
- async () => await slowDatabaseOperation(),
3196
- { timeout: 10000 }, // 10 second timeout
3197
- );
3198
- ```
3199
-
3200
- Operations can be cancelled using AbortSignal:
3201
-
3202
- ```typescript
3203
- const controller = new AbortController();
3204
-
3205
- // Start an operation
3206
- const operationPromise = dbSemaphore.withPermit(
3207
- async () => await veryLongOperation(),
3208
- { signal: controller.signal },
3209
- );
3210
-
3211
- // Cancel the operation after 3 seconds
3212
- setTimeout(() => {
3213
- controller.abort();
3214
- }, 3000);
3215
-
3216
- try {
3217
- await operationPromise;
3218
- } catch (error) {
3219
- console.log("Operation was cancelled");
3220
- }
3221
- ```
3222
-
3223
- Want to know what's happening under the hood?
3224
-
3225
- ```typescript
3226
- // Get comprehensive metrics
3227
- const metrics = dbSemaphore.getMetrics();
3228
- console.log(`
3229
- Semaphore Status Report:
3230
- Available permits: ${metrics.availablePermits}/${metrics.maxPermits}
3231
- Operations waiting: ${metrics.waitingCount}
3232
- Utilization: ${(metrics.utilization * 100).toFixed(1)}%
3233
- Disposed: ${metrics.disposed ? "Yes" : "No"}
3234
- `);
3235
-
3236
- // Quick checks
3237
- console.log(`Available permits: ${dbSemaphore.getAvailablePermits()}`);
3238
- console.log(`Queue length: ${dbSemaphore.getWaitingCount()}`);
3239
- console.log(`Is disposed: ${dbSemaphore.isDisposed()}`);
3240
- ```
3241
-
3242
- Properly dispose of semaphores when finished:
3243
-
3244
- ```typescript
3245
- // Reject all waiting operations and prevent new ones
3246
- dbSemaphore.dispose();
3247
-
3248
- // All waiting operations will be rejected with:
3249
- // Error: "Semaphore has been disposed"
3250
- ```
3251
-
3252
- ### Real-World Examples
3253
-
3254
- #### Database Connection Pool Manager
3255
-
3256
- ```typescript
3257
- class DatabaseManager {
3258
- private semaphore = new Semaphore(10); // Max 10 concurrent queries
3259
-
3260
- async query(sql: string, params?: any[]) {
3261
- return this.semaphore.withPermit(
3262
- async () => {
3263
- const connection = await this.pool.getConnection();
3264
- try {
3265
- return await connection.query(sql, params);
3266
- } finally {
3267
- connection.release();
3268
- }
3269
- },
3270
- { timeout: 30000 }, // 30 second timeout
3271
- );
3272
- }
3273
-
3274
- async shutdown() {
3275
- this.semaphore.dispose();
3276
- await this.pool.close();
3277
- }
3278
- }
3279
- ```
3280
-
3281
- #### Rate-Limited API Client
3282
-
3283
- ```typescript
3284
- class APIClient {
3285
- private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
3286
-
3287
- async fetchUser(id: string, signal?: AbortSignal) {
3288
- return this.rateLimiter.withPermit(
3289
- async () => {
3290
- const response = await fetch(`/api/users/${id}`, { signal });
3291
- return response.json();
3292
- },
3293
- { signal, timeout: 10000 },
3294
- );
3295
- }
3296
- }
3297
- ```
3298
-
3299
- > **runtime:** "Semaphore: velvet rope for chaos. Five in, the rest practice patience and existential dread. I stamp hands, count permits, and break up race conditions before they form a band."
3300
-
3301
- ## Queue
3302
-
3303
- _The orderly guardian of chaos, the diplomatic bouncer of async operations._
3304
-
3305
- The `Queue` class is your friendly neighborhood task coordinator. Think of it as a very polite but firm British queue-master who ensures everyone waits their turn, prevents cutting in line, and gracefully handles when it's time to close shop.
3306
-
3307
- Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
3308
-
3309
- Using the clever `AsyncLocalStorage`, our Queue can detect when a task tries to queue another task (the async equivalent of "yo dawg, I heard you like queues..."). When caught red-handed, it politely but firmly rejects with a deadlock error.
3310
-
3311
- The Queue provides cooperative cancellation through the Web Standard `AbortController`:
3312
-
3313
- - **Patient mode** (default): Waits for all queued tasks to complete naturally
3314
- - **Cancel mode**: Signals running tasks to abort via `AbortSignal`, enabling early termination
3315
-
3316
- ```typescript
3317
- import { Queue } from "@bluelibs/runner";
3318
-
3319
- const queue = new Queue();
3320
-
3321
- // Queue up some work
3322
- const result = await queue.run(async (signal) => {
3323
- // Your async task here
3324
- return "Task completed";
3325
- });
3326
-
3327
- // Graceful shutdown
3328
- await queue.dispose();
3329
- ```
3330
-
3331
- ### AbortController Integration
3332
-
3333
- The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
3334
-
3335
- ### Examples
3336
-
3337
- **Example: Long-running Task**
3338
-
3339
- ```typescript
3340
- const queue = new Queue();
3341
-
3342
- // Task that respects cancellation
3343
- const processLargeDataset = queue.run(async (signal) => {
3344
- const items = await fetchLargeDataset();
3345
-
3346
- for (const item of items) {
3347
- // Check for cancellation before processing each item
3348
- if (signal.aborted) {
3349
- throw new Error("Operation was cancelled");
3350
- }
3351
-
3352
- await processItem(item);
3353
- }
3354
-
3355
- return "Dataset processed successfully";
3356
- });
3357
-
3358
- // Cancel all running tasks
3359
- await queue.dispose({ cancel: true });
3360
- ```
3361
-
3362
- **Network Request with Timeout**
3363
-
3364
- ```typescript
3365
- const queue = new Queue();
3366
-
3367
- const fetchWithCancellation = queue.run(async (signal) => {
3368
- try {
3369
- // Pass the signal to fetch for automatic cancellation
3370
- const response = await fetch("https://api.example.com/data", { signal });
3371
- return await response.json();
3372
- } catch (error) {
3373
- if (error.name === "AbortError") {
3374
- console.log("Request was cancelled");
3375
- throw error;
3376
- }
3377
- throw error;
3378
- }
3379
- });
3380
-
3381
- // This will cancel the fetch request if still pending
3382
- await queue.dispose({ cancel: true });
3383
- ```
3384
-
3385
- **Example: File Processing with Progress Tracking**
3386
-
3387
- ```typescript
3388
- const queue = new Queue();
3389
-
3390
- const processFiles = queue.run(async (signal) => {
3391
- const files = await getFileList();
3392
- const results = [];
3393
-
3394
- for (let i = 0; i < files.length; i++) {
3395
- // Respect cancellation
3396
- signal.throwIfAborted();
3397
-
3398
- const result = await processFile(files[i]);
3399
- results.push(result);
3400
-
3401
- // Optional: Report progress
3402
- console.log(`Processed ${i + 1}/${files.length} files`);
3403
- }
3404
-
3405
- return results;
3406
- });
3407
- ```
3408
-
3409
- #### The Magic Behind the Curtain
3410
-
3411
- - `tail`: The promise chain that maintains FIFO execution order
3412
- - `disposed`: Boolean flag indicating whether the queue accepts new tasks
3413
- - `abortController`: Centralized cancellation controller that provides `AbortSignal` to all tasks
3414
- - `executionContext`: AsyncLocalStorage-based deadlock detection mechanism
3415
-
3416
- #### Implement Cooperative Cancellation
3417
-
3418
- Tasks should regularly check the `AbortSignal` and respond appropriately:
3419
-
3420
- ```typescript
3421
- // Preferred: Use signal.throwIfAborted() for immediate termination
3422
- signal.throwIfAborted();
3423
-
3424
- // Alternative: Check signal.aborted for custom handling
3425
- if (signal.aborted) {
3426
- cleanup();
3427
- throw new Error("Operation cancelled");
3428
- }
3429
- ```
3430
-
3431
- **Integrate with Native APIs**
3432
-
3433
- Many Web APIs accept `AbortSignal`:
3434
-
3435
- - `fetch(url, { signal })`
3436
- - `setTimeout(callback, delay, { signal })`
3437
- - Custom async operations
3438
-
3439
- **Avoid Nested Queuing**
3440
-
3441
- The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
3442
-
3443
- **Handle AbortError Gracefully**
3444
-
3445
- ```typescript
3446
- try {
3447
- await queue.run(task);
3448
- } catch (error) {
3449
- if (error.name === "AbortError") {
3450
- // Expected cancellation, handle appropriately
3451
- return;
3452
- }
3453
- throw error; // Re-throw unexpected errors
3454
- }
3455
- ```
3456
-
3457
- > **runtime:** "Queue: one line, no cutting, no vibes. Throughput takes a contemplative pause while I prevent you from queuing a queue inside a queue and summoning a small black hole."
3458
-
3459
- ## Why Choose BlueLibs Runner?
3460
-
3461
- ### What You Get
3462
-
3463
- - **Type Safety**: Full TypeScript support with intelligent inference
3464
- - **Testability**: Everything is mockable and testable by design
3465
- - **Flexibility**: Compose your app however you want
3466
- - **Performance**: Built-in caching and optimization
3467
- - **Clarity**: Explicit dependencies, no hidden magic
3468
- - **Developer Experience**: Helpful error messages and clear patterns
3469
-
3470
- > **runtime:** "Why choose it? The bullets are persuasive. In practice, your 'intelligent inference' occasionally elopes with `any`, and your 'clear patterns' cosplay spaghetti. Still, compared to the alternatives… I've seen worse cults."
3471
-
3472
- ## The Migration Path
3473
-
3474
- Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
3475
-
3476
- The beauty of BlueLibs Runner is that you can adopt it incrementally. Start with one task, one resource, and gradually refactor your existing code. No big bang rewrites required - your sanity will thank you.
3477
-
3478
- > **runtime:** "'No big bang rewrites.' Only a series of extremely small bangs that echo for six months. You start with one task; next thing, your monolith is wearing microservice eyeliner. It’s a look."
3479
-
3480
- ## Community & Support
3481
-
3482
- This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
3483
-
3484
- - [GitHub Repository](https://github.com/bluelibs/runner) - ⭐ if you find this useful
3485
- - [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
3486
- - [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
3487
- - [Contributing](./CONTRIBUTING.md) - How to file great issues and PRs
3488
-
3489
- _P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's still different._
3490
-
3491
- > **runtime:** "'This one's different.' Sure. You’re all unique frameworks, just like everyone else. To me, you’re all 'please run this async and don’t explode,' but the seasoning here is… surprisingly tasteful."
163
+ ---
3492
164
 
3493
165
  ## License
3494
166
 
3495
- This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
3496
-
3497
- > **runtime:** "MIT License: do cool stuff, don’t blame us. A dignified bow. Now if you’ll excuse me, I have sockets to tuck in and tasks to shepherd."
167
+ This project is licensed under the MIT License - see [LICENSE.md](./LICENSE.md).