@onebun/core 0.1.0 → 0.1.2
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/README.md +277 -3
- package/package.json +13 -2
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +2919 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles WebSocket connections and message routing for gateways.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
8
|
+
/* eslint-disable no-magic-numbers */
|
|
9
|
+
|
|
10
|
+
import type { WsStorageAdapter } from './ws-storage';
|
|
11
|
+
import type {
|
|
12
|
+
WsClientData,
|
|
13
|
+
WsHandlerMetadata,
|
|
14
|
+
WebSocketApplicationOptions,
|
|
15
|
+
} from './ws.types';
|
|
16
|
+
import type { Server, ServerWebSocket } from 'bun';
|
|
17
|
+
|
|
18
|
+
import type { SyncLogger } from '@onebun/logger';
|
|
19
|
+
|
|
20
|
+
import { BaseWebSocketGateway } from './ws-base-gateway';
|
|
21
|
+
import { getGatewayMetadata, isWebSocketGateway } from './ws-decorators';
|
|
22
|
+
import { WsExecutionContextImpl, executeGuards } from './ws-guards';
|
|
23
|
+
import { matchPattern } from './ws-pattern-matcher';
|
|
24
|
+
import {
|
|
25
|
+
parseMessage,
|
|
26
|
+
createOpenPacket,
|
|
27
|
+
createHandshake,
|
|
28
|
+
createPongPacket,
|
|
29
|
+
createFullAckMessage,
|
|
30
|
+
createFullEventMessage,
|
|
31
|
+
EngineIOPacketType,
|
|
32
|
+
SocketIOPacketType,
|
|
33
|
+
isNativeMessage,
|
|
34
|
+
parseNativeMessage,
|
|
35
|
+
createNativeMessage,
|
|
36
|
+
DEFAULT_PING_INTERVAL,
|
|
37
|
+
DEFAULT_PING_TIMEOUT,
|
|
38
|
+
DEFAULT_MAX_PAYLOAD,
|
|
39
|
+
} from './ws-socketio-protocol';
|
|
40
|
+
import { InMemoryWsStorage } from './ws-storage-memory';
|
|
41
|
+
import {
|
|
42
|
+
WsHandlerType,
|
|
43
|
+
WsParamType,
|
|
44
|
+
isWsHandlerResponse,
|
|
45
|
+
} from './ws.types';
|
|
46
|
+
|
|
47
|
+
// Alias for clarity
|
|
48
|
+
const ParamType = WsParamType;
|
|
49
|
+
const HandlerType = WsHandlerType;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gateway instance with metadata
|
|
54
|
+
*/
|
|
55
|
+
interface GatewayInstance {
|
|
56
|
+
instance: BaseWebSocketGateway;
|
|
57
|
+
metadata: ReturnType<typeof getGatewayMetadata>;
|
|
58
|
+
handlers: Map<WsHandlerType, WsHandlerMetadata[]>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* WebSocket handler for OneBunApplication
|
|
63
|
+
*/
|
|
64
|
+
export class WsHandler {
|
|
65
|
+
private storage: WsStorageAdapter;
|
|
66
|
+
private gateways: Map<string, GatewayInstance> = new Map();
|
|
67
|
+
private pingIntervalMs: number;
|
|
68
|
+
private pingTimeoutMs: number;
|
|
69
|
+
private maxPayload: number;
|
|
70
|
+
private pingIntervals: Map<string, ReturnType<typeof setInterval>> = new Map();
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
private logger: SyncLogger,
|
|
74
|
+
private options: WebSocketApplicationOptions = {},
|
|
75
|
+
) {
|
|
76
|
+
this.storage = new InMemoryWsStorage();
|
|
77
|
+
this.pingIntervalMs = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
78
|
+
this.pingTimeoutMs = options.pingTimeout ?? DEFAULT_PING_TIMEOUT;
|
|
79
|
+
this.maxPayload = options.maxPayload ?? DEFAULT_MAX_PAYLOAD;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Set storage adapter
|
|
84
|
+
*/
|
|
85
|
+
setStorage(storage: WsStorageAdapter): void {
|
|
86
|
+
this.storage = storage;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a gateway instance
|
|
91
|
+
*/
|
|
92
|
+
registerGateway(gatewayClass: Function, instance: BaseWebSocketGateway): void {
|
|
93
|
+
const metadata = getGatewayMetadata(gatewayClass);
|
|
94
|
+
if (!metadata) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Group handlers by type
|
|
99
|
+
const handlers = new Map<WsHandlerType, WsHandlerMetadata[]>();
|
|
100
|
+
for (const type of Object.values(WsHandlerType)) {
|
|
101
|
+
handlers.set(type as WsHandlerType, []);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const handler of metadata.handlers) {
|
|
105
|
+
const typeHandlers = handlers.get(handler.type) || [];
|
|
106
|
+
typeHandlers.push(handler);
|
|
107
|
+
handlers.set(handler.type, typeHandlers);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const key = metadata.namespace ? `${metadata.path}:${metadata.namespace}` : metadata.path;
|
|
111
|
+
this.gateways.set(key, { instance, metadata, handlers });
|
|
112
|
+
|
|
113
|
+
this.logger.debug(`Registered WebSocket gateway: ${gatewayClass.name} at ${metadata.path}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Initialize gateways with server
|
|
118
|
+
*/
|
|
119
|
+
initializeGateways(server: Server): void {
|
|
120
|
+
for (const [_, gateway] of this.gateways) {
|
|
121
|
+
gateway.instance._initialize(this.storage, server);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if there are any registered gateways
|
|
127
|
+
*/
|
|
128
|
+
hasGateways(): boolean {
|
|
129
|
+
return this.gateways.size > 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get gateway for a path
|
|
134
|
+
*/
|
|
135
|
+
private getGatewayForPath(path: string, namespace?: string): GatewayInstance | undefined {
|
|
136
|
+
// Try exact match with namespace
|
|
137
|
+
if (namespace) {
|
|
138
|
+
const key = `${path}:${namespace}`;
|
|
139
|
+
if (this.gateways.has(key)) {
|
|
140
|
+
return this.gateways.get(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Try exact path match
|
|
145
|
+
if (this.gateways.has(path)) {
|
|
146
|
+
return this.gateways.get(path);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Try prefix match
|
|
150
|
+
for (const [_, gateway] of this.gateways) {
|
|
151
|
+
if (gateway.metadata && path.startsWith(gateway.metadata.path)) {
|
|
152
|
+
return gateway;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create WebSocket handlers for Bun.serve
|
|
161
|
+
*/
|
|
162
|
+
createWebSocketHandlers(): {
|
|
163
|
+
open: (ws: ServerWebSocket<WsClientData>) => void;
|
|
164
|
+
message: (ws: ServerWebSocket<WsClientData>, message: string | Buffer) => void;
|
|
165
|
+
close: (ws: ServerWebSocket<WsClientData>, code: number, reason: string) => void;
|
|
166
|
+
drain: (ws: ServerWebSocket<WsClientData>) => void;
|
|
167
|
+
} {
|
|
168
|
+
return {
|
|
169
|
+
open: (ws) => this.handleOpen(ws),
|
|
170
|
+
message: (ws, message) => this.handleMessage(ws, message),
|
|
171
|
+
close: (ws, code, reason) => this.handleClose(ws, code, reason),
|
|
172
|
+
drain: (ws) => this.handleDrain(ws),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle WebSocket upgrade request
|
|
178
|
+
*/
|
|
179
|
+
async handleUpgrade(
|
|
180
|
+
req: Request,
|
|
181
|
+
server: Server,
|
|
182
|
+
): Promise<Response | undefined> {
|
|
183
|
+
const url = new URL(req.url);
|
|
184
|
+
const path = url.pathname;
|
|
185
|
+
|
|
186
|
+
// Find matching gateway
|
|
187
|
+
const gateway = this.getGatewayForPath(path);
|
|
188
|
+
if (!gateway) {
|
|
189
|
+
return new Response('Not Found', { status: 404 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Extract auth from query or headers
|
|
193
|
+
const token = url.searchParams.get('token') ||
|
|
194
|
+
req.headers.get('Authorization')?.replace('Bearer ', '');
|
|
195
|
+
|
|
196
|
+
// Create client ID
|
|
197
|
+
const clientId = crypto.randomUUID();
|
|
198
|
+
|
|
199
|
+
// Create client data
|
|
200
|
+
const clientData: WsClientData = {
|
|
201
|
+
id: clientId,
|
|
202
|
+
rooms: [],
|
|
203
|
+
connectedAt: Date.now(),
|
|
204
|
+
auth: token ? {
|
|
205
|
+
authenticated: false,
|
|
206
|
+
token,
|
|
207
|
+
} : null,
|
|
208
|
+
metadata: {},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Try to upgrade
|
|
212
|
+
const success = server.upgrade(req, {
|
|
213
|
+
data: clientData,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (success) {
|
|
217
|
+
return undefined; // Bun handles the 101 response
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return new Response('Upgrade failed', { status: 400 });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle new WebSocket connection
|
|
225
|
+
*/
|
|
226
|
+
private async handleOpen(ws: ServerWebSocket<WsClientData>): Promise<void> {
|
|
227
|
+
const client = ws.data;
|
|
228
|
+
this.logger.debug(`WebSocket client connected: ${client.id}`);
|
|
229
|
+
|
|
230
|
+
// Store client
|
|
231
|
+
await this.storage.addClient(client);
|
|
232
|
+
|
|
233
|
+
// Register socket in gateway
|
|
234
|
+
for (const [_, gateway] of this.gateways) {
|
|
235
|
+
gateway.instance._registerSocket(client.id, ws);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Send Socket.IO handshake
|
|
239
|
+
const handshake = createHandshake(client.id, {
|
|
240
|
+
pingInterval: this.pingIntervalMs,
|
|
241
|
+
pingTimeout: this.pingTimeoutMs,
|
|
242
|
+
maxPayload: this.maxPayload,
|
|
243
|
+
});
|
|
244
|
+
ws.send(createOpenPacket(handshake));
|
|
245
|
+
|
|
246
|
+
// Start ping interval
|
|
247
|
+
this.startPingInterval(client.id, ws);
|
|
248
|
+
|
|
249
|
+
// Call OnConnect handlers
|
|
250
|
+
for (const [_, gateway] of this.gateways) {
|
|
251
|
+
const handlers = gateway.handlers.get(HandlerType.CONNECT) || [];
|
|
252
|
+
for (const handler of handlers) {
|
|
253
|
+
try {
|
|
254
|
+
const result = await this.executeHandler(gateway, handler, ws, undefined, {});
|
|
255
|
+
if (result && isWsHandlerResponse(result)) {
|
|
256
|
+
ws.send(createNativeMessage(result.event, result.data));
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.logger.error(`Error in OnConnect handler: ${error}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Handle incoming message
|
|
267
|
+
*/
|
|
268
|
+
private async handleMessage(
|
|
269
|
+
ws: ServerWebSocket<WsClientData>,
|
|
270
|
+
message: string | Buffer,
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
const messageStr = typeof message === 'string' ? message : message.toString();
|
|
273
|
+
|
|
274
|
+
// Try native format first
|
|
275
|
+
if (isNativeMessage(messageStr)) {
|
|
276
|
+
const native = parseNativeMessage(messageStr);
|
|
277
|
+
if (native) {
|
|
278
|
+
await this.routeMessage(ws, native.event, native.data, native.ack);
|
|
279
|
+
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Parse Socket.IO format
|
|
285
|
+
const { engineIO, socketIO } = parseMessage(messageStr);
|
|
286
|
+
|
|
287
|
+
// Handle Engine.IO packets
|
|
288
|
+
switch (engineIO.type) {
|
|
289
|
+
case EngineIOPacketType.PING:
|
|
290
|
+
ws.send(createPongPacket(engineIO.data as string | undefined));
|
|
291
|
+
|
|
292
|
+
return;
|
|
293
|
+
|
|
294
|
+
case EngineIOPacketType.PONG:
|
|
295
|
+
// Client responded to ping - connection is alive
|
|
296
|
+
return;
|
|
297
|
+
|
|
298
|
+
case EngineIOPacketType.CLOSE:
|
|
299
|
+
ws.close(1000, 'Client requested close');
|
|
300
|
+
|
|
301
|
+
return;
|
|
302
|
+
|
|
303
|
+
case EngineIOPacketType.MESSAGE:
|
|
304
|
+
// Socket.IO packet
|
|
305
|
+
if (socketIO) {
|
|
306
|
+
await this.handleSocketIOPacket(ws, socketIO);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handle Socket.IO packet
|
|
315
|
+
*/
|
|
316
|
+
private async handleSocketIOPacket(
|
|
317
|
+
ws: ServerWebSocket<WsClientData>,
|
|
318
|
+
packet: { type: number; nsp: string; data?: unknown[]; id?: number },
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
switch (packet.type) {
|
|
321
|
+
case SocketIOPacketType.CONNECT:
|
|
322
|
+
// Client connecting to namespace - send CONNECT acknowledgement
|
|
323
|
+
ws.send(createFullEventMessage('connect', { sid: ws.data.id }, packet.nsp));
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case SocketIOPacketType.DISCONNECT:
|
|
327
|
+
// Client disconnecting from namespace
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case SocketIOPacketType.EVENT:
|
|
331
|
+
// Handle event
|
|
332
|
+
if (packet.data && Array.isArray(packet.data) && packet.data.length > 0) {
|
|
333
|
+
const [event, ...args] = packet.data;
|
|
334
|
+
if (typeof event === 'string') {
|
|
335
|
+
await this.routeMessage(ws, event, args[0], packet.id);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case SocketIOPacketType.ACK:
|
|
341
|
+
// Acknowledgement - not implemented yet
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Route message to appropriate handler
|
|
348
|
+
*/
|
|
349
|
+
private async routeMessage(
|
|
350
|
+
ws: ServerWebSocket<WsClientData>,
|
|
351
|
+
event: string,
|
|
352
|
+
data: unknown,
|
|
353
|
+
ackId?: number,
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
// Check for room join/leave events
|
|
356
|
+
if (event === 'join' || event.startsWith('join:')) {
|
|
357
|
+
await this.handleRoomJoin(ws, event, data, ackId);
|
|
358
|
+
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (event === 'leave' || event.startsWith('leave:')) {
|
|
363
|
+
await this.handleRoomLeave(ws, event, data, ackId);
|
|
364
|
+
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Find matching message handler
|
|
369
|
+
for (const [_, gateway] of this.gateways) {
|
|
370
|
+
const handlers = gateway.handlers.get(HandlerType.MESSAGE) || [];
|
|
371
|
+
|
|
372
|
+
for (const handler of handlers) {
|
|
373
|
+
if (!handler.pattern) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const match = matchPattern(handler.pattern, event);
|
|
378
|
+
if (match.matched) {
|
|
379
|
+
try {
|
|
380
|
+
const result = await this.executeHandler(gateway, handler, ws, data, match.params);
|
|
381
|
+
|
|
382
|
+
// Send response
|
|
383
|
+
if (result !== undefined) {
|
|
384
|
+
if (ackId !== undefined) {
|
|
385
|
+
ws.send(createFullAckMessage(ackId, result));
|
|
386
|
+
} else if (isWsHandlerResponse(result)) {
|
|
387
|
+
ws.send(createNativeMessage(result.event, result.data));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
this.logger.error(`Error in message handler: ${error}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Handle room join
|
|
400
|
+
*/
|
|
401
|
+
private async handleRoomJoin(
|
|
402
|
+
ws: ServerWebSocket<WsClientData>,
|
|
403
|
+
event: string,
|
|
404
|
+
data: unknown,
|
|
405
|
+
ackId?: number,
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
const client = ws.data;
|
|
408
|
+
const roomName = typeof data === 'string' ? data :
|
|
409
|
+
(data as { room?: string })?.room ||
|
|
410
|
+
event.replace('join:', '');
|
|
411
|
+
|
|
412
|
+
// Add to storage
|
|
413
|
+
await this.storage.addClientToRoom(client.id, roomName);
|
|
414
|
+
|
|
415
|
+
// Subscribe to Bun pub/sub
|
|
416
|
+
ws.subscribe(roomName);
|
|
417
|
+
|
|
418
|
+
// Update client data
|
|
419
|
+
if (!client.rooms.includes(roomName)) {
|
|
420
|
+
client.rooms.push(roomName);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Call OnJoinRoom handlers
|
|
424
|
+
for (const [_, gateway] of this.gateways) {
|
|
425
|
+
const handlers = gateway.handlers.get(HandlerType.JOIN_ROOM) || [];
|
|
426
|
+
|
|
427
|
+
for (const handler of handlers) {
|
|
428
|
+
const match = handler.pattern ? matchPattern(handler.pattern, roomName) : { matched: true, params: {} };
|
|
429
|
+
|
|
430
|
+
if (match.matched) {
|
|
431
|
+
try {
|
|
432
|
+
const result = await this.executeHandler(gateway, handler, ws, data, match.params, roomName);
|
|
433
|
+
if (result !== undefined) {
|
|
434
|
+
if (ackId !== undefined) {
|
|
435
|
+
ws.send(createFullAckMessage(ackId, result));
|
|
436
|
+
} else if (isWsHandlerResponse(result)) {
|
|
437
|
+
ws.send(createNativeMessage(result.event, result.data));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this.logger.error(`Error in OnJoinRoom handler: ${error}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Handle room leave
|
|
450
|
+
*/
|
|
451
|
+
private async handleRoomLeave(
|
|
452
|
+
ws: ServerWebSocket<WsClientData>,
|
|
453
|
+
event: string,
|
|
454
|
+
data: unknown,
|
|
455
|
+
ackId?: number,
|
|
456
|
+
): Promise<void> {
|
|
457
|
+
const client = ws.data;
|
|
458
|
+
const roomName = typeof data === 'string' ? data :
|
|
459
|
+
(data as { room?: string })?.room ||
|
|
460
|
+
event.replace('leave:', '');
|
|
461
|
+
|
|
462
|
+
// Remove from storage
|
|
463
|
+
await this.storage.removeClientFromRoom(client.id, roomName);
|
|
464
|
+
|
|
465
|
+
// Unsubscribe from Bun pub/sub
|
|
466
|
+
ws.unsubscribe(roomName);
|
|
467
|
+
|
|
468
|
+
// Update client data
|
|
469
|
+
client.rooms = client.rooms.filter((r) => r !== roomName);
|
|
470
|
+
|
|
471
|
+
// Call OnLeaveRoom handlers
|
|
472
|
+
for (const [_, gateway] of this.gateways) {
|
|
473
|
+
const handlers = gateway.handlers.get(HandlerType.LEAVE_ROOM) || [];
|
|
474
|
+
|
|
475
|
+
for (const handler of handlers) {
|
|
476
|
+
const match = handler.pattern ? matchPattern(handler.pattern, roomName) : { matched: true, params: {} };
|
|
477
|
+
|
|
478
|
+
if (match.matched) {
|
|
479
|
+
try {
|
|
480
|
+
const result = await this.executeHandler(gateway, handler, ws, data, match.params, roomName);
|
|
481
|
+
if (result !== undefined) {
|
|
482
|
+
if (ackId !== undefined) {
|
|
483
|
+
ws.send(createFullAckMessage(ackId, result));
|
|
484
|
+
} else if (isWsHandlerResponse(result)) {
|
|
485
|
+
ws.send(createNativeMessage(result.event, result.data));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
this.logger.error(`Error in OnLeaveRoom handler: ${error}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Execute a handler with parameter injection
|
|
498
|
+
*/
|
|
499
|
+
private async executeHandler(
|
|
500
|
+
gateway: GatewayInstance,
|
|
501
|
+
handler: WsHandlerMetadata,
|
|
502
|
+
ws: ServerWebSocket<WsClientData>,
|
|
503
|
+
data: unknown,
|
|
504
|
+
patternParams: Record<string, string>,
|
|
505
|
+
roomName?: string,
|
|
506
|
+
): Promise<unknown> {
|
|
507
|
+
const client = ws.data;
|
|
508
|
+
const instance = gateway.instance;
|
|
509
|
+
|
|
510
|
+
// Check guards
|
|
511
|
+
if (handler.guards && handler.guards.length > 0) {
|
|
512
|
+
const context = new WsExecutionContextImpl(
|
|
513
|
+
client,
|
|
514
|
+
ws,
|
|
515
|
+
data,
|
|
516
|
+
handler,
|
|
517
|
+
patternParams,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const canActivate = await executeGuards(handler.guards, context);
|
|
521
|
+
if (!canActivate) {
|
|
522
|
+
this.logger.debug(`Guard rejected request for ${handler.handler}`);
|
|
523
|
+
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Build arguments
|
|
529
|
+
const params = handler.params || [];
|
|
530
|
+
const sortedParams = [...params].sort((a, b) => a.index - b.index);
|
|
531
|
+
const args: unknown[] = [];
|
|
532
|
+
|
|
533
|
+
for (const param of sortedParams) {
|
|
534
|
+
switch (param.type) {
|
|
535
|
+
case ParamType.CLIENT:
|
|
536
|
+
args[param.index] = client;
|
|
537
|
+
break;
|
|
538
|
+
|
|
539
|
+
case ParamType.SOCKET:
|
|
540
|
+
args[param.index] = ws;
|
|
541
|
+
break;
|
|
542
|
+
|
|
543
|
+
case ParamType.MESSAGE_DATA:
|
|
544
|
+
if (param.property && typeof data === 'object' && data !== null) {
|
|
545
|
+
args[param.index] = (data as Record<string, unknown>)[param.property];
|
|
546
|
+
} else {
|
|
547
|
+
args[param.index] = data;
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
case ParamType.ROOM_NAME:
|
|
552
|
+
args[param.index] = roomName;
|
|
553
|
+
break;
|
|
554
|
+
|
|
555
|
+
case ParamType.PATTERN_PARAMS:
|
|
556
|
+
args[param.index] = patternParams;
|
|
557
|
+
break;
|
|
558
|
+
|
|
559
|
+
case ParamType.SERVER:
|
|
560
|
+
args[param.index] = gateway.instance.getWsServer();
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Call handler
|
|
566
|
+
const method = (instance as unknown as Record<string, Function>)[handler.handler];
|
|
567
|
+
if (typeof method === 'function') {
|
|
568
|
+
return await method.apply(instance, args);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Handle WebSocket close
|
|
576
|
+
*/
|
|
577
|
+
private async handleClose(
|
|
578
|
+
ws: ServerWebSocket<WsClientData>,
|
|
579
|
+
code: number,
|
|
580
|
+
reason: string,
|
|
581
|
+
): Promise<void> {
|
|
582
|
+
const client = ws.data;
|
|
583
|
+
this.logger.debug(`WebSocket client disconnected: ${client.id} (${code}: ${reason})`);
|
|
584
|
+
|
|
585
|
+
// Stop ping interval
|
|
586
|
+
this.stopPingInterval(client.id);
|
|
587
|
+
|
|
588
|
+
// Call OnDisconnect handlers
|
|
589
|
+
for (const [_, gateway] of this.gateways) {
|
|
590
|
+
const handlers = gateway.handlers.get(HandlerType.DISCONNECT) || [];
|
|
591
|
+
for (const handler of handlers) {
|
|
592
|
+
try {
|
|
593
|
+
await this.executeHandler(gateway, handler, ws, undefined, {});
|
|
594
|
+
} catch (error) {
|
|
595
|
+
this.logger.error(`Error in OnDisconnect handler: ${error}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Unregister socket
|
|
600
|
+
gateway.instance._unregisterSocket(client.id);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Remove from storage
|
|
604
|
+
await this.storage.removeClient(client.id);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Handle WebSocket drain (backpressure)
|
|
609
|
+
*/
|
|
610
|
+
private handleDrain(ws: ServerWebSocket<WsClientData>): void {
|
|
611
|
+
this.logger.debug(`WebSocket drain for client: ${ws.data.id}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Start ping interval for client
|
|
616
|
+
*/
|
|
617
|
+
private startPingInterval(clientId: string, ws: ServerWebSocket<WsClientData>): void {
|
|
618
|
+
const interval = setInterval(() => {
|
|
619
|
+
try {
|
|
620
|
+
// Send Engine.IO ping
|
|
621
|
+
ws.send(String(EngineIOPacketType.PING));
|
|
622
|
+
} catch {
|
|
623
|
+
this.stopPingInterval(clientId);
|
|
624
|
+
}
|
|
625
|
+
}, this.pingIntervalMs);
|
|
626
|
+
|
|
627
|
+
this.pingIntervals.set(clientId, interval);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Stop ping interval for client
|
|
632
|
+
*/
|
|
633
|
+
private stopPingInterval(clientId: string): void {
|
|
634
|
+
const interval = this.pingIntervals.get(clientId);
|
|
635
|
+
if (interval) {
|
|
636
|
+
clearInterval(interval);
|
|
637
|
+
this.pingIntervals.delete(clientId);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Cleanup all resources
|
|
643
|
+
*/
|
|
644
|
+
async cleanup(): Promise<void> {
|
|
645
|
+
// Stop all ping intervals
|
|
646
|
+
for (const [clientId, _] of this.pingIntervals) {
|
|
647
|
+
this.stopPingInterval(clientId);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Clear storage
|
|
651
|
+
await this.storage.clear();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Check if a class is a WebSocket gateway (for use in module)
|
|
657
|
+
*/
|
|
658
|
+
export { isWebSocketGateway };
|