@bluelibs/runner 2.2.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +1315 -942
  2. package/dist/common.types.d.ts +20 -0
  3. package/dist/common.types.js +4 -0
  4. package/dist/common.types.js.map +1 -0
  5. package/dist/context.d.ts +34 -0
  6. package/dist/context.js +58 -0
  7. package/dist/context.js.map +1 -0
  8. package/dist/define.d.ts +22 -3
  9. package/dist/define.js +52 -8
  10. package/dist/define.js.map +1 -1
  11. package/dist/defs.d.ts +52 -31
  12. package/dist/defs.js +10 -2
  13. package/dist/defs.js.map +1 -1
  14. package/dist/errors.js +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/event.types.d.ts +18 -0
  17. package/dist/event.types.js +4 -0
  18. package/dist/event.types.js.map +1 -0
  19. package/dist/examples/registrator-example.d.ts +122 -0
  20. package/dist/examples/registrator-example.js +147 -0
  21. package/dist/examples/registrator-example.js.map +1 -0
  22. package/dist/globals/globalEvents.d.ts +41 -0
  23. package/dist/globals/globalEvents.js +94 -0
  24. package/dist/globals/globalEvents.js.map +1 -0
  25. package/dist/globals/globalMiddleware.d.ts +23 -0
  26. package/dist/globals/globalMiddleware.js +15 -0
  27. package/dist/globals/globalMiddleware.js.map +1 -0
  28. package/dist/globals/globalResources.d.ts +27 -0
  29. package/dist/globals/globalResources.js +47 -0
  30. package/dist/globals/globalResources.js.map +1 -0
  31. package/dist/globals/middleware/cache.middleware.d.ts +34 -0
  32. package/dist/globals/middleware/cache.middleware.js +85 -0
  33. package/dist/globals/middleware/cache.middleware.js.map +1 -0
  34. package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
  35. package/dist/globals/middleware/requireContext.middleware.js +25 -0
  36. package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
  37. package/dist/globals/middleware/retry.middleware.d.ts +20 -0
  38. package/dist/globals/middleware/retry.middleware.js +34 -0
  39. package/dist/globals/middleware/retry.middleware.js.map +1 -0
  40. package/dist/globals/resources/queue.resource.d.ts +7 -0
  41. package/dist/globals/resources/queue.resource.js +31 -0
  42. package/dist/globals/resources/queue.resource.js.map +1 -0
  43. package/dist/index.d.ts +45 -9
  44. package/dist/index.js +14 -9
  45. package/dist/index.js.map +1 -1
  46. package/dist/middleware.types.d.ts +40 -0
  47. package/dist/middleware.types.js +4 -0
  48. package/dist/middleware.types.js.map +1 -0
  49. package/dist/models/DependencyProcessor.d.ts +2 -1
  50. package/dist/models/DependencyProcessor.js +11 -13
  51. package/dist/models/DependencyProcessor.js.map +1 -1
  52. package/dist/models/EventManager.d.ts +5 -0
  53. package/dist/models/EventManager.js +44 -2
  54. package/dist/models/EventManager.js.map +1 -1
  55. package/dist/models/Logger.d.ts +30 -13
  56. package/dist/models/Logger.js +130 -54
  57. package/dist/models/Logger.js.map +1 -1
  58. package/dist/models/OverrideManager.d.ts +13 -0
  59. package/dist/models/OverrideManager.js +70 -0
  60. package/dist/models/OverrideManager.js.map +1 -0
  61. package/dist/models/Queue.d.ts +25 -0
  62. package/dist/models/Queue.js +54 -0
  63. package/dist/models/Queue.js.map +1 -0
  64. package/dist/models/ResourceInitializer.d.ts +5 -2
  65. package/dist/models/ResourceInitializer.js +20 -14
  66. package/dist/models/ResourceInitializer.js.map +1 -1
  67. package/dist/models/Semaphore.d.ts +61 -0
  68. package/dist/models/Semaphore.js +166 -0
  69. package/dist/models/Semaphore.js.map +1 -0
  70. package/dist/models/Store.d.ts +17 -72
  71. package/dist/models/Store.js +71 -269
  72. package/dist/models/Store.js.map +1 -1
  73. package/dist/models/StoreConstants.d.ts +11 -0
  74. package/dist/models/StoreConstants.js +18 -0
  75. package/dist/models/StoreConstants.js.map +1 -0
  76. package/dist/models/StoreRegistry.d.ts +25 -0
  77. package/dist/models/StoreRegistry.js +171 -0
  78. package/dist/models/StoreRegistry.js.map +1 -0
  79. package/dist/models/StoreTypes.d.ts +21 -0
  80. package/dist/models/StoreTypes.js +3 -0
  81. package/dist/models/StoreTypes.js.map +1 -0
  82. package/dist/models/StoreValidator.d.ts +10 -0
  83. package/dist/models/StoreValidator.js +41 -0
  84. package/dist/models/StoreValidator.js.map +1 -0
  85. package/dist/models/TaskRunner.js +39 -24
  86. package/dist/models/TaskRunner.js.map +1 -1
  87. package/dist/models/VarStore.d.ts +17 -0
  88. package/dist/models/VarStore.js +60 -0
  89. package/dist/models/VarStore.js.map +1 -0
  90. package/dist/models/index.d.ts +3 -0
  91. package/dist/models/index.js +3 -0
  92. package/dist/models/index.js.map +1 -1
  93. package/dist/resource.types.d.ts +31 -0
  94. package/dist/resource.types.js +3 -0
  95. package/dist/resource.types.js.map +1 -0
  96. package/dist/run.d.ts +4 -1
  97. package/dist/run.js +6 -3
  98. package/dist/run.js.map +1 -1
  99. package/dist/symbols.d.ts +24 -0
  100. package/dist/symbols.js +29 -0
  101. package/dist/symbols.js.map +1 -0
  102. package/dist/task.types.d.ts +55 -0
  103. package/dist/task.types.js +23 -0
  104. package/dist/task.types.js.map +1 -0
  105. package/dist/tools/registratorId.d.ts +4 -0
  106. package/dist/tools/registratorId.js +40 -0
  107. package/dist/tools/registratorId.js.map +1 -0
  108. package/dist/tools/simpleHash.d.ts +9 -0
  109. package/dist/tools/simpleHash.js +34 -0
  110. package/dist/tools/simpleHash.js.map +1 -0
  111. package/dist/types/base-interfaces.d.ts +18 -0
  112. package/dist/types/base-interfaces.js +6 -0
  113. package/dist/types/base-interfaces.js.map +1 -0
  114. package/dist/types/base.d.ts +13 -0
  115. package/dist/types/base.js +3 -0
  116. package/dist/types/base.js.map +1 -0
  117. package/dist/types/dependencies.d.ts +22 -0
  118. package/dist/types/dependencies.js +3 -0
  119. package/dist/types/dependencies.js.map +1 -0
  120. package/dist/types/dependency-core.d.ts +14 -0
  121. package/dist/types/dependency-core.js +5 -0
  122. package/dist/types/dependency-core.js.map +1 -0
  123. package/dist/types/events.d.ts +52 -0
  124. package/dist/types/events.js +6 -0
  125. package/dist/types/events.js.map +1 -0
  126. package/dist/types/hooks.d.ts +16 -0
  127. package/dist/types/hooks.js +5 -0
  128. package/dist/types/hooks.js.map +1 -0
  129. package/dist/types/index.d.ts +14 -0
  130. package/dist/types/index.js +27 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/meta.d.ts +13 -0
  133. package/dist/types/meta.js +5 -0
  134. package/dist/types/meta.js.map +1 -0
  135. package/dist/types/middleware.d.ts +38 -0
  136. package/dist/types/middleware.js +6 -0
  137. package/dist/types/middleware.js.map +1 -0
  138. package/dist/types/registerable.d.ts +10 -0
  139. package/dist/types/registerable.js +5 -0
  140. package/dist/types/registerable.js.map +1 -0
  141. package/dist/types/resources.d.ts +44 -0
  142. package/dist/types/resources.js +5 -0
  143. package/dist/types/resources.js.map +1 -0
  144. package/dist/types/symbols.d.ts +24 -0
  145. package/dist/types/symbols.js +30 -0
  146. package/dist/types/symbols.js.map +1 -0
  147. package/dist/types/tasks.d.ts +41 -0
  148. package/dist/types/tasks.js +5 -0
  149. package/dist/types/tasks.js.map +1 -0
  150. package/dist/types/utilities.d.ts +7 -0
  151. package/dist/types/utilities.js +5 -0
  152. package/dist/types/utilities.js.map +1 -0
  153. package/package.json +10 -6
  154. package/src/__tests__/benchmark/benchmark.test.ts +1 -1
  155. package/src/__tests__/context.test.ts +91 -0
  156. package/src/__tests__/errors.test.ts +8 -5
  157. package/src/__tests__/globalEvents.test.ts +1 -1
  158. package/src/__tests__/globals/cache.middleware.test.ts +772 -0
  159. package/src/__tests__/globals/queue.resource.test.ts +141 -0
  160. package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
  161. package/src/__tests__/globals/retry.middleware.test.ts +157 -0
  162. package/src/__tests__/index.helper.test.ts +55 -0
  163. package/src/__tests__/models/EventManager.test.ts +144 -0
  164. package/src/__tests__/models/Logger.test.ts +291 -34
  165. package/src/__tests__/models/Queue.test.ts +189 -0
  166. package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
  167. package/src/__tests__/models/Semaphore.test.ts +713 -0
  168. package/src/__tests__/models/Store.test.ts +40 -0
  169. package/src/__tests__/models/TaskRunner.test.ts +86 -5
  170. package/src/__tests__/run.middleware.test.ts +166 -12
  171. package/src/__tests__/run.overrides.test.ts +13 -10
  172. package/src/__tests__/run.test.ts +363 -12
  173. package/src/__tests__/setOutput.test.ts +244 -0
  174. package/src/__tests__/tools/getCallerFile.test.ts +9 -9
  175. package/src/__tests__/typesafety.test.ts +54 -39
  176. package/src/context.ts +86 -0
  177. package/src/define.ts +84 -14
  178. package/src/defs.ts +91 -41
  179. package/src/errors.ts +3 -1
  180. package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
  181. package/src/globals/globalMiddleware.ts +14 -0
  182. package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
  183. package/src/globals/middleware/cache.middleware.ts +115 -0
  184. package/src/globals/middleware/requireContext.middleware.ts +36 -0
  185. package/src/globals/middleware/retry.middleware.ts +56 -0
  186. package/src/globals/resources/queue.resource.ts +34 -0
  187. package/src/index.ts +9 -5
  188. package/src/models/DependencyProcessor.ts +36 -40
  189. package/src/models/EventManager.ts +45 -5
  190. package/src/models/Logger.ts +170 -65
  191. package/src/models/OverrideManager.ts +84 -0
  192. package/src/models/Queue.ts +66 -0
  193. package/src/models/ResourceInitializer.ts +38 -20
  194. package/src/models/Semaphore.ts +208 -0
  195. package/src/models/Store.ts +94 -342
  196. package/src/models/StoreConstants.ts +17 -0
  197. package/src/models/StoreRegistry.ts +217 -0
  198. package/src/models/StoreTypes.ts +46 -0
  199. package/src/models/StoreValidator.ts +38 -0
  200. package/src/models/TaskRunner.ts +53 -40
  201. package/src/models/index.ts +3 -0
  202. package/src/run.ts +7 -4
  203. package/src/__tests__/index.ts +0 -15
  204. package/src/examples/express-mongo/index.ts +0 -1
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # BlueLibs Runner
1
+ # BlueLibs Runner: The Framework That Actually Makes Sense
2
+
3
+ _Or: How I Learned to Stop Worrying and Love Dependency Injection_
2
4
 
3
5
  <p align="center">
4
6
  <a href="https://github.com/bluelibs/runner/actions/workflows/ci.yml"><img src="https://github.com/bluelibs/runner/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status" /></a>
@@ -7,1336 +9,1707 @@
7
9
  <a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
8
10
  </p>
9
11
 
