@elysiumjs/heracles 0.4.2 → 0.4.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elysiumjs/heracles",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Background job processing system for Elysium.js",
5
5
  "main": "index.ts",
6
6
  "bin": {
@@ -26,7 +26,6 @@
26
26
  "author": "Workbud Technologies Inc.",
27
27
  "license": "Apache-2.0",
28
28
  "dependencies": {
29
- "@elysiumjs/core": "latest",
30
29
  "commander": "^13.1.0",
31
30
  "radash": "^12.1.0",
32
31
  "reflect-metadata": "^0.2.2"
@@ -38,6 +37,7 @@
38
37
  "type-fest": "^4.41.0"
39
38
  },
40
39
  "peerDependencies": {
40
+ "@elysiumjs/core": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  }
43
43
  }
@@ -0,0 +1,289 @@
1
+ // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { JobStatusInfo, Transport, TransportEvent } from '../transport';
16
+
17
+ import { InteractsWithConsole } from '@elysiumjs/core';
18
+
19
+ import { JobStatus } from '../job';
20
+ import { TransportMode } from '../transport';
21
+
22
+ /**
23
+ * Manages a Bun Worker as a "thread" channel (producer driven).
24
+ */
25
+ export class ThreadTransport extends InteractsWithConsole implements Transport {
26
+ private worker: Worker | null = null;
27
+ private workerReady = false;
28
+ private messageHandlers: Array<(message: TransportEvent) => void> = [];
29
+ private workerScript = './src/workers/thread.worker.ts'; // Path to ThreadWorker
30
+ // No local cache needed since Queue already manages job statuses
31
+ private registeredWorkers: Map<string, Set<string>> = new Map(); // workerId -> Set of queues
32
+ private options: Record<string, any>;
33
+
34
+ constructor(
35
+ private readonly mode: TransportMode,
36
+ options: Record<string, any> = {}
37
+ ) {
38
+ super();
39
+
40
+ this.options = options;
41
+ this.workerScript = ELYSIUM_BUILD
42
+ ? './thread.worker.js'
43
+ : this.options.workerScript || __dirname + '/../workers/thread.worker.ts';
44
+ }
45
+
46
+ async start(): Promise<void> {
47
+ if (this.worker) return; // Already started
48
+
49
+ this.debug(`Starting ThreadTransport with worker script: ${this.workerScript}`);
50
+ this.worker = new Worker(this.workerScript);
51
+
52
+ // Thread ready flag
53
+ this.worker.addEventListener('open', () => {
54
+ this.workerReady = true;
55
+ this.debug('ThreadTransport worker ready');
56
+ });
57
+
58
+ // Message event listener for handling responses
59
+ this.worker.addEventListener('message', (event: MessageEvent) => {
60
+ const message = event.data as TransportEvent;
61
+
62
+ // Handle specific message types
63
+ if (message.type === 'job:status' || message.type === 'job:result') {
64
+ this.handleStatusMessage(message);
65
+ } else if (message.type === 'worker:register' || message.type === 'worker:unregister') {
66
+ this.updateWorkerRegistration(message);
67
+ }
68
+
69
+ // Forward message to all handlers
70
+ for (const handler of this.messageHandlers) {
71
+ try {
72
+ handler(message);
73
+ } catch (error) {
74
+ this.error(`Error in message handler: ${error}`);
75
+ }
76
+ }
77
+ });
78
+
79
+ // Handle worker errors
80
+ this.worker.addEventListener('error', (event) => {
81
+ this.error(`Worker error: ${event}`);
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Process job status messages
87
+ * This method is called when a job status message is received from a worker thread
88
+ * The message is then forwarded to the Queue via the message handlers
89
+ */
90
+ private handleStatusMessage(message: TransportEvent): void {
91
+ // Simply forward the message to handlers (Queue will store the status)
92
+ this.debug(
93
+ `Received status update for job ${(message as any).jobId}: ${(message as any).status}`
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Update worker registration information
99
+ */
100
+ private updateWorkerRegistration(message: TransportEvent): void {
101
+ if (message.type === 'worker:register' && message.queues) {
102
+ const queues = new Set(message.queues);
103
+ this.registeredWorkers.set(message.workerId, queues);
104
+ this.debug(`Worker ${message.workerId} registered for queues: ${message.queues.join(', ')}`);
105
+ } else if (message.type === 'worker:unregister') {
106
+ this.registeredWorkers.delete(message.workerId);
107
+ this.debug(`Worker ${message.workerId} unregistered`);
108
+ }
109
+ }
110
+
111
+ async stop(): Promise<void> {
112
+ if (this.worker) {
113
+ this.debug('Stopping ThreadTransport worker');
114
+
115
+ try {
116
+ // Send terminate message to gracefully shut down
117
+ await this.send({
118
+ type: 'worker:unregister',
119
+ workerId: 'main'
120
+ });
121
+
122
+ // Wait a moment to allow graceful shutdown
123
+ await new Promise((resolve) => setTimeout(resolve, 100));
124
+
125
+ // Force terminate if still running
126
+ this.worker.terminate();
127
+ } catch (error) {
128
+ this.error(`Error stopping ThreadTransport: ${error}`);
129
+ } finally {
130
+ this.worker = null;
131
+ this.workerReady = false;
132
+ this.messageHandlers = [];
133
+ this.debug('ThreadTransport stopped');
134
+ }
135
+ }
136
+ }
137
+
138
+ async send(message: TransportEvent): Promise<void> {
139
+ if (!this.worker) {
140
+ this.debug('Starting worker before sending message');
141
+ await this.start();
142
+ }
143
+
144
+ // Ensure worker is ready
145
+ if (!this.workerReady) {
146
+ this.debug('Waiting for worker to be ready');
147
+ await new Promise((resolve) =>
148
+ this.worker!.addEventListener('open', () => resolve(undefined), { once: true })
149
+ );
150
+ }
151
+
152
+ try {
153
+ this.debug(`Sending ${message.type} message to worker`);
154
+ this.worker!.postMessage(message);
155
+ } catch (error) {
156
+ this.error(`Error sending message to worker: ${error}`);
157
+ throw new Error(`Failed to send message: ${error}`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Register a callback to handle messages from the transport
163
+ */
164
+ onMessage(handler: (message: TransportEvent) => void): void {
165
+ this.messageHandlers.push(handler);
166
+ }
167
+
168
+ /**
169
+ * Updates the status of a job directly
170
+ * @param jobId The ID of the job to update
171
+ * @param queueName The name of the queue the job is in
172
+ * @param updates The updates to apply to the job status
173
+ */
174
+ async updateJobStatus(
175
+ jobId: string,
176
+ dispatchId: string,
177
+ queueName: string,
178
+ updates: Partial<JobStatusInfo>
179
+ ): Promise<void> {
180
+ // Get the current timestamp for all updates
181
+ const now = Date.now().toString();
182
+ const statusValue = updates.status || 'unknown';
183
+
184
+ // Determine if this is a terminal status that should use job:result type
185
+ const isTerminalStatus =
186
+ statusValue === JobStatus.COMPLETED ||
187
+ statusValue === JobStatus.FAILED ||
188
+ statusValue === JobStatus.CANCELLED;
189
+
190
+ // Create the message with the appropriate type
191
+ const messageType = isTerminalStatus ? 'job:result' : 'job:status';
192
+ const message = {
193
+ type: messageType,
194
+ jobId,
195
+ queue: queueName,
196
+ status: statusValue,
197
+ error: updates.error,
198
+ retries: updates.retries || 0,
199
+ startedAt: updates.startedAt,
200
+ completedAt: updates.completedAt || (isTerminalStatus ? now : undefined),
201
+ updatedAt: updates.updatedAt || now
202
+ };
203
+
204
+ if (this.mode === TransportMode.PRODUCER) {
205
+ // In producer mode, forward the status update to the worker thread
206
+ await this.send(message as TransportEvent);
207
+ this.debug(
208
+ `[Producer] Sent ${messageType} for job ${jobId} to worker thread: ${statusValue}`
209
+ );
210
+ } else {
211
+ // In consumer mode, we're in the worker thread, so send message to main thread
212
+ if (typeof self !== 'undefined') {
213
+ // @ts-ignore - self is available in worker threads
214
+ self.postMessage(message);
215
+ this.debug(
216
+ `[Consumer] Sent ${messageType} for job ${jobId} to main thread: ${statusValue}`
217
+ );
218
+ } else {
219
+ this.warning(`Unable to send job status update: not in a worker context`);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get the current status of a job
226
+ */
227
+ async getJobStatus(jobId: string, dispatchId: string, queueName: string): Promise<JobStatusInfo> {
228
+ try {
229
+ // Request status from worker
230
+ await this.send({
231
+ type: 'job:status',
232
+ jobId,
233
+ dispatchId,
234
+ queue: queueName,
235
+ status: 'unknown'
236
+ });
237
+
238
+ // Since we don't maintain a local cache, we'll return a pending status
239
+ // The Queue will receive the actual status via the message handler when the worker responds
240
+ return {
241
+ jobId,
242
+ dispatchId,
243
+ queue: queueName,
244
+ status: 'unknown',
245
+ retries: 0,
246
+ createdAt: new Date().toISOString()
247
+ };
248
+ } catch (error) {
249
+ this.error(`Error getting job status for ${jobId}: ${error}`);
250
+ // Return a basic status object on error
251
+ return {
252
+ jobId,
253
+ dispatchId,
254
+ queue: queueName,
255
+ status: 'unknown',
256
+ retries: 0,
257
+ createdAt: new Date().toISOString(),
258
+ error: `Failed to get status: ${error}`
259
+ };
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Register a worker with the transport
265
+ */
266
+ async registerWorker(workerId: string, queues: string[]): Promise<void> {
267
+ await this.send({
268
+ type: 'worker:register',
269
+ workerId,
270
+ queues
271
+ });
272
+
273
+ // Store locally too
274
+ this.registeredWorkers.set(workerId, new Set(queues));
275
+ }
276
+
277
+ /**
278
+ * Unregister a worker from the transport
279
+ */
280
+ async unregisterWorker(workerId: string): Promise<void> {
281
+ await this.send({
282
+ type: 'worker:unregister',
283
+ workerId
284
+ });
285
+
286
+ // Remove from local storage
287
+ this.registeredWorkers.delete(workerId);
288
+ }
289
+ }
@@ -0,0 +1,393 @@
1
+ // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { Job, JobDispatchOptions } from './job';
16
+ import type { WorkerInfo } from './worker';
17
+
18
+ import { InteractsWithConsole } from '@elysiumjs/core';
19
+
20
+ /**
21
+ * Interface for tracking worker selection state per queue.
22
+ */
23
+ interface QueueWorkerState {
24
+ /**
25
+ * All workers assigned to this queue
26
+ */
27
+ workers: WorkerInfo[];
28
+
29
+ /**
30
+ * Index of the last selected worker for round-robin
31
+ */
32
+ lastWorkerIndex: number;
33
+ }
34
+
35
+ /**
36
+ * WorkerPool manages a pool of workers and distributes jobs among them
37
+ * using a round-robin algorithm.
38
+ */
39
+ export class WorkerPool extends InteractsWithConsole {
40
+ /**
41
+ * Singleton instance of the worker pool
42
+ */
43
+ private static _instance: WorkerPool;
44
+
45
+ /**
46
+ * All workers in the pool, indexed by worker ID
47
+ */
48
+ private workers: Map<string, WorkerInfo> = new Map();
49
+
50
+ /**
51
+ * Queue-to-workers mapping for tracking which workers handle each queue
52
+ */
53
+ private queueWorkers: Map<string, QueueWorkerState> = new Map();
54
+
55
+ /**
56
+ * Last used worker per queue for true round-robin
57
+ */
58
+ private lastUsedWorker: Map<string, string> = new Map();
59
+
60
+ /**
61
+ * Get the singleton instance of the worker pool
62
+ */
63
+ public static get instance(): WorkerPool {
64
+ if (!WorkerPool._instance) {
65
+ WorkerPool._instance = new WorkerPool();
66
+ }
67
+ return WorkerPool._instance;
68
+ }
69
+
70
+ /**
71
+ * Private constructor to enforce singleton pattern
72
+ */
73
+ private constructor() {
74
+ super();
75
+ }
76
+
77
+ /**
78
+ * Initialize the worker pool
79
+ */
80
+ public async init(): Promise<void> {
81
+ this.info('WorkerPool initialized');
82
+ }
83
+
84
+ /**
85
+ * Add a worker to the pool and associate it with queues
86
+ *
87
+ * @param worker The worker information to add
88
+ * @param queues The queues this worker will handle
89
+ */
90
+ public async addWorker(worker: WorkerInfo, queues: string[] = []): Promise<void> {
91
+ if (this.workers.has(worker.id)) {
92
+ this.warning(`Worker ${worker.id} is already registered in the pool`);
93
+ return;
94
+ }
95
+
96
+ // Store the worker
97
+ this.workers.set(worker.id, worker);
98
+
99
+ // Associate worker with each queue
100
+ for (const queueName of queues) {
101
+ // Get or create queue state
102
+ const queueState = this.getOrCreateQueueState(queueName);
103
+
104
+ // Add worker to queue if not already present
105
+ if (!queueState.workers.some((w) => w.id === worker.id)) {
106
+ queueState.workers.push(worker);
107
+ this.debug(`Worker ${worker.id} assigned to queue '${queueName}'`);
108
+ }
109
+ }
110
+
111
+ this.info(`Worker ${worker.id} added to pool with ${queues.length} queues`);
112
+ }
113
+
114
+ /**
115
+ * Remove a worker from the pool
116
+ *
117
+ * @param workerId The ID of the worker to remove
118
+ */
119
+ public async removeWorker(workerId: string): Promise<void> {
120
+ const worker = this.workers.get(workerId);
121
+ if (!worker) {
122
+ this.warning(`Attempted to remove non-existent worker ${workerId}`);
123
+ return;
124
+ }
125
+
126
+ // Remove worker from all queues
127
+ for (const [queueName, queueState] of this.queueWorkers.entries()) {
128
+ const workerIndex = queueState.workers.findIndex((w) => w.id === workerId);
129
+ if (workerIndex >= 0) {
130
+ queueState.workers.splice(workerIndex, 1);
131
+
132
+ // Adjust lastWorkerIndex if needed
133
+ if (queueState.lastWorkerIndex >= queueState.workers.length) {
134
+ queueState.lastWorkerIndex = queueState.workers.length > 0 ? 0 : -1;
135
+ }
136
+
137
+ // Also adjust lastUsedWorker if it was this worker
138
+ if (this.lastUsedWorker.get(queueName) === workerId) {
139
+ this.lastUsedWorker.delete(queueName);
140
+ }
141
+
142
+ this.debug(`Worker ${workerId} removed from queue '${queueName}'`);
143
+ }
144
+ }
145
+
146
+ // Remove worker from the pool
147
+ this.workers.delete(workerId);
148
+ this.info(`Worker ${workerId} removed from pool`);
149
+ }
150
+
151
+ /**
152
+ * Terminate all workers in the pool
153
+ */
154
+ public async terminate(): Promise<void> {
155
+ for (const workerId of this.workers.keys()) {
156
+ await this.removeWorker(workerId);
157
+ }
158
+
159
+ this.info('WorkerPool terminated');
160
+ }
161
+
162
+ /**
163
+ * Get the next available worker for a queue using round-robin
164
+ *
165
+ * @param queueName The name of the queue to get a worker for
166
+ * @returns The selected worker info or null if none available
167
+ */
168
+ public getNextWorker(queueName: string): WorkerInfo | null {
169
+ const queueState = this.queueWorkers.get(queueName);
170
+ if (!queueState || queueState.workers.length === 0) {
171
+ this.warning(`No workers available for queue '${queueName}'`);
172
+ return null;
173
+ }
174
+
175
+ // True round-robin selection that considers worker load
176
+ // First, sort workers by number of active jobs (lightest load first)
177
+ const sortedWorkers = [...queueState.workers].sort((a, b) => a.activeJobs - b.activeJobs);
178
+
179
+ // If there's a big difference in load (e.g., more than 5 jobs), prefer the least loaded worker
180
+ const leastLoaded = sortedWorkers[0];
181
+ const mostLoaded = sortedWorkers[sortedWorkers.length - 1];
182
+
183
+ if (mostLoaded.activeJobs - leastLoaded.activeJobs > 5) {
184
+ this.debug(
185
+ `Selected least loaded worker ${leastLoaded.id} for queue '${queueName}' (load: ${leastLoaded.activeJobs}/${mostLoaded.activeJobs})`
186
+ );
187
+ return leastLoaded;
188
+ }
189
+
190
+ // Otherwise, use round-robin to ensure fair distribution
191
+ // Find the index of the last used worker, or -1 if none was used yet
192
+ const lastUsedWorkerId = this.lastUsedWorker.get(queueName);
193
+ let lastIndex = -1;
194
+
195
+ if (lastUsedWorkerId) {
196
+ lastIndex = queueState.workers.findIndex((w) => w.id === lastUsedWorkerId);
197
+ }
198
+
199
+ // Move to the next worker
200
+ const nextIndex = (lastIndex + 1) % queueState.workers.length;
201
+ const selectedWorker = queueState.workers[nextIndex];
202
+
203
+ // Update the last used worker
204
+ this.lastUsedWorker.set(queueName, selectedWorker.id);
205
+ queueState.lastWorkerIndex = nextIndex;
206
+
207
+ this.debug(
208
+ `Selected worker ${selectedWorker.id} for queue '${queueName}' (worker ${nextIndex + 1}/${queueState.workers.length}, active jobs: ${selectedWorker.activeJobs})`
209
+ );
210
+ return selectedWorker;
211
+ }
212
+
213
+ /**
214
+ * Execute a job using a worker from the pool
215
+ *
216
+ * @param job The job to execute
217
+ * @param queueName The queue the job belongs to
218
+ * @param options Optional job dispatch options
219
+ * @returns A promise that resolves when the job is dispatched to a worker
220
+ */
221
+ public async runJob(
222
+ job: Job,
223
+ queueName: string,
224
+ options: JobDispatchOptions = {}
225
+ ): Promise<void> {
226
+ const worker = this.getNextWorker(queueName);
227
+ if (!worker) {
228
+ throw new Error(`No workers available for queue '${queueName}'`);
229
+ }
230
+
231
+ // Assign queue name to job
232
+ job.queueName = queueName;
233
+
234
+ // Apply job options
235
+ const jobOptions = {
236
+ scheduledFor: options.scheduledFor,
237
+ priority: options.priority || 10,
238
+ maxRetries: options.maxRetries,
239
+ retryDelay: options.retryDelay
240
+ };
241
+
242
+ // Dispatch job to worker
243
+ try {
244
+ this.debug(`Dispatching job ${job.id} to worker ${worker.id} in queue '${queueName}'`);
245
+ // Pass job options to worker
246
+ if (typeof worker.addJob === 'function') {
247
+ const result = await worker.addJob(job, queueName);
248
+ if (!result) {
249
+ throw new Error(`Worker ${worker.id} failed to accept job ${job.id}`);
250
+ }
251
+ } else {
252
+ throw new Error(`Worker ${worker.id} does not implement addJob method`);
253
+ }
254
+ } catch (error) {
255
+ // If this worker fails, remove it and try another worker
256
+ this.error(`Failed to dispatch job ${job.id} to worker ${worker.id}: ${error}`);
257
+ await this.removeWorker(worker.id);
258
+
259
+ // Retry with another worker
260
+ await this.runJob(job, queueName, options);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Cancel a job across all workers
266
+ *
267
+ * @param jobId The ID of the job to cancel
268
+ * @param queueName Optional queue name to limit search
269
+ * @returns True if the job was canceled, false otherwise
270
+ */
271
+ public async cancelJob(jobId: string, queueName?: string): Promise<boolean> {
272
+ let canceled = false;
273
+
274
+ if (queueName) {
275
+ // Try to cancel in specific queue
276
+ const queueState = this.queueWorkers.get(queueName);
277
+ if (queueState) {
278
+ // Try each worker in the queue
279
+ for (const worker of queueState.workers) {
280
+ try {
281
+ if (await worker.cancelJob(jobId, queueName)) {
282
+ canceled = true;
283
+ this.debug(`Job ${jobId} cancelled on worker ${worker.id}`);
284
+ break;
285
+ }
286
+ } catch (error) {
287
+ this.warning(`Error cancelling job ${jobId} on worker ${worker.id}: ${error}`);
288
+ }
289
+ }
290
+ }
291
+ } else {
292
+ // Try all workers
293
+ for (const worker of this.workers.values()) {
294
+ try {
295
+ if (await worker.cancelJob(jobId)) {
296
+ canceled = true;
297
+ this.debug(`Job ${jobId} cancelled on worker ${worker.id}`);
298
+ break;
299
+ }
300
+ } catch (error) {
301
+ this.warning(`Error cancelling job ${jobId} on worker ${worker.id}: ${error}`);
302
+ }
303
+ }
304
+ }
305
+
306
+ return canceled;
307
+ }
308
+
309
+ /**
310
+ * Cancel all jobs across all workers
311
+ *
312
+ * @param queueName Optional queue name to limit cancellation
313
+ * @returns The number of jobs canceled
314
+ */
315
+ public async cancelAllJobs(queueName?: string): Promise<number> {
316
+ let total = 0;
317
+
318
+ if (queueName) {
319
+ // Cancel in specific queue
320
+ const queueState = this.queueWorkers.get(queueName);
321
+ if (queueState) {
322
+ for (const worker of queueState.workers) {
323
+ try {
324
+ const count = await worker.cancelAllJobs(queueName);
325
+ total += count;
326
+ this.debug(`Cancelled ${count} jobs on worker ${worker.id} in queue ${queueName}`);
327
+ } catch (error) {
328
+ this.warning(`Error cancelling jobs on worker ${worker.id}: ${error}`);
329
+ }
330
+ }
331
+ }
332
+ } else {
333
+ // Cancel in all workers
334
+ for (const worker of this.workers.values()) {
335
+ try {
336
+ const count = await worker.cancelAllJobs();
337
+ total += count;
338
+ this.debug(`Cancelled ${count} jobs on worker ${worker.id}`);
339
+ } catch (error) {
340
+ this.warning(`Error cancelling jobs on worker ${worker.id}: ${error}`);
341
+ }
342
+ }
343
+ }
344
+
345
+ return total;
346
+ }
347
+
348
+ /**
349
+ * Get all queues being handled by this pool
350
+ *
351
+ * @returns An array of queue names
352
+ */
353
+ public getQueues(): string[] {
354
+ return Array.from(this.queueWorkers.keys());
355
+ }
356
+
357
+ /**
358
+ * Get all workers in the pool
359
+ *
360
+ * @returns An array of worker infos
361
+ */
362
+ public getWorkers(): WorkerInfo[] {
363
+ return Array.from(this.workers.values());
364
+ }
365
+
366
+ /**
367
+ * Get workers for a specific queue
368
+ *
369
+ * @param queueName The name of the queue
370
+ * @returns An array of worker infos for the queue
371
+ */
372
+ public getWorkersForQueue(queueName: string): WorkerInfo[] {
373
+ const queueState = this.queueWorkers.get(queueName);
374
+ return queueState ? [...queueState.workers] : [];
375
+ }
376
+
377
+ /**
378
+ * Get or create queue state for tracking workers
379
+ *
380
+ * @param queueName The name of the queue
381
+ * @returns The queue worker state
382
+ */
383
+ private getOrCreateQueueState(queueName: string): QueueWorkerState {
384
+ if (!this.queueWorkers.has(queueName)) {
385
+ this.queueWorkers.set(queueName, {
386
+ workers: [],
387
+ lastWorkerIndex: -1
388
+ });
389
+ }
390
+
391
+ return this.queueWorkers.get(queueName)!;
392
+ }
393
+ }
@@ -0,0 +1,171 @@
1
+ // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import 'reflect-metadata';
16
+
17
+ import type { Class } from 'type-fest';
18
+ import type { Job } from '../job';
19
+ import type { TransportEvent } from '../transport';
20
+
21
+ import { Service } from '@elysiumjs/core';
22
+
23
+ import { WorkerStatus } from '../worker';
24
+ import { ThreadWorker } from './thread.worker';
25
+
26
+ // Define self for TypeScript
27
+ declare const self: Worker;
28
+
29
+ // Ensure we're in a worker context
30
+ if (typeof self === 'undefined') {
31
+ throw new Error('This script must be run in a worker context');
32
+ }
33
+
34
+ // Log worker startup
35
+ console.log(`Heracles thread worker starting...`);
36
+
37
+ // Set a global worker reference
38
+ let worker: ThreadWorker | null = null;
39
+
40
+ // For storing pre-imported job classes
41
+ const jobClasses = new Map<string, Class<Job>>();
42
+
43
+ // Handle all messages from the main thread
44
+ self.addEventListener('message', async (event) => {
45
+ try {
46
+ // Process the message based on its type
47
+ if (event.data && typeof event.data === 'object') {
48
+ const message = event.data as TransportEvent;
49
+
50
+ switch (message.type) {
51
+ case 'worker:init': {
52
+ // Initialize worker if not already initialized
53
+ if (!worker) {
54
+ const { queues = ['default'], options = {} } = message as any;
55
+
56
+ console.log(`Initializing worker with queues: ${queues.join(', ')}`);
57
+
58
+ // Initialize worker with provided queues
59
+ worker = new ThreadWorker(queues);
60
+
61
+ // Configure worker with options
62
+ if (options.concurrency) {
63
+ for (const queue of queues) {
64
+ await worker.setConcurrency(queue, options.concurrency);
65
+ }
66
+ }
67
+
68
+ // Store worker in global scope for debug purposes
69
+ (self as any).__worker = worker;
70
+
71
+ // Start the worker
72
+ await worker.start();
73
+
74
+ // Send ready message back to main thread
75
+ self.postMessage({
76
+ type: 'worker:ready',
77
+ id: worker.id,
78
+ queues: Array.from(worker.getInfo().queues),
79
+ status: worker.status,
80
+ timestamp: Date.now()
81
+ });
82
+ }
83
+ break;
84
+ }
85
+
86
+ case 'job:process': {
87
+ // Process a job
88
+ if (!worker) {
89
+ throw new Error('Worker not initialized');
90
+ }
91
+
92
+ // Handle the job processing
93
+ const { job: jobName, args = [], queue = 'default', options } = message;
94
+ await handleMessage(message);
95
+ break;
96
+ }
97
+
98
+ default:
99
+ // Other messages will be handled by the ThreadWorker itself
100
+ if (worker) {
101
+ await handleMessage(message);
102
+ } else {
103
+ console.warn(`Received message before worker initialization: ${message.type}`);
104
+
105
+ // Try to handle initialization on the fly
106
+ worker = new ThreadWorker(['default']);
107
+ await worker.start();
108
+ await handleMessage(message);
109
+ }
110
+ break;
111
+ }
112
+ }
113
+ } catch (error) {
114
+ console.error('Error handling message in worker thread:', error);
115
+
116
+ // Report error to main thread
117
+ self.postMessage({
118
+ type: 'worker:error',
119
+ error: error instanceof Error ? error.message : String(error),
120
+ timestamp: Date.now()
121
+ });
122
+ }
123
+ });
124
+
125
+ /**
126
+ * Handle a transport message
127
+ */
128
+ async function handleMessage(message: TransportEvent): Promise<void> {
129
+ if (!worker) {
130
+ throw new Error('Worker not initialized');
131
+ }
132
+
133
+ // Forward the message to the worker instance for processing
134
+ await worker.handleMessage(message);
135
+ }
136
+
137
+ // Send open message to main thread to indicate worker is ready for initialization
138
+ self.postMessage({ type: 'worker:open' });
139
+
140
+ // Handle uncaught errors
141
+ self.addEventListener('error', (event) => {
142
+ console.error('Uncaught error in worker thread:', event.error || event.message);
143
+
144
+ // Report error to main thread
145
+ self.postMessage({
146
+ type: 'worker:error',
147
+ error: event.error?.message || event.message || 'Unknown error',
148
+ timestamp: Date.now()
149
+ });
150
+ });
151
+
152
+ // Handle unhandled promise rejections
153
+ self.addEventListener('unhandledrejection', (event) => {
154
+ console.error('Unhandled rejection in worker thread:', event.reason);
155
+
156
+ // Report error to main thread
157
+ self.postMessage({
158
+ type: 'worker:error',
159
+ error: event.reason?.message || String(event.reason) || 'Unhandled promise rejection',
160
+ timestamp: Date.now()
161
+ });
162
+ });
163
+
164
+ // Initialize custom module loading if needed
165
+ if (typeof Bun !== 'undefined') {
166
+ // Bun-specific initialization
167
+ console.log('Running in Bun environment');
168
+ }
169
+
170
+ // Export ThreadWorker and other components for testing/debugging
171
+ export { ThreadWorker, WorkerStatus };
@@ -0,0 +1,525 @@
1
+ // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { Class } from 'type-fest';
16
+ import type { Job } from '../job';
17
+ import type { JobDispatchOptions } from '../queue';
18
+ import type { TransportEvent } from '../transport';
19
+
20
+ import { Service } from '@elysiumjs/core';
21
+
22
+ import { JobStatus } from '../job';
23
+ import { TransportMode } from '../transport';
24
+ import { ThreadTransport } from '../transports/thread.transport';
25
+ import { BaseWorker } from './base.worker';
26
+
27
+ declare const self: Worker;
28
+
29
+ /**
30
+ * Worker class that receives jobs from parent thread.
31
+ */
32
+ export class ThreadWorker extends BaseWorker {
33
+ /**
34
+ * Transport instance used for communication with main thread
35
+ */
36
+ private readonly transport: ThreadTransport;
37
+
38
+ constructor(queues: string[] = ['default'], id?: string) {
39
+ super(id);
40
+
41
+ this.info(`ThreadWorker ${this.id} started with queues: ${queues.join(', ')}`);
42
+
43
+ // Create transport in consumer mode if in worker thread
44
+ this.transport = new ThreadTransport(TransportMode.CONSUMER);
45
+ this.debug('Created transport in consumer mode');
46
+
47
+ // Only set up message listeners if running in a worker thread
48
+ this.setupMessageListeners();
49
+
50
+ // Initialize the specified queues
51
+ for (const queueName of queues) {
52
+ if (!this.queues.has(queueName)) {
53
+ this.createQueue({ name: queueName });
54
+ }
55
+ }
56
+
57
+ // Report that the worker is ready if in worker thread
58
+ self.postMessage({
59
+ type: 'worker:ready',
60
+ id: this.id,
61
+ queues: Array.from(this.queues.keys()),
62
+ status: this.status
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Set up message listeners for worker thread
68
+ */
69
+ private setupMessageListeners(): void {
70
+ self.addEventListener('message', async (event: MessageEvent) => {
71
+ const message = event.data as TransportEvent;
72
+
73
+ try {
74
+ await this.handleMessage(message);
75
+ } catch (error: any) {
76
+ this.error(`Error handling message: ${error}`);
77
+ self.postMessage({
78
+ type: 'worker:error',
79
+ error: error.message || String(error),
80
+ workerId: this.id
81
+ });
82
+ }
83
+ });
84
+
85
+ // Handle errors
86
+ self.addEventListener('error', (e) => {
87
+ this.trace({ ...e.error, filename: e.filename }, `ThreadWorker ${this.id} error:`);
88
+ self.postMessage({
89
+ type: 'worker:error',
90
+ error: e.error?.message || e.message || 'Unknown error',
91
+ workerId: this.id
92
+ });
93
+ });
94
+
95
+ // Handle unhandled promise rejections
96
+ self.addEventListener('unhandledrejection', (e: any) => {
97
+ const error = e.reason || e;
98
+ this.trace(error, `ThreadWorker ${this.id} unhandled rejection:`);
99
+ self.postMessage({
100
+ type: 'worker:error',
101
+ error: error.message || String(error),
102
+ workerId: this.id
103
+ });
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Handle a message from the main thread
109
+ * @public This method is exposed to be called from thread.worker.entry.ts
110
+ */
111
+ public async handleMessage(message: TransportEvent): Promise<void> {
112
+ switch (message.type) {
113
+ case 'job:process': {
114
+ // Process a new job
115
+ const { job: jobName, args = [], queue = 'default', options } = message;
116
+ this.debug(`ThreadWorker ${this.id} received job ${jobName} for queue ${queue}`);
117
+ await this.handleProcessJob(jobName, args, queue, {
118
+ ...options,
119
+ scheduledFor: new Date(options?.scheduledFor ?? Date.now())
120
+ });
121
+ break;
122
+ }
123
+
124
+ case 'job:cancel': {
125
+ // Cancel a job
126
+ const { jobId, queue = 'default' } = message;
127
+ const canceled = await this.cancelJob(jobId, queue);
128
+ this.sendStatusUpdate({
129
+ type: 'job:canceled',
130
+ jobId,
131
+ queue,
132
+ success: canceled,
133
+ count: canceled ? 1 : 0
134
+ });
135
+ break;
136
+ }
137
+
138
+ case 'job:cancelAll': {
139
+ // Cancel all jobs in a queue
140
+ const { queue = 'default' } = message;
141
+ const canceledCount = await this.cancelAllJobs(queue);
142
+ this.sendStatusUpdate({
143
+ type: 'job:canceled',
144
+ queue,
145
+ count: canceledCount
146
+ });
147
+ break;
148
+ }
149
+
150
+ case 'job:status': {
151
+ // Get job status
152
+ const { jobId, queue = 'default' } = message;
153
+ const job = await this.getJob(jobId, queue);
154
+
155
+ if (job) {
156
+ this.sendStatusUpdate({
157
+ type: 'job:status',
158
+ jobId,
159
+ queue,
160
+ status: job.status,
161
+ error: job.lastError?.message,
162
+ retries: job.retries,
163
+ startedAt: job.startedAt?.toISOString(),
164
+ completedAt: job.completedAt?.toISOString()
165
+ });
166
+ } else {
167
+ this.sendStatusUpdate({
168
+ type: 'job:status',
169
+ jobId,
170
+ queue,
171
+ status: 'unknown'
172
+ });
173
+ }
174
+ break;
175
+ }
176
+
177
+ case 'worker:register': {
178
+ // Add new queues to this worker
179
+ if (message.queues?.length) {
180
+ for (const queueName of message.queues) {
181
+ if (!this.queues.has(queueName)) {
182
+ await this.createQueue({ name: queueName });
183
+ }
184
+ }
185
+
186
+ // Send updated worker info
187
+ this.sendWorkerStatusUpdate();
188
+ }
189
+ break;
190
+ }
191
+
192
+ case 'worker:unregister': {
193
+ // Stop the worker
194
+ await this.stop();
195
+
196
+ // Notify main thread that worker is stopping
197
+ this.sendStatusUpdate({
198
+ type: 'worker:status',
199
+ workerId: this.id,
200
+ status: 'stopped',
201
+ queues: Array.from(this.queues.keys()),
202
+ processing: 0,
203
+ waiting: 0
204
+ });
205
+
206
+ break;
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Process the next job in a queue
213
+ */
214
+ public async processNextJob(queueName: string): Promise<void> {
215
+ const queue = this.queues.get(queueName);
216
+ if (!queue || queue.paused || queue.processing >= queue.options.concurrency!) {
217
+ return;
218
+ }
219
+
220
+ const queuedJob = queue.jobs.shift();
221
+ if (!queuedJob) {
222
+ return;
223
+ }
224
+
225
+ if (queuedJob.job.status === JobStatus.CANCELLED) {
226
+ this.info(`Skipping cancelled job ${queuedJob.job.id}`);
227
+ this.sendStatusUpdate({
228
+ type: 'job:status',
229
+ jobId: queuedJob.job.id,
230
+ queue: queueName,
231
+ status: JobStatus.CANCELLED
232
+ });
233
+
234
+ this.processQueueJobs(queueName);
235
+ return;
236
+ }
237
+
238
+ // Add to active jobs
239
+ queue.activeJobs.set(queuedJob.job.id, queuedJob);
240
+ queue.processing++;
241
+
242
+ // Send job started status update
243
+ try {
244
+ await this.transport.updateJobStatus(queuedJob.job.id, queuedJob.job.dispatchId, queueName, {
245
+ status: JobStatus.RUNNING,
246
+ startedAt: new Date().toISOString()
247
+ });
248
+ } catch (error) {
249
+ this.error(`Error updating job status to RUNNING: ${error}`);
250
+ }
251
+
252
+ try {
253
+ this.debug(`Processing job ${queuedJob.job.id} from queue '${queueName}'`);
254
+ await queuedJob.job.run();
255
+
256
+ // Handle job completion based on status
257
+ if (queuedJob.job.status === JobStatus.FAILED) {
258
+ // Determine max retries - use job-specific setting if available, otherwise queue setting
259
+ const maxRetries =
260
+ queuedJob.maxRetries !== undefined ? queuedJob.maxRetries : queue.options.maxRetries!;
261
+
262
+ if (queuedJob.retries < maxRetries) {
263
+ // Schedule for retry
264
+ queuedJob.retries++;
265
+ queuedJob.job.incrementRetries();
266
+ this.warning(
267
+ `Job ${queuedJob.job.id} failed, retrying (${queuedJob.retries}/${maxRetries})`
268
+ );
269
+
270
+ // Determine retry delay - use job-specific setting if available, otherwise queue setting
271
+ const retryDelay =
272
+ queuedJob.retryDelay !== undefined ? queuedJob.retryDelay : queue.options.retryDelay!;
273
+
274
+ // Set scheduled time for retry
275
+ const retryTime = new Date();
276
+ retryTime.setMilliseconds(retryTime.getMilliseconds() + retryDelay);
277
+ queuedJob.scheduledFor = retryTime;
278
+
279
+ // Send status update for scheduled retry
280
+ try {
281
+ await this.transport.updateJobStatus(queuedJob.job.id, queuedJob.job.id, queueName, {
282
+ status: JobStatus.SCHEDULED_FOR_RETRY,
283
+ error: queuedJob.job.lastError?.message,
284
+ retries: queuedJob.retries
285
+ });
286
+ } catch (error) {
287
+ this.error(`Error updating job status to SCHEDULED_FOR_RETRY: ${error}`);
288
+ }
289
+
290
+ // Schedule the retry
291
+ setTimeout(async () => {
292
+ if (queuedJob.job.status !== JobStatus.CANCELLED) {
293
+ queue.jobs.push(queuedJob);
294
+ this.processQueueJobs(queueName);
295
+ } else {
296
+ this.info(`Skipping cancelled job ${queuedJob.job.id} during retry delay`);
297
+ try {
298
+ await this.transport.updateJobStatus(
299
+ queuedJob.job.id,
300
+ queuedJob.job.id,
301
+ queueName,
302
+ {
303
+ status: JobStatus.CANCELLED
304
+ }
305
+ );
306
+ } catch (error) {
307
+ this.error(`Error updating job status to CANCELLED: ${error}`);
308
+ }
309
+ }
310
+ }, retryDelay);
311
+ } else {
312
+ // Job failed permanently after all retries
313
+ const maxRetries =
314
+ queuedJob.maxRetries !== undefined ? queuedJob.maxRetries : queue.options.maxRetries!;
315
+ this.error(
316
+ `Job ${queuedJob.job.id} failed after ${queuedJob.retries} retries (max: ${maxRetries})`
317
+ );
318
+
319
+ // Send final failure status
320
+ const completedAt = queuedJob.job.completedAt?.toISOString() || new Date().toISOString();
321
+ try {
322
+ await this.transport.updateJobStatus(queuedJob.job.id, queuedJob.job.id, queueName, {
323
+ status: JobStatus.FAILED,
324
+ error: queuedJob.job.lastError?.message,
325
+ completedAt
326
+ });
327
+ } catch (error) {
328
+ this.error(`Error updating job status to FAILED: ${error}`);
329
+ }
330
+
331
+ if (queue.options.pauseOnError) {
332
+ this.pause(queueName);
333
+ this.warning(`Queue '${queueName}' paused due to job failure`);
334
+ }
335
+ }
336
+ } else if (queuedJob.job.status === JobStatus.COMPLETED) {
337
+ // Job completed successfully
338
+ this.success(`Job ${queuedJob.job.id} completed successfully`);
339
+
340
+ // Send completion status
341
+ const completedAt = queuedJob.job.completedAt?.toISOString() || new Date().toISOString();
342
+ try {
343
+ await this.transport.updateJobStatus(queuedJob.job.id, queuedJob.job.id, queueName, {
344
+ status: JobStatus.COMPLETED,
345
+ completedAt
346
+ });
347
+ } catch (error) {
348
+ this.error(`Error updating job status to COMPLETED: ${error}`);
349
+ }
350
+ } else if (queuedJob.job.status === JobStatus.CANCELLED) {
351
+ // Job was cancelled while running
352
+ this.info(`Job ${queuedJob.job.id} cancelled while running`);
353
+
354
+ // Send cancellation status
355
+ const completedAt = queuedJob.job.completedAt?.toISOString() || new Date().toISOString();
356
+ try {
357
+ await this.transport.updateJobStatus(queuedJob.job.id, queuedJob.job.id, queueName, {
358
+ status: JobStatus.CANCELLED,
359
+ completedAt
360
+ });
361
+ } catch (error) {
362
+ this.error(`Error updating job status to CANCELLED: ${error}`);
363
+ }
364
+ }
365
+ } catch (error: any) {
366
+ // Handle unexpected errors during job execution
367
+ this.error(`Unexpected error processing job ${queuedJob.job.id}: ${error}`);
368
+
369
+ // Send error status using transport if available
370
+ const completedAt = new Date().toISOString();
371
+ const errorMsg = error.message || String(error);
372
+ if (this.transport && typeof this.transport.updateJobStatus === 'function') {
373
+ await this.transport.updateJobStatus(queuedJob.job.id, queuedJob.job.id, queueName, {
374
+ status: JobStatus.FAILED,
375
+ error: errorMsg,
376
+ completedAt
377
+ });
378
+ } else {
379
+ this.sendStatusUpdate({
380
+ type: 'job:result',
381
+ jobId: queuedJob.job.id,
382
+ queue: queueName,
383
+ status: JobStatus.FAILED,
384
+ error: errorMsg,
385
+ completedAt
386
+ });
387
+ }
388
+
389
+ if (queue.options.pauseOnError) {
390
+ this.pause(queueName);
391
+ this.warning(`Queue '${queueName}' paused due to unexpected error`);
392
+ }
393
+ } finally {
394
+ // Remove job from active jobs and decrement processing count
395
+ queue.activeJobs.delete(queuedJob.job.id);
396
+ queue.processing--;
397
+
398
+ // Send worker status update
399
+ this.sendWorkerStatusUpdate();
400
+
401
+ // Process next job if available
402
+ this.processQueueJobs(queueName);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Create a job from a job class name and add it to a queue
408
+ */
409
+ private async handleProcessJob(
410
+ jobName: string,
411
+ args: any[],
412
+ queue: string = 'default',
413
+ options?: Partial<JobDispatchOptions>
414
+ ): Promise<void> {
415
+ this.debug(`Processing job ${jobName} in ThreadWorker for queue ${queue}`);
416
+ try {
417
+ // Get job class from service container
418
+ const JobClass = Service.get(jobName) as Class<Job>;
419
+ if (!JobClass) {
420
+ this.error(`Job class ${jobName} not found`);
421
+ throw new Error(`Job class ${jobName} not found`);
422
+ }
423
+
424
+ // Create job instance
425
+ const job = new JobClass(...(args || []));
426
+ job.queueName = queue;
427
+
428
+ // Prepare job options
429
+ const jobOptions: JobDispatchOptions = options || {};
430
+
431
+ // Convert ISO string date to Date object if needed
432
+ if (jobOptions.scheduledFor && typeof jobOptions.scheduledFor === 'string') {
433
+ jobOptions.scheduledFor = new Date(jobOptions.scheduledFor);
434
+ }
435
+
436
+ // Set initial job status
437
+ const initialStatus =
438
+ jobOptions.scheduledFor && jobOptions.scheduledFor > new Date()
439
+ ? JobStatus.SCHEDULED_FOR_RETRY
440
+ : JobStatus.PENDING;
441
+
442
+ try {
443
+ await this.transport.updateJobStatus(job.id, job.dispatchId, queue, {
444
+ status: initialStatus,
445
+ createdAt: new Date().toISOString()
446
+ });
447
+ } catch (error) {
448
+ this.error(`Error setting initial job status: ${error}`);
449
+ }
450
+
451
+ // Add job to queue with options
452
+ await this.addJob(job, queue, jobOptions);
453
+
454
+ // Send job received confirmation
455
+ this.sendStatusUpdate({
456
+ type: 'job:received',
457
+ jobId: job.id,
458
+ queue
459
+ });
460
+ } catch (error: any) {
461
+ // Log and report error
462
+ this.trace(
463
+ error,
464
+ `Error while adding job '${jobName}' to queue '${queue}' with worker ${this.id}`
465
+ );
466
+
467
+ this.sendStatusUpdate({
468
+ type: 'job:error',
469
+ jobId: `error_${Date.now()}`,
470
+ jobName,
471
+ queue,
472
+ error: error.message || String(error)
473
+ });
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Send a status update to the main thread (legacy method)
479
+ * @deprecated Use transport.updateJobStatus instead
480
+ */
481
+ private sendStatusUpdate(message: any): void {
482
+ this.warning(`Using deprecated sendStatusUpdate. Use transport.updateJobStatus instead`);
483
+ self.postMessage(message);
484
+ }
485
+
486
+ /**
487
+ * Send worker status update to main thread
488
+ */
489
+ private sendWorkerStatusUpdate(): void {
490
+ // Calculate waiting and processing counts
491
+ let waiting = 0;
492
+ let processing = 0;
493
+
494
+ for (const queue of this.queues.values()) {
495
+ waiting += queue.jobs.length;
496
+ processing += queue.processing;
497
+ }
498
+
499
+ // Send status update
500
+ self.postMessage({
501
+ type: 'worker:status',
502
+ workerId: this.id,
503
+ queues: Array.from(this.queues.keys()),
504
+ status: this.status as string,
505
+ processing,
506
+ waiting
507
+ });
508
+ }
509
+
510
+ /**
511
+ * Override start method to send status update
512
+ */
513
+ public async start(): Promise<void> {
514
+ await super.start();
515
+ this.sendWorkerStatusUpdate();
516
+ }
517
+
518
+ /**
519
+ * Override stop method to send status update
520
+ */
521
+ public async stop(force: boolean = false): Promise<void> {
522
+ await super.stop(force);
523
+ this.sendWorkerStatusUpdate();
524
+ }
525
+ }