bunqueue 1.9.9 → 2.0.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.
Files changed (123) hide show
  1. package/dist/application/backgroundTasks.d.ts +10 -0
  2. package/dist/application/backgroundTasks.d.ts.map +1 -1
  3. package/dist/application/backgroundTasks.js +67 -6
  4. package/dist/application/backgroundTasks.js.map +1 -1
  5. package/dist/application/cleanupTasks.js +1 -1
  6. package/dist/application/cleanupTasks.js.map +1 -1
  7. package/dist/application/clientTracking.d.ts.map +1 -1
  8. package/dist/application/clientTracking.js +23 -10
  9. package/dist/application/clientTracking.js.map +1 -1
  10. package/dist/application/dependencyProcessor.d.ts.map +1 -1
  11. package/dist/application/dependencyProcessor.js +14 -13
  12. package/dist/application/dependencyProcessor.js.map +1 -1
  13. package/dist/application/eventsManager.d.ts.map +1 -1
  14. package/dist/application/eventsManager.js +16 -4
  15. package/dist/application/eventsManager.js.map +1 -1
  16. package/dist/application/jobLogsManager.d.ts +2 -2
  17. package/dist/application/jobLogsManager.d.ts.map +1 -1
  18. package/dist/application/jobLogsManager.js +13 -3
  19. package/dist/application/jobLogsManager.js.map +1 -1
  20. package/dist/application/lockManager.js +1 -1
  21. package/dist/application/lockManager.js.map +1 -1
  22. package/dist/application/operations/ack.d.ts +2 -1
  23. package/dist/application/operations/ack.d.ts.map +1 -1
  24. package/dist/application/operations/ack.js +12 -0
  25. package/dist/application/operations/ack.js.map +1 -1
  26. package/dist/application/operations/jobManagement.d.ts +1 -1
  27. package/dist/application/operations/jobManagement.d.ts.map +1 -1
  28. package/dist/application/operations/jobManagement.js +23 -3
  29. package/dist/application/operations/jobManagement.js.map +1 -1
  30. package/dist/application/operations/push.d.ts +1 -1
  31. package/dist/application/operations/push.d.ts.map +1 -1
  32. package/dist/application/operations/push.js +13 -5
  33. package/dist/application/operations/push.js.map +1 -1
  34. package/dist/application/operations/queryOperations.d.ts +3 -0
  35. package/dist/application/operations/queryOperations.d.ts.map +1 -1
  36. package/dist/application/operations/queryOperations.js +29 -0
  37. package/dist/application/operations/queryOperations.js.map +1 -1
  38. package/dist/application/queueManager.d.ts +15 -1
  39. package/dist/application/queueManager.d.ts.map +1 -1
  40. package/dist/application/queueManager.js +69 -2
  41. package/dist/application/queueManager.js.map +1 -1
  42. package/dist/application/stallDetection.js +25 -20
  43. package/dist/application/stallDetection.js.map +1 -1
  44. package/dist/application/webhookManager.d.ts.map +1 -1
  45. package/dist/application/webhookManager.js +18 -2
  46. package/dist/application/webhookManager.js.map +1 -1
  47. package/dist/application/workerManager.d.ts.map +1 -1
  48. package/dist/application/workerManager.js +4 -2
  49. package/dist/application/workerManager.js.map +1 -1
  50. package/dist/client/events.d.ts +29 -0
  51. package/dist/client/events.d.ts.map +1 -1
  52. package/dist/client/events.js +92 -21
  53. package/dist/client/events.js.map +1 -1
  54. package/dist/client/flow.d.ts +122 -3
  55. package/dist/client/flow.d.ts.map +1 -1
  56. package/dist/client/flow.js +374 -2
  57. package/dist/client/flow.js.map +1 -1
  58. package/dist/client/index.d.ts +2 -2
  59. package/dist/client/index.d.ts.map +1 -1
  60. package/dist/client/queue/queue.d.ts +260 -1
  61. package/dist/client/queue/queue.d.ts.map +1 -1
  62. package/dist/client/queue/queue.js +1343 -16
  63. package/dist/client/queue/queue.js.map +1 -1
  64. package/dist/client/tcpPool.d.ts.map +1 -1
  65. package/dist/client/tcpPool.js +19 -8
  66. package/dist/client/tcpPool.js.map +1 -1
  67. package/dist/client/types.d.ts +430 -13
  68. package/dist/client/types.d.ts.map +1 -1
  69. package/dist/client/types.js +346 -5
  70. package/dist/client/types.js.map +1 -1
  71. package/dist/client/worker/ackBatcher.d.ts +1 -0
  72. package/dist/client/worker/ackBatcher.d.ts.map +1 -1
  73. package/dist/client/worker/ackBatcher.js +9 -0
  74. package/dist/client/worker/ackBatcher.js.map +1 -1
  75. package/dist/client/worker/processor.js +6 -1
  76. package/dist/client/worker/processor.js.map +1 -1
  77. package/dist/client/worker/worker.d.ts +117 -0
  78. package/dist/client/worker/worker.d.ts.map +1 -1
  79. package/dist/client/worker/worker.js +375 -3
  80. package/dist/client/worker/worker.js.map +1 -1
  81. package/dist/domain/queue/priorityQueue.d.ts.map +1 -1
  82. package/dist/domain/queue/priorityQueue.js +24 -18
  83. package/dist/domain/queue/priorityQueue.js.map +1 -1
  84. package/dist/domain/queue/shard.d.ts +4 -0
  85. package/dist/domain/queue/shard.d.ts.map +1 -1
  86. package/dist/domain/queue/shard.js +21 -7
  87. package/dist/domain/queue/shard.js.map +1 -1
  88. package/dist/domain/types/job.d.ts +89 -2
  89. package/dist/domain/types/job.d.ts.map +1 -1
  90. package/dist/domain/types/job.js +94 -26
  91. package/dist/domain/types/job.js.map +1 -1
  92. package/dist/domain/types/queue.d.ts +11 -1
  93. package/dist/domain/types/queue.d.ts.map +1 -1
  94. package/dist/infrastructure/persistence/sqliteBatch.d.ts +9 -4
  95. package/dist/infrastructure/persistence/sqliteBatch.d.ts.map +1 -1
  96. package/dist/infrastructure/persistence/sqliteBatch.js +34 -17
  97. package/dist/infrastructure/persistence/sqliteBatch.js.map +1 -1
  98. package/dist/infrastructure/persistence/sqliteSerializer.d.ts.map +1 -1
  99. package/dist/infrastructure/persistence/sqliteSerializer.js +14 -0
  100. package/dist/infrastructure/persistence/sqliteSerializer.js.map +1 -1
  101. package/dist/infrastructure/scheduler/cronScheduler.d.ts.map +1 -1
  102. package/dist/infrastructure/scheduler/cronScheduler.js +29 -15
  103. package/dist/infrastructure/scheduler/cronScheduler.js.map +1 -1
  104. package/dist/infrastructure/server/handlers/query.d.ts.map +1 -1
  105. package/dist/infrastructure/server/handlers/query.js +1 -16
  106. package/dist/infrastructure/server/handlers/query.js.map +1 -1
  107. package/dist/infrastructure/server/rateLimiter.d.ts.map +1 -1
  108. package/dist/infrastructure/server/rateLimiter.js +5 -3
  109. package/dist/infrastructure/server/rateLimiter.js.map +1 -1
  110. package/dist/infrastructure/server/tcp.d.ts.map +1 -1
  111. package/dist/infrastructure/server/tcp.js +36 -4
  112. package/dist/infrastructure/server/tcp.js.map +1 -1
  113. package/dist/main.js +5 -2
  114. package/dist/main.js.map +1 -1
  115. package/dist/shared/lock.d.ts +1 -1
  116. package/dist/shared/lock.d.ts.map +1 -1
  117. package/dist/shared/lock.js +6 -4
  118. package/dist/shared/lock.js.map +1 -1
  119. package/dist/shared/lru.d.ts +28 -0
  120. package/dist/shared/lru.d.ts.map +1 -1
  121. package/dist/shared/lru.js +28 -0
  122. package/dist/shared/lru.js.map +1 -1
  123. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { EventEmitter } from 'events';
