@colyseus/ws-transport
Version:
```typescript import { Server } from "@colyseus/core"; import { WebSocketTransport } from "@colyseus/ws-transport";
185 lines (183 loc) • 7.79 kB
JavaScript
;
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
});