runsheet 0.4.0 → 0.5.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 CHANGED
@@ -33,6 +33,10 @@ and runsheet makes sure they execute in order with clear contracts between them.
33
33
 
34
34
  ## What this is
35
35
 
36
+ Args persist and outputs accumulate. That's the core model — initial arguments
37
+ flow through the entire pipeline, each step's output merges into the context,
38
+ and every step sees the full picture of everything before it.
39
+
36
40
  A pipeline orchestration library with:
37
41
 
38
42
  - **Strongly typed steps** — each step's `run`, `rollback`, `requires`, and
@@ -300,6 +304,172 @@ propagates. Inner steps retain their own `requires`/`provides` validation,
300
304
  `retry`, and `timeout` behavior. Conditional steps (via `when()`) work inside
301
305
  `parallel()`.
302
306
 
307
+ ## Dependency injection
308
+
309
+ No special mechanism needed — pass dependencies as pipeline args and they're
310
+ available to every step through the accumulated context:
311
+
312
+ ```typescript
313
+ const chargePayment = defineStep({
314
+ name: 'chargePayment',
315
+ requires: z.object({
316
+ order: z.object({ total: z.number() }),
317
+ stripe: z.custom<Stripe>(),
318
+ }),
319
+ provides: z.object({ chargeId: z.string() }),
320
+ run: async (ctx) => {
321
+ const charge = await ctx.stripe.charges.create({ amount: ctx.order.total });
322
+ return { chargeId: charge.id };
323
+ },
324
+ });
325
+
326
+ const pipeline = createPipeline<{
327
+ orderId: string;
328
+ stripe: Stripe;
329
+ db: Database;
330
+ }>('placeOrder')
331
+ .step(validateOrder)
332
+ .step(chargePayment)
333
+ .build();
334
+
335
+ await pipeline.run({
336
+ orderId: '123',
337
+ stripe: stripeClient,
338
+ db: dbClient,
339
+ });
340
+ ```
341
+
342
+ Args persist through the entire pipeline without any step needing to `provides`
343
+ them. TypeScript enforces at compile time that every step's `requires` are
344
+ satisfied by the accumulated context. For testing, swap in mocks at the call
345
+ site.
346
+
347
+ ## Choice (branching)
348
+
349
+ Execute the first branch whose predicate returns `true` — like an AWS Step
350
+ Functions Choice state:
351
+
352
+ ```typescript
353
+ import { choice } from 'runsheet';
354
+
355
+ const placeOrder = buildPipeline({
356
+ name: 'placeOrder',
357
+ steps: [
358
+ validateOrder,
359
+ choice(
360
+ [(ctx) => ctx.method === 'card', chargeCard],
361
+ [(ctx) => ctx.method === 'bank', chargeBankTransfer],
362
+ chargeDefault, // default (bare step)
363
+ ),
364
+ sendConfirmation,
365
+ ],
366
+ });
367
+ ```
368
+
369
+ Predicates are evaluated in order — first match wins. A bare step (without a
370
+ tuple) can be passed as the last argument to serve as a default — equivalent to
371
+ `[() => true, step]`. If no predicate matches, the step fails with a
372
+ `CHOICE_NO_MATCH` error. Only the matched branch participates in rollback.
373
+
374
+ ## Map (collection iteration)
375
+
376
+ Iterate over a collection and run a function or step per item, concurrently —
377
+ like an AWS Step Functions Map state:
378
+
379
+ ```typescript
380
+ import { map } from 'runsheet';
381
+
382
+ // Function form — items can be any type
383
+ const pipeline = buildPipeline({
384
+ name: 'notify',
385
+ steps: [
386
+ map(
387
+ 'emails',
388
+ (ctx) => ctx.users,
389
+ async (user) => {
390
+ await sendEmail(user.email);
391
+ return { email: user.email, sentAt: new Date() };
392
+ },
393
+ ),
394
+ ],
395
+ });
396
+
397
+ // Step form — reuse existing steps
398
+ const pipeline = buildPipeline({
399
+ name: 'process',
400
+ steps: [map('results', (ctx) => ctx.items, processItem)],
401
+ });
402
+ ```
403
+
404
+ Items run concurrently via `Promise.allSettled`. Results are collected into an
405
+ array under the given key. In step form, each item is spread into the pipeline
406
+ context (`{ ...ctx, ...item }`) so the step sees both pipeline-level and
407
+ per-item values. On partial failure, succeeded items are rolled back (step form
408
+ only).
409
+
410
+ ### Filter (collection filtering)
411
+
412
+ ```typescript
413
+ import { filter, map } from 'runsheet';
414
+
415
+ const pipeline = buildPipeline({
416
+ name: 'notify',
417
+ steps: [
418
+ filter(
419
+ 'eligible',
420
+ (ctx) => ctx.users,
421
+ (user) => user.optedIn,
422
+ ),
423
+ map('emails', (ctx) => ctx.eligible, sendEmail),
424
+ ],
425
+ });
426
+
427
+ // Async predicate
428
+ filter(
429
+ 'valid',
430
+ (ctx) => ctx.orders,
431
+ async (order) => {
432
+ const inventory = await checkInventory(order.sku);
433
+ return inventory.available >= order.quantity;
434
+ },
435
+ );
436
+ ```
437
+
438
+ Predicates run concurrently via `Promise.allSettled`. Original order is
439
+ preserved. If any predicate throws, the step fails. No rollback (filtering is a
440
+ pure operation).
441
+
442
+ ### FlatMap (collection expansion)
443
+
444
+ ```typescript
445
+ import { flatMap } from 'runsheet';
446
+
447
+ const pipeline = buildPipeline({
448
+ name: 'process',
449
+ steps: [
450
+ flatMap(
451
+ 'lineItems',
452
+ (ctx) => ctx.orders,
453
+ (order) => order.items,
454
+ ),
455
+ ],
456
+ });
457
+
458
+ // Async callback
459
+ flatMap(
460
+ 'emails',
461
+ (ctx) => ctx.teams,
462
+ async (team) => {
463
+ const members = await fetchMembers(team.id);
464
+ return members.map((m) => m.email);
465
+ },
466
+ );
467
+ ```
468
+
469
+ Maps each item to an array, then flattens one level. Callbacks run concurrently
470
+ via `Promise.allSettled`. If any callback throws, the step fails. No rollback
471
+ (pure operation).
472
+
303
473
  ## Rollback
