@monque/core 1.0.0 → 1.1.1

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 (45) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +20 -0
  3. package/dist/CHANGELOG.md +83 -0
  4. package/dist/LICENSE +15 -0
  5. package/dist/README.md +150 -0
  6. package/dist/index.cjs +2209 -942
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +537 -280
  9. package/dist/index.d.cts.map +1 -1
  10. package/dist/index.d.mts +538 -281
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2213 -951
  13. package/dist/index.mjs.map +1 -1
  14. package/package.json +9 -8
  15. package/src/events/index.ts +1 -0
  16. package/src/events/types.ts +113 -0
  17. package/src/index.ts +51 -0
  18. package/src/jobs/guards.ts +220 -0
  19. package/src/jobs/index.ts +29 -0
  20. package/src/jobs/types.ts +335 -0
  21. package/src/reset.d.ts +1 -0
  22. package/src/scheduler/helpers.ts +107 -0
  23. package/src/scheduler/index.ts +5 -0
  24. package/src/scheduler/monque.ts +1309 -0
  25. package/src/scheduler/services/change-stream-handler.ts +239 -0
  26. package/src/scheduler/services/index.ts +8 -0
  27. package/src/scheduler/services/job-manager.ts +455 -0
  28. package/src/scheduler/services/job-processor.ts +301 -0
  29. package/src/scheduler/services/job-query.ts +411 -0
  30. package/src/scheduler/services/job-scheduler.ts +267 -0
  31. package/src/scheduler/services/types.ts +48 -0
  32. package/src/scheduler/types.ts +123 -0
  33. package/src/shared/errors.ts +225 -0
  34. package/src/shared/index.ts +18 -0
  35. package/src/shared/utils/backoff.ts +77 -0
  36. package/src/shared/utils/cron.ts +67 -0
  37. package/src/shared/utils/index.ts +7 -0
  38. package/src/workers/index.ts +1 -0
  39. package/src/workers/types.ts +39 -0
  40. package/dist/errors-D5ZGG2uI.cjs +0 -155
  41. package/dist/errors-D5ZGG2uI.cjs.map +0 -1
  42. package/dist/errors-DEvnqoOC.mjs +0 -3
  43. package/dist/errors-DQ2_gprw.mjs +0 -125
  44. package/dist/errors-DQ2_gprw.mjs.map +0 -1
  45. package/dist/errors-Dfli-u59.cjs +0 -3
package/dist/index.d.cts CHANGED
@@ -2,7 +2,6 @@ import { Db, ObjectId } from "mongodb";
2
2
  import { EventEmitter } from "node:events";
3
3
 
4
4
  //#region src/jobs/types.d.ts
5
-
6
5
  /**
7
6
  * Represents the lifecycle states of a job in the queue.
8
7
  *
@@ -11,6 +10,7 @@ import { EventEmitter } from "node:events";
11
10
  * - PROCESSING → COMPLETED (on success)
12
11
  * - PROCESSING → PENDING (on failure, if retries remain)
13
12
  * - PROCESSING → FAILED (on failure, after max retries exhausted)
13
+ * - PENDING → CANCELLED (on manual cancellation)
14
14
  *
15
15
  * @example
16
16
  * ```typescript
@@ -20,17 +20,14 @@ import { EventEmitter } from "node:events";
20
20
  * ```
21
21
  */
22
22
  declare const JobStatus: {
23
- /** Job is waiting to be picked up by a worker */
24
- readonly PENDING: "pending";
25
- /** Job is currently being executed by a worker */
26
- readonly PROCESSING: "processing";
27
- /** Job completed successfully */
28
- readonly COMPLETED: "completed";
29
- /** Job permanently failed after exhausting all retry attempts */
30
- readonly FAILED: "failed";
23
+ /** Job is waiting to be picked up by a worker */readonly PENDING: "pending"; /** Job is currently being executed by a worker */
24
+ readonly PROCESSING: "processing"; /** Job completed successfully */
25
+ readonly COMPLETED: "completed"; /** Job permanently failed after exhausting all retry attempts */
26
+ readonly FAILED: "failed"; /** Job was manually cancelled */
27
+ readonly CANCELLED: "cancelled";
31
28
  };
32
29
  /**
33
- * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed'`
30
+ * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'`
34
31
  */
35
32
  type JobStatusType = (typeof JobStatus)[keyof typeof JobStatus];
36
33
  /**
@@ -192,6 +189,120 @@ interface GetJobsFilter {
192
189
  * ```
193
190
  */
194
191
  type JobHandler<T = unknown> = (job: Job<T>) => Promise<void> | void;
