@igniter-js/jobs 0.1.0 → 0.1.1

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.
@@ -1,1129 +1,1058 @@
1
1
  'use strict';
2
2
 
3
+ var adapterBullmq = require('@igniter-js/adapter-bullmq');
3
4
  var core = require('@igniter-js/core');
4
5
 
5
- // src/errors/igniter-jobs.error.ts
6
- var IgniterJobsError = class _IgniterJobsError extends core.IgniterError {
7
- constructor(options) {
8
- super({
9
- code: options.code,
10
- message: options.message,
11
- statusCode: options.statusCode ?? 500,
12
- causer: "@igniter-js/jobs",
13
- cause: options.cause,
14
- details: options.details,
15
- logger: options.logger
16
- });
17
- this.code = options.code;
18
- this.details = options.details;
19
- this.name = "IgniterJobsError";
20
- if (Error.captureStackTrace) {
21
- Error.captureStackTrace(this, _IgniterJobsError);
22
- }
6
+ // src/adapters/bullmq.adapter.ts
7
+
8
+ // src/utils/prefix.ts
9
+ var _IgniterJobsPrefix = class _IgniterJobsPrefix {
10
+ /**
11
+ * Builds a normalized queue name using the global prefix and queue id.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const name = IgniterJobsPrefix.buildQueueName('email')
16
+ * // -> igniter:jobs:email
17
+ * ```
18
+ */
19
+ static buildQueueName(queue) {
20
+ return `${_IgniterJobsPrefix.BASE_PREFIX}:${queue}`;
23
21
  }
24
22
  /**
25
- * Convert error to a plain object for serialization.
23
+ * Builds the event channel used for pub/sub.
24
+ *
25
+ * Unscoped events are published to a global channel per service/environment.
26
+ * Scoped events are also published to an additional channel for that scope.
26
27
  */
27
- toJSON() {
28
- return {
29
- name: this.name,
30
- code: this.code,
31
- message: this.message,
32
- statusCode: this.statusCode,
33
- details: this.details,
34
- stack: this.stack
35
- };
28
+ static buildEventsChannel(params) {
29
+ const base = `${_IgniterJobsPrefix.BASE_PREFIX}:events:${params.environment}:${params.service}`;
30
+ if (!params.scope) return base;
31
+ return `${base}:scope:${params.scope.type}:${params.scope.id}`;
32
+ }
33
+ };
34
+ _IgniterJobsPrefix.BASE_PREFIX = "igniter:jobs";
35
+ var IgniterJobsPrefix = _IgniterJobsPrefix;
36
+ var IgniterJobsError = class extends core.IgniterError {
37
+ constructor(options) {
38
+ super(options);
36
39
  }
37
40
  };
38
41
 
39
42
  // src/adapters/bullmq.adapter.ts
40
- var BullMQAdapter = class _BullMQAdapter {
43
+ function toDateArray(values) {
44
+ if (!values) return void 0;
45
+ return values.map((v) => v instanceof Date ? v : new Date(v));
46
+ }
47
+ var IgniterJobsBullMQAdapter = class _IgniterJobsBullMQAdapter {
41
48
  constructor(options) {
42
- this.queues = /* @__PURE__ */ new Map();
43
- this.workers = /* @__PURE__ */ new Map();
44
- this.queueEvents = /* @__PURE__ */ new Map();
45
- this.BullMQ = null;
49
+ this.subscribers = /* @__PURE__ */ new Map();
50
+ this.coreAdapter = null;
51
+ this.coreExecutor = null;
52
+ this.executorDirty = true;
53
+ this.jobsByQueue = /* @__PURE__ */ new Map();
54
+ this.cronsByQueue = /* @__PURE__ */ new Map();
46
55
  this.redis = options.redis;
56
+ this.publisher = this.redis;
57
+ this.subscriber = this.redis.duplicate();
58
+ this.client = { redis: this.redis };
59
+ this.subscriber.on("message", (channel, message) => {
60
+ const set = this.subscribers.get(channel);
61
+ if (!set || set.size === 0) return;
62
+ let payload = message;
63
+ try {
64
+ payload = JSON.parse(message);
65
+ } catch {
66
+ }
67
+ for (const handler of set) handler(payload);
68
+ });
69
+ this.queues = {
70
+ list: async () => this.listQueues(),
71
+ get: async (name) => this.getQueueInfo(name),
72
+ getJobCounts: async (name) => this.getQueueJobCounts(name),
73
+ getJobs: async (name, filter) => {
74
+ const full = this.toCoreQueueName(name);
75
+ return this.core().queues.getJobs(full, filter);
76
+ },
77
+ pause: async (name) => this.pauseQueue(name),
78
+ resume: async (name) => this.resumeQueue(name),
79
+ isPaused: async (name) => {
80
+ const full = this.toCoreQueueName(name);
81
+ return this.core().queues.isPaused(full);
82
+ },
83
+ drain: async (name) => this.drainQueue(name),
84
+ clean: async (name, options2) => this.cleanQueue(name, options2),
85
+ obliterate: async (name, options2) => this.obliterateQueue(name, options2)
86
+ };
47
87
  }
48
- /**
49
- * Create a new BullMQ adapter.
50
- *
51
- * @param options - Adapter options with Redis connection
52
- * @returns A new BullMQAdapter instance
53
- */
54
88
  static create(options) {
55
- return new _BullMQAdapter(options);
56
- }
57
- /**
58
- * Get the underlying Redis client.
59
- */
60
- get client() {
61
- return this.redis;
89
+ return new _IgniterJobsBullMQAdapter(options);
62
90
  }
63
- /**
64
- * Lazily load BullMQ module.
65
- */
66
- async getBullMQ() {
67
- if (!this.BullMQ) {
68
- this.BullMQ = await import('bullmq');
69
- }
70
- return this.BullMQ;
71
- }
72
- /**
73
- * Get or create a queue instance.
74
- */
75
- async getOrCreateQueue(name) {
76
- if (!this.queues.has(name)) {
77
- const { Queue } = await this.getBullMQ();
78
- const queue = new Queue(name, {
79
- connection: this.redis
91
+ registerJob(queueName, jobName, definition) {
92
+ const map = this.jobsByQueue.get(queueName) ?? /* @__PURE__ */ new Map();
93
+ if (map.has(jobName)) {
94
+ throw new IgniterJobsError({
95
+ code: "JOBS_DUPLICATE_JOB",
96
+ message: `Job "${jobName}" is already registered in queue "${queueName}".`
80
97
  });
81
- this.queues.set(name, queue);
82
98
  }
83
- return this.queues.get(name);
99
+ map.set(jobName, definition);
100
+ this.jobsByQueue.set(queueName, map);
101
+ this.executorDirty = true;
84
102
  }
85
- /**
86
- * Get or create queue events instance.
87
- */
88
- async getQueueEvents(name) {
89
- if (!this.queueEvents.has(name)) {
90
- const { QueueEvents } = await this.getBullMQ();
91
- const events = new QueueEvents(name, {
92
- connection: this.redis
103
+ registerCron(queueName, cronName, definition) {
104
+ const map = this.cronsByQueue.get(queueName) ?? /* @__PURE__ */ new Map();
105
+ if (map.has(cronName)) {
106
+ throw new IgniterJobsError({
107
+ code: "JOBS_INVALID_CRON",
108
+ message: `Cron "${cronName}" is already registered in queue "${queueName}".`
93
109
  });
94
- this.queueEvents.set(name, events);
95
110
  }
96
- return this.queueEvents.get(name);
111
+ map.set(cronName, definition);
112
+ this.cronsByQueue.set(queueName, map);
113
+ this.executorDirty = true;
97
114
  }
98
- /**
99
- * Convert BullMQ job state to IgniterJobStatus.
100
- */
101
- mapJobState(state) {
102
- const stateMap = {
103
- waiting: "waiting",
104
- active: "active",
105
- completed: "completed",
106
- failed: "failed",
107
- delayed: "delayed",
108
- paused: "paused",
109
- "waiting-children": "waiting"
110
- };
111
- return stateMap[state] || "waiting";
112
- }
113
- /**
114
- * Convert BullMQ job to IgniterJobInfo.
115
- */
116
- async mapJobToInfo(job) {
117
- const data = job.data;
118
- const state = await job.getState();
119
- return {
120
- id: job.id,
121
- name: job.name,
122
- queue: job.queueName,
123
- state: this.mapJobState(state || "waiting"),
124
- data: data.input ?? data,
125
- result: job.returnvalue,
126
- error: job.failedReason,
127
- progress: typeof job.progress === "number" ? job.progress : 0,
128
- attempts: job.attemptsMade,
129
- timestamp: job.timestamp,
130
- processedOn: job.processedOn,
131
- finishedOn: job.finishedOn,
132
- delay: job.delay,
133
- priority: job.opts?.priority,
134
- scope: data.scope,
135
- actor: data.actor,
136
- metadata: data.metadata
137
- };
138
- }
139
- // ==========================================
140
- // JOB OPERATIONS
141
- // ==========================================
142
115
  async dispatch(params) {
143
- const queue = await this.getOrCreateQueue(params.queue);
144
- const jobOptions = {
116
+ const executor = await this.executor();
117
+ const namespace = executor[params.queue];
118
+ if (!namespace) {
119
+ throw new IgniterJobsError({
120
+ code: "JOBS_QUEUE_NOT_FOUND",
121
+ message: `Queue "${params.queue}" is not registered in the adapter.`
122
+ });
123
+ }
124
+ return namespace.enqueue({
125
+ task: params.jobName,
126
+ input: params.input,
145
127
  jobId: params.jobId,
146
128
  delay: params.delay,
147
129
  priority: params.priority,
148
- attempts: params.attempts ?? 3,
149
- backoff: params.backoff ? {
150
- type: params.backoff.type === "custom" ? "fixed" : params.backoff.type,
151
- delay: params.backoff.type === "custom" ? params.backoff.delays[0] : params.backoff.delay
152
- } : void 0,
130
+ attempts: params.attempts,
131
+ metadata: params.metadata,
153
132
  removeOnComplete: params.removeOnComplete,
154
- removeOnFail: params.removeOnFail
155
- };
156
- const jobData = {
157
- input: params.data,
158
- scope: params.scope,
159
- actor: params.actor
160
- };
161
- const job = await queue.add(params.name, jobData, jobOptions);
162
- return job.id;
133
+ removeOnFail: params.removeOnFail,
134
+ limiter: params.limiter
135
+ });
163
136
  }
164
137
  async schedule(params) {
165
- const queue = await this.getOrCreateQueue(params.queue);
166
- const jobOptions = {
138
+ const executor = await this.executor();
139
+ const namespace = executor[params.queue];
140
+ if (!namespace) {
141
+ throw new IgniterJobsError({
142
+ code: "JOBS_QUEUE_NOT_FOUND",
143
+ message: `Queue "${params.queue}" is not registered in the adapter.`
144
+ });
145
+ }
146
+ const schedule = {
167
147
  jobId: params.jobId,
168
- delay: params.at ? params.at.getTime() - Date.now() : params.delay,
148
+ delay: params.delay,
169
149
  priority: params.priority,
170
- attempts: params.attempts ?? 3,
171
- backoff: params.backoff ? {
172
- type: params.backoff.type === "custom" ? "fixed" : params.backoff.type,
173
- delay: params.backoff.type === "custom" ? params.backoff.delays[0] : params.backoff.delay
174
- } : void 0,
150
+ attempts: params.attempts,
151
+ metadata: params.metadata,
175
152
  removeOnComplete: params.removeOnComplete,
176
153
  removeOnFail: params.removeOnFail,
177
- repeat: params.cron ? {
178
- pattern: params.cron,
179
- tz: params.timezone
180
- } : params.every ? {
181
- every: params.every
154
+ limiter: params.limiter,
155
+ at: params.at,
156
+ repeat: params.cron || params.every || params.maxExecutions || params.skipWeekends || params.onlyBusinessHours || params.businessHours || params.onlyWeekdays || params.skipDates ? {
157
+ cron: params.cron,
158
+ every: params.every,
159
+ times: params.maxExecutions,
160
+ skipWeekends: params.skipWeekends,
161
+ onlyBusinessHours: params.onlyBusinessHours,
162
+ businessHours: params.businessHours,
163
+ onlyWeekdays: params.onlyWeekdays,
164
+ skipDates: toDateArray(params.skipDates)
182
165
  } : void 0
183
166
  };
184
- const jobData = {
185
- input: params.data,
186
- scope: params.scope,
187
- actor: params.actor
188
- };
189
- const job = await queue.add(params.name, jobData, jobOptions);
190
- return job.id;
167
+ return namespace.schedule({
168
+ task: params.jobName,
169
+ input: params.input,
170
+ ...schedule
171
+ });
191
172
  }
192
- async getJob(queue, jobId) {
193
- const q = await this.getOrCreateQueue(queue);
194
- const job = await q.getJob(jobId);
195
- if (!job) return null;
196
- return await this.mapJobToInfo(job);
173
+ async getJob(jobId, queue) {
174
+ const result = await this.core().job.get(jobId, queue ? this.toCoreQueueName(queue) : void 0);
175
+ return result ? this.mapJob(result, queue) : null;
197
176
  }
198
- async getJobState(queue, jobId) {
199
- const q = await this.getOrCreateQueue(queue);
200
- const job = await q.getJob(jobId);
201
- if (!job) return null;
202
- const state = await job.getState();
203
- return this.mapJobState(state);
177
+ async getJobState(jobId, queue) {
178
+ const state = await this.core().job.getState(jobId, queue ? this.toCoreQueueName(queue) : void 0);
179
+ return state;
204
180
  }
205
- async getJobProgress(queue, jobId) {
206
- const q = await this.getOrCreateQueue(queue);
207
- const job = await q.getJob(jobId);
208
- if (!job) return 0;
209
- return typeof job.progress === "number" ? job.progress : 0;
181
+ async getJobLogs(jobId, queue) {
182
+ const logs = await this.core().job.getLogs(jobId, queue ? this.toCoreQueueName(queue) : void 0);
183
+ return logs;
210
184
  }
211
- async getJobLogs(queue, jobId) {
212
- const q = await this.getOrCreateQueue(queue);
213
- const job = await q.getJob(jobId);
214
- if (!job) return [];
215
- const { logs } = await q.getJobLogs(jobId);
216
- return logs.map((log, index) => ({
217
- timestamp: /* @__PURE__ */ new Date(),
218
- message: log,
219
- level: "info"
220
- }));
221
- }
222
- async retryJob(queue, jobId) {
223
- const q = await this.getOrCreateQueue(queue);
224
- const job = await q.getJob(jobId);
225
- if (!job) {
226
- throw new IgniterJobsError({
227
- code: "JOBS_JOB_NOT_FOUND",
228
- message: `Job "${jobId}" not found in queue "${queue}"`,
229
- statusCode: 404
230
- });
231
- }
232
- await job.retry();
185
+ async getJobProgress(jobId, queue) {
186
+ return this.core().job.getProgress(jobId, queue ? this.toCoreQueueName(queue) : void 0);
233
187
  }
234
- async removeJob(queue, jobId) {
235
- const q = await this.getOrCreateQueue(queue);
236
- const job = await q.getJob(jobId);
237
- if (!job) return;
238
- await job.remove();
188
+ async retryJob(jobId, queue) {
189
+ await this.core().job.retry(jobId, queue ? this.toCoreQueueName(queue) : void 0);
239
190
  }
240
- async promoteJob(queue, jobId) {
241
- const q = await this.getOrCreateQueue(queue);
242
- const job = await q.getJob(jobId);
243
- if (!job) {
244
- throw new IgniterJobsError({
245
- code: "JOBS_JOB_NOT_FOUND",
246
- message: `Job "${jobId}" not found in queue "${queue}"`,
247
- statusCode: 404
248
- });
249
- }
250
- await job.promote();
191
+ async removeJob(jobId, queue) {
192
+ await this.core().job.remove(jobId, queue ? this.toCoreQueueName(queue) : void 0);
251
193
  }
252
- async moveJob(queue, jobId, state, reason) {
253
- const q = await this.getOrCreateQueue(queue);
254
- const job = await q.getJob(jobId);
255
- if (!job) {
256
- throw new IgniterJobsError({
257
- code: "JOBS_JOB_NOT_FOUND",
258
- message: `Job "${jobId}" not found in queue "${queue}"`,
259
- statusCode: 404
260
- });
261
- }
262
- if (state === "failed") {
263
- await job.moveToFailed(new Error(reason || "Manually moved to failed"), "manual");
264
- } else {
265
- await job.moveToCompleted(reason || "Manually completed", "manual");
266
- }
194
+ async promoteJob(jobId, queue) {
195
+ await this.core().job.promote(jobId, queue ? this.toCoreQueueName(queue) : void 0);
267
196
  }
268
- async retryJobs(queue, jobIds) {
269
- const q = await this.getOrCreateQueue(queue);
270
- await Promise.all(
271
- jobIds.map(async (jobId) => {
272
- const job = await q.getJob(jobId);
273
- if (job) await job.retry();
274
- })
275
- );
276
- }
277
- async removeJobs(queue, jobIds) {
278
- const q = await this.getOrCreateQueue(queue);
279
- await Promise.all(
280
- jobIds.map(async (jobId) => {
281
- const job = await q.getJob(jobId);
282
- if (job) await job.remove();
283
- })
284
- );
285
- }
286
- // ==========================================
287
- // QUEUE OPERATIONS
288
- // ==========================================
289
- async getQueue(queue) {
290
- const q = await this.getOrCreateQueue(queue);
291
- const isPaused = await q.isPaused();
292
- const counts = await q.getJobCounts();
293
- return {
294
- name: queue,
295
- isPaused,
296
- jobCounts: {
297
- waiting: counts.waiting || 0,
298
- active: counts.active || 0,
299
- completed: counts.completed || 0,
300
- failed: counts.failed || 0,
301
- delayed: counts.delayed || 0,
302
- paused: counts.paused || 0
303
- }
304
- };
197
+ async moveJobToFailed(jobId, reason, queue) {
198
+ await this.core().job.moveToFailed(jobId, reason, queue ? this.toCoreQueueName(queue) : void 0);
199
+ }
200
+ async retryManyJobs(jobIds, queue) {
201
+ await this.core().job.retryMany(jobIds, queue ? this.toCoreQueueName(queue) : void 0);
202
+ }
203
+ async removeManyJobs(jobIds, queue) {
204
+ await this.core().job.removeMany(jobIds, queue ? this.toCoreQueueName(queue) : void 0);
205
+ }
206
+ async getQueueInfo(queue) {
207
+ const info = await this.core().queues.get(this.toCoreQueueName(queue));
208
+ if (!info) return null;
209
+ return this.mapQueueInfo(info);
210
+ }
211
+ async getQueueJobCounts(queue) {
212
+ const counts = await this.core().queues.getJobCounts(this.toCoreQueueName(queue));
213
+ return counts;
214
+ }
215
+ async listQueues() {
216
+ const list = await this.core().queues.list();
217
+ return list.map((q) => this.mapQueueInfo(q));
305
218
  }
306
219
  async pauseQueue(queue) {
307
- const q = await this.getOrCreateQueue(queue);
308
- await q.pause();
220
+ await this.core().queues.pause(this.toCoreQueueName(queue));
309
221
  }
310
222
  async resumeQueue(queue) {
311
- const q = await this.getOrCreateQueue(queue);
312
- await q.resume();
223
+ await this.core().queues.resume(this.toCoreQueueName(queue));
313
224
  }
314
225
  async drainQueue(queue) {
315
- const q = await this.getOrCreateQueue(queue);
316
- await q.drain();
317
- return 0;
226
+ return this.core().queues.drain(this.toCoreQueueName(queue));
318
227
  }
319
228
  async cleanQueue(queue, options) {
320
- const q = await this.getOrCreateQueue(queue);
321
- const statuses = Array.isArray(options.status) ? options.status : [options.status];
322
- let total = 0;
323
- for (const status of statuses) {
324
- const cleaned = await q.clean(
325
- options.olderThan ?? 0,
326
- options.limit ?? 1e3,
327
- status
328
- );
329
- total += cleaned.length;
330
- }
331
- return total;
229
+ return this.core().queues.clean(this.toCoreQueueName(queue), options);
332
230
  }
333
231
  async obliterateQueue(queue, options) {
334
- const q = await this.getOrCreateQueue(queue);
335
- await q.obliterate({ force: options?.force });
336
- }
337
- async retryAllFailed(queue) {
338
- const q = await this.getOrCreateQueue(queue);
339
- const failed = await q.getFailed();
340
- await Promise.all(failed.map((job) => job.retry()));
341
- return failed.length;
342
- }
343
- async getJobCounts(queue) {
344
- const q = await this.getOrCreateQueue(queue);
345
- const counts = await q.getJobCounts();
346
- return {
347
- waiting: counts.waiting || 0,
348
- active: counts.active || 0,
349
- completed: counts.completed || 0,
350
- failed: counts.failed || 0,
351
- delayed: counts.delayed || 0,
352
- paused: counts.paused || 0
353
- };
232
+ await this.core().queues.obliterate(this.toCoreQueueName(queue), options);
233
+ }
234
+ async retryAllInQueue(queue) {
235
+ const jobs = await this.core().queues.getJobs(this.toCoreQueueName(queue), { status: ["failed"], limit: 1e3 });
236
+ await Promise.all(jobs.map((j) => this.core().job.retry(j.id, this.toCoreQueueName(queue))));
237
+ return jobs.length;
354
238
  }
355
- async listJobs(queue, options) {
356
- const q = await this.getOrCreateQueue(queue);
357
- const statuses = options?.status || ["waiting", "active", "completed", "failed", "delayed"];
358
- const jobs = [];
359
- for (const status of statuses) {
360
- const statusJobs = await q.getJobs([status], options?.start, options?.end);
361
- jobs.push(...statusJobs);
362
- }
363
- return Promise.all(jobs.map(async (job) => {
364
- const data = job.data;
365
- const state = await job.getState();
366
- return {
367
- id: job.id,
368
- name: job.name,
369
- queue: job.queueName,
370
- state: this.mapJobState(state || "waiting"),
371
- data: data.input ?? data,
372
- result: job.returnvalue,
373
- error: job.failedReason,
374
- progress: typeof job.progress === "number" ? job.progress : 0,
375
- attempts: job.attemptsMade,
376
- timestamp: job.timestamp,
377
- processedOn: job.processedOn,
378
- finishedOn: job.finishedOn,
379
- scope: data.scope,
380
- actor: data.actor
381
- };
382
- }));
383
- }
384
- // ==========================================
385
- // PAUSE/RESUME JOB TYPES
386
- // ==========================================
387
239
  async pauseJobType(queue, jobName) {
388
- this.config?.logger?.warn?.(`pauseJobType is not fully supported in BullMQ adapter`);
240
+ throw new IgniterJobsError({
241
+ code: "JOBS_QUEUE_OPERATION_FAILED",
242
+ message: "BullMQ backend does not support pausing a single job type; pause the queue or adjust worker filters."
243
+ });
389
244
  }
390
245
  async resumeJobType(queue, jobName) {
391
- this.config?.logger?.warn?.(`resumeJobType is not fully supported in BullMQ adapter`);
392
- }
393
- // ==========================================
394
- // EVENTS
395
- // ==========================================
396
- async subscribe(pattern, handler) {
397
- const subscriptions = [];
398
- for (const [queueName] of this.queues) {
399
- const events = await this.getQueueEvents(queueName);
400
- const completedHandler = async (args) => {
401
- if (this.matchesPattern(pattern, `${queueName}:${args.jobId}:completed`)) {
402
- await handler({
403
- type: "completed",
404
- data: args,
405
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
406
- });
407
- }
408
- };
409
- const failedHandler = async (args) => {
410
- if (this.matchesPattern(pattern, `${queueName}:${args.jobId}:failed`)) {
411
- await handler({
412
- type: "failed",
413
- data: args,
414
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
415
- });
416
- }
417
- };
418
- events.on("completed", completedHandler);
419
- events.on("failed", failedHandler);
420
- subscriptions.push(async () => {
421
- events.off("completed", completedHandler);
422
- events.off("failed", failedHandler);
423
- });
246
+ throw new IgniterJobsError({
247
+ code: "JOBS_QUEUE_OPERATION_FAILED",
248
+ message: "BullMQ backend does not support resuming a single job type; resume the queue or adjust worker filters."
249
+ });
250
+ }
251
+ async searchJobs(filter) {
252
+ const queue = filter?.queue;
253
+ const status = filter?.status;
254
+ const limit = filter?.limit ?? 100;
255
+ const offset = filter?.offset ?? 0;
256
+ if (queue) {
257
+ const jobs = await this.core().queues.getJobs(this.toCoreQueueName(queue), { status, limit, offset });
258
+ return jobs.map((j) => this.mapJob(j, queue));
259
+ }
260
+ const queues = await this.listQueues();
261
+ const results = [];
262
+ for (const q of queues) {
263
+ const jobs = await this.core().queues.getJobs(this.toCoreQueueName(q.name), { status, limit, offset });
264
+ results.push(...jobs.map((j) => this.mapJob(j, q.name)));
265
+ if (results.length >= limit) break;
266
+ }
267
+ return results.slice(0, limit);
268
+ }
269
+ async searchQueues(filter) {
270
+ const all = await this.listQueues();
271
+ const name = filter?.name;
272
+ const isPaused = filter?.isPaused;
273
+ return all.filter((q) => name ? q.name.includes(name) : true).filter((q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true);
274
+ }
275
+ async searchWorkers(filter) {
276
+ const queue = filter?.queue;
277
+ const isRunning = filter?.isRunning;
278
+ const all = Array.from(this.core().getWorkers().values());
279
+ return all.filter((w) => {
280
+ if (!queue) return true;
281
+ const coreQueue = this.toCoreQueueName(queue);
282
+ const queues = w.config?.queues ?? [w.queueName];
283
+ return Array.isArray(queues) ? queues.includes(coreQueue) : false;
284
+ }).filter((w) => typeof isRunning === "boolean" ? isRunning ? w.isRunning() : !w.isRunning() : true).map((w) => this.mapWorker(w));
285
+ }
286
+ async createWorker(config) {
287
+ await this.executor();
288
+ const queuesSource = config.queues?.length ? config.queues : Array.from(/* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.cronsByQueue.keys()]));
289
+ const queues = queuesSource.map((q) => this.toCoreQueueName(q));
290
+ const coreConfig = {
291
+ queues,
292
+ concurrency: config.concurrency ?? 1,
293
+ limiter: config.limiter,
294
+ onActive: config.handlers?.onActive,
295
+ onSuccess: config.handlers?.onSuccess,
296
+ onFailure: config.handlers?.onFailure,
297
+ onIdle: config.handlers?.onIdle
298
+ };
299
+ const handle = await this.core().worker(coreConfig);
300
+ return this.mapWorker(handle);
301
+ }
302
+ getWorkers() {
303
+ const out = /* @__PURE__ */ new Map();
304
+ for (const [id, handle] of this.core().getWorkers()) out.set(id, this.mapWorker(handle));
305
+ return out;
306
+ }
307
+ async publishEvent(channel, payload) {
308
+ await this.publisher.publish(channel, JSON.stringify(payload));
309
+ }
310
+ async subscribeEvent(channel, handler) {
311
+ const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
312
+ const wrapped = (payload) => void handler(payload);
313
+ set.add(wrapped);
314
+ this.subscribers.set(channel, set);
315
+ if (set.size === 1) {
316
+ await this.subscriber.subscribe(channel);
424
317
  }
425
318
  return async () => {
426
- await Promise.all(subscriptions.map((unsub) => unsub()));
319
+ const current = this.subscribers.get(channel);
320
+ if (!current) return;
321
+ current.delete(wrapped);
322
+ if (current.size === 0) {
323
+ this.subscribers.delete(channel);
324
+ await this.subscriber.unsubscribe(channel);
325
+ }
427
326
  };
428
327
  }
429
- matchesPattern(pattern, eventType) {
430
- if (pattern === "*") return true;
431
- const regex = new RegExp(
432
- "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
433
- );
434
- return regex.test(eventType);
435
- }
436
- // ==========================================
437
- // WORKERS
438
- // ==========================================
439
- async createWorker(config, handler) {
440
- const { Worker } = await this.getBullMQ();
441
- const workerId = `worker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
442
- const workers = [];
443
- for (const queueName of config.queues) {
444
- const workerOptions = {
445
- connection: this.redis,
446
- concurrency: config.concurrency,
447
- lockDuration: config.lockDuration,
448
- limiter: config.limiter
449
- };
450
- const worker = new Worker(
451
- queueName,
452
- async (job) => {
453
- const data = job.data;
454
- return handler({
455
- id: job.id,
456
- name: job.name,
457
- queue: job.queueName,
458
- data: data.input ?? data,
459
- attempt: job.attemptsMade + 1,
460
- timestamp: job.timestamp,
461
- scope: data.scope,
462
- actor: data.actor,
463
- log: async (level, message) => {
464
- await job.log(`[${level.toUpperCase()}] ${message}`);
465
- },
466
- updateProgress: async (progress) => {
467
- await job.updateProgress(progress);
468
- }
469
- });
470
- },
471
- workerOptions
472
- );
473
- if (config.onIdle) {
474
- worker.on("drained", config.onIdle);
475
- }
476
- workers.push(worker);
477
- this.workers.set(`${workerId}-${queueName}`, worker);
478
- }
479
- let isPaused = false;
480
- const startTime = Date.now();
481
- let processed = 0;
482
- let failed = 0;
483
- let completed = 0;
484
- for (const worker of workers) {
485
- worker.on("completed", () => {
486
- processed++;
487
- completed++;
488
- });
489
- worker.on("failed", () => {
490
- processed++;
491
- failed++;
328
+ async shutdown() {
329
+ await this.subscriber.quit();
330
+ }
331
+ core() {
332
+ if (!this.coreAdapter) {
333
+ this.coreAdapter = adapterBullmq.createBullMQAdapter({
334
+ store: { client: this.redis }
492
335
  });
493
336
  }
494
- return {
495
- id: workerId,
496
- pause: async () => {
497
- await Promise.all(workers.map((w) => w.pause()));
498
- isPaused = true;
499
- },
500
- resume: async () => {
501
- await Promise.all(workers.map((w) => w.resume()));
502
- isPaused = false;
503
- },
504
- close: async () => {
505
- await Promise.all(workers.map((w) => w.close()));
506
- for (const worker of workers) {
507
- const key = Array.from(this.workers.entries()).find(
508
- ([_, w]) => w === worker
509
- )?.[0];
510
- if (key) this.workers.delete(key);
337
+ return this.coreAdapter;
338
+ }
339
+ async executor() {
340
+ if (!this.executorDirty && this.coreExecutor) return this.coreExecutor;
341
+ const routers = {};
342
+ const flattened = {};
343
+ const allQueues = /* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.cronsByQueue.keys()]);
344
+ for (const queueName of allQueues) {
345
+ const coreJobs = {};
346
+ const jobs = this.jobsByQueue.get(queueName);
347
+ if (jobs) {
348
+ for (const [jobName, def] of jobs.entries()) {
349
+ const queue = def.queue ? `${queueName}.${def.queue}` : queueName;
350
+ const fullQueue = IgniterJobsPrefix.buildQueueName(queue);
351
+ coreJobs[jobName] = this.toCoreJobDefinition(queueName, jobName, def, fullQueue);
511
352
  }
512
- },
513
- isRunning: () => !isPaused && workers.every((w) => w.isRunning()),
514
- isPaused: () => isPaused,
515
- getMetrics: async () => ({
516
- processed,
517
- failed,
518
- completed,
519
- active: workers.reduce((sum, w) => sum + (w.isRunning() ? 1 : 0), 0),
520
- uptime: Date.now() - startTime
521
- })
522
- };
523
- }
524
- // ==========================================
525
- // SEARCH
526
- // ==========================================
527
- async searchJobs(filter) {
528
- const results = [];
529
- const queuesToSearch = filter.queue ? [filter.queue] : Array.from(this.queues.keys());
530
- for (const queueName of queuesToSearch) {
531
- const jobs = await this.listJobs(queueName, {
532
- status: filter.status,
533
- start: filter.offset,
534
- end: filter.limit ? (filter.offset || 0) + filter.limit : void 0
353
+ }
354
+ const crons = this.cronsByQueue.get(queueName);
355
+ if (crons) {
356
+ for (const [cronName, def] of crons.entries()) {
357
+ const fullQueue = IgniterJobsPrefix.buildQueueName(queueName);
358
+ coreJobs[cronName] = this.toCoreCronJobDefinition(queueName, cronName, def, fullQueue);
359
+ }
360
+ }
361
+ if (Object.keys(coreJobs).length === 0) continue;
362
+ routers[queueName] = this.core().router({
363
+ jobs: coreJobs,
364
+ namespace: queueName
535
365
  });
536
- for (const job of jobs) {
537
- if (filter.jobName && job.name !== filter.jobName) continue;
538
- if (filter.scopeId && job.scope?.id !== filter.scopeId) continue;
539
- if (filter.actorId && job.actor?.id !== filter.actorId) continue;
540
- if (filter.dateRange?.from && job.timestamp < filter.dateRange.from.getTime()) continue;
541
- if (filter.dateRange?.to && job.timestamp > filter.dateRange.to.getTime()) continue;
542
- results.push(job);
366
+ for (const [jobName, def] of Object.entries(coreJobs)) {
367
+ flattened[`${queueName}.${jobName}`] = def;
543
368
  }
544
369
  }
545
- if (filter.orderBy) {
546
- const [field, direction] = filter.orderBy.split(":");
547
- results.sort((a, b) => {
548
- const aVal = field === "createdAt" ? a.timestamp : a[field];
549
- const bVal = field === "createdAt" ? b.timestamp : b[field];
550
- return direction === "asc" ? aVal - bVal : bVal - aVal;
370
+ await this.core().bulkRegister(flattened);
371
+ this.coreExecutor = this.core().merge(routers);
372
+ this.executorDirty = false;
373
+ return this.coreExecutor;
374
+ }
375
+ toCoreQueueName(queueName) {
376
+ return IgniterJobsPrefix.buildQueueName(queueName);
377
+ }
378
+ mapQueueInfo(info) {
379
+ return {
380
+ name: this.fromCoreQueueName(info.name),
381
+ isPaused: info.isPaused,
382
+ jobCounts: info.jobCounts
383
+ };
384
+ }
385
+ fromCoreQueueName(full) {
386
+ const prefix = `${IgniterJobsPrefix.BASE_PREFIX}:`;
387
+ return full.startsWith(prefix) ? full.slice(prefix.length) : full;
388
+ }
389
+ mapJob(job, queue) {
390
+ const q = queue ?? this.fromCoreQueueName(job.metadata?.queue ?? job.queueName ?? "");
391
+ const scope = job.metadata?.__igniter_jobs_scope;
392
+ return {
393
+ id: job.id,
394
+ name: job.name,
395
+ queue: q,
396
+ status: job.status,
397
+ input: job.payload,
398
+ result: job.result,
399
+ error: job.error,
400
+ progress: 0,
401
+ attemptsMade: job.attemptsMade ?? 0,
402
+ priority: job.priority ?? 0,
403
+ createdAt: job.createdAt,
404
+ startedAt: job.processedAt,
405
+ completedAt: job.completedAt,
406
+ metadata: job.metadata,
407
+ scope
408
+ };
409
+ }
410
+ mapWorker(handle) {
411
+ const queues = handle.config?.queues ?? [handle.queueName];
412
+ return {
413
+ id: handle.id,
414
+ queues: queues.map((q) => this.fromCoreQueueName(q)),
415
+ pause: () => handle.pause(),
416
+ resume: () => handle.resume(),
417
+ close: () => handle.close(),
418
+ isRunning: () => handle.isRunning(),
419
+ isPaused: () => handle.isPaused(),
420
+ isClosed: () => handle.isClosed(),
421
+ getMetrics: async () => handle.getMetrics()
422
+ };
423
+ }
424
+ toCoreJobDefinition(queueName, jobName, def, fullQueueName) {
425
+ const handler = async (ctx) => {
426
+ return def.handler({
427
+ input: ctx.input,
428
+ context: ctx.context,
429
+ job: {
430
+ id: ctx.job.id,
431
+ name: jobName,
432
+ queue: queueName,
433
+ attemptsMade: ctx.job.attemptsMade,
434
+ createdAt: ctx.job.createdAt,
435
+ metadata: ctx.job.metadata
436
+ },
437
+ scope: ctx.job.metadata?.__igniter_jobs_scope
551
438
  });
552
- }
553
- return results.slice(0, filter.limit || 100);
439
+ };
440
+ return {
441
+ name: jobName,
442
+ input: def.input,
443
+ handler,
444
+ queue: { name: fullQueueName },
445
+ attempts: def.attempts,
446
+ priority: def.priority,
447
+ delay: def.delay,
448
+ removeOnComplete: def.removeOnComplete,
449
+ removeOnFail: def.removeOnFail,
450
+ metadata: def.metadata,
451
+ limiter: def.limiter,
452
+ onStart: def.onStart,
453
+ onSuccess: def.onSuccess,
454
+ onFailure: def.onFailure,
455
+ onProgress: def.onProgress
456
+ };
554
457
  }
555
- async searchQueues(filter) {
556
- const results = [];
557
- for (const [queueName, queue] of this.queues) {
558
- if (filter.name && !queueName.includes(filter.name)) continue;
559
- const isPaused = await queue.isPaused();
560
- if (filter.isPaused !== void 0 && isPaused !== filter.isPaused) continue;
561
- const counts = await queue.getJobCounts();
562
- results.push({
563
- name: queueName,
564
- isPaused,
565
- jobCounts: {
566
- waiting: counts.waiting || 0,
567
- active: counts.active || 0,
568
- completed: counts.completed || 0,
569
- failed: counts.failed || 0,
570
- delayed: counts.delayed || 0,
571
- paused: counts.paused || 0
572
- }
458
+ toCoreCronJobDefinition(queueName, cronName, def, fullQueueName) {
459
+ const handler = async (ctx) => {
460
+ return def.handler({
461
+ context: ctx.context,
462
+ job: {
463
+ id: ctx.job.id,
464
+ name: cronName,
465
+ queue: queueName,
466
+ attemptsMade: ctx.job.attemptsMade,
467
+ createdAt: ctx.job.createdAt,
468
+ metadata: ctx.job.metadata
469
+ },
470
+ scope: ctx.job.metadata?.__igniter_jobs_scope
573
471
  });
574
- }
575
- return results;
472
+ };
473
+ return {
474
+ name: cronName,
475
+ handler,
476
+ queue: { name: fullQueueName },
477
+ repeat: {
478
+ cron: def.cron,
479
+ tz: def.tz,
480
+ limit: def.maxExecutions,
481
+ startDate: def.startDate,
482
+ endDate: def.endDate
483
+ },
484
+ metadata: def.onlyBusinessHours || def.skipWeekends || def.businessHours || def.onlyWeekdays || def.skipDates || def.startDate && def.endDate ? {
485
+ advancedScheduling: {
486
+ onlyBusinessHours: def.onlyBusinessHours,
487
+ skipWeekends: def.skipWeekends,
488
+ businessHours: def.businessHours,
489
+ skipDates: toDateArray(def.skipDates),
490
+ onlyWeekdays: def.onlyWeekdays,
491
+ between: def.startDate && def.endDate ? [def.startDate, def.endDate] : void 0
492
+ }
493
+ } : void 0
494
+ };
576
495
  }
577
- // ==========================================
578
- // LIFECYCLE
579
- // ==========================================
580
- async shutdown() {
581
- for (const [_, worker] of this.workers) {
582
- await worker.close();
583
- }
584
- this.workers.clear();
585
- for (const [_, events] of this.queueEvents) {
586
- await events.close();
587
- }
588
- this.queueEvents.clear();
589
- for (const [_, queue] of this.queues) {
590
- await queue.close();
591
- }
592
- this.queues.clear();
496
+ };
497
+
498
+ // src/utils/id-generator.ts
499
+ var IgniterJobsIdGenerator = class {
500
+ /**
501
+ * Generates a unique identifier with a prefix.
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * const jobId = IgniterJobsIdGenerator.generate('job')
506
+ * ```
507
+ */
508
+ static generate(prefix) {
509
+ const now = Date.now().toString(36);
510
+ const random = Math.random().toString(36).slice(2, 8);
511
+ return `${prefix}_${now}_${random}`;
593
512
  }
594
513
  };
595
514
 
596
515
  // src/adapters/memory.adapter.ts
597
- var MemoryAdapter = class _MemoryAdapter {
516
+ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
598
517
  constructor() {
599
- this.queues = /* @__PURE__ */ new Map();
600
- this.eventHandlers = /* @__PURE__ */ new Map();
518
+ this.client = {
519
+ type: "memory"
520
+ };
521
+ this.jobsById = /* @__PURE__ */ new Map();
522
+ this.jobsByQueue = /* @__PURE__ */ new Map();
523
+ this.registeredJobs = /* @__PURE__ */ new Map();
524
+ this.registeredCrons = /* @__PURE__ */ new Map();
601
525
  this.workers = /* @__PURE__ */ new Map();
602
- this.processingInterval = null;
526
+ this.subscribers = /* @__PURE__ */ new Map();
527
+ this.queues = {
528
+ list: async () => this.listQueues(),
529
+ get: async (name) => this.getQueueInfo(name),
530
+ getJobCounts: async (name) => this.getQueueJobCounts(name),
531
+ getJobs: async (name, filter) => {
532
+ const statuses = filter?.status;
533
+ const limit = filter?.limit ?? 100;
534
+ const offset = filter?.offset ?? 0;
535
+ const results = await this.searchJobs({
536
+ queue: name,
537
+ status: statuses,
538
+ limit,
539
+ offset
540
+ });
541
+ return results;
542
+ },
543
+ pause: async (name) => this.pauseQueue(name),
544
+ resume: async (name) => this.resumeQueue(name),
545
+ isPaused: async (name) => {
546
+ const info = await this.getQueueInfo(name);
547
+ return info?.isPaused ?? false;
548
+ },
549
+ drain: async (name) => this.drainQueue(name),
550
+ clean: async (name, options) => this.cleanQueue(name, options),
551
+ obliterate: async (name, options) => this.obliterateQueue(name, options)
552
+ };
553
+ this.pausedQueues = /* @__PURE__ */ new Set();
603
554
  }
604
- /**
605
- * Create a new memory adapter.
606
- */
607
555
  static create() {
608
- return new _MemoryAdapter();
609
- }
610
- /**
611
- * Get the underlying client (null for memory adapter).
612
- */
613
- get client() {
614
- return null;
556
+ return new _IgniterJobsMemoryAdapter();
615
557
  }
616
- /**
617
- * Get or create a queue.
618
- */
619
- getOrCreateQueue(name) {
620
- if (!this.queues.has(name)) {
621
- this.queues.set(name, {
622
- name,
623
- isPaused: false,
624
- jobs: /* @__PURE__ */ new Map(),
625
- pausedJobTypes: /* @__PURE__ */ new Set()
558
+ registerJob(queueName, jobName, definition) {
559
+ const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
560
+ if (queueJobs.has(jobName)) {
561
+ throw new IgniterJobsError({
562
+ code: "JOBS_DUPLICATE_JOB",
563
+ message: `Job "${jobName}" already registered for queue "${queueName}".`
626
564
  });
627
565
  }
628
- return this.queues.get(name);
629
- }
630
- /**
631
- * Generate a unique job ID.
632
- */
633
- generateJobId() {
634
- return `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
566
+ queueJobs.set(jobName, definition);
567
+ this.registeredJobs.set(queueName, queueJobs);
635
568
  }
636
- /**
637
- * Emit an event to subscribers.
638
- */
639
- async emitEvent(type, data) {
640
- for (const [pattern, handlers] of this.eventHandlers) {
641
- if (this.matchesPattern(pattern, type)) {
642
- for (const handler of handlers) {
643
- try {
644
- await handler({
645
- type,
646
- data,
647
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
648
- });
649
- } catch {
650
- }
651
- }
652
- }
569
+ registerCron(queueName, cronName, definition) {
570
+ const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
571
+ if (queueCrons.has(cronName)) {
572
+ throw new IgniterJobsError({
573
+ code: "JOBS_INVALID_CRON",
574
+ message: `Cron "${cronName}" already registered for queue "${queueName}".`
575
+ });
653
576
  }
577
+ queueCrons.set(cronName, definition);
578
+ this.registeredCrons.set(queueName, queueCrons);
654
579
  }
655
- /**
656
- * Check if a pattern matches an event type.
657
- */
658
- matchesPattern(pattern, eventType) {
659
- if (pattern === "*") return true;
660
- const regex = new RegExp(
661
- "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
662
- );
663
- return regex.test(eventType);
664
- }
665
- // ==========================================
666
- // JOB OPERATIONS
667
- // ==========================================
668
580
  async dispatch(params) {
669
- const queue = this.getOrCreateQueue(params.queue);
670
- const jobId = params.jobId || this.generateJobId();
581
+ const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
582
+ const maxAttempts = params.attempts ?? 1;
583
+ const metadata = params.metadata ?? {};
671
584
  const job = {
672
585
  id: jobId,
673
- name: params.name,
586
+ name: params.jobName,
674
587
  queue: params.queue,
675
- data: params.data,
676
- state: params.delay ? "delayed" : "waiting",
588
+ input: params.input,
589
+ status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
677
590
  progress: 0,
678
- attempts: 0,
679
- maxAttempts: params.attempts ?? 3,
680
- timestamp: Date.now(),
681
- delay: params.delay,
591
+ attemptsMade: 0,
592
+ maxAttempts,
682
593
  priority: params.priority ?? 0,
594
+ createdAt: /* @__PURE__ */ new Date(),
595
+ metadata,
683
596
  scope: params.scope,
684
- actor: params.actor,
685
- logs: [],
686
- scheduledAt: params.delay ? Date.now() + params.delay : void 0
597
+ logs: []
687
598
  };
688
- queue.jobs.set(jobId, job);
689
- await this.emitEvent(`${params.queue}:${params.name}:enqueued`, {
690
- jobId,
691
- name: params.name,
692
- queue: params.queue
693
- });
694
- this.startProcessing();
599
+ this.jobsById.set(jobId, job);
600
+ const queueList = this.jobsByQueue.get(params.queue) ?? [];
601
+ queueList.push(jobId);
602
+ this.jobsByQueue.set(params.queue, queueList);
603
+ if (params.delay && params.delay > 0) {
604
+ setTimeout(() => {
605
+ const stored = this.jobsById.get(jobId);
606
+ if (!stored) return;
607
+ if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
608
+ void this.kickWorkers(params.queue);
609
+ }, params.delay);
610
+ return jobId;
611
+ }
612
+ void this.kickWorkers(params.queue);
695
613
  return jobId;
696
614
  }
697
615
  async schedule(params) {
698
- const delay = params.at ? params.at.getTime() - Date.now() : params.delay ?? 0;
699
- return this.dispatch({
700
- ...params,
701
- delay
702
- });
616
+ if (params.at) {
617
+ const delay = params.at.getTime() - Date.now();
618
+ if (delay <= 0) {
619
+ throw new IgniterJobsError({
620
+ code: "JOBS_INVALID_SCHEDULE",
621
+ message: "Scheduled time must be in the future."
622
+ });
623
+ }
624
+ return this.dispatch({ ...params, delay });
625
+ }
626
+ if (params.cron || params.every) {
627
+ return this.dispatch({ ...params, delay: params.delay ?? 0 });
628
+ }
629
+ return this.dispatch(params);
703
630
  }
704
- async getJob(queue, jobId) {
705
- const q = this.getOrCreateQueue(queue);
706
- const job = q.jobs.get(jobId);
631
+ async getJob(jobId, queue) {
632
+ const job = this.jobsById.get(jobId);
707
633
  if (!job) return null;
708
- return {
709
- id: job.id,
710
- name: job.name,
711
- queue: job.queue,
712
- state: job.state,
713
- data: job.data,
714
- result: job.result,
715
- error: job.error,
716
- progress: job.progress,
717
- attempts: job.attempts,
718
- timestamp: job.timestamp,
719
- processedOn: job.processedOn,
720
- finishedOn: job.finishedOn,
721
- delay: job.delay,
722
- priority: job.priority,
723
- scope: job.scope,
724
- actor: job.actor
725
- };
634
+ if (queue && job.queue !== queue) return null;
635
+ return this.toSearchResult(job);
726
636
  }
727
- async getJobState(queue, jobId) {
728
- const q = this.getOrCreateQueue(queue);
729
- const job = q.jobs.get(jobId);
730
- return job?.state ?? null;
637
+ async getJobState(jobId, queue) {
638
+ const job = this.jobsById.get(jobId);
639
+ if (!job) return null;
640
+ if (queue && job.queue !== queue) return null;
641
+ return job.status;
731
642
  }
732
- async getJobProgress(queue, jobId) {
733
- const q = this.getOrCreateQueue(queue);
734
- const job = q.jobs.get(jobId);
735
- return job?.progress ?? 0;
643
+ async getJobLogs(jobId, queue) {
644
+ const job = this.jobsById.get(jobId);
645
+ if (!job) return [];
646
+ if (queue && job.queue !== queue) return [];
647
+ return job.logs;
736
648
  }
737
- async getJobLogs(queue, jobId) {
738
- const q = this.getOrCreateQueue(queue);
739
- const job = q.jobs.get(jobId);
740
- return job?.logs ?? [];
649
+ async getJobProgress(jobId, queue) {
650
+ const job = this.jobsById.get(jobId);
651
+ if (!job) return 0;
652
+ if (queue && job.queue !== queue) return 0;
653
+ return job.progress;
741
654
  }
742
- async retryJob(queue, jobId) {
743
- const q = this.getOrCreateQueue(queue);
744
- const job = q.jobs.get(jobId);
655
+ async retryJob(jobId, queue) {
656
+ const job = this.jobsById.get(jobId);
745
657
  if (!job) {
746
- throw new IgniterJobsError({
747
- code: "JOBS_JOB_NOT_FOUND",
748
- message: `Job "${jobId}" not found`,
749
- statusCode: 404
750
- });
658
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
659
+ }
660
+ if (queue && job.queue !== queue) {
661
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
751
662
  }
752
- job.state = "waiting";
663
+ job.status = "waiting";
753
664
  job.error = void 0;
754
- job.attempts = 0;
665
+ job.completedAt = void 0;
666
+ job.progress = 0;
667
+ void this.kickWorkers(job.queue);
755
668
  }
756
- async removeJob(queue, jobId) {
757
- const q = this.getOrCreateQueue(queue);
758
- q.jobs.delete(jobId);
669
+ async removeJob(jobId, queue) {
670
+ const job = this.jobsById.get(jobId);
671
+ if (!job) return;
672
+ if (queue && job.queue !== queue) return;
673
+ this.jobsById.delete(jobId);
674
+ const list = this.jobsByQueue.get(job.queue);
675
+ if (list) this.jobsByQueue.set(job.queue, list.filter((id) => id !== jobId));
759
676
  }
760
- async promoteJob(queue, jobId) {
761
- const q = this.getOrCreateQueue(queue);
762
- const job = q.jobs.get(jobId);
677
+ async promoteJob(jobId, queue) {
678
+ const job = this.jobsById.get(jobId);
763
679
  if (!job) {
764
- throw new IgniterJobsError({
765
- code: "JOBS_JOB_NOT_FOUND",
766
- message: `Job "${jobId}" not found`,
767
- statusCode: 404
768
- });
680
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
681
+ }
682
+ if (queue && job.queue !== queue) {
683
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
769
684
  }
770
- if (job.state === "delayed") {
771
- job.state = "waiting";
772
- job.scheduledAt = void 0;
773
- job.delay = void 0;
685
+ if (job.status === "delayed" || job.status === "paused") {
686
+ job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
687
+ void this.kickWorkers(job.queue);
774
688
  }
775
689
  }
776
- async moveJob(queue, jobId, state, reason) {
777
- const q = this.getOrCreateQueue(queue);
778
- const job = q.jobs.get(jobId);
690
+ async moveJobToFailed(jobId, reason, queue) {
691
+ const job = this.jobsById.get(jobId);
779
692
  if (!job) {
780
- throw new IgniterJobsError({
781
- code: "JOBS_JOB_NOT_FOUND",
782
- message: `Job "${jobId}" not found`,
783
- statusCode: 404
784
- });
693
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
785
694
  }
786
- job.state = state;
787
- job.finishedOn = Date.now();
788
- if (state === "failed") {
789
- job.error = reason;
790
- } else {
791
- job.result = reason;
695
+ if (queue && job.queue !== queue) {
696
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
792
697
  }
698
+ job.status = "failed";
699
+ job.error = reason;
700
+ job.completedAt = /* @__PURE__ */ new Date();
793
701
  }
794
- async retryJobs(queue, jobIds) {
795
- await Promise.all(jobIds.map((id) => this.retryJob(queue, id)));
702
+ async retryManyJobs(jobIds, queue) {
703
+ await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
796
704
  }
797
- async removeJobs(queue, jobIds) {
798
- await Promise.all(jobIds.map((id) => this.removeJob(queue, id)));
705
+ async removeManyJobs(jobIds, queue) {
706
+ await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
799
707
  }
800
- // ==========================================
801
- // QUEUE OPERATIONS
802
- // ==========================================
803
- async getQueue(queue) {
804
- const q = this.getOrCreateQueue(queue);
805
- const counts = await this.getJobCounts(queue);
708
+ async getQueueInfo(queue) {
709
+ const counts = await this.getQueueJobCounts(queue);
806
710
  return {
807
711
  name: queue,
808
- isPaused: q.isPaused,
712
+ isPaused: this.pausedQueues.has(queue),
809
713
  jobCounts: counts
810
714
  };
811
715
  }
716
+ async getQueueJobCounts(queue) {
717
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
718
+ const counts = {
719
+ waiting: 0,
720
+ active: 0,
721
+ completed: 0,
722
+ failed: 0,
723
+ delayed: 0,
724
+ paused: 0
725
+ };
726
+ for (const id of jobIds) {
727
+ const job = this.jobsById.get(id);
728
+ if (!job) continue;
729
+ if (job.status in counts) {
730
+ counts[job.status]++;
731
+ }
732
+ }
733
+ return counts;
734
+ }
735
+ async listQueues() {
736
+ const queues = Array.from(/* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.registeredJobs.keys(), ...this.registeredCrons.keys()]));
737
+ const result = [];
738
+ for (const q of queues) {
739
+ result.push(await this.getQueueInfo(q));
740
+ }
741
+ return result;
742
+ }
812
743
  async pauseQueue(queue) {
813
- const q = this.getOrCreateQueue(queue);
814
- q.isPaused = true;
744
+ this.pausedQueues.add(queue);
745
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
746
+ for (const id of jobIds) {
747
+ const job = this.jobsById.get(id);
748
+ if (!job) continue;
749
+ if (job.status === "waiting") job.status = "paused";
750
+ }
815
751
  }
816
752
  async resumeQueue(queue) {
817
- const q = this.getOrCreateQueue(queue);
818
- q.isPaused = false;
753
+ this.pausedQueues.delete(queue);
754
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
755
+ for (const id of jobIds) {
756
+ const job = this.jobsById.get(id);
757
+ if (!job) continue;
758
+ if (job.status === "paused") job.status = "waiting";
759
+ }
760
+ void this.kickWorkers(queue);
819
761
  }
820
762
  async drainQueue(queue) {
821
- const q = this.getOrCreateQueue(queue);
822
- let count = 0;
823
- for (const [id, job] of q.jobs) {
824
- if (job.state === "waiting" || job.state === "delayed") {
825
- q.jobs.delete(id);
826
- count++;
763
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
764
+ let removed = 0;
765
+ for (const id of jobIds) {
766
+ const job = this.jobsById.get(id);
767
+ if (!job) continue;
768
+ if (job.status === "waiting" || job.status === "paused") {
769
+ this.jobsById.delete(id);
770
+ removed++;
827
771
  }
828
772
  }
829
- return count;
773
+ this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
774
+ return removed;
830
775
  }
831
776
  async cleanQueue(queue, options) {
832
- const q = this.getOrCreateQueue(queue);
833
777
  const statuses = Array.isArray(options.status) ? options.status : [options.status];
778
+ const olderThan = options.olderThan ?? 0;
779
+ const limit = options.limit ?? Number.POSITIVE_INFINITY;
780
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
834
781
  const now = Date.now();
835
- let count = 0;
836
- let removed = 0;
837
- for (const [id, job] of q.jobs) {
838
- if (options.limit && removed >= options.limit) break;
839
- if (statuses.includes(job.state)) {
840
- const age = now - job.timestamp;
841
- if (!options.olderThan || age >= options.olderThan) {
842
- q.jobs.delete(id);
843
- removed++;
844
- count++;
845
- }
846
- }
782
+ let cleaned = 0;
783
+ for (const id of [...jobIds]) {
784
+ if (cleaned >= limit) break;
785
+ const job = this.jobsById.get(id);
786
+ if (!job) continue;
787
+ if (!statuses.includes(job.status)) continue;
788
+ const ageMs = now - job.createdAt.getTime();
789
+ if (ageMs < olderThan) continue;
790
+ this.jobsById.delete(id);
791
+ cleaned++;
847
792
  }
848
- return count;
793
+ this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
794
+ return cleaned;
849
795
  }
850
796
  async obliterateQueue(queue, options) {
851
- this.queues.delete(queue);
852
- }
853
- async retryAllFailed(queue) {
854
- const q = this.getOrCreateQueue(queue);
855
- let count = 0;
856
- for (const job of q.jobs.values()) {
857
- if (job.state === "failed") {
858
- job.state = "waiting";
859
- job.error = void 0;
860
- job.attempts = 0;
861
- count++;
797
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
798
+ for (const id of jobIds) this.jobsById.delete(id);
799
+ this.jobsByQueue.delete(queue);
800
+ this.registeredJobs.delete(queue);
801
+ this.registeredCrons.delete(queue);
802
+ this.pausedQueues.delete(queue);
803
+ }
804
+ async retryAllInQueue(queue) {
805
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
806
+ let retried = 0;
807
+ for (const id of jobIds) {
808
+ const job = this.jobsById.get(id);
809
+ if (!job) continue;
810
+ if (job.status === "failed") {
811
+ await this.retryJob(id, queue);
812
+ retried++;
862
813
  }
863
814
  }
864
- return count;
815
+ return retried;
865
816
  }
866
- async getJobCounts(queue) {
867
- const q = this.getOrCreateQueue(queue);
868
- const counts = {
869
- waiting: 0,
870
- active: 0,
871
- completed: 0,
872
- failed: 0,
873
- delayed: 0,
874
- paused: 0
875
- };
876
- for (const job of q.jobs.values()) {
877
- counts[job.state]++;
817
+ async pauseJobType(queue, jobName) {
818
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
819
+ for (const id of jobIds) {
820
+ const job = this.jobsById.get(id);
821
+ if (!job) continue;
822
+ if (job.name === jobName && job.status === "waiting") job.status = "paused";
878
823
  }
879
- return counts;
880
824
  }
881
- async listJobs(queue, options) {
882
- const q = this.getOrCreateQueue(queue);
883
- const results = [];
884
- const statuses = options?.status || ["waiting", "active", "completed", "failed", "delayed", "paused"];
885
- for (const job of q.jobs.values()) {
886
- if (statuses.includes(job.state)) {
887
- results.push({
888
- id: job.id,
889
- name: job.name,
890
- queue: job.queue,
891
- state: job.state,
892
- data: job.data,
893
- result: job.result,
894
- error: job.error,
895
- progress: job.progress,
896
- attempts: job.attempts,
897
- timestamp: job.timestamp,
898
- processedOn: job.processedOn,
899
- finishedOn: job.finishedOn,
900
- scope: job.scope,
901
- actor: job.actor
902
- });
903
- }
825
+ async resumeJobType(queue, jobName) {
826
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
827
+ for (const id of jobIds) {
828
+ const job = this.jobsById.get(id);
829
+ if (!job) continue;
830
+ if (job.name === jobName && job.status === "paused") job.status = "waiting";
904
831
  }
905
- const start = options?.start ?? 0;
906
- const end = options?.end ?? results.length;
907
- return results.slice(start, end);
832
+ void this.kickWorkers(queue);
908
833
  }
909
- // ==========================================
910
- // PAUSE/RESUME JOB TYPES
911
- // ==========================================
912
- async pauseJobType(queue, jobName) {
913
- const q = this.getOrCreateQueue(queue);
914
- q.pausedJobTypes.add(jobName);
834
+ async searchJobs(filter) {
835
+ const queue = filter?.queue;
836
+ const statuses = filter?.status;
837
+ const limit = filter?.limit ?? 100;
838
+ const offset = filter?.offset ?? 0;
839
+ const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort((a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime());
840
+ return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
915
841
  }
916
- async resumeJobType(queue, jobName) {
917
- const q = this.getOrCreateQueue(queue);
918
- q.pausedJobTypes.delete(jobName);
919
- }
920
- // ==========================================
921
- // EVENTS
922
- // ==========================================
923
- async subscribe(pattern, handler) {
924
- if (!this.eventHandlers.has(pattern)) {
925
- this.eventHandlers.set(pattern, /* @__PURE__ */ new Set());
926
- }
927
- this.eventHandlers.get(pattern).add(handler);
842
+ async searchQueues(filter) {
843
+ const name = filter?.name;
844
+ const isPaused = filter?.isPaused;
845
+ const all = await this.listQueues();
846
+ return all.filter((q) => name ? q.name.includes(name) : true).filter((q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true);
847
+ }
848
+ async searchWorkers(filter) {
849
+ const queue = filter?.queue;
850
+ const isRunning = filter?.isRunning;
851
+ return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter((w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true).map((w) => this.toWorkerHandle(w));
852
+ }
853
+ async createWorker(config) {
854
+ const workerId = IgniterJobsIdGenerator.generate("worker");
855
+ const state = {
856
+ id: workerId,
857
+ queues: config.queues ?? [],
858
+ concurrency: config.concurrency ?? 1,
859
+ paused: false,
860
+ closed: false,
861
+ startedAt: /* @__PURE__ */ new Date(),
862
+ metrics: { processed: 0, failed: 0, totalDuration: 0 },
863
+ handlers: config.handlers
864
+ };
865
+ this.workers.set(workerId, state);
866
+ for (const q of state.queues) void this.kickWorkers(q);
867
+ return this.toWorkerHandle(state);
868
+ }
869
+ getWorkers() {
870
+ const out = /* @__PURE__ */ new Map();
871
+ for (const [id, state] of this.workers) out.set(id, this.toWorkerHandle(state));
872
+ return out;
873
+ }
874
+ async publishEvent(channel, payload) {
875
+ const handlers = this.subscribers.get(channel);
876
+ if (!handlers) return;
877
+ await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
878
+ }
879
+ async subscribeEvent(channel, handler) {
880
+ const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
881
+ set.add(handler);
882
+ this.subscribers.set(channel, set);
928
883
  return async () => {
929
- this.eventHandlers.get(pattern)?.delete(handler);
884
+ const current = this.subscribers.get(channel);
885
+ if (!current) return;
886
+ current.delete(handler);
887
+ if (current.size === 0) this.subscribers.delete(channel);
930
888
  };
931
889
  }
932
- // ==========================================
933
- // WORKERS
934
- // ==========================================
935
- async createWorker(config, handler) {
936
- const workerId = `worker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
937
- this.workers.set(workerId, {
938
- handler,
939
- config,
940
- running: true,
941
- paused: false
942
- });
943
- const startTime = Date.now();
944
- let processed = 0;
945
- let failed = 0;
946
- let completed = 0;
890
+ async shutdown() {
891
+ this.workers.clear();
892
+ this.subscribers.clear();
893
+ }
894
+ toSearchResult(job) {
947
895
  return {
948
- id: workerId,
896
+ id: job.id,
897
+ name: job.name,
898
+ queue: job.queue,
899
+ status: job.status,
900
+ input: job.input,
901
+ result: job.result,
902
+ error: job.error,
903
+ progress: job.progress,
904
+ attemptsMade: job.attemptsMade,
905
+ priority: job.priority,
906
+ createdAt: job.createdAt,
907
+ startedAt: job.startedAt,
908
+ completedAt: job.completedAt,
909
+ metadata: job.metadata,
910
+ scope: job.scope
911
+ };
912
+ }
913
+ toWorkerHandle(worker) {
914
+ return {
915
+ id: worker.id,
916
+ queues: worker.queues,
949
917
  pause: async () => {
950
- const worker = this.workers.get(workerId);
951
- if (worker) worker.paused = true;
918
+ worker.paused = true;
952
919
  },
953
920
  resume: async () => {
954
- const worker = this.workers.get(workerId);
955
- if (worker) worker.paused = false;
921
+ worker.paused = false;
922
+ for (const q of worker.queues) void this.kickWorkers(q);
956
923
  },
957
924
  close: async () => {
958
- this.workers.delete(workerId);
959
- },
960
- isRunning: () => {
961
- const worker = this.workers.get(workerId);
962
- return worker?.running && !worker?.paused || false;
963
- },
964
- isPaused: () => {
965
- return this.workers.get(workerId)?.paused || false;
925
+ worker.closed = true;
966
926
  },
967
- getMetrics: async () => ({
968
- processed,
969
- failed,
970
- completed,
971
- active: 0,
972
- uptime: Date.now() - startTime
973
- })
927
+ isRunning: () => !worker.closed && !worker.paused,
928
+ isPaused: () => worker.paused,
929
+ isClosed: () => worker.closed,
930
+ getMetrics: async () => this.toWorkerMetrics(worker)
974
931
  };
975
932
  }
976
- /**
977
- * Start the internal job processing loop.
978
- */
979
- startProcessing() {
980
- if (this.processingInterval) return;
981
- this.processingInterval = setInterval(async () => {
982
- await this.processJobs();
983
- }, 100);
933
+ toWorkerMetrics(worker) {
934
+ const uptime = Date.now() - worker.startedAt.getTime();
935
+ const processed = worker.metrics.processed;
936
+ return {
937
+ processed,
938
+ failed: worker.metrics.failed,
939
+ avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
940
+ concurrency: worker.concurrency,
941
+ uptime
942
+ };
984
943
  }
985
- /**
986
- * Process pending jobs.
987
- */
988
- async processJobs() {
989
- for (const [_, worker] of this.workers) {
990
- if (!worker.running || worker.paused) continue;
991
- for (const queueName of worker.config.queues) {
992
- const queue = this.queues.get(queueName);
993
- if (!queue || queue.isPaused) continue;
994
- for (const job of queue.jobs.values()) {
995
- if (job.state !== "waiting") continue;
996
- if (queue.pausedJobTypes.has(job.name)) continue;
997
- if (job.scheduledAt && job.scheduledAt > Date.now()) continue;
998
- job.state = "active";
999
- job.processedOn = Date.now();
1000
- job.attempts++;
1001
- try {
1002
- const result = await worker.handler({
1003
- id: job.id,
1004
- name: job.name,
1005
- queue: job.queue,
1006
- data: job.data,
1007
- attempt: job.attempts,
1008
- timestamp: job.timestamp,
1009
- scope: job.scope,
1010
- actor: job.actor,
1011
- log: async (level, message) => {
1012
- job.logs.push({
1013
- timestamp: /* @__PURE__ */ new Date(),
1014
- message,
1015
- level
1016
- });
1017
- },
1018
- updateProgress: async (progress) => {
1019
- job.progress = progress;
1020
- }
1021
- });
1022
- job.state = "completed";
1023
- job.result = result;
1024
- job.finishedOn = Date.now();
1025
- await this.emitEvent(`${job.queue}:${job.name}:completed`, {
1026
- jobId: job.id,
1027
- result
1028
- });
1029
- } catch (error) {
1030
- const errorMessage = error instanceof Error ? error.message : String(error);
1031
- if (job.attempts < job.maxAttempts) {
1032
- job.state = "waiting";
1033
- await this.emitEvent(`${job.queue}:${job.name}:retrying`, {
1034
- jobId: job.id,
1035
- error: errorMessage,
1036
- attempt: job.attempts
1037
- });
1038
- } else {
1039
- job.state = "failed";
1040
- job.error = errorMessage;
1041
- job.finishedOn = Date.now();
1042
- await this.emitEvent(`${job.queue}:${job.name}:failed`, {
1043
- jobId: job.id,
1044
- error: errorMessage
1045
- });
1046
- }
1047
- }
1048
- }
1049
- }
944
+ async kickWorkers(queue) {
945
+ if (this.pausedQueues.has(queue)) return;
946
+ const relevant = Array.from(this.workers.values()).filter((w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue)));
947
+ if (relevant.length === 0) return;
948
+ for (const w of relevant) {
949
+ void this.processLoop(w, queue);
1050
950
  }
1051
951
  }
1052
- // ==========================================
1053
- // SEARCH
1054
- // ==========================================
1055
- async searchJobs(filter) {
1056
- const results = [];
1057
- const queuesToSearch = filter.queue ? [filter.queue] : Array.from(this.queues.keys());
1058
- for (const queueName of queuesToSearch) {
1059
- const queue = this.queues.get(queueName);
1060
- if (!queue) continue;
1061
- for (const job of queue.jobs.values()) {
1062
- if (filter.status && !filter.status.includes(job.state)) continue;
1063
- if (filter.jobName && job.name !== filter.jobName) continue;
1064
- if (filter.scopeId && job.scope?.id !== filter.scopeId) continue;
1065
- if (filter.actorId && job.actor?.id !== filter.actorId) continue;
1066
- if (filter.dateRange?.from && job.timestamp < filter.dateRange.from.getTime()) continue;
1067
- if (filter.dateRange?.to && job.timestamp > filter.dateRange.to.getTime()) continue;
1068
- results.push({
1069
- id: job.id,
1070
- name: job.name,
1071
- queue: job.queue,
1072
- state: job.state,
1073
- data: job.data,
1074
- result: job.result,
1075
- error: job.error,
1076
- progress: job.progress,
1077
- attempts: job.attempts,
1078
- timestamp: job.timestamp,
1079
- processedOn: job.processedOn,
1080
- finishedOn: job.finishedOn,
952
+ async processLoop(worker, queue) {
953
+ if (worker.closed || worker.paused) return;
954
+ const concurrency = Math.max(1, worker.concurrency);
955
+ const running = worker.__running;
956
+ const currentRunning = running ?? 0;
957
+ if (currentRunning >= concurrency) return;
958
+ worker.__running = currentRunning + 1;
959
+ try {
960
+ const next = this.nextJob(queue);
961
+ if (!next) return;
962
+ await this.processJob(worker, next);
963
+ } finally {
964
+ worker.__running = worker.__running - 1;
965
+ if (this.nextJob(queue)) void this.processLoop(worker, queue);
966
+ else if (worker.handlers?.onIdle) await worker.handlers.onIdle();
967
+ }
968
+ }
969
+ nextJob(queue) {
970
+ const ids = this.jobsByQueue.get(queue) ?? [];
971
+ const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort((a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime());
972
+ return candidates[0] ?? null;
973
+ }
974
+ async processJob(worker, job) {
975
+ if (this.pausedQueues.has(job.queue)) {
976
+ job.status = "paused";
977
+ return;
978
+ }
979
+ job.status = "active";
980
+ job.startedAt = /* @__PURE__ */ new Date();
981
+ job.attemptsMade += 1;
982
+ job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: "Job started" });
983
+ if (worker.handlers?.onActive) await worker.handlers.onActive({ job: this.toSearchResult(job) });
984
+ const start = Date.now();
985
+ try {
986
+ const definition = this.registeredJobs.get(job.queue)?.get(job.name);
987
+ if (!definition) {
988
+ throw new IgniterJobsError({
989
+ code: "JOBS_NOT_REGISTERED",
990
+ message: `Job "${job.name}" is not registered for queue "${job.queue}".`
991
+ });
992
+ }
993
+ if (definition.onStart) {
994
+ await definition.onStart({
995
+ input: job.input,
996
+ context: {},
997
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
1081
998
  scope: job.scope,
1082
- actor: job.actor
999
+ startedAt: job.startedAt
1083
1000
  });
1084
1001
  }
1085
- }
1086
- if (filter.orderBy) {
1087
- const [field, direction] = filter.orderBy.split(":");
1088
- results.sort((a, b) => {
1089
- const aVal = field === "createdAt" ? a.timestamp : a[field];
1090
- const bVal = field === "createdAt" ? b.timestamp : b[field];
1091
- return direction === "asc" ? aVal - bVal : bVal - aVal;
1092
- });
1093
- }
1094
- const offset = filter.offset ?? 0;
1095
- const limit = filter.limit ?? 100;
1096
- return results.slice(offset, offset + limit);
1097
- }
1098
- async searchQueues(filter) {
1099
- const results = [];
1100
- for (const [queueName, queue] of this.queues) {
1101
- if (filter.name && !queueName.includes(filter.name)) continue;
1102
- if (filter.isPaused !== void 0 && queue.isPaused !== filter.isPaused) continue;
1103
- const counts = await this.getJobCounts(queueName);
1104
- results.push({
1105
- name: queueName,
1106
- isPaused: queue.isPaused,
1107
- jobCounts: counts
1002
+ const result = await definition.handler({
1003
+ input: job.input,
1004
+ context: {},
1005
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
1006
+ scope: job.scope
1108
1007
  });
1008
+ const duration = Date.now() - start;
1009
+ job.status = "completed";
1010
+ job.completedAt = /* @__PURE__ */ new Date();
1011
+ job.result = result;
1012
+ job.progress = 100;
1013
+ job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: `Job completed in ${duration}ms` });
1014
+ worker.metrics.processed += 1;
1015
+ worker.metrics.totalDuration += duration;
1016
+ if (definition.onSuccess) {
1017
+ await definition.onSuccess({
1018
+ input: job.input,
1019
+ context: {},
1020
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
1021
+ scope: job.scope,
1022
+ result,
1023
+ duration
1024
+ });
1025
+ }
1026
+ if (worker.handlers?.onSuccess) await worker.handlers.onSuccess({ job: this.toSearchResult(job), result });
1027
+ } catch (error) {
1028
+ job.error = error?.message ?? String(error);
1029
+ job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "error", message: job.error ?? "Unknown error" });
1030
+ const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
1031
+ if (isFinalAttempt) {
1032
+ job.status = "failed";
1033
+ job.completedAt = /* @__PURE__ */ new Date();
1034
+ worker.metrics.failed += 1;
1035
+ const definition = this.registeredJobs.get(job.queue)?.get(job.name);
1036
+ if (definition?.onFailure) {
1037
+ await definition.onFailure({
1038
+ input: job.input,
1039
+ context: {},
1040
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
1041
+ scope: job.scope,
1042
+ error,
1043
+ isFinalAttempt: true
1044
+ });
1045
+ }
1046
+ if (worker.handlers?.onFailure) await worker.handlers.onFailure({ job: this.toSearchResult(job), error });
1047
+ } else {
1048
+ job.status = "waiting";
1049
+ void this.kickWorkers(job.queue);
1050
+ }
1109
1051
  }
1110
- return results;
1111
- }
1112
- // ==========================================
1113
- // LIFECYCLE
1114
- // ==========================================
1115
- async shutdown() {
1116
- if (this.processingInterval) {
1117
- clearInterval(this.processingInterval);
1118
- this.processingInterval = null;
1119
- }
1120
- this.workers.clear();
1121
- this.queues.clear();
1122
- this.eventHandlers.clear();
1123
1052
  }
1124
1053
  };
1125
1054
 
1126
- exports.BullMQAdapter = BullMQAdapter;
1127
- exports.MemoryAdapter = MemoryAdapter;
1055
+ exports.IgniterJobsBullMQAdapter = IgniterJobsBullMQAdapter;
1056
+ exports.IgniterJobsMemoryAdapter = IgniterJobsMemoryAdapter;
1128
1057
  //# sourceMappingURL=index.js.map
1129
1058
  //# sourceMappingURL=index.js.map