runsheet 0.0.1 → 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
@@ -247,6 +251,225 @@ const placeOrder = createPipeline<{ orderId: string }>('placeOrder')
247
251
  .build();
248
252
  ```
249
253
 
254
+ ## Retry and timeout
255
+
256
+ Steps can declare retry policies and timeouts directly:
257
+
258
+ ```typescript
259
+ const callExternalApi = defineStep({
260
+ name: 'callExternalApi',
261
+ provides: z.object({ response: z.string() }),
262
+ retry: { count: 3, delay: 200, backoff: 'exponential' },
263
+ timeout: 5000,
264
+ run: async () => {
265
+ const res = await fetch('https://api.example.com/data');
266
+ return { response: await res.text() };
267
+ },
268
+ });
269
+ ```
270
+
271
+ **Retry** re-executes the step's `run` function on failure. The `retryIf`
272
+ predicate lets you inspect errors and decide whether to retry:
273
+
274
+ ```typescript
275
+ retry: {
276
+ count: 3,
277
+ retryIf: (errors) => errors.some((e) => e.message.includes('ECONNRESET')),
278
+ }
279
+ ```
280
+
281
+ **Timeout** races `run` against a timer. If the step exceeds the limit, it fails
282
+ with a `RunsheetError` code `'TIMEOUT'`. When both are set, each retry attempt
283
+ gets its own timeout.
284
+
285
+ ## Parallel steps
286
+
287
+ Run steps concurrently with `parallel()`. Outputs merge in array order:
288
+
289
+ ```typescript
290
+ import { parallel } from 'runsheet';
291
+
292
+ const placeOrder = buildPipeline({
293
+ name: 'placeOrder',
294
+ steps: [
295
+ validateOrder,
296
+ parallel(reserveInventory, chargePayment),
297
+ sendConfirmation,
298
+ ],
299
+ });
300
+ ```
301
+
302
+ On partial failure, succeeded inner steps are rolled back before the error
303
+ propagates. Inner steps retain their own `requires`/`provides` validation,
304
+ `retry`, and `timeout` behavior. Conditional steps (via `when()`) work inside
305
+ `parallel()`.
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
+
250
473
  ## Rollback
251
474
 
252
475
  When a step fails, rollback handlers for all previously completed steps execute
@@ -314,13 +537,15 @@ Define a pipeline step. Returns a strongly typed `TypedStep` — `run`,
314
537
  `rollback`, `requires`, and `provides` all carry concrete types matching the
315
538
  schemas or generics you provide.
316
539
 
317
- | Option | Type | Description |
318
- | ---------- | ----------------------- | ------------------------------------------------- |
319
- | `name` | `string` | Step name (used in metadata and rollback reports) |
320
- | `requires` | `ZodSchema` | Optional schema for required context keys |
321
- | `provides` | `ZodSchema` | Optional schema for provided context keys |
322
- | `run` | `(ctx) => output` | Step implementation (sync or async) |
323
- | `rollback` | `(ctx, output) => void` | Optional rollback handler |
540
+ | Option | Type | Description |
541
+ | ---------- | ----------------------- | -------------------------------------------------- |
542
+ | `name` | `string` | Step name (used in metadata and rollback reports) |
543
+ | `requires` | `ZodSchema` | Optional schema for required context keys |
544
+ | `provides` | `ZodSchema` | Optional schema for provided context keys |
545
+ | `run` | `(ctx) => output` | Step implementation (sync or async) |
546
+ | `rollback` | `(ctx, output) => void` | Optional rollback handler |
547
+ | `retry` | `RetryPolicy` | Optional retry policy for transient failures |
548
+ | `timeout` | `number` | Optional max duration in ms for the `run` function |
324
549
 
325
550
  ### `buildPipeline(config)`
326
551
 
@@ -328,14 +553,15 @@ Build a pipeline from an array of steps. The result type is inferred from the
328
553
  steps — `pipeline.run()` returns a `PipelineResult` whose `data` is the
329
554
  intersection of all step output types.
330
555
 
331
- | Option | Type | Description |
332
- | ------------ | ------------------ | --------------------------------------------- |
333
- | `name` | `string` | Pipeline name |
334
- | `steps` | `Step[]` | Steps to execute in order |
335
- | `middleware` | `StepMiddleware[]` | Optional middleware |
336
- | `argsSchema` | `ZodSchema` | Optional schema for pipeline input validation |
556
+ | Option | Type | Description |
557
+ | ------------ | ------------------ | ----------------------------------------------------------------- |
558
+ | `name` | `string` | Pipeline name |
559
+ | `steps` | `Step[]` | Steps to execute in order |
560
+ | `middleware` | `StepMiddleware[]` | Optional middleware |
561
+ | `argsSchema` | `ZodSchema` | Optional schema for pipeline input validation |
562
+ | `strict` | `boolean` | Optional — throws at build time if two steps provide the same key |
337
563
 
338
- ### `createPipeline(name, argsSchema?)`
564
+ ### `createPipeline(name, argsSchema?, options?)`
339
565
 
340
566
  Start a fluent pipeline builder. Returns a `PipelineBuilder` with:
341
567
 
@@ -343,6 +569,46 @@ Start a fluent pipeline builder. Returns a `PipelineBuilder` with:
343
569
  - `.use(...middleware)` — add middleware
344
570
  - `.build()` — produce the pipeline
345
571
 
572
+ The second argument accepts a schema (for runtime args validation) or an options
573
+ object:
574
+
575
+ ```typescript
576
+ createPipeline('order', z.object({ id: z.string() }));
577
+ createPipeline('order', { strict: true });
578
+ createPipeline('order', z.object({ id: z.string() }), { strict: true });
579
+ ```
580
+
581
+ ### `parallel(...steps)`
582
+
583
+ Run steps concurrently and merge their outputs. Returns a single step usable
584
+ anywhere a regular step is accepted. On partial failure, succeeded inner steps
585
+ are rolled back before the error propagates. p
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
+
346
612
  ### `when(predicate, step)`
347
613
 
348
614
  Wrap a step with a conditional predicate. The step only executes when the