@auxiora/job-queue 1.10.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 +191 -0
- package/dist/__tests__/db.test.d.ts +2 -0
- package/dist/__tests__/db.test.d.ts.map +1 -0
- package/dist/__tests__/db.test.js +293 -0
- package/dist/__tests__/db.test.js.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +22 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +124 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/queue.test.d.ts +2 -0
- package/dist/__tests__/queue.test.d.ts.map +1 -0
- package/dist/__tests__/queue.test.js +275 -0
- package/dist/__tests__/queue.test.js.map +1 -0
- package/dist/db.d.ts +30 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +183 -0
- package/dist/db.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/queue.d.ts +24 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +128 -0
- package/dist/queue.js.map +1 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { JobQueue } from '../queue.js';
|
|
6
|
+
import { JobDatabase } from '../db.js';
|
|
7
|
+
function makeDbPath() {
|
|
8
|
+
const dir = path.join(os.tmpdir(), `auxiora-integ-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
return path.join(dir, 'jobs.db');
|
|
11
|
+
}
|
|
12
|
+
function cleanup(dbPath) {
|
|
13
|
+
const dir = path.dirname(dbPath);
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
describe('integration', () => {
|
|
17
|
+
let dbPath;
|
|
18
|
+
let queue;
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
if (queue) {
|
|
21
|
+
try {
|
|
22
|
+
await queue.stop(2000);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// already stopped
|
|
26
|
+
}
|
|
27
|
+
queue = undefined;
|
|
28
|
+
}
|
|
29
|
+
if (dbPath)
|
|
30
|
+
cleanup(dbPath);
|
|
31
|
+
});
|
|
32
|
+
it('recovers a job left in running state by a previous process', async () => {
|
|
33
|
+
dbPath = makeDbPath();
|
|
34
|
+
// Simulate process 1: insert job, poll it to running, then crash (close DB)
|
|
35
|
+
const db1 = new JobDatabase(dbPath);
|
|
36
|
+
db1.insertJob({
|
|
37
|
+
type: 'email',
|
|
38
|
+
payload: { to: 'test@example.com' },
|
|
39
|
+
priority: 0,
|
|
40
|
+
maxAttempts: 3,
|
|
41
|
+
scheduledAt: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
const polled = db1.pollReady(1);
|
|
44
|
+
expect(polled).toHaveLength(1);
|
|
45
|
+
expect(polled[0].status).toBe('running');
|
|
46
|
+
db1.close();
|
|
47
|
+
// Simulate process 2: new JobQueue picks up the crashed job
|
|
48
|
+
const received = [];
|
|
49
|
+
queue = new JobQueue(dbPath, { pollIntervalMs: 50 });
|
|
50
|
+
queue.register('email', async (payload) => {
|
|
51
|
+
received.push(payload);
|
|
52
|
+
return 'sent';
|
|
53
|
+
});
|
|
54
|
+
queue.start();
|
|
55
|
+
await vi.waitFor(() => {
|
|
56
|
+
expect(received).toHaveLength(1);
|
|
57
|
+
}, { timeout: 2000 });
|
|
58
|
+
expect(received[0]).toEqual({ to: 'test@example.com' });
|
|
59
|
+
});
|
|
60
|
+
it('resumes from checkpoint after simulated crash', async () => {
|
|
61
|
+
dbPath = makeDbPath();
|
|
62
|
+
// Process 1: insert job, poll to running, save checkpoint at step 2, crash
|
|
63
|
+
const db1 = new JobDatabase(dbPath);
|
|
64
|
+
const jobId = db1.insertJob({
|
|
65
|
+
type: 'batch',
|
|
66
|
+
payload: { steps: ['a', 'b', 'c', 'd'] },
|
|
67
|
+
priority: 0,
|
|
68
|
+
maxAttempts: 3,
|
|
69
|
+
scheduledAt: Date.now(),
|
|
70
|
+
});
|
|
71
|
+
db1.pollReady(1);
|
|
72
|
+
db1.saveCheckpoint(jobId, { completedSteps: ['a', 'b'] });
|
|
73
|
+
db1.close();
|
|
74
|
+
// Process 2: handler reads checkpoint and continues
|
|
75
|
+
const executedSteps = [];
|
|
76
|
+
queue = new JobQueue(dbPath, { pollIntervalMs: 50 });
|
|
77
|
+
queue.register('batch', async (payload, ctx) => {
|
|
78
|
+
const checkpoint = ctx.getCheckpoint();
|
|
79
|
+
const alreadyDone = new Set(checkpoint?.completedSteps ?? []);
|
|
80
|
+
for (const step of payload.steps) {
|
|
81
|
+
if (!alreadyDone.has(step)) {
|
|
82
|
+
executedSteps.push(step);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return 'done';
|
|
86
|
+
});
|
|
87
|
+
queue.start();
|
|
88
|
+
await vi.waitFor(() => {
|
|
89
|
+
expect(executedSteps.length).toBeGreaterThan(0);
|
|
90
|
+
}, { timeout: 2000 });
|
|
91
|
+
// Only steps c and d should have been executed (a and b were checkpointed)
|
|
92
|
+
expect(executedSteps).toEqual(['c', 'd']);
|
|
93
|
+
});
|
|
94
|
+
it('runs multiple job types concurrently', async () => {
|
|
95
|
+
dbPath = makeDbPath();
|
|
96
|
+
queue = new JobQueue(dbPath, { pollIntervalMs: 50, concurrency: 5 });
|
|
97
|
+
const aCalls = [];
|
|
98
|
+
const bCalls = [];
|
|
99
|
+
queue.register('type-a', async (payload) => {
|
|
100
|
+
aCalls.push(payload);
|
|
101
|
+
return 'a-done';
|
|
102
|
+
});
|
|
103
|
+
queue.register('type-b', async (payload) => {
|
|
104
|
+
bCalls.push(payload);
|
|
105
|
+
return 'b-done';
|
|
106
|
+
});
|
|
107
|
+
queue.enqueue('type-a', { idx: 1 });
|
|
108
|
+
queue.enqueue('type-b', { idx: 2 });
|
|
109
|
+
queue.enqueue('type-a', { idx: 3 });
|
|
110
|
+
queue.start();
|
|
111
|
+
await vi.waitFor(() => {
|
|
112
|
+
expect(aCalls.length + bCalls.length).toBe(3);
|
|
113
|
+
}, { timeout: 2000 });
|
|
114
|
+
expect(aCalls).toHaveLength(2);
|
|
115
|
+
expect(bCalls).toHaveLength(1);
|
|
116
|
+
expect(aCalls.map((c) => c.idx).sort()).toEqual([1, 3]);
|
|
117
|
+
expect(bCalls[0]).toEqual({ idx: 2 });
|
|
118
|
+
// Verify all jobs completed
|
|
119
|
+
const stats = queue.getStats();
|
|
120
|
+
expect(stats.pending).toBe(0);
|
|
121
|
+
expect(stats.running).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.test.js","sourceRoot":"","sources":["../../src/__tests__/integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/queue.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { JobQueue } from '../queue.js';
|
|
6
|
+
import { NonRetryableError } from '../errors.js';
|
|
7
|
+
function makeDbPath() {
|
|
8
|
+
const dir = path.join(os.tmpdir(), `auxiora-queue-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
return path.join(dir, 'jobs.db');
|
|
11
|
+
}
|
|
12
|
+
function cleanup(dbPath) {
|
|
13
|
+
const dir = path.dirname(dbPath);
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
describe('JobQueue', () => {
|
|
17
|
+
let queue;
|
|
18
|
+
let dbPath;
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
try {
|
|
21
|
+
await queue.stop(2000);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// already stopped
|
|
25
|
+
}
|
|
26
|
+
cleanup(dbPath);
|
|
27
|
+
});
|
|
28
|
+
function createQueue(opts) {
|
|
29
|
+
dbPath = makeDbPath();
|
|
30
|
+
queue = new JobQueue(dbPath, { pollIntervalMs: 50, concurrency: 2, ...opts });
|
|
31
|
+
return queue;
|
|
32
|
+
}
|
|
33
|
+
describe('register and enqueue', () => {
|
|
34
|
+
it('handler runs for enqueued job', async () => {
|
|
35
|
+
const q = createQueue();
|
|
36
|
+
const results = [];
|
|
37
|
+
q.register('greet', async (payload) => {
|
|
38
|
+
results.push(payload.name);
|
|
39
|
+
return `hello ${payload.name}`;
|
|
40
|
+
});
|
|
41
|
+
const id = q.enqueue('greet', { name: 'world' });
|
|
42
|
+
expect(id).toBeDefined();
|
|
43
|
+
q.start();
|
|
44
|
+
await vi.waitFor(() => {
|
|
45
|
+
const job = q.getJob(id);
|
|
46
|
+
expect(job?.status).toBe('completed');
|
|
47
|
+
}, { timeout: 3000 });
|
|
48
|
+
expect(results).toEqual(['world']);
|
|
49
|
+
const job = q.getJob(id);
|
|
50
|
+
expect(job?.result).toBe('hello world');
|
|
51
|
+
});
|
|
52
|
+
it('throws on unregistered type', () => {
|
|
53
|
+
const q = createQueue();
|
|
54
|
+
expect(() => q.enqueue('unknown', {})).toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('scheduling', () => {
|
|
58
|
+
it('job delayed until scheduledAt', async () => {
|
|
59
|
+
const q = createQueue();
|
|
60
|
+
q.register('delayed', async () => 'done');
|
|
61
|
+
const scheduledAt = Date.now() + 500;
|
|
62
|
+
const id = q.enqueue('delayed', {}, { scheduledAt });
|
|
63
|
+
q.start();
|
|
64
|
+
// Should still be pending immediately
|
|
65
|
+
const jobEarly = q.getJob(id);
|
|
66
|
+
expect(jobEarly?.status).toBe('pending');
|
|
67
|
+
await vi.waitFor(() => {
|
|
68
|
+
const job = q.getJob(id);
|
|
69
|
+
expect(job?.status).toBe('completed');
|
|
70
|
+
}, { timeout: 3000 });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('priority', () => {
|
|
74
|
+
it('higher priority jobs processed first', async () => {
|
|
75
|
+
const q = createQueue({ concurrency: 1 });
|
|
76
|
+
const order = [];
|
|
77
|
+
q.register('ordered', async (payload) => {
|
|
78
|
+
order.push(payload.label);
|
|
79
|
+
});
|
|
80
|
+
q.enqueue('ordered', { label: 'low' }, { priority: 1 });
|
|
81
|
+
q.enqueue('ordered', { label: 'high' }, { priority: 10 });
|
|
82
|
+
q.enqueue('ordered', { label: 'mid' }, { priority: 5 });
|
|
83
|
+
q.start();
|
|
84
|
+
await vi.waitFor(() => {
|
|
85
|
+
expect(order).toHaveLength(3);
|
|
86
|
+
}, { timeout: 3000 });
|
|
87
|
+
expect(order).toEqual(['high', 'mid', 'low']);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('retry on failure', () => {
|
|
91
|
+
it('failing job retries up to maxAttempts, then succeeds', { timeout: 15000 }, async () => {
|
|
92
|
+
const q = createQueue();
|
|
93
|
+
let callCount = 0;
|
|
94
|
+
q.register('flaky', async () => {
|
|
95
|
+
callCount++;
|
|
96
|
+
if (callCount < 3) {
|
|
97
|
+
throw new Error('transient');
|
|
98
|
+
}
|
|
99
|
+
return 'ok';
|
|
100
|
+
});
|
|
101
|
+
const id = q.enqueue('flaky', {}, { maxAttempts: 5 });
|
|
102
|
+
q.start();
|
|
103
|
+
await vi.waitFor(() => {
|
|
104
|
+
const job = q.getJob(id);
|
|
105
|
+
expect(job?.status).toBe('completed');
|
|
106
|
+
}, { timeout: 10000 });
|
|
107
|
+
expect(callCount).toBe(3);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('NonRetryableError', () => {
|
|
111
|
+
it('skips retries, goes straight to dead', async () => {
|
|
112
|
+
const q = createQueue();
|
|
113
|
+
let callCount = 0;
|
|
114
|
+
q.register('fatal', async () => {
|
|
115
|
+
callCount++;
|
|
116
|
+
throw new NonRetryableError('permanent failure');
|
|
117
|
+
});
|
|
118
|
+
const id = q.enqueue('fatal', {}, { maxAttempts: 5 });
|
|
119
|
+
q.start();
|
|
120
|
+
await vi.waitFor(() => {
|
|
121
|
+
const job = q.getJob(id);
|
|
122
|
+
expect(job?.status).toBe('dead');
|
|
123
|
+
}, { timeout: 3000 });
|
|
124
|
+
expect(callCount).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('concurrency', () => {
|
|
128
|
+
it('runs up to N jobs in parallel, not more', { timeout: 15000 }, async () => {
|
|
129
|
+
const q = createQueue({ concurrency: 2 });
|
|
130
|
+
let concurrent = 0;
|
|
131
|
+
let maxConcurrent = 0;
|
|
132
|
+
let completedCount = 0;
|
|
133
|
+
q.register('slow', async () => {
|
|
134
|
+
concurrent++;
|
|
135
|
+
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
137
|
+
concurrent--;
|
|
138
|
+
completedCount++;
|
|
139
|
+
return 'done';
|
|
140
|
+
});
|
|
141
|
+
for (let i = 0; i < 6; i++) {
|
|
142
|
+
q.enqueue('slow', { i });
|
|
143
|
+
}
|
|
144
|
+
q.start();
|
|
145
|
+
await vi.waitFor(() => {
|
|
146
|
+
expect(completedCount).toBe(6);
|
|
147
|
+
}, { timeout: 10000 });
|
|
148
|
+
expect(maxConcurrent).toBeLessThanOrEqual(2);
|
|
149
|
+
expect(maxConcurrent).toBeGreaterThanOrEqual(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('events', () => {
|
|
153
|
+
it('emits job:started and job:completed', async () => {
|
|
154
|
+
const q = createQueue();
|
|
155
|
+
const events = [];
|
|
156
|
+
q.register('evented', async () => 'result');
|
|
157
|
+
q.on('job:started', () => events.push('started'));
|
|
158
|
+
q.on('job:completed', () => events.push('completed'));
|
|
159
|
+
const id = q.enqueue('evented', {});
|
|
160
|
+
q.start();
|
|
161
|
+
await vi.waitFor(() => {
|
|
162
|
+
expect(events).toContain('completed');
|
|
163
|
+
}, { timeout: 3000 });
|
|
164
|
+
expect(events).toContain('started');
|
|
165
|
+
expect(events).toContain('completed');
|
|
166
|
+
});
|
|
167
|
+
it('emits job:dead on NonRetryableError', async () => {
|
|
168
|
+
const q = createQueue();
|
|
169
|
+
const deadJobs = [];
|
|
170
|
+
q.register('doomed', async () => {
|
|
171
|
+
throw new NonRetryableError('nope');
|
|
172
|
+
});
|
|
173
|
+
q.on('job:dead', (data) => deadJobs.push(data.job.id));
|
|
174
|
+
const id = q.enqueue('doomed', {});
|
|
175
|
+
q.start();
|
|
176
|
+
await vi.waitFor(() => {
|
|
177
|
+
expect(deadJobs).toContain(id);
|
|
178
|
+
}, { timeout: 3000 });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('checkpoint', () => {
|
|
182
|
+
it('handler writes checkpoint, on retry reads it back', async () => {
|
|
183
|
+
const q = createQueue();
|
|
184
|
+
let callCount = 0;
|
|
185
|
+
const checkpointValues = [];
|
|
186
|
+
q.register('checkpointed', async (_payload, ctx) => {
|
|
187
|
+
callCount++;
|
|
188
|
+
const prev = ctx.getCheckpoint();
|
|
189
|
+
checkpointValues.push(prev);
|
|
190
|
+
if (callCount === 1) {
|
|
191
|
+
ctx.checkpoint({ step: 1 });
|
|
192
|
+
throw new Error('retry me');
|
|
193
|
+
}
|
|
194
|
+
return 'done';
|
|
195
|
+
});
|
|
196
|
+
const id = q.enqueue('checkpointed', {}, { maxAttempts: 3 });
|
|
197
|
+
q.start();
|
|
198
|
+
await vi.waitFor(() => {
|
|
199
|
+
const job = q.getJob(id);
|
|
200
|
+
expect(job?.status).toBe('completed');
|
|
201
|
+
}, { timeout: 10000 });
|
|
202
|
+
expect(callCount).toBe(2);
|
|
203
|
+
expect(checkpointValues[0]).toBeUndefined();
|
|
204
|
+
expect(checkpointValues[1]).toEqual({ step: 1 });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('graceful shutdown', () => {
|
|
208
|
+
it('stop() waits for running jobs to complete', async () => {
|
|
209
|
+
const q = createQueue({ concurrency: 1 });
|
|
210
|
+
let finished = false;
|
|
211
|
+
q.register('longish', async () => {
|
|
212
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
213
|
+
finished = true;
|
|
214
|
+
return 'done';
|
|
215
|
+
});
|
|
216
|
+
q.enqueue('longish', {});
|
|
217
|
+
q.start();
|
|
218
|
+
// Give tick time to pick up the job
|
|
219
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
220
|
+
await q.stop(5000);
|
|
221
|
+
expect(finished).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('crash recovery', () => {
|
|
225
|
+
it('first attempt throws, retry succeeds', async () => {
|
|
226
|
+
const q = createQueue();
|
|
227
|
+
let callCount = 0;
|
|
228
|
+
q.register('recoverable', async () => {
|
|
229
|
+
callCount++;
|
|
230
|
+
if (callCount === 1)
|
|
231
|
+
throw new Error('crash');
|
|
232
|
+
return 'recovered';
|
|
233
|
+
});
|
|
234
|
+
const id = q.enqueue('recoverable', {}, { maxAttempts: 3 });
|
|
235
|
+
q.start();
|
|
236
|
+
await vi.waitFor(() => {
|
|
237
|
+
const job = q.getJob(id);
|
|
238
|
+
expect(job?.status).toBe('completed');
|
|
239
|
+
}, { timeout: 10000 });
|
|
240
|
+
expect(callCount).toBe(2);
|
|
241
|
+
expect(q.getJob(id)?.result).toBe('recovered');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('getStats', () => {
|
|
245
|
+
it('returns pending/running counts', () => {
|
|
246
|
+
const q = createQueue();
|
|
247
|
+
q.register('stat-test', async () => {
|
|
248
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
249
|
+
});
|
|
250
|
+
q.enqueue('stat-test', {});
|
|
251
|
+
q.enqueue('stat-test', {});
|
|
252
|
+
const stats = q.getStats();
|
|
253
|
+
expect(stats.pending).toBe(2);
|
|
254
|
+
expect(stats.running).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe('getJob', () => {
|
|
258
|
+
it('returns job by id', () => {
|
|
259
|
+
const q = createQueue();
|
|
260
|
+
q.register('lookup', async () => 'ok');
|
|
261
|
+
const id = q.enqueue('lookup', { data: 123 });
|
|
262
|
+
const job = q.getJob(id);
|
|
263
|
+
expect(job).toBeDefined();
|
|
264
|
+
expect(job?.id).toBe(id);
|
|
265
|
+
expect(job?.type).toBe('lookup');
|
|
266
|
+
expect(job?.payload).toEqual({ data: 123 });
|
|
267
|
+
expect(job?.status).toBe('pending');
|
|
268
|
+
});
|
|
269
|
+
it('returns undefined for non-existent id', () => {
|
|
270
|
+
const q = createQueue();
|
|
271
|
+
expect(q.getJob('nonexistent')).toBeUndefined();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
//# sourceMappingURL=queue.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.test.js","sourceRoot":"","sources":["../../src/__tests__/queue.test.ts"],"names":[],"mappings":"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"}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Job, JobFilter, JobQueueStats } from './types.js';
|
|
2
|
+
export interface InsertJobInput {
|
|
3
|
+
type: string;
|
|
4
|
+
payload: unknown;
|
|
5
|
+
priority: number;
|
|
6
|
+
maxAttempts: number;
|
|
7
|
+
scheduledAt: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class JobDatabase {
|
|
10
|
+
private db;
|
|
11
|
+
constructor(dbPath: string);
|
|
12
|
+
private migrate;
|
|
13
|
+
insertJob(input: InsertJobInput): string;
|
|
14
|
+
getJob(id: string): Job | undefined;
|
|
15
|
+
pollReady(limit: number): Job[];
|
|
16
|
+
completeJob(id: string, result: unknown): void;
|
|
17
|
+
failJob(id: string, errorMsg: string): void;
|
|
18
|
+
killJob(id: string): void;
|
|
19
|
+
recoverCrashed(): number;
|
|
20
|
+
saveCheckpoint(jobId: string, data: unknown): void;
|
|
21
|
+
getCheckpoint<T = unknown>(jobId: string): T | undefined;
|
|
22
|
+
listJobs(filter?: JobFilter): Job[];
|
|
23
|
+
getStats(): JobQueueStats;
|
|
24
|
+
cleanupOld(maxAgeMs: number): number;
|
|
25
|
+
/** Test helper: force a timestamp column to a specific value. */
|
|
26
|
+
forceTimestamp(id: string, column: string, value: number): void;
|
|
27
|
+
close(): void;
|
|
28
|
+
private rowToJob;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=db.d.ts.map
|
package/dist/db.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAa,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAUD,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAe;gBAEb,MAAM,EAAE,MAAM;IAO1B,OAAO,CAAC,OAAO;IA6Bf,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM;IAUxC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAKnC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAoB/B,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAO9C,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAqB3C,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAOzB,cAAc,IAAI,MAAM;IA4BxB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IASlD,aAAa,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAOxD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE;IAwBnC,QAAQ,IAAI,aAAa;IA0BzB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAQpC,iEAAiE;IACjE,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQ/D,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,QAAQ;CAiBjB"}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
const ALLOWED_TIMESTAMP_COLUMNS = new Set([
|
|
4
|
+
'scheduled_at',
|
|
5
|
+
'started_at',
|
|
6
|
+
'completed_at',
|
|
7
|
+
'created_at',
|
|
8
|
+
'updated_at',
|
|
9
|
+
]);
|
|
10
|
+
export class JobDatabase {
|
|
11
|
+
db;
|
|
12
|
+
constructor(dbPath) {
|
|
13
|
+
this.db = new DatabaseSync(dbPath);
|
|
14
|
+
this.db.exec('PRAGMA journal_mode=WAL');
|
|
15
|
+
this.db.exec('PRAGMA foreign_keys=ON');
|
|
16
|
+
this.migrate();
|
|
17
|
+
}
|
|
18
|
+
migrate() {
|
|
19
|
+
this.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
type TEXT NOT NULL,
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
24
|
+
payload TEXT NOT NULL,
|
|
25
|
+
result TEXT,
|
|
26
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
29
|
+
scheduled_at INTEGER NOT NULL,
|
|
30
|
+
started_at INTEGER,
|
|
31
|
+
completed_at INTEGER,
|
|
32
|
+
created_at INTEGER NOT NULL,
|
|
33
|
+
updated_at INTEGER NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_poll ON jobs(status, scheduled_at, priority DESC);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(type, status);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS job_checkpoints (
|
|
40
|
+
job_id TEXT PRIMARY KEY REFERENCES jobs(id) ON DELETE CASCADE,
|
|
41
|
+
data TEXT NOT NULL,
|
|
42
|
+
updated_at INTEGER NOT NULL
|
|
43
|
+
);
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
insertJob(input) {
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
this.db.prepare(`INSERT INTO jobs (id, type, status, payload, priority, max_attempts, scheduled_at, created_at, updated_at)
|
|
50
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?)`).run(id, input.type, JSON.stringify(input.payload), input.priority, input.maxAttempts, input.scheduledAt, now, now);
|
|
51
|
+
return id;
|
|
52
|
+
}
|
|
53
|
+
getJob(id) {
|
|
54
|
+
const row = this.db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
|
|
55
|
+
return row ? this.rowToJob(row) : undefined;
|
|
56
|
+
}
|
|
57
|
+
pollReady(limit) {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const rows = this.db.prepare(`SELECT * FROM jobs
|
|
60
|
+
WHERE status = 'pending' AND scheduled_at <= ?
|
|
61
|
+
ORDER BY priority DESC, scheduled_at ASC
|
|
62
|
+
LIMIT ?`).all(now, limit);
|
|
63
|
+
const jobs = [];
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
const id = row.id;
|
|
66
|
+
this.db.prepare(`UPDATE jobs SET status = 'running', started_at = ?, updated_at = ? WHERE id = ?`).run(now, now, id);
|
|
67
|
+
jobs.push(this.rowToJob({ ...row, status: 'running', started_at: now, updated_at: now }));
|
|
68
|
+
}
|
|
69
|
+
return jobs;
|
|
70
|
+
}
|
|
71
|
+
completeJob(id, result) {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
this.db.prepare(`UPDATE jobs SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(result), now, now, id);
|
|
74
|
+
}
|
|
75
|
+
failJob(id, errorMsg) {
|
|
76
|
+
const row = this.db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
|
|
77
|
+
if (!row)
|
|
78
|
+
return;
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const nextAttempt = row.attempt + 1;
|
|
81
|
+
const maxAttempts = row.max_attempts;
|
|
82
|
+
if (nextAttempt >= maxAttempts) {
|
|
83
|
+
this.db.prepare(`UPDATE jobs SET status = 'dead', result = ?, attempt = ?, completed_at = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(errorMsg), nextAttempt, now, now, id);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const backoff = Math.pow(2, nextAttempt) * 1000;
|
|
87
|
+
const nextScheduledAt = now + backoff;
|
|
88
|
+
this.db.prepare(`UPDATE jobs SET status = 'pending', result = ?, attempt = ?, scheduled_at = ?, started_at = NULL, updated_at = ? WHERE id = ?`).run(JSON.stringify(errorMsg), nextAttempt, nextScheduledAt, now, id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
killJob(id) {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
this.db.prepare(`UPDATE jobs SET status = 'dead', completed_at = ?, updated_at = ? WHERE id = ?`).run(now, now, id);
|
|
94
|
+
}
|
|
95
|
+
recoverCrashed() {
|
|
96
|
+
const rows = this.db.prepare(`SELECT * FROM jobs WHERE status = 'running'`).all();
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
let count = 0;
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
const id = row.id;
|
|
101
|
+
const nextAttempt = row.attempt + 1;
|
|
102
|
+
const maxAttempts = row.max_attempts;
|
|
103
|
+
if (nextAttempt >= maxAttempts) {
|
|
104
|
+
this.db.prepare(`UPDATE jobs SET status = 'dead', completed_at = ?, updated_at = ? WHERE id = ?`).run(now, now, id);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.db.prepare(`UPDATE jobs SET status = 'pending', attempt = ?, started_at = NULL, updated_at = ? WHERE id = ?`).run(nextAttempt, now, id);
|
|
108
|
+
}
|
|
109
|
+
count++;
|
|
110
|
+
}
|
|
111
|
+
return count;
|
|
112
|
+
}
|
|
113
|
+
saveCheckpoint(jobId, data) {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
this.db.prepare(`INSERT INTO job_checkpoints (job_id, data, updated_at)
|
|
116
|
+
VALUES (?, ?, ?)
|
|
117
|
+
ON CONFLICT(job_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`).run(jobId, JSON.stringify(data), now);
|
|
118
|
+
}
|
|
119
|
+
getCheckpoint(jobId) {
|
|
120
|
+
const row = this.db.prepare('SELECT data FROM job_checkpoints WHERE job_id = ?').get(jobId);
|
|
121
|
+
return row ? JSON.parse(row.data) : undefined;
|
|
122
|
+
}
|
|
123
|
+
listJobs(filter) {
|
|
124
|
+
const conditions = [];
|
|
125
|
+
const params = [];
|
|
126
|
+
if (filter?.type) {
|
|
127
|
+
conditions.push('type = ?');
|
|
128
|
+
params.push(filter.type);
|
|
129
|
+
}
|
|
130
|
+
if (filter?.status) {
|
|
131
|
+
conditions.push('status = ?');
|
|
132
|
+
params.push(filter.status);
|
|
133
|
+
}
|
|
134
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
135
|
+
const limit = filter?.limit ?? 100;
|
|
136
|
+
params.push(limit);
|
|
137
|
+
const rows = this.db.prepare(`SELECT * FROM jobs ${where} ORDER BY created_at DESC LIMIT ?`).all(...params);
|
|
138
|
+
return rows.map(r => this.rowToJob(r));
|
|
139
|
+
}
|
|
140
|
+
getStats() {
|
|
141
|
+
const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
142
|
+
const pending = this.db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'pending'`).get().count;
|
|
143
|
+
const running = this.db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'running'`).get().count;
|
|
144
|
+
const completed24h = this.db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'completed' AND completed_at >= ?`).get(twentyFourHoursAgo).count;
|
|
145
|
+
const failed24h = this.db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'failed' AND completed_at >= ?`).get(twentyFourHoursAgo).count;
|
|
146
|
+
const dead = this.db.prepare(`SELECT COUNT(*) as count FROM jobs WHERE status = 'dead'`).get().count;
|
|
147
|
+
return { pending, running, completed24h, failed24h, dead };
|
|
148
|
+
}
|
|
149
|
+
cleanupOld(maxAgeMs) {
|
|
150
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
151
|
+
const result = this.db.prepare(`DELETE FROM jobs WHERE status IN ('completed', 'dead') AND completed_at < ?`).run(cutoff);
|
|
152
|
+
return Number(result.changes);
|
|
153
|
+
}
|
|
154
|
+
/** Test helper: force a timestamp column to a specific value. */
|
|
155
|
+
forceTimestamp(id, column, value) {
|
|
156
|
+
if (!ALLOWED_TIMESTAMP_COLUMNS.has(column)) {
|
|
157
|
+
throw new Error(`Column "${column}" is not an allowed timestamp column`);
|
|
158
|
+
}
|
|
159
|
+
// Column name is validated against whitelist above, safe to interpolate
|
|
160
|
+
this.db.prepare(`UPDATE jobs SET ${column} = ? WHERE id = ?`).run(value, id);
|
|
161
|
+
}
|
|
162
|
+
close() {
|
|
163
|
+
this.db.close();
|
|
164
|
+
}
|
|
165
|
+
rowToJob(row) {
|
|
166
|
+
return {
|
|
167
|
+
id: row.id,
|
|
168
|
+
type: row.type,
|
|
169
|
+
status: row.status,
|
|
170
|
+
payload: row.payload ? JSON.parse(row.payload) : undefined,
|
|
171
|
+
result: row.result != null ? JSON.parse(row.result) : undefined,
|
|
172
|
+
priority: row.priority,
|
|
173
|
+
attempt: row.attempt,
|
|
174
|
+
maxAttempts: row.max_attempts,
|
|
175
|
+
scheduledAt: row.scheduled_at,
|
|
176
|
+
startedAt: row.started_at ?? undefined,
|
|
177
|
+
completedAt: row.completed_at ?? undefined,
|
|
178
|
+
createdAt: row.created_at,
|
|
179
|
+
updatedAt: row.updated_at,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=db.js.map
|