@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.
@@ -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
+ }
@@ -10,7 +10,11 @@ import {
10
10
  afterEach,
11
11
  mock,
12
12
  } from 'bun:test';
13
- import { Context } from 'effect';
13
+ import {
14
+ Context,
15
+ Effect,
16
+ Layer,
17
+ } from 'effect';
14
18
 
15
19
  import { Module } from '../decorators/decorators';
16
20
  import { makeMockLoggerLayer } from '../testing/test-utils';
@@ -157,7 +161,6 @@ describe('OneBunModule', () => {
157
161
 
158
162
  test('should detect circular dependencies and provide detailed error message', () => {
159
163
  const { registerDependencies } = require('../decorators/decorators');
160
- const { Effect, Layer } = require('effect');
161
164
  const { LoggerService } = require('@onebun/logger');
162
165
 
163
166
  // Collect error messages
@@ -849,4 +852,145 @@ describe('OneBunModule', () => {
849
852
  expect((apiService as ApiService).getConnectionTimeout()).toBe(5000);
850
853
  });
851
854
  });
855
+
856
+ describe('Module DI scoping (exports only for cross-module)', () => {
857
+ const {
858
+ Controller: ControllerDecorator,
859
+ Get,
860
+ Inject,
861
+ clearGlobalModules,
862
+ } = require('../decorators/decorators');
863
+ const { Controller: BaseController } = require('./controller');
864
+ const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
865
+
866
+ beforeEach(() => {
867
+ clearGlobalModules();
868
+ clearRegistry();
869
+ });
870
+
871
+ afterEach(() => {
872
+ clearGlobalModules();
873
+ clearRegistry();
874
+ });
875
+
876
+ test('controller can inject provider from same module without exports', async () => {
877
+ @Service()
878
+ class CounterService {
879
+ private count = 0;
880
+ getCount() {
881
+ return this.count;
882
+ }
883
+ increment() {
884
+ this.count += 1;
885
+ }
886
+ }
887
+
888
+ class CounterController extends BaseController {
889
+ constructor(@Inject(CounterService) private readonly counterService: CounterService) {
890
+ super();
891
+ }
892
+ getCount() {
893
+ return this.counterService.getCount();
894
+ }
895
+ }
896
+ const CounterControllerDecorated = ControllerDecorator('/counter')(CounterController);
897
+ Get('/')(CounterControllerDecorated.prototype, 'getCount', Object.getOwnPropertyDescriptor(CounterControllerDecorated.prototype, 'getCount')!);
898
+
899
+ @Module({
900
+ providers: [CounterService],
901
+ controllers: [CounterControllerDecorated],
902
+ // No exports - CounterService is only used inside this module
903
+ })
904
+ class FeatureModule {}
905
+
906
+ const module = new ModuleClass(FeatureModule, mockLoggerLayer);
907
+ module.getLayer();
908
+ await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
909
+
910
+ const controller = module.getControllerInstance(CounterControllerDecorated) as CounterController;
911
+ expect(controller).toBeDefined();
912
+ expect(controller.getCount()).toBe(0);
913
+ });
914
+
915
+ test('child module controller injects own provider; root can resolve controller', async () => {
916
+ @Service()
917
+ class ChildService {
918
+ getValue() {
919
+ return 'child';
920
+ }
921
+ }
922
+
923
+ class ChildController extends BaseController {
924
+ constructor(@Inject(ChildService) private readonly childService: ChildService) {
925
+ super();
926
+ }
927
+ getValue() {
928
+ return this.childService.getValue();
929
+ }
930
+ }
931
+ const ChildControllerDecorated = ControllerDecorator('/child')(ChildController);
932
+ Get('/')(ChildControllerDecorated.prototype, 'getValue', Object.getOwnPropertyDescriptor(ChildControllerDecorated.prototype, 'getValue')!);
933
+
934
+ @Module({
935
+ providers: [ChildService],
936
+ controllers: [ChildControllerDecorated],
937
+ })
938
+ class ChildModule {}
939
+
940
+ @Module({
941
+ imports: [ChildModule],
942
+ })
943
+ class RootModule {}
944
+
945
+ const rootModule = new ModuleClass(RootModule, mockLoggerLayer);
946
+ rootModule.getLayer();
947
+ await Effect.runPromise(rootModule.setup() as Effect.Effect<unknown, never, never>);
948
+
949
+ const allControllers = rootModule.getControllers();
950
+ expect(allControllers).toContain(ChildControllerDecorated);
951
+ const controller = rootModule.getControllerInstance(ChildControllerDecorated) as ChildController;
952
+ expect(controller).toBeDefined();
953
+ expect(controller.getValue()).toBe('child');
954
+ });
955
+
956
+ test('exported service from imported module is injectable in importing module', async () => {
957
+ @Service()
958
+ class SharedService {
959
+ getLabel() {
960
+ return 'shared';
961
+ }
962
+ }
963
+
964
+ @Module({
965
+ providers: [SharedService],
966
+ exports: [SharedService],
967
+ })
968
+ class SharedModule {}
969
+
970
+ class AppController extends BaseController {
971
+ constructor(@Inject(SharedService) private readonly sharedService: SharedService) {
972
+ super();
973
+ }
974
+ getLabel() {
975
+ return this.sharedService.getLabel();
976
+ }
977
+ }
978
+ const AppControllerDecorated = ControllerDecorator('/app')(AppController);
979
+ Get('/')(AppControllerDecorated.prototype, 'getLabel', Object.getOwnPropertyDescriptor(AppControllerDecorated.prototype, 'getLabel')!);
980
+
981
+ @Module({
982
+ imports: [SharedModule],
983
+ controllers: [AppControllerDecorated],
984
+ })
985
+ class AppModule {}
986
+
987
+ const module = new ModuleClass(AppModule, mockLoggerLayer);
988
+ module.getLayer();
989
+ await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
990
+
991
+ const controller = module.getControllerInstance(AppControllerDecorated) as AppController;
992
+ expect(controller).toBeDefined();
993
+ expect(controller.getLabel()).toBe('shared');
994
+ });
995
+ });
852
996
  });