@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.
- package/build/Room.cjs +4 -1
- package/build/Room.cjs.map +2 -2
- package/build/Room.mjs +4 -1
- package/build/Room.mjs.map +2 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +15 -0
- package/build/Transport.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +16 -16
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +1 -3
- package/build/serializer/SchemaSerializer.mjs +16 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/package.json +7 -7
- package/src/Room.ts +5 -1
- package/src/Transport.ts +15 -0
- package/src/serializer/SchemaSerializer.ts +26 -19
|
@@ -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": ";AAAA,SAAS,UAAU,wBAAwB;AAG3C,SAA0C,mBAAmB;AAE7D,SAAwB,YAAY,QAAQ,SAAS,aAAa,UAAU,kBAAkD;AAC9H,SAAS,kBAAkB;AAC3B,SAAS,cAAc;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,iBAAiB;AAC3C,QAAM,KAAe,EAAE,QAAQ,EAAE;AACjC,SAAO,OAAO,KAAK,SAAS,GAAG,EAAE;AACjC,SAAO,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,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,UAAU,GAAG,KAAK;AAC9C,UAAI,SAAS,qBAAqB,UAAU,CAAC,GAAG;AAAE,eAAO;AAAA,MAAM;AAAA,IACjE;AAAA,EACF;AACA,SAAO;AACT;AAIA,SAAS,qBAAqB,QAAyB;AACrD,SAAO,OAAO,UAAU,YAAY,UAAU,OAAO,kBAAkB;AACzE;AAEA,IAAM,cAAc,CAAC;AAEd,IAAM,mBAAN,MAAkE;AAAA,EAAlE;AACL,SAAO,KAAK;AAGZ,SAAU,aAAsB;AAKhC;AAAA,SAAU,iBAA0B;
|
|
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,SAAS,UAAU,wBAAwB;AAG3C,SAA0C,mBAAmB;AAE7D,SAAwB,YAAY,QAAQ,SAAS,aAAa,UAAU,kBAAkD;AAC9H,SAAS,kBAAkB;AAC3B,SAAS,cAAc;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,iBAAiB;AAC3C,QAAM,KAAe,EAAE,QAAQ,EAAE;AACjC,SAAO,OAAO,KAAK,SAAS,GAAG,EAAE;AACjC,SAAO,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,SAAS,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,UAAU,GAAG,KAAK;AAC9C,UAAI,SAAS,qBAAqB,UAAU,CAAC,GAAG;AAAE,eAAO;AAAA,MAAM;AAAA,IACjE;AAAA,EACF;AACA,SAAO;AACT;AAIA,SAAS,qBAAqB,QAAyB;AACrD,SAAO,OAAO,UAAU,YAAY,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,QAAQ,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,SAAS;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,YAAY,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,SAAS;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,YAAY,OAAQ;AACzC,0BAAgB,QAAQ,gBAAgB,MAAM;AAAA,QAChD;AAAA,MACF;AAMA,aAAO;AAAA,IACT;AAEA,SAAK,iBAAiB;AAGtB,QAAI,WAAW,SAAS;AACtB,MAAC,WAAmB,cAAc,YAAY,KAAK,QAAQ,KAAK;AAAA,IAClE;AAGA,UAAM,KAAe,EAAE,QAAQ,EAAE;AACjC,SAAK,QAAQ,aAAa,CAAC,IAAI,SAAS;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,YAAY,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,YAAY,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,WAAW,SAAS;AACtB;AAAA,QACE;AAAA,QACA,eAAe;AAAA,QACf,QAAQ;AAAA,QACP,WAAmB;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,SAAS,mBAAmB,iBAAiB;AAC5E,WAAO,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,WAAO;AAAA,MACL;AAAA,IAGF;AAAA,EACF;AAAA,EAEO,YAAY;AAIjB,QAAI,CAAC,KAAK,gBAAgB;AAIxB,WAAK,iBAAkB,KAAK,QAAQ,SAAS,WAAW,OAAO,KAAK,OAAO;AAAA,IAC7E;AAEA,WAAO,KAAK;AAAA,EACd;AAEF;",
|
|
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.
|
|
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/
|
|
58
|
-
"@colyseus/
|
|
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-
|
|
64
|
+
"@colyseus/redis-presence": "^0.18.5",
|
|
65
65
|
"@colyseus/tools": "^0.18.10",
|
|
66
|
-
"@colyseus/redis-
|
|
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/
|
|
75
|
-
"@colyseus/
|
|
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
|
-
//
|
|
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.
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,
|
|
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,
|
|
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
|
}
|