@monque/core 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -11,6 +11,7 @@ import { EventEmitter } from "node:events";
11
11
  * - PROCESSING → COMPLETED (on success)
12
12
  * - PROCESSING → PENDING (on failure, if retries remain)
13
13
  * - PROCESSING → FAILED (on failure, after max retries exhausted)
14
+ * - PENDING → CANCELLED (on manual cancellation)
14
15
  *
15
16
  * @example
16
17
  * ```typescript
@@ -28,9 +29,11 @@ declare const JobStatus: {
28
29
  readonly COMPLETED: "completed";
29
30
  /** Job permanently failed after exhausting all retry attempts */
30
31
  readonly FAILED: "failed";
32
+ /** Job was manually cancelled */
33
+ readonly CANCELLED: "cancelled";
31
34
  };
32
35
  /**
33
- * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed'`
36
+ * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'`
34
37
  */
35
38
  type JobStatusType = (typeof JobStatus)[keyof typeof JobStatus];
36
39
  /**
@@ -192,6 +195,120 @@ interface GetJobsFilter {
192
195
  * ```
193
196
  */
194
197
  type JobHandler<T = unknown> = (job: Job<T>) => Promise<void> | void;
198
+ /**
199
+ * Valid cursor directions for pagination.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const direction = CursorDirection.FORWARD;
204
+ * ```
205
+ */
206
+ declare const CursorDirection: {
207
+ readonly FORWARD: "forward";
208
+ readonly BACKWARD: "backward";
209
+ };
210
+ type CursorDirectionType = (typeof CursorDirection)[keyof typeof CursorDirection];
211
+ /**
212
+ * Selector options for bulk operations.
213
+ *
214
+ * Used to select multiple jobs for operations like cancellation or deletion.
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * // Select all failed jobs older than 7 days
219
+ * const selector: JobSelector = {
220
+ * status: JobStatus.FAILED,
221
+ * olderThan: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
222
+ * };
223
+ * ```
224
+ */
225
+ interface JobSelector {
226
+ name?: string;
227
+ status?: JobStatusType | JobStatusType[];
228
+ olderThan?: Date;
229
+ newerThan?: Date;
230
+ }
231
+ /**
232
+ * Options for cursor-based pagination.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const options: CursorOptions = {
237
+ * limit: 50,
238
+ * direction: CursorDirection.FORWARD,
239
+ * filter: { status: JobStatus.PENDING },
240
+ * };
241
+ * ```
242
+ */
243
+ interface CursorOptions {
244
+ cursor?: string;
245
+ limit?: number;
246
+ direction?: CursorDirectionType;
247
+ filter?: Pick<GetJobsFilter, 'name' | 'status'>;
248
+ }
249
+ /**
250
+ * Response structure for cursor-based pagination.
251
+ *
252
+ * @template T - The type of the job's data payload
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const page = await monque.listJobs({ limit: 10 });
257
+ * console.log(`Got ${page.jobs.length} jobs`);
258
+ *
259
+ * if (page.hasNextPage) {
260
+ * console.log(`Next cursor: ${page.cursor}`);
261
+ * }
262
+ * ```
263
+ */
264
+ interface CursorPage<T = unknown> {
265
+ jobs: PersistedJob<T>[];
266
+ cursor: string | null;
267
+ hasNextPage: boolean;
268
+ hasPreviousPage: boolean;
269
+ }
270
+ /**
271
+ * Aggregated statistics for the job queue.
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const stats = await monque.getQueueStats();
276
+ * console.log(`Total jobs: ${stats.total}`);
277
+ * console.log(`Pending: ${stats.pending}`);
278
+ * console.log(`Processing: ${stats.processing}`);
279
+ * console.log(`Failed: ${stats.failed}`);
280
+ * console.log(`Start to finish avg: ${stats.avgProcessingDurationMs}ms`);
281
+ * ```
282
+ */
283
+ interface QueueStats {
284
+ pending: number;
285
+ processing: number;
286
+ completed: number;
287
+ failed: number;
288
+ cancelled: number;
289
+ total: number;
290
+ avgProcessingDurationMs?: number;
291
+ }
292
+ /**
293
+ * Result of a bulk operation.
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const result = await monque.cancelJobs(selector);
298
+ * console.log(`Cancelled ${result.count} jobs`);
299
+ *
300
+ * if (result.errors.length > 0) {
301
+ * console.warn('Some jobs could not be cancelled:', result.errors);
302
+ * }
303
+ * ```
304
+ */
305
+ interface BulkOperationResult {
306
+ count: number;
307
+ errors: Array<{
308
+ jobId: string;
309
+ error: string;
310
+ }>;
311
+ }
195
312
  //#endregion
