@bluelibs/runner 2.2.4 → 3.0.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 +1315 -942
- 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 +22 -3
- package/dist/define.js +52 -8
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +52 -31
- package/dist/defs.js +10 -2
- package/dist/defs.js.map +1 -1
- package/dist/errors.js +1 -1
- 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 +45 -9
- 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 +2 -1
- package/dist/models/DependencyProcessor.js +11 -13
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.d.ts +5 -0
- 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 +130 -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 +20 -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 +17 -72
- 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.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/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 +144 -0
- 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.middleware.test.ts +166 -12
- package/src/__tests__/run.overrides.test.ts +13 -10
- package/src/__tests__/run.test.ts +363 -12
- package/src/__tests__/setOutput.test.ts +244 -0
- package/src/__tests__/tools/getCallerFile.test.ts +9 -9
- package/src/__tests__/typesafety.test.ts +54 -39
- package/src/context.ts +86 -0
- package/src/define.ts +84 -14
- package/src/defs.ts +91 -41
- package/src/errors.ts +3 -1
- 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 +36 -40
- package/src/models/EventManager.ts +45 -5
- package/src/models/Logger.ts +170 -65
- package/src/models/OverrideManager.ts +84 -0
- package/src/models/Queue.ts +66 -0
- package/src/models/ResourceInitializer.ts +38 -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 +217 -0
- package/src/models/StoreTypes.ts +46 -0
- package/src/models/StoreValidator.ts +38 -0
- package/src/models/TaskRunner.ts +53 -40
- package/src/models/index.ts +3 -0
- package/src/run.ts +7 -4
- 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,1707 @@
|
|
|
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, 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, { port: 3000 });
|
|
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
|
-
|
|
882
|
+
#### 2. Include Context in Errors
|
|
749
883
|
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
857
|
-
|
|
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
|
|
1043
|
+
|
|
1044
|
+
We expose the internal services for advanced use cases (but try not to use them unless you really need to):
|
|
863
1045
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
const app = resource({
|
|
1067
|
-
id: "app",
|
|
1068
|
-
register: [securityResource, afterSecurityInitTask],
|
|
1069
|
-
});
|
|
1070
|
-
```
|
|
1337
|
+
Operations can be cancelled using AbortSignal:
|
|
1071
1338
|
|
|
1072
|
-
|
|
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
|
+
}
|
|
1358
|
+
```
|
|
1073
1359
|
|
|
1074
|
-
|
|
1075
|
-
import { resource, run, event } from "@bluelibs/runner";
|
|
1360
|
+
### Monitoring: Metrics & Debugging
|
|
1076
1361
|
|
|
1077
|
-
|
|
1078
|
-
id: "app.security.configurationPhase",
|
|
1079
|
-
});
|
|
1362
|
+
Want to know what's happening under the hood?
|
|
1080
1363
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
+
```
|
|
1090
1380
|
|
|
1091
|
-
|
|
1092
|
-
// ... based on config
|
|
1093
|
-
};
|
|
1094
|
-
},
|
|
1095
|
-
});
|
|
1381
|
+
### Resource Cleanup
|
|
1096
1382
|
|
|
1097
|
-
|
|
1383
|
+
Properly dispose of semaphores when finished:
|
|
1098
1384
|
|
|
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
|
-
});
|
|
1385
|
+
```typescript
|
|
1386
|
+
// Reject all waiting operations and prevent new ones
|
|
1387
|
+
dbSemaphore.dispose();
|
|
1107
1388
|
|
|
1108
|
-
//
|
|
1109
|
-
|
|
1110
|
-
id: "app",
|
|
1111
|
-
register: [securityResource, securityConfigTask],
|
|
1112
|
-
});
|
|
1389
|
+
// All waiting operations will be rejected with:
|
|
1390
|
+
// Error: "Semaphore has been disposed"
|
|
1113
1391
|
```
|
|
1114
1392
|
|
|
1115
|
-
|
|
1393
|
+
## Queue
|
|
1116
1394
|
|
|
1117
|
-
|
|
1395
|
+
_The orderly guardian of chaos, the diplomatic bouncer of async operations._
|
|
1118
1396
|
|
|
1119
|
-
|
|
1120
|
-
import { resource, run, event } from "@bluelibs/runner";
|
|
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.
|
|
1121
1398
|
|
|
1122
|
-
|
|
1123
|
-
const securityResource = resource({
|
|
1124
|
-
id: "app.security",
|
|
1125
|
-
async init() {
|
|
1126
|
-
// returns a security service
|
|
1127
|
-
},
|
|
1128
|
-
});
|
|
1399
|
+
### **FIFO Ordering**
|
|
1129
1400
|
|
|
1130
|
-
|
|
1131
|
-
...securityResource,
|
|
1132
|
-
init: async () => {
|
|
1133
|
-
// a new and custom service
|
|
1134
|
-
},
|
|
1135
|
-
});
|
|
1401
|
+
Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
|
|
1136
1402
|
|
|
1137
|
-
|
|
1138
|
-
id: "app",
|
|
1139
|
-
register: [securityResource], // this resource might be registered by any element in the dependency tree.
|
|
1140
|
-
overrides: [override],
|
|
1141
|
-
});
|
|
1142
|
-
```
|
|
1403
|
+
### **Deadlock Detective**
|
|
1143
1404
|
|
|
1144
|
-
|
|
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.
|
|
1145
1406
|
|
|
1146
|
-
|
|
1407
|
+
### **Graceful Disposal & Cancellation**
|
|
1147
1408
|
|
|
1148
|
-
|
|
1409
|
+
The Queue provides cooperative cancellation through the Web Standard `AbortController`:
|
|
1149
1410
|
|
|
1150
|
-
|
|
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
|
|
1151
1413
|
|
|
1152
|
-
|
|
1153
|
-
import { task, run, event, globals } from "@bluelibs/runner";
|
|
1414
|
+
### Basic Example
|
|
1154
1415
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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";
|
|
1164
1425
|
});
|
|
1426
|
+
|
|
1427
|
+
// Graceful shutdown
|
|
1428
|
+
await queue.dispose();
|
|
1165
1429
|
```
|
|
1166
1430
|
|
|
1167
|
-
###
|
|
1431
|
+
### AbortController Integration
|
|
1168
1432
|
|
|
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." |
|
|
1433
|
+
The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
|
|
1177
1434
|
|
|
1178
|
-
|
|
1435
|
+
#### Example: Long-running Task with Cancellation Support
|
|
1179
1436
|
|
|
1180
|
-
|
|
1437
|
+
```typescript
|
|
1438
|
+
const queue = new Queue();
|
|
1181
1439
|
|
|
1182
|
-
|
|
1440
|
+
// Task that respects cancellation
|
|
1441
|
+
const processLargeDataset = queue.run(async (signal) => {
|
|
1442
|
+
const items = await fetchLargeDataset();
|
|
1183
1443
|
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
+
}
|
|
1186
1449
|
|
|
1187
|
-
|
|
1450
|
+
await processItem(item);
|
|
1451
|
+
}
|
|
1188
1452
|
|
|
1189
|
-
|
|
1190
|
-
id: "app.task.updatePrintThreshold",
|
|
1191
|
-
on: logger.events.afterInit,
|
|
1192
|
-
// Note: logger is
|
|
1193
|
-
run: async (event, deps) => {
|
|
1194
|
-
const logger = event.data.value;
|
|
1195
|
-
logger.setPrintThreshold("trace"); // will print all logs
|
|
1196
|
-
logger.setPrintThreshold("error"); // will print only "error" and "critical" logs
|
|
1197
|
-
},
|
|
1453
|
+
return "Dataset processed successfully";
|
|
1198
1454
|
});
|
|
1199
1455
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
+
}
|
|
1203
1477
|
});
|
|
1204
1478
|
|
|
1205
|
-
//
|
|
1479
|
+
// This will cancel the fetch request if still pending
|
|
1480
|
+
await queue.dispose({ cancel: true });
|
|
1206
1481
|
```
|
|
1207
1482
|
|
|
1208
|
-
|
|
1483
|
+
#### Example: File Processing with Progress Tracking
|
|
1209
1484
|
|
|
1210
|
-
|
|
1485
|
+
```typescript
|
|
1486
|
+
const queue = new Queue();
|
|
1211
1487
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1488
|
+
const processFiles = queue.run(async (signal) => {
|
|
1489
|
+
const files = await getFileList();
|
|
1490
|
+
const results = [];
|
|
1214
1491
|
|
|
1215
|
-
|
|
1492
|
+
for (let i = 0; i < files.length; i++) {
|
|
1493
|
+
// Respect cancellation
|
|
1494
|
+
signal.throwIfAborted();
|
|
1216
1495
|
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (log.level === "error" || log.level === "critical") {
|
|
1226
|
-
// Ensure no extra log() calls are made here to prevent infinite loops
|
|
1227
|
-
await deps.warehouseService.push(log);
|
|
1228
|
-
}
|
|
1229
|
-
},
|
|
1496
|
+
const result = await processFile(files[i]);
|
|
1497
|
+
results.push(result);
|
|
1498
|
+
|
|
1499
|
+
// Optional: Report progress
|
|
1500
|
+
console.log(`Processed ${i + 1}/${files.length} files`);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
return results;
|
|
1230
1504
|
});
|
|
1231
1505
|
```
|
|
1232
1506
|
|
|
1233
|
-
|
|
1507
|
+
## The Magic Behind the Curtain
|
|
1234
1508
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1509
|
+
### Internal State
|
|
1510
|
+
|
|
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
|
|
1515
|
+
|
|
1516
|
+
### Error Handling
|
|
1517
|
+
|
|
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)
|
|
1520
|
+
|
|
1521
|
+
### Best Practices
|
|
1522
|
+
|
|
1523
|
+
#### 1. Always Dispose Resources
|
|
1524
|
+
|
|
1525
|
+
```typescript
|
|
1526
|
+
const queue = new Queue();
|
|
1527
|
+
try {
|
|
1528
|
+
await queue.run(task);
|
|
1529
|
+
} finally {
|
|
1530
|
+
await queue.dispose();
|
|
1531
|
+
}
|
|
1250
1532
|
```
|
|
1251
1533
|
|
|
1252
|
-
|
|
1534
|
+
#### 2. Implement Cooperative Cancellation
|
|
1253
1535
|
|
|
1254
|
-
|
|
1536
|
+
Tasks should regularly check the `AbortSignal` and respond appropriately:
|
|
1255
1537
|
|
|
1256
|
-
|
|
1538
|
+
```typescript
|
|
1539
|
+
// Preferred: Use signal.throwIfAborted() for immediate termination
|
|
1540
|
+
signal.throwIfAborted();
|
|
1257
1541
|
|
|
1258
|
-
|
|
1542
|
+
// Alternative: Check signal.aborted for custom handling
|
|
1543
|
+
if (signal.aborted) {
|
|
1544
|
+
cleanup();
|
|
1545
|
+
throw new Error("Operation cancelled");
|
|
1546
|
+
}
|
|
1547
|
+
```
|
|
1259
1548
|
|
|
1260
|
-
|
|
1549
|
+
#### 3. Integrate with Native APIs
|
|
1261
1550
|
|
|
1262
|
-
|
|
1551
|
+
Many Web APIs accept `AbortSignal`:
|
|
1263
1552
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1553
|
+
- `fetch(url, { signal })`
|
|
1554
|
+
- `setTimeout(callback, delay, { signal })`
|
|
1555
|
+
- Custom async operations
|
|
1266
1556
|
|
|
1267
|
-
|
|
1268
|
-
id: "app.helloWorld",
|
|
1269
|
-
run: async () => {
|
|
1270
|
-
return "Hello World!";
|
|
1271
|
-
},
|
|
1272
|
-
});
|
|
1557
|
+
### 4. Avoid Nested Queuing
|
|
1273
1558
|
|
|
1274
|
-
|
|
1275
|
-
id: "app.helloWorldResource",
|
|
1276
|
-
init: async () => {
|
|
1277
|
-
return "Hello World!";
|
|
1278
|
-
},
|
|
1279
|
-
});
|
|
1559
|
+
The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
|
|
1280
1560
|
|
|
1281
|
-
|
|
1282
|
-
describe("app.helloWorld", () => {
|
|
1283
|
-
it("should return Hello World!", async () => {
|
|
1284
|
-
const result = await helloWorld.run(input, dependencies); // pass in the arguments and the mocked dependencies.
|
|
1285
|
-
expect(result).toBe("Hello World!");
|
|
1286
|
-
});
|
|
1287
|
-
});
|
|
1561
|
+
### 5. Handle AbortError Gracefully
|
|
1288
1562
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
+
}
|
|
1296
1573
|
```
|
|
1297
1574
|
|
|
1298
|
-
|
|
1575
|
+
---
|
|
1299
1576
|
|
|
1300
|
-
|
|
1577
|
+
_Cooperative task scheduling with professional-grade cancellation support_
|
|
1301
1578
|
|
|
1302
|
-
|
|
1303
|
-
import { task, resource, run, global } from "@bluelibs/runner";
|
|
1579
|
+
## Real-World Examples
|
|
1304
1580
|
|
|
1305
|
-
|
|
1306
|
-
id: "app.myTask",
|
|
1307
|
-
run: async () => {
|
|
1308
|
-
return "Hello World!";
|
|
1309
|
-
},
|
|
1310
|
-
});
|
|
1581
|
+
### Database Connection Pool Manager
|
|
1311
1582
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
+
}
|
|
1316
1606
|
```
|
|
1317
1607
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
```
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1608
|
+
### Rate-Limited API Client
|
|
1609
|
+
|
|
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();
|
|
1329
1619
|
},
|
|
1620
|
+
{ signal, timeout: 10000 }
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
### Batch Processing with Progress
|
|
1627
|
+
|
|
1628
|
+
```typescript
|
|
1629
|
+
async function processBatch(items: any[]) {
|
|
1630
|
+
const semaphore = new Semaphore(3); // Max 3 concurrent items
|
|
1631
|
+
const results = [];
|
|
1632
|
+
|
|
1633
|
+
console.log("Starting batch processing...");
|
|
1634
|
+
|
|
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);
|
|
1330
1639
|
});
|
|
1331
1640
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1641
|
+
results.push(result);
|
|
1642
|
+
|
|
1643
|
+
// Show progress
|
|
1644
|
+
const metrics = semaphore.getMetrics();
|
|
1645
|
+
console.log(
|
|
1646
|
+
`Active: ${metrics.maxPermits - metrics.availablePermits}, Waiting: ${
|
|
1647
|
+
metrics.waitingCount
|
|
1648
|
+
}`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
semaphore.dispose();
|
|
1653
|
+
console.log("Batch processing complete!");
|
|
1654
|
+
return results;
|
|
1655
|
+
}
|
|
1335
1656
|
```
|
|
1336
1657
|
|
|
1337
|
-
##
|
|
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
|
+
## Why Choose BlueLibs Runner?
|
|
1674
|
+
|
|
1675
|
+
### What You Get
|
|
1676
|
+
|
|
1677
|
+
- **Type Safety**: Full TypeScript support with intelligent inference
|
|
1678
|
+
- **Testability**: Everything is mockable and testable by design
|
|
1679
|
+
- **Flexibility**: Compose your app however you want
|
|
1680
|
+
- **Performance**: Built-in caching and optimization
|
|
1681
|
+
- **Clarity**: Explicit dependencies, no hidden magic
|
|
1682
|
+
- **Developer Experience**: Helpful error messages and clear patterns
|
|
1683
|
+
|
|
1684
|
+
### What You Don't Get
|
|
1685
|
+
|
|
1686
|
+
- Complex configuration files that require a PhD to understand
|
|
1687
|
+
- Decorator hell that makes your code look like a Christmas tree
|
|
1688
|
+
- Hidden dependencies that break when you least expect it
|
|
1689
|
+
- Framework lock-in that makes you feel trapped
|
|
1690
|
+
- Mysterious behavior at runtime that makes you question reality
|
|
1691
|
+
|
|
1692
|
+
## The Migration Path
|
|
1693
|
+
|
|
1694
|
+
Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
|
|
1695
|
+
|
|
1696
|
+
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.
|
|
1697
|
+
|
|
1698
|
+
## Community & Support
|
|
1699
|
+
|
|
1700
|
+
This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
|
|
1701
|
+
|
|
1702
|
+
- [GitHub Repository](https://github.com/bluelibs/runner) - ⭐ if you find this useful
|
|
1703
|
+
- [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
|
|
1704
|
+
- [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
|
|
1705
|
+
|
|
1706
|
+
## The Bottom Line
|
|
1707
|
+
|
|
1708
|
+
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.
|
|
1709
|
+
|
|
1710
|
+
Give it a try. Your future self (and your team) will thank you.
|
|
1338
1711
|
|
|
1339
|
-
|
|
1712
|
+
_P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's different. (No, really, it is.)_
|
|
1340
1713
|
|
|
1341
1714
|
## License
|
|
1342
1715
|
|