@bluelibs/runner 1.2.0 → 1.4.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 (54) hide show
  1. package/README.md +310 -178
  2. package/dist/define.d.ts +2 -2
  3. package/dist/define.js +12 -9
  4. package/dist/define.js.map +1 -1
  5. package/dist/defs.d.ts +45 -12
  6. package/dist/examples/express-mongo/index.d.ts +0 -0
  7. package/dist/examples/express-mongo/index.js +3 -0
  8. package/dist/examples/express-mongo/index.js.map +1 -0
  9. package/dist/globalEvents.d.ts +3 -1
  10. package/dist/globalEvents.js +1 -0
  11. package/dist/globalEvents.js.map +1 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/models/DependencyProcessor.d.ts +4 -2
  14. package/dist/models/DependencyProcessor.js +45 -20
  15. package/dist/models/DependencyProcessor.js.map +1 -1
  16. package/dist/models/EventManager.d.ts +2 -1
  17. package/dist/models/EventManager.js +47 -21
  18. package/dist/models/EventManager.js.map +1 -1
  19. package/dist/models/Logger.d.ts +15 -10
  20. package/dist/models/Logger.js +27 -15
  21. package/dist/models/Logger.js.map +1 -1
  22. package/dist/models/ResourceInitializer.d.ts +4 -1
  23. package/dist/models/ResourceInitializer.js +39 -7
  24. package/dist/models/ResourceInitializer.js.map +1 -1
  25. package/dist/models/Store.d.ts +5 -1
  26. package/dist/models/Store.js +27 -3
  27. package/dist/models/Store.js.map +1 -1
  28. package/dist/models/TaskRunner.d.ts +3 -1
  29. package/dist/models/TaskRunner.js +13 -3
  30. package/dist/models/TaskRunner.js.map +1 -1
  31. package/dist/run.d.ts +0 -8
  32. package/dist/run.js +8 -6
  33. package/dist/run.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/index.ts +2 -0
  36. package/src/__tests__/models/EventManager.test.ts +338 -67
  37. package/src/__tests__/models/Logger.test.ts +44 -1
  38. package/src/__tests__/models/ResourceInitializer.test.ts +6 -4
  39. package/src/__tests__/models/Store.test.ts +4 -2
  40. package/src/__tests__/models/TaskRunner.test.ts +9 -4
  41. package/src/__tests__/run.middleware.test.ts +249 -0
  42. package/src/__tests__/run.test.ts +112 -131
  43. package/src/__tests__/typesafety.test.ts +127 -0
  44. package/src/define.ts +15 -11
  45. package/src/defs.ts +63 -21
  46. package/src/examples/express-mongo/index.ts +1 -0
  47. package/src/globalEvents.ts +5 -2
  48. package/src/models/DependencyProcessor.ts +77 -33
  49. package/src/models/EventManager.ts +55 -25
  50. package/src/models/Logger.ts +36 -16
  51. package/src/models/ResourceInitializer.ts +54 -8
  52. package/src/models/Store.ts +34 -3
  53. package/src/models/TaskRunner.ts +13 -3
  54. package/src/run.ts +16 -13
package/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
  <a href="https://bluelibs.github.io/runner/" target="_blank"><img src="https://img.shields.io/badge/read-typedocs-blue" alt="Docs" /></a>
7
7
  </p>
8
8
 
9
- These are the building blocks to create amazing applications. It's a more functional approach to building small and large-scale applications.
9
+ 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.
10
10
 
11
- These are the building blocks:
11
+ ## Building Blocks
12
12
 
13
13
  - **Tasks**: Core units of logic that encapsulate specific tasks. They can depend on resources, other tasks, and event emitters.
14
14
  - **Resources**: Singleton objects providing shared functionality. They can be constants, services, functions. They can depend on other resources, tasks, and event emitters.
15
15
  - **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.
16
- - **Middleware**: Intercept and modify the execution of tasks. They can be used to add additional functionality to your tasks. Middleware can be global or task-specific.
16
+ - **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.
17
17
 
18
18
  These are the concepts and philosophy:
19
19
 
@@ -23,11 +23,11 @@ These are the concepts and philosophy:
23
23
  - **Explicit Registration**: All tasks, resources, events, and middleware have to be explicitly registered to be used.
24
24
  - **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.
25
25
 
26
- Resources return through `async init()` their value to the container which can be used throughout the application. Resources might not have a value, they can just register things, like tasks, events, or middleware.
26
+ Resources return their value to the container using the async `init()` function, making them available throughout the application.
27
27
 
28
- Tasks return through `async run()` function and the value from run, can be used throughout the application.
28
+ Tasks provide their output through the async `run()` function, allowing the results to be used across the application.
29
29
 
