@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.
Files changed (156) hide show
  1. package/AI.md +319 -579
  2. package/README.md +885 -731
  3. package/dist/browser/index.cjs +1438 -251
  4. package/dist/browser/index.cjs.map +1 -1
  5. package/dist/browser/index.mjs +1433 -252
  6. package/dist/browser/index.mjs.map +1 -1
  7. package/dist/context.d.ts +31 -0
  8. package/dist/define.d.ts +9 -0
  9. package/dist/definers/builders/core.d.ts +30 -0
  10. package/dist/definers/builders/event.d.ts +12 -0
  11. package/dist/definers/builders/hook.d.ts +20 -0
  12. package/dist/definers/builders/middleware.d.ts +39 -0
  13. package/dist/definers/builders/resource.d.ts +40 -0
  14. package/dist/definers/builders/tag.d.ts +10 -0
  15. package/dist/definers/builders/task.d.ts +37 -0
  16. package/dist/definers/builders/task.phantom.d.ts +27 -0
  17. package/dist/definers/builders/utils.d.ts +4 -0
  18. package/dist/definers/defineEvent.d.ts +2 -0
  19. package/dist/definers/defineHook.d.ts +6 -0
  20. package/dist/definers/defineOverride.d.ts +17 -0
  21. package/dist/definers/defineResource.d.ts +2 -0
  22. package/dist/definers/defineResourceMiddleware.d.ts +2 -0
  23. package/dist/definers/defineTag.d.ts +12 -0
  24. package/dist/definers/defineTask.d.ts +18 -0
  25. package/dist/definers/defineTaskMiddleware.d.ts +2 -0
  26. package/dist/definers/tools.d.ts +47 -0
  27. package/dist/defs.d.ts +29 -0
  28. package/dist/edge/index.cjs +1438 -251
  29. package/dist/edge/index.cjs.map +1 -1
  30. package/dist/edge/index.mjs +1433 -252
  31. package/dist/edge/index.mjs.map +1 -1
  32. package/dist/errors.d.ts +104 -0
  33. package/dist/globals/globalEvents.d.ts +8 -0
  34. package/dist/globals/globalMiddleware.d.ts +31 -0
  35. package/dist/globals/globalResources.d.ts +32 -0
  36. package/dist/globals/globalTags.d.ts +11 -0
  37. package/dist/globals/middleware/cache.middleware.d.ts +27 -0
  38. package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
  39. package/dist/globals/middleware/retry.middleware.d.ts +21 -0
  40. package/dist/globals/middleware/timeout.middleware.d.ts +9 -0
  41. package/dist/globals/middleware/tunnel.middleware.d.ts +2 -0
  42. package/dist/globals/resources/debug/debug.resource.d.ts +7 -0
  43. package/dist/globals/resources/debug/debug.tag.d.ts +2 -0
  44. package/dist/globals/resources/debug/debugConfig.resource.d.ts +22 -0
  45. package/dist/globals/resources/debug/executionTracker.middleware.d.ts +50 -0
  46. package/dist/globals/resources/debug/globalEvent.hook.d.ts +27 -0
  47. package/dist/globals/resources/debug/hook.hook.d.ts +30 -0
  48. package/dist/globals/resources/debug/index.d.ts +6 -0
  49. package/dist/globals/resources/debug/middleware.hook.d.ts +30 -0
  50. package/dist/globals/resources/debug/types.d.ts +25 -0
  51. package/dist/globals/resources/debug/utils.d.ts +2 -0
  52. package/dist/globals/resources/queue.resource.d.ts +10 -0
  53. package/dist/globals/resources/tunnel/ejson-extensions.d.ts +1 -0
  54. package/dist/globals/resources/tunnel/error-utils.d.ts +1 -0
  55. package/dist/globals/resources/tunnel/plan.d.ts +19 -0
  56. package/dist/globals/resources/tunnel/protocol.d.ts +40 -0
  57. package/dist/globals/resources/tunnel/serializer.d.ts +9 -0
  58. package/dist/globals/resources/tunnel/tunnel.policy.tag.d.ts +18 -0
  59. package/dist/globals/resources/tunnel/tunnel.tag.d.ts +2 -0
  60. package/dist/globals/resources/tunnel/types.d.ts +17 -0
  61. package/dist/globals/tunnels/index.d.ts +23 -0
  62. package/dist/globals/types.d.ts +1 -0
  63. package/dist/http-client.d.ts +23 -0
  64. package/dist/http-fetch-tunnel.resource.d.ts +22 -0
  65. package/dist/index.d.ts +99 -0
  66. package/dist/models/DependencyProcessor.d.ts +48 -0
  67. package/dist/models/EventManager.d.ts +153 -0
  68. package/dist/models/LogPrinter.d.ts +55 -0
  69. package/dist/models/Logger.d.ts +85 -0
  70. package/dist/models/MiddlewareManager.d.ts +86 -0
  71. package/dist/models/OverrideManager.d.ts +13 -0
  72. package/dist/models/Queue.d.ts +26 -0
  73. package/dist/models/ResourceInitializer.d.ts +20 -0
  74. package/dist/models/RunResult.d.ts +35 -0
  75. package/dist/models/Semaphore.d.ts +61 -0
  76. package/dist/models/Store.d.ts +69 -0
  77. package/dist/models/StoreRegistry.d.ts +43 -0
  78. package/dist/models/StoreValidator.d.ts +8 -0
  79. package/dist/models/TaskRunner.d.ts +27 -0
  80. package/dist/models/UnhandledError.d.ts +11 -0
  81. package/dist/models/index.d.ts +11 -0
  82. package/dist/models/utils/findCircularDependencies.d.ts +16 -0
  83. package/dist/models/utils/safeStringify.d.ts +3 -0
  84. package/dist/node/exposure/allowList.d.ts +3 -0
  85. package/dist/node/exposure/authenticator.d.ts +6 -0
  86. package/dist/node/exposure/cors.d.ts +4 -0
  87. package/dist/node/exposure/createNodeExposure.d.ts +2 -0
  88. package/dist/node/exposure/exposureServer.d.ts +18 -0
  89. package/dist/node/exposure/httpResponse.d.ts +10 -0
  90. package/dist/node/exposure/logging.d.ts +4 -0
  91. package/dist/node/exposure/multipart.d.ts +27 -0
  92. package/dist/node/exposure/requestBody.d.ts +11 -0
  93. package/dist/node/exposure/requestContext.d.ts +17 -0
  94. package/dist/node/exposure/requestHandlers.d.ts +24 -0
  95. package/dist/node/exposure/resourceTypes.d.ts +60 -0
  96. package/dist/node/exposure/router.d.ts +17 -0
  97. package/dist/node/exposure/serverLifecycle.d.ts +13 -0
  98. package/dist/node/exposure/types.d.ts +31 -0
  99. package/dist/node/exposure/utils.d.ts +17 -0
  100. package/dist/node/exposure.resource.d.ts +12 -0
  101. package/dist/node/files.d.ts +9 -0
  102. package/dist/node/http-smart-client.model.d.ts +22 -0
  103. package/dist/node/index.d.ts +1 -0
  104. package/dist/node/inputFile.model.d.ts +22 -0
  105. package/dist/node/inputFile.utils.d.ts +14 -0
  106. package/dist/node/mixed-http-client.node.d.ts +27 -0
  107. package/dist/node/node.cjs +11168 -0
  108. package/dist/node/node.cjs.map +1 -0
  109. package/dist/node/node.d.ts +6 -0
  110. package/dist/node/node.mjs +11099 -0
  111. package/dist/node/node.mjs.map +1 -0
  112. package/dist/node/platform/createFile.d.ts +9 -0
  113. package/dist/node/tunnel.allowlist.d.ts +7 -0
  114. package/dist/node/upload/manifest.d.ts +22 -0
  115. package/dist/platform/adapters/browser.d.ts +14 -0
  116. package/dist/platform/adapters/edge.d.ts +5 -0
  117. package/dist/platform/adapters/node-als.d.ts +1 -0
  118. package/dist/platform/adapters/node.d.ts +15 -0
  119. package/dist/platform/adapters/universal-generic.d.ts +14 -0
  120. package/dist/platform/adapters/universal.d.ts +17 -0
  121. package/dist/platform/createFile.d.ts +10 -0
  122. package/dist/platform/createWebFile.d.ts +11 -0
  123. package/dist/platform/factory.d.ts +2 -0
  124. package/dist/platform/index.d.ts +27 -0
  125. package/dist/platform/types.d.ts +29 -0
  126. package/dist/processHooks.d.ts +2 -0
  127. package/dist/run.d.ts +14 -0
  128. package/dist/testing.d.ts +25 -0
  129. package/dist/tools/getCallerFile.d.ts +1 -0
  130. package/dist/tunnels/buildUniversalManifest.d.ts +24 -0
  131. package/dist/types/contracts.d.ts +63 -0
  132. package/dist/types/event.d.ts +74 -0
  133. package/dist/types/hook.d.ts +23 -0
  134. package/dist/types/inputFile.d.ts +34 -0
  135. package/dist/types/meta.d.ts +18 -0
  136. package/dist/types/resource.d.ts +87 -0
  137. package/dist/types/resourceMiddleware.d.ts +47 -0
  138. package/dist/types/runner.d.ts +55 -0
  139. package/dist/types/storeTypes.d.ts +40 -0
  140. package/dist/types/symbols.d.ts +28 -0
  141. package/dist/types/tag.d.ts +46 -0
  142. package/dist/types/task.d.ts +50 -0
  143. package/dist/types/taskMiddleware.d.ts +48 -0
  144. package/dist/types/utilities.d.ts +111 -0
  145. package/dist/universal/index.cjs +1438 -251
  146. package/dist/universal/index.cjs.map +1 -1
  147. package/dist/universal/index.mjs +1433 -252
  148. package/dist/universal/index.mjs.map +1 -1
  149. package/package.json +32 -4
  150. package/dist/index.d.mts +0 -1747
  151. package/dist/index.unused.js +0 -4466
  152. package/dist/index.unused.js.map +0 -1
  153. package/dist/node/index.cjs +0 -4498
  154. package/dist/node/index.cjs.map +0 -1
  155. package/dist/node/index.mjs +0 -4466
  156. 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 | Type | Notes |
