@boringnode/queue 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +103 -20
  2. package/build/chunk-6IO4P6RB.js +145 -0
  3. package/build/chunk-6IO4P6RB.js.map +1 -0
  4. package/build/{chunk-VHN3XZDC.js → chunk-AHUVTAI7.js} +278 -29
  5. package/build/chunk-AHUVTAI7.js.map +1 -0
  6. package/build/chunk-S37X3CBO.js +500 -0
  7. package/build/chunk-S37X3CBO.js.map +1 -0
  8. package/build/index.d.ts +34 -8
  9. package/build/index.js +187 -31
  10. package/build/index.js.map +1 -1
  11. package/build/{job-DImdhRFO.d.ts → job-C4oyCVxR.d.ts} +275 -15
  12. package/build/src/contracts/adapter.d.ts +1 -1
  13. package/build/src/drivers/fake_adapter.d.ts +12 -6
  14. package/build/src/drivers/fake_adapter.js +1 -1
  15. package/build/src/drivers/knex_adapter.d.ts +6 -5
  16. package/build/src/drivers/knex_adapter.js +112 -0
  17. package/build/src/drivers/knex_adapter.js.map +1 -1
  18. package/build/src/drivers/redis_adapter.d.ts +6 -5
  19. package/build/src/drivers/redis_adapter.js +166 -402
  20. package/build/src/drivers/redis_adapter.js.map +1 -1
  21. package/build/src/drivers/redis_job_storage.d.ts +17 -0
  22. package/build/src/drivers/redis_job_storage.js +14 -0
  23. package/build/src/drivers/redis_job_storage.js.map +1 -0
  24. package/build/src/drivers/redis_scripts.d.ts +87 -0
  25. package/build/src/drivers/redis_scripts.js +29 -0
  26. package/build/src/drivers/redis_scripts.js.map +1 -0
  27. package/build/src/drivers/sync_adapter.d.ts +2 -1
  28. package/build/src/drivers/sync_adapter.js +7 -1
  29. package/build/src/drivers/sync_adapter.js.map +1 -1
  30. package/build/src/otel.d.ts +2 -2
  31. package/build/src/otel.js +3 -0
  32. package/build/src/otel.js.map +1 -1
  33. package/build/src/types/index.d.ts +1 -1
  34. package/build/src/types/main.d.ts +1 -1
  35. package/build/src/types/tracing_channels.d.ts +7 -1
  36. package/package.json +18 -19
  37. package/build/chunk-VHN3XZDC.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/drivers/redis_adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { Redis, type RedisOptions } from 'ioredis'\nimport { DEFAULT_PRIORITY } from '../constants.js'\nimport { calculateScore } from '../utils.js'\nimport type { Adapter, AcquiredJob } from '../contracts/adapter.js'\nimport type {\n JobData,\n JobRecord,\n JobRetention,\n ScheduleConfig,\n ScheduleData,\n ScheduleListOptions,\n} from '../types/main.js'\nimport { resolveRetention } from '../utils.js'\n\nconst redisKey = 'jobs'\nconst schedulesKey = 'schedules'\nconst schedulesIndexKey = 'schedules::index'\ntype RedisConfig = Redis | RedisOptions\n\n/**\n * Lua script for pushing a job to the queue.\n * Stores job data in the central hash and adds jobId to pending ZSET.\n */\nconst PUSH_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local score = tonumber(ARGV[3])\n\n redis.call('HSET', data_key, job_id, job_data)\n redis.call('ZADD', pending_key, score, job_id)\n\n return 1\n`\n\n/**\n * Lua script for pushing a delayed job.\n * Stores job data in the central hash and adds jobId to delayed ZSET.\n */\nconst PUSH_DELAYED_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local delayed_key = KEYS[2]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local execute_at = tonumber(ARGV[3])\n\n redis.call('HSET', data_key, job_id, job_data)\n redis.call('ZADD', delayed_key, execute_at, job_id)\n\n return 1\n`\n\n/**\n * Lua script for atomic job acquisition.\n * 1. Check and process delayed jobs\n * 2. Pop from pending queue\n * 3. Add to active hash with worker info\n * 4. Return job data\n */\nconst ACQUIRE_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local active_key = KEYS[3]\n local delayed_key = KEYS[4]\n local worker_id = ARGV[1]\n local now = tonumber(ARGV[2])\n\n -- Process delayed jobs: move ready jobs to pending\n local ready_job_ids = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)\n if #ready_job_ids > 0 then\n for i = 1, #ready_job_ids do\n local job_id = ready_job_ids[i]\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local job = cjson.decode(job_data)\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n redis.call('ZREM', delayed_key, job_id)\n end\n end\n end\n\n -- Pop highest priority job (lowest score)\n local result = redis.call('ZPOPMIN', pending_key)\n if not result or #result == 0 then\n return nil\n end\n\n local job_id = result[1]\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return nil\n end\n\n -- Store in active hash (without data, it's in data_key)\n local active_data = cjson.encode({\n workerId = worker_id,\n acquiredAt = now\n })\n redis.call('HSET', active_key, job_id, active_data)\n\n -- Return job with acquiredAt\n local job = cjson.decode(job_data)\n job.acquiredAt = now\n return cjson.encode(job)\n`\n\n/**\n * Lua script for removing a job completely (no history).\n */\nconst REMOVE_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local job_id = ARGV[1]\n\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n redis.call('HDEL', active_key, job_id)\n redis.call('HDEL', data_key, job_id)\n\n return 1\n`\n\n/**\n * Lua script for finalizing a job in history.\n * Removes from active, stores finalization info, and prunes old records.\n */\nconst FINALIZE_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local history_key = KEYS[3]\n local index_key = KEYS[4]\n local job_id = ARGV[1]\n local now = tonumber(ARGV[2])\n local max_age = tonumber(ARGV[3])\n local max_count = tonumber(ARGV[4])\n local error_message = ARGV[5]\n\n -- Verify job is active\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Store finalization info (data stays in data_key)\n local record = {\n finishedAt = now\n }\n if error_message and error_message ~= '' then\n record.error = error_message\n end\n redis.call('HSET', history_key, job_id, cjson.encode(record))\n redis.call('ZADD', index_key, now, job_id)\n\n -- Prune by age\n if max_age and max_age > 0 then\n local cutoff = now - max_age\n local expired = redis.call('ZRANGEBYSCORE', index_key, 0, cutoff)\n if #expired > 0 then\n redis.call('ZREM', index_key, unpack(expired))\n redis.call('HDEL', history_key, unpack(expired))\n redis.call('HDEL', data_key, unpack(expired))\n end\n end\n\n -- Prune by count\n if max_count and max_count > 0 then\n local size = tonumber(redis.call('ZCARD', index_key))\n if size > max_count then\n local excess = size - max_count\n local stale = redis.call('ZRANGE', index_key, 0, excess - 1)\n if #stale > 0 then\n redis.call('ZREM', index_key, unpack(stale))\n redis.call('HDEL', history_key, unpack(stale))\n redis.call('HDEL', data_key, unpack(stale))\n end\n end\n end\n\n return 1\n`\n\n/**\n * Lua script for retrying a job.\n * 1. Verify job is active\n * 2. Remove from active hash\n * 3. Increment attempts in data\n * 4. Add back to pending (or delayed if retryAt is set)\n */\nconst RETRY_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local pending_key = KEYS[3]\n local delayed_key = KEYS[4]\n local job_id = ARGV[1]\n local retry_at = tonumber(ARGV[2])\n local now = tonumber(ARGV[3])\n\n -- Verify job is active\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Get job data\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return 0\n end\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Increment attempts and update data\n local job = cjson.decode(job_data)\n job.attempts = (job.attempts or 0) + 1\n redis.call('HSET', data_key, job_id, cjson.encode(job))\n\n -- Add back to pending or delayed\n if retry_at and retry_at > now then\n redis.call('ZADD', delayed_key, retry_at, job_id)\n else\n -- Score = priority * 1e13 + timestamp\n -- Lower score = higher priority, FIFO within same priority\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n end\n\n return 1\n`\n\n/**\n * Lua script for recovering stalled jobs.\n * Scans the active hash for jobs that have been active too long.\n * - Jobs within maxStalledCount: move back to pending with incremented stalledCount\n * - Jobs exceeding maxStalledCount: remove permanently (fail)\n * Returns the number of recovered jobs (not including failed ones).\n */\nconst RECOVER_STALLED_JOBS_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local pending_key = KEYS[3]\n local now = tonumber(ARGV[1])\n local stalled_threshold = tonumber(ARGV[2])\n local max_stalled_count = tonumber(ARGV[3])\n\n local recovered = 0\n local stalled_cutoff = now - stalled_threshold\n\n -- Get all active jobs\n local active_jobs = redis.call('HGETALL', active_key)\n\n -- HGETALL returns [field1, value1, field2, value2, ...]\n for i = 1, #active_jobs, 2 do\n local job_id = active_jobs[i]\n local active_data = active_jobs[i + 1]\n local active = cjson.decode(active_data)\n\n -- Check if job is stalled\n if active.acquiredAt < stalled_cutoff then\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local job = cjson.decode(job_data)\n local current_stalled_count = job.stalledCount or 0\n\n -- Remove from active hash\n redis.call('HDEL', active_key, job_id)\n\n -- Check if job has exceeded max stalled count\n if current_stalled_count >= max_stalled_count then\n -- Job failed permanently, remove data too\n redis.call('HDEL', data_key, job_id)\n else\n -- Recover: increment stalledCount and put back in pending\n job.stalledCount = current_stalled_count + 1\n redis.call('HSET', data_key, job_id, cjson.encode(job))\n -- Score = priority * 1e13 + timestamp\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n recovered = recovered + 1\n end\n end\n end\n end\n\n return recovered\n`\n\n/**\n * Lua script for getting a job record with its status.\n */\nconst GET_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local delayed_key = KEYS[3]\n local active_key = KEYS[4]\n local completed_key = KEYS[5]\n local failed_key = KEYS[6]\n local job_id = ARGV[1]\n\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return nil\n end\n\n local status = nil\n local finished_at = nil\n local error_msg = nil\n\n -- Check status in order\n if redis.call('HEXISTS', active_key, job_id) == 1 then\n status = 'active'\n elseif redis.call('ZSCORE', pending_key, job_id) then\n status = 'pending'\n elseif redis.call('ZSCORE', delayed_key, job_id) then\n status = 'delayed'\n else\n local completed_data = redis.call('HGET', completed_key, job_id)\n if completed_data then\n status = 'completed'\n local record = cjson.decode(completed_data)\n finished_at = record.finishedAt\n else\n local failed_data = redis.call('HGET', failed_key, job_id)\n if failed_data then\n status = 'failed'\n local record = cjson.decode(failed_data)\n finished_at = record.finishedAt\n error_msg = record.error\n end\n end\n end\n\n if not status then\n return nil\n end\n\n return cjson.encode({\n status = status,\n data = cjson.decode(job_data),\n finishedAt = finished_at,\n error = error_msg\n })\n`\n\n/**\n * Lua script for atomically claiming a due schedule.\n * Takes a schedule key as KEYS[1] and checks if it's due.\n * Returns the schedule data if claimed, nil otherwise.\n *\n * This script is called per-schedule from the JS side which handles iteration.\n */\nconst CLAIM_SCHEDULE_SCRIPT = `\n local schedule_key = KEYS[1]\n local now = tonumber(ARGV[1])\n\n -- Get schedule data\n local data = redis.call('HGETALL', schedule_key)\n if #data == 0 then\n return nil\n end\n\n -- Convert HGETALL result to table\n local schedule = {}\n for j = 1, #data, 2 do\n schedule[data[j]] = data[j + 1]\n end\n\n -- Check if schedule is due\n if schedule.status ~= 'active' then\n return nil\n end\n\n local next_run_at = tonumber(schedule.next_run_at)\n if not next_run_at or next_run_at > now then\n return nil\n end\n\n local run_count = tonumber(schedule.run_count or '0')\n local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil\n local to_date = schedule.to_date and tonumber(schedule.to_date) or nil\n\n -- Check limits\n if run_limit and run_count >= run_limit then\n return nil\n end\n\n if to_date and now > to_date then\n return nil\n end\n\n -- This schedule is claimable - atomically update it\n local new_run_count = run_count + 1\n\n -- Calculate new next_run_at (simple interval-based for now)\n -- Complex cron calculation happens in the caller\n local new_next_run_at = ''\n local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil\n if every_ms then\n new_next_run_at = tostring(now + every_ms)\n end\n\n -- Check if we've hit the limit after this run\n if run_limit and new_run_count >= run_limit then\n new_next_run_at = ''\n end\n\n -- Check if past end date\n if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then\n new_next_run_at = ''\n end\n\n -- Update the schedule atomically\n redis.call('HSET', schedule_key,\n 'next_run_at', new_next_run_at,\n 'last_run_at', tostring(now),\n 'run_count', tostring(new_run_count))\n\n -- Return the schedule data (before update) as JSON\n return cjson.encode(schedule)\n`\n\n/**\n * Create a new Redis adapter factory.\n * Accepts either a Redis instance or Redis options.\n *\n * When passing options, the adapter will create and manage\n * the connection lifecycle (closing it on destroy).\n *\n * When passing a Redis instance, the caller is responsible for\n * managing the connection lifecycle.\n */\nexport function redis(config?: RedisConfig) {\n return () => {\n if (config instanceof Redis) {\n return new RedisAdapter(config, false)\n }\n\n const options: RedisOptions = {\n host: 'localhost',\n port: 6379,\n keyPrefix: 'boringnode::queue::',\n db: 0,\n ...config,\n }\n\n const connection = new Redis(options)\n return new RedisAdapter(connection, true)\n }\n}\n\nexport class RedisAdapter implements Adapter {\n readonly #connection: Redis\n readonly #ownsConnection: boolean\n #workerId: string = ''\n\n constructor(connection: Redis, ownsConnection: boolean = false) {\n this.#connection = connection\n this.#ownsConnection = ownsConnection\n }\n\n #getKeys(queue: string) {\n return {\n data: `${redisKey}::${queue}::data`,\n pending: `${redisKey}::${queue}::pending`,\n delayed: `${redisKey}::${queue}::delayed`,\n active: `${redisKey}::${queue}::active`,\n completed: `${redisKey}::${queue}::completed`,\n completedIndex: `${redisKey}::${queue}::completed::index`,\n failed: `${redisKey}::${queue}::failed`,\n failedIndex: `${redisKey}::${queue}::failed::index`,\n }\n }\n\n setWorkerId(workerId: string): void {\n this.#workerId = workerId\n }\n\n async destroy(): Promise<void> {\n if (this.#ownsConnection) {\n await this.#connection.quit()\n }\n }\n\n pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n async popFrom(queue: string): Promise<AcquiredJob | null> {\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n const result = await this.#connection.eval(\n ACQUIRE_JOB_SCRIPT,\n 4,\n keys.data,\n keys.pending,\n keys.active,\n keys.delayed,\n this.#workerId,\n now.toString()\n )\n\n if (!result) {\n return null\n }\n\n return JSON.parse(result as string)\n }\n\n async completeJob(jobId: string, queue: string, removeOnComplete?: JobRetention): Promise<void> {\n const keys = this.#getKeys(queue)\n const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete)\n\n if (!keep) {\n await this.#connection.eval(REMOVE_JOB_SCRIPT, 2, keys.data, keys.active, jobId)\n return\n }\n\n await this.#connection.eval(\n FINALIZE_JOB_SCRIPT,\n 4,\n keys.data,\n keys.active,\n keys.completed,\n keys.completedIndex,\n jobId,\n Date.now().toString(),\n maxAge.toString(),\n maxCount.toString(),\n ''\n )\n }\n\n async failJob(\n jobId: string,\n queue: string,\n error?: Error,\n removeOnFail?: JobRetention\n ): Promise<void> {\n const keys = this.#getKeys(queue)\n const { keep, maxAge, maxCount } = resolveRetention(removeOnFail)\n\n if (!keep) {\n await this.#connection.eval(REMOVE_JOB_SCRIPT, 2, keys.data, keys.active, jobId)\n return\n }\n\n await this.#connection.eval(\n FINALIZE_JOB_SCRIPT,\n 4,\n keys.data,\n keys.active,\n keys.failed,\n keys.failedIndex,\n jobId,\n Date.now().toString(),\n maxAge.toString(),\n maxCount.toString(),\n error?.message || ''\n )\n }\n\n async retryJob(jobId: string, queue: string, retryAt?: Date): Promise<void> {\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n await this.#connection.eval(\n RETRY_JOB_SCRIPT,\n 4,\n keys.data,\n keys.active,\n keys.pending,\n keys.delayed,\n jobId,\n retryAt ? retryAt.getTime().toString() : '0',\n now.toString()\n )\n }\n\n async getJob(jobId: string, queue: string): Promise<JobRecord | null> {\n const keys = this.#getKeys(queue)\n\n const result = await this.#connection.eval(\n GET_JOB_SCRIPT,\n 6,\n keys.data,\n keys.pending,\n keys.delayed,\n keys.active,\n keys.completed,\n keys.failed,\n jobId\n )\n\n if (!result) {\n return null\n }\n\n return JSON.parse(result as string)\n }\n\n push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n async pushLaterOn(queue: string, jobData: JobData, delay: number): Promise<void> {\n const keys = this.#getKeys(queue)\n const executeAt = Date.now() + delay\n\n await this.#connection.eval(\n PUSH_DELAYED_JOB_SCRIPT,\n 2,\n keys.data,\n keys.delayed,\n jobData.id,\n JSON.stringify(jobData),\n executeAt.toString()\n )\n }\n\n async pushOn(queue: string, jobData: JobData): Promise<void> {\n const keys = this.#getKeys(queue)\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const timestamp = Date.now()\n const score = calculateScore(priority, timestamp)\n\n await this.#connection.eval(\n PUSH_JOB_SCRIPT,\n 2,\n keys.data,\n keys.pending,\n jobData.id,\n JSON.stringify(jobData),\n score.toString()\n )\n }\n\n pushMany(jobs: JobData[]): Promise<void> {\n return this.pushManyOn('default', jobs)\n }\n\n async pushManyOn(queue: string, jobs: JobData[]): Promise<void> {\n if (jobs.length === 0) return\n\n const keys = this.#getKeys(queue)\n const now = Date.now()\n const multi = this.#connection.multi()\n\n for (const job of jobs) {\n const priority = job.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n multi.hset(keys.data, job.id, JSON.stringify(job))\n multi.zadd(keys.pending, score, job.id)\n }\n\n await multi.exec()\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(queue: string): Promise<number> {\n const keys = this.#getKeys(queue)\n return this.#connection.zcard(keys.pending)\n }\n\n async recoverStalledJobs(\n queue: string,\n stalledThreshold: number,\n maxStalledCount: number\n ): Promise<number> {\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n const recovered = await this.#connection.eval(\n RECOVER_STALLED_JOBS_SCRIPT,\n 3,\n keys.data,\n keys.active,\n keys.pending,\n now.toString(),\n stalledThreshold.toString(),\n maxStalledCount.toString()\n )\n\n return recovered as number\n }\n\n async upsertSchedule(config: ScheduleConfig): Promise<string> {\n const id = config.id ?? randomUUID()\n const now = Date.now()\n const scheduleKey = `${schedulesKey}::${id}`\n const [existingRunCount, existingCreatedAt] = await this.#connection.hmget(\n scheduleKey,\n 'run_count',\n 'created_at'\n )\n\n const scheduleData: Record<string, string> = {\n id,\n name: config.name,\n payload: JSON.stringify(config.payload),\n timezone: config.timezone,\n status: 'active',\n run_count: existingRunCount ?? '0',\n created_at: existingCreatedAt ?? now.toString(),\n }\n\n if (config.cronExpression !== undefined) scheduleData.cron_expression = config.cronExpression\n if (config.everyMs !== undefined) scheduleData.every_ms = config.everyMs.toString()\n if (config.from !== undefined) scheduleData.from_date = config.from.getTime().toString()\n if (config.to !== undefined) scheduleData.to_date = config.to.getTime().toString()\n if (config.limit !== undefined) scheduleData.run_limit = config.limit.toString()\n\n // Upsert schedule and clear stale optional fields from previous config.\n await this.#connection\n .multi()\n .hdel(scheduleKey, 'cron_expression', 'every_ms', 'from_date', 'to_date', 'run_limit')\n .hset(scheduleKey, scheduleData)\n .sadd(schedulesIndexKey, id)\n .exec()\n\n return id\n }\n\n /**\n * @deprecated Use `upsertSchedule` instead.\n */\n createSchedule(config: ScheduleConfig): Promise<string> {\n return this.upsertSchedule(config)\n }\n\n async getSchedule(id: string): Promise<ScheduleData | null> {\n const scheduleKey = `${schedulesKey}::${id}`\n const data = await this.#connection.hgetall(scheduleKey)\n\n if (!data || Object.keys(data).length === 0) {\n return null\n }\n\n return this.#hashToScheduleData(data)\n }\n\n async listSchedules(options?: ScheduleListOptions): Promise<ScheduleData[]> {\n const ids = await this.#connection.smembers(schedulesIndexKey)\n if (ids.length === 0) {\n return []\n }\n\n const pipeline = this.#connection.pipeline()\n\n for (const id of ids) {\n pipeline.hgetall(`${schedulesKey}::${id}`)\n }\n\n const results = await pipeline.exec()\n if (!results) {\n return []\n }\n\n const schedules: ScheduleData[] = []\n\n for (const [, data] of results) {\n if (!data || Object.keys(data).length === 0) {\n continue\n }\n\n const schedule = this.#hashToScheduleData(data as Record<string, string>)\n\n // Filter by status if provided\n if (options?.status && schedule.status !== options.status) {\n continue\n }\n\n schedules.push(schedule)\n }\n\n return schedules\n }\n\n async updateSchedule(\n id: string,\n updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>\n ): Promise<void> {\n const scheduleKey = `${schedulesKey}::${id}`\n const data: Record<string, string> = {}\n\n if (updates.status !== undefined) data.status = updates.status\n if (updates.nextRunAt !== undefined) {\n data.next_run_at = updates.nextRunAt ? updates.nextRunAt.getTime().toString() : ''\n }\n if (updates.lastRunAt !== undefined) {\n data.last_run_at = updates.lastRunAt ? updates.lastRunAt.getTime().toString() : ''\n }\n if (updates.runCount !== undefined) data.run_count = updates.runCount.toString()\n\n if (Object.keys(data).length > 0) {\n await this.#connection.hset(scheduleKey, data)\n }\n }\n\n async deleteSchedule(id: string): Promise<void> {\n const scheduleKey = `${schedulesKey}::${id}`\n await this.#connection.multi().del(scheduleKey).srem(schedulesIndexKey, id).exec()\n }\n\n async claimDueSchedule(): Promise<ScheduleData | null> {\n const now = Date.now()\n const ids = await this.#connection.smembers(schedulesIndexKey)\n\n // Try to claim each schedule atomically using Lua script\n for (const id of ids) {\n const scheduleKey = `${schedulesKey}::${id}`\n\n // Use Lua script for atomic check-and-update\n const result = await this.#connection.eval(\n CLAIM_SCHEDULE_SCRIPT,\n 1,\n scheduleKey,\n now.toString()\n )\n\n if (!result) {\n continue\n }\n\n const data = JSON.parse(result as string) as Record<string, string>\n\n // If cron expression, we need to recalculate next_run_at properly\n // The Lua script only handles simple interval; cron needs JS cron-parser\n // This is safe because the schedule is already claimed (run_count incremented)\n if (data.cron_expression) {\n const { CronExpressionParser } = await import('cron-parser')\n const cron = CronExpressionParser.parse(data.cron_expression, {\n currentDate: new Date(now),\n tz: data.timezone || 'UTC',\n })\n const nextRun = cron.next().toDate().getTime()\n\n // Check limits before updating\n const runCount = Number.parseInt(data.run_count || '0', 10) + 1\n const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null\n const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null\n\n let newNextRunAt: number | string = nextRun\n\n if (runLimit !== null && runCount >= runLimit) {\n newNextRunAt = ''\n } else if (toDate && nextRun > toDate) {\n newNextRunAt = ''\n }\n\n await this.#connection.hset(scheduleKey, 'next_run_at', newNextRunAt.toString())\n }\n\n return this.#hashToScheduleData(data)\n }\n\n return null\n }\n\n #hashToScheduleData(data: Record<string, string>): ScheduleData {\n return {\n id: data.id,\n name: data.name,\n payload: JSON.parse(data.payload || '{}'),\n cronExpression: data.cron_expression || null,\n everyMs: data.every_ms ? Number.parseInt(data.every_ms, 10) : null,\n timezone: data.timezone || 'UTC',\n from: data.from_date ? new Date(Number.parseInt(data.from_date, 10)) : null,\n to: data.to_date ? new Date(Number.parseInt(data.to_date, 10)) : null,\n limit: data.run_limit ? Number.parseInt(data.run_limit, 10) : null,\n runCount: Number.parseInt(data.run_count || '0', 10),\n nextRunAt: data.next_run_at ? new Date(Number.parseInt(data.next_run_at, 10)) : null,\n lastRunAt: data.last_run_at ? new Date(Number.parseInt(data.last_run_at, 10)) : null,\n status: (data.status as 'active' | 'paused') || 'active',\n createdAt: data.created_at ? new Date(Number.parseInt(data.created_at, 10)) : new Date(),\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/drivers/redis_adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { Redis, type RedisOptions } from 'ioredis'\nimport { DEFAULT_PRIORITY } from '../constants.js'\nimport { calculateScore } from '../utils.js'\nimport type { Adapter, AcquiredJob, PushResult } from '../contracts/adapter.js'\nimport type { DedupOutcome } from '../types/main.js'\nimport type {\n JobData,\n JobRecord,\n JobRetention,\n ScheduleConfig,\n ScheduleData,\n ScheduleListOptions,\n} from '../types/main.js'\nimport { resolveRetention } from '../utils.js'\nimport { encodeRedisJobPayloadOverlay, hydrateRedisJob } from './redis_job_storage.js'\nimport {\n ACQUIRE_JOB_SCRIPT,\n CLAIM_SCHEDULE_SCRIPT,\n FINALIZE_JOB_SCRIPT,\n GET_JOB_SCRIPT,\n PUSH_DEDUP_JOB_SCRIPT,\n PUSH_DELAYED_JOB_SCRIPT,\n PUSH_JOB_SCRIPT,\n RECOVER_STALLED_JOBS_SCRIPT,\n REMOVE_JOB_SCRIPT,\n RENEW_JOBS_SCRIPT,\n RETRY_JOB_SCRIPT,\n} from './redis_scripts.js'\n\nconst redisKey = 'jobs'\nconst schedulesKey = 'schedules'\nconst schedulesIndexKey = 'schedules::index'\ntype RedisConfig = Redis | RedisOptions\n\n/**\n * Create a new Redis adapter factory.\n * Accepts either a Redis instance or Redis options.\n *\n * When passing options, the adapter will create and manage\n * the connection lifecycle (closing it on destroy).\n *\n * When passing a Redis instance, the caller is responsible for\n * managing the connection lifecycle.\n */\nexport function redis(config?: RedisConfig) {\n return () => {\n if (config instanceof Redis) {\n return new RedisAdapter(config, false)\n }\n\n const options: RedisOptions = {\n host: 'localhost',\n port: 6379,\n keyPrefix: 'boringnode::queue::',\n db: 0,\n ...config,\n }\n\n const connection = new Redis(options)\n return new RedisAdapter(connection, true)\n }\n}\n\nexport class RedisAdapter implements Adapter {\n readonly #connection: Redis\n readonly #ownsConnection: boolean\n #workerId: string = ''\n\n constructor(connection: Redis, ownsConnection: boolean = false) {\n this.#connection = connection\n this.#ownsConnection = ownsConnection\n }\n\n #getKeys(queue: string) {\n return {\n data: `${redisKey}::${queue}::data`,\n pending: `${redisKey}::${queue}::pending`,\n delayed: `${redisKey}::${queue}::delayed`,\n active: `${redisKey}::${queue}::active`,\n overlay: `${redisKey}::${queue}::metadata`,\n completed: `${redisKey}::${queue}::completed`,\n completedIndex: `${redisKey}::${queue}::completed::index`,\n failed: `${redisKey}::${queue}::failed`,\n failedIndex: `${redisKey}::${queue}::failed::index`,\n }\n }\n\n #getDedupKey(queue: string, dedupId: string): string {\n return `${this.#getDedupPrefix(queue)}${dedupId}`\n }\n\n #getDedupPrefix(queue: string): string {\n return `${redisKey}::${queue}::dedup::`\n }\n\n setWorkerId(workerId: string): void {\n this.#workerId = workerId\n }\n\n async destroy(): Promise<void> {\n if (this.#ownsConnection) {\n await this.#connection.quit()\n }\n }\n\n pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n async popFrom(queue: string): Promise<AcquiredJob | null> {\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n const result = await this.#connection.eval(\n ACQUIRE_JOB_SCRIPT,\n 5,\n keys.data,\n keys.pending,\n keys.active,\n keys.delayed,\n keys.overlay,\n this.#workerId,\n now.toString()\n )\n\n if (!result) {\n return null\n }\n\n const { data, overlay, acquiredAt } = JSON.parse(result as string) as {\n data: string\n overlay?: string\n acquiredAt: number\n }\n\n return { ...hydrateRedisJob(data, overlay), acquiredAt }\n }\n\n async completeJob(jobId: string, queue: string, removeOnComplete?: JobRetention): Promise<void> {\n const keys = this.#getKeys(queue)\n const dedupPrefix = this.#getDedupPrefix(queue)\n const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete)\n\n if (!keep) {\n await this.#connection.eval(\n REMOVE_JOB_SCRIPT,\n 3,\n keys.data,\n keys.active,\n keys.overlay,\n jobId,\n dedupPrefix\n )\n return\n }\n\n await this.#connection.eval(\n FINALIZE_JOB_SCRIPT,\n 5,\n keys.data,\n keys.active,\n keys.completed,\n keys.completedIndex,\n keys.overlay,\n jobId,\n Date.now().toString(),\n maxAge.toString(),\n maxCount.toString(),\n '',\n dedupPrefix\n )\n }\n\n async failJob(\n jobId: string,\n queue: string,\n error?: Error,\n removeOnFail?: JobRetention\n ): Promise<void> {\n const keys = this.#getKeys(queue)\n const dedupPrefix = this.#getDedupPrefix(queue)\n const { keep, maxAge, maxCount } = resolveRetention(removeOnFail)\n\n if (!keep) {\n await this.#connection.eval(\n REMOVE_JOB_SCRIPT,\n 3,\n keys.data,\n keys.active,\n keys.overlay,\n jobId,\n dedupPrefix\n )\n return\n }\n\n await this.#connection.eval(\n FINALIZE_JOB_SCRIPT,\n 5,\n keys.data,\n keys.active,\n keys.failed,\n keys.failedIndex,\n keys.overlay,\n jobId,\n Date.now().toString(),\n maxAge.toString(),\n maxCount.toString(),\n error?.message || '',\n dedupPrefix\n )\n }\n\n async retryJob(jobId: string, queue: string, retryAt?: Date): Promise<void> {\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n await this.#connection.eval(\n RETRY_JOB_SCRIPT,\n 5,\n keys.data,\n keys.active,\n keys.pending,\n keys.delayed,\n keys.overlay,\n jobId,\n retryAt ? retryAt.getTime().toString() : '0',\n now.toString()\n )\n }\n\n async getJob(jobId: string, queue: string): Promise<JobRecord | null> {\n const keys = this.#getKeys(queue)\n\n const result = await this.#connection.eval(\n GET_JOB_SCRIPT,\n 7,\n keys.data,\n keys.pending,\n keys.delayed,\n keys.active,\n keys.completed,\n keys.failed,\n keys.overlay,\n jobId\n )\n\n if (!result) {\n return null\n }\n\n const record = JSON.parse(result as string) as Omit<JobRecord, 'data'> & {\n data: string\n overlay?: string\n }\n\n return { ...record, data: hydrateRedisJob(record.data, record.overlay) }\n }\n\n push(jobData: JobData): Promise<PushResult | void> {\n return this.pushOn('default', jobData)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<PushResult | void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n async pushLaterOn(queue: string, jobData: JobData, delay: number): Promise<PushResult | void> {\n const keys = this.#getKeys(queue)\n const executeAt = Date.now() + delay\n\n if (jobData.dedup) {\n const dedupKey = this.#getDedupKey(queue, jobData.dedup.id)\n const [payloadData, payloadIsUndefined] = encodeRedisJobPayloadOverlay(jobData.payload)\n const result = (await this.#connection.eval(\n PUSH_DEDUP_JOB_SCRIPT,\n 5,\n keys.data,\n keys.delayed,\n dedupKey,\n keys.pending,\n keys.overlay,\n jobData.id,\n JSON.stringify(jobData),\n executeAt.toString(),\n (jobData.dedup.ttl ?? 0).toString(),\n jobData.dedup.extend ? '1' : '0',\n jobData.dedup.replace ? '1' : '0',\n payloadData,\n payloadIsUndefined\n )) as [string, string]\n return { outcome: result[0] as DedupOutcome, jobId: result[1] }\n }\n\n await this.#connection.eval(\n PUSH_DELAYED_JOB_SCRIPT,\n 3,\n keys.data,\n keys.delayed,\n keys.overlay,\n jobData.id,\n JSON.stringify(jobData),\n executeAt.toString()\n )\n }\n\n async pushOn(queue: string, jobData: JobData): Promise<PushResult | void> {\n const keys = this.#getKeys(queue)\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const timestamp = Date.now()\n const score = calculateScore(priority, timestamp)\n\n if (jobData.dedup) {\n const dedupKey = this.#getDedupKey(queue, jobData.dedup.id)\n const [payloadData, payloadIsUndefined] = encodeRedisJobPayloadOverlay(jobData.payload)\n const result = (await this.#connection.eval(\n PUSH_DEDUP_JOB_SCRIPT,\n 5,\n keys.data,\n keys.pending,\n dedupKey,\n keys.delayed,\n keys.overlay,\n jobData.id,\n JSON.stringify(jobData),\n score.toString(),\n (jobData.dedup.ttl ?? 0).toString(),\n jobData.dedup.extend ? '1' : '0',\n jobData.dedup.replace ? '1' : '0',\n payloadData,\n payloadIsUndefined\n )) as [string, string]\n return { outcome: result[0] as DedupOutcome, jobId: result[1] }\n }\n\n await this.#connection.eval(\n PUSH_JOB_SCRIPT,\n 3,\n keys.data,\n keys.pending,\n keys.overlay,\n jobData.id,\n JSON.stringify(jobData),\n score.toString()\n )\n }\n\n pushMany(jobs: JobData[]): Promise<void> {\n return this.pushManyOn('default', jobs)\n }\n\n async pushManyOn(queue: string, jobs: JobData[]): Promise<void> {\n if (jobs.length === 0) return\n\n if (jobs.some((j) => j.dedup)) {\n throw new Error('dedup is not supported in batch dispatch; use single dispatch')\n }\n\n const keys = this.#getKeys(queue)\n const now = Date.now()\n const multi = this.#connection.multi()\n\n for (const job of jobs) {\n const priority = job.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n multi.hdel(keys.overlay, job.id)\n multi.hset(keys.data, job.id, JSON.stringify(job))\n multi.zadd(keys.pending, score, job.id)\n }\n\n await multi.exec()\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(queue: string): Promise<number> {\n const keys = this.#getKeys(queue)\n return this.#connection.zcard(keys.pending)\n }\n\n async recoverStalledJobs(\n queue: string,\n stalledThreshold: number,\n maxStalledCount: number\n ): Promise<number> {\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n const recovered = await this.#connection.eval(\n RECOVER_STALLED_JOBS_SCRIPT,\n 4,\n keys.data,\n keys.active,\n keys.pending,\n keys.overlay,\n now.toString(),\n stalledThreshold.toString(),\n maxStalledCount.toString(),\n this.#getDedupPrefix(queue)\n )\n\n return recovered as number\n }\n\n async renewJobs(queue: string, jobIds: string[]): Promise<number> {\n if (jobIds.length === 0) {\n return 0\n }\n\n const keys = this.#getKeys(queue)\n const now = Date.now()\n\n const renewed = await this.#connection.eval(\n RENEW_JOBS_SCRIPT,\n 1,\n keys.active,\n now.toString(),\n this.#workerId,\n ...jobIds\n )\n\n return renewed as number\n }\n\n async upsertSchedule(config: ScheduleConfig): Promise<string> {\n const id = config.id ?? randomUUID()\n const now = Date.now()\n const scheduleKey = `${schedulesKey}::${id}`\n const [existingRunCount, existingCreatedAt] = await this.#connection.hmget(\n scheduleKey,\n 'run_count',\n 'created_at'\n )\n\n const scheduleData: Record<string, string> = {\n id,\n name: config.name,\n payload: JSON.stringify(config.payload),\n timezone: config.timezone,\n status: 'active',\n run_count: existingRunCount ?? '0',\n created_at: existingCreatedAt ?? now.toString(),\n }\n\n if (config.cronExpression !== undefined) scheduleData.cron_expression = config.cronExpression\n if (config.everyMs !== undefined) scheduleData.every_ms = config.everyMs.toString()\n if (config.from !== undefined) scheduleData.from_date = config.from.getTime().toString()\n if (config.to !== undefined) scheduleData.to_date = config.to.getTime().toString()\n if (config.limit !== undefined) scheduleData.run_limit = config.limit.toString()\n\n // Upsert schedule and clear stale optional fields from previous config.\n await this.#connection\n .multi()\n .hdel(scheduleKey, 'cron_expression', 'every_ms', 'from_date', 'to_date', 'run_limit')\n .hset(scheduleKey, scheduleData)\n .sadd(schedulesIndexKey, id)\n .exec()\n\n return id\n }\n\n /**\n * @deprecated Use `upsertSchedule` instead.\n */\n createSchedule(config: ScheduleConfig): Promise<string> {\n return this.upsertSchedule(config)\n }\n\n async getSchedule(id: string): Promise<ScheduleData | null> {\n const scheduleKey = `${schedulesKey}::${id}`\n const data = await this.#connection.hgetall(scheduleKey)\n\n if (!data || Object.keys(data).length === 0) {\n return null\n }\n\n return this.#hashToScheduleData(data)\n }\n\n async listSchedules(options?: ScheduleListOptions): Promise<ScheduleData[]> {\n const ids = await this.#connection.smembers(schedulesIndexKey)\n if (ids.length === 0) {\n return []\n }\n\n const pipeline = this.#connection.pipeline()\n\n for (const id of ids) {\n pipeline.hgetall(`${schedulesKey}::${id}`)\n }\n\n const results = await pipeline.exec()\n if (!results) {\n return []\n }\n\n const schedules: ScheduleData[] = []\n\n for (const [, data] of results) {\n if (!data || Object.keys(data).length === 0) {\n continue\n }\n\n const schedule = this.#hashToScheduleData(data as Record<string, string>)\n\n // Filter by status if provided\n if (options?.status && schedule.status !== options.status) {\n continue\n }\n\n schedules.push(schedule)\n }\n\n return schedules\n }\n\n async updateSchedule(\n id: string,\n updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>\n ): Promise<void> {\n const scheduleKey = `${schedulesKey}::${id}`\n const data: Record<string, string> = {}\n\n if (updates.status !== undefined) data.status = updates.status\n if (updates.nextRunAt !== undefined) {\n data.next_run_at = updates.nextRunAt ? updates.nextRunAt.getTime().toString() : ''\n }\n if (updates.lastRunAt !== undefined) {\n data.last_run_at = updates.lastRunAt ? updates.lastRunAt.getTime().toString() : ''\n }\n if (updates.runCount !== undefined) data.run_count = updates.runCount.toString()\n\n if (Object.keys(data).length > 0) {\n await this.#connection.hset(scheduleKey, data)\n }\n }\n\n async deleteSchedule(id: string): Promise<void> {\n const scheduleKey = `${schedulesKey}::${id}`\n await this.#connection.multi().del(scheduleKey).srem(schedulesIndexKey, id).exec()\n }\n\n async claimDueSchedule(): Promise<ScheduleData | null> {\n const now = Date.now()\n const result = await this.#connection.eval(\n CLAIM_SCHEDULE_SCRIPT,\n 2,\n schedulesIndexKey,\n `${schedulesKey}::`,\n now.toString()\n )\n\n if (!result) {\n return null\n }\n\n const data = JSON.parse(result as string) as Record<string, string>\n\n // If cron expression, we need to recalculate next_run_at properly.\n // The Lua script only handles simple interval; cron needs JS cron-parser.\n // This is safe because the schedule is already claimed (run_count incremented).\n if (data.cron_expression) {\n const { CronExpressionParser } = await import('cron-parser')\n const cron = CronExpressionParser.parse(data.cron_expression, {\n currentDate: new Date(now),\n tz: data.timezone || 'UTC',\n })\n const nextRun = cron.next().toDate().getTime()\n\n // Check limits before updating\n const runCount = Number.parseInt(data.run_count || '0', 10) + 1\n const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null\n const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null\n\n let newNextRunAt: number | string = nextRun\n\n if (runLimit !== null && runCount >= runLimit) {\n newNextRunAt = ''\n } else if (toDate && nextRun > toDate) {\n newNextRunAt = ''\n }\n\n await this.#connection.hset(\n `${schedulesKey}::${data.id}`,\n 'next_run_at',\n newNextRunAt.toString()\n )\n }\n\n return this.#hashToScheduleData(data)\n }\n\n #hashToScheduleData(data: Record<string, string>): ScheduleData {\n return {\n id: data.id,\n name: data.name,\n payload: JSON.parse(data.payload || '{}'),\n cronExpression: data.cron_expression || null,\n everyMs: data.every_ms ? Number.parseInt(data.every_ms, 10) : null,\n timezone: data.timezone || 'UTC',\n from: data.from_date ? new Date(Number.parseInt(data.from_date, 10)) : null,\n to: data.to_date ? new Date(Number.parseInt(data.to_date, 10)) : null,\n limit: data.run_limit ? Number.parseInt(data.run_limit, 10) : null,\n runCount: Number.parseInt(data.run_count || '0', 10),\n nextRunAt: data.next_run_at ? new Date(Number.parseInt(data.next_run_at, 10)) : null,\n lastRunAt: data.last_run_at ? new Date(Number.parseInt(data.last_run_at, 10)) : null,\n status: (data.status as 'active' | 'paused') || 'active',\n createdAt: data.created_at ? new Date(Number.parseInt(data.created_at, 10)) : new Date(),\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,17 @@
