UNPKG

@colyseus/ws-transport

Version:

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

185 lines (183 loc) 7.79 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/transport/ws-transport/src/WebSocketTransport.ts var WebSocketTransport_exports = {}; __export(WebSocketTransport_exports, { WebSocketTransport: () => WebSocketTransport }); module.exports = __toCommonJS(WebSocketTransport_exports); var import_http = __toESM(require("http"), 1); var import_url = require("url"); var import_ws = require("ws"); var import_express = __toESM(require("express"), 1); var import_core = require("@colyseus/core"); var import_WebSocketClient = require("./WebSocketClient.cjs"); function noop() { } function heartbeat() { this.pingCount = 0; } var WebSocketTransport = class extends import_core.Transport { constructor(options = {}) { super(); // False when sharing an external HTTP server, such as Vite's dev server. this.shouldShutdownServer = true; this._originalSend = null; if (options.maxPayload === void 0) { options.maxPayload = 4 * 1024; } if (options.perMessageDeflate === void 0) { options.perMessageDeflate = false; } this.pingIntervalMS = options.pingInterval !== void 0 ? options.pingInterval : 3e3; this.pingMaxRetries = options.pingMaxRetries !== void 0 ? options.pingMaxRetries : 2; if (!options.server && !options.noServer) { options.server = import_http.default.createServer(); } this.wss = new import_ws.WebSocketServer(options); this.wss.on("connection", this.onConnection); this.wss.on("error", (err) => (0, import_core.debugAndPrintError)(err)); this.server = options.server; if (this.server && this.pingIntervalMS > 0 && this.pingMaxRetries > 0) { this.server.on("listening", () => this.autoTerminateUnresponsiveClients(this.pingIntervalMS, this.pingMaxRetries)); this.server.on("close", () => clearInterval(this.pingInterval)); } } getExpressApp() { if (!this.server) { throw new Error("WebSocketTransport is not attached to an HTTP server."); } if (!this._expressApp) { this._expressApp = (0, import_express.default)(); this.server.on("request", this._expressApp); } return this._expressApp; } listen(port, hostname, backlog, listeningListener) { if (!this.server) { throw new Error("WebSocketTransport is not attached to an HTTP server."); } this.server.listen(port, hostname, backlog, listeningListener); return this; } /** * Attach this transport to an already-running HTTP server. * * `colyseus/vite` uses this in dev mode so Colyseus can reuse Vite's HTTP server * instead of creating and owning a separate one. */ attachToServer(server, options = {}) { this.server = server; this.shouldShutdownServer = false; server.on("upgrade", (req, socket, head) => { if (options.filter && !options.filter(req)) { return; } this.wss.handleUpgrade(req, socket, head, (ws) => { this.wss.emit("connection", ws, req); }); }); if (this.pingIntervalMS > 0 && this.pingMaxRetries > 0 && !this.pingInterval) { this.autoTerminateUnresponsiveClients(this.pingIntervalMS, this.pingMaxRetries); server.on("close", () => clearInterval(this.pingInterval)); } return this; } /** * Close the websocket server and all active websocket connections. * * When attached through `attachToServer()`, keep the shared HTTP server alive. * This is required for `colyseus/vite`, which does not own the Vite dev server. */ shutdown() { this.wss.close(); if (this.shouldShutdownServer) { this.server?.close(); } } simulateLatency(milliseconds) { if (this._originalSend == null) { this._originalSend = import_WebSocketClient.WebSocketClient.prototype.raw; } const originalSend = this._originalSend; import_WebSocketClient.WebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalSend : function(...args) { let [buf, ...rest] = args; buf = Array.from(buf); setTimeout(() => originalSend.apply(this, [buf, ...rest]), milliseconds); }; } autoTerminateUnresponsiveClients(pingInterval, pingMaxRetries) { this.pingInterval = setInterval(() => { this.wss.clients.forEach((client) => { if (client.pingCount >= pingMaxRetries) { (0, import_core.debugConnection)(`terminating unresponsive client`); return client.terminate(); } client.pingCount++; client.ping(noop); }); }, pingInterval); } async onConnection(rawClient, req) { rawClient.on("error", (err) => (0, import_core.debugAndPrintError)(err.message + "\n" + err.stack)); rawClient.on("pong", heartbeat); rawClient.pingCount = 0; const parsedURL = new import_url.URL(`ws://server/${req.url}`); const sessionId = parsedURL.searchParams.get("sessionId"); const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/); const roomId = processAndRoomId && processAndRoomId[1]; if (!sessionId && !roomId) { const timeout = setTimeout(() => rawClient.close(import_core.CloseCode.NORMAL_CLOSURE), 1e3); rawClient.on("message", (_) => rawClient.send(new Uint8Array([import_core.Protocol.PING]))); rawClient.on("close", () => clearTimeout(timeout)); return; } const room = import_core.matchMaker.getLocalRoomById(roomId); const client = new import_WebSocketClient.WebSocketClient(sessionId, rawClient); const reconnectionToken = parsedURL.searchParams.get("reconnectionToken"); const skipHandshake = parsedURL.searchParams.has("skipHandshake"); try { await (0, import_core.connectClientToRoom)(room, client, { headers: new Headers(req.headers), token: parsedURL.searchParams.get("_authToken") ?? (0, import_core.getBearerToken)(req.headers.authorization), ip: req.headers["x-real-ip"] ?? req.headers["x-forwarded-for"] ?? req.socket.remoteAddress }, { reconnectionToken, skipHandshake }); } catch (e) { (0, import_core.debugAndPrintError)(e); client.error(e.code, e.message, () => rawClient.close(reconnectionToken ? import_core.isDevMode ? import_core.CloseCode.MAY_TRY_RECONNECT : import_core.CloseCode.FAILED_TO_RECONNECT : import_core.CloseCode.WITH_ERROR)); } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { WebSocketTransport });