runsheet 0.4.0 → 0.6.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
@@ -5,7 +5,7 @@
5
5
 
6
6
  Type-safe, composable business logic pipelines for TypeScript.
7
7
 
8
- Built on [composable-functions] for `Result` semantics and error handling.
8
+ Pipelines are steps compose them freely, nest them arbitrarily.
9
9
 
10
10
  ## Why runsheet
11
11
 
@@ -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
@@ -40,7 +44,7 @@ A pipeline orchestration library with:
40
44
  on hover. Both sync and async `run` functions are supported.
41
45
  - **Type-safe accumulated context** — each step declares what it requires and
42
46
  provides. TypeScript enforces at compile time that requirements are satisfied,
43
- and `buildPipeline` infers the full output type from the steps you pass.
47
+ and `pipeline` infers the full output type from the steps you pass.
44
48
  - **Immutable step boundaries** — context is frozen between steps. Each step
45
49
  receives a snapshot and returns only what it adds.
46
50
  - **Rollback with snapshots** — on failure, rollback handlers execute in reverse
@@ -123,9 +127,9 @@ integrity from one step to the next.
123
127
  ### Build and run a pipeline
124
128
 
125
129
  ```typescript
126
- import { buildPipeline } from 'runsheet';
130
+ import { pipeline } from 'runsheet';
127
131
 
128
- const placeOrder = buildPipeline({
132
+ const placeOrder = pipeline({
129
133
  name: 'placeOrder',
130
134
  steps: [validateOrder, chargePayment, sendConfirmation],
131
135
  });
@@ -136,7 +140,7 @@ if (result.success) {
136
140
  console.log(result.data.chargeId); // string — fully typed
137
141
  console.log(result.data.sentAt); // Date
138
142
  } else {
139
- console.error(result.errors); // what went wrong
143
+ console.error(result.error); // what went wrong
140
144
  console.log(result.rollback); // { completed: [...], failed: [...] }
141
145
  }
142
146
  ```
