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/dist/index.cjs CHANGED
@@ -20,18 +20,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ArgsValidationError: () => ArgsValidationError,
24
+ ChoiceNoMatchError: () => ChoiceNoMatchError,
25
+ PredicateError: () => PredicateError,
26
+ ProvidesValidationError: () => ProvidesValidationError,
27
+ RequiresValidationError: () => RequiresValidationError,
28
+ RetryExhaustedError: () => RetryExhaustedError,
29
+ RollbackError: () => RollbackError,
23
30
  RunsheetError: () => RunsheetError,
24
- buildPipeline: () => buildPipeline,
25
- createPipeline: () => createPipeline,
31
+ StrictOverlapError: () => StrictOverlapError,
32
+ TimeoutError: () => TimeoutError,
33
+ UnknownError: () => UnknownError,
34
+ choice: () => choice,
26
35
  defineStep: () => defineStep,
36
+ filter: () => filter,
37
+ flatMap: () => flatMap,
38
+ map: () => map,
27
39
  parallel: () => parallel,
40
+ pipeline: () => pipeline,
28
41
  when: () => when
29
42
  });
30
43
  module.exports = __toCommonJS(index_exports);
31
44
 
32
- // src/define-step.ts
33
- var import_composable_functions = require("composable-functions");
34
-
35
45
  // src/errors.ts
36
46
  var RunsheetError = class extends Error {
37
47
  /** Discriminant code identifying the type of library error. */
@@ -46,19 +56,142 @@ var RunsheetError = class extends Error {
46
56
  this.code = code;
47
57
  }
48
58
  };
59
+ var RequiresValidationError = class extends RunsheetError {
60
+ constructor(message) {
61
+ super("REQUIRES_VALIDATION", message);
62
+ this.name = "RequiresValidationError";
63
+ }
64
+ };
65
+ var ProvidesValidationError = class extends RunsheetError {
66
+ constructor(message) {
67
+ super("PROVIDES_VALIDATION", message);
68
+ this.name = "ProvidesValidationError";
69
+ }
70
+ };
71
+ var ArgsValidationError = class extends RunsheetError {
72
+ constructor(message) {
73
+ super("ARGS_VALIDATION", message);
74
+ this.name = "ArgsValidationError";
75
+ }
76
+ };
77
+ var PredicateError = class extends RunsheetError {
78
+ constructor(message) {
79
+ super("PREDICATE", message);
80
+ this.name = "PredicateError";
81
+ }
82
+ };
83
+ var TimeoutError = class extends RunsheetError {
84
+ /** The timeout duration in milliseconds that was exceeded. */
85
+ timeoutMs;
86
+ constructor(message, timeoutMs) {
87
+ super("TIMEOUT", message);
88
+ this.name = "TimeoutError";
89
+ this.timeoutMs = timeoutMs;
90
+ }
91
+ };
92
+ var RetryExhaustedError = class extends RunsheetError {
93
+ /** Total number of attempts (initial + retries). */
94
+ attempts;
95
+ constructor(message, attempts) {
96
+ super("RETRY_EXHAUSTED", message);
97
+ this.name = "RetryExhaustedError";
98
+ this.attempts = attempts;
99
+ }
100
+ };
101
+ var StrictOverlapError = class extends RunsheetError {
102
+ /** The key that is provided by multiple steps. */
103
+ key;
104
+ /** The names of the two steps that both provide the key. */
105
+ steps;
106
+ constructor(message, key, steps) {
107
+ super("STRICT_OVERLAP", message);
108
+ this.name = "StrictOverlapError";
109
+ this.key = key;
110
+ this.steps = steps;
111
+ }
112
+ };
113
+ var ChoiceNoMatchError = class extends RunsheetError {
114
+ constructor(message) {
115
+ super("CHOICE_NO_MATCH", message);
116
+ this.name = "ChoiceNoMatchError";
117
+ }
118
+ };
119
+ var UnknownError = class extends RunsheetError {
120
+ /** The original thrown value before stringification. */
121
+ originalValue;
122
+ constructor(message, originalValue) {
123
+ super("UNKNOWN", message);
124
+ this.name = "UnknownError";
125
+ this.originalValue = originalValue;
126
+ }
127
+ };
128
+ var RollbackError = class extends RunsheetError {
129
+ /** The individual errors from each failed rollback handler. */
130
+ causes;
131
+ constructor(message, causes = []) {
132
+ super("ROLLBACK", message);
133
+ this.name = "RollbackError";
134
+ this.causes = causes;
135
+ this.cause = causes.length === 1 ? causes[0] : new AggregateError(causes, message);
136
+ }
137
+ };
138
+
139
+ // src/internal.ts
140
+ function toError(err) {
141
+ if (err instanceof Error) return err;
142
+ return new UnknownError(String(err), err);
143
+ }
144
+ var EMPTY_ROLLBACK = Object.freeze({
145
+ completed: Object.freeze([]),
146
+ failed: Object.freeze([])
147
+ });
148
+ function formatIssues(issues) {
149
+ return issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
150
+ }
151
+ function collapseErrors(errors, message) {
152
+ return errors.length === 1 ? errors[0] : new AggregateError(errors, message);
153
+ }
154
+ function createStepObject(fields) {
155
+ return Object.freeze({
156
+ name: fields.name,
157
+ requires: fields.requires ?? void 0,
158
+ provides: fields.provides ?? void 0,
159
+ run: fields.run,
160
+ rollback: fields.rollback ?? void 0,
161
+ retry: fields.retry ?? void 0,
162
+ timeout: fields.timeout ?? void 0
163
+ });
164
+ }
165
+ function baseMeta(name, args) {
166
+ return Object.freeze({ name, args });
167
+ }
168
+ function aggregateMeta(name, args, stepsExecuted) {
169
+ return Object.freeze({ name, args, stepsExecuted });
170
+ }
171
+ function stepSuccess(data, meta) {
172
+ return Object.freeze({ success: true, data, meta });
173
+ }
174
+ function stepFailure(error, meta, failedStep, rollback = EMPTY_ROLLBACK) {
175
+ return Object.freeze({ success: false, error, meta, failedStep, rollback });
176
+ }
177
+ function aggregateSuccess(data, meta) {
178
+ return Object.freeze({ success: true, data, meta });
179
+ }
180
+ function aggregateFailure(error, meta, failedStep, rollback = EMPTY_ROLLBACK) {
181
+ return Object.freeze({ success: false, error, meta, failedStep, rollback });
182
+ }
49
183
 
