workmatic 1.0.5 → 1.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 +114 -13
- package/dashboard/app.js +1 -3
- package/dashboard/index.html +0 -14
- package/dist/cli.cjs +905 -108
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +905 -108
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +360 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -6
- package/dist/index.d.ts +118 -6
- package/dist/index.js +357 -66
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A persistent job queue for Node.js using SQLite. Simple, reliable, and zero external dependencies beyond SQLite.
|
|
4
4
|
|
|
5
|
+
**Full single-page reference (same material, browsable offline): [`docs/index.html`](docs/index.html)**.
|
|
6
|
+
|
|
5
7
|
## Why?
|
|
6
8
|
|
|
7
9
|
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.
|
|
@@ -61,13 +63,57 @@ worker.process(async (job) => {
|
|
|
61
63
|
worker.start();
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
## Multi-queue orchestration
|
|
67
|
+
|
|
68
|
+
Use `createOrchestrator` to register several queues, control all workers together, and move jobs between queues:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createDatabase, createOrchestrator } from 'workmatic';
|
|
72
|
+
|
|
73
|
+
const db = createDatabase({ filename: './jobs.db' });
|
|
74
|
+
const orch = createOrchestrator({ db });
|
|
75
|
+
|
|
76
|
+
orch.register('emails', { worker: { concurrency: 4 } });
|
|
77
|
+
orch.register('reports', { worker: { concurrency: 1 } });
|
|
78
|
+
|
|
79
|
+
orch.process('emails', async (job) => { /* send email */ });
|
|
80
|
+
orch.process('reports', async (job) => { /* generate PDF */ });
|
|
81
|
+
|
|
82
|
+
await orch.client('emails').add({ to: 'user@example.com' });
|
|
83
|
+
|
|
84
|
+
orch.startAll();
|
|
85
|
+
|
|
86
|
+
// Move ready/dead jobs to another queue (e.g. retry pipeline)
|
|
87
|
+
await orch.transfer({ from: 'emails', to: 'emails-retry', status: 'dead', resetForRetry: true });
|
|
88
|
+
|
|
89
|
+
await orch.pause('reports');
|
|
90
|
+
await orch.resume('reports');
|
|
91
|
+
|
|
92
|
+
await orch.stopAll();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
CLI transfer: `workmatic transfer ./jobs.db emails emails-retry --status=dead --retry`
|
|
96
|
+
|
|
97
|
+
See [`examples/orchestrator.ts`](examples/orchestrator.ts) (transfer between queues) and [`examples/orchestrator-processors.ts`](examples/orchestrator-processors.ts) (different `process()` per queue).
|
|
98
|
+
|
|
99
|
+
## Testing
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm test
|
|
103
|
+
npm run test:coverage # requires ≥90% line/branch/function coverage
|
|
104
|
+
```
|
|
105
|
+
|
|
64
106
|
## API Reference
|
|
65
107
|
|
|
108
|
+
Browsable copy with anchored sections: **[`docs/index.html`](docs/index.html)** (API from [`#api-database`](docs/index.html#api-database)).
|
|
109
|
+
|
|
66
110
|
### `createDatabase(options)`
|
|
67
111
|
|
|
68
112
|
Initialize the database connection and schema.
|
|
69
113
|
|
|
70
114
|
```typescript
|
|
115
|
+
import { createDatabase, getUnderlyingDb } from 'workmatic';
|
|
116
|
+
|
|
71
117
|
const db = createDatabase({
|
|
72
118
|
// Option 1: File path (creates or opens existing)
|
|
73
119
|
filename: './jobs.db',
|
|
@@ -78,6 +124,9 @@ const db = createDatabase({
|
|
|
78
124
|
// Option 3: Existing better-sqlite3 instance
|
|
79
125
|
db: existingSqliteInstance,
|
|
80
126
|
});
|
|
127
|
+
|
|
128
|
+
// Underlying driver (only reliable for instances from createDatabase above)
|
|
129
|
+
const sqlite = getUnderlyingDb(db);
|
|
81
130
|
```
|
|
82
131
|
|
|
83
132
|
### `createClient(options)`
|
|
@@ -112,7 +161,18 @@ Get job statistics for the queue.
|
|
|
112
161
|
|
|
113
162
|
```typescript
|
|
114
163
|
const stats = await client.stats();
|
|
115
|
-
// { ready: 5, running: 2, done: 100,
|
|
164
|
+
// { ready: 5, running: 2, done: 100, dead: 1, total: 108 }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### `client.addMany(payloads, options?)`
|
|
168
|
+
|
|
169
|
+
Insert many jobs in a **single transaction**, sharing the same `priority`, `delayMs`, and `maxAttempts`.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const { ok, ids } = await client.addMany(
|
|
173
|
+
[{ email: 'a@x.com' }, { email: 'b@x.com' }],
|
|
174
|
+
{ priority: 1, delayMs: 0, maxAttempts: 3 }
|
|
175
|
+
);
|
|
116
176
|
```
|
|
117
177
|
|
|
118
178
|
#### `client.clear(options?)`
|
|
@@ -140,10 +200,13 @@ const worker = createWorker({
|
|
|
140
200
|
concurrency: 1, // Optional: Parallel job count (default: 1)
|
|
141
201
|
leaseMs: 30000, // Optional: Job lease duration in ms (default: 30000)
|
|
142
202
|
pollMs: 1000, // Optional: Poll interval when idle (default: 1000)
|
|
143
|
-
timeoutMs: 60000, // Optional: Job execution timeout in ms (default:
|
|
203
|
+
timeoutMs: 60000, // Optional: Job execution timeout in ms (default: 60000). Use 0 for no limit
|
|
144
204
|
backoff: (n) => 1000 * Math.pow(2, n), // Optional: Retry backoff function
|
|
145
205
|
persistState: false, // Optional: Persist worker state to database (default: false)
|
|
146
206
|
autoRestore: true, // Optional: Auto-restore state on creation (default: true)
|
|
207
|
+
pauseCheckIntervalMs: 300, // Optional: Min ms between DB pause checks (CLI pause). Default: 300
|
|
208
|
+
requeueExpiredIntervalMs: 0, // Optional: Min ms between lease requeue scans; 0 = every pump
|
|
209
|
+
onPumpError: (err) => { /* optional hook after default log */ },
|
|
147
210
|
});
|
|
148
211
|
```
|
|
149
212
|
|
|
@@ -178,6 +241,26 @@ Stop processing and wait for current jobs to finish.
|
|
|
178
241
|
await worker.stop();
|
|
179
242
|
```
|
|
180
243
|
|
|
244
|
+
### `createOrchestrator(options)`
|
|
245
|
+
|
|
246
|
+
Manage multiple queues with shared lifecycle and transfer helpers.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const orch = createOrchestrator({ db });
|
|
250
|
+
|
|
251
|
+
orch.register('emails', { worker: { concurrency: 4 } });
|
|
252
|
+
const client = orch.client('emails'); // or use return value of register()
|
|
253
|
+
|
|
254
|
+
orch.process('emails', async (job) => { /* ... */ });
|
|
255
|
+
orch.startAll();
|
|
256
|
+
await orch.stopAll();
|
|
257
|
+
|
|
258
|
+
await orch.transfer({ from: 'emails', to: 'retry', status: 'dead', resetForRetry: true });
|
|
259
|
+
await orch.moveJob(jobId, 'archive');
|
|
260
|
+
await orch.pause('emails');
|
|
261
|
+
await orch.resume('emails');
|
|
262
|
+
```
|
|
263
|
+
|
|
181
264
|
#### `worker.pause()` / `worker.resume()`
|
|
182
265
|
|
|
183
266
|
Pause and resume job processing.
|
|
@@ -330,7 +413,7 @@ interface Job<TPayload> {
|
|
|
330
413
|
id: string; // Unique public ID (nanoid)
|
|
331
414
|
queue: string; // Queue name
|
|
332
415
|
payload: TPayload; // Your job data
|
|
333
|
-
status: JobStatus; // 'ready' | 'running' | 'done' | '
|
|
416
|
+
status: JobStatus; // 'ready' | 'running' | 'done' | 'dead' while running
|
|
334
417
|
priority: number; // Priority value
|
|
335
418
|
attempts: number; // Current attempt count (starts at 0)
|
|
336
419
|
maxAttempts: number; // Maximum attempts allowed
|
|
@@ -406,11 +489,14 @@ worker.process(async (job) => {
|
|
|
406
489
|
| `concurrency` | `1` | Number of jobs to process in parallel |
|
|
407
490
|
| `leaseMs` | `30000` | How long a job is "locked" during processing |
|
|
408
491
|
| `pollMs` | `1000` | How often to check for new jobs when idle |
|
|
409
|
-
| `timeoutMs` | `
|
|
492
|
+
| `timeoutMs` | `60000` | Job timeout in ms (`0` = no limit) |
|
|
410
493
|
| `priority` | `0` | Job priority (lower = processed first) |
|
|
411
494
|
| `delayMs` | `0` | Delay before job becomes available |
|
|
412
495
|
| `maxAttempts` | `3` | Maximum processing attempts |
|
|
413
496
|
| `backoff` | `2^n * 1000` | Function returning retry delay in ms |
|
|
497
|
+
| `pauseCheckIntervalMs` | `300` | Throttle CLI/live pause checks from the pump loop |
|
|
498
|
+
| `requeueExpiredIntervalMs` | `0` | Throttle expired-lease requeue (0 = run every pump tick) |
|
|
499
|
+
| `onPumpError` | `undefined` | Callback after the default pump error log |
|
|
414
500
|
|
|
415
501
|
## Dashboard
|
|
416
502
|
|
|
@@ -450,6 +536,25 @@ npm run bench
|
|
|
450
536
|
npm run bench -- --file
|
|
451
537
|
```
|
|
452
538
|
|
|
539
|
+
### Micro benchmarks
|
|
540
|
+
|
|
541
|
+
Short suite: **2,000 sequential `add()` calls** (same code path as the full insert benchmark, smaller batch) plus **1,000 `client.stats()` calls** on a queue that already holds jobs. Useful for quick regression checks without running the full workload.
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
npm run bench:micro
|
|
545
|
+
# or: npm run bench -- --micro
|
|
546
|
+
|
|
547
|
+
npm run bench:micro -- --file
|
|
548
|
+
# or: npm run bench -- --micro --file
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
| Benchmark | In-Memory | File-based |
|
|
552
|
+
|-----------|-----------|------------|
|
|
553
|
+
| Micro Sequential Insert (2,000) | ~27,000/s | ~9,300/s |
|
|
554
|
+
| Stats Query (×1,000) | ~9,200/s | ~5,500/s |
|
|
555
|
+
|
|
556
|
+
Figures are rounded from a representative run; throughput changes with hardware, SQLite settings, and how “warm” the database is.
|
|
557
|
+
|
|
453
558
|
### Results Comparison
|
|
454
559
|
|
|
455
560
|
| Benchmark | In-Memory | File-based |
|
|
@@ -461,6 +566,8 @@ npm run bench -- --file
|
|
|
461
566
|
| Process (concurrency=8) | 10,000/s | 8,300/s |
|
|
462
567
|
| Process (concurrency=16) | 18,000/s | 5,700/s |
|
|
463
568
|
| Mixed Insert+Process | 7,500/s | 3,500/s |
|
|
569
|
+
| Micro Sequential Insert (2,000) | ~27,000/s | ~9,300/s |
|
|
570
|
+
| Stats Query (×1,000) | ~9,200/s | ~5,500/s |
|
|
464
571
|
| Claim + Process Batch | 23,600/s | 11,800/s |
|
|
465
572
|
|
|
466
573
|
**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.
|
|
@@ -485,15 +592,9 @@ npx workmatic list ./jobs.db --status=dead --limit=10
|
|
|
485
592
|
|
|
486
593
|
# Export jobs to CSV
|
|
487
594
|
npx workmatic export ./jobs.db backup.csv
|
|
488
|
-
npx workmatic export ./jobs.db --status=
|
|
489
|
-
|
|
490
|
-
# Import jobs from CSV
|
|
491
|
-
npx workmatic import ./jobs.db backup.csv
|
|
492
|
-
|
|
493
|
-
# Delete jobs by status
|
|
494
|
-
npx workmatic purge ./jobs.db --status=done
|
|
595
|
+
npx workmatic export ./jobs.db --status=dead > dead-export.csv
|
|
495
596
|
|
|
496
|
-
# Retry dead
|
|
597
|
+
# Retry dead jobs (reset to ready)
|
|
497
598
|
npx workmatic retry ./jobs.db --status=dead
|
|
498
599
|
```
|
|
499
600
|
|
|
@@ -515,7 +616,7 @@ npx workmatic retry ./jobs.db --status=dead
|
|
|
515
616
|
|
|
516
617
|
| Option | Description |
|
|
517
618
|
|--------|-------------|
|
|
518
|
-
| `--status=<status>` | Filter by status (ready/running/done/
|
|
619
|
+
| `--status=<status>` | Filter by status (ready/running/done/dead) |
|
|
519
620
|
| `--queue=<queue>` | Filter by queue name |
|
|
520
621
|
| `--limit=<n>` | Limit results (default: 100) |
|
|
521
622
|
|
package/dashboard/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Dashboard state
|
|
2
2
|
const state = {
|
|
3
|
-
stats: { ready: 0, running: 0, done: 0,
|
|
3
|
+
stats: { ready: 0, running: 0, done: 0, dead: 0, total: 0 },
|
|
4
4
|
jobs: [],
|
|
5
5
|
queues: [],
|
|
6
6
|
workers: [],
|
|
@@ -18,7 +18,6 @@ const elements = {
|
|
|
18
18
|
statReady: document.getElementById('stat-ready'),
|
|
19
19
|
statRunning: document.getElementById('stat-running'),
|
|
20
20
|
statDone: document.getElementById('stat-done'),
|
|
21
|
-
statFailed: document.getElementById('stat-failed'),
|
|
22
21
|
statDead: document.getElementById('stat-dead'),
|
|
23
22
|
workersSection: document.getElementById('workers-section'),
|
|
24
23
|
workersGrid: document.getElementById('workers-grid'),
|
|
@@ -95,7 +94,6 @@ function updateStatsUI() {
|
|
|
95
94
|
elements.statReady.textContent = state.stats.ready.toLocaleString();
|
|
96
95
|
elements.statRunning.textContent = state.stats.running.toLocaleString();
|
|
97
96
|
elements.statDone.textContent = state.stats.done.toLocaleString();
|
|
98
|
-
elements.statFailed.textContent = state.stats.failed.toLocaleString();
|
|
99
97
|
elements.statDead.textContent = state.stats.dead.toLocaleString();
|
|
100
98
|
}
|
|
101
99
|
|
package/dashboard/index.html
CHANGED
|
@@ -60,19 +60,6 @@
|
|
|
60
60
|
<span class="stat-label">Done</span>
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
|
-
<div class="stat-card failed">
|
|
64
|
-
<div class="stat-icon">
|
|
65
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
66
|
-
<circle cx="12" cy="12" r="10"/>
|
|
67
|
-
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
68
|
-
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
69
|
-
</svg>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="stat-content">
|
|
72
|
-
<span class="stat-value" id="stat-failed">0</span>
|
|
73
|
-
<span class="stat-label">Failed</span>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
63
|
<div class="stat-card dead">
|
|
77
64
|
<div class="stat-icon">
|
|
78
65
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -105,7 +92,6 @@
|
|
|
105
92
|
<option value="ready">Ready</option>
|
|
106
93
|
<option value="running">Running</option>
|
|
107
94
|
<option value="done">Done</option>
|
|
108
|
-
<option value="failed">Failed</option>
|
|
109
95
|
<option value="dead">Dead</option>
|
|
110
96
|
</select>
|
|
111
97
|
</div>
|