duron 0.3.0-beta.5 → 0.3.0-beta.6
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/client.d.ts +25 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +102 -2
- package/package.json +1 -1
- package/src/client.ts +187 -8
package/dist/client.d.ts
CHANGED
|
@@ -3,7 +3,27 @@ import * as z from 'zod';
|
|
|
3
3
|
import type { Action } from './action.js';
|
|
4
4
|
import type { Adapter, GetActionsResult, GetJobStepsOptions, GetJobStepsResult, GetJobsOptions, GetJobsResult, GetMetricsOptions, GetMetricsResult, Job, JobStep } from './adapters/adapter.js';
|
|
5
5
|
import type { JobStatusResult, JobStepStatusResult } from './adapters/schemas.js';
|
|
6
|
+
import { type JobStatus } from './constants.js';
|
|
6
7
|
import { type TelemetryAdapter } from './telemetry/index.js';
|
|
8
|
+
type InferActionSchema<T> = T extends z.ZodTypeAny ? z.infer<T> : Record<string, unknown>;
|
|
9
|
+
export interface JobResult {
|
|
10
|
+
jobId: string;
|
|
11
|
+
actionName: string;
|
|
12
|
+
status: JobStatus;
|
|
13
|
+
groupKey: string;
|
|
14
|
+
input: unknown;
|
|
15
|
+
output: unknown;
|
|
16
|
+
error: Job['error'];
|
|
17
|
+
}
|
|
18
|
+
export interface TypedJobResult<TAction extends Action<any, any, any>> {
|
|
19
|
+
jobId: string;
|
|
20
|
+
actionName: string;
|
|
21
|
+
status: JobStatus;
|
|
22
|
+
groupKey: string;
|
|
23
|
+
input: InferActionSchema<NonNullable<TAction['input']>>;
|
|
24
|
+
output: InferActionSchema<NonNullable<TAction['output']>>;
|
|
25
|
+
error: Job['error'];
|
|
26
|
+
}
|
|
7
27
|
declare const BaseOptionsSchema: z.ZodObject<{
|
|
8
28
|
id: z.ZodOptional<z.ZodString>;
|
|
9
29
|
syncPattern: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"pull">, z.ZodLiteral<"push">, z.ZodLiteral<"hybrid">, z.ZodLiteral<false>]>>;
|
|
@@ -48,6 +68,10 @@ export declare class Client<TActions extends Record<string, Action<any, any, TVa
|
|
|
48
68
|
id?: string | undefined;
|
|
49
69
|
};
|
|
50
70
|
runAction<TActionName extends keyof TActions>(actionName: TActionName, input?: NonNullable<TActions[TActionName]['input']> extends z.ZodObject ? z.input<NonNullable<TActions[TActionName]['input']>> : never): Promise<string>;
|
|
71
|
+
runActionAndWait<TActionName extends keyof TActions>(actionName: TActionName, input?: NonNullable<TActions[TActionName]['input']> extends z.ZodObject ? z.input<NonNullable<TActions[TActionName]['input']>> : never, options?: {
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
timeout?: number;
|
|
74
|
+
}): Promise<TypedJobResult<TActions[TActionName]>>;
|
|
51
75
|
fetch(options: FetchOptions): Promise<{
|
|
52
76
|
id: string;
|
|
53
77
|
actionName: string;
|
|
@@ -79,7 +103,7 @@ export declare class Client<TActions extends Record<string, Action<any, any, TVa
|
|
|
79
103
|
waitForJob(jobId: string, options?: {
|
|
80
104
|
timeout?: number;
|
|
81
105
|
signal?: AbortSignal;
|
|
82
|
-
}): Promise<
|
|
106
|
+
}): Promise<JobResult | null>;
|
|
83
107
|
getActions(): Promise<GetActionsResult>;
|
|
84
108
|
getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult>;
|
|
85
109
|
getActionsMetadata(): Promise<Array<{
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,EAAE,EAAE,KAAK,MAAM,EAAE,MAAM,MAAM,CAAA;AAExC,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAExB,OAAO,KAAK,EAAE,MAAM,EAA6B,MAAM,aAAa,CAAA;AAEpE,OAAO,KAAK,EACV,OAAO,EACP,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,GAAG,EACH,OAAO,EACR,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -117,6 +117,87 @@ export class Client {
|
|
|
117
117
|
this.#logger.debug({ jobId, actionName: String(actionName), groupKey }, '[Duron] Action sent/created');
|
|
118
118
|
return jobId;
|
|
119
119
|
}
|
|
120
|
+
async runActionAndWait(actionName, input, options) {
|
|
121
|
+
if (options?.signal?.aborted) {
|
|
122
|
+
throw new Error('Operation was aborted');
|
|
123
|
+
}
|
|
124
|
+
const jobId = await this.runAction(actionName, input);
|
|
125
|
+
let abortHandler;
|
|
126
|
+
if (options?.signal) {
|
|
127
|
+
abortHandler = () => {
|
|
128
|
+
this.cancelJob(jobId).catch((err) => {
|
|
129
|
+
this.#logger.error({ err, jobId }, '[Duron] Error cancelling job on abort');
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
133
|
+
}
|
|
134
|
+
let timeoutId;
|
|
135
|
+
let timeoutAbortController;
|
|
136
|
+
if (options?.timeout) {
|
|
137
|
+
timeoutAbortController = new AbortController();
|
|
138
|
+
timeoutId = setTimeout(() => {
|
|
139
|
+
timeoutAbortController.abort();
|
|
140
|
+
this.cancelJob(jobId).catch((err) => {
|
|
141
|
+
this.#logger.error({ err, jobId }, '[Duron] Error cancelling job on timeout');
|
|
142
|
+
});
|
|
143
|
+
}, options.timeout);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
let waitSignal;
|
|
147
|
+
if (options?.signal && timeoutAbortController) {
|
|
148
|
+
waitSignal = AbortSignal.any([options.signal, timeoutAbortController.signal]);
|
|
149
|
+
}
|
|
150
|
+
else if (options?.signal) {
|
|
151
|
+
waitSignal = options.signal;
|
|
152
|
+
}
|
|
153
|
+
else if (timeoutAbortController) {
|
|
154
|
+
waitSignal = timeoutAbortController.signal;
|
|
155
|
+
}
|
|
156
|
+
const job = await this.waitForJob(jobId, { signal: waitSignal });
|
|
157
|
+
if (timeoutId) {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
}
|
|
160
|
+
if (options?.signal && abortHandler) {
|
|
161
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
162
|
+
}
|
|
163
|
+
if (!job) {
|
|
164
|
+
if (options?.signal?.aborted) {
|
|
165
|
+
throw new Error('Operation was aborted');
|
|
166
|
+
}
|
|
167
|
+
if (timeoutAbortController?.signal.aborted) {
|
|
168
|
+
throw new Error('Operation timed out');
|
|
169
|
+
}
|
|
170
|
+
throw new Error('Job not found');
|
|
171
|
+
}
|
|
172
|
+
if (job.status === JOB_STATUS_CANCELLED) {
|
|
173
|
+
if (options?.signal?.aborted) {
|
|
174
|
+
throw new Error('Operation was aborted');
|
|
175
|
+
}
|
|
176
|
+
if (timeoutAbortController?.signal.aborted) {
|
|
177
|
+
throw new Error('Operation timed out');
|
|
178
|
+
}
|
|
179
|
+
throw new Error('Job was cancelled');
|
|
180
|
+
}
|
|
181
|
+
if (job.status === JOB_STATUS_FAILED) {
|
|
182
|
+
const errorMessage = job.error?.message ?? 'Job failed';
|
|
183
|
+
const error = new Error(errorMessage);
|
|
184
|
+
if (job.error?.stack) {
|
|
185
|
+
error.stack = job.error.stack;
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
return job;
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (timeoutId) {
|
|
193
|
+
clearTimeout(timeoutId);
|
|
194
|
+
}
|
|
195
|
+
if (options?.signal && abortHandler) {
|
|
196
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
120
201
|
async fetch(options) {
|
|
121
202
|
await this.start();
|
|
122
203
|
if (!this.#actions) {
|
|
@@ -194,7 +275,15 @@ export class Client {
|
|
|
194
275
|
if (!job) {
|
|
195
276
|
return null;
|
|
196
277
|
}
|
|
197
|
-
return
|
|
278
|
+
return {
|
|
279
|
+
jobId: job.id,
|
|
280
|
+
actionName: job.actionName,
|
|
281
|
+
status: job.status,
|
|
282
|
+
groupKey: job.groupKey,
|
|
283
|
+
input: job.input,
|
|
284
|
+
output: job.output,
|
|
285
|
+
error: job.error,
|
|
286
|
+
};
|
|
198
287
|
}
|
|
199
288
|
}
|
|
200
289
|
this.#setupJobStatusListener();
|
|
@@ -347,6 +436,17 @@ export class Client {
|
|
|
347
436
|
return;
|
|
348
437
|
}
|
|
349
438
|
const job = await this.getJobById(event.jobId);
|
|
439
|
+
const result = job
|
|
440
|
+
? {
|
|
441
|
+
jobId: job.id,
|
|
442
|
+
actionName: job.actionName,
|
|
443
|
+
status: job.status,
|
|
444
|
+
groupKey: job.groupKey,
|
|
445
|
+
input: job.input,
|
|
446
|
+
output: job.output,
|
|
447
|
+
error: job.error,
|
|
448
|
+
}
|
|
449
|
+
: null;
|
|
350
450
|
const waitsToResolve = Array.from(pendingWaits);
|
|
351
451
|
this.#pendingJobWaits.delete(event.jobId);
|
|
352
452
|
for (const wait of waitsToResolve) {
|
|
@@ -356,7 +456,7 @@ export class Client {
|
|
|
356
456
|
if (wait.signal && wait.abortHandler) {
|
|
357
457
|
wait.signal.removeEventListener('abort', wait.abortHandler);
|
|
358
458
|
}
|
|
359
|
-
wait.resolve(
|
|
459
|
+
wait.resolve(result);
|
|
360
460
|
}
|
|
361
461
|
});
|
|
362
462
|
}
|
package/package.json
CHANGED
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
|