UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

5 lines 52.2 kB
{ "version": 3, "sources": ["../src/MatchMaker.ts"], "sourcesContent": ["import { EventEmitter } from 'events';\n\nimport { requestFromIPC, subscribeIPC, subscribeWithTimeout } from './IPC.ts';\n\nimport { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, type MethodName, type ExtractMethodOrPropertyType } from './utils/Utils.ts';\nimport { isDevMode, cacheRoomHistory, getPreviousProcessId, getRoomRestoreListKey, reloadFromCache } from './utils/DevMode.ts';\n\nimport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nimport { type OnCreateOptions, Room, RoomInternalState } from './Room.ts';\n\nimport { LocalPresence } from './presence/LocalPresence.ts';\nimport { createScopedPresence, type Presence } from './presence/Presence.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from './Debug.ts';\nimport { SeatReservationError } from './errors/SeatReservationError.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport { type IRoomCache, type MatchMakerDriver, type SortOptions, LocalDriver } from './matchmaker/LocalDriver/LocalDriver.ts';\nimport { controller } from './matchmaker/controller.ts';\nimport * as stats from './Stats.ts';\n\nimport { logger } from './Logger.ts';\nimport type { AuthContext, Client } from './Transport.ts';\nimport { getLockId, initializeRoomCache, type ExtractRoomCacheMetadata } from './matchmaker/driver.ts';\n\nimport { type ISeatReservation, CloseCode, ErrorCode } from '@colyseus/shared-types';\nimport { getDefaultDriver, getDefaultPresence, getDefaultPublicAddress } from './utils/Env.ts';\nexport type { ISeatReservation, ExtractRoomCacheMetadata };\n\nexport { controller, stats, type MatchMakerDriver };\n\nexport type ClientOptions = any;\nexport type SelectProcessIdCallback = (roomName: string, clientOptions: ClientOptions) => Promise<string>;\n\nconst handlers: {[id: string]: RegisteredHandler} = {};\nconst rooms: {[roomId: string]: Room} = {};\nconst events = new EventEmitter();\n\nexport let publicAddress: string;\nexport let processId: string;\nexport let presence: Presence;\nexport let driver: MatchMakerDriver;\n\n/**\n * Function to select the processId to create the room on.\n * By default, returns the process with least amount of rooms created.\n * @returns The processId to create the room on.\n */\nexport let selectProcessIdToCreateRoom: SelectProcessIdCallback = async function () {\n return (await stats.fetchAll())\n .sort((p1, p2) => p1.roomCount > p2.roomCount ? 1 : -1)[0]?.processId || processId;\n};\n\n/**\n * Whether health checks are enabled or not. (default: true)\n *\n * Health checks are automatically performed on theses scenarios:\n * - At startup, to check for leftover/invalid processId's\n * - When a remote room creation request times out\n * - When a remote seat reservation request times out\n */\nlet enableHealthChecks: boolean = true;\nexport function setHealthChecksEnabled(value: boolean) {\n enableHealthChecks = value;\n}\n\nexport let onReady: Deferred = new Deferred(); // onReady needs to be immediately available to @colyseus/auth integration.\n\nexport const MatchMakerState = {\n INITIALIZING: 0,\n READY: 1,\n SHUTTING_DOWN: 2,\n} as const;\nexport type MatchMakerState = (typeof MatchMakerState)[keyof typeof MatchMakerState];\n\n/**\n * Internal MatchMaker state\n */\nexport let state: MatchMakerState;\n\n/**\n * @private\n */\nexport async function setup(\n _presence?: Presence,\n _driver?: MatchMakerDriver,\n _publicAddress?: string,\n _selectProcessIdToCreateRoom?: SelectProcessIdCallback,\n) {\n if (onReady === undefined) {\n //\n // for testing purposes only: onReady is turned into undefined on shutdown\n // (needs refactoring.)\n //\n onReady = new Deferred();\n }\n\n state = MatchMakerState.INITIALIZING;\n\n presence = _presence || await getDefaultPresence();\n driver = _driver || await getDefaultDriver();\n publicAddress = _publicAddress || getDefaultPublicAddress();\n\n stats.reset(false);\n\n // devMode: try to retrieve previous processId\n if (isDevMode) { processId = await getPreviousProcessId(); }\n\n // ensure processId is set\n if (!processId) { processId = generateId(); }\n\n /**\n * Override default `selectProcessIdToCreateRoom` function.\n */\n if (_selectProcessIdToCreateRoom) {\n selectProcessIdToCreateRoom = _selectProcessIdToCreateRoom;\n }\n\n // boot driver if necessary (e.g. RedisDriver/PostgresDriver)\n if (driver.boot) {\n await driver.boot();\n }\n\n onReady.resolve();\n}\n\n/**\n * - Accept receiving remote room creation requests\n * - Check for leftover/invalid processId's on startup\n * @private\n */\nexport async function accept(isStandalone: boolean = false) {\n await onReady; // make sure \"processId\" is available\n\n /**\n * Skip setting up IPC and health checks if this process is running as a\n * standalone match-maker. \n * \n * When in \"standalone\" mode, this process will not spawn rooms and will only\n * be responsible for matchmaking.\n */\n if (isStandalone) {\n state = MatchMakerState.READY;\n return; \n }\n\n /**\n * Process-level subscription\n * - handle remote process healthcheck\n * - handle remote room creation\n */\n await subscribeIPC(presence, getProcessChannel(), (method: string, args: any) => {\n if (method === 'healthcheck') {\n // health check for this processId\n return true;\n\n } else {\n // handle room creation\n return handleCreateRoom.apply(undefined, args);\n }\n });\n\n /**\n * Check for leftover/invalid processId's on startup\n */\n if (enableHealthChecks) {\n await healthCheckAllProcesses();\n\n /*\n * persist processId every 1 minute\n *\n * FIXME: this is a workaround in case this `processId` gets excluded\n * (`stats.excludeProcess()`) by mistake due to health-check failure\n */\n stats.setAutoPersistInterval();\n }\n\n state = MatchMakerState.READY;\n\n await stats.persist();\n\n if (isDevMode) {\n await reloadFromCache();\n }\n}\n\n/**\n * Join or create into a room and return seat reservation\n */\nexport async function joinOrCreate(roomName: string, clientOptions: ClientOptions = {}, authContext?: AuthContext) {\n return await retry<Promise<ISeatReservation>>(async () => {\n const authData = await callOnAuth(roomName, clientOptions, authContext);\n let room: IRoomCache = await findOneRoomAvailable(roomName, clientOptions);\n\n if (!room) {\n const handler = getHandler(roomName);\n const filterOptions = handler.getFilterOptions(clientOptions);\n const concurrencyKey = getLockId(filterOptions);\n\n //\n // Prevent multiple rooms of same filter from being created concurrently\n //\n await concurrentJoinOrCreateRoomLock(handler, concurrencyKey, async (roomId?: string) => {\n if (roomId) {\n room = await driver.findOne({ roomId })\n }\n\n // If the room is not found or is already locked, try to find a new one\n if (!room || room.locked) {\n room = await findOneRoomAvailable(roomName, clientOptions);\n }\n\n if (!room) {\n //\n // TODO [?]\n // should we expose the \"creator\" auth data of the room during `onCreate()`?\n // it would be useful, though it could be accessed via `onJoin()` for now.\n //\n room = await createRoom(roomName, clientOptions);\n\n // Notify waiting concurrent requests about the new room\n presence.publish(`concurrent:${handler.name}:${concurrencyKey}`, room.roomId);\n }\n\n return room;\n });\n }\n\n return await reserveSeatFor(room, clientOptions, authData);\n }, 5, [SeatReservationError]);\n}\n\n/**\n * Create a room and return seat reservation\n */\nexport async function create(roomName: string, clientOptions: ClientOptions = {}, authContext?: AuthContext) {\n const authData = await callOnAuth(roomName, clientOptions, authContext);\n const room = await createRoom(roomName, clientOptions);\n return reserveSeatFor(room, clientOptions, authData);\n}\n\n/**\n * Join a room and return seat reservation\n */\nexport async function join(roomName: string, clientOptions: ClientOptions = {}, authContext?: AuthContext) {\n return await retry<Promise<ISeatReservation>>(async () => {\n const authData = await callOnAuth(roomName, clientOptions, authContext);\n const room = await findOneRoomAvailable(roomName, clientOptions);\n\n if (!room) {\n throw new ServerError(ErrorCode.MATCHMAKE_INVALID_CRITERIA, `no rooms found with provided criteria`);\n }\n\n return reserveSeatFor(room, clientOptions, authData);\n });\n}\n\n/**\n * Join a room by id and return seat reservation\n */\nexport async function reconnect(roomId: string, clientOptions: ClientOptions = {}) {\n const room = await driver.findOne({ roomId });\n if (!room) {\n // TODO: support a \"logLevel\" out of the box?\n if (process.env.NODE_ENV !== 'production') {\n logger.info(`\u274C room \"${roomId}\" has been disposed. Did you miss .allowReconnection()?\\n\uD83D\uDC49 https://docs.colyseus.io/room#allow-reconnection`);\n }\n\n throw new ServerError(ErrorCode.MATCHMAKE_INVALID_ROOM_ID, `room \"${roomId}\" has been disposed.`);\n }\n\n // check for reconnection\n const reconnectionToken = clientOptions.reconnectionToken;\n if (!reconnectionToken) { throw new ServerError(ErrorCode.MATCHMAKE_UNHANDLED, `'reconnectionToken' must be provided for reconnection.`); }\n\n // respond to re-connection!\n const sessionId = await remoteRoomCall(room.roomId, 'checkReconnectionToken', [reconnectionToken]);\n if (sessionId) {\n return buildSeatReservation(room, sessionId);\n\n } else {\n // TODO: support a \"logLevel\" out of the box?\n if (process.env.NODE_ENV !== 'production') {\n logger.info(`\u274C reconnection token invalid or expired. Did you miss .allowReconnection()?\\n\uD83D\uDC49 https://docs.colyseus.io/room#allow-reconnection`);\n }\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, `reconnection token invalid or expired.`);\n }\n}\n\n/**\n * Join a room by id and return client seat reservation. An exception is thrown if a room is not found for roomId.\n *\n * @param roomId - The Id of the specific room instance.\n * @param clientOptions - Options for the client seat reservation (for `onJoin`/`onAuth`)\n * @param authContext - Optional authentication token\n *\n * @returns Promise<SeatReservation> - A promise which contains `sessionId` and `IRoomCache`.\n */\nexport async function joinById(roomId: string, clientOptions: ClientOptions = {}, authContext?: AuthContext) {\n const room = await driver.findOne({ roomId });\n\n if (!room) {\n throw new ServerError(ErrorCode.MATCHMAKE_INVALID_ROOM_ID, `room \"${roomId}\" not found`);\n\n } else if (room.locked) {\n throw new ServerError(ErrorCode.MATCHMAKE_INVALID_ROOM_ID, `room \"${roomId}\" is locked`);\n }\n\n const authData = await callOnAuth(room.name, clientOptions, authContext);\n\n return reserveSeatFor(room, clientOptions, authData);\n}\n\n/**\n * Perform a query for all cached rooms\n */\nexport async function query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>> = {},\n sortOptions?: SortOptions,\n) {\n return await driver.query<T>(conditions, sortOptions);\n}\n\n/**\n * Find for a public and unlocked room available.\n *\n * @param roomName - The Id of the specific room.\n * @param filterOptions - Filter options.\n * @param sortOptions - Sorting options.\n *\n * @returns Promise<IRoomCache> - A promise contaning an object which includes room metadata and configurations.\n */\nexport async function findOneRoomAvailable(\n roomName: string,\n filterOptions: ClientOptions,\n additionalSortOptions?: SortOptions,\n) {\n const handler = getHandler(roomName);\n const sortOptions = Object.assign({}, handler.sortOptions ?? {});\n\n if (additionalSortOptions) {\n Object.assign(sortOptions, additionalSortOptions);\n }\n\n return await driver.findOne({\n locked: false,\n name: roomName,\n private: false,\n ...handler.getFilterOptions(filterOptions),\n }, sortOptions);\n}\n\n/**\n * Call a method or return a property on a remote room.\n *\n * @param roomId - The Id of the specific room instance.\n * @param method - Method or attribute to call or retrive.\n * @param args - Array of arguments for the method\n *\n * @returns Promise<any> - Returned value from the called or retrieved method/attribute.\n */\nexport async function remoteRoomCall<TRoom = Room>(\n roomId: string,\n method: keyof TRoom,\n args?: any[],\n rejectionTimeout = REMOTE_ROOM_SHORT_TIMEOUT,\n): Promise<ExtractMethodOrPropertyType<TRoom, typeof method>> {\n const room = rooms[roomId] as TRoom;\n\n if (!room) {\n try {\n return await requestFromIPC(presence, getRoomChannel(roomId), method as string, args, rejectionTimeout);\n\n } catch (e: any) {\n\n //\n // the room cache from an unavailable process might've been used here.\n // perform a health-check on the process before proceeding.\n // (this is a broken state when a process wasn't gracefully shut down)\n //\n if (method === '_reserveSeat' && e.message === \"ipc_timeout\") {\n throw e;\n }\n\n // TODO: for 1.0, consider always throwing previous error directly.\n\n const request = `${String(method)}${args && ' with args ' + JSON.stringify(args) || ''}`;\n throw new ServerError(\n ErrorCode.MATCHMAKE_UNHANDLED,\n `remote room (${roomId}) timed out, requesting \"${request}\". (${rejectionTimeout}ms exceeded)`,\n );\n }\n\n } else {\n return (!args && typeof (room[method]) !== 'function')\n ? room[method as string]\n : (await room[method as string].apply(room, args && JSON.parse(JSON.stringify(args))));\n }\n}\n\nexport function defineRoomType<T extends Type<Room>>(\n roomName: string,\n klass: T,\n defaultOptions?: OnCreateOptions<T>,\n): RegisteredHandler<InstanceType<T>> {\n const registeredHandler = new RegisteredHandler(klass, defaultOptions) as unknown as RegisteredHandler<InstanceType<T>>;\n registeredHandler.name = roomName;\n\n handlers[roomName] = registeredHandler;\n\n if (klass.prototype['onAuth'] !== Room.prototype['onAuth']) {\n // TODO: soft-deprecate instance level `onAuth` on 0.16\n // logger.warn(\"DEPRECATION WARNING: onAuth() at the instance level will be deprecated soon. Please use static onAuth() instead.\");\n\n if (klass['onAuth'] !== Room['onAuth']) {\n logger.info(`\u274C \"${roomName}\"'s onAuth() defined at the instance level will be ignored.`);\n }\n }\n\n return registeredHandler;\n}\n\nexport function addRoomType(handler: RegisteredHandler) {\n handlers[handler.name] = handler;\n}\n\nexport function removeRoomType(roomName: string) {\n delete handlers[roomName];\n}\n\nexport function getAllHandlers() {\n return handlers;\n}\n\nexport function getHandler(roomName: string) {\n const handler = handlers[roomName];\n\n if (!handler) {\n throw new ServerError(ErrorCode.MATCHMAKE_NO_HANDLER, `provided room name \"${roomName}\" not defined`);\n }\n\n return handler;\n}\n\nexport function getRoomClass(roomName: string): Type<Room> {\n return handlers[roomName]?.klass;\n}\n\n\n/**\n * Creates a new room.\n *\n * @param roomName - The identifier you defined on `gameServer.define()`\n * @param clientOptions - Options for `onCreate`\n *\n * @returns Promise<IRoomCache> - A promise contaning an object which includes room metadata and configurations.\n */\nexport async function createRoom(roomName: string, clientOptions: ClientOptions): Promise<IRoomCache> {\n //\n // - select a process to create the room\n // - use local processId if MatchMaker is not ready yet\n //\n const selectedProcessId = (state === MatchMakerState.READY)\n ? await selectProcessIdToCreateRoom(roomName, clientOptions)\n : processId;\n\n let room: IRoomCache;\n if (selectedProcessId === undefined) {\n\n if (isDevMode && processId === undefined) {\n //\n // WORKAROUND: wait for processId to be available\n // TODO: Remove this check on 1.0\n //\n // - This is a workaround when using matchMaker.createRoom() before the processId is available.\n // - We need to use top-level await to retrieve processId\n //\n await onReady;\n return createRoom(roomName, clientOptions);\n\n } else {\n throw new ServerError(ErrorCode.MATCHMAKE_UNHANDLED, `no processId available to create room ${roomName}`);\n }\n\n } else if (selectedProcessId === processId) {\n // create the room on this process!\n room = await handleCreateRoom(roomName, clientOptions);\n\n } else {\n // ask other process to create the room!\n try {\n room = await requestFromIPC<IRoomCache>(\n presence,\n getProcessChannel(selectedProcessId),\n undefined,\n [roomName, clientOptions],\n REMOTE_ROOM_SHORT_TIMEOUT,\n );\n\n } catch (e: any) {\n if (e.message === \"ipc_timeout\") {\n debugAndPrintError(`${e.message}: create room request timed out for ${roomName} on processId ${selectedProcessId}.`);\n\n //\n // clean-up possibly stale process from redis.\n // when a process disconnects ungracefully, it may leave its previous processId under \"roomcount\"\n // if the process is still alive, it will re-add itself shortly after the load-balancer selects it again.\n //\n if (enableHealthChecks) {\n await stats.excludeProcess(selectedProcessId);\n }\n\n // if other process failed to respond, create the room on this process\n room = await handleCreateRoom(roomName, clientOptions);\n\n } else {\n // re-throw intentional exception thrown during remote onCreate()\n throw e;\n }\n }\n }\n\n if (isDevMode) {\n presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify({\n \"clientOptions\": clientOptions,\n \"roomName\": roomName,\n \"processId\": processId\n }));\n }\n\n return room;\n}\n\nexport async function handleCreateRoom(roomName: string, clientOptions: ClientOptions, restoringRoomId?: string): Promise<IRoomCache> {\n const handler = getHandler(roomName);\n const room: Room = new handler.klass();\n\n // set room public attributes\n if (restoringRoomId && isDevMode) {\n room.roomId = restoringRoomId;\n\n } else {\n room.roomId = generateId();\n }\n\n //\n // Initialize .state (if set).\n //\n // Define getters and setters for:\n // - autoDispose\n // - patchRate\n //\n room['__init']();\n\n room.roomName = roomName;\n room.presence = createScopedPresence(room, presence);\n\n // initialize a RoomCache instance\n room['_listing'] = initializeRoomCache({\n name: roomName,\n processId,\n ...handler.getMetadataFromOptions(clientOptions)\n });\n\n // assign public host\n if (publicAddress) {\n room['_listing'].publicAddress = publicAddress;\n }\n\n if (room.onCreate) {\n try {\n await room.onCreate(merge({}, clientOptions, handler.options));\n\n } catch (e: any) {\n debugAndPrintError(e);\n throw new ServerError(\n e.code || ErrorCode.MATCHMAKE_UNHANDLED,\n e.message,\n );\n }\n }\n\n room['_internalState'] = RoomInternalState.CREATED;\n\n room['_listing'].roomId = room.roomId;\n room['_listing'].maxClients = room.maxClients;\n\n // imediatelly ask client to join the room\n debugMatchMaking('creating room \\'%s\\', roomId: \\'%s\\', processId: \\'%s\\'', roomName, room.roomId, processId);\n\n // increment amount of rooms this process is handling\n stats.local.roomCount++;\n stats.persist();\n\n room['_events'].on('lock', lockRoom.bind(undefined, room));\n room['_events'].on('unlock', unlockRoom.bind(undefined, room));\n room['_events'].on('join', onClientJoinRoom.bind(undefined, room));\n room['_events'].on('leave', onClientLeaveRoom.bind(undefined, room));\n room['_events'].once('dispose', disposeRoom.bind(undefined, roomName, room));\n\n if (handler.realtimeListingEnabled) {\n room['_events'].on('visibility-change', onVisibilityChange.bind(undefined, room));\n room['_events'].on('metadata-change', onMetadataChange.bind(undefined, room));\n }\n\n // when disconnect()'ing, keep only join/leave events for stat counting\n room['_events'].once('disconnect', () => {\n room['_events'].removeAllListeners('lock');\n room['_events'].removeAllListeners('unlock');\n room['_events'].removeAllListeners('dispose');\n\n if (handler.realtimeListingEnabled) {\n room['_events'].removeAllListeners('visibility-change');\n room['_events'].removeAllListeners('metadata-change');\n }\n\n //\n // emit \"no active rooms\" event when there are no more rooms in this process\n // (used during graceful shutdown)\n //\n if (stats.local.roomCount <= 0) {\n events.emit('no-active-rooms');\n }\n });\n\n // room always start unlocked\n await createRoomReferences(room, true);\n\n // persist room data only if match-making is enabled\n if (state !== MatchMakerState.SHUTTING_DOWN) {\n await driver.persist(room['_listing'], true);\n }\n\n handler.emit('create', room);\n\n return room['_listing'];\n}\n\n/**\n * Get room data by roomId.\n * This method does not return the actual room instance, use `getLocalRoomById` for that.\n */\nexport function getRoomById(roomId: string) {\n return driver.findOne({ roomId });\n}\n\n/**\n * Get local room instance by roomId. (Can return \"undefined\" if the room is not available on this process)\n */\nexport function getLocalRoomById(roomId: string) {\n return rooms[roomId];\n}\n\n/**\n * Disconnects every client on every room in the current process.\n */\nexport function disconnectAll(closeCode?: number) {\n const promises: Array<Promise<any>> = [];\n\n for (const roomId in rooms) {\n if (!rooms.hasOwnProperty(roomId)) {\n continue;\n }\n\n promises.push(rooms[roomId].disconnect(closeCode));\n }\n\n return promises;\n}\n\nasync function lockAndDisposeAll(): Promise<any> {\n // remove processId from room count key\n // (stops accepting new rooms on this process)\n await stats.excludeProcess(processId);\n\n // clear auto-persisting stats interval\n if (enableHealthChecks) {\n stats.clearAutoPersistInterval();\n }\n\n const noActiveRooms = new Deferred();\n if (stats.local.roomCount <= 0) {\n // no active rooms to dispose\n noActiveRooms.resolve();\n\n } else {\n // wait for all rooms to be disposed\n // TODO: set generous timeout in case\n events.once('no-active-rooms', () => noActiveRooms.resolve());\n }\n\n // - lock all local rooms to prevent new joins\n // - trigger `onBeforeShutdown()` on each room\n for (const roomId in rooms) {\n if (!rooms.hasOwnProperty(roomId)) {\n continue;\n }\n\n const room = rooms[roomId];\n room.lock();\n\n if (isDevMode) {\n // call default implementation of onBeforeShutdown() in dev mode\n Room.prototype.onBeforeShutdown.call(room);\n\n } else {\n // call custom implementation of onBeforeShutdown() in production\n room.onBeforeShutdown();\n }\n }\n\n await noActiveRooms;\n}\n\nexport async function gracefullyShutdown(): Promise<any> {\n if (state === MatchMakerState.SHUTTING_DOWN) {\n return Promise.reject('already_shutting_down');\n }\n\n debugMatchMaking(`${processId} is shutting down!`);\n\n state = MatchMakerState.SHUTTING_DOWN;\n\n onReady = undefined;\n\n if (isDevMode) {\n // Reject pending allowReconnection() deferreds BEFORE caching.\n // This should trigger a state cleanup via user's onLeave (e.g. players.delete)\n // so the cached state doesn't contain stale player data from clients\n for (const roomId in rooms) {\n if (!rooms.hasOwnProperty(roomId)) { continue; }\n rooms[roomId]['_rejectPendingReconnections']?.(\"devmode_restart\");\n }\n\n // Wait for async onLeave handlers to finish state cleanup.\n await new Promise(resolve => setTimeout(resolve, 50));\n\n await cacheRoomHistory(rooms);\n }\n\n // - lock existing rooms\n // - stop accepting new rooms on this process\n // - wait for all rooms to be disposed\n await lockAndDisposeAll();\n\n // make sure rooms are removed from cache\n await removeRoomsByProcessId(processId);\n\n // unsubscribe from process id channel\n presence.unsubscribe(getProcessChannel());\n\n // make sure all rooms are disposed\n return Promise.all(disconnectAll(\n (isDevMode)\n ? CloseCode.MAY_TRY_RECONNECT\n : CloseCode.SERVER_SHUTDOWN\n ));\n}\n\n/**\n * DO NOT USE THIS IN PRODUCTION. \n * THIS METHOD IS MEANT TO BE USED FOR VITE DEV SERVER ONLY.\n * ---------------------------------------------------------\n *\n * \n * Lightweight HMR reload for dev mode. \n *\n * Unlike gracefullyShutdown() + setup() + accept(), this preserves the\n * matchMaker infrastructure (presence, driver, IPC subscriptions, processId)\n * and only cycles room instances: cache state \u2192 dispose \u2192 restore.\n */\nexport async function hotReload(): Promise<void> {\n state = MatchMakerState.SHUTTING_DOWN;\n\n // Reject pending allowReconnection() deferreds BEFORE caching.\n // Triggers onLeave state cleanup so cached state is clean.\n for (const roomId in rooms) {\n if (!rooms.hasOwnProperty(roomId)) { continue; }\n rooms[roomId]['_rejectPendingReconnections']?.(\"devmode_restart\");\n }\n\n // Wait for async onLeave handlers to finish state cleanup.\n await new Promise(resolve => setTimeout(resolve, 50));\n\n await cacheRoomHistory(rooms);\n\n // Lock all rooms and trigger default onBeforeShutdown (dev mode impl).\n const noActiveRooms = new Deferred();\n if (stats.local.roomCount <= 0) {\n noActiveRooms.resolve();\n } else {\n events.once('no-active-rooms', () => noActiveRooms.resolve());\n }\n\n for (const roomId in rooms) {\n if (!rooms.hasOwnProperty(roomId)) { continue; }\n const room = rooms[roomId];\n room.lock();\n Room.prototype.onBeforeShutdown.call(room);\n }\n\n await noActiveRooms;\n\n // Disconnect all clients \u2014 they will auto-reconnect.\n await Promise.all(disconnectAll(CloseCode.MAY_TRY_RECONNECT));\n\n // Clear driver cache so reloadFromCache() can recreate rooms.\n await removeRoomsByProcessId(processId);\n\n // Restore rooms from cached state.\n state = MatchMakerState.READY;\n await reloadFromCache();\n await stats.persist();\n}\n\n/**\n * Reserve a seat for a client in a room\n */\nexport async function reserveSeatFor(room: IRoomCache, options: ClientOptions, authData?: any) {\n const sessionId: string = authData?.sessionId || generateId();\n\n let successfulSeatReservation: boolean;\n\n try {\n successfulSeatReservation = await remoteRoomCall<Room>(\n room.roomId,\n '_reserveSeat' as keyof Room,\n [sessionId, options, authData],\n REMOTE_ROOM_SHORT_TIMEOUT,\n );\n\n } catch (e: any) {\n debugMatchMaking(e);\n\n //\n // the room cache from an unavailable process might've been used here.\n // (this is a broken state when a process wasn't gracefully shut down)\n // perform a health-check on the process before proceeding.\n //\n if (\n e.message === \"ipc_timeout\" &&\n !(\n enableHealthChecks &&\n await healthCheckProcessId(room.processId)\n )\n ) {\n throw new SeatReservationError(`process ${room.processId} is not available.`);\n\n } else {\n successfulSeatReservation = false;\n }\n }\n\n if (!successfulSeatReservation) {\n throw new SeatReservationError(`${room.roomId} is already full.`);\n }\n\n return buildSeatReservation(room, sessionId);\n}\n\n/**\n * Reserve multiple seats for clients in a room\n */\nexport async function reserveMultipleSeatsFor(room: IRoomCache, clientsData: Array<{ sessionId: string, options: ClientOptions, auth: any }>) {\n let sessionIds: string[] = [];\n let options: ClientOptions[] = [];\n let authData: any[] = [];\n\n for (const clientData of clientsData) {\n sessionIds.push(clientData.sessionId);\n options.push(clientData.options);\n authData.push(clientData.auth);\n }\n\n debugMatchMaking(\n 'reserving multiple seats. sessionIds: \\'%s\\', roomId: \\'%s\\', processId: \\'%s\\'',\n sessionIds.join(', '), room.roomId, processId,\n );\n\n let successfulSeatReservations: boolean[];\n\n try {\n successfulSeatReservations = await remoteRoomCall<Room>(\n room.roomId,\n '_reserveMultipleSeats' as keyof Room,\n [sessionIds, options, authData],\n REMOTE_ROOM_SHORT_TIMEOUT,\n );\n\n } catch (e: any) {\n debugMatchMaking(e);\n\n //\n // the room cache from an unavailable process might've been used here.\n // (this is a broken state when a process wasn't gracefully shut down)\n // perform a health-check on the process before proceeding.\n //\n if (\n e.message === \"ipc_timeout\" &&\n !(\n enableHealthChecks &&\n await healthCheckProcessId(room.processId)\n )\n ) {\n throw new SeatReservationError(`process ${room.processId} is not available.`);\n\n } else {\n throw new SeatReservationError(`${room.roomId} is already full.`);\n }\n }\n\n return successfulSeatReservations;\n}\n\n/**\n * Build a seat reservation object.\n * @param room - The room to build a seat reservation for.\n * @param sessionId - The session ID of the client.\n * @returns A seat reservation object.\n */\nexport function buildSeatReservation(room: IRoomCache, sessionId: string) {\n const seatReservation: ISeatReservation = {\n name: room.name,\n sessionId,\n roomId: room.roomId,\n processId: room.processId,\n };\n\n if (isDevMode) {\n seatReservation.devMode = isDevMode;\n }\n\n if (room.publicAddress) {\n seatReservation.publicAddress = room.publicAddress;\n }\n\n return seatReservation;\n}\n\nasync function callOnAuth(roomName: string, clientOptions?: ClientOptions, authContext?: AuthContext) {\n const roomClass = getRoomClass(roomName);\n if (roomClass && roomClass['onAuth'] && roomClass['onAuth'] !== Room['onAuth']) {\n const result = await roomClass['onAuth'](authContext.token, clientOptions, authContext)\n if (!result) {\n throw new ServerError(ErrorCode.AUTH_FAILED, 'onAuth failed');\n }\n return result;\n }\n}\n\n/**\n * Perform health check on all processes\n */\nexport async function healthCheckAllProcesses() {\n const allStats = await stats.fetchAll();\n const activeProcessChannels = (await presence.channels(\"p:*\")).map(c => c.substring(2));\n\n if (allStats.length > 0) {\n await Promise.all(\n allStats\n .filter(stat => (\n stat.processId !== processId && // skip current process\n !activeProcessChannels.includes(stat.processId) // skip if channel is still listening\n ))\n .map(stat => healthCheckProcessId(stat.processId))\n );\n }\n}\n\n/**\n * Perform health check on a remote process\n * @param processId\n */\nconst _healthCheckByProcessId: { [processId: string]: Promise<any> } = {};\nexport function healthCheckProcessId(processId: string) {\n //\n // re-use the same promise if health-check is already in progress\n // (may occur when _reserveSeat() fails multiple times for the same 'processId')\n //\n if (_healthCheckByProcessId[processId] !== undefined) {\n return _healthCheckByProcessId[processId];\n }\n\n _healthCheckByProcessId[processId] = new Promise<boolean>(async (resolve, reject) => {\n logger.debug(`> Performing health-check against processId: '${processId}'...`);\n\n try {\n const requestTime = Date.now();\n\n await requestFromIPC<IRoomCache>(\n presence,\n getProcessChannel(processId),\n 'healthcheck',\n [],\n REMOTE_ROOM_SHORT_TIMEOUT,\n );\n\n logger.debug(`\u2705 Process '${processId}' successfully responded (${Date.now() - requestTime}ms)`);\n\n // succeeded to respond\n resolve(true)\n\n } catch (e) {\n // process failed to respond - remove it from stats\n logger.debug(`\u274C Process '${processId}' failed to respond. Cleaning it up.`);\n await stats.excludeProcess(processId);\n\n // clean-up possibly stale room ids\n if (!isDevMode) {\n await removeRoomsByProcessId(processId);\n }\n\n resolve(false);\n } finally {\n delete _healthCheckByProcessId[processId];\n }\n });\n\n return _healthCheckByProcessId[processId];\n}\n\n/**\n * Remove cached rooms by processId\n * @param processId\n */\nasync function removeRoomsByProcessId(processId: string) {\n //\n // clean-up possibly stale room ids\n // (ungraceful shutdowns using Redis can result on stale room ids still on memory.)\n //\n await driver.cleanup(processId);\n}\n\nasync function createRoomReferences(room: Room, init: boolean = false): Promise<boolean> {\n rooms[room.roomId] = room;\n\n if (init) {\n await subscribeIPC(\n presence,\n getRoomChannel(room.roomId),\n (method, args) => {\n return (!args && typeof (room[method]) !== 'function')\n ? room[method]\n : room[method].apply(room, args);\n },\n );\n }\n\n return true;\n}\n\n/**\n * Used only during `joinOrCreate` to handle concurrent requests for creating a room.\n */\nasync function concurrentJoinOrCreateRoomLock(\n handler: RegisteredHandler,\n concurrencyKey: string,\n callback: (roomId?: string) => Promise<IRoomCache>\n): Promise<IRoomCache> {\n return new Promise(async (resolve, reject) => {\n const hkey = getConcurrencyHashKey(handler.name);\n const concurrency = await presence.hincrbyex(\n hkey,\n concurrencyKey,\n 1, // increment by 1\n MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME * 2 // expire in 2x the time of MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME\n ) - 1; // do not consider the current request\n\n const fulfill = async (roomId?: string) => {\n try {\n resolve(await callback(roomId));\n\n } catch (e) {\n reject(e);\n\n } finally {\n await presence.hincrbyex(hkey, concurrencyKey, -1, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME * 2);\n }\n };\n\n if (concurrency > 0) {\n debugMatchMaking(\n 'receiving %d concurrent joinOrCreate for \\'%s\\' (%s)',\n concurrency, handler.name, concurrencyKey\n );\n\n try {\n const roomId = await subscribeWithTimeout(\n presence,\n `concurrent:${handler.name}:${concurrencyKey}`,\n (MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME +\n (Math.min(concurrency, 3) * 0.2)) * 1000 // convert to milliseconds\n );\n\n return await fulfill(roomId);\n } catch (error) {\n // Ignore ipc_timeout error\n }\n }\n\n return await fulfill();\n });\n}\n\nfunction onClientJoinRoom(room: Room, client: Client) {\n // increment local CCU\n stats.local.ccu++;\n stats.persist();\n\n handlers[room.roomName].emit('join', room, client);\n}\n\nfunction onClientLeaveRoom(room: Room, client: Client, willDispose: boolean) {\n // decrement local CCU\n stats.local.ccu--;\n stats.persist();\n\n handlers[room.roomName].emit('leave', room, client, willDispose);\n}\n\nfunction lockRoom(room: Room): void {\n // emit public event on registered handler\n handlers[room.roomName].emit('lock', room);\n}\n\nasync function unlockRoom(room: Room) {\n if (await createRoomReferences(room)) {\n // emit public event on registered handler\n handlers[room.roomName].emit('unlock', room);\n }\n}\n\nfunction onVisibilityChange(room: Room, isInvisible: boolean): void {\n handlers[room.roomName].emit('visibility-change', room, isInvisible);\n}\n\nfunction onMetadataChange(room: Room): void {\n handlers[room.roomName].emit('metadata-change', room);\n}\n\nasync function disposeRoom(roomName: string, room: Room) {\n debugMatchMaking('disposing \\'%s\\' (%s) on processId \\'%s\\' (graceful shutdown: %s)', roomName, room.roomId, processId, state === MatchMakerState.SHUTTING_DOWN);\n\n //\n // FIXME: this call should not be necessary.\n //\n // there's an unidentified edge case using LocalDriver where Room._dispose()\n // doesn't seem to be called [?], but \"disposeRoom\" is, leaving the matchmaker\n // in a broken state. (repeated ipc_timeout's for seat reservation on\n // non-existing rooms)\n //\n driver.remove(room['_listing'].roomId);\n stats.local.roomCount--;\n\n // decrease amount of rooms this process is handling\n if (state !== MatchMakerState.SHUTTING_DOWN) {\n stats.persist();\n\n // remove from devMode restore list\n if (isDevMode) {\n await presence.hdel(getRoomRestoreListKey(), room.roomId);\n }\n }\n\n // emit disposal on registered session handler\n handlers[roomName].emit('dispose', room);\n\n // unsubscribe from remote connections\n presence.unsubscribe(getRoomChannel(room.roomId));\n\n // remove actual room reference\n delete rooms[room.roomId];\n}\n\n//\n// Presence keys\n//\nfunction getRoomChannel(roomId: string) {\n return `$${roomId}`;\n}\n\nfunction getConcurrencyHashKey(roomName: string) {\n // concurrency hash\n return `ch:${roomName}`;\n}\n\nfunction getProcessChannel(id: string = processId) {\n return `p:${id}`;\n}\n"], "mappings": "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