@bluelibs/runner 3.4.0 → 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 +129 -320
- package/dist/define.d.ts +67 -3
- package/dist/define.js +84 -20
- package/dist/define.js.map +1 -1
- package/dist/models/Store.d.ts +3 -1
- package/dist/models/Store.js +6 -0
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreRegistry.d.ts +3 -1
- package/dist/models/StoreRegistry.js +16 -0
- package/dist/models/StoreRegistry.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__/models/Store.test.ts +57 -0
- 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/models/Store.ts +9 -0
- package/src/models/StoreRegistry.ts +25 -0
- 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,13 +601,65 @@ 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
|
+
|
|
606
|
+
The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
|
|
607
|
+
timeout. Works for resources and tasks.
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
import { globals } from "@bluelibs/runner";
|
|
611
|
+
|
|
612
|
+
const apiTask = task({
|
|
613
|
+
id: "app.tasks.externalApi",
|
|
614
|
+
middleware: [
|
|
615
|
+
globals.middleware.timeout.with({ ttl: 5000 }), // 5 second timeout
|
|
616
|
+
],
|
|
617
|
+
run: async () => {
|
|
618
|
+
// This operation will be aborted if it takes longer than 5 seconds
|
|
619
|
+
return await fetch("https://slow-api.example.com/data");
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Combine with retry for robust error handling
|
|
624
|
+
const resilientTask = task({
|
|
625
|
+
id: "app.tasks.resilient",
|
|
626
|
+
middleware: [
|
|
627
|
+
// Order matters here. Imagine a big onion.
|
|
628
|
+
globals.middleware.retry.with({
|
|
629
|
+
retries: 3,
|
|
630
|
+
delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
|
|
631
|
+
}),
|
|
632
|
+
globals.middleware.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
|
|
633
|
+
],
|
|
634
|
+
run: async () => {
|
|
635
|
+
// Each retry attempt gets its own 10-second timeout
|
|
636
|
+
return await unreliableOperation();
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
How it works:
|
|
642
|
+
|
|
643
|
+
- Uses AbortController and Promise.race() for clean cancellation
|
|
644
|
+
- Throws TimeoutError when the timeout is reached
|
|
645
|
+
- Works with any async operation in tasks and resources
|
|
646
|
+
- Integrates seamlessly with retry middleware for layered resilience
|
|
647
|
+
- Zero timeout (ttl: 0) throws immediately for testing edge cases
|
|
648
|
+
|
|
649
|
+
Best practices:
|
|
650
|
+
|
|
651
|
+
- Set timeouts based on expected operation duration plus buffer
|
|
652
|
+
- Combine with retry middleware for transient failures
|
|
653
|
+
- Use longer timeouts for resource initialization than task execution
|
|
654
|
+
- Consider network conditions when setting API call timeouts
|
|
655
|
+
|
|
656
|
+
## Logging
|
|
605
657
|
|
|
606
658
|
_The structured logging system that actually makes debugging enjoyable_
|
|
607
659
|
|
|
608
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).
|
|
609
661
|
|
|
610
|
-
###
|
|
662
|
+
### Basic Logging
|
|
611
663
|
|
|
612
664
|
```typescript
|
|
613
665
|
import { globals } from "@bluelibs/runner";
|
|
@@ -642,7 +694,7 @@ RUNNER_LOG_LEVEL=debug node your-app.js
|
|
|
642
694
|
RUNNER_DISABLE_LOGS=true node your-app.js
|
|
643
695
|
```
|
|
644
696
|
|
|
645
|
-
### Log Levels
|
|
697
|
+
### Log Levels
|
|
646
698
|
|
|
647
699
|
The logger supports six log levels with increasing severity:
|
|
648
700
|
|
|
@@ -665,7 +717,7 @@ logger.error("Houston, we have a problem");
|
|
|
665
717
|
logger.critical("DEFCON 1: Everything is broken");
|
|
666
718
|
```
|
|
667
719
|
|
|
668
|
-
### Structured Logging
|
|
720
|
+
### Structured Logging
|
|
669
721
|
|
|
670
722
|
The logger accepts rich, structured data that makes debugging actually useful:
|
|
671
723
|
|
|
@@ -705,7 +757,7 @@ const userTask = task({
|
|
|
705
757
|
});
|
|
706
758
|
```
|
|
707
759
|
|
|
708
|
-
### Context-Aware Logging
|
|
760
|
+
### Context-Aware Logging
|
|
709
761
|
|
|
710
762
|
Create logger instances with bound context for consistent metadata across related operations:
|
|
711
763
|
|
|
@@ -745,11 +797,11 @@ const requestHandler = task({
|
|
|
745
797
|
});
|
|
746
798
|
```
|
|
747
799
|
|
|
748
|
-
### Print Threshold
|
|
800
|
+
### Print Threshold
|
|
749
801
|
|
|
750
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:
|
|
751
803
|
|
|
752
|
-
|
|
804
|
+
### Environment Variables
|
|
753
805
|
|
|
754
806
|
```bash
|
|
755
807
|
# Disable all logging output
|
|
@@ -760,7 +812,7 @@ RUNNER_LOG_LEVEL=debug node your-app.js
|
|
|
760
812
|
RUNNER_LOG_LEVEL=error node your-app.js
|
|
761
813
|
```
|
|
762
814
|
|
|
763
|
-
|
|
815
|
+
### Programmatic Control
|
|
764
816
|
|
|
765
817
|
```typescript
|
|
766
818
|
// Override the default print threshold programmatically
|
|
@@ -782,7 +834,7 @@ const setupLogging = task({
|
|
|
782
834
|
});
|
|
783
835
|
```
|
|
784
836
|
|
|
785
|
-
### Event-Driven Log Handling
|
|
837
|
+
### Event-Driven Log Handling
|
|
786
838
|
|
|
787
839
|
Every log generates an event that you can listen to. This is where the real power comes in:
|
|
788
840
|
|
|
@@ -843,7 +895,7 @@ const databaseLogHandler = task({
|
|
|
843
895
|
});
|
|
844
896
|
```
|
|
845
897
|
|
|
846
|
-
### Integration with Winston
|
|
898
|
+
### Integration with Winston
|
|
847
899
|
|
|
848
900
|
Want to use Winston as your transport? No problem - integrate it seamlessly:
|
|
849
901
|
|
|
@@ -899,7 +951,7 @@ const winstonBridge = task({
|
|
|
899
951
|
});
|
|
900
952
|
```
|
|
901
953
|
|
|
902
|
-
###
|
|
954
|
+
### Custom Log Formatters
|
|
903
955
|
|
|
904
956
|
Want to customize how logs are printed? You can override the print behavior:
|
|
905
957
|
|
|
@@ -937,7 +989,7 @@ const customLogger = resource({
|
|
|
937
989
|
// Or you could simply add it as "globals.resources.logger" and override the default logger
|
|
938
990
|
```
|
|
939
991
|
|
|
940
|
-
### Log Structure
|
|
992
|
+
### Log Structure
|
|
941
993
|
|
|
942
994
|
Every log event contains:
|
|
943
995
|
|
|
@@ -960,7 +1012,7 @@ interface ILog {
|
|
|
960
1012
|
|
|
961
1013
|
### Debugging Tips & Best Practices
|
|
962
1014
|
|
|
963
|
-
|
|
1015
|
+
Use Structured Data Liberally
|
|
964
1016
|
|
|
965
1017
|
```typescript
|
|
966
1018
|
// Bad - hard to search and filter
|
|
@@ -977,7 +1029,7 @@ await logger.error("Order processing failed", {
|
|
|
977
1029
|
});
|
|
978
1030
|
```
|
|
979
1031
|
|
|
980
|
-
|
|
1032
|
+
Include Context in Errors
|
|
981
1033
|
|
|
982
1034
|
```typescript
|
|
983
1035
|
// Include relevant context with errors
|
|
@@ -997,7 +1049,7 @@ try {
|
|
|
997
1049
|
}
|
|
998
1050
|
```
|
|
999
1051
|
|
|
1000
|
-
|
|
1052
|
+
Use Different Log Levels Appropriately
|
|
1001
1053
|
|
|
1002
1054
|
```typescript
|
|
1003
1055
|
// Good level usage
|
|
@@ -1013,7 +1065,7 @@ await logger.error("Database connection failed", {
|
|
|
1013
1065
|
await logger.critical("System out of memory", { data: { available: "0MB" } });
|
|
1014
1066
|
```
|
|
1015
1067
|
|
|
1016
|
-
|
|
1068
|
+
Create Domain-Specific Loggers
|
|
1017
1069
|
|
|
1018
1070
|
```typescript
|
|
1019
1071
|
// Create loggers with domain context
|
|
@@ -1026,13 +1078,13 @@ await paymentLogger.info("Processing payment", { data: paymentData });
|
|
|
1026
1078
|
await authLogger.warn("Failed login attempt", { data: { email, ip } });
|
|
1027
1079
|
```
|
|
1028
1080
|
|
|
1029
|
-
## Meta
|
|
1081
|
+
## Meta
|
|
1030
1082
|
|
|
1031
1083
|
_The structured way to describe what your components do and control their behavior_
|
|
1032
1084
|
|
|
1033
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.
|
|
1034
1086
|
|
|
1035
|
-
###
|
|
1087
|
+
### Metadata Properties
|
|
1036
1088
|
|
|
1037
1089
|
Every component can have these basic metadata properties:
|
|
1038
1090
|
|
|
@@ -1080,9 +1132,9 @@ const sendWelcomeEmail = task({
|
|
|
1080
1132
|
});
|
|
1081
1133
|
```
|
|
1082
1134
|
|
|
1083
|
-
### Tags
|
|
1135
|
+
### Tags
|
|
1084
1136
|
|
|
1085
|
-
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.
|
|
1086
1138
|
|
|
1087
1139
|
#### String Tags for Simple Classification
|
|
1088
1140
|
|
|
@@ -1191,7 +1243,9 @@ const apiEndpoint = task({
|
|
|
1191
1243
|
});
|
|
1192
1244
|
```
|
|
1193
1245
|
|
|
1194
|
-
|
|
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
|
+
|
|
1248
|
+
#### Structured Tags
|
|
1195
1249
|
|
|
1196
1250
|
```typescript
|
|
1197
1251
|
const performanceMiddleware = middleware({
|
|
@@ -1231,7 +1285,7 @@ const performanceMiddleware = middleware({
|
|
|
1231
1285
|
});
|
|
1232
1286
|
```
|
|
1233
1287
|
|
|
1234
|
-
#### Contract Tags
|
|
1288
|
+
#### Contract Tags
|
|
1235
1289
|
|
|
1236
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>`.
|
|
1237
1291
|
|
|
@@ -1293,85 +1347,6 @@ const badTask = task({
|
|
|
1293
1347
|
});
|
|
1294
1348
|
```
|
|
1295
1349
|
|
|
1296
|
-
### When to Use Metadata
|
|
1297
|
-
|
|
1298
|
-
#### ✅ Great Use Cases
|
|
1299
|
-
|
|
1300
|
-
**Documentation & Discovery**
|
|
1301
|
-
|
|
1302
|
-
```typescript
|
|
1303
|
-
const paymentProcessor = resource({
|
|
1304
|
-
meta: {
|
|
1305
|
-
title: "Payment Processing Service",
|
|
1306
|
-
description:
|
|
1307
|
-
"Handles credit card payments via Stripe API with fraud detection",
|
|
1308
|
-
tags: ["payment", "stripe", "pci-compliant", "critical"],
|
|
1309
|
-
},
|
|
1310
|
-
// ... implementation
|
|
1311
|
-
});
|
|
1312
|
-
```
|
|
1313
|
-
|
|
1314
|
-
**Conditional Behavior**
|
|
1315
|
-
|
|
1316
|
-
```typescript
|
|
1317
|
-
const backgroundTask = task({
|
|
1318
|
-
meta: {
|
|
1319
|
-
tags: ["background", "low-priority", retryTag.with({ maxAttempts: 5 })],
|
|
1320
|
-
},
|
|
1321
|
-
// ... implementation
|
|
1322
|
-
});
|
|
1323
|
-
```
|
|
1324
|
-
|
|
1325
|
-
**Cross-Cutting Concerns**
|
|
1326
|
-
|
|
1327
|
-
```typescript
|
|
1328
|
-
// All tasks tagged with "audit" get automatic logging
|
|
1329
|
-
const sensitiveOperation = task({
|
|
1330
|
-
meta: {
|
|
1331
|
-
tags: ["audit", "sensitive", "admin-only"],
|
|
1332
|
-
},
|
|
1333
|
-
// ... implementation
|
|
1334
|
-
});
|
|
1335
|
-
```
|
|
1336
|
-
|
|
1337
|
-
**Environment-Specific Behavior**
|
|
1338
|
-
|
|
1339
|
-
```typescript
|
|
1340
|
-
const developmentTask = task({
|
|
1341
|
-
meta: {
|
|
1342
|
-
tags: ["development-only", debugTag.with({ verbose: true })],
|
|
1343
|
-
},
|
|
1344
|
-
// ... implementation
|
|
1345
|
-
});
|
|
1346
|
-
```
|
|
1347
|
-
|
|
1348
|
-
#### ❌ When NOT to Use Metadata
|
|
1349
|
-
|
|
1350
|
-
**Simple Internal Logic** - Don't overcomplicate straightforward code:
|
|
1351
|
-
|
|
1352
|
-
```typescript
|
|
1353
|
-
// ❌ Overkill
|
|
1354
|
-
const simple = task({
|
|
1355
|
-
meta: { tags: ["internal", "utility"] },
|
|
1356
|
-
run: () => Math.random(),
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
// ✅ Better
|
|
1360
|
-
const generateId = () => Math.random().toString(36);
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
**One-Off Tasks** - If it's used once, metadata won't help:
|
|
1364
|
-
|
|
1365
|
-
```typescript
|
|
1366
|
-
// ❌ Unnecessary
|
|
1367
|
-
const oneTimeScript = task({
|
|
1368
|
-
meta: { title: "Migration Script", tags: ["migration"] },
|
|
1369
|
-
run: () => {
|
|
1370
|
-
/* run once and forget */
|
|
1371
|
-
},
|
|
1372
|
-
});
|
|
1373
|
-
```
|
|
1374
|
-
|
|
1375
1350
|
### Extending Metadata: Custom Properties
|
|
1376
1351
|
|
|
1377
1352
|
For advanced use cases, you can extend the metadata interfaces to add your own properties:
|
|
@@ -1424,25 +1399,7 @@ const database = resource({
|
|
|
1424
1399
|
});
|
|
1425
1400
|
```
|
|
1426
1401
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
#### Tag-Based Component Selection
|
|
1430
|
-
|
|
1431
|
-
```typescript
|
|
1432
|
-
// Find all API endpoints
|
|
1433
|
-
function getApiTasks(store: Store) {
|
|
1434
|
-
return store.getAllTasks().filter((task) => task.meta?.tags?.includes("api"));
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
// Find all tasks with specific performance requirements
|
|
1438
|
-
function getPerformanceCriticalTasks(store: Store) {
|
|
1439
|
-
return store.tasks.values().filter(({ task }) => {
|
|
1440
|
-
return performanceTag.extract(task) !== null;
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
```
|
|
1444
|
-
|
|
1445
|
-
#### Dynamic Middleware Application
|
|
1402
|
+
#### Global Middleware Application
|
|
1446
1403
|
|
|
1447
1404
|
```typescript
|
|
1448
1405
|
const app = resource({
|
|
@@ -1466,7 +1423,7 @@ Metadata transforms your components from anonymous functions into self-documenti
|
|
|
1466
1423
|
|
|
1467
1424
|
## Advanced Usage: When You Need More Power
|
|
1468
1425
|
|
|
1469
|
-
|
|
1426
|
+
## Overrides
|
|
1470
1427
|
|
|
1471
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.
|
|
1472
1429
|
|
|
@@ -1524,7 +1481,7 @@ const overriddenMiddleware = override(originalMiddleware, {
|
|
|
1524
1481
|
|
|
1525
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.
|
|
1526
1483
|
|
|
1527
|
-
|
|
1484
|
+
## Namespacing
|
|
1528
1485
|
|
|
1529
1486
|
As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
|
|
1530
1487
|
|
|
@@ -1548,7 +1505,7 @@ const userTask = task({
|
|
|
1548
1505
|
});
|
|
1549
1506
|
```
|
|
1550
1507
|
|
|
1551
|
-
|
|
1508
|
+
## Factory Pattern
|
|
1552
1509
|
|
|
1553
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:
|
|
1554
1511
|
|
|
@@ -1572,12 +1529,10 @@ const app = resource({
|
|
|
1572
1529
|
});
|
|
1573
1530
|
```
|
|
1574
1531
|
|
|
1575
|
-
|
|
1532
|
+
## Runtime Validation
|
|
1576
1533
|
|
|
1577
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.
|
|
1578
1535
|
|
|
1579
|
-
#### The Validation Interface
|
|
1580
|
-
|
|
1581
1536
|
The framework defines a simple `IValidationSchema<T>` interface that any validation library can implement:
|
|
1582
1537
|
|
|
1583
1538
|
```typescript
|
|
@@ -1593,7 +1548,7 @@ Popular validation libraries already implement this interface:
|
|
|
1593
1548
|
- **Joi**: Use `.assert()` or create a wrapper
|
|
1594
1549
|
- **Custom validators**: Implement the interface yourself
|
|
1595
1550
|
|
|
1596
|
-
|
|
1551
|
+
### Task Input Validation
|
|
1597
1552
|
|
|
1598
1553
|
Add an `inputSchema` to any task to validate inputs before execution:
|
|
1599
1554
|
|
|
@@ -1643,7 +1598,7 @@ const app = resource({
|
|
|
1643
1598
|
});
|
|
1644
1599
|
```
|
|
1645
1600
|
|
|
1646
|
-
|
|
1601
|
+
### Resource Config Validation
|
|
1647
1602
|
|
|
1648
1603
|
Add a `configSchema` to resources to validate configurations. **Validation happens immediately when `.with()` is called**, ensuring configuration errors are caught early:
|
|
1649
1604
|
|
|
@@ -1693,7 +1648,7 @@ const app = resource({
|
|
|
1693
1648
|
});
|
|
1694
1649
|
```
|
|
1695
1650
|
|
|
1696
|
-
|
|
1651
|
+
### Event Payload Validation
|
|
1697
1652
|
|
|
1698
1653
|
Add a `payloadSchema` to events to validate payloads every time they're emitted:
|
|
1699
1654
|
|
|
@@ -1742,7 +1697,7 @@ const app = resource({
|
|
|
1742
1697
|
});
|
|
1743
1698
|
```
|
|
1744
1699
|
|
|
1745
|
-
|
|
1700
|
+
### Middleware Config Validation
|
|
1746
1701
|
|
|
1747
1702
|
Add a `configSchema` to middleware to validate configurations. Like resources, **validation happens immediately when `.with()` is called**:
|
|
1748
1703
|
|
|
@@ -1824,7 +1779,7 @@ const paymentTask = task({
|
|
|
1824
1779
|
});
|
|
1825
1780
|
```
|
|
1826
1781
|
|
|
1827
|
-
|
|
1782
|
+
### Error Handling
|
|
1828
1783
|
|
|
1829
1784
|
Validation errors are thrown with clear, descriptive messages that include the component ID:
|
|
1830
1785
|
|
|
@@ -1842,7 +1797,7 @@ Validation errors are thrown with clear, descriptive messages that include the c
|
|
|
1842
1797
|
// "Middleware config validation failed for {middlewareId}: {validationErrorMessage}"
|
|
1843
1798
|
```
|
|
1844
1799
|
|
|
1845
|
-
####
|
|
1800
|
+
#### Other Libraries
|
|
1846
1801
|
|
|
1847
1802
|
The framework works with any validation library that implements the `IValidationSchema<T>` interface:
|
|
1848
1803
|
|
|
@@ -1900,13 +1855,14 @@ While runtime validation happens with your chosen library, TypeScript still enfo
|
|
|
1900
1855
|
|
|
1901
1856
|
```typescript
|
|
1902
1857
|
// With Zod, define your type and schema together
|
|
1903
|
-
type UserData = z.infer<typeof userSchema>;
|
|
1904
1858
|
|
|
1905
1859
|
const userSchema = z.object({
|
|
1906
1860
|
name: z.string(),
|
|
1907
1861
|
email: z.string().email(),
|
|
1908
1862
|
});
|
|
1909
1863
|
|
|
1864
|
+
type UserData = z.infer<typeof userSchema>;
|
|
1865
|
+
|
|
1910
1866
|
const createUser = task({
|
|
1911
1867
|
inputSchema: userSchema,
|
|
1912
1868
|
run: async (input: UserData) => {
|
|
@@ -1916,7 +1872,7 @@ const createUser = task({
|
|
|
1916
1872
|
});
|
|
1917
1873
|
```
|
|
1918
1874
|
|
|
1919
|
-
|
|
1875
|
+
## Internal Services
|
|
1920
1876
|
|
|
1921
1877
|
We expose the internal services for advanced use cases (but try not to use them unless you really need to):
|
|
1922
1878
|
|
|
@@ -1937,7 +1893,7 @@ const advancedTask = task({
|
|
|
1937
1893
|
});
|
|
1938
1894
|
```
|
|
1939
1895
|
|
|
1940
|
-
###
|
|
1896
|
+
### Dynamic Dependencies
|
|
1941
1897
|
|
|
1942
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:
|
|
1943
1899
|
|
|
@@ -1963,7 +1919,7 @@ const advancedService = resource({
|
|
|
1963
1919
|
conditionalService:
|
|
1964
1920
|
process.env.NODE_ENV === "production" ? serviceA : serviceB,
|
|
1965
1921
|
}), // Function - evaluated when needed
|
|
1966
|
-
register: (config) => [
|
|
1922
|
+
register: (config: ConfigType) => [
|
|
1967
1923
|
// Config is what you receive when you register the resource with .with()
|
|
1968
1924
|
// Register dependencies dynamically
|
|
1969
1925
|
process.env.NODE_ENV === "production"
|
|
@@ -1980,11 +1936,11 @@ The function pattern essentially gives you "just-in-time" dependency resolution
|
|
|
1980
1936
|
|
|
1981
1937
|
**Performance note**: Function-based dependencies have minimal overhead - they're only called once during dependency resolution.
|
|
1982
1938
|
|
|
1983
|
-
|
|
1939
|
+
## Handling Circular Dependencies
|
|
1984
1940
|
|
|
1985
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.
|
|
1986
1942
|
|
|
1987
|
-
|
|
1943
|
+
### The Problem
|
|
1988
1944
|
|
|
1989
1945
|
Consider these resources that create a circular dependency:
|
|
1990
1946
|
|
|
@@ -2017,7 +1973,7 @@ export const cResource = defineResource({
|
|
|
2017
1973
|
|
|
2018
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.
|
|
2019
1975
|
|
|
2020
|
-
|
|
1976
|
+
### The Solution
|
|
2021
1977
|
|
|
2022
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:
|
|
2023
1979
|
|
|
@@ -2232,7 +2188,7 @@ process.on("SIGTERM", async () => {
|
|
|
2232
2188
|
});
|
|
2233
2189
|
```
|
|
2234
2190
|
|
|
2235
|
-
## Testing
|
|
2191
|
+
## Testing
|
|
2236
2192
|
|
|
2237
2193
|
### Unit Testing: Mock Everything, Test Everything
|
|
2238
2194
|
|
|
@@ -2330,8 +2286,6 @@ Ever had too many database connections competing for resources? Your connection
|
|
|
2330
2286
|
|
|
2331
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.
|
|
2332
2288
|
|
|
2333
|
-
### Quick Start
|
|
2334
|
-
|
|
2335
2289
|
```typescript
|
|
2336
2290
|
import { Semaphore } from "@bluelibs/runner";
|
|
2337
2291
|
|
|
@@ -2349,8 +2303,6 @@ try {
|
|
|
2349
2303
|
}
|
|
2350
2304
|
```
|
|
2351
2305
|
|
|
2352
|
-
### The Elegant Approach: withPermit()
|
|
2353
|
-
|
|
2354
2306
|
Why manage permits manually when you can let the semaphore do the heavy lifting?
|
|
2355
2307
|
|
|
2356
2308
|
```typescript
|
|
@@ -2360,8 +2312,6 @@ const users = await dbSemaphore.withPermit(async () => {
|
|
|
2360
2312
|
});
|
|
2361
2313
|
```
|
|
2362
2314
|
|
|
2363
|
-
### Timeout Support
|
|
2364
|
-
|
|
2365
2315
|
Prevent operations from hanging indefinitely with configurable timeouts:
|
|
2366
2316
|
|
|
2367
2317
|
```typescript
|
|
@@ -2380,8 +2330,6 @@ const result = await dbSemaphore.withPermit(
|
|
|
2380
2330
|
);
|
|
2381
2331
|
```
|
|
2382
2332
|
|
|
2383
|
-
### Cancellation Support
|
|
2384
|
-
|
|
2385
2333
|
Operations can be cancelled using AbortSignal:
|
|
2386
2334
|
|
|
2387
2335
|
```typescript
|
|
@@ -2405,8 +2353,6 @@ try {
|
|
|
2405
2353
|
}
|
|
2406
2354
|
```
|
|
2407
2355
|
|
|
2408
|
-
### Monitoring: Metrics & Debugging
|
|
2409
|
-
|
|
2410
2356
|
Want to know what's happening under the hood?
|
|
2411
2357
|
|
|
2412
2358
|
```typescript
|
|
@@ -2426,8 +2372,6 @@ console.log(`Queue length: ${dbSemaphore.getWaitingCount()}`);
|
|
|
2426
2372
|
console.log(`Is disposed: ${dbSemaphore.isDisposed()}`);
|
|
2427
2373
|
```
|
|
2428
2374
|
|
|
2429
|
-
### Resource Cleanup
|
|
2430
|
-
|
|
2431
2375
|
Properly dispose of semaphores when finished:
|
|
2432
2376
|
|
|
2433
2377
|
```typescript
|
|
@@ -2444,23 +2388,15 @@ _The orderly guardian of chaos, the diplomatic bouncer of async operations._
|
|
|
2444
2388
|
|
|
2445
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.
|
|
2446
2390
|
|
|
2447
|
-
### **FIFO Ordering**
|
|
2448
|
-
|
|
2449
2391
|
Tasks execute one after another in first-in, first-out order. No cutting, no exceptions, no drama.
|
|
2450
2392
|
|
|
2451
|
-
### **Deadlock Detective**
|
|
2452
|
-
|
|
2453
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.
|
|
2454
2394
|
|
|
2455
|
-
### **Graceful Disposal & Cancellation**
|
|
2456
|
-
|
|
2457
2395
|
The Queue provides cooperative cancellation through the Web Standard `AbortController`:
|
|
2458
2396
|
|
|
2459
2397
|
- **Patient mode** (default): Waits for all queued tasks to complete naturally
|
|
2460
2398
|
- **Cancel mode**: Signals running tasks to abort via `AbortSignal`, enabling early termination
|
|
2461
2399
|
|
|
2462
|
-
### Basic Example
|
|
2463
|
-
|
|
2464
2400
|
```typescript
|
|
2465
2401
|
import { Queue } from "@bluelibs/runner";
|
|
2466
2402
|
|
|
@@ -2480,7 +2416,7 @@ await queue.dispose();
|
|
|
2480
2416
|
|
|
2481
2417
|
The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
|
|
2482
2418
|
|
|
2483
|
-
#### Example: Long-running Task
|
|
2419
|
+
#### Example: Long-running Task
|
|
2484
2420
|
|
|
2485
2421
|
```typescript
|
|
2486
2422
|
const queue = new Queue();
|
|
@@ -2552,34 +2488,14 @@ const processFiles = queue.run(async (signal) => {
|
|
|
2552
2488
|
});
|
|
2553
2489
|
```
|
|
2554
2490
|
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
### Internal State
|
|
2491
|
+
#### The Magic Behind the Curtain
|
|
2558
2492
|
|
|
2559
2493
|
- `tail`: The promise chain that maintains FIFO execution order
|
|
2560
2494
|
- `disposed`: Boolean flag indicating whether the queue accepts new tasks
|
|
2561
2495
|
- `abortController`: Centralized cancellation controller that provides `AbortSignal` to all tasks
|
|
2562
2496
|
- `executionContext`: AsyncLocalStorage-based deadlock detection mechanism
|
|
2563
2497
|
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
- **"Queue has been disposed"**: You tried to add work after closing time
|
|
2567
|
-
- **"Dead-lock detected"**: A task tried to queue another task (infinite recursion prevention)
|
|
2568
|
-
|
|
2569
|
-
### Best Practices
|
|
2570
|
-
|
|
2571
|
-
#### 1. Always Dispose Resources
|
|
2572
|
-
|
|
2573
|
-
```typescript
|
|
2574
|
-
const queue = new Queue();
|
|
2575
|
-
try {
|
|
2576
|
-
await queue.run(task);
|
|
2577
|
-
} finally {
|
|
2578
|
-
await queue.dispose();
|
|
2579
|
-
}
|
|
2580
|
-
```
|
|
2581
|
-
|
|
2582
|
-
#### 2. Implement Cooperative Cancellation
|
|
2498
|
+
#### Implement Cooperative Cancellation
|
|
2583
2499
|
|
|
2584
2500
|
Tasks should regularly check the `AbortSignal` and respond appropriately:
|
|
2585
2501
|
|
|
@@ -2594,7 +2510,7 @@ if (signal.aborted) {
|
|
|
2594
2510
|
}
|
|
2595
2511
|
```
|
|
2596
2512
|
|
|
2597
|
-
|
|
2513
|
+
##### Integrate with Native APIs
|
|
2598
2514
|
|
|
2599
2515
|
Many Web APIs accept `AbortSignal`:
|
|
2600
2516
|
|
|
@@ -2602,11 +2518,11 @@ Many Web APIs accept `AbortSignal`:
|
|
|
2602
2518
|
- `setTimeout(callback, delay, { signal })`
|
|
2603
2519
|
- Custom async operations
|
|
2604
2520
|
|
|
2605
|
-
|
|
2521
|
+
##### Avoid Nested Queuing
|
|
2606
2522
|
|
|
2607
2523
|
The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
|
|
2608
2524
|
|
|
2609
|
-
|
|
2525
|
+
##### Handle AbortError Gracefully
|
|
2610
2526
|
|
|
2611
2527
|
```typescript
|
|
2612
2528
|
try {
|
|
@@ -2624,7 +2540,7 @@ try {
|
|
|
2624
2540
|
|
|
2625
2541
|
_Cooperative task scheduling with professional-grade cancellation support_
|
|
2626
2542
|
|
|
2627
|
-
|
|
2543
|
+
### Real-World Examples
|
|
2628
2544
|
|
|
2629
2545
|
### Database Connection Pool Manager
|
|
2630
2546
|
|
|
@@ -2671,54 +2587,7 @@ class APIClient {
|
|
|
2671
2587
|
}
|
|
2672
2588
|
```
|
|
2673
2589
|
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
```typescript
|
|
2677
|
-
async function processBatch(items: any[]) {
|
|
2678
|
-
const semaphore = new Semaphore(3); // Max 3 concurrent items
|
|
2679
|
-
const results = [];
|
|
2680
|
-
|
|
2681
|
-
console.log("Starting batch processing...");
|
|
2682
|
-
|
|
2683
|
-
for (const [index, item] of items.entries()) {
|
|
2684
|
-
const result = await semaphore.withPermit(async () => {
|
|
2685
|
-
console.log(`Processing item ${index + 1}/${items.length}`);
|
|
2686
|
-
return await processItem(item);
|
|
2687
|
-
});
|
|
2688
|
-
|
|
2689
|
-
results.push(result);
|
|
2690
|
-
|
|
2691
|
-
// Show progress
|
|
2692
|
-
const metrics = semaphore.getMetrics();
|
|
2693
|
-
console.log(
|
|
2694
|
-
`Active: ${metrics.maxPermits - metrics.availablePermits}, Waiting: ${
|
|
2695
|
-
metrics.waitingCount
|
|
2696
|
-
}`
|
|
2697
|
-
);
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
semaphore.dispose();
|
|
2701
|
-
console.log("Batch processing complete!");
|
|
2702
|
-
return results;
|
|
2703
|
-
}
|
|
2704
|
-
```
|
|
2705
|
-
|
|
2706
|
-
## Best Practices
|
|
2707
|
-
|
|
2708
|
-
1. **Always dispose**: Clean up your semaphores when finished to prevent memory leaks
|
|
2709
|
-
2. **Use withPermit()**: It's cleaner and prevents resource leaks
|
|
2710
|
-
3. **Set timeouts**: Don't let operations hang forever
|
|
2711
|
-
4. **Monitor metrics**: Keep an eye on utilization to tune your permit count
|
|
2712
|
-
5. **Handle errors**: Timeouts and cancellations throw errors - catch them!
|
|
2713
|
-
|
|
2714
|
-
## Common Pitfalls
|
|
2715
|
-
|
|
2716
|
-
- **Forgetting to release**: Manual acquire/release is error-prone - prefer `withPermit()`
|
|
2717
|
-
- **No timeout**: Operations can hang forever without timeouts
|
|
2718
|
-
- **Ignoring disposal**: Always dispose semaphores to prevent memory leaks
|
|
2719
|
-
- **Wrong permit count**: Too few = slow, too many = defeats the purpose
|
|
2720
|
-
|
|
2721
|
-
## Anonymous IDs: Because Naming Things Is Hard
|
|
2590
|
+
## Anonymous IDs
|
|
2722
2591
|
|
|
2723
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.
|
|
2724
2593
|
|
|
@@ -2746,63 +2615,11 @@ const createUser = task({
|
|
|
2746
2615
|
### Benefits of Anonymous IDs
|
|
2747
2616
|
|
|
2748
2617
|
1. **Less Bikeshedding**: No more debates about naming conventions
|
|
2749
|
-
2. **Automatic Uniqueness**: Guaranteed collision-free identifiers
|
|
2618
|
+
2. **Automatic Uniqueness**: Guaranteed collision-free identifiers folder based
|
|
2750
2619
|
3. **Faster Prototyping**: Just write code, framework handles the rest
|
|
2751
2620
|
4. **Refactor-Friendly**: Rename files/variables and IDs update automatically
|
|
2752
2621
|
5. **Stack Trace Integration**: Error messages include helpful file locations
|
|
2753
2622
|
|
|
2754
|
-
### When to Use Manual vs Anonymous IDs
|
|
2755
|
-
|
|
2756
|
-
| Use Case | Recommendation | Reason |
|
|
2757
|
-
| ----------------------- | -------------- | --------------------------------------- |
|
|
2758
|
-
| Internal tasks | Anonymous | No external references needed |
|
|
2759
|
-
| Event definitions | Manual | Need predictable names for listeners |
|
|
2760
|
-
| Public APIs | Manual | External modules need stable references |
|
|
2761
|
-
| Middleware | Manual | Often reused across projects |
|
|
2762
|
-
| Configuration resources | Anonymous | Usually internal infrastructure |
|
|
2763
|
-
| Test doubles/mocks | Anonymous | One-off usage in tests |
|
|
2764
|
-
| Cross-module services | Manual | Multiple files depend on them |
|
|
2765
|
-
|
|
2766
|
-
### Anonymous ID Examples by Pattern
|
|
2767
|
-
|
|
2768
|
-
```typescript
|
|
2769
|
-
// ✅ Great for anonymous IDs
|
|
2770
|
-
const database = resource({
|
|
2771
|
-
init: async () => new Database(),
|
|
2772
|
-
dispose: async (db) => db.close(),
|
|
2773
|
-
});
|
|
2774
|
-
|
|
2775
|
-
const processPayment = task({
|
|
2776
|
-
dependencies: { database },
|
|
2777
|
-
run: async (payment, { database }) => {
|
|
2778
|
-
// Internal business logic
|
|
2779
|
-
},
|
|
2780
|
-
});
|
|
2781
|
-
|
|
2782
|
-
// ✅ Better with manual IDs
|
|
2783
|
-
const paymentProcessed = event<{ paymentId: string }>({
|
|
2784
|
-
id: "app.events.paymentProcessed", // Other modules listen to this
|
|
2785
|
-
});
|
|
2786
|
-
|
|
2787
|
-
const authMiddleware = middleware({
|
|
2788
|
-
id: "app.middleware.auth", // Reused across multiple tasks
|
|
2789
|
-
run: async ({ task, next }) => {
|
|
2790
|
-
// Auth logic
|
|
2791
|
-
},
|
|
2792
|
-
});
|
|
2793
|
-
|
|
2794
|
-
// ✅ Mixed approach - totally fine!
|
|
2795
|
-
const app = resource({
|
|
2796
|
-
id: "app", // Main entry point gets manual ID
|
|
2797
|
-
register: [
|
|
2798
|
-
database, // Anonymous
|
|
2799
|
-
processPayment, // Anonymous
|
|
2800
|
-
paymentProcessed, // Manual
|
|
2801
|
-
authMiddleware, // Manual
|
|
2802
|
-
],
|
|
2803
|
-
});
|
|
2804
|
-
```
|
|
2805
|
-
|
|
2806
2623
|
### Debugging with Anonymous IDs
|
|
2807
2624
|
|
|
2808
2625
|
Anonymous IDs show up clearly in error messages and logs:
|
|
@@ -2830,14 +2647,6 @@ logger.info("Processing payment", {
|
|
|
2830
2647
|
- **Clarity**: Explicit dependencies, no hidden magic
|
|
2831
2648
|
- **Developer Experience**: Helpful error messages and clear patterns
|
|
2832
2649
|
|
|
2833
|
-
### What You Don't Get
|
|
2834
|
-
|
|
2835
|
-
- Complex configuration files that require a PhD to understand
|
|
2836
|
-
- Decorator hell that makes your code look like a Christmas tree
|
|
2837
|
-
- Hidden dependencies that break when you least expect it
|
|
2838
|
-
- Framework lock-in that makes you feel trapped
|
|
2839
|
-
- Mysterious behavior at runtime that makes you question reality
|
|
2840
|
-
|
|
2841
2650
|
## The Migration Path
|
|
2842
2651
|
|
|
2843
2652
|
Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
|