@bluelibs/runner 3.3.2 → 3.4.1

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 (76) hide show
  1. package/README.md +491 -74
  2. package/dist/define.d.ts +5 -5
  3. package/dist/define.js +22 -2
  4. package/dist/define.js.map +1 -1
  5. package/dist/defs.d.ts +55 -21
  6. package/dist/defs.js.map +1 -1
  7. package/dist/defs.returnTag.d.ts +36 -0
  8. package/dist/defs.returnTag.js +4 -0
  9. package/dist/defs.returnTag.js.map +1 -0
  10. package/dist/errors.d.ts +60 -10
  11. package/dist/errors.js +103 -12
  12. package/dist/errors.js.map +1 -1
  13. package/dist/globals/globalMiddleware.d.ts +4 -4
  14. package/dist/globals/globalResources.d.ts +28 -10
  15. package/dist/globals/middleware/cache.middleware.d.ts +9 -9
  16. package/dist/globals/resources/queue.resource.d.ts +5 -2
  17. package/dist/index.d.ts +33 -14
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/models/DependencyProcessor.js +4 -4
  21. package/dist/models/DependencyProcessor.js.map +1 -1
  22. package/dist/models/EventManager.js +10 -1
  23. package/dist/models/EventManager.js.map +1 -1
  24. package/dist/models/Logger.d.ts +8 -0
  25. package/dist/models/Logger.js +24 -0
  26. package/dist/models/Logger.js.map +1 -1
  27. package/dist/models/OverrideManager.js +1 -1
  28. package/dist/models/OverrideManager.js.map +1 -1
  29. package/dist/models/ResourceInitializer.d.ts +2 -2
  30. package/dist/models/ResourceInitializer.js.map +1 -1
  31. package/dist/models/Store.d.ts +5 -3
  32. package/dist/models/Store.js +7 -1
  33. package/dist/models/Store.js.map +1 -1
  34. package/dist/models/StoreConstants.d.ts +6 -3
  35. package/dist/models/StoreRegistry.d.ts +5 -3
  36. package/dist/models/StoreRegistry.js +17 -1
  37. package/dist/models/StoreRegistry.js.map +1 -1
  38. package/dist/models/StoreTypes.d.ts +1 -1
  39. package/dist/models/StoreValidator.js +5 -5
  40. package/dist/models/StoreValidator.js.map +1 -1
  41. package/dist/models/TaskRunner.js +10 -0
  42. package/dist/models/TaskRunner.js.map +1 -1
  43. package/dist/run.d.ts +3 -3
  44. package/dist/run.js +1 -1
  45. package/dist/run.js.map +1 -1
  46. package/dist/t1.d.ts +1 -0
  47. package/dist/t1.js +13 -0
  48. package/dist/t1.js.map +1 -0
  49. package/dist/testing.d.ts +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/errors.test.ts +92 -11
  52. package/src/__tests__/models/EventManager.test.ts +0 -1
  53. package/src/__tests__/models/Logger.test.ts +82 -5
  54. package/src/__tests__/models/Store.test.ts +57 -0
  55. package/src/__tests__/recursion/c.resource.ts +1 -1
  56. package/src/__tests__/run.overrides.test.ts +3 -3
  57. package/src/__tests__/typesafety.test.ts +112 -9
  58. package/src/__tests__/validation-edge-cases.test.ts +111 -0
  59. package/src/__tests__/validation-interface.test.ts +428 -0
  60. package/src/define.ts +47 -15
  61. package/src/defs.returnTag.ts +91 -0
  62. package/src/defs.ts +84 -27
  63. package/src/errors.ts +95 -23
  64. package/src/index.ts +1 -0
  65. package/src/models/DependencyProcessor.ts +9 -5
  66. package/src/models/EventManager.ts +12 -3
  67. package/src/models/Logger.ts +28 -0
  68. package/src/models/OverrideManager.ts +2 -7
  69. package/src/models/ResourceInitializer.ts +8 -3
  70. package/src/models/Store.ts +12 -3
  71. package/src/models/StoreRegistry.ts +27 -2
  72. package/src/models/StoreTypes.ts +1 -1
  73. package/src/models/StoreValidator.ts +6 -6
  74. package/src/models/TaskRunner.ts +10 -1
  75. package/src/run.ts +8 -5
  76. package/src/testing.ts +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ _Or: How I Learned to Stop Worrying and Love Dependency Injection_
