@onebun/core 0.1.21 → 0.1.22
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 +1 -1
- package/src/docs-examples.test.ts +94 -0
- package/src/module/module.test.ts +182 -0
- package/src/module/module.ts +18 -17
package/package.json
CHANGED
|
@@ -1150,6 +1150,100 @@ describe('Lifecycle Hooks API Documentation Examples (docs/api/services.md)', ()
|
|
|
1150
1150
|
await callOnApplicationDestroy(emptyObj, 'SIGTERM');
|
|
1151
1151
|
});
|
|
1152
1152
|
});
|
|
1153
|
+
|
|
1154
|
+
describe('Standalone Service Pattern (docs/api/services.md)', () => {
|
|
1155
|
+
/**
|
|
1156
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
1157
|
+
* Standalone services (not injected anywhere) still have their
|
|
1158
|
+
* onModuleInit called. This is useful for background workers,
|
|
1159
|
+
* cron jobs, event listeners, etc.
|
|
1160
|
+
*/
|
|
1161
|
+
it('should call onModuleInit for standalone services not injected anywhere', async () => {
|
|
1162
|
+
const moduleMod = await import('./module/module');
|
|
1163
|
+
const testUtils = await import('./testing/test-utils');
|
|
1164
|
+
const effectLib = await import('effect');
|
|
1165
|
+
|
|
1166
|
+
let schedulerStarted = false;
|
|
1167
|
+
|
|
1168
|
+
// From docs: Standalone service pattern
|
|
1169
|
+
@Service()
|
|
1170
|
+
class TaskSchedulerService extends BaseService implements OnModuleInit {
|
|
1171
|
+
async onModuleInit(): Promise<void> {
|
|
1172
|
+
// Main work happens here — no need to be injected anywhere
|
|
1173
|
+
schedulerStarted = true;
|
|
1174
|
+
this.logger.info('Task scheduler started');
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
@Module({
|
|
1179
|
+
providers: [TaskSchedulerService],
|
|
1180
|
+
// No controllers use this service — it works on its own
|
|
1181
|
+
})
|
|
1182
|
+
class SchedulerModule {}
|
|
1183
|
+
|
|
1184
|
+
const mod = new moduleMod.OneBunModule(SchedulerModule, testUtils.makeMockLoggerLayer());
|
|
1185
|
+
await effectLib.Effect.runPromise(mod.setup() as import('effect').Effect.Effect<unknown, never, never>);
|
|
1186
|
+
|
|
1187
|
+
// Scheduler was started even though nothing injected it
|
|
1188
|
+
expect(schedulerStarted).toBe(true);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* @source docs/api/services.md#lifecycle-hooks
|
|
1193
|
+
* onModuleInit is called sequentially in dependency order:
|
|
1194
|
+
* dependencies complete their init before dependents start theirs.
|
|
1195
|
+
*/
|
|
1196
|
+
it('should call onModuleInit in dependency order so dependencies are fully initialized', async () => {
|
|
1197
|
+
const moduleMod = await import('./module/module');
|
|
1198
|
+
const testUtils = await import('./testing/test-utils');
|
|
1199
|
+
const effectLib = await import('effect');
|
|
1200
|
+
const decorators = await import('./decorators/decorators');
|
|
1201
|
+
|
|
1202
|
+
const initOrder: string[] = [];
|
|
1203
|
+
|
|
1204
|
+
@Service()
|
|
1205
|
+
class DatabaseService extends BaseService implements OnModuleInit {
|
|
1206
|
+
private ready = false;
|
|
1207
|
+
|
|
1208
|
+
async onModuleInit(): Promise<void> {
|
|
1209
|
+
this.ready = true;
|
|
1210
|
+
initOrder.push('database');
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
isReady(): boolean {
|
|
1214
|
+
return this.ready;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
@Service()
|
|
1219
|
+
class CacheService extends BaseService implements OnModuleInit {
|
|
1220
|
+
private db: DatabaseService;
|
|
1221
|
+
|
|
1222
|
+
constructor(db: DatabaseService) {
|
|
1223
|
+
super();
|
|
1224
|
+
this.db = db;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async onModuleInit(): Promise<void> {
|
|
1228
|
+
// At this point, DatabaseService.onModuleInit has already completed
|
|
1229
|
+
initOrder.push(`cache:db-ready=${this.db.isReady()}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
decorators.registerDependencies(CacheService, [DatabaseService]);
|
|
1234
|
+
|
|
1235
|
+
@Module({
|
|
1236
|
+
providers: [DatabaseService, CacheService],
|
|
1237
|
+
})
|
|
1238
|
+
class AppModule {}
|
|
1239
|
+
|
|
1240
|
+
const mod = new moduleMod.OneBunModule(AppModule, testUtils.makeMockLoggerLayer());
|
|
1241
|
+
await effectLib.Effect.runPromise(mod.setup() as import('effect').Effect.Effect<unknown, never, never>);
|
|
1242
|
+
|
|
1243
|
+
// Database initialized first, then cache saw database was ready
|
|
1244
|
+
expect(initOrder).toEqual(['database', 'cache:db-ready=true']);
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1153
1247
|
});
|
|
1154
1248
|
|
|
1155
1249
|
describe('getService API Documentation Examples (docs/api/core.md)', () => {
|
|
@@ -853,6 +853,188 @@ describe('OneBunModule', () => {
|
|
|
853
853
|
});
|
|
854
854
|
});
|
|
855
855
|
|
|
856
|
+
describe('Lifecycle hooks', () => {
|
|
857
|
+
const { clearGlobalModules } = require('../decorators/decorators');
|
|
858
|
+
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
|
|
859
|
+
const { OnModuleInit } = require('./lifecycle');
|
|
860
|
+
|
|
861
|
+
beforeEach(() => {
|
|
862
|
+
clearGlobalModules();
|
|
863
|
+
clearRegistry();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
afterEach(() => {
|
|
867
|
+
clearGlobalModules();
|
|
868
|
+
clearRegistry();
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test('should call onModuleInit for a service that is not injected anywhere', async () => {
|
|
872
|
+
let initCalled = false;
|
|
873
|
+
|
|
874
|
+
@Service()
|
|
875
|
+
class StandaloneService {
|
|
876
|
+
async onModuleInit(): Promise<void> {
|
|
877
|
+
initCalled = true;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@Module({
|
|
882
|
+
providers: [StandaloneService],
|
|
883
|
+
// No controllers, no exports — this service is not injected anywhere
|
|
884
|
+
})
|
|
885
|
+
class TestModule {}
|
|
886
|
+
|
|
887
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
888
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
889
|
+
|
|
890
|
+
expect(initCalled).toBe(true);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
test('should call onModuleInit for multiple standalone services', async () => {
|
|
894
|
+
const initLog: string[] = [];
|
|
895
|
+
|
|
896
|
+
@Service()
|
|
897
|
+
class WorkerServiceA {
|
|
898
|
+
async onModuleInit(): Promise<void> {
|
|
899
|
+
initLog.push('A');
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
@Service()
|
|
904
|
+
class WorkerServiceB {
|
|
905
|
+
async onModuleInit(): Promise<void> {
|
|
906
|
+
initLog.push('B');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
@Service()
|
|
911
|
+
class WorkerServiceC {
|
|
912
|
+
async onModuleInit(): Promise<void> {
|
|
913
|
+
initLog.push('C');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
@Module({
|
|
918
|
+
providers: [WorkerServiceA, WorkerServiceB, WorkerServiceC],
|
|
919
|
+
})
|
|
920
|
+
class TestModule {}
|
|
921
|
+
|
|
922
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
923
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
924
|
+
|
|
925
|
+
expect(initLog).toContain('A');
|
|
926
|
+
expect(initLog).toContain('B');
|
|
927
|
+
expect(initLog).toContain('C');
|
|
928
|
+
expect(initLog.length).toBe(3);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('should call onModuleInit for standalone service in a child module', async () => {
|
|
932
|
+
let childInitCalled = false;
|
|
933
|
+
|
|
934
|
+
@Service()
|
|
935
|
+
class ChildStandaloneService {
|
|
936
|
+
async onModuleInit(): Promise<void> {
|
|
937
|
+
childInitCalled = true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
@Module({
|
|
942
|
+
providers: [ChildStandaloneService],
|
|
943
|
+
})
|
|
944
|
+
class ChildModule {}
|
|
945
|
+
|
|
946
|
+
@Module({
|
|
947
|
+
imports: [ChildModule],
|
|
948
|
+
})
|
|
949
|
+
class RootModule {}
|
|
950
|
+
|
|
951
|
+
const module = new ModuleClass(RootModule, mockLoggerLayer);
|
|
952
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
953
|
+
|
|
954
|
+
expect(childInitCalled).toBe(true);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('should call onModuleInit sequentially in dependency order', async () => {
|
|
958
|
+
const initLog: string[] = [];
|
|
959
|
+
const { registerDependencies } = require('../decorators/decorators');
|
|
960
|
+
|
|
961
|
+
@Service()
|
|
962
|
+
class DependencyService {
|
|
963
|
+
async onModuleInit(): Promise<void> {
|
|
964
|
+
// Simulate async work to make ordering matter
|
|
965
|
+
await new Promise<void>((resolve) => {
|
|
966
|
+
setTimeout(resolve, 5);
|
|
967
|
+
});
|
|
968
|
+
initLog.push('dependency-completed');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
getValue() {
|
|
972
|
+
return 42;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
@Service()
|
|
977
|
+
class DependentService {
|
|
978
|
+
async onModuleInit(): Promise<void> {
|
|
979
|
+
initLog.push('dependent-started');
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Register DependentService -> DependencyService dependency
|
|
984
|
+
registerDependencies(DependentService, [DependencyService]);
|
|
985
|
+
|
|
986
|
+
@Module({
|
|
987
|
+
providers: [DependencyService, DependentService],
|
|
988
|
+
})
|
|
989
|
+
class TestModule {}
|
|
990
|
+
|
|
991
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
992
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
993
|
+
|
|
994
|
+
// DependencyService.onModuleInit must complete BEFORE DependentService.onModuleInit starts
|
|
995
|
+
expect(initLog).toEqual(['dependency-completed', 'dependent-started']);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test('should have dependencies already injected when onModuleInit is called', async () => {
|
|
999
|
+
let depValueInInit: number | null = null;
|
|
1000
|
+
const { registerDependencies } = require('../decorators/decorators');
|
|
1001
|
+
|
|
1002
|
+
@Service()
|
|
1003
|
+
class ConfigService {
|
|
1004
|
+
getPort() {
|
|
1005
|
+
return 8080;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
@Service()
|
|
1010
|
+
class ServerService {
|
|
1011
|
+
private configService: ConfigService;
|
|
1012
|
+
|
|
1013
|
+
constructor(configService: ConfigService) {
|
|
1014
|
+
this.configService = configService;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async onModuleInit(): Promise<void> {
|
|
1018
|
+
// At this point configService should already be injected
|
|
1019
|
+
depValueInInit = this.configService.getPort();
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
registerDependencies(ServerService, [ConfigService]);
|
|
1024
|
+
|
|
1025
|
+
@Module({
|
|
1026
|
+
providers: [ConfigService, ServerService],
|
|
1027
|
+
})
|
|
1028
|
+
class TestModule {}
|
|
1029
|
+
|
|
1030
|
+
const module = new ModuleClass(TestModule, mockLoggerLayer);
|
|
1031
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
1032
|
+
|
|
1033
|
+
expect(depValueInInit).not.toBeNull();
|
|
1034
|
+
expect(depValueInInit as unknown as number).toBe(8080);
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
|
|
856
1038
|
describe('Module DI scoping (exports only for cross-module)', () => {
|
|
857
1039
|
const {
|
|
858
1040
|
Controller: ControllerDecorator,
|
package/src/module/module.ts
CHANGED
|
@@ -555,7 +555,10 @@ export class OneBunModule implements ModuleInstance {
|
|
|
555
555
|
}
|
|
556
556
|
|
|
557
557
|
/**
|
|
558
|
-
* Call onModuleInit lifecycle hook for all services that implement it
|
|
558
|
+
* Call onModuleInit lifecycle hook for all services that implement it.
|
|
559
|
+
* Hooks are called sequentially in dependency order (dependencies first),
|
|
560
|
+
* so each service's onModuleInit completes before its dependents' onModuleInit starts.
|
|
561
|
+
* This is called for ALL services in providers, even if they are not injected anywhere.
|
|
559
562
|
*/
|
|
560
563
|
callServicesOnModuleInit(): Effect.Effect<unknown, never, void> {
|
|
561
564
|
if (this.pendingServiceInits.length === 0) {
|
|
@@ -564,25 +567,23 @@ export class OneBunModule implements ModuleInstance {
|
|
|
564
567
|
|
|
565
568
|
this.logger.debug(`Calling onModuleInit for ${this.pendingServiceInits.length} service(s)`);
|
|
566
569
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
570
|
+
return Effect.promise(async () => {
|
|
571
|
+
// Run onModuleInit hooks sequentially in dependency order
|
|
572
|
+
// (pendingServiceInits is already ordered: dependencies first)
|
|
573
|
+
for (const { name, instance } of this.pendingServiceInits) {
|
|
574
|
+
try {
|
|
575
|
+
if (hasOnModuleInit(instance)) {
|
|
576
|
+
await instance.onModuleInit();
|
|
577
|
+
}
|
|
578
|
+
this.logger.debug(`Service ${name} onModuleInit completed`);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
this.logger.error(`Service ${name} onModuleInit failed: ${error}`);
|
|
581
|
+
throw error;
|
|
572
582
|
}
|
|
573
|
-
this.logger.debug(`Service ${name} onModuleInit completed`);
|
|
574
|
-
} catch (error) {
|
|
575
|
-
this.logger.error(`Service ${name} onModuleInit failed: ${error}`);
|
|
576
|
-
throw error;
|
|
577
583
|
}
|
|
584
|
+
// Clear the list after initialization
|
|
585
|
+
this.pendingServiceInits = [];
|
|
578
586
|
});
|
|
579
|
-
|
|
580
|
-
return Effect.promise(() => Promise.all(initPromises)).pipe(
|
|
581
|
-
Effect.map(() => {
|
|
582
|
-
// Clear the list after initialization
|
|
583
|
-
this.pendingServiceInits = [];
|
|
584
|
-
}),
|
|
585
|
-
);
|
|
586
587
|
}
|
|
587
588
|
|
|
588
589
|
/**
|