duron 0.1.0

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 (82) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +140 -0
  3. package/dist/action-job.d.ts +24 -0
  4. package/dist/action-job.d.ts.map +1 -0
  5. package/dist/action-job.js +108 -0
  6. package/dist/action-manager.d.ts +21 -0
  7. package/dist/action-manager.d.ts.map +1 -0
  8. package/dist/action-manager.js +78 -0
  9. package/dist/action.d.ts +129 -0
  10. package/dist/action.d.ts.map +1 -0
  11. package/dist/action.js +87 -0
  12. package/dist/adapters/adapter.d.ts +92 -0
  13. package/dist/adapters/adapter.d.ts.map +1 -0
  14. package/dist/adapters/adapter.js +424 -0
  15. package/dist/adapters/postgres/drizzle.config.d.ts +3 -0
  16. package/dist/adapters/postgres/drizzle.config.d.ts.map +1 -0
  17. package/dist/adapters/postgres/drizzle.config.js +10 -0
  18. package/dist/adapters/postgres/pglite.d.ts +13 -0
  19. package/dist/adapters/postgres/pglite.d.ts.map +1 -0
  20. package/dist/adapters/postgres/pglite.js +36 -0
  21. package/dist/adapters/postgres/postgres.d.ts +51 -0
  22. package/dist/adapters/postgres/postgres.d.ts.map +1 -0
  23. package/dist/adapters/postgres/postgres.js +867 -0
  24. package/dist/adapters/postgres/schema.d.ts +581 -0
  25. package/dist/adapters/postgres/schema.d.ts.map +1 -0
  26. package/dist/adapters/postgres/schema.default.d.ts +577 -0
  27. package/dist/adapters/postgres/schema.default.d.ts.map +1 -0
  28. package/dist/adapters/postgres/schema.default.js +3 -0
  29. package/dist/adapters/postgres/schema.js +87 -0
  30. package/dist/adapters/schemas.d.ts +516 -0
  31. package/dist/adapters/schemas.d.ts.map +1 -0
  32. package/dist/adapters/schemas.js +184 -0
  33. package/dist/client.d.ts +85 -0
  34. package/dist/client.d.ts.map +1 -0
  35. package/dist/client.js +416 -0
  36. package/dist/constants.d.ts +14 -0
  37. package/dist/constants.d.ts.map +1 -0
  38. package/dist/constants.js +22 -0
  39. package/dist/errors.d.ts +43 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +75 -0
  42. package/dist/index.d.ts +8 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +6 -0
  45. package/dist/server.d.ts +1193 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +516 -0
  48. package/dist/step-manager.d.ts +46 -0
  49. package/dist/step-manager.d.ts.map +1 -0
  50. package/dist/step-manager.js +216 -0
  51. package/dist/utils/checksum.d.ts +2 -0
  52. package/dist/utils/checksum.d.ts.map +1 -0
  53. package/dist/utils/checksum.js +6 -0
  54. package/dist/utils/p-retry.d.ts +19 -0
  55. package/dist/utils/p-retry.d.ts.map +1 -0
  56. package/dist/utils/p-retry.js +130 -0
  57. package/dist/utils/wait-for-abort.d.ts +5 -0
  58. package/dist/utils/wait-for-abort.d.ts.map +1 -0
  59. package/dist/utils/wait-for-abort.js +32 -0
  60. package/migrations/postgres/0000_lethal_speed_demon.sql +64 -0
  61. package/migrations/postgres/meta/0000_snapshot.json +606 -0
  62. package/migrations/postgres/meta/_journal.json +13 -0
  63. package/package.json +88 -0
  64. package/src/action-job.ts +201 -0
  65. package/src/action-manager.ts +166 -0
  66. package/src/action.ts +247 -0
  67. package/src/adapters/adapter.ts +969 -0
  68. package/src/adapters/postgres/drizzle.config.ts +11 -0
  69. package/src/adapters/postgres/pglite.ts +86 -0
  70. package/src/adapters/postgres/postgres.ts +1346 -0
  71. package/src/adapters/postgres/schema.default.ts +5 -0
  72. package/src/adapters/postgres/schema.ts +119 -0
  73. package/src/adapters/schemas.ts +320 -0
  74. package/src/client.ts +859 -0
  75. package/src/constants.ts +37 -0
  76. package/src/errors.ts +205 -0
  77. package/src/index.ts +14 -0
  78. package/src/server.ts +718 -0
  79. package/src/step-manager.ts +471 -0
  80. package/src/utils/checksum.ts +7 -0
  81. package/src/utils/p-retry.ts +213 -0
  82. package/src/utils/wait-for-abort.ts +40 -0
