@elysiumjs/heracles 0.4.3 → 0.5.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.
@@ -1,525 +0,0 @@
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
- }
package/types.d.ts DELETED
@@ -1,21 +0,0 @@
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
- declare namespace globalThis {
16
- /**
17
- * A macro indicating if the code is running from a build.
18
- * @author Axel Nana <axel.nana@workbud.com>
19
- */
20
- var ELYSIUM_BUILD: boolean;
21
- }