@igniter-js/jobs 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,570 +2,568 @@
2
2
 
3
3
  var core = require('@igniter-js/core');
4
4
 
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
- }
23
- }
5
+ // src/utils/id-generator.ts
6
+ var IgniterJobsIdGenerator = class {
24
7
  /**
25
- * Convert error to a plain object for serialization.
8
+ * Generates a unique identifier with a prefix.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const jobId = IgniterJobsIdGenerator.generate('job')
13
+ * ```
26
14
  */
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
- };
15
+ static generate(prefix) {
16
+ const now = Date.now().toString(36);
17
+ const random = Math.random().toString(36).slice(2, 8);
18
+ return `${prefix}_${now}_${random}`;
19
+ }
20
+ };
21
+ var IgniterJobsError = class extends core.IgniterError {
22
+ constructor(options) {
23
+ super(options);
36
24
  }
37
25
  };
38
26
 
39
27
  // src/adapters/memory.adapter.ts
40
- var MemoryAdapter = class _MemoryAdapter {
28
+ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
41
29
  constructor() {
42
- this.queues = /* @__PURE__ */ new Map();
43
- this.eventHandlers = /* @__PURE__ */ new Map();
30
+ this.client = {
31
+ type: "memory"
32
+ };
33
+ this.jobsById = /* @__PURE__ */ new Map();
34
+ this.jobsByQueue = /* @__PURE__ */ new Map();
35
+ this.registeredJobs = /* @__PURE__ */ new Map();
36
+ this.registeredCrons = /* @__PURE__ */ new Map();
44
37
  this.workers = /* @__PURE__ */ new Map();
45
- this.processingInterval = null;
38
+ this.subscribers = /* @__PURE__ */ new Map();
39
+ this.queues = {
40
+ list: async () => this.listQueues(),
41
+ get: async (name) => this.getQueueInfo(name),
42
+ getJobCounts: async (name) => this.getQueueJobCounts(name),
43
+ getJobs: async (name, filter) => {
44
+ const statuses = filter?.status;
45
+ const limit = filter?.limit ?? 100;
46
+ const offset = filter?.offset ?? 0;
47
+ const results = await this.searchJobs({
48
+ queue: name,
49
+ status: statuses,
50
+ limit,
51
+ offset
52
+ });
53
+ return results;
54
+ },
55
+ pause: async (name) => this.pauseQueue(name),
56
+ resume: async (name) => this.resumeQueue(name),
57
+ isPaused: async (name) => {
58
+ const info = await this.getQueueInfo(name);
59
+ return info?.isPaused ?? false;
60
+ },
61
+ drain: async (name) => this.drainQueue(name),
62
+ clean: async (name, options) => this.cleanQueue(name, options),
63
+ obliterate: async (name, options) => this.obliterateQueue(name, options)
64
+ };
65
+ this.pausedQueues = /* @__PURE__ */ new Set();
46
66
  }
47
- /**
48
- * Create a new memory adapter.
49
- */
50
67
  static create() {
51
- return new _MemoryAdapter();
68
+ return new _IgniterJobsMemoryAdapter();
52
69
  }
53
- /**
54
- * Get the underlying client (null for memory adapter).
55
- */
56
- get client() {
57
- return null;
58
- }
59
- /**
60
- * Get or create a queue.
61
- */
62
- getOrCreateQueue(name) {
63
- if (!this.queues.has(name)) {
64
- this.queues.set(name, {
65
- name,
66
- isPaused: false,
67
- jobs: /* @__PURE__ */ new Map(),
68
- pausedJobTypes: /* @__PURE__ */ new Set()
70
+ registerJob(queueName, jobName, definition) {
71
+ const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
72
+ if (queueJobs.has(jobName)) {
73
+ throw new IgniterJobsError({
74
+ code: "JOBS_DUPLICATE_JOB",
75
+ message: `Job "${jobName}" already registered for queue "${queueName}".`
69
76
  });
70
77
  }
71
- return this.queues.get(name);
72
- }
73
- /**
74
- * Generate a unique job ID.
75
- */
76
- generateJobId() {
77
- return `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
78
+ queueJobs.set(jobName, definition);
79
+ this.registeredJobs.set(queueName, queueJobs);
78
80
  }
79
- /**
80
- * Emit an event to subscribers.
81
- */
82
- async emitEvent(type, data) {
83
- for (const [pattern, handlers] of this.eventHandlers) {
84
- if (this.matchesPattern(pattern, type)) {
85
- for (const handler of handlers) {
86
- try {
87
- await handler({
88
- type,
89
- data,
90
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
91
- });
92
- } catch {
93
- }
94
- }
95
- }
81
+ registerCron(queueName, cronName, definition) {
82
+ const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
83
+ if (queueCrons.has(cronName)) {
84
+ throw new IgniterJobsError({
85
+ code: "JOBS_INVALID_CRON",
86
+ message: `Cron "${cronName}" already registered for queue "${queueName}".`
87
+ });
96
88
  }
89
+ queueCrons.set(cronName, definition);
90
+ this.registeredCrons.set(queueName, queueCrons);
97
91
  }
98
- /**
99
- * Check if a pattern matches an event type.
100
- */
101
- matchesPattern(pattern, eventType) {
102
- if (pattern === "*") return true;
103
- const regex = new RegExp(
104
- "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
105
- );
106
- return regex.test(eventType);
107
- }
108
- // ==========================================
109
- // JOB OPERATIONS
110
- // ==========================================
111
92
  async dispatch(params) {
112
- const queue = this.getOrCreateQueue(params.queue);
113
- const jobId = params.jobId || this.generateJobId();
93
+ const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
94
+ const maxAttempts = params.attempts ?? 1;
95
+ const metadata = params.metadata ?? {};
114
96
  const job = {
115
97
  id: jobId,
116
- name: params.name,
98
+ name: params.jobName,
117
99
  queue: params.queue,
118
- data: params.data,
119
- state: params.delay ? "delayed" : "waiting",
100
+ input: params.input,
101
+ status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
120
102
  progress: 0,
121
- attempts: 0,
122
- maxAttempts: params.attempts ?? 3,
123
- timestamp: Date.now(),
124
- delay: params.delay,
103
+ attemptsMade: 0,
104
+ maxAttempts,
125
105
  priority: params.priority ?? 0,
106
+ createdAt: /* @__PURE__ */ new Date(),
107
+ metadata,
126
108
  scope: params.scope,
127
- actor: params.actor,
128
- logs: [],
129
- scheduledAt: params.delay ? Date.now() + params.delay : void 0
109
+ logs: []
130
110
  };
131
- queue.jobs.set(jobId, job);
132
- await this.emitEvent(`${params.queue}:${params.name}:enqueued`, {
133
- jobId,
134
- name: params.name,
135
- queue: params.queue
136
- });
137
- this.startProcessing();
111
+ this.jobsById.set(jobId, job);
112
+ const queueList = this.jobsByQueue.get(params.queue) ?? [];
113
+ queueList.push(jobId);
114
+ this.jobsByQueue.set(params.queue, queueList);
115
+ if (params.delay && params.delay > 0) {
116
+ setTimeout(() => {
117
+ const stored = this.jobsById.get(jobId);
118
+ if (!stored) return;
119
+ if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
120
+ void this.kickWorkers(params.queue);
121
+ }, params.delay);
122
+ return jobId;
123
+ }
124
+ void this.kickWorkers(params.queue);
138
125
  return jobId;
139
126
  }
140
127
  async schedule(params) {
141
- const delay = params.at ? params.at.getTime() - Date.now() : params.delay ?? 0;
142
- return this.dispatch({
143
- ...params,
144
- delay
145
- });
146
- }
147
- async getJob(queue, jobId) {
148
- const q = this.getOrCreateQueue(queue);
149
- const job = q.jobs.get(jobId);
150
- if (!job) return null;
151
- return {
152
- id: job.id,
153
- name: job.name,
154
- queue: job.queue,
155
- state: job.state,
156
- data: job.data,
157
- result: job.result,
158
- error: job.error,
159
- progress: job.progress,
160
- attempts: job.attempts,
161
- timestamp: job.timestamp,
162
- processedOn: job.processedOn,
163
- finishedOn: job.finishedOn,
164
- delay: job.delay,
165
- priority: job.priority,
166
- scope: job.scope,
167
- actor: job.actor
168
- };
169
- }
170
- async getJobState(queue, jobId) {
171
- const q = this.getOrCreateQueue(queue);
172
- const job = q.jobs.get(jobId);
173
- return job?.state ?? null;
174
- }
175
- async getJobProgress(queue, jobId) {
176
- const q = this.getOrCreateQueue(queue);
177
- const job = q.jobs.get(jobId);
178
- return job?.progress ?? 0;
128
+ if (params.at) {
129
+ const delay = params.at.getTime() - Date.now();
130
+ if (delay <= 0) {
131
+ throw new IgniterJobsError({
132
+ code: "JOBS_INVALID_SCHEDULE",
133
+ message: "Scheduled time must be in the future."
134
+ });
135
+ }
136
+ return this.dispatch({ ...params, delay });
137
+ }
138
+ if (params.cron || params.every) {
139
+ return this.dispatch({ ...params, delay: params.delay ?? 0 });
140
+ }
141
+ return this.dispatch(params);
179
142
  }
180
- async getJobLogs(queue, jobId) {
181
- const q = this.getOrCreateQueue(queue);
182
- const job = q.jobs.get(jobId);
183
- return job?.logs ?? [];
143
+ async getJob(jobId, queue) {
144
+ const job = this.jobsById.get(jobId);
145
+ if (!job) return null;
146
+ if (queue && job.queue !== queue) return null;
147
+ return this.toSearchResult(job);
184
148
  }
185
- async retryJob(queue, jobId) {
186
- const q = this.getOrCreateQueue(queue);
187
- const job = q.jobs.get(jobId);
149
+ async getJobState(jobId, queue) {
150
+ const job = this.jobsById.get(jobId);
151
+ if (!job) return null;
152
+ if (queue && job.queue !== queue) return null;
153
+ return job.status;
154
+ }
155
+ async getJobLogs(jobId, queue) {
156
+ const job = this.jobsById.get(jobId);
157
+ if (!job) return [];
158
+ if (queue && job.queue !== queue) return [];
159
+ return job.logs;
160
+ }
161
+ async getJobProgress(jobId, queue) {
162
+ const job = this.jobsById.get(jobId);
163
+ if (!job) return 0;
164
+ if (queue && job.queue !== queue) return 0;
165
+ return job.progress;
166
+ }
167
+ async retryJob(jobId, queue) {
168
+ const job = this.jobsById.get(jobId);
188
169
  if (!job) {
189
- throw new IgniterJobsError({
190
- code: "JOBS_JOB_NOT_FOUND",
191
- message: `Job "${jobId}" not found`,
192
- statusCode: 404
193
- });
170
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
171
+ }
172
+ if (queue && job.queue !== queue) {
173
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
194
174
  }
195
- job.state = "waiting";
175
+ job.status = "waiting";
196
176
  job.error = void 0;
197
- job.attempts = 0;
198
- }
199
- async removeJob(queue, jobId) {
200
- const q = this.getOrCreateQueue(queue);
201
- q.jobs.delete(jobId);
202
- }
203
- async promoteJob(queue, jobId) {
204
- const q = this.getOrCreateQueue(queue);
205
- const job = q.jobs.get(jobId);
177
+ job.completedAt = void 0;
178
+ job.progress = 0;
179
+ void this.kickWorkers(job.queue);
180
+ }
181
+ async removeJob(jobId, queue) {
182
+ const job = this.jobsById.get(jobId);
183
+ if (!job) return;
184
+ if (queue && job.queue !== queue) return;
185
+ this.jobsById.delete(jobId);
186
+ const list = this.jobsByQueue.get(job.queue);
187
+ if (list) this.jobsByQueue.set(job.queue, list.filter((id) => id !== jobId));
188
+ }
189
+ async promoteJob(jobId, queue) {
190
+ const job = this.jobsById.get(jobId);
206
191
  if (!job) {
207
- throw new IgniterJobsError({
208
- code: "JOBS_JOB_NOT_FOUND",
209
- message: `Job "${jobId}" not found`,
210
- statusCode: 404
211
- });
192
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
193
+ }
194
+ if (queue && job.queue !== queue) {
195
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
212
196
  }
213
- if (job.state === "delayed") {
214
- job.state = "waiting";
215
- job.scheduledAt = void 0;
216
- job.delay = void 0;
197
+ if (job.status === "delayed" || job.status === "paused") {
198
+ job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
199
+ void this.kickWorkers(job.queue);
217
200
  }
218
201
  }
219
- async moveJob(queue, jobId, state, reason) {
220
- const q = this.getOrCreateQueue(queue);
221
- const job = q.jobs.get(jobId);
202
+ async moveJobToFailed(jobId, reason, queue) {
203
+ const job = this.jobsById.get(jobId);
222
204
  if (!job) {
223
- throw new IgniterJobsError({
224
- code: "JOBS_JOB_NOT_FOUND",
225
- message: `Job "${jobId}" not found`,
226
- statusCode: 404
227
- });
205
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found.` });
228
206
  }
229
- job.state = state;
230
- job.finishedOn = Date.now();
231
- if (state === "failed") {
232
- job.error = reason;
233
- } else {
234
- job.result = reason;
207
+ if (queue && job.queue !== queue) {
208
+ throw new IgniterJobsError({ code: "JOBS_NOT_FOUND", message: `Job "${jobId}" not found in queue "${queue}".` });
235
209
  }
210
+ job.status = "failed";
211
+ job.error = reason;
212
+ job.completedAt = /* @__PURE__ */ new Date();
236
213
  }
237
- async retryJobs(queue, jobIds) {
238
- await Promise.all(jobIds.map((id) => this.retryJob(queue, id)));
214
+ async retryManyJobs(jobIds, queue) {
215
+ await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
239
216
  }
240
- async removeJobs(queue, jobIds) {
241
- await Promise.all(jobIds.map((id) => this.removeJob(queue, id)));
217
+ async removeManyJobs(jobIds, queue) {
218
+ await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
242
219
  }
243
- // ==========================================
244
- // QUEUE OPERATIONS
245
- // ==========================================
246
- async getQueue(queue) {
247
- const q = this.getOrCreateQueue(queue);
248
- const counts = await this.getJobCounts(queue);
220
+ async getQueueInfo(queue) {
221
+ const counts = await this.getQueueJobCounts(queue);
249
222
  return {
250
223
  name: queue,
251
- isPaused: q.isPaused,
224
+ isPaused: this.pausedQueues.has(queue),
252
225
  jobCounts: counts
253
226
  };
254
227
  }
228
+ async getQueueJobCounts(queue) {
229
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
230
+ const counts = {
231
+ waiting: 0,
232
+ active: 0,
233
+ completed: 0,
234
+ failed: 0,
235
+ delayed: 0,
236
+ paused: 0
237
+ };
238
+ for (const id of jobIds) {
239
+ const job = this.jobsById.get(id);
240
+ if (!job) continue;
241
+ if (job.status in counts) {
242
+ counts[job.status]++;
243
+ }
244
+ }
245
+ return counts;
246
+ }
247
+ async listQueues() {
248
+ const queues = Array.from(/* @__PURE__ */ new Set([...this.jobsByQueue.keys(), ...this.registeredJobs.keys(), ...this.registeredCrons.keys()]));
249
+ const result = [];
250
+ for (const q of queues) {
251
+ result.push(await this.getQueueInfo(q));
252
+ }
253
+ return result;
254
+ }
255
255
  async pauseQueue(queue) {
256
- const q = this.getOrCreateQueue(queue);
257
- q.isPaused = true;
256
+ this.pausedQueues.add(queue);
257
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
258
+ for (const id of jobIds) {
259
+ const job = this.jobsById.get(id);
260
+ if (!job) continue;
261
+ if (job.status === "waiting") job.status = "paused";
262
+ }
258
263
  }
259
264
  async resumeQueue(queue) {
260
- const q = this.getOrCreateQueue(queue);
261
- q.isPaused = false;
265
+ this.pausedQueues.delete(queue);
266
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
267
+ for (const id of jobIds) {
268
+ const job = this.jobsById.get(id);
269
+ if (!job) continue;
270
+ if (job.status === "paused") job.status = "waiting";
271
+ }
272
+ void this.kickWorkers(queue);
262
273
  }
263
274
  async drainQueue(queue) {
264
- const q = this.getOrCreateQueue(queue);
265
- let count = 0;
266
- for (const [id, job] of q.jobs) {
267
- if (job.state === "waiting" || job.state === "delayed") {
268
- q.jobs.delete(id);
269
- count++;
275
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
276
+ let removed = 0;
277
+ for (const id of jobIds) {
278
+ const job = this.jobsById.get(id);
279
+ if (!job) continue;
280
+ if (job.status === "waiting" || job.status === "paused") {
281
+ this.jobsById.delete(id);
282
+ removed++;
270
283
  }
271
284
  }
272
- return count;
285
+ this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
286
+ return removed;
273
287
  }
274
288
  async cleanQueue(queue, options) {
275
- const q = this.getOrCreateQueue(queue);
276
289
  const statuses = Array.isArray(options.status) ? options.status : [options.status];
290
+ const olderThan = options.olderThan ?? 0;
291
+ const limit = options.limit ?? Number.POSITIVE_INFINITY;
292
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
277
293
  const now = Date.now();
278
- let count = 0;
279
- let removed = 0;
280
- for (const [id, job] of q.jobs) {
281
- if (options.limit && removed >= options.limit) break;
282
- if (statuses.includes(job.state)) {
283
- const age = now - job.timestamp;
284
- if (!options.olderThan || age >= options.olderThan) {
285
- q.jobs.delete(id);
286
- removed++;
287
- count++;
288
- }
289
- }
294
+ let cleaned = 0;
295
+ for (const id of [...jobIds]) {
296
+ if (cleaned >= limit) break;
297
+ const job = this.jobsById.get(id);
298
+ if (!job) continue;
299
+ if (!statuses.includes(job.status)) continue;
300
+ const ageMs = now - job.createdAt.getTime();
301
+ if (ageMs < olderThan) continue;
302
+ this.jobsById.delete(id);
303
+ cleaned++;
290
304
  }
291
- return count;
305
+ this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
306
+ return cleaned;
292
307
  }
293
308
  async obliterateQueue(queue, options) {
294
- this.queues.delete(queue);
295
- }
296
- async retryAllFailed(queue) {
297
- const q = this.getOrCreateQueue(queue);
298
- let count = 0;
299
- for (const job of q.jobs.values()) {
300
- if (job.state === "failed") {
301
- job.state = "waiting";
302
- job.error = void 0;
303
- job.attempts = 0;
304
- count++;
309
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
310
+ for (const id of jobIds) this.jobsById.delete(id);
311
+ this.jobsByQueue.delete(queue);
312
+ this.registeredJobs.delete(queue);
313
+ this.registeredCrons.delete(queue);
314
+ this.pausedQueues.delete(queue);
315
+ }
316
+ async retryAllInQueue(queue) {
317
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
318
+ let retried = 0;
319
+ for (const id of jobIds) {
320
+ const job = this.jobsById.get(id);
321
+ if (!job) continue;
322
+ if (job.status === "failed") {
323
+ await this.retryJob(id, queue);
324
+ retried++;
305
325
  }
306
326
  }
307
- return count;
327
+ return retried;
308
328
  }
309
- async getJobCounts(queue) {
310
- const q = this.getOrCreateQueue(queue);
311
- const counts = {
312
- waiting: 0,
313
- active: 0,
314
- completed: 0,
315
- failed: 0,
316
- delayed: 0,
317
- paused: 0
318
- };
319
- for (const job of q.jobs.values()) {
320
- counts[job.state]++;
329
+ async pauseJobType(queue, jobName) {
330
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
331
+ for (const id of jobIds) {
332
+ const job = this.jobsById.get(id);
333
+ if (!job) continue;
334
+ if (job.name === jobName && job.status === "waiting") job.status = "paused";
321
335
  }
322
- return counts;
323
336
  }
324
- async listJobs(queue, options) {
325
- const q = this.getOrCreateQueue(queue);
326
- const results = [];
327
- const statuses = options?.status || ["waiting", "active", "completed", "failed", "delayed", "paused"];
328
- for (const job of q.jobs.values()) {
329
- if (statuses.includes(job.state)) {
330
- results.push({
331
- id: job.id,
332
- name: job.name,
333
- queue: job.queue,
334
- state: job.state,
335
- data: job.data,
336
- result: job.result,
337
- error: job.error,
338
- progress: job.progress,
339
- attempts: job.attempts,
340
- timestamp: job.timestamp,
341
- processedOn: job.processedOn,
342
- finishedOn: job.finishedOn,
343
- scope: job.scope,
344
- actor: job.actor
345
- });
346
- }
337
+ async resumeJobType(queue, jobName) {
338
+ const jobIds = this.jobsByQueue.get(queue) ?? [];
339
+ for (const id of jobIds) {
340
+ const job = this.jobsById.get(id);
341
+ if (!job) continue;
342
+ if (job.name === jobName && job.status === "paused") job.status = "waiting";
347
343
  }
348
- const start = options?.start ?? 0;
349
- const end = options?.end ?? results.length;
350
- return results.slice(start, end);
344
+ void this.kickWorkers(queue);
351
345
  }
352
- // ==========================================
353
- // PAUSE/RESUME JOB TYPES
354
- // ==========================================
355
- async pauseJobType(queue, jobName) {
356
- const q = this.getOrCreateQueue(queue);
357
- q.pausedJobTypes.add(jobName);
346
+ async searchJobs(filter) {
347
+ const queue = filter?.queue;
348
+ const statuses = filter?.status;
349
+ const limit = filter?.limit ?? 100;
350
+ const offset = filter?.offset ?? 0;
351
+ const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort((a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime());
352
+ return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
358
353
  }
359
- async resumeJobType(queue, jobName) {
360
- const q = this.getOrCreateQueue(queue);
361
- q.pausedJobTypes.delete(jobName);
362
- }
363
- // ==========================================
364
- // EVENTS
365
- // ==========================================
366
- async subscribe(pattern, handler) {
367
- if (!this.eventHandlers.has(pattern)) {
368
- this.eventHandlers.set(pattern, /* @__PURE__ */ new Set());
369
- }
370
- this.eventHandlers.get(pattern).add(handler);
354
+ async searchQueues(filter) {
355
+ const name = filter?.name;
356
+ const isPaused = filter?.isPaused;
357
+ const all = await this.listQueues();
358
+ return all.filter((q) => name ? q.name.includes(name) : true).filter((q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true);
359
+ }
360
+ async searchWorkers(filter) {
361
+ const queue = filter?.queue;
362
+ const isRunning = filter?.isRunning;
363
+ return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter((w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true).map((w) => this.toWorkerHandle(w));
364
+ }
365
+ async createWorker(config) {
366
+ const workerId = IgniterJobsIdGenerator.generate("worker");
367
+ const state = {
368
+ id: workerId,
369
+ queues: config.queues ?? [],
370
+ concurrency: config.concurrency ?? 1,
371
+ paused: false,
372
+ closed: false,
373
+ startedAt: /* @__PURE__ */ new Date(),
374
+ metrics: { processed: 0, failed: 0, totalDuration: 0 },
375
+ handlers: config.handlers
376
+ };
377
+ this.workers.set(workerId, state);
378
+ for (const q of state.queues) void this.kickWorkers(q);
379
+ return this.toWorkerHandle(state);
380
+ }
381
+ getWorkers() {
382
+ const out = /* @__PURE__ */ new Map();
383
+ for (const [id, state] of this.workers) out.set(id, this.toWorkerHandle(state));
384
+ return out;
385
+ }
386
+ async publishEvent(channel, payload) {
387
+ const handlers = this.subscribers.get(channel);
388
+ if (!handlers) return;
389
+ await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
390
+ }
391
+ async subscribeEvent(channel, handler) {
392
+ const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
393
+ set.add(handler);
394
+ this.subscribers.set(channel, set);
371
395
  return async () => {
372
- this.eventHandlers.get(pattern)?.delete(handler);
396
+ const current = this.subscribers.get(channel);
397
+ if (!current) return;
398
+ current.delete(handler);
399
+ if (current.size === 0) this.subscribers.delete(channel);
373
400
  };
374
401
  }
375
- // ==========================================
376
- // WORKERS
377
- // ==========================================
378
- async createWorker(config, handler) {
379
- const workerId = `worker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
380
- this.workers.set(workerId, {
381
- handler,
382
- config,
383
- running: true,
384
- paused: false
385
- });
386
- const startTime = Date.now();
387
- let processed = 0;
388
- let failed = 0;
389
- let completed = 0;
402
+ async shutdown() {
403
+ this.workers.clear();
404
+ this.subscribers.clear();
405
+ }
406
+ toSearchResult(job) {
390
407
  return {
391
- id: workerId,
408
+ id: job.id,
409
+ name: job.name,
410
+ queue: job.queue,
411
+ status: job.status,
412
+ input: job.input,
413
+ result: job.result,
414
+ error: job.error,
415
+ progress: job.progress,
416
+ attemptsMade: job.attemptsMade,
417
+ priority: job.priority,
418
+ createdAt: job.createdAt,
419
+ startedAt: job.startedAt,
420
+ completedAt: job.completedAt,
421
+ metadata: job.metadata,
422
+ scope: job.scope
423
+ };
424
+ }
425
+ toWorkerHandle(worker) {
426
+ return {
427
+ id: worker.id,
428
+ queues: worker.queues,
392
429
  pause: async () => {
393
- const worker = this.workers.get(workerId);
394
- if (worker) worker.paused = true;
430
+ worker.paused = true;
395
431
  },
396
432
  resume: async () => {
397
- const worker = this.workers.get(workerId);
398
- if (worker) worker.paused = false;
433
+ worker.paused = false;
434
+ for (const q of worker.queues) void this.kickWorkers(q);
399
435
  },
400
436
  close: async () => {
401
- this.workers.delete(workerId);
437
+ worker.closed = true;
402
438
  },
403
- isRunning: () => {
404
- const worker = this.workers.get(workerId);
405
- return worker?.running && !worker?.paused || false;
406
- },
407
- isPaused: () => {
408
- return this.workers.get(workerId)?.paused || false;
409
- },
410
- getMetrics: async () => ({
411
- processed,
412
- failed,
413
- completed,
414
- active: 0,
415
- uptime: Date.now() - startTime
416
- })
439
+ isRunning: () => !worker.closed && !worker.paused,
440
+ isPaused: () => worker.paused,
441
+ isClosed: () => worker.closed,
442
+ getMetrics: async () => this.toWorkerMetrics(worker)
417
443
  };
418
444
  }
419
- /**
420
- * Start the internal job processing loop.
421
- */
422
- startProcessing() {
423
- if (this.processingInterval) return;
424
- this.processingInterval = setInterval(async () => {
425
- await this.processJobs();
426
- }, 100);
445
+ toWorkerMetrics(worker) {
446
+ const uptime = Date.now() - worker.startedAt.getTime();
447
+ const processed = worker.metrics.processed;
448
+ return {
449
+ processed,
450
+ failed: worker.metrics.failed,
451
+ avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
452
+ concurrency: worker.concurrency,
453
+ uptime
454
+ };
427
455
  }
428
- /**
429
- * Process pending jobs.
430
- */
431
- async processJobs() {
432
- for (const [_, worker] of this.workers) {
433
- if (!worker.running || worker.paused) continue;
434
- for (const queueName of worker.config.queues) {
435
- const queue = this.queues.get(queueName);
436
- if (!queue || queue.isPaused) continue;
437
- for (const job of queue.jobs.values()) {
438
- if (job.state !== "waiting") continue;
439
- if (queue.pausedJobTypes.has(job.name)) continue;
440
- if (job.scheduledAt && job.scheduledAt > Date.now()) continue;
441
- job.state = "active";
442
- job.processedOn = Date.now();
443
- job.attempts++;
444
- try {
445
- const result = await worker.handler({
446
- id: job.id,
447
- name: job.name,
448
- queue: job.queue,
449
- data: job.data,
450
- attempt: job.attempts,
451
- timestamp: job.timestamp,
452
- scope: job.scope,
453
- actor: job.actor,
454
- log: async (level, message) => {
455
- job.logs.push({
456
- timestamp: /* @__PURE__ */ new Date(),
457
- message,
458
- level
459
- });
460
- },
461
- updateProgress: async (progress) => {
462
- job.progress = progress;
463
- }
464
- });
465
- job.state = "completed";
466
- job.result = result;
467
- job.finishedOn = Date.now();
468
- await this.emitEvent(`${job.queue}:${job.name}:completed`, {
469
- jobId: job.id,
470
- result
471
- });
472
- } catch (error) {
473
- const errorMessage = error instanceof Error ? error.message : String(error);
474
- if (job.attempts < job.maxAttempts) {
475
- job.state = "waiting";
476
- await this.emitEvent(`${job.queue}:${job.name}:retrying`, {
477
- jobId: job.id,
478
- error: errorMessage,
479
- attempt: job.attempts
480
- });
481
- } else {
482
- job.state = "failed";
483
- job.error = errorMessage;
484
- job.finishedOn = Date.now();
485
- await this.emitEvent(`${job.queue}:${job.name}:failed`, {
486
- jobId: job.id,
487
- error: errorMessage
488
- });
489
- }
490
- }
491
- }
492
- }
456
+ async kickWorkers(queue) {
457
+ if (this.pausedQueues.has(queue)) return;
458
+ const relevant = Array.from(this.workers.values()).filter((w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue)));
459
+ if (relevant.length === 0) return;
460
+ for (const w of relevant) {
461
+ void this.processLoop(w, queue);
493
462
  }
494
463
  }
495
- // ==========================================
496
- // SEARCH
497
- // ==========================================
498
- async searchJobs(filter) {
499
- const results = [];
500
- const queuesToSearch = filter.queue ? [filter.queue] : Array.from(this.queues.keys());
501
- for (const queueName of queuesToSearch) {
502
- const queue = this.queues.get(queueName);
503
- if (!queue) continue;
504
- for (const job of queue.jobs.values()) {
505
- if (filter.status && !filter.status.includes(job.state)) continue;
506
- if (filter.jobName && job.name !== filter.jobName) continue;
507
- if (filter.scopeId && job.scope?.id !== filter.scopeId) continue;
508
- if (filter.actorId && job.actor?.id !== filter.actorId) continue;
509
- if (filter.dateRange?.from && job.timestamp < filter.dateRange.from.getTime()) continue;
510
- if (filter.dateRange?.to && job.timestamp > filter.dateRange.to.getTime()) continue;
511
- results.push({
512
- id: job.id,
513
- name: job.name,
514
- queue: job.queue,
515
- state: job.state,
516
- data: job.data,
517
- result: job.result,
518
- error: job.error,
519
- progress: job.progress,
520
- attempts: job.attempts,
521
- timestamp: job.timestamp,
522
- processedOn: job.processedOn,
523
- finishedOn: job.finishedOn,
464
+ async processLoop(worker, queue) {
465
+ if (worker.closed || worker.paused) return;
466
+ const concurrency = Math.max(1, worker.concurrency);
467
+ const running = worker.__running;
468
+ const currentRunning = running ?? 0;
469
+ if (currentRunning >= concurrency) return;
470
+ worker.__running = currentRunning + 1;
471
+ try {
472
+ const next = this.nextJob(queue);
473
+ if (!next) return;
474
+ await this.processJob(worker, next);
475
+ } finally {
476
+ worker.__running = worker.__running - 1;
477
+ if (this.nextJob(queue)) void this.processLoop(worker, queue);
478
+ else if (worker.handlers?.onIdle) await worker.handlers.onIdle();
479
+ }
480
+ }
481
+ nextJob(queue) {
482
+ const ids = this.jobsByQueue.get(queue) ?? [];
483
+ const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort((a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime());
484
+ return candidates[0] ?? null;
485
+ }
486
+ async processJob(worker, job) {
487
+ if (this.pausedQueues.has(job.queue)) {
488
+ job.status = "paused";
489
+ return;
490
+ }
491
+ job.status = "active";
492
+ job.startedAt = /* @__PURE__ */ new Date();
493
+ job.attemptsMade += 1;
494
+ job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: "Job started" });
495
+ if (worker.handlers?.onActive) await worker.handlers.onActive({ job: this.toSearchResult(job) });
496
+ const start = Date.now();
497
+ try {
498
+ const definition = this.registeredJobs.get(job.queue)?.get(job.name);
499
+ if (!definition) {
500
+ throw new IgniterJobsError({
501
+ code: "JOBS_NOT_REGISTERED",
502
+ message: `Job "${job.name}" is not registered for queue "${job.queue}".`
503
+ });
504
+ }
505
+ if (definition.onStart) {
506
+ await definition.onStart({
507
+ input: job.input,
508
+ context: {},
509
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
524
510
  scope: job.scope,
525
- actor: job.actor
511
+ startedAt: job.startedAt
526
512
  });
527
513
  }
528
- }
529
- if (filter.orderBy) {
530
- const [field, direction] = filter.orderBy.split(":");
531
- results.sort((a, b) => {
532
- const aVal = field === "createdAt" ? a.timestamp : a[field];
533
- const bVal = field === "createdAt" ? b.timestamp : b[field];
534
- return direction === "asc" ? aVal - bVal : bVal - aVal;
535
- });
536
- }
537
- const offset = filter.offset ?? 0;
538
- const limit = filter.limit ?? 100;
539
- return results.slice(offset, offset + limit);
540
- }
541
- async searchQueues(filter) {
542
- const results = [];
543
- for (const [queueName, queue] of this.queues) {
544
- if (filter.name && !queueName.includes(filter.name)) continue;
545
- if (filter.isPaused !== void 0 && queue.isPaused !== filter.isPaused) continue;
546
- const counts = await this.getJobCounts(queueName);
547
- results.push({
548
- name: queueName,
549
- isPaused: queue.isPaused,
550
- jobCounts: counts
514
+ const result = await definition.handler({
515
+ input: job.input,
516
+ context: {},
517
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
518
+ scope: job.scope
551
519
  });
520
+ const duration = Date.now() - start;
521
+ job.status = "completed";
522
+ job.completedAt = /* @__PURE__ */ new Date();
523
+ job.result = result;
524
+ job.progress = 100;
525
+ job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: `Job completed in ${duration}ms` });
526
+ worker.metrics.processed += 1;
527
+ worker.metrics.totalDuration += duration;
528
+ if (definition.onSuccess) {
529
+ await definition.onSuccess({
530
+ input: job.input,
531
+ context: {},
532
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
533
+ scope: job.scope,
534
+ result,
535
+ duration
536
+ });
537
+ }
538
+ if (worker.handlers?.onSuccess) await worker.handlers.onSuccess({ job: this.toSearchResult(job), result });
539
+ } catch (error) {
540
+ job.error = error?.message ?? String(error);
541
+ job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "error", message: job.error ?? "Unknown error" });
542
+ const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
543
+ if (isFinalAttempt) {
544
+ job.status = "failed";
545
+ job.completedAt = /* @__PURE__ */ new Date();
546
+ worker.metrics.failed += 1;
547
+ const definition = this.registeredJobs.get(job.queue)?.get(job.name);
548
+ if (definition?.onFailure) {
549
+ await definition.onFailure({
550
+ input: job.input,
551
+ context: {},
552
+ job: { id: job.id, name: job.name, queue: job.queue, attemptsMade: job.attemptsMade, metadata: job.metadata },
553
+ scope: job.scope,
554
+ error,
555
+ isFinalAttempt: true
556
+ });
557
+ }
558
+ if (worker.handlers?.onFailure) await worker.handlers.onFailure({ job: this.toSearchResult(job), error });
559
+ } else {
560
+ job.status = "waiting";
561
+ void this.kickWorkers(job.queue);
562
+ }
552
563
  }
553
- return results;
554
- }
555
- // ==========================================
556
- // LIFECYCLE
557
- // ==========================================
558
- async shutdown() {
559
- if (this.processingInterval) {
560
- clearInterval(this.processingInterval);
561
- this.processingInterval = null;
562
- }
563
- this.workers.clear();
564
- this.queues.clear();
565
- this.eventHandlers.clear();
566
564
  }
567
565
  };
568
566
 
569
- exports.MemoryAdapter = MemoryAdapter;
567
+ exports.IgniterJobsMemoryAdapter = IgniterJobsMemoryAdapter;
570
568
  //# sourceMappingURL=memory.adapter.js.map
571
569
  //# sourceMappingURL=memory.adapter.js.map