@bluelibs/runner 4.6.1 → 4.7.0-alpha
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/AI.md +319 -579
- package/README.md +886 -731
- package/dist/browser/index.cjs +1438 -251
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.mjs +1433 -252
- package/dist/browser/index.mjs.map +1 -1
- package/dist/context.d.ts +31 -0
- package/dist/define.d.ts +9 -0
- package/dist/definers/builders/core.d.ts +30 -0
- package/dist/definers/builders/event.d.ts +12 -0
- package/dist/definers/builders/hook.d.ts +20 -0
- package/dist/definers/builders/middleware.d.ts +39 -0
- package/dist/definers/builders/resource.d.ts +40 -0
- package/dist/definers/builders/tag.d.ts +10 -0
- package/dist/definers/builders/task.d.ts +37 -0
- package/dist/definers/builders/task.phantom.d.ts +27 -0
- package/dist/definers/builders/utils.d.ts +4 -0
- package/dist/definers/defineEvent.d.ts +2 -0
- package/dist/definers/defineHook.d.ts +6 -0
- package/dist/definers/defineOverride.d.ts +17 -0
- package/dist/definers/defineResource.d.ts +2 -0
- package/dist/definers/defineResourceMiddleware.d.ts +2 -0
- package/dist/definers/defineTag.d.ts +12 -0
- package/dist/definers/defineTask.d.ts +18 -0
- package/dist/definers/defineTaskMiddleware.d.ts +2 -0
- package/dist/definers/tools.d.ts +47 -0
- package/dist/defs.d.ts +29 -0
- package/dist/edge/index.cjs +1438 -251
- package/dist/edge/index.cjs.map +1 -1
- package/dist/edge/index.mjs +1433 -252
- package/dist/edge/index.mjs.map +1 -1
- package/dist/errors.d.ts +104 -0
- package/dist/globals/globalEvents.d.ts +8 -0
- package/dist/globals/globalMiddleware.d.ts +31 -0
- package/dist/globals/globalResources.d.ts +32 -0
- package/dist/globals/globalTags.d.ts +11 -0
- package/dist/globals/middleware/cache.middleware.d.ts +27 -0
- package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
- package/dist/globals/middleware/retry.middleware.d.ts +21 -0
- package/dist/globals/middleware/timeout.middleware.d.ts +9 -0
- package/dist/globals/middleware/tunnel.middleware.d.ts +2 -0
- package/dist/globals/resources/debug/debug.resource.d.ts +7 -0
- package/dist/globals/resources/debug/debug.tag.d.ts +2 -0
- package/dist/globals/resources/debug/debugConfig.resource.d.ts +22 -0
- package/dist/globals/resources/debug/executionTracker.middleware.d.ts +50 -0
- package/dist/globals/resources/debug/globalEvent.hook.d.ts +27 -0
- package/dist/globals/resources/debug/hook.hook.d.ts +30 -0
- package/dist/globals/resources/debug/index.d.ts +6 -0
- package/dist/globals/resources/debug/middleware.hook.d.ts +30 -0
- package/dist/globals/resources/debug/types.d.ts +25 -0
- package/dist/globals/resources/debug/utils.d.ts +2 -0
- package/dist/globals/resources/queue.resource.d.ts +10 -0
- package/dist/globals/resources/tunnel/ejson-extensions.d.ts +1 -0
- package/dist/globals/resources/tunnel/error-utils.d.ts +1 -0
- package/dist/globals/resources/tunnel/plan.d.ts +19 -0
- package/dist/globals/resources/tunnel/protocol.d.ts +40 -0
- package/dist/globals/resources/tunnel/serializer.d.ts +9 -0
- package/dist/globals/resources/tunnel/tunnel.policy.tag.d.ts +18 -0
- package/dist/globals/resources/tunnel/tunnel.tag.d.ts +2 -0
- package/dist/globals/resources/tunnel/types.d.ts +17 -0
- package/dist/globals/tunnels/index.d.ts +23 -0
- package/dist/globals/types.d.ts +1 -0
- package/dist/http-client.d.ts +23 -0
- package/dist/http-fetch-tunnel.resource.d.ts +22 -0
- package/dist/index.d.ts +99 -0
- package/dist/models/DependencyProcessor.d.ts +48 -0
- package/dist/models/EventManager.d.ts +153 -0
- package/dist/models/LogPrinter.d.ts +55 -0
- package/dist/models/Logger.d.ts +85 -0
- package/dist/models/MiddlewareManager.d.ts +86 -0
- package/dist/models/OverrideManager.d.ts +13 -0
- package/dist/models/Queue.d.ts +26 -0
- package/dist/models/ResourceInitializer.d.ts +20 -0
- package/dist/models/RunResult.d.ts +35 -0
- package/dist/models/Semaphore.d.ts +61 -0
- package/dist/models/Store.d.ts +69 -0
- package/dist/models/StoreRegistry.d.ts +43 -0
- package/dist/models/StoreValidator.d.ts +8 -0
- package/dist/models/TaskRunner.d.ts +27 -0
- package/dist/models/UnhandledError.d.ts +11 -0
- package/dist/models/index.d.ts +11 -0
- package/dist/models/utils/findCircularDependencies.d.ts +16 -0
- package/dist/models/utils/safeStringify.d.ts +3 -0
- package/dist/node/exposure/allowList.d.ts +3 -0
- package/dist/node/exposure/authenticator.d.ts +6 -0
- package/dist/node/exposure/cors.d.ts +4 -0
- package/dist/node/exposure/createNodeExposure.d.ts +2 -0
- package/dist/node/exposure/exposureServer.d.ts +18 -0
- package/dist/node/exposure/httpResponse.d.ts +10 -0
- package/dist/node/exposure/logging.d.ts +4 -0
- package/dist/node/exposure/multipart.d.ts +27 -0
- package/dist/node/exposure/requestBody.d.ts +11 -0
- package/dist/node/exposure/requestContext.d.ts +17 -0
- package/dist/node/exposure/requestHandlers.d.ts +24 -0
- package/dist/node/exposure/resourceTypes.d.ts +60 -0
- package/dist/node/exposure/router.d.ts +17 -0
- package/dist/node/exposure/serverLifecycle.d.ts +13 -0
- package/dist/node/exposure/types.d.ts +31 -0
- package/dist/node/exposure/utils.d.ts +17 -0
- package/dist/node/exposure.resource.d.ts +12 -0
- package/dist/node/files.d.ts +9 -0
- package/dist/node/http-smart-client.model.d.ts +22 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/inputFile.model.d.ts +22 -0
- package/dist/node/inputFile.utils.d.ts +14 -0
- package/dist/node/mixed-http-client.node.d.ts +27 -0
- package/dist/node/node.cjs +11168 -0
- package/dist/node/node.cjs.map +1 -0
- package/dist/node/node.d.ts +6 -0
- package/dist/node/node.mjs +11099 -0
- package/dist/node/node.mjs.map +1 -0
- package/dist/node/platform/createFile.d.ts +9 -0
- package/dist/node/tunnel.allowlist.d.ts +7 -0
- package/dist/node/upload/manifest.d.ts +22 -0
- package/dist/platform/adapters/browser.d.ts +14 -0
- package/dist/platform/adapters/edge.d.ts +5 -0
- package/dist/platform/adapters/node-als.d.ts +1 -0
- package/dist/platform/adapters/node.d.ts +15 -0
- package/dist/platform/adapters/universal-generic.d.ts +14 -0
- package/dist/platform/adapters/universal.d.ts +17 -0
- package/dist/platform/createFile.d.ts +10 -0
- package/dist/platform/createWebFile.d.ts +11 -0
- package/dist/platform/factory.d.ts +2 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/types.d.ts +29 -0
- package/dist/processHooks.d.ts +2 -0
- package/dist/run.d.ts +14 -0
- package/dist/testing.d.ts +25 -0
- package/dist/tools/getCallerFile.d.ts +1 -0
- package/dist/tunnels/buildUniversalManifest.d.ts +24 -0
- package/dist/types/contracts.d.ts +63 -0
- package/dist/types/event.d.ts +74 -0
- package/dist/types/hook.d.ts +23 -0
- package/dist/types/inputFile.d.ts +34 -0
- package/dist/types/meta.d.ts +18 -0
- package/dist/types/resource.d.ts +87 -0
- package/dist/types/resourceMiddleware.d.ts +47 -0
- package/dist/types/runner.d.ts +55 -0
- package/dist/types/storeTypes.d.ts +40 -0
- package/dist/types/symbols.d.ts +28 -0
- package/dist/types/tag.d.ts +46 -0
- package/dist/types/task.d.ts +50 -0
- package/dist/types/taskMiddleware.d.ts +48 -0
- package/dist/types/utilities.d.ts +111 -0
- package/dist/universal/index.cjs +1438 -251
- package/dist/universal/index.cjs.map +1 -1
- package/dist/universal/index.mjs +1433 -252
- package/dist/universal/index.mjs.map +1 -1
- package/package.json +32 -4
- package/dist/index.d.mts +0 -1747
- package/dist/index.unused.js +0 -4466
- package/dist/index.unused.js.map +0 -1
- package/dist/node/index.cjs +0 -4498
- package/dist/node/index.cjs.map +0 -1
- package/dist/node/index.mjs +0 -4466
- package/dist/node/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -9,18 +9,19 @@ _Or: How I Learned to Stop Worrying and Love Dependency Injection_
|
|
|
9
9
|
<a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
| Resource
|
|
13
|
-
|
|
|
14
|
-
| [Presentation Website](https://runner.bluelibs.com/)
|
|
15
|
-
| [BlueLibs Runner GitHub](https://github.com/bluelibs/runner)
|
|
16
|
-
| [BlueLibs Runner Dev](https://github.com/bluelibs/runner-dev)
|
|
17
|
-
| [UX Friendly Docs](https://bluelibs.github.io/runner/)
|
|
18
|
-
| [AI Friendly Docs (<5000 tokens)](https://github.com/bluelibs/runner/blob/main/AI.md)
|
|
19
|
-
| [Migrate from 3.x.x to 4.x.x](https://github.com/bluelibs/runner/blob/main/readmes/MIGRATION.md)
|
|
20
|
-
| [Runner Lore](https://github.com/bluelibs/runner/blob/main/readmes)
|
|
21
|
-
| [Example: Express + OpenAPI + SQLite](https://github.com/bluelibs/runner/tree/main/examples/express-openapi-sqlite)
|
|
22
|
-
| [Example: Fastify + MikroORM + PostgreSQL](https://github.com/bluelibs/runner/tree/main/examples/fastify-mikroorm)
|
|
23
|
-
| [
|
|
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
|
+
| [Example: Streaming Append Route](https://github.com/bluelibs/runner/blob/main/examples/streaming-append.example.ts) | Example | Demonstrates request/response streaming |
|
|
24
|
+
| [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
|
|
|
25
26
|
### Community & Policies
|
|
26
27
|
|
|
@@ -55,73 +56,189 @@ Here's a complete Express server in less lines than most frameworks need for the
|
|
|
55
56
|
|
|
56
57
|
```typescript
|
|
57
58
|
import express from "express";
|
|
58
|
-
import {
|
|
59
|
+
import { r, run, globals } from "@bluelibs/runner";
|
|
60
|
+
import { nodeExposure } from "@bluelibs/runner/node";
|
|
59
61
|
|
|
60
62
|
// A resource is anything you want to share across your app, a singleton
|
|
61
|
-
const server =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
})
|
|
63
|
+
const server = r
|
|
64
|
+
.resource<{ port: number }>("app.server")
|
|
65
|
+
.context(() => ({ app: express() }))
|
|
66
|
+
.init(async ({ port }, _deps, ctx) => {
|
|
67
|
+
ctx.app.use(express.json());
|
|
68
|
+
const listener = ctx.app.listen(port);
|
|
69
|
+
console.log(`Server running on port ${port}`);
|
|
70
|
+
return { ...ctx, listener };
|
|
71
|
+
})
|
|
72
|
+
.dispose(async ({ listener }) => listener.close())
|
|
73
|
+
.build();
|
|
71
74
|
|
|
72
75
|
// Tasks are your business logic - easily testable functions
|
|
73
|
-
const createUser =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
run
|
|
78
|
-
|
|
79
|
-
return { id: "user-123",
|
|
80
|
-
}
|
|
81
|
-
|
|
76
|
+
const createUser = r
|
|
77
|
+
.task("app.tasks.createUser")
|
|
78
|
+
.dependencies({ server, logger: globals.resources.logger })
|
|
79
|
+
.inputSchema<{ name: string }>({ parse: (value) => value })
|
|
80
|
+
.run(async ({ input }, { server, logger }) => {
|
|
81
|
+
await logger.info(`Creating ${input.name}`);
|
|
82
|
+
return { id: "user-123", name: input.name };
|
|
83
|
+
})
|
|
84
|
+
.build();
|
|
82
85
|
|
|
83
86
|
// Wire everything together
|
|
84
|
-
const app =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const app = r
|
|
88
|
+
.resource("app")
|
|
89
|
+
.register([
|
|
90
|
+
server.with({ port: 3000 }),
|
|
91
|
+
nodeExposure.with({
|
|
92
|
+
http: { basePath: "/__runner", listen: { port: 3000 } },
|
|
93
|
+
}),
|
|
94
|
+
createUser,
|
|
95
|
+
])
|
|
96
|
+
.dependencies({ server, createUser })
|
|
97
|
+
.init(async (_config, { server, createUser }) => {
|
|
98
|
+
server.listener.on("listening", () => {
|
|
99
|
+
console.log("Runner HTTP server ready");
|
|
100
|
+
});
|
|
101
|
+
|
|
90
102
|
server.app.post("/users", async (req, res) => {
|
|
91
103
|
const user = await createUser(req.body);
|
|
92
104
|
res.json(user);
|
|
93
105
|
});
|
|
94
|
-
}
|
|
95
|
-
|
|
106
|
+
})
|
|
107
|
+
.build();
|
|
96
108
|
|
|
97
109
|
// That's it. Each run is fully isolated
|
|
98
|
-
const
|
|
110
|
+
const runtime = await run(app);
|
|
111
|
+
const { dispose, runTask, getResourceValue, emitEvent } = runtime;
|
|
99
112
|
|
|
100
113
|
// Or with debug logging enabled
|
|
101
|
-
|
|
114
|
+
await run(app, { debug: "verbose" });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Classic API (still supported)
|
|
118
|
+
|
|
119
|
+
Prefer fluent builders for new code, but the classic `define`-style API remains supported and can be mixed in the same app:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { resource, task, run } from "@bluelibs/runner";
|
|
123
|
+
|
|
124
|
+
const db = resource({ id: "app.db", init: async () => "conn" });
|
|
125
|
+
const add = task({
|
|
126
|
+
id: "app.tasks.add",
|
|
127
|
+
run: async (i: { a: number; b: number }) => i.a + i.b,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const app = resource({ id: "app", register: [db, add] });
|
|
131
|
+
await run(app);
|
|
102
132
|
```
|
|
103
133
|
|
|
134
|
+
See readmes/FLUENT_BUILDERS.md for migration tips and side‑by‑side patterns.
|
|
135
|
+
|
|
104
136
|
### Platform & Async Context
|
|
105
137
|
|
|
106
138
|
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.
|
|
107
139
|
|
|
140
|
+
### Serialization (EJSON)
|
|
141
|
+
|
|
142
|
+
Runner uses 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.
|
|
143
|
+
|
|
144
|
+
- By default, Runner’s HTTP clients and exposures use the EJSON serializer
|
|
145
|
+
- You can call `getDefaultSerializer()` for the shared serializer instance
|
|
146
|
+
- A global serializer is also exposed as a resource: `globals.resources.serializer`
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { getDefaultSerializer, EJSON, r, globals } from "@bluelibs/runner";
|
|
150
|
+
|
|
151
|
+
// 1) Quick use
|
|
152
|
+
const s = getDefaultSerializer();
|
|
153
|
+
const text = s.stringify({ when: new Date() });
|
|
154
|
+
const obj = s.parse<{ when: Date }>(text);
|
|
155
|
+
|
|
156
|
+
// 2) Register custom EJSON types centrally via the global serializer resource
|
|
157
|
+
const ejsonSetup = r
|
|
158
|
+
.resource("app.serialization.setup")
|
|
159
|
+
.dependencies({ serializer: globals.resources.serializer })
|
|
160
|
+
.init(async (_config, { serializer }) => {
|
|
161
|
+
class Distance {
|
|
162
|
+
constructor(public value: number, public unit: string) {}
|
|
163
|
+
toJSONValue() {
|
|
164
|
+
return { value: this.value, unit: this.unit } as const;
|
|
165
|
+
}
|
|
166
|
+
typeName() {
|
|
167
|
+
return "Distance" as const;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
serializer.addType(
|
|
172
|
+
"Distance",
|
|
173
|
+
(j: { value: number; unit: string }) => new Distance(j.value, j.unit),
|
|
174
|
+
);
|
|
175
|
+
})
|
|
176
|
+
.build();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Tunnels: Bridging Runners
|
|
180
|
+
|
|
181
|
+
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.
|
|
182
|
+
|
|
183
|
+
Here's a sneak peek of how you can expose your application and configure a client tunnel to consume a remote Runner:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { r, globals } from "@bluelibs/runner";
|
|
187
|
+
import { nodeExposure } from "@bluelibs/runner/node";
|
|
188
|
+
|
|
189
|
+
// 1. Expose your local tasks and events over HTTP
|
|
190
|
+
const app = r
|
|
191
|
+
.resource("app")
|
|
192
|
+
.register([
|
|
193
|
+
// ... your tasks and events
|
|
194
|
+
nodeExposure.with({
|
|
195
|
+
http: {
|
|
196
|
+
basePath: "/__runner",
|
|
197
|
+
listen: { port: 7070 },
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
])
|
|
201
|
+
.build();
|
|
202
|
+
|
|
203
|
+
// 2. In another app, define a tunnel resource to call a remote Runner
|
|
204
|
+
const httpClientTunnel = r
|
|
205
|
+
.resource("app.tunnels.http")
|
|
206
|
+
.tags([globals.tags.tunnel])
|
|
207
|
+
.init(async () => ({
|
|
208
|
+
mode: "client" as const,
|
|
209
|
+
transport: "http" as const,
|
|
210
|
+
// Selectively forward tasks starting with "remote.tasks."
|
|
211
|
+
tasks: (t) => t.id.startsWith("remote.tasks."),
|
|
212
|
+
client: globals.tunnels.http.createClient({
|
|
213
|
+
url: "http://remote-runner:8080/__runner",
|
|
214
|
+
}),
|
|
215
|
+
}))
|
|
216
|
+
.build();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This is just a glimpse. With tunnels, you can build microservices, CLIs, and admin panels that interact with your main application securely and efficiently.
|
|
220
|
+
|
|
221
|
+
For a deep dive into streaming, authentication, file uploads, and more, check out the [full Tunnels documentation](./readmes/TUNNELS.md).
|
|
222
|
+
|
|
108
223
|
## The Big Five
|
|
109
224
|
|
|
110
225
|
The framework is built around five core concepts: Tasks, Resources, Events, Middleware, and Tags. Understanding them is key to using the runner effectively.
|
|
111
226
|
|
|
112
227
|
### Tasks
|
|
113
228
|
|
|
114
|
-
Tasks are functions with superpowers. They're testable, and
|
|
229
|
+
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.
|
|
115
230
|
|
|
116
231
|
```typescript
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
232
|
+
import { r } from "@bluelibs/runner";
|
|
233
|
+
|
|
234
|
+
const sendEmail = r
|
|
235
|
+
.task("app.tasks.sendEmail")
|
|
236
|
+
.dependencies({ emailService, logger })
|
|
237
|
+
.run(async ({ input }, { emailService, logger }) => {
|
|
238
|
+
await logger.info(`Sending email to ${input.to}`);
|
|
239
|
+
return emailService.send(input);
|
|
240
|
+
})
|
|
241
|
+
.build();
|
|
125
242
|
|
|
126
243
|
// Test it like a normal function (because it basically is)
|
|
127
244
|
const result = await sendEmail.run(
|
|
@@ -149,32 +266,33 @@ Think of tasks as the "main characters" in your application story, not every sin
|
|
|
149
266
|
|
|
150
267
|
### Resources
|
|
151
268
|
|
|
152
|
-
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.
|
|
269
|
+
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.
|
|
153
270
|
|
|
154
271
|
```typescript
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
272
|
+
import { r } from "@bluelibs/runner";
|
|
273
|
+
|
|
274
|
+
const database = r
|
|
275
|
+
.resource("app.db")
|
|
276
|
+
.init(async () => {
|
|
158
277
|
const client = new MongoClient(process.env.DATABASE_URL as string);
|
|
159
278
|
await client.connect();
|
|
160
|
-
|
|
161
279
|
return client;
|
|
162
|
-
}
|
|
163
|
-
dispose
|
|
164
|
-
|
|
280
|
+
})
|
|
281
|
+
.dispose(async (client) => client.close())
|
|
282
|
+
.build();
|
|
165
283
|
|
|
166
|
-
const userService =
|
|
167
|
-
|
|
168
|
-
dependencies
|
|
169
|
-
init
|
|
284
|
+
const userService = r
|
|
285
|
+
.resource("app.services.user")
|
|
286
|
+
.dependencies({ database })
|
|
287
|
+
.init(async (_config, { database }) => ({
|
|
170
288
|
async createUser(userData: UserData) {
|
|
171
289
|
return database.collection("users").insertOne(userData);
|
|
172
290
|
},
|
|
173
291
|
async getUser(id: string) {
|
|
174
292
|
return database.collection("users").findOne({ _id: id });
|
|
175
293
|
},
|
|
176
|
-
})
|
|
177
|
-
|
|
294
|
+
}))
|
|
295
|
+
.build();
|
|
178
296
|
```
|
|
179
297
|
|
|
180
298
|
#### Resource Configuration
|
|
@@ -187,26 +305,26 @@ type SMTPConfig = {
|
|
|
187
305
|
from: string;
|
|
188
306
|
};
|
|
189
307
|
|
|
190
|
-
const emailer =
|
|
191
|
-
|
|
192
|
-
init
|
|
308
|
+
const emailer = r
|
|
309
|
+
.resource<{ smtpUrl: string; from: string }>("app.emailer")
|
|
310
|
+
.init(async (config) => ({
|
|
193
311
|
send: async (to: string, subject: string, body: string) => {
|
|
194
312
|
// Use config.smtpUrl and config.from
|
|
195
313
|
},
|
|
196
|
-
})
|
|
197
|
-
|
|
314
|
+
}))
|
|
315
|
+
.build();
|
|
198
316
|
|
|
199
317
|
// Register with specific config
|
|
200
|
-
const app =
|
|
201
|
-
|
|
202
|
-
register
|
|
318
|
+
const app = r
|
|
319
|
+
.resource("app")
|
|
320
|
+
.register([
|
|
203
321
|
emailer.with({
|
|
204
322
|
smtpUrl: "smtp://localhost",
|
|
205
323
|
from: "noreply@myapp.com",
|
|
206
324
|
}),
|
|
207
325
|
// using emailer without with() will throw a type-error ;)
|
|
208
|
-
]
|
|
209
|
-
|
|
326
|
+
])
|
|
327
|
+
.build();
|
|
210
328
|
```
|
|
211
329
|
|
|
212
330
|
#### Private Context
|
|
@@ -214,28 +332,27 @@ const app = resource({
|
|
|
214
332
|
For cases where you need to share variables between `init()` and `dispose()` methods (because sometimes cleanup is complicated), use the enhanced context pattern:
|
|
215
333
|
|
|
216
334
|
```typescript
|
|
217
|
-
const dbResource =
|
|
218
|
-
|
|
219
|
-
context
|
|
220
|
-
connections: new Map(),
|
|
221
|
-
pools: []
|
|
222
|
-
})
|
|
223
|
-
async
|
|
335
|
+
const dbResource = r
|
|
336
|
+
.resource("db.service")
|
|
337
|
+
.context(() => ({
|
|
338
|
+
connections: new Map<string, unknown>(),
|
|
339
|
+
pools: [] as Array<{ drain(): Promise<void> }>,
|
|
340
|
+
}))
|
|
341
|
+
.init(async (_config, _deps, ctx) => {
|
|
224
342
|
const db = await connectToDatabase();
|
|
225
343
|
ctx.connections.set("main", db);
|
|
226
344
|
ctx.pools.push(createPool(db));
|
|
227
345
|
return db;
|
|
228
|
-
}
|
|
229
|
-
async
|
|
230
|
-
// This is to avoid exposing internals as resource result.
|
|
346
|
+
})
|
|
347
|
+
.dispose(async (_db, _config, _deps, ctx) => {
|
|
231
348
|
for (const pool of ctx.pools) {
|
|
232
349
|
await pool.drain();
|
|
233
350
|
}
|
|
234
|
-
for (const [
|
|
235
|
-
await conn.close();
|
|
351
|
+
for (const [, conn] of ctx.connections) {
|
|
352
|
+
await (conn as { close(): Promise<void> }).close();
|
|
236
353
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
354
|
+
})
|
|
355
|
+
.build();
|
|
239
356
|
```
|
|
240
357
|
|
|
241
358
|
### Events
|
|
@@ -243,34 +360,30 @@ const dbResource = resource({
|
|
|
243
360
|
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.
|
|
244
361
|
|
|
245
362
|
```typescript
|
|
246
|
-
|
|
247
|
-
id: "app.events.userRegistered",
|
|
248
|
-
});
|
|
363
|
+
import { r } from "@bluelibs/runner";
|
|
249
364
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const user = await userService.createUser(userData);
|
|
365
|
+
const userRegistered = r
|
|
366
|
+
.event("app.events.userRegistered")
|
|
367
|
+
.payloadSchema<{ userId: string; email: string }>({ parse: (value) => value })
|
|
368
|
+
.build();
|
|
255
369
|
|
|
256
|
-
|
|
257
|
-
|
|
370
|
+
const registerUser = r
|
|
371
|
+
.task("app.tasks.registerUser")
|
|
372
|
+
.dependencies({ userService, userRegistered })
|
|
373
|
+
.run(async ({ input }, { userService, userRegistered }) => {
|
|
374
|
+
const user = await userService.createUser(input);
|
|
375
|
+
await userRegistered.emit({ userId: user.id, email: user.email });
|
|
258
376
|
return user;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// Someone else handles the welcome email using a hook
|
|
263
|
-
// This is great for building very modular apps
|
|
264
|
-
import { hook } from "@bluelibs/runner";
|
|
377
|
+
})
|
|
378
|
+
.build();
|
|
265
379
|
|
|
266
|
-
const sendWelcomeEmail =
|
|
267
|
-
|
|
268
|
-
on
|
|
269
|
-
run
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
380
|
+
const sendWelcomeEmail = r
|
|
381
|
+
.hook("app.hooks.sendWelcomeEmail")
|
|
382
|
+
.on(userRegistered)
|
|
383
|
+
.run(async (event) => {
|
|
384
|
+
console.log(`Welcome email sent to ${event.data.email}`);
|
|
385
|
+
})
|
|
386
|
+
.build();
|
|
274
387
|
```
|
|
275
388
|
|
|
276
389
|
#### Wildcard Events
|
|
@@ -278,16 +391,13 @@ const sendWelcomeEmail = hook({
|
|
|
278
391
|
Sometimes you need to be the nosy neighbor of your application:
|
|
279
392
|
|
|
280
393
|
```typescript
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
on: "*", // Listen to EVERYTHING
|
|
286
|
-
run(event) {
|
|
394
|
+
const logAllEventsHook = r
|
|
395
|
+
.hook("app.hooks.logAllEvents")
|
|
396
|
+
.on("*")
|
|
397
|
+
.run((event) => {
|
|
287
398
|
console.log("Event detected", event.id, event.data);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
});
|
|
399
|
+
})
|
|
400
|
+
.build();
|
|
291
401
|
```
|
|
292
402
|
|
|
293
403
|
#### Excluding Events from Global Listeners
|
|
@@ -295,13 +405,13 @@ const logAllEventsHook = hook({
|
|
|
295
405
|
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:
|
|
296
406
|
|
|
297
407
|
```typescript
|
|
298
|
-
import {
|
|
408
|
+
import { r, globals } from "@bluelibs/runner";
|
|
299
409
|
|
|
300
410
|
// Internal event that won't be seen by global listeners
|
|
301
|
-
const internalEvent =
|
|
302
|
-
|
|
303
|
-
tags
|
|
304
|
-
|
|
411
|
+
const internalEvent = r
|
|
412
|
+
.event("app.events.internal")
|
|
413
|
+
.tags([globals.tags.excludeFromGlobalHooks])
|
|
414
|
+
.build();
|
|
305
415
|
```
|
|
306
416
|
|
|
307
417
|
**When to exclude events from global listeners:**
|
|
@@ -317,16 +427,14 @@ const internalEvent = event({
|
|
|
317
427
|
The modern way to listen to events is through hooks. They are lightweight event listeners, similar to tasks, but with a few key differences.
|
|
318
428
|
|
|
319
429
|
```typescript
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
dependencies: { logger },
|
|
326
|
-
run: async (event, { logger }) => {
|
|
430
|
+
const myHook = r
|
|
431
|
+
.hook("app.hooks.myEventHandler")
|
|
432
|
+
.on(userRegistered)
|
|
433
|
+
.dependencies({ logger })
|
|
434
|
+
.run(async (event, { logger }) => {
|
|
327
435
|
await logger.info(`User registered: ${event.data.email}`);
|
|
328
|
-
}
|
|
329
|
-
|
|
436
|
+
})
|
|
437
|
+
.build();
|
|
330
438
|
```
|
|
331
439
|
|
|
332
440
|
#### Multiple Events (type-safe intersection)
|
|
@@ -334,34 +442,43 @@ const myHook = hook({
|
|
|
334
442
|
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.
|
|
335
443
|
|
|
336
444
|
```typescript
|
|
337
|
-
import {
|
|
445
|
+
import { r, onAnyOf, isOneOf } from "@bluelibs/runner";
|
|
338
446
|
|
|
339
|
-
const eUser =
|
|
340
|
-
|
|
341
|
-
id:
|
|
342
|
-
|
|
343
|
-
const
|
|
447
|
+
const eUser = r
|
|
448
|
+
.event("app.events.user")
|
|
449
|
+
.payloadSchema<{ id: string; email: string }>({ parse: (v) => v })
|
|
450
|
+
.build();
|
|
451
|
+
const eAdmin = r
|
|
452
|
+
.event("app.events.admin")
|
|
453
|
+
.payloadSchema<{ id: string; role: "admin" | "superadmin" }>({
|
|
454
|
+
parse: (v) => v,
|
|
455
|
+
})
|
|
456
|
+
.build();
|
|
457
|
+
const eGuest = r
|
|
458
|
+
.event("app.events.guest")
|
|
459
|
+
.payloadSchema<{ id: string; guest: true }>({ parse: (v) => v })
|
|
460
|
+
.build();
|
|
344
461
|
|
|
345
462
|
// The common field across all three is { id: string }
|
|
346
|
-
const auditUsers =
|
|
347
|
-
|
|
348
|
-
on
|
|
349
|
-
run
|
|
463
|
+
const auditUsers = r
|
|
464
|
+
.hook("app.hooks.auditUsers")
|
|
465
|
+
.on([eUser, eAdmin, eGuest])
|
|
466
|
+
.run(async (ev) => {
|
|
350
467
|
ev.data.id; // OK: common field inferred
|
|
351
468
|
// ev.data.email; // TS error: not common to all
|
|
352
|
-
}
|
|
353
|
-
|
|
469
|
+
})
|
|
470
|
+
.build();
|
|
354
471
|
|
|
355
472
|
// Guard usage to refine at runtime (still narrows to common payload)
|
|
356
|
-
const auditSome =
|
|
357
|
-
|
|
358
|
-
on
|
|
359
|
-
run
|
|
473
|
+
const auditSome = r
|
|
474
|
+
.hook("app.hooks.auditSome")
|
|
475
|
+
.on(onAnyOf([eUser, eAdmin])) // to get a combined event
|
|
476
|
+
.run(async (ev) => {
|
|
360
477
|
if (isOneOf(ev, [eUser, eAdmin])) {
|
|
361
478
|
ev.data.id; // common field of eUser and eAdmin
|
|
362
479
|
}
|
|
363
|
-
}
|
|
364
|
-
|
|
480
|
+
})
|
|
481
|
+
.build();
|
|
365
482
|
```
|
|
366
483
|
|
|
367
484
|
Notes:
|
|
@@ -389,13 +506,13 @@ The framework exposes a minimal system-level event for observability:
|
|
|
389
506
|
```typescript
|
|
390
507
|
import { globals } from "@bluelibs/runner";
|
|
391
508
|
|
|
392
|
-
const systemReadyHook =
|
|
393
|
-
|
|
394
|
-
on
|
|
395
|
-
run
|
|
509
|
+
const systemReadyHook = r
|
|
510
|
+
.hook("app.hooks.systemReady")
|
|
511
|
+
.on(globals.events.ready)
|
|
512
|
+
.run(async () => {
|
|
396
513
|
console.log("🚀 System is ready and operational!");
|
|
397
|
-
}
|
|
398
|
-
|
|
514
|
+
})
|
|
515
|
+
.build();
|
|
399
516
|
```
|
|
400
517
|
|
|
401
518
|
Available system event:
|
|
@@ -408,24 +525,23 @@ Available system event:
|
|
|
408
525
|
Sometimes you need to prevent other event listeners from processing an event. The `stopPropagation()` method gives you fine-grained control over event flow:
|
|
409
526
|
|
|
410
527
|
```typescript
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
meta: {
|
|
528
|
+
const criticalAlert = r
|
|
529
|
+
.event("app.events.alert")
|
|
530
|
+
.payloadSchema<{ severity: "low" | "medium" | "high" | "critical" }>({
|
|
531
|
+
parse: (v) => v,
|
|
532
|
+
})
|
|
533
|
+
.meta({
|
|
418
534
|
title: "System Alert Event",
|
|
419
535
|
description: "Emitted when system issues are detected",
|
|
420
|
-
}
|
|
421
|
-
|
|
536
|
+
})
|
|
537
|
+
.build();
|
|
422
538
|
|
|
423
539
|
// High-priority handler that can stop propagation
|
|
424
|
-
const emergencyHandler =
|
|
425
|
-
|
|
426
|
-
on
|
|
427
|
-
order
|
|
428
|
-
run
|
|
540
|
+
const emergencyHandler = r
|
|
541
|
+
.hook("app.hooks.emergencyHandler")
|
|
542
|
+
.on(criticalAlert)
|
|
543
|
+
.order(-100) // Higher priority (lower numbers run first)
|
|
544
|
+
.run(async (event) => {
|
|
429
545
|
console.log(`Alert received: ${event.data.severity}`);
|
|
430
546
|
|
|
431
547
|
if (event.data.severity === "critical") {
|
|
@@ -437,8 +553,8 @@ const emergencyHandler = hook({
|
|
|
437
553
|
|
|
438
554
|
console.log("🛑 Event propagation stopped - emergency protocols active");
|
|
439
555
|
}
|
|
440
|
-
}
|
|
441
|
-
|
|
556
|
+
})
|
|
557
|
+
.build();
|
|
442
558
|
```
|
|
443
559
|
|
|
444
560
|
> **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."
|
|
@@ -450,23 +566,23 @@ Middleware wraps around your tasks and resources, adding cross-cutting concerns
|
|
|
450
566
|
Note: Middleware is now split by target. Use `taskMiddleware(...)` for task middleware and `resourceMiddleware(...)` for resource middleware.
|
|
451
567
|
|
|
452
568
|
```typescript
|
|
453
|
-
import {
|
|
569
|
+
import { r } from "@bluelibs/runner";
|
|
454
570
|
|
|
455
571
|
// Task middleware with config
|
|
456
572
|
type AuthMiddlewareConfig = { requiredRole: string };
|
|
457
|
-
const authMiddleware =
|
|
458
|
-
|
|
459
|
-
run
|
|
573
|
+
const authMiddleware = r.middleware
|
|
574
|
+
.task("app.middleware.task.auth")
|
|
575
|
+
.run(async ({ task, next }, _deps, config: AuthMiddlewareConfig) => {
|
|
460
576
|
// Must return the value
|
|
461
|
-
return await next(task.input);
|
|
462
|
-
}
|
|
463
|
-
|
|
577
|
+
return await next(task.input as any);
|
|
578
|
+
})
|
|
579
|
+
.build();
|
|
464
580
|
|
|
465
|
-
const adminTask =
|
|
466
|
-
|
|
467
|
-
middleware
|
|
468
|
-
run
|
|
469
|
-
|
|
581
|
+
const adminTask = r
|
|
582
|
+
.task("app.tasks.adminOnly")
|
|
583
|
+
.middleware([authMiddleware.with({ requiredRole: "admin" })])
|
|
584
|
+
.run(async ({ input }: { input: { user: User } }) => "Secret admin data")
|
|
585
|
+
.build();
|
|
470
586
|
```
|
|
471
587
|
|
|
472
588
|
For middleware with input/output contracts:
|
|
@@ -477,42 +593,38 @@ type AuthConfig = { requiredRole: string };
|
|
|
477
593
|
type AuthInput = { user: { role: string } };
|
|
478
594
|
type AuthOutput = { user: { role: string; verified: boolean } };
|
|
479
595
|
|
|
480
|
-
const authMiddleware =
|
|
481
|
-
|
|
482
|
-
run
|
|
483
|
-
if (task.input.user.role !== config.requiredRole) {
|
|
596
|
+
const authMiddleware = r.middleware
|
|
597
|
+
.task("app.middleware.task.auth")
|
|
598
|
+
.run(async ({ task, next }, _deps, config: AuthConfig) => {
|
|
599
|
+
if ((task.input as AuthInput).user.role !== config.requiredRole) {
|
|
484
600
|
throw new Error("Insufficient permissions");
|
|
485
601
|
}
|
|
486
602
|
const result = await next(task.input);
|
|
487
603
|
return {
|
|
488
604
|
user: {
|
|
489
|
-
...task.input.user,
|
|
605
|
+
...(task.input as AuthInput).user,
|
|
490
606
|
verified: true,
|
|
491
607
|
},
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
608
|
+
} as AuthOutput;
|
|
609
|
+
})
|
|
610
|
+
.build();
|
|
495
611
|
|
|
496
612
|
// For resources
|
|
497
|
-
const resourceAuthMiddleware =
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
AuthOutput
|
|
501
|
-
>({
|
|
502
|
-
id: "app.middleware.resource.auth",
|
|
503
|
-
run: async ({ next }, _deps, config) => {
|
|
613
|
+
const resourceAuthMiddleware = r.middleware
|
|
614
|
+
.resource("app.middleware.resource.auth")
|
|
615
|
+
.run(async ({ next }) => {
|
|
504
616
|
// Resource middleware logic
|
|
505
617
|
return await next();
|
|
506
|
-
}
|
|
507
|
-
|
|
618
|
+
})
|
|
619
|
+
.build();
|
|
508
620
|
|
|
509
|
-
const adminTask =
|
|
510
|
-
|
|
511
|
-
middleware
|
|
512
|
-
run
|
|
621
|
+
const adminTask = r
|
|
622
|
+
.task("app.tasks.adminOnly")
|
|
623
|
+
.middleware([authMiddleware.with({ requiredRole: "admin" })])
|
|
624
|
+
.run(async ({ input }: { input: { user: { role: string } } }) => ({
|
|
513
625
|
user: { role: input.user.role, verified: true },
|
|
514
|
-
})
|
|
515
|
-
|
|
626
|
+
}))
|
|
627
|
+
.build();
|
|
516
628
|
```
|
|
517
629
|
|
|
518
630
|
#### Global Middleware
|
|
@@ -520,23 +632,19 @@ const adminTask = task({
|
|
|
520
632
|
Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
|
|
521
633
|
|
|
522
634
|
```typescript
|
|
523
|
-
import {
|
|
635
|
+
import { r, globals } from "@bluelibs/runner";
|
|
524
636
|
|
|
525
|
-
const logTaskMiddleware =
|
|
526
|
-
|
|
527
|
-
everywhere
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return true;
|
|
531
|
-
}, // true means it gets included.
|
|
532
|
-
dependencies: { logger: globals.resources.logger },
|
|
533
|
-
run: async ({ task, next }, { logger }) => {
|
|
637
|
+
const logTaskMiddleware = r.middleware
|
|
638
|
+
.task("app.middleware.log.task")
|
|
639
|
+
.everywhere(() => true)
|
|
640
|
+
.dependencies({ logger: globals.resources.logger })
|
|
641
|
+
.run(async ({ task, next }, { logger }) => {
|
|
534
642
|
logger.info(`Executing: ${String(task!.definition.id)}`);
|
|
535
643
|
const result = await next(task!.input);
|
|
536
644
|
logger.info(`Completed: ${String(task!.definition.id)}`);
|
|
537
645
|
return result;
|
|
538
|
-
}
|
|
539
|
-
|
|
646
|
+
})
|
|
647
|
+
.build();
|
|
540
648
|
```
|
|
541
649
|
|
|
542
650
|
**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.
|
|
@@ -558,26 +666,33 @@ Access `eventManager` via `globals.resources.eventManager` if needed.
|
|
|
558
666
|
Middleware can now enforce type contracts using the `<Config, Input, Output>` signature:
|
|
559
667
|
|
|
560
668
|
```typescript
|
|
669
|
+
import { r } from "@bluelibs/runner";
|
|
670
|
+
|
|
561
671
|
// Middleware that transforms input and output types
|
|
562
672
|
type LogConfig = { includeTimestamp: boolean };
|
|
563
673
|
type LogInput = { data: any };
|
|
564
674
|
type LogOutput = { data: any; logged: boolean };
|
|
565
675
|
|
|
566
|
-
const loggingMiddleware =
|
|
567
|
-
|
|
568
|
-
run
|
|
569
|
-
console.log(
|
|
570
|
-
|
|
676
|
+
const loggingMiddleware = r.middleware
|
|
677
|
+
.task("app.middleware.logging")
|
|
678
|
+
.run(async ({ task, next }, _deps, config: LogConfig) => {
|
|
679
|
+
console.log(
|
|
680
|
+
config.includeTimestamp ? new Date() : "",
|
|
681
|
+
(task.input as LogInput).data,
|
|
682
|
+
);
|
|
683
|
+
const result = (await next(task.input)) as LogOutput;
|
|
571
684
|
return { ...result, logged: true };
|
|
572
|
-
}
|
|
573
|
-
|
|
685
|
+
})
|
|
686
|
+
.build();
|
|
574
687
|
|
|
575
688
|
// Tasks using this middleware must conform to the Input/Output types
|
|
576
|
-
const loggedTask =
|
|
577
|
-
|
|
578
|
-
middleware
|
|
579
|
-
run
|
|
580
|
-
|
|
689
|
+
const loggedTask = r
|
|
690
|
+
.task("app.tasks.logged")
|
|
691
|
+
.middleware([loggingMiddleware.with({ includeTimestamp: true })])
|
|
692
|
+
.run(async ({ input }: { input: { data: string } }) => ({
|
|
693
|
+
data: input.data.toUpperCase(),
|
|
694
|
+
}))
|
|
695
|
+
.build();
|
|
581
696
|
```
|
|
582
697
|
|
|
583
698
|
> **runtime:** "Ah, the onion pattern. A matryoshka doll made of promises. Every peel reveals… another logger. Another tracer. Another 'just a tiny wrapper'."
|
|
@@ -589,25 +704,23 @@ Tags are metadata that can influence system behavior. Unlike meta properties, ta
|
|
|
589
704
|
#### Basic Usage
|
|
590
705
|
|
|
591
706
|
```typescript
|
|
592
|
-
import {
|
|
707
|
+
import { r } from "@bluelibs/runner";
|
|
593
708
|
|
|
594
709
|
// Simple string tags
|
|
595
|
-
const apiTask =
|
|
596
|
-
|
|
597
|
-
tags
|
|
598
|
-
run
|
|
599
|
-
|
|
710
|
+
const apiTask = r
|
|
711
|
+
.task("app.tasks.getUserData")
|
|
712
|
+
.tags(["api", "public", "cacheable"])
|
|
713
|
+
.run(async ({ input }) => getUserFromDatabase(input as any))
|
|
714
|
+
.build();
|
|
600
715
|
|
|
601
716
|
// Structured tags with configuration
|
|
602
|
-
const httpTag = tag<{ method: string; path: string }>(
|
|
603
|
-
id: "http.route",
|
|
604
|
-
});
|
|
717
|
+
const httpTag = r.tag<{ method: string; path: string }>("http.route").build();
|
|
605
718
|
|
|
606
|
-
const getUserTask =
|
|
607
|
-
|
|
608
|
-
tags
|
|
609
|
-
run
|
|
610
|
-
|
|
719
|
+
const getUserTask = r
|
|
720
|
+
.task("app.tasks.getUser")
|
|
721
|
+
.tags(["api", httpTag.with({ method: "GET", path: "/users/:id" })])
|
|
722
|
+
.run(async ({ input }) => getUserFromDatabase((input as any).id))
|
|
723
|
+
.build();
|
|
611
724
|
```
|
|
612
725
|
|
|
613
726
|
#### Discovering Components by Tags
|
|
@@ -615,17 +728,14 @@ const getUserTask = task({
|
|
|
615
728
|
The core power of tags is runtime discovery. Use `store.getTasksWithTag()` to find components:
|
|
616
729
|
|
|
617
730
|
```typescript
|
|
618
|
-
import {
|
|
731
|
+
import { r, globals } from "@bluelibs/runner";
|
|
619
732
|
|
|
620
733
|
// Auto-register HTTP routes based on tags
|
|
621
|
-
const routeRegistration =
|
|
622
|
-
|
|
623
|
-
on
|
|
624
|
-
dependencies:
|
|
625
|
-
|
|
626
|
-
server: expressServer,
|
|
627
|
-
},
|
|
628
|
-
run: async (_, { store, server }) => {
|
|
734
|
+
const routeRegistration = r
|
|
735
|
+
.hook("app.hooks.registerRoutes")
|
|
736
|
+
.on(globals.events.ready)
|
|
737
|
+
.dependencies({ store: globals.resources.store, server: expressServer })
|
|
738
|
+
.run(async (_event, { store, server }) => {
|
|
629
739
|
// Find all tasks with HTTP tags
|
|
630
740
|
const apiTasks = store.getTasksWithTag(httpTag);
|
|
631
741
|
|
|
@@ -635,7 +745,7 @@ const routeRegistration = hook({
|
|
|
635
745
|
|
|
636
746
|
const { method, path } = config;
|
|
637
747
|
server.app[method.toLowerCase()](path, async (req, res) => {
|
|
638
|
-
const result = await taskDef({ ...req.params, ...req.body });
|
|
748
|
+
const result = await taskDef({ ...req.params, ...req.body } as any);
|
|
639
749
|
res.json(result);
|
|
640
750
|
});
|
|
641
751
|
});
|
|
@@ -643,28 +753,28 @@ const routeRegistration = hook({
|
|
|
643
753
|
// Also find by string tags
|
|
644
754
|
const cacheableTasks = store.getTasksWithTag("cacheable");
|
|
645
755
|
console.log(`Found ${cacheableTasks.length} cacheable tasks`);
|
|
646
|
-
}
|
|
647
|
-
|
|
756
|
+
})
|
|
757
|
+
.build();
|
|
648
758
|
```
|
|
649
759
|
|
|
650
760
|
#### Tag Extraction and Processing
|
|
651
761
|
|
|
652
762
|
```typescript
|
|
653
763
|
// Check if a tag exists and extract its configuration
|
|
654
|
-
const performanceTag =
|
|
655
|
-
|
|
656
|
-
|
|
764
|
+
const performanceTag = r
|
|
765
|
+
.tag<{ warnAboveMs: number }>("performance.monitor")
|
|
766
|
+
.build();
|
|
657
767
|
|
|
658
|
-
const performanceMiddleware =
|
|
659
|
-
|
|
660
|
-
run
|
|
768
|
+
const performanceMiddleware = r.middleware
|
|
769
|
+
.task("app.middleware.performance")
|
|
770
|
+
.run(async ({ task, next }) => {
|
|
661
771
|
// Check if task has performance monitoring enabled
|
|
662
772
|
if (!performanceTag.exists(task.definition)) {
|
|
663
773
|
return next(task.input);
|
|
664
774
|
}
|
|
665
775
|
|
|
666
776
|
// Extract the configuration
|
|
667
|
-
const config = performanceTag.extract(task.definition)
|
|
777
|
+
const config = performanceTag.extract(task.definition)!;
|
|
668
778
|
const startTime = Date.now();
|
|
669
779
|
|
|
670
780
|
try {
|
|
@@ -681,8 +791,8 @@ const performanceMiddleware = taskMiddleware({
|
|
|
681
791
|
console.error(`Task failed after ${duration}ms`, error);
|
|
682
792
|
throw error;
|
|
683
793
|
}
|
|
684
|
-
}
|
|
685
|
-
|
|
794
|
+
})
|
|
795
|
+
.build();
|
|
686
796
|
```
|
|
687
797
|
|
|
688
798
|
#### System Tags
|
|
@@ -690,21 +800,21 @@ const performanceMiddleware = taskMiddleware({
|
|
|
690
800
|
Built-in tags for framework behavior:
|
|
691
801
|
|
|
692
802
|
```typescript
|
|
693
|
-
import { globals } from "@bluelibs/runner";
|
|
803
|
+
import { r, globals } from "@bluelibs/runner";
|
|
694
804
|
|
|
695
|
-
const internalTask =
|
|
696
|
-
|
|
697
|
-
tags
|
|
805
|
+
const internalTask = r
|
|
806
|
+
.task("app.internal.cleanup")
|
|
807
|
+
.tags([
|
|
698
808
|
globals.tags.system, // Excludes from debug logs
|
|
699
809
|
globals.tags.debug.with({ logTaskInput: true }), // Per-component debug config
|
|
700
|
-
]
|
|
701
|
-
run
|
|
702
|
-
|
|
810
|
+
])
|
|
811
|
+
.run(async () => performCleanup())
|
|
812
|
+
.build();
|
|
703
813
|
|
|
704
|
-
const internalEvent =
|
|
705
|
-
|
|
706
|
-
tags
|
|
707
|
-
|
|
814
|
+
const internalEvent = r
|
|
815
|
+
.event("app.events.internal")
|
|
816
|
+
.tags([globals.tags.excludeFromGlobalHooks]) // Won't trigger wildcard listeners
|
|
817
|
+
.build();
|
|
708
818
|
```
|
|
709
819
|
|
|
710
820
|
#### Contract Tags
|
|
@@ -713,15 +823,15 @@ Enforce return value shapes at compile time:
|
|
|
713
823
|
|
|
714
824
|
```typescript
|
|
715
825
|
// Tags that enforce type contracts
|
|
716
|
-
const userContract =
|
|
717
|
-
|
|
718
|
-
|
|
826
|
+
const userContract = r
|
|
827
|
+
.tag<void, void, { name: string }>("contract.user")
|
|
828
|
+
.build();
|
|
719
829
|
|
|
720
|
-
const profileTask =
|
|
721
|
-
|
|
722
|
-
tags
|
|
723
|
-
run
|
|
724
|
-
|
|
830
|
+
const profileTask = r
|
|
831
|
+
.task("app.tasks.getProfile")
|
|
832
|
+
.tags([userContract]) // Must return { name: string }
|
|
833
|
+
.run(async () => ({ name: "Ada" })) // ✅ Satisfies contract
|
|
834
|
+
.build();
|
|
725
835
|
```
|
|
726
836
|
|
|
727
837
|
## run() and RunOptions
|
|
@@ -731,24 +841,18 @@ The `run()` function boots a root `resource` and returns a `RunResult` handle to
|
|
|
731
841
|
Basic usage:
|
|
732
842
|
|
|
733
843
|
```ts
|
|
734
|
-
import {
|
|
735
|
-
import { run } from "@bluelibs/runner";
|
|
844
|
+
import { r, run } from "@bluelibs/runner";
|
|
736
845
|
|
|
737
|
-
const ping =
|
|
738
|
-
|
|
739
|
-
async
|
|
740
|
-
|
|
741
|
-
},
|
|
742
|
-
});
|
|
846
|
+
const ping = r
|
|
847
|
+
.task("ping.task")
|
|
848
|
+
.run(async () => "pong")
|
|
849
|
+
.build();
|
|
743
850
|
|
|
744
|
-
const app =
|
|
745
|
-
|
|
746
|
-
register
|
|
747
|
-
async
|
|
748
|
-
|
|
749
|
-
return "ready";
|
|
750
|
-
},
|
|
751
|
-
});
|
|
851
|
+
const app = r
|
|
852
|
+
.resource("app")
|
|
853
|
+
.register([ping])
|
|
854
|
+
.init(async () => "ready")
|
|
855
|
+
.build();
|
|
752
856
|
|
|
753
857
|
const result = await run(app);
|
|
754
858
|
console.log(result.value); // "ready"
|
|
@@ -829,22 +933,20 @@ _Resources can dynamically modify task behavior during initialization_
|
|
|
829
933
|
Task interceptors (`task.intercept()`) are the modern replacement for component lifecycle events, allowing resources to dynamically modify task behavior without tight coupling.
|
|
830
934
|
|
|
831
935
|
```typescript
|
|
832
|
-
import {
|
|
936
|
+
import { r, run } from "@bluelibs/runner";
|
|
833
937
|
|
|
834
|
-
const calculatorTask =
|
|
835
|
-
|
|
836
|
-
run
|
|
938
|
+
const calculatorTask = r
|
|
939
|
+
.task("app.tasks.calculator")
|
|
940
|
+
.run(async ({ input }: { input: { value: number } }) => {
|
|
837
941
|
console.log("3. Task is running...");
|
|
838
942
|
return { result: input.value + 1 };
|
|
839
|
-
}
|
|
840
|
-
|
|
943
|
+
})
|
|
944
|
+
.build();
|
|
841
945
|
|
|
842
|
-
const interceptorResource =
|
|
843
|
-
|
|
844
|
-
dependencies
|
|
845
|
-
|
|
846
|
-
},
|
|
847
|
-
init: async (_, { calculatorTask }) => {
|
|
946
|
+
const interceptorResource = r
|
|
947
|
+
.resource("app.interceptor")
|
|
948
|
+
.dependencies({ calculatorTask })
|
|
949
|
+
.init(async (_config, { calculatorTask }) => {
|
|
848
950
|
// Intercept the task to modify its behavior
|
|
849
951
|
calculatorTask.intercept(async (next, input) => {
|
|
850
952
|
console.log("1. Interceptor before task run");
|
|
@@ -852,20 +954,20 @@ const interceptorResource = resource({
|
|
|
852
954
|
console.log("4. Interceptor after task run");
|
|
853
955
|
return { ...result, intercepted: true };
|
|
854
956
|
});
|
|
855
|
-
}
|
|
856
|
-
|
|
957
|
+
})
|
|
958
|
+
.build();
|
|
857
959
|
|
|
858
|
-
const app =
|
|
859
|
-
|
|
860
|
-
register
|
|
861
|
-
dependencies
|
|
862
|
-
init
|
|
960
|
+
const app = r
|
|
961
|
+
.resource("app")
|
|
962
|
+
.register([calculatorTask, interceptorResource])
|
|
963
|
+
.dependencies({ calculatorTask })
|
|
964
|
+
.init(async (_config, { calculatorTask }) => {
|
|
863
965
|
console.log("2. Calling the task...");
|
|
864
966
|
const result = await calculatorTask({ value: 10 });
|
|
865
967
|
console.log("5. Final result:", result);
|
|
866
968
|
// Final result: { result: 11, intercepted: true }
|
|
867
|
-
}
|
|
868
|
-
|
|
969
|
+
})
|
|
970
|
+
.build();
|
|
869
971
|
|
|
870
972
|
await run(app);
|
|
871
973
|
```
|
|
@@ -881,24 +983,26 @@ Sometimes you want your application to gracefully handle missing dependencies in
|
|
|
881
983
|
Keep in mind that you have full control over dependency registration by functionalising `dependencies(config) => ({ ... })` and `register(config) => []`.
|
|
882
984
|
|
|
883
985
|
```typescript
|
|
884
|
-
|
|
885
|
-
id: "app.services.email",
|
|
886
|
-
init: async () => new EmailService(),
|
|
887
|
-
});
|
|
986
|
+
import { r } from "@bluelibs/runner";
|
|
888
987
|
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
init
|
|
892
|
-
|
|
988
|
+
const emailService = r
|
|
989
|
+
.resource("app.services.email")
|
|
990
|
+
.init(async () => new EmailService())
|
|
991
|
+
.build();
|
|
893
992
|
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
993
|
+
const paymentService = r
|
|
994
|
+
.resource("app.services.payment")
|
|
995
|
+
.init(async () => new PaymentService())
|
|
996
|
+
.build();
|
|
997
|
+
|
|
998
|
+
const userRegistration = r
|
|
999
|
+
.task("app.tasks.registerUser")
|
|
1000
|
+
.dependencies({
|
|
897
1001
|
database: userDatabase, // Required - will fail if not available
|
|
898
1002
|
emailService: emailService.optional(), // Optional - won't fail if missing
|
|
899
1003
|
analytics: analyticsService.optional(), // Optional - graceful degradation
|
|
900
|
-
}
|
|
901
|
-
run
|
|
1004
|
+
})
|
|
1005
|
+
.run(async ({ input }, { database, emailService, analytics }) => {
|
|
902
1006
|
// Create user (required)
|
|
903
1007
|
const user = await database.users.create(userData);
|
|
904
1008
|
|
|
@@ -939,37 +1043,98 @@ const userRegistration = task({
|
|
|
939
1043
|
Ever tried to pass user data through 15 function calls? Yeah, we've been there. Context fixes that without turning your code into a game of telephone. This is very different from the Private Context from resources.
|
|
940
1044
|
|
|
941
1045
|
```typescript
|
|
1046
|
+
import { r } from "@bluelibs/runner";
|
|
1047
|
+
|
|
942
1048
|
const UserContext = createContext<{ userId: string; role: string }>(
|
|
943
1049
|
"app.userContext",
|
|
944
1050
|
);
|
|
945
1051
|
|
|
946
|
-
const getUserData =
|
|
947
|
-
|
|
948
|
-
middleware
|
|
949
|
-
run
|
|
1052
|
+
const getUserData = r
|
|
1053
|
+
.task("app.tasks.getUserData")
|
|
1054
|
+
.middleware([UserContext.require()]) // ensure context is available
|
|
1055
|
+
.run(async () => {
|
|
950
1056
|
const user = UserContext.use(); // Available anywhere in the async chain
|
|
951
1057
|
return `Current user: ${user.userId} (${user.role})`;
|
|
952
|
-
}
|
|
953
|
-
|
|
1058
|
+
})
|
|
1059
|
+
.build();
|
|
954
1060
|
|
|
955
1061
|
// Provide context at the entry point
|
|
956
|
-
const handleRequest =
|
|
957
|
-
|
|
958
|
-
init
|
|
1062
|
+
const handleRequest = r
|
|
1063
|
+
.resource("app.requestHandler")
|
|
1064
|
+
.init(async () => {
|
|
959
1065
|
return UserContext.provide({ userId: "123", role: "admin" }, async () => {
|
|
960
1066
|
// All tasks called within this scope have access to UserContext
|
|
961
1067
|
return await getUserData();
|
|
962
1068
|
});
|
|
963
|
-
}
|
|
1069
|
+
})
|
|
1070
|
+
.build();
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
## Fluent Builders (`r.*`)
|
|
1074
|
+
|
|
1075
|
+
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.
|
|
1076
|
+
|
|
1077
|
+
Here’s a quick taste of how it looks, with and without `zod` for validation:
|
|
1078
|
+
|
|
1079
|
+
```typescript
|
|
1080
|
+
import { r, run } from "@bluelibs/runner";
|
|
1081
|
+
import { z } from "zod";
|
|
1082
|
+
|
|
1083
|
+
// With Zod, the config type is inferred automatically
|
|
1084
|
+
const emailerConfigSchema = z.object({
|
|
1085
|
+
smtpUrl: z.string().url(),
|
|
1086
|
+
from: z.string().email(),
|
|
964
1087
|
});
|
|
1088
|
+
|
|
1089
|
+
const emailer = r
|
|
1090
|
+
.resource("app.emailer")
|
|
1091
|
+
.configSchema(emailerConfigSchema)
|
|
1092
|
+
.init(async ({ config }) => ({
|
|
1093
|
+
send: (to: string, body: string) => {
|
|
1094
|
+
console.log(
|
|
1095
|
+
`Sending from ${config.from} to ${to} via ${config.smtpUrl}: ${body}`,
|
|
1096
|
+
);
|
|
1097
|
+
},
|
|
1098
|
+
}))
|
|
1099
|
+
.build();
|
|
1100
|
+
|
|
1101
|
+
// Without a schema library, you can provide the type explicitly
|
|
1102
|
+
const greeter = r
|
|
1103
|
+
.resource("app.greeter")
|
|
1104
|
+
.init(async (cfg: { name: string }) => ({
|
|
1105
|
+
greet: () => `Hello, ${cfg.name}!`,
|
|
1106
|
+
}))
|
|
1107
|
+
.build();
|
|
1108
|
+
|
|
1109
|
+
const app = r
|
|
1110
|
+
.resource("app")
|
|
1111
|
+
.register([
|
|
1112
|
+
emailer.with({
|
|
1113
|
+
smtpUrl: "smtp://example.com",
|
|
1114
|
+
from: "noreply@example.com",
|
|
1115
|
+
}),
|
|
1116
|
+
greeter.with({ name: "World" }),
|
|
1117
|
+
])
|
|
1118
|
+
.dependencies({ emailer, greeter })
|
|
1119
|
+
.init(async (_, { emailer, greeter }) => {
|
|
1120
|
+
console.log(greeter.greet());
|
|
1121
|
+
emailer.send("test@example.com", "This is a test.");
|
|
1122
|
+
})
|
|
1123
|
+
.build();
|
|
1124
|
+
|
|
1125
|
+
await run(app);
|
|
965
1126
|
```
|
|
966
1127
|
|
|
1128
|
+
The builder API provides a clean, step-by-step way to construct everything from simple tasks to complex resources with middleware, tags, and schemas.
|
|
1129
|
+
|
|
1130
|
+
For a complete guide and more examples, check out the [full Fluent Builders documentation](./readmes/FLUENT_BUILDERS.md).
|
|
1131
|
+
|
|
967
1132
|
## Type Helpers
|
|
968
1133
|
|
|
969
1134
|
These utility types help you extract the generics from tasks, resources, and events without re-declaring them. Import them from `@bluelibs/runner`.
|
|
970
1135
|
|
|
971
1136
|
```ts
|
|
972
|
-
import {
|
|
1137
|
+
import { r } from "@bluelibs/runner";
|
|
973
1138
|
import type {
|
|
974
1139
|
ExtractTaskInput,
|
|
975
1140
|
ExtractTaskOutput,
|
|
@@ -979,27 +1144,30 @@ import type {
|
|
|
979
1144
|
} from "@bluelibs/runner";
|
|
980
1145
|
|
|
981
1146
|
// Task example
|
|
982
|
-
const add =
|
|
983
|
-
|
|
984
|
-
run
|
|
985
|
-
})
|
|
1147
|
+
const add = r
|
|
1148
|
+
.task("calc.add")
|
|
1149
|
+
.run(
|
|
1150
|
+
async ({ input }: { input: { a: number; b: number } }) => input.a + input.b,
|
|
1151
|
+
)
|
|
1152
|
+
.build();
|
|
986
1153
|
|
|
987
1154
|
type AddInput = ExtractTaskInput<typeof add>; // { a: number; b: number }
|
|
988
1155
|
type AddOutput = ExtractTaskOutput<typeof add>; // number
|
|
989
1156
|
|
|
990
1157
|
// Resource example
|
|
991
|
-
const config =
|
|
992
|
-
|
|
993
|
-
init
|
|
994
|
-
|
|
1158
|
+
const config = r
|
|
1159
|
+
.resource("app.config")
|
|
1160
|
+
.init(async (cfg: { baseUrl: string }) => ({ baseUrl: cfg.baseUrl }))
|
|
1161
|
+
.build();
|
|
995
1162
|
|
|
996
1163
|
type ConfigInput = ExtractResourceConfig<typeof config>; // { baseUrl: string }
|
|
997
1164
|
type ConfigValue = ExtractResourceValue<typeof config>; // { baseUrl: string }
|
|
998
1165
|
|
|
999
1166
|
// Event example
|
|
1000
|
-
const userRegistered =
|
|
1001
|
-
|
|
1002
|
-
})
|
|
1167
|
+
const userRegistered = r
|
|
1168
|
+
.event("app.events.userRegistered")
|
|
1169
|
+
.payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
|
|
1170
|
+
.build();
|
|
1003
1171
|
type UserRegisteredPayload = ExtractEventPayload<typeof userRegistered>; // { userId: string; email: string }
|
|
1004
1172
|
```
|
|
1005
1173
|
|
|
@@ -1034,15 +1202,15 @@ const requestMiddleware = taskMiddleware({
|
|
|
1034
1202
|
},
|
|
1035
1203
|
});
|
|
1036
1204
|
|
|
1037
|
-
const handleRequest =
|
|
1038
|
-
|
|
1039
|
-
middleware
|
|
1040
|
-
run
|
|
1205
|
+
const handleRequest = r
|
|
1206
|
+
.task("app.handleRequest")
|
|
1207
|
+
.middleware([requestMiddleware])
|
|
1208
|
+
.run(async ({ input }: { input: { path: string } }) => {
|
|
1041
1209
|
const request = RequestContext.use();
|
|
1042
1210
|
console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
|
|
1043
1211
|
return { success: true, requestId: request.requestId };
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1212
|
+
})
|
|
1213
|
+
.build();
|
|
1046
1214
|
```
|
|
1047
1215
|
|
|
1048
1216
|
> **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."
|
|
@@ -1070,36 +1238,36 @@ process.on("SIGTERM", async () => {
|
|
|
1070
1238
|
});
|
|
1071
1239
|
|
|
1072
1240
|
// Resources with cleanup logic
|
|
1073
|
-
const databaseResource =
|
|
1074
|
-
|
|
1075
|
-
init
|
|
1241
|
+
const databaseResource = r
|
|
1242
|
+
.resource("app.database")
|
|
1243
|
+
.init(async () => {
|
|
1076
1244
|
const connection = await connectToDatabase();
|
|
1077
1245
|
console.log("Database connected");
|
|
1078
1246
|
return connection;
|
|
1079
|
-
}
|
|
1080
|
-
dispose
|
|
1247
|
+
})
|
|
1248
|
+
.dispose(async (connection) => {
|
|
1081
1249
|
await connection.close();
|
|
1082
1250
|
// console.log("Database connection closed");
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1251
|
+
})
|
|
1252
|
+
.build();
|
|
1085
1253
|
|
|
1086
|
-
const serverResource =
|
|
1087
|
-
|
|
1088
|
-
dependencies
|
|
1089
|
-
init
|
|
1254
|
+
const serverResource = r
|
|
1255
|
+
.resource("app.server")
|
|
1256
|
+
.dependencies({ database: databaseResource })
|
|
1257
|
+
.init(async (config: { port: number }, { database }) => {
|
|
1090
1258
|
const server = express().listen(config.port);
|
|
1091
1259
|
console.log(`Server listening on port ${config.port}`);
|
|
1092
1260
|
return server;
|
|
1093
|
-
}
|
|
1094
|
-
dispose
|
|
1095
|
-
return new Promise((resolve) => {
|
|
1261
|
+
})
|
|
1262
|
+
.dispose(async (server) => {
|
|
1263
|
+
return new Promise<void>((resolve) => {
|
|
1096
1264
|
server.close(() => {
|
|
1097
1265
|
console.log("Server closed");
|
|
1098
1266
|
resolve();
|
|
1099
1267
|
});
|
|
1100
1268
|
});
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1269
|
+
})
|
|
1270
|
+
.build();
|
|
1103
1271
|
```
|
|
1104
1272
|
|
|
1105
1273
|
### Error Boundary Integration
|
|
@@ -1178,25 +1346,25 @@ Because nobody likes waiting for the same expensive operation twice:
|
|
|
1178
1346
|
```typescript
|
|
1179
1347
|
import { globals } from "@bluelibs/runner";
|
|
1180
1348
|
|
|
1181
|
-
const expensiveTask =
|
|
1182
|
-
|
|
1183
|
-
middleware
|
|
1349
|
+
const expensiveTask = r
|
|
1350
|
+
.task("app.tasks.expensive")
|
|
1351
|
+
.middleware([
|
|
1184
1352
|
globals.middleware.task.cache.with({
|
|
1185
1353
|
// lru-cache options by default
|
|
1186
1354
|
ttl: 60 * 1000, // Cache for 1 minute
|
|
1187
|
-
keyBuilder: (taskId, input) => `${taskId}-${input.userId}`, // optional key builder
|
|
1355
|
+
keyBuilder: (taskId, input) => `${taskId}-${(input as any).userId}`, // optional key builder
|
|
1188
1356
|
}),
|
|
1189
|
-
]
|
|
1190
|
-
run
|
|
1357
|
+
])
|
|
1358
|
+
.run(async ({ input }: { input: { userId: string } }) => {
|
|
1191
1359
|
// This expensive operation will be cached
|
|
1192
|
-
return await doExpensiveCalculation(userId);
|
|
1193
|
-
}
|
|
1360
|
+
return await doExpensiveCalculation(input.userId);
|
|
1361
|
+
})
|
|
1194
1362
|
});
|
|
1195
1363
|
|
|
1196
1364
|
// Global cache configuration
|
|
1197
|
-
const app =
|
|
1198
|
-
|
|
1199
|
-
register
|
|
1365
|
+
const app = r
|
|
1366
|
+
.resource("app.cache")
|
|
1367
|
+
.register([
|
|
1200
1368
|
// You have to register it, cache resource is not enabled by default.
|
|
1201
1369
|
globals.resources.cache.with({
|
|
1202
1370
|
defaultOptions: {
|
|
@@ -1204,27 +1372,25 @@ const app = resource({
|
|
|
1204
1372
|
ttl: 30 * 1000, // Default TTL
|
|
1205
1373
|
},
|
|
1206
1374
|
}),
|
|
1207
|
-
]
|
|
1208
|
-
|
|
1375
|
+
])
|
|
1376
|
+
.build();
|
|
1209
1377
|
```
|
|
1210
1378
|
|
|
1211
1379
|
Want Redis instead of the default LRU cache? No problem, just override the cache factory task:
|
|
1212
1380
|
|
|
1213
1381
|
```typescript
|
|
1214
|
-
import {
|
|
1382
|
+
import { r } from "@bluelibs/runner";
|
|
1215
1383
|
|
|
1216
|
-
const redisCacheFactory =
|
|
1217
|
-
|
|
1218
|
-
run
|
|
1219
|
-
|
|
1220
|
-
},
|
|
1221
|
-
});
|
|
1384
|
+
const redisCacheFactory = r
|
|
1385
|
+
.task("globals.tasks.cacheFactory") // Same ID as the default task
|
|
1386
|
+
.run(async ({ input }: { input: any }) => new RedisCache(input))
|
|
1387
|
+
.build();
|
|
1222
1388
|
|
|
1223
|
-
const app =
|
|
1224
|
-
|
|
1225
|
-
register
|
|
1226
|
-
overrides
|
|
1227
|
-
|
|
1389
|
+
const app = r
|
|
1390
|
+
.resource("app")
|
|
1391
|
+
.register([globals.resources.cache])
|
|
1392
|
+
.overrides([redisCacheFactory]) // Override the default cache factory
|
|
1393
|
+
.build();
|
|
1228
1394
|
```
|
|
1229
1395
|
|
|
1230
1396
|
> **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."
|
|
@@ -1259,13 +1425,11 @@ Here are real performance metrics from our comprehensive benchmark suite on an M
|
|
|
1259
1425
|
|
|
1260
1426
|
```typescript
|
|
1261
1427
|
// This executes in ~0.005ms on average
|
|
1262
|
-
const userTask =
|
|
1263
|
-
|
|
1264
|
-
middleware
|
|
1265
|
-
run
|
|
1266
|
-
|
|
1267
|
-
},
|
|
1268
|
-
});
|
|
1428
|
+
const userTask = r
|
|
1429
|
+
.task("user.create")
|
|
1430
|
+
.middleware([auth, logging, metrics])
|
|
1431
|
+
.run(async ({ input }) => database.users.create(input as any))
|
|
1432
|
+
.build();
|
|
1269
1433
|
|
|
1270
1434
|
// 1000 executions = ~5ms total time
|
|
1271
1435
|
for (let i = 0; i < 1000; i++) {
|
|
@@ -1294,37 +1458,41 @@ for (let i = 0; i < 1000; i++) {
|
|
|
1294
1458
|
**Middleware Ordering**: Place faster middleware first
|
|
1295
1459
|
|
|
1296
1460
|
```typescript
|
|
1297
|
-
const task =
|
|
1461
|
+
const task = r
|
|
1462
|
+
.task("app.performance.example")
|
|
1298
1463
|
middleware: [
|
|
1299
1464
|
fastAuthCheck, // ~0.1ms
|
|
1300
1465
|
slowRateLimiting, // ~2ms
|
|
1301
1466
|
expensiveLogging, // ~5ms
|
|
1302
1467
|
],
|
|
1303
|
-
|
|
1468
|
+
.run(async () => null)
|
|
1469
|
+
.build();
|
|
1304
1470
|
```
|
|
1305
1471
|
|
|
1306
1472
|
**Resource Reuse**: Resources are singletons—perfect for expensive setup
|
|
1307
1473
|
|
|
1308
1474
|
```typescript
|
|
1309
|
-
const database =
|
|
1310
|
-
|
|
1475
|
+
const database = r
|
|
1476
|
+
.resource("app.performance.db")
|
|
1477
|
+
.init(async () => {
|
|
1311
1478
|
// Expensive connection setup happens once
|
|
1312
1479
|
const connection = await createDbConnection();
|
|
1313
1480
|
return connection;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1481
|
+
})
|
|
1482
|
+
.build();
|
|
1316
1483
|
```
|
|
1317
1484
|
|
|
1318
1485
|
**Cache Strategically**: Use built-in caching for expensive operations
|
|
1319
1486
|
|
|
1320
1487
|
```typescript
|
|
1321
|
-
const expensiveTask =
|
|
1322
|
-
|
|
1323
|
-
|
|
1488
|
+
const expensiveTask = r
|
|
1489
|
+
.task("app.performance.expensive")
|
|
1490
|
+
.middleware([globals.middleware.cache.with({ ttl: 60000 })])
|
|
1491
|
+
.run(async ({ input }) => {
|
|
1324
1492
|
// This expensive computation is cached
|
|
1325
1493
|
return performExpensiveCalculation(input);
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1494
|
+
})
|
|
1495
|
+
.build();
|
|
1328
1496
|
```
|
|
1329
1497
|
|
|
1330
1498
|
#### Memory Considerations
|
|
@@ -1386,25 +1554,22 @@ For when things go wrong, but you know they'll probably work if you just try aga
|
|
|
1386
1554
|
```typescript
|
|
1387
1555
|
import { globals } from "@bluelibs/runner";
|
|
1388
1556
|
|
|
1389
|
-
const flakyApiCall =
|
|
1390
|
-
|
|
1391
|
-
middleware
|
|
1557
|
+
const flakyApiCall = r
|
|
1558
|
+
.task("app.tasks.flakyApiCall")
|
|
1559
|
+
.middleware([
|
|
1392
1560
|
globals.middleware.task.retry.with({
|
|
1393
1561
|
retries: 5, // Try up to 5 times
|
|
1394
1562
|
delayStrategy: (attempt) => 100 * Math.pow(2, attempt), // Exponential backoff
|
|
1395
1563
|
stopRetryIf: (error) => error.message === "Invalid credentials", // Don't retry auth errors
|
|
1396
1564
|
}),
|
|
1397
|
-
]
|
|
1398
|
-
run
|
|
1565
|
+
])
|
|
1566
|
+
.run(async () => {
|
|
1399
1567
|
// This might fail due to network issues, rate limiting, etc.
|
|
1400
1568
|
return await fetchFromUnreliableService();
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1569
|
+
})
|
|
1570
|
+
.build();
|
|
1403
1571
|
|
|
1404
|
-
const app = resource(
|
|
1405
|
-
id: "app",
|
|
1406
|
-
register: [flakyApiCall],
|
|
1407
|
-
});
|
|
1572
|
+
const app = r.resource("app").register([flakyApiCall]).build();
|
|
1408
1573
|
```
|
|
1409
1574
|
|
|
1410
1575
|
The retry middleware can be configured with:
|
|
@@ -1423,22 +1588,22 @@ timeout. Works for resources and tasks.
|
|
|
1423
1588
|
```typescript
|
|
1424
1589
|
import { globals } from "@bluelibs/runner";
|
|
1425
1590
|
|
|
1426
|
-
const apiTask =
|
|
1427
|
-
|
|
1428
|
-
middleware
|
|
1591
|
+
const apiTask = r
|
|
1592
|
+
.task("app.tasks.externalApi")
|
|
1593
|
+
.middleware([
|
|
1429
1594
|
// Works for tasks and resources via globals.middleware.resource.timeout
|
|
1430
1595
|
globals.middleware.task.timeout.with({ ttl: 5000 }), // 5 second timeout
|
|
1431
|
-
]
|
|
1432
|
-
run
|
|
1596
|
+
])
|
|
1597
|
+
.run(async () => {
|
|
1433
1598
|
// This operation will be aborted if it takes longer than 5 seconds
|
|
1434
1599
|
return await fetch("https://slow-api.example.com/data");
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1600
|
+
})
|
|
1601
|
+
.build();
|
|
1437
1602
|
|
|
1438
1603
|
// Combine with retry for robust error handling
|
|
1439
|
-
const resilientTask =
|
|
1440
|
-
|
|
1441
|
-
middleware
|
|
1604
|
+
const resilientTask = r
|
|
1605
|
+
.task("app.tasks.resilient")
|
|
1606
|
+
.middleware([
|
|
1442
1607
|
// Order matters here. Imagine a big onion.
|
|
1443
1608
|
// Works for resources as well via globals.middleware.resource.retry
|
|
1444
1609
|
globals.middleware.task.retry.with({
|
|
@@ -1446,12 +1611,12 @@ const resilientTask = task({
|
|
|
1446
1611
|
delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
|
|
1447
1612
|
}),
|
|
1448
1613
|
globals.middleware.task.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
|
|
1449
|
-
]
|
|
1450
|
-
run
|
|
1614
|
+
])
|
|
1615
|
+
.run(async () => {
|
|
1451
1616
|
// Each retry attempt gets its own 10-second timeout
|
|
1452
1617
|
return await unreliableOperation();
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1618
|
+
})
|
|
1619
|
+
.build();
|
|
1455
1620
|
```
|
|
1456
1621
|
|
|
1457
1622
|
How it works:
|
|
@@ -1480,14 +1645,12 @@ BlueLibs Runner comes with a built-in logging system that's structured, and does
|
|
|
1480
1645
|
### Basic Logging
|
|
1481
1646
|
|
|
1482
1647
|
```ts
|
|
1483
|
-
import {
|
|
1648
|
+
import { r, globals } from "@bluelibs/runner";
|
|
1484
1649
|
|
|
1485
|
-
const app =
|
|
1486
|
-
|
|
1487
|
-
dependencies:
|
|
1488
|
-
|
|
1489
|
-
},
|
|
1490
|
-
init: async () => {
|
|
1650
|
+
const app = r
|
|
1651
|
+
.resource("app")
|
|
1652
|
+
.dependencies({ logger: globals.resources.logger })
|
|
1653
|
+
.init(async (_config, { logger }) => {
|
|
1491
1654
|
logger.info("Starting business process"); // ✅ Visible by default
|
|
1492
1655
|
logger.warn("This might take a while"); // ✅ Visible by default
|
|
1493
1656
|
logger.error("Oops, something went wrong", {
|
|
@@ -1504,9 +1667,9 @@ const app = resource({
|
|
|
1504
1667
|
logger.onLog(async (log) => {
|
|
1505
1668
|
// Sub-loggers instantiated .with() share the same log listeners.
|
|
1506
1669
|
// Catch logs
|
|
1507
|
-
})
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1670
|
+
});
|
|
1671
|
+
})
|
|
1672
|
+
.build();
|
|
1510
1673
|
|
|
1511
1674
|
run(app, {
|
|
1512
1675
|
logs: {
|
|
@@ -1545,10 +1708,10 @@ logger.critical("DEFCON 1: Everything is broken");
|
|
|
1545
1708
|
The logger accepts rich, structured data that makes debugging actually useful:
|
|
1546
1709
|
|
|
1547
1710
|
```typescript
|
|
1548
|
-
const userTask =
|
|
1549
|
-
|
|
1550
|
-
dependencies
|
|
1551
|
-
run
|
|
1711
|
+
const userTask = r
|
|
1712
|
+
.task("app.tasks.user.create")
|
|
1713
|
+
.dependencies({ logger: globals.resources.logger })
|
|
1714
|
+
.run(async ({ input }, { logger }) => {
|
|
1552
1715
|
// Basic message
|
|
1553
1716
|
logger.info("Creating new user");
|
|
1554
1717
|
|
|
@@ -1556,7 +1719,7 @@ const userTask = task({
|
|
|
1556
1719
|
logger.info("User creation attempt", {
|
|
1557
1720
|
source: userTask.id,
|
|
1558
1721
|
data: {
|
|
1559
|
-
email:
|
|
1722
|
+
email: (input as any).email,
|
|
1560
1723
|
registrationSource: "web",
|
|
1561
1724
|
timestamp: new Date().toISOString(),
|
|
1562
1725
|
},
|
|
@@ -1564,7 +1727,7 @@ const userTask = task({
|
|
|
1564
1727
|
|
|
1565
1728
|
// With error information
|
|
1566
1729
|
try {
|
|
1567
|
-
const user = await createUser(
|
|
1730
|
+
const user = await createUser(input as any);
|
|
1568
1731
|
logger.info("User created successfully", {
|
|
1569
1732
|
data: { userId: user.id, email: user.email },
|
|
1570
1733
|
});
|
|
@@ -1572,13 +1735,13 @@ const userTask = task({
|
|
|
1572
1735
|
logger.error("User creation failed", {
|
|
1573
1736
|
error,
|
|
1574
1737
|
data: {
|
|
1575
|
-
attemptedEmail:
|
|
1738
|
+
attemptedEmail: (input as any).email,
|
|
1576
1739
|
validationErrors: error.validationErrors,
|
|
1577
1740
|
},
|
|
1578
1741
|
});
|
|
1579
1742
|
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1743
|
+
})
|
|
1744
|
+
.build();
|
|
1582
1745
|
```
|
|
1583
1746
|
|
|
1584
1747
|
### Context-Aware Logging
|
|
@@ -1590,10 +1753,10 @@ const RequestContext = createContext<{ requestId: string; userId: string }>(
|
|
|
1590
1753
|
"app.requestContext",
|
|
1591
1754
|
);
|
|
1592
1755
|
|
|
1593
|
-
const requestHandler =
|
|
1594
|
-
|
|
1595
|
-
dependencies
|
|
1596
|
-
run
|
|
1756
|
+
const requestHandler = r
|
|
1757
|
+
.task("app.tasks.handleRequest")
|
|
1758
|
+
.dependencies({ logger: globals.resources.logger })
|
|
1759
|
+
.run(async ({ input: requestData }, { logger }) => {
|
|
1597
1760
|
const request = RequestContext.use();
|
|
1598
1761
|
|
|
1599
1762
|
// Create a contextual logger with bound metadata with source and context
|
|
@@ -1619,8 +1782,8 @@ const requestHandler = task({
|
|
|
1619
1782
|
error: new Error("Invalid input"),
|
|
1620
1783
|
data: { stage: "validation" },
|
|
1621
1784
|
});
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1785
|
+
})
|
|
1786
|
+
.build();
|
|
1624
1787
|
```
|
|
1625
1788
|
|
|
1626
1789
|
### Integration with Winston
|
|
@@ -1629,7 +1792,7 @@ Want to use Winston as your transport? No problem - integrate it seamlessly:
|
|
|
1629
1792
|
|
|
1630
1793
|
```typescript
|
|
1631
1794
|
import winston from "winston";
|
|
1632
|
-
import {
|
|
1795
|
+
import { r, globals } from "@bluelibs/runner";
|
|
1633
1796
|
|
|
1634
1797
|
// Create Winston logger, put it in a resource if used from various places.
|
|
1635
1798
|
const winstonLogger = winston.createLogger({
|
|
@@ -1649,12 +1812,10 @@ const winstonLogger = winston.createLogger({
|
|
|
1649
1812
|
});
|
|
1650
1813
|
|
|
1651
1814
|
// Bridge BlueLibs logs to Winston using hooks
|
|
1652
|
-
const winstonBridgeResource =
|
|
1653
|
-
|
|
1654
|
-
dependencies:
|
|
1655
|
-
|
|
1656
|
-
},
|
|
1657
|
-
init: async (_, { logger }) => {
|
|
1815
|
+
const winstonBridgeResource = r
|
|
1816
|
+
.resource("app.resources.winstonBridge")
|
|
1817
|
+
.dependencies({ logger: globals.resources.logger })
|
|
1818
|
+
.init(async (_config, { logger }) => {
|
|
1658
1819
|
// Map log levels (BlueLibs -> Winston)
|
|
1659
1820
|
const levelMapping = {
|
|
1660
1821
|
trace: "silly",
|
|
@@ -1678,8 +1839,8 @@ const winstonBridgeResource = resource({
|
|
|
1678
1839
|
const winstonLevel = levelMapping[log.level] || "info";
|
|
1679
1840
|
winstonLogger.log(winstonLevel, log.message, winstonMeta);
|
|
1680
1841
|
});
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1842
|
+
})
|
|
1843
|
+
.build();
|
|
1683
1844
|
```
|
|
1684
1845
|
|
|
1685
1846
|
### Custom Log Formatters
|
|
@@ -1709,13 +1870,11 @@ class JSONLogger extends Logger {
|
|
|
1709
1870
|
}
|
|
1710
1871
|
|
|
1711
1872
|
// Custom logger resource
|
|
1712
|
-
const customLogger =
|
|
1713
|
-
|
|
1714
|
-
dependencies
|
|
1715
|
-
init
|
|
1716
|
-
|
|
1717
|
-
},
|
|
1718
|
-
});
|
|
1873
|
+
const customLogger = r
|
|
1874
|
+
.resource("app.logger.custom")
|
|
1875
|
+
.dependencies({ eventManager: globals.resources.eventManager })
|
|
1876
|
+
.init(async (_config, { eventManager }) => new JSONLogger(eventManager))
|
|
1877
|
+
.build();
|
|
1719
1878
|
|
|
1720
1879
|
// Or you could simply add it as "globals.resources.logger" and override the default logger
|
|
1721
1880
|
```
|
|
@@ -1776,9 +1935,9 @@ run(app, { debug: "verbose" });
|
|
|
1776
1935
|
**Custom Configuration**:
|
|
1777
1936
|
|
|
1778
1937
|
```typescript
|
|
1779
|
-
const app =
|
|
1780
|
-
|
|
1781
|
-
register
|
|
1938
|
+
const app = r
|
|
1939
|
+
.resource("app")
|
|
1940
|
+
.register([
|
|
1782
1941
|
globals.resources.debug.with({
|
|
1783
1942
|
logTaskInput: true,
|
|
1784
1943
|
logTaskResult: false,
|
|
@@ -1789,8 +1948,8 @@ const app = resource({
|
|
|
1789
1948
|
// Hook/middleware lifecycle visibility is available via interceptors
|
|
1790
1949
|
// ... other fine-grained options
|
|
1791
1950
|
}),
|
|
1792
|
-
]
|
|
1793
|
-
|
|
1951
|
+
])
|
|
1952
|
+
.build();
|
|
1794
1953
|
```
|
|
1795
1954
|
|
|
1796
1955
|
### Per-Component Debug Configuration
|
|
@@ -1800,20 +1959,20 @@ Use debug tags to configure debugging on individual components, when you're inte
|
|
|
1800
1959
|
```typescript
|
|
1801
1960
|
import { globals } from "@bluelibs/runner";
|
|
1802
1961
|
|
|
1803
|
-
const criticalTask =
|
|
1804
|
-
|
|
1805
|
-
tags
|
|
1962
|
+
const criticalTask = r
|
|
1963
|
+
.task("app.tasks.critical")
|
|
1964
|
+
.tags([
|
|
1806
1965
|
globals.tags.debug.with({
|
|
1807
1966
|
logTaskInput: true,
|
|
1808
1967
|
logTaskResult: true,
|
|
1809
1968
|
logTaskOnError: true,
|
|
1810
1969
|
}),
|
|
1811
|
-
]
|
|
1812
|
-
run
|
|
1970
|
+
])
|
|
1971
|
+
.run(async ({ input }) => {
|
|
1813
1972
|
// This task will have verbose debug logging
|
|
1814
|
-
return await processPayment(input);
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1973
|
+
return await processPayment(input as any);
|
|
1974
|
+
})
|
|
1975
|
+
.build();
|
|
1817
1976
|
```
|
|
1818
1977
|
|
|
1819
1978
|
### Integration with Run Options
|
|
@@ -1929,35 +2088,35 @@ interface IMeta {
|
|
|
1929
2088
|
### Simple Documentation Example
|
|
1930
2089
|
|
|
1931
2090
|
```typescript
|
|
1932
|
-
const userService =
|
|
1933
|
-
|
|
1934
|
-
meta
|
|
2091
|
+
const userService = r
|
|
2092
|
+
.resource("app.services.user")
|
|
2093
|
+
.meta({
|
|
1935
2094
|
title: "User Management Service",
|
|
1936
2095
|
description:
|
|
1937
2096
|
"Handles user creation, authentication, and profile management",
|
|
1938
|
-
}
|
|
1939
|
-
dependencies
|
|
1940
|
-
init
|
|
2097
|
+
})
|
|
2098
|
+
.dependencies({ database })
|
|
2099
|
+
.init(async (_config, { database }) => ({
|
|
1941
2100
|
createUser: async (userData) => {
|
|
1942
2101
|
/* ... */
|
|
1943
2102
|
},
|
|
1944
2103
|
authenticateUser: async (credentials) => {
|
|
1945
2104
|
/* ... */
|
|
1946
2105
|
},
|
|
1947
|
-
})
|
|
1948
|
-
|
|
2106
|
+
}))
|
|
2107
|
+
.build();
|
|
1949
2108
|
|
|
1950
|
-
const sendWelcomeEmail =
|
|
1951
|
-
|
|
1952
|
-
meta
|
|
2109
|
+
const sendWelcomeEmail = r
|
|
2110
|
+
.task("app.tasks.sendWelcomeEmail")
|
|
2111
|
+
.meta({
|
|
1953
2112
|
title: "Send Welcome Email",
|
|
1954
2113
|
description: "Sends a welcome email to newly registered users",
|
|
1955
|
-
}
|
|
1956
|
-
dependencies
|
|
1957
|
-
run
|
|
2114
|
+
} as any)
|
|
2115
|
+
.dependencies({ emailService })
|
|
2116
|
+
.run(async ({ input: userData }, { emailService }) => {
|
|
1958
2117
|
// Email sending logic
|
|
1959
|
-
}
|
|
1960
|
-
|
|
2118
|
+
})
|
|
2119
|
+
.build();
|
|
1961
2120
|
```
|
|
1962
2121
|
|
|
1963
2122
|
### Extending Metadata: Custom Properties
|
|
@@ -1983,31 +2142,31 @@ declare module "@bluelibs/runner" {
|
|
|
1983
2142
|
}
|
|
1984
2143
|
|
|
1985
2144
|
// Now use your custom properties
|
|
1986
|
-
const expensiveApiTask =
|
|
1987
|
-
|
|
1988
|
-
meta
|
|
2145
|
+
const expensiveApiTask = r
|
|
2146
|
+
.task("app.tasks.ai.generateImage")
|
|
2147
|
+
.meta({
|
|
1989
2148
|
title: "AI Image Generation",
|
|
1990
2149
|
description: "Uses OpenAI DALL-E to generate images from text prompts",
|
|
1991
2150
|
author: "AI Team",
|
|
1992
2151
|
version: "2.1.0",
|
|
1993
2152
|
apiVersion: "v2",
|
|
1994
2153
|
costLevel: "high", // Custom property!
|
|
1995
|
-
}
|
|
1996
|
-
run
|
|
2154
|
+
} as any)
|
|
2155
|
+
.run(async ({ input: prompt }) => {
|
|
1997
2156
|
// AI generation logic
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2157
|
+
})
|
|
2158
|
+
.build();
|
|
2000
2159
|
|
|
2001
|
-
const database =
|
|
2002
|
-
|
|
2003
|
-
meta
|
|
2160
|
+
const database = r
|
|
2161
|
+
.resource("app.database.primary")
|
|
2162
|
+
.meta({
|
|
2004
2163
|
title: "Primary PostgreSQL Database",
|
|
2005
2164
|
healthCheck: "/health/db", // Custom property!
|
|
2006
2165
|
dependencies: ["postgresql", "connection-pool"],
|
|
2007
2166
|
scalingPolicy: "auto",
|
|
2008
|
-
}
|
|
2009
|
-
// ...
|
|
2010
|
-
|
|
2167
|
+
} as any)
|
|
2168
|
+
// .init(async () => { /* ... */ })
|
|
2169
|
+
.build();
|
|
2011
2170
|
```
|
|
2012
2171
|
|
|
2013
2172
|
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.
|
|
@@ -2021,10 +2180,10 @@ Sometimes you need to replace a component entirely. Maybe you're doing integrati
|
|
|
2021
2180
|
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.
|
|
2022
2181
|
|
|
2023
2182
|
```typescript
|
|
2024
|
-
const productionEmailer =
|
|
2025
|
-
|
|
2026
|
-
init
|
|
2027
|
-
|
|
2183
|
+
const productionEmailer = r
|
|
2184
|
+
.resource("app.emailer")
|
|
2185
|
+
.init(async () => new SMTPEmailer())
|
|
2186
|
+
.build();
|
|
2028
2187
|
|
|
2029
2188
|
// Option 1: Using override() to change behavior while preserving id (Recommended)
|
|
2030
2189
|
const testEmailer = override(productionEmailer, {
|
|
@@ -2033,27 +2192,33 @@ const testEmailer = override(productionEmailer, {
|
|
|
2033
2192
|
|
|
2034
2193
|
// Option 2: The system is really flexible, and override is just bringing in type safety, nothing else under the hood.
|
|
2035
2194
|
// Using spread operator works the same way but does not provide type-safety.
|
|
2036
|
-
const testEmailer =
|
|
2037
|
-
|
|
2038
|
-
init
|
|
2039
|
-
|
|
2195
|
+
const testEmailer = r
|
|
2196
|
+
.resource("app.emailer")
|
|
2197
|
+
.init(async () => ({} as any))
|
|
2198
|
+
.build();
|
|
2040
2199
|
|
|
2041
|
-
const app =
|
|
2042
|
-
|
|
2043
|
-
register
|
|
2044
|
-
overrides
|
|
2045
|
-
|
|
2200
|
+
const app = r
|
|
2201
|
+
.resource("app")
|
|
2202
|
+
.register([productionEmailer])
|
|
2203
|
+
.overrides([testEmailer]) // This replaces the production version
|
|
2204
|
+
.build();
|
|
2046
2205
|
|
|
2047
2206
|
import { override } from "@bluelibs/runner";
|
|
2048
2207
|
|
|
2049
2208
|
// Tasks
|
|
2050
|
-
const originalTask =
|
|
2209
|
+
const originalTask = r
|
|
2210
|
+
.task("app.tasks.compute")
|
|
2211
|
+
.run(async () => 1)
|
|
2212
|
+
.build();
|
|
2051
2213
|
const overriddenTask = override(originalTask, {
|
|
2052
2214
|
run: async () => 2,
|
|
2053
2215
|
});
|
|
2054
2216
|
|
|
2055
2217
|
// Resources
|
|
2056
|
-
const originalResource =
|
|
2218
|
+
const originalResource = r
|
|
2219
|
+
.resource("app.db")
|
|
2220
|
+
.init(async () => "conn")
|
|
2221
|
+
.build();
|
|
2057
2222
|
const overriddenResource = override(originalResource, {
|
|
2058
2223
|
init: async () => "mock-conn",
|
|
2059
2224
|
});
|
|
@@ -2119,10 +2284,10 @@ function namespaced(id: string) {
|
|
|
2119
2284
|
return `mycompany.myapp.${id}`;
|
|
2120
2285
|
}
|
|
2121
2286
|
|
|
2122
|
-
const userTask =
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2287
|
+
const userTask = r
|
|
2288
|
+
.task(namespaced("tasks.user.create-user"))
|
|
2289
|
+
.run(async () => null)
|
|
2290
|
+
.build();
|
|
2126
2291
|
```
|
|
2127
2292
|
|
|
2128
2293
|
> **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."
|
|
@@ -2135,26 +2300,24 @@ To keep things dead simple, we avoided poluting the D.I. with this concept. Ther
|
|
|
2135
2300
|
// Assume MyClass is defined elsewhere
|
|
2136
2301
|
// class MyClass { constructor(input: any, option: string) { ... } }
|
|
2137
2302
|
|
|
2138
|
-
const myFactory =
|
|
2139
|
-
|
|
2140
|
-
init
|
|
2303
|
+
const myFactory = r
|
|
2304
|
+
.resource("app.factories.myFactory")
|
|
2305
|
+
.init(async (config: { someOption: string }) => {
|
|
2141
2306
|
// This resource's value is a factory function
|
|
2142
|
-
return (input: any) =>
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
},
|
|
2146
|
-
});
|
|
2307
|
+
return (input: any) => new MyClass(input, config.someOption);
|
|
2308
|
+
})
|
|
2309
|
+
.build();
|
|
2147
2310
|
|
|
2148
|
-
const app =
|
|
2149
|
-
|
|
2311
|
+
const app = r
|
|
2312
|
+
.resource("app")
|
|
2150
2313
|
// Configure the factory resource upon registration
|
|
2151
|
-
register
|
|
2152
|
-
dependencies
|
|
2153
|
-
init
|
|
2314
|
+
.register([myFactory.with({ someOption: "configured-value" })])
|
|
2315
|
+
.dependencies({ myFactory })
|
|
2316
|
+
.init(async (_config, { myFactory }) => {
|
|
2154
2317
|
// `myFactory` is now the configured factory function
|
|
2155
2318
|
const instance = myFactory({ someInput: "hello" });
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2319
|
+
})
|
|
2320
|
+
.build();
|
|
2158
2321
|
```
|
|
2159
2322
|
|
|
2160
2323
|
> **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."
|
|
@@ -2192,20 +2355,20 @@ const userSchema = z.object({
|
|
|
2192
2355
|
age: z.number().min(0).max(150),
|
|
2193
2356
|
});
|
|
2194
2357
|
|
|
2195
|
-
const createUserTask =
|
|
2196
|
-
|
|
2197
|
-
inputSchema
|
|
2198
|
-
run
|
|
2358
|
+
const createUserTask = r
|
|
2359
|
+
.task("app.tasks.createUser")
|
|
2360
|
+
.inputSchema(userSchema) // Works directly with Zod!
|
|
2361
|
+
.run(async ({ input: userData }) => {
|
|
2199
2362
|
// userData is validated and properly typed
|
|
2200
|
-
return { id: "user-123", ...userData };
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2363
|
+
return { id: "user-123", ...(userData as any) };
|
|
2364
|
+
})
|
|
2365
|
+
.build();
|
|
2203
2366
|
|
|
2204
|
-
const app =
|
|
2205
|
-
|
|
2206
|
-
register
|
|
2207
|
-
dependencies
|
|
2208
|
-
init
|
|
2367
|
+
const app = r
|
|
2368
|
+
.resource("app")
|
|
2369
|
+
.register([createUserTask])
|
|
2370
|
+
.dependencies({ createUserTask })
|
|
2371
|
+
.init(async (_config, { createUserTask }) => {
|
|
2209
2372
|
// This works - valid input
|
|
2210
2373
|
const user = await createUserTask({
|
|
2211
2374
|
name: "John Doe",
|
|
@@ -2224,8 +2387,8 @@ const app = resource({
|
|
|
2224
2387
|
console.log(error.message);
|
|
2225
2388
|
// "Task input validation failed for app.tasks.createUser: ..."
|
|
2226
2389
|
}
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2390
|
+
})
|
|
2391
|
+
.build();
|
|
2229
2392
|
```
|
|
2230
2393
|
|
|
2231
2394
|
### Resource Config Validation
|
|
@@ -2240,10 +2403,10 @@ const databaseConfigSchema = z.object({
|
|
|
2240
2403
|
ssl: z.boolean().default(false), // Optional with default
|
|
2241
2404
|
});
|
|
2242
2405
|
|
|
2243
|
-
const databaseResource =
|
|
2244
|
-
|
|
2245
|
-
configSchema
|
|
2246
|
-
init
|
|
2406
|
+
const databaseResource = r
|
|
2407
|
+
.resource("app.resources.database")
|
|
2408
|
+
.configSchema(databaseConfigSchema) // Validation on .with()
|
|
2409
|
+
.init(async (config) => {
|
|
2247
2410
|
// config is already validated and has proper types
|
|
2248
2411
|
return createConnection({
|
|
2249
2412
|
host: config.host,
|
|
@@ -2251,8 +2414,8 @@ const databaseResource = resource({
|
|
|
2251
2414
|
database: config.database,
|
|
2252
2415
|
ssl: config.ssl,
|
|
2253
2416
|
});
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2417
|
+
})
|
|
2418
|
+
.build();
|
|
2256
2419
|
|
|
2257
2420
|
// Validation happens here, not during init!
|
|
2258
2421
|
try {
|
|
@@ -2265,17 +2428,17 @@ try {
|
|
|
2265
2428
|
// "Resource config validation failed for app.resources.database: ..."
|
|
2266
2429
|
}
|
|
2267
2430
|
|
|
2268
|
-
const app =
|
|
2269
|
-
|
|
2270
|
-
register
|
|
2431
|
+
const app = r
|
|
2432
|
+
.resource("app")
|
|
2433
|
+
.register([
|
|
2271
2434
|
databaseResource.with({
|
|
2272
2435
|
host: "localhost",
|
|
2273
2436
|
port: 5432,
|
|
2274
2437
|
database: "myapp",
|
|
2275
2438
|
// ssl defaults to false
|
|
2276
2439
|
}),
|
|
2277
|
-
]
|
|
2278
|
-
|
|
2440
|
+
])
|
|
2441
|
+
.build();
|
|
2279
2442
|
```
|
|
2280
2443
|
|
|
2281
2444
|
### Event Payload Validation
|
|
@@ -2289,25 +2452,25 @@ const userActionSchema = z.object({
|
|
|
2289
2452
|
timestamp: z.date().default(() => new Date()),
|
|
2290
2453
|
});
|
|
2291
2454
|
|
|
2292
|
-
const userActionEvent =
|
|
2293
|
-
|
|
2294
|
-
payloadSchema
|
|
2295
|
-
|
|
2455
|
+
const userActionEvent = r
|
|
2456
|
+
.event("app.events.userAction")
|
|
2457
|
+
.payloadSchema(userActionSchema) // Validates on emit
|
|
2458
|
+
.build();
|
|
2296
2459
|
|
|
2297
|
-
const notificationHook =
|
|
2298
|
-
|
|
2299
|
-
on
|
|
2300
|
-
run
|
|
2460
|
+
const notificationHook = r
|
|
2461
|
+
.hook("app.tasks.sendNotification")
|
|
2462
|
+
.on(userActionEvent)
|
|
2463
|
+
.run(async (eventData) => {
|
|
2301
2464
|
// eventData.data is validated and properly typed
|
|
2302
2465
|
console.log(`User ${eventData.data.userId} was ${eventData.data.action}`);
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2466
|
+
})
|
|
2467
|
+
.build();
|
|
2305
2468
|
|
|
2306
|
-
const app =
|
|
2307
|
-
|
|
2308
|
-
register
|
|
2309
|
-
dependencies
|
|
2310
|
-
init
|
|
2469
|
+
const app = r
|
|
2470
|
+
.resource("app")
|
|
2471
|
+
.register([userActionEvent, notificationHook])
|
|
2472
|
+
.dependencies({ userActionEvent })
|
|
2473
|
+
.init(async (_config, { userActionEvent }) => {
|
|
2311
2474
|
// This works - valid payload
|
|
2312
2475
|
await userActionEvent({
|
|
2313
2476
|
userId: "123e4567-e89b-12d3-a456-426614174000",
|
|
@@ -2323,8 +2486,8 @@ const app = resource({
|
|
|
2323
2486
|
} catch (error) {
|
|
2324
2487
|
// "Event payload validation failed for app.events.userAction: ..."
|
|
2325
2488
|
}
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2489
|
+
})
|
|
2490
|
+
.build();
|
|
2328
2491
|
```
|
|
2329
2492
|
|
|
2330
2493
|
### Middleware Config Validation
|
|
@@ -2338,10 +2501,10 @@ const timingConfigSchema = z.object({
|
|
|
2338
2501
|
logSuccessful: z.boolean().default(true),
|
|
2339
2502
|
});
|
|
2340
2503
|
|
|
2341
|
-
const timingMiddleware =
|
|
2342
|
-
|
|
2343
|
-
configSchema
|
|
2344
|
-
run
|
|
2504
|
+
const timingMiddleware = r.middleware
|
|
2505
|
+
.task("app.middleware.timing") // or r.middleware.resource("...")
|
|
2506
|
+
.configSchema(timingConfigSchema) // Validation on .with()
|
|
2507
|
+
.run(async ({ next }, _, config) => {
|
|
2345
2508
|
const start = Date.now();
|
|
2346
2509
|
try {
|
|
2347
2510
|
const result = await next();
|
|
@@ -2355,8 +2518,8 @@ const timingMiddleware = taskMiddleware({ // or resourceMiddleware()
|
|
|
2355
2518
|
console.log(`Operation failed after ${duration}ms`);
|
|
2356
2519
|
throw error;
|
|
2357
2520
|
}
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2521
|
+
})
|
|
2522
|
+
.build();
|
|
2360
2523
|
|
|
2361
2524
|
// Validation happens here, not during execution!
|
|
2362
2525
|
try {
|
|
@@ -2368,17 +2531,17 @@ try {
|
|
|
2368
2531
|
// "Middleware config validation failed for app.middleware.timing: ..."
|
|
2369
2532
|
}
|
|
2370
2533
|
|
|
2371
|
-
const myTask =
|
|
2372
|
-
|
|
2373
|
-
middleware
|
|
2534
|
+
const myTask = r
|
|
2535
|
+
.task("app.tasks.example")
|
|
2536
|
+
.middleware([
|
|
2374
2537
|
timingMiddleware.with({
|
|
2375
2538
|
timeout: 5000,
|
|
2376
2539
|
logLevel: "debug",
|
|
2377
2540
|
logSuccessful: true,
|
|
2378
2541
|
}),
|
|
2379
|
-
]
|
|
2380
|
-
run
|
|
2381
|
-
|
|
2542
|
+
])
|
|
2543
|
+
.run(async () => "success")
|
|
2544
|
+
.build();
|
|
2382
2545
|
```
|
|
2383
2546
|
|
|
2384
2547
|
#### Advanced Validation Features
|
|
@@ -2398,15 +2561,15 @@ const advancedSchema = z
|
|
|
2398
2561
|
path: ["amount"],
|
|
2399
2562
|
});
|
|
2400
2563
|
|
|
2401
|
-
const paymentTask =
|
|
2402
|
-
|
|
2403
|
-
inputSchema
|
|
2404
|
-
run
|
|
2564
|
+
const paymentTask = r
|
|
2565
|
+
.task("app.tasks.payment")
|
|
2566
|
+
.inputSchema(advancedSchema)
|
|
2567
|
+
.run(async ({ input: payment }) => {
|
|
2405
2568
|
// payment.amount is now a number (transformed from string)
|
|
2406
2569
|
// All validations have passed
|
|
2407
2570
|
return processPayment(payment);
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2571
|
+
})
|
|
2572
|
+
.build();
|
|
2410
2573
|
```
|
|
2411
2574
|
|
|
2412
2575
|
### Error Handling
|
|
@@ -2493,13 +2656,14 @@ const userSchema = z.object({
|
|
|
2493
2656
|
|
|
2494
2657
|
type UserData = z.infer<typeof userSchema>;
|
|
2495
2658
|
|
|
2496
|
-
const createUser =
|
|
2497
|
-
|
|
2498
|
-
|
|
2659
|
+
const createUser = r
|
|
2660
|
+
.task("app.tasks.createUser.zod")
|
|
2661
|
+
.inputSchema(userSchema)
|
|
2662
|
+
.run(async ({ input }: { input: UserData }) => {
|
|
2499
2663
|
// Both runtime validation AND compile-time typing
|
|
2500
2664
|
return { id: "user-123", ...input };
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2665
|
+
})
|
|
2666
|
+
.build();
|
|
2503
2667
|
```
|
|
2504
2668
|
|
|
2505
2669
|
> **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."
|
|
@@ -2511,18 +2675,18 @@ We expose the internal services for advanced use cases (but try not to use them
|
|
|
2511
2675
|
```typescript
|
|
2512
2676
|
import { globals } from "@bluelibs/runner";
|
|
2513
2677
|
|
|
2514
|
-
const advancedTask =
|
|
2515
|
-
|
|
2516
|
-
dependencies
|
|
2678
|
+
const advancedTask = r
|
|
2679
|
+
.task("app.advanced")
|
|
2680
|
+
.dependencies({
|
|
2517
2681
|
store: globals.resources.store,
|
|
2518
2682
|
taskRunner: globals.resources.taskRunner,
|
|
2519
2683
|
eventManager: globals.resources.eventManager,
|
|
2520
|
-
}
|
|
2521
|
-
run
|
|
2684
|
+
})
|
|
2685
|
+
.run(async (_param, { store, taskRunner, eventManager }) => {
|
|
2522
2686
|
// Direct access to the framework internals
|
|
2523
2687
|
// (Use with caution!)
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2688
|
+
})
|
|
2689
|
+
.build();
|
|
2526
2690
|
```
|
|
2527
2691
|
|
|
2528
2692
|
### Dynamic Dependencies
|
|
@@ -2531,37 +2695,36 @@ Dependencies can be defined in two ways - as a static object or as a function th
|
|
|
2531
2695
|
|
|
2532
2696
|
```typescript
|
|
2533
2697
|
// Static dependencies (most common)
|
|
2534
|
-
const userService =
|
|
2535
|
-
|
|
2536
|
-
dependencies
|
|
2537
|
-
init
|
|
2698
|
+
const userService = r
|
|
2699
|
+
.resource("app.services.user")
|
|
2700
|
+
.dependencies({ database, logger }) // Object - evaluated immediately
|
|
2701
|
+
.init(async (_config, { database, logger }) => {
|
|
2538
2702
|
// Dependencies are available here
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2703
|
+
})
|
|
2704
|
+
.build();
|
|
2541
2705
|
|
|
2542
2706
|
// Dynamic dependencies (for circular references or conditional dependencies)
|
|
2543
|
-
const advancedService =
|
|
2544
|
-
|
|
2707
|
+
const advancedService = r
|
|
2708
|
+
.resource("app.services.advanced")
|
|
2545
2709
|
// A function gives you the chance
|
|
2546
|
-
dependencies
|
|
2547
|
-
// Config is what you receive when you register
|
|
2710
|
+
.dependencies((_config) => ({
|
|
2711
|
+
// Config is what you receive when you register this resource with .with()
|
|
2548
2712
|
// So you can have conditional dependencies based on resource configuration as well.
|
|
2549
2713
|
database,
|
|
2550
2714
|
logger,
|
|
2551
2715
|
conditionalService:
|
|
2552
2716
|
process.env.NODE_ENV === "production" ? serviceA : serviceB,
|
|
2553
|
-
})
|
|
2554
|
-
register
|
|
2555
|
-
// Config is what you receive when you register the resource with .with()
|
|
2717
|
+
})) // Function - evaluated when needed
|
|
2718
|
+
.register((_config: ConfigType) => [
|
|
2556
2719
|
// Register dependencies dynamically
|
|
2557
2720
|
process.env.NODE_ENV === "production"
|
|
2558
2721
|
? serviceA.with({ config: "value" })
|
|
2559
2722
|
: serviceB.with({ config: "value" }),
|
|
2560
|
-
]
|
|
2561
|
-
init
|
|
2723
|
+
])
|
|
2724
|
+
.init(async (_config, { database, logger, conditionalService }) => {
|
|
2562
2725
|
// Same interface, different evaluation timing
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2726
|
+
})
|
|
2727
|
+
.build();
|
|
2565
2728
|
```
|
|
2566
2729
|
|
|
2567
2730
|
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.
|
|
@@ -2678,26 +2841,26 @@ import {
|
|
|
2678
2841
|
} from "@bluelibs/runner";
|
|
2679
2842
|
|
|
2680
2843
|
// Configuration
|
|
2681
|
-
const config =
|
|
2682
|
-
|
|
2683
|
-
init
|
|
2844
|
+
const config = r
|
|
2845
|
+
.resource("app.config")
|
|
2846
|
+
.init(async () => ({
|
|
2684
2847
|
port: parseInt(process.env.PORT || "3000"),
|
|
2685
2848
|
databaseUrl: process.env.DATABASE_URL!,
|
|
2686
2849
|
jwtSecret: process.env.JWT_SECRET!,
|
|
2687
|
-
})
|
|
2688
|
-
|
|
2850
|
+
}))
|
|
2851
|
+
.build();
|
|
2689
2852
|
|
|
2690
2853
|
// Database
|
|
2691
|
-
const database =
|
|
2692
|
-
|
|
2693
|
-
dependencies
|
|
2694
|
-
init
|
|
2854
|
+
const database = r
|
|
2855
|
+
.resource("app.database")
|
|
2856
|
+
.dependencies({ config })
|
|
2857
|
+
.init(async (_config, { config }) => {
|
|
2695
2858
|
const client = new MongoClient(config.databaseUrl);
|
|
2696
2859
|
await client.connect();
|
|
2697
2860
|
return client;
|
|
2698
|
-
}
|
|
2699
|
-
dispose
|
|
2700
|
-
|
|
2861
|
+
})
|
|
2862
|
+
.dispose(async (client) => await client.close())
|
|
2863
|
+
.build();
|
|
2701
2864
|
|
|
2702
2865
|
// Context for request data
|
|
2703
2866
|
const RequestContext = createContext<{ userId?: string; role?: string }>(
|
|
@@ -2705,78 +2868,70 @@ const RequestContext = createContext<{ userId?: string; role?: string }>(
|
|
|
2705
2868
|
);
|
|
2706
2869
|
|
|
2707
2870
|
// Events
|
|
2708
|
-
const userRegistered =
|
|
2709
|
-
|
|
2710
|
-
})
|
|
2871
|
+
const userRegistered = r
|
|
2872
|
+
.event("app.events.userRegistered")
|
|
2873
|
+
.payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
|
|
2874
|
+
.build();
|
|
2711
2875
|
|
|
2712
2876
|
// Middleware
|
|
2713
|
-
const authMiddleware =
|
|
2714
|
-
|
|
2715
|
-
run
|
|
2877
|
+
const authMiddleware = r.middleware
|
|
2878
|
+
.task("app.middleware.task.auth")
|
|
2879
|
+
.run(async ({ task, next }, deps, config?: { requiredRole?: string }) => {
|
|
2716
2880
|
const context = RequestContext.use();
|
|
2717
2881
|
if (config?.requiredRole && context.role !== config.requiredRole) {
|
|
2718
2882
|
throw new Error("Insufficient permissions");
|
|
2719
2883
|
}
|
|
2720
2884
|
return next(task.input);
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2885
|
+
})
|
|
2886
|
+
.build();
|
|
2723
2887
|
|
|
2724
2888
|
// Services
|
|
2725
|
-
const userService =
|
|
2726
|
-
|
|
2727
|
-
dependencies
|
|
2728
|
-
init
|
|
2889
|
+
const userService = r
|
|
2890
|
+
.resource("app.services.user")
|
|
2891
|
+
.dependencies({ database })
|
|
2892
|
+
.init(async (_config, { database }) => ({
|
|
2729
2893
|
async createUser(userData: { name: string; email: string }) {
|
|
2730
2894
|
const users = database.collection("users");
|
|
2731
2895
|
const result = await users.insertOne(userData);
|
|
2732
2896
|
return { id: result.insertedId.toString(), ...userData };
|
|
2733
2897
|
},
|
|
2734
|
-
})
|
|
2735
|
-
|
|
2898
|
+
}))
|
|
2899
|
+
.build();
|
|
2736
2900
|
|
|
2737
2901
|
// Business Logic
|
|
2738
|
-
const registerUser =
|
|
2739
|
-
|
|
2740
|
-
dependencies
|
|
2741
|
-
run
|
|
2902
|
+
const registerUser = r
|
|
2903
|
+
.task("app.tasks.registerUser")
|
|
2904
|
+
.dependencies({ userService, userRegistered })
|
|
2905
|
+
.run(async ({ input: userData }, { userService, userRegistered }) => {
|
|
2742
2906
|
const user = await userService.createUser(userData);
|
|
2743
2907
|
await userRegistered({ userId: user.id, email: user.email });
|
|
2744
2908
|
return user;
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2909
|
+
})
|
|
2910
|
+
.build();
|
|
2747
2911
|
|
|
2748
|
-
const adminOnlyTask =
|
|
2749
|
-
|
|
2750
|
-
middleware
|
|
2751
|
-
run
|
|
2752
|
-
|
|
2753
|
-
},
|
|
2754
|
-
});
|
|
2912
|
+
const adminOnlyTask = r
|
|
2913
|
+
.task("app.tasks.adminOnly")
|
|
2914
|
+
.middleware([authMiddleware.with({ requiredRole: "admin" })])
|
|
2915
|
+
.run(async () => "Top secret admin data")
|
|
2916
|
+
.build();
|
|
2755
2917
|
|
|
2756
2918
|
// Event Handlers using hooks
|
|
2757
|
-
const sendWelcomeEmail =
|
|
2758
|
-
|
|
2759
|
-
on
|
|
2760
|
-
dependencies
|
|
2761
|
-
run
|
|
2919
|
+
const sendWelcomeEmail = r
|
|
2920
|
+
.hook("app.hooks.sendWelcomeEmail")
|
|
2921
|
+
.on(userRegistered)
|
|
2922
|
+
.dependencies({ emailService })
|
|
2923
|
+
.run(async (event, { emailService }) => {
|
|
2762
2924
|
console.log(`Sending welcome email to ${event.data.email}`);
|
|
2763
2925
|
await emailService.sendWelcome(event.data.email);
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2926
|
+
})
|
|
2927
|
+
.build();
|
|
2766
2928
|
|
|
2767
2929
|
// Express server
|
|
2768
|
-
const server =
|
|
2769
|
-
|
|
2770
|
-
register
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
userService,
|
|
2774
|
-
registerUser,
|
|
2775
|
-
adminOnlyTask,
|
|
2776
|
-
sendWelcomeEmail,
|
|
2777
|
-
],
|
|
2778
|
-
dependencies: { config, registerUser, adminOnlyTask },
|
|
2779
|
-
init: async (_, { config, registerUser, adminOnlyTask }) => {
|
|
2930
|
+
const server = r
|
|
2931
|
+
.resource("app.server")
|
|
2932
|
+
.register([config, database, userService, registerUser, adminOnlyTask, sendWelcomeEmail])
|
|
2933
|
+
.dependencies({ config, registerUser, adminOnlyTask })
|
|
2934
|
+
.init(async (_config, { config, registerUser, adminOnlyTask }) => {
|
|
2780
2935
|
const app = express();
|
|
2781
2936
|
app.use(express.json());
|
|
2782
2937
|
|
|
@@ -2866,32 +3021,32 @@ This contains the classic `value` and `dispose()` but it also exposes `logger`,
|
|
|
2866
3021
|
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.
|
|
2867
3022
|
|
|
2868
3023
|
```typescript
|
|
2869
|
-
import { run,
|
|
3024
|
+
import { run, r, override } from "@bluelibs/runner";
|
|
2870
3025
|
|
|
2871
3026
|
// Your real app
|
|
2872
|
-
const app =
|
|
2873
|
-
|
|
2874
|
-
register
|
|
3027
|
+
const app = r
|
|
3028
|
+
.resource("app")
|
|
3029
|
+
.register([
|
|
2875
3030
|
/* tasks, resources, middleware */
|
|
2876
|
-
]
|
|
2877
|
-
|
|
3031
|
+
])
|
|
3032
|
+
.build();
|
|
2878
3033
|
|
|
2879
3034
|
// Optional: overrides for infra (hello, fast tests!)
|
|
2880
|
-
const testDb =
|
|
2881
|
-
|
|
2882
|
-
init
|
|
2883
|
-
|
|
3035
|
+
const testDb = r
|
|
3036
|
+
.resource("app.database")
|
|
3037
|
+
.init(async () => new InMemoryDb())
|
|
3038
|
+
.build();
|
|
2884
3039
|
// If you use with override() it will enforce the same interface upon the overriden resource to ensure typesafety
|
|
2885
3040
|
const mockMailer = override(realMailer, { init: async () => fakeMailer });
|
|
2886
3041
|
|
|
2887
3042
|
// Create the test harness
|
|
2888
|
-
const harness = resource(
|
|
2889
|
-
id: "test",
|
|
2890
|
-
overrides: [mockMailer, testDb],
|
|
2891
|
-
});
|
|
3043
|
+
const harness = r.resource("test").overrides([mockMailer, testDb]).build();
|
|
2892
3044
|
|
|
2893
3045
|
// A task you want to drive in your tests
|
|
2894
|
-
const registerUser =
|
|
3046
|
+
const registerUser = r
|
|
3047
|
+
.task("app.tasks.registerUser")
|
|
3048
|
+
.run(async () => ({}))
|
|
3049
|
+
.build();
|
|
2895
3050
|
|
|
2896
3051
|
// Boom: full ecosystem
|
|
2897
3052
|
const { value: t, dispose } = await run(harness);
|
|
@@ -3230,7 +3385,7 @@ try {
|
|
|
3230
3385
|
- **Clarity**: Explicit dependencies, no hidden magic
|
|
3231
3386
|
- **Developer Experience**: Helpful error messages and clear patterns
|
|
3232
3387
|
|
|
3233
|
-
> **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
|
|
3388
|
+
> **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."
|
|
3234
3389
|
|
|
3235
3390
|
## The Migration Path
|
|
3236
3391
|
|