@igniter-js/jobs 0.1.0

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.
@@ -0,0 +1,1129 @@
1
+ 'use strict';
2
+
3
+ var core = require('@igniter-js/core');
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
+ }
24
+ /**
25
+ * Convert error to a plain object for serialization.
26
+ */
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
+ };
36
+ }
37
+ };
38
+
39
+ // src/adapters/bullmq.adapter.ts
40
+ var BullMQAdapter = class _BullMQAdapter {
41
+ constructor(options) {
42
+ this.queues = /* @__PURE__ */ new Map();
43
+ this.workers = /* @__PURE__ */ new Map();
44
+ this.queueEvents = /* @__PURE__ */ new Map();
45
+ this.BullMQ = null;
46
+ this.redis = options.redis;
47
+ }
48
+ /**
49
+ * Create a new BullMQ adapter.
50
+ *
51
+ * @param options - Adapter options with Redis connection
52
+ * @returns A new BullMQAdapter instance
53
+ */
54
+ 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;
71
+ }
72
+ /**
73
+ * Get or create a queue instance.
74
+ */
75
+ async getOrCreateQueue(name) {
76
+ if (!this.queues.has(name)) {
77
+ const { Queue } = await this.getBullMQ();
78
+ const queue = new Queue(name, {
79
+ connection: this.redis
80
+ });
81
+ this.queues.set(name, queue);
82
+ }
83
+ return this.queues.get(name);
84
+ }
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
93
+ });
94
+ this.queueEvents.set(name, events);
95
+ }
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
+ };
138
+ }
139
+ // ==========================================
140
+ // JOB OPERATIONS
141
+ // ==========================================
142
+ async dispatch(params) {
143
+ const queue = await this.getOrCreateQueue(params.queue);
144
+ const jobOptions = {
145
+ jobId: params.jobId,
146
+ delay: params.delay,
147
+ 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,
153
+ 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;
163
+ }
164
+ async schedule(params) {
165
+ const queue = await this.getOrCreateQueue(params.queue);
166
+ const jobOptions = {
167
+ jobId: params.jobId,
168
+ delay: params.at ? params.at.getTime() - Date.now() : params.delay,
169
+ 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,
175
+ removeOnComplete: params.removeOnComplete,
176
+ removeOnFail: params.removeOnFail,
177
+ repeat: params.cron ? {
178
+ pattern: params.cron,
179
+ tz: params.timezone
180
+ } : params.every ? {
181
+ every: params.every
182
+ } : void 0
183
+ };
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;
191
+ }
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);
197
+ }
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);
204
+ }
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;
210
+ }
211
+ async getJobLogs(queue, jobId) {
212
+ const q = await this.getOrCreateQueue(queue);
213
+ const job = await q.getJob(jobId);
214
+ if (!job) return [];
215
+ const { logs } = await q.getJobLogs(jobId);
216
+ return logs.map((log, index) => ({
217
+ timestamp: /* @__PURE__ */ new Date(),
218
+ message: log,
219
+ level: "info"
220
+ }));
221
+ }
222
+ async retryJob(queue, jobId) {
223
+ const q = await this.getOrCreateQueue(queue);
224
+ const job = await q.getJob(jobId);
225
+ if (!job) {
226
+ throw new IgniterJobsError({
227
+ code: "JOBS_JOB_NOT_FOUND",
228
+ message: `Job "${jobId}" not found in queue "${queue}"`,
229
+ statusCode: 404
230
+ });
231
+ }
232
+ await job.retry();
233
+ }
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();
239
+ }
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();
251
+ }
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
+ }
267
+ }
268
+ async retryJobs(queue, jobIds) {
269
+ const q = await this.getOrCreateQueue(queue);
270
+ await Promise.all(
271
+ jobIds.map(async (jobId) => {
272
+ const job = await q.getJob(jobId);
273
+ if (job) await job.retry();
274
+ })
275
+ );
276
+ }
277
+ async removeJobs(queue, jobIds) {
278
+ const q = await this.getOrCreateQueue(queue);
279
+ await Promise.all(
280
+ jobIds.map(async (jobId) => {
281
+ const job = await q.getJob(jobId);
282
+ if (job) await job.remove();
283
+ })
284
+ );
285
+ }
286
+ // ==========================================
287
+ // QUEUE OPERATIONS
288
+ // ==========================================
289
+ async getQueue(queue) {
290
+ const q = await this.getOrCreateQueue(queue);
291
+ const isPaused = await q.isPaused();
292
+ const counts = await q.getJobCounts();
293
+ return {
294
+ name: queue,
295
+ isPaused,
296
+ jobCounts: {
297
+ waiting: counts.waiting || 0,
298
+ active: counts.active || 0,
299
+ completed: counts.completed || 0,
300
+ failed: counts.failed || 0,
301
+ delayed: counts.delayed || 0,
302
+ paused: counts.paused || 0
303
+ }
304
+ };
305
+ }
306
+ async pauseQueue(queue) {
307
+ const q = await this.getOrCreateQueue(queue);
308
+ await q.pause();
309
+ }
310
+ async resumeQueue(queue) {
311
+ const q = await this.getOrCreateQueue(queue);
312
+ await q.resume();
313
+ }
314
+ async drainQueue(queue) {
315
+ const q = await this.getOrCreateQueue(queue);
316
+ await q.drain();
317
+ return 0;
318
+ }
319
+ 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;
332
+ }
333
+ 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
+ };
354
+ }
355
+ async listJobs(queue, options) {
356
+ const q = await this.getOrCreateQueue(queue);
357
+ const statuses = options?.status || ["waiting", "active", "completed", "failed", "delayed"];
358
+ const jobs = [];
359
+ for (const status of statuses) {
360
+ const statusJobs = await q.getJobs([status], options?.start, options?.end);
361
+ jobs.push(...statusJobs);
362
+ }
363
+ return Promise.all(jobs.map(async (job) => {
364
+ const data = job.data;
365
+ const state = await job.getState();
366
+ return {
367
+ id: job.id,
368
+ name: job.name,
369
+ queue: job.queueName,
370
+ state: this.mapJobState(state || "waiting"),
371
+ data: data.input ?? data,
372
+ result: job.returnvalue,
373
+ error: job.failedReason,
374
+ progress: typeof job.progress === "number" ? job.progress : 0,
375
+ attempts: job.attemptsMade,
376
+ timestamp: job.timestamp,
377
+ processedOn: job.processedOn,
378
+ finishedOn: job.finishedOn,
379
+ scope: data.scope,
380
+ actor: data.actor
381
+ };
382
+ }));
383
+ }
384
+ // ==========================================
385
+ // PAUSE/RESUME JOB TYPES
386
+ // ==========================================
387
+ async pauseJobType(queue, jobName) {
388
+ this.config?.logger?.warn?.(`pauseJobType is not fully supported in BullMQ adapter`);
389
+ }
390
+ 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
+ });
424
+ }
425
+ return async () => {
426
+ await Promise.all(subscriptions.map((unsub) => unsub()));
427
+ };
428
+ }
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++;
492
+ });
493
+ }
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);
511
+ }
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
535
+ });
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);
543
+ }
544
+ }
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;
551
+ });
552
+ }
553
+ return results.slice(0, filter.limit || 100);
554
+ }
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
+ }
573
+ });
574
+ }
575
+ return results;
576
+ }
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();
593
+ }
594
+ };
595
+
596
+ // src/adapters/memory.adapter.ts
597
+ var MemoryAdapter = class _MemoryAdapter {
598
+ constructor() {
599
+ this.queues = /* @__PURE__ */ new Map();
600
+ this.eventHandlers = /* @__PURE__ */ new Map();
601
+ this.workers = /* @__PURE__ */ new Map();
602
+ this.processingInterval = null;
603
+ }
604
+ /**
605
+ * Create a new memory adapter.
606
+ */
607
+ static create() {
608
+ return new _MemoryAdapter();
609
+ }
610
+ /**
611
+ * Get the underlying client (null for memory adapter).
612
+ */
613
+ get client() {
614
+ return null;
615
+ }
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()
626
+ });
627
+ }
628
+ return this.queues.get(name);
629
+ }
630
+ /**
631
+ * Generate a unique job ID.
632
+ */
633
+ generateJobId() {
634
+ return `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
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
+ }
653
+ }
654
+ }
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
+ async dispatch(params) {
669
+ const queue = this.getOrCreateQueue(params.queue);
670
+ const jobId = params.jobId || this.generateJobId();
671
+ const job = {
672
+ id: jobId,
673
+ name: params.name,
674
+ queue: params.queue,
675
+ data: params.data,
676
+ state: params.delay ? "delayed" : "waiting",
677
+ progress: 0,
678
+ attempts: 0,
679
+ maxAttempts: params.attempts ?? 3,
680
+ timestamp: Date.now(),
681
+ delay: params.delay,
682
+ priority: params.priority ?? 0,
683
+ scope: params.scope,
684
+ actor: params.actor,
685
+ logs: [],
686
+ scheduledAt: params.delay ? Date.now() + params.delay : void 0
687
+ };
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();
695
+ return jobId;
696
+ }
697
+ 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
+ });
703
+ }
704
+ async getJob(queue, jobId) {
705
+ const q = this.getOrCreateQueue(queue);
706
+ const job = q.jobs.get(jobId);
707
+ 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
+ };
726
+ }
727
+ async getJobState(queue, jobId) {
728
+ const q = this.getOrCreateQueue(queue);
729
+ const job = q.jobs.get(jobId);
730
+ return job?.state ?? null;
731
+ }
732
+ async getJobProgress(queue, jobId) {
733
+ const q = this.getOrCreateQueue(queue);
734
+ const job = q.jobs.get(jobId);
735
+ return job?.progress ?? 0;
736
+ }
737
+ async getJobLogs(queue, jobId) {
738
+ const q = this.getOrCreateQueue(queue);
739
+ const job = q.jobs.get(jobId);
740
+ return job?.logs ?? [];
741
+ }
742
+ async retryJob(queue, jobId) {
743
+ const q = this.getOrCreateQueue(queue);
744
+ const job = q.jobs.get(jobId);
745
+ if (!job) {
746
+ throw new IgniterJobsError({
747
+ code: "JOBS_JOB_NOT_FOUND",
748
+ message: `Job "${jobId}" not found`,
749
+ statusCode: 404
750
+ });
751
+ }
752
+ job.state = "waiting";
753
+ job.error = void 0;
754
+ job.attempts = 0;
755
+ }
756
+ async removeJob(queue, jobId) {
757
+ const q = this.getOrCreateQueue(queue);
758
+ q.jobs.delete(jobId);
759
+ }
760
+ async promoteJob(queue, jobId) {
761
+ const q = this.getOrCreateQueue(queue);
762
+ const job = q.jobs.get(jobId);
763
+ if (!job) {
764
+ throw new IgniterJobsError({
765
+ code: "JOBS_JOB_NOT_FOUND",
766
+ message: `Job "${jobId}" not found`,
767
+ statusCode: 404
768
+ });
769
+ }
770
+ if (job.state === "delayed") {
771
+ job.state = "waiting";
772
+ job.scheduledAt = void 0;
773
+ job.delay = void 0;
774
+ }
775
+ }
776
+ async moveJob(queue, jobId, state, reason) {
777
+ const q = this.getOrCreateQueue(queue);
778
+ const job = q.jobs.get(jobId);
779
+ if (!job) {
780
+ throw new IgniterJobsError({
781
+ code: "JOBS_JOB_NOT_FOUND",
782
+ message: `Job "${jobId}" not found`,
783
+ statusCode: 404
784
+ });
785
+ }
786
+ job.state = state;
787
+ job.finishedOn = Date.now();
788
+ if (state === "failed") {
789
+ job.error = reason;
790
+ } else {
791
+ job.result = reason;
792
+ }
793
+ }
794
+ async retryJobs(queue, jobIds) {
795
+ await Promise.all(jobIds.map((id) => this.retryJob(queue, id)));
796
+ }
797
+ async removeJobs(queue, jobIds) {
798
+ await Promise.all(jobIds.map((id) => this.removeJob(queue, id)));
799
+ }
800
+ // ==========================================
801
+ // QUEUE OPERATIONS
802
+ // ==========================================
803
+ async getQueue(queue) {
804
+ const q = this.getOrCreateQueue(queue);
805
+ const counts = await this.getJobCounts(queue);
806
+ return {
807
+ name: queue,
808
+ isPaused: q.isPaused,
809
+ jobCounts: counts
810
+ };
811
+ }
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++;
827
+ }
828
+ }
829
+ return count;
830
+ }
831
+ async cleanQueue(queue, options) {
832
+ const q = this.getOrCreateQueue(queue);
833
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
834
+ const now = Date.now();
835
+ let count = 0;
836
+ let removed = 0;
837
+ for (const [id, job] of q.jobs) {
838
+ if (options.limit && removed >= options.limit) break;
839
+ if (statuses.includes(job.state)) {
840
+ const age = now - job.timestamp;
841
+ if (!options.olderThan || age >= options.olderThan) {
842
+ q.jobs.delete(id);
843
+ removed++;
844
+ count++;
845
+ }
846
+ }
847
+ }
848
+ return count;
849
+ }
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++;
862
+ }
863
+ }
864
+ return count;
865
+ }
866
+ async getJobCounts(queue) {
867
+ const q = this.getOrCreateQueue(queue);
868
+ const counts = {
869
+ waiting: 0,
870
+ active: 0,
871
+ completed: 0,
872
+ failed: 0,
873
+ delayed: 0,
874
+ paused: 0
875
+ };
876
+ for (const job of q.jobs.values()) {
877
+ counts[job.state]++;
878
+ }
879
+ return counts;
880
+ }
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
+ }
904
+ }
905
+ const start = options?.start ?? 0;
906
+ const end = options?.end ?? results.length;
907
+ return results.slice(start, end);
908
+ }
909
+ // ==========================================
910
+ // PAUSE/RESUME JOB TYPES
911
+ // ==========================================
912
+ async pauseJobType(queue, jobName) {
913
+ const q = this.getOrCreateQueue(queue);
914
+ q.pausedJobTypes.add(jobName);
915
+ }
916
+ async resumeJobType(queue, jobName) {
917
+ const q = this.getOrCreateQueue(queue);
918
+ q.pausedJobTypes.delete(jobName);
919
+ }
920
+ // ==========================================
921
+ // EVENTS
922
+ // ==========================================
923
+ async subscribe(pattern, handler) {
924
+ if (!this.eventHandlers.has(pattern)) {
925
+ this.eventHandlers.set(pattern, /* @__PURE__ */ new Set());
926
+ }
927
+ this.eventHandlers.get(pattern).add(handler);
928
+ return async () => {
929
+ this.eventHandlers.get(pattern)?.delete(handler);
930
+ };
931
+ }
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;
947
+ return {
948
+ id: workerId,
949
+ pause: async () => {
950
+ const worker = this.workers.get(workerId);
951
+ if (worker) worker.paused = true;
952
+ },
953
+ resume: async () => {
954
+ const worker = this.workers.get(workerId);
955
+ if (worker) worker.paused = false;
956
+ },
957
+ 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;
966
+ },
967
+ getMetrics: async () => ({
968
+ processed,
969
+ failed,
970
+ completed,
971
+ active: 0,
972
+ uptime: Date.now() - startTime
973
+ })
974
+ };
975
+ }
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);
984
+ }
985
+ /**
986
+ * Process pending jobs.
987
+ */
988
+ async processJobs() {
989
+ for (const [_, worker] of this.workers) {
990
+ if (!worker.running || worker.paused) continue;
991
+ for (const queueName of worker.config.queues) {
992
+ const queue = this.queues.get(queueName);
993
+ if (!queue || queue.isPaused) continue;
994
+ for (const job of queue.jobs.values()) {
995
+ if (job.state !== "waiting") continue;
996
+ if (queue.pausedJobTypes.has(job.name)) continue;
997
+ if (job.scheduledAt && job.scheduledAt > Date.now()) continue;
998
+ job.state = "active";
999
+ job.processedOn = Date.now();
1000
+ job.attempts++;
1001
+ try {
1002
+ const result = await worker.handler({
1003
+ id: job.id,
1004
+ name: job.name,
1005
+ queue: job.queue,
1006
+ data: job.data,
1007
+ attempt: job.attempts,
1008
+ timestamp: job.timestamp,
1009
+ scope: job.scope,
1010
+ actor: job.actor,
1011
+ log: async (level, message) => {
1012
+ job.logs.push({
1013
+ timestamp: /* @__PURE__ */ new Date(),
1014
+ message,
1015
+ level
1016
+ });
1017
+ },
1018
+ updateProgress: async (progress) => {
1019
+ job.progress = progress;
1020
+ }
1021
+ });
1022
+ job.state = "completed";
1023
+ job.result = result;
1024
+ job.finishedOn = Date.now();
1025
+ await this.emitEvent(`${job.queue}:${job.name}:completed`, {
1026
+ jobId: job.id,
1027
+ result
1028
+ });
1029
+ } catch (error) {
1030
+ const errorMessage = error instanceof Error ? error.message : String(error);
1031
+ if (job.attempts < job.maxAttempts) {
1032
+ job.state = "waiting";
1033
+ await this.emitEvent(`${job.queue}:${job.name}:retrying`, {
1034
+ jobId: job.id,
1035
+ error: errorMessage,
1036
+ attempt: job.attempts
1037
+ });
1038
+ } else {
1039
+ job.state = "failed";
1040
+ job.error = errorMessage;
1041
+ job.finishedOn = Date.now();
1042
+ await this.emitEvent(`${job.queue}:${job.name}:failed`, {
1043
+ jobId: job.id,
1044
+ error: errorMessage
1045
+ });
1046
+ }
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ // ==========================================
1053
+ // SEARCH
1054
+ // ==========================================
1055
+ async searchJobs(filter) {
1056
+ const results = [];
1057
+ const queuesToSearch = filter.queue ? [filter.queue] : Array.from(this.queues.keys());
1058
+ for (const queueName of queuesToSearch) {
1059
+ const queue = this.queues.get(queueName);
1060
+ if (!queue) continue;
1061
+ for (const job of queue.jobs.values()) {
1062
+ if (filter.status && !filter.status.includes(job.state)) continue;
1063
+ if (filter.jobName && job.name !== filter.jobName) continue;
1064
+ if (filter.scopeId && job.scope?.id !== filter.scopeId) continue;
1065
+ if (filter.actorId && job.actor?.id !== filter.actorId) continue;
1066
+ if (filter.dateRange?.from && job.timestamp < filter.dateRange.from.getTime()) continue;
1067
+ if (filter.dateRange?.to && job.timestamp > filter.dateRange.to.getTime()) continue;
1068
+ results.push({
1069
+ id: job.id,
1070
+ name: job.name,
1071
+ queue: job.queue,
1072
+ state: job.state,
1073
+ data: job.data,
1074
+ result: job.result,
1075
+ error: job.error,
1076
+ progress: job.progress,
1077
+ attempts: job.attempts,
1078
+ timestamp: job.timestamp,
1079
+ processedOn: job.processedOn,
1080
+ finishedOn: job.finishedOn,
1081
+ scope: job.scope,
1082
+ actor: job.actor
1083
+ });
1084
+ }
1085
+ }
1086
+ if (filter.orderBy) {
1087
+ const [field, direction] = filter.orderBy.split(":");
1088
+ results.sort((a, b) => {
1089
+ const aVal = field === "createdAt" ? a.timestamp : a[field];
1090
+ const bVal = field === "createdAt" ? b.timestamp : b[field];
1091
+ return direction === "asc" ? aVal - bVal : bVal - aVal;
1092
+ });
1093
+ }
1094
+ const offset = filter.offset ?? 0;
1095
+ const limit = filter.limit ?? 100;
1096
+ return results.slice(offset, offset + limit);
1097
+ }
1098
+ async searchQueues(filter) {
1099
+ const results = [];
1100
+ for (const [queueName, queue] of this.queues) {
1101
+ if (filter.name && !queueName.includes(filter.name)) continue;
1102
+ if (filter.isPaused !== void 0 && queue.isPaused !== filter.isPaused) continue;
1103
+ const counts = await this.getJobCounts(queueName);
1104
+ results.push({
1105
+ name: queueName,
1106
+ isPaused: queue.isPaused,
1107
+ jobCounts: counts
1108
+ });
1109
+ }
1110
+ return results;
1111
+ }
1112
+ // ==========================================
1113
+ // LIFECYCLE
1114
+ // ==========================================
1115
+ async shutdown() {
1116
+ if (this.processingInterval) {
1117
+ clearInterval(this.processingInterval);
1118
+ this.processingInterval = null;
1119
+ }
1120
+ this.workers.clear();
1121
+ this.queues.clear();
1122
+ this.eventHandlers.clear();
1123
+ }
1124
+ };
1125
+
1126
+ exports.BullMQAdapter = BullMQAdapter;
1127
+ exports.MemoryAdapter = MemoryAdapter;
1128
+ //# sourceMappingURL=index.js.map
1129
+ //# sourceMappingURL=index.js.map