4
4
 
5
5
  <p align="center">
6
6
  <a href="https://github.com/bluelibs/runner/actions/workflows/ci.yml"><img src="https://github.com/bluelibs/runner/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status" /></a>
7
- <a href="https://coveralls.io/github/bluelibs/runner?branch=main"><img src="https://coveralls.io/repos/github/bluelibs/runner/badge.svg?branch=main" alt="Coverage Status" /></a>
7
+ <a href="https://github.com/bluelibs/runner"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="Coverage 100% is enforced. Code does not build without 100% on all branches, lines, etc." /></a>
8
8
  <a href="https://bluelibs.github.io/runner/" target="_blank"><img src="https://img.shields.io/badge/read-typedocs-blue" alt="Docs" /></a>
9
9
  <a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
10
10
  </p>
@@ -601,6 +601,58 @@ 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
+ ## Timeout Middleware: Never Wait Forever
605
+
606
+ The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
607
+ timeout:
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
+
604
656
  ## Logging: Because Console.log Isn't Professional
605
657
 
606
658
  _The structured logging system that actually makes debugging enjoyable_
@@ -616,18 +668,32 @@ const businessTask = task({
616
668
  id: "app.tasks.business",
617
669
  dependencies: { logger: globals.resources.logger },
618
670
  run: async (_, { logger }) => {
619
- logger.info("Starting business process");
620
- logger.warn("This might take a while");
671
+ logger.info("Starting business process"); // ✅ Visible by default
672
+ logger.warn("This might take a while"); // ✅ Visible by default
621
673
  logger.error("Oops, something went wrong", {
674
+ // ✅ Visible by default
622
675
  error: new Error("Database connection failed"),
623
676
  });
624
677
  logger.critical("System is on fire", {
678
+ // ✅ Visible by default
625
679
  data: { temperature: "9000°C" },
626
680
  });
681
+ logger.debug("Debug information"); // ❌ Hidden by default
682
+ logger.trace("Very detailed trace"); // ❌ Hidden by default
627
683
  },
628
684
  });
629
685
  ```
630
686
 
687
+ **Good news!** Logs at `info` level and above are visible by default, so you'll see your application logs immediately without any configuration. For development and debugging, you can easily show more detailed logs:
688
+
689
+ ```bash
690
+ # Show debug logs and framework internals
691
+ RUNNER_LOG_LEVEL=debug node your-app.js
692
+
693
+ # Hide all logs for production
694
+ RUNNER_DISABLE_LOGS=true node your-app.js
695
+ ```
696
+
631
697
  ### Log Levels: From Whispers to Screams
632
698
 
633
699
  The logger supports six log levels with increasing severity:
@@ -733,18 +799,31 @@ const requestHandler = task({
733
799
 
734
800
  ### Print Threshold: Control What Shows Up
735
801
 
736
- By default, logs are just events - they don't print to console unless you tell them to. Set a print threshold to automatically output logs at or above a certain level:
802
+ By default, logs at `info` level and above are automatically printed to console for better developer experience. You can easily control this behavior through environment variables or by setting a print threshold programmatically:
803
+
804
+ #### Environment Variable Controls
805
+
806
+ ```bash
807
+ # Disable all logging output
808
+ RUNNER_DISABLE_LOGS=true node your-app.js
809
+
810
+ # Set specific log level (trace, debug, info, warn, error, critical)
811
+ RUNNER_LOG_LEVEL=debug node your-app.js
812
+ RUNNER_LOG_LEVEL=error node your-app.js
813
+ ```
814
+
815
+ #### Programmatic Control
737
816
 
738
817
  ```typescript
