@nicnocquee/dataqueue 1.34.0 → 1.35.0-beta.20260224110011

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/types.ts CHANGED
@@ -1,6 +1,47 @@
1
1
  // Utility type for job type keys
2
2
  export type JobType<PayloadMap> = keyof PayloadMap & string;
3
3
 
4
+ /**
5
+ * Abstract database client interface for transactional job creation.
6
+ * Compatible with `pg.Pool`, `pg.PoolClient`, `pg.Client`, or any object
7
+ * that exposes a `.query()` method matching the `pg` signature.
8
+ */
9
+ export interface DatabaseClient {
10
+ query(
11
+ text: string,
12
+ values?: any[],
13
+ ): Promise<{ rows: any[]; rowCount: number | null }>;
14
+ }
15
+
16
+ /**
17
+ * Options for `addJob()` beyond the job itself.
18
+ * Use `db` to insert the job within an existing database transaction.
19
+ */
20
+ export interface AddJobOptions {
21
+ /**
22
+ * An external database client (e.g., a `pg.PoolClient` inside a transaction).
23
+ * When provided, the INSERT runs on this client instead of the internal pool,
24
+ * so the job is part of the caller's transaction.
25
+ *
26
+ * **PostgreSQL only.** Throws if used with the Redis backend.
27
+ */
28
+ db?: DatabaseClient;
29
+ }
30
+
31
+ /**
32
+ * Optional grouping metadata for a job.
33
+ * Use `id` to enforce global per-group concurrency limits when
34
+ * `ProcessorOptions.groupConcurrency` is set.
35
+ *
36
+ * `tier` is reserved for future tier-based policies.
37
+ */
38
+ export interface JobGroup {
39
+ /** Stable group identifier (for example: tenant ID, user ID, organization ID). */
40
+ id: string;
41
+ /** Optional tier label reserved for future tier-based concurrency controls. */
42
+ tier?: string;
43
+ }
44
+
4
45
  export interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
5
46
  jobType: T;
6
47
  payload: PayloadMap[T];
@@ -73,6 +114,32 @@ export interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
73
114
  * Once a key exists, it cannot be reused until the job is cleaned up (via `cleanupOldJobs`).
74
115
  */
75
116
  idempotencyKey?: string;
117
+ /**
118
+ * Base delay between retries in seconds. When `retryBackoff` is true (the default),
119
+ * this is the base for exponential backoff: `retryDelay * 2^attempts`.
120
+ * When `retryBackoff` is false, retries use this fixed delay.
121
+ * @default 60
122
+ */
123
+ retryDelay?: number;
124
+ /**
125
+ * Whether to use exponential backoff for retries. When true, delay doubles
126
+ * with each attempt and includes jitter to prevent thundering herd.
127
+ * When false, a fixed `retryDelay` is used between every retry.
128
+ * @default true
129
+ */
130
+ retryBackoff?: boolean;
131
+ /**
132
+ * Maximum delay between retries in seconds. Caps the exponential backoff
133
+ * so retries never wait longer than this value. Only meaningful when
134
+ * `retryBackoff` is true. No limit when omitted.
135
+ */
136
+ retryDelayMax?: number;
137
+ /**
138
+ * Optional group metadata for this job.
139
+ * When `ProcessorOptions.groupConcurrency` is configured, grouped jobs are
140
+ * globally limited by `group.id` across all workers/instances.
141
+ */
142
+ group?: JobGroup;
76
143
  }
77
144
 
78
145
  /**
@@ -196,6 +263,31 @@ export interface JobRecord<PayloadMap, T extends JobType<PayloadMap>> {
196
263
  * Updated by the handler via `ctx.setProgress(percent)`.
197
264
  */
198
265
  progress?: number | null;
