@nicnocquee/dataqueue 1.24.0 → 1.26.0-beta.20260223195940

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 (72) hide show
  1. package/README.md +44 -0
  2. package/ai/build-docs-content.ts +96 -0
  3. package/ai/build-llms-full.ts +42 -0
  4. package/ai/docs-content.json +278 -0
  5. package/ai/rules/advanced.md +132 -0
  6. package/ai/rules/basic.md +159 -0
  7. package/ai/rules/react-dashboard.md +83 -0
  8. package/ai/skills/dataqueue-advanced/SKILL.md +320 -0
  9. package/ai/skills/dataqueue-core/SKILL.md +234 -0
  10. package/ai/skills/dataqueue-react/SKILL.md +189 -0
  11. package/dist/cli.cjs +1149 -14
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.d.cts +66 -1
  14. package/dist/cli.d.ts +66 -1
  15. package/dist/cli.js +1146 -13
  16. package/dist/cli.js.map +1 -1
  17. package/dist/index.cjs +4630 -928
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1033 -15
  20. package/dist/index.d.ts +1033 -15
  21. package/dist/index.js +4626 -929
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp-server.cjs +186 -0
  24. package/dist/mcp-server.cjs.map +1 -0
  25. package/dist/mcp-server.d.cts +32 -0
  26. package/dist/mcp-server.d.ts +32 -0
  27. package/dist/mcp-server.js +175 -0
  28. package/dist/mcp-server.js.map +1 -0
  29. package/migrations/1751131910825_add_timeout_seconds_to_job_queue.sql +2 -2
  30. package/migrations/1751186053000_add_job_events_table.sql +12 -8
  31. package/migrations/1751984773000_add_tags_to_job_queue.sql +1 -1
  32. package/migrations/1765809419000_add_force_kill_on_timeout_to_job_queue.sql +1 -1
  33. package/migrations/1771100000000_add_idempotency_key_to_job_queue.sql +7 -0
  34. package/migrations/1781200000000_add_wait_support.sql +12 -0
  35. package/migrations/1781200000001_create_waitpoints_table.sql +18 -0
  36. package/migrations/1781200000002_add_performance_indexes.sql +34 -0
  37. package/migrations/1781200000003_add_progress_to_job_queue.sql +7 -0
  38. package/migrations/1781200000004_create_cron_schedules_table.sql +33 -0
  39. package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
  40. package/package.json +40 -23
  41. package/src/backend.ts +328 -0
  42. package/src/backends/postgres.ts +2040 -0
  43. package/src/backends/redis-scripts.ts +865 -0
  44. package/src/backends/redis.test.ts +1906 -0
  45. package/src/backends/redis.ts +1792 -0
  46. package/src/cli.test.ts +82 -6
  47. package/src/cli.ts +73 -10
  48. package/src/cron.test.ts +126 -0
  49. package/src/cron.ts +40 -0
  50. package/src/db-util.ts +4 -2
  51. package/src/index.test.ts +688 -1
  52. package/src/index.ts +277 -39
  53. package/src/init-command.test.ts +449 -0
  54. package/src/init-command.ts +709 -0
  55. package/src/install-mcp-command.test.ts +216 -0
  56. package/src/install-mcp-command.ts +185 -0
  57. package/src/install-rules-command.test.ts +218 -0
  58. package/src/install-rules-command.ts +233 -0
  59. package/src/install-skills-command.test.ts +176 -0
  60. package/src/install-skills-command.ts +124 -0
  61. package/src/mcp-server.test.ts +162 -0
  62. package/src/mcp-server.ts +231 -0
  63. package/src/processor.test.ts +559 -18
  64. package/src/processor.ts +456 -49
  65. package/src/queue.test.ts +682 -6
  66. package/src/queue.ts +135 -944
  67. package/src/supervisor.test.ts +340 -0
  68. package/src/supervisor.ts +162 -0
  69. package/src/test-util.ts +32 -0
  70. package/src/types.ts +726 -17
  71. package/src/wait.test.ts +698 -0
  72. package/LICENSE +0 -21
package/src/queue.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Backward-compatible re-exports.
3
+ * All SQL logic has moved to backends/postgres.ts (PostgresBackend class).
4
+ * These functions delegate to a temporary PostgresBackend instance so that
5
+ * any existing internal callers continue to work.
6
+ *
7
+ * Wait-related functions (waitJob, updateStepData, createWaitpoint, etc.)
8
+ * are PostgreSQL-only and use direct SQL queries.
9
+ */
1
10
  import { Pool } from 'pg';
