onequeue 0.0.1 → 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/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # ⚡ OneQueue
2
+
3
+ **Background jobs in one line. Production-ready by default.**
4
+
5
+ OneQueue is a modern background job framework for Node.js that removes the pain of queues, workers, retries, and scheduling. Define jobs in one line and let OneQueue handle the rest.
6
+
7
+ ---
8
+
9
+ ## ✨ Why OneQueue?
10
+
11
+ Traditional job queues require:
12
+
13
+ - Redis setup
14
+ - worker wiring
15
+ - retry plumbing
16
+ - cron configuration
17
+ - dashboard setup
18
+
19
+ **OneQueue gives you all of this with zero config.**
20
+
21
+ ---
22
+
23
+ ## 🚀 Quick Start (30 seconds)
24
+
25
+ ### Install
26
+
27
+ ```bash
28
+ npm install onequeue
29
+ ```
30
+
31
+ ### Define a job
32
+
33
+ ```javascript
34
+ import { job } from "onequeue";
35
+
36
+ job("sendWelcomeEmail", async (user) => {
37
+ await email.send(user.email);
38
+ });
39
+ ```
40
+
41
+ ### Enqueue work
42
+
43
+ ```javascript
44
+ import { enqueue } from "onequeue";
45
+
46
+ await enqueue("sendWelcomeEmail", {
47
+ email: "raj@example.com",
48
+ });
49
+ ```
50
+
51
+ ### Done. Background processing is live.
52
+
53
+ ## 🧠 Features
54
+
55
+ - ⚡ One-line job definition
56
+ - 🔁 Automatic retries with backoff
57
+ - ⏱️ Human-friendly delays ("10s", "5m")
58
+ - 💾 SQLite persistence (jobs survive restarts)
59
+ - 🧵 Concurrency control
60
+ - 📊 Live dev dashboard
61
+ - 🛑 Graceful shutdown
62
+ - 🔒 Zero-config by default
63
+ - 🧩 TypeScript-first
64
+
65
+ ## ⏳ Delayed Jobs
66
+
67
+ ```javascript
68
+ await enqueue("sendEmail", payload, {
69
+ delay: "10s",
70
+ });
71
+ ```
72
+
73
+ Supports:
74
+
75
+ - `"500ms"`
76
+ - `"10s"`
77
+ - `"5m"`
78
+ - `"1h"`
79
+
80
+ ## 🔁 Retries
81
+
82
+ ```javascript
83
+ job("unstableTask", handler, {
84
+ retries: 3,
85
+ backoffMs: 1000,
86
+ });
87
+ ```
88
+
89
+ OneQueue automatically retries failed jobs with exponential backoff.
90
+
91
+ ## 🧵 Concurrency
92
+
93
+ ```javascript
94
+ import { configure } from "onequeue";
95
+
96
+ configure({ concurrency: 5 });
97
+ ```
98
+
99
+ ## 📊 Dev Dashboard
100
+
101
+ Run locally:
102
+
103
+ ```bash
104
+ npx onequeue dev
105
+ ```
106
+
107
+ Open:
108
+
109
+ ```
110
+ http://localhost:3210
111
+ ```
112
+
113
+ Monitor:
114
+
115
+ - queued jobs
116
+ - running jobs
117
+ - completed jobs
118
+ - failed jobs
119
+
120
+ Live updates included.
121
+
122
+ ## 🛑 Graceful Shutdown
123
+
124
+ OneQueue automatically handles:
125
+
126
+ - SIGINT
127
+ - SIGTERM
128
+ - draining active jobs
129
+ - clean worker exit
130
+
131
+ Safe for deploy restarts and containers.
132
+
133
+ ## 🏗️ Philosophy
134
+
135
+ OneQueue is built around a simple idea:
136
+
137
+ Background jobs should be boring to set up and reliable by default.
138
+
139
+ The goal is to provide Express-level simplicity for background processing while remaining production-capable.
140
+
141
+ ## 🚧 Roadmap
142
+
143
+ - [ ] Redis adapter
144
+ - [ ] Distributed workers
145
+ - [ ] Job priorities
146
+ - [ ] Rate limiting
147
+ - [ ] Production dashboard
148
+
149
+ ## 🤝 Contributing
150
+
151
+ PRs and feedback are welcome. If you build something cool with OneQueue, open an issue or share it.
152
+
153
+ ## 📄 License
154
+
155
+ MIT
@@ -0,0 +1,21 @@
1
+ // src/db.ts
2
+ import Database from "better-sqlite3";
3
+ var db = new Database("onequeue.db");
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS jobs (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ name TEXT NOT NULL,
8
+ payload TEXT NOT NULL,
9
+ attempts INTEGER NOT NULL,
10
+ runAt INTEGER NOT NULL,
11
+ status TEXT NOT NULL DEFAULT 'queued',
12
+ startedAt INTEGER,
13
+ finishedAt INTEGER,
14
+ error TEXT
15
+ );
16
+ `);
17
+ var db_default = db;
18
+
19
+ export {
20
+ db_default
21
+ };
@@ -0,0 +1,17 @@
1
+ // src/db.ts
2
+ import Database from "better-sqlite3";
3
+ var db = new Database("onequeue.db");
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS jobs (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ name TEXT NOT NULL,
8
+ payload TEXT NOT NULL,
9
+ attempts INTEGER NOT NULL,
10
+ runAt INTEGER NOT NULL
11
+ );
12
+ `);
13
+ var db_default = db;
14
+
15
+ export {
16
+ db_default
17
+ };
package/dist/cli.cjs ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/dev.ts
27
+ var import_node_http = __toESM(require("http"), 1);
28
+
29
+ // src/db.ts
30
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
31
+ var db = new import_better_sqlite3.default("onequeue.db");
32
+ db.exec(`
33
+ CREATE TABLE IF NOT EXISTS jobs (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ name TEXT NOT NULL,
36
+ payload TEXT NOT NULL,
37
+ attempts INTEGER NOT NULL,
38
+ runAt INTEGER NOT NULL,
39
+ status TEXT NOT NULL DEFAULT 'queued',
40
+ startedAt INTEGER,
41
+ finishedAt INTEGER,
42
+ error TEXT
43
+ );
44
+ `);
45
+ var db_default = db;
46
+
47
+ // src/dev.ts
48
+ var PORT = 3210;
49
+ function getStats() {
50
+ const total = db_default.prepare(`SELECT COUNT(*) as c FROM jobs`).get();
51
+ const scheduled = db_default.prepare(`SELECT COUNT(*) as c FROM jobs WHERE runAt > ?`).get(Date.now());
52
+ return {
53
+ total: total.c,
54
+ scheduled: scheduled.c
55
+ };
56
+ }
57
+ function getJobs() {
58
+ return db_default.prepare(
59
+ `
60
+ SELECT id, name, attempts, runAt, status
61
+ FROM jobs
62
+ ORDER BY id DESC
63
+ LIMIT 50
64
+ `
65
+ ).all();
66
+ }
67
+ var server = import_node_http.default.createServer((req, res) => {
68
+ if (req.url === "/api/jobs") {
69
+ res.setHeader("Content-Type", "application/json");
70
+ res.end(JSON.stringify(getJobs()));
71
+ return;
72
+ }
73
+ if (req.url === "/api/stats") {
74
+ res.setHeader("Content-Type", "application/json");
75
+ res.end(JSON.stringify(getStats()));
76
+ return;
77
+ }
78
+ res.setHeader("Content-Type", "text/html");
79
+ res.end(`
80
+ <!DOCTYPE html>
81
+ <html>
82
+ <head>
83
+ <meta charset="UTF-8" />
84
+ <title>OneQueue Dev</title>
85
+ <style>
86
+ :root {
87
+ --bg: #0b0f17;
88
+ --card: #121826;
89
+ --border: #1f2937;
90
+ --text: #e5e7eb;
91
+ --muted: #9ca3af;
92
+ --accent: #6366f1;
93
+ --accent2: #22c55e;
94
+ }
95
+
96
+ * { box-sizing: border-box; }
97
+
98
+ body {
99
+ margin: 0;
100
+ font-family: ui-sans-serif, system-ui, -apple-system;
101
+ background: radial-gradient(circle at top, #111827, #020617);
102
+ color: var(--text);
103
+ padding: 40px;
104
+ }
105
+
106
+ .container {
107
+ max-width: 1100px;
108
+ margin: 0 auto;
109
+ }
110
+
111
+ .header {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: space-between;
115
+ margin-bottom: 28px;
116
+ }
117
+
118
+ .title {
119
+ font-size: 32px;
120
+ font-weight: 700;
121
+ letter-spacing: -0.02em;
122
+ }
123
+
124
+ .live {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 8px;
128
+ font-size: 14px;
129
+ color: var(--muted);
130
+ }
131
+
132
+ .dot {
133
+ width: 10px;
134
+ height: 10px;
135
+ background: var(--accent2);
136
+ border-radius: 50%;
137
+ box-shadow: 0 0 12px var(--accent2);
138
+ animation: pulse 1.5s infinite;
139
+ }
140
+
141
+ @keyframes pulse {
142
+ 0% { opacity: 1; }
143
+ 50% { opacity: 0.4; }
144
+ 100% { opacity: 1; }
145
+ }
146
+
147
+ .stats {
148
+ display: grid;
149
+ grid-template-columns: repeat(2, 1fr);
150
+ gap: 16px;
151
+ margin-bottom: 28px;
152
+ }
153
+
154
+ .card {
155
+ background: linear-gradient(180deg, #121826, #0f172a);
156
+ border: 1px solid var(--border);
157
+ border-radius: 16px;
158
+ padding: 20px;
159
+ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
160
+ }
161
+
162
+ .stat-label {
163
+ color: var(--muted);
164
+ font-size: 13px;
165
+ margin-bottom: 6px;
166
+ }
167
+
168
+ .stat-value {
169
+ font-size: 28px;
170
+ font-weight: 700;
171
+ }
172
+
173
+ .table-card h3 {
174
+ margin-top: 0;
175
+ margin-bottom: 14px;
176
+ }
177
+
178
+ table {
179
+ width: 100%;
180
+ border-collapse: collapse;
181
+ font-size: 14px;
182
+ }
183
+
184
+ th {
185
+ text-align: left;
186
+ color: var(--muted);
187
+ font-weight: 500;
188
+ padding: 10px 8px;
189
+ border-bottom: 1px solid var(--border);
190
+ }
191
+
192
+ td {
193
+ padding: 12px 8px;
194
+ border-bottom: 1px solid #111827;
195
+ }
196
+
197
+ tr:hover td {
198
+ background: rgba(99,102,241,0.06);
199
+ }
200
+
201
+ .footer {
202
+ margin-top: 18px;
203
+ font-size: 12px;
204
+ color: var(--muted);
205
+ text-align: right;
206
+ }
207
+ </style>
208
+ </head>
209
+ <body>
210
+ <div class="container">
211
+ <div class="header">
212
+ <div class="title">\u26A1 OneQueue Dev</div>
213
+ <div class="live">
214
+ <div class="dot"></div>
215
+ live
216
+ </div>
217
+ </div>
218
+
219
+ <div class="stats">
220
+ <div class="card">
221
+ <div class="stat-label">Total Jobs</div>
222
+ <div id="total" class="stat-value">\u2014</div>
223
+ </div>
224
+ <div class="card">
225
+ <div class="stat-label">Scheduled</div>
226
+ <div id="scheduled" class="stat-value">\u2014</div>
227
+ </div>
228
+ </div>
229
+
230
+ <div class="card table-card">
231
+ <h3>Recent Jobs</h3>
232
+ <table id="jobs"></table>
233
+ </div>
234
+
235
+ <div class="footer">
236
+ OneQueue Dev Dashboard
237
+ </div>
238
+ </div>
239
+
240
+ <script>
241
+ async function load() {
242
+ const stats = await fetch('/api/stats').then(r => r.json());
243
+ const jobs = await fetch('/api/jobs').then(r => r.json());
244
+
245
+ document.getElementById('total').textContent = stats.total;
246
+ document.getElementById('scheduled').textContent = stats.scheduled;
247
+
248
+ const table = document.getElementById('jobs');
249
+ table.innerHTML =
250
+ '<tr><th>ID</th><th>Name</th><th>Status</th><th>Attempts</th><th>Run At</th></tr>' +
251
+ jobs.map(j =>
252
+ '<tr>' +
253
+ '<td>' + j.id + '</td>' +
254
+ '<td>' + j.name + '</td>' +
255
+ '<td>' + j.status + '</td>' +
256
+ '<td>' + j.attempts + '</td>' +
257
+ '<td>' + new Date(j.runAt).toLocaleTimeString() + '</td>' +
258
+ '</tr>'
259
+ ).join('');
260
+ }
261
+
262
+ load();
263
+ setInterval(load, 1000);
264
+ </script>
265
+ </body>
266
+ </html>
267
+ `);
268
+ });
269
+ function startDevServer() {
270
+ server.listen(PORT, () => {
271
+ console.log(`\u26A1 OneQueue Dev running at http://localhost:${PORT}`);
272
+ });
273
+ }
274
+
275
+ // src/cli.ts
276
+ var cmd = process.argv[2];
277
+ if (cmd === "dev") {
278
+ startDevServer();
279
+ } else {
280
+ console.log("OneQueue CLI");
281
+ }
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ db_default
4
+ } from "./chunk-CKCBPWXR.js";
5
+
6
+ // src/dev.ts
7
+ import http from "http";
8
+ var PORT = 3210;
9
+ function getStats() {
10
+ const total = db_default.prepare(`SELECT COUNT(*) as c FROM jobs`).get();
11
+ const scheduled = db_default.prepare(`SELECT COUNT(*) as c FROM jobs WHERE runAt > ?`).get(Date.now());
12
+ return {
13
+ total: total.c,
14
+ scheduled: scheduled.c
15
+ };
16
+ }
17
+ function getJobs() {
18
+ return db_default.prepare(
19
+ `
20
+ SELECT id, name, attempts, runAt, status
21
+ FROM jobs
22
+ ORDER BY id DESC
23
+ LIMIT 50
24
+ `
25
+ ).all();
26
+ }
27
+ var server = http.createServer((req, res) => {
28
+ if (req.url === "/api/jobs") {
29
+ res.setHeader("Content-Type", "application/json");
30
+ res.end(JSON.stringify(getJobs()));
31
+ return;
32
+ }
33
+ if (req.url === "/api/stats") {
34
+ res.setHeader("Content-Type", "application/json");
35
+ res.end(JSON.stringify(getStats()));
36
+ return;
37
+ }
38
+ res.setHeader("Content-Type", "text/html");
39
+ res.end(`
40
+ <!DOCTYPE html>
41
+ <html>
42
+ <head>
43
+ <meta charset="UTF-8" />
44
+ <title>OneQueue Dev</title>
45
+ <style>
46
+ :root {
47
+ --bg: #0b0f17;
48
+ --card: #121826;
49
+ --border: #1f2937;
50
+ --text: #e5e7eb;
51
+ --muted: #9ca3af;
52
+ --accent: #6366f1;
53
+ --accent2: #22c55e;
54
+ }
55
+
56
+ * { box-sizing: border-box; }
57
+
58
+ body {
59
+ margin: 0;
60
+ font-family: ui-sans-serif, system-ui, -apple-system;
61
+ background: radial-gradient(circle at top, #111827, #020617);
62
+ color: var(--text);
63
+ padding: 40px;
64
+ }
65
+
66
+ .container {
67
+ max-width: 1100px;
68
+ margin: 0 auto;
69
+ }
70
+
71
+ .header {
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: space-between;
75
+ margin-bottom: 28px;
76
+ }
77
+
78
+ .title {
79
+ font-size: 32px;
80
+ font-weight: 700;
81
+ letter-spacing: -0.02em;
82
+ }
83
+
84
+ .live {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 8px;
88
+ font-size: 14px;
89
+ color: var(--muted);
90
+ }
91
+
92
+ .dot {
93
+ width: 10px;
94
+ height: 10px;
95
+ background: var(--accent2);
96
+ border-radius: 50%;
97
+ box-shadow: 0 0 12px var(--accent2);
98
+ animation: pulse 1.5s infinite;
99
+ }
100
+
101
+ @keyframes pulse {
102
+ 0% { opacity: 1; }
103
+ 50% { opacity: 0.4; }
104
+ 100% { opacity: 1; }
105
+ }
106
+
107
+ .stats {
108
+ display: grid;
109
+ grid-template-columns: repeat(2, 1fr);
110
+ gap: 16px;
111
+ margin-bottom: 28px;
112
+ }
113
+
114
+ .card {
115
+ background: linear-gradient(180deg, #121826, #0f172a);
116
+ border: 1px solid var(--border);
117
+ border-radius: 16px;
118
+ padding: 20px;
119
+ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
120
+ }
121
+
122
+ .stat-label {
123
+ color: var(--muted);
124
+ font-size: 13px;
125
+ margin-bottom: 6px;
126
+ }
127
+
128
+ .stat-value {
129
+ font-size: 28px;
130
+ font-weight: 700;
131
+ }
132
+
133
+ .table-card h3 {
134
+ margin-top: 0;
135
+ margin-bottom: 14px;
136
+ }
137
+
138
+ table {
139
+ width: 100%;
140
+ border-collapse: collapse;
141
+ font-size: 14px;
142
+ }
143
+
144
+ th {
145
+ text-align: left;
146
+ color: var(--muted);
147
+ font-weight: 500;
148
+ padding: 10px 8px;
149
+ border-bottom: 1px solid var(--border);
150
+ }
151
+
152
+ td {
153
+ padding: 12px 8px;
154
+ border-bottom: 1px solid #111827;
155
+ }
156
+
157
+ tr:hover td {
158
+ background: rgba(99,102,241,0.06);
159
+ }
160
+
161
+ .footer {
162
+ margin-top: 18px;
163
+ font-size: 12px;
164
+ color: var(--muted);
165
+ text-align: right;
166
+ }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <div class="container">
171
+ <div class="header">
172
+ <div class="title">\u26A1 OneQueue Dev</div>
173
+ <div class="live">
174
+ <div class="dot"></div>
175
+ live
176
+ </div>
177
+ </div>
178
+
179
+ <div class="stats">
180
+ <div class="card">
181
+ <div class="stat-label">Total Jobs</div>
182
+ <div id="total" class="stat-value">\u2014</div>
183
+ </div>
184
+ <div class="card">
185
+ <div class="stat-label">Scheduled</div>
186
+ <div id="scheduled" class="stat-value">\u2014</div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="card table-card">
191
+ <h3>Recent Jobs</h3>
192
+ <table id="jobs"></table>
193
+ </div>
194
+
195
+ <div class="footer">
196
+ OneQueue Dev Dashboard
197
+ </div>
198
+ </div>
199
+
200
+ <script>
201
+ async function load() {
202
+ const stats = await fetch('/api/stats').then(r => r.json());
203
+ const jobs = await fetch('/api/jobs').then(r => r.json());
204
+
205
+ document.getElementById('total').textContent = stats.total;
206
+ document.getElementById('scheduled').textContent = stats.scheduled;
207
+
208
+ const table = document.getElementById('jobs');
209
+ table.innerHTML =
210
+ '<tr><th>ID</th><th>Name</th><th>Status</th><th>Attempts</th><th>Run At</th></tr>' +
211
+ jobs.map(j =>
212
+ '<tr>' +
213
+ '<td>' + j.id + '</td>' +
214
+ '<td>' + j.name + '</td>' +
215
+ '<td>' + j.status + '</td>' +
216
+ '<td>' + j.attempts + '</td>' +
217
+ '<td>' + new Date(j.runAt).toLocaleTimeString() + '</td>' +
218
+ '</tr>'
219
+ ).join('');
220
+ }
221
+
222
+ load();
223
+ setInterval(load, 1000);
224
+ </script>
225
+ </body>
226
+ </html>
227
+ `);
228
+ });
229
+ function startDevServer() {
230
+ server.listen(PORT, () => {
231
+ console.log(`\u26A1 OneQueue Dev running at http://localhost:${PORT}`);
232
+ });
233
+ }
234
+
235
+ // src/cli.ts
236
+ var cmd = process.argv[2];
237
+ if (cmd === "dev") {
238
+ startDevServer();
239
+ } else {
240
+ console.log("OneQueue CLI");
241
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ configure: () => configure,
34
+ enqueue: () => enqueue,
35
+ job: () => job,
36
+ shutdown: () => shutdown
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/db.ts
41
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
42
+ var db = new import_better_sqlite3.default("onequeue.db");
43
+ db.exec(`
44
+ CREATE TABLE IF NOT EXISTS jobs (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ name TEXT NOT NULL,
47
+ payload TEXT NOT NULL,
48
+ attempts INTEGER NOT NULL,
49
+ runAt INTEGER NOT NULL,
50
+ status TEXT NOT NULL DEFAULT 'queued',
51
+ startedAt INTEGER,
52
+ finishedAt INTEGER,
53
+ error TEXT
54
+ );
55
+ `);
56
+ var db_default = db;
57
+
58
+ // src/index.ts
59
+ var registry = /* @__PURE__ */ new Map();
60
+ var isWorkerRunning = false;
61
+ var concurrency = 1;
62
+ var activeCount = 0;
63
+ var isShuttingDown = false;
64
+ function job(name, handler, options = {}) {
65
+ if (registry.has(name)) {
66
+ throw new Error(`Job "${name}" is already registered`);
67
+ }
68
+ registry.set(name, {
69
+ handler,
70
+ options: {
71
+ retries: options.retries ?? 0,
72
+ backoffMs: options.backoffMs ?? 1e3
73
+ }
74
+ });
75
+ startWorker();
76
+ }
77
+ async function enqueue(name, payload, options = {}) {
78
+ if (!registry.has(name)) {
79
+ throw new Error(`Job "${name}" is not registered`);
80
+ }
81
+ const delayMs = parseDelay(options.delay);
82
+ db_default.prepare(
83
+ `INSERT INTO jobs (name, payload, attempts, runAt, status)
84
+ VALUES (?, ?, ?, ?, 'queued')`
85
+ ).run(name, JSON.stringify(payload), 0, Date.now() + delayMs);
86
+ }
87
+ function configure(options) {
88
+ if (options.concurrency && options.concurrency > 0) {
89
+ concurrency = options.concurrency;
90
+ }
91
+ }
92
+ function startWorker() {
93
+ if (isWorkerRunning) return;
94
+ isWorkerRunning = true;
95
+ setInterval(processQueue, 50);
96
+ }
97
+ async function processQueue() {
98
+ if (isShuttingDown) return;
99
+ if (activeCount >= concurrency) return;
100
+ const row = db_default.prepare(
101
+ `SELECT * FROM jobs
102
+ WHERE status = 'queued'
103
+ AND runAt <= ?
104
+ ORDER BY id ASC
105
+ LIMIT 1`
106
+ ).get(Date.now());
107
+ if (!row) return;
108
+ const def = registry.get(row.name);
109
+ if (!def) return;
110
+ db_default.prepare(
111
+ `UPDATE jobs
112
+ SET status = 'running',
113
+ startedAt = ?
114
+ WHERE id = ?`
115
+ ).run(Date.now(), row.id);
116
+ activeCount++;
117
+ try {
118
+ await def.handler(JSON.parse(row.payload));
119
+ db_default.prepare(
120
+ `UPDATE jobs
121
+ SET status = 'completed',
122
+ finishedAt = ?
123
+ WHERE id = ?`
124
+ ).run(Date.now(), row.id);
125
+ } catch (err) {
126
+ await handleRetry(row, def);
127
+ } finally {
128
+ activeCount--;
129
+ }
130
+ }
131
+ async function handleRetry(row, def) {
132
+ const attempts = row.attempts + 1;
133
+ const { retries, backoffMs } = def.options;
134
+ if (attempts > retries) {
135
+ db_default.prepare(
136
+ `UPDATE jobs
137
+ SET status = 'failed',
138
+ finishedAt = ?,
139
+ error = ?
140
+ WHERE id = ?`
141
+ ).run(Date.now(), String(row.error ?? "Unknown error"), row.id);
142
+ return;
143
+ }
144
+ const delay = backoffMs * attempts;
145
+ db_default.prepare(
146
+ `UPDATE jobs
147
+ SET attempts = ?,
148
+ runAt = ?,
149
+ status = 'queued'
150
+ WHERE id = ?`
151
+ ).run(attempts, Date.now() + delay, row.id);
152
+ }
153
+ function parseDelay(delay) {
154
+ if (!delay) return 0;
155
+ if (typeof delay === "number") return delay;
156
+ const match = /^(\d+)(ms|s|m|h)?$/.exec(delay.trim());
157
+ if (!match) {
158
+ throw new Error(`[OneQueue] Invalid delay format: ${delay}`);
159
+ }
160
+ const value = Number(match[1]);
161
+ const unit = match[2] ?? "ms";
162
+ switch (unit) {
163
+ case "ms":
164
+ return value;
165
+ case "s":
166
+ return value * 1e3;
167
+ case "m":
168
+ return value * 6e4;
169
+ case "h":
170
+ return value * 36e5;
171
+ default:
172
+ return value;
173
+ }
174
+ }
175
+ async function shutdown() {
176
+ if (isShuttingDown) return;
177
+ console.log("[OneQueue] Graceful shutdown started\u2026");
178
+ isShuttingDown = true;
179
+ while (activeCount > 0) {
180
+ await new Promise((r) => setTimeout(r, 50));
181
+ }
182
+ console.log("[OneQueue] All jobs finished. Exiting cleanly.");
183
+ }
184
+ function setupSignalHandlers() {
185
+ const handler = async () => {
186
+ await shutdown();
187
+ process.exit(0);
188
+ };
189
+ process.once("SIGINT", handler);
190
+ process.once("SIGTERM", handler);
191
+ }
192
+ setupSignalHandlers();
193
+ // Annotate the CommonJS export names for ESM import in node:
194
+ 0 && (module.exports = {
195
+ configure,
196
+ enqueue,
197
+ job,
198
+ shutdown
199
+ });
@@ -0,0 +1,25 @@
1
+ type JobHandler<T = any> = (payload: T) => Promise<void> | void;
2
+ type JobOptions = {
3
+ retries?: number;
4
+ backoffMs?: number;
5
+ };
6
+ type EnqueueOptions = {
7
+ delay?: number | string;
8
+ };
9
+ /**
10
+ * Register a job
11
+ */
12
+ declare function job<T = any>(name: string, handler: JobHandler<T>, options?: JobOptions): void;
13
+ /**
14
+ * Enqueue job (persistent)
15
+ */
16
+ declare function enqueue<T = any>(name: string, payload: T, options?: EnqueueOptions): Promise<void>;
17
+ /**
18
+ * Configure
19
+ */
20
+ declare function configure(options: {
21
+ concurrency?: number;
22
+ }): void;
23
+ declare function shutdown(): Promise<void>;
24
+
25
+ export { configure, enqueue, job, shutdown };
@@ -0,0 +1,25 @@
1
+ type JobHandler<T = any> = (payload: T) => Promise<void> | void;
2
+ type JobOptions = {
3
+ retries?: number;
4
+ backoffMs?: number;
5
+ };
6
+ type EnqueueOptions = {
7
+ delay?: number | string;
8
+ };
9
+ /**
10
+ * Register a job
11
+ */
12
+ declare function job<T = any>(name: string, handler: JobHandler<T>, options?: JobOptions): void;
13
+ /**
14
+ * Enqueue job (persistent)
15
+ */
16
+ declare function enqueue<T = any>(name: string, payload: T, options?: EnqueueOptions): Promise<void>;
17
+ /**
18
+ * Configure
19
+ */
20
+ declare function configure(options: {
21
+ concurrency?: number;
22
+ }): void;
23
+ declare function shutdown(): Promise<void>;
24
+
25
+ export { configure, enqueue, job, shutdown };
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ import {
2
+ db_default
3
+ } from "./chunk-CKCBPWXR.js";
4
+
5
+ // src/index.ts
6
+ var registry = /* @__PURE__ */ new Map();
7
+ var isWorkerRunning = false;
8
+ var concurrency = 1;
9
+ var activeCount = 0;
10
+ var isShuttingDown = false;
11
+ function job(name, handler, options = {}) {
12
+ if (registry.has(name)) {
13
+ throw new Error(`Job "${name}" is already registered`);
14
+ }
15
+ registry.set(name, {
16
+ handler,
17
+ options: {
18
+ retries: options.retries ?? 0,
19
+ backoffMs: options.backoffMs ?? 1e3
20
+ }
21
+ });
22
+ startWorker();
23
+ }
24
+ async function enqueue(name, payload, options = {}) {
25
+ if (!registry.has(name)) {
26
+ throw new Error(`Job "${name}" is not registered`);
27
+ }
28
+ const delayMs = parseDelay(options.delay);
29
+ db_default.prepare(
30
+ `INSERT INTO jobs (name, payload, attempts, runAt, status)
31
+ VALUES (?, ?, ?, ?, 'queued')`
32
+ ).run(name, JSON.stringify(payload), 0, Date.now() + delayMs);
33
+ }
34
+ function configure(options) {
35
+ if (options.concurrency && options.concurrency > 0) {
36
+ concurrency = options.concurrency;
37
+ }
38
+ }
39
+ function startWorker() {
40
+ if (isWorkerRunning) return;
41
+ isWorkerRunning = true;
42
+ setInterval(processQueue, 50);
43
+ }
44
+ async function processQueue() {
45
+ if (isShuttingDown) return;
46
+ if (activeCount >= concurrency) return;
47
+ const row = db_default.prepare(
48
+ `SELECT * FROM jobs
49
+ WHERE status = 'queued'
50
+ AND runAt <= ?
51
+ ORDER BY id ASC
52
+ LIMIT 1`
53
+ ).get(Date.now());
54
+ if (!row) return;
55
+ const def = registry.get(row.name);
56
+ if (!def) return;
57
+ db_default.prepare(
58
+ `UPDATE jobs
59
+ SET status = 'running',
60
+ startedAt = ?
61
+ WHERE id = ?`
62
+ ).run(Date.now(), row.id);
63
+ activeCount++;
64
+ try {
65
+ await def.handler(JSON.parse(row.payload));
66
+ db_default.prepare(
67
+ `UPDATE jobs
68
+ SET status = 'completed',
69
+ finishedAt = ?
70
+ WHERE id = ?`
71
+ ).run(Date.now(), row.id);
72
+ } catch (err) {
73
+ await handleRetry(row, def);
74
+ } finally {
75
+ activeCount--;
76
+ }
77
+ }
78
+ async function handleRetry(row, def) {
79
+ const attempts = row.attempts + 1;
80
+ const { retries, backoffMs } = def.options;
81
+ if (attempts > retries) {
82
+ db_default.prepare(
83
+ `UPDATE jobs
84
+ SET status = 'failed',
85
+ finishedAt = ?,
86
+ error = ?
87
+ WHERE id = ?`
88
+ ).run(Date.now(), String(row.error ?? "Unknown error"), row.id);
89
+ return;
90
+ }
91
+ const delay = backoffMs * attempts;
92
+ db_default.prepare(
93
+ `UPDATE jobs
94
+ SET attempts = ?,
95
+ runAt = ?,
96
+ status = 'queued'
97
+ WHERE id = ?`
98
+ ).run(attempts, Date.now() + delay, row.id);
99
+ }
100
+ function parseDelay(delay) {
101
+ if (!delay) return 0;
102
+ if (typeof delay === "number") return delay;
103
+ const match = /^(\d+)(ms|s|m|h)?$/.exec(delay.trim());
104
+ if (!match) {
105
+ throw new Error(`[OneQueue] Invalid delay format: ${delay}`);
106
+ }
107
+ const value = Number(match[1]);
108
+ const unit = match[2] ?? "ms";
109
+ switch (unit) {
110
+ case "ms":
111
+ return value;
112
+ case "s":
113
+ return value * 1e3;
114
+ case "m":
115
+ return value * 6e4;
116
+ case "h":
117
+ return value * 36e5;
118
+ default:
119
+ return value;
120
+ }
121
+ }
122
+ async function shutdown() {
123
+ if (isShuttingDown) return;
124
+ console.log("[OneQueue] Graceful shutdown started\u2026");
125
+ isShuttingDown = true;
126
+ while (activeCount > 0) {
127
+ await new Promise((r) => setTimeout(r, 50));
128
+ }
129
+ console.log("[OneQueue] All jobs finished. Exiting cleanly.");
130
+ }
131
+ function setupSignalHandlers() {
132
+ const handler = async () => {
133
+ await shutdown();
134
+ process.exit(0);
135
+ };
136
+ process.once("SIGINT", handler);
137
+ process.once("SIGTERM", handler);
138
+ }
139
+ setupSignalHandlers();
140
+ export {
141
+ configure,
142
+ enqueue,
143
+ job,
144
+ shutdown
145
+ };
package/package.json CHANGED
@@ -1,13 +1,47 @@
1
1
  {
2
2
  "name": "onequeue",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Background jobs in one line",
5
- "main": "index.js",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
6
18
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
19
+ "build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts",
20
+ "dev": "tsup src/index.ts --watch",
21
+ "prepublishOnly": "npm run build"
8
22
  },
9
- "keywords": [],
23
+ "keywords": [
24
+ "background-jobs",
25
+ "queue",
26
+ "nodejs",
27
+ "typescript",
28
+ "async",
29
+ "job-queue"
30
+ ],
10
31
  "author": "",
11
32
  "license": "MIT",
12
- "type": "module"
13
- }
33
+ "type": "module",
34
+ "devDependencies": {
35
+ "@types/better-sqlite3": "^7.6.13",
36
+ "@types/node": "^25.3.0",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "dependencies": {
41
+ "better-sqlite3": "^12.6.2",
42
+ "sirv": "^3.0.2"
43
+ },
44
+ "bin": {
45
+ "onequeue": "dist/cli.js"
46
+ }
47
+ }
package/index.js DELETED
@@ -1,3 +0,0 @@
1
- export const hello = () => {
2
- console.log("OneQueue coming soon 🚀");
3
- };