@igniter-js/jobs 0.1.0 → 0.1.12

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