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