@colyseus/ws-transport
Version:
```typescript import { Server } from "@colyseus/core"; import { WebSocketTransport } from "@colyseus/ws-transport";
8 lines (7 loc) • 12.4 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/WebSocketTransport.ts"],
"sourcesContent": ["import http from 'http';\nimport { URL } from 'url';\nimport WebSocket, { type ServerOptions, WebSocketServer } from 'ws';\nimport express from 'express';\n\nimport { matchMaker, Protocol, Transport, debugAndPrintError, debugConnection, getBearerToken, CloseCode, connectClientToRoom, isDevMode } from '@colyseus/core';\nimport { WebSocketClient } from './WebSocketClient.ts';\n\nfunction noop() {}\nfunction heartbeat(this: any) { this.pingCount = 0; }\n\ntype RawWebSocketClient = WebSocket & { pingCount: number };\n\nexport interface TransportOptions extends ServerOptions {\n pingInterval?: number;\n pingMaxRetries?: number;\n}\n\n/**\n * Options for binding this transport to an existing HTTP server.\n *\n * This is primarily used by `colyseus/vite`, which shares Vite's dev HTTP server\n * and forwards only Colyseus websocket upgrade requests to this transport.\n */\nexport interface AttachToServerOptions {\n /**\n * Return `true` to let this transport handle the upgrade request.\n * Requests that return `false` are left for the host HTTP server.\n */\n filter?: (req: http.IncomingMessage) => boolean;\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 // False when sharing an external HTTP server, such as Vite's dev server.\n protected shouldShutdownServer: boolean = true;\n\n private _originalSend: typeof WebSocketClient.prototype.raw | null = null;\n private _expressApp?: express.Application;\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 // `noServer: true` lets callers attach later via `attachToServer()`.\n // `colyseus/vite` uses this to share the Vite dev server instead of creating a new one.\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.server && 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 getExpressApp(): express.Application {\n if (!this.server) {\n throw new Error('WebSocketTransport is not attached to an HTTP server.');\n }\n\n if (!this._expressApp) {\n this._expressApp = express();\n this.server.on('request', this._expressApp);\n }\n return this._expressApp;\n }\n\n public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n if (!this.server) {\n throw new Error('WebSocketTransport is not attached to an HTTP server.');\n }\n\n this.server.listen(port, hostname, backlog, listeningListener);\n return this;\n }\n\n /**\n * Attach this transport to an already-running HTTP server.\n *\n * `colyseus/vite` uses this in dev mode so Colyseus can reuse Vite's HTTP server\n * instead of creating and owning a separate one.\n */\n public attachToServer(server: http.Server, options: AttachToServerOptions = {}) {\n this.server = server;\n this.shouldShutdownServer = false;\n\n server.on('upgrade', (req, socket, head) => {\n if (options.filter && !options.filter(req)) {\n return;\n }\n\n this.wss.handleUpgrade(req, socket as any, head, (ws) => {\n this.wss.emit('connection', ws, req);\n });\n });\n\n if (this.pingIntervalMS > 0 && this.pingMaxRetries > 0 && !this.pingInterval) {\n // An externally-managed server may already be listening, so start heartbeat here\n // instead of waiting for a future \"listening\" event.\n this.autoTerminateUnresponsiveClients(this.pingIntervalMS, this.pingMaxRetries);\n server.on('close', () => clearInterval(this.pingInterval));\n }\n\n return this;\n }\n\n /**\n * Close the websocket server and all active websocket connections.\n *\n * When attached through `attachToServer()`, keep the shared HTTP server alive.\n * This is required for `colyseus/vite`, which does not own the Vite dev server.\n */\n public shutdown() {\n this.wss.close();\n\n if (this.shouldShutdownServer) {\n this.server?.close();\n }\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 // @ts-ignore\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 rawClient.pingCount = 0;\n\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 // If sessionId is not provided, allow ping-pong utility.\n if (!sessionId && !roomId) {\n // Disconnect automatically after 1 second if no message is received.\n const timeout = setTimeout(() => rawClient.close(CloseCode.NORMAL_CLOSURE), 1000);\n rawClient.on('message', (_) => rawClient.send(new Uint8Array([Protocol.PING])));\n rawClient.on('close', () => clearTimeout(timeout));\n return;\n }\n\n const room = matchMaker.getLocalRoomById(roomId);\n\n const client = new WebSocketClient(sessionId, rawClient);\n const reconnectionToken = parsedURL.searchParams.get(\"reconnectionToken\");\n const skipHandshake = (parsedURL.searchParams.has(\"skipHandshake\"));\n\n try {\n await connectClientToRoom(room, client, {\n headers: new Headers(req.headers as Record<string, string>),\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 reconnectionToken,\n skipHandshake\n });\n\n } catch (e: any) {\n debugAndPrintError(e);\n\n // send error code to client then terminate.\n // Use MAY_TRY_RECONNECT when a reconnection token is present so the\n // SDK retries \u2014 the seat may not be reserved yet during devMode HMR.\n client.error(e.code, e.message, () =>\n rawClient.close(reconnectionToken\n ? (isDevMode)\n ? CloseCode.MAY_TRY_RECONNECT \n : CloseCode.FAILED_TO_RECONNECT\n : CloseCode.WITH_ERROR));\n }\n }\n\n}\n"],
"mappings": "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",
"names": []
}