10
- - [View the documentation page here](https://bluelibs.github.io/runner/).
11
- - [Google Notebook LM Podcast](https://notebooklm.google.com/notebook/59bd49fa-346b-4cfb-bb4b-b59857c3b9b4/audio)
12
- - [Continue GPT Conversation](https://chatgpt.com/share/670392f8-7188-800b-9b4b-e49b437d77f7)
13
-
14
- BlueLibs Runner is a framework that provides a functional approach to building applications, whether small or large-scale. Its core concepts include Tasks, Resources, Events, and Middleware. Tasks represent the units of logic, while resources are singletons that provide shared services across the application. Events facilitate communication between different parts of the system, and Middleware allows interception and modification of task execution. The framework emphasizes an async-first philosophy, ensuring that all operations are executed asynchronously for smoother application flow.
15
-
16
- ## Building Blocks
17
-
18
- - **Tasks**: Core units of logic that encapsulate specific tasks. They can depend on resources, other tasks, and event emitters.
19
- - **Resources**: Singleton objects providing shared functionality. They can be constants, services, functions. They can depend on other resources, tasks, and event emitters.
20
- - **Events**: Facilitate asynchronous communication between different parts of your application. All tasks and resources emit events, allowing you to easily hook. Events can be listened to by tasks, resources, and middleware.
21
- - **Middleware**: Intercept and modify the execution of tasks or initialisation of your resources. They can be used to add additional functionality to your tasks. Middleware can be global or task-specific.
12
+ - [View the documentation page here](https://bluelibs.github.io/runner/)
22
13
 
23
- These are the concepts and philosophy:
14
+ Welcome to BlueLibs Runner, where we've taken the chaos of modern application architecture and turned it into something that won't make you question your life choices at 3am. This isn't just another framework – it's your new best friend who actually understands that code should be readable, testable, and not require a PhD in abstract nonsense to maintain.
24
15
 
25
- - **Async**: Everything is async, no more sync code for this framework. Sync-code can be done via resource services or within tasks, but the high-level flow needs to run async.
26
- - **Type safety**: Built with TypeScript for enhanced developer experience and type-safety everywhere, no more type mistakes.
27
- - **Functional**: We use functions and objects instead of classes for DI. This is a functional approach to building applications.
28
- - **Explicit Registration**: All tasks, resources, events, and middleware have to be explicitly registered to be used.
29
- - **Dependencies**: Tasks, resources, and middleware can have access to each other by depending on one another and event emitters. This is a powerful way to explicitly declare the dependencies.
16
+ ## What Is This Thing?
30
17
 
31
- Resources return their value to the container using the async `init()` function, making them available throughout the application.
18
+ BlueLibs Runner is a TypeScript-first framework that embraces functional programming principles while keeping dependency injection simple enough that you won't need a flowchart to understand your own code. Think of it as the anti-framework framework it gets out of your way and lets you build stuff that actually works.
32
19
 
33
- Tasks provide their output through the async `run()` function, allowing the results to be used across the application.
20
+ ### The Core Philosophy (AKA Why We Built This)
34
21
 
35
- All tasks, resources, events, and middleware must be explicitly registered to be used. Registration can only be done within resources.
22
+ - **Tasks are functions** - Not classes with 47 methods you'll never use
23
+ - **Resources are singletons** - Database connections, configs, services - the usual suspects
24
+ - **Events are just events** - Revolutionary concept, we know
25
+ - **Everything is async** - Because it's 2025 and blocking code is so 2005
26
+ - **Explicit beats implicit** - No magic, no surprises, no "how the hell does this work?"
36
27
 
37
- ## Installation
28
+ ## Quick Start (The "Just Show Me Code" Section)
38
29
 
39
30
  ```bash
40
31
  npm install @bluelibs/runner
41
32
  ```
42
33
 
43
- ## Basic Usage
34
+ Here's a complete Express server in less lines than most frameworks need for their "Hello World":
44
35
 
45
36
  ```typescript
46
- import { run, resource } from "@bluelibs/runner";
37
+ import express from "express";
38
+ import { resource, task, run } from "@bluelibs/runner";
47
39
 
48
- const minimal = resource({
49
- async init() {
50
- return "Hello world!";
40
+ // A resource is anything you want to share across your app
41
+ const server = resource({
42
+ id: "app.server",
43
+ init: async (config: { port: number }) => {
44
+ const app = express();
45
+ const server = app.listen(config.port);
46
+ console.log(`Server running on port ${config.port}`);
47
+ return { app, server };
51
48
  },
49
+ dispose: async ({ server }) => server.close(),
52
50
  });
53
51
 
54
- run(minimal).then((result) => {
55
- expect(result).toBe("Hello world!");
52
+ // Tasks are your business logic - pure-ish, easily testable functions
53
+ const createUser = task({
54
+ id: "app.tasks.createUser",
55
+ dependencies: { server },
56
+ run: async (userData: { name: string }, { server }) => {
57
+ // Your actual business logic here
58
+ return { id: "user-123", ...userData };
59
+ },
56
60
  });
57
- ```
58
61
 
59
- ## Resources and Tasks
62
+ // Wire everything together
63
+ const app = resource({
64
+ id: "app",
65
+ // Here you make the system aware of resources, tasks, middleware, and events.
66
+ register: [server, createUser],
67
+ dependencies: { server, createUser },
68
+ init: async (_, { server, createUser }) => {
69
+ server.app.post("/users", async (req, res) => {
70
+ const user = await createUser(req.body);
71
+ res.json(user);
72
+ });
73
+ },
74
+ });
60
75
 
61
- Resources are singletons and can include constants, services, functions, and more. They can depend on other resources, tasks, and event emitters.
76
+ // That's it. No webpack configs, no decorators, no XML.
77
+ const { dispose } = await run(app, { port: 3000 });
78
+ ```
62
79
 
63
- Tasks are designed to be trackable units of logic, such as handling specific routes on your HTTP server or performing actions needed by different parts of the application. This makes it easy to monitor what’s happening in your application.
80
+ ## The Big Four: Your New Building Blocks
64
81
 
65
- ```ts
66
- import { task, run, resource } from "@bluelibs/runner";
82
+ ### 1. Tasks: The Heart of Your Business Logic
67
83
 
68
- const helloTask = task({
69
- id: "app.hello",
70
- run: async () => "Hello World!",
71
- });
84
+ Tasks are functions with superpowers. They're pure-ish, testable, and composable. Unlike classes that accumulate methods like a hoarder accumulates stuff, tasks do one thing well.
72
85
 
73
- const app = resource({
74
- id: "app",
75
- register: [helloTask],
76
- dependencies: {
77
- hello: helloTask,
78
- },
79
- async init(_, deps) {
80
- return await deps.hello();
86
+ ```typescript
87
+ const sendEmail = task({
88
+ id: "app.tasks.sendEmail",
89
+ dependencies: { emailService, logger },
90
+ run: async ({ to, subject, body }: EmailData, { emailService, logger }) => {
91
+ await logger.info(`Sending email to ${to}`);
92
+ return await emailService.send({ to, subject, body });
81
93
  },
82
94
  });
83
95
 
84
- const result = await run(app); // "Hello World!"
96
+ // Test it like a normal function (because it basically is)
97
+ const result = await sendEmail.run(
98
+ { to: "user@example.com", subject: "Hi", body: "Hello!" },
99
+ { emailService: mockEmailService, logger: mockLogger }
100
+ );
85
101
  ```
86
102
 
87
- ### When to use each?
103
+ #### When to Task and When Not to Task
88
104
 
89
- It is unrealistic to create a task for everything you're doing in your system, not only it will be tedious for the developer, but it will affect performance unnecessarily. The idea is to think of a task of something that you want trackable as a higher-level action, for example:
105
+ Look, we get it. You could turn every function into a task, but that's like using a sledgehammer to crack nuts. Here's the deal:
90
106
 
91
- - "app.user.register" - this is a task, registers the user, returns a token
92
- - "app.user.createComment" - this is a task, creates a comment, returns the comment maybe
93
- - "app.user.updateFriendList" - this task can be re-used from many other tasks or resources as necessary
107
+ **Make it a task when:**
94
108
 
95
- Resources are more like services, they are singletons, they are meant to be used as a shared functionality across your application. They can be constants, services, functions, etc.
109
+ - It's a high-level business action: `"app.user.register"`, `"app.order.process"`
110
+ - You want it trackable and observable
111
+ - Multiple parts of your app need it
112
+ - It's complex enough to benefit from dependency injection
96
113
 
97
- ### Resource dispose()
114
+ **Don't make it a task when:**
98
115
 
99
- Resources can include a `dispose()` method for cleanup tasks. This is useful for actions like closing database connections. You should use `dispose()` when you have open connections or need to perform cleanup during a graceful shutdown.
116
+ - It's a simple utility function
117
+ - It's used in only one place or to help other tasks
118
+ - It's performance-critical and doesn't need DI overhead
100
119
 
101
- ```ts
102
- import { task, run, resource } from "@bluelibs/runner";
120
+ Think of tasks as the "main characters" in your application story, not every single line of dialogue.
103
121
 
104
- const dbResource = resource({
105
- async init(config, deps) {
106
- const db = await connectToDatabase();
107
- return db;
108
- },
109
- // the value returned from init() will be passed to dispose()
110
- async dispose(db, config, deps) {
111
- return db.close();
112
- },
113
- });
114
- ```
122
+ ### 2. Resources: Your Singletons That Don't Suck
115
123
 
116
- To call dispose(), you need to use the global resource called store, since everything is encapsulated. This allows you to access the internal parts of the system to start the disposal process.
124
+ Resources are 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.
117
125
 
118
- ```ts
119
- import { task, run, resource, globals } from "@bluelibs/runner";
126
+ ```typescript
127
+ const database = resource({
128
+ id: "app.db",
129
+ init: async () => {
130
+ const client = new MongoClient(process.env.DATABASE_URL as string);
131
+ await client.connect();
120
132
 
121
- const app = resource({
122
- id: "app",
123
- register: [dbResource],
124
- dependencies: {
125
- store: globals.resources.store,
126
- },
127
- async init(_, deps) {
128
- // We use the fact that we can reuse the value we got from here
129
- return {
130
- dispose: async () => deps.store.dispose(),
131
- };
133
+ return client;
132
134
  },
135
+ dispose: async (client) => await client.close(),
133
136
  });
134
137
 
135
- const value = await run(app);
136
- // To begin the disposal process.
137
- await value.dispose();
138
+ const userService = resource({
139
+ id: "app.services.user",
140
+ dependencies: { database },
141
+ init: async (_, { database }) => ({
142
+ async createUser(userData: UserData) {
143
+ return database.collection("users").insertOne(userData);
144
+ },
145
+ async getUser(id: string) {
146
+ return database.collection("users").findOne({ _id: id });
147
+ },
148
+ }),
149
+ });
138
150
  ```
139
151
 
140
- ### Resource configuration
141
-
142
- Resources can be set up with a configuration object, which is helpful for passing in specific settings. For example, if you’re building a library and initializing a mailer service, you can provide the SMTP credentials through this configuration.
152
+ #### Resource Configuration: Because Hardcoding Is for Amateurs
143
153
 
144
- ```ts
145
- import { task, run, resource } from "@bluelibs/runner";
154
+ Resources can be configured with type-safe options. No more "config object of unknown shape" nonsense.
146
155
 
147
- type Config = { smtpUrl: string; defaultFrom: string };
156
+ ```typescript
157
+ type SMTPConfig = {
158
+ smtpUrl: string;
159
+ from: string;
160
+ };
148
161
 
149
- const emailerResource = resource({
150
- // automatic type inference.
151
- async init(config: Config) {
152
- // todo: perform config checks with a library like zod
153
- return {
154
- sendEmail: async (to: string, subject: string, body: string) => {
155
- // send *email*
156
- },
157
- };
158
- },
162
+ const emailer = resource({
163
+ id: "app.emailer",
164
+ init: async (config: SMTPConfig) => ({
165
+ send: async (to: string, subject: string, body: string) => {
166
+ // Use config.smtpUrl and config.from
167
+ },
168
+ }),
159
169
  });
160
170
 
171
+ // Register with specific config
161
172
  const app = resource({
162
173
  id: "app",
163
174
  register: [
164
- // proper autocompletion is present
165
- emailerResource.with({ smtpUrl: "smtp://localhost", defaultFrom: "" }),
175
+ emailer.with({
176
+ smtpUrl: "smtp://localhost",
177
+ from: "noreply@myapp.com",
178
+ }),
179
+ // using emailer without with() will throw a type-error ;)
166
180
  ],
167
181
  });
168
182
  ```
169
183
 
170
- If by any chance your main `app` has configs then they will be passed via the second argument of `run`, like this:
184
+ #### Shared Context Between Init and Dispose
171
185
 
172
- ```ts
173
- run(app, config);
186
+ For cases where you need to share variables between `init()` and `dispose()` methods (because sometimes cleanup is complicated), use the enhanced context pattern:
187
+
188
+ ```typescript
189
+ const dbResource = resource({
190
+ id: "db.service",
191
+ context: () => ({
192
+ connections: new Map(),
193
+ pools: [],
194
+ }),
195
+ async init(config, deps, ctx) {
196
+ const db = await connectToDatabase();
197
+ ctx.connections.set("main", db);
198
+ ctx.pools.push(createPool(db));
199
+ return db;
200
+ },
201
+ async dispose(db, config, deps, ctx) {
202
+ // Same context available - no more "how do I access that thing I created?"
203
+ for (const pool of ctx.pools) {
204
+ await pool.drain();
205
+ }
206
+ for (const [name, conn] of ctx.connections) {
207
+ await conn.close();
208
+ }
209
+ },
210
+ });
174
211
  ```
175
212
 
176
- ## Dependencies
213
+ ### 3. Events: Async Communication That Actually Works
214
+
215
+ 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.
177
216
 
178
- You can depend on `tasks`, `resources`, `events` and (indirectly) on `middleware`.
217
+ ```typescript
218
+ const userRegistered = event<{ userId: string; email: string }>({
219
+ id: "app.events.userRegistered",
220
+ });
179
221
 
180
- ```ts
181
- import { task, resource, run, event } from "@bluelibs/runner";
222
+ const registerUser = task({
223
+ id: "app.tasks.registerUser",
224
+ dependencies: { userService, userRegistered },
225
+ run: async (userData, { userService, userRegistered }) => {
226
+ const user = await userService.createUser(userData);
182
227
 
183
- const helloWorld = task({
184
- middleware: [logMiddleware],
185
- dependencies: {
186
- userRegisteredEvent,
187
- },
188
- async run(_, deps) {
189
- await deps.userRegisteredEvent();
190
- return "Hello World!";
228
+ // Tell the world about it
229
+ await userRegistered({ userId: user.id, email: user.email });
230
+ return user;
191
231
  },
192
232
  });
193
233
 
194
- const app = resource({
195
- id: "app",
196
- // You have to register everything you use.
197
- register: [helloWorld, logMiddleware],
198
- dependencies: {
199
- helloWorld,
200
- },
201
- async init(_, deps) {
202
- await deps.helloWorld();
234
+ // Someone else handles the welcome email
235
+ const sendWelcomeEmail = task({
236
+ id: "app.tasks.sendWelcomeEmail",
237
+ on: userRegistered, // Listen to the event, notice the "on"
238
+ run: async (eventData) => {
239
+ // Everything is type-safe, automatically inferred from the 'on' property
240
+ console.log(`Welcome email sent to ${eventData.data.email}`);
203
241
  },
204
242
  });
205
-
206
- run(app);
207
243
  ```
208
244
 
209
- We have a circular dependency checker to ensure consistency. If a circular dependency is found, an error will be thrown, showing the exact paths involved.
210
-
211
- Tasks, however, are not bound by this restriction; they can freely depend on each other as needed.
212
-
213
- The dependencies get injected as follows:
214
-
215
- | Component | Injection Description |
216
- | ------------ | --------------------------------------------------------- |
217
- | `tasks` | Injected as functions with their input argument |
218
- | `resources` | Injected as their return value |
219
- | `events` | Injected as functions with their payload argument |
220
- | `middleware` | Not typically injected; used via a `middleware: []` array |
245
+ #### Wildcard Events: For When You Want to Know Everything
221
246
 
222
- ### Circular dependencies
247
+ Sometimes you need to be the nosy neighbor of your application:
223
248
 
224
- There are situations in which you can, with a resource ("A"), register a resource or task ("B"), which in turn depends on resource ("A"). The system permits this, because "A" does not depend on "B" for initialization. But TypeScript will break due to infinite type recursion guessing. Instead of manually specifying the types, just defer registration into a separate statement.
225
-
226
- ```ts
227
- import { task, run, resource } from "@bluelibs/runner";
228
-
229
- const resourceB = defineResource({
230
- id: "task",
231
- init: async (_, deps) => {
232
- deps.resource;
233
- return "";
249
+ ```typescript
250
+ const logAllEventsTask = task({
251
+ id: "app.tasks.logAllEvents",
252
+ on: "*", // Listen to EVERYTHING
253
+ run(event) {
254
+ console.log("Event detected", event.id, event.data);
255
+ // Note: Be careful with dependencies here since some events fire before initialization
234
256
  },
235
- dependencies: () => ({
236
- resource,
237
- }),
238
257
  });
258
+ ```
239
259
 
240
- const resourceA = defineResource({
241
- id: "resource",
242
- });
260
+ #### Tasks and Resources built-in events
243
261
 
244
- // Important: store, the registration separately, otherwise TypeScript will break
245
- resourceA.register = [resourceB];
262
+ Tasks and resources have their own lifecycle events that you can hook into:
263
+
264
+ ```typescript
265
+ const myTask = task({ ... });
266
+ const myResource = resource({ ... });
246
267
  ```
247
268
 
248
- ## Events
269
+ - `myTask.events.beforeRun` - Fires before the task runs
270
+ - `myTask.events.afterRun` - Fires after the task completes
271
+ - `myTask.events.onError` - Fires when the task fails
272
+ - `myResource.events.beforeInit` - Fires before the resource initializes
273
+ - `myResource.events.afterInit` - Fires after the resource initializes
274
+ - `myResource.events.onError` - Fires when the resource initialization fails
249
275
 
250
- Events are triggered when specific actions occur in your app, like a user registration or a new comment. When you catch these events, you also receive the emitted data along with the source of the event. Knowing the source of the event without explicitly specifying it can be very helpful in large applications.
276
+ Each event has its own utilities and functions.
251
277
 
252
- You can listen for these events using tasks and resources, and similarly, emit them from tasks and resources through dependencies.
278
+ #### Global Events: The System's Built-in Gossip Network
253
279
 
254
- ```ts
255
- import { task, run, event } from "@bluelibs/runner";
280
+ The framework comes with its own set of events that fire during the lifecycle. Think of them as the system's way of keeping you informed:
256
281
 
257
- const afterRegisterEvent = event<{ userId: string }>({
258
- id: "app.user.afterRegister",
259
- });
282
+ - `globals.tasks.beforeRun` - "Hey, I'm about to run this task"
283
+ - `globals.tasks.afterRun` - "Task completed, here's what happened"
284
+ - `globals.tasks.onError` - "Oops, something went wrong"
285
+ - `globals.resources.beforeInit` - "Initializing a resource"
286
+ - `globals.resources.afterInit` - "Resource is ready"
287
+ - `globals.resources.onError` - "Resource initialization failed"
260
288
 
261
- const root = resource({
262
- id: "app",
263
- register: [afterRegisterEvent],
264
- dependencies: {
265
- afterRegisterEvent,
266
- },
267
- async init(_, deps) {
268
- // the event becomes a function that you run with the propper payload
269
- await deps.afterRegisterEvent({ userId: string });
289
+ ```typescript
290
+ const taskLogger = task({
291
+ id: "app.logging.taskLogger",
292
+ on: globalEvents.tasks.beforeRun,
293
+ run(event) {
294
+ console.log(`Running task: ${event.source} with input:`, event.data.input);
270
295
  },
271
296
  });
272
297
  ```
273
298
 
274
- To listen to events you have to create a task.
275
-
276
- ### `task.on` property
277
-
278
- ```ts
279
- import { task, run, event } from "@bluelibs/runner";
299
+ ### 4. Middleware: The Interceptor Pattern Done Right
280
300
 
281
- const afterRegisterEvent = event<{ userId: string }>({
282
- id: "app.user.afterRegister",
283
- });
301
+ Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
284
302
 
285
- const helloTask = task({
286
- id: "app.hello",
287
- on: afterRegisterEvent,
288
- listenerPriority: 0, // this is the order in which the task will be executed when `on` is present
289
- run(event) {
290
- event.source; // id which middleware, task, resource triggered it
291
- console.log("User has been registered!");
303
+ ```typescript
304
+ // This is a middleware that accepts a config
305
+ const authMiddleware = middleware({
306
+ id: "app.middleware.auth",
307
+ // You can also add dependencies, no problem.
308
+ run: async (
309
+ { task, next },
310
+ dependencies,
311
+ config: { requiredRole: string }
312
+ ) => {
313
+ const user = task.input.user;
314
+ if (!user || user.role !== config.requiredRole) {
315
+ throw new Error("Unauthorized");
316
+ }
317
+ return next(task.input);
292
318
  },
293
319
  });
294
320
 
295
- const app = resource({
296
- id: "app",
297
- register: [afterRegisterEvent, helloTask],
298
- dependencies: {
299
- afterRegisterEvent,
300
- },
301
- async init(_, deps) {
302
- await deps.afterRegisterEvent({ userId: "XXX" });
321
+ const adminTask = task({
322
+ id: "app.tasks.adminOnly",
323
+ // If the configuration accepts {} or is empty, .with() becomes optional, otherwise it becomes enforced.
324
+ middleware: [authMiddleware.with({ requiredRole: "admin" })],
325
+ run: async (input: { user: User }) => {
326
+ return "Secret admin data";
303
327
  },
304
328
  });
305
329
  ```
306
330
 
307
- ### wildcard events
331
+ #### Global Middleware: Apply Everywhere, Configure Once
308
332
 
309
- You can listen to all events by using the wildcard `*`. However you need to **manually check** if your dependencies have been computed. For example we dispatch events like 'global.beforeInit' before anything is initialized.
333
+ Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
310
334
 
311
- ```ts
312
- import { task, resource, run, event, global } from "@bluelibs/runner";
313
-
314
- const afterRegisterEvent = event<{ userId: string }>({
315
- id: "app.user.registered",
316
- });
317
-
318
- const logAllEventsTask = task({
319
- id: "app.tasks.logAllEvents",
320
- on: "*",
321
- run(event) {
322
- console.log("Event detected", event.id, event.data);
335
+ ```typescript
336
+ const logMiddleware = middleware({
337
+ id: "app.middleware.log",
338
+ run: async ({ task, next }) => {
339
+ console.log(`Executing: ${task.definition.id}`);
340
+ const result = await next(task.input);
341
+ console.log(`Completed: ${task.definition.id}`);
342
+ return result;
323
343
  },
324
344
  });
325
345
 
326
- const root = resource({
346
+ const app = resource({
327
347
  id: "app",
328
- register: [afterRegisterEvent, logAllEventsTask],
329
- dependencies: {},
330
- async init(_, deps) {
331
- deps.afterRegisterEvent({ userId: "XXX" });
332
- },
348
+ register: [
349
+ logMiddleware.everywhere({ tasks: true, resources: false }), // Only tasks get logged
350
+ ],
333
351
  });
334
352
  ```
335
353
 
336
- ## Middleware
337
-
338
- Middleware intercepts the execution of tasks or the initialization of resources, providing a powerful means to enhance functionality. The order in which middleware is registered dictates its execution priority: the first middleware registered is the first to run, while the last middleware in the middleware array at the task level is the closest to the task itself, executing just before the task completes. (Imagine an onion if you will, with the task at the core.)
354
+ ## Context: Request-Scoped Data That Doesn't Drive You Insane
339
355
 
340
- ```ts
341
- import { task, resource, run, event } from "@bluelibs/runner";
356
+ 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.
342
357
 
343
- const logMiddleware = middleware({
344
- id: "app.middleware.log",
345
- dependencies: {
346
- // inject tasks, resources, eventCallers here.
347
- },
348
- async run(data, deps) {
349
- const { taskDefinition, resourceDefinition, config, next, input } = data;
350
-
351
- // The middleware can be for a task or a resource, depending on which you get the right elements.
352
- if (taskDefinition) {
353
- console.log("Before task", taskDefinition.id);
354
- const result = await next(input); // pass the input to the next middleware or task
355
- console.log("After task", taskDefinition.id);
356
- } else {
357
- console.log("Before resource", resourceDefinition.id);
358
- const result = await next(config); // pass the input to the next middleware or task
359
- console.log("After resource", resourceDefinition.id);
360
- }
358
+ ```typescript
359
+ const UserContext = createContext<{ userId: string; role: string }>(
360
+ "app.userContext"
361
+ );
361
362
 
362
- return result;
363
+ const getUserData = task({
364
+ id: "app.tasks.getUserData",
365
+ middleware: [UserContext.require()], // This is a middleware that ensures the context is available before task runs, throws if not.
366
+ run: async () => {
367
+ const user = UserContext.use(); // Available anywhere in the async chain
368
+ return `Current user: ${user.userId} (${user.role})`;
363
369
  },
364
370
  });
365
371
 
366
- const helloTask = task({
367
- id: "app.hello",
368
- middleware: [logMiddleware],
369
- run(event) {
370
- console.log("User has been registered!");
372
+ // Provide context at the entry point
373
+ const handleRequest = resource({
374
+ id: "app.requestHandler",
375
+ init: async () => {
376
+ return UserContext.provide({ userId: "123", role: "admin" }, async () => {
377
+ // All tasks called within this scope have access to UserContext
378
+ return await getUserData();
379
+ });
371
380
  },
372
381
  });
373
382
  ```
374
383
 
375
- ### Global
376
-
377
- If you want to register a middleware for all tasks and resources, here's how you can do it:
384
+ ### Context with Middleware: The Power Couple
378
385
 
379
- ```ts
380
- import { run, resource } from "@bluelibs/runner";
386
+ Context shines when combined with middleware for request-scoped data:
381
387
 
382
- const logMiddleware = middleware({
383
- id: "app.middleware.log",
384
- // ... rest
388
+ ```typescript
389
+ const RequestContext = createContext<{
390
+ requestId: string;
391
+ startTime: number;
392
+ userAgent?: string;
393
+ }>("app.requestContext");
394
+
395
+ const requestMiddleware = middleware({
396
+ id: "app.middleware.request",
397
+ run: async ({ task, next }) => {
398
+ // This works even in express middleware if needed.
399
+ return RequestContext.provide(
400
+ {
401
+ requestId: crypto.randomUUID(),
402
+ startTime: Date.now(),
403
+ userAgent: "MyApp/1.0",
404
+ },
405
+ async () => {
406
+ return next(task.input);
407
+ }
408
+ );
409
+ },
385
410
  });
386
411
 
387
- const root = resource({
388
- id: "app",
389
- register: [logMiddleware.global() /* this will apply to all tasks */],
412
+ const handleRequest = task({
413
+ id: "app.handleRequest",
414
+ middleware: [requestMiddleware],
415
+ run: async (input: { path: string }) => {
416
+ const request = RequestContext.use();
417
+ console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
418
+ return { success: true, requestId: request.requestId };
419
+ },
390
420
  });
391
421
  ```
392
422
 
393
- The middleware can only be registered once. This means that if you register a middleware as global, you cannot specify it as a task middleware. This is to avoid confusion and to keep the system clean.
423
+ ## Dependency Management: The Index Pattern
394
424
 
395
- ## Errors
425
+ When your app grows beyond "hello world", you'll want to group related dependencies. The `index()` helper is your friend - it's basically a 3-in-1 resource that registers, depends on, and returns everything you give it.
396
426
 
397
- If an error is thrown in a task, the error will be propagated up to the top runner.
398
-
399
- ```ts
400
- import { task, run, event } from "@bluelibs/runner";
401
-
402
- const helloWorld = task({
403
- id: "app.helloWorld",
404
- run() {
405
- throw new Error("Something went wrong");
406
- },
427
+ ```typescript
428
+ // This registers all services, depends on them, and returns them as one clean interface
429
+ const services = index({
430
+ userService,
431
+ emailService,
432
+ paymentService,
433
+ notificationService,
407
434
  });
408
435
 
409
436
  const app = resource({
410
437
  id: "app",
411
- register: [helloWorld],
412
- dependencies: {
413
- helloWorld,
438
+ register: [services],
439
+ dependencies: { services },
440
+ init: async (_, { services }) => {
441
+ // Access everything through one clean interface
442
+ const user = await services.userService.createUser(userData);
443
+ await services.emailService.sendWelcome(user.email);
414
444
  },
415
- async init() {
416
- await helloWorld();
417
- },
418
- });
419
-
420
- run(app).catch((err) => {
421
- console.error(err);
422
445
  });
423
446
  ```
424
447
 
425
- You can listen to errors via events:
448
+ ## Error Handling: Graceful Failures
426
449
 
427
- ```ts
428
- const helloWorld = task({
429
- id: "app.tasks.helloWorld.onError",
430
- on: helloWorld.events.onError,
431
- run({ error, input, suppress }, deps) {
432
- // this will be called when an error happens
450
+ Errors happen. When they do, you can listen for them and decide what to do. No more unhandled promise rejections ruining your day.
433
451
 
434
- // if you handled the error, and you don't want it propagated to the top, supress the propagation.
435
- suppress();
452
+ ```typescript
453
+ const riskyTask = task({
454
+ id: "app.tasks.risky",
455
+ run: async () => {
456
+ throw new Error("Something went wrong");
436
457
  },
458
+ // Behind the scenes when you create a task() we create these 3 events for you (onError, beforeRun, afterRun)
437
459
  });
438
- ```
439
460
 
440
- ```ts
441
- const helloWorld = resource({
442
- id: "app.resources.helloWorld.onError",
443
- on: helloWorld.events.onError,
444
- init({ error, input, suppress }, deps) {
445
- // this will be called when an error happens
461
+ const errorHandler = task({
462
+ id: "app.tasks.errorHandler",
463
+ on: riskyTask.events.onError,
464
+ run: async (event) => {
465
+ console.error("Task failed:", event.data.error);
446
466
 
447
- // if you handled the error, and you don't want it propagated to the top, supress the propagation.
448
- suppress();
467
+ // Don't let the error bubble up - this makes the task return undefined
468
+ event.data.suppress();
449
469
  },
450
470
  });
451
471
  ```
452
472
 
453
- ## Meta
454
-
455
- You can attach metadata to tasks, resources, events, and middleware.
473
+ ## Caching: Built-in Performance
456
474
 
457
- ```ts
458
- import { task, run, event } from "@bluelibs/runner";
475
+ Because nobody likes waiting for the same expensive operation twice:
459
476
 
460
- const helloWorld = task({
461
- id: "app.helloWorld",
462
- meta: {
463
- title: "Hello World",
464
- description: "This is a hello world task",
465
- tags: ["api"],
466
- },
467
- run() {
468
- return "Hello World!";
477
+ ```typescript
478
+ import { globals } from "@bluelibs/runner";
479
+
480
+ const expensiveTask = task({
481
+ id: "app.tasks.expensive",
482
+ middleware: [
483
+ globals.middleware.cache.with({
484
+ // lru-cache options by default
485
+ ttl: 60 * 1000, // Cache for 1 minute
486
+ keyBuilder: (taskId, input) => `${taskId}-${input.userId}`, // optional key builder
487
+ }),
488
+ ],
489
+ run: async ({ userId }) => {
490
+ // This expensive operation will be cached
491
+ return await doExpensiveCalculation(userId);
469
492
  },
470
493
  });
494
+
495
+ // Global cache configuration
496
+ const app = resource({
497
+ id: "app.cache",
498
+ register: [
499
+ // You have to register it, cache resource is not enabled by default.
500
+ globals.resources.cache.with({
501
+ defaultOptions: {
502
+ max: 1000, // Maximum items in cache
503
+ ttl: 30 * 1000, // Default TTL
504
+ },
505
+ async: false, // in-memory is sync by default
506
+ // When using redis or others mark this as true to await response.
507
+ }),
508
+ ],
509
+ });
471
510
  ```
472
511
 
473
- This is particularly helpful to use in conjunction with global middlewares, or global events, they can read some meta tag definition and act accordingly, decorate them or log them.
512
+ Want Redis instead of the default LRU cache? No problem, just override the cache factory task:
474
513
 
475
- The interfaces look like this:
514
+ ```typescript
515
+ import { task } from "@bluelibs/runner";
476
516
 
477
- ```ts
478
- export interface IMeta {
479
- title?: string;
480
- description?: string;
481
- tags: string[];
482
- }
517
+ const redisCacheFactory = task({
518
+ id: "globals.tasks.cacheFactory", // Same ID as the default task
519
+ run: async (options: any) => {
520
+ return new RedisCache(options); // Make sure to turn async on in the cacher.
521
+ },
522
+ });
483
523
 
484
- export interface ITaskMeta extends IMeta {}
485
- export interface IResourceMeta extends IMeta {}
486
- export interface IEventMeta extends IMeta {}
487
- export interface IMiddlewareMeta extends IMeta {}
524
+ const app = resource({
525
+ id: "app",
526
+ register: [
527
+ // Your other stuff
528
+ ],
529
+ overrides: [redisCacheFactory], // Override the default cache factory
530
+ });
488
531
  ```
489
532
 
490
- Which means you can extend them in your system to add more keys to better describe your actions.
491
-
492
- ## Internal Services
533
+ ## Logging: Because Console.log Isn't Professional
493
534
 
494
- We expose direct access to the following internal services:
535
+ _The structured logging system that actually makes debugging enjoyable_
495
536
 
496
- - Store (contains Map()s for events, tasks, resources, middleware configurations)
497
- - TaskRunner (can run tasks definitions directly and within D.I. context)
498
- - EventManager (can emit and listen to events)
537
+ BlueLibs Runner comes with a built-in logging system that's event-driven, structured, and doesn't make you hate your life when you're trying to debug at 2 AM. It emits events for everything, so you can handle logs however you want - ship them to your favorite log warehouse, pretty-print them to console, or ignore them entirely (we won't judge).
499
538
 
500
- Attention, we do not encourage you to use these services directly, unless you really have to, they are exposed for advanced use-case scenarios.
539
+ ### Quick Start: Basic Logging
501
540
 
502
- ```ts
503
- import { task, run, event, globals } from "@bluelibs/runner";
541
+ ```typescript
542
+ import { globals } from "@bluelibs/runner";
504
543
 
505
- const helloWorld = task({
506
- id: "app.helloWorld",
507
- dependencies: {
508
- store: globals.resources.store,
509
- taskRunner: globals.resources.taskRunner,
510
- eventManager: globals.resources.eventManager,
511
- }
512
- run(_, deps) {
513
- // you benefit of full autocompletion here
544
+ const businessTask = task({
545
+ id: "app.tasks.business",
546
+ dependencies: { logger: globals.resources.logger },
547
+ run: async (_, { logger }) => {
548
+ logger.info("Starting business process");
549
+ logger.warn("This might take a while");
550
+ logger.error("Oops, something went wrong", {
551
+ error: new Error("Database connection failed"),
552
+ });
553
+ logger.critical("System is on fire", {
554
+ data: { temperature: "9000°C" },
555
+ });
514
556
  },
515
557
  });
516
558
  ```
517
559
 
518
- ## Namespacing
519
-
520
- Domain usually is "app", but as your application grows or you plan on building external libraries the naming convention should be: "companyName.packageName".
560
+ ### Log Levels: From Whispers to Screams
521
561
 
522
- | Type | Format |
523
- | -------------- | ----------------------------------------- |
524
- | Tasks | `{domain}.tasks.{taskName}` |
525
- | Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
526
- | Resources | `{domain}.resources.{resourceName}` |
527
- | Events | `{domain}.events.{eventName}` |
528
- | Middleware | `{domain}.middleware.{middlewareName}` |
562
+ The logger supports six log levels with increasing severity:
529
563
 
530
- You can always create helpers for you as you're creating your tasks, resources, middleware:
564
+ | Level | Severity | When to Use | Color |
565
+ | ---------- | -------- | ------------------------------------------- | ------- |
566
+ | `trace` | 0 | Ultra-detailed debugging info | Gray |
567
+ | `debug` | 1 | Development and debugging information | Cyan |
568
+ | `info` | 2 | General information about normal operations | Green |
569
+ | `warn` | 3 | Something's not right, but still working | Yellow |
570
+ | `error` | 4 | Errors that need attention | Red |
571
+ | `critical` | 5 | System-threatening issues | Magenta |
531
572
 
532
- ```ts
533
- function namespaced(id) {
534
- return `bluelibs.core.${id}`;
535
- }
573
+ ```typescript
574
+ // All log levels are available as methods
575
+ logger.trace("Ultra-detailed debugging info");
576
+ logger.debug("Development debugging");
577
+ logger.info("Normal operation");
578
+ logger.warn("Something's fishy");
579
+ logger.error("Houston, we have a problem");
580
+ logger.critical("DEFCON 1: Everything is broken");
536
581
  ```
537
582
 
538
- We need to import all the tasks, resources, events, and middlewares, a convention for their naming is to export them like this
539
-
540
- ```ts
541
- import { userCreatedEvent } from "./events";
542
- export const events = {
543
- userCreated: userCreatedEvent,
544
- // ...
545
- };
546
-
547
- export const tasks = {
548
- doSomething: doSomethingTask,
549
- };
550
-
551
- export const resources = {
552
- root: rootResource,
553
- user: userResource,
554
- };
555
- ```
583
+ ### Structured Logging: More Than Just Strings
556
584
 
557
- Often the root will register all needed items, so you don't have to register anything but the root.
585
+ The logger accepts rich, structured data that makes debugging actually useful:
558
586
 
559
- ```ts
560
- import { resource } from "@bluelibs/runner";
561
- import * as packageName from "package-name";
587
+ ```typescript
588
+ const userTask = task({
589
+ id: "app.tasks.user.create",
590
+ dependencies: { logger: globals.resources.logger },
591
+ run: async (userData, { logger }) => {
592
+ // Basic message
593
+ logger.info("Creating new user");
594
+
595
+ // With structured data
596
+ logger.info("User creation attempt", {
597
+ data: {
598
+ email: userData.email,
599
+ registrationSource: "web",
600
+ timestamp: new Date().toISOString(),
601
+ },
602
+ });
562
603
 
563
- const app = resource({
564
- id: "app",
565
- register: [packageName.resources.root],
604
+ // With error information
605
+ try {
606
+ const user = await createUser(userData);
607
+ logger.info("User created successfully", {
608
+ data: { userId: user.id, email: user.email },
609
+ });
610
+ } catch (error) {
611
+ logger.error("User creation failed", {
612
+ error,
613
+ data: {
614
+ attemptedEmail: userData.email,
615
+ validationErrors: error.validationErrors,
616
+ },
617
+ });
618
+ }
619
+ },
566
620
  });
567
-
568
- run(app);
569
621
  ```
570
622
 
571
- Now you can freely use any of the tasks, resources, events, and middlewares from the `packageName` namespace.
572
-
573
- This approach is very powerful when you have multiple packages and you want to compose them together.
574
-
575
- ## Real life
576
-
577
- Or is it just fantasy?
623
+ ### Context-Aware Logging: The with() Method
578
624
 
579
- Typically, an application consists of an Express server (to handle HTTP requests), a database, and various services. You can conveniently define all of these components within a single file and execute them together.
625
+ Create logger instances with bound context for consistent metadata across related operations:
580
626
 
581
- ```ts
582
- import { task, resource, run, event } from "@bluelibs/runner";
583
- import express from "express";
627
+ ```typescript
628
+ const RequestContext = createContext<{ requestId: string; userId: string }>(
629
+ "app.requestContext"
630
+ );
631
+
632
+ const requestHandler = task({
633
+ id: "app.tasks.handleRequest",
634
+ dependencies: { logger: globals.resources.logger },
635
+ run: async (requestData, { logger }) => {
636
+ const request = RequestContext.use();
637
+
638
+ // Create a contextual logger with bound metadata
639
+ const requestLogger = logger.with({
640
+ requestId: request.requestId,
641
+ userId: request.userId,
642
+ source: "api.handler",
643
+ });
584
644
 
585
- const expressServer = resource({
586
- id: "app.express",
587
- async init() {
588
- const app = express();
589
- app.listen(3000).then(() => {
590
- console.log("Server is running on port 3000");
645
+ // All logs from this logger will include the bound context
646
+ requestLogger.info("Processing request", {
647
+ data: { endpoint: requestData.path },
591
648
  });
592
649
 
593
- // because we return it you can now access it via dependencies
594
- return app;
595
- },
596
- });
650
+ requestLogger.debug("Validating input", {
651
+ data: { inputSize: JSON.stringify(requestData).length },
652
+ });
597
653
 
598
- const setupRoutes = resource({
599
- id: "app.routes",
600
- dependencies: {
601
- expressServer,
602
- },
603
- async init(_, deps) {
604
- deps.expressServer.use("/api", (req, res) => {
605
- res.json({ hello: "world" });
654
+ // Context is automatically included in all log events
655
+ requestLogger.error("Request processing failed", {
656
+ error: new Error("Invalid input"),
657
+ data: { stage: "validation" },
606
658
  });
607
659
  },
608
660
  });
609
-
610
- // Just run them, init() will be called everywhere.
611
- const app = resource({
612
- id: "app",
613
- register: [expressServer, setupRoutes],
614
- });
615
-
616
- run();
617
661
  ```
618
662
 
619
- The system intelligently determines the order in which init() functions should be called, ensuring that all dependencies are initialized first. In the case of circular dependencies, it will throw an error, providing the exact paths to help identify the issue.
663
+ ### Print Threshold: Control What Shows Up
620
664
 
621
- ### Business config
665
+ By default, logs are just events - they don't print to console unless you tell them to. Set a print threshold to automatically output logs at or above a certain level:
622
666
 
623
- Or just simple config, you can do it for your business logic, environment variables, etc.
624
-
625
- ```ts
626
- import { resource, run } from "@bluelibs/runner";
667
+ ```typescript
668
+ // Set up log printing (they don't print by default)
669
+ const setupLogging = task({
670
+ id: "app.logging.setup",
671
+ on: globals.resources.logger.events.afterInit,
672
+ run: async (event) => {
673
+ const logger = event.data.value;
627
674
 
628
- // we keep it as const because we will also benefit of type-safety
629
- const businessData = {
630
- pricePerSubscription: 9.99,
631
- };
675
+ // Print info level and above (info, warn, error, critical)
676
+ logger.setPrintThreshold("info");
632
677
 
633
- const businessConfig = resource({
634
- id: "app.config",
635
- async init() {
636
- return businessData;
637
- },
638
- });
678
+ // Print only errors and critical issues
679
+ logger.setPrintThreshold("error");
639
680
 
640
- const app = resource({
641
- id: "app",
642
- register: [businessConfig],
643
- dependencies: { businessConfig },
644
- async init(_, deps) {
645
- console.log(deps.businessConfig.pricePerSubscription);
681
+ // Disable auto-printing entirely
682
+ logger.setPrintThreshold(null);
646
683
  },
647
684
  });
648
-
649
- run();
650
685
  ```
651
686
 
652
- ## Global Events
687
+ ### Event-Driven Log Handling: Ship Logs Anywhere
653
688
 
654
- You can listen to all events by using the wildcard `*`. However, keep in mind that to avoid infinite recursion, all the events coming from the same source will be ignored.
689
+ Every log generates an event that you can listen to. This is where the real power comes in:
655
690
 
656
- At the same time, if a task is listening to all events such as `beforeRun`, since it's a task, triggering `beforeRun` will lead to infinite recursion, this is why we ignore emitting the same event from the same source.
691
+ ```typescript
692
+ // Ship logs to your favorite log warehouse
693
+ const logShipper = task({
694
+ id: "app.logging.shipper", // or pretty printer, or winston, pino bridge, etc.
695
+ on: globals.events.log,
696
+ run: async (event) => {
697
+ const log = event.data;
698
+
699
+ // Ship critical errors to PagerDuty
700
+ if (log.level === "critical") {
701
+ await pagerDuty.alert({
702
+ message: log.message,
703
+ details: log.data,
704
+ source: log.source,
705
+ });
706
+ }
707
+
708
+ // Ship all errors to error tracking
709
+ if (log.level === "error" || log.level === "critical") {
710
+ await sentry.captureException(log.error || new Error(log.message), {
711
+ tags: { source: log.source },
712
+ extra: log.data,
713
+ level: log.level,
714
+ });
715
+ }
657
716
 
658
- This guide outlines the key global events that can be used throughout your application to hook into resource and task lifecycle moments. These events help monitor initialization, execution, and errors, making your system more resilient and traceable.
717
+ // Ship everything to your log warehouse
718
+ await logWarehouse.ship({
719
+ timestamp: log.timestamp,
720
+ level: log.level,
721
+ message: log.message,
722
+ source: log.source,
723
+ data: log.data,
724
+ context: log.context,
725
+ });
726
+ },
727
+ });
659
728
 
660
- ### Overview
729
+ // Filter logs by source
730
+ const databaseLogHandler = task({
731
+ id: "app.logging.database",
732
+ on: globals.events.log,
733
+ run: async (event) => {
734
+ const log = event.data;
661
735
 
662
- Global events are categorized into:
736
+ // Only handle database-related logs
737
+ if (log.source?.includes("database")) {
738
+ await databaseMonitoring.recordMetric({
739
+ operation: log.data?.operation,
740
+ duration: log.data?.duration,
741
+ level: log.level,
742
+ });
743
+ }
744
+ },
745
+ });
746
+ ```
663
747
 
664
- - **Initialization events**: Before and after a resource or task is initialized.
665
- - **Execution events**: Before and after a task runs.
666
- - **Error events**: Handling errors for both tasks and resources.
748
+ ### Integration with Winston: Best of Both Worlds
667
749
 
668
- ### Events in Tasks
750
+ Want to use Winston as your transport? No problem - integrate it seamlessly:
669
751
 
670
- #### `global.tasks.beforeRun`
752
+ ```typescript
753
+ import winston from "winston";
754
+
755
+ // Create Winston logger, put it in a resource if used from various places.
756
+ const winstonLogger = winston.createLogger({
757
+ level: "info",
758
+ format: winston.format.combine(
759
+ winston.format.timestamp(),
760
+ winston.format.errors({ stack: true }),
761
+ winston.format.json()
762
+ ),
763
+ transports: [
764
+ new winston.transports.File({ filename: "error.log", level: "error" }),
765
+ new winston.transports.File({ filename: "combined.log" }),
766
+ new winston.transports.Console({
767
+ format: winston.format.simple(),
768
+ }),
769
+ ],
770
+ });
671
771
 
672
- This event is triggered just before a task is executed. It allows you to inspect or modify the input to the task.
772
+ // Bridge BlueLibs logs to Winston
773
+ const winstonBridge = task({
774
+ id: "app.logging.winston",
775
+ on: globals.events.log,
776
+ run: async (event) => {
777
+ const log = event.data;
778
+
779
+ // Convert BlueLibs log to Winston format
780
+ const winstonMeta = {
781
+ source: log.source,
782
+ timestamp: log.timestamp,
783
+ data: log.data,
784
+ context: log.context,
785
+ ...(log.error && { error: log.error }),
786
+ };
673
787
 
674
- ##### Example:
788
+ // Map log levels (BlueLibs -> Winston)
789
+ const levelMapping = {
790
+ trace: "silly",
791
+ debug: "debug",
792
+ info: "info",
793
+ warn: "warn",
794
+ error: "error",
795
+ critical: "error", // Winston doesn't have critical, use error
796
+ };
675
797
 
676
- ```ts
677
- task({
678
- id: "logBeforeRun",
679
- on: globalEvents.tasks.beforeRun, // Listening to the beforeRun event
680
- run(event) {
681
- console.log("Task is about to run with input:", event.data.input);
798
+ const winstonLevel = levelMapping[log.level] || "info";
799
+ winstonLogger.log(winstonLevel, log.message, winstonMeta);
682
800
  },
683
801
  });
684
802
  ```
685
803
 
686
- **Use Case**: You can use this event to log input data or modify it before the task execution.
687
-
688
- #### `global.tasks.afterRun`
689
-
690
- This event fires immediately after a task finishes. It provides access to both the task input and the result (output).
804
+ ### Advanced: Custom Log Formatters
691
805
 
692
- ##### Example:
806
+ Want to customize how logs are printed? You can override the print behavior:
693
807
 
694
- ```ts
695
- task({
696
- id: "logAfterRun",
697
- on: globalEvents.tasks.afterRun, // Listening to the afterRun event
698
- run(event) {
808
+ ```typescript
809
+ // Custom logger with JSON output
810
+ class JSONLogger extends Logger {
811
+ print(log: ILog) {
699
812
  console.log(
700
- "Task completed. Input:",
701
- event.data.input,
702
- "Output:",
703
- event.data.output
813
+ JSON.stringify(
814
+ {
815
+ timestamp: log.timestamp.toISOString(),
816
+ level: log.level.toUpperCase(),
817
+ source: log.source,
818
+ message: log.message,
819
+ data: log.data,
820
+ context: log.context,
821
+ error: log.error,
822
+ },
823
+ null,
824
+ 2
825
+ )
704
826
  );
827
+ }
828
+ }
829
+
830
+ // Custom logger resource
831
+ const customLogger = resource({
832
+ id: "app.logger.custom",
833
+ dependencies: { eventManager: globals.resources.eventManager },
834
+ init: async (_, { eventManager }) => {
835
+ return new JSONLogger(eventManager);
705
836
  },
706
837
  });
707
- ```
708
838
 
709
- **Use Case**: Useful for logging or post-processing based on the task's output.
710
-
711
- #### `global.tasks.onError`
839
+ // Or you could simply add it as "globals.resources.logger" and override the default logger
840
+ ```
712
841
 
713
- If an error occurs during the execution of a task, this event is triggered. You can log or suppress the error.
842
+ ### Log Structure: What You Get
714
843
 
715
- ##### Example:
844
+ Every log event contains:
716
845
 
717
- ```ts
718
- task({
719
- id: "handleTaskError",
720
- on: globalEvents.tasks.onError, // Listening to the onError event
721
- run(event) {
722
- console.error("Error occurred:", event.data.error);
723
- event.data.suppress(); // Optionally suppress the error to prevent propagation
724
- },
725
- });
846
+ ```typescript
847
+ interface ILog {
848
+ level: string; // The log level (trace, debug, info, etc.)
849
+ source?: string; // Where the log came from
850
+ message: any; // The main log message (can be object or string)
851
+ timestamp: Date; // When the log was created
852
+ error?: {
853
+ // Structured error information
854
+ name: string;
855
+ message: string;
856
+ stack?: string;
857
+ };
858
+ data?: Record<string, any>; // Additional structured data, it's about the log itself
859
+ context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
860
+ }
726
861
  ```
727
862
 
728
- **Use Case**: Error handling logic for specific tasks. For example, you may want to send alerts when a task fails.
863
+ ### Debugging Tips & Best Practices
729
864
 
730
- ### Events in Resources
865
+ #### 1. Use Structured Data Liberally
731
866
 
732
- #### `global.resources.beforeInit`
733
-
734
- This event is triggered before a resource starts its initialization. It allows inspection or modification of the configuration before the resource is fully initialized.
735
-
736
- ##### Example:
867
+ ```typescript
868
+ // Bad - hard to search and filter
869
+ await logger.error(`Failed to process user ${userId} order ${orderId}`);
737
870
 
738
- ```ts
739
- task({
740
- id: "logBeforeResourceInit",
741
- on: globalEvents.resources.beforeInit, // Listening to beforeInit event for resources
742
- run(event) {
743
- console.log("Initializing resource with config:", event.data.config);
871
+ // Good - searchable and filterable
872
+ await logger.error("Order processing failed", {
873
+ data: {
874
+ userId,
875
+ orderId,
876
+ step: "payment",
877
+ paymentMethod: "credit_card",
744
878
  },
745
879
  });
746
880
  ```
747
881
 
748
- **Use Case**: Logging or validating the resource's configuration before initialization.
882
+ #### 2. Include Context in Errors
749
883
 
750
- #### `global.resources.afterInit`
751
-
752
- This event fires after a resource is initialized, giving access to the initialization result.
884
+ ```typescript
885
+ // Include relevant context with errors
886
+ try {
887
+ await processPayment(order);
888
+ } catch (error) {
889
+ await logger.error("Payment processing failed", {
890
+ error,
891
+ data: {
892
+ orderId: order.id,
893
+ amount: order.total,
894
+ currency: order.currency,
895
+ paymentMethod: order.paymentMethod,
896
+ attemptNumber: order.paymentAttempts,
897
+ },
898
+ });
899
+ }
900
+ ```
753
901
 
754
- ##### Example:
902
+ #### 3. Use Different Log Levels Appropriately
755
903
 
756
- ```ts
757
- task({
758
- id: "logAfterResourceInit",
759
- on: globalEvents.resources.afterInit, // Listening to afterInit event
760
- run(event) {
761
- console.log("Resource initialized with value:", event.data.value);
762
- },
904
+ ```typescript
905
+ // Good level usage
906
+ await logger.debug("Cache hit", { data: { key, ttl: remainingTTL } });
907
+ await logger.info("User logged in", { data: { userId, loginMethod } });
908
+ await logger.warn("Rate limit approaching", {
909
+ data: { current: 95, limit: 100 },
910
+ });
911
+ await logger.error("Database connection failed", {
912
+ error,
913
+ data: { attempt: 3 },
763
914
  });
915
+ await logger.critical("System out of memory", { data: { available: "0MB" } });
764
916
  ```
765
917
 
766
- **Use Case**: Post-processing or logging resource initialization details.
918
+ #### 4. Create Domain-Specific Loggers
767
919
 
768
- #### `global.resources.onError`
920
+ ```typescript
921
+ // Create loggers with domain context
922
+ const paymentLogger = logger.with({ source: "payment.processor" });
923
+ const authLogger = logger.with({ source: "auth.service" });
924
+ const emailLogger = logger.with({ source: "email.service" });
925
+
926
+ // Use throughout your domain
927
+ await paymentLogger.info("Processing payment", { data: paymentData });
928
+ await authLogger.warn("Failed login attempt", { data: { email, ip } });
929
+ ```
769
930
 
770
- If an error occurs during resource initialization, this event is triggered. You can log or handle the error.
931
+ ### Common Pitfalls
771
932
 
772
- ##### Example:
933
+ 1. **Logging sensitive data**: Never log passwords, tokens, or PII
934
+ 2. **Over-logging in hot paths**: Check print thresholds for expensive operations
935
+ 3. **Forgetting error objects**: Always include the original error when logging failures
936
+ 4. **Poor log levels**: Don't use `error` for expected conditions
937
+ 5. **Missing context**: Include relevant identifiers (user ID, request ID, etc.)
773
938
 
774
- ```ts
775
- task({
776
- id: "handleResourceError",
777
- on: globalEvents.resources.onError, // Listening to resource onError event
778
- run(event) {
779
- console.error("Resource initialization error:", event.data.error);
780
- event.data.suppress(); // Optionally suppress the error to prevent propagation
939
+ The Logger system is designed to be fast, flexible, and non-intrusive. Use it liberally - good logging is the difference between debugging hell and debugging heaven.
940
+
941
+ ## Meta: Tagging Your Components
942
+
943
+ Sometimes you want to attach metadata to your tasks and resources for documentation, filtering, or middleware logic:
944
+
945
+ ```typescript
946
+ const apiTask = task({
947
+ id: "app.tasks.api.createUser",
948
+ meta: {
949
+ title: "Create User API",
950
+ description: "Creates a new user account",
951
+ tags: ["api", "user", "public"],
952
+ },
953
+ run: async (userData) => {
954
+ // Business logic
955
+ },
956
+ });
957
+
958
+ // Middleware that only applies to API tasks
959
+ const apiMiddleware = middleware({
960
+ id: "app.middleware.api",
961
+ run: async ({ task, next }) => {
962
+ if (task.meta?.tags?.includes("api")) {
963
+ // Apply API-specific logic
964
+ }
965
+ return next(task.input);
781
966
  },
782
967
  });
783
968
  ```
784
969
 
785
- **Use Case**: Error handling for critical resources, allowing for fallback mechanisms or error logging.
970
+ ## Advanced Usage: When You Need More Power
786
971
 
787
- #### Common Usage Pattern
972
+ ### Overrides: Swapping Components at Runtime
788
973
 
789
- To make use of these events, you will typically define tasks that respond to these global events. These tasks can then be registered in your main application resource to handle events for resources and tasks alike.
974
+ Sometimes you need to replace a component entirely. Maybe you're testing, maybe you're A/B testing, maybe you just changed your mind:
790
975
 
791
- #### Example of registering event-handling tasks:
976
+ ```typescript
977
+ const productionEmailer = resource({
978
+ id: "app.emailer",
979
+ init: async () => new SMTPEmailer(),
980
+ });
981
+
982
+ const testEmailer = resource({
983
+ ...productionEmailer, // Copy everything else
984
+ init: async () => new MockEmailer(), // But use a different implementation
985
+ });
792
986
 
793
- ```ts
794
987
  const app = resource({
795
988
  id: "app",
796
- register: [
797
- logBeforeRun,
798
- logAfterRun,
799
- handleTaskError,
800
- logBeforeResourceInit,
801
- logAfterResourceInit,
802
- handleResourceError,
803
- ],
989
+ register: [productionEmailer],
990
+ overrides: [testEmailer], // This replaces the production version
804
991
  });
805
-
806
- run(app);
807
992
  ```
808
993
 
809
- This structure helps you create a centralized and modular approach to manage events and handle tasks and resource lifecycles in your system.
810
-
811
- ### Available Global Events
994
+ ### Namespacing: Keeping Things Organized
812
995
 
813
- Here’s a summary of all the global events you can listen to:
996
+ As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
814
997
 
815
- - `global.beforeInit`: Triggered before any resource is initialized.
816
- - `global.afterInit`: Triggered after any resource is initialized.
817
- - `global.log`: Used for logging across the system.
818
- - **Task-specific events**:
819
- - `global.tasks.beforeRun`: Fired before a task begins execution.
820
- - `global.tasks.afterRun`: Fired after a task completes.
821
- - `global.tasks.onError`: Fired if a task encounters an error.
822
- - **Resource-specific events**:
823
- - `global.resources.beforeInit`: Fired before a resource is initialized.
824
- - `global.resources.afterInit`: Fired after a resource is initialized.
825
- - `global.resources.onError`: Fired if an error occurs during resource initialization.
998
+ | Type | Format |
999
+ | -------------- | ----------------------------------------- |
1000
+ | Tasks | `{domain}.tasks.{taskName}` |
1001
+ | Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
1002
+ | Resources | `{domain}.resources.{resourceName}` |
1003
+ | Events | `{domain}.events.{eventName}` |
1004
+ | Middleware | `{domain}.middleware.{middlewareName}` |
826
1005
 
827
- This modular event system helps in building more reactive and error-tolerant applications.
1006
+ ```typescript
1007
+ // Helper function for consistency
1008
+ function namespaced(id: string) {
1009
+ return `mycompany.myapp.${id}`;
1010
+ }
828
1011
 
829
- ### Individual Task level
1012
+ const userTask = task({
1013
+ id: namespaced("tasks.user.create"),
1014
+ // ...
1015
+ });
1016
+ ```
830
1017
 
831
- When creating tasks or resources we also create lifecycle events for them stored in `events` property.
1018
+ ### Factory Pattern: For When You Need Instances
832
1019
 
833
- ```ts
834
- import { task, run, event } from "@bluelibs/runner";
1020
+ To keep things dead simple, we avoided poluting the D.I. with this concept. Therefore, we recommend using a resource with a factory function to create instances of your classes:
835
1021
 
836
- // Define the task
837
- const helloWorld = task({
838
- id: "app.helloWorld",
839
- async run() {
840
- // Task logic here
841
- return "Hello World!";
1022
+ ```typescript
1023
+ const myFactory = resource({
1024
+ id: "app.factories.myFactory",
1025
+ init: async (config: { someOption: string }) => {
1026
+ return (input: any) => {
1027
+ return new MyClass(input, config.someOption);
1028
+ };
842
1029
  },
843
1030
  });
844
1031
 
845
- // Define the tasks for beforeRun, afterRun, and onError using the `on` property
846
- const beforeHelloWorldTask = task({
847
- id: "app.helloWorld.beforeRun",
848
- on: helloWorld.events.beforeRun, // Listening to beforeRun event
849
- async run(event) {
850
- const input = event.data.input; // Handle the input before task runs
851
- console.log("Before run:", input);
1032
+ const app = resource({
1033
+ id: "app",
1034
+ register: [myFactory],
1035
+ dependencies: { myFactory },
1036
+ init: async (_, { myFactory }) => {
1037
+ const instance = myFactory({ someOption: "value" });
852
1038
  },
853
1039
  });
1040
+ ```
854
1041
 
855
- const afterHelloWorldTask = task({
856
- id: "app.helloWorld.afterRun",
857
- on: helloWorld.events.afterRun, // Listening to afterRun event
858
- async run(event) {
859
- const output = event.data.output; // Handle the output after task runs
860
- console.log("After run:", output);
861
- },
862
- });
1042
+ ### Internal Services: For When You Need Direct Access
1043
+
1044
+ We expose the internal services for advanced use cases (but try not to use them unless you really need to):
863
1045
 
864
- const helloWorldErrorTask = task({
865
- id: "app.helloWorld.onError",
866
- on: helloWorld.events.onError, // Listening to onError event
867
- async run(event) {
868
- const error = event.data.error; // Handle errors during task execution
869
- console.error("Error:", error);
1046
+ ```typescript
1047
+ import { globals } from "@bluelibs/runner";
1048
+
1049
+ const advancedTask = task({
1050
+ id: "app.advanced",
1051
+ dependencies: {
1052
+ store: globals.resources.store,
1053
+ taskRunner: globals.resources.taskRunner,
1054
+ eventManager: globals.resources.eventManager,
1055
+ },
1056
+ run: async (_, { store, taskRunner, eventManager }) => {
1057
+ // Direct access to the framework internals
1058
+ // (Use with caution!)
870
1059
  },
871
1060
  });
1061
+ ```
872
1062
 
873
- // Register all tasks to the app
874
- const app = resource({
875
- id: "app",
876
- register: [
877
- helloWorld,
878
- beforeHelloWorldTask,
879
- afterHelloWorldTask,
880
- helloWorldErrorTask,
881
- ],
1063
+ ## Real-World Example: The Complete Package
1064
+
1065
+ Here's a more realistic application structure that shows everything working together:
1066
+
1067
+ ```typescript
1068
+ import {
1069
+ resource,
1070
+ task,
1071
+ event,
1072
+ middleware,
1073
+ index,
1074
+ run,
1075
+ createContext,
1076
+ } from "@bluelibs/runner";
1077
+
1078
+ // Configuration
1079
+ const config = resource({
1080
+ id: "app.config",
1081
+ init: async () => ({
1082
+ port: parseInt(process.env.PORT || "3000"),
1083
+ databaseUrl: process.env.DATABASE_URL!,
1084
+ jwtSecret: process.env.JWT_SECRET!,
1085
+ }),
882
1086
  });
883
1087
 
884
- // Run the app
885
- run(app);
886
- ```
1088
+ // Database
1089
+ const database = resource({
1090
+ id: "app.database",
1091
+ dependencies: { config },
1092
+ init: async (_, { config }) => {
1093
+ const client = new MongoClient(config.databaseUrl);
1094
+ await client.connect();
1095
+ return client;
1096
+ },
1097
+ dispose: async (client) => await client.close(),
1098
+ });
887
1099
 
888
- ### Resource level
1100
+ // Context for request data
1101
+ const RequestContext = createContext<{ userId?: string; role?: string }>(
1102
+ "app.requestContext"
1103
+ );
889
1104
 
890
- ```ts
891
- import { task, run, event } from "@bluelibs/runner";
1105
+ // Events
1106
+ const userRegistered = event<{ userId: string; email: string }>({
1107
+ id: "app.events.userRegistered",
1108
+ });
892
1109
 
893
- // Define the resource
894
- const businessConfig = resource({
895
- id: "app.businessConfig",
896
- async init(config) {
897
- // Business logic to initialize config
898
- return { value: "Business Configuration Loaded" };
1110
+ // Middleware
1111
+ const authMiddleware = middleware<{ requiredRole?: string }>({
1112
+ id: "app.middleware.auth",
1113
+ run: async ({ task, next }, deps, config) => {
1114
+ const context = RequestContext.use();
1115
+ if (config?.requiredRole && context.role !== config.requiredRole) {
1116
+ throw new Error("Insufficient permissions");
1117
+ }
1118
+ return next(task.input);
899
1119
  },
900
1120
  });
901
1121
 
902
- // Define tasks for handling events beforeInit, afterInit, and onError
1122
+ // Services
1123
+ const userService = resource({
1124
+ id: "app.services.user",
1125
+ dependencies: { database },
1126
+ init: async (_, { database }) => ({
1127
+ async createUser(userData: { name: string; email: string }) {
1128
+ const users = database.collection("users");
1129
+ const result = await users.insertOne(userData);
1130
+ return { id: result.insertedId.toString(), ...userData };
1131
+ },
1132
+ }),
1133
+ });
903
1134
 
904
- const beforeInitTask = task({
905
- id: "app.businessConfig.beforeInit",
906
- on: businessConfig.events.beforeInit, // Listening to beforeInit event
907
- async run(event) {
908
- const config = event.data.config; // Handle the config input before resource initialization
909
- console.log("Before init:", config);
1135
+ // Business Logic
1136
+ const registerUser = task({
1137
+ id: "app.tasks.registerUser",
1138
+ dependencies: { userService, userRegistered },
1139
+ run: async (userData, { userService, userRegistered }) => {
1140
+ const user = await userService.createUser(userData);
1141
+ await userRegistered({ userId: user.id, email: user.email });
1142
+ return user;
910
1143
  },
911
1144
  });
912
1145
 
913
- const afterInitTask = task({
914
- id: "app.businessConfig.afterInit",
915
- on: businessConfig.events.afterInit, // Listening to afterInit event
916
- async run(event) {
917
- const value = event.data.value; // Handle the return value after resource initialization
918
- console.log("After init:", value);
1146
+ const adminOnlyTask = task({
1147
+ id: "app.tasks.adminOnly",
1148
+ middleware: [authMiddleware.with({ requiredRole: "admin" })],
1149
+ run: async () => {
1150
+ return "Top secret admin data";
919
1151
  },
920
1152
  });
921
1153
 
922
- const businessConfigErrorTask = task({
923
- id: "app.businessConfig.onError",
924
- on: businessConfig.events.onError, // Listening to onError event
925
- async run(event) {
926
- const error = event.data.error; // Handle errors during resource initialization
927
- console.error("Error during initialization:", error);
1154
+ // Event Handlers
1155
+ const sendWelcomeEmail = task({
1156
+ id: "app.tasks.sendWelcomeEmail",
1157
+ on: userRegistered,
1158
+ run: async (event) => {
1159
+ console.log(`Sending welcome email to ${event.data.email}`);
1160
+ // Email sending logic here
928
1161
  },
929
1162
  });
930
1163
 
931
- // Register all tasks and the businessConfig resource to the app
932
- const app = resource({
933
- id: "app",
934
- register: [
935
- businessConfig,
936
- beforeInitTask,
937
- afterInitTask,
938
- businessConfigErrorTask,
939
- ],
1164
+ // Group everything together
1165
+ const services = index({
1166
+ userService,
1167
+ registerUser,
1168
+ adminOnlyTask,
940
1169
  });
941
1170
 
942
- // Run the app
943
- run(app);
944
- ```
945
-
946
- ## Advanced Usage
1171
+ // Express server
1172
+ const server = resource({
1173
+ id: "app.server",
1174
+ register: [config, database, services, sendWelcomeEmail],
1175
+ dependencies: { config, services },
1176
+ init: async (_, { config, services }) => {
1177
+ const app = express();
1178
+ app.use(express.json());
1179
+
1180
+ // Middleware to set up request context
1181
+ app.use((req, res, next) => {
1182
+ RequestContext.provide(
1183
+ { userId: req.headers["user-id"], role: req.headers["user-role"] },
1184
+ () => next()
1185
+ );
1186
+ });
947
1187
 
948
- This is just a "language" of developing applications. It simplifies dependency injection to the barebones, it forces you to think more functional and use classes less.
1188
+ app.post("/register", async (req, res) => {
1189
+ try {
1190
+ const user = await services.registerUser(req.body);
1191
+ res.json({ success: true, user });
1192
+ } catch (error) {
1193
+ res.status(400).json({ error: error.message });
1194
+ }
1195
+ });
949
1196
 
950
- This doesn't mean you shouldn't use classes, just not for hooking things up together.
1197
+ app.get("/admin", async (req, res) => {
1198
+ try {
1199
+ const data = await services.adminOnlyTask();
1200
+ res.json({ data });
1201
+ } catch (error) {
1202
+ res.status(403).json({ error: error.message });
1203
+ }
1204
+ });
951
1205
 
952
- You can add many services or external things into the runner ecosystem with things like:
1206
+ const server = app.listen(config.port);
1207
+ console.log(`Server running on port ${config.port}`);
1208
+ return server;
1209
+ },
1210
+ dispose: async (server) => server.close(),
1211
+ });
953
1212
 
954
- ```ts
955
- import { task, run, event } from "@bluelibs/runner";
1213
+ // Start the application
1214
+ const { dispose } = await run(server);
956
1215
 
957
- // proxy declaration pattern
958
- const expressResource = resource({
959
- id: "app.helloWorld",
960
- run: async (app: express.Application) => app,
1216
+ // Graceful shutdown
1217
+ process.on("SIGTERM", async () => {
1218
+ console.log("Shutting down gracefully...");
1219
+ await dispose();
1220
+ process.exit(0);
961
1221
  });
1222
+ ```
962
1223
 
963
- const app = resource({
964
- id: "app",
965
- register: [expressResource.with(express())],
966
- dependencies: {
967
- express: expressResource,
968
- },
969
- init: async (_, { express }) => {
970
- express.get("/", (req, res) => {
971
- res.send("Hello World!");
1224
+ ## Testing: Actually Enjoyable
1225
+
1226
+ ### Unit Testing: Mock Everything, Test Everything
1227
+
1228
+ Unit testing is straightforward because everything is explicit:
1229
+
1230
+ ```typescript
1231
+ describe("registerUser task", () => {
1232
+ it("should create a user and emit event", async () => {
1233
+ const mockUserService = {
1234
+ createUser: jest.fn().mockResolvedValue({ id: "123", name: "John" }),
1235
+ };
1236
+ const mockEvent = jest.fn();
1237
+
1238
+ const result = await registerUser.run(
1239
+ { name: "John", email: "john@example.com" },
1240
+ { userService: mockUserService, userRegistered: mockEvent }
1241
+ );
1242
+
1243
+ expect(result.id).toBe("123");
1244
+ expect(mockEvent).toHaveBeenCalledWith({
1245
+ userId: "123",
1246
+ email: "john@example.com",
972
1247
  });
973
- },
1248
+ });
974
1249
  });
975
-
976
- run(app);
977
1250
  ```
978
1251
 
979
- This demonstrates how effortlessly an external service can be encapsulated within the runner ecosystem. This ‘pattern’ of storing objects in this manner is quite unique, as it typically involves configurations with various options, rather than directly using an Express instance like this:
1252
+ ### Integration Testing: The Real Deal
980
1253
 
981
- ```ts
982
- type Config = {
983
- port: number;
984
- };
1254
+ Integration testing with overrides lets you test the whole system with controlled components:
985
1255
 
986
- const expressResource = resource({
987
- id: "app.helloWorld",
988
- init: async (config: Config) => {
989
- const app = express();
990
- app.listen(config.port);
991
- return app;
992
- },
1256
+ ```typescript
1257
+ const testDatabase = resource({
1258
+ id: "app.database",
1259
+ init: async () => new MemoryDatabase(), // In-memory test database
993
1260
  });
994
1261
 
995
- const app = resource({
996
- id: "app",
997
- register: [expressResource.with({ port: 3000 })],
998
- dependencies: {
999
- express: expressResource,
1000
- },
1001
- init: async (_, { express }) => {
1002
- // type is automagically infered.
1003
- express.get("/", (req, res) => {
1004
- res.send("Hello World!");
1005
- });
1006
- },
1262
+ const testApp = resource({
1263
+ id: "test.app",
1264
+ register: [productionApp],
1265
+ overrides: [testDatabase], // Replace real database with test one
1007
1266
  });
1008
1267
 
1009
- run(app);
1268
+ describe("Full application", () => {
1269
+ it("should handle user registration flow", async () => {
1270
+ const { dispose } = await run(testApp);
1271
+
1272
+ // Test your application end-to-end
1273
+
1274
+ await dispose(); // Clean up
1275
+ });
1276
+ });
1010
1277
  ```
1011
1278
 
1012
- ### Inter-communication between resources
1279
+ ## Semaphore
1013
1280
 
1014
- When registering resources with specific configuration, the initialization order usually doesn’t matter. However, there are cases where it becomes crucial. For instance, consider a security service that allows the injection of a custom hashing function to transition from MD5 to SHA-256.
1281
+ Ever had too many database connections competing for resources? Your connection pool under pressure? The `Semaphore` is here to manage concurrent operations like a professional traffic controller.
1015
1282
 
1016
- In such cases, your resource should provide a method for other resources to update it. A straightforward approach is to expose a configuration option that lets you set a custom hasher, like so:
1283
+ Think of it as a VIP rope at an exclusive venue. Only a limited number of operations can proceed at once. The rest wait in an orderly queue like well-behaved async functions.
1017
1284
 
1018
- ```ts
1019
- type SecurityResourceConfig = {
1020
- hasher: (str: string) => string;
1021
- };
1285
+ ### Quick Start
1022
1286
 
1023
- const securityResource = resource({
1024
- id: "app.security",
1025
- async init(config: SecurityResourceConfig) {
1026
- return {
1027
- hash: (input: string) => config.hasher(input),
1028
- };
1029
- },
1030
- });
1287
+ ```typescript
1288
+ import { Semaphore } from "@bluelibs/runner";
1289
+
1290
+ // Create a semaphore that allows max 5 concurrent operations
1291
+ const dbSemaphore = new Semaphore(5);
1292
+
1293
+ // Basic usage - acquire and release manually
1294
+ await dbSemaphore.acquire();
1295
+ try {
1296
+ // Do your database magic here
1297
+ const result = await db.query("SELECT * FROM users");
1298
+ console.log(result);
1299
+ } finally {
1300
+ dbSemaphore.release(); // Critical: always release to prevent bottlenecks
1301
+ }
1302
+ ```
1031
1303
 
1032
- const app = resource({
1033
- id: "app",
1034
- register: [securityResource.with({ hasher: (input) => md5(input) })],
1304
+ ### The Elegant Approach: withPermit()
1305
+
1306
+ Why manage permits manually when you can let the semaphore do the heavy lifting?
1307
+
1308
+ ```typescript
1309
+ // The elegant approach - automatic cleanup guaranteed!
1310
+ const users = await dbSemaphore.withPermit(async () => {
1311
+ return await db.query("SELECT * FROM users WHERE active = true");
1035
1312
  });
1036
1313
  ```
1037
1314
 
1038
- However, other resources might need to modify this dynamically as extensions. This is where events become valuable.
1315
+ ### Timeout Support
1039
1316
 
1040
- ```ts
1041
- import { resource, run, event } from "@bluelibs/runner";
1317
+ Prevent operations from hanging indefinitely with configurable timeouts:
1042
1318
 
1043
- type SecurityOptions = {
1044
- hashFunction: (input: string) => string;
1045
- };
1319
+ ```typescript
1320
+ try {
1321
+ // Wait max 5 seconds, then throw timeout error
1322
+ await dbSemaphore.acquire({ timeout: 5000 });
1323
+ // Your code here
1324
+ } catch (error) {
1325
+ console.log("Operation timed out waiting for permit");
1326
+ }
1046
1327
 
1047
- const securityResource = resource({
1048
- /* Same as above, but create a setHasher method */
1049
- });
1050
- const afterSecurityInitTask = task({
1051
- id: "app.security.afterInit",
1052
- on: securityResource.events.afterInit, // Listening to afterInit event
1053
- async run(event, deps) {
1054
- const { config, value } = event.data;
1055
- const security = value;
1328
+ // Or with withPermit
1329
+ const result = await dbSemaphore.withPermit(
1330
+ async () => await slowDatabaseOperation(),
1331
+ { timeout: 10000 } // 10 second timeout
1332
+ );
1333
+ ```
1056
1334
 
1057
- // Custom hasher implementation
1058
- security.setHasher((input) => {
1059
- // Implement custom hashing logic here
1060
- console.log("Hashing input:", input);
1061
- });
1062
- },
1063
- });
1335
+ ### Cancellation Support
1064
1336
 
1065
- // Register the security resource and the afterInit task in the app
1066
- const app = resource({
1067
- id: "app",
1068
- register: [securityResource, afterSecurityInitTask],
1069
- });
1070
- ```
1337
+ Operations can be cancelled using AbortSignal:
1071
1338
 
1072
- Another approach is to create a new event that contains the configuration, providing the flexibility to update it as needed.
1339
+ ```typescript
1340
+ const controller = new AbortController();
1341
+
1342
+ // Start an operation
1343
+ const operationPromise = dbSemaphore.withPermit(
1344
+ async () => await veryLongOperation(),
1345
+ { signal: controller.signal }
1346
+ );
1347
+
1348
+ // Cancel the operation after 3 seconds
1349
+ setTimeout(() => {
1350
+ controller.abort();
1351
+ }, 3000);
1352
+
1353
+ try {
1354
+ await operationPromise;
1355
+ } catch (error) {
1356
+ console.log("Operation was cancelled");
1357
+ }
1358
+ ```
1073
1359
 
1074
- ```ts
1075
- import { resource, run, event } from "@bluelibs/runner";
1360
+ ### Monitoring: Metrics & Debugging
1076
1361
 
1077
- const securityConfigurationPhaseEvent = event<SecurityOptions>({
1078
- id: "app.security.configurationPhase",
1079
- });
1362
+ Want to know what's happening under the hood?
1080
1363
 
1081
- const securityResource = resource({
1082
- id: "app.security",
1083
- dependencies: {
1084
- securityConfigurationPhaseEvent,
1085
- },
1086
- async init(config: SecurityOptions) {
1087
- // Give the ability to other listeners to modify the configuration
1088
- securityConfigurationPhaseEvent(config);
1089
- Objecte.freeze(config);
1364
+ ```typescript
1365
+ // Get comprehensive metrics
1366
+ const metrics = dbSemaphore.getMetrics();
1367
+ console.log(`
1368
+ Semaphore Status Report:
1369
+ Available permits: ${metrics.availablePermits}/${metrics.maxPermits}
1370
+ Operations waiting: ${metrics.waitingCount}
1371
+ Utilization: ${(metrics.utilization * 100).toFixed(1)}%
1372
+ Disposed: ${metrics.disposed ? "Yes" : "No"}
1373
+ `);
1374
+
1375
+ // Quick checks
1376
+ console.log(`Available permits: ${dbSemaphore.getAvailablePermits()}`);
1377
+ console.log(`Queue length: ${dbSemaphore.getWaitingCount()}`);
1378
+ console.log(`Is disposed: ${dbSemaphore.isDisposed()}`);
1379
+ ```
1090
1380
 
1091
- return {
1092
- // ... based on config
1093
- };
1094
- },
1095
- });
1381
+ ### Resource Cleanup
1096
1382
 
1097
- // Define securityResource and securityConfigurationPhaseEvent as needed
1383
+ Properly dispose of semaphores when finished:
1098
1384
 
1099
- const securityConfigTask = task({
1100
- id: "app.security.config",
1101
- on: securityConfigurationPhaseEvent, // Listening to securityConfigurationPhaseEvent
1102
- async run(event, deps) {
1103
- const { config } = event.data; // config is SecurityOptions
1104
- config.setHasher(newHashFunction); // Apply the new hash function
1105
- },
1106
- });
1385
+ ```typescript
1386
+ // Reject all waiting operations and prevent new ones
1387
+ dbSemaphore.dispose();
1107
1388
 
1108
- // Register the security resource and configuration task in the app
1109
- const app = resource({
1110
- id: "app",
1111
- register: [securityResource, securityConfigTask],
1112
- });
1389
+ // All waiting operations will be rejected with:
1390
+ // Error: "Semaphore has been disposed"
1113
1391
  ```
1114
1392
 
1115
- ### Overrides
1393
+ ## Queue
1116
1394
 
1117
- Previously, we discussed how to extend functionality using events. However, there are times when you need to replace an existing resource with a new one or swap out a task or middleware imported from another package that doesn’t support such changes.
1395
+ _The orderly guardian of chaos, the diplomatic bouncer of async operations._
1118
1396
 
1119
- ```ts
1120
- import { resource, run, event } from "@bluelibs/runner";
1397
+ The `Queue` class is your friendly neighborhood task coordinator. Think of it as a very polite but firm British queue-master who ensures everyone waits their turn, prevents cutting in line, and gracefully handles when it's time to close shop.
1121
1398
 
1122
- // This example is for resources but override works for tasks, events, and middleware as well.
1123
- const securityResource = resource({
1124
- id: "app.security",
1125
- async init() {
1126
- // returns a security service
1127
- },
1128
- });
1399
+ ### **FIFO Ordering**
1129
1400
 
1130
- const override = resource({
1131
- ...securityResource,
1132
- init: async () => {
1133
- // a new and custom service
1134
- },
1135
- });
1401
+ Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
1136
1402
 
1137
- const app = resource({
1138
- id: "app",
1139
- register: [securityResource], // this resource might be registered by any element in the dependency tree.
1140
- overrides: [override],
1141
- });
1142
- ```
1403
+ ### **Deadlock Detective**
1143
1404
 
1144
- The new securityResource will replace the existing one, ensuring all future references point to the updated version.
1405
+ Using the clever `AsyncLocalStorage`, our Queue can detect when a task tries to queue another task (the async equivalent of "yo dawg, I heard you like queues..."). When caught red-handed, it politely but firmly rejects with a deadlock error.
1145
1406
 
1146
- Overrides work if the resource being overridden is already registered. If multiple resources attempt to override the same one, no error will be thrown. This is a common scenario, where the root resource typically contains the most authoritative overrides. But it's also to be mindful about.
1407
+ ### **Graceful Disposal & Cancellation**
1147
1408
 
1148
- ## Logging
1409
+ The Queue provides cooperative cancellation through the Web Standard `AbortController`:
1149
1410
 
1150
- We expose through globals a `logger` that you can use to log things. Essentially what this service does it emits a `global.events.log` event with an `ILog` object.
1411
+ - **Patient mode** (default): Waits for all queued tasks to complete naturally
1412
+ - **Cancel mode**: Signals running tasks to abort via `AbortSignal`, enabling early termination
1151
1413
 
1152
- ```ts
1153
- import { task, run, event, globals } from "@bluelibs/runner";
1414
+ ### Basic Example
1154
1415
 
1155
- const helloWorld = task({
1156
- id: "app.helloWorld",
1157
- dependencies: {
1158
- logger: globals.resources.logger,
1159
- },
1160
- run: async (_, { logger }) => {
1161
- await logger.info("Hello World!");
1162
- // or logger.log(level, data);
1163
- },
1416
+ ```typescript
1417
+ import { Queue } from "@bluelibs/runner";
1418
+
1419
+ const queue = new Queue();
1420
+
1421
+ // Queue up some work
1422
+ const result = await queue.run(async (signal) => {
1423
+ // Your async task here
1424
+ return "Task completed";
1164
1425
  });
1426
+
1427
+ // Graceful shutdown
1428
+ await queue.dispose();
1165
1429
  ```
1166
1430
 
1167
- ### Logs Summary Table
1431
+ ### AbortController Integration
1168
1432
 
1169
- | Log Level | Description | Usage Example |
1170
- | ------------ | ----------------------------------------- | -------------------------------------- |
1171
- | **trace** | Very detailed logs, usually for debugging | "Entering function X with params Y." |
1172
- | **debug** | Detailed debug information | "Fetching user data: userId=123." |
1173
- | **info** | General application information | "Service started on port 8080." |
1174
- | **warn** | Indicates a potential issue | "Disk space running low." |
1175
- | **error** | Indicates a significant problem | "Unable to connect to database." |
1176
- | **critical** | Serious problem causing a crash | "System out of memory, shutting down." |
1433
+ The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
1177
1434
 
1178
- ### Print logs
1435
+ #### Example: Long-running Task with Cancellation Support
1179
1436
 
1180
- Logs don't get printed by default. You have to set the print threshold to a certain level. This is useful when you want to print only errors and critical logs in production, but you want to print all logs in development. Your codebase, your rules.
1437
+ ```typescript
1438
+ const queue = new Queue();
1181
1439
 
1182
- To showcase the versatility of the system, here are some ways you could do it:
1440
+ // Task that respects cancellation
1441
+ const processLargeDataset = queue.run(async (signal) => {
1442
+ const items = await fetchLargeDataset();
1183
1443
 
1184
- ```ts
1185
- import { task, run, event, globals, resource } from "@bluelibs/runner";
1444
+ for (const item of items) {
1445
+ // Check for cancellation before processing each item
1446
+ if (signal.aborted) {
1447
+ throw new Error("Operation was cancelled");
1448
+ }
1186
1449
 
1187
- const { logger } = globals.resources;
1450
+ await processItem(item);
1451
+ }
1188
1452
 
1189
- const printLog = task({
1190
- id: "app.task.updatePrintThreshold",
1191
- on: logger.events.afterInit,
1192
- // Note: logger is
1193
- run: async (event, deps) => {
1194
- const logger = event.data.value;
1195
- logger.setPrintThreshold("trace"); // will print all logs
1196
- logger.setPrintThreshold("error"); // will print only "error" and "critical" logs
1197
- },
1453
+ return "Dataset processed successfully";
1198
1454
  });
1199
1455
 
1200
- const app = resource({
1201
- id: "root",
1202
- register: [printLog],
1456
+ // Cancel all running tasks
1457
+ await queue.dispose({ cancel: true });
1458
+ ```
1459
+
1460
+ #### Example: Network Request with Timeout
1461
+
1462
+ ```typescript
1463
+ const queue = new Queue();
1464
+
1465
+ const fetchWithCancellation = queue.run(async (signal) => {
1466
+ try {
1467
+ // Pass the signal to fetch for automatic cancellation
1468
+ const response = await fetch("https://api.example.com/data", { signal });
1469
+ return await response.json();
1470
+ } catch (error) {
1471
+ if (error.name === "AbortError") {
1472
+ console.log("Request was cancelled");
1473
+ throw error;
1474
+ }
1475
+ throw error;
1476
+ }
1203
1477
  });
1204
1478
 
1205
- // Now your app will print all logs
1479
+ // This will cancel the fetch request if still pending
1480
+ await queue.dispose({ cancel: true });
1206
1481
  ```
1207
1482
 
1208
- The logger’s log() function is asynchronous because it handles events. If you want to prevent your system from waiting for log operations to complete, simply omit the await when calling log(). This is useful if you have listeners that send logs to external log storage systems.
1483
+ #### Example: File Processing with Progress Tracking
1209
1484
 
1210
- Additionally, there is a `global.events.log` event available. You can use this event both to emit log messages and to listen for all log activities.
1485
+ ```typescript
1486
+ const queue = new Queue();
1211
1487
 
1212
- ```ts
1213
- import { task, run, event, globals } from "@bluelibs/runner";
1488
+ const processFiles = queue.run(async (signal) => {
1489
+ const files = await getFileList();
1490
+ const results = [];
1214
1491
 
1215
- const { logger } = globals.resources;
1492
+ for (let i = 0; i < files.length; i++) {
1493
+ // Respect cancellation
1494
+ signal.throwIfAborted();
1216
1495
 
1217
- const shipLogsToWarehouse = task({
1218
- id: "app.task.shipLogsToWarehouse",
1219
- on: logger.events.log,
1220
- dependencies: {
1221
- warehouseService: warehouseServiceResource,
1222
- },
1223
- run: async (event, deps) => {
1224
- const log = event.data; // ILog
1225
- if (log.level === "error" || log.level === "critical") {
1226
- // Ensure no extra log() calls are made here to prevent infinite loops
1227
- await deps.warehouseService.push(log);
1228
- }
1229
- },
1496
+ const result = await processFile(files[i]);
1497
+ results.push(result);
1498
+
1499
+ // Optional: Report progress
1500
+ console.log(`Processed ${i + 1}/${files.length} files`);
1501
+ }
1502
+
1503
+ return results;
1230
1504
  });
1231
1505
  ```
1232
1506
 
1233
- And yes, this would also work:
1507
+ ## The Magic Behind the Curtain
1234
1508
 
1235
- ```ts
1236
- const task = task({
1237
- id: "app.task.logSomething",
1238
- dependencies: {
1239
- log: globals.events.log,
1240
- },
1241
- run: async (_, { log }) => {
1242
- await log({
1243
- level: "info",
1244
- data: { anything: "you want" };
1245
- timestamp: new Date();
1246
- context: "app.task.logSomething"; // optional
1247
- })
1248
- },
1249
- });
1509
+ ### Internal State
1510
+
1511
+ - `tail`: The promise chain that maintains FIFO execution order
1512
+ - `disposed`: Boolean flag indicating whether the queue accepts new tasks
1513
+ - `abortController`: Centralized cancellation controller that provides `AbortSignal` to all tasks
1514
+ - `executionContext`: AsyncLocalStorage-based deadlock detection mechanism
1515
+
1516
+ ### Error Handling
1517
+
1518
+ - **"Queue has been disposed"**: You tried to add work after closing time
1519
+ - **"Dead-lock detected"**: A task tried to queue another task (infinite recursion prevention)
1520
+
1521
+ ### Best Practices
1522
+
1523
+ #### 1. Always Dispose Resources
1524
+
1525
+ ```typescript
1526
+ const queue = new Queue();
1527
+ try {
1528
+ await queue.run(task);
1529
+ } finally {
1530
+ await queue.dispose();
1531
+ }
1250
1532
  ```
1251
1533
 
1252
- Fair Warning: If you plan to use the global.events.log event, ensure you avoid creating a circular dependency. This event is emitted by the logger itself. Additionally, some logs are sent before all resources are fully initialized. Therefore, it’s important to carefully review and verify your dependencies to prevent potential issues.
1534
+ #### 2. Implement Cooperative Cancellation
1253
1535
 
1254
- ## Testing
1536
+ Tasks should regularly check the `AbortSignal` and respond appropriately:
1255
1537
 
1256
- Oh yes, testing is a breeze with this system. You can easily test your tasks, resources, and middleware by running them in a test environment. It's designed to be tested.
1538
+ ```typescript
1539
+ // Preferred: Use signal.throwIfAborted() for immediate termination
1540
+ signal.throwIfAborted();
1257
1541
 
1258
- ### Unit Testing
1542
+ // Alternative: Check signal.aborted for custom handling
1543
+ if (signal.aborted) {
1544
+ cleanup();
1545
+ throw new Error("Operation cancelled");
1546
+ }
1547
+ ```
1259
1548
 
1260
- You can easily test your middleware, resources and tasks by running them in a test environment.
1549
+ #### 3. Integrate with Native APIs
1261
1550
 
1262
- The only components you need to test are the run function and the init functions, along with their proper dependencies.
1551
+ Many Web APIs accept `AbortSignal`:
1263
1552
 
1264
- ```ts
1265
- import { task, resource } from "@bluelibs/runner";
1553
+ - `fetch(url, { signal })`
1554
+ - `setTimeout(callback, delay, { signal })`
1555
+ - Custom async operations
1266
1556
 
1267
- const helloWorld = task({
1268
- id: "app.helloWorld",
1269
- run: async () => {
1270
- return "Hello World!";
1271
- },
1272
- });
1557
+ ### 4. Avoid Nested Queuing
1273
1558
 
1274
- const helloWorldResource = resource({
1275
- id: "app.helloWorldResource",
1276
- init: async () => {
1277
- return "Hello World!";
1278
- },
1279
- });
1559
+ The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
1280
1560
 
1281
- // sample tests for the task
1282
- describe("app.helloWorld", () => {
1283
- it("should return Hello World!", async () => {
1284
- const result = await helloWorld.run(input, dependencies); // pass in the arguments and the mocked dependencies.
1285
- expect(result).toBe("Hello World!");
1286
- });
1287
- });
1561
+ ### 5. Handle AbortError Gracefully
1288
1562
 
1289
- // sample tests for the resource
1290
- describe("app.helloWorldResource", () => {
1291
- it("should return Hello World!", async () => {
1292
- const result = await helloWorldResource.init(config, dependencies); // pass in the arguments and the mocked dependencies.
1293
- expect(result).toBe("Hello World!");
1294
- });
1295
- });
1563
+ ```typescript
1564
+ try {
1565
+ await queue.run(task);
1566
+ } catch (error) {
1567
+ if (error.name === "AbortError") {
1568
+ // Expected cancellation, handle appropriately
1569
+ return;
1570
+ }
1571
+ throw error; // Re-throw unexpected errors
1572
+ }
1296
1573
  ```
1297
1574
 
1298
- ### Integration
1575
+ ---
1299
1576
 
1300
- Unit testing becomes straightforward with mocks, as all dependencies are explicitly defined. However, if you wish to run an integration test, you can have a task tested within the full container environment.
1577
+ _Cooperative task scheduling with professional-grade cancellation support_
1301
1578
 
1302
- ```ts
1303
- import { task, resource, run, global } from "@bluelibs/runner";
1579
+ ## Real-World Examples
1304
1580
 
1305
- const task = task({
1306
- id: "app.myTask",
1307
- run: async () => {
1308
- return "Hello World!";
1309
- },
1310
- });
1581
+ ### Database Connection Pool Manager
1311
1582
 
1312
- const app = resource({
1313
- id: "app",
1314
- register: [myTask],
1315
- });
1583
+ ```typescript
1584
+ class DatabaseManager {
1585
+ private semaphore = new Semaphore(10); // Max 10 concurrent queries
1586
+
1587
+ async query(sql: string, params?: any[]) {
1588
+ return this.semaphore.withPermit(
1589
+ async () => {
1590
+ const connection = await this.pool.getConnection();
1591
+ try {
1592
+ return await connection.query(sql, params);
1593
+ } finally {
1594
+ connection.release();
1595
+ }
1596
+ },
1597
+ { timeout: 30000 } // 30 second timeout
1598
+ );
1599
+ }
1600
+
1601
+ async shutdown() {
1602
+ this.semaphore.dispose();
1603
+ await this.pool.close();
1604
+ }
1605
+ }
1316
1606
  ```
1317
1607
 
1318
- Then your tests can now be cleaner, as you can use `overrides` and a wrapper resource to mock your task.
1319
-
1320
- ```ts
1321
- describe("app", () => {
1322
- it("an example to override a task or resource", async () => {
1323
- const testApp = resource({
1324
- id: "app.test",
1325
- register: [myApp], // wrap your existing app
1326
- overrides: [override], // apply the overrides for "app.myTask"
1327
- init: async (_, deps) => {
1328
- // you can now test a task simply by depending on it, and running it, then asserting the response of run()
1608
+ ### Rate-Limited API Client
1609
+
1610
+ ```typescript
1611
+ class APIClient {
1612
+ private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
1613
+
1614
+ async fetchUser(id: string, signal?: AbortSignal) {
1615
+ return this.rateLimiter.withPermit(
1616
+ async () => {
1617
+ const response = await fetch(`/api/users/${id}`, { signal });
1618
+ return response.json();
1329
1619
  },
1620
+ { signal, timeout: 10000 }
1621
+ );
1622
+ }
1623
+ }
1624
+ ```
1625
+
1626
+ ### Batch Processing with Progress
1627
+
1628
+ ```typescript
1629
+ async function processBatch(items: any[]) {
1630
+ const semaphore = new Semaphore(3); // Max 3 concurrent items
1631
+ const results = [];
1632
+
1633
+ console.log("Starting batch processing...");
1634
+
1635
+ for (const [index, item] of items.entries()) {
1636
+ const result = await semaphore.withPermit(async () => {
1637
+ console.log(`Processing item ${index + 1}/${items.length}`);
1638
+ return await processItem(item);
1330
1639
  });
1331
1640
 
1332
- await run(testApp);
1333
- });
1334
- });
1641
+ results.push(result);
1642
+
1643
+ // Show progress
1644
+ const metrics = semaphore.getMetrics();
1645
+ console.log(
1646
+ `Active: ${metrics.maxPermits - metrics.availablePermits}, Waiting: ${
1647
+ metrics.waitingCount
1648
+ }`
1649
+ );
1650
+ }
1651
+
1652
+ semaphore.dispose();
1653
+ console.log("Batch processing complete!");
1654
+ return results;
1655
+ }
1335
1656
  ```
1336
1657
 
1337
- ## Support
1658
+ ## Best Practices
1659
+
1660
+ 1. **Always dispose**: Clean up your semaphores when finished to prevent memory leaks
1661
+ 2. **Use withPermit()**: It's cleaner and prevents resource leaks
1662
+ 3. **Set timeouts**: Don't let operations hang forever
1663
+ 4. **Monitor metrics**: Keep an eye on utilization to tune your permit count
1664
+ 5. **Handle errors**: Timeouts and cancellations throw errors - catch them!
1665
+
1666
+ ## Common Pitfalls
1667
+
1668
+ - **Forgetting to release**: Manual acquire/release is error-prone - prefer `withPermit()`
1669
+ - **No timeout**: Operations can hang forever without timeouts
1670
+ - **Ignoring disposal**: Always dispose semaphores to prevent memory leaks
1671
+ - **Wrong permit count**: Too few = slow, too many = defeats the purpose
1672
+
1673
+ ## Why Choose BlueLibs Runner?
1674
+
1675
+ ### What You Get
1676
+
1677
+ - **Type Safety**: Full TypeScript support with intelligent inference
1678
+ - **Testability**: Everything is mockable and testable by design
1679
+ - **Flexibility**: Compose your app however you want
1680
+ - **Performance**: Built-in caching and optimization
1681
+ - **Clarity**: Explicit dependencies, no hidden magic
1682
+ - **Developer Experience**: Helpful error messages and clear patterns
1683
+
1684
+ ### What You Don't Get
1685
+
1686
+ - Complex configuration files that require a PhD to understand
1687
+ - Decorator hell that makes your code look like a Christmas tree
1688
+ - Hidden dependencies that break when you least expect it
1689
+ - Framework lock-in that makes you feel trapped
1690
+ - Mysterious behavior at runtime that makes you question reality
1691
+
1692
+ ## The Migration Path
1693
+
1694
+ Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
1695
+
1696
+ The beauty of BlueLibs Runner is that you can adopt it incrementally. Start with one task, one resource, and gradually refactor your existing code. No big bang rewrites required - your sanity will thank you.
1697
+
1698
+ ## Community & Support
1699
+
1700
+ This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
1701
+
1702
+ - [GitHub Repository](https://github.com/bluelibs/runner) - ⭐ if you find this useful
1703
+ - [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
1704
+ - [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
1705
+
1706
+ ## The Bottom Line
1707
+
1708
+ BlueLibs Runner is what happens when you take all the good ideas from modern frameworks and leave out the parts that make you want to switch careers. It's TypeScript-first, test-friendly, and actually makes sense when you read it six months later.
1709
+
1710
+ Give it a try. Your future self (and your team) will thank you.
1338
1711
 
1339
- This package is part of the [BlueLibs](https://www.bluelibs.com) family. If you enjoy this work, please show your support by starring [the main repository](https://github.com/bluelibs/runner).
1712
+ _P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's different. (No, really, it is.)_
1340
1713
 
1341
1714
  ## License
1342
1715