13
- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
14
- | [Presentation Website](https://runner.bluelibs.com/) | Website | Overview, features, and highlights |
15
- | [BlueLibs Runner GitHub](https://github.com/bluelibs/runner) | GitHub | Source code, issues, and releases |
16
- | [BlueLibs Runner Dev](https://github.com/bluelibs/runner-dev) | GitHub | Development tools and CLI for BlueLibs Runner |
17
- | [UX Friendly Docs](https://bluelibs.github.io/runner/) | Docs | Clean, navigable documentation |
18
- | [AI Friendly Docs (<5000 tokens)](https://github.com/bluelibs/runner/blob/main/AI.md) | Docs | Short, token-friendly summary (<5000 tokens) |
19
- | [Migrate from 3.x.x to 4.x.x](https://github.com/bluelibs/runner/blob/main/readmes/MIGRATION.md) | Guide | Step-by-step upgrade from v3 to v4 |
20
- | [Runner Lore](https://github.com/bluelibs/runner/blob/main/readmes) | Docs | Design notes, deep dives, and context |
21
- | [Example: Express + OpenAPI + SQLite](https://github.com/bluelibs/runner/tree/main/examples/express-openapi-sqlite) | Example | Full Express + OpenAPI + SQLite demo |
22
- | [Example: Fastify + MikroORM + PostgreSQL](https://github.com/bluelibs/runner/tree/main/examples/fastify-mikroorm) | Example | Full Fastify + MikroORM + PostgreSQL demo |
23
- | [OpenAI Runner Chatbot](https://chatgpt.com/g/g-68b756abec648191aa43eaa1ea7a7945-runner?model=gpt-5-thinking) | Chatbot | Ask questions interactively, or feed README.md to your own AI |
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 { resource, task, run } from "@bluelibs/runner";
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 = resource({
62
- id: "app.server",
63
- init: async (config: { port: number }) => {
64
- const app = express();
65
- const server = app.listen(config.port);
66
- console.log(`Server running on port ${config.port}`);
67
- return { app, server };
68
- },
69
- dispose: async ({ server }) => server.close(),
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 = task({
74
- id: "app.tasks.createUser",
75
- // That's how you depend "value": resource as singleton value, task as function, event as emittor
76
- dependencies: { server },
77
- run: async (userData: { name: string }, { server }) => {
78
- // Your actual business logic here
79
- return { id: "user-123", ...userData };
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 = resource({
85
- id: "app",
86
- // Here you make the system aware of resources, tasks, middleware, and events.
87
- register: [server.with({ port: 3000 }), createUser],
88
- dependencies: { server, createUser },
89
- init: async (_, { server, createUser }) => {
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 { dispose, getResourceValue, runTask, emitEvent } = await run(app);
110
+ const runtime = await run(app);
111
+ const { dispose, runTask, getResourceValue, emitEvent } = runtime;
99
112
 
100
113
  // Or with debug logging enabled
101
- const { dispose } = await run(app, { debug: "verbose" });
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 composable. Unlike classes that accumulate methods like a hoarder accumulates stuff, tasks do one thing well.
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
- const sendEmail = task({
118
- id: "app.tasks.sendEmail",
119
- dependencies: { emailService, logger },
120
- run: async ({ to, subject, body }: EmailData, { emailService, logger }) => {
121
- await logger.info(`Sending email to ${to}`);
122
- return await emailService.send({ to, subject, body });
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. They have to be registered (via `register: []`) only once before they can be used.
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
- const database = resource({
156
- id: "app.db",
157
- init: async () => {
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: async (client) => await client.close(),
164
- });
279
+ })
280
+ .dispose(async (client) => client.close())
281
+ .build();
165
282
 
166
- const userService = resource({
167
- id: "app.services.user",
168
- dependencies: { database },
169
- init: async (_, { database }) => ({
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 = resource({
191
- id: "app.emailer",
192
- init: async (config: SMTPConfig) => ({
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 = resource({
201
- id: "app",
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 = resource({
218
- id: "db.service",
219
- context: () => ({
220
- connections: new Map(),
221
- pools: [],
222
- }),
223
- async init(config, deps, ctx) {
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 dispose(db, config, deps, ctx) {
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 [name, conn] of ctx.connections) {
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
- const userRegistered = event<{ userId: string; email: string }>({
247
- id: "app.events.userRegistered",
248
- });
362
+ import { r } from "@bluelibs/runner";
249
363
 
250
- const registerUser = task({
251
- id: "app.tasks.registerUser",
252
- dependencies: { userService, userRegistered },
253
- run: async (userData, { userService, userRegistered }) => {
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
- // Tell the world about it
257
- await userRegistered({ userId: user.id, email: user.email });
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 = hook({
267
- id: "app.hooks.sendWelcomeEmail",
268
- on: userRegistered, // Listen to the event
269
- run: async (eventData) => {
270
- // Everything is type-safe, automatically inferred from the 'on' property
271
- console.log(`Welcome email sent to ${eventData.data.email}`);
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
- import { hook } from "@bluelibs/runner";
282
-
283
- const logAllEventsHook = hook({
284
- id: "app.hooks.logAllEvents",
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
- // Note: Be careful with dependencies here since some events fire before initialization
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 { event, hook, globals } from "@bluelibs/runner";
407
+ import { r, globals } from "@bluelibs/runner";
299
408
 
300
409
  // Internal event that won't be seen by global listeners
301
- const internalEvent = event({
302
- id: "app.events.internal",
303
- tags: [globals.tags.excludeFromGlobalHooks],
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
- import { hook } from "@bluelibs/runner";
321
-
322
- const myHook = hook({
323
- id: "app.hooks.myEventHandler",
324
- on: userRegistered,
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 { hook, onAnyOf, isOneOf, event } from "@bluelibs/runner";
444
+ import { r, onAnyOf, isOneOf } from "@bluelibs/runner";
338
445
 
339
- const eUser = event<{ id: string; email: string }>({ id: "app.events.user" });
340
- const eAdmin = event<{ id: string; role: "admin" | "superadmin" }>({
341
- id: "app.events.admin",
342
- });
343
- const eGuest = event<{ id: string; guest: true }>({ id: "app.events.guest" });
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 = hook({
347
- id: "app.hooks.auditUsers",
348
- on: [eUser, eAdmin, eGuest],
349
- run: async (ev) => {
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 = hook({
357
- id: "app.hooks.auditSome",
358
- on: onAnyOf([eUser, eAdmin]), // to get a combined event
359
- run: async (ev) => {
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 = hook({
393
- id: "app.hooks.systemReady",
394
- on: globals.events.ready,
395
- run: async () => {
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
- import { event, hook } from "@bluelibs/runner";
412
-
413
- const criticalAlert = event<{
414
- severity: "low" | "medium" | "high" | "critical";
415
- }>({
416
- id: "app.events.alert",
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 = hook({
425
- id: "app.hooks.emergencyHandler",
426
- on: criticalAlert,
427
- order: -100, // Higher priority (lower numbers run first)
428
- run: async (event) => {
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 { taskMiddleware } from "@bluelibs/runner";
568
+ import { r } from "@bluelibs/runner";
454
569
 
455
570
  // Task middleware with config
456
571
  type AuthMiddlewareConfig = { requiredRole: string };
457
- const authMiddleware = taskMiddleware<AuthMiddlewareConfig>({
458
- id: "app.middleware.task.auth",
459
- run: async ({ task, next }, _deps, config) => {
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 = task({
466
- id: "app.tasks.adminOnly",
467
- middleware: [authMiddleware.with({ requiredRole: "admin" })],
468
- run: async (input: { user: User }) => "Secret admin data",
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 = taskMiddleware<AuthConfig, AuthInput, AuthOutput>({
481
- id: "app.middleware.task.auth",
482
- run: async ({ task, next }, _deps, config) => {
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 = resourceMiddleware<
498
- AuthConfig,
499
- AuthInput,
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 = task({
510
- id: "app.tasks.adminOnly",
511
- middleware: [authMiddleware.with({ requiredRole: "admin" })],
512
- run: async (input: { user: { role: string } }) => ({
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 { taskMiddleware, globals } from "@bluelibs/runner";
634
+ import { r, globals } from "@bluelibs/runner";
524
635
 
525
- const logTaskMiddleware = taskMiddleware({
526
- id: "app.middleware.log.task",
527
- everywhere: true,
528
- // or use a filter if you want to depend on certain tasks to exclude them from getting the middleware applied
529
- everywhere(task) {
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 = taskMiddleware<LogConfig, LogInput, LogOutput>({
567
- id: "app.middleware.logging",
568
- run: async ({ task, next }, _deps, config) => {
569
- console.log(config.includeTimestamp ? new Date() : "", task.input.data);
570
- const result = await next(task.input);
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 = task({
577
- id: "app.tasks.logged",
578
- middleware: [loggingMiddleware.with({ includeTimestamp: true })],
579
- run: async (input: { data: string }) => ({ data: input.data.toUpperCase() }),
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 { tag, task, hook, globals } from "@bluelibs/runner";
706
+ import { r } from "@bluelibs/runner";
593
707
 
594
708
  // Simple string tags
595
- const apiTask = task({
596
- id: "app.tasks.getUserData",
597
- tags: ["api", "public", "cacheable"],
598
- run: async (userId) => getUserFromDatabase(userId),
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 = task({
607
- id: "app.tasks.getUser",
608
- tags: ["api", httpTag.with({ method: "GET", path: "/users/:id" })],
609
- run: async ({ id }) => getUserFromDatabase(id),
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 { hook, globals } from "@bluelibs/runner";
730
+ import { r, globals } from "@bluelibs/runner";
619
731
 
620
732
  // Auto-register HTTP routes based on tags
621
- const routeRegistration = hook({
622
- id: "app.hooks.registerRoutes",
623
- on: globals.events.ready,
624
- dependencies: {
625
- store: globals.resources.store,
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 = tag<{ warnAboveMs: number }>({
655
- id: "performance.monitor",
656
- });
763
+ const performanceTag = r
764
+ .tag<{ warnAboveMs: number }>("performance.monitor")
765
+ .build();
657
766
 
658
- const performanceMiddleware = taskMiddleware({
659
- id: "app.middleware.performance",
660
- run: async ({ task, next }) => {
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 = task({
696
- id: "app.internal.cleanup",
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: async () => performCleanup(),
702
- });
809
+ ])
810
+ .run(async () => performCleanup())
811
+ .build();
703
812
 
704
- const internalEvent = event({
705
- id: "app.events.internal",
706
- tags: [globals.tags.excludeFromGlobalHooks], // Won't trigger wildcard listeners
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 = tag<void, void, { name: string }>({
717
- id: "contract.user",
718
- });
825
+ const userContract = r
826
+ .tag<void, void, { name: string }>("contract.user")
827
+ .build();
719
828
 
720
- const profileTask = task({
721
- id: "app.tasks.getProfile",
722
- tags: [userContract], // Must return { name: string }
723
- run: async () => ({ name: "Ada" }), // ✅ Satisfies contract
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 { resource, task } from "@bluelibs/runner";
735
- import { run } from "@bluelibs/runner";
843
+ import { r, run } from "@bluelibs/runner";
736
844
 
737
- const ping = task({
738
- id: "ping.task",
739
- async run() {
740
- return "pong";
741
- },
742
- });
845
+ const ping = r
846
+ .task("ping.task")
847
+ .run(async () => "pong")
848
+ .build();
743
849
 
744
- const app = resource({
745
- id: "app",
746
- register: [ping],
747
- async init(_, {}) {
748
- // your boot logic here
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 { task, resource, run } from "@bluelibs/runner";
935
+ import { r, run } from "@bluelibs/runner";
833
936
 
834
- const calculatorTask = task({
835
- id: "app.tasks.calculator",
836
- run: async (input: { value: number }) => {
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 = resource({
843
- id: "app.interceptor",
844
- dependencies: {
845
- calculatorTask,
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 = resource({
859
- id: "app",
860
- register: [calculatorTask, interceptorResource],
861
- dependencies: { calculatorTask },
862
- init: async (_, { calculatorTask }) => {
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
- const emailService = resource({
885
- id: "app.services.email",
886
- init: async () => new EmailService(),
887
- });
985
+ import { r } from "@bluelibs/runner";
888
986
 
889
- const paymentService = resource({
890
- id: "app.services.payment",
891
- init: async () => new PaymentService(),
892
- });
987
+ const emailService = r
988
+ .resource("app.services.email")
989
+ .init(async () => new EmailService())
990
+ .build();
893
991
 
894
- const userRegistration = task({
895
- id: "app.tasks.registerUser",
896
- dependencies: {
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: async (userData, { database, emailService, analytics }) => {
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 = task({
947
- id: "app.tasks.getUserData",
948
- middleware: [UserContext.require()], // This is a middleware that ensures the context is available before task runs, throws if not.
949
- run: async () => {
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 = resource({
957
- id: "app.requestHandler",
958
- init: async () => {
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 { task, resource, event } from "@bluelibs/runner";
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 = task({
983
- id: "calc.add",
984
- run: async (input: { a: number; b: number }) => input.a + input.b,
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 = resource({
992
- id: "app.config",
993
- init: async (cfg: { baseUrl: string }) => ({ baseUrl: cfg.baseUrl }),
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 = event<{ userId: string; email: string }>({
1001
- id: "app.events.userRegistered",
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 = task({
1038
- id: "app.handleRequest",
1039
- middleware: [requestMiddleware],
1040
- run: async (input: { path: string }) => {
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 = resource({
1074
- id: "app.database",
1075
- init: async () => {
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: async (connection) => {
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 = resource({
1087
- id: "app.server",
1088
- dependencies: { database: databaseResource },
1089
- init: async (config, { database }) => {
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: async (server) => {
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 = task({
1182
- id: "app.tasks.expensive",
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: async ({ userId }) => {
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 = resource({
1198
- id: "app.cache",
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 { task } from "@bluelibs/runner";
1381
+ import { r } from "@bluelibs/runner";
1215
1382
 
1216
- const redisCacheFactory = task({
1217
- id: "globals.tasks.cacheFactory", // Same ID as the default task
1218
- run: async (options: any) => {
1219
- return new RedisCache(options);
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 = resource({
1224
- id: "app",
1225
- register: [globals.resources.cache],
1226
- overrides: [redisCacheFactory], // Override the default cache factory
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 = task({
1263
- id: "user.create",
1264
- middleware: [auth, logging, metrics],
1265
- run: async (userData) => {
1266
- return database.users.create(userData);
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 = 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 = resource({
1310
- init: async () => {
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 = task({
1322
- middleware: [globals.middleware.cache.with({ ttl: 60000 })],
1323
- run: async (input) => {
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 = task({
1390
- id: "app.tasks.flakyApiCall",
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: async () => {
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 = task({
1427
- id: "app.tasks.externalApi",
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: async () => {
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 = task({
1440
- id: "app.tasks.resilient",
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: async () => {
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 { resource, globals } from "@bluelibs/runner";
1647
+ import { r, globals } from "@bluelibs/runner";
1484
1648
 
1485
- const app = resource({
1486
- id: "app",
1487
- dependencies: {
1488
- logger: globals.resources.logger;
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 = task({
1549
- id: "app.tasks.user.create",
1550
- dependencies: { logger: globals.resources.logger },
1551
- run: async (userData, { logger }) => {
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: userData.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(userData);
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: userData.email,
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 = task({
1594
- id: "app.tasks.handleRequest",
1595
- dependencies: { logger: globals.resources.logger },
1596
- run: async (requestData, { logger }) => {
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 { resource, globals } from "@bluelibs/runner";
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 = resource({
1653
- id: "app.resources.winstonBridge",
1654
- dependencies: {
1655
- logger: globals.resources.logger,
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 = resource({
1713
- id: "app.logger.custom",
1714
- dependencies: { eventManager: globals.resources.eventManager },
1715
- init: async (_, { eventManager }) => {
1716
- return new JSONLogger(eventManager);
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 = resource({
1780
- id: "app",
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 = task({
1804
- id: "app.tasks.critical",
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: async (input) => {
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 = resource({
1933
- id: "app.services.user",
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: { database },
1940
- init: async (_, { database }) => ({
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 = task({
1951
- id: "app.tasks.sendWelcomeEmail",
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: { emailService },
1957
- run: async (userData, { emailService }) => {
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 = task({
1987
- id: "app.tasks.ai.generateImage",
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: async (prompt) => {
2153
+ } as any)
2154
+ .run(async ({ input: prompt }) => {
1997
2155
  // AI generation logic
1998
- },
1999
- });
2156
+ })
2157
+ .build();
2000
2158
 
2001
- const database = resource({
2002
- id: "app.database.primary",
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
- // ... implementation
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 = resource({
2025
- id: "app.emailer",
2026
- init: async () => new SMTPEmailer(),
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 = resource({
2037
- ...productionEmailer,
2038
- init: async () => {},
2039
- });
2194
+ const testEmailer = r
2195
+ .resource("app.emailer")
2196
+ .init(async () => ({} as any))
2197
+ .build();
2040
2198
 
2041
- const app = resource({
2042
- id: "app",
2043
- register: [productionEmailer],
2044
- overrides: [testEmailer], // This replaces the production version
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 = task({ id: "app.tasks.compute", run: async () => 1 });
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 = resource({ id: "app.db", init: async () => "conn" });
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 = task({
2123
- id: namespaced("tasks.user.create-user"),
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 = resource({
2139
- id: "app.factories.myFactory",
2140
- init: async (config: { someOption: string }) => {
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
- return new MyClass(input, config.someOption);
2144
- };
2145
- },
2146
- });
2306
+ return (input: any) => new MyClass(input, config.someOption);
2307
+ })
2308
+ .build();
2147
2309
 
2148
- const app = resource({
2149
- id: "app",
2310
+ const app = r
2311
+ .resource("app")
2150
2312
  // Configure the factory resource upon registration
2151
- register: [myFactory.with({ someOption: "configured-value" })],
2152
- dependencies: { myFactory },
2153
- init: async (_, { myFactory }) => {
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 = task({
2196
- id: "app.tasks.createUser",
2197
- inputSchema: userSchema, // Works directly with Zod!
2198
- run: async (userData) => {
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 = resource({
2205
- id: "app",
2206
- register: [createUserTask],
2207
- dependencies: { createUserTask },
2208
- init: async (_, { createUserTask }) => {
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 = resource({
2244
- id: "app.resources.database",
2245
- configSchema: databaseConfigSchema, // Validation on .with()
2246
- init: async (config) => {
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 = resource({
2269
- id: "app",
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 = event({
2293
- id: "app.events.userAction",
2294
- payloadSchema: userActionSchema, // Validates on emit
2295
- });
2454
+ const userActionEvent = r
2455
+ .event("app.events.userAction")
2456
+ .payloadSchema(userActionSchema) // Validates on emit
2457
+ .build();
2296
2458
 
2297
- const notificationHook = hook({
2298
- id: "app.tasks.sendNotification",
2299
- on: userActionEvent,
2300
- run: async (eventData) => {
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 = resource({
2307
- id: "app",
2308
- register: [userActionEvent, notificationHook],
2309
- dependencies: { userActionEvent },
2310
- init: async (_, { userActionEvent }) => {
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 = taskMiddleware({ // or resourceMiddleware()
2342
- id: "app.middleware.timing",
2343
- configSchema: timingConfigSchema, // Validation on .with()
2344
- run: async ({ next }, _, config) => {
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 = task({
2372
- id: "app.tasks.example",
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: async () => "success",
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 = task({
2402
- id: "app.tasks.payment",
2403
- inputSchema: advancedSchema,
2404
- run: async (payment) => {
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 = task({
2497
- inputSchema: userSchema,
2498
- run: async (input: UserData) => {
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 = task({
2515
- id: "app.advanced",
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: async (_, { store, taskRunner, eventManager }) => {
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 = resource({
2535
- id: "app.services.user",
2536
- dependencies: { database, logger }, // Object - evaluated immediately
2537
- init: async (_, { database, logger }) => {
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 = resource({
2544
- id: "app.services.advanced",
2706
+ const advancedService = r
2707
+ .resource("app.services.advanced")
2545
2708
  // A function gives you the chance
2546
- dependencies: (config) => ({
2547
- // Config is what you receive when you register tise resource with .with()
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
- }), // Function - evaluated when needed
2554
- register: (config: ConfigType) => [
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: async (_, { database, logger, conditionalService }) => {
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 = resource({
2682
- id: "app.config",
2683
- init: async () => ({
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 = resource({
2692
- id: "app.database",
2693
- dependencies: { config },
2694
- init: async (_, { config }) => {
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: async (client) => await client.close(),
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 = event<{ userId: string; email: string }>({
2709
- id: "app.events.userRegistered",
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 = taskMiddleware<{ requiredRole?: string }>({
2714
- id: "app.middleware.task.auth",
2715
- run: async ({ task, next }, deps, config) => {
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 = resource({
2726
- id: "app.services.user",
2727
- dependencies: { database },
2728
- init: async (_, { database }) => ({
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 = task({
2739
- id: "app.tasks.registerUser",
2740
- dependencies: { userService, userRegistered },
2741
- run: async (userData, { userService, userRegistered }) => {
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 = task({
2749
- id: "app.tasks.adminOnly",
2750
- middleware: [authMiddleware.with({ requiredRole: "admin" })],
2751
- run: async () => {
2752
- return "Top secret admin data";
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 = hook({
2758
- id: "app.hooks.sendWelcomeEmail",
2759
- on: userRegistered,
2760
- dependencies: { emailService },
2761
- run: async (event, { emailService }) => {
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 = resource({
2769
- id: "app.server",
2770
- register: [
2771
- config,
2772
- database,
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, resource, task, event, override } from "@bluelibs/runner";
3023
+ import { run, r, override } from "@bluelibs/runner";
2870
3024
 
2871
3025
  // Your real app
2872
- const app = resource({
2873
- id: "app",
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 = resource({
2881
- id: "app.database",
2882
- init: async () => new InMemoryDb(),
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 = task({ id: "app.tasks.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… Ive seen worse cults."
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