@onebun/core 0.2.3 → 0.2.4
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/docs-examples.test.ts +68 -0
- package/src/module/controller.test.ts +111 -3
- package/src/module/controller.ts +71 -2
- package/src/module/middleware.ts +62 -3
- package/src/module/module.test.ts +226 -0
- package/src/module/module.ts +41 -11
- package/src/websocket/ws-base-gateway.test.ts +150 -4
- package/src/websocket/ws-base-gateway.ts +64 -3
package/package.json
CHANGED
|
@@ -693,6 +693,27 @@ describe('Controllers API Documentation Examples', () => {
|
|
|
693
693
|
|
|
694
694
|
expect(UserController).toBeDefined();
|
|
695
695
|
});
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @source docs/api/controllers.md#constructor-access
|
|
699
|
+
* Controllers have this.config and this.logger available immediately after super()
|
|
700
|
+
*/
|
|
701
|
+
it('should have config and logger available in controller constructor after super()', () => {
|
|
702
|
+
@Service()
|
|
703
|
+
class UserService extends BaseService {}
|
|
704
|
+
|
|
705
|
+
// From docs: Controller with constructor access to config and logger
|
|
706
|
+
@Controller('/users')
|
|
707
|
+
class UserController extends BaseController {
|
|
708
|
+
constructor(private userService: UserService) {
|
|
709
|
+
super();
|
|
710
|
+
// config and logger are available immediately after super()
|
|
711
|
+
// e.g. this.config.get('api.prefix'), this.logger.info('...')
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
expect(UserController).toBeDefined();
|
|
716
|
+
});
|
|
696
717
|
});
|
|
697
718
|
|
|
698
719
|
describe('Response Methods (docs/api/controllers.md)', () => {
|
|
@@ -1393,6 +1414,28 @@ describe('Services API Documentation Examples', () => {
|
|
|
1393
1414
|
|
|
1394
1415
|
expect(UserService).toBeDefined();
|
|
1395
1416
|
});
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* @source docs/api/services.md#constructor-access
|
|
1420
|
+
* Services have this.config and this.logger available immediately after super()
|
|
1421
|
+
*/
|
|
1422
|
+
it('should have config and logger available in service constructor after super()', () => {
|
|
1423
|
+
@Service()
|
|
1424
|
+
class ConfiguredService extends BaseService {
|
|
1425
|
+
readonly configAvailable: boolean;
|
|
1426
|
+
readonly loggerAvailable: boolean;
|
|
1427
|
+
|
|
1428
|
+
constructor() {
|
|
1429
|
+
super();
|
|
1430
|
+
// config and logger are available immediately after super()
|
|
1431
|
+
this.configAvailable = this.config !== undefined;
|
|
1432
|
+
this.loggerAvailable = this.logger !== undefined;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Verify the class is well-formed — actual DI test is in module.test.ts
|
|
1437
|
+
expect(ConfiguredService).toBeDefined();
|
|
1438
|
+
});
|
|
1396
1439
|
});
|
|
1397
1440
|
|
|
1398
1441
|
describe('getServiceTag (docs/api/services.md)', () => {
|
|
@@ -4020,6 +4063,31 @@ describe('WebSocket Gateway DI (docs/api/websocket.md#basewebsocketgateway)', ()
|
|
|
4020
4063
|
expect(gateway.getConfigForTest()).toBeDefined();
|
|
4021
4064
|
expect(typeof gateway.getConfigForTest().get).toBe('function');
|
|
4022
4065
|
});
|
|
4066
|
+
|
|
4067
|
+
/**
|
|
4068
|
+
* @source docs/api/websocket.md#constructor-access
|
|
4069
|
+
* Gateways have this.config and this.logger available immediately after super()
|
|
4070
|
+
*/
|
|
4071
|
+
it('should have config and logger available in WS gateway constructor after super()', () => {
|
|
4072
|
+
// From docs: Gateway with constructor access to config and logger
|
|
4073
|
+
@WebSocketGateway({ path: '/ws' })
|
|
4074
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
4075
|
+
readonly configAvailable: boolean;
|
|
4076
|
+
|
|
4077
|
+
constructor() {
|
|
4078
|
+
super();
|
|
4079
|
+
// config and logger are available immediately after super()
|
|
4080
|
+
this.configAvailable = this.config !== undefined;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
@OnMessage('chat:message')
|
|
4084
|
+
handleMessage(@Client() client: WsClientData, @MessageData() data: unknown) {
|
|
4085
|
+
this.broadcast('chat:message', { userId: client.id, data });
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
expect(ChatGateway).toBeDefined();
|
|
4090
|
+
});
|
|
4023
4091
|
});
|
|
4024
4092
|
|
|
4025
4093
|
// ============================================================================
|
|
@@ -229,7 +229,7 @@ describe('Controller', () => {
|
|
|
229
229
|
expect(mockLogger.child).toHaveBeenCalledWith({ className: 'EmptyController' });
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
-
test('should handle re-initialization attempts', () => {
|
|
232
|
+
test('should handle re-initialization attempts (no-op after first init)', () => {
|
|
233
233
|
class TestController extends Controller {
|
|
234
234
|
testMethod() {
|
|
235
235
|
return 'test';
|
|
@@ -242,7 +242,7 @@ describe('Controller', () => {
|
|
|
242
242
|
controller.initializeController(mockLogger, mockConfig);
|
|
243
243
|
expect(mockLogger.debug).toHaveBeenCalledWith('Controller TestController initialized');
|
|
244
244
|
|
|
245
|
-
// Second initialization
|
|
245
|
+
// Second initialization — should be a no-op (already initialized)
|
|
246
246
|
const newMockLogger = {
|
|
247
247
|
...mockLogger,
|
|
248
248
|
child: mock(() => newMockLogger),
|
|
@@ -251,7 +251,11 @@ describe('Controller', () => {
|
|
|
251
251
|
|
|
252
252
|
const newConfig = createMockConfig({ 'newConfig': true });
|
|
253
253
|
controller.initializeController(newMockLogger, newConfig);
|
|
254
|
-
|
|
254
|
+
|
|
255
|
+
// Should NOT have been called — initializeController is a no-op after first init
|
|
256
|
+
expect(newMockLogger.debug).not.toHaveBeenCalled();
|
|
257
|
+
// Should still have original config
|
|
258
|
+
expect((controller as any).config).toBe(mockConfig);
|
|
255
259
|
});
|
|
256
260
|
});
|
|
257
261
|
|
|
@@ -579,4 +583,108 @@ describe('Controller', () => {
|
|
|
579
583
|
expect(text).toBe('');
|
|
580
584
|
});
|
|
581
585
|
});
|
|
586
|
+
|
|
587
|
+
describe('Initialization via static init context (constructor)', () => {
|
|
588
|
+
test('should initialize controller via static init context in constructor', () => {
|
|
589
|
+
class TestController extends Controller {
|
|
590
|
+
configAvailableInConstructor = false;
|
|
591
|
+
loggerAvailableInConstructor = false;
|
|
592
|
+
|
|
593
|
+
constructor() {
|
|
594
|
+
super();
|
|
595
|
+
this.configAvailableInConstructor = this.config !== undefined;
|
|
596
|
+
this.loggerAvailableInConstructor = this.logger !== undefined;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Set init context before construction (as the framework does)
|
|
601
|
+
Controller.setInitContext(mockLogger, mockConfig);
|
|
602
|
+
let controller: TestController;
|
|
603
|
+
try {
|
|
604
|
+
controller = new TestController();
|
|
605
|
+
} finally {
|
|
606
|
+
Controller.clearInitContext();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
expect(controller.configAvailableInConstructor).toBe(true);
|
|
610
|
+
expect(controller.loggerAvailableInConstructor).toBe(true);
|
|
611
|
+
expect((controller as any).config).toBe(mockConfig);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('should allow using config.get() in constructor when init context is set', () => {
|
|
615
|
+
class TestController extends Controller {
|
|
616
|
+
readonly dbHost: string;
|
|
617
|
+
|
|
618
|
+
constructor() {
|
|
619
|
+
super();
|
|
620
|
+
this.dbHost = this.config.get('database.host') as string;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
Controller.setInitContext(mockLogger, mockConfig);
|
|
625
|
+
let controller: TestController;
|
|
626
|
+
try {
|
|
627
|
+
controller = new TestController();
|
|
628
|
+
} finally {
|
|
629
|
+
Controller.clearInitContext();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
expect(controller.dbHost).toBe('localhost');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('should create child logger with correct className in constructor', () => {
|
|
636
|
+
class MyCustomController extends Controller {}
|
|
637
|
+
|
|
638
|
+
Controller.setInitContext(mockLogger, mockConfig);
|
|
639
|
+
try {
|
|
640
|
+
new MyCustomController();
|
|
641
|
+
} finally {
|
|
642
|
+
Controller.clearInitContext();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
expect(mockLogger.child).toHaveBeenCalledWith({ className: 'MyCustomController' });
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('should not initialize if no init context is set', () => {
|
|
649
|
+
// Ensure context is clear
|
|
650
|
+
Controller.clearInitContext();
|
|
651
|
+
|
|
652
|
+
class TestController extends Controller {}
|
|
653
|
+
const controller = new TestController();
|
|
654
|
+
|
|
655
|
+
expect((controller as any)._initialized).toBe(false);
|
|
656
|
+
expect((controller as any).logger).toBeUndefined();
|
|
657
|
+
expect((controller as any).config).toBeUndefined();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('initializeController should be a no-op if already initialized via init context', () => {
|
|
661
|
+
class TestController extends Controller {}
|
|
662
|
+
|
|
663
|
+
Controller.setInitContext(mockLogger, mockConfig);
|
|
664
|
+
let controller: TestController;
|
|
665
|
+
try {
|
|
666
|
+
controller = new TestController();
|
|
667
|
+
} finally {
|
|
668
|
+
Controller.clearInitContext();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Call initializeController again — should be a no-op
|
|
672
|
+
const otherLogger = { ...mockLogger, child: mock(() => ({ ...mockLogger })) };
|
|
673
|
+
const otherConfig = createMockConfig({ other: 'config' });
|
|
674
|
+
controller.initializeController(otherLogger, otherConfig);
|
|
675
|
+
|
|
676
|
+
// Should still have original config (no reinit)
|
|
677
|
+
expect((controller as any).config).toBe(mockConfig);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test('clearInitContext should prevent subsequent constructors from picking up context', () => {
|
|
681
|
+
Controller.setInitContext(mockLogger, mockConfig);
|
|
682
|
+
Controller.clearInitContext();
|
|
683
|
+
|
|
684
|
+
class TestController extends Controller {}
|
|
685
|
+
const controller = new TestController();
|
|
686
|
+
|
|
687
|
+
expect((controller as any)._initialized).toBe(false);
|
|
688
|
+
});
|
|
689
|
+
});
|
|
582
690
|
});
|
package/src/module/controller.ts
CHANGED
|
@@ -31,7 +31,26 @@ export const DEFAULT_SSE_HEARTBEAT_MS = 30_000;
|
|
|
31
31
|
export const DEFAULT_SSE_TIMEOUT = 600;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Base controller class that can be extended to add common functionality
|
|
34
|
+
* Base controller class that can be extended to add common functionality.
|
|
35
|
+
*
|
|
36
|
+
* Controllers extending this class have `this.config` and `this.logger` available
|
|
37
|
+
* immediately after `super()` in the constructor when created through the framework DI.
|
|
38
|
+
* The framework sets an ambient init context before calling the constructor, and
|
|
39
|
+
* the Controller base class reads from it in its constructor.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* @Controller('/users')
|
|
44
|
+
* class UserController extends BaseController {
|
|
45
|
+
* private readonly prefix: string;
|
|
46
|
+
*
|
|
47
|
+
* constructor(private userService: UserService) {
|
|
48
|
+
* super();
|
|
49
|
+
* // this.config and this.logger are available here!
|
|
50
|
+
* this.prefix = this.config.get('api.prefix');
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
35
54
|
*/
|
|
36
55
|
export class Controller {
|
|
37
56
|
// Store service instances
|
|
@@ -42,12 +61,61 @@ export class Controller {
|
|
|
42
61
|
protected logger!: SyncLogger;
|
|
43
62
|
// Configuration instance for accessing environment variables
|
|
44
63
|
protected config!: IConfig<OneBunAppConfig>;
|
|
64
|
+
// Flag to track initialization status
|
|
65
|
+
private _initialized = false;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ambient init context set by the framework before controller construction.
|
|
69
|
+
* This allows the Controller constructor to pick up logger and config
|
|
70
|
+
* so they are available immediately after super() in subclass constructors.
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
private static _initContext: { logger: SyncLogger; config: IConfig<OneBunAppConfig> } | null =
|
|
74
|
+
null;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set the ambient init context before constructing a controller.
|
|
78
|
+
* Called by the framework (OneBunModule) before `new ControllerClass(...)`.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
static setInitContext(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
|
|
82
|
+
Controller._initContext = { logger, config };
|
|
83
|
+
}
|
|
45
84
|
|
|
46
85
|
/**
|
|
47
|
-
*
|
|
86
|
+
* Clear the ambient init context after controller construction.
|
|
87
|
+
* Called by the framework (OneBunModule) after `new ControllerClass(...)`.
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
static clearInitContext(): void {
|
|
91
|
+
Controller._initContext = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
// Pick up logger and config from ambient init context if available.
|
|
96
|
+
// This makes this.config and this.logger available immediately after super()
|
|
97
|
+
// in subclass constructors.
|
|
98
|
+
if (Controller._initContext) {
|
|
99
|
+
const { logger, config } = Controller._initContext;
|
|
100
|
+
const className = this.constructor.name;
|
|
101
|
+
this.logger = logger.child({ className });
|
|
102
|
+
this.config = config;
|
|
103
|
+
this._initialized = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Initialize controller with logger and config (called by the framework).
|
|
109
|
+
* This is a fallback for controllers not constructed through the DI system
|
|
110
|
+
* (e.g., in tests or when created manually). If the controller was already
|
|
111
|
+
* initialized via the constructor init context, this is a no-op.
|
|
48
112
|
* @internal
|
|
49
113
|
*/
|
|
50
114
|
initializeController(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
|
|
115
|
+
if (this._initialized) {
|
|
116
|
+
return; // Already initialized (via constructor or previous call)
|
|
117
|
+
}
|
|
118
|
+
|
|
51
119
|
const className = this.constructor.name;
|
|
52
120
|
|
|
53
121
|
if (logger) {
|
|
@@ -61,6 +129,7 @@ export class Controller {
|
|
|
61
129
|
|
|
62
130
|
// Set configuration instance
|
|
63
131
|
this.config = config;
|
|
132
|
+
this._initialized = true;
|
|
64
133
|
|
|
65
134
|
this.logger.debug(`Controller ${className} initialized`);
|
|
66
135
|
}
|
package/src/module/middleware.ts
CHANGED
|
@@ -11,6 +11,11 @@ import type { SyncLogger } from '@onebun/logger';
|
|
|
11
11
|
* Constructor-based DI is fully supported — inject any service from
|
|
12
12
|
* the module's DI scope just like you would in a controller.
|
|
13
13
|
*
|
|
14
|
+
* Middleware extending this class have `this.config` and `this.logger` available
|
|
15
|
+
* immediately after `super()` in the constructor when created through the framework DI.
|
|
16
|
+
* The framework sets an ambient init context before calling the constructor, and
|
|
17
|
+
* BaseMiddleware reads from it in its constructor.
|
|
18
|
+
*
|
|
14
19
|
* Middleware is instantiated **once** at application startup and reused
|
|
15
20
|
* for every matching request.
|
|
16
21
|
*
|
|
@@ -24,17 +29,20 @@ import type { SyncLogger } from '@onebun/logger';
|
|
|
24
29
|
* }
|
|
25
30
|
* ```
|
|
26
31
|
*
|
|
27
|
-
* @example Middleware with DI
|
|
32
|
+
* @example Middleware with DI and constructor access
|
|
28
33
|
* ```typescript
|
|
29
34
|
* class AuthMiddleware extends BaseMiddleware {
|
|
35
|
+
* private readonly jwtSecret: string;
|
|
36
|
+
*
|
|
30
37
|
* constructor(private authService: AuthService) {
|
|
31
38
|
* super();
|
|
39
|
+
* // this.config and this.logger are available here!
|
|
40
|
+
* this.jwtSecret = this.config.get('auth.jwtSecret');
|
|
32
41
|
* }
|
|
33
42
|
*
|
|
34
43
|
* async use(req: OneBunRequest, next: () => Promise<OneBunResponse>) {
|
|
35
|
-
* const secret = this.config.get('auth.jwtSecret');
|
|
36
44
|
* const token = req.headers.get('Authorization');
|
|
37
|
-
* if (!this.authService.verify(token,
|
|
45
|
+
* if (!this.authService.verify(token, this.jwtSecret)) {
|
|
38
46
|
* this.logger.warn('Authentication failed');
|
|
39
47
|
* return new Response('Unauthorized', { status: 401 });
|
|
40
48
|
* }
|
|
@@ -50,15 +58,66 @@ export abstract class BaseMiddleware {
|
|
|
50
58
|
/** Configuration instance for accessing environment variables */
|
|
51
59
|
protected config!: IConfig<OneBunAppConfig>;
|
|
52
60
|
|
|
61
|
+
/** Flag to track initialization status */
|
|
62
|
+
private _initialized = false;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ambient init context set by the framework before middleware construction.
|
|
66
|
+
* This allows the BaseMiddleware constructor to pick up logger and config
|
|
67
|
+
* so they are available immediately after super() in subclass constructors.
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
private static _initContext: { logger: SyncLogger; config: IConfig<OneBunAppConfig> } | null =
|
|
71
|
+
null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set the ambient init context before constructing a middleware.
|
|
75
|
+
* Called by the framework (OneBunModule) before `new MiddlewareClass(...)`.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
static setInitContext(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
|
|
79
|
+
BaseMiddleware._initContext = { logger, config };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear the ambient init context after middleware construction.
|
|
84
|
+
* Called by the framework (OneBunModule) after `new MiddlewareClass(...)`.
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
static clearInitContext(): void {
|
|
88
|
+
BaseMiddleware._initContext = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
constructor() {
|
|
92
|
+
// Pick up logger and config from ambient init context if available.
|
|
93
|
+
// This makes this.config and this.logger available immediately after super()
|
|
94
|
+
// in subclass constructors.
|
|
95
|
+
if (BaseMiddleware._initContext) {
|
|
96
|
+
const { logger, config } = BaseMiddleware._initContext;
|
|
97
|
+
const className = this.constructor.name;
|
|
98
|
+
this.logger = logger.child({ className });
|
|
99
|
+
this.config = config;
|
|
100
|
+
this._initialized = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
53
104
|
/**
|
|
54
105
|
* Initialize middleware with logger and config.
|
|
106
|
+
* This is a fallback for middleware not constructed through the DI system
|
|
107
|
+
* (e.g., in tests or when created manually). If the middleware was already
|
|
108
|
+
* initialized via the constructor init context, this is a no-op.
|
|
55
109
|
* Called by the framework after DI construction — do NOT call manually.
|
|
56
110
|
* @internal
|
|
57
111
|
*/
|
|
58
112
|
initializeMiddleware(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
|
|
113
|
+
if (this._initialized) {
|
|
114
|
+
return; // Already initialized (via constructor or previous call)
|
|
115
|
+
}
|
|
116
|
+
|
|
59
117
|
const className = this.constructor.name;
|
|
60
118
|
this.logger = logger.child({ className });
|
|
61
119
|
this.config = config;
|
|
120
|
+
this._initialized = true;
|
|
62
121
|
this.logger.debug(`Middleware ${className} initialized`);
|
|
63
122
|
}
|
|
64
123
|
|
|
@@ -26,6 +26,8 @@ import type {
|
|
|
26
26
|
|
|
27
27
|
import { Controller as CtrlDeco, Module } from '../decorators/decorators';
|
|
28
28
|
import { makeMockLoggerLayer } from '../testing/test-utils';
|
|
29
|
+
import { BaseWebSocketGateway } from '../websocket/ws-base-gateway';
|
|
30
|
+
import { WebSocketGateway } from '../websocket/ws-decorators';
|
|
29
31
|
|
|
30
32
|
import { Controller as CtrlBase } from './controller';
|
|
31
33
|
import { BaseMiddleware } from './middleware';
|
|
@@ -963,6 +965,230 @@ describe('OneBunModule', () => {
|
|
|
963
965
|
});
|
|
964
966
|
});
|
|
965
967
|
|
|
968
|
+
describe('Controller with ambient init context (config/logger in constructor)', () => {
|
|
969
|
+
const { Module: ModuleDecorator, Controller: ControllerDecorator, clearGlobalModules } = require('../decorators/decorators');
|
|
970
|
+
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
|
|
971
|
+
const { Controller: BaseController } = require('./controller');
|
|
972
|
+
|
|
973
|
+
beforeEach(() => {
|
|
974
|
+
clearGlobalModules();
|
|
975
|
+
clearRegistry();
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
afterEach(() => {
|
|
979
|
+
clearGlobalModules();
|
|
980
|
+
clearRegistry();
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Test that this.config and this.logger are available in the controller constructor
|
|
985
|
+
* when the controller is created through the DI system (via ambient init context).
|
|
986
|
+
*/
|
|
987
|
+
test('should have this.config and this.logger available in controller constructor via DI', async () => {
|
|
988
|
+
let configInConstructor: unknown = undefined;
|
|
989
|
+
let loggerInConstructor: unknown = undefined;
|
|
990
|
+
|
|
991
|
+
@ControllerDecorator('/test')
|
|
992
|
+
class TestCtrl extends BaseController {
|
|
993
|
+
constructor() {
|
|
994
|
+
super();
|
|
995
|
+
configInConstructor = this.config;
|
|
996
|
+
loggerInConstructor = this.logger;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
@ModuleDecorator({
|
|
1001
|
+
controllers: [TestCtrl],
|
|
1002
|
+
})
|
|
1003
|
+
class TestModule {}
|
|
1004
|
+
|
|
1005
|
+
// Initialize module and run setup — this triggers DI and controller creation
|
|
1006
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
1007
|
+
module.getLayer();
|
|
1008
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
1009
|
+
|
|
1010
|
+
// config and logger should have been available in the constructor
|
|
1011
|
+
expect(configInConstructor).toBeDefined();
|
|
1012
|
+
expect(loggerInConstructor).toBeDefined();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Test that this.config and this.logger are available in the controller constructor
|
|
1017
|
+
* when the controller has injected service dependencies.
|
|
1018
|
+
*/
|
|
1019
|
+
test('should have this.config and this.logger in constructor of controller with dependencies', async () => {
|
|
1020
|
+
let configAvailable = false;
|
|
1021
|
+
let loggerAvailable = false;
|
|
1022
|
+
|
|
1023
|
+
@Service()
|
|
1024
|
+
class SomeService {
|
|
1025
|
+
getValue() {
|
|
1026
|
+
return 42;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
@ControllerDecorator('/test')
|
|
1031
|
+
class TestCtrl extends BaseController {
|
|
1032
|
+
constructor(private svc: SomeService) {
|
|
1033
|
+
super();
|
|
1034
|
+
configAvailable = this.config !== undefined;
|
|
1035
|
+
loggerAvailable = this.logger !== undefined;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
@ModuleDecorator({
|
|
1040
|
+
providers: [SomeService],
|
|
1041
|
+
controllers: [TestCtrl],
|
|
1042
|
+
})
|
|
1043
|
+
class TestModule {}
|
|
1044
|
+
|
|
1045
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
1046
|
+
module.getLayer();
|
|
1047
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
1048
|
+
|
|
1049
|
+
expect(configAvailable).toBe(true);
|
|
1050
|
+
expect(loggerAvailable).toBe(true);
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
describe('WebSocket gateway with ambient init context (config/logger in constructor)', () => {
|
|
1055
|
+
const { Module: ModuleDecorator, clearGlobalModules } = require('../decorators/decorators');
|
|
1056
|
+
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
|
|
1057
|
+
|
|
1058
|
+
beforeEach(() => {
|
|
1059
|
+
clearGlobalModules();
|
|
1060
|
+
clearRegistry();
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
afterEach(() => {
|
|
1064
|
+
clearGlobalModules();
|
|
1065
|
+
clearRegistry();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Test that this.config and this.logger are available in the WS gateway constructor
|
|
1070
|
+
* when the gateway is created through the DI system (via ambient init context).
|
|
1071
|
+
*/
|
|
1072
|
+
test('should have this.config and this.logger available in WS gateway constructor via DI', async () => {
|
|
1073
|
+
let configInConstructor: unknown = undefined;
|
|
1074
|
+
let loggerInConstructor: unknown = undefined;
|
|
1075
|
+
|
|
1076
|
+
@WebSocketGateway({ path: '/ws' })
|
|
1077
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
1078
|
+
constructor() {
|
|
1079
|
+
super();
|
|
1080
|
+
configInConstructor = this.config;
|
|
1081
|
+
loggerInConstructor = this.logger;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
@ModuleDecorator({
|
|
1086
|
+
controllers: [TestGateway],
|
|
1087
|
+
})
|
|
1088
|
+
class TestModule {}
|
|
1089
|
+
|
|
1090
|
+
// Initialize module and run setup — this triggers DI and gateway creation
|
|
1091
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
1092
|
+
module.getLayer();
|
|
1093
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
1094
|
+
|
|
1095
|
+
// config and logger should have been available in the constructor
|
|
1096
|
+
expect(configInConstructor).toBeDefined();
|
|
1097
|
+
expect(loggerInConstructor).toBeDefined();
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Test that this.config and this.logger are available in the WS gateway constructor
|
|
1102
|
+
* when the gateway has injected service dependencies.
|
|
1103
|
+
*/
|
|
1104
|
+
test('should have this.config and this.logger in constructor of WS gateway with dependencies', async () => {
|
|
1105
|
+
let configAvailable = false;
|
|
1106
|
+
let loggerAvailable = false;
|
|
1107
|
+
|
|
1108
|
+
@Service()
|
|
1109
|
+
class WsAuthService {
|
|
1110
|
+
verify() {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
@WebSocketGateway({ path: '/ws' })
|
|
1116
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
1117
|
+
constructor(private auth: WsAuthService) {
|
|
1118
|
+
super();
|
|
1119
|
+
configAvailable = this.config !== undefined;
|
|
1120
|
+
loggerAvailable = this.logger !== undefined;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
@ModuleDecorator({
|
|
1125
|
+
providers: [WsAuthService],
|
|
1126
|
+
controllers: [TestGateway],
|
|
1127
|
+
})
|
|
1128
|
+
class TestModule {}
|
|
1129
|
+
|
|
1130
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
1131
|
+
module.getLayer();
|
|
1132
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
1133
|
+
|
|
1134
|
+
expect(configAvailable).toBe(true);
|
|
1135
|
+
expect(loggerAvailable).toBe(true);
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
describe('Middleware with ambient init context (config/logger in constructor)', () => {
|
|
1140
|
+
const { Module: ModuleDecorator, Controller: ControllerDecorator, clearGlobalModules } = require('../decorators/decorators');
|
|
1141
|
+
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleInstance } = require('./module');
|
|
1142
|
+
const { Controller: BaseController } = require('./controller');
|
|
1143
|
+
|
|
1144
|
+
beforeEach(() => {
|
|
1145
|
+
clearGlobalModules();
|
|
1146
|
+
clearRegistry();
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
afterEach(() => {
|
|
1150
|
+
clearGlobalModules();
|
|
1151
|
+
clearRegistry();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Test that this.config and this.logger are available in the middleware constructor
|
|
1156
|
+
* when the middleware is created through the DI system (via ambient init context).
|
|
1157
|
+
*/
|
|
1158
|
+
test('should have this.config and this.logger available in middleware constructor via DI', () => {
|
|
1159
|
+
let configInConstructor: unknown = undefined;
|
|
1160
|
+
let loggerInConstructor: unknown = undefined;
|
|
1161
|
+
|
|
1162
|
+
class TestMiddleware extends BaseMiddleware {
|
|
1163
|
+
constructor() {
|
|
1164
|
+
super();
|
|
1165
|
+
configInConstructor = this.config;
|
|
1166
|
+
loggerInConstructor = this.logger;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async use(req: OneBunRequest, next: () => Promise<OneBunResponse>): Promise<OneBunResponse> {
|
|
1170
|
+
return await next();
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
@ControllerDecorator('/test')
|
|
1175
|
+
class TestCtrl extends BaseController {}
|
|
1176
|
+
|
|
1177
|
+
@ModuleDecorator({
|
|
1178
|
+
controllers: [TestCtrl],
|
|
1179
|
+
})
|
|
1180
|
+
class TestModule {}
|
|
1181
|
+
|
|
1182
|
+
// Initialize module and resolve middleware (as the framework does)
|
|
1183
|
+
const module = new ModuleInstance(TestModule, mockLoggerLayer);
|
|
1184
|
+
module.resolveMiddleware([TestMiddleware]);
|
|
1185
|
+
|
|
1186
|
+
// config and logger should have been available in the constructor
|
|
1187
|
+
expect(configInConstructor).toBeDefined();
|
|
1188
|
+
expect(loggerInConstructor).toBeDefined();
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
966
1192
|
describe('Lifecycle hooks', () => {
|
|
967
1193
|
const { clearGlobalModules } = require('../decorators/decorators');
|
|
968
1194
|
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
|
package/src/module/module.ts
CHANGED
|
@@ -4,9 +4,8 @@ import {
|
|
|
4
4
|
Layer,
|
|
5
5
|
} from 'effect';
|
|
6
6
|
|
|
7
|
-
import type { Controller } from './controller';
|
|
8
7
|
import type { ModuleInstance } from '../types';
|
|
9
|
-
|
|
8
|
+
|
|
10
9
|
|
|
11
10
|
import {
|
|
12
11
|
createSyncLogger,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
isGlobalModule,
|
|
24
23
|
registerControllerDependencies,
|
|
25
24
|
} from '../decorators/decorators';
|
|
25
|
+
import { BaseWebSocketGateway } from '../websocket/ws-base-gateway';
|
|
26
26
|
import { isWebSocketGateway } from '../websocket/ws-decorators';
|
|
27
27
|
|
|
28
28
|
import {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type IConfig,
|
|
31
31
|
type OneBunAppConfig,
|
|
32
32
|
} from './config.interface';
|
|
33
|
+
import { Controller } from './controller';
|
|
33
34
|
import {
|
|
34
35
|
hasOnModuleInit,
|
|
35
36
|
hasOnApplicationInit,
|
|
@@ -462,7 +463,19 @@ export class OneBunModule implements ModuleInstance {
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
const middlewareConstructor = cls as new (...args: unknown[]) => BaseMiddleware;
|
|
465
|
-
|
|
466
|
+
|
|
467
|
+
// Set ambient init context so BaseMiddleware constructor can pick up logger/config,
|
|
468
|
+
// making them available immediately after super() in subclass constructors.
|
|
469
|
+
BaseMiddleware.setInitContext(this.logger, this.config);
|
|
470
|
+
let instance: BaseMiddleware;
|
|
471
|
+
try {
|
|
472
|
+
instance = new middlewareConstructor(...deps);
|
|
473
|
+
} finally {
|
|
474
|
+
BaseMiddleware.clearInitContext();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Fallback: call initializeMiddleware for middleware not initialized via
|
|
478
|
+
// the constructor (e.g., not extending BaseMiddleware, or for backwards compatibility).
|
|
466
479
|
instance.initializeMiddleware(this.logger, this.config);
|
|
467
480
|
|
|
468
481
|
return instance.use.bind(instance);
|
|
@@ -534,16 +547,33 @@ export class OneBunModule implements ModuleInstance {
|
|
|
534
547
|
}
|
|
535
548
|
}
|
|
536
549
|
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
// Create controller with resolved dependencies
|
|
550
|
+
// Create controller with resolved dependencies.
|
|
551
|
+
// Set ambient init context so the base class constructor can pick up logger/config,
|
|
552
|
+
// making them available immediately after super() in subclass constructors.
|
|
541
553
|
const controllerConstructor = controllerClass as new (...args: unknown[]) => Controller;
|
|
542
|
-
const
|
|
554
|
+
const isGateway = isWebSocketGateway(controllerClass);
|
|
555
|
+
let controller: Controller;
|
|
556
|
+
|
|
557
|
+
if (isGateway) {
|
|
558
|
+
BaseWebSocketGateway.setInitContext(this.logger, this.config);
|
|
559
|
+
} else {
|
|
560
|
+
Controller.setInitContext(this.logger, this.config);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
controller = new controllerConstructor(...dependencies);
|
|
565
|
+
} finally {
|
|
566
|
+
if (isGateway) {
|
|
567
|
+
BaseWebSocketGateway.clearInitContext();
|
|
568
|
+
} else {
|
|
569
|
+
Controller.clearInitContext();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
543
572
|
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
573
|
+
// Fallback: call initializeController / _initializeBase for controllers/gateways
|
|
574
|
+
// that were not initialized via the constructor (e.g., not extending the base class,
|
|
575
|
+
// or for backwards compatibility).
|
|
576
|
+
if (isGateway) {
|
|
547
577
|
const gateway = controller as unknown as BaseWebSocketGateway;
|
|
548
578
|
if (typeof gateway._initializeBase === 'function') {
|
|
549
579
|
gateway._initializeBase(this.logger, this.config);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/* eslint-disable
|
|
2
|
+
@typescript-eslint/no-explicit-any,
|
|
3
|
+
@typescript-eslint/naming-convention,
|
|
4
|
+
@typescript-eslint/no-empty-function */
|
|
1
5
|
/**
|
|
2
6
|
* Unit tests for ws-base-gateway.ts
|
|
3
7
|
*/
|
|
@@ -49,22 +53,22 @@ function createClientData(id: string, rooms: string[] = []): WsClientData {
|
|
|
49
53
|
class TestGateway extends BaseWebSocketGateway {
|
|
50
54
|
// Expose internal methods for testing
|
|
51
55
|
public exposeRegisterSocket(clientId: string, socket: MockWebSocket): void {
|
|
52
|
-
|
|
56
|
+
|
|
53
57
|
(this as any)._registerSocket(clientId, socket);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
public exposeUnregisterSocket(clientId: string): void {
|
|
57
|
-
|
|
61
|
+
|
|
58
62
|
(this as any)._unregisterSocket(clientId);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
public exposeInitialize(storage: InMemoryWsStorage, server: unknown): void {
|
|
62
|
-
|
|
66
|
+
|
|
63
67
|
(this as any)._initialize(storage, server);
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
public exposeGetSocket(clientId: string): MockWebSocket | undefined {
|
|
67
|
-
|
|
71
|
+
|
|
68
72
|
return (this as any).getSocket(clientId);
|
|
69
73
|
}
|
|
70
74
|
}
|
|
@@ -477,4 +481,146 @@ describe('BaseWebSocketGateway', () => {
|
|
|
477
481
|
expect(typeof wsServer?.publish).toBe('function');
|
|
478
482
|
});
|
|
479
483
|
});
|
|
484
|
+
|
|
485
|
+
describe('Initialization via static init context (constructor)', () => {
|
|
486
|
+
let mockLogger: ReturnType<typeof createMockLogger>;
|
|
487
|
+
let mockConfig: ReturnType<typeof createMockConfig>;
|
|
488
|
+
|
|
489
|
+
function createMockLogger() {
|
|
490
|
+
const logger = {
|
|
491
|
+
debug: mock(() => {}),
|
|
492
|
+
info: mock(() => {}),
|
|
493
|
+
warn: mock(() => {}),
|
|
494
|
+
error: mock(() => {}),
|
|
495
|
+
fatal: mock(() => {}),
|
|
496
|
+
trace: mock(() => {}),
|
|
497
|
+
child: mock(() => logger),
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
return logger;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function createMockConfig(data: Record<string, unknown> = {}) {
|
|
504
|
+
return {
|
|
505
|
+
get: mock((key: string) => data[key]),
|
|
506
|
+
getOrThrow: mock((key: string) => {
|
|
507
|
+
if (key in data) {
|
|
508
|
+
return data[key];
|
|
509
|
+
}
|
|
510
|
+
throw new Error(`Missing config: ${key}`);
|
|
511
|
+
}),
|
|
512
|
+
has: mock((key: string) => key in data),
|
|
513
|
+
set: mock(() => {}),
|
|
514
|
+
entries: mock(() => Object.entries(data)),
|
|
515
|
+
} as any;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
beforeEach(() => {
|
|
519
|
+
mockLogger = createMockLogger();
|
|
520
|
+
mockConfig = createMockConfig({
|
|
521
|
+
'ws.port': 8080,
|
|
522
|
+
'ws.path': '/ws',
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should initialize gateway via static init context in constructor', () => {
|
|
527
|
+
class ContextTestGateway extends BaseWebSocketGateway {
|
|
528
|
+
configAvailableInConstructor = false;
|
|
529
|
+
loggerAvailableInConstructor = false;
|
|
530
|
+
|
|
531
|
+
constructor() {
|
|
532
|
+
super();
|
|
533
|
+
this.configAvailableInConstructor = this.config !== undefined;
|
|
534
|
+
this.loggerAvailableInConstructor = this.logger !== undefined;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
BaseWebSocketGateway.setInitContext(mockLogger as any, mockConfig);
|
|
539
|
+
let gw: ContextTestGateway;
|
|
540
|
+
try {
|
|
541
|
+
gw = new ContextTestGateway();
|
|
542
|
+
} finally {
|
|
543
|
+
BaseWebSocketGateway.clearInitContext();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
expect(gw.configAvailableInConstructor).toBe(true);
|
|
547
|
+
expect(gw.loggerAvailableInConstructor).toBe(true);
|
|
548
|
+
expect((gw as any).config).toBe(mockConfig);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should allow using config.get() in constructor when init context is set', () => {
|
|
552
|
+
class ContextTestGateway extends BaseWebSocketGateway {
|
|
553
|
+
readonly wsPort: number;
|
|
554
|
+
|
|
555
|
+
constructor() {
|
|
556
|
+
super();
|
|
557
|
+
this.wsPort = this.config.get('ws.port') as number;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
BaseWebSocketGateway.setInitContext(mockLogger as any, mockConfig);
|
|
562
|
+
let gw: ContextTestGateway;
|
|
563
|
+
try {
|
|
564
|
+
gw = new ContextTestGateway();
|
|
565
|
+
} finally {
|
|
566
|
+
BaseWebSocketGateway.clearInitContext();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
expect(gw.wsPort).toBe(8080);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('should create child logger with correct className in constructor', () => {
|
|
573
|
+
class MyCustomGateway extends BaseWebSocketGateway {}
|
|
574
|
+
|
|
575
|
+
BaseWebSocketGateway.setInitContext(mockLogger as any, mockConfig);
|
|
576
|
+
try {
|
|
577
|
+
new MyCustomGateway();
|
|
578
|
+
} finally {
|
|
579
|
+
BaseWebSocketGateway.clearInitContext();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
expect(mockLogger.child).toHaveBeenCalledWith({ className: 'MyCustomGateway' });
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should not initialize if no init context is set', () => {
|
|
586
|
+
BaseWebSocketGateway.clearInitContext();
|
|
587
|
+
|
|
588
|
+
class ContextTestGateway extends BaseWebSocketGateway {}
|
|
589
|
+
const gw = new ContextTestGateway();
|
|
590
|
+
|
|
591
|
+
expect((gw as any)._initialized).toBe(false);
|
|
592
|
+
expect((gw as any).logger).toBeUndefined();
|
|
593
|
+
expect((gw as any).config).toBeUndefined();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('_initializeBase should be a no-op if already initialized via init context', () => {
|
|
597
|
+
class ContextTestGateway extends BaseWebSocketGateway {}
|
|
598
|
+
|
|
599
|
+
BaseWebSocketGateway.setInitContext(mockLogger as any, mockConfig);
|
|
600
|
+
let gw: ContextTestGateway;
|
|
601
|
+
try {
|
|
602
|
+
gw = new ContextTestGateway();
|
|
603
|
+
} finally {
|
|
604
|
+
BaseWebSocketGateway.clearInitContext();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Call _initializeBase again — should be a no-op
|
|
608
|
+
const otherLogger = createMockLogger();
|
|
609
|
+
const otherConfig = createMockConfig({ other: 'config' });
|
|
610
|
+
(gw as any)._initializeBase(otherLogger, otherConfig);
|
|
611
|
+
|
|
612
|
+
// Should still have original config (no reinit)
|
|
613
|
+
expect((gw as any).config).toBe(mockConfig);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('clearInitContext should prevent subsequent constructors from picking up context', () => {
|
|
617
|
+
BaseWebSocketGateway.setInitContext(mockLogger as any, mockConfig);
|
|
618
|
+
BaseWebSocketGateway.clearInitContext();
|
|
619
|
+
|
|
620
|
+
class ContextTestGateway extends BaseWebSocketGateway {}
|
|
621
|
+
const gw = new ContextTestGateway();
|
|
622
|
+
|
|
623
|
+
expect((gw as any)._initialized).toBe(false);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
480
626
|
});
|
|
@@ -34,12 +34,22 @@ export function _resetClientSocketsForTesting(): void {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Base class for WebSocket gateways
|
|
37
|
+
* Base class for WebSocket gateways.
|
|
38
|
+
*
|
|
39
|
+
* Gateways extending this class have `this.config` and `this.logger` available
|
|
40
|
+
* immediately after `super()` in the constructor when created through the framework DI.
|
|
41
|
+
* The framework sets an ambient init context before calling the constructor, and
|
|
42
|
+
* BaseWebSocketGateway reads from it in its constructor.
|
|
38
43
|
*
|
|
39
44
|
* @example
|
|
40
45
|
* ```typescript
|
|
41
46
|
* @WebSocketGateway({ path: '/ws' })
|
|
42
47
|
* export class ChatGateway extends BaseWebSocketGateway {
|
|
48
|
+
* constructor() {
|
|
49
|
+
* super();
|
|
50
|
+
* // this.config and this.logger are available here!
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
43
53
|
* @OnMessage('chat:message')
|
|
44
54
|
* handleMessage(@Client() client: WsClientData, @MessageData() data: any) {
|
|
45
55
|
* this.broadcast('chat:message', { userId: client.id, ...data });
|
|
@@ -63,19 +73,70 @@ export abstract class BaseWebSocketGateway {
|
|
|
63
73
|
/** Unique instance ID (for multi-instance setups) */
|
|
64
74
|
protected instanceId: string = crypto.randomUUID();
|
|
65
75
|
|
|
76
|
+
/** Flag to track initialization status */
|
|
77
|
+
private _initialized = false;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ambient init context set by the framework before gateway construction.
|
|
81
|
+
* This allows the BaseWebSocketGateway constructor to pick up logger and config
|
|
82
|
+
* so they are available immediately after super() in subclass constructors.
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
private static _initContext: { logger: SyncLogger; config: IConfig<OneBunAppConfig> } | null =
|
|
86
|
+
null;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set the ambient init context before constructing a gateway.
|
|
90
|
+
* Called by the framework (OneBunModule) before `new GatewayClass(...)`.
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
static setInitContext(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
|
|
94
|
+
BaseWebSocketGateway._initContext = { logger, config };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Clear the ambient init context after gateway construction.
|
|
99
|
+
* Called by the framework (OneBunModule) after `new GatewayClass(...)`.
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
static clearInitContext(): void {
|
|
103
|
+
BaseWebSocketGateway._initContext = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
constructor() {
|
|
107
|
+
// Pick up logger and config from ambient init context if available.
|
|
108
|
+
// This makes this.config and this.logger available immediately after super()
|
|
109
|
+
// in subclass constructors.
|
|
110
|
+
if (BaseWebSocketGateway._initContext) {
|
|
111
|
+
const { logger, config } = BaseWebSocketGateway._initContext;
|
|
112
|
+
const className = this.constructor.name;
|
|
113
|
+
this.logger = logger.child({ className });
|
|
114
|
+
this.config = config;
|
|
115
|
+
this._initialized = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
66
119
|
// ============================================================================
|
|
67
120
|
// Initialization
|
|
68
121
|
// ============================================================================
|
|
69
122
|
|
|
70
123
|
/**
|
|
71
|
-
* Initialize the gateway with logger and config
|
|
72
|
-
*
|
|
124
|
+
* Initialize the gateway with logger and config.
|
|
125
|
+
* This is a fallback for gateways not constructed through the DI system
|
|
126
|
+
* (e.g., in tests or when created manually). If the gateway was already
|
|
127
|
+
* initialized via the constructor init context, this is a no-op.
|
|
128
|
+
* Called internally by the framework during DI.
|
|
73
129
|
* @internal
|
|
74
130
|
*/
|
|
75
131
|
_initializeBase(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
|
|
132
|
+
if (this._initialized) {
|
|
133
|
+
return; // Already initialized (via constructor or previous call)
|
|
134
|
+
}
|
|
135
|
+
|
|
76
136
|
const className = this.constructor.name;
|
|
77
137
|
this.logger = logger.child({ className });
|
|
78
138
|
this.config = config;
|
|
139
|
+
this._initialized = true;
|
|
79
140
|
}
|
|
80
141
|
|
|
81
142
|
/**
|