light-async-queue 1.1.0 → 2.0.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.
Files changed (51) hide show
  1. package/README.md +314 -30
  2. package/dist/src/constants.d.ts +24 -5
  3. package/dist/src/constants.d.ts.map +1 -1
  4. package/dist/src/constants.js +20 -0
  5. package/dist/src/constants.js.map +1 -1
  6. package/dist/src/dashboard/Dashboard.d.ts +70 -0
  7. package/dist/src/dashboard/Dashboard.d.ts.map +1 -0
  8. package/dist/src/dashboard/Dashboard.js +308 -0
  9. package/dist/src/dashboard/Dashboard.js.map +1 -0
  10. package/dist/src/dashboard/index.d.ts +3 -0
  11. package/dist/src/dashboard/index.d.ts.map +1 -0
  12. package/dist/src/dashboard/index.js +2 -0
  13. package/dist/src/dashboard/index.js.map +1 -0
  14. package/dist/src/index.d.ts +13 -2
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +11 -1
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/queue/Job.d.ts +35 -4
  19. package/dist/src/queue/Job.d.ts.map +1 -1
  20. package/dist/src/queue/Job.js +92 -8
  21. package/dist/src/queue/Job.js.map +1 -1
  22. package/dist/src/queue/Queue.d.ts +73 -3
  23. package/dist/src/queue/Queue.d.ts.map +1 -1
  24. package/dist/src/queue/Queue.js +357 -35
  25. package/dist/src/queue/Queue.js.map +1 -1
  26. package/dist/src/queue/Scheduler.d.ts.map +1 -1
  27. package/dist/src/queue/Scheduler.js +8 -1
  28. package/dist/src/queue/Scheduler.js.map +1 -1
  29. package/dist/src/types.d.ts +79 -5
  30. package/dist/src/types.d.ts.map +1 -1
  31. package/dist/src/types.js +1 -1
  32. package/dist/src/types.js.map +1 -1
  33. package/dist/src/utils/CronParser.d.ts +12 -0
  34. package/dist/src/utils/CronParser.d.ts.map +1 -0
  35. package/dist/src/utils/CronParser.js +28 -0
  36. package/dist/src/utils/CronParser.js.map +1 -0
  37. package/dist/src/utils/RateLimiter.d.ts +37 -0
  38. package/dist/src/utils/RateLimiter.d.ts.map +1 -0
  39. package/dist/src/utils/RateLimiter.js +68 -0
  40. package/dist/src/utils/RateLimiter.js.map +1 -0
  41. package/dist/src/utils/WebhookManager.d.ts +29 -0
  42. package/dist/src/utils/WebhookManager.d.ts.map +1 -0
  43. package/dist/src/utils/WebhookManager.js +82 -0
  44. package/dist/src/utils/WebhookManager.js.map +1 -0
  45. package/dist/src/worker/Worker.d.ts +2 -2
  46. package/dist/src/worker/Worker.d.ts.map +1 -1
  47. package/dist/src/worker/Worker.js +57 -36
  48. package/dist/src/worker/Worker.js.map +1 -1
  49. package/dist/src/worker/childProcessor.js +17 -1
  50. package/dist/src/worker/childProcessor.js.map +1 -1
  51. package/package.json +27 -5
@@ -1,8 +1,9 @@
1
- import { QueueConfig, JobProcessor, JobData } from '../types.js';
1
+ import { QueueConfig, JobProcessor, JobData, JobOptions } from "../types.js";
2
+ import { EventEmitter } from "node:events";
2
3
  /**
3
4
  * Main Queue class - orchestrates job processing
4
5
  */
