duron 0.3.0-beta.5 → 0.3.0-beta.7
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.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.js +3 -1
- package/dist/client.d.ts +25 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +102 -2
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +28 -0
- package/dist/telemetry/adapter.d.ts +22 -0
- package/dist/telemetry/adapter.d.ts.map +1 -1
- package/dist/telemetry/adapter.js +6 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/local.d.ts +2 -1
- package/dist/telemetry/local.d.ts.map +1 -1
- package/dist/telemetry/local.js +63 -0
- package/dist/telemetry/noop.d.ts +2 -1
- package/dist/telemetry/noop.d.ts.map +1 -1
- package/dist/telemetry/noop.js +27 -0
- package/dist/telemetry/opentelemetry.d.ts +2 -1
- package/dist/telemetry/opentelemetry.d.ts.map +1 -1
- package/dist/telemetry/opentelemetry.js +110 -0
- package/migrations/postgres/20260119153838_flimsy_thor_girl/snapshot.json +12 -36
- package/package.json +1 -1
- package/src/action.ts +5 -11
- package/src/adapters/postgres/schema.ts +3 -1
- package/src/client.ts +187 -8
- package/src/step-manager.ts +43 -1
- package/src/telemetry/adapter.ts +174 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/local.ts +93 -0
- package/src/telemetry/noop.ts +46 -0
- package/src/telemetry/opentelemetry.ts +145 -2
|
@@ -5,6 +5,7 @@ export class OpenTelemetryAdapter extends TelemetryAdapter {
|
|
|
5
5
|
#traceDatabaseQueries;
|
|
6
6
|
#tracer = null;
|
|
7
7
|
#spanMap = new Map();
|
|
8
|
+
#tracerCache = new Map();
|
|
8
9
|
constructor(options = {}) {
|
|
9
10
|
super();
|
|
10
11
|
this.#serviceName = options.serviceName ?? 'duron';
|
|
@@ -198,5 +199,114 @@ export class OpenTelemetryAdapter extends TelemetryAdapter {
|
|
|
198
199
|
extSpan.otelSpan.setAttribute(options.key, options.value);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
202
|
+
_getTracer(name) {
|
|
203
|
+
const cached = this.#tracerCache.get(name);
|
|
204
|
+
if (cached) {
|
|
205
|
+
return cached;
|
|
206
|
+
}
|
|
207
|
+
const adapter = this;
|
|
208
|
+
const tracer = {
|
|
209
|
+
name,
|
|
210
|
+
startSpan(spanName, options) {
|
|
211
|
+
let otelSpan = null;
|
|
212
|
+
let api = null;
|
|
213
|
+
let ended = false;
|
|
214
|
+
const initPromise = (async () => {
|
|
215
|
+
api = await import('@opentelemetry/api');
|
|
216
|
+
let otelTracer;
|
|
217
|
+
if (adapter.#tracerProvider) {
|
|
218
|
+
otelTracer = adapter.#tracerProvider.getTracer(name);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
otelTracer = api.trace.getTracer(name);
|
|
222
|
+
}
|
|
223
|
+
let spanKind = api.SpanKind.INTERNAL;
|
|
224
|
+
if (options?.kind === 'client')
|
|
225
|
+
spanKind = api.SpanKind.CLIENT;
|
|
226
|
+
else if (options?.kind === 'server')
|
|
227
|
+
spanKind = api.SpanKind.SERVER;
|
|
228
|
+
else if (options?.kind === 'producer')
|
|
229
|
+
spanKind = api.SpanKind.PRODUCER;
|
|
230
|
+
else if (options?.kind === 'consumer')
|
|
231
|
+
spanKind = api.SpanKind.CONSUMER;
|
|
232
|
+
let parentContext = api.context.active();
|
|
233
|
+
if (options?.parentSpan) {
|
|
234
|
+
const parentOtelSpan = options.parentSpan._otelSpan;
|
|
235
|
+
if (parentOtelSpan) {
|
|
236
|
+
parentContext = api.trace.setSpan(api.context.active(), parentOtelSpan);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
otelSpan = otelTracer.startSpan(spanName, {
|
|
240
|
+
kind: spanKind,
|
|
241
|
+
attributes: options?.attributes,
|
|
242
|
+
}, parentContext);
|
|
243
|
+
})();
|
|
244
|
+
const tracerSpan = {
|
|
245
|
+
setAttribute(key, value) {
|
|
246
|
+
initPromise.then(() => {
|
|
247
|
+
if (otelSpan && !ended) {
|
|
248
|
+
otelSpan.setAttribute(key, value);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
setAttributes(attributes) {
|
|
253
|
+
initPromise.then(() => {
|
|
254
|
+
if (otelSpan && !ended) {
|
|
255
|
+
otelSpan.setAttributes(attributes);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
addEvent(eventName, attributes) {
|
|
260
|
+
initPromise.then(() => {
|
|
261
|
+
if (otelSpan && !ended) {
|
|
262
|
+
otelSpan.addEvent(eventName, attributes);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
recordException(error) {
|
|
267
|
+
initPromise.then(() => {
|
|
268
|
+
if (otelSpan && !ended) {
|
|
269
|
+
otelSpan.recordException(error);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
setStatusOk() {
|
|
274
|
+
initPromise.then(() => {
|
|
275
|
+
if (otelSpan && api && !ended) {
|
|
276
|
+
otelSpan.setStatus({ code: api.SpanStatusCode.OK });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
setStatusError(message) {
|
|
281
|
+
initPromise.then(() => {
|
|
282
|
+
if (otelSpan && api && !ended) {
|
|
283
|
+
otelSpan.setStatus({ code: api.SpanStatusCode.ERROR, message });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
end() {
|
|
288
|
+
initPromise.then(() => {
|
|
289
|
+
if (otelSpan && !ended) {
|
|
290
|
+
ended = true;
|
|
291
|
+
otelSpan.end();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
isRecording() {
|
|
296
|
+
return otelSpan?.isRecording() ?? false;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
initPromise.then(() => {
|
|
300
|
+
if (otelSpan) {
|
|
301
|
+
;
|
|
302
|
+
tracerSpan._otelSpan = otelSpan;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return tracerSpan;
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
this.#tracerCache.set(name, tracer);
|
|
309
|
+
return tracer;
|
|
310
|
+
}
|
|
201
311
|
}
|
|
202
312
|
export const openTelemetryAdapter = (options) => new OpenTelemetryAdapter(options);
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
"version": "8",
|
|
3
3
|
"dialect": "postgres",
|
|
4
4
|
"id": "4e93c0bf-b135-40b4-b130-0cf0968bffe3",
|
|
5
|
-
"prevIds": [
|
|
6
|
-
"00000000-0000-0000-0000-000000000000"
|
|
7
|
-
],
|
|
5
|
+
"prevIds": ["00000000-0000-0000-0000-000000000000"],
|
|
8
6
|
"ddl": [
|
|
9
7
|
{
|
|
10
8
|
"name": "duron",
|
|
@@ -1268,14 +1266,10 @@
|
|
|
1268
1266
|
},
|
|
1269
1267
|
{
|
|
1270
1268
|
"nameExplicit": false,
|
|
1271
|
-
"columns": [
|
|
1272
|
-
"job_id"
|
|
1273
|
-
],
|
|
1269
|
+
"columns": ["job_id"],
|
|
1274
1270
|
"schemaTo": "duron",
|
|
1275
1271
|
"tableTo": "jobs",
|
|
1276
|
-
"columnsTo": [
|
|
1277
|
-
"id"
|
|
1278
|
-
],
|
|
1272
|
+
"columnsTo": ["id"],
|
|
1279
1273
|
"onUpdate": "NO ACTION",
|
|
1280
1274
|
"onDelete": "CASCADE",
|
|
1281
1275
|
"name": "job_steps_job_id_jobs_id_fkey",
|
|
@@ -1285,14 +1279,10 @@
|
|
|
1285
1279
|
},
|
|
1286
1280
|
{
|
|
1287
1281
|
"nameExplicit": false,
|
|
1288
|
-
"columns": [
|
|
1289
|
-
"job_id"
|
|
1290
|
-
],
|
|
1282
|
+
"columns": ["job_id"],
|
|
1291
1283
|
"schemaTo": "duron",
|
|
1292
1284
|
"tableTo": "jobs",
|
|
1293
|
-
"columnsTo": [
|
|
1294
|
-
"id"
|
|
1295
|
-
],
|
|
1285
|
+
"columnsTo": ["id"],
|
|
1296
1286
|
"onUpdate": "NO ACTION",
|
|
1297
1287
|
"onDelete": "CASCADE",
|
|
1298
1288
|
"name": "metrics_job_id_jobs_id_fkey",
|
|
@@ -1302,14 +1292,10 @@
|
|
|
1302
1292
|
},
|
|
1303
1293
|
{
|
|
1304
1294
|
"nameExplicit": false,
|
|
1305
|
-
"columns": [
|
|
1306
|
-
"step_id"
|
|
1307
|
-
],
|
|
1295
|
+
"columns": ["step_id"],
|
|
1308
1296
|
"schemaTo": "duron",
|
|
1309
1297
|
"tableTo": "job_steps",
|
|
1310
|
-
"columnsTo": [
|
|
1311
|
-
"id"
|
|
1312
|
-
],
|
|
1298
|
+
"columnsTo": ["id"],
|
|
1313
1299
|
"onUpdate": "NO ACTION",
|
|
1314
1300
|
"onDelete": "CASCADE",
|
|
1315
1301
|
"name": "metrics_step_id_job_steps_id_fkey",
|
|
@@ -1318,9 +1304,7 @@
|
|
|
1318
1304
|
"table": "metrics"
|
|
1319
1305
|
},
|
|
1320
1306
|
{
|
|
1321
|
-
"columns": [
|
|
1322
|
-
"id"
|
|
1323
|
-
],
|
|
1307
|
+
"columns": ["id"],
|
|
1324
1308
|
"nameExplicit": false,
|
|
1325
1309
|
"name": "job_steps_pkey",
|
|
1326
1310
|
"schema": "duron",
|
|
@@ -1328,9 +1312,7 @@
|
|
|
1328
1312
|
"entityType": "pks"
|
|
1329
1313
|
},
|
|
1330
1314
|
{
|
|
1331
|
-
"columns": [
|
|
1332
|
-
"id"
|
|
1333
|
-
],
|
|
1315
|
+
"columns": ["id"],
|
|
1334
1316
|
"nameExplicit": false,
|
|
1335
1317
|
"name": "jobs_pkey",
|
|
1336
1318
|
"schema": "duron",
|
|
@@ -1338,9 +1320,7 @@
|
|
|
1338
1320
|
"entityType": "pks"
|
|
1339
1321
|
},
|
|
1340
1322
|
{
|
|
1341
|
-
"columns": [
|
|
1342
|
-
"id"
|
|
1343
|
-
],
|
|
1323
|
+
"columns": ["id"],
|
|
1344
1324
|
"nameExplicit": false,
|
|
1345
1325
|
"name": "metrics_pkey",
|
|
1346
1326
|
"schema": "duron",
|
|
@@ -1349,11 +1329,7 @@
|
|
|
1349
1329
|
},
|
|
1350
1330
|
{
|
|
1351
1331
|
"nameExplicit": true,
|
|
1352
|
-
"columns": [
|
|
1353
|
-
"job_id",
|
|
1354
|
-
"name",
|
|
1355
|
-
"parent_step_id"
|
|
1356
|
-
],
|
|
1332
|
+
"columns": ["job_id", "name", "parent_step_id"],
|
|
1357
1333
|
"nullsNotDistinct": true,
|
|
1358
1334
|
"name": "unique_job_step_name_parent",
|
|
1359
1335
|
"entityType": "uniques",
|
|
@@ -1383,4 +1359,4 @@
|
|
|
1383
1359
|
}
|
|
1384
1360
|
],
|
|
1385
1361
|
"renames": []
|
|
1386
|
-
}
|
|
1362
|
+
}
|
package/package.json
CHANGED
package/src/action.ts
CHANGED
|
@@ -133,18 +133,13 @@ export interface StepDefinitionHandlerContext<TInput extends z.ZodObject, TVaria
|
|
|
133
133
|
* The job ID this step belongs to.
|
|
134
134
|
*/
|
|
135
135
|
jobId: string
|
|
136
|
-
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
/**
|
|
140
139
|
* A reusable step definition created with createStep().
|
|
141
140
|
* Can be executed within an action handler using ctx.run().
|
|
142
141
|
*/
|
|
143
|
-
export interface StepDefinition<
|
|
144
|
-
TInput extends z.ZodObject,
|
|
145
|
-
TResult,
|
|
146
|
-
TVariables = Record<string, unknown>,
|
|
147
|
-
> {
|
|
142
|
+
export interface StepDefinition<TInput extends z.ZodObject, TResult, TVariables = Record<string, unknown>> {
|
|
148
143
|
/**
|
|
149
144
|
* The name of the step.
|
|
150
145
|
* Can be a static string or a function that generates the name from the input.
|
|
@@ -417,11 +412,10 @@ export const defineAction = <TVariables = Record<string, unknown>>() => {
|
|
|
417
412
|
/**
|
|
418
413
|
* Input type for createStep() - the definition object before transformation.
|
|
419
414
|
*/
|
|
420
|
-
export type StepDefinitionInput<
|
|
421
|
-
TInput
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
> = Omit<StepDefinition<TInput, TResult, TVariables>, '__stepDefinition'>
|
|
415
|
+
export type StepDefinitionInput<TInput extends z.ZodObject, TResult, TVariables = Record<string, unknown>> = Omit<
|
|
416
|
+
StepDefinition<TInput, TResult, TVariables>,
|
|
417
|
+
'__stepDefinition'
|
|
418
|
+
>
|
|
425
419
|
|
|
426
420
|
/**
|
|
427
421
|
* Creates a reusable step definition that can be executed within action handlers.
|
|
@@ -119,7 +119,9 @@ export default function createSchema(schemaName: string) {
|
|
|
119
119
|
index('idx_job_steps_output_fts').using('gin', sql`to_tsvector('english', ${table.output}::text)`),
|
|
120
120
|
// Unique constraint - step name is unique within a parent (name + parentStepId)
|
|
121
121
|
// nullsNotDistinct ensures NULL parent_step_id values are treated as equal for uniqueness
|
|
122
|
-
unique('unique_job_step_name_parent')
|
|
122
|
+
unique('unique_job_step_name_parent')
|
|
123
|
+
.on(table.job_id, table.name, table.parent_step_id)
|
|
124
|
+
.nullsNotDistinct(),
|
|
123
125
|
check(
|
|
124
126
|
'job_steps_status_check',
|
|
125
127
|
sql`${table.status} IN ${sql.raw(`(${STEP_STATUSES.map((s) => `'${s}'`).join(',')})`)}`,
|
package/src/client.ts
CHANGED
|
@@ -20,6 +20,38 @@ import type { JobStatusResult, JobStepStatusResult } from './adapters/schemas.js
|
|
|
20
20
|
import { JOB_STATUS_CANCELLED, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, type JobStatus } from './constants.js'
|
|
21
21
|
import { LocalTelemetryAdapter, noopTelemetryAdapter, type TelemetryAdapter } from './telemetry/index.js'
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Extracts the inferred type from an action's input/output schema.
|
|
25
|
+
* Handles the case where the schema might be undefined.
|
|
26
|
+
*/
|
|
27
|
+
type InferActionSchema<T> = T extends z.ZodTypeAny ? z.infer<T> : Record<string, unknown>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result returned from waitForJob with untyped input and output.
|
|
31
|
+
*/
|
|
32
|
+
export interface JobResult {
|
|
33
|
+
jobId: string
|
|
34
|
+
actionName: string
|
|
35
|
+
status: JobStatus
|
|
36
|
+
groupKey: string
|
|
37
|
+
input: unknown
|
|
38
|
+
output: unknown
|
|
39
|
+
error: Job['error']
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Result returned from runActionAndWait with typed input and output based on the action's Zod schemas.
|
|
44
|
+
*/
|
|
45
|
+
export interface TypedJobResult<TAction extends Action<any, any, any>> {
|
|
46
|
+
jobId: string
|
|
47
|
+
actionName: string
|
|
48
|
+
status: JobStatus
|
|
49
|
+
groupKey: string
|
|
50
|
+
input: InferActionSchema<NonNullable<TAction['input']>>
|
|
51
|
+
output: InferActionSchema<NonNullable<TAction['output']>>
|
|
52
|
+
error: Job['error']
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
const BaseOptionsSchema = z.object({
|
|
24
56
|
/**
|
|
25
57
|
* Unique identifier for this Duron instance.
|
|
@@ -184,7 +216,7 @@ export class Client<
|
|
|
184
216
|
#pendingJobWaits = new Map<
|
|
185
217
|
string,
|
|
186
218
|
Set<{
|
|
187
|
-
resolve: (
|
|
219
|
+
resolve: (result: JobResult | null) => void
|
|
188
220
|
timeoutId?: NodeJS.Timeout
|
|
189
221
|
signal?: AbortSignal
|
|
190
222
|
abortHandler?: () => void
|
|
@@ -335,6 +367,132 @@ export class Client<
|
|
|
335
367
|
return jobId
|
|
336
368
|
}
|
|
337
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Run an action and wait for its completion.
|
|
372
|
+
* This is a convenience method that combines `runAction` and `waitForJob`.
|
|
373
|
+
*
|
|
374
|
+
* @param actionName - Name of the action to run
|
|
375
|
+
* @param input - Input data for the action (validated against action's input schema if provided)
|
|
376
|
+
* @param options - Options including abort signal and timeout
|
|
377
|
+
* @returns Promise resolving to the job result with typed input and output
|
|
378
|
+
* @throws Error if action is not found, job creation fails, job is cancelled, or operation is aborted
|
|
379
|
+
*/
|
|
380
|
+
async runActionAndWait<TActionName extends keyof TActions>(
|
|
381
|
+
actionName: TActionName,
|
|
382
|
+
input?: NonNullable<TActions[TActionName]['input']> extends z.ZodObject
|
|
383
|
+
? z.input<NonNullable<TActions[TActionName]['input']>>
|
|
384
|
+
: never,
|
|
385
|
+
options?: {
|
|
386
|
+
/**
|
|
387
|
+
* AbortSignal to cancel the operation. If aborted, the job will be cancelled and the promise will reject.
|
|
388
|
+
*/
|
|
389
|
+
signal?: AbortSignal
|
|
390
|
+
/**
|
|
391
|
+
* Timeout in milliseconds. If the job doesn't complete within this time, the job will be cancelled and the promise will reject.
|
|
392
|
+
*/
|
|
393
|
+
timeout?: number
|
|
394
|
+
},
|
|
395
|
+
): Promise<TypedJobResult<TActions[TActionName]>> {
|
|
396
|
+
// Check if already aborted before starting
|
|
397
|
+
if (options?.signal?.aborted) {
|
|
398
|
+
throw new Error('Operation was aborted')
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create the job
|
|
402
|
+
const jobId = await this.runAction(actionName, input)
|
|
403
|
+
|
|
404
|
+
// Set up abort handler to cancel the job if signal is aborted
|
|
405
|
+
let abortHandler: (() => void) | undefined
|
|
406
|
+
if (options?.signal) {
|
|
407
|
+
abortHandler = () => {
|
|
408
|
+
this.cancelJob(jobId).catch((err) => {
|
|
409
|
+
this.#logger.error({ err, jobId }, '[Duron] Error cancelling job on abort')
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
options.signal.addEventListener('abort', abortHandler, { once: true })
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Set up timeout handler to cancel the job if timeout is reached
|
|
416
|
+
let timeoutId: NodeJS.Timeout | undefined
|
|
417
|
+
let timeoutAbortController: AbortController | undefined
|
|
418
|
+
if (options?.timeout) {
|
|
419
|
+
timeoutAbortController = new AbortController()
|
|
420
|
+
timeoutId = setTimeout(() => {
|
|
421
|
+
timeoutAbortController!.abort()
|
|
422
|
+
this.cancelJob(jobId).catch((err) => {
|
|
423
|
+
this.#logger.error({ err, jobId }, '[Duron] Error cancelling job on timeout')
|
|
424
|
+
})
|
|
425
|
+
}, options.timeout)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Combine signals if both are provided
|
|
430
|
+
let waitSignal: AbortSignal | undefined
|
|
431
|
+
if (options?.signal && timeoutAbortController) {
|
|
432
|
+
waitSignal = AbortSignal.any([options.signal, timeoutAbortController.signal])
|
|
433
|
+
} else if (options?.signal) {
|
|
434
|
+
waitSignal = options.signal
|
|
435
|
+
} else if (timeoutAbortController) {
|
|
436
|
+
waitSignal = timeoutAbortController.signal
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Wait for the job to complete
|
|
440
|
+
const job = await this.waitForJob(jobId, { signal: waitSignal })
|
|
441
|
+
|
|
442
|
+
// Clean up
|
|
443
|
+
if (timeoutId) {
|
|
444
|
+
clearTimeout(timeoutId)
|
|
445
|
+
}
|
|
446
|
+
if (options?.signal && abortHandler) {
|
|
447
|
+
options.signal.removeEventListener('abort', abortHandler)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Handle null result (aborted or timed out)
|
|
451
|
+
if (!job) {
|
|
452
|
+
if (options?.signal?.aborted) {
|
|
453
|
+
throw new Error('Operation was aborted')
|
|
454
|
+
}
|
|
455
|
+
if (timeoutAbortController?.signal.aborted) {
|
|
456
|
+
throw new Error('Operation timed out')
|
|
457
|
+
}
|
|
458
|
+
throw new Error('Job not found')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Handle cancelled job
|
|
462
|
+
if (job.status === JOB_STATUS_CANCELLED) {
|
|
463
|
+
if (options?.signal?.aborted) {
|
|
464
|
+
throw new Error('Operation was aborted')
|
|
465
|
+
}
|
|
466
|
+
if (timeoutAbortController?.signal.aborted) {
|
|
467
|
+
throw new Error('Operation timed out')
|
|
468
|
+
}
|
|
469
|
+
throw new Error('Job was cancelled')
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Handle failed job
|
|
473
|
+
if (job.status === JOB_STATUS_FAILED) {
|
|
474
|
+
const errorMessage = job.error?.message ?? 'Job failed'
|
|
475
|
+
const error = new Error(errorMessage)
|
|
476
|
+
if (job.error?.stack) {
|
|
477
|
+
error.stack = job.error.stack
|
|
478
|
+
}
|
|
479
|
+
throw error
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Return the job result with typed input/output
|
|
483
|
+
return job as TypedJobResult<TActions[TActionName]>
|
|
484
|
+
} catch (err) {
|
|
485
|
+
// Clean up on error
|
|
486
|
+
if (timeoutId) {
|
|
487
|
+
clearTimeout(timeoutId)
|
|
488
|
+
}
|
|
489
|
+
if (options?.signal && abortHandler) {
|
|
490
|
+
options.signal.removeEventListener('abort', abortHandler)
|
|
491
|
+
}
|
|
492
|
+
throw err
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
338
496
|
/**
|
|
339
497
|
* Fetch and process jobs from the database.
|
|
340
498
|
* Concurrency limits are determined from the latest job created for each groupKey.
|
|
@@ -515,11 +673,11 @@ export class Client<
|
|
|
515
673
|
|
|
516
674
|
/**
|
|
517
675
|
* Wait for a job to change status by subscribing to job-status-changed events.
|
|
518
|
-
* When the job status changes, the job is
|
|
676
|
+
* When the job status changes, the job result is returned.
|
|
519
677
|
*
|
|
520
678
|
* @param jobId - The ID of the job to wait for
|
|
521
679
|
* @param options - Optional configuration including timeout
|
|
522
|
-
* @returns Promise resolving to the job when its status changes, or `null` if timeout
|
|
680
|
+
* @returns Promise resolving to the job result when its status changes, or `null` if timeout
|
|
523
681
|
*/
|
|
524
682
|
async waitForJob(
|
|
525
683
|
jobId: string,
|
|
@@ -534,7 +692,7 @@ export class Client<
|
|
|
534
692
|
*/
|
|
535
693
|
signal?: AbortSignal
|
|
536
694
|
},
|
|
537
|
-
): Promise<
|
|
695
|
+
): Promise<JobResult | null> {
|
|
538
696
|
await this.start()
|
|
539
697
|
|
|
540
698
|
// First, check if the job already exists and is in a terminal state
|
|
@@ -546,14 +704,22 @@ export class Client<
|
|
|
546
704
|
if (!job) {
|
|
547
705
|
return null
|
|
548
706
|
}
|
|
549
|
-
return
|
|
707
|
+
return {
|
|
708
|
+
jobId: job.id,
|
|
709
|
+
actionName: job.actionName,
|
|
710
|
+
status: job.status,
|
|
711
|
+
groupKey: job.groupKey,
|
|
712
|
+
input: job.input,
|
|
713
|
+
output: job.output,
|
|
714
|
+
error: job.error,
|
|
715
|
+
}
|
|
550
716
|
}
|
|
551
717
|
}
|
|
552
718
|
|
|
553
719
|
// Set up the shared event listener if not already set up
|
|
554
720
|
this.#setupJobStatusListener()
|
|
555
721
|
|
|
556
|
-
return new Promise<
|
|
722
|
+
return new Promise<JobResult | null>((resolve) => {
|
|
557
723
|
// Check if already aborted before setting up wait
|
|
558
724
|
if (options?.signal?.aborted) {
|
|
559
725
|
resolve(null)
|
|
@@ -796,6 +962,19 @@ export class Client<
|
|
|
796
962
|
// Fetch the job once for all pending waits
|
|
797
963
|
const job = await this.getJobById(event.jobId)
|
|
798
964
|
|
|
965
|
+
// Transform to JobResult
|
|
966
|
+
const result: JobResult | null = job
|
|
967
|
+
? {
|
|
968
|
+
jobId: job.id,
|
|
969
|
+
actionName: job.actionName,
|
|
970
|
+
status: job.status,
|
|
971
|
+
groupKey: job.groupKey,
|
|
972
|
+
input: job.input,
|
|
973
|
+
output: job.output,
|
|
974
|
+
error: job.error,
|
|
975
|
+
}
|
|
976
|
+
: null
|
|
977
|
+
|
|
799
978
|
// Resolve all pending waits for this job
|
|
800
979
|
const waitsToResolve = Array.from(pendingWaits)
|
|
801
980
|
this.#pendingJobWaits.delete(event.jobId)
|
|
@@ -808,7 +987,7 @@ export class Client<
|
|
|
808
987
|
if (wait.signal && wait.abortHandler) {
|
|
809
988
|
wait.signal.removeEventListener('abort', wait.abortHandler)
|
|
810
989
|
}
|
|
811
|
-
wait.resolve(
|
|
990
|
+
wait.resolve(result)
|
|
812
991
|
}
|
|
813
992
|
},
|
|
814
993
|
)
|
|
@@ -820,7 +999,7 @@ export class Client<
|
|
|
820
999
|
* @param jobId - The job ID
|
|
821
1000
|
* @param resolve - The resolve function to remove
|
|
822
1001
|
*/
|
|
823
|
-
#removeJobWait(jobId: string, resolve: (
|
|
1002
|
+
#removeJobWait(jobId: string, resolve: (result: JobResult | null) => void) {
|
|
824
1003
|
const pendingWaits = this.#pendingJobWaits.get(jobId)
|
|
825
1004
|
if (!pendingWaits) {
|
|
826
1005
|
return
|
package/src/step-manager.ts
CHANGED
|
@@ -23,7 +23,46 @@ import {
|
|
|
23
23
|
serializeError,
|
|
24
24
|
UnhandledChildStepsError,
|
|
25
25
|
} from './errors.js'
|
|
26
|
-
import type { ObserveContext, Span, TelemetryAdapter } from './telemetry/adapter.js'
|
|
26
|
+
import type { ObserveContext, Span, TelemetryAdapter, Tracer, TracerSpan } from './telemetry/adapter.js'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Noop Tracer (for fallback observe context)
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const noopTracerSpan: TracerSpan = {
|
|
33
|
+
setAttribute() {
|
|
34
|
+
// No-op
|
|
35
|
+
},
|
|
36
|
+
setAttributes() {
|
|
37
|
+
// No-op
|
|
38
|
+
},
|
|
39
|
+
addEvent() {
|
|
40
|
+
// No-op
|
|
41
|
+
},
|
|
42
|
+
recordException() {
|
|
43
|
+
// No-op
|
|
44
|
+
},
|
|
45
|
+
setStatusOk() {
|
|
46
|
+
// No-op
|
|
47
|
+
},
|
|
48
|
+
setStatusError() {
|
|
49
|
+
// No-op
|
|
50
|
+
},
|
|
51
|
+
end() {
|
|
52
|
+
// No-op
|
|
53
|
+
},
|
|
54
|
+
isRecording() {
|
|
55
|
+
return false
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const createNoopTracer = (name: string): Tracer => ({
|
|
60
|
+
name,
|
|
61
|
+
startSpan(): TracerSpan {
|
|
62
|
+
return noopTracerSpan
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
27
66
|
import pRetry from './utils/p-retry.js'
|
|
28
67
|
import waitForAbort from './utils/wait-for-abort.js'
|
|
29
68
|
|
|
@@ -251,6 +290,9 @@ export class StepManager {
|
|
|
251
290
|
addSpanEvent: () => {
|
|
252
291
|
// No-op
|
|
253
292
|
},
|
|
293
|
+
getTracer: (name: string) => {
|
|
294
|
+
return createNoopTracer(name)
|
|
295
|
+
},
|
|
254
296
|
}
|
|
255
297
|
}
|
|
256
298
|
|