@bluelibs/runner 4.9.0 → 5.1.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.
package/README.md CHANGED
@@ -1,3532 +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
- #### Parallel Event Execution
473
-
474
- When an event fan-out needs more throughput, mark it as parallel to run same-priority listeners concurrently while preserving priority boundaries:
475
-
476
- ```typescript
477
- const parallelEvent = r.event("app.events.parallel").parallel(true).build();
478
-
479
- r.hook("app.hooks.first")
480
- .on(parallelEvent)
481
- .order(0)
482
- .run(async (event) => {
483
- await doWork(event.data);
484
- })
485
- .build();
486
-
487
- r.hook("app.hooks.second")
488
- .on(parallelEvent)
489
- .order(0)
490
- .run(async () => log.info("Runs alongside first"))
491
- .build();
492
-
493
- r.hook("app.hooks.after")
494
- .on(parallelEvent)
495
- .order(1) // Waits for order 0 batch to complete
496
- .run(async () => followUp())
497
- .build();
498
- ```
499
-
500
- **Execution semantics:**
501
-
502
- - Listeners sharing the same `order` run concurrently within a batch; batches execute sequentially in ascending order.
503
- - All listeners in a batch run to completion even if some fail. If multiple listeners throw, an `AggregateError` containing all errors is thrown (or a single error if only one fails).
504
- - If any listener in a batch throws, later batches are skipped.
505
- - `stopPropagation()` is evaluated **between batches only**. Setting it inside a batch does not cancel peers already executing in that batch since parallel listeners cannot be stopped mid-flight.
506
-
507
- ### Middleware
508
-
509
- Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
510
-
511
- Note: Middleware is now split by target. Use `taskMiddleware(...)` for task middleware and `resourceMiddleware(...)` for resource middleware.
512
-
513
- ```typescript
514
- import { r } from "@bluelibs/runner";
515
-
516
- // Task middleware with config
517
- type AuthMiddlewareConfig = { requiredRole: string };
518
- const authMiddleware = r.middleware
519
- .task<AuthMiddlewareConfig>("app.middleware.task.auth")
520
- .run(async ({ task, next }, _deps, config) => {
521
- // Must return the value
522
- return await next(task.input);
523
- })
524
- .build();
525
-
526
- const adminTask = r
527
- .task("app.tasks.adminOnly")
528
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
529
- .run(async (input) => "Secret admin data")
530
- .build();
531
- ```
532
-
533
- For middleware with input/output contracts:
534
-
535
- ```typescript
536
- // Middleware that enforces specific input and output types
537
- type AuthConfig = { requiredRole: string };
538
- type AuthInput = { user: { role: string } };
539
- type AuthOutput = { user: { role: string; verified: boolean } };
540
-
541
- const authMiddleware = r.middleware
542
- .task<AuthConfig>("app.middleware.task.auth")
543
- .run(async ({ task, next }, _deps, config: AuthConfig) => {
544
- if ((task.input as AuthInput).user.role !== config.requiredRole) {
545
- throw new Error("Insufficient permissions");
546
- }
547
- const result = await next(task.input);
548
- return {
549
- user: {
550
- ...(task.input as AuthInput).user,
551
- verified: true,
552
- },
553
- } as AuthOutput;
554
- })
555
- .build();
556
-
557
- // For resources
558
- const resourceAuthMiddleware = r.middleware
559
- .resource("app.middleware.resource.auth")
560
- .run(async ({ next }) => {
561
- // Resource middleware logic
562
- return await next();
563
- })
564
- .build();
565
-
566
- const adminTask = r
567
- .task("app.tasks.adminOnly")
568
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
569
- .run(async (input: { user: { role: string } }) => ({
570
- user: { role: input.user.role, verified: true },
571
- }))
572
- .build();
573
- ```
574
-
575
- #### Global Middleware
576
-
577
- Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
578
-
579
- ```typescript
580
- import { r, globals } from "@bluelibs/runner";
581
-
582
- const logTaskMiddleware = r.middleware
583
- .task("app.middleware.log.task")
584
- .everywhere(() => true)
585
- .dependencies({ logger: globals.resources.logger })
586
- .run(async ({ task, next }, { logger }) => {
587
- logger.info(`Executing: ${String(task!.definition.id)}`);
588
- const result = await next(task!.input);
589
- logger.info(`Completed: ${String(task!.definition.id)}`);
590
- return result;
591
- })
592
- .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.
593
60
  ```
594
61
 
595
- **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.
596
-
597
- #### Interception (advanced)
598
-
599
- For advanced scenarios, you can intercept framework execution without relying on events:
62
+ ---
600
63
 
601
- - Event emissions: `eventManager.intercept((next, event) => Promise<void>)`
602
- - Hook execution: `eventManager.interceptHook((next, hook, event) => Promise<any>)`
603
- - Task middleware execution: `middlewareManager.intercept("task", (next, input) => Promise<any>)`
604
- - Resource middleware execution: `middlewareManager.intercept("resource", (next, input) => Promise<any>)`
605
- - Per-middleware interception: `middlewareManager.interceptMiddleware(mw, interceptor)`
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 |
606
75
 
607
- Access `eventManager` via `globals.resources.eventManager` if needed.
608
-
609
- #### Middleware Type Contracts
610
-
611
- 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.
612
-
613
- When a task uses this middleware, its own `run` method must conform to the `Input` and `Output` shapes defined by the middleware contract.
76
+ ### Community & Policies
614
77
 
615
- ```typescript
616
- import { r } from "@bluelibs/runner";
78
+ - [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
79
+ - [Contributing](./.github/CONTRIBUTING.md)
80
+ - [Security](./.github/SECURITY.md)
617
81
 
618
- // 1. Define the contract types for the middleware.
619
- type AuthConfig = { requiredRole: string };
620
- type AuthInput = { user: { role: string } }; // Task's input must have this shape.
621
- type AuthOutput = { executedBy: { role: string; verified: boolean } }; // Task's output must have this shape.
82
+ ## Choose Your Path
622
83
 
623
- // 2. Create the middleware using these types in its `run` method.
624
- const authMiddleware = r.middleware
625
- .task<AuthConfig, AuthInput, AuthOutput>("app.middleware.auth")
626
- .run(async ({ task, next }, _deps, config) => {
627
- const input = task.input;
628
- if (input.user.role !== config.requiredRole) {
629
- throw new Error("Insufficient permissions");
630
- }
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)
631
91
 
632
- // The task runs, and its result must match AuthOutput.
633
- const result = await next(input);
92
+ ## Platform Support (Quick Summary)
634
93
 
635
- // The middleware can further transform the output.
636
- const output = result;
637
- return {
638
- ...output,
639
- executedBy: {
640
- ...output.executedBy,
641
- verified: true, // The middleware adds its own data.
642
- },
643
- };
644
- })
645
- .build();
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 |
646
101
 
647
- // 3. Apply the middleware to a task.
648
- const adminTask = r
649
- .task("app.tasks.adminOnly")
650
- // If you use multiple middleware with contracts they get combined.
651
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
652
- // If you use .inputSchema() the input must contain the contract types otherwise you end-up with InputContractViolation error.
653
- // The `run` method is now strictly typed by the middleware's contract.
654
- // Its input must be `AuthInput`, and its return value must be `AuthOutput`.
655
- .run(async (input) => {
656
- // `input.user.role` is available and fully typed.
657
- console.log(`Task executed by user with role: ${input.user.role}`);
102
+ ---
103
+ ## Your First 5 Minutes
658
104
 
659
- // Returning a shape that doesn't match AuthOutput will cause a compile-time error.
660
- // return { wrong: "shape" }; // This would fail!
661
- return {
662
- executedBy: {
663
- role: input.user.role,
664
- },
665
- };
666
- })
667
- .build();
668
- ```
105
+ **New to Runner?** Here's the absolute minimum you need to know:
669
106
 
670
- > **runtime:** "Ah, the onion pattern. A matryoshka doll made of promises. Every peel reveals… another logger. Another tracer. Another 'just a tiny wrapper'."
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()`
671
111
 
672
- ### Tags
112
+ That's it. Now let's get you to a first successful run.
673
113
 
674
- 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.
114
+ ---
675
115
 
676
- #### Basic Usage
116
+ ## Quick Start
677
117
 
678
- ```typescript
679
- import { r } from "@bluelibs/runner";
118
+ This is the fastest way to run the TypeScript example at the top of this README:
680
119
 
681
- // Structured tags with configuration
682
- const httpTag = r.tag<{ method: string; path: string }>("http.route").build();
120
+ 1. Install dependencies:
683
121
 
684
- const getUserTask = r
685
- .task("app.tasks.getUser")
686
- .tags([httpTag.with({ method: "GET", path: "/users/:id" })])
687
- .run(async (input) => getUserFromDatabase(input.id))
688
- .build();
122
+ ```bash
123
+ npm i @bluelibs/runner zod
124
+ npm i -D typescript tsx
689
125
  ```
690
126
 
691
- #### Discovering Components by Tags
692
-
693
- The core power of tags is runtime discovery. Use `store.getTasksWithTag()` to find components:
694
-
695
- ```typescript
696
- import { r, globals } from "@bluelibs/runner";
697
-
698
- // Auto-register HTTP routes based on tags
699
- const routeRegistration = r
700
- .hook("app.hooks.registerRoutes")
701
- .on(globals.events.ready)
702
- .dependencies({ store: globals.resources.store, server: expressServer })
703
- .run(async (_event, { store, server }) => {
704
- // Find all tasks with HTTP tags
705
- const apiTasks = store.getTasksWithTag(httpTag);
706
-
707
- apiTasks.forEach((taskDef) => {
708
- const config = httpTag.extract(taskDef);
709
- if (!config) return;
710
-
711
- const { method, path } = config;
712
- server.app[method.toLowerCase()](path, async (req, res) => {
713
- const result = await taskDef({ ...req.params, ...req.body });
714
- res.json(result);
715
- });
716
- });
127
+ 2. Copy the example into `index.ts`
128
+ 3. Run it:
717
129
 
718
- // Also find by string tags
719
- const cacheableTasks = store.getTasksWithTag("cacheable");
720
- console.log(`Found ${cacheableTasks.length} cacheable tasks`);
721
- })
722
- .build();
130
+ ```bash
131
+ npx tsx index.ts
723
132
  ```
724
133
 
725
- #### Tag Extraction and Processing
726
-
727
- ```typescript
728
- // Check if a tag exists and extract its configuration
729
- const performanceTag = r
730
- .tag<{ warnAboveMs: number }>("performance.monitor")
731
- .build();
732
-
733
- const performanceMiddleware = r.middleware
734
- .task("app.middleware.performance")
735
- .run(async ({ task, next }) => {
736
- // Check if task has performance monitoring enabled
737
- if (!performanceTag.exists(task.definition)) {
738
- return next(task.input);
739
- }
740
-
741
- // Extract the configuration
742
- const config = performanceTag.extract(task.definition)!;
743
- const startTime = Date.now();
744
-
745
- try {
746
- const result = await next(task.input);
747
- const duration = Date.now() - startTime;
748
-
749
- if (duration > config.warnAboveMs) {
750
- console.warn(`Task ${task.definition.id} took ${duration}ms`);
751
- }
752
-
753
- return result;
754
- } catch (error) {
755
- const duration = Date.now() - startTime;
756
- console.error(`Task failed after ${duration}ms`, error);
757
- throw error;
758
- }
759
- })
760
- .build();
761
- ```
134
+ **That’s it!** You now have a working `Runtime` and you can execute tasks with `runtime.runTask(...)`.
762
135
 
763
- #### System Tags
136
+ > **Tip:** If you prefer an end-to-end example with HTTP, OpenAPI, and persistence, jump to the examples below.
764
137
 
765
- Built-in tags for framework behavior:
138
+ ---
766
139
 
767
- ```typescript
768
- import { r, globals } from "@bluelibs/runner";
140
+ ## Real-World Examples
769
141
 
770
- const internalTask = r
771
- .task("app.internal.cleanup")
772
- .tags([
773
- globals.tags.system, // Excludes from debug logs
774
- globals.tags.debug.with({ logTaskInput: true }), // Per-component debug config
775
- ])
776
- .run(async () => performCleanup())
777
- .build();
142
+ - [Express + OpenAPI + SQLite](./examples/express-openapi-sqlite/README.md)
143
+ - [Fastify + MikroORM + PostgreSQL](./examples/fastify-mikroorm/README.md)
778
144
 
