@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Core package for OneBun framework - decorators, DI, modules, controllers",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -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}`);
@@ -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
- pingInterval: 25000,
2930
- pingTimeout: 20000,
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
- pingInterval: 25000,
3208
- pingTimeout: 20000,
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,
@@ -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
- // Run all service onModuleInit hooks sequentially
568
- const initPromises = this.pendingServiceInits.map(async ({ name, instance }) => {
569
- try {
570
- if (hasOnModuleInit(instance)) {
571
- await instance.onModuleInit();
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
- * Get controller instance (searches this module then child modules recursively).
768
+ * Find controller instance (searches this module then child modules recursively, no logging).
768
769
  */
769
- getControllerInstance(controllerClass: Function): Controller | undefined {
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.getControllerInstance(controllerClass);
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
  }
@@ -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
  }