@bluelibs/runner 3.4.1 → 3.4.2
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 +77 -281
- package/dist/define.d.ts +67 -3
- package/dist/define.js +84 -20
- package/dist/define.js.map +1 -1
- package/dist/tools/getCallerFile.js +16 -6
- package/dist/tools/getCallerFile.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/globalEvents.test.ts +1 -1
- package/src/__tests__/run.anonymous.test.ts +27 -0
- package/src/__tests__/tools/getCallerFile.test.ts +20 -5
- package/src/define.ts +101 -25
- package/src/tools/getCallerFile.ts +18 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# BlueLibs Runner
|
|
1
|
+
# BlueLibs Runner
|
|
2
2
|
|
|
3
3
|
_Or: How I Learned to Stop Worrying and Love Dependency Injection_
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ Welcome to BlueLibs Runner, where we've taken the chaos of modern application ar
|
|
|
17
17
|
|
|
18
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.
|
|
19
19
|
|
|
20
|
-
### The Core
|
|
20
|
+
### The Core
|
|
21
21
|
|
|
22
22
|
- **Tasks are functions** - Not classes with 47 methods you'll never use
|
|
23
23
|
- **Resources are singletons** - Database connections, configs, services - the usual suspects
|
|
@@ -25,7 +25,7 @@ BlueLibs Runner is a TypeScript-first framework that embraces functional program
|
|
|
25
25
|
- **Everything is async** - Because it's 2025 and blocking code is so 2005
|
|
26
26
|
- **Explicit beats implicit** - No magic, no surprises, no "how the hell does this work?"
|
|
27
27
|
|
|
28
|
-
## Quick Start
|
|
28
|
+
## Quick Start
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm install @bluelibs/runner
|
|
@@ -77,9 +77,11 @@ const app = resource({
|
|
|
77
77
|
const { dispose } = await run(app);
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
## The Big Four
|
|
80
|
+
## The Big Four
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Another term to define them would be TERM. (tasks, events, resources, middleware)
|
|
83
|
+
|
|
84
|
+
### Tasks
|
|
83
85
|
|
|
84
86
|
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.
|
|
85
87
|
|
|
@@ -100,8 +102,6 @@ const result = await sendEmail.run(
|
|
|
100
102
|
);
|
|
101
103
|
```
|
|
102
104
|
|
|
103
|
-
#### When to Task and When Not to Task
|
|
104
|
-
|
|
105
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:
|
|
106
106
|
|
|
107
107
|
**Make it a task when:**
|
|
@@ -119,9 +119,9 @@ Look, we get it. You could turn every function into a task, but that's like usin
|
|
|
119
119
|
|
|
120
120
|
Think of tasks as the "main characters" in your application story, not every single line of dialogue.
|
|
121
121
|
|
|
122
|
-
###
|
|
122
|
+
### Resources
|
|
123
123
|
|
|
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.
|
|
124
|
+
Resources are the singletons, 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.
|
|
125
125
|
|
|
126
126
|
```typescript
|
|
127
127
|
const database = resource({
|
|
@@ -149,7 +149,7 @@ const userService = resource({
|
|
|
149
149
|
});
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
#### Resource Configuration
|
|
152
|
+
#### Resource Configuration
|
|
153
153
|
|
|
154
154
|
Resources can be configured with type-safe options. No more "config object of unknown shape" nonsense.
|
|
155
155
|
|
|
@@ -181,7 +181,7 @@ const app = resource({
|
|
|
181
181
|
});
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
####
|
|
184
|
+
#### Private Context
|
|
185
185
|
|
|
186
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
187
|
|
|
@@ -210,7 +210,7 @@ const dbResource = resource({
|
|
|
210
210
|
});
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
-
###
|
|
213
|
+
### Events
|
|
214
214
|
|
|
215
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.
|
|
216
216
|
|
|
@@ -242,7 +242,7 @@ const sendWelcomeEmail = task({
|
|
|
242
242
|
});
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
#### Wildcard Events
|
|
245
|
+
#### Wildcard Events
|
|
246
246
|
|
|
247
247
|
Sometimes you need to be the nosy neighbor of your application:
|
|
248
248
|
|
|
@@ -257,7 +257,7 @@ const logAllEventsTask = task({
|
|
|
257
257
|
});
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
####
|
|
260
|
+
#### Built-in Events
|
|
261
261
|
|
|
262
262
|
Tasks and resources have their own lifecycle events that you can hook into:
|
|
263
263
|
|
|
@@ -275,7 +275,7 @@ const myResource = resource({ ... });
|
|
|
275
275
|
|
|
276
276
|
Each event has its own utilities and functions.
|
|
277
277
|
|
|
278
|
-
#### Global Events
|
|
278
|
+
#### Global Events
|
|
279
279
|
|
|
280
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:
|
|
281
281
|
|
|
@@ -296,7 +296,7 @@ const taskLogger = task({
|
|
|
296
296
|
});
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
####
|
|
299
|
+
#### stopPropagation()
|
|
300
300
|
|
|
301
301
|
Sometimes you need to prevent other event listeners from processing an event. The `stopPropagation()` method gives you fine-grained control over event flow:
|
|
302
302
|
|
|
@@ -333,7 +333,7 @@ const emergencyHandler = task({
|
|
|
333
333
|
});
|
|
334
334
|
```
|
|
335
335
|
|
|
336
|
-
###
|
|
336
|
+
### Middleware
|
|
337
337
|
|
|
338
338
|
Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
|
|
339
339
|
|
|
@@ -365,7 +365,7 @@ const adminTask = task({
|
|
|
365
365
|
});
|
|
366
366
|
```
|
|
367
367
|
|
|
368
|
-
#### Global Middleware
|
|
368
|
+
#### Global Middleware
|
|
369
369
|
|
|
370
370
|
Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
|
|
371
371
|
|
|
@@ -388,9 +388,9 @@ const app = resource({
|
|
|
388
388
|
});
|
|
389
389
|
```
|
|
390
390
|
|
|
391
|
-
## Context
|
|
391
|
+
## Context
|
|
392
392
|
|
|
393
|
-
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.
|
|
393
|
+
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. This is very different from the Private Context from resources.
|
|
394
394
|
|
|
395
395
|
```typescript
|
|
396
396
|
const UserContext = createContext<{ userId: string; role: string }>(
|
|
@@ -418,7 +418,7 @@ const handleRequest = resource({
|
|
|
418
418
|
});
|
|
419
419
|
```
|
|
420
420
|
|
|
421
|
-
### Context with Middleware
|
|
421
|
+
### Context with Middleware
|
|
422
422
|
|
|
423
423
|
Context shines when combined with middleware for request-scoped data:
|
|
424
424
|
|
|
@@ -457,7 +457,7 @@ const handleRequest = task({
|
|
|
457
457
|
});
|
|
458
458
|
```
|
|
459
459
|
|
|
460
|
-
##
|
|
460
|
+
## The Index Pattern
|
|
461
461
|
|
|
462
462
|
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.
|
|
463
463
|
|
|
@@ -482,7 +482,7 @@ const app = resource({
|
|
|
482
482
|
});
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
-
## Error Handling
|
|
485
|
+
## Error Handling
|
|
486
486
|
|
|
487
487
|
Errors happen. When they do, you can listen for them and decide what to do. No more unhandled promise rejections ruining your day.
|
|
488
488
|
|
|
@@ -507,7 +507,7 @@ const errorHandler = task({
|
|
|
507
507
|
});
|
|
508
508
|
```
|
|
509
509
|
|
|
510
|
-
## Caching
|
|
510
|
+
## Caching
|
|
511
511
|
|
|
512
512
|
Because nobody likes waiting for the same expensive operation twice:
|
|
513
513
|
|
|
@@ -601,10 +601,10 @@ The retry middleware can be configured with:
|
|
|
601
601
|
- `delayStrategy`: A function that returns the delay in milliseconds before the next attempt.
|
|
602
602
|
- `stopRetryIf`: A function to prevent retries for certain types of errors.
|
|
603
603
|
|
|
604
|
-
##
|
|
604
|
+
## Timeouts
|
|
605
605
|
|
|
606
606
|
The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
|
|
607
|
-
timeout
|
|
607
|
+
timeout. Works for resources and tasks.
|
|
608
608
|
|
|
609
609
|
```typescript
|
|
610
610
|
import { globals } from "@bluelibs/runner";
|
|
@@ -653,13 +653,13 @@ Best practices:
|
|
|
653
653
|
- Use longer timeouts for resource initialization than task execution
|
|
654
654
|
- Consider network conditions when setting API call timeouts
|
|
655
655
|
|
|
656
|
-
## Logging
|
|
656
|
+
## Logging
|
|
657
657
|
|
|
658
658
|
_The structured logging system that actually makes debugging enjoyable_
|
|
659
659
|
|
|
660
660
|
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).
|
|
661
661
|
|
|
662
|
-
###
|
|
662
|
+
### Basic Logging
|
|
663
663
|
|
|
664
664
|
```typescript
|
|
665
665
|
import { globals } from "@bluelibs/runner";
|
|
@@ -694,7 +694,7 @@ RUNNER_LOG_LEVEL=debug node your-app.js
|
|
|
694
694
|
RUNNER_DISABLE_LOGS=true node your-app.js
|
|
695
695
|
```
|
|
696
696
|
|
|
697
|
-
### Log Levels
|
|
697
|
+
### Log Levels
|
|
698
698
|
|
|
699
699
|
The logger supports six log levels with increasing severity:
|
|
700
700
|
|
|
@@ -717,7 +717,7 @@ logger.error("Houston, we have a problem");
|
|
|
717
717
|
logger.critical("DEFCON 1: Everything is broken");
|
|
718
718
|
```
|
|
719
719
|
|
|
720
|
-
### Structured Logging
|
|
720
|
+
### Structured Logging
|
|
721
721
|
|
|
722
722
|
The logger accepts rich, structured data that makes debugging actually useful:
|
|
723
723
|
|
|
@@ -757,7 +757,7 @@ const userTask = task({
|
|
|
757
757
|
});
|
|
758
758
|
```
|
|
759
759
|
|
|
760
|
-
### Context-Aware Logging
|
|
760
|
+
### Context-Aware Logging
|
|
761
761
|
|
|
762
762
|
Create logger instances with bound context for consistent metadata across related operations:
|
|
763
763
|
|
|
@@ -797,11 +797,11 @@ const requestHandler = task({
|
|
|
797
797
|
});
|
|
798
798
|
```
|
|
799
799
|
|
|
800
|
-
### Print Threshold
|
|
800
|
+
### Print Threshold
|
|
801
801
|
|
|
802
802
|
By default, logs at `info` level and above are automatically printed to console for better developer experience. You can easily control this behavior through environment variables or by setting a print threshold programmatically:
|
|
803
803
|
|
|
804
|
-
|
|
804
|
+
### Environment Variables
|
|
805
805
|
|
|
806
806
|
```bash
|
|
807
807
|
# Disable all logging output
|
|
@@ -812,7 +812,7 @@ RUNNER_LOG_LEVEL=debug node your-app.js
|
|
|
812
812
|
RUNNER_LOG_LEVEL=error node your-app.js
|
|
813
813
|
```
|
|
814
814
|
|
|
815
|
-
|
|
815
|
+
### Programmatic Control
|
|
816
816
|
|
|
817
817
|
```typescript
|
|
818
818
|
// Override the default print threshold programmatically
|
|
@@ -834,7 +834,7 @@ const setupLogging = task({
|
|
|
834
834
|
});
|
|
835
835
|
```
|
|
836
836
|
|
|
837
|
-
### Event-Driven Log Handling
|
|
837
|
+
### Event-Driven Log Handling
|
|
838
838
|
|
|
839
839
|
Every log generates an event that you can listen to. This is where the real power comes in:
|
|
840
840
|
|
|
@@ -895,7 +895,7 @@ const databaseLogHandler = task({
|
|
|
895
895
|
});
|
|
896
896
|
```
|
|
897
897
|
|
|
898
|
-
### Integration with Winston
|
|
898
|
+
### Integration with Winston
|
|
899
899
|
|
|
900
900
|
Want to use Winston as your transport? No problem - integrate it seamlessly:
|
|
901
901
|
|
|
@@ -951,7 +951,7 @@ const winstonBridge = task({
|
|
|
951
951
|
});
|
|
952
952
|
```
|
|
953
953
|
|
|
954
|
-
###
|
|
954
|
+
### Custom Log Formatters
|
|
955
955
|
|
|
956
956
|
Want to customize how logs are printed? You can override the print behavior:
|
|
957
957
|
|
|
@@ -989,7 +989,7 @@ const customLogger = resource({
|
|
|
989
989
|
// Or you could simply add it as "globals.resources.logger" and override the default logger
|
|
990
990
|
```
|
|
991
991
|
|
|
992
|
-
### Log Structure
|
|
992
|
+
### Log Structure
|
|
993
993
|
|
|
994
994
|
Every log event contains:
|
|
995
995
|
|
|
@@ -1012,7 +1012,7 @@ interface ILog {
|
|
|
1012
1012
|
|
|
1013
1013
|
### Debugging Tips & Best Practices
|
|
1014
1014
|
|
|
1015
|
-
|
|
1015
|
+
Use Structured Data Liberally
|
|
1016
1016
|
|
|
1017
1017
|
```typescript
|
|
1018
1018
|
// Bad - hard to search and filter
|
|
@@ -1029,7 +1029,7 @@ await logger.error("Order processing failed", {
|
|
|
1029
1029
|
});
|
|
1030
1030
|
```
|
|
1031
1031
|
|
|
1032
|
-
|
|
1032
|
+
Include Context in Errors
|
|
1033
1033
|
|
|
1034
1034
|
```typescript
|
|
1035
1035
|
// Include relevant context with errors
|
|
@@ -1049,7 +1049,7 @@ try {
|
|
|
1049
1049
|
}
|
|
1050
1050
|
```
|
|
1051
1051
|
|
|
1052
|
-
|
|
1052
|
+
Use Different Log Levels Appropriately
|
|
1053
1053
|
|
|
1054
1054
|
```typescript
|
|
1055
1055
|
// Good level usage
|
|
@@ -1065,7 +1065,7 @@ await logger.error("Database connection failed", {
|
|
|
1065
1065
|
await logger.critical("System out of memory", { data: { available: "0MB" } });
|
|
1066
1066
|
```
|
|
1067
1067
|
|
|
1068
|
-
|
|
1068
|
+
Create Domain-Specific Loggers
|
|
1069
1069
|
|
|
1070
1070
|
```typescript
|
|
1071
1071
|
// Create loggers with domain context
|
|
@@ -1078,13 +1078,13 @@ await paymentLogger.info("Processing payment", { data: paymentData });
|
|
|
1078
1078
|
await authLogger.warn("Failed login attempt", { data: { email, ip } });
|
|
1079
1079
|
```
|
|
1080
1080
|
|
|
1081
|
-
## Meta
|
|
1081
|
+
## Meta
|
|
1082
1082
|
|
|
1083
1083
|
_The structured way to describe what your components do and control their behavior_
|
|
1084
1084
|
|
|
1085
1085
|
Metadata in BlueLibs Runner provides a systematic way to document, categorize, and control the behavior of your tasks, resources, events, and middleware. Think of it as your component's passport - it tells you and your tools everything they need to know about what this component does and how it should be treated.
|
|
1086
1086
|
|
|
1087
|
-
###
|
|
1087
|
+
### Metadata Properties
|
|
1088
1088
|
|
|
1089
1089
|
Every component can have these basic metadata properties:
|
|
1090
1090
|
|
|
@@ -1132,9 +1132,9 @@ const sendWelcomeEmail = task({
|
|
|
1132
1132
|
});
|
|
1133
1133
|
```
|
|
1134
1134
|
|
|
1135
|
-
### Tags
|
|
1135
|
+
### Tags
|
|
1136
1136
|
|
|
1137
|
-
Tags are the most powerful part of the metadata system. They can be simple strings or sophisticated configuration objects that control component behavior.
|
|
1137
|
+
Tags are the most powerful part of the metadata system used for classification. They can be simple strings or sophisticated configuration objects that control component behavior.
|
|
1138
1138
|
|
|
1139
1139
|
#### String Tags for Simple Classification
|
|
1140
1140
|
|
|
@@ -1245,7 +1245,7 @@ const apiEndpoint = task({
|
|
|
1245
1245
|
|
|
1246
1246
|
To process these tags you can hook into `globals.events.afterInit`, use the global store as dependency and use the `getTasksWithTag()` and `getResourcesWithTag()` functionality.
|
|
1247
1247
|
|
|
1248
|
-
####
|
|
1248
|
+
#### Structured Tags
|
|
1249
1249
|
|
|
1250
1250
|
```typescript
|
|
1251
1251
|
const performanceMiddleware = middleware({
|
|
@@ -1285,7 +1285,7 @@ const performanceMiddleware = middleware({
|
|
|
1285
1285
|
});
|
|
1286
1286
|
```
|
|
1287
1287
|
|
|
1288
|
-
#### Contract Tags
|
|
1288
|
+
#### Contract Tags
|
|
1289
1289
|
|
|
1290
1290
|
You can attach contracts to tags to enforce the shape of a task's returned value and a resource's `init()` value at compile time. Contracts are specified via the second generic of `defineTag<TConfig, TContract>`.
|
|
1291
1291
|
|
|
@@ -1347,60 +1347,6 @@ const badTask = task({
|
|
|
1347
1347
|
});
|
|
1348
1348
|
```
|
|
1349
1349
|
|
|
1350
|
-
### When to Use Metadata
|
|
1351
|
-
|
|
1352
|
-
Always strive to provide a title and a description.
|
|
1353
|
-
|
|
1354
|
-
#### ✅ Great Use Cases
|
|
1355
|
-
|
|
1356
|
-
**Documentation & Discovery**
|
|
1357
|
-
|
|
1358
|
-
```typescript
|
|
1359
|
-
const paymentProcessor = resource({
|
|
1360
|
-
meta: {
|
|
1361
|
-
title: "Payment Processing Service",
|
|
1362
|
-
description:
|
|
1363
|
-
"Handles credit card payments via Stripe API with fraud detection",
|
|
1364
|
-
tags: ["payment", "stripe", "pci-compliant", "critical"],
|
|
1365
|
-
},
|
|
1366
|
-
// ... implementation
|
|
1367
|
-
});
|
|
1368
|
-
```
|
|
1369
|
-
|
|
1370
|
-
**Conditional Behavior**
|
|
1371
|
-
|
|
1372
|
-
```typescript
|
|
1373
|
-
const backgroundTask = task({
|
|
1374
|
-
meta: {
|
|
1375
|
-
tags: ["background", "low-priority", retryTag.with({ maxAttempts: 5 })],
|
|
1376
|
-
},
|
|
1377
|
-
// ... implementation
|
|
1378
|
-
});
|
|
1379
|
-
```
|
|
1380
|
-
|
|
1381
|
-
**Cross-Cutting Concerns**
|
|
1382
|
-
|
|
1383
|
-
```typescript
|
|
1384
|
-
// All tasks tagged with "audit" get automatic logging
|
|
1385
|
-
const sensitiveOperation = task({
|
|
1386
|
-
meta: {
|
|
1387
|
-
tags: ["audit", "sensitive", "admin-only"],
|
|
1388
|
-
},
|
|
1389
|
-
// ... implementation
|
|
1390
|
-
});
|
|
1391
|
-
```
|
|
1392
|
-
|
|
1393
|
-
**Environment-Specific Behavior**
|
|
1394
|
-
|
|
1395
|
-
```typescript
|
|
1396
|
-
const developmentTask = task({
|
|
1397
|
-
meta: {
|
|
1398
|
-
tags: ["development-only", debugTag.with({ verbose: true })],
|
|
1399
|
-
},
|
|
1400
|
-
// ... implementation
|
|
1401
|
-
});
|
|
1402
|
-
```
|
|
1403
|
-
|
|
1404
1350
|
### Extending Metadata: Custom Properties
|
|
1405
1351
|
|
|
1406
1352
|
For advanced use cases, you can extend the metadata interfaces to add your own properties:
|
|
@@ -1453,9 +1399,7 @@ const database = resource({
|
|
|
1453
1399
|
});
|
|
1454
1400
|
```
|
|
1455
1401
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
#### Dynamic Middleware Application
|
|
1402
|
+
#### Global Middleware Application
|
|
1459
1403
|
|
|
1460
1404
|
```typescript
|
|
1461
1405
|
const app = resource({
|
|
@@ -1479,7 +1423,7 @@ Metadata transforms your components from anonymous functions into self-documenti
|
|
|
1479
1423
|
|
|
1480
1424
|
## Advanced Usage: When You Need More Power
|
|
1481
1425
|
|
|
1482
|
-
|
|
1426
|
+
## Overrides
|
|
1483
1427
|
|
|
1484
1428
|
Sometimes you need to replace a component entirely. Maybe you're doing integration testing or you want to override a library from an external package.
|
|
1485
1429
|
|
|
@@ -1537,7 +1481,7 @@ const overriddenMiddleware = override(originalMiddleware, {
|
|
|
1537
1481
|
|
|
1538
1482
|
Overrides are applied after everything is registered. If multiple overrides target the same id, the one defined higher in the resource tree (closer to the root) wins, because it’s applied last. Conflicting overrides are allowed; overriding something that wasn’t registered throws. Use override() to change behavior safely while preserving the original id.
|
|
1539
1483
|
|
|
1540
|
-
|
|
1484
|
+
## Namespacing
|
|
1541
1485
|
|
|
1542
1486
|
As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
|
|
1543
1487
|
|
|
@@ -1561,7 +1505,7 @@ const userTask = task({
|
|
|
1561
1505
|
});
|
|
1562
1506
|
```
|
|
1563
1507
|
|
|
1564
|
-
|
|
1508
|
+
## Factory Pattern
|
|
1565
1509
|
|
|
1566
1510
|
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:
|
|
1567
1511
|
|
|
@@ -1585,12 +1529,10 @@ const app = resource({
|
|
|
1585
1529
|
});
|
|
1586
1530
|
```
|
|
1587
1531
|
|
|
1588
|
-
|
|
1532
|
+
## Runtime Validation
|
|
1589
1533
|
|
|
1590
1534
|
BlueLibs Runner includes a generic validation interface that works with any validation library, including [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), [Joi](https://joi.dev/), and others. The framework provides runtime validation with excellent TypeScript inference while remaining library-agnostic.
|
|
1591
1535
|
|
|
1592
|
-
#### The Validation Interface
|
|
1593
|
-
|
|
1594
1536
|
The framework defines a simple `IValidationSchema<T>` interface that any validation library can implement:
|
|
1595
1537
|
|
|
1596
1538
|
```typescript
|
|
@@ -1606,7 +1548,7 @@ Popular validation libraries already implement this interface:
|
|
|
1606
1548
|
- **Joi**: Use `.assert()` or create a wrapper
|
|
1607
1549
|
- **Custom validators**: Implement the interface yourself
|
|
1608
1550
|
|
|
1609
|
-
|
|
1551
|
+
### Task Input Validation
|
|
1610
1552
|
|
|
1611
1553
|
Add an `inputSchema` to any task to validate inputs before execution:
|
|
1612
1554
|
|
|
@@ -1656,7 +1598,7 @@ const app = resource({
|
|
|
1656
1598
|
});
|
|
1657
1599
|
```
|
|
1658
1600
|
|
|
1659
|
-
|
|
1601
|
+
### Resource Config Validation
|
|
1660
1602
|
|
|
1661
1603
|
Add a `configSchema` to resources to validate configurations. **Validation happens immediately when `.with()` is called**, ensuring configuration errors are caught early:
|
|
1662
1604
|
|
|
@@ -1706,7 +1648,7 @@ const app = resource({
|
|
|
1706
1648
|
});
|
|
1707
1649
|
```
|
|
1708
1650
|
|
|
1709
|
-
|
|
1651
|
+
### Event Payload Validation
|
|
1710
1652
|
|
|
1711
1653
|
Add a `payloadSchema` to events to validate payloads every time they're emitted:
|
|
1712
1654
|
|
|
@@ -1755,7 +1697,7 @@ const app = resource({
|
|
|
1755
1697
|
});
|
|
1756
1698
|
```
|
|
1757
1699
|
|
|
1758
|
-
|
|
1700
|
+
### Middleware Config Validation
|
|
1759
1701
|
|
|
1760
1702
|
Add a `configSchema` to middleware to validate configurations. Like resources, **validation happens immediately when `.with()` is called**:
|
|
1761
1703
|
|
|
@@ -1837,7 +1779,7 @@ const paymentTask = task({
|
|
|
1837
1779
|
});
|
|
1838
1780
|
```
|
|
1839
1781
|
|
|
1840
|
-
|
|
1782
|
+
### Error Handling
|
|
1841
1783
|
|
|
1842
1784
|
Validation errors are thrown with clear, descriptive messages that include the component ID:
|
|
1843
1785
|
|
|
@@ -1855,7 +1797,7 @@ Validation errors are thrown with clear, descriptive messages that include the c
|
|
|
1855
1797
|
// "Middleware config validation failed for {middlewareId}: {validationErrorMessage}"
|
|
1856
1798
|
```
|
|
1857
1799
|
|
|
1858
|
-
####
|
|
1800
|
+
#### Other Libraries
|
|
1859
1801
|
|
|
1860
1802
|
The framework works with any validation library that implements the `IValidationSchema<T>` interface:
|
|
1861
1803
|
|
|
@@ -1913,13 +1855,14 @@ While runtime validation happens with your chosen library, TypeScript still enfo
|
|
|
1913
1855
|
|
|
1914
1856
|
```typescript
|
|
1915
1857
|
// With Zod, define your type and schema together
|
|
1916
|
-
type UserData = z.infer<typeof userSchema>;
|
|
1917
1858
|
|
|
1918
1859
|
const userSchema = z.object({
|
|
1919
1860
|
name: z.string(),
|
|
1920
1861
|
email: z.string().email(),
|
|
1921
1862
|
});
|
|
1922
1863
|
|
|
1864
|
+
type UserData = z.infer<typeof userSchema>;
|
|
1865
|
+
|
|
1923
1866
|
const createUser = task({
|
|
1924
1867
|
inputSchema: userSchema,
|
|
1925
1868
|
run: async (input: UserData) => {
|
|
@@ -1929,7 +1872,7 @@ const createUser = task({
|
|
|
1929
1872
|
});
|
|
1930
1873
|
```
|
|
1931
1874
|
|
|
1932
|
-
|
|
1875
|
+
## Internal Services
|
|
1933
1876
|
|
|
1934
1877
|
We expose the internal services for advanced use cases (but try not to use them unless you really need to):
|
|
1935
1878
|
|
|
@@ -1950,7 +1893,7 @@ const advancedTask = task({
|
|
|
1950
1893
|
});
|
|
1951
1894
|
```
|
|
1952
1895
|
|
|
1953
|
-
###
|
|
1896
|
+
### Dynamic Dependencies
|
|
1954
1897
|
|
|
1955
1898
|
Dependencies can be defined in two ways - as a static object or as a function that returns an object. Each approach has its use cases:
|
|
1956
1899
|
|
|
@@ -1976,7 +1919,7 @@ const advancedService = resource({
|
|
|
1976
1919
|
conditionalService:
|
|
1977
1920
|
process.env.NODE_ENV === "production" ? serviceA : serviceB,
|
|
1978
1921
|
}), // Function - evaluated when needed
|
|
1979
|
-
register: (config) => [
|
|
1922
|
+
register: (config: ConfigType) => [
|
|
1980
1923
|
// Config is what you receive when you register the resource with .with()
|
|
1981
1924
|
// Register dependencies dynamically
|
|
1982
1925
|
process.env.NODE_ENV === "production"
|
|
@@ -1993,11 +1936,11 @@ The function pattern essentially gives you "just-in-time" dependency resolution
|
|
|
1993
1936
|
|
|
1994
1937
|
**Performance note**: Function-based dependencies have minimal overhead - they're only called once during dependency resolution.
|
|
1995
1938
|
|
|
1996
|
-
|
|
1939
|
+
## Handling Circular Dependencies
|
|
1997
1940
|
|
|
1998
1941
|
Sometimes you'll run into circular type dependencies because of your file structure not necessarily because of a real circular dependency. TypeScript struggles with these, but there's a way to handle it gracefully.
|
|
1999
1942
|
|
|
2000
|
-
|
|
1943
|
+
### The Problem
|
|
2001
1944
|
|
|
2002
1945
|
Consider these resources that create a circular dependency:
|
|
2003
1946
|
|
|
@@ -2030,7 +1973,7 @@ export const cResource = defineResource({
|
|
|
2030
1973
|
|
|
2031
1974
|
A depends B depends C depends ATask. No circular dependency, yet Typescript struggles with these, but there's a way to handle it gracefully.
|
|
2032
1975
|
|
|
2033
|
-
|
|
1976
|
+
### The Solution
|
|
2034
1977
|
|
|
2035
1978
|
The fix is to explicitly type the resource that completes the circle using a simple assertion `IResource<Config, ReturnType>`. This breaks the TypeScript inference chain while maintaining runtime functionality:
|
|
2036
1979
|
|
|
@@ -2245,7 +2188,7 @@ process.on("SIGTERM", async () => {
|
|
|
2245
2188
|
});
|
|
2246
2189
|
```
|
|
2247
2190
|
|
|
2248
|
-
## Testing
|
|
2191
|
+
## Testing
|
|
2249
2192
|
|
|
2250
2193
|
### Unit Testing: Mock Everything, Test Everything
|
|
2251
2194
|
|
|
@@ -2343,8 +2286,6 @@ Ever had too many database connections competing for resources? Your connection
|
|
|
2343
2286
|
|
|
2344
2287
|
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.
|
|
2345
2288
|
|
|
2346
|
-
### Quick Start
|
|
2347
|
-
|
|
2348
2289
|
```typescript
|
|
2349
2290
|
import { Semaphore } from "@bluelibs/runner";
|
|
2350
2291
|
|
|
@@ -2362,8 +2303,6 @@ try {
|
|
|
2362
2303
|
}
|
|
2363
2304
|
```
|
|
2364
2305
|
|
|
2365
|
-
### The Elegant Approach: withPermit()
|
|
2366
|
-
|
|
2367
2306
|
Why manage permits manually when you can let the semaphore do the heavy lifting?
|
|
2368
2307
|
|
|
2369
2308
|
```typescript
|
|
@@ -2373,8 +2312,6 @@ const users = await dbSemaphore.withPermit(async () => {
|
|
|
2373
2312
|
});
|
|
2374
2313
|
```
|
|
2375
2314
|
|
|
2376
|
-
### Timeout Support
|
|
2377
|
-
|
|
2378
2315
|
Prevent operations from hanging indefinitely with configurable timeouts:
|
|
2379
2316
|
|
|
2380
2317
|
```typescript
|
|
@@ -2393,8 +2330,6 @@ const result = await dbSemaphore.withPermit(
|
|
|
2393
2330
|
);
|
|
2394
2331
|
```
|
|
2395
2332
|
|
|
2396
|
-
### Cancellation Support
|
|
2397
|
-
|
|
2398
2333
|
Operations can be cancelled using AbortSignal:
|
|
2399
2334
|
|
|
2400
2335
|
```typescript
|
|
@@ -2418,8 +2353,6 @@ try {
|
|
|
2418
2353
|
}
|
|
2419
2354
|
```
|
|
2420
2355
|
|
|
2421
|
-
### Monitoring: Metrics & Debugging
|
|
2422
|
-
|
|
2423
2356
|
Want to know what's happening under the hood?
|
|
2424
2357
|
|
|
2425
2358
|
```typescript
|
|
@@ -2439,8 +2372,6 @@ console.log(`Queue length: ${dbSemaphore.getWaitingCount()}`);
|
|
|
2439
2372
|
console.log(`Is disposed: ${dbSemaphore.isDisposed()}`);
|
|
2440
2373
|
```
|
|
2441
2374
|
|
|
2442
|
-
### Resource Cleanup
|
|
2443
|
-
|
|
2444
2375
|
Properly dispose of semaphores when finished:
|
|
2445
2376
|
|
|
2446
2377
|
```typescript
|
|
@@ -2457,23 +2388,15 @@ _The orderly guardian of chaos, the diplomatic bouncer of async operations._
|
|
|
2457
2388
|
|
|
2458
2389
|
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.
|
|
2459
2390
|
|
|
2460
|
-
### **FIFO Ordering**
|
|
2461
|
-
|
|
2462
2391
|
Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
|
|
2463
2392
|
|
|
2464
|
-
### **Deadlock Detective**
|
|
2465
|
-
|
|
2466
2393
|
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.
|
|
2467
2394
|
|
|
2468
|
-
### **Graceful Disposal & Cancellation**
|
|
2469
|
-
|
|
2470
2395
|
The Queue provides cooperative cancellation through the Web Standard `AbortController`:
|
|
2471
2396
|
|
|
2472
2397
|
- **Patient mode** (default): Waits for all queued tasks to complete naturally
|
|
2473
2398
|
- **Cancel mode**: Signals running tasks to abort via `AbortSignal`, enabling early termination
|
|
2474
2399
|
|
|
2475
|
-
### Basic Example
|
|
2476
|
-
|
|
2477
2400
|
```typescript
|
|
2478
2401
|
import { Queue } from "@bluelibs/runner";
|
|
2479
2402
|
|
|
@@ -2493,7 +2416,7 @@ await queue.dispose();
|
|
|
2493
2416
|
|
|
2494
2417
|
The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
|
|
2495
2418
|
|
|
2496
|
-
#### Example: Long-running Task
|
|
2419
|
+
#### Example: Long-running Task
|
|
2497
2420
|
|
|
2498
2421
|
```typescript
|
|
2499
2422
|
const queue = new Queue();
|
|
@@ -2565,34 +2488,14 @@ const processFiles = queue.run(async (signal) => {
|
|
|
2565
2488
|
});
|
|
2566
2489
|
```
|
|
2567
2490
|
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
### Internal State
|
|
2491
|
+
#### The Magic Behind the Curtain
|
|
2571
2492
|
|
|
2572
2493
|
- `tail`: The promise chain that maintains FIFO execution order
|
|
2573
2494
|
- `disposed`: Boolean flag indicating whether the queue accepts new tasks
|
|
2574
2495
|
- `abortController`: Centralized cancellation controller that provides `AbortSignal` to all tasks
|
|
2575
2496
|
- `executionContext`: AsyncLocalStorage-based deadlock detection mechanism
|
|
2576
2497
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
- **"Queue has been disposed"**: You tried to add work after closing time
|
|
2580
|
-
- **"Dead-lock detected"**: A task tried to queue another task (infinite recursion prevention)
|
|
2581
|
-
|
|
2582
|
-
### Best Practices
|
|
2583
|
-
|
|
2584
|
-
#### 1. Always Dispose Resources
|
|
2585
|
-
|
|
2586
|
-
```typescript
|
|
2587
|
-
const queue = new Queue();
|
|
2588
|
-
try {
|
|
2589
|
-
await queue.run(task);
|
|
2590
|
-
} finally {
|
|
2591
|
-
await queue.dispose();
|
|
2592
|
-
}
|
|
2593
|
-
```
|
|
2594
|
-
|
|
2595
|
-
#### 2. Implement Cooperative Cancellation
|
|
2498
|
+
#### Implement Cooperative Cancellation
|
|
2596
2499
|
|
|
2597
2500
|
Tasks should regularly check the `AbortSignal` and respond appropriately:
|
|
2598
2501
|
|
|
@@ -2607,7 +2510,7 @@ if (signal.aborted) {
|
|
|
2607
2510
|
}
|
|
2608
2511
|
```
|
|
2609
2512
|
|
|
2610
|
-
|
|
2513
|
+
##### Integrate with Native APIs
|
|
2611
2514
|
|
|
2612
2515
|
Many Web APIs accept `AbortSignal`:
|
|
2613
2516
|
|
|
@@ -2615,11 +2518,11 @@ Many Web APIs accept `AbortSignal`:
|
|
|
2615
2518
|
- `setTimeout(callback, delay, { signal })`
|
|
2616
2519
|
- Custom async operations
|
|
2617
2520
|
|
|
2618
|
-
|
|
2521
|
+
##### Avoid Nested Queuing
|
|
2619
2522
|
|
|
2620
2523
|
The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
|
|
2621
2524
|
|
|
2622
|
-
|
|
2525
|
+
##### Handle AbortError Gracefully
|
|
2623
2526
|
|
|
2624
2527
|
```typescript
|
|
2625
2528
|
try {
|
|
@@ -2637,7 +2540,7 @@ try {
|
|
|
2637
2540
|
|
|
2638
2541
|
_Cooperative task scheduling with professional-grade cancellation support_
|
|
2639
2542
|
|
|
2640
|
-
|
|
2543
|
+
### Real-World Examples
|
|
2641
2544
|
|
|
2642
2545
|
### Database Connection Pool Manager
|
|
2643
2546
|
|
|
@@ -2684,54 +2587,7 @@ class APIClient {
|
|
|
2684
2587
|
}
|
|
2685
2588
|
```
|
|
2686
2589
|
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
```typescript
|
|
2690
|
-
async function processBatch(items: any[]) {
|
|
2691
|
-
const semaphore = new Semaphore(3); // Max 3 concurrent items
|
|
2692
|
-
const results = [];
|
|
2693
|
-
|
|
2694
|
-
console.log("Starting batch processing...");
|
|
2695
|
-
|
|
2696
|
-
for (const [index, item] of items.entries()) {
|
|
2697
|
-
const result = await semaphore.withPermit(async () => {
|
|
2698
|
-
console.log(`Processing item ${index + 1}/${items.length}`);
|
|
2699
|
-
return await processItem(item);
|
|
2700
|
-
});
|
|
2701
|
-
|
|
2702
|
-
results.push(result);
|
|
2703
|
-
|
|
2704
|
-
// Show progress
|
|
2705
|
-
const metrics = semaphore.getMetrics();
|
|
2706
|
-
console.log(
|
|
2707
|
-
`Active: ${metrics.maxPermits - metrics.availablePermits}, Waiting: ${
|
|
2708
|
-
metrics.waitingCount
|
|
2709
|
-
}`
|
|
2710
|
-
);
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
semaphore.dispose();
|
|
2714
|
-
console.log("Batch processing complete!");
|
|
2715
|
-
return results;
|
|
2716
|
-
}
|
|
2717
|
-
```
|
|
2718
|
-
|
|
2719
|
-
## Best Practices
|
|
2720
|
-
|
|
2721
|
-
1. **Always dispose**: Clean up your semaphores when finished to prevent memory leaks
|
|
2722
|
-
2. **Use withPermit()**: It's cleaner and prevents resource leaks
|
|
2723
|
-
3. **Set timeouts**: Don't let operations hang forever
|
|
2724
|
-
4. **Monitor metrics**: Keep an eye on utilization to tune your permit count
|
|
2725
|
-
5. **Handle errors**: Timeouts and cancellations throw errors - catch them!
|
|
2726
|
-
|
|
2727
|
-
## Common Pitfalls
|
|
2728
|
-
|
|
2729
|
-
- **Forgetting to release**: Manual acquire/release is error-prone - prefer `withPermit()`
|
|
2730
|
-
- **No timeout**: Operations can hang forever without timeouts
|
|
2731
|
-
- **Ignoring disposal**: Always dispose semaphores to prevent memory leaks
|
|
2732
|
-
- **Wrong permit count**: Too few = slow, too many = defeats the purpose
|
|
2733
|
-
|
|
2734
|
-
## Anonymous IDs: Because Naming Things Is Hard
|
|
2590
|
+
## Anonymous IDs
|
|
2735
2591
|
|
|
2736
2592
|
One of our favorite quality-of-life features: **anonymous IDs**. Instead of manually naming every component, the framework can generate unique symbol-based identifiers based on your file path and variable name. It's like having a really good naming assistant who never gets tired.
|
|
2737
2593
|
|
|
@@ -2759,63 +2615,11 @@ const createUser = task({
|
|
|
2759
2615
|
### Benefits of Anonymous IDs
|
|
2760
2616
|
|
|
2761
2617
|
1. **Less Bikeshedding**: No more debates about naming conventions
|
|
2762
|
-
2. **Automatic Uniqueness**: Guaranteed collision-free identifiers
|
|
2618
|
+
2. **Automatic Uniqueness**: Guaranteed collision-free identifiers folder based
|
|
2763
2619
|
3. **Faster Prototyping**: Just write code, framework handles the rest
|
|
2764
2620
|
4. **Refactor-Friendly**: Rename files/variables and IDs update automatically
|
|
2765
2621
|
5. **Stack Trace Integration**: Error messages include helpful file locations
|
|
2766
2622
|
|
|
2767
|
-
### When to Use Manual vs Anonymous IDs
|
|
2768
|
-
|
|
2769
|
-
| Use Case | Recommendation | Reason |
|
|
2770
|
-
| ----------------------- | -------------- | --------------------------------------- |
|
|
2771
|
-
| Internal tasks | Anonymous | No external references needed |
|
|
2772
|
-
| Event definitions | Manual | Need predictable names for listeners |
|
|
2773
|
-
| Public APIs | Manual | External modules need stable references |
|
|
2774
|
-
| Middleware | Manual | Often reused across projects |
|
|
2775
|
-
| Configuration resources | Anonymous | Usually internal infrastructure |
|
|
2776
|
-
| Test doubles/mocks | Anonymous | One-off usage in tests |
|
|
2777
|
-
| Cross-module services | Manual | Multiple files depend on them |
|
|
2778
|
-
|
|
2779
|
-
### Anonymous ID Examples by Pattern
|
|
2780
|
-
|
|
2781
|
-
```typescript
|
|
2782
|
-
// ✅ Great for anonymous IDs
|
|
2783
|
-
const database = resource({
|
|
2784
|
-
init: async () => new Database(),
|
|
2785
|
-
dispose: async (db) => db.close(),
|
|
2786
|
-
});
|
|
2787
|
-
|
|
2788
|
-
const processPayment = task({
|
|
2789
|
-
dependencies: { database },
|
|
2790
|
-
run: async (payment, { database }) => {
|
|
2791
|
-
// Internal business logic
|
|
2792
|
-
},
|
|
2793
|
-
});
|
|
2794
|
-
|
|
2795
|
-
// ✅ Better with manual IDs
|
|
2796
|
-
const paymentProcessed = event<{ paymentId: string }>({
|
|
2797
|
-
id: "app.events.paymentProcessed", // Other modules listen to this
|
|
2798
|
-
});
|
|
2799
|
-
|
|
2800
|
-
const authMiddleware = middleware({
|
|
2801
|
-
id: "app.middleware.auth", // Reused across multiple tasks
|
|
2802
|
-
run: async ({ task, next }) => {
|
|
2803
|
-
// Auth logic
|
|
2804
|
-
},
|
|
2805
|
-
});
|
|
2806
|
-
|
|
2807
|
-
// ✅ Mixed approach - totally fine!
|
|
2808
|
-
const app = resource({
|
|
2809
|
-
id: "app", // Main entry point gets manual ID
|
|
2810
|
-
register: [
|
|
2811
|
-
database, // Anonymous
|
|
2812
|
-
processPayment, // Anonymous
|
|
2813
|
-
paymentProcessed, // Manual
|
|
2814
|
-
authMiddleware, // Manual
|
|
2815
|
-
],
|
|
2816
|
-
});
|
|
2817
|
-
```
|
|
2818
|
-
|
|
2819
2623
|
### Debugging with Anonymous IDs
|
|
2820
2624
|
|
|
2821
2625
|
Anonymous IDs show up clearly in error messages and logs:
|
|
@@ -2843,14 +2647,6 @@ logger.info("Processing payment", {
|
|
|
2843
2647
|
- **Clarity**: Explicit dependencies, no hidden magic
|
|
2844
2648
|
- **Developer Experience**: Helpful error messages and clear patterns
|
|
2845
2649
|
|
|
2846
|
-
### What You Don't Get
|
|
2847
|
-
|
|
2848
|
-
- Complex configuration files that require a PhD to understand
|
|
2849
|
-
- Decorator hell that makes your code look like a Christmas tree
|
|
2850
|
-
- Hidden dependencies that break when you least expect it
|
|
2851
|
-
- Framework lock-in that makes you feel trapped
|
|
2852
|
-
- Mysterious behavior at runtime that makes you question reality
|
|
2853
|
-
|
|
2854
2650
|
## The Migration Path
|
|
2855
2651
|
|
|
2856
2652
|
Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
|