304
474
 
305
475
  When a step fails, rollback handlers for all previously completed steps execute
@@ -414,6 +584,31 @@ Run steps concurrently and merge their outputs. Returns a single step usable
414
584
  anywhere a regular step is accepted. On partial failure, succeeded inner steps
415
585
  are rolled back before the error propagates. p
416
586
 
587
+ ### `choice(...branches)`
588
+
589
+ Execute the first branch whose predicate returns `true`. Each branch is a
590
+ `[predicate, step]` tuple. A bare step can be passed as the last argument as a
591
+ default. Returns a single step usable anywhere a regular step is accepted. Only
592
+ the matched branch participates in rollback.
593
+
594
+ ### `map(key, collection, fnOrStep)`
595
+
596
+ Iterate over a collection and run a function or step per item, concurrently.
597
+ Results are collected into `{ [key]: Result[] }`. Accepts a plain function
598
+ `(item, ctx) => result` or a `TypedStep` (items must be objects, spread into
599
+ context). Step form supports per-item rollback on partial and external failure.
600
+
601
+ ### `filter(key, collection, predicate)`
602
+
603
+ Filter a collection from context using a sync or async predicate. Predicates run
604
+ concurrently. Items where the predicate returns `true` are kept; original order
605
+ is preserved. Results are collected into `{ [key]: Item[] }`. No rollback.
606
+
607
+ ### `flatMap(key, collection, fn)`
608
+
609
+ Map each item in a collection to an array, then flatten one level. Callbacks run
610
+ concurrently. Results are collected into `{ [key]: Result[] }`. No rollback.
611
+
417
612
  ### `when(predicate, step)`
418
613
 
419
614
  Wrap a step with a conditional predicate. The step only executes when the