5
- export declare class Queue {
6
+ export declare class Queue extends EventEmitter {
6
7
  private config;
7
8
  private storage;
8
9
  private scheduler;
@@ -10,9 +11,15 @@ export declare class Queue {
10
11
  private backoff;
11
12
  private processor;
12
13
  private workers;
14
+ private reservedWorkers;
13
15
  private activeJobs;
16
+ private completedJobIds;
17
+ private repeatingJobs;
14
18
  private isShuttingDown;
15
19
  private isInitialized;
20
+ private rateLimiter?;
21
+ private webhookManager?;
22
+ private stalledCheckInterval?;
16
23
  constructor(config: QueueConfig);
17
24
  /**
18
25
  * Initialize the queue
@@ -25,7 +32,7 @@ export declare class Queue {
25
32
  /**
26
33
  * Add a job to the queue
27
34
  */
28
- add(payload: unknown): Promise<string>;
35
+ add(payload: unknown, options?: JobOptions): Promise<string>;
29
36
  /**
30
37
  * Handle a job that's ready to process
31
38
  */
@@ -38,6 +45,62 @@ export declare class Queue {
38
45
  * Get an available worker or create a new one
39
46
  */
40
47
  private getAvailableWorker;
48
+ /**
49
+ * Create a job with methods for use in processor
50
+ */
51
+ private createJobWithMethods;
52
+ /**
53
+ * Load completed job IDs for dependency tracking
54
+ */
55
+ private loadCompletedJobIds;
56
+ /**
57
+ * Check and update waiting jobs whose dependencies are now satisfied
58
+ */
59
+ private checkDependentJobs;
60
+ /**
61
+ * Schedule a repeating job
62
+ */
63
+ private scheduleRepeat;
64
+ /**
65
+ * Start stalled job checker
66
+ */
67
+ private startStalledChecker;
68
+ /**
69
+ * Check for stalled jobs and mark them
70
+ */
71
+ private checkStalledJobs;
72
+ /**
73
+ * Send webhook notification
74
+ */
75
+ private sendWebhook;
76
+ /**
77
+ * Get a specific job by ID
78
+ */
79
+ getJob(jobId: string): Promise<JobData | null>;
80
+ /**
81
+ * Get all jobs from the queue
82
+ */
83
+ getAllJobs(): Promise<JobData[]>;
84
+ /**
85
+ * Remove a specific job
86
+ */
87
+ removeJob(jobId: string): Promise<boolean>;
88
+ /**
89
+ * Pause the queue (stop processing new jobs)
90
+ */
91
+ pause(): void;
92
+ /**
93
+ * Resume the queue
94
+ */
95
+ resume(): void;
96
+ /**
97
+ * Drain the queue - process all pending jobs
98
+ */
99
+ drain(): Promise<void>;
100
+ /**
101
+ * Clean completed jobs older than a certain age
102
+ */
103
+ clean(maxAge?: number): Promise<number>;
41
104
  /**
42
105
  * Get all failed jobs from DLQ
43
106
  */
@@ -46,14 +109,21 @@ export declare class Queue {
46
109
  * Reprocess a failed job from DLQ
47
110
  */
48
111
  reprocessFailed(jobId: string): Promise<boolean>;
112
+ /**
113
+ * Get queue statistics
114
+ */
115
+ getConcurrency(): number;
49
116
  /**
50
117
  * Get queue statistics
51
118
  */
52
119
  getStats(): Promise<{
53
120
  active: number;
121
+ waiting: number;
122
+ delayed: number;
54
123
  pending: number;
55
124
  failed: number;
56
125
  completed: number;
126
+ stalled: number;
57
127
  }>;
58
128
  /**
59
129
  * Set up graceful shutdown handlers
@@ -1 +1 @@
1
- {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/queue/Queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAoB,OAAO,EAA0B,MAAM,aAAa,CAAC;AAS3G;;GAEG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,aAAa,CAAU;gBAEnB,MAAM,EAAE,WAAW;IAkC/B;;OAEG;YACW,UAAU;IASxB;;OAEG;IACH,OAAO,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI;IAItC;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB5C;;OAEG;YACW,cAAc;IAsD5B;;OAEG;YACW,gBAAgB;IAiB9B;;OAEG;YACW,kBAAkB;IAkChC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAOzC;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBtD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAgBF;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CA2BhC"}
1
+ {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/queue/Queue.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,YAAY,EAEZ,OAAO,EAGP,UAAU,EAGX,MAAM,aAAa,CAAC;AAWrB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;GAEG;AACH,qBAAa,KAAM,SAAQ,YAAY;IACrC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAc;IAClC,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,oBAAoB,CAAC,CAAiB;gBAElC,MAAM,EAAE,WAAW;IAmD/B;;OAEG;YACW,UAAU;IAexB;;OAEG;IACH,OAAO,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI;IAItC;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAkCtE;;OAEG;YACW,cAAc;IAuF5B;;OAEG;YACW,gBAAgB;IAwB9B;;OAEG;YACW,kBAAkB;IAqChC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;YACW,mBAAmB;IAUjC;;OAEG;YACW,kBAAkB;IAkBhC;;OAEG;YACW,cAAc;IAmD5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;OAEG;YACW,gBAAgB;IAoB9B;;OAEG;YACW,WAAW;IAazB;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAOpD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAOtC;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAsBhD;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,IAAI,IAAI;IAMd;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B5B;;OAEG;IACG,KAAK,CAAC,MAAM,GAAE,MAA4B,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBlE;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAOzC;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBtD;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IAmBF;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAwChC"}
@@ -1,15 +1,19 @@
1
- import { StorageType, JobStatus } from '../types.js';
2
- import { Job } from './Job.js';
3
- import { Backoff } from './Backoff.js';
4
- import { Scheduler } from './Scheduler.js';
5
- import { Worker } from '../worker/Worker.js';
6
- import { DeadLetterQueue } from '../dlq/DeadLetterQueue.js';
7
- import { MemoryStore } from '../storage/MemoryStore.js';
8
- import { FileStore } from '../storage/FileStore.js';
1
+ import { StorageType, JobStatus, QueueEventType, } from "../types.js";
2
+ import { Job } from "./Job.js";
3
+ import { Backoff } from "./Backoff.js";
4
+ import { Scheduler } from "./Scheduler.js";
5
+ import { Worker } from "../worker/Worker.js";
6
+ import { DeadLetterQueue } from "../dlq/DeadLetterQueue.js";
7
+ import { MemoryStore } from "../storage/MemoryStore.js";
8
+ import { FileStore } from "../storage/FileStore.js";
9
+ import { RateLimiter } from "../utils/RateLimiter.js";
10
+ import { WebhookManager } from "../utils/WebhookManager.js";
11
+ import { CronParser } from "../utils/CronParser.js";
12
+ import { EventEmitter } from "node:events";
9
13
  /**
10
14
  * Main Queue class - orchestrates job processing
11
15
  */
12
- export class Queue {
16
+ export class Queue extends EventEmitter {
13
17
  config;
14
18
  storage;
15
19
  scheduler;
@@ -17,14 +21,24 @@ export class Queue {
17
21
  backoff;
18
22
  processor;
19
23
  workers;
24
+ reservedWorkers;
20
25
  activeJobs;
26
+ completedJobIds;
27
+ repeatingJobs;
21
28
  isShuttingDown;
22
29
  isInitialized;
30
+ rateLimiter;
31
+ webhookManager;
32
+ stalledCheckInterval;
23
33
  constructor(config) {
34
+ super();
24
35
  this.config = config;
25
36
  this.processor = null;
26
37
  this.workers = [];
38
+ this.reservedWorkers = new Set();
27
39
  this.activeJobs = new Map();
40
+ this.completedJobIds = new Set();
41
+ this.repeatingJobs = new Map();
28
42
  this.isShuttingDown = false;
29
43
  this.isInitialized = false;
30
44
  // Initialize storage based on config
@@ -41,10 +55,19 @@ export class Queue {
41
55
  this.scheduler = new Scheduler(this.storage);
42
56
  this.dlq = new DeadLetterQueue(this.storage);
43
57
  this.backoff = new Backoff(config.retry.backoff);
58
+ // Initialize rate limiter if configured
59
+ if (config.rateLimiter) {
60
+ this.rateLimiter = new RateLimiter(config.rateLimiter);
61
+ }
62
+ // Initialize webhook manager if configured
63
+ if (config.webhooks && config.webhooks.length > 0) {
64
+ this.webhookManager = new WebhookManager(config.webhooks);
65
+ }
44
66
  // Set up scheduler event handler
45
- this.scheduler.on('job-ready', (jobData) => {
46
- this.handleJobReady(jobData).catch(error => {
47
- console.error('[Queue] Error handling job:', error);
67
+ this.scheduler.on("job-ready", (jobData) => {
68
+ this.handleJobReady(jobData).catch((error) => {
69
+ console.error("[Queue] Error handling job:", error);
70
+ this.emit(QueueEventType.ERROR, error);
48
71
  });
49
72
  });
50
73
  // Set up graceful shutdown
@@ -59,6 +82,10 @@ export class Queue {
59
82
  }
60
83
  await this.storage.initialize();
61
84
  this.isInitialized = true;
85
+ // Start stalled job checker
86
+ this.startStalledChecker();
87
+ // Load completed job IDs for dependency tracking
88
+ await this.loadCompletedJobIds();
62
89
  }
63
90
  /**
64
91
  * Set the job processor function
@@ -69,15 +96,28 @@ export class Queue {
69
96
  /**
70
97
  * Add a job to the queue
71
98
  */
72
- async add(payload) {
99
+ async add(payload, options = {}) {
73
100
  if (!this.isInitialized) {
74
101
  await this.initialize();
75
102
  }
76
103
  if (this.isShuttingDown) {
77
- throw new Error('Queue is shutting down, cannot accept new jobs');
104
+ throw new Error("Queue is shutting down, cannot accept new jobs");
78
105
  }
79
- const job = new Job(payload, this.config.retry.maxAttempts);
106
+ const job = new Job(payload, this.config.retry.maxAttempts, options);
80
107
  await this.storage.addJob(job.toData());
108
+ // Emit event based on job status
109
+ if (job.status === JobStatus.DELAYED) {
110
+ this.emit(QueueEventType.DELAYED, job.toData());
111
+ await this.sendWebhook(QueueEventType.DELAYED, { job: job.toData() });
112
+ }
113
+ else if (job.status === JobStatus.WAITING) {
114
+ this.emit(QueueEventType.WAITING, job.toData());
115
+ await this.sendWebhook(QueueEventType.WAITING, { job: job.toData() });
116
+ }
117
+ // Set up repeating job if configured
118
+ if (options.repeat) {
119
+ await this.scheduleRepeat(job);
120
+ }
81
121
  // Start scheduler if not already running
82
122
  if (!this.scheduler.getIsRunning()) {
83
123
  this.scheduler.start();
@@ -100,50 +140,85 @@ export class Queue {
100
140
  return;
101
141
  }
102
142
  if (!this.processor) {
103
- console.error('[Queue] No processor function set');
143
+ console.error("[Queue] No processor function set");
104
144
  return;
105
145
  }
106
146
  const job = Job.fromData(jobData);
147
+ // Check if dependencies are satisfied
148
+ if (!job.areDependenciesSatisfied(this.completedJobIds)) {
149
+ return; // Job is still waiting for dependencies
150
+ }
151
+ // Apply rate limiting if configured
152
+ if (this.rateLimiter) {
153
+ const allowed = await this.rateLimiter.consume();
154
+ if (!allowed) {
155
+ return; // Rate limit reached, will retry on next tick
156
+ }
157
+ }
107
158
  // Mark job as processing
108
159
  job.markProcessing();
109
160
  this.activeJobs.set(job.id, job);
110
161
  await this.storage.updateJob(job.toData());
162
+ // Emit active event
163
+ this.emit(QueueEventType.ACTIVE, job.toData());
164
+ await this.sendWebhook(QueueEventType.ACTIVE, { job: job.toData() });
111
165
  // Get or create a worker
112
166
  const worker = await this.getAvailableWorker();
113
167
  try {
168
+ // Create job with methods for processor
169
+ const jobWithMethods = this.createJobWithMethods(job);
114
170
  // Execute job in worker
115
- const result = await worker.execute(job.toData());
171
+ const result = await worker.execute(job.toData(), jobWithMethods);
116
172
  if (result.success) {
117
173
  // Job succeeded
118
- job.markCompleted();
174
+ job.markCompleted(result.result);
119
175
  await this.storage.updateJob(job.toData());
176
+ // Track completed job for dependency resolution
177
+ this.completedJobIds.add(job.id);
178
+ // Check for dependent jobs
179
+ await this.checkDependentJobs(job.id);
180
+ // Emit completed event
181
+ this.emit(QueueEventType.COMPLETED, job.toData(), result.result);
182
+ await this.sendWebhook(QueueEventType.COMPLETED, {
183
+ job: job.toData(),
184
+ result: result.result,
185
+ });
120
186
  }
121
187
  else {
122
188
  // Job failed
123
- await this.handleJobFailure(job, result.error || 'Unknown error');
189
+ const error = new Error(result.error || "Unknown error");
190
+ await this.handleJobFailure(job, error);
124
191
  }
125
192
  }
126
193
  catch (error) {
127
194
  // Worker execution error
128
- await this.handleJobFailure(job, error instanceof Error ? error.message : String(error));
195
+ const err = error instanceof Error ? error : new Error(String(error));
196
+ await this.handleJobFailure(job, err);
129
197
  }
130
198
  finally {
131
199
  // Remove from active jobs
132
200
  this.activeJobs.delete(job.id);
201
+ this.reservedWorkers.delete(worker);
133
202
  }
134
203
  }
135
204
  /**
136
205
  * Handle job failure with retry logic
137
206
  */
138
207
  async handleJobFailure(job, error) {
139
- console.error(`[Queue] Job ${job.id} failed:`, error);
208
+ console.error(`[Queue] Job ${job.id} failed:`, error.message);
140
209
  // Calculate next run time with backoff
141
210
  const nextRunAt = this.backoff.getNextRunAt(job.attempts + 1);
142
- job.markFailed(nextRunAt);
211
+ job.markFailed(error.message, nextRunAt);
143
212
  if (job.hasExceededMaxAttempts()) {
144
213
  // Move to dead letter queue
145
214
  console.log(`[Queue] Job ${job.id} exceeded max attempts, moving to DLQ`);
146
215
  await this.dlq.add(job);
216
+ // Emit failed event
217
+ this.emit(QueueEventType.FAILED, job.toData(), error);
218
+ await this.sendWebhook(QueueEventType.FAILED, {
219
+ job: job.toData(),
220
+ error,
221
+ });
147
222
  }
148
223
  else {
149
224
  // Update job for retry
@@ -156,26 +231,29 @@ export class Queue {
156
231
  async getAvailableWorker() {
157
232
  // Find an idle worker
158
233
  for (const worker of this.workers) {
159
- if (!worker.isBusy()) {
234
+ if (!worker.isBusy() && !this.reservedWorkers.has(worker)) {
235
+ this.reservedWorkers.add(worker);
160
236
  return worker;
161
237
  }
162
238
  }
163
239
  // Create a new worker if under concurrency limit
164
240
  if (this.workers.length < this.config.concurrency) {
165
241
  if (!this.processor) {
166
- throw new Error('Processor function not set');
242
+ throw new Error("Processor function not set");
167
243
  }
168
244
  const worker = new Worker(this.processor);
169
245
  await worker.initialize();
170
246
  this.workers.push(worker);
247
+ this.reservedWorkers.add(worker);
171
248
  return worker;
172
249
  }
173
250
  // Wait for a worker to become available
174
251
  return new Promise((resolve) => {
175
252
  const checkInterval = setInterval(() => {
176
253
  for (const worker of this.workers) {
177
- if (!worker.isBusy()) {
254
+ if (!worker.isBusy() && !this.reservedWorkers.has(worker)) {
178
255
  clearInterval(checkInterval);
256
+ this.reservedWorkers.add(worker);
179
257
  resolve(worker);
180
258
  return;
181
259
  }
@@ -183,6 +261,232 @@ export class Queue {
183
261
  }, 100);
184
262
  });
185
263
  }
264
+ /**
265
+ * Create a job with methods for use in processor
266
+ */
267
+ createJobWithMethods(job) {
268
+ return {
269
+ ...job.toData(),
270
+ updateProgress: async (progress) => {
271
+ job.updateProgress(progress);
272
+ await this.storage.updateJob(job.toData());
273
+ this.emit(QueueEventType.PROGRESS, job.toData(), progress);
274
+ await this.sendWebhook(QueueEventType.PROGRESS, { job: job.toData() });
275
+ },
276
+ log: (message) => {
277
+ console.log(`[Job ${job.id}] ${message}`);
278
+ },
279
+ };
280
+ }
281
+ /**
282
+ * Load completed job IDs for dependency tracking
283
+ */
284
+ async loadCompletedJobIds() {
285
+ const allJobs = await this.storage.getAllJobs();
286
+ this.completedJobIds.clear();
287
+ for (const job of allJobs) {
288
+ if (job.status === JobStatus.COMPLETED) {
289
+ this.completedJobIds.add(job.id);
290
+ }
291
+ }
292
+ }
293
+ /**
294
+ * Check and update waiting jobs whose dependencies are now satisfied
295
+ */
296
+ async checkDependentJobs(completedJobId) {
297
+ const allJobs = await this.storage.getAllJobs();
298
+ for (const jobData of allJobs) {
299
+ if (jobData.status === JobStatus.WAITING &&
300
+ jobData.dependsOn?.includes(completedJobId)) {
301
+ const job = Job.fromData(jobData);
302
+ if (job.areDependenciesSatisfied(this.completedJobIds)) {
303
+ // All dependencies satisfied, move to pending
304
+ job.status = JobStatus.PENDING;
305
+ await this.storage.updateJob(job.toData());
306
+ }
307
+ }
308
+ }
309
+ }
310
+ /**
311
+ * Schedule a repeating job
312
+ */
313
+ async scheduleRepeat(job) {
314
+ if (!job.repeatConfig) {
315
+ return;
316
+ }
317
+ const repeatConfig = job.repeatConfig;
318
+ // Check if we've hit the repeat limit
319
+ if (repeatConfig.limit && job.repeatCount >= repeatConfig.limit) {
320
+ return;
321
+ }
322
+ // Calculate next run time
323
+ let nextRunAt;
324
+ if (repeatConfig.pattern) {
325
+ // Cron pattern
326
+ const cronParser = new CronParser(repeatConfig.pattern);
327
+ nextRunAt = cronParser.getNextRunTime(Date.now());
328
+ }
329
+ else if (repeatConfig.every) {
330
+ // Repeat every X ms
331
+ nextRunAt = Date.now() + repeatConfig.every;
332
+ }
333
+ else {
334
+ return;
335
+ }
336
+ // Check date constraints
337
+ if (repeatConfig.startDate &&
338
+ nextRunAt < repeatConfig.startDate.getTime()) {
339
+ nextRunAt = repeatConfig.startDate.getTime();
340
+ }
341
+ if (repeatConfig.endDate && nextRunAt > repeatConfig.endDate.getTime()) {
342
+ return;
343
+ }
344
+ // Schedule the next instance
345
+ const delay = nextRunAt - Date.now();
346
+ const timeout = setTimeout(async () => {
347
+ const nextJob = job.createRepeatInstance();
348
+ nextJob.nextRunAt = nextRunAt;
349
+ await this.storage.addJob(nextJob.toData());
350
+ // Schedule the next repeat
351
+ await this.scheduleRepeat(nextJob);
352
+ }, delay);
353
+ this.repeatingJobs.set(job.id, timeout);
354
+ }
355
+ /**
356
+ * Start stalled job checker
357
+ */
358
+ startStalledChecker() {
359
+ const interval = this.config.stalledInterval || 30000;
360
+ this.stalledCheckInterval = setInterval(async () => {
361
+ await this.checkStalledJobs();
362
+ }, interval);
363
+ }
364
+ /**
365
+ * Check for stalled jobs and mark them
366
+ */
367
+ async checkStalledJobs() {
368
+ const stalledThreshold = this.config.stalledInterval || 30000;
369
+ const allJobs = await this.storage.getAllJobs();
370
+ for (const jobData of allJobs) {
371
+ if (jobData.status === JobStatus.PROCESSING) {
372
+ const job = Job.fromData(jobData);
373
+ if (job.isStalled(stalledThreshold)) {
374
+ console.warn(`[Queue] Job ${job.id} appears stalled`);
375
+ job.markStalled();
376
+ await this.storage.updateJob(job.toData());
377
+ // Emit stalled event
378
+ this.emit(QueueEventType.STALLED, job.toData());
379
+ await this.sendWebhook(QueueEventType.STALLED, { job: job.toData() });
380
+ }
381
+ }
382
+ }
383
+ }
384
+ /**
385
+ * Send webhook notification
386
+ */
387
+ async sendWebhook(event, data) {
388
+ if (this.webhookManager) {
389
+ try {
390
+ await this.webhookManager.sendEvent(event, data);
391
+ }
392
+ catch (error) {
393
+ console.error("[Queue] Webhook error:", error);
394
+ }
395
+ }
396
+ }
397
+ /**
398
+ * Get a specific job by ID
399
+ */
400
+ async getJob(jobId) {
401
+ if (!this.isInitialized) {
402
+ await this.initialize();
403
+ }
404
+ return this.storage.getJob(jobId);
405
+ }
406
+ /**
407
+ * Get all jobs from the queue
408
+ */
409
+ async getAllJobs() {
410
+ if (!this.isInitialized) {
411
+ await this.initialize();
412
+ }
413
+ return this.storage.getAllJobs();
414
+ }
415
+ /**
416
+ * Remove a specific job
417
+ */
418
+ async removeJob(jobId) {
419
+ if (!this.isInitialized) {
420
+ await this.initialize();
421
+ }
422
+ // Check if job is active
423
+ if (this.activeJobs.has(jobId)) {
424
+ return false; // Cannot remove active job
425
+ }
426
+ const job = await this.storage.getJob(jobId);
427
+ if (!job) {
428
+ return false;
429
+ }
430
+ // Remove from storage by updating status
431
+ job.status = JobStatus.FAILED;
432
+ await this.storage.updateJob(job);
433
+ return true;
434
+ }
435
+ /**
436
+ * Pause the queue (stop processing new jobs)
437
+ */
438
+ pause() {
439
+ this.scheduler.stop();
440
+ }
441
+ /**
442
+ * Resume the queue
443
+ */
444
+ resume() {
445
+ if (!this.isShuttingDown) {
446
+ this.scheduler.start();
447
+ }
448
+ }
449
+ /**
450
+ * Drain the queue - process all pending jobs
451
+ */
452
+ async drain() {
453
+ if (!this.isInitialized) {
454
+ await this.initialize();
455
+ }
456
+ // Wait for all pending jobs to be processed
457
+ while (true) {
458
+ const allJobs = await this.storage.getAllJobs();
459
+ const pendingJobs = allJobs.filter((j) => j.status === JobStatus.PENDING ||
460
+ j.status === JobStatus.WAITING ||
461
+ j.status === JobStatus.DELAYED);
462
+ if (pendingJobs.length === 0 && this.activeJobs.size === 0) {
463
+ break;
464
+ }
465
+ await new Promise((resolve) => setTimeout(resolve, 100));
466
+ }
467
+ this.emit(QueueEventType.DRAINED);
468
+ await this.sendWebhook(QueueEventType.DRAINED, {});
469
+ }
470
+ /**
471
+ * Clean completed jobs older than a certain age
472
+ */
473
+ async clean(maxAge = 24 * 60 * 60 * 1000) {
474
+ if (!this.isInitialized) {
475
+ await this.initialize();
476
+ }
477
+ const allJobs = await this.storage.getAllJobs();
478
+ const now = Date.now();
479
+ let cleaned = 0;
480
+ for (const job of allJobs) {
481
+ if (job.status === JobStatus.COMPLETED && now - job.updatedAt > maxAge) {
482
+ job.status = JobStatus.FAILED; // Mark for removal
483
+ await this.storage.updateJob(job);
484
+ this.completedJobIds.delete(job.id);
485
+ cleaned++;
486
+ }
487
+ }
488
+ return cleaned;
489
+ }
186
490
  /**
187
491
  * Get all failed jobs from DLQ
188
492
  */
@@ -207,6 +511,12 @@ export class Queue {
207
511
  await this.storage.addJob(job.toData());
208
512
  return true;
209
513
  }
514
+ /**
515
+ * Get queue statistics
516
+ */
517
+ getConcurrency() {
518
+ return this.config.concurrency;
519
+ }
210
520
  /**
211
521
  * Get queue statistics
212
522
  */
@@ -218,9 +528,12 @@ export class Queue {
218
528
  const failedJobs = await this.dlq.getAll();
219
529
  return {
220
530
  active: this.activeJobs.size,
221
- pending: allJobs.filter(j => j.status === JobStatus.PENDING).length,
531
+ waiting: allJobs.filter((j) => j.status === JobStatus.WAITING).length,
532
+ delayed: allJobs.filter((j) => j.status === JobStatus.DELAYED).length,
533
+ pending: allJobs.filter((j) => j.status === JobStatus.PENDING).length,
222
534
  failed: failedJobs.length,
223
- completed: allJobs.filter(j => j.status === JobStatus.COMPLETED).length,
535
+ completed: allJobs.filter((j) => j.status === JobStatus.COMPLETED).length,
536
+ stalled: allJobs.filter((j) => j.status === JobStatus.STALLED).length,
224
537
  };
225
538
  }
226
539
  /**
@@ -228,12 +541,12 @@ export class Queue {
228
541
  */
229
542
  setupGracefulShutdown() {
230
543
  const shutdown = async () => {
231
- console.log('[Queue] Graceful shutdown initiated...');
544
+ console.log("[Queue] Graceful shutdown initiated...");
232
545
  await this.shutdown();
233
546
  process.exit(0);
234
547
  };
235
- process.on('SIGINT', shutdown);
236
- process.on('SIGTERM', shutdown);
548
+ process.on("SIGINT", shutdown);
549
+ process.on("SIGTERM", shutdown);
237
550
  }
238
551
  /**
239
552
  * Gracefully shutdown the queue
@@ -245,18 +558,27 @@ export class Queue {
245
558
  this.isShuttingDown = true;
246
559
  // Stop accepting new jobs
247
560
  this.scheduler.stop();
561
+ // Stop stalled checker
562
+ if (this.stalledCheckInterval) {
563
+ clearInterval(this.stalledCheckInterval);
564
+ }
565
+ // Cancel all repeating jobs
566
+ for (const timeout of this.repeatingJobs.values()) {
567
+ clearTimeout(timeout);
568
+ }
569
+ this.repeatingJobs.clear();
248
570
  // Wait for active jobs to complete
249
571
  console.log(`[Queue] Waiting for ${this.activeJobs.size} active jobs to complete...`);
250
572
  while (this.activeJobs.size > 0) {
251
- await new Promise(resolve => setTimeout(resolve, 100));
573
+ await new Promise((resolve) => setTimeout(resolve, 100));
252
574
  }
253
575
  // Terminate all workers
254
- console.log('[Queue] Terminating workers...');
255
- await Promise.all(this.workers.map(worker => worker.terminate()));
576
+ console.log("[Queue] Terminating workers...");
577
+ await Promise.all(this.workers.map((worker) => worker.terminate()));
256
578
  // Close storage
257
- console.log('[Queue] Closing storage...');
579
+ console.log("[Queue] Closing storage...");
258
580
  await this.storage.close();
259
- console.log('[Queue] Shutdown complete');
581
+ console.log("[Queue] Shutdown complete");
260
582
  }
261
583
  }
262
584
  //# sourceMappingURL=Queue.js.map