30
- All tasks, resources, events, and middleware have to be explicitly registered to be used. Registration can only be done in resources.
30
+ All tasks, resources, events, and middleware must be explicitly registered to be used. Registration can only be done within resources.
31
31
 
32
32
  ## Installation
33
33
 
@@ -38,7 +38,7 @@ npm install @bluelibs/runner
38
38
  ## Basic Usage
39
39
 
40
40
  ```typescript
41
- import { task, run, resource } from "@bluelibs/runner";
41
+ import { run, resource } from "@bluelibs/runner";
42
42
 
43
43
  const minimal = resource({
44
44
  async init() {
@@ -53,16 +53,16 @@ run(minimal).then((result) => {
53
53
 
54
54
  ## Resources and Tasks
55
55
 
56
- Resources are singletons. They can be constants, services, functions, etc. They can depend on other resources, tasks, and event emitters.
56
+ Resources are singletons and can include constants, services, functions, and more. They can depend on other resources, tasks, and event emitters.
57
57
 
58
- On the other hand, tasks are designed to be trackable units of logic. Like things that handle a specific route on your HTTP server, or any kind of action that is needed from various places. This will allow you to easily track what is happening in your application.
58
+ 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.
59
59
 
60
60
  ```ts
61
61
  import { task, run, resource } from "@bluelibs/runner";
62
62
 
63
63
  const helloTask = task({
64
64
  id: "app.hello",
65
- run: async () => console.log("Hello World!"),
65
+ run: async () => "Hello World!",
66
66
  });
67
67
 
68
68
  const app = resource({
@@ -72,24 +72,26 @@ const app = resource({
72
72
  hello: helloTask,
73
73
  },
74
74
  async init(_, deps) {
75
- await deps.hello();
75
+ return await deps.hello();
76
76
  },
77
77
  });
78
+
79
+ const result = await run(app); // "Hello World!"
78
80
  ```
79
81
 
80
82
  ### When to use each?
81
83
 
82
- 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 an action, for example:
84
+ 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:
83
85
 
84
86
  - "app.user.register" - this is a task, registers the user, returns a token
85
- - "app.user.createComment" - this is a task, creates a comment, returns the comment
87
+ - "app.user.createComment" - this is a task, creates a comment, returns the comment maybe
86
88
  - "app.user.updateFriendList" - this task can be re-used from many other tasks or resources as necessary
87
89
 
88
90
  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.
89
91
 
90
92
  ### Resource dispose()
91
93
 
92
- Resources can have a `dispose()` method that can be used to clean up resources. This is useful for cleaning up resources like closing database connections, etc. You typically want to use this when you have opened pending connections or you need to do some cleanup or a graceful shutdown.
94
+ 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.
93
95
 
94
96
  ```ts
95
97
  import { task, run, resource } from "@bluelibs/runner";
@@ -99,13 +101,14 @@ const dbResource = resource({
99
101
  const db = await connectToDatabase();
100
102
  return db;
101
103
  },
104
+ // the value returned from init() will be passed to dispose()
102
105
  async dispose(db, config, deps) {
103
106
  return db.close();
104
107
  },
105
108
  });
106
109
  ```
107
110
 
108
- If you want to call dispose, you have to do it through the global store.
111
+ 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.
109
112
 
110
113
  ```ts
111
114
  import { task, run, resource, globals } from "@bluelibs/runner";
