@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
|
@@ -239,6 +239,286 @@ describe('QueueScheduler', () => {
|
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
describe('addJob', () => {
|
|
243
|
+
it('should add a cron job visible via getJob', () => {
|
|
244
|
+
scheduler.addJob({
|
|
245
|
+
type: 'cron',
|
|
246
|
+
name: 'my-cron',
|
|
247
|
+
expression: '0 0 * * * *',
|
|
248
|
+
pattern: 'test.cron',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const job = scheduler.getJob('my-cron');
|
|
252
|
+
expect(job).toBeDefined();
|
|
253
|
+
expect(job!.type).toBe('cron');
|
|
254
|
+
expect(job!.paused).toBe(false);
|
|
255
|
+
expect(job!.declarative).toBe(false);
|
|
256
|
+
expect(job!.schedule.cron).toBe('0 0 * * * *');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should add an interval job visible via getJob', () => {
|
|
260
|
+
scheduler.addJob({
|
|
261
|
+
type: 'interval',
|
|
262
|
+
name: 'my-interval',
|
|
263
|
+
intervalMs: 5000,
|
|
264
|
+
pattern: 'test.interval',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const job = scheduler.getJob('my-interval');
|
|
268
|
+
expect(job).toBeDefined();
|
|
269
|
+
expect(job!.type).toBe('interval');
|
|
270
|
+
expect(job!.paused).toBe(false);
|
|
271
|
+
expect(job!.declarative).toBe(false);
|
|
272
|
+
expect(job!.schedule.every).toBe(5000);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should add a timeout job visible via getJob', () => {
|
|
276
|
+
scheduler.addJob({
|
|
277
|
+
type: 'timeout',
|
|
278
|
+
name: 'my-timeout',
|
|
279
|
+
timeoutMs: 3000,
|
|
280
|
+
pattern: 'test.timeout',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const job = scheduler.getJob('my-timeout');
|
|
284
|
+
expect(job).toBeDefined();
|
|
285
|
+
expect(job!.type).toBe('timeout');
|
|
286
|
+
expect(job!.paused).toBe(false);
|
|
287
|
+
expect(job!.declarative).toBe(false);
|
|
288
|
+
expect(job!.schedule.timeout).toBe(3000);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('getJobs returns type, paused, and timeout', () => {
|
|
293
|
+
it('should return correct type, paused, and schedule for all job types', () => {
|
|
294
|
+
scheduler.addJob({
|
|
295
|
+
type: 'cron', name: 'c1', expression: '* * * * *', pattern: 'p',
|
|
296
|
+
});
|
|
297
|
+
scheduler.addJob({
|
|
298
|
+
type: 'interval', name: 'i1', intervalMs: 1000, pattern: 'p',
|
|
299
|
+
});
|
|
300
|
+
scheduler.addJob({
|
|
301
|
+
type: 'timeout', name: 't1', timeoutMs: 2000, pattern: 'p',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const jobs = scheduler.getJobs();
|
|
305
|
+
expect(jobs.length).toBe(3);
|
|
306
|
+
|
|
307
|
+
const cronJob = jobs.find((j) => j.name === 'c1');
|
|
308
|
+
expect(cronJob!.type).toBe('cron');
|
|
309
|
+
expect(cronJob!.paused).toBe(false);
|
|
310
|
+
|
|
311
|
+
const intervalJob = jobs.find((j) => j.name === 'i1');
|
|
312
|
+
expect(intervalJob!.type).toBe('interval');
|
|
313
|
+
expect(intervalJob!.paused).toBe(false);
|
|
314
|
+
expect(intervalJob!.schedule.every).toBe(1000);
|
|
315
|
+
|
|
316
|
+
const timeoutJob = jobs.find((j) => j.name === 't1');
|
|
317
|
+
expect(timeoutJob!.type).toBe('timeout');
|
|
318
|
+
expect(timeoutJob!.paused).toBe(false);
|
|
319
|
+
expect(timeoutJob!.schedule.timeout).toBe(2000);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('declarative field', () => {
|
|
324
|
+
it('should mark jobs added via addCronJob with declarative option as declarative', () => {
|
|
325
|
+
scheduler.addCronJob('dec-cron', '* * * * *', 'p', undefined, { declarative: true });
|
|
326
|
+
|
|
327
|
+
const job = scheduler.getJob('dec-cron');
|
|
328
|
+
expect(job).toBeDefined();
|
|
329
|
+
expect(job!.declarative).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should mark jobs added via addIntervalJob with declarative option as declarative', () => {
|
|
333
|
+
scheduler.addIntervalJob('dec-interval', 1000, 'p', undefined, { declarative: true });
|
|
334
|
+
|
|
335
|
+
const job = scheduler.getJob('dec-interval');
|
|
336
|
+
expect(job).toBeDefined();
|
|
337
|
+
expect(job!.declarative).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should mark jobs added via addTimeoutJob with declarative option as declarative', () => {
|
|
341
|
+
scheduler.addTimeoutJob('dec-timeout', 1000, 'p', undefined, { declarative: true });
|
|
342
|
+
|
|
343
|
+
const job = scheduler.getJob('dec-timeout');
|
|
344
|
+
expect(job).toBeDefined();
|
|
345
|
+
expect(job!.declarative).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should mark jobs added via addJob as not declarative', () => {
|
|
349
|
+
scheduler.addJob({
|
|
350
|
+
type: 'cron', name: 'dyn-cron', expression: '* * * * *', pattern: 'p',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const job = scheduler.getJob('dyn-cron');
|
|
354
|
+
expect(job).toBeDefined();
|
|
355
|
+
expect(job!.declarative).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('pauseJob', () => {
|
|
360
|
+
it('should pause an existing job and return true', () => {
|
|
361
|
+
scheduler.addJob({
|
|
362
|
+
type: 'interval', name: 'j1', intervalMs: 1000, pattern: 'p',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const result = scheduler.pauseJob('j1');
|
|
366
|
+
expect(result).toBe(true);
|
|
367
|
+
|
|
368
|
+
const job = scheduler.getJob('j1');
|
|
369
|
+
expect(job!.paused).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should return false for nonexistent job', () => {
|
|
373
|
+
const result = scheduler.pauseJob('nonexistent');
|
|
374
|
+
expect(result).toBe(false);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('resumeJob', () => {
|
|
379
|
+
it('should resume a paused job and return true', () => {
|
|
380
|
+
scheduler.addJob({
|
|
381
|
+
type: 'interval', name: 'j1', intervalMs: 1000, pattern: 'p',
|
|
382
|
+
});
|
|
383
|
+
scheduler.pauseJob('j1');
|
|
384
|
+
|
|
385
|
+
const result = scheduler.resumeJob('j1');
|
|
386
|
+
expect(result).toBe(true);
|
|
387
|
+
|
|
388
|
+
const job = scheduler.getJob('j1');
|
|
389
|
+
expect(job!.paused).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should return false for nonexistent job', () => {
|
|
393
|
+
const result = scheduler.resumeJob('nonexistent');
|
|
394
|
+
expect(result).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should return false for non-paused job', () => {
|
|
398
|
+
scheduler.addJob({
|
|
399
|
+
type: 'interval', name: 'j1', intervalMs: 1000, pattern: 'p',
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const result = scheduler.resumeJob('j1');
|
|
403
|
+
expect(result).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('updateJob', () => {
|
|
408
|
+
it('should update cron expression', () => {
|
|
409
|
+
scheduler.addJob({
|
|
410
|
+
type: 'cron',
|
|
411
|
+
name: 'c1',
|
|
412
|
+
expression: '0 0 * * * *',
|
|
413
|
+
pattern: 'p',
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const result = scheduler.updateJob({ type: 'cron', name: 'c1', expression: '*/5 * * * *' });
|
|
417
|
+
expect(result).toBe(true);
|
|
418
|
+
|
|
419
|
+
const job = scheduler.getJob('c1');
|
|
420
|
+
expect(job!.schedule.cron).toBe('*/5 * * * *');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should update interval', () => {
|
|
424
|
+
scheduler.addJob({
|
|
425
|
+
type: 'interval', name: 'i1', intervalMs: 1000, pattern: 'p',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const result = scheduler.updateJob({ type: 'interval', name: 'i1', intervalMs: 5000 });
|
|
429
|
+
expect(result).toBe(true);
|
|
430
|
+
|
|
431
|
+
const job = scheduler.getJob('i1');
|
|
432
|
+
expect(job!.schedule.every).toBe(5000);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should update timeout', () => {
|
|
436
|
+
scheduler.addJob({
|
|
437
|
+
type: 'timeout', name: 't1', timeoutMs: 1000, pattern: 'p',
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const result = scheduler.updateJob({ type: 'timeout', name: 't1', timeoutMs: 9000 });
|
|
441
|
+
expect(result).toBe(true);
|
|
442
|
+
|
|
443
|
+
const job = scheduler.getJob('t1');
|
|
444
|
+
expect(job!.schedule.timeout).toBe(9000);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should return false when type does not match', () => {
|
|
448
|
+
scheduler.addJob({
|
|
449
|
+
type: 'interval', name: 'i1', intervalMs: 1000, pattern: 'p',
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const result = scheduler.updateJob({ type: 'cron', name: 'i1', expression: '* * * * *' });
|
|
453
|
+
expect(result).toBe(false);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should return false for nonexistent job', () => {
|
|
457
|
+
const result = scheduler.updateJob({ type: 'cron', name: 'nope', expression: '* * * * *' });
|
|
458
|
+
expect(result).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('paused jobs do not fire', () => {
|
|
463
|
+
it('should not fire paused cron job', async () => {
|
|
464
|
+
const received: Message[] = [];
|
|
465
|
+
await adapter.subscribe('test.cron', async (message) => {
|
|
466
|
+
received.push(message);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Every second cron
|
|
470
|
+
scheduler.addJob({
|
|
471
|
+
type: 'cron',
|
|
472
|
+
name: 'cron-paused',
|
|
473
|
+
expression: '* * * * * *',
|
|
474
|
+
pattern: 'test.cron',
|
|
475
|
+
});
|
|
476
|
+
scheduler.pauseJob('cron-paused');
|
|
477
|
+
scheduler.start();
|
|
478
|
+
|
|
479
|
+
// Advance time well past when cron would fire
|
|
480
|
+
advanceTime(5000);
|
|
481
|
+
await Promise.resolve();
|
|
482
|
+
|
|
483
|
+
expect(received.length).toBe(0);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should not fire paused interval job, and resume restarts it', async () => {
|
|
487
|
+
const received: Message[] = [];
|
|
488
|
+
await adapter.subscribe('test.interval', async (message) => {
|
|
489
|
+
received.push(message);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
scheduler.addJob({
|
|
493
|
+
type: 'interval',
|
|
494
|
+
name: 'int-paused',
|
|
495
|
+
intervalMs: 100,
|
|
496
|
+
pattern: 'test.interval',
|
|
497
|
+
});
|
|
498
|
+
scheduler.start();
|
|
499
|
+
|
|
500
|
+
// Should fire immediately on start
|
|
501
|
+
advanceTime(10);
|
|
502
|
+
await Promise.resolve();
|
|
503
|
+
expect(received.length).toBe(1);
|
|
504
|
+
|
|
505
|
+
// Pause the job
|
|
506
|
+
scheduler.pauseJob('int-paused');
|
|
507
|
+
|
|
508
|
+
// Advance time — no new messages
|
|
509
|
+
advanceTime(500);
|
|
510
|
+
await Promise.resolve();
|
|
511
|
+
expect(received.length).toBe(1);
|
|
512
|
+
|
|
513
|
+
// Resume — should restart interval and fire immediately
|
|
514
|
+
scheduler.resumeJob('int-paused');
|
|
515
|
+
|
|
516
|
+
advanceTime(10);
|
|
517
|
+
await Promise.resolve();
|
|
518
|
+
expect(received.length).toBe(2);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
242
522
|
describe('createQueueScheduler', () => {
|
|
243
523
|
it('should create scheduler instance', () => {
|
|
244
524
|
const created = createQueueScheduler(adapter);
|
package/src/queue/scheduler.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
+
AddJobOptions,
|
|
10
|
+
UpdateJobOptions,
|
|
9
11
|
QueueAdapter,
|
|
10
12
|
ScheduledJobInfo,
|
|
11
13
|
OverlapStrategy,
|
|
@@ -42,6 +44,12 @@ interface ScheduledJob {
|
|
|
42
44
|
// Timeout-specific
|
|
43
45
|
timeoutMs?: number;
|
|
44
46
|
|
|
47
|
+
// Pause state
|
|
48
|
+
paused?: boolean;
|
|
49
|
+
|
|
50
|
+
// Whether created via decorator
|
|
51
|
+
declarative?: boolean;
|
|
52
|
+
|
|
45
53
|
// Runtime state
|
|
46
54
|
timer?: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>;
|
|
47
55
|
isRunning?: boolean;
|
|
@@ -64,9 +72,17 @@ export class QueueScheduler {
|
|
|
64
72
|
private running = false;
|
|
65
73
|
private cronCheckInterval?: ReturnType<typeof setInterval>;
|
|
66
74
|
private readonly cronCheckIntervalMs = 1000; // Check cron jobs every second
|
|
75
|
+
private onJobError?: (jobName: string, error: unknown) => void;
|
|
67
76
|
|
|
68
77
|
constructor(private readonly adapter: QueueAdapter) {}
|
|
69
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Set error handler for scheduled job failures
|
|
81
|
+
*/
|
|
82
|
+
setErrorHandler(handler: (jobName: string, error: unknown) => void): void {
|
|
83
|
+
this.onJobError = handler;
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
/**
|
|
71
87
|
* Start the scheduler
|
|
72
88
|
*/
|
|
@@ -82,8 +98,12 @@ export class QueueScheduler {
|
|
|
82
98
|
this.checkCronJobs();
|
|
83
99
|
}, this.cronCheckIntervalMs);
|
|
84
100
|
|
|
85
|
-
// Start all interval and timeout jobs
|
|
101
|
+
// Start all interval and timeout jobs (skip paused)
|
|
86
102
|
for (const job of this.jobs.values()) {
|
|
103
|
+
if (job.paused) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
87
107
|
if (job.type === 'interval' && job.intervalMs) {
|
|
88
108
|
this.startIntervalJob(job);
|
|
89
109
|
} else if (job.type === 'timeout' && job.timeoutMs) {
|
|
@@ -129,6 +149,7 @@ export class QueueScheduler {
|
|
|
129
149
|
options?: {
|
|
130
150
|
metadata?: Partial<MessageMetadata>;
|
|
131
151
|
overlapStrategy?: OverlapStrategy;
|
|
152
|
+
declarative?: boolean;
|
|
132
153
|
},
|
|
133
154
|
): void {
|
|
134
155
|
const schedule = parseCronExpression(expression);
|
|
@@ -144,6 +165,7 @@ export class QueueScheduler {
|
|
|
144
165
|
getDataFn,
|
|
145
166
|
metadata: options?.metadata,
|
|
146
167
|
overlapStrategy: options?.overlapStrategy ?? 'skip',
|
|
168
|
+
declarative: options?.declarative,
|
|
147
169
|
};
|
|
148
170
|
|
|
149
171
|
this.jobs.set(name, job);
|
|
@@ -159,6 +181,7 @@ export class QueueScheduler {
|
|
|
159
181
|
getDataFn?: () => unknown | Promise<unknown>,
|
|
160
182
|
options?: {
|
|
161
183
|
metadata?: Partial<MessageMetadata>;
|
|
184
|
+
declarative?: boolean;
|
|
162
185
|
},
|
|
163
186
|
): void {
|
|
164
187
|
const job: ScheduledJob = {
|
|
@@ -168,6 +191,7 @@ export class QueueScheduler {
|
|
|
168
191
|
intervalMs,
|
|
169
192
|
getDataFn,
|
|
170
193
|
metadata: options?.metadata,
|
|
194
|
+
declarative: options?.declarative,
|
|
171
195
|
};
|
|
172
196
|
|
|
173
197
|
this.jobs.set(name, job);
|
|
@@ -188,6 +212,7 @@ export class QueueScheduler {
|
|
|
188
212
|
getDataFn?: () => unknown | Promise<unknown>,
|
|
189
213
|
options?: {
|
|
190
214
|
metadata?: Partial<MessageMetadata>;
|
|
215
|
+
declarative?: boolean;
|
|
191
216
|
},
|
|
192
217
|
): void {
|
|
193
218
|
const job: ScheduledJob = {
|
|
@@ -197,6 +222,7 @@ export class QueueScheduler {
|
|
|
197
222
|
timeoutMs,
|
|
198
223
|
getDataFn,
|
|
199
224
|
metadata: options?.metadata,
|
|
225
|
+
declarative: options?.declarative,
|
|
200
226
|
};
|
|
201
227
|
|
|
202
228
|
this.jobs.set(name, job);
|
|
@@ -207,6 +233,118 @@ export class QueueScheduler {
|
|
|
207
233
|
}
|
|
208
234
|
}
|
|
209
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Add a job using the unified API
|
|
238
|
+
*/
|
|
239
|
+
addJob(options: AddJobOptions): void {
|
|
240
|
+
switch (options.type) {
|
|
241
|
+
case 'cron':
|
|
242
|
+
this.addCronJob(options.name, options.expression, options.pattern, options.getDataFn, {
|
|
243
|
+
metadata: options.metadata,
|
|
244
|
+
overlapStrategy: options.overlapStrategy,
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
case 'interval':
|
|
248
|
+
this.addIntervalJob(options.name, options.intervalMs, options.pattern, options.getDataFn, {
|
|
249
|
+
metadata: options.metadata,
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
case 'timeout':
|
|
253
|
+
this.addTimeoutJob(options.name, options.timeoutMs, options.pattern, options.getDataFn, {
|
|
254
|
+
metadata: options.metadata,
|
|
255
|
+
});
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Pause a scheduled job
|
|
262
|
+
*/
|
|
263
|
+
pauseJob(name: string): boolean {
|
|
264
|
+
const job = this.jobs.get(name);
|
|
265
|
+
if (!job) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
job.paused = true;
|
|
270
|
+
|
|
271
|
+
if (job.timer) {
|
|
272
|
+
clearTimeout(job.timer);
|
|
273
|
+
clearInterval(job.timer);
|
|
274
|
+
job.timer = undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Resume a paused job
|
|
282
|
+
*/
|
|
283
|
+
resumeJob(name: string): boolean {
|
|
284
|
+
const job = this.jobs.get(name);
|
|
285
|
+
if (!job || !job.paused) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
job.paused = false;
|
|
290
|
+
|
|
291
|
+
if (this.running) {
|
|
292
|
+
if (job.type === 'interval' && job.intervalMs) {
|
|
293
|
+
this.startIntervalJob(job);
|
|
294
|
+
} else if (job.type === 'timeout' && job.timeoutMs) {
|
|
295
|
+
this.startTimeoutJob(job);
|
|
296
|
+
} else if (job.type === 'cron' && job.cronSchedule) {
|
|
297
|
+
job.nextRun = getNextRun(job.cronSchedule) ?? undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Update a scheduled job's timing configuration
|
|
306
|
+
*/
|
|
307
|
+
updateJob(options: UpdateJobOptions): boolean {
|
|
308
|
+
const job = this.jobs.get(options.name);
|
|
309
|
+
if (!job || job.type !== options.type) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
switch (options.type) {
|
|
314
|
+
case 'cron': {
|
|
315
|
+
const schedule = parseCronExpression(options.expression);
|
|
316
|
+
job.cronExpression = options.expression;
|
|
317
|
+
job.cronSchedule = schedule;
|
|
318
|
+
job.nextRun = getNextRun(schedule) ?? undefined;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'interval': {
|
|
322
|
+
if (job.timer) {
|
|
323
|
+
clearInterval(job.timer);
|
|
324
|
+
job.timer = undefined;
|
|
325
|
+
}
|
|
326
|
+
job.intervalMs = options.intervalMs;
|
|
327
|
+
if (this.running && !job.paused) {
|
|
328
|
+
this.startIntervalJob(job);
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'timeout': {
|
|
333
|
+
if (job.timer) {
|
|
334
|
+
clearTimeout(job.timer);
|
|
335
|
+
job.timer = undefined;
|
|
336
|
+
}
|
|
337
|
+
job.timeoutMs = options.timeoutMs;
|
|
338
|
+
if (this.running && !job.paused) {
|
|
339
|
+
this.startTimeoutJob(job);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
210
348
|
/**
|
|
211
349
|
* Remove a job
|
|
212
350
|
*/
|
|
@@ -236,10 +374,14 @@ export class QueueScheduler {
|
|
|
236
374
|
for (const job of this.jobs.values()) {
|
|
237
375
|
result.push({
|
|
238
376
|
name: job.name,
|
|
377
|
+
type: job.type,
|
|
378
|
+
paused: job.paused ?? false,
|
|
379
|
+
declarative: job.declarative ?? false,
|
|
239
380
|
pattern: job.pattern,
|
|
240
381
|
schedule: {
|
|
241
382
|
cron: job.cronExpression,
|
|
242
383
|
every: job.intervalMs,
|
|
384
|
+
timeout: job.timeoutMs,
|
|
243
385
|
},
|
|
244
386
|
nextRun: job.nextRun,
|
|
245
387
|
lastRun: job.lastRun,
|
|
@@ -261,10 +403,14 @@ export class QueueScheduler {
|
|
|
261
403
|
|
|
262
404
|
return {
|
|
263
405
|
name: job.name,
|
|
406
|
+
type: job.type,
|
|
407
|
+
paused: job.paused ?? false,
|
|
408
|
+
declarative: job.declarative ?? false,
|
|
264
409
|
pattern: job.pattern,
|
|
265
410
|
schedule: {
|
|
266
411
|
cron: job.cronExpression,
|
|
267
412
|
every: job.intervalMs,
|
|
413
|
+
timeout: job.timeoutMs,
|
|
268
414
|
},
|
|
269
415
|
nextRun: job.nextRun,
|
|
270
416
|
lastRun: job.lastRun,
|
|
@@ -294,6 +440,10 @@ export class QueueScheduler {
|
|
|
294
440
|
continue;
|
|
295
441
|
}
|
|
296
442
|
|
|
443
|
+
if (job.paused) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
297
447
|
// Check if it's time to run
|
|
298
448
|
if (job.nextRun && now >= job.nextRun) {
|
|
299
449
|
// Handle overlap strategy
|
|
@@ -363,8 +513,11 @@ export class QueueScheduler {
|
|
|
363
513
|
await this.adapter.publish(job.pattern, data, {
|
|
364
514
|
metadata: job.metadata,
|
|
365
515
|
});
|
|
366
|
-
} catch {
|
|
367
|
-
//
|
|
516
|
+
} catch (error) {
|
|
517
|
+
// Report error via handler if set, otherwise silently continue
|
|
518
|
+
if (this.onJobError) {
|
|
519
|
+
this.onJobError(job.name, error);
|
|
520
|
+
}
|
|
368
521
|
} finally {
|
|
369
522
|
job.isRunning = false;
|
|
370
523
|
}
|
package/src/queue/types.ts
CHANGED
|
@@ -224,30 +224,79 @@ export interface Subscription {
|
|
|
224
224
|
export type OverlapStrategy = 'skip' | 'queue';
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
*
|
|
227
|
+
* Add a cron job
|
|
228
228
|
*/
|
|
229
|
-
export interface
|
|
230
|
-
|
|
229
|
+
export interface AddCronJob {
|
|
230
|
+
type: 'cron';
|
|
231
|
+
name: string;
|
|
232
|
+
expression: string;
|
|
231
233
|
pattern: string;
|
|
234
|
+
getDataFn?: () => unknown | Promise<unknown>;
|
|
235
|
+
metadata?: Partial<MessageMetadata>;
|
|
236
|
+
overlapStrategy?: OverlapStrategy;
|
|
237
|
+
}
|
|
232
238
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Add an interval job
|
|
241
|
+
*/
|
|
242
|
+
export interface AddIntervalJob {
|
|
243
|
+
type: 'interval';
|
|
244
|
+
name: string;
|
|
245
|
+
intervalMs: number;
|
|
246
|
+
pattern: string;
|
|
247
|
+
getDataFn?: () => unknown | Promise<unknown>;
|
|
248
|
+
metadata?: Partial<MessageMetadata>;
|
|
249
|
+
}
|
|
243
250
|
|
|
244
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Add a timeout job
|
|
253
|
+
*/
|
|
254
|
+
export interface AddTimeoutJob {
|
|
255
|
+
type: 'timeout';
|
|
256
|
+
name: string;
|
|
257
|
+
timeoutMs: number;
|
|
258
|
+
pattern: string;
|
|
259
|
+
getDataFn?: () => unknown | Promise<unknown>;
|
|
245
260
|
metadata?: Partial<MessageMetadata>;
|
|
261
|
+
}
|
|
246
262
|
|
|
247
|
-
|
|
248
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Discriminated union for adding any scheduled job type
|
|
265
|
+
*/
|
|
266
|
+
export type AddJobOptions = AddCronJob | AddIntervalJob | AddTimeoutJob;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Update a cron job's expression
|
|
270
|
+
*/
|
|
271
|
+
export interface UpdateCronJob {
|
|
272
|
+
type: 'cron';
|
|
273
|
+
name: string;
|
|
274
|
+
expression: string;
|
|
249
275
|
}
|
|
250
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Update an interval job's interval
|
|
279
|
+
*/
|
|
280
|
+
export interface UpdateIntervalJob {
|
|
281
|
+
type: 'interval';
|
|
282
|
+
name: string;
|
|
283
|
+
intervalMs: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update a timeout job's delay
|
|
288
|
+
*/
|
|
289
|
+
export interface UpdateTimeoutJob {
|
|
290
|
+
type: 'timeout';
|
|
291
|
+
name: string;
|
|
292
|
+
timeoutMs: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Discriminated union for updating any scheduled job type
|
|
297
|
+
*/
|
|
298
|
+
export type UpdateJobOptions = UpdateCronJob | UpdateIntervalJob | UpdateTimeoutJob;
|
|
299
|
+
|
|
251
300
|
/**
|
|
252
301
|
* Information about a scheduled job
|
|
253
302
|
*/
|
|
@@ -255,13 +304,23 @@ export interface ScheduledJobInfo {
|
|
|
255
304
|
/** Job name */
|
|
256
305
|
name: string;
|
|
257
306
|
|
|
307
|
+
/** Job type */
|
|
308
|
+
type: 'cron' | 'interval' | 'timeout';
|
|
309
|
+
|
|
258
310
|
/** Pattern to publish to */
|
|
259
311
|
pattern: string;
|
|
260
312
|
|
|
313
|
+
/** Whether the job is paused */
|
|
314
|
+
paused: boolean;
|
|
315
|
+
|
|
316
|
+
/** Whether the job was created via decorator (@Cron, @Interval, @Timeout) */
|
|
317
|
+
declarative: boolean;
|
|
318
|
+
|
|
261
319
|
/** Schedule configuration */
|
|
262
320
|
schedule: {
|
|
263
321
|
cron?: string;
|
|
264
322
|
every?: number;
|
|
323
|
+
timeout?: number;
|
|
265
324
|
};
|
|
266
325
|
|
|
267
326
|
/** Next scheduled run time */
|
|
@@ -286,7 +345,6 @@ export type QueueFeature =
|
|
|
286
345
|
| 'priority'
|
|
287
346
|
| 'dead-letter-queue'
|
|
288
347
|
| 'retry'
|
|
289
|
-
| 'scheduled-jobs'
|
|
290
348
|
| 'consumer-groups'
|
|
291
349
|
| 'pattern-subscriptions';
|
|
292
350
|
|
|
@@ -353,16 +411,6 @@ export interface QueueAdapter {
|
|
|
353
411
|
options?: SubscribeOptions
|
|
354
412
|
): Promise<Subscription>;
|
|
355
413
|
|
|
356
|
-
// Scheduled Jobs
|
|
357
|
-
/** Add a scheduled job */
|
|
358
|
-
addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void>;
|
|
359
|
-
|
|
360
|
-
/** Remove a scheduled job */
|
|
361
|
-
removeScheduledJob(name: string): Promise<boolean>;
|
|
362
|
-
|
|
363
|
-
/** Get all scheduled jobs */
|
|
364
|
-
getScheduledJobs(): Promise<ScheduledJobInfo[]>;
|
|
365
|
-
|
|
366
414
|
// Feature Support
|
|
367
415
|
/** Check if a feature is supported by this adapter */
|
|
368
416
|
supports(feature: QueueFeature): boolean;
|
|
@@ -232,7 +232,7 @@ export const fakeTimers = new FakeTimers();
|
|
|
232
232
|
*
|
|
233
233
|
* @example
|
|
234
234
|
* ```typescript
|
|
235
|
-
* import { useFakeTimers } from '@onebun/core';
|
|
235
|
+
* import { useFakeTimers } from '@onebun/core/testing';
|
|
236
236
|
*
|
|
237
237
|
* describe('My tests', () => {
|
|
238
238
|
* const { advanceTime, restore } = useFakeTimers();
|