192
+ /**
193
+ * Valid cursor directions for pagination.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const direction = CursorDirection.FORWARD;
198
+ * ```
199
+ */
200
+ declare const CursorDirection: {
201
+ readonly FORWARD: "forward";
202
+ readonly BACKWARD: "backward";
203
+ };
204
+ type CursorDirectionType = (typeof CursorDirection)[keyof typeof CursorDirection];
205
+ /**
206
+ * Selector options for bulk operations.
207
+ *
208
+ * Used to select multiple jobs for operations like cancellation or deletion.
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * // Select all failed jobs older than 7 days
213
+ * const selector: JobSelector = {
214
+ * status: JobStatus.FAILED,
215
+ * olderThan: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
216
+ * };
217
+ * ```
218
+ */
219
+ interface JobSelector {
220
+ name?: string;
221
+ status?: JobStatusType | JobStatusType[];
222
+ olderThan?: Date;
223
+ newerThan?: Date;
224
+ }
225
+ /**
226
+ * Options for cursor-based pagination.
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * const options: CursorOptions = {
231
+ * limit: 50,
232
+ * direction: CursorDirection.FORWARD,
233
+ * filter: { status: JobStatus.PENDING },
234
+ * };
235
+ * ```
236
+ */
237
+ interface CursorOptions {
238
+ cursor?: string;
239
+ limit?: number;
240
+ direction?: CursorDirectionType;
241
+ filter?: Pick<GetJobsFilter, 'name' | 'status'>;
242
+ }
243
+ /**
244
+ * Response structure for cursor-based pagination.
245
+ *
246
+ * @template T - The type of the job's data payload
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const page = await monque.listJobs({ limit: 10 });
251
+ * console.log(`Got ${page.jobs.length} jobs`);
252
+ *
253
+ * if (page.hasNextPage) {
254
+ * console.log(`Next cursor: ${page.cursor}`);
255
+ * }
256
+ * ```
257
+ */
258
+ interface CursorPage<T = unknown> {
259
+ jobs: PersistedJob<T>[];
260
+ cursor: string | null;
261
+ hasNextPage: boolean;
262
+ hasPreviousPage: boolean;
263
+ }
264
+ /**
265
+ * Aggregated statistics for the job queue.
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const stats = await monque.getQueueStats();
270
+ * console.log(`Total jobs: ${stats.total}`);
271
+ * console.log(`Pending: ${stats.pending}`);
272
+ * console.log(`Processing: ${stats.processing}`);
273
+ * console.log(`Failed: ${stats.failed}`);
274
+ * console.log(`Start to finish avg: ${stats.avgProcessingDurationMs}ms`);
275
+ * ```
276
+ */
277
+ interface QueueStats {
278
+ pending: number;
279
+ processing: number;
280
+ completed: number;
281
+ failed: number;
282
+ cancelled: number;
283
+ total: number;
284
+ avgProcessingDurationMs?: number;
285
+ }
286
+ /**
287
+ * Result of a bulk operation.
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const result = await monque.cancelJobs(selector);
292
+ * console.log(`Cancelled ${result.count} jobs`);
293
+ *
294
+ * if (result.errors.length > 0) {
295
+ * console.warn('Some jobs could not be cancelled:', result.errors);
296
+ * }
297
+ * ```
298
+ */
299
+ interface BulkOperationResult {
300
+ count: number;
301
+ errors: Array<{
302
+ jobId: string;
303
+ error: string;
304
+ }>;
305
+ }
195
306
  //#endregion
196
307
  //#region src/jobs/guards.d.ts
197
308
  /**
@@ -231,8 +342,8 @@ declare function isPersistedJob<T>(job: Job<T>): job is PersistedJob<T>;
231
342
  /**
232
343
  * Type guard to check if a value is a valid job status.
233
344
  *
234
- * Validates that a value is one of the four valid job statuses: `'pending'`,
235
- * `'processing'`, `'completed'`, or `'failed'`. Useful for runtime validation
345
+ * Validates that a value is one of the five valid job statuses: `'pending'`,
346
+ * `'processing'`, `'completed'`, `'failed'`, or `'cancelled'`. Useful for runtime validation
236
347
  * of user input or external data.
237
348
  *
238
349
  * @param value - The value to check
@@ -344,6 +455,24 @@ declare function isCompletedJob<T>(job: Job<T>): boolean;
344
455
  * ```
345
456
  */
346
457
  declare function isFailedJob<T>(job: Job<T>): boolean;
458
+ /**
459
+ * Type guard to check if a job has been manually cancelled.
460
+ *
461
+ * A convenience helper for checking if a job was cancelled by an operator.
462
+ * Equivalent to `job.status === JobStatus.CANCELLED` but with better semantics.
463
+ *
464
+ * @template T - The type of the job's data payload
465
+ * @param job - The job to check
466
+ * @returns `true` if the job status is `'cancelled'`
467
+ *
468
+ * @example Filter cancelled jobs
469
+ * ```typescript
470
+ * const jobs = await monque.getJobs();
471
+ * const cancelledJobs = jobs.filter(isCancelledJob);
472
+ * console.log(`${cancelledJobs.length} jobs were cancelled`);
473
+ * ```
474
+ */
475
+ declare function isCancelledJob<T>(job: Job<T>): boolean;
347
476
  /**
348
477
  * Type guard to check if a job is a recurring scheduled job.
349
478
  *
@@ -384,8 +513,7 @@ interface MonqueEventMap {
384
513
  * Emitted when a job finishes successfully.
385
514
  */
386
515
  'job:complete': {
387
- job: Job;
388
- /** Processing duration in milliseconds */
516
+ job: Job; /** Processing duration in milliseconds */
389
517
  duration: number;
390
518
  };
391
519
  /**
@@ -393,8 +521,7 @@ interface MonqueEventMap {
393
521
  */
