duron 0.2.2 → 0.3.0-beta.1
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/action-job.d.ts +2 -0
- package/dist/action-job.d.ts.map +1 -1
- package/dist/action-job.js +20 -1
- package/dist/action-manager.d.ts +2 -0
- package/dist/action-manager.d.ts.map +1 -1
- package/dist/action-manager.js +3 -0
- package/dist/action.d.ts +27 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +9 -0
- package/dist/adapters/adapter.d.ts +10 -2
- package/dist/adapters/adapter.d.ts.map +1 -1
- package/dist/adapters/adapter.js +59 -1
- package/dist/adapters/postgres/base.d.ts +9 -4
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +269 -19
- package/dist/adapters/postgres/schema.d.ts +249 -105
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.d.ts +249 -106
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.js +2 -2
- package/dist/adapters/postgres/schema.js +29 -1
- package/dist/adapters/schemas.d.ts +140 -7
- package/dist/adapters/schemas.d.ts.map +1 -1
- package/dist/adapters/schemas.js +52 -4
- package/dist/client.d.ts +8 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/server.d.ts +220 -16
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -8
- package/dist/step-manager.d.ts +8 -2
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +174 -15
- package/dist/telemetry/adapter.d.ts +85 -0
- package/dist/telemetry/adapter.d.ts.map +1 -0
- package/dist/telemetry/adapter.js +128 -0
- package/dist/telemetry/index.d.ts +5 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +4 -0
- package/dist/telemetry/local.d.ts +21 -0
- package/dist/telemetry/local.d.ts.map +1 -0
- package/dist/telemetry/local.js +180 -0
- package/dist/telemetry/noop.d.ts +16 -0
- package/dist/telemetry/noop.d.ts.map +1 -0
- package/dist/telemetry/noop.js +39 -0
- package/dist/telemetry/opentelemetry.d.ts +24 -0
- package/dist/telemetry/opentelemetry.d.ts.map +1 -0
- package/dist/telemetry/opentelemetry.js +202 -0
- package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +3 -0
- package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +988 -0
- package/migrations/postgres/20260118202533_wealthy_mysterio/migration.sql +24 -0
- package/migrations/postgres/20260118202533_wealthy_mysterio/snapshot.json +1362 -0
- package/package.json +6 -4
- package/src/action-job.ts +35 -0
- package/src/action-manager.ts +5 -0
- package/src/action.ts +199 -0
- package/src/adapters/adapter.ts +151 -0
- package/src/adapters/postgres/base.ts +342 -23
- package/src/adapters/postgres/schema.default.ts +2 -2
- package/src/adapters/postgres/schema.ts +49 -1
- package/src/adapters/schemas.ts +81 -5
- package/src/client.ts +78 -0
- package/src/errors.ts +45 -1
- package/src/index.ts +10 -2
- package/src/server.ts +163 -8
- package/src/step-manager.ts +293 -13
- package/src/telemetry/adapter.ts +468 -0
- package/src/telemetry/index.ts +17 -0
- package/src/telemetry/local.ts +336 -0
- package/src/telemetry/noop.ts +95 -0
- package/src/telemetry/opentelemetry.ts +310 -0
package/src/step-manager.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { z } from 'zod'
|
|
|
5
5
|
import {
|
|
6
6
|
type Action,
|
|
7
7
|
type ActionHandlerContext,
|
|
8
|
+
type StepDefinition,
|
|
9
|
+
type StepDefinitionHandlerContext,
|
|
8
10
|
type StepHandlerContext,
|
|
9
11
|
type StepOptions,
|
|
10
12
|
StepOptionsSchema,
|
|
@@ -19,7 +21,9 @@ import {
|
|
|
19
21
|
StepAlreadyExecutedError,
|
|
20
22
|
StepTimeoutError,
|
|
21
23
|
serializeError,
|
|
24
|
+
UnhandledChildStepsError,
|
|
22
25
|
} from './errors.js'
|
|
26
|
+
import type { ObserveContext, Span, TelemetryAdapter } from './telemetry/adapter.js'
|
|
23
27
|
import pRetry from './utils/p-retry.js'
|
|
24
28
|
import waitForAbort from './utils/wait-for-abort.js'
|
|
25
29
|
|
|
@@ -28,6 +32,8 @@ export interface TaskStep {
|
|
|
28
32
|
cb: (ctx: StepHandlerContext) => Promise<any>
|
|
29
33
|
options: StepOptions
|
|
30
34
|
abortSignal: AbortSignal
|
|
35
|
+
parentStepId: string | null
|
|
36
|
+
parallel: boolean
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -61,16 +67,27 @@ export class StepStore {
|
|
|
61
67
|
* @param name - The name of the step
|
|
62
68
|
* @param timeoutMs - Timeout in milliseconds for the step
|
|
63
69
|
* @param retriesLimit - Maximum number of retries for the step
|
|
70
|
+
* @param parentStepId - The ID of the parent step (null for root steps)
|
|
71
|
+
* @param parallel - Whether this step runs in parallel (independent from siblings during time travel)
|
|
64
72
|
* @returns Promise resolving to the created step ID
|
|
65
73
|
* @throws Error if step creation fails
|
|
66
74
|
*/
|
|
67
|
-
async getOrCreate(
|
|
75
|
+
async getOrCreate(
|
|
76
|
+
jobId: string,
|
|
77
|
+
name: string,
|
|
78
|
+
timeoutMs: number,
|
|
79
|
+
retriesLimit: number,
|
|
80
|
+
parentStepId: string | null = null,
|
|
81
|
+
parallel: boolean = false,
|
|
82
|
+
) {
|
|
68
83
|
try {
|
|
69
84
|
return await this.#adapter.createOrRecoverJobStep({
|
|
70
85
|
jobId,
|
|
71
86
|
name,
|
|
72
87
|
timeoutMs,
|
|
73
88
|
retriesLimit,
|
|
89
|
+
parentStepId,
|
|
90
|
+
parallel,
|
|
74
91
|
})
|
|
75
92
|
} catch (error) {
|
|
76
93
|
throw new NonRetriableError(`Failed to get or create step "${name}" for job "${jobId}"`, { cause: error })
|
|
@@ -115,6 +132,7 @@ export interface StepManagerOptions {
|
|
|
115
132
|
jobId: string
|
|
116
133
|
actionName: string
|
|
117
134
|
adapter: Adapter
|
|
135
|
+
telemetry: TelemetryAdapter
|
|
118
136
|
logger: Logger
|
|
119
137
|
concurrencyLimit: number
|
|
120
138
|
}
|
|
@@ -127,10 +145,15 @@ export class StepManager {
|
|
|
127
145
|
#jobId: string
|
|
128
146
|
#actionName: string
|
|
129
147
|
#stepStore: StepStore
|
|
148
|
+
#telemetry: TelemetryAdapter
|
|
130
149
|
#queue: fastq.queueAsPromised<TaskStep, any>
|
|
131
150
|
#logger: Logger
|
|
132
151
|
// each step name should be executed only once per action job
|
|
133
152
|
#historySteps = new Set<string>()
|
|
153
|
+
// Store step spans for nested step tracking
|
|
154
|
+
#stepSpans = new Map<string, Span>()
|
|
155
|
+
// Store the job span for creating step spans
|
|
156
|
+
#jobSpan: Span | null = null
|
|
134
157
|
|
|
135
158
|
// ============================================================================
|
|
136
159
|
// Constructor
|
|
@@ -145,16 +168,25 @@ export class StepManager {
|
|
|
145
168
|
this.#jobId = options.jobId
|
|
146
169
|
this.#actionName = options.actionName
|
|
147
170
|
this.#logger = options.logger
|
|
171
|
+
this.#telemetry = options.telemetry
|
|
148
172
|
this.#stepStore = new StepStore(options.adapter)
|
|
149
173
|
this.#queue = fastq.promise(async (task: TaskStep) => {
|
|
150
174
|
if (this.#historySteps.has(task.name)) {
|
|
151
175
|
throw new StepAlreadyExecutedError(task.name, this.#jobId, this.#actionName)
|
|
152
176
|
}
|
|
153
177
|
this.#historySteps.add(task.name)
|
|
154
|
-
return this.#executeStep(task.name, task.cb, task.options, task.abortSignal)
|
|
178
|
+
return this.#executeStep(task.name, task.cb, task.options, task.abortSignal, task.parentStepId, task.parallel)
|
|
155
179
|
}, options.concurrencyLimit)
|
|
156
180
|
}
|
|
157
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Set the job span for this step manager.
|
|
184
|
+
* Called from ActionJob after the job span is created.
|
|
185
|
+
*/
|
|
186
|
+
setJobSpan(span: Span): void {
|
|
187
|
+
this.#jobSpan = span
|
|
188
|
+
}
|
|
189
|
+
|
|
158
190
|
// ============================================================================
|
|
159
191
|
// Public API Methods
|
|
160
192
|
// ============================================================================
|
|
@@ -168,6 +200,7 @@ export class StepManager {
|
|
|
168
200
|
* @param variables - Variables available to the action
|
|
169
201
|
* @param abortSignal - Abort signal for cancelling the action
|
|
170
202
|
* @param logger - Pino child logger for this job
|
|
203
|
+
* @param observeContext - Observability context for telemetry
|
|
171
204
|
* @returns ActionHandlerContext instance
|
|
172
205
|
*/
|
|
173
206
|
createActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVariables = Record<string, unknown>>(
|
|
@@ -176,8 +209,35 @@ export class StepManager {
|
|
|
176
209
|
variables: TVariables,
|
|
177
210
|
abortSignal: AbortSignal,
|
|
178
211
|
logger: Logger,
|
|
212
|
+
observeContext: ObserveContext,
|
|
179
213
|
): ActionHandlerContext<TInput, TVariables> {
|
|
180
|
-
return new ActionContext(this, job, action, variables, abortSignal, logger)
|
|
214
|
+
return new ActionContext(this, job, action, variables, abortSignal, logger, observeContext)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create an observe context for a step.
|
|
219
|
+
*/
|
|
220
|
+
createStepObserveContext(stepId: string): ObserveContext {
|
|
221
|
+
const stepSpan = this.#stepSpans.get(stepId)
|
|
222
|
+
if (stepSpan) {
|
|
223
|
+
return this.#telemetry.createObserveContext(this.#jobId, stepId, stepSpan)
|
|
224
|
+
}
|
|
225
|
+
// Fallback to job span if step span not found
|
|
226
|
+
if (this.#jobSpan) {
|
|
227
|
+
return this.#telemetry.createObserveContext(this.#jobId, stepId, this.#jobSpan)
|
|
228
|
+
}
|
|
229
|
+
// No-op observe context
|
|
230
|
+
return {
|
|
231
|
+
recordMetric: () => {
|
|
232
|
+
// No-op
|
|
233
|
+
},
|
|
234
|
+
addSpanAttribute: () => {
|
|
235
|
+
// No-op
|
|
236
|
+
},
|
|
237
|
+
addSpanEvent: () => {
|
|
238
|
+
// No-op
|
|
239
|
+
},
|
|
240
|
+
}
|
|
181
241
|
}
|
|
182
242
|
|
|
183
243
|
/**
|
|
@@ -206,9 +266,11 @@ export class StepManager {
|
|
|
206
266
|
* @param cb - The step handler function
|
|
207
267
|
* @param options - Step options including concurrency, retry, and expire settings
|
|
208
268
|
* @param abortSignal - Abort signal for cancelling the step
|
|
269
|
+
* @param parentStepId - The ID of the parent step (null for root steps)
|
|
209
270
|
* @returns Promise resolving to the step result
|
|
210
271
|
* @throws StepTimeoutError if the step times out
|
|
211
272
|
* @throws StepCancelError if the step is cancelled
|
|
273
|
+
* @throws UnhandledChildStepsError if child steps are not awaited
|
|
212
274
|
* @throws Error if the step fails
|
|
213
275
|
*/
|
|
214
276
|
async #executeStep<TResult>(
|
|
@@ -216,6 +278,8 @@ export class StepManager {
|
|
|
216
278
|
cb: (ctx: StepHandlerContext) => Promise<TResult>,
|
|
217
279
|
options: StepOptions,
|
|
218
280
|
abortSignal: AbortSignal,
|
|
281
|
+
parentStepId: string | null,
|
|
282
|
+
parallel: boolean,
|
|
219
283
|
): Promise<TResult> {
|
|
220
284
|
const expire = options.expire
|
|
221
285
|
const retryOptions = options.retry
|
|
@@ -227,8 +291,15 @@ export class StepManager {
|
|
|
227
291
|
throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled before create step' })
|
|
228
292
|
}
|
|
229
293
|
|
|
230
|
-
// Create step record
|
|
231
|
-
const newStep = await this.#stepStore.getOrCreate(
|
|
294
|
+
// Create step record with parentStepId and parallel
|
|
295
|
+
const newStep = await this.#stepStore.getOrCreate(
|
|
296
|
+
this.#jobId,
|
|
297
|
+
name,
|
|
298
|
+
expire,
|
|
299
|
+
retryOptions.limit,
|
|
300
|
+
parentStepId,
|
|
301
|
+
parallel,
|
|
302
|
+
)
|
|
232
303
|
if (!newStep) {
|
|
233
304
|
throw new NonRetriableError(
|
|
234
305
|
`Failed to create step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`,
|
|
@@ -238,6 +309,17 @@ export class StepManager {
|
|
|
238
309
|
|
|
239
310
|
step = newStep
|
|
240
311
|
|
|
312
|
+
// Start step telemetry span
|
|
313
|
+
const parentSpan = parentStepId ? this.#stepSpans.get(parentStepId) : this.#jobSpan
|
|
314
|
+
const stepSpan = await this.#telemetry.startStepSpan({
|
|
315
|
+
jobId: this.#jobId,
|
|
316
|
+
stepId: step.id,
|
|
317
|
+
stepName: name,
|
|
318
|
+
parentSpan: parentSpan ?? undefined,
|
|
319
|
+
parentStepId,
|
|
320
|
+
})
|
|
321
|
+
this.#stepSpans.set(step.id, stepSpan)
|
|
322
|
+
|
|
241
323
|
if (abortSignal.aborted) {
|
|
242
324
|
throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled after create step' })
|
|
243
325
|
}
|
|
@@ -245,7 +327,7 @@ export class StepManager {
|
|
|
245
327
|
if (step.status === STEP_STATUS_COMPLETED) {
|
|
246
328
|
// this is how we recover a completed step
|
|
247
329
|
this.#logger.debug(
|
|
248
|
-
{ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id },
|
|
330
|
+
{ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id, parentStepId },
|
|
249
331
|
'[StepManager] Step recovered (already completed)',
|
|
250
332
|
)
|
|
251
333
|
return step.output as TResult
|
|
@@ -265,11 +347,12 @@ export class StepManager {
|
|
|
265
347
|
|
|
266
348
|
// Log step start
|
|
267
349
|
this.#logger.debug(
|
|
268
|
-
{ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id },
|
|
350
|
+
{ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id, parentStepId },
|
|
269
351
|
'[StepManager] Step started executing',
|
|
270
352
|
)
|
|
271
353
|
}
|
|
272
354
|
|
|
355
|
+
// Create abort controller for this step's timeout
|
|
273
356
|
const stepAbortController = new AbortController()
|
|
274
357
|
const timeoutId = setTimeout(() => {
|
|
275
358
|
const timeoutError = new StepTimeoutError(name, this.#jobId, expire)
|
|
@@ -278,18 +361,85 @@ export class StepManager {
|
|
|
278
361
|
|
|
279
362
|
timeoutId?.unref?.()
|
|
280
363
|
|
|
281
|
-
// Combine abort signals
|
|
282
|
-
const
|
|
364
|
+
// Combine abort signals: parent chain + this step's timeout
|
|
365
|
+
const stepSignal = AbortSignal.any([abortSignal, stepAbortController.signal])
|
|
366
|
+
|
|
367
|
+
// Track child steps for enforcement
|
|
368
|
+
interface TrackedChildStep {
|
|
369
|
+
promise: Promise<any>
|
|
370
|
+
settled: boolean
|
|
371
|
+
}
|
|
372
|
+
const childSteps: TrackedChildStep[] = []
|
|
373
|
+
|
|
374
|
+
// Create abort controller for child steps (used when parent returns with pending children)
|
|
375
|
+
const childAbortController = new AbortController()
|
|
376
|
+
const childSignal = AbortSignal.any([stepSignal, childAbortController.signal])
|
|
377
|
+
|
|
378
|
+
// Create observe context for this step
|
|
379
|
+
const stepObserveContext = this.createStepObserveContext(step.id)
|
|
380
|
+
|
|
381
|
+
// Create StepHandlerContext with nested step support
|
|
382
|
+
const stepContext: StepHandlerContext = {
|
|
383
|
+
signal: stepSignal,
|
|
384
|
+
stepId: step.id,
|
|
385
|
+
parentStepId,
|
|
386
|
+
observe: stepObserveContext,
|
|
387
|
+
step: <TChildResult>(
|
|
388
|
+
childName: string,
|
|
389
|
+
childCb: (ctx: StepHandlerContext) => Promise<TChildResult>,
|
|
390
|
+
childOptions: z.input<typeof StepOptionsSchema> = {},
|
|
391
|
+
): Promise<TChildResult> => {
|
|
392
|
+
// Inherit parent step options EXCEPT parallel (each step's parallel status is independent)
|
|
393
|
+
const { parallel: _parentParallel, ...inheritableOptions } = options
|
|
394
|
+
const parsedChildOptions = StepOptionsSchema.parse({
|
|
395
|
+
...inheritableOptions,
|
|
396
|
+
...childOptions,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Push child step with this step as parent
|
|
400
|
+
const childPromise = this.push({
|
|
401
|
+
name: childName,
|
|
402
|
+
cb: childCb,
|
|
403
|
+
options: parsedChildOptions,
|
|
404
|
+
abortSignal: childSignal, // Child uses composed signal
|
|
405
|
+
parentStepId: step!.id, // This step is the parent
|
|
406
|
+
parallel: parsedChildOptions.parallel, // Pass parallel option
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Track the child promise
|
|
410
|
+
const trackedChild: TrackedChildStep = {
|
|
411
|
+
promise: childPromise,
|
|
412
|
+
settled: false,
|
|
413
|
+
}
|
|
414
|
+
childSteps.push(trackedChild)
|
|
415
|
+
|
|
416
|
+
// Mark as settled when done (success or failure)
|
|
417
|
+
// Use .then/.catch instead of .finally to properly handle rejections
|
|
418
|
+
childPromise
|
|
419
|
+
.then(() => {
|
|
420
|
+
trackedChild.settled = true
|
|
421
|
+
})
|
|
422
|
+
.catch(() => {
|
|
423
|
+
trackedChild.settled = true
|
|
424
|
+
// Swallow the error here - it will be re-thrown to the caller via the returned promise
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
return childPromise
|
|
428
|
+
},
|
|
429
|
+
}
|
|
283
430
|
|
|
284
431
|
try {
|
|
285
432
|
// Race between abort signal and callback execution
|
|
286
|
-
const abortPromise = waitForAbort(
|
|
287
|
-
const callbackPromise = cb(
|
|
433
|
+
const abortPromise = waitForAbort(stepSignal)
|
|
434
|
+
const callbackPromise = cb(stepContext)
|
|
288
435
|
|
|
289
436
|
let result: any = null
|
|
437
|
+
let aborted = false
|
|
290
438
|
|
|
291
439
|
await Promise.race([
|
|
292
|
-
abortPromise.promise
|
|
440
|
+
abortPromise.promise.then(() => {
|
|
441
|
+
aborted = true
|
|
442
|
+
}),
|
|
293
443
|
callbackPromise
|
|
294
444
|
.then((res) => {
|
|
295
445
|
if (res !== undefined && res !== null) {
|
|
@@ -301,12 +451,54 @@ export class StepManager {
|
|
|
301
451
|
}),
|
|
302
452
|
])
|
|
303
453
|
|
|
454
|
+
// If aborted, wait for child steps to settle before propagating
|
|
455
|
+
if (aborted) {
|
|
456
|
+
// Wait for all child steps to settle (they'll be aborted via signal propagation)
|
|
457
|
+
if (childSteps.length > 0) {
|
|
458
|
+
await Promise.allSettled(childSteps.map((c) => c.promise))
|
|
459
|
+
}
|
|
460
|
+
// Re-throw the abort reason
|
|
461
|
+
throw stepSignal.reason
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// After parent callback returns, check for pending children
|
|
465
|
+
const unsettledChildren = childSteps.filter((c) => !c.settled)
|
|
466
|
+
if (unsettledChildren.length > 0) {
|
|
467
|
+
this.#logger.warn(
|
|
468
|
+
{
|
|
469
|
+
jobId: this.#jobId,
|
|
470
|
+
actionName: this.#actionName,
|
|
471
|
+
stepName: name,
|
|
472
|
+
stepId: step.id,
|
|
473
|
+
pendingCount: unsettledChildren.length,
|
|
474
|
+
},
|
|
475
|
+
'[StepManager] Parent step completed with unhandled child steps - aborting children',
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
// Abort all pending children
|
|
479
|
+
const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length)
|
|
480
|
+
childAbortController.abort(unhandledError)
|
|
481
|
+
|
|
482
|
+
// Wait for all children to settle (they'll reject with cancellation)
|
|
483
|
+
await Promise.allSettled(unsettledChildren.map((c) => c.promise))
|
|
484
|
+
|
|
485
|
+
// Now throw the error
|
|
486
|
+
throw unhandledError
|
|
487
|
+
}
|
|
488
|
+
|
|
304
489
|
// Update step as completed
|
|
305
490
|
const completed = await this.#stepStore.updateStatus(step.id, 'completed', result)
|
|
306
491
|
if (!completed) {
|
|
307
492
|
throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
|
|
308
493
|
}
|
|
309
494
|
|
|
495
|
+
// End step telemetry span successfully
|
|
496
|
+
const stepSpan = this.#stepSpans.get(step.id)
|
|
497
|
+
if (stepSpan) {
|
|
498
|
+
await this.#telemetry.endStepSpan(stepSpan, { status: 'ok' })
|
|
499
|
+
this.#stepSpans.delete(step.id)
|
|
500
|
+
}
|
|
501
|
+
|
|
310
502
|
// Log step completion
|
|
311
503
|
this.#logger.debug(
|
|
312
504
|
{ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id },
|
|
@@ -347,6 +539,17 @@ export class StepManager {
|
|
|
347
539
|
},
|
|
348
540
|
}).catch(async (error) => {
|
|
349
541
|
if (step) {
|
|
542
|
+
// End step telemetry span with error/cancelled status
|
|
543
|
+
const stepSpan = this.#stepSpans.get(step.id)
|
|
544
|
+
if (stepSpan) {
|
|
545
|
+
if (isCancelError(error)) {
|
|
546
|
+
await this.#telemetry.endStepSpan(stepSpan, { status: 'cancelled' })
|
|
547
|
+
} else {
|
|
548
|
+
await this.#telemetry.endStepSpan(stepSpan, { status: 'error', error })
|
|
549
|
+
}
|
|
550
|
+
this.#stepSpans.delete(step.id)
|
|
551
|
+
}
|
|
552
|
+
|
|
350
553
|
if (isCancelError(error)) {
|
|
351
554
|
await this.#stepStore.updateStatus(step.id, 'cancelled')
|
|
352
555
|
} else {
|
|
@@ -377,6 +580,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
377
580
|
#jobId: string
|
|
378
581
|
#groupKey: string = '@default'
|
|
379
582
|
#action: Action<TInput, TOutput, TVariables>
|
|
583
|
+
#observeContext: ObserveContext
|
|
380
584
|
|
|
381
585
|
// ============================================================================
|
|
382
586
|
// Constructor
|
|
@@ -389,6 +593,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
389
593
|
variables: TVariables,
|
|
390
594
|
abortSignal: AbortSignal,
|
|
391
595
|
logger: Logger,
|
|
596
|
+
observeContext: ObserveContext,
|
|
392
597
|
) {
|
|
393
598
|
this.#stepManager = stepManager
|
|
394
599
|
this.#variables = variables
|
|
@@ -397,6 +602,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
397
602
|
this.#action = action
|
|
398
603
|
this.#jobId = job.id
|
|
399
604
|
this.#groupKey = job.groupKey ?? '@default'
|
|
605
|
+
this.#observeContext = observeContext
|
|
400
606
|
if (action.input) {
|
|
401
607
|
this.#input = action.input.parse(job.input, {
|
|
402
608
|
error: () => 'Error parsing action input',
|
|
@@ -405,6 +611,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
405
611
|
}
|
|
406
612
|
this.#input = job.input ?? {}
|
|
407
613
|
this.step = this.step.bind(this)
|
|
614
|
+
this.run = this.run.bind(this)
|
|
408
615
|
}
|
|
409
616
|
|
|
410
617
|
// ============================================================================
|
|
@@ -450,8 +657,16 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
450
657
|
return this.#logger
|
|
451
658
|
}
|
|
452
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Get the observability context for recording metrics and span data.
|
|
662
|
+
*/
|
|
663
|
+
get observe(): ObserveContext {
|
|
664
|
+
return this.#observeContext
|
|
665
|
+
}
|
|
666
|
+
|
|
453
667
|
/**
|
|
454
668
|
* Execute a step within the action.
|
|
669
|
+
* This creates a root step (no parent).
|
|
455
670
|
*
|
|
456
671
|
* @param name - The name of the step
|
|
457
672
|
* @param cb - The step handler function
|
|
@@ -467,6 +682,71 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
467
682
|
...this.#action.steps,
|
|
468
683
|
...options,
|
|
469
684
|
})
|
|
470
|
-
return this.#stepManager.push({
|
|
685
|
+
return this.#stepManager.push({
|
|
686
|
+
name,
|
|
687
|
+
cb,
|
|
688
|
+
options: parsedOptions,
|
|
689
|
+
abortSignal: this.#abortSignal,
|
|
690
|
+
parentStepId: null, // Root steps have no parent
|
|
691
|
+
parallel: parsedOptions.parallel, // Pass parallel option
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Execute a reusable step definition created with createStep().
|
|
697
|
+
*
|
|
698
|
+
* @param stepDef - The step definition to execute
|
|
699
|
+
* @param input - The input data for the step (validated against the step's input schema)
|
|
700
|
+
* @param options - Optional step configuration overrides
|
|
701
|
+
* @returns Promise resolving to the step result
|
|
702
|
+
*/
|
|
703
|
+
async run<TStepInput extends z.ZodObject, TResult>(
|
|
704
|
+
stepDef: StepDefinition<TStepInput, TResult, TVariables>,
|
|
705
|
+
input: z.input<TStepInput>,
|
|
706
|
+
options: Partial<z.input<typeof StepOptionsSchema>> = {},
|
|
707
|
+
): Promise<TResult> {
|
|
708
|
+
// Validate input against the step's schema if provided
|
|
709
|
+
// After parsing, validatedInput is z.output<TStepInput> (same as z.infer<TStepInput>)
|
|
710
|
+
const validatedInput: z.infer<TStepInput> = stepDef.input
|
|
711
|
+
? stepDef.input.parse(input, {
|
|
712
|
+
error: () => 'Error parsing step input',
|
|
713
|
+
reportInput: true,
|
|
714
|
+
})
|
|
715
|
+
: (input as z.infer<TStepInput>)
|
|
716
|
+
|
|
717
|
+
// Resolve step name (static or dynamic)
|
|
718
|
+
const stepName = typeof stepDef.name === 'function' ? stepDef.name({ input: validatedInput }) : stepDef.name
|
|
719
|
+
|
|
720
|
+
// Merge options: action defaults -> step definition -> call-time overrides
|
|
721
|
+
const mergedOptions: z.input<typeof StepOptionsSchema> = {
|
|
722
|
+
...this.#action.steps,
|
|
723
|
+
...(stepDef.retry !== undefined && { retry: stepDef.retry }),
|
|
724
|
+
...(stepDef.expire !== undefined && { expire: stepDef.expire }),
|
|
725
|
+
...(stepDef.parallel !== undefined && { parallel: stepDef.parallel }),
|
|
726
|
+
...options,
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const parsedOptions = StepOptionsSchema.parse(mergedOptions)
|
|
730
|
+
|
|
731
|
+
// Create a wrapper callback that provides the extended context
|
|
732
|
+
const wrappedCb = async (baseCtx: StepHandlerContext): Promise<TResult> => {
|
|
733
|
+
const extendedCtx: StepDefinitionHandlerContext<TStepInput, TVariables> = {
|
|
734
|
+
...baseCtx,
|
|
735
|
+
input: validatedInput,
|
|
736
|
+
var: this.#variables,
|
|
737
|
+
logger: this.#logger,
|
|
738
|
+
jobId: this.#jobId,
|
|
739
|
+
}
|
|
740
|
+
return stepDef.handler(extendedCtx)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return this.#stepManager.push({
|
|
744
|
+
name: stepName,
|
|
745
|
+
cb: wrappedCb,
|
|
746
|
+
options: parsedOptions,
|
|
747
|
+
abortSignal: this.#abortSignal,
|
|
748
|
+
parentStepId: null, // Root steps have no parent
|
|
749
|
+
parallel: parsedOptions.parallel,
|
|
750
|
+
})
|
|
471
751
|
}
|
|
472
752
|
}
|