2
11
  import {
3
12
  JobOptions,
@@ -6,146 +15,41 @@ import {
6
15
  JobEvent,
7
16
  JobEventType,
8
17
  TagQueryMode,
18
+ WaitpointRecord,
19
+ AddJobOptions,
9
20
  } from './types.js';
10
- import { log } from './log-context.js';
21
+ import { PostgresBackend } from './backends/postgres.js';
22
+
23
+ /* Thin wrappers — every function creates a lightweight backend wrapper
24
+ around the given pool and forwards the call. The class itself holds
25
+ no mutable state so this is safe and cheap. */
11
26
 
12
- /**
13
- * Record a job event in the job_events table
14
- */
15
27
  export const recordJobEvent = async (
16
28
  pool: Pool,
17
29
  jobId: number,
18
30
  eventType: JobEventType,
19
31
  metadata?: any,
20
- ): Promise<void> => {
21
- const client = await pool.connect();
22
- try {
23
- await client.query(
24
- `INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
25
- [jobId, eventType, metadata ? JSON.stringify(metadata) : null],
26
- );
27
- } catch (error) {
28
- log(`Error recording job event for job ${jobId}: ${error}`);
29
- // Do not throw, to avoid interfering with main job logic
30
- } finally {
31
- client.release();
32
- }
33
- };
32
+ ): Promise<void> =>
33
+ new PostgresBackend(pool).recordJobEvent(jobId, eventType, metadata);
34
34
 
35
- /**
36
- * Add a job to the queue
37
- */
38
35
  export const addJob = async <PayloadMap, T extends keyof PayloadMap & string>(
39
36
  pool: Pool,
40
- {
41
- jobType,
42
- payload,
43
- maxAttempts = 3,
44
- priority = 0,
45
- runAt = null,
46
- timeoutMs = undefined,
47
- forceKillOnTimeout = false,
48
- tags = undefined,
49
- }: JobOptions<PayloadMap, T>,
50
- ): Promise<number> => {
51
- const client = await pool.connect();
52
- try {
53
- let result;
54
- if (runAt) {
55
- result = await client.query(
56
- `INSERT INTO job_queue
57
- (job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags)
58
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
59
- RETURNING id`,
60
- [
61
- jobType,
62
- payload,
63
- maxAttempts,
64
- priority,
65
- runAt,
66
- timeoutMs ?? null,
67
- forceKillOnTimeout ?? false,
68
- tags ?? null,
69
- ],
70
- );
71
- log(
72
- `Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, runAt ${runAt.toISOString()}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}, tags ${JSON.stringify(tags)}`,
73
- );
74
- } else {
75
- result = await client.query(
76
- `INSERT INTO job_queue
77
- (job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags)
78
- VALUES ($1, $2, $3, $4, $5, $6, $7)
79
- RETURNING id`,
80
- [
81
- jobType,
82
- payload,
83
- maxAttempts,
84
- priority,
85
- timeoutMs ?? null,
86
- forceKillOnTimeout ?? false,
87
- tags ?? null,
88
- ],
89
- );
90
- log(
91
- `Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}, tags ${JSON.stringify(tags)}`,
92
- );
93
- }
94
- await recordJobEvent(pool, result.rows[0].id, JobEventType.Added, {
95
- jobType,
96
- payload,
97
- tags,
98
- });
99
- return result.rows[0].id;
100
- } catch (error) {
101
- log(`Error adding job: ${error}`);
102
- throw error;
103
- } finally {
104
- client.release();
105
- }
106
- };
37
+ job: JobOptions<PayloadMap, T>,
38
+ options?: AddJobOptions,
39
+ ): Promise<number> => new PostgresBackend(pool).addJob(job, options);
40
+
41
+ export const addJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
42
+ pool: Pool,
43
+ jobs: JobOptions<PayloadMap, T>[],
44
+ options?: AddJobOptions,
45
+ ): Promise<number[]> => new PostgresBackend(pool).addJobs(jobs, options);
107
46
 
108
- /**
109
- * Get a job by ID
110
- */
111
47
  export const getJob = async <PayloadMap, T extends keyof PayloadMap & string>(
112
48
  pool: Pool,
113
49
  id: number,
114
- ): Promise<JobRecord<PayloadMap, T> | null> => {
115
- const client = await pool.connect();
116
- try {
117
- const result = await client.query(
118
- `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags FROM job_queue WHERE id = $1`,
119
- [id],
120
- );
121
-
122
- if (result.rows.length === 0) {
123
- log(`Job ${id} not found`);
124
- return null;
125
- }
50
+ ): Promise<JobRecord<PayloadMap, T> | null> =>
51
+ new PostgresBackend(pool).getJob<PayloadMap, T>(id);
126
52
 
127
- log(`Found job ${id}`);
128
-
129
- const job = result.rows[0] as JobRecord<PayloadMap, T>;
130
-
131
- return {
132
- ...job,
133
- payload: job.payload,
134
- timeoutMs: job.timeoutMs,
135
- forceKillOnTimeout: job.forceKillOnTimeout,
136
- failureReason: job.failureReason,
137
- };
138
- } catch (error) {
139
- log(`Error getting job ${id}: ${error}`);
140
- throw error;
141
- } finally {
142
- client.release();
143
- }
144
- };
145
-
146
- /**
147
- * Get jobs by status
148
- */
149
53
  export const getJobsByStatus = async <
150
54
  PayloadMap,
151
55
  T extends keyof PayloadMap & string,
@@ -154,38 +58,13 @@ export const getJobsByStatus = async <
154
58
  status: string,
155
59
  limit = 100,
156
60
  offset = 0,
157
- ): Promise<JobRecord<PayloadMap, T>[]> => {
158
- const client = await pool.connect();
159
- try {
160
- const result = await client.query(
161
- `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason" FROM job_queue WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
162
- [status, limit, offset],
163
- );
164
-
165
- log(`Found ${result.rows.length} jobs by status ${status}`);
166
-
167
- return result.rows.map((job) => ({
168
- ...job,
169
- payload: job.payload,
170
- timeoutMs: job.timeoutMs,
171
- forceKillOnTimeout: job.forceKillOnTimeout,
172
- failureReason: job.failureReason,
173
- }));
174
- } catch (error) {
175
- log(`Error getting jobs by status ${status}: ${error}`);
176
- throw error;
177
- } finally {
178
- client.release();
179
- }
180
- };
61
+ ): Promise<JobRecord<PayloadMap, T>[]> =>
62
+ new PostgresBackend(pool).getJobsByStatus<PayloadMap, T>(
63
+ status,
64
+ limit,
65
+ offset,
66
+ );
181
67
 
182
- /**
183
- * Get the next batch of jobs to process
184
- * @param pool - The database pool
185
- * @param workerId - The worker ID
186
- * @param batchSize - The batch size
187
- * @param jobType - Only fetch jobs with this job type (string or array of strings)
188
- */
189
68
  export const getNextBatch = async <
190
69
  PayloadMap,
191
70
  T extends keyof PayloadMap & string,
@@ -194,234 +73,40 @@ export const getNextBatch = async <
194
73
  workerId: string,
195
74
  batchSize = 10,
196
75
  jobType?: string | string[],
