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