1
+ import { J as JobData } from '../../job-C4oyCVxR.js';
2
+
3
+ /**
4
+ * Redis stores the original job JSON as an opaque payload in the data hash.
5
+ * Lua scripts write only mutable Redis-side overrides in this overlay so
6
+ * legacy jobs without overlays keep decoding from their original data.
7
+ */
8
+ type RedisJobOverlay = Partial<Pick<JobData, 'attempts' | 'stalledCount'>> & {
9
+ payload?: string;
10
+ payloadUndefined?: boolean;
11
+ };
12
+ declare function hydrateRedisJob(data: string, overlay?: string | null | false): JobData;
13
+ declare function encodeRedisJobPayloadOverlay(payload: JobData['payload']): [string, string];
14
+ declare const REDIS_JOB_STORAGE_LUA = "\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n";
15
+ declare const REDIS_DEDUP_LUA = "\n local function resolve_dedup_existing_job(\n data_key,\n state_key_a,\n state_key_b,\n overlay_key,\n dedup_key,\n extend,\n replace,\n payload_data,\n payload_is_undefined\n )\n local existing = redis.call('GET', dedup_key)\n if not existing then\n return nil\n end\n\n local existing_data = redis.call('HGET', data_key, existing)\n if not existing_data then\n return nil\n end\n\n local in_state_a = redis.call('ZSCORE', state_key_a, existing)\n local in_state_b = redis.call('ZSCORE', state_key_b, existing)\n local replaceable = in_state_a or in_state_b\n local ok, existing_job = pcall(cjson.decode, existing_data)\n local original_ttl = nil\n\n if ok and type(existing_job) == 'table' and existing_job.dedup then\n original_ttl = tonumber(existing_job.dedup.ttl)\n end\n\n if replace == 1 and replaceable then\n if ok and existing_job then\n local overlay = read_job_overlay(overlay_key, existing)\n apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n write_job_overlay(overlay_key, existing, overlay)\n\n if extend == 1 and original_ttl and original_ttl > 0 then\n redis.call('PEXPIRE', dedup_key, original_ttl)\n end\n\n return {'replaced', existing}\n end\n\n return {'skipped', existing}\n end\n\n if extend == 1 and original_ttl and original_ttl > 0 then\n redis.call('PEXPIRE', dedup_key, original_ttl)\n return {'extended', existing}\n end\n\n return {'skipped', existing}\n end\n";
16
+
17
+ export { REDIS_DEDUP_LUA, REDIS_JOB_STORAGE_LUA, type RedisJobOverlay, encodeRedisJobPayloadOverlay, hydrateRedisJob };
@@ -0,0 +1,14 @@
1
+ import {
2
+ REDIS_DEDUP_LUA,
3
+ REDIS_JOB_STORAGE_LUA,
4
+ encodeRedisJobPayloadOverlay,
5
+ hydrateRedisJob
6
+ } from "../../chunk-6IO4P6RB.js";
7
+ import "../../chunk-PZ5AY32C.js";
8
+ export {
9
+ REDIS_DEDUP_LUA,
10
+ REDIS_JOB_STORAGE_LUA,
11
+ encodeRedisJobPayloadOverlay,
12
+ hydrateRedisJob
13
+ };
14
+ //# sourceMappingURL=redis_job_storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Lua script for pushing a job to the queue.
3
+ * Stores job data in the central hash and adds jobId to pending ZSET.
4
+ */
5
+ declare const PUSH_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local overlay_key = KEYS[3]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local score = tonumber(ARGV[3])\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('ZADD', pending_key, score, job_id)\n\n return 1\n";
6
+ /**
7
+ * Lua script for pushing a dedup job.
8
+ *
9
+ * Behavior:
10
+ * - If dedup key exists AND job still exists AND within TTL: apply replace/extend, skip insert.
11
+ * - If dedup key exists but job data missing (orphan): proceed to insert new.
12
+ * - If TTL expired or no prior entry: insert new job, record dedup key with TTL.
13
+ *
14
+ * Replace only applies to jobs in pending or delayed state. Active and
15
+ * retained completed/failed jobs are left untouched (returns 'skipped').
16
+ * Replace swaps the payload only — priority/queue/delay/groupId/dedup
17
+ * options of the existing job are preserved.
18
+ *
19
+ * Extend uses the ORIGINAL ttl recorded on the existing job (stored in
20
+ * its dedup field), not the ttl arg of the current dispatch. Matches
21
+ * Knex/Fake behavior: extend resets the clock but never changes the
22
+ * window length.
23
+ *
24
+ * Returns {outcome, job_id}: outcome ∈ 'added' | 'skipped' | 'replaced' | 'extended'.
25
+ */
26
+ declare const PUSH_DEDUP_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local target_key = KEYS[2]\n local dedup_key = KEYS[3]\n local other_state_key = KEYS[4]\n local overlay_key = KEYS[5]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local score = tonumber(ARGV[3])\n local ttl = tonumber(ARGV[4])\n local extend = tonumber(ARGV[5])\n local replace = tonumber(ARGV[6])\n local payload_data = ARGV[7]\n local payload_is_undefined = tonumber(ARGV[8])\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n local function resolve_dedup_existing_job(\n data_key,\n state_key_a,\n state_key_b,\n overlay_key,\n dedup_key,\n extend,\n replace,\n payload_data,\n payload_is_undefined\n )\n local existing = redis.call('GET', dedup_key)\n if not existing then\n return nil\n end\n\n local existing_data = redis.call('HGET', data_key, existing)\n if not existing_data then\n return nil\n end\n\n local in_state_a = redis.call('ZSCORE', state_key_a, existing)\n local in_state_b = redis.call('ZSCORE', state_key_b, existing)\n local replaceable = in_state_a or in_state_b\n local ok, existing_job = pcall(cjson.decode, existing_data)\n local original_ttl = nil\n\n if ok and type(existing_job) == 'table' and existing_job.dedup then\n original_ttl = tonumber(existing_job.dedup.ttl)\n end\n\n if replace == 1 and replaceable then\n if ok and existing_job then\n local overlay = read_job_overlay(overlay_key, existing)\n apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n write_job_overlay(overlay_key, existing, overlay)\n\n if extend == 1 and original_ttl and original_ttl > 0 then\n redis.call('PEXPIRE', dedup_key, original_ttl)\n end\n\n return {'replaced', existing}\n end\n\n return {'skipped', existing}\n end\n\n if extend == 1 and original_ttl and original_ttl > 0 then\n redis.call('PEXPIRE', dedup_key, original_ttl)\n return {'extended', existing}\n end\n\n return {'skipped', existing}\n end\n\n\n local existing_result = resolve_dedup_existing_job(\n data_key,\n target_key,\n other_state_key,\n overlay_key,\n dedup_key,\n extend,\n replace,\n payload_data,\n payload_is_undefined\n )\n if existing_result then\n return existing_result\n end\n\n store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('ZADD', target_key, score, job_id)\n redis.call('SET', dedup_key, job_id)\n if ttl > 0 then\n redis.call('PEXPIRE', dedup_key, ttl)\n end\n return {'added', job_id}\n";
27
+ /**
28
+ * Lua script for pushing a delayed job.
29
+ * Stores job data in the central hash and adds jobId to delayed ZSET.
30
+ */
31
+ declare const PUSH_DELAYED_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local delayed_key = KEYS[2]\n local overlay_key = KEYS[3]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local execute_at = tonumber(ARGV[3])\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('ZADD', delayed_key, execute_at, job_id)\n\n return 1\n";
32
+ /**
33
+ * Lua script for atomic job acquisition.
34
+ * 1. Check and process delayed jobs
35
+ * 2. Pop from pending queue
36
+ * 3. Add to active hash with worker info
37
+ * 4. Return job data
38
+ */
39
+ declare const ACQUIRE_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local active_key = KEYS[3]\n local delayed_key = KEYS[4]\n local overlay_key = KEYS[5]\n local worker_id = ARGV[1]\n local now = tonumber(ARGV[2])\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n -- Process delayed jobs: move ready jobs to pending\n local ready_job_ids = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)\n if #ready_job_ids > 0 then\n for i = 1, #ready_job_ids do\n local job_id = ready_job_ids[i]\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local job = cjson.decode(job_data)\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n redis.call('ZREM', delayed_key, job_id)\n end\n end\n end\n\n -- Pop highest priority job (lowest score)\n local result = redis.call('ZPOPMIN', pending_key)\n if not result or #result == 0 then\n return nil\n end\n\n local job_id = result[1]\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return nil\n end\n\n -- Store in active hash (without data, it's in data_key)\n local active_data = cjson.encode({\n workerId = worker_id,\n acquiredAt = now\n })\n redis.call('HSET', active_key, job_id, active_data)\n\n return encode_job_result(job_data, overlay_key, job_id, {\n acquiredAt = now\n })\n";
40
+ /**
41
+ * Lua script for removing a job completely (no history).
42
+ * Also cleans up the dedup key if the job had dedup metadata.
43
+ */
44
+ declare const REMOVE_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local overlay_key = KEYS[3]\n local job_id = ARGV[1]\n local dedup_prefix = ARGV[2]\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Read job data to extract dedup.id before deleting\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local ok, job = pcall(cjson.decode, job_data)\n if ok and job and job.dedup and job.dedup.id then\n local dkey = dedup_prefix .. job.dedup.id\n if redis.call('GET', dkey) == job_id then\n redis.call('DEL', dkey)\n end\n end\n end\n\n redis.call('HDEL', active_key, job_id)\n delete_job_data(data_key, overlay_key, job_id)\n\n return 1\n";
45
+ /**
46
+ * Lua script for finalizing a job in history.
47
+ * Removes from active, stores finalization info, and prunes old records.
48
+ * When pruning removes job data, also deletes the associated dedup key.
49
+ */
50
+ declare const FINALIZE_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local history_key = KEYS[3]\n local index_key = KEYS[4]\n local overlay_key = KEYS[5]\n local job_id = ARGV[1]\n local now = tonumber(ARGV[2])\n local max_age = tonumber(ARGV[3])\n local max_count = tonumber(ARGV[4])\n local error_message = ARGV[5]\n local dedup_prefix = ARGV[6]\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n -- Verify job is active\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Store finalization info (data stays in data_key)\n local record = {\n finishedAt = now\n }\n if error_message and error_message ~= '' then\n record.error = error_message\n end\n redis.call('HSET', history_key, job_id, cjson.encode(record))\n redis.call('ZADD', index_key, now, job_id)\n\n local function delete_dedup_for(ids)\n for i = 1, #ids do\n local id = ids[i]\n local d = redis.call('HGET', data_key, id)\n if d then\n local ok, job = pcall(cjson.decode, d)\n if ok and job and job.dedup and job.dedup.id then\n local dkey = dedup_prefix .. job.dedup.id\n if redis.call('GET', dkey) == id then\n redis.call('DEL', dkey)\n end\n end\n end\n end\n end\n\n -- Prune by age\n if max_age and max_age > 0 then\n local cutoff = now - max_age\n local expired = redis.call('ZRANGEBYSCORE', index_key, 0, cutoff)\n if #expired > 0 then\n delete_dedup_for(expired)\n redis.call('ZREM', index_key, unpack(expired))\n redis.call('HDEL', history_key, unpack(expired))\n delete_jobs_data(data_key, overlay_key, expired)\n end\n end\n\n -- Prune by count\n if max_count and max_count > 0 then\n local size = tonumber(redis.call('ZCARD', index_key))\n if size > max_count then\n local excess = size - max_count\n local stale = redis.call('ZRANGE', index_key, 0, excess - 1)\n if #stale > 0 then\n delete_dedup_for(stale)\n redis.call('ZREM', index_key, unpack(stale))\n redis.call('HDEL', history_key, unpack(stale))\n delete_jobs_data(data_key, overlay_key, stale)\n end\n end\n end\n\n return 1\n";
51
+ /**
52
+ * Lua script for retrying a job.
53
+ * 1. Verify job is active
54
+ * 2. Remove from active hash
55
+ * 3. Increment attempts in data
56
+ * 4. Add back to pending (or delayed if retryAt is set)
57
+ */
58
+ declare const RETRY_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local pending_key = KEYS[3]\n local delayed_key = KEYS[4]\n local overlay_key = KEYS[5]\n local job_id = ARGV[1]\n local retry_at = tonumber(ARGV[2])\n local now = tonumber(ARGV[3])\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n -- Verify job is active\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Get job data\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return 0\n end\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Increment attempts without rewriting opaque job JSON.\n local job = cjson.decode(job_data)\n local overlay = read_job_overlay(overlay_key, job_id)\n overlay.attempts = (overlay.attempts or job.attempts or 0) + 1\n write_job_overlay(overlay_key, job_id, overlay)\n\n -- Add back to pending or delayed\n if retry_at and retry_at > now then\n redis.call('ZADD', delayed_key, retry_at, job_id)\n else\n -- Score = priority * 1e13 + timestamp\n -- Lower score = higher priority, FIFO within same priority\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n end\n\n return 1\n";
59
+ /**
60
+ * Lua script for recovering stalled jobs.
61
+ * Scans the active hash for jobs that have been active too long.
62
+ * - Jobs within maxStalledCount: move back to pending with incremented stalledCount
63
+ * - Jobs exceeding maxStalledCount: remove permanently (fail)
64
+ * Returns the number of recovered jobs (not including failed ones).
65
+ */
66
+ declare const RECOVER_STALLED_JOBS_SCRIPT = "\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local pending_key = KEYS[3]\n local overlay_key = KEYS[4]\n local now = tonumber(ARGV[1])\n local stalled_threshold = tonumber(ARGV[2])\n local max_stalled_count = tonumber(ARGV[3])\n local dedup_prefix = ARGV[4]\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n local recovered = 0\n local stalled_cutoff = now - stalled_threshold\n\n -- Get all active jobs\n local active_jobs = redis.call('HGETALL', active_key)\n\n -- HGETALL returns [field1, value1, field2, value2, ...]\n for i = 1, #active_jobs, 2 do\n local job_id = active_jobs[i]\n local active_data = active_jobs[i + 1]\n local active = cjson.decode(active_data)\n\n -- Check if job is stalled\n if active.acquiredAt < stalled_cutoff then\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local job = cjson.decode(job_data)\n local overlay = read_job_overlay(overlay_key, job_id)\n local current_stalled_count = overlay.stalledCount or job.stalledCount or 0\n\n -- Remove from active hash\n redis.call('HDEL', active_key, job_id)\n\n -- Check if job has exceeded max stalled count\n if current_stalled_count >= max_stalled_count then\n -- Job failed permanently, remove data + dedup key (only if pointer still ours)\n if job.dedup and job.dedup.id then\n local dkey = dedup_prefix .. job.dedup.id\n if redis.call('GET', dkey) == job_id then\n redis.call('DEL', dkey)\n end\n end\n delete_job_data(data_key, overlay_key, job_id)\n else\n -- Recover: increment stalledCount without rewriting opaque job JSON.\n overlay.stalledCount = current_stalled_count + 1\n write_job_overlay(overlay_key, job_id, overlay)\n -- Score = priority * 1e13 + timestamp\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n recovered = recovered + 1\n end\n end\n end\n end\n\n return recovered\n";
67
+ /**
68
+ * Lua script for renewing the acquired timestamp of in-flight jobs (heartbeat).
69
+ * Only entries still present in the active hash AND still owned by the calling
70
+ * worker are renewed, so a job that was already recovered, finalized, or
71
+ * re-acquired by another worker is never resurrected by a late heartbeat.
72
+ * Preserves the existing worker info, updating only acquiredAt.
73
+ * Returns the number of jobs renewed.
74
+ */
75
+ declare const RENEW_JOBS_SCRIPT = "\n local active_key = KEYS[1]\n local now = tonumber(ARGV[1])\n local worker_id = ARGV[2]\n\n local renewed = 0\n for i = 3, #ARGV do\n local job_id = ARGV[i]\n local active_data = redis.call('HGET', active_key, job_id)\n if active_data then\n local active = cjson.decode(active_data)\n -- Only the worker that currently owns the lease may renew it.\n if active.workerId == worker_id then\n active.acquiredAt = now\n redis.call('HSET', active_key, job_id, cjson.encode(active))\n renewed = renewed + 1\n end\n end\n end\n\n return renewed\n";
76
+ /**
77
+ * Lua script for getting a job record with its status.
78
+ */
79
+ declare const GET_JOB_SCRIPT = "\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local delayed_key = KEYS[3]\n local active_key = KEYS[4]\n local completed_key = KEYS[5]\n local failed_key = KEYS[6]\n local overlay_key = KEYS[7]\n local job_id = ARGV[1]\n\n\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n\n\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return nil\n end\n\n local status = nil\n local finished_at = nil\n local error_msg = nil\n\n -- Check status in order\n if redis.call('HEXISTS', active_key, job_id) == 1 then\n status = 'active'\n elseif redis.call('ZSCORE', pending_key, job_id) then\n status = 'pending'\n elseif redis.call('ZSCORE', delayed_key, job_id) then\n status = 'delayed'\n else\n local completed_data = redis.call('HGET', completed_key, job_id)\n if completed_data then\n status = 'completed'\n local record = cjson.decode(completed_data)\n finished_at = record.finishedAt\n else\n local failed_data = redis.call('HGET', failed_key, job_id)\n if failed_data then\n status = 'failed'\n local record = cjson.decode(failed_data)\n finished_at = record.finishedAt\n error_msg = record.error\n end\n end\n end\n\n if not status then\n return nil\n end\n\n return encode_job_result(job_data, overlay_key, job_id, {\n status = status,\n finishedAt = finished_at,\n error = error_msg\n })\n";
80
+ /**
81
+ * Lua script for atomically claiming a due schedule.
82
+ * Iterates the schedule index server-side and claims the first due schedule.
83
+ * Returns the schedule data if claimed, nil otherwise.
84
+ */
85
+ declare const CLAIM_SCHEDULE_SCRIPT = "\n local schedules_index_key = KEYS[1]\n local schedule_key_prefix = KEYS[2]\n local now = tonumber(ARGV[1])\n\n local ids = redis.call('SMEMBERS', schedules_index_key)\n\n for i = 1, #ids do\n local schedule_key = schedule_key_prefix .. ids[i]\n\n -- Get schedule data\n local data = redis.call('HGETALL', schedule_key)\n if #data > 0 then\n -- Convert HGETALL result to table\n local schedule = {}\n for j = 1, #data, 2 do\n schedule[data[j]] = data[j + 1]\n end\n\n -- Check if schedule is due\n if schedule.status == 'active' then\n local next_run_at = tonumber(schedule.next_run_at)\n\n if next_run_at and next_run_at <= now then\n local run_count = tonumber(schedule.run_count or '0')\n local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil\n local to_date = schedule.to_date and tonumber(schedule.to_date) or nil\n\n -- Check limits\n if not (run_limit and run_count >= run_limit) and not (to_date and now > to_date) then\n -- This schedule is claimable - atomically update it\n local new_run_count = run_count + 1\n\n -- Calculate new next_run_at (simple interval-based for now)\n -- Complex cron calculation happens in the caller\n local new_next_run_at = ''\n local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil\n if every_ms then\n new_next_run_at = tostring(now + every_ms)\n end\n\n -- Check if we've hit the limit after this run\n if run_limit and new_run_count >= run_limit then\n new_next_run_at = ''\n end\n\n -- Check if past end date\n if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then\n new_next_run_at = ''\n end\n\n -- Update the schedule atomically\n redis.call('HSET', schedule_key,\n 'next_run_at', new_next_run_at,\n 'last_run_at', tostring(now),\n 'run_count', tostring(new_run_count))\n\n -- Return the schedule data (before update) as JSON\n return cjson.encode(schedule)\n end\n end\n end\n end\n end\n\n return nil\n";
86
+
87
+ export { ACQUIRE_JOB_SCRIPT, CLAIM_SCHEDULE_SCRIPT, FINALIZE_JOB_SCRIPT, GET_JOB_SCRIPT, PUSH_DEDUP_JOB_SCRIPT, PUSH_DELAYED_JOB_SCRIPT, PUSH_JOB_SCRIPT, RECOVER_STALLED_JOBS_SCRIPT, REMOVE_JOB_SCRIPT, RENEW_JOBS_SCRIPT, RETRY_JOB_SCRIPT };
@@ -0,0 +1,29 @@
1
+ import {
2
+ ACQUIRE_JOB_SCRIPT,
3
+ CLAIM_SCHEDULE_SCRIPT,
4
+ FINALIZE_JOB_SCRIPT,
5
+ GET_JOB_SCRIPT,
6
+ PUSH_DEDUP_JOB_SCRIPT,
7
+ PUSH_DELAYED_JOB_SCRIPT,
8
+ PUSH_JOB_SCRIPT,
9
+ RECOVER_STALLED_JOBS_SCRIPT,
10
+ REMOVE_JOB_SCRIPT,
11
+ RENEW_JOBS_SCRIPT,
12
+ RETRY_JOB_SCRIPT
13
+ } from "../../chunk-S37X3CBO.js";
14
+ import "../../chunk-6IO4P6RB.js";
15
+ import "../../chunk-PZ5AY32C.js";
16
+ export {
17
+ ACQUIRE_JOB_SCRIPT,
18
+ CLAIM_SCHEDULE_SCRIPT,
19
+ FINALIZE_JOB_SCRIPT,
20
+ GET_JOB_SCRIPT,
21
+ PUSH_DEDUP_JOB_SCRIPT,
22
+ PUSH_DELAYED_JOB_SCRIPT,
23
+ PUSH_JOB_SCRIPT,
24
+ RECOVER_STALLED_JOBS_SCRIPT,
25
+ REMOVE_JOB_SCRIPT,
26
+ RENEW_JOBS_SCRIPT,
27
+ RETRY_JOB_SCRIPT
28
+ };
29
+ //# sourceMappingURL=redis_scripts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,4 +1,4 @@
1
- import { A as Adapter, J as JobData, b as AcquiredJob, c as JobRetention, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-DImdhRFO.js';
1
+ import { A as Adapter, J as JobData, b as AcquiredJob, c as JobRetention, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-C4oyCVxR.js';
2
2
 
3
3
  /**
4
4
  * Create a sync adapter factory.
@@ -25,6 +25,7 @@ declare class SyncAdapter implements Adapter {
25
25
  failJob(_jobId: string, _queue: string, _error?: Error, _removeOnFail?: JobRetention): Promise<void>;
26
26
  retryJob(_jobId: string, _queue: string, _retryAt?: Date): Promise<void>;
27
27
  recoverStalledJobs(_queue: string, _stalledThreshold: number, _maxStalledCount: number): Promise<number>;
28
+ renewJobs(_queue: string, _jobIds: string[]): Promise<number>;
28
29
  getJob(_jobId: string, _queue: string): Promise<null>;
29
30
  destroy(): Promise<void>;
30
31
  upsertSchedule(_config: ScheduleConfig): Promise<string>;
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  Locator,
6
6
  QueueManager
7
- } from "../../chunk-VHN3XZDC.js";
7
+ } from "../../chunk-AHUVTAI7.js";
8
8
  import {
9
9
  executeChannel
10
10
  } from "../../chunk-WVLSICD4.js";
@@ -45,6 +45,9 @@ var SyncAdapter = class {
45
45
  return this.pushManyOn("default", jobs);
46
46
  }
47
47
  async pushManyOn(queue, jobs) {
48
+ if (jobs.some((j) => j.dedup)) {
49
+ throw new Error("dedup is not supported in batch dispatch; use single dispatch");
50
+ }
48
51
  for (const job of jobs) {
49
52
  await this.pushOn(queue, job);
50
53
  }
@@ -73,6 +76,9 @@ var SyncAdapter = class {
73
76
  recoverStalledJobs(_queue, _stalledThreshold, _maxStalledCount) {
74
77
  return Promise.resolve(0);
75
78
  }
79
+ renewJobs(_queue, _jobIds) {
80
+ return Promise.resolve(0);
81
+ }
76
82
  getJob(_jobId, _queue) {
77
83
  return Promise.resolve(null);
78
84
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/drivers/sync_adapter.ts"],"sourcesContent":["import { setTimeout as sleep } from 'node:timers/promises'\nimport { Locator } from '../locator.js'\nimport { QueueManager } from '../queue_manager.js'\nimport { JobExecutionRuntime } from '../job_runtime.js'\nimport { executeChannel } from '../tracing_channels.js'\nimport type { Adapter, AcquiredJob } from '../contracts/adapter.js'\nimport type {\n JobContext,\n JobData,\n JobRetention,\n ScheduleConfig,\n ScheduleData,\n ScheduleListOptions,\n} from '../types/main.js'\nimport type { JobExecuteMessage } from '../types/tracing_channels.js'\nimport { DEFAULT_PRIORITY } from '../constants.js'\n\n/**\n * Create a sync adapter factory.\n */\nexport function sync() {\n return () => new SyncAdapter()\n}\n\n/**\n * Sync adapter executes jobs immediately when pushed.\n * Pop/complete/fail/retry are not supported as jobs are executed synchronously.\n */\nexport class SyncAdapter implements Adapter {\n setWorkerId(_workerId: string): void {}\n\n push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n pushOn(queue: string, jobData: JobData): Promise<void> {\n return this.#execute(jobData, queue)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n pushLaterOn(queue: string, jobData: JobData, delay: number): Promise<void> {\n setTimeout(() => {\n void this.#execute(jobData, queue).catch((error) => {\n QueueManager.getLogger().error(\n { err: error, jobId: jobData.id, jobName: jobData.name, queue },\n 'Failed to execute delayed sync job'\n )\n })\n }, delay)\n\n return Promise.resolve()\n }\n\n pushMany(jobs: JobData[]): Promise<void> {\n return this.pushManyOn('default', jobs)\n }\n\n async pushManyOn(queue: string, jobs: JobData[]): Promise<void> {\n for (const job of jobs) {\n await this.pushOn(queue, job)\n }\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(_queue: string): Promise<number> {\n return Promise.resolve(0)\n }\n\n pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n popFrom(_queue: string): Promise<AcquiredJob | null> {\n throw new Error('SyncAdapter does not support pop - jobs are executed immediately on push')\n }\n\n completeJob(_jobId: string, _queue: string, _removeOnComplete?: JobRetention): Promise<void> {\n return Promise.resolve()\n }\n\n failJob(\n _jobId: string,\n _queue: string,\n _error?: Error,\n _removeOnFail?: JobRetention\n ): Promise<void> {\n return Promise.resolve()\n }\n\n retryJob(_jobId: string, _queue: string, _retryAt?: Date): Promise<void> {\n return Promise.resolve()\n }\n\n recoverStalledJobs(\n _queue: string,\n _stalledThreshold: number,\n _maxStalledCount: number\n ): Promise<number> {\n // SyncAdapter has no stalled jobs - jobs are executed immediately\n return Promise.resolve(0)\n }\n\n getJob(_jobId: string, _queue: string): Promise<null> {\n return Promise.resolve(null)\n }\n\n destroy(): Promise<void> {\n return Promise.resolve()\n }\n\n upsertSchedule(_config: ScheduleConfig): Promise<string> {\n // No-op: schedules don't make sense for sync adapter\n // Return a fake ID so code doesn't break in dev\n return Promise.resolve(`sync-schedule-${Date.now()}`)\n }\n\n /**\n * @deprecated Use `upsertSchedule` instead.\n */\n createSchedule(config: ScheduleConfig): Promise<string> {\n return this.upsertSchedule(config)\n }\n\n getSchedule(_id: string): Promise<ScheduleData | null> {\n return Promise.resolve(null)\n }\n\n listSchedules(_options?: ScheduleListOptions): Promise<ScheduleData[]> {\n return Promise.resolve([])\n }\n\n updateSchedule(\n _id: string,\n _updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>\n ): Promise<void> {\n return Promise.resolve()\n }\n\n deleteSchedule(_id: string): Promise<void> {\n return Promise.resolve()\n }\n\n claimDueSchedule(): Promise<ScheduleData | null> {\n // SyncAdapter doesn't support scheduling\n return Promise.resolve(null)\n }\n\n async #execute(jobData: JobData, queue: string = 'default'): Promise<void> {\n const JobClass = Locator.get(jobData.name)\n\n if (!JobClass) {\n throw new Error(`Job class ${jobData.name} not found.`)\n }\n\n const options = JobClass.options || {}\n const configResolver = QueueManager.getConfigResolver()\n const runtime = JobExecutionRuntime.from({\n jobName: jobData.name,\n options,\n retryConfig: configResolver.resolveRetryConfig(queue, options),\n defaultTimeout: configResolver.getWorkerTimeout(),\n })\n const jobFactory = QueueManager.getJobFactory()\n const executionWrapper = QueueManager.getExecutionWrapper()\n let attempts = jobData.attempts\n\n while (true) {\n const now = Date.now()\n const acquiredJob: AcquiredJob = { ...jobData, attempts, acquiredAt: now }\n\n const context: JobContext = {\n jobId: jobData.id,\n name: jobData.name,\n attempt: attempts + 1,\n queue,\n priority: jobData.priority ?? DEFAULT_PRIORITY,\n acquiredAt: new Date(now),\n stalledCount: jobData.stalledCount ?? 0,\n }\n\n const jobInstance = jobFactory ? await jobFactory(JobClass) : new JobClass()\n\n const startTime = performance.now()\n const executeMessage: JobExecuteMessage = { job: acquiredJob, queue }\n\n const run = () => {\n return executeChannel.tracePromise(async () => {\n try {\n await runtime.execute(jobInstance, jobData.payload, context)\n executeMessage.status = 'completed'\n } catch (error) {\n const outcome = runtime.resolveFailure(error as Error, attempts)\n executeMessage.error = error as Error\n\n if (outcome.type === 'failed') {\n executeMessage.status = 'failed'\n await jobInstance.failed?.(outcome.hookError)\n } else if (outcome.type === 'retry') {\n executeMessage.status = 'retrying'\n executeMessage.nextRetryAt = outcome.retryAt\n }\n }\n\n executeMessage.duration = Number((performance.now() - startTime).toFixed(2))\n }, executeMessage)\n }\n\n await executionWrapper(run, acquiredJob, queue)\n\n if (executeMessage.status !== 'retrying') return\n\n attempts++\n\n if (executeMessage.nextRetryAt) {\n const delay = executeMessage.nextRetryAt.getTime() - Date.now()\n if (delay > 0) {\n await sleep(delay)\n }\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/drivers/sync_adapter.ts"],"sourcesContent":["import { setTimeout as sleep } from 'node:timers/promises'\nimport { Locator } from '../locator.js'\nimport { QueueManager } from '../queue_manager.js'\nimport { JobExecutionRuntime } from '../job_runtime.js'\nimport { executeChannel } from '../tracing_channels.js'\nimport type { Adapter, AcquiredJob } from '../contracts/adapter.js'\nimport type {\n JobContext,\n JobData,\n JobRetention,\n ScheduleConfig,\n ScheduleData,\n ScheduleListOptions,\n} from '../types/main.js'\nimport type { JobExecuteMessage } from '../types/tracing_channels.js'\nimport { DEFAULT_PRIORITY } from '../constants.js'\n\n/**\n * Create a sync adapter factory.\n */\nexport function sync() {\n return () => new SyncAdapter()\n}\n\n/**\n * Sync adapter executes jobs immediately when pushed.\n * Pop/complete/fail/retry are not supported as jobs are executed synchronously.\n */\nexport class SyncAdapter implements Adapter {\n setWorkerId(_workerId: string): void {}\n\n push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n pushOn(queue: string, jobData: JobData): Promise<void> {\n return this.#execute(jobData, queue)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n pushLaterOn(queue: string, jobData: JobData, delay: number): Promise<void> {\n setTimeout(() => {\n void this.#execute(jobData, queue).catch((error) => {\n QueueManager.getLogger().error(\n { err: error, jobId: jobData.id, jobName: jobData.name, queue },\n 'Failed to execute delayed sync job'\n )\n })\n }, delay)\n\n return Promise.resolve()\n }\n\n pushMany(jobs: JobData[]): Promise<void> {\n return this.pushManyOn('default', jobs)\n }\n\n async pushManyOn(queue: string, jobs: JobData[]): Promise<void> {\n if (jobs.some((j) => j.dedup)) {\n throw new Error('dedup is not supported in batch dispatch; use single dispatch')\n }\n\n for (const job of jobs) {\n await this.pushOn(queue, job)\n }\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(_queue: string): Promise<number> {\n return Promise.resolve(0)\n }\n\n pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n popFrom(_queue: string): Promise<AcquiredJob | null> {\n throw new Error('SyncAdapter does not support pop - jobs are executed immediately on push')\n }\n\n completeJob(_jobId: string, _queue: string, _removeOnComplete?: JobRetention): Promise<void> {\n return Promise.resolve()\n }\n\n failJob(\n _jobId: string,\n _queue: string,\n _error?: Error,\n _removeOnFail?: JobRetention\n ): Promise<void> {\n return Promise.resolve()\n }\n\n retryJob(_jobId: string, _queue: string, _retryAt?: Date): Promise<void> {\n return Promise.resolve()\n }\n\n recoverStalledJobs(\n _queue: string,\n _stalledThreshold: number,\n _maxStalledCount: number\n ): Promise<number> {\n // SyncAdapter has no stalled jobs - jobs are executed immediately\n return Promise.resolve(0)\n }\n\n renewJobs(_queue: string, _jobIds: string[]): Promise<number> {\n // SyncAdapter executes jobs immediately - there is nothing to renew\n return Promise.resolve(0)\n }\n\n getJob(_jobId: string, _queue: string): Promise<null> {\n return Promise.resolve(null)\n }\n\n destroy(): Promise<void> {\n return Promise.resolve()\n }\n\n upsertSchedule(_config: ScheduleConfig): Promise<string> {\n // No-op: schedules don't make sense for sync adapter\n // Return a fake ID so code doesn't break in dev\n return Promise.resolve(`sync-schedule-${Date.now()}`)\n }\n\n /**\n * @deprecated Use `upsertSchedule` instead.\n */\n createSchedule(config: ScheduleConfig): Promise<string> {\n return this.upsertSchedule(config)\n }\n\n getSchedule(_id: string): Promise<ScheduleData | null> {\n return Promise.resolve(null)\n }\n\n listSchedules(_options?: ScheduleListOptions): Promise<ScheduleData[]> {\n return Promise.resolve([])\n }\n\n updateSchedule(\n _id: string,\n _updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>\n ): Promise<void> {\n return Promise.resolve()\n }\n\n deleteSchedule(_id: string): Promise<void> {\n return Promise.resolve()\n }\n\n claimDueSchedule(): Promise<ScheduleData | null> {\n // SyncAdapter doesn't support scheduling\n return Promise.resolve(null)\n }\n\n async #execute(jobData: JobData, queue: string = 'default'): Promise<void> {\n const JobClass = Locator.get(jobData.name)\n\n if (!JobClass) {\n throw new Error(`Job class ${jobData.name} not found.`)\n }\n\n const options = JobClass.options || {}\n const configResolver = QueueManager.getConfigResolver()\n const runtime = JobExecutionRuntime.from({\n jobName: jobData.name,\n options,\n retryConfig: configResolver.resolveRetryConfig(queue, options),\n defaultTimeout: configResolver.getWorkerTimeout(),\n })\n const jobFactory = QueueManager.getJobFactory()\n const executionWrapper = QueueManager.getExecutionWrapper()\n let attempts = jobData.attempts\n\n while (true) {\n const now = Date.now()\n const acquiredJob: AcquiredJob = { ...jobData, attempts, acquiredAt: now }\n\n const context: JobContext = {\n jobId: jobData.id,\n name: jobData.name,\n attempt: attempts + 1,\n queue,\n priority: jobData.priority ?? DEFAULT_PRIORITY,\n acquiredAt: new Date(now),\n stalledCount: jobData.stalledCount ?? 0,\n }\n\n const jobInstance = jobFactory ? await jobFactory(JobClass) : new JobClass()\n\n const startTime = performance.now()\n const executeMessage: JobExecuteMessage = { job: acquiredJob, queue }\n\n const run = () => {\n return executeChannel.tracePromise(async () => {\n try {\n await runtime.execute(jobInstance, jobData.payload, context)\n executeMessage.status = 'completed'\n } catch (error) {\n const outcome = runtime.resolveFailure(error as Error, attempts)\n executeMessage.error = error as Error\n\n if (outcome.type === 'failed') {\n executeMessage.status = 'failed'\n await jobInstance.failed?.(outcome.hookError)\n } else if (outcome.type === 'retry') {\n executeMessage.status = 'retrying'\n executeMessage.nextRetryAt = outcome.retryAt\n }\n }\n\n executeMessage.duration = Number((performance.now() - startTime).toFixed(2))\n }, executeMessage)\n }\n\n await executionWrapper(run, acquiredJob, queue)\n\n if (executeMessage.status !== 'retrying') return\n\n attempts++\n\n if (executeMessage.nextRetryAt) {\n const delay = executeMessage.nextRetryAt.getTime() - Date.now()\n if (delay > 0) {\n await sleep(delay)\n }\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,cAAc,aAAa;AAoB7B,SAAS,OAAO;AACrB,SAAO,MAAM,IAAI,YAAY;AAC/B;AAMO,IAAM,cAAN,MAAqC;AAAA,EAC1C,YAAY,WAAyB;AAAA,EAAC;AAAA,EAEtC,KAAK,SAAiC;AACpC,WAAO,KAAK,OAAO,WAAW,OAAO;AAAA,EACvC;AAAA,EAEA,OAAO,OAAe,SAAiC;AACrD,WAAO,KAAK,SAAS,SAAS,KAAK;AAAA,EACrC;AAAA,EAEA,UAAU,SAAkB,OAA8B;AACxD,WAAO,KAAK,YAAY,WAAW,SAAS,KAAK;AAAA,EACnD;AAAA,EAEA,YAAY,OAAe,SAAkB,OAA8B;AACzE,eAAW,MAAM;AACf,WAAK,KAAK,SAAS,SAAS,KAAK,EAAE,MAAM,CAAC,UAAU;AAClD,qBAAa,UAAU,EAAE;AAAA,UACvB,EAAE,KAAK,OAAO,OAAO,QAAQ,IAAI,SAAS,QAAQ,MAAM,MAAM;AAAA,UAC9D;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,GAAG,KAAK;AAER,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,SAAS,MAAgC;AACvC,WAAO,KAAK,WAAW,WAAW,IAAI;AAAA,EACxC;AAAA,EAEA,MAAM,WAAW,OAAe,MAAgC;AAC9D,QAAI,KAAK,KAAK,CAAC,MAAM,EAAE,KAAK,GAAG;AAC7B,YAAM,IAAI,MAAM,+DAA+D;AAAA,IACjF;AAEA,eAAW,OAAO,MAAM;AACtB,YAAM,KAAK,OAAO,OAAO,GAAG;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,OAAwB;AACtB,WAAO,KAAK,OAAO,SAAS;AAAA,EAC9B;AAAA,EAEA,OAAO,QAAiC;AACtC,WAAO,QAAQ,QAAQ,CAAC;AAAA,EAC1B;AAAA,EAEA,MAAmC;AACjC,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAAA,EAEA,QAAQ,QAA6C;AACnD,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AAAA,EAEA,YAAY,QAAgB,QAAgB,mBAAiD;AAC3F,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,QACE,QACA,QACA,QACA,eACe;AACf,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,SAAS,QAAgB,QAAgB,UAAgC;AACvE,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,mBACE,QACA,mBACA,kBACiB;AAEjB,WAAO,QAAQ,QAAQ,CAAC;AAAA,EAC1B;AAAA,EAEA,UAAU,QAAgB,SAAoC;AAE5D,WAAO,QAAQ,QAAQ,CAAC;AAAA,EAC1B;AAAA,EAEA,OAAO,QAAgB,QAA+B;AACpD,WAAO,QAAQ,QAAQ,IAAI;AAAA,EAC7B;AAAA,EAEA,UAAyB;AACvB,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,eAAe,SAA0C;AAGvD,WAAO,QAAQ,QAAQ,iBAAiB,KAAK,IAAI,CAAC,EAAE;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,QAAyC;AACtD,WAAO,KAAK,eAAe,MAAM;AAAA,EACnC;AAAA,EAEA,YAAY,KAA2C;AACrD,WAAO,QAAQ,QAAQ,IAAI;AAAA,EAC7B;AAAA,EAEA,cAAc,UAAyD;AACrE,WAAO,QAAQ,QAAQ,CAAC,CAAC;AAAA,EAC3B;AAAA,EAEA,eACE,KACA,UACe;AACf,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,eAAe,KAA4B;AACzC,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,mBAAiD;AAE/C,WAAO,QAAQ,QAAQ,IAAI;AAAA,EAC7B;AAAA,EAEA,MAAM,SAAS,SAAkB,QAAgB,WAA0B;AACzE,UAAM,WAAW,QAAQ,IAAI,QAAQ,IAAI;AAEzC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,aAAa,QAAQ,IAAI,aAAa;AAAA,IACxD;AAEA,UAAM,UAAU,SAAS,WAAW,CAAC;AACrC,UAAM,iBAAiB,aAAa,kBAAkB;AACtD,UAAM,UAAU,oBAAoB,KAAK;AAAA,MACvC,SAAS,QAAQ;AAAA,MACjB;AAAA,MACA,aAAa,eAAe,mBAAmB,OAAO,OAAO;AAAA,MAC7D,gBAAgB,eAAe,iBAAiB;AAAA,IAClD,CAAC;AACD,UAAM,aAAa,aAAa,cAAc;AAC9C,UAAM,mBAAmB,aAAa,oBAAoB;AAC1D,QAAI,WAAW,QAAQ;AAEvB,WAAO,MAAM;AACX,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,cAA2B,EAAE,GAAG,SAAS,UAAU,YAAY,IAAI;AAEzE,YAAM,UAAsB;AAAA,QAC1B,OAAO,QAAQ;AAAA,QACf,MAAM,QAAQ;AAAA,QACd,SAAS,WAAW;AAAA,QACpB;AAAA,QACA,UAAU,QAAQ,YAAY;AAAA,QAC9B,YAAY,IAAI,KAAK,GAAG;AAAA,QACxB,cAAc,QAAQ,gBAAgB;AAAA,MACxC;AAEA,YAAM,cAAc,aAAa,MAAM,WAAW,QAAQ,IAAI,IAAI,SAAS;AAE3E,YAAM,YAAY,YAAY,IAAI;AAClC,YAAM,iBAAoC,EAAE,KAAK,aAAa,MAAM;AAEpE,YAAM,MAAM,MAAM;AAChB,eAAO,eAAe,aAAa,YAAY;AAC7C,cAAI;AACF,kBAAM,QAAQ,QAAQ,aAAa,QAAQ,SAAS,OAAO;AAC3D,2BAAe,SAAS;AAAA,UAC1B,SAAS,OAAO;AACd,kBAAM,UAAU,QAAQ,eAAe,OAAgB,QAAQ;AAC/D,2BAAe,QAAQ;AAEvB,gBAAI,QAAQ,SAAS,UAAU;AAC7B,6BAAe,SAAS;AACxB,oBAAM,YAAY,SAAS,QAAQ,SAAS;AAAA,YAC9C,WAAW,QAAQ,SAAS,SAAS;AACnC,6BAAe,SAAS;AACxB,6BAAe,cAAc,QAAQ;AAAA,YACvC;AAAA,UACF;AAEA,yBAAe,WAAW,QAAQ,YAAY,IAAI,IAAI,WAAW,QAAQ,CAAC,CAAC;AAAA,QAC7E,GAAG,cAAc;AAAA,MACnB;AAEA,YAAM,iBAAiB,KAAK,aAAa,KAAK;AAE9C,UAAI,eAAe,WAAW,WAAY;AAE1C;AAEA,UAAI,eAAe,aAAa;AAC9B,cAAM,QAAQ,eAAe,YAAY,QAAQ,IAAI,KAAK,IAAI;AAC9D,YAAI,QAAQ,GAAG;AACb,gBAAM,MAAM,KAAK;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -1,8 +1,8 @@
1
- import '../job-DImdhRFO.js';
1
+ import '../job-C4oyCVxR.js';
2
2
  import { JobDispatchMessage, JobExecuteMessage } from './types/tracing_channels.js';
3
3
  import { Span } from '@opentelemetry/api';
4
4
  import { TracingChannelSubscribers } from 'node:diagnostics_channel';
5
- import { InstrumentationConfig, InstrumentationBase } from '@opentelemetry/instrumentation';
5
+ import { InstrumentationBase, InstrumentationConfig } from '@opentelemetry/instrumentation';
6
6
 
7
7
  interface QueueInstrumentationConfig extends InstrumentationConfig {
8
8
  /**
package/build/src/otel.js CHANGED
@@ -177,6 +177,9 @@ var QueueInstrumentation = class extends InstrumentationBase {
177
177
  },
178
178
  baseContext
179
179
  );
180
+ if (job.createdAt) {
181
+ span.setAttribute("messaging.job.queue_time_ms", Date.now() - job.createdAt);
182
+ }
180
183
  this.executeSpans.set(job.id, span);
181
184
  const executionContext = trace.setSpan(baseContext, span);
182
185
  return context.with(executionContext, fn);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/otel.ts"],"sourcesContent":["import {\n context,\n propagation,\n trace,\n SpanKind,\n SpanStatusCode,\n ROOT_CONTEXT,\n type Span,\n type Link,\n} from '@opentelemetry/api'\nimport type { TracingChannelSubscribers } from 'node:diagnostics_channel'\nimport { suppressTracing } from '@opentelemetry/core'\nimport { InstrumentationBase } from '@opentelemetry/instrumentation'\nimport type { InstrumentationConfig } from '@opentelemetry/instrumentation'\nimport { dispatchChannel, executeChannel } from './tracing_channels.js'\nimport type { AcquiredJob } from './contracts/adapter.js'\nimport type { JobDispatchMessage, JobExecuteMessage } from './types/tracing_channels.js'\n\nexport interface QueueInstrumentationConfig extends InstrumentationConfig {\n /**\n * How execution spans relate to the dispatch span.\n *\n * - `'link'` (default): Independent trace, linked to dispatch span\n * - `'parent'`: Child of the dispatch span (same trace)\n */\n executionSpanLinkMode?: 'link' | 'parent'\n\n /**\n * The messaging system identifier.\n *\n * @default 'boringqueue'\n */\n messagingSystem?: string\n}\n\n/**\n * OpenTelemetry instrumentation for @boringnode/queue.\n *\n * Creates PRODUCER spans for job dispatch and CONSUMER spans for\n * job execution, following OTel messaging semantic conventions.\n *\n * Uses `diagnostics_channel` for span lifecycle management and\n * patches `QueueManager.init()` to inject wrappers automatically.\n */\nexport class QueueInstrumentation extends InstrumentationBase<QueueInstrumentationConfig> {\n protected subscribed = false\n protected executeSpans = new Map<string, Span>()\n protected dispatchSpans = new WeakMap<JobDispatchMessage, Span>()\n protected executeHandlers?: TracingChannelSubscribers<JobExecuteMessage>\n protected dispatchHandlers?: TracingChannelSubscribers<JobDispatchMessage>\n\n #originalInit?: (...args: any[]) => any\n #patchedManager?: { init: (...args: any[]) => any }\n\n constructor(config: QueueInstrumentationConfig = {}) {\n super('@boringnode/queue', '0.1.0', config)\n }\n\n get #messagingSystem(): string {\n return this.getConfig().messagingSystem ?? 'boringqueue'\n }\n\n get #executionSpanLinkMode(): 'link' | 'parent' {\n return this.getConfig().executionSpanLinkMode ?? 'link'\n }\n\n /**\n * Required by InstrumentationBase. Returns undefined since we use\n * diagnostics_channel instead of module patching.\n */\n protected init() {\n return undefined\n }\n\n /**\n * Subscribes to diagnostics_channels for span lifecycle.\n */\n enable() {\n super.enable()\n if (this.subscribed !== undefined) this.#subscribe()\n }\n\n /**\n * Unsubscribes from diagnostics_channels and restores patched methods.\n */\n disable() {\n if (this.subscribed !== undefined) {\n this.#unsubscribe()\n this.#unpatchInit()\n }\n\n super.disable()\n }\n\n /**\n * Patches `QueueManager.init()` to auto-inject OTel wrappers\n * and subscribes to diagnostics_channels.\n */\n manuallyRegister(queueModule: { QueueManager: { init: (...args: any[]) => any } }) {\n this.#patchInit(queueModule.QueueManager)\n this.#subscribe()\n }\n\n #patchInit(manager: { init: (...args: any[]) => any }) {\n if (this.#originalInit) return\n\n this.#patchedManager = manager\n this.#originalInit = manager.init.bind(manager)\n const instrumentation = this\n\n manager.init = async (config: any) => {\n return this.#originalInit!({\n ...config,\n internalOperationWrapper: <T>(fn: () => Promise<T>) => {\n return context.with(suppressTracing(context.active()), fn)\n },\n executionWrapper: <T>(fn: () => Promise<T>, job: AcquiredJob, queue: string) => {\n return instrumentation.#wrapExecution(fn, job, queue)\n },\n })\n }\n }\n\n #unpatchInit() {\n if (!this.#originalInit || !this.#patchedManager) return\n\n this.#patchedManager.init = this.#originalInit\n this.#originalInit = undefined\n this.#patchedManager = undefined\n }\n\n #subscribe() {\n if (this.subscribed) return\n if (!this.isEnabled()) return\n\n this.subscribed = true\n\n this.executeHandlers = {\n start: () => {},\n end: () => {},\n asyncStart: () => {},\n asyncEnd: (msg) => this.#handleExecuteAsyncEnd(msg as unknown as JobExecuteMessage),\n error: () => {},\n }\n\n this.dispatchHandlers = {\n start: (msg) => this.#handleDispatchStart(msg as unknown as JobDispatchMessage),\n end: () => {},\n asyncStart: () => {},\n asyncEnd: (msg) => this.#handleDispatchAsyncEnd(msg as unknown as JobDispatchMessage),\n error: () => {},\n }\n\n executeChannel.subscribe(this.executeHandlers as any)\n dispatchChannel.subscribe(this.dispatchHandlers as any)\n }\n\n #unsubscribe() {\n if (!this.subscribed) return\n\n if (this.executeHandlers) executeChannel.unsubscribe(this.executeHandlers as any)\n if (this.dispatchHandlers) dispatchChannel.unsubscribe(this.dispatchHandlers as any)\n\n this.subscribed = false\n this.executeHandlers = undefined\n this.dispatchHandlers = undefined\n this.executeSpans.clear()\n this.dispatchSpans = new WeakMap()\n }\n\n /**\n * Called on dispatchChannel `start` — injects trace context into jobData\n * and creates/enriches a PRODUCER span.\n */\n #handleDispatchStart(message: JobDispatchMessage) {\n const attributes = this.#buildDispatchAttributes(message)\n const span = this.tracer.startSpan(`publish ${message.queue}`, {\n kind: SpanKind.PRODUCER,\n attributes,\n })\n\n const dispatchContext = trace.setSpan(context.active(), span)\n for (const job of message.jobs) {\n if (!job.traceContext) job.traceContext = {}\n propagation.inject(dispatchContext, job.traceContext)\n }\n\n this.dispatchSpans.set(message, span)\n }\n\n #handleDispatchAsyncEnd(message: JobDispatchMessage) {\n const span = this.dispatchSpans.get(message)\n if (!span) return\n\n if (message.error) {\n span.recordException(message.error)\n span.setStatus({ code: SpanStatusCode.ERROR, message: message.error.message })\n }\n\n span.end()\n this.dispatchSpans.delete(message)\n }\n\n /**\n * Called by `executionWrapper` config — creates CONSUMER span and wraps\n * execution in OTel context for proper child span parenting.\n */\n #wrapExecution<T>(fn: () => Promise<T>, job: AcquiredJob, queue: string): Promise<T> {\n const extractedContext = this.#extractParentContext(job.traceContext)\n const parentSpanContext = trace.getSpanContext(extractedContext)\n\n let baseContext: typeof extractedContext\n let links: Link[]\n\n if (this.#executionSpanLinkMode === 'parent' && parentSpanContext) {\n baseContext = extractedContext\n links = []\n } else {\n links = parentSpanContext ? [{ context: parentSpanContext }] : []\n baseContext = ROOT_CONTEXT\n }\n\n const span = this.tracer.startSpan(\n `process ${queue}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: this.#buildExecuteAttributes(job, queue),\n links,\n },\n baseContext\n )\n\n this.executeSpans.set(job.id, span)\n const executionContext = trace.setSpan(baseContext, span)\n\n return context.with(executionContext, fn)\n }\n\n #handleExecuteAsyncEnd(message: JobExecuteMessage) {\n const span = this.executeSpans.get(message.job.id)\n if (!span) return\n\n if (message.status) span.setAttribute('messaging.job.status', message.status)\n if (message.error) span.recordException(message.error)\n\n if (message.status === 'retrying' && message.nextRetryAt) {\n span.addEvent('messaging.retry', {\n 'messaging.message.retry.count': message.job.attempts + 1,\n 'messaging.job.retry_at': message.nextRetryAt.toISOString(),\n })\n }\n\n if (message.status === 'failed') {\n span.setStatus({ code: SpanStatusCode.ERROR, message: message.error?.message })\n } else {\n span.setStatus({ code: SpanStatusCode.OK })\n }\n\n span.end()\n this.executeSpans.delete(message.job.id)\n }\n\n #extractParentContext(traceContext?: Record<string, string>) {\n if (!traceContext || Object.keys(traceContext).length === 0) return ROOT_CONTEXT\n return propagation.extract(ROOT_CONTEXT, traceContext)\n }\n\n #buildDispatchAttributes(message: JobDispatchMessage) {\n const firstJob = message.jobs[0]\n const attributes: Record<string, string | number | boolean> = {\n 'messaging.system': this.#messagingSystem,\n 'messaging.operation.name': 'publish',\n 'messaging.operation.type': 'send',\n 'messaging.destination.name': message.queue,\n 'messaging.job.name': firstJob.name,\n }\n\n if (message.jobs.length === 1) attributes['messaging.message.id'] = firstJob.id\n if (message.jobs.length > 1) attributes['messaging.batch.message_count'] = message.jobs.length\n if (firstJob.groupId) attributes['messaging.job.group_id'] = firstJob.groupId\n if (firstJob.priority !== undefined) attributes['messaging.job.priority'] = firstJob.priority\n if (message.delay !== undefined) attributes['messaging.job.delay_ms'] = message.delay\n\n return attributes\n }\n\n #buildExecuteAttributes(job: AcquiredJob, queue: string) {\n const attributes: Record<string, string | number | boolean> = {\n 'entry_point.type': 'job',\n 'messaging.system': this.#messagingSystem,\n 'messaging.operation.name': 'process',\n 'messaging.operation.type': 'process',\n 'messaging.destination.name': queue,\n 'messaging.message.id': job.id,\n 'messaging.message.retry.count': job.attempts,\n 'messaging.job.name': job.name,\n }\n\n if (job.groupId) attributes['messaging.job.group_id'] = job.groupId\n if (job.priority !== undefined) attributes['messaging.job.priority'] = job.priority\n\n return attributes\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/otel.ts"],"sourcesContent":["import {\n context,\n propagation,\n trace,\n SpanKind,\n SpanStatusCode,\n ROOT_CONTEXT,\n type Span,\n type Link,\n} from '@opentelemetry/api'\nimport type { TracingChannelSubscribers } from 'node:diagnostics_channel'\nimport { suppressTracing } from '@opentelemetry/core'\nimport { InstrumentationBase } from '@opentelemetry/instrumentation'\nimport type { InstrumentationConfig } from '@opentelemetry/instrumentation'\nimport { dispatchChannel, executeChannel } from './tracing_channels.js'\nimport type { AcquiredJob } from './contracts/adapter.js'\nimport type { JobDispatchMessage, JobExecuteMessage } from './types/tracing_channels.js'\n\nexport interface QueueInstrumentationConfig extends InstrumentationConfig {\n /**\n * How execution spans relate to the dispatch span.\n *\n * - `'link'` (default): Independent trace, linked to dispatch span\n * - `'parent'`: Child of the dispatch span (same trace)\n */\n executionSpanLinkMode?: 'link' | 'parent'\n\n /**\n * The messaging system identifier.\n *\n * @default 'boringqueue'\n */\n messagingSystem?: string\n}\n\n/**\n * OpenTelemetry instrumentation for @boringnode/queue.\n *\n * Creates PRODUCER spans for job dispatch and CONSUMER spans for\n * job execution, following OTel messaging semantic conventions.\n *\n * Uses `diagnostics_channel` for span lifecycle management and\n * patches `QueueManager.init()` to inject wrappers automatically.\n */\nexport class QueueInstrumentation extends InstrumentationBase<QueueInstrumentationConfig> {\n protected subscribed = false\n protected executeSpans = new Map<string, Span>()\n protected dispatchSpans = new WeakMap<JobDispatchMessage, Span>()\n protected executeHandlers?: TracingChannelSubscribers<JobExecuteMessage>\n protected dispatchHandlers?: TracingChannelSubscribers<JobDispatchMessage>\n\n #originalInit?: (...args: any[]) => any\n #patchedManager?: { init: (...args: any[]) => any }\n\n constructor(config: QueueInstrumentationConfig = {}) {\n super('@boringnode/queue', '0.1.0', config)\n }\n\n get #messagingSystem(): string {\n return this.getConfig().messagingSystem ?? 'boringqueue'\n }\n\n get #executionSpanLinkMode(): 'link' | 'parent' {\n return this.getConfig().executionSpanLinkMode ?? 'link'\n }\n\n /**\n * Required by InstrumentationBase. Returns undefined since we use\n * diagnostics_channel instead of module patching.\n */\n protected init() {\n return undefined\n }\n\n /**\n * Subscribes to diagnostics_channels for span lifecycle.\n */\n enable() {\n super.enable()\n if (this.subscribed !== undefined) this.#subscribe()\n }\n\n /**\n * Unsubscribes from diagnostics_channels and restores patched methods.\n */\n disable() {\n if (this.subscribed !== undefined) {\n this.#unsubscribe()\n this.#unpatchInit()\n }\n\n super.disable()\n }\n\n /**\n * Patches `QueueManager.init()` to auto-inject OTel wrappers\n * and subscribes to diagnostics_channels.\n */\n manuallyRegister(queueModule: { QueueManager: { init: (...args: any[]) => any } }) {\n this.#patchInit(queueModule.QueueManager)\n this.#subscribe()\n }\n\n #patchInit(manager: { init: (...args: any[]) => any }) {\n if (this.#originalInit) return\n\n this.#patchedManager = manager\n this.#originalInit = manager.init.bind(manager)\n const instrumentation = this\n\n manager.init = async (config: any) => {\n return this.#originalInit!({\n ...config,\n internalOperationWrapper: <T>(fn: () => Promise<T>) => {\n return context.with(suppressTracing(context.active()), fn)\n },\n executionWrapper: <T>(fn: () => Promise<T>, job: AcquiredJob, queue: string) => {\n return instrumentation.#wrapExecution(fn, job, queue)\n },\n })\n }\n }\n\n #unpatchInit() {\n if (!this.#originalInit || !this.#patchedManager) return\n\n this.#patchedManager.init = this.#originalInit\n this.#originalInit = undefined\n this.#patchedManager = undefined\n }\n\n #subscribe() {\n if (this.subscribed) return\n if (!this.isEnabled()) return\n\n this.subscribed = true\n\n this.executeHandlers = {\n start: () => {},\n end: () => {},\n asyncStart: () => {},\n asyncEnd: (msg) => this.#handleExecuteAsyncEnd(msg as unknown as JobExecuteMessage),\n error: () => {},\n }\n\n this.dispatchHandlers = {\n start: (msg) => this.#handleDispatchStart(msg as unknown as JobDispatchMessage),\n end: () => {},\n asyncStart: () => {},\n asyncEnd: (msg) => this.#handleDispatchAsyncEnd(msg as unknown as JobDispatchMessage),\n error: () => {},\n }\n\n executeChannel.subscribe(this.executeHandlers as any)\n dispatchChannel.subscribe(this.dispatchHandlers as any)\n }\n\n #unsubscribe() {\n if (!this.subscribed) return\n\n if (this.executeHandlers) executeChannel.unsubscribe(this.executeHandlers as any)\n if (this.dispatchHandlers) dispatchChannel.unsubscribe(this.dispatchHandlers as any)\n\n this.subscribed = false\n this.executeHandlers = undefined\n this.dispatchHandlers = undefined\n this.executeSpans.clear()\n this.dispatchSpans = new WeakMap()\n }\n\n /**\n * Called on dispatchChannel `start` — injects trace context into jobData\n * and creates/enriches a PRODUCER span.\n */\n #handleDispatchStart(message: JobDispatchMessage) {\n const attributes = this.#buildDispatchAttributes(message)\n const span = this.tracer.startSpan(`publish ${message.queue}`, {\n kind: SpanKind.PRODUCER,\n attributes,\n })\n\n const dispatchContext = trace.setSpan(context.active(), span)\n for (const job of message.jobs) {\n if (!job.traceContext) job.traceContext = {}\n propagation.inject(dispatchContext, job.traceContext)\n }\n\n this.dispatchSpans.set(message, span)\n }\n\n #handleDispatchAsyncEnd(message: JobDispatchMessage) {\n const span = this.dispatchSpans.get(message)\n if (!span) return\n\n if (message.error) {\n span.recordException(message.error)\n span.setStatus({ code: SpanStatusCode.ERROR, message: message.error.message })\n }\n\n span.end()\n this.dispatchSpans.delete(message)\n }\n\n /**\n * Called by `executionWrapper` config — creates CONSUMER span and wraps\n * execution in OTel context for proper child span parenting.\n */\n #wrapExecution<T>(fn: () => Promise<T>, job: AcquiredJob, queue: string): Promise<T> {\n const extractedContext = this.#extractParentContext(job.traceContext)\n const parentSpanContext = trace.getSpanContext(extractedContext)\n\n let baseContext: typeof extractedContext\n let links: Link[]\n\n if (this.#executionSpanLinkMode === 'parent' && parentSpanContext) {\n baseContext = extractedContext\n links = []\n } else {\n links = parentSpanContext ? [{ context: parentSpanContext }] : []\n baseContext = ROOT_CONTEXT\n }\n\n const span = this.tracer.startSpan(\n `process ${queue}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: this.#buildExecuteAttributes(job, queue),\n links,\n },\n baseContext\n )\n\n if (job.createdAt) {\n span.setAttribute('messaging.job.queue_time_ms', Date.now() - job.createdAt)\n }\n\n this.executeSpans.set(job.id, span)\n const executionContext = trace.setSpan(baseContext, span)\n\n return context.with(executionContext, fn)\n }\n\n #handleExecuteAsyncEnd(message: JobExecuteMessage) {\n const span = this.executeSpans.get(message.job.id)\n if (!span) return\n\n if (message.status) span.setAttribute('messaging.job.status', message.status)\n if (message.error) span.recordException(message.error)\n\n if (message.status === 'retrying' && message.nextRetryAt) {\n span.addEvent('messaging.retry', {\n 'messaging.message.retry.count': message.job.attempts + 1,\n 'messaging.job.retry_at': message.nextRetryAt.toISOString(),\n })\n }\n\n if (message.status === 'failed') {\n span.setStatus({ code: SpanStatusCode.ERROR, message: message.error?.message })\n } else {\n span.setStatus({ code: SpanStatusCode.OK })\n }\n\n span.end()\n this.executeSpans.delete(message.job.id)\n }\n\n #extractParentContext(traceContext?: Record<string, string>) {\n if (!traceContext || Object.keys(traceContext).length === 0) return ROOT_CONTEXT\n return propagation.extract(ROOT_CONTEXT, traceContext)\n }\n\n #buildDispatchAttributes(message: JobDispatchMessage) {\n const firstJob = message.jobs[0]\n const attributes: Record<string, string | number | boolean> = {\n 'messaging.system': this.#messagingSystem,\n 'messaging.operation.name': 'publish',\n 'messaging.operation.type': 'send',\n 'messaging.destination.name': message.queue,\n 'messaging.job.name': firstJob.name,\n }\n\n if (message.jobs.length === 1) attributes['messaging.message.id'] = firstJob.id\n if (message.jobs.length > 1) attributes['messaging.batch.message_count'] = message.jobs.length\n if (firstJob.groupId) attributes['messaging.job.group_id'] = firstJob.groupId\n if (firstJob.priority !== undefined) attributes['messaging.job.priority'] = firstJob.priority\n if (message.delay !== undefined) attributes['messaging.job.delay_ms'] = message.delay\n\n return attributes\n }\n\n #buildExecuteAttributes(job: AcquiredJob, queue: string) {\n const attributes: Record<string, string | number | boolean> = {\n 'entry_point.type': 'job',\n 'messaging.system': this.#messagingSystem,\n 'messaging.operation.name': 'process',\n 'messaging.operation.type': 'process',\n 'messaging.destination.name': queue,\n 'messaging.message.id': job.id,\n 'messaging.message.retry.count': job.attempts,\n 'messaging.job.name': job.name,\n }\n\n if (job.groupId) attributes['messaging.job.group_id'] = job.groupId\n if (job.priority !== undefined) attributes['messaging.job.priority'] = job.priority\n\n return attributes\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- export { b as AcquiredJob, A as Adapter, r as AdapterFactory, B as BackoffConfig, s as BackoffStrategy, t as DispatchManyResult, u as DispatchResult, D as Duration, a as JobClass, v as JobContext, J as JobData, i as JobFactory, g as JobOptions, d as JobRecord, c as JobRetention, w as JobStatus, L as Logger, h as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, x as ScheduleResult, k as ScheduleStatus, y as WorkerConfig, W as WorkerCycle } from '../../job-DImdhRFO.js';
1
+ export { b as AcquiredJob, A as Adapter, s as AdapterFactory, B as BackoffConfig, t as BackoffStrategy, u as DispatchManyResult, v as DispatchResult, i as Duration, a as JobClass, w as JobContext, J as JobData, j as JobFactory, g as JobOptions, d as JobRecord, c as JobRetention, x as JobStatus, L as Logger, h as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, y as ScheduleResult, l as ScheduleStatus, z as WorkerConfig, W as WorkerCycle } from '../../job-C4oyCVxR.js';
2
2
  export { JobDispatchMessage, JobExecuteMessage } from './tracing_channels.js';
3
3
  export { QueueInstrumentationConfig } from '../otel.js';
4
4
  import '@opentelemetry/api';
@@ -1 +1 @@
1
- export { r as AdapterFactory, B as BackoffConfig, s as BackoffStrategy, t as DispatchManyResult, u as DispatchResult, D as Duration, a as JobClass, v as JobContext, J as JobData, i as JobFactory, g as JobOptions, d as JobRecord, c as JobRetention, w as JobStatus, L as Logger, h as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, x as ScheduleResult, k as ScheduleStatus, y as WorkerConfig, W as WorkerCycle } from '../../job-DImdhRFO.js';
1
+ export { s as AdapterFactory, B as BackoffConfig, t as BackoffStrategy, D as DedupOutcome, u as DispatchManyResult, v as DispatchResult, i as Duration, a as JobClass, w as JobContext, J as JobData, j as JobFactory, g as JobOptions, d as JobRecord, c as JobRetention, x as JobStatus, L as Logger, h as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, y as ScheduleResult, l as ScheduleStatus, z as WorkerConfig, W as WorkerCycle } from '../../job-C4oyCVxR.js';