@bluelibs/runner 1.3.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 (45) hide show
  1. package/README.md +191 -76
  2. package/dist/defs.d.ts +31 -7
  3. package/dist/examples/express-mongo/index.d.ts +0 -0
  4. package/dist/examples/express-mongo/index.js +3 -0
  5. package/dist/examples/express-mongo/index.js.map +1 -0
  6. package/dist/globalEvents.js +1 -0
  7. package/dist/globalEvents.js.map +1 -1
  8. package/dist/models/DependencyProcessor.d.ts +4 -2
  9. package/dist/models/DependencyProcessor.js +33 -15
  10. package/dist/models/DependencyProcessor.js.map +1 -1
  11. package/dist/models/EventManager.js.map +1 -1
  12. package/dist/models/Logger.d.ts +15 -10
  13. package/dist/models/Logger.js +27 -15
  14. package/dist/models/Logger.js.map +1 -1
  15. package/dist/models/ResourceInitializer.d.ts +3 -1
  16. package/dist/models/ResourceInitializer.js +4 -4
  17. package/dist/models/ResourceInitializer.js.map +1 -1
  18. package/dist/models/Store.d.ts +4 -1
  19. package/dist/models/Store.js +17 -3
  20. package/dist/models/Store.js.map +1 -1
  21. package/dist/models/TaskRunner.d.ts +3 -1
  22. package/dist/models/TaskRunner.js +7 -1
  23. package/dist/models/TaskRunner.js.map +1 -1
  24. package/dist/run.d.ts +0 -8
  25. package/dist/run.js +8 -6
  26. package/dist/run.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/index.ts +1 -0
  29. package/src/__tests__/models/Logger.test.ts +44 -1
  30. package/src/__tests__/models/ResourceInitializer.test.ts +5 -4
  31. package/src/__tests__/models/Store.test.ts +4 -2
  32. package/src/__tests__/models/TaskRunner.test.ts +5 -2
  33. package/src/__tests__/run.hooks.test.ts +0 -31
  34. package/src/__tests__/run.middleware.test.ts +26 -0
  35. package/src/__tests__/typesafety.test.ts +127 -0
  36. package/src/defs.ts +47 -15
  37. package/src/examples/express-mongo/index.ts +1 -0
  38. package/src/globalEvents.ts +1 -0
  39. package/src/models/DependencyProcessor.ts +72 -37
  40. package/src/models/EventManager.ts +1 -0
  41. package/src/models/Logger.ts +36 -16
  42. package/src/models/ResourceInitializer.ts +5 -4
  43. package/src/models/Store.ts +20 -3
  44. package/src/models/TaskRunner.ts +8 -1
  45. 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
- 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.
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
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 for cleanups. 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";
@@ -106,7 +108,7 @@ const dbResource = resource({
106
108
  });
107
109
  ```
108
110
 
109
- If you want to call dispose, you have to do it through the global resource called `store`, as everything is encapsulated.
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.
110
112
 
111
113
  ```ts
112
114
  import { task, run, resource, globals } from "@bluelibs/runner";
@@ -130,17 +132,19 @@ const value = await run(app);
130
132
  await value.dispose();
131
133
  ```
132
134
 
133
- ### Resource with()
135
+ ### Resource configuration
134
136
 
135
- Resources can be configured with a configuration object. This is useful when you want to pass in configuration to them. For example, you're building a library and you're initialising a mailer service, you can pass in the SMTP credentials as a configuration.
137
+ Resources can be set up with a configuration object, which is helpful for passing in specific settings. For example, if youre building a library and initializing a mailer service, you can provide the SMTP credentials through this configuration.
136
138
 
137
139
  ```ts
138
140
  import { task, run, resource } from "@bluelibs/runner";
139
141
 
140
142
  type Config = { smtpUrl: string; defaultFrom: string };
