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