@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.
@@ -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 pendingAsyncInits: Array<{ name: string; init: () => Promise<void> }> = [];
78
+ private pendingServiceInits: Array<{ name: string; instance: unknown }> = [];
72
79
  private logger: SyncLogger;
73
80
  private config: IConfig<OneBunAppConfig>;
74
81
 
@@ -162,9 +169,6 @@ export class OneBunModule implements ModuleInstance {
162
169
  // Merge layers
163
170
  layer = Layer.merge(layer, childModule.getLayer());
164
171
 
165
- // Add controllers from child module
166
- controllers.push(...childModule.getControllers());
167
-
168
172
  // Get exported services from child module and register them for DI
169
173
  const exportedServices = childModule.getExportedServices();
170
174
  for (const [tag, instance] of exportedServices) {
@@ -303,16 +307,11 @@ export class OneBunModule implements ModuleInstance {
303
307
  .initializeService(this.logger, this.config);
304
308
  }
305
309
 
306
- // Track services that need async initialization
307
- if (
308
- serviceInstance &&
309
- typeof serviceInstance === 'object' &&
310
- 'onAsyncInit' in serviceInstance &&
311
- typeof (serviceInstance as { onAsyncInit: unknown }).onAsyncInit === 'function'
312
- ) {
313
- this.pendingAsyncInits.push({
310
+ // Track services that need lifecycle hooks (onModuleInit)
311
+ if (hasOnModuleInit(serviceInstance)) {
312
+ this.pendingServiceInits.push({
314
313
  name: provider.name,
315
- init: () => (serviceInstance as { onAsyncInit: () => Promise<void> }).onAsyncInit(),
314
+ instance: serviceInstance,
316
315
  });
317
316
  }
318
317
 
@@ -384,48 +383,20 @@ export class OneBunModule implements ModuleInstance {
384
383
  */
385
384
  createControllerInstances(): Effect.Effect<unknown, never, void> {
386
385
  return Effect.sync(() => {
387
- // Services are already created in initModule via createServicesWithDI
388
- // Just need to set up controllers with DI
389
-
390
- // Get module metadata to access providers for controller dependency registration
391
- const moduleMetadata = getModuleMetadata(this.moduleClass);
392
- if (moduleMetadata && moduleMetadata.providers) {
393
- // Create map of available services for dependency resolution
394
- const availableServices = new Map<string, Function>();
395
-
396
- // For each provider that is a class constructor, add to available services map
397
- for (const provider of moduleMetadata.providers) {
398
- if (typeof provider === 'function') {
399
- availableServices.set(provider.name, provider);
400
- }
401
- }
402
-
403
- // Also add services from imported modules
404
- for (const childModule of this.childModules) {
405
- const childMetadata = getModuleMetadata(childModule.moduleClass);
406
- if (childMetadata?.exports) {
407
- for (const exported of childMetadata.exports) {
408
- if (typeof exported === 'function') {
409
- availableServices.set(exported.name, exported);
410
- }
411
- }
412
- }
413
- }
414
-
415
- // Add global services to available services for dependency resolution
416
- for (const [, instance] of globalServicesRegistry) {
417
- if (instance && typeof instance === 'object') {
418
- availableServices.set(instance.constructor.name, instance.constructor);
419
- }
386
+ // Build map of available services from this module's DI scope (own providers + imported exports + global)
387
+ const availableServices = new Map<string, Function>();
388
+ for (const [, instance] of this.serviceInstances) {
389
+ if (instance && typeof instance === 'object') {
390
+ availableServices.set(instance.constructor.name, instance.constructor);
420
391
  }
392
+ }
421
393
 
422
- // Automatically analyze and register dependencies for all controllers
423
- for (const controllerClass of this.controllers) {
424
- registerControllerDependencies(controllerClass, availableServices);
425
- }
394
+ // Automatically analyze and register dependencies for all controllers of this module
395
+ for (const controllerClass of this.controllers) {
396
+ registerControllerDependencies(controllerClass, availableServices);
426
397
  }
427
398
 
428
- // Now create controller instances with automatic dependency injection
399
+ // Create controller instances with automatic dependency injection
429
400
  this.createControllersWithDI();
430
401
  }).pipe(Effect.provide(this.rootLayer));
431
402
  }
@@ -558,35 +529,50 @@ export class OneBunModule implements ModuleInstance {
558
529
  * Setup the module and its dependencies
559
530
  */
560
531
  setup(): Effect.Effect<unknown, never, void> {
561
- return this.runAsyncServiceInit().pipe(
562
- // Also run async init for child modules
532
+ return this.callServicesOnModuleInit().pipe(
533
+ // Also run onModuleInit for child modules' services
534
+ Effect.flatMap(() =>
535
+ Effect.forEach(this.childModules, (childModule) => childModule.callServicesOnModuleInit(), {
536
+ discard: true,
537
+ }),
538
+ ),
539
+ // Create controller instances in child modules first, then this module (each uses its own DI scope)
563
540
  Effect.flatMap(() =>
564
- Effect.forEach(this.childModules, (childModule) => childModule.runAsyncServiceInit(), {
541
+ Effect.forEach(this.childModules, (childModule) => childModule.createControllerInstances(), {
565
542
  discard: true,
566
543
  }),
567
544
  ),
568
- // Then create controller instances
569
545
  Effect.flatMap(() => this.createControllerInstances()),
546
+ // Then call onModuleInit for controllers
547
+ Effect.flatMap(() => this.callControllersOnModuleInit()),
548
+ // Also run onModuleInit for child modules' controllers
549
+ Effect.flatMap(() =>
550
+ Effect.forEach(this.childModules, (childModule) => childModule.callControllersOnModuleInit(), {
551
+ discard: true,
552
+ }),
553
+ ),
570
554
  );
571
555
  }
572
556
 
573
557
  /**
574
- * Run async initialization for all services that need it
558
+ * Call onModuleInit lifecycle hook for all services that implement it
575
559
  */
576
- runAsyncServiceInit(): Effect.Effect<unknown, never, void> {
577
- if (this.pendingAsyncInits.length === 0) {
560
+ callServicesOnModuleInit(): Effect.Effect<unknown, never, void> {
561
+ if (this.pendingServiceInits.length === 0) {
578
562
  return Effect.void;
579
563
  }
580
564
 
581
- this.logger.debug(`Running async initialization for ${this.pendingAsyncInits.length} service(s)`);
565
+ this.logger.debug(`Calling onModuleInit for ${this.pendingServiceInits.length} service(s)`);
582
566
 
583
- // Run all async inits in parallel
584
- const initPromises = this.pendingAsyncInits.map(async ({ name, init }) => {
567
+ // Run all service onModuleInit hooks sequentially
568
+ const initPromises = this.pendingServiceInits.map(async ({ name, instance }) => {
585
569
  try {
586
- await init();
587
- this.logger.debug(`Service ${name} async initialization completed`);
570
+ if (hasOnModuleInit(instance)) {
571
+ await instance.onModuleInit();
572
+ }
573
+ this.logger.debug(`Service ${name} onModuleInit completed`);
588
574
  } catch (error) {
589
- this.logger.error(`Service ${name} async initialization failed: ${error}`);
575
+ this.logger.error(`Service ${name} onModuleInit failed: ${error}`);
590
576
  throw error;
591
577
  }
592
578
  });
@@ -594,45 +580,250 @@ export class OneBunModule implements ModuleInstance {
594
580
  return Effect.promise(() => Promise.all(initPromises)).pipe(
595
581
  Effect.map(() => {
596
582
  // Clear the list after initialization
597
- this.pendingAsyncInits = [];
583
+ this.pendingServiceInits = [];
598
584
  }),
599
585
  );
600
586
  }
601
587
 
602
588
  /**
603
- * Get all controllers from this module
589
+ * Call onModuleInit lifecycle hook for all controllers that implement it
590
+ */
591
+ callControllersOnModuleInit(): Effect.Effect<unknown, never, void> {
592
+ const controllers = Array.from(this.controllerInstances.values());
593
+ const controllersWithInit = controllers.filter((c): c is Controller & { onModuleInit(): Promise<void> | void } =>
594
+ hasOnModuleInit(c),
595
+ );
596
+
597
+ if (controllersWithInit.length === 0) {
598
+ return Effect.void;
599
+ }
600
+
601
+ this.logger.debug(`Calling onModuleInit for ${controllersWithInit.length} controller(s)`);
602
+
603
+ const initPromises = controllersWithInit.map(async (controller) => {
604
+ try {
605
+ await controller.onModuleInit();
606
+ this.logger.debug(`Controller ${controller.constructor.name} onModuleInit completed`);
607
+ } catch (error) {
608
+ this.logger.error(`Controller ${controller.constructor.name} onModuleInit failed: ${error}`);
609
+ throw error;
610
+ }
611
+ });
612
+
613
+ return Effect.promise(() => Promise.all(initPromises));
614
+ }
615
+
616
+ /**
617
+ * Call onApplicationInit lifecycle hook for all services and controllers
618
+ */
619
+ async callOnApplicationInit(): Promise<void> {
620
+ // Call for services
621
+ for (const [, instance] of this.serviceInstances) {
622
+ if (hasOnApplicationInit(instance)) {
623
+ try {
624
+ await instance.onApplicationInit();
625
+ this.logger.debug(`Service ${(instance as object).constructor.name} onApplicationInit completed`);
626
+ } catch (error) {
627
+ this.logger.error(`Service ${(instance as object).constructor.name} onApplicationInit failed: ${error}`);
628
+ throw error;
629
+ }
630
+ }
631
+ }
632
+
633
+ // Call for controllers
634
+ for (const [, controller] of this.controllerInstances) {
635
+ if (hasOnApplicationInit(controller)) {
636
+ try {
637
+ await controller.onApplicationInit();
638
+ this.logger.debug(`Controller ${controller.constructor.name} onApplicationInit completed`);
639
+ } catch (error) {
640
+ this.logger.error(`Controller ${controller.constructor.name} onApplicationInit failed: ${error}`);
641
+ throw error;
642
+ }
643
+ }
644
+ }
645
+
646
+ // Call for child modules
647
+ for (const childModule of this.childModules) {
648
+ await childModule.callOnApplicationInit();
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Call beforeApplicationDestroy lifecycle hook for all services and controllers
654
+ */
655
+ async callBeforeApplicationDestroy(signal?: string): Promise<void> {
656
+ // Call for services
657
+ for (const [, instance] of this.serviceInstances) {
658
+ if (hasBeforeApplicationDestroy(instance)) {
659
+ try {
660
+ await instance.beforeApplicationDestroy(signal);
661
+ this.logger.debug(`Service ${(instance as object).constructor.name} beforeApplicationDestroy completed`);
662
+ } catch (error) {
663
+ this.logger.error(`Service ${(instance as object).constructor.name} beforeApplicationDestroy failed: ${error}`);
664
+ }
665
+ }
666
+ }
667
+
668
+ // Call for controllers
669
+ for (const [, controller] of this.controllerInstances) {
670
+ if (hasBeforeApplicationDestroy(controller)) {
671
+ try {
672
+ await controller.beforeApplicationDestroy(signal);
673
+ this.logger.debug(`Controller ${controller.constructor.name} beforeApplicationDestroy completed`);
674
+ } catch (error) {
675
+ this.logger.error(`Controller ${controller.constructor.name} beforeApplicationDestroy failed: ${error}`);
676
+ }
677
+ }
678
+ }
679
+
680
+ // Call for child modules
681
+ for (const childModule of this.childModules) {
682
+ await childModule.callBeforeApplicationDestroy(signal);
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Call onModuleDestroy lifecycle hook for controllers first, then services
688
+ */
689
+ async callOnModuleDestroy(): Promise<void> {
690
+ // Call for controllers first (reverse order of creation)
691
+ const controllers = Array.from(this.controllerInstances.values()).reverse();
692
+ for (const controller of controllers) {
693
+ if (hasOnModuleDestroy(controller)) {
694
+ try {
695
+ await controller.onModuleDestroy();
696
+ this.logger.debug(`Controller ${controller.constructor.name} onModuleDestroy completed`);
697
+ } catch (error) {
698
+ this.logger.error(`Controller ${controller.constructor.name} onModuleDestroy failed: ${error}`);
699
+ }
700
+ }
701
+ }
702
+
703
+ // Call for services (reverse order of creation)
704
+ const services = Array.from(this.serviceInstances.values()).reverse();
705
+ for (const instance of services) {
706
+ if (hasOnModuleDestroy(instance)) {
707
+ try {
708
+ await instance.onModuleDestroy();
709
+ this.logger.debug(`Service ${(instance as object).constructor.name} onModuleDestroy completed`);
710
+ } catch (error) {
711
+ this.logger.error(`Service ${(instance as object).constructor.name} onModuleDestroy failed: ${error}`);
712
+ }
713
+ }
714
+ }
715
+
716
+ // Call for child modules
717
+ for (const childModule of this.childModules) {
718
+ await childModule.callOnModuleDestroy();
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Call onApplicationDestroy lifecycle hook for all services and controllers
724
+ */
725
+ async callOnApplicationDestroy(signal?: string): Promise<void> {
726
+ // Call for services
727
+ for (const [, instance] of this.serviceInstances) {
728
+ if (hasOnApplicationDestroy(instance)) {
729
+ try {
730
+ await instance.onApplicationDestroy(signal);
731
+ this.logger.debug(`Service ${(instance as object).constructor.name} onApplicationDestroy completed`);
732
+ } catch (error) {
733
+ this.logger.error(`Service ${(instance as object).constructor.name} onApplicationDestroy failed: ${error}`);
734
+ }
735
+ }
736
+ }
737
+
738
+ // Call for controllers
739
+ for (const [, controller] of this.controllerInstances) {
740
+ if (hasOnApplicationDestroy(controller)) {
741
+ try {
742
+ await controller.onApplicationDestroy(signal);
743
+ this.logger.debug(`Controller ${controller.constructor.name} onApplicationDestroy completed`);
744
+ } catch (error) {
745
+ this.logger.error(`Controller ${controller.constructor.name} onApplicationDestroy failed: ${error}`);
746
+ }
747
+ }
748
+ }
749
+
750
+ // Call for child modules
751
+ for (const childModule of this.childModules) {
752
+ await childModule.callOnApplicationDestroy(signal);
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Get all controllers from this module and child modules (recursive).
758
+ * Used by the application layer for routing and lifecycle.
604
759
  */
605
760
  getControllers(): Function[] {
606
- return this.controllers;
761
+ const fromChildren = this.childModules.flatMap((child) => child.getControllers());
762
+
763
+ return [...this.controllers, ...fromChildren];
607
764
  }
608
765
 
609
766
  /**
610
- * Get controller instance
767
+ * Get controller instance (searches this module then child modules recursively).
611
768
  */
612
769
  getControllerInstance(controllerClass: Function): Controller | undefined {
613
770
  const instance = this.controllerInstances.get(controllerClass);
614
-
615
- if (!instance) {
616
- this.logger.warn(`No instance found for controller ${controllerClass.name}`);
771
+ if (instance) {
772
+ return instance;
773
+ }
774
+ for (const childModule of this.childModules) {
775
+ const childInstance = childModule.getControllerInstance(controllerClass);
776
+ if (childInstance) {
777
+ return childInstance;
778
+ }
617
779
  }
780
+ this.logger.warn(`No instance found for controller ${controllerClass.name}`);
618
781
 
619
- return instance;
782
+ return undefined;
620
783
  }
621
784
 
622
785
  /**
623
- * Get all controller instances
786
+ * Get all controller instances from this module and child modules (recursive).
624
787
  */
625
788
  getControllerInstances(): Map<Function, Controller> {
626
- return this.controllerInstances;
789
+ const merged = new Map<Function, Controller>(this.controllerInstances);
790
+ for (const childModule of this.childModules) {
791
+ for (const [cls, instance] of childModule.getControllerInstances()) {
792
+ merged.set(cls, instance);
793
+ }
794
+ }
795
+
796
+ return merged;
627
797
  }
628
798
 
629
799
  /**
630
- * Get service instance
800
+ * Get service instance by tag
631
801
  */
632
802
  getServiceInstance<T>(tag: Context.Tag<T, T>): T | undefined {
633
803
  return this.serviceInstances.get(tag as Context.Tag<unknown, unknown>) as T | undefined;
634
804
  }
635
805
 
806
+ /**
807
+ * Get service instance by class
808
+ */
809
+ getServiceByClass<T>(serviceClass: new (...args: unknown[]) => T): T | undefined {
810
+ try {
811
+ const tag = getServiceTag(serviceClass);
812
+
813
+ return this.getServiceInstance(tag);
814
+ } catch {
815
+ // Service doesn't have @Service decorator or not found
816
+ return undefined;
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Get all service instances
822
+ */
823
+ getAllServiceInstances(): Map<Context.Tag<unknown, unknown>, unknown> {
824
+ return new Map(this.serviceInstances);
825
+ }
826
+
636
827
  /**
637
828
  * Get the Layer for this module
638
829
  */
@@ -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 layer to use
111
- * If not provided, a default logger will be created
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