@monque/core 0.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.
@@ -0,0 +1,1460 @@
1
+ import { Db, ObjectId } from "mongodb";
2
+ import { EventEmitter } from "node:events";
3
+
4
+ //#region src/jobs/types.d.ts
5
+
6
+ /**
7
+ * Represents the lifecycle states of a job in the queue.
8
+ *
9
+ * Jobs transition through states as follows:
10
+ * - PENDING → PROCESSING (when picked up by a worker)
11
+ * - PROCESSING → COMPLETED (on success)
12
+ * - PROCESSING → PENDING (on failure, if retries remain)
13
+ * - PROCESSING → FAILED (on failure, after max retries exhausted)
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * if (job.status === JobStatus.PENDING) {
18
+ * // job is waiting to be picked up
19
+ * }
20
+ * ```
21
+ */
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";
31
+ };
32
+ /**
33
+ * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed'`
34
+ */
35
+ type JobStatusType = (typeof JobStatus)[keyof typeof JobStatus];
36
+ /**
37
+ * Represents a job in the Monque queue.
38
+ *
39
+ * @template T - The type of the job's data payload
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * interface EmailJobData {
44
+ * to: string;
45
+ * subject: string;
46
+ * template: string;
47
+ * }
48
+ *
49
+ * const job: Job<EmailJobData> = {
50
+ * name: 'send-email',
51
+ * data: { to: 'user@example.com', subject: 'Welcome!', template: 'welcome' },
52
+ * status: JobStatus.PENDING,
53
+ * nextRunAt: new Date(),
54
+ * failCount: 0,
55
+ * createdAt: new Date(),
56
+ * updatedAt: new Date(),
57
+ * };
58
+ * ```
59
+ */
60
+ interface Job<T = unknown> {
61
+ /** MongoDB document identifier */
62
+ _id?: ObjectId;
63
+ /** Job type identifier, matches worker registration */
64
+ name: string;
65
+ /** Job payload - must be JSON-serializable */
66
+ data: T;
67
+ /** Current lifecycle state */
68
+ status: JobStatusType;
69
+ /** When the job should be processed */
70
+ nextRunAt: Date;
71
+ /** Timestamp when job was locked for processing */
72
+ lockedAt?: Date | null;
73
+ /**
74
+ * Unique identifier of the scheduler instance that claimed this job.
75
+ * Used for atomic claim pattern - ensures only one instance processes each job.
76
+ * Set when a job is claimed, cleared when job completes or fails.
77
+ */
78
+ claimedBy?: string | null;
79
+ /**
80
+ * Timestamp of the last heartbeat update for this job.
81
+ * Used to detect stale jobs when a scheduler instance crashes without releasing.
82
+ * Updated periodically while job is being processed.
83
+ */
84
+ lastHeartbeat?: Date | null;
85
+ /**
86
+ * Heartbeat interval in milliseconds for this job.
87
+ * Stored on the job to allow recovery logic to use the correct timeout.
88
+ */
89
+ heartbeatInterval?: number;
90
+ /** Number of failed attempts */
91
+ failCount: number;
92
+ /** Last failure error message */
93
+ failReason?: string;
94
+ /** Cron expression for recurring jobs */
95
+ repeatInterval?: string;
96
+ /** Deduplication key to prevent duplicate jobs */
97
+ uniqueKey?: string;
98
+ /** Job creation timestamp */
99
+ createdAt: Date;
100
+ /** Last modification timestamp */
101
+ updatedAt: Date;
102
+ }
103
+ /**
104
+ * A job that has been persisted to MongoDB and has a guaranteed `_id`.
105
+ * This is returned by `enqueue()`, `now()`, and `schedule()` methods.
106
+ *
107
+ * @template T - The type of the job's data payload
108
+ */
109
+ type PersistedJob<T = unknown> = Job<T> & {
110
+ _id: ObjectId;
111
+ };
112
+ /**
113
+ * Options for enqueueing a job.
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * await monque.enqueue('sync-user', { userId: '123' }, {
118
+ * uniqueKey: 'sync-user-123',
119
+ * runAt: new Date(Date.now() + 5000), // Run in 5 seconds
120
+ * });
121
+ * ```
122
+ */
123
+ interface EnqueueOptions {
124
+ /**
125
+ * Deduplication key. If a job with this key is already pending or processing,
126
+ * the enqueue operation will not create a duplicate.
127
+ */
128
+ uniqueKey?: string;
129
+ /**
130
+ * When the job should be processed. Defaults to immediately (new Date()).
131
+ */
132
+ runAt?: Date;
133
+ }
134
+ /**
135
+ * Options for scheduling a recurring job.
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * await monque. schedule('0 * * * *', 'hourly-cleanup', { dir: '/tmp' }, {
140
+ * uniqueKey: 'hourly-cleanup-job',
141
+ * });
142
+ * ```
143
+ */
144
+ interface ScheduleOptions {
145
+ /**
146
+ * Deduplication key. If a job with this key is already pending or processing,
147
+ * the schedule operation will not create a duplicate.
148
+ */
149
+ uniqueKey?: string;
150
+ }
151
+ /**
152
+ * Filter options for querying jobs.
153
+ *
154
+ * Use with `monque.getJobs()` to filter jobs by name, status, or limit results.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * // Get all pending email jobs
159
+ * const pendingEmails = await monque.getJobs({
160
+ * name: 'send-email',
161
+ * status: JobStatus.PENDING,
162
+ * });
163
+ *
164
+ * // Get all failed or completed jobs (paginated)
165
+ * const finishedJobs = await monque.getJobs({
166
+ * status: [JobStatus.COMPLETED, JobStatus.FAILED],
167
+ * limit: 50,
168
+ * skip: 100,
169
+ * });
170
+ * ```
171
+ */
172
+ interface GetJobsFilter {
173
+ /** Filter by job type name */
174
+ name?: string;
175
+ /** Filter by status (single or multiple) */
176
+ status?: JobStatusType | JobStatusType[];
177
+ /** Maximum number of jobs to return (default: 100) */
178
+ limit?: number;
179
+ /** Number of jobs to skip for pagination */
180
+ skip?: number;
181
+ }
182
+ /**
183
+ * Handler function signature for processing jobs.
184
+ *
185
+ * @template T - The type of the job's data payload
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const emailHandler: JobHandler<EmailJobData> = async (job) => {
190
+ * await sendEmail(job.data.to, job.data.subject);
191
+ * };
192
+ * ```
193
+ */
194
+ type JobHandler<T = unknown> = (job: Job<T>) => Promise<void> | void;
195
+ //#endregion
196
+ //#region src/jobs/guards.d.ts
197
+ /**
198
+ * Type guard to check if a job has been persisted to MongoDB.
199
+ *
200
+ * A persisted job is guaranteed to have an `_id` field, which means it has been
201
+ * successfully inserted into the database. This is useful when you need to ensure
202
+ * a job can be updated or referenced by its ID.
203
+ *
204
+ * @template T - The type of the job's data payload
205
+ * @param job - The job to check
206
+ * @returns `true` if the job has a valid `_id`, narrowing the type to `PersistedJob<T>`
207
+ *
208
+ * @example Basic usage
209
+ * ```typescript
210
+ * const job: Job<EmailData> = await monque.enqueue('send-email', emailData);
211
+ *
212
+ * if (isPersistedJob(job)) {
213
+ * // TypeScript knows job._id exists
214
+ * console.log(`Job ID: ${job._id.toString()}`);
215
+ * }
216
+ * ```
217
+ *
218
+ * @example In a conditional
219
+ * ```typescript
220
+ * function logJobId(job: Job) {
221
+ * if (!isPersistedJob(job)) {
222
+ * console.log('Job not yet persisted');
223
+ * return;
224
+ * }
225
+ * // TypeScript knows job is PersistedJob here
226
+ * console.log(`Processing job ${job._id.toString()}`);
227
+ * }
228
+ * ```
229
+ */
230
+ declare function isPersistedJob<T>(job: Job<T>): job is PersistedJob<T>;
231
+ /**
232
+ * Type guard to check if a value is a valid job status.
233
+ *
234
+ * Validates that a value is one of the four valid job statuses: `'pending'`,
235
+ * `'processing'`, `'completed'`, or `'failed'`. Useful for runtime validation
236
+ * of user input or external data.
237
+ *
238
+ * @param value - The value to check
239
+ * @returns `true` if the value is a valid `JobStatusType`, narrowing the type
240
+ *
241
+ * @example Validating user input
242
+ * ```typescript
243
+ * function filterByStatus(status: string) {
244
+ * if (!isValidJobStatus(status)) {
245
+ * throw new Error(`Invalid status: ${status}`);
246
+ * }
247
+ * // TypeScript knows status is JobStatusType here
248
+ * return db.jobs.find({ status });
249
+ * }
250
+ * ```
251
+ *
252
+ * @example Runtime validation
253
+ * ```typescript
254
+ * const statusFromApi = externalData.status;
255
+ *
256
+ * if (isValidJobStatus(statusFromApi)) {
257
+ * job.status = statusFromApi;
258
+ * } else {
259
+ * job.status = JobStatus.PENDING;
260
+ * }
261
+ * ```
262
+ */
263
+ declare function isValidJobStatus(value: unknown): value is JobStatusType;
264
+ /**
265
+ * Type guard to check if a job is in pending status.
266
+ *
267
+ * A convenience helper for checking if a job is waiting to be processed.
268
+ * Equivalent to `job.status === JobStatus.PENDING` but with better semantics.
269
+ *
270
+ * @template T - The type of the job's data payload
271
+ * @param job - The job to check
272
+ * @returns `true` if the job status is `'pending'`
273
+ *
274
+ * @example Filter pending jobs
275
+ * ```typescript
276
+ * const jobs = await monque.getJobs();
277
+ * const pendingJobs = jobs.filter(isPendingJob);
278
+ * console.log(`${pendingJobs.length} jobs waiting to be processed`);
279
+ * ```
280
+ *
281
+ * @example Conditional logic
282
+ * ```typescript
283
+ * if (isPendingJob(job)) {
284
+ * await monque.now(job.name, job.data);
285
+ * }
286
+ * ```
287
+ */
288
+ declare function isPendingJob<T>(job: Job<T>): boolean;
289
+ /**
290
+ * Type guard to check if a job is currently being processed.
291
+ *
292
+ * A convenience helper for checking if a job is actively running.
293
+ * Equivalent to `job.status === JobStatus.PROCESSING` but with better semantics.
294
+ *
295
+ * @template T - The type of the job's data payload
296
+ * @param job - The job to check
297
+ * @returns `true` if the job status is `'processing'`
298
+ *
299
+ * @example Monitor active jobs
300
+ * ```typescript
301
+ * const jobs = await monque.getJobs();
302
+ * const activeJobs = jobs.filter(isProcessingJob);
303
+ * console.log(`${activeJobs.length} jobs currently running`);
304
+ * ```
305
+ */
306
+ declare function isProcessingJob<T>(job: Job<T>): boolean;
307
+ /**
308
+ * Type guard to check if a job has completed successfully.
309
+ *
310
+ * A convenience helper for checking if a job finished without errors.
311
+ * Equivalent to `job.status === JobStatus.COMPLETED` but with better semantics.
312
+ *
313
+ * @template T - The type of the job's data payload
314
+ * @param job - The job to check
315
+ * @returns `true` if the job status is `'completed'`
316
+ *
317
+ * @example Find completed jobs
318
+ * ```typescript
319
+ * const jobs = await monque.getJobs();
320
+ * const completedJobs = jobs.filter(isCompletedJob);
321
+ * console.log(`${completedJobs.length} jobs completed successfully`);
322
+ * ```
323
+ */
324
+ declare function isCompletedJob<T>(job: Job<T>): boolean;
325
+ /**
326
+ * Type guard to check if a job has permanently failed.
327
+ *
328
+ * A convenience helper for checking if a job exhausted all retries.
329
+ * Equivalent to `job.status === JobStatus.FAILED` but with better semantics.
330
+ *
331
+ * @template T - The type of the job's data payload
332
+ * @param job - The job to check
333
+ * @returns `true` if the job status is `'failed'`
334
+ *
335
+ * @example Handle failed jobs
336
+ * ```typescript
337
+ * const jobs = await monque.getJobs();
338
+ * const failedJobs = jobs.filter(isFailedJob);
339
+ *
340
+ * for (const job of failedJobs) {
341
+ * console.error(`Job ${job.name} failed: ${job.failReason}`);
342
+ * await sendAlert(job);
343
+ * }
344
+ * ```
345
+ */
346
+ declare function isFailedJob<T>(job: Job<T>): boolean;
347
+ /**
348
+ * Type guard to check if a job is a recurring scheduled job.
349
+ *
350
+ * A recurring job has a `repeatInterval` cron expression and will be automatically
351
+ * rescheduled after each successful completion.
352
+ *
353
+ * @template T - The type of the job's data payload
354
+ * @param job - The job to check
355
+ * @returns `true` if the job has a `repeatInterval` defined
356
+ *
357
+ * @example Filter recurring jobs
358
+ * ```typescript
359
+ * const jobs = await monque.getJobs();
360
+ * const recurringJobs = jobs.filter(isRecurringJob);
361
+ * console.log(`${recurringJobs.length} jobs will repeat automatically`);
362
+ * ```
363
+ *
364
+ * @example Conditional cleanup
365
+ * ```typescript
366
+ * if (!isRecurringJob(job) && isCompletedJob(job)) {
367
+ * // Safe to delete one-time completed jobs
368
+ * await deleteJob(job._id);
369
+ * }
370
+ * ```
371
+ */
372
+ declare function isRecurringJob<T>(job: Job<T>): boolean;
373
+ //#endregion
374
+ //#region src/events/types.d.ts
375
+ /**
376
+ * Event payloads for Monque lifecycle events.
377
+ */
378
+ interface MonqueEventMap {
379
+ /**
380
+ * Emitted when a job begins processing.
381
+ */
382
+ 'job:start': Job;
383
+ /**
384
+ * Emitted when a job finishes successfully.
385
+ */
386
+ 'job:complete': {
387
+ job: Job;
388
+ /** Processing duration in milliseconds */
389
+ duration: number;
390
+ };
391
+ /**
392
+ * Emitted when a job fails (may retry).
393
+ */
394
+ 'job:fail': {
395
+ job: Job;
396
+ error: Error;
397
+ /** Whether the job will be retried */
398
+ willRetry: boolean;
399
+ };
400
+ /**
401
+ * Emitted for unexpected errors during processing.
402
+ */
403
+ 'job:error': {
404
+ error: Error;
405
+ job?: Job;
406
+ };
407
+ /**
408
+ * Emitted when stale jobs are recovered on startup.
409
+ */
410
+ 'stale:recovered': {
411
+ count: number;
412
+ };
413
+ /**
414
+ * Emitted when the change stream is successfully connected.
415
+ */
416
+ 'changestream:connected': undefined;
417
+ /**
418
+ * Emitted when a change stream error occurs.
419
+ */
420
+ 'changestream:error': {
421
+ error: Error;
422
+ };
423
+ /**
424
+ * Emitted when the change stream is closed.
425
+ */
426
+ 'changestream:closed': undefined;
427
+ /**
428
+ * Emitted when falling back from change streams to polling-only mode.
429
+ */
430
+ 'changestream:fallback': {
431
+ reason: string;
432
+ };
433
+ }
434
+ //#endregion
435
+ //#region src/workers/types.d.ts
436
+ /**
437
+ * Options for registering a worker.
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * monque.worker('send-email', emailHandler, {
442
+ * concurrency: 3,
443
+ * });
444
+ * ```
445
+ */
446
+ interface WorkerOptions {
447
+ /**
448
+ * Number of concurrent jobs this worker can process.
449
+ * @default 5 (uses defaultConcurrency from MonqueOptions)
450
+ */
451
+ concurrency?: number;
452
+ /**
453
+ * Allow replacing an existing worker for the same job name.
454
+ * If false (default) and a worker already exists, throws WorkerRegistrationError.
455
+ * @default false
456
+ */
457
+ replace?: boolean;
458
+ }
459
+ //#endregion
460
+ //#region src/scheduler/types.d.ts
461
+ /**
462
+ * Configuration options for the Monque scheduler.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * const monque = new Monque(db, {
467
+ * collectionName: 'jobs',
468
+ * pollInterval: 1000,
469
+ * maxRetries: 10,
470
+ * baseRetryInterval: 1000,
471
+ * shutdownTimeout: 30000,
472
+ * defaultConcurrency: 5,
473
+ * });
474
+ * ```
475
+ */
476
+ interface MonqueOptions {
477
+ /**
478
+ * Name of the MongoDB collection for storing jobs.
479
+ * @default 'monque_jobs'
480
+ */
481
+ collectionName?: string;
482
+ /**
483
+ * Interval in milliseconds between polling for new jobs.
484
+ * @default 1000
485
+ */
486
+ pollInterval?: number;
487
+ /**
488
+ * Maximum number of retry attempts before marking a job as permanently failed.
489
+ * @default 10
490
+ */
491
+ maxRetries?: number;
492
+ /**
493
+ * Base interval in milliseconds for exponential backoff calculation.
494
+ * Actual delay = 2^failCount * baseRetryInterval
495
+ * @default 1000
496
+ */
497
+ baseRetryInterval?: number;
498
+ /**
499
+ * Maximum delay in milliseconds for exponential backoff.
500
+ * If calculated delay exceeds this value, it will be capped.
501
+ *
502
+ * Defaults to 24 hours to prevent unbounded delays.
503
+ * @default 86400000 (24 hours)
504
+ */
505
+ maxBackoffDelay?: number | undefined;
506
+ /**
507
+ * Timeout in milliseconds for graceful shutdown.
508
+ * @default 30000
509
+ */
510
+ shutdownTimeout?: number;
511
+ /**
512
+ * Default number of concurrent jobs per worker.
513
+ * @default 5
514
+ */
515
+ defaultConcurrency?: number;
516
+ /**
517
+ * Maximum time in milliseconds a job can be in 'processing' status before
518
+ * being considered stale and eligible for recovery.
519
+ *
520
+ * Stale recovery uses `lockedAt` as the source of truth; this is an absolute
521
+ * “time locked” limit, not a heartbeat timeout.
522
+ * @default 1800000 (30 minutes)
523
+ */
524
+ lockTimeout?: number;
525
+ /**
526
+ * Unique identifier for this scheduler instance.
527
+ * Used for atomic job claiming - each instance uses this ID to claim jobs.
528
+ * Defaults to a randomly generated UUID v4.
529
+ * @default crypto.randomUUID()
530
+ */
531
+ schedulerInstanceId?: string;
532
+ /**
533
+ * Interval in milliseconds for heartbeat updates during job processing.
534
+ * The scheduler periodically updates `lastHeartbeat` for all jobs it is processing
535
+ * to indicate liveness for monitoring/debugging.
536
+ *
537
+ * Note: stale recovery is based on `lockedAt` + `lockTimeout`, not `lastHeartbeat`.
538
+ * @default 30000 (30 seconds)
539
+ */
540
+ heartbeatInterval?: number;
541
+ /**
542
+ * Whether to recover stale processing jobs on scheduler startup.
543
+ * When true, jobs with lockedAt older than lockTimeout will be reset to pending.
544
+ * @default true
545
+ */
546
+ recoverStaleJobs?: boolean;
547
+ /**
548
+ * Configuration for automatic cleanup of completed and failed jobs.
549
+ * If undefined, no cleanup is performed.
550
+ */
551
+ jobRetention?: {
552
+ /**
553
+ * Age in milliseconds after which completed jobs are deleted.
554
+ * Cleaned up based on 'updatedAt' timestamp.
555
+ */
556
+ completed?: number;
557
+ /**
558
+ * Age in milliseconds after which failed jobs are deleted.
559
+ * Cleaned up based on 'updatedAt' timestamp.
560
+ */
561
+ failed?: number;
562
+ /**
563
+ * Interval in milliseconds for running the cleanup job.
564
+ * @default 3600000 (1 hour)
565
+ */
566
+ interval?: number;
567
+ } | undefined;
568
+ }
569
+ //#endregion
570
+ //#region src/scheduler/monque.d.ts
571
+ /**
572
+ * Monque - MongoDB-backed job scheduler
573
+ *
574
+ * A type-safe job scheduler with atomic locking, exponential backoff, cron scheduling,
575
+ * stale job recovery, and event-driven observability. Built on native MongoDB driver.
576
+ *
577
+ * @example Complete lifecycle
578
+ * ```;
579
+ typescript
580
+ *
581
+
582
+ import { Monque } from '@monque/core';
583
+
584
+ *
585
+
586
+ import { MongoClient } from 'mongodb';
587
+
588
+ *
589
+ *
590
+ const client = new MongoClient('mongodb://localhost:27017');
591
+ * await client.connect()
592
+ *
593
+ const db = client.db('myapp');
594
+ *
595
+ * // Create instance with options
596
+ *
597
+ const monque = new Monque(db, {
598
+ * collectionName: 'jobs',
599
+ * pollInterval: 1000,
600
+ * maxRetries: 10,
601
+ * shutdownTimeout: 30000,
602
+ * });
603
+ *
604
+ * // Initialize (sets up indexes and recovers stale jobs)
605
+ * await monque.initialize()
606
+ *
607
+ * // Register workers with type safety
608
+ *
609
+ type EmailJob = {};
610
+ * to: string
611
+ * subject: string
612
+ * body: string
613
+ * }
614
+ *
615
+ * monque.worker<EmailJob>('send-email', async (job) =>
616
+ {
617
+ * await emailService.send(job.data.to, job.data.subject, job.data.body)
618
+ *
619
+ }
620
+ )
621
+ *
622
+ * // Monitor events for observability
623
+ * monque.on('job:complete', (
624
+ {
625
+ job, duration;
626
+ }
627
+ ) =>
628
+ {
629
+ * logger.info(`Job $job.namecompleted in $durationms`);
630
+ * });
631
+ *
632
+ * monque.on('job:fail', ({ job, error, willRetry }) => {
633
+ * logger.error(`Job $job.namefailed:`, error);
634
+ * });
635
+ *
636
+ * // Start processing
637
+ * monque.start();
638
+ *
639
+ * // Enqueue jobs
640
+ * await monque.enqueue('send-email', {
641
+ * to: 'user@example.com',
642
+ * subject: 'Welcome!',
643
+ * body: 'Thanks for signing up.'
644
+ * });
645
+ *
646
+ * // Graceful shutdown
647
+ * process.on('SIGTERM', async () => {
648
+ * await monque.stop();
649
+ * await client.close();
650
+ * process.exit(0);
651
+ * });
652
+ * ```
653
+ */
654
+ declare class Monque extends EventEmitter {
655
+ private readonly db;
656
+ private readonly options;
657
+ private collection;
658
+ private workers;
659
+ private pollIntervalId;
660
+ private heartbeatIntervalId;
661
+ private cleanupIntervalId;
662
+ private isRunning;
663
+ 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;
692
+ constructor(db: Db, options?: MonqueOptions);
693
+ /**
694
+ * Initialize the scheduler by setting up the MongoDB collection and indexes.
695
+ * Must be called before start().
696
+ *
697
+ * @throws {ConnectionError} If collection or index creation fails
698
+ */
699
+ initialize(): Promise<void>;
700
+ /**
701
+ * Create required MongoDB indexes for efficient job processing.
702
+ *
703
+ * The following indexes are created:
704
+ * - `{status, nextRunAt}` - For efficient job polling queries
705
+ * - `{name, uniqueKey}` - Partial unique index for deduplication (pending/processing only)
706
+ * - `{name, status}` - For job lookup by type
707
+ * - `{claimedBy, status}` - For finding jobs owned by a specific scheduler instance
708
+ * - `{lastHeartbeat, status}` - For monitoring/debugging queries (e.g., inspecting heartbeat age)
709
+ * - `{status, nextRunAt, claimedBy}` - For atomic claim queries (find unclaimed pending jobs)
710
+ * - `{lockedAt, lastHeartbeat, status}` - Supports recovery scans and monitoring access patterns
711
+ */
712
+ private createIndexes;
713
+ /**
714
+ * Recover stale jobs that were left in 'processing' status.
715
+ * A job is considered stale if its `lockedAt` timestamp exceeds the configured `lockTimeout`.
716
+ * Stale jobs are reset to 'pending' so they can be picked up by workers again.
717
+ */
718
+ private recoverStaleJobs;
719
+ /**
720
+ * Clean up old completed and failed jobs based on retention policy.
721
+ *
722
+ * - Removes completed jobs older than `jobRetention.completed`
723
+ * - Removes failed jobs older than `jobRetention.failed`
724
+ *
725
+ * The cleanup runs concurrently for both statuses if configured.
726
+ *
727
+ * @returns Promise resolving when all deletion operations complete
728
+ */
729
+ private cleanupJobs;
730
+ /**
731
+ * Enqueue a job for processing.
732
+ *
733
+ * Jobs are stored in MongoDB and processed by registered workers. Supports
734
+ * delayed execution via `runAt` and deduplication via `uniqueKey`.
735
+ *
736
+ * When a `uniqueKey` is provided, only one pending or processing job with that key
737
+ * can exist. Completed or failed jobs don't block new jobs with the same key.
738
+ *
739
+ * Failed jobs are automatically retried with exponential backoff up to `maxRetries`
740
+ * (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.
741
+ *
742
+ * @template T - The job data payload type (must be JSON-serializable)
743
+ * @param name - Job type identifier, must match a registered worker
744
+ * @param data - Job payload, will be passed to the worker handler
745
+ * @param options - Scheduling and deduplication options
746
+ * @returns Promise resolving to the created or existing job document
747
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
748
+ *
749
+ * @example Basic job enqueueing
750
+ * ```typescript
751
+ * await monque.enqueue('send-email', {
752
+ * to: 'user@example.com',
753
+ * subject: 'Welcome!',
754
+ * body: 'Thanks for signing up.'
755
+ * });
756
+ * ```
757
+ *
758
+ * @example Delayed execution
759
+ * ```typescript
760
+ * const oneHourLater = new Date(Date.now() + 3600000);
761
+ * await monque.enqueue('reminder', { message: 'Check in!' }, {
762
+ * runAt: oneHourLater
763
+ * });
764
+ * ```
765
+ *
766
+ * @example Prevent duplicates with unique key
767
+ * ```typescript
768
+ * await monque.enqueue('sync-user', { userId: '123' }, {
769
+ * uniqueKey: 'sync-user-123'
770
+ * });
771
+ * // Subsequent enqueues with same uniqueKey return existing pending/processing job
772
+ * ```
773
+ */
774
+ enqueue<T>(name: string, data: T, options?: EnqueueOptions): Promise<PersistedJob<T>>;
775
+ /**
776
+ * Enqueue a job for immediate processing.
777
+ *
778
+ * Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.
779
+ * Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).
780
+ *
781
+ * @template T - The job data payload type (must be JSON-serializable)
782
+ * @param name - Job type identifier, must match a registered worker
783
+ * @param data - Job payload, will be passed to the worker handler
784
+ * @returns Promise resolving to the created job document
785
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
786
+ *
787
+ * @example Send email immediately
788
+ * ```typescript
789
+ * await monque.now('send-email', {
790
+ * to: 'admin@example.com',
791
+ * subject: 'Alert',
792
+ * body: 'Immediate attention required'
793
+ * });
794
+ * ```
795
+ *
796
+ * @example Process order in background
797
+ * ```typescript
798
+ * const order = await createOrder(data);
799
+ * await monque.now('process-order', { orderId: order.id });
800
+ * return order; // Return immediately, processing happens async
801
+ * ```
802
+ */
803
+ now<T>(name: string, data: T): Promise<PersistedJob<T>>;
804
+ /**
805
+ * Schedule a recurring job with a cron expression.
806
+ *
807
+ * Creates a job that automatically re-schedules itself based on the cron pattern.
808
+ * Uses standard 5-field cron format: minute, hour, day of month, month, day of week.
809
+ * Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.
810
+ * After successful completion, the job is reset to `pending` status and scheduled
811
+ * for its next run based on the cron expression.
812
+ *
813
+ * When a `uniqueKey` is provided, only one pending or processing job with that key
814
+ * can exist. This prevents duplicate scheduled jobs on application restart.
815
+ *
816
+ * @template T - The job data payload type (must be JSON-serializable)
817
+ * @param cron - Cron expression (5 fields or predefined expression)
818
+ * @param name - Job type identifier, must match a registered worker
819
+ * @param data - Job payload, will be passed to the worker handler on each run
820
+ * @param options - Scheduling options (uniqueKey for deduplication)
821
+ * @returns Promise resolving to the created job document with `repeatInterval` set
822
+ * @throws {InvalidCronError} If cron expression is invalid
823
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
824
+ *
825
+ * @example Hourly cleanup job
826
+ * ```typescript
827
+ * await monque.schedule('0 * * * *', 'cleanup-temp-files', {
828
+ * directory: '/tmp/uploads'
829
+ * });
830
+ * ```
831
+ *
832
+ * @example Prevent duplicate scheduled jobs with unique key
833
+ * ```typescript
834
+ * await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {
835
+ * uniqueKey: 'hourly-report-sales'
836
+ * });
837
+ * // Subsequent calls with same uniqueKey return existing pending/processing job
838
+ * ```
839
+ *
840
+ * @example Daily report at midnight (using predefined expression)
841
+ * ```typescript
842
+ * await monque.schedule('@daily', 'daily-report', {
843
+ * reportType: 'sales',
844
+ * recipients: ['analytics@example.com']
845
+ * });
846
+ * ```
847
+ */
848
+ schedule<T>(cron: string, name: string, data: T, options?: ScheduleOptions): Promise<PersistedJob<T>>;
849
+ /**
850
+ * Register a worker to process jobs of a specific type.
851
+ *
852
+ * Workers can be registered before or after calling `start()`. Each worker
853
+ * processes jobs concurrently up to its configured concurrency limit (default: 5).
854
+ *
855
+ * The handler function receives the full job object including metadata (`_id`, `status`,
856
+ * `failCount`, etc.). If the handler throws an error, the job is retried with exponential
857
+ * backoff up to `maxRetries` times. After exhausting retries, the job is marked as `failed`.
858
+ *
859
+ * Events are emitted during job processing: `job:start`, `job:complete`, `job:fail`, and `job:error`.
860
+ *
861
+ * **Duplicate Registration**: By default, registering a worker for a job name that already has
862
+ * a worker will throw a `WorkerRegistrationError`. This fail-fast behavior prevents accidental
863
+ * replacement of handlers. To explicitly replace a worker, pass `{ replace: true }`.
864
+ *
865
+ * @template T - The job data payload type for type-safe access to `job.data`
866
+ * @param name - Job type identifier to handle
867
+ * @param handler - Async function to execute for each job
868
+ * @param options - Worker configuration
869
+ * @param options.concurrency - Maximum concurrent jobs for this worker (default: `defaultConcurrency`)
870
+ * @param options.replace - When `true`, replace existing worker instead of throwing error
871
+ * @throws {WorkerRegistrationError} When a worker is already registered for `name` and `replace` is not `true`
872
+ *
873
+ * @example Basic email worker
874
+ * ```typescript
875
+ * interface EmailJob {
876
+ * to: string;
877
+ * subject: string;
878
+ * body: string;
879
+ * }
880
+ *
881
+ * monque.worker<EmailJob>('send-email', async (job) => {
882
+ * await emailService.send(job.data.to, job.data.subject, job.data.body);
883
+ * });
884
+ * ```
885
+ *
886
+ * @example Worker with custom concurrency
887
+ * ```typescript
888
+ * // Limit to 2 concurrent video processing jobs (resource-intensive)
889
+ * monque.worker('process-video', async (job) => {
890
+ * await videoProcessor.transcode(job.data.videoId);
891
+ * }, { concurrency: 2 });
892
+ * ```
893
+ *
894
+ * @example Replacing an existing worker
895
+ * ```typescript
896
+ * // Replace the existing handler for 'send-email'
897
+ * monque.worker('send-email', newEmailHandler, { replace: true });
898
+ * ```
899
+ *
900
+ * @example Worker with error handling
901
+ * ```typescript
902
+ * monque.worker('sync-user', async (job) => {
903
+ * try {
904
+ * await externalApi.syncUser(job.data.userId);
905
+ * } catch (error) {
906
+ * // Job will retry with exponential backoff
907
+ * // Delay = 2^failCount × baseRetryInterval (default: 1000ms)
908
+ * throw new Error(`Sync failed: ${error.message}`);
909
+ * }
910
+ * });
911
+ * ```
912
+ */
913
+ worker<T>(name: string, handler: JobHandler<T>, options?: WorkerOptions): void;
914
+ /**
915
+ * Start polling for and processing jobs.
916
+ *
917
+ * Begins polling MongoDB at the configured interval (default: 1 second) to pick up
918
+ * pending jobs and dispatch them to registered workers. Must call `initialize()` first.
919
+ * Workers can be registered before or after calling `start()`.
920
+ *
921
+ * Jobs are processed concurrently up to each worker's configured concurrency limit.
922
+ * The scheduler continues running until `stop()` is called.
923
+ *
924
+ * @example Basic startup
925
+ * ```typescript
926
+ * const monque = new Monque(db);
927
+ * await monque.initialize();
928
+ *
929
+ * monque.worker('send-email', emailHandler);
930
+ * monque.worker('process-order', orderHandler);
931
+ *
932
+ * monque.start(); // Begin processing jobs
933
+ * ```
934
+ *
935
+ * @example With event monitoring
936
+ * ```typescript
937
+ * monque.on('job:start', (job) => {
938
+ * logger.info(`Starting job ${job.name}`);
939
+ * });
940
+ *
941
+ * monque.on('job:complete', ({ job, duration }) => {
942
+ * metrics.recordJobDuration(job.name, duration);
943
+ * });
944
+ *
945
+ * monque.on('job:fail', ({ job, error, willRetry }) => {
946
+ * logger.error(`Job ${job.name} failed:`, error);
947
+ * if (!willRetry) {
948
+ * alerting.sendAlert(`Job permanently failed: ${job.name}`);
949
+ * }
950
+ * });
951
+ *
952
+ * monque.start();
953
+ * ```
954
+ *
955
+ * @throws {ConnectionError} If scheduler not initialized (call `initialize()` first)
956
+ */
957
+ start(): void;
958
+ /**
959
+ * Stop the scheduler gracefully, waiting for in-progress jobs to complete.
960
+ *
961
+ * Stops polling for new jobs and waits for all active jobs to finish processing.
962
+ * Times out after the configured `shutdownTimeout` (default: 30 seconds), emitting
963
+ * a `job:error` event with a `ShutdownTimeoutError` containing incomplete jobs.
964
+ * On timeout, jobs still in progress are left as `processing` for stale job recovery.
965
+ *
966
+ * It's safe to call `stop()` multiple times - subsequent calls are no-ops if already stopped.
967
+ *
968
+ * @returns Promise that resolves when all jobs complete or timeout is reached
969
+ *
970
+ * @example Graceful application shutdown
971
+ * ```typescript
972
+ * process.on('SIGTERM', async () => {
973
+ * console.log('Shutting down gracefully...');
974
+ * await monque.stop(); // Wait for jobs to complete
975
+ * await mongoClient.close();
976
+ * process.exit(0);
977
+ * });
978
+ * ```
979
+ *
980
+ * @example With timeout handling
981
+ * ```typescript
982
+ * monque.on('job:error', ({ error }) => {
983
+ * if (error.name === 'ShutdownTimeoutError') {
984
+ * logger.warn('Forced shutdown after timeout:', error.incompleteJobs);
985
+ * }
986
+ * });
987
+ *
988
+ * await monque.stop();
989
+ * ```
990
+ */
991
+ stop(): Promise<void>;
992
+ /**
993
+ * Check if the scheduler is healthy (running and connected).
994
+ *
995
+ * Returns `true` when the scheduler is started, initialized, and has an active
996
+ * MongoDB collection reference. Useful for health check endpoints and monitoring.
997
+ *
998
+ * A healthy scheduler:
999
+ * - Has called `initialize()` successfully
1000
+ * - Has called `start()` and is actively polling
1001
+ * - Has a valid MongoDB collection reference
1002
+ *
1003
+ * @returns `true` if scheduler is running and connected, `false` otherwise
1004
+ *
1005
+ * @example Express health check endpoint
1006
+ * ```typescript
1007
+ * app.get('/health', (req, res) => {
1008
+ * const healthy = monque.isHealthy();
1009
+ * res.status(healthy ? 200 : 503).json({
1010
+ * status: healthy ? 'ok' : 'unavailable',
1011
+ * scheduler: healthy,
1012
+ * timestamp: new Date().toISOString()
1013
+ * });
1014
+ * });
1015
+ * ```
1016
+ *
1017
+ * @example Kubernetes readiness probe
1018
+ * ```typescript
1019
+ * app.get('/readyz', (req, res) => {
1020
+ * if (monque.isHealthy() && dbConnected) {
1021
+ * res.status(200).send('ready');
1022
+ * } else {
1023
+ * res.status(503).send('not ready');
1024
+ * }
1025
+ * });
1026
+ * ```
1027
+ *
1028
+ * @example Periodic health monitoring
1029
+ * ```typescript
1030
+ * setInterval(() => {
1031
+ * if (!monque.isHealthy()) {
1032
+ * logger.error('Scheduler unhealthy');
1033
+ * metrics.increment('scheduler.unhealthy');
1034
+ * }
1035
+ * }, 60000); // Check every minute
1036
+ * ```
1037
+ */
1038
+ 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
+ *
1120
+ * @private
1121
+ */
1122
+ private poll;
1123
+ /**
1124
+ * Atomically acquire a pending job for processing using the claimedBy pattern.
1125
+ *
1126
+ * Uses MongoDB's `findOneAndUpdate` with atomic operations to ensure only one scheduler
1127
+ * instance can claim a job. The query ensures the job is:
1128
+ * - In pending status
1129
+ * - Has nextRunAt <= now
1130
+ * - Is not claimed by another instance (claimedBy is null/undefined)
1131
+ *
1132
+ * @private
1133
+ * @param name - The job type to acquire
1134
+ * @returns The acquired job with updated status, claimedBy, and heartbeat info, or `null` if no jobs available
1135
+ */
1136
+ private acquireJob;
1137
+ /**
1138
+ * Execute a job using its registered worker handler.
1139
+ *
1140
+ * Tracks the job as active during processing, emits lifecycle events, and handles
1141
+ * both success and failure cases. On success, calls `completeJob()`. On failure,
1142
+ * calls `failJob()` which implements exponential backoff retry logic.
1143
+ *
1144
+ * @private
1145
+ * @param job - The job to process
1146
+ * @param worker - The worker registration containing the handler and active job tracking
1147
+ */
1148
+ private processJob;
1149
+ /**
1150
+ * Mark a job as completed successfully.
1151
+ *
1152
+ * For recurring jobs (with `repeatInterval`), schedules the next run based on the cron
1153
+ * expression and resets `failCount` to 0. For one-time jobs, sets status to `completed`.
1154
+ * Clears `lockedAt` and `failReason` fields in both cases.
1155
+ *
1156
+ * @private
1157
+ * @param job - The job that completed successfully
1158
+ */
1159
+ private completeJob;
1160
+ /**
1161
+ * Handle job failure with exponential backoff retry logic.
1162
+ *
1163
+ * Increments `failCount` and calculates next retry time using exponential backoff:
1164
+ * `nextRunAt = 2^failCount × baseRetryInterval` (capped by optional `maxBackoffDelay`).
1165
+ *
1166
+ * If `failCount >= maxRetries`, marks job as permanently `failed`. Otherwise, resets
1167
+ * to `pending` status for retry. Stores error message in `failReason` field.
1168
+ *
1169
+ * @private
1170
+ * @param job - The job that failed
1171
+ * @param error - The error that caused the failure
1172
+ */
1173
+ private failJob;
1174
+ /**
1175
+ * Ensure the scheduler is initialized before operations.
1176
+ *
1177
+ * @private
1178
+ * @throws {ConnectionError} If scheduler not initialized or collection unavailable
1179
+ */
1180
+ private ensureInitialized;
1181
+ /**
1182
+ * Update heartbeats for all jobs claimed by this scheduler instance.
1183
+ *
1184
+ * This method runs periodically while the scheduler is running to indicate
1185
+ * that jobs are still being actively processed.
1186
+ *
1187
+ * `lastHeartbeat` is primarily an observability signal (monitoring/debugging).
1188
+ * Stale recovery is based on `lockedAt` + `lockTimeout`.
1189
+ *
1190
+ * @private
1191
+ */
1192
+ private updateHeartbeats;
1193
+ /**
1194
+ * Set up MongoDB Change Stream for real-time job notifications.
1195
+ *
1196
+ * Change streams provide instant notifications when jobs are inserted or when
1197
+ * job status changes to pending (e.g., after a retry). This eliminates the
1198
+ * polling delay for reactive job processing.
1199
+ *
1200
+ * The change stream watches for:
1201
+ * - Insert operations (new jobs)
1202
+ * - Update operations where status field changes
1203
+ *
1204
+ * If change streams are unavailable (e.g., standalone MongoDB), the system
1205
+ * gracefully falls back to polling-only mode.
1206
+ *
1207
+ * @private
1208
+ */
1209
+ private setupChangeStream;
1210
+ /**
1211
+ * Handle a change stream event by triggering a debounced poll.
1212
+ *
1213
+ * Events are debounced to prevent "claim storms" when multiple changes arrive
1214
+ * in rapid succession (e.g., bulk job inserts). A 100ms debounce window
1215
+ * collects multiple events and triggers a single poll.
1216
+ *
1217
+ * @private
1218
+ * @param change - The change stream event document
1219
+ */
1220
+ private handleChangeStreamEvent;
1221
+ /**
1222
+ * Handle change stream errors with exponential backoff reconnection.
1223
+ *
1224
+ * Attempts to reconnect up to `maxChangeStreamReconnectAttempts` times with
1225
+ * exponential backoff (base 1000ms). After exhausting retries, falls back to
1226
+ * polling-only mode.
1227
+ *
1228
+ * @private
1229
+ * @param error - The error that caused the change stream failure
1230
+ */
1231
+ private handleChangeStreamError;
1232
+ /**
1233
+ * Close the change stream cursor and emit closed event.
1234
+ *
1235
+ * @private
1236
+ */
1237
+ private closeChangeStream;
1238
+ /**
1239
+ * Get array of active job IDs across all workers.
1240
+ *
1241
+ * @private
1242
+ * @returns Array of job ID strings currently being processed
1243
+ */
1244
+ private getActiveJobs;
1245
+ /**
1246
+ * Get list of active job documents (for shutdown timeout error).
1247
+ *
1248
+ * @private
1249
+ * @returns Array of active Job objects
1250
+ */
1251
+ private getActiveJobsList;
1252
+ /**
1253
+ * Convert a MongoDB document to a typed PersistedJob object.
1254
+ *
1255
+ * Maps raw MongoDB document fields to the strongly-typed `PersistedJob<T>` interface,
1256
+ * ensuring type safety and handling optional fields (`lockedAt`, `failReason`, etc.).
1257
+ *
1258
+ * @private
1259
+ * @template T - The job data payload type
1260
+ * @param doc - The raw MongoDB document with `_id`
1261
+ * @returns A strongly-typed PersistedJob object with guaranteed `_id`
1262
+ */
1263
+ private documentToPersistedJob;
1264
+ /**
1265
+ * Type-safe event emitter methods
1266
+ */
1267
+ emit<K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]): boolean;
1268
+ on<K extends keyof MonqueEventMap>(event: K, listener: (payload: MonqueEventMap[K]) => void): this;
1269
+ once<K extends keyof MonqueEventMap>(event: K, listener: (payload: MonqueEventMap[K]) => void): this;
1270
+ off<K extends keyof MonqueEventMap>(event: K, listener: (payload: MonqueEventMap[K]) => void): this;
1271
+ }
1272
+ //#endregion
1273
+ //#region src/shared/errors.d.ts
1274
+ /**
1275
+ * Base error class for all Monque-related errors.
1276
+ *
1277
+ * @example
1278
+ * ```typescript
1279
+ * try {
1280
+ * await monque.enqueue('job', data);
1281
+ * } catch (error) {
1282
+ * if (error instanceof MonqueError) {
1283
+ * console.error('Monque error:', error.message);
1284
+ * }
1285
+ * }
1286
+ * ```
1287
+ */
1288
+ declare class MonqueError extends Error {
1289
+ constructor(message: string);
1290
+ }
1291
+ /**
1292
+ * Error thrown when an invalid cron expression is provided.
1293
+ *
1294
+ * @example
1295
+ * ```typescript
1296
+ * try {
1297
+ * await monque.schedule('invalid cron', 'job', data);
1298
+ * } catch (error) {
1299
+ * if (error instanceof InvalidCronError) {
1300
+ * console.error('Invalid expression:', error.expression);
1301
+ * }
1302
+ * }
1303
+ * ```
1304
+ */
1305
+ declare class InvalidCronError extends MonqueError {
1306
+ readonly expression: string;
1307
+ constructor(expression: string, message: string);
1308
+ }
1309
+ /**
1310
+ * Error thrown when there's a database connection issue.
1311
+ *
1312
+ * @example
1313
+ * ```typescript
1314
+ * try {
1315
+ * await monque.enqueue('job', data);
1316
+ * } catch (error) {
1317
+ * if (error instanceof ConnectionError) {
1318
+ * console.error('Database connection lost');
1319
+ * }
1320
+ * }
1321
+ * ```
1322
+ */
1323
+ declare class ConnectionError extends MonqueError {
1324
+ constructor(message: string, options?: {
1325
+ cause?: Error;
1326
+ });
1327
+ }
1328
+ /**
1329
+ * Error thrown when graceful shutdown times out.
1330
+ * Includes information about jobs that were still in progress.
1331
+ *
1332
+ * @example
1333
+ * ```typescript
1334
+ * try {
1335
+ * await monque.stop();
1336
+ * } catch (error) {
1337
+ * if (error instanceof ShutdownTimeoutError) {
1338
+ * console.error('Incomplete jobs:', error.incompleteJobs.length);
1339
+ * }
1340
+ * }
1341
+ * ```
1342
+ */
1343
+ declare class ShutdownTimeoutError extends MonqueError {
1344
+ readonly incompleteJobs: Job[];
1345
+ constructor(message: string, incompleteJobs: Job[]);
1346
+ }
1347
+ /**
1348
+ * Error thrown when attempting to register a worker for a job name
1349
+ * that already has a registered worker, without explicitly allowing replacement.
1350
+ *
1351
+ * @example
1352
+ * ```typescript
1353
+ * try {
1354
+ * monque.worker('send-email', handler1);
1355
+ * monque.worker('send-email', handler2); // throws
1356
+ * } catch (error) {
1357
+ * if (error instanceof WorkerRegistrationError) {
1358
+ * console.error('Worker already registered for:', error.jobName);
1359
+ * }
1360
+ * }
1361
+ *
1362
+ * // To intentionally replace a worker:
1363
+ * monque.worker('send-email', handler2, { replace: true });
1364
+ * ```
1365
+ */
1366
+ declare class WorkerRegistrationError extends MonqueError {
1367
+ readonly jobName: string;
1368
+ constructor(message: string, jobName: string);
1369
+ }
1370
+ //#endregion
1371
+ //#region src/shared/utils/backoff.d.ts
1372
+ /**
1373
+ * Default base interval for exponential backoff in milliseconds.
1374
+ * @default 1000
1375
+ */
1376
+ declare const DEFAULT_BASE_INTERVAL = 1000;
1377
+ /**
1378
+ * Default maximum delay cap for exponential backoff in milliseconds.
1379
+ *
1380
+ * This prevents unbounded delays (e.g. failCount=20 is >11 days at 1s base)
1381
+ * and avoids precision/overflow issues for very large fail counts.
1382
+ * @default 86400000 (24 hours)
1383
+ */
1384
+ declare const DEFAULT_MAX_BACKOFF_DELAY: number;
1385
+ /**
1386
+ * Calculate the next run time using exponential backoff.
1387
+ *
1388
+ * Formula: nextRunAt = now + (2^failCount × baseInterval)
1389
+ *
1390
+ * @param failCount - Number of previous failed attempts
1391
+ * @param baseInterval - Base interval in milliseconds (default: 1000ms)
1392
+ * @param maxDelay - Maximum delay in milliseconds (optional)
1393
+ * @returns The next run date
1394
+ *
1395
+ * @example
1396
+ * ```typescript
1397
+ * // First retry (failCount=1): 2^1 * 1000 = 2000ms delay
1398
+ * const nextRun = calculateBackoff(1);
1399
+ *
1400
+ * // Second retry (failCount=2): 2^2 * 1000 = 4000ms delay
1401
+ * const nextRun = calculateBackoff(2);
1402
+ *
1403
+ * // With custom base interval
1404
+ * const nextRun = calculateBackoff(3, 500); // 2^3 * 500 = 4000ms delay
1405
+ *
1406
+ * // With max delay
1407
+ * const nextRun = calculateBackoff(10, 1000, 60000); // capped at 60000ms
1408
+ * ```
1409
+ */
1410
+ declare function calculateBackoff(failCount: number, baseInterval?: number, maxDelay?: number): Date;
1411
+ /**
1412
+ * Calculate just the delay in milliseconds for a given fail count.
1413
+ *
1414
+ * @param failCount - Number of previous failed attempts
1415
+ * @param baseInterval - Base interval in milliseconds (default: 1000ms)
1416
+ * @param maxDelay - Maximum delay in milliseconds (optional)
1417
+ * @returns The delay in milliseconds
1418
+ */
1419
+ declare function calculateBackoffDelay(failCount: number, baseInterval?: number, maxDelay?: number): number;
1420
+ //#endregion
1421
+ //#region src/shared/utils/cron.d.ts
1422
+ /**
1423
+ * Parse a cron expression and return the next scheduled run date.
1424
+ *
1425
+ * @param expression - A 5-field cron expression (minute hour day-of-month month day-of-week) or a predefined expression
1426
+ * @param currentDate - The reference date for calculating next run (default: now)
1427
+ * @returns The next scheduled run date
1428
+ * @throws {InvalidCronError} If the cron expression is invalid
1429
+ *
1430
+ * @example
1431
+ * ```typescript
1432
+ * // Every minute
1433
+ * const nextRun = getNextCronDate('* * * * *');
1434
+ *
1435
+ * // Every day at midnight
1436
+ * const nextRun = getNextCronDate('0 0 * * *');
1437
+ *
1438
+ * // Using predefined expression
1439
+ * const nextRun = getNextCronDate('@daily');
1440
+ *
1441
+ * // Every Monday at 9am
1442
+ * const nextRun = getNextCronDate('0 9 * * 1');
1443
+ * ```
1444
+ */
1445
+ declare function getNextCronDate(expression: string, currentDate?: Date): Date;
1446
+ /**
1447
+ * Validate a cron expression without calculating the next run date.
1448
+ *
1449
+ * @param expression - A 5-field cron expression
1450
+ * @throws {InvalidCronError} If the cron expression is invalid
1451
+ *
1452
+ * @example
1453
+ * ```typescript
1454
+ * validateCronExpression('0 9 * * 1'); // Throws if invalid
1455
+ * ```
1456
+ */
1457
+ declare function validateCronExpression(expression: string): void;
1458
+ //#endregion
1459
+ 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 };
1460
+ //# sourceMappingURL=index.d.cts.map