266
+ /**
267
+ * Handler output stored via `ctx.setOutput(data)` or by returning a value
268
+ * from the handler. `null` if no output has been stored.
269
+ */
270
+ output?: unknown;
271
+ /**
272
+ * Base delay between retries in seconds, or null if using legacy default.
273
+ */
274
+ retryDelay?: number | null;
275
+ /**
276
+ * Whether exponential backoff is enabled for retries, or null if using legacy default.
277
+ */
278
+ retryBackoff?: boolean | null;
279
+ /**
280
+ * Maximum delay cap for retries in seconds, or null if no cap.
281
+ */
282
+ retryDelayMax?: number | null;
283
+ /**
284
+ * Group identifier for this job, if provided at enqueue time.
285
+ */
286
+ groupId?: string | null;
287
+ /**
288
+ * Group tier for this job, if provided at enqueue time.
289
+ */
290
+ groupTier?: string | null;
199
291
  }
200
292
 
201
293
  /**
@@ -292,6 +384,17 @@ export interface JobContext {
292
384
  * @throws If percent is outside the 0-100 range.
293
385
  */
294
386
  setProgress: (percent: number) => Promise<void>;
387
+
388
+ /**
389
+ * Store an output/result for this job. The value is persisted to the database
390
+ * as JSONB and can be read by clients via `getJob()` or the React SDK's `useJob()` hook.
391
+ *
392
+ * Can be called multiple times — each call overwrites the previous value.
393
+ * If `setOutput()` is called, the handler's return value is ignored.
394
+ *
395
+ * @param data - Any JSON-serializable value to store as the job's output.
396
+ */
397
+ setOutput: (data: unknown) => Promise<void>;
295
398
  }
296
399
 
297
400
  /**
@@ -380,7 +483,7 @@ export type JobHandler<PayloadMap, T extends keyof PayloadMap> = (
380
483
  payload: PayloadMap[T],
381
484
  signal: AbortSignal,
382
485
  ctx: JobContext,
383
- ) => Promise<void>;
486
+ ) => Promise<unknown>;
384
487
 
385
488
  export type JobHandlers<PayloadMap> = {
386
489
  [K in keyof PayloadMap]: JobHandler<PayloadMap, K>;
@@ -401,6 +504,13 @@ export interface ProcessorOptions {
401
504
  * - Set to a lower value to avoid resource exhaustion.
402
505
  */
403
506
  concurrency?: number;
