@igniter-js/jobs 0.1.13 → 0.1.15
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 +119 -243
- package/README.md +352 -158
- package/dist/{adapter-CXZxomI9.d.mts → adapter-DDyMVche.d.mts} +125 -20
- package/dist/{adapter-CXZxomI9.d.ts → adapter-DDyMVche.d.ts} +125 -20
- package/dist/adapters/bun.d.mts +101 -0
- package/dist/adapters/bun.d.ts +101 -0
- package/dist/adapters/bun.js +1048 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/bun.mjs +1046 -0
- package/dist/adapters/bun.mjs.map +1 -0
- package/dist/adapters/{memory.adapter.d.ts → mock.d.mts} +7 -3
- package/dist/adapters/{memory.adapter.d.mts → mock.d.ts} +7 -3
- package/dist/adapters/{memory.adapter.js → mock.js} +122 -25
- package/dist/adapters/mock.js.map +1 -0
- package/dist/adapters/{memory.adapter.mjs → mock.mjs} +122 -25
- package/dist/adapters/mock.mjs.map +1 -0
- package/dist/adapters/{bullmq.adapter.d.mts → node.d.mts} +8 -3
- package/dist/adapters/{bullmq.adapter.d.ts → node.d.ts} +8 -3
- package/dist/adapters/{bullmq.adapter.js → node.js} +194 -40
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/{bullmq.adapter.mjs → node.mjs} +194 -40
- package/dist/adapters/node.mjs.map +1 -0
- package/dist/index.d.mts +41 -38
- package/dist/index.d.ts +41 -38
- package/dist/index.js +145 -1856
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +146 -1854
- package/dist/index.mjs.map +1 -1
- package/package.json +29 -41
- 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
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('fs/promises');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var common = require('@igniter-js/common');
|
|
6
|
+
|
|
7
|
+
// src/adapters/bun-sqlite.adapter.ts
|
|
8
|
+
|
|
9
|
+
// src/utils/id-generator.ts
|
|
10
|
+
var IgniterJobsIdGenerator = class {
|
|
11
|
+
/**
|
|
12
|
+
* Generates a unique identifier with a prefix.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const jobId = IgniterJobsIdGenerator.generate('job')
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
static generate(prefix) {
|
|
20
|
+
const now = Date.now().toString(36);
|
|
21
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
22
|
+
return `${prefix}_${now}_${random}`;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var IgniterJobsError = class extends common.IgniterError {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super(options);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/adapters/bun-sqlite.adapter.ts
|
|
32
|
+
var IgniterJobsBunSQLiteAdapter = class _IgniterJobsBunSQLiteAdapter {
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.queues = {
|
|
35
|
+
list: async () => this.listQueues(),
|
|
36
|
+
get: async (name) => this.getQueueInfo(name),
|
|
37
|
+
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
38
|
+
getJobs: async (name, filter) => this.searchJobs({
|
|
39
|
+
queue: name,
|
|
40
|
+
status: filter?.status,
|
|
41
|
+
limit: filter?.limit,
|
|
42
|
+
offset: filter?.offset
|
|
43
|
+
}),
|
|
44
|
+
pause: async (name) => this.pauseQueue(name),
|
|
45
|
+
resume: async (name) => this.resumeQueue(name),
|
|
46
|
+
isPaused: async (name) => (await this.getQueueInfo(name))?.isPaused ?? false,
|
|
47
|
+
drain: async (name) => this.drainQueue(name),
|
|
48
|
+
clean: async (name, options) => this.cleanQueue(name, options),
|
|
49
|
+
obliterate: async (name, options) => this.obliterateQueue(name, options)
|
|
50
|
+
};
|
|
51
|
+
this.bunqueueModulePromise = null;
|
|
52
|
+
this.queueRuntimes = /* @__PURE__ */ new Map();
|
|
53
|
+
this.workers = /* @__PURE__ */ new Map();
|
|
54
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
55
|
+
this.streamSubscribers = /* @__PURE__ */ new Map();
|
|
56
|
+
this.registeredJobs = /* @__PURE__ */ new Map();
|
|
57
|
+
this.streamDatabasePromise = null;
|
|
58
|
+
this.registeredCrons = /* @__PURE__ */ new Map();
|
|
59
|
+
this.options = {
|
|
60
|
+
path: options.path,
|
|
61
|
+
durable: options.durable ?? false,
|
|
62
|
+
heartbeatInterval: options.heartbeatInterval ?? 1e4,
|
|
63
|
+
pollTimeout: options.pollTimeout ?? 0,
|
|
64
|
+
batchSize: options.batchSize ?? 10,
|
|
65
|
+
lockDuration: options.lockDuration ?? 3e4,
|
|
66
|
+
maxStalledCount: options.maxStalledCount ?? 1
|
|
67
|
+
};
|
|
68
|
+
this.client = {
|
|
69
|
+
type: "bun-sqlite",
|
|
70
|
+
path: this.options.path
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
static create(options) {
|
|
74
|
+
return new _IgniterJobsBunSQLiteAdapter(options);
|
|
75
|
+
}
|
|
76
|
+
registerJob(queueName, jobName, definition) {
|
|
77
|
+
const jobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
if (jobs.has(jobName)) {
|
|
79
|
+
throw new IgniterJobsError({
|
|
80
|
+
code: "JOBS_DUPLICATE_JOB",
|
|
81
|
+
message: `Job "${jobName}" already registered for queue "${queueName}".`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
jobs.set(jobName, definition);
|
|
85
|
+
this.registeredJobs.set(queueName, jobs);
|
|
86
|
+
this.markQueueCronsDirty(queueName);
|
|
87
|
+
}
|
|
88
|
+
registerCron(queueName, cronName, definition) {
|
|
89
|
+
const crons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
90
|
+
if (crons.has(cronName)) {
|
|
91
|
+
throw new IgniterJobsError({
|
|
92
|
+
code: "JOBS_INVALID_CRON",
|
|
93
|
+
message: `Cron "${cronName}" already registered for queue "${queueName}".`
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
crons.set(cronName, definition);
|
|
97
|
+
this.registeredCrons.set(queueName, crons);
|
|
98
|
+
this.markQueueCronsDirty(queueName);
|
|
99
|
+
}
|
|
100
|
+
async dispatch(params) {
|
|
101
|
+
const queue = await this.getQueue(params.queue);
|
|
102
|
+
const job = await queue.add(
|
|
103
|
+
params.jobName,
|
|
104
|
+
this.createEnvelope(params.input, params.metadata, params.scope),
|
|
105
|
+
this.toJobOptions(params)
|
|
106
|
+
);
|
|
107
|
+
return job.id;
|
|
108
|
+
}
|
|
109
|
+
async schedule(params) {
|
|
110
|
+
const queue = await this.getQueue(params.queue);
|
|
111
|
+
if (params.at) {
|
|
112
|
+
const delay = params.at.getTime() - Date.now();
|
|
113
|
+
if (delay <= 0) {
|
|
114
|
+
throw new IgniterJobsError({
|
|
115
|
+
code: "JOBS_INVALID_SCHEDULE",
|
|
116
|
+
message: "Scheduled time must be in the future."
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const job2 = await queue.add(
|
|
120
|
+
params.jobName,
|
|
121
|
+
this.createEnvelope(params.input, params.metadata, params.scope),
|
|
122
|
+
this.toJobOptions({ ...params, delay })
|
|
123
|
+
);
|
|
124
|
+
return job2.id;
|
|
125
|
+
}
|
|
126
|
+
const scheduleEnvelope = this.createEnvelope(
|
|
127
|
+
params.input,
|
|
128
|
+
params.metadata,
|
|
129
|
+
params.scope,
|
|
130
|
+
params
|
|
131
|
+
);
|
|
132
|
+
if (params.cron || params.every) {
|
|
133
|
+
const job2 = await queue.add(
|
|
134
|
+
params.jobName,
|
|
135
|
+
scheduleEnvelope,
|
|
136
|
+
this.toJobOptions(params, {
|
|
137
|
+
repeat: {
|
|
138
|
+
pattern: params.cron,
|
|
139
|
+
every: params.every,
|
|
140
|
+
limit: params.maxExecutions,
|
|
141
|
+
tz: params.tz
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
return job2.id;
|
|
146
|
+
}
|
|
147
|
+
const job = await queue.add(
|
|
148
|
+
params.jobName,
|
|
149
|
+
scheduleEnvelope,
|
|
150
|
+
this.toJobOptions(params)
|
|
151
|
+
);
|
|
152
|
+
return job.id;
|
|
153
|
+
}
|
|
154
|
+
async getJob(jobId, queue) {
|
|
155
|
+
if (queue) {
|
|
156
|
+
const job = await this.getQueue(queue).then((q) => q.getJob(jobId));
|
|
157
|
+
return job ? this.mapJob(job, queue) : null;
|
|
158
|
+
}
|
|
159
|
+
for (const queueName of await this.getAllQueueNames()) {
|
|
160
|
+
const job = await this.getQueue(queueName).then((q) => q.getJob(jobId));
|
|
161
|
+
if (job) return this.mapJob(job, queueName);
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
async getJobState(jobId, queue) {
|
|
166
|
+
const job = await this.getJob(jobId, queue);
|
|
167
|
+
return job?.status ?? null;
|
|
168
|
+
}
|
|
169
|
+
async getJobLogs(jobId, queue) {
|
|
170
|
+
const queueName = queue ?? await this.findQueueByJobId(jobId);
|
|
171
|
+
if (!queueName) return [];
|
|
172
|
+
const logs = await this.getQueue(queueName).then(
|
|
173
|
+
(q) => q.getJobLogs(jobId)
|
|
174
|
+
);
|
|
175
|
+
return logs.logs.map((entry) => this.parseLogEntry(entry));
|
|
176
|
+
}
|
|
177
|
+
async getJobProgress(jobId, queue) {
|
|
178
|
+
const job = await this.getJob(jobId, queue);
|
|
179
|
+
return job?.progress ?? 0;
|
|
180
|
+
}
|
|
181
|
+
async retryJob(jobId, queue) {
|
|
182
|
+
const job = await this.requireJob(jobId, queue);
|
|
183
|
+
await job.retry();
|
|
184
|
+
}
|
|
185
|
+
async removeJob(jobId, queue) {
|
|
186
|
+
const job = await this.requireJob(jobId, queue);
|
|
187
|
+
await job.remove();
|
|
188
|
+
}
|
|
189
|
+
async promoteJob(jobId, queue) {
|
|
190
|
+
const job = await this.requireJob(jobId, queue);
|
|
191
|
+
await job.promote();
|
|
192
|
+
}
|
|
193
|
+
async moveJobToFailed(jobId, reason, queue) {
|
|
194
|
+
const queueName = queue ?? await this.findQueueByJobId(jobId);
|
|
195
|
+
if (!queueName) {
|
|
196
|
+
throw new IgniterJobsError({
|
|
197
|
+
code: "JOBS_NOT_FOUND",
|
|
198
|
+
message: `Job "${jobId}" not found.`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
await this.getQueue(queueName).then(
|
|
202
|
+
(q) => q.moveJobToFailed(jobId, new Error(reason))
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
async retryManyJobs(jobIds, queue) {
|
|
206
|
+
await Promise.all(jobIds.map((jobId) => this.retryJob(jobId, queue)));
|
|
207
|
+
}
|
|
208
|
+
async removeManyJobs(jobIds, queue) {
|
|
209
|
+
await Promise.all(jobIds.map((jobId) => this.removeJob(jobId, queue)));
|
|
210
|
+
}
|
|
211
|
+
async getQueueInfo(queue) {
|
|
212
|
+
const counts = await this.getQueueJobCounts(queue);
|
|
213
|
+
return {
|
|
214
|
+
name: queue,
|
|
215
|
+
isPaused: await this.getQueue(queue).then((q) => q.isPausedAsync()),
|
|
216
|
+
jobCounts: counts
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async getQueueJobCounts(queue) {
|
|
220
|
+
const counts = await this.getQueue(queue).then(
|
|
221
|
+
(q) => q.getJobCountsAsync()
|
|
222
|
+
);
|
|
223
|
+
return {
|
|
224
|
+
waiting: counts.waiting + counts.prioritized,
|
|
225
|
+
active: counts.active,
|
|
226
|
+
completed: counts.completed,
|
|
227
|
+
failed: counts.failed,
|
|
228
|
+
delayed: counts.delayed,
|
|
229
|
+
paused: counts.paused
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async listQueues() {
|
|
233
|
+
const queueNames = await this.getAllQueueNames();
|
|
234
|
+
return Promise.all(
|
|
235
|
+
queueNames.map((queueName) => this.getQueueInfo(queueName))
|
|
236
|
+
).then((queues) => queues.filter(Boolean));
|
|
237
|
+
}
|
|
238
|
+
async pauseQueue(queue) {
|
|
239
|
+
const queueClient = await this.getQueue(queue);
|
|
240
|
+
queueClient.pause();
|
|
241
|
+
for (const worker of this.workers.values()) {
|
|
242
|
+
if (worker.workers.has(queue)) {
|
|
243
|
+
worker.paused = true;
|
|
244
|
+
worker.workers.get(queue)?.pause();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async resumeQueue(queue) {
|
|
249
|
+
const queueClient = await this.getQueue(queue);
|
|
250
|
+
queueClient.resume();
|
|
251
|
+
for (const worker of this.workers.values()) {
|
|
252
|
+
if (worker.workers.has(queue)) {
|
|
253
|
+
worker.paused = false;
|
|
254
|
+
worker.workers.get(queue)?.resume();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async drainQueue(queue) {
|
|
259
|
+
const queueClient = await this.getQueue(queue);
|
|
260
|
+
const before = await this.getQueueJobCounts(queue);
|
|
261
|
+
queueClient.drain();
|
|
262
|
+
const after = await this.getQueueJobCounts(queue);
|
|
263
|
+
return Math.max(
|
|
264
|
+
0,
|
|
265
|
+
before.waiting + before.delayed + before.paused - after.waiting - after.delayed - after.paused
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
async cleanQueue(queue, options) {
|
|
269
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
270
|
+
let cleaned = 0;
|
|
271
|
+
const queueClient = await this.getQueue(queue);
|
|
272
|
+
for (const status of statuses) {
|
|
273
|
+
const removed = await queueClient.cleanAsync(
|
|
274
|
+
options.olderThan ?? 0,
|
|
275
|
+
options.limit ?? 1e3,
|
|
276
|
+
this.toBunqueueStatus(status)
|
|
277
|
+
);
|
|
278
|
+
cleaned += removed.length;
|
|
279
|
+
}
|
|
280
|
+
return cleaned;
|
|
281
|
+
}
|
|
282
|
+
async obliterateQueue(queue, _options) {
|
|
283
|
+
const queueClient = await this.getQueue(queue);
|
|
284
|
+
queueClient.obliterate();
|
|
285
|
+
this.registeredJobs.delete(queue);
|
|
286
|
+
this.registeredCrons.delete(queue);
|
|
287
|
+
this.queueRuntimes.delete(queue);
|
|
288
|
+
}
|
|
289
|
+
async retryAllInQueue(queue) {
|
|
290
|
+
const queueClient = await this.getQueue(queue);
|
|
291
|
+
const failedJobs = await queueClient.getFailedAsync(0, 1e4);
|
|
292
|
+
await queueClient.retryJobs({
|
|
293
|
+
state: "failed",
|
|
294
|
+
count: failedJobs.length || 1e4
|
|
295
|
+
});
|
|
296
|
+
return failedJobs.length;
|
|
297
|
+
}
|
|
298
|
+
async searchJobs(filter) {
|
|
299
|
+
const queueName = filter.queue;
|
|
300
|
+
const statuses = filter.status;
|
|
301
|
+
const limit = filter.limit ?? 100;
|
|
302
|
+
const offset = filter.offset ?? 0;
|
|
303
|
+
const queueNames = queueName ? [queueName] : await this.getAllQueueNames();
|
|
304
|
+
const mappedStates = statuses?.flatMap(
|
|
305
|
+
(status) => this.toBunqueueStates(status)
|
|
306
|
+
);
|
|
307
|
+
const results = [];
|
|
308
|
+
for (const currentQueue of queueNames) {
|
|
309
|
+
const queueClient = await this.getQueue(currentQueue);
|
|
310
|
+
const jobs = await queueClient.getJobsAsync({
|
|
311
|
+
state: mappedStates,
|
|
312
|
+
start: 0,
|
|
313
|
+
end: offset + limit - 1
|
|
314
|
+
});
|
|
315
|
+
for (const job of jobs) {
|
|
316
|
+
results.push(
|
|
317
|
+
await this.mapJob(job, currentQueue)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return results.slice(offset, offset + limit);
|
|
322
|
+
}
|
|
323
|
+
async searchQueues(filter) {
|
|
324
|
+
const queues = await this.listQueues();
|
|
325
|
+
const name = filter.name;
|
|
326
|
+
const isPaused = filter.isPaused;
|
|
327
|
+
return queues.filter((queue) => name ? queue.name.includes(name) : true).filter(
|
|
328
|
+
(queue) => typeof isPaused === "boolean" ? queue.isPaused === isPaused : true
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
async searchWorkers(filter) {
|
|
332
|
+
const queue = filter.queue;
|
|
333
|
+
const isRunning = filter.isRunning;
|
|
334
|
+
return Array.from(this.workers.values()).filter((worker) => queue ? worker.queues.includes(queue) : true).filter(
|
|
335
|
+
(worker) => typeof isRunning === "boolean" ? isRunning ? !worker.closed && !worker.paused : worker.closed || worker.paused : true
|
|
336
|
+
).map((worker) => this.toWorkerHandle(worker));
|
|
337
|
+
}
|
|
338
|
+
async createWorker(config) {
|
|
339
|
+
const bunqueue = await this.loadBunqueue();
|
|
340
|
+
const queueNames = config.queues.length ? config.queues : await this.getAllQueueNames();
|
|
341
|
+
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
342
|
+
const workerState = {
|
|
343
|
+
id: workerId,
|
|
344
|
+
queues: [...queueNames],
|
|
345
|
+
concurrency: config.concurrency ?? 1,
|
|
346
|
+
paused: false,
|
|
347
|
+
closed: false,
|
|
348
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
349
|
+
activeJobs: /* @__PURE__ */ new Set(),
|
|
350
|
+
metrics: { processed: 0, failed: 0, totalDuration: 0 },
|
|
351
|
+
workers: /* @__PURE__ */ new Map(),
|
|
352
|
+
handlers: config.handlers
|
|
353
|
+
};
|
|
354
|
+
const perQueueConcurrency = this.distributeConcurrency(
|
|
355
|
+
workerState.concurrency,
|
|
356
|
+
queueNames
|
|
357
|
+
);
|
|
358
|
+
for (const queueName of queueNames) {
|
|
359
|
+
await this.getQueue(queueName);
|
|
360
|
+
await this.syncCronSchedulers(queueName);
|
|
361
|
+
const worker = new bunqueue.Worker(
|
|
362
|
+
queueName,
|
|
363
|
+
async (job) => this.processJob(workerState, queueName, job),
|
|
364
|
+
{
|
|
365
|
+
embedded: true,
|
|
366
|
+
dataPath: this.options.path,
|
|
367
|
+
autorun: false,
|
|
368
|
+
concurrency: perQueueConcurrency.get(queueName) ?? 1,
|
|
369
|
+
heartbeatInterval: this.options.heartbeatInterval,
|
|
370
|
+
batchSize: this.options.batchSize,
|
|
371
|
+
pollTimeout: this.options.pollTimeout,
|
|
372
|
+
lockDuration: this.options.lockDuration,
|
|
373
|
+
maxStalledCount: this.options.maxStalledCount,
|
|
374
|
+
limiter: config.limiter ? { max: config.limiter.max, duration: config.limiter.duration } : void 0
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
this.attachWorkerEvents(workerState, queueName, worker);
|
|
378
|
+
worker.run();
|
|
379
|
+
workerState.workers.set(queueName, worker);
|
|
380
|
+
}
|
|
381
|
+
this.workers.set(workerId, workerState);
|
|
382
|
+
return this.toWorkerHandle(workerState);
|
|
383
|
+
}
|
|
384
|
+
getWorkers() {
|
|
385
|
+
return new Map(
|
|
386
|
+
Array.from(this.workers.entries()).map(([id, worker]) => [
|
|
387
|
+
id,
|
|
388
|
+
this.toWorkerHandle(worker)
|
|
389
|
+
])
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
async publishEvent(channel, payload) {
|
|
393
|
+
const handlers = this.subscribers.get(channel);
|
|
394
|
+
if (!handlers) return;
|
|
395
|
+
await Promise.all(
|
|
396
|
+
Array.from(handlers).map(async (handler) => handler(payload))
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
async subscribeEvent(channel, handler) {
|
|
400
|
+
const handlers = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
401
|
+
handlers.add(handler);
|
|
402
|
+
this.subscribers.set(channel, handlers);
|
|
403
|
+
return async () => {
|
|
404
|
+
const current = this.subscribers.get(channel);
|
|
405
|
+
if (!current) return;
|
|
406
|
+
current.delete(handler);
|
|
407
|
+
if (current.size === 0) this.subscribers.delete(channel);
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async writeJobStreamEvent(params) {
|
|
411
|
+
const key = this.getStreamKey(params.queue, params.jobId);
|
|
412
|
+
let eventId = String(Date.now());
|
|
413
|
+
if (params.persistence?.enabled) {
|
|
414
|
+
const db = await this.getStreamDatabase();
|
|
415
|
+
const result = db.prepare(
|
|
416
|
+
`
|
|
417
|
+
INSERT INTO igniter_jobs_stream_events (
|
|
418
|
+
queue, job_id, job_name, type, data, timestamp, scope
|
|
419
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
420
|
+
`
|
|
421
|
+
).run(
|
|
422
|
+
params.queue,
|
|
423
|
+
params.jobId,
|
|
424
|
+
params.jobName,
|
|
425
|
+
params.event.type,
|
|
426
|
+
JSON.stringify(params.event.data),
|
|
427
|
+
params.event.timestamp.toISOString(),
|
|
428
|
+
params.scope ? JSON.stringify(params.scope) : null
|
|
429
|
+
);
|
|
430
|
+
eventId = String(result.lastInsertRowid);
|
|
431
|
+
const maxEvents = params.persistence.maxEvents;
|
|
432
|
+
if (typeof maxEvents === "number" && maxEvents > 0) {
|
|
433
|
+
db.prepare(
|
|
434
|
+
`
|
|
435
|
+
DELETE FROM igniter_jobs_stream_events
|
|
436
|
+
WHERE id IN (
|
|
437
|
+
SELECT id FROM igniter_jobs_stream_events
|
|
438
|
+
WHERE queue = ? AND job_id = ?
|
|
439
|
+
ORDER BY id DESC
|
|
440
|
+
LIMIT -1 OFFSET ?
|
|
441
|
+
)
|
|
442
|
+
`
|
|
443
|
+
).run(params.queue, params.jobId, maxEvents);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const event = {
|
|
447
|
+
...params.event,
|
|
448
|
+
id: eventId
|
|
449
|
+
};
|
|
450
|
+
const handlers = this.streamSubscribers.get(key);
|
|
451
|
+
if (handlers?.size) {
|
|
452
|
+
await Promise.all(
|
|
453
|
+
Array.from(handlers).map(async (handler) => handler(event))
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return eventId;
|
|
457
|
+
}
|
|
458
|
+
async readJobStream(params) {
|
|
459
|
+
const db = await this.getStreamDatabase();
|
|
460
|
+
const after = params.after ? Number(params.after) : 0;
|
|
461
|
+
const limit = params.limit ?? 100;
|
|
462
|
+
const rows = db.prepare(
|
|
463
|
+
`
|
|
464
|
+
SELECT id, type, data, timestamp, job_id, job_name, queue, scope
|
|
465
|
+
FROM igniter_jobs_stream_events
|
|
466
|
+
WHERE queue = ? AND job_id = ? AND id > ?
|
|
467
|
+
ORDER BY id ASC
|
|
468
|
+
LIMIT ?
|
|
469
|
+
`
|
|
470
|
+
).all(params.queue, params.jobId, after, limit + 1);
|
|
471
|
+
const hasMore = rows.length > limit;
|
|
472
|
+
const items = rows.slice(0, limit).map((row) => this.mapStreamRow(row));
|
|
473
|
+
return {
|
|
474
|
+
items,
|
|
475
|
+
nextCursor: items.at(-1)?.id,
|
|
476
|
+
hasMore
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async subscribeJobStream(params) {
|
|
480
|
+
const key = this.getStreamKey(params.queue, params.jobId);
|
|
481
|
+
const set = this.streamSubscribers.get(key) ?? /* @__PURE__ */ new Set();
|
|
482
|
+
set.add(params.handler);
|
|
483
|
+
this.streamSubscribers.set(key, set);
|
|
484
|
+
return async () => {
|
|
485
|
+
const current = this.streamSubscribers.get(key);
|
|
486
|
+
if (!current) return;
|
|
487
|
+
current.delete(params.handler);
|
|
488
|
+
if (current.size === 0) this.streamSubscribers.delete(key);
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async shutdown() {
|
|
492
|
+
for (const worker of this.workers.values()) {
|
|
493
|
+
await this.closeWorkerState(worker);
|
|
494
|
+
}
|
|
495
|
+
this.workers.clear();
|
|
496
|
+
for (const runtime of this.queueRuntimes.values()) {
|
|
497
|
+
runtime.queue.close();
|
|
498
|
+
}
|
|
499
|
+
this.queueRuntimes.clear();
|
|
500
|
+
this.subscribers.clear();
|
|
501
|
+
this.streamSubscribers.clear();
|
|
502
|
+
if (this.streamDatabasePromise) {
|
|
503
|
+
const db = await this.streamDatabasePromise;
|
|
504
|
+
db.close();
|
|
505
|
+
this.streamDatabasePromise = null;
|
|
506
|
+
}
|
|
507
|
+
if (this.bunqueueModulePromise) {
|
|
508
|
+
const bunqueue = await this.bunqueueModulePromise;
|
|
509
|
+
bunqueue.shutdownManager();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async loadBunqueue() {
|
|
513
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
514
|
+
throw new IgniterJobsError({
|
|
515
|
+
code: "JOBS_ADAPTER_ERROR",
|
|
516
|
+
message: "The Bun SQLite adapter can only run inside Bun. Use the BullMQ adapter in Node.js runtimes."
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
this.bunqueueModulePromise ?? (this.bunqueueModulePromise = import('bunqueue/client'));
|
|
520
|
+
return this.bunqueueModulePromise;
|
|
521
|
+
}
|
|
522
|
+
async getStreamDatabase() {
|
|
523
|
+
this.streamDatabasePromise ?? (this.streamDatabasePromise = (async () => {
|
|
524
|
+
await promises.mkdir(path.dirname(this.options.path), { recursive: true });
|
|
525
|
+
const sqlite = await import('bun:sqlite');
|
|
526
|
+
const db = new sqlite.Database(this.options.path, {
|
|
527
|
+
create: true
|
|
528
|
+
});
|
|
529
|
+
db.exec(`
|
|
530
|
+
CREATE TABLE IF NOT EXISTS igniter_jobs_stream_events (
|
|
531
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
532
|
+
queue TEXT NOT NULL,
|
|
533
|
+
job_id TEXT NOT NULL,
|
|
534
|
+
job_name TEXT NOT NULL,
|
|
535
|
+
type TEXT NOT NULL,
|
|
536
|
+
data TEXT NOT NULL,
|
|
537
|
+
timestamp TEXT NOT NULL,
|
|
538
|
+
scope TEXT
|
|
539
|
+
);
|
|
540
|
+
CREATE INDEX IF NOT EXISTS idx_igniter_jobs_stream_events_job
|
|
541
|
+
ON igniter_jobs_stream_events(queue, job_id, id);
|
|
542
|
+
`);
|
|
543
|
+
return db;
|
|
544
|
+
})());
|
|
545
|
+
return this.streamDatabasePromise;
|
|
546
|
+
}
|
|
547
|
+
async getQueue(queueName) {
|
|
548
|
+
const existing = this.queueRuntimes.get(queueName);
|
|
549
|
+
if (existing) return existing.queue;
|
|
550
|
+
const bunqueue = await this.loadBunqueue();
|
|
551
|
+
await promises.mkdir(path.dirname(this.options.path), { recursive: true });
|
|
552
|
+
const queue = new bunqueue.Queue(queueName, {
|
|
553
|
+
embedded: true,
|
|
554
|
+
dataPath: this.options.path,
|
|
555
|
+
defaultJobOptions: {
|
|
556
|
+
durable: this.options.durable
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
await queue.waitUntilReady();
|
|
560
|
+
this.queueRuntimes.set(queueName, { queue, cronsSynced: false });
|
|
561
|
+
return queue;
|
|
562
|
+
}
|
|
563
|
+
async getAllQueueNames() {
|
|
564
|
+
return Array.from(
|
|
565
|
+
/* @__PURE__ */ new Set([
|
|
566
|
+
...this.registeredJobs.keys(),
|
|
567
|
+
...this.registeredCrons.keys(),
|
|
568
|
+
...this.queueRuntimes.keys()
|
|
569
|
+
])
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
markQueueCronsDirty(queueName) {
|
|
573
|
+
const runtime = this.queueRuntimes.get(queueName);
|
|
574
|
+
if (runtime) runtime.cronsSynced = false;
|
|
575
|
+
}
|
|
576
|
+
async syncCronSchedulers(queueName) {
|
|
577
|
+
const runtime = this.queueRuntimes.get(queueName);
|
|
578
|
+
if (!runtime || runtime.cronsSynced) return;
|
|
579
|
+
const cronDefinitions = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
580
|
+
const existing = await runtime.queue.getJobSchedulers(0, 1e4);
|
|
581
|
+
const existingIds = new Set(existing.map((scheduler) => scheduler.id));
|
|
582
|
+
for (const [cronName, definition] of cronDefinitions.entries()) {
|
|
583
|
+
await runtime.queue.upsertJobScheduler(
|
|
584
|
+
cronName,
|
|
585
|
+
{
|
|
586
|
+
pattern: definition.cron,
|
|
587
|
+
limit: definition.maxExecutions,
|
|
588
|
+
timezone: definition.tz
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: cronName,
|
|
592
|
+
data: this.createEnvelope(
|
|
593
|
+
void 0,
|
|
594
|
+
void 0,
|
|
595
|
+
void 0,
|
|
596
|
+
definition
|
|
597
|
+
),
|
|
598
|
+
opts: { durable: this.options.durable }
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
existingIds.delete(cronName);
|
|
602
|
+
}
|
|
603
|
+
for (const schedulerId of existingIds) {
|
|
604
|
+
await runtime.queue.removeJobScheduler(schedulerId);
|
|
605
|
+
}
|
|
606
|
+
runtime.cronsSynced = true;
|
|
607
|
+
}
|
|
608
|
+
createEnvelope(input, metadata, scope, scheduleSource) {
|
|
609
|
+
return {
|
|
610
|
+
__igniterJobs: true,
|
|
611
|
+
input,
|
|
612
|
+
metadata,
|
|
613
|
+
scope,
|
|
614
|
+
schedule: scheduleSource ? {
|
|
615
|
+
skipWeekends: scheduleSource.skipWeekends,
|
|
616
|
+
onlyBusinessHours: scheduleSource.onlyBusinessHours,
|
|
617
|
+
businessHours: scheduleSource.businessHours,
|
|
618
|
+
onlyWeekdays: scheduleSource.onlyWeekdays,
|
|
619
|
+
skipDates: scheduleSource.skipDates?.map(
|
|
620
|
+
(value) => value instanceof Date ? value.toISOString() : value
|
|
621
|
+
)
|
|
622
|
+
} : void 0
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
toJobOptions(params, overrides) {
|
|
626
|
+
return {
|
|
627
|
+
jobId: params.jobId,
|
|
628
|
+
priority: params.priority,
|
|
629
|
+
delay: params.delay,
|
|
630
|
+
attempts: params.attempts,
|
|
631
|
+
removeOnComplete: params.removeOnComplete,
|
|
632
|
+
removeOnFail: params.removeOnFail,
|
|
633
|
+
durable: this.options.durable,
|
|
634
|
+
...overrides
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
async processJob(workerState, queueName, job) {
|
|
638
|
+
const envelope = this.normalizeEnvelope(job.data);
|
|
639
|
+
if (!this.shouldExecuteScheduledJob(envelope.schedule, /* @__PURE__ */ new Date())) {
|
|
640
|
+
await this.writeLog(
|
|
641
|
+
queueName,
|
|
642
|
+
job.id,
|
|
643
|
+
"info",
|
|
644
|
+
"Scheduled run skipped by adapter rules."
|
|
645
|
+
);
|
|
646
|
+
return { skipped: true };
|
|
647
|
+
}
|
|
648
|
+
const registeredJob = this.registeredJobs.get(queueName)?.get(job.name);
|
|
649
|
+
if (registeredJob) {
|
|
650
|
+
return this.processRegisteredDefinition(
|
|
651
|
+
workerState,
|
|
652
|
+
queueName,
|
|
653
|
+
job,
|
|
654
|
+
envelope,
|
|
655
|
+
registeredJob
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const registeredCron = this.registeredCrons.get(queueName)?.get(job.name);
|
|
659
|
+
if (registeredCron) {
|
|
660
|
+
return this.processRegisteredCron(
|
|
661
|
+
queueName,
|
|
662
|
+
job,
|
|
663
|
+
envelope,
|
|
664
|
+
registeredCron
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
throw new IgniterJobsError({
|
|
668
|
+
code: "JOBS_NOT_REGISTERED",
|
|
669
|
+
message: `Job "${job.name}" is not registered for queue "${queueName}".`
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async processRegisteredDefinition(workerState, queueName, job, envelope, definition) {
|
|
673
|
+
const baseContext = this.createExecutionContext(
|
|
674
|
+
queueName,
|
|
675
|
+
job,
|
|
676
|
+
envelope,
|
|
677
|
+
definition
|
|
678
|
+
);
|
|
679
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
680
|
+
await this.writeLog(queueName, job.id, "info", "Job started.");
|
|
681
|
+
await this.invokeHook(
|
|
682
|
+
() => definition.onStart?.({ ...baseContext, startedAt })
|
|
683
|
+
);
|
|
684
|
+
try {
|
|
685
|
+
const result = await definition.handler(baseContext);
|
|
686
|
+
const duration = Date.now() - startedAt.getTime();
|
|
687
|
+
workerState.metrics.processed += 1;
|
|
688
|
+
workerState.metrics.totalDuration += duration;
|
|
689
|
+
await this.writeLog(
|
|
690
|
+
queueName,
|
|
691
|
+
job.id,
|
|
692
|
+
"info",
|
|
693
|
+
`Job completed in ${duration}ms.`
|
|
694
|
+
);
|
|
695
|
+
await this.invokeHook(
|
|
696
|
+
() => definition.onSuccess?.({
|
|
697
|
+
...baseContext,
|
|
698
|
+
startedAt,
|
|
699
|
+
duration,
|
|
700
|
+
result
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
return result;
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const runtimeError = error instanceof Error ? error : new Error(String(error));
|
|
706
|
+
const duration = Date.now() - startedAt.getTime();
|
|
707
|
+
const maxAttempts = definition.attempts ?? job.opts.attempts ?? 1;
|
|
708
|
+
const isFinalAttempt = job.attemptsMade >= maxAttempts;
|
|
709
|
+
workerState.metrics.failed += 1;
|
|
710
|
+
await this.writeLog(queueName, job.id, "error", runtimeError.message);
|
|
711
|
+
await this.invokeHook(
|
|
712
|
+
() => definition.onFailure?.({
|
|
713
|
+
...baseContext,
|
|
714
|
+
startedAt,
|
|
715
|
+
duration,
|
|
716
|
+
error: runtimeError,
|
|
717
|
+
isFinalAttempt
|
|
718
|
+
})
|
|
719
|
+
);
|
|
720
|
+
throw runtimeError;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async processRegisteredCron(queueName, job, envelope, definition) {
|
|
724
|
+
await this.writeLog(queueName, job.id, "info", "Cron execution started.");
|
|
725
|
+
try {
|
|
726
|
+
const result = await definition.handler({
|
|
727
|
+
context: {},
|
|
728
|
+
job: {
|
|
729
|
+
id: job.id,
|
|
730
|
+
name: job.name,
|
|
731
|
+
queue: queueName,
|
|
732
|
+
attemptsMade: job.attemptsMade,
|
|
733
|
+
createdAt: new Date(job.timestamp),
|
|
734
|
+
metadata: envelope.metadata
|
|
735
|
+
},
|
|
736
|
+
scope: envelope.scope
|
|
737
|
+
});
|
|
738
|
+
await this.writeLog(
|
|
739
|
+
queueName,
|
|
740
|
+
job.id,
|
|
741
|
+
"info",
|
|
742
|
+
"Cron execution completed."
|
|
743
|
+
);
|
|
744
|
+
return result;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
const runtimeError = error instanceof Error ? error : new Error(String(error));
|
|
747
|
+
await this.writeLog(queueName, job.id, "error", runtimeError.message);
|
|
748
|
+
throw runtimeError;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
createExecutionContext(queueName, job, envelope, definition) {
|
|
752
|
+
const baseContext = {
|
|
753
|
+
input: envelope.input,
|
|
754
|
+
context: {},
|
|
755
|
+
job: {
|
|
756
|
+
id: job.id,
|
|
757
|
+
name: job.name,
|
|
758
|
+
queue: queueName,
|
|
759
|
+
attemptsMade: job.attemptsMade,
|
|
760
|
+
createdAt: new Date(job.timestamp),
|
|
761
|
+
metadata: envelope.metadata,
|
|
762
|
+
updateProgress: async (progress, message) => {
|
|
763
|
+
await job.updateProgress(progress, message);
|
|
764
|
+
await this.writeLog(
|
|
765
|
+
queueName,
|
|
766
|
+
job.id,
|
|
767
|
+
"info",
|
|
768
|
+
`Progress updated to ${progress}%.`
|
|
769
|
+
);
|
|
770
|
+
await this.invokeHook(
|
|
771
|
+
() => definition.onProgress?.({
|
|
772
|
+
...baseContext,
|
|
773
|
+
progress,
|
|
774
|
+
message
|
|
775
|
+
})
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
scope: envelope.scope
|
|
780
|
+
};
|
|
781
|
+
return baseContext;
|
|
782
|
+
}
|
|
783
|
+
attachWorkerEvents(workerState, queueName, worker) {
|
|
784
|
+
worker.on("active", async (job) => {
|
|
785
|
+
workerState.activeJobs.add(job.id);
|
|
786
|
+
await this.invokeHandler(
|
|
787
|
+
() => workerState.handlers?.onActive?.({
|
|
788
|
+
job: this.mapJobSync(job, queueName)
|
|
789
|
+
})
|
|
790
|
+
);
|
|
791
|
+
});
|
|
792
|
+
worker.on("completed", async (job, result) => {
|
|
793
|
+
workerState.activeJobs.delete(job.id);
|
|
794
|
+
await this.invokeHandler(
|
|
795
|
+
() => workerState.handlers?.onSuccess?.({
|
|
796
|
+
job: this.mapJobSync(job, queueName, "completed"),
|
|
797
|
+
result
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
});
|
|
801
|
+
worker.on("failed", async (job, error) => {
|
|
802
|
+
workerState.activeJobs.delete(job.id);
|
|
803
|
+
await this.invokeHandler(
|
|
804
|
+
() => workerState.handlers?.onFailure?.({
|
|
805
|
+
job: this.mapJobSync(job, queueName, "failed"),
|
|
806
|
+
error
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
worker.on("drained", async () => {
|
|
811
|
+
if (workerState.activeJobs.size === 0) {
|
|
812
|
+
await this.invokeHandler(() => workerState.handlers?.onIdle?.());
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
worker.on("error", async (error) => {
|
|
816
|
+
await this.writeQueueLog(
|
|
817
|
+
queueName,
|
|
818
|
+
"error",
|
|
819
|
+
`Worker error: ${error.message}`
|
|
820
|
+
);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
toWorkerHandle(worker) {
|
|
824
|
+
return {
|
|
825
|
+
id: worker.id,
|
|
826
|
+
queues: [...worker.queues],
|
|
827
|
+
pause: async () => {
|
|
828
|
+
worker.paused = true;
|
|
829
|
+
for (const runtime of worker.workers.values()) runtime.pause();
|
|
830
|
+
},
|
|
831
|
+
resume: async () => {
|
|
832
|
+
worker.paused = false;
|
|
833
|
+
for (const runtime of worker.workers.values()) runtime.resume();
|
|
834
|
+
},
|
|
835
|
+
close: async () => {
|
|
836
|
+
await this.closeWorkerState(worker);
|
|
837
|
+
},
|
|
838
|
+
isRunning: () => !worker.closed && !worker.paused,
|
|
839
|
+
isPaused: () => worker.paused,
|
|
840
|
+
isClosed: () => worker.closed,
|
|
841
|
+
getMetrics: async () => this.buildWorkerMetrics(worker)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
buildWorkerMetrics(worker) {
|
|
845
|
+
return {
|
|
846
|
+
processed: worker.metrics.processed,
|
|
847
|
+
failed: worker.metrics.failed,
|
|
848
|
+
avgDuration: worker.metrics.processed > 0 ? worker.metrics.totalDuration / worker.metrics.processed : 0,
|
|
849
|
+
concurrency: worker.concurrency,
|
|
850
|
+
uptime: Date.now() - worker.startedAt.getTime()
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
async closeWorkerState(worker) {
|
|
854
|
+
worker.closed = true;
|
|
855
|
+
await Promise.all(
|
|
856
|
+
Array.from(worker.workers.values()).map((runtime) => runtime.close())
|
|
857
|
+
);
|
|
858
|
+
worker.workers.clear();
|
|
859
|
+
}
|
|
860
|
+
async requireJob(jobId, queue) {
|
|
861
|
+
const queueName = queue ?? await this.findQueueByJobId(jobId);
|
|
862
|
+
if (!queueName) {
|
|
863
|
+
throw new IgniterJobsError({
|
|
864
|
+
code: "JOBS_NOT_FOUND",
|
|
865
|
+
message: `Job "${jobId}" not found.`
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
const job = await this.getQueue(queueName).then(
|
|
869
|
+
(queueClient) => queueClient.getJob(jobId)
|
|
870
|
+
);
|
|
871
|
+
if (!job) {
|
|
872
|
+
throw new IgniterJobsError({
|
|
873
|
+
code: "JOBS_NOT_FOUND",
|
|
874
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
return job;
|
|
878
|
+
}
|
|
879
|
+
async findQueueByJobId(jobId) {
|
|
880
|
+
for (const queueName of await this.getAllQueueNames()) {
|
|
881
|
+
const job = await this.getQueue(queueName).then(
|
|
882
|
+
(queueClient) => queueClient.getJob(jobId)
|
|
883
|
+
);
|
|
884
|
+
if (job) return queueName;
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
getStreamKey(queue, jobId) {
|
|
889
|
+
return `${queue}:${jobId}`;
|
|
890
|
+
}
|
|
891
|
+
mapStreamRow(row) {
|
|
892
|
+
return {
|
|
893
|
+
id: String(row.id),
|
|
894
|
+
type: row.type,
|
|
895
|
+
data: JSON.parse(row.data),
|
|
896
|
+
timestamp: new Date(row.timestamp),
|
|
897
|
+
jobId: row.job_id,
|
|
898
|
+
jobName: row.job_name,
|
|
899
|
+
queue: row.queue,
|
|
900
|
+
scope: row.scope ? JSON.parse(row.scope) : void 0
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
async mapJob(job, queueName, forcedState) {
|
|
904
|
+
const queue = await this.getQueue(queueName);
|
|
905
|
+
const rawState = await job.getState();
|
|
906
|
+
return this.mapJobSync(
|
|
907
|
+
job,
|
|
908
|
+
queueName,
|
|
909
|
+
forcedState ?? this.toPublicStatus(rawState, queue.isPaused())
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
mapJobSync(job, queueName, status) {
|
|
913
|
+
const envelope = this.normalizeEnvelope(job.data);
|
|
914
|
+
return {
|
|
915
|
+
id: job.id,
|
|
916
|
+
name: job.name,
|
|
917
|
+
queue: queueName,
|
|
918
|
+
status: status ?? this.toPublicStatus("waiting", false),
|
|
919
|
+
input: envelope.input,
|
|
920
|
+
result: job.returnvalue,
|
|
921
|
+
error: job.failedReason ?? void 0,
|
|
922
|
+
progress: typeof job.progress === "number" ? job.progress : 0,
|
|
923
|
+
attemptsMade: job.attemptsMade,
|
|
924
|
+
priority: job.priority ?? 0,
|
|
925
|
+
createdAt: new Date(job.timestamp),
|
|
926
|
+
startedAt: job.processedOn ? new Date(job.processedOn) : void 0,
|
|
927
|
+
completedAt: job.finishedOn ? new Date(job.finishedOn) : void 0,
|
|
928
|
+
metadata: envelope.metadata,
|
|
929
|
+
scope: envelope.scope
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
normalizeEnvelope(data) {
|
|
933
|
+
if (data && typeof data === "object" && data.__igniterJobs === true) {
|
|
934
|
+
return data;
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
__igniterJobs: true,
|
|
938
|
+
input: data
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
toPublicStatus(rawState, isQueuePaused) {
|
|
942
|
+
if ((rawState === "waiting" || rawState === "prioritized") && isQueuePaused) {
|
|
943
|
+
return "paused";
|
|
944
|
+
}
|
|
945
|
+
switch (rawState) {
|
|
946
|
+
case "prioritized":
|
|
947
|
+
case "waiting-children":
|
|
948
|
+
return "waiting";
|
|
949
|
+
case "waiting":
|
|
950
|
+
case "active":
|
|
951
|
+
case "completed":
|
|
952
|
+
case "failed":
|
|
953
|
+
case "delayed":
|
|
954
|
+
case "paused":
|
|
955
|
+
return rawState;
|
|
956
|
+
default:
|
|
957
|
+
return "waiting";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
toBunqueueStatus(status) {
|
|
961
|
+
switch (status) {
|
|
962
|
+
case "waiting":
|
|
963
|
+
return "waiting";
|
|
964
|
+
default:
|
|
965
|
+
return status;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
toBunqueueStates(status) {
|
|
969
|
+
switch (status) {
|
|
970
|
+
case "waiting":
|
|
971
|
+
return ["waiting", "prioritized"];
|
|
972
|
+
case "paused":
|
|
973
|
+
return ["paused"];
|
|
974
|
+
default:
|
|
975
|
+
return [status];
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
distributeConcurrency(total, queues) {
|
|
979
|
+
const result = /* @__PURE__ */ new Map();
|
|
980
|
+
if (queues.length === 0) return result;
|
|
981
|
+
const base = Math.max(1, Math.floor(total / queues.length));
|
|
982
|
+
let remainder = Math.max(0, total - base * queues.length);
|
|
983
|
+
for (const queue of queues) {
|
|
984
|
+
const value = base + (remainder > 0 ? 1 : 0);
|
|
985
|
+
result.set(queue, value);
|
|
986
|
+
remainder = Math.max(0, remainder - 1);
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
shouldExecuteScheduledJob(schedule, now) {
|
|
991
|
+
if (!schedule) return true;
|
|
992
|
+
const day = now.getDay();
|
|
993
|
+
const isoDate = now.toISOString().slice(0, 10);
|
|
994
|
+
if (schedule.skipWeekends && (day === 0 || day === 6)) return false;
|
|
995
|
+
if (schedule.onlyWeekdays?.length && !schedule.onlyWeekdays.includes(day))
|
|
996
|
+
return false;
|
|
997
|
+
if (schedule.skipDates?.some((value) => value.slice(0, 10) === isoDate))
|
|
998
|
+
return false;
|
|
999
|
+
if (schedule.onlyBusinessHours && schedule.businessHours) {
|
|
1000
|
+
const hour = now.getHours();
|
|
1001
|
+
return hour >= schedule.businessHours.start && hour < schedule.businessHours.end;
|
|
1002
|
+
}
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
parseLogEntry(entry) {
|
|
1006
|
+
try {
|
|
1007
|
+
const parsed = JSON.parse(entry);
|
|
1008
|
+
if (parsed && typeof parsed.message === "string" && typeof parsed.timestamp === "string" && (parsed.level === "info" || parsed.level === "warn" || parsed.level === "error")) {
|
|
1009
|
+
return {
|
|
1010
|
+
timestamp: new Date(parsed.timestamp),
|
|
1011
|
+
level: parsed.level,
|
|
1012
|
+
message: parsed.message
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
} catch {
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1019
|
+
level: "info",
|
|
1020
|
+
message: entry
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
async writeLog(queueName, jobId, level, message) {
|
|
1024
|
+
const queue = await this.getQueue(queueName);
|
|
1025
|
+
await queue.addJobLog(
|
|
1026
|
+
jobId,
|
|
1027
|
+
JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), level, message })
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
async writeQueueLog(queueName, level, message) {
|
|
1031
|
+
}
|
|
1032
|
+
async invokeHook(fn) {
|
|
1033
|
+
try {
|
|
1034
|
+
await fn();
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async invokeHandler(fn) {
|
|
1039
|
+
try {
|
|
1040
|
+
await fn();
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
exports.IgniterJobsBunSQLiteAdapter = IgniterJobsBunSQLiteAdapter;
|
|
1047
|
+
//# sourceMappingURL=bun.js.map
|
|
1048
|
+
//# sourceMappingURL=bun.js.map
|