197
- ): Promise<JobRecord<PayloadMap, T>[]> => {
198
- const client = await pool.connect();
199
- try {
200
- // Begin transaction
201
- await client.query('BEGIN');
202
-
203
- // Build job type filter
204
- let jobTypeFilter = '';
205
- let params: any[] = [workerId, batchSize];
206
- if (jobType) {
207
- if (Array.isArray(jobType)) {
208
- jobTypeFilter = ` AND job_type = ANY($3)`;
209
- params.push(jobType);
210
- } else {
211
- jobTypeFilter = ` AND job_type = $3`;
212
- params.push(jobType);
213
- }
214
- }
215
-
216
- // Get and lock a batch of jobs
217
- const result = await client.query(
218
- `
219
- UPDATE job_queue
220
- SET status = 'processing',
221
- locked_at = NOW(),
222
- locked_by = $1,
223
- attempts = attempts + 1,
224
- updated_at = NOW(),
225
- pending_reason = NULL,
226
- started_at = COALESCE(started_at, NOW()),
227
- last_retried_at = CASE WHEN attempts > 0 THEN NOW() ELSE last_retried_at END
228
- WHERE id IN (
229
- SELECT id FROM job_queue
230
- WHERE (status = 'pending' OR (status = 'failed' AND next_attempt_at <= NOW()))
231
- AND (attempts < max_attempts)
232
- AND run_at <= NOW()
233
- ${jobTypeFilter}
234
- ORDER BY priority DESC, created_at ASC
235
- LIMIT $2
236
- FOR UPDATE SKIP LOCKED
237
- )
238
- RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason"
239
- `,
240
- params,
241
- );
242
-
243
- log(`Found ${result.rows.length} jobs to process`);
244
-
245
- // Commit transaction
246
- await client.query('COMMIT');
76
+ ): Promise<JobRecord<PayloadMap, T>[]> =>
77
+ new PostgresBackend(pool).getNextBatch<PayloadMap, T>(
78
+ workerId,
79
+ batchSize,
80
+ jobType,
81
+ );
247
82
 
248
- // Record processing event for each job
249
- for (const row of result.rows) {
250
- await recordJobEvent(pool, row.id, JobEventType.Processing);
251
- }
83
+ export const completeJob = async (pool: Pool, jobId: number): Promise<void> =>
84
+ new PostgresBackend(pool).completeJob(jobId);
252
85
 
253
- return result.rows.map((job) => ({
254
- ...job,
255
- payload: job.payload,
256
- timeoutMs: job.timeoutMs,
257
- forceKillOnTimeout: job.forceKillOnTimeout,
258
- }));
259
- } catch (error) {
260
- log(`Error getting next batch: ${error}`);
261
- await client.query('ROLLBACK');
262
- throw error;
263
- } finally {
264
- client.release();
265
- }
266
- };
86
+ export const prolongJob = async (pool: Pool, jobId: number): Promise<void> =>
87
+ new PostgresBackend(pool).prolongJob(jobId);
267
88
 
268
- /**
269
- * Mark a job as completed
270
- */
271
- export const completeJob = async (pool: Pool, jobId: number): Promise<void> => {
272
- const client = await pool.connect();
273
- try {
274
- await client.query(
275
- `
276
- UPDATE job_queue
277
- SET status = 'completed', updated_at = NOW(), completed_at = NOW()
278
- WHERE id = $1
279
- `,
280
- [jobId],
281
- );
282
- await recordJobEvent(pool, jobId, JobEventType.Completed);
283
- } catch (error) {
284
- log(`Error completing job ${jobId}: ${error}`);
285
- throw error;
286
- } finally {
287
- log(`Completed job ${jobId}`);
288
- client.release();
289
- }
290
- };
291
-
292
- /**
293
- * Mark a job as failed
294
- */
295
89
  export const failJob = async (
296
90
  pool: Pool,
297
91
  jobId: number,
298
92
  error: Error,
299
93
  failureReason?: FailureReason,
300
- ): Promise<void> => {
301
- const client = await pool.connect();
302
- try {
303
- /**
304
- * The next attempt will be scheduled after `2^attempts * 1 minute` from the last attempt.
305
- */
306
- await client.query(
307
- `
308
- UPDATE job_queue
309
- SET status = 'failed',
310
- updated_at = NOW(),
311
- next_attempt_at = CASE
312
- WHEN attempts < max_attempts THEN NOW() + (POWER(2, attempts) * INTERVAL '1 minute')
313
- ELSE NULL
314
- END,
315
- error_history = COALESCE(error_history, '[]'::jsonb) || $2::jsonb,
316
- failure_reason = $3,
317
- last_failed_at = NOW()
318
- WHERE id = $1
319
- `,
320
- [
321
- jobId,
322
- JSON.stringify([
323
- {
324
- message: error.message || String(error),
325
- timestamp: new Date().toISOString(),
326
- },
327
- ]),
328
- failureReason ?? null,
329
- ],
330
- );
331
- await recordJobEvent(pool, jobId, JobEventType.Failed, {
332
- message: error.message || String(error),
333
- failureReason,
334
- });
335
- } catch (error) {
336
- log(`Error failing job ${jobId}: ${error}`);
337
- throw error;
338
- } finally {
339
- log(`Failed job ${jobId}`);
340
- client.release();
341
- }
342
- };
94
+ ): Promise<void> =>
95
+ new PostgresBackend(pool).failJob(jobId, error, failureReason);
343
96
 
344
- /**
345
- * Retry a failed job immediately
346
- */
347
- export const retryJob = async (pool: Pool, jobId: number): Promise<void> => {
348
- const client = await pool.connect();
349
- try {
350
- await client.query(
351
- `
352
- UPDATE job_queue
353
- SET status = 'pending',
354
- updated_at = NOW(),
355
- locked_at = NULL,
356
- locked_by = NULL,
357
- next_attempt_at = NOW(),
358
- last_retried_at = NOW()
359
- WHERE id = $1
360
- `,
361
- [jobId],
362
- );
363
- await recordJobEvent(pool, jobId, JobEventType.Retried);
364
- } catch (error) {
365
- log(`Error retrying job ${jobId}: ${error}`);
366
- throw error;
367
- } finally {
368
- log(`Retried job ${jobId}`);
369
- client.release();
370
- }
371
- };
97
+ export const retryJob = async (pool: Pool, jobId: number): Promise<void> =>
98
+ new PostgresBackend(pool).retryJob(jobId);
372
99
 
