@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.
@@ -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
 
@@ -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 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({
313
+ // Track services that need lifecycle hooks (onModuleInit)
314
+ if (hasOnModuleInit(serviceInstance)) {
315
+ this.pendingServiceInits.push({
314
316
  name: provider.name,
315
- init: () => (serviceInstance as { onAsyncInit: () => Promise<void> }).onAsyncInit(),
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.runAsyncServiceInit().pipe(
562
- // Also run async init for child modules
563
+ return this.callServicesOnModuleInit().pipe(
564
+ // Also run onModuleInit for child modules' services
563
565
  Effect.flatMap(() =>
564
- Effect.forEach(this.childModules, (childModule) => childModule.runAsyncServiceInit(), {
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
- * Run async initialization for all services that need it
584
+ * Call onModuleInit lifecycle hook for all services that implement it
575
585
  */
576
- runAsyncServiceInit(): Effect.Effect<unknown, never, void> {
577
- if (this.pendingAsyncInits.length === 0) {
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(`Running async initialization for ${this.pendingAsyncInits.length} service(s)`);
591
+ this.logger.debug(`Calling onModuleInit for ${this.pendingServiceInits.length} service(s)`);
582
592
 
583
- // Run all async inits in parallel
584
- const initPromises = this.pendingAsyncInits.map(async ({ name, init }) => {
593
+ // Run all service onModuleInit hooks sequentially
594
+ const initPromises = this.pendingServiceInits.map(async ({ name, instance }) => {
585
595
  try {
586
- await init();
587
- this.logger.debug(`Service ${name} async initialization completed`);
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} async initialization failed: ${error}`);
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.pendingAsyncInits = [];
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
  */
@@ -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