@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.
- package/README.md +223 -82
- package/dist/defs.d.ts +41 -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 +7 -5
- package/dist/models/DependencyProcessor.js +46 -25
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.d.ts +1 -1
- package/dist/models/EventManager.js +2 -2
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Logger.d.ts +16 -11
- package/dist/models/Logger.js +29 -17
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/ResourceInitializer.d.ts +3 -1
- package/dist/models/ResourceInitializer.js +10 -10
- 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 +13 -7
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/run.d.ts +0 -8
- package/dist/run.js +11 -8
- package/dist/run.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/index.ts +1 -0
- package/src/__tests__/models/EventManager.test.ts +21 -21
- package/src/__tests__/models/Logger.test.ts +50 -5
- package/src/__tests__/models/ResourceInitializer.test.ts +61 -25
- 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 +57 -15
- package/src/examples/express-mongo/index.ts +1 -0
- package/src/globalEvents.ts +1 -0
- package/src/models/DependencyProcessor.ts +103 -47
- package/src/models/EventManager.ts +4 -2
- package/src/models/Logger.ts +39 -19
- package/src/models/ResourceInitializer.ts +53 -27
- package/src/models/Store.ts +20 -3
- package/src/models/TaskRunner.ts +45 -18
- 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://
|
|
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
|
-
|
|
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
|
|
30
|
+
Resources return their value to the container using the async `init()` function, making them available throughout the application.
|
|
27
31
|
|
|
28
|
-
Tasks
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 () =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
139
|
+
### Resource configuration
|
|
134
140
|
|
|
135
|
-
Resources can be
|
|
141
|
+
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
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
593
|
+
## Real life
|
|
550
594
|
|
|
551
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
765
|
+
// proxy declaration pattern
|
|
766
|
+
const expressResource = resource({
|
|
720
767
|
id: "app.helloWorld",
|
|
721
|
-
run: async (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
840
|
+
const app = resource({
|
|
841
|
+
id: "app",
|
|
842
|
+
register: [securityResource.with({ hasher: (input) => md5(input) })],
|
|
843
|
+
});
|
|
844
|
+
```
|
|
768
845
|
|
|
769
|
-
|
|
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
|
|
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
|
-
|
|
928
|
+
### Overrides
|
|
850
929
|
|
|
851
|
-
Previously, we
|
|
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
|
-
|
|
957
|
+
The new securityResource will replace the existing one, ensuring all future references point to the updated version.
|
|
879
958
|
|
|
880
|
-
Overrides
|
|
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
|
|
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.
|
|
923
|
-
on:
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
logger.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|