@boringnode/queue 0.4.0 → 0.4.1

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.
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_PRIORITY,
3
3
  calculateScore,
4
4
  resolveRetention
5
- } from "../../chunk-3BIR4IQD.js";
5
+ } from "../../chunk-ZZFSQY36.js";
6
6
 
7
7
  // src/drivers/redis_adapter.ts
8
8
  import { randomUUID } from "crypto";
@@ -577,28 +577,38 @@ var RedisAdapter = class {
577
577
  );
578
578
  return recovered;
579
579
  }
580
- async createSchedule(config) {
580
+ async upsertSchedule(config) {
581
581
  const id = config.id ?? randomUUID();
582
582
  const now = Date.now();
583
+ const scheduleKey = `${schedulesKey}::${id}`;
584
+ const [existingRunCount, existingCreatedAt] = await this.#connection.hmget(
585
+ scheduleKey,
586
+ "run_count",
587
+ "created_at"
588
+ );
583
589
  const scheduleData = {
584
590
  id,
585
591
  name: config.name,
586
592
  payload: JSON.stringify(config.payload),
587
593
  timezone: config.timezone,
588
594
  status: "active",
589
- run_count: "0",
590
- created_at: now.toString()
595
+ run_count: existingRunCount ?? "0",
596
+ created_at: existingCreatedAt ?? now.toString()
591
597
  };
592
- if (config.cronExpression) scheduleData.cron_expression = config.cronExpression;
593
- if (config.everyMs) scheduleData.every_ms = config.everyMs.toString();
594
- if (config.from) scheduleData.from_date = config.from.getTime().toString();
595
- if (config.to) scheduleData.to_date = config.to.getTime().toString();
596
- if (config.limit) scheduleData.run_limit = config.limit.toString();
597
- const scheduleKey = `${schedulesKey}::${id}`;
598
- await this.#connection.hset(scheduleKey, scheduleData);
599
- await this.#connection.sadd(schedulesIndexKey, id);
598
+ if (config.cronExpression !== void 0) scheduleData.cron_expression = config.cronExpression;
599
+ if (config.everyMs !== void 0) scheduleData.every_ms = config.everyMs.toString();
600
+ if (config.from !== void 0) scheduleData.from_date = config.from.getTime().toString();
601
+ if (config.to !== void 0) scheduleData.to_date = config.to.getTime().toString();
602
+ if (config.limit !== void 0) scheduleData.run_limit = config.limit.toString();
603
+ await this.#connection.multi().hdel(scheduleKey, "cron_expression", "every_ms", "from_date", "to_date", "run_limit").hset(scheduleKey, scheduleData).sadd(schedulesIndexKey, id).exec();
600
604
  return id;
601
605
  }
