@onebun/core 0.1.19 → 0.1.21
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 +2 -2
- package/src/application/application.ts +15 -5
- package/src/docs-examples.test.ts +103 -6
- package/src/module/module.test.ts +146 -2
- package/src/module/module.ts +49 -48
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"effect": "^3.13.10",
|
|
43
43
|
"arktype": "^2.0.0",
|
|
44
|
-
"@onebun/logger": "^0.1.
|
|
44
|
+
"@onebun/logger": "^0.1.7",
|
|
45
45
|
"@onebun/envs": "^0.1.4",
|
|
46
46
|
"@onebun/metrics": "^0.1.6",
|
|
47
47
|
"@onebun/requests": "^0.1.3",
|
|
@@ -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}`);
|
|
@@ -2327,6 +2327,49 @@ describe('Architecture Documentation (docs/architecture.md)', () => {
|
|
|
2327
2327
|
expect(SharedModule).toBeDefined();
|
|
2328
2328
|
expect(ApiModule).toBeDefined();
|
|
2329
2329
|
});
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Exports are only required for cross-module injection.
|
|
2333
|
+
* Within a module, any provider can be injected into controllers without being in exports.
|
|
2334
|
+
*/
|
|
2335
|
+
it('should allow controller to inject same-module provider without exports', async () => {
|
|
2336
|
+
const effectLib = await import('effect');
|
|
2337
|
+
const moduleMod = await import('./module/module');
|
|
2338
|
+
const testUtils = await import('./testing/test-utils');
|
|
2339
|
+
const decorators = await import('./decorators/decorators');
|
|
2340
|
+
|
|
2341
|
+
@Service()
|
|
2342
|
+
class InternalService extends BaseService {
|
|
2343
|
+
getData(): string {
|
|
2344
|
+
return 'internal';
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
@Controller('/local')
|
|
2349
|
+
class LocalController extends BaseController {
|
|
2350
|
+
constructor(@decorators.Inject(InternalService) private readonly internal: InternalService) {
|
|
2351
|
+
super();
|
|
2352
|
+
}
|
|
2353
|
+
getData(): string {
|
|
2354
|
+
return this.internal.getData();
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
@Module({
|
|
2359
|
+
providers: [InternalService],
|
|
2360
|
+
controllers: [LocalController],
|
|
2361
|
+
// No exports - InternalService is only used inside this module
|
|
2362
|
+
})
|
|
2363
|
+
class LocalModule {}
|
|
2364
|
+
|
|
2365
|
+
const mod = new moduleMod.OneBunModule(LocalModule, testUtils.makeMockLoggerLayer());
|
|
2366
|
+
mod.getLayer();
|
|
2367
|
+
await effectLib.Effect.runPromise(mod.setup() as import('effect').Effect.Effect<unknown, never, never>);
|
|
2368
|
+
|
|
2369
|
+
const controller = mod.getControllerInstance(LocalController) as LocalController;
|
|
2370
|
+
expect(controller).toBeDefined();
|
|
2371
|
+
expect(controller.getData()).toBe('internal');
|
|
2372
|
+
});
|
|
2330
2373
|
});
|
|
2331
2374
|
});
|
|
2332
2375
|
|
|
@@ -2862,6 +2905,52 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2862
2905
|
expect(typeof client.disconnect).toBe('function');
|
|
2863
2906
|
expect(typeof client.on).toBe('function');
|
|
2864
2907
|
});
|
|
2908
|
+
|
|
2909
|
+
it('should create client with protocol native (default)', () => {
|
|
2910
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2911
|
+
class WsGateway extends BaseWebSocketGateway {}
|
|
2912
|
+
|
|
2913
|
+
@Module({ controllers: [WsGateway] })
|
|
2914
|
+
class AppModule {}
|
|
2915
|
+
|
|
2916
|
+
const definition = createWsServiceDefinition(AppModule);
|
|
2917
|
+
const client = createWsClient(definition, {
|
|
2918
|
+
url: 'ws://localhost:3000/ws',
|
|
2919
|
+
protocol: 'native',
|
|
2920
|
+
});
|
|
2921
|
+
expect(client).toBeDefined();
|
|
2922
|
+
});
|
|
2923
|
+
|
|
2924
|
+
it('should create client with protocol socketio', () => {
|
|
2925
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2926
|
+
class WsGateway extends BaseWebSocketGateway {}
|
|
2927
|
+
|
|
2928
|
+
@Module({ controllers: [WsGateway] })
|
|
2929
|
+
class AppModule {}
|
|
2930
|
+
|
|
2931
|
+
const definition = createWsServiceDefinition(AppModule);
|
|
2932
|
+
const client = createWsClient(definition, {
|
|
2933
|
+
url: 'ws://localhost:3000/socket.io',
|
|
2934
|
+
protocol: 'socketio',
|
|
2935
|
+
});
|
|
2936
|
+
expect(client).toBeDefined();
|
|
2937
|
+
});
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* @source docs/api/websocket.md#standalone-client-no-definition
|
|
2941
|
+
*/
|
|
2942
|
+
it('should create standalone client without definition', () => {
|
|
2943
|
+
const client = createNativeWsClient({
|
|
2944
|
+
url: 'ws://localhost:3000/chat',
|
|
2945
|
+
protocol: 'native',
|
|
2946
|
+
auth: { token: 'xxx' },
|
|
2947
|
+
});
|
|
2948
|
+
expect(client).toBeDefined();
|
|
2949
|
+
expect(typeof client.connect).toBe('function');
|
|
2950
|
+
expect(typeof client.emit).toBe('function');
|
|
2951
|
+
expect(typeof client.send).toBe('function');
|
|
2952
|
+
expect(typeof client.on).toBe('function');
|
|
2953
|
+
});
|
|
2865
2954
|
});
|
|
2866
2955
|
|
|
2867
2956
|
describe('Application Configuration', () => {
|
|
@@ -2875,7 +2964,7 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2875
2964
|
@Module({ controllers: [TestGateway] })
|
|
2876
2965
|
class AppModule {}
|
|
2877
2966
|
|
|
2878
|
-
// From docs: Application Options example
|
|
2967
|
+
// From docs: Application Options example (native + optional Socket.IO)
|
|
2879
2968
|
const app = new OneBunApplication(AppModule, {
|
|
2880
2969
|
port: 3000,
|
|
2881
2970
|
websocket: {
|
|
@@ -2883,8 +2972,12 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2883
2972
|
storage: {
|
|
2884
2973
|
type: 'memory',
|
|
2885
2974
|
},
|
|
2886
|
-
|
|
2887
|
-
|
|
2975
|
+
socketio: {
|
|
2976
|
+
enabled: true,
|
|
2977
|
+
path: '/socket.io',
|
|
2978
|
+
pingInterval: 25000,
|
|
2979
|
+
pingTimeout: 20000,
|
|
2980
|
+
},
|
|
2888
2981
|
maxPayload: 1048576,
|
|
2889
2982
|
},
|
|
2890
2983
|
loggerLayer: makeMockLoggerLayer(),
|
|
@@ -3161,8 +3254,10 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
3161
3254
|
const app = new OneBunApplication(ChatModule, {
|
|
3162
3255
|
port: 3000,
|
|
3163
3256
|
websocket: {
|
|
3164
|
-
|
|
3165
|
-
|
|
3257
|
+
socketio: {
|
|
3258
|
+
pingInterval: 25000,
|
|
3259
|
+
pingTimeout: 20000,
|
|
3260
|
+
},
|
|
3166
3261
|
},
|
|
3167
3262
|
loggerLayer: makeMockLoggerLayer(),
|
|
3168
3263
|
});
|
|
@@ -3197,10 +3292,11 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
3197
3292
|
})
|
|
3198
3293
|
class ChatModule {}
|
|
3199
3294
|
|
|
3200
|
-
// From docs: Typed Client example
|
|
3295
|
+
// From docs: Typed Client (native) example
|
|
3201
3296
|
const definition = createWsServiceDefinition(ChatModule);
|
|
3202
3297
|
const client = createWsClient(definition, {
|
|
3203
3298
|
url: 'ws://localhost:3000/chat',
|
|
3299
|
+
protocol: 'native',
|
|
3204
3300
|
auth: {
|
|
3205
3301
|
token: 'user-jwt-token',
|
|
3206
3302
|
},
|
|
@@ -3280,6 +3376,7 @@ import {
|
|
|
3280
3376
|
SharedRedisProvider,
|
|
3281
3377
|
createWsServiceDefinition,
|
|
3282
3378
|
createWsClient,
|
|
3379
|
+
createNativeWsClient,
|
|
3283
3380
|
matchPattern,
|
|
3284
3381
|
makeMockLoggerLayer,
|
|
3285
3382
|
hasOnModuleInit,
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
afterEach,
|
|
11
11
|
mock,
|
|
12
12
|
} from 'bun:test';
|
|
13
|
-
import {
|
|
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
|
});
|
package/src/module/module.ts
CHANGED
|
@@ -169,9 +169,6 @@ export class OneBunModule implements ModuleInstance {
|
|
|
169
169
|
// Merge layers
|
|
170
170
|
layer = Layer.merge(layer, childModule.getLayer());
|
|
171
171
|
|
|
172
|
-
// Add controllers from child module
|
|
173
|
-
controllers.push(...childModule.getControllers());
|
|
174
|
-
|
|
175
172
|
// Get exported services from child module and register them for DI
|
|
176
173
|
const exportedServices = childModule.getExportedServices();
|
|
177
174
|
for (const [tag, instance] of exportedServices) {
|
|
@@ -386,48 +383,20 @@ export class OneBunModule implements ModuleInstance {
|
|
|
386
383
|
*/
|
|
387
384
|
createControllerInstances(): Effect.Effect<unknown, never, void> {
|
|
388
385
|
return Effect.sync(() => {
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (moduleMetadata && moduleMetadata.providers) {
|
|
395
|
-
// Create map of available services for dependency resolution
|
|
396
|
-
const availableServices = new Map<string, Function>();
|
|
397
|
-
|
|
398
|
-
// For each provider that is a class constructor, add to available services map
|
|
399
|
-
for (const provider of moduleMetadata.providers) {
|
|
400
|
-
if (typeof provider === 'function') {
|
|
401
|
-
availableServices.set(provider.name, provider);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Also add services from imported modules
|
|
406
|
-
for (const childModule of this.childModules) {
|
|
407
|
-
const childMetadata = getModuleMetadata(childModule.moduleClass);
|
|
408
|
-
if (childMetadata?.exports) {
|
|
409
|
-
for (const exported of childMetadata.exports) {
|
|
410
|
-
if (typeof exported === 'function') {
|
|
411
|
-
availableServices.set(exported.name, exported);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Add global services to available services for dependency resolution
|
|
418
|
-
for (const [, instance] of globalServicesRegistry) {
|
|
419
|
-
if (instance && typeof instance === 'object') {
|
|
420
|
-
availableServices.set(instance.constructor.name, instance.constructor);
|
|
421
|
-
}
|
|
386
|
+
// Build map of available services from this module's DI scope (own providers + imported exports + global)
|
|
387
|
+
const availableServices = new Map<string, Function>();
|
|
388
|
+
for (const [, instance] of this.serviceInstances) {
|
|
389
|
+
if (instance && typeof instance === 'object') {
|
|
390
|
+
availableServices.set(instance.constructor.name, instance.constructor);
|
|
422
391
|
}
|
|
392
|
+
}
|
|
423
393
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
394
|
+
// Automatically analyze and register dependencies for all controllers of this module
|
|
395
|
+
for (const controllerClass of this.controllers) {
|
|
396
|
+
registerControllerDependencies(controllerClass, availableServices);
|
|
428
397
|
}
|
|
429
398
|
|
|
430
|
-
//
|
|
399
|
+
// Create controller instances with automatic dependency injection
|
|
431
400
|
this.createControllersWithDI();
|
|
432
401
|
}).pipe(Effect.provide(this.rootLayer));
|
|
433
402
|
}
|
|
@@ -567,7 +536,12 @@ export class OneBunModule implements ModuleInstance {
|
|
|
567
536
|
discard: true,
|
|
568
537
|
}),
|
|
569
538
|
),
|
|
570
|
-
//
|
|
539
|
+
// Create controller instances in child modules first, then this module (each uses its own DI scope)
|
|
540
|
+
Effect.flatMap(() =>
|
|
541
|
+
Effect.forEach(this.childModules, (childModule) => childModule.createControllerInstances(), {
|
|
542
|
+
discard: true,
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
571
545
|
Effect.flatMap(() => this.createControllerInstances()),
|
|
572
546
|
// Then call onModuleInit for controllers
|
|
573
547
|
Effect.flatMap(() => this.callControllersOnModuleInit()),
|
|
@@ -780,18 +754,38 @@ export class OneBunModule implements ModuleInstance {
|
|
|
780
754
|
}
|
|
781
755
|
|
|
782
756
|
/**
|
|
783
|
-
* Get all controllers from this module
|
|
757
|
+
* Get all controllers from this module and child modules (recursive).
|
|
758
|
+
* Used by the application layer for routing and lifecycle.
|
|
784
759
|
*/
|
|
785
760
|
getControllers(): Function[] {
|
|
786
|
-
|
|
761
|
+
const fromChildren = this.childModules.flatMap((child) => child.getControllers());
|
|
762
|
+
|
|
763
|
+
return [...this.controllers, ...fromChildren];
|
|
787
764
|
}
|
|
788
765
|
|
|
789
766
|
/**
|
|
790
|
-
*
|
|
767
|
+
* Find controller instance (searches this module then child modules recursively, no logging).
|
|
791
768
|
*/
|
|
792
|
-
|
|
769
|
+
private findControllerInstance(controllerClass: Function): Controller | undefined {
|
|
793
770
|
const instance = this.controllerInstances.get(controllerClass);
|
|
771
|
+
if (instance) {
|
|
772
|
+
return instance;
|
|
773
|
+
}
|
|
774
|
+
for (const childModule of this.childModules) {
|
|
775
|
+
const childInstance = childModule.findControllerInstance(controllerClass);
|
|
776
|
+
if (childInstance) {
|
|
777
|
+
return childInstance;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
794
783
|
|
|
784
|
+
/**
|
|
785
|
+
* Get controller instance (searches this module then child modules recursively).
|
|
786
|
+
*/
|
|
787
|
+
getControllerInstance(controllerClass: Function): Controller | undefined {
|
|
788
|
+
const instance = this.findControllerInstance(controllerClass);
|
|
795
789
|
if (!instance) {
|
|
796
790
|
this.logger.warn(`No instance found for controller ${controllerClass.name}`);
|
|
797
791
|
}
|
|
@@ -800,10 +794,17 @@ export class OneBunModule implements ModuleInstance {
|
|
|
800
794
|
}
|
|
801
795
|
|
|
802
796
|
/**
|
|
803
|
-
* Get all controller instances
|
|
797
|
+
* Get all controller instances from this module and child modules (recursive).
|
|
804
798
|
*/
|
|
805
799
|
getControllerInstances(): Map<Function, Controller> {
|
|
806
|
-
|
|
800
|
+
const merged = new Map<Function, Controller>(this.controllerInstances);
|
|
801
|
+
for (const childModule of this.childModules) {
|
|
802
|
+
for (const [cls, instance] of childModule.getControllerInstances()) {
|
|
803
|
+
merged.set(cls, instance);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return merged;
|
|
807
808
|
}
|
|
808
809
|
|
|
809
810
|
/**
|
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
|
}
|