@nicnocquee/dataqueue 1.19.3 → 1.21.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.
package/src/queue.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  FailureReason,
6
6
  JobEvent,
7
7
  JobEventType,
8
+ TagQueryMode,
8
9
  } from './types.js';
9
10
  import { log } from './log-context.js';
10
11
 
@@ -43,6 +44,7 @@ export const addJob = async <PayloadMap, T extends keyof PayloadMap & string>(
43
44
  priority = 0,
44
45
  runAt = null,
45
46
  timeoutMs = undefined,
47
+ tags = undefined,
46
48
  }: JobOptions<PayloadMap, T>,
47
49
  ): Promise<number> => {
48
50
  const client = await pool.connect();
@@ -51,29 +53,45 @@ export const addJob = async <PayloadMap, T extends keyof PayloadMap & string>(
51
53
  if (runAt) {
52
54
  result = await client.query(
53
55
  `INSERT INTO job_queue
54
- (job_type, payload, max_attempts, priority, run_at, timeout_ms)
55
- VALUES ($1, $2, $3, $4, $5, $6)
56
+ (job_type, payload, max_attempts, priority, run_at, timeout_ms, tags)
57
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
56
58
  RETURNING id`,
57
- [jobType, payload, maxAttempts, priority, runAt, timeoutMs ?? null],
59
+ [
60
+ jobType,
61
+ payload,
62
+ maxAttempts,
63
+ priority,
64
+ runAt,
65
+ timeoutMs ?? null,
66
+ tags ?? null,
67
+ ],
58
68
  );
59
69
  log(
60
- `Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, runAt ${runAt.toISOString()}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}`,
70
+ `Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, runAt ${runAt.toISOString()}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}, tags ${JSON.stringify(tags)}`,
61
71
  );
62
72
  } else {
63
73
  result = await client.query(
64
74
  `INSERT INTO job_queue
65
- (job_type, payload, max_attempts, priority, timeout_ms)
66
- VALUES ($1, $2, $3, $4, $5)
75
+ (job_type, payload, max_attempts, priority, timeout_ms, tags)
76
+ VALUES ($1, $2, $3, $4, $5, $6)
67
77
  RETURNING id`,
68
- [jobType, payload, maxAttempts, priority, timeoutMs ?? null],
78
+ [
79
+ jobType,
80
+ payload,
81
+ maxAttempts,
82
+ priority,
83
+ timeoutMs ?? null,
84
+ tags ?? null,
85
+ ],
69
86
  );
70
87
  log(
71
- `Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}`,
88
+ `Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}, tags ${JSON.stringify(tags)}`,
72
89
  );
73
90
  }
74
91
  await recordJobEvent(pool, result.rows[0].id, JobEventType.Added, {
75
92
  jobType,
76
93
  payload,
94
+ tags,
77
95
  });
78
96
  return result.rows[0].id;
79
97
  } catch (error) {
@@ -400,7 +418,12 @@ export const cancelJob = async (pool: Pool, jobId: number): Promise<void> => {
400
418
  */
401
419
  export const cancelAllUpcomingJobs = async (
402
420
  pool: Pool,
403
- filters?: { jobType?: string; priority?: number; runAt?: Date },
421
+ filters?: {
422
+ jobType?: string;
423
+ priority?: number;
424
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
425
+ tags?: { values: string[]; mode?: TagQueryMode };
426
+ },
404
427
  ): Promise<number> => {
405
428
  const client = await pool.connect();
406
429
  try {
@@ -420,8 +443,61 @@ export const cancelAllUpcomingJobs = async (
420
443
  params.push(filters.priority);
421
444
  }
422
445
  if (filters.runAt) {
423
- query += ` AND run_at = $${paramIdx++}`;
424
- params.push(filters.runAt);
446
+ if (filters.runAt instanceof Date) {
447
+ query += ` AND run_at = $${paramIdx++}`;
448
+ params.push(filters.runAt);
449
+ } else if (typeof filters.runAt === 'object') {
450
+ const ops = filters.runAt;
451
+ if (ops.gt) {
452
+ query += ` AND run_at > $${paramIdx++}`;
453
+ params.push(ops.gt);
454
+ }
455
+ if (ops.gte) {
456
+ query += ` AND run_at >= $${paramIdx++}`;
457
+ params.push(ops.gte);
458
+ }
459
+ if (ops.lt) {
460
+ query += ` AND run_at < $${paramIdx++}`;
461
+ params.push(ops.lt);
462
+ }
463
+ if (ops.lte) {
464
+ query += ` AND run_at <= $${paramIdx++}`;
465
+ params.push(ops.lte);
466
+ }
467
+ if (ops.eq) {
468
+ query += ` AND run_at = $${paramIdx++}`;
469
+ params.push(ops.eq);
470
+ }
471
+ }
472
+ }
473
+ if (
474
+ filters.tags &&
475
+ filters.tags.values &&
476
+ filters.tags.values.length > 0
477
+ ) {
478
+ const mode = filters.tags.mode || 'all';
479
+ const tagValues = filters.tags.values;
480
+ switch (mode) {
481
+ case 'exact':
482
+ query += ` AND tags = $${paramIdx++}`;
483
+ params.push(tagValues);
484
+ break;
485
+ case 'all':
486
+ query += ` AND tags @> $${paramIdx++}`;
487
+ params.push(tagValues);
488
+ break;
489
+ case 'any':
490
+ query += ` AND tags && $${paramIdx++}`;
491
+ params.push(tagValues);
492
+ break;
493
+ case 'none':
494
+ query += ` AND NOT (tags && $${paramIdx++})`;
495
+ params.push(tagValues);
496
+ break;
497
+ default:
498
+ query += ` AND tags @> $${paramIdx++}`;
499
+ params.push(tagValues);
500
+ }
425
501
  }
426
502
  }
427
503
  query += '\nRETURNING id';
@@ -548,3 +624,184 @@ export const getJobEvents = async (
548
624
  client.release();
549
625
  }
550
626
  };
627
+
628
+ /**
629
+ * Get jobs by tags (matches all specified tags)
630
+ */
631
+ export const getJobsByTags = async <
632
+ PayloadMap,
633
+ T extends keyof PayloadMap & string,
634
+ >(
635
+ pool: Pool,
636
+ tags: string[],
637
+ mode: TagQueryMode = 'all',
638
+ limit = 100,
639
+ offset = 0,
640
+ ): Promise<JobRecord<PayloadMap, T>[]> => {
641
+ const client = await pool.connect();
642
+ try {
643
+ 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
644
+ FROM job_queue`;
645
+ let params: any[] = [];
646
+ switch (mode) {
647
+ case 'exact':
648
+ query += ' WHERE tags = $1';
649
+ params = [tags];
650
+ break;
651
+ case 'all':
652
+ query += ' WHERE tags @> $1';
653
+ params = [tags];
654
+ break;
655
+ case 'any':
656
+ query += ' WHERE tags && $1';
657
+ params = [tags];
658
+ break;
659
+ case 'none':
660
+ query += ' WHERE NOT (tags && $1)';
661
+ params = [tags];
662
+ break;
663
+ default:
664
+ query += ' WHERE tags @> $1';
665
+ params = [tags];
666
+ }
667
+ query += ' ORDER BY created_at DESC LIMIT $2 OFFSET $3';
668
+ params.push(limit, offset);
669
+ const result = await client.query(query, params);
670
+ log(
671
+ `Found ${result.rows.length} jobs by tags ${JSON.stringify(tags)} (mode: ${mode})`,
672
+ );
673
+ return result.rows.map((job) => ({
674
+ ...job,
675
+ payload: job.payload,
676
+ timeoutMs: job.timeoutMs,
677
+ failureReason: job.failureReason,
678
+ }));
679
+ } catch (error) {
680
+ log(
681
+ `Error getting jobs by tags ${JSON.stringify(tags)} (mode: ${mode}): ${error}`,
682
+ );
683
+ throw error;
684
+ } finally {
685
+ client.release();
686
+ }
687
+ };
688
+
689
+ export const getJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
690
+ pool: Pool,
691
+ filters?: {
692
+ jobType?: string;
693
+ priority?: number;
694
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
695
+ tags?: { values: string[]; mode?: TagQueryMode };
696
+ },
697
+ limit = 100,
698
+ offset = 0,
699
+ ): Promise<JobRecord<PayloadMap, T>[]> => {
700
+ const client = await pool.connect();
701
+ try {
702
+ 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 FROM job_queue`;
703
+ const params: any[] = [];
704
+ let where: string[] = [];
705
+ let paramIdx = 1;
706
+ if (filters) {
707
+ if (filters.jobType) {
708
+ where.push(`job_type = $${paramIdx++}`);
709
+ params.push(filters.jobType);
710
+ }
711
+ if (filters.priority !== undefined) {
712
+ where.push(`priority = $${paramIdx++}`);
713
+ params.push(filters.priority);
714
+ }
715
+ if (filters.runAt) {
716
+ if (filters.runAt instanceof Date) {
717
+ where.push(`run_at = $${paramIdx++}`);
718
+ params.push(filters.runAt);
719
+ } else if (
720
+ typeof filters.runAt === 'object' &&
721
+ (filters.runAt.gt !== undefined ||
722
+ filters.runAt.gte !== undefined ||
723
+ filters.runAt.lt !== undefined ||
724
+ filters.runAt.lte !== undefined ||
725
+ filters.runAt.eq !== undefined)
726
+ ) {
727
+ const ops = filters.runAt as {
728
+ gt?: Date;
729
+ gte?: Date;
730
+ lt?: Date;
731
+ lte?: Date;
732
+ eq?: Date;
733
+ };
734
+ if (ops.gt) {
735
+ where.push(`run_at > $${paramIdx++}`);
736
+ params.push(ops.gt);
737
+ }
738
+ if (ops.gte) {
739
+ where.push(`run_at >= $${paramIdx++}`);
740
+ params.push(ops.gte);
741
+ }
742
+ if (ops.lt) {
743
+ where.push(`run_at < $${paramIdx++}`);
744
+ params.push(ops.lt);
745
+ }
746
+ if (ops.lte) {
747
+ where.push(`run_at <= $${paramIdx++}`);
748
+ params.push(ops.lte);
749
+ }
750
+ if (ops.eq) {
751
+ where.push(`run_at = $${paramIdx++}`);
752
+ params.push(ops.eq);
753
+ }
754
+ }
755
+ }
756
+ if (
757
+ filters.tags &&
758
+ filters.tags.values &&
759
+ filters.tags.values.length > 0
760
+ ) {
761
+ const mode = filters.tags.mode || 'all';
762
+ const tagValues = filters.tags.values;
763
+ switch (mode) {
764
+ case 'exact':
765
+ where.push(`tags = $${paramIdx++}`);
766
+ params.push(tagValues);
767
+ break;
768
+ case 'all':
769
+ where.push(`tags @> $${paramIdx++}`);
770
+ params.push(tagValues);
771
+ break;
772
+ case 'any':
773
+ where.push(`tags && $${paramIdx++}`);
774
+ params.push(tagValues);
775
+ break;
776
+ case 'none':
777
+ where.push(`NOT (tags && $${paramIdx++})`);
778
+ params.push(tagValues);
779
+ break;
780
+ default:
781
+ where.push(`tags @> $${paramIdx++}`);
782
+ params.push(tagValues);
783
+ }
784
+ }
785
+ }
786
+ if (where.length > 0) {
787
+ query += ` WHERE ${where.join(' AND ')}`;
788
+ }
789
+ // Always add LIMIT and OFFSET as the last parameters
790
+ paramIdx = params.length + 1;
791
+ query += ` ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx}`;
792
+ params.push(limit, offset);
793
+ const result = await client.query(query, params);
794
+ log(`Found ${result.rows.length} jobs`);
795
+ return result.rows.map((job) => ({
796
+ ...job,
797
+ payload: job.payload,
798
+ timeoutMs: job.timeoutMs,
799
+ failureReason: job.failureReason,
800
+ }));
801
+ } catch (error) {
802
+ log(`Error getting jobs: ${error}`);
803
+ throw error;
804
+ } finally {
805
+ client.release();
806
+ }
807
+ };
package/src/types.ts CHANGED
@@ -13,6 +13,10 @@ export interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
13
13
  * Timeout for this job in milliseconds. If not set, uses the processor default or unlimited.
14
14
  */
15
15
  timeoutMs?: number;
16
+ /**
17
+ * Tags for this job. Used for grouping, searching, or batch operations.
18
+ */
19
+ tags?: string[];
16
20
  }
17
21
 
18
22
  export enum JobEventType {
@@ -89,6 +93,10 @@ export interface JobRecord<PayloadMap, T extends JobType<PayloadMap>> {
89
93
  * The time the job was last cancelled.
90
94
  */
91
95
  lastCancelledAt: Date | null;
96
+ /**
97
+ * Tags for this job. Used for grouping, searching, or batch operations.
98
+ */
99
+ tags?: string[];
92
100
  }
93
101
 
94
102
  export type JobHandler<PayloadMap, T extends keyof PayloadMap> = (
@@ -169,6 +177,8 @@ export interface JobQueueConfig {
169
177
  verbose?: boolean;
170
178
  }
171
179
 
180
+ export type TagQueryMode = 'exact' | 'all' | 'any' | 'none';
181
+
172
182
  export interface JobQueue<PayloadMap> {
173
183
  /**
174
184
  * Add a job to the job queue.
@@ -193,6 +203,21 @@ export interface JobQueue<PayloadMap> {
193
203
  limit?: number,
194
204
  offset?: number,
195
205
  ) => Promise<JobRecord<PayloadMap, T>[]>;
206
+ /**
207
+ * Get jobs by tag(s).
208
+ * - Modes:
209
+ * - 'exact': Jobs with exactly the same tags (no more, no less)
210
+ * - 'all': Jobs that have all the given tags (can have more)
211
+ * - 'any': Jobs that have at least one of the given tags
212
+ * - 'none': Jobs that have none of the given tags
213
+ * - Default mode is 'all'.
214
+ */
215
+ getJobsByTags: <T extends JobType<PayloadMap>>(
216
+ tags: string[],
217
+ mode?: TagQueryMode,
218
+ limit?: number,
219
+ offset?: number,
220
+ ) => Promise<JobRecord<PayloadMap, T>[]>;
196
221
  /**
197
222
  * Get all jobs.
198
223
  */
@@ -200,6 +225,17 @@ export interface JobQueue<PayloadMap> {
200
225
  limit?: number,
201
226
  offset?: number,
202
227
  ) => Promise<JobRecord<PayloadMap, T>[]>;
228
+ /**
229
+ * Get jobs by filters.
230
+ /**
231
+ * Get jobs by filters.
232
+ */
233
+ getJobs: <T extends JobType<PayloadMap>>(filters?: {
234
+ jobType?: string;
235
+ priority?: number;
236
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
237
+ tags?: { values: string[]; mode?: TagQueryMode };
238
+ }) => Promise<JobRecord<PayloadMap, T>[]>;
203
239
  /**
204
240
  * Retry a job given its ID.
205
241
  * - This will set the job status back to 'pending', clear the locked_at and locked_by, and allow it to be picked up by other workers.
@@ -228,12 +264,14 @@ export interface JobQueue<PayloadMap> {
228
264
  * - The filters are:
229
265
  * - jobType: The job type to cancel.
230
266
  * - priority: The priority of the job to cancel.
231
- * - runAt: The time the job is scheduled to run at.
267
+ * - runAt: The time the job is scheduled to run at (now supports gt/gte/lt/lte/eq).
268
+ * - tags: An object with 'values' (string[]) and 'mode' (TagQueryMode) for tag-based cancellation.
232
269
  */
233
270
  cancelAllUpcomingJobs: (filters?: {
234
271
  jobType?: string;
235
272
  priority?: number;
236
- runAt?: Date;
273
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
274
+ tags?: { values: string[]; mode?: TagQueryMode };
237
275
  }) => Promise<number>;
238
276
  /**
239
277
  * Create a job processor. Handlers must be provided per-processor.