@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.
- package/README.md +191 -76
- package/dist/defs.d.ts +31 -7
- package/dist/examples/express-mongo/index.d.ts +0 -0
- package/dist/examples/express-mongo/index.js +3 -0
- package/dist/examples/express-mongo/index.js.map +1 -0
- package/dist/globalEvents.js +1 -0
- package/dist/globalEvents.js.map +1 -1
- package/dist/models/DependencyProcessor.d.ts +4 -2
- package/dist/models/DependencyProcessor.js +33 -15
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Logger.d.ts +15 -10
- package/dist/models/Logger.js +27 -15
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/ResourceInitializer.d.ts +3 -1
- package/dist/models/ResourceInitializer.js +4 -4
- package/dist/models/ResourceInitializer.js.map +1 -1
- package/dist/models/Store.d.ts +4 -1
- package/dist/models/Store.js +17 -3
- package/dist/models/Store.js.map +1 -1
- package/dist/models/TaskRunner.d.ts +3 -1
- package/dist/models/TaskRunner.js +7 -1
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/run.d.ts +0 -8
- package/dist/run.js +8 -6
- package/dist/run.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/index.ts +1 -0
- package/src/__tests__/models/Logger.test.ts +44 -1
- package/src/__tests__/models/ResourceInitializer.test.ts +5 -4
- package/src/__tests__/models/Store.test.ts +4 -2
- package/src/__tests__/models/TaskRunner.test.ts +5 -2
- package/src/__tests__/run.hooks.test.ts +0 -31
- package/src/__tests__/run.middleware.test.ts +26 -0
- package/src/__tests__/typesafety.test.ts +127 -0
- package/src/defs.ts +47 -15
- package/src/examples/express-mongo/index.ts +1 -0
- package/src/globalEvents.ts +1 -0
- package/src/models/DependencyProcessor.ts +72 -37
- package/src/models/EventManager.ts +1 -0
- package/src/models/Logger.ts +36 -16
- package/src/models/ResourceInitializer.ts +5 -4
- package/src/models/Store.ts +20 -3
- package/src/models/TaskRunner.ts +8 -1
- 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
|
|
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
|
|
26
|
+
Resources return their value to the container using the async `init()` function, making them available throughout the application.
|
|
27
27
|
|
|
28
|
-
Tasks
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 () =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
135
|
+
### Resource configuration
|
|
134
136
|
|
|
135
|
-
Resources can be
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
587
|
+
## Real life
|
|
550
588
|
|
|
551
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
759
|
+
// proxy declaration pattern
|
|
760
|
+
const expressResource = resource({
|
|
720
761
|
id: "app.helloWorld",
|
|
721
|
-
run: async (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
902
|
+
### Overrides
|
|
850
903
|
|
|
851
|
-
Previously, we
|
|
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
|
-
|
|
931
|
+
The new securityResource will replace the existing one, ensuring all future references point to the updated version.
|
|
879
932
|
|
|
880
|
-
Overrides
|
|
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
|
|
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.
|
|
923
|
-
on:
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
logger.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|