@onebun/core 0.1.18 → 0.1.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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.5",
44
+ "@onebun/logger": "^0.1.7",
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(() => app.stop()).not.toThrow();
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 a default one
195
- const loggerLayer = this.options.loggerLayer || makeLogger();
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 minLevel if provided
205
- loggerLayer: mergedOptions.logger?.minLevel
206
- ? makeLogger({ minLevel: this.getLogLevel(mergedOptions.logger.minLevel) })
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
  /**
@@ -1981,6 +2327,49 @@ describe('Architecture Documentation (docs/architecture.md)', () => {
1981
2327
  expect(SharedModule).toBeDefined();
1982
2328
  expect(ApiModule).toBeDefined();
1983
2329
  });
2330
+
2331
+ /**
2332
+ * Exports are only required for cross-module injection.
2333
+ * Within a module, any provider can be injected into controllers without being in exports.
2334
+ */
2335
+ it('should allow controller to inject same-module provider without exports', async () => {
2336
+ const effectLib = await import('effect');
2337
+ const moduleMod = await import('./module/module');
2338
+ const testUtils = await import('./testing/test-utils');
2339
+ const decorators = await import('./decorators/decorators');
2340
+
2341
+ @Service()
2342
+ class InternalService extends BaseService {
2343
+ getData(): string {
2344
+ return 'internal';
2345
+ }
2346
+ }
2347
+
2348
+ @Controller('/local')
2349
+ class LocalController extends BaseController {
2350
+ constructor(@decorators.Inject(InternalService) private readonly internal: InternalService) {
2351
+ super();
2352
+ }
2353
+ getData(): string {
2354
+ return this.internal.getData();
2355
+ }
2356
+ }
2357
+
2358
+ @Module({
2359
+ providers: [InternalService],
2360
+ controllers: [LocalController],
2361
+ // No exports - InternalService is only used inside this module
2362
+ })
2363
+ class LocalModule {}
2364
+
2365
+ const mod = new moduleMod.OneBunModule(LocalModule, testUtils.makeMockLoggerLayer());
2366
+ mod.getLayer();
2367
+ await effectLib.Effect.runPromise(mod.setup() as import('effect').Effect.Effect<unknown, never, never>);
2368
+
2369
+ const controller = mod.getControllerInstance(LocalController) as LocalController;
2370
+ expect(controller).toBeDefined();
2371
+ expect(controller.getData()).toBe('internal');
2372
+ });
1984
2373
  });
1985
2374
  });
1986
2375
 
@@ -2936,8 +3325,19 @@ import {
2936
3325
  createWsClient,
2937
3326
  matchPattern,
2938
3327
  makeMockLoggerLayer,
3328
+ hasOnModuleInit,
3329
+ hasOnApplicationInit,
3330
+ hasOnModuleDestroy,
3331
+ hasBeforeApplicationDestroy,
3332
+ hasOnApplicationDestroy,
3333
+ callOnModuleInit,
3334
+ callOnApplicationInit,
3335
+ callOnModuleDestroy,
3336
+ callBeforeApplicationDestroy,
3337
+ callOnApplicationDestroy,
2939
3338
  } from './';
2940
3339
 
3340
+
2941
3341
  describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)', () => {
2942
3342
  describe('SseEvent Type (docs/api/controllers.md)', () => {
2943
3343
  /**
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
@@ -11,3 +11,4 @@ export { Controller as BaseController } from './controller';
11
11
  export * from './service';
12
12
  export * from './config.service';
13
13
  export * from './config.interface';
14
+ export * from './lifecycle';