606
+ /**
607
+ * @deprecated Use `upsertSchedule` instead.
608
+ */
609
+ createSchedule(config) {
610
+ return this.upsertSchedule(config);
611
+ }
602
612
  async getSchedule(id) {
603
613
  const scheduleKey = `${schedulesKey}::${id}`;
604
614
  const data = await this.#connection.hgetall(scheduleKey);
@@ -609,15 +619,27 @@ var RedisAdapter = class {
609
619
  }
610
620
  async listSchedules(options) {
611
621
  const ids = await this.#connection.smembers(schedulesIndexKey);
612
- const schedules = [];
622
+ if (ids.length === 0) {
623
+ return [];
624
+ }
625
+ const pipeline = this.#connection.pipeline();
613
626
  for (const id of ids) {
614
- const schedule = await this.getSchedule(id);
615
- if (schedule) {
616
- if (options?.status && schedule.status !== options.status) {
617
- continue;
618
- }
619
- schedules.push(schedule);
627
+ pipeline.hgetall(`${schedulesKey}::${id}`);
628
+ }
629
+ const results = await pipeline.exec();
630
+ if (!results) {
631
+ return [];
632
+ }
633
+ const schedules = [];
634
+ for (const [, data] of results) {
635
+ if (!data || Object.keys(data).length === 0) {
636
+ continue;
637
+ }
638
+ const schedule = this.#hashToScheduleData(data);
639
+ if (options?.status && schedule.status !== options.status) {
640
+ continue;
620
641
  }
642
+ schedules.push(schedule);
621
643
  }
622
644
  return schedules;
623
645
  }
@@ -638,8 +660,7 @@ var RedisAdapter = class {
638
660
  }
639
661
  async deleteSchedule(id) {
640
662
  const scheduleKey = `${schedulesKey}::${id}`;
641
- await this.#connection.del(scheduleKey);
642
- await this.#connection.srem(schedulesIndexKey, id);
663
+ await this.#connection.multi().del(scheduleKey).srem(schedulesIndexKey, id).exec();
643
664
  }
644
665
  async claimDueSchedule() {
645
666
  const now = Date.now();
@@ -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 createSchedule(config: ScheduleConfig): Promise<string> {\n const id = config.id ?? randomUUID()\n const now = Date.now()\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: '0',\n created_at: now.toString(),\n }\n\n if (config.cronExpression) scheduleData.cron_expression = config.cronExpression\n if (config.everyMs) scheduleData.every_ms = config.everyMs.toString()\n if (config.from) scheduleData.from_date = config.from.getTime().toString()\n if (config.to) scheduleData.to_date = config.to.getTime().toString()\n if (config.limit) scheduleData.run_limit = config.limit.toString()\n\n // Store schedule as hash\n const scheduleKey = `${schedulesKey}::${id}`\n await this.#connection.hset(scheduleKey, scheduleData)\n\n // Add to index set for listing\n await this.#connection.sadd(schedulesIndexKey, id)\n\n return id\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 const schedules: ScheduleData[] = []\n\n for (const id of ids) {\n const schedule = await this.getSchedule(id)\n if (schedule) {\n // Filter by status if provided\n if (options?.status && schedule.status !== options.status) {\n continue\n }\n schedules.push(schedule)\n }\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.del(scheduleKey)\n await this.#connection.srem(schedulesIndexKey, id)\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 } 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,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 '../../index-CoubP-c4.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 '../../index-BAMFA6FI.js';
2
2
 
3
3
  /**
4
4
  * Create a sync adapter factory.
@@ -27,7 +27,11 @@ declare class SyncAdapter implements Adapter {
27
27
  recoverStalledJobs(_queue: string, _stalledThreshold: number, _maxStalledCount: number): Promise<number>;
28
28
  getJob(_jobId: string, _queue: string): Promise<null>;
29
29
  destroy(): Promise<void>;
30
- createSchedule(_config: ScheduleConfig): Promise<string>;
30
+ upsertSchedule(_config: ScheduleConfig): Promise<string>;
31
+ /**
32
+ * @deprecated Use `upsertSchedule` instead.
33
+ */
34
+ createSchedule(config: ScheduleConfig): Promise<string>;
31
35
  getSchedule(_id: string): Promise<ScheduleData | null>;
32
36
  listSchedules(_options?: ScheduleListOptions): Promise<ScheduleData[]>;
33
37
  updateSchedule(_id: string, _updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>): Promise<void>;
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  Locator,
3
3
  QueueManager
4
- } from "../../chunk-H6WOFLPJ.js";
4
+ } from "../../chunk-6EBS7CW4.js";
5
5
  import {
6
6
  DEFAULT_PRIORITY
7
- } from "../../chunk-3BIR4IQD.js";
7
+ } from "../../chunk-ZZFSQY36.js";
8
8
 
9
9
  // src/drivers/sync_adapter.ts
