@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.
Files changed (66) hide show
  1. package/README.md +519 -34
  2. package/dist/cli/extract-docs.d.ts +2 -0
  3. package/dist/cli/extract-docs.js +88 -0
  4. package/dist/cli/extract-docs.js.map +1 -0
  5. package/dist/define.d.ts +22 -2
  6. package/dist/define.js +74 -2
  7. package/dist/define.js.map +1 -1
  8. package/dist/defs.d.ts +175 -4
  9. package/dist/defs.js +30 -0
  10. package/dist/defs.js.map +1 -1
  11. package/dist/docs/introspect.d.ts +7 -0
  12. package/dist/docs/introspect.js +199 -0
  13. package/dist/docs/introspect.js.map +1 -0
  14. package/dist/docs/markdown.d.ts +2 -0
  15. package/dist/docs/markdown.js +148 -0
  16. package/dist/docs/markdown.js.map +1 -0
  17. package/dist/docs/model.d.ts +62 -0
  18. package/dist/docs/model.js +33 -0
  19. package/dist/docs/model.js.map +1 -0
  20. package/dist/express/docsRouter.d.ts +12 -0
  21. package/dist/express/docsRouter.js +54 -0
  22. package/dist/express/docsRouter.js.map +1 -0
  23. package/dist/globals/globalMiddleware.d.ts +1 -0
  24. package/dist/globals/globalMiddleware.js +2 -0
  25. package/dist/globals/globalMiddleware.js.map +1 -1
  26. package/dist/globals/middleware/timeout.middleware.d.ts +8 -0
  27. package/dist/globals/middleware/timeout.middleware.js +35 -0
  28. package/dist/globals/middleware/timeout.middleware.js.map +1 -0
  29. package/dist/index.d.ts +4 -2
  30. package/dist/index.js +5 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/models/DependencyProcessor.js +2 -2
  33. package/dist/models/DependencyProcessor.js.map +1 -1
  34. package/dist/models/EventManager.js +9 -0
  35. package/dist/models/EventManager.js.map +1 -1
  36. package/dist/models/Store.d.ts +1 -1
  37. package/dist/models/StoreConstants.d.ts +1 -1
  38. package/dist/models/StoreConstants.js +2 -1
  39. package/dist/models/StoreConstants.js.map +1 -1
  40. package/dist/models/TaskRunner.d.ts +2 -3
  41. package/dist/models/TaskRunner.js +1 -2
  42. package/dist/models/TaskRunner.js.map +1 -1
  43. package/dist/testing.d.ts +24 -0
  44. package/dist/testing.js +41 -0
  45. package/dist/testing.js.map +1 -0
  46. package/package.json +4 -4
  47. package/src/__tests__/benchmark/task-benchmark.test.ts +132 -0
  48. package/src/__tests__/createTestResource.test.ts +139 -0
  49. package/src/__tests__/globalEvents.test.ts +419 -1
  50. package/src/__tests__/globals/timeout.middleware.test.ts +88 -0
  51. package/src/__tests__/models/Semaphore.test.ts +1 -1
  52. package/src/__tests__/override.test.ts +104 -0
  53. package/src/__tests__/run.overrides.test.ts +50 -21
  54. package/src/__tests__/run.test.ts +19 -0
  55. package/src/__tests__/tags.test.ts +396 -0
  56. package/src/__tests__/typesafety.test.ts +109 -1
  57. package/src/define.ts +101 -3
  58. package/src/defs.ts +180 -8
  59. package/src/globals/globalMiddleware.ts +2 -0
  60. package/src/globals/middleware/timeout.middleware.ts +46 -0
  61. package/src/index.ts +6 -0
  62. package/src/models/DependencyProcessor.ts +2 -10
  63. package/src/models/EventManager.ts +11 -0
  64. package/src/models/StoreConstants.ts +2 -1
  65. package/src/models/TaskRunner.ts +1 -3
  66. 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: Tagging Your Components
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
- Sometimes you want to attach metadata to your tasks and resources for documentation, filtering, or middleware logic:
1020
+ ### Simple Documentation Example
968
1021
 
969
1022
  ```typescript
970
- const apiTask = task({
971
- id: "app.tasks.api.createUser",
1023
+ const userService = resource({
1024
+ id: "app.services.user",
972
1025
  meta: {
973
- title: "Create User API",
974
- description: "Creates a new user account",
975
- tags: ["api", "user", "public"],
1026
+ title: "User Management Service",
1027
+ description:
1028
+ "Handles user creation, authentication, and profile management",
1029
+ tags: ["service", "user", "core"],
976
1030
  },
977
- run: async (userData) => {
978
- // Business logic
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 only applies to API tasks
983
- const apiMiddleware = middleware({
984
- id: "app.middleware.api",
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
- if (task.meta?.tags?.includes("api")) {
987
- // Apply API-specific logic
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, maybe you're A/B testing, maybe you just changed your mind:
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, // Copy everything else
1008
- init: async () => new MockEmailer(), // But use a different implementation
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
- Integration testing with overrides lets you test the whole system with controlled components:
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
- const testDatabase = resource({
1416
- id: "app.database",
1417
- init: async () => new MemoryDatabase(), // In-memory test database
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
- // Just like a shaworma wrap!
1421
- const testApp = resource({
1422
- id: "test.app",
1423
- register: [productionApp],
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
- describe("Full application", () => {
1428
- it("should handle user registration flow", async () => {
1429
- const { dispose } = await run(testApp);
1887
+ // Create the test harness
1888
+ const harness = createTestResource(app, { overrides: [testDb, mockMailer] });
1430
1889
 
1431
- // Test your application end-to-end
1890
+ // A task you want to drive in your tests
1891
+ const registerUser = task({ id: "app.tasks.registerUser" /* ... */ });
1432
1892
 
1433
- await dispose(); // Clean up
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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};