@onebun/core 0.1.20 → 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/application/application.ts +15 -5
- package/src/docs-examples.test.ts +154 -6
- package/src/module/module.test.ts +182 -0
- package/src/module/module.ts +33 -21
- package/src/types.ts +16 -4
- package/src/websocket/ws-base-gateway.test.ts +1 -0
- package/src/websocket/ws-base-gateway.ts +15 -10
- package/src/websocket/ws-client.test.ts +46 -9
- package/src/websocket/ws-client.ts +112 -29
- package/src/websocket/ws-client.types.ts +30 -1
- package/src/websocket/ws-guards.test.ts +1 -0
- package/src/websocket/ws-handler.ts +69 -52
- package/src/websocket/ws-storage-memory.test.ts +3 -0
- package/src/websocket/ws-storage-redis.test.ts +1 -0
- package/src/websocket/ws.types.ts +30 -7
package/package.json
CHANGED
|
@@ -554,15 +554,16 @@ export class OneBunApplication {
|
|
|
554
554
|
// Handle WebSocket upgrade if gateways exist
|
|
555
555
|
if (hasWebSocketGateways && app.wsHandler) {
|
|
556
556
|
const upgradeHeader = req.headers.get('upgrade')?.toLowerCase();
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
557
|
+
const socketioEnabled = app.options.websocket?.socketio?.enabled ?? false;
|
|
558
|
+
const socketioPath = app.options.websocket?.socketio?.path ?? '/socket.io';
|
|
559
|
+
|
|
560
|
+
const isSocketIoPath = socketioEnabled && path.startsWith(socketioPath);
|
|
561
|
+
if (upgradeHeader === 'websocket' || isSocketIoPath) {
|
|
560
562
|
const response = await app.wsHandler.handleUpgrade(req, server);
|
|
561
563
|
if (response === undefined) {
|
|
562
564
|
return undefined; // Successfully upgraded
|
|
563
565
|
}
|
|
564
566
|
|
|
565
|
-
// Return response if upgrade failed
|
|
566
567
|
return response;
|
|
567
568
|
}
|
|
568
569
|
}
|
|
@@ -920,7 +921,16 @@ export class OneBunApplication {
|
|
|
920
921
|
// Initialize WebSocket gateways with server
|
|
921
922
|
if (hasWebSocketGateways && this.wsHandler && this.server) {
|
|
922
923
|
this.wsHandler.initializeGateways(this.server);
|
|
923
|
-
this.logger.info(
|
|
924
|
+
this.logger.info(
|
|
925
|
+
`WebSocket server (native) enabled at ws://${this.options.host}:${this.options.port}`,
|
|
926
|
+
);
|
|
927
|
+
const socketioEnabled = this.options.websocket?.socketio?.enabled ?? false;
|
|
928
|
+
if (socketioEnabled) {
|
|
929
|
+
const sioPath = this.options.websocket?.socketio?.path ?? '/socket.io';
|
|
930
|
+
this.logger.info(
|
|
931
|
+
`WebSocket server (Socket.IO) enabled at ws://${this.options.host}:${this.options.port}${sioPath}`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
924
934
|
}
|
|
925
935
|
|
|
926
936
|
this.logger.info(`Server started on http://${this.options.host}:${this.options.port}`);
|
|
@@ -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)', () => {
|
|
@@ -2905,6 +2999,52 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2905
2999
|
expect(typeof client.disconnect).toBe('function');
|
|
2906
3000
|
expect(typeof client.on).toBe('function');
|
|
2907
3001
|
});
|
|
3002
|
+
|
|
3003
|
+
it('should create client with protocol native (default)', () => {
|
|
3004
|
+
@WebSocketGateway({ path: '/ws' })
|
|
3005
|
+
class WsGateway extends BaseWebSocketGateway {}
|
|
3006
|
+
|
|
3007
|
+
@Module({ controllers: [WsGateway] })
|
|
3008
|
+
class AppModule {}
|
|
3009
|
+
|
|
3010
|
+
const definition = createWsServiceDefinition(AppModule);
|
|
3011
|
+
const client = createWsClient(definition, {
|
|
3012
|
+
url: 'ws://localhost:3000/ws',
|
|
3013
|
+
protocol: 'native',
|
|
3014
|
+
});
|
|
3015
|
+
expect(client).toBeDefined();
|
|
3016
|
+
});
|
|
3017
|
+
|
|
3018
|
+
it('should create client with protocol socketio', () => {
|
|
3019
|
+
@WebSocketGateway({ path: '/ws' })
|
|
3020
|
+
class WsGateway extends BaseWebSocketGateway {}
|
|
3021
|
+
|
|
3022
|
+
@Module({ controllers: [WsGateway] })
|
|
3023
|
+
class AppModule {}
|
|
3024
|
+
|
|
3025
|
+
const definition = createWsServiceDefinition(AppModule);
|
|
3026
|
+
const client = createWsClient(definition, {
|
|
3027
|
+
url: 'ws://localhost:3000/socket.io',
|
|
3028
|
+
protocol: 'socketio',
|
|
3029
|
+
});
|
|
3030
|
+
expect(client).toBeDefined();
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
/**
|
|
3034
|
+
* @source docs/api/websocket.md#standalone-client-no-definition
|
|
3035
|
+
*/
|
|
3036
|
+
it('should create standalone client without definition', () => {
|
|
3037
|
+
const client = createNativeWsClient({
|
|
3038
|
+
url: 'ws://localhost:3000/chat',
|
|
3039
|
+
protocol: 'native',
|
|
3040
|
+
auth: { token: 'xxx' },
|
|
3041
|
+
});
|
|
3042
|
+
expect(client).toBeDefined();
|
|
3043
|
+
expect(typeof client.connect).toBe('function');
|
|
3044
|
+
expect(typeof client.emit).toBe('function');
|
|
3045
|
+
expect(typeof client.send).toBe('function');
|
|
3046
|
+
expect(typeof client.on).toBe('function');
|
|
3047
|
+
});
|
|
2908
3048
|
});
|
|
2909
3049
|
|
|
2910
3050
|
describe('Application Configuration', () => {
|
|
@@ -2918,7 +3058,7 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2918
3058
|
@Module({ controllers: [TestGateway] })
|
|
2919
3059
|
class AppModule {}
|
|
2920
3060
|
|
|
2921
|
-
// From docs: Application Options example
|
|
3061
|
+
// From docs: Application Options example (native + optional Socket.IO)
|
|
2922
3062
|
const app = new OneBunApplication(AppModule, {
|
|
2923
3063
|
port: 3000,
|
|
2924
3064
|
websocket: {
|
|
@@ -2926,8 +3066,12 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2926
3066
|
storage: {
|
|
2927
3067
|
type: 'memory',
|
|
2928
3068
|
},
|
|
2929
|
-
|
|
2930
|
-
|
|
3069
|
+
socketio: {
|
|
3070
|
+
enabled: true,
|
|
3071
|
+
path: '/socket.io',
|
|
3072
|
+
pingInterval: 25000,
|
|
3073
|
+
pingTimeout: 20000,
|
|
3074
|
+
},
|
|
2931
3075
|
maxPayload: 1048576,
|
|
2932
3076
|
},
|
|
2933
3077
|
loggerLayer: makeMockLoggerLayer(),
|
|
@@ -3204,8 +3348,10 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
3204
3348
|
const app = new OneBunApplication(ChatModule, {
|
|
3205
3349
|
port: 3000,
|
|
3206
3350
|
websocket: {
|
|
3207
|
-
|
|
3208
|
-
|
|
3351
|
+
socketio: {
|
|
3352
|
+
pingInterval: 25000,
|
|
3353
|
+
pingTimeout: 20000,
|
|
3354
|
+
},
|
|
3209
3355
|
},
|
|
3210
3356
|
loggerLayer: makeMockLoggerLayer(),
|
|
3211
3357
|
});
|
|
@@ -3240,10 +3386,11 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
3240
3386
|
})
|
|
3241
3387
|
class ChatModule {}
|
|
3242
3388
|
|
|
3243
|
-
// From docs: Typed Client example
|
|
3389
|
+
// From docs: Typed Client (native) example
|
|
3244
3390
|
const definition = createWsServiceDefinition(ChatModule);
|
|
3245
3391
|
const client = createWsClient(definition, {
|
|
3246
3392
|
url: 'ws://localhost:3000/chat',
|
|
3393
|
+
protocol: 'native',
|
|
3247
3394
|
auth: {
|
|
3248
3395
|
token: 'user-jwt-token',
|
|
3249
3396
|
},
|
|
@@ -3323,6 +3470,7 @@ import {
|
|
|
3323
3470
|
SharedRedisProvider,
|
|
3324
3471
|
createWsServiceDefinition,
|
|
3325
3472
|
createWsClient,
|
|
3473
|
+
createNativeWsClient,
|
|
3326
3474
|
matchPattern,
|
|
3327
3475
|
makeMockLoggerLayer,
|
|
3328
3476
|
hasOnModuleInit,
|
|
@@ -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
|
/**
|
|
@@ -764,24 +765,35 @@ export class OneBunModule implements ModuleInstance {
|
|
|
764
765
|
}
|
|
765
766
|
|
|
766
767
|
/**
|
|
767
|
-
*
|
|
768
|
+
* Find controller instance (searches this module then child modules recursively, no logging).
|
|
768
769
|
*/
|
|
769
|
-
|
|
770
|
+
private findControllerInstance(controllerClass: Function): Controller | undefined {
|
|
770
771
|
const instance = this.controllerInstances.get(controllerClass);
|
|
771
772
|
if (instance) {
|
|
772
773
|
return instance;
|
|
773
774
|
}
|
|
774
775
|
for (const childModule of this.childModules) {
|
|
775
|
-
const childInstance = childModule.
|
|
776
|
+
const childInstance = childModule.findControllerInstance(controllerClass);
|
|
776
777
|
if (childInstance) {
|
|
777
778
|
return childInstance;
|
|
778
779
|
}
|
|
779
780
|
}
|
|
780
|
-
this.logger.warn(`No instance found for controller ${controllerClass.name}`);
|
|
781
781
|
|
|
782
782
|
return undefined;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Get controller instance (searches this module then child modules recursively).
|
|
787
|
+
*/
|
|
788
|
+
getControllerInstance(controllerClass: Function): Controller | undefined {
|
|
789
|
+
const instance = this.findControllerInstance(controllerClass);
|
|
790
|
+
if (!instance) {
|
|
791
|
+
this.logger.warn(`No instance found for controller ${controllerClass.name}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return instance;
|
|
795
|
+
}
|
|
796
|
+
|
|
785
797
|
/**
|
|
786
798
|
* Get all controller instances from this module and child modules (recursive).
|
|
787
799
|
*/
|
package/src/types.ts
CHANGED
|
@@ -381,18 +381,30 @@ export interface WsStorageOptions {
|
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Socket.IO-specific options (optional; when enabled, Socket.IO runs on its own path)
|
|
386
|
+
*/
|
|
387
|
+
export interface WebSocketSocketIOOptions {
|
|
388
|
+
/** Enable Socket.IO protocol (default: false) */
|
|
389
|
+
enabled?: boolean;
|
|
390
|
+
/** Path for Socket.IO connections (default: '/socket.io') */
|
|
391
|
+
path?: string;
|
|
392
|
+
/** Ping interval in milliseconds (default: 25000) */
|
|
393
|
+
pingInterval?: number;
|
|
394
|
+
/** Ping timeout in milliseconds (default: 20000) */
|
|
395
|
+
pingTimeout?: number;
|
|
396
|
+
}
|
|
397
|
+
|
|
384
398
|
/**
|
|
385
399
|
* WebSocket configuration for OneBunApplication
|
|
386
400
|
*/
|
|
387
401
|
export interface WebSocketApplicationOptions {
|
|
388
402
|
/** Enable/disable WebSocket (default: auto - enabled if gateways exist) */
|
|
389
403
|
enabled?: boolean;
|
|
404
|
+
/** Socket.IO options; when enabled, Socket.IO is served on socketio.path */
|
|
405
|
+
socketio?: WebSocketSocketIOOptions;
|
|
390
406
|
/** Storage options */
|
|
391
407
|
storage?: WsStorageOptions;
|
|
392
|
-
/** Ping interval in milliseconds for heartbeat (socket.io) */
|
|
393
|
-
pingInterval?: number;
|
|
394
|
-
/** Ping timeout in milliseconds (socket.io) */
|
|
395
|
-
pingTimeout?: number;
|
|
396
408
|
/** Maximum payload size in bytes */
|
|
397
409
|
maxPayload?: number;
|
|
398
410
|
}
|
|
@@ -9,11 +9,11 @@ import type { WsStorageAdapter, WsPubSubStorageAdapter } from './ws-storage';
|
|
|
9
9
|
import type {
|
|
10
10
|
WsClientData,
|
|
11
11
|
WsRoom,
|
|
12
|
-
WsMessage,
|
|
13
12
|
WsServer,
|
|
14
13
|
} from './ws.types';
|
|
15
14
|
import type { Server, ServerWebSocket } from 'bun';
|
|
16
15
|
|
|
16
|
+
import { createFullEventMessage, createNativeMessage } from './ws-socketio-protocol';
|
|
17
17
|
import { WsStorageEvent, isPubSubAdapter } from './ws-storage';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -247,6 +247,16 @@ export abstract class BaseWebSocketGateway {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Encode message for client's protocol
|
|
252
|
+
* @internal
|
|
253
|
+
*/
|
|
254
|
+
private _encodeMessage(protocol: WsClientData['protocol'], event: string, data: unknown): string {
|
|
255
|
+
return protocol === 'socketio'
|
|
256
|
+
? createFullEventMessage(event, data ?? {})
|
|
257
|
+
: createNativeMessage(event, data);
|
|
258
|
+
}
|
|
259
|
+
|
|
250
260
|
/**
|
|
251
261
|
* Send to local client only
|
|
252
262
|
* @internal
|
|
@@ -254,8 +264,7 @@ export abstract class BaseWebSocketGateway {
|
|
|
254
264
|
private _localEmit(clientId: string, event: string, data: unknown): void {
|
|
255
265
|
const socket = clientSockets.get(clientId);
|
|
256
266
|
if (socket) {
|
|
257
|
-
|
|
258
|
-
socket.send(JSON.stringify(message));
|
|
267
|
+
socket.send(this._encodeMessage(socket.data.protocol, event, data));
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
270
|
|
|
@@ -280,12 +289,11 @@ export abstract class BaseWebSocketGateway {
|
|
|
280
289
|
* @internal
|
|
281
290
|
*/
|
|
282
291
|
private _localBroadcast(event: string, data: unknown, excludeClientIds?: string[]): void {
|
|
283
|
-
const message = JSON.stringify({ event, data } as WsMessage);
|
|
284
292
|
const excludeSet = new Set(excludeClientIds || []);
|
|
285
293
|
|
|
286
294
|
for (const [clientId, socket] of clientSockets) {
|
|
287
295
|
if (!excludeSet.has(clientId)) {
|
|
288
|
-
socket.send(
|
|
296
|
+
socket.send(this._encodeMessage(socket.data.protocol, event, data));
|
|
289
297
|
}
|
|
290
298
|
}
|
|
291
299
|
}
|
|
@@ -323,14 +331,13 @@ export abstract class BaseWebSocketGateway {
|
|
|
323
331
|
}
|
|
324
332
|
|
|
325
333
|
const clientIds = await this.storage.getClientsInRoom(roomName);
|
|
326
|
-
const message = JSON.stringify({ event, data } as WsMessage);
|
|
327
334
|
const excludeSet = new Set(excludeClientIds || []);
|
|
328
335
|
|
|
329
336
|
for (const clientId of clientIds) {
|
|
330
337
|
if (!excludeSet.has(clientId)) {
|
|
331
338
|
const socket = clientSockets.get(clientId);
|
|
332
339
|
if (socket) {
|
|
333
|
-
socket.send(
|
|
340
|
+
socket.send(this._encodeMessage(socket.data.protocol, event, data));
|
|
334
341
|
}
|
|
335
342
|
}
|
|
336
343
|
}
|
|
@@ -355,15 +362,13 @@ export abstract class BaseWebSocketGateway {
|
|
|
355
362
|
}
|
|
356
363
|
}
|
|
357
364
|
|
|
358
|
-
// Send to each unique client
|
|
359
|
-
const message = JSON.stringify({ event, data } as WsMessage);
|
|
360
365
|
const excludeSet = new Set(excludeClientIds || []);
|
|
361
366
|
|
|
362
367
|
for (const clientId of clientIdsSet) {
|
|
363
368
|
if (!excludeSet.has(clientId)) {
|
|
364
369
|
const socket = clientSockets.get(clientId);
|
|
365
370
|
if (socket) {
|
|
366
|
-
socket.send(
|
|
371
|
+
socket.send(this._encodeMessage(socket.data.protocol, event, data));
|
|
367
372
|
}
|
|
368
373
|
}
|
|
369
374
|
}
|