373
- /**
374
- * Delete old completed jobs
375
- */
376
100
  export const cleanupOldJobs = async (
377
101
  pool: Pool,
378
102
  daysToKeep = 30,
379
- ): Promise<number> => {
380
- const client = await pool.connect();
381
- try {
382
- const result = await client.query(`
383
- DELETE FROM job_queue
384
- WHERE status = 'completed'
385
- AND updated_at < NOW() - INTERVAL '${daysToKeep} days'
386
- RETURNING id
387
- `);
388
- log(`Deleted ${result.rowCount} old jobs`);
389
- return result.rowCount || 0;
390
- } catch (error) {
391
- log(`Error cleaning up old jobs: ${error}`);
392
- throw error;
393
- } finally {
394
- client.release();
395
- }
396
- };
103
+ batchSize = 1000,
104
+ ): Promise<number> =>
105
+ new PostgresBackend(pool).cleanupOldJobs(daysToKeep, batchSize);
397
106
 
398
- /**
399
- * Cancel a scheduled job (only if still pending)
400
- */
401
- export const cancelJob = async (pool: Pool, jobId: number): Promise<void> => {
402
- const client = await pool.connect();
403
- try {
404
- await client.query(
405
- `
406
- UPDATE job_queue
407
- SET status = 'cancelled', updated_at = NOW(), last_cancelled_at = NOW()
408
- WHERE id = $1 AND status = 'pending'
409
- `,
410
- [jobId],
411
- );
412
- await recordJobEvent(pool, jobId, JobEventType.Cancelled);
413
- } catch (error) {
414
- log(`Error cancelling job ${jobId}: ${error}`);
415
- throw error;
416
- } finally {
417
- log(`Cancelled job ${jobId}`);
418
- client.release();
419
- }
420
- };
107
+ export const cancelJob = async (pool: Pool, jobId: number): Promise<void> =>
108
+ new PostgresBackend(pool).cancelJob(jobId);
421
109
 
422
- /**
423
- * Edit a pending job (only if still pending)
424
- */
425
110
  export const editJob = async <PayloadMap, T extends keyof PayloadMap & string>(
426
111
  pool: Pool,
427
112
  jobId: number,
@@ -432,88 +117,12 @@ export const editJob = async <PayloadMap, T extends keyof PayloadMap & string>(
432
117
  runAt?: Date | null;
433
118
  timeoutMs?: number | null;
434
119
  tags?: string[] | null;
120
+ retryDelay?: number | null;
121
+ retryBackoff?: boolean | null;
122
+ retryDelayMax?: number | null;
435
123
  },
436
- ): Promise<void> => {
437
- const client = await pool.connect();
438
- try {
439
- const updateFields: string[] = [];
440
- const params: any[] = [];
441
- let paramIdx = 1;
442
-
443
- // Build dynamic UPDATE query based on provided fields
444
- if (updates.payload !== undefined) {
445
- updateFields.push(`payload = $${paramIdx++}`);
446
- params.push(updates.payload);
447
- }
448
- if (updates.maxAttempts !== undefined) {
449
- updateFields.push(`max_attempts = $${paramIdx++}`);
450
- params.push(updates.maxAttempts);
451
- }
452
- if (updates.priority !== undefined) {
453
- updateFields.push(`priority = $${paramIdx++}`);
454
- params.push(updates.priority);
455
- }
456
- if (updates.runAt !== undefined) {
457
- if (updates.runAt === null) {
458
- // null means run now (use current timestamp)
459
- updateFields.push(`run_at = NOW()`);
460
- } else {
461
- updateFields.push(`run_at = $${paramIdx++}`);
462
- params.push(updates.runAt);
463
- }
464
- }
465
- if (updates.timeoutMs !== undefined) {
466
- updateFields.push(`timeout_ms = $${paramIdx++}`);
467
- params.push(updates.timeoutMs ?? null);
468
- }
469
- if (updates.tags !== undefined) {
470
- updateFields.push(`tags = $${paramIdx++}`);
471
- params.push(updates.tags ?? null);
472
- }
473
-
474
- // If no fields to update, return early
475
- if (updateFields.length === 0) {
476
- log(`No fields to update for job ${jobId}`);
477
- return;
478
- }
479
-
480
- // Always update updated_at timestamp
481
- updateFields.push(`updated_at = NOW()`);
482
-
483
- // Add jobId as the last parameter for WHERE clause
484
- params.push(jobId);
485
-
486
- const query = `
487
- UPDATE job_queue
488
- SET ${updateFields.join(', ')}
489
- WHERE id = $${paramIdx} AND status = 'pending'
490
- `;
124
+ ): Promise<void> => new PostgresBackend(pool).editJob(jobId, updates);
491
125
 
492
- await client.query(query, params);
493
-
494
- // Record edit event with metadata containing updated fields
495
- const metadata: any = {};
496
- if (updates.payload !== undefined) metadata.payload = updates.payload;
497
- if (updates.maxAttempts !== undefined)
498
- metadata.maxAttempts = updates.maxAttempts;
499
- if (updates.priority !== undefined) metadata.priority = updates.priority;
500
- if (updates.runAt !== undefined) metadata.runAt = updates.runAt;
501
- if (updates.timeoutMs !== undefined) metadata.timeoutMs = updates.timeoutMs;
502
- if (updates.tags !== undefined) metadata.tags = updates.tags;
503
-
504
- await recordJobEvent(pool, jobId, JobEventType.Edited, metadata);
505
- log(`Edited job ${jobId}: ${JSON.stringify(metadata)}`);
506
- } catch (error) {
507
- log(`Error editing job ${jobId}: ${error}`);
508
- throw error;
509
- } finally {
510
- client.release();
511
- }
512
- };
513
-
514
- /**
515
- * Edit all pending jobs matching the filters
516
- */
517
126
  export const editAllPendingJobs = async <