@@ -117,6 +120,7 @@ const app = resource({
117
120
  store: globals.resources.store,
118
121
  },
119
122
  async init(_, deps) {
123
+ // We use the fact that we can reuse the value we got from here
120
124
  return {
121
125
  dispose: async () => deps.store.dispose(),
122
126
  };
@@ -128,15 +132,45 @@ const value = await run(app);
128
132
  await value.dispose();
129
133
  ```
130
134
 
131
- ## Encapsulation
135
+ ### Resource configuration
132
136
 
133
- We want to make sure that our tasks are not dependent on the outside world. This is why we have the `dependencies` object.
137
+ 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.
134
138
 
135
- You cannot call on an task outside from dependencies. And not only that, it has to be explicitly registered to the container.
139
+ ```ts
140
+ import { task, run, resource } from "@bluelibs/runner";
141
+
142
+ type Config = { smtpUrl: string; defaultFrom: string };
143
+
144
+ const emailerResource = resource({
145
+ // automatic type inference.
146
+ async init(config: Config) {
147
+ // todo: perform config checks with a library like zod
148
+ return {
149
+ sendEmail: async (to: string, subject: string, body: string) => {
150
+ // send *email*
151
+ },
152
+ };
153
+ },
154
+ });
155
+
156
+ const app = resource({
157
+ id: "app",
158
+ register: [
159
+ // proper autocompletion is present
160
+ emailerResource.with({ smtpUrl: "smtp://localhost", defaultFrom: "" }),
161
+ ],
162
+ });
163
+ ```
164
+
165
+ If by any chance your main `app` has configs then they will be passed via the second argument of `run`, like this:
166
+
167
+ ```ts
168
+ run(app, config);
169
+ ```
136
170
 
137
171
  ## Dependencies
138
172
 
139
- You can depend on `tasks`, `resources`, `events` and `middleware`.
173
+ You can depend on `tasks`, `resources`, `events` and (indirectly) on `middleware`.
140
174
 
141
175
  ```ts
142
176
  import { task, resource, run, event } from "@bluelibs/runner";
@@ -146,11 +180,16 @@ const helloWorld = task({
146
180
  dependencies: {
147
181
  userRegisteredEvent,
148
182
  },
183
+ async run(_, deps) {
184
+ await deps.userRegisteredEvent();
185
+ return "Hello World!";
186
+ },
149
187
  });
150
188
 
151
189
  const app = resource({
152
190
  id: "app",
153
- register: [helloWorld],
191
+ // You have to register everything you use.
192
+ register: [helloWorld, logMiddleware],
154
193
  dependencies: {
155
194
  helloWorld,
156
195
  },
@@ -162,9 +201,18 @@ const app = resource({
162
201
  run(app);
163
202
  ```
164
203
 
165
- Resources can also depend on other resources and tasks. We have a circular dependency checker which ensures consistency. If a circular dependency is detected, an error will be thrown showing you the exact pathways.
204
+ 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.
205
+
206
+ Tasks, however, are not bound by this restriction; they can freely depend on each other as needed.
207
+
208
+ The dependencies get injected as follows:
166
209
 
167
- Tasks are not limited to this constraint, actions can use depend on each other freely.
210
+ | Component | Injection Description |
211
+ | ------------ | --------------------------------------------------------- |
212
+ | `tasks` | Injected as functions with their input argument |
213
+ | `resources` | Injected as their return value |
214
+ | `events` | Injected as functions with their payload argument |
215
+ | `middleware` | Not typically injected; used via a `middleware: []` array |
168
216
 
169
217
  ## Events
170
218
 
@@ -184,7 +232,6 @@ const root = resource({
184
232
  dependencies: {
185
233
  afterRegisterEvent,
186
234
  },
187
-
188
235
  async init(_, deps) {
189
236
  // the event becomes a function that you run with the propper payload
190
237
  await deps.afterRegisterEvent({ userId: string });
@@ -192,9 +239,9 @@ const root = resource({
192
239
  });
193
240
  ```
194
241
 
195
- There are only 2 ways to listen to events:
242
+ There are only 2 recommended ways to listen to events:
196
243
 
197
- ### `on` property
244
+ ### `task.on` property
198
245
 
199
246
  ```ts
200
247
  import { task, run, event } from "@bluelibs/runner";
@@ -206,24 +253,25 @@ const afterRegisterEvent = event<{ userId: string }>({
206
253
  const helloTask = task({
207
254
  id: "app.hello",
208
255
  on: afterRegisterEvent,
256
+ listenerPriority: 0, // this is the order in which the task will be executed when `on` is present
209
257
  run(event) {
210
258
  console.log("User has been registered!");
211
259
  },
212
260
  });
213
261
 
214
- const root = resource({
262
+ const app = resource({
215
263
  id: "app",
216
264
  register: [afterRegisterEvent, helloTask],
217
265
  dependencies: {
218
266
  afterRegisterEvent,
219
267
  },
220
268
  async init(_, deps) {
221
- deps.afterRegisterEvent({ userId: "XXX" });
269
+ await deps.afterRegisterEvent({ userId: "XXX" });
222
270
  },
223
271
  });
224
272
  ```
225
273
 
226
- ### `hooks` property
274
+ ### `resource.hooks` property
227
275
 
228
276
  This can only be applied to a `resource()`.
229
277
 
@@ -241,11 +289,47 @@ const root = resource({
241
289
  hooks: [
242
290
  {
243
291
  event: global.events.afterInit,
292
+ order: -1000, // event priority, the lower the number, the sooner it will run.
244
293
  async run(event, deps) {
294
+ // both dependencies and event are properly infered through typescript
245
295
  console.log("User has been registered!");
246
296
  },
247
297
  },
248
298
  ],
299
+ async init(_, deps) {
300
+ await deps.afterRegisterEvent({ userId: "XXX" });
301
+ },
302
+ });
303
+ ```
304
+
305
+ #### wildcard events
306
+
307
+ You can listen to all events by using the wildcard `*`.
308
+
309
+ ```ts
310
+ import { task, resource, run, event, global } from "@bluelibs/runner";
311
+
312
+ const afterRegisterEvent = event<{ userId: string }>({
313
+ id: "app.user.registered",
314
+ });
315
+
316
+ const root = resource({
317
+ id: "app",
318
+ register: [afterRegisterEvent],
319
+ dependencies: {},
320
+ hooks: [
321
+ {
322
+ event: "*",
323
+ async run(event, deps) {
324
+ console.log(
325
+ "Generic event detected",
326
+ event.id,
327
+ event.data,
328
+ event.timestamp
329
+ );
330
+ },
331
+ },
332
+ ],
249
333
  async init(_, deps) {
250
334
  deps.afterRegisterEvent({ userId: "XXX" });
251
335
  },
@@ -256,12 +340,17 @@ When using hooks, inside resource() you benefit of autocompletion, in order to k
256
340
 
257
341
  The hooks from a `resource` are mostly used for configuration, and blending in the system.
258
342
 
343
+ ### When to use either?
344
+
345
+ - `hooks` are for resources to extend each other, compose functionalities, they are mostly used for configuration and blending in the system.
346
+ - `on` is for when you want to perform a task when something happens, like send an email, begin processing something, etc.
347
+
259
348
  ## Middleware
260
349
 
261
- Middleware is a way to intercept the execution of tasks. It's a powerful way to add additional functionality to your tasks. First middleware that gets registered is the first that runs, the last middleware that runs is 'closest' to the task, most likely the last element inside `middleware` array at task level.
350
+ 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.)
262
351
 
263
352
  ```ts
264
- import { task, run, event } from "@bluelibs/runner";
353
+ import { task, resource, run, event } from "@bluelibs/runner";
265
354
 
266
355
  const logMiddleware = middleware({
267
356
  id: "app.middleware.log",
@@ -269,11 +358,18 @@ const logMiddleware = middleware({
269
358
  // inject tasks, resources, eventCallers here.
270
359
  },
271
360
  async run(data, deps) {
272
- const { taskDefinition, next, input } = data;
273
-
274
- console.log("Before task", taskDefinition.id);
275
- const result = await next(input); // pass the input to the next middleware or task
276
- console.log("After task", taskDefinition.id);
361
+ const { taskDefinition, resourceDefinition, config, next, input } = data;
362
+
363
+ // The middleware can be for a task or a resource, depending on which you get the right elements.
364
+ if (taskDefinition) {
365
+ console.log("Before task", taskDefinition.id);
366
+ const result = await next(input); // pass the input to the next middleware or task
367
+ console.log("After task", taskDefinition.id);
368
+ } else {
369
+ console.log("Before resource", resourceDefinition.id);
370
+ const result = await next(config); // pass the input to the next middleware or task
371
+ console.log("After resource", resourceDefinition.id);
372
+ }
277
373
 
278
374
  return result;
279
375
  },
@@ -288,34 +384,16 @@ const helloTask = task({
288
384
  });
289
385
  ```
290
386
 
291
- You can use middleware creators (function that returns) for configurable middlewares such as:
387
+ ### Global
292
388
 
293
- ```ts
294
- import { middleware } from "@bluelibs/runner";
295
-
296
- function createLogMiddleware(config) {
297
- return middleware({
298
- // your config-based middleware here.
299
- });
300
- }
301
- ```
302
-
303
- However, if you want to register a middleware for all tasks, here's how you can do it:
389
+ If you want to register a middleware for all tasks and resources, here's how you can do it:
304
390
 
305
391
  ```ts
306
392
  import { run, resource } from "@bluelibs/runner";
307
393
 
308
394
  const logMiddleware = middleware({
309
395
  id: "app.middleware.log",
310
- async run(data, deps) {
311
- const { taskDefinition, next, input } = data;
312
-
313
- console.log("Before task", task.id);
314
- const result = await next(input);
315
- console.log("After task", task.id);
316
-
317
- return result;
318
- },
396
+ // ... rest
319
397
  });
320
398
 
321
399
  const root = resource({
@@ -324,22 +402,11 @@ const root = resource({
324
402
  });
325
403
  ```
326
404
 
327
- 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.
328
-
329
- ### Middleware for resources
330
-
331
- Unfortunately, middleware for resources is not supported at the moment. The main reason for this is simplicity and the fact that resources are not meant to be executed, but rather to be initialized.
332
-
333
- You have access to the global events if you want to hook into the initialisation system.
334
-
335
- ### When to use either?
336
-
337
- - `hooks` are for resources to extend each other, compose functionalities, they are mostly used for configuration and blending in the system.
338
- - `on` is for when you want to perform a task when something happens.
405
+ 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.
339
406
 
340
407
  ## Errors
341
408
 
342
- If an error is thrown in a task, the error will be propagated up.
409
+ If an error is thrown in a task, the error will be propagated up to the top runner.
343
410
 
344
411
  ```ts
345
412
  import { task, run, event } from "@bluelibs/runner";
@@ -362,22 +429,40 @@ const app = resource({
362
429
  },
363
430
  });
364
431
 
365
- run(app);
432
+ run(app).catch((err) => {
433
+ console.error(err);
434
+ });
366
435
  ```
367
436
 
368
437
  You can listen to errors via events:
369
438
 
370
439
  ```ts
371
440
  const helloWorld = task({
372
- id: "app.onError",
441
+ id: "app.tasks.helloWorld.onError",
442
+ on: helloWorld.events.onError,
443
+ run({ error, input, suppress }, deps) {
444
+ // this will be called when an error happens
445
+
446
+ // if you handled the error, and you don't want it propagated to the top, supress the propagation.
447
+ suppress();
448
+ },
449
+ });
450
+ ```
451
+
452
+ ```ts
453
+ const helloWorld = resource({
454
+ id: "app.resources.helloWorld.onError",
373
455
  on: helloWorld.events.onError,
374
- run({ error, input }, deps) {
456
+ init({ error, input, suppress }, deps) {
375
457
  // this will be called when an error happens
458
+
459
+ // if you handled the error, and you don't want it propagated to the top, supress the propagation.
460
+ suppress();
376
461
  },
377
462
  });
378
463
  ```
379
464
 
380
- ## Metadata
465
+ ## Meta
381
466
 
382
467
  You can attach metadata to tasks, resources, events, and middleware.
383
468
 
@@ -397,7 +482,7 @@ const helloWorld = task({
397
482
  });
398
483
  ```
399
484
 
400
- This is particularly helpful to use in conjunction with global middlewares, or global events, etc.
485
+ 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.
401
486
 
402
487
  The interfaces look like this:
403
488
 
@@ -416,7 +501,7 @@ export interface IMiddlewareMeta extends IMeta {}
416
501
 
417
502
  Which means you can extend them in your system to add more keys to better describe your actions.
418
503
 
419
- ## Global Services
504
+ ## Internal Services
420
505
 
421
506
  We expose direct access to the following internal services:
422
507
 
@@ -424,7 +509,7 @@ We expose direct access to the following internal services:
424
509
  - TaskRunner (can run tasks definitions directly and within D.I. context)
425
510
  - EventManager (can emit and listen to events)
426
511
 
427
- Attention, it is not recommended to use these services directly, but they are exposed for advanced use-cases, for when you do not have any other way.
512
+ Attention, we do not encourage you to use these services directly, unless you really have to, they are exposed for advanced use-case scenarios.
428
513
 
429
514
  ```ts
430
515
  import { task, run, event, globals } from "@bluelibs/runner";
@@ -444,16 +529,20 @@ const helloWorld = task({
444
529
 
445
530
  ## Namespacing
446
531
 
447
- We typically namespace using `.` like `app.helloWorld`. This is a convention that we use to make sure that we can easily identify where the task belongs to.
448
-
449
- When creating special packages the convention is:
532
+ Domain usually is "app", but as your application grows or you plan on building external libraries the naming convention should be: "companyName.packageName".
450
533
 
451
- - `{companyName}.{packageName}.{taskName}`
534
+ | Type | Format |
535
+ | -------------- | ----------------------------------------- |
536
+ | Tasks | `{domain}.tasks.{taskName}` |
537
+ | Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
538
+ | Resources | `{domain}.resources.{resourceName}` |
539
+ | Events | `{domain}.events.{eventName}` |
540
+ | Middleware | `{domain}.middleware.{middlewareName}` |
452
541
 
453
542
  You can always create helpers for you as you're creating your tasks, resources, middleware:
454
543
 
455
544
  ```ts
456
- function getNamespace(id) {
545
+ function namespaced(id) {
457
546
  return `bluelibs.core.${id}`;
458
547
  }
459
548
  ```
@@ -495,9 +584,11 @@ Now you can freely use any of the tasks, resources, events, and middlewares from
495
584
 
496
585
  This approach is very powerful when you have multiple packages and you want to compose them together.
497
586
 
498
- ## Real world usage.
587
+ ## Real life
499
588
 
500
- Typically you have an express server (to handle HTTP requests), a database, and a bunch of services. You can define all of these in a single file and run them.
589
+ Or is it just fantasy?
590
+
591
+ 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.
501
592
 
502
593
  ```ts
503
594
  import { task, resource, run, event } from "@bluelibs/runner";
@@ -537,11 +628,11 @@ const app = resource({
537
628
  run();
538
629
  ```
539
630
 
540
- The system is smart enough to know which `init()` to call first. Typically all dependencies are initialised first. If there are circular dependencies, an error will be thrown with the exact paths.
631
+ 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.
541
632
 
542
633
  ### Business config
543
634
 
544
- There's a resource for that! You can define a resource that holds your business configuration.
635
+ Or just simple config, you can do it for your business logic, environment variables, etc.
545
636
 
546
637
  ```ts
547
638
  import { resource, run } from "@bluelibs/runner";
@@ -570,47 +661,7 @@ const app = resource({
570
661
  run();
571
662
  ```
572
663
 
573
- ### Resources can receive configs
574
-
575
- Resources are super configurable.
576
-
577
- ```ts
578
- import { resource, run } from "@bluelibs/runner";
579
-
580
- type EmailerOptions = {
581
- smtpUrl: string;
582
- defaultFrom: string;
583
- };
584
-
585
- const emailerResource = resource({
586
- id: "app.config",
587
- async init(config: EmailerOptions) {
588
- return {
589
- sendEmail: async (to: string, subject: string, body: string) => {
590
- // send *email*
591
- },
592
- };
593
- // or return some service that sends email
594
- },
595
- });
596
-
597
- const app = resource({
598
- id: "app",
599
- register: [
600
- // You can pass the config here
601
- emailerResource.with({
602
- smtpUrl: "smtp://localhost",
603
- defaultFrom: "",
604
- }),
605
- // Leaving it simply emailerResource is similar to passing an empty object.
606
- // We leave this for simplicity in some cases but we recommend using .with() for clarity.
607
- ],
608
- });
609
-
610
- run(app);
611
- ```
612
-
613
- ## Useful events
664
+ ## Global Events
614
665
 
615
666
  ### Task level
616
667
 
@@ -650,15 +701,19 @@ const app = resource({
650
701
  run(app);
651
702
  ```
652
703
 
653
- ## Resource level
704
+ ### Resource level
654
705
 
655
706
  ```ts
656
707
  import { task, run, event } from "@bluelibs/runner";
657
708
 
709
+ const businessData = {
710
+ pricePerSubscription: 9.99,
711
+ };
712
+
658
713
  const businessConfig = resource({
659
714
  id: "app.config",
660
715
  async init() {
661
- return businessData;
716
+ return businessData; // if you use it as a const you will have full typesafety
662
717
  },
663
718
  });
664
719
 
@@ -690,24 +745,30 @@ const app = resource({
690
745
  run(app);
691
746
  ```
692
747
 
693
- ## Moving further
748
+ ## Advanced Usage
694
749
 
695
750
  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.
696
751
 
752
+ This doesn't mean you shouldn't use classes, just not for hooking things up together.
753
+
697
754
  You can add many services or external things into the runner ecosystem with things like:
698
755
 
699
756
  ```ts
700
757
  import { task, run, event } from "@bluelibs/runner";
701
758
 
702
- const expressResource = resource<express.Application>({
759
+ // proxy declaration pattern
760
+ const expressResource = resource({
703
761
  id: "app.helloWorld",
704
- run: async (config) => config,
762
+ run: async (app: express.Application) => app,
705
763
  });
706
764
 
707
765
  const app = resource({
708
766
  id: "app",
709
767
  register: [expressResource.with(express())],
710
- init: async (express) => {
768
+ dependencies: {
769
+ express: expressResource,
770
+ },
771
+ init: async (_, { express }) => {
711
772
  express.get("/", (req, res) => {
712
773
  res.send("Hello World!");
713
774
  });
@@ -717,12 +778,16 @@ const app = resource({
717
778
  run(app);
718
779
  ```
719
780
 
720
- This shows how easy you encapsulate an external service into the runner ecosystem. This 'pattern' of storing objects like this is not that common because usually they require a configuration with propper options and stuff, not an express instance(), like this:
781
+ 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:
721
782
 
722
783
  ```ts
784
+ type Config = {
785
+ port: number;
786
+ };
787
+
723
788
  const expressResource = resource({
724
789
  id: "app.helloWorld",
725
- run: async (config) => {
790
+ init: async (config: Config) => {
726
791
  const app = express();
727
792
  app.listen(config.port);
728
793
  return app;
@@ -732,7 +797,10 @@ const expressResource = resource({
732
797
  const app = resource({
733
798
  id: "app",
734
799
  register: [expressResource.with({ port: 3000 })],
735
- init: async (express) => {
800
+ dependencies: {
801
+ express: expressResource,
802
+ },
803
+ init: async (_, { express }) => {
736
804
  // type is automagically infered.
737
805
  express.get("/", (req, res) => {
738
806
  res.send("Hello World!");
@@ -743,7 +811,7 @@ const app = resource({
743
811
  run(app);
744
812
  ```
745
813
 
746
- ## Inter-communication between resources
814
+ ### Inter-communication between resources
747
815
 
748
816
  By stating dependencies you often don't care about the initialisation order, but sometimes you really do, for example, let's imagine a security service that allows you to inject a custom hashing function let's say to shift from md5 to sha256.
749
817
 
@@ -776,6 +844,7 @@ const app = resource({
776
844
  register: [securityResource],
777
845
  hooks: [
778
846
  {
847
+ // careful when you listen on such events and need dependencies, you might not have them computed yet due to how early these events happen in the system.
779
848
  event: securityResource.events.afterInit,
780
849
  async run(event, deps) {
781
850
  const { config, value } = event.data;
@@ -790,7 +859,7 @@ const app = resource({
790
859
  });
791
860
  ```
792
861
 
793
- Another approach is to create a new event that holds the config and it allows it to be updated.
862
+ Another approach is to create a new event that contains the configuration, providing the flexibility to update it as needed.
794
863
 
795
864
  ```ts
796
865
  import { resource, run, event } from "@bluelibs/runner";
@@ -807,6 +876,7 @@ const securityResource = resource({
807
876
  async init(config: SecurityOptions) {
808
877
  // Give the ability to other listeners to modify the configuration
809
878
  securityConfigurationPhaseEvent(config);
879
+ Objecte.freeze(config);
810
880
 
811
881
  return {
812
882
  // ... based on config
@@ -829,11 +899,42 @@ const app = resource({
829
899
  });
830
900
  ```
831
901
 
832
- ## Logging
902
+ ### Overrides
903
+
904
+ 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.
905
+
906
+ ```ts
907
+ import { resource, run, event } from "@bluelibs/runner";
908
+
909
+ // This example is for resources but override works for tasks, events, and middleware as well.
910
+ const securityResource = resource({
911
+ id: "app.security",
912
+ async init() {
913
+ // returns a security service
914
+ },
915
+ });
916
+
917
+ const override = resource({
918
+ ...securityResource,
919
+ init: async () => {
920
+ // a new and custom service
921
+ },
922
+ });
923
+
924
+ const app = resource({
925
+ id: "app",
926
+ register: [securityResource], // this resource might be registered by any element in the dependency tree.
927
+ overrides: [override],
928
+ });
929
+ ```
833
930
 
834
- We expose through globals a logger that you can use to log things.
931
+ The new securityResource will replace the existing one, ensuring all future references point to the updated version.
835
932
 
836
- By default logs are not printed unless a resource listens to the log event. This is by design, when something is logged an event is emitted. You can listen to this event and print the logs.
933
+ 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.
934
+
935
+ ## Logging
936
+
937
+ 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.
837
938
 
838
939
  ```ts
839
940
  import { task, run, event, globals } from "@bluelibs/runner";
@@ -863,19 +964,23 @@ const helloWorld = task({
863
964
 
864
965
  ### Print logs
865
966
 
866
- Logs don't get printed by default in this system.
967
+ 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.
968
+
969
+ To showcase the versatility of the system, here are some ways you could do it:
867
970
 
868
971
  ```ts
869
972
  import { task, run, event, globals, resource } from "@bluelibs/runner";
870
973
 
974
+ const { logger } = globals.resources;
975
+
871
976
  const printLog = task({
872
- id: "app.task.printLog",
873
- on: globals.events.log,
874
- dependencies: {
875
- logger: globals.resources.logger,
876
- },
877
- run: async (event, { logger }) => {
878
- logger.print(event);
977
+ id: "app.task.updatePrintThreshold",
978
+ on: logger.events.afterInit,
979
+ // Note: logger is
980
+ run: async (event, deps) => {
981
+ const logger = event.data.value;
982
+ logger.setPrintThreshold("trace"); // will print all logs
983
+ logger.setPrintThreshold("error"); // will print only "error" and "critical" logs
879
984
  },
880
985
  });
881
986
 
@@ -887,50 +992,79 @@ const app = resource({
887
992
  // Now your app will print all logs
888
993
  ```
889
994
 
890
- You can in theory do it in `hooks` as well, but as specified `hooks` are mostly used for configuration and blending in the system.
995
+ You can also achieve this using hooks:
891
996
 
892
- The logger's `log()` function is async as it works with events. If you don't want your system hanging on logs, simply omit the `await`
997
+ ```ts
998
+ resource({
999
+ id: "root",
1000
+ hooks: [
1001
+ {
1002
+ // after logger gets initialised as a resource, I'm going to set the print threshold
1003
+ event: logger.events.afterInit,
1004
+ async run(event) {
1005
+ const logger = event.data; // do not depend on the logger
1006
+ logger.setPrintThreshold("trace");
1007
+ },
1008
+ },
1009
+ ],
1010
+ });
1011
+ ```
893
1012
 
894
- ## Overrides
1013
+ 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.
895
1014
 
896
- Previously, we explored how we can extend functionality through events. However, sometimes you want to override a resource with a new one or simply swap out a task or a middleware that you import from another package and they don't offer the ability.
1015
+ 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.
897
1016
 
898
1017
  ```ts
899
- import { resource, run, event } from "@bluelibs/runner";
1018
+ import { task, run, event, globals } from "@bluelibs/runner";
900
1019
 
901
- // This example is for resources but override works for tasks, events, and middleware as well.
902
- const securityResource = resource({
903
- id: "app.security",
904
- async init() {
905
- // returns a security service
906
- },
907
- });
1020
+ const { logger } = globals.resources;
908
1021
 
909
- const override = resource({
910
- ...securityResource,
911
- init: async () => {
912
- // a new and custom service
1022
+ const shipLogsToWarehouse = task({
1023
+ id: "app.task.shipLogsToWarehouse",
1024
+ on: logger.events.log,
1025
+ dependencies: {
1026
+ warehouseService: warehouseServiceResource,
1027
+ },
1028
+ run: async (event, deps) => {
1029
+ const log = event.data; // ILog
1030
+ if (log.level === "error" || log.level === "critical") {
1031
+ // Ensure no extra log() calls are made here to prevent infinite loops
1032
+ await deps.warehouseService.push(log);
1033
+ }
913
1034
  },
914
1035
  });
1036
+ ```
915
1037
 
916
- const app = resource({
917
- id: "app",
918
- register: [securityResource], // this resource might be registered by any element in the dependency tree.
919
- overrides: [override],
1038
+ And yes, this would also work:
1039
+
1040
+ ```ts
1041
+ const task = task({
1042
+ id: "app.task.logSomething",
1043
+ dependencies: {
1044
+ log: globals.events.log,
1045
+ },
1046
+ run: async (_, { log }) => {
1047
+ await log({
1048
+ level: "info",
1049
+ data: { anything: "you want" };
1050
+ timestamp: new Date();
1051
+ context: "app.task.logSomething"; // optional
1052
+ })
1053
+ },
920
1054
  });
921
1055
  ```
922
1056
 
923
- Now the `securityResource` will be overriden by the new one and whenever it's used it will use the new one.
924
-
925
- Overrides can only happen once and only if the overriden resource is registered. If two resources try to override the same resource, an error will be thrown.
1057
+ 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, its important to carefully review and verify your dependencies to prevent potential issues.
926
1058
 
927
1059
  ## Testing
928
1060
 
1061
+ 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.
1062
+
929
1063
  ### Unit Testing
930
1064
 
931
- You can easily test your resources and tasks by running them in a test environment.
1065
+ You can easily test your middleware, resources and tasks by running them in a test environment.
932
1066
 
933
- The only bits that you need to test are the `run` function and the `init` functions with the propper dependencies.
1067
+ The only components you need to test are the run function and the init functions, along with their proper dependencies.
934
1068
 
935
1069
  ```ts
936
1070
  import { task, resource } from "@bluelibs/runner";
@@ -968,7 +1102,7 @@ describe("app.helloWorldResource", () => {
968
1102
 
969
1103
  ### Integration
970
1104
 
971
- Unit testing can be very simply with mocks, since all dependencies are explicit. However, if you would like to run an integration test, and have a task be tested and within the full container.
1105
+ 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.
972
1106
 
973
1107
  ```ts
974
1108
  import { task, resource, run, global } from "@bluelibs/runner";
@@ -986,7 +1120,7 @@ const app = resource({
986
1120
  });
987
1121
  ```
988
1122
 
989
- Then your tests can now be cleaner:
1123
+ Then your tests can now be cleaner, as you can use `overrides` and a wrapper resource to mock your task.
990
1124
 
991
1125
  ```ts
992
1126
  describe("app", () => {
@@ -994,14 +1128,12 @@ describe("app", () => {
994
1128
  const testApp = resource({
995
1129
  id: "app.test",
996
1130
  register: [myApp], // wrap your existing app
997
- overrides: [override], // apply the overrides
1131
+ overrides: [override], // apply the overrides for "app.myTask"
998
1132
  init: async (_, deps) => {
999
1133
  // you can now test a task simply by depending on it, and running it, then asserting the response of run()
1000
1134
  },
1001
1135
  });
1002
1136
 
1003
- // Same concept applies for resources as well.
1004
-
1005
1137
  await run(testApp);
1006
1138
  });
1007
1139
  });