@monlite/queue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emad Jumaah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @monlite/queue
2
+
3
+ A **durable job queue** for [`@monlite/core`](https://www.npmjs.com/package/@monlite/core), backed by SQLite — retries, backoff, delayed jobs, priorities, and concurrency, with no separate server. Part of the [local AI-agent harness](https://github.com/qataruts/monlite#readme) (the BullMQ/Redis role, locally).
4
+
5
+ ```bash
6
+ npm install @monlite/core @monlite/queue
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```ts
12
+ import { createDb } from "@monlite/core";
13
+ import { createQueue } from "@monlite/queue";
14
+
15
+ const db = createDb("app.db");
16
+ const queue = createQueue(db, { maxAttempts: 3 });
17
+
18
+ // Worker
19
+ queue.process("email", async (job) => {
20
+ await sendEmail(job.payload);
21
+ }, { concurrency: 5 });
22
+
23
+ queue.on("completed", (job) => console.log("sent", job.id));
24
+ queue.on("failed", (job, err) => console.warn("failed", job.id, err.message));
25
+
26
+ // Producer
27
+ queue.add("email", { to: "ali@example.com" });
28
+ queue.add("digest", { day: "mon" }, { delay: 60_000, priority: 10 });
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - **Durable & transactional** — jobs live in SQLite, so nothing is lost on crash.
34
+ - **Atomic claim** — workers grab jobs with a single `UPDATE … RETURNING`, so it's
35
+ **multi-process safe** (run workers in several processes against the same db).
36
+ - **Retries with backoff** — failed jobs retry up to `maxAttempts`; backoff is
37
+ exponential by default (override with `backoff(attempt) => ms`).
38
+ - **Delayed / scheduled** — `{ delay }` or `{ runAt }`.
39
+ - **Priorities** — higher `priority` runs first.
40
+ - **Concurrency** — `{ concurrency }` per worker.
41
+ - **Dead-letter** — exhausted jobs become `status: "failed"` and are kept for inspection.
42
+
43
+ ## API
44
+
45
+ ```ts
46
+ const queue = createQueue(db, {
47
+ maxAttempts, // default attempts before dead-lettering (default 1)
48
+ backoff, // (attempt) => ms; default exponential capped at 30s
49
+ removeOnComplete, // delete finished jobs instead of keeping them (default false)
50
+ });
51
+
52
+ queue.add(name, payload, { delay, runAt, priority, maxAttempts }); // → Job
53
+ queue.process(name, handler, { concurrency, pollInterval }); // → Worker
54
+ queue.on("completed" | "failed", (job, resultOrError) => {});
55
+ queue.getJob(id); // Job | undefined
56
+ queue.counts(name?); // { pending, active, done, failed }
57
+ queue.recover(olderThanMs); // requeue jobs stuck "active" from a crash
58
+ await worker.stop(); // stop one worker (drains in-flight)
59
+ await queue.close(); // stop all workers
60
+ ```
61
+
62
+ ## Recovering crashed jobs
63
+
64
+ A worker that dies mid-job leaves the job `active`. Call `queue.recover()` on
65
+ startup (or periodically) to requeue jobs that have been `active` longer than a
66
+ threshold:
67
+
68
+ ```ts
69
+ queue.recover(60_000); // requeue anything stuck > 60s
70
+ ```
71
+
72
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ var events = require('events');
4
+
5
+ // src/index.ts
6
+ var ensured = /* @__PURE__ */ new WeakSet();
7
+ var now = () => Date.now();
8
+ var defaultBackoff = (attempt) => Math.min(3e4, 1e3 * 2 ** (attempt - 1));
9
+ function deserialize(row) {
10
+ return {
11
+ id: row.id,
12
+ queue: row.queue,
13
+ status: row.status,
14
+ priority: row.priority,
15
+ payload: JSON.parse(row.payload),
16
+ attempts: row.attempts,
17
+ maxAttempts: row.max_attempts,
18
+ runAt: row.run_at,
19
+ result: row.result != null ? JSON.parse(row.result) : void 0,
20
+ error: row.error ?? void 0,
21
+ createdAt: row.created_at,
22
+ updatedAt: row.updated_at
23
+ };
24
+ }
25
+ var WorkerImpl = class {
26
+ constructor(q, name, handler, opts) {
27
+ this.q = q;
28
+ this.name = name;
29
+ this.handler = handler;
30
+ this.concurrency = Math.max(1, opts.concurrency ?? 1);
31
+ this.pollInterval = opts.pollInterval ?? 500;
32
+ this.kick();
33
+ }
34
+ q;
35
+ name;
36
+ handler;
37
+ running = true;
38
+ inFlight = 0;
39
+ timer;
40
+ drainResolve;
41
+ concurrency;
42
+ pollInterval;
43
+ /** Fill spare capacity with claimable jobs; otherwise schedule a poll. */
44
+ kick() {
45
+ if (!this.running) return;
46
+ if (this.timer) {
47
+ clearTimeout(this.timer);
48
+ this.timer = void 0;
49
+ }
50
+ while (this.running && this.inFlight < this.concurrency) {
51
+ const job = this.q.claimInternal(this.name);
52
+ if (!job) break;
53
+ this.inFlight++;
54
+ Promise.resolve().then(() => this.handler(job)).then(
55
+ (result) => {
56
+ this.q.completeInternal(job, result);
57
+ this.q.emit("completed", job, result);
58
+ },
59
+ (err) => {
60
+ this.q.failInternal(job, err);
61
+ this.q.emit("failed", job, err);
62
+ }
63
+ ).finally(() => {
64
+ this.inFlight--;
65
+ if (this.running) this.kick();
66
+ else this.checkDrained();
67
+ });
68
+ }
69
+ if (this.running && this.inFlight < this.concurrency) {
70
+ this.timer = setTimeout(() => this.kick(), this.pollInterval);
71
+ this.timer.unref?.();
72
+ }
73
+ }
74
+ checkDrained() {
75
+ if (!this.running && this.inFlight === 0 && this.drainResolve) {
76
+ this.drainResolve();
77
+ this.drainResolve = void 0;
78
+ }
79
+ }
80
+ async stop() {
81
+ this.running = false;
82
+ if (this.timer) clearTimeout(this.timer);
83
+ if (this.inFlight === 0) return;
84
+ await new Promise((resolve) => {
85
+ this.drainResolve = resolve;
86
+ });
87
+ }
88
+ };
89
+ var Queue = class extends events.EventEmitter {
90
+ driver;
91
+ maxAttempts;
92
+ backoff;
93
+ removeOnComplete;
94
+ workerId;
95
+ workers = [];
96
+ constructor(db, opts = {}) {
97
+ super();
98
+ this.driver = db.driver;
99
+ this.maxAttempts = opts.maxAttempts ?? 1;
100
+ this.backoff = opts.backoff ?? defaultBackoff;
101
+ this.removeOnComplete = opts.removeOnComplete ?? false;
102
+ this.workerId = opts.workerId ?? `w-${process.pid}`;
103
+ if (!ensured.has(db)) {
104
+ this.driver.exec(
105
+ `CREATE TABLE IF NOT EXISTS _jobs (
106
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
107
+ queue TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL,
108
+ run_at INTEGER NOT NULL, attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,
109
+ payload TEXT NOT NULL, result TEXT, error TEXT, locked_by TEXT,
110
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
111
+ )`
112
+ );
113
+ this.driver.exec(
114
+ `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`
115
+ );
116
+ ensured.add(db);
117
+ }
118
+ }
119
+ /** Enqueue a job. */
120
+ add(name, payload, opts = {}) {
121
+ const t = now();
122
+ const runAt = opts.runAt ?? (opts.delay ? t + opts.delay : t);
123
+ const info = this.driver.prepare(
124
+ `INSERT INTO _jobs (queue, status, priority, run_at, attempts, max_attempts, payload, created_at, updated_at)
125
+ VALUES (?, 'pending', ?, ?, 0, ?, ?, ?, ?)`
126
+ ).run(
127
+ name,
128
+ opts.priority ?? 0,
129
+ runAt,
130
+ opts.maxAttempts ?? this.maxAttempts,
131
+ JSON.stringify(payload ?? null),
132
+ t,
133
+ t
134
+ );
135
+ const job = this.getJob(Number(info.lastInsertRowid));
136
+ for (const w of this.workers) if (w.name === name) w.kick();
137
+ return job;
138
+ }
139
+ /** Register a worker for a queue. Returns a handle with `stop()`. */
140
+ process(name, handler, opts = {}) {
141
+ const w = new WorkerImpl(this, name, handler, opts);
142
+ this.workers.push(w);
143
+ return w;
144
+ }
145
+ getJob(id) {
146
+ const row = this.driver.prepare(`SELECT * FROM _jobs WHERE id = ?`).get(id);
147
+ return row ? deserialize(row) : void 0;
148
+ }
149
+ /** Count jobs by status (optionally for one queue). */
150
+ counts(name) {
151
+ const rows = name ? this.driver.prepare(
152
+ `SELECT status, COUNT(*) AS n FROM _jobs WHERE queue = ? GROUP BY status`
153
+ ).all(name) : this.driver.prepare(`SELECT status, COUNT(*) AS n FROM _jobs GROUP BY status`).all();
154
+ const out = {
155
+ pending: 0,
156
+ active: 0,
157
+ done: 0,
158
+ failed: 0
159
+ };
160
+ for (const r of rows) out[r.status] = r.n;
161
+ return out;
162
+ }
163
+ /**
164
+ * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
165
+ * if they haven't been touched in `olderThanMs`. Returns the count recovered.
166
+ */
167
+ recover(olderThanMs = 6e4) {
168
+ return this.driver.prepare(
169
+ `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?
170
+ WHERE status='active' AND updated_at < ?`
171
+ ).run(now(), now() - olderThanMs).changes;
172
+ }
173
+ /** Stop all workers and wait for in-flight jobs to finish. */
174
+ async close() {
175
+ await Promise.all(this.workers.map((w) => w.stop()));
176
+ }
177
+ /** @internal Atomically claim the next due job, counting the attempt. */
178
+ claimInternal(name) {
179
+ const t = now();
180
+ const row = this.driver.prepare(
181
+ `UPDATE _jobs SET status='active', attempts=attempts+1, locked_by=?, updated_at=?
182
+ WHERE id = (
183
+ SELECT id FROM _jobs
184
+ WHERE queue=? AND status='pending' AND run_at<=?
185
+ ORDER BY priority DESC, id ASC LIMIT 1
186
+ )
187
+ RETURNING *`
188
+ ).get(this.workerId, t, name, t);
189
+ return row ? deserialize(row) : null;
190
+ }
191
+ /** @internal */
192
+ completeInternal(job, result) {
193
+ if (this.removeOnComplete) {
194
+ this.driver.prepare(`DELETE FROM _jobs WHERE id = ?`).run(job.id);
195
+ } else {
196
+ this.driver.prepare(
197
+ `UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`
198
+ ).run(JSON.stringify(result ?? null), now(), job.id);
199
+ }
200
+ }
201
+ /** @internal */
202
+ failInternal(job, err) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ if (job.attempts < job.maxAttempts) {
205
+ this.driver.prepare(
206
+ `UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`
207
+ ).run(now() + this.backoff(job.attempts), message, now(), job.id);
208
+ } else {
209
+ this.driver.prepare(
210
+ `UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`
211
+ ).run(message, now(), job.id);
212
+ }
213
+ }
214
+ };
215
+ function createQueue(db, opts) {
216
+ return new Queue(db, opts);
217
+ }
218
+
219
+ exports.Queue = Queue;
220
+ exports.createQueue = createQueue;
221
+ //# sourceMappingURL=index.cjs.map
222
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["EventEmitter"],"mappings":";;;;;AAwEA,IAAM,OAAA,uBAAc,OAAA,EAAgB;AACpC,IAAM,GAAA,GAAM,MAAM,IAAA,CAAK,GAAA,EAAI;AAC3B,IAAM,cAAA,GAAiB,CAAC,OAAA,KACtB,IAAA,CAAK,IAAI,GAAA,EAAQ,GAAA,GAAO,CAAA,KAAM,OAAA,GAAU,CAAA,CAAE,CAAA;AAE5C,SAAS,YAAY,GAAA,EAAe;AAClC,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,OAAO,GAAA,CAAI,KAAA;AAAA,IACX,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AAAA,IAC/B,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,aAAa,GAAA,CAAI,YAAA;AAAA,IACjB,OAAO,GAAA,CAAI,MAAA;AAAA,IACX,MAAA,EAAQ,IAAI,MAAA,IAAU,IAAA,GAAO,KAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA,GAAI,MAAA;AAAA,IACtD,KAAA,EAAO,IAAI,KAAA,IAAS,MAAA;AAAA,IACpB,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,WAAW,GAAA,CAAI;AAAA,GACjB;AACF;AAEA,IAAM,aAAN,MAAmC;AAAA,EAQjC,WAAA,CACmB,CAAA,EACR,IAAA,EACQ,OAAA,EACjB,IAAA,EACA;AAJiB,IAAA,IAAA,CAAA,CAAA,GAAA,CAAA;AACR,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACQ,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAGjB,IAAA,IAAA,CAAK,cAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,eAAe,CAAC,CAAA;AACpD,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,YAAA,IAAgB,GAAA;AACzC,IAAA,IAAA,CAAK,IAAA,EAAK;AAAA,EACZ;AAAA,EARmB,CAAA;AAAA,EACR,IAAA;AAAA,EACQ,OAAA;AAAA,EAVX,OAAA,GAAU,IAAA;AAAA,EACV,QAAA,GAAW,CAAA;AAAA,EACX,KAAA;AAAA,EACA,YAAA;AAAA,EACS,WAAA;AAAA,EACA,YAAA;AAAA;AAAA,EAcjB,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACnB,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,YAAA,CAAa,KAAK,KAAK,CAAA;AACvB,MAAA,IAAA,CAAK,KAAA,GAAQ,MAAA;AAAA,IACf;AACA,IAAA,OAAO,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,QAAA,GAAW,KAAK,WAAA,EAAa;AACvD,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,CAAA,CAAE,aAAA,CAAc,KAAK,IAAI,CAAA;AAC1C,MAAA,IAAI,CAAC,GAAA,EAAK;AACV,MAAA,IAAA,CAAK,QAAA,EAAA;AACL,MAAA,OAAA,CAAQ,OAAA,GACL,IAAA,CAAK,MAAM,KAAK,OAAA,CAAQ,GAAG,CAAC,CAAA,CAC5B,IAAA;AAAA,QACC,CAAC,MAAA,KAAW;AACV,UAAA,IAAA,CAAK,CAAA,CAAE,gBAAA,CAAiB,GAAA,EAAK,MAAM,CAAA;AACnC,UAAA,IAAA,CAAK,CAAA,CAAE,IAAA,CAAK,WAAA,EAAa,GAAA,EAAK,MAAM,CAAA;AAAA,QACtC,CAAA;AAAA,QACA,CAAC,GAAA,KAAQ;AACP,UAAA,IAAA,CAAK,CAAA,CAAE,YAAA,CAAa,GAAA,EAAK,GAAG,CAAA;AAC5B,UAAA,IAAA,CAAK,CAAA,CAAE,IAAA,CAAK,QAAA,EAAU,GAAA,EAAK,GAAG,CAAA;AAAA,QAChC;AAAA,OACF,CACC,QAAQ,MAAM;AACb,QAAA,IAAA,CAAK,QAAA,EAAA;AACL,QAAA,IAAI,IAAA,CAAK,OAAA,EAAS,IAAA,CAAK,IAAA,EAAK;AAAA,kBAClB,YAAA,EAAa;AAAA,MACzB,CAAC,CAAA;AAAA,IACL;AACA,IAAA,IAAI,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,QAAA,GAAW,KAAK,WAAA,EAAa;AACpD,MAAA,IAAA,CAAK,QAAQ,UAAA,CAAW,MAAM,KAAK,IAAA,EAAK,EAAG,KAAK,YAAY,CAAA;AAC5D,MAAA,IAAA,CAAK,MAAM,KAAA,IAAQ;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,YAAA,GAAqB;AAC3B,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,KAAK,QAAA,KAAa,CAAA,IAAK,KAAK,YAAA,EAAc;AAC7D,MAAA,IAAA,CAAK,YAAA,EAAa;AAClB,MAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAI,IAAA,CAAK,KAAA,EAAO,YAAA,CAAa,IAAA,CAAK,KAAK,CAAA;AACvC,IAAA,IAAI,IAAA,CAAK,aAAa,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,MAAA,IAAA,CAAK,YAAA,GAAe,OAAA;AAAA,IACtB,CAAC,CAAA;AAAA,EACH;AACF,CAAA;AAOO,IAAM,KAAA,GAAN,cAAoBA,mBAAA,CAAa;AAAA,EACrB,MAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,gBAAA;AAAA,EACR,QAAA;AAAA,EACQ,UAAwB,EAAC;AAAA,EAE1C,WAAA,CAAY,EAAA,EAAa,IAAA,GAAqB,EAAC,EAAG;AAChD,IAAA,KAAA,EAAM;AACN,IAAA,IAAA,CAAK,SAAS,EAAA,CAAG,MAAA;AACjB,IAAA,IAAA,CAAK,WAAA,GAAc,KAAK,WAAA,IAAe,CAAA;AACvC,IAAA,IAAA,CAAK,OAAA,GAAU,KAAK,OAAA,IAAW,cAAA;AAC/B,IAAA,IAAA,CAAK,gBAAA,GAAmB,KAAK,gBAAA,IAAoB,KAAA;AACjD,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,QAAA,IAAY,CAAA,EAAA,EAAK,QAAQ,GAAG,CAAA,CAAA;AAEjD,IAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACpB,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAA;AAAA,OAOF;AACA,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,CAAA,iFAAA;AAAA,OACF;AACA,MAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,GAAA,CAAa,IAAA,EAAc,OAAA,EAAY,IAAA,GAAmB,EAAC,EAAW;AACpE,IAAA,MAAM,IAAI,GAAA,EAAI;AACd,IAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,KAAU,KAAK,KAAA,GAAQ,CAAA,GAAI,KAAK,KAAA,GAAQ,CAAA,CAAA;AAC3D,IAAA,MAAM,IAAA,GAAO,KAAK,MAAA,CACf,OAAA;AAAA,MACC,CAAA;AAAA,mDAAA;AAAA,KAEF,CACC,GAAA;AAAA,MACC,IAAA;AAAA,MACA,KAAK,QAAA,IAAY,CAAA;AAAA,MACjB,KAAA;AAAA,MACA,IAAA,CAAK,eAAe,IAAA,CAAK,WAAA;AAAA,MACzB,IAAA,CAAK,SAAA,CAAU,OAAA,IAAW,IAAI,CAAA;AAAA,MAC9B,CAAA;AAAA,MACA;AAAA,KACF;AACF,IAAA,MAAM,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,eAAe,CAAC,CAAA;AACpD,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,OAAA,EAAS,IAAI,EAAE,IAAA,KAAS,IAAA,IAAQ,IAAA,EAAK;AAC1D,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAA,CACE,IAAA,EACA,OAAA,EACA,IAAA,GAAuB,EAAC,EAChB;AACR,IAAA,MAAM,IAAI,IAAI,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,SAAoB,IAAI,CAAA;AAC7D,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAC,CAAA;AACnB,IAAA,OAAO,CAAA;AAAA,EACT;AAAA,EAEA,OAAgB,EAAA,EAAgC;AAC9C,IAAA,MAAM,MAAM,IAAA,CAAK,MAAA,CACd,QAAQ,CAAA,gCAAA,CAAkC,CAAA,CAC1C,IAAI,EAAE,CAAA;AACT,IAAA,OAAO,GAAA,GAAO,WAAA,CAAY,GAAG,CAAA,GAAe,MAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,OAAO,IAAA,EAA0C;AAC/C,IAAA,MAAM,IAAA,GACJ,IAAA,GACI,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,MACC,CAAA,uEAAA;AAAA,KACF,CACC,IAAI,IAAI,CAAA,GACX,KAAK,MAAA,CACF,OAAA,CAAQ,CAAA,uDAAA,CAAyD,CAAA,CACjE,GAAA,EAAI;AAEb,IAAA,MAAM,GAAA,GAAiC;AAAA,MACrC,OAAA,EAAS,CAAA;AAAA,MACT,MAAA,EAAQ,CAAA;AAAA,MACR,IAAA,EAAM,CAAA;AAAA,MACN,MAAA,EAAQ;AAAA,KACV;AACA,IAAA,KAAA,MAAW,KAAK,IAAA,EAAM,GAAA,CAAI,CAAA,CAAE,MAAM,IAAI,CAAA,CAAE,CAAA;AACxC,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAA,CAAQ,cAAc,GAAA,EAAgB;AACpC,IAAA,OAAO,KAAK,MAAA,CACT,OAAA;AAAA,MACC,CAAA;AAAA,iDAAA;AAAA,MAGD,GAAA,CAAI,GAAA,IAAO,GAAA,EAAI,GAAI,WAAW,CAAA,CAAE,OAAA;AAAA,EACrC;AAAA;AAAA,EAGA,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAC,CAAA;AAAA,EACrD;AAAA;AAAA,EAGA,cAAc,IAAA,EAA0B;AACtC,IAAA,MAAM,IAAI,GAAA,EAAI;AACd,IAAA,MAAM,GAAA,GAAM,KAAK,MAAA,CACd,OAAA;AAAA,MACC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA;AAAA,MAQD,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,CAAA,EAAG,MAAM,CAAC,CAAA;AAChC,IAAA,OAAO,GAAA,GAAM,WAAA,CAAY,GAAG,CAAA,GAAI,IAAA;AAAA,EAClC;AAAA;AAAA,EAGA,gBAAA,CAAiB,KAAU,MAAA,EAAuB;AAChD,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,CAAA,8BAAA,CAAgC,CAAA,CAAE,GAAA,CAAI,IAAI,EAAE,CAAA;AAAA,IAClE,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,6EAAA;AAAA,OACF,CACC,GAAA,CAAI,IAAA,CAAK,SAAA,CAAU,MAAA,IAAU,IAAI,CAAA,EAAG,GAAA,EAAI,EAAG,GAAA,CAAI,EAAE,CAAA;AAAA,IACtD;AAAA,EACF;AAAA;AAAA,EAGA,YAAA,CAAa,KAAU,GAAA,EAAoB;AAEzC,IAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,IAAA,IAAI,GAAA,CAAI,QAAA,GAAW,GAAA,CAAI,WAAA,EAAa;AAClC,MAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,6FAAA;AAAA,OACF,CACC,GAAA,CAAI,GAAA,EAAI,GAAI,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,EAAG,OAAA,EAAS,GAAA,EAAI,EAAG,IAAI,EAAE,CAAA;AAAA,IACnE,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,kEAAA;AAAA,QAED,GAAA,CAAI,OAAA,EAAS,GAAA,EAAI,EAAG,IAAI,EAAE,CAAA;AAAA,IAC/B;AAAA,EACF;AACF;AAGO,SAAS,WAAA,CAAY,IAAa,IAAA,EAA4B;AACnE,EAAA,OAAO,IAAI,KAAA,CAAM,EAAA,EAAI,IAAI,CAAA;AAC3B","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 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 /** 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\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 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 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\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.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 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 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.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 this.workerId = opts.workerId ?? `w-${process.pid}`;\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,\n created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL\n )`,\n );\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`,\n );\n ensured.add(db);\n }\n }\n\n /** Enqueue a job. */\n add<T = any>(name: string, payload: T, opts: AddOptions = {}): Job<T> {\n const t = now();\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, 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 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 /** 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"]}
@@ -0,0 +1,88 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Monlite } from '@monlite/core';
3
+
4
+ type JobStatus = "pending" | "active" | "done" | "failed";
5
+ interface Job<T = any> {
6
+ id: number;
7
+ queue: string;
8
+ status: JobStatus;
9
+ priority: number;
10
+ payload: T;
11
+ /** Number of attempts already made (0 until the first run). */
12
+ attempts: number;
13
+ maxAttempts: number;
14
+ runAt: number;
15
+ result?: any;
16
+ error?: string;
17
+ createdAt: number;
18
+ updatedAt: number;
19
+ }
20
+ interface AddOptions {
21
+ /** Delay before the job becomes runnable (ms). */
22
+ delay?: number;
23
+ /** Explicit epoch-ms run time (overrides `delay`). */
24
+ runAt?: number;
25
+ /** Higher runs first. Default 0. */
26
+ priority?: number;
27
+ /** Total attempts before dead-lettering. Default: the queue's `maxAttempts`. */
28
+ maxAttempts?: number;
29
+ }
30
+ interface ProcessOptions {
31
+ /** Jobs run concurrently per worker. Default 1. */
32
+ concurrency?: number;
33
+ /** How often to poll for due jobs when idle (ms). Default 500. */
34
+ pollInterval?: number;
35
+ }
36
+ interface QueueOptions {
37
+ /** Default attempts before dead-lettering. Default 1 (no retry). */
38
+ maxAttempts?: number;
39
+ /** Backoff before retry N (ms). Default: exponential, capped at 30s. */
40
+ backoff?: (attempt: number) => number;
41
+ /** Delete jobs once completed instead of keeping them as `done`. Default false. */
42
+ removeOnComplete?: boolean;
43
+ /** Identifies this worker process in the `locked_by` column. */
44
+ workerId?: string;
45
+ }
46
+ type Handler<T = any, R = any> = (job: Job<T>) => Promise<R> | R;
47
+ interface Worker {
48
+ /** Stop claiming new jobs and wait for in-flight ones to finish. */
49
+ stop(): Promise<void>;
50
+ }
51
+ /**
52
+ * A durable, multi-process-safe job queue backed by SQLite. Producers `add`
53
+ * jobs; workers `process` them with retries, backoff, delays, and concurrency.
54
+ * Emits `"completed"` (job, result) and `"failed"` (job, error).
55
+ */
56
+ declare class Queue extends EventEmitter {
57
+ private readonly driver;
58
+ private readonly maxAttempts;
59
+ private readonly backoff;
60
+ private readonly removeOnComplete;
61
+ readonly workerId: string;
62
+ private readonly workers;
63
+ constructor(db: Monlite, opts?: QueueOptions);
64
+ /** Enqueue a job. */
65
+ add<T = any>(name: string, payload: T, opts?: AddOptions): Job<T>;
66
+ /** Register a worker for a queue. Returns a handle with `stop()`. */
67
+ process<T = any, R = any>(name: string, handler: Handler<T, R>, opts?: ProcessOptions): Worker;
68
+ getJob<T = any>(id: number): Job<T> | undefined;
69
+ /** Count jobs by status (optionally for one queue). */
70
+ counts(name?: string): Record<JobStatus, number>;
71
+ /**
72
+ * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
73
+ * if they haven't been touched in `olderThanMs`. Returns the count recovered.
74
+ */
75
+ recover(olderThanMs?: number): number;
76
+ /** Stop all workers and wait for in-flight jobs to finish. */
77
+ close(): Promise<void>;
78
+ /** @internal Atomically claim the next due job, counting the attempt. */
79
+ claimInternal(name: string): Job | null;
80
+ /** @internal */
81
+ completeInternal(job: Job, result: unknown): void;
82
+ /** @internal */
83
+ failInternal(job: Job, err: unknown): void;
84
+ }
85
+ /** Create a job queue over a monlite database. */
86
+ declare function createQueue(db: Monlite, opts?: QueueOptions): Queue;
87
+
88
+ export { type AddOptions, type Handler, type Job, type JobStatus, type ProcessOptions, Queue, type QueueOptions, type Worker, createQueue };
@@ -0,0 +1,88 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Monlite } from '@monlite/core';
3
+
4
+ type JobStatus = "pending" | "active" | "done" | "failed";
5
+ interface Job<T = any> {
6
+ id: number;
7
+ queue: string;
8
+ status: JobStatus;
9
+ priority: number;
10
+ payload: T;
11
+ /** Number of attempts already made (0 until the first run). */
12
+ attempts: number;
13
+ maxAttempts: number;
14
+ runAt: number;
15
+ result?: any;
16
+ error?: string;
17
+ createdAt: number;
18
+ updatedAt: number;
19
+ }
20
+ interface AddOptions {
21
+ /** Delay before the job becomes runnable (ms). */
22
+ delay?: number;
23
+ /** Explicit epoch-ms run time (overrides `delay`). */
24
+ runAt?: number;
25
+ /** Higher runs first. Default 0. */
26
+ priority?: number;
27
+ /** Total attempts before dead-lettering. Default: the queue's `maxAttempts`. */
28
+ maxAttempts?: number;
29
+ }
30
+ interface ProcessOptions {
31
+ /** Jobs run concurrently per worker. Default 1. */
32
+ concurrency?: number;
33
+ /** How often to poll for due jobs when idle (ms). Default 500. */
34
+ pollInterval?: number;
35
+ }
36
+ interface QueueOptions {
37
+ /** Default attempts before dead-lettering. Default 1 (no retry). */
38
+ maxAttempts?: number;
39
+ /** Backoff before retry N (ms). Default: exponential, capped at 30s. */
40
+ backoff?: (attempt: number) => number;
41
+ /** Delete jobs once completed instead of keeping them as `done`. Default false. */
42
+ removeOnComplete?: boolean;
43
+ /** Identifies this worker process in the `locked_by` column. */
44
+ workerId?: string;
45
+ }
46
+ type Handler<T = any, R = any> = (job: Job<T>) => Promise<R> | R;
47
+ interface Worker {
48
+ /** Stop claiming new jobs and wait for in-flight ones to finish. */
49
+ stop(): Promise<void>;
50
+ }
51
+ /**
52
+ * A durable, multi-process-safe job queue backed by SQLite. Producers `add`
53
+ * jobs; workers `process` them with retries, backoff, delays, and concurrency.
54
+ * Emits `"completed"` (job, result) and `"failed"` (job, error).
55
+ */
56
+ declare class Queue extends EventEmitter {
57
+ private readonly driver;
58
+ private readonly maxAttempts;
59
+ private readonly backoff;
60
+ private readonly removeOnComplete;
61
+ readonly workerId: string;
62
+ private readonly workers;
63
+ constructor(db: Monlite, opts?: QueueOptions);
64
+ /** Enqueue a job. */
65
+ add<T = any>(name: string, payload: T, opts?: AddOptions): Job<T>;
66
+ /** Register a worker for a queue. Returns a handle with `stop()`. */
67
+ process<T = any, R = any>(name: string, handler: Handler<T, R>, opts?: ProcessOptions): Worker;
68
+ getJob<T = any>(id: number): Job<T> | undefined;
69
+ /** Count jobs by status (optionally for one queue). */
70
+ counts(name?: string): Record<JobStatus, number>;
71
+ /**
72
+ * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
73
+ * if they haven't been touched in `olderThanMs`. Returns the count recovered.
74
+ */
75
+ recover(olderThanMs?: number): number;
76
+ /** Stop all workers and wait for in-flight jobs to finish. */
77
+ close(): Promise<void>;
78
+ /** @internal Atomically claim the next due job, counting the attempt. */
79
+ claimInternal(name: string): Job | null;
80
+ /** @internal */
81
+ completeInternal(job: Job, result: unknown): void;
82
+ /** @internal */
83
+ failInternal(job: Job, err: unknown): void;
84
+ }
85
+ /** Create a job queue over a monlite database. */
86
+ declare function createQueue(db: Monlite, opts?: QueueOptions): Queue;
87
+
88
+ export { type AddOptions, type Handler, type Job, type JobStatus, type ProcessOptions, Queue, type QueueOptions, type Worker, createQueue };
package/dist/index.js ADDED
@@ -0,0 +1,219 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ // src/index.ts
4
+ var ensured = /* @__PURE__ */ new WeakSet();
5
+ var now = () => Date.now();
6
+ var defaultBackoff = (attempt) => Math.min(3e4, 1e3 * 2 ** (attempt - 1));
7
+ function deserialize(row) {
8
+ return {
9
+ id: row.id,
10
+ queue: row.queue,
11
+ status: row.status,
12
+ priority: row.priority,
13
+ payload: JSON.parse(row.payload),
14
+ attempts: row.attempts,
15
+ maxAttempts: row.max_attempts,
16
+ runAt: row.run_at,
17
+ result: row.result != null ? JSON.parse(row.result) : void 0,
18
+ error: row.error ?? void 0,
19
+ createdAt: row.created_at,
20
+ updatedAt: row.updated_at
21
+ };
22
+ }
23
+ var WorkerImpl = class {
24
+ constructor(q, name, handler, opts) {
25
+ this.q = q;
26
+ this.name = name;
27
+ this.handler = handler;
28
+ this.concurrency = Math.max(1, opts.concurrency ?? 1);
29
+ this.pollInterval = opts.pollInterval ?? 500;
30
+ this.kick();
31
+ }
32
+ q;
33
+ name;
34
+ handler;
35
+ running = true;
36
+ inFlight = 0;
37
+ timer;
38
+ drainResolve;
39
+ concurrency;
40
+ pollInterval;
41
+ /** Fill spare capacity with claimable jobs; otherwise schedule a poll. */
42
+ kick() {
43
+ if (!this.running) return;
44
+ if (this.timer) {
45
+ clearTimeout(this.timer);
46
+ this.timer = void 0;
47
+ }
48
+ while (this.running && this.inFlight < this.concurrency) {
49
+ const job = this.q.claimInternal(this.name);
50
+ if (!job) break;
51
+ this.inFlight++;
52
+ Promise.resolve().then(() => this.handler(job)).then(
53
+ (result) => {
54
+ this.q.completeInternal(job, result);
55
+ this.q.emit("completed", job, result);
56
+ },
57
+ (err) => {
58
+ this.q.failInternal(job, err);
59
+ this.q.emit("failed", job, err);
60
+ }
61
+ ).finally(() => {
62
+ this.inFlight--;
63
+ if (this.running) this.kick();
64
+ else this.checkDrained();
65
+ });
66
+ }
67
+ if (this.running && this.inFlight < this.concurrency) {
68
+ this.timer = setTimeout(() => this.kick(), this.pollInterval);
69
+ this.timer.unref?.();
70
+ }
71
+ }
72
+ checkDrained() {
73
+ if (!this.running && this.inFlight === 0 && this.drainResolve) {
74
+ this.drainResolve();
75
+ this.drainResolve = void 0;
76
+ }
77
+ }
78
+ async stop() {
79
+ this.running = false;
80
+ if (this.timer) clearTimeout(this.timer);
81
+ if (this.inFlight === 0) return;
82
+ await new Promise((resolve) => {
83
+ this.drainResolve = resolve;
84
+ });
85
+ }
86
+ };
87
+ var Queue = class extends EventEmitter {
88
+ driver;
89
+ maxAttempts;
90
+ backoff;
91
+ removeOnComplete;
92
+ workerId;
93
+ workers = [];
94
+ constructor(db, opts = {}) {
95
+ super();
96
+ this.driver = db.driver;
97
+ this.maxAttempts = opts.maxAttempts ?? 1;
98
+ this.backoff = opts.backoff ?? defaultBackoff;
99
+ this.removeOnComplete = opts.removeOnComplete ?? false;
100
+ this.workerId = opts.workerId ?? `w-${process.pid}`;
101
+ if (!ensured.has(db)) {
102
+ this.driver.exec(
103
+ `CREATE TABLE IF NOT EXISTS _jobs (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ queue TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL,
106
+ run_at INTEGER NOT NULL, attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,
107
+ payload TEXT NOT NULL, result TEXT, error TEXT, locked_by TEXT,
108
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
109
+ )`
110
+ );
111
+ this.driver.exec(
112
+ `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`
113
+ );
114
+ ensured.add(db);
115
+ }
116
+ }
117
+ /** Enqueue a job. */
118
+ add(name, payload, opts = {}) {
119
+ const t = now();
120
+ const runAt = opts.runAt ?? (opts.delay ? t + opts.delay : t);
121
+ const info = this.driver.prepare(
122
+ `INSERT INTO _jobs (queue, status, priority, run_at, attempts, max_attempts, payload, created_at, updated_at)
123
+ VALUES (?, 'pending', ?, ?, 0, ?, ?, ?, ?)`
124
+ ).run(
125
+ name,
126
+ opts.priority ?? 0,
127
+ runAt,
128
+ opts.maxAttempts ?? this.maxAttempts,
129
+ JSON.stringify(payload ?? null),
130
+ t,
131
+ t
132
+ );
133
+ const job = this.getJob(Number(info.lastInsertRowid));
134
+ for (const w of this.workers) if (w.name === name) w.kick();
135
+ return job;
136
+ }
137
+ /** Register a worker for a queue. Returns a handle with `stop()`. */
138
+ process(name, handler, opts = {}) {
139
+ const w = new WorkerImpl(this, name, handler, opts);
140
+ this.workers.push(w);
141
+ return w;
142
+ }
143
+ getJob(id) {
144
+ const row = this.driver.prepare(`SELECT * FROM _jobs WHERE id = ?`).get(id);
145
+ return row ? deserialize(row) : void 0;
146
+ }
147
+ /** Count jobs by status (optionally for one queue). */
148
+ counts(name) {
149
+ const rows = name ? this.driver.prepare(
150
+ `SELECT status, COUNT(*) AS n FROM _jobs WHERE queue = ? GROUP BY status`
151
+ ).all(name) : this.driver.prepare(`SELECT status, COUNT(*) AS n FROM _jobs GROUP BY status`).all();
152
+ const out = {
153
+ pending: 0,
154
+ active: 0,
155
+ done: 0,
156
+ failed: 0
157
+ };
158
+ for (const r of rows) out[r.status] = r.n;
159
+ return out;
160
+ }
161
+ /**
162
+ * Reset jobs stuck in `active` (e.g. from a crashed worker) back to `pending`
163
+ * if they haven't been touched in `olderThanMs`. Returns the count recovered.
164
+ */
165
+ recover(olderThanMs = 6e4) {
166
+ return this.driver.prepare(
167
+ `UPDATE _jobs SET status='pending', locked_by=NULL, updated_at=?
168
+ WHERE status='active' AND updated_at < ?`
169
+ ).run(now(), now() - olderThanMs).changes;
170
+ }
171
+ /** Stop all workers and wait for in-flight jobs to finish. */
172
+ async close() {
173
+ await Promise.all(this.workers.map((w) => w.stop()));
174
+ }
175
+ /** @internal Atomically claim the next due job, counting the attempt. */
176
+ claimInternal(name) {
177
+ const t = now();
178
+ const row = this.driver.prepare(
179
+ `UPDATE _jobs SET status='active', attempts=attempts+1, locked_by=?, updated_at=?
180
+ WHERE id = (
181
+ SELECT id FROM _jobs
182
+ WHERE queue=? AND status='pending' AND run_at<=?
183
+ ORDER BY priority DESC, id ASC LIMIT 1
184
+ )
185
+ RETURNING *`
186
+ ).get(this.workerId, t, name, t);
187
+ return row ? deserialize(row) : null;
188
+ }
189
+ /** @internal */
190
+ completeInternal(job, result) {
191
+ if (this.removeOnComplete) {
192
+ this.driver.prepare(`DELETE FROM _jobs WHERE id = ?`).run(job.id);
193
+ } else {
194
+ this.driver.prepare(
195
+ `UPDATE _jobs SET status='done', result=?, error=NULL, updated_at=? WHERE id=?`
196
+ ).run(JSON.stringify(result ?? null), now(), job.id);
197
+ }
198
+ }
199
+ /** @internal */
200
+ failInternal(job, err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ if (job.attempts < job.maxAttempts) {
203
+ this.driver.prepare(
204
+ `UPDATE _jobs SET status='pending', run_at=?, error=?, locked_by=NULL, updated_at=? WHERE id=?`
205
+ ).run(now() + this.backoff(job.attempts), message, now(), job.id);
206
+ } else {
207
+ this.driver.prepare(
208
+ `UPDATE _jobs SET status='failed', error=?, updated_at=? WHERE id=?`
209
+ ).run(message, now(), job.id);
210
+ }
211
+ }
212
+ };
213
+ function createQueue(db, opts) {
214
+ return new Queue(db, opts);
215
+ }
216
+
217
+ export { Queue, createQueue };
218
+ //# sourceMappingURL=index.js.map
219
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAwEA,IAAM,OAAA,uBAAc,OAAA,EAAgB;AACpC,IAAM,GAAA,GAAM,MAAM,IAAA,CAAK,GAAA,EAAI;AAC3B,IAAM,cAAA,GAAiB,CAAC,OAAA,KACtB,IAAA,CAAK,IAAI,GAAA,EAAQ,GAAA,GAAO,CAAA,KAAM,OAAA,GAAU,CAAA,CAAE,CAAA;AAE5C,SAAS,YAAY,GAAA,EAAe;AAClC,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,OAAO,GAAA,CAAI,KAAA;AAAA,IACX,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AAAA,IAC/B,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,aAAa,GAAA,CAAI,YAAA;AAAA,IACjB,OAAO,GAAA,CAAI,MAAA;AAAA,IACX,MAAA,EAAQ,IAAI,MAAA,IAAU,IAAA,GAAO,KAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA,GAAI,MAAA;AAAA,IACtD,KAAA,EAAO,IAAI,KAAA,IAAS,MAAA;AAAA,IACpB,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,WAAW,GAAA,CAAI;AAAA,GACjB;AACF;AAEA,IAAM,aAAN,MAAmC;AAAA,EAQjC,WAAA,CACmB,CAAA,EACR,IAAA,EACQ,OAAA,EACjB,IAAA,EACA;AAJiB,IAAA,IAAA,CAAA,CAAA,GAAA,CAAA;AACR,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACQ,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAGjB,IAAA,IAAA,CAAK,cAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,eAAe,CAAC,CAAA;AACpD,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,YAAA,IAAgB,GAAA;AACzC,IAAA,IAAA,CAAK,IAAA,EAAK;AAAA,EACZ;AAAA,EARmB,CAAA;AAAA,EACR,IAAA;AAAA,EACQ,OAAA;AAAA,EAVX,OAAA,GAAU,IAAA;AAAA,EACV,QAAA,GAAW,CAAA;AAAA,EACX,KAAA;AAAA,EACA,YAAA;AAAA,EACS,WAAA;AAAA,EACA,YAAA;AAAA;AAAA,EAcjB,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACnB,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,YAAA,CAAa,KAAK,KAAK,CAAA;AACvB,MAAA,IAAA,CAAK,KAAA,GAAQ,MAAA;AAAA,IACf;AACA,IAAA,OAAO,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,QAAA,GAAW,KAAK,WAAA,EAAa;AACvD,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,CAAA,CAAE,aAAA,CAAc,KAAK,IAAI,CAAA;AAC1C,MAAA,IAAI,CAAC,GAAA,EAAK;AACV,MAAA,IAAA,CAAK,QAAA,EAAA;AACL,MAAA,OAAA,CAAQ,OAAA,GACL,IAAA,CAAK,MAAM,KAAK,OAAA,CAAQ,GAAG,CAAC,CAAA,CAC5B,IAAA;AAAA,QACC,CAAC,MAAA,KAAW;AACV,UAAA,IAAA,CAAK,CAAA,CAAE,gBAAA,CAAiB,GAAA,EAAK,MAAM,CAAA;AACnC,UAAA,IAAA,CAAK,CAAA,CAAE,IAAA,CAAK,WAAA,EAAa,GAAA,EAAK,MAAM,CAAA;AAAA,QACtC,CAAA;AAAA,QACA,CAAC,GAAA,KAAQ;AACP,UAAA,IAAA,CAAK,CAAA,CAAE,YAAA,CAAa,GAAA,EAAK,GAAG,CAAA;AAC5B,UAAA,IAAA,CAAK,CAAA,CAAE,IAAA,CAAK,QAAA,EAAU,GAAA,EAAK,GAAG,CAAA;AAAA,QAChC;AAAA,OACF,CACC,QAAQ,MAAM;AACb,QAAA,IAAA,CAAK,QAAA,EAAA;AACL,QAAA,IAAI,IAAA,CAAK,OAAA,EAAS,IAAA,CAAK,IAAA,EAAK;AAAA,kBAClB,YAAA,EAAa;AAAA,MACzB,CAAC,CAAA;AAAA,IACL;AACA,IAAA,IAAI,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,QAAA,GAAW,KAAK,WAAA,EAAa;AACpD,MAAA,IAAA,CAAK,QAAQ,UAAA,CAAW,MAAM,KAAK,IAAA,EAAK,EAAG,KAAK,YAAY,CAAA;AAC5D,MAAA,IAAA,CAAK,MAAM,KAAA,IAAQ;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,YAAA,GAAqB;AAC3B,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,KAAK,QAAA,KAAa,CAAA,IAAK,KAAK,YAAA,EAAc;AAC7D,MAAA,IAAA,CAAK,YAAA,EAAa;AAClB,MAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAI,IAAA,CAAK,KAAA,EAAO,YAAA,CAAa,IAAA,CAAK,KAAK,CAAA;AACvC,IAAA,IAAI,IAAA,CAAK,aAAa,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,MAAA,IAAA,CAAK,YAAA,GAAe,OAAA;AAAA,IACtB,CAAC,CAAA;AAAA,EACH;AACF,CAAA;AAOO,IAAM,KAAA,GAAN,cAAoB,YAAA,CAAa;AAAA,EACrB,MAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,gBAAA;AAAA,EACR,QAAA;AAAA,EACQ,UAAwB,EAAC;AAAA,EAE1C,WAAA,CAAY,EAAA,EAAa,IAAA,GAAqB,EAAC,EAAG;AAChD,IAAA,KAAA,EAAM;AACN,IAAA,IAAA,CAAK,SAAS,EAAA,CAAG,MAAA;AACjB,IAAA,IAAA,CAAK,WAAA,GAAc,KAAK,WAAA,IAAe,CAAA;AACvC,IAAA,IAAA,CAAK,OAAA,GAAU,KAAK,OAAA,IAAW,cAAA;AAC/B,IAAA,IAAA,CAAK,gBAAA,GAAmB,KAAK,gBAAA,IAAoB,KAAA;AACjD,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,QAAA,IAAY,CAAA,EAAA,EAAK,QAAQ,GAAG,CAAA,CAAA;AAEjD,IAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACpB,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAA;AAAA,OAOF;AACA,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,CAAA,iFAAA;AAAA,OACF;AACA,MAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,GAAA,CAAa,IAAA,EAAc,OAAA,EAAY,IAAA,GAAmB,EAAC,EAAW;AACpE,IAAA,MAAM,IAAI,GAAA,EAAI;AACd,IAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,KAAU,KAAK,KAAA,GAAQ,CAAA,GAAI,KAAK,KAAA,GAAQ,CAAA,CAAA;AAC3D,IAAA,MAAM,IAAA,GAAO,KAAK,MAAA,CACf,OAAA;AAAA,MACC,CAAA;AAAA,mDAAA;AAAA,KAEF,CACC,GAAA;AAAA,MACC,IAAA;AAAA,MACA,KAAK,QAAA,IAAY,CAAA;AAAA,MACjB,KAAA;AAAA,MACA,IAAA,CAAK,eAAe,IAAA,CAAK,WAAA;AAAA,MACzB,IAAA,CAAK,SAAA,CAAU,OAAA,IAAW,IAAI,CAAA;AAAA,MAC9B,CAAA;AAAA,MACA;AAAA,KACF;AACF,IAAA,MAAM,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,eAAe,CAAC,CAAA;AACpD,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,OAAA,EAAS,IAAI,EAAE,IAAA,KAAS,IAAA,IAAQ,IAAA,EAAK;AAC1D,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAA,CACE,IAAA,EACA,OAAA,EACA,IAAA,GAAuB,EAAC,EAChB;AACR,IAAA,MAAM,IAAI,IAAI,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,SAAoB,IAAI,CAAA;AAC7D,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAC,CAAA;AACnB,IAAA,OAAO,CAAA;AAAA,EACT;AAAA,EAEA,OAAgB,EAAA,EAAgC;AAC9C,IAAA,MAAM,MAAM,IAAA,CAAK,MAAA,CACd,QAAQ,CAAA,gCAAA,CAAkC,CAAA,CAC1C,IAAI,EAAE,CAAA;AACT,IAAA,OAAO,GAAA,GAAO,WAAA,CAAY,GAAG,CAAA,GAAe,MAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,OAAO,IAAA,EAA0C;AAC/C,IAAA,MAAM,IAAA,GACJ,IAAA,GACI,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,MACC,CAAA,uEAAA;AAAA,KACF,CACC,IAAI,IAAI,CAAA,GACX,KAAK,MAAA,CACF,OAAA,CAAQ,CAAA,uDAAA,CAAyD,CAAA,CACjE,GAAA,EAAI;AAEb,IAAA,MAAM,GAAA,GAAiC;AAAA,MACrC,OAAA,EAAS,CAAA;AAAA,MACT,MAAA,EAAQ,CAAA;AAAA,MACR,IAAA,EAAM,CAAA;AAAA,MACN,MAAA,EAAQ;AAAA,KACV;AACA,IAAA,KAAA,MAAW,KAAK,IAAA,EAAM,GAAA,CAAI,CAAA,CAAE,MAAM,IAAI,CAAA,CAAE,CAAA;AACxC,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAA,CAAQ,cAAc,GAAA,EAAgB;AACpC,IAAA,OAAO,KAAK,MAAA,CACT,OAAA;AAAA,MACC,CAAA;AAAA,iDAAA;AAAA,MAGD,GAAA,CAAI,GAAA,IAAO,GAAA,EAAI,GAAI,WAAW,CAAA,CAAE,OAAA;AAAA,EACrC;AAAA;AAAA,EAGA,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAC,CAAA;AAAA,EACrD;AAAA;AAAA,EAGA,cAAc,IAAA,EAA0B;AACtC,IAAA,MAAM,IAAI,GAAA,EAAI;AACd,IAAA,MAAM,GAAA,GAAM,KAAK,MAAA,CACd,OAAA;AAAA,MACC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA;AAAA,MAQD,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,CAAA,EAAG,MAAM,CAAC,CAAA;AAChC,IAAA,OAAO,GAAA,GAAM,WAAA,CAAY,GAAG,CAAA,GAAI,IAAA;AAAA,EAClC;AAAA;AAAA,EAGA,gBAAA,CAAiB,KAAU,MAAA,EAAuB;AAChD,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,CAAA,8BAAA,CAAgC,CAAA,CAAE,GAAA,CAAI,IAAI,EAAE,CAAA;AAAA,IAClE,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,6EAAA;AAAA,OACF,CACC,GAAA,CAAI,IAAA,CAAK,SAAA,CAAU,MAAA,IAAU,IAAI,CAAA,EAAG,GAAA,EAAI,EAAG,GAAA,CAAI,EAAE,CAAA;AAAA,IACtD;AAAA,EACF;AAAA;AAAA,EAGA,YAAA,CAAa,KAAU,GAAA,EAAoB;AAEzC,IAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,IAAA,IAAI,GAAA,CAAI,QAAA,GAAW,GAAA,CAAI,WAAA,EAAa;AAClC,MAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,6FAAA;AAAA,OACF,CACC,GAAA,CAAI,GAAA,EAAI,GAAI,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,EAAG,OAAA,EAAS,GAAA,EAAI,EAAG,IAAI,EAAE,CAAA;AAAA,IACnE,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,MAAA,CACF,OAAA;AAAA,QACC,CAAA,kEAAA;AAAA,QAED,GAAA,CAAI,OAAA,EAAS,GAAA,EAAI,EAAG,IAAI,EAAE,CAAA;AAAA,IAC/B;AAAA,EACF;AACF;AAGO,SAAS,WAAA,CAAY,IAAa,IAAA,EAA4B;AACnE,EAAA,OAAO,IAAI,KAAA,CAAM,EAAA,EAAI,IAAI,CAAA;AAC3B","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 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 /** 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\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 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 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\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.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 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 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.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 this.workerId = opts.workerId ?? `w-${process.pid}`;\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,\n created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL\n )`,\n );\n this.driver.exec(\n `CREATE INDEX IF NOT EXISTS _jobs_claim ON _jobs (queue, status, priority, run_at)`,\n );\n ensured.add(db);\n }\n }\n\n /** Enqueue a job. */\n add<T = any>(name: string, payload: T, opts: AddOptions = {}): Job<T> {\n const t = now();\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, 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 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 /** 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"]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@monlite/queue",
3
+ "version": "0.1.0",
4
+ "description": "Durable job queue for @monlite/core: retries, backoff, delayed jobs, concurrency — on SQLite.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "keywords": [
26
+ "monlite",
27
+ "queue",
28
+ "jobs",
29
+ "worker",
30
+ "bullmq",
31
+ "tasks",
32
+ "sqlite"
33
+ ],
34
+ "license": "MIT",
35
+ "author": "Emad Jumaah <emadjumaah@gmail.com>",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/qataruts/monlite.git",
39
+ "directory": "packages/kv"
40
+ },
41
+ "homepage": "https://github.com/qataruts/monlite/tree/main/packages/kv#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/qataruts/monlite/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "dependencies": {
52
+ "@monlite/core": "^1.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.10.0",
56
+ "better-sqlite3": "^11.8.0",
57
+ "tsup": "^8.3.5",
58
+ "typescript": "^5.7.2",
59
+ "vitest": "^2.1.8"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "test": "vitest run",
64
+ "typecheck": "tsc --noEmit"
65
+ }
66
+ }