@@ -144,19 +148,35 @@ if (result.success) {
144
148
  The pipeline's result type is inferred from the steps — `result.data` carries
145
149
  the intersection of all step outputs, not an erased `Record<string, unknown>`.
146
150
 
151
+ ### Pipeline composition
152
+
153
+ Pipelines are steps — use one pipeline as a step in another:
154
+
155
+ ```typescript
156
+ const checkout = pipeline({
157
+ name: 'checkout',
158
+ steps: [validateOrder, chargePayment, sendConfirmation],
159
+ });
160
+
161
+ const fullFlow = pipeline({
162
+ name: 'fullFlow',
163
+ steps: [checkout, shipOrder, notifyWarehouse],
164
+ });
165
+ ```
166
+
147
167
  ### Builder API
148
168
 
149
169
  For complex pipelines, the builder gives progressive type narrowing — each
150
170
  `.step()` call extends the known context type:
151
171
 
152
172
  ```typescript
153
- import { createPipeline } from 'runsheet';
173
+ import { pipeline } from 'runsheet';
154
174
  import { z } from 'zod';
155
175
 
156
- const placeOrder = createPipeline(
157
- 'placeOrder',
158
- z.object({ orderId: z.string() }),
159
- )
176
+ const placeOrder = pipeline({
177
+ name: 'placeOrder',
178
+ argsSchema: z.object({ orderId: z.string() }),
179
+ })
160
180
  .step(validateOrder) // context now includes order
161
181
  .step(chargePayment) // context now includes chargeId
162
182
  .step(sendConfirmation) // context now includes sentAt
@@ -166,7 +186,7 @@ const placeOrder = createPipeline(
166
186
  Type-only args (no runtime validation of pipeline input):
167
187
 
168
188
  ```typescript
169
- const placeOrder = createPipeline<{ orderId: string }>('placeOrder')
189
+ const placeOrder = pipeline<{ orderId: string }>({ name: 'placeOrder' })
170
190
  .step(validateOrder)
171
191
  .step(chargePayment)
172
192
  .step(sendConfirmation)
@@ -193,7 +213,7 @@ const logOrder = defineStep<{ order: { id: string } }, { loggedAt: Date }>({
193
213
  ```typescript
194
214
  import { when } from 'runsheet';
195
215
 
196
- const placeOrder = buildPipeline({
216
+ const placeOrder = pipeline({
197
217
  name: 'placeOrder',
198
218
  steps: [
199
219
  validateOrder,
@@ -204,15 +224,15 @@ const placeOrder = buildPipeline({
204
224
  });
205
225
  ```
206
226
 
207
- Skipped steps produce no snapshot, no rollback entry. The pipeline result tracks
208
- which steps were skipped in `result.meta.stepsSkipped`.
227
+ Skipped steps produce no snapshot, no rollback entry, and do not appear in
228
+ `result.meta.stepsExecuted`.
209
229
 
210
230
  ### Middleware
211
231
 
212
232
  Middleware wraps the entire step lifecycle including schema validation:
213
233
 
214
234
  ```typescript
215
- import { buildPipeline } from 'runsheet';
235
+ import { pipeline } from 'runsheet';
216
236
  import type { StepMiddleware } from 'runsheet';
217
237
 
218
238
  const timing: StepMiddleware = (step, next) => async (ctx) => {
@@ -229,7 +249,7 @@ const logging: StepMiddleware = (step, next) => async (ctx) => {
229
249
  return result;
230
250
  };
231
251
 
232
- const placeOrder = buildPipeline({
252
+ const placeOrder = pipeline({
233
253
  name: 'placeOrder',
234
254
  steps: [validateOrder, chargePayment, sendConfirmation],
235
255
  middleware: [logging, timing],
@@ -239,7 +259,7 @@ const placeOrder = buildPipeline({
239
259
  Middleware with the builder:
240
260
 
241
261
  ```typescript
242
- const placeOrder = createPipeline<{ orderId: string }>('placeOrder')
262
+ const placeOrder = pipeline<{ orderId: string }>({ name: 'placeOrder' })
243
263
  .use(logging, timing)
244
264
  .step(validateOrder)
245
265
  .step(chargePayment)
@@ -285,7 +305,7 @@ Run steps concurrently with `parallel()`. Outputs merge in array order:
285
305
  ```typescript
286
306
  import { parallel } from 'runsheet';
287
307
 
288
- const placeOrder = buildPipeline({
308
+ const placeOrder = pipeline({
289
309
  name: 'placeOrder',
290
310
  steps: [
291
311
  validateOrder,
@@ -300,6 +320,172 @@ propagates. Inner steps retain their own `requires`/`provides` validation,
300
320
  `retry`, and `timeout` behavior. Conditional steps (via `when()`) work inside
301
321
  `parallel()`.
302
322
 
323
+ ## Dependency injection
324
+
325
+ No special mechanism needed — pass dependencies as pipeline args and they're
326
+ available to every step through the accumulated context:
327
+
328
+ ```typescript
329
+ const chargePayment = defineStep({
330
+ name: 'chargePayment',
331
+ requires: z.object({
332
+ order: z.object({ total: z.number() }),
333
+ stripe: z.custom<Stripe>(),
334
+ }),
335
+ provides: z.object({ chargeId: z.string() }),
336
+ run: async (ctx) => {
337
+ const charge = await ctx.stripe.charges.create({ amount: ctx.order.total });
338
+ return { chargeId: charge.id };
339
+ },
340
+ });
341
+
342
+ const placeOrder = pipeline<{
343
+ orderId: string;
344
+ stripe: Stripe;
345
+ db: Database;
346
+ }>({ name: 'placeOrder' })
347
+ .step(validateOrder)
348
+ .step(chargePayment)
349
+ .build();
350
+
351
+ await placeOrder.run({
352
+ orderId: '123',
353
+ stripe: stripeClient,
354
+ db: dbClient,
355
+ });
356
+ ```
357
+
358
+ Args persist through the entire pipeline without any step needing to `provides`
359
+ them. TypeScript enforces at compile time that every step's `requires` are
360
+ satisfied by the accumulated context. For testing, swap in mocks at the call
361
+ site.
362
+
363
+ ## Choice (branching)
364
+
365
+ Execute the first branch whose predicate returns `true` — like an AWS Step
366
+ Functions Choice state:
367
+
368
+ ```typescript
369
+ import { choice } from 'runsheet';
370
+
371
+ const placeOrder = pipeline({
372
+ name: 'placeOrder',
373
+ steps: [
374
+ validateOrder,
375
+ choice(
376
+ [(ctx) => ctx.method === 'card', chargeCard],
377
+ [(ctx) => ctx.method === 'bank', chargeBankTransfer],
378
+ chargeDefault, // default (bare step)
379
+ ),
380
+ sendConfirmation,
381
+ ],
382
+ });
383
+ ```
384
+
385
+ Predicates are evaluated in order — first match wins. A bare step (without a
386
+ tuple) can be passed as the last argument to serve as a default — equivalent to
387
+ `[() => true, step]`. If no predicate matches, the step fails with a
388
+ `CHOICE_NO_MATCH` error. Only the matched branch participates in rollback.
389
+
390
+ ## Map (collection iteration)
391
+
392
+ Iterate over a collection and run a function or step per item, concurrently —
393
+ like an AWS Step Functions Map state:
394
+
395
+ ```typescript
396
+ import { map } from 'runsheet';
397
+
398
+ // Function form — items can be any type
399
+ const p = pipeline({
400
+ name: 'notify',
401
+ steps: [
402
+ map(
403
+ 'emails',
404
+ (ctx) => ctx.users,
405
+ async (user) => {
406
+ await sendEmail(user.email);
407
+ return { email: user.email, sentAt: new Date() };
408
+ },
409
+ ),
410
+ ],
411
+ });
412
+
413
+ // Step form — reuse existing steps
414
+ const p = pipeline({
415
+ name: 'process',
416
+ steps: [map('results', (ctx) => ctx.items, processItem)],
417
+ });
418
+ ```
419
+
420
+ Items run concurrently via `Promise.allSettled`. Results are collected into an
421
+ array under the given key. In step form, each item is spread into the pipeline
422
+ context (`{ ...ctx, ...item }`) so the step sees both pipeline-level and
423
+ per-item values. On partial failure, succeeded items are rolled back (step form
424
+ only).
425
+
426
+ ### Filter (collection filtering)
427
+
428
+ ```typescript
429
+ import { filter, map } from 'runsheet';
430
+
431
+ const p = pipeline({
432
+ name: 'notify',
433
+ steps: [
434
+ filter(
435
+ 'eligible',
436
+ (ctx) => ctx.users,
437
+ (user) => user.optedIn,
438
+ ),
439
+ map('emails', (ctx) => ctx.eligible, sendEmail),
440
+ ],
441
+ });
442
+
443
+ // Async predicate
444
+ filter(
445
+ 'valid',
446
+ (ctx) => ctx.orders,
447
+ async (order) => {
448
+ const inventory = await checkInventory(order.sku);
449
+ return inventory.available >= order.quantity;
450
+ },
451
+ );
452
+ ```
453
+
454
+ Predicates run concurrently via `Promise.allSettled`. Original order is
455
+ preserved. If any predicate throws, the step fails. No rollback (filtering is a
456
+ pure operation).
457
+
458
+ ### FlatMap (collection expansion)
459
+
460
+ ```typescript
461
+ import { flatMap } from 'runsheet';
462
+
463
+ const p = pipeline({
464
+ name: 'process',
465
+ steps: [
466
+ flatMap(
467
+ 'lineItems',
468
+ (ctx) => ctx.orders,
469
+ (order) => order.items,
470
+ ),
471
+ ],
472
+ });
473
+
474
+ // Async callback
475
+ flatMap(
476
+ 'emails',
477
+ (ctx) => ctx.teams,
478
+ async (team) => {
479
+ const members = await fetchMembers(team.id);
480
+ return members.map((m) => m.email);
481
+ },
482
+ );
483
+ ```
484
+
485
+ Maps each item to an array, then flattens one level. Callbacks run concurrently
486
+ via `Promise.allSettled`. If any callback throws, the step fails. No rollback
487
+ (pure operation).
488
+
303
489
  ## Rollback
304
490
 
305
491
  When a step fails, rollback handlers for all previously completed steps execute
@@ -331,29 +517,27 @@ if (!result.success) {
331
517
  }
332
518
  ```
333
519
 
334
- ## Pipeline result
520
+ ## Step result
335
521
 
336
- Every pipeline returns a `PipelineResult` with execution metadata:
522
+ Every `run()` returns a `StepResult` with execution metadata:
337
523
 
338
524
  ```typescript
339
525
  // Success
340
526
  {
341
527
  success: true,
342
528
  data: { /* accumulated context — fully typed */ },
343
- errors: [],
344
529
  meta: {
345
- pipeline: 'placeOrder',
530
+ name: 'placeOrder',
346
531
  args: { orderId: '123' },
347
532
  stepsExecuted: ['validateOrder', 'chargePayment', 'sendConfirmation'],
348
- stepsSkipped: [],
349
533
  }
350
534
  }
351
535
 
352
536
  // Failure
353
537
  {
354
538
  success: false,
355
- errors: [Error],
356
- meta: { pipeline, args, stepsExecuted, stepsSkipped },
539
+ error: Error,
540
+ meta: { name, args, stepsExecuted },
357
541
  failedStep: 'chargePayment',
358
542
  rollback: { completed: [...], failed: [...] },
359
543
  }
@@ -377,42 +561,50 @@ schemas or generics you provide.
377
561
  | `retry` | `RetryPolicy` | Optional retry policy for transient failures |
378
562
  | `timeout` | `number` | Optional max duration in ms for the `run` function |
379
563
 
380
- ### `buildPipeline(config)`
564
+ ### `pipeline(config)`
381
565
 
382
- Build a pipeline from an array of steps. The result type is inferred from the
383
- steps `pipeline.run()` returns a `PipelineResult` whose `data` is the
384
- intersection of all step output types.
566
+ Create a pipeline. When `steps` is provided, returns an `AggregateStep`
567
+ immediately. When `steps` is omitted, returns a `PipelineBuilder` with
568
+ `.step()`, `.use()`, and `.build()` for progressive type narrowing.
385
569
 
386
570
  | Option | Type | Description |
387
571
  | ------------ | ------------------ | ----------------------------------------------------------------- |
388
572
  | `name` | `string` | Pipeline name |
389
- | `steps` | `Step[]` | Steps to execute in order |
573
+ | `steps` | `Step[]` | Steps to execute in order (omit for builder mode) |
390
574
  | `middleware` | `StepMiddleware[]` | Optional middleware |
391
575
  | `argsSchema` | `ZodSchema` | Optional schema for pipeline input validation |
392
576
  | `strict` | `boolean` | Optional — throws at build time if two steps provide the same key |
393
577
 
394
- ### `createPipeline(name, argsSchema?, options?)`
578
+ ### `parallel(...steps)`
395
579
 
396
- Start a fluent pipeline builder. Returns a `PipelineBuilder` with:
580
+ Run steps concurrently and merge their outputs. Returns a single step usable
581
+ anywhere a regular step is accepted. On partial failure, succeeded inner steps
582
+ are rolled back before the error propagates.
397
583
 
398
- - `.step(step)` — add a step
399
- - `.use(...middleware)` — add middleware
400
- - `.build()` — produce the pipeline
584
+ ### `choice(...branches)`
401
585
 
402
- The second argument accepts a schema (for runtime args validation) or an options
403
- object:
586
+ Execute the first branch whose predicate returns `true`. Each branch is a
587
+ `[predicate, step]` tuple. A bare step can be passed as the last argument as a
588
+ default. Returns a single step usable anywhere a regular step is accepted. Only
589
+ the matched branch participates in rollback.
404
590
 
405
- ```typescript
406
- createPipeline('order', z.object({ id: z.string() }));
407
- createPipeline('order', { strict: true });
408
- createPipeline('order', z.object({ id: z.string() }), { strict: true });
409
- ```
591
+ ### `map(key, collection, fnOrStep)`
410
592
 
411
- ### `parallel(...steps)`
593
+ Iterate over a collection and run a function or step per item, concurrently.
594
+ Results are collected into `{ [key]: Result[] }`. Accepts a plain function
595
+ `(item, ctx) => result` or a `TypedStep` (items must be objects, spread into
596
+ context). Step form supports per-item rollback on partial and external failure.
412
597
 
413
- Run steps concurrently and merge their outputs. Returns a single step usable
414
- anywhere a regular step is accepted. On partial failure, succeeded inner steps
415
- are rolled back before the error propagates. p
598
+ ### `filter(key, collection, predicate)`
599
+
600
+ Filter a collection from context using a sync or async predicate. Predicates run
601
+ concurrently. Items where the predicate returns `true` are kept; original order
602
+ is preserved. Results are collected into `{ [key]: Item[] }`. No rollback.
603
+
604
+ ### `flatMap(key, collection, fn)`
605
+
606
+ Map each item in a collection to an array, then flatten one level. Callbacks run
607
+ concurrently. Results are collected into `{ [key]: Result[] }`. No rollback.
416
608
 
417
609
  ### `when(predicate, step)`
418
610
 
@@ -434,7 +626,6 @@ MIT
434
626
  [ci-badge]:
435
627
  https://github.com/shaug/runsheet-js/actions/workflows/ci.yml/badge.svg
436
628
  [ci-url]: https://github.com/shaug/runsheet-js/actions/workflows/ci.yml
437
- [composable-functions]: https://github.com/seasonedcc/composable-functions
438
629
  [license-badge]: https://img.shields.io/npm/l/runsheet
439
630
  [license-url]: https://github.com/shaug/runsheet-js/blob/main/LICENSE
440
631
  [npm-badge]: https://img.shields.io/npm/v/runsheet