UNPKG

@colyseus/ws-transport

Version:

```typescript import { Server } from "@colyseus/core"; import { WebSocketTransport } from "@colyseus/ws-transport";

8 lines (7 loc) 7.77 kB
{ "version": 3, "sources": ["../src/WebSocketTransport.ts"], "sourcesContent": ["import http from 'http';\nimport { URL } from 'url';\nimport WebSocket, { type ServerOptions, WebSocketServer } from 'ws';\n\nimport { matchMaker, Protocol, Transport, debugAndPrintError, debugConnection, getBearerToken } from '@colyseus/core';\nimport { WebSocketClient } from './WebSocketClient.js';\n\nfunction noop() {/* tslint:disable:no-empty */ }\nfunction heartbeat() { this.pingCount = 0; }\n\ntype RawWebSocketClient = WebSocket & { pingCount: number };\n\nexport interface TransportOptions extends ServerOptions {\n pingInterval?: number;\n pingMaxRetries?: number;\n}\n\nexport class WebSocketTransport extends Transport {\n protected wss: WebSocketServer;\n\n protected pingInterval: NodeJS.Timeout;\n protected pingIntervalMS: number;\n protected pingMaxRetries: number;\n\n private _originalSend: typeof WebSocketClient.prototype.raw | null = null;\n\n constructor(options: TransportOptions = {}) {\n super();\n\n if (options.maxPayload === undefined) {\n options.maxPayload = 4 * 1024; // 4Kb\n }\n\n // disable per-message deflate by default\n if (options.perMessageDeflate === undefined) {\n options.perMessageDeflate = false;\n }\n\n this.pingIntervalMS = (options.pingInterval !== undefined)\n ? options.pingInterval\n : 3000;\n\n this.pingMaxRetries = (options.pingMaxRetries !== undefined)\n ? options.pingMaxRetries\n : 2;\n\n // create server by default\n if (!options.server && !options.noServer) {\n options.server = http.createServer();\n }\n\n this.wss = new WebSocketServer(options);\n this.wss.on('connection', this.onConnection);\n\n // this is required to allow the ECONNRESET error to trigger on the `server` instance.\n this.wss.on('error', (err) => debugAndPrintError(err));\n\n this.server = options.server;\n\n if (this.pingIntervalMS > 0 && this.pingMaxRetries > 0) {\n this.server.on('listening', () =>\n this.autoTerminateUnresponsiveClients(this.pingIntervalMS, this.pingMaxRetries));\n\n this.server.on('close', () =>\n clearInterval(this.pingInterval));\n }\n }\n\n public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n this.server.listen(port, hostname, backlog, listeningListener);\n return this;\n }\n\n public shutdown() {\n this.wss.close();\n this.server.close();\n }\n\n public simulateLatency(milliseconds: number) {\n if (this._originalSend == null) {\n this._originalSend = WebSocketClient.prototype.raw;\n }\n\n const originalSend = this._originalSend;\n\n WebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalSend : function (...args: any[]) {\n // copy buffer\n let [buf, ...rest] = args;\n buf = Array.from(buf);\n setTimeout(() => originalSend.apply(this, [buf, ...rest]), milliseconds);\n };\n }\n\n protected autoTerminateUnresponsiveClients(pingInterval: number, pingMaxRetries: number) {\n // interval to detect broken connections\n this.pingInterval = setInterval(() => {\n this.wss.clients.forEach((client: WebSocket) => {\n //\n // if client hasn't responded after the interval, terminate its connection.\n //\n if ((client as RawWebSocketClient).pingCount >= pingMaxRetries) {\n // debugConnection(`terminating unresponsive client ${client.sessionId}`);\n debugConnection(`terminating unresponsive client`);\n return client.terminate();\n }\n\n (client as RawWebSocketClient).pingCount++;\n client.ping(noop);\n });\n }, pingInterval);\n }\n\n protected async onConnection(rawClient: RawWebSocketClient, req: http.IncomingMessage) {\n // prevent server crashes if a single client had unexpected error\n rawClient.on('error', (err) => debugAndPrintError(err.message + '\\n' + err.stack));\n rawClient.on('pong', heartbeat);\n\n // compatibility with ws / uws\n const parsedURL = new URL(`ws://server/${req.url}`);\n\n const sessionId = parsedURL.searchParams.get(\"sessionId\");\n const processAndRoomId = parsedURL.pathname.match(/\\/[a-zA-Z0-9_\\-]+\\/([a-zA-Z0-9_\\-]+)$/);\n const roomId = processAndRoomId && processAndRoomId[1];\n\n const room = matchMaker.getLocalRoomById(roomId);\n\n // set client id\n rawClient.pingCount = 0;\n\n const client = new WebSocketClient(sessionId, rawClient);\n\n //\n // TODO: DRY code below with all transports\n //\n\n try {\n if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get(\"reconnectionToken\"))) {\n throw new Error('seat reservation expired.');\n }\n\n await room._onJoin(client, {\n headers: req.headers,\n token: parsedURL.searchParams.get(\"_authToken\") ?? getBearerToken(req.headers.authorization),\n ip: req.headers['x-real-ip'] ?? req.headers['x-forwarded-for'] ?? req.socket.remoteAddress,\n });\n\n } catch (e) {\n debugAndPrintError(e);\n\n // send error code to client then terminate\n client.error(e.code, e.message, () =>\n rawClient.close(Protocol.WS_CLOSE_WITH_ERROR));\n }\n }\n\n}\n"], "mappings": "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", "names": [] }