@onebun/core 0.1.17 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.1.17",
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.5",
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",
@@ -432,6 +432,236 @@ describe('OneBunApplication', () => {
432
432
  expect(prodApp).toBeInstanceOf(OneBunApplication);
433
433
  });
434
434
 
435
+ test('should use PORT env variable when port is not explicitly provided', () => {
436
+ const originalPort = process.env.PORT;
437
+ const originalHost = process.env.HOST;
438
+ try {
439
+ process.env.PORT = '4567';
440
+ delete process.env.HOST; // Clear HOST to get default
441
+
442
+ @Module({})
443
+ class TestModule {}
444
+
445
+ const app = createTestApp(TestModule);
446
+ expect(app.getHttpUrl()).toBe('http://0.0.0.0:4567');
447
+ } finally {
448
+ if (originalPort === undefined) {
449
+ delete process.env.PORT;
450
+ } else {
451
+ process.env.PORT = originalPort;
452
+ }
453
+ if (originalHost === undefined) {
454
+ delete process.env.HOST;
455
+ } else {
456
+ process.env.HOST = originalHost;
457
+ }
458
+ }
459
+ });
460
+
461
+ test('should use HOST env variable when host is not explicitly provided', () => {
462
+ const originalHost = process.env.HOST;
463
+ const originalPort = process.env.PORT;
464
+ try {
465
+ process.env.HOST = '192.168.1.100';
466
+ delete process.env.PORT; // Clear PORT to get default
467
+
468
+ @Module({})
469
+ class TestModule {}
470
+
471
+ const app = createTestApp(TestModule);
472
+ expect(app.getHttpUrl()).toBe('http://192.168.1.100:3000');
473
+ } finally {
474
+ if (originalHost === undefined) {
475
+ delete process.env.HOST;
476
+ } else {
477
+ process.env.HOST = originalHost;
478
+ }
479
+ if (originalPort === undefined) {
480
+ delete process.env.PORT;
481
+ } else {
482
+ process.env.PORT = originalPort;
483
+ }
484
+ }
485
+ });
486
+
487
+ test('should use both PORT and HOST env variables when not explicitly provided', () => {
488
+ const originalPort = process.env.PORT;
489
+ const originalHost = process.env.HOST;
490
+ try {
491
+ process.env.PORT = '8888';
492
+ process.env.HOST = '10.0.0.1';
493
+
494
+ @Module({})
495
+ class TestModule {}
496
+
497
+ const app = createTestApp(TestModule);
498
+ expect(app.getHttpUrl()).toBe('http://10.0.0.1:8888');
499
+ } finally {
500
+ if (originalPort === undefined) {
501
+ delete process.env.PORT;
502
+ } else {
503
+ process.env.PORT = originalPort;
504
+ }
505
+ if (originalHost === undefined) {
506
+ delete process.env.HOST;
507
+ } else {
508
+ process.env.HOST = originalHost;
509
+ }
510
+ }
511
+ });
512
+
513
+ test('should prefer explicit port over PORT env variable', () => {
514
+ const originalPort = process.env.PORT;
515
+ const originalHost = process.env.HOST;
516
+ try {
517
+ process.env.PORT = '9999';
518
+ delete process.env.HOST; // Clear HOST to get default
519
+
520
+ @Module({})
521
+ class TestModule {}
522
+
523
+ const app = createTestApp(TestModule, { port: 5555 });
524
+ expect(app.getHttpUrl()).toBe('http://0.0.0.0:5555');
525
+ } finally {
526
+ if (originalPort === undefined) {
527
+ delete process.env.PORT;
528
+ } else {
529
+ process.env.PORT = originalPort;
530
+ }
531
+ if (originalHost === undefined) {
532
+ delete process.env.HOST;
533
+ } else {
534
+ process.env.HOST = originalHost;
535
+ }
536
+ }
537
+ });
538
+
539
+ test('should prefer explicit host over HOST env variable', () => {
540
+ const originalHost = process.env.HOST;
541
+ const originalPort = process.env.PORT;
542
+ try {
543
+ process.env.HOST = '192.168.1.100';
544
+ delete process.env.PORT; // Clear PORT to get default
545
+
546
+ @Module({})
547
+ class TestModule {}
548
+
549
+ const app = createTestApp(TestModule, { host: 'localhost' });
550
+ expect(app.getHttpUrl()).toBe('http://localhost:3000');
551
+ } finally {
552
+ if (originalHost === undefined) {
553
+ delete process.env.HOST;
554
+ } else {
555
+ process.env.HOST = originalHost;
556
+ }
557
+ if (originalPort === undefined) {
558
+ delete process.env.PORT;
559
+ } else {
560
+ process.env.PORT = originalPort;
561
+ }
562
+ }
563
+ });
564
+
565
+ test('should use defaults when env variables are not set', () => {
566
+ const originalPort = process.env.PORT;
567
+ const originalHost = process.env.HOST;
568
+ try {
569
+ delete process.env.PORT;
570
+ delete process.env.HOST;
571
+
572
+ @Module({})
573
+ class TestModule {}
574
+
575
+ const app = createTestApp(TestModule);
576
+ expect(app.getHttpUrl()).toBe('http://0.0.0.0:3000');
577
+ } finally {
578
+ if (originalPort !== undefined) {
579
+ process.env.PORT = originalPort;
580
+ }
581
+ if (originalHost !== undefined) {
582
+ process.env.HOST = originalHost;
583
+ }
584
+ }
585
+ });
586
+
587
+ test('should ignore invalid PORT env variable and use default', () => {
588
+ const originalPort = process.env.PORT;
589
+ const originalHost = process.env.HOST;
590
+ try {
591
+ process.env.PORT = 'invalid';
592
+ delete process.env.HOST; // Clear HOST to get default
593
+
594
+ @Module({})
595
+ class TestModule {}
596
+
597
+ const app = createTestApp(TestModule);
598
+ expect(app.getHttpUrl()).toBe('http://0.0.0.0:3000');
599
+ } finally {
600
+ if (originalPort === undefined) {
601
+ delete process.env.PORT;
602
+ } else {
603
+ process.env.PORT = originalPort;
604
+ }
605
+ if (originalHost === undefined) {
606
+ delete process.env.HOST;
607
+ } else {
608
+ process.env.HOST = originalHost;
609
+ }
610
+ }
611
+ });
612
+
613
+ test('should ignore empty PORT env variable and use default', () => {
614
+ const originalPort = process.env.PORT;
615
+ const originalHost = process.env.HOST;
616
+ try {
617
+ process.env.PORT = '';
618
+ delete process.env.HOST; // Clear HOST to get default
619
+
620
+ @Module({})
621
+ class TestModule {}
622
+
623
+ const app = createTestApp(TestModule);
624
+ expect(app.getHttpUrl()).toBe('http://0.0.0.0:3000');
625
+ } finally {
626
+ if (originalPort === undefined) {
627
+ delete process.env.PORT;
628
+ } else {
629
+ process.env.PORT = originalPort;
630
+ }
631
+ if (originalHost === undefined) {
632
+ delete process.env.HOST;
633
+ } else {
634
+ process.env.HOST = originalHost;
635
+ }
636
+ }
637
+ });
638
+
639
+ test('should ignore empty HOST env variable and use default', () => {
640
+ const originalHost = process.env.HOST;
641
+ const originalPort = process.env.PORT;
642
+ try {
643
+ process.env.HOST = '';
644
+ delete process.env.PORT; // Clear PORT to get default
645
+
646
+ @Module({})
647
+ class TestModule {}
648
+
649
+ const app = createTestApp(TestModule);
650
+ expect(app.getHttpUrl()).toBe('http://0.0.0.0:3000');
651
+ } finally {
652
+ if (originalHost === undefined) {
653
+ delete process.env.HOST;
654
+ } else {
655
+ process.env.HOST = originalHost;
656
+ }
657
+ if (originalPort === undefined) {
658
+ delete process.env.PORT;
659
+ } else {
660
+ process.env.PORT = originalPort;
661
+ }
662
+ }
663
+ });
664
+
435
665
  test('should handle complex envSchema configuration', () => {
436
666
  @Module({})
437
667
  class TestModule {}
@@ -596,7 +826,7 @@ describe('OneBunApplication', () => {
596
826
  await expect(app.start()).resolves.toBeUndefined();
597
827
  });
598
828
 
599
- test('should stop server when running', () => {
829
+ test('should stop server when running', async () => {
600
830
  @Module({})
601
831
  class TestModule {}
602
832
 
@@ -608,20 +838,20 @@ describe('OneBunApplication', () => {
608
838
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
609
839
  (app as any).server = mockServer;
610
840
 
611
- app.stop();
841
+ await app.stop();
612
842
 
613
843
  expect(mockServer.stop).toHaveBeenCalled();
614
844
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
615
845
  expect((app as any).server).toBeNull();
616
846
  });
617
847
 
618
- test('should handle stop when server is not running', () => {
848
+ test('should handle stop when server is not running', async () => {
619
849
  @Module({})
620
850
  class TestModule {}
621
851
 
622
852
  const app = createTestApp(TestModule);
623
853
 
624
- expect(() => app.stop()).not.toThrow();
854
+ await expect(app.stop()).resolves.toBeUndefined();
625
855
  });
626
856
  });
627
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 {
@@ -112,17 +113,48 @@ function normalizePath(path: string): string {
112
113
  return path.endsWith('/') ? path.slice(0, -1) : path;
113
114
  }
114
115
 
116
+ /**
117
+ * Resolve port from options, environment variable, or default.
118
+ * Priority: explicit option > PORT env > default (3000)
119
+ */
120
+ function resolvePort(explicitPort: number | undefined): number {
121
+ if (explicitPort !== undefined) {
122
+ return explicitPort;
123
+ }
124
+ const envPort = process.env.PORT;
125
+ if (envPort !== undefined && envPort !== '') {
126
+ const parsed = parseInt(envPort, 10);
127
+ if (!isNaN(parsed) && parsed > 0) {
128
+ return parsed;
129
+ }
130
+ }
131
+
132
+ return 3000;
133
+ }
134
+
135
+ /**
136
+ * Resolve host from options, environment variable, or default.
137
+ * Priority: explicit option > HOST env > default ('0.0.0.0')
138
+ */
139
+ function resolveHost(explicitHost: string | undefined): string {
140
+ if (explicitHost !== undefined) {
141
+ return explicitHost;
142
+ }
143
+ const envHost = process.env.HOST;
144
+ if (envHost !== undefined && envHost !== '') {
145
+ return envHost;
146
+ }
147
+
148
+ return '0.0.0.0';
149
+ }
150
+
115
151
  /**
116
152
  * OneBun Application
117
153
  */
118
154
  export class OneBunApplication {
119
155
  private rootModule: ModuleInstance;
120
156
  private server: ReturnType<typeof Bun.serve> | null = null;
121
- private options: ApplicationOptions = {
122
- port: 3000,
123
- host: '0.0.0.0',
124
- development: process.env.NODE_ENV !== 'production',
125
- };
157
+ private options: ApplicationOptions;
126
158
  private logger: SyncLogger;
127
159
  private config: IConfig<OneBunAppConfig>;
128
160
  private configService: ConfigServiceImpl | null = null;
@@ -144,9 +176,13 @@ export class OneBunApplication {
144
176
  moduleClass: new (...args: unknown[]) => object,
145
177
  options?: Partial<ApplicationOptions>,
146
178
  ) {
147
- if (options) {
148
- this.options = { ...this.options, ...options };
149
- }
179
+ // Resolve port and host with priority: explicit > env > default
180
+ this.options = {
181
+ port: resolvePort(options?.port),
182
+ host: resolveHost(options?.host),
183
+ development: options?.development ?? process.env.NODE_ENV !== 'production',
184
+ ...options,
185
+ };
150
186
 
151
187
  // Initialize configuration - TypedEnv if schema provided, otherwise NotInitializedConfig
152
188
  if (this.options.envSchema) {
@@ -156,8 +192,12 @@ export class OneBunApplication {
156
192
  this.config = new NotInitializedConfig();
157
193
  }
158
194
 
159
- // Use provided logger layer or create a default one
160
- 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());
161
201
 
162
202
  // Initialize logger with application class name as context
163
203
  const effectLogger = Effect.runSync(
@@ -467,6 +507,12 @@ export class OneBunApplication {
467
507
  }
468
508
  }
469
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
+
470
516
  // Get metrics path
471
517
  const metricsPath = this.options.metrics?.path || '/metrics';
472
518
 
@@ -1179,11 +1225,18 @@ export class OneBunApplication {
1179
1225
  * Stop the application with graceful shutdown
1180
1226
  * @param options - Shutdown options
1181
1227
  */
1182
- async stop(options?: { closeSharedRedis?: boolean }): Promise<void> {
1228
+ async stop(options?: { closeSharedRedis?: boolean; signal?: string }): Promise<void> {
1183
1229
  const closeRedis = options?.closeSharedRedis ?? true;
1230
+ const signal = options?.signal;
1184
1231
 
1185
1232
  this.logger.info('Stopping OneBun application...');
1186
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
+
1187
1240
  // Cleanup WebSocket resources
1188
1241
  if (this.wsHandler) {
1189
1242
  this.logger.debug('Cleaning up WebSocket handler');
@@ -1212,12 +1265,24 @@ export class OneBunApplication {
1212
1265
  this.logger.debug('HTTP server stopped');
1213
1266
  }
1214
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
+
1215
1274
  // Close shared Redis connection if configured and requested
1216
1275
  if (closeRedis && SharedRedisProvider.isConnected()) {
1217
1276
  this.logger.debug('Disconnecting shared Redis');
1218
1277
  await SharedRedisProvider.disconnect();
1219
1278
  }
1220
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
+
1221
1286
  this.logger.info('OneBun application stopped');
1222
1287
  }
1223
1288
 
@@ -1405,7 +1470,7 @@ export class OneBunApplication {
1405
1470
  enableGracefulShutdown(): void {
1406
1471
  const shutdown = async (signal: string) => {
1407
1472
  this.logger.info(`Received ${signal}, initiating graceful shutdown...`);
1408
- await this.stop();
1473
+ await this.stop({ signal });
1409
1474
  process.exit(0);
1410
1475
  };
1411
1476
 
@@ -1434,4 +1499,36 @@ export class OneBunApplication {
1434
1499
  getHttpUrl(): string {
1435
1500
  return `http://${this.options.host}:${this.options.port}`;
1436
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
+ }
1437
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
  }