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 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<Job | null>;
106
+ }): Promise<JobResult | null>;
83
107
  getActions(): Promise<GetActionsResult>;
84
108
  getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult>;
85
109
  getActionsMetadata(): Promise<Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
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 job;
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(job);
459
+ wait.resolve(result);
360
460
  }
361
461
  });
362
462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duron",
3
- "version": "0.3.0-beta.5",
3
+ "version": "0.3.0-beta.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
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: (job: Job | null) => void
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 fetched and returned.
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<Job | null> {
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 job
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<Job | null>((resolve) => {
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(job)
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: (job: Job | null) => void) {
1002
+ #removeJobWait(jobId: string, resolve: (result: JobResult | null) => void) {
824
1003
  const pendingWaits = this.#pendingJobWaits.get(jobId)
825
1004
  if (!pendingWaits) {
826
1005
  return