groupmq-plus 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +722 -0
  3. package/dist/index.cjs +2567 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1300 -0
  6. package/dist/index.d.ts +1300 -0
  7. package/dist/index.js +2557 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lua/change-delay.lua +62 -0
  10. package/dist/lua/check-stalled.lua +86 -0
  11. package/dist/lua/clean-status.lua +64 -0
  12. package/dist/lua/cleanup-poisoned-group.lua +46 -0
  13. package/dist/lua/cleanup.lua +46 -0
  14. package/dist/lua/complete-and-reserve-next-with-metadata.lua +221 -0
  15. package/dist/lua/complete-with-metadata.lua +190 -0
  16. package/dist/lua/complete.lua +51 -0
  17. package/dist/lua/dead-letter.lua +86 -0
  18. package/dist/lua/enqueue-batch.lua +149 -0
  19. package/dist/lua/enqueue-flow.lua +107 -0
  20. package/dist/lua/enqueue.lua +154 -0
  21. package/dist/lua/get-active-count.lua +6 -0
  22. package/dist/lua/get-active-jobs.lua +6 -0
  23. package/dist/lua/get-delayed-count.lua +5 -0
  24. package/dist/lua/get-delayed-jobs.lua +5 -0
  25. package/dist/lua/get-unique-groups-count.lua +13 -0
  26. package/dist/lua/get-unique-groups.lua +15 -0
  27. package/dist/lua/get-waiting-count.lua +11 -0
  28. package/dist/lua/get-waiting-jobs.lua +15 -0
  29. package/dist/lua/heartbeat.lua +22 -0
  30. package/dist/lua/is-empty.lua +35 -0
  31. package/dist/lua/promote-delayed-jobs.lua +40 -0
  32. package/dist/lua/promote-delayed-one.lua +44 -0
  33. package/dist/lua/promote-staged.lua +70 -0
  34. package/dist/lua/record-job-result.lua +143 -0
  35. package/dist/lua/remove.lua +55 -0
  36. package/dist/lua/reserve-atomic.lua +114 -0
  37. package/dist/lua/reserve-batch.lua +141 -0
  38. package/dist/lua/reserve.lua +161 -0
  39. package/dist/lua/retry.lua +53 -0
  40. package/package.json +92 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["BaseAdapter","BaseAdapter","safeJsonParse","status: string | undefined","valid: Array<Status>","enabled: boolean","name: string","path","fs","delayMs: number | undefined","childrenIds: string[]","childrenArgs: string[]","JobEntity","parsed: Record<string, any>","data: T","jobs: Array<JobEntity<T>>","sleep","args: string[]","out: Array<ReservedJob<T>>","idSets: string[]","pipe","rows","uniqueIds: string[]","nextRunTime: number","CronParser","defaultBackoff: BackoffStrategy","fetchedJob: Promise<ReservedJob<T> | null>","job","job: ReservedJob<T> | void","inProgressItem: { job: ReservedJob<T>; ts: number }","hbTimer: NodeJS.Timeout | undefined","heartbeatDelayTimer: NodeJS.Timeout | undefined"],"sources":["../node_modules/.pnpm/@bull-board+api@6.13.0_@bull-board+ui@6.13.0/node_modules/@bull-board/api/dist/queueAdapters/base.js","../src/adapters/groupmq-bullboard-adapter.ts","../src/helpers.ts","../src/job.ts","../src/logger.ts","../src/lua/loader.ts","../src/queue.ts","../src/async-fifo-queue.ts","../src/worker.ts"],"sourcesContent":["\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.BaseAdapter = void 0;\nclass BaseAdapter {\n constructor(type, options = {}) {\n this.formatters = new Map();\n this._visibilityGuard = () => true;\n this.readOnlyMode = options.readOnlyMode === true;\n this.allowRetries = this.readOnlyMode ? false : options.allowRetries !== false;\n this.allowCompletedRetries = this.allowRetries && options.allowCompletedRetries !== false;\n this.prefix = options.prefix || '';\n this.delimiter = options.delimiter || '';\n this.description = options.description || '';\n this.displayName = options.displayName || '';\n this.type = type;\n this.externalJobUrl = options.externalJobUrl;\n }\n getDescription() {\n return this.description;\n }\n getDisplayName() {\n return this.displayName;\n }\n setFormatter(field, formatter) {\n this.formatters.set(field, formatter);\n }\n format(field, data, defaultValue = data) {\n const fieldFormatter = this.formatters.get(field);\n return typeof fieldFormatter === 'function' ? fieldFormatter(data) : defaultValue;\n }\n setVisibilityGuard(guard) {\n this._visibilityGuard = guard;\n }\n isVisible(request) {\n return this._visibilityGuard(request);\n }\n}\nexports.BaseAdapter = BaseAdapter;\n//# sourceMappingURL=base.js.map","import { BaseAdapter } from '@bull-board/api/dist/queueAdapters/base.js';\nimport type {\n JobCounts as BullBoardJobCounts,\n JobStatus as BullBoardJobStatus,\n QueueJob as BullBoardQueueJob,\n Status as BullBoardStatus,\n QueueType,\n} from '@bull-board/api/typings/app';\nimport type { Job } from '../job';\nimport type { Queue } from '../queue';\n\nexport type GroupMQBullBoardAdapterOptions = {\n readOnlyMode?: boolean;\n prefix?: string;\n delimiter?: string;\n description?: string;\n displayName?: string;\n};\n\nexport class BullBoardGroupMQAdapter<T = any> extends BaseAdapter {\n private queue: Queue<T>;\n private options: GroupMQBullBoardAdapterOptions;\n\n constructor(queue: Queue<T>, options: GroupMQBullBoardAdapterOptions = {}) {\n const libName = queue.namespace;\n super(libName as QueueType, options);\n this.queue = queue;\n this.options = options;\n }\n\n // ------------ Metadata ------------\n public getDescription(): string {\n return this.options.description || '';\n }\n\n public getDisplayName(): string {\n return this.options.displayName || '';\n }\n\n public getName(): string {\n const prefix = this.options.prefix || '';\n const delimiter = this.options.delimiter || '';\n return `${prefix}${delimiter}${this.queue.rawNamespace}`.replace(\n /(^[\\s:]+)|([\\s:]+$)/g,\n '',\n );\n }\n\n public async getRedisInfo(): Promise<string> {\n return this.queue.redis.info();\n }\n\n // ------------ Getters ------------\n public async getJob(\n id: string,\n ): Promise<BullBoardQueueJob | undefined | null> {\n return (await this.queue.getJob(id)) as unknown as BullBoardQueueJob;\n }\n\n public async getJobs(\n jobStatuses: BullBoardJobStatus[],\n start?: number,\n end?: number,\n ): Promise<BullBoardQueueJob[]> {\n const jobs = await this.queue.getJobsByStatus(\n jobStatuses as any,\n start,\n end,\n );\n // QueueJob's update and updateData methods mismatch Record<string, any> vs T\n return jobs as Omit<Job<T>, 'update' | 'updateData'>[];\n }\n\n public async getJobCounts(): Promise<BullBoardJobCounts> {\n const base = await this.queue.getJobCounts();\n const counts: BullBoardJobCounts = {\n latest: 0,\n active: base.active,\n waiting: base.waiting,\n 'waiting-children': base['waiting-children'],\n prioritized: base.prioritized,\n completed: base.completed,\n failed: base.failed,\n delayed: base.delayed,\n paused: base.paused,\n } as BullBoardJobCounts;\n return counts;\n }\n\n public async getJobLogs(_id: string): Promise<string[]> {\n return [];\n }\n\n public getStatuses(): BullBoardStatus[] {\n return [\n 'latest',\n 'active',\n 'waiting',\n 'waiting-children',\n 'prioritized',\n 'completed',\n 'failed',\n 'delayed',\n 'paused',\n ];\n }\n\n public getJobStatuses(): BullBoardJobStatus[] {\n return [\n 'active',\n 'waiting',\n 'waiting-children',\n 'prioritized',\n 'completed',\n 'failed',\n 'delayed',\n 'paused',\n ];\n }\n\n // ------------ Mutations (read-only stubs) ------------\n private assertWritable(): void {\n if (this.options.readOnlyMode) {\n throw new Error(\n 'This adapter is in read-only mode. Mutations are disabled.',\n );\n }\n }\n\n public async clean(jobStatus: any, graceTimeMs: number): Promise<void> {\n this.assertWritable();\n // Align with BullMQ adapter: delegate to queue.clean\n if (\n jobStatus !== 'completed' &&\n jobStatus !== 'failed' &&\n jobStatus !== 'delayed'\n )\n return;\n await this.queue.clean(graceTimeMs, Number.MAX_SAFE_INTEGER, jobStatus);\n }\n\n public async addJob(\n _name: string,\n data: any,\n options: any,\n ): Promise<BullBoardQueueJob> {\n this.assertWritable();\n const job = await this.queue.add({\n groupId: options.groupId ?? Math.random().toString(36).substring(2, 15),\n data: data,\n ...options,\n });\n // QueueJob's update and updateData methods mismatch Record<string, any> vs T\n return job as Omit<Job<T>, 'update' | 'updateData'>;\n }\n\n public async isPaused(): Promise<boolean> {\n return this.queue.isPaused();\n }\n\n public async pause(): Promise<void> {\n this.assertWritable();\n await this.queue.pause();\n }\n\n public async resume(): Promise<void> {\n this.assertWritable();\n await this.queue.resume();\n }\n\n public async empty(): Promise<void> {\n this.assertWritable();\n throw new Error('Not implemented');\n }\n\n public async promoteAll(): Promise<void> {\n this.assertWritable();\n throw new Error('Not implemented');\n }\n}\n","import type { Queue } from './queue';\nimport type { Worker } from './worker';\n\n/**\n * Wait for a queue to become empty\n * @param queue The queue to monitor\n * @param timeoutMs Maximum time to wait (default: 60 seconds)\n * @returns Promise that resolves when queue is empty or timeout is reached\n */\nexport async function waitForQueueToEmpty(\n queue: Queue,\n timeoutMs = 60_000,\n): Promise<boolean> {\n return queue.waitForEmpty(timeoutMs);\n}\n\n/**\n * Get status of all workers\n */\nexport function getWorkersStatus<T = any>(\n workers: Worker<T>[],\n): {\n total: number;\n processing: number;\n idle: number;\n workers: Array<{\n index: number;\n isProcessing: boolean;\n currentJob?: {\n jobId: string;\n groupId: string;\n processingTimeMs: number;\n };\n }>;\n} {\n const workersStatus = workers.map((worker, index) => {\n const currentJob = worker.getCurrentJob();\n return {\n index,\n isProcessing: worker.isProcessing(),\n currentJob: currentJob\n ? {\n jobId: currentJob.job.id,\n groupId: currentJob.job.groupId,\n processingTimeMs: currentJob.processingTimeMs,\n }\n : undefined,\n };\n });\n\n const processing = workersStatus.filter((w) => w.isProcessing).length;\n const idle = workersStatus.length - processing;\n\n return {\n total: workers.length,\n processing,\n idle,\n workers: workersStatus,\n };\n}\n","import type { Queue, ReservedJob } from './queue';\nimport type { Status } from './status';\n\nexport class Job<T = any> {\n public readonly queue: Queue<T>;\n public readonly id: string;\n public readonly name: string;\n public readonly data: T;\n public readonly groupId: string;\n public readonly attemptsMade: number;\n public readonly opts: { attempts: number; delay?: number };\n public readonly processedOn?: number;\n public readonly finishedOn?: number;\n public readonly failedReason: string;\n public readonly stacktrace?: string;\n public readonly returnvalue?: any;\n public readonly timestamp: number; // ms\n public readonly orderMs?: number;\n public readonly status: Status | 'unknown';\n\n constructor(args: {\n queue: Queue<T>;\n id: string;\n name?: string;\n data: T;\n groupId: string;\n attemptsMade: number;\n opts: { attempts: number; delay?: number };\n processedOn?: number;\n finishedOn?: number;\n failedReason?: string;\n stacktrace?: string;\n returnvalue?: any;\n timestamp: number;\n orderMs?: number;\n status?: Status | 'unknown';\n }) {\n this.queue = args.queue;\n this.id = args.id;\n this.name = args.name ?? 'groupmq';\n this.data = args.data;\n this.groupId = args.groupId;\n this.attemptsMade = args.attemptsMade;\n this.opts = args.opts;\n this.processedOn = args.processedOn;\n this.finishedOn = args.finishedOn;\n this.failedReason = args.failedReason as any;\n this.stacktrace = args.stacktrace as any;\n this.returnvalue = args.returnvalue;\n this.timestamp = args.timestamp;\n this.orderMs = args.orderMs;\n this.status = args.status ?? 'unknown';\n }\n\n async getState(): Promise<\n Status | 'stuck' | 'waiting-children' | 'prioritized' | 'unknown'\n > {\n return this.status ?? 'unknown';\n }\n\n toJSON() {\n return {\n id: this.id,\n name: this.name,\n data: this.data,\n groupId: this.groupId,\n attemptsMade: this.attemptsMade,\n opts: this.opts,\n processedOn: this.processedOn,\n finishedOn: this.finishedOn,\n failedReason: this.failedReason,\n stacktrace: this.stacktrace ? [this.stacktrace] : null,\n returnvalue: this.returnvalue,\n timestamp: this.timestamp,\n orderMs: this.orderMs,\n status: this.status,\n progress: 0, // Default progress value\n };\n }\n\n changeDelay(newDelay: number): Promise<boolean> {\n return this.queue.changeDelay(this.id, newDelay);\n }\n\n async promote(): Promise<void> {\n await this.queue.promote(this.id);\n }\n\n async remove(): Promise<void> {\n await this.queue.remove(this.id);\n }\n\n async retry(_state?: Extract<Status, 'completed' | 'failed'>): Promise<void> {\n await this.queue.retry(this.id);\n }\n\n async updateData(jobData: T): Promise<void> {\n await this.queue.updateData(this.id, jobData);\n }\n\n async update(jobData: T): Promise<void> {\n await this.updateData(jobData);\n }\n\n static fromReserved<T = any>(\n queue: Queue<T>,\n reserved: ReservedJob<T>,\n meta?: {\n processedOn?: number;\n finishedOn?: number;\n failedReason?: string;\n stacktrace?: string;\n returnvalue?: any;\n status?: Status | string;\n delayMs?: number;\n },\n ): Job<T> {\n return new Job<T>({\n queue,\n id: reserved.id,\n name: 'groupmq',\n data: reserved.data as T,\n groupId: reserved.groupId,\n attemptsMade: reserved.attempts,\n opts: {\n attempts: reserved.maxAttempts,\n delay: meta?.delayMs,\n },\n processedOn: meta?.processedOn,\n finishedOn: meta?.finishedOn,\n failedReason: meta?.failedReason,\n stacktrace: meta?.stacktrace,\n returnvalue: meta?.returnvalue,\n timestamp: reserved.timestamp ? reserved.timestamp : Date.now(),\n orderMs: reserved.orderMs,\n status: coerceStatus(meta?.status as any),\n });\n }\n\n /**\n * Create a Job from raw Redis hash data with optional known status\n * This avoids extra Redis lookups when status is already known\n */\n static fromRawHash<T = any>(\n queue: Queue<T>,\n id: string,\n raw: Record<string, string>,\n knownStatus?: Status | 'unknown',\n ): Job<T> {\n const groupId = raw.groupId ?? '';\n const payload = raw.data ? safeJsonParse(raw.data) : null;\n const attempts = raw.attempts ? parseInt(raw.attempts, 10) : 0;\n const maxAttempts = raw.maxAttempts\n ? parseInt(raw.maxAttempts, 10)\n : queue.maxAttemptsDefault;\n const timestampMs = raw.timestamp ? parseInt(raw.timestamp, 10) : 0;\n const orderMs = raw.orderMs ? parseInt(raw.orderMs, 10) : undefined;\n const delayUntil = raw.delayUntil ? parseInt(raw.delayUntil, 10) : 0;\n const processedOn = raw.processedOn\n ? parseInt(raw.processedOn, 10)\n : undefined;\n const finishedOn = raw.finishedOn\n ? parseInt(raw.finishedOn, 10)\n : undefined;\n const failedReason =\n (raw.failedReason ?? raw.lastErrorMessage) || undefined;\n const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || undefined;\n const returnvalue = raw.returnvalue\n ? safeJsonParse(raw.returnvalue)\n : undefined;\n\n return new Job<T>({\n queue,\n id,\n name: 'groupmq',\n data: payload as T,\n groupId,\n attemptsMade: attempts,\n opts: {\n attempts: maxAttempts,\n delay:\n delayUntil && delayUntil > Date.now()\n ? delayUntil - Date.now()\n : undefined,\n },\n processedOn,\n finishedOn,\n failedReason,\n stacktrace,\n returnvalue,\n timestamp: timestampMs || Date.now(),\n orderMs,\n status: knownStatus ?? coerceStatus(raw.status as any),\n });\n }\n\n static async fromStore<T = any>(\n queue: Queue<T>,\n id: string,\n ): Promise<Job<T>> {\n const jobKey = `${queue.namespace}:job:${id}`;\n const raw = await queue.redis.hgetall(jobKey);\n if (!raw || Object.keys(raw).length === 0) {\n throw new Error(`Job ${id} not found`);\n }\n\n const groupId = raw.groupId ?? '';\n const payload = raw.data ? safeJsonParse(raw.data) : null;\n const attempts = raw.attempts ? parseInt(raw.attempts, 10) : 0;\n const maxAttempts = raw.maxAttempts\n ? parseInt(raw.maxAttempts, 10)\n : queue.maxAttemptsDefault;\n const timestampMs = raw.timestamp ? parseInt(raw.timestamp, 10) : 0;\n const orderMs = raw.orderMs ? parseInt(raw.orderMs, 10) : undefined;\n const delayUntil = raw.delayUntil ? parseInt(raw.delayUntil, 10) : 0;\n const processedOn = raw.processedOn\n ? parseInt(raw.processedOn, 10)\n : undefined;\n const finishedOn = raw.finishedOn\n ? parseInt(raw.finishedOn, 10)\n : undefined;\n const failedReason =\n (raw.failedReason ?? raw.lastErrorMessage) || undefined;\n const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || undefined;\n const returnvalue = raw.returnvalue\n ? safeJsonParse(raw.returnvalue)\n : undefined;\n\n // Determine status\n const [inProcessing, inDelayed] = await Promise.all([\n queue.redis.zscore(`${queue.namespace}:processing`, id),\n queue.redis.zscore(`${queue.namespace}:delayed`, id),\n ]);\n\n let status: string | undefined = raw.status;\n if (inProcessing !== null) status = 'active';\n else if (inDelayed !== null) status = 'delayed';\n else if (groupId) {\n const inGroup = await queue.redis.zscore(\n `${queue.namespace}:g:${groupId}`,\n id,\n );\n if (inGroup !== null) status = 'waiting';\n }\n\n return new Job<T>({\n queue,\n id,\n name: 'groupmq',\n data: payload as T,\n groupId,\n attemptsMade: attempts,\n opts: {\n attempts: maxAttempts,\n delay:\n delayUntil && delayUntil > Date.now()\n ? delayUntil - Date.now()\n : undefined,\n },\n processedOn,\n finishedOn,\n failedReason,\n stacktrace,\n returnvalue,\n timestamp: timestampMs || Date.now(),\n orderMs,\n status: coerceStatus(status as any),\n });\n }\n}\n\nfunction safeJsonParse(input: string): any {\n try {\n return JSON.parse(input);\n } catch (_e) {\n return null;\n }\n}\n\nfunction coerceStatus(input?: string | Status): Status | 'unknown' {\n const valid: Array<Status> = [\n 'latest',\n 'active',\n 'waiting',\n 'waiting-children',\n 'prioritized',\n 'completed',\n 'failed',\n 'delayed',\n 'paused',\n ];\n if (!input) return 'unknown';\n if (valid.includes(input as Status)) return input as Status;\n return 'unknown';\n}\n","// Generic logger interface that works with different logger implementations\nexport interface LoggerInterface {\n warn(...args: any[]): void;\n info(...args: any[]): void;\n error(...args: any[]): void;\n debug(...args: any[]): void;\n}\n\n// Default logger implementation\nexport class Logger implements LoggerInterface {\n constructor(\n private readonly enabled: boolean,\n private readonly name: string,\n ) {}\n\n debug(...args: any[]) {\n if (this.enabled) {\n console.debug(`[${this.name}]`, ...args);\n }\n }\n\n info(...args: any[]) {\n if (this.enabled) {\n console.log(`[${this.name}]`, ...args);\n }\n }\n\n warn(...args: any[]) {\n if (this.enabled) {\n console.warn(`⚠️ [${this.name}]`, ...args);\n }\n }\n\n error(...args: any[]) {\n if (this.enabled) {\n console.error(`💥 [${this.name}]`, ...args);\n }\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type Redis from 'ioredis';\n\nexport type ScriptName =\n | 'enqueue'\n | 'enqueue-batch'\n | 'enqueue-flow'\n | 'reserve'\n | 'reserve-batch'\n | 'reserve-atomic'\n | 'complete'\n | 'complete-and-reserve-next-with-metadata'\n | 'complete-with-metadata'\n | 'retry'\n | 'heartbeat'\n | 'cleanup'\n | 'promote-delayed-jobs'\n | 'promote-delayed-one'\n | 'promote-staged'\n | 'change-delay'\n | 'get-active-count'\n | 'get-waiting-count'\n | 'get-delayed-count'\n | 'get-active-jobs'\n | 'get-waiting-jobs'\n | 'get-delayed-jobs'\n | 'get-unique-groups'\n | 'get-unique-groups-count'\n | 'cleanup-poisoned-group'\n | 'remove'\n | 'clean-status'\n | 'is-empty'\n | 'dead-letter'\n | 'record-job-result'\n | 'check-stalled';\n\nconst cacheByClient = new WeakMap<Redis, Map<ScriptName, string>>();\n\nfunction scriptPath(name: ScriptName): string {\n // Resolve Lua script path in both dev (TS) and prod (dist) builds.\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n\n const candidates = [\n // Dev: alongside loader.ts (src/lua/<name>.lua)\n path.join(currentDir, `${name}.lua`),\n // Prod: dist/lua/<name>.lua adjacent to built bundle directory\n path.join(currentDir, 'lua', `${name}.lua`),\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n // Fallback to first candidate; read will throw a helpful error if missing\n return candidates[0];\n}\n\nexport async function loadScript(\n client: Redis,\n name: ScriptName,\n): Promise<string> {\n let map = cacheByClient.get(client);\n if (!map) {\n map = new Map();\n cacheByClient.set(client, map);\n }\n const cached = map.get(name);\n if (cached) return cached;\n\n const file = scriptPath(name);\n const lua = fs.readFileSync(file, 'utf8');\n const sha = await (client as any).script('load', lua);\n map.set(name, sha as string);\n return sha as string;\n}\n\nexport async function evalScript<T = any>(\n client: Redis,\n name: ScriptName,\n argv: Array<string>,\n numKeys: number,\n): Promise<T> {\n const sha = await loadScript(client, name);\n return (client as any).evalsha(sha, numKeys, ...argv);\n}\n","import { randomUUID } from 'node:crypto';\nimport CronParser from 'cron-parser';\nimport type Redis from 'ioredis';\nimport { type Job, Job as JobEntity } from './job';\nimport { Logger, type LoggerInterface } from './logger';\nimport { evalScript } from './lua/loader';\nimport type { Status } from './status';\n\n/**\n * Options for configuring a GroupMQ queue\n */\nexport type QueueOptions = {\n /**\n * Logger configuration for queue operations and debugging.\n *\n * @default false (no logging)\n * @example true // Enable basic logging\n * @example customLogger // Use custom logger instance\n *\n * **When to enable:**\n * - Development: For debugging queue operations\n * - Production monitoring: For operational insights\n * - Troubleshooting: When investigating performance issues\n */\n logger?: LoggerInterface | boolean;\n\n /**\n * Redis client instance for queue operations.\n * Should be a connected ioredis client.\n *\n * @example new Redis('redis://localhost:6379')\n * @example new Redis({ host: 'localhost', port: 6379, db: 0 })\n */\n redis: Redis;\n\n /**\n * Unique namespace for this queue. Used to separate different queues in the same Redis instance.\n * Should be unique across your application to avoid conflicts.\n *\n * @example 'email-queue'\n * @example 'user-notifications'\n * @example 'data-processing'\n */\n namespace: string;\n\n /**\n * Maximum time in milliseconds a job can run before being considered failed.\n * Jobs that exceed this timeout will be retried or moved to failed state.\n *\n * @default 30000 (30 seconds)\n * @example 60000 // 1 minute timeout\n * @example 300000 // 5 minute timeout for long-running jobs\n *\n * **When to adjust:**\n * - Long-running jobs: Increase (5-30 minutes)\n * - Short jobs: Decrease (5-15 seconds) for faster failure detection\n * - External API calls: Consider API timeout + buffer\n * - Database operations: Consider query timeout + buffer\n */\n jobTimeoutMs?: number;\n\n /**\n * Default maximum number of retry attempts for failed jobs.\n * Can be overridden per job or per worker.\n *\n * @default 3\n * @example 5 // Retry failed jobs up to 5 times\n * @example 1 // Fail fast with minimal retries\n *\n * **When to override:**\n * - Critical jobs: Increase retries\n * - Non-critical jobs: Decrease retries\n * - Idempotent operations: Can safely retry more\n * - External API calls: Consider API reliability\n */\n maxAttempts?: number;\n\n /**\n * Maximum number of groups to scan when looking for available jobs.\n * Higher values may find more jobs but use more Redis resources.\n *\n * @default 20\n * @example 50 // Scan more groups for better job distribution\n * @example 10 // Reduce Redis load for simple queues\n *\n * **When to adjust:**\n * - Many groups: Increase (50-100) for better job distribution\n * - Few groups: Decrease (5-10) to reduce Redis overhead\n * - High job volume: Increase for better throughput\n * - Resource constraints: Decrease to reduce Redis load\n */\n reserveScanLimit?: number;\n\n /**\n * Maximum number of completed jobs to retain for inspection.\n * Jobs beyond this limit are automatically cleaned up.\n *\n * @default 0 (no retention)\n * @example 100 // Keep last 100 completed jobs\n * @example 1000 // Keep last 1000 completed jobs for analysis\n *\n * **When to adjust:**\n * - Debugging: Increase to investigate issues\n * - Memory constraints: Decrease to reduce Redis memory usage\n * - Compliance: Increase for audit requirements\n */\n keepCompleted?: number;\n\n /**\n * Maximum number of failed jobs to retain for inspection.\n * Jobs beyond this limit are automatically cleaned up.\n *\n * @default 0 (no retention)\n * @example 1000 // Keep last 1000 failed jobs for analysis\n * @example 10000 // Keep more failed jobs for trend analysis\n *\n * **When to adjust:**\n * - Error analysis: Increase to investigate failure patterns\n * - Memory constraints: Decrease to reduce Redis memory usage\n * - Compliance: Increase for audit requirements\n */\n keepFailed?: number;\n\n /**\n * TTL for scheduler lock in milliseconds.\n * Prevents multiple schedulers from running simultaneously.\n *\n * @default 1500\n * @example 3000 // 3 seconds for slower environments\n * @example 1000 // 1 second for faster environments\n */\n schedulerLockTtlMs?: number;\n\n /**\n * Ordering delay in milliseconds. When set, jobs with orderMs will be staged\n * and promoted only after orderMs + orderingDelayMs to ensure proper ordering\n * even when producers are out of sync.\n *\n * @default 0 (no staging, jobs processed immediately)\n * @example 200 // Wait 200ms to ensure all jobs arrive in order\n * @example 1000 // Wait 1 second for strict ordering\n *\n * **When to use:**\n * - Distributed producers with clock drift\n * - Strict timestamp ordering required\n * - Network latency between producers\n *\n * **Note:** Only applies to jobs with orderMs set. Jobs without orderMs\n * are never staged.\n */\n orderingDelayMs?: number;\n\n /**\n * Enable automatic job batching to reduce Redis load.\n * Jobs are buffered in memory and sent in batches.\n *\n * @default undefined (disabled)\n * @example true // Enable with defaults (size: 10, maxWaitMs: 10)\n * @example { size: 20, maxWaitMs: 5 } // Custom configuration\n *\n * **Trade-offs:**\n * - ✅ 10x fewer Redis calls (huge performance win)\n * - ✅ Higher throughput (5-10x improvement)\n * - ✅ Lower latency per add() call\n * - ⚠️ Jobs buffered in memory briefly before Redis\n * - ⚠️ If process crashes during batch window, those jobs are lost\n *\n * **When to use:**\n * - High job volume (>100 jobs/s)\n * - Using orderingDelayMs (already buffering)\n * - Network latency is a bottleneck\n * - Acceptable risk of losing jobs during crash (e.g., non-critical jobs)\n *\n * **When NOT to use:**\n * - Critical jobs that must be persisted immediately\n * - Very low volume (<10 jobs/s)\n * - Zero tolerance for data loss\n *\n * **Configuration:**\n * - size: Maximum jobs per batch (default: 10)\n * - maxWaitMs: Maximum time to wait before flushing (default: 10)\n *\n * **Safety:**\n * - Keep maxWaitMs small (10ms = very low risk)\n * - Batches are flushed on queue.close()\n * - Consider graceful shutdown handling\n */\n autoBatch?:\n | boolean\n | {\n size?: number;\n maxWaitMs?: number;\n };\n};\n\n/**\n * Configuration for repeating jobs\n */\nexport type RepeatOptions =\n | {\n /**\n * Repeat interval in milliseconds. Job will be created every N milliseconds.\n *\n * @example 60000 // Every minute\n * @example 3600000 // Every hour\n * @example 86400000 // Every day\n *\n * When to use:\n * - Simple intervals: Use for regular, predictable schedules\n * - High frequency: Good for sub-hour intervals\n * - Performance: More efficient than cron for simple intervals\n */\n every: number;\n }\n | {\n /**\n * Cron pattern for complex scheduling. Uses standard cron syntax with seconds.\n * Format: second minute hour day month dayOfWeek\n *\n * When to use:\n * - Complex schedules: Business hours, specific days, etc.\n * - Low frequency: Good for daily, weekly, monthly schedules\n * - Business logic: Align with business requirements\n *\n * Cron format uses standard syntax with seconds precision.\n */\n pattern: string;\n };\n\n/**\n * Options for a single job in a flow\n */\nexport type FlowJob<T = any> = {\n /**\n * Unique ID for the job. If not provided, a UUID will be generated.\n */\n jobId?: string;\n /**\n * Group ID for the job.\n */\n groupId: string;\n /**\n * Data for the job.\n */\n data: T;\n /**\n * Maximum number of retry attempts.\n */\n maxAttempts?: number;\n /**\n * Delay in milliseconds before the job becomes available.\n */\n delay?: number;\n /**\n * Priority/Order timestamp.\n */\n orderMs?: number;\n};\n\n/**\n * Options for creating a parent-child flow\n */\nexport type FlowOptions<PT = any, CT = any> = {\n /**\n * The parent job that will be triggered after all children complete.\n */\n parent: FlowJob<PT>;\n /**\n * List of child jobs that must complete before the parent starts.\n */\n children: FlowJob<CT>[];\n};\n\n/**\n * Options for adding a job to the queue\n *\n * @template T The type of data to store in the job\n */\nexport type AddOptions<T> = {\n /**\n * Group ID for this job. Jobs with the same groupId are processed sequentially (FIFO).\n * Only one job per group can be processed at a time.\n *\n * @example 'user-123' // All jobs for user 123\n * @example 'email-notifications' // All email jobs\n * @example 'order-processing' // All order-related jobs\n *\n * **Best practices:**\n * - Use meaningful group IDs (user ID, resource ID, etc.)\n * - Keep group IDs consistent for related jobs\n * - Avoid too many unique groups (can impact performance)\n */\n groupId: string;\n\n /**\n * The data payload for this job. Can be any serializable data.\n *\n * @example { userId: 123, email: 'user@example.com' }\n * @example { orderId: 'order-456', items: [...] }\n * @example 'simple string data'\n */\n data: T;\n\n /**\n * Custom ordering timestamp in milliseconds. Jobs are processed in orderMs order within each group.\n * If not provided, uses current timestamp (Date.now()).\n *\n * @default Date.now()\n * @example Date.now() + 5000 // Process 5 seconds from now\n * @example 1640995200000 // Specific timestamp\n *\n * **When to use:**\n * - Delayed processing: Set future timestamp\n * - Priority ordering: Use lower timestamps for higher priority\n * - Batch processing: Group related jobs with same timestamp\n */\n orderMs?: number;\n\n /**\n * Maximum number of retry attempts for this specific job.\n * Overrides the queue's default maxAttempts setting.\n *\n * @default queue.maxAttemptsDefault\n * @example 5 // Retry this job up to 5 times\n * @example 1 // Fail fast with no retries\n *\n * **When to override:**\n * - Critical jobs: Increase retries\n * - Non-critical jobs: Decrease retries\n * - Idempotent operations: Can safely retry more\n * - External API calls: Consider API reliability\n */\n maxAttempts?: number;\n\n /**\n * Delay in milliseconds before this job becomes available for processing.\n * Alternative to using orderMs for simple delays.\n *\n * @example 5000 // Process after 5 seconds\n * @example 300000 // Process after 5 minutes\n *\n * **When to use:**\n * - Simple delays: Use delay instead of orderMs\n * - Rate limiting: Delay jobs to spread load\n * - Retry backoff: Delay retry attempts\n */\n delay?: number;\n\n /**\n * Specific time when this job should be processed.\n * Can be a Date object or timestamp in milliseconds.\n *\n * @example new Date('2024-01-01T12:00:00Z')\n * @example Date.now() + 3600000 // 1 hour from now\n *\n * **When to use:**\n * - Scheduled processing: Process at specific time\n * - Business hours: Schedule during working hours\n * - Maintenance windows: Schedule during low-traffic periods\n */\n runAt?: Date | number;\n\n /**\n * Configuration for repeating jobs (cron or interval-based).\n * Creates a repeating job that generates new instances automatically.\n *\n * @example { every: 60000 } // Every minute\n *\n * When to use:\n * - Periodic tasks: Regular cleanup, reports, etc.\n * - Monitoring: Health checks, metrics collection\n * - Maintenance: Regular database cleanup, cache warming\n */\n repeat?: RepeatOptions;\n\n /**\n * Custom job ID for idempotence. If a job with this ID already exists,\n * the new job will be ignored (idempotent behavior).\n *\n * @example 'user-123-email-welcome'\n * @example 'order-456-payment-process'\n *\n * **When to use:**\n * - Idempotent operations: Prevent duplicate processing\n * - External system integration: Use external IDs\n * - Retry scenarios: Ensure same job isn't added multiple times\n * - Deduplication: Prevent duplicate jobs from being created\n */\n jobId?: string;\n};\n\nexport type ReservedJob<T = any> = {\n id: string;\n groupId: string;\n data: T;\n attempts: number;\n maxAttempts: number;\n seq: number;\n timestamp: number; // ms\n orderMs: number;\n score: number;\n deadlineAt: number;\n};\n\nfunction nsKey(ns: string, ...parts: string[]) {\n return [ns, ...parts].join(':');\n}\n\nfunction safeJsonParse(input: string): any {\n try {\n return JSON.parse(input);\n } catch (_e) {\n return null;\n }\n}\n\nexport class Queue<T = any> {\n private logger: LoggerInterface;\n private r: Redis;\n private rawNs: string;\n private ns: string;\n private vt: number;\n private defaultMaxAttempts: number;\n private scanLimit: number;\n private keepCompleted: number;\n\n private keepFailed: number;\n private schedulerLockTtlMs: number;\n public orderingDelayMs: number;\n public name: string;\n\n // Internal tracking for adaptive behavior\n private _consecutiveEmptyReserves = 0;\n\n // Promoter service for staging system\n private promoterRedis?: Redis;\n private promoterRunning = false;\n private promoterLockId?: string;\n private promoterInterval?: NodeJS.Timeout;\n\n // Auto-batching for high-throughput scenarios\n private batchConfig?: { size: number; maxWaitMs: number };\n private batchBuffer: Array<{\n groupId: string;\n data: T | null;\n jobId: string;\n maxAttempts: number;\n delayMs?: number;\n orderMs?: number;\n resolve: (job: Job<T>) => void;\n reject: (err: Error) => void;\n }> = [];\n private batchTimer?: NodeJS.Timeout;\n private flushing = false;\n\n // Inline defineCommand bindings removed; using external Lua via evalsha\n\n constructor(opts: QueueOptions) {\n // Use the provided Redis client for main operations to preserve connection semantics\n // and a dedicated duplicate for blocking operations.\n this.r = opts.redis;\n this.rawNs = opts.namespace;\n this.name = opts.namespace;\n this.ns = `groupmq:${this.rawNs}`;\n const rawVt = opts.jobTimeoutMs ?? 30_000;\n this.vt = Math.max(1, rawVt); // Minimum 1ms\n this.defaultMaxAttempts = opts.maxAttempts ?? 3;\n this.scanLimit = opts.reserveScanLimit ?? 20;\n this.keepCompleted = Math.max(0, opts.keepCompleted ?? 0);\n this.keepFailed = Math.max(0, opts.keepFailed ?? 0);\n this.schedulerLockTtlMs = opts.schedulerLockTtlMs ?? 1500;\n this.orderingDelayMs = opts.orderingDelayMs ?? 0;\n\n // Initialize auto-batching if enabled\n if (opts.autoBatch) {\n this.batchConfig =\n typeof opts.autoBatch === 'boolean'\n ? { size: 10, maxWaitMs: 10 }\n : {\n size: opts.autoBatch.size ?? 10,\n maxWaitMs: opts.autoBatch.maxWaitMs ?? 10,\n };\n }\n\n // Initialize logger first\n this.logger =\n typeof opts.logger === 'object'\n ? opts.logger\n : new Logger(!!opts.logger, this.namespace);\n\n this.r.on('error', (err) => {\n this.logger.error('Redis error (main):', err);\n });\n }\n\n get redis(): Redis {\n return this.r;\n }\n\n get namespace(): string {\n return this.ns;\n }\n\n get rawNamespace(): string {\n return this.rawNs;\n }\n\n get jobTimeoutMs(): number {\n return this.vt;\n }\n\n get maxAttemptsDefault(): number {\n return this.defaultMaxAttempts;\n }\n\n async add(opts: AddOptions<T>): Promise<JobEntity<T>> {\n const maxAttempts = opts.maxAttempts ?? this.defaultMaxAttempts;\n const orderMs = opts.orderMs ?? Date.now();\n const now = Date.now();\n const jobId = opts.jobId ?? randomUUID();\n\n if (opts.repeat) {\n // Keep existing behavior for repeating jobs (returns a repeat key string)\n return this.addRepeatingJob({ ...opts, orderMs, maxAttempts });\n }\n\n // Calculate delay\n let delayMs: number | undefined;\n if (opts.delay !== undefined && opts.delay > 0) {\n delayMs = opts.delay;\n } else if (opts.runAt !== undefined) {\n const runAtTimestamp =\n opts.runAt instanceof Date ? opts.runAt.getTime() : opts.runAt;\n delayMs = Math.max(0, runAtTimestamp - now);\n }\n\n // Handle undefined data by converting to null for consistent JSON serialization\n const data = opts.data === undefined ? null : (opts.data as T);\n\n // Use batching if enabled\n if (this.batchConfig) {\n return new Promise((resolve, reject) => {\n this.batchBuffer.push({\n groupId: opts.groupId,\n data,\n jobId,\n maxAttempts,\n delayMs,\n orderMs,\n resolve,\n reject,\n });\n\n // Flush if batch is full\n if (this.batchBuffer.length >= this.batchConfig!.size) {\n this.flushBatch();\n } else if (!this.batchTimer) {\n // Start timer for partial batch\n this.batchTimer = setTimeout(\n () => this.flushBatch(),\n this.batchConfig!.maxWaitMs,\n );\n }\n });\n }\n\n // Non-batched path (original logic)\n return this.addSingle({\n ...opts,\n data,\n jobId,\n maxAttempts,\n orderMs,\n delayMs,\n });\n }\n\n /**\n * Adds a parent-child flow to the queue.\n * The parent job will only be processed after all child jobs have completed successfully.\n * This operation is atomic.\n *\n * @param flow The flow configuration containing parent and children jobs\n * @returns The parent job entity\n */\n async addFlow<PT = any, CT = any>(\n flow: FlowOptions<PT, CT>,\n ): Promise<JobEntity<PT>> {\n const parentId = flow.parent.jobId ?? randomUUID();\n const parentMaxAttempts =\n flow.parent.maxAttempts ?? this.defaultMaxAttempts;\n const parentOrderMs = flow.parent.orderMs ?? Date.now();\n const parentData = JSON.stringify(\n flow.parent.data === undefined ? null : flow.parent.data,\n );\n\n const childrenIds: string[] = [];\n const childrenArgs: string[] = [];\n\n for (const child of flow.children) {\n const childId = child.jobId ?? randomUUID();\n const childMaxAttempts = child.maxAttempts ?? this.defaultMaxAttempts;\n const childOrderMs = child.orderMs ?? Date.now();\n const childDelay = child.delay ?? 0;\n const childData = JSON.stringify(\n child.data === undefined ? null : child.data,\n );\n\n childrenIds.push(childId);\n childrenArgs.push(\n childId,\n child.groupId,\n childData,\n childMaxAttempts.toString(),\n childOrderMs.toString(),\n childDelay.toString(),\n );\n }\n\n const now = Date.now();\n\n // KEYS: [ns]\n // ARGV: [parentId, parentGroupId, parentData, parentMaxAttempts, parentOrderMs, now, ...childrenArgs]\n await evalScript(\n this.r,\n 'enqueue-flow',\n [\n this.ns,\n parentId,\n flow.parent.groupId,\n parentData,\n parentMaxAttempts.toString(),\n parentOrderMs.toString(),\n now.toString(),\n ...childrenArgs,\n ],\n 1,\n );\n\n return new JobEntity({\n queue: this as any,\n id: parentId,\n groupId: flow.parent.groupId,\n data: flow.parent.data,\n status: 'waiting-children',\n attemptsMade: 0,\n opts: { attempts: parentMaxAttempts },\n timestamp: now,\n orderMs: parentOrderMs,\n });\n }\n\n /**\n * Gets the number of remaining child jobs for a parent job in a flow.\n * @param parentId The ID of the parent job\n * @returns The number of remaining children, or null if the job is not a parent\n */\n async getFlowDependencies(parentId: string): Promise<number | null> {\n const remaining = await this.r.hget(\n `${this.ns}:job:${parentId}`,\n 'flowRemaining',\n );\n return remaining !== null ? parseInt(remaining, 10) : null;\n }\n\n /**\n * Gets the results of all child jobs in a flow.\n * @param parentId The ID of the parent job\n * @returns An object mapping child job IDs to their results\n */\n async getFlowResults(parentId: string): Promise<Record<string, any>> {\n const results = await this.r.hgetall(`${this.ns}:flow:results:${parentId}`);\n const parsed: Record<string, any> = {};\n for (const [id, val] of Object.entries(results)) {\n try {\n parsed[id] = JSON.parse(val);\n } catch (_e) {\n parsed[id] = val;\n }\n }\n return parsed;\n }\n\n private async addSingle(opts: {\n groupId: string;\n data: T | null;\n jobId: string;\n maxAttempts: number;\n orderMs: number;\n delayMs?: number;\n }): Promise<JobEntity<T>> {\n const now = Date.now();\n\n // Calculate delay timestamp\n let delayUntil = 0;\n if (opts.delayMs !== undefined && opts.delayMs > 0) {\n delayUntil = now + opts.delayMs;\n }\n\n const serializedPayload = JSON.stringify(opts.data);\n\n const result = await evalScript<string[] | string>(\n this.r,\n 'enqueue',\n [\n this.ns,\n opts.groupId,\n serializedPayload,\n String(opts.maxAttempts),\n String(opts.orderMs),\n String(delayUntil),\n String(opts.jobId),\n String(this.keepCompleted),\n String(now), // Pass client timestamp for accurate timing calculations\n String(this.orderingDelayMs), // Pass orderingDelayMs for staging logic\n ],\n 1,\n );\n\n // Handle new array format that includes job data (avoids race condition)\n // Format: [jobId, groupId, data, attempts, maxAttempts, timestamp, orderMs, delayUntil, status]\n if (Array.isArray(result)) {\n const [\n returnedJobId,\n returnedGroupId,\n returnedData,\n attempts,\n returnedMaxAttempts,\n timestamp,\n returnedOrderMs,\n returnedDelayUntil,\n status,\n ] = result;\n\n return JobEntity.fromRawHash<T>(\n this,\n returnedJobId,\n {\n id: returnedJobId,\n groupId: returnedGroupId,\n data: returnedData,\n attempts,\n maxAttempts: returnedMaxAttempts,\n timestamp,\n orderMs: returnedOrderMs,\n delayUntil: returnedDelayUntil,\n status,\n },\n status as any,\n );\n }\n\n // Fallback for old format (just jobId string) - this shouldn't happen with updated Lua script\n // but kept for backwards compatibility during rollout\n return this.getJob(result);\n }\n\n private async flushBatch(): Promise<void> {\n // Clear timer\n if (this.batchTimer) {\n clearTimeout(this.batchTimer);\n this.batchTimer = undefined;\n }\n\n if (this.batchBuffer.length === 0 || this.flushing) return;\n\n this.flushing = true;\n const batch = this.batchBuffer.splice(0); // Take all pending jobs\n\n try {\n this.logger.debug(`Flushing batch of ${batch.length} jobs`);\n const now = Date.now();\n\n // Prepare batch data for Lua script\n const jobsData = batch.map((job) => ({\n jobId: job.jobId,\n groupId: job.groupId,\n data: JSON.stringify(job.data),\n maxAttempts: job.maxAttempts,\n orderMs: job.orderMs,\n delayMs: job.delayMs,\n }));\n\n // Call batch enqueue Lua script\n // Returns array of job data arrays: [[jobId, groupId, data, attempts, maxAttempts, timestamp, orderMs, delayUntil, status], ...]\n const jobDataArrays = await evalScript<string[][]>(\n this.r,\n 'enqueue-batch',\n [\n this.ns,\n JSON.stringify(jobsData),\n String(this.keepCompleted),\n String(now),\n String(this.orderingDelayMs),\n ],\n 1,\n );\n\n // Resolve all promises with job entities\n for (let i = 0; i < batch.length; i++) {\n const job = batch[i];\n const jobDataArray = jobDataArrays[i];\n\n try {\n if (jobDataArray && jobDataArray.length >= 9) {\n const [\n returnedJobId,\n returnedGroupId,\n returnedData,\n attempts,\n returnedMaxAttempts,\n timestamp,\n returnedOrderMs,\n returnedDelayUntil,\n status,\n ] = jobDataArray;\n\n const jobEntity = JobEntity.fromRawHash<T>(\n this,\n returnedJobId,\n {\n id: returnedJobId,\n groupId: returnedGroupId,\n data: returnedData,\n attempts,\n maxAttempts: returnedMaxAttempts,\n timestamp,\n orderMs: returnedOrderMs,\n delayUntil: returnedDelayUntil,\n status,\n },\n status as any,\n );\n job.resolve(jobEntity);\n } else {\n throw new Error('Invalid job data returned from batch enqueue');\n }\n } catch (err) {\n job.reject(err instanceof Error ? err : new Error(String(err)));\n }\n }\n } catch (err) {\n // Reject all promises on error\n for (const job of batch) {\n job.reject(err instanceof Error ? err : new Error(String(err)));\n }\n } finally {\n this.flushing = false;\n\n // If there are jobs that accumulated during flush, flush them now\n if (this.batchBuffer.length > 0) {\n // Use setImmediate to avoid deep recursion\n setImmediate(() => this.flushBatch());\n }\n }\n }\n\n async reserve(): Promise<ReservedJob<T> | null> {\n const now = Date.now();\n\n const raw = await evalScript<string | null>(\n this.r,\n 'reserve',\n [this.ns, String(now), String(this.vt), String(this.scanLimit)],\n 1,\n );\n\n if (!raw) return null;\n\n const parts = raw.split('|||');\n if (parts.length !== 10) return null;\n\n let data: T;\n try {\n data = JSON.parse(parts[2]);\n } catch (err) {\n this.logger.warn(\n `Failed to parse job data: ${(err as Error).message}, raw: ${parts[2]}`,\n );\n data = null as T;\n }\n\n const parsedOrderMs = Number.parseInt(parts[7], 10);\n const job = {\n id: parts[0],\n groupId: parts[1],\n data,\n attempts: Number.parseInt(parts[3], 10),\n maxAttempts: Number.parseInt(parts[4], 10),\n seq: Number.parseInt(parts[5], 10),\n timestamp: Number.parseInt(parts[6], 10),\n orderMs: Number.isNaN(parsedOrderMs)\n ? Number.parseInt(parts[6], 10)\n : parsedOrderMs, // Fallback to timestamp if orderMs is NaN\n score: Number(parts[8]),\n deadlineAt: Number.parseInt(parts[9], 10),\n } as ReservedJob<T>;\n\n return job;\n }\n\n /**\n * Check how many jobs are waiting in a specific group\n */\n async getGroupJobCount(groupId: string): Promise<number> {\n const gZ = `${this.ns}:g:${groupId}`;\n return await this.r.zcard(gZ);\n }\n\n /**\n * Complete a job by removing from processing and unlocking the group.\n * Note: Job metadata recording is handled separately by recordCompleted().\n *\n * @deprecated Use completeWithMetadata() for internal operations. This method\n * is kept for backward compatibility and testing only.\n */\n async complete(job: { id: string; groupId: string }) {\n await evalScript<number>(\n this.r,\n 'complete',\n [this.ns, job.id, job.groupId],\n 1,\n );\n }\n\n /**\n * Complete a job AND record metadata in a single atomic operation.\n * This is the efficient internal method used by workers.\n */\n public async completeWithMetadata(\n job: { id: string; groupId: string },\n result: unknown,\n meta: {\n processedOn: number;\n finishedOn: number;\n attempts: number;\n maxAttempts: number;\n },\n ): Promise<void> {\n await evalScript<number>(\n this.r,\n 'complete-with-metadata',\n [\n this.ns,\n job.id,\n job.groupId,\n 'completed',\n String(meta.finishedOn),\n JSON.stringify(result ?? null),\n String(this.keepCompleted),\n String(this.keepFailed),\n String(meta.processedOn),\n String(meta.finishedOn),\n String(meta.attempts),\n String(meta.maxAttempts),\n ],\n 1,\n );\n }\n\n /**\n * Atomically complete a job and try to reserve the next job from the same group\n * This prevents race conditions where other workers can steal subsequent jobs from the same group\n */\n\n /**\n * Atomically complete a job with metadata and reserve the next job from the same group.\n */\n async completeAndReserveNextWithMetadata(\n completedJobId: string,\n groupId: string,\n handlerResult: unknown,\n meta: {\n processedOn: number;\n finishedOn: number;\n attempts: number;\n maxAttempts: number;\n },\n ): Promise<ReservedJob<T> | null> {\n const now = Date.now();\n\n try {\n const result = await evalScript<string | null>(\n this.r,\n 'complete-and-reserve-next-with-metadata',\n [\n this.ns,\n completedJobId,\n groupId,\n 'completed',\n String(meta.finishedOn),\n JSON.stringify(handlerResult ?? null),\n String(this.keepCompleted),\n String(this.keepFailed),\n String(meta.processedOn),\n String(meta.finishedOn),\n String(meta.attempts),\n String(meta.maxAttempts),\n String(now),\n String(this.jobTimeoutMs),\n ],\n 1,\n );\n\n if (!result) {\n return null;\n }\n\n // Parse the result (same format as reserve methods)\n const parts = result.split('|||');\n if (parts.length !== 10) {\n this.logger.error(\n 'Queue completeAndReserveNextWithMetadata: unexpected result format:',\n result,\n );\n return null;\n }\n\n const [\n id,\n ,\n data,\n attempts,\n maxAttempts,\n seq,\n enq,\n orderMs,\n score,\n deadline,\n ] = parts;\n\n return {\n id,\n groupId,\n data: JSON.parse(data),\n attempts: parseInt(attempts, 10),\n maxAttempts: parseInt(maxAttempts, 10),\n seq: parseInt(seq, 10),\n timestamp: parseInt(enq, 10),\n orderMs: parseInt(orderMs, 10),\n score: parseFloat(score),\n deadlineAt: parseInt(deadline, 10),\n };\n } catch (error) {\n this.logger.error(\n 'Queue completeAndReserveNextWithMetadata error:',\n error,\n );\n return null;\n }\n }\n\n /**\n * Check if a job is currently in processing state\n */\n async isJobProcessing(jobId: string): Promise<boolean> {\n const score = await this.r.zscore(`${this.ns}:processing`, jobId);\n return score !== null;\n }\n\n async retry(jobId: string, backoffMs = 0) {\n return evalScript<number>(\n this.r,\n 'retry',\n [this.ns, jobId, String(backoffMs)],\n\n 1,\n );\n }\n\n /**\n * Dead letter a job (remove from group and optionally store in dead letter queue)\n */\n async deadLetter(jobId: string, groupId: string) {\n return evalScript<number>(\n this.r,\n 'dead-letter',\n [this.ns, jobId, groupId],\n 1,\n );\n }\n\n /**\n * Record a successful completion for retention and inspection\n * Uses consolidated Lua script for atomic operation with retention management\n */\n async recordCompleted(\n job: { id: string; groupId: string },\n result: unknown,\n meta: {\n processedOn?: number;\n finishedOn?: number;\n attempts?: number;\n maxAttempts?: number;\n data?: unknown; // legacy\n },\n ): Promise<void> {\n const processedOn = meta.processedOn ?? Date.now();\n const finishedOn = meta.finishedOn ?? Date.now();\n const attempts = meta.attempts ?? 0;\n const maxAttempts = meta.maxAttempts ?? this.defaultMaxAttempts;\n\n try {\n await evalScript<number>(\n this.r,\n 'record-job-result',\n [\n this.ns,\n job.id,\n 'completed',\n String(finishedOn),\n JSON.stringify(result ?? null),\n String(this.keepCompleted),\n String(this.keepFailed),\n String(processedOn),\n String(finishedOn),\n String(attempts),\n String(maxAttempts),\n ],\n 1,\n );\n } catch (error) {\n this.logger.error(`Error recording completion for job ${job.id}:`, error);\n throw error;\n }\n }\n\n /**\n * Record a failure attempt (non-final), storing last error for visibility\n */\n async recordAttemptFailure(\n job: { id: string; groupId: string },\n error: { message?: string; name?: string; stack?: string } | string,\n meta: {\n processedOn?: number;\n finishedOn?: number;\n attempts?: number;\n maxAttempts?: number;\n },\n ): Promise<void> {\n const jobKey = `${this.ns}:job:${job.id}`;\n const processedOn = meta.processedOn ?? Date.now();\n const finishedOn = meta.finishedOn ?? Date.now();\n\n const message =\n typeof error === 'string' ? error : (error.message ?? 'Error');\n const name = typeof error === 'string' ? 'Error' : (error.name ?? 'Error');\n const stack = typeof error === 'string' ? '' : (error.stack ?? '');\n\n await this.r.hset(\n jobKey,\n 'lastErrorMessage',\n message,\n 'lastErrorName',\n name,\n 'lastErrorStack',\n stack,\n 'processedOn',\n String(processedOn),\n 'finishedOn',\n String(finishedOn),\n );\n }\n\n /**\n * Record a final failure (dead-lettered) for retention and inspection\n * Uses consolidated Lua script for atomic operation\n */\n async recordFinalFailure(\n job: { id: string; groupId: string },\n error: { message?: string; name?: string; stack?: string } | string,\n meta: {\n processedOn?: number;\n finishedOn?: number;\n attempts?: number;\n maxAttempts?: number;\n data?: unknown;\n },\n ): Promise<void> {\n const processedOn = meta.processedOn ?? Date.now();\n const finishedOn = meta.finishedOn ?? Date.now();\n const attempts = meta.attempts ?? 0;\n const maxAttempts = meta.maxAttempts ?? this.defaultMaxAttempts;\n\n const message =\n typeof error === 'string' ? error : (error.message ?? 'Error');\n const name = typeof error === 'string' ? 'Error' : (error.name ?? 'Error');\n const stack = typeof error === 'string' ? '' : (error.stack ?? '');\n\n // Package error info as JSON for Lua script\n const errorInfo = JSON.stringify({ message, name, stack });\n\n try {\n await evalScript<number>(\n this.r,\n 'record-job-result',\n [\n this.ns,\n job.id,\n 'failed',\n String(finishedOn),\n errorInfo,\n String(this.keepCompleted),\n String(this.keepFailed),\n String(processedOn),\n String(finishedOn),\n String(attempts),\n String(maxAttempts),\n ],\n 1,\n );\n } catch (err) {\n this.logger.error(\n `Error recording final failure for job ${job.id}:`,\n err,\n );\n throw err;\n }\n }\n\n async getCompleted(limit = this.keepCompleted): Promise<\n Array<{\n id: string;\n groupId: string;\n data: any;\n returnvalue: any;\n processedOn?: number;\n finishedOn?: number;\n attempts: number;\n maxAttempts: number;\n }>\n > {\n const completedKey = `${this.ns}:completed`;\n const ids = await this.r.zrevrange(completedKey, 0, Math.max(0, limit - 1));\n if (ids.length === 0) return [];\n const pipe = this.r.multi();\n for (const id of ids) {\n pipe.hmget(\n `${this.ns}:job:${id}`,\n 'groupId',\n 'data',\n 'returnvalue',\n 'processedOn',\n 'finishedOn',\n 'attempts',\n 'maxAttempts',\n );\n }\n const rows = (await pipe.exec()) ?? [];\n return ids.map((id, idx) => {\n const row = rows[idx]?.[1] as Array<string | null>;\n const [\n groupId,\n dataStr,\n retStr,\n processedOn,\n finishedOn,\n attempts,\n maxAttempts,\n ] = row || [];\n return {\n id,\n groupId: groupId || '',\n data: dataStr ? safeJsonParse(dataStr) : null,\n returnvalue: retStr ? safeJsonParse(retStr) : null,\n processedOn: processedOn ? parseInt(processedOn, 10) : undefined,\n finishedOn: finishedOn ? parseInt(finishedOn, 10) : undefined,\n attempts: attempts ? parseInt(attempts, 10) : 0,\n maxAttempts: maxAttempts\n ? parseInt(maxAttempts, 10)\n : this.defaultMaxAttempts,\n };\n });\n }\n\n async getFailed(limit = this.keepFailed): Promise<\n Array<{\n id: string;\n groupId: string;\n data: any;\n failedReason: string;\n stacktrace?: string;\n processedOn?: number;\n finishedOn?: number;\n attempts: number;\n maxAttempts: number;\n }>\n > {\n const failedKey = `${this.ns}:failed`;\n const ids = await this.r.zrevrange(failedKey, 0, Math.max(0, limit - 1));\n if (ids.length === 0) return [];\n const pipe = this.r.multi();\n for (const id of ids) {\n pipe.hmget(\n `${this.ns}:job:${id}`,\n 'groupId',\n 'data',\n 'failedReason',\n 'stacktrace',\n 'processedOn',\n 'finishedOn',\n 'attempts',\n 'maxAttempts',\n );\n }\n const rows = (await pipe.exec()) ?? [];\n return ids.map((id, idx) => {\n const row = rows[idx]?.[1] as Array<string | null>;\n const [\n groupId,\n dataStr,\n failedReason,\n stacktrace,\n processedOn,\n finishedOn,\n attempts,\n maxAttempts,\n ] = row || [];\n return {\n id,\n groupId: groupId || '',\n data: dataStr ? safeJsonParse(dataStr) : null,\n failedReason: failedReason || '',\n stacktrace: stacktrace || undefined,\n processedOn: processedOn ? parseInt(processedOn, 10) : undefined,\n finishedOn: finishedOn ? parseInt(finishedOn, 10) : undefined,\n attempts: attempts ? parseInt(attempts, 10) : 0,\n maxAttempts: maxAttempts\n ? parseInt(maxAttempts, 10)\n : this.defaultMaxAttempts,\n };\n });\n }\n\n /**\n * Convenience: return completed jobs as Job entities (non-breaking, new API)\n */\n async getCompletedJobs(\n limit = this.keepCompleted,\n ): Promise<Array<JobEntity<T>>> {\n const completedKey = `${this.ns}:completed`;\n const ids = await this.r.zrevrange(completedKey, 0, Math.max(0, limit - 1));\n if (ids.length === 0) return [];\n\n // Atomically fetch all job hashes in one pipeline\n const pipe = this.r.multi();\n for (const id of ids) {\n pipe.hgetall(`${this.ns}:job:${id}`);\n }\n const rows = await pipe.exec();\n\n // Construct jobs directly from pipeline data (atomic, no race condition)\n const jobs: Array<JobEntity<T>> = [];\n for (let i = 0; i < ids.length; i++) {\n const id = ids[i];\n const raw = (rows?.[i]?.[1] as Record<string, string>) || {};\n\n // Skip jobs that were already cleaned up\n if (!raw || Object.keys(raw).length === 0) {\n this.logger.warn(\n `Skipping completed job ${id} - not found (likely cleaned up)`,\n );\n continue;\n }\n\n const job = JobEntity.fromRawHash<T>(this, id, raw, 'completed');\n jobs.push(job);\n }\n return jobs;\n }\n\n /**\n * Convenience: return failed jobs as Job entities (non-breaking, new API)\n */\n async getFailedJobs(limit = this.keepFailed): Promise<Array<JobEntity<T>>> {\n const failedKey = `${this.ns}:failed`;\n const ids = await this.r.zrevrange(failedKey, 0, Math.max(0, limit - 1));\n if (ids.length === 0) return [];\n\n // Atomically fetch all job hashes in one pipeline\n const pipe = this.r.multi();\n for (const id of ids) {\n pipe.hgetall(`${this.ns}:job:${id}`);\n }\n const rows = await pipe.exec();\n\n // Construct jobs directly from pipeline data (atomic, no race condition)\n const jobs: Array<JobEntity<T>> = [];\n for (let i = 0; i < ids.length; i++) {\n const id = ids[i];\n const raw = (rows?.[i]?.[1] as Record<string, string>) || {};\n\n // Skip jobs that were already cleaned up\n if (!raw || Object.keys(raw).length === 0) {\n this.logger.warn(\n `Skipping failed job ${id} - not found (likely cleaned up)`,\n );\n continue;\n }\n\n const job = JobEntity.fromRawHash<T>(this, id, raw, 'failed');\n jobs.push(job);\n }\n return jobs;\n }\n\n async getCompletedCount(): Promise<number> {\n return this.r.zcard(`${this.ns}:completed`);\n }\n\n async getFailedCount(): Promise<number> {\n return this.r.zcard(`${this.ns}:failed`);\n }\n async heartbeat(job: { id: string; groupId: string }, extendMs = this.vt) {\n return evalScript<number>(\n this.r,\n 'heartbeat',\n [this.ns, job.id, job.groupId, String(extendMs)],\n 1,\n );\n }\n\n /**\n * Clean up expired jobs and stale data.\n * Uses distributed lock to ensure only one worker runs cleanup at a time,\n * similar to scheduler lock pattern.\n */\n async cleanup(): Promise<number> {\n // Try to acquire cleanup lock (similar to scheduler lock)\n const cleanupLockKey = `${this.ns}:cleanup:lock`;\n const ttlMs = 60000; // 60 seconds - longer than typical cleanup duration\n\n try {\n const acquired = await (this.r as any).set(\n cleanupLockKey,\n '1',\n 'PX',\n ttlMs,\n 'NX',\n );\n\n if (acquired !== 'OK') {\n // Another worker is running cleanup\n return 0;\n }\n\n // We have the lock, run cleanup\n const now = Date.now();\n return evalScript<number>(this.r, 'cleanup', [this.ns, String(now)], 1);\n } catch (_e) {\n return 0;\n }\n }\n\n /**\n * Calculate adaptive blocking timeout like BullMQ\n * Returns timeout in seconds\n *\n * Inspiration by BullMQ ⭐️\n */\n private getBlockTimeout(maxTimeout: number, blockUntil?: number): number {\n const minimumBlockTimeout = 0.001; // 1ms like BullMQ for fast job pickup\n const maximumBlockTimeout = 5; // 5s max to reduce idle CPU usage\n\n // Handle delayed jobs case (when we know exactly when next job should be processed)\n if (blockUntil) {\n const blockDelay = blockUntil - Date.now();\n\n // If we've reached the time to get new jobs\n if (blockDelay <= 0) {\n return minimumBlockTimeout; // Process immediately\n } else if (blockDelay < minimumBlockTimeout * 1000) {\n return minimumBlockTimeout; // Very short delay, use minimum\n } else {\n // Block until the delayed job is ready, but cap at maximum\n return Math.min(blockDelay / 1000, maximumBlockTimeout);\n }\n }\n\n // Use maxTimeout when draining (similar to BullMQ's drainDelay), but clamp to minimum\n // This keeps the worker responsive while balancing Redis load\n return Math.max(\n minimumBlockTimeout,\n Math.min(maxTimeout, maximumBlockTimeout),\n );\n }\n\n /**\n * Check if an error is a Redis connection error (should retry)\n * Conservative approach: only connection closed and ECONNREFUSED\n */\n isConnectionError(err: any): boolean {\n if (!err) return false;\n\n const message = `${err.message || ''}`;\n\n return (\n message === 'Connection is closed.' || message.includes('ECONNREFUSED')\n );\n }\n\n async reserveBlocking(\n timeoutSec = 5,\n blockUntil?: number,\n blockingClient?: import('ioredis').default,\n ): Promise<ReservedJob<T> | null> {\n const startTime = Date.now();\n\n // Short-circuit if paused\n if (await this.isPaused()) {\n await sleep(50);\n return null;\n }\n\n // Fast path optimization: Skip immediate reserve if we recently had empty reserves\n // This avoids wasteful Lua script calls when queue is idle\n // After 3 consecutive empty reserves, go straight to blocking for better performance\n const skipImmediateReserve = this._consecutiveEmptyReserves >= 3;\n\n if (!skipImmediateReserve) {\n // Fast path: try immediate reserve first (avoids blocking when jobs are available)\n const immediateJob = await this.reserve();\n if (immediateJob) {\n this.logger.debug(\n `Immediate reserve successful (${Date.now() - startTime}ms)`,\n );\n // Reset consecutive empty reserves counter when we get a job via fast path\n this._consecutiveEmptyReserves = 0;\n return immediateJob;\n }\n }\n\n // Use BullMQ-style adaptive timeout with delayed job consideration\n const adaptiveTimeout = this.getBlockTimeout(timeoutSec, blockUntil);\n\n // Only log blocking operations every 10th time to reduce spam\n if (this._consecutiveEmptyReserves % 10 === 0) {\n this.logger.debug(\n `Starting blocking operation (timeout: ${adaptiveTimeout}s, consecutive empty: ${this._consecutiveEmptyReserves})`,\n );\n }\n\n // Use ready queue for blocking behavior (more reliable than marker system)\n const readyKey = nsKey(this.ns, 'ready');\n\n try {\n // Avoid extra zcard during every blocking call to reduce Redis CPU\n\n // Use dedicated blocking connection to avoid interfering with other operations\n const bzpopminStart = Date.now();\n const client = blockingClient ?? this.r;\n const result = await client.bzpopmin(readyKey, adaptiveTimeout);\n const bzpopminDuration = Date.now() - bzpopminStart;\n\n if (!result || result.length < 3) {\n this.logger.debug(\n `Blocking timeout/empty (took ${bzpopminDuration}ms)`,\n );\n // Track consecutive empty reserves for adaptive timeout\n this._consecutiveEmptyReserves = this._consecutiveEmptyReserves + 1;\n return null; // Timeout or no result\n }\n\n const [, groupId, score] = result;\n\n // Only log blocking results every 10th time to reduce spam\n if (this._consecutiveEmptyReserves % 10 === 0) {\n this.logger.debug(\n `Blocking result: group=${groupId}, score=${score} (took ${bzpopminDuration}ms)`,\n );\n }\n\n // Try to reserve atomically from the specific group to eliminate race conditions\n const reserveStart = Date.now();\n const job = await this.reserveAtomic(groupId);\n const reserveDuration = Date.now() - reserveStart;\n\n if (job) {\n this.logger.debug(\n `Successful job reserve after blocking: ${job.id} from group ${job.groupId} (reserve took ${reserveDuration}ms)`,\n );\n // Reset consecutive empty reserves counter\n this._consecutiveEmptyReserves = 0;\n } else {\n this.logger.warn(\n `Blocking found group but reserve failed: group=${groupId} (reserve took ${reserveDuration}ms)`,\n );\n\n // Check if group actually has jobs before restoring to prevent infinite loops\n // This prevents poisoned groups (empty groups in ready queue) from being restored\n try {\n const groupKey = `${this.ns}:g:${groupId}`;\n const jobCount = await this.r.zcard(groupKey);\n\n if (jobCount > 0) {\n // Group has jobs, restore it to ready queue\n await this.r.zadd(readyKey, Number(score), groupId);\n this.logger.debug(\n `Restored group ${groupId} to ready with score ${score} after failed atomic reserve (${jobCount} jobs)`,\n );\n } else {\n // Group is empty (poisoned), don't restore it\n this.logger.warn(\n `Not restoring empty group ${groupId} - preventing poisoned group loop`,\n );\n }\n } catch (_e) {\n // If check fails, err on the side of not restoring to prevent infinite loops\n this.logger.warn(\n `Failed to check group ${groupId} job count, not restoring`,\n );\n }\n\n // Increment consecutive empty reserves and fall back to general reserve scan\n this._consecutiveEmptyReserves = this._consecutiveEmptyReserves + 1;\n return this.reserve();\n }\n return job;\n } catch (err) {\n const errorDuration = Date.now() - startTime;\n this.logger.error(`Blocking error after ${errorDuration}ms:`, err);\n\n // Enhanced error handling - check if it's a connection error\n if (this.isConnectionError(err)) {\n this.logger.error(`Connection error detected - rethrowing`);\n // For connection errors, don't fall back immediately\n throw err;\n }\n // For other errors, fall back to regular reserve\n this.logger.warn(`Falling back to regular reserve due to error`);\n return this.reserve();\n } finally {\n const totalDuration = Date.now() - startTime;\n if (totalDuration > 1000) {\n this.logger.debug(`ReserveBlocking completed in ${totalDuration}ms`);\n }\n }\n }\n\n /**\n * Reserve a job from a specific group atomically (eliminates race conditions)\n * @param groupId - The group to reserve from\n */\n public async reserveAtomic(groupId: string): Promise<ReservedJob<T> | null> {\n const now = Date.now();\n\n const result = await evalScript<string | null>(\n this.r,\n 'reserve-atomic',\n [this.ns, String(now), String(this.vt), String(groupId)],\n 1,\n );\n if (!result) return null;\n\n // Parse the delimited string response (same format as regular reserve)\n const parts = result.split('|||');\n if (parts.length < 10) return null;\n\n const [\n id,\n groupIdRaw,\n data,\n attempts,\n maxAttempts,\n seq,\n timestamp,\n orderMs,\n score,\n deadline,\n ] = parts;\n\n const parsedTimestamp = parseInt(timestamp, 10);\n const parsedOrderMs = parseInt(orderMs, 10);\n return {\n id,\n groupId: groupIdRaw,\n data: JSON.parse(data),\n attempts: parseInt(attempts, 10),\n maxAttempts: parseInt(maxAttempts, 10),\n seq: parseInt(seq, 10),\n timestamp: parsedTimestamp,\n orderMs: Number.isNaN(parsedOrderMs) ? parsedTimestamp : parsedOrderMs, // Fallback to timestamp if orderMs is NaN\n score: parseFloat(score),\n deadlineAt: parseInt(deadline, 10),\n };\n }\n\n /**\n * 获取处于 Ready 状态的 Group 列表\n * @param start \n * @param end \n */\n async getReadyGroups(start = 0, end = -1): Promise<string[]> {\n // groupmq:{ns}:ready 是一个 ZSET,存储 groupId\n return this.r.zrange(`${this.ns}:ready`, start, end);\n }\n\n /**\n * 设置组的元数据 (优先级/并发度)\n * 我们将使用 Hash 存储这些配置: groupmq:{ns}:config:{groupId}\n */\n async setGroupConfig(\n groupId: string,\n config: { priority?: number; concurrency?: number },\n ): Promise<void> {\n const key = `${this.ns}:config:${groupId}`;\n const args: string[] = [];\n if (config.priority !== undefined) args.push('priority', String(config.priority));\n if (config.concurrency !== undefined)\n args.push('concurrency', String(config.concurrency));\n\n if (args.length > 0) {\n await this.r.hset(key, ...args);\n }\n }\n\n async getGroupConfig(\n groupId: string,\n ): Promise<{ priority: number; concurrency: number }> {\n const key = `${this.ns}:config:${groupId}`;\n const [p, c] = await this.r.hmget(key, 'priority', 'concurrency');\n return {\n priority: p ? parseInt(p, 10) : 1, // 默认优先级 1\n concurrency: c ? parseInt(c, 10) : 1, // 默认并发 1\n };\n }\n\n /**\n * 设置指定组的并发上限\n * @param groupId 组 ID\n * @param limit 并发数 (必须 >= 1)\n */\n async setGroupConcurrency(groupId: string, limit: number): Promise<void> {\n const validLimit = Math.max(1, Math.floor(limit));\n await this.r.hset(\n `${this.ns}:config:${groupId}`,\n 'concurrency',\n String(validLimit),\n );\n }\n\n /**\n * 获取指定组的并发上限\n */\n async getGroupConcurrency(groupId: string): Promise<number> {\n const val = await this.r.hget(\n `${this.ns}:config:${groupId}`,\n 'concurrency',\n );\n return val ? parseInt(val, 10) : 1;\n }\n\n /**\n * 获取组内最老任务的入队时间戳\n * 用于 PriorityStrategy 的 aging 算法\n * @param groupId 组 ID\n * @returns 最老任务的时间戳,如果组为空则返回 undefined\n */\n async getGroupOldestTimestamp(groupId: string): Promise<number | undefined> {\n const gZ = `${this.ns}:g:${groupId}`;\n // 获取组内第一个任务(score 最小的)\n const result = await this.r.zrange(gZ, 0, 0);\n if (!result || result.length === 0) {\n return undefined;\n }\n const jobId = result[0];\n const timestamp = await this.r.hget(`${this.ns}:job:${jobId}`, 'timestamp');\n return timestamp ? parseInt(timestamp, 10) : undefined;\n }\n\n /**\n * Reserve up to maxBatch jobs (one per available group) atomically in Lua.\n */\n async reserveBatch(maxBatch = 16): Promise<Array<ReservedJob<T>>> {\n const now = Date.now();\n const results = await evalScript<Array<string | null>>(\n this.r,\n 'reserve-batch',\n [this.ns, String(now), String(this.vt), String(Math.max(1, maxBatch))],\n 1,\n );\n const out: Array<ReservedJob<T>> = [];\n for (const r of results || []) {\n if (!r) continue;\n const parts = r.split('|||');\n if (parts.length !== 10) continue;\n out.push({\n id: parts[0],\n groupId: parts[1],\n data: safeJsonParse(parts[2]),\n attempts: parseInt(parts[3], 10),\n maxAttempts: parseInt(parts[4], 10),\n seq: parseInt(parts[5], 10),\n timestamp: parseInt(parts[6], 10),\n orderMs: parseInt(parts[7], 10),\n score: parseFloat(parts[8]),\n deadlineAt: parseInt(parts[9], 10),\n } as ReservedJob<T>);\n }\n return out;\n }\n\n /**\n * Get the number of jobs currently being processed (active jobs)\n */\n async getActiveCount(): Promise<number> {\n return evalScript<number>(this.r, 'get-active-count', [this.ns], 1);\n }\n\n /**\n * Get the number of jobs waiting to be processed\n */\n async getWaitingCount(): Promise<number> {\n return evalScript<number>(this.r, 'get-waiting-count', [this.ns], 1);\n }\n\n /**\n * Get the number of jobs delayed due to backoff\n */\n async getDelayedCount(): Promise<number> {\n return evalScript<number>(this.r, 'get-delayed-count', [this.ns], 1);\n }\n\n /**\n * Get list of active job IDs\n */\n async getActiveJobs(): Promise<string[]> {\n return evalScript<string[]>(this.r, 'get-active-jobs', [this.ns], 1);\n }\n\n /**\n * Get list of waiting job IDs\n */\n async getWaitingJobs(): Promise<string[]> {\n return evalScript<string[]>(this.r, 'get-waiting-jobs', [this.ns], 1);\n }\n\n /**\n * Get list of delayed job IDs\n */\n async getDelayedJobs(): Promise<string[]> {\n return evalScript<string[]>(this.r, 'get-delayed-jobs', [this.ns], 1);\n }\n\n /**\n * Get list of unique group IDs that have jobs\n */\n async getUniqueGroups(): Promise<string[]> {\n return evalScript<string[]>(this.r, 'get-unique-groups', [this.ns], 1);\n }\n\n /**\n * Get count of unique groups that have jobs\n */\n async getUniqueGroupsCount(): Promise<number> {\n return evalScript<number>(this.r, 'get-unique-groups-count', [this.ns], 1);\n }\n\n /**\n * Fetch a single job by ID with enriched fields for UI/inspection.\n * Attempts to mimic BullMQ's Job shape for fields commonly used by BullBoard.\n */\n async getJob(id: string): Promise<JobEntity<T>> {\n return JobEntity.fromStore<T>(this, id);\n }\n\n /**\n * Fetch jobs by statuses, emulating BullMQ's Queue.getJobs API used by BullBoard.\n * Only getter functionality; ordering is best-effort.\n *\n * Optimized with pagination to reduce Redis load - especially important for BullBoard polling.\n */\n async getJobsByStatus(\n jobStatuses: Array<Status>,\n start = 0,\n end = -1,\n ): Promise<Array<JobEntity<T>>> {\n // Calculate actual limit to fetch (with some buffer for deduplication)\n const requestedCount = end >= 0 ? end - start + 1 : 100; // Default to 100 if unbounded\n const fetchLimit = Math.min(requestedCount * 2, 500); // Cap at 500 to prevent excessive fetches\n\n // Map to track which status each job belongs to (for known status optimization)\n const idToStatus = new Map<string, Status>();\n const idSets: string[] = [];\n\n // Optimized helper that respects pagination\n const pushZRange = async (key: string, status: Status, reverse = false) => {\n try {\n // Fetch only what we need (with buffer), not everything\n const ids = reverse\n ? await this.r.zrevrange(key, 0, fetchLimit - 1)\n : await this.r.zrange(key, 0, fetchLimit - 1);\n for (const id of ids) {\n idToStatus.set(id, status);\n }\n idSets.push(...ids);\n } catch (_e) {\n // ignore\n }\n };\n\n const statuses = new Set(jobStatuses);\n\n if (statuses.has('active')) {\n await pushZRange(`${this.ns}:processing`, 'active');\n }\n if (statuses.has('delayed')) {\n await pushZRange(`${this.ns}:delayed`, 'delayed');\n }\n if (statuses.has('completed')) {\n await pushZRange(`${this.ns}:completed`, 'completed', true);\n }\n if (statuses.has('failed')) {\n await pushZRange(`${this.ns}:failed`, 'failed', true);\n }\n if (statuses.has('waiting')) {\n // Aggregate waiting jobs with limits to prevent scanning all groups\n try {\n const groupIds = await this.r.smembers(`${this.ns}:groups`);\n if (groupIds.length > 0) {\n // Limit groups to scan (prevent excessive iteration)\n const groupsToScan = groupIds.slice(\n 0,\n Math.min(100, groupIds.length),\n );\n const pipe = this.r.multi();\n\n // Fetch only first few jobs from each group (most are at the head anyway)\n const jobsPerGroup = Math.max(\n 1,\n Math.ceil(fetchLimit / groupsToScan.length),\n );\n for (const gid of groupsToScan) {\n pipe.zrange(`${this.ns}:g:${gid}`, 0, jobsPerGroup - 1);\n }\n\n const rows = await pipe.exec();\n for (const r of rows || []) {\n const arr = (r?.[1] as string[]) || [];\n for (const id of arr) {\n idToStatus.set(id, 'waiting');\n }\n idSets.push(...arr);\n }\n }\n } catch (_e) {\n // ignore\n }\n }\n\n // paused, waiting-children, prioritized are not supported; return empty\n\n // De-duplicate keeping first occurrence\n const seen = new Set<string>();\n const uniqueIds: string[] = [];\n for (const id of idSets) {\n if (!seen.has(id)) {\n seen.add(id);\n uniqueIds.push(id);\n }\n }\n\n const slice =\n end >= 0 ? uniqueIds.slice(start, end + 1) : uniqueIds.slice(start);\n if (slice.length === 0) return [];\n\n // Atomically fetch all job hashes in one pipeline\n const pipe = this.r.multi();\n for (const id of slice) {\n pipe.hgetall(`${this.ns}:job:${id}`);\n }\n const rows = await pipe.exec();\n\n // Construct jobs directly from pipeline data (atomic, no race condition)\n const jobs: Array<JobEntity<T>> = [];\n for (let i = 0; i < slice.length; i++) {\n const id = slice[i];\n const raw = (rows?.[i]?.[1] as Record<string, string>) || {};\n\n // Skip jobs that were already cleaned up\n if (!raw || Object.keys(raw).length === 0) {\n this.logger.warn(\n `Skipping job ${id} - not found (likely cleaned up by retention)`,\n );\n continue;\n }\n\n // Use the known status from the index we fetched from\n const knownStatus = idToStatus.get(id);\n const job = JobEntity.fromRawHash<T>(this, id, raw, knownStatus);\n jobs.push(job);\n }\n return jobs;\n }\n\n /**\n * Provide counts structured like BullBoard expects.\n */\n async getJobCounts(): Promise<\n Record<\n | 'active'\n | 'waiting'\n | 'delayed'\n | 'completed'\n | 'failed'\n | 'paused'\n | 'waiting-children'\n | 'prioritized',\n number\n >\n > {\n const [active, waiting, delayed, completed, failed] = await Promise.all([\n this.getActiveCount(),\n this.getWaitingCount(),\n this.getDelayedCount(),\n this.getCompletedCount(),\n this.getFailedCount(),\n ]);\n\n return {\n active,\n waiting,\n delayed,\n completed,\n failed,\n paused: 0,\n 'waiting-children': 0,\n prioritized: 0,\n };\n }\n\n /**\n * Check for stalled jobs and recover or fail them\n * Returns array of [jobId, groupId, action] tuples\n */\n async checkStalledJobs(\n now: number,\n gracePeriod: number,\n maxStalledCount: number,\n ): Promise<string[]> {\n try {\n const results = await evalScript<string[]>(\n this.r,\n 'check-stalled',\n [this.ns, String(now), String(gracePeriod), String(maxStalledCount)],\n 1,\n );\n return results || [];\n } catch (error) {\n this.logger.error('Error checking stalled jobs:', error);\n return [];\n }\n }\n\n /**\n * Start the promoter service for staging system.\n * Promoter listens to Redis keyspace notifications and promotes staged jobs when ready.\n * This is idempotent - calling multiple times has no effect if already running.\n */\n async startPromoter(): Promise<void> {\n if (this.promoterRunning || this.orderingDelayMs <= 0) {\n return; // Already running or not needed\n }\n\n this.promoterRunning = true;\n this.promoterLockId = randomUUID();\n\n try {\n // Create duplicate Redis connection for pub/sub\n this.promoterRedis = this.r.duplicate();\n\n // Try to enable keyspace notifications\n try {\n await this.promoterRedis.config('SET', 'notify-keyspace-events', 'Ex');\n this.logger.debug(\n 'Enabled Redis keyspace notifications for staging promoter',\n );\n } catch (err) {\n this.logger.warn(\n 'Failed to enable keyspace notifications. Promoter will use polling fallback.',\n err,\n );\n }\n\n // Get Redis database number for keyspace event channel\n const db = this.promoterRedis.options.db ?? 0;\n\n const timerKey = `${this.ns}:stage:timer`;\n const expiredChannel = `__keyevent@${db}__:expired`;\n\n // Subscribe to keyspace expiration events\n await this.promoterRedis.subscribe(expiredChannel, (err) => {\n if (err) {\n this.logger.error('Failed to subscribe to keyspace events:', err);\n } else {\n this.logger.debug(`Subscribed to ${expiredChannel}`);\n }\n });\n\n // Handle expiration events\n this.promoterRedis.on('message', async (channel, message) => {\n if (channel === expiredChannel && message === timerKey) {\n await this.runPromotion();\n }\n });\n\n // Fallback: polling interval (100ms) in case keyspace notifications fail\n this.promoterInterval = setInterval(async () => {\n await this.runPromotion();\n }, 100);\n\n // Initial promotion check\n await this.runPromotion();\n\n this.logger.debug('Staging promoter started');\n } catch (err) {\n this.logger.error('Failed to start promoter:', err);\n this.promoterRunning = false;\n await this.stopPromoter();\n }\n }\n\n /**\n * Run a single promotion cycle with distributed locking\n */\n private async runPromotion(): Promise<void> {\n if (!this.promoterRunning) {\n return;\n }\n\n const lockKey = `${this.ns}:promoter:lock`;\n const lockTtl = 30000; // 30 seconds\n\n try {\n // Try to acquire lock\n const acquired = await this.r.set(\n lockKey,\n this.promoterLockId!,\n 'PX',\n lockTtl,\n 'NX',\n );\n\n if (acquired === 'OK') {\n try {\n // Promote staged jobs\n const promoted = await evalScript<number>(\n this.r,\n 'promote-staged',\n [\n this.ns,\n String(Date.now()),\n String(100), // Limit per batch\n ],\n 1,\n );\n\n if (promoted > 0) {\n this.logger.debug(`Promoted ${promoted} staged jobs`);\n }\n } finally {\n // Release lock (only if it's still ours)\n const currentLockValue = await this.r.get(lockKey);\n if (currentLockValue === this.promoterLockId) {\n await this.r.del(lockKey);\n }\n }\n }\n } catch (err) {\n this.logger.error('Error during promotion:', err);\n }\n }\n\n /**\n * Stop the promoter service\n */\n async stopPromoter(): Promise<void> {\n if (!this.promoterRunning) return;\n\n this.promoterRunning = false;\n\n // Clear interval\n if (this.promoterInterval) {\n clearInterval(this.promoterInterval);\n this.promoterInterval = undefined;\n }\n\n // Close promoter Redis connection\n if (this.promoterRedis) {\n try {\n await this.promoterRedis.unsubscribe();\n await this.promoterRedis.quit();\n } catch (_err) {\n try {\n this.promoterRedis.disconnect();\n } catch (_e) {}\n }\n this.promoterRedis = undefined;\n }\n\n this.logger.debug('Staging promoter stopped');\n }\n\n /**\n * Close underlying Redis connections\n */\n async close(): Promise<void> {\n // Flush any pending batched jobs before closing\n if (this.batchConfig && this.batchBuffer.length > 0) {\n this.logger.debug(\n `Flushing ${this.batchBuffer.length} pending batched jobs before close`,\n );\n await this.flushBatch();\n }\n\n // Stop promoter\n await this.stopPromoter();\n\n try {\n await this.r.quit();\n } catch (_e) {\n try {\n this.r.disconnect();\n } catch (_e2) {}\n }\n }\n\n // --------------------- Pause/Resume ---------------------\n private get pausedKey(): string {\n return `${this.ns}:paused`;\n }\n\n async pause(): Promise<void> {\n await this.r.set(this.pausedKey, '1');\n }\n\n async resume(): Promise<void> {\n await this.r.del(this.pausedKey);\n }\n\n async isPaused(): Promise<boolean> {\n const v = await this.r.get(this.pausedKey);\n return v !== null;\n }\n\n /**\n * Wait for the queue to become empty (no active jobs)\n * @param timeoutMs Maximum time to wait in milliseconds (default: 60 seconds)\n * @returns true if queue became empty, false if timeout reached\n */\n async waitForEmpty(timeoutMs = 60_000): Promise<boolean> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeoutMs) {\n try {\n // Single atomic Lua script checks all queue structures\n const isEmpty = await evalScript<number>(\n this.r,\n 'is-empty',\n [this.ns],\n 1,\n );\n\n if (isEmpty === 1) {\n await sleep(0);\n return true;\n }\n\n await sleep(200);\n } catch (err) {\n // Handle connection errors gracefully - Redis might be temporarily unavailable\n if (this.isConnectionError(err)) {\n this.logger.warn(\n 'Redis connection error in waitForEmpty, retrying...',\n );\n // Wait longer before retry on connection errors\n await sleep(1000);\n continue;\n }\n // For non-connection errors, rethrow\n throw err;\n }\n }\n\n return false; // Timeout reached\n }\n\n // Track cleanup calls per group to throttle excessive checking\n private _groupCleanupTracking = new Map<string, number>();\n\n /**\n * Remove problematic groups from ready queue to prevent infinite loops\n * Handles both poisoned groups (only failed/expired jobs) and locked groups\n *\n * Throttled to 1% sampling rate to reduce Redis overhead\n */\n private async cleanupPoisonedGroup(groupId: string): Promise<string> {\n // Throttle: only check 1% of the time to reduce Redis load\n // This is called frequently when workers compete for groups\n if (Math.random() > 0.01) {\n return 'skipped';\n }\n\n // Additional throttle: max once per 10 seconds per group\n const lastCheck = this._groupCleanupTracking.get(groupId) || 0;\n const now = Date.now();\n if (now - lastCheck < 10000) {\n return 'throttled';\n }\n this._groupCleanupTracking.set(groupId, now);\n\n // Periodically clean old tracking entries (keep map bounded)\n if (this._groupCleanupTracking.size > 1000) {\n const cutoff = now - 60000; // 1 minute ago\n for (const [gid, ts] of this._groupCleanupTracking.entries()) {\n if (ts < cutoff) {\n this._groupCleanupTracking.delete(gid);\n }\n }\n }\n\n try {\n const result = await evalScript<string>(\n this.r,\n 'cleanup-poisoned-group',\n [this.ns, groupId, String(now)],\n 1,\n );\n if (result === 'poisoned') {\n this.logger.warn(`Removed poisoned group ${groupId} from ready queue`);\n } else if (result === 'empty') {\n this.logger.warn(`Removed empty group ${groupId} from ready queue`);\n } else if (result === 'locked') {\n // Only log locked group warnings occasionally\n if (Math.random() < 0.1) {\n this.logger.debug(\n `Detected group ${groupId} is locked by another worker (this is normal with high concurrency)`,\n );\n }\n }\n return result as string;\n } catch (error) {\n this.logger.error(`Error cleaning up group ${groupId}:`, error);\n return 'error';\n }\n }\n\n /**\n * Distributed one-shot scheduler: promotes delayed jobs and processes repeating jobs.\n * Only proceeds if a short-lived scheduler lock can be acquired.\n */\n private schedulerLockKey(): string {\n return `${this.ns}:sched:lock`;\n }\n\n async acquireSchedulerLock(ttlMs = 1500): Promise<boolean> {\n try {\n const res = (await (this.r as any).set(\n this.schedulerLockKey(),\n '1',\n 'PX',\n ttlMs,\n 'NX',\n )) as string | null;\n return res === 'OK';\n } catch (_e) {\n return false;\n }\n }\n\n async runSchedulerOnce(now = Date.now()): Promise<void> {\n const ok = await this.acquireSchedulerLock(this.schedulerLockTtlMs);\n if (!ok) return;\n // Reduced limits for faster execution: process a few jobs per tick instead of hundreds\n await this.promoteDelayedJobsBounded(32, now);\n await this.processRepeatingJobsBounded(16, now);\n }\n\n /**\n * Promote up to `limit` delayed jobs that are due now. Uses a small Lua to move one item per tick.\n */\n async promoteDelayedJobsBounded(\n limit = 256,\n now = Date.now(),\n ): Promise<number> {\n let moved = 0;\n for (let i = 0; i < limit; i++) {\n try {\n const n = await evalScript<number>(\n this.r,\n 'promote-delayed-one',\n [this.ns, String(now)],\n 1,\n );\n if (!n || n <= 0) break;\n moved += n;\n } catch (_e) {\n break;\n }\n }\n return moved;\n }\n\n /**\n * Process up to `limit` repeating job ticks.\n * Intentionally small per-tick work to keep Redis CPU flat.\n */\n async processRepeatingJobsBounded(\n limit = 128,\n now = Date.now(),\n ): Promise<number> {\n const scheduleKey = `${this.ns}:repeat:schedule`;\n let processed = 0;\n for (let i = 0; i < limit; i++) {\n // Get one due entry\n const due = await this.r.zrangebyscore(\n scheduleKey,\n 0,\n now,\n 'LIMIT',\n 0,\n 1,\n );\n if (!due || due.length === 0) break;\n const repeatKey = due[0];\n\n try {\n const repeatJobKey = `${this.ns}:repeat:${repeatKey}`;\n const repeatJobDataStr = await this.r.get(repeatJobKey);\n\n if (!repeatJobDataStr) {\n await this.r.zrem(scheduleKey, repeatKey);\n continue;\n }\n\n const repeatJobData = JSON.parse(repeatJobDataStr);\n if (repeatJobData.removed) {\n await this.r.zrem(scheduleKey, repeatKey);\n await this.r.del(repeatJobKey);\n continue;\n }\n\n // Remove from schedule first to prevent duplicates\n await this.r.zrem(scheduleKey, repeatKey);\n\n // Compute next run\n let nextRunTime: number;\n if ('every' in repeatJobData.repeat) {\n nextRunTime = now + repeatJobData.repeat.every;\n } else {\n nextRunTime = this.getNextCronTime(repeatJobData.repeat.pattern, now);\n }\n\n repeatJobData.nextRunTime = nextRunTime;\n repeatJobData.lastRunTime = now;\n await this.r.set(repeatJobKey, JSON.stringify(repeatJobData));\n await this.r.zadd(scheduleKey, nextRunTime, repeatKey);\n\n // Enqueue the instance\n await evalScript<string>(\n this.r,\n 'enqueue',\n [\n this.ns,\n repeatJobData.groupId,\n JSON.stringify(repeatJobData.data),\n String(repeatJobData.maxAttempts ?? this.defaultMaxAttempts),\n String(repeatJobData.orderMs ?? now),\n String(0),\n String(randomUUID()),\n String(this.keepCompleted),\n ],\n 1,\n );\n\n processed++;\n } catch (error) {\n this.logger.error(\n `Error processing repeating job ${repeatKey}:`,\n error,\n );\n await this.r.zrem(scheduleKey, repeatKey);\n }\n }\n return processed;\n }\n\n /**\n * Promote delayed jobs that are now ready to be processed\n * This should be called periodically to move jobs from delayed set to ready queue\n */\n async promoteDelayedJobs(): Promise<number> {\n try {\n return await evalScript<number>(\n this.r,\n 'promote-delayed-jobs',\n [this.ns, String(Date.now())],\n 1,\n );\n } catch (error) {\n this.logger.error(`Error promoting delayed jobs:`, error);\n return 0;\n }\n }\n\n /**\n * Change the delay of a specific job\n */\n async changeDelay(jobId: string, newDelay: number): Promise<boolean> {\n const newDelayUntil = newDelay > 0 ? Date.now() + newDelay : 0;\n\n try {\n const result = await evalScript<number>(\n this.r,\n 'change-delay',\n [this.ns, jobId, String(newDelayUntil), String(Date.now())],\n 1,\n );\n return result === 1;\n } catch (error) {\n this.logger.error(`Error changing delay for job ${jobId}:`, error);\n return false;\n }\n }\n\n /**\n * Promote a delayed job to be ready immediately\n */\n async promote(jobId: string): Promise<boolean> {\n return this.changeDelay(jobId, 0);\n }\n\n /**\n * Remove a job from the queue regardless of state (waiting, delayed, processing)\n */\n async remove(jobId: string): Promise<boolean> {\n try {\n const result = await evalScript<number>(\n this.r,\n 'remove',\n [this.ns, jobId],\n 1,\n );\n return result === 1;\n } catch (error) {\n this.logger.error(`Error removing job ${jobId}:`, error);\n return false;\n }\n }\n\n /**\n * Clean jobs of a given status older than graceTimeMs\n * @param graceTimeMs Remove jobs with finishedOn <= now - graceTimeMs (for completed/failed)\n * @param limit Max number of jobs to clean in one call\n * @param status Either 'completed' or 'failed'\n */\n async clean(\n graceTimeMs: number,\n limit: number,\n status: 'completed' | 'failed' | 'delayed',\n ): Promise<number> {\n const graceAt = Date.now() - graceTimeMs;\n try {\n const removed = await evalScript<number>(\n this.r,\n 'clean-status',\n [\n this.ns,\n status,\n String(graceAt),\n String(Math.max(0, Math.min(limit, 100000))),\n ],\n 1,\n );\n return removed ?? 0;\n } catch (error) {\n console.log('HERE?', error);\n\n this.logger.error(`Error cleaning ${status} jobs:`, error);\n return 0;\n }\n }\n\n /**\n * Update a job's data payload (BullMQ-style)\n */\n async updateData(jobId: string, data: T): Promise<void> {\n const jobKey = `${this.ns}:job:${jobId}`;\n const exists = await this.r.exists(jobKey);\n if (!exists) {\n throw new Error(`Job ${jobId} not found`);\n }\n const serialized = JSON.stringify(data === undefined ? null : data);\n await this.r.hset(jobKey, 'data', serialized);\n }\n\n /**\n * Add a repeating job (cron job)\n */\n private async addRepeatingJob(opts: AddOptions<T>): Promise<JobEntity> {\n if (!opts.repeat) {\n throw new Error('Repeat options are required for repeating jobs');\n }\n\n const now = Date.now();\n // Make repeatKey unique by including a timestamp and random component\n const repeatKey = `${opts.groupId}:${JSON.stringify(opts.repeat)}:${now}:${Math.random().toString(36).slice(2)}`;\n\n // Calculate next run time\n let nextRunTime: number;\n\n if ('every' in opts.repeat) {\n // Simple interval-based repeat\n nextRunTime = now + opts.repeat.every;\n } else {\n // Cron pattern-based repeat\n nextRunTime = this.getNextCronTime(opts.repeat.pattern, now);\n }\n\n // Store repeating job metadata\n const repeatJobData = {\n groupId: opts.groupId,\n data: opts.data === undefined ? null : opts.data,\n maxAttempts: opts.maxAttempts ?? this.defaultMaxAttempts,\n orderMs: opts.orderMs,\n repeat: opts.repeat,\n nextRunTime,\n lastRunTime: null as number | null,\n removed: false, // Track if this repeat job has been removed\n };\n\n // Store in Redis (metadata JSON)\n const repeatJobKey = `${this.ns}:repeat:${repeatKey}`;\n await this.r.set(repeatJobKey, JSON.stringify(repeatJobData));\n\n // Add to repeating jobs sorted set for efficient scheduling\n await this.r.zadd(`${this.ns}:repeat:schedule`, nextRunTime, repeatKey);\n\n // Create a reverse mapping for easier removal\n const lookupKey = `${this.ns}:repeat:lookup:${opts.groupId}:${JSON.stringify(opts.repeat)}`;\n await this.r.set(lookupKey, repeatKey);\n\n // Persist a synthetic Job entity for this repeating definition so that\n // Queue.add consistently returns a Job. Use a special repeat id namespace.\n const repeatId = `repeat:${repeatKey}`;\n const jobHashKey = `${this.ns}:job:${repeatId}`;\n try {\n await this.r.hmset(\n jobHashKey,\n 'id',\n repeatId,\n 'groupId',\n repeatJobData.groupId,\n 'data',\n JSON.stringify(repeatJobData.data),\n 'attempts',\n '0',\n 'maxAttempts',\n String(repeatJobData.maxAttempts),\n 'seq',\n '0',\n 'timestamp',\n String(Date.now()),\n 'orderMs',\n String(repeatJobData.orderMs ?? now),\n 'status',\n 'waiting',\n );\n } catch (_e) {\n // best-effort; even if this fails, the repeat metadata exists\n }\n\n // Don't schedule the first job immediately - let the cron processor handle it\n // Return the persisted Job entity handle for the repeating definition\n return JobEntity.fromStore<T>(this as any, repeatId);\n }\n\n /**\n * Compute next execution time using cron-parser (BullMQ-style)\n */\n private getNextCronTime(pattern: string, fromTime: number): number {\n try {\n const interval = CronParser.parseExpression(pattern, {\n currentDate: new Date(fromTime),\n });\n return interval.next().getTime();\n } catch (_e) {\n throw new Error(`Invalid cron pattern: ${pattern}`);\n }\n }\n\n /**\n * Remove a repeating job\n */\n async removeRepeatingJob(\n groupId: string,\n repeat: RepeatOptions,\n ): Promise<boolean> {\n try {\n // Use the lookup key to find the actual repeatKey\n const lookupKey = `${this.ns}:repeat:lookup:${groupId}:${JSON.stringify(repeat)}`;\n const repeatKey = await this.r.get(lookupKey);\n\n if (!repeatKey) {\n // No such repeating job exists\n return false;\n }\n\n const repeatJobKey = `${this.ns}:repeat:${repeatKey}`;\n const scheduleKey = `${this.ns}:repeat:schedule`;\n\n // Get the repeat job data before modifying\n const repeatJobDataStr = await this.r.get(repeatJobKey);\n\n if (!repeatJobDataStr) {\n // Clean up orphaned lookup\n await this.r.del(lookupKey);\n return false;\n }\n\n const repeatJobData = JSON.parse(repeatJobDataStr);\n\n // Mark as removed to prevent future scheduling\n repeatJobData.removed = true;\n await this.r.set(repeatJobKey, JSON.stringify(repeatJobData));\n\n // Remove from future schedule (but keep the metadata for cleanup)\n await this.r.zrem(scheduleKey, repeatKey);\n\n // Clean up the lookup key\n await this.r.del(lookupKey);\n\n // Note: Cleanup of existing job instances is best-effort and not critical.\n // Jobs will naturally complete or be cleaned up by the retention policies.\n\n // Remove the synthetic repeat job hash persisted at creation time\n try {\n const repeatId = `repeat:${repeatKey}`;\n await this.r.del(`${this.ns}:job:${repeatId}`);\n } catch (_e) {\n // best-effort cleanup\n }\n\n return true;\n } catch (error) {\n this.logger.error(`Error removing repeating job:`, error);\n return false;\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * This file contains code copied from BullMQ (https://github.com/taskforcesh/bullmq)\n *\n * BullMQ is a fantastic library and one of the most popular Redis-based job queue\n * libraries for Node.js. We've copied the AsyncFifoQueue implementation from BullMQ\n * as it's a well-designed component that fits our needs perfectly.\n *\n * Original copyright notice:\n * Copyright (c) Taskforce.sh and contributors\n *\n * This code is used under the MIT License. The original license can be found at:\n * https://github.com/taskforcesh/bullmq/blob/main/LICENSE\n *\n * Modifications may have been made to adapt this code for use in GroupMQ.\n */\n\nclass Node<T> {\n value: T | undefined = undefined;\n next: Node<T> | null = null;\n\n constructor(value: T) {\n this.value = value;\n }\n}\n\nclass LinkedList<T> {\n length = 0;\n private head: Node<T> | null;\n private tail: Node<T> | null;\n\n constructor() {\n this.head = null;\n this.tail = null;\n }\n\n push(value: T) {\n const newNode = new Node(value);\n if (!this.length) {\n this.head = newNode;\n } else {\n this.tail!.next = newNode;\n }\n\n this.tail = newNode;\n this.length += 1;\n return newNode;\n }\n\n shift() {\n if (!this.length) {\n return null;\n }\n const head = this.head;\n this.head = this.head!.next;\n this.length -= 1;\n\n return head;\n }\n}\n\n/**\n * AsyncFifoQueue\n *\n * A minimal FIFO queue for asynchronous operations. Allows adding asynchronous operations\n * and consume them in the order they are resolved.\n */\nexport class AsyncFifoQueue<T> {\n /**\n * A queue of completed promises. As the pending\n * promises are resolved, they are added to this queue.\n */\n private queue: LinkedList<T> = new LinkedList();\n\n /**\n * A set of pending promises.\n */\n private pending = new Set<Promise<T>>();\n\n /**\n * The next promise to be resolved. As soon as a pending promise\n * is resolved, this promise is resolved with the result of the\n * pending promise.\n */\n private nextPromise: Promise<T | undefined> | undefined;\n private resolve: ((value: T | undefined) => void) | undefined;\n private reject: ((reason?: any) => void) | undefined;\n\n constructor(private ignoreErrors = false) {\n this.newPromise();\n }\n\n public add(promise: Promise<T>): void {\n this.pending.add(promise);\n\n promise\n .then((data) => {\n this.pending.delete(promise);\n\n if (this.queue.length === 0) {\n this.resolvePromise(data);\n }\n this.queue.push(data);\n })\n .catch((err) => {\n this.pending.delete(promise);\n\n if (this.ignoreErrors) {\n // Push undefined and resolve instead of rejecting\n if (this.queue.length === 0) {\n this.resolvePromise(undefined as T);\n }\n this.queue.push(undefined as T);\n } else {\n this.rejectPromise(err);\n }\n });\n }\n\n public async waitAll(): Promise<void> {\n await Promise.all(this.pending);\n }\n\n public numTotal(): number {\n return this.pending.size + this.queue.length;\n }\n\n public numPending(): number {\n return this.pending.size;\n }\n\n public numQueued(): number {\n return this.queue.length;\n }\n\n private resolvePromise(data: T) {\n this.resolve!(data);\n this.newPromise();\n }\n\n private rejectPromise(err: any) {\n this.reject!(err);\n this.newPromise();\n }\n\n private newPromise() {\n this.nextPromise = new Promise<T | undefined>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n private async wait() {\n return this.nextPromise;\n }\n\n public async fetch(): Promise<T | void> {\n if (this.pending.size === 0 && this.queue.length === 0) {\n return;\n }\n while (this.queue.length === 0) {\n try {\n await this.wait();\n } catch (err) {\n // Ignore errors\n if (!this.ignoreErrors) {\n console.error('Unexpected Error in AsyncFifoQueue', err);\n }\n }\n }\n return this.queue.shift()?.value;\n }\n}\n","import { AsyncFifoQueue } from './async-fifo-queue';\nimport { Job } from './job';\nimport { Logger, type LoggerInterface } from './logger';\nimport type { AddOptions, Queue, ReservedJob } from './queue';\nimport { DispatchStrategy } from './strategies/dispatch-strategy';\n\nexport type BackoffStrategy = (attempt: number) => number; // ms\n\n// Typed event system for Worker\nexport interface WorkerEvents<T = any>\n extends Record<string, (...args: any[]) => void> {\n error: (error: Error) => void;\n closed: () => void;\n ready: () => void;\n failed: (job: Job<T>) => void;\n completed: (job: Job<T>) => void;\n 'ioredis:close': () => void;\n 'graceful-timeout': (job: Job<T>) => void;\n stalled: (jobId: string, groupId: string) => void;\n}\n\nclass TypedEventEmitter<\n TEvents extends Record<string, (...args: any[]) => void>,\n> {\n private listeners = new Map<keyof TEvents, Array<TEvents[keyof TEvents]>>();\n\n on<K extends keyof TEvents>(event: K, listener: TEvents[K]): this {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, []);\n }\n this.listeners.get(event)!.push(listener);\n return this;\n }\n\n off<K extends keyof TEvents>(event: K, listener: TEvents[K]): this {\n const eventListeners = this.listeners.get(event);\n if (eventListeners) {\n const index = eventListeners.indexOf(listener);\n if (index !== -1) {\n eventListeners.splice(index, 1);\n }\n }\n return this;\n }\n\n emit<K extends keyof TEvents>(\n event: K,\n ...args: Parameters<TEvents[K]>\n ): boolean {\n const eventListeners = this.listeners.get(event);\n if (eventListeners && eventListeners.length > 0) {\n for (const listener of eventListeners) {\n try {\n listener(...args);\n } catch (error) {\n // Don't let listener errors break the emit\n console.error(\n `Error in event listener for '${String(event)}':`,\n error,\n );\n }\n }\n return true;\n }\n return false;\n }\n\n removeAllListeners<K extends keyof TEvents>(event?: K): this {\n if (event) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n return this;\n }\n}\n\n/**\n * Configuration options for a GroupMQ Worker\n *\n * @template T The type of data stored in jobs\n */\nexport type WorkerOptions<T> = {\n /** The queue instance this worker will process jobs from */\n queue: Queue<T>;\n\n /**\n * Optional worker name for logging and identification\n * @default queue.name\n */\n name?: string;\n\n /**\n * The function that processes jobs. Must be async and handle job failures gracefully.\n * @param job The reserved job to process\n * @returns Promise that resolves when job is complete\n */\n handler: (job: ReservedJob<T>) => Promise<unknown>;\n\n /**\n * Heartbeat interval in milliseconds to keep jobs alive during processing.\n * Prevents jobs from timing out during long-running operations.\n *\n * @default Math.max(1000, queue.jobTimeoutMs / 3)\n * @example 5000 // Heartbeat every 5 seconds\n *\n * **When to adjust:**\n * - Long-running jobs: Increase to reduce Redis overhead\n * - Short jobs: Decrease for faster timeout detection\n * - High job volume: Increase to reduce Redis commands\n */\n heartbeatMs?: number;\n\n /**\n * Error handler called when job processing fails or worker encounters errors\n * @param err The error that occurred\n * @param job The job that failed (if applicable)\n */\n onError?: (err: unknown, job?: ReservedJob<T>) => void;\n\n /**\n * Maximum number of retry attempts for failed jobs at the worker level.\n * This overrides the queue's default maxAttempts setting.\n *\n * @default queue.maxAttemptsDefault\n * @example 5 // Retry failed jobs up to 5 times\n *\n * **When to adjust:**\n * - Critical jobs: Increase for more retries\n * - Non-critical jobs: Decrease to fail faster\n * - External API calls: Consider network reliability\n */\n maxAttempts?: number;\n\n /**\n * Backoff strategy for retrying failed jobs. Determines delay between retries.\n *\n * @default Exponential backoff with jitter (500ms, 1s, 2s, 4s, 8s, 16s, 30s max)\n * @example (attempt) => Math.min(10000, attempt * 1000) // Linear backoff\n *\n * **When to adjust:**\n * - Rate-limited APIs: Use longer delays\n * - Database timeouts: Use shorter delays\n * - External services: Consider their retry policies\n */\n backoff?: BackoffStrategy;\n\n /**\n * Whether to enable automatic cleanup of expired and completed jobs.\n * Cleanup removes old jobs to prevent Redis memory growth.\n *\n * @default true\n * @example false // Disable if you handle cleanup manually\n *\n * **When to disable:**\n * - Manual cleanup: If you have your own cleanup process\n * - Job auditing: If you need to keep all job history\n * - Development: For debugging job states\n */\n enableCleanup?: boolean;\n\n /**\n * Interval in milliseconds between cleanup operations.\n * Cleanup removes expired jobs and trims completed/failed job retention.\n *\n * @default 300000 (5 minutes)\n * @example 600000 // Cleanup every 10 minutes\n *\n * **When to adjust:**\n * - High job volume: Increase to reduce Redis overhead\n * - Low job volume: Decrease for more frequent cleanup\n * - Memory constraints: Decrease to prevent Redis memory growth\n * - Job retention needs: Adjust based on keepCompleted/keepFailed settings\n */\n cleanupIntervalMs?: number;\n\n /**\n * Interval in milliseconds between scheduler operations.\n * Scheduler promotes delayed jobs and processes cron/repeating jobs.\n *\n * @default 5000 (5 seconds)\n * @example 1000 // For fast cron jobs (every minute or less)\n * @example 10000 // For slow cron jobs (hourly or daily)\n *\n * **When to adjust:**\n * - Fast cron jobs: Decrease (1000-2000ms) for sub-minute schedules\n * - Slow cron jobs: Increase (10000-60000ms) to reduce Redis overhead\n * - No cron jobs: Increase (5000-10000ms) since only delayed jobs are affected\n */\n schedulerIntervalMs?: number;\n\n /**\n * Maximum time in seconds to wait for new jobs when queue is empty.\n * Shorter timeouts make workers more responsive but use more Redis resources.\n *\n * @default 1\n * @example 0.5 // Very responsive, higher Redis usage\n * @example 2 // Less responsive, lower Redis usage\n *\n * **When to adjust:**\n * - High job volume: Use 1s or less for faster job pickup\n * - Low job volume: Increase (2-3s) to reduce Redis overhead\n * - Real-time requirements: Decrease to 0.5-1s for lower latency\n * - Resource constraints: Increase to 2-5s to reduce Redis load\n *\n * **Note:** The actual timeout is adaptive and can go as low as 1ms\n * based on queue activity and delayed job schedules.\n */\n blockingTimeoutSec?: number;\n\n /**\n * Logger configuration for worker operations and debugging.\n *\n * @default false (no logging)\n * @example true // Enable basic logging\n * @example customLogger // Use custom logger instance\n *\n * **When to enable:**\n * - Development: For debugging job processing\n * - Production monitoring: For operational insights\n * - Troubleshooting: When investigating performance issues\n */\n logger?: LoggerInterface | true;\n\n /**\n * Number of jobs this worker can process concurrently.\n * Higher concurrency increases throughput but uses more memory and CPU.\n *\n * @default 1\n * @example 4 // Process 4 jobs simultaneously\n * @example 8 // For CPU-intensive jobs on multi-core systems\n *\n * **When to adjust:**\n * - CPU-bound jobs: Set to number of CPU cores\n * - I/O-bound jobs: Set to 2-4x number of CPU cores\n * - Memory constraints: Lower concurrency to reduce memory usage\n * - High job volume: Increase for better throughput\n * - Single-threaded requirements: Keep at 1\n */\n concurrency?: number;\n\n /**\n * Interval in milliseconds between stalled job checks.\n * Stalled jobs are those whose worker crashed or lost connection.\n *\n * @default 30000 (30 seconds)\n * @example 60000 // Check every minute for lower overhead\n * @example 10000 // Check every 10 seconds for faster recovery\n *\n * **When to adjust:**\n * - Fast recovery needed: Decrease (10-20s)\n * - Lower Redis overhead: Increase (60s+)\n * - Unreliable workers: Decrease for faster detection\n */\n stalledInterval?: number;\n\n /**\n * Maximum number of times a job can become stalled before being failed.\n * A job becomes stalled when its worker crashes or loses connection.\n *\n * @default 1\n * @example 2 // Allow jobs to stall twice before failing\n * @example 0 // Never fail jobs due to stalling (not recommended)\n *\n * **When to adjust:**\n * - Unreliable infrastructure: Increase to tolerate more failures\n * - Critical jobs: Increase to allow more recovery attempts\n * - Quick failure detection: Keep at 1\n */\n maxStalledCount?: number;\n\n /**\n * Grace period in milliseconds before a job is considered stalled.\n * Jobs are only marked as stalled if their deadline has passed by this amount.\n *\n * @default 0 (no grace period)\n * @example 5000 // 5 second grace period for clock skew\n * @example 1000 // 1 second grace for network latency\n *\n * **When to adjust:**\n * - Clock skew between servers: Add 1-5s grace\n * - Network latency: Add 1-2s grace\n * - Strict timing: Keep at 0\n */\n stalledGracePeriod?: number;\n\n /**\n * 自定义调度策略。如果设置,Worker 将忽略默认的 FIFO 调度,\n * 转而使用策略轮询模式。\n */\n strategy?: DispatchStrategy;\n\n /**\n * 策略模式下的轮询间隔 (ms)\n * 当使用 Strategy 时,我们无法使用阻塞读取 (BZPOPMIN),必须退化为短轮询\n * @default 50\n */\n strategyPollInterval?: number;\n};\n\nconst defaultBackoff: BackoffStrategy = (attempt) => {\n const base = Math.min(30_000, 2 ** (attempt - 1) * 500);\n const jitter = Math.floor(base * 0.25 * Math.random());\n return base + jitter;\n};\n\nclass _Worker<T = any> extends TypedEventEmitter<WorkerEvents<T>> {\n private logger: LoggerInterface;\n public readonly name: string;\n private q: Queue<T>;\n private handler: WorkerOptions<T>['handler'];\n private hbMs: number;\n private onError?: WorkerOptions<T>['onError'];\n private stopping = false;\n private opts: WorkerOptions<T>;\n private ready = false;\n private closed = false;\n private maxAttempts: number;\n private backoff: BackoffStrategy;\n private enableCleanup: boolean;\n private cleanupMs: number;\n private cleanupTimer?: NodeJS.Timeout;\n private schedulerTimer?: NodeJS.Timeout;\n private schedulerMs: number;\n private blockingTimeoutSec: number;\n private concurrency: number;\n private blockingClient: import('ioredis').default | null = null;\n\n // Stalled job detection\n private stalledCheckTimer?: NodeJS.Timeout;\n private stalledInterval: number;\n private maxStalledCount: number;\n private stalledGracePeriod: number;\n\n // Track all jobs in progress (for all concurrency levels)\n private jobsInProgress = new Set<{ job: ReservedJob<T>; ts: number }>();\n\n // Blocking detection and monitoring\n private lastJobPickupTime = Date.now(); // Initialize to now so we start in \"active\" mode\n private totalJobsProcessed = 0;\n private blockingStats = {\n totalBlockingCalls: 0,\n consecutiveEmptyReserves: 0,\n lastActivityTime: Date.now(),\n };\n private emptyReserveBackoffMs = 0;\n\n private redisCloseHandler?: () => void;\n private redisErrorHandler?: (error: Error) => void;\n private redisReadyHandler?: () => void;\n private runLoopPromise?: Promise<void>;\n\n constructor(opts: WorkerOptions<T>) {\n super();\n\n if (!opts.handler || typeof opts.handler !== 'function') {\n throw new Error('Worker handler must be a function');\n }\n\n this.opts = opts;\n this.q = opts.queue;\n this.name = opts.name ?? this.q.name;\n this.logger =\n typeof opts.logger === 'object'\n ? opts.logger\n : new Logger(!!opts.logger, this.name);\n this.handler = opts.handler;\n const jobTimeoutMs = this.q.jobTimeoutMs ?? 30_000;\n this.hbMs =\n opts.heartbeatMs ?? Math.max(1000, Math.floor(jobTimeoutMs / 3));\n this.onError = opts.onError;\n this.maxAttempts = opts.maxAttempts ?? this.q.maxAttemptsDefault ?? 3;\n this.backoff = opts.backoff ?? defaultBackoff;\n this.enableCleanup = opts.enableCleanup ?? true;\n this.cleanupMs = opts.cleanupIntervalMs ?? 60_000; // 1 minutes for high-concurrency production\n\n // Scheduler interval for delayed jobs and cron jobs\n const defaultSchedulerMs = 1000; // 1 second for responsive job processing\n this.schedulerMs = opts.schedulerIntervalMs ?? defaultSchedulerMs;\n\n this.blockingTimeoutSec = opts.blockingTimeoutSec ?? 5; // 1s default for responsive job pickup (adaptive logic can go lower)\n // With AsyncFifoQueue, we can safely use atomic completion for all concurrency levels\n this.concurrency = Math.max(1, opts.concurrency ?? 1);\n\n // Initialize stalled job detection settings\n // BullMQ-inspired: More conservative settings for high concurrency\n this.stalledInterval =\n opts.stalledInterval ?? (this.concurrency > 50 ? 60000 : 30000); // 60s for high concurrency, 30s otherwise\n this.maxStalledCount =\n opts.maxStalledCount ?? (this.concurrency > 50 ? 2 : 1); // Allow 2 stalls for high concurrency\n // CRITICAL: Grace period must be >= heartbeat startup delay to prevent false positives\n // Default 5s covers heartbeat startup (2s) + 1 heartbeat interval (2s) + network/load buffer (1s)\n this.stalledGracePeriod = opts.stalledGracePeriod ?? 5000; // 5s grace for all configurations\n\n // Set up Redis connection event handlers\n this.setupRedisEventHandlers();\n\n // Auto-start promoter if orderingDelayMs is configured\n if (this.q.orderingDelayMs > 0) {\n this.q.startPromoter().catch((err) => {\n this.logger.error('Failed to start staging promoter:', err);\n });\n }\n\n this.run();\n }\n\n get isClosed() {\n return this.closed;\n }\n\n /**\n * Add jitter to prevent thundering herd problems in high-concurrency environments\n * @param baseInterval The base interval in milliseconds\n * @param jitterPercent Percentage of jitter to add (0-1, default 0.1 for 10%)\n * @returns The interval with jitter applied\n */\n private addJitter(baseInterval: number, jitterPercent = 0.1): number {\n const jitter = Math.random() * baseInterval * jitterPercent;\n return baseInterval + jitter;\n }\n\n private setupRedisEventHandlers() {\n // Get Redis instance from the queue to monitor connection events\n const redis = this.q.redis;\n if (redis) {\n this.redisCloseHandler = () => {\n this.ready = false;\n this.emit('ioredis:close');\n };\n this.redisErrorHandler = (error: Error) => {\n this.emit('error', error);\n };\n this.redisReadyHandler = () => {\n if (!this.ready && !this.stopping) {\n this.ready = true;\n this.emit('ready');\n }\n };\n\n redis.on('close', this.redisCloseHandler);\n redis.on('error', this.redisErrorHandler);\n redis.on('ready', this.redisReadyHandler);\n }\n }\n\n async run(): Promise<void> {\n if (this.runLoopPromise) {\n return this.runLoopPromise;\n }\n\n // Store the run loop promise so close() can wait for it\n const runPromise = this._runLoop();\n this.runLoopPromise = runPromise;\n return runPromise;\n }\n\n private async _runLoop(): Promise<void> {\n this.logger.info(`🚀 Worker ${this.name} starting...`);\n const strategyPollInterval = this.opts.strategyPollInterval ?? 50;\n\n // Dedicated blocking client per worker with auto-pipelining to reduce contention\n try {\n this.blockingClient = this.q.redis.duplicate({\n enableAutoPipelining: true,\n // Infinite retries for blocking connections to prevent \"Max retries exceeded\" errors\n maxRetriesPerRequest: null,\n // Exponential backoff retry strategy\n retryStrategy: (times: number) => {\n return Math.max(Math.min(Math.exp(times) * 1000, 20000), 1000);\n },\n });\n\n // Add reconnection handlers for resilience\n this.blockingClient.on('error', (err) => {\n if (!this.q.isConnectionError(err)) {\n this.logger.error('Blocking client error (non-connection):', err);\n } else {\n this.logger.warn('Blocking client connection error:', err.message);\n }\n this.emit('error', err instanceof Error ? err : new Error(String(err)));\n });\n\n this.blockingClient.on('close', () => {\n // Only log close if not during shutdown\n if (!this.stopping && !this.closed) {\n this.logger.warn(\n 'Blocking client disconnected, will reconnect on next operation',\n );\n }\n });\n\n this.blockingClient.on('reconnecting', () => {\n if (!this.stopping && !this.closed) {\n this.logger.info('Blocking client reconnecting...');\n }\n });\n\n this.blockingClient.on('ready', () => {\n if (!this.stopping && !this.closed) {\n this.logger.info('Blocking client ready');\n }\n });\n } catch (err) {\n this.logger.error('Failed to create blocking client:', err);\n this.blockingClient = null; // fall back to queue's blocking client\n }\n\n // Start cleanup timer if enabled\n if (this.enableCleanup) {\n // Cleanup timer: only runs cleanup, not scheduler\n // Add jitter to prevent all workers from running cleanup simultaneously\n this.cleanupTimer = setInterval(async () => {\n try {\n await this.q.cleanup();\n } catch (err) {\n this.onError?.(err);\n }\n }, this.addJitter(this.cleanupMs));\n\n // Scheduler timer: promotes delayed jobs and processes cron jobs\n // Runs independently in the background, even when worker is blocked on BZPOPMIN\n // Distributed lock ensures only one worker executes at a time\n const schedulerInterval = Math.min(this.schedulerMs, this.cleanupMs);\n this.schedulerTimer = setInterval(async () => {\n try {\n await this.q.runSchedulerOnce();\n } catch (_err) {\n // Ignore errors, this is best-effort\n }\n }, this.addJitter(schedulerInterval));\n }\n\n // Start stalled job checker for automatic recovery\n this.startStalledChecker();\n\n let connectionRetries = 0;\n const maxConnectionRetries = 10; // Allow more retries with exponential backoff\n\n // BullMQ-style async queue for clean promise management\n const asyncFifoQueue = new AsyncFifoQueue<void | ReservedJob<T> | null>(\n true,\n );\n\n while (!this.stopping || asyncFifoQueue.numTotal() > 0) {\n try {\n // Phase 1: Fetch jobs efficiently until we reach concurrency capacity\n // CRITICAL: Use asyncFifoQueue.numTotal() for concurrency control (matches BullMQ pattern)\n // The queue tracks all promises (both fetch and processing), so this is accurate\n while (!this.stopping) {\n if (asyncFifoQueue.numTotal() >= this.concurrency) {\n break; // At capacity, exit fetch loop\n }\n\n this.blockingStats.totalBlockingCalls++;\n\n // Prevent overflow: reset counter after 1 billion calls (keeps number manageable)\n if (this.blockingStats.totalBlockingCalls >= 1_000_000_000) {\n this.blockingStats.totalBlockingCalls = 0;\n }\n\n this.logger.debug(\n `Fetching job (call #${this.blockingStats.totalBlockingCalls}, processing: ${this.jobsInProgress.size}/${this.concurrency}, queue: ${asyncFifoQueue.numTotal()} (queued: ${asyncFifoQueue.numQueued()}, pending: ${asyncFifoQueue.numPending()}), total: ${asyncFifoQueue.numTotal()}/${this.concurrency})...`,\n );\n\n let fetchedJob: Promise<ReservedJob<T> | null>;\n\n if (this.opts.strategy) {\n // A. 策略模式 (Polling)\n fetchedJob = (async () => {\n // 1. 询问策略:下一个该谁?\n const targetGroupId = await this.opts.strategy!.getNextGroup(\n this.q,\n );\n\n if (!targetGroupId) {\n // 策略说没有合适的组,或者队列为空\n // 等待一段时间再轮询,避免空转 CPU\n await this.delay(strategyPollInterval);\n return null;\n }\n\n // 2. 原子抢占:尝试从指定组拿任务\n // 注意:这里可能会失败(比如并发满了,或者刚刚被别的 Worker 抢了)\n const job = await this.q.reserveAtomic(targetGroupId);\n\n if (!job) {\n // 抢占失败,可能是竞争导致,稍作退避\n // 也可以立即重试,取决于激进程度\n return null;\n }\n return job;\n })();\n } else {\n // B. 默认模式 (原有逻辑)\n // Try batch reserve first for better efficiency\n // Use batch reserve even for concurrency=1 since it's more efficient than blocking+atomic\n // But limit batch size to available concurrency capacity\n // Only batch reserve when queue is empty (process existing jobs first)\n const availableCapacity =\n this.concurrency - asyncFifoQueue.numTotal();\n if (availableCapacity > 0 && asyncFifoQueue.numTotal() === 0) {\n const batchSize = Math.min(availableCapacity, 8); // Cap at 8 for efficiency\n const batchJobs = await this.q.reserveBatch(batchSize);\n\n if (batchJobs.length > 0) {\n this.logger.debug(`Batch reserved ${batchJobs.length} jobs`);\n for (const job of batchJobs) {\n asyncFifoQueue.add(Promise.resolve(job));\n }\n // Reset counters for successful batch\n connectionRetries = 0;\n this.lastJobPickupTime = Date.now();\n this.blockingStats.consecutiveEmptyReserves = 0;\n this.blockingStats.lastActivityTime = Date.now();\n this.emptyReserveBackoffMs = 0;\n continue; // Skip individual reserve\n }\n }\n\n // BullMQ-style: only perform blocking reserve when truly drained\n // Require 2 consecutive empty reserves before considering queue drained\n // This prevents false positives from worker competition while staying responsive\n // Check both queued/pending jobs AND actively processing jobs\n const allowBlocking =\n this.blockingStats.consecutiveEmptyReserves >= 2 &&\n asyncFifoQueue.numTotal() === 0 &&\n this.jobsInProgress.size === 0;\n\n // Use a consistent blocking timeout - BullMQ style\n // Job completion resets consecutiveEmptyReserves to 0, ensuring fast pickup\n const adaptiveTimeout = this.blockingTimeoutSec;\n\n fetchedJob = allowBlocking\n ? this.q.reserveBlocking(\n adaptiveTimeout,\n undefined, // blockUntil removed (was always 0, dead code)\n this.blockingClient ?? undefined,\n )\n : this.q.reserve();\n }\n\n asyncFifoQueue.add(fetchedJob);\n\n // Sequential fetching: wait for this fetch before next (prevents thundering herd)\n const job = await fetchedJob;\n\n if (job) {\n // Reset connection retry count and empty reserves\n connectionRetries = 0;\n this.lastJobPickupTime = Date.now();\n this.blockingStats.consecutiveEmptyReserves = 0;\n this.blockingStats.lastActivityTime = Date.now();\n this.emptyReserveBackoffMs = 0; // Reset backoff when we get a job\n\n this.logger.debug(`Fetched job ${job.id} from group ${job.groupId}`);\n } else {\n // 注意:策略模式下,如果 job 为 null,需要处理空转等待\n if (this.opts.strategy) {\n // 如果是策略模式,且没拿任务,break 出去让 loop 重新转,\n // 这样可以检查 stopping 状态,并在上面的闭包里已经做了 delay\n if (\n asyncFifoQueue.numTotal() === 0 &&\n this.jobsInProgress.size === 0\n ) {\n break;\n }\n }\n\n // No more jobs available - increment counter\n this.blockingStats.consecutiveEmptyReserves++;\n\n // Only log every 50th empty reserve to reduce spam\n if (this.blockingStats.consecutiveEmptyReserves % 50 === 0) {\n this.logger.debug(\n `No job available (consecutive empty: ${this.blockingStats.consecutiveEmptyReserves})`,\n );\n }\n\n // Only apply exponential backoff when queue is truly empty (no jobs processing)\n // This prevents slowdown during the tail end when a few jobs are still processing\n const backoffThreshold = this.concurrency >= 100 ? 5 : 3;\n if (\n this.blockingStats.consecutiveEmptyReserves > backoffThreshold &&\n asyncFifoQueue.numTotal() === 0 && // No queued or pending jobs\n this.jobsInProgress.size === 0 // Critical: only backoff when nothing is processing\n ) {\n // Adaptive backoff based on concurrency level\n const maxBackoff = this.concurrency >= 100 ? 2000 : 5000;\n if (this.emptyReserveBackoffMs === 0) {\n this.emptyReserveBackoffMs = this.concurrency >= 100 ? 100 : 50;\n } else {\n this.emptyReserveBackoffMs = Math.min(\n maxBackoff,\n Math.max(100, this.emptyReserveBackoffMs * 1.2),\n );\n }\n\n // Only log backoff every 20th time to reduce spam\n if (this.blockingStats.consecutiveEmptyReserves % 20 === 0) {\n this.logger.debug(\n `Applying backoff: ${Math.round(this.emptyReserveBackoffMs)}ms (consecutive empty: ${this.blockingStats.consecutiveEmptyReserves}, jobs in progress: ${this.jobsInProgress.size})`,\n );\n }\n\n await this.delay(this.emptyReserveBackoffMs);\n }\n\n // BullMQ-inspired: Break immediately when no jobs found and queue is idle\n // This prevents tight polling and allows backoff to work properly\n if (\n asyncFifoQueue.numTotal() === 0 &&\n this.jobsInProgress.size === 0\n ) {\n break; // Fully idle - exit fetch loop\n }\n\n // If we have jobs queued/pending or processing, break to process them\n if (asyncFifoQueue.numTotal() > 0 || this.jobsInProgress.size > 0) {\n break;\n }\n }\n }\n\n // Phase 2: BullMQ-style - Fetch jobs and process immediately\n // This is more responsive than batching, especially at high concurrency\n let job: ReservedJob<T> | void;\n do {\n const fetchedJob = await asyncFifoQueue.fetch();\n job = fetchedJob ?? undefined;\n } while (!job && asyncFifoQueue.numQueued() > 0);\n\n if (job && typeof job === 'object' && 'id' in job) {\n // We fetched an actual job from the queue\n this.totalJobsProcessed++;\n this.logger.debug(\n `Processing job ${job.id} from group ${job.groupId} immediately`,\n );\n\n // Add processing promise immediately, don't wait for completion\n // The promise resolves to void or a chained job\n // When it resolves to a chained job, that job will be fetched from the queue\n // and processed by this same worker (maintaining atomic chaining)\n const processingPromise = this.processJob(\n job,\n () => {\n // Check if we have capacity for atomic chaining\n // Use asyncFifoQueue.numTotal() to match BullMQ pattern\n return asyncFifoQueue.numTotal() <= this.concurrency;\n },\n this.jobsInProgress,\n );\n\n asyncFifoQueue.add(processingPromise);\n }\n // Note: No delay here - just loop back to Phase 1 immediately\n // The adaptive timeout in Phase 1's blocking reserve handles idle efficiently\n } catch (err) {\n if (this.stopping) {\n return;\n }\n // Distinguish between connection errors (retry) and other errors (log and continue)\n const isConnErr = this.q.isConnectionError(err);\n\n if (isConnErr) {\n // Connection error - retry with exponential backoff\n connectionRetries++;\n\n this.logger.error(\n `Connection error (retry ${connectionRetries}/${maxConnectionRetries}):`,\n err,\n );\n\n if (connectionRetries >= maxConnectionRetries) {\n this.logger.error(\n `⚠️ Max connection retries (${maxConnectionRetries}) exceeded! Worker will continue but may be experiencing persistent Redis issues.`,\n );\n this.emit(\n 'error',\n new Error(\n `Max connection retries (${maxConnectionRetries}) exceeded - worker continuing with backoff`,\n ),\n );\n // Use maximum backoff delay before continuing\n await this.delay(20000);\n connectionRetries = 0; // Reset to continue trying\n } else {\n // Exponential backoff with 1s min, 20s max\n const delayMs = Math.max(\n Math.min(Math.exp(connectionRetries) * 1000, 20000),\n 1000,\n );\n this.logger.debug(\n `Waiting ${Math.round(delayMs)}ms before retry (exponential backoff)`,\n );\n await this.delay(delayMs);\n }\n } else {\n // Non-connection error (programming error, Lua script error, etc.)\n // Log it, emit it, but don't retry - just continue with next iteration\n this.logger.error(\n `Worker loop error (non-connection, continuing):`,\n err,\n );\n this.emit(\n 'error',\n err instanceof Error ? err : new Error(String(err)),\n );\n\n // Reset connection retries since this wasn't a connection issue\n connectionRetries = 0;\n\n // Small delay to avoid tight error loops\n await this.delay(100);\n }\n\n this.onError?.(err);\n }\n }\n\n this.logger.info(`Stopped`);\n }\n\n private async delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Process a job and return the next job if atomic completion succeeds\n * This matches BullMQ's processJob signature\n */\n private async processJob(\n job: ReservedJob<T>,\n fetchNextCallback: () => boolean,\n jobsInProgress: Set<{ job: ReservedJob<T>; ts: number }>,\n ): Promise<void | ReservedJob<T>> {\n // Check if this job is already tracked (it's a chained job)\n const existingItem = Array.from(jobsInProgress).find(\n (item) => item.job.id === job.id,\n );\n\n let inProgressItem: { job: ReservedJob<T>; ts: number };\n if (existingItem) {\n // Chained job - already tracked, just update timestamp\n existingItem.ts = Date.now();\n inProgressItem = existingItem;\n } else {\n // New job - add to tracking\n inProgressItem = { job, ts: Date.now() };\n jobsInProgress.add(inProgressItem);\n }\n\n try {\n const nextJob = await this.processSingleJob(job, fetchNextCallback);\n\n // If a chained job is returned from atomic completion:\n // - It's already reserved in Redis (in processing set)\n // - We need to track it in jobsInProgress BEFORE removing the original job\n // to maintain accurate concurrency tracking\n // - This ensures totalInFlight calculation is correct\n if (\n nextJob &&\n typeof nextJob === 'object' &&\n 'id' in nextJob &&\n 'groupId' in nextJob\n ) {\n // Add chained job to jobsInProgress before removing original\n // This maintains accurate concurrency count during the transition\n const chainedItem = { job: nextJob, ts: Date.now() };\n jobsInProgress.add(chainedItem);\n // Now remove original job - chained job takes its place\n jobsInProgress.delete(inProgressItem);\n return nextJob;\n }\n\n // No chained job - original job will be removed in finally block\n return nextJob;\n } finally {\n // Only remove if not already removed (i.e., no chained job replaced it)\n // Also check if this is still the same item (in case it was a chained job that got replaced)\n if (jobsInProgress.has(inProgressItem)) {\n jobsInProgress.delete(inProgressItem);\n }\n }\n }\n\n /**\n * Complete a job and try to atomically get next job from same group\n */\n private async completeJob(\n job: ReservedJob<T>,\n handlerResult: unknown,\n fetchNextCallback?: () => boolean,\n processedOn?: number,\n finishedOn?: number,\n ): Promise<ReservedJob<T> | undefined> {\n if (fetchNextCallback?.()) {\n // Try atomic completion with next job reservation\n const nextJob = await this.q.completeAndReserveNextWithMetadata(\n job.id,\n job.groupId,\n handlerResult,\n {\n processedOn: processedOn || Date.now(),\n finishedOn: finishedOn || Date.now(),\n attempts: job.attempts,\n maxAttempts: job.maxAttempts,\n },\n );\n if (nextJob) {\n this.logger.debug(\n `Got next job ${nextJob.id} from same group ${nextJob.groupId} atomically`,\n );\n return nextJob;\n }\n // Atomic chaining failed - one of these scenarios:\n // 1. Job was already completed/recovered (early return from Lua script)\n // 2. Job was completed but no next job to chain (group empty, ordering delay, or not at active list head)\n // In both cases, the job is properly completed and group is unlocked. No action needed.\n this.logger.debug(\n `Atomic chaining returned nil for job ${job.id} - job completed, but no next job chained`,\n );\n\n // CRITICAL: For high concurrency, add a small delay to prevent thundering herd\n // This reduces the chance of multiple workers hitting the same race condition\n if (Math.random() < 0.1) {\n // 10% chance\n await new Promise((resolve) =>\n setTimeout(resolve, Math.random() * 100),\n );\n }\n } else {\n // Use completeWithMetadata for atomic completion with metadata\n await this.q.completeWithMetadata(job, handlerResult, {\n processedOn: processedOn || Date.now(),\n finishedOn: finishedOn || Date.now(),\n attempts: job.attempts,\n maxAttempts: job.maxAttempts,\n });\n }\n\n return undefined;\n }\n\n /**\n * Start the stalled job checker\n * Checks periodically for jobs that exceeded their deadline and recovers or fails them\n */\n private startStalledChecker(): void {\n if (this.stalledInterval <= 0) {\n return; // Disabled\n }\n\n this.stalledCheckTimer = setInterval(async () => {\n try {\n await this.checkStalled();\n } catch (err) {\n this.logger.error('Error in stalled job checker:', err);\n this.emit('error', err instanceof Error ? err : new Error(String(err)));\n }\n }, this.stalledInterval);\n }\n\n /**\n * Check for stalled jobs and recover or fail them\n * A job is stalled when its worker crashed or lost connection\n */\n private async checkStalled(): Promise<void> {\n if (this.stopping || this.closed) {\n return;\n }\n\n try {\n const now = Date.now();\n const results = await this.q.checkStalledJobs(\n now,\n this.stalledGracePeriod,\n this.maxStalledCount,\n );\n\n if (results.length > 0) {\n // Process results in groups of 3: [jobId, groupId, action]\n for (let i = 0; i < results.length; i += 3) {\n const jobId = results[i];\n const groupId = results[i + 1];\n const action = results[i + 2];\n\n if (action === 'recovered') {\n this.logger.info(\n `Recovered stalled job ${jobId} from group ${groupId}`,\n );\n this.emit('stalled', jobId, groupId);\n } else if (action === 'failed') {\n this.logger.warn(\n `Failed stalled job ${jobId} from group ${groupId} (exceeded max stalled count)`,\n );\n this.emit('stalled', jobId, groupId);\n }\n }\n }\n } catch (err) {\n // Don't throw, just log - stalled checker should be resilient\n this.logger.error('Error checking stalled jobs:', err);\n }\n }\n\n /**\n * Get worker performance metrics\n */\n getWorkerMetrics() {\n const now = Date.now();\n return {\n name: this.name,\n totalJobsProcessed: this.totalJobsProcessed,\n lastJobPickupTime: this.lastJobPickupTime,\n timeSinceLastJob:\n this.lastJobPickupTime > 0 ? now - this.lastJobPickupTime : null,\n blockingStats: { ...this.blockingStats },\n isProcessing: this.jobsInProgress.size > 0,\n jobsInProgressCount: this.jobsInProgress.size,\n jobsInProgress: Array.from(this.jobsInProgress).map((item) => ({\n jobId: item.job.id,\n groupId: item.job.groupId,\n processingTimeMs: now - item.ts,\n })),\n };\n }\n\n /**\n * Stop the worker gracefully\n * @param gracefulTimeoutMs Maximum time to wait for current job to finish (default: 30 seconds)\n */\n async close(gracefulTimeoutMs = 30_000): Promise<void> {\n this.stopping = true;\n // Give some time if we just received a job\n // Otherwise jobsInProgress will be 0 and we will exit immediately\n await this.delay(100);\n\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n\n if (this.schedulerTimer) {\n clearInterval(this.schedulerTimer);\n }\n\n if (this.stalledCheckTimer) {\n clearInterval(this.stalledCheckTimer);\n }\n\n // Wait for jobs to finish first\n const startTime = Date.now();\n while (\n this.jobsInProgress.size > 0 &&\n Date.now() - startTime < gracefulTimeoutMs\n ) {\n await sleep(100);\n }\n\n // Close the blocking client to interrupt any blocking operations\n if (this.blockingClient) {\n try {\n if (this.jobsInProgress.size > 0 && gracefulTimeoutMs > 0) {\n // Graceful: use quit() to allow in-flight commands to complete\n this.logger.debug('Gracefully closing blocking client (quit)...');\n await this.blockingClient.quit();\n } else {\n // Force or no jobs: use disconnect() for immediate termination\n this.logger.debug('Force closing blocking client (disconnect)...');\n this.blockingClient.disconnect();\n }\n } catch (err) {\n // Swallow errors during close\n this.logger.debug('Error closing blocking client:', err);\n }\n this.blockingClient = null;\n }\n\n // Now wait for the run loop to fully exit, but with a much shorter timeout\n // Since we closed the blocking client, the run loop should exit immediately\n if (this.runLoopPromise) {\n const runLoopTimeout =\n this.jobsInProgress.size > 0\n ? gracefulTimeoutMs // If jobs are still running, use full timeout\n : 2000; // Run loop should exit in 2 seconds after blocking client is closed\n\n const timeoutPromise = new Promise<void>((resolve) => {\n setTimeout(resolve, runLoopTimeout);\n });\n\n try {\n await Promise.race([this.runLoopPromise, timeoutPromise]);\n } catch (err) {\n this.logger.warn('Error while waiting for run loop to exit:', err);\n }\n }\n\n if (this.jobsInProgress.size > 0) {\n this.logger.warn(\n `Worker stopped with ${this.jobsInProgress.size} jobs still processing after ${gracefulTimeoutMs}ms timeout.`,\n );\n // Emit graceful-timeout event for each job still processing\n const nowWall = Date.now();\n for (const item of this.jobsInProgress) {\n this.emit(\n 'graceful-timeout',\n Job.fromReserved(this.q, item.job, {\n processedOn: item.ts,\n finishedOn: nowWall,\n status: 'active',\n }),\n );\n }\n }\n\n // Clear tracking\n this.jobsInProgress.clear();\n this.ready = false;\n this.closed = true;\n\n // Blocking client was already closed earlier to interrupt blocking operations\n\n // Remove Redis event listeners to avoid leaks\n try {\n const redis = this.q.redis;\n if (redis) {\n if (this.redisCloseHandler)\n redis.off?.('close', this.redisCloseHandler);\n if (this.redisErrorHandler)\n redis.off?.('error', this.redisErrorHandler);\n if (this.redisReadyHandler)\n redis.off?.('ready', this.redisReadyHandler);\n }\n } catch (_e) {\n // ignore listener cleanup errors\n }\n\n // Emit closed event\n this.emit('closed');\n }\n\n /**\n * Get information about the first currently processing job (if any)\n * For concurrency > 1, returns the oldest job in progress\n */\n getCurrentJob(): { job: ReservedJob<T>; processingTimeMs: number } | null {\n if (this.jobsInProgress.size === 0) {\n return null;\n }\n\n // Return the oldest job (first one added to the set)\n const oldest = Array.from(this.jobsInProgress)[0];\n const now = Date.now();\n return {\n job: oldest.job,\n processingTimeMs: now - oldest.ts,\n };\n }\n\n /**\n * Get information about all currently processing jobs\n */\n getCurrentJobs(): Array<{ job: ReservedJob<T>; processingTimeMs: number }> {\n const now = Date.now();\n return Array.from(this.jobsInProgress).map((item) => ({\n job: item.job,\n processingTimeMs: now - item.ts,\n }));\n }\n\n /**\n * Check if the worker is currently processing any jobs\n */\n isProcessing(): boolean {\n return this.jobsInProgress.size > 0;\n }\n\n // Proxy to the underlying queue.add with correct data typing inferred from the queue\n async add(opts: AddOptions<T>) {\n return this.q.add(opts);\n }\n\n private async processSingleJob(\n job: ReservedJob<T>,\n fetchNextCallback?: () => boolean,\n ): Promise<void | ReservedJob<T>> {\n const jobStartWallTime = Date.now();\n\n let hbTimer: NodeJS.Timeout | undefined;\n let heartbeatDelayTimer: NodeJS.Timeout | undefined;\n\n const startHeartbeat = () => {\n // BullMQ-inspired: Adaptive heartbeat interval based on concurrency\n // CRITICAL: Heartbeat must run frequently enough to prevent stalled detection\n // Run every jobTimeout/3 (with max 10s) to ensure multiple heartbeats before timeout\n const jobTimeout = this.q.jobTimeoutMs || 30000;\n const minInterval = Math.min(\n this.hbMs, // Use the worker's configured heartbeat interval\n Math.floor(jobTimeout / 3), // At least 3 heartbeats within timeout window\n 10000, // Cap at 10s maximum\n );\n\n this.logger.debug(\n `Starting heartbeat for job ${job.id} (interval: ${minInterval}ms, concurrency: ${this.concurrency})`,\n );\n\n hbTimer = setInterval(async () => {\n try {\n const result = await this.q.heartbeat(job);\n if (result === 0) {\n // Job no longer exists or is not in processing state\n this.logger.warn(\n `Heartbeat failed for job ${job.id} - job may have been removed or completed elsewhere`,\n );\n // Stop heartbeat since job is gone\n if (hbTimer) {\n clearInterval(hbTimer);\n }\n }\n } catch (e) {\n // Only log heartbeat errors if they're not connection errors during shutdown\n const isConnErr = this.q.isConnectionError(e);\n if (!isConnErr || !this.stopping) {\n this.logger.error(\n `Heartbeat error for job ${job.id}:`,\n e instanceof Error ? e.message : String(e),\n );\n }\n\n this.onError?.(e, job);\n\n // Only emit error if not a connection error during shutdown\n if (!isConnErr || !this.stopping) {\n this.emit('error', e instanceof Error ? e : new Error(String(e)));\n }\n }\n }, minInterval);\n };\n\n try {\n // BullMQ-inspired: Smart heartbeat with adaptive timing\n // CRITICAL FIX: Start heartbeat much earlier to prevent false stalled detection\n // Under high Redis load, jobs can be marked stalled before heartbeat even starts!\n const jobTimeout = this.q.jobTimeoutMs || 30000;\n // Start heartbeat after 10% of timeout OR 2 seconds (whichever is smaller)\n // This ensures heartbeat is active long before stalled detection can trigger\n const heartbeatThreshold = Math.min(jobTimeout * 0.1, 2000);\n\n // Start heartbeat early for potentially long-running jobs\n heartbeatDelayTimer = setTimeout(() => {\n startHeartbeat();\n }, heartbeatThreshold);\n\n // Execute the user's handler\n const handlerResult = await this.handler(job);\n\n // Job finished quickly, cancel delayed heartbeat start\n if (heartbeatDelayTimer) {\n clearTimeout(heartbeatDelayTimer);\n }\n\n // Clean up heartbeat if it was started\n if (hbTimer) {\n clearInterval(hbTimer);\n }\n\n // Capture finish time before completing the job\n const finishedAtWall = Date.now();\n\n // Complete the job and optionally get next job from same group\n const nextJob = await this.completeJob(\n job,\n handlerResult,\n fetchNextCallback,\n jobStartWallTime,\n finishedAtWall,\n );\n\n // Reset adaptive timeout after successful job completion\n // This ensures the worker uses the low timeout (0.1s) for the next fetch\n this.blockingStats.consecutiveEmptyReserves = 0;\n this.emptyReserveBackoffMs = 0;\n\n // Emit completed event\n this.emit(\n 'completed',\n Job.fromReserved(this.q, job, {\n processedOn: jobStartWallTime,\n finishedOn: finishedAtWall,\n returnvalue: handlerResult,\n status: 'completed',\n }),\n );\n\n // Return chained job if available and we have capacity\n return nextJob;\n } catch (err) {\n // Clean up timers\n if (heartbeatDelayTimer) {\n clearTimeout(heartbeatDelayTimer);\n }\n if (hbTimer) {\n clearInterval(hbTimer);\n }\n await this.handleJobFailure(err, job, jobStartWallTime);\n }\n }\n\n /**\n * Handle job failure: emit events, retry or dead-letter\n */\n private async handleJobFailure(\n err: unknown,\n job: ReservedJob<T>,\n jobStartWallTime: number,\n ): Promise<void> {\n this.onError?.(err, job);\n\n // Reset adaptive timeout after job failure\n // This ensures the worker uses the low timeout (0.1s) for the next fetch\n this.blockingStats.consecutiveEmptyReserves = 0;\n this.emptyReserveBackoffMs = 0;\n\n // Safely emit error event\n try {\n this.emit('error', err instanceof Error ? err : new Error(String(err)));\n } catch (_emitError) {\n // Silently ignore emit errors\n }\n\n const failedAt = Date.now();\n\n // Emit failed event\n this.emit(\n 'failed',\n Job.fromReserved(this.q, job, {\n processedOn: jobStartWallTime,\n finishedOn: failedAt,\n failedReason: err instanceof Error ? err.message : String(err),\n stacktrace:\n err instanceof Error\n ? err.stack\n : typeof err === 'object' && err !== null\n ? (err as any).stack\n : undefined,\n status: 'failed',\n }),\n );\n\n // Calculate next attempt and backoff\n const nextAttempt = job.attempts + 1;\n const backoffMs = this.backoff(nextAttempt);\n\n // Check if we should dead-letter (max attempts reached)\n if (nextAttempt >= this.maxAttempts) {\n await this.deadLetterJob(\n err,\n job,\n jobStartWallTime,\n failedAt,\n nextAttempt,\n );\n return;\n }\n\n // Retry the job\n const retryResult = await this.q.retry(job.id, backoffMs);\n if (retryResult === -1) {\n // Queue-level max attempts exceeded\n await this.deadLetterJob(\n err,\n job,\n jobStartWallTime,\n failedAt,\n job.maxAttempts,\n );\n return;\n }\n\n // Record attempt failure\n await this.recordFailureAttempt(\n err,\n job,\n jobStartWallTime,\n failedAt,\n nextAttempt,\n );\n }\n\n /**\n * Dead-letter a job that exceeded max attempts\n */\n private async deadLetterJob(\n err: unknown,\n job: ReservedJob<T>,\n processedOn: number,\n finishedOn: number,\n attempts: number,\n ): Promise<void> {\n this.logger.info(\n `Dead lettering job ${job.id} from group ${job.groupId} (attempts: ${attempts}/${job.maxAttempts})`,\n );\n\n const errObj = err instanceof Error ? err : new Error(String(err));\n\n try {\n await this.q.recordFinalFailure(\n { id: job.id, groupId: job.groupId },\n { name: errObj.name, message: errObj.message, stack: errObj.stack },\n {\n processedOn,\n finishedOn,\n attempts,\n maxAttempts: job.maxAttempts,\n data: job.data,\n },\n );\n } catch (e) {\n this.logger.warn('Failed to record final failure', e);\n }\n\n await this.q.deadLetter(job.id, job.groupId);\n }\n\n /**\n * Record a failed attempt (not final)\n */\n private async recordFailureAttempt(\n err: unknown,\n job: ReservedJob<T>,\n processedOn: number,\n finishedOn: number,\n attempts: number,\n ): Promise<void> {\n const errObj = err instanceof Error ? err : new Error(String(err));\n\n try {\n await this.q.recordAttemptFailure(\n { id: job.id, groupId: job.groupId },\n { name: errObj.name, message: errObj.message, stack: errObj.stack },\n {\n processedOn,\n finishedOn,\n attempts,\n maxAttempts: job.maxAttempts,\n },\n );\n } catch (e) {\n this.logger.warn('Failed to record attempt failure', e);\n }\n }\n}\n\n// Export a value with a generic constructor so T is inferred from opts.queue\nexport type Worker<T = any> = _Worker<T>;\ntype WorkerConstructor = new <T>(opts: WorkerOptions<T>) => _Worker<T>;\nexport const Worker = _Worker as unknown as WorkerConstructor;\n\nfunction sleep(ms: number) {\n return new Promise((r) => setTimeout(r, ms));\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}