@elysiumjs/heracles 0.4.4 → 0.5.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.
@@ -1,289 +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 { 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
- }
package/src/utils.ts DELETED
@@ -1,61 +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 { Job, JobClass, JobMetadata } from './job';
16
-
17
- export namespace Symbols {
18
- /**
19
- * Symbol for job metadata
20
- */
21
- export const job = Symbol('elysium:job');
22
-
23
- /**
24
- * Symbol for queue metadata
25
- */
26
- export const queue = Symbol('elysium:queue');
27
-
28
- /**
29
- * Symbol for worker metadata
30
- */
31
- export const worker = Symbol('elysium:worker');
32
-
33
- /**
34
- * Symbol for transport metadata
35
- */
36
- export const transport = Symbol('elysium:transport');
37
-
38
- /**
39
- * Symbol for job transaction data
40
- */
41
- export const jobTransaction = Symbol('elysium:jobTransaction');
42
-
43
- /**
44
- * Symbol for worker pool metadata
45
- */
46
- export const workerPool = Symbol('elysium:workerPool');
47
- }
48
-
49
- /**
50
- * Get job metadata from a job class
51
- * @author Axel Nana <axel.nana@workbud.com>
52
- * @param jobClass The job class
53
- * @returns The job metadata or null if not found
54
- */
55
- export const getJobMetadata = <T extends Job>(jobClass: JobClass<T>): JobMetadata | null => {
56
- if (!Reflect.hasMetadata(Symbols.job, jobClass)) {
57
- return null;
58
- }
59
-
60
- return Reflect.getMetadata(Symbols.job, jobClass) as JobMetadata;
61
- };
package/src/worker.ts DELETED
@@ -1,312 +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 { Job, JobOverlapBehavior } from './job';
16
- import type { QueueOptions } from './queue';
17
- import type { Transport } from './transport';
18
-
19
- /**
20
- * Represents a job in the queue with its metadata.
21
- * @author Axel Nana <axel.nana@workbud.com>
22
- */
23
- export type QueuedJob = {
24
- /**
25
- * The job instance.
26
- */
27
- job: Job;
28
-
29
- /**
30
- * The number of times this job has been retried.
31
- */
32
- retries: number;
33
-
34
- /**
35
- * The queue this job belongs to.
36
- */
37
- queueName: string;
38
-
39
- /**
40
- * When the job was added to the queue.
41
- */
42
- enqueuedAt: Date;
43
-
44
- /**
45
- * When the job is scheduled to be processed.
46
- * This is used for delayed jobs or retry scheduling.
47
- */
48
- scheduledFor?: Date;
49
-
50
- /**
51
- * Priority of this job (lower number = higher priority).
52
- * @see JobDispatchOptions.priority
53
- */
54
- priority?: number;
55
-
56
- /**
57
- * Maximum number of retries for this specific job.
58
- * Overrides queue-level setting if specified.
59
- * @see JobDispatchOptions.maxRetries
60
- */
61
- maxRetries?: number;
62
-
63
- /**
64
- * Delay between retries in milliseconds for this specific job.
65
- * Overrides queue-level setting if specified.
66
- * @see JobDispatchOptions.retryDelay
67
- */
68
- retryDelay?: number;
69
-
70
- /**
71
- * Defines how the job handles overlap with other instances of the same job ID.
72
- * @see JobOverlapBehavior
73
- */
74
- overlapBehavior?: JobOverlapBehavior;
75
-
76
- /**
77
- * When using NO_OVERLAP behavior, specifies the delay in milliseconds
78
- * between sequential executions of jobs with the same ID.
79
- */
80
- overlapDelay?: number;
81
- };
82
-
83
- /**
84
- * State associated with a job queue managed by a worker.
85
- * @author Axel Nana <axel.nana@workbud.com>
86
- */
87
- export type QueueState = {
88
- /**
89
- * The queue configuration options.
90
- */
91
- options: QueueOptions;
92
-
93
- /**
94
- * The jobs waiting to be processed.
95
- */
96
- jobs: QueuedJob[];
97
-
98
- /**
99
- * The jobs currently being processed.
100
- */
101
- activeJobs: Map<string, QueuedJob>;
102
-
103
- /**
104
- * The number of jobs currently being processed.
105
- */
106
- processing: number;
107
-
108
- /**
109
- * Whether the queue is currently paused.
110
- */
111
- paused: boolean;
112
-
113
- /**
114
- * Whether the queue is currently being drained (completing existing jobs but not accepting new ones).
115
- */
116
- draining: boolean;
117
-
118
- /**
119
- * The transport used by this queue, if different from the worker's default.
120
- */
121
- transport?: Transport;
122
- };
123
-
124
- /**
125
- * Worker status information.
126
- * @author Axel Nana <axel.nana@workbud.com>
127
- */
128
- export enum WorkerStatus {
129
- /**
130
- * Worker is idle - no jobs are being processed.
131
- */
132
- IDLE = 'idle',
133
-
134
- /**
135
- * Worker is active - processing jobs.
136
- */
137
- ACTIVE = 'active',
138
-
139
- /**
140
- * Worker is paused - not accepting new jobs.
141
- */
142
- PAUSED = 'paused',
143
-
144
- /**
145
- * Worker is draining - completing current jobs but not accepting new ones.
146
- */
147
- DRAINING = 'draining',
148
-
149
- /**
150
- * Worker is stopping - gracefully shutting down.
151
- */
152
- STOPPING = 'stopping',
153
-
154
- /**
155
- * Worker is stopped - no longer processing jobs.
156
- */
157
- STOPPED = 'stopped',
158
-
159
- /**
160
- * Worker is disconnected - lost connection.
161
- */
162
- DISCONNECTED = 'disconnected'
163
- }
164
-
165
- /**
166
- * Worker information used by the worker pool.
167
- * @author Axel Nana <axel.nana@workbud.com>
168
- */
169
- export type WorkerInfo = {
170
- /**
171
- * The worker ID.
172
- */
173
- id: string;
174
-
175
- /**
176
- * The worker status.
177
- */
178
- status: WorkerStatus;
179
-
180
- /**
181
- * The queues handled by this worker.
182
- */
183
- queues: string[];
184
-
185
- /**
186
- * The jobs being processed by this worker.
187
- */
188
- activeJobs: number;
189
-
190
- /**
191
- * The transport used by this worker.
192
- */
193
- transport: string;
194
-
195
- /**
196
- * Start time of this worker.
197
- */
198
- startedAt: Date;
199
-
200
- /**
201
- * Add a job to this worker.
202
- */
203
- addJob(job: Job, queueName?: string): Promise<boolean>;
204
-
205
- /**
206
- * Cancel a job on this worker.
207
- */
208
- cancelJob(jobId: string, queueName?: string): Promise<boolean>;
209
-
210
- /**
211
- * Cancel all jobs on this worker.
212
- */
213
- cancelAllJobs(queueName?: string): Promise<number>;
214
- };
215
-
216
- /**
217
- * Standardized interface for a worker managing job queues.
218
- * @author Axel Nana <axel.nana@workbud.com>
219
- */
220
- export interface Worker {
221
- /**
222
- * The worker ID.
223
- */
224
- id: string;
225
-
226
- /**
227
- * The worker status.
228
- */
229
- status: WorkerStatus;
230
-
231
- /**
232
- * Create a new queue with the specified options.
233
- */
234
- createQueue(options: QueueOptions): Promise<Worker>;
235
-
236
- /**
237
- * Add a job to a queue.
238
- */
239
- addJob(job: Job, queueName?: string): Promise<Worker>;
240
-
241
- /**
242
- * Pause a queue to stop processing new jobs.
243
- */
244
- pause(queueName?: string): Promise<Worker>;
245
-
246
- /**
247
- * Resume a paused queue.
248
- */
249
- resume(queueName?: string): Promise<Worker>;
250
-
251
- /**
252
- * Drain a queue - complete current jobs but don't accept new ones.
253
- */
254
- drain(queueName?: string): Promise<Worker>;
255
-
256
- /**
257
- * Clear all jobs from a queue.
258
- */
259
- clear(queueName?: string): Promise<Worker>;
260
-
261
- /**
262
- * Get the number of jobs in a queue.
263
- */
264
- size(queueName?: string): Promise<number>;
265
-
266
- /**
267
- * Get the total number of jobs across all queues.
268
- */
269
- totalSize(): Promise<number>;
270
-
271
- /**
272
- * Get statistics about all queues.
273
- */
274
- getStats(): Promise<
275
- Record<string, { waiting: number; processing: number; paused: boolean; draining: boolean }>
276
- >;
277
-
278
- /**
279
- * Get information about this worker for the worker pool.
280
- */
281
- getInfo(): WorkerInfo;
282
-
283
- /**
284
- * Cancel a job.
285
- */
286
- cancelJob(jobId: string, queueName?: string): Promise<boolean>;
287
-
288
- /**
289
- * Cancel all jobs.
290
- */
291
- cancelAllJobs(queueName?: string): Promise<number>;
292
-
293
- /**
294
- * Get the job with the given ID.
295
- */
296
- getJob(jobId: string, queueName?: string): Promise<Job | null>;
297
-
298
- /**
299
- * Start the worker.
300
- */
301
- start(): Promise<void>;
302
-
303
- /**
304
- * Stop the worker.
305
- */
306
- stop(force?: boolean): Promise<void>;
307
-
308
- /**
309
- * Set the concurrency for a queue.
310
- */
311
- setConcurrency(queueName: string, concurrency: number): Promise<void>;
312
- }