394
522
  'job:fail': {
395
523
  job: Job;
396
- error: Error;
397
- /** Whether the job will be retried */
524
+ error: Error; /** Whether the job will be retried */
398
525
  willRetry: boolean;
399
526
  };
400
527
  /**
@@ -430,6 +557,45 @@ interface MonqueEventMap {
430
557
  'changestream:fallback': {
431
558
  reason: string;
432
559
  };
560
+ /**
561
+ * Emitted when a job is manually cancelled.
562
+ */
563
+ 'job:cancelled': {
564
+ job: Job;
565
+ };
566
+ /**
567
+ * Emitted when a job is manually retried.
568
+ */
569
+ 'job:retried': {
570
+ job: Job;
571
+ previousStatus: 'failed' | 'cancelled';
572
+ };
573
+ /**
574
+ * Emitted when a job is manually deleted.
575
+ */
576
+ 'job:deleted': {
577
+ jobId: string;
578
+ };
579
+ /**
580
+ * Emitted when multiple jobs are cancelled in bulk.
581
+ */
582
+ 'jobs:cancelled': {
583
+ jobIds: string[];
584
+ count: number;
585
+ };
586
+ /**
587
+ * Emitted when multiple jobs are retried in bulk.
588
+ */
589
+ 'jobs:retried': {
590
+ jobIds: string[];
591
+ count: number;
592
+ };
593
+ /**
594
+ * Emitted when multiple jobs are deleted in bulk.
595
+ */
596
+ 'jobs:deleted': {
597
+ count: number;
598
+ };
433
599
  }
434
600
  //#endregion
435
601
  //#region src/workers/types.d.ts
