duron 0.3.0-beta.10 → 0.3.0-beta.11

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.
Files changed (60) hide show
  1. package/dist/action-job.d.ts +31 -0
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +59 -0
  4. package/dist/action-manager.d.ts +42 -0
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +61 -0
  7. package/dist/action.d.ts +143 -0
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +131 -0
  10. package/dist/adapters/adapter.d.ts +359 -0
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +208 -0
  13. package/dist/adapters/postgres/base.d.ts +166 -0
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +268 -17
  16. package/dist/adapters/postgres/pglite.d.ts +37 -0
  17. package/dist/adapters/postgres/pglite.d.ts.map +1 -1
  18. package/dist/adapters/postgres/pglite.js +38 -0
  19. package/dist/adapters/postgres/postgres.d.ts +35 -0
  20. package/dist/adapters/postgres/postgres.d.ts.map +1 -1
  21. package/dist/adapters/postgres/postgres.js +42 -0
  22. package/dist/adapters/postgres/schema.js +11 -1
  23. package/dist/adapters/schemas.d.ts +9 -0
  24. package/dist/adapters/schemas.d.ts.map +1 -1
  25. package/dist/adapters/schemas.js +73 -1
  26. package/dist/client.d.ts +224 -0
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +311 -1
  29. package/dist/constants.js +6 -0
  30. package/dist/errors.d.ts +119 -0
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +111 -0
  33. package/dist/server.d.ts +44 -0
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +56 -0
  36. package/dist/step-manager.d.ts +83 -0
  37. package/dist/step-manager.d.ts.map +1 -1
  38. package/dist/step-manager.js +237 -5
  39. package/dist/telemetry/adapter.d.ts +322 -0
  40. package/dist/telemetry/adapter.d.ts.map +1 -1
  41. package/dist/telemetry/adapter.js +145 -0
  42. package/dist/telemetry/index.js +1 -0
  43. package/dist/telemetry/local.d.ts +48 -0
  44. package/dist/telemetry/local.d.ts.map +1 -1
  45. package/dist/telemetry/local.js +102 -0
  46. package/dist/telemetry/noop.d.ts +10 -0
  47. package/dist/telemetry/noop.d.ts.map +1 -1
  48. package/dist/telemetry/noop.js +43 -0
  49. package/dist/telemetry/opentelemetry.d.ts +23 -0
  50. package/dist/telemetry/opentelemetry.d.ts.map +1 -1
  51. package/dist/telemetry/opentelemetry.js +39 -0
  52. package/dist/utils/p-retry.d.ts +5 -0
  53. package/dist/utils/p-retry.d.ts.map +1 -1
  54. package/dist/utils/p-retry.js +8 -0
  55. package/dist/utils/wait-for-abort.d.ts +1 -0
  56. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  57. package/dist/utils/wait-for-abort.js +1 -0
  58. package/package.json +1 -1
  59. package/src/adapters/postgres/base.ts +40 -17
  60. package/src/adapters/schemas.ts +11 -1
@@ -2,20 +2,30 @@ import fastq from 'fastq';
2
2
  import { StepOptionsSchema, } from './action.js';
3
3
  import { STEP_STATUS_CANCELLED, STEP_STATUS_COMPLETED, STEP_STATUS_FAILED } from './constants.js';
4
4
  import { ActionCancelError, isCancelError, isNonRetriableError, isTimeoutError, NonRetriableError, StepAlreadyExecutedError, StepTimeoutError, serializeError, UnhandledChildStepsError, } from './errors.js';
