@colyseus/core 0.18.19 → 0.18.21

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/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.21",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -54,16 +54,16 @@
54
54
  "msgpackr": "^2.0.1",
55
55
  "nanoid": "^3.3.11",
56
56
  "@colyseus/greeting-banner": "^4.0.3",
57
- "@colyseus/better-call": "^1.3.1",
58
- "@colyseus/shared-types": "^0.18.3"
57
+ "@colyseus/shared-types": "^0.18.3",
58
+ "@colyseus/better-call": "^1.3.1"
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
- "@colyseus/redis-driver": "^0.18.5",
64
+ "@colyseus/redis-presence": "^0.18.5",
65
65
  "@colyseus/tools": "^0.18.10",
66
- "@colyseus/redis-presence": "^0.18.5"
66
+ "@colyseus/redis-driver": "^0.18.5"
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/Room.ts CHANGED
@@ -2007,6 +2007,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
2007
2007
  }
2008
2008
 
2009
2009
  const cleanup = () => {
2010
+ clearTimeout(this._reservedSeatTimeouts[sessionId]);
2010
2011
  delete this._reconnections[reconnectionToken];
2011
2012
  delete this._reservedSeats[sessionId];
2012
2013
  delete this._reservedSeatTimeouts[sessionId];
@@ -2022,7 +2023,6 @@ export class Room<T extends RoomOptions = RoomOptions> {
2022
2023
  previousClient.state = ClientState.RECONNECTED;
2023
2024
  previousClient.ref = newClient.ref;
2024
2025
  previousClient.reconnectionToken = newClient.reconnectionToken;
2025
- clearTimeout(this._reservedSeatTimeouts[sessionId]);
2026
2026
 
2027
2027
  }, () => {
2028
2028
  this.resetAutoDisposeTimeout();
@@ -2258,6 +2258,10 @@ export class Room<T extends RoomOptions = RoomOptions> {
2258
2258
  this._autoDisposeTimeout = undefined;
2259
2259
  }
2260
2260
 
2261
+ for (const sessionId in this._reservedSeatTimeouts) {
2262
+ clearTimeout(this._reservedSeatTimeouts[sessionId]);
2263
+ }
2264
+
2261
2265
  // clear all timeouts/intervals + force to stop ticking
2262
2266
  this.clock.clear();
2263
2267
  this.clock.stop();
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
  }