88 waitForNextEvent ,
99 waitForNextJob ,
1010 waitForNthJob ,
11+ type EmailJob ,
1112 type EmailJobPayload ,
1213 type EmailJobResult ,
1314} from "test/utils" ;
@@ -52,6 +53,7 @@ describe("PrismaQueue", () => {
5253 expect ( Object . keys ( job ) ) . toMatchInlineSnapshot ( `
5354 [
5455 "id",
56+ "createdAt",
5557 ]
5658 ` ) ;
5759 const record = await job . fetch ( ) ;
@@ -66,6 +68,7 @@ describe("PrismaQueue", () => {
6668 expect ( Object . keys ( job ) ) . toMatchInlineSnapshot ( `
6769 [
6870 "id",
71+ "createdAt",
6972 ]
7073 ` ) ;
7174 const record = await job . fetch ( ) ;
@@ -183,24 +186,44 @@ describe("PrismaQueue", () => {
183186 } ) ;
184187 it ( "should properly handle multiple restarts" , async ( ) => {
185188 const JOB_WAIT = 50 ;
186- void queue . stop ( ) ;
189+
190+ // Stop the queue that was started in beforeEach
191+ await queue . stop ( ) ;
192+
187193 queue . worker = vi . fn ( async ( _job ) => {
188194 await waitFor ( JOB_WAIT ) ;
189195 return { code : "200" } ;
190196 } ) ;
197+
198+ // Enqueue 2 jobs while stopped
191199 await Promise . all ( [
192200 queue . enqueue ( { email :
"[email protected] " } ) , 193201 queue . enqueue ( { email :
"[email protected] " } ) , 194202 ] ) ;
195- void queue . start ( ) ;
203+
204+ // Verify no jobs processed yet
196205 expect ( queue . worker ) . toHaveBeenCalledTimes ( 0 ) ;
197- void queue . stop ( ) ;
206+
207+ // Start briefly and stop (simulating an interrupted start)
198208 void queue . start ( ) ;
199209 await waitFor ( 10 ) ;
200- expect ( queue . worker ) . toHaveBeenCalledTimes ( 1 ) ;
201- await waitFor ( JOB_WAIT + 10 ) ;
202- expect ( queue . worker ) . toHaveBeenCalledTimes ( 1 ) ;
203- } ) ;
210+ await queue . stop ( ) ;
211+
212+ // May or may not have started processing
213+ const countAfterInterruption = queue . worker . mock . calls . length ;
214+
215+ // Now properly start and let all jobs complete
216+ void queue . start ( ) ;
217+
218+ // Wait for the remaining jobs to complete
219+ const remainingJobs = 2 - countAfterInterruption ;
220+ if ( remainingJobs > 0 ) {
221+ await waitForNthJob ( queue , remainingJobs ) ;
222+ }
223+
224+ // Eventually both jobs should be processed
225+ expect ( queue . worker . mock . calls . length ) . toBe ( 2 ) ;
226+ } , 10000 ) ; // Increase timeout for this test
204227 afterAll ( ( ) => {
205228 void queue . stop ( ) ;
206229 } ) ;
@@ -432,4 +455,145 @@ describe("PrismaQueue", () => {
432455 void queue . stop ( ) ;
433456 } ) ;
434457 } ) ;
458+
459+ describe ( "polling behavior" , ( ) => {
460+ let queue : PrismaQueue < EmailJobPayload , EmailJobResult > ;
461+ beforeAll ( ( ) => {
462+ queue = createEmailQueue ( { pollInterval : 100 , jobInterval : 10 } ) ;
463+ } ) ;
464+ beforeEach ( async ( ) => {
465+ await prisma . queueJob . deleteMany ( ) ;
466+ } ) ;
467+ afterEach ( ( ) => {
468+ void queue . stop ( ) ;
469+ } ) ;
470+
471+ it ( "should not process more jobs than exist in queue" , async ( ) => {
472+ const jobsProcessed : bigint [ ] = [ ] ;
473+ queue . worker = vi . fn ( async ( job : EmailJob ) => {
474+ jobsProcessed . push ( job . id ) ;
475+ await waitFor ( 50 ) ;
476+ return { code : "200" } ;
477+ } ) ;
478+
479+ // Enqueue 3 jobs
480+ await Promise . all ( [
481+ queue . enqueue ( { email :
"[email protected] " } ) , 482+ queue . enqueue ( { email :
"[email protected] " } ) , 483+ queue . enqueue ( { email :
"[email protected] " } ) , 484+ ] ) ;
485+
486+ void queue . start ( ) ;
487+ await waitForNthJob ( queue , 3 ) ;
488+
489+ // Should process exactly 3 jobs, no more
490+ expect ( queue . worker ) . toHaveBeenCalledTimes ( 3 ) ;
491+ expect ( new Set ( jobsProcessed ) . size ) . toBe ( 3 ) ; // All unique job IDs
492+ } ) ;
493+
494+ it ( "should respect concurrency limits when processing burst of jobs" , async ( ) => {
495+ const concurrentQueue = createEmailQueue ( { pollInterval : 100 , jobInterval : 10 , maxConcurrency : 2 } ) ;
496+ const processing : bigint [ ] = [ ] ;
497+ const completed : bigint [ ] = [ ] ;
498+ let maxConcurrent = 0 ;
499+
500+ concurrentQueue . worker = vi . fn ( async ( job : EmailJob ) => {
501+ processing . push ( job . id ) ;
502+ maxConcurrent = Math . max ( maxConcurrent , processing . length ) ;
503+ await waitFor ( 100 ) ;
504+ completed . push ( job . id ) ;
505+ processing . splice ( processing . indexOf ( job . id ) , 1 ) ;
506+ return { code : "200" } ;
507+ } ) ;
508+
509+ // Enqueue 5 jobs
510+ await Promise . all ( [
511+ concurrentQueue . enqueue ( { email :
"[email protected] " } ) , 512+ concurrentQueue . enqueue ( { email :
"[email protected] " } ) , 513+ concurrentQueue . enqueue ( { email :
"[email protected] " } ) , 514+ concurrentQueue . enqueue ( { email :
"[email protected] " } ) , 515+ concurrentQueue . enqueue ( { email :
"[email protected] " } ) , 516+ ] ) ;
517+
518+ void concurrentQueue . start ( ) ;
519+ await waitForNthJob ( concurrentQueue , 5 ) ;
520+ await concurrentQueue . stop ( ) ;
521+
522+ // Should never exceed maxConcurrency
523+ expect ( maxConcurrent ) . toBeLessThanOrEqual ( 2 ) ;
524+ expect ( concurrentQueue . worker ) . toHaveBeenCalledTimes ( 5 ) ;
525+ } ) ;
526+
527+ it ( "should process jobs in priority order" , async ( ) => {
528+ const processedEmails : string [ ] = [ ] ;
529+ // eslint-disable-next-line @typescript-eslint/require-await
530+ queue . worker = vi . fn ( async ( job : EmailJob ) => {
531+ processedEmails . push ( job . payload . email ) ;
532+ return { code : "200" } ;
533+ } ) ;
534+
535+ // Enqueue jobs with different priorities
536+ await queue . enqueue ( { email :
"[email protected] " } , { priority :
0 } ) ; 537+ await queue . enqueue ( { email :
"[email protected] " } , { priority :
- 10 } ) ; 538+ await queue . enqueue ( { email :
"[email protected] " } , { priority :
10 } ) ; 539+
540+ void queue . start ( ) ;
541+ await waitForNthJob ( queue , 3 ) ;
542+
543+ // Should process in priority order (lower priority value = higher priority)
544+ expect ( processedEmails [ 0 ] ) . toBe ( "[email protected] " ) ; 545+ expect ( processedEmails [ 1 ] ) . toBe ( "[email protected] " ) ; 546+ expect ( processedEmails [ 2 ] ) . toBe ( "[email protected] " ) ; 547+ } ) ;
548+
549+ it ( "should continue polling after queue becomes empty" , async ( ) => {
550+ queue . worker = vi . fn ( async ( _job ) => {
551+ await waitFor ( 50 ) ;
552+ return { code : "200" } ;
553+ } ) ;
554+
555+ await queue . enqueue ( { email :
"[email protected] " } ) ; 556+ void queue . start ( ) ;
557+ await waitForNextJob ( queue ) ;
558+ expect ( queue . worker ) . toHaveBeenCalledTimes ( 1 ) ;
559+
560+ // Queue is now empty, should continue polling
561+ await waitFor ( 150 ) ; // Wait more than pollInterval
562+
563+ // Add another job - should be picked up
564+ await queue . enqueue ( { email :
"[email protected] " } ) ; 565+ await waitForNextJob ( queue ) ;
566+ expect ( queue . worker ) . toHaveBeenCalledTimes ( 2 ) ;
567+ } ) ;
568+
569+ it ( "should handle jobs added while processing" , async ( ) => {
570+ let firstJobProcessing = false ;
571+ queue . worker = vi . fn ( async ( job : EmailJob ) => {
572+ if ( job . payload . email === "[email protected] " ) { 573+ firstJobProcessing = true ;
574+ await waitFor ( 100 ) ;
575+ firstJobProcessing = false ;
576+ }
577+ return { code : "200" } ;
578+ } ) ;
579+
580+ await queue . enqueue ( { email :
"[email protected] " } ) ; 581+ void queue . start ( ) ;
582+
583+ // Wait for first job to start processing
584+ await waitFor ( 20 ) ;
585+ expect ( firstJobProcessing ) . toBe ( true ) ;
586+
587+ // Add second job while first is processing
588+ await queue . enqueue ( { email :
"[email protected] " } ) ; 589+
590+ // Both should complete
591+ await waitForNthJob ( queue , 2 ) ;
592+ expect ( queue . worker ) . toHaveBeenCalledTimes ( 2 ) ;
593+ } ) ;
594+
595+ afterAll ( ( ) => {
596+ void queue . stop ( ) ;
597+ } ) ;
598+ } ) ;
435599} ) ;
0 commit comments