@monlite/queue 0.3.1 → 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/README.md +4 -1
- package/dist/index.cjs +33 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -6
- package/dist/index.d.ts +13 -6
- package/dist/index.js +33 -25
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -36,7 +36,10 @@ queue.add("digest", { day: "monday" }, { delay: 60_000, priority: 10 });
|
|
|
36
36
|
to requeue any jobs that were `active` when the process died.
|
|
37
37
|
|
|
38
38
|
**Multi-process safe.** Workers claim jobs with a single `UPDATE … RETURNING`. Run the same
|
|
39
|
-
queue in multiple processes against the same `.db`; each job is claimed exactly
|
|
39
|
+
queue in multiple processes against the same `.db`; each pending job is claimed by exactly
|
|
40
|
+
one worker. **Execution is at-least-once**, though: once a stuck job is recovered (via
|
|
41
|
+
`recover()` or `visibilityTimeout`), a crashed/slow worker's job can run again — so make
|
|
42
|
+
handlers **idempotent** (e.g. key external side-effects on `job.id`).
|
|
40
43
|
|
|
41
44
|
**Retries with backoff.** Failed jobs retry up to `maxAttempts`; backoff is exponential by
|
|
42
45
|
default (capped at 30s). Exhausted jobs become `status: "failed"` and are kept for inspection.
|
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,7 @@ var WorkerImpl = class {
|
|
|
33
33
|
this.visibilityTimeout = Math.max(0, opts.visibilityTimeout ?? 0);
|
|
34
34
|
if (this.visibilityTimeout > 0) {
|
|
35
35
|
this.reaper = setInterval(() => {
|
|
36
|
-
if (this.running) this.q.recover(this.visibilityTimeout);
|
|
36
|
+
if (this.running) this.q.recover(this.visibilityTimeout, this.name);
|
|
37
37
|
}, Math.max(1e3, Math.floor(this.visibilityTimeout / 2)));
|
|
38
38
|
this.reaper.unref?.();
|
|
39
39
|
}
|
|
@@ -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);
|
|
@@ -201,16 +200,22 @@ var Queue = class extends events.EventEmitter {
|
|
|
201
200
|
/**
|
|
202
201
|
* Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
|
|
203
202
|
* if they haven't been touched in `olderThanMs`. Returns the count recovered.
|
|
203
|
+
* Pass `name` to scope it to one queue — the per-worker reaper does this so a
|
|
204
|
+
* fast queue's reaper can't reclaim a slow queue's still-running jobs.
|
|
204
205
|
*/
|
|
205
|
-
recover(olderThanMs = 6e4) {
|
|
206
|
+
recover(olderThanMs = 6e4, name) {
|
|
207
|
+
const filter = name ? " AND queue = ?" : "";
|
|
208
|
+
const params = name ? [now(), now() - olderThanMs, name] : [now(), now() - olderThanMs];
|
|
206
209
|
return this.driver.prepare(
|
|
207
210
|
`UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?
|
|
208
|
-
WHERE status='active' AND updated_at <
|
|
209
|
-
).run(
|
|
211
|
+
WHERE status='active' AND updated_at < ?${filter}`
|
|
212
|
+
).run(...params).changes;
|
|
210
213
|
}
|
|
211
214
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
212
|
-
heartbeatInternal(id) {
|
|
213
|
-
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);
|
|
214
219
|
}
|
|
215
220
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
216
221
|
async close() {
|
|
@@ -230,28 +235,31 @@ var Queue = class extends events.EventEmitter {
|
|
|
230
235
|
).get(this.workerId, t, name, t);
|
|
231
236
|
return row ? deserialize(row) : null;
|
|
232
237
|
}
|
|
233
|
-
/**
|
|
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
|
+
*/
|
|
234
244
|
completeInternal(job, result) {
|
|
235
245
|
if (this.removeOnComplete) {
|
|
236
|
-
this.driver.prepare(`DELETE FROM _jobs WHERE id
|
|
237
|
-
} else {
|
|
238
|
-
this.driver.prepare(
|
|
239
|
-
`UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`
|
|
240
|
-
).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;
|
|
241
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;
|
|
242
251
|
}
|
|
243
|
-
/** @internal */
|
|
252
|
+
/** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */
|
|
244
253
|
failInternal(job, err) {
|
|
245
254
|
const message = err instanceof Error ? err.message : String(err);
|
|
246
255
|
if (job.attempts < job.maxAttempts) {
|
|
247
|
-
this.driver.prepare(
|
|
248
|
-
`UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`
|
|
249
|
-
).run(now() + this.backoff(job.attempts), message, now(), job.id);
|
|
250
|
-
} else {
|
|
251
|
-
this.driver.prepare(
|
|
252
|
-
`UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`
|
|
253
|
-
).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;
|
|
254
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;
|
|
255
263
|
}
|
|
256
264
|
};
|
|
257
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);\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 */\n recover(olderThanMs = 60_000): number {\n return this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?\n WHERE status='active' AND updated_at < ?`,\n )\n .run(now(), now() - olderThanMs).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
|
@@ -86,18 +86,25 @@ declare class Queue extends EventEmitter {
|
|
|
86
86
|
/**
|
|
87
87
|
* Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
|
|
88
88
|
* if they haven't been touched in `olderThanMs`. Returns the count recovered.
|
|
89
|
+
* Pass `name` to scope it to one queue — the per-worker reaper does this so a
|
|
90
|
+
* fast queue's reaper can't reclaim a slow queue's still-running jobs.
|
|
89
91
|
*/
|
|
90
|
-
recover(olderThanMs?: number): number;
|
|
92
|
+
recover(olderThanMs?: number, name?: string): number;
|
|
91
93
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
92
|
-
heartbeatInternal(id: number): void;
|
|
94
|
+
heartbeatInternal(id: number, attempts: number): void;
|
|
93
95
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
94
96
|
close(): Promise<void>;
|
|
95
97
|
/** @internal Atomically claim the next due job, counting the attempt. */
|
|
96
98
|
claimInternal(name: string): Job | null;
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
108
|
}
|
|
102
109
|
/** Create a job queue over a monlite database. */
|
|
103
110
|
declare function createQueue(db: Monlite, opts?: QueueOptions): Queue;
|
package/dist/index.d.ts
CHANGED
|
@@ -86,18 +86,25 @@ declare class Queue extends EventEmitter {
|
|
|
86
86
|
/**
|
|
87
87
|
* Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
|
|
88
88
|
* if they haven't been touched in `olderThanMs`. Returns the count recovered.
|
|
89
|
+
* Pass `name` to scope it to one queue — the per-worker reaper does this so a
|
|
90
|
+
* fast queue's reaper can't reclaim a slow queue's still-running jobs.
|
|
89
91
|
*/
|
|
90
|
-
recover(olderThanMs?: number): number;
|
|
92
|
+
recover(olderThanMs?: number, name?: string): number;
|
|
91
93
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
92
|
-
heartbeatInternal(id: number): void;
|
|
94
|
+
heartbeatInternal(id: number, attempts: number): void;
|
|
93
95
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
94
96
|
close(): Promise<void>;
|
|
95
97
|
/** @internal Atomically claim the next due job, counting the attempt. */
|
|
96
98
|
claimInternal(name: string): Job | null;
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
108
|
}
|
|
102
109
|
/** Create a job queue over a monlite database. */
|
|
103
110
|
declare function createQueue(db: Monlite, opts?: QueueOptions): Queue;
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var WorkerImpl = class {
|
|
|
31
31
|
this.visibilityTimeout = Math.max(0, opts.visibilityTimeout ?? 0);
|
|
32
32
|
if (this.visibilityTimeout > 0) {
|
|
33
33
|
this.reaper = setInterval(() => {
|
|
34
|
-
if (this.running) this.q.recover(this.visibilityTimeout);
|
|
34
|
+
if (this.running) this.q.recover(this.visibilityTimeout, this.name);
|
|
35
35
|
}, Math.max(1e3, Math.floor(this.visibilityTimeout / 2)));
|
|
36
36
|
this.reaper.unref?.();
|
|
37
37
|
}
|
|
@@ -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);
|
|
@@ -199,16 +198,22 @@ var Queue = class extends EventEmitter {
|
|
|
199
198
|
/**
|
|
200
199
|
* Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
|
|
201
200
|
* if they haven't been touched in `olderThanMs`. Returns the count recovered.
|
|
201
|
+
* Pass `name` to scope it to one queue — the per-worker reaper does this so a
|
|
202
|
+
* fast queue's reaper can't reclaim a slow queue's still-running jobs.
|
|
202
203
|
*/
|
|
203
|
-
recover(olderThanMs = 6e4) {
|
|
204
|
+
recover(olderThanMs = 6e4, name) {
|
|
205
|
+
const filter = name ? " AND queue = ?" : "";
|
|
206
|
+
const params = name ? [now(), now() - olderThanMs, name] : [now(), now() - olderThanMs];
|
|
204
207
|
return this.driver.prepare(
|
|
205
208
|
`UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?
|
|
206
|
-
WHERE status='active' AND updated_at <
|
|
207
|
-
).run(
|
|
209
|
+
WHERE status='active' AND updated_at < ?${filter}`
|
|
210
|
+
).run(...params).changes;
|
|
208
211
|
}
|
|
209
212
|
/** @internal Extend a running job's visibility timeout (worker heartbeat). */
|
|
210
|
-
heartbeatInternal(id) {
|
|
211
|
-
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);
|
|
212
217
|
}
|
|
213
218
|
/** Stop all workers and wait for in-flight jobs to finish. */
|
|
214
219
|
async close() {
|
|
@@ -228,28 +233,31 @@ var Queue = class extends EventEmitter {
|
|
|
228
233
|
).get(this.workerId, t, name, t);
|
|
229
234
|
return row ? deserialize(row) : null;
|
|
230
235
|
}
|
|
231
|
-
/**
|
|
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
|
+
*/
|
|
232
242
|
completeInternal(job, result) {
|
|
233
243
|
if (this.removeOnComplete) {
|
|
234
|
-
this.driver.prepare(`DELETE FROM _jobs WHERE id
|
|
235
|
-
} else {
|
|
236
|
-
this.driver.prepare(
|
|
237
|
-
`UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`
|
|
238
|
-
).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;
|
|
239
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;
|
|
240
249
|
}
|
|
241
|
-
/** @internal */
|
|
250
|
+
/** @internal Record a failure (retry or dead-letter). Returns false if fenced out (see completeInternal). */
|
|
242
251
|
failInternal(job, err) {
|
|
243
252
|
const message = err instanceof Error ? err.message : String(err);
|
|
244
253
|
if (job.attempts < job.maxAttempts) {
|
|
245
|
-
this.driver.prepare(
|
|
246
|
-
`UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`
|
|
247
|
-
).run(now() + this.backoff(job.attempts), message, now(), job.id);
|
|
248
|
-
} else {
|
|
249
|
-
this.driver.prepare(
|
|
250
|
-
`UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`
|
|
251
|
-
).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;
|
|
252
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;
|
|
253
261
|
}
|
|
254
262
|
};
|
|
255
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);\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 */\n recover(olderThanMs = 60_000): number {\n return this.driver\n .prepare(\n `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?\n WHERE status='active' AND updated_at < ?`,\n )\n .run(now(), now() - olderThanMs).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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monlite/queue",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Durable job queue for @monlite/core: retries, backoff, delayed jobs, concurrency — on SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
25
30
|
"keywords": [
|
|
26
31
|
"monlite",
|
|
27
32
|
"queue",
|
|
@@ -49,7 +54,7 @@
|
|
|
49
54
|
"node": ">=18"
|
|
50
55
|
},
|
|
51
56
|
"dependencies": {
|
|
52
|
-
"@monlite/core": "
|
|
57
|
+
"@monlite/core": "workspace:^"
|
|
53
58
|
},
|
|
54
59
|
"devDependencies": {
|
|
55
60
|
"@types/node": "^22.10.0",
|
|
@@ -57,10 +62,5 @@
|
|
|
57
62
|
"tsup": "^8.3.5",
|
|
58
63
|
"typescript": "^5.7.2",
|
|
59
64
|
"vitest": "^2.1.8"
|
|
60
|
-
},
|
|
61
|
-
"scripts": {
|
|
62
|
-
"build": "tsup",
|
|
63
|
-
"test": "vitest run",
|
|
64
|
-
"typecheck": "tsc --noEmit"
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|