@colyseus/core 0.18.19 → 0.18.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/Transport.ts"],
4
- "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, 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 * Rate limiting via maxMessagesPerSecond: frames seen so far, and the\n * moment the count goes back to zero.\n */\n _numMessagesLastSecond?: number;\n _messageCountResetsAt?: number;\n\n /**\n * Time of the most recent inbound frame. Moves on every frame \u2014 this is\n * the one to read for \"how long has this client been quiet?\" (see\n * IdleKickPlugin). The rate-limit counter above only resets once a second.\n */\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip\n return (await handler(request, context)) || undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n /**\n * Send raw bytes over the transport's reliable channel.\n *\n * The transport owns `data` from this call on: it may write it later (a\n * socket with backpressure, permessage-deflate), so callers never mutate it\n * afterwards. The same `data` may be handed to several clients.\n */\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n /**\n * {@link raw}, or held until the client finishes joining (or until after the\n * next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.\n */\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 * Unlike {@link raw}, `data` is only valid during this call: the caller\n * reuses its bytes afterwards, so a transport that sends it later copies it\n * first.\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 * Rate limiting via maxMessagesPerSecond: frames seen so far, and the\n * moment the count goes back to zero.\n */\n _numMessagesLastSecond?: number;\n _messageCountResetsAt?: number;\n\n /**\n * Time of the most recent inbound frame. Moves on every frame \u2014 this is\n * the one to read for \"how long has this client been quiet?\" (see\n * IdleKickPlugin). The rate-limit counter above only resets once a second.\n */\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
  }
@@ -169,7 +169,18 @@ export interface Client<T extends {
169
169
  * The format provided by .reconnect() from the client-side must follow: "${roomId}:${reconnectionToken}"
170
170
  */
171
171
  reconnectionToken: string;
172
+ /**
173
+ * Send raw bytes over the transport's reliable channel.
174
+ *
175
+ * The transport owns `data` from this call on: it may write it later (a
176
+ * socket with backpressure, permessage-deflate), so callers never mutate it
177
+ * afterwards. The same `data` may be handed to several clients.
178
+ */
172
179
  raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
180
+ /**
181
+ * {@link raw}, or held until the client finishes joining (or until after the
182
+ * next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.
183
+ */
173
184
  enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;
174
185
  /**
175
186
  * Send raw bytes over the transport's UNRELIABLE channel — no delivery,
@@ -178,6 +189,10 @@ export interface Client<T extends {
178
189
  * Absent on transports with no datagram channel (every WebSocket transport).
179
190
  * Its presence IS the capability check — callers feature-detect rather than
180
191
  * reading a separate flag, and skip clients that can't receive.
192
+ *
193
+ * Unlike {@link raw}, `data` is only valid during this call: the caller
194
+ * reuses its bytes afterwards, so a transport that sends it later copies it
195
+ * first.
181
196
  */
182
197
  rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
183
198
  /**
@@ -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 // `||`, 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 * Rate limiting via maxMessagesPerSecond: frames seen so far, and the\n * moment the count goes back to zero.\n */\n _numMessagesLastSecond?: number;\n _messageCountResetsAt?: number;\n\n /**\n * Time of the most recent inbound frame. Moves on every frame \u2014 this is\n * the one to read for \"how long has this client been quiet?\" (see\n * IdleKickPlugin). The rate-limit counter above only resets once a second.\n */\n _lastMessageTime?: number;\n\n /**\n * Per-client input Schema instance, allocated on join when the Room\n * declares `input`. Mutated in-place by {@link _inputDecoder} on each\n * incoming ROOM_INPUT_* packet.\n *\n * Typed loosely (`any`) so duplicate `@colyseus/schema` installs don't\n * trigger type-identity errors against user-defined input classes.\n */\n _input?: any;\n _inputDecoder?: InputDecoder;\n\n /**\n * Per-client buffer of cloned input snapshots, allocated on join when\n * `Room.inputOptions.bufferMaxSize > 0`. Populated on each decoded frame.\n */\n _inputBuffer?: import('./input/InputBuffer.ts').InputBufferImpl;\n\n /**\n * Cached per-client accessor returned by `room.input(sessionId)`. Built\n * once at join (when the Room called `defineInput()`), so the public API\n * call is a Map lookup + property read with no per-call allocation.\n */\n _inputAccessor?: import('./input/types.ts').InputAccessor;\n\n /**\n * Used for rate limiting ROOM_INPUT_* packets via maxInputsPerSecond,\n * independent of maxMessagesPerSecond.\n */\n _numInputsLastSecond?: number;\n _lastInputTime?: number;\n\n /**\n * `performance.now()` recorded when the most recent ROOM_INPUT_* packet\n * from this client was received. Receive-side diagnostic only \u2014 NOT on the\n * wire: the {@link ProtocolModifier.TIMED} state prefix acks the seq of the\n * last input CONSUMED into the state (the input buffer's `ackSeq`).\n *\n * `0` until the client has sent its first input.\n */\n _lastInputReceivedAt?: number;\n\n /**\n * Monotonic count of *reliable* inputs successfully received from this\n * client. Receive-time counter \u2014 it LEADS the state by inputs still\n * buffered; what the TIMED prefix acks is the CONSUMED seq (the input\n * buffer's `ackSeq`), not this. Stays at the default `0` until the client\n * sends its first reliable input.\n *\n * Only ROOM_INPUT_RELIABLE bumps this \u2014 unreliable's redundant-ring\n * pattern would double-count.\n */\n _receivedInputCount?: number;\n\n /**\n * Running baseline for the DELTA-CODED lag-comp stamp on ROOM_INPUT_RELIABLE\n * frames (the {@link ProtocolModifier.TIMED} prefix). Each frame carries only\n * the signed change from the previous stamp; this accumulates them back into\n * the absolute timeline value. Zeroed on (re)connect alongside the SDK's own\n * baseline so the first delta after a reset is absolute. `0` until allocated.\n */\n _reckonBaseline?: number;\n\n /**\n * @internal Per-client raw frames staged to ride out right AFTER this client's\n * next state patch \u2014 per-client `afterNextPatch` messages. Lazily allocated.\n * Pushed by {@link enqueueClientRaw} (the `afterNextPatch` path), flushed as\n * standalone frames after the patch by {@link Room._flushPendingClientFrames}.\n * Room-level `broadcast` `afterNextPatch` uses the Room's own queue instead, not\n * this buffer.\n */\n _pendingFrames?: Uint8Array[];\n\n /**\n * @internal Back-reference to the Room's \"clients with staged frames\" list,\n * shared by reference at join. {@link enqueueClientRaw} pushes the client here\n * on its first staged frame of a cycle, so the after-patch flush iterates just\n * those clients rather than scanning every client.\n */\n _pendingFrameClients?: Array<Client & ClientPrivate>;\n}\n\n/**\n * The framework-level send path shared by every transport's `enqueueRaw` \u2014 the\n * single source of truth so each transport implements only the wire-level `raw`.\n * Routes a raw frame by where it should go:\n *\n * - `afterNextPatch` \u2192 stage onto the per-client {@link ClientPrivate._pendingFrames}\n * buffer, sent as a standalone frame right AFTER the next state patch; a\n * no-allocation push into a reused array. The client announces itself to the\n * Room's {@link ClientPrivate._pendingFrameClients} list on its first staged\n * frame of a cycle.\n * - before JOIN \u2192 buffer in `_enqueuedMessages` until the JOIN_ROOM handshake flushes.\n * - otherwise \u2192 send now via the transport's `raw`.\n *\n * @internal\n */\nexport function enqueueClientRaw(\n client: Client & ClientPrivate,\n data: Uint8Array | Buffer,\n options?: ISendOptions,\n): void {\n if (options?.afterNextPatch) {\n let frames = client._pendingFrames;\n if (frames === undefined) { frames = client._pendingFrames = []; }\n if (frames.length === 0) { client._pendingFrameClients?.push(client); } // first frame this cycle\n frames.push(data);\n return;\n }\n if (client.state !== ClientState.JOINED) {\n // During `onJoin` / `onReconnect` the client can't register onMessage\n // handlers yet \u2014 buffer until JOIN_ROOM has been sent.\n client._enqueuedMessages?.push(data);\n return;\n }\n client.raw(data, options);\n}\n\nexport class ClientArray<C extends Client = Client> extends Array<C> {\n /**\n * Secondary index for O(1) lookup by sessionId. Kept in sync by the\n * mutating methods overridden below. Direct index assignment\n * (`arr[i] = client`) and `arr.length = 0` bypass this index \u2014 use\n * `push` / `splice` / `delete` / `pop` / `shift` / `unshift` instead.\n */\n private _byId: Map<string, C> = new Map();\n\n /** The client for `sessionId`, or `undefined` \u2014 O(1). The canonical per-session\n * lookup (mirrors `room.inputs.get(sessionId)`). */\n public get(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n /** @deprecated Use {@link get}. */\n public getById(sessionId: string): C | undefined {\n return this._byId.get(sessionId);\n }\n\n public delete(client: C): boolean {\n const removed = spliceOne(this, this.indexOf(client));\n if (removed) this._byId.delete(client.sessionId);\n return removed;\n }\n\n public push(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.push(...items);\n }\n\n public pop(): C | undefined {\n const removed = super.pop();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public shift(): C | undefined {\n const removed = super.shift();\n if (removed !== undefined) this._byId.delete(removed.sessionId);\n return removed;\n }\n\n public unshift(...items: C[]): number {\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return super.unshift(...items);\n }\n\n public splice(start: number, deleteCount?: number, ...items: C[]): C[] {\n const removed = (deleteCount === undefined)\n ? super.splice(start)\n : super.splice(start, deleteCount, ...items);\n for (let i = 0; i < removed.length; i++) this._byId.delete(removed[i].sessionId);\n for (let i = 0; i < items.length; i++) this._byId.set(items[i].sessionId, items[i]);\n return removed;\n }\n}\n\n/**\n * Shared internal method to connect a Client into a Room.\n * Validates seat reservation and joins the client to the room.\n *\n * @remarks\n * **\u26A0\uFE0F This is an internal API and not intended for end-user use.**\n *\n * @internal\n */\nexport async function connectClientToRoom(\n room: Room | undefined,\n client: Client & ClientPrivate,\n authContext: AuthContext,\n connectionOptions: {\n reconnectionToken?: string;\n skipHandshake?: boolean;\n },\n): Promise<void> {\n if (!room || !room.hasReservedSeat(client.sessionId, connectionOptions.reconnectionToken)) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, 'seat reservation expired.');\n }\n\n await room['_onJoin'](client, authContext, connectionOptions);\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as http from 'http';\nimport * as https from 'https';\n\nimport type { Router } from '@colyseus/better-call';\n\nimport { ErrorCode } from '@colyseus/shared-types';\nimport { StateView } from '@colyseus/schema';\nimport type { InputDecoder } from '@colyseus/schema/input';\n\nimport { EventEmitter } from 'events';\nimport { debugAndPrintError } from './Debug.ts';\nimport { getBearerToken, spliceOne } from './utils/Utils.ts';\nimport { ServerError } from './errors/ServerError.ts';\n\nimport type { Room } from './Room.ts';\n\nlet _transport: Transport | undefined;\nexport function setTransport(transport: Transport) { _transport = transport; }\nexport function getTransport() { return _transport; }\n\nexport abstract class Transport {\n public protocol?: string;\n /** Self-signed cert SHA-256 hash (byte array), surfaced to clients in the\n * matchmake response so a WebTransport client can pin it via\n * `serverCertificateHashes`. Set by transports that generate their own cert\n * (h3). Undefined for transports using a CA-trusted cert. */\n public fingerprint?: number[];\n public server?: http.Server | https.Server;\n\n public abstract listen(port?: number | string, hostname?: string, backlog?: number, listeningListener?: Function): this;\n public abstract shutdown(): void;\n\n public abstract simulateLatency(milliseconds: number): void;\n\n /**\n * Returns an Express-compatible application for HTTP route handling.\n * For uWebSockets transport, this uses the uwebsockets-express module.\n * This method is called lazily only when an express callback is provided in server options.\n */\n public getExpressApp?(): Promise<import('express').Application> | import('express').Application | undefined;\n\n /**\n * Binds a router to the transport.\n * Some transports may have a custom way to bind a router to the transport.\n * (uWebSocketsTransport)\n */\n public bindRouter?(router: Router): void;\n}\n\n/**\n * Intercepts an incoming WebSocket upgrade request, before the handshake.\n *\n * Return a `Response` to answer the request instead of upgrading it. Return\n * nothing to upgrade as usual. The handler may be async, and the handshake waits\n * for it to resolve.\n *\n * `context` is the same shape `onAuth()` receives, read-only here: mutating it\n * does not carry over to `onAuth()`.\n *\n * Not supported by `H3Transport`: WebTransport has no upgrade handshake.\n *\n * @example\n * ```typescript\n * new uWebSocketsTransport({\n * beforeUpgrade: async (request, context) => {\n * if (await isBanned(context.ip)) {\n * return new Response(null, { status: 403 });\n * }\n * }\n * });\n * ```\n */\nexport type BeforeUpgradeHandler = (\n request: Request,\n context: Readonly<AuthContext>,\n) => Response | void | Promise<Response | void>;\n\n/**\n * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send\n * instead of upgrading, or `undefined` to proceed with the upgrade.\n *\n * Every transport goes through here, so a handler written against one keeps\n * working on the others. Never rejects: uWebSockets.js aborts the process on an\n * upgrade handler that yields without responding, and on the other transports a\n * raw socket left behind leaks a connection.\n *\n * @internal\n */\nexport async function runBeforeUpgrade(\n handler: BeforeUpgradeHandler,\n url: string, // path, optionally including the query string\n context: AuthContext,\n): Promise<Response | undefined> {\n let request: Request;\n\n try {\n const host = context.headers.get('host') || 'localhost';\n request = new Request(`http://${host}${url}`, { headers: context.headers });\n\n } catch (e: any) {\n // a `Host` header that isn't a valid authority fails to parse as a URL\n debugAndPrintError(e);\n return new Response(null, { status: 400 });\n }\n\n try {\n // `||`, not `??`: a handler that returns nothing is typed `void`, which `??` doesn't strip\n return (await handler(request, context)) || undefined;\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(null, { status: 500 });\n }\n}\n\n/** Headers as the transport has them: uWebSockets.js and Node give a plain record. */\ntype RawHeaders = Headers | Record<string, string | undefined>;\n\nconst readHeader = (headers: RawHeaders, name: string) =>\n (headers instanceof Headers) ? headers.get(name) : headers[name];\n\n/**\n * Builds the context passed to `beforeUpgrade` and `onAuth`.\n *\n * Every transport goes through here, so the context is identical everywhere,\n * down to how the client address is resolved. `headers` is materialized on\n * first read: a connection nobody inspects pays nothing for the conversion.\n *\n * @internal\n */\nexport function createAuthContext(options: {\n headers: RawHeaders,\n token?: string | null,\n remoteAddress?: string,\n req?: any,\n}): AuthContext {\n const source = options.headers;\n let headers: Headers | undefined;\n\n return {\n token: options.token ?? getBearerToken(readHeader(source, 'authorization')),\n ip: resolveClientIp(source, options.remoteAddress),\n req: options.req,\n get headers() {\n return headers ??= (source instanceof Headers)\n ? source\n : new Headers(source as Record<string, string>);\n },\n };\n}\n\n/**\n * A single address, resolved the same way on every transport: `x-forwarded-for`\n * carries the whole proxy chain, and only its first entry is the client.\n */\nfunction resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {\n // an empty header counts as absent\n const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;\n\n return (\n firstHop('x-real-ip') ??\n firstHop('x-forwarded-for') ??\n firstHop('x-client-ip') ??\n (remoteAddress || undefined)\n );\n}\n\nexport type AuthContext = {\n token?: string,\n /** Undefined when no proxy header carries it and the transport has no peer address. */\n ip: string | undefined;\n headers: Headers,\n /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */\n req?: any;\n};\n\nexport interface ISendOptions {\n afterNextPatch?: boolean;\n}\n\nexport const ClientState = {\n JOINING: 0,\n JOINED: 1,\n RECONNECTING: 2,\n RECONNECTED: 3,\n LEAVING: 4,\n CLOSED: 5\n} as const;\nexport type ClientState = (typeof ClientState)[keyof typeof ClientState];\n\n// Helper types to extract properties from the Client type parameter\ntype ExtractClientUserData<T> = T extends { userData: infer U } ? U : T;\ntype ExtractClientAuth<T> = T extends { auth: infer A } ? A : any;\ntype ExtractClientMessages<T> = T extends { messages: infer M } ? M : any;\n\n// Helper type to make message required when the message type demands it\nexport type MessageArgs<M, Options> =\n unknown extends M ? [message?: M, options?: Options] : // Handle 'any' type (backwards compatibility)\n [M] extends [never] ? [message?: M, options?: Options] :\n [M] extends [void] ? [message?: M, options?: Options] :\n [M] extends [undefined] ? [message?: M, options?: Options] :\n undefined extends M ? [message?: M, options?: Options] :\n [message: M, options?: Options];\n\n/**\n * The client instance from the server-side is responsible for the transport layer between the server and the client.\n * It should not be confused with the Client from the client-side SDK, as they have completely different purposes!\n * You operate on client instances from `this.clients`, `Room#onJoin()`, `Room#onLeave()` and `Room#onMessage()`.\n *\n * - This is the raw WebSocket connection coming from the `ws` package. There are more methods available which aren't\n * encouraged to use along with Colyseus.\n */\nexport interface Client<T extends { userData?: any, auth?: any, messages?: Record<string | number, any> } = any> {\n '~messages': ExtractClientMessages<T>;\n\n ref: EventEmitter;\n\n /**\n * Unique id per session.\n */\n sessionId: string;\n\n /**\n * Connection state\n */\n state: ClientState;\n\n /**\n * Optional: when using `@view()` decorator in your state properties, this will be the view instance for this client.\n */\n view?: StateView;\n\n /**\n * User-defined data can be attached to the Client instance through this variable.\n * - Can be used to store custom data about the client's connection. userData is not synchronized with the client,\n * and should be used only to keep player-specific with its connection.\n */\n userData?: ExtractClientUserData<T>;\n\n /**\n * auth data provided by your `onAuth`\n */\n auth?: ExtractClientAuth<T>;\n\n /**\n * Reconnection token used to re-join the room after onLeave + allowReconnection().\n *\n * IMPORTANT:\n * This is not the full reconnection token the client provides for the server.\n * The format provided by .reconnect() from the client-side must follow: \"${roomId}:${reconnectionToken}\"\n */\n reconnectionToken: string;\n\n // TODO: move these to ClientPrivate\n /**\n * Send raw bytes over the transport's reliable channel.\n *\n * The transport owns `data` from this call on: it may write it later (a\n * socket with backpressure, permessage-deflate), so callers never mutate it\n * afterwards. The same `data` may be handed to several clients.\n */\n raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;\n /**\n * {@link raw}, or held until the client finishes joining (or until after the\n * next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.\n */\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 * Unlike {@link raw}, `data` is only valid during this call: the caller\n * reuses its bytes afterwards, so a transport that sends it later copies it\n * first.\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 * Rate limiting via maxMessagesPerSecond: frames seen so far, and the\n * moment the count goes back to zero.\n */\n _numMessagesLastSecond?: number;\n _messageCountResetsAt?: number;\n\n /**\n * Time of the most recent inbound frame. Moves on every frame \u2014 this is\n * the one to read for \"how long has this client been quiet?\" (see\n * IdleKickPlugin). The rate-limit counter above only resets once a second.\n */\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
  }
@@ -50,6 +50,10 @@ function sendClientFrame(client, encodedBody, timing) {
50
50
  client.raw(encodedBody);
51
51
  }
52
52
  }