143
+
141
144
  const emailerResource = resource({
145
+ // automatic type inference.
142
146
  async init(config: Config) {
143
- // run config checks
147
+ // todo: perform config checks with a library like zod
144
148
  return {
145
149
  sendEmail: async (to: string, subject: string, body: string) => {
146
150
  // send *email*
@@ -176,10 +180,15 @@ const helloWorld = task({
176
180
  dependencies: {
177
181
  userRegisteredEvent,
178
182
  },
183
+ async run(_, deps) {
184
+ await deps.userRegisteredEvent();
185
+ return "Hello World!";
186
+ },
179
187
  });
180
188
 
181
189
  const app = resource({
182
190
  id: "app",
191
+ // You have to register everything you use.
183
192
  register: [helloWorld, logMiddleware],
184
193
  dependencies: {
185
194
  helloWorld,
@@ -192,9 +201,18 @@ const app = resource({
192
201
  run(app);
193
202
  ```
194
203
 
195
- 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.
196
207
 
197
- Tasks are not limited to this constraint, actions can use depend on each other freely.
208
+ The dependencies get injected as follows:
209
+
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 |
198
216
 
199
217
  ## Events
200
218
 
@@ -214,7 +232,6 @@ const root = resource({
214
232
  dependencies: {
215
233
  afterRegisterEvent,
216
234
  },
217
-
218
235
  async init(_, deps) {
219
236
  // the event becomes a function that you run with the propper payload
220
237
  await deps.afterRegisterEvent({ userId: string });
@@ -222,9 +239,9 @@ const root = resource({
222
239
  });
223
240
  ```
224
241
 
225
- There are only 2 ways to listen to events:
242
+ There are only 2 recommended ways to listen to events:
226
243
 
227
- ### `on` property
244
+ ### `task.on` property
228
245
 
229
246
  ```ts
230
247
  import { task, run, event } from "@bluelibs/runner";
@@ -236,6 +253,7 @@ const afterRegisterEvent = event<{ userId: string }>({
236
253
  const helloTask = task({
237
254
  id: "app.hello",
238
255
  on: afterRegisterEvent,
256
+ listenerPriority: 0, // this is the order in which the task will be executed when `on` is present
239
257
  run(event) {
240
258
  console.log("User has been registered!");
241
259
  },
@@ -253,7 +271,7 @@ const app = resource({
253
271
  });
254
272
  ```
255
273
 
256
- ### `hooks` property
274
+ ### `resource.hooks` property
257
275
 
258
276
  This can only be applied to a `resource()`.
259
277
 
@@ -271,6 +289,7 @@ const root = resource({
271
289
  hooks: [
272
290
  {
273
291
  event: global.events.afterInit,
292
+ order: -1000, // event priority, the lower the number, the sooner it will run.
274
293
  async run(event, deps) {
275
294
  // both dependencies and event are properly infered through typescript
276
295
  console.log("User has been registered!");
@@ -278,12 +297,12 @@ const root = resource({
278
297
  },
279
298
  ],
280
299
  async init(_, deps) {
281
- deps.afterRegisterEvent({ userId: "XXX" });
300
+ await deps.afterRegisterEvent({ userId: "XXX" });
282
301
  },
283
302
  });
284
303
  ```
285
304
 
286
- ### hooks wildcard
305
+ #### wildcard events
287
306
 
288
307
  You can listen to all events by using the wildcard `*`.
289
308
 
@@ -324,11 +343,11 @@ The hooks from a `resource` are mostly used for configuration, and blending in t
324
343
  ### When to use either?
325
344
 
326
345
  - `hooks` are for resources to extend each other, compose functionalities, they are mostly used for configuration and blending in the system.
327
- - `on` is for when you want to perform a task when something happens.
346
+ - `on` is for when you want to perform a task when something happens, like send an email, begin processing something, etc.
328
347
 
329
348
  ## Middleware
330
349
 
331
- Middleware is a way to intercept the execution of tasks or initialization of resources. It's a powerful way to add additional functionality. First middleware that gets registered is the first that runs, giving it a form of priority, 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.)
332
351
 
333
352
  ```ts
334
353
  import { task, resource, run, event } from "@bluelibs/runner";
@@ -365,7 +384,9 @@ const helloTask = task({
365
384
  });
366
385
  ```
367
386
 
368
- However, if you want to register a middleware for all tasks and resources, here's how you can do it:
387
+ ### Global
388
+
389
+ If you want to register a middleware for all tasks and resources, here's how you can do it:
369
390
 
370
391
  ```ts
371
392
  import { run, resource } from "@bluelibs/runner";
@@ -381,7 +402,7 @@ const root = resource({
381
402
  });
382
403
  ```
383
404
 
384
- 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.
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.
385
406
 
386
407
  ## Errors
387
408
 
@@ -417,7 +438,7 @@ You can listen to errors via events:
417
438
 
418
439
  ```ts
419
440
  const helloWorld = task({
420
- id: "app.onError",
441
+ id: "app.tasks.helloWorld.onError",
421
442
  on: helloWorld.events.onError,
422
443
  run({ error, input, suppress }, deps) {
423
444
  // this will be called when an error happens
@@ -428,7 +449,20 @@ const helloWorld = task({
428
449
  });
429
450
  ```
430
451
 
431
- ## Metadata
452
+ ```ts
453
+ const helloWorld = resource({
454
+ id: "app.resources.helloWorld.onError",
455
+ on: helloWorld.events.onError,
456
+ init({ error, input, suppress }, deps) {
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();
461
+ },
462
+ });
463
+ ```
464
+
465
+ ## Meta
432
466
 
433
467
  You can attach metadata to tasks, resources, events, and middleware.
434
468
 
@@ -448,7 +482,7 @@ const helloWorld = task({
448
482
  });
449
483
  ```
450
484
 
451
- This is particularly helpful to use in conjunction with global middlewares, or global events, they can read some meta tag definition and act accordingly.
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.
452
486
 
453
487
  The interfaces look like this:
454
488
 
@@ -467,7 +501,7 @@ export interface IMiddlewareMeta extends IMeta {}
467
501
 
468
502
  Which means you can extend them in your system to add more keys to better describe your actions.
469
503
 
470
- ## Global Services
504
+ ## Internal Services
471
505
 
472
506
  We expose direct access to the following internal services:
473
507
 
@@ -475,7 +509,7 @@ We expose direct access to the following internal services:
475
509
  - TaskRunner (can run tasks definitions directly and within D.I. context)
476
510
  - EventManager (can emit and listen to events)
477
511
 
478
- 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.
479
513
 
480
514
  ```ts
481
515
  import { task, run, event, globals } from "@bluelibs/runner";
@@ -495,16 +529,20 @@ const helloWorld = task({
495
529
 
496
530
  ## Namespacing
497
531
 
498
- 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.
499
-
500
- 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".
501
533
 
502
- - `{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}` |
503
541
 
504
542
  You can always create helpers for you as you're creating your tasks, resources, middleware:
505
543
 
506
544
  ```ts
507
- function getNamespace(id) {
545
+ function namespaced(id) {
508
546
  return `bluelibs.core.${id}`;
509
547
  }
510
548
  ```
@@ -546,9 +584,11 @@ Now you can freely use any of the tasks, resources, events, and middlewares from
546
584
 
547
585
  This approach is very powerful when you have multiple packages and you want to compose them together.
548
586
 
549
- ## Real world examples
587
+ ## Real life
550
588
 
551
- 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.
552
592
 
553
593
  ```ts
554
594
  import { task, resource, run, event } from "@bluelibs/runner";
@@ -588,11 +628,11 @@ const app = resource({
588
628
  run();
589
629
  ```
590
630
 
591
- 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.
592
632
 
593
633
  ### Business config
594
634
 
595
- 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.
596
636
 
597
637
  ```ts
598
638
  import { resource, run } from "@bluelibs/runner";
@@ -661,7 +701,7 @@ const app = resource({
661
701
  run(app);
662
702
  ```
663
703
 
664
- ### Business Config
704
+ ### Resource level
665
705
 
666
706
  ```ts
667
707
  import { task, run, event } from "@bluelibs/runner";
@@ -705,7 +745,7 @@ const app = resource({
705
745
  run(app);
706
746
  ```
707
747
 
708
- ### Moving further
748
+ ## Advanced Usage
709
749
 
710
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.
711
751
 
@@ -716,15 +756,19 @@ You can add many services or external things into the runner ecosystem with thin
716
756
  ```ts
717
757
  import { task, run, event } from "@bluelibs/runner";
718
758
 
719
- const expressResource = resource<express.Application>({
759
+ // proxy declaration pattern
760
+ const expressResource = resource({
720
761
  id: "app.helloWorld",
721
- run: async (config) => config,
762
+ run: async (app: express.Application) => app,
722
763
  });
723
764
 
724
765
  const app = resource({
725
766
  id: "app",
726
767
  register: [expressResource.with(express())],
727
- init: async (express) => {
768
+ dependencies: {
769
+ express: expressResource,
770
+ },
771
+ init: async (_, { express }) => {
728
772
  express.get("/", (req, res) => {
729
773
  res.send("Hello World!");
730
774
  });
@@ -734,12 +778,16 @@ const app = resource({
734
778
  run(app);
735
779
  ```
736
780
 
737
- 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:
738
782
 
739
783
  ```ts
784
+ type Config = {
785
+ port: number;
786
+ };
787
+
740
788
  const expressResource = resource({
741
789
  id: "app.helloWorld",
742
- run: async (config) => {
790
+ init: async (config: Config) => {
743
791
  const app = express();
744
792
  app.listen(config.port);
745
793
  return app;
@@ -749,7 +797,10 @@ const expressResource = resource({
749
797
  const app = resource({
750
798
  id: "app",
751
799
  register: [expressResource.with({ port: 3000 })],
752
- init: async (express) => {
800
+ dependencies: {
801
+ express: expressResource,
802
+ },
803
+ init: async (_, { express }) => {
753
804
  // type is automagically infered.
754
805
  express.get("/", (req, res) => {
755
806
  res.send("Hello World!");
@@ -793,6 +844,7 @@ const app = resource({
793
844
  register: [securityResource],
794
845
  hooks: [
795
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.
796
848
  event: securityResource.events.afterInit,
797
849
  async run(event, deps) {
798
850
  const { config, value } = event.data;
@@ -807,7 +859,7 @@ const app = resource({
807
859
  });
808
860
  ```
809
861
 
810
- 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.
811
863
 
812
864
  ```ts
813
865
  import { resource, run, event } from "@bluelibs/runner";
@@ -824,6 +876,7 @@ const securityResource = resource({
824
876
  async init(config: SecurityOptions) {
825
877
  // Give the ability to other listeners to modify the configuration
826
878
  securityConfigurationPhaseEvent(config);
879
+ Objecte.freeze(config);
827
880
 
828
881
  return {
829
882
  // ... based on config
@@ -846,9 +899,9 @@ const app = resource({
846
899
  });
847
900
  ```
848
901
 
849
- ## Overrides
902
+ ### Overrides
850
903
 
851
- 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.
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.
852
905
 
853
906
  ```ts
854
907
  import { resource, run, event } from "@bluelibs/runner";
@@ -875,15 +928,13 @@ const app = resource({
875
928
  });
876
929
  ```
877
930
 
878
- Now the `securityResource` will be overriden by the new one and whenever it's used it will use the new one.
931
+ The new securityResource will replace the existing one, ensuring all future references point to the updated version.
879
932
 
880
- 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.
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.
881
934
 
882
935
  ## Logging
883
936
 
884
- We expose through globals a logger that you can use to log things.
885
-
886
- 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.
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.
887
938
 
888
939
  ```ts
889
940
  import { task, run, event, globals } from "@bluelibs/runner";
@@ -913,19 +964,23 @@ const helloWorld = task({
913
964
 
914
965
  ### Print logs
915
966
 
916
- 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:
917
970
 
918
971
  ```ts
919
972
  import { task, run, event, globals, resource } from "@bluelibs/runner";
920
973
 
974
+ const { logger } = globals.resources;
975
+
921
976
  const printLog = task({
922
- id: "app.task.printLog",
923
- on: globals.events.log,
924
- dependencies: {
925
- logger: globals.resources.logger,
926
- },
927
- run: async (event, { logger }) => {
928
- 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
929
984
  },
930
985
  });
931
986
 
@@ -937,17 +992,79 @@ const app = resource({
937
992
  // Now your app will print all logs
938
993
  ```
939
994
 
940
- 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:
996
+
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
+ ```
1012
+
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.
941
1014
 
942
- 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`
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.
1016
+
1017
+ ```ts
1018
+ import { task, run, event, globals } from "@bluelibs/runner";
1019
+
1020
+ const { logger } = globals.resources;
1021
+
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
+ }
1034
+ },
1035
+ });
1036
+ ```
1037
+
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
+ },
1054
+ });
1055
+ ```
1056
+
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, it’s important to carefully review and verify your dependencies to prevent potential issues.
943
1058
 
944
1059
  ## Testing
945
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
+
946
1063
  ### Unit Testing
947
1064
 
948
- 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.
949
1066
 
950
- 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.
951
1068
 
952
1069
  ```ts
953
1070
  import { task, resource } from "@bluelibs/runner";
@@ -985,7 +1102,7 @@ describe("app.helloWorldResource", () => {
985
1102
 
986
1103
  ### Integration
987
1104
 
988
- 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.
989
1106
 
990
1107
  ```ts
991
1108
  import { task, resource, run, global } from "@bluelibs/runner";
@@ -1003,7 +1120,7 @@ const app = resource({
1003
1120
  });
1004
1121
  ```
1005
1122
 
1006
- 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.
1007
1124
 
1008
1125
  ```ts
1009
1126
  describe("app", () => {
@@ -1011,14 +1128,12 @@ describe("app", () => {
1011
1128
  const testApp = resource({
1012
1129
  id: "app.test",
1013
1130
  register: [myApp], // wrap your existing app
1014
- overrides: [override], // apply the overrides
1131
+ overrides: [override], // apply the overrides for "app.myTask"
1015
1132
  init: async (_, deps) => {
1016
1133
  // you can now test a task simply by depending on it, and running it, then asserting the response of run()
1017
1134
  },
1018
1135
  });
1019
1136
 
1020
- // Same concept applies for resources as well.
1021
-
1022
1137
  await run(testApp);
1023
1138
  });
1024
1139
  });