10
10
  function sync() {
@@ -66,9 +66,15 @@ var SyncAdapter = class {
66
66
  destroy() {
67
67
  return Promise.resolve();
68
68
  }
69
- createSchedule(_config) {
69
+ upsertSchedule(_config) {
70
70
  return Promise.resolve(`sync-schedule-${Date.now()}`);
71
71
  }
72
+ /**
73
+ * @deprecated Use `upsertSchedule` instead.
74
+ */
75
+ createSchedule(config) {
76
+ return this.upsertSchedule(config);
77
+ }
72
78
  getSchedule(_id) {
73
79
  return Promise.resolve(null);
74
80
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/drivers/sync_adapter.ts"],"sourcesContent":["import { Locator } from '../locator.js'\nimport { QueueManager } from '../queue_manager.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 { 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.name, jobData.payload, 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.name, jobData.payload, queue)\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 createSchedule(_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 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(jobName: string, payload: any, queue: string = 'default'): Promise<any> {\n const JobClass = Locator.get(jobName)\n\n if (!JobClass) {\n throw new Error(`Job class ${jobName} not found.`)\n }\n\n const context: JobContext = {\n jobId: `sync-${Date.now()}`,\n name: jobName,\n attempt: 1,\n queue,\n priority: DEFAULT_PRIORITY,\n acquiredAt: new Date(),\n stalledCount: 0,\n }\n\n const jobFactory = QueueManager.getJobFactory()\n const jobInstance = jobFactory ? await jobFactory(JobClass) : new JobClass()\n\n jobInstance.$hydrate(payload, context)\n await jobInstance.execute()\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/drivers/sync_adapter.ts"],"sourcesContent":["import { Locator } from '../locator.js'\nimport { QueueManager } from '../queue_manager.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 { 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.name, jobData.payload, 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.name, jobData.payload, queue)\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(jobName: string, payload: unknown, queue: string = 'default'): Promise<void> {\n const JobClass = Locator.get(jobName)\n\n if (!JobClass) {\n throw new Error(`Job class ${jobName} not found.`)\n }\n\n const context: JobContext = {\n jobId: `sync-${Date.now()}`,\n name: jobName,\n attempt: 1,\n queue,\n priority: DEFAULT_PRIORITY,\n acquiredAt: new Date(),\n stalledCount: 0,\n }\n\n const jobFactory = QueueManager.getJobFactory()\n const jobInstance = jobFactory ? await jobFactory(JobClass) : new JobClass()\n\n jobInstance.$hydrate(payload, context)\n await jobInstance.execute()\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- export { b as AcquiredJob, A as Adapter, x as AdapterFactory, u as BackoffConfig, B as BackoffStrategy, s as DispatchManyResult, r as DispatchResult, D as Duration, a as JobClass, t as JobContext, J as JobData, g as JobFactory, h as JobOptions, d as JobRecord, c as JobRetention, q as JobStatus, L as Logger, v as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, y as ScheduleResult, j as ScheduleStatus, w as WorkerConfig, W as WorkerCycle } from '../../index-CoubP-c4.js';
1
+ export { b as AcquiredJob, A as Adapter, x as AdapterFactory, u as BackoffConfig, B as BackoffStrategy, s as DispatchManyResult, r as DispatchResult, D as Duration, a as JobClass, t as JobContext, J as JobData, g as JobFactory, h as JobOptions, d as JobRecord, c as JobRetention, q as JobStatus, L as Logger, v as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, y as ScheduleResult, j as ScheduleStatus, w as WorkerConfig, W as WorkerCycle } from '../../index-BAMFA6FI.js';
@@ -1 +1 @@
1
- export { x as AdapterFactory, u as BackoffConfig, B as BackoffStrategy, s as DispatchManyResult, r as DispatchResult, D as Duration, a as JobClass, t as JobContext, J as JobData, g as JobFactory, h as JobOptions, d as JobRecord, c as JobRetention, q as JobStatus, L as Logger, v as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, y as ScheduleResult, j as ScheduleStatus, w as WorkerConfig, W as WorkerCycle } from '../../index-CoubP-c4.js';
1
+ export { x as AdapterFactory, u as BackoffConfig, B as BackoffStrategy, s as DispatchManyResult, r as DispatchResult, D as Duration, a as JobClass, t as JobContext, J as JobData, g as JobFactory, h as JobOptions, d as JobRecord, c as JobRetention, q as JobStatus, L as Logger, v as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions, y as ScheduleResult, j as ScheduleStatus, w as WorkerConfig, W as WorkerCycle } from '../../index-BAMFA6FI.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boringnode/queue",
3
3
  "description": "A simple and efficient queue system for Node.js applications",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
7
7
  "files": [
@@ -31,32 +31,32 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@lukeed/ms": "^2.0.2",
34
- "@poppinss/utils": "^6.10.1",
35
- "cron-parser": "^5.4.0"
34
+ "@poppinss/utils": "^7.0.1",
35
+ "cron-parser": "^5.5.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@adonisjs/tsconfig": "^2.0.0-next.3",
39
- "@japa/assert": "^4.1.1",
40
- "@japa/expect-type": "^2.0.3",
41
- "@japa/file-system": "^2.3.2",
42
- "@japa/runner": "^4.4.0",
43
- "@poppinss/ts-exec": "^1.4.1",
38
+ "@adonisjs/tsconfig": "^2.0.0",
39
+ "@japa/assert": "^4.2.0",
40
+ "@japa/expect-type": "^2.0.4",
41
+ "@japa/file-system": "^3.0.0",
42
+ "@japa/runner": "^5.3.0",
43
+ "@poppinss/ts-exec": "^1.4.4",
44
44
  "@types/better-sqlite3": "^7.6.13",
45
- "@types/node": "^24.3.1",
46
- "@types/pg": "^8",
47
- "better-sqlite3": "^12.5.0",
48
- "bullmq": "^5.65.1",
49
- "c8": "^10.1.3",
45
+ "@types/node": "^24.11.0",
46
+ "@types/pg": "^8.18.0",
47
+ "better-sqlite3": "^12.6.2",
48
+ "bullmq": "^5.70.1",
49
+ "c8": "^11.0.0",
50
50
  "del-cli": "^7.0.0",
51
- "ioredis": "^5.7.0",
51
+ "ioredis": "^5.10.0",
52
52
  "knex": "^3.1.0",
53
- "oxfmt": "^0.26.0",
54
- "oxlint": "^1.41.0",
55
- "oxlint-tsgolint": "^0.11.1",
56
- "pg": "^8.16.3",
57
- "release-it": "^19.0.4",
53
+ "oxfmt": "^0.36.0",
54
+ "oxlint": "^1.51.0",
55
+ "oxlint-tsgolint": "^0.15.0",
56
+ "pg": "^8.19.0",
57
+ "release-it": "^19.2.4",
58
58
  "tsup": "^8.5.1",
59
- "typescript": "^5.9.2"
59
+ "typescript": "^5.9.3"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "ioredis": "^5.0.0",
@@ -72,6 +72,14 @@
72
72
  },
73
73
  "author": "Romain Lanz <romain.lanz@pm.me>",
74
74
  "license": "MIT",
75
+ "homepage": "https://github.com/boringnode/queue#readme",
76
+ "bugs": {
77
+ "url": "https://github.com/boringnode/queue/issues"
78
+ },
79
+ "repository": {
80
+ "type": "git",
81
+ "url": "https://github.com/boringnode/queue.git"
82
+ },
75
83
  "keywords": [
76
84
  "queue",
77
85
  "job",
@@ -95,7 +103,7 @@
95
103
  "web": true
96
104
  }
97
105
  },
98
- "packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
106
+ "packageManager": "yarn@4.12.0",
99
107
  "engines": {
100
108
  "node": ">=24.0.0"
101
109
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/exceptions.ts","../src/constants.ts","../src/utils.ts"],"sourcesContent":["import { createError } from '@poppinss/utils'\n\nexport const E_INVALID_DURATION_EXPRESSION = createError(\n 'Invalid duration expression: \"%s\"',\n 'E_INVALID_DURATION_EXPRESSION',\n 500\n)\n\nexport const E_INVALID_BASE_DELAY = createError<[reason: string]>(\n 'Invalid base delay. Reason: %s',\n 'E_INVALID_BASE_DELAY',\n 500\n)\n\nexport const E_INVALID_MAX_DELAY = createError<[reason: string]>(\n 'Invalid max delay. Reason: %s',\n 'E_INVALID_MAX_DELAY',\n 500\n)\n\nexport const E_INVALID_MULTIPLIER = createError<[reason: string]>(\n 'Invalid multiplier. Reason: %s',\n 'E_INVALID_MULTIPLIER',\n 500\n)\n\nexport const E_CONFIGURATION_ERROR = createError<[reason: string]>(\n 'Configuration error. Reason: %s',\n 'E_CONFIGURATION_ERROR',\n 500\n)\n\nexport const E_JOB_NOT_FOUND = createError<[jobName: string]>(\n 'Requested job \"%s\" is not registered',\n 'E_JOB_NOT_FOUND'\n)\n\nexport const E_JOB_MAX_ATTEMPTS_REACHED = createError<[jobName: string]>(\n 'The job \"%s\" has reached the maximum number of retry attempts',\n 'E_JOB_MAX_ATTEMPTS_REACHED'\n)\n\nexport const E_JOB_TIMEOUT = createError<[jobName: string, timeout: number]>(\n 'The job \"%s\" has exceeded the timeout of %dms',\n 'E_JOB_TIMEOUT'\n)\n\nexport const E_QUEUE_NOT_INITIALIZED = createError(\n 'QueueManager is not initialized. Call QueueManager.init() before using it.',\n 'E_QUEUE_NOT_INITIALIZED',\n 500\n)\n\nexport const E_ADAPTER_INIT_ERROR = createError<[adapterName: string, originalMessage: string]>(\n 'Failed to initialize adapter \"%s\". Reason: %s',\n 'E_ADAPTER_INIT_ERROR',\n 500\n)\n\nexport const E_NO_JOBS_FOUND = createError<[patterns: string]>(\n 'No jobs found for the specified locations: %s. Verify your glob patterns match your job files.',\n 'E_NO_JOBS_FOUND',\n 500\n)\n\nexport const E_INVALID_CRON_EXPRESSION = createError<[expression: string, reason: string]>(\n 'Invalid cron expression \"%s\": %s',\n 'E_INVALID_CRON_EXPRESSION',\n 500\n)\n\nexport const E_INVALID_SCHEDULE_CONFIG = createError<[reason: string]>(\n 'Invalid schedule configuration: %s',\n 'E_INVALID_SCHEDULE_CONFIG',\n 500\n)\n","/**\n * Default job priority (1-10 scale, lower = higher priority)\n */\nexport const DEFAULT_PRIORITY = 5\n\n/**\n * Multiplier used in score calculation: priority * multiplier + timestamp\n *\n * This ensures higher priority jobs are processed first,\n * while preserving FIFO order within the same priority.\n * The value (1e13) leaves room for ~300 years of millisecond timestamps.\n */\nexport const PRIORITY_SCORE_MULTIPLIER = 1e13\n\n/**\n * Default delay when the worker is idle (no jobs in queue)\n */\nexport const DEFAULT_IDLE_DELAY = '2s'\n\n/**\n * Default interval between stalled job checks\n */\nexport const DEFAULT_STALLED_INTERVAL = '30s'\n\n/**\n * Default threshold after which a job is considered stalled\n */\nexport const DEFAULT_STALLED_THRESHOLD = '30s'\n\n/**\n * Default delay before retrying after an error\n */\nexport const DEFAULT_ERROR_RETRY_DELAY = '5s'\n","import { parse as parseDuration } from '@lukeed/ms'\nimport type { Duration, JobRetention } from './types/main.js'\nimport * as errors from './exceptions.js'\nimport { PRIORITY_SCORE_MULTIPLIER } from './constants.js'\n\nexport interface ResolvedRetention {\n keep: boolean\n maxAge: number\n maxCount: number\n}\n\nexport function resolveRetention(retention?: JobRetention): ResolvedRetention {\n if (retention === undefined || retention === true) {\n return { keep: false, maxAge: 0, maxCount: 0 }\n }\n\n if (retention === false) {\n return { keep: true, maxAge: 0, maxCount: 0 }\n }\n\n return {\n keep: true,\n maxAge: retention.age ? parse(retention.age) : 0,\n maxCount: retention.count ?? 0,\n }\n}\n\nexport function parse(duration: Duration): number {\n if (typeof duration === 'number') {\n return duration\n }\n\n const milliseconds = parseDuration(duration)\n\n if (typeof milliseconds === 'undefined') {\n throw new errors.E_INVALID_DURATION_EXPRESSION([duration])\n }\n\n return milliseconds\n}\n\n/**\n * Calculate the score for job ordering in the queue.\n * Lower scores are processed first.\n *\n * @param priority - Job priority (1-10, lower = higher priority)\n * @param timestamp - Timestamp in milliseconds\n * @returns Score for queue ordering\n */\nexport function calculateScore(priority: number, timestamp: number): number {\n return priority * PRIORITY_SCORE_MULTIPLIER + timestamp\n}\n"],"mappings":";;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,mBAAmB;AAErB,IAAM,gCAAgC;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;AAEO,IAAM,6BAA6B;AAAA,EACxC;AAAA,EACA;AACF;AAEO,IAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AAEO,IAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,4BAA4B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,4BAA4B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AACF;;;ACxEO,IAAM,mBAAmB;AASzB,IAAM,4BAA4B;AAKlC,IAAM,qBAAqB;AAK3B,IAAM,2BAA2B;AAKjC,IAAM,4BAA4B;AAKlC,IAAM,4BAA4B;;;AChCzC,SAAS,SAAS,qBAAqB;AAWhC,SAAS,iBAAiB,WAA6C;AAC5E,MAAI,cAAc,UAAa,cAAc,MAAM;AACjD,WAAO,EAAE,MAAM,OAAO,QAAQ,GAAG,UAAU,EAAE;AAAA,EAC/C;AAEA,MAAI,cAAc,OAAO;AACvB,WAAO,EAAE,MAAM,MAAM,QAAQ,GAAG,UAAU,EAAE;AAAA,EAC9C;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,UAAU,MAAM,MAAM,UAAU,GAAG,IAAI;AAAA,IAC/C,UAAU,UAAU,SAAS;AAAA,EAC/B;AACF;AAEO,SAAS,MAAM,UAA4B;AAChD,MAAI,OAAO,aAAa,UAAU;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,cAAc,QAAQ;AAE3C,MAAI,OAAO,iBAAiB,aAAa;AACvC,UAAM,IAAW,8BAA8B,CAAC,QAAQ,CAAC;AAAA,EAC3D;AAEA,SAAO;AACT;AAUO,SAAS,eAAe,UAAkB,WAA2B;AAC1E,SAAO,WAAW,4BAA4B;AAChD;","names":[]}