739
- // Set up log printing (they don't print by default)
818
+ // Override the default print threshold programmatically
740
819
  const setupLogging = task({
741
820
  id: "app.logging.setup",
742
821
  on: globals.resources.logger.events.afterInit,
743
822
  run: async (event) => {
744
823
  const logger = event.data.value;
745
824
 
746
- // Print info level and above (info, warn, error, critical)
747
- logger.setPrintThreshold("info");
825
+ // Print debug level and above (debug, info, warn, error, critical)
826
+ logger.setPrintThreshold("debug");
748
827
 
749
828
  // Print only errors and critical issues
750
829
  logger.setPrintThreshold("error");
@@ -1164,6 +1243,8 @@ const apiEndpoint = task({
1164
1243
  });
1165
1244
  ```
1166
1245
 
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
+
1167
1248
  #### Smart Middleware Using Structured Tags
1168
1249
 
1169
1250
  ```typescript
@@ -1171,16 +1252,16 @@ const performanceMiddleware = middleware({
1171
1252
  id: "app.middleware.performance",
1172
1253
  run: async ({ task, next }) => {
1173
1254
  const tags = task.definition.meta?.tags || [];
1174
- const perfConfig = performanceTag.extract(tags);
1255
+ const perfConfigTag = performanceTag.extract(tags); // or easier: .extract(task.definition)
1175
1256
 
1176
- if (perfConfig) {
1257
+ if (perfConfigTag) {
1177
1258
  const startTime = Date.now();
1178
1259
 
1179
1260
  try {
1180
1261
  const result = await next(task.input);
1181
1262
  const duration = Date.now() - startTime;
1182
1263
 
1183
- if (duration > perfConfig.config.criticalAboveMs) {
1264
+ if (duration > perfConfigTag.config.criticalAboveMs) {
1184
1265
  await alerting.critical(
1185
1266
  `Task ${task.definition.id} took ${duration}ms`
1186
1267
  );
@@ -1202,38 +1283,74 @@ const performanceMiddleware = middleware({
1202
1283
  return next(task.input);
1203
1284
  },
1204
1285
  });
1286
+ ```
1205
1287
 
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);
1288
+ #### Contract Tags: Enforcing Return Types
1212
1289
 
1213
- // Alternative way
1214
- const tags = task.definition.meta?.tags;
1215
- const rateLimitCurrentTag = rateLimitTag.extract(tags);
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>`.
1216
1291
 
1217
- if (rateLimitCurrentTag) {
1218
- const key = `rateLimit:${task.definition.id}`;
1219
- const current = await redis.incr(key);
1292
+ ```typescript
1293
+ // A tag that enforces the returned value to include { name: string }
1294
+ const userContract = tag<void, { name: string }>({ id: "contract.user" });
1220
1295
 
1221
- if (current === 1) {
1222
- await redis.expire(key, 60); // 1 minute window
1223
- }
1296
+ // Another tag that enforces { age: number }
1297
+ const ageContract = tag<void, { age: number }>({ id: "contract.age" });
1224
1298
 
1225
- if (current > rateLimitCurrentTag.config.maxRequestsPerMinute) {
1226
- throw new Error("Rate limit exceeded");
1227
- }
1228
- }
1299
+ // Works with configured tags too
1300
+ const preferenceContract = tag<{ locale: string }, { preferredLocale: string }>(
1301
+ { id: "contract.preferences" }
1302
+ );
1303
+ ```
1229
1304
 
1230
- return next(task.input);
1305
+ When these tags are present in `meta.tags`, the returned value must satisfy the intersection of all contract types:
1306
+
1307
+ ```typescript
1308
+ // Task: the awaited return value must satisfy { name: string } & { age: number }
1309
+ const getProfile = task({
1310
+ id: "app.tasks.getProfile",
1311
+ meta: {
1312
+ tags: [
1313
+ userContract,
1314
+ ageContract,
1315
+ preferenceContract.with({ locale: "en" }),
1316
+ ],
1317
+ },
1318
+ run: async () => {
1319
+ return { name: "Ada", age: 37, preferredLocale: "en" }; // OK
1231
1320
  },
1232
1321
  });
1322
+
1323
+ // Resource: init() return must satisfy the same intersection
1324
+ const profileService = resource({
1325
+ id: "app.resources.profileService",
1326
+ meta: { tags: [userContract, ageContract] },
1327
+ init: async () => {
1328
+ return { name: "Ada", age: 37 }; // OK
1329
+ },
1330
+ });
1331
+ ```
1332
+
1333
+ If the returned value does not satisfy the intersection, TypeScript surfaces a readable, verbose type error that includes what was expected and what was received.
1334
+
1335
+ ```typescript
1336
+ const badTask = task({
1337
+ id: "app.tasks.bad",
1338
+ meta: { tags: [userContract, ageContract] },
1339
+ // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
1340
+ run: async () => ({ name: "Ada" }), // Missing { age: number }
1341
+ // Type error includes a helpful shape similar to:
1342
+ // ContractViolationError<
1343
+ // { message: "Value does not satisfy all tag contracts";
1344
+ // expected: { name: string } & { age: number };
1345
+ // received: { name: string } }
1346
+ // >
1347
+ });
1233
1348
  ```
1234
1349
 
1235
1350
  ### When to Use Metadata
1236
1351
 
1352
+ Always strive to provide a title and a description.
1353
+
1237
1354
  #### ✅ Great Use Cases
1238
1355
 
1239
1356
  **Documentation & Discovery**
@@ -1284,33 +1401,6 @@ const developmentTask = task({
1284
1401
  });
1285
1402
  ```
1286
1403
 
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
1404
  ### Extending Metadata: Custom Properties
1315
1405
 
1316
1406
  For advanced use cases, you can extend the metadata interfaces to add your own properties:
@@ -1365,23 +1455,6 @@ const database = resource({
1365
1455
 
1366
1456
  ### Advanced Patterns
1367
1457
 
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
1458
  #### Dynamic Middleware Application
1386
1459
 
1387
1460
  ```typescript
@@ -1512,6 +1585,350 @@ const app = resource({
1512
1585
  });
1513
1586
  ```
1514
1587
 
1588
+ ### Runtime Input and Config Validation
1589
+
1590
+ BlueLibs Runner includes a generic validation interface that works with any validation library, including [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), [Joi](https://joi.dev/), and others. The framework provides runtime validation with excellent TypeScript inference while remaining library-agnostic.
1591
+
1592
+ #### The Validation Interface
1593
+
1594
+ The framework defines a simple `IValidationSchema<T>` interface that any validation library can implement:
1595
+
1596
+ ```typescript
1597
+ interface IValidationSchema<T> {
1598
+ parse(input: unknown): T;
1599
+ }
1600
+ ```
1601
+
1602
+ Popular validation libraries already implement this interface:
1603
+
1604
+ - **Zod**: `.parse()` method works directly
1605
+ - **Yup**: Use `.validateSync()` or create a wrapper
1606
+ - **Joi**: Use `.assert()` or create a wrapper
1607
+ - **Custom validators**: Implement the interface yourself
1608
+
1609
+ #### Task Input Validation
1610
+
1611
+ Add an `inputSchema` to any task to validate inputs before execution:
1612
+
1613
+ ```typescript
1614
+ import { z } from "zod";
1615
+ import { task, resource, run } from "@bluelibs/runner";
1616
+
1617
+ const userSchema = z.object({
1618
+ name: z.string().min(2),
1619
+ email: z.string().email(),
1620
+ age: z.number().min(0).max(150),
1621
+ });
1622
+
1623
+ const createUserTask = task({
1624
+ id: "app.tasks.createUser",
1625
+ inputSchema: userSchema, // Works directly with Zod!
1626
+ run: async (userData) => {
1627
+ // userData is validated and properly typed
1628
+ return { id: "user-123", ...userData };
1629
+ },
1630
+ });
1631
+
1632
+ const app = resource({
1633
+ id: "app",
1634
+ register: [createUserTask],
1635
+ dependencies: { createUserTask },
1636
+ init: async (_, { createUserTask }) => {
1637
+ // This works - valid input
1638
+ const user = await createUserTask({
1639
+ name: "John Doe",
1640
+ email: "john@example.com",
1641
+ age: 30,
1642
+ });
1643
+
1644
+ // This throws a validation error at runtime
1645
+ try {
1646
+ await createUserTask({
1647
+ name: "J", // Too short
1648
+ email: "invalid-email", // Invalid format
1649
+ age: -5, // Negative age
1650
+ });
1651
+ } catch (error) {
1652
+ console.log(error.message);
1653
+ // "Task input validation failed for app.tasks.createUser: ..."
1654
+ }
1655
+ },
1656
+ });
1657
+ ```
1658
+
1659
+ #### Resource Config Validation (Fail Fast)
1660
+
1661
+ Add a `configSchema` to resources to validate configurations. **Validation happens immediately when `.with()` is called**, ensuring configuration errors are caught early:
1662
+
1663
+ ```typescript
1664
+ const databaseConfigSchema = z.object({
1665
+ host: z.string(),
1666
+ port: z.number().min(1).max(65535),
1667
+ database: z.string(),
1668
+ ssl: z.boolean().default(false), // Optional with default
1669
+ });
1670
+
1671
+ const databaseResource = resource({
1672
+ id: "app.resources.database",
1673
+ configSchema: databaseConfigSchema, // Validation on .with()
1674
+ init: async (config) => {
1675
+ // config is already validated and has proper types
1676
+ return createConnection({
1677
+ host: config.host,
1678
+ port: config.port,
1679
+ database: config.database,
1680
+ ssl: config.ssl,
1681
+ });
1682
+ },
1683
+ });
1684
+
1685
+ // Validation happens here, not during init!
1686
+ try {
1687
+ const configuredResource = databaseResource.with({
1688
+ host: "localhost",
1689
+ port: 99999, // Invalid: port too high
1690
+ database: "myapp",
1691
+ });
1692
+ } catch (error) {
1693
+ // "Resource config validation failed for app.resources.database: ..."
1694
+ }
1695
+
1696
+ const app = resource({
1697
+ id: "app",
1698
+ register: [
1699
+ databaseResource.with({
1700
+ host: "localhost",
1701
+ port: 5432,
1702
+ database: "myapp",
1703
+ // ssl defaults to false
1704
+ }),
1705
+ ],
1706
+ });
1707
+ ```
1708
+
1709
+ #### Event Payload Validation
1710
+
1711
+ Add a `payloadSchema` to events to validate payloads every time they're emitted:
1712
+
1713
+ ```typescript
1714
+ const userActionSchema = z.object({
1715
+ userId: z.string().uuid(),
1716
+ action: z.enum(["created", "updated", "deleted"]),
1717
+ timestamp: z.date().default(() => new Date()),
1718
+ });
1719
+
1720
+ const userActionEvent = event({
1721
+ id: "app.events.userAction",
1722
+ payloadSchema: userActionSchema, // Validates on emit
1723
+ });
1724
+
1725
+ const notificationTask = task({
1726
+ id: "app.tasks.sendNotification",
1727
+ on: userActionEvent,
1728
+ run: async (eventData) => {
1729
+ // eventData.data is validated and properly typed
1730
+ console.log(`User ${eventData.data.userId} was ${eventData.data.action}`);
1731
+ },
1732
+ });
1733
+
1734
+ const app = resource({
1735
+ id: "app",
1736
+ register: [userActionEvent, notificationTask],
1737
+ dependencies: { userActionEvent },
1738
+ init: async (_, { userActionEvent }) => {
1739
+ // This works - valid payload
1740
+ await userActionEvent({
1741
+ userId: "123e4567-e89b-12d3-a456-426614174000",
1742
+ action: "created",
1743
+ });
1744
+
1745
+ // This throws validation error when emitted
1746
+ try {
1747
+ await userActionEvent({
1748
+ userId: "invalid-uuid",
1749
+ action: "unknown",
1750
+ });
1751
+ } catch (error) {
1752
+ // "Event payload validation failed for app.events.userAction: ..."
1753
+ }
1754
+ },
1755
+ });
1756
+ ```
1757
+
1758
+ #### Middleware Config Validation (Fail Fast)
1759
+
1760
+ Add a `configSchema` to middleware to validate configurations. Like resources, **validation happens immediately when `.with()` is called**:
1761
+
1762
+ ```typescript
1763
+ const timingConfigSchema = z.object({
1764
+ timeout: z.number().positive(),
1765
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
1766
+ logSuccessful: z.boolean().default(true),
1767
+ });
1768
+
1769
+ const timingMiddleware = middleware({
1770
+ id: "app.middleware.timing",
1771
+ configSchema: timingConfigSchema, // Validation on .with()
1772
+ run: async ({ next }, _, config) => {
1773
+ const start = Date.now();
1774
+ try {
1775
+ const result = await next();
1776
+ const duration = Date.now() - start;
1777
+ if (config.logSuccessful && config.logLevel === "debug") {
1778
+ console.log(`Operation completed in ${duration}ms`);
1779
+ }
1780
+ return result;
1781
+ } catch (error) {
1782
+ const duration = Date.now() - start;
1783
+ console.log(`Operation failed after ${duration}ms`);
1784
+ throw error;
1785
+ }
1786
+ },
1787
+ });
1788
+
1789
+ // Validation happens here, not during execution!
1790
+ try {
1791
+ const configuredMiddleware = timingMiddleware.with({
1792
+ timeout: -5, // Invalid: negative timeout
1793
+ logLevel: "invalid", // Invalid: not in enum
1794
+ });
1795
+ } catch (error) {
1796
+ // "Middleware config validation failed for app.middleware.timing: ..."
1797
+ }
1798
+
1799
+ const myTask = task({
1800
+ id: "app.tasks.example",
1801
+ middleware: [
1802
+ timingMiddleware.with({
1803
+ timeout: 5000,
1804
+ logLevel: "debug",
1805
+ logSuccessful: true,
1806
+ }),
1807
+ ],
1808
+ run: async () => "success",
1809
+ });
1810
+ ```
1811
+
1812
+ #### Advanced Validation Features
1813
+
1814
+ Any validation library features work with the generic interface. Here's an example with transformations and refinements:
1815
+
1816
+ ```typescript
1817
+ const advancedSchema = z
1818
+ .object({
1819
+ userId: z.string().uuid(),
1820
+ amount: z.string().transform((val) => parseFloat(val)), // Transform string to number
1821
+ currency: z.enum(["USD", "EUR", "GBP"]),
1822
+ metadata: z.record(z.string()).optional(),
1823
+ })
1824
+ .refine((data) => data.amount > 0, {
1825
+ message: "Amount must be positive",
1826
+ path: ["amount"],
1827
+ });
1828
+
1829
+ const paymentTask = task({
1830
+ id: "app.tasks.payment",
1831
+ inputSchema: advancedSchema,
1832
+ run: async (payment) => {
1833
+ // payment.amount is now a number (transformed from string)
1834
+ // All validations have passed
1835
+ return processPayment(payment);
1836
+ },
1837
+ });
1838
+ ```
1839
+
1840
+ #### Error Handling
1841
+
1842
+ Validation errors are thrown with clear, descriptive messages that include the component ID:
1843
+
1844
+ ```typescript
1845
+ // Task validation error format:
1846
+ // "Task input validation failed for {taskId}: {validationErrorMessage}"
1847
+
1848
+ // Resource validation error format (thrown on .with() call):
1849
+ // "Resource config validation failed for {resourceId}: {validationErrorMessage}"
1850
+
1851
+ // Event validation error format (thrown on emit):
1852
+ // "Event payload validation failed for {eventId}: {validationErrorMessage}"
1853
+
1854
+ // Middleware validation error format (thrown on .with() call):
1855
+ // "Middleware config validation failed for {middlewareId}: {validationErrorMessage}"
1856
+ ```
1857
+
1858
+ #### Using Different Validation Libraries
1859
+
1860
+ The framework works with any validation library that implements the `IValidationSchema<T>` interface:
1861
+
1862
+ ```typescript
1863
+ // Zod (works directly)
1864
+ import { z } from "zod";
1865
+ const zodSchema = z.string().email();
1866
+
1867
+ // Yup (with wrapper)
1868
+ import * as yup from "yup";
1869
+ const yupSchema = {
1870
+ parse: (input: unknown) => yup.string().email().validateSync(input),
1871
+ };
1872
+
1873
+ // Joi (with wrapper)
1874
+ import Joi from "joi";
1875
+ const joiSchema = {
1876
+ parse: (input: unknown) => {
1877
+ const { error, value } = Joi.string().email().validate(input);
1878
+ if (error) throw error;
1879
+ return value;
1880
+ },
1881
+ };
1882
+
1883
+ // Custom validation
1884
+ const customSchema = {
1885
+ parse: (input: unknown) => {
1886
+ if (typeof input !== "string" || !input.includes("@")) {
1887
+ throw new Error("Must be a valid email");
1888
+ }
1889
+ return input;
1890
+ },
1891
+ };
1892
+ ```
1893
+
1894
+ #### When to Use Validation
1895
+
1896
+ - **API boundaries**: Validating user inputs from HTTP requests
1897
+ - **External data**: Processing data from files, databases, or APIs
1898
+ - **Configuration**: Ensuring environment variables and configs are correct (fail fast)
1899
+ - **Event payloads**: Validating data in event-driven architectures
1900
+ - **Middleware configs**: Validating middleware settings at registration time (fail fast)
1901
+
1902
+ #### Performance Notes
1903
+
1904
+ - Validation only runs when schemas are provided (zero overhead when not used)
1905
+ - Resource and middleware validation happens once at registration time (`.with()`)
1906
+ - Task and event validation happens at runtime
1907
+ - Consider the validation library's performance characteristics for your use case
1908
+ - All major validation libraries are optimized for runtime validation
1909
+
1910
+ #### TypeScript Integration
1911
+
1912
+ While runtime validation happens with your chosen library, TypeScript still enforces compile-time types. For the best experience:
1913
+
1914
+ ```typescript
1915
+ // With Zod, define your type and schema together
1916
+ type UserData = z.infer<typeof userSchema>;
1917
+
1918
+ const userSchema = z.object({
1919
+ name: z.string(),
1920
+ email: z.string().email(),
1921
+ });
1922
+
1923
+ const createUser = task({
1924
+ inputSchema: userSchema,
1925
+ run: async (input: UserData) => {
1926
+ // Both runtime validation AND compile-time typing
1927
+ return { id: "user-123", ...input };
1928
+ },
1929
+ });
1930
+ ```
1931
+
1515
1932
  ### Internal Services: For When You Need Direct Access
1516
1933
 
1517
1934
  We expose the internal services for advanced use cases (but try not to use them unless you really need to):
package/dist/define.d.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  * metadata: anonymous IDs, file path tags (for better debugging), lifecycle
6
6
  * events, and global middleware flags. See README for high-level concepts.
7
7
  */
8
- import { ITask, ITaskDefinition, IResource, IResourceWithConfig, IResourceDefinition, IEventDefinition, IMiddlewareDefinition, DependencyMapType, DependencyValuesType, IMiddleware, IEvent, RegisterableItems, ITag, ITagDefinition } from "./defs";
9
- export declare function defineTask<Input = undefined, Output extends Promise<any> = any, Deps extends DependencyMapType = any, TOn extends "*" | IEventDefinition | undefined = undefined>(taskConfig: ITaskDefinition<Input, Output, Deps, TOn>): ITask<Input, Output, Deps, TOn>;
10
- export declare function defineResource<TConfig = void, TValue = any, TDeps extends DependencyMapType = {}, TPrivate = any>(constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate>): IResource<TConfig, TValue, TDeps, TPrivate>;
8
+ import { ITask, ITaskDefinition, IResource, IResourceWithConfig, IResourceDefinition, IEventDefinition, IMiddlewareDefinition, DependencyMapType, DependencyValuesType, IMiddleware, IEvent, RegisterableItems, ITag, ITagDefinition, ITaskMeta, IResourceMeta } from "./defs";
9
+ export declare function defineTask<Input = undefined, Output extends Promise<any> = any, Deps extends DependencyMapType = any, TOn extends "*" | IEventDefinition | undefined = undefined, TMeta extends ITaskMeta = any>(taskConfig: ITaskDefinition<Input, Output, Deps, TOn, TMeta>): ITask<Input, Output, Deps, TOn, TMeta>;
10
+ export declare function defineResource<TConfig = void, TValue extends Promise<any> = Promise<any>, TDeps extends DependencyMapType = {}, TPrivate = any, TMeta extends IResourceMeta = any>(constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate, any, any, TMeta>): IResource<TConfig, TValue, TDeps, TPrivate, TMeta>;
11
11
  /**
12
12
  * Creates an "index" resource which groups multiple registerable items under
13
13
  * a single dependency. The resulting resource registers every item, depends
@@ -16,7 +16,7 @@ export declare function defineResource<TConfig = void, TValue = any, TDeps exten
16
16
  */
17
17
  export declare function defineIndex<T extends Record<string, RegisterableItems>, D extends {
18
18
  [K in keyof T]: T[K] extends IResourceWithConfig<any, any, any> ? T[K]["resource"] : T[K];
19
- } & DependencyMapType>(items: T): IResource<void, DependencyValuesType<D>, D>;
19
+ } & DependencyMapType>(items: T): IResource<void, Promise<DependencyValuesType<D>>, D>;
20
20
  export declare function defineEvent<TPayload = void>(config?: IEventDefinition<TPayload>): IEvent<TPayload>;
21
21
  export type MiddlewareEverywhereOptions = {
22
22
  /**
@@ -46,4 +46,4 @@ export declare function defineOverride<T extends IMiddleware<any, any>>(base: T,
46
46
  * - `.with(config)` to create configured instances
47
47
  * - `.extract(tags)` to extract this tag from a list of tags
48
48
  */
49
- export declare function defineTag<TConfig = void>(definition: ITagDefinition<TConfig>): ITag<TConfig>;
49
+ export declare function defineTag<TConfig = void, TEnforceContract = void>(definition: ITagDefinition<TConfig, TEnforceContract>): ITag<TConfig, TEnforceContract>;