runsheet 0.6.0 → 0.7.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 +244 -198
- package/dist/index.cjs +175 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -140
- package/dist/index.d.ts +64 -140
- package/dist/index.js +173 -280
- package/dist/index.js.map +1 -1
- package/llms.txt +38 -90
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,53 +5,181 @@
|
|
|
5
5
|
|
|
6
6
|
Type-safe, composable business logic pipelines for TypeScript.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
```typescript
|
|
9
|
+
import { step, pipeline } from 'runsheet';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
const validateOrder = step({
|
|
13
|
+
name: 'validateOrder',
|
|
14
|
+
requires: z.object({ orderId: z.string() }),
|
|
15
|
+
provides: z.object({
|
|
16
|
+
order: z.object({ id: z.string(), total: z.number() }),
|
|
17
|
+
}),
|
|
18
|
+
run: async (ctx) => ({ order: await db.orders.find(ctx.orderId) }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const chargePayment = step({
|
|
22
|
+
name: 'chargePayment',
|
|
23
|
+
requires: z.object({ order: z.object({ total: z.number() }) }),
|
|
24
|
+
provides: z.object({ chargeId: z.string() }),
|
|
25
|
+
run: async (ctx) => {
|
|
26
|
+
const charge = await stripe.charges.create({ amount: ctx.order.total });
|
|
27
|
+
return { chargeId: charge.id };
|
|
28
|
+
},
|
|
29
|
+
rollback: async (_ctx, output) => {
|
|
30
|
+
await stripe.refunds.create({ charge: output.chargeId });
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const placeOrder = pipeline({
|
|
35
|
+
name: 'placeOrder',
|
|
36
|
+
steps: [validateOrder, chargePayment],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = await placeOrder.run({ orderId: '123' });
|
|
40
|
+
// result.data.chargeId — fully typed, inferred from the steps
|
|
41
|
+
```
|
|
9
42
|
|
|
10
43
|
## Why runsheet
|
|
11
44
|
|
|
12
45
|
Business logic has a way of growing into tangled, hard-to-test code. A checkout
|
|
13
46
|
flow starts as one function, then gains validation, payment processing,
|
|
14
|
-
inventory reservation, email notifications
|
|
47
|
+
inventory reservation, email notifications — each with its own failure modes and
|
|
15
48
|
cleanup logic. Before long you're staring at a 300-line function with nested
|
|
16
49
|
try/catch blocks and no clear way to reuse any of it.
|
|
17
50
|
|
|
18
51
|
runsheet gives that logic structure. You break work into small, focused steps
|
|
19
52
|
with explicit inputs and outputs, then compose them into pipelines. Each step is
|
|
20
|
-
independently testable. The pipeline handles context
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
can't accidentally
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
independently testable. The pipeline handles context accumulation — args
|
|
54
|
+
persist, outputs merge — along with rollback on failure and (optionally) schema
|
|
55
|
+
validation at step boundaries. TypeScript enforces that steps fit together at
|
|
56
|
+
compile time, and immutable context boundaries mean steps can't accidentally
|
|
57
|
+
interfere with each other.
|
|
58
|
+
|
|
59
|
+
runsheet is for **in-process business logic orchestration** — multi-step flows
|
|
60
|
+
inside an application service. It is not a distributed workflow engine, job
|
|
61
|
+
queue, or durable execution runtime. That said, the two are complementary: a
|
|
62
|
+
runsheet pipeline works well as the business logic inside a [Temporal] activity
|
|
63
|
+
or a serverless function handler.
|
|
28
64
|
|
|
29
65
|
The name takes its inspiration from the world of stage productions and live
|
|
30
66
|
broadcast events. A runsheet is the document that sequences every cue, handoff,
|
|
31
67
|
and contingency so the show runs smoothly. Same idea here: you define the steps,
|
|
32
68
|
and runsheet makes sure they execute in order with clear contracts between them.
|
|
33
69
|
|
|
34
|
-
##
|
|
70
|
+
## When to use it
|
|
71
|
+
|
|
72
|
+
**Good fit:**
|
|
73
|
+
|
|
74
|
+
- Multi-step business flows — checkout, onboarding, provisioning, data import
|
|
75
|
+
- Operations that need compensating actions (rollback) on failure
|
|
76
|
+
- Reusable orchestration shared across handlers, jobs, and routes
|
|
77
|
+
- Logic where schema-checked boundaries between steps add confidence
|
|
78
|
+
- Anywhere you'd otherwise write a long imperative function with a growing
|
|
79
|
+
number of intermediate variables and try/catch blocks
|
|
80
|
+
|
|
81
|
+
**Not the right tool:**
|
|
82
|
+
|
|
83
|
+
- Trivial one-off functions that don't benefit from step decomposition
|
|
84
|
+
- Long-running durable workflows that survive process restarts — use [Temporal]
|
|
85
|
+
or [Inngest]
|
|
86
|
+
- Cross-service event choreography — use a message bus
|
|
87
|
+
- Simple CRUD handlers with no orchestration complexity
|
|
88
|
+
|
|
89
|
+
## What you get
|
|
35
90
|
|
|
36
|
-
|
|
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.
|
|
91
|
+
### Typed accumulated context
|
|
39
92
|
|
|
40
|
-
|
|
93
|
+
Args persist and outputs accumulate. Initial arguments flow through the entire
|
|
94
|
+
pipeline, each step's output merges into the context, and every step sees the
|
|
95
|
+
full picture of everything before it. TypeScript infers the accumulated type
|
|
96
|
+
from the steps you pass — `result.data` is a concrete intersection, not
|
|
97
|
+
`Record<string, unknown>`.
|
|
41
98
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
### Immutable step boundaries
|
|
100
|
+
|
|
101
|
+
Context is frozen (`Object.freeze`) at every step boundary — this is a
|
|
102
|
+
guarantee, not an implementation detail. Each step receives a read-only snapshot
|
|
103
|
+
and returns only what it adds. Steps cannot mutate shared pipeline state; the
|
|
104
|
+
pipeline engine manages accumulation. This eliminates a class of bugs where step
|
|
105
|
+
B accidentally corrupts step A's data, and it makes rollback reliable because
|
|
106
|
+
each handler receives the exact snapshot from before the step ran.
|
|
107
|
+
|
|
108
|
+
### Two modes of type safety
|
|
109
|
+
|
|
110
|
+
**TypeScript generics only** — compile-time composition safety, no runtime cost:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const logOrder = step<{ order: { id: string } }, { loggedAt: Date }>({
|
|
114
|
+
name: 'logOrder',
|
|
115
|
+
run: async (ctx) => {
|
|
116
|
+
console.log(`Processing order ${ctx.order.id}`);
|
|
117
|
+
return { loggedAt: new Date() };
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Zod schemas** — adds runtime validation at step boundaries (requires and/or
|
|
123
|
+
provides):
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const chargePayment = step({
|
|
127
|
+
name: 'chargePayment',
|
|
128
|
+
requires: z.object({ order: z.object({ total: z.number() }) }),
|
|
129
|
+
provides: z.object({ chargeId: z.string() }),
|
|
130
|
+
run: async (ctx) => {
|
|
131
|
+
const charge = await stripe.charges.create({ amount: ctx.order.total });
|
|
132
|
+
return { chargeId: charge.id };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Mix and match freely — some steps can use schemas while others use generics
|
|
138
|
+
alone. Schemas are per-step, not all-or-nothing.
|
|
139
|
+
|
|
140
|
+
### Rollback with snapshots
|
|
141
|
+
|
|
142
|
+
On failure, rollback handlers execute in reverse order. Each handler receives
|
|
143
|
+
the pre-step context snapshot and the step's output, so it knows exactly what to
|
|
144
|
+
undo:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const reserveInventory = step({
|
|
148
|
+
name: 'reserveInventory',
|
|
149
|
+
requires: z.object({ order: z.object({ items: z.array(z.string()) }) }),
|
|
150
|
+
provides: z.object({ reservationId: z.string() }),
|
|
151
|
+
run: async (ctx) => {
|
|
152
|
+
const reservation = await inventory.reserve(ctx.order.items);
|
|
153
|
+
return { reservationId: reservation.id };
|
|
154
|
+
},
|
|
155
|
+
rollback: async (_ctx, output) => {
|
|
156
|
+
await inventory.release(output.reservationId);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Rollback is best-effort: if a handler throws, remaining rollbacks still execute.
|
|
162
|
+
The result includes a structured report:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
if (!result.success) {
|
|
166
|
+
result.rollback.completed; // ['chargePayment', 'reserveInventory']
|
|
167
|
+
result.rollback.failed; // [{ step: 'sendNotification', error: Error }]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Compared to alternatives
|
|
172
|
+
|
|
173
|
+
| Capability | Plain functions | Ad hoc orchestration | runsheet |
|
|
174
|
+
| ------------------------------- | --------------- | -------------------- | ------------------------- |
|
|
175
|
+
| Reusable business steps | Manual | Manual | Built-in |
|
|
176
|
+
| Typed accumulated context | — | Manual | Inferred from steps |
|
|
177
|
+
| Rollback / compensation | Manual | Manual | Automatic, snapshot-based |
|
|
178
|
+
| Schema validation at boundaries | — | Manual | Optional (Zod) |
|
|
179
|
+
| Middleware (logging, timing) | Manual | Manual | Built-in |
|
|
180
|
+
| Control-flow combinators | Manual | Manual | Built-in |
|
|
181
|
+
| Composable (nest pipelines) | Manual | Difficult | Pipelines are steps |
|
|
182
|
+
| Immutable context boundaries | — | Rarely | Always (`Object.freeze`) |
|
|
55
183
|
|
|
56
184
|
## Install
|
|
57
185
|
|
|
@@ -72,15 +200,13 @@ pnpm add runsheet
|
|
|
72
200
|
### Define steps
|
|
73
201
|
|
|
74
202
|
Each step declares what it reads from context (`requires`) and what it adds
|
|
75
|
-
(`provides`).
|
|
76
|
-
|
|
77
|
-
Step `run` functions can be sync or async — both are supported.
|
|
203
|
+
(`provides`). Step `run` functions can be sync or async.
|
|
78
204
|
|
|
79
205
|
```typescript
|
|
80
|
-
import {
|
|
206
|
+
import { step } from 'runsheet';
|
|
81
207
|
import { z } from 'zod';
|
|
82
208
|
|
|
83
|
-
const validateOrder =
|
|
209
|
+
const validateOrder = step({
|
|
84
210
|
name: 'validateOrder',
|
|
85
211
|
requires: z.object({ orderId: z.string() }),
|
|
86
212
|
provides: z.object({
|
|
@@ -93,7 +219,7 @@ const validateOrder = defineStep({
|
|
|
93
219
|
},
|
|
94
220
|
});
|
|
95
221
|
|
|
96
|
-
const chargePayment =
|
|
222
|
+
const chargePayment = step({
|
|
97
223
|
name: 'chargePayment',
|
|
98
224
|
requires: z.object({ order: z.object({ total: z.number() }) }),
|
|
99
225
|
provides: z.object({ chargeId: z.string() }),
|
|
@@ -106,7 +232,7 @@ const chargePayment = defineStep({
|
|
|
106
232
|
},
|
|
107
233
|
});
|
|
108
234
|
|
|
109
|
-
const sendConfirmation =
|
|
235
|
+
const sendConfirmation = step({
|
|
110
236
|
name: 'sendConfirmation',
|
|
111
237
|
requires: z.object({
|
|
112
238
|
order: z.object({ id: z.string() }),
|
|
@@ -120,10 +246,6 @@ const sendConfirmation = defineStep({
|
|
|
120
246
|
});
|
|
121
247
|
```
|
|
122
248
|
|
|
123
|
-
Each step is fully typed — your IDE (or other favorite typechecker) can see its
|
|
124
|
-
exact input and output types, allowing you to compose steps that maintain type
|
|
125
|
-
integrity from one step to the next.
|
|
126
|
-
|
|
127
249
|
### Build and run a pipeline
|
|
128
250
|
|
|
129
251
|
```typescript
|
|
@@ -145,8 +267,54 @@ if (result.success) {
|
|
|
145
267
|
}
|
|
146
268
|
```
|
|
147
269
|
|
|
148
|
-
|
|
149
|
-
|
|
270
|
+
### A second example: workspace onboarding
|
|
271
|
+
|
|
272
|
+
Steps compose across domains. The same patterns — typed inputs, rollback on
|
|
273
|
+
failure, accumulated context — apply to any multi-step flow:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const createWorkspace = step({
|
|
277
|
+
name: 'createWorkspace',
|
|
278
|
+
requires: z.object({ ownerEmail: z.string(), plan: z.string() }),
|
|
279
|
+
provides: z.object({ workspaceId: z.string() }),
|
|
280
|
+
run: async (ctx) => {
|
|
281
|
+
const ws = await db.workspaces.create({
|
|
282
|
+
owner: ctx.ownerEmail,
|
|
283
|
+
plan: ctx.plan,
|
|
284
|
+
});
|
|
285
|
+
return { workspaceId: ws.id };
|
|
286
|
+
},
|
|
287
|
+
rollback: async (_ctx, output) => {
|
|
288
|
+
await db.workspaces.delete(output.workspaceId);
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const provisionResources = step({
|
|
293
|
+
name: 'provisionResources',
|
|
294
|
+
requires: z.object({ workspaceId: z.string(), plan: z.string() }),
|
|
295
|
+
provides: z.object({ bucketArn: z.string(), dbUrl: z.string() }),
|
|
296
|
+
run: async (ctx) => {
|
|
297
|
+
const infra = await provisioner.create(ctx.workspaceId, ctx.plan);
|
|
298
|
+
return { bucketArn: infra.bucketArn, dbUrl: infra.dbUrl };
|
|
299
|
+
},
|
|
300
|
+
rollback: async (_ctx, output) => {
|
|
301
|
+
await provisioner.teardown(output.bucketArn, output.dbUrl);
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const onboardWorkspace = pipeline({
|
|
306
|
+
name: 'onboardWorkspace',
|
|
307
|
+
steps: [createWorkspace, provisionResources, sendWelcomeEmail],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const result = await onboardWorkspace.run({
|
|
311
|
+
ownerEmail: 'alice@co.com',
|
|
312
|
+
plan: 'team',
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
If `sendWelcomeEmail` fails, provisioned resources are torn down and the
|
|
317
|
+
workspace is deleted — automatically, in reverse order.
|
|
150
318
|
|
|
151
319
|
### Pipeline composition
|
|
152
320
|
|
|
@@ -193,21 +361,6 @@ const placeOrder = pipeline<{ orderId: string }>({ name: 'placeOrder' })
|
|
|
193
361
|
.build();
|
|
194
362
|
```
|
|
195
363
|
|
|
196
|
-
### Generics-only steps
|
|
197
|
-
|
|
198
|
-
Steps don't need Zod schemas — TypeScript generics provide compile-time safety
|
|
199
|
-
without runtime validation at step boundaries:
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
const logOrder = defineStep<{ order: { id: string } }, { loggedAt: Date }>({
|
|
203
|
-
name: 'logOrder',
|
|
204
|
-
run: async (ctx) => {
|
|
205
|
-
console.log(`Processing order ${ctx.order.id}`);
|
|
206
|
-
return { loggedAt: new Date() };
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
364
|
### Conditional steps
|
|
212
365
|
|
|
213
366
|
```typescript
|
|
@@ -272,7 +425,7 @@ const placeOrder = pipeline<{ orderId: string }>({ name: 'placeOrder' })
|
|
|
272
425
|
Steps can declare retry policies and timeouts directly:
|
|
273
426
|
|
|
274
427
|
```typescript
|
|
275
|
-
const callExternalApi =
|
|
428
|
+
const callExternalApi = step({
|
|
276
429
|
name: 'callExternalApi',
|
|
277
430
|
provides: z.object({ response: z.string() }),
|
|
278
431
|
retry: { count: 3, delay: 200, backoff: 'exponential' },
|
|
@@ -326,7 +479,7 @@ No special mechanism needed — pass dependencies as pipeline args and they're
|
|
|
326
479
|
available to every step through the accumulated context:
|
|
327
480
|
|
|
328
481
|
```typescript
|
|
329
|
-
const chargePayment =
|
|
482
|
+
const chargePayment = step({
|
|
330
483
|
name: 'chargePayment',
|
|
331
484
|
requires: z.object({
|
|
332
485
|
order: z.object({ total: z.number() }),
|
|
@@ -384,138 +537,39 @@ const placeOrder = pipeline({
|
|
|
384
537
|
|
|
385
538
|
Predicates are evaluated in order — first match wins. A bare step (without a
|
|
386
539
|
tuple) can be passed as the last argument to serve as a default — equivalent to
|
|
387
|
-
`[() => true, step]`. If no predicate matches, the step
|
|
388
|
-
|
|
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
|
-
});
|
|
540
|
+
`[() => true, step]`. If no predicate matches, the step is skipped (empty data,
|
|
541
|
+
no rollback entry). Only the matched branch participates in rollback.
|
|
412
542
|
|
|
413
|
-
|
|
414
|
-
const p = pipeline({
|
|
415
|
-
name: 'process',
|
|
416
|
-
steps: [map('results', (ctx) => ctx.items, processItem)],
|
|
417
|
-
});
|
|
418
|
-
```
|
|
543
|
+
## Distribute (collection fan-out)
|
|
419
544
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
per-item values. On partial failure, succeeded items are rolled back (step form
|
|
424
|
-
only).
|
|
425
|
-
|
|
426
|
-
### Filter (collection filtering)
|
|
545
|
+
Fan out a step over one or more context collections, binding each item to the
|
|
546
|
+
step's named scalar inputs. With multiple collections, `distribute` computes the
|
|
547
|
+
cross product and runs the step once per combination.
|
|
427
548
|
|
|
428
549
|
```typescript
|
|
429
|
-
import {
|
|
550
|
+
import { distribute } from 'runsheet';
|
|
430
551
|
|
|
431
|
-
|
|
432
|
-
|
|
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(
|
|
552
|
+
// Single collection — run sendEmail once per accountId
|
|
553
|
+
const sendEmails = distribute(
|
|
476
554
|
'emails',
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const members = await fetchMembers(team.id);
|
|
480
|
-
return members.map((m) => m.email);
|
|
481
|
-
},
|
|
555
|
+
{ accountIds: 'accountId' },
|
|
556
|
+
sendEmailStep,
|
|
482
557
|
);
|
|
558
|
+
// Requires: { orgId: string, accountIds: string[] }
|
|
559
|
+
// Provides: { emails: { emailId: string }[] }
|
|
560
|
+
|
|
561
|
+
// Cross product — run once per (accountId, regionId) pair
|
|
562
|
+
const generateReports = distribute(
|
|
563
|
+
'reports',
|
|
564
|
+
{ accountIds: 'accountId', regionIds: 'regionId' },
|
|
565
|
+
generateReportStep,
|
|
566
|
+
);
|
|
567
|
+
// 2 accounts × 3 regions = 6 concurrent executions
|
|
483
568
|
```
|
|
484
569
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
## Rollback
|
|
490
|
-
|
|
491
|
-
When a step fails, rollback handlers for all previously completed steps execute
|
|
492
|
-
in reverse order. Each handler receives the pre-step context snapshot and the
|
|
493
|
-
step's output:
|
|
494
|
-
|
|
495
|
-
```typescript
|
|
496
|
-
const reserveInventory = defineStep({
|
|
497
|
-
name: 'reserveInventory',
|
|
498
|
-
requires: z.object({ order: z.object({ items: z.array(z.string()) }) }),
|
|
499
|
-
provides: z.object({ reservationId: z.string() }),
|
|
500
|
-
run: async (ctx) => {
|
|
501
|
-
const reservation = await inventory.reserve(ctx.order.items);
|
|
502
|
-
return { reservationId: reservation.id };
|
|
503
|
-
},
|
|
504
|
-
rollback: async (_ctx, output) => {
|
|
505
|
-
await inventory.release(output.reservationId);
|
|
506
|
-
},
|
|
507
|
-
});
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Rollback is best-effort: if a rollback handler throws, remaining rollbacks still
|
|
511
|
-
execute. The result includes a structured report:
|
|
512
|
-
|
|
513
|
-
```typescript
|
|
514
|
-
if (!result.success) {
|
|
515
|
-
result.rollback.completed; // ['chargePayment', 'reserveInventory']
|
|
516
|
-
result.rollback.failed; // [{ step: 'sendNotification', error: Error }]
|
|
517
|
-
}
|
|
518
|
-
```
|
|
570
|
+
The mapping object connects context array keys to the step's scalar input keys.
|
|
571
|
+
All non-mapped context keys pass through unchanged. Items run concurrently and
|
|
572
|
+
support partial-failure rollback.
|
|
519
573
|
|
|
520
574
|
## Step result
|
|
521
575
|
|
|
@@ -545,7 +599,7 @@ Every `run()` returns a `StepResult` with execution metadata:
|
|
|
545
599
|
|
|
546
600
|
## API reference
|
|
547
601
|
|
|
548
|
-
### `
|
|
602
|
+
### `step(config)`
|
|
549
603
|
|
|
550
604
|
Define a pipeline step. Returns a strongly typed `TypedStep` — `run`,
|
|
551
605
|
`rollback`, `requires`, and `provides` all carry concrete types matching the
|
|
@@ -588,23 +642,13 @@ Execute the first branch whose predicate returns `true`. Each branch is a
|
|
|
588
642
|
default. Returns a single step usable anywhere a regular step is accepted. Only
|
|
589
643
|
the matched branch participates in rollback.
|
|
590
644
|
|
|
591
|
-
### `
|
|
592
|
-
|
|
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.
|
|
597
|
-
|
|
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)`
|
|
645
|
+
### `distribute(key, mapping, step)`
|
|
605
646
|
|
|
606
|
-
|
|
607
|
-
|
|
647
|
+
Distribute collections from context across a step. The mapping object connects
|
|
648
|
+
context array keys to the step's scalar input keys. Runs the step once per item
|
|
649
|
+
(single mapping) or once per cross-product combination (multiple mappings).
|
|
650
|
+
Non-mapped context keys pass through. Supports per-item rollback on partial and
|
|
651
|
+
external failure.
|
|
608
652
|
|
|
609
653
|
### `when(predicate, step)`
|
|
610
654
|
|
|
@@ -626,7 +670,9 @@ MIT
|
|
|
626
670
|
[ci-badge]:
|
|
627
671
|
https://github.com/shaug/runsheet-js/actions/workflows/ci.yml/badge.svg
|
|
628
672
|
[ci-url]: https://github.com/shaug/runsheet-js/actions/workflows/ci.yml
|
|
673
|
+
[Inngest]: https://www.inngest.com/
|
|
629
674
|
[license-badge]: https://img.shields.io/npm/l/runsheet
|
|
630
675
|
[license-url]: https://github.com/shaug/runsheet-js/blob/main/LICENSE
|
|
631
676
|
[npm-badge]: https://img.shields.io/npm/v/runsheet
|
|
632
677
|
[npm-url]: https://www.npmjs.com/package/runsheet
|
|
678
|
+
[Temporal]: https://temporal.io/
|