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 +238 -47
- package/dist/index.cjs +639 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +497 -243
- package/dist/index.d.ts +497 -243
- package/dist/index.js +624 -247
- package/dist/index.js.map +1 -1
- package/llms.txt +191 -36
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
Type-safe, composable business logic pipelines for TypeScript.
|
|
7
7
|
|
|
8
|
-
|
|
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 `
|
|
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 {
|
|
130
|
+
import { pipeline } from 'runsheet';
|
|
127
131
|
|
|
128
|
-
const placeOrder =
|
|
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.
|
|
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 {
|
|
173
|
+
import { pipeline } from 'runsheet';
|
|
154
174
|
import { z } from 'zod';
|
|
155
175
|
|
|
156
|
-
const placeOrder =
|
|
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 =
|
|
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 =
|
|
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
|
|
208
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
##
|
|
520
|
+
## Step result
|
|
335
521
|
|
|
336
|
-
Every
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
meta: {
|
|
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
|
-
### `
|
|
564
|
+
### `pipeline(config)`
|
|
381
565
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
### `
|
|
578
|
+
### `parallel(...steps)`
|
|
395
579
|
|
|
396
|
-
|
|
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
|
-
|
|
399
|
-
- `.use(...middleware)` — add middleware
|
|
400
|
-
- `.build()` — produce the pipeline
|
|
584
|
+
### `choice(...branches)`
|
|
401
585
|
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|