@onebun/core 0.2.5 → 0.2.7
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 -6
- package/src/application/application.test.ts +865 -1
- package/src/application/application.ts +247 -48
- package/src/application/multi-service-application.test.ts +15 -0
- package/src/application/multi-service-application.ts +2 -0
- package/src/application/multi-service.types.ts +7 -1
- package/src/decorators/decorators.test.ts +28 -0
- package/src/decorators/decorators.ts +26 -0
- package/src/docs-examples.test.ts +29 -1
- package/src/index.ts +1 -0
- package/src/module/module.test.ts +60 -7
- package/src/module/module.ts +36 -0
- package/src/queue/docs-examples.test.ts +72 -12
- package/src/queue/index.ts +4 -0
- package/src/queue/queue-service-proxy.test.ts +82 -0
- package/src/queue/queue-service-proxy.ts +114 -0
- package/src/queue/types.ts +2 -2
- package/src/types.ts +62 -3
|
@@ -24,7 +24,11 @@ import type {
|
|
|
24
24
|
} from '../types';
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
Controller as CtrlDeco,
|
|
29
|
+
Middleware,
|
|
30
|
+
Module,
|
|
31
|
+
} from '../decorators/decorators';
|
|
28
32
|
import { makeMockLoggerLayer } from '../testing/test-utils';
|
|
29
33
|
import { BaseWebSocketGateway } from '../websocket/ws-base-gateway';
|
|
30
34
|
import { WebSocketGateway } from '../websocket/ws-decorators';
|
|
@@ -685,7 +689,7 @@ describe('OneBunModule', () => {
|
|
|
685
689
|
|
|
686
690
|
describe('Controller DI with @Inject decorator', () => {
|
|
687
691
|
const {
|
|
688
|
-
Inject, getConstructorParamTypes, Controller: ControllerDecorator, clearGlobalModules,
|
|
692
|
+
Inject: InjectDecorator, getConstructorParamTypes, Controller: ControllerDecorator, clearGlobalModules,
|
|
689
693
|
} = require('../decorators/decorators');
|
|
690
694
|
const { Controller: BaseController } = require('./controller');
|
|
691
695
|
const { clearGlobalServicesRegistry: clearRegistry } = require('./module');
|
|
@@ -716,7 +720,7 @@ describe('OneBunModule', () => {
|
|
|
716
720
|
|
|
717
721
|
// Define controller with @Inject BEFORE @Controller
|
|
718
722
|
class OriginalController extends BaseController {
|
|
719
|
-
constructor(@
|
|
723
|
+
constructor(@InjectDecorator(SimpleService) private svc: SimpleService) {
|
|
720
724
|
super();
|
|
721
725
|
}
|
|
722
726
|
}
|
|
@@ -1189,6 +1193,55 @@ describe('OneBunModule', () => {
|
|
|
1189
1193
|
});
|
|
1190
1194
|
});
|
|
1191
1195
|
|
|
1196
|
+
describe('Middleware with service injection', () => {
|
|
1197
|
+
test('should resolve middleware with injected service and use it in use()', async () => {
|
|
1198
|
+
@Service()
|
|
1199
|
+
class HelperService {
|
|
1200
|
+
getValue(): string {
|
|
1201
|
+
return 'injected';
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
@Middleware()
|
|
1206
|
+
class MiddlewareWithService extends BaseMiddleware {
|
|
1207
|
+
constructor(private readonly helper: HelperService) {
|
|
1208
|
+
super();
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async use(_req: OneBunRequest, next: () => Promise<OneBunResponse>): Promise<OneBunResponse> {
|
|
1212
|
+
const res = await next();
|
|
1213
|
+
res.headers.set('X-Injected-Value', this.helper.getValue());
|
|
1214
|
+
|
|
1215
|
+
return res;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
@CtrlDeco('/test')
|
|
1220
|
+
class TestCtrl extends CtrlBase {}
|
|
1221
|
+
|
|
1222
|
+
@Module({
|
|
1223
|
+
controllers: [TestCtrl],
|
|
1224
|
+
providers: [HelperService],
|
|
1225
|
+
})
|
|
1226
|
+
class TestModule {}
|
|
1227
|
+
|
|
1228
|
+
const module = new OneBunModule(TestModule, mockLoggerLayer);
|
|
1229
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
1230
|
+
|
|
1231
|
+
const resolved = module.resolveMiddleware([MiddlewareWithService]);
|
|
1232
|
+
expect(resolved).toHaveLength(1);
|
|
1233
|
+
|
|
1234
|
+
const mockReq = Object.assign(new Request('http://localhost/'), {
|
|
1235
|
+
params: {},
|
|
1236
|
+
cookies: new Map(),
|
|
1237
|
+
}) as unknown as OneBunRequest;
|
|
1238
|
+
const next = async (): Promise<OneBunResponse> => new Response('ok');
|
|
1239
|
+
const response = await resolved[0](mockReq, next);
|
|
1240
|
+
|
|
1241
|
+
expect(response.headers.get('X-Injected-Value')).toBe('injected');
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1192
1245
|
describe('Lifecycle hooks', () => {
|
|
1193
1246
|
const { clearGlobalModules } = require('../decorators/decorators');
|
|
1194
1247
|
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
|
|
@@ -1375,7 +1428,7 @@ describe('OneBunModule', () => {
|
|
|
1375
1428
|
const {
|
|
1376
1429
|
Controller: ControllerDecorator,
|
|
1377
1430
|
Get,
|
|
1378
|
-
Inject,
|
|
1431
|
+
Inject: InjectDecorator,
|
|
1379
1432
|
clearGlobalModules,
|
|
1380
1433
|
} = require('../decorators/decorators');
|
|
1381
1434
|
const { Controller: BaseController } = require('./controller');
|
|
@@ -1404,7 +1457,7 @@ describe('OneBunModule', () => {
|
|
|
1404
1457
|
}
|
|
1405
1458
|
|
|
1406
1459
|
class CounterController extends BaseController {
|
|
1407
|
-
constructor(@
|
|
1460
|
+
constructor(@InjectDecorator(CounterService) private readonly counterService: CounterService) {
|
|
1408
1461
|
super();
|
|
1409
1462
|
}
|
|
1410
1463
|
getCount() {
|
|
@@ -1439,7 +1492,7 @@ describe('OneBunModule', () => {
|
|
|
1439
1492
|
}
|
|
1440
1493
|
|
|
1441
1494
|
class ChildController extends BaseController {
|
|
1442
|
-
constructor(@
|
|
1495
|
+
constructor(@InjectDecorator(ChildService) private readonly childService: ChildService) {
|
|
1443
1496
|
super();
|
|
1444
1497
|
}
|
|
1445
1498
|
getValue() {
|
|
@@ -1486,7 +1539,7 @@ describe('OneBunModule', () => {
|
|
|
1486
1539
|
class SharedModule {}
|
|
1487
1540
|
|
|
1488
1541
|
class AppController extends BaseController {
|
|
1489
|
-
constructor(@
|
|
1542
|
+
constructor(@InjectDecorator(SharedService) private readonly sharedService: SharedService) {
|
|
1490
1543
|
super();
|
|
1491
1544
|
}
|
|
1492
1545
|
getLabel() {
|
package/src/module/module.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isGlobalModule,
|
|
23
23
|
registerControllerDependencies,
|
|
24
24
|
} from '../decorators/decorators';
|
|
25
|
+
import { QueueService, QueueServiceTag } from '../queue';
|
|
25
26
|
import { BaseWebSocketGateway } from '../websocket/ws-base-gateway';
|
|
26
27
|
import { isWebSocketGateway } from '../websocket/ws-decorators';
|
|
27
28
|
|
|
@@ -616,6 +617,16 @@ export class OneBunModule implements ModuleInstance {
|
|
|
616
617
|
* Resolve dependency by type (constructor function)
|
|
617
618
|
*/
|
|
618
619
|
private resolveDependencyByType(type: Function): unknown {
|
|
620
|
+
// QueueService is registered by tag (QueueServiceTag) before setup(); resolve by tag
|
|
621
|
+
if (type === QueueService) {
|
|
622
|
+
const byTag = this.serviceInstances.get(
|
|
623
|
+
QueueServiceTag as Context.Tag<unknown, unknown>,
|
|
624
|
+
);
|
|
625
|
+
if (byTag !== undefined) {
|
|
626
|
+
return byTag;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
619
630
|
// Find service instance that matches the type
|
|
620
631
|
const serviceInstance = Array.from(this.serviceInstances.values()).find((instance) => {
|
|
621
632
|
if (!instance) {
|
|
@@ -974,6 +985,24 @@ export class OneBunModule implements ModuleInstance {
|
|
|
974
985
|
return [];
|
|
975
986
|
}
|
|
976
987
|
|
|
988
|
+
/**
|
|
989
|
+
* Get the module instance that owns the given controller.
|
|
990
|
+
* Used to resolve controller-level and route-level middleware with the owner module's DI.
|
|
991
|
+
*/
|
|
992
|
+
getOwnerModuleForController(controllerClass: Function): ModuleInstance | undefined {
|
|
993
|
+
if (this.controllers.includes(controllerClass)) {
|
|
994
|
+
return this;
|
|
995
|
+
}
|
|
996
|
+
for (const childModule of this.childModules) {
|
|
997
|
+
const owner = childModule.getOwnerModuleForController(controllerClass);
|
|
998
|
+
if (owner) {
|
|
999
|
+
return owner;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
977
1006
|
/**
|
|
978
1007
|
* Get all controller instances from this module and child modules (recursive).
|
|
979
1008
|
*/
|
|
@@ -1023,6 +1052,13 @@ export class OneBunModule implements ModuleInstance {
|
|
|
1023
1052
|
return this.rootLayer;
|
|
1024
1053
|
}
|
|
1025
1054
|
|
|
1055
|
+
/**
|
|
1056
|
+
* Register a service instance by tag (e.g. before setup() for application-provided services like QueueService proxy).
|
|
1057
|
+
*/
|
|
1058
|
+
registerService<T>(tag: Context.Tag<unknown, T>, instance: T): void {
|
|
1059
|
+
this.serviceInstances.set(tag as Context.Tag<unknown, unknown>, instance);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1026
1062
|
/**
|
|
1027
1063
|
* Create a module from class
|
|
1028
1064
|
* @param moduleClass - The module class
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
MessageAnyGuard,
|
|
36
36
|
createMessageGuard,
|
|
37
37
|
type Message,
|
|
38
|
+
type QueueAdapter,
|
|
38
39
|
CronExpression,
|
|
39
40
|
parseCronExpression,
|
|
40
41
|
getNextRun,
|
|
@@ -113,9 +114,9 @@ describe('Setup Section Examples (docs/api/queue.md)', () => {
|
|
|
113
114
|
* @source docs/api/queue.md#quick-start
|
|
114
115
|
*/
|
|
115
116
|
describe('Quick Start Example (docs/api/queue.md)', () => {
|
|
116
|
-
it('should define
|
|
117
|
-
// From docs/api/queue.md: Quick Start
|
|
118
|
-
class
|
|
117
|
+
it('should define controller with queue decorators', () => {
|
|
118
|
+
// From docs/api/queue.md: Quick Start (queue handlers must be in controllers)
|
|
119
|
+
class EventProcessor {
|
|
119
120
|
@OnQueueReady()
|
|
120
121
|
onReady() {
|
|
121
122
|
// console.log('Queue connected');
|
|
@@ -134,19 +135,19 @@ describe('Quick Start Example (docs/api/queue.md)', () => {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// Verify decorators are registered
|
|
137
|
-
expect(hasQueueDecorators(
|
|
138
|
+
expect(hasQueueDecorators(EventProcessor)).toBe(true);
|
|
138
139
|
|
|
139
|
-
const subscriptions = getSubscribeMetadata(
|
|
140
|
+
const subscriptions = getSubscribeMetadata(EventProcessor);
|
|
140
141
|
expect(subscriptions.length).toBe(1);
|
|
141
142
|
expect(subscriptions[0].pattern).toBe('orders.created');
|
|
142
143
|
|
|
143
|
-
const cronJobs = getCronMetadata(
|
|
144
|
+
const cronJobs = getCronMetadata(EventProcessor);
|
|
144
145
|
expect(cronJobs.length).toBe(1);
|
|
145
146
|
expect(cronJobs[0].expression).toBe(CronExpression.EVERY_HOUR);
|
|
146
147
|
expect(cronJobs[0].options.pattern).toBe('cleanup.expired');
|
|
147
148
|
});
|
|
148
149
|
|
|
149
|
-
it('should define
|
|
150
|
+
it('should define controller with interval decorator', () => {
|
|
150
151
|
// From docs/api/queue.md: Quick Start - Interval example
|
|
151
152
|
class EventProcessor {
|
|
152
153
|
@Subscribe('orders.created')
|
|
@@ -389,9 +390,9 @@ describe('Message Guards Examples (docs/api/queue.md)', () => {
|
|
|
389
390
|
* @source docs/api/queue.md#lifecycle-decorators
|
|
390
391
|
*/
|
|
391
392
|
describe('Lifecycle Decorators Examples (docs/api/queue.md)', () => {
|
|
392
|
-
it('should define lifecycle handlers', () => {
|
|
393
|
-
// From docs/api/queue.md: Lifecycle Decorators
|
|
394
|
-
class
|
|
393
|
+
it('should define lifecycle handlers on controller', () => {
|
|
394
|
+
// From docs/api/queue.md: Lifecycle Decorators (handlers must be in controllers)
|
|
395
|
+
class EventProcessor {
|
|
395
396
|
@OnQueueReady()
|
|
396
397
|
handleReady() {
|
|
397
398
|
// console.log('Queue connected');
|
|
@@ -418,8 +419,67 @@ describe('Lifecycle Decorators Examples (docs/api/queue.md)', () => {
|
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
// Class
|
|
422
|
-
expect(
|
|
422
|
+
// Class with lifecycle handlers only; hasQueueDecorators checks Subscribe/Cron/Interval/Timeout only
|
|
423
|
+
expect(EventProcessor).toBeDefined();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @source docs/api/queue.md#custom-adapter-nats-jetstream
|
|
429
|
+
*/
|
|
430
|
+
describe('Custom adapter NATS JetStream (docs/api/queue.md)', () => {
|
|
431
|
+
it('should use custom adapter constructor with options', async () => {
|
|
432
|
+
// From docs/api/queue.md: Custom adapter: NATS JetStream
|
|
433
|
+
// Minimal adapter class that implements QueueAdapter for use with queue: { adapter, options }
|
|
434
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
435
|
+
class NatsJetStreamAdapter implements QueueAdapter {
|
|
436
|
+
readonly name = 'nats-jetstream';
|
|
437
|
+
readonly type = 'jetstream';
|
|
438
|
+
constructor(private opts: { servers: string; stream?: string }) {}
|
|
439
|
+
async connect(): Promise<void> {}
|
|
440
|
+
async disconnect(): Promise<void> {}
|
|
441
|
+
isConnected(): boolean {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
async publish(): Promise<string> {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
async publishBatch(): Promise<string[]> {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
async subscribe(): Promise<import('./types').Subscription> {
|
|
451
|
+
return {
|
|
452
|
+
async unsubscribe() {},
|
|
453
|
+
pause() {},
|
|
454
|
+
resume() {},
|
|
455
|
+
pattern: '',
|
|
456
|
+
isActive: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
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
|
+
supports(): boolean {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
on(): void {}
|
|
470
|
+
off(): void {}
|
|
471
|
+
}
|
|
472
|
+
/* eslint-enable @typescript-eslint/no-empty-function */
|
|
473
|
+
|
|
474
|
+
const adapter = new NatsJetStreamAdapter({
|
|
475
|
+
servers: 'nats://localhost:4222',
|
|
476
|
+
stream: 'EVENTS',
|
|
477
|
+
});
|
|
478
|
+
await adapter.connect();
|
|
479
|
+
expect(adapter.name).toBe('nats-jetstream');
|
|
480
|
+
expect(adapter.type).toBe('jetstream');
|
|
481
|
+
expect(adapter.isConnected()).toBe(true);
|
|
482
|
+
await adapter.disconnect();
|
|
423
483
|
});
|
|
424
484
|
});
|
|
425
485
|
|
package/src/queue/index.ts
CHANGED
|
@@ -113,6 +113,10 @@ export {
|
|
|
113
113
|
createQueueService,
|
|
114
114
|
resolveAdapterType,
|
|
115
115
|
} from './queue.service';
|
|
116
|
+
export {
|
|
117
|
+
QueueServiceProxy,
|
|
118
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
119
|
+
} from './queue-service-proxy';
|
|
116
120
|
|
|
117
121
|
// Adapters (re-export from adapters folder)
|
|
118
122
|
export * from './adapters';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for QueueServiceProxy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
test,
|
|
8
|
+
expect,
|
|
9
|
+
} from 'bun:test';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import { InMemoryQueueAdapter } from './adapters/memory.adapter';
|
|
13
|
+
import { QueueServiceProxy, QUEUE_NOT_ENABLED_ERROR_MESSAGE } from './queue-service-proxy';
|
|
14
|
+
import { QueueService } from './queue.service';
|
|
15
|
+
|
|
16
|
+
describe('QueueServiceProxy', () => {
|
|
17
|
+
test('throws with expected message when delegate is null', async () => {
|
|
18
|
+
const proxy = new QueueServiceProxy();
|
|
19
|
+
expect(() => proxy.getAdapter()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
20
|
+
expect(() => proxy.getScheduler()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
21
|
+
await expect(proxy.publish('e', {})).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
22
|
+
await expect(proxy.publishBatch([{ pattern: 'e', data: {} }])).rejects.toThrow(
|
|
23
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
24
|
+
);
|
|
25
|
+
await expect(
|
|
26
|
+
proxy.subscribe('e', async () => {
|
|
27
|
+
/* no-op for throw test */
|
|
28
|
+
}),
|
|
29
|
+
).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
30
|
+
await expect(proxy.addScheduledJob('j', { pattern: 'e', schedule: { every: 1000 } })).rejects.toThrow(
|
|
31
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
32
|
+
);
|
|
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
|
+
expect(() => proxy.supports('pattern-subscriptions')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
36
|
+
expect(() =>
|
|
37
|
+
proxy.on('onReady', () => {
|
|
38
|
+
/* no-op */
|
|
39
|
+
}),
|
|
40
|
+
).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
41
|
+
expect(() =>
|
|
42
|
+
proxy.off('onReady', () => {
|
|
43
|
+
/* no-op */
|
|
44
|
+
}),
|
|
45
|
+
).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('delegates to real QueueService after setDelegate', async () => {
|
|
49
|
+
const adapter = new InMemoryQueueAdapter();
|
|
50
|
+
const real = new QueueService({ adapter: 'memory' as const });
|
|
51
|
+
await real.initialize(adapter);
|
|
52
|
+
await real.start();
|
|
53
|
+
|
|
54
|
+
const proxy = new QueueServiceProxy();
|
|
55
|
+
proxy.setDelegate(real);
|
|
56
|
+
|
|
57
|
+
expect(proxy.getAdapter()).toBe(adapter);
|
|
58
|
+
expect(proxy.getScheduler()).toBe(real.getScheduler());
|
|
59
|
+
expect(proxy.supports('pattern-subscriptions')).toBe(true);
|
|
60
|
+
|
|
61
|
+
const ids = await proxy.publish('test.event', { x: 1 });
|
|
62
|
+
expect(ids).toBeDefined();
|
|
63
|
+
expect(typeof ids).toBe('string');
|
|
64
|
+
|
|
65
|
+
const received: unknown[] = [];
|
|
66
|
+
const sub = await proxy.subscribe('test.event', async (msg) => {
|
|
67
|
+
received.push(msg.data);
|
|
68
|
+
});
|
|
69
|
+
expect(sub.isActive).toBe(true);
|
|
70
|
+
|
|
71
|
+
await proxy.publish('test.event', { y: 2 });
|
|
72
|
+
// Allow microtask for handler
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
75
|
+
expect(received.length).toBe(1);
|
|
76
|
+
expect(received[0]).toEqual({ y: 2 });
|
|
77
|
+
|
|
78
|
+
await real.stop();
|
|
79
|
+
proxy.setDelegate(null);
|
|
80
|
+
expect(() => proxy.getAdapter()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueServiceProxy — placeholder for DI when the real QueueService is created after setup().
|
|
3
|
+
* When the queue is enabled, the application sets the real QueueService via setDelegate().
|
|
4
|
+
* When the queue is not enabled, any method call throws a clear error.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { QueueService } from './queue.service';
|
|
8
|
+
import type { QueueScheduler } from './scheduler';
|
|
9
|
+
import type { QueueAdapter } from './types';
|
|
10
|
+
import type {
|
|
11
|
+
MessageHandler,
|
|
12
|
+
PublishOptions,
|
|
13
|
+
QueueEvents,
|
|
14
|
+
ScheduledJobInfo,
|
|
15
|
+
ScheduledJobOptions,
|
|
16
|
+
SubscribeOptions,
|
|
17
|
+
Subscription,
|
|
18
|
+
QueueFeature,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
const QUEUE_NOT_ENABLED_MESSAGE =
|
|
22
|
+
'Queue is not enabled. Enable it via `queue.enabled: true` in application options or register at least one controller with queue decorators (@Subscribe, @Cron, @Interval, @Timeout).';
|
|
23
|
+
|
|
24
|
+
function throwIfNoDelegate(delegate: QueueService | null): asserts delegate is QueueService {
|
|
25
|
+
if (delegate === null) {
|
|
26
|
+
throw new Error(QUEUE_NOT_ENABLED_MESSAGE);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Proxy for QueueService used in DI before the real service is created.
|
|
32
|
+
* After initializeQueue(), the application calls setDelegate(realQueueService) when the queue is enabled.
|
|
33
|
+
*/
|
|
34
|
+
export class QueueServiceProxy {
|
|
35
|
+
private delegate: QueueService | null = null;
|
|
36
|
+
|
|
37
|
+
setDelegate(service: QueueService | null): void {
|
|
38
|
+
this.delegate = service;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getAdapter(): QueueAdapter {
|
|
42
|
+
throwIfNoDelegate(this.delegate);
|
|
43
|
+
|
|
44
|
+
return this.delegate.getAdapter();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getScheduler(): QueueScheduler {
|
|
48
|
+
throwIfNoDelegate(this.delegate);
|
|
49
|
+
|
|
50
|
+
return this.delegate.getScheduler();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
|
|
54
|
+
throwIfNoDelegate(this.delegate);
|
|
55
|
+
|
|
56
|
+
return await this.delegate.publish(pattern, data, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async publishBatch<T>(
|
|
60
|
+
messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
|
|
61
|
+
): Promise<string[]> {
|
|
62
|
+
throwIfNoDelegate(this.delegate);
|
|
63
|
+
|
|
64
|
+
return await this.delegate.publishBatch(messages);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async subscribe<T>(
|
|
68
|
+
pattern: string,
|
|
69
|
+
handler: MessageHandler<T>,
|
|
70
|
+
options?: SubscribeOptions,
|
|
71
|
+
): Promise<Subscription> {
|
|
72
|
+
throwIfNoDelegate(this.delegate);
|
|
73
|
+
|
|
74
|
+
return await this.delegate.subscribe(pattern, handler, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
78
|
+
throwIfNoDelegate(this.delegate);
|
|
79
|
+
|
|
80
|
+
return await this.delegate.addScheduledJob(name, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async removeScheduledJob(name: string): Promise<boolean> {
|
|
84
|
+
throwIfNoDelegate(this.delegate);
|
|
85
|
+
|
|
86
|
+
return await this.delegate.removeScheduledJob(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
90
|
+
throwIfNoDelegate(this.delegate);
|
|
91
|
+
|
|
92
|
+
return await this.delegate.getScheduledJobs();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
supports(feature: QueueFeature): boolean {
|
|
96
|
+
throwIfNoDelegate(this.delegate);
|
|
97
|
+
|
|
98
|
+
return this.delegate.supports(feature);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
102
|
+
throwIfNoDelegate(this.delegate);
|
|
103
|
+
|
|
104
|
+
return this.delegate.on(event, handler);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
108
|
+
throwIfNoDelegate(this.delegate);
|
|
109
|
+
|
|
110
|
+
return this.delegate.off(event, handler);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const QUEUE_NOT_ENABLED_ERROR_MESSAGE = QUEUE_NOT_ENABLED_MESSAGE;
|
package/src/queue/types.ts
CHANGED
|
@@ -395,8 +395,8 @@ export interface QueueAdapterConstructor {
|
|
|
395
395
|
* Queue configuration options
|
|
396
396
|
*/
|
|
397
397
|
export interface QueueConfig {
|
|
398
|
-
/** Adapter type or custom adapter class */
|
|
399
|
-
adapter:
|
|
398
|
+
/** Adapter type (built-in or custom, e.g. 'jetstream') or custom adapter class */
|
|
399
|
+
adapter: QueueAdapterType | QueueAdapterConstructor;
|
|
400
400
|
|
|
401
401
|
/** Adapter-specific options */
|
|
402
402
|
options?: Record<string, unknown>;
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { BaseMiddleware } from './module/middleware';
|
|
2
|
+
import type { QueueAdapterConstructor } from './queue/types';
|
|
2
3
|
import type { Type } from 'arktype';
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
Context,
|
|
6
|
+
Effect,
|
|
7
|
+
Layer,
|
|
8
|
+
} from 'effect';
|
|
4
9
|
|
|
5
10
|
import type { Logger, LoggerOptions } from '@onebun/logger';
|
|
6
11
|
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* HTTP Request type used in OneBun controllers.
|
|
9
15
|
* Extends standard Web API Request with:
|
|
@@ -163,11 +169,23 @@ export interface ModuleInstance {
|
|
|
163
169
|
*/
|
|
164
170
|
getModuleMiddleware?(controllerClass: Function): Function[];
|
|
165
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Get the module instance that owns the given controller (the module in whose
|
|
174
|
+
* `controllers` array the controller is declared). Returns this module or a
|
|
175
|
+
* child module, or undefined if the controller is not in this module tree.
|
|
176
|
+
*/
|
|
177
|
+
getOwnerModuleForController?(controllerClass: Function): ModuleInstance | undefined;
|
|
178
|
+
|
|
166
179
|
/**
|
|
167
180
|
* Resolve middleware class constructors into bound `use()` functions
|
|
168
181
|
* using this module's DI scope (services + logger + config).
|
|
169
182
|
*/
|
|
170
183
|
resolveMiddleware?(classes: Function[]): Function[];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register a service instance by tag (e.g. before setup() for application-provided services like QueueService proxy).
|
|
187
|
+
*/
|
|
188
|
+
registerService?<T>(tag: Context.Tag<unknown, T>, instance: T): void;
|
|
171
189
|
}
|
|
172
190
|
|
|
173
191
|
/**
|
|
@@ -415,6 +433,12 @@ export interface ApplicationOptions {
|
|
|
415
433
|
*/
|
|
416
434
|
queue?: QueueApplicationOptions;
|
|
417
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Static file serving: serve files from a directory for requests not matched by API routes.
|
|
438
|
+
* Optional path prefix and fallback file (e.g. index.html) for SPA-style routing.
|
|
439
|
+
*/
|
|
440
|
+
static?: StaticApplicationOptions;
|
|
441
|
+
|
|
418
442
|
/**
|
|
419
443
|
* Documentation configuration (OpenAPI/Swagger)
|
|
420
444
|
*/
|
|
@@ -460,8 +484,10 @@ export type QueueAdapterType = 'memory' | 'redis';
|
|
|
460
484
|
export interface QueueApplicationOptions {
|
|
461
485
|
/** Enable/disable queue (default: auto - enabled if handlers exist) */
|
|
462
486
|
enabled?: boolean;
|
|
463
|
-
/** Adapter type or custom adapter
|
|
464
|
-
adapter?: QueueAdapterType;
|
|
487
|
+
/** Adapter type, or custom adapter constructor (e.g. for NATS JetStream) */
|
|
488
|
+
adapter?: QueueAdapterType | QueueAdapterConstructor;
|
|
489
|
+
/** Options passed to the custom adapter constructor when adapter is a class */
|
|
490
|
+
options?: unknown;
|
|
465
491
|
/** Redis-specific options (only used when adapter is 'redis') */
|
|
466
492
|
redis?: {
|
|
467
493
|
/** Use shared Redis provider instead of dedicated connection */
|
|
@@ -521,6 +547,39 @@ export interface WebSocketApplicationOptions {
|
|
|
521
547
|
maxPayload?: number;
|
|
522
548
|
}
|
|
523
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Static file serving configuration for OneBunApplication.
|
|
552
|
+
* Serves files from a directory for requests not matched by API/ws/docs/metrics routes.
|
|
553
|
+
*/
|
|
554
|
+
export interface StaticApplicationOptions {
|
|
555
|
+
/**
|
|
556
|
+
* Filesystem path to the directory to serve (static root).
|
|
557
|
+
* Can be absolute or relative to the process cwd.
|
|
558
|
+
*/
|
|
559
|
+
root: string;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* URL path prefix under which static files are served.
|
|
563
|
+
* Empty or '/' means "everything not matched by API routes".
|
|
564
|
+
* Example: '/app' — only paths starting with /app are served from static root
|
|
565
|
+
* (the prefix is stripped when resolving the file path).
|
|
566
|
+
*/
|
|
567
|
+
pathPrefix?: string;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Fallback file name (e.g. 'index.html') for SPA-style client-side routing.
|
|
571
|
+
* When the requested file is not found, this file under static root is returned instead.
|
|
572
|
+
*/
|
|
573
|
+
fallbackFile?: string;
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* TTL in milliseconds for caching file existence checks.
|
|
577
|
+
* Reduces disk I/O for repeated requests. Use 0 to disable caching.
|
|
578
|
+
* @defaultValue 60000
|
|
579
|
+
*/
|
|
580
|
+
fileExistenceCacheTtlMs?: number;
|
|
581
|
+
}
|
|
582
|
+
|
|
524
583
|
/**
|
|
525
584
|
* Documentation configuration for OneBunApplication
|
|
526
585
|
* Enables automatic OpenAPI spec generation and Swagger UI
|