@colyseus/core
Version:
Multiplayer Framework for Node.js.
225 lines (224 loc) • 7.51 kB
JavaScript
// packages/core/src/Server.ts
import { greet } from "@colyseus/greeting-banner";
import { debugAndPrintError } from "./Debug.mjs";
import * as matchMaker from "./MatchMaker.mjs";
import { RegisteredHandler } from "./matchmaker/RegisteredHandler.mjs";
import { Room } from "./Room.mjs";
import { Deferred, registerGracefulShutdown, dynamicImport } from "./utils/Utils.mjs";
import { setTransport } from "./Transport.mjs";
import { logger, setLogger } from "./Logger.mjs";
import { setDevMode, isDevMode } from "./utils/DevMode.mjs";
import { bindRouterToTransport } from "./router/index.mjs";
import "@colyseus/shared-types";
import { getDefaultRouter } from "./router/default_routes.mjs";
var Server = class {
constructor(options = {}) {
this._onTransportReady = new Deferred();
this._originalRoomOnMessage = null;
this.onShutdownCallback = () => Promise.resolve();
this.onBeforeShutdownCallback = () => Promise.resolve();
const {
gracefullyShutdown: gracefullyShutdown2 = true,
greet: greet2 = true
} = options;
setDevMode(options.devMode === true);
this.options = options;
this.greet = greet2;
this.attach(options);
matchMaker.setup(
options.presence,
options.driver,
options.publicAddress,
options.selectProcessIdToCreateRoom
).then(() => {
this.presence = matchMaker.presence;
this.driver = matchMaker.driver;
});
if (gracefullyShutdown2) {
registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));
}
if (options.logger) {
setLogger(options.logger);
}
}
async attach(options) {
this.transport = options.transport || await this.getDefaultTransport(options);
if (options.express && this.transport.getExpressApp) {
const expressApp = await this.transport.getExpressApp();
await options.express(expressApp);
}
this._onTransportReady.resolve(this.transport);
}
/**
* Bind the server into the port specified.
*
* @param port - Port number or Unix socket path
* @param hostname
* @param backlog
* @param listeningListener
*/
async listen(port, hostname, backlog, listeningListener) {
if (this.options.beforeListen) {
await this.options.beforeListen();
}
if (process.env.COLYSEUS_CLOUD !== void 0) {
if (typeof hostname === "number") {
hostname = void 0;
} else {
try {
return (await dynamicImport("@colyseus/tools")).listen(this);
} catch (error) {
const err = new Error("Please install @colyseus/tools to be able to host on Colyseus Cloud.");
err.cause = error;
throw err;
}
}
}
this.port = port;
await matchMaker.accept(this.options.isStandaloneMatchMaker);
if (this.greet) {
greet();
}
await this._onTransportReady;
return new Promise((resolve, reject) => {
setTransport(this.transport);
this.transport.listen(port, hostname, backlog, (err) => {
if (this.transport.server) {
this.transport.server.on("error", (err2) => reject(err2));
}
if (!this.router) {
this.router = getDefaultRouter();
} else {
this.router = this.router.extend({ ...getDefaultRouter().endpoints });
}
bindRouterToTransport(this.transport, this.router, this.options.express !== void 0);
if (listeningListener) {
listeningListener(err);
}
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
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;
}
try {
await this.onBeforeShutdownCallback();
await matchMaker.gracefullyShutdown();
this.transport.shutdown();
this.presence?.shutdown();
await 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;
}
async getDefaultTransport(options) {
try {
const module = await dynamicImport("@colyseus/ws-transport");
const WebSocketTransport = module.WebSocketTransport;
return new WebSocketTransport(options);
} catch (error) {
this._onTransportReady.reject(error);
throw new Error("Please provide a 'transport' layer. Default transport not set.");
}
}
};
function isRegisteredHandler(value) {
return value instanceof RegisteredHandler || typeof value === "object" && value !== null && "klass" in value;
}
function registerRoomDefinitions(rooms) {
const roomNames = [];
for (const [name, value] of Object.entries(rooms)) {
if (isRegisteredHandler(value)) {
value.name = name;
matchMaker.addRoomType(value);
} else {
matchMaker.defineRoomType(name, value);
}
roomNames.push(name);
}
return roomNames;
}
function unregisterRoomDefinitions(roomNames) {
for (const roomName of roomNames) {
matchMaker.removeRoomType(roomName);
}
}
function defineServer(options) {
const { rooms, routes, ...serverOptions } = options;
if (isDevMode) {
return {
options: serverOptions,
router: routes,
"~rooms": rooms
};
}
const server = new Server(serverOptions);
server.router = routes;
registerRoomDefinitions(rooms);
return server;
}
function defineRoom(roomKlass, defaultOptions) {
return new RegisteredHandler(roomKlass, defaultOptions);
}
export {
Server,
defineRoom,
defineServer,
registerRoomDefinitions,
unregisterRoomDefinitions
};