518
127
  PayloadMap,
519
128
  T extends keyof PayloadMap & string,
@@ -528,7 +137,7 @@ export const editAllPendingJobs = async <
528
137
  | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
529
138
  tags?: { values: string[]; mode?: TagQueryMode };
530
139
  }
531
- | undefined = undefined,
140
+ | undefined,
532
141
  updates: {
533
142
  payload?: PayloadMap[T];
534
143
  maxAttempts?: number;
@@ -536,160 +145,13 @@ export const editAllPendingJobs = async <
536
145
  runAt?: Date | null;
537
146
  timeoutMs?: number;
538
147
  tags?: string[];
148
+ retryDelay?: number | null;
149
+ retryBackoff?: boolean | null;
150
+ retryDelayMax?: number | null;
539
151
  },
540
- ): Promise<number> => {
541
- const client = await pool.connect();
542
- try {
543
- // Build SET clause from updates
544
- const updateFields: string[] = [];
545
- const params: any[] = [];
546
- let paramIdx = 1;
547
-
548
- if (updates.payload !== undefined) {
549
- updateFields.push(`payload = $${paramIdx++}`);
550
- params.push(updates.payload);
551
- }
552
- if (updates.maxAttempts !== undefined) {
553
- updateFields.push(`max_attempts = $${paramIdx++}`);
554
- params.push(updates.maxAttempts);
555
- }
556
- if (updates.priority !== undefined) {
557
- updateFields.push(`priority = $${paramIdx++}`);
558
- params.push(updates.priority);
559
- }
560
- if (updates.runAt !== undefined) {
561
- if (updates.runAt === null) {
562
- // null means run now (use current timestamp)
563
- updateFields.push(`run_at = NOW()`);
564
- } else {
565
- updateFields.push(`run_at = $${paramIdx++}`);
566
- params.push(updates.runAt);
567
- }
568
- }
569
- if (updates.timeoutMs !== undefined) {
570
- updateFields.push(`timeout_ms = $${paramIdx++}`);
571
- params.push(updates.timeoutMs ?? null);
572
- }
573
- if (updates.tags !== undefined) {
574
- updateFields.push(`tags = $${paramIdx++}`);
575
- params.push(updates.tags ?? null);
576
- }
577
-
578
- // If no fields to update, return early
579
- if (updateFields.length === 0) {
580
- log(`No fields to update for batch edit`);
581
- return 0;
582
- }
583
-
584
- // Always update updated_at timestamp
585
- updateFields.push(`updated_at = NOW()`);
586
-
587
- // Build WHERE clause from filters
588
- let query = `
589
- UPDATE job_queue
590
- SET ${updateFields.join(', ')}
591
- WHERE status = 'pending'`;
592
-
593
- if (filters) {
594
- if (filters.jobType) {
595
- query += ` AND job_type = $${paramIdx++}`;
596
- params.push(filters.jobType);
597
- }
598
- if (filters.priority !== undefined) {
599
- query += ` AND priority = $${paramIdx++}`;
600
- params.push(filters.priority);
601
- }
602
- if (filters.runAt) {
603
- if (filters.runAt instanceof Date) {
604
- query += ` AND run_at = $${paramIdx++}`;
605
- params.push(filters.runAt);
606
- } else if (typeof filters.runAt === 'object') {
607
- const ops = filters.runAt;
608
- if (ops.gt) {
609
- query += ` AND run_at > $${paramIdx++}`;
610
- params.push(ops.gt);
611
- }
612
- if (ops.gte) {
613
- query += ` AND run_at >= $${paramIdx++}`;
614
- params.push(ops.gte);
615
- }
616
- if (ops.lt) {
617
- query += ` AND run_at < $${paramIdx++}`;
618
- params.push(ops.lt);
619
- }
620
- if (ops.lte) {
621
- query += ` AND run_at <= $${paramIdx++}`;
622
- params.push(ops.lte);
623
- }
624
- if (ops.eq) {
625
- query += ` AND run_at = $${paramIdx++}`;
626
- params.push(ops.eq);
627
- }
628
- }
629
- }
630
- if (
631
- filters.tags &&
632
- filters.tags.values &&
633
- filters.tags.values.length > 0
634
- ) {
635
- const mode = filters.tags.mode || 'all';
636
- const tagValues = filters.tags.values;
637
- switch (mode) {
638
- case 'exact':
639
- query += ` AND tags = $${paramIdx++}`;
640
- params.push(tagValues);
641
- break;
642
- case 'all':
643
- query += ` AND tags @> $${paramIdx++}`;
644
- params.push(tagValues);
645
- break;
646
- case 'any':
647
- query += ` AND tags && $${paramIdx++}`;
648
- params.push(tagValues);
649
- break;
650
- case 'none':
651
- query += ` AND NOT (tags && $${paramIdx++})`;
652
- params.push(tagValues);
653
- break;
654
- default:
655
- query += ` AND tags @> $${paramIdx++}`;
656
- params.push(tagValues);
657
- }
658
- }
659
- }
660
- query += '\nRETURNING id';
661
-
662
- const result = await client.query(query, params);
663
- const editedCount = result.rowCount || 0;
664
-
665
- // Record edit event with metadata containing updated fields for each job
666
- const metadata: any = {};
667
- if (updates.payload !== undefined) metadata.payload = updates.payload;
668
- if (updates.maxAttempts !== undefined)
669
- metadata.maxAttempts = updates.maxAttempts;
670
- if (updates.priority !== undefined) metadata.priority = updates.priority;
671
- if (updates.runAt !== undefined) metadata.runAt = updates.runAt;
672
- if (updates.timeoutMs !== undefined) metadata.timeoutMs = updates.timeoutMs;
673
- if (updates.tags !== undefined) metadata.tags = updates.tags;
152
+ ): Promise<number> =>
153
+ new PostgresBackend(pool).editAllPendingJobs(filters, updates);
674
154
 
675
- // Record events for each affected job
676
- for (const row of result.rows) {
677
- await recordJobEvent(pool, row.id, JobEventType.Edited, metadata);
678
- }
679
-
680
- log(`Edited ${editedCount} pending jobs: ${JSON.stringify(metadata)}`);
681
- return editedCount;
682
- } catch (error) {
683
- log(`Error editing pending jobs: ${error}`);
684
- throw error;
685
- } finally {
686
- client.release();
687
- }
688
- };
689
-
690
- /**
691
- * Cancel all upcoming jobs (pending and scheduled in the future) with optional filters
692
- */
693
155
  export const cancelAllUpcomingJobs = async (
694
156
  pool: Pool,
695
157
  filters?: {
@@ -698,97 +160,8 @@ export const cancelAllUpcomingJobs = async (
698
160
  runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
699
161
  tags?: { values: string[]; mode?: TagQueryMode };
700
162
  },
701
- ): Promise<number> => {
702
- const client = await pool.connect();
703
- try {
704
- let query = `
705
- UPDATE job_queue
706
- SET status = 'cancelled', updated_at = NOW()
707
- WHERE status = 'pending'`;
708
- const params: any[] = [];
709
- let paramIdx = 1;
710
- if (filters) {
711
- if (filters.jobType) {
712
- query += ` AND job_type = $${paramIdx++}`;
713
- params.push(filters.jobType);
714
- }
715
- if (filters.priority !== undefined) {
716
- query += ` AND priority = $${paramIdx++}`;
717
- params.push(filters.priority);
718
- }
719
- if (filters.runAt) {
720
- if (filters.runAt instanceof Date) {
721
- query += ` AND run_at = $${paramIdx++}`;
722
- params.push(filters.runAt);
723
- } else if (typeof filters.runAt === 'object') {
724
- const ops = filters.runAt;
725
- if (ops.gt) {
726
- query += ` AND run_at > $${paramIdx++}`;
727
- params.push(ops.gt);
728
- }
729
- if (ops.gte) {
730
- query += ` AND run_at >= $${paramIdx++}`;
731
- params.push(ops.gte);
732
- }
733
- if (ops.lt) {
734
- query += ` AND run_at < $${paramIdx++}`;
735
- params.push(ops.lt);
736
- }
737
- if (ops.lte) {
738
- query += ` AND run_at <= $${paramIdx++}`;
739
- params.push(ops.lte);
740
- }
741
- if (ops.eq) {
742
- query += ` AND run_at = $${paramIdx++}`;
743
- params.push(ops.eq);
744
- }
745
- }
746
- }
747
- if (
748
- filters.tags &&
749
- filters.tags.values &&
750
- filters.tags.values.length > 0
751
- ) {
752
- const mode = filters.tags.mode || 'all';
753
- const tagValues = filters.tags.values;
754
- switch (mode) {
755
- case 'exact':
756
- query += ` AND tags = $${paramIdx++}`;
757
- params.push(tagValues);
758
- break;
759
- case 'all':
760
- query += ` AND tags @> $${paramIdx++}`;
761
- params.push(tagValues);
762
- break;
763
- case 'any':
764
- query += ` AND tags && $${paramIdx++}`;
765
- params.push(tagValues);
766
- break;
767
- case 'none':
768
- query += ` AND NOT (tags && $${paramIdx++})`;
769
- params.push(tagValues);
770
- break;
771
- default:
772
- query += ` AND tags @> $${paramIdx++}`;
773
- params.push(tagValues);
774
- }
775
- }
776
- }
777
- query += '\nRETURNING id';
778
- const result = await client.query(query, params);
779
- log(`Cancelled ${result.rowCount} jobs`);
780
- return result.rowCount || 0;
781
- } catch (error) {
782
- log(`Error cancelling upcoming jobs: ${error}`);
783
- throw error;
784
- } finally {
785
- client.release();
786
- }
787
- };
163
+ ): Promise<number> => new PostgresBackend(pool).cancelAllUpcomingJobs(filters);
788
164
 
