@onebun/core 0.2.12 → 0.2.14
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 +2 -2
- package/src/application/application.test.ts +0 -7
- package/src/application/application.ts +3 -2
- package/src/decorators/metadata.test.ts +86 -0
- package/src/decorators/metadata.ts +199 -0
- package/src/docs-examples.test.ts +95 -0
- package/src/index.ts +1 -0
- 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 +130 -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 +143 -1
- package/src/queue/types.ts +80 -31
- package/src/types.ts +10 -6
|
@@ -158,76 +158,12 @@ describe('RedisQueueAdapter', () => {
|
|
|
158
158
|
// current RedisClient.raw() implementation. These features need a proper
|
|
159
159
|
// implementation using Bun's Redis client's sendCommand API.
|
|
160
160
|
|
|
161
|
-
describe('scheduled jobs', () => {
|
|
162
|
-
beforeEach(async () => {
|
|
163
|
-
await adapter.connect();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('should add and get scheduled jobs', async () => {
|
|
167
|
-
await adapter.addScheduledJob('test-job', {
|
|
168
|
-
pattern: 'job:test',
|
|
169
|
-
data: { action: 'process' },
|
|
170
|
-
schedule: { every: 1000 },
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const jobs = await adapter.getScheduledJobs();
|
|
174
|
-
|
|
175
|
-
expect(jobs.find((j) => j.name === 'test-job')).toBeDefined();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should add cron scheduled job', async () => {
|
|
179
|
-
await adapter.addScheduledJob('cron-job', {
|
|
180
|
-
pattern: 'job:cron',
|
|
181
|
-
data: { action: 'cron' },
|
|
182
|
-
schedule: { cron: '0 * * * *' },
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const jobs = await adapter.getScheduledJobs();
|
|
186
|
-
|
|
187
|
-
expect(jobs.find((j) => j.name === 'cron-job')).toBeDefined();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should remove scheduled job', async () => {
|
|
191
|
-
await adapter.addScheduledJob('removable-job', {
|
|
192
|
-
pattern: 'job:remove',
|
|
193
|
-
data: {},
|
|
194
|
-
schedule: { every: 1000 },
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const removed = await adapter.removeScheduledJob('removable-job');
|
|
198
|
-
|
|
199
|
-
expect(removed).toBe(true);
|
|
200
|
-
|
|
201
|
-
const jobs = await adapter.getScheduledJobs();
|
|
202
|
-
expect(jobs.find((j) => j.name === 'removable-job')).toBeUndefined();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should return false when removing non-existent job', async () => {
|
|
206
|
-
const removed = await adapter.removeScheduledJob('non-existent');
|
|
207
|
-
|
|
208
|
-
expect(removed).toBe(false);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should throw when adding job while disconnected', async () => {
|
|
212
|
-
await adapter.disconnect();
|
|
213
|
-
|
|
214
|
-
await expect(
|
|
215
|
-
adapter.addScheduledJob('fail-job', {
|
|
216
|
-
pattern: 'job:fail',
|
|
217
|
-
data: {},
|
|
218
|
-
schedule: { every: 1000 },
|
|
219
|
-
}),
|
|
220
|
-
).rejects.toThrow('not connected');
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
161
|
describe('features', () => {
|
|
225
162
|
it('should support all standard queue features', () => {
|
|
226
163
|
expect(adapter.supports('delayed-messages')).toBe(true);
|
|
227
164
|
expect(adapter.supports('priority')).toBe(true);
|
|
228
165
|
expect(adapter.supports('dead-letter-queue')).toBe(true);
|
|
229
166
|
expect(adapter.supports('retry')).toBe(true);
|
|
230
|
-
expect(adapter.supports('scheduled-jobs')).toBe(true);
|
|
231
167
|
expect(adapter.supports('consumer-groups')).toBe(true);
|
|
232
168
|
expect(adapter.supports('pattern-subscriptions')).toBe(true);
|
|
233
169
|
});
|
|
@@ -21,8 +21,6 @@ import type {
|
|
|
21
21
|
PublishOptions,
|
|
22
22
|
SubscribeOptions,
|
|
23
23
|
Subscription,
|
|
24
|
-
ScheduledJobOptions,
|
|
25
|
-
ScheduledJobInfo,
|
|
26
24
|
MessageHandler,
|
|
27
25
|
} from '../types';
|
|
28
26
|
|
|
@@ -409,45 +407,6 @@ export class RedisQueueAdapter implements QueueAdapter {
|
|
|
409
407
|
return subscription;
|
|
410
408
|
}
|
|
411
409
|
|
|
412
|
-
// ============================================================================
|
|
413
|
-
// Scheduled Jobs
|
|
414
|
-
// ============================================================================
|
|
415
|
-
|
|
416
|
-
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
417
|
-
this.ensureConnected();
|
|
418
|
-
|
|
419
|
-
if (!this.scheduler) {
|
|
420
|
-
throw new Error('Scheduler not initialized');
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (options.schedule.cron) {
|
|
424
|
-
this.scheduler.addCronJob(name, options.schedule.cron, options.pattern, () => options.data, {
|
|
425
|
-
metadata: options.metadata,
|
|
426
|
-
overlapStrategy: options.overlapStrategy,
|
|
427
|
-
});
|
|
428
|
-
} else if (options.schedule.every) {
|
|
429
|
-
this.scheduler.addIntervalJob(name, options.schedule.every, options.pattern, () => options.data, {
|
|
430
|
-
metadata: options.metadata,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
async removeScheduledJob(name: string): Promise<boolean> {
|
|
436
|
-
if (!this.scheduler) {
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return this.scheduler.removeJob(name);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
444
|
-
if (!this.scheduler) {
|
|
445
|
-
return [];
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return this.scheduler.getJobs();
|
|
449
|
-
}
|
|
450
|
-
|
|
451
410
|
// ============================================================================
|
|
452
411
|
// Features
|
|
453
412
|
// ============================================================================
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getTimeoutMetadata,
|
|
51
51
|
hasQueueDecorators,
|
|
52
52
|
QueueScheduler,
|
|
53
|
+
QueueService,
|
|
53
54
|
} from './index';
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -457,13 +458,6 @@ describe('Custom adapter NATS JetStream (docs/api/queue.md)', () => {
|
|
|
457
458
|
isActive: true,
|
|
458
459
|
};
|
|
459
460
|
}
|
|
460
|
-
async addScheduledJob(): Promise<void> {}
|
|
461
|
-
async removeScheduledJob(): Promise<boolean> {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
async getScheduledJobs(): Promise<import('./types').ScheduledJobInfo[]> {
|
|
465
|
-
return [];
|
|
466
|
-
}
|
|
467
461
|
supports(): boolean {
|
|
468
462
|
return false;
|
|
469
463
|
}
|
|
@@ -586,8 +580,6 @@ describe('Feature Support Matrix (docs/api/queue.md)', () => {
|
|
|
586
580
|
expect(adapter.supports('pattern-subscriptions')).toBe(true);
|
|
587
581
|
expect(adapter.supports('delayed-messages')).toBe(true);
|
|
588
582
|
expect(adapter.supports('priority')).toBe(true);
|
|
589
|
-
expect(adapter.supports('scheduled-jobs')).toBe(true);
|
|
590
|
-
|
|
591
583
|
// Not supported
|
|
592
584
|
expect(adapter.supports('consumer-groups')).toBe(false);
|
|
593
585
|
expect(adapter.supports('dead-letter-queue')).toBe(false);
|
|
@@ -683,3 +675,132 @@ describe('Scheduled Job Error Handling (docs/api/queue.md)', () => {
|
|
|
683
675
|
scheduler.stop();
|
|
684
676
|
});
|
|
685
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
|
|