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