507
+ /**
508
+ * Global per-group concurrency limit across all workers/instances.
509
+ * - Applies only to jobs with `group.id` set.
510
+ * - Jobs without a group are unaffected.
511
+ * - Disabled when omitted.
512
+ */
513
+ groupConcurrency?: number;
404
514
  /**
405
515
  * The interval in milliseconds to poll for new jobs.
406
516
  * - If not provided, the processor will process jobs every 5 seconds when startInBackground is called.
@@ -452,6 +562,91 @@ export interface Processor {
452
562
  start: () => Promise<number>;
453
563
  }
454
564
 
565
+ export interface SupervisorOptions {
566
+ /**
567
+ * How often the maintenance loop runs, in milliseconds.
568
+ * @default 60000 (1 minute)
569
+ */
570
+ intervalMs?: number;
571
+ /**
572
+ * Reclaim jobs stuck in `processing` longer than this many minutes.
573
+ * @default 10
574
+ */
575
+ stuckJobsTimeoutMinutes?: number;
576
+ /**
577
+ * Auto-delete completed jobs older than this many days. Set to 0 to disable.
578
+ * @default 30
579
+ */
580
+ cleanupJobsDaysToKeep?: number;
581
+ /**
582
+ * Auto-delete job events older than this many days. Set to 0 to disable.
583
+ * @default 30
584
+ */
585
+ cleanupEventsDaysToKeep?: number;
586
+ /**
587
+ * Batch size for cleanup deletions.
588
+ * @default 1000
589
+ */
590
+ cleanupBatchSize?: number;
591
+ /**
592
+ * Whether to reclaim stuck jobs each cycle.
593
+ * @default true
594
+ */
595
+ reclaimStuckJobs?: boolean;
596
+ /**
597
+ * Whether to expire timed-out waitpoint tokens each cycle.
598
+ * @default true
599
+ */
600
+ expireTimedOutTokens?: boolean;
601
+ /**
602
+ * Called when a maintenance task throws. One failure does not block other tasks.
603
+ * @default console.error
604
+ */
605
+ onError?: (error: Error) => void;
606
+ /** Enable verbose logging. */
607
+ verbose?: boolean;
608
+ }
609
+
610
+ export interface SupervisorRunResult {
611
+ /** Number of stuck jobs reclaimed back to pending. */
612
+ reclaimedJobs: number;
613
+ /** Number of old completed jobs deleted. */
614
+ cleanedUpJobs: number;
615
+ /** Number of old job events deleted. */
616
+ cleanedUpEvents: number;
617
+ /** Number of timed-out waitpoint tokens expired. */
618
+ expiredTokens: number;
619
+ }
620
+
621
+ export interface Supervisor {
622
+ /**
623
+ * Run all maintenance tasks once and return the results.
624
+ * Ideal for serverless or cron-triggered invocations.
625
+ */
626
+ start: () => Promise<SupervisorRunResult>;
627
+ /**
628
+ * Start the maintenance loop in the background.
629
+ * Runs every `intervalMs` milliseconds (default: 60 000).
630
+ * Call `stop()` or `stopAndDrain()` to halt the loop.
631
+ */
632
+ startInBackground: () => void;
633
+ /**
634
+ * Stop the background maintenance loop immediately.
635
+ * Does not wait for an in-flight maintenance run to complete.
636
+ */
637
+ stop: () => void;
638
+ /**
639
+ * Stop the background loop and wait for the current maintenance run
640
+ * (if any) to finish before resolving.
641
+ *
642
+ * @param timeoutMs - Maximum time to wait (default: 30 000 ms).
643
+ * If the run does not finish within this time the promise resolves anyway.
644
+ */
645
+ stopAndDrain: (timeoutMs?: number) => Promise<void>;
646
+ /** Whether the background maintenance loop is currently running. */
647
+ isRunning: () => boolean;
648
+ }
649
+
455
650
  export interface DatabaseSSLConfig {
456
651
  /**
457
652
  * CA certificate as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
@@ -474,10 +669,13 @@ export interface DatabaseSSLConfig {
474
669
  /**
475
670
  * Configuration for PostgreSQL backend (default).
476
671
  * Backward-compatible: omitting `backend` defaults to 'postgres'.
672
+ *
673
+ * Provide either `databaseConfig` (the library creates a pool) or `pool`
674
+ * (bring your own `pg.Pool`). At least one must be set.
477
675
  */
478
676
  export interface PostgresJobQueueConfig {
479
677
  backend?: 'postgres';
480
- databaseConfig: {
678
+ databaseConfig?: {
481
679
  connectionString?: string;
482
680
  host?: string;
483
681
  port?: number;
@@ -503,6 +701,11 @@ export interface PostgresJobQueueConfig {
503
701
  */
504
702
  connectionTimeoutMillis?: number;
505
703
  };
704
+ /**
705
+ * Bring your own `pg.Pool` instance. When provided, `databaseConfig` is
706
+ * ignored and the library will not close the pool on shutdown.
707
+ */
708
+ pool?: import('pg').Pool;
506
709
  verbose?: boolean;
507
710
  }
508
711
 
@@ -518,10 +721,13 @@ export interface RedisTLSConfig {
518
721
 
519
722
  /**
520
723
  * Configuration for Redis backend.
724
+ *
725
+ * Provide either `redisConfig` (the library creates an ioredis client) or
726
+ * `client` (bring your own ioredis instance). At least one must be set.
521
727
  */
522
728
  export interface RedisJobQueueConfig {
523
729
  backend: 'redis';
524
- redisConfig: {
730
+ redisConfig?: {
525
731
  /** Redis URL (e.g. redis://localhost:6379) */
526
732
  url?: string;
527
733
  host?: string;
@@ -536,6 +742,17 @@ export interface RedisJobQueueConfig {
536
742
  */
537
743
  keyPrefix?: string;
538
744
  };
745
+ /**
746
+ * Bring your own ioredis client instance. When provided, `redisConfig` is
747
+ * ignored and the library will not close the client on shutdown.
748
+ * Use `keyPrefix` to set the key namespace (default: 'dq:').
749
+ */
750
+ client?: unknown;
751
+ /**
752
+ * Key prefix when using an external `client`. Ignored when `redisConfig` is used
753
+ * (set `redisConfig.keyPrefix` instead). Default: 'dq:'.
754
+ */
755
+ keyPrefix?: string;
539
756
  verbose?: boolean;
540
757
  }
541
758
 
@@ -592,6 +809,12 @@ export interface CronScheduleOptions<
592
809
  * is still pending, processing, or waiting.
593
810
  */
594
811
  allowOverlap?: boolean;
812
+ /** Base delay between retries in seconds for each job instance (default: 60). */
813
+ retryDelay?: number;
814
+ /** Whether to use exponential backoff for retries (default: true). */
815
+ retryBackoff?: boolean;
816
+ /** Maximum delay cap for retries in seconds. */
817
+ retryDelayMax?: number;
595
818
  }
596
819
 
597
820
  /**
@@ -616,6 +839,9 @@ export interface CronScheduleRecord {
616
839
  nextRunAt: Date | null;
617
840
  createdAt: Date;
618
841
  updatedAt: Date;
842
+ retryDelay: number | null;
843
+ retryBackoff: boolean | null;
844
+ retryDelayMax: number | null;
619
845
  }
620
846
 
621
847
  /**
@@ -632,15 +858,87 @@ export interface EditCronScheduleOptions {
632
858
  tags?: string[] | null;
633
859
  timezone?: string;
634
860
  allowOverlap?: boolean;
861
+ retryDelay?: number | null;
862
+ retryBackoff?: boolean | null;
863
+ retryDelayMax?: number | null;
864
+ }
865
+
866
+ // ── Event hooks ──────────────────────────────────────────────────────
867
+
868
+ /**
869
+ * Payload types for each event emitted by the job queue.
870
+ */
871
+ export interface QueueEventMap {
872
+ /** Fired after a job is successfully added to the queue. */
873
+ 'job:added': { jobId: number; jobType: string };
874
+ /** Fired when a processor claims a job and begins executing its handler. */
875
+ 'job:processing': { jobId: number; jobType: string };
876
+ /** Fired when a job handler completes successfully. */
877
+ 'job:completed': { jobId: number; jobType: string };
878
+ /** Fired when a job handler fails. `willRetry` indicates whether the job will be retried. */
879
+ 'job:failed': {
880
+ jobId: number;
881
+ jobType: string;
882
+ error: Error;
883
+ willRetry: boolean;
884
+ };
885
+ /** Fired after a job is cancelled via `cancelJob()`. */
886
+ 'job:cancelled': { jobId: number };
887
+ /** Fired after a failed job is manually retried via `retryJob()`. */
888
+ 'job:retried': { jobId: number };
889
+ /** Fired when a job enters the `waiting` state (via `ctx.waitFor`, `ctx.waitUntil`, or `ctx.waitForToken`). */
890
+ 'job:waiting': { jobId: number; jobType: string };
891
+ /** Fired when a job reports progress via `ctx.setProgress()`. */
892
+ 'job:progress': { jobId: number; progress: number };
893
+ /** Fired when a job stores output via `ctx.setOutput()`. */
894
+ 'job:output': { jobId: number; output: unknown };
895
+ /** Fired on internal errors from the processor or supervisor. */
896
+ error: Error;
635
897
  }
636
898
 
899
+ /** Union of all event names supported by the job queue. */
900
+ export type QueueEventName = keyof QueueEventMap;
901
+
902
+ /**
903
+ * Callback type for `emit`. Used internally to pass the emitter
904
+ * from `initJobQueue` into the processor and supervisor.
905
+ */
906
+ export type QueueEmitFn = <K extends QueueEventName>(
907
+ event: K,
908
+ data: QueueEventMap[K],
909
+ ) => void;
910
+
637
911
  export interface JobQueue<PayloadMap> {
638
912
  /**
639
913
  * Add a job to the job queue.
914
+ *
915
+ * @param job - The job to enqueue.
916
+ * @param options - Optional. Pass `{ db }` with an external database client
917
+ * to insert the job within an existing transaction (PostgreSQL only).
640
918
  */
641
919
  addJob: <T extends JobType<PayloadMap>>(
642
920
  job: JobOptions<PayloadMap, T>,
921
+ options?: AddJobOptions,
643
922
  ) => Promise<number>;
923
+ /**
924
+ * Add multiple jobs to the queue in a single operation.
925
+ *
926
+ * More efficient than calling `addJob` in a loop because it batches the
927
+ * INSERT into a single database round-trip (PostgreSQL) or a single
928
+ * atomic Lua script (Redis).
929
+ *
930
+ * Returns an array of job IDs in the same order as the input array.
931
+ * Each job may independently have an `idempotencyKey`; duplicates
932
+ * resolve to the existing job's ID without creating a new row.
933
+ *
934
+ * @param jobs - Array of jobs to enqueue.
935
+ * @param options - Optional. Pass `{ db }` with an external database client
936
+ * to insert the jobs within an existing transaction (PostgreSQL only).
937
+ */
938
+ addJobs: <T extends JobType<PayloadMap>>(
939
+ jobs: JobOptions<PayloadMap, T>[],
940
+ options?: AddJobOptions,
941
+ ) => Promise<number[]>;
644
942
  /**
645
943
  * Get a job by its ID.
646
944
  */
@@ -795,6 +1093,13 @@ export interface JobQueue<PayloadMap> {
795
1093
  options?: ProcessorOptions,
796
1094
  ) => Processor;
797
1095
 
1096
+ /**
1097
+ * Create a background supervisor that automatically reclaims stuck jobs,
1098
+ * cleans up old completed jobs/events, and expires timed-out waitpoint
1099
+ * tokens on a configurable interval.
1100
+ */
1101
+ createSupervisor: (options?: SupervisorOptions) => Supervisor;
1102
+
798
1103
  /**
799
1104
  * Get the job events for a job.
800
1105
  */
@@ -898,6 +1203,51 @@ export interface JobQueue<PayloadMap> {
898
1203
  */
899
1204
  enqueueDueCronJobs: () => Promise<number>;
900
1205
 
1206
+ // ── Event hooks ───────────────────────────────────────────────────────
1207
+
1208
+ /**
1209
+ * Register a listener for a queue event. The listener is called every
1210
+ * time the event fires. Works identically with both PostgreSQL and Redis.
1211
+ *
1212
+ * @param event - The event name (e.g. `'job:completed'`, `'error'`).
1213
+ * @param listener - Callback receiving the event payload.
1214
+ */
1215
+ on: <K extends QueueEventName>(
1216
+ event: K,
1217
+ listener: (data: QueueEventMap[K]) => void,
1218
+ ) => void;
1219
+
1220
+ /**
1221
+ * Register a one-time listener. The listener is automatically removed
1222
+ * after it fires once.
1223
+ *
1224
+ * @param event - The event name.
1225
+ * @param listener - Callback receiving the event payload.
1226
+ */
1227
+ once: <K extends QueueEventName>(
1228
+ event: K,
1229
+ listener: (data: QueueEventMap[K]) => void,
1230
+ ) => void;
1231
+
1232
+ /**
1233
+ * Remove a previously registered listener.
1234
+ *
1235
+ * @param event - The event name.
1236
+ * @param listener - The exact function reference passed to `on` or `once`.
1237
+ */
1238
+ off: <K extends QueueEventName>(
1239
+ event: K,
1240
+ listener: (data: QueueEventMap[K]) => void,
1241
+ ) => void;
1242
+
1243
+ /**
1244
+ * Remove all listeners for a specific event, or all listeners for
1245
+ * all events when called without arguments.
1246
+ *
1247
+ * @param event - Optional event name. If omitted, removes everything.
1248
+ */
1249
+ removeAllListeners: (event?: QueueEventName) => void;
1250
+
901
1251
  // ── Advanced access ───────────────────────────────────────────────────
902
1252
 
903
1253
  /**