queue-jobs-worker 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/CHANGELOG.md +76 -0
- package/LICENSE +21 -0
- package/README.md +821 -0
- package/dist/core/backoff.d.ts +24 -0
- package/dist/core/backoff.d.ts.map +1 -0
- package/dist/core/client.d.ts +93 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/job.d.ts +75 -0
- package/dist/core/job.d.ts.map +1 -0
- package/dist/core/queue.d.ts +70 -0
- package/dist/core/queue.d.ts.map +1 -0
- package/dist/core/worker.d.ts +65 -0
- package/dist/core/worker.d.ts.map +1 -0
- package/dist/events/emitter.d.ts +24 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/index.cjs +2229 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2219 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/in-memory.adapter.d.ts +32 -0
- package/dist/storage/in-memory.adapter.d.ts.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/mysql.adapter.d.ts +37 -0
- package/dist/storage/mysql.adapter.d.ts.map +1 -0
- package/dist/storage/postgres.adapter.d.ts +37 -0
- package/dist/storage/postgres.adapter.d.ts.map +1 -0
- package/dist/storage/redis.adapter.d.ts +44 -0
- package/dist/storage/redis.adapter.d.ts.map +1 -0
- package/dist/types/client.types.d.ts +41 -0
- package/dist/types/client.types.d.ts.map +1 -0
- package/dist/types/events.types.d.ts +22 -0
- package/dist/types/events.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/job.types.d.ts +97 -0
- package/dist/types/job.types.d.ts.map +1 -0
- package/dist/types/queue.types.d.ts +43 -0
- package/dist/types/queue.types.d.ts.map +1 -0
- package/dist/types/storage.types.d.ts +120 -0
- package/dist/types/storage.types.d.ts.map +1 -0
- package/dist/types/worker.types.d.ts +25 -0
- package/dist/types/worker.types.d.ts.map +1 -0
- package/package.json +97 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var events = require('events');
|
|
5
|
+
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/storage/redis.adapter.ts
|
|
17
|
+
var redis_adapter_exports = {};
|
|
18
|
+
__export(redis_adapter_exports, {
|
|
19
|
+
RedisStorageAdapter: () => exports.RedisStorageAdapter
|
|
20
|
+
});
|
|
21
|
+
async function loadRedis() {
|
|
22
|
+
try {
|
|
23
|
+
return await import('redis');
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'RedisStorageAdapter requires the "redis" package (node-redis v4+).\nInstall it: npm install redis'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function jobToHash(job) {
|
|
31
|
+
return {
|
|
32
|
+
id: job.id,
|
|
33
|
+
queue: job.queue,
|
|
34
|
+
type: job.type,
|
|
35
|
+
payload: JSON.stringify(job.payload),
|
|
36
|
+
status: job.status,
|
|
37
|
+
attemptsMade: String(job.attemptsMade),
|
|
38
|
+
maxAttempts: String(job.maxAttempts),
|
|
39
|
+
retryDelay: String(job.retryDelay),
|
|
40
|
+
backoff: job.backoff,
|
|
41
|
+
timeout: String(job.timeout),
|
|
42
|
+
priority: String(job.priority),
|
|
43
|
+
runAt: job.runAt,
|
|
44
|
+
...job.cron !== void 0 ? { cron: job.cron } : {},
|
|
45
|
+
attempts: JSON.stringify(job.attempts),
|
|
46
|
+
lockId: job.lockId ?? "",
|
|
47
|
+
lockExpiresAt: job.lockExpiresAt ?? "",
|
|
48
|
+
createdAt: job.createdAt,
|
|
49
|
+
updatedAt: job.updatedAt,
|
|
50
|
+
completedAt: job.completedAt ?? "",
|
|
51
|
+
failedAt: job.failedAt ?? ""
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function hashToJob(h) {
|
|
55
|
+
const job = {
|
|
56
|
+
id: h["id"] ?? "",
|
|
57
|
+
queue: h["queue"] ?? "",
|
|
58
|
+
type: h["type"] ?? "",
|
|
59
|
+
payload: JSON.parse(h["payload"] ?? "null"),
|
|
60
|
+
status: h["status"] ?? "waiting",
|
|
61
|
+
attemptsMade: Number(h["attemptsMade"] ?? 0),
|
|
62
|
+
maxAttempts: Number(h["maxAttempts"] ?? 1),
|
|
63
|
+
retryDelay: Number(h["retryDelay"] ?? 1e3),
|
|
64
|
+
backoff: h["backoff"] ?? "exponential",
|
|
65
|
+
timeout: Number(h["timeout"] ?? 3e4),
|
|
66
|
+
priority: Number(h["priority"] ?? 0),
|
|
67
|
+
runAt: h["runAt"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
68
|
+
attempts: JSON.parse(h["attempts"] ?? "[]"),
|
|
69
|
+
lockId: h["lockId"] || null,
|
|
70
|
+
lockExpiresAt: h["lockExpiresAt"] || null,
|
|
71
|
+
createdAt: h["createdAt"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
72
|
+
updatedAt: h["updatedAt"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
73
|
+
completedAt: h["completedAt"] || null,
|
|
74
|
+
failedAt: h["failedAt"] || null
|
|
75
|
+
};
|
|
76
|
+
if (h["cron"]) job.cron = h["cron"];
|
|
77
|
+
return job;
|
|
78
|
+
}
|
|
79
|
+
var PREFIX, k, CLAIM_LUA; exports.RedisStorageAdapter = void 0;
|
|
80
|
+
var init_redis_adapter = __esm({
|
|
81
|
+
"src/storage/redis.adapter.ts"() {
|
|
82
|
+
PREFIX = "qjw:";
|
|
83
|
+
k = {
|
|
84
|
+
job: (id) => `${PREFIX}job:${id}`,
|
|
85
|
+
waiting: (q) => `${PREFIX}queue:${q}:waiting`,
|
|
86
|
+
delayed: (q) => `${PREFIX}queue:${q}:delayed`,
|
|
87
|
+
active: (q) => `${PREFIX}queue:${q}:active`,
|
|
88
|
+
completed: (q) => `${PREFIX}queue:${q}:completed`,
|
|
89
|
+
dead: (q) => `${PREFIX}queue:${q}:dead`,
|
|
90
|
+
rateCount: (q) => `${PREFIX}rate:${q}`,
|
|
91
|
+
rateTs: (q) => `${PREFIX}rate:${q}:ts`
|
|
92
|
+
};
|
|
93
|
+
CLAIM_LUA = `
|
|
94
|
+
local prefix = ARGV[4]
|
|
95
|
+
local now_ms = tonumber(ARGV[3])
|
|
96
|
+
|
|
97
|
+
-- Promote due delayed jobs into the waiting set.
|
|
98
|
+
local due = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now_ms)
|
|
99
|
+
for _, jid in ipairs(due) do
|
|
100
|
+
local pri_raw = redis.call('HGET', prefix .. 'job:' .. jid, 'priority')
|
|
101
|
+
local pri = tonumber(pri_raw) or 0
|
|
102
|
+
redis.call('ZADD', KEYS[1], -pri, jid)
|
|
103
|
+
redis.call('ZREM', KEYS[2], jid)
|
|
104
|
+
redis.call('HSET', prefix .. 'job:' .. jid, 'status', 'waiting')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
-- Pop the top-priority job.
|
|
108
|
+
local items = redis.call('ZPOPMIN', KEYS[1], 1)
|
|
109
|
+
if #items == 0 then return '' end
|
|
110
|
+
local job_id = items[1]
|
|
111
|
+
|
|
112
|
+
-- Lock it.
|
|
113
|
+
redis.call('SADD', KEYS[3], job_id)
|
|
114
|
+
redis.call('HSET', prefix .. 'job:' .. job_id,
|
|
115
|
+
'status', 'active',
|
|
116
|
+
'lockId', ARGV[1],
|
|
117
|
+
'lockExpiresAt', ARGV[2],
|
|
118
|
+
'updatedAt', ARGV[2]
|
|
119
|
+
)
|
|
120
|
+
return job_id
|
|
121
|
+
`;
|
|
122
|
+
exports.RedisStorageAdapter = class {
|
|
123
|
+
client;
|
|
124
|
+
url;
|
|
125
|
+
constructor(connectionString) {
|
|
126
|
+
this.url = connectionString;
|
|
127
|
+
}
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
// Lifecycle
|
|
130
|
+
// -------------------------------------------------------------------------
|
|
131
|
+
async initialize() {
|
|
132
|
+
const { createClient } = await loadRedis();
|
|
133
|
+
this.client = createClient({ url: this.url });
|
|
134
|
+
this.client.on("error", () => {
|
|
135
|
+
});
|
|
136
|
+
await this.client.connect();
|
|
137
|
+
const pong = await this.client.ping();
|
|
138
|
+
if (pong !== "PONG") {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"RedisStorageAdapter: PING returned unexpected response. Connection may be unhealthy."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async close() {
|
|
145
|
+
if (this.client) {
|
|
146
|
+
await this.client.quit();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
// Enqueue
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
async enqueue(input) {
|
|
153
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
154
|
+
const runAtMs = new Date(input.runAt).getTime();
|
|
155
|
+
const isDelayed = runAtMs > Date.now();
|
|
156
|
+
const job = {
|
|
157
|
+
id: input.id,
|
|
158
|
+
queue: input.queue,
|
|
159
|
+
type: input.type,
|
|
160
|
+
payload: input.payload,
|
|
161
|
+
status: isDelayed ? "delayed" : "waiting",
|
|
162
|
+
attemptsMade: 0,
|
|
163
|
+
maxAttempts: input.maxAttempts,
|
|
164
|
+
retryDelay: input.retryDelay,
|
|
165
|
+
backoff: input.backoff,
|
|
166
|
+
timeout: input.timeout,
|
|
167
|
+
priority: input.priority,
|
|
168
|
+
runAt: input.runAt,
|
|
169
|
+
...input.cron !== void 0 ? { cron: input.cron } : {},
|
|
170
|
+
attempts: [],
|
|
171
|
+
lockId: null,
|
|
172
|
+
lockExpiresAt: null,
|
|
173
|
+
createdAt: now,
|
|
174
|
+
updatedAt: now,
|
|
175
|
+
completedAt: null,
|
|
176
|
+
failedAt: null
|
|
177
|
+
};
|
|
178
|
+
const key = k.job(input.id);
|
|
179
|
+
const exists = await this.client.exists(key);
|
|
180
|
+
if (exists) {
|
|
181
|
+
const hash = await this.client.hGetAll(key);
|
|
182
|
+
return hashToJob(hash);
|
|
183
|
+
}
|
|
184
|
+
const multi = this.client.multi();
|
|
185
|
+
multi.hSet(key, jobToHash(job));
|
|
186
|
+
if (isDelayed) {
|
|
187
|
+
multi.zAdd(k.delayed(input.queue), { score: runAtMs, value: input.id });
|
|
188
|
+
} else {
|
|
189
|
+
multi.zAdd(k.waiting(input.queue), { score: -input.priority, value: input.id });
|
|
190
|
+
}
|
|
191
|
+
await multi.exec();
|
|
192
|
+
return job;
|
|
193
|
+
}
|
|
194
|
+
// -------------------------------------------------------------------------
|
|
195
|
+
// Claim (atomic Lua)
|
|
196
|
+
// -------------------------------------------------------------------------
|
|
197
|
+
async claim(input) {
|
|
198
|
+
const { queue, lockId, lockDuration, now } = input;
|
|
199
|
+
const nowMs = new Date(now).getTime();
|
|
200
|
+
const lockExpiresAt = new Date(nowMs + lockDuration).toISOString();
|
|
201
|
+
const jobId = await this.client.eval(CLAIM_LUA, {
|
|
202
|
+
keys: [k.waiting(queue), k.delayed(queue), k.active(queue)],
|
|
203
|
+
arguments: [lockId, lockExpiresAt, String(nowMs), PREFIX]
|
|
204
|
+
});
|
|
205
|
+
if (!jobId) return null;
|
|
206
|
+
const hash = await this.client.hGetAll(k.job(jobId));
|
|
207
|
+
if (!hash || Object.keys(hash).length === 0) return null;
|
|
208
|
+
return hashToJob(hash);
|
|
209
|
+
}
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// Complete
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
async complete(jobId) {
|
|
214
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
215
|
+
const hash = await this.client.hGetAll(k.job(jobId));
|
|
216
|
+
if (!hash) return;
|
|
217
|
+
const queue = hash["queue"] ?? "";
|
|
218
|
+
const multi = this.client.multi();
|
|
219
|
+
multi.hSet(k.job(jobId), {
|
|
220
|
+
status: "completed",
|
|
221
|
+
lockId: "",
|
|
222
|
+
lockExpiresAt: "",
|
|
223
|
+
completedAt: now,
|
|
224
|
+
updatedAt: now
|
|
225
|
+
});
|
|
226
|
+
multi.sRem(k.active(queue), jobId);
|
|
227
|
+
multi.sAdd(k.completed(queue), jobId);
|
|
228
|
+
await multi.exec();
|
|
229
|
+
}
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
// Requeue
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
async requeue(input) {
|
|
234
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
235
|
+
const hash = await this.client.hGetAll(k.job(input.jobId));
|
|
236
|
+
if (!hash) return;
|
|
237
|
+
const queue = hash["queue"] ?? "";
|
|
238
|
+
const attempts = JSON.parse(hash["attempts"] ?? "[]");
|
|
239
|
+
attempts.push({
|
|
240
|
+
attempt: input.attemptNumber,
|
|
241
|
+
startedAt: hash["updatedAt"] ?? now,
|
|
242
|
+
finishedAt: now,
|
|
243
|
+
error: input.error,
|
|
244
|
+
...input.stack !== void 0 ? { stack: input.stack } : {}
|
|
245
|
+
});
|
|
246
|
+
const multi = this.client.multi();
|
|
247
|
+
multi.hSet(k.job(input.jobId), {
|
|
248
|
+
status: "waiting",
|
|
249
|
+
attemptsMade: String(input.attemptNumber),
|
|
250
|
+
attempts: JSON.stringify(attempts),
|
|
251
|
+
runAt: input.runAt,
|
|
252
|
+
lockId: "",
|
|
253
|
+
lockExpiresAt: "",
|
|
254
|
+
updatedAt: now
|
|
255
|
+
});
|
|
256
|
+
multi.sRem(k.active(queue), input.jobId);
|
|
257
|
+
multi.zAdd(k.delayed(queue), {
|
|
258
|
+
score: new Date(input.runAt).getTime(),
|
|
259
|
+
value: input.jobId
|
|
260
|
+
});
|
|
261
|
+
await multi.exec();
|
|
262
|
+
}
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// Move to DLQ
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
async moveToDlq(input) {
|
|
267
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
268
|
+
const hash = await this.client.hGetAll(k.job(input.jobId));
|
|
269
|
+
if (!hash) return;
|
|
270
|
+
const queue = hash["queue"] ?? "";
|
|
271
|
+
const attempts = JSON.parse(hash["attempts"] ?? "[]");
|
|
272
|
+
attempts.push({
|
|
273
|
+
attempt: input.attemptNumber,
|
|
274
|
+
startedAt: hash["updatedAt"] ?? now,
|
|
275
|
+
finishedAt: now,
|
|
276
|
+
error: input.error,
|
|
277
|
+
...input.stack !== void 0 ? { stack: input.stack } : {}
|
|
278
|
+
});
|
|
279
|
+
const multi = this.client.multi();
|
|
280
|
+
multi.hSet(k.job(input.jobId), {
|
|
281
|
+
status: "dead",
|
|
282
|
+
attemptsMade: String(input.attemptNumber),
|
|
283
|
+
attempts: JSON.stringify(attempts),
|
|
284
|
+
lockId: "",
|
|
285
|
+
lockExpiresAt: "",
|
|
286
|
+
failedAt: now,
|
|
287
|
+
updatedAt: now
|
|
288
|
+
});
|
|
289
|
+
multi.sRem(k.active(queue), input.jobId);
|
|
290
|
+
multi.sAdd(k.dead(queue), input.jobId);
|
|
291
|
+
await multi.exec();
|
|
292
|
+
}
|
|
293
|
+
// -------------------------------------------------------------------------
|
|
294
|
+
// Release lock
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
async releaseLock(jobId) {
|
|
297
|
+
await this.client.hSet(k.job(jobId), {
|
|
298
|
+
lockId: "",
|
|
299
|
+
lockExpiresAt: "",
|
|
300
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// -------------------------------------------------------------------------
|
|
304
|
+
// Recover stalled jobs (batched — one pipeline per stalled job)
|
|
305
|
+
// -------------------------------------------------------------------------
|
|
306
|
+
async recoverStalledJobs(queue, now) {
|
|
307
|
+
const nowMs = new Date(now).getTime();
|
|
308
|
+
const activeIds = await this.client.sMembers(k.active(queue));
|
|
309
|
+
if (activeIds.length === 0) return [];
|
|
310
|
+
const fetchPipeline = this.client.multi();
|
|
311
|
+
for (const jobId of activeIds) {
|
|
312
|
+
fetchPipeline.hmGet(k.job(jobId), ["lockExpiresAt", "priority"]);
|
|
313
|
+
}
|
|
314
|
+
const fetchResults = await fetchPipeline.exec();
|
|
315
|
+
const recovered = [];
|
|
316
|
+
const recoverPipeline = this.client.multi();
|
|
317
|
+
for (let i = 0; i < activeIds.length; i++) {
|
|
318
|
+
const jobId = activeIds[i];
|
|
319
|
+
const fields = fetchResults[i];
|
|
320
|
+
if (!fields) continue;
|
|
321
|
+
const [lockExpiresAt, priorityStr] = fields;
|
|
322
|
+
if (!lockExpiresAt) continue;
|
|
323
|
+
if (new Date(lockExpiresAt).getTime() > nowMs) continue;
|
|
324
|
+
const priority = Number(priorityStr ?? "0");
|
|
325
|
+
recoverPipeline.hSet(k.job(jobId), {
|
|
326
|
+
status: "waiting",
|
|
327
|
+
lockId: "",
|
|
328
|
+
lockExpiresAt: "",
|
|
329
|
+
updatedAt: now
|
|
330
|
+
});
|
|
331
|
+
recoverPipeline.sRem(k.active(queue), jobId);
|
|
332
|
+
recoverPipeline.zAdd(k.waiting(queue), { score: -priority, value: jobId });
|
|
333
|
+
recovered.push(jobId);
|
|
334
|
+
}
|
|
335
|
+
if (recovered.length > 0) {
|
|
336
|
+
await recoverPipeline.exec();
|
|
337
|
+
}
|
|
338
|
+
return recovered;
|
|
339
|
+
}
|
|
340
|
+
// -------------------------------------------------------------------------
|
|
341
|
+
// Reads
|
|
342
|
+
// -------------------------------------------------------------------------
|
|
343
|
+
async getJob(jobId) {
|
|
344
|
+
const hash = await this.client.hGetAll(k.job(jobId));
|
|
345
|
+
if (!hash || Object.keys(hash).length === 0) return null;
|
|
346
|
+
return hashToJob(hash);
|
|
347
|
+
}
|
|
348
|
+
async getJobs(filter) {
|
|
349
|
+
const { queue, status, limit = 100, offset = 0 } = filter;
|
|
350
|
+
let ids = [];
|
|
351
|
+
if (queue && status) {
|
|
352
|
+
switch (status) {
|
|
353
|
+
case "waiting":
|
|
354
|
+
ids = await this.client.zRange(k.waiting(queue), 0, -1);
|
|
355
|
+
break;
|
|
356
|
+
case "delayed":
|
|
357
|
+
ids = await this.client.zRange(k.delayed(queue), 0, -1);
|
|
358
|
+
break;
|
|
359
|
+
case "active":
|
|
360
|
+
ids = await this.client.sMembers(k.active(queue));
|
|
361
|
+
break;
|
|
362
|
+
case "completed":
|
|
363
|
+
ids = await this.client.sMembers(k.completed(queue));
|
|
364
|
+
break;
|
|
365
|
+
case "dead":
|
|
366
|
+
ids = await this.client.sMembers(k.dead(queue));
|
|
367
|
+
break;
|
|
368
|
+
default:
|
|
369
|
+
ids = [];
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
for await (const key of this.client.scanIterator({
|
|
373
|
+
MATCH: `${PREFIX}job:*`,
|
|
374
|
+
COUNT: 500
|
|
375
|
+
})) {
|
|
376
|
+
const keyStr = Array.isArray(key) ? String(key[0]) : String(key);
|
|
377
|
+
ids.push(keyStr.replace(`${PREFIX}job:`, ""));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const results = [];
|
|
381
|
+
const page = ids.slice(offset, offset + limit);
|
|
382
|
+
for (const id of page) {
|
|
383
|
+
const job = await this.getJob(id);
|
|
384
|
+
if (!job) continue;
|
|
385
|
+
if (queue && job.queue !== queue) continue;
|
|
386
|
+
if (status && job.status !== status) continue;
|
|
387
|
+
results.push(job);
|
|
388
|
+
}
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
async getJobCounts(queue) {
|
|
392
|
+
const [waiting, delayed, active, completed, dead] = await Promise.all([
|
|
393
|
+
this.client.zCard(k.waiting(queue)),
|
|
394
|
+
this.client.zCard(k.delayed(queue)),
|
|
395
|
+
this.client.sCard(k.active(queue)),
|
|
396
|
+
this.client.sCard(k.completed(queue)),
|
|
397
|
+
this.client.sCard(k.dead(queue))
|
|
398
|
+
]);
|
|
399
|
+
return { waiting, delayed, active, completed, dead };
|
|
400
|
+
}
|
|
401
|
+
// -------------------------------------------------------------------------
|
|
402
|
+
// Rate limiting
|
|
403
|
+
// -------------------------------------------------------------------------
|
|
404
|
+
async checkAndIncrementRateLimit(queue, max, windowMs, now) {
|
|
405
|
+
const nowMs = new Date(now).getTime();
|
|
406
|
+
const ck = k.rateCount(queue);
|
|
407
|
+
const tk = k.rateTs(queue);
|
|
408
|
+
const windowStart = await this.client.get(tk);
|
|
409
|
+
const ttlSec = Math.ceil(windowMs / 1e3);
|
|
410
|
+
if (!windowStart || nowMs - Number(windowStart) >= windowMs) {
|
|
411
|
+
const multi = this.client.multi();
|
|
412
|
+
multi.set(ck, "1");
|
|
413
|
+
multi.expire(ck, ttlSec);
|
|
414
|
+
multi.set(tk, String(nowMs));
|
|
415
|
+
multi.expire(tk, ttlSec);
|
|
416
|
+
await multi.exec();
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
const count = await this.client.incr(ck);
|
|
420
|
+
if (count > max) {
|
|
421
|
+
await this.client.decr(ck);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// src/storage/postgres.adapter.ts
|
|
431
|
+
var postgres_adapter_exports = {};
|
|
432
|
+
__export(postgres_adapter_exports, {
|
|
433
|
+
PostgreSQLStorageAdapter: () => exports.PostgreSQLStorageAdapter
|
|
434
|
+
});
|
|
435
|
+
async function loadPg() {
|
|
436
|
+
try {
|
|
437
|
+
return await import('pg');
|
|
438
|
+
} catch {
|
|
439
|
+
throw new Error(
|
|
440
|
+
'PostgreSQLStorageAdapter requires the "pg" package (node-postgres).\nInstall it: npm install pg'
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function rowToJob(row) {
|
|
445
|
+
const job = {
|
|
446
|
+
id: row.id,
|
|
447
|
+
queue: row.queue,
|
|
448
|
+
type: row.type,
|
|
449
|
+
payload: row.payload,
|
|
450
|
+
status: row.status,
|
|
451
|
+
attemptsMade: row.attempts_made,
|
|
452
|
+
maxAttempts: row.max_attempts,
|
|
453
|
+
retryDelay: row.retry_delay,
|
|
454
|
+
backoff: row.backoff,
|
|
455
|
+
timeout: row.timeout,
|
|
456
|
+
priority: row.priority,
|
|
457
|
+
runAt: row.run_at.toISOString(),
|
|
458
|
+
attempts: row.attempts,
|
|
459
|
+
lockId: row.lock_id,
|
|
460
|
+
lockExpiresAt: row.lock_expires_at ? row.lock_expires_at.toISOString() : null,
|
|
461
|
+
createdAt: row.created_at.toISOString(),
|
|
462
|
+
updatedAt: row.updated_at.toISOString(),
|
|
463
|
+
completedAt: row.completed_at ? row.completed_at.toISOString() : null,
|
|
464
|
+
failedAt: row.failed_at ? row.failed_at.toISOString() : null
|
|
465
|
+
};
|
|
466
|
+
if (row.cron) job.cron = row.cron;
|
|
467
|
+
return job;
|
|
468
|
+
}
|
|
469
|
+
var CREATE_JOBS_TABLE, CREATE_RATE_TABLE; exports.PostgreSQLStorageAdapter = void 0;
|
|
470
|
+
var init_postgres_adapter = __esm({
|
|
471
|
+
"src/storage/postgres.adapter.ts"() {
|
|
472
|
+
CREATE_JOBS_TABLE = `
|
|
473
|
+
CREATE TABLE IF NOT EXISTS qjw_jobs (
|
|
474
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
475
|
+
queue TEXT NOT NULL,
|
|
476
|
+
type TEXT NOT NULL,
|
|
477
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
478
|
+
status TEXT NOT NULL DEFAULT 'waiting',
|
|
479
|
+
attempts_made INTEGER NOT NULL DEFAULT 0,
|
|
480
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
481
|
+
retry_delay INTEGER NOT NULL DEFAULT 1000,
|
|
482
|
+
backoff TEXT NOT NULL DEFAULT 'exponential',
|
|
483
|
+
timeout INTEGER NOT NULL DEFAULT 30000,
|
|
484
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
485
|
+
run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
486
|
+
cron TEXT,
|
|
487
|
+
attempts JSONB NOT NULL DEFAULT '[]',
|
|
488
|
+
lock_id TEXT,
|
|
489
|
+
lock_expires_at TIMESTAMPTZ,
|
|
490
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
491
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
492
|
+
completed_at TIMESTAMPTZ,
|
|
493
|
+
failed_at TIMESTAMPTZ
|
|
494
|
+
);
|
|
495
|
+
CREATE INDEX IF NOT EXISTS qjw_jobs_claim_idx
|
|
496
|
+
ON qjw_jobs (queue, status, run_at, priority DESC)
|
|
497
|
+
WHERE status IN ('waiting', 'delayed');
|
|
498
|
+
`;
|
|
499
|
+
CREATE_RATE_TABLE = `
|
|
500
|
+
CREATE TABLE IF NOT EXISTS qjw_rate_limits (
|
|
501
|
+
queue TEXT NOT NULL PRIMARY KEY,
|
|
502
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
503
|
+
window_start BIGINT NOT NULL DEFAULT 0
|
|
504
|
+
);
|
|
505
|
+
`;
|
|
506
|
+
exports.PostgreSQLStorageAdapter = class {
|
|
507
|
+
pool;
|
|
508
|
+
connectionString;
|
|
509
|
+
constructor(connectionString) {
|
|
510
|
+
this.connectionString = connectionString;
|
|
511
|
+
}
|
|
512
|
+
// -------------------------------------------------------------------------
|
|
513
|
+
// Lifecycle
|
|
514
|
+
// -------------------------------------------------------------------------
|
|
515
|
+
async initialize() {
|
|
516
|
+
const { Pool } = await loadPg();
|
|
517
|
+
this.pool = new Pool({ connectionString: this.connectionString });
|
|
518
|
+
const client = await this.pool.connect();
|
|
519
|
+
try {
|
|
520
|
+
await client.query("SELECT 1");
|
|
521
|
+
await client.query(CREATE_JOBS_TABLE);
|
|
522
|
+
await client.query(CREATE_RATE_TABLE);
|
|
523
|
+
} finally {
|
|
524
|
+
client.release();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async close() {
|
|
528
|
+
if (this.pool) {
|
|
529
|
+
await this.pool.end();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// -------------------------------------------------------------------------
|
|
533
|
+
// Enqueue
|
|
534
|
+
// -------------------------------------------------------------------------
|
|
535
|
+
async enqueue(input) {
|
|
536
|
+
const res = await this.pool.query(
|
|
537
|
+
`INSERT INTO qjw_jobs
|
|
538
|
+
(id, queue, type, payload, status, attempts_made, max_attempts,
|
|
539
|
+
retry_delay, backoff, timeout, priority, run_at, cron,
|
|
540
|
+
attempts, lock_id, lock_expires_at, created_at, updated_at,
|
|
541
|
+
completed_at, failed_at)
|
|
542
|
+
VALUES ($1,$2,$3,$4,
|
|
543
|
+
CASE WHEN $5::timestamptz > NOW() THEN 'delayed' ELSE 'waiting' END,
|
|
544
|
+
0,$6,$7,$8,$9,$10,$5,$11,
|
|
545
|
+
'[]'::jsonb, NULL, NULL, NOW(), NOW(), NULL, NULL)
|
|
546
|
+
ON CONFLICT (id) DO NOTHING
|
|
547
|
+
RETURNING *`,
|
|
548
|
+
[
|
|
549
|
+
input.id,
|
|
550
|
+
input.queue,
|
|
551
|
+
input.type,
|
|
552
|
+
JSON.stringify(input.payload),
|
|
553
|
+
input.runAt,
|
|
554
|
+
input.maxAttempts,
|
|
555
|
+
input.retryDelay,
|
|
556
|
+
input.backoff,
|
|
557
|
+
input.timeout,
|
|
558
|
+
input.priority,
|
|
559
|
+
input.cron ?? null
|
|
560
|
+
]
|
|
561
|
+
);
|
|
562
|
+
if (res.rows.length > 0) {
|
|
563
|
+
return rowToJob(res.rows[0]);
|
|
564
|
+
}
|
|
565
|
+
const existing = await this.pool.query("SELECT * FROM qjw_jobs WHERE id = $1", [
|
|
566
|
+
input.id
|
|
567
|
+
]);
|
|
568
|
+
return rowToJob(existing.rows[0]);
|
|
569
|
+
}
|
|
570
|
+
// -------------------------------------------------------------------------
|
|
571
|
+
// Claim (SELECT … FOR UPDATE SKIP LOCKED)
|
|
572
|
+
// -------------------------------------------------------------------------
|
|
573
|
+
async claim(input) {
|
|
574
|
+
const { queue, lockId, lockDuration, now } = input;
|
|
575
|
+
const lockExpiresAt = new Date(new Date(now).getTime() + lockDuration).toISOString();
|
|
576
|
+
const client = await this.pool.connect();
|
|
577
|
+
try {
|
|
578
|
+
await client.query("BEGIN");
|
|
579
|
+
const res = await client.query(
|
|
580
|
+
`SELECT * FROM qjw_jobs
|
|
581
|
+
WHERE queue = $1
|
|
582
|
+
AND status IN ('waiting', 'delayed')
|
|
583
|
+
AND run_at <= $2::timestamptz
|
|
584
|
+
AND (lock_expires_at IS NULL OR lock_expires_at <= $2::timestamptz)
|
|
585
|
+
ORDER BY priority DESC, run_at ASC, created_at ASC
|
|
586
|
+
LIMIT 1
|
|
587
|
+
FOR UPDATE SKIP LOCKED`,
|
|
588
|
+
[queue, now]
|
|
589
|
+
);
|
|
590
|
+
if (res.rows.length === 0) {
|
|
591
|
+
await client.query("ROLLBACK");
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const row = res.rows[0];
|
|
595
|
+
await client.query(
|
|
596
|
+
`UPDATE qjw_jobs
|
|
597
|
+
SET status = 'active', lock_id = $1, lock_expires_at = $2, updated_at = NOW()
|
|
598
|
+
WHERE id = $3`,
|
|
599
|
+
[lockId, lockExpiresAt, row.id]
|
|
600
|
+
);
|
|
601
|
+
await client.query("COMMIT");
|
|
602
|
+
const updated = await this.pool.query("SELECT * FROM qjw_jobs WHERE id = $1", [
|
|
603
|
+
row.id
|
|
604
|
+
]);
|
|
605
|
+
return rowToJob(updated.rows[0]);
|
|
606
|
+
} catch (err) {
|
|
607
|
+
await client.query("ROLLBACK");
|
|
608
|
+
throw err;
|
|
609
|
+
} finally {
|
|
610
|
+
client.release();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// -------------------------------------------------------------------------
|
|
614
|
+
// Complete
|
|
615
|
+
// -------------------------------------------------------------------------
|
|
616
|
+
async complete(jobId) {
|
|
617
|
+
await this.pool.query(
|
|
618
|
+
`UPDATE qjw_jobs
|
|
619
|
+
SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
|
|
620
|
+
completed_at = NOW(), updated_at = NOW()
|
|
621
|
+
WHERE id = $1`,
|
|
622
|
+
[jobId]
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
// -------------------------------------------------------------------------
|
|
626
|
+
// Requeue
|
|
627
|
+
// -------------------------------------------------------------------------
|
|
628
|
+
async requeue(input) {
|
|
629
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
630
|
+
const attempt = {
|
|
631
|
+
attempt: input.attemptNumber,
|
|
632
|
+
startedAt: now,
|
|
633
|
+
finishedAt: now,
|
|
634
|
+
error: input.error,
|
|
635
|
+
...input.stack !== void 0 ? { stack: input.stack } : {}
|
|
636
|
+
};
|
|
637
|
+
await this.pool.query(
|
|
638
|
+
`UPDATE qjw_jobs
|
|
639
|
+
SET status = 'waiting',
|
|
640
|
+
attempts_made = attempts_made + 1,
|
|
641
|
+
attempts = attempts || $1::jsonb,
|
|
642
|
+
run_at = $2::timestamptz,
|
|
643
|
+
lock_id = NULL,
|
|
644
|
+
lock_expires_at = NULL,
|
|
645
|
+
updated_at = NOW()
|
|
646
|
+
WHERE id = $3`,
|
|
647
|
+
[JSON.stringify([attempt]), input.runAt, input.jobId]
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
// -------------------------------------------------------------------------
|
|
651
|
+
// Move to DLQ
|
|
652
|
+
// -------------------------------------------------------------------------
|
|
653
|
+
async moveToDlq(input) {
|
|
654
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
655
|
+
const attempt = {
|
|
656
|
+
attempt: input.attemptNumber,
|
|
657
|
+
startedAt: now,
|
|
658
|
+
finishedAt: now,
|
|
659
|
+
error: input.error,
|
|
660
|
+
...input.stack !== void 0 ? { stack: input.stack } : {}
|
|
661
|
+
};
|
|
662
|
+
await this.pool.query(
|
|
663
|
+
`UPDATE qjw_jobs
|
|
664
|
+
SET status = 'dead',
|
|
665
|
+
attempts_made = attempts_made + 1,
|
|
666
|
+
attempts = attempts || $1::jsonb,
|
|
667
|
+
lock_id = NULL,
|
|
668
|
+
lock_expires_at = NULL,
|
|
669
|
+
failed_at = NOW(),
|
|
670
|
+
updated_at = NOW()
|
|
671
|
+
WHERE id = $2`,
|
|
672
|
+
[JSON.stringify([attempt]), input.jobId]
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
// -------------------------------------------------------------------------
|
|
676
|
+
// Release lock
|
|
677
|
+
// -------------------------------------------------------------------------
|
|
678
|
+
async releaseLock(jobId) {
|
|
679
|
+
await this.pool.query(
|
|
680
|
+
`UPDATE qjw_jobs
|
|
681
|
+
SET lock_id = NULL, lock_expires_at = NULL, updated_at = NOW()
|
|
682
|
+
WHERE id = $1`,
|
|
683
|
+
[jobId]
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
// -------------------------------------------------------------------------
|
|
687
|
+
// Recover stalled jobs
|
|
688
|
+
// -------------------------------------------------------------------------
|
|
689
|
+
async recoverStalledJobs(queue, now) {
|
|
690
|
+
const res = await this.pool.query(
|
|
691
|
+
`UPDATE qjw_jobs
|
|
692
|
+
SET status = 'waiting', lock_id = NULL, lock_expires_at = NULL, updated_at = NOW()
|
|
693
|
+
WHERE queue = $1
|
|
694
|
+
AND status = 'active'
|
|
695
|
+
AND lock_expires_at <= $2::timestamptz
|
|
696
|
+
RETURNING id`,
|
|
697
|
+
[queue, now]
|
|
698
|
+
);
|
|
699
|
+
return res.rows.map((r) => r.id);
|
|
700
|
+
}
|
|
701
|
+
// -------------------------------------------------------------------------
|
|
702
|
+
// Reads
|
|
703
|
+
// -------------------------------------------------------------------------
|
|
704
|
+
async getJob(jobId) {
|
|
705
|
+
const res = await this.pool.query("SELECT * FROM qjw_jobs WHERE id = $1", [jobId]);
|
|
706
|
+
if (res.rows.length === 0) return null;
|
|
707
|
+
return rowToJob(res.rows[0]);
|
|
708
|
+
}
|
|
709
|
+
async getJobs(filter) {
|
|
710
|
+
const { queue, status, limit = 100, offset = 0 } = filter;
|
|
711
|
+
const conditions = [];
|
|
712
|
+
const params = [];
|
|
713
|
+
let idx = 1;
|
|
714
|
+
if (queue !== void 0) {
|
|
715
|
+
conditions.push(`queue = $${idx++}`);
|
|
716
|
+
params.push(queue);
|
|
717
|
+
}
|
|
718
|
+
if (status !== void 0) {
|
|
719
|
+
conditions.push(`status = $${idx++}`);
|
|
720
|
+
params.push(status);
|
|
721
|
+
}
|
|
722
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
723
|
+
params.push(limit, offset);
|
|
724
|
+
const res = await this.pool.query(
|
|
725
|
+
`SELECT * FROM qjw_jobs ${where}
|
|
726
|
+
ORDER BY priority DESC, run_at ASC, created_at ASC
|
|
727
|
+
LIMIT $${idx++} OFFSET $${idx}`,
|
|
728
|
+
params
|
|
729
|
+
);
|
|
730
|
+
return res.rows.map((r) => rowToJob(r));
|
|
731
|
+
}
|
|
732
|
+
async getJobCounts(queue) {
|
|
733
|
+
const res = await this.pool.query(
|
|
734
|
+
"SELECT status, COUNT(*)::int AS count FROM qjw_jobs WHERE queue = $1 GROUP BY status",
|
|
735
|
+
[queue]
|
|
736
|
+
);
|
|
737
|
+
const counts = {
|
|
738
|
+
waiting: 0,
|
|
739
|
+
active: 0,
|
|
740
|
+
completed: 0,
|
|
741
|
+
delayed: 0,
|
|
742
|
+
dead: 0
|
|
743
|
+
};
|
|
744
|
+
for (const row of res.rows) {
|
|
745
|
+
if (row.status in counts) {
|
|
746
|
+
counts[row.status] = Number(row.count);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return counts;
|
|
750
|
+
}
|
|
751
|
+
// -------------------------------------------------------------------------
|
|
752
|
+
// Rate limiting
|
|
753
|
+
// -------------------------------------------------------------------------
|
|
754
|
+
async checkAndIncrementRateLimit(queue, max, windowMs, now) {
|
|
755
|
+
const nowMs = new Date(now).getTime();
|
|
756
|
+
const res = await this.pool.query(
|
|
757
|
+
"SELECT count, window_start FROM qjw_rate_limits WHERE queue = $1",
|
|
758
|
+
[queue]
|
|
759
|
+
);
|
|
760
|
+
if (res.rows.length === 0 || nowMs - Number(res.rows[0]?.window_start ?? 0) >= windowMs) {
|
|
761
|
+
await this.pool.query(
|
|
762
|
+
`INSERT INTO qjw_rate_limits (queue, count, window_start)
|
|
763
|
+
VALUES ($1, 1, $2)
|
|
764
|
+
ON CONFLICT (queue) DO UPDATE
|
|
765
|
+
SET count = 1, window_start = EXCLUDED.window_start`,
|
|
766
|
+
[queue, nowMs]
|
|
767
|
+
);
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
const current = res.rows[0].count;
|
|
771
|
+
if (current >= max) return false;
|
|
772
|
+
await this.pool.query("UPDATE qjw_rate_limits SET count = count + 1 WHERE queue = $1", [queue]);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// src/storage/mysql.adapter.ts
|
|
780
|
+
var mysql_adapter_exports = {};
|
|
781
|
+
__export(mysql_adapter_exports, {
|
|
782
|
+
MySQLStorageAdapter: () => exports.MySQLStorageAdapter
|
|
783
|
+
});
|
|
784
|
+
async function loadMySQL2() {
|
|
785
|
+
try {
|
|
786
|
+
return await import('mysql2/promise');
|
|
787
|
+
} catch {
|
|
788
|
+
throw new Error(
|
|
789
|
+
'MySQLStorageAdapter requires the "mysql2" package.\nInstall it: npm install mysql2'
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function rowToJob2(row) {
|
|
794
|
+
const payload = typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
|
|
795
|
+
const attempts = typeof row.attempts === "string" ? JSON.parse(row.attempts) : row.attempts ?? [];
|
|
796
|
+
const job = {
|
|
797
|
+
id: row.id,
|
|
798
|
+
queue: row.queue,
|
|
799
|
+
type: row.type,
|
|
800
|
+
payload,
|
|
801
|
+
status: row.status,
|
|
802
|
+
attemptsMade: Number(row.attempts_made),
|
|
803
|
+
maxAttempts: Number(row.max_attempts),
|
|
804
|
+
retryDelay: Number(row.retry_delay),
|
|
805
|
+
backoff: row.backoff,
|
|
806
|
+
timeout: Number(row.timeout),
|
|
807
|
+
priority: Number(row.priority),
|
|
808
|
+
runAt: row.run_at instanceof Date ? row.run_at.toISOString() : String(row.run_at),
|
|
809
|
+
attempts,
|
|
810
|
+
lockId: row.lock_id,
|
|
811
|
+
lockExpiresAt: row.lock_expires_at instanceof Date ? row.lock_expires_at.toISOString() : null,
|
|
812
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
813
|
+
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
|
|
814
|
+
completedAt: row.completed_at instanceof Date ? row.completed_at.toISOString() : null,
|
|
815
|
+
failedAt: row.failed_at instanceof Date ? row.failed_at.toISOString() : null
|
|
816
|
+
};
|
|
817
|
+
if (row.cron) job.cron = row.cron;
|
|
818
|
+
return job;
|
|
819
|
+
}
|
|
820
|
+
var CREATE_JOBS_TABLE2, CREATE_RATE_TABLE2; exports.MySQLStorageAdapter = void 0;
|
|
821
|
+
var init_mysql_adapter = __esm({
|
|
822
|
+
"src/storage/mysql.adapter.ts"() {
|
|
823
|
+
CREATE_JOBS_TABLE2 = `
|
|
824
|
+
CREATE TABLE IF NOT EXISTS qjw_jobs (
|
|
825
|
+
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
826
|
+
queue VARCHAR(255) NOT NULL,
|
|
827
|
+
type VARCHAR(255) NOT NULL,
|
|
828
|
+
payload JSON NOT NULL,
|
|
829
|
+
status VARCHAR(20) NOT NULL DEFAULT 'waiting',
|
|
830
|
+
attempts_made INT NOT NULL DEFAULT 0,
|
|
831
|
+
max_attempts INT NOT NULL DEFAULT 3,
|
|
832
|
+
retry_delay INT NOT NULL DEFAULT 1000,
|
|
833
|
+
backoff VARCHAR(20) NOT NULL DEFAULT 'exponential',
|
|
834
|
+
timeout INT NOT NULL DEFAULT 30000,
|
|
835
|
+
priority INT NOT NULL DEFAULT 0,
|
|
836
|
+
run_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
837
|
+
cron VARCHAR(255),
|
|
838
|
+
attempts JSON NOT NULL,
|
|
839
|
+
lock_id VARCHAR(255),
|
|
840
|
+
lock_expires_at DATETIME(3),
|
|
841
|
+
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
842
|
+
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
|
843
|
+
completed_at DATETIME(3),
|
|
844
|
+
failed_at DATETIME(3),
|
|
845
|
+
INDEX qjw_jobs_claim_idx (queue, status, run_at, priority)
|
|
846
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
847
|
+
`;
|
|
848
|
+
CREATE_RATE_TABLE2 = `
|
|
849
|
+
CREATE TABLE IF NOT EXISTS qjw_rate_limits (
|
|
850
|
+
queue VARCHAR(255) NOT NULL PRIMARY KEY,
|
|
851
|
+
count INT NOT NULL DEFAULT 0,
|
|
852
|
+
window_start BIGINT NOT NULL DEFAULT 0
|
|
853
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
854
|
+
`;
|
|
855
|
+
exports.MySQLStorageAdapter = class {
|
|
856
|
+
pool;
|
|
857
|
+
connectionString;
|
|
858
|
+
constructor(connectionString) {
|
|
859
|
+
this.connectionString = connectionString;
|
|
860
|
+
}
|
|
861
|
+
// -------------------------------------------------------------------------
|
|
862
|
+
// Lifecycle
|
|
863
|
+
// -------------------------------------------------------------------------
|
|
864
|
+
async initialize() {
|
|
865
|
+
const mysql2 = await loadMySQL2();
|
|
866
|
+
this.pool = mysql2.createPool({
|
|
867
|
+
uri: this.connectionString,
|
|
868
|
+
waitForConnections: true,
|
|
869
|
+
connectionLimit: 10,
|
|
870
|
+
queueLimit: 0
|
|
871
|
+
});
|
|
872
|
+
const conn = await this.pool.getConnection();
|
|
873
|
+
try {
|
|
874
|
+
await conn.query("SELECT 1");
|
|
875
|
+
await conn.query(CREATE_JOBS_TABLE2);
|
|
876
|
+
await conn.query(CREATE_RATE_TABLE2);
|
|
877
|
+
} finally {
|
|
878
|
+
conn.release();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async close() {
|
|
882
|
+
if (this.pool) {
|
|
883
|
+
await this.pool.end();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// -------------------------------------------------------------------------
|
|
887
|
+
// Enqueue
|
|
888
|
+
// -------------------------------------------------------------------------
|
|
889
|
+
async enqueue(input) {
|
|
890
|
+
const runAtMs = new Date(input.runAt).getTime();
|
|
891
|
+
const isDelayed = runAtMs > Date.now();
|
|
892
|
+
const status = isDelayed ? "delayed" : "waiting";
|
|
893
|
+
await this.pool.query(
|
|
894
|
+
`INSERT IGNORE INTO qjw_jobs
|
|
895
|
+
(id, queue, type, payload, status, attempts_made, max_attempts,
|
|
896
|
+
retry_delay, backoff, timeout, priority, run_at, cron,
|
|
897
|
+
attempts, lock_id, lock_expires_at, created_at, updated_at,
|
|
898
|
+
completed_at, failed_at)
|
|
899
|
+
VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?,?,NULL,NULL,NOW(3),NOW(3),NULL,NULL)`,
|
|
900
|
+
[
|
|
901
|
+
input.id,
|
|
902
|
+
input.queue,
|
|
903
|
+
input.type,
|
|
904
|
+
JSON.stringify(input.payload),
|
|
905
|
+
status,
|
|
906
|
+
input.maxAttempts,
|
|
907
|
+
input.retryDelay,
|
|
908
|
+
input.backoff,
|
|
909
|
+
input.timeout,
|
|
910
|
+
input.priority,
|
|
911
|
+
input.runAt,
|
|
912
|
+
input.cron ?? null,
|
|
913
|
+
JSON.stringify([])
|
|
914
|
+
]
|
|
915
|
+
);
|
|
916
|
+
const rows = await this.pool.query("SELECT * FROM qjw_jobs WHERE id = ?", [input.id]);
|
|
917
|
+
const row = rows[0][0];
|
|
918
|
+
return rowToJob2(row);
|
|
919
|
+
}
|
|
920
|
+
// -------------------------------------------------------------------------
|
|
921
|
+
// Claim (transaction + SELECT … FOR UPDATE SKIP LOCKED)
|
|
922
|
+
// -------------------------------------------------------------------------
|
|
923
|
+
async claim(input) {
|
|
924
|
+
const { queue, lockId, lockDuration, now } = input;
|
|
925
|
+
const lockExpiresAt = new Date(new Date(now).getTime() + lockDuration).toISOString().slice(0, 23).replace("T", " ");
|
|
926
|
+
const nowMysql = new Date(now).toISOString().slice(0, 23).replace("T", " ");
|
|
927
|
+
const conn = await this.pool.getConnection();
|
|
928
|
+
try {
|
|
929
|
+
await conn.beginTransaction();
|
|
930
|
+
const [rows] = await conn.query(
|
|
931
|
+
`SELECT * FROM qjw_jobs
|
|
932
|
+
WHERE queue = ?
|
|
933
|
+
AND status IN ('waiting', 'delayed')
|
|
934
|
+
AND run_at <= ?
|
|
935
|
+
AND (lock_expires_at IS NULL OR lock_expires_at <= ?)
|
|
936
|
+
ORDER BY priority DESC, run_at ASC, created_at ASC
|
|
937
|
+
LIMIT 1
|
|
938
|
+
FOR UPDATE SKIP LOCKED`,
|
|
939
|
+
[queue, nowMysql, nowMysql]
|
|
940
|
+
);
|
|
941
|
+
if (!rows || rows.length === 0) {
|
|
942
|
+
await conn.rollback();
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
const row = rows[0];
|
|
946
|
+
await conn.query(
|
|
947
|
+
`UPDATE qjw_jobs
|
|
948
|
+
SET status = 'active', lock_id = ?, lock_expires_at = ?, updated_at = NOW(3)
|
|
949
|
+
WHERE id = ?`,
|
|
950
|
+
[lockId, lockExpiresAt, row.id]
|
|
951
|
+
);
|
|
952
|
+
await conn.commit();
|
|
953
|
+
const [updated] = await this.pool.query("SELECT * FROM qjw_jobs WHERE id = ?", [
|
|
954
|
+
row.id
|
|
955
|
+
]);
|
|
956
|
+
return rowToJob2(updated[0]);
|
|
957
|
+
} catch (err) {
|
|
958
|
+
await conn.rollback();
|
|
959
|
+
throw err;
|
|
960
|
+
} finally {
|
|
961
|
+
conn.release();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// -------------------------------------------------------------------------
|
|
965
|
+
// Complete
|
|
966
|
+
// -------------------------------------------------------------------------
|
|
967
|
+
async complete(jobId) {
|
|
968
|
+
await this.pool.query(
|
|
969
|
+
`UPDATE qjw_jobs
|
|
970
|
+
SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
|
|
971
|
+
completed_at = NOW(3), updated_at = NOW(3)
|
|
972
|
+
WHERE id = ?`,
|
|
973
|
+
[jobId]
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
// -------------------------------------------------------------------------
|
|
977
|
+
// Requeue
|
|
978
|
+
// -------------------------------------------------------------------------
|
|
979
|
+
async requeue(input) {
|
|
980
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
981
|
+
const attempt = JSON.stringify({
|
|
982
|
+
attempt: input.attemptNumber,
|
|
983
|
+
startedAt: now,
|
|
984
|
+
finishedAt: now,
|
|
985
|
+
error: input.error,
|
|
986
|
+
...input.stack !== void 0 ? { stack: input.stack } : {}
|
|
987
|
+
});
|
|
988
|
+
await this.pool.query(
|
|
989
|
+
`UPDATE qjw_jobs
|
|
990
|
+
SET status = 'waiting',
|
|
991
|
+
attempts_made = attempts_made + 1,
|
|
992
|
+
attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
|
|
993
|
+
run_at = ?,
|
|
994
|
+
lock_id = NULL,
|
|
995
|
+
lock_expires_at = NULL,
|
|
996
|
+
updated_at = NOW(3)
|
|
997
|
+
WHERE id = ?`,
|
|
998
|
+
[attempt, input.runAt, input.jobId]
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
// -------------------------------------------------------------------------
|
|
1002
|
+
// Move to DLQ
|
|
1003
|
+
// -------------------------------------------------------------------------
|
|
1004
|
+
async moveToDlq(input) {
|
|
1005
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1006
|
+
const attempt = JSON.stringify({
|
|
1007
|
+
attempt: input.attemptNumber,
|
|
1008
|
+
startedAt: now,
|
|
1009
|
+
finishedAt: now,
|
|
1010
|
+
error: input.error,
|
|
1011
|
+
...input.stack !== void 0 ? { stack: input.stack } : {}
|
|
1012
|
+
});
|
|
1013
|
+
await this.pool.query(
|
|
1014
|
+
`UPDATE qjw_jobs
|
|
1015
|
+
SET status = 'dead',
|
|
1016
|
+
attempts_made = attempts_made + 1,
|
|
1017
|
+
attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
|
|
1018
|
+
lock_id = NULL,
|
|
1019
|
+
lock_expires_at = NULL,
|
|
1020
|
+
failed_at = NOW(3),
|
|
1021
|
+
updated_at = NOW(3)
|
|
1022
|
+
WHERE id = ?`,
|
|
1023
|
+
[attempt, input.jobId]
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
// -------------------------------------------------------------------------
|
|
1027
|
+
// Release lock
|
|
1028
|
+
// -------------------------------------------------------------------------
|
|
1029
|
+
async releaseLock(jobId) {
|
|
1030
|
+
await this.pool.query(
|
|
1031
|
+
`UPDATE qjw_jobs
|
|
1032
|
+
SET lock_id = NULL, lock_expires_at = NULL, updated_at = NOW(3)
|
|
1033
|
+
WHERE id = ?`,
|
|
1034
|
+
[jobId]
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
// -------------------------------------------------------------------------
|
|
1038
|
+
// Recover stalled jobs (atomic — SELECT … FOR UPDATE inside transaction)
|
|
1039
|
+
// -------------------------------------------------------------------------
|
|
1040
|
+
async recoverStalledJobs(queue, now) {
|
|
1041
|
+
const nowMysql = new Date(now).toISOString().slice(0, 23).replace("T", " ");
|
|
1042
|
+
const conn = await this.pool.getConnection();
|
|
1043
|
+
try {
|
|
1044
|
+
await conn.beginTransaction();
|
|
1045
|
+
const [rows] = await conn.query(
|
|
1046
|
+
`SELECT id FROM qjw_jobs
|
|
1047
|
+
WHERE queue = ?
|
|
1048
|
+
AND status = 'active'
|
|
1049
|
+
AND lock_expires_at <= ?
|
|
1050
|
+
FOR UPDATE SKIP LOCKED`,
|
|
1051
|
+
[queue, nowMysql]
|
|
1052
|
+
);
|
|
1053
|
+
const ids = rows.map((r) => String(r["id"]));
|
|
1054
|
+
if (ids.length > 0) {
|
|
1055
|
+
await conn.query(
|
|
1056
|
+
`UPDATE qjw_jobs
|
|
1057
|
+
SET status = 'waiting', lock_id = NULL, lock_expires_at = NULL, updated_at = NOW(3)
|
|
1058
|
+
WHERE id IN (?)`,
|
|
1059
|
+
[ids]
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
await conn.commit();
|
|
1063
|
+
return ids;
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
await conn.rollback();
|
|
1066
|
+
throw err;
|
|
1067
|
+
} finally {
|
|
1068
|
+
conn.release();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// -------------------------------------------------------------------------
|
|
1072
|
+
// Reads
|
|
1073
|
+
// -------------------------------------------------------------------------
|
|
1074
|
+
async getJob(jobId) {
|
|
1075
|
+
const [rows] = await this.pool.query("SELECT * FROM qjw_jobs WHERE id = ?", [jobId]);
|
|
1076
|
+
const list = rows;
|
|
1077
|
+
if (!list || list.length === 0) return null;
|
|
1078
|
+
return rowToJob2(list[0]);
|
|
1079
|
+
}
|
|
1080
|
+
async getJobs(filter) {
|
|
1081
|
+
const { queue, status, limit = 100, offset = 0 } = filter;
|
|
1082
|
+
const conditions = [];
|
|
1083
|
+
const params = [];
|
|
1084
|
+
if (queue !== void 0) {
|
|
1085
|
+
conditions.push("queue = ?");
|
|
1086
|
+
params.push(queue);
|
|
1087
|
+
}
|
|
1088
|
+
if (status !== void 0) {
|
|
1089
|
+
conditions.push("status = ?");
|
|
1090
|
+
params.push(status);
|
|
1091
|
+
}
|
|
1092
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1093
|
+
params.push(limit, offset);
|
|
1094
|
+
const [rows] = await this.pool.query(
|
|
1095
|
+
`SELECT * FROM qjw_jobs ${where}
|
|
1096
|
+
ORDER BY priority DESC, run_at ASC, created_at ASC
|
|
1097
|
+
LIMIT ? OFFSET ?`,
|
|
1098
|
+
params
|
|
1099
|
+
);
|
|
1100
|
+
return rows.map((r) => rowToJob2(r));
|
|
1101
|
+
}
|
|
1102
|
+
async getJobCounts(queue) {
|
|
1103
|
+
const [rows] = await this.pool.query(
|
|
1104
|
+
"SELECT status, COUNT(*) AS count FROM qjw_jobs WHERE queue = ? GROUP BY status",
|
|
1105
|
+
[queue]
|
|
1106
|
+
);
|
|
1107
|
+
const counts = {
|
|
1108
|
+
waiting: 0,
|
|
1109
|
+
active: 0,
|
|
1110
|
+
completed: 0,
|
|
1111
|
+
delayed: 0,
|
|
1112
|
+
dead: 0
|
|
1113
|
+
};
|
|
1114
|
+
for (const row of rows) {
|
|
1115
|
+
const s = String(row["status"]);
|
|
1116
|
+
if (s in counts) counts[s] = Number(row["count"]);
|
|
1117
|
+
}
|
|
1118
|
+
return counts;
|
|
1119
|
+
}
|
|
1120
|
+
// -------------------------------------------------------------------------
|
|
1121
|
+
// Rate limiting
|
|
1122
|
+
// -------------------------------------------------------------------------
|
|
1123
|
+
async checkAndIncrementRateLimit(queue, max, windowMs, now) {
|
|
1124
|
+
const nowMs = new Date(now).getTime();
|
|
1125
|
+
const [rows] = await this.pool.query(
|
|
1126
|
+
"SELECT count, window_start FROM qjw_rate_limits WHERE queue = ?",
|
|
1127
|
+
[queue]
|
|
1128
|
+
);
|
|
1129
|
+
const existing = rows[0];
|
|
1130
|
+
if (!existing || nowMs - Number(existing["window_start"]) >= windowMs) {
|
|
1131
|
+
await this.pool.query(
|
|
1132
|
+
`INSERT INTO qjw_rate_limits (queue, count, window_start) VALUES (?,1,?)
|
|
1133
|
+
ON DUPLICATE KEY UPDATE count = 1, window_start = VALUES(window_start)`,
|
|
1134
|
+
[queue, nowMs]
|
|
1135
|
+
);
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
if (Number(existing["count"]) >= max) return false;
|
|
1139
|
+
await this.pool.query("UPDATE qjw_rate_limits SET count = count + 1 WHERE queue = ?", [queue]);
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// src/core/job.ts
|
|
1147
|
+
var Job = class {
|
|
1148
|
+
/** @internal Raw data record — treat as immutable outside storage layer. */
|
|
1149
|
+
_data;
|
|
1150
|
+
constructor(data) {
|
|
1151
|
+
this._data = data;
|
|
1152
|
+
}
|
|
1153
|
+
// -------------------------------------------------------------------------
|
|
1154
|
+
// Identity
|
|
1155
|
+
// -------------------------------------------------------------------------
|
|
1156
|
+
/** Unique, stable job identifier. */
|
|
1157
|
+
get id() {
|
|
1158
|
+
return this._data.id;
|
|
1159
|
+
}
|
|
1160
|
+
/** Name of the queue this job belongs to. */
|
|
1161
|
+
get queue() {
|
|
1162
|
+
return this._data.queue;
|
|
1163
|
+
}
|
|
1164
|
+
/** Application-defined job type (matches the registered processor). */
|
|
1165
|
+
get type() {
|
|
1166
|
+
return this._data.type;
|
|
1167
|
+
}
|
|
1168
|
+
// -------------------------------------------------------------------------
|
|
1169
|
+
// Payload
|
|
1170
|
+
// -------------------------------------------------------------------------
|
|
1171
|
+
/**
|
|
1172
|
+
* User-supplied job payload.
|
|
1173
|
+
* Never log this value — it may contain sensitive data.
|
|
1174
|
+
*/
|
|
1175
|
+
get data() {
|
|
1176
|
+
return this._data.payload;
|
|
1177
|
+
}
|
|
1178
|
+
// -------------------------------------------------------------------------
|
|
1179
|
+
// Status & attempts
|
|
1180
|
+
// -------------------------------------------------------------------------
|
|
1181
|
+
/** Current lifecycle status. */
|
|
1182
|
+
get status() {
|
|
1183
|
+
return this._data.status;
|
|
1184
|
+
}
|
|
1185
|
+
/** Number of attempts already executed (0 = not yet started). */
|
|
1186
|
+
get attemptsMade() {
|
|
1187
|
+
return this._data.attemptsMade;
|
|
1188
|
+
}
|
|
1189
|
+
/** Maximum allowed attempts. */
|
|
1190
|
+
get maxAttempts() {
|
|
1191
|
+
return this._data.maxAttempts;
|
|
1192
|
+
}
|
|
1193
|
+
/** How many attempts remain (including the current one). */
|
|
1194
|
+
get attemptsRemaining() {
|
|
1195
|
+
return Math.max(0, this._data.maxAttempts - this._data.attemptsMade);
|
|
1196
|
+
}
|
|
1197
|
+
/** Ordered list of past attempt records. */
|
|
1198
|
+
get attemptHistory() {
|
|
1199
|
+
return this._data.attempts;
|
|
1200
|
+
}
|
|
1201
|
+
// -------------------------------------------------------------------------
|
|
1202
|
+
// Retry configuration
|
|
1203
|
+
// -------------------------------------------------------------------------
|
|
1204
|
+
/** Base delay in ms between retry attempts. */
|
|
1205
|
+
get retryDelay() {
|
|
1206
|
+
return this._data.retryDelay;
|
|
1207
|
+
}
|
|
1208
|
+
/** Backoff strategy applied on retry. */
|
|
1209
|
+
get backoff() {
|
|
1210
|
+
return this._data.backoff;
|
|
1211
|
+
}
|
|
1212
|
+
// -------------------------------------------------------------------------
|
|
1213
|
+
// Execution config
|
|
1214
|
+
// -------------------------------------------------------------------------
|
|
1215
|
+
/** Per-attempt timeout in ms. */
|
|
1216
|
+
get timeout() {
|
|
1217
|
+
return this._data.timeout;
|
|
1218
|
+
}
|
|
1219
|
+
/** Processing priority — higher numbers are processed first. */
|
|
1220
|
+
get priority() {
|
|
1221
|
+
return this._data.priority;
|
|
1222
|
+
}
|
|
1223
|
+
// -------------------------------------------------------------------------
|
|
1224
|
+
// Scheduling
|
|
1225
|
+
// -------------------------------------------------------------------------
|
|
1226
|
+
/** ISO timestamp when this job is eligible to run. */
|
|
1227
|
+
get runAt() {
|
|
1228
|
+
return this._data.runAt;
|
|
1229
|
+
}
|
|
1230
|
+
/** Cron expression for recurring jobs, or undefined. */
|
|
1231
|
+
get cron() {
|
|
1232
|
+
return this._data.cron;
|
|
1233
|
+
}
|
|
1234
|
+
// -------------------------------------------------------------------------
|
|
1235
|
+
// Lock
|
|
1236
|
+
// -------------------------------------------------------------------------
|
|
1237
|
+
/** ID of the worker currently holding the lock, or null. */
|
|
1238
|
+
get lockId() {
|
|
1239
|
+
return this._data.lockId;
|
|
1240
|
+
}
|
|
1241
|
+
/** ISO timestamp when the current lock expires, or null. */
|
|
1242
|
+
get lockExpiresAt() {
|
|
1243
|
+
return this._data.lockExpiresAt;
|
|
1244
|
+
}
|
|
1245
|
+
// -------------------------------------------------------------------------
|
|
1246
|
+
// Timestamps
|
|
1247
|
+
// -------------------------------------------------------------------------
|
|
1248
|
+
/** ISO timestamp when the job was first enqueued. */
|
|
1249
|
+
get createdAt() {
|
|
1250
|
+
return this._data.createdAt;
|
|
1251
|
+
}
|
|
1252
|
+
/** ISO timestamp of the last status change. */
|
|
1253
|
+
get updatedAt() {
|
|
1254
|
+
return this._data.updatedAt;
|
|
1255
|
+
}
|
|
1256
|
+
/** ISO timestamp when the job completed successfully, or null. */
|
|
1257
|
+
get completedAt() {
|
|
1258
|
+
return this._data.completedAt;
|
|
1259
|
+
}
|
|
1260
|
+
/** ISO timestamp when the job was moved to the DLQ, or null. */
|
|
1261
|
+
get failedAt() {
|
|
1262
|
+
return this._data.failedAt;
|
|
1263
|
+
}
|
|
1264
|
+
// -------------------------------------------------------------------------
|
|
1265
|
+
// Derived helpers
|
|
1266
|
+
// -------------------------------------------------------------------------
|
|
1267
|
+
/** Returns true when the job has been successfully processed. */
|
|
1268
|
+
isCompleted() {
|
|
1269
|
+
return this._data.status === "completed";
|
|
1270
|
+
}
|
|
1271
|
+
/** Returns true when the job is in the Dead Letter Queue. */
|
|
1272
|
+
isDead() {
|
|
1273
|
+
return this._data.status === "dead";
|
|
1274
|
+
}
|
|
1275
|
+
/** Returns true when the job is currently being processed by a worker. */
|
|
1276
|
+
isActive() {
|
|
1277
|
+
return this._data.status === "active";
|
|
1278
|
+
}
|
|
1279
|
+
/** Returns true when the job is waiting to be claimed. */
|
|
1280
|
+
isWaiting() {
|
|
1281
|
+
return this._data.status === "waiting";
|
|
1282
|
+
}
|
|
1283
|
+
/** Returns true when the job is scheduled for a future time. */
|
|
1284
|
+
isDelayed() {
|
|
1285
|
+
return this._data.status === "delayed";
|
|
1286
|
+
}
|
|
1287
|
+
// -------------------------------------------------------------------------
|
|
1288
|
+
// Debug representation (intentionally omits payload)
|
|
1289
|
+
// -------------------------------------------------------------------------
|
|
1290
|
+
toJSON() {
|
|
1291
|
+
return {
|
|
1292
|
+
id: this._data.id,
|
|
1293
|
+
queue: this._data.queue,
|
|
1294
|
+
type: this._data.type,
|
|
1295
|
+
status: this._data.status,
|
|
1296
|
+
attemptsMade: this._data.attemptsMade,
|
|
1297
|
+
maxAttempts: this._data.maxAttempts,
|
|
1298
|
+
priority: this._data.priority,
|
|
1299
|
+
runAt: this._data.runAt,
|
|
1300
|
+
createdAt: this._data.createdAt,
|
|
1301
|
+
updatedAt: this._data.updatedAt,
|
|
1302
|
+
completedAt: this._data.completedAt,
|
|
1303
|
+
failedAt: this._data.failedAt
|
|
1304
|
+
// Payload intentionally excluded to prevent accidental logging.
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
toString() {
|
|
1308
|
+
return `Job(${this._data.id}, type=${this._data.type}, status=${this._data.status}, attempt=${this._data.attemptsMade}/${this._data.maxAttempts})`;
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// src/core/backoff.ts
|
|
1313
|
+
var MAX_DELAY_MS = 10 * 60 * 1e3;
|
|
1314
|
+
function calculateBackoff(strategy, baseDelay, attemptNumber) {
|
|
1315
|
+
const attempt = Math.max(1, attemptNumber);
|
|
1316
|
+
switch (strategy) {
|
|
1317
|
+
case "fixed":
|
|
1318
|
+
return baseDelay;
|
|
1319
|
+
case "linear":
|
|
1320
|
+
return baseDelay * attempt;
|
|
1321
|
+
case "exponential": {
|
|
1322
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
1323
|
+
return Math.min(delay, MAX_DELAY_MS);
|
|
1324
|
+
}
|
|
1325
|
+
default: {
|
|
1326
|
+
return baseDelay;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function nextRunAt(delayMs, fromDate = /* @__PURE__ */ new Date()) {
|
|
1331
|
+
return new Date(fromDate.getTime() + delayMs).toISOString();
|
|
1332
|
+
}
|
|
1333
|
+
function generateJobId() {
|
|
1334
|
+
return crypto.randomUUID();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/core/worker.ts
|
|
1338
|
+
function resolveConfig(workerOptions, queueOptions, defaults) {
|
|
1339
|
+
return {
|
|
1340
|
+
concurrency: workerOptions.concurrency ?? queueOptions.concurrency ?? defaults.concurrency,
|
|
1341
|
+
shutdownTimeout: workerOptions.shutdownTimeout ?? 3e4,
|
|
1342
|
+
pollInterval: queueOptions.pollInterval ?? defaults.pollInterval,
|
|
1343
|
+
stalledInterval: queueOptions.stalledInterval ?? defaults.stalledInterval,
|
|
1344
|
+
lockDuration: queueOptions.lockDuration ?? defaults.lockDuration,
|
|
1345
|
+
rateLimit: queueOptions.rateLimit ?? defaults.rateLimit
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
var Worker = class {
|
|
1349
|
+
/** Unique identifier for this worker instance. */
|
|
1350
|
+
id;
|
|
1351
|
+
queueName;
|
|
1352
|
+
storage;
|
|
1353
|
+
emitter;
|
|
1354
|
+
processors;
|
|
1355
|
+
config;
|
|
1356
|
+
_status = "idle";
|
|
1357
|
+
activeCount = 0;
|
|
1358
|
+
/**
|
|
1359
|
+
* Tracks job IDs currently being processed so we can release their locks
|
|
1360
|
+
* when graceful shutdown times out before they finish.
|
|
1361
|
+
*/
|
|
1362
|
+
activeJobIds = /* @__PURE__ */ new Set();
|
|
1363
|
+
pollTimer = null;
|
|
1364
|
+
stalledTimer = null;
|
|
1365
|
+
/** Resolves when all active jobs finish during shutdown. */
|
|
1366
|
+
drainResolve = null;
|
|
1367
|
+
constructor(queueName, storage, emitter, processors, workerOptions, queueOptions, defaults) {
|
|
1368
|
+
this.id = `worker:${queueName}:${crypto.randomUUID()}`;
|
|
1369
|
+
this.queueName = queueName;
|
|
1370
|
+
this.storage = storage;
|
|
1371
|
+
this.emitter = emitter;
|
|
1372
|
+
this.processors = processors;
|
|
1373
|
+
this.config = resolveConfig(workerOptions, queueOptions, defaults);
|
|
1374
|
+
}
|
|
1375
|
+
// -------------------------------------------------------------------------
|
|
1376
|
+
// Public API
|
|
1377
|
+
// -------------------------------------------------------------------------
|
|
1378
|
+
get status() {
|
|
1379
|
+
return this._status;
|
|
1380
|
+
}
|
|
1381
|
+
/** Start polling for jobs. */
|
|
1382
|
+
start() {
|
|
1383
|
+
if (this._status !== "idle" && this._status !== "stopped") {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
this._status = "running";
|
|
1387
|
+
this.emitter.emit("worker:started", this.id);
|
|
1388
|
+
this.emitter.emit("worker:status", this.id, this._status);
|
|
1389
|
+
this.schedulePoll();
|
|
1390
|
+
this.scheduleStallCheck();
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Gracefully stop the worker.
|
|
1394
|
+
*
|
|
1395
|
+
* 1. Stop accepting new jobs.
|
|
1396
|
+
* 2. Wait up to `shutdownTimeout` ms for active jobs to finish.
|
|
1397
|
+
* 3. Release locks on any jobs that did not finish in time so another
|
|
1398
|
+
* worker can reclaim them.
|
|
1399
|
+
* 4. Emit stopped event.
|
|
1400
|
+
*/
|
|
1401
|
+
async stop() {
|
|
1402
|
+
if (this._status === "stopped" || this._status === "stopping") {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
this._status = "stopping";
|
|
1406
|
+
this.emitter.emit("worker:status", this.id, this._status);
|
|
1407
|
+
if (this.pollTimer) {
|
|
1408
|
+
clearTimeout(this.pollTimer);
|
|
1409
|
+
this.pollTimer = null;
|
|
1410
|
+
}
|
|
1411
|
+
if (this.stalledTimer) {
|
|
1412
|
+
clearTimeout(this.stalledTimer);
|
|
1413
|
+
this.stalledTimer = null;
|
|
1414
|
+
}
|
|
1415
|
+
if (this.activeCount > 0) {
|
|
1416
|
+
await Promise.race([
|
|
1417
|
+
new Promise((resolve) => {
|
|
1418
|
+
this.drainResolve = resolve;
|
|
1419
|
+
}),
|
|
1420
|
+
new Promise((resolve) => setTimeout(resolve, this.config.shutdownTimeout))
|
|
1421
|
+
]);
|
|
1422
|
+
}
|
|
1423
|
+
if (this.activeJobIds.size > 0) {
|
|
1424
|
+
await Promise.all(
|
|
1425
|
+
Array.from(this.activeJobIds).map(
|
|
1426
|
+
(jobId) => this.storage.releaseLock(jobId).catch(() => {
|
|
1427
|
+
})
|
|
1428
|
+
)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
this._status = "stopped";
|
|
1432
|
+
this.emitter.emit("worker:stopped", this.id);
|
|
1433
|
+
this.emitter.emit("worker:status", this.id, this._status);
|
|
1434
|
+
}
|
|
1435
|
+
// -------------------------------------------------------------------------
|
|
1436
|
+
// Poll loop
|
|
1437
|
+
// -------------------------------------------------------------------------
|
|
1438
|
+
schedulePoll() {
|
|
1439
|
+
if (this._status !== "running") return;
|
|
1440
|
+
this.pollTimer = setTimeout(() => {
|
|
1441
|
+
void this.poll();
|
|
1442
|
+
}, this.config.pollInterval);
|
|
1443
|
+
}
|
|
1444
|
+
async poll() {
|
|
1445
|
+
if (this._status !== "running") return;
|
|
1446
|
+
try {
|
|
1447
|
+
while (this._status === "running" && this.activeCount < this.config.concurrency) {
|
|
1448
|
+
const claimed = await this.claimNext();
|
|
1449
|
+
if (!claimed) break;
|
|
1450
|
+
}
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1453
|
+
this.emitter.emit("queue:error", this.queueName, error);
|
|
1454
|
+
this.emitter.emit("worker:error", this.id, error);
|
|
1455
|
+
}
|
|
1456
|
+
this.schedulePoll();
|
|
1457
|
+
}
|
|
1458
|
+
// -------------------------------------------------------------------------
|
|
1459
|
+
// Claim & execute
|
|
1460
|
+
// -------------------------------------------------------------------------
|
|
1461
|
+
async claimNext() {
|
|
1462
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1463
|
+
if (this.config.rateLimit) {
|
|
1464
|
+
const allowed = await this.storage.checkAndIncrementRateLimit(
|
|
1465
|
+
this.queueName,
|
|
1466
|
+
this.config.rateLimit.max,
|
|
1467
|
+
this.config.rateLimit.duration,
|
|
1468
|
+
now
|
|
1469
|
+
);
|
|
1470
|
+
if (!allowed) return false;
|
|
1471
|
+
}
|
|
1472
|
+
const raw = await this.storage.claim({
|
|
1473
|
+
queue: this.queueName,
|
|
1474
|
+
lockId: this.id,
|
|
1475
|
+
lockDuration: this.config.lockDuration,
|
|
1476
|
+
now
|
|
1477
|
+
});
|
|
1478
|
+
if (!raw) return false;
|
|
1479
|
+
const job = new Job(raw);
|
|
1480
|
+
this.activeCount += 1;
|
|
1481
|
+
this.activeJobIds.add(job.id);
|
|
1482
|
+
void this.executeJob(job);
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
async executeJob(job) {
|
|
1486
|
+
this.emitter.emit("job:started", job._data);
|
|
1487
|
+
const processor = this.processors.get(job.type);
|
|
1488
|
+
if (!processor) {
|
|
1489
|
+
const error = new Error(
|
|
1490
|
+
`No processor registered for job type "${job.type}" in queue "${this.queueName}"`
|
|
1491
|
+
);
|
|
1492
|
+
await this.handleFailure(job, error);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
let timeoutHandle = null;
|
|
1496
|
+
try {
|
|
1497
|
+
await new Promise((resolve, reject) => {
|
|
1498
|
+
timeoutHandle = setTimeout(() => {
|
|
1499
|
+
reject(new Error(`Job timed out after ${job.timeout}ms`));
|
|
1500
|
+
}, job.timeout);
|
|
1501
|
+
Promise.resolve(processor(job)).then(resolve, reject);
|
|
1502
|
+
});
|
|
1503
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1504
|
+
await this.storage.complete(job.id);
|
|
1505
|
+
const completedData = await this.storage.getJob(job.id) ?? job._data;
|
|
1506
|
+
this.emitter.emit("job:completed", completedData);
|
|
1507
|
+
if (job.cron) {
|
|
1508
|
+
await this.enqueueCronNext(job);
|
|
1509
|
+
}
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1512
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1513
|
+
await this.handleFailure(job, error);
|
|
1514
|
+
} finally {
|
|
1515
|
+
this.activeJobIds.delete(job.id);
|
|
1516
|
+
this.activeCount -= 1;
|
|
1517
|
+
if (this.activeCount === 0 && this.drainResolve) {
|
|
1518
|
+
this.drainResolve();
|
|
1519
|
+
this.drainResolve = null;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
// -------------------------------------------------------------------------
|
|
1524
|
+
// Cron — re-enqueue the next occurrence after a successful run
|
|
1525
|
+
// -------------------------------------------------------------------------
|
|
1526
|
+
async enqueueCronNext(job) {
|
|
1527
|
+
try {
|
|
1528
|
+
let nextMs;
|
|
1529
|
+
try {
|
|
1530
|
+
const specifier = "croner";
|
|
1531
|
+
const croner = await import(
|
|
1532
|
+
/* @vite-ignore */
|
|
1533
|
+
specifier
|
|
1534
|
+
);
|
|
1535
|
+
const CronClass = croner.Cron ?? croner.default?.Cron ?? croner.default;
|
|
1536
|
+
if (typeof CronClass === "function") {
|
|
1537
|
+
const cronInstance = new CronClass(job.cron);
|
|
1538
|
+
const nextDate = cronInstance.nextRun();
|
|
1539
|
+
nextMs = nextDate ? nextDate.getTime() : Date.now() + 6e4;
|
|
1540
|
+
} else {
|
|
1541
|
+
nextMs = Date.now() + 6e4;
|
|
1542
|
+
}
|
|
1543
|
+
} catch {
|
|
1544
|
+
nextMs = Date.now() + 6e4;
|
|
1545
|
+
}
|
|
1546
|
+
const runAt = new Date(nextMs).toISOString();
|
|
1547
|
+
await this.storage.enqueue({
|
|
1548
|
+
id: generateJobId(),
|
|
1549
|
+
queue: this.queueName,
|
|
1550
|
+
type: job.type,
|
|
1551
|
+
payload: job._data.payload,
|
|
1552
|
+
maxAttempts: job.maxAttempts,
|
|
1553
|
+
retryDelay: job.retryDelay,
|
|
1554
|
+
backoff: job.backoff,
|
|
1555
|
+
timeout: job.timeout,
|
|
1556
|
+
priority: job.priority,
|
|
1557
|
+
runAt,
|
|
1558
|
+
cron: job.cron
|
|
1559
|
+
});
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1562
|
+
this.emitter.emit("worker:error", this.id, error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
// -------------------------------------------------------------------------
|
|
1566
|
+
// Failure handling — requeue or DLQ
|
|
1567
|
+
// -------------------------------------------------------------------------
|
|
1568
|
+
async handleFailure(job, error) {
|
|
1569
|
+
const attemptNumber = job.attemptsMade + 1;
|
|
1570
|
+
const hasMore = attemptNumber < job.maxAttempts;
|
|
1571
|
+
this.emitter.emit("job:failed", job._data, error);
|
|
1572
|
+
if (hasMore) {
|
|
1573
|
+
const delayMs = calculateBackoff(job.backoff, job.retryDelay, attemptNumber);
|
|
1574
|
+
const runAt = nextRunAt(delayMs);
|
|
1575
|
+
await this.storage.requeue({
|
|
1576
|
+
jobId: job.id,
|
|
1577
|
+
runAt,
|
|
1578
|
+
error: error.message,
|
|
1579
|
+
attemptNumber,
|
|
1580
|
+
...error.stack !== void 0 && { stack: error.stack }
|
|
1581
|
+
});
|
|
1582
|
+
const updated = await this.storage.getJob(job.id);
|
|
1583
|
+
if (updated) {
|
|
1584
|
+
this.emitter.emit("job:retrying", updated, error, runAt);
|
|
1585
|
+
}
|
|
1586
|
+
} else {
|
|
1587
|
+
await this.storage.moveToDlq({
|
|
1588
|
+
jobId: job.id,
|
|
1589
|
+
error: error.message,
|
|
1590
|
+
attemptNumber,
|
|
1591
|
+
...error.stack !== void 0 && { stack: error.stack }
|
|
1592
|
+
});
|
|
1593
|
+
const dead = await this.storage.getJob(job.id);
|
|
1594
|
+
if (dead) {
|
|
1595
|
+
this.emitter.emit("job:dead", dead, error);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
// -------------------------------------------------------------------------
|
|
1600
|
+
// Stalled-job recovery
|
|
1601
|
+
// -------------------------------------------------------------------------
|
|
1602
|
+
scheduleStallCheck() {
|
|
1603
|
+
if (this._status !== "running") return;
|
|
1604
|
+
this.stalledTimer = setTimeout(() => {
|
|
1605
|
+
void this.recoverStalledJobs();
|
|
1606
|
+
}, this.config.stalledInterval);
|
|
1607
|
+
}
|
|
1608
|
+
async recoverStalledJobs() {
|
|
1609
|
+
if (this._status !== "running") return;
|
|
1610
|
+
try {
|
|
1611
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1612
|
+
const recovered = await this.storage.recoverStalledJobs(this.queueName, now);
|
|
1613
|
+
for (const jobId of recovered) {
|
|
1614
|
+
this.emitter.emit("job:stalled", jobId);
|
|
1615
|
+
this.emitter.emit("job:recovered", jobId);
|
|
1616
|
+
}
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1619
|
+
this.emitter.emit("worker:error", this.id, error);
|
|
1620
|
+
}
|
|
1621
|
+
this.scheduleStallCheck();
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
// src/core/queue.ts
|
|
1626
|
+
function mergeQueueConfig(options, defaults) {
|
|
1627
|
+
return {
|
|
1628
|
+
concurrency: options?.concurrency ?? defaults.concurrency,
|
|
1629
|
+
attempts: options?.attempts ?? defaults.attempts,
|
|
1630
|
+
retryDelay: options?.retryDelay ?? defaults.retryDelay,
|
|
1631
|
+
backoff: options?.backoff ?? defaults.backoff,
|
|
1632
|
+
timeout: options?.timeout ?? defaults.timeout,
|
|
1633
|
+
rateLimit: options?.rateLimit ?? defaults.rateLimit ?? void 0,
|
|
1634
|
+
pollInterval: options?.pollInterval ?? defaults.pollInterval,
|
|
1635
|
+
stalledInterval: options?.stalledInterval ?? defaults.stalledInterval,
|
|
1636
|
+
lockDuration: options?.lockDuration ?? defaults.lockDuration
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
var Queue = class {
|
|
1640
|
+
/** The name of this queue — unique within a QueueClient. */
|
|
1641
|
+
name;
|
|
1642
|
+
storage;
|
|
1643
|
+
emitter;
|
|
1644
|
+
resolvedConfig;
|
|
1645
|
+
clientDefaults;
|
|
1646
|
+
/** Registered processors keyed by job type. */
|
|
1647
|
+
processors = /* @__PURE__ */ new Map();
|
|
1648
|
+
/** Active worker instances created by this queue. */
|
|
1649
|
+
workers = [];
|
|
1650
|
+
constructor(name, storage, emitter, options, defaults) {
|
|
1651
|
+
this.name = name;
|
|
1652
|
+
this.storage = storage;
|
|
1653
|
+
this.emitter = emitter;
|
|
1654
|
+
this.clientDefaults = defaults;
|
|
1655
|
+
this.resolvedConfig = mergeQueueConfig(options, defaults);
|
|
1656
|
+
}
|
|
1657
|
+
// -------------------------------------------------------------------------
|
|
1658
|
+
// Enqueue
|
|
1659
|
+
// -------------------------------------------------------------------------
|
|
1660
|
+
/**
|
|
1661
|
+
* Add a new job to the queue.
|
|
1662
|
+
*
|
|
1663
|
+
* @param type - Job type string, must match a registered processor.
|
|
1664
|
+
* @param payload - Arbitrary serialisable payload (never logged by default).
|
|
1665
|
+
* @param options - Per-job overrides (attempts, delay, priority, etc.).
|
|
1666
|
+
*/
|
|
1667
|
+
async enqueue(type, payload, options) {
|
|
1668
|
+
const cfg = this.resolvedConfig;
|
|
1669
|
+
const now = /* @__PURE__ */ new Date();
|
|
1670
|
+
let runAt;
|
|
1671
|
+
if (options?.schedule?.runAt !== void 0) {
|
|
1672
|
+
runAt = new Date(options.schedule.runAt);
|
|
1673
|
+
} else if (options?.schedule?.delay !== void 0) {
|
|
1674
|
+
runAt = new Date(now.getTime() + options.schedule.delay);
|
|
1675
|
+
} else {
|
|
1676
|
+
runAt = now;
|
|
1677
|
+
}
|
|
1678
|
+
const raw = await this.storage.enqueue({
|
|
1679
|
+
id: generateJobId(),
|
|
1680
|
+
queue: this.name,
|
|
1681
|
+
type,
|
|
1682
|
+
payload,
|
|
1683
|
+
maxAttempts: options?.attempts ?? cfg.attempts,
|
|
1684
|
+
retryDelay: options?.retryDelay ?? cfg.retryDelay,
|
|
1685
|
+
backoff: options?.backoff ?? cfg.backoff,
|
|
1686
|
+
timeout: options?.timeout ?? cfg.timeout,
|
|
1687
|
+
priority: options?.priority ?? 0,
|
|
1688
|
+
runAt: runAt.toISOString(),
|
|
1689
|
+
...options?.schedule?.cron !== void 0 && { cron: options.schedule.cron }
|
|
1690
|
+
});
|
|
1691
|
+
const job = new Job(raw);
|
|
1692
|
+
this.emitter.emit("job:enqueued", raw);
|
|
1693
|
+
return job;
|
|
1694
|
+
}
|
|
1695
|
+
// -------------------------------------------------------------------------
|
|
1696
|
+
// Processor registration
|
|
1697
|
+
// -------------------------------------------------------------------------
|
|
1698
|
+
/**
|
|
1699
|
+
* Register a processor function for a given job type.
|
|
1700
|
+
*
|
|
1701
|
+
* Only one processor per type per queue is supported.
|
|
1702
|
+
* Registering a second processor for the same type replaces the first.
|
|
1703
|
+
*/
|
|
1704
|
+
process(type, processor) {
|
|
1705
|
+
this.processors.set(type, processor);
|
|
1706
|
+
}
|
|
1707
|
+
// -------------------------------------------------------------------------
|
|
1708
|
+
// Worker creation
|
|
1709
|
+
// -------------------------------------------------------------------------
|
|
1710
|
+
/**
|
|
1711
|
+
* Create and start a Worker that consumes jobs from this queue.
|
|
1712
|
+
*
|
|
1713
|
+
* Worker-level `concurrency` overrides queue-level concurrency.
|
|
1714
|
+
*/
|
|
1715
|
+
createWorker(options) {
|
|
1716
|
+
const worker = new Worker(
|
|
1717
|
+
this.name,
|
|
1718
|
+
this.storage,
|
|
1719
|
+
this.emitter,
|
|
1720
|
+
this.processors,
|
|
1721
|
+
options ?? {},
|
|
1722
|
+
this.resolvedConfig,
|
|
1723
|
+
this.clientDefaults
|
|
1724
|
+
);
|
|
1725
|
+
this.workers.push(worker);
|
|
1726
|
+
worker.start();
|
|
1727
|
+
return worker;
|
|
1728
|
+
}
|
|
1729
|
+
// -------------------------------------------------------------------------
|
|
1730
|
+
// Reads
|
|
1731
|
+
// -------------------------------------------------------------------------
|
|
1732
|
+
/** Fetch a single job by its ID. Returns null if not found or wrong queue. */
|
|
1733
|
+
async getJob(jobId) {
|
|
1734
|
+
const raw = await this.storage.getJob(jobId);
|
|
1735
|
+
if (!raw || raw.queue !== this.name) return null;
|
|
1736
|
+
return new Job(raw);
|
|
1737
|
+
}
|
|
1738
|
+
/** Fetch jobs from this queue, optionally filtered by status. */
|
|
1739
|
+
async getJobs(status, limit = 100, offset = 0) {
|
|
1740
|
+
const raws = await this.storage.getJobs({
|
|
1741
|
+
queue: this.name,
|
|
1742
|
+
...status !== void 0 && { status },
|
|
1743
|
+
limit,
|
|
1744
|
+
offset
|
|
1745
|
+
});
|
|
1746
|
+
return raws.map((r) => new Job(r));
|
|
1747
|
+
}
|
|
1748
|
+
/** Get job counts by status for this queue. */
|
|
1749
|
+
async getJobCounts() {
|
|
1750
|
+
return this.storage.getJobCounts(this.name);
|
|
1751
|
+
}
|
|
1752
|
+
// -------------------------------------------------------------------------
|
|
1753
|
+
// Lifecycle
|
|
1754
|
+
// -------------------------------------------------------------------------
|
|
1755
|
+
/**
|
|
1756
|
+
* Gracefully stop all workers attached to this queue.
|
|
1757
|
+
* Called automatically by QueueClient.close().
|
|
1758
|
+
*/
|
|
1759
|
+
async close() {
|
|
1760
|
+
await Promise.all(this.workers.map((w) => w.stop()));
|
|
1761
|
+
}
|
|
1762
|
+
/** Return all active worker instances on this queue. */
|
|
1763
|
+
getWorkers() {
|
|
1764
|
+
return this.workers;
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
// src/storage/in-memory.adapter.ts
|
|
1769
|
+
var InMemoryStorageAdapter = class {
|
|
1770
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1771
|
+
rateLimitBuckets = /* @__PURE__ */ new Map();
|
|
1772
|
+
// -------------------------------------------------------------------------
|
|
1773
|
+
// Lifecycle
|
|
1774
|
+
// -------------------------------------------------------------------------
|
|
1775
|
+
async initialize() {
|
|
1776
|
+
}
|
|
1777
|
+
async close() {
|
|
1778
|
+
this.jobs.clear();
|
|
1779
|
+
this.rateLimitBuckets.clear();
|
|
1780
|
+
}
|
|
1781
|
+
// -------------------------------------------------------------------------
|
|
1782
|
+
// Enqueue
|
|
1783
|
+
// -------------------------------------------------------------------------
|
|
1784
|
+
async enqueue(input) {
|
|
1785
|
+
if (this.jobs.has(input.id)) {
|
|
1786
|
+
return this.jobs.get(input.id);
|
|
1787
|
+
}
|
|
1788
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1789
|
+
const runAt = input.runAt;
|
|
1790
|
+
const isDelayed = new Date(runAt).getTime() > Date.now();
|
|
1791
|
+
const job = {
|
|
1792
|
+
id: input.id,
|
|
1793
|
+
queue: input.queue,
|
|
1794
|
+
type: input.type,
|
|
1795
|
+
payload: input.payload,
|
|
1796
|
+
status: isDelayed ? "delayed" : "waiting",
|
|
1797
|
+
attemptsMade: 0,
|
|
1798
|
+
maxAttempts: input.maxAttempts,
|
|
1799
|
+
retryDelay: input.retryDelay,
|
|
1800
|
+
backoff: input.backoff,
|
|
1801
|
+
timeout: input.timeout,
|
|
1802
|
+
priority: input.priority,
|
|
1803
|
+
runAt,
|
|
1804
|
+
...input.cron !== void 0 && { cron: input.cron },
|
|
1805
|
+
attempts: [],
|
|
1806
|
+
lockId: null,
|
|
1807
|
+
lockExpiresAt: null,
|
|
1808
|
+
createdAt: now,
|
|
1809
|
+
updatedAt: now,
|
|
1810
|
+
completedAt: null,
|
|
1811
|
+
failedAt: null
|
|
1812
|
+
};
|
|
1813
|
+
this.jobs.set(job.id, job);
|
|
1814
|
+
return job;
|
|
1815
|
+
}
|
|
1816
|
+
// -------------------------------------------------------------------------
|
|
1817
|
+
// Claim (atomic within a single event-loop tick)
|
|
1818
|
+
// -------------------------------------------------------------------------
|
|
1819
|
+
async claim(input) {
|
|
1820
|
+
const { queue, lockId, lockDuration, now } = input;
|
|
1821
|
+
const nowMs = new Date(now).getTime();
|
|
1822
|
+
const eligible = [];
|
|
1823
|
+
for (const job2 of this.jobs.values()) {
|
|
1824
|
+
if (job2.queue !== queue) continue;
|
|
1825
|
+
if (job2.status !== "waiting" && job2.status !== "delayed") continue;
|
|
1826
|
+
if (new Date(job2.runAt).getTime() > nowMs) continue;
|
|
1827
|
+
if (job2.lockId !== null && job2.lockExpiresAt !== null) {
|
|
1828
|
+
const lockExpiry = new Date(job2.lockExpiresAt).getTime();
|
|
1829
|
+
if (lockExpiry > nowMs) continue;
|
|
1830
|
+
}
|
|
1831
|
+
eligible.push(job2);
|
|
1832
|
+
}
|
|
1833
|
+
if (eligible.length === 0) return null;
|
|
1834
|
+
eligible.sort((a, b) => {
|
|
1835
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
1836
|
+
const runAtDiff = new Date(a.runAt).getTime() - new Date(b.runAt).getTime();
|
|
1837
|
+
if (runAtDiff !== 0) return runAtDiff;
|
|
1838
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
1839
|
+
});
|
|
1840
|
+
const job = eligible[0];
|
|
1841
|
+
if (!job) return null;
|
|
1842
|
+
const lockExpiresAt = new Date(nowMs + lockDuration).toISOString();
|
|
1843
|
+
job.status = "active";
|
|
1844
|
+
job.lockId = lockId;
|
|
1845
|
+
job.lockExpiresAt = lockExpiresAt;
|
|
1846
|
+
job.updatedAt = now;
|
|
1847
|
+
return job;
|
|
1848
|
+
}
|
|
1849
|
+
// -------------------------------------------------------------------------
|
|
1850
|
+
// Complete
|
|
1851
|
+
// -------------------------------------------------------------------------
|
|
1852
|
+
async complete(jobId) {
|
|
1853
|
+
const job = this.requireJob(jobId);
|
|
1854
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1855
|
+
job.status = "completed";
|
|
1856
|
+
job.lockId = null;
|
|
1857
|
+
job.lockExpiresAt = null;
|
|
1858
|
+
job.completedAt = now;
|
|
1859
|
+
job.updatedAt = now;
|
|
1860
|
+
}
|
|
1861
|
+
// -------------------------------------------------------------------------
|
|
1862
|
+
// Requeue (retry)
|
|
1863
|
+
// -------------------------------------------------------------------------
|
|
1864
|
+
async requeue(input) {
|
|
1865
|
+
const job = this.requireJob(input.jobId);
|
|
1866
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1867
|
+
job.attempts.push({
|
|
1868
|
+
attempt: input.attemptNumber,
|
|
1869
|
+
startedAt: job.updatedAt,
|
|
1870
|
+
finishedAt: now,
|
|
1871
|
+
error: input.error,
|
|
1872
|
+
...input.stack !== void 0 && { stack: input.stack }
|
|
1873
|
+
});
|
|
1874
|
+
job.attemptsMade = input.attemptNumber;
|
|
1875
|
+
job.status = "waiting";
|
|
1876
|
+
job.runAt = input.runAt;
|
|
1877
|
+
job.lockId = null;
|
|
1878
|
+
job.lockExpiresAt = null;
|
|
1879
|
+
job.updatedAt = now;
|
|
1880
|
+
}
|
|
1881
|
+
// -------------------------------------------------------------------------
|
|
1882
|
+
// Move to DLQ
|
|
1883
|
+
// -------------------------------------------------------------------------
|
|
1884
|
+
async moveToDlq(input) {
|
|
1885
|
+
const job = this.requireJob(input.jobId);
|
|
1886
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1887
|
+
job.attempts.push({
|
|
1888
|
+
attempt: input.attemptNumber,
|
|
1889
|
+
startedAt: job.updatedAt,
|
|
1890
|
+
finishedAt: now,
|
|
1891
|
+
error: input.error,
|
|
1892
|
+
...input.stack !== void 0 && { stack: input.stack }
|
|
1893
|
+
});
|
|
1894
|
+
job.attemptsMade = input.attemptNumber;
|
|
1895
|
+
job.status = "dead";
|
|
1896
|
+
job.lockId = null;
|
|
1897
|
+
job.lockExpiresAt = null;
|
|
1898
|
+
job.failedAt = now;
|
|
1899
|
+
job.updatedAt = now;
|
|
1900
|
+
}
|
|
1901
|
+
// -------------------------------------------------------------------------
|
|
1902
|
+
// Release lock
|
|
1903
|
+
// -------------------------------------------------------------------------
|
|
1904
|
+
async releaseLock(jobId) {
|
|
1905
|
+
const job = this.jobs.get(jobId);
|
|
1906
|
+
if (!job) return;
|
|
1907
|
+
job.lockId = null;
|
|
1908
|
+
job.lockExpiresAt = null;
|
|
1909
|
+
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1910
|
+
}
|
|
1911
|
+
// -------------------------------------------------------------------------
|
|
1912
|
+
// Recover stalled jobs
|
|
1913
|
+
// -------------------------------------------------------------------------
|
|
1914
|
+
async recoverStalledJobs(queue, now) {
|
|
1915
|
+
const nowMs = new Date(now).getTime();
|
|
1916
|
+
const recovered = [];
|
|
1917
|
+
for (const job of this.jobs.values()) {
|
|
1918
|
+
if (job.queue !== queue) continue;
|
|
1919
|
+
if (job.status !== "active") continue;
|
|
1920
|
+
if (job.lockExpiresAt === null) continue;
|
|
1921
|
+
const lockExpiry = new Date(job.lockExpiresAt).getTime();
|
|
1922
|
+
if (lockExpiry <= nowMs) {
|
|
1923
|
+
job.status = "waiting";
|
|
1924
|
+
job.lockId = null;
|
|
1925
|
+
job.lockExpiresAt = null;
|
|
1926
|
+
job.updatedAt = now;
|
|
1927
|
+
recovered.push(job.id);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return recovered;
|
|
1931
|
+
}
|
|
1932
|
+
// -------------------------------------------------------------------------
|
|
1933
|
+
// Reads
|
|
1934
|
+
// -------------------------------------------------------------------------
|
|
1935
|
+
async getJob(jobId) {
|
|
1936
|
+
return this.jobs.get(jobId) ?? null;
|
|
1937
|
+
}
|
|
1938
|
+
async getJobs(filter) {
|
|
1939
|
+
const { queue, status, limit = 100, offset = 0 } = filter;
|
|
1940
|
+
const results = [];
|
|
1941
|
+
for (const job of this.jobs.values()) {
|
|
1942
|
+
if (queue !== void 0 && job.queue !== queue) continue;
|
|
1943
|
+
if (status !== void 0 && job.status !== status) continue;
|
|
1944
|
+
results.push(job);
|
|
1945
|
+
}
|
|
1946
|
+
results.sort((a, b) => {
|
|
1947
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
1948
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
1949
|
+
});
|
|
1950
|
+
return results.slice(offset, offset + limit);
|
|
1951
|
+
}
|
|
1952
|
+
async getJobCounts(queue) {
|
|
1953
|
+
const counts = {
|
|
1954
|
+
waiting: 0,
|
|
1955
|
+
active: 0,
|
|
1956
|
+
completed: 0,
|
|
1957
|
+
delayed: 0,
|
|
1958
|
+
dead: 0
|
|
1959
|
+
};
|
|
1960
|
+
for (const job of this.jobs.values()) {
|
|
1961
|
+
if (job.queue !== queue) continue;
|
|
1962
|
+
counts[job.status] += 1;
|
|
1963
|
+
}
|
|
1964
|
+
return counts;
|
|
1965
|
+
}
|
|
1966
|
+
// -------------------------------------------------------------------------
|
|
1967
|
+
// Rate limiting
|
|
1968
|
+
// -------------------------------------------------------------------------
|
|
1969
|
+
async checkAndIncrementRateLimit(queue, max, windowMs, now) {
|
|
1970
|
+
const nowMs = new Date(now).getTime();
|
|
1971
|
+
const existing = this.rateLimitBuckets.get(queue);
|
|
1972
|
+
if (!existing || nowMs - existing.windowStart >= windowMs) {
|
|
1973
|
+
this.rateLimitBuckets.set(queue, { count: 1, windowStart: nowMs });
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
if (existing.count >= max) {
|
|
1977
|
+
return false;
|
|
1978
|
+
}
|
|
1979
|
+
existing.count += 1;
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
// -------------------------------------------------------------------------
|
|
1983
|
+
// Internal helpers
|
|
1984
|
+
// -------------------------------------------------------------------------
|
|
1985
|
+
requireJob(jobId) {
|
|
1986
|
+
const job = this.jobs.get(jobId);
|
|
1987
|
+
if (!job) {
|
|
1988
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
1989
|
+
}
|
|
1990
|
+
return job;
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
var QueueEventEmitter = class {
|
|
1994
|
+
emitter;
|
|
1995
|
+
constructor() {
|
|
1996
|
+
this.emitter = new events.EventEmitter();
|
|
1997
|
+
this.emitter.setMaxListeners(100);
|
|
1998
|
+
}
|
|
1999
|
+
// -------------------------------------------------------------------------
|
|
2000
|
+
// Subscribe
|
|
2001
|
+
// -------------------------------------------------------------------------
|
|
2002
|
+
on(event, listener) {
|
|
2003
|
+
this.emitter.on(event, listener);
|
|
2004
|
+
return this;
|
|
2005
|
+
}
|
|
2006
|
+
once(event, listener) {
|
|
2007
|
+
this.emitter.once(event, listener);
|
|
2008
|
+
return this;
|
|
2009
|
+
}
|
|
2010
|
+
off(event, listener) {
|
|
2011
|
+
this.emitter.off(event, listener);
|
|
2012
|
+
return this;
|
|
2013
|
+
}
|
|
2014
|
+
// ---------------------------------------------------------------------------
|
|
2015
|
+
// Publish
|
|
2016
|
+
// ---------------------------------------------------------------------------
|
|
2017
|
+
emit(event, ...args) {
|
|
2018
|
+
return this.emitter.emit(event, ...args);
|
|
2019
|
+
}
|
|
2020
|
+
// ---------------------------------------------------------------------------
|
|
2021
|
+
// Utility
|
|
2022
|
+
// ---------------------------------------------------------------------------
|
|
2023
|
+
removeAllListeners(event) {
|
|
2024
|
+
if (event) {
|
|
2025
|
+
this.emitter.removeAllListeners(event);
|
|
2026
|
+
} else {
|
|
2027
|
+
this.emitter.removeAllListeners();
|
|
2028
|
+
}
|
|
2029
|
+
return this;
|
|
2030
|
+
}
|
|
2031
|
+
listenerCount(event) {
|
|
2032
|
+
return this.emitter.listenerCount(event);
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
// src/core/client.ts
|
|
2037
|
+
var HARD_DEFAULTS = {
|
|
2038
|
+
attempts: 3,
|
|
2039
|
+
retryDelay: 1e3,
|
|
2040
|
+
backoff: "exponential",
|
|
2041
|
+
timeout: 3e4,
|
|
2042
|
+
concurrency: 10,
|
|
2043
|
+
rateLimit: void 0,
|
|
2044
|
+
pollInterval: 1e3,
|
|
2045
|
+
stalledInterval: 3e4,
|
|
2046
|
+
lockDuration: 6e4
|
|
2047
|
+
};
|
|
2048
|
+
var QueueClient = class _QueueClient {
|
|
2049
|
+
/** @internal — mutable so withAdapter() can replace it */
|
|
2050
|
+
_storage;
|
|
2051
|
+
emitter;
|
|
2052
|
+
defaults;
|
|
2053
|
+
dialect;
|
|
2054
|
+
connectionString;
|
|
2055
|
+
queues = /* @__PURE__ */ new Map();
|
|
2056
|
+
initialised = false;
|
|
2057
|
+
closed = false;
|
|
2058
|
+
constructor(options = {}) {
|
|
2059
|
+
this.defaults = { ...HARD_DEFAULTS, ...options.defaults };
|
|
2060
|
+
this.dialect = options.dialect ?? "memory";
|
|
2061
|
+
this.connectionString = options.connectionString;
|
|
2062
|
+
this._storage = this.buildMemoryOrEagerAdapter();
|
|
2063
|
+
this.emitter = new QueueEventEmitter();
|
|
2064
|
+
}
|
|
2065
|
+
// -------------------------------------------------------------------------
|
|
2066
|
+
// Eager adapter construction (memory only; external adapters built in init)
|
|
2067
|
+
// -------------------------------------------------------------------------
|
|
2068
|
+
buildMemoryOrEagerAdapter() {
|
|
2069
|
+
if (this.dialect === "memory") {
|
|
2070
|
+
return new InMemoryStorageAdapter();
|
|
2071
|
+
}
|
|
2072
|
+
return new InMemoryStorageAdapter();
|
|
2073
|
+
}
|
|
2074
|
+
// -------------------------------------------------------------------------
|
|
2075
|
+
// Initialisation
|
|
2076
|
+
// -------------------------------------------------------------------------
|
|
2077
|
+
/**
|
|
2078
|
+
* Initialise the client and storage adapter.
|
|
2079
|
+
*
|
|
2080
|
+
* **Must be called** before creating queues when using Redis, PostgreSQL,
|
|
2081
|
+
* or MySQL.
|
|
2082
|
+
*
|
|
2083
|
+
* What each adapter does:
|
|
2084
|
+
* - **memory** — no-op (always safe to call)
|
|
2085
|
+
* - **redis** — connects, sends PING, verifies PONG
|
|
2086
|
+
* - **postgres** — connects, runs `SELECT 1`, auto-creates `qjw_` tables
|
|
2087
|
+
* - **mysql** — connects, runs `SELECT 1`, auto-creates `qjw_` tables
|
|
2088
|
+
*
|
|
2089
|
+
* Idempotent — safe to call multiple times.
|
|
2090
|
+
*/
|
|
2091
|
+
async init() {
|
|
2092
|
+
if (this.initialised) return;
|
|
2093
|
+
this._storage = await this.resolveAdapter();
|
|
2094
|
+
await this._storage.initialize();
|
|
2095
|
+
this.initialised = true;
|
|
2096
|
+
}
|
|
2097
|
+
/** @deprecated Use `init()`. */
|
|
2098
|
+
async initialize() {
|
|
2099
|
+
return this.init();
|
|
2100
|
+
}
|
|
2101
|
+
async resolveAdapter() {
|
|
2102
|
+
const cs = this.connectionString;
|
|
2103
|
+
switch (this.dialect) {
|
|
2104
|
+
case "memory":
|
|
2105
|
+
return new InMemoryStorageAdapter();
|
|
2106
|
+
case "redis": {
|
|
2107
|
+
if (!cs) throw new Error('dialect "redis" requires a connectionString.');
|
|
2108
|
+
const { RedisStorageAdapter: RedisStorageAdapter2 } = await Promise.resolve().then(() => (init_redis_adapter(), redis_adapter_exports));
|
|
2109
|
+
return new RedisStorageAdapter2(cs);
|
|
2110
|
+
}
|
|
2111
|
+
case "postgres": {
|
|
2112
|
+
if (!cs) throw new Error('dialect "postgres" requires a connectionString.');
|
|
2113
|
+
const { PostgreSQLStorageAdapter: PostgreSQLStorageAdapter2 } = await Promise.resolve().then(() => (init_postgres_adapter(), postgres_adapter_exports));
|
|
2114
|
+
return new PostgreSQLStorageAdapter2(cs);
|
|
2115
|
+
}
|
|
2116
|
+
case "mysql": {
|
|
2117
|
+
if (!cs) throw new Error('dialect "mysql" requires a connectionString.');
|
|
2118
|
+
const { MySQLStorageAdapter: MySQLStorageAdapter2 } = await Promise.resolve().then(() => (init_mysql_adapter(), mysql_adapter_exports));
|
|
2119
|
+
return new MySQLStorageAdapter2(cs);
|
|
2120
|
+
}
|
|
2121
|
+
default:
|
|
2122
|
+
throw new Error(`Unknown storage dialect: "${this.dialect}"`);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
// -------------------------------------------------------------------------
|
|
2126
|
+
// Queue management
|
|
2127
|
+
// -------------------------------------------------------------------------
|
|
2128
|
+
/**
|
|
2129
|
+
* Create a named queue with optional per-queue configuration.
|
|
2130
|
+
*
|
|
2131
|
+
* For external dialects (redis/postgres/mysql), call `await client.init()`
|
|
2132
|
+
* first.
|
|
2133
|
+
*/
|
|
2134
|
+
createQueue(name, options) {
|
|
2135
|
+
if (this.closed) throw new Error("QueueClient has been closed.");
|
|
2136
|
+
if (this.queues.has(name)) {
|
|
2137
|
+
throw new Error(`A queue named "${name}" already exists on this client.`);
|
|
2138
|
+
}
|
|
2139
|
+
if (!this.initialised && this.dialect === "memory") {
|
|
2140
|
+
void this.init();
|
|
2141
|
+
}
|
|
2142
|
+
const queue = new Queue(name, this._storage, this.emitter, options, this.defaults);
|
|
2143
|
+
this.queues.set(name, queue);
|
|
2144
|
+
return queue;
|
|
2145
|
+
}
|
|
2146
|
+
/** Returns the queue with the given name, or `undefined`. */
|
|
2147
|
+
getQueue(name) {
|
|
2148
|
+
return this.queues.get(name);
|
|
2149
|
+
}
|
|
2150
|
+
/** Returns the queue with the given name, or throws. */
|
|
2151
|
+
requireQueue(name) {
|
|
2152
|
+
const queue = this.getQueue(name);
|
|
2153
|
+
if (!queue) {
|
|
2154
|
+
throw new Error(`Queue "${name}" not found. Did you call createQueue("${name}")?`);
|
|
2155
|
+
}
|
|
2156
|
+
return queue;
|
|
2157
|
+
}
|
|
2158
|
+
/** Names of all queues registered on this client. */
|
|
2159
|
+
get queueNames() {
|
|
2160
|
+
return Array.from(this.queues.keys());
|
|
2161
|
+
}
|
|
2162
|
+
/** `true` after `init()` has completed successfully. */
|
|
2163
|
+
get isInitialised() {
|
|
2164
|
+
return this.initialised;
|
|
2165
|
+
}
|
|
2166
|
+
// -------------------------------------------------------------------------
|
|
2167
|
+
// Events
|
|
2168
|
+
// -------------------------------------------------------------------------
|
|
2169
|
+
on(event, listener) {
|
|
2170
|
+
this.emitter.on(event, listener);
|
|
2171
|
+
return this;
|
|
2172
|
+
}
|
|
2173
|
+
once(event, listener) {
|
|
2174
|
+
this.emitter.once(event, listener);
|
|
2175
|
+
return this;
|
|
2176
|
+
}
|
|
2177
|
+
off(event, listener) {
|
|
2178
|
+
this.emitter.off(event, listener);
|
|
2179
|
+
return this;
|
|
2180
|
+
}
|
|
2181
|
+
// -------------------------------------------------------------------------
|
|
2182
|
+
// Graceful shutdown
|
|
2183
|
+
// -------------------------------------------------------------------------
|
|
2184
|
+
/**
|
|
2185
|
+
* Stop all workers, close all queues, and release the storage connection.
|
|
2186
|
+
* Safe to call multiple times.
|
|
2187
|
+
*/
|
|
2188
|
+
async close() {
|
|
2189
|
+
if (this.closed) return;
|
|
2190
|
+
this.closed = true;
|
|
2191
|
+
await Promise.all(Array.from(this.queues.values()).map((q) => q.close()));
|
|
2192
|
+
await this._storage.close();
|
|
2193
|
+
this.emitter.removeAllListeners();
|
|
2194
|
+
}
|
|
2195
|
+
// -------------------------------------------------------------------------
|
|
2196
|
+
// Advanced: custom StorageAdapter
|
|
2197
|
+
// -------------------------------------------------------------------------
|
|
2198
|
+
/**
|
|
2199
|
+
* Create a QueueClient with a fully custom StorageAdapter.
|
|
2200
|
+
*
|
|
2201
|
+
* `init()` will call `adapter.initialize()`.
|
|
2202
|
+
*
|
|
2203
|
+
* @example
|
|
2204
|
+
* const client = QueueClient.withAdapter(myAdapter);
|
|
2205
|
+
* await client.init();
|
|
2206
|
+
*/
|
|
2207
|
+
static withAdapter(adapter, options = {}) {
|
|
2208
|
+
const client = new _QueueClient(options);
|
|
2209
|
+
client._storage = adapter;
|
|
2210
|
+
return client;
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
// src/index.ts
|
|
2215
|
+
init_redis_adapter();
|
|
2216
|
+
init_postgres_adapter();
|
|
2217
|
+
init_mysql_adapter();
|
|
2218
|
+
|
|
2219
|
+
exports.InMemoryStorageAdapter = InMemoryStorageAdapter;
|
|
2220
|
+
exports.Job = Job;
|
|
2221
|
+
exports.Queue = Queue;
|
|
2222
|
+
exports.QueueClient = QueueClient;
|
|
2223
|
+
exports.QueueEventEmitter = QueueEventEmitter;
|
|
2224
|
+
exports.Worker = Worker;
|
|
2225
|
+
exports.calculateBackoff = calculateBackoff;
|
|
2226
|
+
exports.generateJobId = generateJobId;
|
|
2227
|
+
exports.nextRunAt = nextRunAt;
|
|
2228
|
+
//# sourceMappingURL=index.cjs.map
|
|
2229
|
+
//# sourceMappingURL=index.cjs.map
|