789
- /**
790
- * Get all jobs with optional pagination
791
- */
792
165
  export const getAllJobs = async <
793
166
  PayloadMap,
794
167
  T extends keyof PayloadMap & string,
@@ -796,113 +169,27 @@ export const getAllJobs = async <
796
169
  pool: Pool,
797
170
  limit = 100,
798
171
  offset = 0,
799
- ): Promise<JobRecord<PayloadMap, T>[]> => {
800
- const client = await pool.connect();
801
- try {
802
- const result = await client.query(
803
- `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason" FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
804
- [limit, offset],
805
- );
806
- log(`Found ${result.rows.length} jobs (all)`);
807
- return result.rows.map((job) => ({
808
- ...job,
809
- payload: job.payload,
810
- timeoutMs: job.timeoutMs,
811
- forceKillOnTimeout: job.forceKillOnTimeout,
812
- }));
813
- } catch (error) {
814
- log(`Error getting all jobs: ${error}`);
815
- throw error;
816
- } finally {
817
- client.release();
818
- }
819
- };
172
+ ): Promise<JobRecord<PayloadMap, T>[]> =>
173
+ new PostgresBackend(pool).getAllJobs<PayloadMap, T>(limit, offset);
820
174
 
821
- /**
822
- * Set a pending reason for unpicked jobs
823
- */
824
175
  export const setPendingReasonForUnpickedJobs = async (
825
176
  pool: Pool,
826
177
  reason: string,
827
178
  jobType?: string | string[],
828
- ) => {
829
- const client = await pool.connect();
830
- try {
831
- let jobTypeFilter = '';
832
- let params: any[] = [reason];
833
- if (jobType) {
834
- if (Array.isArray(jobType)) {
835
- jobTypeFilter = ` AND job_type = ANY($2)`;
836
- params.push(jobType);
837
- } else {
838
- jobTypeFilter = ` AND job_type = $2`;
839
- params.push(jobType);
840
- }
841
- }
842
- await client.query(
843
- `UPDATE job_queue SET pending_reason = $1 WHERE status = 'pending'${jobTypeFilter}`,
844
- params,
845
- );
846
- } finally {
847
- client.release();
848
- }
849
- };
179
+ ): Promise<void> =>
180
+ new PostgresBackend(pool).setPendingReasonForUnpickedJobs(reason, jobType);
850
181
 
851
- /**
852
- * Reclaim jobs stuck in 'processing' for too long.
853
- *
854
- * If a process (e.g., API route or worker) crashes after marking a job as 'processing' but before completing it, the job can remain stuck in the 'processing' state indefinitely. This can happen if the process is killed or encounters an unhandled error after updating the job status but before marking it as 'completed' or 'failed'.
855
- * @param pool - The database pool
856
- * @param maxProcessingTimeMinutes - Max allowed processing time in minutes (default: 10)
857
- * @returns Number of jobs reclaimed
858
- */
859
182
  export const reclaimStuckJobs = async (
860
183
  pool: Pool,
861
184
  maxProcessingTimeMinutes = 10,
862
- ): Promise<number> => {
863
- const client = await pool.connect();
864
- try {
865
- const result = await client.query(
866
- `
867
- UPDATE job_queue
868
- SET status = 'pending', locked_at = NULL, locked_by = NULL, updated_at = NOW()
869
- WHERE status = 'processing'
870
- AND locked_at < NOW() - INTERVAL '${maxProcessingTimeMinutes} minutes'
871
- RETURNING id
872
- `,
873
- );
874
- log(`Reclaimed ${result.rowCount} stuck jobs`);
875
- return result.rowCount || 0;
876
- } catch (error) {
877
- log(`Error reclaiming stuck jobs: ${error}`);
878
- throw error;
879
- } finally {
880
- client.release();
881
- }
882
- };
185
+ ): Promise<number> =>
186
+ new PostgresBackend(pool).reclaimStuckJobs(maxProcessingTimeMinutes);
883
187
 
884
- /**
885
- * Get all events for a job, ordered by createdAt ascending
886
- */
887
188
  export const getJobEvents = async (
888
189
  pool: Pool,
889
190
  jobId: number,
890
- ): Promise<JobEvent[]> => {
891
- const client = await pool.connect();
892
- try {
893
- const res = await client.query(
894
- `SELECT id, job_id AS "jobId", event_type AS "eventType", metadata, created_at AS "createdAt" FROM job_events WHERE job_id = $1 ORDER BY created_at ASC`,
895
- [jobId],
896
- );
897
- return res.rows as JobEvent[];
898
- } finally {
899
- client.release();
900
- }
901
- };
191
+ ): Promise<JobEvent[]> => new PostgresBackend(pool).getJobEvents(jobId);
902
192
 
903
- /**
904
- * Get jobs by tags (matches all specified tags)
905
- */
906
193
  export const getJobsByTags = async <
907
194
  PayloadMap,
908
195
  T extends keyof PayloadMap & string,
@@ -912,55 +199,13 @@ export const getJobsByTags = async <
912
199
  mode: TagQueryMode = 'all',
913
200
  limit = 100,
914
201
  offset = 0,
915
- ): Promise<JobRecord<PayloadMap, T>[]> => {
916
- const client = await pool.connect();
917
- try {
918
- let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags
919
- FROM job_queue`;
920
- let params: any[] = [];
921
- switch (mode) {
922
- case 'exact':
923
- query += ' WHERE tags = $1';
924
- params = [tags];
925
- break;
926
- case 'all':
927
- query += ' WHERE tags @> $1';
928
- params = [tags];
929
- break;
930
- case 'any':
931
- query += ' WHERE tags && $1';
932
- params = [tags];
933
- break;
934
- case 'none':
935
- query += ' WHERE NOT (tags && $1)';
936
- params = [tags];
937
- break;
938
- default:
939
- query += ' WHERE tags @> $1';
940
- params = [tags];
941
- }
942
- query += ' ORDER BY created_at DESC LIMIT $2 OFFSET $3';
943
- params.push(limit, offset);
944
- const result = await client.query(query, params);
945
- log(
946
- `Found ${result.rows.length} jobs by tags ${JSON.stringify(tags)} (mode: ${mode})`,
947
- );
948
- return result.rows.map((job) => ({
949
- ...job,
950
- payload: job.payload,
951
- timeoutMs: job.timeoutMs,
952
- forceKillOnTimeout: job.forceKillOnTimeout,
953
- failureReason: job.failureReason,
954
- }));
955
- } catch (error) {
956
- log(
957
- `Error getting jobs by tags ${JSON.stringify(tags)} (mode: ${mode}): ${error}`,
958
- );
959
- throw error;
960
- } finally {
961
- client.release();
962
- }
963
- };
202
+ ): Promise<JobRecord<PayloadMap, T>[]> =>
203
+ new PostgresBackend(pool).getJobsByTags<PayloadMap, T>(
204
+ tags,
205
+ mode,
206
+ limit,
207
+ offset,
208
+ );
964
209
 
