@boringnode/queue 0.3.4 → 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.
@@ -1,5 +1,5 @@
1
1
  import { Knex } from 'knex';
2
- import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../index-CoubP-c4.js';
2
+ import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../index-BAMFA6FI.js';
3
3
 
4
4
  interface KnexAdapterOptions {
5
5
  connection: Knex;
@@ -43,6 +43,10 @@ declare class KnexAdapter implements Adapter {
43
43
  size(): Promise<number>;
44
44
  sizeOf(queue: string): Promise<number>;
45
45
  recoverStalledJobs(queue: string, stalledThreshold: number, maxStalledCount: number): Promise<number>;
46
+ upsertSchedule(config: ScheduleConfig): Promise<string>;
47
+ /**
48
+ * @deprecated Use `upsertSchedule` instead.
49
+ */
46
50
  createSchedule(config: ScheduleConfig): Promise<string>;
47
51
  getSchedule(id: string): Promise<ScheduleData | null>;
48
52
  listSchedules(options?: ScheduleListOptions): Promise<ScheduleData[]>;
@@ -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/knex_adapter.ts
8
8
  import { randomUUID } from "crypto";
@@ -20,7 +20,6 @@ var KnexAdapter = class {
20
20
  #schedulesTable;
21
21
  #ownsConnection;
22
22
  #workerId = "";
23
- #initialized = false;
24
23
  constructor(config) {
25
24
  this.#connection = config.connection;
26
25
  this.#jobsTable = config.tableName ?? "queue_jobs";
@@ -30,66 +29,6 @@ var KnexAdapter = class {
30
29
  setWorkerId(workerId) {
31
30
  this.#workerId = workerId;
32
31
  }
33
- /**
34
- * Ensure all required tables exist.
35
- * Creates them if not exists, handles race conditions.
36
- */
37
- async #ensureTables() {
38
- if (this.#initialized) return;
39
- await Promise.all([this.#createJobsTable(), this.#createSchedulesTable()]);
40
- this.#initialized = true;
41
- }
42
- async #createJobsTable() {
43
- try {
44
- await this.#connection.schema.createTable(this.#jobsTable, (table) => {
45
- table.string("id", 255).notNullable();
46
- table.string("queue", 255).notNullable();
47
- table.enu("status", ["pending", "active", "delayed", "completed", "failed"]).notNullable();
48
- table.text("data").notNullable();
49
- table.bigint("score").unsigned().nullable();
50
- table.string("worker_id", 255).nullable();
51
- table.bigint("acquired_at").unsigned().nullable();
52
- table.bigint("execute_at").unsigned().nullable();
53
- table.bigint("finished_at").unsigned().nullable();
54
- table.text("error").nullable();
55
- table.primary(["id", "queue"]);
56
- table.index(["queue", "status", "score"]);
57
- table.index(["queue", "status", "execute_at"]);
58
- table.index(["queue", "status", "finished_at"]);
59
- });
60
- } catch {
61
- const hasTable = await this.#connection.schema.hasTable(this.#jobsTable);
62
- if (!hasTable) {
63
- throw new Error(`Failed to create table "${this.#jobsTable}"`);
64
- }
65
- }
66
- }
67
- async #createSchedulesTable() {
68
- try {
69
- await this.#connection.schema.createTable(this.#schedulesTable, (table) => {
70
- table.string("id", 255).primary();
71
- table.string("status", 50).notNullable().defaultTo("active");
72
- table.string("name", 255).notNullable();
73
- table.text("payload").notNullable();
74
- table.string("cron_expression", 255).nullable();
75
- table.bigint("every_ms").unsigned().nullable();
76
- table.string("timezone", 100).notNullable().defaultTo("UTC");
77
- table.timestamp("from_date").nullable();
78
- table.timestamp("to_date").nullable();
79
- table.integer("run_limit").unsigned().nullable();
80
- table.integer("run_count").unsigned().notNullable().defaultTo(0);
81
- table.timestamp("next_run_at").nullable();
82
- table.timestamp("last_run_at").nullable();
83
- table.timestamp("created_at").notNullable().defaultTo(this.#connection.fn.now());
84
- table.index(["status", "next_run_at"]);
85
- });
86
- } catch {
87
- const hasTable = await this.#connection.schema.hasTable(this.#schedulesTable);
88
- if (!hasTable) {
89
- throw new Error(`Failed to create table "${this.#schedulesTable}"`);
90
- }
91
- }
92
- }
93
32
  async destroy() {
94
33
  if (this.#ownsConnection) {
95
34
  await this.#connection.destroy();
@@ -99,7 +38,6 @@ var KnexAdapter = class {
99
38
  return this.popFrom("default");
100
39
  }
101
40
  async popFrom(queue) {
102
- await this.#ensureTables();
103
41
  const now = Date.now();
104
42
  await this.#processDelayedJobs(queue, now);
105
43
  return this.#connection.transaction(async (trx) => {
@@ -160,7 +98,6 @@ var KnexAdapter = class {
160
98
  });
161
99
  }
162
100
  async completeJob(jobId, queue, removeOnComplete) {
163
- await this.#ensureTables();
164
101
  const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete);
165
102
  if (!keep) {
166
103
  await this.#connection(this.#jobsTable).where("id", jobId).where("queue", queue).where("status", "active").delete();
@@ -179,7 +116,6 @@ var KnexAdapter = class {
179
116
  await this.#pruneHistory(queue, "completed", maxAge, maxCount, now);
180
117
  }
181
118
  async failJob(jobId, queue, error, removeOnFail) {
182
- await this.#ensureTables();
183
119
  const { keep, maxAge, maxCount } = resolveRetention(removeOnFail);
184
120
  if (!keep) {
185
121
  await this.#connection(this.#jobsTable).where("id", jobId).where("queue", queue).where("status", "active").delete();
@@ -199,7 +135,6 @@ var KnexAdapter = class {
199
135
  await this.#pruneHistory(queue, "failed", maxAge, maxCount, now);
200
136
  }
201
137
  async getJob(jobId, queue) {
202
- await this.#ensureTables();
203
138
  const row = await this.#connection(this.#jobsTable).where("id", jobId).where("queue", queue).first();
204
139
  if (!row) {
205
140
  return null;
@@ -223,7 +158,6 @@ var KnexAdapter = class {
223
158
  }
224
159
  }
225
160
  async retryJob(jobId, queue, retryAt) {
226
- await this.#ensureTables();
227
161
  const now = Date.now();
228
162
  const activeJob = await this.#connection(this.#jobsTable).where("id", jobId).where("queue", queue).where("status", "active").first();
229
163
  if (!activeJob) return;
@@ -256,7 +190,6 @@ var KnexAdapter = class {
256
190
  return this.pushOn("default", jobData);
257
191
  }
258
192
  async pushOn(queue, jobData) {
259
- await this.#ensureTables();
260
193
  const priority = jobData.priority ?? DEFAULT_PRIORITY;
261
194
  const timestamp = Date.now();
262
195
  const score = calculateScore(priority, timestamp);
@@ -272,7 +205,6 @@ var KnexAdapter = class {
272
205
  return this.pushLaterOn("default", jobData, delay);
273
206
  }
274
207
  async pushLaterOn(queue, jobData, delay) {
275
- await this.#ensureTables();
276
208
  const executeAt = Date.now() + delay;
277
209
  await this.#connection(this.#jobsTable).insert({
278
210
  id: jobData.id,
@@ -287,7 +219,6 @@ var KnexAdapter = class {
287
219
  }
288
220
  async pushManyOn(queue, jobs) {
289
221
  if (jobs.length === 0) return;
290
- await this.#ensureTables();
291
222
  const now = Date.now();
292
223
  const rows = jobs.map((job) => ({
293
224
  id: job.id,
@@ -302,12 +233,10 @@ var KnexAdapter = class {
302
233
  return this.sizeOf("default");
303
234
  }
304
235
  async sizeOf(queue) {
305
- await this.#ensureTables();
306
236
  const result = await this.#connection(this.#jobsTable).where("queue", queue).where("status", "pending").count("* as count").first();
307
237
  return Number(result?.count ?? 0);
308
238
  }
309
239
  async recoverStalledJobs(queue, stalledThreshold, maxStalledCount) {
310
- await this.#ensureTables();
311
240
  const now = Date.now();
312
241
  const stalledCutoff = now - stalledThreshold;
313
242
  return this.#connection.transaction(async (trx) => {
@@ -339,8 +268,7 @@ var KnexAdapter = class {
339
268
  return recovered;
340
269
  });
341
270
  }
342
- async createSchedule(config) {
343
- await this.#ensureTables();
271
+ async upsertSchedule(config) {
344
272
  const id = config.id ?? randomUUID();
345
273
  const data = {
346
274
  id,
@@ -371,14 +299,18 @@ var KnexAdapter = class {
371
299
  });
372
300
  return id;
373
301
  }
302
+ /**
303
+ * @deprecated Use `upsertSchedule` instead.
304
+ */
305
+ createSchedule(config) {
306
+ return this.upsertSchedule(config);
307
+ }
374
308
  async getSchedule(id) {
375
- await this.#ensureTables();
376
309
  const row = await this.#connection(this.#schedulesTable).where("id", id).first();
377
310
  if (!row) return null;
378
311
  return this.#rowToScheduleData(row);
379
312
  }
380
313
  async listSchedules(options) {
381
- await this.#ensureTables();
382
314
  let query = this.#connection(this.#schedulesTable).whereNot("status", "cancelled");
383
315
  if (options?.status) {
384
316
  query = query.where("status", options.status);
@@ -387,7 +319,6 @@ var KnexAdapter = class {
387
319
  return rows.map((row) => this.#rowToScheduleData(row));
388
320
  }
389
321
  async updateSchedule(id, updates) {
390
- await this.#ensureTables();
391
322
  const data = {};
392
323
  if (updates.status !== void 0) data.status = updates.status;
393
324
  if (updates.nextRunAt !== void 0) data.next_run_at = updates.nextRunAt;
@@ -398,11 +329,9 @@ var KnexAdapter = class {
398
329
  }
399
330
  }
400
331
  async deleteSchedule(id) {
401
- await this.#ensureTables();
402
332
  await this.#connection(this.#schedulesTable).where("id", id).delete();
403
333
  }
404
334
  async claimDueSchedule() {
405
- await this.#ensureTables();
406
335
  const now = /* @__PURE__ */ new Date();
407
336
  return this.#connection.transaction(async (trx) => {
408
337
  let query = trx(this.#schedulesTable).where("status", "active").whereNotNull("next_run_at").where("next_run_at", "<=", now).where((builder) => {
@@ -416,7 +345,7 @@ var KnexAdapter = class {
416
345
  const row = await query.first();
417
346
  if (!row) return null;
418
347
  let nextRunAt = null;
419
- const newRunCount = (row.run_count ?? 0) + 1;
348
+ const newRunCount = Number(row.run_count ?? 0) + 1;
420
349
  if (row.every_ms) {
421
350
  nextRunAt = new Date(now.getTime() + Number(row.every_ms));
422
351
  } else if (row.cron_expression) {
@@ -427,7 +356,7 @@ var KnexAdapter = class {
427
356
  });
428
357
  nextRunAt = cron.next().toDate();
429
358
  }
430
- if (row.run_limit !== null && newRunCount >= row.run_limit) {
359
+ if (row.run_limit !== null && newRunCount >= Number(row.run_limit)) {
431
360
  nextRunAt = null;
432
361
  }
433
362
  if (nextRunAt && row.to_date && nextRunAt > new Date(row.to_date)) {
@@ -455,7 +384,7 @@ var KnexAdapter = class {
455
384
  runCount: Number(row.run_count ?? 0),
456
385
  nextRunAt: row.next_run_at ? new Date(row.next_run_at) : null,
457
386
  lastRunAt: row.last_run_at ? new Date(row.last_run_at) : null,
458
- status: row.status === "cancelled" ? "paused" : row.status,
387
+ status: row.status === "paused" || row.status === "cancelled" ? "paused" : "active",
459
388
  createdAt: row.created_at ? new Date(row.created_at) : /* @__PURE__ */ new Date()
460
389
  };
461
390
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/drivers/knex_adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport KnexPkg from 'knex'\nimport type { Knex } from 'knex'\nimport type { Adapter, AcquiredJob } from '../contracts/adapter.js'\nimport type {\n JobData,\n JobRecord,\n JobRetention,\n JobStatus,\n ScheduleConfig,\n ScheduleData,\n ScheduleListOptions,\n} from '../types/main.js'\nimport { DEFAULT_PRIORITY } from '../constants.js'\nimport { calculateScore, resolveRetention } from '../utils.js'\n\nexport interface KnexAdapterOptions {\n connection: Knex\n tableName?: string\n schedulesTableName?: string\n ownsConnection?: boolean\n}\n\ntype KnexConfig = Knex | Knex.Config\n\n/**\n * Create a new Knex adapter factory.\n * Accepts either a Knex instance or a Knex configuration object.\n *\n * When passing a config object, the adapter will create and manage\n * the connection lifecycle (closing it on destroy).\n *\n * When passing a Knex instance, the caller is responsible for\n * managing the connection lifecycle.\n */\nexport function knex(config: KnexConfig, tableName?: string) {\n return () => {\n const isKnexInstance = typeof config === 'function'\n const connection = isKnexInstance ? config : KnexPkg(config)\n return new KnexAdapter({ connection, tableName, ownsConnection: !isKnexInstance })\n }\n}\n\n/**\n * Knex adapter for the queue system.\n * Stores jobs in a SQL database using Knex.\n */\nexport class KnexAdapter implements Adapter {\n readonly #connection: Knex\n readonly #jobsTable: string\n readonly #schedulesTable: string\n readonly #ownsConnection: boolean\n #workerId: string = ''\n #initialized: boolean = false\n\n constructor(config: KnexAdapterOptions) {\n this.#connection = config.connection\n this.#jobsTable = config.tableName ?? 'queue_jobs'\n this.#schedulesTable = config.schedulesTableName ?? 'queue_schedules'\n this.#ownsConnection = config.ownsConnection ?? false\n }\n\n setWorkerId(workerId: string): void {\n this.#workerId = workerId\n }\n\n /**\n * Ensure all required tables exist.\n * Creates them if not exists, handles race conditions.\n */\n async #ensureTables(): Promise<void> {\n if (this.#initialized) return\n\n await Promise.all([this.#createJobsTable(), this.#createSchedulesTable()])\n\n this.#initialized = true\n }\n\n async #createJobsTable(): Promise<void> {\n try {\n await this.#connection.schema.createTable(this.#jobsTable, (table) => {\n table.string('id', 255).notNullable()\n table.string('queue', 255).notNullable()\n table.enu('status', ['pending', 'active', 'delayed', 'completed', 'failed']).notNullable()\n table.text('data').notNullable()\n table.bigint('score').unsigned().nullable()\n table.string('worker_id', 255).nullable()\n table.bigint('acquired_at').unsigned().nullable()\n table.bigint('execute_at').unsigned().nullable()\n table.bigint('finished_at').unsigned().nullable()\n table.text('error').nullable()\n table.primary(['id', 'queue'])\n table.index(['queue', 'status', 'score'])\n table.index(['queue', 'status', 'execute_at'])\n table.index(['queue', 'status', 'finished_at'])\n })\n } catch {\n /**\n * If table creation fails, verify the table actually exists.\n * This handles race conditions where multiple instances try to create\n * the table simultaneously.\n */\n const hasTable = await this.#connection.schema.hasTable(this.#jobsTable)\n if (!hasTable) {\n throw new Error(`Failed to create table \"${this.#jobsTable}\"`)\n }\n }\n }\n\n async #createSchedulesTable(): Promise<void> {\n try {\n await this.#connection.schema.createTable(this.#schedulesTable, (table) => {\n table.string('id', 255).primary()\n table.string('status', 50).notNullable().defaultTo('active')\n table.string('name', 255).notNullable()\n table.text('payload').notNullable()\n table.string('cron_expression', 255).nullable()\n table.bigint('every_ms').unsigned().nullable()\n table.string('timezone', 100).notNullable().defaultTo('UTC')\n table.timestamp('from_date').nullable()\n table.timestamp('to_date').nullable()\n table.integer('run_limit').unsigned().nullable()\n table.integer('run_count').unsigned().notNullable().defaultTo(0)\n table.timestamp('next_run_at').nullable()\n table.timestamp('last_run_at').nullable()\n table.timestamp('created_at').notNullable().defaultTo(this.#connection.fn.now())\n // Indexes\n table.index(['status', 'next_run_at'])\n })\n } catch {\n /**\n * If table creation fails, verify the table actually exists.\n * This handles race conditions where multiple instances try to create\n * the table simultaneously.\n */\n const hasTable = await this.#connection.schema.hasTable(this.#schedulesTable)\n if (!hasTable) {\n throw new Error(`Failed to create table \"${this.#schedulesTable}\"`)\n }\n }\n }\n\n async destroy(): Promise<void> {\n if (this.#ownsConnection) {\n await this.#connection.destroy()\n }\n }\n\n async pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n async popFrom(queue: string): Promise<AcquiredJob | null> {\n await this.#ensureTables()\n\n const now = Date.now()\n\n // First, move ready delayed jobs to pending\n await this.#processDelayedJobs(queue, now)\n\n // Use a transaction to atomically pop a job\n return this.#connection.transaction(async (trx) => {\n // Build the query for highest priority job (lowest score)\n let query = trx(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'pending')\n .orderBy('score', 'asc')\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const job = await query.first()\n\n if (!job) {\n return null\n }\n\n // Update job to active status\n // For SQLite (no SKIP LOCKED), add status='pending' guard to prevent double-claim\n const updateQuery = trx(this.#jobsTable).where('id', job.id).where('queue', queue)\n\n if (!this.#supportsSkipLocked()) {\n updateQuery.where('status', 'pending')\n }\n\n const updated = await updateQuery.update({\n status: 'active',\n worker_id: this.#workerId,\n acquired_at: now,\n })\n\n // Another worker already claimed this job\n if (updated === 0) {\n return null\n }\n\n const jobData: JobData = JSON.parse(job.data)\n\n return {\n ...jobData,\n acquiredAt: now,\n }\n })\n }\n\n /**\n * Check if the database supports FOR UPDATE SKIP LOCKED.\n * PostgreSQL 9.5+, MySQL 8.0+, and MariaDB 10.6+ support it.\n * SQLite does not, but it's single-writer so it doesn't need it.\n */\n #supportsSkipLocked(): boolean {\n const client = this.#connection.client.config.client\n return client === 'pg' || client === 'mysql' || client === 'mysql2' || client === 'mariadb'\n }\n\n async #processDelayedJobs(queue: string, now: number): Promise<void> {\n // Use a transaction with row locking to prevent race conditions\n await this.#connection.transaction(async (trx) => {\n let query = trx(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'delayed')\n .where('execute_at', '<=', now)\n .select('id', 'data')\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const delayedJobs = await query\n\n if (delayedJobs.length === 0) return\n\n // Move them to pending\n for (const job of delayedJobs) {\n const jobData: JobData = JSON.parse(job.data)\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n\n await trx(this.#jobsTable).where('id', job.id).where('queue', queue).update({\n status: 'pending',\n score,\n execute_at: null,\n })\n }\n })\n }\n\n async completeJob(jobId: string, queue: string, removeOnComplete?: JobRetention): Promise<void> {\n await this.#ensureTables()\n\n const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete)\n\n if (!keep) {\n await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .delete()\n return\n }\n\n const now = Date.now()\n\n const updated = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .update({\n status: 'completed',\n worker_id: null,\n acquired_at: null,\n finished_at: now,\n })\n\n if (!updated) {\n return\n }\n\n await this.#pruneHistory(queue, 'completed', maxAge, maxCount, now)\n }\n\n async failJob(\n jobId: string,\n queue: string,\n error?: Error,\n removeOnFail?: JobRetention\n ): Promise<void> {\n await this.#ensureTables()\n\n const { keep, maxAge, maxCount } = resolveRetention(removeOnFail)\n\n if (!keep) {\n await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .delete()\n return\n }\n\n const now = Date.now()\n\n const updated = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .update({\n status: 'failed',\n worker_id: null,\n acquired_at: null,\n finished_at: now,\n error: error?.message || null,\n })\n\n if (!updated) {\n return\n }\n\n await this.#pruneHistory(queue, 'failed', maxAge, maxCount, now)\n }\n\n async getJob(jobId: string, queue: string): Promise<JobRecord | null> {\n await this.#ensureTables()\n\n const row = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .first()\n\n if (!row) {\n return null\n }\n\n const jobData: JobData = JSON.parse(row.data)\n\n return {\n status: row.status as JobStatus,\n data: jobData,\n finishedAt: row.finished_at ? Number(row.finished_at) : undefined,\n error: row.error || undefined,\n }\n }\n\n async #pruneHistory(\n queue: string,\n status: 'completed' | 'failed',\n maxAge: number,\n maxCount: number,\n now: number\n ): Promise<void> {\n if (maxAge > 0) {\n const cutoff = now - maxAge\n await this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', status)\n .where('finished_at', '<', cutoff)\n .delete()\n }\n\n if (maxCount > 0) {\n const toKeep = this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', status)\n .orderBy('finished_at', 'desc')\n .limit(maxCount)\n .select('id')\n\n await this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', status)\n .whereNotIn('id', toKeep)\n .delete()\n }\n }\n\n async retryJob(jobId: string, queue: string, retryAt?: Date): Promise<void> {\n await this.#ensureTables()\n\n const now = Date.now()\n\n // Get the active job\n const activeJob = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .first()\n\n if (!activeJob) return\n\n const jobData: JobData = JSON.parse(activeJob.data)\n jobData.attempts = (jobData.attempts || 0) + 1\n\n const updatedData = JSON.stringify(jobData)\n\n if (retryAt && retryAt.getTime() > now) {\n // Move to delayed\n await this.#connection(this.#jobsTable).where('id', jobId).where('queue', queue).update({\n status: 'delayed',\n data: updatedData,\n worker_id: null,\n acquired_at: null,\n score: null,\n execute_at: retryAt.getTime(),\n })\n } else {\n // Move back to pending\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n\n await this.#connection(this.#jobsTable).where('id', jobId).where('queue', queue).update({\n status: 'pending',\n data: updatedData,\n worker_id: null,\n acquired_at: null,\n score,\n execute_at: null,\n })\n }\n }\n\n async push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n async pushOn(queue: string, jobData: JobData): Promise<void> {\n await this.#ensureTables()\n\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const timestamp = Date.now()\n const score = calculateScore(priority, timestamp)\n\n await this.#connection(this.#jobsTable).insert({\n id: jobData.id,\n queue,\n status: 'pending',\n data: JSON.stringify(jobData),\n score,\n })\n }\n\n async 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 await this.#ensureTables()\n\n const executeAt = Date.now() + delay\n\n await this.#connection(this.#jobsTable).insert({\n id: jobData.id,\n queue,\n status: 'delayed',\n data: JSON.stringify(jobData),\n execute_at: executeAt,\n })\n }\n\n async 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 await this.#ensureTables()\n\n const now = Date.now()\n const rows = jobs.map((job) => ({\n id: job.id,\n queue,\n status: 'pending' as const,\n data: JSON.stringify(job),\n score: calculateScore(job.priority ?? DEFAULT_PRIORITY, now),\n }))\n\n await this.#connection(this.#jobsTable).insert(rows)\n }\n\n async size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n async sizeOf(queue: string): Promise<number> {\n await this.#ensureTables()\n\n const result = await this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'pending')\n .count('* as count')\n .first()\n\n return Number(result?.count ?? 0)\n }\n\n async recoverStalledJobs(\n queue: string,\n stalledThreshold: number,\n maxStalledCount: number\n ): Promise<number> {\n await this.#ensureTables()\n\n const now = Date.now()\n const stalledCutoff = now - stalledThreshold\n\n // Use a transaction with row locking to prevent race conditions\n return this.#connection.transaction(async (trx) => {\n let recovered = 0\n\n let query = trx(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'active')\n .where('acquired_at', '<', stalledCutoff)\n .select('id', 'data')\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const stalledJobs = await query\n\n for (const row of stalledJobs) {\n const jobData: JobData = JSON.parse(row.data)\n const currentStalledCount = jobData.stalledCount ?? 0\n\n if (currentStalledCount >= maxStalledCount) {\n // Fail permanently - remove the job\n await trx(this.#jobsTable).where('id', row.id).where('queue', queue).delete()\n } else {\n // Recover: increment stalledCount and put back in pending\n jobData.stalledCount = currentStalledCount + 1\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n\n await trx(this.#jobsTable)\n .where('id', row.id)\n .where('queue', queue)\n .update({\n status: 'pending',\n data: JSON.stringify(jobData),\n worker_id: null,\n acquired_at: null,\n score,\n })\n\n recovered++\n }\n }\n\n return recovered\n })\n }\n\n async createSchedule(config: ScheduleConfig): Promise<string> {\n await this.#ensureTables()\n\n const id = config.id ?? randomUUID()\n\n const data = {\n id,\n name: config.name,\n payload: JSON.stringify(config.payload),\n cron_expression: config.cronExpression ?? null,\n every_ms: config.everyMs ?? null,\n timezone: config.timezone,\n from_date: config.from ?? null,\n to_date: config.to ?? null,\n run_limit: config.limit ?? null,\n status: 'active',\n }\n\n // Atomic upsert\n await this.#connection(this.#schedulesTable)\n .insert({\n ...data,\n run_count: 0,\n created_at: this.#connection.fn.now(),\n })\n .onConflict('id')\n .merge({\n name: data.name,\n payload: data.payload,\n cron_expression: data.cron_expression,\n every_ms: data.every_ms,\n timezone: data.timezone,\n from_date: data.from_date,\n to_date: data.to_date,\n run_limit: data.run_limit,\n status: 'active',\n })\n\n return id\n }\n\n async getSchedule(id: string): Promise<ScheduleData | null> {\n await this.#ensureTables()\n\n const row = await this.#connection(this.#schedulesTable).where('id', id).first()\n if (!row) return null\n\n return this.#rowToScheduleData(row)\n }\n\n async listSchedules(options?: ScheduleListOptions): Promise<ScheduleData[]> {\n await this.#ensureTables()\n\n let query = this.#connection(this.#schedulesTable).whereNot('status', 'cancelled')\n\n if (options?.status) {\n query = query.where('status', options.status)\n }\n\n const rows = await query\n return rows.map((row: any) => this.#rowToScheduleData(row))\n }\n\n async updateSchedule(\n id: string,\n updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>\n ): Promise<void> {\n await this.#ensureTables()\n\n const data: Record<string, any> = {}\n\n if (updates.status !== undefined) data.status = updates.status\n if (updates.nextRunAt !== undefined) data.next_run_at = updates.nextRunAt\n if (updates.lastRunAt !== undefined) data.last_run_at = updates.lastRunAt\n if (updates.runCount !== undefined) data.run_count = updates.runCount\n\n if (Object.keys(data).length > 0) {\n await this.#connection(this.#schedulesTable).where('id', id).update(data)\n }\n }\n\n async deleteSchedule(id: string): Promise<void> {\n await this.#ensureTables()\n\n await this.#connection(this.#schedulesTable).where('id', id).delete()\n }\n\n async claimDueSchedule(): Promise<ScheduleData | null> {\n await this.#ensureTables()\n\n const now = new Date()\n\n return this.#connection.transaction(async (trx) => {\n // Find one due schedule with row locking\n let query = trx(this.#schedulesTable)\n .where('status', 'active')\n .whereNotNull('next_run_at')\n .where('next_run_at', '<=', now)\n .where((builder) => {\n builder.whereNull('run_limit').orWhereRaw('run_count < run_limit')\n })\n .where((builder) => {\n builder.whereNull('to_date').orWhere('to_date', '>=', now)\n })\n .orderBy('next_run_at', 'asc')\n .limit(1)\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const row = await query.first()\n if (!row) return null\n\n // Calculate next run time\n let nextRunAt: Date | null = null\n const newRunCount = (row.run_count ?? 0) + 1\n\n if (row.every_ms) {\n nextRunAt = new Date(now.getTime() + Number(row.every_ms))\n } else if (row.cron_expression) {\n // Import cron-parser dynamically to calculate next run\n const { CronExpressionParser } = await import('cron-parser')\n const cron = CronExpressionParser.parse(row.cron_expression, {\n currentDate: now,\n tz: row.timezone || 'UTC',\n })\n nextRunAt = cron.next().toDate()\n }\n\n // Check if limit will be reached\n if (row.run_limit !== null && newRunCount >= row.run_limit) {\n nextRunAt = null\n }\n\n // Check if past end date\n if (nextRunAt && row.to_date && nextRunAt > new Date(row.to_date)) {\n nextRunAt = null\n }\n\n // Update atomically\n await trx(this.#schedulesTable).where('id', row.id).update({\n next_run_at: nextRunAt,\n last_run_at: now,\n run_count: newRunCount,\n })\n\n // Return schedule data (before update state for payload)\n return this.#rowToScheduleData(row)\n })\n }\n\n #rowToScheduleData(row: any): ScheduleData {\n return {\n id: row.id,\n name: row.name,\n payload: typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload,\n cronExpression: row.cron_expression ?? null,\n everyMs: row.every_ms ? Number(row.every_ms) : null,\n timezone: row.timezone ?? 'UTC',\n from: row.from_date ? new Date(row.from_date) : null,\n to: row.to_date ? new Date(row.to_date) : null,\n limit: row.run_limit ? Number(row.run_limit) : null,\n runCount: Number(row.run_count ?? 0),\n nextRunAt: row.next_run_at ? new Date(row.next_run_at) : null,\n lastRunAt: row.last_run_at ? new Date(row.last_run_at) : null,\n status: row.status === 'cancelled' ? 'paused' : row.status,\n createdAt: row.created_at ? new Date(row.created_at) : new Date(),\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/drivers/knex_adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport KnexPkg from 'knex'\nimport type { Knex } from 'knex'\nimport type { Adapter, AcquiredJob } from '../contracts/adapter.js'\nimport type {\n JobData,\n JobRecord,\n JobRetention,\n JobStatus,\n ScheduleConfig,\n ScheduleData,\n ScheduleListOptions,\n} from '../types/main.js'\nimport { DEFAULT_PRIORITY } from '../constants.js'\nimport { calculateScore, resolveRetention } from '../utils.js'\n\nexport interface KnexAdapterOptions {\n connection: Knex\n tableName?: string\n schedulesTableName?: string\n ownsConnection?: boolean\n}\n\ntype KnexConfig = Knex | Knex.Config\ntype DbRow = Record<string, unknown>\n\ninterface ScheduleRow extends DbRow {\n id: string\n name: string\n payload: unknown\n cron_expression: string | null\n every_ms: number | string | null\n timezone: string | null\n from_date: Date | string | number | null\n to_date: Date | string | number | null\n run_limit: number | string | null\n run_count: number | string | null\n next_run_at: Date | string | number | null\n last_run_at: Date | string | number | null\n status: string\n created_at: Date | string | number | null\n}\n\n/**\n * Create a new Knex adapter factory.\n * Accepts either a Knex instance or a Knex configuration object.\n *\n * When passing a config object, the adapter will create and manage\n * the connection lifecycle (closing it on destroy).\n *\n * When passing a Knex instance, the caller is responsible for\n * managing the connection lifecycle.\n */\nexport function knex(config: KnexConfig, tableName?: string) {\n return () => {\n const isKnexInstance = typeof config === 'function'\n const connection = isKnexInstance ? config : KnexPkg(config)\n return new KnexAdapter({ connection, tableName, ownsConnection: !isKnexInstance })\n }\n}\n\n/**\n * Knex adapter for the queue system.\n * Stores jobs in a SQL database using Knex.\n */\nexport class KnexAdapter implements Adapter {\n readonly #connection: Knex\n readonly #jobsTable: string\n readonly #schedulesTable: string\n readonly #ownsConnection: boolean\n #workerId: string = ''\n\n constructor(config: KnexAdapterOptions) {\n this.#connection = config.connection\n this.#jobsTable = config.tableName ?? 'queue_jobs'\n this.#schedulesTable = config.schedulesTableName ?? 'queue_schedules'\n this.#ownsConnection = config.ownsConnection ?? false\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.destroy()\n }\n }\n\n async pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n async popFrom(queue: string): Promise<AcquiredJob | null> {\n const now = Date.now()\n\n // First, move ready delayed jobs to pending\n await this.#processDelayedJobs(queue, now)\n\n // Use a transaction to atomically pop a job\n return this.#connection.transaction(async (trx) => {\n // Build the query for highest priority job (lowest score)\n let query = trx(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'pending')\n .orderBy('score', 'asc')\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const job = await query.first()\n\n if (!job) {\n return null\n }\n\n // Update job to active status\n // For SQLite (no SKIP LOCKED), add status='pending' guard to prevent double-claim\n const updateQuery = trx(this.#jobsTable)\n .where('id', job.id)\n .where('queue', queue)\n\n if (!this.#supportsSkipLocked()) {\n updateQuery.where('status', 'pending')\n }\n\n const updated = await updateQuery.update({\n status: 'active',\n worker_id: this.#workerId,\n acquired_at: now,\n })\n\n // Another worker already claimed this job\n if (updated === 0) {\n return null\n }\n\n const jobData: JobData = JSON.parse(job.data)\n\n return {\n ...jobData,\n acquiredAt: now,\n }\n })\n }\n\n /**\n * Check if the database supports FOR UPDATE SKIP LOCKED.\n * PostgreSQL 9.5+, MySQL 8.0+, and MariaDB 10.6+ support it.\n * SQLite does not, but it's single-writer so it doesn't need it.\n */\n #supportsSkipLocked(): boolean {\n const client = this.#connection.client.config.client\n return client === 'pg' || client === 'mysql' || client === 'mysql2' || client === 'mariadb'\n }\n\n async #processDelayedJobs(queue: string, now: number): Promise<void> {\n // Use a transaction with row locking to prevent race conditions\n await this.#connection.transaction(async (trx) => {\n let query = trx(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'delayed')\n .where('execute_at', '<=', now)\n .select('id', 'data')\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const delayedJobs = await query\n\n if (delayedJobs.length === 0) return\n\n // Move them to pending\n for (const job of delayedJobs) {\n const jobData: JobData = JSON.parse(job.data)\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n\n await trx(this.#jobsTable)\n .where('id', job.id)\n .where('queue', queue)\n .update({\n status: 'pending',\n score,\n execute_at: null,\n })\n }\n })\n }\n\n async completeJob(jobId: string, queue: string, removeOnComplete?: JobRetention): Promise<void> {\n const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete)\n\n if (!keep) {\n await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .delete()\n return\n }\n\n const now = Date.now()\n\n const updated = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .update({\n status: 'completed',\n worker_id: null,\n acquired_at: null,\n finished_at: now,\n })\n\n if (!updated) {\n return\n }\n\n await this.#pruneHistory(queue, 'completed', maxAge, maxCount, now)\n }\n\n async failJob(\n jobId: string,\n queue: string,\n error?: Error,\n removeOnFail?: JobRetention\n ): Promise<void> {\n const { keep, maxAge, maxCount } = resolveRetention(removeOnFail)\n\n if (!keep) {\n await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .delete()\n return\n }\n\n const now = Date.now()\n\n const updated = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .update({\n status: 'failed',\n worker_id: null,\n acquired_at: null,\n finished_at: now,\n error: error?.message || null,\n })\n\n if (!updated) {\n return\n }\n\n await this.#pruneHistory(queue, 'failed', maxAge, maxCount, now)\n }\n\n async getJob(jobId: string, queue: string): Promise<JobRecord | null> {\n const row = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .first()\n\n if (!row) {\n return null\n }\n\n const jobData: JobData = JSON.parse(row.data)\n\n return {\n status: row.status as JobStatus,\n data: jobData,\n finishedAt: row.finished_at ? Number(row.finished_at) : undefined,\n error: row.error || undefined,\n }\n }\n\n async #pruneHistory(\n queue: string,\n status: 'completed' | 'failed',\n maxAge: number,\n maxCount: number,\n now: number\n ): Promise<void> {\n if (maxAge > 0) {\n const cutoff = now - maxAge\n await this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', status)\n .where('finished_at', '<', cutoff)\n .delete()\n }\n\n if (maxCount > 0) {\n const toKeep = this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', status)\n .orderBy('finished_at', 'desc')\n .limit(maxCount)\n .select('id')\n\n await this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', status)\n .whereNotIn('id', toKeep)\n .delete()\n }\n }\n\n async retryJob(jobId: string, queue: string, retryAt?: Date): Promise<void> {\n const now = Date.now()\n\n // Get the active job\n const activeJob = await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .where('status', 'active')\n .first()\n\n if (!activeJob) return\n\n const jobData: JobData = JSON.parse(activeJob.data)\n jobData.attempts = (jobData.attempts || 0) + 1\n\n const updatedData = JSON.stringify(jobData)\n\n if (retryAt && retryAt.getTime() > now) {\n // Move to delayed\n await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .update({\n status: 'delayed',\n data: updatedData,\n worker_id: null,\n acquired_at: null,\n score: null,\n execute_at: retryAt.getTime(),\n })\n } else {\n // Move back to pending\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n\n await this.#connection(this.#jobsTable)\n .where('id', jobId)\n .where('queue', queue)\n .update({\n status: 'pending',\n data: updatedData,\n worker_id: null,\n acquired_at: null,\n score,\n execute_at: null,\n })\n }\n }\n\n async push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n async pushOn(queue: string, jobData: JobData): Promise<void> {\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const timestamp = Date.now()\n const score = calculateScore(priority, timestamp)\n\n await this.#connection(this.#jobsTable).insert({\n id: jobData.id,\n queue,\n status: 'pending',\n data: JSON.stringify(jobData),\n score,\n })\n }\n\n async 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 executeAt = Date.now() + delay\n\n await this.#connection(this.#jobsTable).insert({\n id: jobData.id,\n queue,\n status: 'delayed',\n data: JSON.stringify(jobData),\n execute_at: executeAt,\n })\n }\n\n async 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 now = Date.now()\n const rows = jobs.map((job) => ({\n id: job.id,\n queue,\n status: 'pending' as const,\n data: JSON.stringify(job),\n score: calculateScore(job.priority ?? DEFAULT_PRIORITY, now),\n }))\n\n await this.#connection(this.#jobsTable).insert(rows)\n }\n\n async size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n async sizeOf(queue: string): Promise<number> {\n const result = await this.#connection(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'pending')\n .count('* as count')\n .first()\n\n return Number(result?.count ?? 0)\n }\n\n async recoverStalledJobs(\n queue: string,\n stalledThreshold: number,\n maxStalledCount: number\n ): Promise<number> {\n const now = Date.now()\n const stalledCutoff = now - stalledThreshold\n\n // Use a transaction with row locking to prevent race conditions\n return this.#connection.transaction(async (trx) => {\n let recovered = 0\n\n let query = trx(this.#jobsTable)\n .where('queue', queue)\n .where('status', 'active')\n .where('acquired_at', '<', stalledCutoff)\n .select('id', 'data')\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const stalledJobs = await query\n\n for (const row of stalledJobs) {\n const jobData: JobData = JSON.parse(row.data)\n const currentStalledCount = jobData.stalledCount ?? 0\n\n if (currentStalledCount >= maxStalledCount) {\n // Fail permanently - remove the job\n await trx(this.#jobsTable)\n .where('id', row.id)\n .where('queue', queue)\n .delete()\n } else {\n // Recover: increment stalledCount and put back in pending\n jobData.stalledCount = currentStalledCount + 1\n const priority = jobData.priority ?? DEFAULT_PRIORITY\n const score = calculateScore(priority, now)\n\n await trx(this.#jobsTable)\n .where('id', row.id)\n .where('queue', queue)\n .update({\n status: 'pending',\n data: JSON.stringify(jobData),\n worker_id: null,\n acquired_at: null,\n score,\n })\n\n recovered++\n }\n }\n\n return recovered\n })\n }\n\n async upsertSchedule(config: ScheduleConfig): Promise<string> {\n const id = config.id ?? randomUUID()\n\n const data = {\n id,\n name: config.name,\n payload: JSON.stringify(config.payload),\n cron_expression: config.cronExpression ?? null,\n every_ms: config.everyMs ?? null,\n timezone: config.timezone,\n from_date: config.from ?? null,\n to_date: config.to ?? null,\n run_limit: config.limit ?? null,\n status: 'active',\n }\n\n // Atomic upsert\n await this.#connection(this.#schedulesTable)\n .insert({\n ...data,\n run_count: 0,\n created_at: this.#connection.fn.now(),\n })\n .onConflict('id')\n .merge({\n name: data.name,\n payload: data.payload,\n cron_expression: data.cron_expression,\n every_ms: data.every_ms,\n timezone: data.timezone,\n from_date: data.from_date,\n to_date: data.to_date,\n run_limit: data.run_limit,\n status: 'active',\n })\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 row = (await this.#connection(this.#schedulesTable)\n .where('id', id)\n .first()) as ScheduleRow | undefined\n if (!row) return null\n\n return this.#rowToScheduleData(row)\n }\n\n async listSchedules(options?: ScheduleListOptions): Promise<ScheduleData[]> {\n let query = this.#connection(this.#schedulesTable).whereNot('status', 'cancelled')\n\n if (options?.status) {\n query = query.where('status', options.status)\n }\n\n const rows = (await query) as ScheduleRow[]\n return rows.map((row) => this.#rowToScheduleData(row))\n }\n\n async updateSchedule(\n id: string,\n updates: Partial<Pick<ScheduleData, 'status' | 'nextRunAt' | 'lastRunAt' | 'runCount'>>\n ): Promise<void> {\n const data: Record<string, unknown> = {}\n\n if (updates.status !== undefined) data.status = updates.status\n if (updates.nextRunAt !== undefined) data.next_run_at = updates.nextRunAt\n if (updates.lastRunAt !== undefined) data.last_run_at = updates.lastRunAt\n if (updates.runCount !== undefined) data.run_count = updates.runCount\n\n if (Object.keys(data).length > 0) {\n await this.#connection(this.#schedulesTable)\n .where('id', id)\n .update(data)\n }\n }\n\n async deleteSchedule(id: string): Promise<void> {\n await this.#connection(this.#schedulesTable)\n .where('id', id)\n .delete()\n }\n\n async claimDueSchedule(): Promise<ScheduleData | null> {\n const now = new Date()\n\n return this.#connection.transaction(async (trx) => {\n // Find one due schedule with row locking\n let query = trx(this.#schedulesTable)\n .where('status', 'active')\n .whereNotNull('next_run_at')\n .where('next_run_at', '<=', now)\n .where((builder) => {\n builder.whereNull('run_limit').orWhereRaw('run_count < run_limit')\n })\n .where((builder) => {\n builder.whereNull('to_date').orWhere('to_date', '>=', now)\n })\n .orderBy('next_run_at', 'asc')\n .limit(1)\n\n if (this.#supportsSkipLocked()) {\n query = query.forUpdate().skipLocked()\n }\n\n const row = (await query.first()) as ScheduleRow | undefined\n if (!row) return null\n\n // Calculate next run time\n let nextRunAt: Date | null = null\n const newRunCount = Number(row.run_count ?? 0) + 1\n\n if (row.every_ms) {\n nextRunAt = new Date(now.getTime() + Number(row.every_ms))\n } else if (row.cron_expression) {\n // Import cron-parser dynamically to calculate next run\n const { CronExpressionParser } = await import('cron-parser')\n const cron = CronExpressionParser.parse(row.cron_expression, {\n currentDate: now,\n tz: row.timezone || 'UTC',\n })\n nextRunAt = cron.next().toDate()\n }\n\n // Check if limit will be reached\n if (row.run_limit !== null && newRunCount >= Number(row.run_limit)) {\n nextRunAt = null\n }\n\n // Check if past end date\n if (nextRunAt && row.to_date && nextRunAt > new Date(row.to_date)) {\n nextRunAt = null\n }\n\n // Update atomically\n await trx(this.#schedulesTable)\n .where('id', row.id)\n .update({\n next_run_at: nextRunAt,\n last_run_at: now,\n run_count: newRunCount,\n })\n\n // Return schedule data (before update state for payload)\n return this.#rowToScheduleData(row)\n })\n }\n\n #rowToScheduleData(row: ScheduleRow): ScheduleData {\n return {\n id: row.id,\n name: row.name,\n payload: typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload,\n cronExpression: row.cron_expression ?? null,\n everyMs: row.every_ms ? Number(row.every_ms) : null,\n timezone: row.timezone ?? 'UTC',\n from: row.from_date ? new Date(row.from_date) : null,\n to: row.to_date ? new Date(row.to_date) : null,\n limit: row.run_limit ? Number(row.run_limit) : null,\n runCount: Number(row.run_count ?? 0),\n nextRunAt: row.next_run_at ? new Date(row.next_run_at) : null,\n lastRunAt: row.last_run_at ? new Date(row.last_run_at) : null,\n status: row.status === 'paused' || row.status === 'cancelled' ? 'paused' : 'active',\n createdAt: row.created_at ? new Date(row.created_at) : new Date(),\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  import { Redis, RedisOptions } from 'ioredis';
2
- import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../index-CoubP-c4.js';
2
+ import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../index-BAMFA6FI.js';
3
3
 
4
4
  type RedisConfig = Redis | RedisOptions;
5
5
  /**
@@ -33,6 +33,10 @@ declare class RedisAdapter implements Adapter {
33
33
  size(): Promise<number>;
34
34
  sizeOf(queue: string): Promise<number>;
35
35
  recoverStalledJobs(queue: string, stalledThreshold: number, maxStalledCount: number): Promise<number>;
36
+ upsertSchedule(config: ScheduleConfig): Promise<string>;
37
+ /**
38
+ * @deprecated Use `upsertSchedule` instead.
39
+ */
36
40
  createSchedule(config: ScheduleConfig): Promise<string>;
37
41
  getSchedule(id: string): Promise<ScheduleData | null>;
38
42
  listSchedules(options?: ScheduleListOptions): Promise<ScheduleData[]>;
@@ -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();