53
+ function ownedPatch(encoded, timing) {
54
+ return timing ? encoded : Buffer.from(encoded);
55
+ }
56
+ var HEARTBEAT_BODY = new Uint8Array([import_shared_types.Protocol.ROOM_STATE_PATCH]);
53
57
  var UNRELIABLE_HEADER_SIZE = 2;
54
58
  function stateHasUnreliableFields(context) {
55
59
  for (const klass of context.schemas.keys()) {
@@ -75,9 +79,6 @@ var SchemaSerializer = class {
75
79
  this.hasFilters = false;
76
80
  // flag to avoid re-encoding full state if no changes were made
77
81
  this.needFullEncode = true;
78
- // TODO: make this optional. allocating a new buffer for each room may not be always necessary.
79
- this.fullEncodeBuffer = new Uint8Array(import_schema.Encoder.BUFFER_SIZE);
80
- this.sharedOffsetCache = { offset: 0 };
81
82
  /** @see {@link Serializer.hasUnreliableFields} */
82
83
  this.hasUnreliableFields = false;
83
84
  /** Room-wide counter stamped into every unreliable frame so the client can
@@ -90,7 +91,7 @@ var SchemaSerializer = class {
90
91
  this.encoder = new import_schema.Encoder(newState);
91
92
  this.hasFilters = this.encoder.context.hasFilters;
92
93
  this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);
93
- this.fullEncodeBuffer[0] = import_shared_types.Protocol.ROOM_STATE;
94
+ this.needFullEncode = true;
94
95
  if (this.hasFilters) {
95
96
  this.encodedViews = /* @__PURE__ */ new Map();
96
97
  if (this.hasUnreliableFields) {
@@ -100,16 +101,15 @@ var SchemaSerializer = class {
100
101
  }
101
102
  getFullState(client, timing) {
102
103
  if (this.needFullEncode || this.encoder.root.changes.next !== void 0) {
103
- this.sharedOffsetCache = { offset: 1 };
104
- this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);
104
+ this.encoder.sharedBuffer[0] = import_shared_types.Protocol.ROOM_STATE;
105
+ this.fullEncodeCache = this.encoder.encodeAll({ offset: 1 }).slice();
105
106
  this.needFullEncode = false;
106
107
  }
107
- const body = this.hasFilters && client?.view ? this.encoder.encodeAllView(
108
- client.view,
109
- this.sharedOffsetCache.offset,
110
- { ...this.sharedOffsetCache },
111
- this.fullEncodeBuffer
112
- ) : this.fullEncodeCache;
108
+ let body = this.fullEncodeCache;
109
+ if (this.hasFilters && client?.view) {
110
+ this.encoder.sharedBuffer.set(body);
111
+ body = this.encoder.encodeAllView(client.view, body.byteLength, { offset: body.byteLength });
112
+ }
113
113
  if (timing && client) {
114
114
  return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));
115
115
  }
@@ -140,11 +140,10 @@ var SchemaSerializer = class {
140
140
  }
141
141
  }
142
142
  if (timing) {
143
- const heartbeatBody = Buffer.from([import_shared_types.Protocol.ROOM_STATE_PATCH]);
144
143
  for (let i = 0; i < clients.length; i++) {
145
144
  const client = clients[i];
146
145
  if (client.state !== import_Transport.ClientState.JOINED) continue;
147
- sendClientFrame(client, heartbeatBody, timing);
146
+ sendClientFrame(client, HEARTBEAT_BODY, timing);
148
147
  }
149
148
  }
150
149
  return false;
@@ -157,12 +156,13 @@ var SchemaSerializer = class {
157
156
  this.encoder.sharedBuffer[0] = import_shared_types.Protocol.ROOM_STATE_PATCH;
158
157
  const encodedChanges = this.encoder.encode(it);
159
158
  if (!this.hasFilters) {
159
+ const frame = ownedPatch(encodedChanges, timing);
160
160
  while (numClients--) {
161
161
  const client = clients[numClients];
162
162
  if (client.state !== import_Transport.ClientState.JOINED) {
163
163
  continue;
164
164
  }
165
- sendClientFrame(client, encodedChanges, timing);
165
+ sendClientFrame(client, frame, timing);
166
166
  }
167
167
  } else {
168
168
  const sharedOffset = it.offset;
@@ -174,7 +174,7 @@ var SchemaSerializer = class {
174
174
  const view = client.view || SHARED_VIEW;
175
175
  let encodedView = this.encodedViews.get(view);
176
176
  if (encodedView === void 0) {
177
- encodedView = view === SHARED_VIEW ? encodedChanges : this.encoder.encodeView(client.view, sharedOffset, it);
177
+ encodedView = view === SHARED_VIEW ? ownedPatch(encodedChanges, timing) : this.encoder.encodeView(client.view, sharedOffset, it);
178
178
  this.encodedViews.set(view, encodedView);
179
179
  }
180
180
  sendClientFrame(client, encodedView, timing);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/serializer/SchemaSerializer.ts"],
4
- "sourcesContent": ["import { Protocol, ProtocolModifier } from '@colyseus/shared-types';\n\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\nimport { type Client, type ClientPrivate, ClientState } from '../Transport.ts';\n\nimport { type Iterator, $numFields, encode, Encoder, dumpChanges, Metadata, Reflection, Schema, StateView, TypeContext } from '@colyseus/schema';\nimport { debugPatch } from '../Debug.ts';\nimport { logger } from '../Logger.ts';\n\n/**\n * Size of the {@link ProtocolModifier.TIMED} prefix that follows the\n * protocol byte:\n * `[uint32 sNow][uint32 inputSeq]`\n *\n * - `sNow` \u2014 server clock as **milliseconds since room start**\n * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,\n * language-agnostic integer (C/C#/Lua have no `performance.now()`); the\n * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the\n * client's reconstructed `serverNow()` stays in phase. Wraps at u32\n * (~49.7 days uptime) \u2014 irrelevant per session. Drives the client clock\n * offset / `serverNow()`.\n * - `inputSeq` \u2014 seq VALUE of the last input CONSUMED into the authoritative\n * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:\n * equals the consumed count (inputs are sequenced implicitly by receive\n * order). Unreliable: the framework wire seq, so a fully-dropped input\n * doesn't make the ack lag the client's sent seq by the lost count. This\n * single number is the canonical reconciliation ack (\"the sequence number\n * of the last input the server processed\"); the client prunes its\n * pending-input buffer against it AND derives RTT from its round-trip\n * (`now \u2212 sendTime(inputSeq)`). No separate received-seq / received-time \u2014\n * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.\n */\nconst TIMED_PREFIX_SIZE = 8;\n\n/**\n * Construct a per-recipient TIMED buffer:\n * `[code|TIMED][sNow][inputSeq][...schema bytes]`\n *\n * `encodedBody` is the shared encoded buffer whose byte 0 already holds the\n * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the\n * schema payload. We construct a fresh per-client Buffer so the transport\n * never sees a buffer that might mutate between queue and send.\n *\n * Uses the schema `encode.*` helpers to keep the wire layout symmetric with\n * the SDK's `decode.*`-driven reader.\n */\nfunction buildTimedFrame(\n encodedBody: Uint8Array,\n sNow: number,\n inputSeq: number,\n): Buffer {\n const schemaPayload = encodedBody.subarray(1);\n const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);\n out[0] = encodedBody[0] | ProtocolModifier.TIMED;\n const it: Iterator = { offset: 1 };\n encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)\n encode.uint32(out, inputSeq >>> 0, it);\n out.set(schemaPayload, it.offset);\n return out;\n}\n\n/** Seq VALUE of the last input consumed from the client's buffer \u2014 the\n * reconciliation ack (and RTT correlation key). Reliable: equals the consumed\n * count; unreliable: the framework wire seq (so packet loss doesn't skew it).\n * 0 when the room doesn't buffer input. */\nfunction clientProcessedInputSeq(client: Client): number {\n return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;\n}\n\n/**\n * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,\n * else the shared `encodedBody` passed straight through (no per-client copy). Any\n * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here\n * \u2014 they go out standalone right after the patch via `Room._flushPendingClientFrames`.\n */\nfunction sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {\n if (timing) {\n client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));\n } else {\n client.raw(encodedBody);\n }\n}\n\n/**\n * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the\n * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for\n * the wrap-safe freshness rule the client applies to `seq`.\n */\nconst UNRELIABLE_HEADER_SIZE = 2;\n\n/**\n * Does any Schema class reachable from the state root declare an `@unreliable`\n * field? Walked once per room, in {@link SchemaSerializer.reset} \u2014 the result\n * gates whether the unreliable channel is wired up at all, so rooms that don't\n * use it never reach any of the code below.\n */\nfunction stateHasUnreliableFields(context: TypeContext): boolean {\n for (const klass of context.schemas.keys()) {\n const metadata = klass[Symbol.metadata];\n if (metadata === undefined) { continue; }\n\n // `$numFields` is the last index, inclusive (matches `Metadata.getFields`).\n for (let i = 0; i <= metadata[$numFields]; i++) {\n if (Metadata.hasUnreliableAtIndex(metadata, i)) { return true; }\n }\n }\n return false;\n}\n\n/** Can this client receive the unreliable channel right now? Its transport\n * exposing {@link Client.rawUnreliable} IS the capability check. */\nfunction canReceiveUnreliable(client: Client): boolean {\n return client.state === ClientState.JOINED && client.rawUnreliable !== undefined;\n}\n\nconst SHARED_VIEW = {};\n\nexport class SchemaSerializer<T extends Schema> implements Serializer<T> {\n public id = 'schema';\n\n protected encoder: Encoder<T>;\n protected hasFilters: boolean = false;\n\n protected handshakeCache: Uint8Array;\n\n // flag to avoid re-encoding full state if no changes were made\n protected needFullEncode: boolean = true;\n\n // TODO: make this optional. allocating a new buffer for each room may not be always necessary.\n protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);\n protected fullEncodeCache: Uint8Array;\n protected sharedOffsetCache: Iterator = { offset: 0 };\n\n protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** @see {@link Serializer.hasUnreliableFields} */\n public hasUnreliableFields: boolean = false;\n\n /** Room-wide counter stamped into every unreliable frame so the client can\n * drop reordered datagrams. Wraps at 65536 \u2014 see {@link ProtocolModifier.UNRELIABLE}. */\n protected unreliableSeq: number = 0;\n\n protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */\n protected warnedNoUnreliableTransport = false;\n\n public reset(newState: T & Schema) {\n this.encoder = new Encoder(newState);\n this.hasFilters = this.encoder.context.hasFilters;\n this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);\n\n // cache ROOM_STATE byte as part of the encoded buffer\n this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;\n\n if (this.hasFilters) {\n this.encodedViews = new Map();\n\n if (this.hasUnreliableFields) {\n // Separate from `encodedViews`: the two channels encode on independent\n // cadences, so a reliable tick's cache must never be served to one.\n this.encodedUnreliableViews = new Map();\n }\n }\n }\n\n public getFullState(client?: Client, timing?: PatchTimingContext) {\n if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {\n this.sharedOffsetCache = { offset: 1 };\n this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);\n this.needFullEncode = false;\n }\n\n const body = (this.hasFilters && client?.view)\n ? this.encoder.encodeAllView(\n client.view,\n this.sharedOffsetCache.offset,\n { ...this.sharedOffsetCache },\n this.fullEncodeBuffer\n )\n : this.fullEncodeCache;\n\n if (timing && client) {\n return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));\n }\n return body;\n }\n\n public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {\n let numClients = clients.length;\n\n if (numClients === 0) {\n if (this.encoder.hasChanges) {\n // if there are changes but no clients, we need to encode full state on next patch\n this.needFullEncode = true;\n }\n // skip patching and clear changes\n this.encoder.discardChanges();\n return false;\n }\n\n if (!this.encoder.hasChanges) {\n\n // check if views have changes (manual add() or remove() items)\n if (this.hasFilters) {\n //\n // FIXME: refactor this to avoid duplicating code.\n //\n // it's probably better to have 2 different 'applyPatches' methods.\n // (one for handling state with filters, and another for handling state without filters)\n //\n const clientsWithViewChange = clients.filter((client) => {\n return client.state === ClientState.JOINED && client.view?.changes.size > 0\n });\n\n if (clientsWithViewChange.length > 0) {\n const it: Iterator = { offset: 1 };\n\n const sharedOffset = it.offset;\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n clientsWithViewChange.forEach((client) => {\n const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);\n sendClientFrame(client, encodedView, timing);\n });\n }\n }\n\n // Heartbeat: `defineInput()` rooms still need periodic timing samples\n // to keep client clock offset / RTT estimates fresh during quiet\n // ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema\n // body \u2014 1 byte protocol + 8 bytes prefix.\n // No-op for rooms without `defineInput()` (timing is undefined).\n if (timing) {\n const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);\n for (let i = 0; i < clients.length; i++) {\n const client = clients[i];\n if (client.state !== ClientState.JOINED) continue;\n sendClientFrame(client, heartbeatBody, timing);\n }\n }\n\n // skip patching state if:\n // - no clients are connected\n // - no changes were made\n // - no \"filtered changes\" were made when using filters\n return false;\n }\n\n this.needFullEncode = true;\n\n // dump changes for patch debugging\n if (debugPatch.enabled) {\n (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);\n }\n\n // get patch bytes\n const it: Iterator = { offset: 1 };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encode(it);\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n sendClientFrame(client, encodedChanges, timing);\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n // encode state multiple times, for each client\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeView(client.view, sharedOffset, it);\n this.encodedViews.set(view, encodedView);\n }\n\n sendClientFrame(client, encodedView, timing);\n }\n\n // clear views\n this.encodedViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardChanges();\n\n // debug patches\n if (debugPatch.enabled) {\n debugPatch(\n '%d bytes sent to %d clients, %j',\n encodedChanges.length,\n clients.length,\n (debugPatch as any).dumpChanges,\n );\n }\n\n return true;\n }\n\n /**\n * Encode and send the UNRELIABLE channel \u2014 the `@unreliable` fields, which\n * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}\n * (a WebTransport datagram); clients whose transport has no unreliable\n * channel are skipped entirely.\n *\n * Runs on its own cadence, independent of `applyPatches`. Only ever called on\n * rooms where {@link hasUnreliableFields} is true.\n */\n public applyUnreliablePatches(clients: Client[]): boolean {\n if (!this.encoder.hasUnreliableChanges) { return false; }\n\n // These mutations are visible to `encodeAll`, so the cached full state is\n // stale \u2014 even though `root.changes` (the reliable queue) is empty and\n // `getFullState`'s own guard would therefore miss it.\n this.needFullEncode = true;\n\n if (!clients.some(canReceiveUnreliable)) {\n this.warnNoUnreliableTransport(clients);\n // Drain regardless: the queue is not self-clearing, and an un-discarded\n // tick would grow it without bound.\n this.encoder.discardUnreliableChanges();\n return false;\n }\n\n this.unreliableSeq = (this.unreliableSeq + 1) & 0xFFFF;\n\n // Stamp the header before encoding, as the reliable path does. Safe across\n // a buffer overflow: `Encoder._resizeBuffer` copies the old bytes forward.\n const it: Iterator = { offset: 1 + UNRELIABLE_HEADER_SIZE };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH | ProtocolModifier.UNRELIABLE;\n encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encodeUnreliable(it);\n\n let numClients = clients.length;\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n if (canReceiveUnreliable(client)) { client.rawUnreliable!(encodedChanges); }\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n while (numClients--) {\n const client = clients[numClients];\n if (!canReceiveUnreliable(client)) { continue; }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedUnreliableViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);\n this.encodedUnreliableViews.set(view, encodedView);\n }\n\n client.rawUnreliable!(encodedView);\n }\n\n // clear views\n this.encodedUnreliableViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardUnreliableChanges();\n\n return true;\n }\n\n /**\n * The state declares `@unreliable` fields but not one connected client can\n * receive them \u2014 every WebSocket transport lacks a datagram channel. Those\n * fields keep their join-time value and never update, which is silent\n * otherwise. Warns once per room.\n */\n private warnNoUnreliableTransport(clients: Client[]) {\n if (this.warnedNoUnreliableTransport || clients.length === 0) { return; }\n this.warnedNoUnreliableTransport = true;\n logger.warn(\n \"@colyseus/core: state has @unreliable fields, but no connected client's transport\" +\n \" provides an unreliable channel \u2014 those fields will not be patched.\" +\n \" Use @colyseus/h3-transport (WebTransport) to enable them.\"\n );\n }\n\n public handshake() {\n /**\n * Cache handshake to avoid encoding it for each client joining\n */\n if (!this.handshakeCache) {\n //\n // TODO: re-use handshake buffer for all rooms of same type (?)\n //\n this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));\n }\n\n return this.handshakeCache;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Protocol, ProtocolModifier } from '@colyseus/shared-types';\n\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\nimport { type Client, type ClientPrivate, ClientState } from '../Transport.ts';\n\nimport { type Iterator, $numFields, encode, Encoder, dumpChanges, Metadata, Reflection, Schema, StateView, TypeContext } from '@colyseus/schema';\nimport { debugPatch } from '../Debug.ts';\nimport { logger } from '../Logger.ts';\n\n/**\n * Size of the {@link ProtocolModifier.TIMED} prefix that follows the\n * protocol byte:\n * `[uint32 sNow][uint32 inputSeq]`\n *\n * - `sNow` \u2014 server clock as **milliseconds since room start**\n * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,\n * language-agnostic integer (C/C#/Lua have no `performance.now()`); the\n * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the\n * client's reconstructed `serverNow()` stays in phase. Wraps at u32\n * (~49.7 days uptime) \u2014 irrelevant per session. Drives the client clock\n * offset / `serverNow()`.\n * - `inputSeq` \u2014 seq VALUE of the last input CONSUMED into the authoritative\n * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:\n * equals the consumed count (inputs are sequenced implicitly by receive\n * order). Unreliable: the framework wire seq, so a fully-dropped input\n * doesn't make the ack lag the client's sent seq by the lost count. This\n * single number is the canonical reconciliation ack (\"the sequence number\n * of the last input the server processed\"); the client prunes its\n * pending-input buffer against it AND derives RTT from its round-trip\n * (`now \u2212 sendTime(inputSeq)`). No separate received-seq / received-time \u2014\n * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.\n */\nconst TIMED_PREFIX_SIZE = 8;\n\n/**\n * Construct a per-recipient TIMED buffer:\n * `[code|TIMED][sNow][inputSeq][...schema bytes]`\n *\n * `encodedBody` is the shared encoded buffer whose byte 0 already holds the\n * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the\n * schema payload. We construct a fresh per-client Buffer so the transport\n * never sees a buffer that might mutate between queue and send.\n *\n * Uses the schema `encode.*` helpers to keep the wire layout symmetric with\n * the SDK's `decode.*`-driven reader.\n */\nfunction buildTimedFrame(\n encodedBody: Uint8Array,\n sNow: number,\n inputSeq: number,\n): Buffer {\n const schemaPayload = encodedBody.subarray(1);\n const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);\n out[0] = encodedBody[0] | ProtocolModifier.TIMED;\n const it: Iterator = { offset: 1 };\n encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)\n encode.uint32(out, inputSeq >>> 0, it);\n out.set(schemaPayload, it.offset);\n return out;\n}\n\n/** Seq VALUE of the last input consumed from the client's buffer \u2014 the\n * reconciliation ack (and RTT correlation key). Reliable: equals the consumed\n * count; unreliable: the framework wire seq (so packet loss doesn't skew it).\n * 0 when the room doesn't buffer input. */\nfunction clientProcessedInputSeq(client: Client): number {\n return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;\n}\n\n/**\n * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,\n * else the shared `encodedBody` passed straight through (no per-client copy). Any\n * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here\n * \u2014 they go out standalone right after the patch via `Room._flushPendingClientFrames`.\n */\nfunction sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {\n if (timing) {\n client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));\n } else {\n client.raw(encodedBody);\n }\n}\n\n/**\n * Detach an untimed patch from the encoder's shared buffer: the transport may\n * still hold it when the next encode reuses that buffer (see {@link Client.raw}).\n * Timed frames skip this \u2014 {@link buildTimedFrame} already copies per client.\n */\nfunction ownedPatch(encoded: Uint8Array, timing?: PatchTimingContext): Uint8Array {\n return timing ? encoded : Buffer.from(encoded);\n}\n\n/** Empty quiet-tick patch body; sent only timed, so sharing it is safe. */\nconst HEARTBEAT_BODY = new Uint8Array([Protocol.ROOM_STATE_PATCH]);\n\n/**\n * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the\n * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for\n * the wrap-safe freshness rule the client applies to `seq`.\n */\nconst UNRELIABLE_HEADER_SIZE = 2;\n\n/**\n * Does any Schema class reachable from the state root declare an `@unreliable`\n * field? Walked once per room, in {@link SchemaSerializer.reset} \u2014 the result\n * gates whether the unreliable channel is wired up at all, so rooms that don't\n * use it never reach any of the code below.\n */\nfunction stateHasUnreliableFields(context: TypeContext): boolean {\n for (const klass of context.schemas.keys()) {\n const metadata = klass[Symbol.metadata];\n if (metadata === undefined) { continue; }\n\n // `$numFields` is the last index, inclusive (matches `Metadata.getFields`).\n for (let i = 0; i <= metadata[$numFields]; i++) {\n if (Metadata.hasUnreliableAtIndex(metadata, i)) { return true; }\n }\n }\n return false;\n}\n\n/** Can this client receive the unreliable channel right now? Its transport\n * exposing {@link Client.rawUnreliable} IS the capability check. */\nfunction canReceiveUnreliable(client: Client): boolean {\n return client.state === ClientState.JOINED && client.rawUnreliable !== undefined;\n}\n\nconst SHARED_VIEW = {};\n\nexport class SchemaSerializer<T extends Schema> implements Serializer<T> {\n public id = 'schema';\n\n protected encoder: Encoder<T>;\n protected hasFilters: boolean = false;\n\n protected handshakeCache: Uint8Array;\n\n // flag to avoid re-encoding full state if no changes were made\n protected needFullEncode: boolean = true;\n\n protected fullEncodeCache: Uint8Array;\n\n protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** @see {@link Serializer.hasUnreliableFields} */\n public hasUnreliableFields: boolean = false;\n\n /** Room-wide counter stamped into every unreliable frame so the client can\n * drop reordered datagrams. Wraps at 65536 \u2014 see {@link ProtocolModifier.UNRELIABLE}. */\n protected unreliableSeq: number = 0;\n\n protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */\n protected warnedNoUnreliableTransport = false;\n\n public reset(newState: T & Schema) {\n this.encoder = new Encoder(newState);\n this.hasFilters = this.encoder.context.hasFilters;\n this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);\n\n this.needFullEncode = true; // the cached full state belongs to the previous encoder\n\n if (this.hasFilters) {\n this.encodedViews = new Map();\n\n if (this.hasUnreliableFields) {\n // Separate from `encodedViews`: the two channels encode on independent\n // cadences, so a reliable tick's cache must never be served to one.\n this.encodedUnreliableViews = new Map();\n }\n }\n }\n\n public getFullState(client?: Client, timing?: PatchTimingContext) {\n if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE;\n // copy: the next encode reuses the shared buffer (slice(): a pooled Buffer would pin 8KB)\n this.fullEncodeCache = this.encoder.encodeAll({ offset: 1 }).slice();\n this.needFullEncode = false;\n }\n\n let body = this.fullEncodeCache;\n if (this.hasFilters && client?.view) {\n this.encoder.sharedBuffer.set(body); // patches since the full encode may have overwritten it\n body = this.encoder.encodeAllView(client.view, body.byteLength, { offset: body.byteLength });\n }\n\n if (timing && client) {\n return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));\n }\n return body;\n }\n\n public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {\n let numClients = clients.length;\n\n if (numClients === 0) {\n if (this.encoder.hasChanges) {\n // if there are changes but no clients, we need to encode full state on next patch\n this.needFullEncode = true;\n }\n // skip patching and clear changes\n this.encoder.discardChanges();\n return false;\n }\n\n if (!this.encoder.hasChanges) {\n\n // check if views have changes (manual add() or remove() items)\n if (this.hasFilters) {\n //\n // FIXME: refactor this to avoid duplicating code.\n //\n // it's probably better to have 2 different 'applyPatches' methods.\n // (one for handling state with filters, and another for handling state without filters)\n //\n const clientsWithViewChange = clients.filter((client) => {\n return client.state === ClientState.JOINED && client.view?.changes.size > 0\n });\n\n if (clientsWithViewChange.length > 0) {\n const it: Iterator = { offset: 1 };\n\n const sharedOffset = it.offset;\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n clientsWithViewChange.forEach((client) => {\n const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);\n sendClientFrame(client, encodedView, timing);\n });\n }\n }\n\n // Heartbeat: `defineInput()` rooms still need periodic timing samples\n // to keep client clock offset / RTT estimates fresh during quiet\n // ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema\n // body \u2014 1 byte protocol + 8 bytes prefix.\n // No-op for rooms without `defineInput()` (timing is undefined).\n if (timing) {\n for (let i = 0; i < clients.length; i++) {\n const client = clients[i];\n if (client.state !== ClientState.JOINED) continue;\n sendClientFrame(client, HEARTBEAT_BODY, timing);\n }\n }\n\n // skip patching state if:\n // - no clients are connected\n // - no changes were made\n // - no \"filtered changes\" were made when using filters\n return false;\n }\n\n this.needFullEncode = true;\n\n // dump changes for patch debugging\n if (debugPatch.enabled) {\n (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);\n }\n\n // get patch bytes\n const it: Iterator = { offset: 1 };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encode(it);\n\n if (!this.hasFilters) {\n const frame = ownedPatch(encodedChanges, timing);\n\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n sendClientFrame(client, frame, timing);\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n // encode state multiple times, for each client\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? ownedPatch(encodedChanges, timing) // encodeView already returns a copy\n : this.encoder.encodeView(client.view, sharedOffset, it);\n this.encodedViews.set(view, encodedView);\n }\n\n sendClientFrame(client, encodedView, timing);\n }\n\n // clear views\n this.encodedViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardChanges();\n\n // debug patches\n if (debugPatch.enabled) {\n debugPatch(\n '%d bytes sent to %d clients, %j',\n encodedChanges.length,\n clients.length,\n (debugPatch as any).dumpChanges,\n );\n }\n\n return true;\n }\n\n /**\n * Encode and send the UNRELIABLE channel \u2014 the `@unreliable` fields, which\n * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}\n * (a WebTransport datagram); clients whose transport has no unreliable\n * channel are skipped entirely.\n *\n * Runs on its own cadence, independent of `applyPatches`. Only ever called on\n * rooms where {@link hasUnreliableFields} is true.\n */\n public applyUnreliablePatches(clients: Client[]): boolean {\n if (!this.encoder.hasUnreliableChanges) { return false; }\n\n // These mutations are visible to `encodeAll`, so the cached full state is\n // stale \u2014 even though `root.changes` (the reliable queue) is empty and\n // `getFullState`'s own guard would therefore miss it.\n this.needFullEncode = true;\n\n if (!clients.some(canReceiveUnreliable)) {\n this.warnNoUnreliableTransport(clients);\n // Drain regardless: the queue is not self-clearing, and an un-discarded\n // tick would grow it without bound.\n this.encoder.discardUnreliableChanges();\n return false;\n }\n\n this.unreliableSeq = (this.unreliableSeq + 1) & 0xFFFF;\n\n // Stamp the header before encoding, as the reliable path does. Safe across\n // a buffer overflow: `Encoder._resizeBuffer` copies the old bytes forward.\n const it: Iterator = { offset: 1 + UNRELIABLE_HEADER_SIZE };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH | ProtocolModifier.UNRELIABLE;\n encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encodeUnreliable(it);\n\n let numClients = clients.length;\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n if (canReceiveUnreliable(client)) { client.rawUnreliable!(encodedChanges); }\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n while (numClients--) {\n const client = clients[numClients];\n if (!canReceiveUnreliable(client)) { continue; }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedUnreliableViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);\n this.encodedUnreliableViews.set(view, encodedView);\n }\n\n client.rawUnreliable!(encodedView);\n }\n\n // clear views\n this.encodedUnreliableViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardUnreliableChanges();\n\n return true;\n }\n\n /**\n * The state declares `@unreliable` fields but not one connected client can\n * receive them \u2014 every WebSocket transport lacks a datagram channel. Those\n * fields keep their join-time value and never update, which is silent\n * otherwise. Warns once per room.\n */\n private warnNoUnreliableTransport(clients: Client[]) {\n if (this.warnedNoUnreliableTransport || clients.length === 0) { return; }\n this.warnedNoUnreliableTransport = true;\n logger.warn(\n \"@colyseus/core: state has @unreliable fields, but no connected client's transport\" +\n \" provides an unreliable channel \u2014 those fields will not be patched.\" +\n \" Use @colyseus/h3-transport (WebTransport) to enable them.\"\n );\n }\n\n public handshake() {\n /**\n * Cache handshake to avoid encoding it for each client joining\n */\n if (!this.handshakeCache) {\n //\n // TODO: re-use handshake buffer for all rooms of same type (?)\n //\n this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));\n }\n\n return this.handshakeCache;\n }\n\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAA2C;AAG3C,uBAA6D;AAE7D,oBAA8H;AAC9H,mBAA2B;AAC3B,oBAAuB;AAyBvB,IAAM,oBAAoB;AAc1B,SAAS,gBACP,aACA,MACA,UACQ;AACR,QAAM,gBAAgB,YAAY,SAAS,CAAC;AAC5C,QAAM,MAAM,OAAO,YAAY,IAAI,oBAAoB,cAAc,MAAM;AAC3E,MAAI,CAAC,IAAI,YAAY,CAAC,IAAI,qCAAiB;AAC3C,QAAM,KAAe,EAAE,QAAQ,EAAE;AACjC,uBAAO,OAAO,KAAK,SAAS,GAAG,EAAE;AACjC,uBAAO,OAAO,KAAK,aAAa,GAAG,EAAE;AACrC,MAAI,IAAI,eAAe,GAAG,MAAM;AAChC,SAAO;AACT;AAMA,SAAS,wBAAwB,QAAwB;AACvD,SAAQ,OAAkC,cAAc,UAAU;AACpE;AAQA,SAAS,gBAAgB,QAAgB,aAAyB,QAAmC;AACnG,MAAI,QAAQ;AACV,WAAO,IAAI,gBAAgB,aAAa,OAAO,MAAM,wBAAwB,MAAM,CAAC,CAAC;AAAA,EACvF,OAAO;AACL,WAAO,IAAI,WAAW;AAAA,EACxB;AACF;AAOA,SAAS,WAAW,SAAqB,QAAyC;AAChF,SAAO,SAAS,UAAU,OAAO,KAAK,OAAO;AAC/C;AAGA,IAAM,iBAAiB,IAAI,WAAW,CAAC,6BAAS,gBAAgB,CAAC;AAOjE,IAAM,yBAAyB;AAQ/B,SAAS,yBAAyB,SAA+B;AAC/D,aAAW,SAAS,QAAQ,QAAQ,KAAK,GAAG;AAC1C,UAAM,WAAW,MAAM,OAAO,QAAQ;AACtC,QAAI,aAAa,QAAW;AAAE;AAAA,IAAU;AAGxC,aAAS,IAAI,GAAG,KAAK,SAAS,wBAAU,GAAG,KAAK;AAC9C,UAAI,uBAAS,qBAAqB,UAAU,CAAC,GAAG;AAAE,eAAO;AAAA,MAAM;AAAA,IACjE;AAAA,EACF;AACA,SAAO;AACT;AAIA,SAAS,qBAAqB,QAAyB;AACrD,SAAO,OAAO,UAAU,6BAAY,UAAU,OAAO,kBAAkB;AACzE;AAEA,IAAM,cAAc,CAAC;AAEd,IAAM,mBAAN,MAAkE;AAAA,EAAlE;AACL,SAAO,KAAK;AAGZ,SAAU,aAAsB;AAKhC;AAAA,SAAU,iBAA0B;AAOpC;AAAA,SAAO,sBAA+B;AAItC;AAAA;AAAA,SAAU,gBAAwB;AAKlC;AAAA,SAAU,8BAA8B;AAAA;AAAA,EAEjC,MAAM,UAAsB;AACjC,SAAK,UAAU,IAAI,sBAAQ,QAAQ;AACnC,SAAK,aAAa,KAAK,QAAQ,QAAQ;AACvC,SAAK,sBAAsB,yBAAyB,KAAK,QAAQ,OAAO;AAExE,SAAK,iBAAiB;AAEtB,QAAI,KAAK,YAAY;AACnB,WAAK,eAAe,oBAAI,IAAI;AAE5B,UAAI,KAAK,qBAAqB;AAG5B,aAAK,yBAAyB,oBAAI,IAAI;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAAA,EAEO,aAAa,QAAiB,QAA6B;AAChE,QAAI,KAAK,kBAAkB,KAAK,QAAQ,KAAK,QAAQ,SAAS,QAAW;AACvE,WAAK,QAAQ,aAAa,CAAC,IAAI,6BAAS;AAExC,WAAK,kBAAkB,KAAK,QAAQ,UAAU,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM;AACnE,WAAK,iBAAiB;AAAA,IACxB;AAEA,QAAI,OAAO,KAAK;AAChB,QAAI,KAAK,cAAc,QAAQ,MAAM;AACnC,WAAK,QAAQ,aAAa,IAAI,IAAI;AAClC,aAAO,KAAK,QAAQ,cAAc,OAAO,MAAM,KAAK,YAAY,EAAE,QAAQ,KAAK,WAAW,CAAC;AAAA,IAC7F;AAEA,QAAI,UAAU,QAAQ;AACpB,aAAO,gBAAgB,MAAM,OAAO,MAAM,wBAAwB,MAAM,CAAC;AAAA,IAC3E;AACA,WAAO;AAAA,EACT;AAAA,EAEO,aAAa,SAAmB,QAAkB,QAA6B;AACpF,QAAI,aAAa,QAAQ;AAEzB,QAAI,eAAe,GAAG;AACpB,UAAI,KAAK,QAAQ,YAAY;AAE3B,aAAK,iBAAiB;AAAA,MACxB;AAEA,WAAK,QAAQ,eAAe;AAC5B,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,QAAQ,YAAY;AAG5B,UAAI,KAAK,YAAY;AAOnB,cAAM,wBAAwB,QAAQ,OAAO,CAAC,WAAW;AACvD,iBAAO,OAAO,UAAU,6BAAY,UAAU,OAAO,MAAM,QAAQ,OAAO;AAAA,QAC5E,CAAC;AAED,YAAI,sBAAsB,SAAS,GAAG;AACpC,gBAAMA,MAAe,EAAE,QAAQ,EAAE;AAEjC,gBAAM,eAAeA,IAAG;AACxB,eAAK,QAAQ,aAAa,CAAC,IAAI,6BAAS;AAExC,gCAAsB,QAAQ,CAAC,WAAW;AACxC,kBAAM,cAAc,KAAK,QAAQ,WAAW,OAAO,MAAM,cAAcA,GAAE;AACzE,4BAAgB,QAAQ,aAAa,MAAM;AAAA,UAC7C,CAAC;AAAA,QACH;AAAA,MACF;AAOA,UAAI,QAAQ;AACV,iBAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,gBAAM,SAAS,QAAQ,CAAC;AACxB,cAAI,OAAO,UAAU,6BAAY,OAAQ;AACzC,0BAAgB,QAAQ,gBAAgB,MAAM;AAAA,QAChD;AAAA,MACF;AAMA,aAAO;AAAA,IACT;AAEA,SAAK,iBAAiB;AAGtB,QAAI,wBAAW,SAAS;AACtB,MAAC,wBAAmB,kBAAc,2BAAY,KAAK,QAAQ,KAAK;AAAA,IAClE;AAGA,UAAM,KAAe,EAAE,QAAQ,EAAE;AACjC,SAAK,QAAQ,aAAa,CAAC,IAAI,6BAAS;AAGxC,UAAM,iBAAiB,KAAK,QAAQ,OAAO,EAAE;AAE7C,QAAI,CAAC,KAAK,YAAY;AACpB,YAAM,QAAQ,WAAW,gBAAgB,MAAM;AAE/C,aAAO,cAAc;AACnB,cAAM,SAAS,QAAQ,UAAU;AAKjC,YAAI,OAAO,UAAU,6BAAY,QAAQ;AACvC;AAAA,QACF;AAEA,wBAAgB,QAAQ,OAAO,MAAM;AAAA,MACvC;AAAA,IAEF,OAAO;AAEL,YAAM,eAAe,GAAG;AAGxB,aAAO,cAAc;AACnB,cAAM,SAAS,QAAQ,UAAU;AAKjC,YAAI,OAAO,UAAU,6BAAY,QAAQ;AACvC;AAAA,QACF;AAEA,cAAM,OAAO,OAAO,QAAQ;AAE5B,YAAI,cAAc,KAAK,aAAa,IAAI,IAAI;AAG5C,YAAI,gBAAgB,QAAW;AAC7B,wBAAe,SAAS,cACpB,WAAW,gBAAgB,MAAM,IACjC,KAAK,QAAQ,WAAW,OAAO,MAAM,cAAc,EAAE;AACzD,eAAK,aAAa,IAAI,MAAM,WAAW;AAAA,QACzC;AAEA,wBAAgB,QAAQ,aAAa,MAAM;AAAA,MAC7C;AAGA,WAAK,aAAa,MAAM;AAAA,IAC1B;AAGA,SAAK,QAAQ,eAAe;AAG5B,QAAI,wBAAW,SAAS;AACtB;AAAA,QACE;AAAA,QACA,eAAe;AAAA,QACf,QAAQ;AAAA,QACP,wBAAmB;AAAA,MACtB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWO,uBAAuB,SAA4B;AACxD,QAAI,CAAC,KAAK,QAAQ,sBAAsB;AAAE,aAAO;AAAA,IAAO;AAKxD,SAAK,iBAAiB;AAEtB,QAAI,CAAC,QAAQ,KAAK,oBAAoB,GAAG;AACvC,WAAK,0BAA0B,OAAO;AAGtC,WAAK,QAAQ,yBAAyB;AACtC,aAAO;AAAA,IACT;AAEA,SAAK,gBAAiB,KAAK,gBAAgB,IAAK;AAIhD,UAAM,KAAe,EAAE,QAAQ,IAAI,uBAAuB;AAC1D,SAAK,QAAQ,aAAa,CAAC,IAAI,6BAAS,mBAAmB,qCAAiB;AAC5E,yBAAO,OAAO,KAAK,QAAQ,cAAc,KAAK,eAAe,EAAE,QAAQ,EAAE,CAAC;AAG1E,UAAM,iBAAiB,KAAK,QAAQ,iBAAiB,EAAE;AAEvD,QAAI,aAAa,QAAQ;AAEzB,QAAI,CAAC,KAAK,YAAY;AACpB,aAAO,cAAc;AACnB,cAAM,SAAS,QAAQ,UAAU;AACjC,YAAI,qBAAqB,MAAM,GAAG;AAAE,iBAAO,cAAe,cAAc;AAAA,QAAG;AAAA,MAC7E;AAAA,IAEF,OAAO;AAEL,YAAM,eAAe,GAAG;AAExB,aAAO,cAAc;AACnB,cAAM,SAAS,QAAQ,UAAU;AACjC,YAAI,CAAC,qBAAqB,MAAM,GAAG;AAAE;AAAA,QAAU;AAE/C,cAAM,OAAO,OAAO,QAAQ;AAE5B,YAAI,cAAc,KAAK,uBAAuB,IAAI,IAAI;AAGtD,YAAI,gBAAgB,QAAW;AAC7B,wBAAe,SAAS,cACpB,iBACA,KAAK,QAAQ,qBAAqB,OAAO,MAAM,cAAc,EAAE;AACnE,eAAK,uBAAuB,IAAI,MAAM,WAAW;AAAA,QACnD;AAEA,eAAO,cAAe,WAAW;AAAA,MACnC;AAGA,WAAK,uBAAuB,MAAM;AAAA,IACpC;AAGA,SAAK,QAAQ,yBAAyB;AAEtC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,0BAA0B,SAAmB;AACnD,QAAI,KAAK,+BAA+B,QAAQ,WAAW,GAAG;AAAE;AAAA,IAAQ;AACxE,SAAK,8BAA8B;AACnC,yBAAO;AAAA,MACL;AAAA,IAGF;AAAA,EACF;AAAA,EAEO,YAAY;AAIjB,QAAI,CAAC,KAAK,gBAAgB;AAIxB,WAAK,iBAAkB,KAAK,QAAQ,SAAS,yBAAW,OAAO,KAAK,OAAO;AAAA,IAC7E;AAEA,WAAO,KAAK;AAAA,EACd;AAEF;",
6
6
  "names": ["it"]
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import type { PatchTimingContext, Serializer } from './Serializer.ts';
2
2
  import { type Client } from '../Transport.ts';
3
- import { type Iterator, Encoder, Schema, StateView } from '@colyseus/schema';
3
+ import { Encoder, Schema, StateView } from '@colyseus/schema';
4
4
  declare const SHARED_VIEW: {};
5
5
  export declare class SchemaSerializer<T extends Schema> implements Serializer<T> {
6
6
  id: string;
@@ -8,9 +8,7 @@ export declare class SchemaSerializer<T extends Schema> implements Serializer<T>
8
8
  protected hasFilters: boolean;
9
9
  protected handshakeCache: Uint8Array;
10
10
  protected needFullEncode: boolean;
11
- protected fullEncodeBuffer: Uint8Array;
12
11
  protected fullEncodeCache: Uint8Array;
13
- protected sharedOffsetCache: Iterator;
14
12
  protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
15
13
  /** @see {@link Serializer.hasUnreliableFields} */
16
14
  hasUnreliableFields: boolean;
@@ -25,6 +25,10 @@ function sendClientFrame(client, encodedBody, timing) {
25
25
  client.raw(encodedBody);
26
26
  }
27
27
  }
28
+ function ownedPatch(encoded, timing) {
29
+ return timing ? encoded : Buffer.from(encoded);
30
+ }
31
+ var HEARTBEAT_BODY = new Uint8Array([Protocol.ROOM_STATE_PATCH]);
28
32
  var UNRELIABLE_HEADER_SIZE = 2;
29
33
  function stateHasUnreliableFields(context) {
30
34
  for (const klass of context.schemas.keys()) {
@@ -50,9 +54,6 @@ var SchemaSerializer = class {
50
54
  this.hasFilters = false;
51
55
  // flag to avoid re-encoding full state if no changes were made
52
56
  this.needFullEncode = true;
53
- // TODO: make this optional. allocating a new buffer for each room may not be always necessary.
54
- this.fullEncodeBuffer = new Uint8Array(Encoder.BUFFER_SIZE);
55
- this.sharedOffsetCache = { offset: 0 };
56
57
  /** @see {@link Serializer.hasUnreliableFields} */
57
58
  this.hasUnreliableFields = false;
58
59
  /** Room-wide counter stamped into every unreliable frame so the client can
@@ -65,7 +66,7 @@ var SchemaSerializer = class {
65
66
  this.encoder = new Encoder(newState);
66
67
  this.hasFilters = this.encoder.context.hasFilters;
67
68
  this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);
68
- this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;
69
+ this.needFullEncode = true;
69
70
  if (this.hasFilters) {
70
71
  this.encodedViews = /* @__PURE__ */ new Map();
71
72
  if (this.hasUnreliableFields) {
@@ -75,16 +76,15 @@ var SchemaSerializer = class {
75
76
  }
76
77
  getFullState(client, timing) {
77
78
  if (this.needFullEncode || this.encoder.root.changes.next !== void 0) {
78
- this.sharedOffsetCache = { offset: 1 };
79
- this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);
79
+ this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE;
80
+ this.fullEncodeCache = this.encoder.encodeAll({ offset: 1 }).slice();
80
81
  this.needFullEncode = false;
81
82
  }
82
- const body = this.hasFilters && client?.view ? this.encoder.encodeAllView(
83
- client.view,
84
- this.sharedOffsetCache.offset,
85
- { ...this.sharedOffsetCache },
86
- this.fullEncodeBuffer
87
- ) : this.fullEncodeCache;
83
+ let body = this.fullEncodeCache;
84
+ if (this.hasFilters && client?.view) {
85
+ this.encoder.sharedBuffer.set(body);
86
+ body = this.encoder.encodeAllView(client.view, body.byteLength, { offset: body.byteLength });
87
+ }
88
88
  if (timing && client) {
89
89
  return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));
90
90
  }
@@ -115,11 +115,10 @@ var SchemaSerializer = class {
115
115
  }
116
116
  }
117
117
  if (timing) {
118
- const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);
119
118
  for (let i = 0; i < clients.length; i++) {
120
119
  const client = clients[i];
121
120
  if (client.state !== ClientState.JOINED) continue;
122
- sendClientFrame(client, heartbeatBody, timing);
121
+ sendClientFrame(client, HEARTBEAT_BODY, timing);
123
122
  }
124
123
  }
125
124
  return false;
@@ -132,12 +131,13 @@ var SchemaSerializer = class {
132
131
  this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;
133
132
  const encodedChanges = this.encoder.encode(it);
134
133
  if (!this.hasFilters) {
134
+ const frame = ownedPatch(encodedChanges, timing);
135
135
  while (numClients--) {
136
136
  const client = clients[numClients];
137
137
  if (client.state !== ClientState.JOINED) {
138
138
  continue;
139
139
  }
140
- sendClientFrame(client, encodedChanges, timing);
140
+ sendClientFrame(client, frame, timing);
141
141
  }
142
142
  } else {
143
143
  const sharedOffset = it.offset;
@@ -149,7 +149,7 @@ var SchemaSerializer = class {
149
149
  const view = client.view || SHARED_VIEW;
150
150
  let encodedView = this.encodedViews.get(view);
151
151
  if (encodedView === void 0) {
152
- encodedView = view === SHARED_VIEW ? encodedChanges : this.encoder.encodeView(client.view, sharedOffset, it);
152
+ encodedView = view === SHARED_VIEW ? ownedPatch(encodedChanges, timing) : this.encoder.encodeView(client.view, sharedOffset, it);
153
153
  this.encodedViews.set(view, encodedView);
154
154
  }
155
155
  sendClientFrame(client, encodedView, timing);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/serializer/SchemaSerializer.ts"],
4
- "sourcesContent": ["import { Protocol, ProtocolModifier } from '@colyseus/shared-types';\n\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\nimport { type Client, type ClientPrivate, ClientState } from '../Transport.ts';\n\nimport { type Iterator, $numFields, encode, Encoder, dumpChanges, Metadata, Reflection, Schema, StateView, TypeContext } from '@colyseus/schema';\nimport { debugPatch } from '../Debug.ts';\nimport { logger } from '../Logger.ts';\n\n/**\n * Size of the {@link ProtocolModifier.TIMED} prefix that follows the\n * protocol byte:\n * `[uint32 sNow][uint32 inputSeq]`\n *\n * - `sNow` \u2014 server clock as **milliseconds since room start**\n * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,\n * language-agnostic integer (C/C#/Lua have no `performance.now()`); the\n * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the\n * client's reconstructed `serverNow()` stays in phase. Wraps at u32\n * (~49.7 days uptime) \u2014 irrelevant per session. Drives the client clock\n * offset / `serverNow()`.\n * - `inputSeq` \u2014 seq VALUE of the last input CONSUMED into the authoritative\n * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:\n * equals the consumed count (inputs are sequenced implicitly by receive\n * order). Unreliable: the framework wire seq, so a fully-dropped input\n * doesn't make the ack lag the client's sent seq by the lost count. This\n * single number is the canonical reconciliation ack (\"the sequence number\n * of the last input the server processed\"); the client prunes its\n * pending-input buffer against it AND derives RTT from its round-trip\n * (`now \u2212 sendTime(inputSeq)`). No separate received-seq / received-time \u2014\n * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.\n */\nconst TIMED_PREFIX_SIZE = 8;\n\n/**\n * Construct a per-recipient TIMED buffer:\n * `[code|TIMED][sNow][inputSeq][...schema bytes]`\n *\n * `encodedBody` is the shared encoded buffer whose byte 0 already holds the\n * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the\n * schema payload. We construct a fresh per-client Buffer so the transport\n * never sees a buffer that might mutate between queue and send.\n *\n * Uses the schema `encode.*` helpers to keep the wire layout symmetric with\n * the SDK's `decode.*`-driven reader.\n */\nfunction buildTimedFrame(\n encodedBody: Uint8Array,\n sNow: number,\n inputSeq: number,\n): Buffer {\n const schemaPayload = encodedBody.subarray(1);\n const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);\n out[0] = encodedBody[0] | ProtocolModifier.TIMED;\n const it: Iterator = { offset: 1 };\n encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)\n encode.uint32(out, inputSeq >>> 0, it);\n out.set(schemaPayload, it.offset);\n return out;\n}\n\n/** Seq VALUE of the last input consumed from the client's buffer \u2014 the\n * reconciliation ack (and RTT correlation key). Reliable: equals the consumed\n * count; unreliable: the framework wire seq (so packet loss doesn't skew it).\n * 0 when the room doesn't buffer input. */\nfunction clientProcessedInputSeq(client: Client): number {\n return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;\n}\n\n/**\n * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,\n * else the shared `encodedBody` passed straight through (no per-client copy). Any\n * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here\n * \u2014 they go out standalone right after the patch via `Room._flushPendingClientFrames`.\n */\nfunction sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {\n if (timing) {\n client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));\n } else {\n client.raw(encodedBody);\n }\n}\n\n/**\n * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the\n * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for\n * the wrap-safe freshness rule the client applies to `seq`.\n */\nconst UNRELIABLE_HEADER_SIZE = 2;\n\n/**\n * Does any Schema class reachable from the state root declare an `@unreliable`\n * field? Walked once per room, in {@link SchemaSerializer.reset} \u2014 the result\n * gates whether the unreliable channel is wired up at all, so rooms that don't\n * use it never reach any of the code below.\n */\nfunction stateHasUnreliableFields(context: TypeContext): boolean {\n for (const klass of context.schemas.keys()) {\n const metadata = klass[Symbol.metadata];\n if (metadata === undefined) { continue; }\n\n // `$numFields` is the last index, inclusive (matches `Metadata.getFields`).\n for (let i = 0; i <= metadata[$numFields]; i++) {\n if (Metadata.hasUnreliableAtIndex(metadata, i)) { return true; }\n }\n }\n return false;\n}\n\n/** Can this client receive the unreliable channel right now? Its transport\n * exposing {@link Client.rawUnreliable} IS the capability check. */\nfunction canReceiveUnreliable(client: Client): boolean {\n return client.state === ClientState.JOINED && client.rawUnreliable !== undefined;\n}\n\nconst SHARED_VIEW = {};\n\nexport class SchemaSerializer<T extends Schema> implements Serializer<T> {\n public id = 'schema';\n\n protected encoder: Encoder<T>;\n protected hasFilters: boolean = false;\n\n protected handshakeCache: Uint8Array;\n\n // flag to avoid re-encoding full state if no changes were made\n protected needFullEncode: boolean = true;\n\n // TODO: make this optional. allocating a new buffer for each room may not be always necessary.\n protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);\n protected fullEncodeCache: Uint8Array;\n protected sharedOffsetCache: Iterator = { offset: 0 };\n\n protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** @see {@link Serializer.hasUnreliableFields} */\n public hasUnreliableFields: boolean = false;\n\n /** Room-wide counter stamped into every unreliable frame so the client can\n * drop reordered datagrams. Wraps at 65536 \u2014 see {@link ProtocolModifier.UNRELIABLE}. */\n protected unreliableSeq: number = 0;\n\n protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */\n protected warnedNoUnreliableTransport = false;\n\n public reset(newState: T & Schema) {\n this.encoder = new Encoder(newState);\n this.hasFilters = this.encoder.context.hasFilters;\n this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);\n\n // cache ROOM_STATE byte as part of the encoded buffer\n this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;\n\n if (this.hasFilters) {\n this.encodedViews = new Map();\n\n if (this.hasUnreliableFields) {\n // Separate from `encodedViews`: the two channels encode on independent\n // cadences, so a reliable tick's cache must never be served to one.\n this.encodedUnreliableViews = new Map();\n }\n }\n }\n\n public getFullState(client?: Client, timing?: PatchTimingContext) {\n if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {\n this.sharedOffsetCache = { offset: 1 };\n this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);\n this.needFullEncode = false;\n }\n\n const body = (this.hasFilters && client?.view)\n ? this.encoder.encodeAllView(\n client.view,\n this.sharedOffsetCache.offset,\n { ...this.sharedOffsetCache },\n this.fullEncodeBuffer\n )\n : this.fullEncodeCache;\n\n if (timing && client) {\n return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));\n }\n return body;\n }\n\n public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {\n let numClients = clients.length;\n\n if (numClients === 0) {\n if (this.encoder.hasChanges) {\n // if there are changes but no clients, we need to encode full state on next patch\n this.needFullEncode = true;\n }\n // skip patching and clear changes\n this.encoder.discardChanges();\n return false;\n }\n\n if (!this.encoder.hasChanges) {\n\n // check if views have changes (manual add() or remove() items)\n if (this.hasFilters) {\n //\n // FIXME: refactor this to avoid duplicating code.\n //\n // it's probably better to have 2 different 'applyPatches' methods.\n // (one for handling state with filters, and another for handling state without filters)\n //\n const clientsWithViewChange = clients.filter((client) => {\n return client.state === ClientState.JOINED && client.view?.changes.size > 0\n });\n\n if (clientsWithViewChange.length > 0) {\n const it: Iterator = { offset: 1 };\n\n const sharedOffset = it.offset;\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n clientsWithViewChange.forEach((client) => {\n const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);\n sendClientFrame(client, encodedView, timing);\n });\n }\n }\n\n // Heartbeat: `defineInput()` rooms still need periodic timing samples\n // to keep client clock offset / RTT estimates fresh during quiet\n // ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema\n // body \u2014 1 byte protocol + 8 bytes prefix.\n // No-op for rooms without `defineInput()` (timing is undefined).\n if (timing) {\n const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);\n for (let i = 0; i < clients.length; i++) {\n const client = clients[i];\n if (client.state !== ClientState.JOINED) continue;\n sendClientFrame(client, heartbeatBody, timing);\n }\n }\n\n // skip patching state if:\n // - no clients are connected\n // - no changes were made\n // - no \"filtered changes\" were made when using filters\n return false;\n }\n\n this.needFullEncode = true;\n\n // dump changes for patch debugging\n if (debugPatch.enabled) {\n (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);\n }\n\n // get patch bytes\n const it: Iterator = { offset: 1 };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encode(it);\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n sendClientFrame(client, encodedChanges, timing);\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n // encode state multiple times, for each client\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeView(client.view, sharedOffset, it);\n this.encodedViews.set(view, encodedView);\n }\n\n sendClientFrame(client, encodedView, timing);\n }\n\n // clear views\n this.encodedViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardChanges();\n\n // debug patches\n if (debugPatch.enabled) {\n debugPatch(\n '%d bytes sent to %d clients, %j',\n encodedChanges.length,\n clients.length,\n (debugPatch as any).dumpChanges,\n );\n }\n\n return true;\n }\n\n /**\n * Encode and send the UNRELIABLE channel \u2014 the `@unreliable` fields, which\n * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}\n * (a WebTransport datagram); clients whose transport has no unreliable\n * channel are skipped entirely.\n *\n * Runs on its own cadence, independent of `applyPatches`. Only ever called on\n * rooms where {@link hasUnreliableFields} is true.\n */\n public applyUnreliablePatches(clients: Client[]): boolean {\n if (!this.encoder.hasUnreliableChanges) { return false; }\n\n // These mutations are visible to `encodeAll`, so the cached full state is\n // stale \u2014 even though `root.changes` (the reliable queue) is empty and\n // `getFullState`'s own guard would therefore miss it.\n this.needFullEncode = true;\n\n if (!clients.some(canReceiveUnreliable)) {\n this.warnNoUnreliableTransport(clients);\n // Drain regardless: the queue is not self-clearing, and an un-discarded\n // tick would grow it without bound.\n this.encoder.discardUnreliableChanges();\n return false;\n }\n\n this.unreliableSeq = (this.unreliableSeq + 1) & 0xFFFF;\n\n // Stamp the header before encoding, as the reliable path does. Safe across\n // a buffer overflow: `Encoder._resizeBuffer` copies the old bytes forward.\n const it: Iterator = { offset: 1 + UNRELIABLE_HEADER_SIZE };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH | ProtocolModifier.UNRELIABLE;\n encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encodeUnreliable(it);\n\n let numClients = clients.length;\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n if (canReceiveUnreliable(client)) { client.rawUnreliable!(encodedChanges); }\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n while (numClients--) {\n const client = clients[numClients];\n if (!canReceiveUnreliable(client)) { continue; }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedUnreliableViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);\n this.encodedUnreliableViews.set(view, encodedView);\n }\n\n client.rawUnreliable!(encodedView);\n }\n\n // clear views\n this.encodedUnreliableViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardUnreliableChanges();\n\n return true;\n }\n\n /**\n * The state declares `@unreliable` fields but not one connected client can\n * receive them \u2014 every WebSocket transport lacks a datagram channel. Those\n * fields keep their join-time value and never update, which is silent\n * otherwise. Warns once per room.\n */\n private warnNoUnreliableTransport(clients: Client[]) {\n if (this.warnedNoUnreliableTransport || clients.length === 0) { return; }\n this.warnedNoUnreliableTransport = true;\n logger.warn(\n \"@colyseus/core: state has @unreliable fields, but no connected client's transport\" +\n \" provides an unreliable channel \u2014 those fields will not be patched.\" +\n \" Use @colyseus/h3-transport (WebTransport) to enable them.\"\n );\n }\n\n public handshake() {\n /**\n * Cache handshake to avoid encoding it for each client joining\n */\n if (!this.handshakeCache) {\n //\n // TODO: re-use handshake buffer for all rooms of same type (?)\n //\n this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));\n }\n\n return this.handshakeCache;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Protocol, ProtocolModifier } from '@colyseus/shared-types';\n\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\nimport { type Client, type ClientPrivate, ClientState } from '../Transport.ts';\n\nimport { type Iterator, $numFields, encode, Encoder, dumpChanges, Metadata, Reflection, Schema, StateView, TypeContext } from '@colyseus/schema';\nimport { debugPatch } from '../Debug.ts';\nimport { logger } from '../Logger.ts';\n\n/**\n * Size of the {@link ProtocolModifier.TIMED} prefix that follows the\n * protocol byte:\n * `[uint32 sNow][uint32 inputSeq]`\n *\n * - `sNow` \u2014 server clock as **milliseconds since room start**\n * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,\n * language-agnostic integer (C/C#/Lua have no `performance.now()`); the\n * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the\n * client's reconstructed `serverNow()` stays in phase. Wraps at u32\n * (~49.7 days uptime) \u2014 irrelevant per session. Drives the client clock\n * offset / `serverNow()`.\n * - `inputSeq` \u2014 seq VALUE of the last input CONSUMED into the authoritative\n * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:\n * equals the consumed count (inputs are sequenced implicitly by receive\n * order). Unreliable: the framework wire seq, so a fully-dropped input\n * doesn't make the ack lag the client's sent seq by the lost count. This\n * single number is the canonical reconciliation ack (\"the sequence number\n * of the last input the server processed\"); the client prunes its\n * pending-input buffer against it AND derives RTT from its round-trip\n * (`now \u2212 sendTime(inputSeq)`). No separate received-seq / received-time \u2014\n * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.\n */\nconst TIMED_PREFIX_SIZE = 8;\n\n/**\n * Construct a per-recipient TIMED buffer:\n * `[code|TIMED][sNow][inputSeq][...schema bytes]`\n *\n * `encodedBody` is the shared encoded buffer whose byte 0 already holds the\n * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the\n * schema payload. We construct a fresh per-client Buffer so the transport\n * never sees a buffer that might mutate between queue and send.\n *\n * Uses the schema `encode.*` helpers to keep the wire layout symmetric with\n * the SDK's `decode.*`-driven reader.\n */\nfunction buildTimedFrame(\n encodedBody: Uint8Array,\n sNow: number,\n inputSeq: number,\n): Buffer {\n const schemaPayload = encodedBody.subarray(1);\n const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);\n out[0] = encodedBody[0] | ProtocolModifier.TIMED;\n const it: Iterator = { offset: 1 };\n encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)\n encode.uint32(out, inputSeq >>> 0, it);\n out.set(schemaPayload, it.offset);\n return out;\n}\n\n/** Seq VALUE of the last input consumed from the client's buffer \u2014 the\n * reconciliation ack (and RTT correlation key). Reliable: equals the consumed\n * count; unreliable: the framework wire seq (so packet loss doesn't skew it).\n * 0 when the room doesn't buffer input. */\nfunction clientProcessedInputSeq(client: Client): number {\n return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;\n}\n\n/**\n * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,\n * else the shared `encodedBody` passed straight through (no per-client copy). Any\n * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here\n * \u2014 they go out standalone right after the patch via `Room._flushPendingClientFrames`.\n */\nfunction sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {\n if (timing) {\n client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));\n } else {\n client.raw(encodedBody);\n }\n}\n\n/**\n * Detach an untimed patch from the encoder's shared buffer: the transport may\n * still hold it when the next encode reuses that buffer (see {@link Client.raw}).\n * Timed frames skip this \u2014 {@link buildTimedFrame} already copies per client.\n */\nfunction ownedPatch(encoded: Uint8Array, timing?: PatchTimingContext): Uint8Array {\n return timing ? encoded : Buffer.from(encoded);\n}\n\n/** Empty quiet-tick patch body; sent only timed, so sharing it is safe. */\nconst HEARTBEAT_BODY = new Uint8Array([Protocol.ROOM_STATE_PATCH]);\n\n/**\n * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the\n * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for\n * the wrap-safe freshness rule the client applies to `seq`.\n */\nconst UNRELIABLE_HEADER_SIZE = 2;\n\n/**\n * Does any Schema class reachable from the state root declare an `@unreliable`\n * field? Walked once per room, in {@link SchemaSerializer.reset} \u2014 the result\n * gates whether the unreliable channel is wired up at all, so rooms that don't\n * use it never reach any of the code below.\n */\nfunction stateHasUnreliableFields(context: TypeContext): boolean {\n for (const klass of context.schemas.keys()) {\n const metadata = klass[Symbol.metadata];\n if (metadata === undefined) { continue; }\n\n // `$numFields` is the last index, inclusive (matches `Metadata.getFields`).\n for (let i = 0; i <= metadata[$numFields]; i++) {\n if (Metadata.hasUnreliableAtIndex(metadata, i)) { return true; }\n }\n }\n return false;\n}\n\n/** Can this client receive the unreliable channel right now? Its transport\n * exposing {@link Client.rawUnreliable} IS the capability check. */\nfunction canReceiveUnreliable(client: Client): boolean {\n return client.state === ClientState.JOINED && client.rawUnreliable !== undefined;\n}\n\nconst SHARED_VIEW = {};\n\nexport class SchemaSerializer<T extends Schema> implements Serializer<T> {\n public id = 'schema';\n\n protected encoder: Encoder<T>;\n protected hasFilters: boolean = false;\n\n protected handshakeCache: Uint8Array;\n\n // flag to avoid re-encoding full state if no changes were made\n protected needFullEncode: boolean = true;\n\n protected fullEncodeCache: Uint8Array;\n\n protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** @see {@link Serializer.hasUnreliableFields} */\n public hasUnreliableFields: boolean = false;\n\n /** Room-wide counter stamped into every unreliable frame so the client can\n * drop reordered datagrams. Wraps at 65536 \u2014 see {@link ProtocolModifier.UNRELIABLE}. */\n protected unreliableSeq: number = 0;\n\n protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */\n protected warnedNoUnreliableTransport = false;\n\n public reset(newState: T & Schema) {\n this.encoder = new Encoder(newState);\n this.hasFilters = this.encoder.context.hasFilters;\n this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);\n\n this.needFullEncode = true; // the cached full state belongs to the previous encoder\n\n if (this.hasFilters) {\n this.encodedViews = new Map();\n\n if (this.hasUnreliableFields) {\n // Separate from `encodedViews`: the two channels encode on independent\n // cadences, so a reliable tick's cache must never be served to one.\n this.encodedUnreliableViews = new Map();\n }\n }\n }\n\n public getFullState(client?: Client, timing?: PatchTimingContext) {\n if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE;\n // copy: the next encode reuses the shared buffer (slice(): a pooled Buffer would pin 8KB)\n this.fullEncodeCache = this.encoder.encodeAll({ offset: 1 }).slice();\n this.needFullEncode = false;\n }\n\n let body = this.fullEncodeCache;\n if (this.hasFilters && client?.view) {\n this.encoder.sharedBuffer.set(body); // patches since the full encode may have overwritten it\n body = this.encoder.encodeAllView(client.view, body.byteLength, { offset: body.byteLength });\n }\n\n if (timing && client) {\n return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));\n }\n return body;\n }\n\n public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {\n let numClients = clients.length;\n\n if (numClients === 0) {\n if (this.encoder.hasChanges) {\n // if there are changes but no clients, we need to encode full state on next patch\n this.needFullEncode = true;\n }\n // skip patching and clear changes\n this.encoder.discardChanges();\n return false;\n }\n\n if (!this.encoder.hasChanges) {\n\n // check if views have changes (manual add() or remove() items)\n if (this.hasFilters) {\n //\n // FIXME: refactor this to avoid duplicating code.\n //\n // it's probably better to have 2 different 'applyPatches' methods.\n // (one for handling state with filters, and another for handling state without filters)\n //\n const clientsWithViewChange = clients.filter((client) => {\n return client.state === ClientState.JOINED && client.view?.changes.size > 0\n });\n\n if (clientsWithViewChange.length > 0) {\n const it: Iterator = { offset: 1 };\n\n const sharedOffset = it.offset;\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n clientsWithViewChange.forEach((client) => {\n const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);\n sendClientFrame(client, encodedView, timing);\n });\n }\n }\n\n // Heartbeat: `defineInput()` rooms still need periodic timing samples\n // to keep client clock offset / RTT estimates fresh during quiet\n // ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema\n // body \u2014 1 byte protocol + 8 bytes prefix.\n // No-op for rooms without `defineInput()` (timing is undefined).\n if (timing) {\n for (let i = 0; i < clients.length; i++) {\n const client = clients[i];\n if (client.state !== ClientState.JOINED) continue;\n sendClientFrame(client, HEARTBEAT_BODY, timing);\n }\n }\n\n // skip patching state if:\n // - no clients are connected\n // - no changes were made\n // - no \"filtered changes\" were made when using filters\n return false;\n }\n\n this.needFullEncode = true;\n\n // dump changes for patch debugging\n if (debugPatch.enabled) {\n (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);\n }\n\n // get patch bytes\n const it: Iterator = { offset: 1 };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encode(it);\n\n if (!this.hasFilters) {\n const frame = ownedPatch(encodedChanges, timing);\n\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n sendClientFrame(client, frame, timing);\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n // encode state multiple times, for each client\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? ownedPatch(encodedChanges, timing) // encodeView already returns a copy\n : this.encoder.encodeView(client.view, sharedOffset, it);\n this.encodedViews.set(view, encodedView);\n }\n\n sendClientFrame(client, encodedView, timing);\n }\n\n // clear views\n this.encodedViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardChanges();\n\n // debug patches\n if (debugPatch.enabled) {\n debugPatch(\n '%d bytes sent to %d clients, %j',\n encodedChanges.length,\n clients.length,\n (debugPatch as any).dumpChanges,\n );\n }\n\n return true;\n }\n\n /**\n * Encode and send the UNRELIABLE channel \u2014 the `@unreliable` fields, which\n * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}\n * (a WebTransport datagram); clients whose transport has no unreliable\n * channel are skipped entirely.\n *\n * Runs on its own cadence, independent of `applyPatches`. Only ever called on\n * rooms where {@link hasUnreliableFields} is true.\n */\n public applyUnreliablePatches(clients: Client[]): boolean {\n if (!this.encoder.hasUnreliableChanges) { return false; }\n\n // These mutations are visible to `encodeAll`, so the cached full state is\n // stale \u2014 even though `root.changes` (the reliable queue) is empty and\n // `getFullState`'s own guard would therefore miss it.\n this.needFullEncode = true;\n\n if (!clients.some(canReceiveUnreliable)) {\n this.warnNoUnreliableTransport(clients);\n // Drain regardless: the queue is not self-clearing, and an un-discarded\n // tick would grow it without bound.\n this.encoder.discardUnreliableChanges();\n return false;\n }\n\n this.unreliableSeq = (this.unreliableSeq + 1) & 0xFFFF;\n\n // Stamp the header before encoding, as the reliable path does. Safe across\n // a buffer overflow: `Encoder._resizeBuffer` copies the old bytes forward.\n const it: Iterator = { offset: 1 + UNRELIABLE_HEADER_SIZE };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH | ProtocolModifier.UNRELIABLE;\n encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encodeUnreliable(it);\n\n let numClients = clients.length;\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n if (canReceiveUnreliable(client)) { client.rawUnreliable!(encodedChanges); }\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n while (numClients--) {\n const client = clients[numClients];\n if (!canReceiveUnreliable(client)) { continue; }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedUnreliableViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);\n this.encodedUnreliableViews.set(view, encodedView);\n }\n\n client.rawUnreliable!(encodedView);\n }\n\n // clear views\n this.encodedUnreliableViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardUnreliableChanges();\n\n return true;\n }\n\n /**\n * The state declares `@unreliable` fields but not one connected client can\n * receive them \u2014 every WebSocket transport lacks a datagram channel. Those\n * fields keep their join-time value and never update, which is silent\n * otherwise. Warns once per room.\n */\n private warnNoUnreliableTransport(clients: Client[]) {\n if (this.warnedNoUnreliableTransport || clients.length === 0) { return; }\n this.warnedNoUnreliableTransport = true;\n logger.warn(\n \"@colyseus/core: state has @unreliable fields, but no connected client's transport\" +\n \" provides an unreliable channel \u2014 those fields will not be patched.\" +\n \" Use @colyseus/h3-transport (WebTransport) to enable them.\"\n );\n }\n\n public handshake() {\n /**\n * Cache handshake to avoid encoding it for each client joining\n */\n if (!this.handshakeCache) {\n //\n // TODO: re-use handshake buffer for all rooms of same type (?)\n //\n this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));\n }\n\n return this.handshakeCache;\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["it"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.19",
3
+ "version": "0.18.20",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -53,17 +53,17 @@
53
53
  "debug": "^4.3.4",
54
54
  "msgpackr": "^2.0.1",
55
55
  "nanoid": "^3.3.11",
56
- "@colyseus/greeting-banner": "^4.0.3",
57
56
  "@colyseus/better-call": "^1.3.1",
58
- "@colyseus/shared-types": "^0.18.3"
57
+ "@colyseus/shared-types": "^0.18.3",
58
+ "@colyseus/greeting-banner": "^4.0.3"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@colyseus/schema": "^5.0.8",
62
62
  "express": "^5.0.0",
63
63
  "vitest": "^3.1.1",
64
64
  "@colyseus/redis-driver": "^0.18.5",
65
- "@colyseus/tools": "^0.18.10",
66
- "@colyseus/redis-presence": "^0.18.5"
65
+ "@colyseus/redis-presence": "^0.18.5",
66
+ "@colyseus/tools": "^0.18.10"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@colyseus/schema": "^5.0.8",
@@ -71,8 +71,8 @@
71
71
  "express": "^4.16.0 || ^5.0.0",
72
72
  "zod": "^4.1.12",
73
73
  "@colyseus/auth": "^0.18.4",
74
- "@colyseus/better-call": "^1.3.1",
75
- "@colyseus/ws-transport": "^0.18.4"
74
+ "@colyseus/ws-transport": "^0.18.4",
75
+ "@colyseus/better-call": "^1.3.1"
76
76
  },
77
77
  "peerDependenciesMeta": {
78
78
  "@colyseus/auth": {
package/src/Transport.ts CHANGED
@@ -252,7 +252,18 @@ export interface Client<T extends { userData?: any, auth?: any, messages?: Recor
252
252
  reconnectionToken: string;
253
253
 
254
254
  // TODO: move these to ClientPrivate
255
+ /**
256
+ * Send raw bytes over the transport's reliable channel.
257
+ *
258
+ * The transport owns `data` from this call on: it may write it later (a
259
+ * socket with backpressure, permessage-deflate), so callers never mutate it
260
+ * afterwards. The same `data` may be handed to several clients.
261
+ */
255
262
  raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
263
+ /**
264
+ * {@link raw}, or held until the client finishes joining (or until after the
265
+ * next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.
266
+ */
256
267
  enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;
257
268
 
258
269
  /**
@@ -262,6 +273,10 @@ export interface Client<T extends { userData?: any, auth?: any, messages?: Recor
262
273
  * Absent on transports with no datagram channel (every WebSocket transport).
263
274
  * Its presence IS the capability check — callers feature-detect rather than
264
275
  * reading a separate flag, and skip clients that can't receive.
276
+ *
277
+ * Unlike {@link raw}, `data` is only valid during this call: the caller
278
+ * reuses its bytes afterwards, so a transport that sends it later copies it
279
+ * first.
265
280
  */
266
281
  rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
267
282
 
@@ -81,6 +81,18 @@ function sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: Patch
81
81
  }
82
82
  }
83
83
 
84
+ /**
85
+ * Detach an untimed patch from the encoder's shared buffer: the transport may
86
+ * still hold it when the next encode reuses that buffer (see {@link Client.raw}).
87
+ * Timed frames skip this — {@link buildTimedFrame} already copies per client.
88
+ */
89
+ function ownedPatch(encoded: Uint8Array, timing?: PatchTimingContext): Uint8Array {
90
+ return timing ? encoded : Buffer.from(encoded);
91
+ }
92
+
93
+ /** Empty quiet-tick patch body; sent only timed, so sharing it is safe. */
94
+ const HEARTBEAT_BODY = new Uint8Array([Protocol.ROOM_STATE_PATCH]);
95
+
84
96
  /**
85
97
  * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the
86
98
  * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for
@@ -126,10 +138,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
126
138
  // flag to avoid re-encoding full state if no changes were made
127
139
  protected needFullEncode: boolean = true;
128
140
 
129
- // TODO: make this optional. allocating a new buffer for each room may not be always necessary.
130
- protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);
131
141
  protected fullEncodeCache: Uint8Array;
132
- protected sharedOffsetCache: Iterator = { offset: 0 };
133
142
 
134
143
  protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
135
144
 
@@ -150,8 +159,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
150
159
  this.hasFilters = this.encoder.context.hasFilters;
151
160
  this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);
152
161
 
153
- // cache ROOM_STATE byte as part of the encoded buffer
154
- this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;
162
+ this.needFullEncode = true; // the cached full state belongs to the previous encoder
155
163
 
156
164
  if (this.hasFilters) {
157
165
  this.encodedViews = new Map();
@@ -166,19 +174,17 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
166
174
 
167
175
  public getFullState(client?: Client, timing?: PatchTimingContext) {
168
176
  if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {
169
- this.sharedOffsetCache = { offset: 1 };
170
- this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);
177
+ this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE;
178
+ // copy: the next encode reuses the shared buffer (slice(): a pooled Buffer would pin 8KB)
179
+ this.fullEncodeCache = this.encoder.encodeAll({ offset: 1 }).slice();
171
180
  this.needFullEncode = false;
172
181
  }
173
182
 
174
- const body = (this.hasFilters && client?.view)
175
- ? this.encoder.encodeAllView(
176
- client.view,
177
- this.sharedOffsetCache.offset,
178
- { ...this.sharedOffsetCache },
179
- this.fullEncodeBuffer
180
- )
181
- : this.fullEncodeCache;
183
+ let body = this.fullEncodeCache;
184
+ if (this.hasFilters && client?.view) {
185
+ this.encoder.sharedBuffer.set(body); // patches since the full encode may have overwritten it
186
+ body = this.encoder.encodeAllView(client.view, body.byteLength, { offset: body.byteLength });
187
+ }
182
188
 
183
189
  if (timing && client) {
184
190
  return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));
@@ -232,11 +238,10 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
232
238
  // body — 1 byte protocol + 8 bytes prefix.
233
239
  // No-op for rooms without `defineInput()` (timing is undefined).
234
240
  if (timing) {
235
- const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);
236
241
  for (let i = 0; i < clients.length; i++) {
237
242
  const client = clients[i];
238
243
  if (client.state !== ClientState.JOINED) continue;
239
- sendClientFrame(client, heartbeatBody, timing);
244
+ sendClientFrame(client, HEARTBEAT_BODY, timing);
240
245
  }
241
246
  }
242
247
 
@@ -262,6 +267,8 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
262
267
  const encodedChanges = this.encoder.encode(it);
263
268
 
264
269
  if (!this.hasFilters) {
270
+ const frame = ownedPatch(encodedChanges, timing);
271
+
265
272
  while (numClients--) {
266
273
  const client = clients[numClients];
267
274
 
@@ -272,7 +279,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
272
279
  continue;
273
280
  }
274
281
 
275
- sendClientFrame(client, encodedChanges, timing);
282
+ sendClientFrame(client, frame, timing);
276
283
  }
277
284
 
278
285
  } else {
@@ -297,7 +304,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
297
304
  // allow to pass the same encoded view for multiple clients
298
305
  if (encodedView === undefined) {
299
306
  encodedView = (view === SHARED_VIEW)
300
- ? encodedChanges
307
+ ? ownedPatch(encodedChanges, timing) // encodeView already returns a copy
301
308
  : this.encoder.encodeView(client.view, sharedOffset, it);
302
309
  this.encodedViews.set(view, encodedView);
303
310
  }