qwerk 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 +21 -0
- package/README.md +380 -0
- package/dist/index.d.ts +744 -0
- package/dist/index.js +1739 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2026 Gabriel Vaquer
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free
|
|
4
|
+
of charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# qwerk
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe job queue for Bun.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero dependencies** - No external runtime dependencies
|
|
8
|
+
- **Multiple backends** - Memory, BroadcastChannel, or Redis
|
|
9
|
+
- **Type-safe** - Full TypeScript generics for job types and results
|
|
10
|
+
- **Retry with backoff** - Exponential, linear, or fixed strategies with jitter
|
|
11
|
+
- **Job progress** - Report and track job progress (0-100%)
|
|
12
|
+
- **Job results** - Store and retrieve return values from handlers
|
|
13
|
+
- **Repeatable jobs** - Cron expressions or fixed intervals
|
|
14
|
+
- **Rate limiting** - Token bucket algorithm
|
|
15
|
+
- **Priority queues** - Lower number = higher priority
|
|
16
|
+
- **Dead letter queue** - Failed jobs are preserved for inspection/retry
|
|
17
|
+
- **Graceful shutdown** - Wait for in-flight jobs with timeout
|
|
18
|
+
- **Job timeout** - Automatic cancellation via AbortController
|
|
19
|
+
- **Visibility timeout** - Stalled job recovery
|
|
20
|
+
- **Deduplication** - Prevent duplicate jobs via custom IDs
|
|
21
|
+
- **Metrics** - Throughput, processing time, queue depth
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun add qwerk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { Queue, MemoryBackend } from "qwerk";
|
|
33
|
+
|
|
34
|
+
// Define your job types
|
|
35
|
+
type Jobs = {
|
|
36
|
+
"send-email": { to: string; subject: string; body: string };
|
|
37
|
+
"process-image": { url: string; width: number };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Create queue with in-memory backend
|
|
41
|
+
const queue = new Queue<Jobs>(new MemoryBackend());
|
|
42
|
+
|
|
43
|
+
// Register handlers
|
|
44
|
+
queue.process("send-email", async (job, ctx) => {
|
|
45
|
+
await ctx.updateProgress(50);
|
|
46
|
+
console.log(`Sending email to ${job.data.to}`);
|
|
47
|
+
await ctx.updateProgress(100);
|
|
48
|
+
return { sent: true, timestamp: Date.now() };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Add jobs
|
|
52
|
+
await queue.add("send-email", {
|
|
53
|
+
to: "user@example.com",
|
|
54
|
+
subject: "Hello",
|
|
55
|
+
body: "World",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Start processing
|
|
59
|
+
queue.start();
|
|
60
|
+
|
|
61
|
+
// Later: graceful shutdown
|
|
62
|
+
await queue.stop();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Backends
|
|
66
|
+
|
|
67
|
+
### MemoryBackend
|
|
68
|
+
|
|
69
|
+
Single-process, non-persistent. Good for development and testing.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { MemoryBackend } from "qwerk";
|
|
73
|
+
|
|
74
|
+
const backend = new MemoryBackend();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### BroadcastBackend
|
|
78
|
+
|
|
79
|
+
Cross-tab/worker communication using the BroadcastChannel API. Works in browsers and Bun/Node worker threads.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { BroadcastBackend } from "qwerk";
|
|
83
|
+
|
|
84
|
+
const backend = new BroadcastBackend("my-queue-channel");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### RedisBackend
|
|
88
|
+
|
|
89
|
+
Distributed, persistent queue using Bun's built-in Redis client.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { RedisBackend } from "qwerk";
|
|
93
|
+
|
|
94
|
+
const backend = new RedisBackend("redis://localhost:6379", {
|
|
95
|
+
prefix: "myapp:queue",
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### Queue Options
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const queue = new Queue<Jobs>(backend, {
|
|
105
|
+
pollInterval: 1000, // How often to check for jobs (ms)
|
|
106
|
+
concurrency: 5, // Max parallel jobs
|
|
107
|
+
visibilityTimeout: 30000, // Job lock timeout (ms)
|
|
108
|
+
stalledInterval: 5000, // How often to check for stalled jobs
|
|
109
|
+
logger: silentLogger, // Custom logger (or consoleLogger)
|
|
110
|
+
maxPayloadSize: 1024, // Max job data size (bytes)
|
|
111
|
+
maxQueueSize: 10000, // Max pending jobs
|
|
112
|
+
rateLimit: {
|
|
113
|
+
max: 100, // Max jobs per interval
|
|
114
|
+
duration: 1000, // Interval in ms
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Adding Jobs
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Simple add
|
|
123
|
+
const job = await queue.add("send-email", { to: "a@b.com", subject: "Hi", body: "Hello" });
|
|
124
|
+
|
|
125
|
+
// With options
|
|
126
|
+
await queue.add("send-email", data, {
|
|
127
|
+
delay: 5000, // Delay execution by 5s
|
|
128
|
+
maxAttempts: 5, // Retry up to 5 times
|
|
129
|
+
priority: 1, // Lower = higher priority
|
|
130
|
+
timeout: 60000, // Job timeout (ms)
|
|
131
|
+
jobId: "unique-123", // Custom ID for deduplication
|
|
132
|
+
backoff: {
|
|
133
|
+
type: "exponential", // or "linear" | "fixed"
|
|
134
|
+
delay: 1000,
|
|
135
|
+
maxDelay: 30000,
|
|
136
|
+
},
|
|
137
|
+
repeat: {
|
|
138
|
+
cron: "0 9 * * MON", // Every Monday at 9am
|
|
139
|
+
// or: every: 3600000, // Every hour
|
|
140
|
+
limit: 10, // Max 10 repetitions
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Bulk add
|
|
145
|
+
const jobs = await queue.addBulk([
|
|
146
|
+
{ name: "send-email", data: { to: "a@b.com", subject: "1", body: "..." } },
|
|
147
|
+
{ name: "send-email", data: { to: "b@c.com", subject: "2", body: "..." } },
|
|
148
|
+
]);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Processing Jobs
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Basic handler
|
|
155
|
+
queue.process("send-email", async (job) => {
|
|
156
|
+
console.log(job.data.to);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// With progress and result
|
|
160
|
+
queue.process<"process-image", { thumbnail: string }>("process-image", async (job, ctx) => {
|
|
161
|
+
await ctx.updateProgress(10);
|
|
162
|
+
|
|
163
|
+
// ctx.signal is an AbortSignal for cancellation
|
|
164
|
+
const result = await processImage(job.data.url, { signal: ctx.signal });
|
|
165
|
+
|
|
166
|
+
await ctx.updateProgress(100);
|
|
167
|
+
return { thumbnail: result.url };
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Events
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
queue.on("added", (job) => console.log("Job added:", job.id));
|
|
175
|
+
queue.on("active", (job) => console.log("Job started:", job.id));
|
|
176
|
+
queue.on("progress", (job, progress) => console.log(`Job ${job.id}: ${progress}%`));
|
|
177
|
+
queue.on("completed", (job, result) => console.log("Job done:", result));
|
|
178
|
+
queue.on("failed", (job, error) => console.log("Job failed:", error.message));
|
|
179
|
+
queue.on("retry", (job, error) => console.log("Job retrying:", job.attempts));
|
|
180
|
+
queue.on("stalled", (job) => console.log("Job stalled:", job.id));
|
|
181
|
+
queue.on("timeout", (job) => console.log("Job timed out:", job.id));
|
|
182
|
+
|
|
183
|
+
// One-time listener
|
|
184
|
+
queue.once("completed", (job) => console.log("First job done!"));
|
|
185
|
+
|
|
186
|
+
// Remove listener
|
|
187
|
+
queue.off("completed", myHandler);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Queue Control
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
queue.start(); // Start processing
|
|
194
|
+
queue.pause(); // Stop picking up new jobs
|
|
195
|
+
queue.resume(); // Resume after pause
|
|
196
|
+
await queue.stop(); // Graceful shutdown (waits for in-flight)
|
|
197
|
+
await queue.stop(5000); // Shutdown with 5s timeout
|
|
198
|
+
await queue.drain(); // Wait for all current jobs to complete
|
|
199
|
+
await queue.close(); // Stop + close backend
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Inspecting the Queue
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
await queue.size(); // Pending jobs count
|
|
206
|
+
await queue.activeCount(); // Currently processing
|
|
207
|
+
await queue.failedCount(); // Jobs in DLQ
|
|
208
|
+
await queue.completedCount(); // Completed jobs with results
|
|
209
|
+
|
|
210
|
+
// Get failed jobs
|
|
211
|
+
const failed = await queue.getFailed(100);
|
|
212
|
+
for (const { job, error, failedAt } of failed) {
|
|
213
|
+
console.log(`${job.id} failed: ${error}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Retry a failed job
|
|
217
|
+
await queue.retryFailed(jobId);
|
|
218
|
+
|
|
219
|
+
// Get completed jobs with results
|
|
220
|
+
const completed = await queue.getCompletedJobs(100);
|
|
221
|
+
for (const { job, result, completedAt } of completed) {
|
|
222
|
+
console.log(`${job.id} returned:`, result);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get specific completed job
|
|
226
|
+
const entry = await queue.getCompleted(jobId);
|
|
227
|
+
|
|
228
|
+
// Remove a job
|
|
229
|
+
await queue.remove(jobId);
|
|
230
|
+
|
|
231
|
+
// Clear all jobs
|
|
232
|
+
await queue.clear();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Metrics
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const metrics = await queue.getMetrics();
|
|
239
|
+
// {
|
|
240
|
+
// waiting: 42,
|
|
241
|
+
// active: 3,
|
|
242
|
+
// failed: 1,
|
|
243
|
+
// completed: 1337,
|
|
244
|
+
// totalFailed: 5,
|
|
245
|
+
// totalProcessingTime: 45230,
|
|
246
|
+
// avgProcessingTime: 33.8,
|
|
247
|
+
// throughput: 2.5 // jobs/second (last 60s)
|
|
248
|
+
// }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Custom Logger
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { Queue, MemoryBackend, silentLogger, consoleLogger, type Logger } from "qwerk";
|
|
255
|
+
|
|
256
|
+
// Built-in loggers
|
|
257
|
+
const queue1 = new Queue(backend, { logger: silentLogger });
|
|
258
|
+
const queue2 = new Queue(backend, { logger: consoleLogger });
|
|
259
|
+
|
|
260
|
+
// Custom logger
|
|
261
|
+
const myLogger: Logger = {
|
|
262
|
+
debug: (msg, ...args) => myDebugFn(msg, ...args),
|
|
263
|
+
info: (msg, ...args) => myInfoFn(msg, ...args),
|
|
264
|
+
warn: (msg, ...args) => myWarnFn(msg, ...args),
|
|
265
|
+
error: (msg, ...args) => myErrorFn(msg, ...args),
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Cron Expressions
|
|
270
|
+
|
|
271
|
+
Standard 5-field cron syntax:
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
┌───────────── minute (0-59)
|
|
275
|
+
│ ┌───────────── hour (0-23)
|
|
276
|
+
│ │ ┌───────────── day of month (1-31)
|
|
277
|
+
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
|
|
278
|
+
│ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT, 0 and 7 are Sunday)
|
|
279
|
+
│ │ │ │ │
|
|
280
|
+
* * * * *
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
|
|
285
|
+
- `0 * * * *` - Every hour at :00
|
|
286
|
+
- `*/15 * * * *` - Every 15 minutes
|
|
287
|
+
- `0 9 * * MON-FRI` - Weekdays at 9am
|
|
288
|
+
- `0 0 1 * *` - First day of every month at midnight
|
|
289
|
+
|
|
290
|
+
## Implementing a Custom Backend
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import type { Backend, Job } from "qwerk";
|
|
294
|
+
|
|
295
|
+
class MyBackend implements Backend {
|
|
296
|
+
async push(job: Job): Promise<boolean> {
|
|
297
|
+
/* ... */
|
|
298
|
+
}
|
|
299
|
+
async pushBulk(jobs: Job[]): Promise<number> {
|
|
300
|
+
/* ... */
|
|
301
|
+
}
|
|
302
|
+
async pop(visibilityTimeout: number): Promise<Job | null> {
|
|
303
|
+
/* ... */
|
|
304
|
+
}
|
|
305
|
+
async ack(jobId: string, result?: unknown): Promise<void> {
|
|
306
|
+
/* ... */
|
|
307
|
+
}
|
|
308
|
+
async nack(job: Job, nextAttemptAt: number): Promise<void> {
|
|
309
|
+
/* ... */
|
|
310
|
+
}
|
|
311
|
+
async fail(job: Job, error: Error): Promise<void> {
|
|
312
|
+
/* ... */
|
|
313
|
+
}
|
|
314
|
+
async getStalled(): Promise<Job[]> {
|
|
315
|
+
/* ... */
|
|
316
|
+
}
|
|
317
|
+
async updateProgress(jobId: string, progress: number): Promise<void> {
|
|
318
|
+
/* ... */
|
|
319
|
+
}
|
|
320
|
+
async getCompleted(jobId: string): Promise<CompletedJob | null> {
|
|
321
|
+
/* ... */
|
|
322
|
+
}
|
|
323
|
+
async completedCount(): Promise<number> {
|
|
324
|
+
/* ... */
|
|
325
|
+
}
|
|
326
|
+
async getCompletedJobs(limit?: number): Promise<CompletedJob[]> {
|
|
327
|
+
/* ... */
|
|
328
|
+
}
|
|
329
|
+
subscribe(callback: () => void): void {
|
|
330
|
+
/* ... */
|
|
331
|
+
}
|
|
332
|
+
unsubscribe(): void {
|
|
333
|
+
/* ... */
|
|
334
|
+
}
|
|
335
|
+
async size(): Promise<number> {
|
|
336
|
+
/* ... */
|
|
337
|
+
}
|
|
338
|
+
async activeCount(): Promise<number> {
|
|
339
|
+
/* ... */
|
|
340
|
+
}
|
|
341
|
+
async failedCount(): Promise<number> {
|
|
342
|
+
/* ... */
|
|
343
|
+
}
|
|
344
|
+
async getFailed(limit?: number): Promise<FailedJob[]> {
|
|
345
|
+
/* ... */
|
|
346
|
+
}
|
|
347
|
+
async retryFailed(jobId: string): Promise<boolean> {
|
|
348
|
+
/* ... */
|
|
349
|
+
}
|
|
350
|
+
async remove(jobId: string): Promise<boolean> {
|
|
351
|
+
/* ... */
|
|
352
|
+
}
|
|
353
|
+
async clear(): Promise<void> {
|
|
354
|
+
/* ... */
|
|
355
|
+
}
|
|
356
|
+
async close?(): Promise<void> {
|
|
357
|
+
/* ... */
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Comparison
|
|
363
|
+
|
|
364
|
+
| Feature | qwerk | BullMQ | Agenda | Bee-Queue |
|
|
365
|
+
| ----------------- | -------------- | ------ | ------- | --------- |
|
|
366
|
+
| Backend | Pluggable | Redis | MongoDB | Redis |
|
|
367
|
+
| Zero dependencies | Yes | No | No | No |
|
|
368
|
+
| TypeScript native | Yes | Yes | Partial | Partial |
|
|
369
|
+
| Bundle size | ~15KB | ~150KB | ~80KB | ~40KB |
|
|
370
|
+
| Job progress | Yes | Yes | No | Yes |
|
|
371
|
+
| Job results | Yes | Yes | No | Yes |
|
|
372
|
+
| Cron jobs | Yes (built-in) | Yes | Yes | No |
|
|
373
|
+
| Rate limiting | Yes | Yes | No | No |
|
|
374
|
+
| Priority queues | Yes | Yes | Yes | Yes |
|
|
375
|
+
| Multi-tab/worker | Yes | No | No | No |
|
|
376
|
+
| Web UI | No | Yes | Yes | No |
|
|
377
|
+
|
|
378
|
+
## License
|
|
379
|
+
|
|
380
|
+
MIT
|