@bluelibs/runner 1.3.0 → 1.5.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 (48) hide show
  1. package/README.md +223 -82
  2. package/dist/defs.d.ts +41 -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 +7 -5
  9. package/dist/models/DependencyProcessor.js +46 -25
  10. package/dist/models/DependencyProcessor.js.map +1 -1
  11. package/dist/models/EventManager.d.ts +1 -1
  12. package/dist/models/EventManager.js +2 -2
  13. package/dist/models/EventManager.js.map +1 -1
  14. package/dist/models/Logger.d.ts +16 -11
  15. package/dist/models/Logger.js +29 -17
  16. package/dist/models/Logger.js.map +1 -1
  17. package/dist/models/ResourceInitializer.d.ts +3 -1
  18. package/dist/models/ResourceInitializer.js +10 -10
  19. package/dist/models/ResourceInitializer.js.map +1 -1
  20. package/dist/models/Store.d.ts +4 -1
  21. package/dist/models/Store.js +17 -3
  22. package/dist/models/Store.js.map +1 -1
  23. package/dist/models/TaskRunner.d.ts +3 -1
  24. package/dist/models/TaskRunner.js +13 -7
  25. package/dist/models/TaskRunner.js.map +1 -1
  26. package/dist/run.d.ts +0 -8
  27. package/dist/run.js +11 -8
  28. package/dist/run.js.map +1 -1
  29. package/package.json +3 -2
  30. package/src/__tests__/index.ts +1 -0
  31. package/src/__tests__/models/EventManager.test.ts +21 -21
  32. package/src/__tests__/models/Logger.test.ts +50 -5
  33. package/src/__tests__/models/ResourceInitializer.test.ts +61 -25
  34. package/src/__tests__/models/Store.test.ts +4 -2
  35. package/src/__tests__/models/TaskRunner.test.ts +5 -2
  36. package/src/__tests__/run.hooks.test.ts +0 -31
  37. package/src/__tests__/run.middleware.test.ts +26 -0
  38. package/src/__tests__/typesafety.test.ts +127 -0
  39. package/src/defs.ts +57 -15
  40. package/src/examples/express-mongo/index.ts +1 -0
  41. package/src/globalEvents.ts +1 -0
  42. package/src/models/DependencyProcessor.ts +103 -47
  43. package/src/models/EventManager.ts +4 -2
  44. package/src/models/Logger.ts +39 -19
  45. package/src/models/ResourceInitializer.ts +53 -27
  46. package/src/models/Store.ts +20 -3
  47. package/src/models/TaskRunner.ts +45 -18
  48. package/src/run.ts +19 -15
package/README.md CHANGED
@@ -1,19 +1,23 @@
1
1
  # BlueLibs Runner
2
2
 
3
3
  <p align="center">