6
6
  import type { WorkerOptions, Processor } from '../types';
7
+ import type { Job as InternalJob } from '../../domain/types/job';
7
8
  /**
8
9
  * Worker class for processing jobs
9
10
  */
@@ -16,25 +17,141 @@ export declare class Worker<T = unknown, R = unknown> extends EventEmitter {
16
17
  private readonly tcpPool;
17
18
  private readonly ackBatcher;
18
19
  private running;
20
+ private paused;
19
21
  private closing;
22
+ private closed;
20
23
  private activeJobs;
21
24
  private pollTimer;
22
25
  private consecutiveErrors;
23
26
  private readonly activeJobIds;
24
27
  private readonly pulledJobIds;
25
28
  private readonly jobTokens;
29
+ private readonly cancelledJobs;
26
30
  private heartbeatTimer;
27
31
  private readonly workerId;
28
32
  private pendingJobs;
29
33
  private pendingJobsHead;
30
34
  private processingScheduled;
35
+ private readonly limiter;
36
+ private limiterTokens;
37
+ private lastDrainedEmit;
38
+ private stalledUnsubscribe;
31
39
  constructor(name: string, processor: Processor<T, R>, opts?: WorkerOptions);
32
40
  /** Start processing */
33
41
  run(): void;
42
+ /** Subscribe to stalled events from QueueManager (BullMQ v5 compatible) */
43
+ private subscribeToStalledEvents;
34
44
  /** Pause processing */
35
45
  pause(): void;
36
46
  /** Resume processing */
37
47
  resume(): void;
48
+ /** Check if worker is currently running */
49
+ isRunning(): boolean;
50
+ /** Check if worker is paused */
51
+ isPaused(): boolean;
52
+ /** Check if worker is closed */
53
+ isClosed(): boolean;
54
+ /**
55
+ * Wait until the worker is ready (BullMQ v5 compatible).
56
+ * In embedded mode, resolves immediately.
57
+ * In TCP mode, waits for connection to be established.
58
+ */
59
+ waitUntilReady(): Promise<void>;
60
+ /**
61
+ * Mark a job for cancellation (BullMQ v5 compatible).
62
+ * The job will be failed with the given reason when it completes processing.
63
+ * Returns true if the job was found and marked for cancellation.
64
+ */
65
+ cancelJob(jobId: string, reason?: string): boolean;
66
+ /**
67
+ * Mark all active jobs for cancellation (BullMQ v5 compatible).
68
+ */
69
+ cancelAllJobs(reason?: string): void;
70
+ /**
71
+ * Check if a job has been marked for cancellation.
72
+ * Can be called from within a processor to check if the job should stop.
73
+ */
74
+ isJobCancelled(jobId: string): boolean;
75
+ /**
76
+ * Check if rate limiter allows processing another job.
77
+ * Returns true if we can process, false if rate limited.
78
+ */
79
+ private canProcessWithinLimit;
80
+ /**
81
+ * Record a job completion for rate limiting.
82
+ */
83
+ private recordJobForLimiter;
84
+ /**
85
+ * Get time until rate limiter allows next job (ms).
86
+ * Returns 0 if not rate limited.
87
+ */
88
+ private getTimeUntilNextSlot;
89
+ /**
90
+ * Get rate limiter info (for debugging/monitoring).
91
+ */
92
+ getRateLimiterInfo(): {
93
+ current: number;
94
+ max: number;
95
+ duration: number;
96
+ } | null;
97
+ /**
98
+ * Apply rate limiting to this worker (BullMQ v5 compatible).
99
+ * The worker will not process jobs until the rate limit expires.
100
+ *
101
+ * @param expireTimeMs - Time in milliseconds until rate limit expires
102
+ */
103
+ rateLimit(expireTimeMs: number): void;
104
+ /** Rate limit expiration timestamp (internal) */
105
+ private rateLimitExpiration;
106
+ /**
107
+ * Check if worker is currently rate limited.
108
+ */
109
+ isRateLimited(): boolean;
110
+ /**
111
+ * Manually start the stalled job check timer (BullMQ v5 compatible).
112
+ * The check will run once immediately and then every stalledInterval ms.
113
+ * In bunqueue, stall detection is handled automatically by the manager/server.
114
+ */
115
+ startStalledCheckTimer(): Promise<void>;
116
+ /**
117
+ * Delay processing for a specified time (BullMQ v5 compatible).
118
+ * The worker will not pick up new jobs during this delay.
119
+ *
120
+ * @param milliseconds - Time to delay in ms (default: 0)
121
+ * @param abortController - Optional AbortController to cancel the delay
122
+ */
123
+ delay(milliseconds?: number, abortController?: AbortController): Promise<void>;
124
+ /**
125
+ * Get the next job from the queue (BullMQ v5 compatible).
126
+ * This is for manual job processing - typically you'd use the processor callback instead.
127
+ *
128
+ * @param token - Lock token for job ownership
129
+ * @param opts - Options (currently unused, for API compatibility)
130
+ * @returns The next job or undefined if queue is empty
131
+ */
132
+ getNextJob(token?: string, _opts?: {
133
+ block?: boolean;
134
+ }): Promise<InternalJob | undefined>;
135
+ /**
136
+ * Manually process a job (BullMQ v5 compatible).
137
+ * This is for advanced use cases where you need manual control over job processing.
138
+ *
139
+ * @param job - The job to process
140
+ * @param token - Lock token for job ownership
141
+ * @param fetchNextCallback - Optional callback to fetch next job after completion
142
+ * @returns The processed job or void
143
+ */
144
+ processJobManually(job: InternalJob, token?: string, fetchNextCallback?: () => Promise<InternalJob | undefined>): Promise<InternalJob | undefined>;
145
+ /**
146
+ * Extend locks on multiple jobs (BullMQ v5 compatible).
147
+ * Used to prevent jobs from being considered stalled during long processing.
148
+ *
149
+ * @param jobIds - Array of job IDs to extend locks for
150
+ * @param tokens - Array of lock tokens corresponding to each job
151
+ * @param duration - Duration in milliseconds to extend the lock
152
+ * @returns Number of locks successfully extended
153
+ */
154
+ extendJobLocks(jobIds: string[], tokens: string[], duration: number): Promise<number>;
38
155
  /** Close worker gracefully */
39
156
  close(force?: boolean): Promise<void>;
40
157
  private startHeartbeat;
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/client/worker/worker.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAqB,MAAM,UAAU,CAAC;AAQ5E;;GAEG;AACH,qBAAa,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,CAAE,SAAQ,YAAY;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,iBAAiB,CAAK;IAI9B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,cAAc,CAA+C;IAGrE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAGlC,OAAO,CAAC,WAAW,CAAyD;IAC5E,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,mBAAmB,CAAS;gBAExB,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,GAAE,aAAkB;IA+C9E,uBAAuB;IACvB,GAAG,IAAI,IAAI;IAYX,uBAAuB;IACvB,KAAK,IAAI,IAAI;IAQb,wBAAwB;IACxB,MAAM,IAAI,IAAI;IAId,8BAA8B;IACxB,KAAK,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAwCzC,OAAO,CAAC,cAAc;YAIR,aAAa;IAiC3B,OAAO,CAAC,IAAI;YAYE,UAAU;IAqCxB,kDAAkD;IAClD,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,cAAc;YAWR,SAAS;YAQT,YAAY;YAwBZ,OAAO;IA2CrB,OAAO,CAAC,QAAQ;IA2ChB,OAAO,CAAC,eAAe;CAoBxB"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/client/worker/worker.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,9 @@ export class Worker extends EventEmitter {
21
21
  tcpPool;
22
22
  ackBatcher;
23
23
  running = false;
24
+ paused = false;
24
25
  closing = false;
26
+ closed = false;
25
27
  activeJobs = 0;
26
28
  pollTimer = null;
27
29
  consecutiveErrors = 0;
@@ -30,6 +32,7 @@ export class Worker extends EventEmitter {
30
32
  activeJobIds = new Set();
31
33
  pulledJobIds = new Set(); // All pulled jobs (for heartbeat)
32
34
  jobTokens = new Map(); // jobId -> lockToken
35
+ cancelledJobs = new Set(); // Jobs marked for cancellation
33
36
  heartbeatTimer = null;
34
37
  // Unique worker ID for lock ownership
35
38
  workerId;
@@ -37,6 +40,13 @@ export class Worker extends EventEmitter {
37
40
  pendingJobs = [];
38
41
  pendingJobsHead = 0;
39
42
  processingScheduled = false; // Prevent multiple setImmediate calls
43
+ // Rate limiter state (BullMQ v5 compatible)
44
+ limiter;
45
+ limiterTokens = []; // Timestamps of recent job completions
46
+ // Drained event tracking
47
+ lastDrainedEmit = 0;
48
+ // Stalled event subscription (BullMQ v5 compatible)
49
+ stalledUnsubscribe = null;
40
50
  constructor(name, processor, opts = {}) {
41
51
  super();
42
52
  this.name = name;
@@ -55,6 +65,8 @@ export class Worker extends EventEmitter {
55
65
  // Lock-based ownership: disable for high-throughput scenarios where stall detection is sufficient
56
66
  useLocks: opts.useLocks ?? true,
57
67
  };
68
+ // Initialize rate limiter if provided
69
+ this.limiter = opts.limiter ?? null;
58
70
  this.ackBatcher = new AckBatcher({
59
71
  batchSize: opts.batchSize ?? 10,
60
72
  interval: WORKER_CONSTANTS.DEFAULT_ACK_INTERVAL,
@@ -81,19 +93,41 @@ export class Worker extends EventEmitter {
81
93
  }
82
94
  /** Start processing */
83
95
  run() {
84
- if (this.running)
96
+ if (this.running || this.closed)
85
97
  return;
86
98
  this.running = true;
99
+ this.paused = false;
87
100
  this.closing = false;
88
101
  this.emit('ready');
102
+ // Subscribe to stalled events in embedded mode (BullMQ v5)
103
+ if (this.embedded && !this.stalledUnsubscribe) {
104
+ this.subscribeToStalledEvents();
105
+ }
89
106
  if (!this.embedded && this.opts.heartbeatInterval > 0) {
90
107
  this.startHeartbeat();
91
108
  }
92
109
  this.poll();
93
110
  }
111
+ /** Subscribe to stalled events from QueueManager (BullMQ v5 compatible) */
112
+ subscribeToStalledEvents() {
113
+ if (!this.embedded)
114
+ return;
115
+ const manager = getSharedManager();
116
+ this.stalledUnsubscribe = manager.subscribe((event) => {
117
+ if (event.queue !== this.name)
118
+ return;
119
+ if (event.eventType === "stalled" /* EventType.Stalled */) {
120
+ // Emit stalled event (BullMQ v5 format: jobId, prev)
121
+ this.emit('stalled', event.jobId, 'active');
122
+ }
123
+ });
124
+ }
94
125
  /** Pause processing */
95
126
  pause() {
127
+ if (!this.running)
128
+ return;
96
129
  this.running = false;
130
+ this.paused = true;
97
131
  if (this.pollTimer) {
98
132
  clearTimeout(this.pollTimer);
99
133
  this.pollTimer = null;
@@ -101,12 +135,325 @@ export class Worker extends EventEmitter {
101
135
  }
102
136
  /** Resume processing */
103
137
  resume() {
138
+ if (this.closed)
139
+ return;
140
+ this.paused = false;
104
141
  this.run();
105
142
  }
143
+ /** Check if worker is currently running */
144
+ isRunning() {
145
+ return this.running;
146
+ }
147
+ /** Check if worker is paused */
148
+ isPaused() {
149
+ return this.paused && !this.closed;
150
+ }
151
+ /** Check if worker is closed */
152
+ isClosed() {
153
+ return this.closed;
154
+ }
155
+ /**
156
+ * Wait until the worker is ready (BullMQ v5 compatible).
157
+ * In embedded mode, resolves immediately.
158
+ * In TCP mode, waits for connection to be established.
159
+ */
160
+ async waitUntilReady() {
161
+ if (this.embedded) {
162
+ // Embedded mode is always ready
163
+ return;
164
+ }
165
+ if (this.tcpPool) {
166
+ // Wait for TCP connection by sending a ping
167
+ await this.tcpPool.send({ cmd: 'Ping' });
168
+ }
169
+ }
170
+ /**
171
+ * Mark a job for cancellation (BullMQ v5 compatible).
172
+ * The job will be failed with the given reason when it completes processing.
173
+ * Returns true if the job was found and marked for cancellation.
174
+ */
175
+ cancelJob(jobId, reason) {
176
+ if (this.activeJobIds.has(jobId)) {
177
+ this.cancelledJobs.add(jobId);
178
+ this.emit('cancelled', { jobId, reason: reason ?? 'Job cancelled by worker' });
179
+ return true;
180
+ }
181
+ return false;
182
+ }
183
+ /**
184
+ * Mark all active jobs for cancellation (BullMQ v5 compatible).
185
+ */
186
+ cancelAllJobs(reason) {
187
+ for (const jobId of this.activeJobIds) {
188
+ this.cancelledJobs.add(jobId);
189
+ this.emit('cancelled', { jobId, reason: reason ?? 'All jobs cancelled' });
190
+ }
191
+ }
192
+ /**
193
+ * Check if a job has been marked for cancellation.
194
+ * Can be called from within a processor to check if the job should stop.
195
+ */
196
+ isJobCancelled(jobId) {
197
+ return this.cancelledJobs.has(jobId);
198
+ }
199
+ /**
200
+ * Check if rate limiter allows processing another job.
201
+ * Returns true if we can process, false if rate limited.
202
+ */
203
+ canProcessWithinLimit() {
204
+ if (!this.limiter)
205
+ return true;
206
+ const now = Date.now();
207
+ const windowStart = now - this.limiter.duration;
208
+ // Remove expired tokens
209
+ this.limiterTokens = this.limiterTokens.filter((t) => t > windowStart);
210
+ // Check if we have capacity
211
+ return this.limiterTokens.length < this.limiter.max;
212
+ }
213
+ /**
214
+ * Record a job completion for rate limiting.
215
+ */
216
+ recordJobForLimiter() {
217
+ if (!this.limiter)
218
+ return;
219
+ this.limiterTokens.push(Date.now());
220
+ }
221
+ /**
222
+ * Get time until rate limiter allows next job (ms).
223
+ * Returns 0 if not rate limited.
224
+ */
225
+ getTimeUntilNextSlot() {
226
+ if (!this.limiter)
227
+ return 0;
228
+ const now = Date.now();
229
+ const windowStart = now - this.limiter.duration;
230
+ // Remove expired tokens
231
+ this.limiterTokens = this.limiterTokens.filter((t) => t > windowStart);
232
+ if (this.limiterTokens.length < this.limiter.max) {
233
+ return 0;
234
+ }
235
+ // Find oldest token and calculate when it expires
236
+ const oldestToken = Math.min(...this.limiterTokens);
237
+ return oldestToken + this.limiter.duration - now;
238
+ }
239
+ /**
240
+ * Get rate limiter info (for debugging/monitoring).
241
+ */
242
+ getRateLimiterInfo() {
243
+ if (!this.limiter)
244
+ return null;
245
+ const now = Date.now();
246
+ const windowStart = now - this.limiter.duration;
247
+ const currentTokens = this.limiterTokens.filter((t) => t > windowStart).length;
248
+ return {
249
+ current: currentTokens,
250
+ max: this.limiter.max,
251
+ duration: this.limiter.duration,
252
+ };
253
+ }
254
+ /**
255
+ * Apply rate limiting to this worker (BullMQ v5 compatible).
256
+ * The worker will not process jobs until the rate limit expires.
257
+ *
258
+ * @param expireTimeMs - Time in milliseconds until rate limit expires
259
+ */
260
+ rateLimit(expireTimeMs) {
261
+ if (expireTimeMs <= 0)
262
+ return;
263
+ // Fill rate limiter tokens to block processing
264
+ if (this.limiter) {
265
+ const now = Date.now();
266
+ // Add enough tokens to block for the specified time
267
+ for (let i = 0; i < this.limiter.max; i++) {
268
+ this.limiterTokens.push(now + expireTimeMs - this.limiter.duration);
269
+ }
270
+ }
271
+ // Store rate limit expiration
272
+ this.rateLimitExpiration = Date.now() + expireTimeMs;
273
+ }
274
+ /** Rate limit expiration timestamp (internal) */
275
+ rateLimitExpiration = 0;
276
+ /**
277
+ * Check if worker is currently rate limited.
278
+ */
279
+ isRateLimited() {
280
+ return Date.now() < this.rateLimitExpiration;
281
+ }
282
+ /**
283
+ * Manually start the stalled job check timer (BullMQ v5 compatible).
284
+ * The check will run once immediately and then every stalledInterval ms.
285
+ * In bunqueue, stall detection is handled automatically by the manager/server.
286
+ */
287
+ async startStalledCheckTimer() {
288
+ // In bunqueue, stall detection is handled automatically by:
289
+ // - Embedded mode: QueueManager.checkStalledJobs() runs periodically
290
+ // - TCP mode: Server-side stall detection
291
+ // This method is a no-op for API compatibility
292
+ }
293
+ /**
294
+ * Delay processing for a specified time (BullMQ v5 compatible).
295
+ * The worker will not pick up new jobs during this delay.
296
+ *
297
+ * @param milliseconds - Time to delay in ms (default: 0)
298
+ * @param abortController - Optional AbortController to cancel the delay
299
+ */
300
+ async delay(milliseconds = 0, abortController) {
301
+ if (milliseconds <= 0)
302
+ return;
303
+ return new Promise((resolve, reject) => {
304
+ const timeout = setTimeout(resolve, milliseconds);
305
+ if (abortController) {
306
+ abortController.signal.addEventListener('abort', () => {
307
+ clearTimeout(timeout);
308
+ reject(new Error('Delay aborted'));
309
+ });
310
+ }
311
+ });
312
+ }
313
+ // ============================================================================
314
+ // BullMQ v5 Manual Job Control Methods
315
+ // ============================================================================
316
+ /**
317
+ * Get the next job from the queue (BullMQ v5 compatible).
318
+ * This is for manual job processing - typically you'd use the processor callback instead.
319
+ *
320
+ * @param token - Lock token for job ownership
321
+ * @param opts - Options (currently unused, for API compatibility)
322
+ * @returns The next job or undefined if queue is empty
323
+ */
324
+ async getNextJob(token, _opts) {
325
+ if (this.closed)
326
+ return undefined;
327
+ if (this.embedded) {
328
+ const manager = getSharedManager();
329
+ if (this.opts.useLocks) {
330
+ const { job, token: lockToken } = await manager.pullWithLock(this.name, this.workerId, 0);
331
+ if (job && lockToken) {
332
+ const jobIdStr = String(job.id);
333
+ this.pulledJobIds.add(jobIdStr);
334
+ this.jobTokens.set(jobIdStr, lockToken);
335
+ }
336
+ return job ?? undefined;
337
+ }
338
+ const job = await manager.pull(this.name, 0);
339
+ if (job) {
340
+ this.pulledJobIds.add(String(job.id));
341
+ }
342
+ return job ?? undefined;
343
+ }
344
+ // TCP mode
345
+ if (!this.tcp)
346
+ return undefined;
347
+ const cmd = {
348
+ cmd: 'PULL',
349
+ queue: this.name,
350
+ timeout: 0,
351
+ };
352
+ if (this.opts.useLocks) {
353
+ cmd.owner = this.workerId;
354
+ if (token)
355
+ cmd.token = token;
356
+ }
357
+ const response = await this.tcp.send(cmd);
358
+ if (!response.ok || !response.job)
359
+ return undefined;
360
+ const job = parseJobFromResponse(response.job, this.name);
361
+ const jobIdStr = String(job.id);
362
+ this.pulledJobIds.add(jobIdStr);
363
+ if (this.opts.useLocks && response.token) {
364
+ this.jobTokens.set(jobIdStr, response.token);
365
+ }
366
+ return job;
367
+ }
368
+ /**
369
+ * Manually process a job (BullMQ v5 compatible).
370
+ * This is for advanced use cases where you need manual control over job processing.
371
+ *
372
+ * @param job - The job to process
373
+ * @param token - Lock token for job ownership
374
+ * @param fetchNextCallback - Optional callback to fetch next job after completion
375
+ * @returns The processed job or void
376
+ */
377
+ async processJobManually(job, token, fetchNextCallback) {
378
+ if (this.closed)
379
+ return undefined;
380
+ const jobIdStr = String(job.id);
381
+ this.activeJobs++;
382
+ this.activeJobIds.add(jobIdStr);
383
+ this.pulledJobIds.add(jobIdStr);
384
+ if (this.opts.useLocks && token) {
385
+ this.jobTokens.set(jobIdStr, token);
386
+ }
387
+ try {
388
+ await processJob(job, {
389
+ name: this.name,
390
+ processor: this.processor,
391
+ embedded: this.embedded,
392
+ tcp: this.tcp,
393
+ ackBatcher: this.ackBatcher,
394
+ emitter: this,
395
+ token: this.opts.useLocks ? token : undefined,
396
+ });
397
+ // Fetch next job if callback provided
398
+ if (fetchNextCallback) {
399
+ return await fetchNextCallback();
400
+ }
401
+ }
402
+ finally {
403
+ this.activeJobs--;
404
+ this.activeJobIds.delete(jobIdStr);
405
+ this.pulledJobIds.delete(jobIdStr);
406
+ this.cancelledJobs.delete(jobIdStr);
407
+ if (this.opts.useLocks) {
408
+ this.jobTokens.delete(jobIdStr);
409
+ }
410
+ this.recordJobForLimiter();
411
+ }
412
+ }
413
+ /**
414
+ * Extend locks on multiple jobs (BullMQ v5 compatible).
415
+ * Used to prevent jobs from being considered stalled during long processing.
416
+ *
417
+ * @param jobIds - Array of job IDs to extend locks for
418
+ * @param tokens - Array of lock tokens corresponding to each job
419
+ * @param duration - Duration in milliseconds to extend the lock
420
+ * @returns Number of locks successfully extended
421
+ */
422
+ async extendJobLocks(jobIds, tokens, duration) {
423
+ if (this.closed || jobIds.length === 0)
424
+ return 0;
425
+ if (jobIds.length !== tokens.length) {
426
+ throw new Error('jobIds and tokens arrays must have the same length');
427
+ }
428
+ if (this.embedded) {
429
+ const manager = getSharedManager();
430
+ let extended = 0;
431
+ for (let i = 0; i < jobIds.length; i++) {
432
+ const success = await manager.extendLock(jobIds[i], tokens[i], duration);
433
+ if (success)
434
+ extended++;
435
+ }
436
+ return extended;
437
+ }
438
+ // TCP mode
439
+ if (!this.tcp)
440
+ return 0;
441
+ const response = await this.tcp.send({
442
+ cmd: 'ExtendLocks',
443
+ ids: jobIds,
444
+ tokens,
445
+ duration,
446
+ });
447
+ const extended = response.extended;
448
+ return extended ?? 0;
449
+ }
106
450
  /** Close worker gracefully */
107
451
  async close(force = false) {
452
+ if (this.closed)
453
+ return;
108
454
  this.closing = true;
109
455
  this.running = false;
456
+ this.paused = false;
110
457
  if (this.pollTimer) {
111
458
  clearTimeout(this.pollTimer);
112
459
  this.pollTimer = null;
@@ -129,14 +476,22 @@ export class Worker extends EventEmitter {
129
476
  this.ackBatcher.stop();
130
477
  // Small delay to ensure TCP responses are processed
131
478
  await new Promise((r) => setTimeout(r, 100));
479
+ // Unsubscribe from stalled events
480
+ if (this.stalledUnsubscribe) {
481
+ this.stalledUnsubscribe();
482
+ this.stalledUnsubscribe = null;
483
+ }
132
484
  // Clear tracking sets
133
485
  this.activeJobIds.clear();
134
486
  this.pulledJobIds.clear();
135
487
  this.jobTokens.clear();
488
+ this.cancelledJobs.clear();
136
489
  this.pendingJobs = [];
137
490
  this.pendingJobsHead = 0;
138
491
  if (this.tcpPool)
139
492
  this.tcpPool.close();
493
+ this.closed = true;
494
+ this.closing = false;
140
495
  this.emit('closed');
141
496
  }
142
497
  startHeartbeat() {
@@ -187,6 +542,14 @@ export class Worker extends EventEmitter {
187
542
  }, 10);
188
543
  return;
189
544
  }
545
+ // Check rate limiter
546
+ if (!this.canProcessWithinLimit()) {
547
+ const waitTime = this.getTimeUntilNextSlot();
548
+ this.pollTimer = setTimeout(() => {
549
+ this.poll();
550
+ }, Math.max(waitTime, 10));
551
+ return;
552
+ }
190
553
  void this.tryProcess();
191
554
  }
192
555
  async tryProcess() {
@@ -196,9 +559,9 @@ export class Worker extends EventEmitter {
196
559
  let item = this.getBufferedJob();
197
560
  if (!item) {
198
561
  const items = await this.pullBatch();
199
- // Re-check closing after async operation (can be modified during await)
562
+ // Re-check state after async operation (can be modified during await)
200
563
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
201
- if (this.closing)
564
+ if (!this.running || this.closing)
202
565
  return;
203
566
  if (items.length > 0) {
204
567
  // Register ALL pulled jobs for heartbeat tracking immediately
@@ -213,6 +576,12 @@ export class Worker extends EventEmitter {
213
576
  this.startJob(item.job, item.token);
214
577
  }
215
578
  else {
579
+ // Emit drained event when queue is empty (throttled to avoid spam)
580
+ const now = Date.now();
581
+ if (now - this.lastDrainedEmit > 1000) {
582
+ this.lastDrainedEmit = now;
583
+ this.emit('drained');
584
+ }
216
585
  const waitTime = this.opts.pollTimeout > 0 ? 10 : 50;
217
586
  this.pollTimer = setTimeout(() => {
218
587
  this.poll();
@@ -335,9 +704,12 @@ export class Worker extends EventEmitter {
335
704
  this.activeJobs--;
336
705
  this.activeJobIds.delete(jobIdStr);
337
706
  this.pulledJobIds.delete(jobIdStr); // Remove from heartbeat tracking
707
+ this.cancelledJobs.delete(jobIdStr); // Clean up cancellation flag
338
708
  if (this.opts.useLocks) {
339
709
  this.jobTokens.delete(jobIdStr); // Clean up token
340
710
  }
711
+ // Record job completion for rate limiter
712
+ this.recordJobForLimiter();
341
713
  if (this.running && !this.closing)
342
714
  this.poll();
343
715
  });