@igniter-js/jobs 0.1.13 → 0.1.14
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/AGENTS.md +117 -243
- package/README.md +311 -153
- package/dist/{adapter-CXZxomI9.d.mts → adapter-Cax_40HW.d.mts} +27 -6
- package/dist/{adapter-CXZxomI9.d.ts → adapter-Cax_40HW.d.ts} +27 -6
- package/dist/adapters/bun.d.mts +93 -0
- package/dist/adapters/bun.d.ts +93 -0
- package/dist/adapters/bun.js +914 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/bun.mjs +912 -0
- package/dist/adapters/bun.mjs.map +1 -0
- package/dist/adapters/{memory.adapter.d.ts → mock.d.mts} +1 -3
- package/dist/adapters/{memory.adapter.d.mts → mock.d.ts} +1 -3
- package/dist/adapters/{memory.adapter.js → mock.js} +58 -25
- package/dist/adapters/mock.js.map +1 -0
- package/dist/adapters/{memory.adapter.mjs → mock.mjs} +58 -25
- package/dist/adapters/mock.mjs.map +1 -0
- package/dist/adapters/{bullmq.adapter.d.ts → node.d.mts} +1 -3
- package/dist/adapters/{bullmq.adapter.d.mts → node.d.ts} +1 -3
- package/dist/adapters/{bullmq.adapter.js → node.js} +117 -40
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/{bullmq.adapter.mjs → node.mjs} +117 -40
- package/dist/adapters/node.mjs.map +1 -0
- package/dist/index.d.mts +17 -21
- package/dist/index.d.ts +17 -21
- package/dist/index.js +20 -1854
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +21 -1852
- package/dist/index.mjs.map +1 -1
- package/package.json +28 -40
- package/CHANGELOG.md +0 -13
- package/dist/adapters/bullmq.adapter.js.map +0 -1
- package/dist/adapters/bullmq.adapter.mjs.map +0 -1
- package/dist/adapters/index.d.mts +0 -143
- package/dist/adapters/index.d.ts +0 -143
- package/dist/adapters/index.js +0 -1891
- package/dist/adapters/index.js.map +0 -1
- package/dist/adapters/index.mjs +0 -1887
- package/dist/adapters/index.mjs.map +0 -1
- package/dist/adapters/memory.adapter.js.map +0 -1
- package/dist/adapters/memory.adapter.mjs.map +0 -1
package/dist/adapters/index.mjs
DELETED
|
@@ -1,1887 +0,0 @@
|
|
|
1
|
-
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq';
|
|
2
|
-
import { IgniterError } from '@igniter-js/common';
|
|
3
|
-
|
|
4
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
-
}) : x)(function(x) {
|
|
7
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// src/utils/prefix.ts
|
|
12
|
-
var _IgniterJobsPrefix = class _IgniterJobsPrefix {
|
|
13
|
-
/**
|
|
14
|
-
* Builds a normalized queue name using the global prefix and queue id.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const name = IgniterJobsPrefix.buildQueueName('email')
|
|
19
|
-
* // -> igniter:jobs:email
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
static buildQueueName(queue) {
|
|
23
|
-
return `${_IgniterJobsPrefix.BASE_PREFIX}:${queue}`;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Builds the event channel used for pub/sub.
|
|
27
|
-
*
|
|
28
|
-
* Unscoped events are published to a global channel per service/environment.
|
|
29
|
-
* Scoped events are also published to an additional channel for that scope.
|
|
30
|
-
*/
|
|
31
|
-
static buildEventsChannel(params) {
|
|
32
|
-
const base = `${_IgniterJobsPrefix.BASE_PREFIX}:events:${params.environment}:${params.service}`;
|
|
33
|
-
if (!params.scope) return base;
|
|
34
|
-
return `${base}:scope:${params.scope.type}:${params.scope.id}`;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
_IgniterJobsPrefix.BASE_PREFIX = "igniter:jobs";
|
|
38
|
-
var IgniterJobsPrefix = _IgniterJobsPrefix;
|
|
39
|
-
var IgniterJobsError = class extends IgniterError {
|
|
40
|
-
constructor(options) {
|
|
41
|
-
super(options);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// src/adapters/bullmq.adapter.ts
|
|
46
|
-
function toDateArray(values) {
|
|
47
|
-
if (!values) return void 0;
|
|
48
|
-
return values.map((v) => v instanceof Date ? v : new Date(v));
|
|
49
|
-
}
|
|
50
|
-
var IgniterJobsBullMQAdapter = class _IgniterJobsBullMQAdapter {
|
|
51
|
-
constructor(options) {
|
|
52
|
-
this.subscribers = /* @__PURE__ */ new Map();
|
|
53
|
-
this.coreAdapter = null;
|
|
54
|
-
this.coreExecutor = null;
|
|
55
|
-
this.executorDirty = true;
|
|
56
|
-
this.jobsByQueue = /* @__PURE__ */ new Map();
|
|
57
|
-
this.cronsByQueue = /* @__PURE__ */ new Map();
|
|
58
|
-
this.redis = options.redis;
|
|
59
|
-
this.publisher = this.redis;
|
|
60
|
-
this.subscriber = this.redis.duplicate();
|
|
61
|
-
this.client = { redis: this.redis };
|
|
62
|
-
this.subscriber.on("message", (channel, message) => {
|
|
63
|
-
const set = this.subscribers.get(channel);
|
|
64
|
-
if (!set || set.size === 0) return;
|
|
65
|
-
let payload = message;
|
|
66
|
-
try {
|
|
67
|
-
payload = JSON.parse(message);
|
|
68
|
-
} catch {
|
|
69
|
-
}
|
|
70
|
-
for (const handler of set) handler(payload);
|
|
71
|
-
});
|
|
72
|
-
this.queues = {
|
|
73
|
-
list: async () => this.listQueues(),
|
|
74
|
-
get: async (name) => this.getQueueInfo(name),
|
|
75
|
-
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
76
|
-
getJobs: async (name, filter) => {
|
|
77
|
-
const full = this.toCoreQueueName(name);
|
|
78
|
-
return this.core().queues.getJobs(full, filter);
|
|
79
|
-
},
|
|
80
|
-
pause: async (name) => this.pauseQueue(name),
|
|
81
|
-
resume: async (name) => this.resumeQueue(name),
|
|
82
|
-
isPaused: async (name) => {
|
|
83
|
-
const full = this.toCoreQueueName(name);
|
|
84
|
-
return this.core().queues.isPaused(full);
|
|
85
|
-
},
|
|
86
|
-
drain: async (name) => this.drainQueue(name),
|
|
87
|
-
clean: async (name, options2) => this.cleanQueue(name, options2),
|
|
88
|
-
obliterate: async (name, options2) => this.obliterateQueue(name, options2)
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
static create(options) {
|
|
92
|
-
return new _IgniterJobsBullMQAdapter(options);
|
|
93
|
-
}
|
|
94
|
-
registerJob(queueName, jobName, definition) {
|
|
95
|
-
const map = this.jobsByQueue.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
96
|
-
if (map.has(jobName)) {
|
|
97
|
-
throw new IgniterJobsError({
|
|
98
|
-
code: "JOBS_DUPLICATE_JOB",
|
|
99
|
-
message: `Job "${jobName}" is already registered in queue "${queueName}".`
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
map.set(jobName, definition);
|
|
103
|
-
this.jobsByQueue.set(queueName, map);
|
|
104
|
-
this.executorDirty = true;
|
|
105
|
-
}
|
|
106
|
-
registerCron(queueName, cronName, definition) {
|
|
107
|
-
const map = this.cronsByQueue.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
108
|
-
if (map.has(cronName)) {
|
|
109
|
-
throw new IgniterJobsError({
|
|
110
|
-
code: "JOBS_INVALID_CRON",
|
|
111
|
-
message: `Cron "${cronName}" is already registered in queue "${queueName}".`
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
map.set(cronName, definition);
|
|
115
|
-
this.cronsByQueue.set(queueName, map);
|
|
116
|
-
this.executorDirty = true;
|
|
117
|
-
}
|
|
118
|
-
async dispatch(params) {
|
|
119
|
-
const executor = await this.executor();
|
|
120
|
-
const namespace = executor[params.queue];
|
|
121
|
-
if (!namespace) {
|
|
122
|
-
throw new IgniterJobsError({
|
|
123
|
-
code: "JOBS_QUEUE_NOT_FOUND",
|
|
124
|
-
message: `Queue "${params.queue}" is not registered in the adapter.`
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return namespace.enqueue({
|
|
128
|
-
task: params.jobName,
|
|
129
|
-
input: params.input,
|
|
130
|
-
jobId: params.jobId,
|
|
131
|
-
delay: params.delay,
|
|
132
|
-
priority: params.priority,
|
|
133
|
-
attempts: params.attempts,
|
|
134
|
-
metadata: params.metadata,
|
|
135
|
-
removeOnComplete: params.removeOnComplete,
|
|
136
|
-
removeOnFail: params.removeOnFail,
|
|
137
|
-
limiter: params.limiter
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
async schedule(params) {
|
|
141
|
-
const executor = await this.executor();
|
|
142
|
-
const namespace = executor[params.queue];
|
|
143
|
-
if (!namespace) {
|
|
144
|
-
throw new IgniterJobsError({
|
|
145
|
-
code: "JOBS_QUEUE_NOT_FOUND",
|
|
146
|
-
message: `Queue "${params.queue}" is not registered in the adapter.`
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const schedule = {
|
|
150
|
-
jobId: params.jobId,
|
|
151
|
-
delay: params.delay,
|
|
152
|
-
priority: params.priority,
|
|
153
|
-
attempts: params.attempts,
|
|
154
|
-
metadata: params.metadata,
|
|
155
|
-
removeOnComplete: params.removeOnComplete,
|
|
156
|
-
removeOnFail: params.removeOnFail,
|
|
157
|
-
limiter: params.limiter,
|
|
158
|
-
at: params.at,
|
|
159
|
-
repeat: params.cron || params.every || params.maxExecutions || params.skipWeekends || params.onlyBusinessHours || params.businessHours || params.onlyWeekdays || params.skipDates ? {
|
|
160
|
-
cron: params.cron,
|
|
161
|
-
every: params.every,
|
|
162
|
-
times: params.maxExecutions,
|
|
163
|
-
skipWeekends: params.skipWeekends,
|
|
164
|
-
onlyBusinessHours: params.onlyBusinessHours,
|
|
165
|
-
businessHours: params.businessHours,
|
|
166
|
-
onlyWeekdays: params.onlyWeekdays,
|
|
167
|
-
skipDates: toDateArray(params.skipDates)
|
|
168
|
-
} : void 0
|
|
169
|
-
};
|
|
170
|
-
return namespace.schedule({
|
|
171
|
-
task: params.jobName,
|
|
172
|
-
input: params.input,
|
|
173
|
-
...schedule
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
async getJob(jobId, queue) {
|
|
177
|
-
const result = await this.core().job.get(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
178
|
-
return result ? this.mapJob(result, queue) : null;
|
|
179
|
-
}
|
|
180
|
-
async getJobState(jobId, queue) {
|
|
181
|
-
const state = await this.core().job.getState(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
182
|
-
return state;
|
|
183
|
-
}
|
|
184
|
-
async getJobLogs(jobId, queue) {
|
|
185
|
-
const logs = await this.core().job.getLogs(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
186
|
-
return logs;
|
|
187
|
-
}
|
|
188
|
-
async getJobProgress(jobId, queue) {
|
|
189
|
-
return this.core().job.getProgress(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
190
|
-
}
|
|
191
|
-
async retryJob(jobId, queue) {
|
|
192
|
-
await this.core().job.retry(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
193
|
-
}
|
|
194
|
-
async removeJob(jobId, queue) {
|
|
195
|
-
await this.core().job.remove(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
196
|
-
}
|
|
197
|
-
async promoteJob(jobId, queue) {
|
|
198
|
-
await this.core().job.promote(jobId, queue ? this.toCoreQueueName(queue) : void 0);
|
|
199
|
-
}
|
|
200
|
-
async moveJobToFailed(jobId, reason, queue) {
|
|
201
|
-
await this.core().job.moveToFailed(jobId, reason, queue ? this.toCoreQueueName(queue) : void 0);
|
|
202
|
-
}
|
|
203
|
-
async retryManyJobs(jobIds, queue) {
|
|
204
|
-
await this.core().job.retryMany(jobIds, queue ? this.toCoreQueueName(queue) : void 0);
|
|
205
|
-
}
|
|
206
|
-
async removeManyJobs(jobIds, queue) {
|
|
207
|
-
await this.core().job.removeMany(jobIds, queue ? this.toCoreQueueName(queue) : void 0);
|
|
208
|
-
}
|
|
209
|
-
async getQueueInfo(queue) {
|
|
210
|
-
const info = await this.core().queues.get(this.toCoreQueueName(queue));
|
|
211
|
-
if (!info) return null;
|
|
212
|
-
return this.mapQueueInfo(info);
|
|
213
|
-
}
|
|
214
|
-
async getQueueJobCounts(queue) {
|
|
215
|
-
const counts = await this.core().queues.getJobCounts(this.toCoreQueueName(queue));
|
|
216
|
-
return counts;
|
|
217
|
-
}
|
|
218
|
-
async listQueues() {
|
|
219
|
-
const list = await this.core().queues.list();
|
|
220
|
-
return list.map((q) => this.mapQueueInfo(q));
|
|
221
|
-
}
|
|
222
|
-
async pauseQueue(queue) {
|
|
223
|
-
await this.core().queues.pause(this.toCoreQueueName(queue));
|
|
224
|
-
}
|
|
225
|
-
async resumeQueue(queue) {
|
|
226
|
-
await this.core().queues.resume(this.toCoreQueueName(queue));
|
|
227
|
-
}
|
|
228
|
-
async drainQueue(queue) {
|
|
229
|
-
return this.core().queues.drain(this.toCoreQueueName(queue));
|
|
230
|
-
}
|
|
231
|
-
async cleanQueue(queue, options) {
|
|
232
|
-
return this.core().queues.clean(this.toCoreQueueName(queue), options);
|
|
233
|
-
}
|
|
234
|
-
async obliterateQueue(queue, options) {
|
|
235
|
-
await this.core().queues.obliterate(this.toCoreQueueName(queue), options);
|
|
236
|
-
}
|
|
237
|
-
async retryAllInQueue(queue) {
|
|
238
|
-
const jobs = await this.core().queues.getJobs(this.toCoreQueueName(queue), { status: ["failed"], limit: 1e3 });
|
|
239
|
-
await Promise.all(jobs.map((j) => this.core().job.retry(j.id, this.toCoreQueueName(queue))));
|
|
240
|
-
return jobs.length;
|
|
241
|
-
}
|
|
242
|
-
async pauseJobType(queue, jobName) {
|
|
243
|
-
throw new IgniterJobsError({
|
|
244
|
-
code: "JOBS_QUEUE_OPERATION_FAILED",
|
|
245
|
-
message: "BullMQ backend does not support pausing a single job type; pause the queue or adjust worker filters."
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
async resumeJobType(queue, jobName) {
|
|
249
|
-
throw new IgniterJobsError({
|
|
250
|
-
code: "JOBS_QUEUE_OPERATION_FAILED",
|
|
251
|
-
message: "BullMQ backend does not support resuming a single job type; resume the queue or adjust worker filters."
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
async searchJobs(filter) {
|
|
255
|
-
const queue = filter?.queue;
|
|
256
|
-
const status = filter?.status;
|
|
257
|
-
const limit = filter?.limit ?? 100;
|
|
258
|
-
const offset = filter?.offset ?? 0;
|
|
259
|
-
if (queue) {
|
|
260
|
-
const jobs = await this.core().queues.getJobs(this.toCoreQueueName(queue), { status, limit, offset });
|
|
261
|
-
return jobs.map((j) => this.mapJob(j, queue));
|
|
262
|
-
}
|
|
263
|
-
const queues = await this.listQueues();
|
|
264
|
-
const results = [];
|
|
265
|
-
for (const q of queues) {
|
|
266
|
-
const jobs = await this.core().queues.getJobs(this.toCoreQueueName(q.name), { status, limit, offset });
|
|
267
|
-
results.push(...jobs.map((j) => this.mapJob(j, q.name)));
|
|
268
|
-
if (results.length >= limit) break;
|
|
269
|
-
}
|
|
270
|
-
return results.slice(0, limit);
|
|
271
|
-
}
|
|
272
|
-
async searchQueues(filter) {
|
|
273
|
-
const all = await this.listQueues();
|
|
274
|
-
const name = filter?.name;
|
|
275
|
-
const isPaused = filter?.isPaused;
|
|
276
|
-
return all.filter((q) => name ? q.name.includes(name) : true).filter((q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true);
|
|
277
|
-
}
|
|
278
|
-
async searchWorkers(filter) {
|
|
279
|
-
const queue = filter?.queue;
|
|
280
|
-
const isRunning = filter?.isRunning;
|
|
281
|
-
const all = Array.from(this.core().getWorkers().values());
|
|
282
|
-
return all.filter((w) => {
|
|
283
|
-
if (!queue) return true;
|
|
284
|
-
const coreQueue = this.toCoreQueueName(queue);
|
|
285
|
-
const queues = w.config?.queues ?? [w.queueName];
|
|
286
|
-
return Array.isArray(queues) ? queues.includes(coreQueue) : false;
|
|
287
|
-
}).filter((w) => typeof isRunning === "boolean" ? isRunning ? w.isRunning() : !w.isRunning() : true).map((w) => this.mapWorker(w));
|
|
288
|
-
}
|
|
289
|
-
async createWorker(config) {
|
|
290
|
-
await this.executor();
|
|
291
|
-
const queuesSource = config.queues?.length ? config.queues : Array.from(/* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.cronsByQueue.keys()]));
|
|
292
|
-
const queues = queuesSource.map((q) => this.toCoreQueueName(q));
|
|
293
|
-
const coreConfig = {
|
|
294
|
-
queues,
|
|
295
|
-
concurrency: config.concurrency ?? 1,
|
|
296
|
-
limiter: config.limiter,
|
|
297
|
-
onActive: config.handlers?.onActive,
|
|
298
|
-
onSuccess: config.handlers?.onSuccess,
|
|
299
|
-
onFailure: config.handlers?.onFailure,
|
|
300
|
-
onIdle: config.handlers?.onIdle
|
|
301
|
-
};
|
|
302
|
-
const handle = await this.core().worker(coreConfig);
|
|
303
|
-
return this.mapWorker(handle);
|
|
304
|
-
}
|
|
305
|
-
getWorkers() {
|
|
306
|
-
const out = /* @__PURE__ */ new Map();
|
|
307
|
-
for (const [id, handle] of this.core().getWorkers()) out.set(id, this.mapWorker(handle));
|
|
308
|
-
return out;
|
|
309
|
-
}
|
|
310
|
-
async publishEvent(channel, payload) {
|
|
311
|
-
await this.publisher.publish(channel, JSON.stringify(payload));
|
|
312
|
-
}
|
|
313
|
-
async subscribeEvent(channel, handler) {
|
|
314
|
-
const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
315
|
-
const wrapped = (payload) => void handler(payload);
|
|
316
|
-
set.add(wrapped);
|
|
317
|
-
this.subscribers.set(channel, set);
|
|
318
|
-
if (set.size === 1) {
|
|
319
|
-
await this.subscriber.subscribe(channel);
|
|
320
|
-
}
|
|
321
|
-
return async () => {
|
|
322
|
-
const current = this.subscribers.get(channel);
|
|
323
|
-
if (!current) return;
|
|
324
|
-
current.delete(wrapped);
|
|
325
|
-
if (current.size === 0) {
|
|
326
|
-
this.subscribers.delete(channel);
|
|
327
|
-
await this.subscriber.unsubscribe(channel);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
async shutdown() {
|
|
332
|
-
await this.subscriber.quit();
|
|
333
|
-
}
|
|
334
|
-
core() {
|
|
335
|
-
if (!this.coreAdapter) {
|
|
336
|
-
this.coreAdapter = createBullMQAdapter({
|
|
337
|
-
store: { client: this.redis }
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
return this.coreAdapter;
|
|
341
|
-
}
|
|
342
|
-
async executor() {
|
|
343
|
-
if (!this.executorDirty && this.coreExecutor) return this.coreExecutor;
|
|
344
|
-
const routers = {};
|
|
345
|
-
const flattened = {};
|
|
346
|
-
const allQueues = /* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.cronsByQueue.keys()]);
|
|
347
|
-
for (const queueName of allQueues) {
|
|
348
|
-
const coreJobs = {};
|
|
349
|
-
const jobs = this.jobsByQueue.get(queueName);
|
|
350
|
-
if (jobs) {
|
|
351
|
-
for (const [jobName, def] of jobs.entries()) {
|
|
352
|
-
const queue = def.queue ? `${queueName}.${def.queue}` : queueName;
|
|
353
|
-
const fullQueue = IgniterJobsPrefix.buildQueueName(queue);
|
|
354
|
-
coreJobs[jobName] = this.toCoreJobDefinition(queueName, jobName, def, fullQueue);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const crons = this.cronsByQueue.get(queueName);
|
|
358
|
-
if (crons) {
|
|
359
|
-
for (const [cronName, def] of crons.entries()) {
|
|
360
|
-
const fullQueue = IgniterJobsPrefix.buildQueueName(queueName);
|
|
361
|
-
coreJobs[cronName] = this.toCoreCronJobDefinition(queueName, cronName, def, fullQueue);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (Object.keys(coreJobs).length === 0) continue;
|
|
365
|
-
routers[queueName] = this.core().router({
|
|
366
|
-
jobs: coreJobs,
|
|
367
|
-
namespace: queueName
|
|
368
|
-
});
|
|
369
|
-
for (const [jobName, def] of Object.entries(coreJobs)) {
|
|
370
|
-
flattened[`${queueName}.${jobName}`] = def;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
await this.core().bulkRegister(flattened);
|
|
374
|
-
this.coreExecutor = this.core().merge(routers);
|
|
375
|
-
this.executorDirty = false;
|
|
376
|
-
return this.coreExecutor;
|
|
377
|
-
}
|
|
378
|
-
toCoreQueueName(queueName) {
|
|
379
|
-
return IgniterJobsPrefix.buildQueueName(queueName);
|
|
380
|
-
}
|
|
381
|
-
mapQueueInfo(info) {
|
|
382
|
-
return {
|
|
383
|
-
name: this.fromCoreQueueName(info.name),
|
|
384
|
-
isPaused: info.isPaused,
|
|
385
|
-
jobCounts: info.jobCounts
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
fromCoreQueueName(full) {
|
|
389
|
-
const prefix = `${IgniterJobsPrefix.BASE_PREFIX}:`;
|
|
390
|
-
return full.startsWith(prefix) ? full.slice(prefix.length) : full;
|
|
391
|
-
}
|
|
392
|
-
mapJob(job, queue) {
|
|
393
|
-
const q = queue ?? this.fromCoreQueueName(job.metadata?.queue ?? job.queueName ?? "");
|
|
394
|
-
const scope = job.metadata?.__igniter_jobs_scope;
|
|
395
|
-
return {
|
|
396
|
-
id: job.id,
|
|
397
|
-
name: job.name,
|
|
398
|
-
queue: q,
|
|
399
|
-
status: job.status,
|
|
400
|
-
input: job.payload,
|
|
401
|
-
result: job.result,
|
|
402
|
-
error: job.error,
|
|
403
|
-
progress: 0,
|
|
404
|
-
attemptsMade: job.attemptsMade ?? 0,
|
|
405
|
-
priority: job.priority ?? 0,
|
|
406
|
-
createdAt: job.createdAt,
|
|
407
|
-
startedAt: job.processedAt,
|
|
408
|
-
completedAt: job.completedAt,
|
|
409
|
-
metadata: job.metadata,
|
|
410
|
-
scope
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
mapWorker(handle) {
|
|
414
|
-
const queues = handle.config?.queues ?? [handle.queueName];
|
|
415
|
-
return {
|
|
416
|
-
id: handle.id,
|
|
417
|
-
queues: queues.map((q) => this.fromCoreQueueName(q)),
|
|
418
|
-
pause: () => handle.pause(),
|
|
419
|
-
resume: () => handle.resume(),
|
|
420
|
-
close: () => handle.close(),
|
|
421
|
-
isRunning: () => handle.isRunning(),
|
|
422
|
-
isPaused: () => handle.isPaused(),
|
|
423
|
-
isClosed: () => handle.isClosed(),
|
|
424
|
-
getMetrics: async () => handle.getMetrics()
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
toCoreJobDefinition(queueName, jobName, def, fullQueueName) {
|
|
428
|
-
const handler = async (ctx) => {
|
|
429
|
-
return def.handler({
|
|
430
|
-
input: ctx.input,
|
|
431
|
-
context: ctx.context,
|
|
432
|
-
job: {
|
|
433
|
-
id: ctx.job.id,
|
|
434
|
-
name: jobName,
|
|
435
|
-
queue: queueName,
|
|
436
|
-
attemptsMade: ctx.job.attemptsMade,
|
|
437
|
-
createdAt: ctx.job.createdAt,
|
|
438
|
-
metadata: ctx.job.metadata
|
|
439
|
-
},
|
|
440
|
-
scope: ctx.job.metadata?.__igniter_jobs_scope
|
|
441
|
-
});
|
|
442
|
-
};
|
|
443
|
-
return {
|
|
444
|
-
name: jobName,
|
|
445
|
-
input: def.input,
|
|
446
|
-
handler,
|
|
447
|
-
queue: { name: fullQueueName },
|
|
448
|
-
attempts: def.attempts,
|
|
449
|
-
priority: def.priority,
|
|
450
|
-
delay: def.delay,
|
|
451
|
-
removeOnComplete: def.removeOnComplete,
|
|
452
|
-
removeOnFail: def.removeOnFail,
|
|
453
|
-
metadata: def.metadata,
|
|
454
|
-
limiter: def.limiter,
|
|
455
|
-
onStart: def.onStart,
|
|
456
|
-
onSuccess: def.onSuccess,
|
|
457
|
-
onFailure: def.onFailure,
|
|
458
|
-
onProgress: def.onProgress
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
toCoreCronJobDefinition(queueName, cronName, def, fullQueueName) {
|
|
462
|
-
const handler = async (ctx) => {
|
|
463
|
-
return def.handler({
|
|
464
|
-
context: ctx.context,
|
|
465
|
-
job: {
|
|
466
|
-
id: ctx.job.id,
|
|
467
|
-
name: cronName,
|
|
468
|
-
queue: queueName,
|
|
469
|
-
attemptsMade: ctx.job.attemptsMade,
|
|
470
|
-
createdAt: ctx.job.createdAt,
|
|
471
|
-
metadata: ctx.job.metadata
|
|
472
|
-
},
|
|
473
|
-
scope: ctx.job.metadata?.__igniter_jobs_scope
|
|
474
|
-
});
|
|
475
|
-
};
|
|
476
|
-
return {
|
|
477
|
-
name: cronName,
|
|
478
|
-
handler,
|
|
479
|
-
queue: { name: fullQueueName },
|
|
480
|
-
repeat: {
|
|
481
|
-
cron: def.cron,
|
|
482
|
-
tz: def.tz,
|
|
483
|
-
limit: def.maxExecutions,
|
|
484
|
-
startDate: def.startDate,
|
|
485
|
-
endDate: def.endDate
|
|
486
|
-
},
|
|
487
|
-
metadata: def.onlyBusinessHours || def.skipWeekends || def.businessHours || def.onlyWeekdays || def.skipDates || def.startDate && def.endDate ? {
|
|
488
|
-
advancedScheduling: {
|
|
489
|
-
onlyBusinessHours: def.onlyBusinessHours,
|
|
490
|
-
skipWeekends: def.skipWeekends,
|
|
491
|
-
businessHours: def.businessHours,
|
|
492
|
-
skipDates: toDateArray(def.skipDates),
|
|
493
|
-
onlyWeekdays: def.onlyWeekdays,
|
|
494
|
-
between: def.startDate && def.endDate ? [def.startDate, def.endDate] : void 0
|
|
495
|
-
}
|
|
496
|
-
} : void 0
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// src/utils/id-generator.ts
|
|
502
|
-
var IgniterJobsIdGenerator = class {
|
|
503
|
-
/**
|
|
504
|
-
* Generates a unique identifier with a prefix.
|
|
505
|
-
*
|
|
506
|
-
* @example
|
|
507
|
-
* ```typescript
|
|
508
|
-
* const jobId = IgniterJobsIdGenerator.generate('job')
|
|
509
|
-
* ```
|
|
510
|
-
*/
|
|
511
|
-
static generate(prefix) {
|
|
512
|
-
const now = Date.now().toString(36);
|
|
513
|
-
const random = Math.random().toString(36).slice(2, 8);
|
|
514
|
-
return `${prefix}_${now}_${random}`;
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
// src/adapters/memory.adapter.ts
|
|
519
|
-
var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
520
|
-
constructor() {
|
|
521
|
-
this.client = {
|
|
522
|
-
type: "memory"
|
|
523
|
-
};
|
|
524
|
-
this.jobsById = /* @__PURE__ */ new Map();
|
|
525
|
-
this.jobsByQueue = /* @__PURE__ */ new Map();
|
|
526
|
-
this.registeredJobs = /* @__PURE__ */ new Map();
|
|
527
|
-
this.registeredCrons = /* @__PURE__ */ new Map();
|
|
528
|
-
this.workers = /* @__PURE__ */ new Map();
|
|
529
|
-
this.subscribers = /* @__PURE__ */ new Map();
|
|
530
|
-
this.queues = {
|
|
531
|
-
list: async () => this.listQueues(),
|
|
532
|
-
get: async (name) => this.getQueueInfo(name),
|
|
533
|
-
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
534
|
-
getJobs: async (name, filter) => {
|
|
535
|
-
const statuses = filter?.status;
|
|
536
|
-
const limit = filter?.limit ?? 100;
|
|
537
|
-
const offset = filter?.offset ?? 0;
|
|
538
|
-
const results = await this.searchJobs({
|
|
539
|
-
queue: name,
|
|
540
|
-
status: statuses,
|
|
541
|
-
limit,
|
|
542
|
-
offset
|
|
543
|
-
});
|
|
544
|
-
return results;
|
|
545
|
-
},
|
|
546
|
-
pause: async (name) => this.pauseQueue(name),
|
|
547
|
-
resume: async (name) => this.resumeQueue(name),
|
|
548
|
-
isPaused: async (name) => {
|
|
549
|
-
const info = await this.getQueueInfo(name);
|
|
550
|
-
return info?.isPaused ?? false;
|
|
551
|
-
},
|
|
552
|
-
drain: async (name) => this.drainQueue(name),
|
|
553
|
-
clean: async (name, options) => this.cleanQueue(name, options),
|
|
554
|
-
obliterate: async (name, options) => this.obliterateQueue(name, options)
|
|
555
|
-
};
|
|
556
|
-
this.pausedQueues = /* @__PURE__ */ new Set();
|
|
557
|
-
}
|
|
558
|
-
static create() {
|
|
559
|
-
return new _IgniterJobsMemoryAdapter();
|
|
560
|
-
}
|
|
561
|
-
registerJob(queueName, jobName, definition) {
|
|
562
|
-
const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
563
|
-
if (queueJobs.has(jobName)) {
|
|
564
|
-
throw new IgniterJobsError({
|
|
565
|
-
code: "JOBS_DUPLICATE_JOB",
|
|
566
|
-
message: `Job "${jobName}" already registered for queue "${queueName}".`
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
queueJobs.set(jobName, definition);
|
|
570
|
-
this.registeredJobs.set(queueName, queueJobs);
|
|
571
|
-
}
|
|
572
|
-
registerCron(queueName, cronName, definition) {
|
|
573
|
-
const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
574
|
-
if (queueCrons.has(cronName)) {
|
|
575
|
-
throw new IgniterJobsError({
|
|
576
|
-
code: "JOBS_INVALID_CRON",
|
|
577
|
-
message: `Cron "${cronName}" already registered for queue "${queueName}".`
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
queueCrons.set(cronName, definition);
|
|
581
|
-
this.registeredCrons.set(queueName, queueCrons);
|
|
582
|
-
}
|
|
583
|
-
async dispatch(params) {
|
|
584
|
-
const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
|
|
585
|
-
const maxAttempts = params.attempts ?? 1;
|
|
586
|
-
const metadata = params.metadata ?? {};
|
|
587
|
-
const job = {
|
|
588
|
-
id: jobId,
|
|
589
|
-
name: params.jobName,
|
|
590
|
-
queue: params.queue,
|
|
591
|
-
input: params.input,
|
|
592
|
-
status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
|
|
593
|
-
progress: 0,
|
|
594
|
-
attemptsMade: 0,
|
|
595
|
-
maxAttempts,
|
|
596
|
-
priority: params.priority ?? 0,
|
|
597
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
598
|
-
metadata,
|
|
599
|
-
scope: params.scope,
|
|
600
|
-
logs: []
|
|
601
|
-
};
|
|
602
|
-
this.jobsById.set(jobId, job);
|
|
603
|
-
const queueList = this.jobsByQueue.get(params.queue) ?? [];
|
|
604
|
-
queueList.push(jobId);
|
|
605
|
-
this.jobsByQueue.set(params.queue, queueList);
|
|
606
|
-
if (params.delay && params.delay > 0) {
|
|
607
|
-
setTimeout(() => {
|
|
608
|
-
const stored = this.jobsById.get(jobId);
|
|
609
|
-
if (!stored) return;
|
|
610
|
-
if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
|
|
611
|
-
void this.kickWorkers(params.queue);
|
|
612
|
-
}, params.delay);
|
|
613
|
-
return jobId;
|
|
614
|
-
}
|
|
615
|
-
void this.kickWorkers(params.queue);
|
|
616
|
-
return jobId;
|
|
617
|
-
}
|
|
618
|
-
async schedule(params) {
|
|
619
|
-
if (params.at) {
|
|
620
|
-
const delay = params.at.getTime() - Date.now();
|
|
621
|
-
if (delay <= 0) {
|
|
622
|
-
throw new IgniterJobsError({
|
|
623
|
-
code: "JOBS_INVALID_SCHEDULE",
|
|
624
|
-
message: "Scheduled time must be in the future."
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
return this.dispatch({ ...params, delay });
|
|
628
|
-
}
|
|
629
|
-
if (params.cron || params.every) {
|
|
630
|
-
return this.dispatch({ ...params, delay: params.delay ?? 0 });
|
|
631
|
-
}
|
|
632
|
-
return this.dispatch(params);
|
|
633
|
-
}
|
|
634
|
-
async getJob(jobId, queue) {
|
|
635
|
-
const job = this.jobsById.get(jobId);
|
|
636
|
-
if (!job) return null;
|
|
637
|
-
if (queue && job.queue !== queue) return null;
|
|
638
|
-
return this.toSearchResult(job);
|
|
639
|
-
}
|
|
640
|
-
async getJobState(jobId, queue) {
|
|
641
|
-
const job = this.jobsById.get(jobId);
|
|
642
|
-
if (!job) return null;
|
|
643
|
-
if (queue && job.queue !== queue) return null;
|
|
644
|
-
return job.status;
|
|
645
|
-
}
|
|
646
|
-
async getJobLogs(jobId, queue) {
|
|
647
|
-
const job = this.jobsById.get(jobId);
|
|
648
|
-
if (!job) return [];
|
|
649
|
-
if (queue && job.queue !== queue) return [];
|
|
650
|
-
return job.logs;
|
|
651
|
-
}
|
|
652
|
-
async getJobProgress(jobId, queue) {
|
|
653
|
-
const job = this.jobsById.get(jobId);
|
|
654
|
-
if (!job) return 0;
|
|
655
|
-
if (queue && job.queue !== queue) return 0;
|
|
656
|
-
return job.progress;
|
|
657
|
-
}
|
|
658
|
-
async retryJob(jobId, queue) {
|
|
659
|
-
const job = this.jobsById.get(jobId);
|
|
660
|
-
if (!job) {
|
|
661
|
-
throw new IgniterJobsError({
|
|
662
|
-
code: "JOBS_NOT_FOUND",
|
|
663
|
-
message: `Job "${jobId}" not found.`
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
if (queue && job.queue !== queue) {
|
|
667
|
-
throw new IgniterJobsError({
|
|
668
|
-
code: "JOBS_NOT_FOUND",
|
|
669
|
-
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
job.status = "waiting";
|
|
673
|
-
job.error = void 0;
|
|
674
|
-
job.completedAt = void 0;
|
|
675
|
-
job.progress = 0;
|
|
676
|
-
void this.kickWorkers(job.queue);
|
|
677
|
-
}
|
|
678
|
-
async removeJob(jobId, queue) {
|
|
679
|
-
const job = this.jobsById.get(jobId);
|
|
680
|
-
if (!job) return;
|
|
681
|
-
if (queue && job.queue !== queue) return;
|
|
682
|
-
this.jobsById.delete(jobId);
|
|
683
|
-
const list = this.jobsByQueue.get(job.queue);
|
|
684
|
-
if (list)
|
|
685
|
-
this.jobsByQueue.set(
|
|
686
|
-
job.queue,
|
|
687
|
-
list.filter((id) => id !== jobId)
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
async promoteJob(jobId, queue) {
|
|
691
|
-
const job = this.jobsById.get(jobId);
|
|
692
|
-
if (!job) {
|
|
693
|
-
throw new IgniterJobsError({
|
|
694
|
-
code: "JOBS_NOT_FOUND",
|
|
695
|
-
message: `Job "${jobId}" not found.`
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
if (queue && job.queue !== queue) {
|
|
699
|
-
throw new IgniterJobsError({
|
|
700
|
-
code: "JOBS_NOT_FOUND",
|
|
701
|
-
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
if (job.status === "delayed" || job.status === "paused") {
|
|
705
|
-
job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
|
|
706
|
-
void this.kickWorkers(job.queue);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
async moveJobToFailed(jobId, reason, queue) {
|
|
710
|
-
const job = this.jobsById.get(jobId);
|
|
711
|
-
if (!job) {
|
|
712
|
-
throw new IgniterJobsError({
|
|
713
|
-
code: "JOBS_NOT_FOUND",
|
|
714
|
-
message: `Job "${jobId}" not found.`
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
if (queue && job.queue !== queue) {
|
|
718
|
-
throw new IgniterJobsError({
|
|
719
|
-
code: "JOBS_NOT_FOUND",
|
|
720
|
-
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
job.status = "failed";
|
|
724
|
-
job.error = reason;
|
|
725
|
-
job.completedAt = /* @__PURE__ */ new Date();
|
|
726
|
-
}
|
|
727
|
-
async retryManyJobs(jobIds, queue) {
|
|
728
|
-
await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
|
|
729
|
-
}
|
|
730
|
-
async removeManyJobs(jobIds, queue) {
|
|
731
|
-
await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
|
|
732
|
-
}
|
|
733
|
-
async getQueueInfo(queue) {
|
|
734
|
-
const counts = await this.getQueueJobCounts(queue);
|
|
735
|
-
return {
|
|
736
|
-
name: queue,
|
|
737
|
-
isPaused: this.pausedQueues.has(queue),
|
|
738
|
-
jobCounts: counts
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
async getQueueJobCounts(queue) {
|
|
742
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
743
|
-
const counts = {
|
|
744
|
-
waiting: 0,
|
|
745
|
-
active: 0,
|
|
746
|
-
completed: 0,
|
|
747
|
-
failed: 0,
|
|
748
|
-
delayed: 0,
|
|
749
|
-
paused: 0
|
|
750
|
-
};
|
|
751
|
-
for (const id of jobIds) {
|
|
752
|
-
const job = this.jobsById.get(id);
|
|
753
|
-
if (!job) continue;
|
|
754
|
-
if (job.status in counts) {
|
|
755
|
-
counts[job.status]++;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
return counts;
|
|
759
|
-
}
|
|
760
|
-
async listQueues() {
|
|
761
|
-
const queues = Array.from(
|
|
762
|
-
/* @__PURE__ */ new Set([
|
|
763
|
-
...this.jobsByQueue.keys(),
|
|
764
|
-
...this.registeredJobs.keys(),
|
|
765
|
-
...this.registeredCrons.keys()
|
|
766
|
-
])
|
|
767
|
-
);
|
|
768
|
-
const result = [];
|
|
769
|
-
for (const q of queues) {
|
|
770
|
-
result.push(await this.getQueueInfo(q));
|
|
771
|
-
}
|
|
772
|
-
return result;
|
|
773
|
-
}
|
|
774
|
-
async pauseQueue(queue) {
|
|
775
|
-
this.pausedQueues.add(queue);
|
|
776
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
777
|
-
for (const id of jobIds) {
|
|
778
|
-
const job = this.jobsById.get(id);
|
|
779
|
-
if (!job) continue;
|
|
780
|
-
if (job.status === "waiting") job.status = "paused";
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async resumeQueue(queue) {
|
|
784
|
-
this.pausedQueues.delete(queue);
|
|
785
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
786
|
-
for (const id of jobIds) {
|
|
787
|
-
const job = this.jobsById.get(id);
|
|
788
|
-
if (!job) continue;
|
|
789
|
-
if (job.status === "paused") job.status = "waiting";
|
|
790
|
-
}
|
|
791
|
-
void this.kickWorkers(queue);
|
|
792
|
-
}
|
|
793
|
-
async drainQueue(queue) {
|
|
794
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
795
|
-
let removed = 0;
|
|
796
|
-
for (const id of jobIds) {
|
|
797
|
-
const job = this.jobsById.get(id);
|
|
798
|
-
if (!job) continue;
|
|
799
|
-
if (job.status === "waiting" || job.status === "paused") {
|
|
800
|
-
this.jobsById.delete(id);
|
|
801
|
-
removed++;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
this.jobsByQueue.set(
|
|
805
|
-
queue,
|
|
806
|
-
jobIds.filter((id) => this.jobsById.has(id))
|
|
807
|
-
);
|
|
808
|
-
return removed;
|
|
809
|
-
}
|
|
810
|
-
async cleanQueue(queue, options) {
|
|
811
|
-
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
812
|
-
const olderThan = options.olderThan ?? 0;
|
|
813
|
-
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
814
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
815
|
-
const now = Date.now();
|
|
816
|
-
let cleaned = 0;
|
|
817
|
-
for (const id of [...jobIds]) {
|
|
818
|
-
if (cleaned >= limit) break;
|
|
819
|
-
const job = this.jobsById.get(id);
|
|
820
|
-
if (!job) continue;
|
|
821
|
-
if (!statuses.includes(job.status)) continue;
|
|
822
|
-
const ageMs = now - job.createdAt.getTime();
|
|
823
|
-
if (ageMs < olderThan) continue;
|
|
824
|
-
this.jobsById.delete(id);
|
|
825
|
-
cleaned++;
|
|
826
|
-
}
|
|
827
|
-
this.jobsByQueue.set(
|
|
828
|
-
queue,
|
|
829
|
-
jobIds.filter((id) => this.jobsById.has(id))
|
|
830
|
-
);
|
|
831
|
-
return cleaned;
|
|
832
|
-
}
|
|
833
|
-
async obliterateQueue(queue, options) {
|
|
834
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
835
|
-
for (const id of jobIds) this.jobsById.delete(id);
|
|
836
|
-
this.jobsByQueue.delete(queue);
|
|
837
|
-
this.registeredJobs.delete(queue);
|
|
838
|
-
this.registeredCrons.delete(queue);
|
|
839
|
-
this.pausedQueues.delete(queue);
|
|
840
|
-
}
|
|
841
|
-
async retryAllInQueue(queue) {
|
|
842
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
843
|
-
let retried = 0;
|
|
844
|
-
for (const id of jobIds) {
|
|
845
|
-
const job = this.jobsById.get(id);
|
|
846
|
-
if (!job) continue;
|
|
847
|
-
if (job.status === "failed") {
|
|
848
|
-
await this.retryJob(id, queue);
|
|
849
|
-
retried++;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return retried;
|
|
853
|
-
}
|
|
854
|
-
async pauseJobType(queue, jobName) {
|
|
855
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
856
|
-
for (const id of jobIds) {
|
|
857
|
-
const job = this.jobsById.get(id);
|
|
858
|
-
if (!job) continue;
|
|
859
|
-
if (job.name === jobName && job.status === "waiting")
|
|
860
|
-
job.status = "paused";
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
async resumeJobType(queue, jobName) {
|
|
864
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
865
|
-
for (const id of jobIds) {
|
|
866
|
-
const job = this.jobsById.get(id);
|
|
867
|
-
if (!job) continue;
|
|
868
|
-
if (job.name === jobName && job.status === "paused")
|
|
869
|
-
job.status = "waiting";
|
|
870
|
-
}
|
|
871
|
-
void this.kickWorkers(queue);
|
|
872
|
-
}
|
|
873
|
-
async searchJobs(filter) {
|
|
874
|
-
const queue = filter?.queue;
|
|
875
|
-
const statuses = filter?.status;
|
|
876
|
-
const limit = filter?.limit ?? 100;
|
|
877
|
-
const offset = filter?.offset ?? 0;
|
|
878
|
-
const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort(
|
|
879
|
-
(a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
|
|
880
|
-
);
|
|
881
|
-
return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
|
|
882
|
-
}
|
|
883
|
-
async searchQueues(filter) {
|
|
884
|
-
const name = filter?.name;
|
|
885
|
-
const isPaused = filter?.isPaused;
|
|
886
|
-
const all = await this.listQueues();
|
|
887
|
-
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
888
|
-
(q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
async searchWorkers(filter) {
|
|
892
|
-
const queue = filter?.queue;
|
|
893
|
-
const isRunning = filter?.isRunning;
|
|
894
|
-
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
895
|
-
(w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
|
|
896
|
-
).map((w) => this.toWorkerHandle(w));
|
|
897
|
-
}
|
|
898
|
-
async createWorker(config) {
|
|
899
|
-
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
900
|
-
const state = {
|
|
901
|
-
id: workerId,
|
|
902
|
-
queues: config.queues ?? [],
|
|
903
|
-
concurrency: config.concurrency ?? 1,
|
|
904
|
-
paused: false,
|
|
905
|
-
closed: false,
|
|
906
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
907
|
-
metrics: { processed: 0, failed: 0, totalDuration: 0 },
|
|
908
|
-
handlers: config.handlers
|
|
909
|
-
};
|
|
910
|
-
this.workers.set(workerId, state);
|
|
911
|
-
for (const q of state.queues) void this.kickWorkers(q);
|
|
912
|
-
return this.toWorkerHandle(state);
|
|
913
|
-
}
|
|
914
|
-
getWorkers() {
|
|
915
|
-
const out = /* @__PURE__ */ new Map();
|
|
916
|
-
for (const [id, state] of this.workers)
|
|
917
|
-
out.set(id, this.toWorkerHandle(state));
|
|
918
|
-
return out;
|
|
919
|
-
}
|
|
920
|
-
async publishEvent(channel, payload) {
|
|
921
|
-
const handlers = this.subscribers.get(channel);
|
|
922
|
-
if (!handlers) return;
|
|
923
|
-
await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
|
|
924
|
-
}
|
|
925
|
-
async subscribeEvent(channel, handler) {
|
|
926
|
-
const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
927
|
-
set.add(handler);
|
|
928
|
-
this.subscribers.set(channel, set);
|
|
929
|
-
return async () => {
|
|
930
|
-
const current = this.subscribers.get(channel);
|
|
931
|
-
if (!current) return;
|
|
932
|
-
current.delete(handler);
|
|
933
|
-
if (current.size === 0) this.subscribers.delete(channel);
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
async shutdown() {
|
|
937
|
-
this.workers.clear();
|
|
938
|
-
this.subscribers.clear();
|
|
939
|
-
}
|
|
940
|
-
toSearchResult(job) {
|
|
941
|
-
return {
|
|
942
|
-
id: job.id,
|
|
943
|
-
name: job.name,
|
|
944
|
-
queue: job.queue,
|
|
945
|
-
status: job.status,
|
|
946
|
-
input: job.input,
|
|
947
|
-
result: job.result,
|
|
948
|
-
error: job.error,
|
|
949
|
-
progress: job.progress,
|
|
950
|
-
attemptsMade: job.attemptsMade,
|
|
951
|
-
priority: job.priority,
|
|
952
|
-
createdAt: job.createdAt,
|
|
953
|
-
startedAt: job.startedAt,
|
|
954
|
-
completedAt: job.completedAt,
|
|
955
|
-
metadata: job.metadata,
|
|
956
|
-
scope: job.scope
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
toWorkerHandle(worker) {
|
|
960
|
-
return {
|
|
961
|
-
id: worker.id,
|
|
962
|
-
queues: worker.queues,
|
|
963
|
-
pause: async () => {
|
|
964
|
-
worker.paused = true;
|
|
965
|
-
},
|
|
966
|
-
resume: async () => {
|
|
967
|
-
worker.paused = false;
|
|
968
|
-
for (const q of worker.queues) void this.kickWorkers(q);
|
|
969
|
-
},
|
|
970
|
-
close: async () => {
|
|
971
|
-
worker.closed = true;
|
|
972
|
-
},
|
|
973
|
-
isRunning: () => !worker.closed && !worker.paused,
|
|
974
|
-
isPaused: () => worker.paused,
|
|
975
|
-
isClosed: () => worker.closed,
|
|
976
|
-
getMetrics: async () => this.toWorkerMetrics(worker)
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
toWorkerMetrics(worker) {
|
|
980
|
-
const uptime = Date.now() - worker.startedAt.getTime();
|
|
981
|
-
const processed = worker.metrics.processed;
|
|
982
|
-
return {
|
|
983
|
-
processed,
|
|
984
|
-
failed: worker.metrics.failed,
|
|
985
|
-
avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
|
|
986
|
-
concurrency: worker.concurrency,
|
|
987
|
-
uptime
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
async kickWorkers(queue) {
|
|
991
|
-
if (this.pausedQueues.has(queue)) return;
|
|
992
|
-
const relevant = Array.from(this.workers.values()).filter(
|
|
993
|
-
(w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue))
|
|
994
|
-
);
|
|
995
|
-
if (relevant.length === 0) return;
|
|
996
|
-
for (const w of relevant) {
|
|
997
|
-
void this.processLoop(w, queue);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
async processLoop(worker, queue) {
|
|
1001
|
-
if (worker.closed || worker.paused) return;
|
|
1002
|
-
const concurrency = Math.max(1, worker.concurrency);
|
|
1003
|
-
const running = worker.__running;
|
|
1004
|
-
const currentRunning = running ?? 0;
|
|
1005
|
-
if (currentRunning >= concurrency) return;
|
|
1006
|
-
worker.__running = currentRunning + 1;
|
|
1007
|
-
try {
|
|
1008
|
-
const next = this.nextJob(queue);
|
|
1009
|
-
if (!next) return;
|
|
1010
|
-
await this.processJob(worker, next);
|
|
1011
|
-
} finally {
|
|
1012
|
-
worker.__running = worker.__running - 1;
|
|
1013
|
-
if (this.nextJob(queue)) void this.processLoop(worker, queue);
|
|
1014
|
-
else if (worker.handlers?.onIdle) await worker.handlers.onIdle();
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
nextJob(queue) {
|
|
1018
|
-
const ids = this.jobsByQueue.get(queue) ?? [];
|
|
1019
|
-
const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort(
|
|
1020
|
-
(a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
|
|
1021
|
-
);
|
|
1022
|
-
return candidates[0] ?? null;
|
|
1023
|
-
}
|
|
1024
|
-
async processJob(worker, job) {
|
|
1025
|
-
if (this.pausedQueues.has(job.queue)) {
|
|
1026
|
-
job.status = "paused";
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
job.status = "active";
|
|
1030
|
-
job.startedAt = /* @__PURE__ */ new Date();
|
|
1031
|
-
job.attemptsMade += 1;
|
|
1032
|
-
job.logs.push({
|
|
1033
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1034
|
-
level: "info",
|
|
1035
|
-
message: "Job started"
|
|
1036
|
-
});
|
|
1037
|
-
if (worker.handlers?.onActive)
|
|
1038
|
-
await worker.handlers.onActive({ job: this.toSearchResult(job) });
|
|
1039
|
-
const start = Date.now();
|
|
1040
|
-
try {
|
|
1041
|
-
const definition = this.registeredJobs.get(job.queue)?.get(job.name);
|
|
1042
|
-
if (!definition) {
|
|
1043
|
-
throw new IgniterJobsError({
|
|
1044
|
-
code: "JOBS_NOT_REGISTERED",
|
|
1045
|
-
message: `Job "${job.name}" is not registered for queue "${job.queue}".`
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
if (definition.onStart) {
|
|
1049
|
-
await definition.onStart({
|
|
1050
|
-
input: job.input,
|
|
1051
|
-
context: {},
|
|
1052
|
-
job: {
|
|
1053
|
-
id: job.id,
|
|
1054
|
-
name: job.name,
|
|
1055
|
-
queue: job.queue,
|
|
1056
|
-
attemptsMade: job.attemptsMade,
|
|
1057
|
-
metadata: job.metadata
|
|
1058
|
-
},
|
|
1059
|
-
scope: job.scope,
|
|
1060
|
-
startedAt: job.startedAt
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
const result = await definition.handler({
|
|
1064
|
-
input: job.input,
|
|
1065
|
-
context: {},
|
|
1066
|
-
job: {
|
|
1067
|
-
id: job.id,
|
|
1068
|
-
name: job.name,
|
|
1069
|
-
queue: job.queue,
|
|
1070
|
-
attemptsMade: job.attemptsMade,
|
|
1071
|
-
metadata: job.metadata
|
|
1072
|
-
},
|
|
1073
|
-
scope: job.scope
|
|
1074
|
-
});
|
|
1075
|
-
const duration = Date.now() - start;
|
|
1076
|
-
job.status = "completed";
|
|
1077
|
-
job.completedAt = /* @__PURE__ */ new Date();
|
|
1078
|
-
job.result = result;
|
|
1079
|
-
job.progress = 100;
|
|
1080
|
-
job.logs.push({
|
|
1081
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1082
|
-
level: "info",
|
|
1083
|
-
message: `Job completed in ${duration}ms`
|
|
1084
|
-
});
|
|
1085
|
-
worker.metrics.processed += 1;
|
|
1086
|
-
worker.metrics.totalDuration += duration;
|
|
1087
|
-
if (definition.onSuccess) {
|
|
1088
|
-
await definition.onSuccess({
|
|
1089
|
-
input: job.input,
|
|
1090
|
-
context: {},
|
|
1091
|
-
job: {
|
|
1092
|
-
id: job.id,
|
|
1093
|
-
name: job.name,
|
|
1094
|
-
queue: job.queue,
|
|
1095
|
-
attemptsMade: job.attemptsMade,
|
|
1096
|
-
metadata: job.metadata
|
|
1097
|
-
},
|
|
1098
|
-
scope: job.scope,
|
|
1099
|
-
result,
|
|
1100
|
-
duration
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
if (worker.handlers?.onSuccess)
|
|
1104
|
-
await worker.handlers.onSuccess({
|
|
1105
|
-
job: this.toSearchResult(job),
|
|
1106
|
-
result
|
|
1107
|
-
});
|
|
1108
|
-
} catch (error) {
|
|
1109
|
-
job.error = error?.message ?? String(error);
|
|
1110
|
-
job.logs.push({
|
|
1111
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1112
|
-
level: "error",
|
|
1113
|
-
message: job.error ?? "Unknown error"
|
|
1114
|
-
});
|
|
1115
|
-
const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
|
|
1116
|
-
if (isFinalAttempt) {
|
|
1117
|
-
job.status = "failed";
|
|
1118
|
-
job.completedAt = /* @__PURE__ */ new Date();
|
|
1119
|
-
worker.metrics.failed += 1;
|
|
1120
|
-
const definition = this.registeredJobs.get(job.queue)?.get(job.name);
|
|
1121
|
-
if (definition?.onFailure) {
|
|
1122
|
-
await definition.onFailure({
|
|
1123
|
-
input: job.input,
|
|
1124
|
-
context: {},
|
|
1125
|
-
job: {
|
|
1126
|
-
id: job.id,
|
|
1127
|
-
name: job.name,
|
|
1128
|
-
queue: job.queue,
|
|
1129
|
-
attemptsMade: job.attemptsMade,
|
|
1130
|
-
metadata: job.metadata
|
|
1131
|
-
},
|
|
1132
|
-
scope: job.scope,
|
|
1133
|
-
error,
|
|
1134
|
-
isFinalAttempt: true
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
if (worker.handlers?.onFailure)
|
|
1138
|
-
await worker.handlers.onFailure({
|
|
1139
|
-
job: this.toSearchResult(job),
|
|
1140
|
-
error
|
|
1141
|
-
});
|
|
1142
|
-
} else {
|
|
1143
|
-
job.status = "waiting";
|
|
1144
|
-
void this.kickWorkers(job.queue);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
};
|
|
1149
|
-
|
|
1150
|
-
// src/adapters/sqlite.adapter.ts
|
|
1151
|
-
var IgniterJobsSQLiteAdapter = class _IgniterJobsSQLiteAdapter {
|
|
1152
|
-
constructor(options) {
|
|
1153
|
-
this.registeredJobs = /* @__PURE__ */ new Map();
|
|
1154
|
-
this.registeredCrons = /* @__PURE__ */ new Map();
|
|
1155
|
-
this.workers = /* @__PURE__ */ new Map();
|
|
1156
|
-
this.subscribers = /* @__PURE__ */ new Map();
|
|
1157
|
-
this.pausedQueues = /* @__PURE__ */ new Set();
|
|
1158
|
-
this.queues = {
|
|
1159
|
-
list: async () => this.listQueues(),
|
|
1160
|
-
get: async (name) => this.getQueueInfo(name),
|
|
1161
|
-
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
1162
|
-
getJobs: async (name, filter) => {
|
|
1163
|
-
const statuses = filter?.status;
|
|
1164
|
-
const limit = filter?.limit ?? 100;
|
|
1165
|
-
const offset = filter?.offset ?? 0;
|
|
1166
|
-
const results = await this.searchJobs({
|
|
1167
|
-
queue: name,
|
|
1168
|
-
status: statuses,
|
|
1169
|
-
limit,
|
|
1170
|
-
offset
|
|
1171
|
-
});
|
|
1172
|
-
return results;
|
|
1173
|
-
},
|
|
1174
|
-
pause: async (name) => this.pauseQueue(name),
|
|
1175
|
-
resume: async (name) => this.resumeQueue(name),
|
|
1176
|
-
isPaused: async (name) => {
|
|
1177
|
-
const info = await this.getQueueInfo(name);
|
|
1178
|
-
return info?.isPaused ?? false;
|
|
1179
|
-
},
|
|
1180
|
-
drain: async (name) => this.drainQueue(name),
|
|
1181
|
-
clean: async (name, options) => this.cleanQueue(name, options),
|
|
1182
|
-
obliterate: async (name, options) => this.obliterateQueue(name, options)
|
|
1183
|
-
};
|
|
1184
|
-
this.options = {
|
|
1185
|
-
path: options.path,
|
|
1186
|
-
pollingInterval: options.pollingInterval ?? 500,
|
|
1187
|
-
enableWAL: options.enableWAL ?? true
|
|
1188
|
-
};
|
|
1189
|
-
this.client = {
|
|
1190
|
-
type: "sqlite",
|
|
1191
|
-
path: this.options.path
|
|
1192
|
-
};
|
|
1193
|
-
const Database = __require("better-sqlite3");
|
|
1194
|
-
this.db = new Database(this.options.path);
|
|
1195
|
-
this.initializeSchema();
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Creates a new SQLite adapter instance.
|
|
1199
|
-
*
|
|
1200
|
-
* @param options - Configuration options
|
|
1201
|
-
* @returns A new adapter instance
|
|
1202
|
-
*
|
|
1203
|
-
* @example
|
|
1204
|
-
* ```ts
|
|
1205
|
-
* // File-based database (persistent)
|
|
1206
|
-
* const adapter = IgniterJobsSQLiteAdapter.create({
|
|
1207
|
-
* path: './data/jobs.sqlite'
|
|
1208
|
-
* });
|
|
1209
|
-
*
|
|
1210
|
-
* // In-memory database (for testing)
|
|
1211
|
-
* const testAdapter = IgniterJobsSQLiteAdapter.create({
|
|
1212
|
-
* path: ':memory:'
|
|
1213
|
-
* });
|
|
1214
|
-
* ```
|
|
1215
|
-
*/
|
|
1216
|
-
static create(options) {
|
|
1217
|
-
return new _IgniterJobsSQLiteAdapter(options);
|
|
1218
|
-
}
|
|
1219
|
-
initializeSchema() {
|
|
1220
|
-
if (this.options.enableWAL) {
|
|
1221
|
-
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
1222
|
-
}
|
|
1223
|
-
this.db.exec(`
|
|
1224
|
-
CREATE TABLE IF NOT EXISTS jobs (
|
|
1225
|
-
id TEXT PRIMARY KEY,
|
|
1226
|
-
name TEXT NOT NULL,
|
|
1227
|
-
queue TEXT NOT NULL,
|
|
1228
|
-
input TEXT NOT NULL,
|
|
1229
|
-
status TEXT NOT NULL DEFAULT 'waiting',
|
|
1230
|
-
progress REAL NOT NULL DEFAULT 0,
|
|
1231
|
-
attempts_made INTEGER NOT NULL DEFAULT 0,
|
|
1232
|
-
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
1233
|
-
priority INTEGER NOT NULL DEFAULT 0,
|
|
1234
|
-
created_at TEXT NOT NULL,
|
|
1235
|
-
started_at TEXT,
|
|
1236
|
-
completed_at TEXT,
|
|
1237
|
-
scheduled_at TEXT,
|
|
1238
|
-
result TEXT,
|
|
1239
|
-
error TEXT,
|
|
1240
|
-
metadata TEXT,
|
|
1241
|
-
scope TEXT
|
|
1242
|
-
);
|
|
1243
|
-
|
|
1244
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_queue_status ON jobs(queue, status);
|
|
1245
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
1246
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_scheduled_at ON jobs(scheduled_at);
|
|
1247
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_priority ON jobs(priority DESC, created_at ASC);
|
|
1248
|
-
`);
|
|
1249
|
-
this.db.exec(`
|
|
1250
|
-
CREATE TABLE IF NOT EXISTS job_logs (
|
|
1251
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1252
|
-
job_id TEXT NOT NULL,
|
|
1253
|
-
timestamp TEXT NOT NULL,
|
|
1254
|
-
level TEXT NOT NULL,
|
|
1255
|
-
message TEXT NOT NULL,
|
|
1256
|
-
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
1257
|
-
);
|
|
1258
|
-
|
|
1259
|
-
CREATE INDEX IF NOT EXISTS idx_job_logs_job_id ON job_logs(job_id);
|
|
1260
|
-
`);
|
|
1261
|
-
this.db.exec(`
|
|
1262
|
-
CREATE TABLE IF NOT EXISTS paused_queues (
|
|
1263
|
-
name TEXT PRIMARY KEY
|
|
1264
|
-
);
|
|
1265
|
-
`);
|
|
1266
|
-
const pausedRows = this.db.prepare(
|
|
1267
|
-
"SELECT name FROM paused_queues"
|
|
1268
|
-
).all();
|
|
1269
|
-
for (const row of pausedRows) {
|
|
1270
|
-
this.pausedQueues.add(row.name);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
registerJob(queueName, jobName, definition) {
|
|
1274
|
-
const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
1275
|
-
if (queueJobs.has(jobName)) {
|
|
1276
|
-
throw new IgniterJobsError({
|
|
1277
|
-
code: "JOBS_DUPLICATE_JOB",
|
|
1278
|
-
message: `Job "${jobName}" already registered for queue "${queueName}".`
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
queueJobs.set(jobName, definition);
|
|
1282
|
-
this.registeredJobs.set(queueName, queueJobs);
|
|
1283
|
-
}
|
|
1284
|
-
registerCron(queueName, cronName, definition) {
|
|
1285
|
-
const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
1286
|
-
if (queueCrons.has(cronName)) {
|
|
1287
|
-
throw new IgniterJobsError({
|
|
1288
|
-
code: "JOBS_INVALID_CRON",
|
|
1289
|
-
message: `Cron "${cronName}" already registered for queue "${queueName}".`
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
queueCrons.set(cronName, definition);
|
|
1293
|
-
this.registeredCrons.set(queueName, queueCrons);
|
|
1294
|
-
}
|
|
1295
|
-
async dispatch(params) {
|
|
1296
|
-
const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
|
|
1297
|
-
const maxAttempts = params.attempts ?? 1;
|
|
1298
|
-
const now = /* @__PURE__ */ new Date();
|
|
1299
|
-
let status = "waiting";
|
|
1300
|
-
let scheduledAt = null;
|
|
1301
|
-
if (this.pausedQueues.has(params.queue)) {
|
|
1302
|
-
status = "paused";
|
|
1303
|
-
} else if (params.delay && params.delay > 0) {
|
|
1304
|
-
status = "delayed";
|
|
1305
|
-
scheduledAt = new Date(now.getTime() + params.delay);
|
|
1306
|
-
}
|
|
1307
|
-
const stmt = this.db.prepare(`
|
|
1308
|
-
INSERT INTO jobs (
|
|
1309
|
-
id, name, queue, input, status, progress, attempts_made, max_attempts,
|
|
1310
|
-
priority, created_at, scheduled_at, metadata, scope
|
|
1311
|
-
) VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
|
1312
|
-
`);
|
|
1313
|
-
stmt.run(
|
|
1314
|
-
jobId,
|
|
1315
|
-
params.jobName,
|
|
1316
|
-
params.queue,
|
|
1317
|
-
JSON.stringify(params.input ?? {}),
|
|
1318
|
-
status,
|
|
1319
|
-
maxAttempts,
|
|
1320
|
-
params.priority ?? 0,
|
|
1321
|
-
now.toISOString(),
|
|
1322
|
-
scheduledAt?.toISOString() ?? null,
|
|
1323
|
-
params.metadata ? JSON.stringify(params.metadata) : null,
|
|
1324
|
-
params.scope ? JSON.stringify(params.scope) : null
|
|
1325
|
-
);
|
|
1326
|
-
if (params.delay && params.delay > 0) {
|
|
1327
|
-
setTimeout(() => {
|
|
1328
|
-
this.promoteDelayedJob(jobId, params.queue);
|
|
1329
|
-
}, params.delay);
|
|
1330
|
-
}
|
|
1331
|
-
return jobId;
|
|
1332
|
-
}
|
|
1333
|
-
promoteDelayedJob(jobId, queue) {
|
|
1334
|
-
if (this.pausedQueues.has(queue)) return;
|
|
1335
|
-
const stmt = this.db.prepare(`
|
|
1336
|
-
UPDATE jobs SET status = 'waiting', scheduled_at = NULL
|
|
1337
|
-
WHERE id = ? AND status = 'delayed'
|
|
1338
|
-
`);
|
|
1339
|
-
stmt.run(jobId);
|
|
1340
|
-
}
|
|
1341
|
-
async schedule(params) {
|
|
1342
|
-
if (params.at) {
|
|
1343
|
-
const delay = params.at.getTime() - Date.now();
|
|
1344
|
-
if (delay <= 0) {
|
|
1345
|
-
throw new IgniterJobsError({
|
|
1346
|
-
code: "JOBS_INVALID_SCHEDULE",
|
|
1347
|
-
message: "Scheduled time must be in the future."
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
|
-
return this.dispatch({ ...params, delay });
|
|
1351
|
-
}
|
|
1352
|
-
if (params.cron || params.every) {
|
|
1353
|
-
return this.dispatch({ ...params, delay: params.delay ?? 0 });
|
|
1354
|
-
}
|
|
1355
|
-
return this.dispatch(params);
|
|
1356
|
-
}
|
|
1357
|
-
async getJob(jobId, queue) {
|
|
1358
|
-
let sql = "SELECT * FROM jobs WHERE id = ?";
|
|
1359
|
-
const params = [jobId];
|
|
1360
|
-
if (queue) {
|
|
1361
|
-
sql += " AND queue = ?";
|
|
1362
|
-
params.push(queue);
|
|
1363
|
-
}
|
|
1364
|
-
const row = this.db.prepare(sql).get(...params);
|
|
1365
|
-
if (!row) return null;
|
|
1366
|
-
return this.rowToSearchResult(row);
|
|
1367
|
-
}
|
|
1368
|
-
async getJobState(jobId, queue) {
|
|
1369
|
-
let sql = "SELECT status FROM jobs WHERE id = ?";
|
|
1370
|
-
const params = [jobId];
|
|
1371
|
-
if (queue) {
|
|
1372
|
-
sql += " AND queue = ?";
|
|
1373
|
-
params.push(queue);
|
|
1374
|
-
}
|
|
1375
|
-
const row = this.db.prepare(sql).get(...params);
|
|
1376
|
-
return row?.status ?? null;
|
|
1377
|
-
}
|
|
1378
|
-
async getJobLogs(jobId, queue) {
|
|
1379
|
-
if (queue) {
|
|
1380
|
-
const job = await this.getJob(jobId, queue);
|
|
1381
|
-
if (!job) return [];
|
|
1382
|
-
}
|
|
1383
|
-
const rows = this.db.prepare("SELECT * FROM job_logs WHERE job_id = ? ORDER BY timestamp ASC").all(jobId);
|
|
1384
|
-
return rows.map((row) => ({
|
|
1385
|
-
timestamp: new Date(row.timestamp),
|
|
1386
|
-
level: row.level,
|
|
1387
|
-
message: row.message
|
|
1388
|
-
}));
|
|
1389
|
-
}
|
|
1390
|
-
async getJobProgress(jobId, queue) {
|
|
1391
|
-
let sql = "SELECT progress FROM jobs WHERE id = ?";
|
|
1392
|
-
const params = [jobId];
|
|
1393
|
-
if (queue) {
|
|
1394
|
-
sql += " AND queue = ?";
|
|
1395
|
-
params.push(queue);
|
|
1396
|
-
}
|
|
1397
|
-
const row = this.db.prepare(sql).get(...params);
|
|
1398
|
-
return row?.progress ?? 0;
|
|
1399
|
-
}
|
|
1400
|
-
async retryJob(jobId, queue) {
|
|
1401
|
-
let sql = "SELECT id FROM jobs WHERE id = ?";
|
|
1402
|
-
const checkParams = [jobId];
|
|
1403
|
-
if (queue) {
|
|
1404
|
-
sql += " AND queue = ?";
|
|
1405
|
-
checkParams.push(queue);
|
|
1406
|
-
}
|
|
1407
|
-
const exists = this.db.prepare(sql).get(...checkParams);
|
|
1408
|
-
if (!exists) {
|
|
1409
|
-
throw new IgniterJobsError({
|
|
1410
|
-
code: "JOBS_NOT_FOUND",
|
|
1411
|
-
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
let updateSql = "UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0 WHERE id = ?";
|
|
1415
|
-
const updateParams = [jobId];
|
|
1416
|
-
if (queue) {
|
|
1417
|
-
updateSql = updateSql.replace("WHERE id = ?", "WHERE id = ? AND queue = ?");
|
|
1418
|
-
updateParams.push(queue);
|
|
1419
|
-
}
|
|
1420
|
-
this.db.prepare(updateSql).run(...updateParams);
|
|
1421
|
-
}
|
|
1422
|
-
async removeJob(jobId, queue) {
|
|
1423
|
-
let sql = "DELETE FROM jobs WHERE id = ?";
|
|
1424
|
-
const params = [jobId];
|
|
1425
|
-
if (queue) {
|
|
1426
|
-
sql += " AND queue = ?";
|
|
1427
|
-
params.push(queue);
|
|
1428
|
-
}
|
|
1429
|
-
this.db.prepare(sql).run(...params);
|
|
1430
|
-
}
|
|
1431
|
-
async promoteJob(jobId, queue) {
|
|
1432
|
-
let sql = "SELECT id, status, queue FROM jobs WHERE id = ?";
|
|
1433
|
-
const checkParams = [jobId];
|
|
1434
|
-
if (queue) {
|
|
1435
|
-
sql += " AND queue = ?";
|
|
1436
|
-
checkParams.push(queue);
|
|
1437
|
-
}
|
|
1438
|
-
const row = this.db.prepare(sql).get(...checkParams);
|
|
1439
|
-
if (!row) {
|
|
1440
|
-
throw new IgniterJobsError({
|
|
1441
|
-
code: "JOBS_NOT_FOUND",
|
|
1442
|
-
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
if (row.status === "delayed" || row.status === "paused") {
|
|
1446
|
-
const newStatus = this.pausedQueues.has(row.queue) ? "paused" : "waiting";
|
|
1447
|
-
this.db.prepare("UPDATE jobs SET status = ?, scheduled_at = NULL WHERE id = ?").run(newStatus, jobId);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
async moveJobToFailed(jobId, reason, queue) {
|
|
1451
|
-
let sql = "SELECT id FROM jobs WHERE id = ?";
|
|
1452
|
-
const checkParams = [jobId];
|
|
1453
|
-
if (queue) {
|
|
1454
|
-
sql += " AND queue = ?";
|
|
1455
|
-
checkParams.push(queue);
|
|
1456
|
-
}
|
|
1457
|
-
const exists = this.db.prepare(sql).get(...checkParams);
|
|
1458
|
-
if (!exists) {
|
|
1459
|
-
throw new IgniterJobsError({
|
|
1460
|
-
code: "JOBS_NOT_FOUND",
|
|
1461
|
-
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
this.db.prepare(`
|
|
1465
|
-
UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
|
|
1466
|
-
WHERE id = ?
|
|
1467
|
-
`).run(reason, (/* @__PURE__ */ new Date()).toISOString(), jobId);
|
|
1468
|
-
}
|
|
1469
|
-
async retryManyJobs(jobIds, queue) {
|
|
1470
|
-
await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
|
|
1471
|
-
}
|
|
1472
|
-
async removeManyJobs(jobIds, queue) {
|
|
1473
|
-
await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
|
|
1474
|
-
}
|
|
1475
|
-
async getQueueInfo(queue) {
|
|
1476
|
-
const counts = await this.getQueueJobCounts(queue);
|
|
1477
|
-
return {
|
|
1478
|
-
name: queue,
|
|
1479
|
-
isPaused: this.pausedQueues.has(queue),
|
|
1480
|
-
jobCounts: counts
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
async getQueueJobCounts(queue) {
|
|
1484
|
-
const counts = {
|
|
1485
|
-
waiting: 0,
|
|
1486
|
-
active: 0,
|
|
1487
|
-
completed: 0,
|
|
1488
|
-
failed: 0,
|
|
1489
|
-
delayed: 0,
|
|
1490
|
-
paused: 0
|
|
1491
|
-
};
|
|
1492
|
-
const rows = this.db.prepare(
|
|
1493
|
-
"SELECT status, COUNT(*) as count FROM jobs WHERE queue = ? GROUP BY status"
|
|
1494
|
-
).all(queue);
|
|
1495
|
-
for (const row of rows) {
|
|
1496
|
-
if (row.status in counts) {
|
|
1497
|
-
counts[row.status] = row.count;
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
return counts;
|
|
1501
|
-
}
|
|
1502
|
-
async listQueues() {
|
|
1503
|
-
const jobQueues = this.db.prepare("SELECT DISTINCT queue FROM jobs").all().map((r) => r.queue);
|
|
1504
|
-
const allQueues = /* @__PURE__ */ new Set([
|
|
1505
|
-
...jobQueues,
|
|
1506
|
-
...this.registeredJobs.keys(),
|
|
1507
|
-
...this.registeredCrons.keys()
|
|
1508
|
-
]);
|
|
1509
|
-
const result = [];
|
|
1510
|
-
for (const q of allQueues) {
|
|
1511
|
-
const info = await this.getQueueInfo(q);
|
|
1512
|
-
if (info) result.push(info);
|
|
1513
|
-
}
|
|
1514
|
-
return result;
|
|
1515
|
-
}
|
|
1516
|
-
async pauseQueue(queue) {
|
|
1517
|
-
this.pausedQueues.add(queue);
|
|
1518
|
-
this.db.prepare("INSERT OR IGNORE INTO paused_queues (name) VALUES (?)").run(queue);
|
|
1519
|
-
this.db.prepare(
|
|
1520
|
-
"UPDATE jobs SET status = 'paused' WHERE queue = ? AND status = 'waiting'"
|
|
1521
|
-
).run(queue);
|
|
1522
|
-
}
|
|
1523
|
-
async resumeQueue(queue) {
|
|
1524
|
-
this.pausedQueues.delete(queue);
|
|
1525
|
-
this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
|
|
1526
|
-
this.db.prepare(
|
|
1527
|
-
"UPDATE jobs SET status = 'waiting' WHERE queue = ? AND status = 'paused'"
|
|
1528
|
-
).run(queue);
|
|
1529
|
-
}
|
|
1530
|
-
async drainQueue(queue) {
|
|
1531
|
-
const result = this.db.prepare(
|
|
1532
|
-
"DELETE FROM jobs WHERE queue = ? AND status IN ('waiting', 'paused')"
|
|
1533
|
-
).run(queue);
|
|
1534
|
-
return result.changes;
|
|
1535
|
-
}
|
|
1536
|
-
async cleanQueue(queue, options) {
|
|
1537
|
-
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
1538
|
-
const olderThan = options.olderThan ?? 0;
|
|
1539
|
-
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
1540
|
-
const cutoffTime = new Date(Date.now() - olderThan).toISOString();
|
|
1541
|
-
const statusPlaceholders = statuses.map(() => "?").join(", ");
|
|
1542
|
-
let sql = `
|
|
1543
|
-
DELETE FROM jobs WHERE id IN (
|
|
1544
|
-
SELECT id FROM jobs
|
|
1545
|
-
WHERE queue = ? AND status IN (${statusPlaceholders}) AND created_at < ?
|
|
1546
|
-
ORDER BY created_at ASC
|
|
1547
|
-
LIMIT ?
|
|
1548
|
-
)
|
|
1549
|
-
`;
|
|
1550
|
-
const result = this.db.prepare(sql).run(
|
|
1551
|
-
queue,
|
|
1552
|
-
...statuses,
|
|
1553
|
-
cutoffTime,
|
|
1554
|
-
limit === Number.POSITIVE_INFINITY ? -1 : limit
|
|
1555
|
-
);
|
|
1556
|
-
return result.changes;
|
|
1557
|
-
}
|
|
1558
|
-
async obliterateQueue(queue, _options) {
|
|
1559
|
-
this.db.prepare("DELETE FROM jobs WHERE queue = ?").run(queue);
|
|
1560
|
-
this.registeredJobs.delete(queue);
|
|
1561
|
-
this.registeredCrons.delete(queue);
|
|
1562
|
-
this.pausedQueues.delete(queue);
|
|
1563
|
-
this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
|
|
1564
|
-
}
|
|
1565
|
-
async retryAllInQueue(queue) {
|
|
1566
|
-
const result = this.db.prepare(`
|
|
1567
|
-
UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0
|
|
1568
|
-
WHERE queue = ? AND status = 'failed'
|
|
1569
|
-
`).run(queue);
|
|
1570
|
-
return result.changes;
|
|
1571
|
-
}
|
|
1572
|
-
async pauseJobType(queue, jobName) {
|
|
1573
|
-
this.db.prepare(
|
|
1574
|
-
"UPDATE jobs SET status = 'paused' WHERE queue = ? AND name = ? AND status = 'waiting'"
|
|
1575
|
-
).run(queue, jobName);
|
|
1576
|
-
}
|
|
1577
|
-
async resumeJobType(queue, jobName) {
|
|
1578
|
-
this.db.prepare(
|
|
1579
|
-
"UPDATE jobs SET status = 'waiting' WHERE queue = ? AND name = ? AND status = 'paused'"
|
|
1580
|
-
).run(queue, jobName);
|
|
1581
|
-
}
|
|
1582
|
-
async searchJobs(filter) {
|
|
1583
|
-
const queue = filter?.queue;
|
|
1584
|
-
const statuses = filter?.status;
|
|
1585
|
-
const limit = filter?.limit ?? 100;
|
|
1586
|
-
const offset = filter?.offset ?? 0;
|
|
1587
|
-
let sql = "SELECT * FROM jobs WHERE 1=1";
|
|
1588
|
-
const params = [];
|
|
1589
|
-
if (queue) {
|
|
1590
|
-
sql += " AND queue = ?";
|
|
1591
|
-
params.push(queue);
|
|
1592
|
-
}
|
|
1593
|
-
if (statuses && statuses.length > 0) {
|
|
1594
|
-
const placeholders = statuses.map(() => "?").join(", ");
|
|
1595
|
-
sql += ` AND status IN (${placeholders})`;
|
|
1596
|
-
params.push(...statuses);
|
|
1597
|
-
}
|
|
1598
|
-
sql += " ORDER BY priority DESC, created_at ASC LIMIT ? OFFSET ?";
|
|
1599
|
-
params.push(limit, offset);
|
|
1600
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
1601
|
-
return rows.map((row) => this.rowToSearchResult(row));
|
|
1602
|
-
}
|
|
1603
|
-
async searchQueues(filter) {
|
|
1604
|
-
const name = filter?.name;
|
|
1605
|
-
const isPaused = filter?.isPaused;
|
|
1606
|
-
const all = await this.listQueues();
|
|
1607
|
-
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
1608
|
-
(q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
|
|
1609
|
-
);
|
|
1610
|
-
}
|
|
1611
|
-
async searchWorkers(filter) {
|
|
1612
|
-
const queue = filter?.queue;
|
|
1613
|
-
const isRunning = filter?.isRunning;
|
|
1614
|
-
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
1615
|
-
(w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
|
|
1616
|
-
).map((w) => this.toWorkerHandle(w));
|
|
1617
|
-
}
|
|
1618
|
-
async createWorker(config) {
|
|
1619
|
-
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
1620
|
-
const state = {
|
|
1621
|
-
id: workerId,
|
|
1622
|
-
queues: config.queues ?? [],
|
|
1623
|
-
concurrency: config.concurrency ?? 1,
|
|
1624
|
-
paused: false,
|
|
1625
|
-
closed: false,
|
|
1626
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
1627
|
-
metrics: { processed: 0, failed: 0, totalDuration: 0 },
|
|
1628
|
-
handlers: config.handlers,
|
|
1629
|
-
activeJobs: 0
|
|
1630
|
-
};
|
|
1631
|
-
this.workers.set(workerId, state);
|
|
1632
|
-
this.startPollingLoop(state);
|
|
1633
|
-
return this.toWorkerHandle(state);
|
|
1634
|
-
}
|
|
1635
|
-
getWorkers() {
|
|
1636
|
-
const out = /* @__PURE__ */ new Map();
|
|
1637
|
-
for (const [id, state] of this.workers) {
|
|
1638
|
-
out.set(id, this.toWorkerHandle(state));
|
|
1639
|
-
}
|
|
1640
|
-
return out;
|
|
1641
|
-
}
|
|
1642
|
-
async publishEvent(channel, payload) {
|
|
1643
|
-
const handlers = this.subscribers.get(channel);
|
|
1644
|
-
if (!handlers) return;
|
|
1645
|
-
await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
|
|
1646
|
-
}
|
|
1647
|
-
async subscribeEvent(channel, handler) {
|
|
1648
|
-
const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
1649
|
-
set.add(handler);
|
|
1650
|
-
this.subscribers.set(channel, set);
|
|
1651
|
-
return async () => {
|
|
1652
|
-
const current = this.subscribers.get(channel);
|
|
1653
|
-
if (!current) return;
|
|
1654
|
-
current.delete(handler);
|
|
1655
|
-
if (current.size === 0) this.subscribers.delete(channel);
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
async shutdown() {
|
|
1659
|
-
for (const worker of this.workers.values()) {
|
|
1660
|
-
worker.closed = true;
|
|
1661
|
-
if (worker.pollingTimer) {
|
|
1662
|
-
clearInterval(worker.pollingTimer);
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
this.workers.clear();
|
|
1666
|
-
this.subscribers.clear();
|
|
1667
|
-
this.db.close();
|
|
1668
|
-
}
|
|
1669
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1670
|
-
// Private helpers
|
|
1671
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1672
|
-
rowToSearchResult(row) {
|
|
1673
|
-
return {
|
|
1674
|
-
id: row.id,
|
|
1675
|
-
name: row.name,
|
|
1676
|
-
queue: row.queue,
|
|
1677
|
-
status: row.status,
|
|
1678
|
-
input: JSON.parse(row.input),
|
|
1679
|
-
result: row.result ? JSON.parse(row.result) : void 0,
|
|
1680
|
-
error: row.error ?? void 0,
|
|
1681
|
-
progress: row.progress,
|
|
1682
|
-
attemptsMade: row.attempts_made,
|
|
1683
|
-
priority: row.priority,
|
|
1684
|
-
createdAt: new Date(row.created_at),
|
|
1685
|
-
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1686
|
-
completedAt: row.completed_at ? new Date(row.completed_at) : void 0,
|
|
1687
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
1688
|
-
scope: row.scope ? JSON.parse(row.scope) : void 0
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
toWorkerHandle(worker) {
|
|
1692
|
-
return {
|
|
1693
|
-
id: worker.id,
|
|
1694
|
-
queues: worker.queues,
|
|
1695
|
-
pause: async () => {
|
|
1696
|
-
worker.paused = true;
|
|
1697
|
-
},
|
|
1698
|
-
resume: async () => {
|
|
1699
|
-
worker.paused = false;
|
|
1700
|
-
},
|
|
1701
|
-
close: async () => {
|
|
1702
|
-
worker.closed = true;
|
|
1703
|
-
if (worker.pollingTimer) {
|
|
1704
|
-
clearInterval(worker.pollingTimer);
|
|
1705
|
-
}
|
|
1706
|
-
},
|
|
1707
|
-
isRunning: () => !worker.closed && !worker.paused,
|
|
1708
|
-
isPaused: () => worker.paused,
|
|
1709
|
-
isClosed: () => worker.closed,
|
|
1710
|
-
getMetrics: async () => this.toWorkerMetrics(worker)
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
toWorkerMetrics(worker) {
|
|
1714
|
-
const uptime = Date.now() - worker.startedAt.getTime();
|
|
1715
|
-
const processed = worker.metrics.processed;
|
|
1716
|
-
return {
|
|
1717
|
-
processed,
|
|
1718
|
-
failed: worker.metrics.failed,
|
|
1719
|
-
avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
|
|
1720
|
-
concurrency: worker.concurrency,
|
|
1721
|
-
uptime
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
startPollingLoop(worker) {
|
|
1725
|
-
const poll = () => {
|
|
1726
|
-
if (worker.closed || worker.paused) return;
|
|
1727
|
-
void this.processNextJobs(worker);
|
|
1728
|
-
};
|
|
1729
|
-
poll();
|
|
1730
|
-
worker.pollingTimer = setInterval(poll, this.options.pollingInterval);
|
|
1731
|
-
}
|
|
1732
|
-
async processNextJobs(worker) {
|
|
1733
|
-
if (worker.closed || worker.paused) return;
|
|
1734
|
-
const availableSlots = worker.concurrency - worker.activeJobs;
|
|
1735
|
-
if (availableSlots <= 0) return;
|
|
1736
|
-
const queueFilter = worker.queues.length > 0 ? `queue IN (${worker.queues.map(() => "?").join(", ")})` : "1=1";
|
|
1737
|
-
const params = worker.queues.length > 0 ? [...worker.queues, availableSlots] : [availableSlots];
|
|
1738
|
-
const rows = this.db.prepare(`
|
|
1739
|
-
SELECT * FROM jobs
|
|
1740
|
-
WHERE status = 'waiting' AND ${queueFilter}
|
|
1741
|
-
ORDER BY priority DESC, created_at ASC
|
|
1742
|
-
LIMIT ?
|
|
1743
|
-
`).all(...params);
|
|
1744
|
-
for (const row of rows) {
|
|
1745
|
-
if (worker.closed || worker.paused) break;
|
|
1746
|
-
if (worker.activeJobs >= worker.concurrency) break;
|
|
1747
|
-
const claimed = this.db.prepare(`
|
|
1748
|
-
UPDATE jobs SET status = 'active', started_at = ?
|
|
1749
|
-
WHERE id = ? AND status = 'waiting'
|
|
1750
|
-
`).run((/* @__PURE__ */ new Date()).toISOString(), row.id);
|
|
1751
|
-
if (claimed.changes === 0) continue;
|
|
1752
|
-
worker.activeJobs++;
|
|
1753
|
-
void this.processJob(worker, row.id).finally(() => {
|
|
1754
|
-
worker.activeJobs--;
|
|
1755
|
-
if (worker.handlers?.onIdle && worker.activeJobs === 0) {
|
|
1756
|
-
void worker.handlers.onIdle();
|
|
1757
|
-
}
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
async processJob(worker, jobId) {
|
|
1762
|
-
const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(jobId);
|
|
1763
|
-
if (!row) return;
|
|
1764
|
-
const job = this.rowToSearchResult(row);
|
|
1765
|
-
this.addJobLog(jobId, "info", "Job started");
|
|
1766
|
-
if (worker.handlers?.onActive) {
|
|
1767
|
-
await worker.handlers.onActive({ job });
|
|
1768
|
-
}
|
|
1769
|
-
const start = Date.now();
|
|
1770
|
-
try {
|
|
1771
|
-
const definition = this.registeredJobs.get(row.queue)?.get(row.name);
|
|
1772
|
-
if (!definition) {
|
|
1773
|
-
throw new IgniterJobsError({
|
|
1774
|
-
code: "JOBS_NOT_REGISTERED",
|
|
1775
|
-
message: `Job "${row.name}" is not registered for queue "${row.queue}".`
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
this.db.prepare("UPDATE jobs SET attempts_made = attempts_made + 1 WHERE id = ?").run(jobId);
|
|
1779
|
-
if (definition.onStart) {
|
|
1780
|
-
await definition.onStart({
|
|
1781
|
-
input: job.input,
|
|
1782
|
-
context: {},
|
|
1783
|
-
job: {
|
|
1784
|
-
id: job.id,
|
|
1785
|
-
name: job.name,
|
|
1786
|
-
queue: job.queue,
|
|
1787
|
-
attemptsMade: job.attemptsMade + 1,
|
|
1788
|
-
metadata: job.metadata
|
|
1789
|
-
},
|
|
1790
|
-
scope: job.scope,
|
|
1791
|
-
startedAt: /* @__PURE__ */ new Date()
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
const result = await definition.handler({
|
|
1795
|
-
input: job.input,
|
|
1796
|
-
context: {},
|
|
1797
|
-
job: {
|
|
1798
|
-
id: job.id,
|
|
1799
|
-
name: job.name,
|
|
1800
|
-
queue: job.queue,
|
|
1801
|
-
attemptsMade: job.attemptsMade + 1,
|
|
1802
|
-
metadata: job.metadata
|
|
1803
|
-
},
|
|
1804
|
-
scope: job.scope
|
|
1805
|
-
});
|
|
1806
|
-
const duration = Date.now() - start;
|
|
1807
|
-
this.db.prepare(`
|
|
1808
|
-
UPDATE jobs SET status = 'completed', completed_at = ?, result = ?, progress = 100
|
|
1809
|
-
WHERE id = ?
|
|
1810
|
-
`).run((/* @__PURE__ */ new Date()).toISOString(), JSON.stringify(result), jobId);
|
|
1811
|
-
this.addJobLog(jobId, "info", `Job completed in ${duration}ms`);
|
|
1812
|
-
worker.metrics.processed++;
|
|
1813
|
-
worker.metrics.totalDuration += duration;
|
|
1814
|
-
if (definition.onSuccess) {
|
|
1815
|
-
await definition.onSuccess({
|
|
1816
|
-
input: job.input,
|
|
1817
|
-
context: {},
|
|
1818
|
-
job: {
|
|
1819
|
-
id: job.id,
|
|
1820
|
-
name: job.name,
|
|
1821
|
-
queue: job.queue,
|
|
1822
|
-
attemptsMade: job.attemptsMade + 1,
|
|
1823
|
-
metadata: job.metadata
|
|
1824
|
-
},
|
|
1825
|
-
scope: job.scope,
|
|
1826
|
-
result,
|
|
1827
|
-
duration
|
|
1828
|
-
});
|
|
1829
|
-
}
|
|
1830
|
-
if (worker.handlers?.onSuccess) {
|
|
1831
|
-
const updatedJob = await this.getJob(jobId);
|
|
1832
|
-
if (updatedJob) {
|
|
1833
|
-
await worker.handlers.onSuccess({ job: updatedJob, result });
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
const errorMessage = error?.message ?? String(error);
|
|
1838
|
-
this.addJobLog(jobId, "error", errorMessage);
|
|
1839
|
-
const current = this.db.prepare(
|
|
1840
|
-
"SELECT attempts_made, max_attempts FROM jobs WHERE id = ?"
|
|
1841
|
-
).get(jobId);
|
|
1842
|
-
const isFinalAttempt = (current?.attempts_made ?? 0) >= (current?.max_attempts ?? 1);
|
|
1843
|
-
if (isFinalAttempt) {
|
|
1844
|
-
this.db.prepare(`
|
|
1845
|
-
UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
|
|
1846
|
-
WHERE id = ?
|
|
1847
|
-
`).run(errorMessage, (/* @__PURE__ */ new Date()).toISOString(), jobId);
|
|
1848
|
-
worker.metrics.failed++;
|
|
1849
|
-
const definition = this.registeredJobs.get(row.queue)?.get(row.name);
|
|
1850
|
-
if (definition?.onFailure) {
|
|
1851
|
-
await definition.onFailure({
|
|
1852
|
-
input: job.input,
|
|
1853
|
-
context: {},
|
|
1854
|
-
job: {
|
|
1855
|
-
id: job.id,
|
|
1856
|
-
name: job.name,
|
|
1857
|
-
queue: job.queue,
|
|
1858
|
-
attemptsMade: current?.attempts_made ?? 1,
|
|
1859
|
-
metadata: job.metadata
|
|
1860
|
-
},
|
|
1861
|
-
scope: job.scope,
|
|
1862
|
-
error,
|
|
1863
|
-
isFinalAttempt: true
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
if (worker.handlers?.onFailure) {
|
|
1867
|
-
const updatedJob = await this.getJob(jobId);
|
|
1868
|
-
if (updatedJob) {
|
|
1869
|
-
await worker.handlers.onFailure({ job: updatedJob, error });
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
} else {
|
|
1873
|
-
this.db.prepare("UPDATE jobs SET status = 'waiting' WHERE id = ?").run(jobId);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
addJobLog(jobId, level, message) {
|
|
1878
|
-
this.db.prepare(`
|
|
1879
|
-
INSERT INTO job_logs (job_id, timestamp, level, message)
|
|
1880
|
-
VALUES (?, ?, ?, ?)
|
|
1881
|
-
`).run(jobId, (/* @__PURE__ */ new Date()).toISOString(), level, message);
|
|
1882
|
-
}
|
|
1883
|
-
};
|
|
1884
|
-
|
|
1885
|
-
export { IgniterJobsBullMQAdapter, IgniterJobsMemoryAdapter, IgniterJobsSQLiteAdapter };
|
|
1886
|
-
//# sourceMappingURL=index.mjs.map
|
|
1887
|
-
//# sourceMappingURL=index.mjs.map
|