@onebun/core 0.2.11 → 0.2.13
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.
- package/package.json +6 -2
- package/src/application/application.test.ts +325 -8
- package/src/application/application.ts +23 -6
- package/src/decorators/decorators.test.ts +1 -1
- package/src/decorators/decorators.ts +1 -1
- package/src/decorators/metadata.test.ts +86 -0
- package/src/decorators/metadata.ts +199 -0
- package/src/docs-examples.test.ts +2 -1
- package/src/index.ts +2 -2
- package/src/module/module.ts +36 -0
- package/src/queue/adapters/memory.adapter.test.ts +0 -4
- package/src/queue/adapters/memory.adapter.ts +0 -46
- package/src/queue/adapters/redis.adapter.test.ts +0 -64
- package/src/queue/adapters/redis.adapter.ts +0 -41
- package/src/queue/docs-examples.test.ts +220 -9
- package/src/queue/index.ts +8 -1
- package/src/queue/queue-service-proxy.test.ts +12 -3
- package/src/queue/queue-service-proxy.ts +37 -7
- package/src/queue/queue.service.test.ts +138 -16
- package/src/queue/queue.service.ts +48 -11
- package/src/queue/scheduler.test.ts +280 -0
- package/src/queue/scheduler.ts +156 -3
- package/src/queue/types.ts +75 -27
- package/src/testing/test-utils.ts +1 -1
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
getIntervalMetadata,
|
|
50
50
|
getTimeoutMetadata,
|
|
51
51
|
hasQueueDecorators,
|
|
52
|
+
QueueScheduler,
|
|
53
|
+
QueueService,
|
|
52
54
|
} from './index';
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -456,13 +458,6 @@ describe('Custom adapter NATS JetStream (docs/api/queue.md)', () => {
|
|
|
456
458
|
isActive: true,
|
|
457
459
|
};
|
|
458
460
|
}
|
|
459
|
-
async addScheduledJob(): Promise<void> {}
|
|
460
|
-
async removeScheduledJob(): Promise<boolean> {
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
async getScheduledJobs(): Promise<import('./types').ScheduledJobInfo[]> {
|
|
464
|
-
return [];
|
|
465
|
-
}
|
|
466
461
|
supports(): boolean {
|
|
467
462
|
return false;
|
|
468
463
|
}
|
|
@@ -585,11 +580,227 @@ describe('Feature Support Matrix (docs/api/queue.md)', () => {
|
|
|
585
580
|
expect(adapter.supports('pattern-subscriptions')).toBe(true);
|
|
586
581
|
expect(adapter.supports('delayed-messages')).toBe(true);
|
|
587
582
|
expect(adapter.supports('priority')).toBe(true);
|
|
588
|
-
expect(adapter.supports('scheduled-jobs')).toBe(true);
|
|
589
|
-
|
|
590
583
|
// Not supported
|
|
591
584
|
expect(adapter.supports('consumer-groups')).toBe(false);
|
|
592
585
|
expect(adapter.supports('dead-letter-queue')).toBe(false);
|
|
593
586
|
expect(adapter.supports('retry')).toBe(false);
|
|
594
587
|
});
|
|
595
588
|
});
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* @source docs/api/queue.md#setup (scheduled-only tip)
|
|
592
|
+
*/
|
|
593
|
+
describe('Scheduled-only Controllers (docs/api/queue.md)', () => {
|
|
594
|
+
it('should auto-detect queue decorators on controller with only @Interval', () => {
|
|
595
|
+
// From docs/api/queue.md: Scheduled-only Controllers tip
|
|
596
|
+
class ScheduledOnlyController {
|
|
597
|
+
@Interval(60000, { pattern: 'metrics.collect' })
|
|
598
|
+
getMetrics() {
|
|
599
|
+
return { cpu: process.cpuUsage() };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// No @Subscribe — only scheduling decorators
|
|
604
|
+
expect(hasQueueDecorators(ScheduledOnlyController)).toBe(true);
|
|
605
|
+
expect(getSubscribeMetadata(ScheduledOnlyController).length).toBe(0);
|
|
606
|
+
expect(getIntervalMetadata(ScheduledOnlyController).length).toBe(1);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('should auto-detect queue decorators on controller with only @Cron', () => {
|
|
610
|
+
class CronOnlyController {
|
|
611
|
+
@Cron(CronExpression.EVERY_HOUR, { pattern: 'cleanup.expired' })
|
|
612
|
+
getCleanupData() {
|
|
613
|
+
return { timestamp: Date.now() };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
expect(hasQueueDecorators(CronOnlyController)).toBe(true);
|
|
618
|
+
expect(getSubscribeMetadata(CronOnlyController).length).toBe(0);
|
|
619
|
+
expect(getCronMetadata(CronOnlyController).length).toBe(1);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should auto-detect queue decorators on controller with only @Timeout', () => {
|
|
623
|
+
class TimeoutOnlyController {
|
|
624
|
+
@Timeout(5000, { pattern: 'startup.warmup' })
|
|
625
|
+
getWarmupData() {
|
|
626
|
+
return { type: 'warmup' };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
expect(hasQueueDecorators(TimeoutOnlyController)).toBe(true);
|
|
631
|
+
expect(getSubscribeMetadata(TimeoutOnlyController).length).toBe(0);
|
|
632
|
+
expect(getTimeoutMetadata(TimeoutOnlyController).length).toBe(1);
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* @source docs/api/queue.md#setup (error handling info)
|
|
638
|
+
*/
|
|
639
|
+
describe('Scheduled Job Error Handling (docs/api/queue.md)', () => {
|
|
640
|
+
let adapter: InMemoryQueueAdapter;
|
|
641
|
+
|
|
642
|
+
beforeEach(async () => {
|
|
643
|
+
adapter = new InMemoryQueueAdapter();
|
|
644
|
+
await adapter.connect();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
afterEach(async () => {
|
|
648
|
+
await adapter.disconnect();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should continue scheduler after handler error via setErrorHandler', async () => {
|
|
652
|
+
// From docs/api/queue.md: Scheduled Job Error Handling info
|
|
653
|
+
const scheduler = new QueueScheduler(adapter);
|
|
654
|
+
|
|
655
|
+
const errors: Array<{ name: string; error: unknown }> = [];
|
|
656
|
+
scheduler.setErrorHandler((name: string, error: unknown) => {
|
|
657
|
+
errors.push({ name, error });
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Add a job that will fail
|
|
661
|
+
scheduler.addIntervalJob('failing-job', 60000, 'test.fail', () => {
|
|
662
|
+
throw new Error('Job failed');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
scheduler.start();
|
|
666
|
+
|
|
667
|
+
// executeJob is async fire-and-forget, wait for it
|
|
668
|
+
await new Promise(r => setTimeout(r, 50));
|
|
669
|
+
|
|
670
|
+
// Error handler should have been called (immediate execution)
|
|
671
|
+
expect(errors.length).toBe(1);
|
|
672
|
+
expect(errors[0].name).toBe('failing-job');
|
|
673
|
+
expect((errors[0].error as Error).message).toBe('Job failed');
|
|
674
|
+
|
|
675
|
+
scheduler.stop();
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* @source docs/api/queue.md#dynamic-job-management
|
|
681
|
+
*/
|
|
682
|
+
describe('Dynamic Job Management (docs/api/queue.md)', () => {
|
|
683
|
+
let queueService: QueueService;
|
|
684
|
+
|
|
685
|
+
beforeEach(async () => {
|
|
686
|
+
queueService = new QueueService({ adapter: 'memory' });
|
|
687
|
+
const adapter = new InMemoryQueueAdapter();
|
|
688
|
+
await queueService.initialize(adapter);
|
|
689
|
+
await queueService.start();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
afterEach(async () => {
|
|
693
|
+
await queueService.stop();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('should add and get a cron job', () => {
|
|
697
|
+
// From docs/api/queue.md: Dynamic Job Management - addJob (cron)
|
|
698
|
+
queueService.addJob({
|
|
699
|
+
type: 'cron',
|
|
700
|
+
name: 'cleanup',
|
|
701
|
+
expression: '0 * * * *',
|
|
702
|
+
pattern: 'jobs.cleanup',
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const job = queueService.getJob('cleanup');
|
|
706
|
+
expect(job).toBeDefined();
|
|
707
|
+
expect(job!.type).toBe('cron');
|
|
708
|
+
expect(job!.schedule.cron).toBe('0 * * * *');
|
|
709
|
+
expect(job!.paused).toBe(false);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('should add and get an interval job', () => {
|
|
713
|
+
// From docs/api/queue.md: Dynamic Job Management - addJob (interval)
|
|
714
|
+
queueService.addJob({
|
|
715
|
+
type: 'interval',
|
|
716
|
+
name: 'heartbeat',
|
|
717
|
+
intervalMs: 5000,
|
|
718
|
+
pattern: 'jobs.heartbeat',
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const job = queueService.getJob('heartbeat');
|
|
722
|
+
expect(job).toBeDefined();
|
|
723
|
+
expect(job!.type).toBe('interval');
|
|
724
|
+
expect(job!.schedule.every).toBe(5000);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('should add and get a timeout job', () => {
|
|
728
|
+
// From docs/api/queue.md: Dynamic Job Management - addJob (timeout)
|
|
729
|
+
queueService.addJob({
|
|
730
|
+
type: 'timeout',
|
|
731
|
+
name: 'warmup',
|
|
732
|
+
timeoutMs: 3000,
|
|
733
|
+
pattern: 'jobs.warmup',
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const job = queueService.getJob('warmup');
|
|
737
|
+
expect(job).toBeDefined();
|
|
738
|
+
expect(job!.type).toBe('timeout');
|
|
739
|
+
expect(job!.schedule.timeout).toBe(3000);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should pause and resume a job', () => {
|
|
743
|
+
// From docs/api/queue.md: Dynamic Job Management - pauseJob / resumeJob
|
|
744
|
+
queueService.addJob({
|
|
745
|
+
type: 'interval',
|
|
746
|
+
name: 'metrics',
|
|
747
|
+
intervalMs: 10000,
|
|
748
|
+
pattern: 'jobs.metrics',
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
expect(queueService.pauseJob('metrics')).toBe(true);
|
|
752
|
+
expect(queueService.getJob('metrics')!.paused).toBe(true);
|
|
753
|
+
|
|
754
|
+
expect(queueService.resumeJob('metrics')).toBe(true);
|
|
755
|
+
expect(queueService.getJob('metrics')!.paused).toBe(false);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should update a cron job expression', () => {
|
|
759
|
+
// From docs/api/queue.md: Dynamic Job Management - updateJob
|
|
760
|
+
queueService.addJob({
|
|
761
|
+
type: 'cron',
|
|
762
|
+
name: 'report',
|
|
763
|
+
expression: '0 0 * * *',
|
|
764
|
+
pattern: 'jobs.report',
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
expect(queueService.getJob('report')!.schedule.cron).toBe('0 0 * * *');
|
|
768
|
+
|
|
769
|
+
queueService.updateJob({
|
|
770
|
+
type: 'cron',
|
|
771
|
+
name: 'report',
|
|
772
|
+
expression: '0 */2 * * *',
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
expect(queueService.getJob('report')!.schedule.cron).toBe('0 */2 * * *');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('should list and remove jobs', () => {
|
|
779
|
+
// From docs/api/queue.md: Dynamic Job Management - getJobs / removeJob
|
|
780
|
+
queueService.addJob({
|
|
781
|
+
type: 'cron',
|
|
782
|
+
name: 'job-a',
|
|
783
|
+
expression: '0 * * * *',
|
|
784
|
+
pattern: 'jobs.a',
|
|
785
|
+
});
|
|
786
|
+
queueService.addJob({
|
|
787
|
+
type: 'interval',
|
|
788
|
+
name: 'job-b',
|
|
789
|
+
intervalMs: 5000,
|
|
790
|
+
pattern: 'jobs.b',
|
|
791
|
+
});
|
|
792
|
+
queueService.addJob({
|
|
793
|
+
type: 'timeout',
|
|
794
|
+
name: 'job-c',
|
|
795
|
+
timeoutMs: 1000,
|
|
796
|
+
pattern: 'jobs.c',
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const jobs = queueService.getJobs();
|
|
800
|
+
expect(jobs.length).toBe(3);
|
|
801
|
+
|
|
802
|
+
expect(queueService.removeJob('job-b')).toBe(true);
|
|
803
|
+
expect(queueService.hasJob('job-b')).toBe(false);
|
|
804
|
+
expect(queueService.getJobs().length).toBe(2);
|
|
805
|
+
});
|
|
806
|
+
});
|
package/src/queue/index.ts
CHANGED
|
@@ -17,7 +17,14 @@ export type {
|
|
|
17
17
|
QueueEvents,
|
|
18
18
|
Subscription,
|
|
19
19
|
OverlapStrategy,
|
|
20
|
-
|
|
20
|
+
AddCronJob,
|
|
21
|
+
AddIntervalJob,
|
|
22
|
+
AddTimeoutJob,
|
|
23
|
+
AddJobOptions,
|
|
24
|
+
UpdateCronJob,
|
|
25
|
+
UpdateIntervalJob,
|
|
26
|
+
UpdateTimeoutJob,
|
|
27
|
+
UpdateJobOptions,
|
|
21
28
|
ScheduledJobInfo,
|
|
22
29
|
QueueFeature,
|
|
23
30
|
QueueAdapterType,
|
|
@@ -27,11 +27,20 @@ describe('QueueServiceProxy', () => {
|
|
|
27
27
|
/* no-op for throw test */
|
|
28
28
|
}),
|
|
29
29
|
).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
30
|
-
|
|
30
|
+
expect(() => proxy.addJob({
|
|
31
|
+
type: 'cron', name: 'j', expression: '* * * * *', pattern: 'e',
|
|
32
|
+
})).toThrow(
|
|
33
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
34
|
+
);
|
|
35
|
+
expect(() => proxy.removeJob('j')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
36
|
+
expect(() => proxy.getJob('j')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
37
|
+
expect(() => proxy.getJobs()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
38
|
+
expect(() => proxy.hasJob('j')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
39
|
+
expect(() => proxy.pauseJob('j')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
40
|
+
expect(() => proxy.resumeJob('j')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
41
|
+
expect(() => proxy.updateJob({ type: 'cron', name: 'j', expression: '* * * * *' })).toThrow(
|
|
31
42
|
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
32
43
|
);
|
|
33
|
-
await expect(proxy.removeScheduledJob('j')).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
34
|
-
await expect(proxy.getScheduledJobs()).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
35
44
|
expect(() => proxy.supports('pattern-subscriptions')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
36
45
|
expect(() =>
|
|
37
46
|
proxy.on('onReady', () => {
|
|
@@ -8,14 +8,15 @@ import type { QueueService } from './queue.service';
|
|
|
8
8
|
import type { QueueScheduler } from './scheduler';
|
|
9
9
|
import type { QueueAdapter } from './types';
|
|
10
10
|
import type {
|
|
11
|
+
AddJobOptions,
|
|
11
12
|
MessageHandler,
|
|
12
13
|
PublishOptions,
|
|
13
14
|
QueueEvents,
|
|
14
15
|
ScheduledJobInfo,
|
|
15
|
-
ScheduledJobOptions,
|
|
16
16
|
SubscribeOptions,
|
|
17
17
|
Subscription,
|
|
18
18
|
QueueFeature,
|
|
19
|
+
UpdateJobOptions,
|
|
19
20
|
} from './types';
|
|
20
21
|
|
|
21
22
|
const QUEUE_NOT_ENABLED_MESSAGE =
|
|
@@ -74,22 +75,51 @@ export class QueueServiceProxy {
|
|
|
74
75
|
return await this.delegate.subscribe(pattern, handler, options);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
addJob(options: AddJobOptions): void {
|
|
79
|
+
throwIfNoDelegate(this.delegate);
|
|
80
|
+
this.delegate.addJob(options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
removeJob(name: string): boolean {
|
|
84
|
+
throwIfNoDelegate(this.delegate);
|
|
85
|
+
|
|
86
|
+
return this.delegate.removeJob(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getJob(name: string): ScheduledJobInfo | undefined {
|
|
90
|
+
throwIfNoDelegate(this.delegate);
|
|
91
|
+
|
|
92
|
+
return this.delegate.getJob(name);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getJobs(): ScheduledJobInfo[] {
|
|
96
|
+
throwIfNoDelegate(this.delegate);
|
|
97
|
+
|
|
98
|
+
return this.delegate.getJobs();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
hasJob(name: string): boolean {
|
|
102
|
+
throwIfNoDelegate(this.delegate);
|
|
103
|
+
|
|
104
|
+
return this.delegate.hasJob(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pauseJob(name: string): boolean {
|
|
78
108
|
throwIfNoDelegate(this.delegate);
|
|
79
109
|
|
|
80
|
-
return
|
|
110
|
+
return this.delegate.pauseJob(name);
|
|
81
111
|
}
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
resumeJob(name: string): boolean {
|
|
84
114
|
throwIfNoDelegate(this.delegate);
|
|
85
115
|
|
|
86
|
-
return
|
|
116
|
+
return this.delegate.resumeJob(name);
|
|
87
117
|
}
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
updateJob(options: UpdateJobOptions): boolean {
|
|
90
120
|
throwIfNoDelegate(this.delegate);
|
|
91
121
|
|
|
92
|
-
return
|
|
122
|
+
return this.delegate.updateJob(options);
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
supports(feature: QueueFeature): boolean {
|
|
@@ -192,30 +192,152 @@ describe('QueueService', () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
describe('scheduled jobs
|
|
196
|
-
test('should
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
describe('scheduled jobs', () => {
|
|
196
|
+
test('addJob should delegate to scheduler addJob', () => {
|
|
197
|
+
service.addJob({
|
|
198
|
+
type: 'interval',
|
|
199
|
+
name: 'test-interval',
|
|
200
|
+
intervalMs: 5000,
|
|
201
|
+
pattern: 'test.pattern',
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
expect(service.hasJob('test-interval')).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('removeJob should delegate to scheduler removeJob', () => {
|
|
208
|
+
service.addJob({
|
|
209
|
+
type: 'interval',
|
|
210
|
+
name: 'remove-me',
|
|
211
|
+
intervalMs: 5000,
|
|
212
|
+
pattern: 'test.pattern',
|
|
213
|
+
});
|
|
206
214
|
|
|
207
|
-
const removed =
|
|
215
|
+
const removed = service.removeJob('remove-me');
|
|
208
216
|
expect(removed).toBe(true);
|
|
217
|
+
expect(service.hasJob('remove-me')).toBe(false);
|
|
218
|
+
});
|
|
209
219
|
|
|
210
|
-
|
|
211
|
-
expect(
|
|
220
|
+
test('removeJob should return false for non-existent job', () => {
|
|
221
|
+
expect(service.removeJob('nonexistent')).toBe(false);
|
|
212
222
|
});
|
|
213
223
|
|
|
214
|
-
test('should
|
|
215
|
-
|
|
224
|
+
test('getJob should delegate to scheduler getJob', () => {
|
|
225
|
+
service.addJob({
|
|
226
|
+
type: 'cron',
|
|
227
|
+
name: 'my-cron',
|
|
228
|
+
expression: '*/5 * * * *',
|
|
229
|
+
pattern: 'cron.pattern',
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const job = service.getJob('my-cron');
|
|
233
|
+
expect(job).toBeDefined();
|
|
234
|
+
expect(job!.name).toBe('my-cron');
|
|
235
|
+
expect(job!.type).toBe('cron');
|
|
236
|
+
expect(job!.pattern).toBe('cron.pattern');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('getJob should return undefined for non-existent job', () => {
|
|
240
|
+
expect(service.getJob('nonexistent')).toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('getJobs should delegate to scheduler getJobs', () => {
|
|
244
|
+
service.addJob({
|
|
245
|
+
type: 'interval',
|
|
246
|
+
name: 'job-a',
|
|
247
|
+
intervalMs: 1000,
|
|
248
|
+
pattern: 'a.pattern',
|
|
249
|
+
});
|
|
250
|
+
service.addJob({
|
|
251
|
+
type: 'timeout',
|
|
252
|
+
name: 'job-b',
|
|
253
|
+
timeoutMs: 2000,
|
|
254
|
+
pattern: 'b.pattern',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const jobs = service.getJobs();
|
|
258
|
+
expect(jobs.length).toBe(2);
|
|
259
|
+
expect(jobs.some((j) => j.name === 'job-a')).toBe(true);
|
|
260
|
+
expect(jobs.some((j) => j.name === 'job-b')).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('hasJob should delegate to scheduler hasJob', () => {
|
|
264
|
+
expect(service.hasJob('nope')).toBe(false);
|
|
265
|
+
|
|
266
|
+
service.addJob({
|
|
267
|
+
type: 'interval',
|
|
268
|
+
name: 'exists',
|
|
269
|
+
intervalMs: 1000,
|
|
270
|
+
pattern: 'test.pattern',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(service.hasJob('exists')).toBe(true);
|
|
274
|
+
});
|
|
216
275
|
|
|
217
|
-
|
|
218
|
-
|
|
276
|
+
test('pauseJob should delegate to scheduler pauseJob', () => {
|
|
277
|
+
service.addJob({
|
|
278
|
+
type: 'interval',
|
|
279
|
+
name: 'pausable',
|
|
280
|
+
intervalMs: 1000,
|
|
281
|
+
pattern: 'test.pattern',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const paused = service.pauseJob('pausable');
|
|
285
|
+
expect(paused).toBe(true);
|
|
286
|
+
|
|
287
|
+
const job = service.getJob('pausable');
|
|
288
|
+
expect(job!.paused).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('pauseJob should return false for non-existent job', () => {
|
|
292
|
+
expect(service.pauseJob('nonexistent')).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('resumeJob should delegate to scheduler resumeJob', () => {
|
|
296
|
+
service.addJob({
|
|
297
|
+
type: 'interval',
|
|
298
|
+
name: 'resumable',
|
|
299
|
+
intervalMs: 1000,
|
|
300
|
+
pattern: 'test.pattern',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
service.pauseJob('resumable');
|
|
304
|
+
const resumed = service.resumeJob('resumable');
|
|
305
|
+
expect(resumed).toBe(true);
|
|
306
|
+
|
|
307
|
+
const job = service.getJob('resumable');
|
|
308
|
+
expect(job!.paused).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('resumeJob should return false for non-existent job', () => {
|
|
312
|
+
expect(service.resumeJob('nonexistent')).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('updateJob should delegate to scheduler updateJob', () => {
|
|
316
|
+
service.addJob({
|
|
317
|
+
type: 'interval',
|
|
318
|
+
name: 'updatable',
|
|
319
|
+
intervalMs: 1000,
|
|
320
|
+
pattern: 'test.pattern',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const updated = service.updateJob({
|
|
324
|
+
type: 'interval',
|
|
325
|
+
name: 'updatable',
|
|
326
|
+
intervalMs: 5000,
|
|
327
|
+
});
|
|
328
|
+
expect(updated).toBe(true);
|
|
329
|
+
|
|
330
|
+
const job = service.getJob('updatable');
|
|
331
|
+
expect(job!.schedule.every).toBe(5000);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('updateJob should return false for non-existent job', () => {
|
|
335
|
+
const updated = service.updateJob({
|
|
336
|
+
type: 'interval',
|
|
337
|
+
name: 'nonexistent',
|
|
338
|
+
intervalMs: 1000,
|
|
339
|
+
});
|
|
340
|
+
expect(updated).toBe(false);
|
|
219
341
|
});
|
|
220
342
|
});
|
|
221
343
|
|
|
@@ -18,7 +18,8 @@ import type {
|
|
|
18
18
|
PublishOptions,
|
|
19
19
|
SubscribeOptions,
|
|
20
20
|
Subscription,
|
|
21
|
-
|
|
21
|
+
AddJobOptions,
|
|
22
|
+
UpdateJobOptions,
|
|
22
23
|
ScheduledJobInfo,
|
|
23
24
|
QueueFeature,
|
|
24
25
|
QueueEvents,
|
|
@@ -184,24 +185,59 @@ export class QueueService {
|
|
|
184
185
|
// ============================================================================
|
|
185
186
|
|
|
186
187
|
/**
|
|
187
|
-
* Add a scheduled job
|
|
188
|
+
* Add a scheduled job dynamically
|
|
188
189
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
addJob(options: AddJobOptions): void {
|
|
191
|
+
this.getScheduler().addJob(options);
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/**
|
|
194
|
-
* Remove a scheduled job
|
|
195
|
+
* Remove a scheduled job by name
|
|
195
196
|
*/
|
|
196
|
-
|
|
197
|
-
return
|
|
197
|
+
removeJob(name: string): boolean {
|
|
198
|
+
return this.getScheduler().removeJob(name);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get information about a specific scheduled job
|
|
203
|
+
*/
|
|
204
|
+
getJob(name: string): ScheduledJobInfo | undefined {
|
|
205
|
+
return this.getScheduler().getJob(name);
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
/**
|
|
201
209
|
* Get all scheduled jobs
|
|
202
210
|
*/
|
|
203
|
-
|
|
204
|
-
return
|
|
211
|
+
getJobs(): ScheduledJobInfo[] {
|
|
212
|
+
return this.getScheduler().getJobs();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a scheduled job exists
|
|
217
|
+
*/
|
|
218
|
+
hasJob(name: string): boolean {
|
|
219
|
+
return this.getScheduler().hasJob(name);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Pause a scheduled job
|
|
224
|
+
*/
|
|
225
|
+
pauseJob(name: string): boolean {
|
|
226
|
+
return this.getScheduler().pauseJob(name);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resume a paused scheduled job
|
|
231
|
+
*/
|
|
232
|
+
resumeJob(name: string): boolean {
|
|
233
|
+
return this.getScheduler().resumeJob(name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Update a scheduled job's timing configuration
|
|
238
|
+
*/
|
|
239
|
+
updateJob(options: UpdateJobOptions): boolean {
|
|
240
|
+
return this.getScheduler().updateJob(options);
|
|
205
241
|
}
|
|
206
242
|
|
|
207
243
|
// ============================================================================
|
|
@@ -288,6 +324,7 @@ export class QueueService {
|
|
|
288
324
|
{
|
|
289
325
|
metadata: cron.options.metadata,
|
|
290
326
|
overlapStrategy: cron.options.overlapStrategy,
|
|
327
|
+
declarative: true,
|
|
291
328
|
},
|
|
292
329
|
);
|
|
293
330
|
}
|
|
@@ -301,7 +338,7 @@ export class QueueService {
|
|
|
301
338
|
interval.milliseconds,
|
|
302
339
|
interval.options.pattern,
|
|
303
340
|
method,
|
|
304
|
-
{ metadata: interval.options.metadata },
|
|
341
|
+
{ metadata: interval.options.metadata, declarative: true },
|
|
305
342
|
);
|
|
306
343
|
}
|
|
307
344
|
|
|
@@ -314,7 +351,7 @@ export class QueueService {
|
|
|
314
351
|
timeout.milliseconds,
|
|
315
352
|
timeout.options.pattern,
|
|
316
353
|
method,
|
|
317
|
-
{ metadata: timeout.options.metadata },
|
|
354
|
+
{ metadata: timeout.options.metadata, declarative: true },
|
|
318
355
|
);
|
|
319
356
|
}
|
|
320
357
|
|