@colyseus/core
Version:
Multiplayer Framework for Node.js.
874 lines (873 loc) • 34.3 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);
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
});