@bluelibs/runner 3.2.0 → 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 +482 -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 +21 -1
- package/dist/define.js +71 -0
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +163 -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/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__/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 +97 -0
- package/src/defs.ts +168 -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/StoreConstants.ts +2 -1
- package/src/models/TaskRunner.ts +1 -3
- package/src/testing.ts +66 -0
package/README.md
CHANGED
|
@@ -999,40 +999,418 @@ await paymentLogger.info("Processing payment", { data: paymentData });
|
|
|
999
999
|
await authLogger.warn("Failed login attempt", { data: { email, ip } });
|
|
1000
1000
|
```
|
|
1001
1001
|
|
|
1002
|
-
## Meta:
|
|
1002
|
+
## Meta: Documenting and Organizing Your Components
|
|
1003
1003
|
|
|
1004
|
-
|
|
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
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### Simple Documentation Example
|
|
1005
1021
|
|
|
1006
1022
|
```typescript
|
|
1007
|
-
const
|
|
1008
|
-
id: "app.
|
|
1023
|
+
const userService = resource({
|
|
1024
|
+
id: "app.services.user",
|
|
1009
1025
|
meta: {
|
|
1010
|
-
title: "
|
|
1011
|
-
description:
|
|
1012
|
-
|
|
1026
|
+
title: "User Management Service",
|
|
1027
|
+
description:
|
|
1028
|
+
"Handles user creation, authentication, and profile management",
|
|
1029
|
+
tags: ["service", "user", "core"],
|
|
1013
1030
|
},
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
|
1016
1052
|
},
|
|
1017
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.
|
|
1018
1059
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Middleware that adds extra logging for destructive operations
|
|
1081
|
+
const auditMiddleware = middleware({
|
|
1082
|
+
id: "app.middleware.audit",
|
|
1022
1083
|
run: async ({ task, next }) => {
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
+
});
|
|
1025
1093
|
}
|
|
1094
|
+
|
|
1026
1095
|
return next(task.input);
|
|
1027
1096
|
},
|
|
1028
1097
|
});
|
|
1029
1098
|
```
|
|
1030
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
|
+
|
|
1031
1407
|
## Advanced Usage: When You Need More Power
|
|
1032
1408
|
|
|
1033
1409
|
### Overrides: Swapping Components at Runtime
|
|
1034
1410
|
|
|
1035
|
-
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.
|
|
1036
1414
|
|
|
1037
1415
|
```typescript
|
|
1038
1416
|
const productionEmailer = resource({
|
|
@@ -1040,9 +1418,15 @@ const productionEmailer = resource({
|
|
|
1040
1418
|
init: async () => new SMTPEmailer(),
|
|
1041
1419
|
});
|
|
1042
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
|
|
1043
1427
|
const testEmailer = resource({
|
|
1044
|
-
...productionEmailer,
|
|
1045
|
-
init: async () =>
|
|
1428
|
+
...productionEmailer,
|
|
1429
|
+
init: async () => {},
|
|
1046
1430
|
});
|
|
1047
1431
|
|
|
1048
1432
|
const app = resource({
|
|
@@ -1050,8 +1434,36 @@ const app = resource({
|
|
|
1050
1434
|
register: [productionEmailer],
|
|
1051
1435
|
overrides: [testEmailer], // This replaces the production version
|
|
1052
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
|
+
});
|
|
1053
1463
|
```
|
|
1054
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
|
+
|
|
1055
1467
|
### Namespacing: Keeping Things Organized
|
|
1056
1468
|
|
|
1057
1469
|
As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
|
|
@@ -1444,34 +1856,70 @@ describe("registerUser task", () => {
|
|
|
1444
1856
|
});
|
|
1445
1857
|
```
|
|
1446
1858
|
|
|
1447
|
-
### Integration Testing: The Real Deal
|
|
1859
|
+
### Integration Testing: The Real Deal (But Actually Fun)
|
|
1448
1860
|
|
|
1449
|
-
|
|
1861
|
+
Spin up your whole app, keep all the middleware/events, and still test like a human. The trick: a tiny test harness.
|
|
1450
1862
|
|
|
1451
1863
|
```typescript
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
+
],
|
|
1455
1878
|
});
|
|
1456
1879
|
|
|
1457
|
-
//
|
|
1458
|
-
const
|
|
1459
|
-
id: "
|
|
1460
|
-
|
|
1461
|
-
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(),
|
|
1462
1884
|
});
|
|
1885
|
+
const mockMailer = override(realMailer, { init: async () => fakeMailer });
|
|
1463
1886
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
const { dispose } = await run(testApp);
|
|
1887
|
+
// Create the test harness
|
|
1888
|
+
const harness = createTestResource(app, { overrides: [testDb, mockMailer] });
|
|
1467
1889
|
|
|
1468
|
-
|
|
1890
|
+
// A task you want to drive in your tests
|
|
1891
|
+
const registerUser = task({ id: "app.tasks.registerUser" /* ... */ });
|
|
1469
1892
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
});
|
|
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();
|
|
1473
1898
|
```
|
|
1474
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();
|
|
1915
|
+
```
|
|
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
|
+
|
|
1475
1923
|
## Semaphore
|
|
1476
1924
|
|
|
1477
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.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
// Note: avoid file URLs to keep CommonJS require working well
|
|
7
|
+
const introspect_1 = require("../docs/introspect");
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = { entry: "", outDir: "./documentation" };
|
|
10
|
+
const offset = argv[2] === "extract-docs" ? 3 : 2;
|
|
11
|
+
const rest = argv.slice(offset);
|
|
12
|
+
if (!rest[0]) {
|
|
13
|
+
console.error("Usage: runner extract-docs <entry> [--export <name>] [--out-dir <dir>] [--include-globals] [--config <path>] [--format json|md|all]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
args.entry = rest[0];
|
|
17
|
+
for (let i = 1; i < rest.length; i++) {
|
|
18
|
+
const flag = rest[i];
|
|
19
|
+
if (flag === "--export") {
|
|
20
|
+
args.exportName = rest[++i];
|
|
21
|
+
}
|
|
22
|
+
else if (flag === "--out-dir") {
|
|
23
|
+
args.outDir = rest[++i];
|
|
24
|
+
}
|
|
25
|
+
else if (flag === "--include-globals") {
|
|
26
|
+
args.includeGlobals = true;
|
|
27
|
+
}
|
|
28
|
+
else if (flag === "--config") {
|
|
29
|
+
args.configPath = rest[++i];
|
|
30
|
+
}
|
|
31
|
+
else if (flag === "--format") {
|
|
32
|
+
const val = rest[++i];
|
|
33
|
+
if (val === "json" || val === "md" || val === "all") {
|
|
34
|
+
args.format = val;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return args;
|
|
39
|
+
}
|
|
40
|
+
async function loadModule(entryPath) {
|
|
41
|
+
const resolved = path.isAbsolute(entryPath)
|
|
42
|
+
? entryPath
|
|
43
|
+
: path.resolve(process.cwd(), entryPath);
|
|
44
|
+
// Prefer dynamic import; in CommonJS this becomes require(resolved)
|
|
45
|
+
return await Promise.resolve(`${resolved}`).then(s => require(s));
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const args = parseArgs(process.argv);
|
|
49
|
+
const mod = await loadModule(args.entry);
|
|
50
|
+
const exportName = args.exportName || "default";
|
|
51
|
+
let resource;
|
|
52
|
+
if (exportName === "default") {
|
|
53
|
+
resource = (mod && mod.default) || mod;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
resource = mod[exportName];
|
|
57
|
+
}
|
|
58
|
+
if (!resource || typeof resource !== "object") {
|
|
59
|
+
console.error(`Could not find exported resource '${exportName}' from ${args.entry}`);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
const config = args.configPath
|
|
63
|
+
? JSON.parse(fs.readFileSync(args.configPath, "utf-8"))
|
|
64
|
+
: {};
|
|
65
|
+
const graph = await (0, introspect_1.introspectResource)(resource, config, {
|
|
66
|
+
includeGlobals: args.includeGlobals,
|
|
67
|
+
});
|
|
68
|
+
const outDir = path.isAbsolute(args.outDir)
|
|
69
|
+
? args.outDir
|
|
70
|
+
: path.resolve(process.cwd(), args.outDir);
|
|
71
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
72
|
+
const format = args.format || "json";
|
|
73
|
+
if (format === "json" || format === "all") {
|
|
74
|
+
const outFile = path.join(outDir, "graph.json");
|
|
75
|
+
fs.writeFileSync(outFile, JSON.stringify(graph, null, 2), "utf-8");
|
|
76
|
+
console.log(`Docs graph written to ${outFile}`);
|
|
77
|
+
}
|
|
78
|
+
if (format === "md" || format === "all") {
|
|
79
|
+
const { renderMarkdown } = await Promise.resolve().then(() => require("../docs/markdown"));
|
|
80
|
+
renderMarkdown(graph, outDir);
|
|
81
|
+
console.log(`Markdown docs written under ${outDir}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
main().catch((err) => {
|
|
85
|
+
console.error(err);
|
|
86
|
+
process.exit(99);
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=extract-docs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-docs.js","sourceRoot":"","sources":["../../src/cli/extract-docs.ts"],"names":[],"mappings":"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"}
|