@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.
- package/build/Room.cjs +3 -7
- package/build/Room.cjs.map +2 -2
- package/build/Room.mjs +3 -7
- package/build/Room.mjs.map +2 -2
- package/build/Server.cjs +31 -14
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +18 -2
- package/build/Server.mjs +31 -14
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +1 -1
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.mjs +1 -1
- package/build/Transport.mjs.map +2 -2
- package/build/internal.cjs +5 -0
- package/build/internal.cjs.map +2 -2
- package/build/internal.d.ts +12 -0
- package/build/internal.mjs +4 -0
- package/build/internal.mjs.map +2 -2
- package/package.json +4 -4
- package/src/Room.ts +9 -9
- package/src/Server.ts +35 -13
- package/src/Transport.ts +2 -1
- package/src/internal.ts +19 -0
package/build/Transport.cjs.map
CHANGED
|
@@ -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": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAAsB;AACtB,YAAuB;AAIvB,0BAA0B;AAC1B,oBAA0B;AAG1B,oBAA6B;AAC7B,mBAAmC;AACnC,mBAA0C;AAC1C,yBAA4B;AAI5B,IAAI;AACG,SAAS,aAAa,WAAsB;AAAE,eAAa;AAAW;AACtE,SAAS,eAAe;AAAE,SAAO;AAAY;AAE7C,IAAe,YAAf,MAAyB;AA2BhC;AAyCA,eAAsB,iBACpB,SACA,KACA,SAC+B;AAC/B,MAAI;AAEJ,MAAI;AACF,UAAM,OAAO,QAAQ,QAAQ,IAAI,MAAM,KAAK;AAC5C,cAAU,IAAI,QAAQ,UAAU,IAAI,GAAG,GAAG,IAAI,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAAA,EAE5E,SAAS,GAAQ;AAEf,yCAAmB,CAAC;AACpB,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAEA,MAAI;
|
|
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
|
}
|
package/build/Transport.mjs
CHANGED
|
@@ -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)
|
|
29
|
+
return await handler(request, context) || void 0;
|
|
30
30
|
} catch (e) {
|
|
31
31
|
debugAndPrintError(e);
|
|
32
32
|
return new Response(null, { status: 500 });
|
package/build/Transport.mjs.map
CHANGED
|
@@ -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": ";AAAA,OAAsB;AACtB,OAAuB;AAIvB,SAAS,iBAAiB;AAC1B,OAA0B;AAG1B,OAA6B;AAC7B,SAAS,0BAA0B;AACnC,SAAS,gBAAgB,iBAAiB;AAC1C,SAAS,mBAAmB;AAI5B,IAAI;AACG,SAAS,aAAa,WAAsB;AAAE,eAAa;AAAW;AACtE,SAAS,eAAe;AAAE,SAAO;AAAY;AAE7C,IAAe,YAAf,MAAyB;AA2BhC;AAyCA,eAAsB,iBACpB,SACA,KACA,SAC+B;AAC/B,MAAI;AAEJ,MAAI;AACF,UAAM,OAAO,QAAQ,QAAQ,IAAI,MAAM,KAAK;AAC5C,cAAU,IAAI,QAAQ,UAAU,IAAI,GAAG,GAAG,IAAI,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAAA,EAE5E,SAAS,GAAQ;AAEf,uBAAmB,CAAC;AACpB,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAEA,MAAI;
|
|
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
|
}
|
package/build/internal.cjs
CHANGED
|
@@ -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,
|
package/build/internal.cjs.map
CHANGED
|
@@ -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;
|
|
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
|
}
|
package/build/internal.d.ts
CHANGED
|
@@ -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>;
|
package/build/internal.mjs
CHANGED
|
@@ -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,
|
package/build/internal.mjs.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
// not consumed
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|