@@ -0,0 +1,969 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ import type { Logger } from 'pino'
4
+ import { z } from 'zod'
5
+
6
+ import {
7
+ JOB_STATUS_CANCELLED,
8
+ JOB_STATUS_COMPLETED,
9
+ JOB_STATUS_FAILED,
10
+ type JobStatus,
11
+ STEP_STATUS_CANCELLED,
12
+ STEP_STATUS_COMPLETED,
13
+ STEP_STATUS_FAILED,
14
+ type StepStatus,
15
+ } from '../constants.js'
16
+ import type {
17
+ CancelJobOptions,
18
+ CancelJobStepOptions,
19
+ CompleteJobOptions,
20
+ CompleteJobStepOptions,
21
+ CreateJobOptions,
22
+ CreateOrRecoverJobStepOptions,
23
+ CreateOrRecoverJobStepResult,
24
+ DelayJobStepOptions,
25
+ DeleteJobOptions,
26
+ DeleteJobsOptions,
27
+ FailJobOptions,
28
+ FailJobStepOptions,
29
+ FetchOptions,
30
+ GetActionsResult,
31
+ GetJobStepsOptions,
32
+ GetJobStepsResult,
33
+ GetJobsOptions,
34
+ GetJobsResult,
35
+ Job,
36
+ JobStatusResult,
37
+ JobStep,
38
+ JobStepStatusResult,
39
+ RecoverJobsOptions,
40
+ RetryJobOptions,
41
+ } from './schemas.js'
42
+ import {
43
+ BooleanResultSchema,
44
+ CancelJobOptionsSchema,
45
+ CancelJobStepOptionsSchema,
46
+ CompleteJobOptionsSchema,
47
+ CompleteJobStepOptionsSchema,
48
+ CreateJobOptionsSchema,
49
+ CreateOrRecoverJobStepOptionsSchema,
50
+ CreateOrRecoverJobStepResultNullableSchema,
51
+ DelayJobStepOptionsSchema,
52
+ DeleteJobOptionsSchema,
53
+ DeleteJobsOptionsSchema,
54
+ FailJobOptionsSchema,
55
+ FailJobStepOptionsSchema,
56
+ FetchOptionsSchema,
57
+ GetActionsResultSchema,
58
+ GetJobStepsOptionsSchema,
59
+ GetJobStepsResultSchema,
60
+ GetJobsOptionsSchema,
61
+ GetJobsResultSchema,
62
+ JobIdResultSchema,
63
+ JobSchema,
64
+ JobStatusResultSchema,
65
+ JobStepSchema,
66
+ JobStepStatusResultSchema,
67
+ JobsArrayResultSchema,
68
+ NumberResultSchema,
69
+ RecoverJobsOptionsSchema,
70
+ RetryJobOptionsSchema,
71
+ } from './schemas.js'
72
+
73
+ // Re-export types from schemas for backward compatibility
74
+ export type {
75
+ ActionStats,
76
+ CancelJobOptions,
77
+ CancelJobStepOptions,
78
+ CompleteJobOptions,
79
+ CompleteJobStepOptions,
80
+ CreateJobOptions,
81
+ CreateOrRecoverJobStepOptions,
82
+ CreateOrRecoverJobStepResult,
83
+ DelayJobStepOptions,
84
+ DeleteJobOptions,
85
+ DeleteJobsOptions,
86
+ FailJobOptions,
87
+ FailJobStepOptions,
88
+ FetchOptions,
89
+ GetActionsResult,
90
+ GetJobStepsOptions,
91
+ GetJobStepsResult,
92
+ GetJobsOptions,
93
+ GetJobsResult,
94
+ Job,
95
+ JobFilters,
96
+ JobSort,
97
+ JobSortField,
98
+ JobStatusResult,
99
+ JobStep,
100
+ JobStepStatusResult,
101
+ RecoverJobsOptions,
102
+ RetryJobOptions,
103
+ SortOrder,
104
+ } from './schemas.js'
105
+
106
+ // ============================================================================
107
+ // Adapter Events
108
+ // ============================================================================
109
+
110
+ export interface AdapterEvents {
111
+ 'job-status-changed': [
112
+ {
113
+ jobId: string
114
+ status: JobStatus | 'retried'
115
+ ownerId: string
116
+ },
117
+ ]
118
+ 'job-available': [
119
+ {
120
+ jobId: string
121
+ },
122
+ ]
123
+ 'step-status-changed': [
124
+ {
125
+ jobId: string
126
+ stepId: string
127
+ status: StepStatus
128
+ error: any | null
129
+ ownerId: string
130
+ },
131
+ ]
132
+ 'step-delayed': [
133
+ {
134
+ jobId: string
135
+ stepId: string
136
+ delayedMs: number
137
+ error: any
138
+ ownerId: string
139
+ },
140
+ ]
141
+ }
142
+
143
+ // ============================================================================
144
+ // Abstract Adapter Class
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Abstract base class for database adapters.
149
+ * All adapters must extend this class and implement its abstract methods.
150
+ */
151
+ export abstract class Adapter extends EventEmitter<AdapterEvents> {
152
+ #id!: string
153
+ #started: boolean = false
154
+ #stopped: boolean = false
155
+ #starting: Promise<boolean> | null = null
156
+ #stopping: Promise<boolean> | null = null
157
+ #logger: Logger | null = null
158
+
159
+ // ============================================================================
160
+ // Lifecycle Methods
161
+ // ============================================================================
162
+
163
+ /**
164
+ * Start the adapter.
165
+ * Performs any necessary initialization, such as running migrations or setting up listeners.
166
+ *
167
+ * @returns Promise resolving to `true` if started successfully, `false` otherwise
168
+ */
169
+ async start() {
170
+ try {
171
+ if (!this.#id) {
172
+ throw new Error('Adapter ID is not set')
173
+ }
174
+
175
+ if (this.#stopping || this.#stopped) {
176
+ return false
177
+ }
178
+
179
+ if (this.#started) {
180
+ return true
181
+ }
182
+
183
+ if (this.#starting) {
184
+ return this.#starting
185
+ }
186
+
187
+ this.#starting = (async () => {
188
+ await this._start()
189
+ this.#started = true
190
+ this.#starting = null
191
+ return true
192
+ })()
193
+
194
+ return this.#starting
195
+ } catch (error) {
196
+ this.#logger?.error(error, 'Error in Adapter.start()')
197
+ throw error
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Stop the adapter.
203
+ * Performs cleanup, such as closing database connections.
204
+ *
205
+ * @returns Promise resolving to `true` if stopped successfully, `false` otherwise
206
+ */
207
+ async stop() {
208
+ try {
209
+ if (this.#stopped) {
210
+ return true
211
+ }
212
+
213
+ if (this.#stopping) {
214
+ return this.#stopping
215
+ }
216
+
217
+ this.#stopping = (async () => {
218
+ await this._stop()
219
+ this.#stopped = true
220
+ this.#stopping = null
221
+ return true
222
+ })()
223
+
224
+ return this.#stopping
225
+ } catch (error) {
226
+ this.#logger?.error(error, 'Error in Adapter.stop()')
227
+ throw error
228
+ }
229
+ }
230
+
231
+ // ============================================================================
232
+ // Configuration Methods
233
+ // ============================================================================
234
+
235
+ /**
236
+ * Set the unique identifier for this adapter instance.
237
+ * Used for multi-process coordination and job ownership.
238
+ *
239
+ * @param id - The unique identifier for this adapter instance
240
+ */
241
+ setId(id: string) {
242
+ try {
243
+ this.#id = id
244
+ } catch (error) {
245
+ this.#logger?.error(error, 'Error in Adapter.setId()')
246
+ throw error
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Set the logger instance for this adapter.
252
+ *
253
+ * @param logger - The logger instance to use for logging
254
+ */
255
+ setLogger(logger: Logger) {
256
+ try {
257
+ this.#logger = logger
258
+ } catch (error) {
259
+ this.#logger?.error(error, 'Error in Adapter.setLogger()')
260
+ throw error
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Get the unique identifier for this adapter instance.
266
+ *
267
+ * @returns The unique identifier for this adapter instance
268
+ */
269
+ get id(): string {
270
+ return this.#id
271
+ }
272
+
273
+ /**
274
+ * Get the logger instance for this adapter.
275
+ *
276
+ * @returns The logger instance, or `null` if not set
277
+ */
278
+ get logger(): Logger | null {
279
+ return this.#logger
280
+ }
281
+
282
+ // ============================================================================
283
+ // Job Methods
284
+ // ============================================================================
285
+
286
+ /**
287
+ * Create a new job in the database.
288
+ *
289
+ * @returns Promise resolving to the job ID, or `null` if creation failed
290
+ */
291
+ async createJob(options: CreateJobOptions): Promise<string | null> {
292
+ try {
293
+ await this.start()
294
+ const parsedOptions = CreateJobOptionsSchema.parse(options)
295
+ const result = await this._createJob(parsedOptions)
296
+ const jobId = JobIdResultSchema.parse(result)
297
+ if (jobId !== null) {
298
+ await this._notify('job-available', { jobId })
299
+ }
300
+ return jobId
301
+ } catch (error) {
302
+ this.#logger?.error(error, 'Error in Adapter.createJob()')
303
+ throw error
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Mark a job as completed.
309
+ *
310
+ * @returns Promise resolving to `true` if completed, `false` otherwise
311
+ */
312
+ async completeJob(options: CompleteJobOptions): Promise<boolean> {
313
+ try {
314
+ await this.start()
315
+ const parsedOptions = CompleteJobOptionsSchema.parse(options)
316
+ const result = await this._completeJob(parsedOptions)
317
+ const success = BooleanResultSchema.parse(result)
318
+ if (success) {
319
+ await this._notify('job-status-changed', {
320
+ jobId: parsedOptions.jobId,
321
+ status: JOB_STATUS_COMPLETED,
322
+ ownerId: this.id,
323
+ })
324
+ }
325
+ return success
326
+ } catch (error) {
327
+ this.#logger?.error(error, 'Error in Adapter.completeJob()')
328
+ throw error
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Mark a job as failed.
334
+ *
335
+ * @returns Promise resolving to `true` if failed, `false` otherwise
336
+ */
337
+ async failJob(options: FailJobOptions): Promise<boolean> {
338
+ try {
339
+ await this.start()
340
+ const parsedOptions = FailJobOptionsSchema.parse(options)
341
+ const result = await this._failJob(parsedOptions)
342
+ const success = BooleanResultSchema.parse(result)
343
+ if (success) {
344
+ await this._notify('job-status-changed', {
345
+ jobId: parsedOptions.jobId,
346
+ status: JOB_STATUS_FAILED,
347
+ ownerId: this.id,
348
+ })
349
+ }
350
+ return success
351
+ } catch (error) {
352
+ this.#logger?.error(error, 'Error in Adapter.failJob()')
353
+ throw error
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Cancel a job.
359
+ *
360
+ * @returns Promise resolving to `true` if cancelled, `false` otherwise
361
+ */
362
+ async cancelJob(options: CancelJobOptions): Promise<boolean> {
363
+ try {
364
+ await this.start()
365
+ const parsedOptions = CancelJobOptionsSchema.parse(options)
366
+ const result = await this._cancelJob(parsedOptions)
367
+ const success = BooleanResultSchema.parse(result)
368
+ if (success) {
369
+ await this._notify('job-status-changed', {
370
+ jobId: parsedOptions.jobId,
371
+ status: JOB_STATUS_CANCELLED,
372
+ ownerId: this.id,
373
+ })
374
+ }
375
+ return success
376
+ } catch (error) {
377
+ this.#logger?.error(error, 'Error in Adapter.cancelJob()')
378
+ throw error
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Retry a failed job by creating a copy of it with status 'created' and cleared output/error.
384
+ *
385
+ * @returns Promise resolving to the job ID, or `null` if creation failed
386
+ */
387
+ async retryJob(options: RetryJobOptions): Promise<string | null> {
388
+ try {
389
+ await this.start()
390
+ const parsedOptions = RetryJobOptionsSchema.parse(options)
391
+ const result = await this._retryJob(parsedOptions)
392
+ const jobId = JobIdResultSchema.parse(result)
393
+ if (jobId !== null) {
394
+ await this._notify('job-available', { jobId })
395
+ }
396
+ return jobId
397
+ } catch (error) {
398
+ this.#logger?.error(error, 'Error in Adapter.retryJob()')
399
+ throw error
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Delete a job by its ID.
405
+ * Active jobs cannot be deleted.
406
+ *
407
+ * @returns Promise resolving to `true` if deleted, `false` otherwise
408
+ */
409
+ async deleteJob(options: DeleteJobOptions): Promise<boolean> {
410
+ try {
411
+ await this.start()
412
+ const parsedOptions = DeleteJobOptionsSchema.parse(options)
413
+ const result = await this._deleteJob(parsedOptions)
414
+ return BooleanResultSchema.parse(result)
415
+ } catch (error) {
416
+ this.#logger?.error(error, 'Error in Adapter.deleteJob()')
417
+ throw error
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Delete multiple jobs using the same filters as getJobs.
423
+ * Active jobs cannot be deleted and will be excluded from deletion.
424
+ *
425
+ * @returns Promise resolving to the number of jobs deleted
426
+ */
427
+ async deleteJobs(options?: DeleteJobsOptions): Promise<number> {
428
+ try {
429
+ await this.start()
430
+ const parsedOptions = options ? DeleteJobsOptionsSchema.parse(options) : undefined
431
+ const result = await this._deleteJobs(parsedOptions)
432
+ return NumberResultSchema.parse(result)
433
+ } catch (error) {
434
+ this.#logger?.error(error, 'Error in Adapter.deleteJobs()')
435
+ throw error
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Fetch jobs from the database respecting concurrency limits per group.
441
+ *
442
+ * @returns Promise resolving to an array of fetched jobs
443
+ */
444
+ async fetch(options: FetchOptions): Promise<Job[]> {
445
+ try {
446
+ await this.start()
447
+ const parsedOptions = FetchOptionsSchema.parse(options)
448
+ const result = await this._fetch(parsedOptions)
449
+ return JobsArrayResultSchema.parse(result)
450
+ } catch (error) {
451
+ this.#logger?.error(error, 'Error in Adapter.fetch()')
452
+ throw error
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Recover stuck jobs (jobs that were active but the process that owned them is no longer running).
458
+ *
459
+ * @returns Promise resolving to the number of jobs recovered
460
+ */
461
+ async recoverJobs(options: RecoverJobsOptions): Promise<number> {
462
+ try {
463
+ await this.start()
464
+ const parsedOptions = RecoverJobsOptionsSchema.parse(options)
465
+ const result = await this._recoverJobs(parsedOptions)
466
+ return NumberResultSchema.parse(result)
467
+ } catch (error) {
468
+ this.#logger?.error(error, 'Error in Adapter.recoverJobs()')
469
+ throw error
470
+ }
471
+ }
472
+
473
+ // ============================================================================
474
+ // Step Methods
475
+ // ============================================================================
476
+
477
+ /**
478
+ * Create or recover a job step by creating or resetting a step record in the database.
479
+ *
480
+ * @returns Promise resolving to the step, or `null` if creation failed
481
+ */
482
+ async createOrRecoverJobStep(options: CreateOrRecoverJobStepOptions): Promise<CreateOrRecoverJobStepResult | null> {
483
+ try {
484
+ await this.start()
485
+ const parsedOptions = CreateOrRecoverJobStepOptionsSchema.parse(options)
486
+ const result = await this._createOrRecoverJobStep(parsedOptions)
487
+ return CreateOrRecoverJobStepResultNullableSchema.parse(result)
488
+ } catch (error) {
489
+ this.#logger?.error(error, 'Error in Adapter.createOrRecoverJobStep()')
490
+ throw error
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Mark a job step as completed.
496
+ *
497
+ * @returns Promise resolving to `true` if completed, `false` otherwise
498
+ */
499
+ async completeJobStep(options: CompleteJobStepOptions): Promise<boolean> {
500
+ try {
501
+ await this.start()
502
+ const parsedOptions = CompleteJobStepOptionsSchema.parse(options)
503
+ const result = await this._completeJobStep(parsedOptions)
504
+ const success = BooleanResultSchema.parse(result)
505
+ if (success) {
506
+ // Fetch jobId for notification
507
+ const step = await this._getJobStepById(parsedOptions.stepId)
508
+ if (step) {
509
+ await this._notify('step-status-changed', {
510
+ jobId: step.jobId,
511
+ stepId: parsedOptions.stepId,
512
+ status: STEP_STATUS_COMPLETED,
513
+ error: null,
514
+ ownerId: this.id,
515
+ })
516
+ }
517
+ }
518
+ return success
519
+ } catch (error) {
520
+ this.#logger?.error(error, 'Error in Adapter.completeJobStep()')
521
+ throw error
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Mark a job step as failed.
527
+ *
528
+ * @returns Promise resolving to `true` if failed, `false` otherwise
529
+ */
530
+ async failJobStep(options: FailJobStepOptions): Promise<boolean> {
531
+ try {
532
+ await this.start()
533
+ const parsedOptions = FailJobStepOptionsSchema.parse(options)
534
+ const result = await this._failJobStep(parsedOptions)
535
+ const success = BooleanResultSchema.parse(result)
536
+ if (success) {
537
+ // Fetch jobId for notification
538
+ const step = await this._getJobStepById(parsedOptions.stepId)
539
+ if (step) {
540
+ await this._notify('step-status-changed', {
541
+ jobId: step.jobId,
542
+ stepId: parsedOptions.stepId,
543
+ status: STEP_STATUS_FAILED,
544
+ error: parsedOptions.error,
545
+ ownerId: this.id,
546
+ })
547
+ }
548
+ }
549
+ return success
550
+ } catch (error) {
551
+ this.#logger?.error(error, 'Error in Adapter.failJobStep()')
552
+ throw error
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Delay a job step.
558
+ *
559
+ * @returns Promise resolving to `true` if delayed, `false` otherwise
560
+ */
561
+ async delayJobStep(options: DelayJobStepOptions): Promise<boolean> {
562
+ try {
563
+ await this.start()
564
+ const parsedOptions = DelayJobStepOptionsSchema.parse(options)
565
+ const result = await this._delayJobStep(parsedOptions)
566
+ const success = BooleanResultSchema.parse(result)
567
+ if (success) {
568
+ // Fetch jobId for notification
569
+ const step = await this._getJobStepById(parsedOptions.stepId)
570
+ if (step) {
571
+ await this._notify('step-delayed', {
572
+ jobId: step.jobId,
573
+ stepId: parsedOptions.stepId,
574
+ delayedMs: parsedOptions.delayMs,
575
+ error: parsedOptions.error,
576
+ ownerId: this.id,
577
+ })
578
+ }
579
+ }
580
+ return success
581
+ } catch (error) {
582
+ this.#logger?.error(error, 'Error in Adapter.delayJobStep()')
583
+ throw error
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Cancel a job step.
589
+ *
590
+ * @returns Promise resolving to `true` if cancelled, `false` otherwise
591
+ */
592
+ async cancelJobStep(options: CancelJobStepOptions): Promise<boolean> {
593
+ try {
594
+ await this.start()
595
+ const parsedOptions = CancelJobStepOptionsSchema.parse(options)
596
+ const result = await this._cancelJobStep(parsedOptions)
597
+ const success = BooleanResultSchema.parse(result)
598
+ if (success) {
599
+ // Fetch jobId for notification
600
+ const step = await this._getJobStepById(parsedOptions.stepId)
601
+ if (step) {
602
+ await this._notify('step-status-changed', {
603
+ jobId: step.jobId,
604
+ stepId: parsedOptions.stepId,
605
+ status: STEP_STATUS_CANCELLED,
606
+ error: null,
607
+ ownerId: this.id,
608
+ })
609
+ }
610
+ }
611
+ return success
612
+ } catch (error) {
613
+ this.#logger?.error(error, 'Error in Adapter.cancelJobStep()')
614
+ throw error
615
+ }
616
+ }
617
+
618
+ // ============================================================================
619
+ // Private Job Methods (to be implemented by adapters)
620
+ // ============================================================================
621
+
622
+ /**
623
+ * Internal method to create a new job in the database.
624
+ *
625
+ * @param options - Validated job creation options
626
+ * @returns Promise resolving to the job ID, or `null` if creation failed
627
+ */
628
+ protected abstract _createJob(options: CreateJobOptions): Promise<string | null>
629
+
630
+ /**
631
+ * Internal method to mark a job as completed.
632
+ *
633
+ * @param options - Validated job completion options
634
+ * @returns Promise resolving to `true` if completed, `false` otherwise
635
+ */
636
+ protected abstract _completeJob(options: CompleteJobOptions): Promise<boolean>
637
+
638
+ /**
639
+ * Internal method to mark a job as failed.
640
+ *
641
+ * @param options - Validated job failure options
642
+ * @returns Promise resolving to `true` if failed, `false` otherwise
643
+ */
644
+ protected abstract _failJob(options: FailJobOptions): Promise<boolean>
645
+
646
+ /**
647
+ * Internal method to cancel a job.
648
+ *
649
+ * @param options - Validated job cancellation options
650
+ * @returns Promise resolving to `true` if cancelled, `false` otherwise
651
+ */
652
+ protected abstract _cancelJob(options: CancelJobOptions): Promise<boolean>
653
+
654
+ /**
655
+ * Internal method to retry a failed job by creating a copy of it with status 'created' and cleared output/error.
656
+ *
657
+ * @param options - Validated job retry options
658
+ * @returns Promise resolving to the job ID, or `null` if creation failed
659
+ */
660
+ protected abstract _retryJob(options: RetryJobOptions): Promise<string | null>
661
+
662
+ /**
663
+ * Internal method to delete a job by its ID.
664
+ * Active jobs cannot be deleted.
665
+ *
666
+ * @param options - Validated job deletion options
667
+ * @returns Promise resolving to `true` if deleted, `false` otherwise
668
+ */
669
+ protected abstract _deleteJob(options: DeleteJobOptions): Promise<boolean>
670
+
671
+ /**
672
+ * Internal method to delete multiple jobs using the same filters as getJobs.
673
+ * Active jobs cannot be deleted and will be excluded from deletion.
674
+ *
675
+ * @param options - Validated deletion options (same as GetJobsOptions)
676
+ * @returns Promise resolving to the number of jobs deleted
677
+ */
678
+ protected abstract _deleteJobs(options?: DeleteJobsOptions): Promise<number>
679
+
680
+ /**
681
+ * Internal method to fetch jobs from the database respecting concurrency limits per group.
682
+ *
683
+ * @param options - Validated fetch options
684
+ * @returns Promise resolving to an array of fetched jobs
685
+ */
686
+ protected abstract _fetch(options: FetchOptions): Promise<Job[]>
687
+
688
+ /**
689
+ * Internal method to recover stuck jobs (jobs that were active but the process that owned them is no longer running).
690
+ *
691
+ * @param options - Validated recovery options
692
+ * @returns Promise resolving to the number of jobs recovered
693
+ */
694
+ protected abstract _recoverJobs(options: RecoverJobsOptions): Promise<number>
695
+
696
+ // ============================================================================
697
+ // Private Step Methods (to be implemented by adapters)
698
+ // ============================================================================
699
+
700
+ /**
701
+ * Internal method to create or recover a job step by creating or resetting a step record in the database.
702
+ *
703
+ * @param options - Validated step creation options
704
+ * @returns Promise resolving to the step, or `null` if creation failed
705
+ */
706
+ protected abstract _createOrRecoverJobStep(
707
+ options: CreateOrRecoverJobStepOptions,
708
+ ): Promise<CreateOrRecoverJobStepResult | null>
709
+
710
+ /**
711
+ * Internal method to mark a job step as completed.
712
+ *
713
+ * @param options - Validated step completion options
714
+ * @returns Promise resolving to `true` if completed, `false` otherwise
715
+ */
716
+ protected abstract _completeJobStep(options: CompleteJobStepOptions): Promise<boolean>
717
+
718
+ /**
719
+ * Internal method to mark a job step as failed.
720
+ *
721
+ * @param options - Validated step failure options
722
+ * @returns Promise resolving to `true` if failed, `false` otherwise
723
+ */
724
+ protected abstract _failJobStep(options: FailJobStepOptions): Promise<boolean>
725
+
726
+ /**
727
+ * Internal method to delay a job step.
728
+ *
729
+ * @param options - Validated step delay options
730
+ * @returns Promise resolving to `true` if delayed, `false` otherwise
731
+ */
732
+ protected abstract _delayJobStep(options: DelayJobStepOptions): Promise<boolean>
733
+
734
+ /**
735
+ * Internal method to cancel a job step.
736
+ *
737
+ * @param options - Validated step cancellation options
738
+ * @returns Promise resolving to `true` if cancelled, `false` otherwise
739
+ */
740
+ protected abstract _cancelJobStep(options: CancelJobStepOptions): Promise<boolean>
741
+
742
+ // ============================================================================
743
+ // Query Methods
744
+ // ============================================================================
745
+
746
+ /**
747
+ * Get a job by its ID. Does not include step information.
748
+ *
749
+ * @param jobId - The ID of the job to retrieve
750
+ * @returns Promise resolving to the job, or `null` if not found
751
+ */
752
+ async getJobById(jobId: string): Promise<Job | null> {
753
+ try {
754
+ const parsedJobId = z.string().parse(jobId)
755
+ const result = await this._getJobById(parsedJobId)
756
+ if (result === null) {
757
+ return null
758
+ }
759
+ return JobSchema.parse(result)
760
+ } catch (error) {
761
+ this.#logger?.error(error, 'Error in Adapter.getJobById()')
762
+ throw error
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Get steps for a job with pagination and fuzzy search.
768
+ * Steps are always ordered by created_at ASC.
769
+ * Steps do not include output data.
770
+ *
771
+ * @param options - Query options including jobId, pagination, and search
772
+ * @returns Promise resolving to steps result with pagination info
773
+ */
774
+ async getJobSteps(options: GetJobStepsOptions): Promise<GetJobStepsResult> {
775
+ try {
776
+ const parsedOptions = GetJobStepsOptionsSchema.parse(options)
777
+ const result = await this._getJobSteps(parsedOptions)
778
+ return GetJobStepsResultSchema.parse(result)
779
+ } catch (error) {
780
+ this.#logger?.error(error, 'Error in Adapter.getJobSteps()')
781
+ throw error
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Get jobs with pagination, filtering, and sorting.
787
+ * Does not include step information or job output.
788
+ *
789
+ * @param options - Query options including pagination, filters, and sort
790
+ * @returns Promise resolving to jobs result with pagination info
791
+ */
792
+ async getJobs(options?: GetJobsOptions): Promise<GetJobsResult> {
793
+ try {
794
+ const parsedOptions = options ? GetJobsOptionsSchema.parse(options) : undefined
795
+ const result = await this._getJobs(parsedOptions)
796
+ return GetJobsResultSchema.parse(result)
797
+ } catch (error) {
798
+ this.#logger?.error(error, 'Error in Adapter.getJobs()')
799
+ throw error
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Get a step by its ID with all information.
805
+ *
806
+ * @param stepId - The ID of the step to retrieve
807
+ * @returns Promise resolving to the step, or `null` if not found
808
+ */
809
+ async getJobStepById(stepId: string): Promise<JobStep | null> {
810
+ try {
811
+ const parsedStepId = z.string().parse(stepId)
812
+ const result = await this._getJobStepById(parsedStepId)
813
+ if (result === null) {
814
+ return null
815
+ }
816
+ return JobStepSchema.parse(result)
817
+ } catch (error) {
818
+ this.#logger?.error(error, 'Error in Adapter.getJobStepById()')
819
+ throw error
820
+ }
821
+ }
822
+
823
+ /**
824
+ * Get job status and updatedAt timestamp.
825
+ *
826
+ * @param jobId - The ID of the job
827
+ * @returns Promise resolving to job status result, or `null` if not found
828
+ */
829
+ async getJobStatus(jobId: string): Promise<JobStatusResult | null> {
830
+ try {
831
+ const parsedJobId = z.string().parse(jobId)
832
+ const result = await this._getJobStatus(parsedJobId)
833
+ if (result === null) {
834
+ return null
835
+ }
836
+ return JobStatusResultSchema.parse(result)
837
+ } catch (error) {
838
+ this.#logger?.error(error, 'Error in Adapter.getJobStatus()')
839
+ throw error
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Get job step status and updatedAt timestamp.
845
+ *
846
+ * @param stepId - The ID of the step
847
+ * @returns Promise resolving to step status result, or `null` if not found
848
+ */
849
+ async getJobStepStatus(stepId: string): Promise<JobStepStatusResult | null> {
850
+ try {
851
+ const parsedStepId = z.string().parse(stepId)
852
+ const result = await this._getJobStepStatus(parsedStepId)
853
+ if (result === null) {
854
+ return null
855
+ }
856
+ return JobStepStatusResultSchema.parse(result)
857
+ } catch (error) {
858
+ this.#logger?.error(error, 'Error in Adapter.getJobStepStatus()')
859
+ throw error
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Get action statistics including counts and last job created date.
865
+ *
866
+ * @returns Promise resolving to action statistics
867
+ */
868
+ async getActions(): Promise<GetActionsResult> {
869
+ try {
870
+ const result = await this._getActions()
871
+ return GetActionsResultSchema.parse(result)
872
+ } catch (error) {
873
+ this.#logger?.error(error, 'Error in Adapter.getActions()')
874
+ throw error
875
+ }
876
+ }
877
+
878
+ // ============================================================================
879
+ // Private Query Methods (to be implemented by adapters)
880
+ // ============================================================================
881
+
882
+ /**
883
+ * Internal method to get a job by its ID. Does not include step information.
884
+ *
885
+ * @param jobId - The validated ID of the job to retrieve
886
+ * @returns Promise resolving to the job, or `null` if not found
887
+ */
888
+ protected abstract _getJobById(jobId: string): Promise<Job | null>
889
+
890
+ /**
891
+ * Internal method to get steps for a job with pagination and fuzzy search.
892
+ * Steps are always ordered by created_at ASC.
893
+ * Steps do not include output data.
894
+ *
895
+ * @param options - Validated query options including jobId, pagination, and search
896
+ * @returns Promise resolving to steps result with pagination info
897
+ */
898
+ protected abstract _getJobSteps(options: GetJobStepsOptions): Promise<GetJobStepsResult>
899
+
900
+ /**
901
+ * Internal method to get jobs with pagination, filtering, and sorting.
902
+ * Does not include step information or job output.
903
+ *
904
+ * @param options - Validated query options including pagination, filters, and sort
905
+ * @returns Promise resolving to jobs result with pagination info
906
+ */
907
+ protected abstract _getJobs(options?: GetJobsOptions): Promise<GetJobsResult>
908
+
909
+ /**
910
+ * Internal method to get a step by its ID with all information.
911
+ *
912
+ * @param stepId - The validated ID of the step to retrieve
913
+ * @returns Promise resolving to the step, or `null` if not found
914
+ */
915
+ protected abstract _getJobStepById(stepId: string): Promise<JobStep | null>
916
+
917
+ /**
918
+ * Internal method to get job status and updatedAt timestamp.
919
+ *
920
+ * @param jobId - The validated ID of the job
921
+ * @returns Promise resolving to job status result, or `null` if not found
922
+ */
923
+ protected abstract _getJobStatus(jobId: string): Promise<JobStatusResult | null>
924
+
925
+ /**
926
+ * Internal method to get job step status and updatedAt timestamp.
927
+ *
928
+ * @param stepId - The validated ID of the step
929
+ * @returns Promise resolving to step status result, or `null` if not found
930
+ */
931
+ protected abstract _getJobStepStatus(stepId: string): Promise<JobStepStatusResult | null>
932
+
933
+ /**
934
+ * Internal method to get action statistics including counts and last job created date.
935
+ *
936
+ * @returns Promise resolving to action statistics
937
+ */
938
+ protected abstract _getActions(): Promise<GetActionsResult>
939
+
940
+ // ============================================================================
941
+ // Protected Abstract Methods (to be implemented by adapters)
942
+ // ============================================================================
943
+
944
+ /**
945
+ * Start the adapter.
946
+ * Performs any necessary initialization, such as running migrations or setting up listeners.
947
+ *
948
+ * @returns Promise resolving to `void`
949
+ */
950
+ protected abstract _start(): Promise<void>
951
+
952
+ /**
953
+ * Stop the adapter.
954
+ * Performs cleanup, such as closing database connections.
955
+ *
956
+ * @returns Promise resolving to `void`
957
+ */
958
+ protected abstract _stop(): Promise<void>
959
+
960
+ /**
961
+ * Send a notification event.
962
+ * This is adapter-specific (e.g., PostgreSQL NOTIFY).
963
+ *
964
+ * @param event - The event name
965
+ * @param data - The data to send
966
+ * @returns Promise resolving to `void`
967
+ */
968
+ protected abstract _notify(event: string, data: any): Promise<void>
969
+ }