@onebun/core 0.2.12 → 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 +1 -1
- package/src/application/application.test.ts +0 -7
- package/src/decorators/metadata.test.ts +86 -0
- package/src/decorators/metadata.ts +199 -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 +75 -27
package/package.json
CHANGED
|
@@ -3964,13 +3964,6 @@ describe('OneBunApplication', () => {
|
|
|
3964
3964
|
};
|
|
3965
3965
|
}
|
|
3966
3966
|
|
|
3967
|
-
async addScheduledJob(): Promise<void> {}
|
|
3968
|
-
async removeScheduledJob(): Promise<boolean> {
|
|
3969
|
-
return false;
|
|
3970
|
-
}
|
|
3971
|
-
async getScheduledJobs(): Promise<import('../queue/types').ScheduledJobInfo[]> {
|
|
3972
|
-
return [];
|
|
3973
|
-
}
|
|
3974
3967
|
supports(): boolean {
|
|
3975
3968
|
return false;
|
|
3976
3969
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
defineMetadata,
|
|
17
|
+
diagnoseDecoratorMetadata,
|
|
17
18
|
getMetadata,
|
|
18
19
|
getConstructorParamTypes,
|
|
19
20
|
setConstructorParamTypes,
|
|
@@ -731,6 +732,91 @@ describe('Metadata System', () => {
|
|
|
731
732
|
});
|
|
732
733
|
});
|
|
733
734
|
|
|
735
|
+
describe('diagnoseDecoratorMetadata', () => {
|
|
736
|
+
test('should return ok when no classes have constructor params', () => {
|
|
737
|
+
class NoParams {}
|
|
738
|
+
|
|
739
|
+
const result = diagnoseDecoratorMetadata([NoParams]);
|
|
740
|
+
|
|
741
|
+
expect(result.ok).toBe(true);
|
|
742
|
+
expect(result.classesWithParams).toBe(0);
|
|
743
|
+
expect(result.classesWithMetadata).toBe(0);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test('should return ok when classes with params have metadata', () => {
|
|
747
|
+
class DepA {}
|
|
748
|
+
class WithMeta {
|
|
749
|
+
constructor(_dep: DepA) {}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Simulate Bun emitting design:paramtypes via Reflect polyfill
|
|
753
|
+
(globalThis as any).Reflect.defineMetadata('design:paramtypes', [DepA], WithMeta);
|
|
754
|
+
|
|
755
|
+
const result = diagnoseDecoratorMetadata([WithMeta]);
|
|
756
|
+
|
|
757
|
+
expect(result.ok).toBe(true);
|
|
758
|
+
expect(result.classesWithParams).toBe(1);
|
|
759
|
+
expect(result.classesWithMetadata).toBe(1);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('should return not ok when classes with params have NO metadata', () => {
|
|
763
|
+
// Use a fresh class without any metadata set
|
|
764
|
+
class NoDep {}
|
|
765
|
+
class BrokenService {
|
|
766
|
+
constructor(_dep: NoDep) {}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const result = diagnoseDecoratorMetadata([BrokenService]);
|
|
770
|
+
|
|
771
|
+
expect(result.ok).toBe(false);
|
|
772
|
+
expect(result.classesWithParams).toBe(1);
|
|
773
|
+
expect(result.classesWithMetadata).toBe(0);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test('should return ok when at least one class has metadata', () => {
|
|
777
|
+
class DepA {}
|
|
778
|
+
class DepB {}
|
|
779
|
+
|
|
780
|
+
class ServiceA {
|
|
781
|
+
constructor(_dep: DepA) {}
|
|
782
|
+
}
|
|
783
|
+
class ServiceB {
|
|
784
|
+
constructor(_dep: DepB) {}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Only one has metadata — still ok (metadata emission works in principle)
|
|
788
|
+
(globalThis as any).Reflect.defineMetadata('design:paramtypes', [DepA], ServiceA);
|
|
789
|
+
|
|
790
|
+
const result = diagnoseDecoratorMetadata([ServiceA, ServiceB]);
|
|
791
|
+
|
|
792
|
+
expect(result.ok).toBe(true);
|
|
793
|
+
expect(result.classesWithParams).toBe(2);
|
|
794
|
+
expect(result.classesWithMetadata).toBe(1);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test('should return ok for empty class list', () => {
|
|
798
|
+
const result = diagnoseDecoratorMetadata([]);
|
|
799
|
+
|
|
800
|
+
expect(result.ok).toBe(true);
|
|
801
|
+
expect(result.classesWithParams).toBe(0);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test('should handle mixed classes with and without params', () => {
|
|
805
|
+
class Dep {}
|
|
806
|
+
class NoParamsService {}
|
|
807
|
+
class WithParamsService {
|
|
808
|
+
constructor(_dep: Dep) {}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// No metadata for WithParamsService
|
|
812
|
+
const result = diagnoseDecoratorMetadata([NoParamsService, WithParamsService]);
|
|
813
|
+
|
|
814
|
+
expect(result.ok).toBe(false);
|
|
815
|
+
expect(result.classesWithParams).toBe(1);
|
|
816
|
+
expect(result.classesWithMetadata).toBe(0);
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
734
820
|
describe('Metadata storage edge cases', () => {
|
|
735
821
|
test('should handle metadata on object values', () => {
|
|
736
822
|
const objectValue = { value: 42 };
|
|
@@ -286,6 +286,205 @@ export function getConstructorParamTypes(target: Function): Function[] | undefin
|
|
|
286
286
|
return undefined;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Diagnostic: check whether Bun is emitting decorator metadata (design:paramtypes).
|
|
291
|
+
*
|
|
292
|
+
* Scans an array of decorated classes. If at least one class has constructor
|
|
293
|
+
* parameters (target.length > 0) but NONE of them have design:paramtypes,
|
|
294
|
+
* then emitDecoratorMetadata is not working — typically because the setting
|
|
295
|
+
* is missing from the root tsconfig.json that Bun actually reads.
|
|
296
|
+
*
|
|
297
|
+
* @param decoratedClasses - Classes registered via @Service / @Controller / @Middleware
|
|
298
|
+
* @returns Object with diagnostic result and details
|
|
299
|
+
*/
|
|
300
|
+
export function diagnoseDecoratorMetadata(decoratedClasses: Function[]): {
|
|
301
|
+
ok: boolean;
|
|
302
|
+
classesWithParams: number;
|
|
303
|
+
classesWithMetadata: number;
|
|
304
|
+
} {
|
|
305
|
+
let classesWithParams = 0;
|
|
306
|
+
let classesWithMetadata = 0;
|
|
307
|
+
|
|
308
|
+
for (const cls of decoratedClasses) {
|
|
309
|
+
if (cls.length > 0) {
|
|
310
|
+
classesWithParams++;
|
|
311
|
+
const types = (globalThis as any).Reflect?.getMetadata?.('design:paramtypes', cls);
|
|
312
|
+
if (types && Array.isArray(types) && types.length > 0) {
|
|
313
|
+
classesWithMetadata++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
ok: classesWithParams === 0 || classesWithMetadata > 0,
|
|
320
|
+
classesWithParams,
|
|
321
|
+
classesWithMetadata,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Build a detailed diagnostic message by inspecting the project's tsconfig files.
|
|
327
|
+
*
|
|
328
|
+
* Walks up from `process.cwd()` looking for tsconfig.json files, reads them,
|
|
329
|
+
* and tells the user exactly which file to edit and what to add.
|
|
330
|
+
*/
|
|
331
|
+
export function buildDecoratorMetadataDiagnosticMessage(
|
|
332
|
+
classesWithParams: number,
|
|
333
|
+
): string {
|
|
334
|
+
const fs = require('node:fs');
|
|
335
|
+
const pathModule = require('node:path');
|
|
336
|
+
|
|
337
|
+
const header =
|
|
338
|
+
`[OneBun] Dependency injection is broken: none of the ${classesWithParams} ` +
|
|
339
|
+
'service(s) with constructor parameters have design:paramtypes metadata.\n' +
|
|
340
|
+
'Bun is NOT emitting decorator metadata.\n';
|
|
341
|
+
|
|
342
|
+
type TsconfigInfo = { path: string; content: any; hasEmit: boolean; hasExperimental: boolean };
|
|
343
|
+
|
|
344
|
+
const readTsconfig = (tsconfigPath: string): TsconfigInfo | null => {
|
|
345
|
+
try {
|
|
346
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const raw = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
350
|
+
// Strip comments (single-line // and multi-line /* */) for JSON.parse
|
|
351
|
+
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
352
|
+
const parsed = JSON.parse(stripped);
|
|
353
|
+
const compilerOptions = parsed.compilerOptions || {};
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
path: tsconfigPath,
|
|
357
|
+
content: parsed,
|
|
358
|
+
hasEmit: compilerOptions.emitDecoratorMetadata === true,
|
|
359
|
+
hasExperimental: compilerOptions.experimentalDecorators === true,
|
|
360
|
+
};
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// 1. Find tsconfig.json files walking UP from cwd to filesystem root
|
|
367
|
+
const tsconfigFiles: TsconfigInfo[] = [];
|
|
368
|
+
let dir = process.cwd();
|
|
369
|
+
const root = pathModule.parse(dir).root;
|
|
370
|
+
|
|
371
|
+
while (dir !== root) {
|
|
372
|
+
const info = readTsconfig(pathModule.join(dir, 'tsconfig.json'));
|
|
373
|
+
if (info) {
|
|
374
|
+
tsconfigFiles.push(info);
|
|
375
|
+
}
|
|
376
|
+
dir = pathModule.dirname(dir);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 2. Also scan immediate subdirectories of cwd for child tsconfig.json files
|
|
380
|
+
// (e.g. packages/backend/tsconfig.json in a monorepo)
|
|
381
|
+
const childTsconfigFiles: TsconfigInfo[] = [];
|
|
382
|
+
try {
|
|
383
|
+
const scanDirs = [process.cwd()];
|
|
384
|
+
// Scan up to 2 levels deep to find packages/*/tsconfig.json and similar
|
|
385
|
+
for (const scanDir of scanDirs) {
|
|
386
|
+
const entries = fs.readdirSync(scanDir, { withFileTypes: true });
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
389
|
+
const subdir = pathModule.join(scanDir, entry.name);
|
|
390
|
+
const info = readTsconfig(pathModule.join(subdir, 'tsconfig.json'));
|
|
391
|
+
if (info) {
|
|
392
|
+
childTsconfigFiles.push(info);
|
|
393
|
+
}
|
|
394
|
+
// One level deeper (e.g. packages/backend/)
|
|
395
|
+
try {
|
|
396
|
+
const subEntries = fs.readdirSync(subdir, { withFileTypes: true });
|
|
397
|
+
for (const subEntry of subEntries) {
|
|
398
|
+
if (subEntry.isDirectory() && !subEntry.name.startsWith('.') && subEntry.name !== 'node_modules') {
|
|
399
|
+
const info2 = readTsconfig(pathModule.join(subdir, subEntry.name, 'tsconfig.json'));
|
|
400
|
+
if (info2) {
|
|
401
|
+
childTsconfigFiles.push(info2);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
// Skip unreadable directories
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
// Skip if directory scanning fails
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (tsconfigFiles.length === 0) {
|
|
416
|
+
return (
|
|
417
|
+
header +
|
|
418
|
+
'\nNo tsconfig.json found. Create one in your project root with:\n\n' +
|
|
419
|
+
' {\n' +
|
|
420
|
+
' "compilerOptions": {\n' +
|
|
421
|
+
' "experimentalDecorators": true,\n' +
|
|
422
|
+
' "emitDecoratorMetadata": true\n' +
|
|
423
|
+
' }\n' +
|
|
424
|
+
' }\n'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The first (deepest / closest to cwd) tsconfig is what the user likely expects to work.
|
|
429
|
+
// The last (shallowest / closest to root) is what Bun actually reads.
|
|
430
|
+
const rootTsconfig = tsconfigFiles[tsconfigFiles.length - 1];
|
|
431
|
+
|
|
432
|
+
// Check if any child tsconfig has the settings but root does not
|
|
433
|
+
// Look in both parent chain and scanned subdirectories
|
|
434
|
+
const allConfigs = [...tsconfigFiles, ...childTsconfigFiles];
|
|
435
|
+
const childWithSettings = allConfigs.find(
|
|
436
|
+
(t) => t.path !== rootTsconfig.path && (t.hasEmit || t.hasExperimental),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const lines: string[] = [header];
|
|
440
|
+
|
|
441
|
+
if (rootTsconfig.hasEmit && rootTsconfig.hasExperimental) {
|
|
442
|
+
// Root has both settings — unusual, might be a different issue
|
|
443
|
+
lines.push(`\nRoot tsconfig (${rootTsconfig.path}) already has both settings.`);
|
|
444
|
+
lines.push('If DI is still broken, check that Bun is reading this file for your entry point.');
|
|
445
|
+
|
|
446
|
+
return lines.join('\n');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Report what's missing from the root tsconfig
|
|
450
|
+
const missing: string[] = [];
|
|
451
|
+
if (!rootTsconfig.hasExperimental) {
|
|
452
|
+
missing.push('"experimentalDecorators": true');
|
|
453
|
+
}
|
|
454
|
+
if (!rootTsconfig.hasEmit) {
|
|
455
|
+
missing.push('"emitDecoratorMetadata": true');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
lines.push(`\nRoot tsconfig: ${rootTsconfig.path}`);
|
|
459
|
+
lines.push(`Missing in "compilerOptions": ${missing.join(', ')}`);
|
|
460
|
+
|
|
461
|
+
if (childWithSettings) {
|
|
462
|
+
const childHas: string[] = [];
|
|
463
|
+
if (childWithSettings.hasExperimental) {
|
|
464
|
+
childHas.push('"experimentalDecorators": true');
|
|
465
|
+
}
|
|
466
|
+
if (childWithSettings.hasEmit) {
|
|
467
|
+
childHas.push('"emitDecoratorMetadata": true');
|
|
468
|
+
}
|
|
469
|
+
lines.push(
|
|
470
|
+
`\nNote: ${childWithSettings.path} has ${childHas.join(' and ')},` +
|
|
471
|
+
' but Bun ignores these when they are only in a child tsconfig.',
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
lines.push(`\nFix: add the missing option(s) to ${rootTsconfig.path}:\n`);
|
|
476
|
+
lines.push(' "compilerOptions": {');
|
|
477
|
+
if (!rootTsconfig.hasExperimental) {
|
|
478
|
+
lines.push(' "experimentalDecorators": true,');
|
|
479
|
+
}
|
|
480
|
+
if (!rootTsconfig.hasEmit) {
|
|
481
|
+
lines.push(' "emitDecoratorMetadata": true');
|
|
482
|
+
}
|
|
483
|
+
lines.push(' }');
|
|
484
|
+
|
|
485
|
+
return lines.join('\n');
|
|
486
|
+
}
|
|
487
|
+
|
|
289
488
|
/**
|
|
290
489
|
* Namespace for metadata functions to mimic Reflect API
|
|
291
490
|
*/
|
package/src/module/module.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isGlobalModule,
|
|
23
23
|
registerControllerDependencies,
|
|
24
24
|
} from '../decorators/decorators';
|
|
25
|
+
import { buildDecoratorMetadataDiagnosticMessage, diagnoseDecoratorMetadata } from '../decorators/metadata';
|
|
25
26
|
import { QueueService, QueueServiceTag } from '../queue';
|
|
26
27
|
import { BaseWebSocketGateway } from '../websocket/ws-base-gateway';
|
|
27
28
|
import { isWebSocketGateway } from '../websocket/ws-decorators';
|
|
@@ -70,6 +71,7 @@ const processedGlobalModules: Set<Function> = ((globalThis as any)[Symbol.for('o
|
|
|
70
71
|
export function clearGlobalServicesRegistry(): void {
|
|
71
72
|
globalServicesRegistry.clear();
|
|
72
73
|
processedGlobalModules.clear();
|
|
74
|
+
OneBunModule.resetDecoratorMetadataDiagnosis();
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -274,6 +276,20 @@ export class OneBunModule implements ModuleInstance {
|
|
|
274
276
|
* Create services with automatic dependency injection
|
|
275
277
|
* Services can depend on other services (including imported ones)
|
|
276
278
|
*/
|
|
279
|
+
/**
|
|
280
|
+
* Whether the decorator metadata diagnostic has already run (once per process).
|
|
281
|
+
* Prevents repeated checks across multiple module initializations.
|
|
282
|
+
*/
|
|
283
|
+
private static decoratorMetadataDiagnosed = false;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Reset the diagnostic flag (for testing).
|
|
287
|
+
* @internal
|
|
288
|
+
*/
|
|
289
|
+
static resetDecoratorMetadataDiagnosis(): void {
|
|
290
|
+
OneBunModule.decoratorMetadataDiagnosed = false;
|
|
291
|
+
}
|
|
292
|
+
|
|
277
293
|
private createServicesWithDI(metadata: ReturnType<typeof getModuleMetadata>): void {
|
|
278
294
|
if (!metadata?.providers) {
|
|
279
295
|
return;
|
|
@@ -295,6 +311,26 @@ export class OneBunModule implements ModuleInstance {
|
|
|
295
311
|
}
|
|
296
312
|
}
|
|
297
313
|
|
|
314
|
+
// Run decorator metadata diagnostic once: check that Bun emits design:paramtypes.
|
|
315
|
+
// If emitDecoratorMetadata is missing from the root tsconfig.json, Bun silently
|
|
316
|
+
// skips metadata emission and ALL constructor-based DI breaks.
|
|
317
|
+
// Only mark as diagnosed when we actually found classes with constructor params
|
|
318
|
+
// (modules with only zero-arg services can't tell us anything).
|
|
319
|
+
if (!OneBunModule.decoratorMetadataDiagnosed) {
|
|
320
|
+
const providerClasses = metadata.providers.filter(
|
|
321
|
+
(p): p is Function => typeof p === 'function',
|
|
322
|
+
);
|
|
323
|
+
const diagnosis = diagnoseDecoratorMetadata(providerClasses);
|
|
324
|
+
if (diagnosis.classesWithParams > 0) {
|
|
325
|
+
OneBunModule.decoratorMetadataDiagnosed = true;
|
|
326
|
+
if (!diagnosis.ok) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
buildDecoratorMetadataDiagnosticMessage(diagnosis.classesWithParams),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
298
334
|
// Create services in dependency order
|
|
299
335
|
const pendingProviders = [...metadata.providers.filter((p) => typeof p === 'function')];
|
|
300
336
|
const createdServices = new Set<string>();
|
|
@@ -384,10 +384,6 @@ describe('InMemoryQueueAdapter', () => {
|
|
|
384
384
|
expect(adapter.supports('priority')).toBe(true);
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
it('should support scheduled-jobs', () => {
|
|
388
|
-
expect(adapter.supports('scheduled-jobs')).toBe(true);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
387
|
it('should not support consumer-groups', () => {
|
|
392
388
|
expect(adapter.supports('consumer-groups')).toBe(false);
|
|
393
389
|
});
|
|
@@ -15,8 +15,6 @@ import type {
|
|
|
15
15
|
PublishOptions,
|
|
16
16
|
SubscribeOptions,
|
|
17
17
|
Subscription,
|
|
18
|
-
ScheduledJobOptions,
|
|
19
|
-
ScheduledJobInfo,
|
|
20
18
|
MessageHandler,
|
|
21
19
|
} from '../types';
|
|
22
20
|
|
|
@@ -322,49 +320,6 @@ export class InMemoryQueueAdapter implements QueueAdapter {
|
|
|
322
320
|
return subscription;
|
|
323
321
|
}
|
|
324
322
|
|
|
325
|
-
// ============================================================================
|
|
326
|
-
// Scheduled Jobs
|
|
327
|
-
// ============================================================================
|
|
328
|
-
|
|
329
|
-
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
330
|
-
this.ensureConnected();
|
|
331
|
-
|
|
332
|
-
if (!this.scheduler) {
|
|
333
|
-
throw new Error('Scheduler not initialized');
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (options.schedule.cron) {
|
|
337
|
-
this.scheduler.addCronJob(name, options.schedule.cron, options.pattern, () => options.data, {
|
|
338
|
-
metadata: options.metadata,
|
|
339
|
-
overlapStrategy: options.overlapStrategy,
|
|
340
|
-
});
|
|
341
|
-
} else if (options.schedule.every) {
|
|
342
|
-
this.scheduler.addIntervalJob(name, options.schedule.every, options.pattern, () => options.data, {
|
|
343
|
-
metadata: options.metadata,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async removeScheduledJob(name: string): Promise<boolean> {
|
|
349
|
-
this.ensureConnected();
|
|
350
|
-
|
|
351
|
-
if (!this.scheduler) {
|
|
352
|
-
return false;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return this.scheduler.removeJob(name);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
359
|
-
this.ensureConnected();
|
|
360
|
-
|
|
361
|
-
if (!this.scheduler) {
|
|
362
|
-
return [];
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return this.scheduler.getJobs();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
323
|
// ============================================================================
|
|
369
324
|
// Features
|
|
370
325
|
// ============================================================================
|
|
@@ -373,7 +328,6 @@ export class InMemoryQueueAdapter implements QueueAdapter {
|
|
|
373
328
|
switch (feature) {
|
|
374
329
|
case 'delayed-messages':
|
|
375
330
|
case 'priority':
|
|
376
|
-
case 'scheduled-jobs':
|
|
377
331
|
case 'pattern-subscriptions':
|
|
378
332
|
return true;
|
|
379
333
|
case 'consumer-groups':
|
|
@@ -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
|
// ============================================================================
|