@onebun/core 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/application/application.test.ts +4 -4
- package/src/application/application.ts +66 -4
- package/src/application/multi-service-application.ts +4 -23
- package/src/docs-examples.test.ts +357 -0
- package/src/index.ts +17 -0
- package/src/module/index.ts +1 -0
- package/src/module/lifecycle.ts +197 -0
- package/src/module/module.ts +225 -24
- package/src/module/service.ts +0 -10
- package/src/types.ts +48 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"effect": "^3.13.10",
|
|
43
43
|
"arktype": "^2.0.0",
|
|
44
|
-
"@onebun/logger": "^0.1.
|
|
44
|
+
"@onebun/logger": "^0.1.6",
|
|
45
45
|
"@onebun/envs": "^0.1.4",
|
|
46
46
|
"@onebun/metrics": "^0.1.6",
|
|
47
47
|
"@onebun/requests": "^0.1.3",
|
|
@@ -826,7 +826,7 @@ describe('OneBunApplication', () => {
|
|
|
826
826
|
await expect(app.start()).resolves.toBeUndefined();
|
|
827
827
|
});
|
|
828
828
|
|
|
829
|
-
test('should stop server when running', () => {
|
|
829
|
+
test('should stop server when running', async () => {
|
|
830
830
|
@Module({})
|
|
831
831
|
class TestModule {}
|
|
832
832
|
|
|
@@ -838,20 +838,20 @@ describe('OneBunApplication', () => {
|
|
|
838
838
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
839
839
|
(app as any).server = mockServer;
|
|
840
840
|
|
|
841
|
-
app.stop();
|
|
841
|
+
await app.stop();
|
|
842
842
|
|
|
843
843
|
expect(mockServer.stop).toHaveBeenCalled();
|
|
844
844
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
845
845
|
expect((app as any).server).toBeNull();
|
|
846
846
|
});
|
|
847
847
|
|
|
848
|
-
test('should handle stop when server is not running', () => {
|
|
848
|
+
test('should handle stop when server is not running', async () => {
|
|
849
849
|
@Module({})
|
|
850
850
|
class TestModule {}
|
|
851
851
|
|
|
852
852
|
const app = createTestApp(TestModule);
|
|
853
853
|
|
|
854
|
-
expect(
|
|
854
|
+
await expect(app.stop()).resolves.toBeUndefined();
|
|
855
855
|
});
|
|
856
856
|
});
|
|
857
857
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type Logger,
|
|
14
14
|
LoggerService,
|
|
15
15
|
makeLogger,
|
|
16
|
+
makeLoggerFromOptions,
|
|
16
17
|
type SyncLogger,
|
|
17
18
|
} from '@onebun/logger';
|
|
18
19
|
import {
|
|
@@ -191,8 +192,12 @@ export class OneBunApplication {
|
|
|
191
192
|
this.config = new NotInitializedConfig();
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
// Use provided logger layer or create
|
|
195
|
-
|
|
195
|
+
// Use provided logger layer, or create from options, or use default
|
|
196
|
+
// Priority: loggerLayer > loggerOptions > env variables > NODE_ENV defaults
|
|
197
|
+
const loggerLayer = this.options.loggerLayer
|
|
198
|
+
?? (this.options.loggerOptions
|
|
199
|
+
? makeLoggerFromOptions(this.options.loggerOptions)
|
|
200
|
+
: makeLogger());
|
|
196
201
|
|
|
197
202
|
// Initialize logger with application class name as context
|
|
198
203
|
const effectLogger = Effect.runSync(
|
|
@@ -502,6 +507,12 @@ export class OneBunApplication {
|
|
|
502
507
|
}
|
|
503
508
|
}
|
|
504
509
|
|
|
510
|
+
// Call onApplicationInit lifecycle hook for all services and controllers
|
|
511
|
+
if (this.rootModule.callOnApplicationInit) {
|
|
512
|
+
await this.rootModule.callOnApplicationInit();
|
|
513
|
+
this.logger.debug('Application initialization hooks completed');
|
|
514
|
+
}
|
|
515
|
+
|
|
505
516
|
// Get metrics path
|
|
506
517
|
const metricsPath = this.options.metrics?.path || '/metrics';
|
|
507
518
|
|
|
@@ -1214,11 +1225,18 @@ export class OneBunApplication {
|
|
|
1214
1225
|
* Stop the application with graceful shutdown
|
|
1215
1226
|
* @param options - Shutdown options
|
|
1216
1227
|
*/
|
|
1217
|
-
async stop(options?: { closeSharedRedis?: boolean }): Promise<void> {
|
|
1228
|
+
async stop(options?: { closeSharedRedis?: boolean; signal?: string }): Promise<void> {
|
|
1218
1229
|
const closeRedis = options?.closeSharedRedis ?? true;
|
|
1230
|
+
const signal = options?.signal;
|
|
1219
1231
|
|
|
1220
1232
|
this.logger.info('Stopping OneBun application...');
|
|
1221
1233
|
|
|
1234
|
+
// Call beforeApplicationDestroy lifecycle hook
|
|
1235
|
+
if (this.rootModule.callBeforeApplicationDestroy) {
|
|
1236
|
+
this.logger.debug('Calling beforeApplicationDestroy hooks');
|
|
1237
|
+
await this.rootModule.callBeforeApplicationDestroy(signal);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1222
1240
|
// Cleanup WebSocket resources
|
|
1223
1241
|
if (this.wsHandler) {
|
|
1224
1242
|
this.logger.debug('Cleaning up WebSocket handler');
|
|
@@ -1247,12 +1265,24 @@ export class OneBunApplication {
|
|
|
1247
1265
|
this.logger.debug('HTTP server stopped');
|
|
1248
1266
|
}
|
|
1249
1267
|
|
|
1268
|
+
// Call onModuleDestroy lifecycle hook
|
|
1269
|
+
if (this.rootModule.callOnModuleDestroy) {
|
|
1270
|
+
this.logger.debug('Calling onModuleDestroy hooks');
|
|
1271
|
+
await this.rootModule.callOnModuleDestroy();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1250
1274
|
// Close shared Redis connection if configured and requested
|
|
1251
1275
|
if (closeRedis && SharedRedisProvider.isConnected()) {
|
|
1252
1276
|
this.logger.debug('Disconnecting shared Redis');
|
|
1253
1277
|
await SharedRedisProvider.disconnect();
|
|
1254
1278
|
}
|
|
1255
1279
|
|
|
1280
|
+
// Call onApplicationDestroy lifecycle hook
|
|
1281
|
+
if (this.rootModule.callOnApplicationDestroy) {
|
|
1282
|
+
this.logger.debug('Calling onApplicationDestroy hooks');
|
|
1283
|
+
await this.rootModule.callOnApplicationDestroy(signal);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1256
1286
|
this.logger.info('OneBun application stopped');
|
|
1257
1287
|
}
|
|
1258
1288
|
|
|
@@ -1440,7 +1470,7 @@ export class OneBunApplication {
|
|
|
1440
1470
|
enableGracefulShutdown(): void {
|
|
1441
1471
|
const shutdown = async (signal: string) => {
|
|
1442
1472
|
this.logger.info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1443
|
-
await this.stop();
|
|
1473
|
+
await this.stop({ signal });
|
|
1444
1474
|
process.exit(0);
|
|
1445
1475
|
};
|
|
1446
1476
|
|
|
@@ -1469,4 +1499,36 @@ export class OneBunApplication {
|
|
|
1469
1499
|
getHttpUrl(): string {
|
|
1470
1500
|
return `http://${this.options.host}:${this.options.port}`;
|
|
1471
1501
|
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Get a service instance by class from the module container.
|
|
1505
|
+
* Useful for accessing services outside of the request context.
|
|
1506
|
+
*
|
|
1507
|
+
* @param serviceClass - The service class to get
|
|
1508
|
+
* @returns The service instance
|
|
1509
|
+
* @throws Error if service is not found
|
|
1510
|
+
*
|
|
1511
|
+
* @example
|
|
1512
|
+
* ```typescript
|
|
1513
|
+
* const app = new OneBunApplication(AppModule, options);
|
|
1514
|
+
* await app.start();
|
|
1515
|
+
*
|
|
1516
|
+
* const userService = app.getService(UserService);
|
|
1517
|
+
* await userService.performBackgroundTask();
|
|
1518
|
+
* ```
|
|
1519
|
+
*/
|
|
1520
|
+
getService<T>(serviceClass: new (...args: unknown[]) => T): T {
|
|
1521
|
+
if (!this.rootModule.getServiceByClass) {
|
|
1522
|
+
throw new Error('Module does not support getServiceByClass');
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const service = this.rootModule.getServiceByClass(serviceClass);
|
|
1526
|
+
if (!service) {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
`Service ${serviceClass.name} not found. Make sure it's registered in the module's providers.`,
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
return service;
|
|
1533
|
+
}
|
|
1472
1534
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Logger,
|
|
13
13
|
LoggerService,
|
|
14
14
|
makeLogger,
|
|
15
|
+
parseLogLevel,
|
|
15
16
|
type SyncLogger,
|
|
16
17
|
} from '@onebun/logger';
|
|
17
18
|
|
|
@@ -201,9 +202,9 @@ export class MultiServiceApplication<TServices extends ServicesMap = ServicesMap
|
|
|
201
202
|
...this.options.envOptions,
|
|
202
203
|
valueOverrides: resolvedOverrides,
|
|
203
204
|
},
|
|
204
|
-
// Logger configuration - use
|
|
205
|
-
|
|
206
|
-
?
|
|
205
|
+
// Logger configuration - use loggerOptions if minLevel provided
|
|
206
|
+
loggerOptions: mergedOptions.logger?.minLevel
|
|
207
|
+
? { minLevel: parseLogLevel(mergedOptions.logger.minLevel) }
|
|
207
208
|
: undefined,
|
|
208
209
|
metrics: {
|
|
209
210
|
...mergedOptions.metrics,
|
|
@@ -316,24 +317,4 @@ export class MultiServiceApplication<TServices extends ServicesMap = ServicesMap
|
|
|
316
317
|
getLogger(): SyncLogger {
|
|
317
318
|
return this.logger;
|
|
318
319
|
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Convert log level string to numeric LogLevel value.
|
|
322
|
-
* LogLevel values: Fatal=60, Error=50, Warning=40, Info=30, Debug=20, Trace=10
|
|
323
|
-
*/
|
|
324
|
-
private getLogLevel(level: string): number {
|
|
325
|
-
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
|
326
|
-
const LOG_LEVEL_INFO = 30;
|
|
327
|
-
const levelMap: Record<string, number> = {
|
|
328
|
-
fatal: 60,
|
|
329
|
-
error: 50,
|
|
330
|
-
warning: 40,
|
|
331
|
-
info: LOG_LEVEL_INFO,
|
|
332
|
-
debug: 20,
|
|
333
|
-
trace: 10,
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
return levelMap[level.toLowerCase()] ?? LOG_LEVEL_INFO;
|
|
337
|
-
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
|
338
|
-
}
|
|
339
320
|
}
|
|
@@ -26,6 +26,13 @@ import type {
|
|
|
26
26
|
WsExecutionContext,
|
|
27
27
|
WsServerType,
|
|
28
28
|
} from './';
|
|
29
|
+
import type {
|
|
30
|
+
OnModuleInit,
|
|
31
|
+
OnApplicationInit,
|
|
32
|
+
OnModuleDestroy,
|
|
33
|
+
BeforeApplicationDestroy,
|
|
34
|
+
OnApplicationDestroy,
|
|
35
|
+
} from './';
|
|
29
36
|
import type { SseEvent, SseGenerator } from './types';
|
|
30
37
|
import type { ServerWebSocket } from 'bun';
|
|
31
38
|
|
|
@@ -877,6 +884,345 @@ describe('Services API Documentation Examples', () => {
|
|
|
877
884
|
});
|
|
878
885
|
});
|
|
879
886
|
|
|
887
|
+
describe('Lifecycle Hooks API Documentation Examples (docs/api/services.md)', () => {
|
|
888
|
+
describe('OnModuleInit Interface', () => {
|
|
889
|
+
/**
|
|
890
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
891
|
+
*/
|
|
892
|
+
it('should implement OnModuleInit interface', () => {
|
|
893
|
+
// From docs: OnModuleInit example
|
|
894
|
+
@Service()
|
|
895
|
+
class DatabaseService extends BaseService implements OnModuleInit {
|
|
896
|
+
private connection: unknown = null;
|
|
897
|
+
|
|
898
|
+
async onModuleInit(): Promise<void> {
|
|
899
|
+
// Called after service instantiation and DI
|
|
900
|
+
this.connection = { connected: true };
|
|
901
|
+
this.logger.info('Database connected');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
isConnected(): boolean {
|
|
905
|
+
return this.connection !== null;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
expect(DatabaseService).toBeDefined();
|
|
910
|
+
// Verify method exists
|
|
911
|
+
const service = new DatabaseService();
|
|
912
|
+
expect(typeof service.onModuleInit).toBe('function');
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
describe('OnApplicationInit Interface', () => {
|
|
917
|
+
/**
|
|
918
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
919
|
+
*/
|
|
920
|
+
it('should implement OnApplicationInit interface', () => {
|
|
921
|
+
// From docs: OnApplicationInit example
|
|
922
|
+
@Service()
|
|
923
|
+
class CacheService extends BaseService implements OnApplicationInit {
|
|
924
|
+
async onApplicationInit(): Promise<void> {
|
|
925
|
+
// Called after all modules initialized, before HTTP server starts
|
|
926
|
+
this.logger.info('Warming up cache');
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
expect(CacheService).toBeDefined();
|
|
931
|
+
const service = new CacheService();
|
|
932
|
+
expect(typeof service.onApplicationInit).toBe('function');
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
describe('OnModuleDestroy Interface', () => {
|
|
937
|
+
/**
|
|
938
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
939
|
+
*/
|
|
940
|
+
it('should implement OnModuleDestroy interface', () => {
|
|
941
|
+
// From docs: OnModuleDestroy example
|
|
942
|
+
@Service()
|
|
943
|
+
class ConnectionService extends BaseService implements OnModuleDestroy {
|
|
944
|
+
async onModuleDestroy(): Promise<void> {
|
|
945
|
+
// Called during shutdown, after HTTP server stops
|
|
946
|
+
this.logger.info('Closing connections');
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
expect(ConnectionService).toBeDefined();
|
|
951
|
+
const service = new ConnectionService();
|
|
952
|
+
expect(typeof service.onModuleDestroy).toBe('function');
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
describe('BeforeApplicationDestroy Interface', () => {
|
|
957
|
+
/**
|
|
958
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
959
|
+
*/
|
|
960
|
+
it('should implement BeforeApplicationDestroy interface', () => {
|
|
961
|
+
// From docs: BeforeApplicationDestroy example
|
|
962
|
+
@Service()
|
|
963
|
+
class GracefulService extends BaseService implements BeforeApplicationDestroy {
|
|
964
|
+
beforeApplicationDestroy(signal?: string): void {
|
|
965
|
+
// Called at the very start of shutdown
|
|
966
|
+
this.logger.info(`Shutdown initiated by signal: ${signal || 'unknown'}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
expect(GracefulService).toBeDefined();
|
|
971
|
+
const service = new GracefulService();
|
|
972
|
+
expect(typeof service.beforeApplicationDestroy).toBe('function');
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
describe('OnApplicationDestroy Interface', () => {
|
|
977
|
+
/**
|
|
978
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
979
|
+
*/
|
|
980
|
+
it('should implement OnApplicationDestroy interface', () => {
|
|
981
|
+
// From docs: OnApplicationDestroy example
|
|
982
|
+
@Service()
|
|
983
|
+
class CleanupService extends BaseService implements OnApplicationDestroy {
|
|
984
|
+
async onApplicationDestroy(signal?: string): Promise<void> {
|
|
985
|
+
// Called at the very end of shutdown
|
|
986
|
+
this.logger.info(`Final cleanup, signal: ${signal || 'unknown'}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
expect(CleanupService).toBeDefined();
|
|
991
|
+
const service = new CleanupService();
|
|
992
|
+
expect(typeof service.onApplicationDestroy).toBe('function');
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
describe('Multiple Lifecycle Hooks', () => {
|
|
997
|
+
/**
|
|
998
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
999
|
+
*/
|
|
1000
|
+
it('should implement multiple lifecycle interfaces', () => {
|
|
1001
|
+
// From docs: Complete lifecycle example
|
|
1002
|
+
@Service()
|
|
1003
|
+
class FullLifecycleService extends BaseService
|
|
1004
|
+
implements OnModuleInit, OnApplicationInit, OnModuleDestroy, BeforeApplicationDestroy, OnApplicationDestroy {
|
|
1005
|
+
|
|
1006
|
+
async onModuleInit(): Promise<void> {
|
|
1007
|
+
this.logger.info('Service initialized');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
async onApplicationInit(): Promise<void> {
|
|
1011
|
+
this.logger.info('Application initialized');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
beforeApplicationDestroy(signal?: string): void {
|
|
1015
|
+
this.logger.info(`Shutdown starting: ${signal}`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async onModuleDestroy(): Promise<void> {
|
|
1019
|
+
this.logger.info('Module destroying');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async onApplicationDestroy(signal?: string): Promise<void> {
|
|
1023
|
+
this.logger.info(`Application destroyed: ${signal}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
expect(FullLifecycleService).toBeDefined();
|
|
1028
|
+
const service = new FullLifecycleService();
|
|
1029
|
+
expect(typeof service.onModuleInit).toBe('function');
|
|
1030
|
+
expect(typeof service.onApplicationInit).toBe('function');
|
|
1031
|
+
expect(typeof service.beforeApplicationDestroy).toBe('function');
|
|
1032
|
+
expect(typeof service.onModuleDestroy).toBe('function');
|
|
1033
|
+
expect(typeof service.onApplicationDestroy).toBe('function');
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
describe('Controller Lifecycle Hooks', () => {
|
|
1038
|
+
/**
|
|
1039
|
+
* @source docs/api/controllers.md#lifecycle-hooks
|
|
1040
|
+
*/
|
|
1041
|
+
it('should implement lifecycle hooks in controllers', () => {
|
|
1042
|
+
// From docs: Controller lifecycle hooks example
|
|
1043
|
+
@Controller('/api')
|
|
1044
|
+
class ApiController extends BaseController implements OnModuleInit, OnModuleDestroy {
|
|
1045
|
+
async onModuleInit(): Promise<void> {
|
|
1046
|
+
this.logger.info('Controller initialized');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async onModuleDestroy(): Promise<void> {
|
|
1050
|
+
this.logger.info('Controller destroying');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
@Get('/test')
|
|
1054
|
+
test(): Response {
|
|
1055
|
+
return this.success({ message: 'test' });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
expect(ApiController).toBeDefined();
|
|
1060
|
+
const controller = new ApiController();
|
|
1061
|
+
expect(typeof controller.onModuleInit).toBe('function');
|
|
1062
|
+
expect(typeof controller.onModuleDestroy).toBe('function');
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
describe('Lifecycle Helper Functions', () => {
|
|
1067
|
+
/**
|
|
1068
|
+
* Tests for lifecycle helper functions
|
|
1069
|
+
*/
|
|
1070
|
+
it('should detect hasOnModuleInit correctly', () => {
|
|
1071
|
+
const withHook = { onModuleInit: () => Promise.resolve() };
|
|
1072
|
+
const withoutHook = { someOtherMethod: () => 'nothing' };
|
|
1073
|
+
|
|
1074
|
+
expect(hasOnModuleInit(withHook)).toBe(true);
|
|
1075
|
+
expect(hasOnModuleInit(withoutHook)).toBe(false);
|
|
1076
|
+
expect(hasOnModuleInit(null)).toBe(false);
|
|
1077
|
+
expect(hasOnModuleInit(undefined)).toBe(false);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('should detect hasOnApplicationInit correctly', () => {
|
|
1081
|
+
const withHook = { onApplicationInit: () => Promise.resolve() };
|
|
1082
|
+
const withoutHook = {};
|
|
1083
|
+
|
|
1084
|
+
expect(hasOnApplicationInit(withHook)).toBe(true);
|
|
1085
|
+
expect(hasOnApplicationInit(withoutHook)).toBe(false);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
it('should detect hasOnModuleDestroy correctly', () => {
|
|
1089
|
+
const withHook = { onModuleDestroy: () => Promise.resolve() };
|
|
1090
|
+
const withoutHook = {};
|
|
1091
|
+
|
|
1092
|
+
expect(hasOnModuleDestroy(withHook)).toBe(true);
|
|
1093
|
+
expect(hasOnModuleDestroy(withoutHook)).toBe(false);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('should detect hasBeforeApplicationDestroy correctly', () => {
|
|
1097
|
+
const withHook = { beforeApplicationDestroy: () => undefined };
|
|
1098
|
+
const withoutHook = {};
|
|
1099
|
+
|
|
1100
|
+
expect(hasBeforeApplicationDestroy(withHook)).toBe(true);
|
|
1101
|
+
expect(hasBeforeApplicationDestroy(withoutHook)).toBe(false);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('should detect hasOnApplicationDestroy correctly', () => {
|
|
1105
|
+
const withHook = { onApplicationDestroy: () => Promise.resolve() };
|
|
1106
|
+
const withoutHook = {};
|
|
1107
|
+
|
|
1108
|
+
expect(hasOnApplicationDestroy(withHook)).toBe(true);
|
|
1109
|
+
expect(hasOnApplicationDestroy(withoutHook)).toBe(false);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it('should call lifecycle hooks safely', async () => {
|
|
1113
|
+
const results: string[] = [];
|
|
1114
|
+
|
|
1115
|
+
const service = {
|
|
1116
|
+
async onModuleInit() {
|
|
1117
|
+
results.push('init');
|
|
1118
|
+
},
|
|
1119
|
+
async onApplicationInit() {
|
|
1120
|
+
results.push('appInit');
|
|
1121
|
+
},
|
|
1122
|
+
beforeApplicationDestroy(signal?: string) {
|
|
1123
|
+
results.push(`before:${signal}`);
|
|
1124
|
+
},
|
|
1125
|
+
async onModuleDestroy() {
|
|
1126
|
+
results.push('destroy');
|
|
1127
|
+
},
|
|
1128
|
+
async onApplicationDestroy(signal?: string) {
|
|
1129
|
+
results.push(`appDestroy:${signal}`);
|
|
1130
|
+
},
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
await callOnModuleInit(service);
|
|
1134
|
+
await callOnApplicationInit(service);
|
|
1135
|
+
await callBeforeApplicationDestroy(service, 'SIGTERM');
|
|
1136
|
+
await callOnModuleDestroy(service);
|
|
1137
|
+
await callOnApplicationDestroy(service, 'SIGTERM');
|
|
1138
|
+
|
|
1139
|
+
expect(results).toEqual(['init', 'appInit', 'before:SIGTERM', 'destroy', 'appDestroy:SIGTERM']);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('should not throw when calling hooks on objects without them', async () => {
|
|
1143
|
+
const emptyObj = {};
|
|
1144
|
+
|
|
1145
|
+
// These should not throw
|
|
1146
|
+
await callOnModuleInit(emptyObj);
|
|
1147
|
+
await callOnApplicationInit(emptyObj);
|
|
1148
|
+
await callBeforeApplicationDestroy(emptyObj, 'SIGTERM');
|
|
1149
|
+
await callOnModuleDestroy(emptyObj);
|
|
1150
|
+
await callOnApplicationDestroy(emptyObj, 'SIGTERM');
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
describe('getService API Documentation Examples (docs/api/core.md)', () => {
|
|
1156
|
+
/**
|
|
1157
|
+
* @source docs/api/core.md#accessing-services-outside-of-requests
|
|
1158
|
+
*/
|
|
1159
|
+
it('should have getService method on OneBunApplication', () => {
|
|
1160
|
+
@Module({ controllers: [] })
|
|
1161
|
+
class AppModule {}
|
|
1162
|
+
|
|
1163
|
+
const app = new OneBunApplication(AppModule, {
|
|
1164
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
expect(typeof app.getService).toBe('function');
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* @source docs/api/core.md#accessing-services-outside-of-requests
|
|
1172
|
+
*/
|
|
1173
|
+
it('should get service instance by class', async () => {
|
|
1174
|
+
@Service()
|
|
1175
|
+
class TaskService extends BaseService {
|
|
1176
|
+
performTask(): string {
|
|
1177
|
+
return 'task completed';
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
@Module({
|
|
1182
|
+
providers: [TaskService],
|
|
1183
|
+
controllers: [],
|
|
1184
|
+
})
|
|
1185
|
+
class AppModule {}
|
|
1186
|
+
|
|
1187
|
+
const app = new OneBunApplication(AppModule, {
|
|
1188
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
await app.start();
|
|
1192
|
+
|
|
1193
|
+
// From docs: getService usage example
|
|
1194
|
+
const taskService = app.getService(TaskService);
|
|
1195
|
+
expect(taskService).toBeDefined();
|
|
1196
|
+
expect(taskService.performTask()).toBe('task completed');
|
|
1197
|
+
|
|
1198
|
+
await app.stop();
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* @source docs/api/core.md#accessing-services-outside-of-requests
|
|
1203
|
+
*/
|
|
1204
|
+
it('should throw error for non-existent service', async () => {
|
|
1205
|
+
@Service()
|
|
1206
|
+
class NonExistentService extends BaseService {}
|
|
1207
|
+
|
|
1208
|
+
@Module({
|
|
1209
|
+
controllers: [],
|
|
1210
|
+
})
|
|
1211
|
+
class AppModule {}
|
|
1212
|
+
|
|
1213
|
+
const app = new OneBunApplication(AppModule, {
|
|
1214
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
await app.start();
|
|
1218
|
+
|
|
1219
|
+
// getService throws when service is not found
|
|
1220
|
+
expect(() => app.getService(NonExistentService)).toThrow();
|
|
1221
|
+
|
|
1222
|
+
await app.stop();
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
|
|
880
1226
|
describe('Validation API Documentation Examples', () => {
|
|
881
1227
|
describe('validate function (docs/api/validation.md)', () => {
|
|
882
1228
|
/**
|
|
@@ -2936,8 +3282,19 @@ import {
|
|
|
2936
3282
|
createWsClient,
|
|
2937
3283
|
matchPattern,
|
|
2938
3284
|
makeMockLoggerLayer,
|
|
3285
|
+
hasOnModuleInit,
|
|
3286
|
+
hasOnApplicationInit,
|
|
3287
|
+
hasOnModuleDestroy,
|
|
3288
|
+
hasBeforeApplicationDestroy,
|
|
3289
|
+
hasOnApplicationDestroy,
|
|
3290
|
+
callOnModuleInit,
|
|
3291
|
+
callOnApplicationInit,
|
|
3292
|
+
callOnModuleDestroy,
|
|
3293
|
+
callBeforeApplicationDestroy,
|
|
3294
|
+
callOnApplicationDestroy,
|
|
2939
3295
|
} from './';
|
|
2940
3296
|
|
|
3297
|
+
|
|
2941
3298
|
describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)', () => {
|
|
2942
3299
|
describe('SseEvent Type (docs/api/controllers.md)', () => {
|
|
2943
3300
|
/**
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,23 @@ export {
|
|
|
70
70
|
type IConfig,
|
|
71
71
|
type OneBunAppConfig,
|
|
72
72
|
NotInitializedConfig,
|
|
73
|
+
// Lifecycle hooks interfaces
|
|
74
|
+
type OnModuleInit,
|
|
75
|
+
type OnApplicationInit,
|
|
76
|
+
type OnModuleDestroy,
|
|
77
|
+
type BeforeApplicationDestroy,
|
|
78
|
+
type OnApplicationDestroy,
|
|
79
|
+
// Lifecycle hooks helper functions
|
|
80
|
+
hasOnModuleInit,
|
|
81
|
+
hasOnApplicationInit,
|
|
82
|
+
hasOnModuleDestroy,
|
|
83
|
+
hasBeforeApplicationDestroy,
|
|
84
|
+
hasOnApplicationDestroy,
|
|
85
|
+
callOnModuleInit,
|
|
86
|
+
callOnApplicationInit,
|
|
87
|
+
callOnModuleDestroy,
|
|
88
|
+
callBeforeApplicationDestroy,
|
|
89
|
+
callOnApplicationDestroy,
|
|
73
90
|
} from './module';
|
|
74
91
|
|
|
75
92
|
// Application
|
package/src/module/index.ts
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle hooks interfaces for services and controllers.
|
|
3
|
+
* Implement these interfaces to hook into the application lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { Service, BaseService, OnModuleInit, OnModuleDestroy } from '@onebun/core';
|
|
8
|
+
*
|
|
9
|
+
* @Service()
|
|
10
|
+
* export class DatabaseService extends BaseService implements OnModuleInit, OnModuleDestroy {
|
|
11
|
+
* async onModuleInit() {
|
|
12
|
+
* await this.pool.connect();
|
|
13
|
+
* this.logger.info('Database connected');
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* async onModuleDestroy() {
|
|
17
|
+
* await this.pool.end();
|
|
18
|
+
* this.logger.info('Database disconnected');
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Interface for hook called after module initialization.
|
|
26
|
+
* Called after the service/controller is instantiated and dependencies are injected.
|
|
27
|
+
*/
|
|
28
|
+
export interface OnModuleInit {
|
|
29
|
+
/**
|
|
30
|
+
* Called after the module has been initialized.
|
|
31
|
+
* Can be async - the framework will await completion.
|
|
32
|
+
*/
|
|
33
|
+
onModuleInit(): Promise<void> | void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Interface for hook called after application initialization.
|
|
38
|
+
* Called after all modules are initialized and before the HTTP server starts.
|
|
39
|
+
*/
|
|
40
|
+
export interface OnApplicationInit {
|
|
41
|
+
/**
|
|
42
|
+
* Called after the application has been initialized.
|
|
43
|
+
* Can be async - the framework will await completion.
|
|
44
|
+
*/
|
|
45
|
+
onApplicationInit(): Promise<void> | void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Interface for hook called when module is being destroyed.
|
|
50
|
+
* Called during application shutdown, after HTTP server stops.
|
|
51
|
+
*/
|
|
52
|
+
export interface OnModuleDestroy {
|
|
53
|
+
/**
|
|
54
|
+
* Called when the module is being destroyed.
|
|
55
|
+
* Can be async - the framework will await completion.
|
|
56
|
+
*/
|
|
57
|
+
onModuleDestroy(): Promise<void> | void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Interface for hook called before application shutdown begins.
|
|
62
|
+
* Called at the very start of the shutdown process.
|
|
63
|
+
*/
|
|
64
|
+
export interface BeforeApplicationDestroy {
|
|
65
|
+
/**
|
|
66
|
+
* Called before the application shutdown process begins.
|
|
67
|
+
* Can be async - the framework will await completion.
|
|
68
|
+
* @param signal - The signal that triggered the shutdown (e.g., 'SIGTERM', 'SIGINT')
|
|
69
|
+
*/
|
|
70
|
+
beforeApplicationDestroy(signal?: string): Promise<void> | void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Interface for hook called after application shutdown completes.
|
|
75
|
+
* Called at the very end of the shutdown process.
|
|
76
|
+
*/
|
|
77
|
+
export interface OnApplicationDestroy {
|
|
78
|
+
/**
|
|
79
|
+
* Called after the application shutdown process completes.
|
|
80
|
+
* Can be async - the framework will await completion.
|
|
81
|
+
* @param signal - The signal that triggered the shutdown (e.g., 'SIGTERM', 'SIGINT')
|
|
82
|
+
*/
|
|
83
|
+
onApplicationDestroy(signal?: string): Promise<void> | void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Helper functions for checking if an object implements lifecycle hooks
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if an object has an onModuleInit method
|
|
92
|
+
*/
|
|
93
|
+
export function hasOnModuleInit(obj: unknown): obj is OnModuleInit {
|
|
94
|
+
return (
|
|
95
|
+
typeof obj === 'object' &&
|
|
96
|
+
obj !== null &&
|
|
97
|
+
'onModuleInit' in obj &&
|
|
98
|
+
typeof (obj as OnModuleInit).onModuleInit === 'function'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if an object has an onApplicationInit method
|
|
104
|
+
*/
|
|
105
|
+
export function hasOnApplicationInit(obj: unknown): obj is OnApplicationInit {
|
|
106
|
+
return (
|
|
107
|
+
typeof obj === 'object' &&
|
|
108
|
+
obj !== null &&
|
|
109
|
+
'onApplicationInit' in obj &&
|
|
110
|
+
typeof (obj as OnApplicationInit).onApplicationInit === 'function'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if an object has an onModuleDestroy method
|
|
116
|
+
*/
|
|
117
|
+
export function hasOnModuleDestroy(obj: unknown): obj is OnModuleDestroy {
|
|
118
|
+
return (
|
|
119
|
+
typeof obj === 'object' &&
|
|
120
|
+
obj !== null &&
|
|
121
|
+
'onModuleDestroy' in obj &&
|
|
122
|
+
typeof (obj as OnModuleDestroy).onModuleDestroy === 'function'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if an object has a beforeApplicationDestroy method
|
|
128
|
+
*/
|
|
129
|
+
export function hasBeforeApplicationDestroy(obj: unknown): obj is BeforeApplicationDestroy {
|
|
130
|
+
return (
|
|
131
|
+
typeof obj === 'object' &&
|
|
132
|
+
obj !== null &&
|
|
133
|
+
'beforeApplicationDestroy' in obj &&
|
|
134
|
+
typeof (obj as BeforeApplicationDestroy).beforeApplicationDestroy === 'function'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if an object has an onApplicationDestroy method
|
|
140
|
+
*/
|
|
141
|
+
export function hasOnApplicationDestroy(obj: unknown): obj is OnApplicationDestroy {
|
|
142
|
+
return (
|
|
143
|
+
typeof obj === 'object' &&
|
|
144
|
+
obj !== null &&
|
|
145
|
+
'onApplicationDestroy' in obj &&
|
|
146
|
+
typeof (obj as OnApplicationDestroy).onApplicationDestroy === 'function'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Helper functions to call lifecycle hooks safely
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Call onModuleInit on an object if it implements the hook
|
|
156
|
+
*/
|
|
157
|
+
export async function callOnModuleInit(obj: unknown): Promise<void> {
|
|
158
|
+
if (hasOnModuleInit(obj)) {
|
|
159
|
+
await obj.onModuleInit();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Call onApplicationInit on an object if it implements the hook
|
|
165
|
+
*/
|
|
166
|
+
export async function callOnApplicationInit(obj: unknown): Promise<void> {
|
|
167
|
+
if (hasOnApplicationInit(obj)) {
|
|
168
|
+
await obj.onApplicationInit();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Call onModuleDestroy on an object if it implements the hook
|
|
174
|
+
*/
|
|
175
|
+
export async function callOnModuleDestroy(obj: unknown): Promise<void> {
|
|
176
|
+
if (hasOnModuleDestroy(obj)) {
|
|
177
|
+
await obj.onModuleDestroy();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Call beforeApplicationDestroy on an object if it implements the hook
|
|
183
|
+
*/
|
|
184
|
+
export async function callBeforeApplicationDestroy(obj: unknown, signal?: string): Promise<void> {
|
|
185
|
+
if (hasBeforeApplicationDestroy(obj)) {
|
|
186
|
+
await obj.beforeApplicationDestroy(signal);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Call onApplicationDestroy on an object if it implements the hook
|
|
192
|
+
*/
|
|
193
|
+
export async function callOnApplicationDestroy(obj: unknown, signal?: string): Promise<void> {
|
|
194
|
+
if (hasOnApplicationDestroy(obj)) {
|
|
195
|
+
await obj.onApplicationDestroy(signal);
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/module/module.ts
CHANGED
|
@@ -29,6 +29,13 @@ import {
|
|
|
29
29
|
type IConfig,
|
|
30
30
|
type OneBunAppConfig,
|
|
31
31
|
} from './config.interface';
|
|
32
|
+
import {
|
|
33
|
+
hasOnModuleInit,
|
|
34
|
+
hasOnApplicationInit,
|
|
35
|
+
hasOnModuleDestroy,
|
|
36
|
+
hasBeforeApplicationDestroy,
|
|
37
|
+
hasOnApplicationDestroy,
|
|
38
|
+
} from './lifecycle';
|
|
32
39
|
import { getServiceMetadata, getServiceTag } from './service';
|
|
33
40
|
|
|
34
41
|
/**
|
|
@@ -68,7 +75,7 @@ export class OneBunModule implements ModuleInstance {
|
|
|
68
75
|
private controllers: Function[] = [];
|
|
69
76
|
private controllerInstances: Map<Function, Controller> = new Map();
|
|
70
77
|
private serviceInstances: Map<Context.Tag<unknown, unknown>, unknown> = new Map();
|
|
71
|
-
private
|
|
78
|
+
private pendingServiceInits: Array<{ name: string; instance: unknown }> = [];
|
|
72
79
|
private logger: SyncLogger;
|
|
73
80
|
private config: IConfig<OneBunAppConfig>;
|
|
74
81
|
|
|
@@ -303,16 +310,11 @@ export class OneBunModule implements ModuleInstance {
|
|
|
303
310
|
.initializeService(this.logger, this.config);
|
|
304
311
|
}
|
|
305
312
|
|
|
306
|
-
// Track services that need
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
typeof serviceInstance === 'object' &&
|
|
310
|
-
'onAsyncInit' in serviceInstance &&
|
|
311
|
-
typeof (serviceInstance as { onAsyncInit: unknown }).onAsyncInit === 'function'
|
|
312
|
-
) {
|
|
313
|
-
this.pendingAsyncInits.push({
|
|
313
|
+
// Track services that need lifecycle hooks (onModuleInit)
|
|
314
|
+
if (hasOnModuleInit(serviceInstance)) {
|
|
315
|
+
this.pendingServiceInits.push({
|
|
314
316
|
name: provider.name,
|
|
315
|
-
|
|
317
|
+
instance: serviceInstance,
|
|
316
318
|
});
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -558,35 +560,45 @@ export class OneBunModule implements ModuleInstance {
|
|
|
558
560
|
* Setup the module and its dependencies
|
|
559
561
|
*/
|
|
560
562
|
setup(): Effect.Effect<unknown, never, void> {
|
|
561
|
-
return this.
|
|
562
|
-
// Also run
|
|
563
|
+
return this.callServicesOnModuleInit().pipe(
|
|
564
|
+
// Also run onModuleInit for child modules' services
|
|
563
565
|
Effect.flatMap(() =>
|
|
564
|
-
Effect.forEach(this.childModules, (childModule) => childModule.
|
|
566
|
+
Effect.forEach(this.childModules, (childModule) => childModule.callServicesOnModuleInit(), {
|
|
565
567
|
discard: true,
|
|
566
568
|
}),
|
|
567
569
|
),
|
|
568
570
|
// Then create controller instances
|
|
569
571
|
Effect.flatMap(() => this.createControllerInstances()),
|
|
572
|
+
// Then call onModuleInit for controllers
|
|
573
|
+
Effect.flatMap(() => this.callControllersOnModuleInit()),
|
|
574
|
+
// Also run onModuleInit for child modules' controllers
|
|
575
|
+
Effect.flatMap(() =>
|
|
576
|
+
Effect.forEach(this.childModules, (childModule) => childModule.callControllersOnModuleInit(), {
|
|
577
|
+
discard: true,
|
|
578
|
+
}),
|
|
579
|
+
),
|
|
570
580
|
);
|
|
571
581
|
}
|
|
572
582
|
|
|
573
583
|
/**
|
|
574
|
-
*
|
|
584
|
+
* Call onModuleInit lifecycle hook for all services that implement it
|
|
575
585
|
*/
|
|
576
|
-
|
|
577
|
-
if (this.
|
|
586
|
+
callServicesOnModuleInit(): Effect.Effect<unknown, never, void> {
|
|
587
|
+
if (this.pendingServiceInits.length === 0) {
|
|
578
588
|
return Effect.void;
|
|
579
589
|
}
|
|
580
590
|
|
|
581
|
-
this.logger.debug(`
|
|
591
|
+
this.logger.debug(`Calling onModuleInit for ${this.pendingServiceInits.length} service(s)`);
|
|
582
592
|
|
|
583
|
-
// Run all
|
|
584
|
-
const initPromises = this.
|
|
593
|
+
// Run all service onModuleInit hooks sequentially
|
|
594
|
+
const initPromises = this.pendingServiceInits.map(async ({ name, instance }) => {
|
|
585
595
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
596
|
+
if (hasOnModuleInit(instance)) {
|
|
597
|
+
await instance.onModuleInit();
|
|
598
|
+
}
|
|
599
|
+
this.logger.debug(`Service ${name} onModuleInit completed`);
|
|
588
600
|
} catch (error) {
|
|
589
|
-
this.logger.error(`Service ${name}
|
|
601
|
+
this.logger.error(`Service ${name} onModuleInit failed: ${error}`);
|
|
590
602
|
throw error;
|
|
591
603
|
}
|
|
592
604
|
});
|
|
@@ -594,11 +606,179 @@ export class OneBunModule implements ModuleInstance {
|
|
|
594
606
|
return Effect.promise(() => Promise.all(initPromises)).pipe(
|
|
595
607
|
Effect.map(() => {
|
|
596
608
|
// Clear the list after initialization
|
|
597
|
-
this.
|
|
609
|
+
this.pendingServiceInits = [];
|
|
598
610
|
}),
|
|
599
611
|
);
|
|
600
612
|
}
|
|
601
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Call onModuleInit lifecycle hook for all controllers that implement it
|
|
616
|
+
*/
|
|
617
|
+
callControllersOnModuleInit(): Effect.Effect<unknown, never, void> {
|
|
618
|
+
const controllers = Array.from(this.controllerInstances.values());
|
|
619
|
+
const controllersWithInit = controllers.filter((c): c is Controller & { onModuleInit(): Promise<void> | void } =>
|
|
620
|
+
hasOnModuleInit(c),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
if (controllersWithInit.length === 0) {
|
|
624
|
+
return Effect.void;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
this.logger.debug(`Calling onModuleInit for ${controllersWithInit.length} controller(s)`);
|
|
628
|
+
|
|
629
|
+
const initPromises = controllersWithInit.map(async (controller) => {
|
|
630
|
+
try {
|
|
631
|
+
await controller.onModuleInit();
|
|
632
|
+
this.logger.debug(`Controller ${controller.constructor.name} onModuleInit completed`);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
this.logger.error(`Controller ${controller.constructor.name} onModuleInit failed: ${error}`);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
return Effect.promise(() => Promise.all(initPromises));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Call onApplicationInit lifecycle hook for all services and controllers
|
|
644
|
+
*/
|
|
645
|
+
async callOnApplicationInit(): Promise<void> {
|
|
646
|
+
// Call for services
|
|
647
|
+
for (const [, instance] of this.serviceInstances) {
|
|
648
|
+
if (hasOnApplicationInit(instance)) {
|
|
649
|
+
try {
|
|
650
|
+
await instance.onApplicationInit();
|
|
651
|
+
this.logger.debug(`Service ${(instance as object).constructor.name} onApplicationInit completed`);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
this.logger.error(`Service ${(instance as object).constructor.name} onApplicationInit failed: ${error}`);
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Call for controllers
|
|
660
|
+
for (const [, controller] of this.controllerInstances) {
|
|
661
|
+
if (hasOnApplicationInit(controller)) {
|
|
662
|
+
try {
|
|
663
|
+
await controller.onApplicationInit();
|
|
664
|
+
this.logger.debug(`Controller ${controller.constructor.name} onApplicationInit completed`);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
this.logger.error(`Controller ${controller.constructor.name} onApplicationInit failed: ${error}`);
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Call for child modules
|
|
673
|
+
for (const childModule of this.childModules) {
|
|
674
|
+
await childModule.callOnApplicationInit();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Call beforeApplicationDestroy lifecycle hook for all services and controllers
|
|
680
|
+
*/
|
|
681
|
+
async callBeforeApplicationDestroy(signal?: string): Promise<void> {
|
|
682
|
+
// Call for services
|
|
683
|
+
for (const [, instance] of this.serviceInstances) {
|
|
684
|
+
if (hasBeforeApplicationDestroy(instance)) {
|
|
685
|
+
try {
|
|
686
|
+
await instance.beforeApplicationDestroy(signal);
|
|
687
|
+
this.logger.debug(`Service ${(instance as object).constructor.name} beforeApplicationDestroy completed`);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
this.logger.error(`Service ${(instance as object).constructor.name} beforeApplicationDestroy failed: ${error}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Call for controllers
|
|
695
|
+
for (const [, controller] of this.controllerInstances) {
|
|
696
|
+
if (hasBeforeApplicationDestroy(controller)) {
|
|
697
|
+
try {
|
|
698
|
+
await controller.beforeApplicationDestroy(signal);
|
|
699
|
+
this.logger.debug(`Controller ${controller.constructor.name} beforeApplicationDestroy completed`);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
this.logger.error(`Controller ${controller.constructor.name} beforeApplicationDestroy failed: ${error}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Call for child modules
|
|
707
|
+
for (const childModule of this.childModules) {
|
|
708
|
+
await childModule.callBeforeApplicationDestroy(signal);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Call onModuleDestroy lifecycle hook for controllers first, then services
|
|
714
|
+
*/
|
|
715
|
+
async callOnModuleDestroy(): Promise<void> {
|
|
716
|
+
// Call for controllers first (reverse order of creation)
|
|
717
|
+
const controllers = Array.from(this.controllerInstances.values()).reverse();
|
|
718
|
+
for (const controller of controllers) {
|
|
719
|
+
if (hasOnModuleDestroy(controller)) {
|
|
720
|
+
try {
|
|
721
|
+
await controller.onModuleDestroy();
|
|
722
|
+
this.logger.debug(`Controller ${controller.constructor.name} onModuleDestroy completed`);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
this.logger.error(`Controller ${controller.constructor.name} onModuleDestroy failed: ${error}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Call for services (reverse order of creation)
|
|
730
|
+
const services = Array.from(this.serviceInstances.values()).reverse();
|
|
731
|
+
for (const instance of services) {
|
|
732
|
+
if (hasOnModuleDestroy(instance)) {
|
|
733
|
+
try {
|
|
734
|
+
await instance.onModuleDestroy();
|
|
735
|
+
this.logger.debug(`Service ${(instance as object).constructor.name} onModuleDestroy completed`);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
this.logger.error(`Service ${(instance as object).constructor.name} onModuleDestroy failed: ${error}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Call for child modules
|
|
743
|
+
for (const childModule of this.childModules) {
|
|
744
|
+
await childModule.callOnModuleDestroy();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Call onApplicationDestroy lifecycle hook for all services and controllers
|
|
750
|
+
*/
|
|
751
|
+
async callOnApplicationDestroy(signal?: string): Promise<void> {
|
|
752
|
+
// Call for services
|
|
753
|
+
for (const [, instance] of this.serviceInstances) {
|
|
754
|
+
if (hasOnApplicationDestroy(instance)) {
|
|
755
|
+
try {
|
|
756
|
+
await instance.onApplicationDestroy(signal);
|
|
757
|
+
this.logger.debug(`Service ${(instance as object).constructor.name} onApplicationDestroy completed`);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
this.logger.error(`Service ${(instance as object).constructor.name} onApplicationDestroy failed: ${error}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Call for controllers
|
|
765
|
+
for (const [, controller] of this.controllerInstances) {
|
|
766
|
+
if (hasOnApplicationDestroy(controller)) {
|
|
767
|
+
try {
|
|
768
|
+
await controller.onApplicationDestroy(signal);
|
|
769
|
+
this.logger.debug(`Controller ${controller.constructor.name} onApplicationDestroy completed`);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
this.logger.error(`Controller ${controller.constructor.name} onApplicationDestroy failed: ${error}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Call for child modules
|
|
777
|
+
for (const childModule of this.childModules) {
|
|
778
|
+
await childModule.callOnApplicationDestroy(signal);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
602
782
|
/**
|
|
603
783
|
* Get all controllers from this module
|
|
604
784
|
*/
|
|
@@ -627,12 +807,33 @@ export class OneBunModule implements ModuleInstance {
|
|
|
627
807
|
}
|
|
628
808
|
|
|
629
809
|
/**
|
|
630
|
-
* Get service instance
|
|
810
|
+
* Get service instance by tag
|
|
631
811
|
*/
|
|
632
812
|
getServiceInstance<T>(tag: Context.Tag<T, T>): T | undefined {
|
|
633
813
|
return this.serviceInstances.get(tag as Context.Tag<unknown, unknown>) as T | undefined;
|
|
634
814
|
}
|
|
635
815
|
|
|
816
|
+
/**
|
|
817
|
+
* Get service instance by class
|
|
818
|
+
*/
|
|
819
|
+
getServiceByClass<T>(serviceClass: new (...args: unknown[]) => T): T | undefined {
|
|
820
|
+
try {
|
|
821
|
+
const tag = getServiceTag(serviceClass);
|
|
822
|
+
|
|
823
|
+
return this.getServiceInstance(tag);
|
|
824
|
+
} catch {
|
|
825
|
+
// Service doesn't have @Service decorator or not found
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Get all service instances
|
|
832
|
+
*/
|
|
833
|
+
getAllServiceInstances(): Map<Context.Tag<unknown, unknown>, unknown> {
|
|
834
|
+
return new Map(this.serviceInstances);
|
|
835
|
+
}
|
|
836
|
+
|
|
636
837
|
/**
|
|
637
838
|
* Get the Layer for this module
|
|
638
839
|
*/
|
package/src/module/service.ts
CHANGED
|
@@ -99,16 +99,6 @@ export class BaseService {
|
|
|
99
99
|
this.logger.debug(`Service ${className} initialized`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
* Async initialization hook - called by the framework after initializeService()
|
|
104
|
-
* Override in subclasses that need async initialization (e.g., database connections)
|
|
105
|
-
* The framework will await this method before making the service available
|
|
106
|
-
* @internal
|
|
107
|
-
*/
|
|
108
|
-
async onAsyncInit(): Promise<void> {
|
|
109
|
-
// Default: no async init needed
|
|
110
|
-
}
|
|
111
|
-
|
|
112
102
|
/**
|
|
113
103
|
* Check if service is initialized
|
|
114
104
|
* @internal
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Type } from 'arktype';
|
|
2
2
|
import type { Effect, Layer } from 'effect';
|
|
3
3
|
|
|
4
|
-
import type { Logger } from '@onebun/logger';
|
|
4
|
+
import type { Logger, LoggerOptions } from '@onebun/logger';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Base interface for all OneBun services
|
|
@@ -58,6 +58,31 @@ export interface ModuleInstance {
|
|
|
58
58
|
* Get controller instance
|
|
59
59
|
*/
|
|
60
60
|
getControllerInstance?(controllerClass: Function): unknown;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Call onApplicationInit lifecycle hook for all services and controllers
|
|
64
|
+
*/
|
|
65
|
+
callOnApplicationInit?(): Promise<void>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Call beforeApplicationDestroy lifecycle hook for all services and controllers
|
|
69
|
+
*/
|
|
70
|
+
callBeforeApplicationDestroy?(signal?: string): Promise<void>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Call onModuleDestroy lifecycle hook for controllers first, then services
|
|
74
|
+
*/
|
|
75
|
+
callOnModuleDestroy?(): Promise<void>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Call onApplicationDestroy lifecycle hook for all services and controllers
|
|
79
|
+
*/
|
|
80
|
+
callOnApplicationDestroy?(signal?: string): Promise<void>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get service instance by class
|
|
84
|
+
*/
|
|
85
|
+
getServiceByClass?<T>(serviceClass: new (...args: unknown[]) => T): T | undefined;
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
/**
|
|
@@ -107,8 +132,28 @@ export interface ApplicationOptions {
|
|
|
107
132
|
development?: boolean;
|
|
108
133
|
|
|
109
134
|
/**
|
|
110
|
-
* Logger
|
|
111
|
-
*
|
|
135
|
+
* Logger configuration options.
|
|
136
|
+
* Provides a declarative way to configure logging.
|
|
137
|
+
*
|
|
138
|
+
* Priority: loggerLayer > loggerOptions > LOG_LEVEL/LOG_FORMAT env > NODE_ENV defaults
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const app = new OneBunApplication(AppModule, {
|
|
143
|
+
* loggerOptions: {
|
|
144
|
+
* minLevel: 'info',
|
|
145
|
+
* format: 'json',
|
|
146
|
+
* defaultContext: { service: 'user-service' },
|
|
147
|
+
* },
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
loggerOptions?: LoggerOptions;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Logger layer to use (advanced).
|
|
155
|
+
* If provided, takes precedence over loggerOptions.
|
|
156
|
+
* Use loggerOptions for simpler configuration.
|
|
112
157
|
*/
|
|
113
158
|
loggerLayer?: Layer.Layer<Logger>;
|
|
114
159
|
|