@@ -575,62 +741,43 @@ interface MonqueOptions {
575
741
  * stale job recovery, and event-driven observability. Built on native MongoDB driver.
576
742
  *
577
743
  * @example Complete lifecycle
578
- * ```;
579
- typescript
744
+ * ```typescript
745
+ * import { Monque } from '@monque/core';
746
+ * import { MongoClient } from 'mongodb';
580
747
  *
581
-
582
- import { Monque } from '@monque/core';
583
-
584
- *
585
-
586
- import { MongoClient } from 'mongodb';
587
-
588
- *
748
+ * const client = new MongoClient('mongodb://localhost:27017');
749
+ * await client.connect();
750
+ * const db = client.db('myapp');
589
751
  *
590
- const client = new MongoClient('mongodb://localhost:27017');
591
- * await client.connect()
592
- *
593
- const db = client.db('myapp');
594
- *
595
752
  * // Create instance with options
596
- *
597
- const monque = new Monque(db, {
753
+ * const monque = new Monque(db, {
598
754
  * collectionName: 'jobs',
599
755
  * pollInterval: 1000,
600
756
  * maxRetries: 10,
601
757
  * shutdownTimeout: 30000,
602
758
  * });
603
- *
759
+ *
604
760
  * // Initialize (sets up indexes and recovers stale jobs)
605
- * await monque.initialize()
606
- *
761
+ * await monque.initialize();
762
+ *
607
763
  * // Register workers with type safety
764
+ * type EmailJob = {
765
+ * to: string;
766
+ * subject: string;
767
+ * body: string;
768
+ * };
608
769
  *
609
- type EmailJob = {};
610
- * to: string
611
- * subject: string
612
- * body: string
613
- * }
770
+ * monque.register<EmailJob>('send-email', async (job) => {
771
+ * await emailService.send(job.data.to, job.data.subject, job.data.body);
772
+ * });
614
773
  *
615
- * monque.register<EmailJob>('send-email', async (job) =>
616
- {
617
- * await emailService.send(job.data.to, job.data.subject, job.data.body)
618
- *
619
- }
620
- )
621
- *
622
774
  * // Monitor events for observability
623
- * monque.on('job:complete', (
624
- {
625
- job, duration;
626
- }
627
- ) =>
628
- {
629
- * logger.info(`Job $job.namecompleted in $durationms`);
775
+ * monque.on('job:complete', ({ job, duration }) => {
776
+ * logger.info(`Job ${job.name} completed in ${duration}ms`);
630
777
  * });
631
778
  *
632
779
  * monque.on('job:fail', ({ job, error, willRetry }) => {
633
- * logger.error(`Job $job.namefailed:`, error);
780
+ * logger.error(`Job ${job.name} failed:`, error);
634
781
  * });
635
782
  *
636
783
  * // Start processing
@@ -661,34 +808,11 @@ declare class Monque extends EventEmitter {
661
808
  private cleanupIntervalId;
662
809
  private isRunning;
663
810
  private isInitialized;
664
- /**
665
- * MongoDB Change Stream for real-time job notifications.
666
- * When available, provides instant job processing without polling delay.
667
- */
668
- private changeStream;
669
- /**
670
- * Number of consecutive reconnection attempts for change stream.
671
- * Used for exponential backoff during reconnection.
672
- */
673
- private changeStreamReconnectAttempts;
674
- /**
675
- * Maximum reconnection attempts before falling back to polling-only mode.
676
- */
677
- private readonly maxChangeStreamReconnectAttempts;
678
- /**
679
- * Debounce timer for change stream event processing.
680
- * Prevents claim storms when multiple events arrive in quick succession.
681
- */
682
- private changeStreamDebounceTimer;
683
- /**
684
- * Whether the scheduler is currently using change streams for notifications.
685
- */
686
- private usingChangeStreams;
687
- /**
688
- * Timer ID for change stream reconnection with exponential backoff.
689
- * Tracked to allow cancellation during shutdown.
690
- */
691
- private changeStreamReconnectTimer;
811
+ private _scheduler;
812
+ private _manager;
813
+ private _query;
814
+ private _processor;
815
+ private _changeStreamHandler;
692
816
  constructor(db: Db, options?: MonqueOptions);
693
817
  /**
694
818
  * Initialize the scheduler by setting up the MongoDB collection and indexes.
@@ -697,6 +821,20 @@ declare class Monque extends EventEmitter {
697
821
  * @throws {ConnectionError} If collection or index creation fails
698
822
  */
699
823
  initialize(): Promise<void>;
824
+ /** @throws {ConnectionError} if not initialized */
825
+ private get scheduler();
826
+ /** @throws {ConnectionError} if not initialized */
827
+ private get manager();
828
+ /** @throws {ConnectionError} if not initialized */
829
+ private get query();
830
+ /** @throws {ConnectionError} if not initialized */
831
+ private get processor();
832
+ /** @throws {ConnectionError} if not initialized */
833
+ private get changeStreamHandler();
834
+ /**
835
+ * Build the shared context for internal services.
836
+ */
837
+ private buildContext;
700
838
  /**
701
839
  * Create required MongoDB indexes for efficient job processing.
702
840
  *
@@ -846,6 +984,266 @@ declare class Monque extends EventEmitter {
846
984
  * ```
847
985
  */
848
986
  schedule<T>(cron: string, name: string, data: T, options?: ScheduleOptions): Promise<PersistedJob<T>>;
987
+ /**
988
+ * Cancel a pending or scheduled job.
989
+ *
990
+ * Sets the job status to 'cancelled' and emits a 'job:cancelled' event.
991
+ * If the job is already cancelled, this is a no-op and returns the job.
992
+ * Cannot cancel jobs that are currently 'processing', 'completed', or 'failed'.
993
+ *
994
+ * @param jobId - The ID of the job to cancel
995
+ * @returns The cancelled job, or null if not found
996
+ * @throws {JobStateError} If job is in an invalid state for cancellation
997
+ *
998
+ * @example Cancel a pending job
999
+ * ```typescript
1000
+ * const job = await monque.enqueue('report', { type: 'daily' });
1001
+ * await monque.cancelJob(job._id.toString());
1002
+ * ```
1003
+ */
1004
+ cancelJob(jobId: string): Promise<PersistedJob<unknown> | null>;
1005
+ /**
1006
+ * Retry a failed or cancelled job.
1007
+ *
1008
+ * Resets the job to 'pending' status, clears failure count/reason, and sets
1009
+ * nextRunAt to now (immediate retry). Emits a 'job:retried' event.
1010
+ *
1011
+ * @param jobId - The ID of the job to retry
1012
+ * @returns The updated job, or null if not found
1013
+ * @throws {JobStateError} If job is in an invalid state for retry (must be failed or cancelled)
1014
+ *
1015
+ * @example Retry a failed job
1016
+ * ```typescript
1017
+ * monque.on('job:fail', async ({ job }) => {
1018
+ * console.log(`Job ${job._id} failed, retrying manually...`);
1019
+ * await monque.retryJob(job._id.toString());
1020
+ * });
1021
+ * ```
1022
+ */
1023
+ retryJob(jobId: string): Promise<PersistedJob<unknown> | null>;
1024
+ /**
1025
+ * Reschedule a pending job to run at a different time.
1026
+ *
1027
+ * Only works for jobs in 'pending' status.
1028
+ *
1029
+ * @param jobId - The ID of the job to reschedule
1030
+ * @param runAt - The new Date when the job should run
1031
+ * @returns The updated job, or null if not found
1032
+ * @throws {JobStateError} If job is not in pending state
1033
+ *
1034
+ * @example Delay a job by 1 hour
1035
+ * ```typescript
1036
+ * const nextHour = new Date(Date.now() + 60 * 60 * 1000);
1037
+ * await monque.rescheduleJob(jobId, nextHour);
1038
+ * ```
1039
+ */
1040
+ rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null>;
1041
+ /**
1042
+ * Permanently delete a job.
1043
+ *
1044
+ * This action is irreversible. Emits a 'job:deleted' event upon success.
1045
+ * Can delete a job in any state.
1046
+ *
1047
+ * @param jobId - The ID of the job to delete
1048
+ * @returns true if deleted, false if job not found
1049
+ *
1050
+ * @example Delete a cleanup job
1051
+ * ```typescript
1052
+ * const deleted = await monque.deleteJob(jobId);
1053
+ * if (deleted) {
1054
+ * console.log('Job permanently removed');
1055
+ * }
1056
+ * ```
1057
+ */
1058
+ deleteJob(jobId: string): Promise<boolean>;
1059
+ /**
1060
+ * Cancel multiple jobs matching the given filter.
1061
+ *
1062
+ * Only cancels jobs in 'pending' status. Jobs in other states are collected
1063
+ * as errors in the result. Emits a 'jobs:cancelled' event with the IDs of
1064
+ * successfully cancelled jobs.
1065
+ *
1066
+ * @param filter - Selector for which jobs to cancel (name, status, date range)
1067
+ * @returns Result with count of cancelled jobs and any errors encountered
1068
+ *
1069
+ * @example Cancel all pending jobs for a queue
1070
+ * ```typescript
1071
+ * const result = await monque.cancelJobs({
1072
+ * name: 'email-queue',
1073
+ * status: 'pending'
1074
+ * });
1075
+ * console.log(`Cancelled ${result.count} jobs`);
1076
+ * ```
1077
+ */
1078
+ cancelJobs(filter: JobSelector): Promise<BulkOperationResult>;
1079
+ /**
1080
+ * Retry multiple jobs matching the given filter.
1081
+ *
1082
+ * Only retries jobs in 'failed' or 'cancelled' status. Jobs in other states
1083
+ * are collected as errors in the result. Emits a 'jobs:retried' event with
1084
+ * the IDs of successfully retried jobs.
1085
+ *
1086
+ * @param filter - Selector for which jobs to retry (name, status, date range)
1087
+ * @returns Result with count of retried jobs and any errors encountered
1088
+ *
1089
+ * @example Retry all failed jobs
1090
+ * ```typescript
1091
+ * const result = await monque.retryJobs({
1092
+ * status: 'failed'
1093
+ * });
1094
+ * console.log(`Retried ${result.count} jobs`);
1095
+ * ```
1096
+ */
1097
+ retryJobs(filter: JobSelector): Promise<BulkOperationResult>;
1098
+ /**
1099
+ * Delete multiple jobs matching the given filter.
1100
+ *
1101
+ * Deletes jobs in any status. Uses a batch delete for efficiency.
1102
+ * Does not emit individual 'job:deleted' events to avoid noise.
1103
+ *
1104
+ * @param filter - Selector for which jobs to delete (name, status, date range)
1105
+ * @returns Result with count of deleted jobs (errors array always empty for delete)
1106
+ *
1107
+ * @example Delete old completed jobs
1108
+ * ```typescript
1109
+ * const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
1110
+ * const result = await monque.deleteJobs({
1111
+ * status: 'completed',
1112
+ * olderThan: weekAgo
1113
+ * });
1114
+ * console.log(`Deleted ${result.count} jobs`);
1115
+ * ```
1116
+ */
1117
+ deleteJobs(filter: JobSelector): Promise<BulkOperationResult>;
1118
+ /**
1119
+ * Get a single job by its MongoDB ObjectId.
1120
+ *
1121
+ * Useful for retrieving job details when you have a job ID from events,
1122
+ * logs, or stored references.
1123
+ *
1124
+ * @template T - The expected type of the job data payload
1125
+ * @param id - The job's ObjectId
1126
+ * @returns Promise resolving to the job if found, null otherwise
1127
+ * @throws {ConnectionError} If scheduler not initialized
1128
+ *
1129
+ * @example Look up job from event
1130
+ * ```typescript
1131
+ * monque.on('job:fail', async ({ job }) => {
1132
+ * // Later, retrieve the job to check its status
1133
+ * const currentJob = await monque.getJob(job._id);
1134
+ * console.log(`Job status: ${currentJob?.status}`);
1135
+ * });
1136
+ * ```
1137
+ *
1138
+ * @example Admin endpoint
1139
+ * ```typescript
1140
+ * app.get('/jobs/:id', async (req, res) => {
1141
+ * const job = await monque.getJob(new ObjectId(req.params.id));
1142
+ * if (!job) {
1143
+ * return res.status(404).json({ error: 'Job not found' });
1144
+ * }
1145
+ * res.json(job);
1146
+ * });
1147
+ * ```
1148
+ */
1149
+ getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null>;
1150
+ /**
1151
+ * Query jobs from the queue with optional filters.
1152
+ *
1153
+ * Provides read-only access to job data for monitoring, debugging, and
1154
+ * administrative purposes. Results are ordered by `nextRunAt` ascending.
1155
+ *
1156
+ * @template T - The expected type of the job data payload
1157
+ * @param filter - Optional filter criteria
1158
+ * @returns Promise resolving to array of matching jobs
1159
+ * @throws {ConnectionError} If scheduler not initialized
1160
+ *
1161
+ * @example Get all pending jobs
1162
+ * ```typescript
1163
+ * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });
1164
+ * console.log(`${pendingJobs.length} jobs waiting`);
1165
+ * ```
1166
+ *
1167
+ * @example Get failed email jobs
1168
+ * ```typescript
1169
+ * const failedEmails = await monque.getJobs({
1170
+ * name: 'send-email',
1171
+ * status: JobStatus.FAILED,
1172
+ * });
1173
+ * for (const job of failedEmails) {
1174
+ * console.error(`Job ${job._id} failed: ${job.failReason}`);
1175
+ * }
1176
+ * ```
1177
+ *
1178
+ * @example Paginated job listing
1179
+ * ```typescript
1180
+ * const page1 = await monque.getJobs({ limit: 50, skip: 0 });
1181
+ * const page2 = await monque.getJobs({ limit: 50, skip: 50 });
1182
+ * ```
1183
+ *
1184
+ * @example Use with type guards from @monque/core
1185
+ * ```typescript
1186
+ * import { isPendingJob, isRecurringJob } from '@monque/core';
1187
+ *
1188
+ * const jobs = await monque.getJobs();
1189
+ * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));
1190
+ * ```
1191
+ */
1192
+ getJobs<T = unknown>(filter?: GetJobsFilter): Promise<PersistedJob<T>[]>;
1193
+ /**
1194
+ * Get a paginated list of jobs using opaque cursors.
1195
+ *
1196
+ * Provides stable pagination for large job lists. Supports forward and backward
1197
+ * navigation, filtering, and efficient database access via index-based cursor queries.
1198
+ *
1199
+ * @template T - The job data payload type
1200
+ * @param options - Pagination options (cursor, limit, direction, filter)
1201
+ * @returns Page of jobs with next/prev cursors
1202
+ * @throws {InvalidCursorError} If the provided cursor is malformed
1203
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
1204
+ *
1205
+ * @example List pending jobs
1206
+ * ```typescript
1207
+ * const page = await monque.getJobsWithCursor({
1208
+ * limit: 20,
1209
+ * filter: { status: 'pending' }
1210
+ * });
1211
+ * const jobs = page.jobs;
1212
+ *
1213
+ * // Get next page
1214
+ * if (page.hasNextPage) {
1215
+ * const page2 = await monque.getJobsWithCursor({
1216
+ * cursor: page.cursor,
1217
+ * limit: 20
1218
+ * });
1219
+ * }
1220
+ * ```
1221
+ */
1222
+ getJobsWithCursor<T = unknown>(options?: CursorOptions): Promise<CursorPage<T>>;
1223
+ /**
1224
+ * Get aggregate statistics for the job queue.
1225
+ *
1226
+ * Uses MongoDB aggregation pipeline for efficient server-side calculation.
1227
+ * Returns counts per status and optional average processing duration for completed jobs.
1228
+ *
1229
+ * @param filter - Optional filter to scope statistics by job name
1230
+ * @returns Promise resolving to queue statistics
1231
+ * @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
1232
+ * @throws {ConnectionError} If database operation fails
1233
+ *
1234
+ * @example Get overall queue statistics
1235
+ * ```typescript
1236
+ * const stats = await monque.getQueueStats();
1237
+ * console.log(`Pending: ${stats.pending}, Failed: ${stats.failed}`);
1238
+ * ```
1239
+ *
1240
+ * @example Get statistics for a specific job type
1241
+ * ```typescript
1242
+ * const emailStats = await monque.getQueueStats({ name: 'send-email' });
1243
+ * console.log(`${emailStats.total} email jobs in queue`);
1244
+ * ```
1245
+ */
1246
+ getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats>;
849
1247
  /**
850
1248
  * Register a worker to process jobs of a specific type.
851
1249
  *
@@ -1036,144 +1434,6 @@ declare class Monque extends EventEmitter {
1036
1434
  * ```
1037
1435
  */
1038
1436
  isHealthy(): boolean;
1039
- /**
1040
- * Query jobs from the queue with optional filters.
1041
- *
1042
- * Provides read-only access to job data for monitoring, debugging, and
1043
- * administrative purposes. Results are ordered by `nextRunAt` ascending.
1044
- *
1045
- * @template T - The expected type of the job data payload
1046
- * @param filter - Optional filter criteria
1047
- * @returns Promise resolving to array of matching jobs
1048
- * @throws {ConnectionError} If scheduler not initialized
1049
- *
1050
- * @example Get all pending jobs
1051
- * ```typescript
1052
- * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });
1053
- * console.log(`${pendingJobs.length} jobs waiting`);
1054
- * ```
1055
- *
1056
- * @example Get failed email jobs
1057
- * ```typescript
1058
- * const failedEmails = await monque.getJobs({
1059
- * name: 'send-email',
1060
- * status: JobStatus.FAILED,
1061
- * });
1062
- * for (const job of failedEmails) {
1063
- * console.error(`Job ${job._id} failed: ${job.failReason}`);
1064
- * }
1065
- * ```
1066
- *
1067
- * @example Paginated job listing
1068
- * ```typescript
1069
- * const page1 = await monque.getJobs({ limit: 50, skip: 0 });
1070
- * const page2 = await monque.getJobs({ limit: 50, skip: 50 });
1071
- * ```
1072
- *
1073
- * @example Use with type guards from @monque/core
1074
- * ```typescript
1075
- * import { isPendingJob, isRecurringJob } from '@monque/core';
1076
- *
1077
- * const jobs = await monque.getJobs();
1078
- * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));
1079
- * ```
1080
- */
1081
- getJobs<T = unknown>(filter?: GetJobsFilter): Promise<PersistedJob<T>[]>;
1082
- /**
1083
- * Get a single job by its MongoDB ObjectId.
1084
- *
1085
- * Useful for retrieving job details when you have a job ID from events,
1086
- * logs, or stored references.
1087
- *
1088
- * @template T - The expected type of the job data payload
1089
- * @param id - The job's ObjectId
1090
- * @returns Promise resolving to the job if found, null otherwise
1091
- * @throws {ConnectionError} If scheduler not initialized
1092
- *
1093
- * @example Look up job from event
1094
- * ```typescript
1095
- * monque.on('job:fail', async ({ job }) => {
1096
- * // Later, retrieve the job to check its status
1097
- * const currentJob = await monque.getJob(job._id);
1098
- * console.log(`Job status: ${currentJob?.status}`);
1099
- * });
1100
- * ```
1101
- *
1102
- * @example Admin endpoint
1103
- * ```typescript
1104
- * app.get('/jobs/:id', async (req, res) => {
1105
- * const job = await monque.getJob(new ObjectId(req.params.id));
1106
- * if (!job) {
1107
- * return res.status(404).json({ error: 'Job not found' });
1108
- * }
1109
- * res.json(job);
1110
- * });
1111
- * ```
1112
- */
1113
- getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null>;
1114
- /**
1115
- * Poll for available jobs and process them.
1116
- *
1117
- * Called at regular intervals (configured by `pollInterval`). For each registered worker,
1118
- * attempts to acquire jobs up to the worker's available concurrency slots.
1119
- * Aborts early if the scheduler is stopping (`isRunning` is false).
1120
- *
1121
- * @private
1122
- */
1123
- private poll;
1124
- /**
1125
- * Atomically acquire a pending job for processing using the claimedBy pattern.
1126
- *
1127
- * Uses MongoDB's `findOneAndUpdate` with atomic operations to ensure only one scheduler
1128
- * instance can claim a job. The query ensures the job is:
1129
- * - In pending status
1130
- * - Has nextRunAt <= now
1131
- * - Is not claimed by another instance (claimedBy is null/undefined)
1132
- *
1133
- * Returns `null` immediately if scheduler is stopping (`isRunning` is false).
1134
- *
1135
- * @private
1136
- * @param name - The job type to acquire
1137
- * @returns The acquired job with updated status, claimedBy, and heartbeat info, or `null` if no jobs available
1138
- */
1139
- private acquireJob;
1140
- /**
1141
- * Execute a job using its registered worker handler.
1142
- *
1143
- * Tracks the job as active during processing, emits lifecycle events, and handles
1144
- * both success and failure cases. On success, calls `completeJob()`. On failure,
1145
- * calls `failJob()` which implements exponential backoff retry logic.
1146
- *
1147
- * @private
1148
- * @param job - The job to process
1149
- * @param worker - The worker registration containing the handler and active job tracking
1150
- */
1151
- private processJob;
1152
- /**
1153
- * Mark a job as completed successfully.
1154
- *
1155
- * For recurring jobs (with `repeatInterval`), schedules the next run based on the cron
1156
- * expression and resets `failCount` to 0. For one-time jobs, sets status to `completed`.
1157
- * Clears `lockedAt` and `failReason` fields in both cases.
1158
- *
1159
- * @private
1160
- * @param job - The job that completed successfully
1161
- */
1162
- private completeJob;
1163
- /**
1164
- * Handle job failure with exponential backoff retry logic.
1165
- *
1166
- * Increments `failCount` and calculates next retry time using exponential backoff:
1167
- * `nextRunAt = 2^failCount × baseRetryInterval` (capped by optional `maxBackoffDelay`).
1168
- *
1169
- * If `failCount >= maxRetries`, marks job as permanently `failed`. Otherwise, resets
1170
- * to `pending` status for retry. Stores error message in `failReason` field.
1171
- *
1172
- * @private
1173
- * @param job - The job that failed
1174
- * @param error - The error that caused the failure
1175
- */
1176
- private failJob;
1177
1437
  /**
1178
1438
  * Ensure the scheduler is initialized before operations.
1179
1439
  *
@@ -1181,63 +1441,6 @@ declare class Monque extends EventEmitter {
1181
1441
  * @throws {ConnectionError} If scheduler not initialized or collection unavailable
1182
1442
  */
1183
1443
  private ensureInitialized;
1184
- /**
1185
- * Update heartbeats for all jobs claimed by this scheduler instance.
1186
- *
1187
- * This method runs periodically while the scheduler is running to indicate
1188
- * that jobs are still being actively processed.
1189
- *
1190
- * `lastHeartbeat` is primarily an observability signal (monitoring/debugging).
1191
- * Stale recovery is based on `lockedAt` + `lockTimeout`.
1192
- *
1193
- * @private
1194
- */
1195
- private updateHeartbeats;
1196
- /**
1197
- * Set up MongoDB Change Stream for real-time job notifications.
1198
- *
1199
- * Change streams provide instant notifications when jobs are inserted or when
1200
- * job status changes to pending (e.g., after a retry). This eliminates the
1201
- * polling delay for reactive job processing.
1202
- *
1203
- * The change stream watches for:
1204
- * - Insert operations (new jobs)
1205
- * - Update operations where status field changes
1206
- *
1207
- * If change streams are unavailable (e.g., standalone MongoDB), the system
1208
- * gracefully falls back to polling-only mode.
1209
- *
1210
- * @private
1211
- */
1212
- private setupChangeStream;
1213
- /**
1214
- * Handle a change stream event by triggering a debounced poll.
1215
- *
1216
- * Events are debounced to prevent "claim storms" when multiple changes arrive
1217
- * in rapid succession (e.g., bulk job inserts). A 100ms debounce window
1218
- * collects multiple events and triggers a single poll.
1219
- *
1220
- * @private
1221
- * @param change - The change stream event document
1222
- */
1223
- private handleChangeStreamEvent;
1224
- /**
1225
- * Handle change stream errors with exponential backoff reconnection.
1226
- *
1227
- * Attempts to reconnect up to `maxChangeStreamReconnectAttempts` times with
1228
- * exponential backoff (base 1000ms). After exhausting retries, falls back to
1229
- * polling-only mode.
1230
- *
1231
- * @private
1232
- * @param error - The error that caused the change stream failure
1233
- */
1234
- private handleChangeStreamError;
1235
- /**
1236
- * Close the change stream cursor and emit closed event.
1237
- *
1238
- * @private
1239
- */
1240
- private closeChangeStream;
1241
1444
  /**
1242
1445
  * Get array of active job IDs across all workers.
1243
1446
  *
@@ -1370,6 +1573,60 @@ declare class WorkerRegistrationError extends MonqueError {
1370
1573
  readonly jobName: string;
1371
1574
  constructor(message: string, jobName: string);
1372
1575
  }
1576
+ /**
1577
+ * Error thrown when a state transition is invalid.
1578
+ *
1579
+ * @example
1580
+ * ```typescript
1581
+ * try {
1582
+ * await monque.cancelJob(jobId);
1583
+ * } catch (error) {
1584
+ * if (error instanceof JobStateError) {
1585
+ * console.error(`Cannot cancel job in state: ${error.currentStatus}`);
1586
+ * }
1587
+ * }
1588
+ * ```
1589
+ */
1590
+ declare class JobStateError extends MonqueError {
1591
+ readonly jobId: string;
1592
+ readonly currentStatus: string;
1593
+ readonly attemptedAction: 'cancel' | 'retry' | 'reschedule';
1594
+ constructor(message: string, jobId: string, currentStatus: string, attemptedAction: 'cancel' | 'retry' | 'reschedule');
1595
+ }
1596
+ /**
1597
+ * Error thrown when a pagination cursor is invalid or malformed.
1598
+ *
1599
+ * @example
1600
+ * ```typescript
1601
+ * try {
1602
+ * await monque.listJobs({ cursor: 'invalid-cursor' });
1603
+ * } catch (error) {
1604
+ * if (error instanceof InvalidCursorError) {
1605
+ * console.error('Invalid cursor provided');
1606
+ * }
1607
+ * }
1608
+ * ```
1609
+ */
1610
+ declare class InvalidCursorError extends MonqueError {
1611
+ constructor(message: string);
1612
+ }
1613
+ /**
1614
+ * Error thrown when a statistics aggregation times out.
1615
+ *
1616
+ * @example
1617
+ * ```typescript
1618
+ * try {
1619
+ * const stats = await monque.getQueueStats();
1620
+ * } catch (error) {
1621
+ * if (error instanceof AggregationTimeoutError) {
1622
+ * console.error('Stats took too long to calculate');
1623
+ * }
1624
+ * }
1625
+ * ```
1626
+ */
1627
+ declare class AggregationTimeoutError extends MonqueError {
1628
+ constructor(message?: string);
1629
+ }
1373
1630
  //#endregion
1374
1631
  //#region src/shared/utils/backoff.d.ts
1375
1632
  /**
@@ -1459,5 +1716,5 @@ declare function getNextCronDate(expression: string, currentDate?: Date): Date;
1459
1716
  */
1460
1717
  declare function validateCronExpression(expression: string): void;
1461
1718
  //#endregion
1462
- export { ConnectionError, DEFAULT_BASE_INTERVAL, DEFAULT_MAX_BACKOFF_DELAY, type EnqueueOptions, type GetJobsFilter, InvalidCronError, type Job, type JobHandler, JobStatus, type JobStatusType, Monque, MonqueError, type MonqueEventMap, type MonqueOptions, type PersistedJob, type ScheduleOptions, ShutdownTimeoutError, type WorkerOptions, WorkerRegistrationError, calculateBackoff, calculateBackoffDelay, getNextCronDate, isCompletedJob, isFailedJob, isPendingJob, isPersistedJob, isProcessingJob, isRecurringJob, isValidJobStatus, validateCronExpression };
1719
+ export { AggregationTimeoutError, type BulkOperationResult, ConnectionError, CursorDirection, type CursorOptions, type CursorPage, DEFAULT_BASE_INTERVAL, DEFAULT_MAX_BACKOFF_DELAY, type EnqueueOptions, type GetJobsFilter, InvalidCronError, InvalidCursorError, type Job, type JobHandler, type JobSelector, JobStateError, JobStatus, type JobStatusType, Monque, MonqueError, type MonqueEventMap, type MonqueOptions, type PersistedJob, type QueueStats, type ScheduleOptions, ShutdownTimeoutError, type WorkerOptions, WorkerRegistrationError, calculateBackoff, calculateBackoffDelay, getNextCronDate, isCancelledJob, isCompletedJob, isFailedJob, isPendingJob, isPersistedJob, isProcessingJob, isRecurringJob, isValidJobStatus, validateCronExpression };
1463
1720
  //# sourceMappingURL=index.d.cts.map