runsheet 0.5.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 +291 -249
- package/dist/index.cjs +418 -520
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +365 -400
- package/dist/index.d.ts +365 -400
- package/dist/index.js +415 -513
- package/dist/index.js.map +1 -1
- package/llms.txt +93 -124
- package/package.json +2 -5
package/llms.txt
CHANGED
|
@@ -7,8 +7,8 @@ focused steps with explicit inputs and outputs, then compose them into pipelines
|
|
|
7
7
|
that handle context passing, rollback on failure, and schema validation at every
|
|
8
8
|
boundary.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
schema validation but is optional — TypeScript generics alone provide
|
|
10
|
+
Uses its own `StepResult` type for result semantics. Zod is supported for
|
|
11
|
+
runtime schema validation but is optional — TypeScript generics alone provide
|
|
12
12
|
compile-time safety.
|
|
13
13
|
|
|
14
14
|
## Core concepts
|
|
@@ -20,7 +20,9 @@ compile-time safety.
|
|
|
20
20
|
context).
|
|
21
21
|
- Pipelines run steps sequentially. On failure, rollback handlers execute in
|
|
22
22
|
reverse order with snapshot-based context.
|
|
23
|
-
- Results use `
|
|
23
|
+
- Results use `StepResult<T>` — a discriminated union: `StepSuccess<T>`
|
|
24
|
+
(`success`, `data`, `meta`) or `StepFailure` (`success`, `error`, `meta`,
|
|
25
|
+
`failedStep`, `rollback`). Never throws.
|
|
24
26
|
|
|
25
27
|
## Quick reference
|
|
26
28
|
|
|
@@ -34,16 +36,16 @@ zod is an optional peer dependency — only needed if you use schema validation.
|
|
|
34
36
|
Install it alongside runsheet if you want runtime schema validation at step
|
|
35
37
|
boundaries: `pnpm add runsheet zod`
|
|
36
38
|
|
|
37
|
-
###
|
|
39
|
+
### step
|
|
38
40
|
|
|
39
41
|
Define a pipeline step with typed inputs and outputs.
|
|
40
42
|
|
|
41
43
|
```typescript
|
|
42
|
-
import {
|
|
44
|
+
import { step } from 'runsheet';
|
|
43
45
|
import { z } from 'zod';
|
|
44
46
|
|
|
45
47
|
// With Zod schemas (runtime + compile-time validation)
|
|
46
|
-
const fetchUser =
|
|
48
|
+
const fetchUser = step({
|
|
47
49
|
name: 'fetchUser',
|
|
48
50
|
requires: z.object({ userId: z.string() }),
|
|
49
51
|
provides: z.object({ user: z.object({ name: z.string() }) }),
|
|
@@ -54,7 +56,7 @@ const fetchUser = defineStep({
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
// With generics only (compile-time validation, no runtime schemas)
|
|
57
|
-
const logUser =
|
|
59
|
+
const logUser = step<{ user: { name: string } }, { loggedAt: Date }>({
|
|
58
60
|
name: 'logUser',
|
|
59
61
|
run: async (ctx) => {
|
|
60
62
|
console.log(ctx.user.name);
|
|
@@ -63,7 +65,7 @@ const logUser = defineStep<{ user: { name: string } }, { loggedAt: Date }>({
|
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
// With rollback
|
|
66
|
-
const chargePayment =
|
|
68
|
+
const chargePayment = step({
|
|
67
69
|
name: 'chargePayment',
|
|
68
70
|
requires: z.object({ amount: z.number() }),
|
|
69
71
|
provides: z.object({ chargeId: z.string() }),
|
|
@@ -82,7 +84,7 @@ Step run functions can be sync or async.
|
|
|
82
84
|
### Retry and timeout
|
|
83
85
|
|
|
84
86
|
```typescript
|
|
85
|
-
const callApi =
|
|
87
|
+
const callApi = step({
|
|
86
88
|
name: 'callApi',
|
|
87
89
|
provides: z.object({ data: z.string() }),
|
|
88
90
|
retry: { count: 3, delay: 200, backoff: 'exponential' },
|
|
@@ -94,7 +96,7 @@ const callApi = defineStep({
|
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
// With retryIf to only retry specific errors
|
|
97
|
-
const selective =
|
|
99
|
+
const selective = step({
|
|
98
100
|
name: 'selective',
|
|
99
101
|
retry: {
|
|
100
102
|
count: 3,
|
|
@@ -117,25 +119,25 @@ RetryPolicy type:
|
|
|
117
119
|
- backoff?: 'linear' | 'exponential' (default 'linear')
|
|
118
120
|
- retryIf?: (errors: Error[]) => boolean — return false to stop retrying
|
|
119
121
|
|
|
120
|
-
###
|
|
122
|
+
### pipeline
|
|
121
123
|
|
|
122
124
|
Build a pipeline from an array of steps. The result type is inferred from the
|
|
123
125
|
steps.
|
|
124
126
|
|
|
125
127
|
```typescript
|
|
126
|
-
import {
|
|
128
|
+
import { pipeline } from 'runsheet';
|
|
127
129
|
|
|
128
|
-
const
|
|
130
|
+
const p = pipeline({
|
|
129
131
|
name: 'checkout',
|
|
130
132
|
steps: [fetchUser, chargePayment, sendEmail],
|
|
131
133
|
});
|
|
132
134
|
|
|
133
|
-
const result = await
|
|
135
|
+
const result = await p.run({ userId: '123', amount: 50 });
|
|
134
136
|
|
|
135
137
|
if (result.success) {
|
|
136
138
|
result.data.chargeId; // string — fully typed
|
|
137
139
|
} else {
|
|
138
|
-
result.
|
|
140
|
+
result.error; // what went wrong (single Error)
|
|
139
141
|
result.rollback; // { completed: string[], failed: RollbackFailure[] }
|
|
140
142
|
}
|
|
141
143
|
```
|
|
@@ -143,7 +145,7 @@ if (result.success) {
|
|
|
143
145
|
Optional argsSchema validates pipeline input:
|
|
144
146
|
|
|
145
147
|
```typescript
|
|
146
|
-
const
|
|
148
|
+
const p = pipeline({
|
|
147
149
|
name: 'checkout',
|
|
148
150
|
steps: [fetchUser, chargePayment],
|
|
149
151
|
argsSchema: z.object({ userId: z.string(), amount: z.number() }),
|
|
@@ -154,21 +156,37 @@ Use `strict: true` to detect provides key collisions at build time. Throws a
|
|
|
154
156
|
RunsheetError with code 'STRICT_OVERLAP' if two steps provide the same key:
|
|
155
157
|
|
|
156
158
|
```typescript
|
|
157
|
-
const
|
|
159
|
+
const p = pipeline({
|
|
158
160
|
name: 'checkout',
|
|
159
161
|
steps: [fetchUser, chargePayment],
|
|
160
162
|
strict: true,
|
|
161
163
|
});
|
|
162
164
|
```
|
|
163
165
|
|
|
164
|
-
###
|
|
166
|
+
### Pipeline composition
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
Pipelines are steps — pipeline returns an AggregateStep. A pipeline can be used
|
|
169
|
+
directly as a step in another pipeline's steps array:
|
|
167
170
|
|
|
168
171
|
```typescript
|
|
169
|
-
|
|
172
|
+
const inner = pipeline({ name: 'inner', steps: [a, b] });
|
|
173
|
+
const outer = pipeline({ name: 'outer', steps: [inner, c, d] });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
If a later outer step fails, the inner pipeline's steps are rolled back.
|
|
177
|
+
|
|
178
|
+
### Builder API
|
|
170
179
|
|
|
171
|
-
|
|
180
|
+
Omit `steps` from the config to get a fluent builder with progressive type
|
|
181
|
+
narrowing.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { pipeline } from 'runsheet';
|
|
185
|
+
|
|
186
|
+
const checkout = pipeline({
|
|
187
|
+
name: 'checkout',
|
|
188
|
+
argsSchema: z.object({ userId: z.string() }),
|
|
189
|
+
})
|
|
172
190
|
.step(fetchUser)
|
|
173
191
|
.step(chargePayment)
|
|
174
192
|
.step(sendEmail)
|
|
@@ -178,7 +196,7 @@ const pipeline = createPipeline('checkout', z.object({ userId: z.string() }))
|
|
|
178
196
|
Type-only args (no runtime validation of pipeline input):
|
|
179
197
|
|
|
180
198
|
```typescript
|
|
181
|
-
const
|
|
199
|
+
const checkout = pipeline<{ userId: string }>({ name: 'checkout' })
|
|
182
200
|
.step(fetchUser)
|
|
183
201
|
.build();
|
|
184
202
|
```
|
|
@@ -186,13 +204,17 @@ const pipeline = createPipeline<{ userId: string }>('checkout')
|
|
|
186
204
|
Strict mode via the builder:
|
|
187
205
|
|
|
188
206
|
```typescript
|
|
189
|
-
|
|
207
|
+
pipeline({ name: 'checkout', strict: true })
|
|
190
208
|
.step(fetchUser)
|
|
191
209
|
.step(chargePayment)
|
|
192
210
|
.build();
|
|
193
211
|
|
|
194
212
|
// Or with a schema:
|
|
195
|
-
|
|
213
|
+
pipeline({
|
|
214
|
+
name: 'checkout',
|
|
215
|
+
argsSchema: z.object({ userId: z.string() }),
|
|
216
|
+
strict: true,
|
|
217
|
+
})
|
|
196
218
|
.step(fetchUser)
|
|
197
219
|
.step(chargePayment)
|
|
198
220
|
.build();
|
|
@@ -203,7 +225,7 @@ createPipeline('checkout', z.object({ userId: z.string() }), { strict: true })
|
|
|
203
225
|
```typescript
|
|
204
226
|
import { parallel } from 'runsheet';
|
|
205
227
|
|
|
206
|
-
const
|
|
228
|
+
const p = pipeline({
|
|
207
229
|
name: 'checkout',
|
|
208
230
|
steps: [
|
|
209
231
|
validateOrder,
|
|
@@ -224,7 +246,7 @@ their own requires/provides validation, retry, and timeout. Conditional steps
|
|
|
224
246
|
```typescript
|
|
225
247
|
import { choice } from 'runsheet';
|
|
226
248
|
|
|
227
|
-
const
|
|
249
|
+
const p = pipeline({
|
|
228
250
|
name: 'checkout',
|
|
229
251
|
steps: [
|
|
230
252
|
validateOrder,
|
|
@@ -240,84 +262,34 @@ const pipeline = buildPipeline({
|
|
|
240
262
|
|
|
241
263
|
Predicates are evaluated in order — first match wins. A bare step (without a
|
|
242
264
|
tuple) can be passed as the last argument to serve as a default — equivalent to
|
|
243
|
-
`[() => true, step]`. If no predicate matches, the step
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
### map (collection iteration)
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
import { map } from 'runsheet';
|
|
250
|
-
|
|
251
|
-
// Function form — items can be any type
|
|
252
|
-
const pipeline = buildPipeline({
|
|
253
|
-
name: 'notify',
|
|
254
|
-
steps: [
|
|
255
|
-
map(
|
|
256
|
-
'emails',
|
|
257
|
-
(ctx) => ctx.users,
|
|
258
|
-
async (user) => {
|
|
259
|
-
await sendEmail(user.email);
|
|
260
|
-
return { email: user.email, sentAt: new Date() };
|
|
261
|
-
},
|
|
262
|
-
),
|
|
263
|
-
],
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Step form — reuse existing steps, item spread into context
|
|
267
|
-
const pipeline = buildPipeline({
|
|
268
|
-
name: 'process',
|
|
269
|
-
steps: [map('results', (ctx) => ctx.items, processItem)],
|
|
270
|
-
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Items run concurrently via Promise.allSettled. Results collected into an array
|
|
274
|
-
under the given key. In step form, each item is spread into the pipeline context
|
|
275
|
-
({ ...ctx, ...item }) so the step sees both pipeline-level and per-item values.
|
|
276
|
-
On partial failure, succeeded items are rolled back (step form only).
|
|
277
|
-
|
|
278
|
-
### filter (collection filtering)
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
import { filter, map } from 'runsheet';
|
|
282
|
-
|
|
283
|
-
const pipeline = buildPipeline({
|
|
284
|
-
name: 'notify',
|
|
285
|
-
steps: [
|
|
286
|
-
filter(
|
|
287
|
-
'eligible',
|
|
288
|
-
(ctx) => ctx.users,
|
|
289
|
-
(user) => user.optedIn,
|
|
290
|
-
),
|
|
291
|
-
map('emails', (ctx) => ctx.eligible, sendEmail),
|
|
292
|
-
],
|
|
293
|
-
});
|
|
294
|
-
```
|
|
265
|
+
`[() => true, step]`. If no predicate matches, the step returns empty data and
|
|
266
|
+
is treated as a no-op (like a skipped `when()`). Only the matched branch
|
|
267
|
+
participates in rollback.
|
|
295
268
|
|
|
296
|
-
|
|
297
|
-
predicates. Items where the predicate returns true are kept; original order is
|
|
298
|
-
preserved. If any predicate throws, the step fails. No rollback (pure
|
|
299
|
-
operation).
|
|
300
|
-
|
|
301
|
-
### flatMap (collection expansion)
|
|
269
|
+
### distribute (collection distribution)
|
|
302
270
|
|
|
303
271
|
```typescript
|
|
304
|
-
import {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
272
|
+
import { distribute } from 'runsheet';
|
|
273
|
+
|
|
274
|
+
// Single collection — run sendEmail once per accountId
|
|
275
|
+
const d = distribute('emails', { accountIds: 'accountId' }, sendEmail);
|
|
276
|
+
// Requires: { orgId: string, accountIds: string[] }
|
|
277
|
+
// Provides: { emails: { emailId: string }[] }
|
|
278
|
+
|
|
279
|
+
// Cross product — run once per (accountId, regionId) pair
|
|
280
|
+
const d = distribute(
|
|
281
|
+
'reports',
|
|
282
|
+
{ accountIds: 'accountId', regionIds: 'regionId' },
|
|
283
|
+
generateReport,
|
|
284
|
+
);
|
|
285
|
+
// 2 accounts × 3 regions = 6 concurrent executions
|
|
316
286
|
```
|
|
317
287
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
288
|
+
The mapping object connects context array keys to the step's scalar input keys.
|
|
289
|
+
Non-mapped context keys pass through unchanged. Items run concurrently via
|
|
290
|
+
Promise.allSettled. Supports partial-failure rollback (succeeded items rolled
|
|
291
|
+
back) and external-failure rollback (all items rolled back when a later pipeline
|
|
292
|
+
step fails).
|
|
321
293
|
|
|
322
294
|
### Dependency injection
|
|
323
295
|
|
|
@@ -325,16 +297,16 @@ Pass dependencies as pipeline args — they're available to every step through t
|
|
|
325
297
|
accumulated context without any step needing to provides them:
|
|
326
298
|
|
|
327
299
|
```typescript
|
|
328
|
-
const
|
|
300
|
+
const placeOrder = pipeline<{
|
|
329
301
|
orderId: string;
|
|
330
302
|
stripe: Stripe;
|
|
331
303
|
db: Database;
|
|
332
|
-
}>('placeOrder')
|
|
304
|
+
}>({ name: 'placeOrder' })
|
|
333
305
|
.step(validateOrder)
|
|
334
306
|
.step(chargePayment)
|
|
335
307
|
.build();
|
|
336
308
|
|
|
337
|
-
await
|
|
309
|
+
await placeOrder.run({
|
|
338
310
|
orderId: '123',
|
|
339
311
|
stripe: stripeClient,
|
|
340
312
|
db: dbClient,
|
|
@@ -349,7 +321,7 @@ testing, swap in mocks at the call site.
|
|
|
349
321
|
```typescript
|
|
350
322
|
import { when } from 'runsheet';
|
|
351
323
|
|
|
352
|
-
const
|
|
324
|
+
const p = pipeline({
|
|
353
325
|
name: 'checkout',
|
|
354
326
|
steps: [
|
|
355
327
|
fetchUser,
|
|
@@ -376,43 +348,41 @@ const timing: StepMiddleware = (step, next) => async (ctx) => {
|
|
|
376
348
|
return result;
|
|
377
349
|
};
|
|
378
350
|
|
|
379
|
-
const
|
|
351
|
+
const p = pipeline({
|
|
380
352
|
name: 'checkout',
|
|
381
353
|
steps: [fetchUser, chargePayment],
|
|
382
354
|
middleware: [timing],
|
|
383
355
|
});
|
|
384
356
|
|
|
385
357
|
// Or with the builder:
|
|
386
|
-
|
|
358
|
+
pipeline({ name: 'checkout' })
|
|
387
359
|
.use(timing)
|
|
388
360
|
.step(fetchUser)
|
|
389
361
|
.step(chargePayment)
|
|
390
362
|
.build();
|
|
391
363
|
```
|
|
392
364
|
|
|
393
|
-
###
|
|
365
|
+
### StepResult
|
|
394
366
|
|
|
395
|
-
Every
|
|
367
|
+
Every run() returns a StepResult (never throws):
|
|
396
368
|
|
|
397
369
|
```typescript
|
|
398
370
|
// Success
|
|
399
371
|
{
|
|
400
372
|
success: true,
|
|
401
373
|
data: { /* accumulated context */ },
|
|
402
|
-
errors: [],
|
|
403
374
|
meta: {
|
|
404
|
-
|
|
375
|
+
name: 'checkout',
|
|
405
376
|
args: { userId: '123' },
|
|
406
377
|
stepsExecuted: ['fetchUser', 'chargePayment'],
|
|
407
|
-
stepsSkipped: [],
|
|
408
378
|
},
|
|
409
379
|
}
|
|
410
380
|
|
|
411
381
|
// Failure
|
|
412
382
|
{
|
|
413
383
|
success: false,
|
|
414
|
-
|
|
415
|
-
meta: {
|
|
384
|
+
error: Error,
|
|
385
|
+
meta: { name, args, stepsExecuted },
|
|
416
386
|
failedStep: 'chargePayment',
|
|
417
387
|
rollback: { completed: ['fetchUser'], failed: [] },
|
|
418
388
|
}
|
|
@@ -429,24 +399,23 @@ RunsheetError instances with a discriminated `code` property:
|
|
|
429
399
|
- PREDICATE — when() or choice() predicate threw
|
|
430
400
|
- TIMEOUT — step exceeded its timeout
|
|
431
401
|
- RETRY_EXHAUSTED — step failed after all retry attempts
|
|
432
|
-
- STRICT_OVERLAP — two steps provide the same key (strict mode
|
|
433
|
-
- CHOICE_NO_MATCH — no branch matched in a choice() step
|
|
402
|
+
- STRICT_OVERLAP — two steps provide the same key (strict mode)
|
|
434
403
|
- ROLLBACK — one or more rollback handlers failed in a combinator
|
|
435
|
-
- UNKNOWN — a non-Error value was thrown and caught by the
|
|
404
|
+
- UNKNOWN — a non-Error value was thrown and caught by the engine
|
|
436
405
|
|
|
437
406
|
Application errors (thrown from step run functions) pass through as-is.
|
|
438
407
|
|
|
439
408
|
## Key exports
|
|
440
409
|
|
|
441
|
-
Functions:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
410
|
+
Functions: step, pipeline, when, parallel, choice, distribute Classes:
|
|
411
|
+
RunsheetError, RequiresValidationError, ProvidesValidationError,
|
|
412
|
+
ArgsValidationError, PredicateError, TimeoutError, RetryExhaustedError,
|
|
413
|
+
StrictOverlapError, RollbackError, UnknownError Types: Step, TypedStep,
|
|
414
|
+
AggregateStep, StepConfig, StepContext, StepOutput, StepSchema, RetryPolicy,
|
|
415
|
+
StepResult, StepSuccess, StepFailure, StepMeta, AggregateResult,
|
|
416
|
+
AggregateSuccess, AggregateFailure, AggregateMeta, RollbackReport,
|
|
417
|
+
RollbackFailure, PipelineBuilder, PipelineConfig, StepMiddleware, StepInfo,
|
|
418
|
+
StepExecutor, RunsheetErrorCode, ExtractRequires, ExtractProvides
|
|
450
419
|
|
|
451
420
|
## Important patterns
|
|
452
421
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runsheet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Type-safe, composable business logic pipelines for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"lint:fix": "./scripts/lint.sh --fix",
|
|
36
36
|
"format": "prettier --check .",
|
|
37
37
|
"format:fix": "prettier --write .",
|
|
38
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.check.json",
|
|
39
39
|
"prepare": "husky",
|
|
40
40
|
"prepublishOnly": "pnpm run build"
|
|
41
41
|
},
|
|
@@ -81,9 +81,6 @@
|
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": ">=20"
|
|
83
83
|
},
|
|
84
|
-
"dependencies": {
|
|
85
|
-
"composable-functions": "^4.0.0"
|
|
86
|
-
},
|
|
87
84
|
"peerDependencies": {
|
|
88
85
|
"zod": "^3.22.0"
|
|
89
86
|
},
|