4
- <a href="https://travis-ci.org/bluelibs/runner"><img src="https://github.com/bluelibs/runner/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status" /></a>
4
+ <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>
5
5
  <a href="https://coveralls.io/github/bluelibs/runner?branch=main"><img src="https://coveralls.io/repos/github/bluelibs/runner/badge.svg?branch=main" alt="Coverage Status" /></a>
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
+ - [View the documentation page here](https://bluelibs.github.io/runner/).
10
+ - [Google Notebook LM Podcast](https://notebooklm.google.com/notebook/59bd49fa-346b-4cfb-bb4b-b59857c3b9b4/audio)
11
+ - [Continue GPT Conversation](https://chatgpt.com/share/670392f8-7188-800b-9b4b-e49b437d77f7)
12
+
13
+ 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
14
 
11
15
  ## Building Blocks
12
16
 
13
17
  - **Tasks**: Core units of logic that encapsulate specific tasks. They can depend on resources, other tasks, and event emitters.
14
18
  - **Resources**: Singleton objects providing shared functionality. They can be constants, services, functions. They can depend on other resources, tasks, and event emitters.
15
19
  - **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.
20
+ - **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
21
 
18
22
  These are the concepts and philosophy:
19
23
 
@@ -23,11 +27,11 @@ These are the concepts and philosophy:
23
27
  - **Explicit Registration**: All tasks, resources, events, and middleware have to be explicitly registered to be used.
24
28
  - **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
29
 
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.
30
+ Resources return their value to the container using the async `init()` function, making them available throughout the application.
27
31
 
28
- Tasks return through `async run()` function and the value from run, can be used throughout the application.
32
+ Tasks provide their output through the async `run()` function, allowing the results to be used across the application.
29
33
 
30
- All tasks, resources, events, and middleware have to be explicitly registered to be used. Registration can only be done in resources.
34
+ All tasks, resources, events, and middleware must be explicitly registered to be used. Registration can only be done within resources.
31
35
 
32
36
  ## Installation
33
37
 
@@ -38,7 +42,7 @@ npm install @bluelibs/runner
38
42
  ## Basic Usage
39
43
 
40
44
  ```typescript
41
- import { task, run, resource } from "@bluelibs/runner";
45
+ import { run, resource } from "@bluelibs/runner";
42
46
 
43
47
  const minimal = resource({
44
48
  async init() {
@@ -53,16 +57,16 @@ run(minimal).then((result) => {
53
57
 
54
58
  ## Resources and Tasks
55
59
 
56
- Resources are singletons. They can be constants, services, functions, etc. They can depend on other resources, tasks, and event emitters.
60
+ Resources are singletons and can include constants, services, functions, and more. They can depend on other resources, tasks, and event emitters.
57
61
 
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.
62
+ 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
63
 
60
64
  ```ts
61
65
  import { task, run, resource } from "@bluelibs/runner";
62
66
 
63
67
  const helloTask = task({
64
68
  id: "app.hello",
65
- run: async () => console.log("Hello World!"),
69
+ run: async () => "Hello World!",
66
70
  });
67
71
 
68
72
  const app = resource({
@@ -72,24 +76,26 @@ const app = resource({
72
76
  hello: helloTask,
73
77
  },
74
78
  async init(_, deps) {
75
- await deps.hello();
79
+ return await deps.hello();
76
80
  },
77
81
  });
82
+
83
+ const result = await run(app); // "Hello World!"
78
84
  ```
79
85
 
80
86
  ### When to use each?
81
87
 
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:
88
+ 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
89
 
84
90
  - "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
91
+ - "app.user.createComment" - this is a task, creates a comment, returns the comment maybe
86
92
  - "app.user.updateFriendList" - this task can be re-used from many other tasks or resources as necessary
87
93
 
88
94
  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
95
 
90
96
  ### Resource dispose()
91
97
 
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.
98
+ 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
99
 
94
100
  ```ts
95
101
  import { task, run, resource } from "@bluelibs/runner";
@@ -106,7 +112,7 @@ const dbResource = resource({
106
112
  });
107
113
  ```
108
114
 
109
- If you want to call dispose, you have to do it through the global resource called `store`, as everything is encapsulated.
115
+ 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
116
 
111
117
  ```ts
112
118
  import { task, run, resource, globals } from "@bluelibs/runner";
@@ -130,17 +136,19 @@ const value = await run(app);
130
136
  await value.dispose();
131
137
  ```
132
138
 
133
- ### Resource with()
139
+ ### Resource configuration
134
140
 
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.
141
+ 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
142
 
137
143
  ```ts
138
144
  import { task, run, resource } from "@bluelibs/runner";
139
145
 
140
146
  type Config = { smtpUrl: string; defaultFrom: string };
147
+
141
148
  const emailerResource = resource({
149
+ // automatic type inference.
142
150
  async init(config: Config) {
143
- // run config checks
151
+ // todo: perform config checks with a library like zod
144
152
  return {
145
153
  sendEmail: async (to: string, subject: string, body: string) => {
146
154
  // send *email*
@@ -176,10 +184,15 @@ const helloWorld = task({
176
184
  dependencies: {
177
185
  userRegisteredEvent,
178
186
  },
187
+ async run(_, deps) {
188
+ await deps.userRegisteredEvent();
189
+ return "Hello World!";
190
+ },
179
191
  });
180
192
 
181
193
  const app = resource({
182
194
  id: "app",
195
+ // You have to register everything you use.
183
196
  register: [helloWorld, logMiddleware],
184
197
  dependencies: {
185
198
  helloWorld,
@@ -192,14 +205,24 @@ const app = resource({
192
205
  run(app);
193
206
  ```
194
207
 
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.
208
+ 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.
209
+
210
+ Tasks, however, are not bound by this restriction; they can freely depend on each other as needed.
211
+
212
+ The dependencies get injected as follows:
196
213
 
197
- Tasks are not limited to this constraint, actions can use depend on each other freely.
214
+ | Component | Injection Description |
215
+ | ------------ | --------------------------------------------------------- |
216
+ | `tasks` | Injected as functions with their input argument |
217
+ | `resources` | Injected as their return value |
218
+ | `events` | Injected as functions with their payload argument |
219
+ | `middleware` | Not typically injected; used via a `middleware: []` array |
198
220
 
199
221
  ## Events
200
222
 
201
- You emit events when certain things in your app happen, a user registered, a comment has been added, etc.
202
- You listen to them through tasks and resources, and you can emit them from tasks and resources through `dependencies`.
223
+ 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.
224
+
225
+ You can listen for these events using tasks and resources, and similarly, emit them from tasks and resources through dependencies.
203
226
 
204
227
  ```ts
205
228
  import { task, run, event } from "@bluelibs/runner";
@@ -214,7 +237,6 @@ const root = resource({
214
237
  dependencies: {
215
238
  afterRegisterEvent,
216
239
  },
217
-
218
240
  async init(_, deps) {
219
241
  // the event becomes a function that you run with the propper payload
220
242
  await deps.afterRegisterEvent({ userId: string });
@@ -222,9 +244,9 @@ const root = resource({
222
244
  });
223
245
  ```
224
246
 
225
- There are only 2 ways to listen to events:
247
+ There are only 2 recommended ways to listen to events:
226
248
 
227
- ### `on` property
249
+ ### `task.on` property
228
250
 
229
251
  ```ts
230
252
  import { task, run, event } from "@bluelibs/runner";
@@ -236,7 +258,9 @@ const afterRegisterEvent = event<{ userId: string }>({
236
258
  const helloTask = task({
237
259
  id: "app.hello",
238
260
  on: afterRegisterEvent,
261
+ listenerPriority: 0, // this is the order in which the task will be executed when `on` is present
239
262
  run(event) {
263
+ event.source; // id which middleware, task, resource triggered it
240
264
  console.log("User has been registered!");
241
265
  },
242
266
  });
@@ -253,7 +277,7 @@ const app = resource({
253
277
  });
254
278
  ```
255
279
 
256
- ### `hooks` property
280
+ ### `resource.hooks` property
257
281
 
258
282
  This can only be applied to a `resource()`.
259
283
 
@@ -271,6 +295,7 @@ const root = resource({
271
295
  hooks: [
272
296
  {
273
297
  event: global.events.afterInit,
298
+ order: -1000, // event priority, the lower the number, the sooner it will run.
274
299
  async run(event, deps) {
275
300
  // both dependencies and event are properly infered through typescript
276
301
  console.log("User has been registered!");
@@ -278,12 +303,12 @@ const root = resource({
278
303
  },
279
304
  ],
280
305
  async init(_, deps) {
281
- deps.afterRegisterEvent({ userId: "XXX" });
306
+ await deps.afterRegisterEvent({ userId: "XXX" });
282
307
  },
283
308
  });
284
309
  ```
285
310
 
286
- ### hooks wildcard
311
+ #### wildcard events
287
312
 
288
313
  You can listen to all events by using the wildcard `*`.
289
314
 
@@ -324,11 +349,11 @@ The hooks from a `resource` are mostly used for configuration, and blending in t
324
349
  ### When to use either?
325
350
 
326
351
  - `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.
352
+ - `on` is for when you want to perform a task when something happens, like send an email, begin processing something, etc.
328
353
 
329
354
  ## Middleware
330
355
 
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.
356
+ 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
357
 
333
358
  ```ts
334
359
  import { task, resource, run, event } from "@bluelibs/runner";
@@ -365,7 +390,9 @@ const helloTask = task({
365
390
  });
366
391
  ```
367
392
 
368
- However, if you want to register a middleware for all tasks and resources, here's how you can do it:
393
+ ### Global
394
+
395
+ If you want to register a middleware for all tasks and resources, here's how you can do it:
369
396
 
370
397
  ```ts
371
398
  import { run, resource } from "@bluelibs/runner";
@@ -381,7 +408,7 @@ const root = resource({
381
408
  });
382
409
  ```
383
410
 
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.
411
+ 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
412
 
386
413
  ## Errors
387
414
 
@@ -417,7 +444,7 @@ You can listen to errors via events:
417
444
 
418
445
  ```ts
419
446
  const helloWorld = task({
420
- id: "app.onError",
447
+ id: "app.tasks.helloWorld.onError",
421
448
  on: helloWorld.events.onError,
422
449
  run({ error, input, suppress }, deps) {
423
450
  // this will be called when an error happens
@@ -428,7 +455,20 @@ const helloWorld = task({
428
455
  });
429
456
  ```
430
457
 
431
- ## Metadata
458
+ ```ts
459
+ const helloWorld = resource({
460
+ id: "app.resources.helloWorld.onError",
461
+ on: helloWorld.events.onError,
462
+ init({ error, input, suppress }, deps) {
463
+ // this will be called when an error happens
464
+
465
+ // if you handled the error, and you don't want it propagated to the top, supress the propagation.
466
+ suppress();
467
+ },
468
+ });
469
+ ```
470
+
471
+ ## Meta
432
472
 
433
473
  You can attach metadata to tasks, resources, events, and middleware.
434
474
 
@@ -448,7 +488,7 @@ const helloWorld = task({
448
488
  });
449
489
  ```
450
490
 
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.
491
+ 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
492
 
453
493
  The interfaces look like this:
454
494
 
@@ -467,7 +507,7 @@ export interface IMiddlewareMeta extends IMeta {}
467
507
 
468
508
  Which means you can extend them in your system to add more keys to better describe your actions.
469
509
 
470
- ## Global Services
510
+ ## Internal Services
471
511
 
472
512
  We expose direct access to the following internal services:
473
513
 
@@ -475,7 +515,7 @@ We expose direct access to the following internal services:
475
515
  - TaskRunner (can run tasks definitions directly and within D.I. context)
476
516
  - EventManager (can emit and listen to events)
477
517
 
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.
518
+ 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
519
 
480
520
  ```ts
481
521
  import { task, run, event, globals } from "@bluelibs/runner";
@@ -495,16 +535,20 @@ const helloWorld = task({
495
535
 
496
536
  ## Namespacing
497
537
 
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:
538
+ Domain usually is "app", but as your application grows or you plan on building external libraries the naming convention should be: "companyName.packageName".
501
539
 
502
- - `{companyName}.{packageName}.{taskName}`
540
+ | Type | Format |
541
+ | -------------- | ----------------------------------------- |
542
+ | Tasks | `{domain}.tasks.{taskName}` |
543
+ | Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
544
+ | Resources | `{domain}.resources.{resourceName}` |
545
+ | Events | `{domain}.events.{eventName}` |
546
+ | Middleware | `{domain}.middleware.{middlewareName}` |
503
547
 
504
548
  You can always create helpers for you as you're creating your tasks, resources, middleware:
505
549
 
506
550
  ```ts
507
- function getNamespace(id) {
551
+ function namespaced(id) {
508
552
  return `bluelibs.core.${id}`;
509
553
  }
510
554
  ```
@@ -546,9 +590,11 @@ Now you can freely use any of the tasks, resources, events, and middlewares from
546
590
 
547
591
  This approach is very powerful when you have multiple packages and you want to compose them together.
548
592
 
549
- ## Real world examples
593
+ ## Real life
550
594
 
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.
595
+ Or is it just fantasy?
596
+
597
+ 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
598
 
553
599
  ```ts
554
600
  import { task, resource, run, event } from "@bluelibs/runner";
@@ -588,11 +634,11 @@ const app = resource({
588
634
  run();
589
635
  ```
590
636
 
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.
637
+ 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
638
 
593
639
  ### Business config
594
640
 
595
- There's a resource for that! You can define a resource that holds your business configuration.
641
+ Or just simple config, you can do it for your business logic, environment variables, etc.
596
642
 
597
643
  ```ts
598
644
  import { resource, run } from "@bluelibs/runner";
@@ -661,7 +707,7 @@ const app = resource({
661
707
  run(app);
662
708
  ```
663
709
 
664
- ### Business Config
710
+ ### Resource level
665
711
 
666
712
  ```ts
667
713
  import { task, run, event } from "@bluelibs/runner";
@@ -705,7 +751,7 @@ const app = resource({
705
751
  run(app);
706
752
  ```
707
753
 
708
- ### Moving further
754
+ ## Advanced Usage
709
755
 
710
756
  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
757
 
@@ -716,15 +762,19 @@ You can add many services or external things into the runner ecosystem with thin
716
762
  ```ts
717
763
  import { task, run, event } from "@bluelibs/runner";
718
764
 
719
- const expressResource = resource<express.Application>({
765
+ // proxy declaration pattern
766
+ const expressResource = resource({
720
767
  id: "app.helloWorld",
721
- run: async (config) => config,
768
+ run: async (app: express.Application) => app,
722
769
  });
723
770
 
724
771
  const app = resource({
725
772
  id: "app",
726
773
  register: [expressResource.with(express())],
727
- init: async (express) => {
774
+ dependencies: {
775
+ express: expressResource,
776
+ },
777
+ init: async (_, { express }) => {
728
778
  express.get("/", (req, res) => {
729
779
  res.send("Hello World!");
730
780
  });
@@ -734,12 +784,16 @@ const app = resource({
734
784
  run(app);
735
785
  ```
736
786
 
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:
787
+ 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
788
 
739
789
  ```ts
790
+ type Config = {
791
+ port: number;
792
+ };
793
+
740
794
  const expressResource = resource({
741
795
  id: "app.helloWorld",
742
- run: async (config) => {
796
+ init: async (config: Config) => {
743
797
  const app = express();
744
798
  app.listen(config.port);
745
799
  return app;
@@ -749,7 +803,10 @@ const expressResource = resource({
749
803
  const app = resource({
750
804
  id: "app",
751
805
  register: [expressResource.with({ port: 3000 })],
752
- init: async (express) => {
806
+ dependencies: {
807
+ express: expressResource,
808
+ },
809
+ init: async (_, { express }) => {
753
810
  // type is automagically infered.
754
811
  express.get("/", (req, res) => {
755
812
  res.send("Hello World!");
@@ -762,11 +819,31 @@ run(app);
762
819
 
763
820
  ### Inter-communication between resources
764
821
 
765
- 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.
822
+ 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.
823
+
824
+ 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:
825
+
826
+ ```ts
827
+ type SecurityResourceConfig = {
828
+ hasher: (str: string) => string;
829
+ };
830
+
831
+ const securityResource = resource({
832
+ id: "app.security",
833
+ async init(config: SecurityResourceConfig) {
834
+ return {
835
+ hash: (input: string) => config.hasher(input),
836
+ };
837
+ },
838
+ });
766
839
 
767
- This means your `resource` needs to provide a way for other resources to `update` it. The most obvious way is to expose a configuration that allows you to set a custom hasher `register: [securityResource.with({ ... })]`.
840
+ const app = resource({
841
+ id: "app",
842
+ register: [securityResource.with({ hasher: (input) => md5(input) })],
843
+ });
844
+ ```
768
845
 
769
- But other resources might want to do this dynamically as extensions. This is where `hooks` come in.
846
+ However, other resources might need to modify this dynamically as extensions. This is where hooks become valuable.
770
847
 
771
848
  ```ts
772
849
  import { resource, run, event } from "@bluelibs/runner";
@@ -793,6 +870,7 @@ const app = resource({
793
870
  register: [securityResource],
794
871
  hooks: [
795
872
  {
873
+ // 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
874
  event: securityResource.events.afterInit,
797
875
  async run(event, deps) {
798
876
  const { config, value } = event.data;
@@ -807,7 +885,7 @@ const app = resource({
807
885
  });
808
886
  ```
809
887
 
810
- Another approach is to create a new event that holds the config and it allows it to be updated.
888
+ Another approach is to create a new event that contains the configuration, providing the flexibility to update it as needed.
811
889
 
812
890
  ```ts
813
891
  import { resource, run, event } from "@bluelibs/runner";
@@ -824,6 +902,7 @@ const securityResource = resource({
824
902
  async init(config: SecurityOptions) {
825
903
  // Give the ability to other listeners to modify the configuration
826
904
  securityConfigurationPhaseEvent(config);
905
+ Objecte.freeze(config);
827
906
 
828
907
  return {
829
908
  // ... based on config
@@ -846,9 +925,9 @@ const app = resource({
846
925
  });
847
926
  ```
848
927
 
849
- ## Overrides
928
+ ### Overrides
850
929
 
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.
930
+ 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
931
 
853
932
  ```ts
854
933
  import { resource, run, event } from "@bluelibs/runner";
@@ -875,15 +954,13 @@ const app = resource({
875
954
  });
876
955
  ```
877
956
 
878
- Now the `securityResource` will be overriden by the new one and whenever it's used it will use the new one.
957
+ The new securityResource will replace the existing one, ensuring all future references point to the updated version.
879
958
 
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.
959
+ 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
960
 
882
961
  ## Logging
883
962
 
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.
963
+ 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
964
 
888
965
  ```ts
889
966
  import { task, run, event, globals } from "@bluelibs/runner";
@@ -913,19 +990,23 @@ const helloWorld = task({
913
990
 
914
991
  ### Print logs
915
992
 
916
- Logs don't get printed by default in this system.
993
+ 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.
994
+
995
+ To showcase the versatility of the system, here are some ways you could do it:
917
996
 
918
997
  ```ts
919
998
  import { task, run, event, globals, resource } from "@bluelibs/runner";
920
999
 
1000
+ const { logger } = globals.resources;
1001
+
921
1002
  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);
1003
+ id: "app.task.updatePrintThreshold",
1004
+ on: logger.events.afterInit,
1005
+ // Note: logger is
1006
+ run: async (event, deps) => {
1007
+ const logger = event.data.value;
1008
+ logger.setPrintThreshold("trace"); // will print all logs
1009
+ logger.setPrintThreshold("error"); // will print only "error" and "critical" logs
929
1010
  },
930
1011
  });
931
1012
 
@@ -937,17 +1018,79 @@ const app = resource({
937
1018
  // Now your app will print all logs
938
1019
  ```
939
1020
 
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.
1021
+ You can also achieve this using hooks:
1022
+
1023
+ ```ts
1024
+ resource({
1025
+ id: "root",
1026
+ hooks: [
1027
+ {
1028
+ // after logger gets initialised as a resource, I'm going to set the print threshold
1029
+ event: logger.events.afterInit,
1030
+ async run(event) {
1031
+ const logger = event.data; // do not depend on the logger
1032
+ logger.setPrintThreshold("trace");
1033
+ },
1034
+ },
1035
+ ],
1036
+ });
1037
+ ```
1038
+
1039
+ 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.
1040
+
1041
+ 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.
1042
+
1043
+ ```ts
1044
+ import { task, run, event, globals } from "@bluelibs/runner";
1045
+
1046
+ const { logger } = globals.resources;
1047
+
1048
+ const shipLogsToWarehouse = task({
1049
+ id: "app.task.shipLogsToWarehouse",
1050
+ on: logger.events.log,
1051
+ dependencies: {
1052
+ warehouseService: warehouseServiceResource,
1053
+ },
1054
+ run: async (event, deps) => {
1055
+ const log = event.data; // ILog
1056
+ if (log.level === "error" || log.level === "critical") {
1057
+ // Ensure no extra log() calls are made here to prevent infinite loops
1058
+ await deps.warehouseService.push(log);
1059
+ }
1060
+ },
1061
+ });
1062
+ ```
1063
+
1064
+ And yes, this would also work:
941
1065
 
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`
1066
+ ```ts
1067
+ const task = task({
1068
+ id: "app.task.logSomething",
1069
+ dependencies: {
1070
+ log: globals.events.log,
1071
+ },
1072
+ run: async (_, { log }) => {
1073
+ await log({
1074
+ level: "info",
1075
+ data: { anything: "you want" };
1076
+ timestamp: new Date();
1077
+ context: "app.task.logSomething"; // optional
1078
+ })
1079
+ },
1080
+ });
1081
+ ```
1082
+
1083
+ 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
1084
 
944
1085
  ## Testing
945
1086
 
1087
+ 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.
1088
+
946
1089
  ### Unit Testing
947
1090
 
948
- You can easily test your resources and tasks by running them in a test environment.
1091
+ You can easily test your middleware, resources and tasks by running them in a test environment.
949
1092
 
950
- The only bits that you need to test are the `run` function and the `init` functions with the propper dependencies.
1093
+ The only components you need to test are the run function and the init functions, along with their proper dependencies.
951
1094
 
952
1095
  ```ts
953
1096
  import { task, resource } from "@bluelibs/runner";
@@ -985,7 +1128,7 @@ describe("app.helloWorldResource", () => {
985
1128
 
986
1129
  ### Integration
987
1130
 
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.
1131
+ 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
1132
 
990
1133
  ```ts
991
1134
  import { task, resource, run, global } from "@bluelibs/runner";
@@ -1003,7 +1146,7 @@ const app = resource({
1003
1146
  });
1004
1147
  ```
1005
1148
 
1006
- Then your tests can now be cleaner:
1149
+ Then your tests can now be cleaner, as you can use `overrides` and a wrapper resource to mock your task.
1007
1150
 
1008
1151
  ```ts
1009
1152
  describe("app", () => {
@@ -1011,14 +1154,12 @@ describe("app", () => {
1011
1154
  const testApp = resource({
1012
1155
  id: "app.test",
1013
1156
  register: [myApp], // wrap your existing app
1014
- overrides: [override], // apply the overrides
1157
+ overrides: [override], // apply the overrides for "app.myTask"
1015
1158
  init: async (_, deps) => {
1016
1159
  // you can now test a task simply by depending on it, and running it, then asserting the response of run()
1017
1160
  },
1018
1161
  });
1019
1162
 
1020
- // Same concept applies for resources as well.
1021
-
1022
1163
  await run(testApp);
1023
1164
  });
1024
1165
  });