@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
|
@@ -15,7 +15,7 @@ import type { WsServiceDefinition } from './ws-service-definition';
|
|
|
15
15
|
|
|
16
16
|
import { useFakeTimers } from '../testing/test-utils';
|
|
17
17
|
|
|
18
|
-
import { createWsClient } from './ws-client';
|
|
18
|
+
import { createWsClient, createNativeWsClient } from './ws-client';
|
|
19
19
|
import { WsConnectionState } from './ws-client.types';
|
|
20
20
|
import { WsHandlerType } from './ws.types';
|
|
21
21
|
|
|
@@ -114,7 +114,7 @@ describe('WsClient', () => {
|
|
|
114
114
|
it('should create client with default options', () => {
|
|
115
115
|
const definition = createMockDefinition();
|
|
116
116
|
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
expect(client).toBeDefined();
|
|
119
119
|
expect(typeof client.connect).toBe('function');
|
|
120
120
|
expect(typeof client.disconnect).toBe('function');
|
|
@@ -123,6 +123,40 @@ describe('WsClient', () => {
|
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
describe('createNativeWsClient', () => {
|
|
127
|
+
it('should create standalone client without definition', () => {
|
|
128
|
+
const client = createNativeWsClient({ url: 'ws://localhost:3000/chat' });
|
|
129
|
+
|
|
130
|
+
expect(client).toBeDefined();
|
|
131
|
+
expect(typeof client.connect).toBe('function');
|
|
132
|
+
expect(typeof client.disconnect).toBe('function');
|
|
133
|
+
expect(typeof client.isConnected).toBe('function');
|
|
134
|
+
expect(typeof client.getState).toBe('function');
|
|
135
|
+
expect(typeof client.on).toBe('function');
|
|
136
|
+
expect(typeof client.off).toBe('function');
|
|
137
|
+
expect(typeof client.emit).toBe('function');
|
|
138
|
+
expect(typeof client.send).toBe('function');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should connect and use emit/send/on like typed client', async () => {
|
|
142
|
+
const client = createNativeWsClient({ url: 'ws://localhost:3000/chat' });
|
|
143
|
+
await client.connect();
|
|
144
|
+
|
|
145
|
+
const ws = MockWebSocket.getLastInstance();
|
|
146
|
+
expect(ws).toBeDefined();
|
|
147
|
+
expect(client.isConnected()).toBe(true);
|
|
148
|
+
|
|
149
|
+
client.on('welcome', (data) => expect(data).toBeDefined());
|
|
150
|
+
ws?.receiveMessage(JSON.stringify({ event: 'welcome', data: { msg: 'hi' } }));
|
|
151
|
+
|
|
152
|
+
client.send('ping', {});
|
|
153
|
+
expect(ws?.sentMessages.some((m) => m.includes('ping'))).toBe(true);
|
|
154
|
+
|
|
155
|
+
client.disconnect();
|
|
156
|
+
expect(client.isConnected()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
126
160
|
describe('connect', () => {
|
|
127
161
|
it('should connect to WebSocket server', async () => {
|
|
128
162
|
const definition = createMockDefinition();
|
|
@@ -312,20 +346,23 @@ describe('WsClient', () => {
|
|
|
312
346
|
expect(handler).toHaveBeenCalled();
|
|
313
347
|
});
|
|
314
348
|
|
|
315
|
-
it('should handle Engine.IO PING packet', async () => {
|
|
349
|
+
it('should handle Engine.IO PING packet when using Socket.IO protocol', async () => {
|
|
316
350
|
const definition = createMockDefinition();
|
|
317
|
-
const client = createWsClient(definition, {
|
|
318
|
-
|
|
351
|
+
const client = createWsClient(definition, {
|
|
352
|
+
url: 'ws://localhost:3000/socket.io',
|
|
353
|
+
protocol: 'socketio',
|
|
354
|
+
});
|
|
355
|
+
|
|
319
356
|
await client.connect();
|
|
320
|
-
|
|
357
|
+
|
|
321
358
|
const ws = MockWebSocket.getLastInstance();
|
|
322
359
|
if (ws) {
|
|
323
|
-
ws.sentMessages.length = 0;
|
|
324
|
-
|
|
360
|
+
ws.sentMessages.length = 0;
|
|
361
|
+
|
|
325
362
|
// Send PING (Engine.IO packet type 2)
|
|
326
363
|
ws.receiveMessage('2');
|
|
327
364
|
}
|
|
328
|
-
|
|
365
|
+
|
|
329
366
|
// Should respond with PONG (Engine.IO packet type 3)
|
|
330
367
|
expect(ws?.sentMessages).toContain('3');
|
|
331
368
|
});
|
|
@@ -6,27 +6,49 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
|
9
|
+
/* eslint-disable import/order -- type vs value from same path; keep single mixed import */
|
|
9
10
|
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
import {
|
|
12
|
+
type NativeWsClient,
|
|
13
|
+
type PendingRequest,
|
|
14
|
+
type TypedWsClient,
|
|
15
|
+
type WsClient,
|
|
16
|
+
type WsClientEventListeners,
|
|
17
|
+
type WsClientEvent,
|
|
18
|
+
type WsClientOptions,
|
|
19
|
+
type WsEventListener,
|
|
20
|
+
type WsGatewayClient,
|
|
21
|
+
WsConnectionState,
|
|
19
22
|
} from './ws-client.types';
|
|
20
23
|
import type { WsServiceDefinition } from './ws-service-definition';
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
/** Client lifecycle event names (subscriptions go to client, not server events) */
|
|
26
|
+
const WS_CLIENT_EVENTS: WsClientEvent[] = [
|
|
27
|
+
'connect',
|
|
28
|
+
'disconnect',
|
|
29
|
+
'error',
|
|
30
|
+
'reconnect',
|
|
31
|
+
'reconnect_attempt',
|
|
32
|
+
'reconnect_failed',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function isClientEvent(event: string): event is WsClientEvent {
|
|
36
|
+
return WS_CLIENT_EVENTS.includes(event as WsClientEvent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Dummy definition for standalone client (single logical gateway) */
|
|
40
|
+
const NATIVE_WS_DUMMY_DEFINITION: WsServiceDefinition = {
|
|
41
|
+
_module: null,
|
|
42
|
+
_endpoints: [],
|
|
43
|
+
_gateways: new Map([['default', { name: 'default', path: '/', events: new Map() }]]),
|
|
44
|
+
};
|
|
23
45
|
import { matchPattern, isPattern } from './ws-pattern-matcher';
|
|
24
46
|
import {
|
|
25
47
|
parseMessage,
|
|
26
48
|
createPongPacket,
|
|
49
|
+
createFullEventMessage,
|
|
27
50
|
EngineIOPacketType,
|
|
28
51
|
SocketIOPacketType,
|
|
29
|
-
isNativeMessage,
|
|
30
52
|
parseNativeMessage,
|
|
31
53
|
createNativeMessage,
|
|
32
54
|
} from './ws-socketio-protocol';
|
|
@@ -35,6 +57,7 @@ import {
|
|
|
35
57
|
* Default client options
|
|
36
58
|
*/
|
|
37
59
|
const DEFAULT_OPTIONS: Partial<WsClientOptions> = {
|
|
60
|
+
protocol: 'native',
|
|
38
61
|
reconnect: true,
|
|
39
62
|
reconnectInterval: 1000,
|
|
40
63
|
maxReconnectAttempts: 10,
|
|
@@ -85,23 +108,22 @@ class WsClientImpl<TDef extends WsServiceDefinition> implements WsClient<TDef> {
|
|
|
85
108
|
let url = this.options.url;
|
|
86
109
|
const params = new URLSearchParams();
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
const protocol = this.options.protocol ?? 'native';
|
|
112
|
+
if (protocol === 'socketio') {
|
|
113
|
+
params.set('EIO', '4');
|
|
114
|
+
params.set('transport', 'websocket');
|
|
115
|
+
}
|
|
91
116
|
|
|
92
|
-
// Add auth token
|
|
93
117
|
if (this.options.auth?.token) {
|
|
94
118
|
params.set('token', this.options.auth.token);
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
// Add namespace
|
|
98
121
|
if (this.options.namespace) {
|
|
99
122
|
params.set('namespace', this.options.namespace);
|
|
100
123
|
}
|
|
101
124
|
|
|
102
|
-
// Append params to URL
|
|
103
125
|
const separator = url.includes('?') ? '&' : '?';
|
|
104
|
-
url = `${url}${separator}${params.toString()}
|
|
126
|
+
url = params.toString() ? `${url}${separator}${params.toString()}` : url;
|
|
105
127
|
|
|
106
128
|
// Create WebSocket connection
|
|
107
129
|
// Use globalThis.WebSocket for browser compatibility
|
|
@@ -204,23 +226,22 @@ class WsClientImpl<TDef extends WsServiceDefinition> implements WsClient<TDef> {
|
|
|
204
226
|
* Handle incoming message
|
|
205
227
|
*/
|
|
206
228
|
private handleMessage(data: string): void {
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
const protocol = this.options.protocol ?? 'native';
|
|
230
|
+
|
|
231
|
+
if (protocol === 'native') {
|
|
209
232
|
const native = parseNativeMessage(data);
|
|
210
233
|
if (native) {
|
|
211
234
|
this.handleEvent(native.event, native.data, native.ack);
|
|
212
|
-
|
|
213
|
-
return;
|
|
214
235
|
}
|
|
236
|
+
|
|
237
|
+
return;
|
|
215
238
|
}
|
|
216
239
|
|
|
217
|
-
//
|
|
240
|
+
// Socket.IO format
|
|
218
241
|
const { engineIO, socketIO } = parseMessage(data);
|
|
219
242
|
|
|
220
|
-
// Handle Engine.IO packets
|
|
221
243
|
switch (engineIO.type) {
|
|
222
244
|
case EngineIOPacketType.OPEN:
|
|
223
|
-
// Handle handshake
|
|
224
245
|
if (engineIO.data) {
|
|
225
246
|
try {
|
|
226
247
|
const handshake = JSON.parse(engineIO.data as string);
|
|
@@ -239,7 +260,6 @@ class WsClientImpl<TDef extends WsServiceDefinition> implements WsClient<TDef> {
|
|
|
239
260
|
return;
|
|
240
261
|
|
|
241
262
|
case EngineIOPacketType.PONG:
|
|
242
|
-
// Server responded to our ping
|
|
243
263
|
return;
|
|
244
264
|
|
|
245
265
|
case EngineIOPacketType.CLOSE:
|
|
@@ -248,7 +268,6 @@ class WsClientImpl<TDef extends WsServiceDefinition> implements WsClient<TDef> {
|
|
|
248
268
|
return;
|
|
249
269
|
|
|
250
270
|
case EngineIOPacketType.MESSAGE:
|
|
251
|
-
// Socket.IO packet
|
|
252
271
|
if (socketIO) {
|
|
253
272
|
this.handleSocketIOPacket(socketIO);
|
|
254
273
|
}
|
|
@@ -468,8 +487,11 @@ class WsClientImpl<TDef extends WsServiceDefinition> implements WsClient<TDef> {
|
|
|
468
487
|
throw new Error('Not connected');
|
|
469
488
|
}
|
|
470
489
|
|
|
471
|
-
|
|
472
|
-
const message =
|
|
490
|
+
const protocol = this.options.protocol ?? 'native';
|
|
491
|
+
const message =
|
|
492
|
+
protocol === 'socketio'
|
|
493
|
+
? createFullEventMessage(event, data ?? {}, '/', ackId)
|
|
494
|
+
: createNativeMessage(event, data, ackId);
|
|
473
495
|
this.ws.send(message);
|
|
474
496
|
}
|
|
475
497
|
|
|
@@ -626,3 +648,64 @@ export function createWsClient<TDef extends WsServiceDefinition>(
|
|
|
626
648
|
},
|
|
627
649
|
});
|
|
628
650
|
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Create a standalone WebSocket client without a service definition.
|
|
654
|
+
* Uses the same native message format and API (emit, send, on, off) as the typed client.
|
|
655
|
+
* Use in frontend or when you do not want to depend on backend modules.
|
|
656
|
+
*
|
|
657
|
+
* @param options - Client options (url, protocol, auth, reconnect, etc.)
|
|
658
|
+
* @returns Standalone client with connect, disconnect, on, off, emit, send
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* import { createNativeWsClient } from '@onebun/core';
|
|
663
|
+
*
|
|
664
|
+
* const client = createNativeWsClient({
|
|
665
|
+
* url: 'ws://localhost:3000/chat',
|
|
666
|
+
* protocol: 'native',
|
|
667
|
+
* auth: { token: 'xxx' },
|
|
668
|
+
* });
|
|
669
|
+
*
|
|
670
|
+
* await client.connect();
|
|
671
|
+
* client.on('welcome', (data) => console.log(data));
|
|
672
|
+
* client.on('connect', () => console.log('Connected'));
|
|
673
|
+
*
|
|
674
|
+
* await client.emit('chat:message', { text: 'Hello' });
|
|
675
|
+
* client.send('typing', {});
|
|
676
|
+
* client.disconnect();
|
|
677
|
+
* ```
|
|
678
|
+
*/
|
|
679
|
+
export function createNativeWsClient(options: WsClientOptions): NativeWsClient {
|
|
680
|
+
const typed = createWsClient(NATIVE_WS_DUMMY_DEFINITION, options);
|
|
681
|
+
const gateway = (typed as unknown as Record<string, WsGatewayClient>).default;
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
connect: () => typed.connect(),
|
|
685
|
+
disconnect: () => typed.disconnect(),
|
|
686
|
+
isConnected: () => typed.isConnected(),
|
|
687
|
+
getState: () => typed.getState(),
|
|
688
|
+
|
|
689
|
+
on(event: string, listener: WsEventListener | WsClientEventListeners[WsClientEvent]): void {
|
|
690
|
+
if (isClientEvent(event)) {
|
|
691
|
+
typed.on(event, listener as WsClientEventListeners[typeof event]);
|
|
692
|
+
} else {
|
|
693
|
+
gateway.on(event, listener as WsEventListener);
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
off(
|
|
698
|
+
event: string,
|
|
699
|
+
listener?: WsEventListener | WsClientEventListeners[WsClientEvent],
|
|
700
|
+
): void {
|
|
701
|
+
if (isClientEvent(event)) {
|
|
702
|
+
typed.off(event, listener as WsClientEventListeners[typeof event]);
|
|
703
|
+
} else {
|
|
704
|
+
gateway.off(event, listener as WsEventListener);
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
emit: <T = unknown>(event: string, data?: unknown) => gateway.emit<T>(event, data),
|
|
709
|
+
send: (event: string, data?: unknown) => gateway.send(event, data),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import type { WsServiceDefinition, WsGatewayDefinition } from './ws-service-definition';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* WebSocket protocol to use when connecting
|
|
11
|
+
*/
|
|
12
|
+
export type WsClientProtocol = 'native' | 'socketio';
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Options for WebSocket client
|
|
11
16
|
*/
|
|
12
17
|
export interface WsClientOptions {
|
|
13
|
-
/** WebSocket server URL */
|
|
18
|
+
/** WebSocket server URL (for Socket.IO use the server root or socketio path, e.g. ws://host/socket.io) */
|
|
14
19
|
url: string;
|
|
20
|
+
/** Protocol to use (default: 'native') */
|
|
21
|
+
protocol?: WsClientProtocol;
|
|
15
22
|
/** Authentication options */
|
|
16
23
|
auth?: {
|
|
17
24
|
/** Bearer token */
|
|
@@ -127,3 +134,25 @@ export interface PendingRequest {
|
|
|
127
134
|
reject: (error: Error) => void;
|
|
128
135
|
timeout: ReturnType<typeof setTimeout>;
|
|
129
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Standalone WebSocket client (no service definition).
|
|
140
|
+
* Same message format and API as the typed client, but without gateway proxies.
|
|
141
|
+
* Use in frontend or when you do not want to depend on backend module/definitions.
|
|
142
|
+
*/
|
|
143
|
+
export interface NativeWsClient {
|
|
144
|
+
connect(): Promise<void>;
|
|
145
|
+
disconnect(): void;
|
|
146
|
+
isConnected(): boolean;
|
|
147
|
+
getState(): WsConnectionState;
|
|
148
|
+
/** Lifecycle events: connect, disconnect, error, reconnect, reconnect_attempt, reconnect_failed */
|
|
149
|
+
on<E extends WsClientEvent>(event: E, listener: WsClientEventListeners[E]): void;
|
|
150
|
+
/** Server events (event names from your gateway) */
|
|
151
|
+
on<T = unknown>(event: string, listener: WsEventListener<T>): void;
|
|
152
|
+
off<E extends WsClientEvent>(event: E, listener?: WsClientEventListeners[E]): void;
|
|
153
|
+
off(event: string, listener?: WsEventListener): void;
|
|
154
|
+
/** Send event and wait for acknowledgement */
|
|
155
|
+
emit<T = unknown>(event: string, data?: unknown): Promise<T>;
|
|
156
|
+
/** Send event without waiting for response */
|
|
157
|
+
send(event: string, data?: unknown): void;
|
|
158
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
WsHandlerMetadata,
|
|
14
14
|
WebSocketApplicationOptions,
|
|
15
15
|
} from './ws.types';
|
|
16
|
+
import type { WsHandlerResponse } from './ws.types';
|
|
16
17
|
import type { Server, ServerWebSocket } from 'bun';
|
|
17
18
|
|
|
18
19
|
import type { SyncLogger } from '@onebun/logger';
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
createFullEventMessage,
|
|
31
32
|
EngineIOPacketType,
|
|
32
33
|
SocketIOPacketType,
|
|
33
|
-
isNativeMessage,
|
|
34
34
|
parseNativeMessage,
|
|
35
35
|
createNativeMessage,
|
|
36
36
|
DEFAULT_PING_INTERVAL,
|
|
@@ -68,14 +68,19 @@ export class WsHandler {
|
|
|
68
68
|
private pingTimeoutMs: number;
|
|
69
69
|
private maxPayload: number;
|
|
70
70
|
private pingIntervals: Map<string, ReturnType<typeof setInterval>> = new Map();
|
|
71
|
+
private socketioEnabled: boolean;
|
|
72
|
+
private socketioPath: string;
|
|
71
73
|
|
|
72
74
|
constructor(
|
|
73
75
|
private logger: SyncLogger,
|
|
74
76
|
private options: WebSocketApplicationOptions = {},
|
|
75
77
|
) {
|
|
76
78
|
this.storage = new InMemoryWsStorage();
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
+
const socketio = options.socketio;
|
|
80
|
+
this.socketioEnabled = socketio?.enabled ?? false;
|
|
81
|
+
this.socketioPath = socketio?.path ?? '/socket.io';
|
|
82
|
+
this.pingIntervalMs = socketio?.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
83
|
+
this.pingTimeoutMs = socketio?.pingTimeout ?? DEFAULT_PING_TIMEOUT;
|
|
79
84
|
this.maxPayload = options.maxPayload ?? DEFAULT_MAX_PAYLOAD;
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -183,15 +188,20 @@ export class WsHandler {
|
|
|
183
188
|
const url = new URL(req.url);
|
|
184
189
|
const path = url.pathname;
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
191
|
+
let protocol: WsClientData['protocol'] = 'native';
|
|
192
|
+
|
|
193
|
+
if (this.socketioEnabled && path.startsWith(this.socketioPath)) {
|
|
194
|
+
protocol = 'socketio';
|
|
195
|
+
} else {
|
|
196
|
+
const gateway = this.getGatewayForPath(path);
|
|
197
|
+
if (!gateway) {
|
|
198
|
+
return new Response('Not Found', { status: 404 });
|
|
199
|
+
}
|
|
190
200
|
}
|
|
191
201
|
|
|
192
202
|
// Extract auth from query or headers
|
|
193
|
-
const token = url.searchParams.get('token') ||
|
|
194
|
-
|
|
203
|
+
const token = url.searchParams.get('token') ||
|
|
204
|
+
req.headers.get('Authorization')?.replace('Bearer ', '');
|
|
195
205
|
|
|
196
206
|
// Create client ID
|
|
197
207
|
const clientId = crypto.randomUUID();
|
|
@@ -201,11 +211,14 @@ export class WsHandler {
|
|
|
201
211
|
id: clientId,
|
|
202
212
|
rooms: [],
|
|
203
213
|
connectedAt: Date.now(),
|
|
204
|
-
auth: token
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
auth: token
|
|
215
|
+
? {
|
|
216
|
+
authenticated: false,
|
|
217
|
+
token,
|
|
218
|
+
}
|
|
219
|
+
: null,
|
|
208
220
|
metadata: {},
|
|
221
|
+
protocol,
|
|
209
222
|
};
|
|
210
223
|
|
|
211
224
|
// Try to upgrade
|
|
@@ -225,7 +238,7 @@ export class WsHandler {
|
|
|
225
238
|
*/
|
|
226
239
|
private async handleOpen(ws: ServerWebSocket<WsClientData>): Promise<void> {
|
|
227
240
|
const client = ws.data;
|
|
228
|
-
this.logger.debug(`WebSocket client connected: ${client.id}`);
|
|
241
|
+
this.logger.debug(`WebSocket client connected: ${client.id} (${client.protocol})`);
|
|
229
242
|
|
|
230
243
|
// Store client
|
|
231
244
|
await this.storage.addClient(client);
|
|
@@ -235,16 +248,16 @@ export class WsHandler {
|
|
|
235
248
|
gateway.instance._registerSocket(client.id, ws);
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
if (client.protocol === 'socketio') {
|
|
252
|
+
// Send Socket.IO handshake
|
|
253
|
+
const handshake = createHandshake(client.id, {
|
|
254
|
+
pingInterval: this.pingIntervalMs,
|
|
255
|
+
pingTimeout: this.pingTimeoutMs,
|
|
256
|
+
maxPayload: this.maxPayload,
|
|
257
|
+
});
|
|
258
|
+
ws.send(createOpenPacket(handshake));
|
|
259
|
+
this.startPingInterval(client.id, ws);
|
|
260
|
+
}
|
|
248
261
|
|
|
249
262
|
// Call OnConnect handlers
|
|
250
263
|
for (const [_, gateway] of this.gateways) {
|
|
@@ -253,7 +266,7 @@ export class WsHandler {
|
|
|
253
266
|
try {
|
|
254
267
|
const result = await this.executeHandler(gateway, handler, ws, undefined, {});
|
|
255
268
|
if (result && isWsHandlerResponse(result)) {
|
|
256
|
-
ws.send(
|
|
269
|
+
ws.send(this.encodeResponse(client.protocol, result));
|
|
257
270
|
}
|
|
258
271
|
} catch (error) {
|
|
259
272
|
this.logger.error(`Error in OnConnect handler: ${error}`);
|
|
@@ -262,6 +275,25 @@ export class WsHandler {
|
|
|
262
275
|
}
|
|
263
276
|
}
|
|
264
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Encode handler response for the client's protocol
|
|
280
|
+
*/
|
|
281
|
+
private encodeResponse(
|
|
282
|
+
protocol: WsClientData['protocol'],
|
|
283
|
+
result: WsHandlerResponse,
|
|
284
|
+
ackId?: number,
|
|
285
|
+
): string {
|
|
286
|
+
if (protocol === 'socketio') {
|
|
287
|
+
if (ackId !== undefined) {
|
|
288
|
+
return createFullAckMessage(ackId, result);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return createFullEventMessage(result.event, result.data);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return createNativeMessage(result.event, result.data, ackId);
|
|
295
|
+
}
|
|
296
|
+
|
|
265
297
|
/**
|
|
266
298
|
* Handle incoming message
|
|
267
299
|
*/
|
|
@@ -270,21 +302,20 @@ export class WsHandler {
|
|
|
270
302
|
message: string | Buffer,
|
|
271
303
|
): Promise<void> {
|
|
272
304
|
const messageStr = typeof message === 'string' ? message : message.toString();
|
|
305
|
+
const protocol = ws.data.protocol;
|
|
273
306
|
|
|
274
|
-
|
|
275
|
-
if (isNativeMessage(messageStr)) {
|
|
307
|
+
if (protocol === 'native') {
|
|
276
308
|
const native = parseNativeMessage(messageStr);
|
|
277
309
|
if (native) {
|
|
278
310
|
await this.routeMessage(ws, native.event, native.data, native.ack);
|
|
279
|
-
|
|
280
|
-
return;
|
|
281
311
|
}
|
|
312
|
+
|
|
313
|
+
return;
|
|
282
314
|
}
|
|
283
315
|
|
|
284
|
-
//
|
|
316
|
+
// Socket.IO format
|
|
285
317
|
const { engineIO, socketIO } = parseMessage(messageStr);
|
|
286
318
|
|
|
287
|
-
// Handle Engine.IO packets
|
|
288
319
|
switch (engineIO.type) {
|
|
289
320
|
case EngineIOPacketType.PING:
|
|
290
321
|
ws.send(createPongPacket(engineIO.data as string | undefined));
|
|
@@ -292,7 +323,6 @@ export class WsHandler {
|
|
|
292
323
|
return;
|
|
293
324
|
|
|
294
325
|
case EngineIOPacketType.PONG:
|
|
295
|
-
// Client responded to ping - connection is alive
|
|
296
326
|
return;
|
|
297
327
|
|
|
298
328
|
case EngineIOPacketType.CLOSE:
|
|
@@ -301,7 +331,6 @@ export class WsHandler {
|
|
|
301
331
|
return;
|
|
302
332
|
|
|
303
333
|
case EngineIOPacketType.MESSAGE:
|
|
304
|
-
// Socket.IO packet
|
|
305
334
|
if (socketIO) {
|
|
306
335
|
await this.handleSocketIOPacket(ws, socketIO);
|
|
307
336
|
}
|
|
@@ -378,14 +407,10 @@ export class WsHandler {
|
|
|
378
407
|
if (match.matched) {
|
|
379
408
|
try {
|
|
380
409
|
const result = await this.executeHandler(gateway, handler, ws, data, match.params);
|
|
381
|
-
|
|
410
|
+
|
|
382
411
|
// Send response
|
|
383
|
-
if (result !== undefined) {
|
|
384
|
-
|
|
385
|
-
ws.send(createFullAckMessage(ackId, result));
|
|
386
|
-
} else if (isWsHandlerResponse(result)) {
|
|
387
|
-
ws.send(createNativeMessage(result.event, result.data));
|
|
388
|
-
}
|
|
412
|
+
if (result !== undefined && isWsHandlerResponse(result)) {
|
|
413
|
+
ws.send(this.encodeResponse(ws.data.protocol, result, ackId));
|
|
389
414
|
}
|
|
390
415
|
} catch (error) {
|
|
391
416
|
this.logger.error(`Error in message handler: ${error}`);
|
|
@@ -430,12 +455,8 @@ export class WsHandler {
|
|
|
430
455
|
if (match.matched) {
|
|
431
456
|
try {
|
|
432
457
|
const result = await this.executeHandler(gateway, handler, ws, data, match.params, roomName);
|
|
433
|
-
if (result !== undefined) {
|
|
434
|
-
|
|
435
|
-
ws.send(createFullAckMessage(ackId, result));
|
|
436
|
-
} else if (isWsHandlerResponse(result)) {
|
|
437
|
-
ws.send(createNativeMessage(result.event, result.data));
|
|
438
|
-
}
|
|
458
|
+
if (result !== undefined && isWsHandlerResponse(result)) {
|
|
459
|
+
ws.send(this.encodeResponse(ws.data.protocol, result, ackId));
|
|
439
460
|
}
|
|
440
461
|
} catch (error) {
|
|
441
462
|
this.logger.error(`Error in OnJoinRoom handler: ${error}`);
|
|
@@ -478,12 +499,8 @@ export class WsHandler {
|
|
|
478
499
|
if (match.matched) {
|
|
479
500
|
try {
|
|
480
501
|
const result = await this.executeHandler(gateway, handler, ws, data, match.params, roomName);
|
|
481
|
-
if (result !== undefined) {
|
|
482
|
-
|
|
483
|
-
ws.send(createFullAckMessage(ackId, result));
|
|
484
|
-
} else if (isWsHandlerResponse(result)) {
|
|
485
|
-
ws.send(createNativeMessage(result.event, result.data));
|
|
486
|
-
}
|
|
502
|
+
if (result !== undefined && isWsHandlerResponse(result)) {
|
|
503
|
+
ws.send(this.encodeResponse(ws.data.protocol, result, ackId));
|
|
487
504
|
}
|
|
488
505
|
} catch (error) {
|
|
489
506
|
this.logger.error(`Error in OnLeaveRoom handler: ${error}`);
|
|
@@ -28,6 +28,7 @@ describe('InMemoryWsStorage', () => {
|
|
|
28
28
|
connectedAt: Date.now(),
|
|
29
29
|
auth: null,
|
|
30
30
|
metadata: {},
|
|
31
|
+
protocol: 'native',
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it('should add and retrieve a client', async () => {
|
|
@@ -135,6 +136,7 @@ describe('InMemoryWsStorage', () => {
|
|
|
135
136
|
connectedAt: Date.now(),
|
|
136
137
|
auth: null,
|
|
137
138
|
metadata: {},
|
|
139
|
+
protocol: 'native',
|
|
138
140
|
});
|
|
139
141
|
|
|
140
142
|
it('should add client to room', async () => {
|
|
@@ -227,6 +229,7 @@ describe('InMemoryWsStorage', () => {
|
|
|
227
229
|
connectedAt: Date.now(),
|
|
228
230
|
auth: null,
|
|
229
231
|
metadata: {},
|
|
232
|
+
protocol: 'native',
|
|
230
233
|
};
|
|
231
234
|
await storage.addClient(client);
|
|
232
235
|
await storage.createRoom({ name: 'test-room', clientIds: [] });
|