196
313
  //#region src/jobs/guards.d.ts
197
314
  /**
@@ -231,8 +348,8 @@ declare function isPersistedJob<T>(job: Job<T>): job is PersistedJob<T>;
231
348
  /**
232
349
  * Type guard to check if a value is a valid job status.
233
350
  *
234
- * Validates that a value is one of the four valid job statuses: `'pending'`,
235
- * `'processing'`, `'completed'`, or `'failed'`. Useful for runtime validation
351
+ * Validates that a value is one of the five valid job statuses: `'pending'`,
352
+ * `'processing'`, `'completed'`, `'failed'`, or `'cancelled'`. Useful for runtime validation
236
353
  * of user input or external data.
237
354
  *
238
355
  * @param value - The value to check
@@ -344,6 +461,24 @@ declare function isCompletedJob<T>(job: Job<T>): boolean;
344
461
  * ```
345
462
  */
346
463
  declare function isFailedJob<T>(job: Job<T>): boolean;
464
+ /**
465
+ * Type guard to check if a job has been manually cancelled.
466
+ *
467
+ * A convenience helper for checking if a job was cancelled by an operator.
468
+ * Equivalent to `job.status === JobStatus.CANCELLED` but with better semantics.
469
+ *
470
+ * @template T - The type of the job's data payload
471
+ * @param job - The job to check
472
+ * @returns `true` if the job status is `'cancelled'`
473
+ *
474
+ * @example Filter cancelled jobs
475
+ * ```typescript
476
+ * const jobs = await monque.getJobs();
477
+ * const cancelledJobs = jobs.filter(isCancelledJob);
478
+ * console.log(`${cancelledJobs.length} jobs were cancelled`);
479
+ * ```
480
+ */
481
+ declare function isCancelledJob<T>(job: Job<T>): boolean;
347
482
  /**
348
483
  * Type guard to check if a job is a recurring scheduled job.
349
484
  *
@@ -430,6 +565,45 @@ interface MonqueEventMap {
430
565
  'changestream:fallback': {
431
566
  reason: string;
432
567
  };
568
+ /**
569
+ * Emitted when a job is manually cancelled.
570
+ */
571
+ 'job:cancelled': {
572
+ job: Job;
573
+ };
574
+ /**
575
+ * Emitted when a job is manually retried.
576
+ */
577
+ 'job:retried': {
578
+ job: Job;
579
+ previousStatus: 'failed' | 'cancelled';
580
+ };
581
+ /**
582
+ * Emitted when a job is manually deleted.
583
+ */
584
+ 'job:deleted': {
585
+ jobId: string;
586
+ };
587
+ /**
588
+ * Emitted when multiple jobs are cancelled in bulk.
589
+ */
590
+ 'jobs:cancelled': {
591
+ jobIds: string[];
592
+ count: number;
593
+ };
594
+ /**
595
+ * Emitted when multiple jobs are retried in bulk.
596
+ */
597
+ 'jobs:retried': {
598
+ jobIds: string[];
599
+ count: number;
600
+ };
601
+ /**
602
+ * Emitted when multiple jobs are deleted in bulk.
603
+ */
604
+ 'jobs:deleted': {
605
+ count: number;
606
+ };
433
607
  }
434
608
  //#endregion
435
609
  //#region src/workers/types.d.ts