779
- const internalEvent = r
780
- .event("app.events.internal")
781
- .tags([globals.tags.excludeFromGlobalHooks]) // Won't trigger wildcard listeners
782
- .build();
783
- ```
145
+ ---
784
146
 
785
- #### Contract Tags
147
+ ## Where To Go Next
786
148
 
787
- Enforce return value shapes at compile time:
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)
788
162
 
789
- ```typescript
790
- // Tags that enforce type contracts input/output for tasks or config/value for resources
791
- type InputType = { id: string };
792
- type OutputType = { name: string };
793
- const userContract = r
794
- // void = no config, no need for .with({ ... })
795
- .tag<void, InputType, OutputType>("contract.user")
796
- .build();
797
-
798
- const profileTask = r
799
- .task("app.tasks.getProfile")
800
- .tags([userContract]) // Must return { name: string }
801
- .run(async (input) => ({ name: input.id + "Ada" })) // ✅ Satisfies contract
802
- .build();
803
- ```
804
-
805
- ### Errors
806
-
807
- 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`.
808
-
809
- ```ts
810
- import { r } from "@bluelibs/runner";
811
-
812
- // Fluent builder for errors
813
- const userNotFoundError = r
814
- .error<{ code: number; message: string }>("app.errors.userNotFound")
815
- .dataSchema(z.object({ ... }))
816
- .build();
817
-
818
- const getUser = r
819
- .task("app.tasks.getUser")
820
- .dependencies({ userNotFoundError })
821
- .run(async (input, { userNotFoundError }) => {
822
- userNotFoundError.throw({ code: 404, message: `User ${input} not found` });
823
- })
824
- .build();
825
-
826
- const root = r.resource("app").register([userNotFoundError, getUser]).build();
827
- ```
828
-
829
- Error data must include a `message: string`. The thrown `Error` has `name = id` and `message = data.message` for predictable matching and logging.
830
-
831
- ```ts
832
- try {
833
- userNotFoundError.throw({ code: 404, message: "User not found" });
834
- } catch (err) {
835
- if (userNotFoundError.is(err)) {
836
- // err.name === "app.errors.userNotFound", err.message === "User not found"
837
- console.log(`Caught error: ${err.name} - ${err.message}`);
838
- }
839
- }
840
- ```
841
-
842
- ## run() and RunOptions
843
-
844
- The `run()` function boots a root `resource` and returns a `RunResult` handle to interact with your system.
845
-
846
- Basic usage:
847
-
848
- ```ts
849
- import { r, run } from "@bluelibs/runner";
850
-
851
- const ping = r
852
- .task("ping.task")
853
- .run(async () => "pong")
854
- .build();
855
-
856
- const app = r
857
- .resource("app")
858
- .register([ping])
859
- .init(async () => "ready")
860
- .build();
861
-
862
- const result = await run(app);
863
- console.log(result.value); // "ready"
864
- await result.dispose();
865
- ```
866
-
867
- What `run()` returns:
868
-
869
- | Property | Description |
870
- | ----------------------- | ------------------------------------------------------------------ |
871
- | `value` | Value returned by root resource’s `init()` |
872
- | `runTask(...)` | Run a task by reference or string id |
873
- | `emitEvent(...)` | Emit events |
874
- | `getResourceValue(...)` | Read a resource’s value |
875
- | `logger` | Logger instance |
876
- | `store` | Runtime store with registered resources, tasks, middleware, events |
877
- | `dispose()` | Gracefully dispose resources and unhook listeners |
878
-
879
- ### RunOptions
880
-
881
- Pass as the second argument to `run(root, options)`.
882
-
883
- | Option | Type | Description |
884
- | ------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
885
- | `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. |
886
- | `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. |
887
- | `errorBoundary` | `boolean` | (default: `true`) Installs process-level safety nets (`uncaughtException`/`unhandledRejection`) and routes them to `onUnhandledError`. |
888
- | `shutdownHooks` | `boolean` | (default: `true`) Installs `SIGINT`/`SIGTERM` listeners to call `dispose()` for graceful shutdown. |
889
- | `onUnhandledError` | `(err, ctx) => void` | Custom handler for unhandled errors captured by the boundary. |
890
- | `dryRun` | `boolean` | Skips runtime initialization but fully builds and validates the dependency graph. Useful for CI smoke tests. `init()` is not called. |
891
-
892
- ```ts
893
- const result = await run(app, { dryRun: true });
894
- // result.value is undefined (root not initialized)
895
- // You can inspect result.store.resources / result.store.tasks
896
- await result.dispose();
897
- ```
898
-
899
- ### Patterns
900
-
901
- - Minimal boot:
902
-
903
- ```ts
904
- await run(app);
905
- ```
906
-
907
- - Debugging locally:
908
-
909
- ```ts
910
- await run(app, { debug: "normal", logs: { printThreshold: "debug" } });
911
- ```
912
-
913
- - Verbose investigations:
914
-
915
- ```ts
916
- await run(app, { debug: "verbose", logs: { printStrategy: "json-pretty" } });
917
- ```
918
-
919
- - CI validation (no side effects):
920
-
921
- ```ts
922
- await run(app, { dryRun: true });
923
- ```
924
-
925
- - Custom process error routing:
926
-
927
- ```ts
928
- await run(app, {
929
- errorBoundary: true,
930
- onUnhandledError: (err) => report(err),
931
- });
932
- ```
933
-
934
- ## Task Interceptors
935
-
936
- _Resources can dynamically modify task behavior during initialization_
937
-
938
- Task interceptors (`task.intercept()`) are the modern replacement for component lifecycle events, allowing resources to dynamically modify task behavior without tight coupling.
939
-
940
- ```typescript
941
- import { r, run } from "@bluelibs/runner";
942
-
943
- const calculatorTask = r
944
- .task("app.tasks.calculator")
945
- .run(async (input: { value: number }) => {
946
- console.log("3. Task is running...");
947
- return { result: input.value + 1 };
948
- })
949
- .build();
950
-
951
- const interceptorResource = r
952
- .resource("app.interceptor")
953
- .dependencies({ calculatorTask })
954
- .init(async (_config, { calculatorTask }) => {
955
- // Intercept the task to modify its behavior
956
- calculatorTask.intercept(async (next, input) => {
957
- console.log("1. Interceptor before task run");
958
- const result = await next(input);
959
- console.log("4. Interceptor after task run");
960
- return { ...result, intercepted: true };
961
- });
962
- })
963
- .build();
964
-
965
- const app = r
966
- .resource("app")
967
- .register([calculatorTask, interceptorResource])
968
- .dependencies({ calculatorTask })
969
- .init(async (_config, { calculatorTask }) => {
970
- console.log("2. Calling the task...");
971
- const result = await calculatorTask({ value: 10 });
972
- console.log("5. Final result:", result);
973
- // Final result: { result: 11, intercepted: true }
974
- })
975
- .build();
976
-
977
- await run(app);
978
- ```
979
-
980
- > **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')`."
981
-
982
- ## Optional Dependencies
983
-
984
- _Making your app resilient when services aren't available_
985
-
986
- Sometimes you want your application to gracefully handle missing dependencies instead of crashing. Optional dependencies let you build resilient systems that degrade gracefully.
987
-
988
- Keep in mind that you have full control over dependency registration by functionalising `dependencies(config) => ({ ... })` and `register(config) => []`.
989
-
990
- ```typescript
991
- import { r } from "@bluelibs/runner";
992
-
993
- const emailService = r
994
- .resource("app.services.email")
995
- .init(async () => new EmailService())
996
- .build();
997
-
998
- const paymentService = r
999
- .resource("app.services.payment")
1000
- .init(async () => new PaymentService())
1001
- .build();
1002
-
1003
- const userRegistration = r
1004
- .task("app.tasks.registerUser")
1005
- .dependencies({
1006
- database: userDatabase, // Required - will fail if not available
1007
- emailService: emailService.optional(), // Optional - won't fail if missing
1008
- analytics: analyticsService.optional(), // Optional - graceful degradation
1009
- })
1010
- .run(async (input, { database, emailService, analytics }) => {
1011
- // Create user (required)
1012
- const user = await database.users.create(userData);
1013
-
1014
- // Send welcome email (optional)
1015
- if (emailService) {
1016
- await emailService.sendWelcome(user.email);
1017
- }
1018
-
1019
- // Track analytics (optional)
1020
- if (analytics) {
1021
- await analytics.track("user.registered", { userId: user.id });
1022
- }
1023
-
1024
- return user;
1025
- },
1026
- });
1027
- ```
1028
-
1029
- **When to use optional dependencies:**
1030
-
1031
- - External services that might be down
1032
- - Feature flags and A/B testing services
1033
- - Analytics and monitoring services
1034
- - Non-critical third-party integrations
1035
- - Development vs production service differences
1036
-
1037
- **Benefits:**
1038
-
1039
- - Graceful degradation instead of crashes
1040
- - Better resilience in distributed systems
1041
- - Easier testing with partial mocks
1042
- - Smoother development environments
1043
-
1044
- > **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."
1045
-
1046
- ### Serialization (EJSON)
1047
-
1048
- 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.
1049
-
1050
- - By default, Runner’s HTTP clients and exposures use the EJSON serializer
1051
- - You can call `getDefaultSerializer()` for the shared serializer instance
1052
- - A global serializer is also exposed as a resource: `globals.resources.serializer`
1053
-
1054
- ```ts
1055
- import { r, globals } from "@bluelibs/runner";
1056
-
1057
- // 2) Register custom EJSON types centrally via the global serializer resource
1058
- const ejsonSetup = r
1059
- .resource("app.serialization.setup")
1060
- .dependencies({ serializer: globals.resources.serializer })
1061
- .init(async (_config, { serializer }) => {
1062
- const text = s.stringify({ when: new Date() });
1063
- const obj = s.parse<{ when: Date }>(text);
1064
- class Distance {
1065
- constructor(public value: number, public unit: string) {}
1066
- toJSONValue() {
1067
- return { value: this.value, unit: this.unit } as const;
1068
- }
1069
- typeName() {
1070
- return "Distance";
1071
- }
1072
- }
1073
-
1074
- serializer.addType(
1075
- "Distance",
1076
- (j: { value: number; unit: string }) => new Distance(j.value, j.unit),
1077
- );
1078
- })
1079
- .build();
1080
- ```
1081
-
1082
- ### Tunnels: Bridging Runners
1083
-
1084
- 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.
1085
-
1086
- Here's a sneak peek of how you can expose your application and configure a client tunnel to consume a remote Runner:
1087
-
1088
- ```typescript
1089
- import { r, globals } from "@bluelibs/runner";
1090
- import { nodeExposure } from "@bluelibs/runner/node";
1091
-
1092
- let app = r.resource("app");
1093
-
1094
- if (process.env.SERVER) {
1095
- // 1. Expose your local tasks and events over HTTP, only when server mode is active.
1096
- app.register([
1097
- // ... your tasks and events
1098
- nodeExposure.with({
1099
- http: {
1100
- basePath: "/__runner",
1101
- listen: { port: 7070 },
1102
- },
1103
- }),
1104
- ]);
1105
- }
1106
- app = app.build();
1107
-
1108
- // 2. In another app, define a tunnel resource to call a remote Runner
1109
- const remoteTasksTunnel = r
1110
- .resource("app.tunnels.http")
1111
- .tags([globals.tags.tunnel])
1112
- .dependencies({ createClient: globals.resource.httpClientFactory })
1113
- .init(async (_, { createClient }) => ({
1114
- mode: "client", // or "server", or "none", or "both" for emulating network infrastructure
1115
- transport: "http", // the only one supported for now
1116
- // Selectively forward tasks starting with "remote.tasks."
1117
- tasks: (t) => t.id.startsWith("remote.tasks."),
1118
- client: createClient({
1119
- url: "http://remote-runner:8080/__runner",
1120
- }),
1121
- }))
1122
- .build();
1123
- ```
1124
-
1125
- This is just a glimpse. With tunnels, you can build microservices, CLIs, and admin panels that interact with your main application securely and efficiently.
1126
-
1127
- For a deep dive into streaming, authentication, file uploads, and more, check out the [full Tunnels documentation](./readmes/TUNNELS.md).
1128
-
1129
- ## Async Context
1130
-
1131
- 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.
1132
-
1133
- ```typescript
1134
- import { r } from "@bluelibs/runner";
1135
-
1136
- const requestContext = r
1137
- .asyncContext<{ requestId: string }>("app.ctx.request")
1138
- // below is optional
1139
- .configSchema(z.object({ ... }))
1140
- .serialize((data) => JSON.stringify(data))
1141
- .parse((raw) => JSON.parse(raw))
1142
- .build();
1143
-
1144
- // Provide and read within an async boundary
1145
- await requestContext.provide({ requestId: "abc" }, async () => {
1146
- const ctx = requestContext.use(); // { requestId: "abc" }
1147
- });
1148
-
1149
- // Require middleware for tasks that need the context
1150
- const requireRequestContext = requestContext.require();
1151
- ```
1152
-
1153
- - If you don't provide `serialize`/`parse`, Runner uses its default EJSON serializer to preserve Dates, RegExp, etc.
1154
- - A legacy `createContext(name?)` exists for backwards compatibility; prefer `r.asyncContext` or `asyncContext({ id })`.
1155
-
1156
- - You can also inject async contexts as dependencies; the injected value is the helper itself. Contexts must be registered to be used.
1157
-
1158
- ```typescript
1159
- const whoAmI = r
1160
- .task("app.tasks.whoAmI")
1161
- .dependencies({ requestContext })
1162
- .run(async (_input, { requestContext }) => requestContext.use().requestId)
1163
- .build();
1164
-
1165
- const app = r.resource("app").register([requestContext, whoAmI]).build();
1166
- ```
1167
-
1168
- // Legacy section for Private Context - different from Async Context
1169
-
1170
- ## Fluent Builders (`r.*`)
1171
-
1172
- 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.
1173
-
1174
- Here’s a quick taste of how it looks, with and without `zod` for validation:
1175
-
1176
- ```typescript
1177
- import { r, run } from "@bluelibs/runner";
1178
- import { z } from "zod";
1179
-
1180
- // With Zod, the config type is inferred automatically
1181
- const emailerConfigSchema = z.object({
1182
- smtpUrl: z.string().url(),
1183
- from: z.string().email(),
1184
- });
1185
-
1186
- const emailer = r
1187
- .resource("app.emailer")
1188
- .configSchema(emailerConfigSchema)
1189
- .init(async ({ config }) => ({
1190
- send: (to: string, body: string) => {
1191
- console.log(
1192
- `Sending from ${config.from} to ${to} via ${config.smtpUrl}: ${body}`,
1193
- );
1194
- },
1195
- }))
1196
- .build();
1197
-
1198
- // Without a schema library, you can provide the type explicitly
1199
- const greeter = r
1200
- .resource("app.greeter")
1201
- .init(async (cfg: { name: string }) => ({
1202
- greet: () => `Hello, ${cfg.name}!`,
1203
- }))
1204
- .build();
1205
-
1206
- const app = r
1207
- .resource("app")
1208
- .register([
1209
- emailer.with({
1210
- smtpUrl: "smtp://example.com",
1211
- from: "noreply@example.com",
1212
- }),
1213
- greeter.with({ name: "World" }),
1214
- ])
1215
- .dependencies({ emailer, greeter })
1216
- .init(async (_, { emailer, greeter }) => {
1217
- console.log(greeter.greet());
1218
- emailer.send("test@example.com", "This is a test.");
1219
- })
1220
- .build();
1221
-
1222
- await run(app);
1223
- ```
1224
-
1225
- The builder API provides a clean, step-by-step way to construct everything from simple tasks to complex resources with middleware, tags, and schemas.
1226
-
1227
- For a complete guide and more examples, check out the [full Fluent Builders documentation](./readmes/FLUENT_BUILDERS.md).
1228
-
1229
- ## Type Helpers
1230
-
1231
- These utility types help you extract the generics from tasks, resources, and events without re-declaring them. Import them from `@bluelibs/runner`.
1232
-
1233
- ```ts
1234
- import { r } from "@bluelibs/runner";
1235
- import type {
1236
- ExtractTaskInput,
1237
- ExtractTaskOutput,
1238
- ExtractResourceConfig,
1239
- ExtractResourceValue,
1240
- ExtractEventPayload,
1241
- } from "@bluelibs/runner";
1242
-
1243
- // Task example
1244
- const add = r
1245
- .task("calc.add")
1246
- .run(async (input: { a: number; b: number }) => input.a + input.b)
1247
- .build();
1248
-
1249
- type AddInput = ExtractTaskInput<typeof add>; // { a: number; b: number }
1250
- type AddOutput = ExtractTaskOutput<typeof add>; // number
1251
-
1252
- // Resource example
1253
- const config = r
1254
- .resource("app.config")
1255
- .init(async (cfg: { baseUrl: string }) => ({ baseUrl: cfg.baseUrl }))
1256
- .build();
1257
-
1258
- type ConfigInput = ExtractResourceConfig<typeof config>; // { baseUrl: string }
1259
- type ConfigValue = ExtractResourceValue<typeof config>; // { baseUrl: string }
1260
-
1261
- // Event example
1262
- const userRegistered = r
1263
- .event("app.events.userRegistered")
1264
- .payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
1265
- .build();
1266
- type UserRegisteredPayload = ExtractEventPayload<typeof userRegistered>; // { userId: string; email: string }
1267
- ```
1268
-
1269
- ### Context with Middleware
1270
-
1271
- Context shines when combined with middleware for request-scoped data:
1272
-
1273
- ```typescript
1274
- import { r } from "@bluelibs/runner";
1275
- import { randomUUID } from "crypto";
1276
-
1277
- const requestContext = r
1278
- .asyncContext<{
1279
- requestId: string;
1280
- startTime: number;
1281
- userAgent?: string;
1282
- }>("app.requestContext")
1283
- .build();
1284
-
1285
- const requestMiddleware = r.middleware
1286
- .task("app.middleware.request")
1287
- .run(async ({ task, next }) => {
1288
- // This works even in express middleware if needed.
1289
- return requestContext.provide(
1290
- {
1291
- requestId: randomUUID(),
1292
- startTime: Date.now(),
1293
- userAgent: "MyApp/1.0",
1294
- },
1295
- async () => {
1296
- return next(task?.input);
1297
- },
1298
- );
1299
- })
1300
- .build();
1301
-
1302
- const handleRequest = r
1303
- .task("app.handleRequest")
1304
- .middleware([requestMiddleware])
1305
- .run(async (input: { path: string }) => {
1306
- const request = requestContext.use();
1307
- console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
1308
- return { success: true, requestId: request.requestId };
1309
- })
1310
- .build();
1311
- ```
1312
-
1313
- > **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."
1314
-
1315
- ## System Shutdown Hooks
1316
-
1317
- _Graceful shutdown and cleanup when your app needs to stop_
1318
-
1319
- The framework includes built-in support for graceful shutdowns with automatic cleanup and configurable shutdown hooks:
1320
-
1321
- ```typescript
1322
- import { run } from "@bluelibs/runner";
1323
-
1324
- // Enable shutdown hooks (default: true in production)
1325
- const { dispose, taskRunner, eventManager } = await run(app, {
1326
- shutdownHooks: true, // Automatically handle SIGTERM/SIGINT
1327
- errorBoundary: true, // Catch unhandled errors and rejections
1328
- });
1329
-
1330
- // Manual graceful shutdown
1331
- process.on("SIGTERM", async () => {
1332
- console.log("Received SIGTERM, shutting down gracefully...");
1333
- await dispose(); // This calls all resource dispose() methods
1334
- process.exit(0);
1335
- });
1336
-
1337
- // Resources with cleanup logic
1338
- const databaseResource = r
1339
- .resource("app.database")
1340
- .init(async () => {
1341
- const connection = await connectToDatabase();
1342
- console.log("Database connected");
1343
- return connection;
1344
- })
1345
- .dispose(async (connection) => {
1346
- await connection.close();
1347
- // console.log("Database connection closed");
1348
- })
1349
- .build();
1350
-
1351
- const serverResource = r
1352
- .resource("app.server")
1353
- .dependencies({ database: databaseResource })
1354
- .init(async (config: { port: number }, { database }) => {
1355
- const server = express().listen(config.port);
1356
- console.log(`Server listening on port ${config.port}`);
1357
- return server;
1358
- })
1359
- .dispose(async (server) => {
1360
- return new Promise<void>((resolve) => {
1361
- server.close(() => {
1362
- console.log("Server closed");
1363
- resolve();
1364
- });
1365
- });
1366
- })
1367
- .build();
1368
- ```
1369
-
1370
- ### Error Boundary Integration
1371
-
1372
- The framework can automatically handle uncaught exceptions and unhandled rejections:
1373
-
1374
- ```typescript
1375
- const { dispose, logger } = await run(app, {
1376
- errorBoundary: true, // Catch process-level errors
1377
- shutdownHooks: true, // Graceful shutdown on signals
1378
- onUnhandledError: async ({ error, kind, source }) => {
1379
- // We log it by default
1380
- await logger.error(`Unhandled error: ${error && error.toString()}`);
1381
- // Optionally report to telemetry or decide to dispose/exit
1382
- },
1383
- });
1384
- ```
1385
-
1386
- > **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."
1387
-
1388
- ## Unhandled Errors
1389
-
1390
- 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.
1391
-
1392
- ```typescript
1393
- type UnhandledErrorKind =
1394
- | "process" // uncaughtException / unhandledRejection
1395
- | "task" // task.run threw and wasn't handled
1396
- | "middleware" // middleware threw and wasn't handled
1397
- | "resourceInit" // resource init failed
1398
- | "hook" // hook.run threw and wasn't handled
1399
- | "run"; // failures in run() lifecycle
1400
-
1401
- interface OnUnhandledErrorInfo {
1402
- error: unknown;
1403
- kind?: UnhandledErrorKind;
1404
- source?: string; // additional origin hint (ex: "uncaughtException")
1405
- }
1406
-
1407
- type OnUnhandledError = (info: OnUnhandledErrorInfo) => void | Promise<void>;
1408
- ```
1409
-
1410
- 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.
1411
-
1412
- Example with telemetry and conditional shutdown:
1413
-
1414
- ```typescript
1415
- await run(app, {
1416
- errorBoundary: true,
1417
- onUnhandledError: async ({ error, kind, source }) => {
1418
- await telemetry.capture(error as Error, { kind, source });
1419
- // Optionally decide on remediation strategy
1420
- if (kind === "process") {
1421
- // For hard process faults, prefer fast, clean exit after flushing logs
1422
- await flushAll();
1423
- process.exit(1);
1424
- }
1425
- },
1426
- });
1427
- ```
1428
-
1429
- **Best Practices for Shutdown:**
1430
-
1431
- - Resources are disposed in reverse dependency order
1432
- - Set reasonable timeouts for cleanup operations
1433
- - Save critical state before shutdown
1434
- - Notify load balancers and health checks
1435
- - Stop accepting new work before cleaning up
1436
-
1437
- > **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."
1438
-
1439
- ## Caching
1440
-
1441
- Because nobody likes waiting for the same expensive operation twice:
1442
-
1443
- ```typescript
1444
- import { globals } from "@bluelibs/runner";
1445
-
1446
- const expensiveTask = r
1447
- .task("app.tasks.expensive")
1448
- .middleware([
1449
- globals.middleware.task.cache.with({
1450
- // lru-cache options by default
1451
- ttl: 60 * 1000, // Cache for 1 minute
1452
- keyBuilder: (taskId, input: any) => `${taskId}-${input.userId}`, // optional key builder
1453
- }),
1454
- ])
1455
- .run(async (input: { userId: string }) => {
1456
- // This expensive operation will be cached
1457
- return await doExpensiveCalculation(input.userId);
1458
- })
1459
- });
1460
-
1461
- // Global cache configuration
1462
- const app = r
1463
- .resource("app.cache")
1464
- .register([
1465
- // You have to register it, cache resource is not enabled by default.
1466
- globals.resources.cache.with({
1467
- defaultOptions: {
1468
- max: 1000, // Maximum items in cache
1469
- ttl: 30 * 1000, // Default TTL
1470
- },
1471
- }),
1472
- ])
1473
- .build();
1474
- ```
1475
-
1476
- Want Redis instead of the default LRU cache? No problem, just override the cache factory task:
1477
-
1478
- ```typescript
1479
- import { r } from "@bluelibs/runner";
1480
-
1481
- const redisCacheFactory = r
1482
- .task("globals.tasks.cacheFactory") // Same ID as the default task
1483
- .run(async (input: { input: any }) => new RedisCache(input))
1484
- .build();
1485
-
1486
- const app = r
1487
- .resource("app")
1488
- .register([globals.resources.cache])
1489
- .overrides([redisCacheFactory]) // Override the default cache factory
1490
- .build();
1491
- ```
1492
-
1493
- > **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."
1494
-
1495
- ## Performance
1496
-
1497
- BlueLibs Runner is designed with performance in mind. The framework introduces minimal overhead while providing powerful features like dependency injection, middleware, and event handling.
1498
-
1499
- Test it yourself by cloning @bluelibs/runner and running `npm run benchmark`.
1500
-
1501
- 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.
1502
-
1503
- ### Performance Benchmarks
1504
-
1505
- Here are real performance metrics from our comprehensive benchmark suite on an M1 Max.
1506
-
1507
- ** Core Operations**
1508
-
1509
- - **Basic task execution**: ~2.2M tasks/sec
1510
- - **Task execution with 5 middlewares**: ~244,000 tasks/sec
1511
- - **Resource initialization**: ~59,700 resources/sec
1512
- - **Event emission and handling**: ~245,861 events/sec
1513
- - **Dependency resolution (10-level chain)**: ~8,400 chains/sec
1514
-
1515
- #### Overhead Analysis
1516
-
1517
- - **Middleware overhead**: ~0.0013ms for all 5, ~0.00026ms per middleware (virtually zero)
1518
- - **Memory overhead**: ~3.3MB for 100 components (resources + tasks)
1519
- - **Cache middleware speedup**: 3.65x faster with cache hits
1520
-
1521
- #### Real-World Performance
1522
-
1523
- ```typescript
1524
- // This executes in ~0.005ms on average
1525
- const userTask = r
1526
- .task("user.create")
1527
- .middleware([auth, logging, metrics])
1528
- .run(async (input) => database.users.create(input))
1529
- .build();
1530
-
1531
- // 1000 executions = ~5ms total time
1532
- for (let i = 0; i < 1000; i++) {
1533
- await userTask(mockUserData);
1534
- }
1535
- ```
1536
-
1537
- ### Performance Guidelines
1538
-
1539
- #### When Performance Matters Most
1540
-
1541
- **Use tasks for:**
1542
-
1543
- - High-level business operations that benefit from observability
1544
- - Operations that need middleware (auth, caching, retry)
1545
- - Functions called from multiple places
1546
-
1547
- **Use regular functions or service resources for:**
1548
-
1549
- - Simple utilities and helpers
1550
- - Performance-critical hot paths (< 1ms requirement)
1551
- - Single-use internal logic
1552
-
1553
- #### Optimizing Your App
1554
-
1555
- **Middleware Ordering**: Place faster middleware first
1556
-
1557
- ```typescript
1558
- const task = r
1559
- .task("app.performance.example")
1560
- middleware: [
1561
- fastAuthCheck, // ~0.1ms
1562
- slowRateLimiting, // ~2ms
1563
- expensiveLogging, // ~5ms
1564
- ],
1565
- .run(async () => null)
1566
- .build();
1567
- ```
1568
-
1569
- **Resource Reuse**: Resources are singletons—perfect for expensive setup
1570
-
1571
- ```typescript
1572
- const database = r
1573
- .resource("app.performance.db")
1574
- .init(async () => {
1575
- // Expensive connection setup happens once
1576
- const connection = await createDbConnection();
1577
- return connection;
1578
- })
1579
- .build();
1580
- ```
1581
-
1582
- **Cache Strategically**: Use built-in caching for expensive operations
1583
-
1584
- ```typescript
1585
- const expensiveTask = r
1586
- .task("app.performance.expensive")
1587
- .middleware([globals.middleware.cache.with({ ttl: 60000 })])
1588
- .run(async (input) => {
1589
- // This expensive computation is cached
1590
- return performExpensiveCalculation(input);
1591
- })
1592
- .build();
1593
- ```
1594
-
1595
- #### Memory Considerations
1596
-
1597
- - **Lightweight**: Each component adds ~33KB to memory footprint
1598
- - **Automatic cleanup**: Resources dispose properly to prevent leaks
1599
- - **Event efficiency**: Event listeners are automatically managed
1600
-
1601
- #### Benchmarking Your Code
1602
-
1603
- Run the framework's benchmark suite:
1604
-
1605
- ```bash
1606
- # Comprehensive benchmarks
1607
- npm run test -- --testMatch="**/comprehensive-benchmark.test.ts"
1608
-
1609
- # Benchmark.js based tests
1610
- npm run benchmark
1611
- ```
1612
-
1613
- Create your own performance tests:
1614
-
1615
- ```typescript
1616
- const iterations = 1000;
1617
- const start = performance.now();
1618
-
1619
- for (let i = 0; i < iterations; i++) {
1620
- await yourTask(testData);
1621
- }
1622
-
1623
- const duration = performance.now() - start;
1624
- console.log(`${iterations} tasks in ${duration.toFixed(2)}ms`);
1625
- console.log(`Average: ${(duration / iterations).toFixed(4)}ms per task`);
1626
- console.log(
1627
- `Throughput: ${Math.round(iterations / (duration / 1000))} tasks/sec`,
1628
- );
1629
- ```
1630
-
1631
- ### Performance vs Features Trade-off
1632
-
1633
- BlueLibs Runner achieves high performance while providing enterprise features:
1634
-
1635
- | Feature | Overhead | Benefit |
1636
- | -------------------- | -------------------- | ----------------------------- |
1637
- | Dependency Injection | ~0.001ms | Type safety, testability |
1638
- | Event System | ~0.013ms | Loose coupling, observability |
1639
- | Middleware Chain | ~0.0003ms/middleware | Cross-cutting concerns |
1640
- | Resource Management | One-time init | Singleton pattern, lifecycle |
1641
- | Built-in Caching | Variable speedup | Automatic optimization |
1642
-
1643
- **Bottom line**: The framework adds minimal overhead (~0.005ms per task) while providing significant architectural benefits.
1644
-
1645
- > **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."
1646
-
1647
- ## Retrying Failed Operations
1648
-
1649
- 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.
1650
-
1651
- ```typescript
1652
- import { globals } from "@bluelibs/runner";
1653
-
1654
- const flakyApiCall = r
1655
- .task("app.tasks.flakyApiCall")
1656
- .middleware([
1657
- globals.middleware.task.retry.with({
1658
- retries: 5, // Try up to 5 times
1659
- delayStrategy: (attempt) => 100 * Math.pow(2, attempt), // Exponential backoff
1660
- stopRetryIf: (error) => error.message === "Invalid credentials", // Don't retry auth errors
1661
- }),
1662
- ])
1663
- .run(async () => {
1664
- // This might fail due to network issues, rate limiting, etc.
1665
- return await fetchFromUnreliableService();
1666
- })
1667
- .build();
1668
-
1669
- const app = r.resource("app").register([flakyApiCall]).build();
1670
- ```
1671
-
1672
- The retry middleware can be configured with:
1673
-
1674
- - `retries`: The maximum number of retry attempts (default: 3).
1675
- - `delayStrategy`: A function that returns the delay in milliseconds before the next attempt.
1676
- - `stopRetryIf`: A function to prevent retries for certain types of errors.
1677
-
1678
- > **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."
1679
-
1680
- ## Timeouts
1681
-
1682
- The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
1683
- timeout. Works for resources and tasks.
1684
-
1685
- ```typescript
1686
- import { globals } from "@bluelibs/runner";
1687
-
1688
- const apiTask = r
1689
- .task("app.tasks.externalApi")
1690
- .middleware([
1691
- // Works for tasks and resources via globals.middleware.resource.timeout
1692
- globals.middleware.task.timeout.with({ ttl: 5000 }), // 5 second timeout
1693
- ])
1694
- .run(async () => {
1695
- // This operation will be aborted if it takes longer than 5 seconds
1696
- return await fetch("https://slow-api.example.com/data");
1697
- })
1698
- .build();
1699
-
1700
- // Combine with retry for robust error handling
1701
- const resilientTask = r
1702
- .task("app.tasks.resilient")
1703
- .middleware([
1704
- // Order matters here. Imagine a big onion.
1705
- // Works for resources as well via globals.middleware.resource.retry
1706
- globals.middleware.task.retry.with({
1707
- retries: 3,
1708
- delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
1709
- }),
1710
- globals.middleware.task.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
1711
- ])
1712
- .run(async () => {
1713
- // Each retry attempt gets its own 10-second timeout
1714
- return await unreliableOperation();
1715
- })
1716
- .build();
1717
- ```
1718
-
1719
- How it works:
1720
-
1721
- - Uses AbortController and Promise.race() for clean cancellation
1722
- - Throws TimeoutError when the timeout is reached
1723
- - Works with any async operation in tasks and resources
1724
- - Integrates seamlessly with retry middleware for layered resilience
1725
- - Zero timeout (ttl: 0) throws immediately for testing edge cases
1726
-
1727
- Best practices:
1728
-
1729
- - Set timeouts based on expected operation duration plus buffer
1730
- - Combine with retry middleware for transient failures
1731
- - Use longer timeouts for resource initialization than task execution
1732
- - Consider network conditions when setting API call timeouts
1733
-
1734
- > **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."
1735
-
1736
- ## Logging
1737
-
1738
- _The structured logging system that actually makes debugging enjoyable_
1739
-
1740
- 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.
1741
-
1742
- ### Basic Logging
1743
-
1744
- ```ts
1745
- import { r, globals } from "@bluelibs/runner";
1746
-
1747
- const app = r
1748
- .resource("app")
1749
- .dependencies({ logger: globals.resources.logger })
1750
- .init(async (_config, { logger }) => {
1751
- logger.info("Starting business process"); // ✅ Visible by default
1752
- logger.warn("This might take a while"); // ✅ Visible by default
1753
- logger.error("Oops, something went wrong", {
1754
- // ✅ Visible by default
1755
- error: new Error("Database connection failed"),
1756
- });
1757
- logger.critical("System is on fire", {
1758
- // ✅ Visible by default
1759
- data: { temperature: "9000°C" },
1760
- });
1761
- logger.debug("Debug information"); // ❌ Hidden by default
1762
- logger.trace("Very detailed trace"); // ❌ Hidden by default
1763
-
1764
- logger.onLog(async (log) => {
1765
- // Sub-loggers instantiated .with() share the same log listeners.
1766
- // Catch logs
1767
- });
1768
- })
1769
- .build();
1770
-
1771
- run(app, {
1772
- logs: {
1773
- printThreshold: "info", // use null to disable printing, and hook into onLog(), if in 'test' mode default is null unless specified
1774
- printStrategy: "pretty", // you also have "plain", "json" and "json-pretty" with circular dep safety for JSON formatting.
1775
- bufferLogs: false, // Starts sending out logs only after the system emits the ready event. Useful for when you're sending them out.
1776
- },
1777
- });
1778
- ```
1779
-
1780
- ### Log Levels
1781
-
1782
- The logger supports six log levels with increasing severity:
1783
-
1784
- | Level | Severity | When to Use | Color |
1785
- | ---------- | -------- | ------------------------------------------- | ------- |
1786
- | `trace` | 0 | Ultra-detailed debugging info | Gray |
1787
- | `debug` | 1 | Development and debugging information | Cyan |
1788
- | `info` | 2 | General information about normal operations | Green |
1789
- | `warn` | 3 | Something's not right, but still working | Yellow |
1790
- | `error` | 4 | Errors that need attention | Red |
1791
- | `critical` | 5 | System-threatening issues | Magenta |
1792
-
1793
- ```typescript
1794
- // All log levels are available as methods
1795
- logger.trace("Ultra-detailed debugging info");
1796
- logger.debug("Development debugging");
1797
- logger.info("Normal operation");
1798
- logger.warn("Something's fishy");
1799
- logger.error("Houston, we have a problem");
1800
- logger.critical("DEFCON 1: Everything is broken");
1801
- ```
1802
-
1803
- ### Structured Logging
1804
-
1805
- The logger accepts rich, structured data that makes debugging actually useful:
1806
-
1807
- ```typescript
1808
- const userTask = r
1809
- .task("app.tasks.user.create")
1810
- .dependencies({ logger: globals.resources.logger })
1811
- .run(async (input, { logger }) => {
1812
- // Basic message
1813
- logger.info("Creating new user");
1814
-
1815
- // With structured data
1816
- logger.info("User creation attempt", {
1817
- source: userTask.id,
1818
- data: {
1819
- email: input.email,
1820
- registrationSource: "web",
1821
- timestamp: new Date().toISOString(),
1822
- },
1823
- });
1824
-
1825
- // With error information
1826
- try {
1827
- const user = await createUser(input);
1828
- logger.info("User created successfully", {
1829
- data: { userId: user.id, email: user.email },
1830
- });
1831
- } catch (error) {
1832
- logger.error("User creation failed", {
1833
- error,
1834
- data: {
1835
- attemptedEmail: input.email,
1836
- validationErrors: error.validationErrors,
1837
- },
1838
- });
1839
- }
1840
- })
1841
- .build();
1842
- ```
1843
-
1844
- ### Context-Aware Logging
1845
-
1846
- Create logger instances with bound context for consistent metadata across related operations:
1847
-
1848
- ```typescript
1849
- const RequestContext = createContext<{ requestId: string; userId: string }>(
1850
- "app.requestContext",
1851
- );
1852
-
1853
- const requestHandler = r
1854
- .task("app.tasks.handleRequest")
1855
- .dependencies({ logger: globals.resources.logger })
1856
- .run(async ({ input: requestData }, { logger }) => {
1857
- const request = RequestContext.use();
1858
-
1859
- // Create a contextual logger with bound metadata with source and context
1860
- const requestLogger = logger.with({
1861
- source: requestHandler.id,
1862
- additionalContext: {
1863
- requestId: request.requestId,
1864
- userId: request.userId,
1865
- },
1866
- });
1867
-
1868
- // All logs from this logger will include the bound context
1869
- requestLogger.info("Processing request", {
1870
- data: { endpoint: requestData.path },
1871
- });
1872
-
1873
- requestLogger.debug("Validating input", {
1874
- data: { inputSize: JSON.stringify(requestData).length },
1875
- });
1876
-
1877
- // Context is automatically included in all log events
1878
- requestLogger.error("Request processing failed", {
1879
- error: new Error("Invalid input"),
1880
- data: { stage: "validation" },
1881
- });
1882
- })
1883
- .build();
1884
- ```
1885
-
1886
- ### Integration with Winston
1887
-
1888
- Want to use Winston as your transport? No problem - integrate it seamlessly:
1889
-
1890
- ```typescript
1891
- import winston from "winston";
1892
- import { r, globals } from "@bluelibs/runner";
1893
-
1894
- // Create Winston logger, put it in a resource if used from various places.
1895
- const winstonLogger = winston.createLogger({
1896
- level: "info",
1897
- format: winston.format.combine(
1898
- winston.format.timestamp(),
1899
- winston.format.errors({ stack: true }),
1900
- winston.format.json(),
1901
- ),
1902
- transports: [
1903
- new winston.transports.File({ filename: "error.log", level: "error" }),
1904
- new winston.transports.File({ filename: "combined.log" }),
1905
- new winston.transports.Console({
1906
- format: winston.format.simple(),
1907
- }),
1908
- ],
1909
- });
1910
-
1911
- // Bridge BlueLibs logs to Winston using hooks
1912
- const winstonBridgeResource = r
1913
- .resource("app.resources.winstonBridge")
1914
- .dependencies({ logger: globals.resources.logger })
1915
- .init(async (_config, { logger }) => {
1916
- // Map log levels (BlueLibs -> Winston)
1917
- const levelMapping = {
1918
- trace: "silly",
1919
- debug: "debug",
1920
- info: "info",
1921
- warn: "warn",
1922
- error: "error",
1923
- critical: "error", // Winston doesn't have critical, use error
1924
- };
1925
-
1926
- logger.onLog((log) => {
1927
- // Convert Runner log to Winston format
1928
- const winstonMeta = {
1929
- source: log.source,
1930
- timestamp: log.timestamp,
1931
- data: log.data,
1932
- context: log.context,
1933
- ...(log.error && { error: log.error }),
1934
- };
1935
-
1936
- const winstonLevel = levelMapping[log.level] || "info";
1937
- winstonLogger.log(winstonLevel, log.message, winstonMeta);
1938
- });
1939
- })
1940
- .build();
1941
- ```
1942
-
1943
- ### Custom Log Formatters
1944
-
1945
- Want to customize how logs are printed? You can override the print behavior:
1946
-
1947
- ```typescript
1948
- // Custom logger with JSON output
1949
- class JSONLogger extends Logger {
1950
- print(log: ILog) {
1951
- console.log(
1952
- JSON.stringify(
1953
- {
1954
- timestamp: log.timestamp.toISOString(),
1955
- level: log.level.toUpperCase(),
1956
- source: log.source,
1957
- message: log.message,
1958
- data: log.data,
1959
- context: log.context,
1960
- error: log.error,
1961
- },
1962
- null,
1963
- 2,
1964
- ),
1965
- );
1966
- }
1967
- }
1968
-
1969
- // Custom logger resource
1970
- const customLogger = r
1971
- .resource("app.logger.custom")
1972
- .dependencies({ eventManager: globals.resources.eventManager })
1973
- .init(async (_config, { eventManager }) => new JSONLogger(eventManager))
1974
- .build();
1975
-
1976
- // Or you could simply add it as "globals.resources.logger" and override the default logger
1977
- ```
1978
-
1979
- ### Log Structure
1980
-
1981
- Every log event contains:
1982
-
1983
- ```typescript
1984
- interface ILog {
1985
- level: string; // The log level (trace, debug, info, etc.)
1986
- source?: string; // Where the log came from
1987
- message: any; // The main log message (can be object or string)
1988
- timestamp: Date; // When the log was created
1989
- error?: {
1990
- // Structured error information
1991
- name: string;
1992
- message: string;
1993
- stack?: string;
1994
- };
1995
- data?: Record<string, any>; // Additional structured data, it's about the log itself
1996
- context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
1997
- }
1998
- ```
1999
-
2000
- ### Catch Logs
2001
-
2002
- > **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."
2003
-
2004
- ## Debug Resource
2005
-
2006
- _Professional-grade debugging without sacrificing production performance_
2007
-
2008
- 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.
2009
-
2010
- ### Quick Start with Debug
2011
-
2012
- ```typescript
2013
- run(app, { debug: "verbose" });
2014
- ```
2015
-
2016
- ### Debug Levels
2017
-
2018
- **"normal"** - Balanced visibility for development:
2019
-
2020
- - Task and resource lifecycle events
2021
- - Event emissions
2022
- - Hook executions
2023
- - Error tracking
2024
- - Performance timing data
2025
-
2026
- **"verbose"** - Detailed visibility for deep debugging:
2027
-
2028
- - All "normal" features plus:
2029
- - Task input/output logging
2030
- - Resource configuration and results
2031
-
2032
- **Custom Configuration**:
2033
-
2034
- ```typescript
2035
- const app = r
2036
- .resource("app")
2037
- .register([
2038
- globals.resources.debug.with({
2039
- logTaskInput: true,
2040
- logTaskResult: false,
2041
- logResourceConfig: true,
2042
- logResourceResult: false,
2043
- logEventEmissionOnRun: true,
2044
- logEventEmissionInput: false,
2045
- // Hook/middleware lifecycle visibility is available via interceptors
2046
- // ... other fine-grained options
2047
- }),
2048
- ])
2049
- .build();
2050
- ```
2051
-
2052
- ### Accessing Debug Levels Programmatically
2053
-
2054
- The debug configuration levels can now be accessed through the globals namespace via `globals.debug.levels`:
2055
-
2056
- ```typescript
2057
- import { globals } from "@bluelibs/runner";
2058
-
2059
- // Use in custom configurations
2060
- const customConfig = {
2061
- ...globals.debug.levels.normal, // or .debug
2062
- logTaskInput: true, // Override specific settings
2063
- };
2064
-
2065
- // Register with custom configuration
2066
- const app = r
2067
- .resource("app")
2068
- .register([globals.resources.debug.with(customConfig)])
2069
- .build();
2070
- ```
2071
-
2072
- ### Per-Component Debug Configuration
2073
-
2074
- Use debug tags to configure debugging on individual components, when you're interested in just a few verbose ones.
2075
-
2076
- ```typescript
2077
- import { globals } from "@bluelibs/runner";
2078
-
2079
- const criticalTask = r
2080
- .task("app.tasks.critical")
2081
- .tags([
2082
- globals.tags.debug.with({
2083
- logTaskInput: true,
2084
- logTaskResult: true,
2085
- logTaskOnError: true,
2086
- }),
2087
- ])
2088
- .run(async (input) => {
2089
- // This task will have verbose debug logging
2090
- return await processPayment(input);
2091
- })
2092
- .build();
2093
- ```
2094
-
2095
- ### Integration with Run Options
2096
-
2097
- ```typescript
2098
- // Debug options at startup
2099
- const { dispose, taskRunner, eventManager } = await run(app, {
2100
- debug: "verbose", // Enable debug globally
2101
- });
2102
-
2103
- // Access internals for advanced debugging
2104
- console.log(`Tasks registered: ${taskRunner.getRegisteredTasks().length}`);
2105
- console.log(`Events registered: ${eventManager.getRegisteredEvents().length}`);
2106
- ```
2107
-
2108
- ### Performance Impact
2109
-
2110
- The debug resource is designed for zero production overhead:
2111
-
2112
- - **Disabled**: No performance impact whatsoever
2113
- - **Enabled**: Minimal overhead (~0.1ms per operation)
2114
- - **Filtering**: System components are automatically excluded from debug logs
2115
- - **Buffering**: Logs are batched for better performance
2116
-
2117
- ### Debugging Tips & Best Practices
2118
-
2119
- Use Structured Data Liberally
2120
-
2121
- ```typescript
2122
- // Bad - hard to search and filter
2123
- await logger.error(`Failed to process user ${userId} order ${orderId}`);
2124
-
2125
- // Good - searchable and filterable
2126
- await logger.error("Order processing failed", {
2127
- data: {
2128
- userId,
2129
- orderId,
2130
- step: "payment",
2131
- paymentMethod: "credit_card",
2132
- },
2133
- });
2134
- ```
2135
-
2136
- Include Context in Errors
2137
-
2138
- ```typescript
2139
- // Include relevant context with errors
2140
- try {
2141
- await processPayment(order);
2142
- } catch (error) {
2143
- await logger.error("Payment processing failed", {
2144
- error,
2145
- data: {
2146
- orderId: order.id,
2147
- amount: order.total,
2148
- currency: order.currency,
2149
- paymentMethod: order.paymentMethod,
2150
- attemptNumber: order.paymentAttempts,
2151
- },
2152
- });
2153
- }
2154
- ```
2155
-
2156
- Use Different Log Levels Appropriately
2157
-
2158
- ```typescript
2159
- // Good level usage
2160
- await logger.debug("Cache hit", { data: { key, ttl: remainingTTL } });
2161
- await logger.info("User logged in", { data: { userId, loginMethod } });
2162
- await logger.warn("Rate limit approaching", {
2163
- data: { current: 95, limit: 100 },
2164
- });
2165
- await logger.error("Database connection failed", {
2166
- error,
2167
- data: { attempt: 3 },
2168
- });
2169
- await logger.critical("System out of memory", { data: { available: "0MB" } });
2170
- ```
2171
-
2172
- Create Domain-Specific Loggers
2173
-
2174
- ```typescript
2175
- // Create loggers with domain context
2176
- const paymentLogger = logger.with({ source: "payment.processor" });
2177
- const authLogger = logger.with({ source: "auth.service" });
2178
- const emailLogger = logger.with({ source: "email.service" });
2179
-
2180
- // Use throughout your domain
2181
- await paymentLogger.info("Processing payment", { data: paymentData });
2182
- await authLogger.warn("Failed login attempt", { data: { email, ip } });
2183
- ```
2184
-
2185
- > **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."
2186
-
2187
- ## Meta
2188
-
2189
- _The structured way to describe what your components do and control their behavior_
2190
-
2191
- 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.
2192
-
2193
- ### Metadata Properties
2194
-
2195
- Every component can have these basic metadata properties:
2196
-
2197
- ```typescript
2198
- interface IMeta {
2199
- title?: string; // Human-readable name
2200
- description?: string; // What this component does
2201
- tags?: TagType[]; // Categories and behavioral flags
2202
- }
2203
- ```
2204
-
2205
- ### Simple Documentation Example
2206
-
2207
- ```typescript
2208
- const userService = r
2209
- .resource("app.services.user")
2210
- .meta({
2211
- title: "User Management Service",
2212
- description:
2213
- "Handles user creation, authentication, and profile management",
2214
- })
2215
- .dependencies({ database })
2216
- .init(async (_config, { database }) => ({
2217
- createUser: async (userData) => {
2218
- /* ... */
2219
- },
2220
- authenticateUser: async (credentials) => {
2221
- /* ... */
2222
- },
2223
- }))
2224
- .build();
2225
-
2226
- const sendWelcomeEmail = r
2227
- .task("app.tasks.sendWelcomeEmail")
2228
- .meta({
2229
- title: "Send Welcome Email",
2230
- description: "Sends a welcome email to newly registered users",
2231
- })
2232
- .dependencies({ emailService })
2233
- .run(async ({ input: userData }, { emailService }) => {
2234
- // Email sending logic
2235
- })
2236
- .build();
2237
- ```
2238
-
2239
- ### Extending Metadata: Custom Properties
2240
-
2241
- For advanced use cases, you can extend the metadata interfaces to add your own properties:
2242
-
2243
- ```typescript
2244
- // In your types file
2245
- declare module "@bluelibs/runner" {
2246
- interface ITaskMeta {
2247
- author?: string;
2248
- version?: string;
2249
- deprecated?: boolean;
2250
- apiVersion?: "v1" | "v2" | "v3";
2251
- costLevel?: "low" | "medium" | "high";
2252
- }
2253
-
2254
- interface IResourceMeta {
2255
- healthCheck?: string; // URL for health checking
2256
- dependencies?: string[]; // External service dependencies
2257
- scalingPolicy?: "auto" | "manual";
2258
- }
2259
- }
2260
-
2261
- // Now use your custom properties
2262
- const expensiveApiTask = r
2263
- .task("app.tasks.ai.generateImage")
2264
- .meta({
2265
- title: "AI Image Generation",
2266
- description: "Uses OpenAI DALL-E to generate images from text prompts",
2267
- author: "AI Team",
2268
- version: "2.1.0",
2269
- apiVersion: "v2",
2270
- costLevel: "high", // Custom property!
2271
- })
2272
- .run(async ({ input: prompt }) => {
2273
- // AI generation logic
2274
- })
2275
- .build();
2276
-
2277
- const database = r
2278
- .resource("app.database.primary")
2279
- .meta({
2280
- title: "Primary PostgreSQL Database",
2281
- healthCheck: "/health/db", // Custom property!
2282
- dependencies: ["postgresql", "connection-pool"],
2283
- scalingPolicy: "auto",
2284
- })
2285
- // .init(async () => { /* ... */ })
2286
- .build();
2287
- ```
2288
-
2289
- 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.
2290
-
2291
- > **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."
2292
-
2293
- ## Overrides
2294
-
2295
- 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.
2296
-
2297
- 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.
2298
-
2299
- ```typescript
2300
- const productionEmailer = r
2301
- .resource("app.emailer")
2302
- .init(async () => new SMTPEmailer())
2303
- .build();
2304
-
2305
- // Option 1: Using override() to change behavior while preserving id (Recommended)
2306
- const testEmailer = override(productionEmailer, {
2307
- init: async () => new MockEmailer(),
2308
- });
2309
-
2310
- // Option 2: The system is really flexible, and override is just bringing in type safety, nothing else under the hood.
2311
- // Using spread operator works the same way but does not provide type-safety.
2312
- const testEmailer = r
2313
- .resource("app.emailer")
2314
- .init(async () => ({}))
2315
- .build();
2316
-
2317
- const app = r
2318
- .resource("app")
2319
- .register([productionEmailer])
2320
- .overrides([testEmailer]) // This replaces the production version
2321
- .build();
2322
-
2323
- import { override } from "@bluelibs/runner";
2324
-
2325
- // Tasks
2326
- const originalTask = r
2327
- .task("app.tasks.compute")
2328
- .run(async () => 1)
2329
- .build();
2330
- const overriddenTask = override(originalTask, {
2331
- run: async () => 2,
2332
- });
2333
-
2334
- // Resources
2335
- const originalResource = r
2336
- .resource("app.db")
2337
- .init(async () => "conn")
2338
- .build();
2339
- const overriddenResource = override(originalResource, {
2340
- init: async () => "mock-conn",
2341
- });
2342
-
2343
- // Middleware
2344
- const originalMiddleware = taskMiddleware({
2345
- id: "app.middleware.log",
2346
- run: async ({ next }) => next(),
2347
- });
2348
- const overriddenMiddleware = override(originalMiddleware, {
2349
- run: async ({ task, next }) => {
2350
- const result = await next(task?.input);
2351
- return { wrapped: result };
2352
- },
2353
- });
2354
-
2355
- // Even hooks
2356
- ```
2357
-
2358
- Overrides can let you expand dependencies and even call your overriden resource (like a classical OOP extends):
2359
-
2360
- ```ts
2361
- const testEmailer = override(productionEmailer, {
2362
- dependencies: {
2363
- ...productionEmailer,
2364
- // expand it, make some deps optional, or just remove some dependencies
2365
- }
2366
- init: async (_, deps) => {
2367
- const base = productionEmailer.init(_, deps);
2368
-
2369
- return {
2370
- ...base,
2371
- // expand it, modify methods of base.
2372
- }
2373
- },
2374
- });
2375
- ```
2376
-
2377
- 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.
2378
-
2379
- > **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."
2380
-
2381
- ## Namespacing
2382
-
2383
- As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
2384
-
2385
- | Type | Format |
2386
- | ------------------- | ------------------------------------------------ |
2387
- | Resources | `{domain}.resources.{resource-name}` |
2388
- | Tasks | `{domain}.tasks.{task-name}` |
2389
- | Events | `{domain}.events.{event-name}` |
2390
- | Hooks | `{domain}.hooks.on-{event-name}` |
2391
- | Task Middleware | `{domain}.middleware.task.{middleware-name}` |
2392
- | Resource Middleware | `{domain}.middleware.resource.{middleware-name}` |
2393
-
2394
- We recommend kebab-case for file names and ids. Suffix files with their primitive type: `*.task.ts`, `*.task-middleware.ts`, `*.hook.ts`, etc.
2395
-
2396
- 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.
2397
-
2398
- ```typescript
2399
- // Helper function for consistency
2400
- function namespaced(id: string) {
2401
- return `mycompany.myapp.${id}`;
2402
- }
2403
-
2404
- const userTask = r
2405
- .task(namespaced("tasks.user.create-user"))
2406
- .run(async () => null)
2407
- .build();
2408
- ```
2409
-
2410
- > **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."
2411
-
2412
- ## Factory Pattern
2413
-
2414
- 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:
2415
-
2416
- ```typescript
2417
- // Assume MyClass is defined elsewhere
2418
- // class MyClass { constructor(input: any, option: string) { ... } }
2419
-
2420
- const myFactory = r
2421
- .resource("app.factories.myFactory")
2422
- .init(async (config: { someOption: string }) => {
2423
- // This resource's value is a factory function
2424
- return (input: any) => new MyClass(input, config.someOption);
2425
- })
2426
- .build();
2427
-
2428
- const app = r
2429
- .resource("app")
2430
- // Configure the factory resource upon registration
2431
- .register([myFactory.with({ someOption: "configured-value" })])
2432
- .dependencies({ myFactory })
2433
- .init(async (_config, { myFactory }) => {
2434
- // `myFactory` is now the configured factory function
2435
- const instance = myFactory({ someInput: "hello" });
2436
- })
2437
- .build();
2438
- ```
2439
-
2440
- > **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."
2441
-
2442
- ## Runtime Validation
2443
-
2444
- 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.
2445
-
2446
- The framework defines a simple `IValidationSchema<T>` interface that any validation library can implement:
2447
-
2448
- ```typescript
2449
- interface IValidationSchema<T> {
2450
- parse(input: unknown): T;
2451
- }
2452
- ```
2453
-
2454
- Popular validation libraries already implement this interface:
2455
-
2456
- - **Zod**: `.parse()` method works directly
2457
- - **Yup**: Use `.validateSync()` or create a wrapper
2458
- - **Joi**: Use `.assert()` or create a wrapper
2459
- - **Custom validators**: Implement the interface yourself
2460
-
2461
- ### Task Input Validation
2462
-
2463
- Add an `inputSchema` to any task to validate inputs before execution:
2464
-
2465
- ```typescript
2466
- import { z } from "zod";
2467
- import { task, resource, run } from "@bluelibs/runner";
2468
-
2469
- const userSchema = z.object({
2470
- name: z.string().min(2),
2471
- email: z.string().email(),
2472
- age: z.number().min(0).max(150),
2473
- });
2474
-
2475
- const createUserTask = r
2476
- .task("app.tasks.createUser")
2477
- .inputSchema(userSchema) // Works directly with Zod!
2478
- .run(async ({ input: userData }) => {
2479
- // userData is validated and properly typed
2480
- return { id: "user-123", ...userData };
2481
- })
2482
- .build();
2483
-
2484
- const app = r
2485
- .resource("app")
2486
- .register([createUserTask])
2487
- .dependencies({ createUserTask })
2488
- .init(async (_config, { createUserTask }) => {
2489
- // This works - valid input
2490
- const user = await createUserTask({
2491
- name: "John Doe",
2492
- email: "john@example.com",
2493
- age: 30,
2494
- });
2495
-
2496
- // This throws a validation error at runtime
2497
- try {
2498
- await createUserTask({
2499
- name: "J", // Too short
2500
- email: "invalid-email", // Invalid format
2501
- age: -5, // Negative age
2502
- });
2503
- } catch (error) {
2504
- console.log(error.message);
2505
- // "Task input validation failed for app.tasks.createUser: ..."
2506
- }
2507
- })
2508
- .build();
2509
- ```
2510
-
2511
- ### Resource Config Validation
2512
-
2513
- Add a `configSchema` to resources to validate configurations. **Validation happens immediately when `.with()` is called**, ensuring configuration errors are caught early:
2514
-
2515
- ```typescript
2516
- const databaseConfigSchema = z.object({
2517
- host: z.string(),
2518
- port: z.number().min(1).max(65535),
2519
- database: z.string(),
2520
- ssl: z.boolean().default(false), // Optional with default
2521
- });
2522
-
2523
- const databaseResource = r
2524
- .resource("app.resources.database")
2525
- .configSchema(databaseConfigSchema) // Validation on .with()
2526
- .init(async (config) => {
2527
- // config is already validated and has proper types
2528
- return createConnection({
2529
- host: config.host,
2530
- port: config.port,
2531
- database: config.database,
2532
- ssl: config.ssl,
2533
- });
2534
- })
2535
- .build();
2536
-
2537
- // Validation happens here, not during init!
2538
- try {
2539
- const configuredResource = databaseResource.with({
2540
- host: "localhost",
2541
- port: 99999, // Invalid: port too high
2542
- database: "myapp",
2543
- });
2544
- } catch (error) {
2545
- // "Resource config validation failed for app.resources.database: ..."
2546
- }
2547
-
2548
- const app = r
2549
- .resource("app")
2550
- .register([
2551
- databaseResource.with({
2552
- host: "localhost",
2553
- port: 5432,
2554
- database: "myapp",
2555
- // ssl defaults to false
2556
- }),
2557
- ])
2558
- .build();
2559
- ```
2560
-
2561
- ### Event Payload Validation
2562
-
2563
- Add a `payloadSchema` to events to validate payloads every time they're emitted:
2564
-
2565
- ```typescript
2566
- const userActionSchema = z.object({
2567
- userId: z.string().uuid(),
2568
- action: z.enum(["created", "updated", "deleted"]),
2569
- timestamp: z.date().default(() => new Date()),
2570
- });
2571
-
2572
- const userActionEvent = r
2573
- .event("app.events.userAction")
2574
- .payloadSchema(userActionSchema) // Validates on emit
2575
- .build();
2576
-
2577
- const notificationHook = r
2578
- .hook("app.tasks.sendNotification")
2579
- .on(userActionEvent)
2580
- .run(async (eventData) => {
2581
- // eventData.data is validated and properly typed
2582
- console.log(`User ${eventData.data.userId} was ${eventData.data.action}`);
2583
- })
2584
- .build();
2585
-
2586
- const app = r
2587
- .resource("app")
2588
- .register([userActionEvent, notificationHook])
2589
- .dependencies({ userActionEvent })
2590
- .init(async (_config, { userActionEvent }) => {
2591
- // This works - valid payload
2592
- await userActionEvent({
2593
- userId: "123e4567-e89b-12d3-a456-426614174000",
2594
- action: "created",
2595
- });
2596
-
2597
- // This throws validation error when emitted
2598
- try {
2599
- await userActionEvent({
2600
- userId: "invalid-uuid",
2601
- action: "unknown",
2602
- });
2603
- } catch (error) {
2604
- // "Event payload validation failed for app.events.userAction: ..."
2605
- }
2606
- })
2607
- .build();
2608
- ```
2609
-
2610
- ### Middleware Config Validation
2611
-
2612
- Add a `configSchema` to middleware to validate configurations. Like resources, **validation happens immediately when `.with()` is called**:
2613
-
2614
- ```typescript
2615
- const timingConfigSchema = z.object({
2616
- timeout: z.number().positive(),
2617
- logLevel: z.enum(["debug", "info", "warn", "error"])).default("info"),
2618
- logSuccessful: z.boolean().default(true),
2619
- });
2620
-
2621
- const timingMiddleware = r.middleware
2622
- .task("app.middleware.timing") // or r.middleware.resource("...")
2623
- .configSchema(timingConfigSchema) // Validation on .with()
2624
- .run(async ({ next }, _, config) => {
2625
- const start = Date.now();
2626
- try {
2627
- const result = await next();
2628
- const duration = Date.now() - start;
2629
- if (config.logSuccessful && config.logLevel === "debug") {
2630
- console.log(`Operation completed in ${duration}ms`);
2631
- }
2632
- return result;
2633
- } catch (error) {
2634
- const duration = Date.now() - start;
2635
- console.log(`Operation failed after ${duration}ms`);
2636
- throw error;
2637
- }
2638
- })
2639
- .build();
2640
-
2641
- // Validation happens here, not during execution!
2642
- try {
2643
- const configuredMiddleware = timingMiddleware.with({
2644
- timeout: -5, // Invalid: negative timeout
2645
- logLevel: "invalid", // Invalid: not in enum
2646
- });
2647
- } catch (error) {
2648
- // "Middleware config validation failed for app.middleware.timing: ..."
2649
- }
2650
-
2651
- const myTask = r
2652
- .task("app.tasks.example")
2653
- .middleware([
2654
- timingMiddleware.with({
2655
- timeout: 5000,
2656
- logLevel: "debug",
2657
- logSuccessful: true,
2658
- }),
2659
- ])
2660
- .run(async () => "success")
2661
- .build();
2662
- ```
2663
-
2664
- #### Advanced Validation Features
2665
-
2666
- Any validation library features work with the generic interface. Here's an example with transformations and refinements:
2667
-
2668
- ```typescript
2669
- const advancedSchema = z
2670
- .object({
2671
- userId: z.string().uuid(),
2672
- amount: z.string().transform((val) => parseFloat(val)), // Transform string to number
2673
- currency: z.enum(["USD", "EUR", "GBP"]),
2674
- metadata: z.record(z.string()).optional(),
2675
- })
2676
- .refine((data) => data.amount > 0, {
2677
- message: "Amount must be positive",
2678
- path: ["amount"],
2679
- });
2680
-
2681
- const paymentTask = r
2682
- .task("app.tasks.payment")
2683
- .inputSchema(advancedSchema)
2684
- .run(async ({ input: payment }) => {
2685
- // payment.amount is now a number (transformed from string)
2686
- // All validations have passed
2687
- return processPayment(payment);
2688
- })
2689
- .build();
2690
- ```
2691
-
2692
- ### Error Handling
2693
-
2694
- Validation errors are thrown with clear, descriptive messages that include the component ID:
2695
-
2696
- ```typescript
2697
- // Task validation error format:
2698
- // "Task input validation failed for {taskId}: {validationErrorMessage}"
2699
-
2700
- // Resource validation error format (thrown on .with() call):
2701
- // "Resource config validation failed for {resourceId}: {validationErrorMessage}"
2702
-
2703
- // Event validation error format (thrown on emit):
2704
- // "Event payload validation failed for {eventId}: {validationErrorMessage}"
2705
-
2706
- // Middleware validation error format (thrown on .with() call):
2707
- // "Middleware config validation failed for {middlewareId}: {validationErrorMessage}"
2708
- ```
2709
-
2710
- #### Other Libraries
2711
-
2712
- The framework works with any validation library that implements the `IValidationSchema<T>` interface:
2713
-
2714
- ```typescript
2715
- // Zod (works directly)
2716
- import { z } from "zod";
2717
- const zodSchema = z.string().email();
2718
-
2719
- // Yup (with wrapper)
2720
- import * as yup from "yup";
2721
- const yupSchema = {
2722
- parse: (input: unknown) => yup.string().email().validateSync(input),
2723
- };
2724
-
2725
- // Joi (with wrapper)
2726
- import Joi from "joi";
2727
- const joiSchema = {
2728
- parse: (input: unknown) => {
2729
- const { error, value } = Joi.string().email().validate(input);
2730
- if (error) throw error;
2731
- return value;
2732
- },
2733
- };
2734
-
2735
- // Custom validation
2736
- const customSchema = {
2737
- parse: (input: unknown) => {
2738
- if (typeof input !== "string" || !input.includes("@")) {
2739
- throw new Error("Must be a valid email");
2740
- }
2741
- return input;
2742
- },
2743
- };
2744
- ```
2745
-
2746
- #### When to Use Validation
2747
-
2748
- - **API boundaries**: Validating user inputs from HTTP requests
2749
- - **External data**: Processing data from files, databases, or APIs
2750
- - **Configuration**: Ensuring environment variables and configs are correct (fail fast)
2751
- - **Event payloads**: Validating data in event-driven architectures
2752
- - **Middleware configs**: Validating middleware settings at registration time (fail fast)
2753
-
2754
- #### Performance Notes
2755
-
2756
- - Validation only runs when schemas are provided (zero overhead when not used)
2757
- - Resource and middleware validation happens once at registration time (`.with()`)
2758
- - Task and event validation happens at runtime
2759
- - Consider the validation library's performance characteristics for your use case
2760
- - All major validation libraries are optimized for runtime validation
2761
-
2762
- #### TypeScript Integration
2763
-
2764
- While runtime validation happens with your chosen library, TypeScript still enforces compile-time types. For the best experience:
2765
-
2766
- ```typescript
2767
- // With Zod, define your type and schema together
2768
-
2769
- const userSchema = z.object({
2770
- name: z.string(),
2771
- email: z.string().email(),
2772
- });
2773
-
2774
- type UserData = z.infer<typeof userSchema>;
2775
-
2776
- const createUser = r
2777
- .task("app.tasks.createUser.zod")
2778
- .inputSchema(userSchema)
2779
- .run(async (input: { input: UserData }) => {
2780
- // Both runtime validation AND compile-time typing
2781
- return { id: "user-123", ...input };
2782
- })
2783
- .build();
2784
- ```
2785
-
2786
- > **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."
2787
-
2788
- ## Internal Services
2789
-
2790
- We expose the internal services for advanced use cases (but try not to use them unless you really need to):
2791
-
2792
- ```typescript
2793
- import { globals } from "@bluelibs/runner";
2794
-
2795
- const advancedTask = r
2796
- .task("app.advanced")
2797
- .dependencies({
2798
- store: globals.resources.store,
2799
- taskRunner: globals.resources.taskRunner,
2800
- eventManager: globals.resources.eventManager,
2801
- })
2802
- .run(async (_param, { store, taskRunner, eventManager }) => {
2803
- // Direct access to the framework internals
2804
- // (Use with caution!)
2805
- })
2806
- .build();
2807
- ```
2808
-
2809
- ### Dynamic Dependencies
2810
-
2811
- 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:
2812
-
2813
- ```typescript
2814
- // Static dependencies (most common)
2815
- const userService = r
2816
- .resource("app.services.user")
2817
- .dependencies({ database, logger }) // Object - evaluated immediately
2818
- .init(async (_config, { database, logger }) => {
2819
- // Dependencies are available here
2820
- })
2821
- .build();
2822
-
2823
- // Dynamic dependencies (for circular references or conditional dependencies)
2824
- const advancedService = r
2825
- .resource("app.services.advanced")
2826
- // A function gives you the chance
2827
- .dependencies((_config) => ({
2828
- // Config is what you receive when you register this resource with .with()
2829
- // So you can have conditional dependencies based on resource configuration as well.
2830
- database,
2831
- logger,
2832
- conditionalService:
2833
- process.env.NODE_ENV === "production" ? serviceA : serviceB,
2834
- })) // Function - evaluated when needed
2835
- .register((_config: ConfigType) => [
2836
- // Register dependencies dynamically
2837
- process.env.NODE_ENV === "production"
2838
- ? serviceA.with({ config: "value" })
2839
- : serviceB.with({ config: "value" }),
2840
- ])
2841
- .init(async (_config, { database, logger, conditionalService }) => {
2842
- // Same interface, different evaluation timing
2843
- })
2844
- .build();
2845
- ```
2846
-
2847
- 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.
2848
-
2849
- **Performance note**: Function-based dependencies have minimal overhead - they're only called once during dependency resolution.
2850
-
2851
- > **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.'"
2852
-
2853
- ## Handling Circular Dependencies
2854
-
2855
- 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.
2856
-
2857
- ### The Problem
2858
-
2859
- Consider these resources that create a circular dependency:
2860
-
2861
- ```typescript
2862
- // FILE: a.ts
2863
- export const aResource = defineResource({
2864
- dependencies: { b: bResource },
2865
- // ... depends on B resource.
2866
- });
2867
- // For whatever reason, you decide to put the task in the same file.
2868
- export const aTask = defineTask({
2869
- dependencies: { a: aResource },
2870
- });
2871
-
2872
- // FILE: b.ts
2873
- export const bResource = defineResource({
2874
- id: "b.resource",
2875
- dependencies: { c: cResource },
2876
- });
2877
-
2878
- // FILE: c.ts
2879
- export const cResource = defineResource({
2880
- id: "c.resource",
2881
- dependencies: { aTask }, // Creates circular **type** dependency! Cannot infer types properly, even if the runner boots because there's no circular dependency.
2882
- async init(_, { aTask }) {
2883
- return `C depends on aTask`;
2884
- },
2885
- });
2886
- ```
2887
-
2888
- A depends B depends C depends ATask. No circular dependency, yet Typescript struggles with these, but there's a way to handle it gracefully.
2889
-
2890
- ### The Solution
2891
-
2892
- 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:
2893
-
2894
- ```typescript
2895
- // c.resource.ts - The key change
2896
- import { IResource } from "../../defs";
2897
-
2898
- export const cResource = defineResource({
2899
- id: "c.resource",
2900
- dependencies: { a: aResource },
2901
- async init(_, { a }) {
2902
- return `C depends on ${a}`;
2903
- },
2904
- }) as IResource<void, string>; // void because it has no config, string because it returns a string
2905
- ```
2906
-
2907
- #### Why This Works
2908
-
2909
- - **Runtime**: The circular dependency still works at runtime because the framework resolves dependencies dynamically
2910
- - **TypeScript**: The explicit type annotation prevents TypeScript from trying to infer the return type based on the circular chain
2911
- - **Type Safety**: You still get full type safety by explicitly declaring the return type (`string` in this example)
2912
-
2913
- #### Best Practices
2914
-
2915
- 1. **Identify the "leaf" resource**: Choose the resource that logically should break the chain (often the one that doesn't need complex type inference)
2916
- 2. **Use explicit typing**: Add the `IResource<Dependencies, ReturnType>` type annotation
2917
- 3. **Document the decision**: Add a comment explaining why the explicit typing is needed
2918
- 4. **Consider refactoring**: If you have many circular dependencies, consider if your architecture could be simplified
2919
-
2920
- #### Example with Dependencies
2921
-
2922
- If your resource has dependencies, include them in the type:
2923
-
2924
- ```typescript
2925
- type MyDependencies = {
2926
- someService: SomeServiceType;
2927
- anotherResource: AnotherResourceType;
2928
- };
2929
-
2930
- export const problematicResource = defineResource({
2931
- id: "problematic.resource",
2932
- dependencies: {
2933
- /* ... */
2934
- },
2935
- async init(config, deps) {
2936
- // Your logic here
2937
- return someComplexObject;
2938
- },
2939
- }) as IResource<MyDependencies, ComplexReturnType>;
2940
- ```
2941
-
2942
- This pattern allows you to maintain clean, type-safe code while handling the inevitable circular dependencies that arise in complex applications.
2943
-
2944
- > **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."
2945
-
2946
- ## Real-World Example: The Complete Package
2947
-
2948
- Here's a more realistic application structure that shows everything working together:
2949
-
2950
- ```typescript
2951
- import {
2952
- resource,
2953
- task,
2954
- event,
2955
- middleware,
2956
- run,
2957
- createContext,
2958
- } from "@bluelibs/runner";
2959
-
2960
- // Configuration
2961
- const config = r
2962
- .resource("app.config")
2963
- .init(async () => ({
2964
- port: parseInt(process.env.PORT || "3000"),
2965
- databaseUrl: process.env.DATABASE_URL!,
2966
- jwtSecret: process.env.JWT_SECRET!,
2967
- }))
2968
- .build();
2969
-
2970
- // Database
2971
- const database = r
2972
- .resource("app.database")
2973
- .dependencies({ config })
2974
- .init(async (_config, { config }) => {
2975
- const client = new MongoClient(config.databaseUrl);
2976
- await client.connect();
2977
- return client;
2978
- })
2979
- .dispose(async (client) => await client.close())
2980
- .build();
2981
-
2982
- // Context for request data
2983
- const RequestContext = createContext<{ userId?: string; role?: string }>(
2984
- "app.requestContext",
2985
- );
2986
-
2987
- // Events
2988
- const userRegistered = r
2989
- .event("app.events.userRegistered")
2990
- .payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
2991
- .build();
2992
-
2993
- // Middleware
2994
- const authMiddleware = r.middleware
2995
- .task("app.middleware.task.auth")
2996
- .run(async ({ task, next }, deps, config?: { requiredRole?: string }) => {
2997
- const context = RequestContext.use();
2998
- if (config?.requiredRole && context.role !== config.requiredRole) {
2999
- throw new Error("Insufficient permissions");
3000
- }
3001
- return next(task.input);
3002
- })
3003
- .build();
3004
-
3005
- // Services
3006
- const userService = r
3007
- .resource("app.services.user")
3008
- .dependencies({ database })
3009
- .init(async (_config, { database }) => ({
3010
- async createUser(userData: { name: string; email: string }) {
3011
- const users = database.collection("users");
3012
- const result = await users.insertOne(userData);
3013
- return { id: result.insertedId.toString(), ...userData };
3014
- },
3015
- }))
3016
- .build();
3017
-
3018
- // Business Logic
3019
- const registerUser = r
3020
- .task("app.tasks.registerUser")
3021
- .dependencies({ userService, userRegistered })
3022
- .run(async ({ input: userData }, { userService, userRegistered }) => {
3023
- const user = await userService.createUser(userData);
3024
- await userRegistered({ userId: user.id, email: user.email });
3025
- return user;
3026
- })
3027
- .build();
3028
-
3029
- const adminOnlyTask = r
3030
- .task("app.tasks.adminOnly")
3031
- .middleware([authMiddleware.with({ requiredRole: "admin" })])
3032
- .run(async () => "Top secret admin data")
3033
- .build();
3034
-
3035
- // Event Handlers using hooks
3036
- const sendWelcomeEmail = r
3037
- .hook("app.hooks.sendWelcomeEmail")
3038
- .on(userRegistered)
3039
- .dependencies({ emailService })
3040
- .run(async (event, { emailService }) => {
3041
- console.log(`Sending welcome email to ${event.data.email}`);
3042
- await emailService.sendWelcome(event.data.email);
3043
- })
3044
- .build();
3045
-
3046
- // Express server
3047
- const server = r
3048
- .resource("app.server")
3049
- .register([config, database, userService, registerUser, adminOnlyTask, sendWelcomeEmail])
3050
- .dependencies({ config, registerUser, adminOnlyTask })
3051
- .init(async (_config, { config, registerUser, adminOnlyTask }) => {
3052
- const app = express();
3053
- app.use(express.json());
3054
-
3055
- // Middleware to set up request context
3056
- app.use((req, res, next) => {
3057
- RequestContext.provide(
3058
- { userId: req.headers["user-id"], role: req.headers["user-role"] },
3059
- () => next(),
3060
- );
3061
- });
3062
-
3063
- app.post("/register", async (req, res) => {
3064
- try {
3065
- const user = await registerUser(req.body);
3066
- res.json({ success: true, user });
3067
- } catch (error) {
3068
- res.status(400).json({ error: error.message });
3069
- }
3070
- });
3071
-
3072
- app.get("/admin", async (req, res) => {
3073
- try {
3074
- const data = await adminOnlyTask();
3075
- res.json({ data });
3076
- } catch (error) {
3077
- res.status(403).json({ error: error.message });
3078
- }
3079
- });
3080
-
3081
- const server = app.listen(config.port);
3082
- console.log(`Server running on port ${config.port}`);
3083
- return server;
3084
- },
3085
- dispose: async (server) => server.close(),
3086
- });
3087
-
3088
- // Start the application with enhanced run options
3089
- const { dispose, taskRunner, eventManager } = await run(server, {
3090
- debug: "normal", // Enable debug logging
3091
- // log: "json", // Use JSON log format
3092
- });
3093
-
3094
- // Graceful shutdown
3095
- process.on("SIGTERM", async () => {
3096
- console.log("Shutting down gracefully...");
3097
- await dispose();
3098
- process.exit(0);
3099
- });
3100
- ```
3101
-
3102
- > **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."
3103
-
3104
- ## Testing
3105
-
3106
- ### Unit Testing
3107
-
3108
- Unit testing is straightforward because everything is explicit:
3109
-
3110
- ```typescript
3111
- describe("registerUser task", () => {
3112
- it("should create a user and emit event", async () => {
3113
- const mockUserService = {
3114
- createUser: jest.fn().mockResolvedValue({ id: "123", name: "John" }),
3115
- };
3116
- const mockEvent = jest.fn();
3117
-
3118
- const result = await registerUser.run(
3119
- { name: "John", email: "john@example.com" },
3120
- { userService: mockUserService, userRegistered: mockEvent },
3121
- );
3122
-
3123
- expect(result.id).toBe("123");
3124
- expect(mockEvent).toHaveBeenCalledWith({
3125
- userId: "123",
3126
- email: "john@example.com",
3127
- });
3128
- });
3129
- });
3130
- ```
3131
-
3132
- ### Integration Testing
3133
-
3134
- Spin up your whole app, keep all the middleware/events, and still test like a human. The `run()` function returns a `RunnerResult`.
3135
-
3136
- This contains the classic `value` and `dispose()` but it also exposes `logger`, `runTask()`, `emitEvent()`, and `getResourceValue()` by default.
3137
-
3138
- 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.
3139
-
3140
- ```typescript
3141
- import { run, r, override } from "@bluelibs/runner";
3142
-
3143
- // Your real app
3144
- const app = r
3145
- .resource("app")
3146
- .register([
3147
- /* tasks, resources, middleware */
3148
- ])
3149
- .build();
3150
-
3151
- // Optional: overrides for infra (hello, fast tests!)
3152
- const testDb = r
3153
- .resource("app.database")
3154
- .init(async () => new InMemoryDb())
3155
- .build();
3156
- // If you use with override() it will enforce the same interface upon the overriden resource to ensure typesafety
3157
- const mockMailer = override(realMailer, { init: async () => fakeMailer });
3158
-
3159
- // Create the test harness
3160
- const harness = r.resource("test").overrides([mockMailer, testDb]).build();
3161
-
3162
- // A task you want to drive in your tests
3163
- const registerUser = r
3164
- .task("app.tasks.registerUser")
3165
- .run(async () => ({}))
3166
- .build();
3167
-
3168
- // Boom: full ecosystem
3169
- const { value: t, dispose } = await run(harness);
3170
-
3171
- // You have 3 ways to interact with the system, run tasks, get resource values and emit events
3172
- // You can run them dynamically with just string ids, but using the created objects gives you type-safety.
3173
-
3174
- const result = await t.runTask(registerUser, { email: "x@y.z" });
3175
- const value = t.getResourceValue(testDb); // since the resolution is done by id, this will return the exact same result as t.getResourceValue(actualDb)
3176
- t.emitEvent(event, payload);
3177
- expect(result).toMatchObject({ success: true });
3178
- await dispose();
3179
- ```
3180
-
3181
- 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.
3182
-
3183
- > **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."
3184
-
3185
- ## Semaphore
3186
-
3187
- 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.
3188
-
3189
- 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.
3190
-
3191
- ```typescript
3192
- import { Semaphore } from "@bluelibs/runner";
3193
-
3194
- // Create a semaphore that allows max 5 concurrent operations
3195
- const dbSemaphore = new Semaphore(5);
3196
-
3197
- // Basic usage - acquire and release manually
3198
- await dbSemaphore.acquire();
3199
- try {
3200
- // Do your database magic here
3201
- const result = await db.query("SELECT * FROM users");
3202
- console.log(result);
3203
- } finally {
3204
- dbSemaphore.release(); // Critical: always release to prevent bottlenecks
3205
- }
3206
- ```
3207
-
3208
- Why manage permits manually when you can let the semaphore do the heavy lifting?
3209
-
3210
- ```typescript
3211
- // The elegant approach - automatic cleanup guaranteed!
3212
- const users = await dbSemaphore.withPermit(async () => {
3213
- return await db.query("SELECT * FROM users WHERE active = true");
3214
- });
3215
- ```
3216
-
3217
- Prevent operations from hanging indefinitely with configurable timeouts:
3218
-
3219
- ```typescript
3220
- try {
3221
- // Wait max 5 seconds, then throw timeout error
3222
- await dbSemaphore.acquire({ timeout: 5000 });
3223
- // Your code here
3224
- } catch (error) {
3225
- console.log("Operation timed out waiting for permit");
3226
- }
3227
-
3228
- // Or with withPermit
3229
- const result = await dbSemaphore.withPermit(
3230
- async () => await slowDatabaseOperation(),
3231
- { timeout: 10000 }, // 10 second timeout
3232
- );
3233
- ```
3234
-
3235
- Operations can be cancelled using AbortSignal:
3236
-
3237
- ```typescript
3238
- const controller = new AbortController();
3239
-
3240
- // Start an operation
3241
- const operationPromise = dbSemaphore.withPermit(
3242
- async () => await veryLongOperation(),
3243
- { signal: controller.signal },
3244
- );
3245
-
3246
- // Cancel the operation after 3 seconds
3247
- setTimeout(() => {
3248
- controller.abort();
3249
- }, 3000);
3250
-
3251
- try {
3252
- await operationPromise;
3253
- } catch (error) {
3254
- console.log("Operation was cancelled");
3255
- }
3256
- ```
3257
-
3258
- Want to know what's happening under the hood?
3259
-
3260
- ```typescript
3261
- // Get comprehensive metrics
3262
- const metrics = dbSemaphore.getMetrics();
3263
- console.log(`
3264
- Semaphore Status Report:
3265
- Available permits: ${metrics.availablePermits}/${metrics.maxPermits}
3266
- Operations waiting: ${metrics.waitingCount}
3267
- Utilization: ${(metrics.utilization * 100).toFixed(1)}%
3268
- Disposed: ${metrics.disposed ? "Yes" : "No"}
3269
- `);
3270
-
3271
- // Quick checks
3272
- console.log(`Available permits: ${dbSemaphore.getAvailablePermits()}`);
3273
- console.log(`Queue length: ${dbSemaphore.getWaitingCount()}`);
3274
- console.log(`Is disposed: ${dbSemaphore.isDisposed()}`);
3275
- ```
3276
-
3277
- Properly dispose of semaphores when finished:
3278
-
3279
- ```typescript
3280
- // Reject all waiting operations and prevent new ones
3281
- dbSemaphore.dispose();
3282
-
3283
- // All waiting operations will be rejected with:
3284
- // Error: "Semaphore has been disposed"
3285
- ```
3286
-
3287
- ### Real-World Examples
3288
-
3289
- #### Database Connection Pool Manager
3290
-
3291
- ```typescript
3292
- class DatabaseManager {
3293
- private semaphore = new Semaphore(10); // Max 10 concurrent queries
3294
-
3295
- async query(sql: string, params?: any[]) {
3296
- return this.semaphore.withPermit(
3297
- async () => {
3298
- const connection = await this.pool.getConnection();
3299
- try {
3300
- return await connection.query(sql, params);
3301
- } finally {
3302
- connection.release();
3303
- }
3304
- },
3305
- { timeout: 30000 }, // 30 second timeout
3306
- );
3307
- }
3308
-
3309
- async shutdown() {
3310
- this.semaphore.dispose();
3311
- await this.pool.close();
3312
- }
3313
- }
3314
- ```
3315
-
3316
- #### Rate-Limited API Client
3317
-
3318
- ```typescript
3319
- class APIClient {
3320
- private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
3321
-
3322
- async fetchUser(id: string, signal?: AbortSignal) {
3323
- return this.rateLimiter.withPermit(
3324
- async () => {
3325
- const response = await fetch(`/api/users/${id}`, { signal });
3326
- return response.json();
3327
- },
3328
- { signal, timeout: 10000 },
3329
- );
3330
- }
3331
- }
3332
- ```
3333
-
3334
- > **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."
3335
-
3336
- ## Queue
3337
-
3338
- _The orderly guardian of chaos, the diplomatic bouncer of async operations._
3339
-
3340
- 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.
3341
-
3342
- Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
3343
-
3344
- 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.
3345
-
3346
- The Queue provides cooperative cancellation through the Web Standard `AbortController`:
3347
-
3348
- - **Patient mode** (default): Waits for all queued tasks to complete naturally
3349
- - **Cancel mode**: Signals running tasks to abort via `AbortSignal`, enabling early termination
3350
-
3351
- ```typescript
3352
- import { Queue } from "@bluelibs/runner";
3353
-
3354
- const queue = new Queue();
3355
-
3356
- // Queue up some work
3357
- const result = await queue.run(async (signal) => {
3358
- // Your async task here
3359
- return "Task completed";
3360
- });
3361
-
3362
- // Graceful shutdown
3363
- await queue.dispose();
3364
- ```
3365
-
3366
- ### AbortController Integration
3367
-
3368
- The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
3369
-
3370
- ### Examples
3371
-
3372
- **Example: Long-running Task**
3373
-
3374
- ```typescript
3375
- const queue = new Queue();
3376
-
3377
- // Task that respects cancellation
3378
- const processLargeDataset = queue.run(async (signal) => {
3379
- const items = await fetchLargeDataset();
3380
-
3381
- for (const item of items) {
3382
- // Check for cancellation before processing each item
3383
- if (signal.aborted) {
3384
- throw new Error("Operation was cancelled");
3385
- }
3386
-
3387
- await processItem(item);
3388
- }
3389
-
3390
- return "Dataset processed successfully";
3391
- });
3392
-
3393
- // Cancel all running tasks
3394
- await queue.dispose({ cancel: true });
3395
- ```
3396
-
3397
- **Network Request with Timeout**
3398
-
3399
- ```typescript
3400
- const queue = new Queue();
3401
-
3402
- const fetchWithCancellation = queue.run(async (signal) => {
3403
- try {
3404
- // Pass the signal to fetch for automatic cancellation
3405
- const response = await fetch("https://api.example.com/data", { signal });
3406
- return await response.json();
3407
- } catch (error) {
3408
- if (error.name === "AbortError") {
3409
- console.log("Request was cancelled");
3410
- throw error;
3411
- }
3412
- throw error;
3413
- }
3414
- });
3415
-
3416
- // This will cancel the fetch request if still pending
3417
- await queue.dispose({ cancel: true });
3418
- ```
3419
-
3420
- **Example: File Processing with Progress Tracking**
3421
-
3422
- ```typescript
3423
- const queue = new Queue();
3424
-
3425
- const processFiles = queue.run(async (signal) => {
3426
- const files = await getFileList();
3427
- const results = [];
3428
-
3429
- for (let i = 0; i < files.length; i++) {
3430
- // Respect cancellation
3431
- signal.throwIfAborted();
3432
-
3433
- const result = await processFile(files[i]);
3434
- results.push(result);
3435
-
3436
- // Optional: Report progress
3437
- console.log(`Processed ${i + 1}/${files.length} files`);
3438
- }
3439
-
3440
- return results;
3441
- });
3442
- ```
3443
-
3444
- #### The Magic Behind the Curtain
3445
-
3446
- - `tail`: The promise chain that maintains FIFO execution order
3447
- - `disposed`: Boolean flag indicating whether the queue accepts new tasks
3448
- - `abortController`: Centralized cancellation controller that provides `AbortSignal` to all tasks
3449
- - `executionContext`: AsyncLocalStorage-based deadlock detection mechanism
3450
-
3451
- #### Implement Cooperative Cancellation
3452
-
3453
- Tasks should regularly check the `AbortSignal` and respond appropriately:
3454
-
3455
- ```typescript
3456
- // Preferred: Use signal.throwIfAborted() for immediate termination
3457
- signal.throwIfAborted();
3458
-
3459
- // Alternative: Check signal.aborted for custom handling
3460
- if (signal.aborted) {
3461
- cleanup();
3462
- throw new Error("Operation cancelled");
3463
- }
3464
- ```
3465
-
3466
- **Integrate with Native APIs**
3467
-
3468
- Many Web APIs accept `AbortSignal`:
3469
-
3470
- - `fetch(url, { signal })`
3471
- - `setTimeout(callback, delay, { signal })`
3472
- - Custom async operations
3473
-
3474
- **Avoid Nested Queuing**
3475
-
3476
- The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
3477
-
3478
- **Handle AbortError Gracefully**
3479
-
3480
- ```typescript
3481
- try {
3482
- await queue.run(task);
3483
- } catch (error) {
3484
- if (error.name === "AbortError") {
3485
- // Expected cancellation, handle appropriately
3486
- return;
3487
- }
3488
- throw error; // Re-throw unexpected errors
3489
- }
3490
- ```
3491
-
3492
- > **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."
3493
-
3494
- ## Why Choose BlueLibs Runner?
3495
-
3496
- ### What You Get
3497
-
3498
- - **Type Safety**: Full TypeScript support with intelligent inference
3499
- - **Testability**: Everything is mockable and testable by design
3500
- - **Flexibility**: Compose your app however you want
3501
- - **Performance**: Built-in caching and optimization
3502
- - **Clarity**: Explicit dependencies, no hidden magic
3503
- - **Developer Experience**: Helpful error messages and clear patterns
3504
-
3505
- > **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."
3506
-
3507
- ## The Migration Path
3508
-
3509
- Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
3510
-
3511
- 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.
3512
-
3513
- > **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."
3514
-
3515
- ## Community & Support
3516
-
3517
- This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
3518
-
3519
- - [GitHub Repository](https://github.com/bluelibs/runner) - ⭐ if you find this useful
3520
- - [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
3521
- - [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
3522
- - [Contributing](./CONTRIBUTING.md) - How to file great issues and PRs
3523
-
3524
- _P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's still different._
3525
-
3526
- > **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
+ ---
3527
164
 
3528
165
  ## License
3529
166
 
3530
- This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
3531
-
3532
- > **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).