@bluelibs/runner 3.1.1 → 3.3.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 +519 -34
- package/dist/cli/extract-docs.d.ts +2 -0
- package/dist/cli/extract-docs.js +88 -0
- package/dist/cli/extract-docs.js.map +1 -0
- package/dist/define.d.ts +22 -2
- package/dist/define.js +74 -2
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +175 -4
- package/dist/defs.js +30 -0
- package/dist/defs.js.map +1 -1
- package/dist/docs/introspect.d.ts +7 -0
- package/dist/docs/introspect.js +199 -0
- package/dist/docs/introspect.js.map +1 -0
- package/dist/docs/markdown.d.ts +2 -0
- package/dist/docs/markdown.js +148 -0
- package/dist/docs/markdown.js.map +1 -0
- package/dist/docs/model.d.ts +62 -0
- package/dist/docs/model.js +33 -0
- package/dist/docs/model.js.map +1 -0
- package/dist/express/docsRouter.d.ts +12 -0
- package/dist/express/docsRouter.js +54 -0
- package/dist/express/docsRouter.js.map +1 -0
- package/dist/globals/globalMiddleware.d.ts +1 -0
- package/dist/globals/globalMiddleware.js +2 -0
- package/dist/globals/globalMiddleware.js.map +1 -1
- package/dist/globals/middleware/timeout.middleware.d.ts +8 -0
- package/dist/globals/middleware/timeout.middleware.js +35 -0
- package/dist/globals/middleware/timeout.middleware.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/models/DependencyProcessor.js +2 -2
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.js +9 -0
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Store.d.ts +1 -1
- package/dist/models/StoreConstants.d.ts +1 -1
- package/dist/models/StoreConstants.js +2 -1
- package/dist/models/StoreConstants.js.map +1 -1
- package/dist/models/TaskRunner.d.ts +2 -3
- package/dist/models/TaskRunner.js +1 -2
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/testing.d.ts +24 -0
- package/dist/testing.js +41 -0
- package/dist/testing.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/benchmark/task-benchmark.test.ts +132 -0
- package/src/__tests__/createTestResource.test.ts +139 -0
- package/src/__tests__/globalEvents.test.ts +419 -1
- package/src/__tests__/globals/timeout.middleware.test.ts +88 -0
- package/src/__tests__/models/Semaphore.test.ts +1 -1
- package/src/__tests__/override.test.ts +104 -0
- package/src/__tests__/run.overrides.test.ts +50 -21
- package/src/__tests__/run.test.ts +19 -0
- package/src/__tests__/tags.test.ts +396 -0
- package/src/__tests__/typesafety.test.ts +109 -1
- package/src/define.ts +101 -3
- package/src/defs.ts +180 -8
- package/src/globals/globalMiddleware.ts +2 -0
- package/src/globals/middleware/timeout.middleware.ts +46 -0
- package/src/index.ts +6 -0
- package/src/models/DependencyProcessor.ts +2 -10
- package/src/models/EventManager.ts +11 -0
- package/src/models/StoreConstants.ts +2 -1
- package/src/models/TaskRunner.ts +1 -3
- package/src/testing.ts +66 -0
package/README.md
CHANGED
|
@@ -296,6 +296,43 @@ const taskLogger = task({
|
|
|
296
296
|
});
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
+
#### Event Propagation Control with stopPropagation()
|
|
300
|
+
|
|
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
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
const criticalAlert = event<{
|
|
305
|
+
severity: "low" | "medium" | "high" | "critical";
|
|
306
|
+
}>({
|
|
307
|
+
id: "app.events.alert",
|
|
308
|
+
meta: {
|
|
309
|
+
title: "System Alert Event",
|
|
310
|
+
description: "Emitted when system issues are detected",
|
|
311
|
+
tags: ["monitoring", "alerts"],
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// High-priority handler that can stop propagation
|
|
316
|
+
const emergencyHandler = task({
|
|
317
|
+
id: "app.tasks.emergencyHandler",
|
|
318
|
+
on: criticalAlert, // Works with global events too
|
|
319
|
+
listenerOrder: -100, // Higher priority (lower numbers run first)
|
|
320
|
+
run: async (event) => {
|
|
321
|
+
console.log(`Alert received: ${event.data.severity}`);
|
|
322
|
+
|
|
323
|
+
if (event.data.severity === "critical") {
|
|
324
|
+
console.log("🚨 CRITICAL ALERT - Activating emergency protocols");
|
|
325
|
+
|
|
326
|
+
// Stop other handlers from running
|
|
327
|
+
event.stopPropagation();
|
|
328
|
+
// Notify the on-call team, escalate, etc.
|
|
329
|
+
|
|
330
|
+
console.log("🛑 Event propagation stopped - emergency protocols active");
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
299
336
|
### 4. Middleware: The Interceptor Pattern Done Right
|
|
300
337
|
|
|
301
338
|
Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
|
|
@@ -962,40 +999,418 @@ await paymentLogger.info("Processing payment", { data: paymentData });
|
|
|
962
999
|
await authLogger.warn("Failed login attempt", { data: { email, ip } });
|
|
963
1000
|
```
|
|
964
1001
|
|
|
965
|
-
## Meta:
|
|
1002
|
+
## Meta: Documenting and Organizing Your Components
|
|
1003
|
+
|
|
1004
|
+
_The structured way to describe what your components do and control their behavior_
|
|
1005
|
+
|
|
1006
|
+
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.
|
|
1007
|
+
|
|
1008
|
+
### Basic Metadata Properties
|
|
1009
|
+
|
|
1010
|
+
Every component can have these basic metadata properties:
|
|
1011
|
+
|
|
1012
|
+
```typescript
|
|
1013
|
+
interface IMeta {
|
|
1014
|
+
title?: string; // Human-readable name
|
|
1015
|
+
description?: string; // What this component does
|
|
1016
|
+
tags?: TagType[]; // Categories and behavioral flags
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
966
1019
|
|
|
967
|
-
|
|
1020
|
+
### Simple Documentation Example
|
|
968
1021
|
|
|
969
1022
|
```typescript
|
|
970
|
-
const
|
|
971
|
-
id: "app.
|
|
1023
|
+
const userService = resource({
|
|
1024
|
+
id: "app.services.user",
|
|
972
1025
|
meta: {
|
|
973
|
-
title: "
|
|
974
|
-
description:
|
|
975
|
-
|
|
1026
|
+
title: "User Management Service",
|
|
1027
|
+
description:
|
|
1028
|
+
"Handles user creation, authentication, and profile management",
|
|
1029
|
+
tags: ["service", "user", "core"],
|
|
976
1030
|
},
|
|
977
|
-
|
|
978
|
-
|
|
1031
|
+
dependencies: { database },
|
|
1032
|
+
init: async (_, { database }) => ({
|
|
1033
|
+
createUser: async (userData) => {
|
|
1034
|
+
/* ... */
|
|
1035
|
+
},
|
|
1036
|
+
authenticateUser: async (credentials) => {
|
|
1037
|
+
/* ... */
|
|
1038
|
+
},
|
|
1039
|
+
}),
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const sendWelcomeEmail = task({
|
|
1043
|
+
id: "app.tasks.sendWelcomeEmail",
|
|
1044
|
+
meta: {
|
|
1045
|
+
title: "Send Welcome Email",
|
|
1046
|
+
description: "Sends a welcome email to newly registered users",
|
|
1047
|
+
tags: ["email", "automation", "user-onboarding"],
|
|
1048
|
+
},
|
|
1049
|
+
dependencies: { emailService },
|
|
1050
|
+
run: async (userData, { emailService }) => {
|
|
1051
|
+
// Email sending logic
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
### Tags: The Powerful Classification System
|
|
1057
|
+
|
|
1058
|
+
Tags are the most powerful part of the metadata system. They can be simple strings or sophisticated configuration objects that control component behavior.
|
|
1059
|
+
|
|
1060
|
+
#### String Tags for Simple Classification
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
const adminTask = task({
|
|
1064
|
+
id: "app.tasks.admin.deleteUser",
|
|
1065
|
+
meta: {
|
|
1066
|
+
title: "Delete User Account",
|
|
1067
|
+
description: "Permanently removes a user account and all associated data",
|
|
1068
|
+
tags: [
|
|
1069
|
+
"admin", // Access level
|
|
1070
|
+
"destructive", // Behavioral flag
|
|
1071
|
+
"user", // Domain
|
|
1072
|
+
"gdpr-compliant", // Compliance flag
|
|
1073
|
+
],
|
|
1074
|
+
},
|
|
1075
|
+
run: async (userId) => {
|
|
1076
|
+
// Deletion logic
|
|
979
1077
|
},
|
|
980
1078
|
});
|
|
981
1079
|
|
|
982
|
-
// Middleware that
|
|
983
|
-
const
|
|
984
|
-
id: "app.middleware.
|
|
1080
|
+
// Middleware that adds extra logging for destructive operations
|
|
1081
|
+
const auditMiddleware = middleware({
|
|
1082
|
+
id: "app.middleware.audit",
|
|
985
1083
|
run: async ({ task, next }) => {
|
|
986
|
-
|
|
987
|
-
|
|
1084
|
+
const isDestructive = task.definition.meta?.tags?.includes("destructive");
|
|
1085
|
+
|
|
1086
|
+
if (isDestructive) {
|
|
1087
|
+
console.log(`🔥 DESTRUCTIVE OPERATION: ${task.definition.id}`);
|
|
1088
|
+
await auditLogger.log({
|
|
1089
|
+
operation: task.definition.id,
|
|
1090
|
+
user: getCurrentUser(),
|
|
1091
|
+
timestamp: new Date(),
|
|
1092
|
+
});
|
|
988
1093
|
}
|
|
1094
|
+
|
|
989
1095
|
return next(task.input);
|
|
990
1096
|
},
|
|
991
1097
|
});
|
|
992
1098
|
```
|
|
993
1099
|
|
|
1100
|
+
#### Advanced Tags with Configuration
|
|
1101
|
+
|
|
1102
|
+
For more sophisticated control, you can create structured tags that carry configuration:
|
|
1103
|
+
|
|
1104
|
+
```typescript
|
|
1105
|
+
import { tag } from "@bluelibs/runner";
|
|
1106
|
+
|
|
1107
|
+
// Define a reusable tag with configuration
|
|
1108
|
+
const performanceTag = tag<{ alertAboveMs: number; criticalAboveMs: number }>({
|
|
1109
|
+
id: "performance.monitoring",
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
const rateLimitTag = tag<{ maxRequestsPerMinute: number; burstLimit?: number }>(
|
|
1113
|
+
{
|
|
1114
|
+
id: "rate.limit",
|
|
1115
|
+
}
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
const cacheTag = tag<{ ttl: number; keyPattern?: string }>({
|
|
1119
|
+
id: "cache.strategy",
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// Use structured tags in your components
|
|
1123
|
+
const expensiveTask = task({
|
|
1124
|
+
id: "app.tasks.expensiveCalculation",
|
|
1125
|
+
meta: {
|
|
1126
|
+
title: "Complex Data Processing",
|
|
1127
|
+
description: "Performs heavy computational analysis on large datasets",
|
|
1128
|
+
tags: [
|
|
1129
|
+
"computation",
|
|
1130
|
+
"background",
|
|
1131
|
+
performanceTag.with({
|
|
1132
|
+
alertAboveMs: 5000,
|
|
1133
|
+
criticalAboveMs: 15000,
|
|
1134
|
+
}),
|
|
1135
|
+
cacheTag.with({
|
|
1136
|
+
ttl: 300000, // 5 minutes
|
|
1137
|
+
keyPattern: "calc-{userId}-{datasetId}",
|
|
1138
|
+
}),
|
|
1139
|
+
],
|
|
1140
|
+
},
|
|
1141
|
+
run: async (input) => {
|
|
1142
|
+
// Heavy computation here
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
const apiEndpoint = task({
|
|
1147
|
+
id: "app.tasks.api.getUserProfile",
|
|
1148
|
+
meta: {
|
|
1149
|
+
title: "Get User Profile",
|
|
1150
|
+
description: "Returns user profile information with privacy filtering",
|
|
1151
|
+
tags: [
|
|
1152
|
+
"api",
|
|
1153
|
+
"public",
|
|
1154
|
+
rateLimitTag.with({
|
|
1155
|
+
maxRequestsPerMinute: 100,
|
|
1156
|
+
burstLimit: 20,
|
|
1157
|
+
}),
|
|
1158
|
+
cacheTag.with({ ttl: 60000 }), // 1 minute cache
|
|
1159
|
+
],
|
|
1160
|
+
},
|
|
1161
|
+
run: async (userId) => {
|
|
1162
|
+
// API logic
|
|
1163
|
+
},
|
|
1164
|
+
});
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
#### Smart Middleware Using Structured Tags
|
|
1168
|
+
|
|
1169
|
+
```typescript
|
|
1170
|
+
const performanceMiddleware = middleware({
|
|
1171
|
+
id: "app.middleware.performance",
|
|
1172
|
+
run: async ({ task, next }) => {
|
|
1173
|
+
const tags = task.definition.meta?.tags || [];
|
|
1174
|
+
const perfConfig = performanceTag.extract(tags);
|
|
1175
|
+
|
|
1176
|
+
if (perfConfig) {
|
|
1177
|
+
const startTime = Date.now();
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
const result = await next(task.input);
|
|
1181
|
+
const duration = Date.now() - startTime;
|
|
1182
|
+
|
|
1183
|
+
if (duration > perfConfig.config.criticalAboveMs) {
|
|
1184
|
+
await alerting.critical(
|
|
1185
|
+
`Task ${task.definition.id} took ${duration}ms`
|
|
1186
|
+
);
|
|
1187
|
+
} else if (duration > perfConfig.config.alertAboveMs) {
|
|
1188
|
+
await alerting.warn(`Task ${task.definition.id} took ${duration}ms`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return result;
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
const duration = Date.now() - startTime;
|
|
1194
|
+
await alerting.error(
|
|
1195
|
+
`Task ${task.definition.id} failed after ${duration}ms`,
|
|
1196
|
+
error
|
|
1197
|
+
);
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return next(task.input);
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
const rateLimitMiddleware = middleware({
|
|
1207
|
+
id: "app.middleware.rateLimit",
|
|
1208
|
+
dependencies: { redis },
|
|
1209
|
+
run: async ({ task, next }, { redis }) => {
|
|
1210
|
+
// Extraction can be done at task.definition level or at task.definition.meta.tags
|
|
1211
|
+
const rateLimitCurrentTag = rateLimitTag.extract(task.definition);
|
|
1212
|
+
|
|
1213
|
+
// Alternative way
|
|
1214
|
+
const tags = task.definition.meta?.tags;
|
|
1215
|
+
const rateLimitCurrentTag = rateLimitTag.extract(tags);
|
|
1216
|
+
|
|
1217
|
+
if (rateLimitCurrentTag) {
|
|
1218
|
+
const key = `rateLimit:${task.definition.id}`;
|
|
1219
|
+
const current = await redis.incr(key);
|
|
1220
|
+
|
|
1221
|
+
if (current === 1) {
|
|
1222
|
+
await redis.expire(key, 60); // 1 minute window
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (current > rateLimitCurrentTag.config.maxRequestsPerMinute) {
|
|
1226
|
+
throw new Error("Rate limit exceeded");
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return next(task.input);
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
### When to Use Metadata
|
|
1236
|
+
|
|
1237
|
+
#### ✅ Great Use Cases
|
|
1238
|
+
|
|
1239
|
+
**Documentation & Discovery**
|
|
1240
|
+
|
|
1241
|
+
```typescript
|
|
1242
|
+
const paymentProcessor = resource({
|
|
1243
|
+
meta: {
|
|
1244
|
+
title: "Payment Processing Service",
|
|
1245
|
+
description:
|
|
1246
|
+
"Handles credit card payments via Stripe API with fraud detection",
|
|
1247
|
+
tags: ["payment", "stripe", "pci-compliant", "critical"],
|
|
1248
|
+
},
|
|
1249
|
+
// ... implementation
|
|
1250
|
+
});
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
**Conditional Behavior**
|
|
1254
|
+
|
|
1255
|
+
```typescript
|
|
1256
|
+
const backgroundTask = task({
|
|
1257
|
+
meta: {
|
|
1258
|
+
tags: ["background", "low-priority", retryTag.with({ maxAttempts: 5 })],
|
|
1259
|
+
},
|
|
1260
|
+
// ... implementation
|
|
1261
|
+
});
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
**Cross-Cutting Concerns**
|
|
1265
|
+
|
|
1266
|
+
```typescript
|
|
1267
|
+
// All tasks tagged with "audit" get automatic logging
|
|
1268
|
+
const sensitiveOperation = task({
|
|
1269
|
+
meta: {
|
|
1270
|
+
tags: ["audit", "sensitive", "admin-only"],
|
|
1271
|
+
},
|
|
1272
|
+
// ... implementation
|
|
1273
|
+
});
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
**Environment-Specific Behavior**
|
|
1277
|
+
|
|
1278
|
+
```typescript
|
|
1279
|
+
const developmentTask = task({
|
|
1280
|
+
meta: {
|
|
1281
|
+
tags: ["development-only", debugTag.with({ verbose: true })],
|
|
1282
|
+
},
|
|
1283
|
+
// ... implementation
|
|
1284
|
+
});
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
#### ❌ When NOT to Use Metadata
|
|
1288
|
+
|
|
1289
|
+
**Simple Internal Logic** - Don't overcomplicate straightforward code:
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
// ❌ Overkill
|
|
1293
|
+
const simple = task({
|
|
1294
|
+
meta: { tags: ["internal", "utility"] },
|
|
1295
|
+
run: () => Math.random(),
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// ✅ Better
|
|
1299
|
+
const generateId = () => Math.random().toString(36);
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
**One-Off Tasks** - If it's used once, metadata won't help:
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
// ❌ Unnecessary
|
|
1306
|
+
const oneTimeScript = task({
|
|
1307
|
+
meta: { title: "Migration Script", tags: ["migration"] },
|
|
1308
|
+
run: () => {
|
|
1309
|
+
/* run once and forget */
|
|
1310
|
+
},
|
|
1311
|
+
});
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
### Extending Metadata: Custom Properties
|
|
1315
|
+
|
|
1316
|
+
For advanced use cases, you can extend the metadata interfaces to add your own properties:
|
|
1317
|
+
|
|
1318
|
+
```typescript
|
|
1319
|
+
// In your types file
|
|
1320
|
+
declare module "@bluelibs/runner" {
|
|
1321
|
+
interface ITaskMeta {
|
|
1322
|
+
author?: string;
|
|
1323
|
+
version?: string;
|
|
1324
|
+
deprecated?: boolean;
|
|
1325
|
+
apiVersion?: "v1" | "v2" | "v3";
|
|
1326
|
+
costLevel?: "low" | "medium" | "high";
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
interface IResourceMeta {
|
|
1330
|
+
healthCheck?: string; // URL for health checking
|
|
1331
|
+
dependencies?: string[]; // External service dependencies
|
|
1332
|
+
scalingPolicy?: "auto" | "manual";
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Now use your custom properties
|
|
1337
|
+
const expensiveApiTask = task({
|
|
1338
|
+
id: "app.tasks.ai.generateImage",
|
|
1339
|
+
meta: {
|
|
1340
|
+
title: "AI Image Generation",
|
|
1341
|
+
description: "Uses OpenAI DALL-E to generate images from text prompts",
|
|
1342
|
+
tags: ["ai", "expensive", "external-api"],
|
|
1343
|
+
author: "AI Team",
|
|
1344
|
+
version: "2.1.0",
|
|
1345
|
+
apiVersion: "v2",
|
|
1346
|
+
costLevel: "high", // Custom property!
|
|
1347
|
+
},
|
|
1348
|
+
run: async (prompt) => {
|
|
1349
|
+
// AI generation logic
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
const database = resource({
|
|
1354
|
+
id: "app.database.primary",
|
|
1355
|
+
meta: {
|
|
1356
|
+
title: "Primary PostgreSQL Database",
|
|
1357
|
+
tags: ["database", "critical", "persistent"],
|
|
1358
|
+
healthCheck: "/health/db", // Custom property!
|
|
1359
|
+
dependencies: ["postgresql", "connection-pool"],
|
|
1360
|
+
scalingPolicy: "auto",
|
|
1361
|
+
},
|
|
1362
|
+
// ... implementation
|
|
1363
|
+
});
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
### Advanced Patterns
|
|
1367
|
+
|
|
1368
|
+
#### Tag-Based Component Selection
|
|
1369
|
+
|
|
1370
|
+
```typescript
|
|
1371
|
+
// Find all API endpoints
|
|
1372
|
+
function getApiTasks(store: Store) {
|
|
1373
|
+
return store.getAllTasks().filter((task) => task.meta?.tags?.includes("api"));
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Find all tasks with specific performance requirements
|
|
1377
|
+
function getPerformanceCriticalTasks(store: Store) {
|
|
1378
|
+
return store.getAllTasks().filter((task) => {
|
|
1379
|
+
const tags = task.meta?.tags || [];
|
|
1380
|
+
return performanceTag.extract(tags) !== null;
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
#### Dynamic Middleware Application
|
|
1386
|
+
|
|
1387
|
+
```typescript
|
|
1388
|
+
const app = resource({
|
|
1389
|
+
id: "app",
|
|
1390
|
+
register: [
|
|
1391
|
+
// Apply performance middleware globally but only to tagged tasks
|
|
1392
|
+
performanceMiddleware.everywhere({
|
|
1393
|
+
tasks: true,
|
|
1394
|
+
resources: false,
|
|
1395
|
+
}),
|
|
1396
|
+
// Apply rate limiting only to API tasks
|
|
1397
|
+
rateLimitMiddleware.everywhere({
|
|
1398
|
+
tasks: true,
|
|
1399
|
+
resources: false,
|
|
1400
|
+
}),
|
|
1401
|
+
],
|
|
1402
|
+
});
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
Metadata transforms your components from anonymous functions into self-documenting, discoverable, and controllable building blocks. Use it wisely, and your future self (and your team) will thank you.
|
|
1406
|
+
|
|
994
1407
|
## Advanced Usage: When You Need More Power
|
|
995
1408
|
|
|
996
1409
|
### Overrides: Swapping Components at Runtime
|
|
997
1410
|
|
|
998
|
-
Sometimes you need to replace a component entirely. Maybe you're testing
|
|
1411
|
+
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.
|
|
1412
|
+
|
|
1413
|
+
You can now use a dedicated helper `override()` to safely override any property on tasks, resources, or middleware — except `id`. This ensures the identity is preserved, while allowing behavior changes.
|
|
999
1414
|
|
|
1000
1415
|
```typescript
|
|
1001
1416
|
const productionEmailer = resource({
|
|
@@ -1003,9 +1418,15 @@ const productionEmailer = resource({
|
|
|
1003
1418
|
init: async () => new SMTPEmailer(),
|
|
1004
1419
|
});
|
|
1005
1420
|
|
|
1421
|
+
// Option 1: Using override() to change behavior while preserving id (Recommended)
|
|
1422
|
+
const testEmailer = override(productionEmailer, {
|
|
1423
|
+
init: async () => new MockEmailer(),
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// Option 2: Using spread operator, does not provide type-safety
|
|
1006
1427
|
const testEmailer = resource({
|
|
1007
|
-
...productionEmailer,
|
|
1008
|
-
init: async () =>
|
|
1428
|
+
...productionEmailer,
|
|
1429
|
+
init: async () => {},
|
|
1009
1430
|
});
|
|
1010
1431
|
|
|
1011
1432
|
const app = resource({
|
|
@@ -1013,8 +1434,36 @@ const app = resource({
|
|
|
1013
1434
|
register: [productionEmailer],
|
|
1014
1435
|
overrides: [testEmailer], // This replaces the production version
|
|
1015
1436
|
});
|
|
1437
|
+
|
|
1438
|
+
import { override } from "@bluelibs/runner";
|
|
1439
|
+
|
|
1440
|
+
// Tasks
|
|
1441
|
+
const originalTask = task({ id: "app.tasks.compute", run: async () => 1 });
|
|
1442
|
+
const overriddenTask = override(originalTask, {
|
|
1443
|
+
run: async () => 2,
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// Resources
|
|
1447
|
+
const originalResource = resource({ id: "app.db", init: async () => "conn" });
|
|
1448
|
+
const overriddenResource = override(originalResource, {
|
|
1449
|
+
init: async () => "mock-conn",
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
// Middleware
|
|
1453
|
+
const originalMiddleware = middleware({
|
|
1454
|
+
id: "app.middleware.log",
|
|
1455
|
+
run: async ({ next }) => next(),
|
|
1456
|
+
});
|
|
1457
|
+
const overriddenMiddleware = override(originalMiddleware, {
|
|
1458
|
+
run: async ({ task, next }) => {
|
|
1459
|
+
const result = await next(task?.input as any);
|
|
1460
|
+
return { wrapped: result } as any;
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1016
1463
|
```
|
|
1017
1464
|
|
|
1465
|
+
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.
|
|
1466
|
+
|
|
1018
1467
|
### Namespacing: Keeping Things Organized
|
|
1019
1468
|
|
|
1020
1469
|
As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
|
|
@@ -1407,34 +1856,70 @@ describe("registerUser task", () => {
|
|
|
1407
1856
|
});
|
|
1408
1857
|
```
|
|
1409
1858
|
|
|
1410
|
-
### Integration Testing: The Real Deal
|
|
1859
|
+
### Integration Testing: The Real Deal (But Actually Fun)
|
|
1411
1860
|
|
|
1412
|
-
|
|
1861
|
+
Spin up your whole app, keep all the middleware/events, and still test like a human. The trick: a tiny test harness.
|
|
1413
1862
|
|
|
1414
1863
|
```typescript
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1864
|
+
import {
|
|
1865
|
+
run,
|
|
1866
|
+
createTestResource,
|
|
1867
|
+
resource,
|
|
1868
|
+
task,
|
|
1869
|
+
override,
|
|
1870
|
+
} from "@bluelibs/runner";
|
|
1871
|
+
|
|
1872
|
+
// Your real app
|
|
1873
|
+
const app = resource({
|
|
1874
|
+
id: "app",
|
|
1875
|
+
register: [
|
|
1876
|
+
/* tasks, resources, middleware */
|
|
1877
|
+
],
|
|
1418
1878
|
});
|
|
1419
1879
|
|
|
1420
|
-
//
|
|
1421
|
-
const
|
|
1422
|
-
id: "
|
|
1423
|
-
|
|
1424
|
-
overrides: [testDatabase], // Replace real database with test one
|
|
1880
|
+
// Optional: overrides for infra (hello, fast tests!)
|
|
1881
|
+
const testDb = resource({
|
|
1882
|
+
id: "app.database",
|
|
1883
|
+
init: async () => new InMemoryDb(),
|
|
1425
1884
|
});
|
|
1885
|
+
const mockMailer = override(realMailer, { init: async () => fakeMailer });
|
|
1426
1886
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const { dispose } = await run(testApp);
|
|
1887
|
+
// Create the test harness
|
|
1888
|
+
const harness = createTestResource(app, { overrides: [testDb, mockMailer] });
|
|
1430
1889
|
|
|
1431
|
-
|
|
1890
|
+
// A task you want to drive in your tests
|
|
1891
|
+
const registerUser = task({ id: "app.tasks.registerUser" /* ... */ });
|
|
1432
1892
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
});
|
|
1893
|
+
// Boom: full ecosystem run (middleware, events, overrides) with a tiny driver
|
|
1894
|
+
const { value: t, dispose } = await run(harness);
|
|
1895
|
+
const result = await t.runTask(registerUser, { email: "x@y.z" });
|
|
1896
|
+
expect(result).toMatchObject({ success: true });
|
|
1897
|
+
await dispose();
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
Prefer scenario tests? Return whatever you want from the harness and assert outside:
|
|
1901
|
+
|
|
1902
|
+
```typescript
|
|
1903
|
+
const flowHarness = createTestResource(
|
|
1904
|
+
resource({
|
|
1905
|
+
id: "app",
|
|
1906
|
+
register: [db, createUser, issueToken],
|
|
1907
|
+
})
|
|
1908
|
+
);
|
|
1909
|
+
|
|
1910
|
+
const { value: t, dispose } = await run(flowHarness);
|
|
1911
|
+
const user = await t.runTask(createUser, { email: "a@b.c" });
|
|
1912
|
+
const token = await t.runTask(issueToken, { userId: user.id });
|
|
1913
|
+
expect(token).toBeTruthy();
|
|
1914
|
+
await dispose();
|
|
1436
1915
|
```
|
|
1437
1916
|
|
|
1917
|
+
Why this rocks:
|
|
1918
|
+
|
|
1919
|
+
- Minimal ceremony, no API pollution
|
|
1920
|
+
- Real wiring (middleware/events/overrides) – what runs in prod runs in tests
|
|
1921
|
+
- You choose: drive tasks directly or build domain-y flows
|
|
1922
|
+
|
|
1438
1923
|
## Semaphore
|
|
1439
1924
|
|
|
1440
1925
|
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.
|