UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

874 lines (873 loc) 34.3 kB
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); var Room_exports = {}; __export(Room_exports, { DEFAULT_SEAT_RESERVATION_TIME: () => DEFAULT_SEAT_RESERVATION_TIME, Room: () => Room, RoomInternalState: () => RoomInternalState }); module.exports = __toCommonJS(Room_exports); var import_msgpackr = require("@colyseus/msgpackr"); var import_schema = require("@colyseus/schema"); var import_timer = __toESM(require("@colyseus/timer")); var import_events = require("events"); var import_Logger = require("./Logger.js"); var import_NoneSerializer = require("./serializer/NoneSerializer.js"); var import_SchemaSerializer = require("./serializer/SchemaSerializer.js"); var import_Protocol = require("./Protocol.js"); var import_Utils = require("./utils/Utils.js"); var import_DevMode = require("./utils/DevMode.js"); var import_Debug = require("./Debug.js"); var import_ServerError = require("./errors/ServerError.js"); var import_Transport = require("./Transport.js"); var import_RoomExceptions = require("./errors/RoomExceptions.js"); const DEFAULT_PATCH_RATE = 1e3 / 20; const DEFAULT_SIMULATION_INTERVAL = 1e3 / 60; const noneSerializer = new import_NoneSerializer.NoneSerializer(); const DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15); var RoomInternalState = /* @__PURE__ */ ((RoomInternalState2) => { RoomInternalState2[RoomInternalState2["CREATING"] = 0] = "CREATING"; RoomInternalState2[RoomInternalState2["CREATED"] = 1] = "CREATED"; RoomInternalState2[RoomInternalState2["DISPOSING"] = 2] = "DISPOSING"; return RoomInternalState2; })(RoomInternalState || {}); class Room { constructor() { /** * Timing events tied to the room instance. * Intervals and timeouts are cleared when the room is disposed. */ this.clock = new import_timer.default(); this.#_onLeaveConcurrent = 0; // number of onLeave calls in progress /** * Maximum number of clients allowed to connect into the room. When room reaches this limit, * it is locked automatically. Unless the room was explicitly locked by you via `lock()` method, * the room will be unlocked as soon as a client disconnects from it. */ this.maxClients = Infinity; this.#_maxClientsReached = false; /** * Automatically dispose the room when last client disconnects. * * @default true */ this.autoDispose = true; /** * Frequency to send the room state to connected clients, in milliseconds. * * @default 50ms (20fps) */ this.patchRate = DEFAULT_PATCH_RATE; /** * The array of connected clients. * * @see {@link https://docs.colyseus.io/colyseus/server/room/#client|Client instance} */ this.clients = new import_Transport.ClientArray(); /** @internal */ this._events = new import_events.EventEmitter(); // seat reservation & reconnection this.seatReservationTime = DEFAULT_SEAT_RESERVATION_TIME; this.reservedSeats = {}; this.reservedSeatTimeouts = {}; this._reconnections = {}; this._reconnectingSessionId = /* @__PURE__ */ new Map(); this.onMessageHandlers = { "__no_message_handler": { callback: (client, messageType, _) => { const errorMessage = `room onMessage for "${messageType}" not registered.`; (0, import_Debug.debugAndPrintError)(`${errorMessage} (roomId: ${this.roomId})`); if (import_DevMode.isDevMode) { client.error(import_Protocol.ErrorCode.INVALID_PAYLOAD, errorMessage); } else { client.leave(import_Protocol.Protocol.WS_CLOSE_WITH_ERROR, errorMessage); } } } }; this._serializer = noneSerializer; this._afterNextPatchQueue = []; this._internalState = 0 /* CREATING */; this._lockedExplicitly = false; this.#_locked = false; this._events.once("dispose", () => { this._dispose().catch((e) => (0, import_Debug.debugAndPrintError)(`onDispose error: ${e && e.stack || e.message || e || "promise rejected"} (roomId: ${this.roomId})`)).finally(() => this._events.emit("disconnect")); }); if (this.onUncaughtException !== void 0) { this.#registerUncaughtExceptionHandlers(); } } /** * This property will change on these situations: * - The maximum number of allowed clients has been reached (`maxClients`) * - You manually locked, or unlocked the room using lock() or `unlock()`. * * @readonly */ get locked() { return this.#_locked; } get metadata() { return this.listing.metadata; } #_roomId; #_roomName; #_onLeaveConcurrent; #_maxClientsReached; #_maxClients; #_autoDispose; #_patchRate; #_patchInterval; #_state; #_locked; /** * This method is called by the MatchMaker before onCreate() * @internal */ __init() { this.#_state = this.state; this.#_autoDispose = this.autoDispose; this.#_patchRate = this.patchRate; this.#_maxClients = this.maxClients; Object.defineProperties(this, { state: { enumerable: true, get: () => this.#_state, set: (newState) => { if (newState?.constructor[Symbol.metadata] !== void 0 || newState[import_schema.$changes] !== void 0) { this.setSerializer(new import_SchemaSerializer.SchemaSerializer()); } else if ("_definition" in newState) { throw new Error("@colyseus/schema v2 compatibility currently missing (reach out if you need it)"); } else if (import_schema.$changes === void 0) { throw new Error("Multiple @colyseus/schema versions detected. Please make sure you don't have multiple versions of @colyseus/schema installed."); } this._serializer.reset(newState); this.#_state = newState; } }, maxClients: { enumerable: true, get: () => this.#_maxClients, set: (value) => { this.#_maxClients = value; if (this._internalState === 1 /* CREATED */) { const hasReachedMaxClients = this.hasReachedMaxClients(); if (!this._lockedExplicitly && this.#_maxClientsReached && !hasReachedMaxClients) { this.#_maxClientsReached = false; this.#_locked = false; this.listing.locked = false; } if (hasReachedMaxClients) { this.#_maxClientsReached = true; this.#_locked = true; this.listing.locked = true; } this.listing.maxClients = value; this.listing.save(); } } }, autoDispose: { enumerable: true, get: () => this.#_autoDispose, set: (value) => { if (value !== this.#_autoDispose && this._internalState !== 2 /* DISPOSING */) { this.#_autoDispose = value; this.resetAutoDisposeTimeout(); } } }, patchRate: { enumerable: true, get: () => this.#_patchRate, set: (milliseconds) => { this.#_patchRate = milliseconds; if (this.#_patchInterval) { clearInterval(this.#_patchInterval); this.#_patchInterval = void 0; } if (milliseconds !== null && milliseconds !== 0) { this.#_patchInterval = setInterval(() => this.broadcastPatch(), milliseconds); } } } }); this.patchRate = this.#_patchRate; if (this.#_state) { this.state = this.#_state; } this.resetAutoDisposeTimeout(this.seatReservationTime); this.clock.start(); } /** * The name of the room you provided as first argument for `gameServer.define()`. * * @returns roomName string */ get roomName() { return this.#_roomName; } /** * Setting the name of the room. Overwriting this property is restricted. * * @param roomName */ set roomName(roomName) { if (this.#_roomName) { throw new import_ServerError.ServerError(import_Protocol.ErrorCode.APPLICATION_ERROR, "'roomName' cannot be overwritten."); } this.#_roomName = roomName; } /** * A unique, auto-generated, 9-character-long id of the room. * You may replace `this.roomId` during `onCreate()`. * * @returns roomId string */ get roomId() { return this.#_roomId; } /** * Setting the roomId, is restricted in room lifetime except upon room creation. * * @param roomId * @returns roomId string */ set roomId(roomId) { if (this._internalState !== 0 /* CREATING */ && !import_DevMode.isDevMode) { throw new import_ServerError.ServerError(import_Protocol.ErrorCode.APPLICATION_ERROR, "'roomId' can only be overridden upon room creation."); } this.#_roomId = roomId; } onAuth(client, options, context) { return true; } static async onAuth(token, options, context) { return true; } /** * This method is called during graceful shutdown of the server process * You may override this method to dispose the room in your own way. * * Once process reaches room count of 0, the room process will be terminated. */ onBeforeShutdown() { this.disconnect( import_DevMode.isDevMode ? import_Protocol.Protocol.WS_CLOSE_DEVMODE_RESTART : import_Protocol.Protocol.WS_CLOSE_CONSENTED ); } /** * Returns whether the sum of connected clients and reserved seats exceeds maximum number of clients. * * @returns boolean */ hasReachedMaxClients() { return this.clients.length + Object.keys(this.reservedSeats).length >= this.maxClients || this._internalState === 2 /* DISPOSING */; } /** * Set the number of seconds a room can wait for a client to effectively join the room. * You should consider how long your `onAuth()` will have to wait for setting a different seat reservation time. * The default value is 15 seconds. You may set the `COLYSEUS_SEAT_RESERVATION_TIME` * environment variable if you'd like to change the seat reservation time globally. * * @default 15 seconds * * @param seconds - number of seconds. * @returns The modified Room object. */ setSeatReservationTime(seconds) { this.seatReservationTime = seconds; return this; } hasReservedSeat(sessionId, reconnectionToken) { const reservedSeat = this.reservedSeats[sessionId]; if (reservedSeat === void 0) { return false; } if (reservedSeat[3]) { return reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId && this._reconnectingSessionId.has(sessionId); } else { return reservedSeat[2] === false; } } checkReconnectionToken(reconnectionToken) { const sessionId = this._reconnections[reconnectionToken]?.[0]; const reservedSeat = this.reservedSeats[sessionId]; if (reservedSeat && reservedSeat[3]) { this._reconnectingSessionId.set(sessionId, reconnectionToken); return sessionId; } else { return void 0; } } /** * (Optional) Set a simulation interval that can change the state of the game. * The simulation interval is your game loop. * * @default 16.6ms (60fps) * * @param onTickCallback - You can implement your physics or world updates here! * This is a good place to update the room state. * @param delay - Interval delay on executing `onTickCallback` in milliseconds. */ setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) { if (this._simulationInterval) { clearInterval(this._simulationInterval); } if (onTickCallback) { if (this.onUncaughtException !== void 0) { onTickCallback = (0, import_Utils.wrapTryCatch)(onTickCallback, this.onUncaughtException.bind(this), import_RoomExceptions.SimulationIntervalException, "setSimulationInterval"); } this._simulationInterval = setInterval(() => { this.clock.tick(); onTickCallback(this.clock.deltaTime); }, delay); } } /** * @deprecated Use `.patchRate=` instead. */ setPatchRate(milliseconds) { this.patchRate = milliseconds; } /** * @deprecated Use `.state =` instead. */ setState(newState) { this.state = newState; } setSerializer(serializer) { this._serializer = serializer; } async setMetadata(meta) { if (!this.listing.metadata) { this.listing.metadata = meta; } else { for (const field in meta) { if (!meta.hasOwnProperty(field)) { continue; } this.listing.metadata[field] = meta[field]; } if ("markModified" in this.listing) { this.listing.markModified("metadata"); } } if (this._internalState === 1 /* CREATED */) { await this.listing.save(); } } async setPrivate(bool = true) { if (this.listing.private === bool) return; this.listing.private = bool; if (this._internalState === 1 /* CREATED */) { await this.listing.save(); } this._events.emit("visibility-change", bool); } /** * Locking the room will remove it from the pool of available rooms for new clients to connect to. */ async lock() { this._lockedExplicitly = arguments[0] === void 0; if (this.#_locked) { return; } this.#_locked = true; await this.listing.updateOne({ $set: { locked: this.#_locked } }); this._events.emit("lock"); } /** * Unlocking the room returns it to the pool of available rooms for new clients to connect to. */ async unlock() { if (arguments[0] === void 0) { this._lockedExplicitly = false; } if (!this.#_locked) { return; } this.#_locked = false; await this.listing.updateOne({ $set: { locked: this.#_locked } }); this._events.emit("unlock"); } send(client, messageOrType, messageOrOptions, options) { import_Logger.logger.warn("DEPRECATION WARNING: use client.send(...) instead of this.send(client, ...)"); client.send(messageOrType, messageOrOptions, options); } broadcast(type, message, options) { if (options && options.afterNextPatch) { delete options.afterNextPatch; this._afterNextPatchQueue.push(["broadcast", arguments]); return; } this.broadcastMessageType(type, message, options); } /** * Broadcast bytes (UInt8Arrays) to a particular room */ broadcastBytes(type, message, options) { if (options && options.afterNextPatch) { delete options.afterNextPatch; this._afterNextPatchQueue.push(["broadcastBytes", arguments]); return; } this.broadcastMessageType(type, message, options); } /** * Checks whether mutations have occurred in the state, and broadcast them to all connected clients. */ broadcastPatch() { if (this.onBeforePatch) { this.onBeforePatch(this.state); } if (!this._simulationInterval) { this.clock.tick(); } if (!this.state) { return false; } const hasChanges = this._serializer.applyPatches(this.clients, this.state); this._dequeueAfterPatchMessages(); return hasChanges; } onMessage(messageType, callback, validate) { this.onMessageHandlers[messageType] = this.onUncaughtException !== void 0 ? { validate, callback: (0, import_Utils.wrapTryCatch)(callback, this.onUncaughtException.bind(this), import_RoomExceptions.OnMessageException, "onMessage", false, messageType) } : { validate, callback }; return () => delete this.onMessageHandlers[messageType]; } /** * Disconnect all connected clients, and then dispose the room. * * @param closeCode WebSocket close code (default = 4000, which is a "consented leave") * @returns Promise<void> */ disconnect(closeCode = import_Protocol.Protocol.WS_CLOSE_CONSENTED) { if (this._internalState === 2 /* DISPOSING */) { return Promise.resolve(`disconnect() ignored: room (${this.roomId}) is already disposing.`); } else if (this._internalState === 0 /* CREATING */) { throw new Error("cannot disconnect during onCreate()"); } this._internalState = 2 /* DISPOSING */; this.listing.remove(); this.#_autoDispose = true; const delayedDisconnection = new Promise((resolve) => this._events.once("disconnect", () => resolve())); for (const [_, reconnection] of Object.values(this._reconnections)) { reconnection.reject(new Error("disconnecting")); } let numClients = this.clients.length; if (numClients > 0) { while (numClients--) { this._forciblyCloseClient(this.clients[numClients], closeCode); } } else { this._events.emit("dispose"); } return delayedDisconnection; } async ["_onJoin"](client, authContext) { const sessionId = client.sessionId; client.reconnectionToken = (0, import_Utils.generateId)(); if (this.reservedSeatTimeouts[sessionId]) { clearTimeout(this.reservedSeatTimeouts[sessionId]); delete this.reservedSeatTimeouts[sessionId]; } if (this._autoDisposeTimeout) { clearTimeout(this._autoDisposeTimeout); this._autoDisposeTimeout = void 0; } const [joinOptions, authData, isConsumed, isWaitingReconnection] = this.reservedSeats[sessionId]; if (isConsumed) { throw new import_ServerError.ServerError(import_Protocol.ErrorCode.MATCHMAKE_EXPIRED, "already consumed"); } this.reservedSeats[sessionId][2] = true; (0, import_Debug.debugMatchMaking)("consuming seat reservation, sessionId: '%s' (roomId: %s)", client.sessionId, this.roomId); client._afterNextPatchQueue = this._afterNextPatchQueue; client.ref["onleave"] = (_) => client.state = import_Transport.ClientState.LEAVING; client.ref.once("close", client.ref["onleave"]); if (isWaitingReconnection) { const previousReconnectionToken = this._reconnectingSessionId.get(sessionId); if (previousReconnectionToken) { this.clients.push(client); await this._reconnections[previousReconnectionToken]?.[1].resolve(client); } else { const errorMessage = process.env.NODE_ENV === "production" ? "already consumed" : "bad reconnection token"; throw new import_ServerError.ServerError(import_Protocol.ErrorCode.MATCHMAKE_EXPIRED, errorMessage); } } else { try { if (authData) { client.auth = authData; } else if (this.onAuth !== Room.prototype.onAuth) { try { client.auth = await this.onAuth(client, joinOptions, authContext); if (!client.auth) { throw new import_ServerError.ServerError(import_Protocol.ErrorCode.AUTH_FAILED, "onAuth failed"); } } catch (e) { delete this.reservedSeats[sessionId]; await this._decrementClientCount(); throw e; } } if (client.state === import_Transport.ClientState.LEAVING) { throw new import_ServerError.ServerError(import_Protocol.Protocol.WS_CLOSE_GOING_AWAY, "already disconnected"); } this.clients.push(client); Object.defineProperty(this.reservedSeats, sessionId, { value: this.reservedSeats[sessionId], enumerable: false }); if (this.onJoin) { await this.onJoin(client, joinOptions, client.auth); } if (client.state === import_Transport.ClientState.LEAVING) { throw new Error("early_leave"); } else { delete this.reservedSeats[sessionId]; this._events.emit("join", client); } } catch (e) { await this._onLeave(client, import_Protocol.Protocol.WS_CLOSE_GOING_AWAY); delete this.reservedSeats[sessionId]; if (!e.code) { e.code = import_Protocol.ErrorCode.APPLICATION_ERROR; } throw e; } } if (client.state === import_Transport.ClientState.JOINING) { client.ref.removeListener("close", client.ref["onleave"]); client.ref["onleave"] = this._onLeave.bind(this, client); client.ref.once("close", client.ref["onleave"]); client.ref.on("message", this._onMessage.bind(this, client)); client.raw(import_Protocol.getMessageBytes[import_Protocol.Protocol.JOIN_ROOM]( client.reconnectionToken, this._serializer.id, this._serializer.handshake && this._serializer.handshake() )); } } /** * Allow the specified client to reconnect into the room. Must be used inside `onLeave()` method. * If seconds is provided, the reconnection is going to be cancelled after the provided amount of seconds. * * @param previousClient - The client which is to be waiting until re-connection happens. * @param seconds - Timeout period on re-connection in seconds. * * @returns Deferred<Client> - The differed is a promise like type. * This type can forcibly reject the promise by calling `.reject()`. */ allowReconnection(previousClient, seconds) { if (previousClient._enqueuedMessages !== void 0) { return Promise.reject(new Error("not joined")); } if (seconds === void 0) { console.warn('DEPRECATED: allowReconnection() requires a second argument. Using "manual" mode.'); seconds = "manual"; } if (seconds === "manual") { seconds = Infinity; } if (this._internalState === 2 /* DISPOSING */) { return Promise.reject(new Error("disposing")); } const sessionId = previousClient.sessionId; const reconnectionToken = previousClient.reconnectionToken; this._reserveSeat(sessionId, true, previousClient.auth, seconds, true); const reconnection = new import_Utils.Deferred(); this._reconnections[reconnectionToken] = [sessionId, reconnection]; if (seconds !== Infinity) { this.reservedSeatTimeouts[sessionId] = setTimeout(() => reconnection.reject(false), seconds * 1e3); } const cleanup = () => { delete this._reconnections[reconnectionToken]; delete this.reservedSeats[sessionId]; delete this.reservedSeatTimeouts[sessionId]; this._reconnectingSessionId.delete(sessionId); }; reconnection.then((newClient) => { newClient.auth = previousClient.auth; newClient.userData = previousClient.userData; newClient.view = previousClient.view; previousClient.state = import_Transport.ClientState.RECONNECTED; previousClient.ref = newClient.ref; previousClient.reconnectionToken = newClient.reconnectionToken; clearTimeout(this.reservedSeatTimeouts[sessionId]); cleanup(); }).catch(() => { cleanup(); this.resetAutoDisposeTimeout(); }); return reconnection; } resetAutoDisposeTimeout(timeoutInSeconds = 1) { clearTimeout(this._autoDisposeTimeout); if (!this.#_autoDispose) { return; } this._autoDisposeTimeout = setTimeout(() => { this._autoDisposeTimeout = void 0; this._disposeIfEmpty(); }, timeoutInSeconds * 1e3); } broadcastMessageType(type, message, options = {}) { (0, import_Debug.debugMessage)("broadcast: %O (roomId: %s)", message, this.roomId); const encodedMessage = message instanceof Uint8Array ? import_Protocol.getMessageBytes.raw(import_Protocol.Protocol.ROOM_DATA_BYTES, type, void 0, message) : import_Protocol.getMessageBytes.raw(import_Protocol.Protocol.ROOM_DATA, type, message); const except = typeof options.except !== "undefined" ? Array.isArray(options.except) ? options.except : [options.except] : void 0; let numClients = this.clients.length; while (numClients--) { const client = this.clients[numClients]; if (!except || !except.includes(client)) { client.enqueueRaw(encodedMessage); } } } sendFullState(client) { client.raw(this._serializer.getFullState(client)); } _dequeueAfterPatchMessages() { const length = this._afterNextPatchQueue.length; if (length > 0) { for (let i = 0; i < length; i++) { const [target, args] = this._afterNextPatchQueue[i]; if (target === "broadcast") { this.broadcast.apply(this, args); } else { target.raw.apply(target, args); } } this._afterNextPatchQueue.splice(0, length); } } async _reserveSeat(sessionId, joinOptions = true, authData = void 0, seconds = this.seatReservationTime, allowReconnection = false, devModeReconnection) { if (!allowReconnection && this.hasReachedMaxClients()) { return false; } this.reservedSeats[sessionId] = [joinOptions, authData, false, allowReconnection]; if (!allowReconnection) { await this._incrementClientCount(); this.reservedSeatTimeouts[sessionId] = setTimeout(async () => { delete this.reservedSeats[sessionId]; delete this.reservedSeatTimeouts[sessionId]; await this._decrementClientCount(); }, seconds * 1e3); this.resetAutoDisposeTimeout(seconds); } if (devModeReconnection) { this._reconnectingSessionId.set(sessionId, sessionId); } return true; } _disposeIfEmpty() { const willDispose = this.#_onLeaveConcurrent === 0 && // no "onLeave" calls in progress this.#_autoDispose && this._autoDisposeTimeout === void 0 && this.clients.length === 0 && Object.keys(this.reservedSeats).length === 0; if (willDispose) { this._events.emit("dispose"); } return willDispose; } async _dispose() { this._internalState = 2 /* DISPOSING */; this.listing.remove(); let userReturnData; if (this.onDispose) { userReturnData = this.onDispose(); } if (this.#_patchInterval) { clearInterval(this.#_patchInterval); this.#_patchInterval = void 0; } if (this._simulationInterval) { clearInterval(this._simulationInterval); this._simulationInterval = void 0; } if (this._autoDisposeTimeout) { clearInterval(this._autoDisposeTimeout); this._autoDisposeTimeout = void 0; } this.clock.clear(); this.clock.stop(); return await (userReturnData || Promise.resolve()); } _onMessage(client, buffer) { if (client.state === import_Transport.ClientState.LEAVING) { return; } const it = { offset: 1 }; const code = buffer[0]; if (!buffer) { (0, import_Debug.debugAndPrintError)(`${this.roomName} (roomId: ${this.roomId}), couldn't decode message: ${buffer}`); return; } if (code === import_Protocol.Protocol.ROOM_DATA) { const messageType = import_schema.decode.stringCheck(buffer, it) ? import_schema.decode.string(buffer, it) : import_schema.decode.number(buffer, it); const messageTypeHandler = this.onMessageHandlers[messageType]; let message; try { message = buffer.byteLength > it.offset ? (0, import_msgpackr.unpack)(buffer.subarray(it.offset, buffer.byteLength)) : void 0; (0, import_Debug.debugMessage)("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId); if (messageTypeHandler?.validate !== void 0) { message = messageTypeHandler.validate(message); } } catch (e) { (0, import_Debug.debugAndPrintError)(e); client.leave(import_Protocol.Protocol.WS_CLOSE_WITH_ERROR); return; } if (messageTypeHandler) { messageTypeHandler.callback(client, message); } else { (this.onMessageHandlers["*"] || this.onMessageHandlers["__no_message_handler"]).callback(client, messageType, message); } } else if (code === import_Protocol.Protocol.ROOM_DATA_BYTES) { const messageType = import_schema.decode.stringCheck(buffer, it) ? import_schema.decode.string(buffer, it) : import_schema.decode.number(buffer, it); const messageTypeHandler = this.onMessageHandlers[messageType]; let message = buffer.subarray(it.offset, buffer.byteLength); (0, import_Debug.debugMessage)("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId); if (messageTypeHandler?.validate !== void 0) { message = messageTypeHandler.validate(message); } if (messageTypeHandler) { messageTypeHandler.callback(client, message); } else { (this.onMessageHandlers["*"] || this.onMessageHandlers["__no_message_handler"]).callback(client, messageType, message); } } else if (code === import_Protocol.Protocol.JOIN_ROOM && client.state === import_Transport.ClientState.JOINING) { client.state = import_Transport.ClientState.JOINED; client._joinedAt = this.clock.elapsedTime; if (this.state) { this.sendFullState(client); } if (client._enqueuedMessages.length > 0) { client._enqueuedMessages.forEach((enqueued) => client.raw(enqueued)); } delete client._enqueuedMessages; } else if (code === import_Protocol.Protocol.LEAVE_ROOM) { this._forciblyCloseClient(client, import_Protocol.Protocol.WS_CLOSE_CONSENTED); } } _forciblyCloseClient(client, closeCode) { client.ref.removeAllListeners("message"); client.ref.removeListener("close", client.ref["onleave"]); this._onLeave(client, closeCode).then(() => client.leave(closeCode)); } async _onLeave(client, code) { (0, import_Debug.debugMatchMaking)("onLeave, sessionId: '%s' (close code: %d, roomId: %s)", client.sessionId, code, this.roomId); client.state = import_Transport.ClientState.LEAVING; if (!this.clients.delete(client)) { return; } if (this.onLeave) { try { this.#_onLeaveConcurrent++; await this.onLeave(client, code === import_Protocol.Protocol.WS_CLOSE_CONSENTED); } catch (e) { (0, import_Debug.debugAndPrintError)(`onLeave error: ${e && e.message || e || "promise rejected"} (roomId: ${this.roomId})`); } finally { this.#_onLeaveConcurrent--; } } if (this._reconnections[client.reconnectionToken]) { this._reconnections[client.reconnectionToken][1].catch(async () => { await this._onAfterLeave(client); }); } else if (client.state !== import_Transport.ClientState.RECONNECTED) { await this._onAfterLeave(client); } } async _onAfterLeave(client) { const willDispose = await this._decrementClientCount(); if (this.reservedSeats[client.sessionId] === void 0) { this._events.emit("leave", client, willDispose); } } async _incrementClientCount() { if (!this.#_locked && this.hasReachedMaxClients()) { this.#_maxClientsReached = true; this.lock.call(this, true); } await this.listing.updateOne({ $inc: { clients: 1 }, $set: { locked: this.#_locked } }); } async _decrementClientCount() { const willDispose = this._disposeIfEmpty(); if (this._internalState === 2 /* DISPOSING */) { return true; } if (!willDispose) { if (this.#_maxClientsReached && !this._lockedExplicitly) { this.#_maxClientsReached = false; this.unlock.call(this, true); } await this.listing.updateOne({ $inc: { clients: -1 }, $set: { locked: this.#_locked } }); } return willDispose; } #registerUncaughtExceptionHandlers() { const onUncaughtException = this.onUncaughtException.bind(this); const originalSetTimeout = this.clock.setTimeout; this.clock.setTimeout = (cb, timeout, ...args) => { return originalSetTimeout.call(this.clock, (0, import_Utils.wrapTryCatch)(cb, onUncaughtException, import_RoomExceptions.TimedEventException, "setTimeout"), timeout, ...args); }; const originalSetInterval = this.clock.setInterval; this.clock.setInterval = (cb, timeout, ...args) => { return originalSetInterval.call(this.clock, (0, import_Utils.wrapTryCatch)(cb, onUncaughtException, import_RoomExceptions.TimedEventException, "setInterval"), timeout, ...args); }; if (this.onCreate !== void 0) { this.onCreate = (0, import_Utils.wrapTryCatch)(this.onCreate.bind(this), onUncaughtException, import_RoomExceptions.OnCreateException, "onCreate", true); } if (this.onAuth !== void 0) { this.onAuth = (0, import_Utils.wrapTryCatch)(this.onAuth.bind(this), onUncaughtException, import_RoomExceptions.OnAuthException, "onAuth", true); } if (this.onJoin !== void 0) { this.onJoin = (0, import_Utils.wrapTryCatch)(this.onJoin.bind(this), onUncaughtException, import_RoomExceptions.OnJoinException, "onJoin", true); } if (this.onLeave !== void 0) { this.onLeave = (0, import_Utils.wrapTryCatch)(this.onLeave.bind(this), onUncaughtException, import_RoomExceptions.OnLeaveException, "onLeave", true); } if (this.onDispose !== void 0) { this.onDispose = (0, import_Utils.wrapTryCatch)(this.onDispose.bind(this), onUncaughtException, import_RoomExceptions.OnDisposeException, "onDispose"); } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { DEFAULT_SEAT_RESERVATION_TIME, Room, RoomInternalState });