@bluelibs/runner 4.8.1 → 4.8.3

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/AI.md CHANGED
@@ -50,7 +50,7 @@ const createUser = r
50
50
  .dependencies({ logger: globals.resources.logger })
51
51
  .inputSchema<{ name: string }>({ parse: (value) => value })
52
52
  .resultSchema<{ id: string; name: string }>({ parse: (value) => value })
53
- .run(async ({ input }, { logger }) => {
53
+ .run(async (input, { logger }) => {
54
54
  await logger.info(`Creating user ${input.name}`);
55
55
  return { id: "user-1", name: input.name };
56
56
  })
@@ -104,7 +104,7 @@ const sendEmail = r
104
104
  loggingMiddleware.with({ label: "email" }),
105
105
  tracingMiddleware,
106
106
  ])
107
- .run(async ({ input }, { emailer }) => {
107
+ .run(async (input, { emailer }) => {
108
108
  await emailer.send(input);
109
109
  return { delivered: true };
110
110
  })
@@ -131,9 +131,9 @@ const userRegistered = r
131
131
  const registerUser = r
132
132
  .task("app.tasks.registerUser")
133
133
  .dependencies({ userRegistered, userService })
134
- .run(async ({ input }, deps) => {
134
+ .run(async (input, deps) => {
135
135
  const user = await deps.userService.create(input);
136
- await deps.userRegistered.emit({ userId: user.id, email: user.email });
136
+ await deps.userRegistered({ userId: user.id, email: user.email });
137
137
  return user;
138
138
  })
139
139
  .build();
@@ -309,7 +309,46 @@ const root = r
309
309
  .build();
310
310
  ```
311
311
 
312
- Use the unified HTTP client anywhere:
312
+ ### HTTP Client Factory (Recommended)
313
+
314
+ The `globals.resources.httpClientFactory` automatically injects serializer, error registry, and async contexts from the store:
315
+
316
+ ```ts
317
+ import { r, globals } from "@bluelibs/runner";
318
+
319
+ const myTask = r
320
+ .task("app.tasks.callRemote")
321
+ .dependencies({ clientFactory: globals.resources.httpClientFactory })
322
+ .run(async (input, { clientFactory }) => {
323
+ // Client automatically has serializer, errors, and contexts injected
324
+ const client = clientFactory({
325
+ baseUrl: process.env.API_URL,
326
+ auth: { token: process.env.API_TOKEN },
327
+ });
328
+
329
+ return await client.task("remote.task", input);
330
+ })
331
+ .build();
332
+
333
+ // Node streaming clients via Node DI factories
334
+ import { globals as nodeGlobals } from "@bluelibs/runner/node";
335
+
336
+ const nodeTask = r
337
+ .task("app.tasks.streamingCall")
338
+ .dependencies({ smartFactory: nodeGlobals.resources.httpSmartClientFactory })
339
+ .run(async (input, { smartFactory }) => {
340
+ const client = smartFactory({
341
+ baseUrl: process.env.API_URL,
342
+ });
343
+ // Supports duplex streams and multipart uploads
344
+ return await client.task("remote.streaming.task", input);
345
+ })
346
+ .build();
347
+ ```
348
+
349
+ ### Direct Client Creation (Legacy)
350
+
351
+ You can also create clients directly without DI (manual serializer/error/context passing):
313
352
 
314
353
  ```ts
315
354
  import { createHttpClient } from "@bluelibs/runner";
package/README.md CHANGED
@@ -76,7 +76,7 @@ const createUser = r
76
76
  .task("app.tasks.createUser")
77
77
  .dependencies({ server, logger: globals.resources.logger })
78
78
  .inputSchema<{ name: string }>({ parse: (value) => value })
79
- .run(async ({ input }, { server, logger }) => {
79
+ .run(async (input, { server, logger }) => {
80
80
  await logger.info(`Creating ${input.name}`);
81
81
  return { id: "user-123", name: input.name };
82
82
  })
@@ -144,7 +144,7 @@ import { r } from "@bluelibs/runner";
144
144
  const sendEmail = r
145
145
  .task("app.tasks.sendEmail")
146
146
  .dependencies({ emailService, logger })
147
- .run(async ({ input }, { emailService, logger }) => {
147
+ .run(async (input, { emailService, logger }) => {
148
148
  await logger.info(`Sending email to ${input.to}`);
149
149
  return emailService.send(input);
150
150
  })
@@ -280,9 +280,9 @@ const userRegistered = r
280
280
  const registerUser = r
281
281
  .task("app.tasks.registerUser")
282
282
  .dependencies({ userService, userRegistered })
283
- .run(async ({ input }, { userService, userRegistered }) => {
283
+ .run(async (input, { userService, userRegistered }) => {
284
284
  const user = await userService.createUser(input);
285
- await userRegistered.emit({ userId: user.id, email: user.email });
285
+ await userRegistered({ userId: user.id, email: user.email });
286
286
  return user;
287
287
  })
288
288
  .build();
@@ -484,14 +484,14 @@ const authMiddleware = r.middleware
484
484
  .task("app.middleware.task.auth")
485
485
  .run(async ({ task, next }, _deps, config: AuthMiddlewareConfig) => {
486
486
  // Must return the value
487
- return await next(task.input as any);
487
+ return await next(task.input);
488
488
  })
489
489
  .build();
490
490
 
491
491
  const adminTask = r
492
492
  .task("app.tasks.adminOnly")
493
493
  .middleware([authMiddleware.with({ requiredRole: "admin" })])
494
- .run(async ({ input }: { input: { user: User } }) => "Secret admin data")
494
+ .run(async (input) => "Secret admin data")
495
495
  .build();
496
496
  ```
497
497
 
@@ -531,7 +531,7 @@ const resourceAuthMiddleware = r.middleware
531
531
  const adminTask = r
532
532
  .task("app.tasks.adminOnly")
533
533
  .middleware([authMiddleware.with({ requiredRole: "admin" })])
534
- .run(async ({ input }: { input: { user: { role: string } } }) => ({
534
+ .run(async (input: { user: { role: string } }) => ({
535
535
  user: { role: input.user.role, verified: true },
536
536
  }))
537
537
  .build();
@@ -573,35 +573,62 @@ Access `eventManager` via `globals.resources.eventManager` if needed.
573
573
 
574
574
  #### Middleware Type Contracts
575
575
 
576
- Middleware can now enforce type contracts using the `<Config, Input, Output>` signature:
576
+ Middleware can enforce type contracts on the tasks that use them, ensuring data integrity as it flows through the system. This is achieved by defining `Input` and `Output` types within the middleware's implementation.
577
+
578
+ When a task uses this middleware, its own `run` method must conform to the `Input` and `Output` shapes defined by the middleware contract.
577
579
 
578
580
  ```typescript
579
581
  import { r } from "@bluelibs/runner";
580
582
 
581
- // Middleware that transforms input and output types
582
- type LogConfig = { includeTimestamp: boolean };
583
- type LogInput = { data: any };
584
- type LogOutput = { data: any; logged: boolean };
583
+ // 1. Define the contract types for the middleware.
584
+ type AuthConfig = { requiredRole: string };
585
+ type AuthInput = { user: { role: string } }; // Task's input must have this shape.
586
+ type AuthOutput = { executedBy: { role: string; verified: boolean } }; // Task's output must have this shape.
585
587
 
586
- const loggingMiddleware = r.middleware
587
- .task("app.middleware.logging")
588
- .run(async ({ task, next }, _deps, config: LogConfig) => {
589
- console.log(
590
- config.includeTimestamp ? new Date() : "",
591
- (task.input as LogInput).data,
592
- );
593
- const result = (await next(task.input)) as LogOutput;
594
- return { ...result, logged: true };
588
+ // 2. Create the middleware using these types in its `run` method.
589
+ const authMiddleware = r.middleware
590
+ .task<AuthConfig, AuthInput, AuthOutput>("app.middleware.auth")
591
+ .run(async ({ task, next }, _deps, config) => {
592
+ const input = task.input;
593
+ if (input.user.role !== config.requiredRole) {
594
+ throw new Error("Insufficient permissions");
595
+ }
596
+
597
+ // The task runs, and its result must match AuthOutput.
598
+ const result = await next(input);
599
+
600
+ // The middleware can further transform the output.
601
+ const output = result;
602
+ return {
603
+ ...output,
604
+ executedBy: {
605
+ ...output.executedBy,
606
+ verified: true, // The middleware adds its own data.
607
+ },
608
+ };
595
609
  })
596
610
  .build();
597
611
 
598
- // Tasks using this middleware must conform to the Input/Output types
599
- const loggedTask = r
600
- .task("app.tasks.logged")
601
- .middleware([loggingMiddleware.with({ includeTimestamp: true })])
602
- .run(async ({ input }: { input: { data: string } }) => ({
603
- data: input.data.toUpperCase(),
604
- }))
612
+ // 3. Apply the middleware to a task.
613
+ const adminTask = r
614
+ .task("app.tasks.adminOnly")
615
+ // If you use multiple middleware with contracts they get combined.
616
+ .middleware([authMiddleware.with({ requiredRole: "admin" })])
617
+ // If you use .inputSchema() the input must contain the contract types otherwise you end-up with InputContractViolation error.
618
+ // The `run` method is now strictly typed by the middleware's contract.
619
+ // Its input must be `AuthInput`, and its return value must be `AuthOutput`.
620
+ .run(async (input) => {
621
+ // `input.user.role` is available and fully typed.
622
+ console.log(`Task executed by user with role: ${input.user.role}`);
623
+
624
+ // Returning a shape that doesn't match AuthOutput will cause a compile-time error.
625
+ // return { wrong: "shape" }; // This would fail!
626
+ return {
627
+ executedBy: {
628
+ role: input.user.role,
629
+ },
630
+ };
631
+ })
605
632
  .build();
606
633
  ```
607
634
 
@@ -616,20 +643,13 @@ Tags are metadata that can influence system behavior. Unlike meta properties, ta
616
643
  ```typescript
617
644
  import { r } from "@bluelibs/runner";
618
645
 
619
- // Simple string tags
620
- const apiTask = r
621
- .task("app.tasks.getUserData")
622
- .tags(["api", "public", "cacheable"])
623
- .run(async ({ input }) => getUserFromDatabase(input as any))
624
- .build();
625
-
626
646
  // Structured tags with configuration
627
647
  const httpTag = r.tag<{ method: string; path: string }>("http.route").build();
628
648
 
629
649
  const getUserTask = r
630
650
  .task("app.tasks.getUser")
631
- .tags(["api", httpTag.with({ method: "GET", path: "/users/:id" })])
632
- .run(async ({ input }) => getUserFromDatabase((input as any).id))
651
+ .tags([httpTag.with({ method: "GET", path: "/users/:id" })])
652
+ .run(async (input) => getUserFromDatabase(input.id))
633
653
  .build();
634
654
  ```
635
655
 
@@ -655,7 +675,7 @@ const routeRegistration = r
655
675
 
656
676
  const { method, path } = config;
657
677
  server.app[method.toLowerCase()](path, async (req, res) => {
658
- const result = await taskDef({ ...req.params, ...req.body } as any);
678
+ const result = await taskDef({ ...req.params, ...req.body });
659
679
  res.json(result);
660
680
  });
661
681
  });
@@ -732,15 +752,18 @@ const internalEvent = r
732
752
  Enforce return value shapes at compile time:
733
753
 
734
754
  ```typescript
735
- // Tags that enforce type contracts
755
+ // Tags that enforce type contracts input/output for tasks or config/value for resources
756
+ type InputType = { id: string };
757
+ type OutputType = { name: string };
736
758
  const userContract = r
737
- .tag<void, void, { name: string }>("contract.user")
759
+ // void = no config, no need for .with({ ... })
760
+ .tag<void, InputType, OutputType>("contract.user")
738
761
  .build();
739
762
 
740
763
  const profileTask = r
741
764
  .task("app.tasks.getProfile")
742
765
  .tags([userContract]) // Must return { name: string }
743
- .run(async () => ({ name: "Ada" })) // ✅ Satisfies contract
766
+ .run(async (input) => ({ name: input.id + "Ada" })) // ✅ Satisfies contract
744
767
  .build();
745
768
  ```
746
769
 
@@ -760,7 +783,7 @@ const userNotFoundError = r
760
783
  const getUser = r
761
784
  .task("app.tasks.getUser")
762
785
  .dependencies({ userNotFoundError })
763
- .run(async ({ input }, { userNotFoundError }) => {
786
+ .run(async (input, { userNotFoundError }) => {
764
787
  userNotFoundError.throw({ code: 404, message: `User ${input} not found` });
765
788
  })
766
789
  .build();
@@ -884,7 +907,7 @@ import { r, run } from "@bluelibs/runner";
884
907
 
885
908
  const calculatorTask = r
886
909
  .task("app.tasks.calculator")
887
- .run(async ({ input }: { input: { value: number } }) => {
910
+ .run(async (input: { value: number }) => {
888
911
  console.log("3. Task is running...");
889
912
  return { result: input.value + 1 };
890
913
  })
@@ -949,7 +972,7 @@ const userRegistration = r
949
972
  emailService: emailService.optional(), // Optional - won't fail if missing
950
973
  analytics: analyticsService.optional(), // Optional - graceful degradation
951
974
  })
952
- .run(async ({ input }, { database, emailService, analytics }) => {
975
+ .run(async (input, { database, emailService, analytics }) => {
953
976
  // Create user (required)
954
977
  const user = await database.users.create(userData);
955
978
 
@@ -1051,12 +1074,13 @@ app = app.build();
1051
1074
  const remoteTasksTunnel = r
1052
1075
  .resource("app.tunnels.http")
1053
1076
  .tags([globals.tags.tunnel])
1054
- .init(async () => ({
1077
+ .dependencies({ createClient: globals.resource.httpClientFactory })
1078
+ .init(async (_, { createClient }) => ({
1055
1079
  mode: "client", // or "server", or "none", or "both" for emulating network infrastructure
1056
1080
  transport: "http", // the only one supported for now
1057
1081
  // Selectively forward tasks starting with "remote.tasks."
1058
1082
  tasks: (t) => t.id.startsWith("remote.tasks."),
1059
- client: globals.tunnels.http.createClient({
1083
+ client: createClient({
1060
1084
  url: "http://remote-runner:8080/__runner",
1061
1085
  }),
1062
1086
  }))
@@ -1184,9 +1208,7 @@ import type {
1184
1208
  // Task example
1185
1209
  const add = r
1186
1210
  .task("calc.add")
1187
- .run(
1188
- async ({ input }: { input: { a: number; b: number } }) => input.a + input.b,
1189
- )
1211
+ .run(async (input: { a: number; b: number }) => input.a + input.b)
1190
1212
  .build();
1191
1213
 
1192
1214
  type AddInput = ExtractTaskInput<typeof add>; // { a: number; b: number }
@@ -1245,7 +1267,7 @@ const requestMiddleware = r.middleware
1245
1267
  const handleRequest = r
1246
1268
  .task("app.handleRequest")
1247
1269
  .middleware([requestMiddleware])
1248
- .run(async ({ input }: { input: { path: string } }) => {
1270
+ .run(async (input: { path: string }) => {
1249
1271
  const request = requestContext.use();
1250
1272
  console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
1251
1273
  return { success: true, requestId: request.requestId };
@@ -1392,10 +1414,10 @@ const expensiveTask = r
1392
1414
  globals.middleware.task.cache.with({
1393
1415
  // lru-cache options by default
1394
1416
  ttl: 60 * 1000, // Cache for 1 minute
1395
- keyBuilder: (taskId, input) => `${taskId}-${(input as any).userId}`, // optional key builder
1417
+ keyBuilder: (taskId, input: any) => `${taskId}-${input.userId}`, // optional key builder
1396
1418
  }),
1397
1419
  ])
1398
- .run(async ({ input }: { input: { userId: string } }) => {
1420
+ .run(async (input: { userId: string }) => {
1399
1421
  // This expensive operation will be cached
1400
1422
  return await doExpensiveCalculation(input.userId);
1401
1423
  })
@@ -1423,7 +1445,7 @@ import { r } from "@bluelibs/runner";
1423
1445
 
1424
1446
  const redisCacheFactory = r
1425
1447
  .task("globals.tasks.cacheFactory") // Same ID as the default task
1426
- .run(async ({ input }: { input: any }) => new RedisCache(input))
1448
+ .run(async (input: { input: any }) => new RedisCache(input))
1427
1449
  .build();
1428
1450
 
1429
1451
  const app = r
@@ -1468,7 +1490,7 @@ Here are real performance metrics from our comprehensive benchmark suite on an M
1468
1490
  const userTask = r
1469
1491
  .task("user.create")
1470
1492
  .middleware([auth, logging, metrics])
1471
- .run(async ({ input }) => database.users.create(input as any))
1493
+ .run(async (input) => database.users.create(input))
1472
1494
  .build();
1473
1495
 
1474
1496
  // 1000 executions = ~5ms total time
@@ -1528,7 +1550,7 @@ const database = r
1528
1550
  const expensiveTask = r
1529
1551
  .task("app.performance.expensive")
1530
1552
  .middleware([globals.middleware.cache.with({ ttl: 60000 })])
1531
- .run(async ({ input }) => {
1553
+ .run(async (input) => {
1532
1554
  // This expensive computation is cached
1533
1555
  return performExpensiveCalculation(input);
1534
1556
  })
@@ -1751,7 +1773,7 @@ The logger accepts rich, structured data that makes debugging actually useful:
1751
1773
  const userTask = r
1752
1774
  .task("app.tasks.user.create")
1753
1775
  .dependencies({ logger: globals.resources.logger })
1754
- .run(async ({ input }, { logger }) => {
1776
+ .run(async (input, { logger }) => {
1755
1777
  // Basic message
1756
1778
  logger.info("Creating new user");
1757
1779
 
@@ -1759,7 +1781,7 @@ const userTask = r
1759
1781
  logger.info("User creation attempt", {
1760
1782
  source: userTask.id,
1761
1783
  data: {
1762
- email: (input as any).email,
1784
+ email: input.email,
1763
1785
  registrationSource: "web",
1764
1786
  timestamp: new Date().toISOString(),
1765
1787
  },
@@ -1767,7 +1789,7 @@ const userTask = r
1767
1789
 
1768
1790
  // With error information
1769
1791
  try {
1770
- const user = await createUser(input as any);
1792
+ const user = await createUser(input);
1771
1793
  logger.info("User created successfully", {
1772
1794
  data: { userId: user.id, email: user.email },
1773
1795
  });
@@ -1775,7 +1797,7 @@ const userTask = r
1775
1797
  logger.error("User creation failed", {
1776
1798
  error,
1777
1799
  data: {
1778
- attemptedEmail: (input as any).email,
1800
+ attemptedEmail: input.email,
1779
1801
  validationErrors: error.validationErrors,
1780
1802
  },
1781
1803
  });
@@ -2028,9 +2050,9 @@ const criticalTask = r
2028
2050
  logTaskOnError: true,
2029
2051
  }),
2030
2052
  ])
2031
- .run(async ({ input }) => {
2053
+ .run(async (input) => {
2032
2054
  // This task will have verbose debug logging
2033
- return await processPayment(input as any);
2055
+ return await processPayment(input);
2034
2056
  })
2035
2057
  .build();
2036
2058
  ```
@@ -2171,7 +2193,7 @@ const sendWelcomeEmail = r
2171
2193
  .meta({
2172
2194
  title: "Send Welcome Email",
2173
2195
  description: "Sends a welcome email to newly registered users",
2174
- } as any)
2196
+ })
2175
2197
  .dependencies({ emailService })
2176
2198
  .run(async ({ input: userData }, { emailService }) => {
2177
2199
  // Email sending logic
@@ -2211,7 +2233,7 @@ const expensiveApiTask = r
2211
2233
  version: "2.1.0",
2212
2234
  apiVersion: "v2",
2213
2235
  costLevel: "high", // Custom property!
2214
- } as any)
2236
+ })
2215
2237
  .run(async ({ input: prompt }) => {
2216
2238
  // AI generation logic
2217
2239
  })
@@ -2224,7 +2246,7 @@ const database = r
2224
2246
  healthCheck: "/health/db", // Custom property!
2225
2247
  dependencies: ["postgresql", "connection-pool"],
2226
2248
  scalingPolicy: "auto",
2227
- } as any)
2249
+ })
2228
2250
  // .init(async () => { /* ... */ })
2229
2251
  .build();
2230
2252
  ```
@@ -2254,7 +2276,7 @@ const testEmailer = override(productionEmailer, {
2254
2276
  // Using spread operator works the same way but does not provide type-safety.
2255
2277
  const testEmailer = r
2256
2278
  .resource("app.emailer")
2257
- .init(async () => ({} as any))
2279
+ .init(async () => ({}))
2258
2280
  .build();
2259
2281
 
2260
2282
  const app = r
@@ -2291,7 +2313,7 @@ const originalMiddleware = taskMiddleware({
2291
2313
  const overriddenMiddleware = override(originalMiddleware, {
2292
2314
  run: async ({ task, next }) => {
2293
2315
  const result = await next(task?.input);
2294
- return { wrapped: result } as any;
2316
+ return { wrapped: result };
2295
2317
  },
2296
2318
  });
2297
2319
 
@@ -2420,7 +2442,7 @@ const createUserTask = r
2420
2442
  .inputSchema(userSchema) // Works directly with Zod!
2421
2443
  .run(async ({ input: userData }) => {
2422
2444
  // userData is validated and properly typed
2423
- return { id: "user-123", ...(userData as any) };
2445
+ return { id: "user-123", ...userData };
2424
2446
  })
2425
2447
  .build();
2426
2448
 
@@ -2719,7 +2741,7 @@ type UserData = z.infer<typeof userSchema>;
2719
2741
  const createUser = r
2720
2742
  .task("app.tasks.createUser.zod")
2721
2743
  .inputSchema(userSchema)
2722
- .run(async ({ input }: { input: UserData }) => {
2744
+ .run(async (input: { input: UserData }) => {
2723
2745
  // Both runtime validation AND compile-time typing
2724
2746
  return { id: "user-123", ...input };
2725
2747
  })