Skip to content

Commit f02a8f9

Browse files
committed
feat: enhance job handler registration in queue system
- Introduced `setHandlers` method to allow bulk registration of job handlers with improved type safety. - Added `getHandler` method to retrieve the current handler for a specific job type. - Implemented `validateHandlers` method to ensure handlers are registered before queue execution. - Updated various queue implementations and tests to utilize the new handler registration methods.
1 parent 3da9e1a commit f02a8f9

15 files changed

Lines changed: 529 additions & 159 deletions

File tree

example-queue-project/src/email-queue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FileQueue } from "@muniter/queue";
1+
import { FileQueue, Queue } from "@muniter/queue";
22
import { createSQSQueue } from "@muniter/queue/sqs";
33

44
interface EmailJobs {

src/core/queue.ts

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EventEmitter } from 'events';
2-
import type { JobStatus, JobMeta, QueueMessage, QueueEvent, JobData, JobOptions, BaseJobRequest } from '../interfaces/job.ts';
2+
import type { JobStatus, JobMeta, QueueMessage, QueueEvent, JobData, JobOptions, BaseJobRequest, JobContext, JobHandlers } from '../interfaces/job.ts';
33
import type { QueuePlugin, QueueOptions } from '../interfaces/plugin.ts';
44

55
/**
@@ -34,6 +34,16 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
3434
protected pluginDisposers: Array<() => Promise<void>> = [];
3535
public readonly name?: string;
3636

37+
/**
38+
* Registry of job handlers mapping job names to their handler functions.
39+
*/
40+
public handlers: Map<string, Function> = new Map();
41+
42+
/**
43+
* Flag indicating whether handlers have been registered.
44+
*/
45+
public handlersRegistered = false;
46+
3747
/**
3848
* Indicates whether this queue driver supports long polling.
3949
* Drivers that support long polling (like SQS) can efficiently wait for messages.
@@ -111,31 +121,78 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
111121
}
112122

113123
/**
114-
* Registers a handler function for a specific job type with type safety.
124+
* Sets all job handlers at once. This method must be called before starting the queue.
125+
* All job types defined in TJobMap must have corresponding handlers.
126+
*
127+
* @param handlers - Complete mapping of job names to their handler functions
128+
*
129+
* @example
130+
* ```typescript
131+
* queue.setHandlers({
132+
* 'send-email': async ({ payload }, queue) => {
133+
* await emailService.send(payload.to, payload.subject, payload.body);
134+
* },
135+
* 'resize-image': async (job, queue) => {
136+
* const { payload, id } = job;
137+
* console.log(`Processing image resize job ${id}`);
138+
* await imageService.resize(payload.url, payload.width, payload.height);
139+
* }
140+
* });
141+
* ```
142+
*/
143+
setHandlers(handlers: JobHandlers<TJobMap>): void {
144+
this.handlers.clear();
145+
for (const [jobName, handler] of Object.entries(handlers) as Array<[string, Function]>) {
146+
this.handlers.set(jobName, handler);
147+
}
148+
this.handlersRegistered = true;
149+
}
150+
151+
/**
152+
* Sets or replaces a handler for a specific job type.
153+
* Useful for testing or dynamically updating handlers.
115154
*
116155
* @template K - The job name type from TJobMap
117156
* @param jobName - The name of the job type to handle
118157
* @param handler - Function to execute when this job type is processed
119-
* @returns This queue instance for method chaining
120158
*
121159
* @example
122160
* ```typescript
123-
* queue.onJob('send-email', async (payload) => {
124-
* // payload is automatically typed as { to: string; subject: string; body: string }
125-
* await emailService.send(payload.to, payload.subject, payload.body);
126-
* });
127-
*
128-
* queue.onJob('resize-image', async (payload) => {
129-
* // payload is automatically typed as { url: string; width: number; height: number }
130-
* await imageService.resize(payload.url, payload.width, payload.height);
161+
* // Replace handler for testing
162+
* queue.setHandler('send-email', async ({ payload }, queue) => {
163+
* console.log('Mock email sent to:', payload.to);
131164
* });
132165
* ```
133166
*/
134-
onJob<K extends keyof TJobMap>(
167+
setHandler<K extends keyof TJobMap>(
135168
jobName: K,
136-
handler: (payload: TJobMap[K]) => Promise<void>
137-
): this {
138-
return super.on(`job:${String(jobName)}`, handler);
169+
handler: (job: JobContext<TJobMap[K]>, queue: Queue<TJobMap>) => Promise<void> | void
170+
): void {
171+
this.handlers.set(String(jobName), handler);
172+
}
173+
174+
/**
175+
* Gets the current handler for a specific job type.
176+
* Useful for testing or introspection.
177+
*
178+
* @template K - The job name type from TJobMap
179+
* @param jobName - The name of the job type
180+
* @returns The handler function or undefined if not registered
181+
*/
182+
getHandler<K extends keyof TJobMap>(jobName: K): Function | undefined {
183+
return this.handlers.get(String(jobName));
184+
}
185+
186+
/**
187+
* Validates that handlers have been registered before starting the queue.
188+
*/
189+
public validateHandlers(): void {
190+
if (!this.handlersRegistered) {
191+
throw new Error(
192+
'Handlers must be registered with setHandlers() before calling run(). ' +
193+
'Use queue.setHandlers({ ... }) to register all job type handlers.'
194+
);
195+
}
139196
}
140197

141198
override on(event: string | symbol, listener: (...args: any[]) => void): this {
@@ -162,6 +219,9 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
162219
* ```
163220
*/
164221
async run(repeat: boolean = false, timeout: number = 0): Promise<void> {
222+
// Validate that handlers have been registered
223+
this.validateHandlers();
224+
165225
const disposers = [...this.pluginDisposers];
166226

167227
// 1. Initialize plugins if not already initialized
@@ -295,18 +355,23 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
295355
const beforeEvent: QueueEvent = { type: 'beforeExec', id: message.id, name, payload, meta: message.meta };
296356
this.emit('beforeExec', beforeEvent);
297357

298-
// Execute the job handler
299-
const jobEvent = `job:${name}`;
300-
301-
if (this.listenerCount(jobEvent) === 0) {
358+
// Get the registered handler for this job type
359+
const handler = this.handlers.get(name);
360+
if (!handler) {
302361
throw new Error(`No handler registered for job type: ${name}`);
303362
}
304363

305-
// Get all handlers for this job type and execute them
306-
const handlers = this.listeners(jobEvent) as Array<(payload: any) => Promise<void>>;
307-
const results = await Promise.all(handlers.map(handler => handler(payload)));
308-
309-
const result = results.length === 1 ? results[0] : results;
364+
// Create job context object with full job information
365+
const jobContext: JobContext<any> = {
366+
id: message.id,
367+
payload: payload,
368+
meta: message.meta,
369+
pushedAt: message.meta.pushedAt,
370+
reservedAt: message.meta.reservedAt
371+
};
372+
373+
// Execute the handler with job context and queue reference
374+
const result = await handler(jobContext, this);
310375

311376
const afterEvent: QueueEvent = { type: 'afterExec', id: message.id, name, payload, meta: message.meta, result };
312377
this.emit('afterExec', afterEvent);

src/drivers/sqs.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ import type {
1818
} from "../interfaces/job.ts";
1919
import type { QueueOptions } from "../interfaces/plugin.ts";
2020

21+
interface SqsClient {
22+
send: SQSClient["send"];
23+
}
24+
2125
export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
2226
TJobMap,
2327
SqsJobRequest<any>
2428
> {
2529
#onFailure: "delete" | "leaveInQueue"
2630

2731
constructor(
28-
private client: SQSClient,
32+
private client: SqsClient,
2933
private queueUrl: string,
3034
options: QueueOptions & { onFailure: "delete" | "leaveInQueue" } = {
3135
onFailure: "delete",

src/interfaces/job.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ export interface JobMeta {
1010
receiptHandle?: string; // For SQS
1111
}
1212

13+
/**
14+
* Job context object passed to handlers containing full job information.
15+
*/
16+
export interface JobContext<T> {
17+
id: string;
18+
payload: T;
19+
meta: JobMeta;
20+
pushedAt?: Date;
21+
reservedAt?: Date;
22+
}
23+
24+
/**
25+
* Type for a single job handler function.
26+
*/
27+
export type JobHandler<T> = (job: JobContext<T>, queue: any) => Promise<void> | void;
28+
29+
/**
30+
* Type mapping all job types to their corresponding handlers.
31+
* Ensures type safety and completeness of handler registration.
32+
*/
33+
export type JobHandlers<TJobMap> = {
34+
[K in keyof TJobMap]: JobHandler<TJobMap[K]>;
35+
}
36+
1337
export interface QueueMessage {
1438
id: string;
1539
payload: string;

0 commit comments

Comments
 (0)