@monque/core 1.5.1 → 1.6.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/CHANGELOG.md +20 -0
- package/dist/index.cjs +266 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +21 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +266 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/scheduler/monque.ts +25 -1
- package/src/scheduler/services/change-stream-handler.ts +222 -51
- package/src/scheduler/services/job-manager.ts +4 -1
- package/src/scheduler/services/job-processor.ts +43 -6
- package/src/scheduler/services/job-scheduler.ts +18 -4
- package/src/scheduler/services/lifecycle-manager.ts +72 -17
- package/src/scheduler/services/types.ts +4 -0
- package/src/scheduler/types.ts +14 -0
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["CronExpressionParser","ObjectId","ObjectId","BSON","EventEmitter"],"sources":["../src/jobs/document-to-persisted-job.ts","../src/jobs/types.ts","../src/jobs/guards.ts","../src/shared/errors.ts","../src/shared/utils/backoff.ts","../src/shared/utils/cron.ts","../src/shared/utils/error.ts","../src/scheduler/helpers.ts","../src/scheduler/services/change-stream-handler.ts","../src/scheduler/services/job-manager.ts","../src/scheduler/services/job-processor.ts","../src/scheduler/services/job-query.ts","../src/scheduler/services/job-scheduler.ts","../src/scheduler/services/lifecycle-manager.ts","../src/scheduler/monque.ts"],"sourcesContent":["import type { Document, WithId } from 'mongodb';\n\nimport type { PersistedJob } from './types.js';\n\n/**\n * Convert a raw MongoDB document to a strongly-typed {@link PersistedJob}.\n *\n * Maps required fields directly and conditionally includes optional fields\n * only when they are present in the document (`!== undefined`).\n *\n * @internal Not part of the public API.\n * @template T - The job data payload type\n * @param doc - The raw MongoDB document with `_id`\n * @returns A strongly-typed PersistedJob object with guaranteed `_id`\n */\nexport function documentToPersistedJob<T = unknown>(doc: WithId<Document>): PersistedJob<T> {\n\tconst job: PersistedJob<T> = {\n\t\t_id: doc._id,\n\t\tname: doc['name'],\n\t\tdata: doc['data'],\n\t\tstatus: doc['status'],\n\t\tnextRunAt: doc['nextRunAt'],\n\t\tfailCount: doc['failCount'],\n\t\tcreatedAt: doc['createdAt'],\n\t\tupdatedAt: doc['updatedAt'],\n\t};\n\n\t// Only set optional properties if they exist\n\tif (doc['lockedAt'] !== undefined) {\n\t\tjob.lockedAt = doc['lockedAt'];\n\t}\n\tif (doc['claimedBy'] !== undefined) {\n\t\tjob.claimedBy = doc['claimedBy'];\n\t}\n\tif (doc['lastHeartbeat'] !== undefined) {\n\t\tjob.lastHeartbeat = doc['lastHeartbeat'];\n\t}\n\tif (doc['heartbeatInterval'] !== undefined) {\n\t\tjob.heartbeatInterval = doc['heartbeatInterval'];\n\t}\n\tif (doc['failReason'] !== undefined) {\n\t\tjob.failReason = doc['failReason'];\n\t}\n\tif (doc['repeatInterval'] !== undefined) {\n\t\tjob.repeatInterval = doc['repeatInterval'];\n\t}\n\tif (doc['uniqueKey'] !== undefined) {\n\t\tjob.uniqueKey = doc['uniqueKey'];\n\t}\n\n\treturn job;\n}\n","import type { ObjectId } from 'mongodb';\n\n/**\n * Represents the lifecycle states of a job in the queue.\n *\n * Jobs transition through states as follows:\n * - PENDING → PROCESSING (when picked up by a worker)\n * - PROCESSING → COMPLETED (on success)\n * - PROCESSING → PENDING (on failure, if retries remain)\n * - PROCESSING → FAILED (on failure, after max retries exhausted)\n * - PENDING → CANCELLED (on manual cancellation)\n *\n * @example\n * ```typescript\n * if (job.status === JobStatus.PENDING) {\n * // job is waiting to be picked up\n * }\n * ```\n */\nexport const JobStatus = {\n\t/** Job is waiting to be picked up by a worker */\n\tPENDING: 'pending',\n\t/** Job is currently being executed by a worker */\n\tPROCESSING: 'processing',\n\t/** Job completed successfully */\n\tCOMPLETED: 'completed',\n\t/** Job permanently failed after exhausting all retry attempts */\n\tFAILED: 'failed',\n\t/** Job was manually cancelled */\n\tCANCELLED: 'cancelled',\n} as const;\n\n/**\n * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'`\n */\nexport type JobStatusType = (typeof JobStatus)[keyof typeof JobStatus];\n\n/**\n * Represents a job in the Monque queue.\n *\n * @template T - The type of the job's data payload\n *\n * @example\n * ```typescript\n * interface EmailJobData {\n * to: string;\n * subject: string;\n * template: string;\n * }\n *\n * const job: Job<EmailJobData> = {\n * name: 'send-email',\n * data: { to: 'user@example.com', subject: 'Welcome!', template: 'welcome' },\n * status: JobStatus.PENDING,\n * nextRunAt: new Date(),\n * failCount: 0,\n * createdAt: new Date(),\n * updatedAt: new Date(),\n * };\n * ```\n */\nexport interface Job<T = unknown> {\n\t/** MongoDB document identifier */\n\t_id?: ObjectId;\n\n\t/** Job type identifier, matches worker registration */\n\tname: string;\n\n\t/** Job payload - must be JSON-serializable */\n\tdata: T;\n\n\t/** Current lifecycle state */\n\tstatus: JobStatusType;\n\n\t/** When the job should be processed */\n\tnextRunAt: Date;\n\n\t/** Timestamp when job was locked for processing */\n\tlockedAt?: Date | null;\n\n\t/**\n\t * Unique identifier of the scheduler instance that claimed this job.\n\t * Used for atomic claim pattern - ensures only one instance processes each job.\n\t * Set when a job is claimed, cleared when job completes or fails.\n\t */\n\tclaimedBy?: string | null;\n\n\t/**\n\t * Timestamp of the last heartbeat update for this job.\n\t * Used to detect stale jobs when a scheduler instance crashes without releasing.\n\t * Updated periodically while job is being processed.\n\t */\n\tlastHeartbeat?: Date | null;\n\n\t/**\n\t * Heartbeat interval in milliseconds for this job.\n\t * Stored on the job to allow recovery logic to use the correct timeout.\n\t */\n\theartbeatInterval?: number;\n\n\t/** Number of failed attempts */\n\tfailCount: number;\n\n\t/** Last failure error message */\n\tfailReason?: string;\n\n\t/** Cron expression for recurring jobs */\n\trepeatInterval?: string;\n\n\t/** Deduplication key to prevent duplicate jobs */\n\tuniqueKey?: string;\n\n\t/** Job creation timestamp */\n\tcreatedAt: Date;\n\n\t/** Last modification timestamp */\n\tupdatedAt: Date;\n}\n\n/**\n * A job that has been persisted to MongoDB and has a guaranteed `_id`.\n * This is returned by `enqueue()`, `now()`, and `schedule()` methods.\n *\n * @template T - The type of the job's data payload\n */\nexport type PersistedJob<T = unknown> = Job<T> & { _id: ObjectId };\n\n/**\n * Options for enqueueing a job.\n *\n * @example\n * ```typescript\n * await monque.enqueue('sync-user', { userId: '123' }, {\n * uniqueKey: 'sync-user-123',\n * runAt: new Date(Date.now() + 5000), // Run in 5 seconds\n * });\n * ```\n */\nexport interface EnqueueOptions {\n\t/**\n\t * Deduplication key. If a job with this key is already pending or processing,\n\t * the enqueue operation will not create a duplicate.\n\t */\n\tuniqueKey?: string;\n\n\t/**\n\t * When the job should be processed. Defaults to immediately (new Date()).\n\t */\n\trunAt?: Date;\n}\n\n/**\n * Options for scheduling a recurring job.\n *\n * @example\n * ```typescript\n * await monque. schedule('0 * * * *', 'hourly-cleanup', { dir: '/tmp' }, {\n * uniqueKey: 'hourly-cleanup-job',\n * });\n * ```\n */\nexport interface ScheduleOptions {\n\t/**\n\t * Deduplication key. If a job with this key is already pending or processing,\n\t * the schedule operation will not create a duplicate.\n\t */\n\tuniqueKey?: string;\n}\n\n/**\n * Filter options for querying jobs.\n *\n * Use with `monque.getJobs()` to filter jobs by name, status, or limit results.\n *\n * @example\n * ```typescript\n * // Get all pending email jobs\n * const pendingEmails = await monque.getJobs({\n * name: 'send-email',\n * status: JobStatus.PENDING,\n * });\n *\n * // Get all failed or completed jobs (paginated)\n * const finishedJobs = await monque.getJobs({\n * status: [JobStatus.COMPLETED, JobStatus.FAILED],\n * limit: 50,\n * skip: 100,\n * });\n * ```\n */\nexport interface GetJobsFilter {\n\t/** Filter by job type name */\n\tname?: string;\n\n\t/** Filter by status (single or multiple) */\n\tstatus?: JobStatusType | JobStatusType[];\n\n\t/** Maximum number of jobs to return (default: 100) */\n\tlimit?: number;\n\n\t/** Number of jobs to skip for pagination */\n\tskip?: number;\n}\n\n/**\n * Handler function signature for processing jobs.\n *\n * @template T - The type of the job's data payload\n *\n * @example\n * ```typescript\n * const emailHandler: JobHandler<EmailJobData> = async (job) => {\n * await sendEmail(job.data.to, job.data.subject);\n * };\n * ```\n */\nexport type JobHandler<T = unknown> = (job: Job<T>) => Promise<void> | void;\n\n/**\n * Valid cursor directions for pagination.\n *\n * @example\n * ```typescript\n * const direction = CursorDirection.FORWARD;\n * ```\n */\nexport const CursorDirection = {\n\tFORWARD: 'forward',\n\tBACKWARD: 'backward',\n} as const;\n\nexport type CursorDirectionType = (typeof CursorDirection)[keyof typeof CursorDirection];\n\n/**\n * Selector options for bulk operations.\n *\n * Used to select multiple jobs for operations like cancellation or deletion.\n *\n * @example\n * ```typescript\n * // Select all failed jobs older than 7 days\n * const selector: JobSelector = {\n * status: JobStatus.FAILED,\n * olderThan: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),\n * };\n * ```\n */\nexport interface JobSelector {\n\tname?: string;\n\tstatus?: JobStatusType | JobStatusType[];\n\tolderThan?: Date;\n\tnewerThan?: Date;\n}\n\n/**\n * Options for cursor-based pagination.\n *\n * @example\n * ```typescript\n * const options: CursorOptions = {\n * limit: 50,\n * direction: CursorDirection.FORWARD,\n * filter: { status: JobStatus.PENDING },\n * };\n * ```\n */\nexport interface CursorOptions {\n\tcursor?: string;\n\tlimit?: number;\n\tdirection?: CursorDirectionType;\n\tfilter?: Pick<GetJobsFilter, 'name' | 'status'>;\n}\n\n/**\n * Response structure for cursor-based pagination.\n *\n * @template T - The type of the job's data payload\n *\n * @example\n * ```typescript\n * const page = await monque.listJobs({ limit: 10 });\n * console.log(`Got ${page.jobs.length} jobs`);\n *\n * if (page.hasNextPage) {\n * console.log(`Next cursor: ${page.cursor}`);\n * }\n * ```\n */\nexport interface CursorPage<T = unknown> {\n\tjobs: PersistedJob<T>[];\n\tcursor: string | null;\n\thasNextPage: boolean;\n\thasPreviousPage: boolean;\n}\n\n/**\n * Aggregated statistics for the job queue.\n *\n * @example\n * ```typescript\n * const stats = await monque.getQueueStats();\n * console.log(`Total jobs: ${stats.total}`);\n * console.log(`Pending: ${stats.pending}`);\n * console.log(`Processing: ${stats.processing}`);\n * console.log(`Failed: ${stats.failed}`);\n * console.log(`Start to finish avg: ${stats.avgProcessingDurationMs}ms`);\n * ```\n */\nexport interface QueueStats {\n\tpending: number;\n\tprocessing: number;\n\tcompleted: number;\n\tfailed: number;\n\tcancelled: number;\n\ttotal: number;\n\tavgProcessingDurationMs?: number;\n}\n\n/**\n * Result of a bulk operation.\n *\n * @example\n * ```typescript\n * const result = await monque.cancelJobs(selector);\n * console.log(`Cancelled ${result.count} jobs`);\n *\n * if (result.errors.length > 0) {\n * console.warn('Some jobs could not be cancelled:', result.errors);\n * }\n * ```\n */\nexport interface BulkOperationResult {\n\tcount: number;\n\terrors: Array<{ jobId: string; error: string }>;\n}\n","import type { Job, JobStatusType, PersistedJob } from './types.js';\nimport { JobStatus } from './types.js';\n\n/**\n * Type guard to check if a job has been persisted to MongoDB.\n *\n * A persisted job is guaranteed to have an `_id` field, which means it has been\n * successfully inserted into the database. This is useful when you need to ensure\n * a job can be updated or referenced by its ID.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job has a valid `_id`, narrowing the type to `PersistedJob<T>`\n *\n * @example Basic usage\n * ```typescript\n * const job: Job<EmailData> = await monque.enqueue('send-email', emailData);\n *\n * if (isPersistedJob(job)) {\n * // TypeScript knows job._id exists\n * console.log(`Job ID: ${job._id.toString()}`);\n * }\n * ```\n *\n * @example In a conditional\n * ```typescript\n * function logJobId(job: Job) {\n * if (!isPersistedJob(job)) {\n * console.log('Job not yet persisted');\n * return;\n * }\n * // TypeScript knows job is PersistedJob here\n * console.log(`Processing job ${job._id.toString()}`);\n * }\n * ```\n */\nexport function isPersistedJob<T>(job: Job<T>): job is PersistedJob<T> {\n\treturn '_id' in job && job._id !== undefined && job._id !== null;\n}\n\n/**\n * Type guard to check if a value is a valid job status.\n *\n * Validates that a value is one of the five valid job statuses: `'pending'`,\n * `'processing'`, `'completed'`, `'failed'`, or `'cancelled'`. Useful for runtime validation\n * of user input or external data.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid `JobStatusType`, narrowing the type\n *\n * @example Validating user input\n * ```typescript\n * function filterByStatus(status: string) {\n * if (!isValidJobStatus(status)) {\n * throw new Error(`Invalid status: ${status}`);\n * }\n * // TypeScript knows status is JobStatusType here\n * return db.jobs.find({ status });\n * }\n * ```\n *\n * @example Runtime validation\n * ```typescript\n * const statusFromApi = externalData.status;\n *\n * if (isValidJobStatus(statusFromApi)) {\n * job.status = statusFromApi;\n * } else {\n * job.status = JobStatus.PENDING;\n * }\n * ```\n */\nexport function isValidJobStatus(value: unknown): value is JobStatusType {\n\treturn typeof value === 'string' && Object.values(JobStatus).includes(value as JobStatusType);\n}\n\n/**\n * Type guard to check if a job is in pending status.\n *\n * A convenience helper for checking if a job is waiting to be processed.\n * Equivalent to `job.status === JobStatus.PENDING` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'pending'`\n *\n * @example Filter pending jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const pendingJobs = jobs.filter(isPendingJob);\n * console.log(`${pendingJobs.length} jobs waiting to be processed`);\n * ```\n *\n * @example Conditional logic\n * ```typescript\n * if (isPendingJob(job)) {\n * await monque.now(job.name, job.data);\n * }\n * ```\n */\nexport function isPendingJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.PENDING;\n}\n\n/**\n * Type guard to check if a job is currently being processed.\n *\n * A convenience helper for checking if a job is actively running.\n * Equivalent to `job.status === JobStatus.PROCESSING` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'processing'`\n *\n * @example Monitor active jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const activeJobs = jobs.filter(isProcessingJob);\n * console.log(`${activeJobs.length} jobs currently running`);\n * ```\n */\nexport function isProcessingJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.PROCESSING;\n}\n\n/**\n * Type guard to check if a job has completed successfully.\n *\n * A convenience helper for checking if a job finished without errors.\n * Equivalent to `job.status === JobStatus.COMPLETED` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'completed'`\n *\n * @example Find completed jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const completedJobs = jobs.filter(isCompletedJob);\n * console.log(`${completedJobs.length} jobs completed successfully`);\n * ```\n */\nexport function isCompletedJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.COMPLETED;\n}\n\n/**\n * Type guard to check if a job has permanently failed.\n *\n * A convenience helper for checking if a job exhausted all retries.\n * Equivalent to `job.status === JobStatus.FAILED` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'failed'`\n *\n * @example Handle failed jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const failedJobs = jobs.filter(isFailedJob);\n *\n * for (const job of failedJobs) {\n * console.error(`Job ${job.name} failed: ${job.failReason}`);\n * await sendAlert(job);\n * }\n * ```\n */\nexport function isFailedJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.FAILED;\n}\n\n/**\n * Type guard to check if a job has been manually cancelled.\n *\n * A convenience helper for checking if a job was cancelled by an operator.\n * Equivalent to `job.status === JobStatus.CANCELLED` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'cancelled'`\n *\n * @example Filter cancelled jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const cancelledJobs = jobs.filter(isCancelledJob);\n * console.log(`${cancelledJobs.length} jobs were cancelled`);\n * ```\n */\nexport function isCancelledJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.CANCELLED;\n}\n\n/**\n * Type guard to check if a job is a recurring scheduled job.\n *\n * A recurring job has a `repeatInterval` cron expression and will be automatically\n * rescheduled after each successful completion.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job has a `repeatInterval` defined\n *\n * @example Filter recurring jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const recurringJobs = jobs.filter(isRecurringJob);\n * console.log(`${recurringJobs.length} jobs will repeat automatically`);\n * ```\n *\n * @example Conditional cleanup\n * ```typescript\n * if (!isRecurringJob(job) && isCompletedJob(job)) {\n * // Safe to delete one-time completed jobs\n * await deleteJob(job._id);\n * }\n * ```\n */\nexport function isRecurringJob<T>(job: Job<T>): boolean {\n\treturn job.repeatInterval !== undefined && job.repeatInterval !== null;\n}\n","import type { Job } from '@/jobs';\n\n/**\n * Base error class for all Monque-related errors.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof MonqueError) {\n * console.error('Monque error:', error.message);\n * }\n * }\n * ```\n */\nexport class MonqueError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'MonqueError';\n\t\t// Maintains proper stack trace for where our error was thrown (only available on V8)\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, MonqueError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when an invalid cron expression is provided.\n *\n * @example\n * ```typescript\n * try {\n * await monque.schedule('invalid cron', 'job', data);\n * } catch (error) {\n * if (error instanceof InvalidCronError) {\n * console.error('Invalid expression:', error.expression);\n * }\n * }\n * ```\n */\nexport class InvalidCronError extends MonqueError {\n\tconstructor(\n\t\tpublic readonly expression: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'InvalidCronError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, InvalidCronError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when there's a database connection issue.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof ConnectionError) {\n * console.error('Database connection lost');\n * }\n * }\n * ```\n */\nexport class ConnectionError extends MonqueError {\n\tconstructor(message: string, options?: { cause?: Error }) {\n\t\tsuper(message);\n\t\tthis.name = 'ConnectionError';\n\t\tif (options?.cause) {\n\t\t\tthis.cause = options.cause;\n\t\t}\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ConnectionError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when graceful shutdown times out.\n * Includes information about jobs that were still in progress.\n *\n * @example\n * ```typescript\n * try {\n * await monque.stop();\n * } catch (error) {\n * if (error instanceof ShutdownTimeoutError) {\n * console.error('Incomplete jobs:', error.incompleteJobs.length);\n * }\n * }\n * ```\n */\nexport class ShutdownTimeoutError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly incompleteJobs: Job[],\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'ShutdownTimeoutError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ShutdownTimeoutError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when attempting to register a worker for a job name\n * that already has a registered worker, without explicitly allowing replacement.\n *\n * @example\n * ```typescript\n * try {\n * monque.register('send-email', handler1);\n * monque.register('send-email', handler2); // throws\n * } catch (error) {\n * if (error instanceof WorkerRegistrationError) {\n * console.error('Worker already registered for:', error.jobName);\n * }\n * }\n *\n * // To intentionally replace a worker:\n * monque.register('send-email', handler2, { replace: true });\n * ```\n */\nexport class WorkerRegistrationError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly jobName: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'WorkerRegistrationError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, WorkerRegistrationError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a state transition is invalid.\n *\n * @example\n * ```typescript\n * try {\n * await monque.cancelJob(jobId);\n * } catch (error) {\n * if (error instanceof JobStateError) {\n * console.error(`Cannot cancel job in state: ${error.currentStatus}`);\n * }\n * }\n * ```\n */\nexport class JobStateError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly jobId: string,\n\t\tpublic readonly currentStatus: string,\n\t\tpublic readonly attemptedAction: 'cancel' | 'retry' | 'reschedule',\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'JobStateError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, JobStateError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a pagination cursor is invalid or malformed.\n *\n * @example\n * ```typescript\n * try {\n * await monque.listJobs({ cursor: 'invalid-cursor' });\n * } catch (error) {\n * if (error instanceof InvalidCursorError) {\n * console.error('Invalid cursor provided');\n * }\n * }\n * ```\n */\nexport class InvalidCursorError extends MonqueError {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'InvalidCursorError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, InvalidCursorError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a statistics aggregation times out.\n *\n * @example\n * ```typescript\n * try {\n * const stats = await monque.getQueueStats();\n * } catch (error) {\n * if (error instanceof AggregationTimeoutError) {\n * console.error('Stats took too long to calculate');\n * }\n * }\n * ```\n */\nexport class AggregationTimeoutError extends MonqueError {\n\tconstructor(message: string = 'Statistics aggregation exceeded 30 second timeout') {\n\t\tsuper(message);\n\t\tthis.name = 'AggregationTimeoutError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, AggregationTimeoutError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a job payload exceeds the configured maximum BSON byte size.\n *\n * @example\n * ```typescript\n * const monque = new Monque(db, { maxPayloadSize: 1_000_000 }); // 1 MB\n *\n * try {\n * await monque.enqueue('job', hugePayload);\n * } catch (error) {\n * if (error instanceof PayloadTooLargeError) {\n * console.error(`Payload ${error.actualSize} bytes exceeds limit ${error.maxSize} bytes`);\n * }\n * }\n * ```\n */\nexport class PayloadTooLargeError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly actualSize: number,\n\t\tpublic readonly maxSize: number,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'PayloadTooLargeError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, PayloadTooLargeError);\n\t\t}\n\t}\n}\n","/**\n * Default base interval for exponential backoff in milliseconds.\n * @default 1000\n */\nexport const DEFAULT_BASE_INTERVAL = 1000;\n\n/**\n * Default maximum delay cap for exponential backoff in milliseconds.\n *\n * This prevents unbounded delays (e.g. failCount=20 is >11 days at 1s base)\n * and avoids precision/overflow issues for very large fail counts.\n * @default 86400000 (24 hours)\n */\nexport const DEFAULT_MAX_BACKOFF_DELAY = 24 * 60 * 60 * 1_000;\n\n/**\n * Calculate the next run time using exponential backoff.\n *\n * Formula: nextRunAt = now + (2^failCount × baseInterval)\n *\n * @param failCount - Number of previous failed attempts\n * @param baseInterval - Base interval in milliseconds (default: 1000ms)\n * @param maxDelay - Maximum delay in milliseconds (optional)\n * @returns The next run date\n *\n * @example\n * ```typescript\n * // First retry (failCount=1): 2^1 * 1000 = 2000ms delay\n * const nextRun = calculateBackoff(1);\n *\n * // Second retry (failCount=2): 2^2 * 1000 = 4000ms delay\n * const nextRun = calculateBackoff(2);\n *\n * // With custom base interval\n * const nextRun = calculateBackoff(3, 500); // 2^3 * 500 = 4000ms delay\n *\n * // With max delay\n * const nextRun = calculateBackoff(10, 1000, 60000); // capped at 60000ms\n * ```\n */\nexport function calculateBackoff(\n\tfailCount: number,\n\tbaseInterval: number = DEFAULT_BASE_INTERVAL,\n\tmaxDelay?: number,\n): Date {\n\tconst effectiveMaxDelay = maxDelay ?? DEFAULT_MAX_BACKOFF_DELAY;\n\tlet delay = 2 ** failCount * baseInterval;\n\n\tif (delay > effectiveMaxDelay) {\n\t\tdelay = effectiveMaxDelay;\n\t}\n\n\treturn new Date(Date.now() + delay);\n}\n\n/**\n * Calculate just the delay in milliseconds for a given fail count.\n *\n * @param failCount - Number of previous failed attempts\n * @param baseInterval - Base interval in milliseconds (default: 1000ms)\n * @param maxDelay - Maximum delay in milliseconds (optional)\n * @returns The delay in milliseconds\n */\nexport function calculateBackoffDelay(\n\tfailCount: number,\n\tbaseInterval: number = DEFAULT_BASE_INTERVAL,\n\tmaxDelay?: number,\n): number {\n\tconst effectiveMaxDelay = maxDelay ?? DEFAULT_MAX_BACKOFF_DELAY;\n\tlet delay = 2 ** failCount * baseInterval;\n\n\tif (delay > effectiveMaxDelay) {\n\t\tdelay = effectiveMaxDelay;\n\t}\n\n\treturn delay;\n}\n","import { CronExpressionParser } from 'cron-parser';\n\nimport { InvalidCronError } from '../errors.js';\n\n/**\n * Parse a cron expression and return the next scheduled run date.\n *\n * @param expression - A 5-field cron expression (minute hour day-of-month month day-of-week) or a predefined expression\n * @param currentDate - The reference date for calculating next run (default: now)\n * @returns The next scheduled run date\n * @throws {InvalidCronError} If the cron expression is invalid\n *\n * @example\n * ```typescript\n * // Every minute\n * const nextRun = getNextCronDate('* * * * *');\n *\n * // Every day at midnight\n * const nextRun = getNextCronDate('0 0 * * *');\n *\n * // Using predefined expression\n * const nextRun = getNextCronDate('@daily');\n *\n * // Every Monday at 9am\n * const nextRun = getNextCronDate('0 9 * * 1');\n * ```\n */\nexport function getNextCronDate(expression: string, currentDate?: Date): Date {\n\ttry {\n\t\tconst interval = CronExpressionParser.parse(expression, {\n\t\t\tcurrentDate: currentDate ?? new Date(),\n\t\t});\n\t\treturn interval.next().toDate();\n\t} catch (error) {\n\t\thandleCronParseError(expression, error);\n\t}\n}\n\n/**\n * Validate a cron expression without calculating the next run date.\n *\n * @param expression - A 5-field cron expression\n * @throws {InvalidCronError} If the cron expression is invalid\n *\n * @example\n * ```typescript\n * validateCronExpression('0 9 * * 1'); // Throws if invalid\n * ```\n */\nexport function validateCronExpression(expression: string): void {\n\ttry {\n\t\tCronExpressionParser.parse(expression);\n\t} catch (error) {\n\t\thandleCronParseError(expression, error);\n\t}\n}\n\nfunction handleCronParseError(expression: string, error: unknown): never {\n\t/* istanbul ignore next -- @preserve cron-parser always throws Error objects */\n\tconst errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';\n\tthrow new InvalidCronError(\n\t\texpression,\n\t\t`Invalid cron expression \"${expression}\": ${errorMessage}. ` +\n\t\t\t'Expected 5-field format: \"minute hour day-of-month month day-of-week\" or predefined expression (e.g. @daily). ' +\n\t\t\t'Example: \"0 9 * * 1\" (every Monday at 9am)',\n\t);\n}\n","/**\n * Normalize an unknown caught value into a proper `Error` instance.\n *\n * In JavaScript, any value can be thrown — strings, numbers, objects, `undefined`, etc.\n * This function ensures we always have a real `Error` with a proper stack trace and message.\n *\n * @param value - The caught value (typically from a `catch` block typed as `unknown`).\n * @returns The original value if already an `Error`, otherwise a new `Error` wrapping `String(value)`.\n *\n * @example\n * ```ts\n * try {\n * riskyOperation();\n * } catch (error: unknown) {\n * const normalized = toError(error);\n * console.error(normalized.message);\n * }\n * ```\n *\n * @internal\n */\nexport function toError(value: unknown): Error {\n\tif (value instanceof Error) return value;\n\n\ttry {\n\t\treturn new Error(String(value));\n\t} catch (conversionError: unknown) {\n\t\tconst detail =\n\t\t\tconversionError instanceof Error ? conversionError.message : 'unknown conversion failure';\n\n\t\treturn new Error(`Unserializable value (${detail})`);\n\t}\n}\n","import { type Document, type Filter, ObjectId } from 'mongodb';\n\nimport { CursorDirection, type CursorDirectionType, type JobSelector } from '@/jobs';\nimport { InvalidCursorError } from '@/shared';\n\n/**\n * Build a MongoDB query filter from a JobSelector.\n *\n * Translates the high-level `JobSelector` interface into a MongoDB `Filter<Document>`.\n * Handles array values for status (using `$in`) and date range filtering.\n *\n * @param filter - The user-provided job selector\n * @returns A standard MongoDB filter object\n */\nexport function buildSelectorQuery(filter: JobSelector): Filter<Document> {\n\tconst query: Filter<Document> = {};\n\n\tif (filter.name) {\n\t\tquery['name'] = filter.name;\n\t}\n\n\tif (filter.status) {\n\t\tif (Array.isArray(filter.status)) {\n\t\t\tquery['status'] = { $in: filter.status };\n\t\t} else {\n\t\t\tquery['status'] = filter.status;\n\t\t}\n\t}\n\n\tif (filter.olderThan || filter.newerThan) {\n\t\tquery['createdAt'] = {};\n\t\tif (filter.olderThan) {\n\t\t\tquery['createdAt'].$lt = filter.olderThan;\n\t\t}\n\t\tif (filter.newerThan) {\n\t\t\tquery['createdAt'].$gt = filter.newerThan;\n\t\t}\n\t}\n\n\treturn query;\n}\n\n/**\n * Encode an ObjectId and direction into an opaque cursor string.\n *\n * Format: `prefix` + `base64url(objectId)`\n * Prefix: 'F' (forward) or 'B' (backward)\n *\n * @param id - The job ID to use as the cursor anchor (exclusive)\n * @param direction - 'forward' or 'backward'\n * @returns Base64url-encoded cursor string\n */\nexport function encodeCursor(id: ObjectId, direction: CursorDirectionType): string {\n\tconst prefix = direction === 'forward' ? 'F' : 'B';\n\tconst buffer = Buffer.from(id.toHexString(), 'hex');\n\n\treturn prefix + buffer.toString('base64url');\n}\n\n/**\n * Decode an opaque cursor string into an ObjectId and direction.\n *\n * Validates format and returns the components.\n *\n * @param cursor - The opaque cursor string\n * @returns The decoded ID and direction\n * @throws {InvalidCursorError} If the cursor format is invalid or ID is malformed\n */\nexport function decodeCursor(cursor: string): {\n\tid: ObjectId;\n\tdirection: CursorDirectionType;\n} {\n\tif (!cursor || cursor.length < 2) {\n\t\tthrow new InvalidCursorError('Cursor is empty or too short');\n\t}\n\n\tconst prefix = cursor.charAt(0);\n\tconst payload = cursor.slice(1);\n\n\tlet direction: CursorDirectionType;\n\n\tif (prefix === 'F') {\n\t\tdirection = CursorDirection.FORWARD;\n\t} else if (prefix === 'B') {\n\t\tdirection = CursorDirection.BACKWARD;\n\t} else {\n\t\tthrow new InvalidCursorError(`Invalid cursor prefix: ${prefix}`);\n\t}\n\n\ttry {\n\t\tconst buffer = Buffer.from(payload, 'base64url');\n\t\tconst hex = buffer.toString('hex');\n\t\t// standard ObjectID is 12 bytes = 24 hex chars\n\t\tif (hex.length !== 24) {\n\t\t\tthrow new InvalidCursorError('Invalid length');\n\t\t}\n\n\t\tconst id = new ObjectId(hex);\n\n\t\treturn { id, direction };\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\tthrow error;\n\t\t}\n\t\tthrow new InvalidCursorError('Invalid cursor payload');\n\t}\n}\n","import type { ChangeStream, ChangeStreamDocument, Document } from 'mongodb';\n\nimport { JobStatus } from '@/jobs';\nimport { toError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for MongoDB Change Stream lifecycle.\n *\n * Provides real-time job notifications when available, with automatic\n * reconnection and graceful fallback to polling-only mode.\n *\n * @internal Not part of public API.\n */\nexport class ChangeStreamHandler {\n\t/** MongoDB Change Stream for real-time job notifications */\n\tprivate changeStream: ChangeStream | null = null;\n\n\t/** Number of consecutive reconnection attempts */\n\tprivate reconnectAttempts = 0;\n\n\t/** Maximum reconnection attempts before falling back to polling-only mode */\n\tprivate readonly maxReconnectAttempts = 3;\n\n\t/** Debounce timer for change stream event processing */\n\tprivate debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n\t/** Timer ID for reconnection with exponential backoff */\n\tprivate reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n\t/** Whether the scheduler is currently using change streams */\n\tprivate usingChangeStreams = false;\n\n\tconstructor(\n\t\tprivate readonly ctx: SchedulerContext,\n\t\tprivate readonly onPoll: () => Promise<void>,\n\t) {}\n\n\t/**\n\t * Set up MongoDB Change Stream for real-time job notifications.\n\t *\n\t * Change streams provide instant notifications when jobs are inserted or when\n\t * job status changes to pending (e.g., after a retry). This eliminates the\n\t * polling delay for reactive job processing.\n\t *\n\t * The change stream watches for:\n\t * - Insert operations (new jobs)\n\t * - Update operations where status field changes\n\t *\n\t * If change streams are unavailable (e.g., standalone MongoDB), the system\n\t * gracefully falls back to polling-only mode.\n\t */\n\tsetup(): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// Create change stream with pipeline to filter relevant events\n\t\t\tconst pipeline = [\n\t\t\t\t{\n\t\t\t\t\t$match: {\n\t\t\t\t\t\t$or: [\n\t\t\t\t\t\t\t{ operationType: 'insert' },\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\toperationType: 'update',\n\t\t\t\t\t\t\t\t'updateDescription.updatedFields.status': { $exists: true },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t];\n\n\t\t\tthis.changeStream = this.ctx.collection.watch(pipeline, {\n\t\t\t\tfullDocument: 'updateLookup',\n\t\t\t});\n\n\t\t\t// Handle change events\n\t\t\tthis.changeStream.on('change', (change) => {\n\t\t\t\tthis.handleEvent(change);\n\t\t\t});\n\n\t\t\t// Handle errors with reconnection\n\t\t\tthis.changeStream.on('error', (error: Error) => {\n\t\t\t\tthis.ctx.emit('changestream:error', { error });\n\t\t\t\tthis.handleError(error);\n\t\t\t});\n\n\t\t\t// Mark as connected\n\t\t\tthis.usingChangeStreams = true;\n\t\t\tthis.reconnectAttempts = 0;\n\t\t\tthis.ctx.emit('changestream:connected', undefined);\n\t\t} catch (error) {\n\t\t\t// Change streams not available (e.g., standalone MongoDB)\n\t\t\tthis.usingChangeStreams = false;\n\t\t\tconst reason = error instanceof Error ? error.message : 'Unknown error';\n\t\t\tthis.ctx.emit('changestream:fallback', { reason });\n\t\t}\n\t}\n\n\t/**\n\t * Handle a change stream event by triggering a debounced poll.\n\t *\n\t * Events are debounced to prevent \"claim storms\" when multiple changes arrive\n\t * in rapid succession (e.g., bulk job inserts). A 100ms debounce window\n\t * collects multiple events and triggers a single poll.\n\t *\n\t * @param change - The change stream event document\n\t */\n\thandleEvent(change: ChangeStreamDocument<Document>): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Trigger poll on insert (new job) or update where status changes\n\t\tconst isInsert = change.operationType === 'insert';\n\t\tconst isUpdate = change.operationType === 'update';\n\n\t\t// Get fullDocument if available (for insert or with updateLookup option)\n\t\tconst fullDocument = 'fullDocument' in change ? change.fullDocument : undefined;\n\t\tconst isPendingStatus = fullDocument?.['status'] === JobStatus.PENDING;\n\n\t\t// For inserts: always trigger since new pending jobs need processing\n\t\t// For updates: trigger if status changed to pending (retry/release scenario)\n\t\tconst shouldTrigger = isInsert || (isUpdate && isPendingStatus);\n\n\t\tif (shouldTrigger) {\n\t\t\t// Debounce poll triggers to avoid claim storms\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer);\n\t\t\t}\n\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.debounceTimer = null;\n\t\t\t\tthis.onPoll().catch((error: unknown) => {\n\t\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t\t});\n\t\t\t}, 100);\n\t\t}\n\t}\n\n\t/**\n\t * Handle change stream errors with exponential backoff reconnection.\n\t *\n\t * Attempts to reconnect up to `maxReconnectAttempts` times with\n\t * exponential backoff (base 1000ms). After exhausting retries, falls back to\n\t * polling-only mode.\n\t *\n\t * @param error - The error that caused the change stream failure\n\t */\n\thandleError(error: Error): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reconnectAttempts++;\n\n\t\tif (this.reconnectAttempts > this.maxReconnectAttempts) {\n\t\t\t// Fall back to polling-only mode\n\t\t\tthis.usingChangeStreams = false;\n\n\t\t\tif (this.reconnectTimer) {\n\t\t\t\tclearTimeout(this.reconnectTimer);\n\t\t\t\tthis.reconnectTimer = null;\n\t\t\t}\n\n\t\t\tif (this.changeStream) {\n\t\t\t\tthis.changeStream.close().catch(() => {});\n\t\t\t\tthis.changeStream = null;\n\t\t\t}\n\n\t\t\tthis.ctx.emit('changestream:fallback', {\n\t\t\t\treason: `Exhausted ${this.maxReconnectAttempts} reconnection attempts: ${error.message}`,\n\t\t\t});\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Exponential backoff: 1s, 2s, 4s\n\t\tconst delay = 2 ** (this.reconnectAttempts - 1) * 1000;\n\n\t\t// Clear any existing reconnect timer before scheduling a new one\n\t\tif (this.reconnectTimer) {\n\t\t\tclearTimeout(this.reconnectTimer);\n\t\t}\n\n\t\tthis.reconnectTimer = setTimeout(() => {\n\t\t\tthis.reconnectTimer = null;\n\t\t\tif (this.ctx.isRunning()) {\n\t\t\t\t// Close existing change stream before reconnecting\n\t\t\t\tif (this.changeStream) {\n\t\t\t\t\tthis.changeStream.close().catch(() => {});\n\t\t\t\t\tthis.changeStream = null;\n\t\t\t\t}\n\t\t\t\tthis.setup();\n\t\t\t}\n\t\t}, delay);\n\t}\n\n\t/**\n\t * Close the change stream cursor and emit closed event.\n\t */\n\tasync close(): Promise<void> {\n\t\t// Clear debounce timer\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer);\n\t\t\tthis.debounceTimer = null;\n\t\t}\n\n\t\t// Clear reconnection timer\n\t\tif (this.reconnectTimer) {\n\t\t\tclearTimeout(this.reconnectTimer);\n\t\t\tthis.reconnectTimer = null;\n\t\t}\n\n\t\tif (this.changeStream) {\n\t\t\ttry {\n\t\t\t\tawait this.changeStream.close();\n\t\t\t} catch {\n\t\t\t\t// Ignore close errors during shutdown\n\t\t\t}\n\t\t\tthis.changeStream = null;\n\n\t\t\tif (this.usingChangeStreams) {\n\t\t\t\tthis.ctx.emit('changestream:closed', undefined);\n\t\t\t}\n\t\t}\n\n\t\tthis.usingChangeStreams = false;\n\t\tthis.reconnectAttempts = 0;\n\t}\n\n\t/**\n\t * Check if change streams are currently active.\n\t */\n\tisActive(): boolean {\n\t\treturn this.usingChangeStreams;\n\t}\n}\n","import { ObjectId } from 'mongodb';\n\nimport { type BulkOperationResult, type JobSelector, JobStatus, type PersistedJob } from '@/jobs';\nimport { buildSelectorQuery } from '@/scheduler';\nimport { ConnectionError, JobStateError, MonqueError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for job lifecycle management operations.\n *\n * Provides atomic state transitions (cancel, retry, reschedule) and deletion.\n * Emits appropriate events on each operation.\n *\n * @internal Not part of public API - use Monque class methods instead.\n */\nexport class JobManager {\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Cancel a pending or scheduled job.\n\t *\n\t * Sets the job status to 'cancelled' and emits a 'job:cancelled' event.\n\t * If the job is already cancelled, this is a no-op and returns the job.\n\t * Cannot cancel jobs that are currently 'processing', 'completed', or 'failed'.\n\t *\n\t * @param jobId - The ID of the job to cancel\n\t * @returns The cancelled job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for cancellation\n\t *\n\t * @example Cancel a pending job\n\t * ```typescript\n\t * const job = await monque.enqueue('report', { type: 'daily' });\n\t * await monque.cancelJob(job._id.toString());\n\t * ```\n\t */\n\tasync cancelJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tif (!ObjectId.isValid(jobId)) return null;\n\n\t\tconst _id = new ObjectId(jobId);\n\n\t\t// Fetch job first to allow emitting the full job object in the event\n\t\tconst jobDoc = await this.ctx.collection.findOne({ _id });\n\t\tif (!jobDoc) return null;\n\n\t\tif (jobDoc['status'] === JobStatus.CANCELLED) {\n\t\t\treturn this.ctx.documentToPersistedJob(jobDoc);\n\t\t}\n\n\t\tif (jobDoc['status'] !== JobStatus.PENDING) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t`Cannot cancel job in status '${jobDoc['status']}'`,\n\t\t\t\tjobId,\n\t\t\t\tjobDoc['status'],\n\t\t\t\t'cancel',\n\t\t\t);\n\t\t}\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id, status: JobStatus.PENDING },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.CANCELLED,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\t// Race condition: job changed state between check and update\n\t\t\tthrow new JobStateError(\n\t\t\t\t'Job status changed during cancellation attempt',\n\t\t\t\tjobId,\n\t\t\t\t'unknown',\n\t\t\t\t'cancel',\n\t\t\t);\n\t\t}\n\n\t\tconst job = this.ctx.documentToPersistedJob(result);\n\t\tthis.ctx.emit('job:cancelled', { job });\n\t\treturn job;\n\t}\n\n\t/**\n\t * Retry a failed or cancelled job.\n\t *\n\t * Resets the job to 'pending' status, clears failure count/reason, and sets\n\t * nextRunAt to now (immediate retry). Emits a 'job:retried' event.\n\t *\n\t * @param jobId - The ID of the job to retry\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for retry (must be failed or cancelled)\n\t *\n\t * @example Retry a failed job\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * console.log(`Job ${job._id} failed, retrying manually...`);\n\t * await monque.retryJob(job._id.toString());\n\t * });\n\t * ```\n\t */\n\tasync retryJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tif (!ObjectId.isValid(jobId)) return null;\n\n\t\tconst _id = new ObjectId(jobId);\n\t\tconst currentJob = await this.ctx.collection.findOne({ _id });\n\n\t\tif (!currentJob) return null;\n\n\t\tif (currentJob['status'] !== JobStatus.FAILED && currentJob['status'] !== JobStatus.CANCELLED) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t`Cannot retry job in status '${currentJob['status']}'`,\n\t\t\t\tjobId,\n\t\t\t\tcurrentJob['status'],\n\t\t\t\t'retry',\n\t\t\t);\n\t\t}\n\n\t\tconst previousStatus = currentJob['status'] as 'failed' | 'cancelled';\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{\n\t\t\t\t_id,\n\t\t\t\tstatus: { $in: [JobStatus.FAILED, JobStatus.CANCELLED] },\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\tfailCount: 0,\n\t\t\t\t\tnextRunAt: new Date(),\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tfailReason: '',\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\tthrow new JobStateError('Job status changed during retry attempt', jobId, 'unknown', 'retry');\n\t\t}\n\n\t\tconst job = this.ctx.documentToPersistedJob(result);\n\t\tthis.ctx.emit('job:retried', { job, previousStatus });\n\t\treturn job;\n\t}\n\n\t/**\n\t * Reschedule a pending job to run at a different time.\n\t *\n\t * Only works for jobs in 'pending' status.\n\t *\n\t * @param jobId - The ID of the job to reschedule\n\t * @param runAt - The new Date when the job should run\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is not in pending state\n\t *\n\t * @example Delay a job by 1 hour\n\t * ```typescript\n\t * const nextHour = new Date(Date.now() + 60 * 60 * 1000);\n\t * await monque.rescheduleJob(jobId, nextHour);\n\t * ```\n\t */\n\tasync rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null> {\n\t\tif (!ObjectId.isValid(jobId)) return null;\n\n\t\tconst _id = new ObjectId(jobId);\n\t\tconst currentJobDoc = await this.ctx.collection.findOne({ _id });\n\n\t\tif (!currentJobDoc) return null;\n\n\t\tif (currentJobDoc['status'] !== JobStatus.PENDING) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t`Cannot reschedule job in status '${currentJobDoc['status']}'`,\n\t\t\t\tjobId,\n\t\t\t\tcurrentJobDoc['status'],\n\t\t\t\t'reschedule',\n\t\t\t);\n\t\t}\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id, status: JobStatus.PENDING },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tnextRunAt: runAt,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t'Job status changed during reschedule attempt',\n\t\t\t\tjobId,\n\t\t\t\t'unknown',\n\t\t\t\t'reschedule',\n\t\t\t);\n\t\t}\n\n\t\treturn this.ctx.documentToPersistedJob(result);\n\t}\n\n\t/**\n\t * Permanently delete a job.\n\t *\n\t * This action is irreversible. Emits a 'job:deleted' event upon success.\n\t * Can delete a job in any state.\n\t *\n\t * @param jobId - The ID of the job to delete\n\t * @returns true if deleted, false if job not found\n\t *\n\t * @example Delete a cleanup job\n\t * ```typescript\n\t * const deleted = await monque.deleteJob(jobId);\n\t * if (deleted) {\n\t * console.log('Job permanently removed');\n\t * }\n\t * ```\n\t */\n\tasync deleteJob(jobId: string): Promise<boolean> {\n\t\tif (!ObjectId.isValid(jobId)) return false;\n\n\t\tconst _id = new ObjectId(jobId);\n\n\t\tconst result = await this.ctx.collection.deleteOne({ _id });\n\n\t\tif (result.deletedCount > 0) {\n\t\t\tthis.ctx.emit('job:deleted', { jobId });\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Bulk Operations\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Cancel multiple jobs matching the given filter via a single updateMany call.\n\t *\n\t * Only cancels jobs in 'pending' status — the status guard is applied regardless\n\t * of what the filter specifies. Jobs in other states are silently skipped (not\n\t * matched by the query). Emits a 'jobs:cancelled' event with the count of\n\t * successfully cancelled jobs.\n\t *\n\t * @param filter - Selector for which jobs to cancel (name, status, date range)\n\t * @returns Result with count of cancelled jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Cancel all pending jobs for a queue\n\t * ```typescript\n\t * const result = await monque.cancelJobs({\n\t * name: 'email-queue',\n\t * status: 'pending'\n\t * });\n\t * console.log(`Cancelled ${result.count} jobs`);\n\t * ```\n\t */\n\tasync cancelJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tconst query = buildSelectorQuery(filter);\n\n\t\t// Enforce allowed status, but respect explicit status filters\n\t\tif (filter.status !== undefined) {\n\t\t\tconst requested = Array.isArray(filter.status) ? filter.status : [filter.status];\n\t\t\tif (!requested.includes(JobStatus.PENDING)) {\n\t\t\t\treturn { count: 0, errors: [] };\n\t\t\t}\n\t\t}\n\t\tquery['status'] = JobStatus.PENDING;\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.collection.updateMany(query, {\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.CANCELLED,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst count = result.modifiedCount;\n\n\t\t\tif (count > 0) {\n\t\t\t\tthis.ctx.emit('jobs:cancelled', { count });\n\t\t\t}\n\n\t\t\treturn { count, errors: [] };\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during cancelJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to cancel jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.\n\t *\n\t * Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied\n\t * regardless of what the filter specifies. Jobs in other states are silently skipped.\n\t * Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.\n\t * Emits a 'jobs:retried' event with the count of successfully retried jobs.\n\t *\n\t * @param filter - Selector for which jobs to retry (name, status, date range)\n\t * @returns Result with count of retried jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Retry all failed jobs\n\t * ```typescript\n\t * const result = await monque.retryJobs({\n\t * status: 'failed'\n\t * });\n\t * console.log(`Retried ${result.count} jobs`);\n\t * ```\n\t */\n\tasync retryJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tconst query = buildSelectorQuery(filter);\n\n\t\t// Enforce allowed statuses, but respect explicit status filters\n\t\tconst retryable = [JobStatus.FAILED, JobStatus.CANCELLED] as const;\n\t\tif (filter.status !== undefined) {\n\t\t\tconst requested = Array.isArray(filter.status) ? filter.status : [filter.status];\n\t\t\tconst allowed = requested.filter(\n\t\t\t\t(status): status is (typeof retryable)[number] =>\n\t\t\t\t\tstatus === JobStatus.FAILED || status === JobStatus.CANCELLED,\n\t\t\t);\n\t\t\tif (allowed.length === 0) {\n\t\t\t\treturn { count: 0, errors: [] };\n\t\t\t}\n\t\t\tquery['status'] = allowed.length === 1 ? allowed[0] : { $in: allowed };\n\t\t} else {\n\t\t\tquery['status'] = { $in: retryable };\n\t\t}\n\n\t\tconst spreadWindowMs = 30_000; // 30s max spread for staggered retry\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.collection.updateMany(query, [\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\t\tfailCount: 0,\n\t\t\t\t\t\tnextRunAt: {\n\t\t\t\t\t\t\t$add: [new Date(), { $multiply: [{ $rand: {} }, spreadWindowMs] }],\n\t\t\t\t\t\t},\n\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t$unset: ['failReason', 'lockedAt', 'claimedBy', 'lastHeartbeat', 'heartbeatInterval'],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tconst count = result.modifiedCount;\n\n\t\t\tif (count > 0) {\n\t\t\t\tthis.ctx.emit('jobs:retried', { count });\n\t\t\t}\n\n\t\t\treturn { count, errors: [] };\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during retryJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to retry jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Delete multiple jobs matching the given filter.\n\t *\n\t * Deletes jobs in any status. Uses a batch delete for efficiency.\n\t * Emits a 'jobs:deleted' event with the count of deleted jobs.\n\t * Does not emit individual 'job:deleted' events to avoid noise.\n\t *\n\t * @param filter - Selector for which jobs to delete (name, status, date range)\n\t * @returns Result with count of deleted jobs (errors array always empty for delete)\n\t *\n\t * @example Delete old completed jobs\n\t * ```typescript\n\t * const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\t * const result = await monque.deleteJobs({\n\t * status: 'completed',\n\t * olderThan: weekAgo\n\t * });\n\t * console.log(`Deleted ${result.count} jobs`);\n\t * ```\n\t */\n\tasync deleteJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tconst query = buildSelectorQuery(filter);\n\n\t\ttry {\n\t\t\t// Use deleteMany for efficiency\n\t\t\tconst result = await this.ctx.collection.deleteMany(query);\n\n\t\t\tif (result.deletedCount > 0) {\n\t\t\t\tthis.ctx.emit('jobs:deleted', { count: result.deletedCount });\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcount: result.deletedCount,\n\t\t\t\terrors: [],\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during deleteJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to delete jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n","import { isPersistedJob, type Job, JobStatus, type PersistedJob } from '@/jobs';\nimport { calculateBackoff, getNextCronDate, toError } from '@/shared';\nimport type { WorkerRegistration } from '@/workers';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for job processing and execution.\n *\n * Manages the poll loop, atomic job acquisition, handler execution,\n * and job completion/failure with exponential backoff retry logic.\n *\n * @internal Not part of public API.\n */\nexport class JobProcessor {\n\t/** Guard flag to prevent concurrent poll() execution */\n\tprivate _isPolling = false;\n\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Get the total number of active jobs across all workers.\n\t *\n\t * Used for instance-level throttling when `instanceConcurrency` is configured.\n\t */\n\tprivate getTotalActiveJobs(): number {\n\t\tlet total = 0;\n\t\tfor (const worker of this.ctx.workers.values()) {\n\t\t\ttotal += worker.activeJobs.size;\n\t\t}\n\t\treturn total;\n\t}\n\n\t/**\n\t * Get the number of available slots considering the global instanceConcurrency limit.\n\t *\n\t * @param workerAvailableSlots - Available slots for the specific worker\n\t * @returns Number of slots available after applying global limit\n\t */\n\tprivate getGloballyAvailableSlots(workerAvailableSlots: number): number {\n\t\tconst { instanceConcurrency } = this.ctx.options;\n\n\t\tif (instanceConcurrency === undefined) {\n\t\t\treturn workerAvailableSlots;\n\t\t}\n\n\t\tconst totalActive = this.getTotalActiveJobs();\n\t\tconst globalAvailable = instanceConcurrency - totalActive;\n\n\t\treturn Math.min(workerAvailableSlots, globalAvailable);\n\t}\n\n\t/**\n\t * Poll for available jobs and process them.\n\t *\n\t * Called at regular intervals (configured by `pollInterval`). For each registered worker,\n\t * attempts to acquire jobs up to the worker's available concurrency slots.\n\t * Aborts early if the scheduler is stopping (`isRunning` is false) or if\n\t * the instance-level `instanceConcurrency` limit is reached.\n\t */\n\tasync poll(): Promise<void> {\n\t\tif (!this.ctx.isRunning() || this._isPolling) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._isPolling = true;\n\n\t\ttry {\n\t\t\tawait this._doPoll();\n\t\t} finally {\n\t\t\tthis._isPolling = false;\n\t\t}\n\t}\n\n\t/**\n\t * Internal poll implementation.\n\t */\n\tprivate async _doPoll(): Promise<void> {\n\t\t// Early exit if global instanceConcurrency is reached\n\t\tconst { instanceConcurrency } = this.ctx.options;\n\n\t\tif (instanceConcurrency !== undefined && this.getTotalActiveJobs() >= instanceConcurrency) {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const [name, worker] of this.ctx.workers) {\n\t\t\t// Check if worker has capacity\n\t\t\tconst workerAvailableSlots = worker.concurrency - worker.activeJobs.size;\n\n\t\t\tif (workerAvailableSlots <= 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Apply global concurrency limit\n\t\t\tconst availableSlots = this.getGloballyAvailableSlots(workerAvailableSlots);\n\n\t\t\tif (availableSlots <= 0) {\n\t\t\t\t// Global limit reached, stop processing all workers\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Try to acquire jobs up to available slots\n\t\t\tfor (let i = 0; i < availableSlots; i++) {\n\t\t\t\tif (!this.ctx.isRunning()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Re-check global limit before each acquisition\n\t\t\t\tif (instanceConcurrency !== undefined && this.getTotalActiveJobs() >= instanceConcurrency) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst job = await this.acquireJob(name);\n\n\t\t\t\tif (job) {\n\t\t\t\t\t// Add to activeJobs immediately to correctly track concurrency\n\t\t\t\t\tworker.activeJobs.set(job._id.toString(), job);\n\n\t\t\t\t\tthis.processJob(job, worker).catch((error: unknown) => {\n\t\t\t\t\t\tthis.ctx.emit('job:error', { error: toError(error), job });\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t// No more jobs available for this worker\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Atomically acquire a pending job for processing using the claimedBy pattern.\n\t *\n\t * Uses MongoDB's `findOneAndUpdate` with atomic operations to ensure only one scheduler\n\t * instance can claim a job. The query ensures the job is:\n\t * - In pending status\n\t * - Has nextRunAt <= now\n\t * - Is not claimed by another instance (claimedBy is null/undefined)\n\t *\n\t * Returns `null` immediately if scheduler is stopping (`isRunning` is false).\n\t *\n\t * @param name - The job type to acquire\n\t * @returns The acquired job with updated status, claimedBy, and heartbeat info, or `null` if no jobs available\n\t */\n\tasync acquireJob(name: string): Promise<PersistedJob | null> {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst now = new Date();\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\tnextRunAt: { $lte: now },\n\t\t\t\t$or: [{ claimedBy: null }, { claimedBy: { $exists: false } }],\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\t\t\tclaimedBy: this.ctx.instanceId,\n\t\t\t\t\tlockedAt: now,\n\t\t\t\t\tlastHeartbeat: now,\n\t\t\t\t\theartbeatInterval: this.ctx.options.heartbeatInterval,\n\t\t\t\t\tupdatedAt: now,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tsort: { nextRunAt: 1 },\n\t\t\t\treturnDocument: 'after',\n\t\t\t},\n\t\t);\n\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (!result) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.ctx.documentToPersistedJob(result);\n\t}\n\n\t/**\n\t * Execute a job using its registered worker handler.\n\t *\n\t * Tracks the job as active during processing, emits lifecycle events, and handles\n\t * both success and failure cases. On success, calls `completeJob()`. On failure,\n\t * calls `failJob()` which implements exponential backoff retry logic.\n\t *\n\t * Events are only emitted when the underlying atomic status transition succeeds,\n\t * ensuring event consumers receive reliable, consistent data backed by the actual\n\t * database state.\n\t *\n\t * @param job - The job to process\n\t * @param worker - The worker registration containing the handler and active job tracking\n\t */\n\tasync processJob(job: PersistedJob, worker: WorkerRegistration): Promise<void> {\n\t\tconst jobId = job._id.toString();\n\t\tconst startTime = Date.now();\n\t\tthis.ctx.emit('job:start', job);\n\n\t\ttry {\n\t\t\tawait worker.handler(job);\n\n\t\t\t// Job completed successfully\n\t\t\tconst duration = Date.now() - startTime;\n\t\t\tconst updatedJob = await this.completeJob(job);\n\n\t\t\tif (updatedJob) {\n\t\t\t\tthis.ctx.emit('job:complete', { job: updatedJob, duration });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// Job failed\n\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\tconst updatedJob = await this.failJob(job, err);\n\n\t\t\tif (updatedJob) {\n\t\t\t\tconst willRetry = updatedJob.status === JobStatus.PENDING;\n\t\t\t\tthis.ctx.emit('job:fail', { job: updatedJob, error: err, willRetry });\n\t\t\t}\n\t\t} finally {\n\t\t\tworker.activeJobs.delete(jobId);\n\t\t}\n\t}\n\n\t/**\n\t * Mark a job as completed successfully using an atomic status transition.\n\t *\n\t * Uses `findOneAndUpdate` with `status: processing` and `claimedBy: instanceId`\n\t * preconditions to ensure the transition only occurs if the job is still owned by this\n\t * scheduler instance. Returns `null` if the job was concurrently modified (e.g., reclaimed\n\t * by another instance after stale recovery).\n\t *\n\t * For recurring jobs (with `repeatInterval`), schedules the next run based on the cron\n\t * expression and resets `failCount` to 0. For one-time jobs, sets status to `completed`.\n\t * Clears `lockedAt` and `failReason` fields in both cases.\n\t *\n\t * @param job - The job that completed successfully\n\t * @returns The updated job document, or `null` if the transition could not be applied\n\t */\n\tasync completeJob(job: Job): Promise<PersistedJob | null> {\n\t\tif (!isPersistedJob(job)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (job.repeatInterval) {\n\t\t\t// Recurring job - schedule next run\n\t\t\tconst nextRunAt = getNextCronDate(job.repeatInterval);\n\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\t\tnextRunAt,\n\t\t\t\t\t\tfailCount: 0,\n\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t},\n\t\t\t\t\t$unset: {\n\t\t\t\t\t\tlockedAt: '',\n\t\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t\t\tfailReason: '',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ returnDocument: 'after' },\n\t\t\t);\n\n\t\t\treturn result ? this.ctx.documentToPersistedJob(result) : null;\n\t\t}\n\n\t\t// One-time job - mark as completed\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.COMPLETED,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t\tfailReason: '',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\treturn result ? this.ctx.documentToPersistedJob(result) : null;\n\t}\n\n\t/**\n\t * Handle job failure with exponential backoff retry logic using an atomic status transition.\n\t *\n\t * Uses `findOneAndUpdate` with `status: processing` and `claimedBy: instanceId`\n\t * preconditions to ensure the transition only occurs if the job is still owned by this\n\t * scheduler instance. Returns `null` if the job was concurrently modified (e.g., reclaimed\n\t * by another instance after stale recovery).\n\t *\n\t * Increments `failCount` and calculates next retry time using exponential backoff:\n\t * `nextRunAt = 2^failCount * baseRetryInterval` (capped by optional `maxBackoffDelay`).\n\t *\n\t * If `failCount >= maxRetries`, marks job as permanently `failed`. Otherwise, resets\n\t * to `pending` status for retry. Stores error message in `failReason` field.\n\t *\n\t * @param job - The job that failed\n\t * @param error - The error that caused the failure\n\t * @returns The updated job document, or `null` if the transition could not be applied\n\t */\n\tasync failJob(job: Job, error: Error): Promise<PersistedJob | null> {\n\t\tif (!isPersistedJob(job)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst newFailCount = job.failCount + 1;\n\n\t\tif (newFailCount >= this.ctx.options.maxRetries) {\n\t\t\t// Permanent failure\n\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tstatus: JobStatus.FAILED,\n\t\t\t\t\t\tfailCount: newFailCount,\n\t\t\t\t\t\tfailReason: error.message,\n\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t},\n\t\t\t\t\t$unset: {\n\t\t\t\t\t\tlockedAt: '',\n\t\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ returnDocument: 'after' },\n\t\t\t);\n\n\t\t\treturn result ? this.ctx.documentToPersistedJob(result) : null;\n\t\t}\n\n\t\t// Schedule retry with exponential backoff\n\t\tconst nextRunAt = calculateBackoff(\n\t\t\tnewFailCount,\n\t\t\tthis.ctx.options.baseRetryInterval,\n\t\t\tthis.ctx.options.maxBackoffDelay,\n\t\t);\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\tfailCount: newFailCount,\n\t\t\t\t\tfailReason: error.message,\n\t\t\t\t\tnextRunAt,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\treturn result ? this.ctx.documentToPersistedJob(result) : null;\n\t}\n\n\t/**\n\t * Update heartbeats for all jobs claimed by this scheduler instance.\n\t *\n\t * This method runs periodically while the scheduler is running to indicate\n\t * that jobs are still being actively processed.\n\t *\n\t * `lastHeartbeat` is primarily an observability signal (monitoring/debugging).\n\t * Stale recovery is based on `lockedAt` + `lockTimeout`.\n\t */\n\tasync updateHeartbeats(): Promise<void> {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = new Date();\n\n\t\tawait this.ctx.collection.updateMany(\n\t\t\t{\n\t\t\t\tclaimedBy: this.ctx.instanceId,\n\t\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tlastHeartbeat: now,\n\t\t\t\t\tupdatedAt: now,\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n}\n","import type { Document, Filter, ObjectId, WithId } from 'mongodb';\n\nimport {\n\tCursorDirection,\n\ttype CursorDirectionType,\n\ttype CursorOptions,\n\ttype CursorPage,\n\ttype GetJobsFilter,\n\ttype JobSelector,\n\tJobStatus,\n\ttype PersistedJob,\n\ttype QueueStats,\n} from '@/jobs';\nimport { AggregationTimeoutError, ConnectionError } from '@/shared';\n\nimport { buildSelectorQuery, decodeCursor, encodeCursor } from '../helpers.js';\nimport type { SchedulerContext } from './types.js';\n\ninterface StatsCacheEntry {\n\tdata: QueueStats;\n\texpiresAt: number;\n}\n\n/**\n * Internal service for job query operations.\n *\n * Provides read-only access to jobs with filtering and cursor-based pagination.\n * All queries use efficient index-backed access patterns.\n *\n * @internal Not part of public API - use Monque class methods instead.\n */\nexport class JobQueryService {\n\tprivate readonly statsCache = new Map<string, StatsCacheEntry>();\n\tprivate static readonly MAX_CACHE_SIZE = 100;\n\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Get a single job by its MongoDB ObjectId.\n\t *\n\t * Useful for retrieving job details when you have a job ID from events,\n\t * logs, or stored references.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param id - The job's ObjectId\n\t * @returns Promise resolving to the job if found, null otherwise\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Look up job from event\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * // Later, retrieve the job to check its status\n\t * const currentJob = await monque.getJob(job._id);\n\t * console.log(`Job status: ${currentJob?.status}`);\n\t * });\n\t * ```\n\t *\n\t * @example Admin endpoint\n\t * ```typescript\n\t * app.get('/jobs/:id', async (req, res) => {\n\t * const job = await monque.getJob(new ObjectId(req.params.id));\n\t * if (!job) {\n\t * return res.status(404).json({ error: 'Job not found' });\n\t * }\n\t * res.json(job);\n\t * });\n\t * ```\n\t */\n\tasync getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null> {\n\t\ttry {\n\t\t\tconst doc = await this.ctx.collection.findOne({ _id: id });\n\t\t\tif (!doc) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn this.ctx.documentToPersistedJob<T>(doc as WithId<Document>);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during getJob';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to get job: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Query jobs from the queue with optional filters.\n\t *\n\t * Provides read-only access to job data for monitoring, debugging, and\n\t * administrative purposes. Results are ordered by `nextRunAt` ascending.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param filter - Optional filter criteria\n\t * @returns Promise resolving to array of matching jobs\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Get all pending jobs\n\t * ```typescript\n\t * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });\n\t * console.log(`${pendingJobs.length} jobs waiting`);\n\t * ```\n\t *\n\t * @example Get failed email jobs\n\t * ```typescript\n\t * const failedEmails = await monque.getJobs({\n\t * name: 'send-email',\n\t * status: JobStatus.FAILED,\n\t * });\n\t * for (const job of failedEmails) {\n\t * console.error(`Job ${job._id} failed: ${job.failReason}`);\n\t * }\n\t * ```\n\t *\n\t * @example Paginated job listing\n\t * ```typescript\n\t * const page1 = await monque.getJobs({ limit: 50, skip: 0 });\n\t * const page2 = await monque.getJobs({ limit: 50, skip: 50 });\n\t * ```\n\t *\n\t * @example Use with type guards from @monque/core\n\t * ```typescript\n\t * import { isPendingJob, isRecurringJob } from '@monque/core';\n\t *\n\t * const jobs = await monque.getJobs();\n\t * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));\n\t * ```\n\t */\n\tasync getJobs<T = unknown>(filter: GetJobsFilter = {}): Promise<PersistedJob<T>[]> {\n\t\tconst query: Document = {};\n\n\t\tif (filter.name !== undefined) {\n\t\t\tquery['name'] = filter.name;\n\t\t}\n\n\t\tif (filter.status !== undefined) {\n\t\t\tif (Array.isArray(filter.status)) {\n\t\t\t\tquery['status'] = { $in: filter.status };\n\t\t\t} else {\n\t\t\t\tquery['status'] = filter.status;\n\t\t\t}\n\t\t}\n\n\t\tconst limit = filter.limit ?? 100;\n\t\tconst skip = filter.skip ?? 0;\n\n\t\ttry {\n\t\t\tconst cursor = this.ctx.collection.find(query).sort({ nextRunAt: 1 }).skip(skip).limit(limit);\n\n\t\t\tconst docs = await cursor.toArray();\n\t\t\treturn docs.map((doc) => this.ctx.documentToPersistedJob<T>(doc));\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during getJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to query jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a paginated list of jobs using opaque cursors.\n\t *\n\t * Provides stable pagination for large job lists. Supports forward and backward\n\t * navigation, filtering, and efficient database access via index-based cursor queries.\n\t *\n\t * @template T - The job data payload type\n\t * @param options - Pagination options (cursor, limit, direction, filter)\n\t * @returns Page of jobs with next/prev cursors\n\t * @throws {InvalidCursorError} If the provided cursor is malformed\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example List pending jobs\n\t * ```typescript\n\t * const page = await monque.getJobsWithCursor({\n\t * limit: 20,\n\t * filter: { status: 'pending' }\n\t * });\n\t * const jobs = page.jobs;\n\t *\n\t * // Get next page\n\t * if (page.hasNextPage) {\n\t * const page2 = await monque.getJobsWithCursor({\n\t * cursor: page.cursor,\n\t * limit: 20\n\t * });\n\t * }\n\t * ```\n\t */\n\tasync getJobsWithCursor<T = unknown>(options: CursorOptions = {}): Promise<CursorPage<T>> {\n\t\tconst limit = options.limit ?? 50;\n\t\t// Default to forward if not specified.\n\t\tconst direction: CursorDirectionType = options.direction ?? CursorDirection.FORWARD;\n\t\tlet anchorId: ObjectId | null = null;\n\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tanchorId = decoded.id;\n\t\t}\n\n\t\t// Build base query from filters\n\t\tconst query: Filter<Document> = options.filter ? buildSelectorQuery(options.filter) : {};\n\n\t\t// Add cursor condition to query\n\t\tconst sortDir = direction === CursorDirection.FORWARD ? 1 : -1;\n\n\t\tif (anchorId) {\n\t\t\tif (direction === CursorDirection.FORWARD) {\n\t\t\t\tquery._id = { ...query._id, $gt: anchorId };\n\t\t\t} else {\n\t\t\t\tquery._id = { ...query._id, $lt: anchorId };\n\t\t\t}\n\t\t}\n\n\t\t// Fetch limit + 1 to detect hasNext/hasPrev\n\t\tconst fetchLimit = limit + 1;\n\n\t\t// Sort: Always deterministic.\n\t\tlet docs: WithId<Document>[];\n\t\ttry {\n\t\t\tdocs = await this.ctx.collection\n\t\t\t\t.find(query)\n\t\t\t\t.sort({ _id: sortDir })\n\t\t\t\t.limit(fetchLimit)\n\t\t\t\t.toArray();\n\t\t} catch (error) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : 'Unknown error during getJobsWithCursor';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to query jobs with cursor: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\n\t\tlet hasMore = false;\n\t\tif (docs.length > limit) {\n\t\t\thasMore = true;\n\t\t\tdocs.pop(); // Remove the extra item\n\t\t}\n\n\t\tif (direction === CursorDirection.BACKWARD) {\n\t\t\tdocs.reverse();\n\t\t}\n\n\t\tconst jobs = docs.map((doc) => this.ctx.documentToPersistedJob<T>(doc as WithId<Document>));\n\n\t\tlet nextCursor: string | null = null;\n\n\t\tif (jobs.length > 0) {\n\t\t\tconst lastJob = jobs[jobs.length - 1];\n\t\t\t// Check for existence to satisfy strict null checks/noUncheckedIndexedAccess\n\t\t\tif (lastJob) {\n\t\t\t\tnextCursor = encodeCursor(lastJob._id, direction);\n\t\t\t}\n\t\t}\n\n\t\tlet hasNextPage = false;\n\t\tlet hasPreviousPage = false;\n\n\t\t// Determine availability of next/prev pages\n\t\tif (direction === CursorDirection.FORWARD) {\n\t\t\thasNextPage = hasMore;\n\t\t\thasPreviousPage = !!anchorId;\n\t\t} else {\n\t\t\thasNextPage = !!anchorId;\n\t\t\thasPreviousPage = hasMore;\n\t\t}\n\n\t\treturn {\n\t\t\tjobs,\n\t\t\tcursor: nextCursor,\n\t\t\thasNextPage,\n\t\t\thasPreviousPage,\n\t\t};\n\t}\n\n\t/**\n\t * Clear all cached getQueueStats() results.\n\t * Called on scheduler stop() for clean state on restart.\n\t * @internal\n\t */\n\tclearStatsCache(): void {\n\t\tthis.statsCache.clear();\n\t}\n\n\t/**\n\t * Get aggregate statistics for the job queue.\n\t *\n\t * Uses MongoDB aggregation pipeline for efficient server-side calculation.\n\t * Returns counts per status and optional average processing duration for completed jobs.\n\t *\n\t * Results are cached per unique filter with a configurable TTL (default 5s).\n\t * Set `statsCacheTtlMs: 0` to disable caching.\n\t *\n\t * @param filter - Optional filter to scope statistics by job name\n\t * @returns Promise resolving to queue statistics\n\t * @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout\n\t * @throws {ConnectionError} If database operation fails\n\t *\n\t * @example Get overall queue statistics\n\t * ```typescript\n\t * const stats = await monque.getQueueStats();\n\t * console.log(`Pending: ${stats.pending}, Failed: ${stats.failed}`);\n\t * ```\n\t *\n\t * @example Get statistics for a specific job type\n\t * ```typescript\n\t * const emailStats = await monque.getQueueStats({ name: 'send-email' });\n\t * console.log(`${emailStats.total} email jobs in queue`);\n\t * ```\n\t */\n\tasync getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats> {\n\t\tconst ttl = this.ctx.options.statsCacheTtlMs;\n\t\tconst cacheKey = filter?.name ?? '';\n\n\t\tif (ttl > 0) {\n\t\t\tconst cached = this.statsCache.get(cacheKey);\n\t\t\tif (cached && cached.expiresAt > Date.now()) {\n\t\t\t\treturn { ...cached.data };\n\t\t\t}\n\t\t}\n\n\t\tconst matchStage: Document = {};\n\n\t\tif (filter?.name) {\n\t\t\tmatchStage['name'] = filter.name;\n\t\t}\n\n\t\tconst pipeline: Document[] = [\n\t\t\t// Optional match stage for filtering by name\n\t\t\t...(Object.keys(matchStage).length > 0 ? [{ $match: matchStage }] : []),\n\t\t\t// Facet to calculate counts and avg processing duration in parallel\n\t\t\t{\n\t\t\t\t$facet: {\n\t\t\t\t\t// Count by status\n\t\t\t\t\tstatusCounts: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$group: {\n\t\t\t\t\t\t\t\t_id: '$status',\n\t\t\t\t\t\t\t\tcount: { $sum: 1 },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\t// Calculate average job lifetime for completed jobs.\n\t\t\t\t\t// Uses createdAt → updatedAt (total lifetime = queue wait + processing)\n\t\t\t\t\t// since completeJob() unsets lockedAt, making pure processing time unavailable.\n\t\t\t\t\tavgDuration: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$match: {\n\t\t\t\t\t\t\t\tstatus: JobStatus.COMPLETED,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$group: {\n\t\t\t\t\t\t\t\t_id: null,\n\t\t\t\t\t\t\t\tavgMs: {\n\t\t\t\t\t\t\t\t\t$avg: {\n\t\t\t\t\t\t\t\t\t\t$subtract: ['$updatedAt', '$createdAt'],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\t// Total count\n\t\t\t\t\ttotal: [{ $count: 'count' }],\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\n\t\ttry {\n\t\t\tconst results = await this.ctx.collection.aggregate(pipeline, { maxTimeMS: 30000 }).toArray();\n\n\t\t\tconst result = results[0];\n\n\t\t\t// Initialize with zeros\n\t\t\tconst stats: QueueStats = {\n\t\t\t\tpending: 0,\n\t\t\t\tprocessing: 0,\n\t\t\t\tcompleted: 0,\n\t\t\t\tfailed: 0,\n\t\t\t\tcancelled: 0,\n\t\t\t\ttotal: 0,\n\t\t\t};\n\n\t\t\tif (result) {\n\t\t\t\t// Map status counts to stats\n\t\t\t\tconst statusCounts = result['statusCounts'] as Array<{ _id: string; count: number }>;\n\t\t\t\tfor (const entry of statusCounts) {\n\t\t\t\t\tconst status = entry._id;\n\t\t\t\t\tconst count = entry.count;\n\n\t\t\t\t\tswitch (status) {\n\t\t\t\t\t\tcase JobStatus.PENDING:\n\t\t\t\t\t\t\tstats.pending = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.PROCESSING:\n\t\t\t\t\t\t\tstats.processing = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.COMPLETED:\n\t\t\t\t\t\t\tstats.completed = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.FAILED:\n\t\t\t\t\t\t\tstats.failed = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.CANCELLED:\n\t\t\t\t\t\t\tstats.cancelled = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Extract total\n\t\t\t\tconst totalResult = result['total'] as Array<{ count: number }>;\n\t\t\t\tif (totalResult.length > 0 && totalResult[0]) {\n\t\t\t\t\tstats.total = totalResult[0].count;\n\t\t\t\t}\n\n\t\t\t\t// Extract average processing duration\n\t\t\t\tconst avgDurationResult = result['avgDuration'] as Array<{ avgMs: number }>;\n\t\t\t\tif (avgDurationResult.length > 0 && avgDurationResult[0]) {\n\t\t\t\t\tconst avgMs = avgDurationResult[0].avgMs;\n\t\t\t\t\tif (typeof avgMs === 'number' && !Number.isNaN(avgMs)) {\n\t\t\t\t\t\tstats.avgProcessingDurationMs = Math.round(avgMs);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Cache the result if TTL is enabled\n\t\t\tif (ttl > 0) {\n\t\t\t\t// Delete existing entry first so re-insertion moves it to end (Map insertion order = LRU)\n\t\t\t\tthis.statsCache.delete(cacheKey);\n\t\t\t\t// LRU eviction: if cache is still full after removing existing key, evict the oldest entry\n\t\t\t\tif (this.statsCache.size >= JobQueryService.MAX_CACHE_SIZE) {\n\t\t\t\t\tconst oldestKey = this.statsCache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tthis.statsCache.delete(oldestKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.statsCache.set(cacheKey, {\n\t\t\t\t\tdata: { ...stats },\n\t\t\t\t\texpiresAt: Date.now() + ttl,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn stats;\n\t\t} catch (error) {\n\t\t\t// Check for timeout error\n\t\t\tif (error instanceof Error && error.message.includes('exceeded time limit')) {\n\t\t\t\tthrow new AggregationTimeoutError();\n\t\t\t}\n\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during getQueueStats';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to get queue stats: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n","import { BSON, type Document } from 'mongodb';\n\nimport {\n\ttype EnqueueOptions,\n\ttype Job,\n\tJobStatus,\n\ttype PersistedJob,\n\ttype ScheduleOptions,\n} from '@/jobs';\nimport { ConnectionError, getNextCronDate, MonqueError, PayloadTooLargeError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for job scheduling operations.\n *\n * Handles enqueueing new jobs, immediate dispatch, and cron scheduling.\n * All operations are atomic and support deduplication via uniqueKey.\n *\n * @internal Not part of public API - use Monque class methods instead.\n */\nexport class JobScheduler {\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Validate that the job data payload does not exceed the configured maximum BSON byte size.\n\t *\n\t * @param data - The job data payload to validate\n\t * @throws {PayloadTooLargeError} If the payload exceeds `maxPayloadSize`\n\t */\n\tprivate validatePayloadSize(data: unknown): void {\n\t\tconst maxSize = this.ctx.options.maxPayloadSize;\n\t\tif (maxSize === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet size: number;\n\t\ttry {\n\t\t\tsize = BSON.calculateObjectSize({ data } as Document);\n\t\t} catch (error) {\n\t\t\tconst cause = error instanceof Error ? error : new Error(String(error));\n\t\t\tconst sizeError = new PayloadTooLargeError(\n\t\t\t\t`Failed to calculate job payload size: ${cause.message}`,\n\t\t\t\t-1,\n\t\t\t\tmaxSize,\n\t\t\t);\n\t\t\tsizeError.cause = cause;\n\t\t\tthrow sizeError;\n\t\t}\n\n\t\tif (size > maxSize) {\n\t\t\tthrow new PayloadTooLargeError(\n\t\t\t\t`Job payload exceeds maximum size: ${size} bytes > ${maxSize} bytes`,\n\t\t\t\tsize,\n\t\t\t\tmaxSize,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue a job for processing.\n\t *\n\t * Jobs are stored in MongoDB and processed by registered workers. Supports\n\t * delayed execution via `runAt` and deduplication via `uniqueKey`.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. Completed or failed jobs don't block new jobs with the same key.\n\t *\n\t * Failed jobs are automatically retried with exponential backoff up to `maxRetries`\n\t * (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @param options - Scheduling and deduplication options\n\t * @returns Promise resolving to the created or existing job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Basic job enqueueing\n\t * ```typescript\n\t * await monque.enqueue('send-email', {\n\t * to: 'user@example.com',\n\t * subject: 'Welcome!',\n\t * body: 'Thanks for signing up.'\n\t * });\n\t * ```\n\t *\n\t * @example Delayed execution\n\t * ```typescript\n\t * const oneHourLater = new Date(Date.now() + 3600000);\n\t * await monque.enqueue('reminder', { message: 'Check in!' }, {\n\t * runAt: oneHourLater\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicates with unique key\n\t * ```typescript\n\t * await monque.enqueue('sync-user', { userId: '123' }, {\n\t * uniqueKey: 'sync-user-123'\n\t * });\n\t * // Subsequent enqueues with same uniqueKey return existing pending/processing job\n\t * ```\n\t */\n\tasync enqueue<T>(name: string, data: T, options: EnqueueOptions = {}): Promise<PersistedJob<T>> {\n\t\tthis.validatePayloadSize(data);\n\t\tconst now = new Date();\n\t\tconst job: Omit<Job<T>, '_id'> = {\n\t\t\tname,\n\t\t\tdata,\n\t\t\tstatus: JobStatus.PENDING,\n\t\t\tnextRunAt: options.runAt ?? now,\n\t\t\tfailCount: 0,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\n\t\tif (options.uniqueKey) {\n\t\t\tjob.uniqueKey = options.uniqueKey;\n\t\t}\n\n\t\ttry {\n\t\t\tif (options.uniqueKey) {\n\t\t\t\t// Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)\n\t\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t\t{\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tuniqueKey: options.uniqueKey,\n\t\t\t\t\t\tstatus: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t$setOnInsert: job,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tupsert: true,\n\t\t\t\t\t\treturnDocument: 'after',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tif (!result) {\n\t\t\t\t\tthrow new ConnectionError('Failed to enqueue job: findOneAndUpdate returned no document');\n\t\t\t\t}\n\n\t\t\t\treturn this.ctx.documentToPersistedJob<T>(result);\n\t\t\t}\n\n\t\t\tconst result = await this.ctx.collection.insertOne(job as Document);\n\n\t\t\treturn { ...job, _id: result.insertedId } as PersistedJob<T>;\n\t\t} catch (error) {\n\t\t\tif (error instanceof ConnectionError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during enqueue';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to enqueue job: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue a job for immediate processing.\n\t *\n\t * Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.\n\t * Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @returns Promise resolving to the created job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example Send email immediately\n\t * ```typescript\n\t * await monque.now('send-email', {\n\t * to: 'admin@example.com',\n\t * subject: 'Alert',\n\t * body: 'Immediate attention required'\n\t * });\n\t * ```\n\t *\n\t * @example Process order in background\n\t * ```typescript\n\t * const order = await createOrder(data);\n\t * await monque.now('process-order', { orderId: order.id });\n\t * return order; // Return immediately, processing happens async\n\t * ```\n\t */\n\tasync now<T>(name: string, data: T): Promise<PersistedJob<T>> {\n\t\treturn this.enqueue(name, data, { runAt: new Date() });\n\t}\n\n\t/**\n\t * Schedule a recurring job with a cron expression.\n\t *\n\t * Creates a job that automatically re-schedules itself based on the cron pattern.\n\t * Uses standard 5-field cron format: minute, hour, day of month, month, day of week.\n\t * Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.\n\t * After successful completion, the job is reset to `pending` status and scheduled\n\t * for its next run based on the cron expression.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. This prevents duplicate scheduled jobs on application restart.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param cron - Cron expression (5 fields or predefined expression)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler on each run\n\t * @param options - Scheduling options (uniqueKey for deduplication)\n\t * @returns Promise resolving to the created job document with `repeatInterval` set\n\t * @throws {InvalidCronError} If cron expression is invalid\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Hourly cleanup job\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'cleanup-temp-files', {\n\t * directory: '/tmp/uploads'\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicate scheduled jobs with unique key\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {\n\t * uniqueKey: 'hourly-report-sales'\n\t * });\n\t * // Subsequent calls with same uniqueKey return existing pending/processing job\n\t * ```\n\t *\n\t * @example Daily report at midnight (using predefined expression)\n\t * ```typescript\n\t * await monque.schedule('@daily', 'daily-report', {\n\t * reportType: 'sales',\n\t * recipients: ['analytics@example.com']\n\t * });\n\t * ```\n\t */\n\tasync schedule<T>(\n\t\tcron: string,\n\t\tname: string,\n\t\tdata: T,\n\t\toptions: ScheduleOptions = {},\n\t): Promise<PersistedJob<T>> {\n\t\tthis.validatePayloadSize(data);\n\n\t\t// Validate cron and get next run date (throws InvalidCronError if invalid)\n\t\tconst nextRunAt = getNextCronDate(cron);\n\n\t\tconst now = new Date();\n\t\tconst job: Omit<Job<T>, '_id'> = {\n\t\t\tname,\n\t\t\tdata,\n\t\t\tstatus: JobStatus.PENDING,\n\t\t\tnextRunAt,\n\t\t\trepeatInterval: cron,\n\t\t\tfailCount: 0,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\n\t\tif (options.uniqueKey) {\n\t\t\tjob.uniqueKey = options.uniqueKey;\n\t\t}\n\n\t\ttry {\n\t\t\tif (options.uniqueKey) {\n\t\t\t\t// Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)\n\t\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t\t{\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tuniqueKey: options.uniqueKey,\n\t\t\t\t\t\tstatus: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t$setOnInsert: job,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tupsert: true,\n\t\t\t\t\t\treturnDocument: 'after',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tif (!result) {\n\t\t\t\t\tthrow new ConnectionError(\n\t\t\t\t\t\t'Failed to schedule job: findOneAndUpdate returned no document',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn this.ctx.documentToPersistedJob<T>(result);\n\t\t\t}\n\n\t\t\tconst result = await this.ctx.collection.insertOne(job as Document);\n\n\t\t\treturn { ...job, _id: result.insertedId } as PersistedJob<T>;\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during schedule';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to schedule job: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n","import type { DeleteResult } from 'mongodb';\n\nimport { JobStatus } from '@/jobs';\nimport { toError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Default retention check interval (1 hour).\n */\nconst DEFAULT_RETENTION_INTERVAL = 3600_000;\n\n/**\n * Callbacks for timer-driven operations.\n *\n * These are provided by the Monque facade to wire LifecycleManager's timers\n * to JobProcessor methods without creating a direct dependency.\n */\ninterface TimerCallbacks {\n\t/** Poll for pending jobs */\n\tpoll: () => Promise<void>;\n\t/** Update heartbeats for claimed jobs */\n\tupdateHeartbeats: () => Promise<void>;\n}\n\n/**\n * Manages scheduler lifecycle timers and job cleanup.\n *\n * Owns poll interval, heartbeat interval, cleanup interval, and the\n * cleanupJobs logic. Extracted from Monque to keep the facade thin.\n *\n * @internal Not part of public API.\n */\nexport class LifecycleManager {\n\tprivate readonly ctx: SchedulerContext;\n\tprivate pollIntervalId: ReturnType<typeof setInterval> | null = null;\n\tprivate heartbeatIntervalId: ReturnType<typeof setInterval> | null = null;\n\tprivate cleanupIntervalId: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(ctx: SchedulerContext) {\n\t\tthis.ctx = ctx;\n\t}\n\n\t/**\n\t * Start all lifecycle timers.\n\t *\n\t * Sets up poll interval, heartbeat interval, and (if configured)\n\t * cleanup interval. Runs an initial poll immediately.\n\t *\n\t * @param callbacks - Functions to invoke on each timer tick\n\t */\n\tstartTimers(callbacks: TimerCallbacks): void {\n\t\t// Set up polling as backup (runs at configured interval)\n\t\tthis.pollIntervalId = setInterval(() => {\n\t\t\tcallbacks.poll().catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\t\t}, this.ctx.options.pollInterval);\n\n\t\t// Start heartbeat interval for claimed jobs\n\t\tthis.heartbeatIntervalId = setInterval(() => {\n\t\t\tcallbacks.updateHeartbeats().catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\t\t}, this.ctx.options.heartbeatInterval);\n\n\t\t// Start cleanup interval if retention is configured\n\t\tif (this.ctx.options.jobRetention) {\n\t\t\tconst interval = this.ctx.options.jobRetention.interval ?? DEFAULT_RETENTION_INTERVAL;\n\n\t\t\t// Run immediately on start\n\t\t\tthis.cleanupJobs().catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\n\t\t\tthis.cleanupIntervalId = setInterval(() => {\n\t\t\t\tthis.cleanupJobs().catch((error: unknown) => {\n\t\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t\t});\n\t\t\t}, interval);\n\t\t}\n\n\t\t// Run initial poll immediately to pick up any existing jobs\n\t\tcallbacks.poll().catch((error: unknown) => {\n\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t});\n\t}\n\n\t/**\n\t * Stop all lifecycle timers.\n\t *\n\t * Clears poll, heartbeat, and cleanup intervals.\n\t */\n\tstopTimers(): void {\n\t\tif (this.cleanupIntervalId) {\n\t\t\tclearInterval(this.cleanupIntervalId);\n\t\t\tthis.cleanupIntervalId = null;\n\t\t}\n\n\t\tif (this.pollIntervalId) {\n\t\t\tclearInterval(this.pollIntervalId);\n\t\t\tthis.pollIntervalId = null;\n\t\t}\n\n\t\tif (this.heartbeatIntervalId) {\n\t\t\tclearInterval(this.heartbeatIntervalId);\n\t\t\tthis.heartbeatIntervalId = null;\n\t\t}\n\t}\n\n\t/**\n\t * Clean up old completed and failed jobs based on retention policy.\n\t *\n\t * - Removes completed jobs older than `jobRetention.completed`\n\t * - Removes failed jobs older than `jobRetention.failed`\n\t *\n\t * The cleanup runs concurrently for both statuses if configured.\n\t *\n\t * @returns Promise resolving when all deletion operations complete\n\t */\n\tasync cleanupJobs(): Promise<void> {\n\t\tif (!this.ctx.options.jobRetention) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { completed, failed } = this.ctx.options.jobRetention;\n\t\tconst now = Date.now();\n\t\tconst deletions: Promise<DeleteResult>[] = [];\n\n\t\tif (completed != null) {\n\t\t\tconst cutoff = new Date(now - completed);\n\t\t\tdeletions.push(\n\t\t\t\tthis.ctx.collection.deleteMany({\n\t\t\t\t\tstatus: JobStatus.COMPLETED,\n\t\t\t\t\tupdatedAt: { $lt: cutoff },\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (failed != null) {\n\t\t\tconst cutoff = new Date(now - failed);\n\t\t\tdeletions.push(\n\t\t\t\tthis.ctx.collection.deleteMany({\n\t\t\t\t\tstatus: JobStatus.FAILED,\n\t\t\t\t\tupdatedAt: { $lt: cutoff },\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (deletions.length > 0) {\n\t\t\tawait Promise.all(deletions);\n\t\t}\n\t}\n}\n","import { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport type { Collection, Db, Document, ObjectId, WithId } from 'mongodb';\n\nimport type { MonqueEventMap } from '@/events';\nimport {\n\ttype BulkOperationResult,\n\ttype CursorOptions,\n\ttype CursorPage,\n\tdocumentToPersistedJob,\n\ttype EnqueueOptions,\n\ttype GetJobsFilter,\n\ttype Job,\n\ttype JobHandler,\n\ttype JobSelector,\n\tJobStatus,\n\ttype PersistedJob,\n\ttype QueueStats,\n\ttype ScheduleOptions,\n} from '@/jobs';\nimport { ConnectionError, ShutdownTimeoutError, WorkerRegistrationError } from '@/shared';\nimport type { WorkerOptions, WorkerRegistration } from '@/workers';\n\nimport {\n\tChangeStreamHandler,\n\tJobManager,\n\tJobProcessor,\n\tJobQueryService,\n\tJobScheduler,\n\tLifecycleManager,\n\ttype ResolvedMonqueOptions,\n\ttype SchedulerContext,\n} from './services/index.js';\nimport type { MonqueOptions } from './types.js';\n\n/**\n * Default configuration values\n */\nconst DEFAULTS = {\n\tcollectionName: 'monque_jobs',\n\tpollInterval: 1000,\n\tmaxRetries: 10,\n\tbaseRetryInterval: 1000,\n\tshutdownTimeout: 30000,\n\tworkerConcurrency: 5,\n\tlockTimeout: 1_800_000, // 30 minutes\n\trecoverStaleJobs: true,\n\theartbeatInterval: 30000, // 30 seconds\n\tretentionInterval: 3600_000, // 1 hour\n} as const;\n\n/**\n * Monque - MongoDB-backed job scheduler\n *\n * A type-safe job scheduler with atomic locking, exponential backoff, cron scheduling,\n * stale job recovery, and event-driven observability. Built on native MongoDB driver.\n *\n * @example Complete lifecycle\n * ```typescript\n * import { Monque } from '@monque/core';\n * import { MongoClient } from 'mongodb';\n *\n * const client = new MongoClient('mongodb://localhost:27017');\n * await client.connect();\n * const db = client.db('myapp');\n *\n * // Create instance with options\n * const monque = new Monque(db, {\n * collectionName: 'jobs',\n * pollInterval: 1000,\n * maxRetries: 10,\n * shutdownTimeout: 30000,\n * });\n *\n * // Initialize (sets up indexes and recovers stale jobs)\n * await monque.initialize();\n *\n * // Register workers with type safety\n * type EmailJob = {\n * to: string;\n * subject: string;\n * body: string;\n * };\n *\n * monque.register<EmailJob>('send-email', async (job) => {\n * await emailService.send(job.data.to, job.data.subject, job.data.body);\n * });\n *\n * // Monitor events for observability\n * monque.on('job:complete', ({ job, duration }) => {\n * logger.info(`Job ${job.name} completed in ${duration}ms`);\n * });\n *\n * monque.on('job:fail', ({ job, error, willRetry }) => {\n * logger.error(`Job ${job.name} failed:`, error);\n * });\n *\n * // Start processing\n * monque.start();\n *\n * // Enqueue jobs\n * await monque.enqueue('send-email', {\n * to: 'user@example.com',\n * subject: 'Welcome!',\n * body: 'Thanks for signing up.'\n * });\n *\n * // Graceful shutdown\n * process.on('SIGTERM', async () => {\n * await monque.stop();\n * await client.close();\n * process.exit(0);\n * });\n * ```\n */\nexport class Monque extends EventEmitter {\n\tprivate readonly db: Db;\n\tprivate readonly options: ResolvedMonqueOptions;\n\tprivate collection: Collection<Document> | null = null;\n\tprivate workers: Map<string, WorkerRegistration> = new Map();\n\tprivate isRunning = false;\n\tprivate isInitialized = false;\n\n\t// Internal services (initialized in initialize())\n\tprivate _scheduler: JobScheduler | null = null;\n\tprivate _manager: JobManager | null = null;\n\tprivate _query: JobQueryService | null = null;\n\tprivate _processor: JobProcessor | null = null;\n\tprivate _changeStreamHandler: ChangeStreamHandler | null = null;\n\tprivate _lifecycleManager: LifecycleManager | null = null;\n\n\tconstructor(db: Db, options: MonqueOptions = {}) {\n\t\tsuper();\n\t\tthis.db = db;\n\t\tthis.options = {\n\t\t\tcollectionName: options.collectionName ?? DEFAULTS.collectionName,\n\t\t\tpollInterval: options.pollInterval ?? DEFAULTS.pollInterval,\n\t\t\tmaxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n\t\t\tbaseRetryInterval: options.baseRetryInterval ?? DEFAULTS.baseRetryInterval,\n\t\t\tshutdownTimeout: options.shutdownTimeout ?? DEFAULTS.shutdownTimeout,\n\t\t\tworkerConcurrency:\n\t\t\t\toptions.workerConcurrency ?? options.defaultConcurrency ?? DEFAULTS.workerConcurrency,\n\t\t\tlockTimeout: options.lockTimeout ?? DEFAULTS.lockTimeout,\n\t\t\trecoverStaleJobs: options.recoverStaleJobs ?? DEFAULTS.recoverStaleJobs,\n\t\t\tmaxBackoffDelay: options.maxBackoffDelay,\n\t\t\tinstanceConcurrency: options.instanceConcurrency ?? options.maxConcurrency,\n\t\t\tschedulerInstanceId: options.schedulerInstanceId ?? randomUUID(),\n\t\t\theartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,\n\t\t\tjobRetention: options.jobRetention,\n\t\t\tskipIndexCreation: options.skipIndexCreation ?? false,\n\t\t\tmaxPayloadSize: options.maxPayloadSize,\n\t\t\tstatsCacheTtlMs: options.statsCacheTtlMs ?? 5000,\n\t\t};\n\t}\n\n\t/**\n\t * Initialize the scheduler by setting up the MongoDB collection and indexes.\n\t * Must be called before start().\n\t *\n\t * @throws {ConnectionError} If collection or index creation fails\n\t */\n\tasync initialize(): Promise<void> {\n\t\tif (this.isInitialized) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.collection = this.db.collection(this.options.collectionName);\n\n\t\t\t// Create indexes for efficient queries (unless externally managed)\n\t\t\tif (!this.options.skipIndexCreation) {\n\t\t\t\tawait this.createIndexes();\n\t\t\t}\n\n\t\t\t// Recover stale jobs if enabled\n\t\t\tif (this.options.recoverStaleJobs) {\n\t\t\t\tawait this.recoverStaleJobs();\n\t\t\t}\n\n\t\t\t// Check for instance ID collisions (after stale recovery to avoid false positives)\n\t\t\tawait this.checkInstanceCollision();\n\n\t\t\t// Initialize services with shared context\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis._scheduler = new JobScheduler(ctx);\n\t\t\tthis._manager = new JobManager(ctx);\n\t\t\tthis._query = new JobQueryService(ctx);\n\t\t\tthis._processor = new JobProcessor(ctx);\n\t\t\tthis._changeStreamHandler = new ChangeStreamHandler(ctx, () => this.processor.poll());\n\t\t\tthis._lifecycleManager = new LifecycleManager(ctx);\n\n\t\t\tthis.isInitialized = true;\n\t\t} catch (error) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : 'Unknown error during initialization';\n\t\t\tthrow new ConnectionError(`Failed to initialize Monque: ${message}`);\n\t\t}\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Service Accessors (throw if not initialized)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get scheduler(): JobScheduler {\n\t\tif (!this._scheduler) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._scheduler;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get manager(): JobManager {\n\t\tif (!this._manager) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._manager;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get query(): JobQueryService {\n\t\tif (!this._query) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._query;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get processor(): JobProcessor {\n\t\tif (!this._processor) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._processor;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get changeStreamHandler(): ChangeStreamHandler {\n\t\tif (!this._changeStreamHandler) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._changeStreamHandler;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get lifecycleManager(): LifecycleManager {\n\t\tif (!this._lifecycleManager) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._lifecycleManager;\n\t}\n\n\t/**\n\t * Build the shared context for internal services.\n\t */\n\tprivate buildContext(): SchedulerContext {\n\t\tif (!this.collection) {\n\t\t\tthrow new ConnectionError('Collection not initialized');\n\t\t}\n\n\t\treturn {\n\t\t\tcollection: this.collection,\n\t\t\toptions: this.options,\n\t\t\tinstanceId: this.options.schedulerInstanceId,\n\t\t\tworkers: this.workers,\n\t\t\tisRunning: () => this.isRunning,\n\t\t\temit: <K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]) =>\n\t\t\t\tthis.emit(event, payload),\n\t\t\tdocumentToPersistedJob: <T>(doc: WithId<Document>) => documentToPersistedJob<T>(doc),\n\t\t};\n\t}\n\t/**\n\t * Create required MongoDB indexes for efficient job processing.\n\t *\n\t * The following indexes are created:\n\t * - `{status, nextRunAt}` - For efficient job polling queries\n\t * - `{name, uniqueKey}` - Partial unique index for deduplication (pending/processing only)\n\t * - `{name, status}` - For job lookup by type\n\t * - `{claimedBy, status}` - For finding jobs owned by a specific scheduler instance\n\t * - `{lastHeartbeat, status}` - For monitoring/debugging queries (e.g., inspecting heartbeat age)\n\t * - `{status, nextRunAt, claimedBy}` - For atomic claim queries (find unclaimed pending jobs)\n\t * - `{lockedAt, lastHeartbeat, status}` - Supports recovery scans and monitoring access patterns\n\t */\n\tprivate async createIndexes(): Promise<void> {\n\t\tif (!this.collection) {\n\t\t\tthrow new ConnectionError('Collection not initialized');\n\t\t}\n\n\t\tawait this.collection.createIndexes([\n\t\t\t// Compound index for job polling - status + nextRunAt for efficient queries\n\t\t\t{ key: { status: 1, nextRunAt: 1 }, background: true },\n\t\t\t// Partial unique index for deduplication - scoped by name + uniqueKey\n\t\t\t// Only enforced where uniqueKey exists and status is pending/processing\n\t\t\t{\n\t\t\t\tkey: { name: 1, uniqueKey: 1 },\n\t\t\t\tunique: true,\n\t\t\t\tpartialFilterExpression: {\n\t\t\t\t\tuniqueKey: { $exists: true },\n\t\t\t\t\tstatus: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },\n\t\t\t\t},\n\t\t\t\tbackground: true,\n\t\t\t},\n\t\t\t// Index for job lookup by name\n\t\t\t{ key: { name: 1, status: 1 }, background: true },\n\t\t\t// Compound index for finding jobs claimed by a specific scheduler instance.\n\t\t\t// Used for heartbeat updates and cleanup on shutdown.\n\t\t\t{ key: { claimedBy: 1, status: 1 }, background: true },\n\t\t\t// Compound index for monitoring/debugging via heartbeat timestamps.\n\t\t\t// Note: stale recovery uses lockedAt + lockTimeout as the source of truth.\n\t\t\t{ key: { lastHeartbeat: 1, status: 1 }, background: true },\n\t\t\t// Compound index for atomic claim queries.\n\t\t\t// Optimizes the findOneAndUpdate query that claims unclaimed pending jobs.\n\t\t\t{ key: { status: 1, nextRunAt: 1, claimedBy: 1 }, background: true },\n\t\t\t// Expanded index that supports recovery scans (status + lockedAt) plus heartbeat monitoring patterns.\n\t\t\t{ key: { status: 1, lockedAt: 1, lastHeartbeat: 1 }, background: true },\n\t\t]);\n\t}\n\n\t/**\n\t * Recover stale jobs that were left in 'processing' status.\n\t * A job is considered stale if its `lockedAt` timestamp exceeds the configured `lockTimeout`.\n\t * Stale jobs are reset to 'pending' so they can be picked up by workers again.\n\t */\n\tprivate async recoverStaleJobs(): Promise<void> {\n\t\tif (!this.collection) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst staleThreshold = new Date(Date.now() - this.options.lockTimeout);\n\n\t\tconst result = await this.collection.updateMany(\n\t\t\t{\n\t\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\t\tlockedAt: { $lt: staleThreshold },\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tif (result.modifiedCount > 0) {\n\t\t\t// Emit event for recovered jobs\n\t\t\tthis.emit('stale:recovered', {\n\t\t\t\tcount: result.modifiedCount,\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Check if another active instance is using the same schedulerInstanceId.\n\t * Uses heartbeat staleness to distinguish active instances from crashed ones.\n\t *\n\t * Called after stale recovery to avoid false positives: stale recovery resets\n\t * jobs with old `lockedAt`, so only jobs with recent heartbeats remain.\n\t *\n\t * @throws {ConnectionError} If an active instance with the same ID is detected\n\t */\n\tprivate async checkInstanceCollision(): Promise<void> {\n\t\tif (!this.collection) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Look for any job currently claimed by this instance ID\n\t\t// that has a recent heartbeat (within 2× heartbeat interval = \"alive\" threshold)\n\t\tconst aliveThreshold = new Date(Date.now() - this.options.heartbeatInterval * 2);\n\n\t\tconst activeJob = await this.collection.findOne({\n\t\t\tclaimedBy: this.options.schedulerInstanceId,\n\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\tlastHeartbeat: { $gte: aliveThreshold },\n\t\t});\n\n\t\tif (activeJob) {\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Another active Monque instance is using schedulerInstanceId \"${this.options.schedulerInstanceId}\". ` +\n\t\t\t\t\t`Found processing job \"${activeJob['name']}\" with recent heartbeat. ` +\n\t\t\t\t\t`Use a unique schedulerInstanceId or wait for the other instance to stop.`,\n\t\t\t);\n\t\t}\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Job Scheduling (delegates to JobScheduler)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Enqueue a job for processing.\n\t *\n\t * Jobs are stored in MongoDB and processed by registered workers. Supports\n\t * delayed execution via `runAt` and deduplication via `uniqueKey`.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. Completed or failed jobs don't block new jobs with the same key.\n\t *\n\t * Failed jobs are automatically retried with exponential backoff up to `maxRetries`\n\t * (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @param options - Scheduling and deduplication options\n\t * @returns Promise resolving to the created or existing job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Basic job enqueueing\n\t * ```typescript\n\t * await monque.enqueue('send-email', {\n\t * to: 'user@example.com',\n\t * subject: 'Welcome!',\n\t * body: 'Thanks for signing up.'\n\t * });\n\t * ```\n\t *\n\t * @example Delayed execution\n\t * ```typescript\n\t * const oneHourLater = new Date(Date.now() + 3600000);\n\t * await monque.enqueue('reminder', { message: 'Check in!' }, {\n\t * runAt: oneHourLater\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicates with unique key\n\t * ```typescript\n\t * await monque.enqueue('sync-user', { userId: '123' }, {\n\t * uniqueKey: 'sync-user-123'\n\t * });\n\t * // Subsequent enqueues with same uniqueKey return existing pending/processing job\n\t * ```\n\t *\n\t * @see {@link JobScheduler.enqueue}\n\t */\n\tasync enqueue<T>(name: string, data: T, options: EnqueueOptions = {}): Promise<PersistedJob<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.scheduler.enqueue(name, data, options);\n\t}\n\n\t/**\n\t * Enqueue a job for immediate processing.\n\t *\n\t * Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.\n\t * Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @returns Promise resolving to the created job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example Send email immediately\n\t * ```typescript\n\t * await monque.now('send-email', {\n\t * to: 'admin@example.com',\n\t * subject: 'Alert',\n\t * body: 'Immediate attention required'\n\t * });\n\t * ```\n\t *\n\t * @example Process order in background\n\t * ```typescript\n\t * const order = await createOrder(data);\n\t * await monque.now('process-order', { orderId: order.id });\n\t * return order; // Return immediately, processing happens async\n\t * ```\n\t *\n\t * @see {@link JobScheduler.now}\n\t */\n\tasync now<T>(name: string, data: T): Promise<PersistedJob<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.scheduler.now(name, data);\n\t}\n\n\t/**\n\t * Schedule a recurring job with a cron expression.\n\t *\n\t * Creates a job that automatically re-schedules itself based on the cron pattern.\n\t * Uses standard 5-field cron format: minute, hour, day of month, month, day of week.\n\t * Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.\n\t * After successful completion, the job is reset to `pending` status and scheduled\n\t * for its next run based on the cron expression.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. This prevents duplicate scheduled jobs on application restart.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param cron - Cron expression (5 fields or predefined expression)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler on each run\n\t * @param options - Scheduling options (uniqueKey for deduplication)\n\t * @returns Promise resolving to the created job document with `repeatInterval` set\n\t * @throws {InvalidCronError} If cron expression is invalid\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Hourly cleanup job\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'cleanup-temp-files', {\n\t * directory: '/tmp/uploads'\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicate scheduled jobs with unique key\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {\n\t * uniqueKey: 'hourly-report-sales'\n\t * });\n\t * // Subsequent calls with same uniqueKey return existing pending/processing job\n\t * ```\n\t *\n\t * @example Daily report at midnight (using predefined expression)\n\t * ```typescript\n\t * await monque.schedule('@daily', 'daily-report', {\n\t * reportType: 'sales',\n\t * recipients: ['analytics@example.com']\n\t * });\n\t * ```\n\t *\n\t * @see {@link JobScheduler.schedule}\n\t */\n\tasync schedule<T>(\n\t\tcron: string,\n\t\tname: string,\n\t\tdata: T,\n\t\toptions: ScheduleOptions = {},\n\t): Promise<PersistedJob<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.scheduler.schedule(cron, name, data, options);\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Job Management (delegates to JobManager)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Cancel a pending or scheduled job.\n\t *\n\t * Sets the job status to 'cancelled' and emits a 'job:cancelled' event.\n\t * If the job is already cancelled, this is a no-op and returns the job.\n\t * Cannot cancel jobs that are currently 'processing', 'completed', or 'failed'.\n\t *\n\t * @param jobId - The ID of the job to cancel\n\t * @returns The cancelled job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for cancellation\n\t *\n\t * @example Cancel a pending job\n\t * ```typescript\n\t * const job = await monque.enqueue('report', { type: 'daily' });\n\t * await monque.cancelJob(job._id.toString());\n\t * ```\n\t *\n\t * @see {@link JobManager.cancelJob}\n\t */\n\tasync cancelJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.cancelJob(jobId);\n\t}\n\n\t/**\n\t * Retry a failed or cancelled job.\n\t *\n\t * Resets the job to 'pending' status, clears failure count/reason, and sets\n\t * nextRunAt to now (immediate retry). Emits a 'job:retried' event.\n\t *\n\t * @param jobId - The ID of the job to retry\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for retry (must be failed or cancelled)\n\t *\n\t * @example Retry a failed job\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * console.log(`Job ${job._id} failed, retrying manually...`);\n\t * await monque.retryJob(job._id.toString());\n\t * });\n\t * ```\n\t *\n\t * @see {@link JobManager.retryJob}\n\t */\n\tasync retryJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.retryJob(jobId);\n\t}\n\n\t/**\n\t * Reschedule a pending job to run at a different time.\n\t *\n\t * Only works for jobs in 'pending' status.\n\t *\n\t * @param jobId - The ID of the job to reschedule\n\t * @param runAt - The new Date when the job should run\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is not in pending state\n\t *\n\t * @example Delay a job by 1 hour\n\t * ```typescript\n\t * const nextHour = new Date(Date.now() + 60 * 60 * 1000);\n\t * await monque.rescheduleJob(jobId, nextHour);\n\t * ```\n\t *\n\t * @see {@link JobManager.rescheduleJob}\n\t */\n\tasync rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.rescheduleJob(jobId, runAt);\n\t}\n\n\t/**\n\t * Permanently delete a job.\n\t *\n\t * This action is irreversible. Emits a 'job:deleted' event upon success.\n\t * Can delete a job in any state.\n\t *\n\t * @param jobId - The ID of the job to delete\n\t * @returns true if deleted, false if job not found\n\t *\n\t * @example Delete a cleanup job\n\t * ```typescript\n\t * const deleted = await monque.deleteJob(jobId);\n\t * if (deleted) {\n\t * console.log('Job permanently removed');\n\t * }\n\t * ```\n\t *\n\t * @see {@link JobManager.deleteJob}\n\t */\n\tasync deleteJob(jobId: string): Promise<boolean> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.deleteJob(jobId);\n\t}\n\n\t/**\n\t * Cancel multiple jobs matching the given filter via a single updateMany call.\n\t *\n\t * Only cancels jobs in 'pending' status — the status guard is applied regardless\n\t * of what the filter specifies. Jobs in other states are silently skipped (not\n\t * matched by the query). Emits a 'jobs:cancelled' event with the count of\n\t * successfully cancelled jobs.\n\t *\n\t * @param filter - Selector for which jobs to cancel (name, status, date range)\n\t * @returns Result with count of cancelled jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Cancel all pending jobs for a queue\n\t * ```typescript\n\t * const result = await monque.cancelJobs({\n\t * name: 'email-queue',\n\t * status: 'pending'\n\t * });\n\t * console.log(`Cancelled ${result.count} jobs`);\n\t * ```\n\t *\n\t * @see {@link JobManager.cancelJobs}\n\t */\n\tasync cancelJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.cancelJobs(filter);\n\t}\n\n\t/**\n\t * Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.\n\t *\n\t * Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied\n\t * regardless of what the filter specifies. Jobs in other states are silently skipped.\n\t * Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.\n\t * Emits a 'jobs:retried' event with the count of successfully retried jobs.\n\t *\n\t * @param filter - Selector for which jobs to retry (name, status, date range)\n\t * @returns Result with count of retried jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Retry all failed jobs\n\t * ```typescript\n\t * const result = await monque.retryJobs({\n\t * status: 'failed'\n\t * });\n\t * console.log(`Retried ${result.count} jobs`);\n\t * ```\n\t *\n\t * @see {@link JobManager.retryJobs}\n\t */\n\tasync retryJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.retryJobs(filter);\n\t}\n\n\t/**\n\t * Delete multiple jobs matching the given filter.\n\t *\n\t * Deletes jobs in any status. Uses a batch delete for efficiency.\n\t * Emits a 'jobs:deleted' event with the count of deleted jobs.\n\t * Does not emit individual 'job:deleted' events to avoid noise.\n\t *\n\t * @param filter - Selector for which jobs to delete (name, status, date range)\n\t * @returns Result with count of deleted jobs (errors array always empty for delete)\n\t *\n\t * @example Delete old completed jobs\n\t * ```typescript\n\t * const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\t * const result = await monque.deleteJobs({\n\t * status: 'completed',\n\t * olderThan: weekAgo\n\t * });\n\t * console.log(`Deleted ${result.count} jobs`);\n\t * ```\n\t *\n\t * @see {@link JobManager.deleteJobs}\n\t */\n\tasync deleteJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.deleteJobs(filter);\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Job Queries (delegates to JobQueryService)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Get a single job by its MongoDB ObjectId.\n\t *\n\t * Useful for retrieving job details when you have a job ID from events,\n\t * logs, or stored references.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param id - The job's ObjectId\n\t * @returns Promise resolving to the job if found, null otherwise\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Look up job from event\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * // Later, retrieve the job to check its status\n\t * const currentJob = await monque.getJob(job._id);\n\t * console.log(`Job status: ${currentJob?.status}`);\n\t * });\n\t * ```\n\t *\n\t * @example Admin endpoint\n\t * ```typescript\n\t * app.get('/jobs/:id', async (req, res) => {\n\t * const job = await monque.getJob(new ObjectId(req.params.id));\n\t * if (!job) {\n\t * return res.status(404).json({ error: 'Job not found' });\n\t * }\n\t * res.json(job);\n\t * });\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getJob}\n\t */\n\tasync getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getJob<T>(id);\n\t}\n\n\t/**\n\t * Query jobs from the queue with optional filters.\n\t *\n\t * Provides read-only access to job data for monitoring, debugging, and\n\t * administrative purposes. Results are ordered by `nextRunAt` ascending.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param filter - Optional filter criteria\n\t * @returns Promise resolving to array of matching jobs\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Get all pending jobs\n\t * ```typescript\n\t * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });\n\t * console.log(`${pendingJobs.length} jobs waiting`);\n\t * ```\n\t *\n\t * @example Get failed email jobs\n\t * ```typescript\n\t * const failedEmails = await monque.getJobs({\n\t * name: 'send-email',\n\t * status: JobStatus.FAILED,\n\t * });\n\t * for (const job of failedEmails) {\n\t * console.error(`Job ${job._id} failed: ${job.failReason}`);\n\t * }\n\t * ```\n\t *\n\t * @example Paginated job listing\n\t * ```typescript\n\t * const page1 = await monque.getJobs({ limit: 50, skip: 0 });\n\t * const page2 = await monque.getJobs({ limit: 50, skip: 50 });\n\t * ```\n\t *\n\t * @example Use with type guards from @monque/core\n\t * ```typescript\n\t * import { isPendingJob, isRecurringJob } from '@monque/core';\n\t *\n\t * const jobs = await monque.getJobs();\n\t * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getJobs}\n\t */\n\tasync getJobs<T = unknown>(filter: GetJobsFilter = {}): Promise<PersistedJob<T>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getJobs<T>(filter);\n\t}\n\n\t/**\n\t * Get a paginated list of jobs using opaque cursors.\n\t *\n\t * Provides stable pagination for large job lists. Supports forward and backward\n\t * navigation, filtering, and efficient database access via index-based cursor queries.\n\t *\n\t * @template T - The job data payload type\n\t * @param options - Pagination options (cursor, limit, direction, filter)\n\t * @returns Page of jobs with next/prev cursors\n\t * @throws {InvalidCursorError} If the provided cursor is malformed\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example List pending jobs\n\t * ```typescript\n\t * const page = await monque.getJobsWithCursor({\n\t * limit: 20,\n\t * filter: { status: 'pending' }\n\t * });\n\t * const jobs = page.jobs;\n\t *\n\t * // Get next page\n\t * if (page.hasNextPage) {\n\t * const page2 = await monque.getJobsWithCursor({\n\t * cursor: page.cursor,\n\t * limit: 20\n\t * });\n\t * }\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getJobsWithCursor}\n\t */\n\tasync getJobsWithCursor<T = unknown>(options: CursorOptions = {}): Promise<CursorPage<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getJobsWithCursor<T>(options);\n\t}\n\n\t/**\n\t * Get aggregate statistics for the job queue.\n\t *\n\t * Uses MongoDB aggregation pipeline for efficient server-side calculation.\n\t * Returns counts per status and optional average processing duration for completed jobs.\n\t *\n\t * Results are cached per unique filter with a configurable TTL (default 5s).\n\t * Set `statsCacheTtlMs: 0` to disable caching.\n\t *\n\t * @param filter - Optional filter to scope statistics by job name\n\t * @returns Promise resolving to queue statistics\n\t * @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout\n\t * @throws {ConnectionError} If database operation fails\n\t *\n\t * @example Get overall queue statistics\n\t * ```typescript\n\t * const stats = await monque.getQueueStats();\n\t * console.log(`Pending: ${stats.pending}, Failed: ${stats.failed}`);\n\t * ```\n\t *\n\t * @example Get statistics for a specific job type\n\t * ```typescript\n\t * const emailStats = await monque.getQueueStats({ name: 'send-email' });\n\t * console.log(`${emailStats.total} email jobs in queue`);\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getQueueStats}\n\t */\n\tasync getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getQueueStats(filter);\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Worker Registration\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Register a worker to process jobs of a specific type.\n\t *\n\t * Workers can be registered before or after calling `start()`. Each worker\n\t * processes jobs concurrently up to its configured concurrency limit (default: 5).\n\t *\n\t * The handler function receives the full job object including metadata (`_id`, `status`,\n\t * `failCount`, etc.). If the handler throws an error, the job is retried with exponential\n\t * backoff up to `maxRetries` times. After exhausting retries, the job is marked as `failed`.\n\t *\n\t * Events are emitted during job processing: `job:start`, `job:complete`, `job:fail`, and `job:error`.\n\t *\n\t * **Duplicate Registration**: By default, registering a worker for a job name that already has\n\t * a worker will throw a `WorkerRegistrationError`. This fail-fast behavior prevents accidental\n\t * replacement of handlers. To explicitly replace a worker, pass `{ replace: true }`.\n\t *\n\t * @template T - The job data payload type for type-safe access to `job.data`\n\t * @param name - Job type identifier to handle\n\t * @param handler - Async function to execute for each job\n\t * @param options - Worker configuration\n\t * @param options.concurrency - Maximum concurrent jobs for this worker (default: `defaultConcurrency`)\n\t * @param options.replace - When `true`, replace existing worker instead of throwing error\n\t * @throws {WorkerRegistrationError} When a worker is already registered for `name` and `replace` is not `true`\n\t *\n\t * @example Basic email worker\n\t * ```typescript\n\t * interface EmailJob {\n\t * to: string;\n\t * subject: string;\n\t * body: string;\n\t * }\n\t *\n\t * monque.register<EmailJob>('send-email', async (job) => {\n\t * await emailService.send(job.data.to, job.data.subject, job.data.body);\n\t * });\n\t * ```\n\t *\n\t * @example Worker with custom concurrency\n\t * ```typescript\n\t * // Limit to 2 concurrent video processing jobs (resource-intensive)\n\t * monque.register('process-video', async (job) => {\n\t * await videoProcessor.transcode(job.data.videoId);\n\t * }, { concurrency: 2 });\n\t * ```\n\t *\n\t * @example Replacing an existing worker\n\t * ```typescript\n\t * // Replace the existing handler for 'send-email'\n\t * monque.register('send-email', newEmailHandler, { replace: true });\n\t * ```\n\t *\n\t * @example Worker with error handling\n\t * ```typescript\n\t * monque.register('sync-user', async (job) => {\n\t * try {\n\t * await externalApi.syncUser(job.data.userId);\n\t * } catch (error) {\n\t * // Job will retry with exponential backoff\n\t * // Delay = 2^failCount × baseRetryInterval (default: 1000ms)\n\t * throw new Error(`Sync failed: ${error.message}`);\n\t * }\n\t * });\n\t * ```\n\t */\n\tregister<T>(name: string, handler: JobHandler<T>, options: WorkerOptions = {}): void {\n\t\tconst concurrency = options.concurrency ?? this.options.workerConcurrency;\n\n\t\t// Check for existing worker and throw unless replace is explicitly true\n\t\tif (this.workers.has(name) && options.replace !== true) {\n\t\t\tthrow new WorkerRegistrationError(\n\t\t\t\t`Worker already registered for job name \"${name}\". Use { replace: true } to replace.`,\n\t\t\t\tname,\n\t\t\t);\n\t\t}\n\n\t\tthis.workers.set(name, {\n\t\t\thandler: handler as JobHandler,\n\t\t\tconcurrency,\n\t\t\tactiveJobs: new Map(),\n\t\t});\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Lifecycle\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Start polling for and processing jobs.\n\t *\n\t * Begins polling MongoDB at the configured interval (default: 1 second) to pick up\n\t * pending jobs and dispatch them to registered workers. Must call `initialize()` first.\n\t * Workers can be registered before or after calling `start()`.\n\t *\n\t * Jobs are processed concurrently up to each worker's configured concurrency limit.\n\t * The scheduler continues running until `stop()` is called.\n\t *\n\t * @example Basic startup\n\t * ```typescript\n\t * const monque = new Monque(db);\n\t * await monque.initialize();\n\t *\n\t * monque.register('send-email', emailHandler);\n\t * monque.register('process-order', orderHandler);\n\t *\n\t * monque.start(); // Begin processing jobs\n\t * ```\n\t *\n\t * @example With event monitoring\n\t * ```typescript\n\t * monque.on('job:start', (job) => {\n\t * logger.info(`Starting job ${job.name}`);\n\t * });\n\t *\n\t * monque.on('job:complete', ({ job, duration }) => {\n\t * metrics.recordJobDuration(job.name, duration);\n\t * });\n\t *\n\t * monque.on('job:fail', ({ job, error, willRetry }) => {\n\t * logger.error(`Job ${job.name} failed:`, error);\n\t * if (!willRetry) {\n\t * alerting.sendAlert(`Job permanently failed: ${job.name}`);\n\t * }\n\t * });\n\t *\n\t * monque.start();\n\t * ```\n\t *\n\t * @throws {ConnectionError} If scheduler not initialized (call `initialize()` first)\n\t */\n\tstart(): void {\n\t\tif (this.isRunning) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.isInitialized) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() before start().');\n\t\t}\n\n\t\tthis.isRunning = true;\n\n\t\t// Set up change streams as the primary notification mechanism\n\t\tthis.changeStreamHandler.setup();\n\n\t\t// Delegate timer management to LifecycleManager\n\t\tthis.lifecycleManager.startTimers({\n\t\t\tpoll: () => this.processor.poll(),\n\t\t\tupdateHeartbeats: () => this.processor.updateHeartbeats(),\n\t\t});\n\t}\n\n\t/**\n\t * Stop the scheduler gracefully, waiting for in-progress jobs to complete.\n\t *\n\t * Stops polling for new jobs and waits for all active jobs to finish processing.\n\t * Times out after the configured `shutdownTimeout` (default: 30 seconds), emitting\n\t * a `job:error` event with a `ShutdownTimeoutError` containing incomplete jobs.\n\t * On timeout, jobs still in progress are left as `processing` for stale job recovery.\n\t *\n\t * It's safe to call `stop()` multiple times - subsequent calls are no-ops if already stopped.\n\t *\n\t * @returns Promise that resolves when all jobs complete or timeout is reached\n\t *\n\t * @example Graceful application shutdown\n\t * ```typescript\n\t * process.on('SIGTERM', async () => {\n\t * console.log('Shutting down gracefully...');\n\t * await monque.stop(); // Wait for jobs to complete\n\t * await mongoClient.close();\n\t * process.exit(0);\n\t * });\n\t * ```\n\t *\n\t * @example With timeout handling\n\t * ```typescript\n\t * monque.on('job:error', ({ error }) => {\n\t * if (error.name === 'ShutdownTimeoutError') {\n\t * logger.warn('Forced shutdown after timeout:', error.incompleteJobs);\n\t * }\n\t * });\n\t *\n\t * await monque.stop();\n\t * ```\n\t */\n\n\tasync stop(): Promise<void> {\n\t\tif (!this.isRunning) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Stop all lifecycle timers FIRST to prevent new poll callbacks\n\t\t// This closes the race window where a queued poll tick could\n\t\t// check isRunning before the flag is set to false\n\t\tthis.lifecycleManager.stopTimers();\n\n\t\tthis.isRunning = false;\n\n\t\t// Clear stats cache for clean state on restart\n\t\tthis._query?.clearStatsCache();\n\n\t\t// Close change stream — catch-and-ignore per shutdown cleanup guideline\n\t\ttry {\n\t\t\tawait this.changeStreamHandler.close();\n\t\t} catch {\n\t\t\t// ignore errors during shutdown cleanup\n\t\t}\n\n\t\t// Wait for all active jobs to complete (with timeout)\n\t\tconst activeJobs = this.getActiveJobs();\n\t\tif (activeJobs.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Create a promise that resolves when all jobs are done\n\t\tlet checkInterval: ReturnType<typeof setInterval> | undefined;\n\t\tconst waitForJobs = new Promise<undefined>((resolve) => {\n\t\t\tcheckInterval = setInterval(() => {\n\t\t\t\tif (this.getActiveJobs().length === 0) {\n\t\t\t\t\tclearInterval(checkInterval);\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t}\n\t\t\t}, 100);\n\t\t});\n\n\t\t// Race between job completion and timeout\n\t\tconst timeout = new Promise<'timeout'>((resolve) => {\n\t\t\tsetTimeout(() => resolve('timeout'), this.options.shutdownTimeout);\n\t\t});\n\n\t\tlet result: undefined | 'timeout';\n\n\t\ttry {\n\t\t\tresult = await Promise.race([waitForJobs, timeout]);\n\t\t} finally {\n\t\t\tif (checkInterval) {\n\t\t\t\tclearInterval(checkInterval);\n\t\t\t}\n\t\t}\n\n\t\tif (result === 'timeout') {\n\t\t\tconst incompleteJobs = this.getActiveJobsList();\n\n\t\t\tconst error = new ShutdownTimeoutError(\n\t\t\t\t`Shutdown timed out after ${this.options.shutdownTimeout}ms with ${incompleteJobs.length} incomplete jobs`,\n\t\t\t\tincompleteJobs,\n\t\t\t);\n\t\t\tthis.emit('job:error', { error });\n\t\t}\n\t}\n\n\t/**\n\t * Check if the scheduler is healthy (running and connected).\n\t *\n\t * Returns `true` when the scheduler is started, initialized, and has an active\n\t * MongoDB collection reference. Useful for health check endpoints and monitoring.\n\t *\n\t * A healthy scheduler:\n\t * - Has called `initialize()` successfully\n\t * - Has called `start()` and is actively polling\n\t * - Has a valid MongoDB collection reference\n\t *\n\t * @returns `true` if scheduler is running and connected, `false` otherwise\n\t *\n\t * @example Express health check endpoint\n\t * ```typescript\n\t * app.get('/health', (req, res) => {\n\t * const healthy = monque.isHealthy();\n\t * res.status(healthy ? 200 : 503).json({\n\t * status: healthy ? 'ok' : 'unavailable',\n\t * scheduler: healthy,\n\t * timestamp: new Date().toISOString()\n\t * });\n\t * });\n\t * ```\n\t *\n\t * @example Kubernetes readiness probe\n\t * ```typescript\n\t * app.get('/readyz', (req, res) => {\n\t * if (monque.isHealthy() && dbConnected) {\n\t * res.status(200).send('ready');\n\t * } else {\n\t * res.status(503).send('not ready');\n\t * }\n\t * });\n\t * ```\n\t *\n\t * @example Periodic health monitoring\n\t * ```typescript\n\t * setInterval(() => {\n\t * if (!monque.isHealthy()) {\n\t * logger.error('Scheduler unhealthy');\n\t * metrics.increment('scheduler.unhealthy');\n\t * }\n\t * }, 60000); // Check every minute\n\t * ```\n\t */\n\tisHealthy(): boolean {\n\t\treturn this.isRunning && this.isInitialized && this.collection !== null;\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Private Helpers\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Ensure the scheduler is initialized before operations.\n\t *\n\t * @private\n\t * @throws {ConnectionError} If scheduler not initialized or collection unavailable\n\t */\n\tprivate ensureInitialized(): void {\n\t\tif (!this.isInitialized || !this.collection) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\t}\n\n\t/**\n\t * Get array of active job IDs across all workers.\n\t *\n\t * @private\n\t * @returns Array of job ID strings currently being processed\n\t */\n\tprivate getActiveJobs(): string[] {\n\t\tconst activeJobs: string[] = [];\n\t\tfor (const worker of this.workers.values()) {\n\t\t\tactiveJobs.push(...worker.activeJobs.keys());\n\t\t}\n\t\treturn activeJobs;\n\t}\n\n\t/**\n\t * Get list of active job documents (for shutdown timeout error).\n\t *\n\t * @private\n\t * @returns Array of active Job objects\n\t */\n\tprivate getActiveJobsList(): Job[] {\n\t\tconst activeJobs: Job[] = [];\n\t\tfor (const worker of this.workers.values()) {\n\t\t\tactiveJobs.push(...worker.activeJobs.values());\n\t\t}\n\t\treturn activeJobs;\n\t}\n\n\t/**\n\t * Type-safe event emitter methods\n\t */\n\toverride emit<K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]): boolean {\n\t\treturn super.emit(event, payload);\n\t}\n\n\toverride on<K extends keyof MonqueEventMap>(\n\t\tevent: K,\n\t\tlistener: (payload: MonqueEventMap[K]) => void,\n\t): this {\n\t\treturn super.on(event, listener);\n\t}\n\n\toverride once<K extends keyof MonqueEventMap>(\n\t\tevent: K,\n\t\tlistener: (payload: MonqueEventMap[K]) => void,\n\t): this {\n\t\treturn super.once(event, listener);\n\t}\n\n\toverride off<K extends keyof MonqueEventMap>(\n\t\tevent: K,\n\t\tlistener: (payload: MonqueEventMap[K]) => void,\n\t): this {\n\t\treturn super.off(event, listener);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["CronExpressionParser","ObjectId","ObjectId","BSON","EventEmitter"],"sources":["../src/jobs/document-to-persisted-job.ts","../src/jobs/types.ts","../src/jobs/guards.ts","../src/shared/errors.ts","../src/shared/utils/backoff.ts","../src/shared/utils/cron.ts","../src/shared/utils/error.ts","../src/scheduler/helpers.ts","../src/scheduler/services/change-stream-handler.ts","../src/scheduler/services/job-manager.ts","../src/scheduler/services/job-processor.ts","../src/scheduler/services/job-query.ts","../src/scheduler/services/job-scheduler.ts","../src/scheduler/services/lifecycle-manager.ts","../src/scheduler/monque.ts"],"sourcesContent":["import type { Document, WithId } from 'mongodb';\n\nimport type { PersistedJob } from './types.js';\n\n/**\n * Convert a raw MongoDB document to a strongly-typed {@link PersistedJob}.\n *\n * Maps required fields directly and conditionally includes optional fields\n * only when they are present in the document (`!== undefined`).\n *\n * @internal Not part of the public API.\n * @template T - The job data payload type\n * @param doc - The raw MongoDB document with `_id`\n * @returns A strongly-typed PersistedJob object with guaranteed `_id`\n */\nexport function documentToPersistedJob<T = unknown>(doc: WithId<Document>): PersistedJob<T> {\n\tconst job: PersistedJob<T> = {\n\t\t_id: doc._id,\n\t\tname: doc['name'],\n\t\tdata: doc['data'],\n\t\tstatus: doc['status'],\n\t\tnextRunAt: doc['nextRunAt'],\n\t\tfailCount: doc['failCount'],\n\t\tcreatedAt: doc['createdAt'],\n\t\tupdatedAt: doc['updatedAt'],\n\t};\n\n\t// Only set optional properties if they exist\n\tif (doc['lockedAt'] !== undefined) {\n\t\tjob.lockedAt = doc['lockedAt'];\n\t}\n\tif (doc['claimedBy'] !== undefined) {\n\t\tjob.claimedBy = doc['claimedBy'];\n\t}\n\tif (doc['lastHeartbeat'] !== undefined) {\n\t\tjob.lastHeartbeat = doc['lastHeartbeat'];\n\t}\n\tif (doc['heartbeatInterval'] !== undefined) {\n\t\tjob.heartbeatInterval = doc['heartbeatInterval'];\n\t}\n\tif (doc['failReason'] !== undefined) {\n\t\tjob.failReason = doc['failReason'];\n\t}\n\tif (doc['repeatInterval'] !== undefined) {\n\t\tjob.repeatInterval = doc['repeatInterval'];\n\t}\n\tif (doc['uniqueKey'] !== undefined) {\n\t\tjob.uniqueKey = doc['uniqueKey'];\n\t}\n\n\treturn job;\n}\n","import type { ObjectId } from 'mongodb';\n\n/**\n * Represents the lifecycle states of a job in the queue.\n *\n * Jobs transition through states as follows:\n * - PENDING → PROCESSING (when picked up by a worker)\n * - PROCESSING → COMPLETED (on success)\n * - PROCESSING → PENDING (on failure, if retries remain)\n * - PROCESSING → FAILED (on failure, after max retries exhausted)\n * - PENDING → CANCELLED (on manual cancellation)\n *\n * @example\n * ```typescript\n * if (job.status === JobStatus.PENDING) {\n * // job is waiting to be picked up\n * }\n * ```\n */\nexport const JobStatus = {\n\t/** Job is waiting to be picked up by a worker */\n\tPENDING: 'pending',\n\t/** Job is currently being executed by a worker */\n\tPROCESSING: 'processing',\n\t/** Job completed successfully */\n\tCOMPLETED: 'completed',\n\t/** Job permanently failed after exhausting all retry attempts */\n\tFAILED: 'failed',\n\t/** Job was manually cancelled */\n\tCANCELLED: 'cancelled',\n} as const;\n\n/**\n * Union type of all possible job status values: `'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'`\n */\nexport type JobStatusType = (typeof JobStatus)[keyof typeof JobStatus];\n\n/**\n * Represents a job in the Monque queue.\n *\n * @template T - The type of the job's data payload\n *\n * @example\n * ```typescript\n * interface EmailJobData {\n * to: string;\n * subject: string;\n * template: string;\n * }\n *\n * const job: Job<EmailJobData> = {\n * name: 'send-email',\n * data: { to: 'user@example.com', subject: 'Welcome!', template: 'welcome' },\n * status: JobStatus.PENDING,\n * nextRunAt: new Date(),\n * failCount: 0,\n * createdAt: new Date(),\n * updatedAt: new Date(),\n * };\n * ```\n */\nexport interface Job<T = unknown> {\n\t/** MongoDB document identifier */\n\t_id?: ObjectId;\n\n\t/** Job type identifier, matches worker registration */\n\tname: string;\n\n\t/** Job payload - must be JSON-serializable */\n\tdata: T;\n\n\t/** Current lifecycle state */\n\tstatus: JobStatusType;\n\n\t/** When the job should be processed */\n\tnextRunAt: Date;\n\n\t/** Timestamp when job was locked for processing */\n\tlockedAt?: Date | null;\n\n\t/**\n\t * Unique identifier of the scheduler instance that claimed this job.\n\t * Used for atomic claim pattern - ensures only one instance processes each job.\n\t * Set when a job is claimed, cleared when job completes or fails.\n\t */\n\tclaimedBy?: string | null;\n\n\t/**\n\t * Timestamp of the last heartbeat update for this job.\n\t * Used to detect stale jobs when a scheduler instance crashes without releasing.\n\t * Updated periodically while job is being processed.\n\t */\n\tlastHeartbeat?: Date | null;\n\n\t/**\n\t * Heartbeat interval in milliseconds for this job.\n\t * Stored on the job to allow recovery logic to use the correct timeout.\n\t */\n\theartbeatInterval?: number;\n\n\t/** Number of failed attempts */\n\tfailCount: number;\n\n\t/** Last failure error message */\n\tfailReason?: string;\n\n\t/** Cron expression for recurring jobs */\n\trepeatInterval?: string;\n\n\t/** Deduplication key to prevent duplicate jobs */\n\tuniqueKey?: string;\n\n\t/** Job creation timestamp */\n\tcreatedAt: Date;\n\n\t/** Last modification timestamp */\n\tupdatedAt: Date;\n}\n\n/**\n * A job that has been persisted to MongoDB and has a guaranteed `_id`.\n * This is returned by `enqueue()`, `now()`, and `schedule()` methods.\n *\n * @template T - The type of the job's data payload\n */\nexport type PersistedJob<T = unknown> = Job<T> & { _id: ObjectId };\n\n/**\n * Options for enqueueing a job.\n *\n * @example\n * ```typescript\n * await monque.enqueue('sync-user', { userId: '123' }, {\n * uniqueKey: 'sync-user-123',\n * runAt: new Date(Date.now() + 5000), // Run in 5 seconds\n * });\n * ```\n */\nexport interface EnqueueOptions {\n\t/**\n\t * Deduplication key. If a job with this key is already pending or processing,\n\t * the enqueue operation will not create a duplicate.\n\t */\n\tuniqueKey?: string;\n\n\t/**\n\t * When the job should be processed. Defaults to immediately (new Date()).\n\t */\n\trunAt?: Date;\n}\n\n/**\n * Options for scheduling a recurring job.\n *\n * @example\n * ```typescript\n * await monque. schedule('0 * * * *', 'hourly-cleanup', { dir: '/tmp' }, {\n * uniqueKey: 'hourly-cleanup-job',\n * });\n * ```\n */\nexport interface ScheduleOptions {\n\t/**\n\t * Deduplication key. If a job with this key is already pending or processing,\n\t * the schedule operation will not create a duplicate.\n\t */\n\tuniqueKey?: string;\n}\n\n/**\n * Filter options for querying jobs.\n *\n * Use with `monque.getJobs()` to filter jobs by name, status, or limit results.\n *\n * @example\n * ```typescript\n * // Get all pending email jobs\n * const pendingEmails = await monque.getJobs({\n * name: 'send-email',\n * status: JobStatus.PENDING,\n * });\n *\n * // Get all failed or completed jobs (paginated)\n * const finishedJobs = await monque.getJobs({\n * status: [JobStatus.COMPLETED, JobStatus.FAILED],\n * limit: 50,\n * skip: 100,\n * });\n * ```\n */\nexport interface GetJobsFilter {\n\t/** Filter by job type name */\n\tname?: string;\n\n\t/** Filter by status (single or multiple) */\n\tstatus?: JobStatusType | JobStatusType[];\n\n\t/** Maximum number of jobs to return (default: 100) */\n\tlimit?: number;\n\n\t/** Number of jobs to skip for pagination */\n\tskip?: number;\n}\n\n/**\n * Handler function signature for processing jobs.\n *\n * @template T - The type of the job's data payload\n *\n * @example\n * ```typescript\n * const emailHandler: JobHandler<EmailJobData> = async (job) => {\n * await sendEmail(job.data.to, job.data.subject);\n * };\n * ```\n */\nexport type JobHandler<T = unknown> = (job: Job<T>) => Promise<void> | void;\n\n/**\n * Valid cursor directions for pagination.\n *\n * @example\n * ```typescript\n * const direction = CursorDirection.FORWARD;\n * ```\n */\nexport const CursorDirection = {\n\tFORWARD: 'forward',\n\tBACKWARD: 'backward',\n} as const;\n\nexport type CursorDirectionType = (typeof CursorDirection)[keyof typeof CursorDirection];\n\n/**\n * Selector options for bulk operations.\n *\n * Used to select multiple jobs for operations like cancellation or deletion.\n *\n * @example\n * ```typescript\n * // Select all failed jobs older than 7 days\n * const selector: JobSelector = {\n * status: JobStatus.FAILED,\n * olderThan: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),\n * };\n * ```\n */\nexport interface JobSelector {\n\tname?: string;\n\tstatus?: JobStatusType | JobStatusType[];\n\tolderThan?: Date;\n\tnewerThan?: Date;\n}\n\n/**\n * Options for cursor-based pagination.\n *\n * @example\n * ```typescript\n * const options: CursorOptions = {\n * limit: 50,\n * direction: CursorDirection.FORWARD,\n * filter: { status: JobStatus.PENDING },\n * };\n * ```\n */\nexport interface CursorOptions {\n\tcursor?: string;\n\tlimit?: number;\n\tdirection?: CursorDirectionType;\n\tfilter?: Pick<GetJobsFilter, 'name' | 'status'>;\n}\n\n/**\n * Response structure for cursor-based pagination.\n *\n * @template T - The type of the job's data payload\n *\n * @example\n * ```typescript\n * const page = await monque.listJobs({ limit: 10 });\n * console.log(`Got ${page.jobs.length} jobs`);\n *\n * if (page.hasNextPage) {\n * console.log(`Next cursor: ${page.cursor}`);\n * }\n * ```\n */\nexport interface CursorPage<T = unknown> {\n\tjobs: PersistedJob<T>[];\n\tcursor: string | null;\n\thasNextPage: boolean;\n\thasPreviousPage: boolean;\n}\n\n/**\n * Aggregated statistics for the job queue.\n *\n * @example\n * ```typescript\n * const stats = await monque.getQueueStats();\n * console.log(`Total jobs: ${stats.total}`);\n * console.log(`Pending: ${stats.pending}`);\n * console.log(`Processing: ${stats.processing}`);\n * console.log(`Failed: ${stats.failed}`);\n * console.log(`Start to finish avg: ${stats.avgProcessingDurationMs}ms`);\n * ```\n */\nexport interface QueueStats {\n\tpending: number;\n\tprocessing: number;\n\tcompleted: number;\n\tfailed: number;\n\tcancelled: number;\n\ttotal: number;\n\tavgProcessingDurationMs?: number;\n}\n\n/**\n * Result of a bulk operation.\n *\n * @example\n * ```typescript\n * const result = await monque.cancelJobs(selector);\n * console.log(`Cancelled ${result.count} jobs`);\n *\n * if (result.errors.length > 0) {\n * console.warn('Some jobs could not be cancelled:', result.errors);\n * }\n * ```\n */\nexport interface BulkOperationResult {\n\tcount: number;\n\terrors: Array<{ jobId: string; error: string }>;\n}\n","import type { Job, JobStatusType, PersistedJob } from './types.js';\nimport { JobStatus } from './types.js';\n\n/**\n * Type guard to check if a job has been persisted to MongoDB.\n *\n * A persisted job is guaranteed to have an `_id` field, which means it has been\n * successfully inserted into the database. This is useful when you need to ensure\n * a job can be updated or referenced by its ID.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job has a valid `_id`, narrowing the type to `PersistedJob<T>`\n *\n * @example Basic usage\n * ```typescript\n * const job: Job<EmailData> = await monque.enqueue('send-email', emailData);\n *\n * if (isPersistedJob(job)) {\n * // TypeScript knows job._id exists\n * console.log(`Job ID: ${job._id.toString()}`);\n * }\n * ```\n *\n * @example In a conditional\n * ```typescript\n * function logJobId(job: Job) {\n * if (!isPersistedJob(job)) {\n * console.log('Job not yet persisted');\n * return;\n * }\n * // TypeScript knows job is PersistedJob here\n * console.log(`Processing job ${job._id.toString()}`);\n * }\n * ```\n */\nexport function isPersistedJob<T>(job: Job<T>): job is PersistedJob<T> {\n\treturn '_id' in job && job._id !== undefined && job._id !== null;\n}\n\n/**\n * Type guard to check if a value is a valid job status.\n *\n * Validates that a value is one of the five valid job statuses: `'pending'`,\n * `'processing'`, `'completed'`, `'failed'`, or `'cancelled'`. Useful for runtime validation\n * of user input or external data.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid `JobStatusType`, narrowing the type\n *\n * @example Validating user input\n * ```typescript\n * function filterByStatus(status: string) {\n * if (!isValidJobStatus(status)) {\n * throw new Error(`Invalid status: ${status}`);\n * }\n * // TypeScript knows status is JobStatusType here\n * return db.jobs.find({ status });\n * }\n * ```\n *\n * @example Runtime validation\n * ```typescript\n * const statusFromApi = externalData.status;\n *\n * if (isValidJobStatus(statusFromApi)) {\n * job.status = statusFromApi;\n * } else {\n * job.status = JobStatus.PENDING;\n * }\n * ```\n */\nexport function isValidJobStatus(value: unknown): value is JobStatusType {\n\treturn typeof value === 'string' && Object.values(JobStatus).includes(value as JobStatusType);\n}\n\n/**\n * Type guard to check if a job is in pending status.\n *\n * A convenience helper for checking if a job is waiting to be processed.\n * Equivalent to `job.status === JobStatus.PENDING` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'pending'`\n *\n * @example Filter pending jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const pendingJobs = jobs.filter(isPendingJob);\n * console.log(`${pendingJobs.length} jobs waiting to be processed`);\n * ```\n *\n * @example Conditional logic\n * ```typescript\n * if (isPendingJob(job)) {\n * await monque.now(job.name, job.data);\n * }\n * ```\n */\nexport function isPendingJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.PENDING;\n}\n\n/**\n * Type guard to check if a job is currently being processed.\n *\n * A convenience helper for checking if a job is actively running.\n * Equivalent to `job.status === JobStatus.PROCESSING` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'processing'`\n *\n * @example Monitor active jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const activeJobs = jobs.filter(isProcessingJob);\n * console.log(`${activeJobs.length} jobs currently running`);\n * ```\n */\nexport function isProcessingJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.PROCESSING;\n}\n\n/**\n * Type guard to check if a job has completed successfully.\n *\n * A convenience helper for checking if a job finished without errors.\n * Equivalent to `job.status === JobStatus.COMPLETED` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'completed'`\n *\n * @example Find completed jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const completedJobs = jobs.filter(isCompletedJob);\n * console.log(`${completedJobs.length} jobs completed successfully`);\n * ```\n */\nexport function isCompletedJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.COMPLETED;\n}\n\n/**\n * Type guard to check if a job has permanently failed.\n *\n * A convenience helper for checking if a job exhausted all retries.\n * Equivalent to `job.status === JobStatus.FAILED` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'failed'`\n *\n * @example Handle failed jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const failedJobs = jobs.filter(isFailedJob);\n *\n * for (const job of failedJobs) {\n * console.error(`Job ${job.name} failed: ${job.failReason}`);\n * await sendAlert(job);\n * }\n * ```\n */\nexport function isFailedJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.FAILED;\n}\n\n/**\n * Type guard to check if a job has been manually cancelled.\n *\n * A convenience helper for checking if a job was cancelled by an operator.\n * Equivalent to `job.status === JobStatus.CANCELLED` but with better semantics.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job status is `'cancelled'`\n *\n * @example Filter cancelled jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const cancelledJobs = jobs.filter(isCancelledJob);\n * console.log(`${cancelledJobs.length} jobs were cancelled`);\n * ```\n */\nexport function isCancelledJob<T>(job: Job<T>): boolean {\n\treturn job.status === JobStatus.CANCELLED;\n}\n\n/**\n * Type guard to check if a job is a recurring scheduled job.\n *\n * A recurring job has a `repeatInterval` cron expression and will be automatically\n * rescheduled after each successful completion.\n *\n * @template T - The type of the job's data payload\n * @param job - The job to check\n * @returns `true` if the job has a `repeatInterval` defined\n *\n * @example Filter recurring jobs\n * ```typescript\n * const jobs = await monque.getJobs();\n * const recurringJobs = jobs.filter(isRecurringJob);\n * console.log(`${recurringJobs.length} jobs will repeat automatically`);\n * ```\n *\n * @example Conditional cleanup\n * ```typescript\n * if (!isRecurringJob(job) && isCompletedJob(job)) {\n * // Safe to delete one-time completed jobs\n * await deleteJob(job._id);\n * }\n * ```\n */\nexport function isRecurringJob<T>(job: Job<T>): boolean {\n\treturn job.repeatInterval !== undefined && job.repeatInterval !== null;\n}\n","import type { Job } from '@/jobs';\n\n/**\n * Base error class for all Monque-related errors.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof MonqueError) {\n * console.error('Monque error:', error.message);\n * }\n * }\n * ```\n */\nexport class MonqueError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'MonqueError';\n\t\t// Maintains proper stack trace for where our error was thrown (only available on V8)\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, MonqueError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when an invalid cron expression is provided.\n *\n * @example\n * ```typescript\n * try {\n * await monque.schedule('invalid cron', 'job', data);\n * } catch (error) {\n * if (error instanceof InvalidCronError) {\n * console.error('Invalid expression:', error.expression);\n * }\n * }\n * ```\n */\nexport class InvalidCronError extends MonqueError {\n\tconstructor(\n\t\tpublic readonly expression: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'InvalidCronError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, InvalidCronError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when there's a database connection issue.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof ConnectionError) {\n * console.error('Database connection lost');\n * }\n * }\n * ```\n */\nexport class ConnectionError extends MonqueError {\n\tconstructor(message: string, options?: { cause?: Error }) {\n\t\tsuper(message);\n\t\tthis.name = 'ConnectionError';\n\t\tif (options?.cause) {\n\t\t\tthis.cause = options.cause;\n\t\t}\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ConnectionError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when graceful shutdown times out.\n * Includes information about jobs that were still in progress.\n *\n * @example\n * ```typescript\n * try {\n * await monque.stop();\n * } catch (error) {\n * if (error instanceof ShutdownTimeoutError) {\n * console.error('Incomplete jobs:', error.incompleteJobs.length);\n * }\n * }\n * ```\n */\nexport class ShutdownTimeoutError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly incompleteJobs: Job[],\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'ShutdownTimeoutError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ShutdownTimeoutError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when attempting to register a worker for a job name\n * that already has a registered worker, without explicitly allowing replacement.\n *\n * @example\n * ```typescript\n * try {\n * monque.register('send-email', handler1);\n * monque.register('send-email', handler2); // throws\n * } catch (error) {\n * if (error instanceof WorkerRegistrationError) {\n * console.error('Worker already registered for:', error.jobName);\n * }\n * }\n *\n * // To intentionally replace a worker:\n * monque.register('send-email', handler2, { replace: true });\n * ```\n */\nexport class WorkerRegistrationError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly jobName: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'WorkerRegistrationError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, WorkerRegistrationError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a state transition is invalid.\n *\n * @example\n * ```typescript\n * try {\n * await monque.cancelJob(jobId);\n * } catch (error) {\n * if (error instanceof JobStateError) {\n * console.error(`Cannot cancel job in state: ${error.currentStatus}`);\n * }\n * }\n * ```\n */\nexport class JobStateError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly jobId: string,\n\t\tpublic readonly currentStatus: string,\n\t\tpublic readonly attemptedAction: 'cancel' | 'retry' | 'reschedule',\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'JobStateError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, JobStateError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a pagination cursor is invalid or malformed.\n *\n * @example\n * ```typescript\n * try {\n * await monque.listJobs({ cursor: 'invalid-cursor' });\n * } catch (error) {\n * if (error instanceof InvalidCursorError) {\n * console.error('Invalid cursor provided');\n * }\n * }\n * ```\n */\nexport class InvalidCursorError extends MonqueError {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'InvalidCursorError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, InvalidCursorError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a statistics aggregation times out.\n *\n * @example\n * ```typescript\n * try {\n * const stats = await monque.getQueueStats();\n * } catch (error) {\n * if (error instanceof AggregationTimeoutError) {\n * console.error('Stats took too long to calculate');\n * }\n * }\n * ```\n */\nexport class AggregationTimeoutError extends MonqueError {\n\tconstructor(message: string = 'Statistics aggregation exceeded 30 second timeout') {\n\t\tsuper(message);\n\t\tthis.name = 'AggregationTimeoutError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, AggregationTimeoutError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when a job payload exceeds the configured maximum BSON byte size.\n *\n * @example\n * ```typescript\n * const monque = new Monque(db, { maxPayloadSize: 1_000_000 }); // 1 MB\n *\n * try {\n * await monque.enqueue('job', hugePayload);\n * } catch (error) {\n * if (error instanceof PayloadTooLargeError) {\n * console.error(`Payload ${error.actualSize} bytes exceeds limit ${error.maxSize} bytes`);\n * }\n * }\n * ```\n */\nexport class PayloadTooLargeError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly actualSize: number,\n\t\tpublic readonly maxSize: number,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'PayloadTooLargeError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, PayloadTooLargeError);\n\t\t}\n\t}\n}\n","/**\n * Default base interval for exponential backoff in milliseconds.\n * @default 1000\n */\nexport const DEFAULT_BASE_INTERVAL = 1000;\n\n/**\n * Default maximum delay cap for exponential backoff in milliseconds.\n *\n * This prevents unbounded delays (e.g. failCount=20 is >11 days at 1s base)\n * and avoids precision/overflow issues for very large fail counts.\n * @default 86400000 (24 hours)\n */\nexport const DEFAULT_MAX_BACKOFF_DELAY = 24 * 60 * 60 * 1_000;\n\n/**\n * Calculate the next run time using exponential backoff.\n *\n * Formula: nextRunAt = now + (2^failCount × baseInterval)\n *\n * @param failCount - Number of previous failed attempts\n * @param baseInterval - Base interval in milliseconds (default: 1000ms)\n * @param maxDelay - Maximum delay in milliseconds (optional)\n * @returns The next run date\n *\n * @example\n * ```typescript\n * // First retry (failCount=1): 2^1 * 1000 = 2000ms delay\n * const nextRun = calculateBackoff(1);\n *\n * // Second retry (failCount=2): 2^2 * 1000 = 4000ms delay\n * const nextRun = calculateBackoff(2);\n *\n * // With custom base interval\n * const nextRun = calculateBackoff(3, 500); // 2^3 * 500 = 4000ms delay\n *\n * // With max delay\n * const nextRun = calculateBackoff(10, 1000, 60000); // capped at 60000ms\n * ```\n */\nexport function calculateBackoff(\n\tfailCount: number,\n\tbaseInterval: number = DEFAULT_BASE_INTERVAL,\n\tmaxDelay?: number,\n): Date {\n\tconst effectiveMaxDelay = maxDelay ?? DEFAULT_MAX_BACKOFF_DELAY;\n\tlet delay = 2 ** failCount * baseInterval;\n\n\tif (delay > effectiveMaxDelay) {\n\t\tdelay = effectiveMaxDelay;\n\t}\n\n\treturn new Date(Date.now() + delay);\n}\n\n/**\n * Calculate just the delay in milliseconds for a given fail count.\n *\n * @param failCount - Number of previous failed attempts\n * @param baseInterval - Base interval in milliseconds (default: 1000ms)\n * @param maxDelay - Maximum delay in milliseconds (optional)\n * @returns The delay in milliseconds\n */\nexport function calculateBackoffDelay(\n\tfailCount: number,\n\tbaseInterval: number = DEFAULT_BASE_INTERVAL,\n\tmaxDelay?: number,\n): number {\n\tconst effectiveMaxDelay = maxDelay ?? DEFAULT_MAX_BACKOFF_DELAY;\n\tlet delay = 2 ** failCount * baseInterval;\n\n\tif (delay > effectiveMaxDelay) {\n\t\tdelay = effectiveMaxDelay;\n\t}\n\n\treturn delay;\n}\n","import { CronExpressionParser } from 'cron-parser';\n\nimport { InvalidCronError } from '../errors.js';\n\n/**\n * Parse a cron expression and return the next scheduled run date.\n *\n * @param expression - A 5-field cron expression (minute hour day-of-month month day-of-week) or a predefined expression\n * @param currentDate - The reference date for calculating next run (default: now)\n * @returns The next scheduled run date\n * @throws {InvalidCronError} If the cron expression is invalid\n *\n * @example\n * ```typescript\n * // Every minute\n * const nextRun = getNextCronDate('* * * * *');\n *\n * // Every day at midnight\n * const nextRun = getNextCronDate('0 0 * * *');\n *\n * // Using predefined expression\n * const nextRun = getNextCronDate('@daily');\n *\n * // Every Monday at 9am\n * const nextRun = getNextCronDate('0 9 * * 1');\n * ```\n */\nexport function getNextCronDate(expression: string, currentDate?: Date): Date {\n\ttry {\n\t\tconst interval = CronExpressionParser.parse(expression, {\n\t\t\tcurrentDate: currentDate ?? new Date(),\n\t\t});\n\t\treturn interval.next().toDate();\n\t} catch (error) {\n\t\thandleCronParseError(expression, error);\n\t}\n}\n\n/**\n * Validate a cron expression without calculating the next run date.\n *\n * @param expression - A 5-field cron expression\n * @throws {InvalidCronError} If the cron expression is invalid\n *\n * @example\n * ```typescript\n * validateCronExpression('0 9 * * 1'); // Throws if invalid\n * ```\n */\nexport function validateCronExpression(expression: string): void {\n\ttry {\n\t\tCronExpressionParser.parse(expression);\n\t} catch (error) {\n\t\thandleCronParseError(expression, error);\n\t}\n}\n\nfunction handleCronParseError(expression: string, error: unknown): never {\n\t/* istanbul ignore next -- @preserve cron-parser always throws Error objects */\n\tconst errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';\n\tthrow new InvalidCronError(\n\t\texpression,\n\t\t`Invalid cron expression \"${expression}\": ${errorMessage}. ` +\n\t\t\t'Expected 5-field format: \"minute hour day-of-month month day-of-week\" or predefined expression (e.g. @daily). ' +\n\t\t\t'Example: \"0 9 * * 1\" (every Monday at 9am)',\n\t);\n}\n","/**\n * Normalize an unknown caught value into a proper `Error` instance.\n *\n * In JavaScript, any value can be thrown — strings, numbers, objects, `undefined`, etc.\n * This function ensures we always have a real `Error` with a proper stack trace and message.\n *\n * @param value - The caught value (typically from a `catch` block typed as `unknown`).\n * @returns The original value if already an `Error`, otherwise a new `Error` wrapping `String(value)`.\n *\n * @example\n * ```ts\n * try {\n * riskyOperation();\n * } catch (error: unknown) {\n * const normalized = toError(error);\n * console.error(normalized.message);\n * }\n * ```\n *\n * @internal\n */\nexport function toError(value: unknown): Error {\n\tif (value instanceof Error) return value;\n\n\ttry {\n\t\treturn new Error(String(value));\n\t} catch (conversionError: unknown) {\n\t\tconst detail =\n\t\t\tconversionError instanceof Error ? conversionError.message : 'unknown conversion failure';\n\n\t\treturn new Error(`Unserializable value (${detail})`);\n\t}\n}\n","import { type Document, type Filter, ObjectId } from 'mongodb';\n\nimport { CursorDirection, type CursorDirectionType, type JobSelector } from '@/jobs';\nimport { InvalidCursorError } from '@/shared';\n\n/**\n * Build a MongoDB query filter from a JobSelector.\n *\n * Translates the high-level `JobSelector` interface into a MongoDB `Filter<Document>`.\n * Handles array values for status (using `$in`) and date range filtering.\n *\n * @param filter - The user-provided job selector\n * @returns A standard MongoDB filter object\n */\nexport function buildSelectorQuery(filter: JobSelector): Filter<Document> {\n\tconst query: Filter<Document> = {};\n\n\tif (filter.name) {\n\t\tquery['name'] = filter.name;\n\t}\n\n\tif (filter.status) {\n\t\tif (Array.isArray(filter.status)) {\n\t\t\tquery['status'] = { $in: filter.status };\n\t\t} else {\n\t\t\tquery['status'] = filter.status;\n\t\t}\n\t}\n\n\tif (filter.olderThan || filter.newerThan) {\n\t\tquery['createdAt'] = {};\n\t\tif (filter.olderThan) {\n\t\t\tquery['createdAt'].$lt = filter.olderThan;\n\t\t}\n\t\tif (filter.newerThan) {\n\t\t\tquery['createdAt'].$gt = filter.newerThan;\n\t\t}\n\t}\n\n\treturn query;\n}\n\n/**\n * Encode an ObjectId and direction into an opaque cursor string.\n *\n * Format: `prefix` + `base64url(objectId)`\n * Prefix: 'F' (forward) or 'B' (backward)\n *\n * @param id - The job ID to use as the cursor anchor (exclusive)\n * @param direction - 'forward' or 'backward'\n * @returns Base64url-encoded cursor string\n */\nexport function encodeCursor(id: ObjectId, direction: CursorDirectionType): string {\n\tconst prefix = direction === 'forward' ? 'F' : 'B';\n\tconst buffer = Buffer.from(id.toHexString(), 'hex');\n\n\treturn prefix + buffer.toString('base64url');\n}\n\n/**\n * Decode an opaque cursor string into an ObjectId and direction.\n *\n * Validates format and returns the components.\n *\n * @param cursor - The opaque cursor string\n * @returns The decoded ID and direction\n * @throws {InvalidCursorError} If the cursor format is invalid or ID is malformed\n */\nexport function decodeCursor(cursor: string): {\n\tid: ObjectId;\n\tdirection: CursorDirectionType;\n} {\n\tif (!cursor || cursor.length < 2) {\n\t\tthrow new InvalidCursorError('Cursor is empty or too short');\n\t}\n\n\tconst prefix = cursor.charAt(0);\n\tconst payload = cursor.slice(1);\n\n\tlet direction: CursorDirectionType;\n\n\tif (prefix === 'F') {\n\t\tdirection = CursorDirection.FORWARD;\n\t} else if (prefix === 'B') {\n\t\tdirection = CursorDirection.BACKWARD;\n\t} else {\n\t\tthrow new InvalidCursorError(`Invalid cursor prefix: ${prefix}`);\n\t}\n\n\ttry {\n\t\tconst buffer = Buffer.from(payload, 'base64url');\n\t\tconst hex = buffer.toString('hex');\n\t\t// standard ObjectID is 12 bytes = 24 hex chars\n\t\tif (hex.length !== 24) {\n\t\t\tthrow new InvalidCursorError('Invalid length');\n\t\t}\n\n\t\tconst id = new ObjectId(hex);\n\n\t\treturn { id, direction };\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\tthrow error;\n\t\t}\n\t\tthrow new InvalidCursorError('Invalid cursor payload');\n\t}\n}\n","import type { ChangeStream, ChangeStreamDocument, Document } from 'mongodb';\n\nimport { JobStatus } from '@/jobs';\nimport { toError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/** Minimum poll interval floor to prevent tight loops (ms) */\nconst MIN_POLL_INTERVAL = 100;\n\n/** Grace period after nextRunAt before scheduling a wakeup poll (ms) */\nconst POLL_GRACE_PERIOD = 200;\n\n/**\n * Internal service for MongoDB Change Stream lifecycle.\n *\n * Provides real-time job notifications when available, with automatic\n * reconnection and graceful fallback to polling-only mode.\n *\n * Leverages the full document from change stream events to:\n * - Trigger **targeted polls** for specific workers (using the job `name`)\n * - Schedule **precise wakeup timers** for future-dated jobs (using `nextRunAt`)\n *\n * @internal Not part of public API.\n */\nexport class ChangeStreamHandler {\n\t/** MongoDB Change Stream for real-time job notifications */\n\tprivate changeStream: ChangeStream | null = null;\n\n\t/** Number of consecutive reconnection attempts */\n\tprivate reconnectAttempts = 0;\n\n\t/** Maximum reconnection attempts before falling back to polling-only mode */\n\tprivate readonly maxReconnectAttempts = 3;\n\n\t/** Debounce timer for change stream event processing */\n\tprivate debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n\t/** Timer ID for reconnection with exponential backoff */\n\tprivate reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n\t/** Whether the scheduler is currently using change streams */\n\tprivate usingChangeStreams = false;\n\n\t/** Job names collected during the current debounce window for targeted polling */\n\tprivate pendingTargetNames: Set<string> = new Set();\n\n\t/** Wakeup timer for the earliest known future job */\n\tprivate wakeupTimer: ReturnType<typeof setTimeout> | null = null;\n\n\t/** Time of the currently scheduled wakeup */\n\tprivate wakeupTime: Date | null = null;\n\n\tconstructor(\n\t\tprivate readonly ctx: SchedulerContext,\n\t\tprivate readonly onPoll: (targetNames?: ReadonlySet<string>) => Promise<void>,\n\t) {}\n\n\t/**\n\t * Set up MongoDB Change Stream for real-time job notifications.\n\t *\n\t * Change streams provide instant notifications when jobs are inserted or when\n\t * job status changes to pending (e.g., after a retry). This eliminates the\n\t * polling delay for reactive job processing.\n\t *\n\t * The change stream watches for:\n\t * - Insert operations (new jobs)\n\t * - Update operations where status field changes\n\t *\n\t * If change streams are unavailable (e.g., standalone MongoDB), the system\n\t * gracefully falls back to polling-only mode.\n\t */\n\tsetup(): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.clearReconnectTimer();\n\n\t\ttry {\n\t\t\t// Create change stream with pipeline to filter relevant events\n\t\t\tconst pipeline = [\n\t\t\t\t{\n\t\t\t\t\t$match: {\n\t\t\t\t\t\t$or: [\n\t\t\t\t\t\t\t{ operationType: 'insert' },\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\toperationType: 'update',\n\t\t\t\t\t\t\t\t$or: [\n\t\t\t\t\t\t\t\t\t{ 'updateDescription.updatedFields.status': { $exists: true } },\n\t\t\t\t\t\t\t\t\t{ 'updateDescription.updatedFields.nextRunAt': { $exists: true } },\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t];\n\n\t\t\tthis.changeStream = this.ctx.collection.watch(pipeline, {\n\t\t\t\tfullDocument: 'updateLookup',\n\t\t\t});\n\n\t\t\t// Handle change events\n\t\t\tthis.changeStream.on('change', (change) => {\n\t\t\t\tthis.handleEvent(change);\n\t\t\t});\n\n\t\t\t// Handle errors with reconnection\n\t\t\tthis.changeStream.on('error', (error: Error) => {\n\t\t\t\tthis.ctx.emit('changestream:error', { error });\n\t\t\t\tthis.handleError(error);\n\t\t\t});\n\n\t\t\t// Mark as connected\n\t\t\tthis.usingChangeStreams = true;\n\t\t\tthis.reconnectAttempts = 0;\n\t\t\tthis.ctx.emit('changestream:connected', undefined);\n\t\t} catch (error) {\n\t\t\t// Change streams not available (e.g., standalone MongoDB)\n\t\t\tthis.usingChangeStreams = false;\n\t\t\tconst reason = error instanceof Error ? error.message : 'Unknown error';\n\t\t\tthis.ctx.emit('changestream:fallback', { reason });\n\t\t}\n\t}\n\n\t/**\n\t * Handle a change stream event using the full document for intelligent routing.\n\t *\n\t * For **immediate jobs** (`nextRunAt <= now`): collects the job name and triggers\n\t * a debounced targeted poll for only the relevant workers.\n\t *\n\t * For **future jobs** (`nextRunAt > now`): schedules a precise wakeup timer so\n\t * the job is picked up near its scheduled time without blind polling.\n\t *\n\t * For **completed/failed jobs** (slot freed): triggers a targeted re-poll for that\n\t * worker so the next pending job is picked up immediately, maintaining continuous\n\t * throughput without waiting for the safety poll interval.\n\t *\n\t * Falls back to a full poll (no target names) if the document is missing\n\t * required fields.\n\t *\n\t * @param change - The change stream event document\n\t */\n\thandleEvent(change: ChangeStreamDocument<Document>): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Trigger poll on insert (new job) or update where status changes\n\t\tconst isInsert = change.operationType === 'insert';\n\t\tconst isUpdate = change.operationType === 'update';\n\n\t\t// Get fullDocument if available (for insert or with updateLookup option)\n\t\tconst fullDocument = 'fullDocument' in change ? change.fullDocument : undefined;\n\t\tconst currentStatus = fullDocument?.['status'] as string | undefined;\n\t\tconst isPendingStatus = currentStatus === JobStatus.PENDING;\n\n\t\t// A completed/failed status change means a concurrency slot was freed.\n\t\t// Trigger a re-poll so the next pending job is picked up immediately,\n\t\t// rather than waiting for the safety poll interval.\n\t\tconst isSlotFreed =\n\t\t\tisUpdate && (currentStatus === JobStatus.COMPLETED || currentStatus === JobStatus.FAILED);\n\n\t\t// For inserts: always trigger since new pending jobs need processing\n\t\t// For updates to pending: trigger (retry/release/recurring reschedule)\n\t\t// For updates to completed/failed: trigger (concurrency slot freed)\n\t\tconst shouldTrigger = isInsert || (isUpdate && isPendingStatus) || isSlotFreed;\n\n\t\tif (!shouldTrigger) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Slot-freed events: targeted poll for that worker to pick up waiting jobs\n\t\tif (isSlotFreed) {\n\t\t\tconst jobName = fullDocument?.['name'] as string | undefined;\n\t\t\tif (jobName) {\n\t\t\t\tthis.pendingTargetNames.add(jobName);\n\t\t\t}\n\t\t\tthis.debouncedPoll();\n\t\t\treturn;\n\t\t}\n\n\t\t// Extract job metadata from the full document for smart routing\n\t\tconst jobName = fullDocument?.['name'] as string | undefined;\n\t\tconst nextRunAt = fullDocument?.['nextRunAt'] as Date | undefined;\n\n\t\tif (jobName && nextRunAt) {\n\t\t\tthis.notifyPendingJob(jobName, nextRunAt);\n\t\t\treturn;\n\t\t}\n\n\t\t// Immediate job or missing metadata — collect for targeted/full poll\n\t\tif (jobName) {\n\t\t\tthis.pendingTargetNames.add(jobName);\n\t\t}\n\t\tthis.debouncedPoll();\n\t}\n\n\t/**\n\t * Notify the handler about a pending job created or updated by this process.\n\t *\n\t * Reuses the same routing logic as change stream events so local writes don't\n\t * depend on the MongoDB change stream cursor already being fully ready.\n\t *\n\t * @param jobName - Worker name for targeted polling\n\t * @param nextRunAt - When the job becomes eligible for processing\n\t */\n\tnotifyPendingJob(jobName: string, nextRunAt: Date): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (nextRunAt.getTime() > Date.now()) {\n\t\t\tthis.scheduleWakeup(nextRunAt);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.pendingTargetNames.add(jobName);\n\t\tthis.debouncedPoll();\n\t}\n\n\t/**\n\t * Schedule a debounced poll with collected target names.\n\t *\n\t * Collects job names from multiple change stream events during the debounce\n\t * window, then triggers a single targeted poll for only those workers.\n\t */\n\tprivate debouncedPoll(): void {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer);\n\t\t}\n\n\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\tthis.debounceTimer = null;\n\t\t\tconst names = this.pendingTargetNames.size > 0 ? new Set(this.pendingTargetNames) : undefined;\n\t\t\tthis.pendingTargetNames.clear();\n\t\t\tthis.onPoll(names).catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\t\t}, 100);\n\t}\n\n\t/**\n\t * Schedule a wakeup timer for a future-dated job.\n\t *\n\t * Maintains a single timer set to the earliest known future job's `nextRunAt`.\n\t * When the timer fires, triggers a full poll to pick up all due jobs.\n\t *\n\t * @param nextRunAt - When the future job should become ready\n\t */\n\tprivate scheduleWakeup(nextRunAt: Date): void {\n\t\t// Only update if this job is earlier than the current wakeup\n\t\tif (this.wakeupTime && nextRunAt >= this.wakeupTime) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.clearWakeupTimer();\n\t\tthis.wakeupTime = nextRunAt;\n\n\t\tconst delay = Math.max(nextRunAt.getTime() - Date.now() + POLL_GRACE_PERIOD, MIN_POLL_INTERVAL);\n\n\t\tthis.wakeupTimer = setTimeout(() => {\n\t\t\tthis.wakeupTime = null;\n\t\t\tthis.wakeupTimer = null;\n\t\t\t// Full poll — there may be multiple jobs due at this time\n\t\t\tthis.onPoll().catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\t\t}, delay);\n\t}\n\n\t/**\n\t * Handle change stream errors with exponential backoff reconnection.\n\t *\n\t * Attempts to reconnect up to `maxReconnectAttempts` times with\n\t * exponential backoff (base 1000ms). After exhausting retries, falls back to\n\t * polling-only mode.\n\t *\n\t * @param error - The error that caused the change stream failure\n\t */\n\thandleError(error: Error): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reconnectAttempts++;\n\n\t\t// Immediately reset active state: clears stale debounce/wakeup timers,\n\t\t// closes the broken cursor, and sets isActive() to false so the lifecycle\n\t\t// manager switches to fast polling during backoff.\n\t\tthis.resetActiveState();\n\t\tthis.closeChangeStream();\n\n\t\tif (this.reconnectAttempts > this.maxReconnectAttempts) {\n\t\t\t// Permanent fallback to polling-only mode\n\t\t\tthis.clearReconnectTimer();\n\n\t\t\tthis.ctx.emit('changestream:fallback', {\n\t\t\t\treason: `Exhausted ${this.maxReconnectAttempts} reconnection attempts: ${error.message}`,\n\t\t\t});\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Exponential backoff: 1s, 2s, 4s\n\t\tconst delay = 2 ** (this.reconnectAttempts - 1) * 1000;\n\n\t\t// Clear any existing reconnect timer before scheduling a new one\n\t\tthis.clearReconnectTimer();\n\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reconnectTimer = setTimeout(() => {\n\t\t\tthis.clearReconnectTimer();\n\t\t\tthis.reconnect();\n\t\t}, delay);\n\t}\n\n\tprivate reconnect(): void {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.closeChangeStream();\n\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setup();\n\t}\n\n\tprivate clearReconnectTimer(): void {\n\t\tif (!this.reconnectTimer) {\n\t\t\treturn;\n\t\t}\n\n\t\tclearTimeout(this.reconnectTimer);\n\t\tthis.reconnectTimer = null;\n\t}\n\n\t/**\n\t * Reset all active change stream state: clear debounce timer, wakeup timer,\n\t * pending target names, and mark as inactive.\n\t *\n\t * Does NOT close the cursor (callers handle sync vs async close) or clear\n\t * the reconnect timer/attempts (callers manage reconnection lifecycle).\n\t */\n\tprivate resetActiveState(): void {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer);\n\t\t\tthis.debounceTimer = null;\n\t\t}\n\n\t\tthis.pendingTargetNames.clear();\n\t\tthis.clearWakeupTimer();\n\t\tthis.usingChangeStreams = false;\n\t}\n\n\tprivate clearWakeupTimer(): void {\n\t\tif (this.wakeupTimer) {\n\t\t\tclearTimeout(this.wakeupTimer);\n\t\t\tthis.wakeupTimer = null;\n\t\t}\n\t\tthis.wakeupTime = null;\n\t}\n\n\tprivate closeChangeStream(): void {\n\t\tif (!this.changeStream) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.changeStream.close().catch(() => {});\n\t\tthis.changeStream = null;\n\t}\n\n\t/**\n\t * Close the change stream cursor and emit closed event.\n\t */\n\tasync close(): Promise<void> {\n\t\tconst wasActive = this.usingChangeStreams;\n\n\t\t// Clear all active state (debounce, wakeup, pending names, flag)\n\t\tthis.resetActiveState();\n\t\tthis.clearReconnectTimer();\n\n\t\tif (this.changeStream) {\n\t\t\ttry {\n\t\t\t\tawait this.changeStream.close();\n\t\t\t} catch {\n\t\t\t\t// Ignore close errors during shutdown\n\t\t\t}\n\t\t\tthis.changeStream = null;\n\n\t\t\tif (wasActive) {\n\t\t\t\tthis.ctx.emit('changestream:closed', undefined);\n\t\t\t}\n\t\t}\n\n\t\tthis.reconnectAttempts = 0;\n\t}\n\n\t/**\n\t * Check if change streams are currently active.\n\t */\n\tisActive(): boolean {\n\t\treturn this.usingChangeStreams;\n\t}\n}\n","import { ObjectId } from 'mongodb';\n\nimport { type BulkOperationResult, type JobSelector, JobStatus, type PersistedJob } from '@/jobs';\nimport { buildSelectorQuery } from '@/scheduler';\nimport { ConnectionError, JobStateError, MonqueError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for job lifecycle management operations.\n *\n * Provides atomic state transitions (cancel, retry, reschedule) and deletion.\n * Emits appropriate events on each operation.\n *\n * @internal Not part of public API - use Monque class methods instead.\n */\nexport class JobManager {\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Cancel a pending or scheduled job.\n\t *\n\t * Sets the job status to 'cancelled' and emits a 'job:cancelled' event.\n\t * If the job is already cancelled, this is a no-op and returns the job.\n\t * Cannot cancel jobs that are currently 'processing', 'completed', or 'failed'.\n\t *\n\t * @param jobId - The ID of the job to cancel\n\t * @returns The cancelled job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for cancellation\n\t *\n\t * @example Cancel a pending job\n\t * ```typescript\n\t * const job = await monque.enqueue('report', { type: 'daily' });\n\t * await monque.cancelJob(job._id.toString());\n\t * ```\n\t */\n\tasync cancelJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tif (!ObjectId.isValid(jobId)) return null;\n\n\t\tconst _id = new ObjectId(jobId);\n\n\t\t// Fetch job first to allow emitting the full job object in the event\n\t\tconst jobDoc = await this.ctx.collection.findOne({ _id });\n\t\tif (!jobDoc) return null;\n\n\t\tif (jobDoc['status'] === JobStatus.CANCELLED) {\n\t\t\treturn this.ctx.documentToPersistedJob(jobDoc);\n\t\t}\n\n\t\tif (jobDoc['status'] !== JobStatus.PENDING) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t`Cannot cancel job in status '${jobDoc['status']}'`,\n\t\t\t\tjobId,\n\t\t\t\tjobDoc['status'],\n\t\t\t\t'cancel',\n\t\t\t);\n\t\t}\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id, status: JobStatus.PENDING },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.CANCELLED,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\t// Race condition: job changed state between check and update\n\t\t\tthrow new JobStateError(\n\t\t\t\t'Job status changed during cancellation attempt',\n\t\t\t\tjobId,\n\t\t\t\t'unknown',\n\t\t\t\t'cancel',\n\t\t\t);\n\t\t}\n\n\t\tconst job = this.ctx.documentToPersistedJob(result);\n\t\tthis.ctx.emit('job:cancelled', { job });\n\t\treturn job;\n\t}\n\n\t/**\n\t * Retry a failed or cancelled job.\n\t *\n\t * Resets the job to 'pending' status, clears failure count/reason, and sets\n\t * nextRunAt to now (immediate retry). Emits a 'job:retried' event.\n\t *\n\t * @param jobId - The ID of the job to retry\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for retry (must be failed or cancelled)\n\t *\n\t * @example Retry a failed job\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * console.log(`Job ${job._id} failed, retrying manually...`);\n\t * await monque.retryJob(job._id.toString());\n\t * });\n\t * ```\n\t */\n\tasync retryJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tif (!ObjectId.isValid(jobId)) return null;\n\n\t\tconst _id = new ObjectId(jobId);\n\t\tconst currentJob = await this.ctx.collection.findOne({ _id });\n\n\t\tif (!currentJob) return null;\n\n\t\tif (currentJob['status'] !== JobStatus.FAILED && currentJob['status'] !== JobStatus.CANCELLED) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t`Cannot retry job in status '${currentJob['status']}'`,\n\t\t\t\tjobId,\n\t\t\t\tcurrentJob['status'],\n\t\t\t\t'retry',\n\t\t\t);\n\t\t}\n\n\t\tconst previousStatus = currentJob['status'] as 'failed' | 'cancelled';\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{\n\t\t\t\t_id,\n\t\t\t\tstatus: { $in: [JobStatus.FAILED, JobStatus.CANCELLED] },\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\tfailCount: 0,\n\t\t\t\t\tnextRunAt: new Date(),\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tfailReason: '',\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\tthrow new JobStateError('Job status changed during retry attempt', jobId, 'unknown', 'retry');\n\t\t}\n\n\t\tconst job = this.ctx.documentToPersistedJob(result);\n\t\tthis.ctx.notifyPendingJob(job.name, job.nextRunAt);\n\t\tthis.ctx.emit('job:retried', { job, previousStatus });\n\t\treturn job;\n\t}\n\n\t/**\n\t * Reschedule a pending job to run at a different time.\n\t *\n\t * Only works for jobs in 'pending' status.\n\t *\n\t * @param jobId - The ID of the job to reschedule\n\t * @param runAt - The new Date when the job should run\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is not in pending state\n\t *\n\t * @example Delay a job by 1 hour\n\t * ```typescript\n\t * const nextHour = new Date(Date.now() + 60 * 60 * 1000);\n\t * await monque.rescheduleJob(jobId, nextHour);\n\t * ```\n\t */\n\tasync rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null> {\n\t\tif (!ObjectId.isValid(jobId)) return null;\n\n\t\tconst _id = new ObjectId(jobId);\n\t\tconst currentJobDoc = await this.ctx.collection.findOne({ _id });\n\n\t\tif (!currentJobDoc) return null;\n\n\t\tif (currentJobDoc['status'] !== JobStatus.PENDING) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t`Cannot reschedule job in status '${currentJobDoc['status']}'`,\n\t\t\t\tjobId,\n\t\t\t\tcurrentJobDoc['status'],\n\t\t\t\t'reschedule',\n\t\t\t);\n\t\t}\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id, status: JobStatus.PENDING },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tnextRunAt: runAt,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\tthrow new JobStateError(\n\t\t\t\t'Job status changed during reschedule attempt',\n\t\t\t\tjobId,\n\t\t\t\t'unknown',\n\t\t\t\t'reschedule',\n\t\t\t);\n\t\t}\n\n\t\tconst job = this.ctx.documentToPersistedJob(result);\n\t\tthis.ctx.notifyPendingJob(job.name, job.nextRunAt);\n\t\treturn job;\n\t}\n\n\t/**\n\t * Permanently delete a job.\n\t *\n\t * This action is irreversible. Emits a 'job:deleted' event upon success.\n\t * Can delete a job in any state.\n\t *\n\t * @param jobId - The ID of the job to delete\n\t * @returns true if deleted, false if job not found\n\t *\n\t * @example Delete a cleanup job\n\t * ```typescript\n\t * const deleted = await monque.deleteJob(jobId);\n\t * if (deleted) {\n\t * console.log('Job permanently removed');\n\t * }\n\t * ```\n\t */\n\tasync deleteJob(jobId: string): Promise<boolean> {\n\t\tif (!ObjectId.isValid(jobId)) return false;\n\n\t\tconst _id = new ObjectId(jobId);\n\n\t\tconst result = await this.ctx.collection.deleteOne({ _id });\n\n\t\tif (result.deletedCount > 0) {\n\t\t\tthis.ctx.emit('job:deleted', { jobId });\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Bulk Operations\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Cancel multiple jobs matching the given filter via a single updateMany call.\n\t *\n\t * Only cancels jobs in 'pending' status — the status guard is applied regardless\n\t * of what the filter specifies. Jobs in other states are silently skipped (not\n\t * matched by the query). Emits a 'jobs:cancelled' event with the count of\n\t * successfully cancelled jobs.\n\t *\n\t * @param filter - Selector for which jobs to cancel (name, status, date range)\n\t * @returns Result with count of cancelled jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Cancel all pending jobs for a queue\n\t * ```typescript\n\t * const result = await monque.cancelJobs({\n\t * name: 'email-queue',\n\t * status: 'pending'\n\t * });\n\t * console.log(`Cancelled ${result.count} jobs`);\n\t * ```\n\t */\n\tasync cancelJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tconst query = buildSelectorQuery(filter);\n\n\t\t// Enforce allowed status, but respect explicit status filters\n\t\tif (filter.status !== undefined) {\n\t\t\tconst requested = Array.isArray(filter.status) ? filter.status : [filter.status];\n\t\t\tif (!requested.includes(JobStatus.PENDING)) {\n\t\t\t\treturn { count: 0, errors: [] };\n\t\t\t}\n\t\t}\n\t\tquery['status'] = JobStatus.PENDING;\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.collection.updateMany(query, {\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.CANCELLED,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst count = result.modifiedCount;\n\n\t\t\tif (count > 0) {\n\t\t\t\tthis.ctx.emit('jobs:cancelled', { count });\n\t\t\t}\n\n\t\t\treturn { count, errors: [] };\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during cancelJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to cancel jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.\n\t *\n\t * Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied\n\t * regardless of what the filter specifies. Jobs in other states are silently skipped.\n\t * Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.\n\t * Emits a 'jobs:retried' event with the count of successfully retried jobs.\n\t *\n\t * @param filter - Selector for which jobs to retry (name, status, date range)\n\t * @returns Result with count of retried jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Retry all failed jobs\n\t * ```typescript\n\t * const result = await monque.retryJobs({\n\t * status: 'failed'\n\t * });\n\t * console.log(`Retried ${result.count} jobs`);\n\t * ```\n\t */\n\tasync retryJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tconst query = buildSelectorQuery(filter);\n\n\t\t// Enforce allowed statuses, but respect explicit status filters\n\t\tconst retryable = [JobStatus.FAILED, JobStatus.CANCELLED] as const;\n\t\tif (filter.status !== undefined) {\n\t\t\tconst requested = Array.isArray(filter.status) ? filter.status : [filter.status];\n\t\t\tconst allowed = requested.filter(\n\t\t\t\t(status): status is (typeof retryable)[number] =>\n\t\t\t\t\tstatus === JobStatus.FAILED || status === JobStatus.CANCELLED,\n\t\t\t);\n\t\t\tif (allowed.length === 0) {\n\t\t\t\treturn { count: 0, errors: [] };\n\t\t\t}\n\t\t\tquery['status'] = allowed.length === 1 ? allowed[0] : { $in: allowed };\n\t\t} else {\n\t\t\tquery['status'] = { $in: retryable };\n\t\t}\n\n\t\tconst spreadWindowMs = 30_000; // 30s max spread for staggered retry\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.collection.updateMany(query, [\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\t\tfailCount: 0,\n\t\t\t\t\t\tnextRunAt: {\n\t\t\t\t\t\t\t$add: [new Date(), { $multiply: [{ $rand: {} }, spreadWindowMs] }],\n\t\t\t\t\t\t},\n\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t$unset: ['failReason', 'lockedAt', 'claimedBy', 'lastHeartbeat', 'heartbeatInterval'],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tconst count = result.modifiedCount;\n\n\t\t\tif (count > 0) {\n\t\t\t\tthis.ctx.emit('jobs:retried', { count });\n\t\t\t}\n\n\t\t\treturn { count, errors: [] };\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during retryJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to retry jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Delete multiple jobs matching the given filter.\n\t *\n\t * Deletes jobs in any status. Uses a batch delete for efficiency.\n\t * Emits a 'jobs:deleted' event with the count of deleted jobs.\n\t * Does not emit individual 'job:deleted' events to avoid noise.\n\t *\n\t * @param filter - Selector for which jobs to delete (name, status, date range)\n\t * @returns Result with count of deleted jobs (errors array always empty for delete)\n\t *\n\t * @example Delete old completed jobs\n\t * ```typescript\n\t * const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\t * const result = await monque.deleteJobs({\n\t * status: 'completed',\n\t * olderThan: weekAgo\n\t * });\n\t * console.log(`Deleted ${result.count} jobs`);\n\t * ```\n\t */\n\tasync deleteJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tconst query = buildSelectorQuery(filter);\n\n\t\ttry {\n\t\t\t// Use deleteMany for efficiency\n\t\t\tconst result = await this.ctx.collection.deleteMany(query);\n\n\t\t\tif (result.deletedCount > 0) {\n\t\t\t\tthis.ctx.emit('jobs:deleted', { count: result.deletedCount });\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcount: result.deletedCount,\n\t\t\t\terrors: [],\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during deleteJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to delete jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n","import { isPersistedJob, type Job, JobStatus, type PersistedJob } from '@/jobs';\nimport { calculateBackoff, getNextCronDate, toError } from '@/shared';\nimport type { WorkerRegistration } from '@/workers';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for job processing and execution.\n *\n * Manages the poll loop, atomic job acquisition, handler execution,\n * and job completion/failure with exponential backoff retry logic.\n *\n * @internal Not part of public API.\n */\nexport class JobProcessor {\n\t/** Guard flag to prevent concurrent poll() execution */\n\tprivate _isPolling = false;\n\n\t/** Flag to request a re-poll after the current poll finishes */\n\tprivate _repollRequested = false;\n\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Get the total number of active jobs across all workers.\n\t *\n\t * Used for instance-level throttling when `instanceConcurrency` is configured.\n\t */\n\tprivate getTotalActiveJobs(): number {\n\t\tlet total = 0;\n\t\tfor (const worker of this.ctx.workers.values()) {\n\t\t\ttotal += worker.activeJobs.size;\n\t\t}\n\t\treturn total;\n\t}\n\n\t/**\n\t * Get the number of available slots considering the global instanceConcurrency limit.\n\t *\n\t * @param workerAvailableSlots - Available slots for the specific worker\n\t * @returns Number of slots available after applying global limit\n\t */\n\tprivate getGloballyAvailableSlots(workerAvailableSlots: number): number {\n\t\tconst { instanceConcurrency } = this.ctx.options;\n\n\t\tif (instanceConcurrency === undefined) {\n\t\t\treturn workerAvailableSlots;\n\t\t}\n\n\t\tconst totalActive = this.getTotalActiveJobs();\n\t\tconst globalAvailable = instanceConcurrency - totalActive;\n\n\t\treturn Math.min(workerAvailableSlots, globalAvailable);\n\t}\n\n\t/**\n\t * Poll for available jobs and process them.\n\t *\n\t * Called at regular intervals (configured by `pollInterval`). For each registered worker,\n\t * attempts to acquire jobs up to the worker's available concurrency slots.\n\t * Aborts early if the scheduler is stopping (`isRunning` is false) or if\n\t * the instance-level `instanceConcurrency` limit is reached.\n\t *\n\t * If a poll is requested while one is already running, it is queued and\n\t * executed as a full poll after the current one finishes. This prevents\n\t * change-stream-triggered polls from being silently dropped.\n\t *\n\t * @param targetNames - Optional set of worker names to poll. When provided, only the\n\t * specified workers are checked. Used by change stream handler for targeted polling.\n\t */\n\tasync poll(targetNames?: ReadonlySet<string>): Promise<void> {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this._isPolling) {\n\t\t\t// Queue a re-poll so work discovered during this poll isn't missed\n\t\t\tthis._repollRequested = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis._isPolling = true;\n\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tthis._repollRequested = false;\n\t\t\t\tawait this._doPoll(targetNames);\n\t\t\t\t// Re-polls are always full polls to catch all pending work\n\t\t\t\ttargetNames = undefined;\n\t\t\t} while (this._repollRequested && this.ctx.isRunning());\n\t\t} finally {\n\t\t\tthis._isPolling = false;\n\t\t}\n\t}\n\n\t/**\n\t * Internal poll implementation.\n\t */\n\tprivate async _doPoll(targetNames?: ReadonlySet<string>): Promise<void> {\n\t\t// Early exit if global instanceConcurrency is reached\n\t\tconst { instanceConcurrency } = this.ctx.options;\n\n\t\tif (instanceConcurrency !== undefined && this.getTotalActiveJobs() >= instanceConcurrency) {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const [name, worker] of this.ctx.workers) {\n\t\t\t// Skip workers not in the target set (if provided)\n\t\t\tif (targetNames && !targetNames.has(name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Check if worker has capacity\n\t\t\tconst workerAvailableSlots = worker.concurrency - worker.activeJobs.size;\n\n\t\t\tif (workerAvailableSlots <= 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Apply global concurrency limit\n\t\t\tconst availableSlots = this.getGloballyAvailableSlots(workerAvailableSlots);\n\n\t\t\tif (availableSlots <= 0) {\n\t\t\t\t// Global limit reached, stop processing all workers\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Try to acquire jobs up to available slots\n\t\t\tfor (let i = 0; i < availableSlots; i++) {\n\t\t\t\tif (!this.ctx.isRunning()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Re-check global limit before each acquisition\n\t\t\t\tif (instanceConcurrency !== undefined && this.getTotalActiveJobs() >= instanceConcurrency) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst job = await this.acquireJob(name);\n\n\t\t\t\tif (job) {\n\t\t\t\t\t// Add to activeJobs immediately to correctly track concurrency\n\t\t\t\t\tworker.activeJobs.set(job._id.toString(), job);\n\n\t\t\t\t\tthis.processJob(job, worker).catch((error: unknown) => {\n\t\t\t\t\t\tthis.ctx.emit('job:error', { error: toError(error), job });\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t// No more jobs available for this worker\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Atomically acquire a pending job for processing using the claimedBy pattern.\n\t *\n\t * Uses MongoDB's `findOneAndUpdate` with atomic operations to ensure only one scheduler\n\t * instance can claim a job. The query ensures the job is:\n\t * - In pending status\n\t * - Has nextRunAt <= now\n\t * - Is not claimed by another instance (claimedBy is null/undefined)\n\t *\n\t * Returns `null` immediately if scheduler is stopping (`isRunning` is false).\n\t *\n\t * @param name - The job type to acquire\n\t * @returns The acquired job with updated status, claimedBy, and heartbeat info, or `null` if no jobs available\n\t */\n\tasync acquireJob(name: string): Promise<PersistedJob | null> {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst now = new Date();\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\tnextRunAt: { $lte: now },\n\t\t\t\t$or: [{ claimedBy: null }, { claimedBy: { $exists: false } }],\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\t\t\tclaimedBy: this.ctx.instanceId,\n\t\t\t\t\tlockedAt: now,\n\t\t\t\t\tlastHeartbeat: now,\n\t\t\t\t\theartbeatInterval: this.ctx.options.heartbeatInterval,\n\t\t\t\t\tupdatedAt: now,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tsort: { nextRunAt: 1 },\n\t\t\t\treturnDocument: 'after',\n\t\t\t},\n\t\t);\n\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (!result) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.ctx.documentToPersistedJob(result);\n\t}\n\n\t/**\n\t * Execute a job using its registered worker handler.\n\t *\n\t * Tracks the job as active during processing, emits lifecycle events, and handles\n\t * both success and failure cases. On success, calls `completeJob()`. On failure,\n\t * calls `failJob()` which implements exponential backoff retry logic.\n\t *\n\t * Events are only emitted when the underlying atomic status transition succeeds,\n\t * ensuring event consumers receive reliable, consistent data backed by the actual\n\t * database state.\n\t *\n\t * @param job - The job to process\n\t * @param worker - The worker registration containing the handler and active job tracking\n\t */\n\tasync processJob(job: PersistedJob, worker: WorkerRegistration): Promise<void> {\n\t\tconst jobId = job._id.toString();\n\t\tconst startTime = Date.now();\n\t\tthis.ctx.emit('job:start', job);\n\n\t\ttry {\n\t\t\tawait worker.handler(job);\n\n\t\t\t// Job completed successfully\n\t\t\tconst duration = Date.now() - startTime;\n\t\t\tconst updatedJob = await this.completeJob(job);\n\n\t\t\tif (updatedJob) {\n\t\t\t\tthis.ctx.emit('job:complete', { job: updatedJob, duration });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// Job failed\n\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\tconst updatedJob = await this.failJob(job, err);\n\n\t\t\tif (updatedJob) {\n\t\t\t\tconst willRetry = updatedJob.status === JobStatus.PENDING;\n\t\t\t\tthis.ctx.emit('job:fail', { job: updatedJob, error: err, willRetry });\n\t\t\t}\n\t\t} finally {\n\t\t\tworker.activeJobs.delete(jobId);\n\t\t}\n\t}\n\n\t/**\n\t * Mark a job as completed successfully using an atomic status transition.\n\t *\n\t * Uses `findOneAndUpdate` with `status: processing` and `claimedBy: instanceId`\n\t * preconditions to ensure the transition only occurs if the job is still owned by this\n\t * scheduler instance. Returns `null` if the job was concurrently modified (e.g., reclaimed\n\t * by another instance after stale recovery).\n\t *\n\t * For recurring jobs (with `repeatInterval`), schedules the next run based on the cron\n\t * expression and resets `failCount` to 0. For one-time jobs, sets status to `completed`.\n\t * Clears `lockedAt` and `failReason` fields in both cases.\n\t *\n\t * @param job - The job that completed successfully\n\t * @returns The updated job document, or `null` if the transition could not be applied\n\t */\n\tasync completeJob(job: Job): Promise<PersistedJob | null> {\n\t\tif (!isPersistedJob(job)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (job.repeatInterval) {\n\t\t\t// Recurring job - schedule next run\n\t\t\tconst nextRunAt = getNextCronDate(job.repeatInterval);\n\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\t\tnextRunAt,\n\t\t\t\t\t\tfailCount: 0,\n\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t},\n\t\t\t\t\t$unset: {\n\t\t\t\t\t\tlockedAt: '',\n\t\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t\t\tfailReason: '',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ returnDocument: 'after' },\n\t\t\t);\n\n\t\t\tif (!result) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst persistedJob = this.ctx.documentToPersistedJob(result);\n\t\t\tthis.ctx.notifyPendingJob(persistedJob.name, persistedJob.nextRunAt);\n\t\t\treturn persistedJob;\n\t\t}\n\n\t\t// One-time job - mark as completed\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.COMPLETED,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t\tfailReason: '',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\tif (!result) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst persistedJob = this.ctx.documentToPersistedJob(result);\n\t\treturn persistedJob;\n\t}\n\n\t/**\n\t * Handle job failure with exponential backoff retry logic using an atomic status transition.\n\t *\n\t * Uses `findOneAndUpdate` with `status: processing` and `claimedBy: instanceId`\n\t * preconditions to ensure the transition only occurs if the job is still owned by this\n\t * scheduler instance. Returns `null` if the job was concurrently modified (e.g., reclaimed\n\t * by another instance after stale recovery).\n\t *\n\t * Increments `failCount` and calculates next retry time using exponential backoff:\n\t * `nextRunAt = 2^failCount * baseRetryInterval` (capped by optional `maxBackoffDelay`).\n\t *\n\t * If `failCount >= maxRetries`, marks job as permanently `failed`. Otherwise, resets\n\t * to `pending` status for retry. Stores error message in `failReason` field.\n\t *\n\t * @param job - The job that failed\n\t * @param error - The error that caused the failure\n\t * @returns The updated job document, or `null` if the transition could not be applied\n\t */\n\tasync failJob(job: Job, error: Error): Promise<PersistedJob | null> {\n\t\tif (!isPersistedJob(job)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst newFailCount = job.failCount + 1;\n\n\t\tif (newFailCount >= this.ctx.options.maxRetries) {\n\t\t\t// Permanent failure\n\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tstatus: JobStatus.FAILED,\n\t\t\t\t\t\tfailCount: newFailCount,\n\t\t\t\t\t\tfailReason: error.message,\n\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t},\n\t\t\t\t\t$unset: {\n\t\t\t\t\t\tlockedAt: '',\n\t\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ returnDocument: 'after' },\n\t\t\t);\n\n\t\t\treturn result ? this.ctx.documentToPersistedJob(result) : null;\n\t\t}\n\n\t\t// Schedule retry with exponential backoff\n\t\tconst nextRunAt = calculateBackoff(\n\t\t\tnewFailCount,\n\t\t\tthis.ctx.options.baseRetryInterval,\n\t\t\tthis.ctx.options.maxBackoffDelay,\n\t\t);\n\n\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\tfailCount: newFailCount,\n\t\t\t\t\tfailReason: error.message,\n\t\t\t\t\tnextRunAt,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ returnDocument: 'after' },\n\t\t);\n\n\t\treturn result ? this.ctx.documentToPersistedJob(result) : null;\n\t}\n\n\t/**\n\t * Update heartbeats for all jobs claimed by this scheduler instance.\n\t *\n\t * This method runs periodically while the scheduler is running to indicate\n\t * that jobs are still being actively processed.\n\t *\n\t * `lastHeartbeat` is primarily an observability signal (monitoring/debugging).\n\t * Stale recovery is based on `lockedAt` + `lockTimeout`.\n\t */\n\tasync updateHeartbeats(): Promise<void> {\n\t\tif (!this.ctx.isRunning()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = new Date();\n\n\t\tawait this.ctx.collection.updateMany(\n\t\t\t{\n\t\t\t\tclaimedBy: this.ctx.instanceId,\n\t\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tlastHeartbeat: now,\n\t\t\t\t\tupdatedAt: now,\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n}\n","import type { Document, Filter, ObjectId, WithId } from 'mongodb';\n\nimport {\n\tCursorDirection,\n\ttype CursorDirectionType,\n\ttype CursorOptions,\n\ttype CursorPage,\n\ttype GetJobsFilter,\n\ttype JobSelector,\n\tJobStatus,\n\ttype PersistedJob,\n\ttype QueueStats,\n} from '@/jobs';\nimport { AggregationTimeoutError, ConnectionError } from '@/shared';\n\nimport { buildSelectorQuery, decodeCursor, encodeCursor } from '../helpers.js';\nimport type { SchedulerContext } from './types.js';\n\ninterface StatsCacheEntry {\n\tdata: QueueStats;\n\texpiresAt: number;\n}\n\n/**\n * Internal service for job query operations.\n *\n * Provides read-only access to jobs with filtering and cursor-based pagination.\n * All queries use efficient index-backed access patterns.\n *\n * @internal Not part of public API - use Monque class methods instead.\n */\nexport class JobQueryService {\n\tprivate readonly statsCache = new Map<string, StatsCacheEntry>();\n\tprivate static readonly MAX_CACHE_SIZE = 100;\n\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Get a single job by its MongoDB ObjectId.\n\t *\n\t * Useful for retrieving job details when you have a job ID from events,\n\t * logs, or stored references.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param id - The job's ObjectId\n\t * @returns Promise resolving to the job if found, null otherwise\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Look up job from event\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * // Later, retrieve the job to check its status\n\t * const currentJob = await monque.getJob(job._id);\n\t * console.log(`Job status: ${currentJob?.status}`);\n\t * });\n\t * ```\n\t *\n\t * @example Admin endpoint\n\t * ```typescript\n\t * app.get('/jobs/:id', async (req, res) => {\n\t * const job = await monque.getJob(new ObjectId(req.params.id));\n\t * if (!job) {\n\t * return res.status(404).json({ error: 'Job not found' });\n\t * }\n\t * res.json(job);\n\t * });\n\t * ```\n\t */\n\tasync getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null> {\n\t\ttry {\n\t\t\tconst doc = await this.ctx.collection.findOne({ _id: id });\n\t\t\tif (!doc) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn this.ctx.documentToPersistedJob<T>(doc as WithId<Document>);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during getJob';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to get job: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Query jobs from the queue with optional filters.\n\t *\n\t * Provides read-only access to job data for monitoring, debugging, and\n\t * administrative purposes. Results are ordered by `nextRunAt` ascending.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param filter - Optional filter criteria\n\t * @returns Promise resolving to array of matching jobs\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Get all pending jobs\n\t * ```typescript\n\t * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });\n\t * console.log(`${pendingJobs.length} jobs waiting`);\n\t * ```\n\t *\n\t * @example Get failed email jobs\n\t * ```typescript\n\t * const failedEmails = await monque.getJobs({\n\t * name: 'send-email',\n\t * status: JobStatus.FAILED,\n\t * });\n\t * for (const job of failedEmails) {\n\t * console.error(`Job ${job._id} failed: ${job.failReason}`);\n\t * }\n\t * ```\n\t *\n\t * @example Paginated job listing\n\t * ```typescript\n\t * const page1 = await monque.getJobs({ limit: 50, skip: 0 });\n\t * const page2 = await monque.getJobs({ limit: 50, skip: 50 });\n\t * ```\n\t *\n\t * @example Use with type guards from @monque/core\n\t * ```typescript\n\t * import { isPendingJob, isRecurringJob } from '@monque/core';\n\t *\n\t * const jobs = await monque.getJobs();\n\t * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));\n\t * ```\n\t */\n\tasync getJobs<T = unknown>(filter: GetJobsFilter = {}): Promise<PersistedJob<T>[]> {\n\t\tconst query: Document = {};\n\n\t\tif (filter.name !== undefined) {\n\t\t\tquery['name'] = filter.name;\n\t\t}\n\n\t\tif (filter.status !== undefined) {\n\t\t\tif (Array.isArray(filter.status)) {\n\t\t\t\tquery['status'] = { $in: filter.status };\n\t\t\t} else {\n\t\t\t\tquery['status'] = filter.status;\n\t\t\t}\n\t\t}\n\n\t\tconst limit = filter.limit ?? 100;\n\t\tconst skip = filter.skip ?? 0;\n\n\t\ttry {\n\t\t\tconst cursor = this.ctx.collection.find(query).sort({ nextRunAt: 1 }).skip(skip).limit(limit);\n\n\t\t\tconst docs = await cursor.toArray();\n\t\t\treturn docs.map((doc) => this.ctx.documentToPersistedJob<T>(doc));\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during getJobs';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to query jobs: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a paginated list of jobs using opaque cursors.\n\t *\n\t * Provides stable pagination for large job lists. Supports forward and backward\n\t * navigation, filtering, and efficient database access via index-based cursor queries.\n\t *\n\t * @template T - The job data payload type\n\t * @param options - Pagination options (cursor, limit, direction, filter)\n\t * @returns Page of jobs with next/prev cursors\n\t * @throws {InvalidCursorError} If the provided cursor is malformed\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example List pending jobs\n\t * ```typescript\n\t * const page = await monque.getJobsWithCursor({\n\t * limit: 20,\n\t * filter: { status: 'pending' }\n\t * });\n\t * const jobs = page.jobs;\n\t *\n\t * // Get next page\n\t * if (page.hasNextPage) {\n\t * const page2 = await monque.getJobsWithCursor({\n\t * cursor: page.cursor,\n\t * limit: 20\n\t * });\n\t * }\n\t * ```\n\t */\n\tasync getJobsWithCursor<T = unknown>(options: CursorOptions = {}): Promise<CursorPage<T>> {\n\t\tconst limit = options.limit ?? 50;\n\t\t// Default to forward if not specified.\n\t\tconst direction: CursorDirectionType = options.direction ?? CursorDirection.FORWARD;\n\t\tlet anchorId: ObjectId | null = null;\n\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tanchorId = decoded.id;\n\t\t}\n\n\t\t// Build base query from filters\n\t\tconst query: Filter<Document> = options.filter ? buildSelectorQuery(options.filter) : {};\n\n\t\t// Add cursor condition to query\n\t\tconst sortDir = direction === CursorDirection.FORWARD ? 1 : -1;\n\n\t\tif (anchorId) {\n\t\t\tif (direction === CursorDirection.FORWARD) {\n\t\t\t\tquery._id = { ...query._id, $gt: anchorId };\n\t\t\t} else {\n\t\t\t\tquery._id = { ...query._id, $lt: anchorId };\n\t\t\t}\n\t\t}\n\n\t\t// Fetch limit + 1 to detect hasNext/hasPrev\n\t\tconst fetchLimit = limit + 1;\n\n\t\t// Sort: Always deterministic.\n\t\tlet docs: WithId<Document>[];\n\t\ttry {\n\t\t\tdocs = await this.ctx.collection\n\t\t\t\t.find(query)\n\t\t\t\t.sort({ _id: sortDir })\n\t\t\t\t.limit(fetchLimit)\n\t\t\t\t.toArray();\n\t\t} catch (error) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : 'Unknown error during getJobsWithCursor';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to query jobs with cursor: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\n\t\tlet hasMore = false;\n\t\tif (docs.length > limit) {\n\t\t\thasMore = true;\n\t\t\tdocs.pop(); // Remove the extra item\n\t\t}\n\n\t\tif (direction === CursorDirection.BACKWARD) {\n\t\t\tdocs.reverse();\n\t\t}\n\n\t\tconst jobs = docs.map((doc) => this.ctx.documentToPersistedJob<T>(doc as WithId<Document>));\n\n\t\tlet nextCursor: string | null = null;\n\n\t\tif (jobs.length > 0) {\n\t\t\tconst lastJob = jobs[jobs.length - 1];\n\t\t\t// Check for existence to satisfy strict null checks/noUncheckedIndexedAccess\n\t\t\tif (lastJob) {\n\t\t\t\tnextCursor = encodeCursor(lastJob._id, direction);\n\t\t\t}\n\t\t}\n\n\t\tlet hasNextPage = false;\n\t\tlet hasPreviousPage = false;\n\n\t\t// Determine availability of next/prev pages\n\t\tif (direction === CursorDirection.FORWARD) {\n\t\t\thasNextPage = hasMore;\n\t\t\thasPreviousPage = !!anchorId;\n\t\t} else {\n\t\t\thasNextPage = !!anchorId;\n\t\t\thasPreviousPage = hasMore;\n\t\t}\n\n\t\treturn {\n\t\t\tjobs,\n\t\t\tcursor: nextCursor,\n\t\t\thasNextPage,\n\t\t\thasPreviousPage,\n\t\t};\n\t}\n\n\t/**\n\t * Clear all cached getQueueStats() results.\n\t * Called on scheduler stop() for clean state on restart.\n\t * @internal\n\t */\n\tclearStatsCache(): void {\n\t\tthis.statsCache.clear();\n\t}\n\n\t/**\n\t * Get aggregate statistics for the job queue.\n\t *\n\t * Uses MongoDB aggregation pipeline for efficient server-side calculation.\n\t * Returns counts per status and optional average processing duration for completed jobs.\n\t *\n\t * Results are cached per unique filter with a configurable TTL (default 5s).\n\t * Set `statsCacheTtlMs: 0` to disable caching.\n\t *\n\t * @param filter - Optional filter to scope statistics by job name\n\t * @returns Promise resolving to queue statistics\n\t * @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout\n\t * @throws {ConnectionError} If database operation fails\n\t *\n\t * @example Get overall queue statistics\n\t * ```typescript\n\t * const stats = await monque.getQueueStats();\n\t * console.log(`Pending: ${stats.pending}, Failed: ${stats.failed}`);\n\t * ```\n\t *\n\t * @example Get statistics for a specific job type\n\t * ```typescript\n\t * const emailStats = await monque.getQueueStats({ name: 'send-email' });\n\t * console.log(`${emailStats.total} email jobs in queue`);\n\t * ```\n\t */\n\tasync getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats> {\n\t\tconst ttl = this.ctx.options.statsCacheTtlMs;\n\t\tconst cacheKey = filter?.name ?? '';\n\n\t\tif (ttl > 0) {\n\t\t\tconst cached = this.statsCache.get(cacheKey);\n\t\t\tif (cached && cached.expiresAt > Date.now()) {\n\t\t\t\treturn { ...cached.data };\n\t\t\t}\n\t\t}\n\n\t\tconst matchStage: Document = {};\n\n\t\tif (filter?.name) {\n\t\t\tmatchStage['name'] = filter.name;\n\t\t}\n\n\t\tconst pipeline: Document[] = [\n\t\t\t// Optional match stage for filtering by name\n\t\t\t...(Object.keys(matchStage).length > 0 ? [{ $match: matchStage }] : []),\n\t\t\t// Facet to calculate counts and avg processing duration in parallel\n\t\t\t{\n\t\t\t\t$facet: {\n\t\t\t\t\t// Count by status\n\t\t\t\t\tstatusCounts: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$group: {\n\t\t\t\t\t\t\t\t_id: '$status',\n\t\t\t\t\t\t\t\tcount: { $sum: 1 },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\t// Calculate average job lifetime for completed jobs.\n\t\t\t\t\t// Uses createdAt → updatedAt (total lifetime = queue wait + processing)\n\t\t\t\t\t// since completeJob() unsets lockedAt, making pure processing time unavailable.\n\t\t\t\t\tavgDuration: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$match: {\n\t\t\t\t\t\t\t\tstatus: JobStatus.COMPLETED,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t$group: {\n\t\t\t\t\t\t\t\t_id: null,\n\t\t\t\t\t\t\t\tavgMs: {\n\t\t\t\t\t\t\t\t\t$avg: {\n\t\t\t\t\t\t\t\t\t\t$subtract: ['$updatedAt', '$createdAt'],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\t// Total count\n\t\t\t\t\ttotal: [{ $count: 'count' }],\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\n\t\ttry {\n\t\t\tconst results = await this.ctx.collection.aggregate(pipeline, { maxTimeMS: 30000 }).toArray();\n\n\t\t\tconst result = results[0];\n\n\t\t\t// Initialize with zeros\n\t\t\tconst stats: QueueStats = {\n\t\t\t\tpending: 0,\n\t\t\t\tprocessing: 0,\n\t\t\t\tcompleted: 0,\n\t\t\t\tfailed: 0,\n\t\t\t\tcancelled: 0,\n\t\t\t\ttotal: 0,\n\t\t\t};\n\n\t\t\tif (result) {\n\t\t\t\t// Map status counts to stats\n\t\t\t\tconst statusCounts = result['statusCounts'] as Array<{ _id: string; count: number }>;\n\t\t\t\tfor (const entry of statusCounts) {\n\t\t\t\t\tconst status = entry._id;\n\t\t\t\t\tconst count = entry.count;\n\n\t\t\t\t\tswitch (status) {\n\t\t\t\t\t\tcase JobStatus.PENDING:\n\t\t\t\t\t\t\tstats.pending = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.PROCESSING:\n\t\t\t\t\t\t\tstats.processing = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.COMPLETED:\n\t\t\t\t\t\t\tstats.completed = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.FAILED:\n\t\t\t\t\t\t\tstats.failed = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase JobStatus.CANCELLED:\n\t\t\t\t\t\t\tstats.cancelled = count;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Extract total\n\t\t\t\tconst totalResult = result['total'] as Array<{ count: number }>;\n\t\t\t\tif (totalResult.length > 0 && totalResult[0]) {\n\t\t\t\t\tstats.total = totalResult[0].count;\n\t\t\t\t}\n\n\t\t\t\t// Extract average processing duration\n\t\t\t\tconst avgDurationResult = result['avgDuration'] as Array<{ avgMs: number }>;\n\t\t\t\tif (avgDurationResult.length > 0 && avgDurationResult[0]) {\n\t\t\t\t\tconst avgMs = avgDurationResult[0].avgMs;\n\t\t\t\t\tif (typeof avgMs === 'number' && !Number.isNaN(avgMs)) {\n\t\t\t\t\t\tstats.avgProcessingDurationMs = Math.round(avgMs);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Cache the result if TTL is enabled\n\t\t\tif (ttl > 0) {\n\t\t\t\t// Delete existing entry first so re-insertion moves it to end (Map insertion order = LRU)\n\t\t\t\tthis.statsCache.delete(cacheKey);\n\t\t\t\t// LRU eviction: if cache is still full after removing existing key, evict the oldest entry\n\t\t\t\tif (this.statsCache.size >= JobQueryService.MAX_CACHE_SIZE) {\n\t\t\t\t\tconst oldestKey = this.statsCache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tthis.statsCache.delete(oldestKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.statsCache.set(cacheKey, {\n\t\t\t\t\tdata: { ...stats },\n\t\t\t\t\texpiresAt: Date.now() + ttl,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn stats;\n\t\t} catch (error) {\n\t\t\t// Check for timeout error\n\t\t\tif (error instanceof Error && error.message.includes('exceeded time limit')) {\n\t\t\t\tthrow new AggregationTimeoutError();\n\t\t\t}\n\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during getQueueStats';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to get queue stats: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n","import { BSON, type Document } from 'mongodb';\n\nimport {\n\ttype EnqueueOptions,\n\ttype Job,\n\tJobStatus,\n\ttype PersistedJob,\n\ttype ScheduleOptions,\n} from '@/jobs';\nimport { ConnectionError, getNextCronDate, MonqueError, PayloadTooLargeError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Internal service for job scheduling operations.\n *\n * Handles enqueueing new jobs, immediate dispatch, and cron scheduling.\n * All operations are atomic and support deduplication via uniqueKey.\n *\n * @internal Not part of public API - use Monque class methods instead.\n */\nexport class JobScheduler {\n\tconstructor(private readonly ctx: SchedulerContext) {}\n\n\t/**\n\t * Validate that the job data payload does not exceed the configured maximum BSON byte size.\n\t *\n\t * @param data - The job data payload to validate\n\t * @throws {PayloadTooLargeError} If the payload exceeds `maxPayloadSize`\n\t */\n\tprivate validatePayloadSize(data: unknown): void {\n\t\tconst maxSize = this.ctx.options.maxPayloadSize;\n\t\tif (maxSize === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet size: number;\n\t\ttry {\n\t\t\tsize = BSON.calculateObjectSize({ data } as Document);\n\t\t} catch (error) {\n\t\t\tconst cause = error instanceof Error ? error : new Error(String(error));\n\t\t\tconst sizeError = new PayloadTooLargeError(\n\t\t\t\t`Failed to calculate job payload size: ${cause.message}`,\n\t\t\t\t-1,\n\t\t\t\tmaxSize,\n\t\t\t);\n\t\t\tsizeError.cause = cause;\n\t\t\tthrow sizeError;\n\t\t}\n\n\t\tif (size > maxSize) {\n\t\t\tthrow new PayloadTooLargeError(\n\t\t\t\t`Job payload exceeds maximum size: ${size} bytes > ${maxSize} bytes`,\n\t\t\t\tsize,\n\t\t\t\tmaxSize,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue a job for processing.\n\t *\n\t * Jobs are stored in MongoDB and processed by registered workers. Supports\n\t * delayed execution via `runAt` and deduplication via `uniqueKey`.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. Completed or failed jobs don't block new jobs with the same key.\n\t *\n\t * Failed jobs are automatically retried with exponential backoff up to `maxRetries`\n\t * (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @param options - Scheduling and deduplication options\n\t * @returns Promise resolving to the created or existing job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Basic job enqueueing\n\t * ```typescript\n\t * await monque.enqueue('send-email', {\n\t * to: 'user@example.com',\n\t * subject: 'Welcome!',\n\t * body: 'Thanks for signing up.'\n\t * });\n\t * ```\n\t *\n\t * @example Delayed execution\n\t * ```typescript\n\t * const oneHourLater = new Date(Date.now() + 3600000);\n\t * await monque.enqueue('reminder', { message: 'Check in!' }, {\n\t * runAt: oneHourLater\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicates with unique key\n\t * ```typescript\n\t * await monque.enqueue('sync-user', { userId: '123' }, {\n\t * uniqueKey: 'sync-user-123'\n\t * });\n\t * // Subsequent enqueues with same uniqueKey return existing pending/processing job\n\t * ```\n\t */\n\tasync enqueue<T>(name: string, data: T, options: EnqueueOptions = {}): Promise<PersistedJob<T>> {\n\t\tthis.validatePayloadSize(data);\n\t\tconst now = new Date();\n\t\tconst job: Omit<Job<T>, '_id'> = {\n\t\t\tname,\n\t\t\tdata,\n\t\t\tstatus: JobStatus.PENDING,\n\t\t\tnextRunAt: options.runAt ?? now,\n\t\t\tfailCount: 0,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\n\t\tif (options.uniqueKey) {\n\t\t\tjob.uniqueKey = options.uniqueKey;\n\t\t}\n\n\t\ttry {\n\t\t\tif (options.uniqueKey) {\n\t\t\t\t// Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)\n\t\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t\t{\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tuniqueKey: options.uniqueKey,\n\t\t\t\t\t\tstatus: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t$setOnInsert: job,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tupsert: true,\n\t\t\t\t\t\treturnDocument: 'after',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tif (!result) {\n\t\t\t\t\tthrow new ConnectionError('Failed to enqueue job: findOneAndUpdate returned no document');\n\t\t\t\t}\n\n\t\t\t\tconst persistedJob = this.ctx.documentToPersistedJob<T>(result);\n\t\t\t\tif (persistedJob.status === JobStatus.PENDING) {\n\t\t\t\t\tthis.ctx.notifyPendingJob(persistedJob.name, persistedJob.nextRunAt);\n\t\t\t\t}\n\n\t\t\t\treturn persistedJob;\n\t\t\t}\n\n\t\t\tconst result = await this.ctx.collection.insertOne(job as Document);\n\t\t\tconst persistedJob = { ...job, _id: result.insertedId } as PersistedJob<T>;\n\t\t\tthis.ctx.notifyPendingJob(persistedJob.name, persistedJob.nextRunAt);\n\n\t\t\treturn persistedJob;\n\t\t} catch (error) {\n\t\t\tif (error instanceof ConnectionError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during enqueue';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to enqueue job: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue a job for immediate processing.\n\t *\n\t * Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.\n\t * Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @returns Promise resolving to the created job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example Send email immediately\n\t * ```typescript\n\t * await monque.now('send-email', {\n\t * to: 'admin@example.com',\n\t * subject: 'Alert',\n\t * body: 'Immediate attention required'\n\t * });\n\t * ```\n\t *\n\t * @example Process order in background\n\t * ```typescript\n\t * const order = await createOrder(data);\n\t * await monque.now('process-order', { orderId: order.id });\n\t * return order; // Return immediately, processing happens async\n\t * ```\n\t */\n\tasync now<T>(name: string, data: T): Promise<PersistedJob<T>> {\n\t\treturn this.enqueue(name, data, { runAt: new Date() });\n\t}\n\n\t/**\n\t * Schedule a recurring job with a cron expression.\n\t *\n\t * Creates a job that automatically re-schedules itself based on the cron pattern.\n\t * Uses standard 5-field cron format: minute, hour, day of month, month, day of week.\n\t * Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.\n\t * After successful completion, the job is reset to `pending` status and scheduled\n\t * for its next run based on the cron expression.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. This prevents duplicate scheduled jobs on application restart.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param cron - Cron expression (5 fields or predefined expression)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler on each run\n\t * @param options - Scheduling options (uniqueKey for deduplication)\n\t * @returns Promise resolving to the created job document with `repeatInterval` set\n\t * @throws {InvalidCronError} If cron expression is invalid\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Hourly cleanup job\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'cleanup-temp-files', {\n\t * directory: '/tmp/uploads'\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicate scheduled jobs with unique key\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {\n\t * uniqueKey: 'hourly-report-sales'\n\t * });\n\t * // Subsequent calls with same uniqueKey return existing pending/processing job\n\t * ```\n\t *\n\t * @example Daily report at midnight (using predefined expression)\n\t * ```typescript\n\t * await monque.schedule('@daily', 'daily-report', {\n\t * reportType: 'sales',\n\t * recipients: ['analytics@example.com']\n\t * });\n\t * ```\n\t */\n\tasync schedule<T>(\n\t\tcron: string,\n\t\tname: string,\n\t\tdata: T,\n\t\toptions: ScheduleOptions = {},\n\t): Promise<PersistedJob<T>> {\n\t\tthis.validatePayloadSize(data);\n\n\t\t// Validate cron and get next run date (throws InvalidCronError if invalid)\n\t\tconst nextRunAt = getNextCronDate(cron);\n\n\t\tconst now = new Date();\n\t\tconst job: Omit<Job<T>, '_id'> = {\n\t\t\tname,\n\t\t\tdata,\n\t\t\tstatus: JobStatus.PENDING,\n\t\t\tnextRunAt,\n\t\t\trepeatInterval: cron,\n\t\t\tfailCount: 0,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\n\t\tif (options.uniqueKey) {\n\t\t\tjob.uniqueKey = options.uniqueKey;\n\t\t}\n\n\t\ttry {\n\t\t\tif (options.uniqueKey) {\n\t\t\t\t// Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)\n\t\t\t\tconst result = await this.ctx.collection.findOneAndUpdate(\n\t\t\t\t\t{\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tuniqueKey: options.uniqueKey,\n\t\t\t\t\t\tstatus: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t$setOnInsert: job,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tupsert: true,\n\t\t\t\t\t\treturnDocument: 'after',\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tif (!result) {\n\t\t\t\t\tthrow new ConnectionError(\n\t\t\t\t\t\t'Failed to schedule job: findOneAndUpdate returned no document',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst persistedJob = this.ctx.documentToPersistedJob<T>(result);\n\t\t\t\tif (persistedJob.status === JobStatus.PENDING) {\n\t\t\t\t\tthis.ctx.notifyPendingJob(persistedJob.name, persistedJob.nextRunAt);\n\t\t\t\t}\n\n\t\t\t\treturn persistedJob;\n\t\t\t}\n\n\t\t\tconst result = await this.ctx.collection.insertOne(job as Document);\n\t\t\tconst persistedJob = { ...job, _id: result.insertedId } as PersistedJob<T>;\n\t\t\tthis.ctx.notifyPendingJob(persistedJob.name, persistedJob.nextRunAt);\n\n\t\t\treturn persistedJob;\n\t\t} catch (error) {\n\t\t\tif (error instanceof MonqueError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst message = error instanceof Error ? error.message : 'Unknown error during schedule';\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Failed to schedule job: ${message}`,\n\t\t\t\terror instanceof Error ? { cause: error } : undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n","import type { DeleteResult } from 'mongodb';\n\nimport { JobStatus } from '@/jobs';\nimport { toError } from '@/shared';\n\nimport type { SchedulerContext } from './types.js';\n\n/**\n * Default retention check interval (1 hour).\n */\nconst DEFAULT_RETENTION_INTERVAL = 3600_000;\n\n/**\n * Callbacks for timer-driven operations.\n *\n * These are provided by the Monque facade to wire LifecycleManager's timers\n * to JobProcessor methods without creating a direct dependency.\n */\ninterface TimerCallbacks {\n\t/** Poll for pending jobs */\n\tpoll: () => Promise<void>;\n\t/** Update heartbeats for claimed jobs */\n\tupdateHeartbeats: () => Promise<void>;\n\t/** Whether change streams are currently active */\n\tisChangeStreamActive: () => boolean;\n}\n\n/**\n * Manages scheduler lifecycle timers and job cleanup.\n *\n * Owns poll scheduling, heartbeat interval, cleanup interval, and the\n * cleanupJobs logic. Extracted from Monque to keep the facade thin.\n *\n * Uses adaptive poll scheduling: when change streams are active, polls at\n * `safetyPollInterval` (safety net only). When change streams are inactive,\n * polls at `pollInterval` (primary discovery mechanism).\n *\n * @internal Not part of public API.\n */\nexport class LifecycleManager {\n\tprivate readonly ctx: SchedulerContext;\n\tprivate callbacks: TimerCallbacks | null = null;\n\tprivate pollTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\tprivate heartbeatIntervalId: ReturnType<typeof setInterval> | null = null;\n\tprivate cleanupIntervalId: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(ctx: SchedulerContext) {\n\t\tthis.ctx = ctx;\n\t}\n\n\t/**\n\t * Start all lifecycle timers.\n\t *\n\t * Sets up adaptive poll scheduling, heartbeat interval, and (if configured)\n\t * cleanup interval. Runs an initial poll immediately.\n\t *\n\t * @param callbacks - Functions to invoke on each timer tick\n\t */\n\tstartTimers(callbacks: TimerCallbacks): void {\n\t\tthis.callbacks = callbacks;\n\n\t\t// Start heartbeat interval for claimed jobs\n\t\tthis.heartbeatIntervalId = setInterval(() => {\n\t\t\tcallbacks.updateHeartbeats().catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\t\t}, this.ctx.options.heartbeatInterval);\n\n\t\t// Start cleanup interval if retention is configured\n\t\tif (this.ctx.options.jobRetention) {\n\t\t\tconst interval = this.ctx.options.jobRetention.interval ?? DEFAULT_RETENTION_INTERVAL;\n\n\t\t\t// Run immediately on start\n\t\t\tthis.cleanupJobs().catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t});\n\n\t\t\tthis.cleanupIntervalId = setInterval(() => {\n\t\t\t\tthis.cleanupJobs().catch((error: unknown) => {\n\t\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t\t});\n\t\t\t}, interval);\n\t\t}\n\n\t\t// Run initial poll immediately, then schedule the next one adaptively\n\t\tthis.executePollAndScheduleNext();\n\t}\n\n\t/**\n\t * Stop all lifecycle timers.\n\t *\n\t * Clears poll timeout, heartbeat interval, and cleanup interval.\n\t */\n\tstopTimers(): void {\n\t\tthis.callbacks = null;\n\n\t\tif (this.cleanupIntervalId) {\n\t\t\tclearInterval(this.cleanupIntervalId);\n\t\t\tthis.cleanupIntervalId = null;\n\t\t}\n\n\t\tif (this.pollTimeoutId) {\n\t\t\tclearTimeout(this.pollTimeoutId);\n\t\t\tthis.pollTimeoutId = null;\n\t\t}\n\n\t\tif (this.heartbeatIntervalId) {\n\t\t\tclearInterval(this.heartbeatIntervalId);\n\t\t\tthis.heartbeatIntervalId = null;\n\t\t}\n\t}\n\n\t/**\n\t * Reset the poll timer to reschedule the next poll.\n\t *\n\t * Called after change-stream-triggered polls to ensure the safety poll timer\n\t * is recalculated (not fired redundantly from an old schedule).\n\t */\n\tresetPollTimer(): void {\n\t\tthis.scheduleNextPoll();\n\t}\n\n\t/**\n\t * Execute a poll and schedule the next one adaptively.\n\t */\n\tprivate executePollAndScheduleNext(): void {\n\t\tif (!this.callbacks) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.callbacks\n\t\t\t.poll()\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tthis.ctx.emit('job:error', { error: toError(error) });\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\tthis.scheduleNextPoll();\n\t\t\t});\n\t}\n\n\t/**\n\t * Schedule the next poll using adaptive timing.\n\t *\n\t * When change streams are active, uses `safetyPollInterval` (longer, safety net only).\n\t * When change streams are inactive, uses `pollInterval` (shorter, primary discovery).\n\t */\n\tprivate scheduleNextPoll(): void {\n\t\tif (this.pollTimeoutId) {\n\t\t\tclearTimeout(this.pollTimeoutId);\n\t\t\tthis.pollTimeoutId = null;\n\t\t}\n\n\t\tif (!this.ctx.isRunning() || !this.callbacks) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = this.callbacks.isChangeStreamActive()\n\t\t\t? this.ctx.options.safetyPollInterval\n\t\t\t: this.ctx.options.pollInterval;\n\n\t\tthis.pollTimeoutId = setTimeout(() => {\n\t\t\tthis.executePollAndScheduleNext();\n\t\t}, delay);\n\t}\n\n\t/**\n\t * Clean up old completed and failed jobs based on retention policy.\n\t *\n\t * - Removes completed jobs older than `jobRetention.completed`\n\t * - Removes failed jobs older than `jobRetention.failed`\n\t *\n\t * The cleanup runs concurrently for both statuses if configured.\n\t *\n\t * @returns Promise resolving when all deletion operations complete\n\t */\n\tasync cleanupJobs(): Promise<void> {\n\t\tif (!this.ctx.options.jobRetention) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { completed, failed } = this.ctx.options.jobRetention;\n\t\tconst now = Date.now();\n\t\tconst deletions: Promise<DeleteResult>[] = [];\n\n\t\tif (completed != null) {\n\t\t\tconst cutoff = new Date(now - completed);\n\t\t\tdeletions.push(\n\t\t\t\tthis.ctx.collection.deleteMany({\n\t\t\t\t\tstatus: JobStatus.COMPLETED,\n\t\t\t\t\tupdatedAt: { $lt: cutoff },\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (failed != null) {\n\t\t\tconst cutoff = new Date(now - failed);\n\t\t\tdeletions.push(\n\t\t\t\tthis.ctx.collection.deleteMany({\n\t\t\t\t\tstatus: JobStatus.FAILED,\n\t\t\t\t\tupdatedAt: { $lt: cutoff },\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (deletions.length > 0) {\n\t\t\tawait Promise.all(deletions);\n\t\t}\n\t}\n}\n","import { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport type { Collection, Db, Document, ObjectId, WithId } from 'mongodb';\n\nimport type { MonqueEventMap } from '@/events';\nimport {\n\ttype BulkOperationResult,\n\ttype CursorOptions,\n\ttype CursorPage,\n\tdocumentToPersistedJob,\n\ttype EnqueueOptions,\n\ttype GetJobsFilter,\n\ttype Job,\n\ttype JobHandler,\n\ttype JobSelector,\n\tJobStatus,\n\ttype PersistedJob,\n\ttype QueueStats,\n\ttype ScheduleOptions,\n} from '@/jobs';\nimport { ConnectionError, ShutdownTimeoutError, WorkerRegistrationError } from '@/shared';\nimport type { WorkerOptions, WorkerRegistration } from '@/workers';\n\nimport {\n\tChangeStreamHandler,\n\tJobManager,\n\tJobProcessor,\n\tJobQueryService,\n\tJobScheduler,\n\tLifecycleManager,\n\ttype ResolvedMonqueOptions,\n\ttype SchedulerContext,\n} from './services/index.js';\nimport type { MonqueOptions } from './types.js';\n\n/**\n * Default configuration values\n */\nconst DEFAULTS = {\n\tcollectionName: 'monque_jobs',\n\tpollInterval: 1000,\n\tsafetyPollInterval: 30_000,\n\tmaxRetries: 10,\n\tbaseRetryInterval: 1000,\n\tshutdownTimeout: 30000,\n\tworkerConcurrency: 5,\n\tlockTimeout: 1_800_000, // 30 minutes\n\trecoverStaleJobs: true,\n\theartbeatInterval: 30000, // 30 seconds\n\tretentionInterval: 3600_000, // 1 hour\n} as const;\n\n/**\n * Monque - MongoDB-backed job scheduler\n *\n * A type-safe job scheduler with atomic locking, exponential backoff, cron scheduling,\n * stale job recovery, and event-driven observability. Built on native MongoDB driver.\n *\n * @example Complete lifecycle\n * ```typescript\n * import { Monque } from '@monque/core';\n * import { MongoClient } from 'mongodb';\n *\n * const client = new MongoClient('mongodb://localhost:27017');\n * await client.connect();\n * const db = client.db('myapp');\n *\n * // Create instance with options\n * const monque = new Monque(db, {\n * collectionName: 'jobs',\n * pollInterval: 1000,\n * maxRetries: 10,\n * shutdownTimeout: 30000,\n * });\n *\n * // Initialize (sets up indexes and recovers stale jobs)\n * await monque.initialize();\n *\n * // Register workers with type safety\n * type EmailJob = {\n * to: string;\n * subject: string;\n * body: string;\n * };\n *\n * monque.register<EmailJob>('send-email', async (job) => {\n * await emailService.send(job.data.to, job.data.subject, job.data.body);\n * });\n *\n * // Monitor events for observability\n * monque.on('job:complete', ({ job, duration }) => {\n * logger.info(`Job ${job.name} completed in ${duration}ms`);\n * });\n *\n * monque.on('job:fail', ({ job, error, willRetry }) => {\n * logger.error(`Job ${job.name} failed:`, error);\n * });\n *\n * // Start processing\n * monque.start();\n *\n * // Enqueue jobs\n * await monque.enqueue('send-email', {\n * to: 'user@example.com',\n * subject: 'Welcome!',\n * body: 'Thanks for signing up.'\n * });\n *\n * // Graceful shutdown\n * process.on('SIGTERM', async () => {\n * await monque.stop();\n * await client.close();\n * process.exit(0);\n * });\n * ```\n */\nexport class Monque extends EventEmitter {\n\tprivate readonly db: Db;\n\tprivate readonly options: ResolvedMonqueOptions;\n\tprivate collection: Collection<Document> | null = null;\n\tprivate workers: Map<string, WorkerRegistration> = new Map();\n\tprivate isRunning = false;\n\tprivate isInitialized = false;\n\n\t// Internal services (initialized in initialize())\n\tprivate _scheduler: JobScheduler | null = null;\n\tprivate _manager: JobManager | null = null;\n\tprivate _query: JobQueryService | null = null;\n\tprivate _processor: JobProcessor | null = null;\n\tprivate _changeStreamHandler: ChangeStreamHandler | null = null;\n\tprivate _lifecycleManager: LifecycleManager | null = null;\n\n\tconstructor(db: Db, options: MonqueOptions = {}) {\n\t\tsuper();\n\t\tthis.db = db;\n\t\tthis.options = {\n\t\t\tcollectionName: options.collectionName ?? DEFAULTS.collectionName,\n\t\t\tpollInterval: options.pollInterval ?? DEFAULTS.pollInterval,\n\t\t\tsafetyPollInterval: options.safetyPollInterval ?? DEFAULTS.safetyPollInterval,\n\t\t\tmaxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n\t\t\tbaseRetryInterval: options.baseRetryInterval ?? DEFAULTS.baseRetryInterval,\n\t\t\tshutdownTimeout: options.shutdownTimeout ?? DEFAULTS.shutdownTimeout,\n\t\t\tworkerConcurrency:\n\t\t\t\toptions.workerConcurrency ?? options.defaultConcurrency ?? DEFAULTS.workerConcurrency,\n\t\t\tlockTimeout: options.lockTimeout ?? DEFAULTS.lockTimeout,\n\t\t\trecoverStaleJobs: options.recoverStaleJobs ?? DEFAULTS.recoverStaleJobs,\n\t\t\tmaxBackoffDelay: options.maxBackoffDelay,\n\t\t\tinstanceConcurrency: options.instanceConcurrency ?? options.maxConcurrency,\n\t\t\tschedulerInstanceId: options.schedulerInstanceId ?? randomUUID(),\n\t\t\theartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,\n\t\t\tjobRetention: options.jobRetention,\n\t\t\tskipIndexCreation: options.skipIndexCreation ?? false,\n\t\t\tmaxPayloadSize: options.maxPayloadSize,\n\t\t\tstatsCacheTtlMs: options.statsCacheTtlMs ?? 5000,\n\t\t};\n\t}\n\n\t/**\n\t * Initialize the scheduler by setting up the MongoDB collection and indexes.\n\t * Must be called before start().\n\t *\n\t * @throws {ConnectionError} If collection or index creation fails\n\t */\n\tasync initialize(): Promise<void> {\n\t\tif (this.isInitialized) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.collection = this.db.collection(this.options.collectionName);\n\n\t\t\t// Create indexes for efficient queries (unless externally managed)\n\t\t\tif (!this.options.skipIndexCreation) {\n\t\t\t\tawait this.createIndexes();\n\t\t\t}\n\n\t\t\t// Recover stale jobs if enabled\n\t\t\tif (this.options.recoverStaleJobs) {\n\t\t\t\tawait this.recoverStaleJobs();\n\t\t\t}\n\n\t\t\t// Check for instance ID collisions (after stale recovery to avoid false positives)\n\t\t\tawait this.checkInstanceCollision();\n\n\t\t\t// Initialize services with shared context\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis._scheduler = new JobScheduler(ctx);\n\t\t\tthis._manager = new JobManager(ctx);\n\t\t\tthis._query = new JobQueryService(ctx);\n\t\t\tthis._processor = new JobProcessor(ctx);\n\t\t\tthis._changeStreamHandler = new ChangeStreamHandler(ctx, (targetNames) =>\n\t\t\t\tthis.handleChangeStreamPoll(targetNames),\n\t\t\t);\n\t\t\tthis._lifecycleManager = new LifecycleManager(ctx);\n\n\t\t\tthis.isInitialized = true;\n\t\t} catch (error) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : 'Unknown error during initialization';\n\t\t\tthrow new ConnectionError(`Failed to initialize Monque: ${message}`);\n\t\t}\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Service Accessors (throw if not initialized)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get scheduler(): JobScheduler {\n\t\tif (!this._scheduler) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._scheduler;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get manager(): JobManager {\n\t\tif (!this._manager) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._manager;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get query(): JobQueryService {\n\t\tif (!this._query) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._query;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get processor(): JobProcessor {\n\t\tif (!this._processor) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._processor;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get changeStreamHandler(): ChangeStreamHandler {\n\t\tif (!this._changeStreamHandler) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._changeStreamHandler;\n\t}\n\n\t/** @throws {ConnectionError} if not initialized */\n\tprivate get lifecycleManager(): LifecycleManager {\n\t\tif (!this._lifecycleManager) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\n\t\treturn this._lifecycleManager;\n\t}\n\n\t/**\n\t * Handle a change-stream-triggered poll and reset the safety poll timer.\n\t *\n\t * Used as the `onPoll` callback for {@link ChangeStreamHandler}. Runs a\n\t * targeted poll for the given worker names, then resets the adaptive safety\n\t * poll timer so it doesn't fire redundantly.\n\t */\n\tprivate async handleChangeStreamPoll(targetNames?: ReadonlySet<string>): Promise<void> {\n\t\tawait this.processor.poll(targetNames);\n\t\tthis.lifecycleManager.resetPollTimer();\n\t}\n\n\t/**\n\t * Build the shared context for internal services.\n\t */\n\tprivate buildContext(): SchedulerContext {\n\t\tif (!this.collection) {\n\t\t\tthrow new ConnectionError('Collection not initialized');\n\t\t}\n\n\t\treturn {\n\t\t\tcollection: this.collection,\n\t\t\toptions: this.options,\n\t\t\tinstanceId: this.options.schedulerInstanceId,\n\t\t\tworkers: this.workers,\n\t\t\tisRunning: () => this.isRunning,\n\t\t\temit: <K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]) =>\n\t\t\t\tthis.emit(event, payload),\n\t\t\tnotifyPendingJob: (name: string, nextRunAt: Date) => {\n\t\t\t\tif (!this.isRunning || !this._changeStreamHandler) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis._changeStreamHandler.notifyPendingJob(name, nextRunAt);\n\t\t\t},\n\t\t\tdocumentToPersistedJob: <T>(doc: WithId<Document>) => documentToPersistedJob<T>(doc),\n\t\t};\n\t}\n\t/**\n\t * Create required MongoDB indexes for efficient job processing.\n\t *\n\t * The following indexes are created:\n\t * - `{status, nextRunAt}` - For efficient job polling queries\n\t * - `{name, uniqueKey}` - Partial unique index for deduplication (pending/processing only)\n\t * - `{name, status}` - For job lookup by type\n\t * - `{claimedBy, status}` - For finding jobs owned by a specific scheduler instance\n\t * - `{lastHeartbeat, status}` - For monitoring/debugging queries (e.g., inspecting heartbeat age)\n\t * - `{status, nextRunAt, claimedBy}` - For atomic claim queries (find unclaimed pending jobs)\n\t * - `{lockedAt, lastHeartbeat, status}` - Supports recovery scans and monitoring access patterns\n\t */\n\tprivate async createIndexes(): Promise<void> {\n\t\tif (!this.collection) {\n\t\t\tthrow new ConnectionError('Collection not initialized');\n\t\t}\n\n\t\tawait this.collection.createIndexes([\n\t\t\t// Compound index for job polling - status + nextRunAt for efficient queries\n\t\t\t{ key: { status: 1, nextRunAt: 1 }, background: true },\n\t\t\t// Partial unique index for deduplication - scoped by name + uniqueKey\n\t\t\t// Only enforced where uniqueKey exists and status is pending/processing\n\t\t\t{\n\t\t\t\tkey: { name: 1, uniqueKey: 1 },\n\t\t\t\tunique: true,\n\t\t\t\tpartialFilterExpression: {\n\t\t\t\t\tuniqueKey: { $exists: true },\n\t\t\t\t\tstatus: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },\n\t\t\t\t},\n\t\t\t\tbackground: true,\n\t\t\t},\n\t\t\t// Index for job lookup by name\n\t\t\t{ key: { name: 1, status: 1 }, background: true },\n\t\t\t// Compound index for finding jobs claimed by a specific scheduler instance.\n\t\t\t// Used for heartbeat updates and cleanup on shutdown.\n\t\t\t{ key: { claimedBy: 1, status: 1 }, background: true },\n\t\t\t// Compound index for monitoring/debugging via heartbeat timestamps.\n\t\t\t// Note: stale recovery uses lockedAt + lockTimeout as the source of truth.\n\t\t\t{ key: { lastHeartbeat: 1, status: 1 }, background: true },\n\t\t\t// Compound index for atomic claim queries.\n\t\t\t// Optimizes the findOneAndUpdate query that claims unclaimed pending jobs.\n\t\t\t{ key: { status: 1, nextRunAt: 1, claimedBy: 1 }, background: true },\n\t\t\t// Expanded index that supports recovery scans (status + lockedAt) plus heartbeat monitoring patterns.\n\t\t\t{ key: { status: 1, lockedAt: 1, lastHeartbeat: 1 }, background: true },\n\t\t]);\n\t}\n\n\t/**\n\t * Recover stale jobs that were left in 'processing' status.\n\t * A job is considered stale if its `lockedAt` timestamp exceeds the configured `lockTimeout`.\n\t * Stale jobs are reset to 'pending' so they can be picked up by workers again.\n\t */\n\tprivate async recoverStaleJobs(): Promise<void> {\n\t\tif (!this.collection) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst staleThreshold = new Date(Date.now() - this.options.lockTimeout);\n\n\t\tconst result = await this.collection.updateMany(\n\t\t\t{\n\t\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\t\tlockedAt: { $lt: staleThreshold },\n\t\t\t},\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tstatus: JobStatus.PENDING,\n\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t},\n\t\t\t\t$unset: {\n\t\t\t\t\tlockedAt: '',\n\t\t\t\t\tclaimedBy: '',\n\t\t\t\t\tlastHeartbeat: '',\n\t\t\t\t\theartbeatInterval: '',\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tif (result.modifiedCount > 0) {\n\t\t\t// Emit event for recovered jobs\n\t\t\tthis.emit('stale:recovered', {\n\t\t\t\tcount: result.modifiedCount,\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Check if another active instance is using the same schedulerInstanceId.\n\t * Uses heartbeat staleness to distinguish active instances from crashed ones.\n\t *\n\t * Called after stale recovery to avoid false positives: stale recovery resets\n\t * jobs with old `lockedAt`, so only jobs with recent heartbeats remain.\n\t *\n\t * @throws {ConnectionError} If an active instance with the same ID is detected\n\t */\n\tprivate async checkInstanceCollision(): Promise<void> {\n\t\tif (!this.collection) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Look for any job currently claimed by this instance ID\n\t\t// that has a recent heartbeat (within 2× heartbeat interval = \"alive\" threshold)\n\t\tconst aliveThreshold = new Date(Date.now() - this.options.heartbeatInterval * 2);\n\n\t\tconst activeJob = await this.collection.findOne({\n\t\t\tclaimedBy: this.options.schedulerInstanceId,\n\t\t\tstatus: JobStatus.PROCESSING,\n\t\t\tlastHeartbeat: { $gte: aliveThreshold },\n\t\t});\n\n\t\tif (activeJob) {\n\t\t\tthrow new ConnectionError(\n\t\t\t\t`Another active Monque instance is using schedulerInstanceId \"${this.options.schedulerInstanceId}\". ` +\n\t\t\t\t\t`Found processing job \"${activeJob['name']}\" with recent heartbeat. ` +\n\t\t\t\t\t`Use a unique schedulerInstanceId or wait for the other instance to stop.`,\n\t\t\t);\n\t\t}\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Job Scheduling (delegates to JobScheduler)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Enqueue a job for processing.\n\t *\n\t * Jobs are stored in MongoDB and processed by registered workers. Supports\n\t * delayed execution via `runAt` and deduplication via `uniqueKey`.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. Completed or failed jobs don't block new jobs with the same key.\n\t *\n\t * Failed jobs are automatically retried with exponential backoff up to `maxRetries`\n\t * (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @param options - Scheduling and deduplication options\n\t * @returns Promise resolving to the created or existing job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Basic job enqueueing\n\t * ```typescript\n\t * await monque.enqueue('send-email', {\n\t * to: 'user@example.com',\n\t * subject: 'Welcome!',\n\t * body: 'Thanks for signing up.'\n\t * });\n\t * ```\n\t *\n\t * @example Delayed execution\n\t * ```typescript\n\t * const oneHourLater = new Date(Date.now() + 3600000);\n\t * await monque.enqueue('reminder', { message: 'Check in!' }, {\n\t * runAt: oneHourLater\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicates with unique key\n\t * ```typescript\n\t * await monque.enqueue('sync-user', { userId: '123' }, {\n\t * uniqueKey: 'sync-user-123'\n\t * });\n\t * // Subsequent enqueues with same uniqueKey return existing pending/processing job\n\t * ```\n\t *\n\t * @see {@link JobScheduler.enqueue}\n\t */\n\tasync enqueue<T>(name: string, data: T, options: EnqueueOptions = {}): Promise<PersistedJob<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.scheduler.enqueue(name, data, options);\n\t}\n\n\t/**\n\t * Enqueue a job for immediate processing.\n\t *\n\t * Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.\n\t * Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler\n\t * @returns Promise resolving to the created job document\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example Send email immediately\n\t * ```typescript\n\t * await monque.now('send-email', {\n\t * to: 'admin@example.com',\n\t * subject: 'Alert',\n\t * body: 'Immediate attention required'\n\t * });\n\t * ```\n\t *\n\t * @example Process order in background\n\t * ```typescript\n\t * const order = await createOrder(data);\n\t * await monque.now('process-order', { orderId: order.id });\n\t * return order; // Return immediately, processing happens async\n\t * ```\n\t *\n\t * @see {@link JobScheduler.now}\n\t */\n\tasync now<T>(name: string, data: T): Promise<PersistedJob<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.scheduler.now(name, data);\n\t}\n\n\t/**\n\t * Schedule a recurring job with a cron expression.\n\t *\n\t * Creates a job that automatically re-schedules itself based on the cron pattern.\n\t * Uses standard 5-field cron format: minute, hour, day of month, month, day of week.\n\t * Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.\n\t * After successful completion, the job is reset to `pending` status and scheduled\n\t * for its next run based on the cron expression.\n\t *\n\t * When a `uniqueKey` is provided, only one pending or processing job with that key\n\t * can exist. This prevents duplicate scheduled jobs on application restart.\n\t *\n\t * @template T - The job data payload type (must be JSON-serializable)\n\t * @param cron - Cron expression (5 fields or predefined expression)\n\t * @param name - Job type identifier, must match a registered worker\n\t * @param data - Job payload, will be passed to the worker handler on each run\n\t * @param options - Scheduling options (uniqueKey for deduplication)\n\t * @returns Promise resolving to the created job document with `repeatInterval` set\n\t * @throws {InvalidCronError} If cron expression is invalid\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t * @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`\n\t *\n\t * @example Hourly cleanup job\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'cleanup-temp-files', {\n\t * directory: '/tmp/uploads'\n\t * });\n\t * ```\n\t *\n\t * @example Prevent duplicate scheduled jobs with unique key\n\t * ```typescript\n\t * await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {\n\t * uniqueKey: 'hourly-report-sales'\n\t * });\n\t * // Subsequent calls with same uniqueKey return existing pending/processing job\n\t * ```\n\t *\n\t * @example Daily report at midnight (using predefined expression)\n\t * ```typescript\n\t * await monque.schedule('@daily', 'daily-report', {\n\t * reportType: 'sales',\n\t * recipients: ['analytics@example.com']\n\t * });\n\t * ```\n\t *\n\t * @see {@link JobScheduler.schedule}\n\t */\n\tasync schedule<T>(\n\t\tcron: string,\n\t\tname: string,\n\t\tdata: T,\n\t\toptions: ScheduleOptions = {},\n\t): Promise<PersistedJob<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.scheduler.schedule(cron, name, data, options);\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Job Management (delegates to JobManager)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Cancel a pending or scheduled job.\n\t *\n\t * Sets the job status to 'cancelled' and emits a 'job:cancelled' event.\n\t * If the job is already cancelled, this is a no-op and returns the job.\n\t * Cannot cancel jobs that are currently 'processing', 'completed', or 'failed'.\n\t *\n\t * @param jobId - The ID of the job to cancel\n\t * @returns The cancelled job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for cancellation\n\t *\n\t * @example Cancel a pending job\n\t * ```typescript\n\t * const job = await monque.enqueue('report', { type: 'daily' });\n\t * await monque.cancelJob(job._id.toString());\n\t * ```\n\t *\n\t * @see {@link JobManager.cancelJob}\n\t */\n\tasync cancelJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.cancelJob(jobId);\n\t}\n\n\t/**\n\t * Retry a failed or cancelled job.\n\t *\n\t * Resets the job to 'pending' status, clears failure count/reason, and sets\n\t * nextRunAt to now (immediate retry). Emits a 'job:retried' event.\n\t *\n\t * @param jobId - The ID of the job to retry\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is in an invalid state for retry (must be failed or cancelled)\n\t *\n\t * @example Retry a failed job\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * console.log(`Job ${job._id} failed, retrying manually...`);\n\t * await monque.retryJob(job._id.toString());\n\t * });\n\t * ```\n\t *\n\t * @see {@link JobManager.retryJob}\n\t */\n\tasync retryJob(jobId: string): Promise<PersistedJob<unknown> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.retryJob(jobId);\n\t}\n\n\t/**\n\t * Reschedule a pending job to run at a different time.\n\t *\n\t * Only works for jobs in 'pending' status.\n\t *\n\t * @param jobId - The ID of the job to reschedule\n\t * @param runAt - The new Date when the job should run\n\t * @returns The updated job, or null if not found\n\t * @throws {JobStateError} If job is not in pending state\n\t *\n\t * @example Delay a job by 1 hour\n\t * ```typescript\n\t * const nextHour = new Date(Date.now() + 60 * 60 * 1000);\n\t * await monque.rescheduleJob(jobId, nextHour);\n\t * ```\n\t *\n\t * @see {@link JobManager.rescheduleJob}\n\t */\n\tasync rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.rescheduleJob(jobId, runAt);\n\t}\n\n\t/**\n\t * Permanently delete a job.\n\t *\n\t * This action is irreversible. Emits a 'job:deleted' event upon success.\n\t * Can delete a job in any state.\n\t *\n\t * @param jobId - The ID of the job to delete\n\t * @returns true if deleted, false if job not found\n\t *\n\t * @example Delete a cleanup job\n\t * ```typescript\n\t * const deleted = await monque.deleteJob(jobId);\n\t * if (deleted) {\n\t * console.log('Job permanently removed');\n\t * }\n\t * ```\n\t *\n\t * @see {@link JobManager.deleteJob}\n\t */\n\tasync deleteJob(jobId: string): Promise<boolean> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.deleteJob(jobId);\n\t}\n\n\t/**\n\t * Cancel multiple jobs matching the given filter via a single updateMany call.\n\t *\n\t * Only cancels jobs in 'pending' status — the status guard is applied regardless\n\t * of what the filter specifies. Jobs in other states are silently skipped (not\n\t * matched by the query). Emits a 'jobs:cancelled' event with the count of\n\t * successfully cancelled jobs.\n\t *\n\t * @param filter - Selector for which jobs to cancel (name, status, date range)\n\t * @returns Result with count of cancelled jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Cancel all pending jobs for a queue\n\t * ```typescript\n\t * const result = await monque.cancelJobs({\n\t * name: 'email-queue',\n\t * status: 'pending'\n\t * });\n\t * console.log(`Cancelled ${result.count} jobs`);\n\t * ```\n\t *\n\t * @see {@link JobManager.cancelJobs}\n\t */\n\tasync cancelJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.cancelJobs(filter);\n\t}\n\n\t/**\n\t * Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.\n\t *\n\t * Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied\n\t * regardless of what the filter specifies. Jobs in other states are silently skipped.\n\t * Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.\n\t * Emits a 'jobs:retried' event with the count of successfully retried jobs.\n\t *\n\t * @param filter - Selector for which jobs to retry (name, status, date range)\n\t * @returns Result with count of retried jobs (errors array always empty for bulk ops)\n\t *\n\t * @example Retry all failed jobs\n\t * ```typescript\n\t * const result = await monque.retryJobs({\n\t * status: 'failed'\n\t * });\n\t * console.log(`Retried ${result.count} jobs`);\n\t * ```\n\t *\n\t * @see {@link JobManager.retryJobs}\n\t */\n\tasync retryJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.retryJobs(filter);\n\t}\n\n\t/**\n\t * Delete multiple jobs matching the given filter.\n\t *\n\t * Deletes jobs in any status. Uses a batch delete for efficiency.\n\t * Emits a 'jobs:deleted' event with the count of deleted jobs.\n\t * Does not emit individual 'job:deleted' events to avoid noise.\n\t *\n\t * @param filter - Selector for which jobs to delete (name, status, date range)\n\t * @returns Result with count of deleted jobs (errors array always empty for delete)\n\t *\n\t * @example Delete old completed jobs\n\t * ```typescript\n\t * const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\t * const result = await monque.deleteJobs({\n\t * status: 'completed',\n\t * olderThan: weekAgo\n\t * });\n\t * console.log(`Deleted ${result.count} jobs`);\n\t * ```\n\t *\n\t * @see {@link JobManager.deleteJobs}\n\t */\n\tasync deleteJobs(filter: JobSelector): Promise<BulkOperationResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.manager.deleteJobs(filter);\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Job Queries (delegates to JobQueryService)\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Get a single job by its MongoDB ObjectId.\n\t *\n\t * Useful for retrieving job details when you have a job ID from events,\n\t * logs, or stored references.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param id - The job's ObjectId\n\t * @returns Promise resolving to the job if found, null otherwise\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Look up job from event\n\t * ```typescript\n\t * monque.on('job:fail', async ({ job }) => {\n\t * // Later, retrieve the job to check its status\n\t * const currentJob = await monque.getJob(job._id);\n\t * console.log(`Job status: ${currentJob?.status}`);\n\t * });\n\t * ```\n\t *\n\t * @example Admin endpoint\n\t * ```typescript\n\t * app.get('/jobs/:id', async (req, res) => {\n\t * const job = await monque.getJob(new ObjectId(req.params.id));\n\t * if (!job) {\n\t * return res.status(404).json({ error: 'Job not found' });\n\t * }\n\t * res.json(job);\n\t * });\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getJob}\n\t */\n\tasync getJob<T = unknown>(id: ObjectId): Promise<PersistedJob<T> | null> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getJob<T>(id);\n\t}\n\n\t/**\n\t * Query jobs from the queue with optional filters.\n\t *\n\t * Provides read-only access to job data for monitoring, debugging, and\n\t * administrative purposes. Results are ordered by `nextRunAt` ascending.\n\t *\n\t * @template T - The expected type of the job data payload\n\t * @param filter - Optional filter criteria\n\t * @returns Promise resolving to array of matching jobs\n\t * @throws {ConnectionError} If scheduler not initialized\n\t *\n\t * @example Get all pending jobs\n\t * ```typescript\n\t * const pendingJobs = await monque.getJobs({ status: JobStatus.PENDING });\n\t * console.log(`${pendingJobs.length} jobs waiting`);\n\t * ```\n\t *\n\t * @example Get failed email jobs\n\t * ```typescript\n\t * const failedEmails = await monque.getJobs({\n\t * name: 'send-email',\n\t * status: JobStatus.FAILED,\n\t * });\n\t * for (const job of failedEmails) {\n\t * console.error(`Job ${job._id} failed: ${job.failReason}`);\n\t * }\n\t * ```\n\t *\n\t * @example Paginated job listing\n\t * ```typescript\n\t * const page1 = await monque.getJobs({ limit: 50, skip: 0 });\n\t * const page2 = await monque.getJobs({ limit: 50, skip: 50 });\n\t * ```\n\t *\n\t * @example Use with type guards from @monque/core\n\t * ```typescript\n\t * import { isPendingJob, isRecurringJob } from '@monque/core';\n\t *\n\t * const jobs = await monque.getJobs();\n\t * const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getJobs}\n\t */\n\tasync getJobs<T = unknown>(filter: GetJobsFilter = {}): Promise<PersistedJob<T>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getJobs<T>(filter);\n\t}\n\n\t/**\n\t * Get a paginated list of jobs using opaque cursors.\n\t *\n\t * Provides stable pagination for large job lists. Supports forward and backward\n\t * navigation, filtering, and efficient database access via index-based cursor queries.\n\t *\n\t * @template T - The job data payload type\n\t * @param options - Pagination options (cursor, limit, direction, filter)\n\t * @returns Page of jobs with next/prev cursors\n\t * @throws {InvalidCursorError} If the provided cursor is malformed\n\t * @throws {ConnectionError} If database operation fails or scheduler not initialized\n\t *\n\t * @example List pending jobs\n\t * ```typescript\n\t * const page = await monque.getJobsWithCursor({\n\t * limit: 20,\n\t * filter: { status: 'pending' }\n\t * });\n\t * const jobs = page.jobs;\n\t *\n\t * // Get next page\n\t * if (page.hasNextPage) {\n\t * const page2 = await monque.getJobsWithCursor({\n\t * cursor: page.cursor,\n\t * limit: 20\n\t * });\n\t * }\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getJobsWithCursor}\n\t */\n\tasync getJobsWithCursor<T = unknown>(options: CursorOptions = {}): Promise<CursorPage<T>> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getJobsWithCursor<T>(options);\n\t}\n\n\t/**\n\t * Get aggregate statistics for the job queue.\n\t *\n\t * Uses MongoDB aggregation pipeline for efficient server-side calculation.\n\t * Returns counts per status and optional average processing duration for completed jobs.\n\t *\n\t * Results are cached per unique filter with a configurable TTL (default 5s).\n\t * Set `statsCacheTtlMs: 0` to disable caching.\n\t *\n\t * @param filter - Optional filter to scope statistics by job name\n\t * @returns Promise resolving to queue statistics\n\t * @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout\n\t * @throws {ConnectionError} If database operation fails\n\t *\n\t * @example Get overall queue statistics\n\t * ```typescript\n\t * const stats = await monque.getQueueStats();\n\t * console.log(`Pending: ${stats.pending}, Failed: ${stats.failed}`);\n\t * ```\n\t *\n\t * @example Get statistics for a specific job type\n\t * ```typescript\n\t * const emailStats = await monque.getQueueStats({ name: 'send-email' });\n\t * console.log(`${emailStats.total} email jobs in queue`);\n\t * ```\n\t *\n\t * @see {@link JobQueryService.getQueueStats}\n\t */\n\tasync getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats> {\n\t\tthis.ensureInitialized();\n\t\treturn this.query.getQueueStats(filter);\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Worker Registration\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Register a worker to process jobs of a specific type.\n\t *\n\t * Workers can be registered before or after calling `start()`. Each worker\n\t * processes jobs concurrently up to its configured concurrency limit (default: 5).\n\t *\n\t * The handler function receives the full job object including metadata (`_id`, `status`,\n\t * `failCount`, etc.). If the handler throws an error, the job is retried with exponential\n\t * backoff up to `maxRetries` times. After exhausting retries, the job is marked as `failed`.\n\t *\n\t * Events are emitted during job processing: `job:start`, `job:complete`, `job:fail`, and `job:error`.\n\t *\n\t * **Duplicate Registration**: By default, registering a worker for a job name that already has\n\t * a worker will throw a `WorkerRegistrationError`. This fail-fast behavior prevents accidental\n\t * replacement of handlers. To explicitly replace a worker, pass `{ replace: true }`.\n\t *\n\t * @template T - The job data payload type for type-safe access to `job.data`\n\t * @param name - Job type identifier to handle\n\t * @param handler - Async function to execute for each job\n\t * @param options - Worker configuration\n\t * @param options.concurrency - Maximum concurrent jobs for this worker (default: `defaultConcurrency`)\n\t * @param options.replace - When `true`, replace existing worker instead of throwing error\n\t * @throws {WorkerRegistrationError} When a worker is already registered for `name` and `replace` is not `true`\n\t *\n\t * @example Basic email worker\n\t * ```typescript\n\t * interface EmailJob {\n\t * to: string;\n\t * subject: string;\n\t * body: string;\n\t * }\n\t *\n\t * monque.register<EmailJob>('send-email', async (job) => {\n\t * await emailService.send(job.data.to, job.data.subject, job.data.body);\n\t * });\n\t * ```\n\t *\n\t * @example Worker with custom concurrency\n\t * ```typescript\n\t * // Limit to 2 concurrent video processing jobs (resource-intensive)\n\t * monque.register('process-video', async (job) => {\n\t * await videoProcessor.transcode(job.data.videoId);\n\t * }, { concurrency: 2 });\n\t * ```\n\t *\n\t * @example Replacing an existing worker\n\t * ```typescript\n\t * // Replace the existing handler for 'send-email'\n\t * monque.register('send-email', newEmailHandler, { replace: true });\n\t * ```\n\t *\n\t * @example Worker with error handling\n\t * ```typescript\n\t * monque.register('sync-user', async (job) => {\n\t * try {\n\t * await externalApi.syncUser(job.data.userId);\n\t * } catch (error) {\n\t * // Job will retry with exponential backoff\n\t * // Delay = 2^failCount × baseRetryInterval (default: 1000ms)\n\t * throw new Error(`Sync failed: ${error.message}`);\n\t * }\n\t * });\n\t * ```\n\t */\n\tregister<T>(name: string, handler: JobHandler<T>, options: WorkerOptions = {}): void {\n\t\tconst concurrency = options.concurrency ?? this.options.workerConcurrency;\n\n\t\t// Check for existing worker and throw unless replace is explicitly true\n\t\tif (this.workers.has(name) && options.replace !== true) {\n\t\t\tthrow new WorkerRegistrationError(\n\t\t\t\t`Worker already registered for job name \"${name}\". Use { replace: true } to replace.`,\n\t\t\t\tname,\n\t\t\t);\n\t\t}\n\n\t\tthis.workers.set(name, {\n\t\t\thandler: handler as JobHandler,\n\t\t\tconcurrency,\n\t\t\tactiveJobs: new Map(),\n\t\t});\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Public API - Lifecycle\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Start polling for and processing jobs.\n\t *\n\t * Begins polling MongoDB at the configured interval (default: 1 second) to pick up\n\t * pending jobs and dispatch them to registered workers. Must call `initialize()` first.\n\t * Workers can be registered before or after calling `start()`.\n\t *\n\t * Jobs are processed concurrently up to each worker's configured concurrency limit.\n\t * The scheduler continues running until `stop()` is called.\n\t *\n\t * @example Basic startup\n\t * ```typescript\n\t * const monque = new Monque(db);\n\t * await monque.initialize();\n\t *\n\t * monque.register('send-email', emailHandler);\n\t * monque.register('process-order', orderHandler);\n\t *\n\t * monque.start(); // Begin processing jobs\n\t * ```\n\t *\n\t * @example With event monitoring\n\t * ```typescript\n\t * monque.on('job:start', (job) => {\n\t * logger.info(`Starting job ${job.name}`);\n\t * });\n\t *\n\t * monque.on('job:complete', ({ job, duration }) => {\n\t * metrics.recordJobDuration(job.name, duration);\n\t * });\n\t *\n\t * monque.on('job:fail', ({ job, error, willRetry }) => {\n\t * logger.error(`Job ${job.name} failed:`, error);\n\t * if (!willRetry) {\n\t * alerting.sendAlert(`Job permanently failed: ${job.name}`);\n\t * }\n\t * });\n\t *\n\t * monque.start();\n\t * ```\n\t *\n\t * @throws {ConnectionError} If scheduler not initialized (call `initialize()` first)\n\t */\n\tstart(): void {\n\t\tif (this.isRunning) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.isInitialized) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() before start().');\n\t\t}\n\n\t\tthis.isRunning = true;\n\n\t\t// Set up change streams as the primary notification mechanism\n\t\tthis.changeStreamHandler.setup();\n\n\t\t// Delegate timer management to LifecycleManager\n\t\tthis.lifecycleManager.startTimers({\n\t\t\tpoll: () => this.processor.poll(),\n\t\t\tupdateHeartbeats: () => this.processor.updateHeartbeats(),\n\t\t\tisChangeStreamActive: () => this.changeStreamHandler.isActive(),\n\t\t});\n\t}\n\n\t/**\n\t * Stop the scheduler gracefully, waiting for in-progress jobs to complete.\n\t *\n\t * Stops polling for new jobs and waits for all active jobs to finish processing.\n\t * Times out after the configured `shutdownTimeout` (default: 30 seconds), emitting\n\t * a `job:error` event with a `ShutdownTimeoutError` containing incomplete jobs.\n\t * On timeout, jobs still in progress are left as `processing` for stale job recovery.\n\t *\n\t * It's safe to call `stop()` multiple times - subsequent calls are no-ops if already stopped.\n\t *\n\t * @returns Promise that resolves when all jobs complete or timeout is reached\n\t *\n\t * @example Graceful application shutdown\n\t * ```typescript\n\t * process.on('SIGTERM', async () => {\n\t * console.log('Shutting down gracefully...');\n\t * await monque.stop(); // Wait for jobs to complete\n\t * await mongoClient.close();\n\t * process.exit(0);\n\t * });\n\t * ```\n\t *\n\t * @example With timeout handling\n\t * ```typescript\n\t * monque.on('job:error', ({ error }) => {\n\t * if (error.name === 'ShutdownTimeoutError') {\n\t * logger.warn('Forced shutdown after timeout:', error.incompleteJobs);\n\t * }\n\t * });\n\t *\n\t * await monque.stop();\n\t * ```\n\t */\n\n\tasync stop(): Promise<void> {\n\t\tif (!this.isRunning) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Stop all lifecycle timers FIRST to prevent new poll callbacks\n\t\t// This closes the race window where a queued poll tick could\n\t\t// check isRunning before the flag is set to false\n\t\tthis.lifecycleManager.stopTimers();\n\n\t\tthis.isRunning = false;\n\n\t\t// Clear stats cache for clean state on restart\n\t\tthis._query?.clearStatsCache();\n\n\t\t// Close change stream — catch-and-ignore per shutdown cleanup guideline\n\t\ttry {\n\t\t\tawait this.changeStreamHandler.close();\n\t\t} catch {\n\t\t\t// ignore errors during shutdown cleanup\n\t\t}\n\n\t\t// Wait for all active jobs to complete (with timeout)\n\t\tconst activeJobs = this.getActiveJobs();\n\t\tif (activeJobs.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Create a promise that resolves when all jobs are done\n\t\tlet checkInterval: ReturnType<typeof setInterval> | undefined;\n\t\tconst waitForJobs = new Promise<undefined>((resolve) => {\n\t\t\tcheckInterval = setInterval(() => {\n\t\t\t\tif (this.getActiveJobs().length === 0) {\n\t\t\t\t\tclearInterval(checkInterval);\n\t\t\t\t\tresolve(undefined);\n\t\t\t\t}\n\t\t\t}, 100);\n\t\t});\n\n\t\t// Race between job completion and timeout\n\t\tconst timeout = new Promise<'timeout'>((resolve) => {\n\t\t\tsetTimeout(() => resolve('timeout'), this.options.shutdownTimeout);\n\t\t});\n\n\t\tlet result: undefined | 'timeout';\n\n\t\ttry {\n\t\t\tresult = await Promise.race([waitForJobs, timeout]);\n\t\t} finally {\n\t\t\tif (checkInterval) {\n\t\t\t\tclearInterval(checkInterval);\n\t\t\t}\n\t\t}\n\n\t\tif (result === 'timeout') {\n\t\t\tconst incompleteJobs = this.getActiveJobsList();\n\n\t\t\tconst error = new ShutdownTimeoutError(\n\t\t\t\t`Shutdown timed out after ${this.options.shutdownTimeout}ms with ${incompleteJobs.length} incomplete jobs`,\n\t\t\t\tincompleteJobs,\n\t\t\t);\n\t\t\tthis.emit('job:error', { error });\n\t\t}\n\t}\n\n\t/**\n\t * Check if the scheduler is healthy (running and connected).\n\t *\n\t * Returns `true` when the scheduler is started, initialized, and has an active\n\t * MongoDB collection reference. Useful for health check endpoints and monitoring.\n\t *\n\t * A healthy scheduler:\n\t * - Has called `initialize()` successfully\n\t * - Has called `start()` and is actively polling\n\t * - Has a valid MongoDB collection reference\n\t *\n\t * @returns `true` if scheduler is running and connected, `false` otherwise\n\t *\n\t * @example Express health check endpoint\n\t * ```typescript\n\t * app.get('/health', (req, res) => {\n\t * const healthy = monque.isHealthy();\n\t * res.status(healthy ? 200 : 503).json({\n\t * status: healthy ? 'ok' : 'unavailable',\n\t * scheduler: healthy,\n\t * timestamp: new Date().toISOString()\n\t * });\n\t * });\n\t * ```\n\t *\n\t * @example Kubernetes readiness probe\n\t * ```typescript\n\t * app.get('/readyz', (req, res) => {\n\t * if (monque.isHealthy() && dbConnected) {\n\t * res.status(200).send('ready');\n\t * } else {\n\t * res.status(503).send('not ready');\n\t * }\n\t * });\n\t * ```\n\t *\n\t * @example Periodic health monitoring\n\t * ```typescript\n\t * setInterval(() => {\n\t * if (!monque.isHealthy()) {\n\t * logger.error('Scheduler unhealthy');\n\t * metrics.increment('scheduler.unhealthy');\n\t * }\n\t * }, 60000); // Check every minute\n\t * ```\n\t */\n\tisHealthy(): boolean {\n\t\treturn this.isRunning && this.isInitialized && this.collection !== null;\n\t}\n\n\t// ─────────────────────────────────────────────────────────────────────────────\n\t// Private Helpers\n\t// ─────────────────────────────────────────────────────────────────────────────\n\n\t/**\n\t * Ensure the scheduler is initialized before operations.\n\t *\n\t * @private\n\t * @throws {ConnectionError} If scheduler not initialized or collection unavailable\n\t */\n\tprivate ensureInitialized(): void {\n\t\tif (!this.isInitialized || !this.collection) {\n\t\t\tthrow new ConnectionError('Monque not initialized. Call initialize() first.');\n\t\t}\n\t}\n\n\t/**\n\t * Get array of active job IDs across all workers.\n\t *\n\t * @private\n\t * @returns Array of job ID strings currently being processed\n\t */\n\tprivate getActiveJobs(): string[] {\n\t\tconst activeJobs: string[] = [];\n\t\tfor (const worker of this.workers.values()) {\n\t\t\tactiveJobs.push(...worker.activeJobs.keys());\n\t\t}\n\t\treturn activeJobs;\n\t}\n\n\t/**\n\t * Get list of active job documents (for shutdown timeout error).\n\t *\n\t * @private\n\t * @returns Array of active Job objects\n\t */\n\tprivate getActiveJobsList(): Job[] {\n\t\tconst activeJobs: Job[] = [];\n\t\tfor (const worker of this.workers.values()) {\n\t\t\tactiveJobs.push(...worker.activeJobs.values());\n\t\t}\n\t\treturn activeJobs;\n\t}\n\n\t/**\n\t * Type-safe event emitter methods\n\t */\n\toverride emit<K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]): boolean {\n\t\treturn super.emit(event, payload);\n\t}\n\n\toverride on<K extends keyof MonqueEventMap>(\n\t\tevent: K,\n\t\tlistener: (payload: MonqueEventMap[K]) => void,\n\t): this {\n\t\treturn super.on(event, listener);\n\t}\n\n\toverride once<K extends keyof MonqueEventMap>(\n\t\tevent: K,\n\t\tlistener: (payload: MonqueEventMap[K]) => void,\n\t): this {\n\t\treturn super.once(event, listener);\n\t}\n\n\toverride off<K extends keyof MonqueEventMap>(\n\t\tevent: K,\n\t\tlistener: (payload: MonqueEventMap[K]) => void,\n\t): this {\n\t\treturn super.off(event, listener);\n\t}\n}\n"],"mappings":"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"}
|