@onebun/core 0.1.22 → 0.1.23

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.22",
3
+ "version": "0.1.23",
4
4
  "description": "Core package for OneBun framework - decorators, DI, modules, controllers",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -816,7 +816,7 @@ describe('Services API Documentation Examples', () => {
816
816
  @Service()
817
817
  class UserService extends BaseService {
818
818
  // Dependencies are auto-injected via constructor
819
- // Logger and config are auto-injected via initializeService()
819
+ // Logger and config are available immediately after super()
820
820
  constructor(private repository: UserRepository) {
821
821
  super();
822
822
  }
@@ -851,6 +851,106 @@ describe('OneBunModule', () => {
851
851
  expect(apiService).toBeDefined();
852
852
  expect((apiService as ApiService).getConnectionTimeout()).toBe(5000);
853
853
  });
854
+
855
+ /**
856
+ * Test that this.config and this.logger are available in the service constructor
857
+ * when the service is created through the DI system (via ambient init context).
858
+ */
859
+ test('should have this.config and this.logger available in constructor via DI', () => {
860
+ let configInConstructor: unknown = undefined;
861
+ let loggerInConstructor: unknown = undefined;
862
+
863
+ @Service()
864
+ class ConfigAwareService extends BaseServiceClass {
865
+ constructor() {
866
+ super();
867
+ configInConstructor = this.config;
868
+ loggerInConstructor = this.logger;
869
+ }
870
+ }
871
+
872
+ @ModuleDecorator({
873
+ providers: [ConfigAwareService],
874
+ })
875
+ class TestModule {}
876
+
877
+ // Initialize module — this triggers DI and service creation
878
+ new ModuleInstance(TestModule, mockLoggerLayer);
879
+
880
+ // config and logger should have been available in the constructor
881
+ expect(configInConstructor).toBeDefined();
882
+ expect(loggerInConstructor).toBeDefined();
883
+ });
884
+
885
+ /**
886
+ * Test that this.config.get() works in the constructor for services created via DI
887
+ */
888
+ test('should allow config.get() in service constructor via DI', () => {
889
+ @Service()
890
+ class ServiceWithConfigInConstructor extends BaseServiceClass {
891
+ readonly configValue: unknown;
892
+
893
+ constructor() {
894
+ super();
895
+ // Config should be available here via init context
896
+ this.configValue = this.config;
897
+ }
898
+
899
+ getConfigValue() {
900
+ return this.configValue;
901
+ }
902
+ }
903
+
904
+ @ModuleDecorator({
905
+ providers: [ServiceWithConfigInConstructor],
906
+ })
907
+ class TestModule {}
908
+
909
+ const module = new ModuleInstance(TestModule, mockLoggerLayer);
910
+
911
+ const { getServiceTag } = require('./service');
912
+ const tag = getServiceTag(ServiceWithConfigInConstructor);
913
+ const service = module.getServiceInstance(tag) as ServiceWithConfigInConstructor;
914
+
915
+ expect(service).toBeDefined();
916
+ // configValue was captured in constructor — should not be undefined
917
+ expect(service.getConfigValue()).toBeDefined();
918
+ });
919
+
920
+ /**
921
+ * Test that this.config is available in constructor of a service with dependencies
922
+ */
923
+ test('should have this.config in constructor of service with dependencies', () => {
924
+ let configAvailable = false;
925
+
926
+ @Service()
927
+ class DependencyService {
928
+ getValue() {
929
+ return 42;
930
+ }
931
+ }
932
+
933
+ @Service()
934
+ class MainService extends BaseServiceClass {
935
+ constructor(private dep: DependencyService) {
936
+ super();
937
+ configAvailable = this.config !== undefined;
938
+ }
939
+
940
+ getDep() {
941
+ return this.dep.getValue();
942
+ }
943
+ }
944
+
945
+ @ModuleDecorator({
946
+ providers: [DependencyService, MainService],
947
+ })
948
+ class TestModule {}
949
+
950
+ new ModuleInstance(TestModule, mockLoggerLayer);
951
+
952
+ expect(configAvailable).toBe(true);
953
+ });
854
954
  });
855
955
 
856
956
  describe('Lifecycle hooks', () => {
@@ -36,7 +36,11 @@ import {
36
36
  hasBeforeApplicationDestroy,
37
37
  hasOnApplicationDestroy,
38
38
  } from './lifecycle';
39
- import { getServiceMetadata, getServiceTag } from './service';
39
+ import {
40
+ BaseService,
41
+ getServiceMetadata,
42
+ getServiceTag,
43
+ } from './service';
40
44
 
41
45
  /**
42
46
  * Global services registry
@@ -290,13 +294,23 @@ export class OneBunModule implements ModuleInstance {
290
294
  continue;
291
295
  }
292
296
 
293
- // Create service instance with resolved dependencies (without logger/config in constructor)
297
+ // Create service instance with resolved dependencies.
298
+ // Set ambient init context so BaseService constructor can pick up logger/config,
299
+ // making them available immediately after super() in subclass constructors.
294
300
  try {
295
301
  const serviceConstructor = provider as new (...args: unknown[]) => unknown;
296
- const serviceInstance = new serviceConstructor(...dependencies);
297
302
 
298
- // Initialize service with logger and config if it has initializeService method
299
- // This is the pattern used by BaseService
303
+ BaseService.setInitContext(this.logger, this.config);
304
+ let serviceInstance: unknown;
305
+ try {
306
+ serviceInstance = new serviceConstructor(...dependencies);
307
+ } finally {
308
+ BaseService.clearInitContext();
309
+ }
310
+
311
+ // Fallback: call initializeService for services that have it but were not
312
+ // initialized via the constructor (e.g., services not extending BaseService
313
+ // but implementing initializeService manually, or for backwards compatibility).
300
314
  if (
301
315
  serviceInstance &&
302
316
  typeof serviceInstance === 'object' &&
@@ -165,6 +165,111 @@ describe('BaseService', () => {
165
165
  });
166
166
  });
167
167
 
168
+ describe('Initialization via static init context (constructor)', () => {
169
+ test('should initialize service via static init context in constructor', () => {
170
+ class TestService extends BaseService {
171
+ configAvailableInConstructor = false;
172
+ loggerAvailableInConstructor = false;
173
+
174
+ constructor() {
175
+ super();
176
+ this.configAvailableInConstructor = this.config !== undefined;
177
+ this.loggerAvailableInConstructor = this.logger !== undefined;
178
+ }
179
+ }
180
+
181
+ // Set init context before construction (as the framework does)
182
+ BaseService.setInitContext(mockLogger, mockConfig);
183
+ let service: TestService;
184
+ try {
185
+ service = new TestService();
186
+ } finally {
187
+ BaseService.clearInitContext();
188
+ }
189
+
190
+ expect(service.isInitialized).toBe(true);
191
+ expect(service.configAvailableInConstructor).toBe(true);
192
+ expect(service.loggerAvailableInConstructor).toBe(true);
193
+ expect((service as any).config).toBe(mockConfig);
194
+ });
195
+
196
+ test('should allow using config.get() in constructor when init context is set', () => {
197
+ class TestService extends BaseService {
198
+ readonly dbHost: string;
199
+
200
+ constructor() {
201
+ super();
202
+ this.dbHost = this.config.get('database.host') as string;
203
+ }
204
+ }
205
+
206
+ BaseService.setInitContext(mockLogger, mockConfig);
207
+ let service: TestService;
208
+ try {
209
+ service = new TestService();
210
+ } finally {
211
+ BaseService.clearInitContext();
212
+ }
213
+
214
+ expect(service.dbHost).toBe('localhost');
215
+ });
216
+
217
+ test('should create child logger with correct className in constructor', () => {
218
+ class MyCustomService extends BaseService {}
219
+
220
+ BaseService.setInitContext(mockLogger, mockConfig);
221
+ try {
222
+ new MyCustomService();
223
+ } finally {
224
+ BaseService.clearInitContext();
225
+ }
226
+
227
+ expect(mockLogger.child).toHaveBeenCalledWith({ className: 'MyCustomService' });
228
+ });
229
+
230
+ test('should not initialize if no init context is set', () => {
231
+ // Ensure context is clear
232
+ BaseService.clearInitContext();
233
+
234
+ class TestService extends BaseService {}
235
+ const service = new TestService();
236
+
237
+ expect(service.isInitialized).toBe(false);
238
+ });
239
+
240
+ test('initializeService should be a no-op if already initialized via init context', () => {
241
+ class TestService extends BaseService {}
242
+
243
+ BaseService.setInitContext(mockLogger, mockConfig);
244
+ let service: TestService;
245
+ try {
246
+ service = new TestService();
247
+ } finally {
248
+ BaseService.clearInitContext();
249
+ }
250
+
251
+ expect(service.isInitialized).toBe(true);
252
+
253
+ // Call initializeService again — should be a no-op
254
+ const otherLogger = { ...mockLogger, child: mock(() => ({ ...mockLogger })) };
255
+ const otherConfig = createMockConfig({ other: 'config' });
256
+ service.initializeService(otherLogger, otherConfig);
257
+
258
+ // Should still have original config (no reinit)
259
+ expect((service as any).config).toBe(mockConfig);
260
+ });
261
+
262
+ test('clearInitContext should prevent subsequent constructors from picking up context', () => {
263
+ BaseService.setInitContext(mockLogger, mockConfig);
264
+ BaseService.clearInitContext();
265
+
266
+ class TestService extends BaseService {}
267
+ const service = new TestService();
268
+
269
+ expect(service.isInitialized).toBe(false);
270
+ });
271
+ });
272
+
168
273
  describe('Error handling edge cases', () => {
169
274
  test('should handle complex effect scenarios', async () => {
170
275
  class TestService extends BaseService {
@@ -19,8 +19,9 @@ const META_SERVICES = new Map<
19
19
  /**
20
20
  * Service decorator
21
21
  * Registers the class as a service with an optional Effect Context tag.
22
- * Services extending BaseService will have logger and config injected
23
- * via the initializeService method called by the module after instantiation.
22
+ * Services extending BaseService will have logger and config available
23
+ * immediately after super() in the constructor (via ambient init context),
24
+ * as well as through the initializeService fallback method.
24
25
  *
25
26
  * @param tag - Optional Effect Context tag for the service
26
27
  */
@@ -62,7 +63,26 @@ export function getServiceTag<T>(serviceClass: new (...args: unknown[]) => T): C
62
63
  }
63
64
 
64
65
  /**
65
- * Base service class that provides utility methods for working with Effect
66
+ * Base service class that provides utility methods for working with Effect.
67
+ *
68
+ * Services extending BaseService have `this.config` and `this.logger` available
69
+ * immediately after `super()` in the constructor when created through the framework DI.
70
+ * The framework sets an ambient init context before calling the constructor, and
71
+ * BaseService reads from it in `super()`.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * @Service()
76
+ * class MyService extends BaseService {
77
+ * private readonly baseUrl: string;
78
+ *
79
+ * constructor(private dep: SomeDep) {
80
+ * super();
81
+ * // this.config and this.logger are available here!
82
+ * this.baseUrl = this.config.get('api.baseUrl');
83
+ * }
84
+ * }
85
+ * ```
66
86
  */
67
87
  export class BaseService {
68
88
  // Logger instance with service class name as context
@@ -73,12 +93,55 @@ export class BaseService {
73
93
  private _initialized = false;
74
94
 
75
95
  /**
76
- * Initialize service with logger and config (called by the framework)
96
+ * Ambient init context set by the framework before service construction.
97
+ * This allows BaseService constructor to pick up logger and config
98
+ * so they are available immediately after super() in subclass constructors.
99
+ * @internal
100
+ */
101
+ private static _initContext: { logger: SyncLogger; config: IConfig<OneBunAppConfig> } | null =
102
+ null;
103
+
104
+ /**
105
+ * Set the ambient init context before constructing a service.
106
+ * Called by the framework (OneBunModule) before `new ServiceClass(...)`.
107
+ * @internal
108
+ */
109
+ static setInitContext(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
110
+ BaseService._initContext = { logger, config };
111
+ }
112
+
113
+ /**
114
+ * Clear the ambient init context after service construction.
115
+ * Called by the framework (OneBunModule) after `new ServiceClass(...)`.
116
+ * @internal
117
+ */
118
+ static clearInitContext(): void {
119
+ BaseService._initContext = null;
120
+ }
121
+
122
+ constructor() {
123
+ // Pick up logger and config from ambient init context if available.
124
+ // This makes this.config and this.logger available immediately after super()
125
+ // in subclass constructors.
126
+ if (BaseService._initContext) {
127
+ const { logger, config } = BaseService._initContext;
128
+ const className = this.constructor.name;
129
+ this.logger = logger.child({ className });
130
+ this.config = config;
131
+ this._initialized = true;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Initialize service with logger and config (called by the framework).
137
+ * This is a fallback for services not constructed through the DI system
138
+ * (e.g., in tests or when created manually). If the service was already
139
+ * initialized via the constructor init context, this is a no-op.
77
140
  * @internal
78
141
  */
79
142
  initializeService(logger: SyncLogger, config: IConfig<OneBunAppConfig>): void {
80
143
  if (this._initialized) {
81
- return; // Already initialized
144
+ return; // Already initialized (via constructor or previous call)
82
145
  }
83
146
 
84
147
  const className = this.constructor.name;