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 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