workmatic 1.0.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,508 @@
1
+ # Workmatic
2
+
3
+ A persistent job queue for Node.js using SQLite. Simple, reliable, and zero external dependencies beyond SQLite.
4
+
5
+ ## Why?
6
+
7
+ I love [fastq](https://github.com/mcollina/fastq) - it's fast, simple, and has a great API. But it's in-memory only, which can be frustrating when you need jobs to survive process restarts or crashes.
8
+
9
+ Workmatic combines the simplicity of fastq with SQLite persistence. No Redis, no external services - just a single file that keeps your jobs safe. Perfect for small to medium workloads where you want durability without infrastructure complexity.
10
+
11
+ ## Features
12
+
13
+ - **Persistent**: Jobs survive restarts via SQLite storage
14
+ - **Concurrent**: Process multiple jobs simultaneously with fastq
15
+ - **Priority**: Process high-priority jobs first
16
+ - **Delayed**: Schedule jobs to run in the future
17
+ - **Retries**: Automatic retries with exponential backoff
18
+ - **Lease-based**: Prevents double processing with lease locks
19
+ - **Dashboard**: Built-in web UI for monitoring
20
+ - **Type-safe**: Full TypeScript support with Kysely
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install workmatic
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```typescript
31
+ import { createDatabase, createClient, createWorker } from 'workmatic';
32
+
33
+ // Create database (use file path for persistence)
34
+ const db = createDatabase({ filename: './jobs.db' });
35
+
36
+ // Create a client to add jobs
37
+ const client = createClient({ db, queue: 'emails' });
38
+
39
+ // Add a job
40
+ const { id } = await client.add({
41
+ to: 'user@example.com',
42
+ subject: 'Hello!'
43
+ });
44
+ console.log(`Job created: ${id}`);
45
+
46
+ // Create a worker to process jobs
47
+ const worker = createWorker({
48
+ db,
49
+ queue: 'emails',
50
+ concurrency: 4,
51
+ });
52
+
53
+ // Define the processor
54
+ worker.process(async (job) => {
55
+ console.log(`Sending email to ${job.payload.to}`);
56
+ await sendEmail(job.payload);
57
+ });
58
+
59
+ // Start processing
60
+ worker.start();
61
+ ```
62
+
63
+ ## API Reference
64
+
65
+ ### `createDatabase(options)`
66
+
67
+ Initialize the database connection and schema.
68
+
69
+ ```typescript
70
+ const db = createDatabase({
71
+ // Option 1: File path (creates or opens existing)
72
+ filename: './jobs.db',
73
+
74
+ // Option 2: In-memory (for testing)
75
+ filename: ':memory:',
76
+
77
+ // Option 3: Existing better-sqlite3 instance
78
+ db: existingSqliteInstance,
79
+ });
80
+ ```
81
+
82
+ ### `createClient(options)`
83
+
84
+ Create a client for adding jobs to a queue.
85
+
86
+ ```typescript
87
+ const client = createClient({
88
+ db, // Required: Database instance
89
+ queue: 'default', // Optional: Queue name (default: 'default')
90
+ });
91
+ ```
92
+
93
+ #### `client.add(payload, options?)`
94
+
95
+ Add a job to the queue.
96
+
97
+ ```typescript
98
+ const { ok, id } = await client.add(
99
+ { email: 'user@example.com' }, // Payload (must be JSON-serializable)
100
+ {
101
+ priority: 0, // Lower = higher priority (default: 0)
102
+ delayMs: 5000, // Delay before job becomes available (default: 0)
103
+ maxAttempts: 3, // Max retry attempts (default: 3)
104
+ }
105
+ );
106
+ ```
107
+
108
+ #### `client.stats()`
109
+
110
+ Get job statistics for the queue.
111
+
112
+ ```typescript
113
+ const stats = await client.stats();
114
+ // { ready: 5, running: 2, done: 100, failed: 0, dead: 1, total: 108 }
115
+ ```
116
+
117
+ ### `createWorker(options)`
118
+
119
+ Create a worker to process jobs from a queue.
120
+
121
+ ```typescript
122
+ const worker = createWorker({
123
+ db, // Required: Database instance
124
+ queue: 'default', // Optional: Queue name (default: 'default')
125
+ concurrency: 1, // Optional: Parallel job count (default: 1)
126
+ leaseMs: 30000, // Optional: Job lease duration in ms (default: 30000)
127
+ pollMs: 1000, // Optional: Poll interval when idle (default: 1000)
128
+ timeoutMs: 60000, // Optional: Job execution timeout in ms (default: none)
129
+ backoff: (n) => 1000 * Math.pow(2, n), // Optional: Retry backoff function
130
+ });
131
+ ```
132
+
133
+ #### `worker.process(fn)`
134
+
135
+ Set the job processor function.
136
+
137
+ ```typescript
138
+ worker.process(async (job) => {
139
+ console.log(`Processing job ${job.id}`);
140
+ console.log(`Payload:`, job.payload);
141
+ console.log(`Attempt ${job.attempts + 1} of ${job.maxAttempts}`);
142
+
143
+ // Do work here
144
+ // Throw an error to trigger retry
145
+ });
146
+ ```
147
+
148
+ #### `worker.start()`
149
+
150
+ Start processing jobs.
151
+
152
+ ```typescript
153
+ worker.start();
154
+ ```
155
+
156
+ #### `worker.stop()`
157
+
158
+ Stop processing and wait for current jobs to finish.
159
+
160
+ ```typescript
161
+ await worker.stop();
162
+ ```
163
+
164
+ #### `worker.pause()` / `worker.resume()`
165
+
166
+ Pause and resume job processing.
167
+
168
+ ```typescript
169
+ worker.pause(); // Stop claiming new jobs
170
+ worker.resume(); // Resume claiming jobs
171
+ ```
172
+
173
+ #### `worker.stats()`
174
+
175
+ Get job statistics for the queue.
176
+
177
+ ```typescript
178
+ const stats = await worker.stats();
179
+ ```
180
+
181
+ #### Worker properties
182
+
183
+ ```typescript
184
+ worker.isRunning; // boolean
185
+ worker.isPaused; // boolean
186
+ worker.queue; // string
187
+ ```
188
+
189
+ ### `createDashboard(options)`
190
+
191
+ Create a standalone web dashboard server for monitoring and control.
192
+
193
+ ```typescript
194
+ const dashboard = createDashboard({
195
+ db, // Required: Database instance
196
+ port: 3000, // Optional: HTTP port (default: 3000)
197
+ workers: [worker1], // Optional: Workers to control
198
+ });
199
+
200
+ console.log(`Dashboard at http://localhost:${dashboard.port}`);
201
+
202
+ // Later, close the server
203
+ await dashboard.close();
204
+ ```
205
+
206
+ ### `createDashboardMiddleware(options)`
207
+
208
+ Create an Express-compatible middleware to mount the dashboard on an existing app.
209
+
210
+ ```typescript
211
+ import express from 'express';
212
+ import { createDashboardMiddleware } from 'workmatic';
213
+
214
+ const app = express();
215
+
216
+ // Mount dashboard at /workmatic
217
+ app.use(createDashboardMiddleware({
218
+ db, // Required: Database instance
219
+ basePath: '/workmatic', // Optional: URL prefix (default: '')
220
+ workers: [worker], // Optional: Workers to control
221
+ }));
222
+
223
+ app.listen(3000);
224
+ // Dashboard available at http://localhost:3000/workmatic
225
+ ```
226
+
227
+ Works with any framework that supports Node.js `(req, res, next)` middleware:
228
+
229
+ ```typescript
230
+ // Fastify
231
+ import fastify from 'fastify';
232
+ import middie from '@fastify/middie';
233
+
234
+ const app = fastify();
235
+ await app.register(middie);
236
+ app.use(createDashboardMiddleware({ db, basePath: '/jobs' }));
237
+
238
+ // Hono
239
+ import { Hono } from 'hono';
240
+ import { handle } from 'hono/node-server';
241
+
242
+ const app = new Hono();
243
+ app.use('/workmatic/*', (c) => {
244
+ return new Promise((resolve) => {
245
+ const middleware = createDashboardMiddleware({ db, basePath: '/workmatic' });
246
+ middleware(c.env.incoming, c.env.outgoing, resolve);
247
+ });
248
+ });
249
+ ```
250
+
251
+ ## Job Object
252
+
253
+ The job object passed to processors has these properties:
254
+
255
+ ```typescript
256
+ interface Job<TPayload> {
257
+ id: string; // Unique public ID (nanoid)
258
+ queue: string; // Queue name
259
+ payload: TPayload; // Your job data
260
+ status: JobStatus; // 'ready' | 'running' | 'done' | 'failed' | 'dead'
261
+ priority: number; // Priority value
262
+ attempts: number; // Current attempt count (starts at 0)
263
+ maxAttempts: number; // Maximum attempts allowed
264
+ createdAt: number; // Unix timestamp (ms)
265
+ lastError: string | null; // Last error message
266
+ }
267
+ ```
268
+
269
+ ## Durability Model
270
+
271
+ Workmatic provides **at-least-once** delivery:
272
+
273
+ - Jobs are persisted to SQLite before `add()` returns
274
+ - A job may be processed multiple times if:
275
+ - The worker crashes during processing
276
+ - The lease expires before completion
277
+ - Jobs are only marked `done` after successful processing
278
+
279
+ ### Idempotency Recommendation
280
+
281
+ Design your job handlers to be idempotent (safe to run multiple times):
282
+
283
+ ```typescript
284
+ worker.process(async (job) => {
285
+ // Check if already processed
286
+ const exists = await db.checkProcessed(job.id);
287
+ if (exists) return;
288
+
289
+ // Process the job
290
+ await processPayment(job.payload);
291
+
292
+ // Mark as processed
293
+ await db.markProcessed(job.id);
294
+ });
295
+ ```
296
+
297
+ ## Job Lifecycle
298
+
299
+ ```
300
+ ┌─────────┐ add() ┌─────────┐
301
+ │ NEW │ ─────────────▶ │ READY │
302
+ └─────────┘ └────┬────┘
303
+
304
+ claim │
305
+
306
+ ┌─────────┐
307
+ │ RUNNING │
308
+ └────┬────┘
309
+
310
+ ┌────────────────┼────────────────┐
311
+ │ │ │
312
+ success failure failure
313
+ │ (retries (max
314
+ │ left) attempts)
315
+ ▼ │ │
316
+ ┌─────────┐ │ ▼
317
+ │ DONE │ │ ┌─────────┐
318
+ └─────────┘ │ │ DEAD │
319
+ │ └─────────┘
320
+
321
+
322
+ ┌─────────────────┐
323
+ │ READY (retry) │
324
+ │ with backoff │
325
+ └─────────────────┘
326
+ ```
327
+
328
+ ## Options Glossary
329
+
330
+ | Option | Default | Description |
331
+ |--------|---------|-------------|
332
+ | `queue` | `'default'` | Queue name for job isolation |
333
+ | `concurrency` | `1` | Number of jobs to process in parallel |
334
+ | `leaseMs` | `30000` | How long a job is "locked" during processing |
335
+ | `pollMs` | `1000` | How often to check for new jobs when idle |
336
+ | `timeoutMs` | `undefined` | Job execution timeout (fails job if exceeded) |
337
+ | `priority` | `0` | Job priority (lower = processed first) |
338
+ | `delayMs` | `0` | Delay before job becomes available |
339
+ | `maxAttempts` | `3` | Maximum processing attempts |
340
+ | `backoff` | `2^n * 1000` | Function returning retry delay in ms |
341
+
342
+ ## Dashboard
343
+
344
+ The built-in dashboard provides:
345
+
346
+ - Real-time job statistics
347
+ - Job list with filtering and pagination
348
+ - Worker status and control (pause/resume)
349
+ - Auto-refresh every 2 seconds
350
+
351
+ ![Dashboard Screenshot](dashboard-screenshot.png)
352
+
353
+ ## Examples
354
+
355
+ See the `examples/` directory:
356
+
357
+ - `basic.ts` - Simple job processing
358
+ - `advanced.ts` - Priority, delays, retries
359
+ - `with-dashboard.ts` - Dashboard monitoring
360
+
361
+ Run examples:
362
+
363
+ ```bash
364
+ npx tsx examples/basic.ts
365
+ npx tsx examples/with-dashboard.ts
366
+ ```
367
+
368
+ ## Benchmarks
369
+
370
+ Run performance benchmarks:
371
+
372
+ ```bash
373
+ # In-memory (fastest, for testing)
374
+ npm run bench
375
+
376
+ # File-based (realistic, persistent)
377
+ npm run bench -- --file
378
+ ```
379
+
380
+ ### Results Comparison
381
+
382
+ | Benchmark | In-Memory | File-based |
383
+ |-----------|-----------|------------|
384
+ | Sequential Insert | 27,000/s | 13,000/s |
385
+ | Parallel Insert | 23,000/s | 12,000/s |
386
+ | Process (concurrency=1) | 1,100/s | 1,100/s |
387
+ | Process (concurrency=4) | 4,800/s | 4,800/s |
388
+ | Process (concurrency=8) | 10,000/s | 8,300/s |
389
+ | Process (concurrency=16) | 18,000/s | 5,700/s |
390
+ | Mixed Insert+Process | 7,500/s | 3,500/s |
391
+ | Claim + Process Batch | 23,600/s | 11,800/s |
392
+
393
+ **Note**: File-based performance degrades at high concurrency due to disk I/O. For file-based databases, `concurrency=8` is often optimal. Performance varies by hardware.
394
+
395
+ ## CLI
396
+
397
+ Workmatic includes a command-line tool for managing jobs directly from the database file.
398
+
399
+ ```bash
400
+ # Show job statistics
401
+ npx workmatic stats ./jobs.db
402
+
403
+ # List queues with pause status
404
+ npx workmatic queues ./jobs.db
405
+
406
+ # Pause/resume a queue (workers stop/start claiming new jobs)
407
+ npx workmatic pause ./jobs.db emails
408
+ npx workmatic resume ./jobs.db emails
409
+
410
+ # List jobs (with filters)
411
+ npx workmatic list ./jobs.db --status=dead --limit=10
412
+
413
+ # Export jobs to CSV
414
+ npx workmatic export ./jobs.db backup.csv
415
+ npx workmatic export ./jobs.db --status=failed > failed.csv
416
+
417
+ # Import jobs from CSV
418
+ npx workmatic import ./jobs.db backup.csv
419
+
420
+ # Delete jobs by status
421
+ npx workmatic purge ./jobs.db --status=done
422
+
423
+ # Retry dead/failed jobs (reset to ready)
424
+ npx workmatic retry ./jobs.db --status=dead
425
+ ```
426
+
427
+ ### CLI Commands
428
+
429
+ | Command | Description |
430
+ |---------|-------------|
431
+ | `stats <db>` | Show job counts by status and queue |
432
+ | `queues <db>` | List all queues with pause status |
433
+ | `pause <db> <queue>` | Pause a queue (workers stop claiming) |
434
+ | `resume <db> <queue>` | Resume a paused queue |
435
+ | `list <db>` | List jobs with optional filters |
436
+ | `export <db> [file]` | Export jobs to CSV (stdout if no file) |
437
+ | `import <db> <file>` | Import jobs from CSV |
438
+ | `purge <db> --status=X` | Delete jobs with specific status |
439
+ | `retry <db> --status=X` | Reset jobs to ready status |
440
+
441
+ ### CLI Options
442
+
443
+ | Option | Description |
444
+ |--------|-------------|
445
+ | `--status=<status>` | Filter by status (ready/running/done/failed/dead) |
446
+ | `--queue=<queue>` | Filter by queue name |
447
+ | `--limit=<n>` | Limit results (default: 100) |
448
+
449
+ ### Live Pause/Resume
450
+
451
+ The `pause` and `resume` commands work on running workers in real-time. When you pause a queue:
452
+ - Running workers immediately stop claiming new jobs
453
+ - Jobs currently being processed will complete
454
+ - The queue resumes when you run `resume`
455
+
456
+ This allows you to manage workers without restarting your application.
457
+
458
+ ### CSV Import for AI Workflows
459
+
460
+ The CSV import feature makes Workmatic particularly useful for AI-powered automation:
461
+
462
+ ```csv
463
+ public_id,queue,payload,status,priority,run_at,attempts,max_attempts,lease_until,created_at,updated_at,last_error
464
+ job_001,emails,"{""to"":""user@example.com"",""template"":""welcome""}",ready,0,1704067200000,0,3,0,1704067200000,1704067200000,
465
+ job_002,emails,"{""to"":""other@example.com"",""template"":""reminder""}",ready,5,1704067200000,0,3,0,1704067200000,1704067200000,
466
+ ```
467
+
468
+ **Use cases:**
469
+
470
+ - **AI Agents**: Tools like Claude, GPT, or custom agents can generate CSV files with batch jobs. Simply ask an AI to "create 100 email jobs for these users" and import the result.
471
+ - **Spreadsheet workflows**: Edit jobs in Excel/Google Sheets, export to CSV, and import into the queue.
472
+ - **Migration**: Move jobs between environments or recover from backups.
473
+ - **Testing**: Generate test datasets with specific job configurations.
474
+ - **Bulk operations**: Create thousands of jobs without writing code.
475
+
476
+ ```bash
477
+ # AI generates jobs.csv, then:
478
+ npx workmatic import ./jobs.db jobs.csv
479
+
480
+ # Or pipe directly from another tool:
481
+ cat ai-generated-jobs.csv | npx workmatic import ./jobs.db /dev/stdin
482
+ ```
483
+
484
+ The simple CSV format means any tool that can output text can create jobs for your queue.
485
+
486
+ ## Architecture
487
+
488
+ ```
489
+ ┌──────────────────────────────────────────────────────────┐
490
+ │ Your App │
491
+ ├────────────────────┬─────────────────────────────────────┤
492
+ │ Client │ Worker │
493
+ │ ┌─────────────┐ │ ┌─────────────┐ ┌─────────────┐ │
494
+ │ │ add() │ │ │ pump() │ │ fastq │ │
495
+ │ │ stats() │ │ │ claim() │ │ pool │ │
496
+ │ └─────────────┘ │ └─────────────┘ └─────────────┘ │
497
+ ├────────────────────┴─────────────────────────────────────┤
498
+ │ Kysely (Query Builder) │
499
+ ├──────────────────────────────────────────────────────────┤
500
+ │ better-sqlite3 (SQLite) │
501
+ ├──────────────────────────────────────────────────────────┤
502
+ │ jobs.db (File) │
503
+ └──────────────────────────────────────────────────────────┘
504
+ ```
505
+
506
+ ## License
507
+
508
+ MIT