@bluelibs/runner 2.2.4 → 3.1.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 +1409 -935
- package/dist/common.types.d.ts +20 -0
- package/dist/common.types.js +4 -0
- package/dist/common.types.js.map +1 -0
- package/dist/context.d.ts +34 -0
- package/dist/context.js +58 -0
- package/dist/context.js.map +1 -0
- package/dist/define.d.ts +24 -5
- package/dist/define.js +89 -20
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +109 -73
- package/dist/defs.js +12 -2
- package/dist/defs.js.map +1 -1
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +6 -5
- package/dist/errors.js.map +1 -1
- package/dist/event.types.d.ts +18 -0
- package/dist/event.types.js +4 -0
- package/dist/event.types.js.map +1 -0
- package/dist/examples/registrator-example.d.ts +122 -0
- package/dist/examples/registrator-example.js +147 -0
- package/dist/examples/registrator-example.js.map +1 -0
- package/dist/globals/globalEvents.d.ts +41 -0
- package/dist/globals/globalEvents.js +94 -0
- package/dist/globals/globalEvents.js.map +1 -0
- package/dist/globals/globalMiddleware.d.ts +23 -0
- package/dist/globals/globalMiddleware.js +15 -0
- package/dist/globals/globalMiddleware.js.map +1 -0
- package/dist/globals/globalResources.d.ts +27 -0
- package/dist/globals/globalResources.js +47 -0
- package/dist/globals/globalResources.js.map +1 -0
- package/dist/globals/middleware/cache.middleware.d.ts +34 -0
- package/dist/globals/middleware/cache.middleware.js +85 -0
- package/dist/globals/middleware/cache.middleware.js.map +1 -0
- package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
- package/dist/globals/middleware/requireContext.middleware.js +25 -0
- package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
- package/dist/globals/middleware/retry.middleware.d.ts +20 -0
- package/dist/globals/middleware/retry.middleware.js +34 -0
- package/dist/globals/middleware/retry.middleware.js.map +1 -0
- package/dist/globals/resources/queue.resource.d.ts +7 -0
- package/dist/globals/resources/queue.resource.js +31 -0
- package/dist/globals/resources/queue.resource.js.map +1 -0
- package/dist/index.d.ts +54 -18
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/middleware.types.d.ts +40 -0
- package/dist/middleware.types.js +4 -0
- package/dist/middleware.types.js.map +1 -0
- package/dist/models/DependencyProcessor.d.ts +6 -5
- package/dist/models/DependencyProcessor.js +13 -15
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.d.ts +9 -4
- package/dist/models/EventManager.js +44 -2
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Logger.d.ts +30 -13
- package/dist/models/Logger.js +132 -54
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/OverrideManager.d.ts +13 -0
- package/dist/models/OverrideManager.js +70 -0
- package/dist/models/OverrideManager.js.map +1 -0
- package/dist/models/Queue.d.ts +25 -0
- package/dist/models/Queue.js +54 -0
- package/dist/models/Queue.js.map +1 -0
- package/dist/models/ResourceInitializer.d.ts +5 -2
- package/dist/models/ResourceInitializer.js +22 -14
- package/dist/models/ResourceInitializer.js.map +1 -1
- package/dist/models/Semaphore.d.ts +61 -0
- package/dist/models/Semaphore.js +166 -0
- package/dist/models/Semaphore.js.map +1 -0
- package/dist/models/Store.d.ts +18 -73
- package/dist/models/Store.js +71 -269
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreConstants.d.ts +11 -0
- package/dist/models/StoreConstants.js +18 -0
- package/dist/models/StoreConstants.js.map +1 -0
- package/dist/models/StoreRegistry.d.ts +25 -0
- package/dist/models/StoreRegistry.js +171 -0
- package/dist/models/StoreRegistry.js.map +1 -0
- package/dist/models/StoreTypes.d.ts +21 -0
- package/dist/models/StoreTypes.js +3 -0
- package/dist/models/StoreTypes.js.map +1 -0
- package/dist/models/StoreValidator.d.ts +10 -0
- package/dist/models/StoreValidator.js +41 -0
- package/dist/models/StoreValidator.js.map +1 -0
- package/dist/models/TaskRunner.d.ts +1 -1
- package/dist/models/TaskRunner.js +39 -24
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/models/VarStore.d.ts +17 -0
- package/dist/models/VarStore.js +60 -0
- package/dist/models/VarStore.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/index.js.map +1 -1
- package/dist/resource.types.d.ts +31 -0
- package/dist/resource.types.js +3 -0
- package/dist/resource.types.js.map +1 -0
- package/dist/run.d.ts +4 -1
- package/dist/run.js +6 -3
- package/dist/run.js.map +1 -1
- package/dist/symbols.d.ts +24 -0
- package/dist/symbols.js +29 -0
- package/dist/symbols.js.map +1 -0
- package/dist/task.types.d.ts +55 -0
- package/dist/task.types.js +23 -0
- package/dist/task.types.js.map +1 -0
- package/dist/tools/getCallerFile.d.ts +9 -1
- package/dist/tools/getCallerFile.js +41 -0
- package/dist/tools/getCallerFile.js.map +1 -1
- package/dist/tools/registratorId.d.ts +4 -0
- package/dist/tools/registratorId.js +40 -0
- package/dist/tools/registratorId.js.map +1 -0
- package/dist/tools/simpleHash.d.ts +9 -0
- package/dist/tools/simpleHash.js +34 -0
- package/dist/tools/simpleHash.js.map +1 -0
- package/dist/types/base-interfaces.d.ts +18 -0
- package/dist/types/base-interfaces.js +6 -0
- package/dist/types/base-interfaces.js.map +1 -0
- package/dist/types/base.d.ts +13 -0
- package/dist/types/base.js +3 -0
- package/dist/types/base.js.map +1 -0
- package/dist/types/dependencies.d.ts +22 -0
- package/dist/types/dependencies.js +3 -0
- package/dist/types/dependencies.js.map +1 -0
- package/dist/types/dependency-core.d.ts +14 -0
- package/dist/types/dependency-core.js +5 -0
- package/dist/types/dependency-core.js.map +1 -0
- package/dist/types/events.d.ts +52 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/hooks.d.ts +16 -0
- package/dist/types/hooks.js +5 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.js +27 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/meta.d.ts +13 -0
- package/dist/types/meta.js +5 -0
- package/dist/types/meta.js.map +1 -0
- package/dist/types/middleware.d.ts +38 -0
- package/dist/types/middleware.js +6 -0
- package/dist/types/middleware.js.map +1 -0
- package/dist/types/registerable.d.ts +10 -0
- package/dist/types/registerable.js +5 -0
- package/dist/types/registerable.js.map +1 -0
- package/dist/types/resources.d.ts +44 -0
- package/dist/types/resources.js +5 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/symbols.d.ts +24 -0
- package/dist/types/symbols.js +30 -0
- package/dist/types/symbols.js.map +1 -0
- package/dist/types/tasks.d.ts +41 -0
- package/dist/types/tasks.js +5 -0
- package/dist/types/tasks.js.map +1 -0
- package/dist/types/utilities.d.ts +7 -0
- package/dist/types/utilities.js +5 -0
- package/dist/types/utilities.js.map +1 -0
- package/package.json +10 -6
- package/src/__tests__/benchmark/benchmark.test.ts +1 -1
- package/src/__tests__/context.test.ts +91 -0
- package/src/__tests__/errors.test.ts +8 -5
- package/src/__tests__/globalEvents.test.ts +1 -1
- package/src/__tests__/globals/cache.middleware.test.ts +772 -0
- package/src/__tests__/globals/queue.resource.test.ts +141 -0
- package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
- package/src/__tests__/globals/retry.middleware.test.ts +157 -0
- package/src/__tests__/index.helper.test.ts +55 -0
- package/src/__tests__/models/EventManager.test.ts +157 -11
- package/src/__tests__/models/Logger.test.ts +291 -34
- package/src/__tests__/models/Queue.test.ts +189 -0
- package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
- package/src/__tests__/models/Semaphore.test.ts +713 -0
- package/src/__tests__/models/Store.test.ts +40 -0
- package/src/__tests__/models/TaskRunner.test.ts +86 -5
- package/src/__tests__/run.anonymous.test.ts +679 -0
- package/src/__tests__/run.middleware.test.ts +312 -12
- package/src/__tests__/run.overrides.test.ts +13 -10
- package/src/__tests__/run.test.ts +364 -13
- package/src/__tests__/setOutput.test.ts +244 -0
- package/src/__tests__/tools/getCallerFile.test.ts +124 -9
- package/src/__tests__/typesafety.test.ts +71 -41
- package/src/context.ts +86 -0
- package/src/define.ts +129 -34
- package/src/defs.ts +156 -119
- package/src/errors.ts +15 -10
- package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
- package/src/globals/globalMiddleware.ts +14 -0
- package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
- package/src/globals/middleware/cache.middleware.ts +115 -0
- package/src/globals/middleware/requireContext.middleware.ts +36 -0
- package/src/globals/middleware/retry.middleware.ts +56 -0
- package/src/globals/resources/queue.resource.ts +34 -0
- package/src/index.ts +9 -5
- package/src/models/DependencyProcessor.ts +42 -49
- package/src/models/EventManager.ts +64 -13
- package/src/models/Logger.ts +181 -64
- package/src/models/OverrideManager.ts +84 -0
- package/src/models/Queue.ts +66 -0
- package/src/models/ResourceInitializer.ts +40 -20
- package/src/models/Semaphore.ts +208 -0
- package/src/models/Store.ts +94 -342
- package/src/models/StoreConstants.ts +17 -0
- package/src/models/StoreRegistry.ts +228 -0
- package/src/models/StoreTypes.ts +46 -0
- package/src/models/StoreValidator.ts +43 -0
- package/src/models/TaskRunner.ts +54 -41
- package/src/models/index.ts +3 -0
- package/src/run.ts +7 -4
- package/src/tools/getCallerFile.ts +54 -2
- package/src/__tests__/index.ts +0 -15
- package/src/examples/express-mongo/index.ts +0 -1
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
# BlueLibs Runner
|
|
1
|
+
# BlueLibs Runner: The Framework That Actually Makes Sense
|
|
2
|
+
|
|
3
|
+
_Or: How I Learned to Stop Worrying and Love Dependency Injection_
|
|
2
4
|
|
|
3
5
|
<p align="center">
|
|
4
6
|
<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>
|
|
@@ -7,1336 +9,1808 @@
|
|
|
7
9
|
<a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
|
|
8
10
|
</p>
|
|
9
11
|
|
|
10
|
-
- [View the documentation page here](https://bluelibs.github.io/runner/)
|
|
11
|
-
- [Google Notebook LM Podcast](https://notebooklm.google.com/notebook/59bd49fa-346b-4cfb-bb4b-b59857c3b9b4/audio)
|
|
12
|
-
- [Continue GPT Conversation](https://chatgpt.com/share/670392f8-7188-800b-9b4b-e49b437d77f7)
|
|
13
|
-
|
|
14
|
-
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.
|
|
15
|
-
|
|
16
|
-
## Building Blocks
|
|
17
|
-
|
|
18
|
-
- **Tasks**: Core units of logic that encapsulate specific tasks. They can depend on resources, other tasks, and event emitters.
|
|
19
|
-
- **Resources**: Singleton objects providing shared functionality. They can be constants, services, functions. They can depend on other resources, tasks, and event emitters.
|
|
20
|
-
- **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.
|
|
21
|
-
- **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.
|
|
12
|
+
- [View the documentation page here](https://bluelibs.github.io/runner/)
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
Welcome to BlueLibs Runner, where we've taken the chaos of modern application architecture and turned it into something that won't make you question your life choices at 3am. This isn't just another framework – it's your new best friend who actually understands that code should be readable, testable, and not require a PhD in abstract nonsense to maintain.
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
- **Type safety**: Built with TypeScript for enhanced developer experience and type-safety everywhere, no more type mistakes.
|
|
27
|
-
- **Functional**: We use functions and objects instead of classes for DI. This is a functional approach to building applications.
|
|
28
|
-
- **Explicit Registration**: All tasks, resources, events, and middleware have to be explicitly registered to be used.
|
|
29
|
-
- **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.
|
|
16
|
+
## What Is This Thing?
|
|
30
17
|
|
|
31
|
-
|
|
18
|
+
BlueLibs Runner is a TypeScript-first framework that embraces functional programming principles while keeping dependency injection simple enough that you won't need a flowchart to understand your own code. Think of it as the anti-framework framework – it gets out of your way and lets you build stuff that actually works.
|
|
32
19
|
|
|
33
|
-
|
|
20
|
+
### The Core Philosophy (AKA Why We Built This)
|
|
34
21
|
|
|
35
|
-
|
|
22
|
+
- **Tasks are functions** - Not classes with 47 methods you'll never use
|
|
23
|
+
- **Resources are singletons** - Database connections, configs, services - the usual suspects
|
|
24
|
+
- **Events are just events** - Revolutionary concept, we know
|
|
25
|
+
- **Everything is async** - Because it's 2025 and blocking code is so 2005
|
|
26
|
+
- **Explicit beats implicit** - No magic, no surprises, no "how the hell does this work?"
|
|
36
27
|
|
|
37
|
-
##
|
|
28
|
+
## Quick Start (The "Just Show Me Code" Section)
|
|
38
29
|
|
|
39
30
|
```bash
|
|
40
31
|
npm install @bluelibs/runner
|
|
41
32
|
```
|
|
42
33
|
|
|
43
|
-
|
|
34
|
+
Here's a complete Express server in less lines than most frameworks need for their "Hello World":
|
|
44
35
|
|
|
45
36
|
```typescript
|
|
46
|
-
import
|
|
37
|
+
import express from "express";
|
|
38
|
+
import { resource, task, run } from "@bluelibs/runner";
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
// A resource is anything you want to share across your app
|
|
41
|
+
const server = resource({
|
|
42
|
+
id: "app.server",
|
|
43
|
+
init: async (config: { port: number }) => {
|
|
44
|
+
const app = express();
|
|
45
|
+
const server = app.listen(config.port);
|
|
46
|
+
console.log(`Server running on port ${config.port}`);
|
|
47
|
+
return { app, server };
|
|
51
48
|
},
|
|
49
|
+
dispose: async ({ server }) => server.close(),
|
|
52
50
|
});
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
// Tasks are your business logic - pure-ish, easily testable functions
|
|
53
|
+
const createUser = task({
|
|
54
|
+
id: "app.tasks.createUser",
|
|
55
|
+
dependencies: { server },
|
|
56
|
+
run: async (userData: { name: string }, { server }) => {
|
|
57
|
+
// Your actual business logic here
|
|
58
|
+
return { id: "user-123", ...userData };
|
|
59
|
+
},
|
|
56
60
|
});
|
|
57
|
-
```
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
// Wire everything together
|
|
63
|
+
const app = resource({
|
|
64
|
+
id: "app",
|
|
65
|
+
// Here you make the system aware of resources, tasks, middleware, and events.
|
|
66
|
+
register: [server.with({ port: 3000 }), createUser],
|
|
67
|
+
dependencies: { server, createUser },
|
|
68
|
+
init: async (_, { server, createUser }) => {
|
|
69
|
+
server.app.post("/users", async (req, res) => {
|
|
70
|
+
const user = await createUser(req.body);
|
|
71
|
+
res.json(user);
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
});
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
// That's it. No webpack configs, no decorators, no XML.
|
|
77
|
+
const { dispose } = await run(app);
|
|
78
|
+
```
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
## The Big Four: Your New Building Blocks
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
import { task, run, resource } from "@bluelibs/runner";
|
|
82
|
+
### 1. Tasks: The Heart of Your Business Logic
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
id: "app.hello",
|
|
70
|
-
run: async () => "Hello World!",
|
|
71
|
-
});
|
|
84
|
+
Tasks are functions with superpowers. They're pure-ish, testable, and composable. Unlike classes that accumulate methods like a hoarder accumulates stuff, tasks do one thing well.
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
dependencies: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return await deps.hello();
|
|
86
|
+
```typescript
|
|
87
|
+
const sendEmail = task({
|
|
88
|
+
id: "app.tasks.sendEmail",
|
|
89
|
+
dependencies: { emailService, logger },
|
|
90
|
+
run: async ({ to, subject, body }: EmailData, { emailService, logger }) => {
|
|
91
|
+
await logger.info(`Sending email to ${to}`);
|
|
92
|
+
return await emailService.send({ to, subject, body });
|
|
81
93
|
},
|
|
82
94
|
});
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
// Test it like a normal function (because it basically is)
|
|
97
|
+
const result = await sendEmail.run(
|
|
98
|
+
{ to: "user@example.com", subject: "Hi", body: "Hello!" },
|
|
99
|
+
{ emailService: mockEmailService, logger: mockLogger }
|
|
100
|
+
);
|
|
85
101
|
```
|
|
86
102
|
|
|
87
|
-
|
|
103
|
+
#### When to Task and When Not to Task
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
Look, we get it. You could turn every function into a task, but that's like using a sledgehammer to crack nuts. Here's the deal:
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
- "app.user.createComment" - this is a task, creates a comment, returns the comment maybe
|
|
93
|
-
- "app.user.updateFriendList" - this task can be re-used from many other tasks or resources as necessary
|
|
107
|
+
**Make it a task when:**
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
- It's a high-level business action: `"app.user.register"`, `"app.order.process"`
|
|
110
|
+
- You want it trackable and observable
|
|
111
|
+
- Multiple parts of your app need it
|
|
112
|
+
- It's complex enough to benefit from dependency injection
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
**Don't make it a task when:**
|
|
98
115
|
|
|
99
|
-
|
|
116
|
+
- It's a simple utility function
|
|
117
|
+
- It's used in only one place or to help other tasks
|
|
118
|
+
- It's performance-critical and doesn't need DI overhead
|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
import { task, run, resource } from "@bluelibs/runner";
|
|
120
|
+
Think of tasks as the "main characters" in your application story, not every single line of dialogue.
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
async init(config, deps) {
|
|
106
|
-
const db = await connectToDatabase();
|
|
107
|
-
return db;
|
|
108
|
-
},
|
|
109
|
-
// the value returned from init() will be passed to dispose()
|
|
110
|
-
async dispose(db, config, deps) {
|
|
111
|
-
return db.close();
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
```
|
|
122
|
+
### 2. Resources: Your Singletons That Don't Suck
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
Resources are the services, configs, and connections that live throughout your app's lifecycle. They initialize once and stick around until cleanup time. They have to be registered (via `register: []`) only once before they can be used.
|
|
117
125
|
|
|
118
|
-
```
|
|
119
|
-
|
|
126
|
+
```typescript
|
|
127
|
+
const database = resource({
|
|
128
|
+
id: "app.db",
|
|
129
|
+
init: async () => {
|
|
130
|
+
const client = new MongoClient(process.env.DATABASE_URL as string);
|
|
131
|
+
await client.connect();
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
id: "app",
|
|
123
|
-
register: [dbResource],
|
|
124
|
-
dependencies: {
|
|
125
|
-
store: globals.resources.store,
|
|
126
|
-
},
|
|
127
|
-
async init(_, deps) {
|
|
128
|
-
// We use the fact that we can reuse the value we got from here
|
|
129
|
-
return {
|
|
130
|
-
dispose: async () => deps.store.dispose(),
|
|
131
|
-
};
|
|
133
|
+
return client;
|
|
132
134
|
},
|
|
135
|
+
dispose: async (client) => await client.close(),
|
|
133
136
|
});
|
|
134
137
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
const userService = resource({
|
|
139
|
+
id: "app.services.user",
|
|
140
|
+
dependencies: { database },
|
|
141
|
+
init: async (_, { database }) => ({
|
|
142
|
+
async createUser(userData: UserData) {
|
|
143
|
+
return database.collection("users").insertOne(userData);
|
|
144
|
+
},
|
|
145
|
+
async getUser(id: string) {
|
|
146
|
+
return database.collection("users").findOne({ _id: id });
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
138
150
|
```
|
|
139
151
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
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.
|
|
152
|
+
#### Resource Configuration: Because Hardcoding Is for Amateurs
|
|
143
153
|
|
|
144
|
-
|
|
145
|
-
import { task, run, resource } from "@bluelibs/runner";
|
|
154
|
+
Resources can be configured with type-safe options. No more "config object of unknown shape" nonsense.
|
|
146
155
|
|
|
147
|
-
|
|
156
|
+
```typescript
|
|
157
|
+
type SMTPConfig = {
|
|
158
|
+
smtpUrl: string;
|
|
159
|
+
from: string;
|
|
160
|
+
};
|
|
148
161
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
async
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
},
|
|
162
|
+
const emailer = resource({
|
|
163
|
+
id: "app.emailer",
|
|
164
|
+
init: async (config: SMTPConfig) => ({
|
|
165
|
+
send: async (to: string, subject: string, body: string) => {
|
|
166
|
+
// Use config.smtpUrl and config.from
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
159
169
|
});
|
|
160
170
|
|
|
171
|
+
// Register with specific config
|
|
161
172
|
const app = resource({
|
|
162
173
|
id: "app",
|
|
163
174
|
register: [
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
emailer.with({
|
|
176
|
+
smtpUrl: "smtp://localhost",
|
|
177
|
+
from: "noreply@myapp.com",
|
|
178
|
+
}),
|
|
179
|
+
// using emailer without with() will throw a type-error ;)
|
|
166
180
|
],
|
|
167
181
|
});
|
|
168
182
|
```
|
|
169
183
|
|
|
170
|
-
|
|
184
|
+
#### Shared Context Between Init and Dispose
|
|
171
185
|
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
For cases where you need to share variables between `init()` and `dispose()` methods (because sometimes cleanup is complicated), use the enhanced context pattern:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const dbResource = resource({
|
|
190
|
+
id: "db.service",
|
|
191
|
+
context: () => ({
|
|
192
|
+
connections: new Map(),
|
|
193
|
+
pools: [],
|
|
194
|
+
}),
|
|
195
|
+
async init(config, deps, ctx) {
|
|
196
|
+
const db = await connectToDatabase();
|
|
197
|
+
ctx.connections.set("main", db);
|
|
198
|
+
ctx.pools.push(createPool(db));
|
|
199
|
+
return db;
|
|
200
|
+
},
|
|
201
|
+
async dispose(db, config, deps, ctx) {
|
|
202
|
+
// Same context available - no more "how do I access that thing I created?"
|
|
203
|
+
for (const pool of ctx.pools) {
|
|
204
|
+
await pool.drain();
|
|
205
|
+
}
|
|
206
|
+
for (const [name, conn] of ctx.connections) {
|
|
207
|
+
await conn.close();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
174
211
|
```
|
|
175
212
|
|
|
176
|
-
|
|
213
|
+
### 3. Events: Async Communication That Actually Works
|
|
214
|
+
|
|
215
|
+
Events let different parts of your app talk to each other without tight coupling. It's like having a really good office messenger who never forgets anything.
|
|
177
216
|
|
|
178
|
-
|
|
217
|
+
```typescript
|
|
218
|
+
const userRegistered = event<{ userId: string; email: string }>({
|
|
219
|
+
id: "app.events.userRegistered",
|
|
220
|
+
});
|
|
179
221
|
|
|
180
|
-
|
|
181
|
-
|
|
222
|
+
const registerUser = task({
|
|
223
|
+
id: "app.tasks.registerUser",
|
|
224
|
+
dependencies: { userService, userRegistered },
|
|
225
|
+
run: async (userData, { userService, userRegistered }) => {
|
|
226
|
+
const user = await userService.createUser(userData);
|
|
182
227
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
userRegisteredEvent,
|
|
187
|
-
},
|
|
188
|
-
async run(_, deps) {
|
|
189
|
-
await deps.userRegisteredEvent();
|
|
190
|
-
return "Hello World!";
|
|
228
|
+
// Tell the world about it
|
|
229
|
+
await userRegistered({ userId: user.id, email: user.email });
|
|
230
|
+
return user;
|
|
191
231
|
},
|
|
192
232
|
});
|
|
193
233
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
async init(_, deps) {
|
|
202
|
-
await deps.helloWorld();
|
|
234
|
+
// Someone else handles the welcome email
|
|
235
|
+
const sendWelcomeEmail = task({
|
|
236
|
+
id: "app.tasks.sendWelcomeEmail",
|
|
237
|
+
on: userRegistered, // Listen to the event, notice the "on"
|
|
238
|
+
run: async (eventData) => {
|
|
239
|
+
// Everything is type-safe, automatically inferred from the 'on' property
|
|
240
|
+
console.log(`Welcome email sent to ${eventData.data.email}`);
|
|
203
241
|
},
|
|
204
242
|
});
|
|
205
|
-
|
|
206
|
-
run(app);
|
|
207
243
|
```
|
|
208
244
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Tasks, however, are not bound by this restriction; they can freely depend on each other as needed.
|
|
212
|
-
|
|
213
|
-
The dependencies get injected as follows:
|
|
214
|
-
|
|
215
|
-
| Component | Injection Description |
|
|
216
|
-
| ------------ | --------------------------------------------------------- |
|
|
217
|
-
| `tasks` | Injected as functions with their input argument |
|
|
218
|
-
| `resources` | Injected as their return value |
|
|
219
|
-
| `events` | Injected as functions with their payload argument |
|
|
220
|
-
| `middleware` | Not typically injected; used via a `middleware: []` array |
|
|
245
|
+
#### Wildcard Events: For When You Want to Know Everything
|
|
221
246
|
|
|
222
|
-
|
|
247
|
+
Sometimes you need to be the nosy neighbor of your application:
|
|
223
248
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
init: async (_, deps) => {
|
|
232
|
-
deps.resource;
|
|
233
|
-
return "";
|
|
249
|
+
```typescript
|
|
250
|
+
const logAllEventsTask = task({
|
|
251
|
+
id: "app.tasks.logAllEvents",
|
|
252
|
+
on: "*", // Listen to EVERYTHING
|
|
253
|
+
run(event) {
|
|
254
|
+
console.log("Event detected", event.id, event.data);
|
|
255
|
+
// Note: Be careful with dependencies here since some events fire before initialization
|
|
234
256
|
},
|
|
235
|
-
dependencies: () => ({
|
|
236
|
-
resource,
|
|
237
|
-
}),
|
|
238
257
|
});
|
|
258
|
+
```
|
|
239
259
|
|
|
240
|
-
|
|
241
|
-
id: "resource",
|
|
242
|
-
});
|
|
260
|
+
#### Tasks and Resources built-in events
|
|
243
261
|
|
|
244
|
-
|
|
245
|
-
|
|
262
|
+
Tasks and resources have their own lifecycle events that you can hook into:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const myTask = task({ ... });
|
|
266
|
+
const myResource = resource({ ... });
|
|
246
267
|
```
|
|
247
268
|
|
|
248
|
-
|
|
269
|
+
- `myTask.events.beforeRun` - Fires before the task runs
|
|
270
|
+
- `myTask.events.afterRun` - Fires after the task completes
|
|
271
|
+
- `myTask.events.onError` - Fires when the task fails
|
|
272
|
+
- `myResource.events.beforeInit` - Fires before the resource initializes
|
|
273
|
+
- `myResource.events.afterInit` - Fires after the resource initializes
|
|
274
|
+
- `myResource.events.onError` - Fires when the resource initialization fails
|
|
249
275
|
|
|
250
|
-
|
|
276
|
+
Each event has its own utilities and functions.
|
|
251
277
|
|
|
252
|
-
|
|
278
|
+
#### Global Events: The System's Built-in Gossip Network
|
|
253
279
|
|
|
254
|
-
|
|
255
|
-
import { task, run, event } from "@bluelibs/runner";
|
|
280
|
+
The framework comes with its own set of events that fire during the lifecycle. Think of them as the system's way of keeping you informed:
|
|
256
281
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
282
|
+
- `globals.tasks.beforeRun` - "Hey, I'm about to run this task"
|
|
283
|
+
- `globals.tasks.afterRun` - "Task completed, here's what happened"
|
|
284
|
+
- `globals.tasks.onError` - "Oops, something went wrong"
|
|
285
|
+
- `globals.resources.beforeInit` - "Initializing a resource"
|
|
286
|
+
- `globals.resources.afterInit` - "Resource is ready"
|
|
287
|
+
- `globals.resources.onError` - "Resource initialization failed"
|
|
260
288
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
async init(_, deps) {
|
|
268
|
-
// the event becomes a function that you run with the propper payload
|
|
269
|
-
await deps.afterRegisterEvent({ userId: string });
|
|
289
|
+
```typescript
|
|
290
|
+
const taskLogger = task({
|
|
291
|
+
id: "app.logging.taskLogger",
|
|
292
|
+
on: globalEvents.tasks.beforeRun,
|
|
293
|
+
run(event) {
|
|
294
|
+
console.log(`Running task: ${event.source} with input:`, event.data.input);
|
|
270
295
|
},
|
|
271
296
|
});
|
|
272
297
|
```
|
|
273
298
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
### `task.on` property
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
import { task, run, event } from "@bluelibs/runner";
|
|
299
|
+
### 4. Middleware: The Interceptor Pattern Done Right
|
|
280
300
|
|
|
281
|
-
|
|
282
|
-
id: "app.user.afterRegister",
|
|
283
|
-
});
|
|
301
|
+
Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
|
|
284
302
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
```typescript
|
|
304
|
+
// This is a middleware that accepts a config
|
|
305
|
+
const authMiddleware = middleware({
|
|
306
|
+
id: "app.middleware.auth",
|
|
307
|
+
// You can also add dependencies, no problem.
|
|
308
|
+
run: async (
|
|
309
|
+
{ task, next },
|
|
310
|
+
dependencies,
|
|
311
|
+
config: { requiredRole: string }
|
|
312
|
+
) => {
|
|
313
|
+
const user = task.input.user;
|
|
314
|
+
if (!user || user.role !== config.requiredRole) {
|
|
315
|
+
throw new Error("Unauthorized");
|
|
316
|
+
}
|
|
317
|
+
return next(task.input);
|
|
292
318
|
},
|
|
293
319
|
});
|
|
294
320
|
|
|
295
|
-
const
|
|
296
|
-
id: "app",
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
async init(_, deps) {
|
|
302
|
-
await deps.afterRegisterEvent({ userId: "XXX" });
|
|
321
|
+
const adminTask = task({
|
|
322
|
+
id: "app.tasks.adminOnly",
|
|
323
|
+
// If the configuration accepts {} or is empty, .with() becomes optional, otherwise it becomes enforced.
|
|
324
|
+
middleware: [authMiddleware.with({ requiredRole: "admin" })],
|
|
325
|
+
run: async (input: { user: User }) => {
|
|
326
|
+
return "Secret admin data";
|
|
303
327
|
},
|
|
304
328
|
});
|
|
305
329
|
```
|
|
306
330
|
|
|
307
|
-
|
|
331
|
+
#### Global Middleware: Apply Everywhere, Configure Once
|
|
308
332
|
|
|
309
|
-
|
|
333
|
+
Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
|
|
310
334
|
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
id: "app.tasks.logAllEvents",
|
|
320
|
-
on: "*",
|
|
321
|
-
run(event) {
|
|
322
|
-
console.log("Event detected", event.id, event.data);
|
|
335
|
+
```typescript
|
|
336
|
+
const logMiddleware = middleware({
|
|
337
|
+
id: "app.middleware.log",
|
|
338
|
+
run: async ({ task, next }) => {
|
|
339
|
+
console.log(`Executing: ${task.definition.id}`);
|
|
340
|
+
const result = await next(task.input);
|
|
341
|
+
console.log(`Completed: ${task.definition.id}`);
|
|
342
|
+
return result;
|
|
323
343
|
},
|
|
324
344
|
});
|
|
325
345
|
|
|
326
|
-
const
|
|
346
|
+
const app = resource({
|
|
327
347
|
id: "app",
|
|
328
|
-
register: [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
deps.afterRegisterEvent({ userId: "XXX" });
|
|
332
|
-
},
|
|
348
|
+
register: [
|
|
349
|
+
logMiddleware.everywhere({ tasks: true, resources: false }), // Only tasks get logged
|
|
350
|
+
],
|
|
333
351
|
});
|
|
334
352
|
```
|
|
335
353
|
|
|
336
|
-
##
|
|
337
|
-
|
|
338
|
-
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.)
|
|
354
|
+
## Context: Request-Scoped Data That Doesn't Drive You Insane
|
|
339
355
|
|
|
340
|
-
|
|
341
|
-
import { task, resource, run, event } from "@bluelibs/runner";
|
|
356
|
+
Ever tried to pass user data through 15 function calls? Yeah, we've been there. Context fixes that without turning your code into a game of telephone.
|
|
342
357
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
async run(data, deps) {
|
|
349
|
-
const { taskDefinition, resourceDefinition, config, next, input } = data;
|
|
350
|
-
|
|
351
|
-
// The middleware can be for a task or a resource, depending on which you get the right elements.
|
|
352
|
-
if (taskDefinition) {
|
|
353
|
-
console.log("Before task", taskDefinition.id);
|
|
354
|
-
const result = await next(input); // pass the input to the next middleware or task
|
|
355
|
-
console.log("After task", taskDefinition.id);
|
|
356
|
-
} else {
|
|
357
|
-
console.log("Before resource", resourceDefinition.id);
|
|
358
|
-
const result = await next(config); // pass the input to the next middleware or task
|
|
359
|
-
console.log("After resource", resourceDefinition.id);
|
|
360
|
-
}
|
|
358
|
+
```typescript
|
|
359
|
+
const UserContext = createContext<{ userId: string; role: string }>(
|
|
360
|
+
"app.userContext"
|
|
361
|
+
);
|
|
361
362
|
|
|
362
|
-
|
|
363
|
+
const getUserData = task({
|
|
364
|
+
id: "app.tasks.getUserData",
|
|
365
|
+
middleware: [UserContext.require()], // This is a middleware that ensures the context is available before task runs, throws if not.
|
|
366
|
+
run: async () => {
|
|
367
|
+
const user = UserContext.use(); // Available anywhere in the async chain
|
|
368
|
+
return `Current user: ${user.userId} (${user.role})`;
|
|
363
369
|
},
|
|
364
370
|
});
|
|
365
371
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
// Provide context at the entry point
|
|
373
|
+
const handleRequest = resource({
|
|
374
|
+
id: "app.requestHandler",
|
|
375
|
+
init: async () => {
|
|
376
|
+
return UserContext.provide({ userId: "123", role: "admin" }, async () => {
|
|
377
|
+
// All tasks called within this scope have access to UserContext
|
|
378
|
+
return await getUserData();
|
|
379
|
+
});
|
|
371
380
|
},
|
|
372
381
|
});
|
|
373
382
|
```
|
|
374
383
|
|
|
375
|
-
###
|
|
376
|
-
|
|
377
|
-
If you want to register a middleware for all tasks and resources, here's how you can do it:
|
|
384
|
+
### Context with Middleware: The Power Couple
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
import { run, resource } from "@bluelibs/runner";
|
|
386
|
+
Context shines when combined with middleware for request-scoped data:
|
|
381
387
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
388
|
+
```typescript
|
|
389
|
+
const RequestContext = createContext<{
|
|
390
|
+
requestId: string;
|
|
391
|
+
startTime: number;
|
|
392
|
+
userAgent?: string;
|
|
393
|
+
}>("app.requestContext");
|
|
394
|
+
|
|
395
|
+
const requestMiddleware = middleware({
|
|
396
|
+
id: "app.middleware.request",
|
|
397
|
+
run: async ({ task, next }) => {
|
|
398
|
+
// This works even in express middleware if needed.
|
|
399
|
+
return RequestContext.provide(
|
|
400
|
+
{
|
|
401
|
+
requestId: crypto.randomUUID(),
|
|
402
|
+
startTime: Date.now(),
|
|
403
|
+
userAgent: "MyApp/1.0",
|
|
404
|
+
},
|
|
405
|
+
async () => {
|
|
406
|
+
return next(task.input);
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
},
|
|
385
410
|
});
|
|
386
411
|
|
|
387
|
-
const
|
|
388
|
-
id: "app",
|
|
389
|
-
|
|
412
|
+
const handleRequest = task({
|
|
413
|
+
id: "app.handleRequest",
|
|
414
|
+
middleware: [requestMiddleware],
|
|
415
|
+
run: async (input: { path: string }) => {
|
|
416
|
+
const request = RequestContext.use();
|
|
417
|
+
console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
|
|
418
|
+
return { success: true, requestId: request.requestId };
|
|
419
|
+
},
|
|
390
420
|
});
|
|
391
421
|
```
|
|
392
422
|
|
|
393
|
-
|
|
423
|
+
## Dependency Management: The Index Pattern
|
|
394
424
|
|
|
395
|
-
|
|
425
|
+
When your app grows beyond "hello world", you'll want to group related dependencies. The `index()` helper is your friend - it's basically a 3-in-1 resource that registers, depends on, and returns everything you give it.
|
|
396
426
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
run() {
|
|
405
|
-
throw new Error("Something went wrong");
|
|
406
|
-
},
|
|
427
|
+
```typescript
|
|
428
|
+
// This registers all services, depends on them, and returns them as one clean interface
|
|
429
|
+
const services = index({
|
|
430
|
+
userService,
|
|
431
|
+
emailService,
|
|
432
|
+
paymentService,
|
|
433
|
+
notificationService,
|
|
407
434
|
});
|
|
408
435
|
|
|
409
436
|
const app = resource({
|
|
410
437
|
id: "app",
|
|
411
|
-
register: [
|
|
412
|
-
dependencies: {
|
|
413
|
-
|
|
438
|
+
register: [services],
|
|
439
|
+
dependencies: { services },
|
|
440
|
+
init: async (_, { services }) => {
|
|
441
|
+
// Access everything through one clean interface
|
|
442
|
+
const user = await services.userService.createUser(userData);
|
|
443
|
+
await services.emailService.sendWelcome(user.email);
|
|
414
444
|
},
|
|
415
|
-
async init() {
|
|
416
|
-
await helloWorld();
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
run(app).catch((err) => {
|
|
421
|
-
console.error(err);
|
|
422
445
|
});
|
|
423
446
|
```
|
|
424
447
|
|
|
425
|
-
|
|
448
|
+
## Error Handling: Graceful Failures
|
|
426
449
|
|
|
427
|
-
|
|
428
|
-
const helloWorld = task({
|
|
429
|
-
id: "app.tasks.helloWorld.onError",
|
|
430
|
-
on: helloWorld.events.onError,
|
|
431
|
-
run({ error, input, suppress }, deps) {
|
|
432
|
-
// this will be called when an error happens
|
|
450
|
+
Errors happen. When they do, you can listen for them and decide what to do. No more unhandled promise rejections ruining your day.
|
|
433
451
|
|
|
434
|
-
|
|
435
|
-
|
|
452
|
+
```typescript
|
|
453
|
+
const riskyTask = task({
|
|
454
|
+
id: "app.tasks.risky",
|
|
455
|
+
run: async () => {
|
|
456
|
+
throw new Error("Something went wrong");
|
|
436
457
|
},
|
|
458
|
+
// Behind the scenes when you create a task() we create these 3 events for you (onError, beforeRun, afterRun)
|
|
437
459
|
});
|
|
438
|
-
```
|
|
439
460
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
// this will be called when an error happens
|
|
461
|
+
const errorHandler = task({
|
|
462
|
+
id: "app.tasks.errorHandler",
|
|
463
|
+
on: riskyTask.events.onError,
|
|
464
|
+
run: async (event) => {
|
|
465
|
+
console.error("Task failed:", event.data.error);
|
|
446
466
|
|
|
447
|
-
//
|
|
448
|
-
suppress();
|
|
467
|
+
// Don't let the error bubble up - this makes the task return undefined
|
|
468
|
+
event.data.suppress();
|
|
449
469
|
},
|
|
450
470
|
});
|
|
451
471
|
```
|
|
452
472
|
|
|
453
|
-
##
|
|
454
|
-
|
|
455
|
-
You can attach metadata to tasks, resources, events, and middleware.
|
|
473
|
+
## Caching: Built-in Performance
|
|
456
474
|
|
|
457
|
-
|
|
458
|
-
import { task, run, event } from "@bluelibs/runner";
|
|
475
|
+
Because nobody likes waiting for the same expensive operation twice:
|
|
459
476
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
477
|
+
```typescript
|
|
478
|
+
import { globals } from "@bluelibs/runner";
|
|
479
|
+
|
|
480
|
+
const expensiveTask = task({
|
|
481
|
+
id: "app.tasks.expensive",
|
|
482
|
+
middleware: [
|
|
483
|
+
globals.middleware.cache.with({
|
|
484
|
+
// lru-cache options by default
|
|
485
|
+
ttl: 60 * 1000, // Cache for 1 minute
|
|
486
|
+
keyBuilder: (taskId, input) => `${taskId}-${input.userId}`, // optional key builder
|
|
487
|
+
}),
|
|
488
|
+
],
|
|
489
|
+
run: async ({ userId }) => {
|
|
490
|
+
// This expensive operation will be cached
|
|
491
|
+
return await doExpensiveCalculation(userId);
|
|
469
492
|
},
|
|
470
493
|
});
|
|
494
|
+
|
|
495
|
+
// Global cache configuration
|
|
496
|
+
const app = resource({
|
|
497
|
+
id: "app.cache",
|
|
498
|
+
register: [
|
|
499
|
+
// You have to register it, cache resource is not enabled by default.
|
|
500
|
+
globals.resources.cache.with({
|
|
501
|
+
defaultOptions: {
|
|
502
|
+
max: 1000, // Maximum items in cache
|
|
503
|
+
ttl: 30 * 1000, // Default TTL
|
|
504
|
+
},
|
|
505
|
+
async: false, // in-memory is sync by default
|
|
506
|
+
// When using redis or others mark this as true to await response.
|
|
507
|
+
}),
|
|
508
|
+
],
|
|
509
|
+
});
|
|
471
510
|
```
|
|
472
511
|
|
|
473
|
-
|
|
512
|
+
Want Redis instead of the default LRU cache? No problem, just override the cache factory task:
|
|
474
513
|
|
|
475
|
-
|
|
514
|
+
```typescript
|
|
515
|
+
import { task } from "@bluelibs/runner";
|
|
476
516
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
517
|
+
const redisCacheFactory = task({
|
|
518
|
+
id: "globals.tasks.cacheFactory", // Same ID as the default task
|
|
519
|
+
run: async (options: any) => {
|
|
520
|
+
return new RedisCache(options); // Make sure to turn async on in the cacher.
|
|
521
|
+
},
|
|
522
|
+
});
|
|
483
523
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
524
|
+
const app = resource({
|
|
525
|
+
id: "app",
|
|
526
|
+
register: [
|
|
527
|
+
// Your other stuff
|
|
528
|
+
],
|
|
529
|
+
overrides: [redisCacheFactory], // Override the default cache factory
|
|
530
|
+
});
|
|
488
531
|
```
|
|
489
532
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
## Internal Services
|
|
533
|
+
## Logging: Because Console.log Isn't Professional
|
|
493
534
|
|
|
494
|
-
|
|
535
|
+
_The structured logging system that actually makes debugging enjoyable_
|
|
495
536
|
|
|
496
|
-
-
|
|
497
|
-
- TaskRunner (can run tasks definitions directly and within D.I. context)
|
|
498
|
-
- EventManager (can emit and listen to events)
|
|
537
|
+
BlueLibs Runner comes with a built-in logging system that's event-driven, structured, and doesn't make you hate your life when you're trying to debug at 2 AM. It emits events for everything, so you can handle logs however you want - ship them to your favorite log warehouse, pretty-print them to console, or ignore them entirely (we won't judge).
|
|
499
538
|
|
|
500
|
-
|
|
539
|
+
### Quick Start: Basic Logging
|
|
501
540
|
|
|
502
|
-
```
|
|
503
|
-
import {
|
|
541
|
+
```typescript
|
|
542
|
+
import { globals } from "@bluelibs/runner";
|
|
504
543
|
|
|
505
|
-
const
|
|
506
|
-
id: "app.
|
|
507
|
-
dependencies: {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
544
|
+
const businessTask = task({
|
|
545
|
+
id: "app.tasks.business",
|
|
546
|
+
dependencies: { logger: globals.resources.logger },
|
|
547
|
+
run: async (_, { logger }) => {
|
|
548
|
+
logger.info("Starting business process");
|
|
549
|
+
logger.warn("This might take a while");
|
|
550
|
+
logger.error("Oops, something went wrong", {
|
|
551
|
+
error: new Error("Database connection failed"),
|
|
552
|
+
});
|
|
553
|
+
logger.critical("System is on fire", {
|
|
554
|
+
data: { temperature: "9000°C" },
|
|
555
|
+
});
|
|
514
556
|
},
|
|
515
557
|
});
|
|
516
558
|
```
|
|
517
559
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
Domain usually is "app", but as your application grows or you plan on building external libraries the naming convention should be: "companyName.packageName".
|
|
560
|
+
### Log Levels: From Whispers to Screams
|
|
521
561
|
|
|
522
|
-
|
|
523
|
-
| -------------- | ----------------------------------------- |
|
|
524
|
-
| Tasks | `{domain}.tasks.{taskName}` |
|
|
525
|
-
| Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
|
|
526
|
-
| Resources | `{domain}.resources.{resourceName}` |
|
|
527
|
-
| Events | `{domain}.events.{eventName}` |
|
|
528
|
-
| Middleware | `{domain}.middleware.{middlewareName}` |
|
|
562
|
+
The logger supports six log levels with increasing severity:
|
|
529
563
|
|
|
530
|
-
|
|
564
|
+
| Level | Severity | When to Use | Color |
|
|
565
|
+
| ---------- | -------- | ------------------------------------------- | ------- |
|
|
566
|
+
| `trace` | 0 | Ultra-detailed debugging info | Gray |
|
|
567
|
+
| `debug` | 1 | Development and debugging information | Cyan |
|
|
568
|
+
| `info` | 2 | General information about normal operations | Green |
|
|
569
|
+
| `warn` | 3 | Something's not right, but still working | Yellow |
|
|
570
|
+
| `error` | 4 | Errors that need attention | Red |
|
|
571
|
+
| `critical` | 5 | System-threatening issues | Magenta |
|
|
531
572
|
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
573
|
+
```typescript
|
|
574
|
+
// All log levels are available as methods
|
|
575
|
+
logger.trace("Ultra-detailed debugging info");
|
|
576
|
+
logger.debug("Development debugging");
|
|
577
|
+
logger.info("Normal operation");
|
|
578
|
+
logger.warn("Something's fishy");
|
|
579
|
+
logger.error("Houston, we have a problem");
|
|
580
|
+
logger.critical("DEFCON 1: Everything is broken");
|
|
536
581
|
```
|
|
537
582
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
```ts
|
|
541
|
-
import { userCreatedEvent } from "./events";
|
|
542
|
-
export const events = {
|
|
543
|
-
userCreated: userCreatedEvent,
|
|
544
|
-
// ...
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
export const tasks = {
|
|
548
|
-
doSomething: doSomethingTask,
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
export const resources = {
|
|
552
|
-
root: rootResource,
|
|
553
|
-
user: userResource,
|
|
554
|
-
};
|
|
555
|
-
```
|
|
583
|
+
### Structured Logging: More Than Just Strings
|
|
556
584
|
|
|
557
|
-
|
|
585
|
+
The logger accepts rich, structured data that makes debugging actually useful:
|
|
558
586
|
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
|
|
587
|
+
```typescript
|
|
588
|
+
const userTask = task({
|
|
589
|
+
id: "app.tasks.user.create",
|
|
590
|
+
dependencies: { logger: globals.resources.logger },
|
|
591
|
+
run: async (userData, { logger }) => {
|
|
592
|
+
// Basic message
|
|
593
|
+
logger.info("Creating new user");
|
|
594
|
+
|
|
595
|
+
// With structured data
|
|
596
|
+
logger.info("User creation attempt", {
|
|
597
|
+
data: {
|
|
598
|
+
email: userData.email,
|
|
599
|
+
registrationSource: "web",
|
|
600
|
+
timestamp: new Date().toISOString(),
|
|
601
|
+
},
|
|
602
|
+
});
|
|
562
603
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
604
|
+
// With error information
|
|
605
|
+
try {
|
|
606
|
+
const user = await createUser(userData);
|
|
607
|
+
logger.info("User created successfully", {
|
|
608
|
+
data: { userId: user.id, email: user.email },
|
|
609
|
+
});
|
|
610
|
+
} catch (error) {
|
|
611
|
+
logger.error("User creation failed", {
|
|
612
|
+
error,
|
|
613
|
+
data: {
|
|
614
|
+
attemptedEmail: userData.email,
|
|
615
|
+
validationErrors: error.validationErrors,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
},
|
|
566
620
|
});
|
|
567
|
-
|
|
568
|
-
run(app);
|
|
569
621
|
```
|
|
570
622
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
This approach is very powerful when you have multiple packages and you want to compose them together.
|
|
574
|
-
|
|
575
|
-
## Real life
|
|
576
|
-
|
|
577
|
-
Or is it just fantasy?
|
|
623
|
+
### Context-Aware Logging: The with() Method
|
|
578
624
|
|
|
579
|
-
|
|
625
|
+
Create logger instances with bound context for consistent metadata across related operations:
|
|
580
626
|
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
|
|
627
|
+
```typescript
|
|
628
|
+
const RequestContext = createContext<{ requestId: string; userId: string }>(
|
|
629
|
+
"app.requestContext"
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const requestHandler = task({
|
|
633
|
+
id: "app.tasks.handleRequest",
|
|
634
|
+
dependencies: { logger: globals.resources.logger },
|
|
635
|
+
run: async (requestData, { logger }) => {
|
|
636
|
+
const request = RequestContext.use();
|
|
637
|
+
|
|
638
|
+
// Create a contextual logger with bound metadata
|
|
639
|
+
const requestLogger = logger.with({
|
|
640
|
+
requestId: request.requestId,
|
|
641
|
+
userId: request.userId,
|
|
642
|
+
source: "api.handler",
|
|
643
|
+
});
|
|
584
644
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const app = express();
|
|
589
|
-
app.listen(3000).then(() => {
|
|
590
|
-
console.log("Server is running on port 3000");
|
|
645
|
+
// All logs from this logger will include the bound context
|
|
646
|
+
requestLogger.info("Processing request", {
|
|
647
|
+
data: { endpoint: requestData.path },
|
|
591
648
|
});
|
|
592
649
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
});
|
|
650
|
+
requestLogger.debug("Validating input", {
|
|
651
|
+
data: { inputSize: JSON.stringify(requestData).length },
|
|
652
|
+
});
|
|
597
653
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
},
|
|
603
|
-
async init(_, deps) {
|
|
604
|
-
deps.expressServer.use("/api", (req, res) => {
|
|
605
|
-
res.json({ hello: "world" });
|
|
654
|
+
// Context is automatically included in all log events
|
|
655
|
+
requestLogger.error("Request processing failed", {
|
|
656
|
+
error: new Error("Invalid input"),
|
|
657
|
+
data: { stage: "validation" },
|
|
606
658
|
});
|
|
607
659
|
},
|
|
608
660
|
});
|
|
609
|
-
|
|
610
|
-
// Just run them, init() will be called everywhere.
|
|
611
|
-
const app = resource({
|
|
612
|
-
id: "app",
|
|
613
|
-
register: [expressServer, setupRoutes],
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
run();
|
|
617
661
|
```
|
|
618
662
|
|
|
619
|
-
|
|
663
|
+
### Print Threshold: Control What Shows Up
|
|
620
664
|
|
|
621
|
-
|
|
665
|
+
By default, logs are just events - they don't print to console unless you tell them to. Set a print threshold to automatically output logs at or above a certain level:
|
|
622
666
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
667
|
+
```typescript
|
|
668
|
+
// Set up log printing (they don't print by default)
|
|
669
|
+
const setupLogging = task({
|
|
670
|
+
id: "app.logging.setup",
|
|
671
|
+
on: globals.resources.logger.events.afterInit,
|
|
672
|
+
run: async (event) => {
|
|
673
|
+
const logger = event.data.value;
|
|
627
674
|
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
pricePerSubscription: 9.99,
|
|
631
|
-
};
|
|
675
|
+
// Print info level and above (info, warn, error, critical)
|
|
676
|
+
logger.setPrintThreshold("info");
|
|
632
677
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
async init() {
|
|
636
|
-
return businessData;
|
|
637
|
-
},
|
|
638
|
-
});
|
|
678
|
+
// Print only errors and critical issues
|
|
679
|
+
logger.setPrintThreshold("error");
|
|
639
680
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
register: [businessConfig],
|
|
643
|
-
dependencies: { businessConfig },
|
|
644
|
-
async init(_, deps) {
|
|
645
|
-
console.log(deps.businessConfig.pricePerSubscription);
|
|
681
|
+
// Disable auto-printing entirely
|
|
682
|
+
logger.setPrintThreshold(null);
|
|
646
683
|
},
|
|
647
684
|
});
|
|
648
|
-
|
|
649
|
-
run();
|
|
650
685
|
```
|
|
651
686
|
|
|
652
|
-
|
|
687
|
+
### Event-Driven Log Handling: Ship Logs Anywhere
|
|
653
688
|
|
|
654
|
-
|
|
689
|
+
Every log generates an event that you can listen to. This is where the real power comes in:
|
|
655
690
|
|
|
656
|
-
|
|
691
|
+
```typescript
|
|
692
|
+
// Ship logs to your favorite log warehouse
|
|
693
|
+
const logShipper = task({
|
|
694
|
+
id: "app.logging.shipper", // or pretty printer, or winston, pino bridge, etc.
|
|
695
|
+
on: globals.events.log,
|
|
696
|
+
run: async (event) => {
|
|
697
|
+
const log = event.data;
|
|
698
|
+
|
|
699
|
+
// Ship critical errors to PagerDuty
|
|
700
|
+
if (log.level === "critical") {
|
|
701
|
+
await pagerDuty.alert({
|
|
702
|
+
message: log.message,
|
|
703
|
+
details: log.data,
|
|
704
|
+
source: log.source,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Ship all errors to error tracking
|
|
709
|
+
if (log.level === "error" || log.level === "critical") {
|
|
710
|
+
await sentry.captureException(log.error || new Error(log.message), {
|
|
711
|
+
tags: { source: log.source },
|
|
712
|
+
extra: log.data,
|
|
713
|
+
level: log.level,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
657
716
|
|
|
658
|
-
|
|
717
|
+
// Ship everything to your log warehouse
|
|
718
|
+
await logWarehouse.ship({
|
|
719
|
+
timestamp: log.timestamp,
|
|
720
|
+
level: log.level,
|
|
721
|
+
message: log.message,
|
|
722
|
+
source: log.source,
|
|
723
|
+
data: log.data,
|
|
724
|
+
context: log.context,
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
});
|
|
659
728
|
|
|
660
|
-
|
|
729
|
+
// Filter logs by source
|
|
730
|
+
const databaseLogHandler = task({
|
|
731
|
+
id: "app.logging.database",
|
|
732
|
+
on: globals.events.log,
|
|
733
|
+
run: async (event) => {
|
|
734
|
+
const log = event.data;
|
|
661
735
|
|
|
662
|
-
|
|
736
|
+
// Only handle database-related logs
|
|
737
|
+
if (log.source?.includes("database")) {
|
|
738
|
+
await databaseMonitoring.recordMetric({
|
|
739
|
+
operation: log.data?.operation,
|
|
740
|
+
duration: log.data?.duration,
|
|
741
|
+
level: log.level,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
```
|
|
663
747
|
|
|
664
|
-
|
|
665
|
-
- **Execution events**: Before and after a task runs.
|
|
666
|
-
- **Error events**: Handling errors for both tasks and resources.
|
|
748
|
+
### Integration with Winston: Best of Both Worlds
|
|
667
749
|
|
|
668
|
-
|
|
750
|
+
Want to use Winston as your transport? No problem - integrate it seamlessly:
|
|
669
751
|
|
|
670
|
-
|
|
752
|
+
```typescript
|
|
753
|
+
import winston from "winston";
|
|
754
|
+
|
|
755
|
+
// Create Winston logger, put it in a resource if used from various places.
|
|
756
|
+
const winstonLogger = winston.createLogger({
|
|
757
|
+
level: "info",
|
|
758
|
+
format: winston.format.combine(
|
|
759
|
+
winston.format.timestamp(),
|
|
760
|
+
winston.format.errors({ stack: true }),
|
|
761
|
+
winston.format.json()
|
|
762
|
+
),
|
|
763
|
+
transports: [
|
|
764
|
+
new winston.transports.File({ filename: "error.log", level: "error" }),
|
|
765
|
+
new winston.transports.File({ filename: "combined.log" }),
|
|
766
|
+
new winston.transports.Console({
|
|
767
|
+
format: winston.format.simple(),
|
|
768
|
+
}),
|
|
769
|
+
],
|
|
770
|
+
});
|
|
671
771
|
|
|
672
|
-
|
|
772
|
+
// Bridge BlueLibs logs to Winston
|
|
773
|
+
const winstonBridge = task({
|
|
774
|
+
id: "app.logging.winston",
|
|
775
|
+
on: globals.events.log,
|
|
776
|
+
run: async (event) => {
|
|
777
|
+
const log = event.data;
|
|
778
|
+
|
|
779
|
+
// Convert BlueLibs log to Winston format
|
|
780
|
+
const winstonMeta = {
|
|
781
|
+
source: log.source,
|
|
782
|
+
timestamp: log.timestamp,
|
|
783
|
+
data: log.data,
|
|
784
|
+
context: log.context,
|
|
785
|
+
...(log.error && { error: log.error }),
|
|
786
|
+
};
|
|
673
787
|
|
|
674
|
-
|
|
788
|
+
// Map log levels (BlueLibs -> Winston)
|
|
789
|
+
const levelMapping = {
|
|
790
|
+
trace: "silly",
|
|
791
|
+
debug: "debug",
|
|
792
|
+
info: "info",
|
|
793
|
+
warn: "warn",
|
|
794
|
+
error: "error",
|
|
795
|
+
critical: "error", // Winston doesn't have critical, use error
|
|
796
|
+
};
|
|
675
797
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
id: "logBeforeRun",
|
|
679
|
-
on: globalEvents.tasks.beforeRun, // Listening to the beforeRun event
|
|
680
|
-
run(event) {
|
|
681
|
-
console.log("Task is about to run with input:", event.data.input);
|
|
798
|
+
const winstonLevel = levelMapping[log.level] || "info";
|
|
799
|
+
winstonLogger.log(winstonLevel, log.message, winstonMeta);
|
|
682
800
|
},
|
|
683
801
|
});
|
|
684
802
|
```
|
|
685
803
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
#### `global.tasks.afterRun`
|
|
689
|
-
|
|
690
|
-
This event fires immediately after a task finishes. It provides access to both the task input and the result (output).
|
|
804
|
+
### Advanced: Custom Log Formatters
|
|
691
805
|
|
|
692
|
-
|
|
806
|
+
Want to customize how logs are printed? You can override the print behavior:
|
|
693
807
|
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
run(event) {
|
|
808
|
+
```typescript
|
|
809
|
+
// Custom logger with JSON output
|
|
810
|
+
class JSONLogger extends Logger {
|
|
811
|
+
print(log: ILog) {
|
|
699
812
|
console.log(
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
813
|
+
JSON.stringify(
|
|
814
|
+
{
|
|
815
|
+
timestamp: log.timestamp.toISOString(),
|
|
816
|
+
level: log.level.toUpperCase(),
|
|
817
|
+
source: log.source,
|
|
818
|
+
message: log.message,
|
|
819
|
+
data: log.data,
|
|
820
|
+
context: log.context,
|
|
821
|
+
error: log.error,
|
|
822
|
+
},
|
|
823
|
+
null,
|
|
824
|
+
2
|
|
825
|
+
)
|
|
704
826
|
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Custom logger resource
|
|
831
|
+
const customLogger = resource({
|
|
832
|
+
id: "app.logger.custom",
|
|
833
|
+
dependencies: { eventManager: globals.resources.eventManager },
|
|
834
|
+
init: async (_, { eventManager }) => {
|
|
835
|
+
return new JSONLogger(eventManager);
|
|
705
836
|
},
|
|
706
837
|
});
|
|
707
|
-
```
|
|
708
838
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
#### `global.tasks.onError`
|
|
839
|
+
// Or you could simply add it as "globals.resources.logger" and override the default logger
|
|
840
|
+
```
|
|
712
841
|
|
|
713
|
-
|
|
842
|
+
### Log Structure: What You Get
|
|
714
843
|
|
|
715
|
-
|
|
844
|
+
Every log event contains:
|
|
716
845
|
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
846
|
+
```typescript
|
|
847
|
+
interface ILog {
|
|
848
|
+
level: string; // The log level (trace, debug, info, etc.)
|
|
849
|
+
source?: string; // Where the log came from
|
|
850
|
+
message: any; // The main log message (can be object or string)
|
|
851
|
+
timestamp: Date; // When the log was created
|
|
852
|
+
error?: {
|
|
853
|
+
// Structured error information
|
|
854
|
+
name: string;
|
|
855
|
+
message: string;
|
|
856
|
+
stack?: string;
|
|
857
|
+
};
|
|
858
|
+
data?: Record<string, any>; // Additional structured data, it's about the log itself
|
|
859
|
+
context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
|
|
860
|
+
}
|
|
726
861
|
```
|
|
727
862
|
|
|
728
|
-
|
|
863
|
+
### Debugging Tips & Best Practices
|
|
729
864
|
|
|
730
|
-
|
|
865
|
+
#### 1. Use Structured Data Liberally
|
|
731
866
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
##### Example:
|
|
867
|
+
```typescript
|
|
868
|
+
// Bad - hard to search and filter
|
|
869
|
+
await logger.error(`Failed to process user ${userId} order ${orderId}`);
|
|
737
870
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
871
|
+
// Good - searchable and filterable
|
|
872
|
+
await logger.error("Order processing failed", {
|
|
873
|
+
data: {
|
|
874
|
+
userId,
|
|
875
|
+
orderId,
|
|
876
|
+
step: "payment",
|
|
877
|
+
paymentMethod: "credit_card",
|
|
744
878
|
},
|
|
745
879
|
});
|
|
746
880
|
```
|
|
747
881
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
#### `global.resources.afterInit`
|
|
882
|
+
#### 2. Include Context in Errors
|
|
751
883
|
|
|
752
|
-
|
|
884
|
+
```typescript
|
|
885
|
+
// Include relevant context with errors
|
|
886
|
+
try {
|
|
887
|
+
await processPayment(order);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
await logger.error("Payment processing failed", {
|
|
890
|
+
error,
|
|
891
|
+
data: {
|
|
892
|
+
orderId: order.id,
|
|
893
|
+
amount: order.total,
|
|
894
|
+
currency: order.currency,
|
|
895
|
+
paymentMethod: order.paymentMethod,
|
|
896
|
+
attemptNumber: order.paymentAttempts,
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
```
|
|
753
901
|
|
|
754
|
-
|
|
902
|
+
#### 3. Use Different Log Levels Appropriately
|
|
755
903
|
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
904
|
+
```typescript
|
|
905
|
+
// Good level usage
|
|
906
|
+
await logger.debug("Cache hit", { data: { key, ttl: remainingTTL } });
|
|
907
|
+
await logger.info("User logged in", { data: { userId, loginMethod } });
|
|
908
|
+
await logger.warn("Rate limit approaching", {
|
|
909
|
+
data: { current: 95, limit: 100 },
|
|
910
|
+
});
|
|
911
|
+
await logger.error("Database connection failed", {
|
|
912
|
+
error,
|
|
913
|
+
data: { attempt: 3 },
|
|
763
914
|
});
|
|
915
|
+
await logger.critical("System out of memory", { data: { available: "0MB" } });
|
|
764
916
|
```
|
|
765
917
|
|
|
766
|
-
|
|
918
|
+
#### 4. Create Domain-Specific Loggers
|
|
767
919
|
|
|
768
|
-
|
|
920
|
+
```typescript
|
|
921
|
+
// Create loggers with domain context
|
|
922
|
+
const paymentLogger = logger.with({ source: "payment.processor" });
|
|
923
|
+
const authLogger = logger.with({ source: "auth.service" });
|
|
924
|
+
const emailLogger = logger.with({ source: "email.service" });
|
|
925
|
+
|
|
926
|
+
// Use throughout your domain
|
|
927
|
+
await paymentLogger.info("Processing payment", { data: paymentData });
|
|
928
|
+
await authLogger.warn("Failed login attempt", { data: { email, ip } });
|
|
929
|
+
```
|
|
769
930
|
|
|
770
|
-
|
|
931
|
+
### Common Pitfalls
|
|
771
932
|
|
|
772
|
-
|
|
933
|
+
1. **Logging sensitive data**: Never log passwords, tokens, or PII
|
|
934
|
+
2. **Over-logging in hot paths**: Check print thresholds for expensive operations
|
|
935
|
+
3. **Forgetting error objects**: Always include the original error when logging failures
|
|
936
|
+
4. **Poor log levels**: Don't use `error` for expected conditions
|
|
937
|
+
5. **Missing context**: Include relevant identifiers (user ID, request ID, etc.)
|
|
773
938
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
939
|
+
The Logger system is designed to be fast, flexible, and non-intrusive. Use it liberally - good logging is the difference between debugging hell and debugging heaven.
|
|
940
|
+
|
|
941
|
+
## Meta: Tagging Your Components
|
|
942
|
+
|
|
943
|
+
Sometimes you want to attach metadata to your tasks and resources for documentation, filtering, or middleware logic:
|
|
944
|
+
|
|
945
|
+
```typescript
|
|
946
|
+
const apiTask = task({
|
|
947
|
+
id: "app.tasks.api.createUser",
|
|
948
|
+
meta: {
|
|
949
|
+
title: "Create User API",
|
|
950
|
+
description: "Creates a new user account",
|
|
951
|
+
tags: ["api", "user", "public"],
|
|
952
|
+
},
|
|
953
|
+
run: async (userData) => {
|
|
954
|
+
// Business logic
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// Middleware that only applies to API tasks
|
|
959
|
+
const apiMiddleware = middleware({
|
|
960
|
+
id: "app.middleware.api",
|
|
961
|
+
run: async ({ task, next }) => {
|
|
962
|
+
if (task.meta?.tags?.includes("api")) {
|
|
963
|
+
// Apply API-specific logic
|
|
964
|
+
}
|
|
965
|
+
return next(task.input);
|
|
781
966
|
},
|
|
782
967
|
});
|
|
783
968
|
```
|
|
784
969
|
|
|
785
|
-
|
|
970
|
+
## Advanced Usage: When You Need More Power
|
|
786
971
|
|
|
787
|
-
|
|
972
|
+
### Overrides: Swapping Components at Runtime
|
|
788
973
|
|
|
789
|
-
|
|
974
|
+
Sometimes you need to replace a component entirely. Maybe you're testing, maybe you're A/B testing, maybe you just changed your mind:
|
|
790
975
|
|
|
791
|
-
|
|
976
|
+
```typescript
|
|
977
|
+
const productionEmailer = resource({
|
|
978
|
+
id: "app.emailer",
|
|
979
|
+
init: async () => new SMTPEmailer(),
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const testEmailer = resource({
|
|
983
|
+
...productionEmailer, // Copy everything else
|
|
984
|
+
init: async () => new MockEmailer(), // But use a different implementation
|
|
985
|
+
});
|
|
792
986
|
|
|
793
|
-
```ts
|
|
794
987
|
const app = resource({
|
|
795
988
|
id: "app",
|
|
796
|
-
register: [
|
|
797
|
-
|
|
798
|
-
logAfterRun,
|
|
799
|
-
handleTaskError,
|
|
800
|
-
logBeforeResourceInit,
|
|
801
|
-
logAfterResourceInit,
|
|
802
|
-
handleResourceError,
|
|
803
|
-
],
|
|
989
|
+
register: [productionEmailer],
|
|
990
|
+
overrides: [testEmailer], // This replaces the production version
|
|
804
991
|
});
|
|
805
|
-
|
|
806
|
-
run(app);
|
|
807
992
|
```
|
|
808
993
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
### Available Global Events
|
|
994
|
+
### Namespacing: Keeping Things Organized
|
|
812
995
|
|
|
813
|
-
|
|
996
|
+
As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
|
|
814
997
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
- **Resource-specific events**:
|
|
823
|
-
- `global.resources.beforeInit`: Fired before a resource is initialized.
|
|
824
|
-
- `global.resources.afterInit`: Fired after a resource is initialized.
|
|
825
|
-
- `global.resources.onError`: Fired if an error occurs during resource initialization.
|
|
998
|
+
| Type | Format |
|
|
999
|
+
| -------------- | ----------------------------------------- |
|
|
1000
|
+
| Tasks | `{domain}.tasks.{taskName}` |
|
|
1001
|
+
| Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
|
|
1002
|
+
| Resources | `{domain}.resources.{resourceName}` |
|
|
1003
|
+
| Events | `{domain}.events.{eventName}` |
|
|
1004
|
+
| Middleware | `{domain}.middleware.{middlewareName}` |
|
|
826
1005
|
|
|
827
|
-
|
|
1006
|
+
```typescript
|
|
1007
|
+
// Helper function for consistency
|
|
1008
|
+
function namespaced(id: string) {
|
|
1009
|
+
return `mycompany.myapp.${id}`;
|
|
1010
|
+
}
|
|
828
1011
|
|
|
829
|
-
|
|
1012
|
+
const userTask = task({
|
|
1013
|
+
id: namespaced("tasks.user.create"),
|
|
1014
|
+
// ...
|
|
1015
|
+
});
|
|
1016
|
+
```
|
|
830
1017
|
|
|
831
|
-
|
|
1018
|
+
### Factory Pattern: For When You Need Instances
|
|
832
1019
|
|
|
833
|
-
|
|
834
|
-
import { task, run, event } from "@bluelibs/runner";
|
|
1020
|
+
To keep things dead simple, we avoided poluting the D.I. with this concept. Therefore, we recommend using a resource with a factory function to create instances of your classes:
|
|
835
1021
|
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
id: "app.
|
|
839
|
-
async
|
|
840
|
-
|
|
841
|
-
|
|
1022
|
+
```typescript
|
|
1023
|
+
const myFactory = resource({
|
|
1024
|
+
id: "app.factories.myFactory",
|
|
1025
|
+
init: async (config: { someOption: string }) => {
|
|
1026
|
+
return (input: any) => {
|
|
1027
|
+
return new MyClass(input, config.someOption);
|
|
1028
|
+
};
|
|
842
1029
|
},
|
|
843
1030
|
});
|
|
844
1031
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
async
|
|
850
|
-
const
|
|
851
|
-
console.log("Before run:", input);
|
|
1032
|
+
const app = resource({
|
|
1033
|
+
id: "app",
|
|
1034
|
+
register: [myFactory],
|
|
1035
|
+
dependencies: { myFactory },
|
|
1036
|
+
init: async (_, { myFactory }) => {
|
|
1037
|
+
const instance = myFactory({ someOption: "value" });
|
|
852
1038
|
},
|
|
853
1039
|
});
|
|
1040
|
+
```
|
|
854
1041
|
|
|
855
|
-
|
|
856
|
-
id: "app.helloWorld.afterRun",
|
|
857
|
-
on: helloWorld.events.afterRun, // Listening to afterRun event
|
|
858
|
-
async run(event) {
|
|
859
|
-
const output = event.data.output; // Handle the output after task runs
|
|
860
|
-
console.log("After run:", output);
|
|
861
|
-
},
|
|
862
|
-
});
|
|
1042
|
+
### Internal Services: For When You Need Direct Access
|
|
863
1043
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1044
|
+
We expose the internal services for advanced use cases (but try not to use them unless you really need to):
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
import { globals } from "@bluelibs/runner";
|
|
1048
|
+
|
|
1049
|
+
const advancedTask = task({
|
|
1050
|
+
id: "app.advanced",
|
|
1051
|
+
dependencies: {
|
|
1052
|
+
store: globals.resources.store,
|
|
1053
|
+
taskRunner: globals.resources.taskRunner,
|
|
1054
|
+
eventManager: globals.resources.eventManager,
|
|
1055
|
+
},
|
|
1056
|
+
run: async (_, { store, taskRunner, eventManager }) => {
|
|
1057
|
+
// Direct access to the framework internals
|
|
1058
|
+
// (Use with caution!)
|
|
870
1059
|
},
|
|
871
1060
|
});
|
|
1061
|
+
```
|
|
872
1062
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1063
|
+
## Real-World Example: The Complete Package
|
|
1064
|
+
|
|
1065
|
+
Here's a more realistic application structure that shows everything working together:
|
|
1066
|
+
|
|
1067
|
+
```typescript
|
|
1068
|
+
import {
|
|
1069
|
+
resource,
|
|
1070
|
+
task,
|
|
1071
|
+
event,
|
|
1072
|
+
middleware,
|
|
1073
|
+
index,
|
|
1074
|
+
run,
|
|
1075
|
+
createContext,
|
|
1076
|
+
} from "@bluelibs/runner";
|
|
1077
|
+
|
|
1078
|
+
// Configuration
|
|
1079
|
+
const config = resource({
|
|
1080
|
+
id: "app.config",
|
|
1081
|
+
init: async () => ({
|
|
1082
|
+
port: parseInt(process.env.PORT || "3000"),
|
|
1083
|
+
databaseUrl: process.env.DATABASE_URL!,
|
|
1084
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
1085
|
+
}),
|
|
882
1086
|
});
|
|
883
1087
|
|
|
884
|
-
//
|
|
885
|
-
|
|
886
|
-
|
|
1088
|
+
// Database
|
|
1089
|
+
const database = resource({
|
|
1090
|
+
id: "app.database",
|
|
1091
|
+
dependencies: { config },
|
|
1092
|
+
init: async (_, { config }) => {
|
|
1093
|
+
const client = new MongoClient(config.databaseUrl);
|
|
1094
|
+
await client.connect();
|
|
1095
|
+
return client;
|
|
1096
|
+
},
|
|
1097
|
+
dispose: async (client) => await client.close(),
|
|
1098
|
+
});
|
|
887
1099
|
|
|
888
|
-
|
|
1100
|
+
// Context for request data
|
|
1101
|
+
const RequestContext = createContext<{ userId?: string; role?: string }>(
|
|
1102
|
+
"app.requestContext"
|
|
1103
|
+
);
|
|
889
1104
|
|
|
890
|
-
|
|
891
|
-
|
|
1105
|
+
// Events
|
|
1106
|
+
const userRegistered = event<{ userId: string; email: string }>({
|
|
1107
|
+
id: "app.events.userRegistered",
|
|
1108
|
+
});
|
|
892
1109
|
|
|
893
|
-
//
|
|
894
|
-
const
|
|
895
|
-
id: "app.
|
|
896
|
-
async
|
|
897
|
-
|
|
898
|
-
|
|
1110
|
+
// Middleware
|
|
1111
|
+
const authMiddleware = middleware<{ requiredRole?: string }>({
|
|
1112
|
+
id: "app.middleware.auth",
|
|
1113
|
+
run: async ({ task, next }, deps, config) => {
|
|
1114
|
+
const context = RequestContext.use();
|
|
1115
|
+
if (config?.requiredRole && context.role !== config.requiredRole) {
|
|
1116
|
+
throw new Error("Insufficient permissions");
|
|
1117
|
+
}
|
|
1118
|
+
return next(task.input);
|
|
899
1119
|
},
|
|
900
1120
|
});
|
|
901
1121
|
|
|
902
|
-
//
|
|
1122
|
+
// Services
|
|
1123
|
+
const userService = resource({
|
|
1124
|
+
id: "app.services.user",
|
|
1125
|
+
dependencies: { database },
|
|
1126
|
+
init: async (_, { database }) => ({
|
|
1127
|
+
async createUser(userData: { name: string; email: string }) {
|
|
1128
|
+
const users = database.collection("users");
|
|
1129
|
+
const result = await users.insertOne(userData);
|
|
1130
|
+
return { id: result.insertedId.toString(), ...userData };
|
|
1131
|
+
},
|
|
1132
|
+
}),
|
|
1133
|
+
});
|
|
903
1134
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1135
|
+
// Business Logic
|
|
1136
|
+
const registerUser = task({
|
|
1137
|
+
id: "app.tasks.registerUser",
|
|
1138
|
+
dependencies: { userService, userRegistered },
|
|
1139
|
+
run: async (userData, { userService, userRegistered }) => {
|
|
1140
|
+
const user = await userService.createUser(userData);
|
|
1141
|
+
await userRegistered({ userId: user.id, email: user.email });
|
|
1142
|
+
return user;
|
|
910
1143
|
},
|
|
911
1144
|
});
|
|
912
1145
|
|
|
913
|
-
const
|
|
914
|
-
id: "app.
|
|
915
|
-
|
|
916
|
-
async
|
|
917
|
-
|
|
918
|
-
console.log("After init:", value);
|
|
1146
|
+
const adminOnlyTask = task({
|
|
1147
|
+
id: "app.tasks.adminOnly",
|
|
1148
|
+
middleware: [authMiddleware.with({ requiredRole: "admin" })],
|
|
1149
|
+
run: async () => {
|
|
1150
|
+
return "Top secret admin data";
|
|
919
1151
|
},
|
|
920
1152
|
});
|
|
921
1153
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
console.
|
|
1154
|
+
// Event Handlers
|
|
1155
|
+
const sendWelcomeEmail = task({
|
|
1156
|
+
id: "app.tasks.sendWelcomeEmail",
|
|
1157
|
+
on: userRegistered,
|
|
1158
|
+
run: async (event) => {
|
|
1159
|
+
console.log(`Sending welcome email to ${event.data.email}`);
|
|
1160
|
+
// Email sending logic here
|
|
928
1161
|
},
|
|
929
1162
|
});
|
|
930
1163
|
|
|
931
|
-
//
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
beforeInitTask,
|
|
937
|
-
afterInitTask,
|
|
938
|
-
businessConfigErrorTask,
|
|
939
|
-
],
|
|
1164
|
+
// Group everything together
|
|
1165
|
+
const services = index({
|
|
1166
|
+
userService,
|
|
1167
|
+
registerUser,
|
|
1168
|
+
adminOnlyTask,
|
|
940
1169
|
});
|
|
941
1170
|
|
|
942
|
-
//
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1171
|
+
// Express server
|
|
1172
|
+
const server = resource({
|
|
1173
|
+
id: "app.server",
|
|
1174
|
+
register: [config, database, services, sendWelcomeEmail],
|
|
1175
|
+
dependencies: { config, services },
|
|
1176
|
+
init: async (_, { config, services }) => {
|
|
1177
|
+
const app = express();
|
|
1178
|
+
app.use(express.json());
|
|
1179
|
+
|
|
1180
|
+
// Middleware to set up request context
|
|
1181
|
+
app.use((req, res, next) => {
|
|
1182
|
+
RequestContext.provide(
|
|
1183
|
+
{ userId: req.headers["user-id"], role: req.headers["user-role"] },
|
|
1184
|
+
() => next()
|
|
1185
|
+
);
|
|
1186
|
+
});
|
|
947
1187
|
|
|
948
|
-
|
|
1188
|
+
app.post("/register", async (req, res) => {
|
|
1189
|
+
try {
|
|
1190
|
+
const user = await services.registerUser(req.body);
|
|
1191
|
+
res.json({ success: true, user });
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
res.status(400).json({ error: error.message });
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
949
1196
|
|
|
950
|
-
|
|
1197
|
+
app.get("/admin", async (req, res) => {
|
|
1198
|
+
try {
|
|
1199
|
+
const data = await services.adminOnlyTask();
|
|
1200
|
+
res.json({ data });
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
res.status(403).json({ error: error.message });
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
951
1205
|
|
|
952
|
-
|
|
1206
|
+
const server = app.listen(config.port);
|
|
1207
|
+
console.log(`Server running on port ${config.port}`);
|
|
1208
|
+
return server;
|
|
1209
|
+
},
|
|
1210
|
+
dispose: async (server) => server.close(),
|
|
1211
|
+
});
|
|
953
1212
|
|
|
954
|
-
|
|
955
|
-
|
|
1213
|
+
// Start the application
|
|
1214
|
+
const { dispose } = await run(server);
|
|
956
1215
|
|
|
957
|
-
//
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1216
|
+
// Graceful shutdown
|
|
1217
|
+
process.on("SIGTERM", async () => {
|
|
1218
|
+
console.log("Shutting down gracefully...");
|
|
1219
|
+
await dispose();
|
|
1220
|
+
process.exit(0);
|
|
961
1221
|
});
|
|
1222
|
+
```
|
|
962
1223
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1224
|
+
## Testing: Actually Enjoyable
|
|
1225
|
+
|
|
1226
|
+
### Unit Testing: Mock Everything, Test Everything
|
|
1227
|
+
|
|
1228
|
+
Unit testing is straightforward because everything is explicit:
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
describe("registerUser task", () => {
|
|
1232
|
+
it("should create a user and emit event", async () => {
|
|
1233
|
+
const mockUserService = {
|
|
1234
|
+
createUser: jest.fn().mockResolvedValue({ id: "123", name: "John" }),
|
|
1235
|
+
};
|
|
1236
|
+
const mockEvent = jest.fn();
|
|
1237
|
+
|
|
1238
|
+
const result = await registerUser.run(
|
|
1239
|
+
{ name: "John", email: "john@example.com" },
|
|
1240
|
+
{ userService: mockUserService, userRegistered: mockEvent }
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
expect(result.id).toBe("123");
|
|
1244
|
+
expect(mockEvent).toHaveBeenCalledWith({
|
|
1245
|
+
userId: "123",
|
|
1246
|
+
email: "john@example.com",
|
|
972
1247
|
});
|
|
973
|
-
}
|
|
1248
|
+
});
|
|
974
1249
|
});
|
|
975
|
-
|
|
976
|
-
run(app);
|
|
977
1250
|
```
|
|
978
1251
|
|
|
979
|
-
|
|
1252
|
+
### Integration Testing: The Real Deal
|
|
980
1253
|
|
|
981
|
-
|
|
982
|
-
type Config = {
|
|
983
|
-
port: number;
|
|
984
|
-
};
|
|
1254
|
+
Integration testing with overrides lets you test the whole system with controlled components:
|
|
985
1255
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
app.listen(config.port);
|
|
991
|
-
return app;
|
|
992
|
-
},
|
|
1256
|
+
```typescript
|
|
1257
|
+
const testDatabase = resource({
|
|
1258
|
+
id: "app.database",
|
|
1259
|
+
init: async () => new MemoryDatabase(), // In-memory test database
|
|
993
1260
|
});
|
|
994
1261
|
|
|
995
|
-
const
|
|
996
|
-
id: "app",
|
|
997
|
-
register: [
|
|
998
|
-
|
|
999
|
-
express: expressResource,
|
|
1000
|
-
},
|
|
1001
|
-
init: async (_, { express }) => {
|
|
1002
|
-
// type is automagically infered.
|
|
1003
|
-
express.get("/", (req, res) => {
|
|
1004
|
-
res.send("Hello World!");
|
|
1005
|
-
});
|
|
1006
|
-
},
|
|
1262
|
+
const testApp = resource({
|
|
1263
|
+
id: "test.app",
|
|
1264
|
+
register: [productionApp],
|
|
1265
|
+
overrides: [testDatabase], // Replace real database with test one
|
|
1007
1266
|
});
|
|
1008
1267
|
|
|
1009
|
-
|
|
1268
|
+
describe("Full application", () => {
|
|
1269
|
+
it("should handle user registration flow", async () => {
|
|
1270
|
+
const { dispose } = await run(testApp);
|
|
1271
|
+
|
|
1272
|
+
// Test your application end-to-end
|
|
1273
|
+
|
|
1274
|
+
await dispose(); // Clean up
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1010
1277
|
```
|
|
1011
1278
|
|
|
1012
|
-
|
|
1279
|
+
## Semaphore
|
|
1013
1280
|
|
|
1014
|
-
|
|
1281
|
+
Ever had too many database connections competing for resources? Your connection pool under pressure? The `Semaphore` is here to manage concurrent operations like a professional traffic controller.
|
|
1015
1282
|
|
|
1016
|
-
|
|
1283
|
+
Think of it as a VIP rope at an exclusive venue. Only a limited number of operations can proceed at once. The rest wait in an orderly queue like well-behaved async functions.
|
|
1017
1284
|
|
|
1018
|
-
|
|
1019
|
-
type SecurityResourceConfig = {
|
|
1020
|
-
hasher: (str: string) => string;
|
|
1021
|
-
};
|
|
1285
|
+
### Quick Start
|
|
1022
1286
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1287
|
+
```typescript
|
|
1288
|
+
import { Semaphore } from "@bluelibs/runner";
|
|
1289
|
+
|
|
1290
|
+
// Create a semaphore that allows max 5 concurrent operations
|
|
1291
|
+
const dbSemaphore = new Semaphore(5);
|
|
1292
|
+
|
|
1293
|
+
// Basic usage - acquire and release manually
|
|
1294
|
+
await dbSemaphore.acquire();
|
|
1295
|
+
try {
|
|
1296
|
+
// Do your database magic here
|
|
1297
|
+
const result = await db.query("SELECT * FROM users");
|
|
1298
|
+
console.log(result);
|
|
1299
|
+
} finally {
|
|
1300
|
+
dbSemaphore.release(); // Critical: always release to prevent bottlenecks
|
|
1301
|
+
}
|
|
1302
|
+
```
|
|
1031
1303
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1304
|
+
### The Elegant Approach: withPermit()
|
|
1305
|
+
|
|
1306
|
+
Why manage permits manually when you can let the semaphore do the heavy lifting?
|
|
1307
|
+
|
|
1308
|
+
```typescript
|
|
1309
|
+
// The elegant approach - automatic cleanup guaranteed!
|
|
1310
|
+
const users = await dbSemaphore.withPermit(async () => {
|
|
1311
|
+
return await db.query("SELECT * FROM users WHERE active = true");
|
|
1035
1312
|
});
|
|
1036
1313
|
```
|
|
1037
1314
|
|
|
1038
|
-
|
|
1315
|
+
### Timeout Support
|
|
1039
1316
|
|
|
1040
|
-
|
|
1041
|
-
import { resource, run, event } from "@bluelibs/runner";
|
|
1317
|
+
Prevent operations from hanging indefinitely with configurable timeouts:
|
|
1042
1318
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1319
|
+
```typescript
|
|
1320
|
+
try {
|
|
1321
|
+
// Wait max 5 seconds, then throw timeout error
|
|
1322
|
+
await dbSemaphore.acquire({ timeout: 5000 });
|
|
1323
|
+
// Your code here
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
console.log("Operation timed out waiting for permit");
|
|
1326
|
+
}
|
|
1046
1327
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
async run(event, deps) {
|
|
1054
|
-
const { config, value } = event.data;
|
|
1055
|
-
const security = value;
|
|
1328
|
+
// Or with withPermit
|
|
1329
|
+
const result = await dbSemaphore.withPermit(
|
|
1330
|
+
async () => await slowDatabaseOperation(),
|
|
1331
|
+
{ timeout: 10000 } // 10 second timeout
|
|
1332
|
+
);
|
|
1333
|
+
```
|
|
1056
1334
|
|
|
1057
|
-
|
|
1058
|
-
security.setHasher((input) => {
|
|
1059
|
-
// Implement custom hashing logic here
|
|
1060
|
-
console.log("Hashing input:", input);
|
|
1061
|
-
});
|
|
1062
|
-
},
|
|
1063
|
-
});
|
|
1335
|
+
### Cancellation Support
|
|
1064
1336
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1337
|
+
Operations can be cancelled using AbortSignal:
|
|
1338
|
+
|
|
1339
|
+
```typescript
|
|
1340
|
+
const controller = new AbortController();
|
|
1341
|
+
|
|
1342
|
+
// Start an operation
|
|
1343
|
+
const operationPromise = dbSemaphore.withPermit(
|
|
1344
|
+
async () => await veryLongOperation(),
|
|
1345
|
+
{ signal: controller.signal }
|
|
1346
|
+
);
|
|
1347
|
+
|
|
1348
|
+
// Cancel the operation after 3 seconds
|
|
1349
|
+
setTimeout(() => {
|
|
1350
|
+
controller.abort();
|
|
1351
|
+
}, 3000);
|
|
1352
|
+
|
|
1353
|
+
try {
|
|
1354
|
+
await operationPromise;
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
console.log("Operation was cancelled");
|
|
1357
|
+
}
|
|
1070
1358
|
```
|
|
1071
1359
|
|
|
1072
|
-
|
|
1360
|
+
### Monitoring: Metrics & Debugging
|
|
1073
1361
|
|
|
1074
|
-
|
|
1075
|
-
import { resource, run, event } from "@bluelibs/runner";
|
|
1362
|
+
Want to know what's happening under the hood?
|
|
1076
1363
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1364
|
+
```typescript
|
|
1365
|
+
// Get comprehensive metrics
|
|
1366
|
+
const metrics = dbSemaphore.getMetrics();
|
|
1367
|
+
console.log(`
|
|
1368
|
+
Semaphore Status Report:
|
|
1369
|
+
Available permits: ${metrics.availablePermits}/${metrics.maxPermits}
|
|
1370
|
+
Operations waiting: ${metrics.waitingCount}
|
|
1371
|
+
Utilization: ${(metrics.utilization * 100).toFixed(1)}%
|
|
1372
|
+
Disposed: ${metrics.disposed ? "Yes" : "No"}
|
|
1373
|
+
`);
|
|
1374
|
+
|
|
1375
|
+
// Quick checks
|
|
1376
|
+
console.log(`Available permits: ${dbSemaphore.getAvailablePermits()}`);
|
|
1377
|
+
console.log(`Queue length: ${dbSemaphore.getWaitingCount()}`);
|
|
1378
|
+
console.log(`Is disposed: ${dbSemaphore.isDisposed()}`);
|
|
1379
|
+
```
|
|
1080
1380
|
|
|
1081
|
-
|
|
1082
|
-
id: "app.security",
|
|
1083
|
-
dependencies: {
|
|
1084
|
-
securityConfigurationPhaseEvent,
|
|
1085
|
-
},
|
|
1086
|
-
async init(config: SecurityOptions) {
|
|
1087
|
-
// Give the ability to other listeners to modify the configuration
|
|
1088
|
-
securityConfigurationPhaseEvent(config);
|
|
1089
|
-
Objecte.freeze(config);
|
|
1381
|
+
### Resource Cleanup
|
|
1090
1382
|
|
|
1091
|
-
|
|
1092
|
-
// ... based on config
|
|
1093
|
-
};
|
|
1094
|
-
},
|
|
1095
|
-
});
|
|
1383
|
+
Properly dispose of semaphores when finished:
|
|
1096
1384
|
|
|
1097
|
-
|
|
1385
|
+
```typescript
|
|
1386
|
+
// Reject all waiting operations and prevent new ones
|
|
1387
|
+
dbSemaphore.dispose();
|
|
1098
1388
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
async run(event, deps) {
|
|
1103
|
-
const { config } = event.data; // config is SecurityOptions
|
|
1104
|
-
config.setHasher(newHashFunction); // Apply the new hash function
|
|
1105
|
-
},
|
|
1106
|
-
});
|
|
1389
|
+
// All waiting operations will be rejected with:
|
|
1390
|
+
// Error: "Semaphore has been disposed"
|
|
1391
|
+
```
|
|
1107
1392
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1393
|
+
## Queue
|
|
1394
|
+
|
|
1395
|
+
_The orderly guardian of chaos, the diplomatic bouncer of async operations._
|
|
1396
|
+
|
|
1397
|
+
The `Queue` class is your friendly neighborhood task coordinator. Think of it as a very polite but firm British queue-master who ensures everyone waits their turn, prevents cutting in line, and gracefully handles when it's time to close shop.
|
|
1398
|
+
|
|
1399
|
+
### **FIFO Ordering**
|
|
1400
|
+
|
|
1401
|
+
Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
|
|
1402
|
+
|
|
1403
|
+
### **Deadlock Detective**
|
|
1404
|
+
|
|
1405
|
+
Using the clever `AsyncLocalStorage`, our Queue can detect when a task tries to queue another task (the async equivalent of "yo dawg, I heard you like queues..."). When caught red-handed, it politely but firmly rejects with a deadlock error.
|
|
1406
|
+
|
|
1407
|
+
### **Graceful Disposal & Cancellation**
|
|
1408
|
+
|
|
1409
|
+
The Queue provides cooperative cancellation through the Web Standard `AbortController`:
|
|
1410
|
+
|
|
1411
|
+
- **Patient mode** (default): Waits for all queued tasks to complete naturally
|
|
1412
|
+
- **Cancel mode**: Signals running tasks to abort via `AbortSignal`, enabling early termination
|
|
1413
|
+
|
|
1414
|
+
### Basic Example
|
|
1415
|
+
|
|
1416
|
+
```typescript
|
|
1417
|
+
import { Queue } from "@bluelibs/runner";
|
|
1418
|
+
|
|
1419
|
+
const queue = new Queue();
|
|
1420
|
+
|
|
1421
|
+
// Queue up some work
|
|
1422
|
+
const result = await queue.run(async (signal) => {
|
|
1423
|
+
// Your async task here
|
|
1424
|
+
return "Task completed";
|
|
1112
1425
|
});
|
|
1426
|
+
|
|
1427
|
+
// Graceful shutdown
|
|
1428
|
+
await queue.dispose();
|
|
1113
1429
|
```
|
|
1114
1430
|
|
|
1115
|
-
###
|
|
1431
|
+
### AbortController Integration
|
|
1116
1432
|
|
|
1117
|
-
|
|
1433
|
+
The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
|
|
1118
1434
|
|
|
1119
|
-
|
|
1120
|
-
import { resource, run, event } from "@bluelibs/runner";
|
|
1435
|
+
#### Example: Long-running Task with Cancellation Support
|
|
1121
1436
|
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
id: "app.security",
|
|
1125
|
-
async init() {
|
|
1126
|
-
// returns a security service
|
|
1127
|
-
},
|
|
1128
|
-
});
|
|
1437
|
+
```typescript
|
|
1438
|
+
const queue = new Queue();
|
|
1129
1439
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1440
|
+
// Task that respects cancellation
|
|
1441
|
+
const processLargeDataset = queue.run(async (signal) => {
|
|
1442
|
+
const items = await fetchLargeDataset();
|
|
1443
|
+
|
|
1444
|
+
for (const item of items) {
|
|
1445
|
+
// Check for cancellation before processing each item
|
|
1446
|
+
if (signal.aborted) {
|
|
1447
|
+
throw new Error("Operation was cancelled");
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
await processItem(item);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return "Dataset processed successfully";
|
|
1135
1454
|
});
|
|
1136
1455
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1456
|
+
// Cancel all running tasks
|
|
1457
|
+
await queue.dispose({ cancel: true });
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
#### Example: Network Request with Timeout
|
|
1461
|
+
|
|
1462
|
+
```typescript
|
|
1463
|
+
const queue = new Queue();
|
|
1464
|
+
|
|
1465
|
+
const fetchWithCancellation = queue.run(async (signal) => {
|
|
1466
|
+
try {
|
|
1467
|
+
// Pass the signal to fetch for automatic cancellation
|
|
1468
|
+
const response = await fetch("https://api.example.com/data", { signal });
|
|
1469
|
+
return await response.json();
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (error.name === "AbortError") {
|
|
1472
|
+
console.log("Request was cancelled");
|
|
1473
|
+
throw error;
|
|
1474
|
+
}
|
|
1475
|
+
throw error;
|
|
1476
|
+
}
|
|
1141
1477
|
});
|
|
1478
|
+
|
|
1479
|
+
// This will cancel the fetch request if still pending
|
|
1480
|
+
await queue.dispose({ cancel: true });
|
|
1142
1481
|
```
|
|
1143
1482
|
|
|
1144
|
-
|
|
1483
|
+
#### Example: File Processing with Progress Tracking
|
|
1145
1484
|
|
|
1146
|
-
|
|
1485
|
+
```typescript
|
|
1486
|
+
const queue = new Queue();
|
|
1147
1487
|
|
|
1148
|
-
|
|
1488
|
+
const processFiles = queue.run(async (signal) => {
|
|
1489
|
+
const files = await getFileList();
|
|
1490
|
+
const results = [];
|
|
1149
1491
|
|
|
1150
|
-
|
|
1492
|
+
for (let i = 0; i < files.length; i++) {
|
|
1493
|
+
// Respect cancellation
|
|
1494
|
+
signal.throwIfAborted();
|
|
1151
1495
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1496
|
+
const result = await processFile(files[i]);
|
|
1497
|
+
results.push(result);
|
|
1154
1498
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
run: async (_, { logger }) => {
|
|
1161
|
-
await logger.info("Hello World!");
|
|
1162
|
-
// or logger.log(level, data);
|
|
1163
|
-
},
|
|
1499
|
+
// Optional: Report progress
|
|
1500
|
+
console.log(`Processed ${i + 1}/${files.length} files`);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
return results;
|
|
1164
1504
|
});
|
|
1165
1505
|
```
|
|
1166
1506
|
|
|
1167
|
-
|
|
1507
|
+
## The Magic Behind the Curtain
|
|
1168
1508
|
|
|
1169
|
-
|
|
1170
|
-
| ------------ | ----------------------------------------- | -------------------------------------- |
|
|
1171
|
-
| **trace** | Very detailed logs, usually for debugging | "Entering function X with params Y." |
|
|
1172
|
-
| **debug** | Detailed debug information | "Fetching user data: userId=123." |
|
|
1173
|
-
| **info** | General application information | "Service started on port 8080." |
|
|
1174
|
-
| **warn** | Indicates a potential issue | "Disk space running low." |
|
|
1175
|
-
| **error** | Indicates a significant problem | "Unable to connect to database." |
|
|
1176
|
-
| **critical** | Serious problem causing a crash | "System out of memory, shutting down." |
|
|
1509
|
+
### Internal State
|
|
1177
1510
|
|
|
1178
|
-
|
|
1511
|
+
- `tail`: The promise chain that maintains FIFO execution order
|
|
1512
|
+
- `disposed`: Boolean flag indicating whether the queue accepts new tasks
|
|
1513
|
+
- `abortController`: Centralized cancellation controller that provides `AbortSignal` to all tasks
|
|
1514
|
+
- `executionContext`: AsyncLocalStorage-based deadlock detection mechanism
|
|
1179
1515
|
|
|
1180
|
-
|
|
1516
|
+
### Error Handling
|
|
1181
1517
|
|
|
1182
|
-
|
|
1518
|
+
- **"Queue has been disposed"**: You tried to add work after closing time
|
|
1519
|
+
- **"Dead-lock detected"**: A task tried to queue another task (infinite recursion prevention)
|
|
1183
1520
|
|
|
1184
|
-
|
|
1185
|
-
import { task, run, event, globals, resource } from "@bluelibs/runner";
|
|
1521
|
+
### Best Practices
|
|
1186
1522
|
|
|
1187
|
-
|
|
1523
|
+
#### 1. Always Dispose Resources
|
|
1188
1524
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
},
|
|
1198
|
-
});
|
|
1525
|
+
```typescript
|
|
1526
|
+
const queue = new Queue();
|
|
1527
|
+
try {
|
|
1528
|
+
await queue.run(task);
|
|
1529
|
+
} finally {
|
|
1530
|
+
await queue.dispose();
|
|
1531
|
+
}
|
|
1532
|
+
```
|
|
1199
1533
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1534
|
+
#### 2. Implement Cooperative Cancellation
|
|
1535
|
+
|
|
1536
|
+
Tasks should regularly check the `AbortSignal` and respond appropriately:
|
|
1537
|
+
|
|
1538
|
+
```typescript
|
|
1539
|
+
// Preferred: Use signal.throwIfAborted() for immediate termination
|
|
1540
|
+
signal.throwIfAborted();
|
|
1204
1541
|
|
|
1205
|
-
//
|
|
1542
|
+
// Alternative: Check signal.aborted for custom handling
|
|
1543
|
+
if (signal.aborted) {
|
|
1544
|
+
cleanup();
|
|
1545
|
+
throw new Error("Operation cancelled");
|
|
1546
|
+
}
|
|
1206
1547
|
```
|
|
1207
1548
|
|
|
1208
|
-
|
|
1549
|
+
#### 3. Integrate with Native APIs
|
|
1209
1550
|
|
|
1210
|
-
|
|
1551
|
+
Many Web APIs accept `AbortSignal`:
|
|
1211
1552
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1553
|
+
- `fetch(url, { signal })`
|
|
1554
|
+
- `setTimeout(callback, delay, { signal })`
|
|
1555
|
+
- Custom async operations
|
|
1214
1556
|
|
|
1215
|
-
|
|
1557
|
+
### 4. Avoid Nested Queuing
|
|
1216
1558
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
run
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
}
|
|
1559
|
+
The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
|
|
1560
|
+
|
|
1561
|
+
### 5. Handle AbortError Gracefully
|
|
1562
|
+
|
|
1563
|
+
```typescript
|
|
1564
|
+
try {
|
|
1565
|
+
await queue.run(task);
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
if (error.name === "AbortError") {
|
|
1568
|
+
// Expected cancellation, handle appropriately
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
throw error; // Re-throw unexpected errors
|
|
1572
|
+
}
|
|
1231
1573
|
```
|
|
1232
1574
|
|
|
1233
|
-
|
|
1575
|
+
---
|
|
1234
1576
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1577
|
+
_Cooperative task scheduling with professional-grade cancellation support_
|
|
1578
|
+
|
|
1579
|
+
## Real-World Examples
|
|
1580
|
+
|
|
1581
|
+
### Database Connection Pool Manager
|
|
1582
|
+
|
|
1583
|
+
```typescript
|
|
1584
|
+
class DatabaseManager {
|
|
1585
|
+
private semaphore = new Semaphore(10); // Max 10 concurrent queries
|
|
1586
|
+
|
|
1587
|
+
async query(sql: string, params?: any[]) {
|
|
1588
|
+
return this.semaphore.withPermit(
|
|
1589
|
+
async () => {
|
|
1590
|
+
const connection = await this.pool.getConnection();
|
|
1591
|
+
try {
|
|
1592
|
+
return await connection.query(sql, params);
|
|
1593
|
+
} finally {
|
|
1594
|
+
connection.release();
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
{ timeout: 30000 } // 30 second timeout
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
async shutdown() {
|
|
1602
|
+
this.semaphore.dispose();
|
|
1603
|
+
await this.pool.close();
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1250
1606
|
```
|
|
1251
1607
|
|
|
1252
|
-
|
|
1608
|
+
### Rate-Limited API Client
|
|
1253
1609
|
|
|
1254
|
-
|
|
1610
|
+
```typescript
|
|
1611
|
+
class APIClient {
|
|
1612
|
+
private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
|
|
1613
|
+
|
|
1614
|
+
async fetchUser(id: string, signal?: AbortSignal) {
|
|
1615
|
+
return this.rateLimiter.withPermit(
|
|
1616
|
+
async () => {
|
|
1617
|
+
const response = await fetch(`/api/users/${id}`, { signal });
|
|
1618
|
+
return response.json();
|
|
1619
|
+
},
|
|
1620
|
+
{ signal, timeout: 10000 }
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
```
|
|
1255
1625
|
|
|
1256
|
-
|
|
1626
|
+
### Batch Processing with Progress
|
|
1257
1627
|
|
|
1258
|
-
|
|
1628
|
+
```typescript
|
|
1629
|
+
async function processBatch(items: any[]) {
|
|
1630
|
+
const semaphore = new Semaphore(3); // Max 3 concurrent items
|
|
1631
|
+
const results = [];
|
|
1259
1632
|
|
|
1260
|
-
|
|
1633
|
+
console.log("Starting batch processing...");
|
|
1261
1634
|
|
|
1262
|
-
|
|
1635
|
+
for (const [index, item] of items.entries()) {
|
|
1636
|
+
const result = await semaphore.withPermit(async () => {
|
|
1637
|
+
console.log(`Processing item ${index + 1}/${items.length}`);
|
|
1638
|
+
return await processItem(item);
|
|
1639
|
+
});
|
|
1263
1640
|
|
|
1264
|
-
|
|
1265
|
-
import { task, resource } from "@bluelibs/runner";
|
|
1641
|
+
results.push(result);
|
|
1266
1642
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1643
|
+
// Show progress
|
|
1644
|
+
const metrics = semaphore.getMetrics();
|
|
1645
|
+
console.log(
|
|
1646
|
+
`Active: ${metrics.maxPermits - metrics.availablePermits}, Waiting: ${
|
|
1647
|
+
metrics.waitingCount
|
|
1648
|
+
}`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1273
1651
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
});
|
|
1652
|
+
semaphore.dispose();
|
|
1653
|
+
console.log("Batch processing complete!");
|
|
1654
|
+
return results;
|
|
1655
|
+
}
|
|
1656
|
+
```
|
|
1280
1657
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1658
|
+
## Best Practices
|
|
1659
|
+
|
|
1660
|
+
1. **Always dispose**: Clean up your semaphores when finished to prevent memory leaks
|
|
1661
|
+
2. **Use withPermit()**: It's cleaner and prevents resource leaks
|
|
1662
|
+
3. **Set timeouts**: Don't let operations hang forever
|
|
1663
|
+
4. **Monitor metrics**: Keep an eye on utilization to tune your permit count
|
|
1664
|
+
5. **Handle errors**: Timeouts and cancellations throw errors - catch them!
|
|
1665
|
+
|
|
1666
|
+
## Common Pitfalls
|
|
1667
|
+
|
|
1668
|
+
- **Forgetting to release**: Manual acquire/release is error-prone - prefer `withPermit()`
|
|
1669
|
+
- **No timeout**: Operations can hang forever without timeouts
|
|
1670
|
+
- **Ignoring disposal**: Always dispose semaphores to prevent memory leaks
|
|
1671
|
+
- **Wrong permit count**: Too few = slow, too many = defeats the purpose
|
|
1672
|
+
|
|
1673
|
+
## Anonymous IDs: Because Naming Things Is Hard
|
|
1674
|
+
|
|
1675
|
+
One of our favorite quality-of-life features: **anonymous IDs**. Instead of manually naming every component, the framework can generate unique symbol-based identifiers based on your file path and variable name. It's like having a really good naming assistant who never gets tired.
|
|
1676
|
+
|
|
1677
|
+
### How Anonymous IDs Work
|
|
1678
|
+
|
|
1679
|
+
When you omit the `id` property, the framework generates a unique symbol based on file path. Takes up until first 'src' or 'node_modules' and generates based on the paths.
|
|
1680
|
+
|
|
1681
|
+
```typescript
|
|
1682
|
+
// In src/services/email.ts
|
|
1683
|
+
const emailService = resource({
|
|
1684
|
+
// Generated ID: Symbol('services.email.resource')
|
|
1685
|
+
init: async () => new EmailService(),
|
|
1287
1686
|
});
|
|
1288
1687
|
|
|
1289
|
-
//
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1688
|
+
// In src/tasks/user.ts
|
|
1689
|
+
const createUser = task({
|
|
1690
|
+
// Generated ID: Symbol('tasks.user.task')
|
|
1691
|
+
dependencies: { emailService },
|
|
1692
|
+
run: async (userData, { emailService }) => {
|
|
1693
|
+
// Business logic
|
|
1694
|
+
},
|
|
1295
1695
|
});
|
|
1296
1696
|
```
|
|
1297
1697
|
|
|
1298
|
-
###
|
|
1698
|
+
### Benefits of Anonymous IDs
|
|
1299
1699
|
|
|
1300
|
-
|
|
1700
|
+
1. **Less Bikeshedding**: No more debates about naming conventions
|
|
1701
|
+
2. **Automatic Uniqueness**: Guaranteed collision-free identifiers
|
|
1702
|
+
3. **Faster Prototyping**: Just write code, framework handles the rest
|
|
1703
|
+
4. **Refactor-Friendly**: Rename files/variables and IDs update automatically
|
|
1704
|
+
5. **Stack Trace Integration**: Error messages include helpful file locations
|
|
1301
1705
|
|
|
1302
|
-
|
|
1303
|
-
import { task, resource, run, global } from "@bluelibs/runner";
|
|
1706
|
+
### When to Use Manual vs Anonymous IDs
|
|
1304
1707
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1708
|
+
| Use Case | Recommendation | Reason |
|
|
1709
|
+
| ----------------------- | -------------- | --------------------------------------- |
|
|
1710
|
+
| Internal tasks | Anonymous | No external references needed |
|
|
1711
|
+
| Event definitions | Manual | Need predictable names for listeners |
|
|
1712
|
+
| Public APIs | Manual | External modules need stable references |
|
|
1713
|
+
| Middleware | Manual | Often reused across projects |
|
|
1714
|
+
| Configuration resources | Anonymous | Usually internal infrastructure |
|
|
1715
|
+
| Test doubles/mocks | Anonymous | One-off usage in tests |
|
|
1716
|
+
| Cross-module services | Manual | Multiple files depend on them |
|
|
1717
|
+
|
|
1718
|
+
### Anonymous ID Examples by Pattern
|
|
1719
|
+
|
|
1720
|
+
```typescript
|
|
1721
|
+
// ✅ Great for anonymous IDs
|
|
1722
|
+
const database = resource({
|
|
1723
|
+
init: async () => new Database(),
|
|
1724
|
+
dispose: async (db) => db.close(),
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
const processPayment = task({
|
|
1728
|
+
dependencies: { database },
|
|
1729
|
+
run: async (payment, { database }) => {
|
|
1730
|
+
// Internal business logic
|
|
1309
1731
|
},
|
|
1310
1732
|
});
|
|
1311
1733
|
|
|
1734
|
+
// ✅ Better with manual IDs
|
|
1735
|
+
const paymentProcessed = event<{ paymentId: string }>({
|
|
1736
|
+
id: "app.events.paymentProcessed", // Other modules listen to this
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
const authMiddleware = middleware({
|
|
1740
|
+
id: "app.middleware.auth", // Reused across multiple tasks
|
|
1741
|
+
run: async ({ task, next }) => {
|
|
1742
|
+
// Auth logic
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
// ✅ Mixed approach - totally fine!
|
|
1312
1747
|
const app = resource({
|
|
1313
|
-
id: "app",
|
|
1314
|
-
register: [
|
|
1748
|
+
id: "app", // Main entry point gets manual ID
|
|
1749
|
+
register: [
|
|
1750
|
+
database, // Anonymous
|
|
1751
|
+
processPayment, // Anonymous
|
|
1752
|
+
paymentProcessed, // Manual
|
|
1753
|
+
authMiddleware, // Manual
|
|
1754
|
+
],
|
|
1315
1755
|
});
|
|
1316
1756
|
```
|
|
1317
1757
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
```ts
|
|
1321
|
-
describe("app", () => {
|
|
1322
|
-
it("an example to override a task or resource", async () => {
|
|
1323
|
-
const testApp = resource({
|
|
1324
|
-
id: "app.test",
|
|
1325
|
-
register: [myApp], // wrap your existing app
|
|
1326
|
-
overrides: [override], // apply the overrides for "app.myTask"
|
|
1327
|
-
init: async (_, deps) => {
|
|
1328
|
-
// you can now test a task simply by depending on it, and running it, then asserting the response of run()
|
|
1329
|
-
},
|
|
1330
|
-
});
|
|
1758
|
+
### Debugging with Anonymous IDs
|
|
1331
1759
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1760
|
+
Anonymous IDs show up clearly in error messages and logs:
|
|
1761
|
+
|
|
1762
|
+
```typescript
|
|
1763
|
+
// Error message example:
|
|
1764
|
+
// TaskRunError: Task failed at Symbol('tasks.payment.task')
|
|
1765
|
+
// at file:///project/src/tasks/payment.ts:15:23
|
|
1766
|
+
|
|
1767
|
+
// Logging with context:
|
|
1768
|
+
logger.info("Processing payment", {
|
|
1769
|
+
taskId: processPayment.definition.id, // Symbol('tasks.payment.task')
|
|
1770
|
+
file: "src/tasks/payment.ts",
|
|
1334
1771
|
});
|
|
1335
1772
|
```
|
|
1336
1773
|
|
|
1337
|
-
##
|
|
1774
|
+
## Why Choose BlueLibs Runner?
|
|
1775
|
+
|
|
1776
|
+
### What You Get
|
|
1777
|
+
|
|
1778
|
+
- **Type Safety**: Full TypeScript support with intelligent inference
|
|
1779
|
+
- **Testability**: Everything is mockable and testable by design
|
|
1780
|
+
- **Flexibility**: Compose your app however you want
|
|
1781
|
+
- **Performance**: Built-in caching and optimization
|
|
1782
|
+
- **Clarity**: Explicit dependencies, no hidden magic
|
|
1783
|
+
- **Developer Experience**: Helpful error messages and clear patterns
|
|
1784
|
+
|
|
1785
|
+
### What You Don't Get
|
|
1786
|
+
|
|
1787
|
+
- Complex configuration files that require a PhD to understand
|
|
1788
|
+
- Decorator hell that makes your code look like a Christmas tree
|
|
1789
|
+
- Hidden dependencies that break when you least expect it
|
|
1790
|
+
- Framework lock-in that makes you feel trapped
|
|
1791
|
+
- Mysterious behavior at runtime that makes you question reality
|
|
1792
|
+
|
|
1793
|
+
## The Migration Path
|
|
1794
|
+
|
|
1795
|
+
Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
|
|
1796
|
+
|
|
1797
|
+
The beauty of BlueLibs Runner is that you can adopt it incrementally. Start with one task, one resource, and gradually refactor your existing code. No big bang rewrites required - your sanity will thank you.
|
|
1798
|
+
|
|
1799
|
+
## Community & Support
|
|
1800
|
+
|
|
1801
|
+
This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
|
|
1802
|
+
|
|
1803
|
+
- [GitHub Repository](https://github.com/bluelibs/runner) - ⭐ if you find this useful
|
|
1804
|
+
- [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
|
|
1805
|
+
- [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
|
|
1806
|
+
|
|
1807
|
+
## The Bottom Line
|
|
1808
|
+
|
|
1809
|
+
BlueLibs Runner is what happens when you take all the good ideas from modern frameworks and leave out the parts that make you want to switch careers. It's TypeScript-first, test-friendly, and actually makes sense when you read it six months later.
|
|
1810
|
+
|
|
1811
|
+
Give it a try. Your future self (and your team) will thank you.
|
|
1338
1812
|
|
|
1339
|
-
|
|
1813
|
+
_P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's different. (No, really, it is.)_
|
|
1340
1814
|
|
|
1341
1815
|
## License
|
|
1342
1816
|
|