@colyseus/core 0.18.9 → 0.18.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/Transport.ts"],
4
- "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n return (await handler(request, context)) ?? undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;\n\n /**\n * Send raw bytes over the transport's UNRELIABLE channel \u2014 no delivery,\n * ordering, or duplication guarantee. Used for `@unreliable` state patches.\n *\n * Absent on transports with no datagram channel (every WebSocket transport).\n * Its presence IS the capability check \u2014 callers feature-detect rather than\n * reading a separate flag, and skip clients that can't receive.\n */\n rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n\n /**\n * Send a type of message to the client. Messages are encoded with MsgPack and can hold any\n * JSON-serializable data structure.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param message Message payload. (automatically encoded with msgpack.)\n * @param options\n */\n send<K extends keyof this['~messages']>(\n type: K,\n ...args: MessageArgs<this['~messages'][K], ISendOptions>\n ): void;\n\n /**\n * Send raw bytes to this specific client.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param bytes Raw byte array payload\n * @param options\n */\n sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions): void;\n\n /**\n * Disconnect this client from the room.\n *\n * @param code Custom close code. Default value is 1000.\n * @param data\n * @see [Leave room](https://docs.colyseus.io/room#leave-room)\n */\n leave(code?: number, data?: string): void;\n\n /**\n * @deprecated Use .leave() instead.\n */\n close(code?: number, data?: string): void;\n\n /**\n * Triggers `onError` with specified code to the client-side.\n *\n * @param code\n * @param message\n */\n error(code: number, message?: string): void;\n}\n\n/**\n * Private properties of the Client instance.\n * Only accessible internally by the framework, should not be encouraged/auto-completed for the user.\n *\n * TODO: refactor this.\n * @private\n */\nexport interface ClientPrivate {\n readyState: number; // TODO: remove readyState on version 1.0.0. Use only \"state\" instead.\n _enqueuedMessages?: any[];\n _joinedAt: number; // \"elapsedTime\" when the client joined the room.\n\n /**\n * Used for rate limiting via maxMessagesPerSecond.\n */\n _numMessagesLastSecond?: number;\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip\n return (await handler(request, context)) || undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;\n\n /**\n * Send raw bytes over the transport's UNRELIABLE channel \u2014 no delivery,\n * ordering, or duplication guarantee. Used for `@unreliable` state patches.\n *\n * Absent on transports with no datagram channel (every WebSocket transport).\n * Its presence IS the capability check \u2014 callers feature-detect rather than\n * reading a separate flag, and skip clients that can't receive.\n */\n rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n\n /**\n * Send a type of message to the client. Messages are encoded with MsgPack and can hold any\n * JSON-serializable data structure.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param message Message payload. (automatically encoded with msgpack.)\n * @param options\n */\n send<K extends keyof this['~messages']>(\n type: K,\n ...args: MessageArgs<this['~messages'][K], ISendOptions>\n ): void;\n\n /**\n * Send raw bytes to this specific client.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param bytes Raw byte array payload\n * @param options\n */\n sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions): void;\n\n /**\n * Disconnect this client from the room.\n *\n * @param code Custom close code. Default value is 1000.\n * @param data\n * @see [Leave room](https://docs.colyseus.io/room#leave-room)\n */\n leave(code?: number, data?: string): void;\n\n /**\n * @deprecated Use .leave() instead.\n */\n close(code?: number, data?: string): void;\n\n /**\n * Triggers `onError` with specified code to the client-side.\n *\n * @param code\n * @param message\n */\n error(code: number, message?: string): void;\n}\n\n/**\n * Private properties of the Client instance.\n * Only accessible internally by the framework, should not be encouraged/auto-completed for the user.\n *\n * TODO: refactor this.\n * @private\n */\nexport interface ClientPrivate {\n readyState: number; // TODO: remove readyState on version 1.0.0. Use only \"state\" instead.\n _enqueuedMessages?: any[];\n _joinedAt: number; // \"elapsedTime\" when the client joined the room.\n\n /**\n * Used for rate limiting via maxMessagesPerSecond.\n */\n _numMessagesLastSecond?: number;\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -26,7 +26,7 @@ async function runBeforeUpgrade(handler, url, context) {
26
26
  return new Response(null, { status: 400 });
