UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

840 lines (839 loc) 31.3 kB
// packages/core/src/Room.ts import { unpack } from "@colyseus/msgpackr"; import { decode, $changes } from "@colyseus/schema"; import Clock from "@colyseus/timer"; import { EventEmitter } from "events"; import { logger } from "./Logger.mjs"; import { NoneSerializer } from "./serializer/NoneSerializer.mjs"; import { SchemaSerializer } from "./serializer/SchemaSerializer.mjs"; import { ErrorCode, getMessageBytes, Protocol } from "./Protocol.mjs"; import { Deferred, generateId, wrapTryCatch } from "./utils/Utils.mjs"; import { isDevMode } from "./utils/DevMode.mjs"; import { debugAndPrintError, debugMatchMaking, debugMessage } from "./Debug.mjs"; import { ServerError } from "./errors/ServerError.mjs"; import { ClientArray, ClientState } from "./Transport.mjs"; import { OnAuthException, OnCreateException, OnDisposeException, OnJoinException, OnLeaveException, OnMessageException, SimulationIntervalException, TimedEventException } from "./errors/RoomExceptions.mjs"; var DEFAULT_PATCH_RATE = 1e3 / 20; var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60; var noneSerializer = new NoneSerializer(); var 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 || {}); var Room = class _Room { constructor() { /** * Timing events tied to the room instance. * Intervals and timeouts are cleared when the room is disposed. */ this.clock = new Clock(); 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 ClientArray(); /** @internal */ this._events = new 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.`; debugAndPrintError(`${errorMessage} (roomId: ${this.roomId})`); if (isDevMode) { client.error(ErrorCode.INVALID_PAYLOAD, errorMessage); } else { client.leave(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) => 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[$changes] !== void 0) { this.setSerializer(new SchemaSerializer()); } else if ("_definition" in newState) { throw new Error("@colyseus/schema v2 compatibility currently missing (reach out if you need it)"); } else if ($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 ServerError(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 */ && !isDevMode) { throw new ServerError(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( isDevMode ? Protocol.WS_CLOSE_DEVMODE_RESTART : 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 = wrapTryCatch(onTickCallback, this.onUncaughtException.bind(this), 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) { 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: wrapTryCatch(callback, this.onUncaughtException.bind(this), 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 = 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 = 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 ServerError(ErrorCode.MATCHMAKE_EXPIRED, "already consumed"); } this.reservedSeats[sessionId][2] = true; debugMatchMaking("consuming seat reservation, sessionId: '%s' (roomId: %s)", client.sessionId, this.roomId); client._afterNextPatchQueue = this._afterNextPatchQueue; client.ref["onleave"] = (_) => client.state = 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 ServerError(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 ServerError(ErrorCode.AUTH_FAILED, "onAuth failed"); } } catch (e) { delete this.reservedSeats[sessionId]; await this._decrementClientCount(); throw e; } } if (client.state === ClientState.LEAVING) { throw new ServerError(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 === ClientState.LEAVING) { throw new Error("early_leave"); } else { delete this.reservedSeats[sessionId]; this._events.emit("join", client); } } catch (e) { await this._onLeave(client, Protocol.WS_CLOSE_GOING_AWAY); delete this.reservedSeats[sessionId]; if (!e.code) { e.code = ErrorCode.APPLICATION_ERROR; } throw e; } } if (client.state === 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(getMessageBytes[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 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 = 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 = {}) { debugMessage("broadcast: %O (roomId: %s)", message, this.roomId); const encodedMessage = message instanceof Uint8Array ? getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, void 0, message) : getMessageBytes.raw(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 === ClientState.LEAVING) { return; } const it = { offset: 1 }; const code = buffer[0]; if (!buffer) { debugAndPrintError(`${this.roomName} (roomId: ${this.roomId}), couldn't decode message: ${buffer}`); return; } if (code === Protocol.ROOM_DATA) { const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it); const messageTypeHandler = this.onMessageHandlers[messageType]; let message; try { message = buffer.byteLength > it.offset ? unpack(buffer.subarray(it.offset, buffer.byteLength)) : void 0; debugMessage("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId); if (messageTypeHandler?.validate !== void 0) { message = messageTypeHandler.validate(message); } } catch (e) { debugAndPrintError(e); client.leave(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 === Protocol.ROOM_DATA_BYTES) { const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it); const messageTypeHandler = this.onMessageHandlers[messageType]; let message = buffer.subarray(it.offset, buffer.byteLength); 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 === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) { client.state = 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 === Protocol.LEAVE_ROOM) { this._forciblyCloseClient(client, 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) { debugMatchMaking("onLeave, sessionId: '%s' (close code: %d, roomId: %s)", client.sessionId, code, this.roomId); client.state = ClientState.LEAVING; if (!this.clients.delete(client)) { return; } if (this.onLeave) { try { this.#_onLeaveConcurrent++; await this.onLeave(client, code === Protocol.WS_CLOSE_CONSENTED); } catch (e) { 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 !== 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, wrapTryCatch(cb, onUncaughtException, TimedEventException, "setTimeout"), timeout, ...args); }; const originalSetInterval = this.clock.setInterval; this.clock.setInterval = (cb, timeout, ...args) => { return originalSetInterval.call(this.clock, wrapTryCatch(cb, onUncaughtException, TimedEventException, "setInterval"), timeout, ...args); }; if (this.onCreate !== void 0) { this.onCreate = wrapTryCatch(this.onCreate.bind(this), onUncaughtException, OnCreateException, "onCreate", true); } if (this.onAuth !== void 0) { this.onAuth = wrapTryCatch(this.onAuth.bind(this), onUncaughtException, OnAuthException, "onAuth", true); } if (this.onJoin !== void 0) { this.onJoin = wrapTryCatch(this.onJoin.bind(this), onUncaughtException, OnJoinException, "onJoin", true); } if (this.onLeave !== void 0) { this.onLeave = wrapTryCatch(this.onLeave.bind(this), onUncaughtException, OnLeaveException, "onLeave", true); } if (this.onDispose !== void 0) { this.onDispose = wrapTryCatch(this.onDispose.bind(this), onUncaughtException, OnDisposeException, "onDispose"); } } }; export { DEFAULT_SEAT_RESERVATION_TIME, Room, RoomInternalState };