965
210
  export const getJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
966
211
  pool: Pool,
@@ -972,113 +217,59 @@ export const getJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
972
217
  },
973
218
  limit = 100,
974
219
  offset = 0,
975
- ): Promise<JobRecord<PayloadMap, T>[]> => {
976
- const client = await pool.connect();
977
- try {
978
- let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags FROM job_queue`;
979
- const params: any[] = [];
980
- let where: string[] = [];
981
- let paramIdx = 1;
982
- if (filters) {
983
- if (filters.jobType) {
984
- where.push(`job_type = $${paramIdx++}`);
985
- params.push(filters.jobType);
986
- }
987
- if (filters.priority !== undefined) {
988
- where.push(`priority = $${paramIdx++}`);
989
- params.push(filters.priority);
990
- }
991
- if (filters.runAt) {
992
- if (filters.runAt instanceof Date) {
993
- where.push(`run_at = $${paramIdx++}`);
994
- params.push(filters.runAt);
995
- } else if (
996
- typeof filters.runAt === 'object' &&
997
- (filters.runAt.gt !== undefined ||
998
- filters.runAt.gte !== undefined ||
999
- filters.runAt.lt !== undefined ||
1000
- filters.runAt.lte !== undefined ||
1001
- filters.runAt.eq !== undefined)
1002
- ) {
1003
- const ops = filters.runAt as {
1004
- gt?: Date;
1005
- gte?: Date;
1006
- lt?: Date;
1007
- lte?: Date;
1008
- eq?: Date;
1009
- };
1010
- if (ops.gt) {
1011
- where.push(`run_at > $${paramIdx++}`);
1012
- params.push(ops.gt);
1013
- }
1014
- if (ops.gte) {
1015
- where.push(`run_at >= $${paramIdx++}`);
1016
- params.push(ops.gte);
1017
- }
1018
- if (ops.lt) {
1019
- where.push(`run_at < $${paramIdx++}`);
1020
- params.push(ops.lt);
1021
- }
1022
- if (ops.lte) {
1023
- where.push(`run_at <= $${paramIdx++}`);
1024
- params.push(ops.lte);
1025
- }
1026
- if (ops.eq) {
1027
- where.push(`run_at = $${paramIdx++}`);
1028
- params.push(ops.eq);
1029
- }
1030
- }
1031
- }
1032
- if (
1033
- filters.tags &&
1034
- filters.tags.values &&
1035
- filters.tags.values.length > 0
1036
- ) {
1037
- const mode = filters.tags.mode || 'all';
1038
- const tagValues = filters.tags.values;
1039
- switch (mode) {
1040
- case 'exact':
1041
- where.push(`tags = $${paramIdx++}`);
1042
- params.push(tagValues);
1043
- break;
1044
- case 'all':
1045
- where.push(`tags @> $${paramIdx++}`);
1046
- params.push(tagValues);
1047
- break;
1048
- case 'any':
1049
- where.push(`tags && $${paramIdx++}`);
1050
- params.push(tagValues);
1051
- break;
1052
- case 'none':
1053
- where.push(`NOT (tags && $${paramIdx++})`);
1054
- params.push(tagValues);
1055
- break;
1056
- default:
1057
- where.push(`tags @> $${paramIdx++}`);
1058
- params.push(tagValues);
1059
- }
1060
- }
1061
- }
1062
- if (where.length > 0) {
1063
- query += ` WHERE ${where.join(' AND ')}`;
1064
- }
1065
- // Always add LIMIT and OFFSET as the last parameters
1066
- paramIdx = params.length + 1;
1067
- query += ` ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx}`;
1068
- params.push(limit, offset);
1069
- const result = await client.query(query, params);
1070
- log(`Found ${result.rows.length} jobs`);
1071
- return result.rows.map((job) => ({
1072
- ...job,
1073
- payload: job.payload,
1074
- timeoutMs: job.timeoutMs,
1075
- forceKillOnTimeout: job.forceKillOnTimeout,
1076
- failureReason: job.failureReason,
1077
- }));
1078
- } catch (error) {
1079
- log(`Error getting jobs: ${error}`);
1080
- throw error;
1081
- } finally {
1082
- client.release();
1083
- }
1084
- };
220
+ ): Promise<JobRecord<PayloadMap, T>[]> =>
221
+ new PostgresBackend(pool).getJobs<PayloadMap, T>(filters, limit, offset);
222
+
223
+ // ── Progress ──────────────────────────────────────────────────────────────────
224
+
225
+ export const updateProgress = async (
226
+ pool: Pool,
227
+ jobId: number,
228
+ progress: number,
229
+ ): Promise<void> => new PostgresBackend(pool).updateProgress(jobId, progress);
230
+
231
+ // ── Wait support functions (backward-compatible delegates) ────────────────────
232
+
233
+ /** @deprecated Use backend.waitJob() directly. Delegates to PostgresBackend. */
234
+ export const waitJob = async (
235
+ pool: Pool,
236
+ jobId: number,
237
+ options: {
238
+ waitUntil?: Date;
239
+ waitTokenId?: string;
240
+ stepData: Record<string, any>;
241
+ },
242
+ ): Promise<void> => new PostgresBackend(pool).waitJob(jobId, options);
243
+
244
+ /** @deprecated Use backend.updateStepData() directly. Delegates to PostgresBackend. */
245
+ export const updateStepData = async (
246
+ pool: Pool,
247
+ jobId: number,
248
+ stepData: Record<string, any>,
249
+ ): Promise<void> => new PostgresBackend(pool).updateStepData(jobId, stepData);
250
+
251
+ /** @deprecated Use backend.createWaitpoint() directly. Delegates to PostgresBackend. */
252
+ export const createWaitpoint = async (
253
+ pool: Pool,
254
+ jobId: number | null,
255
+ options?: { timeout?: string; tags?: string[] },
256
+ ): Promise<{ id: string }> =>
257
+ new PostgresBackend(pool).createWaitpoint(jobId, options);
258
+
259
+ /** @deprecated Use backend.completeWaitpoint() directly. Delegates to PostgresBackend. */
260
+ export const completeWaitpoint = async (
261
+ pool: Pool,
262
+ tokenId: string,
263
+ data?: any,
264
+ ): Promise<void> => new PostgresBackend(pool).completeWaitpoint(tokenId, data);
265
+
266
+ /** @deprecated Use backend.getWaitpoint() directly. Delegates to PostgresBackend. */
267
+ export const getWaitpoint = async (
268
+ pool: Pool,
269
+ tokenId: string,
270
+ ): Promise<WaitpointRecord | null> =>
271
+ new PostgresBackend(pool).getWaitpoint(tokenId);
272
+
273
+ /** @deprecated Use backend.expireTimedOutWaitpoints() directly. Delegates to PostgresBackend. */
274
+ export const expireTimedOutWaitpoints = async (pool: Pool): Promise<number> =>
275
+ new PostgresBackend(pool).expireTimedOutWaitpoints();