@@ -575,62 +749,43 @@ interface MonqueOptions {
575
749
  * stale job recovery, and event-driven observability. Built on native MongoDB driver.
576
750
  *
577
751
  * @example Complete lifecycle
578
- * ```;
579
- typescript
752
+ * ```typescript
753
+ * import { Monque } from '@monque/core';
754
+ * import { MongoClient } from 'mongodb';
580
755
  *
581
-
582
- import { Monque } from '@monque/core';
583
-
584
- *
585
-
586
- import { MongoClient } from 'mongodb';
587
-
588
- *
756
+ * const client = new MongoClient('mongodb://localhost:27017');
757
+ * await client.connect();
758
+ * const db = client.db('myapp');
589
759
  *
590
- const client = new MongoClient('mongodb://localhost:27017');
591
- * await client.connect()
592
- *
593
- const db = client.db('myapp');
594
- *
595
760
  * // Create instance with options
596
- *
597
- const monque = new Monque(db, {
761
+ * const monque = new Monque(db, {
598
762
  * collectionName: 'jobs',
599
763
  * pollInterval: 1000,
600
764
  * maxRetries: 10,
601
765
  * shutdownTimeout: 30000,
602
766
  * });
603
- *
767
+ *
604
768
  * // Initialize (sets up indexes and recovers stale jobs)
605
- * await monque.initialize()
606
- *
769
+ * await monque.initialize();
770
+ *
607
771
  * // Register workers with type safety
772
+ * type EmailJob = {
773
+ * to: string;
774
+ * subject: string;
775
+ * body: string;
776
+ * };
608
777
  *
609
- type EmailJob = {};
610
- * to: string
611
- * subject: string
612
- * body: string
613
- * }
778
+ * monque.register<EmailJob>('send-email', async (job) => {
779
+ * await emailService.send(job.data.to, job.data.subject, job.data.body);
780
+ * });
614
781
  *
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
782
  * // Monitor events for observability
623
- * monque.on('job:complete', (
624
- {
625
- job, duration;
626
- }
627
- ) =>
628
- {
629
- * logger.info(`Job $job.namecompleted in $durationms`);
783
+ * monque.on('job:complete', ({ job, duration }) => {
784
+ * logger.info(`Job ${job.name} completed in ${duration}ms`);
630
785
  * });
631
786
  *
632
787
  * monque.on('job:fail', ({ job, error, willRetry }) => {
633
- * logger.error(`Job $job.namefailed:`, error);
788
+ * logger.error(`Job ${job.name} failed:`, error);
634
789
  * });
635
790
  *
636
791
  * // Start processing
@@ -661,34 +816,11 @@ declare class Monque extends EventEmitter {
661
816
  private cleanupIntervalId;
662
817
  private isRunning;
663
818
  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;
819
+ private _scheduler;
820
+ private _manager;
821
+ private _query;
822
+ private _processor;
823
+ private _changeStreamHandler;
692
824
  constructor(db: Db, options?: MonqueOptions);
693
825
  /**
694
826
  * Initialize the scheduler by setting up the MongoDB collection and indexes.
@@ -697,6 +829,20 @@ declare class Monque extends EventEmitter {
697
829
  * @throws {ConnectionError} If collection or index creation fails
698
830
  */
699
831
  initialize(): Promise<void>;
832
+ /** @throws {ConnectionError} if not initialized */
833
+ private get scheduler();
834
+ /** @throws {ConnectionError} if not initialized */
835
+ private get manager();
836
+ /** @throws {ConnectionError} if not initialized */
837
+ private get query();
838
+ /** @throws {ConnectionError} if not initialized */
839
+ private get processor();
840
+ /** @throws {ConnectionError} if not initialized */
841
+ private get changeStreamHandler();
842
+ /**
843
+ * Build the shared context for internal services.
844
+ */
845
+ private buildContext;
700
846
  /**
701
847
  * Create required MongoDB indexes for efficient job processing.
702
848
  *
@@ -846,6 +992,266 @@ declare class Monque extends EventEmitter {
846
992
  * ```
847
993
  */
848
994
  schedule<T>(cron: string, name: string, data: T, options?: ScheduleOptions): Promise<PersistedJob<T>>;
995
+ /**
996
+ * Cancel a pending or scheduled job.
997
+ *
998
+ * Sets the job status to 'cancelled' and emits a 'job:cancelled' event.
999
+ * If the job is already cancelled, this is a no-op and returns the job.
1000
+ * Cannot cancel jobs that are currently 'processing', 'completed', or 'failed'.
1001
+ *
1002
+ * @param jobId - The ID of the job to cancel
1003
+ * @returns The cancelled job, or null if not found
1004
+ * @throws {JobStateError} If job is in an invalid state for cancellation
1005
+ *
1006
+ * @example Cancel a pending job
1007
+ * ```typescript
1008
+ * const job = await monque.enqueue('report', { type: 'daily' });
1009
+ * await monque.cancelJob(job._id.toString());
1010
+ * ```
1011
+ */
1012
+ cancelJob(jobId: string): Promise<PersistedJob<unknown> | null>;
1013
+ /**
1014
+ * Retry a failed or cancelled job.
1015
+ *
1016
+ * Resets the job to 'pending' status, clears failure count/reason, and sets
1017
+ * nextRunAt to now (immediate retry). Emits a 'job:retried' event.
1018
+ *
1019
+ * @param jobId - The ID of the job to retry
1020
+ * @returns The updated job, or null if not found
1021
+ * @throws {JobStateError} If job is in an invalid state for retry (must be failed or cancelled)
1022
+ *
1023
+ * @example Retry a failed job
1024
+ * ```typescript
1025
+ * monque.on('job:fail', async ({ job }) => {
1026
+ * console.log(`Job ${job._id} failed, retrying manually...`);
1027
+ * await monque.retryJob(job._id.toString());
1028
+ * });
1029
+ * ```
1030
+ */
1031
+ retryJob(jobId: string): Promise<PersistedJob<unknown> | null>;
1032
+ /**
1033
+ * Reschedule a pending job to run at a different time.
1034
+ *
1035
+ * Only works for jobs in 'pending' status.
1036
+ *
1037
+ * @param jobId - The ID of the job to reschedule
1038
+ * @param runAt - The new Date when the job should run
1039
+ * @returns The updated job, or null if not found
1040
+ * @throws {JobStateError} If job is not in pending state
1041
+ *
1042
+ * @example Delay a job by 1 hour
1043
+ * ```typescript
1044
+ * const nextHour = new Date(Date.now() + 60 * 60 * 1000);
1045
+ * await monque.rescheduleJob(jobId, nextHour);
1046
+ * ```
1047
+ */
1048
+ rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null>;
1049
+ /**
1050
+ * Permanently delete a job.
1051
+ *
1052
+ * This action is irreversible. Emits a 'job:deleted' event upon success.
1053
+ * Can delete a job in any state.
1054
+ *
1055
+ * @param jobId - The ID of the job to delete
1056
+ * @returns true if deleted, false if job not found
1057
+ *
1058
+ * @example Delete a cleanup job
1059
+ * ```typescript
1060
+ * const deleted = await monque.deleteJob(jobId);
1061
+ * if (deleted) {
1062
+ * console.log('Job permanently removed');
1063
+ * }
1064
+ * ```
1065
+ */
1066
+ deleteJob(jobId: string): Promise<boolean>;
1067
+ /**
1068
+ * Cancel multiple jobs matching the given filter.
1069
+ *
1070
+ * Only cancels jobs in 'pending' status. Jobs in other states are collected
1071
+ * as errors in the result. Emits a 'jobs:cancelled' event with the IDs of
1072
+ * successfully cancelled jobs.
1073
+ *
1074
+ * @param filter - Selector for which jobs to cancel (name, status, date range)
1075
+ * @returns Result with count of cancelled jobs and any errors encountered
1076
+ *
1077
+ * @example Cancel all pending jobs for a queue
1078
+ * ```typescript
1079
+ * const result = await monque.cancelJobs({
1080
+ * name: 'email-queue',
1081
+ * status: 'pending'
1082
+ * });
1083
+ * console.log(`Cancelled ${result.count} jobs`);
1084
+ * ```
1085
+ */
1086
+ cancelJobs(filter: JobSelector): Promise<BulkOperationResult>;
1087
+ /**
1088
+ * Retry multiple jobs matching the given filter.
1089
+ *
1090
+ * Only retries jobs in 'failed' or 'cancelled' status. Jobs in other states
1091
+ * are collected as errors in the result. Emits a 'jobs:retried' event with
1092
+ * the IDs of successfully retried jobs.
1093
+ *
1094
+ * @param filter - Selector for which jobs to retry (name, status, date range)
1095
+ * @returns Result with count of retried jobs and any errors encountered
1096
+ *
1097
+ * @example Retry all failed jobs
1098
+ * ```typescript
1099
+ * const result = await monque.retryJobs({
1100
+ * status: 'failed'
1101
+ * });
1102
+ * console.log(`Retried ${result.count} jobs`);
1103
+ * ```
1104
+ */
1105
+ retryJobs(filter: JobSelector): Promise<BulkOperationResult>;
1106
+ /**
1107
+ * Delete multiple jobs matching the given filter.
1108
+ *
1109
+ * Deletes jobs in any status. Uses a batch delete for efficiency.
1110
+ * Does not emit individual 'job:deleted' events to avoid noise.
1111
+ *
1112
+ * @param filter - Selector for which jobs to delete (name, status, date range)
1113
+ * @returns Result with count of deleted jobs (errors array always empty for delete)
1114
+ *
1115
+ * @example Delete old completed jobs
1116
+ * ```typescript
1117
+ * const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
1118
+ * const result = await monque.deleteJobs({
1119
+ * status: 'completed',
1120
+ * olderThan: weekAgo
1121
+ * });
1122
+ * console.log(`Deleted ${result.count} jobs`);
1123
+ * ```
1124
+ */
1125
+ deleteJobs(filter: JobSelector): Promise<BulkOperationResult>;
1126
+ /**
1127
+ * Get a single job by its MongoDB ObjectId.
1128
+ *
1129
+ * Useful for retrieving job details when you have a job ID from events,
1130
+ * logs, or stored references.
1131
+ *
1132
+ * @template T - The expected type of the job data payload
1133
+ * @param id - The job's ObjectId
1134
+ * @returns Promise resolving to the job if found, null otherwise
1135
+ * @throws {ConnectionError} If scheduler not initialized
1136
+ *
1137
+ * @example Look up job from event
1138
+ * ```typescript
1139
+ * monque.on('job:fail', async ({ job }) => {
1140
+ * // Later, retrieve the job to check its status
1141
+ * const currentJob = await monque.getJob(job._id);
1142
+ * console.log(`Job status: ${currentJob?.status}`);
1143
+ * });
1144
+ * ```
1145
+ *
1146
+ * @example Admin endpoint
1147
+ * ```typescript
1148
+ * app.get('/jobs/:id', async (req, res) => {
1149
+ * const job = await monque.getJob(new ObjectId(req.params.id));
1150
+ * if (!job) {
1151
+ * return res.status(404).json({ error: 'Job not found' });
1152
+ * }
1153
+ * res.json(job);
1154
+ * });
1155
+ * ```
1156
+ */
1157
+ getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null>;
1158
+ /**
1159
+ * Query jobs from the queue with optional filters.
1160
+ *
1161
+ * Provides read-only access to job data for monitoring, debugging, and
1162
+ * administrative purposes. Results are ordered by `nextRunAt` ascending.
1163
+ *
1164
+ * @template T - The expected type of the job data payload
1165
+ * @param filter - Optional filter criteria
1166
+ * @returns Promise resolving to array of matching jobs
1167
+ * @throws {ConnectionError} If scheduler not initialized
1168
+ *
1169
+ * @example Get all pending jobs
1170
+ * ```typescript
1171
+ * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });
1172
+ * console.log(`${pendingJobs.length} jobs waiting`);
1173
+ * ```
1174
+ *
1175
+ * @example Get failed email jobs
1176
+ * ```typescript
1177
+ * const failedEmails = await monque.getJobs({
1178
+ * name: 'send-email',
1179
+ * status: JobStatus.FAILED,
1180
+ * });
1181
+ * for (const job of failedEmails) {
1182
+ * console.error(`Job ${job._id} failed: ${job.failReason}`);
1183
+ * }
1184
+ * ```
1185
+ *
1186
+ * @example Paginated job listing
1187
+ * ```typescript
1188
+ * const page1 = await monque.getJobs({ limit: 50, skip: 0 });
1189
+ * const page2 = await monque.getJobs({ limit: 50, skip: 50 });
1190
+ * ```
1191
+ *
1192
+ * @example Use with type guards from @monque/core
1193
+ * ```typescript
1194
+ * import { isPendingJob, isRecurringJob } from '@monque/core';
1195
+ *
1196
+ * const jobs = await monque.getJobs();
1197
+ * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));
1198
+ * ```
1199
+ */
1200
+ getJobs<T = unknown>(filter?: GetJobsFilter): Promise<PersistedJob<T>[]>;
1201
+ /**
1202
+ * Get a paginated list of jobs using opaque cursors.
1203
+ *
1204
+ * Provides stable pagination for large job lists. Supports forward and backward
1205
+ * navigation, filtering, and efficient database access via index-based cursor queries.
1206
+ *
1207
+ * @template T - The job data payload type
1208
+ * @param options - Pagination options (cursor, limit, direction, filter)
1209
+ * @returns Page of jobs with next/prev cursors
1210
+ * @throws {InvalidCursorError} If the provided cursor is malformed
1211
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
1212
+ *
1213
+ * @example List pending jobs
1214
+ * ```typescript
1215
+ * const page = await monque.getJobsWithCursor({
1216
+ * limit: 20,
1217
+ * filter: { status: 'pending' }
1218
+ * });
1219
+ * const jobs = page.jobs;
1220
+ *
1221
+ * // Get next page
1222
+ * if (page.hasNextPage) {
1223
+ * const page2 = await monque.getJobsWithCursor({
1224
+ * cursor: page.cursor,
1225
+ * limit: 20
1226
+ * });
1227
+ * }
1228
+ * ```
1229
+ */
1230
+ getJobsWithCursor<T = unknown>(options?: CursorOptions): Promise<CursorPage<T>>;
1231
+ /**
1232
+ * Get aggregate statistics for the job queue.
1233
+ *
1234
+ * Uses MongoDB aggregation pipeline for efficient server-side calculation.
1235
+ * Returns counts per status and optional average processing duration for completed jobs.
1236
+ *
1237
+ * @param filter - Optional filter to scope statistics by job name
1238
+ * @returns Promise resolving to queue statistics
1239
+ * @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
1240
+ * @throws {ConnectionError} If database operation fails
1241
+ *
1242
+ * @example Get overall queue statistics
1243
+ * ```typescript
1244
+ * const stats = await monque.getQueueStats();
1245
+ * console.log(`Pending: ${stats.pending}, Failed: ${stats.failed}`);
1246
+ * ```
1247
+ *
1248
+ * @example Get statistics for a specific job type
1249
+ * ```typescript
1250
+ * const emailStats = await monque.getQueueStats({ name: 'send-email' });
1251
+ * console.log(`${emailStats.total} email jobs in queue`);
1252
+ * ```
1253
+ */
1254
+ getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats>;
849
1255
  /**
850
1256
  * Register a worker to process jobs of a specific type.
851
1257
  *
@@ -1036,144 +1442,6 @@ declare class Monque extends EventEmitter {
1036
1442
  * ```
1037
1443
  */
1038
1444
  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
1445
  /**
1178
1446
  * Ensure the scheduler is initialized before operations.
1179
1447
  *
@@ -1181,63 +1449,6 @@ declare class Monque extends EventEmitter {
1181
1449
  * @throws {ConnectionError} If scheduler not initialized or collection unavailable
1182
1450
  */
1183
1451
  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
1452
  /**
1242
1453
  * Get array of active job IDs across all workers.
1243
1454
  *
@@ -1370,6 +1581,60 @@ declare class WorkerRegistrationError extends MonqueError {
1370
1581
  readonly jobName: string;
1371
1582
  constructor(message: string, jobName: string);
1372
1583
  }
1584
+ /**
1585
+ * Error thrown when a state transition is invalid.
1586
+ *
1587
+ * @example
1588
+ * ```typescript
1589
+ * try {
1590
+ * await monque.cancelJob(jobId);
1591
+ * } catch (error) {
1592
+ * if (error instanceof JobStateError) {
1593
+ * console.error(`Cannot cancel job in state: ${error.currentStatus}`);
1594
+ * }
1595
+ * }
1596
+ * ```
1597
+ */
1598
+ declare class JobStateError extends MonqueError {
1599
+ readonly jobId: string;
1600
+ readonly currentStatus: string;
1601
+ readonly attemptedAction: 'cancel' | 'retry' | 'reschedule';
1602
+ constructor(message: string, jobId: string, currentStatus: string, attemptedAction: 'cancel' | 'retry' | 'reschedule');
1603
+ }
1604
+ /**
1605
+ * Error thrown when a pagination cursor is invalid or malformed.
1606
+ *
1607
+ * @example
1608
+ * ```typescript
1609
+ * try {
1610
+ * await monque.listJobs({ cursor: 'invalid-cursor' });
1611
+ * } catch (error) {
1612
+ * if (error instanceof InvalidCursorError) {
1613
+ * console.error('Invalid cursor provided');
1614
+ * }
1615
+ * }
1616
+ * ```
1617
+ */
1618
+ declare class InvalidCursorError extends MonqueError {
1619
+ constructor(message: string);
1620
+ }
1621
+ /**
1622
+ * Error thrown when a statistics aggregation times out.
1623
+ *
1624
+ * @example
1625
+ * ```typescript
1626
+ * try {
1627
+ * const stats = await monque.getQueueStats();
1628
+ * } catch (error) {
1629
+ * if (error instanceof AggregationTimeoutError) {
1630
+ * console.error('Stats took too long to calculate');
1631
+ * }
1632
+ * }
1633
+ * ```
1634
+ */
1635
+ declare class AggregationTimeoutError extends MonqueError {
1636
+ constructor(message?: string);
1637
+ }
1373
1638
  //#endregion
1374
1639
  //#region src/shared/utils/backoff.d.ts
1375
1640
  /**
@@ -1459,5 +1724,5 @@ declare function getNextCronDate(expression: string, currentDate?: Date): Date;
1459
1724
  */
1460
1725
  declare function validateCronExpression(expression: string): void;
1461
1726
  //#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 };
1727
+ 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
1728
  //# sourceMappingURL=index.d.cts.map