@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.1.19",
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.6",
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
- // Check for WebSocket upgrade or Socket.IO polling
559
- if (upgradeHeader === 'websocket' || path.startsWith('/socket.io')) {
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(`WebSocket server enabled at ws://${this.options.host}:${this.options.port}`);
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
- pingInterval: 25000,
2887
- pingTimeout: 20000,
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
- pingInterval: 25000,
3165
- pingTimeout: 20000,
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 { 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
  });
@@ -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
- // Services are already created in initModule via createServicesWithDI
390
- // Just need to set up controllers with DI
391
-
392
- // Get module metadata to access providers for controller dependency registration
393
- const moduleMetadata = getModuleMetadata(this.moduleClass);
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
- // Automatically analyze and register dependencies for all controllers
425
- for (const controllerClass of this.controllers) {
426
- registerControllerDependencies(controllerClass, availableServices);
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
- // Now create controller instances with automatic dependency injection
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
- // Then create controller instances
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
- return this.controllers;
761
+ const fromChildren = this.childModules.flatMap((child) => child.getControllers());
762
+
763
+ return [...this.controllers, ...fromChildren];
787
764
  }
788
765
 
789
766
  /**
790
- * Get controller instance
767
+ * Find controller instance (searches this module then child modules recursively, no logging).
791
768
  */
792
- getControllerInstance(controllerClass: Function): Controller | undefined {
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
- return this.controllerInstances;
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
  }
@@ -41,6 +41,7 @@ function createClientData(id: string, rooms: string[] = []): WsClientData {
41
41
  connectedAt: Date.now(),
42
42
  auth: null,
43
43
  metadata: {},
44
+ protocol: 'native',
44
45
  };
45
46
  }
46
47
 
@@ -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
- const message: WsMessage = { event, data };
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(message);
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(message);
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(message);
371
+ socket.send(this._encodeMessage(socket.data.protocol, event, data));
367
372
  }
368
373
  }
369
374
  }