@monlite/queue 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +25 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +25 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -64,19 +64,18 @@ var WorkerImpl = class {
|
|
|
64
64
|
let hb;
|
|
65
65
|
if (this.visibilityTimeout > 0) {
|
|
66
66
|
hb = setInterval(
|
|
67
|
-
() => this.q.heartbeatInternal(job.id),
|
|
67
|
+
() => this.q.heartbeatInternal(job.id, job.attempts),
|
|
68
68
|
Math.max(1e3, Math.floor(this.visibilityTimeout / 2))
|
|
69
69
|
);
|
|
70
70
|
hb.unref?.();
|
|
71
71
|
}
|
|
72
72
|
Promise.resolve().then(() => this.handler(job)).then(
|
|
73
73
|
(result) => {
|
|
74
|
-
this.q.completeInternal(job, result)
|
|
75
|
-
|
|
74
|
+
if (this.q.completeInternal(job, result))
|
|
75
|
+
this.q.emit("completed", job, result);
|
|
76
76
|
},
|
|
77
77
|
(err) => {
|
|
78
|
-
this.q.failInternal(job, err);
|
|
79
|
-
this.q.emit("failed", job, err);
|
|
78
|
+
if (this.q.failInternal(job, err)) this.q.emit("failed", job, err);
|
|
80
79
|
}
|
|
81
80
|
).finally(() => {
|
|
82
81
|
if (hb) clearInterval(hb);
|
|
@@ -213,8 +212,10 @@ var Queue = class extends events.EventEmitter {
|
|
|
213
212
|
).run(...params).changes;
|
|
214
213
|
}
|
|
215
214
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
216
|
-
heartbeatInternal(id) {
|
|
217
|
-
this.driver.prepare(
|
|
215
|
+
heartbeatInternal(id, attempts) {
|
|
216
|
+
this.driver.prepare(
|
|
217
|
+
`UPDATE _jobs SET updated_at=? WHERE id=? AND status='active' AND attempts=?`
|
|
218
|
+
).run(now(), id, attempts);
|
|
218
219
|
}
|
|
219
220
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
220
221
|
async close() {
|
|
@@ -234,28 +235,31 @@ var Queue = class extends events.EventEmitter {
|
|
|
234
235
|
).get(this.workerId, t, name, t);
|
|
235
236
|
return row ? deserialize(row) : null;
|
|
236
237
|
}
|
|
237
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* @internal Mark a job done. Returns false if the job was reclaimed by another
|
|
240
|
+
* worker since this one claimed it (fenced on the claim-time attempt) — the
|
|
241
|
+
* caller then skips emitting "completed" so a revived stale worker can't clobber
|
|
242
|
+
* the new run or fire a duplicate event.
|
|
243
|
+
*/
|
|
238
244
|
completeInternal(job, result) {
|
|
239
245
|
if (this.removeOnComplete) {
|
|
240
|
-
this.driver.prepare(`DELETE FROM _jobs WHERE id
|
|
241
|
-
} else {
|
|
242
|
-
this.driver.prepare(
|
|
243
|
-
`UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`
|
|
244
|
-
).run(JSON.stringify(result ?? null), now(), job.id);
|
|
246
|
+
return this.driver.prepare(`DELETE FROM _jobs WHERE id=? AND attempts=?`).run(job.id, job.attempts).changes > 0;
|
|
245
247
|
}
|
|
248
|
+
return this.driver.prepare(
|
|
249
|
+
`UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=? AND attempts=?`
|
|
250
|
+
).run(JSON.stringify(result ?? null), now(), job.id, job.attempts).changes > 0;
|
|
246
251
|
}
|
|
247
|
-
/** @internal */
|
|
252
|
+
/** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */
|
|
248
253
|
failInternal(job, err) {
|
|
249
254
|
const message = err instanceof Error ? err.message : String(err);
|
|
250
255
|
if (job.attempts < job.maxAttempts) {
|
|
251
|
-
this.driver.prepare(
|
|
252
|
-
`UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`
|
|
253
|
-
).run(now() + this.backoff(job.attempts), message, now(), job.id);
|
|
254
|
-
} else {
|
|
255
|
-
this.driver.prepare(
|
|
256
|
-
`UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`
|
|
257
|
-
).run(message, now(), job.id);
|
|
256
|
+
return this.driver.prepare(
|
|
257
|
+
`UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=? AND attempts=?`
|
|
258
|
+
).run(now() + this.backoff(job.attempts), message, now(), job.id, job.attempts).changes > 0;
|
|
258
259
|
}
|
|
260
|
+
return this.driver.prepare(
|
|
261
|
+
`UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=? AND attempts=?`
|
|
262
|
+
).run(message, now(), job.id, job.attempts).changes > 0;
|
|
259
263
|
}
|
|
260
264
|
};
|
|
261
265
|
function createQueue(db, opts) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":"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","file":"index.cjs","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite } from \"@monlite/core\";\n\nexport type JobStatus = \"pending\" | \"active\" | \"done\" | \"failed\";\n\nexport interface Job<T = any> {\n id: number;\n queue: string;\n /** Dedupe key, if the job was added with one. */\n jobId?: string;\n status: JobStatus;\n priority: number;\n payload: T;\n /** Number of attempts already made (0 until the first run). */\n attempts: number;\n maxAttempts: number;\n runAt: number;\n result?: any;\n error?: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AddOptions {\n /** Dedupe key — skip if a job with this id is already pending/active. */\n jobId?: string;\n /** Delay before the job becomes runnable (ms). */\n delay?: number;\n /** Explicit epoch-ms run time (overrides `delay`). */\n runAt?: number;\n /** Higher runs first. Default 0. */\n priority?: number;\n /** Total attempts before dead-lettering. Default: the queue's `maxAttempts`. */\n maxAttempts?: number;\n}\n\nexport interface ProcessOptions {\n /** Jobs run concurrently per worker. Default 1. */\n concurrency?: number;\n /** How often to poll for due jobs when idle (ms). Default 500. */\n pollInterval?: number;\n /**\n * Visibility timeout (ms). If set, a crashed worker's job is automatically\n * reclaimed: a job that stays `active` without a heartbeat for this long is\n * returned to `pending`. While a handler runs, its job is heartbeated so a\n * legitimately long job isn't reaped. Off by default (jobs are never reaped).\n */\n visibilityTimeout?: number;\n}\n\nexport interface QueueOptions {\n /** Default attempts before dead-lettering. Default 1 (no retry). */\n maxAttempts?: number;\n /** Backoff before retry N (ms). Default: exponential, capped at 30s. */\n backoff?: (attempt: number) => number;\n /** Delete jobs once completed instead of keeping them as `done`. Default false. */\n removeOnComplete?: boolean;\n /** Identifies this worker process in the `locked_by` column. */\n workerId?: string;\n}\n\nexport type Handler<T = any, R = any> = (job: Job<T>) => Promise<R> | R;\n\nexport interface Worker {\n /** Stop claiming new jobs and wait for in-flight ones to finish. */\n stop(): Promise<void>;\n}\n\ninterface Row {\n id: number;\n queue: string;\n status: JobStatus;\n priority: number;\n run_at: number;\n attempts: number;\n max_attempts: number;\n payload: string;\n result: string | null;\n error: string | null;\n job_id: string | null;\n created_at: number;\n updated_at: number;\n}\n\nconst ensured = new WeakSet<object>();\nconst now = () => Date.now();\nconst defaultBackoff = (attempt: number) =>\n Math.min(30_000, 1000 * 2 ** (attempt - 1));\n\nfunction deserialize(row: Row): Job {\n return {\n id: row.id,\n queue: row.queue,\n jobId: row.job_id ?? undefined,\n status: row.status,\n priority: row.priority,\n payload: JSON.parse(row.payload),\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n runAt: row.run_at,\n result: row.result != null ? JSON.parse(row.result) : undefined,\n error: row.error ?? undefined,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nclass WorkerImpl implements Worker {\n private running = true;\n private inFlight = 0;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private drainResolve: (() => void) | undefined;\n private readonly concurrency: number;\n private readonly pollInterval: number;\n private readonly visibilityTimeout: number;\n private reaper: ReturnType<typeof setInterval> | undefined;\n\n constructor(\n private readonly q: Queue,\n readonly name: string,\n private readonly handler: Handler,\n opts: ProcessOptions,\n ) {\n this.concurrency = Math.max(1, opts.concurrency ?? 1);\n this.pollInterval = opts.pollInterval ?? 500;\n this.visibilityTimeout = Math.max(0, opts.visibilityTimeout ?? 0);\n if (this.visibilityTimeout > 0) {\n this.reaper = setInterval(() => {\n if (this.running) this.q.recover(this.visibilityTimeout, this.name);\n }, Math.max(1000, Math.floor(this.visibilityTimeout / 2)));\n this.reaper.unref?.();\n }\n this.kick();\n }\n\n /** Fill spare capacity with claimable jobs; otherwise schedule a poll. */\n kick(): void {\n if (!this.running) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = undefined;\n }\n while (this.running && this.inFlight < this.concurrency) {\n const job = this.q.claimInternal(this.name);\n if (!job) break;\n this.inFlight++;\n // Heartbeat the job while it runs so the reaper's visibility timeout won't\n // reclaim a legitimately long-running job.\n let hb: ReturnType<typeof setInterval> | undefined;\n if (this.visibilityTimeout > 0) {\n hb = setInterval(\n () => this.q.heartbeatInternal(job.id),\n Math.max(1000, Math.floor(this.visibilityTimeout / 2)),\n );\n hb.unref?.();\n }\n Promise.resolve()\n .then(() => this.handler(job))\n .then(\n (result) => {\n this.q.completeInternal(job, result);\n this.q.emit(\"completed\", job, result);\n },\n (err) => {\n this.q.failInternal(job, err);\n this.q.emit(\"failed\", job, err);\n },\n )\n .finally(() => {\n if (hb) clearInterval(hb);\n this.inFlight--;\n if (this.running) this.kick();\n else this.checkDrained();\n });\n }\n if (this.running && this.inFlight < this.concurrency) {\n this.timer = setTimeout(() => this.kick(), this.pollInterval);\n this.timer.unref?.();\n }\n }\n\n private checkDrained(): void {\n if (!this.running && this.inFlight === 0 && this.drainResolve) {\n this.drainResolve();\n this.drainResolve = undefined;\n }\n }\n\n async stop(): Promise<void> {\n this.running = false;\n if (this.timer) clearTimeout(this.timer);\n if (this.reaper) clearInterval(this.reaper);\n if (this.inFlight === 0) return;\n await new Promise<void>((resolve) => {\n this.drainResolve = resolve;\n });\n }\n}\n\n/**\n * A durable, multi-process-safe job queue backed by SQLite. Producers `add`\n * jobs; workers `process` them with retries, backoff, delays, and concurrency.\n * Emits `\"completed\"` (job, result) and `\"failed\"` (job, error).\n */\nexport class Queue extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly maxAttempts: number;\n private readonly backoff: (attempt: number) => number;\n private readonly removeOnComplete: boolean;\n readonly workerId: string;\n private readonly workers: WorkerImpl[] = [];\n\n constructor(db: Monlite, opts: QueueOptions = {}) {\n super();\n this.driver = db.driver;\n this.maxAttempts = opts.maxAttempts ?? 1;\n this.backoff = opts.backoff ?? defaultBackoff;\n this.removeOnComplete = opts.removeOnComplete ?? false;\n // `process` is absent in the browser — fall back to a random id there.\n this.workerId =\n opts.workerId ??\n `w-${typeof process !== \"undefined\" && process.pid ? process.pid : Math.floor(Math.random() * 1e6)}`;\n\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _jobs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n queue TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL,\n run_at INTEGER NOT NULL, attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n payload TEXT NOT NULL, result TEXT, error TEXT, locked_by TEXT, job_id TEXT,\n created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL\n )`,\n );\n // Add job_id to pre-existing tables (idempotent).\n try {\n this.driver.exec(`ALTER TABLE _jobs ADD COLUMN job_id TEXT`);\n } catch {\n /* column already exists */\n }\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`,\n );\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_jobid ON _jobs (job_id)`,\n );\n ensured.add(db);\n }\n }\n\n /**\n * Enqueue a job. Pass `opts.jobId` to **dedupe**: if a job with that id is\n * already pending or active, the existing job is returned instead of adding a\n * duplicate (idempotent enqueue — e.g. for resume/replay).\n */\n add<T = any>(name: string, payload: T, opts: AddOptions = {}): Job<T> {\n const t = now();\n if (opts.jobId) {\n const existing = this.driver\n .prepare(\n `SELECT * FROM _jobs WHERE job_id = ? AND status IN ('pending','active') LIMIT 1`,\n )\n .get(opts.jobId) as Row | undefined;\n if (existing) return deserialize(existing) as Job<T>;\n }\n const runAt = opts.runAt ?? (opts.delay ? t + opts.delay : t);\n const info = this.driver\n .prepare(\n `INSERT INTO _jobs (queue, status, priority, run_at, attempts, max_attempts, payload, job_id, created_at, updated_at)\n VALUES (?, 'pending', ?, ?, 0, ?, ?, ?, ?, ?)`,\n )\n .run(\n name,\n opts.priority ?? 0,\n runAt,\n opts.maxAttempts ?? this.maxAttempts,\n JSON.stringify(payload ?? null),\n opts.jobId ?? null,\n t,\n t,\n );\n const job = this.getJob(Number(info.lastInsertRowid))!;\n for (const w of this.workers) if (w.name === name) w.kick();\n return job as Job<T>;\n }\n\n /** Register a worker for a queue. Returns a handle with `stop()`. */\n process<T = any, R = any>(\n name: string,\n handler: Handler<T, R>,\n opts: ProcessOptions = {},\n ): Worker {\n const w = new WorkerImpl(this, name, handler as Handler, opts);\n this.workers.push(w);\n return w;\n }\n\n getJob<T = any>(id: number): Job<T> | undefined {\n const row = this.driver\n .prepare(`SELECT * FROM _jobs WHERE id = ?`)\n .get(id) as Row | undefined;\n return row ? (deserialize(row) as Job<T>) : undefined;\n }\n\n /** Count jobs by status (optionally for one queue). */\n counts(name?: string): Record<JobStatus, number> {\n const rows = (\n name\n ? this.driver\n .prepare(\n `SELECT status, COUNT(*) AS n FROM _jobs WHERE queue = ? GROUP BY status`,\n )\n .all(name)\n : this.driver\n .prepare(`SELECT status, COUNT(*) AS n FROM _jobs GROUP BY status`)\n .all()\n ) as Array<{ status: JobStatus; n: number }>;\n const out: Record<JobStatus, number> = {\n pending: 0,\n active: 0,\n done: 0,\n failed: 0,\n };\n for (const r of rows) out[r.status] = r.n;\n return out;\n }\n\n /**\n * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`\n * if they haven't been touched in `olderThanMs`. Returns the count recovered.\n * Pass `name` to scope it to one queue — the per-worker reaper does this so a\n * fast queue's reaper can't reclaim a slow queue's still-running jobs.\n */\n recover(olderThanMs = 60_000, name?: string): number {\n const filter = name ? \" AND queue = ?\" : \"\";\n const params: any[] = name\n ? [now(), now() - olderThanMs, name]\n : [now(), now() - olderThanMs];\n return this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?\n WHERE status='active' AND updated_at < ?${filter}`,\n )\n .run(...params).changes;\n }\n\n /** @internal Extend a running job's visibility timeout (worker heartbeat). */\n heartbeatInternal(id: number): void {\n this.driver\n .prepare(`UPDATE _jobs SET updated_at=? WHERE id=? AND status='active'`)\n .run(now(), id);\n }\n\n /** Stop all workers and wait for in-flight jobs to finish. */\n async close(): Promise<void> {\n await Promise.all(this.workers.map((w) => w.stop()));\n }\n\n /** @internal Atomically claim the next due job, counting the attempt. */\n claimInternal(name: string): Job | null {\n const t = now();\n const row = this.driver\n .prepare(\n `UPDATE _jobs SET status='active', attempts=attempts+1, locked_by=?, updated_at=?\n WHERE id = (\n SELECT id FROM _jobs\n WHERE queue=? AND status='pending' AND run_at<=?\n ORDER BY priority DESC, id ASC LIMIT 1\n )\n RETURNING *`,\n )\n .get(this.workerId, t, name, t) as Row | undefined;\n return row ? deserialize(row) : null;\n }\n\n /** @internal */\n completeInternal(job: Job, result: unknown): void {\n if (this.removeOnComplete) {\n this.driver.prepare(`DELETE FROM _jobs WHERE id = ?`).run(job.id);\n } else {\n this.driver\n .prepare(\n `UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`,\n )\n .run(JSON.stringify(result ?? null), now(), job.id);\n }\n }\n\n /** @internal */\n failInternal(job: Job, err: unknown): void {\n // `job.attempts` was already incremented at claim time.\n const message = err instanceof Error ? err.message : String(err);\n if (job.attempts < job.maxAttempts) {\n this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`,\n )\n .run(now() + this.backoff(job.attempts), message, now(), job.id);\n } else {\n this.driver\n .prepare(\n `UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`,\n )\n .run(message, now(), job.id);\n }\n }\n}\n\n/** Create a job queue over a monlite database. */\nexport function createQueue(db: Monlite, opts?: QueueOptions): Queue {\n return new Queue(db, opts);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":"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","file":"index.cjs","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite } from \"@monlite/core\";\n\nexport type JobStatus = \"pending\" | \"active\" | \"done\" | \"failed\";\n\nexport interface Job<T = any> {\n id: number;\n queue: string;\n /** Dedupe key, if the job was added with one. */\n jobId?: string;\n status: JobStatus;\n priority: number;\n payload: T;\n /** Number of attempts already made (0 until the first run). */\n attempts: number;\n maxAttempts: number;\n runAt: number;\n result?: any;\n error?: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AddOptions {\n /** Dedupe key — skip if a job with this id is already pending/active. */\n jobId?: string;\n /** Delay before the job becomes runnable (ms). */\n delay?: number;\n /** Explicit epoch-ms run time (overrides `delay`). */\n runAt?: number;\n /** Higher runs first. Default 0. */\n priority?: number;\n /** Total attempts before dead-lettering. Default: the queue's `maxAttempts`. */\n maxAttempts?: number;\n}\n\nexport interface ProcessOptions {\n /** Jobs run concurrently per worker. Default 1. */\n concurrency?: number;\n /** How often to poll for due jobs when idle (ms). Default 500. */\n pollInterval?: number;\n /**\n * Visibility timeout (ms). If set, a crashed worker's job is automatically\n * reclaimed: a job that stays `active` without a heartbeat for this long is\n * returned to `pending`. While a handler runs, its job is heartbeated so a\n * legitimately long job isn't reaped. Off by default (jobs are never reaped).\n */\n visibilityTimeout?: number;\n}\n\nexport interface QueueOptions {\n /** Default attempts before dead-lettering. Default 1 (no retry). */\n maxAttempts?: number;\n /** Backoff before retry N (ms). Default: exponential, capped at 30s. */\n backoff?: (attempt: number) => number;\n /** Delete jobs once completed instead of keeping them as `done`. Default false. */\n removeOnComplete?: boolean;\n /** Identifies this worker process in the `locked_by` column. */\n workerId?: string;\n}\n\nexport type Handler<T = any, R = any> = (job: Job<T>) => Promise<R> | R;\n\nexport interface Worker {\n /** Stop claiming new jobs and wait for in-flight ones to finish. */\n stop(): Promise<void>;\n}\n\ninterface Row {\n id: number;\n queue: string;\n status: JobStatus;\n priority: number;\n run_at: number;\n attempts: number;\n max_attempts: number;\n payload: string;\n result: string | null;\n error: string | null;\n job_id: string | null;\n created_at: number;\n updated_at: number;\n}\n\nconst ensured = new WeakSet<object>();\nconst now = () => Date.now();\nconst defaultBackoff = (attempt: number) =>\n Math.min(30_000, 1000 * 2 ** (attempt - 1));\n\nfunction deserialize(row: Row): Job {\n return {\n id: row.id,\n queue: row.queue,\n jobId: row.job_id ?? undefined,\n status: row.status,\n priority: row.priority,\n payload: JSON.parse(row.payload),\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n runAt: row.run_at,\n result: row.result != null ? JSON.parse(row.result) : undefined,\n error: row.error ?? undefined,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nclass WorkerImpl implements Worker {\n private running = true;\n private inFlight = 0;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private drainResolve: (() => void) | undefined;\n private readonly concurrency: number;\n private readonly pollInterval: number;\n private readonly visibilityTimeout: number;\n private reaper: ReturnType<typeof setInterval> | undefined;\n\n constructor(\n private readonly q: Queue,\n readonly name: string,\n private readonly handler: Handler,\n opts: ProcessOptions,\n ) {\n this.concurrency = Math.max(1, opts.concurrency ?? 1);\n this.pollInterval = opts.pollInterval ?? 500;\n this.visibilityTimeout = Math.max(0, opts.visibilityTimeout ?? 0);\n if (this.visibilityTimeout > 0) {\n this.reaper = setInterval(() => {\n if (this.running) this.q.recover(this.visibilityTimeout, this.name);\n }, Math.max(1000, Math.floor(this.visibilityTimeout / 2)));\n this.reaper.unref?.();\n }\n this.kick();\n }\n\n /** Fill spare capacity with claimable jobs; otherwise schedule a poll. */\n kick(): void {\n if (!this.running) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = undefined;\n }\n while (this.running && this.inFlight < this.concurrency) {\n const job = this.q.claimInternal(this.name);\n if (!job) break;\n this.inFlight++;\n // Heartbeat the job while it runs so the reaper's visibility timeout won't\n // reclaim a legitimately long-running job.\n let hb: ReturnType<typeof setInterval> | undefined;\n if (this.visibilityTimeout > 0) {\n hb = setInterval(\n () => this.q.heartbeatInternal(job.id, job.attempts),\n Math.max(1000, Math.floor(this.visibilityTimeout / 2)),\n );\n hb.unref?.();\n }\n Promise.resolve()\n .then(() => this.handler(job))\n .then(\n (result) => {\n // Only emit if the write landed — a fenced-out (reclaimed) job is now\n // owned by another worker, which will emit its own result.\n if (this.q.completeInternal(job, result))\n this.q.emit(\"completed\", job, result);\n },\n (err) => {\n if (this.q.failInternal(job, err)) this.q.emit(\"failed\", job, err);\n },\n )\n .finally(() => {\n if (hb) clearInterval(hb);\n this.inFlight--;\n if (this.running) this.kick();\n else this.checkDrained();\n });\n }\n if (this.running && this.inFlight < this.concurrency) {\n this.timer = setTimeout(() => this.kick(), this.pollInterval);\n this.timer.unref?.();\n }\n }\n\n private checkDrained(): void {\n if (!this.running && this.inFlight === 0 && this.drainResolve) {\n this.drainResolve();\n this.drainResolve = undefined;\n }\n }\n\n async stop(): Promise<void> {\n this.running = false;\n if (this.timer) clearTimeout(this.timer);\n if (this.reaper) clearInterval(this.reaper);\n if (this.inFlight === 0) return;\n await new Promise<void>((resolve) => {\n this.drainResolve = resolve;\n });\n }\n}\n\n/**\n * A durable, multi-process-safe job queue backed by SQLite. Producers `add`\n * jobs; workers `process` them with retries, backoff, delays, and concurrency.\n * Emits `\"completed\"` (job, result) and `\"failed\"` (job, error).\n */\nexport class Queue extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly maxAttempts: number;\n private readonly backoff: (attempt: number) => number;\n private readonly removeOnComplete: boolean;\n readonly workerId: string;\n private readonly workers: WorkerImpl[] = [];\n\n constructor(db: Monlite, opts: QueueOptions = {}) {\n super();\n this.driver = db.driver;\n this.maxAttempts = opts.maxAttempts ?? 1;\n this.backoff = opts.backoff ?? defaultBackoff;\n this.removeOnComplete = opts.removeOnComplete ?? false;\n // `process` is absent in the browser — fall back to a random id there.\n this.workerId =\n opts.workerId ??\n `w-${typeof process !== \"undefined\" && process.pid ? process.pid : Math.floor(Math.random() * 1e6)}`;\n\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _jobs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n queue TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL,\n run_at INTEGER NOT NULL, attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n payload TEXT NOT NULL, result TEXT, error TEXT, locked_by TEXT, job_id TEXT,\n created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL\n )`,\n );\n // Add job_id to pre-existing tables (idempotent).\n try {\n this.driver.exec(`ALTER TABLE _jobs ADD COLUMN job_id TEXT`);\n } catch {\n /* column already exists */\n }\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`,\n );\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_jobid ON _jobs (job_id)`,\n );\n ensured.add(db);\n }\n }\n\n /**\n * Enqueue a job. Pass `opts.jobId` to **dedupe**: if a job with that id is\n * already pending or active, the existing job is returned instead of adding a\n * duplicate (idempotent enqueue — e.g. for resume/replay).\n */\n add<T = any>(name: string, payload: T, opts: AddOptions = {}): Job<T> {\n const t = now();\n if (opts.jobId) {\n const existing = this.driver\n .prepare(\n `SELECT * FROM _jobs WHERE job_id = ? AND status IN ('pending','active') LIMIT 1`,\n )\n .get(opts.jobId) as Row | undefined;\n if (existing) return deserialize(existing) as Job<T>;\n }\n const runAt = opts.runAt ?? (opts.delay ? t + opts.delay : t);\n const info = this.driver\n .prepare(\n `INSERT INTO _jobs (queue, status, priority, run_at, attempts, max_attempts, payload, job_id, created_at, updated_at)\n VALUES (?, 'pending', ?, ?, 0, ?, ?, ?, ?, ?)`,\n )\n .run(\n name,\n opts.priority ?? 0,\n runAt,\n opts.maxAttempts ?? this.maxAttempts,\n JSON.stringify(payload ?? null),\n opts.jobId ?? null,\n t,\n t,\n );\n const job = this.getJob(Number(info.lastInsertRowid))!;\n for (const w of this.workers) if (w.name === name) w.kick();\n return job as Job<T>;\n }\n\n /** Register a worker for a queue. Returns a handle with `stop()`. */\n process<T = any, R = any>(\n name: string,\n handler: Handler<T, R>,\n opts: ProcessOptions = {},\n ): Worker {\n const w = new WorkerImpl(this, name, handler as Handler, opts);\n this.workers.push(w);\n return w;\n }\n\n getJob<T = any>(id: number): Job<T> | undefined {\n const row = this.driver\n .prepare(`SELECT * FROM _jobs WHERE id = ?`)\n .get(id) as Row | undefined;\n return row ? (deserialize(row) as Job<T>) : undefined;\n }\n\n /** Count jobs by status (optionally for one queue). */\n counts(name?: string): Record<JobStatus, number> {\n const rows = (\n name\n ? this.driver\n .prepare(\n `SELECT status, COUNT(*) AS n FROM _jobs WHERE queue = ? GROUP BY status`,\n )\n .all(name)\n : this.driver\n .prepare(`SELECT status, COUNT(*) AS n FROM _jobs GROUP BY status`)\n .all()\n ) as Array<{ status: JobStatus; n: number }>;\n const out: Record<JobStatus, number> = {\n pending: 0,\n active: 0,\n done: 0,\n failed: 0,\n };\n for (const r of rows) out[r.status] = r.n;\n return out;\n }\n\n /**\n * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`\n * if they haven't been touched in `olderThanMs`. Returns the count recovered.\n * Pass `name` to scope it to one queue — the per-worker reaper does this so a\n * fast queue's reaper can't reclaim a slow queue's still-running jobs.\n */\n recover(olderThanMs = 60_000, name?: string): number {\n const filter = name ? \" AND queue = ?\" : \"\";\n const params: any[] = name\n ? [now(), now() - olderThanMs, name]\n : [now(), now() - olderThanMs];\n return this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?\n WHERE status='active' AND updated_at < ?${filter}`,\n )\n .run(...params).changes;\n }\n\n /** @internal Extend a running job's visibility timeout (worker heartbeat). */\n heartbeatInternal(id: number, attempts: number): void {\n // Fence on the claim-time attempt: don't heartbeat a job that was already\n // reclaimed (attempts bumped) by another worker.\n this.driver\n .prepare(\n `UPDATE _jobs SET updated_at=? WHERE id=? AND status='active' AND attempts=?`,\n )\n .run(now(), id, attempts);\n }\n\n /** Stop all workers and wait for in-flight jobs to finish. */\n async close(): Promise<void> {\n await Promise.all(this.workers.map((w) => w.stop()));\n }\n\n /** @internal Atomically claim the next due job, counting the attempt. */\n claimInternal(name: string): Job | null {\n const t = now();\n const row = this.driver\n .prepare(\n `UPDATE _jobs SET status='active', attempts=attempts+1, locked_by=?, updated_at=?\n WHERE id = (\n SELECT id FROM _jobs\n WHERE queue=? AND status='pending' AND run_at<=?\n ORDER BY priority DESC, id ASC LIMIT 1\n )\n RETURNING *`,\n )\n .get(this.workerId, t, name, t) as Row | undefined;\n return row ? deserialize(row) : null;\n }\n\n /**\n * @internal Mark a job done. Returns false if the job was reclaimed by another\n * worker since this one claimed it (fenced on the claim-time attempt) — the\n * caller then skips emitting \"completed\" so a revived stale worker can't clobber\n * the new run or fire a duplicate event.\n */\n completeInternal(job: Job, result: unknown): boolean {\n if (this.removeOnComplete) {\n return (\n this.driver\n .prepare(`DELETE FROM _jobs WHERE id=? AND attempts=?`)\n .run(job.id, job.attempts).changes > 0\n );\n }\n return (\n this.driver\n .prepare(\n `UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=? AND attempts=?`,\n )\n .run(JSON.stringify(result ?? null), now(), job.id, job.attempts).changes >\n 0\n );\n }\n\n /** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */\n failInternal(job: Job, err: unknown): boolean {\n // `job.attempts` was already incremented at claim time; it also fences this\n // write against a job another worker has since reclaimed.\n const message = err instanceof Error ? err.message : String(err);\n if (job.attempts < job.maxAttempts) {\n return (\n this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=? AND attempts=?`,\n )\n .run(now() + this.backoff(job.attempts), message, now(), job.id, job.attempts)\n .changes > 0\n );\n }\n return (\n this.driver\n .prepare(\n `UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=? AND attempts=?`,\n )\n .run(message, now(), job.id, job.attempts).changes > 0\n );\n }\n}\n\n/** Create a job queue over a monlite database. */\nexport function createQueue(db: Monlite, opts?: QueueOptions): Queue {\n return new Queue(db, opts);\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -91,15 +91,20 @@ declare class Queue extends EventEmitter {
|
|
|
91
91
|
*/
|
|
92
92
|
recover(olderThanMs?: number, name?: string): number;
|
|
93
93
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
94
|
-
heartbeatInternal(id: number): void;
|
|
94
|
+
heartbeatInternal(id: number, attempts: number): void;
|
|
95
95
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
96
96
|
close(): Promise<void>;
|
|
97
97
|
/** @internal Atomically claim the next due job, counting the attempt. */
|
|
98
98
|
claimInternal(name: string): Job | null;
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
/**
|
|
100
|
+
* @internal Mark a job done. Returns false if the job was reclaimed by another
|
|
101
|
+
* worker since this one claimed it (fenced on the claim-time attempt) — the
|
|
102
|
+
* caller then skips emitting "completed" so a revived stale worker can't clobber
|
|
103
|
+
* the new run or fire a duplicate event.
|
|
104
|
+
*/
|
|
105
|
+
completeInternal(job: Job, result: unknown): boolean;
|
|
106
|
+
/** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */
|
|
107
|
+
failInternal(job: Job, err: unknown): boolean;
|
|
103
108
|
}
|
|
104
109
|
/** Create a job queue over a monlite database. */
|
|
105
110
|
declare function createQueue(db: Monlite, opts?: QueueOptions): Queue;
|
package/dist/index.d.ts
CHANGED
|
@@ -91,15 +91,20 @@ declare class Queue extends EventEmitter {
|
|
|
91
91
|
*/
|
|
92
92
|
recover(olderThanMs?: number, name?: string): number;
|
|
93
93
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
94
|
-
heartbeatInternal(id: number): void;
|
|
94
|
+
heartbeatInternal(id: number, attempts: number): void;
|
|
95
95
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
96
96
|
close(): Promise<void>;
|
|
97
97
|
/** @internal Atomically claim the next due job, counting the attempt. */
|
|
98
98
|
claimInternal(name: string): Job | null;
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
/**
|
|
100
|
+
* @internal Mark a job done. Returns false if the job was reclaimed by another
|
|
101
|
+
* worker since this one claimed it (fenced on the claim-time attempt) — the
|
|
102
|
+
* caller then skips emitting "completed" so a revived stale worker can't clobber
|
|
103
|
+
* the new run or fire a duplicate event.
|
|
104
|
+
*/
|
|
105
|
+
completeInternal(job: Job, result: unknown): boolean;
|
|
106
|
+
/** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */
|
|
107
|
+
failInternal(job: Job, err: unknown): boolean;
|
|
103
108
|
}
|
|
104
109
|
/** Create a job queue over a monlite database. */
|
|
105
110
|
declare function createQueue(db: Monlite, opts?: QueueOptions): Queue;
|
package/dist/index.js
CHANGED
|
@@ -62,19 +62,18 @@ var WorkerImpl = class {
|
|
|
62
62
|
let hb;
|
|
63
63
|
if (this.visibilityTimeout > 0) {
|
|
64
64
|
hb = setInterval(
|
|
65
|
-
() => this.q.heartbeatInternal(job.id),
|
|
65
|
+
() => this.q.heartbeatInternal(job.id, job.attempts),
|
|
66
66
|
Math.max(1e3, Math.floor(this.visibilityTimeout / 2))
|
|
67
67
|
);
|
|
68
68
|
hb.unref?.();
|
|
69
69
|
}
|
|
70
70
|
Promise.resolve().then(() => this.handler(job)).then(
|
|
71
71
|
(result) => {
|
|
72
|
-
this.q.completeInternal(job, result)
|
|
73
|
-
|
|
72
|
+
if (this.q.completeInternal(job, result))
|
|
73
|
+
this.q.emit("completed", job, result);
|
|
74
74
|
},
|
|
75
75
|
(err) => {
|
|
76
|
-
this.q.failInternal(job, err);
|
|
77
|
-
this.q.emit("failed", job, err);
|
|
76
|
+
if (this.q.failInternal(job, err)) this.q.emit("failed", job, err);
|
|
78
77
|
}
|
|
79
78
|
).finally(() => {
|
|
80
79
|
if (hb) clearInterval(hb);
|
|
@@ -211,8 +210,10 @@ var Queue = class extends EventEmitter {
|
|
|
211
210
|
).run(...params).changes;
|
|
212
211
|
}
|
|
213
212
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
214
|
-
heartbeatInternal(id) {
|
|
215
|
-
this.driver.prepare(
|
|
213
|
+
heartbeatInternal(id, attempts) {
|
|
214
|
+
this.driver.prepare(
|
|
215
|
+
`UPDATE _jobs SET updated_at=? WHERE id=? AND status='active' AND attempts=?`
|
|
216
|
+
).run(now(), id, attempts);
|
|
216
217
|
}
|
|
217
218
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
218
219
|
async close() {
|
|
@@ -232,28 +233,31 @@ var Queue = class extends EventEmitter {
|
|
|
232
233
|
).get(this.workerId, t, name, t);
|
|
233
234
|
return row ? deserialize(row) : null;
|
|
234
235
|
}
|
|
235
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* @internal Mark a job done. Returns false if the job was reclaimed by another
|
|
238
|
+
* worker since this one claimed it (fenced on the claim-time attempt) — the
|
|
239
|
+
* caller then skips emitting "completed" so a revived stale worker can't clobber
|
|
240
|
+
* the new run or fire a duplicate event.
|
|
241
|
+
*/
|
|
236
242
|
completeInternal(job, result) {
|
|
237
243
|
if (this.removeOnComplete) {
|
|
238
|
-
this.driver.prepare(`DELETE FROM _jobs WHERE id
|
|
239
|
-
} else {
|
|
240
|
-
this.driver.prepare(
|
|
241
|
-
`UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`
|
|
242
|
-
).run(JSON.stringify(result ?? null), now(), job.id);
|
|
244
|
+
return this.driver.prepare(`DELETE FROM _jobs WHERE id=? AND attempts=?`).run(job.id, job.attempts).changes > 0;
|
|
243
245
|
}
|
|
246
|
+
return this.driver.prepare(
|
|
247
|
+
`UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=? AND attempts=?`
|
|
248
|
+
).run(JSON.stringify(result ?? null), now(), job.id, job.attempts).changes > 0;
|
|
244
249
|
}
|
|
245
|
-
/** @internal */
|
|
250
|
+
/** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */
|
|
246
251
|
failInternal(job, err) {
|
|
247
252
|
const message = err instanceof Error ? err.message : String(err);
|
|
248
253
|
if (job.attempts < job.maxAttempts) {
|
|
249
|
-
this.driver.prepare(
|
|
250
|
-
`UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`
|
|
251
|
-
).run(now() + this.backoff(job.attempts), message, now(), job.id);
|
|
252
|
-
} else {
|
|
253
|
-
this.driver.prepare(
|
|
254
|
-
`UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`
|
|
255
|
-
).run(message, now(), job.id);
|
|
254
|
+
return this.driver.prepare(
|
|
255
|
+
`UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=? AND attempts=?`
|
|
256
|
+
).run(now() + this.backoff(job.attempts), message, now(), job.id, job.attempts).changes > 0;
|
|
256
257
|
}
|
|
258
|
+
return this.driver.prepare(
|
|
259
|
+
`UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=? AND attempts=?`
|
|
260
|
+
).run(message, now(), job.id, job.attempts).changes > 0;
|
|
257
261
|
}
|
|
258
262
|
};
|
|
259
263
|
function createQueue(db, opts) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite } from \"@monlite/core\";\n\nexport type JobStatus = \"pending\" | \"active\" | \"done\" | \"failed\";\n\nexport interface Job<T = any> {\n id: number;\n queue: string;\n /** Dedupe key, if the job was added with one. */\n jobId?: string;\n status: JobStatus;\n priority: number;\n payload: T;\n /** Number of attempts already made (0 until the first run). */\n attempts: number;\n maxAttempts: number;\n runAt: number;\n result?: any;\n error?: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AddOptions {\n /** Dedupe key — skip if a job with this id is already pending/active. */\n jobId?: string;\n /** Delay before the job becomes runnable (ms). */\n delay?: number;\n /** Explicit epoch-ms run time (overrides `delay`). */\n runAt?: number;\n /** Higher runs first. Default 0. */\n priority?: number;\n /** Total attempts before dead-lettering. Default: the queue's `maxAttempts`. */\n maxAttempts?: number;\n}\n\nexport interface ProcessOptions {\n /** Jobs run concurrently per worker. Default 1. */\n concurrency?: number;\n /** How often to poll for due jobs when idle (ms). Default 500. */\n pollInterval?: number;\n /**\n * Visibility timeout (ms). If set, a crashed worker's job is automatically\n * reclaimed: a job that stays `active` without a heartbeat for this long is\n * returned to `pending`. While a handler runs, its job is heartbeated so a\n * legitimately long job isn't reaped. Off by default (jobs are never reaped).\n */\n visibilityTimeout?: number;\n}\n\nexport interface QueueOptions {\n /** Default attempts before dead-lettering. Default 1 (no retry). */\n maxAttempts?: number;\n /** Backoff before retry N (ms). Default: exponential, capped at 30s. */\n backoff?: (attempt: number) => number;\n /** Delete jobs once completed instead of keeping them as `done`. Default false. */\n removeOnComplete?: boolean;\n /** Identifies this worker process in the `locked_by` column. */\n workerId?: string;\n}\n\nexport type Handler<T = any, R = any> = (job: Job<T>) => Promise<R> | R;\n\nexport interface Worker {\n /** Stop claiming new jobs and wait for in-flight ones to finish. */\n stop(): Promise<void>;\n}\n\ninterface Row {\n id: number;\n queue: string;\n status: JobStatus;\n priority: number;\n run_at: number;\n attempts: number;\n max_attempts: number;\n payload: string;\n result: string | null;\n error: string | null;\n job_id: string | null;\n created_at: number;\n updated_at: number;\n}\n\nconst ensured = new WeakSet<object>();\nconst now = () => Date.now();\nconst defaultBackoff = (attempt: number) =>\n Math.min(30_000, 1000 * 2 ** (attempt - 1));\n\nfunction deserialize(row: Row): Job {\n return {\n id: row.id,\n queue: row.queue,\n jobId: row.job_id ?? undefined,\n status: row.status,\n priority: row.priority,\n payload: JSON.parse(row.payload),\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n runAt: row.run_at,\n result: row.result != null ? JSON.parse(row.result) : undefined,\n error: row.error ?? undefined,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nclass WorkerImpl implements Worker {\n private running = true;\n private inFlight = 0;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private drainResolve: (() => void) | undefined;\n private readonly concurrency: number;\n private readonly pollInterval: number;\n private readonly visibilityTimeout: number;\n private reaper: ReturnType<typeof setInterval> | undefined;\n\n constructor(\n private readonly q: Queue,\n readonly name: string,\n private readonly handler: Handler,\n opts: ProcessOptions,\n ) {\n this.concurrency = Math.max(1, opts.concurrency ?? 1);\n this.pollInterval = opts.pollInterval ?? 500;\n this.visibilityTimeout = Math.max(0, opts.visibilityTimeout ?? 0);\n if (this.visibilityTimeout > 0) {\n this.reaper = setInterval(() => {\n if (this.running) this.q.recover(this.visibilityTimeout, this.name);\n }, Math.max(1000, Math.floor(this.visibilityTimeout / 2)));\n this.reaper.unref?.();\n }\n this.kick();\n }\n\n /** Fill spare capacity with claimable jobs; otherwise schedule a poll. */\n kick(): void {\n if (!this.running) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = undefined;\n }\n while (this.running && this.inFlight < this.concurrency) {\n const job = this.q.claimInternal(this.name);\n if (!job) break;\n this.inFlight++;\n // Heartbeat the job while it runs so the reaper's visibility timeout won't\n // reclaim a legitimately long-running job.\n let hb: ReturnType<typeof setInterval> | undefined;\n if (this.visibilityTimeout > 0) {\n hb = setInterval(\n () => this.q.heartbeatInternal(job.id),\n Math.max(1000, Math.floor(this.visibilityTimeout / 2)),\n );\n hb.unref?.();\n }\n Promise.resolve()\n .then(() => this.handler(job))\n .then(\n (result) => {\n this.q.completeInternal(job, result);\n this.q.emit(\"completed\", job, result);\n },\n (err) => {\n this.q.failInternal(job, err);\n this.q.emit(\"failed\", job, err);\n },\n )\n .finally(() => {\n if (hb) clearInterval(hb);\n this.inFlight--;\n if (this.running) this.kick();\n else this.checkDrained();\n });\n }\n if (this.running && this.inFlight < this.concurrency) {\n this.timer = setTimeout(() => this.kick(), this.pollInterval);\n this.timer.unref?.();\n }\n }\n\n private checkDrained(): void {\n if (!this.running && this.inFlight === 0 && this.drainResolve) {\n this.drainResolve();\n this.drainResolve = undefined;\n }\n }\n\n async stop(): Promise<void> {\n this.running = false;\n if (this.timer) clearTimeout(this.timer);\n if (this.reaper) clearInterval(this.reaper);\n if (this.inFlight === 0) return;\n await new Promise<void>((resolve) => {\n this.drainResolve = resolve;\n });\n }\n}\n\n/**\n * A durable, multi-process-safe job queue backed by SQLite. Producers `add`\n * jobs; workers `process` them with retries, backoff, delays, and concurrency.\n * Emits `\"completed\"` (job, result) and `\"failed\"` (job, error).\n */\nexport class Queue extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly maxAttempts: number;\n private readonly backoff: (attempt: number) => number;\n private readonly removeOnComplete: boolean;\n readonly workerId: string;\n private readonly workers: WorkerImpl[] = [];\n\n constructor(db: Monlite, opts: QueueOptions = {}) {\n super();\n this.driver = db.driver;\n this.maxAttempts = opts.maxAttempts ?? 1;\n this.backoff = opts.backoff ?? defaultBackoff;\n this.removeOnComplete = opts.removeOnComplete ?? false;\n // `process` is absent in the browser — fall back to a random id there.\n this.workerId =\n opts.workerId ??\n `w-${typeof process !== \"undefined\" && process.pid ? process.pid : Math.floor(Math.random() * 1e6)}`;\n\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _jobs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n queue TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL,\n run_at INTEGER NOT NULL, attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n payload TEXT NOT NULL, result TEXT, error TEXT, locked_by TEXT, job_id TEXT,\n created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL\n )`,\n );\n // Add job_id to pre-existing tables (idempotent).\n try {\n this.driver.exec(`ALTER TABLE _jobs ADD COLUMN job_id TEXT`);\n } catch {\n /* column already exists */\n }\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`,\n );\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_jobid ON _jobs (job_id)`,\n );\n ensured.add(db);\n }\n }\n\n /**\n * Enqueue a job. Pass `opts.jobId` to **dedupe**: if a job with that id is\n * already pending or active, the existing job is returned instead of adding a\n * duplicate (idempotent enqueue — e.g. for resume/replay).\n */\n add<T = any>(name: string, payload: T, opts: AddOptions = {}): Job<T> {\n const t = now();\n if (opts.jobId) {\n const existing = this.driver\n .prepare(\n `SELECT * FROM _jobs WHERE job_id = ? AND status IN ('pending','active') LIMIT 1`,\n )\n .get(opts.jobId) as Row | undefined;\n if (existing) return deserialize(existing) as Job<T>;\n }\n const runAt = opts.runAt ?? (opts.delay ? t + opts.delay : t);\n const info = this.driver\n .prepare(\n `INSERT INTO _jobs (queue, status, priority, run_at, attempts, max_attempts, payload, job_id, created_at, updated_at)\n VALUES (?, 'pending', ?, ?, 0, ?, ?, ?, ?, ?)`,\n )\n .run(\n name,\n opts.priority ?? 0,\n runAt,\n opts.maxAttempts ?? this.maxAttempts,\n JSON.stringify(payload ?? null),\n opts.jobId ?? null,\n t,\n t,\n );\n const job = this.getJob(Number(info.lastInsertRowid))!;\n for (const w of this.workers) if (w.name === name) w.kick();\n return job as Job<T>;\n }\n\n /** Register a worker for a queue. Returns a handle with `stop()`. */\n process<T = any, R = any>(\n name: string,\n handler: Handler<T, R>,\n opts: ProcessOptions = {},\n ): Worker {\n const w = new WorkerImpl(this, name, handler as Handler, opts);\n this.workers.push(w);\n return w;\n }\n\n getJob<T = any>(id: number): Job<T> | undefined {\n const row = this.driver\n .prepare(`SELECT * FROM _jobs WHERE id = ?`)\n .get(id) as Row | undefined;\n return row ? (deserialize(row) as Job<T>) : undefined;\n }\n\n /** Count jobs by status (optionally for one queue). */\n counts(name?: string): Record<JobStatus, number> {\n const rows = (\n name\n ? this.driver\n .prepare(\n `SELECT status, COUNT(*) AS n FROM _jobs WHERE queue = ? GROUP BY status`,\n )\n .all(name)\n : this.driver\n .prepare(`SELECT status, COUNT(*) AS n FROM _jobs GROUP BY status`)\n .all()\n ) as Array<{ status: JobStatus; n: number }>;\n const out: Record<JobStatus, number> = {\n pending: 0,\n active: 0,\n done: 0,\n failed: 0,\n };\n for (const r of rows) out[r.status] = r.n;\n return out;\n }\n\n /**\n * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`\n * if they haven't been touched in `olderThanMs`. Returns the count recovered.\n * Pass `name` to scope it to one queue — the per-worker reaper does this so a\n * fast queue's reaper can't reclaim a slow queue's still-running jobs.\n */\n recover(olderThanMs = 60_000, name?: string): number {\n const filter = name ? \" AND queue = ?\" : \"\";\n const params: any[] = name\n ? [now(), now() - olderThanMs, name]\n : [now(), now() - olderThanMs];\n return this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?\n WHERE status='active' AND updated_at < ?${filter}`,\n )\n .run(...params).changes;\n }\n\n /** @internal Extend a running job's visibility timeout (worker heartbeat). */\n heartbeatInternal(id: number): void {\n this.driver\n .prepare(`UPDATE _jobs SET updated_at=? WHERE id=? AND status='active'`)\n .run(now(), id);\n }\n\n /** Stop all workers and wait for in-flight jobs to finish. */\n async close(): Promise<void> {\n await Promise.all(this.workers.map((w) => w.stop()));\n }\n\n /** @internal Atomically claim the next due job, counting the attempt. */\n claimInternal(name: string): Job | null {\n const t = now();\n const row = this.driver\n .prepare(\n `UPDATE _jobs SET status='active', attempts=attempts+1, locked_by=?, updated_at=?\n WHERE id = (\n SELECT id FROM _jobs\n WHERE queue=? AND status='pending' AND run_at<=?\n ORDER BY priority DESC, id ASC LIMIT 1\n )\n RETURNING *`,\n )\n .get(this.workerId, t, name, t) as Row | undefined;\n return row ? deserialize(row) : null;\n }\n\n /** @internal */\n completeInternal(job: Job, result: unknown): void {\n if (this.removeOnComplete) {\n this.driver.prepare(`DELETE FROM _jobs WHERE id = ?`).run(job.id);\n } else {\n this.driver\n .prepare(\n `UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`,\n )\n .run(JSON.stringify(result ?? null), now(), job.id);\n }\n }\n\n /** @internal */\n failInternal(job: Job, err: unknown): void {\n // `job.attempts` was already incremented at claim time.\n const message = err instanceof Error ? err.message : String(err);\n if (job.attempts < job.maxAttempts) {\n this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`,\n )\n .run(now() + this.backoff(job.attempts), message, now(), job.id);\n } else {\n this.driver\n .prepare(\n `UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`,\n )\n .run(message, now(), job.id);\n }\n }\n}\n\n/** Create a job queue over a monlite database. */\nexport function createQueue(db: Monlite, opts?: QueueOptions): Queue {\n return new Queue(db, opts);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Monlite } from \"@monlite/core\";\n\nexport type JobStatus = \"pending\" | \"active\" | \"done\" | \"failed\";\n\nexport interface Job<T = any> {\n id: number;\n queue: string;\n /** Dedupe key, if the job was added with one. */\n jobId?: string;\n status: JobStatus;\n priority: number;\n payload: T;\n /** Number of attempts already made (0 until the first run). */\n attempts: number;\n maxAttempts: number;\n runAt: number;\n result?: any;\n error?: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AddOptions {\n /** Dedupe key — skip if a job with this id is already pending/active. */\n jobId?: string;\n /** Delay before the job becomes runnable (ms). */\n delay?: number;\n /** Explicit epoch-ms run time (overrides `delay`). */\n runAt?: number;\n /** Higher runs first. Default 0. */\n priority?: number;\n /** Total attempts before dead-lettering. Default: the queue's `maxAttempts`. */\n maxAttempts?: number;\n}\n\nexport interface ProcessOptions {\n /** Jobs run concurrently per worker. Default 1. */\n concurrency?: number;\n /** How often to poll for due jobs when idle (ms). Default 500. */\n pollInterval?: number;\n /**\n * Visibility timeout (ms). If set, a crashed worker's job is automatically\n * reclaimed: a job that stays `active` without a heartbeat for this long is\n * returned to `pending`. While a handler runs, its job is heartbeated so a\n * legitimately long job isn't reaped. Off by default (jobs are never reaped).\n */\n visibilityTimeout?: number;\n}\n\nexport interface QueueOptions {\n /** Default attempts before dead-lettering. Default 1 (no retry). */\n maxAttempts?: number;\n /** Backoff before retry N (ms). Default: exponential, capped at 30s. */\n backoff?: (attempt: number) => number;\n /** Delete jobs once completed instead of keeping them as `done`. Default false. */\n removeOnComplete?: boolean;\n /** Identifies this worker process in the `locked_by` column. */\n workerId?: string;\n}\n\nexport type Handler<T = any, R = any> = (job: Job<T>) => Promise<R> | R;\n\nexport interface Worker {\n /** Stop claiming new jobs and wait for in-flight ones to finish. */\n stop(): Promise<void>;\n}\n\ninterface Row {\n id: number;\n queue: string;\n status: JobStatus;\n priority: number;\n run_at: number;\n attempts: number;\n max_attempts: number;\n payload: string;\n result: string | null;\n error: string | null;\n job_id: string | null;\n created_at: number;\n updated_at: number;\n}\n\nconst ensured = new WeakSet<object>();\nconst now = () => Date.now();\nconst defaultBackoff = (attempt: number) =>\n Math.min(30_000, 1000 * 2 ** (attempt - 1));\n\nfunction deserialize(row: Row): Job {\n return {\n id: row.id,\n queue: row.queue,\n jobId: row.job_id ?? undefined,\n status: row.status,\n priority: row.priority,\n payload: JSON.parse(row.payload),\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n runAt: row.run_at,\n result: row.result != null ? JSON.parse(row.result) : undefined,\n error: row.error ?? undefined,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nclass WorkerImpl implements Worker {\n private running = true;\n private inFlight = 0;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private drainResolve: (() => void) | undefined;\n private readonly concurrency: number;\n private readonly pollInterval: number;\n private readonly visibilityTimeout: number;\n private reaper: ReturnType<typeof setInterval> | undefined;\n\n constructor(\n private readonly q: Queue,\n readonly name: string,\n private readonly handler: Handler,\n opts: ProcessOptions,\n ) {\n this.concurrency = Math.max(1, opts.concurrency ?? 1);\n this.pollInterval = opts.pollInterval ?? 500;\n this.visibilityTimeout = Math.max(0, opts.visibilityTimeout ?? 0);\n if (this.visibilityTimeout > 0) {\n this.reaper = setInterval(() => {\n if (this.running) this.q.recover(this.visibilityTimeout, this.name);\n }, Math.max(1000, Math.floor(this.visibilityTimeout / 2)));\n this.reaper.unref?.();\n }\n this.kick();\n }\n\n /** Fill spare capacity with claimable jobs; otherwise schedule a poll. */\n kick(): void {\n if (!this.running) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = undefined;\n }\n while (this.running && this.inFlight < this.concurrency) {\n const job = this.q.claimInternal(this.name);\n if (!job) break;\n this.inFlight++;\n // Heartbeat the job while it runs so the reaper's visibility timeout won't\n // reclaim a legitimately long-running job.\n let hb: ReturnType<typeof setInterval> | undefined;\n if (this.visibilityTimeout > 0) {\n hb = setInterval(\n () => this.q.heartbeatInternal(job.id, job.attempts),\n Math.max(1000, Math.floor(this.visibilityTimeout / 2)),\n );\n hb.unref?.();\n }\n Promise.resolve()\n .then(() => this.handler(job))\n .then(\n (result) => {\n // Only emit if the write landed — a fenced-out (reclaimed) job is now\n // owned by another worker, which will emit its own result.\n if (this.q.completeInternal(job, result))\n this.q.emit(\"completed\", job, result);\n },\n (err) => {\n if (this.q.failInternal(job, err)) this.q.emit(\"failed\", job, err);\n },\n )\n .finally(() => {\n if (hb) clearInterval(hb);\n this.inFlight--;\n if (this.running) this.kick();\n else this.checkDrained();\n });\n }\n if (this.running && this.inFlight < this.concurrency) {\n this.timer = setTimeout(() => this.kick(), this.pollInterval);\n this.timer.unref?.();\n }\n }\n\n private checkDrained(): void {\n if (!this.running && this.inFlight === 0 && this.drainResolve) {\n this.drainResolve();\n this.drainResolve = undefined;\n }\n }\n\n async stop(): Promise<void> {\n this.running = false;\n if (this.timer) clearTimeout(this.timer);\n if (this.reaper) clearInterval(this.reaper);\n if (this.inFlight === 0) return;\n await new Promise<void>((resolve) => {\n this.drainResolve = resolve;\n });\n }\n}\n\n/**\n * A durable, multi-process-safe job queue backed by SQLite. Producers `add`\n * jobs; workers `process` them with retries, backoff, delays, and concurrency.\n * Emits `\"completed\"` (job, result) and `\"failed\"` (job, error).\n */\nexport class Queue extends EventEmitter {\n private readonly driver: Monlite[\"driver\"];\n private readonly maxAttempts: number;\n private readonly backoff: (attempt: number) => number;\n private readonly removeOnComplete: boolean;\n readonly workerId: string;\n private readonly workers: WorkerImpl[] = [];\n\n constructor(db: Monlite, opts: QueueOptions = {}) {\n super();\n this.driver = db.driver;\n this.maxAttempts = opts.maxAttempts ?? 1;\n this.backoff = opts.backoff ?? defaultBackoff;\n this.removeOnComplete = opts.removeOnComplete ?? false;\n // `process` is absent in the browser — fall back to a random id there.\n this.workerId =\n opts.workerId ??\n `w-${typeof process !== \"undefined\" && process.pid ? process.pid : Math.floor(Math.random() * 1e6)}`;\n\n if (!ensured.has(db)) {\n this.driver.exec(\n `CREATE TABLE IF NOT EXISTS _jobs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n queue TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL,\n run_at INTEGER NOT NULL, attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n payload TEXT NOT NULL, result TEXT, error TEXT, locked_by TEXT, job_id TEXT,\n created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL\n )`,\n );\n // Add job_id to pre-existing tables (idempotent).\n try {\n this.driver.exec(`ALTER TABLE _jobs ADD COLUMN job_id TEXT`);\n } catch {\n /* column already exists */\n }\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`,\n );\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_jobid ON _jobs (job_id)`,\n );\n ensured.add(db);\n }\n }\n\n /**\n * Enqueue a job. Pass `opts.jobId` to **dedupe**: if a job with that id is\n * already pending or active, the existing job is returned instead of adding a\n * duplicate (idempotent enqueue — e.g. for resume/replay).\n */\n add<T = any>(name: string, payload: T, opts: AddOptions = {}): Job<T> {\n const t = now();\n if (opts.jobId) {\n const existing = this.driver\n .prepare(\n `SELECT * FROM _jobs WHERE job_id = ? AND status IN ('pending','active') LIMIT 1`,\n )\n .get(opts.jobId) as Row | undefined;\n if (existing) return deserialize(existing) as Job<T>;\n }\n const runAt = opts.runAt ?? (opts.delay ? t + opts.delay : t);\n const info = this.driver\n .prepare(\n `INSERT INTO _jobs (queue, status, priority, run_at, attempts, max_attempts, payload, job_id, created_at, updated_at)\n VALUES (?, 'pending', ?, ?, 0, ?, ?, ?, ?, ?)`,\n )\n .run(\n name,\n opts.priority ?? 0,\n runAt,\n opts.maxAttempts ?? this.maxAttempts,\n JSON.stringify(payload ?? null),\n opts.jobId ?? null,\n t,\n t,\n );\n const job = this.getJob(Number(info.lastInsertRowid))!;\n for (const w of this.workers) if (w.name === name) w.kick();\n return job as Job<T>;\n }\n\n /** Register a worker for a queue. Returns a handle with `stop()`. */\n process<T = any, R = any>(\n name: string,\n handler: Handler<T, R>,\n opts: ProcessOptions = {},\n ): Worker {\n const w = new WorkerImpl(this, name, handler as Handler, opts);\n this.workers.push(w);\n return w;\n }\n\n getJob<T = any>(id: number): Job<T> | undefined {\n const row = this.driver\n .prepare(`SELECT * FROM _jobs WHERE id = ?`)\n .get(id) as Row | undefined;\n return row ? (deserialize(row) as Job<T>) : undefined;\n }\n\n /** Count jobs by status (optionally for one queue). */\n counts(name?: string): Record<JobStatus, number> {\n const rows = (\n name\n ? this.driver\n .prepare(\n `SELECT status, COUNT(*) AS n FROM _jobs WHERE queue = ? GROUP BY status`,\n )\n .all(name)\n : this.driver\n .prepare(`SELECT status, COUNT(*) AS n FROM _jobs GROUP BY status`)\n .all()\n ) as Array<{ status: JobStatus; n: number }>;\n const out: Record<JobStatus, number> = {\n pending: 0,\n active: 0,\n done: 0,\n failed: 0,\n };\n for (const r of rows) out[r.status] = r.n;\n return out;\n }\n\n /**\n * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`\n * if they haven't been touched in `olderThanMs`. Returns the count recovered.\n * Pass `name` to scope it to one queue — the per-worker reaper does this so a\n * fast queue's reaper can't reclaim a slow queue's still-running jobs.\n */\n recover(olderThanMs = 60_000, name?: string): number {\n const filter = name ? \" AND queue = ?\" : \"\";\n const params: any[] = name\n ? [now(), now() - olderThanMs, name]\n : [now(), now() - olderThanMs];\n return this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?\n WHERE status='active' AND updated_at < ?${filter}`,\n )\n .run(...params).changes;\n }\n\n /** @internal Extend a running job's visibility timeout (worker heartbeat). */\n heartbeatInternal(id: number, attempts: number): void {\n // Fence on the claim-time attempt: don't heartbeat a job that was already\n // reclaimed (attempts bumped) by another worker.\n this.driver\n .prepare(\n `UPDATE _jobs SET updated_at=? WHERE id=? AND status='active' AND attempts=?`,\n )\n .run(now(), id, attempts);\n }\n\n /** Stop all workers and wait for in-flight jobs to finish. */\n async close(): Promise<void> {\n await Promise.all(this.workers.map((w) => w.stop()));\n }\n\n /** @internal Atomically claim the next due job, counting the attempt. */\n claimInternal(name: string): Job | null {\n const t = now();\n const row = this.driver\n .prepare(\n `UPDATE _jobs SET status='active', attempts=attempts+1, locked_by=?, updated_at=?\n WHERE id = (\n SELECT id FROM _jobs\n WHERE queue=? AND status='pending' AND run_at<=?\n ORDER BY priority DESC, id ASC LIMIT 1\n )\n RETURNING *`,\n )\n .get(this.workerId, t, name, t) as Row | undefined;\n return row ? deserialize(row) : null;\n }\n\n /**\n * @internal Mark a job done. Returns false if the job was reclaimed by another\n * worker since this one claimed it (fenced on the claim-time attempt) — the\n * caller then skips emitting \"completed\" so a revived stale worker can't clobber\n * the new run or fire a duplicate event.\n */\n completeInternal(job: Job, result: unknown): boolean {\n if (this.removeOnComplete) {\n return (\n this.driver\n .prepare(`DELETE FROM _jobs WHERE id=? AND attempts=?`)\n .run(job.id, job.attempts).changes > 0\n );\n }\n return (\n this.driver\n .prepare(\n `UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=? AND attempts=?`,\n )\n .run(JSON.stringify(result ?? null), now(), job.id, job.attempts).changes >\n 0\n );\n }\n\n /** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */\n failInternal(job: Job, err: unknown): boolean {\n // `job.attempts` was already incremented at claim time; it also fences this\n // write against a job another worker has since reclaimed.\n const message = err instanceof Error ? err.message : String(err);\n if (job.attempts < job.maxAttempts) {\n return (\n this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=? AND attempts=?`,\n )\n .run(now() + this.backoff(job.attempts), message, now(), job.id, job.attempts)\n .changes > 0\n );\n }\n return (\n this.driver\n .prepare(\n `UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=? AND attempts=?`,\n )\n .run(message, now(), job.id, job.attempts).changes > 0\n );\n }\n}\n\n/** Create a job queue over a monlite database. */\nexport function createQueue(db: Monlite, opts?: QueueOptions): Queue {\n return new Queue(db, opts);\n}\n"]}
|