5
+ // ============================================================================
6
+ // Noop Tracer (for fallback observe context)
7
+ // ============================================================================
5
8
  const noopTracerSpan = {
6
9
  setAttribute() {
10
+ // No-op
7
11
  },
8
12
  setAttributes() {
13
+ // No-op
9
14
  },
10
15
  addEvent() {
16
+ // No-op
11
17
  },
12
18
  recordException() {
19
+ // No-op
13
20
  },
14
21
  setStatusOk() {
22
+ // No-op
15
23
  },
16
24
  setStatusError() {
25
+ // No-op
17
26
  },
18
27
  end() {
28
+ // No-op
19
29
  },
20
30
  isRecording() {
21
31
  return false;
@@ -29,11 +39,38 @@ const createNoopTracer = (name) => ({
29
39
  });
30
40
  import pRetry from './utils/p-retry.js';
31
41
  import waitForAbort from './utils/wait-for-abort.js';
42
+ /**
43
+ * StepStore manages step records in the database.
44
+ * Provides methods to create, update, and delay steps.
45
+ */
32
46
  export class StepStore {
33
47
  #adapter;
48
+ // ============================================================================
49
+ // Constructor
50
+ // ============================================================================
51
+ /**
52
+ * Create a new StepStore instance.
53
+ *
54
+ * @param adapter - The database adapter to use for step operations
55
+ */
34
56
  constructor(adapter) {
35
57
  this.#adapter = adapter;
36
58
  }
59
+ // ============================================================================
60
+ // Public API Methods
61
+ // ============================================================================
62
+ /**
63
+ * Get or create a step record in the database.
64
+ *
65
+ * @param jobId - The ID of the job this step belongs to
66
+ * @param name - The name of the step
67
+ * @param timeoutMs - Timeout in milliseconds for the step
68
+ * @param retriesLimit - Maximum number of retries for the step
69
+ * @param parentStepId - The ID of the parent step (null for root steps)
70
+ * @param parallel - Whether this step runs in parallel (independent from siblings during time travel)
71
+ * @returns Promise resolving to the created step ID
72
+ * @throws Error if step creation fails
73
+ */
37
74
  async getOrCreate(jobId, name, timeoutMs, retriesLimit, parentStepId = null, parallel = false) {
38
75
  try {
39
76
  return await this.#adapter.createOrRecoverJobStep({
@@ -49,6 +86,15 @@ export class StepStore {
49
86
  throw new NonRetriableError(`Failed to get or create step "${name}" for job "${jobId}"`, { cause: error });
50
87
  }
51
88
  }
89
+ /**
90
+ * Update the status of a step in the database.
91
+ *
92
+ * @param stepId - The ID of the step to update
93
+ * @param status - The new status (completed, failed, or cancelled)
94
+ * @param output - Optional output data for completed steps
95
+ * @param error - Optional error data for failed steps
96
+ * @returns Promise resolving to `true` if update succeeded, `false` otherwise
97
+ */
52
98
  async updateStatus(stepId, status, output, error) {
53
99
  if (status === STEP_STATUS_COMPLETED) {
54
100
  return this.#adapter.completeJobStep({ stepId, output });
@@ -61,10 +107,23 @@ export class StepStore {
61
107
  }
62
108
  return false;
63
109
  }
110
+ /**
111
+ * Delay a step execution.
112
+ * Used when a step fails and needs to be retried after a delay.
113
+ *
114
+ * @param stepId - The ID of the step to delay
115
+ * @param delayMs - The delay in milliseconds before retrying
116
+ * @param error - The error that caused the delay
117
+ * @returns Promise resolving to `true` if delayed successfully, `false` otherwise
118
+ */
64
119
  async delay(stepId, delayMs, error) {
65
120
  return this.#adapter.delayJobStep({ stepId, delayMs, error });
66
121
  }
67
122
  }
123
+ /**
124
+ * StepManager manages steps for a single ActionJob.
125
+ * Each ActionJob has its own StepManager instance.
126
+ */
68
127
  export class StepManager {
69
128
  #jobId;
70
129
  #actionName;
@@ -72,10 +131,22 @@ export class StepManager {
72
131
  #telemetry;
73
132
  #queue;
74
133
  #logger;
134
+ // each step name should be executed only once per parent (name + parentStepId)
75
135
  #historySteps = new Set();
136
+ // Store step spans for nested step tracking
76
137
  #stepSpans = new Map();
138
+ // Store the job span for creating step spans
77
139
  #jobSpan = null;
140
+ // Factory function to create run functions with the correct parent step ID and abort signal
78
141
  #runFnFactory = null;
142
+ // ============================================================================
143
+ // Constructor
144
+ // ============================================================================
145
+ /**
146
+ * Create a new StepManager instance.
147
+ *
148
+ * @param options - Configuration options for the step manager
149
+ */
79
150
  constructor(options) {
80
151
  this.#jobId = options.jobId;
81
152
  this.#actionName = options.actionName;
@@ -83,6 +154,7 @@ export class StepManager {
83
154
  this.#telemetry = options.telemetry;
84
155
  this.#stepStore = new StepStore(options.adapter);
85
156
  this.#queue = fastq.promise(async (task) => {
157
+ // Create composite key: name + parentStepId (allows same name under different parents)
86
158
  const stepKey = `${task.parentStepId ?? 'root'}:${task.name}`;
87
159
  if (this.#historySteps.has(stepKey)) {
88
160
  throw new StepAlreadyExecutedError(task.name, this.#jobId, this.#actionName);
@@ -91,36 +163,76 @@ export class StepManager {
91
163
  return this.#executeStep(task.name, task.cb, task.options, task.abortSignal, task.parentStepId, task.parallel);
92
164
  }, options.concurrencyLimit);
93
165
  }
166
+ /**
167
+ * Set the job span for this step manager.
168
+ * Called from ActionJob after the job span is created.
169
+ */
94
170
  setJobSpan(span) {
95
171
  this.#jobSpan = span;
96
172
  }
173
+ /**
174
+ * Set the run function factory for executing step definitions from inline steps.
175
+ * Called from ActionContext after it's initialized.
176
+ *
177
+ * @param factory - A function that creates run functions with the correct parent step ID and abort signal
178
+ */
97
179
  setRunFnFactory(factory) {
98
180
  this.#runFnFactory = factory;
99
181
  }
182
+ // ============================================================================
183
+ // Public API Methods
184
+ // ============================================================================
185
+ /**
186
+ * Create an ActionContext for the action handler.
187
+ * The context provides access to input, variables, logger, and the step function.
188
+ *
189
+ * @param job - The job data including ID, input, and optional group key
190
+ * @param action - The action definition
191
+ * @param variables - Variables available to the action
192
+ * @param abortSignal - Abort signal for cancelling the action
193
+ * @param logger - Pino child logger for this job
194
+ * @param observeContext - Observability context for telemetry
195
+ * @returns ActionHandlerContext instance
196
+ */
100
197
  createActionContext(job, action, variables, abortSignal, logger, observeContext) {
101
198
  return new ActionContext(this, job, action, variables, abortSignal, logger, observeContext);
102
199
  }
200
+ /**
201
+ * Create an observe context for a step.
202
+ */
103
203
  createStepObserveContext(stepId) {
104
204
  const stepSpan = this.#stepSpans.get(stepId);
105
205
  if (stepSpan) {
106
206
  return this.#telemetry.createObserveContext(this.#jobId, stepId, stepSpan);
107
207
  }
208
+ // Fallback to job span if step span not found
108
209
  if (this.#jobSpan) {
109
210
  return this.#telemetry.createObserveContext(this.#jobId, stepId, this.#jobSpan);
110
211
  }
212
+ // No-op observe context
111
213
  return {
112
214
  recordMetric: () => {
215
+ // No-op
113
216
  },
114
217
  addSpanAttribute: () => {
218
+ // No-op
115
219
  },
116
220
  addSpanEvent: () => {
221
+ // No-op
117
222
  },
118
223
  getTracer: (name) => {
119
224
  return createNoopTracer(name);
120
225
  },
121
226
  };
122
227
  }
228
+ /**
229
+ * Queue a step task for execution.
230
+ *
231
+ * @param task - The step task to queue
232
+ * @returns Promise resolving to the step result
233
+ */
123
234
  async push(task) {
235
+ // Warn about potential starvation when child steps are queued and all slots are occupied
124
236
  if (task.parentStepId !== null && this.#queue.running() >= this.#queue.concurrency) {
125
237
  this.#logger.warn({
126
238
  jobId: this.#jobId,
@@ -134,9 +246,28 @@ export class StepManager {
134
246
  }
135
247
  return this.#queue.push(task);
136
248
  }
249
+ /**
250
+ * Clean up step queues by waiting for them to drain.
251
+ * Should be called when the job completes or is cancelled.
252
+ */
137
253
  async drain() {
138
254
  await this.#queue.drain();
139
255
  }
256
+ /**
257
+ * Execute a step with retry logic and timeout handling.
258
+ * Creates a step record, queues the execution, and handles errors appropriately.
259
+ *
260
+ * @param name - The name of the step
261
+ * @param cb - The step handler function
262
+ * @param options - Step options including concurrency, retry, and expire settings
263
+ * @param abortSignal - Abort signal for cancelling the step
264
+ * @param parentStepId - The ID of the parent step (null for root steps)
265
+ * @returns Promise resolving to the step result
266
+ * @throws StepTimeoutError if the step times out
267
+ * @throws StepCancelError if the step is cancelled
268
+ * @throws UnhandledChildStepsError if child steps are not awaited
269
+ * @throws Error if the step fails
270
+ */
140
271
  async #executeStep(name, cb, options, abortSignal, parentStepId, parallel) {
141
272
  const expire = options.expire;
142
273
  const retryOptions = options.retry;
@@ -146,11 +277,13 @@ export class StepManager {
146
277
  if (abortSignal.aborted) {
147
278
  throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled before create step' });
148
279
  }
280
+ // Create step record with parentStepId and parallel
149
281
  const newStep = await this.#stepStore.getOrCreate(this.#jobId, name, expire, retryOptions.limit, parentStepId, parallel);
150
282
  if (!newStep) {
151
283
  throw new NonRetriableError(`Failed to create step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`, { cause: 'step not created' });
152
284
  }
153
285
  step = newStep;
286
+ // Start step telemetry span
154
287
  const parentSpan = parentStepId ? this.#stepSpans.get(parentStepId) : this.#jobSpan;
155
288
  const stepSpan = await this.#telemetry.startStepSpan({
156
289
  jobId: this.#jobId,
@@ -164,6 +297,7 @@ export class StepManager {
164
297
  throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled after create step' });
165
298
  }
166
299
  if (step.status === STEP_STATUS_COMPLETED) {
300
+ // this is how we recover a completed step
167
301
  this.#logger.debug({ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id, parentStepId }, '[StepManager] Step recovered (already completed)');
168
302
  return step.output;
169
303
  }
@@ -175,8 +309,10 @@ export class StepManager {
175
309
  else if (step.status === STEP_STATUS_CANCELLED) {
176
310
  throw new NonRetriableError(`Cannot recover a cancelled step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`, { cause: step.error });
177
311
  }
312
+ // Log step start
178
313
  this.#logger.debug({ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id, parentStepId }, '[StepManager] Step started executing');
179
314
  }
315
+ // Create abort controller for this step's timeout
180
316
  const stepAbortController = new AbortController();
181
317
  const timeoutId = setTimeout(() => {
182
318
  const timeoutError = new StepTimeoutError(name, this.#jobId, expire, {
@@ -187,47 +323,59 @@ export class StepManager {
187
323
  stepAbortController.abort(timeoutError);
188
324
  }, expire);
189
325
  timeoutId?.unref?.();
326
+ // Combine abort signals: parent chain + this step's timeout
190
327
  const stepSignal = AbortSignal.any([abortSignal, stepAbortController.signal]);
191
328
  const childSteps = [];
329
+ // Create abort controller for child steps (used when parent returns with pending children)
192
330
  const childAbortController = new AbortController();
193
331
  const childSignal = AbortSignal.any([stepSignal, childAbortController.signal]);
332
+ // Create observe context for this step
194
333
  const stepObserveContext = this.createStepObserveContext(step.id);
334
+ // Create StepHandlerContext with nested step support
195
335
  const stepContext = {
196
336
  signal: stepSignal,
197
337
  stepId: step.id,
198
338
  parentStepId,
199
339
  observe: stepObserveContext,
200
340
  step: (childName, childCb, childOptions = {}) => {
341
+ // Inherit parent step options EXCEPT parallel (each step's parallel status is independent)
201
342
  const { parallel: _parentParallel, ...inheritableOptions } = options;
202
343
  const parsedChildOptions = StepOptionsSchema.parse({
203
344
  ...inheritableOptions,
204
345
  ...childOptions,
205
346
  });
347
+ // Push child step with this step as parent
206
348
  const childPromise = this.push({
207
349
  name: childName,
208
350
  cb: childCb,
209
351
  options: parsedChildOptions,
210
- abortSignal: childSignal,
211
- parentStepId: step.id,
212
- parallel: parsedChildOptions.parallel,
352
+ abortSignal: childSignal, // Child uses composed signal
353
+ parentStepId: step.id, // This step is the parent
354
+ parallel: parsedChildOptions.parallel, // Pass parallel option
213
355
  });
356
+ // Track the child promise
214
357
  const trackedChild = {
215
358
  promise: childPromise,
216
359
  settled: false,
217
360
  };
218
361
  childSteps.push(trackedChild);
362
+ // Mark as settled when done (success or failure)
363
+ // Use .then/.catch instead of .finally to properly handle rejections
219
364
  childPromise
220
365
  .then(() => {
221
366
  trackedChild.settled = true;
222
367
  })
223
368
  .catch(() => {
224
369
  trackedChild.settled = true;
370
+ // Swallow the error here - it will be re-thrown to the caller via the returned promise
371
+ // Note: sibling steps will be aborted when the error propagates to the action level
225
372
  });
226
373
  return childPromise;
227
374
  },
228
375
  run: this.#runFnFactory(step.id, childSignal),
229
376
  };
230
377
  try {
378
+ // Race between abort signal and callback execution
231
379
  const abortPromise = waitForAbort(stepSignal);
232
380
  const callbackPromise = cb(stepContext);
233
381
  let result = null;
@@ -250,19 +398,26 @@ export class StepManager {
250
398
  abortPromise.release();
251
399
  }),
252
400
  ]);
401
+ // If callback threw an error, abort children and wait for them before re-throwing
253
402
  if (callbackError) {
254
403
  if (childSteps.length > 0) {
404
+ // Abort all children with the callback error as reason
255
405
  childAbortController.abort(callbackError);
406
+ // Wait for all children to settle
256
407
  await Promise.allSettled(childSteps.map((c) => c.promise));
257
408
  }
258
409
  throw callbackError;
259
410
  }
411
+ // If aborted, wait for child steps to settle before propagating
260
412
  if (aborted) {
413
+ // Wait for all child steps to settle (they'll be aborted via signal propagation)
261
414
  if (childSteps.length > 0) {
262
415
  await Promise.allSettled(childSteps.map((c) => c.promise));
263
416
  }
417
+ // Re-throw the abort reason
264
418
  throw stepSignal.reason;
265
419
  }
420
+ // After parent callback returns, check for pending children
266
421
  const unsettledChildren = childSteps.filter((c) => !c.settled);
267
422
  if (unsettledChildren.length > 0) {
268
423
  this.#logger.warn({
@@ -272,6 +427,7 @@ export class StepManager {
272
427
  stepId: step.id,
273
428
  pendingCount: unsettledChildren.length,
274
429
  }, '[StepManager] Parent step completed with unhandled child steps - aborting children');
430
+ // Abort all pending children
275
431
  const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length, {
276
432
  stepId: step.id,
277
433
  parentStepId,
@@ -279,18 +435,23 @@ export class StepManager {
279
435
  actionName: this.#actionName,
280
436
  });
281
437
  childAbortController.abort(unhandledError);
438
+ // Wait for all children to settle (they'll reject with cancellation)
282
439
  await Promise.allSettled(unsettledChildren.map((c) => c.promise));
440
+ // Now throw the error
283
441
  throw unhandledError;
284
442
  }
443
+ // Update step as completed
285
444
  const completed = await this.#stepStore.updateStatus(step.id, 'completed', result);
286
445
  if (!completed) {
287
446
  throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`);
288
447
  }
448
+ // End step telemetry span successfully
289
449
  const stepSpan = this.#stepSpans.get(step.id);
290
450
  if (stepSpan) {
291
451
  await this.#telemetry.endStepSpan(stepSpan, { status: 'ok' });
292
452
  this.#stepSpans.delete(step.id);
293
453
  }
454
+ // Log step completion
294
455
  this.#logger.debug({ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id }, '[StepManager] Step finished executing');
295
456
  return result;
296
457
  }
@@ -298,6 +459,7 @@ export class StepManager {
298
459
  clearTimeout(timeoutId);
299
460
  }
300
461
  };
462
+ // Apply retry logic - skip retries for NonRetriableError
301
463
  return pRetry(executeStep, {
302
464
  retries: retryOptions.limit,
303
465
  factor: retryOptions.factor,
@@ -307,6 +469,7 @@ export class StepManager {
307
469
  maxTimeout: retryOptions.maxTimeout,
308
470
  onFailedAttempt: async (ctx) => {
309
471
  const error = ctx.error;
472
+ // Don't retry if error is non-retriable
310
473
  if (isNonRetriableError(error) ||
311
474
  (error.cause && isNonRetriableError(error.cause)) ||
312
475
  (error instanceof Error && error.name === 'AbortError')) {
@@ -351,6 +514,7 @@ export class StepManager {
351
514
  },
352
515
  }).catch(async (error) => {
353
516
  if (step) {
517
+ // End step telemetry span with error/cancelled status
354
518
  const stepSpan = this.#stepSpans.get(step.id);
355
519
  if (stepSpan) {
356
520
  if (isCancelError(error)) {
@@ -371,6 +535,11 @@ export class StepManager {
371
535
  throw error;
372
536
  });
373
537
  }
538
+ /**
539
+ * Clear the history of nested steps for a given step.
540
+ * We do't need to clear the history for the root step because it's not a parent step, it's the action itself.
541
+ * @param stepId - The ID of the step to clear the history for
542
+ */
374
543
  #clearHistoryForStep(stepId) {
375
544
  this.#historySteps.forEach((stepKey) => {
376
545
  if (stepKey.startsWith(stepId)) {
@@ -379,6 +548,13 @@ export class StepManager {
379
548
  });
380
549
  }
381
550
  }
551
+ // ============================================================================
552
+ // ActionContext Class
553
+ // ============================================================================
554
+ /**
555
+ * ActionContext provides the context for action handlers.
556
+ * It implements ActionHandlerContext and provides access to input, variables, logger, and the step function.
557
+ */
382
558
  class ActionContext {
383
559
  #stepManager;
384
560
  #variables;
@@ -389,6 +565,9 @@ class ActionContext {
389
565
  #groupKey = '@default';
390
566
  #action;
391
567
  #observeContext;
568
+ // ============================================================================
569
+ // Constructor
570
+ // ============================================================================
392
571
  constructor(stepManager, job, action, variables, abortSignal, logger, observeContext) {
393
572
  this.#stepManager = stepManager;
394
573
  this.#variables = variables;
@@ -407,28 +586,63 @@ class ActionContext {
407
586
  this.#input = job.input ?? {};
408
587
  this.step = this.step.bind(this);
409
588
  this.run = this.run.bind(this);
589
+ // Set the run function factory so inline steps can call step definitions with correct parent
410
590
  this.#stepManager.setRunFnFactory((parentStepId, abortSignal) => {
411
591
  return (stepDef, input, options) => this.#runInternal(stepDef, input, options, parentStepId, abortSignal);
412
592
  });
413
593
  }
594
+ // ============================================================================
595
+ // Public API Methods
596
+ // ============================================================================
597
+ /**
598
+ * Get the input data for this action.
599
+ */
414
600
  get input() {
415
601
  return this.#input;
416
602
  }
603
+ /**
604
+ * Get the job ID for this action context.
605
+ *
606
+ * @returns The job ID
607
+ */
417
608
  get jobId() {
418
609
  return this.#jobId;
419
610
  }
611
+ /**
612
+ * Get the group key for this action context.
613
+ *
614
+ * @returns The group key
615
+ */
420
616
  get groupKey() {
421
617
  return this.#groupKey;
422
618
  }
619
+ /**
620
+ * Get the variables available to this action.
621
+ */
423
622
  get var() {
424
623
  return this.#variables;
425
624
  }
625
+ /**
626
+ * Get the logger for this action job.
627
+ */
426
628
  get logger() {
427
629
  return this.#logger;
428
630
  }
631
+ /**
632
+ * Get the observability context for recording metrics and span data.
633
+ */
429
634
  get observe() {
430
635
  return this.#observeContext;
431
636
  }
637
+ /**
638
+ * Execute a step within the action.
639
+ * This creates a root step (no parent).
640
+ *
641
+ * @param name - The name of the step
642
+ * @param cb - The step handler function
643
+ * @param options - Optional step options (will be merged with defaults)
644
+ * @returns Promise resolving to the step result
645
+ */
432
646
  async step(name, cb, options = {}) {
433
647
  const parsedOptions = StepOptionsSchema.parse({
434
648
  ...this.#action.steps,
@@ -439,21 +653,38 @@ class ActionContext {
439
653
  cb,
440
654
  options: parsedOptions,
441
655
  abortSignal: this.#abortSignal,
442
- parentStepId: null,
443
- parallel: parsedOptions.parallel,
656
+ parentStepId: null, // Root steps have no parent
657
+ parallel: parsedOptions.parallel, // Pass parallel option
444
658
  });
445
659
  }
660
+ /**
661
+ * Execute a reusable step definition created with createStep().
662
+ * This is the public method called from action handlers.
663
+ *
664
+ * @param stepDef - The step definition to execute
665
+ * @param input - The input data for the step (validated against the step's input schema)
666
+ * @param options - Optional step configuration overrides
667
+ * @returns Promise resolving to the step result
668
+ */
446
669
  async run(stepDef, input, options = {}) {
447
670
  return this.#runInternal(stepDef, input, options, null, this.#abortSignal);
448
671
  }
672
+ /**
673
+ * Internal method to execute a step definition with explicit parent step ID and abort signal.
674
+ * Used by both the public run method and the run functions passed to step contexts.
675
+ */
449
676
  async #runInternal(stepDef, input, options = {}, parentStepId, abortSignal) {
677
+ // Validate input against the step's schema if provided
678
+ // After parsing, validatedInput is z.output<TStepInput> (same as z.infer<TStepInput>)
450
679
  const validatedInput = stepDef.input
451
680
  ? stepDef.input.parse(input, {
452
681
  error: () => 'Error parsing step input',
453
682
  reportInput: true,
454
683
  })
455
684
  : input;
685
+ // Resolve step name (static or dynamic)
456
686
  const stepName = typeof stepDef.name === 'function' ? stepDef.name({ input: validatedInput }) : stepDef.name;
687
+ // Merge options: action defaults -> step definition -> call-time overrides
457
688
  const mergedOptions = {
458
689
  ...this.#action.steps,
459
690
  ...(stepDef.retry !== undefined && { retry: stepDef.retry }),
@@ -462,6 +693,7 @@ class ActionContext {
462
693
  ...options,
463
694
  };
464
695
  const parsedOptions = StepOptionsSchema.parse(mergedOptions);
696
+ // Create a wrapper callback that provides the extended context
465
697
  const wrappedCb = async (baseCtx) => {
466
698
  const extendedCtx = {
467
699
  ...baseCtx,