50
184
  // src/define-step.ts
51
- function withTimeout(run, stepName, ms) {
52
- return async (ctx) => {
185
+ function withTimeout(fn, stepName, ms) {
186
+ return async () => {
53
187
  let timer;
54
- const timeout = new Promise((resolve) => {
188
+ const timeout = new Promise((_, reject) => {
55
189
  timer = setTimeout(() => {
56
- const error = new RunsheetError("TIMEOUT", `${stepName} timed out after ${ms}ms`);
57
- resolve({ success: false, errors: [error] });
190
+ reject(new TimeoutError(`${stepName} timed out after ${ms}ms`, ms));
58
191
  }, ms);
59
192
  });
60
193
  try {
61
- return await Promise.race([run(ctx), timeout]);
194
+ return await Promise.race([Promise.resolve(fn()), timeout]);
62
195
  } finally {
63
196
  clearTimeout(timer);
64
197
  }
@@ -70,46 +203,111 @@ function computeDelay(policy, attempt) {
70
203
  const strategy = policy.backoff ?? "linear";
71
204
  return strategy === "exponential" ? base * 2 ** (attempt - 1) : base * attempt;
72
205
  }
73
- function withRetry(run, stepName, policy) {
74
- return async (ctx) => {
75
- let lastResult = await run(ctx);
76
- if (lastResult.success) return lastResult;
206
+ function withRetry(fn, stepName, policy) {
207
+ return async () => {
208
+ let lastError;
209
+ const errors = [];
210
+ try {
211
+ return await fn();
212
+ } catch (err) {
213
+ lastError = toError(err);
214
+ errors.push(lastError);
215
+ }
77
216
  for (let attempt = 1; attempt <= policy.count; attempt++) {
78
- if (policy.retryIf && !policy.retryIf(lastResult.errors)) return lastResult;
217
+ if (policy.retryIf && !policy.retryIf(errors)) throw lastError;
79
218
  const delay = computeDelay(policy, attempt);
80
- if (delay > 0) await new Promise((r) => setTimeout(r, delay));
81
- lastResult = await run(ctx);
82
- if (lastResult.success) return lastResult;
219
+ if (delay > 0) await new Promise((r) => setTimeout(() => r(void 0), delay));
220
+ try {
221
+ return await fn();
222
+ } catch (err) {
223
+ lastError = toError(err);
224
+ errors.push(lastError);
225
+ }
83
226
  }
84
- const error = new RunsheetError(
85
- "RETRY_EXHAUSTED",
86
- `${stepName} failed after ${policy.count} retries`
227
+ const error = new RetryExhaustedError(
228
+ `${stepName} failed after ${policy.count} retries`,
229
+ policy.count + 1
87
230
  );
88
- return { success: false, errors: [...lastResult.errors, error] };
231
+ error.cause = errors;
232
+ throw error;
89
233
  };
90
234
  }
91
- function wrapWithTimeoutAndRetry(run, stepName, timeout, retry) {
92
- let wrapped = run;
93
- if (timeout !== void 0) wrapped = withTimeout(wrapped, stepName, timeout);
94
- if (retry !== void 0) wrapped = withRetry(wrapped, stepName, retry);
95
- return wrapped;
235
+ function buildExecutor(config) {
236
+ let fn = (ctx) => Promise.resolve(config.run(ctx));
237
+ if (config.timeout !== void 0) {
238
+ const baseFn = fn;
239
+ const ms = config.timeout;
240
+ fn = (ctx) => withTimeout(() => baseFn(ctx), config.name, ms)();
241
+ }
242
+ if (config.retry !== void 0) {
243
+ const baseFn = fn;
244
+ const policy = config.retry;
245
+ fn = (ctx) => withRetry(() => baseFn(ctx), config.name, policy)();
246
+ }
247
+ return fn;
96
248
  }
97
249
  function defineStep(config) {
98
- const baseRun = (0, import_composable_functions.composable)(config.run);
99
- const wrappedRun = wrapWithTimeoutAndRetry(baseRun, config.name, config.timeout, config.retry);
100
- return Object.freeze({
250
+ const execute = buildExecutor(config);
251
+ const run = async (ctx) => {
252
+ const frozenCtx = Object.freeze({ ...ctx });
253
+ const meta = baseMeta(config.name, frozenCtx);
254
+ if (config.requires) {
255
+ const parsed = config.requires.safeParse(frozenCtx);
256
+ if (!parsed.success) {
257
+ const error = new RequiresValidationError(
258
+ `${config.name} requires: ${formatIssues(parsed.error.issues)}`
259
+ );
260
+ return stepFailure(error, meta, config.name);
261
+ }
262
+ }
263
+ let data;
264
+ try {
265
+ data = await execute(frozenCtx);
266
+ } catch (err) {
267
+ return stepFailure(toError(err), meta, config.name);
268
+ }
269
+ if (config.provides) {
270
+ const parsed = config.provides.safeParse(data);
271
+ if (!parsed.success) {
272
+ const error = new ProvidesValidationError(
273
+ `${config.name} provides: ${formatIssues(parsed.error.issues)}`
274
+ );
275
+ return stepFailure(error, meta, config.name);
276
+ }
277
+ }
278
+ return stepSuccess(data, meta);
279
+ };
280
+ return createStepObject({
101
281
  name: config.name,
102
- requires: config.requires ?? void 0,
103
- provides: config.provides ?? void 0,
104
- run: wrappedRun,
282
+ requires: config.requires,
283
+ provides: config.provides,
284
+ run,
105
285
  rollback: config.rollback ? async (ctx, output) => {
106
- await config.rollback(
107
- ctx,
108
- output
109
- );
286
+ await config.rollback(ctx, output);
110
287
  } : void 0,
111
- retry: config.retry ?? void 0,
112
- timeout: config.timeout ?? void 0
288
+ retry: config.retry,
289
+ timeout: config.timeout
290
+ });
291
+ }
292
+
293
+ // src/builder.ts
294
+ function makeBuilder(state) {
295
+ return Object.freeze({
296
+ step: (step) => makeBuilder({
297
+ ...state,
298
+ steps: [...state.steps, step]
299
+ }),
300
+ use: (...middleware) => makeBuilder({
301
+ ...state,
302
+ middleware: [...state.middleware, ...middleware]
303
+ }),
304
+ build: () => buildPipelineStep({
305
+ name: state.name,
306
+ steps: state.steps,
307
+ middleware: state.middleware.length > 0 ? state.middleware : void 0,
308
+ argsSchema: state.argsSchema,
309
+ strict: state.strict || void 0
310
+ })
113
311
  });
114
312
  }
115
313
 
@@ -120,13 +318,22 @@ function applyMiddleware(middlewares, step, executor) {
120
318
 
121
319
  // src/when.ts
122
320
  function when(predicate, step) {
321
+ const base = createStepObject({
322
+ name: step.name,
323
+ run: step.run,
324
+ rollback: step.rollback,
325
+ requires: step.requires,
326
+ provides: step.provides,
327
+ retry: step.retry,
328
+ timeout: step.timeout
329
+ });
123
330
  return Object.freeze({
124
- ...step,
331
+ ...base,
125
332
  predicate
126
333
  });
127
334
  }
128
335
  function isConditionalStep(step) {
129
- return "predicate" in step && typeof step["predicate"] === "function";
336
+ return "predicate" in step && typeof step.predicate === "function";
130
337
  }
131
338
 
132
339
  // src/pipeline.ts
@@ -139,235 +346,172 @@ function checkStrictOverlap(steps) {
139
346
  for (const key of Object.keys(shape)) {
140
347
  const existing = seen.get(key);
141
348
  if (existing) {
142
- throw new RunsheetError(
143
- "STRICT_OVERLAP",
144
- `strict mode: key "${key}" is provided by both "${existing}" and "${step.name}"`
349
+ throw new StrictOverlapError(
350
+ `strict mode: key "${key}" is provided by both "${existing}" and "${step.name}"`,
351
+ key,
352
+ [existing, step.name]
145
353
  );
146
354
  }
147
355
  seen.set(key, step.name);
148
356
  }
149
357
  }
150
358
  }
151
- function validateSchema(schema, data, label, code) {
152
- if (!schema) return { success: true, data };
153
- const parsed = schema.safeParse(data);
154
- if (parsed.success) return { success: true, data: parsed.data };
155
- const errors = parsed.error.issues.map(
156
- (issue) => new RunsheetError(code, `${label}: ${issue.path.join(".")}: ${issue.message}`)
157
- );
158
- return { success: false, errors };
359
+ function createExecutionState(args) {
360
+ return {
361
+ context: Object.freeze({ ...args }),
362
+ executed: [],
363
+ stepsExecuted: []
364
+ };
159
365
  }
160
- async function executeRollback(executedSteps, snapshots, outputs) {
366
+ async function executeRollback(executed) {
161
367
  const completed = [];
162
368
  const failed = [];
163
- for (let i = executedSteps.length - 1; i >= 0; i--) {
164
- const step = executedSteps[i];
165
- if (!step.rollback) continue;
369
+ for (let i = executed.length - 1; i >= 0; i--) {
370
+ const entry = executed[i];
371
+ if (!entry.step.rollback) continue;
166
372
  try {
167
- await step.rollback(snapshots[i], outputs[i]);
168
- completed.push(step.name);
373
+ await entry.step.rollback(entry.snapshot, entry.output);
374
+ completed.push(entry.step.name);
169
375
  } catch (err) {
170
- failed.push({
171
- step: step.name,
172
- error: err instanceof Error ? err : new Error(String(err))
173
- });
376
+ failed.push({ step: entry.step.name, error: toError(err) });
174
377
  }
175
378
  }
176
379
  return Object.freeze({ completed, failed });
177
380
  }
178
- function createExecutionState(args) {
179
- return {
180
- context: Object.freeze({ ...args }),
181
- snapshots: [],
182
- outputs: [],
183
- executedSteps: [],
184
- stepsExecuted: [],
185
- stepsSkipped: []
186
- };
187
- }
188
- function pipelineFailure(pipelineName, args, state, failedStep, errors, rollback) {
189
- return Object.freeze({
190
- success: false,
191
- errors,
192
- meta: Object.freeze({
193
- pipeline: pipelineName,
194
- args,
195
- stepsExecuted: state.stepsExecuted,
196
- stepsSkipped: state.stepsSkipped
197
- }),
198
- failedStep,
199
- rollback
200
- });
201
- }
202
- function pipelineSuccess(pipelineName, args, state) {
203
- return Object.freeze({
204
- success: true,
205
- data: state.context,
206
- errors: [],
207
- meta: Object.freeze({
208
- pipeline: pipelineName,
209
- args,
210
- stepsExecuted: state.stepsExecuted,
211
- stepsSkipped: state.stepsSkipped
212
- })
213
- });
214
- }
215
- function createStepExecutor(step) {
216
- return async (ctx) => {
217
- const requiresCheck = validateSchema(
218
- step.requires,
219
- ctx,
220
- `${step.name} requires`,
221
- "REQUIRES_VALIDATION"
222
- );
223
- if (!requiresCheck.success) {
224
- return { success: false, errors: requiresCheck.errors };
225
- }
226
- const result = await step.run(ctx);
227
- if (!result.success) return result;
228
- const providesCheck = validateSchema(
229
- step.provides,
230
- result.data,
231
- `${step.name} provides`,
232
- "PROVIDES_VALIDATION"
233
- );
234
- if (!providesCheck.success) {
235
- return { success: false, errors: providesCheck.errors };
236
- }
237
- return {
238
- success: true,
239
- data: providesCheck.data,
240
- errors: []
241
- };
242
- };
243
- }
244
381
  async function executePipeline(config, args) {
382
+ const frozenArgs = Object.freeze({ ...args });
245
383
  if (config.argsSchema) {
246
- const argsCheck = validateSchema(
247
- config.argsSchema,
248
- args,
249
- `${config.name} args`,
250
- "ARGS_VALIDATION"
251
- );
252
- if (!argsCheck.success) {
253
- const state2 = createExecutionState(args);
254
- return pipelineFailure(
255
- config.name,
256
- args,
257
- state2,
258
- config.name,
259
- argsCheck.errors,
260
- Object.freeze({ completed: [], failed: [] })
384
+ const parsed = config.argsSchema.safeParse(frozenArgs);
385
+ if (!parsed.success) {
386
+ const error = new ArgsValidationError(
387
+ `${config.name} args: ${formatIssues(parsed.error.issues)}`
261
388
  );
389
+ const meta2 = aggregateMeta(config.name, frozenArgs, []);
390
+ const state2 = createExecutionState(frozenArgs);
391
+ return { result: aggregateFailure(error, meta2, config.name), state: state2 };
262
392
  }
263
393
  }
264
- const state = createExecutionState(args);
394
+ const state = createExecutionState(frozenArgs);
265
395
  const middlewares = config.middleware ?? [];
266
396
  for (const step of config.steps) {
267
397
  try {
268
398
  if (isConditionalStep(step) && !step.predicate(state.context)) {
269
- state.stepsSkipped.push(step.name);
270
399
  continue;
271
400
  }
272
401
  } catch (err) {
273
- const message = err instanceof Error ? err.message : String(err);
274
- const error = new RunsheetError("PREDICATE", `${step.name} predicate: ${message}`);
275
- if (err instanceof Error) error.cause = err;
276
- const rollback = await executeRollback(state.executedSteps, state.snapshots, state.outputs);
277
- return pipelineFailure(config.name, args, state, step.name, [error], rollback);
402
+ const cause = toError(err);
403
+ const error = new PredicateError(`${step.name} predicate: ${cause.message}`);
404
+ error.cause = cause;
405
+ const rollback = await executeRollback(state.executed);
406
+ const meta2 = aggregateMeta(config.name, frozenArgs, [...state.stepsExecuted]);
407
+ return { result: aggregateFailure(error, meta2, step.name, rollback), state };
278
408
  }
279
- state.snapshots.push(state.context);
280
- const baseExecutor = createStepExecutor(step);
281
- const executor = applyMiddleware(
409
+ const snapshot = state.context;
410
+ const executor = middlewares.length > 0 ? applyMiddleware(
282
411
  middlewares,
283
412
  { name: step.name, requires: step.requires, provides: step.provides },
284
- baseExecutor
285
- );
413
+ (ctx) => step.run(ctx)
414
+ ) : (ctx) => step.run(ctx);
286
415
  let result;
287
416
  try {
288
417
  result = await executor(state.context);
289
418
  } catch (err) {
290
- const error = err instanceof Error ? err : new Error(String(err));
291
- state.snapshots.pop();
292
- const rollback = await executeRollback(state.executedSteps, state.snapshots, state.outputs);
293
- return pipelineFailure(config.name, args, state, step.name, [error], rollback);
419
+ const error = toError(err);
420
+ const rollback = await executeRollback(state.executed);
421
+ const meta2 = aggregateMeta(config.name, frozenArgs, [...state.stepsExecuted]);
422
+ return { result: aggregateFailure(error, meta2, step.name, rollback), state };
294
423
  }
295
424
  if (!result.success) {
296
- state.snapshots.pop();
297
- const rollback = await executeRollback(state.executedSteps, state.snapshots, state.outputs);
298
- return pipelineFailure(config.name, args, state, step.name, result.errors, rollback);
425
+ const rollback = await executeRollback(state.executed);
426
+ const meta2 = aggregateMeta(config.name, frozenArgs, [...state.stepsExecuted]);
427
+ return { result: aggregateFailure(result.error, meta2, step.name, rollback), state };
299
428
  }
300
429
  const output = result.data;
301
- state.outputs.push(output);
302
- state.executedSteps.push(step);
430
+ state.executed.push({ step, snapshot, output });
303
431
  state.stepsExecuted.push(step.name);
304
432
  state.context = Object.freeze({ ...state.context, ...output });
305
433
  }
306
- return pipelineSuccess(config.name, args, state);
434
+ const meta = aggregateMeta(config.name, frozenArgs, [...state.stepsExecuted]);
435
+ return { result: aggregateSuccess(state.context, meta), state };
307
436
  }
308
- function buildPipeline(config) {
437
+ function buildPipelineStep(config) {
309
438
  if (config.strict) checkStrictOverlap(config.steps);
310
- return Object.freeze({
439
+ const pipelineConfig = config;
440
+ const stateMap = /* @__PURE__ */ new WeakMap();
441
+ const run = async (ctx) => {
442
+ const outcome = await executePipeline(pipelineConfig, ctx);
443
+ if (outcome.result.success) {
444
+ stateMap.set(outcome.result.data, outcome.state);
445
+ }
446
+ return outcome.result;
447
+ };
448
+ const rollback = async (_ctx, output) => {
449
+ const state = stateMap.get(output);
450
+ if (state) {
451
+ stateMap.delete(output);
452
+ await executeRollback(state.executed);
453
+ }
454
+ };
455
+ return createStepObject({
311
456
  name: config.name,
312
- run: (args) => executePipeline(config, args)
457
+ requires: config.argsSchema,
458
+ run,
459
+ rollback
460
+ });
461
+ }
462
+ function pipeline(config) {
463
+ if (config.steps) {
464
+ return buildPipelineStep(
465
+ config
466
+ );
467
+ }
468
+ return makeBuilder({
469
+ name: config.name,
470
+ steps: [],
471
+ middleware: config.middleware ? [...config.middleware] : [],
472
+ argsSchema: config.argsSchema,
473
+ strict: config.strict ?? false
313
474
  });
314
475
  }
315
476
 
316
477
  // src/parallel.ts
317
- function validateInnerSchema(schema, data, label, code) {
318
- if (!schema) return null;
319
- const parsed = schema.safeParse(data);
320
- if (parsed.success) return null;
321
- return parsed.error.issues.map(
322
- (issue) => new RunsheetError(code, `${label}: ${issue.path.join(".")}: ${issue.message}`)
323
- );
324
- }
325
478
  async function executeInner(step, ctx) {
326
479
  try {
327
480
  if (isConditionalStep(step) && !step.predicate(ctx)) {
328
481
  return { step, skipped: true };
329
482
  }
330
483
  } catch (err) {
331
- const message = err instanceof Error ? err.message : String(err);
332
- const error = new RunsheetError("PREDICATE", `${step.name} predicate: ${message}`);
333
- if (err instanceof Error) error.cause = err;
334
- return { step, skipped: false, errors: [error] };
484
+ const cause = toError(err);
485
+ const error = new PredicateError(`${step.name} predicate: ${cause.message}`);
486
+ error.cause = cause;
487
+ return { step, skipped: false, error };
335
488
  }
336
- const requiresErrors = validateInnerSchema(
337
- step.requires,
338
- ctx,
339
- `${step.name} requires`,
340
- "REQUIRES_VALIDATION"
341
- );
342
- if (requiresErrors) return { step, skipped: false, errors: requiresErrors };
343
489
  const result = await step.run(ctx);
344
- if (!result.success) return { step, skipped: false, errors: [...result.errors] };
345
- const providesErrors = validateInnerSchema(
346
- step.provides,
347
- result.data,
348
- `${step.name} provides`,
349
- "PROVIDES_VALIDATION"
350
- );
351
- if (providesErrors) return { step, skipped: false, errors: providesErrors };
490
+ if (!result.success) return { step, skipped: false, error: result.error };
352
491
  return { step, skipped: false, output: result.data };
353
492
  }
354
493
  function parallel(...steps) {
355
494
  const name = `parallel(${steps.map((s) => s.name).join(", ")})`;
356
495
  const innerSteps = steps;
357
496
  const run = async (ctx) => {
358
- const settled = await Promise.allSettled(innerSteps.map((step) => executeInner(step, ctx)));
497
+ const frozenCtx = Object.freeze({ ...ctx });
498
+ const settled = await Promise.allSettled(
499
+ innerSteps.map((step) => executeInner(step, frozenCtx))
500
+ );
359
501
  const succeeded = [];
360
502
  const allErrors = [];
503
+ const executed = [];
361
504
  for (const s of settled) {
362
505
  if (s.status === "rejected") {
363
- allErrors.push(s.reason instanceof Error ? s.reason : new Error(String(s.reason)));
506
+ allErrors.push(toError(s.reason));
364
507
  } else {
365
508
  const r = s.value;
366
509
  if (r.skipped) continue;
367
510
  if (r.output) {
368
511
  succeeded.push({ step: r.step, output: r.output });
369
- } else if (r.errors) {
370
- allErrors.push(...r.errors);
512
+ executed.push(r.step.name);
513
+ } else if (r.error) {
514
+ allErrors.push(r.error);
371
515
  }
372
516
  }
373
517
  }
@@ -376,93 +520,339 @@ function parallel(...steps) {
376
520
  const { step, output } = succeeded[i];
377
521
  if (step.rollback) {
378
522
  try {
379
- await step.rollback(ctx, output);
523
+ await step.rollback(frozenCtx, output);
380
524
  } catch {
381
525
  }
382
526
  }
383
527
  }
384
- return { success: false, errors: allErrors };
528
+ const error = collapseErrors(allErrors, `${name}: ${allErrors.length} step(s) failed`);
529
+ const meta2 = aggregateMeta(name, frozenCtx, executed);
530
+ return aggregateFailure(error, meta2, name);
385
531
  }
386
532
  const merged = {};
387
533
  for (const { output } of succeeded) {
388
534
  Object.assign(merged, output);
389
535
  }
390
- return { success: true, data: merged, errors: [] };
536
+ executedMap.set(
537
+ merged,
538
+ succeeded.map((s) => ({ step: s.step, output: s.output }))
539
+ );
540
+ const meta = aggregateMeta(name, frozenCtx, executed);
541
+ return aggregateSuccess(merged, meta);
391
542
  };
543
+ const executedMap = /* @__PURE__ */ new WeakMap();
392
544
  const rollback = async (ctx, mergedOutput) => {
545
+ const entries = executedMap.get(mergedOutput);
546
+ if (!entries) return;
547
+ executedMap.delete(mergedOutput);
393
548
  const errors = [];
394
- for (let i = innerSteps.length - 1; i >= 0; i--) {
395
- const step = innerSteps[i];
549
+ for (let i = entries.length - 1; i >= 0; i--) {
550
+ const { step, output } = entries[i];
396
551
  if (!step.rollback) continue;
397
552
  try {
398
- await step.rollback(ctx, mergedOutput);
553
+ await step.rollback(ctx, output);
399
554
  } catch (err) {
400
- errors.push(err instanceof Error ? err : new Error(String(err)));
555
+ errors.push(toError(err));
401
556
  }
402
557
  }
403
558
  if (errors.length > 0) {
404
- const error = new Error(`${name}: ${errors.length} rollback(s) failed`);
405
- error.cause = errors;
406
- throw error;
559
+ throw new RollbackError(`${name}: ${errors.length} rollback(s) failed`, errors);
407
560
  }
408
561
  };
409
- return Object.freeze({
562
+ return createStepObject({
410
563
  name,
411
- requires: void 0,
412
- provides: void 0,
413
564
  run,
414
- rollback,
415
- retry: void 0,
416
- timeout: void 0
565
+ rollback
417
566
  });
418
567
  }
419
568
 
420
- // src/builder.ts
421
- function makeBuilder(state) {
422
- return Object.freeze({
423
- step: (step) => makeBuilder({
424
- ...state,
425
- steps: [...state.steps, step]
426
- }),
427
- use: (...middleware) => makeBuilder({
428
- ...state,
429
- middleware: [...state.middleware, ...middleware]
430
- }),
431
- build: () => buildPipeline({
432
- name: state.name,
433
- steps: state.steps,
434
- middleware: state.middleware.length > 0 ? state.middleware : void 0,
435
- argsSchema: state.argsSchema,
436
- strict: state.strict || void 0
437
- })
569
+ // src/choice.ts
570
+ function normalizeBranches(args) {
571
+ return args.map((arg) => {
572
+ if (Array.isArray(arg)) return arg;
573
+ return [() => true, arg];
574
+ });
575
+ }
576
+ function choice(...args) {
577
+ const innerBranches = normalizeBranches(args);
578
+ const name = `choice(${innerBranches.map(([, step]) => step.name).join(", ")})`;
579
+ const branchMap = /* @__PURE__ */ new WeakMap();
580
+ const run = async (ctx) => {
581
+ const frozenCtx = Object.freeze({ ...ctx });
582
+ for (let i = 0; i < innerBranches.length; i++) {
583
+ const [predicate, step] = innerBranches[i];
584
+ let matches;
585
+ try {
586
+ matches = predicate(frozenCtx);
587
+ } catch (err) {
588
+ const cause = toError(err);
589
+ const error = new PredicateError(`${name} predicate: ${cause.message}`);
590
+ error.cause = cause;
591
+ const meta3 = aggregateMeta(name, frozenCtx, []);
592
+ return aggregateFailure(error, meta3, name);
593
+ }
594
+ if (!matches) continue;
595
+ const result = await step.run(frozenCtx);
596
+ if (!result.success) {
597
+ const meta3 = aggregateMeta(name, frozenCtx, [step.name]);
598
+ return aggregateFailure(result.error, meta3, name);
599
+ }
600
+ branchMap.set(result.data, i);
601
+ const meta2 = aggregateMeta(name, frozenCtx, [step.name]);
602
+ return aggregateSuccess(result.data, meta2);
603
+ }
604
+ const meta = aggregateMeta(name, frozenCtx, []);
605
+ return aggregateFailure(new ChoiceNoMatchError(`${name}: no branch matched`), meta, name);
606
+ };
607
+ const rollback = async (ctx, output) => {
608
+ const branchIndex = branchMap.get(output);
609
+ if (branchIndex === void 0) return;
610
+ branchMap.delete(output);
611
+ const [, step] = innerBranches[branchIndex];
612
+ if (step.rollback) {
613
+ try {
614
+ await step.rollback(ctx, output);
615
+ } catch (err) {
616
+ throw new RollbackError(`${name}: 1 rollback(s) failed`, [toError(err)]);
617
+ }
618
+ }
619
+ };
620
+ return createStepObject({
621
+ name,
622
+ run,
623
+ rollback
624
+ });
625
+ }
626
+
627
+ // src/map.ts
628
+ function isStep(x) {
629
+ return typeof x === "object" && x !== null && "run" in x && "name" in x;
630
+ }
631
+ function map(key, collection, fnOrStep) {
632
+ const stepMode = isStep(fnOrStep);
633
+ const name = stepMode ? `map(${key}, ${fnOrStep.name})` : `map(${key})`;
634
+ const executionMap = /* @__PURE__ */ new WeakMap();
635
+ const run = async (ctx) => {
636
+ const frozenCtx = Object.freeze({ ...ctx });
637
+ const meta = baseMeta(name, frozenCtx);
638
+ let items;
639
+ try {
640
+ items = collection(frozenCtx);
641
+ } catch (err) {
642
+ return stepFailure(toError(err), meta, name);
643
+ }
644
+ if (stepMode) {
645
+ return runStepMode(fnOrStep, items, frozenCtx, name, key, executionMap, meta);
646
+ } else {
647
+ return runFunctionMode(
648
+ fnOrStep,
649
+ items,
650
+ frozenCtx,
651
+ key,
652
+ name,
653
+ meta
654
+ );
655
+ }
656
+ };
657
+ const rollback = stepMode ? async (_ctx, output) => {
658
+ const step = fnOrStep;
659
+ if (!step.rollback) return;
660
+ const exec = executionMap.get(output);
661
+ if (!exec) return;
662
+ executionMap.delete(output);
663
+ const results = output[key];
664
+ const errors = [];
665
+ for (let i = results.length - 1; i >= 0; i--) {
666
+ try {
667
+ const itemCtx = Object.freeze({ ...exec.ctx, ...exec.items[i] });
668
+ await step.rollback(itemCtx, results[i]);
669
+ } catch (err) {
670
+ errors.push(toError(err));
671
+ }
672
+ }
673
+ if (errors.length > 0) {
674
+ throw new RollbackError(`${name}: ${errors.length} rollback(s) failed`, errors);
675
+ }
676
+ } : void 0;
677
+ return createStepObject({
678
+ name,
679
+ run,
680
+ rollback
438
681
  });
439
682
  }
440
- function createPipeline(name, schemaOrOptions, options) {
441
- let argsSchema;
442
- let strict = false;
443
- if (schemaOrOptions != null) {
444
- if ("safeParse" in schemaOrOptions) {
445
- argsSchema = schemaOrOptions;
446
- strict = options?.strict ?? false;
683
+ async function runStepMode(step, items, ctx, name, key, executionMap, meta) {
684
+ const settled = await Promise.allSettled(
685
+ items.map(async (item) => {
686
+ const itemCtx = { ...ctx, ...item };
687
+ return step.run(itemCtx);
688
+ })
689
+ );
690
+ const succeeded = [];
691
+ const allErrors = [];
692
+ for (let i = 0; i < settled.length; i++) {
693
+ const s = settled[i];
694
+ if (s.status === "rejected") {
695
+ allErrors.push(toError(s.reason));
696
+ } else if (!s.value.success) {
697
+ allErrors.push(s.value.error);
447
698
  } else {
448
- strict = schemaOrOptions.strict ?? false;
699
+ succeeded.push({ index: i, output: s.value.data });
449
700
  }
450
701
  }
451
- return makeBuilder({
702
+ if (allErrors.length > 0) {
703
+ if (step.rollback) {
704
+ for (let i = succeeded.length - 1; i >= 0; i--) {
705
+ try {
706
+ const itemCtx = Object.freeze({ ...ctx, ...items[succeeded[i].index] });
707
+ await step.rollback(itemCtx, succeeded[i].output);
708
+ } catch {
709
+ }
710
+ }
711
+ }
712
+ return stepFailure(
713
+ collapseErrors(allErrors, `${name}: ${allErrors.length} item(s) failed`),
714
+ meta,
715
+ name
716
+ );
717
+ }
718
+ const results = succeeded.map((s) => s.output);
719
+ const data = { [key]: results };
720
+ executionMap.set(data, { items, ctx: { ...ctx } });
721
+ return stepSuccess(data, meta);
722
+ }
723
+ async function runFunctionMode(fn, items, ctx, key, stepName, meta) {
724
+ const settled = await Promise.allSettled(items.map(async (item) => fn(item, ctx)));
725
+ const allErrors = [];
726
+ for (const s of settled) {
727
+ if (s.status === "rejected") allErrors.push(toError(s.reason));
728
+ }
729
+ if (allErrors.length > 0) {
730
+ return stepFailure(
731
+ collapseErrors(allErrors, `${stepName}: ${allErrors.length} item(s) failed`),
732
+ meta,
733
+ stepName
734
+ );
735
+ }
736
+ const results = settled.map((s) => s.value);
737
+ const data = { [key]: results };
738
+ return stepSuccess(data, meta);
739
+ }
740
+
741
+ // src/filter.ts
742
+ function filter(key, collection, predicate) {
743
+ const name = `filter(${key})`;
744
+ const run = async (ctx) => {
745
+ const frozenCtx = Object.freeze({ ...ctx });
746
+ const meta = baseMeta(name, frozenCtx);
747
+ let items;
748
+ try {
749
+ items = collection(frozenCtx);
750
+ } catch (err) {
751
+ return stepFailure(toError(err), meta, name);
752
+ }
753
+ return runFilter(
754
+ items,
755
+ frozenCtx,
756
+ predicate,
757
+ key,
758
+ name,
759
+ meta
760
+ );
761
+ };
762
+ return createStepObject({
452
763
  name,
453
- steps: [],
454
- middleware: [],
455
- argsSchema,
456
- strict
764
+ run
765
+ });
766
+ }
767
+ async function runFilter(items, ctx, predicate, key, name, meta) {
768
+ const settled = await Promise.allSettled(items.map(async (item) => predicate(item, ctx)));
769
+ const allErrors = [];
770
+ for (const s of settled) {
771
+ if (s.status === "rejected") allErrors.push(toError(s.reason));
772
+ }
773
+ if (allErrors.length > 0) {
774
+ return stepFailure(
775
+ collapseErrors(allErrors, `${name}: ${allErrors.length} predicate(s) failed`),
776
+ meta,
777
+ name
778
+ );
779
+ }
780
+ const results = [];
781
+ for (let i = 0; i < settled.length; i++) {
782
+ if (settled[i].value) {
783
+ results.push(items[i]);
784
+ }
785
+ }
786
+ const data = { [key]: results };
787
+ return stepSuccess(data, meta);
788
+ }
789
+
790
+ // src/flat-map.ts
791
+ function flatMap(key, collection, fn) {
792
+ const name = `flatMap(${key})`;
793
+ const run = async (ctx) => {
794
+ const frozenCtx = Object.freeze({ ...ctx });
795
+ const meta = baseMeta(name, frozenCtx);
796
+ let items;
797
+ try {
798
+ items = collection(frozenCtx);
799
+ } catch (err) {
800
+ return stepFailure(toError(err), meta, name);
801
+ }
802
+ return runFlatMap(
803
+ items,
804
+ frozenCtx,
805
+ fn,
806
+ key,
807
+ name,
808
+ meta
809
+ );
810
+ };
811
+ return createStepObject({
812
+ name,
813
+ run
457
814
  });
458
815
  }
816
+ async function runFlatMap(items, ctx, fn, key, name, meta) {
817
+ const settled = await Promise.allSettled(items.map(async (item) => fn(item, ctx)));
818
+ const allErrors = [];
819
+ for (const s of settled) {
820
+ if (s.status === "rejected") allErrors.push(toError(s.reason));
821
+ }
822
+ if (allErrors.length > 0) {
823
+ return stepFailure(
824
+ collapseErrors(allErrors, `${name}: ${allErrors.length} callback(s) failed`),
825
+ meta,
826
+ name
827
+ );
828
+ }
829
+ const results = [];
830
+ for (const s of settled) {
831
+ results.push(...s.value);
832
+ }
833
+ const data = { [key]: results };
834
+ return stepSuccess(data, meta);
835
+ }
459
836
  // Annotate the CommonJS export names for ESM import in node:
460
837
  0 && (module.exports = {
838
+ ArgsValidationError,
839
+ ChoiceNoMatchError,
840
+ PredicateError,
841
+ ProvidesValidationError,
842
+ RequiresValidationError,
843
+ RetryExhaustedError,
844
+ RollbackError,
461
845
  RunsheetError,
462
- buildPipeline,
463
- createPipeline,
846
+ StrictOverlapError,
847
+ TimeoutError,
848
+ UnknownError,
849
+ choice,
464
850
  defineStep,
851
+ filter,
852
+ flatMap,
853
+ map,
465
854
  parallel,
855
+ pipeline,
466
856
  when
467
857
  });
468
858
  //# sourceMappingURL=index.cjs.map