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 +155 -0
- package/dist/chunk-CKCBPWXR.js +21 -0
- package/dist/chunk-FZMBGSEZ.js +17 -0
- package/dist/cli.cjs +281 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +241 -0
- package/dist/index.cjs +199 -0
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +145 -0
- package/package.json +40 -6
- package/index.js +0 -3
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Background jobs in one line",
|
|
5
|
-
"main": "index.
|
|
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
|
-
"
|
|
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