27
27
  }
28
28
  try {
29
- return await handler(request, context) ?? void 0;
29
+ return await handler(request, context) || void 0;
30
30
  } catch (e) {
31
31
  debugAndPrintError(e);
32
32
  return new Response(null, { status: 500 });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/Transport.ts"],
4
- "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n return (await handler(request, context)) ?? undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;\n\n /**\n * Send raw bytes over the transport's UNRELIABLE channel \u2014 no delivery,\n * ordering, or duplication guarantee. Used for `@unreliable` state patches.\n *\n * Absent on transports with no datagram channel (every WebSocket transport).\n * Its presence IS the capability check \u2014 callers feature-detect rather than\n * reading a separate flag, and skip clients that can't receive.\n */\n rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n\n /**\n * Send a type of message to the client. Messages are encoded with MsgPack and can hold any\n * JSON-serializable data structure.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param message Message payload. (automatically encoded with msgpack.)\n * @param options\n */\n send<K extends keyof this['~messages']>(\n type: K,\n ...args: MessageArgs<this['~messages'][K], ISendOptions>\n ): void;\n\n /**\n * Send raw bytes to this specific client.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param bytes Raw byte array payload\n * @param options\n */\n sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions): void;\n\n /**\n * Disconnect this client from the room.\n *\n * @param code Custom close code. Default value is 1000.\n * @param data\n * @see [Leave room](https://docs.colyseus.io/room#leave-room)\n */\n leave(code?: number, data?: string): void;\n\n /**\n * @deprecated Use .leave() instead.\n */\n close(code?: number, data?: string): void;\n\n /**\n * Triggers `onError` with specified code to the client-side.\n *\n * @param code\n * @param message\n */\n error(code: number, message?: string): void;\n}\n\n/**\n * Private properties of the Client instance.\n * Only accessible internally by the framework, should not be encouraged/auto-completed for the user.\n *\n * TODO: refactor this.\n * @private\n */\nexport interface ClientPrivate {\n readyState: number; // TODO: remove readyState on version 1.0.0. Use only \"state\" instead.\n _enqueuedMessages?: any[];\n _joinedAt: number; // \"elapsedTime\" when the client joined the room.\n\n /**\n * Used for rate limiting via maxMessagesPerSecond.\n */\n _numMessagesLastSecond?: number;\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip\n return (await handler(request, context)) || undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;\n\n /**\n * Send raw bytes over the transport's UNRELIABLE channel \u2014 no delivery,\n * ordering, or duplication guarantee. Used for `@unreliable` state patches.\n *\n * Absent on transports with no datagram channel (every WebSocket transport).\n * Its presence IS the capability check \u2014 callers feature-detect rather than\n * reading a separate flag, and skip clients that can't receive.\n */\n rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n\n /**\n * Send a type of message to the client. Messages are encoded with MsgPack and can hold any\n * JSON-serializable data structure.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param message Message payload. (automatically encoded with msgpack.)\n * @param options\n */\n send<K extends keyof this['~messages']>(\n type: K,\n ...args: MessageArgs<this['~messages'][K], ISendOptions>\n ): void;\n\n /**\n * Send raw bytes to this specific client.\n *\n * @param type String or Number identifier the client SDK will use to receive this message\n * @param bytes Raw byte array payload\n * @param options\n */\n sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions): void;\n\n /**\n * Disconnect this client from the room.\n *\n * @param code Custom close code. Default value is 1000.\n * @param data\n * @see [Leave room](https://docs.colyseus.io/room#leave-room)\n */\n leave(code?: number, data?: string): void;\n\n /**\n * @deprecated Use .leave() instead.\n */\n close(code?: number, data?: string): void;\n\n /**\n * Triggers `onError` with specified code to the client-side.\n *\n * @param code\n * @param message\n */\n error(code: number, message?: string): void;\n}\n\n/**\n * Private properties of the Client instance.\n * Only accessible internally by the framework, should not be encouraged/auto-completed for the user.\n *\n * TODO: refactor this.\n * @private\n */\nexport interface ClientPrivate {\n readyState: number; // TODO: remove readyState on version 1.0.0. Use only \"state\" instead.\n _enqueuedMessages?: any[];\n _joinedAt: number; // \"elapsedTime\" when the client joined the room.\n\n /**\n * Used for rate limiting via maxMessagesPerSecond.\n */\n _numMessagesLastSecond?: number;\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -34,6 +34,7 @@ __export(internal_exports, {
34
34
  USER_ROOMS_KEY_PREFIX: () => import_UserSessionIndex2.USER_ROOMS_KEY_PREFIX,
35
35
  listUserSessions: () => import_UserSessionIndex2.listUserSessions,
36
36
  listUserSessionsLive: () => listUserSessionsLive,
37
+ prepareServices: () => prepareServices,
37
38
  releaseRoomLeave: () => import_UserSessionIndex2.releaseRoomLeave,
38
39
  releaseUserSession: () => import_UserSessionIndex2.releaseUserSession,
39
40
  sweepRoomDispose: () => import_UserSessionIndex2.sweepRoomDispose,
@@ -48,11 +49,15 @@ var import_UserSessionIndex2 = require("./utils/UserSessionIndex.cjs");
48
49
  function listUserSessionsLive(userId, options) {
49
50
  return (0, import_UserSessionIndex.listUserSessions)(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);
50
51
  }
52
+ function prepareServices(server, runBeforeListen) {
53
+ return server.prepareServices(runBeforeListen);
54
+ }
51
55
  // Annotate the CommonJS export names for ESM import in node:
52
56
  0 && (module.exports = {
53
57
  USER_ROOMS_KEY_PREFIX,
54
58
  listUserSessions,
55
59
  listUserSessionsLive,
60
+ prepareServices,
56
61
  releaseRoomLeave,
57
62
  releaseUserSession,
58
63
  sweepRoomDispose,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/internal.ts"],
4
- "sourcesContent": ["/**\n * Framework-internal entry \u2014 symbols used by the bundled `colyseus`\n * plugins (track-user-sessions, unique-session) and the admin\n * backend, but NOT part of `@colyseus/core`'s public surface.\n *\n * Third parties should reach for `TrackUserSessionsPlugin` and its\n * `listUserSessions` static instead \u2014 the raw helpers below are\n * implementation details and can change between minor versions.\n *\n * Resolved via the `./*` wildcard in `packages/core/package.json`.\n *\n * @internal\n */\nimport * as matchMaker from './MatchMaker.ts';\nimport {\n listUserSessions,\n type ListUserSessionsOptions,\n type UserSessionInfo,\n} from './utils/UserSessionIndex.ts';\n\nexport {\n userRoomsKey,\n USER_ROOMS_KEY_PREFIX,\n trackUserSession,\n releaseUserSession,\n trackRoomJoin,\n releaseRoomLeave,\n sweepRoomDispose,\n listUserSessions,\n type UserRoomEntry,\n type UserSessionInfo,\n type ListUserSessionsOptions,\n} from './utils/UserSessionIndex.ts';\n\n/**\n * `listUserSessions` wired to the live `matchMaker` \u2014 what\n * `TrackUserSessionsPlugin.listUserSessions` and the admin backend\n * actually call. The pure `listUserSessions` is kept exported above\n * so unit tests can drive it with fake presence + findRooms.\n */\nexport function listUserSessionsLive(\n userId: string,\n options?: ListUserSessionsOptions,\n): Promise<UserSessionInfo[]> {\n return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,iBAA4B;AAC5B,8BAIO;AAEP,IAAAA,2BAYO;AAQA,SAAS,qBACd,QACA,SAC4B;AAC5B,aAAO,0CAA4B,qBAAqB,2BAAgB,QAAQ,OAAO;AACzF;",
4
+ "sourcesContent": ["/**\n * Framework-internal entry \u2014 symbols used by the bundled `colyseus`\n * plugins (track-user-sessions, unique-session) and the admin\n * backend, but NOT part of `@colyseus/core`'s public surface.\n *\n * Third parties should reach for `TrackUserSessionsPlugin` and its\n * `listUserSessions` static instead \u2014 the raw helpers below are\n * implementation details and can change between minor versions.\n *\n * Resolved via the `./*` wildcard in `packages/core/package.json`.\n *\n * @internal\n */\nimport * as matchMaker from './MatchMaker.ts';\nimport type { Server } from './Server.ts';\nimport type { Router } from './router/index.ts';\nimport {\n listUserSessions,\n type ListUserSessionsOptions,\n type UserSessionInfo,\n} from './utils/UserSessionIndex.ts';\n\nexport {\n userRoomsKey,\n USER_ROOMS_KEY_PREFIX,\n trackUserSession,\n releaseUserSession,\n trackRoomJoin,\n releaseRoomLeave,\n sweepRoomDispose,\n listUserSessions,\n type UserRoomEntry,\n type UserSessionInfo,\n type ListUserSessionsOptions,\n} from './utils/UserSessionIndex.ts';\n\n/**\n * `listUserSessions` wired to the live `matchMaker` \u2014 what\n * `TrackUserSessionsPlugin.listUserSessions` and the admin backend\n * actually call. The pure `listUserSessions` is kept exported above\n * so unit tests can drive it with fake presence + findRooms.\n */\nexport function listUserSessionsLive(\n userId: string,\n options?: ListUserSessionsOptions,\n): Promise<UserSessionInfo[]> {\n return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);\n}\n\n/**\n * Boot a server's services without listening \u2014 `colyseus/vite` owns the HTTP\n * server and never calls `Server.listen()`, so it runs this step itself.\n * Returns the finalized router, which the database may have extended.\n *\n * Pass `runBeforeListen: false` when re-booting after an HMR reload: the\n * `beforeListen` hook fires once because the server listens once, while the\n * database and the router defaults track the instances the reload rebuilt.\n */\nexport function prepareServices(server: Server, runBeforeListen: boolean): Promise<Router> {\n // `prepareServices` is protected \u2014 first-party hosts reach it through here\n // rather than widening the published `Server` API.\n return (server as unknown as {\n prepareServices(runBeforeListen: boolean): Promise<Router>;\n }).prepareServices(runBeforeListen);\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,iBAA4B;AAG5B,8BAIO;AAEP,IAAAA,2BAYO;AAQA,SAAS,qBACd,QACA,SAC4B;AAC5B,aAAO,0CAA4B,qBAAqB,2BAAgB,QAAQ,OAAO;AACzF;AAWO,SAAS,gBAAgB,QAAgB,iBAA2C;AAGzF,SAAQ,OAEL,gBAAgB,eAAe;AACpC;",
6
6
  "names": ["import_UserSessionIndex"]
7
7
  }
@@ -1,3 +1,5 @@
1
+ import type { Server } from './Server.ts';
2
+ import type { Router } from './router/index.ts';
1
3
  import { type ListUserSessionsOptions, type UserSessionInfo } from './utils/UserSessionIndex.ts';
2
4
  export { userRoomsKey, USER_ROOMS_KEY_PREFIX, trackUserSession, releaseUserSession, trackRoomJoin, releaseRoomLeave, sweepRoomDispose, listUserSessions, type UserRoomEntry, type UserSessionInfo, type ListUserSessionsOptions, } from './utils/UserSessionIndex.ts';
3
5
  /**
@@ -7,3 +9,13 @@ export { userRoomsKey, USER_ROOMS_KEY_PREFIX, trackUserSession, releaseUserSessi
7
9
  * so unit tests can drive it with fake presence + findRooms.
8
10
  */
9
11
  export declare function listUserSessionsLive(userId: string, options?: ListUserSessionsOptions): Promise<UserSessionInfo[]>;
12
+ /**
13
+ * Boot a server's services without listening — `colyseus/vite` owns the HTTP
14
+ * server and never calls `Server.listen()`, so it runs this step itself.
15
+ * Returns the finalized router, which the database may have extended.
16
+ *
17
+ * Pass `runBeforeListen: false` when re-booting after an HMR reload: the
18
+ * `beforeListen` hook fires once because the server listens once, while the
19
+ * database and the router defaults track the instances the reload rebuilt.
20
+ */
21
+ export declare function prepareServices(server: Server, runBeforeListen: boolean): Promise<Router>;
@@ -16,10 +16,14 @@ import {
16
16
  function listUserSessionsLive(userId, options) {
17
17
  return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);
18
18
  }
19
+ function prepareServices(server, runBeforeListen) {
20
+ return server.prepareServices(runBeforeListen);
21
+ }
19
22
  export {
20
23
  USER_ROOMS_KEY_PREFIX,
21
24
  listUserSessions2 as listUserSessions,
22
25
  listUserSessionsLive,
26
+ prepareServices,
23
27
  releaseRoomLeave,
24
28
  releaseUserSession,
25
29
  sweepRoomDispose,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/internal.ts"],
4
- "sourcesContent": ["/**\n * Framework-internal entry \u2014 symbols used by the bundled `colyseus`\n * plugins (track-user-sessions, unique-session) and the admin\n * backend, but NOT part of `@colyseus/core`'s public surface.\n *\n * Third parties should reach for `TrackUserSessionsPlugin` and its\n * `listUserSessions` static instead \u2014 the raw helpers below are\n * implementation details and can change between minor versions.\n *\n * Resolved via the `./*` wildcard in `packages/core/package.json`.\n *\n * @internal\n */\nimport * as matchMaker from './MatchMaker.ts';\nimport {\n listUserSessions,\n type ListUserSessionsOptions,\n type UserSessionInfo,\n} from './utils/UserSessionIndex.ts';\n\nexport {\n userRoomsKey,\n USER_ROOMS_KEY_PREFIX,\n trackUserSession,\n releaseUserSession,\n trackRoomJoin,\n releaseRoomLeave,\n sweepRoomDispose,\n listUserSessions,\n type UserRoomEntry,\n type UserSessionInfo,\n type ListUserSessionsOptions,\n} from './utils/UserSessionIndex.ts';\n\n/**\n * `listUserSessions` wired to the live `matchMaker` \u2014 what\n * `TrackUserSessionsPlugin.listUserSessions` and the admin backend\n * actually call. The pure `listUserSessions` is kept exported above\n * so unit tests can drive it with fake presence + findRooms.\n */\nexport function listUserSessionsLive(\n userId: string,\n options?: ListUserSessionsOptions,\n): Promise<UserSessionInfo[]> {\n return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);\n}\n"],
5
- "mappings": ";AAaA,YAAY,gBAAgB;AAC5B;AAAA,EACE;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAAA;AAAA,OAIK;AAQA,SAAS,qBACd,QACA,SAC4B;AAC5B,SAAO,iBAA4B,qBAAqB,2BAAgB,QAAQ,OAAO;AACzF;",
4
+ "sourcesContent": ["/**\n * Framework-internal entry \u2014 symbols used by the bundled `colyseus`\n * plugins (track-user-sessions, unique-session) and the admin\n * backend, but NOT part of `@colyseus/core`'s public surface.\n *\n * Third parties should reach for `TrackUserSessionsPlugin` and its\n * `listUserSessions` static instead \u2014 the raw helpers below are\n * implementation details and can change between minor versions.\n *\n * Resolved via the `./*` wildcard in `packages/core/package.json`.\n *\n * @internal\n */\nimport * as matchMaker from './MatchMaker.ts';\nimport type { Server } from './Server.ts';\nimport type { Router } from './router/index.ts';\nimport {\n listUserSessions,\n type ListUserSessionsOptions,\n type UserSessionInfo,\n} from './utils/UserSessionIndex.ts';\n\nexport {\n userRoomsKey,\n USER_ROOMS_KEY_PREFIX,\n trackUserSession,\n releaseUserSession,\n trackRoomJoin,\n releaseRoomLeave,\n sweepRoomDispose,\n listUserSessions,\n type UserRoomEntry,\n type UserSessionInfo,\n type ListUserSessionsOptions,\n} from './utils/UserSessionIndex.ts';\n\n/**\n * `listUserSessions` wired to the live `matchMaker` \u2014 what\n * `TrackUserSessionsPlugin.listUserSessions` and the admin backend\n * actually call. The pure `listUserSessions` is kept exported above\n * so unit tests can drive it with fake presence + findRooms.\n */\nexport function listUserSessionsLive(\n userId: string,\n options?: ListUserSessionsOptions,\n): Promise<UserSessionInfo[]> {\n return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);\n}\n\n/**\n * Boot a server's services without listening \u2014 `colyseus/vite` owns the HTTP\n * server and never calls `Server.listen()`, so it runs this step itself.\n * Returns the finalized router, which the database may have extended.\n *\n * Pass `runBeforeListen: false` when re-booting after an HMR reload: the\n * `beforeListen` hook fires once because the server listens once, while the\n * database and the router defaults track the instances the reload rebuilt.\n */\nexport function prepareServices(server: Server, runBeforeListen: boolean): Promise<Router> {\n // `prepareServices` is protected \u2014 first-party hosts reach it through here\n // rather than widening the published `Server` API.\n return (server as unknown as {\n prepareServices(runBeforeListen: boolean): Promise<Router>;\n }).prepareServices(runBeforeListen);\n}\n"],
5
+ "mappings": ";AAaA,YAAY,gBAAgB;AAG5B;AAAA,EACE;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAAA;AAAA,OAIK;AAQA,SAAS,qBACd,QACA,SAC4B;AAC5B,SAAO,iBAA4B,qBAAqB,2BAAgB,QAAQ,OAAO;AACzF;AAWO,SAAS,gBAAgB,QAAgB,iBAA2C;AAGzF,SAAQ,OAEL,gBAAgB,eAAe;AACpC;",
6
6
  "names": ["listUserSessions"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.9",
3
+ "version": "0.18.11",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -59,17 +59,17 @@
59
59
  "@colyseus/schema": "^5.0.8",
60
60
  "express": "^5.0.0",
61
61
  "vitest": "^3.1.1",
62
- "@colyseus/redis-driver": "^0.18.2",
63
62
  "@colyseus/redis-presence": "^0.18.3",
64
- "@colyseus/tools": "^0.18.3"
63
+ "@colyseus/tools": "^0.18.3",
64
+ "@colyseus/redis-driver": "^0.18.2"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@colyseus/schema": "^5.0.8",
68
68
  "@pm2/io": "^6.1.0",
69
69
  "express": "^4.16.0 || ^5.0.0",
70
70
  "zod": "^4.1.12",
71
- "@colyseus/auth": "^0.18.2",
72
71
  "@colyseus/ws-transport": "^0.18.2",
72
+ "@colyseus/auth": "^0.18.2",
73
73
  "@colyseus/better-call": "^1.3.1"
74
74
  },
75
75
  "peerDependenciesMeta": {
package/src/Room.ts CHANGED
@@ -847,13 +847,13 @@ export class Room<T extends RoomOptions = RoomOptions> {
847
847
  const reservedSeat = this._reservedSeats[sessionId];
848
848
 
849
849
  if (reservedSeat) {
850
- // seat reservation is present
851
- return (
852
- // not consumed
853
- (reservedSeat[2] === false) ||
854
- // reconnection is allowed and the reconnection token is valid.
855
- (reservedSeat[3] && this._reconnections[reconnectionToken]?.[0] === sessionId)
856
- )
850
+ return (reservedSeat[3])
851
+ // seat is held by allowReconnection(): only the issued token may claim it.
852
+ // (a token-less "not consumed yet" pass here would let anyone who knows the
853
+ // sessionId burn the seat — https://github.com/colyseus/colyseus/issues/962)
854
+ ? this._reconnections[reconnectionToken]?.[0] === sessionId
855
+ // fresh seat reservation: valid until consumed.
856
+ : reservedSeat[2] === false;
857
857
 
858
858
  } else if (typeof(reconnectionToken) === "string") {
859
859
  // potentially a stale client reference, so a reconnection attempt is possible.
@@ -2230,10 +2230,10 @@ export class Room<T extends RoomOptions = RoomOptions> {
2230
2230
  }
2231
2231
 
2232
2232
  // reset message count every second
2233
- if (this.clock.currentTime - client._lastMessageTime >= 1000) {
2233
+ if (this.clock.currentTime - (client._lastMessageTime ?? 0) >= 1000) {
2234
2234
  client._numMessagesLastSecond = 0;
2235
2235
  client._lastMessageTime = this.clock.currentTime;
2236
- } else if (++client._numMessagesLastSecond > this.maxMessagesPerSecond) {
2236
+ } else if ((client._numMessagesLastSecond = (client._numMessagesLastSecond ?? 0) + 1) > this.maxMessagesPerSecond) {
2237
2237
  // drop client if it sends more messages than the maximum allowed per second
2238
2238
  debugMatchMaking('dropping client - sessionId: \'%s\' (roomId: %s), too many messages per second', client.sessionId, this.roomId);
2239
2239
  return this.#_forciblyCloseClient(client, CloseCode.WITH_ERROR);
package/src/Server.ts CHANGED
@@ -41,6 +41,8 @@ export type ServerOptions = {
41
41
  */
42
42
  database?: {
43
43
  boot(): Promise<void>;
44
+ /** Release connections — a host that re-boots on reload disposes the instance it replaced. */
45
+ shutdown?(): Promise<void>;
44
46
  applyRouterDefaults?(router: Router): Router | Promise<Router>;
45
47
  },
46
48
 
@@ -149,7 +151,7 @@ export class Server<
149
151
  /**
150
152
  * Every side effect of construction: the dev-mode flag, the transport, the
151
153
  * matchmaker and the process signal handlers. Distinct from
152
- * {@link _bootServices}, which prepares user services once we're listening.
154
+ * {@link prepareServices}, which boots the services user code declared.
153
155
  *
154
156
  * Isolated so {@link DevServer} can opt out of all four — under
155
157
  * `colyseus/vite` the plugin owns them, and user code still gets a real
@@ -196,7 +198,7 @@ export class Server<
196
198
  * @param listeningListener
197
199
  */
198
200
  public async listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: Function) {
199
- await this._bootServices();
201
+ await this._bootForListen();
200
202
 
201
203
  //
202
204
  // if Colyseus Cloud is detected, use @colyseus/tools to listen
@@ -287,7 +289,7 @@ export class Server<
287
289
  * ```
288
290
  */
289
291
  public async serverless(): Promise<HttpServer> {
290
- await this._bootServices();
292
+ await this._bootForListen();
291
293
 
292
294
  // transport (and its HTTP server) must be ready before binding routes
293
295
  await this._onTransportReady;
@@ -440,22 +442,42 @@ export class Server<
440
442
  this.onBeforeShutdownCallback = callback;
441
443
  }
442
444
 
443
- // Boot sequence shared by listen() and serverless(): user `beforeListen`
444
- // hook, database boot, framework router defaults (auth/db endpoints), then
445
- // the express app. serverless() needs this too — its exported server still
446
- // serves the express callback and auth/database routes.
447
- private async _bootServices(): Promise<void> {
448
- const { beforeListen, database, express } = this.options;
445
+ /**
446
+ * Boot the services user code declared: the `beforeListen` hook, the
447
+ * database, and the endpoints they contribute to the router. Returns the
448
+ * finalized router, so a caller can't read a stale one by mistake.
449
+ *
450
+ * Reached from outside via `@colyseus/core/internal` — `colyseus/vite` owns
451
+ * the HTTP server and never calls {@link listen}, but user code there still
452
+ * expects `database.boot()` and the auth routes.
453
+ *
454
+ * `runBeforeListen` is false when a host re-boots after an HMR reload:
455
+ * `beforeListen` fires once because the server listens once, while the
456
+ * database and the router defaults track instances the reload rebuilt.
457
+ */
458
+ protected async prepareServices(runBeforeListen: boolean = true): Promise<Routes> {
459
+ const { beforeListen, database } = this.options;
460
+
461
+ if (runBeforeListen && beforeListen) { await beforeListen(); }
462
+ if (database) { await database.boot(); }
463
+
464
+ await this._applyRouterDefaults();
465
+
466
+ return this.router;
467
+ }
468
+
469
+ // Listen-time boot: the services above, then the express app. serverless()
470
+ // needs this too — its exported server still serves the express callback
471
+ // and the auth/database routes.
472
+ private async _bootForListen(): Promise<void> {
473
+ const { express } = this.options;
449
474
 
450
475
  // boot runs after module evaluation, so the env var wins over
451
476
  // simulateLatency() calls made at the top level of user code
452
477
  const envLatency = parseLatencyEnv();
453
478
  if (envLatency !== undefined) { this.simulateLatency(envLatency); }
454
479
 
455
- if (beforeListen) { await beforeListen(); }
456
- if (database) { await database.boot(); }
457
-
458
- await this._applyRouterDefaults();
480
+ await this.prepareServices();
459
481
 
460
482
  if (express) {
461
483
  await this._onTransportReady;
package/src/Transport.ts CHANGED
@@ -104,7 +104,8 @@ export async function runBeforeUpgrade(
104
104
  }
105
105
 
106
106
  try {
107
- return (await handler(request, context)) ?? undefined;
107
+ // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip
108
+ return (await handler(request, context)) || undefined;
108
109
 
109
110
  } catch (e: any) {
110
111
  debugAndPrintError(e);
package/src/internal.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  * @internal
13
13
  */
14
14
  import * as matchMaker from './MatchMaker.ts';
15
+ import type { Server } from './Server.ts';
16
+ import type { Router } from './router/index.ts';
15
17
  import {
16
18
  listUserSessions,
17
19
  type ListUserSessionsOptions,
@@ -44,3 +46,20 @@ export function listUserSessionsLive(
44
46
  ): Promise<UserSessionInfo[]> {
45
47
  return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);
46
48
  }
49
+
50
+ /**
51
+ * Boot a server's services without listening — `colyseus/vite` owns the HTTP
52
+ * server and never calls `Server.listen()`, so it runs this step itself.
53
+ * Returns the finalized router, which the database may have extended.
54
+ *
55
+ * Pass `runBeforeListen: false` when re-booting after an HMR reload: the
56
+ * `beforeListen` hook fires once because the server listens once, while the
57
+ * database and the router defaults track the instances the reload rebuilt.
58
+ */
59
+ export function prepareServices(server: Server, runBeforeListen: boolean): Promise<Router> {
60
+ // `prepareServices` is protected — first-party hosts reach it through here
61
+ // rather than widening the published `Server` API.
62
+ return (server as unknown as {
63
+ prepareServices(runBeforeListen: boolean): Promise<Router>;
64
+ }).prepareServices(runBeforeListen);
65
+ }