UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

230 lines (229 loc) 8.23 kB
// packages/core/src/Server.ts import greeting from "@colyseus/greeting-banner"; import { debugAndPrintError, debugMatchMaking } from "./Debug.mjs"; import * as matchMaker from "./MatchMaker.mjs"; import { Room } from "./Room.mjs"; import { getBearerToken, registerGracefulShutdown } from "./utils/Utils.mjs"; import { registerNode, unregisterNode } from "./discovery/index.mjs"; import { LocalPresence } from "./presence/LocalPresence.mjs"; import { LocalDriver } from "./matchmaker/driver/local/LocalDriver.mjs"; import { logger, setLogger } from "./Logger.mjs"; import { setDevMode, isDevMode } from "./utils/DevMode.mjs"; var Server = class { constructor(options = {}) { //@ts-expect-error this._originalRoomOnMessage = null; this.onShutdownCallback = () => Promise.resolve(); this.onBeforeShutdownCallback = () => Promise.resolve(); const { gracefullyShutdown: gracefullyShutdown2 = true, greet = true } = options; setDevMode(options.devMode === true); this.presence = options.presence || new LocalPresence(); this.driver = options.driver || new LocalDriver(); this.greet = greet; this.attach(options); matchMaker.setup( this.presence, this.driver, options.publicAddress, options.selectProcessIdToCreateRoom ); if (gracefullyShutdown2) { registerGracefulShutdown((err) => this.gracefullyShutdown(true, err)); } if (options.logger) { setLogger(options.logger); } } attach(options) { if (options.pingInterval !== void 0 || options.pingMaxRetries !== void 0 || options.server !== void 0 || options.verifyClient !== void 0) { logger.warn("DEPRECATION WARNING: 'pingInterval', 'pingMaxRetries', 'server', and 'verifyClient' Server options will be permanently moved to WebSocketTransport on v0.15"); logger.warn(`new Server({ transport: new WebSocketTransport({ pingInterval: ..., pingMaxRetries: ..., server: ..., verifyClient: ... }) })`); logger.warn("\u{1F449} Documentation: https://docs.colyseus.io/server/transport/"); } const transport = options.transport || this.getDefaultTransport(options); this.transport = transport; if (this.transport.server) { this.transport.server.once("listening", () => this.registerProcessForDiscovery()); this.attachMatchMakingRoutes(this.transport.server); } } /** * Bind the server into the port specified. * * @param port * @param hostname * @param backlog * @param listeningListener */ async listen(port, hostname, backlog, listeningListener) { this.port = port; await matchMaker.accept(); if (this.greet) { console.log(greeting); } return new Promise((resolve, reject) => { this.transport.server?.on("error", (err) => reject(err)); this.transport.listen(port, hostname, backlog, (err) => { if (listeningListener) { listeningListener(err); } if (err) { reject(err); } else { resolve(); } }); }); } async registerProcessForDiscovery() { await registerNode(this.presence, { port: this.port, processId: matchMaker.processId }); } define(nameOrHandler, handlerOrOptions, defaultOptions) { const name = typeof nameOrHandler === "string" ? nameOrHandler : nameOrHandler.name; const roomClass = typeof nameOrHandler === "string" ? handlerOrOptions : nameOrHandler; const options = typeof nameOrHandler === "string" ? defaultOptions : handlerOrOptions; return matchMaker.defineRoomType(name, roomClass, options); } /** * Remove a room definition from matchmaking. * This method does not destroy any room. It only dissallows matchmaking */ removeRoomType(name) { matchMaker.removeRoomType(name); } async gracefullyShutdown(exit = true, err) { if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) { return; } await unregisterNode(this.presence, { port: this.port, processId: matchMaker.processId }); try { await this.onBeforeShutdownCallback(); await matchMaker.gracefullyShutdown(); this.transport.shutdown(); this.presence.shutdown(); this.driver.shutdown(); await this.onShutdownCallback(); } catch (e) { debugAndPrintError(`error during shutdown: ${e}`); } finally { if (exit) { process.exit(err && !isDevMode ? 1 : 0); } } } /** * Add simulated latency between client and server. * @param milliseconds round trip latency in milliseconds. */ simulateLatency(milliseconds) { if (milliseconds > 0) { logger.warn(`\u{1F4F6}\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`); } else { logger.warn(`\u{1F4F6}\uFE0F\u2757 Colyseus latency simulation disabled.`); } const halfwayMS = milliseconds / 2; this.transport.simulateLatency(halfwayMS); if (this._originalRoomOnMessage == null) { this._originalRoomOnMessage = Room.prototype["_onMessage"]; } const originalOnMessage = this._originalRoomOnMessage; Room.prototype["_onMessage"] = milliseconds <= Number.EPSILON ? originalOnMessage : function(client, buffer) { const cachedBuffer = Buffer.from(buffer); setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS); }; } /** * Register a callback that is going to be executed before the server shuts down. * @param callback */ onShutdown(callback) { this.onShutdownCallback = callback; } onBeforeShutdown(callback) { this.onBeforeShutdownCallback = callback; } getDefaultTransport(_) { throw new Error("Please provide a 'transport' layer. Default transport not set."); } attachMatchMakingRoutes(server) { const listeners = server.listeners("request").slice(0); server.removeAllListeners("request"); server.on("request", (req, res) => { if (req.url.indexOf(`/${matchMaker.controller.matchmakeRoute}`) !== -1) { debugMatchMaking("received matchmake request: %s", req.url); this.handleMatchMakeRequest(req, res); } else { for (let i = 0, l = listeners.length; i < l; i++) { listeners[i].call(server, req, res); } } }); } async handleMatchMakeRequest(req, res) { if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) { res.writeHead(503, {}); res.end(); return; } const headers = Object.assign( {}, matchMaker.controller.DEFAULT_CORS_HEADERS, matchMaker.controller.getCorsHeaders.call(void 0, req) ); if (req.method === "OPTIONS") { res.writeHead(204, headers); res.end(); } else if (req.method === "POST") { const matchedParams = req.url.match(matchMaker.controller.allowedRoomNameChars); const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute); const method = matchedParams[matchmakeIndex + 1]; const roomName = matchedParams[matchmakeIndex + 2] || ""; const data = []; req.on("data", (chunk) => data.push(chunk)); req.on("end", async () => { headers["Content-Type"] = "application/json"; res.writeHead(200, headers); try { const clientOptions = JSON.parse(Buffer.concat(data).toString()); const response = await matchMaker.controller.invokeMethod( method, roomName, clientOptions, { token: getBearerToken(req.headers["authorization"]), headers: req.headers, ip: req.headers["x-real-ip"] ?? req.headers["x-forwarded-for"] ?? req.socket.remoteAddress, req } ); if (this.transport.protocol !== void 0) { response.protocol = this.transport.protocol; } res.write(JSON.stringify(response)); } catch (e) { res.write(JSON.stringify({ code: e.code, error: e.message })); } res.end(); }); } else if (req.method === "GET") { res.writeHead(404, headers); res.end(); } } }; export { Server };