11import { EventEmitter } from 'events' ;
22import type { JobStatus , JobMeta , QueueMessage , QueueEvent , JobData , JobOptions , BaseJobRequest } from '../interfaces/job.ts' ;
3+ import type { QueuePlugin , QueueOptions } from '../interfaces/plugin.ts' ;
34
45/**
56 * Abstract queue class providing event-based job processing.
@@ -29,16 +30,23 @@ import type { JobStatus, JobMeta, QueueMessage, QueueEvent, JobData, JobOptions,
2930 */
3031export abstract class Queue < TJobMap = Record < string , any > , TJobRequest extends BaseJobRequest < any > = BaseJobRequest < any > > extends EventEmitter {
3132 protected ttrDefault = 300 ;
33+ protected plugins : QueuePlugin [ ] ;
34+ protected pluginDisposers : Array < ( ) => Promise < void > > = [ ] ;
35+ public readonly name ?: string ;
3236
3337 /**
3438 * Creates a new Queue instance.
3539 *
3640 * @param options - Configuration options
3741 * @param options.ttrDefault - Default time-to-run for jobs in seconds (default: 300)
42+ * @param options.name - Optional name for the queue
43+ * @param options.plugins - Array of plugins to use with this queue
3844 */
39- constructor ( options : { ttrDefault ?: number } = { } ) {
45+ constructor ( options : QueueOptions = { } ) {
4046 super ( ) ;
4147 if ( options . ttrDefault ) this . ttrDefault = options . ttrDefault ;
48+ this . name = options . name ;
49+ this . plugins = options . plugins || [ ] ;
4250 }
4351
4452
@@ -147,22 +155,111 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
147155 * ```
148156 */
149157 async run ( repeat : boolean = false , timeout : number = 0 ) : Promise < void > {
150- const canContinue = ( ) => true ;
158+ const disposers = [ ... this . pluginDisposers ] ;
151159
152- while ( canContinue ( ) ) {
153- const message = await this . reserve ( timeout ) ;
154-
155- if ( ! message ) {
156- if ( ! repeat ) break ;
157- if ( timeout > 0 ) {
158- await this . sleep ( timeout * 1000 ) ;
160+ // 1. Initialize plugins if not already initialized
161+ if ( this . pluginDisposers . length === 0 ) {
162+ for ( const plugin of this . plugins ) {
163+ if ( plugin . init ) {
164+ const dispose = await plugin . init ( { queue : this as any , queueName : this . name } ) ;
165+ if ( dispose ) {
166+ this . pluginDisposers . push ( dispose ) ;
167+ disposers . push ( dispose ) ;
168+ }
159169 }
160- continue ;
161170 }
171+ }
172+
173+ try {
174+ // 2. Main processing loop (enhancing existing loop)
175+ let stopped = false ;
176+
177+ while ( ! stopped ) {
178+ // Check if any plugin wants to stop
179+ try {
180+ for ( const plugin of this . plugins ) {
181+ if ( plugin . beforePoll ) {
182+ const result = await plugin . beforePoll ( ) ;
183+ if ( result === 'stop' ) {
184+ stopped = true ;
185+ break ;
186+ }
187+ }
188+ }
189+ } catch ( error ) {
190+ console . error ( 'Plugin beforePoll error:' , error ) ;
191+ // Continue polling despite plugin error
192+ }
193+ if ( stopped ) break ;
194+
195+ const message = await this . reserve ( timeout ) ;
196+ if ( ! message ) {
197+ if ( ! repeat ) break ;
198+ if ( timeout > 0 ) {
199+ await this . sleep ( timeout * 1000 ) ;
200+ }
201+ continue ;
202+ }
162203
163- const success = await this . handleMessage ( message ) ;
164- if ( success ) {
165- await this . release ( message ) ;
204+ // 3. Pre-execution hooks
205+ try {
206+ for ( const plugin of this . plugins ) {
207+ if ( plugin . beforeJob ) {
208+ await plugin . beforeJob ( message ) ;
209+ }
210+ }
211+ } catch ( error ) {
212+ console . error ( 'Plugin beforeJob error:' , error ) ;
213+ // Continue processing despite plugin error
214+ }
215+
216+ // 4. Execute job (with plugin hooks)
217+ let success = false ;
218+ let jobError : unknown ;
219+
220+ // We need to track errors for plugins, but handleMessage catches them internally
221+ // So we'll set up an event listener to capture the error
222+ let capturedError : unknown ;
223+ const errorListener = ( event : QueueEvent ) => {
224+ if ( event . type === 'afterError' && event . id === message . id ) {
225+ capturedError = event . error ;
226+ }
227+ } ;
228+
229+ this . once ( 'afterError' , errorListener ) ;
230+
231+ try {
232+ success = await this . handleMessage ( message ) ;
233+ jobError = capturedError ; // Will be undefined if no error
234+ } catch ( error ) {
235+ // This shouldn't happen since handleMessage catches errors
236+ jobError = error ;
237+ success = false ;
238+ } finally {
239+ this . removeListener ( 'afterError' , errorListener ) ;
240+ }
241+
242+ // 5. Post-execution hooks
243+ try {
244+ for ( const plugin of this . plugins ) {
245+ if ( plugin . afterJob ) {
246+ await plugin . afterJob ( message , jobError ) ;
247+ }
248+ }
249+ } catch ( error ) {
250+ console . error ( 'Plugin afterJob error:' , error ) ;
251+ // Don't let plugin errors affect job completion
252+ }
253+
254+ // Complete the job if successful
255+ if ( success ) {
256+ await this . release ( message ) ;
257+ }
258+ }
259+ } finally {
260+ // 6. Cleanup only if we initialized in this run
261+ for ( const dispose of disposers . reverse ( ) ) {
262+ await dispose ( ) ;
166263 }
167264 }
168265 }
@@ -176,6 +273,7 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
176273 */
177274 protected async handleMessage ( message : QueueMessage ) : Promise < boolean > {
178275 try {
276+ // Parse the job data (this may have been modified by plugins)
179277 const jobData : JobData = JSON . parse ( message . payload ) ;
180278 const { name, payload } = jobData ;
181279
0 commit comments