@colyseus/core 0.18.4 → 0.18.5

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.
Files changed (118) hide show
  1. package/build/Debug.cjs +1 -0
  2. package/build/Debug.cjs.map +1 -1
  3. package/build/IPC.cjs +1 -0
  4. package/build/IPC.cjs.map +1 -1
  5. package/build/Logger.cjs +1 -0
  6. package/build/Logger.cjs.map +1 -1
  7. package/build/MatchMaker.cjs +11 -3
  8. package/build/MatchMaker.cjs.map +2 -2
  9. package/build/MatchMaker.mjs +10 -3
  10. package/build/MatchMaker.mjs.map +2 -2
  11. package/build/Protocol.cjs +1 -0
  12. package/build/Protocol.cjs.map +1 -1
  13. package/build/Rewind.cjs +1 -0
  14. package/build/Rewind.cjs.map +1 -1
  15. package/build/Room.cjs +102 -0
  16. package/build/Room.cjs.map +2 -2
  17. package/build/Room.d.ts +57 -0
  18. package/build/Room.mjs +101 -0
  19. package/build/Room.mjs.map +2 -2
  20. package/build/RoomMessages.cjs +1 -0
  21. package/build/RoomMessages.cjs.map +1 -1
  22. package/build/RoomPlugin.cjs +1 -0
  23. package/build/RoomPlugin.cjs.map +1 -1
  24. package/build/Server.cjs +1 -0
  25. package/build/Server.cjs.map +1 -1
  26. package/build/Stats.cjs +1 -0
  27. package/build/Stats.cjs.map +1 -1
  28. package/build/Transport.cjs +39 -0
  29. package/build/Transport.cjs.map +2 -2
  30. package/build/Transport.d.ts +66 -1
  31. package/build/Transport.mjs +37 -1
  32. package/build/Transport.mjs.map +2 -2
  33. package/build/errors/RoomExceptions.cjs +1 -0
  34. package/build/errors/RoomExceptions.cjs.map +1 -1
  35. package/build/errors/SeatReservationError.cjs +1 -0
  36. package/build/errors/SeatReservationError.cjs.map +1 -1
  37. package/build/errors/ServerError.cjs +1 -0
  38. package/build/errors/ServerError.cjs.map +1 -1
  39. package/build/index.cjs +5 -0
  40. package/build/index.cjs.map +2 -2
  41. package/build/index.d.ts +1 -1
  42. package/build/index.mjs +3 -1
  43. package/build/index.mjs.map +2 -2
  44. package/build/input/InputBuffer.cjs +1 -0
  45. package/build/input/InputBuffer.cjs.map +1 -1
  46. package/build/input/RoomInput.cjs +64 -3
  47. package/build/input/RoomInput.cjs.map +2 -2
  48. package/build/input/RoomInput.d.ts +16 -2
  49. package/build/input/RoomInput.mjs +63 -3
  50. package/build/input/RoomInput.mjs.map +2 -2
  51. package/build/input/types.cjs +1 -0
  52. package/build/input/types.cjs.map +1 -1
  53. package/build/internal.cjs +1 -0
  54. package/build/internal.cjs.map +1 -1
  55. package/build/matchmaker/Lobby.cjs +1 -0
  56. package/build/matchmaker/Lobby.cjs.map +1 -1
  57. package/build/matchmaker/LocalDriver/LocalDriver.cjs +1 -0
  58. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +1 -1
  59. package/build/matchmaker/LocalDriver/Query.cjs +1 -0
  60. package/build/matchmaker/LocalDriver/Query.cjs.map +1 -1
  61. package/build/matchmaker/RegisteredHandler.cjs +1 -0
  62. package/build/matchmaker/RegisteredHandler.cjs.map +1 -1
  63. package/build/matchmaker/controller.cjs +1 -0
  64. package/build/matchmaker/controller.cjs.map +1 -1
  65. package/build/matchmaker/driver.cjs +1 -0
  66. package/build/matchmaker/driver.cjs.map +2 -2
  67. package/build/matchmaker/driver.d.ts +4 -2
  68. package/build/matchmaker/driver.mjs.map +1 -1
  69. package/build/presence/LocalPresence.cjs +1 -0
  70. package/build/presence/LocalPresence.cjs.map +1 -1
  71. package/build/presence/Presence.cjs +1 -0
  72. package/build/presence/Presence.cjs.map +1 -1
  73. package/build/rooms/LobbyRoom.cjs +1 -0
  74. package/build/rooms/LobbyRoom.cjs.map +1 -1
  75. package/build/rooms/QueueRoom.cjs +1 -0
  76. package/build/rooms/QueueRoom.cjs.map +1 -1
  77. package/build/rooms/RelayRoom.cjs +1 -0
  78. package/build/rooms/RelayRoom.cjs.map +1 -1
  79. package/build/router/default_routes.cjs +2 -7
  80. package/build/router/default_routes.cjs.map +2 -2
  81. package/build/router/default_routes.mjs +2 -8
  82. package/build/router/default_routes.mjs.map +2 -2
  83. package/build/router/index.cjs +1 -0
  84. package/build/router/index.cjs.map +1 -1
  85. package/build/router/node.cjs +1 -0
  86. package/build/router/node.cjs.map +1 -1
  87. package/build/serializer/NoneSerializer.cjs +1 -0
  88. package/build/serializer/NoneSerializer.cjs.map +1 -1
  89. package/build/serializer/SchemaSerializer.cjs +98 -0
  90. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  91. package/build/serializer/SchemaSerializer.d.ts +25 -0
  92. package/build/serializer/SchemaSerializer.mjs +98 -1
  93. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  94. package/build/serializer/Serializer.cjs +1 -0
  95. package/build/serializer/Serializer.cjs.map +2 -2
  96. package/build/serializer/Serializer.d.ts +15 -0
  97. package/build/utils/DevMode.cjs +1 -0
  98. package/build/utils/DevMode.cjs.map +1 -1
  99. package/build/utils/Env.cjs +1 -0
  100. package/build/utils/Env.cjs.map +1 -1
  101. package/build/utils/StandardSchema.cjs +1 -0
  102. package/build/utils/StandardSchema.cjs.map +1 -1
  103. package/build/utils/UserSessionIndex.cjs +1 -0
  104. package/build/utils/UserSessionIndex.cjs.map +1 -1
  105. package/build/utils/Utils.cjs +1 -0
  106. package/build/utils/Utils.cjs.map +1 -1
  107. package/build/utils/nanoevents.cjs +1 -0
  108. package/build/utils/nanoevents.cjs.map +1 -1
  109. package/package.json +6 -6
  110. package/src/MatchMaker.ts +20 -3
  111. package/src/Room.ts +122 -0
  112. package/src/Transport.ts +132 -4
  113. package/src/index.ts +1 -1
  114. package/src/input/RoomInput.ts +76 -3
  115. package/src/matchmaker/driver.ts +4 -2
  116. package/src/router/default_routes.ts +2 -8
  117. package/src/serializer/SchemaSerializer.ts +146 -1
  118. package/src/serializer/Serializer.ts +17 -0
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/UserSessionIndex.ts"],
4
4
  "sourcesContent": ["/**\n * User \u2192 active sessions index: \"which active rooms is user X in?\"\n *\n * Without this, answering that question would require fanning out\n * `getInspectorView()` across every running room \u2014 O(rooms) per lookup.\n * Instead, each Room writes a small Presence hash entry on `_onJoin` and\n * removes it on `_onAfterLeave` / dispose. The admin endpoint reads the\n * hash with a single `hgetall`, then reconciles against the matchmaker's\n * live room listing to drop stale entries left behind by hard crashes\n * (no `onLeave` ran).\n *\n * Hash schema:\n * key: colyseus:user-rooms:{userId}\n * field: sessionId\n * value: JSON `{ roomId, roomName, joinedAt }` (joinedAt is unix ms)\n *\n * Anonymous clients (no userId resolvable) are skipped \u2014 the index is\n * for forensic / support workflows, not anonymous traffic. A Room can\n * also opt out wholesale by setting `trackUserSessions = false` (e.g.\n * a high-volume relay room that doesn't want to pay the Presence\n * write per join).\n *\n * Per-Room state (which sessionIds have an entry, and under which userId)\n * lives in a module-level WeakMap rather than as a field on Room itself.\n * Two reasons:\n *\n * 1. Room.ts stays thin \u2014 it only knows about three call points\n * (`trackRoomJoin`, `releaseRoomLeave`, `sweepRoomDispose`) and\n * doesn't have to carry an extra Map field or two private helper\n * methods solely for this concern.\n * 2. The WeakMap is GC-tied to the Room \u2014 when a Room instance is\n * collected, its entries vanish automatically. No explicit teardown\n * hook needed beyond the dispose sweep that drains Presence.\n *\n * @internal\n */\nimport type { Presence } from '../presence/Presence.ts';\n\nexport const USER_ROOMS_KEY_PREFIX = 'colyseus:user-rooms:';\n\nexport function userRoomsKey(userId: string): string {\n return USER_ROOMS_KEY_PREFIX + userId;\n}\n\n/**\n * What's serialized into the Presence hash value (sessionId is the\n * hash field key, not part of the body). Internal write-side shape.\n */\nexport interface UserRoomEntry {\n roomId: string;\n roomName: string;\n joinedAt: number;\n}\n\n/**\n * Public read-side shape returned by `listUserSessions`: a parsed\n * `UserRoomEntry` plus its sessionId, optionally enriched with\n * `processId` when reconcile against the matchmaker was on.\n */\nexport interface UserSessionInfo extends UserRoomEntry {\n sessionId: string;\n /**\n * Process hosting the room, per the matchmaker. Populated only\n * when `listUserSessions` was called with `reconcile: true` AND\n * the room is still in the matchmaker roster.\n */\n processId?: string;\n}\n\n/**\n * Structural subset of `Room` needed by the index. Lets this module\n * avoid importing `Room` (which would create a cycle) while still\n * staying typed at the call site.\n */\ninterface InspectorRoomShape {\n roomId: string;\n roomName: string;\n presence: Presence;\n}\n\n/**\n * Structural subset of a Client we read at join/leave. `userId` and\n * `auth` are both optional \u2014 the index simply skips clients without\n * either, which is the correct \"anonymous traffic doesn't show up\n * in support tooling\" behavior.\n */\ninterface InspectorClientShape {\n sessionId: string;\n userId?: string;\n auth?: { id?: string } | null;\n}\n\n/**\n * sessionId \u2192 userId for clients currently registered in the index,\n * scoped per Room. WeakMap-keyed so a forgotten Room takes its tracking\n * map with it.\n */\nconst tracked = new WeakMap<InspectorRoomShape, Map<string, string>>();\n\nfunction getTrackingMap(room: InspectorRoomShape): Map<string, string> {\n let map = tracked.get(room);\n if (!map) {\n map = new Map();\n tracked.set(room, map);\n }\n return map;\n}\n\nfunction resolveUserId(client: InspectorClientShape): string | undefined {\n return client.userId ?? client.auth?.id;\n}\n\n/**\n * Best-effort: write the join entry. Errors are swallowed because the\n * index is observability metadata \u2014 a Presence outage shouldn't reject\n * a player's join. Exposed as a pure helper for tests + the admin\n * endpoint; `trackRoomJoin` is the Room-flavored entrypoint.\n */\nexport async function trackUserSession(\n presence: Presence,\n userId: string,\n sessionId: string,\n entry: UserRoomEntry,\n): Promise<void> {\n try {\n await presence.hset(userRoomsKey(userId), sessionId, JSON.stringify(entry));\n } catch {\n // intentional: see fn-doc\n }\n}\n\n/**\n * Best-effort: remove the join entry. Errors are swallowed so a Presence\n * blip doesn't bubble into `_onAfterLeave` / `_dispose`.\n */\nexport async function releaseUserSession(\n presence: Presence,\n userId: string,\n sessionId: string,\n): Promise<void> {\n try {\n await presence.hdel(userRoomsKey(userId), sessionId);\n } catch {\n // intentional: see fn-doc\n }\n}\n\n/**\n * Record `client`'s join under `room` in the reverse index. No-op for\n * clients with no resolvable userId (anonymous), and when the room\n * carries no presence (shouldn't happen \u2014 defensive for unit tests).\n *\n * Fire-and-forget against Presence; the in-memory tracking map updates\n * synchronously so a follow-up `releaseRoomLeave` always finds the\n * right userId even if the Presence write is still in flight.\n */\nexport function trackRoomJoin(room: InspectorRoomShape, client: InspectorClientShape): void {\n if (!room.presence) { return; }\n const userId = resolveUserId(client);\n if (!userId) { return; }\n getTrackingMap(room).set(client.sessionId, userId);\n const entry: UserRoomEntry = {\n roomId: room.roomId,\n roomName: room.roomName,\n joinedAt: Date.now(),\n };\n void trackUserSession(room.presence, userId, client.sessionId, entry);\n}\n\n/**\n * Drop `client`'s entry from the reverse index. Idempotent \u2014 no-op\n * when the client wasn't tracked (anonymous, or tracking failed at\n * join time).\n */\nexport function releaseRoomLeave(room: InspectorRoomShape, client: InspectorClientShape): void {\n if (!room.presence) { return; }\n const map = tracked.get(room);\n const userId = map?.get(client.sessionId);\n if (!userId || !map) { return; }\n map.delete(client.sessionId);\n void releaseUserSession(room.presence, userId, client.sessionId);\n}\n\n/**\n * Sweep any still-tracked sessions for `room`. Called during dispose to\n * cover the case where `disconnect()` races the per-client `_onAfterLeave`\n * path \u2014 the read-side reconcile handles cross-process crash recovery,\n * but this is the cheap deterministic cleanup for a clean local dispose.\n *\n * Awaits the pending `hdel`s so the caller can sequence against \"the\n * index is now coherent\" \u2014 the dispose path uses that ordering.\n */\nexport async function sweepRoomDispose(room: InspectorRoomShape): Promise<void> {\n if (!room.presence) { return; }\n const map = tracked.get(room);\n if (!map || map.size === 0) { return; }\n const pending: Promise<void>[] = [];\n for (const [sessionId, userId] of map) {\n pending.push(releaseUserSession(room.presence, userId, sessionId));\n }\n map.clear();\n await Promise.all(pending);\n}\n\n/**\n * Minimal shape of a matchmaker room record needed for reconcile \u2014\n * keeps this module decoupled from the matchmaker / driver types.\n * `matchMaker.query()`'s actual return (`IRoomCache[]`) is structurally\n * a supertype of this, so callers can pass `matchMaker.query` directly.\n */\ninterface MatchmakerRoomLike {\n roomId: string;\n processId?: string;\n}\n\nexport interface ListUserSessionsOptions {\n /**\n * Drop entries whose `roomId` is no longer in the matchmaker roster\n * (the index can lag a crashed process). When `true`, the returned\n * entries also carry `processId` from the live room record.\n *\n * Off by default \u2014 most callers (kick everyone, count) don't need it\n * and the extra `matchMaker.query` round-trip isn't free.\n */\n reconcile?: boolean;\n\n /**\n * Fire-and-forget `hdel` for stale entries \u2014 those dropped by\n * reconcile (matchmaker doesn't know the room anymore) plus any\n * with corrupt JSON. Lets read endpoints self-heal the index on\n * each call. No-op when `reconcile` is `false`.\n */\n removeStale?: boolean;\n}\n\n/**\n * Read the user \u2192 active sessions index. Pure helper \u2014 the\n * `Presence` + matchmaker batch lookup are injected so this module\n * stays free of matchmaker imports (and so unit tests can drive it\n * with fake deps).\n *\n * Wire-op count per call:\n * - 1 HGETALL on the user's hash (always).\n * - 1 batch room lookup when `reconcile: true` AND there are\n * entries to verify; skipped otherwise.\n *\n * Bounded at 2 wire ops regardless of the user's session count.\n */\nexport async function listUserSessions(\n presence: Presence,\n findRooms: (roomIds: string[]) => Promise<Map<string, MatchmakerRoomLike>>,\n userId: string,\n options: ListUserSessionsOptions = {},\n): Promise<UserSessionInfo[]> {\n const reconcile = options.reconcile === true;\n const removeStale = reconcile && options.removeStale === true;\n\n let raw: Record<string, string>;\n try {\n raw = await presence.hgetall(userRoomsKey(userId));\n } catch {\n // Presence outage \u2014 observability shouldn't bring down the caller.\n return [];\n }\n const fields = Object.keys(raw);\n if (fields.length === 0) { return []; }\n\n const staleSessions: string[] = [];\n const parsed: Array<{ sessionId: string; entry: UserRoomEntry }> = [];\n for (const sessionId of fields) {\n try {\n parsed.push({ sessionId, entry: JSON.parse(raw[sessionId]) as UserRoomEntry });\n } catch {\n // Corrupt JSON \u2014 index drift. Removable even without reconcile.\n staleSessions.push(sessionId);\n }\n }\n\n if (!reconcile) {\n return parsed.map(({ sessionId, entry }) => ({ sessionId, ...entry }));\n }\n\n // One batch lookup for the K roomIds we care about. K = entries\n // surviving the JSON.parse stage, not cluster size.\n const live = parsed.length > 0\n ? await findRooms(parsed.map((p) => p.entry.roomId))\n : new Map<string, MatchmakerRoomLike>();\n\n const result: UserSessionInfo[] = [];\n for (const { sessionId, entry } of parsed) {\n const room = live.get(entry.roomId);\n if (!room) {\n // Matchmaker doesn't know this roomId anymore \u2014 stale entry\n // from a crashed process. Drop it (and remove if requested).\n staleSessions.push(sessionId);\n continue;\n }\n const info: UserSessionInfo = { sessionId, ...entry };\n if (room.processId !== undefined) { info.processId = room.processId; }\n result.push(info);\n }\n\n if (removeStale && staleSessions.length > 0) {\n const key = userRoomsKey(userId);\n void Promise.all(\n staleSessions.map((s) => presence.hdel(key, s)),\n ).catch(() => { /* presence outage, swallow */ });\n }\n\n return result;\n}\n"],
5
- "mappings": "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",
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ const __cjsImportMetaUrl = require('node:url').pathToFileURL(__filename).href;
1
2
  "use strict";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Utils.ts"],
4
4
  "sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n/**\n * Return type of `remoteRoomCall()`.\n *\n * Resolves to the method's awaited return type (or the property's type) when\n * the method name was captured as a literal type. Falls back to `any` when it\n * wasn't \u2014 e.g. `remoteRoomCall<MyRoom>(...)` with only the room type given:\n * TypeScript applies the `TMethod` default instead of inferring the literal\n * once an explicit type argument list is present (microsoft/TypeScript#26242).\n * Pass both type arguments (`remoteRoomCall<MyRoom, 'myMethod'>`) for a\n * precise return type.\n */\nexport type RemoteRoomCallReturn<\n TRoom,\n TMethod extends keyof TRoom\n> = keyof TRoom extends TMethod\n ? any\n : ExtractMethodOrPropertyType<TRoom, TMethod>;\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
5
- "mappings": "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",
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ const __cjsImportMetaUrl = require('node:url').pathToFileURL(__filename).href;
1
2
  "use strict";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/nanoevents.ts"],
4
4
  "sourcesContent": ["export const createNanoEvents = () => ({\n emit(event: string, ...args: any[]) {\n for (\n let callbacks = this.events[event] || [],\n i = 0,\n length = callbacks.length;\n i < length;\n i++\n ) {\n callbacks[i](...args)\n }\n },\n // null-prototype: event names are client-supplied (message types), and on a\n // plain object \"__proto__\" / \"constructor\" / \"toString\" resolve to inherited\n // members instead of missing (colyseus/colyseus#951)\n events: Object.create(null) as { [event: string]: Array<(...args: any[]) => void> },\n on(event: string, cb: (...args: any[]) => void) {\n ;(this.events[event] ||= []).push(cb)\n return () => {\n this.events[event] = this.events[event]?.filter(i => cb !== i)\n }\n }\n })"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,mBAAmB,OAAO;AAAA,EACnC,KAAK,UAAkB,MAAa;AAClC,aACM,YAAY,KAAK,OAAO,KAAK,KAAK,CAAC,GACrC,IAAI,GACJ,SAAS,UAAU,QACrB,IAAI,QACJ,KACA;AACA,gBAAU,CAAC,EAAE,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAIA,QAAQ,uBAAO,OAAO,IAAI;AAAA,EAC1B,GAAG,OAAe,IAA8B;AAC9C;AAAC,KAAC,KAAK,OAAO,KAAK,MAAM,CAAC,GAAG,KAAK,EAAE;AACpC,WAAO,MAAM;AACX,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,OAAO,OAAK,OAAO,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;",
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,mBAAmB,OAAO;AAAA,EACnC,KAAK,UAAkB,MAAa;AAClC,aACM,YAAY,KAAK,OAAO,KAAK,KAAK,CAAC,GACrC,IAAI,GACJ,SAAS,UAAU,QACrB,IAAI,QACJ,KACA;AACA,gBAAU,CAAC,EAAE,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAIA,QAAQ,uBAAO,OAAO,IAAI;AAAA,EAC1B,GAAG,OAAe,IAA8B;AAC9C;AAAC,KAAC,KAAK,OAAO,KAAK,MAAM,CAAC,GAAG,KAAK,EAAE;AACpC,WAAO,MAAM;AACX,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,OAAO,OAAK,OAAO,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -52,14 +52,14 @@
52
52
  "msgpackr": "^2.0.1",
53
53
  "nanoid": "^3.3.11",
54
54
  "@colyseus/better-call": "^1.3.1",
55
- "@colyseus/greeting-banner": "^4.0.1",
56
- "@colyseus/shared-types": "^0.18.1"
55
+ "@colyseus/shared-types": "^0.18.1",
56
+ "@colyseus/greeting-banner": "^4.0.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@colyseus/schema": "^5.0.8",
60
60
  "express": "^5.0.0",
61
61
  "vitest": "^3.1.1",
62
- "@colyseus/redis-driver": "^0.18.1",
62
+ "@colyseus/redis-driver": "^0.18.2",
63
63
  "@colyseus/redis-presence": "^0.18.2",
64
64
  "@colyseus/tools": "^0.18.2"
65
65
  },
@@ -68,9 +68,9 @@
68
68
  "@pm2/io": "^6.1.0",
69
69
  "express": "^4.16.0 || ^5.0.0",
70
70
  "zod": "^4.1.12",
71
+ "@colyseus/ws-transport": "^0.18.1",
71
72
  "@colyseus/auth": "^0.18.1",
72
- "@colyseus/better-call": "^1.3.1",
73
- "@colyseus/ws-transport": "^0.18.1"
73
+ "@colyseus/better-call": "^1.3.1"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@colyseus/auth": {
package/src/MatchMaker.ts CHANGED
@@ -37,6 +37,16 @@ const handlers: {[id: string]: RegisteredHandler} = Object.create(null);
37
37
  const rooms: {[roomId: string]: Room} = {};
38
38
  const events = new EventEmitter();
39
39
 
40
+ // A room isn't gone when 'dispose' fires — that only *starts* `Room.#_dispose()`,
41
+ // which awaits the user's async `onDispose()`. `roomCount` is decremented there,
42
+ // so this covers the window until 'disconnect' (emitted once `#_dispose()`
43
+ // settles). Invariant: roomCount + disposingRoomCount = rooms not yet fully gone.
44
+ let disposingRoomCount = 0;
45
+
46
+ function hasActiveRooms() {
47
+ return stats.local.roomCount > 0 || disposingRoomCount > 0;
48
+ }
49
+
40
50
  export let publicAddress: string;
41
51
  export let processId: string;
42
52
  export let presence: Presence;
@@ -103,6 +113,7 @@ export async function setup(
103
113
  publicAddress = _publicAddress || getDefaultPublicAddress();
104
114
 
105
115
  stats.reset(false);
116
+ disposingRoomCount = 0;
106
117
 
107
118
  // ensure processId is set
108
119
  if (!processId) { processId = generateId(); }
@@ -648,11 +659,14 @@ export async function handleCreateRoom(roomName: string, clientOptions: ClientOp
648
659
  room['_events'].removeAllListeners('metadata-change');
649
660
  }
650
661
 
662
+ // this room's `onDispose()` has settled
663
+ disposingRoomCount--;
664
+
651
665
  //
652
666
  // emit "no active rooms" event when there are no more rooms in this process
653
667
  // (used during graceful shutdown)
654
668
  //
655
- if (stats.local.roomCount <= 0) {
669
+ if (!hasActiveRooms()) {
656
670
  events.emit('no-active-rooms');
657
671
  }
658
672
  });
@@ -713,7 +727,7 @@ async function lockAndDisposeAll(): Promise<any> {
713
727
  }
714
728
 
715
729
  const noActiveRooms = new Deferred();
716
- if (stats.local.roomCount <= 0) {
730
+ if (!hasActiveRooms()) {
717
731
  // no active rooms to dispose
718
732
  noActiveRooms.resolve();
719
733
 
@@ -820,7 +834,7 @@ export async function hotReload(): Promise<void> {
820
834
 
821
835
  // Lock all rooms and trigger default onBeforeShutdown (dev mode impl).
822
836
  const noActiveRooms = new Deferred();
823
- if (stats.local.roomCount <= 0) {
837
+ if (!hasActiveRooms()) {
824
838
  noActiveRooms.resolve();
825
839
  } else {
826
840
  events.once('no-active-rooms', () => noActiveRooms.resolve());
@@ -1205,6 +1219,9 @@ async function disposeRoom(roomName: string, room: Room) {
1205
1219
  driver.remove(room['_listing'].roomId);
1206
1220
  stats.local.roomCount--;
1207
1221
 
1222
+ // `onDispose()` is still pending — released on 'disconnect'
1223
+ disposingRoomCount++;
1224
+
1208
1225
  // decrease amount of rooms this process is handling
1209
1226
  if (state !== MatchMakerState.SHUTTING_DOWN) {
1210
1227
  stats.persist();
package/src/Room.ts CHANGED
@@ -56,6 +56,9 @@ export {
56
56
 
57
57
  const DEFAULT_PATCH_RATE = 1000 / 20; // 20fps (50ms)
58
58
  const DEFAULT_SIMULATION_INTERVAL = 1000 / 60; // 60fps (16.66ms)
59
+
60
+ // Shared so the unarmed `_flushUnreliable` call site stays monomorphic.
61
+ const NOOP = () => {};
59
62
  const noneSerializer = new NoneSerializer();
60
63
 
61
64
  /** Shared `enqueueRaw` options routing a frame onto `_pendingFrames` to ride the
@@ -276,6 +279,45 @@ export class Room<T extends RoomOptions = RoomOptions> {
276
279
  #_patchRate: number;
277
280
  #_patchInterval: NodeJS.Timeout;
278
281
 
282
+ /**
283
+ * Frequency to flush `@unreliable` state fields, in milliseconds.
284
+ *
285
+ * Those fields never ride a state patch — they go out over the transport's
286
+ * unreliable channel (a WebTransport datagram), so a dropped frame costs one
287
+ * stale value instead of stalling the reliable stream behind a retransmit.
288
+ * Setting this decouples them from {@link patchRate}, which is the point:
289
+ * 60Hz movement over a 20Hz structural patch.
290
+ *
291
+ * KNOWN COST of a rate faster than {@link patchRate}: an entity's ADD travels
292
+ * the reliable channel, so datagrams sent between patches can reference a
293
+ * refId the client hasn't been told about yet. Those frames are skipped by the
294
+ * client's decoder — safe (the ref graph can't desync, since `@unreliable` is
295
+ * primitives-only) but each one logs `"refId" not found`, and that entity's
296
+ * first value lands one mutation later. Measured at `patchRate/this` reports
297
+ * per mid-session spawn. Leave this unset and the flush rides
298
+ * {@link broadcastPatch}, which ships the ADD first and avoids it entirely.
299
+ *
300
+ * Requires a transport with a datagram channel — today only
301
+ * `@colyseus/h3-transport` (WebTransport), which is **experimental**. Every
302
+ * WebSocket transport lacks one, and those clients are skipped entirely (the
303
+ * room warns once), so `@unreliable` fields keep their join-time value there.
304
+ *
305
+ * @default null — flush alongside every {@link broadcastPatch}, and only when
306
+ * the state actually declares an `@unreliable` field.
307
+ */
308
+ public unreliablePatchRate: number | null = null;
309
+ #_unreliablePatchRate: number | null = null;
310
+ #_unreliablePatchInterval: NodeJS.Timeout;
311
+
312
+ /**
313
+ * The unreliable flush, called unconditionally at the end of every
314
+ * {@link broadcastPatch}. Stays {@link NOOP} unless the state actually
315
+ * declares an `@unreliable` field, so a room that never uses the channel
316
+ * pays an empty call the engine inlines away — and there is no second
317
+ * entry point that could drift from `broadcastPatch()`.
318
+ */
319
+ private _flushUnreliable: () => void = NOOP;
320
+
279
321
  /**
280
322
  * Maximum number of messages a client can send to the server per second.
281
323
  * If a client sends more messages than this, it will be disconnected.
@@ -420,6 +462,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
420
462
  this.#_state = this.state;
421
463
  this.#_autoDispose = this.autoDispose;
422
464
  this.#_patchRate = this.patchRate;
465
+ this.#_unreliablePatchRate = this.unreliablePatchRate;
423
466
  this.#_maxClients = this.maxClients;
424
467
 
425
468
  Object.defineProperties(this, {
@@ -436,6 +479,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
436
479
  }
437
480
  this._serializer.reset(newState);
438
481
  this.#_state = newState;
482
+ this._armUnreliablePatches();
439
483
  },
440
484
  },
441
485
 
@@ -479,6 +523,15 @@ export class Room<T extends RoomOptions = RoomOptions> {
479
523
  }
480
524
  },
481
525
  },
526
+
527
+ unreliablePatchRate: {
528
+ enumerable: true,
529
+ get: () => this.#_unreliablePatchRate,
530
+ set: (milliseconds: number | null) => {
531
+ this.#_unreliablePatchRate = milliseconds;
532
+ this._armUnreliablePatches();
533
+ },
534
+ },
482
535
  });
483
536
 
484
537
  // set patch interval, now with the setter
@@ -1300,6 +1353,12 @@ export class Room<T extends RoomOptions = RoomOptions> {
1300
1353
  this._inputController !== undefined ? { sNow } : undefined,
1301
1354
  );
1302
1355
 
1356
+ // Flush `@unreliable` fields over the datagram channel, after the reliable
1357
+ // patch so a spawn's ADD is already on the wire when the datagram mutating
1358
+ // it goes out. No-op unless the state declares such a field, and skipped
1359
+ // when `unreliablePatchRate` gave the flush its own timer.
1360
+ this._flushUnreliable();
1361
+
1303
1362
  // Deliver any per-client `afterNextPatch` frames as standalone frames right
1304
1363
  // after the patch (never coalesced into it). Iterates only the clients that
1305
1364
  // staged frames this cycle (`#pendingFrameClients`), never the full list.
@@ -1321,6 +1380,64 @@ export class Room<T extends RoomOptions = RoomOptions> {
1321
1380
  return hasChanges;
1322
1381
  }
1323
1382
 
1383
+ /**
1384
+ * Encode and send the `@unreliable` state fields over each client's
1385
+ * unreliable channel. Those fields never appear in a {@link broadcastPatch}
1386
+ * frame, and clients on a transport without a datagram channel are skipped.
1387
+ *
1388
+ * Driven automatically once the state declares an `@unreliable` field — right
1389
+ * after each {@link broadcastPatch}, or on its own timer when
1390
+ * {@link unreliablePatchRate} is set. Public so a room driving
1391
+ * `broadcastPatch()` by hand can drive this by hand too.
1392
+ */
1393
+ public broadcastUnreliablePatch() {
1394
+ if (!this.state) {
1395
+ return false;
1396
+ }
1397
+ return this._serializer.applyUnreliablePatches?.(this.clients) ?? false;
1398
+ }
1399
+
1400
+ /**
1401
+ * Decide how the unreliable channel is driven — and, for the rooms that never
1402
+ * touch it, decide to not drive it at all. Runs when the state is assigned
1403
+ * (the serializer knows by then whether any `@unreliable` field exists) and
1404
+ * whenever {@link unreliablePatchRate} changes.
1405
+ *
1406
+ * A room whose state declares no `@unreliable` field leaves
1407
+ * {@link _flushUnreliable} at {@link NOOP}, so its patch tick costs exactly
1408
+ * what it did before this feature existed.
1409
+ */
1410
+ private _armUnreliablePatches() {
1411
+ if (this.#_unreliablePatchInterval) {
1412
+ clearInterval(this.#_unreliablePatchInterval);
1413
+ this.#_unreliablePatchInterval = undefined;
1414
+ }
1415
+
1416
+ const armed = this._serializer?.hasUnreliableFields === true;
1417
+ const rate = this.#_unreliablePatchRate;
1418
+ const dedicated = armed && rate !== null && rate !== 0;
1419
+
1420
+ // Default mode flushes from the patch itself rather than an independent
1421
+ // timer of the same period, which would put the datagram ahead of a spawn's
1422
+ // ADD about half the time.
1423
+ const inline = armed && !dedicated;
1424
+
1425
+ this._flushUnreliable = inline
1426
+ ? () => { this.broadcastUnreliablePatch(); }
1427
+ : NOOP;
1428
+
1429
+ if (dedicated) {
1430
+ this.#_unreliablePatchInterval = setInterval(() => this.broadcastUnreliablePatch(), rate);
1431
+
1432
+ } else if (inline && !this.#_patchRate) {
1433
+ // patchRate 0/null means no patch tick to piggyback on.
1434
+ logger.warn(
1435
+ "@colyseus/core: state has @unreliable fields but patchRate is disabled —" +
1436
+ " set `room.unreliablePatchRate` to flush them, or they will never update."
1437
+ );
1438
+ }
1439
+ }
1440
+
1324
1441
  /**
1325
1442
  * Register a message handler for a specific message type.
1326
1443
  * This method is used to handle messages sent by clients to the room.
@@ -2078,6 +2195,11 @@ export class Room<T extends RoomOptions = RoomOptions> {
2078
2195
  this.#_patchInterval = undefined;
2079
2196
  }
2080
2197
 
2198
+ if (this.#_unreliablePatchInterval) {
2199
+ clearInterval(this.#_unreliablePatchInterval);
2200
+ this.#_unreliablePatchInterval = undefined;
2201
+ }
2202
+
2081
2203
  if (this._simulationInterval) {
2082
2204
  clearInterval(this._simulationInterval);
2083
2205
  this._simulationInterval = undefined;
package/src/Transport.ts CHANGED
@@ -8,7 +8,8 @@ import { StateView } from '@colyseus/schema';
8
8
  import type { InputDecoder } from '@colyseus/schema/input';
9
9
 
10
10
  import { EventEmitter } from 'events';
11
- import { spliceOne } from './utils/Utils.ts';
11
+ import { debugAndPrintError } from './Debug.ts';
12
+ import { getBearerToken, spliceOne } from './utils/Utils.ts';
12
13
  import { ServerError } from './errors/ServerError.ts';
13
14
 
14
15
  import type { Room } from './Room.ts';
@@ -46,12 +47,129 @@ export abstract class Transport {
46
47
  public bindRouter?(router: Router): void;
47
48
  }
48
49
 
50
+ /**
51
+ * Intercepts an incoming WebSocket upgrade request, before the handshake.
52
+ *
53
+ * Return a `Response` to answer the request instead of upgrading it. Return
54
+ * nothing to upgrade as usual. The handler may be async, and the handshake waits
55
+ * for it to resolve.
56
+ *
57
+ * `context` is the same shape `onAuth()` receives, read-only here: mutating it
58
+ * does not carry over to `onAuth()`.
59
+ *
60
+ * Not supported by `H3Transport`: WebTransport has no upgrade handshake.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * new uWebSocketsTransport({
65
+ * beforeUpgrade: async (request, context) => {
66
+ * if (await isBanned(context.ip)) {
67
+ * return new Response(null, { status: 403 });
68
+ * }
69
+ * }
70
+ * });
71
+ * ```
72
+ */
73
+ export type BeforeUpgradeHandler = (
74
+ request: Request,
75
+ context: Readonly<AuthContext>,
76
+ ) => Response | void | Promise<Response | void>;
77
+
78
+ /**
79
+ * Invokes a `beforeUpgrade` handler, resolving with the `Response` to send
80
+ * instead of upgrading, or `undefined` to proceed with the upgrade.
81
+ *
82
+ * Every transport goes through here, so a handler written against one keeps
83
+ * working on the others. Never rejects: uWebSockets.js aborts the process on an
84
+ * upgrade handler that yields without responding, and on the other transports a
85
+ * raw socket left behind leaks a connection.
86
+ *
87
+ * @internal
88
+ */
89
+ export async function runBeforeUpgrade(
90
+ handler: BeforeUpgradeHandler,
91
+ url: string, // path, optionally including the query string
92
+ context: AuthContext,
93
+ ): Promise<Response | undefined> {
94
+ let request: Request;
95
+
96
+ try {
97
+ const host = context.headers.get('host') || 'localhost';
98
+ request = new Request(`http://${host}${url}`, { headers: context.headers });
99
+
100
+ } catch (e: any) {
101
+ // a `Host` header that isn't a valid authority fails to parse as a URL
102
+ debugAndPrintError(e);
103
+ return new Response(null, { status: 400 });
104
+ }
105
+
106
+ try {
107
+ return (await handler(request, context)) ?? undefined;
108
+
109
+ } catch (e: any) {
110
+ debugAndPrintError(e);
111
+ return new Response(null, { status: 500 });
112
+ }
113
+ }
114
+
115
+ /** Headers as the transport has them: uWebSockets.js and Node give a plain record. */
116
+ type RawHeaders = Headers | Record<string, string | undefined>;
117
+
118
+ const readHeader = (headers: RawHeaders, name: string) =>
119
+ (headers instanceof Headers) ? headers.get(name) : headers[name];
120
+
121
+ /**
122
+ * Builds the context passed to `beforeUpgrade` and `onAuth`.
123
+ *
124
+ * Every transport goes through here, so the context is identical everywhere,
125
+ * down to how the client address is resolved. `headers` is materialized on
126
+ * first read: a connection nobody inspects pays nothing for the conversion.
127
+ *
128
+ * @internal
129
+ */
130
+ export function createAuthContext(options: {
131
+ headers: RawHeaders,
132
+ token?: string | null,
133
+ remoteAddress?: string,
134
+ req?: any,
135
+ }): AuthContext {
136
+ const source = options.headers;
137
+ let headers: Headers | undefined;
138
+
139
+ return {
140
+ token: options.token ?? getBearerToken(readHeader(source, 'authorization')),
141
+ ip: resolveClientIp(source, options.remoteAddress),
142
+ req: options.req,
143
+ get headers() {
144
+ return headers ??= (source instanceof Headers)
145
+ ? source
146
+ : new Headers(source as Record<string, string>);
147
+ },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * A single address, resolved the same way on every transport: `x-forwarded-for`
153
+ * carries the whole proxy chain, and only its first entry is the client.
154
+ */
155
+ function resolveClientIp(headers: RawHeaders, remoteAddress?: string): string | undefined {
156
+ // an empty header counts as absent
157
+ const firstHop = (name: string) => readHeader(headers, name)?.split(',')[0].trim() || undefined;
158
+
159
+ return (
160
+ firstHop('x-real-ip') ??
161
+ firstHop('x-forwarded-for') ??
162
+ firstHop('x-client-ip') ??
163
+ (remoteAddress || undefined)
164
+ );
165
+ }
166
+
49
167
  export type AuthContext = {
50
168
  token?: string,
169
+ /** Undefined when no proxy header carries it and the transport has no peer address. */
170
+ ip: string | undefined;
51
171
  headers: Headers,
52
- ip: string | string[];
53
- // FIXME: each transport may have its own specific properties.
54
- // "req" only applies to WebSocketTransport.
172
+ /** Only set on the HTTP matchmaking request, where it is the `Request` itself. */
55
173
  req?: any;
56
174
  };
57
175
 
@@ -136,6 +254,16 @@ export interface Client<T extends { userData?: any, auth?: any, messages?: Recor
136
254
  raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
137
255
  enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;
138
256
 
257
+ /**
258
+ * Send raw bytes over the transport's UNRELIABLE channel — no delivery,
259
+ * ordering, or duplication guarantee. Used for `@unreliable` state patches.
260
+ *
261
+ * Absent on transports with no datagram channel (every WebSocket transport).
262
+ * Its presence IS the capability check — callers feature-detect rather than
263
+ * reading a separate flag, and skip clients that can't receive.
264
+ */
265
+ rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
266
+
139
267
  /**
140
268
  * Send a type of message to the client. Messages are encoded with MsgPack and can hold any
141
269
  * JSON-serializable data structure.
package/src/index.ts CHANGED
@@ -56,7 +56,7 @@ export * from './matchmaker/LocalDriver/LocalDriver.ts';
56
56
  export { initializeRoomCache } from './matchmaker/driver.ts';
57
57
 
58
58
  // Transport
59
- export { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, connectClientToRoom, enqueueClientRaw } from './Transport.ts';
59
+ export { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';
60
60
 
61
61
  // Presence
62
62
  export { type Presence } from './presence/Presence.ts';
@@ -21,6 +21,20 @@ import { debugAndPrintError } from '../Debug.ts';
21
21
  */
22
22
  const _inputReflectionCache = new WeakMap<Function, Uint8Array>();
23
23
 
24
+ /**
25
+ * Rebuild one `k`-length series of the unreliable stamp block: `newest` is the
26
+ * absolute anchor, and each wire delta walks one slot older
27
+ * (`out[i] = out[i+1] − Δ`). Mirrors the SDK's `_writeSeriesDeltas`.
28
+ */
29
+ function readSeriesDeltas(buffer: Buffer, it: Iterator, k: number, newest: number): number[] {
30
+ const out = new Array<number>(k);
31
+ out[k - 1] = newest;
32
+ for (let i = k - 2; i >= 0; i--) {
33
+ out[i] = out[i + 1] - decode.number(buffer, it);
34
+ }
35
+ return out;
36
+ }
37
+
24
38
  /**
25
39
  * Runtime behind {@link InputAPI}. A class, not a per-`define()` object literal:
26
40
  * literal (and `defineProperty`) accessors carry their closure identity in the
@@ -272,11 +286,70 @@ export class RoomInput {
272
286
  }
273
287
 
274
288
  /** Decode a `ROOM_INPUT_UNRELIABLE` redundancy ring — each slot carries its
275
- * framework seq (base seq + position) for ring dedupe, no user seqField. */
276
- decodeUnreliable(client: ClientPrivate, buffer: Buffer, _modifiers: number): void {
289
+ * framework seq (base seq + position) for ring dedupe, no user seqField.
290
+ *
291
+ * With the TIMED bit, a self-contained lag-comp stamp block precedes the ring:
292
+ *
293
+ * [varint k][uint32 newest][varint Δ]×(k−1)
294
+ * [uint16 rdNewest][varint Δrd]×(k−1) ← BOTH mode only
295
+ *
296
+ * One stamp per slot, because a packet carries k inputs sampled at k
297
+ * different instants. The anchor is absolute and the deltas never leave the
298
+ * packet, so — unlike the reliable channel's running baseline — no amount of
299
+ * loss or reordering can desync it, and an input recovered redundantly from a
300
+ * later packet still arrives with its own instant. Stamps are paired
301
+ * positionally with `decodeAll`'s oldest→newest yields; a `k` that disagrees
302
+ * with the decoded slot count means a malformed packet, so the stamps are
303
+ * dropped rather than misapplied (inputs still land, read live). */
304
+ decodeUnreliable(client: ClientPrivate, buffer: Buffer, modifiers: number): void {
277
305
  if (!client._inputDecoder) { return; }
306
+
307
+ const it: Iterator = { offset: 1 };
308
+ let stamps: number[] | undefined;
309
+ let renderDeltas: number[] | undefined;
310
+
311
+ if (modifiers & ProtocolModifier.TIMED) {
312
+ this.#resolveWireModes();
313
+ try {
314
+ const k = decode.number(buffer, it);
315
+ stamps = readSeriesDeltas(buffer, it, k, decode.uint32(buffer, it));
316
+ // BOTH mode trails the renderDelta series in the same shape, so each
317
+ // slot keeps the latency term it was actually sampled with.
318
+ if (this.#stampReckon && this.#stampRender) {
319
+ renderDeltas = readSeriesDeltas(buffer, it, k, decode.uint16(buffer, it));
320
+ }
321
+ } catch (e: any) {
322
+ debugAndPrintError(e);
323
+ return;
324
+ }
325
+ }
326
+
327
+ let i = 0;
278
328
  try {
279
- client._inputDecoder.decodeAll(buffer.subarray(1), (_inst, seq) => this.capture(client, 0, 0, seq));
329
+ const count = client._inputDecoder.decodeAll(buffer.subarray(it.offset), (_inst, seq) => {
330
+ // Positional pairing — `decodeAll` yields oldest→newest, the order the
331
+ // block was written in.
332
+ const slot = i++;
333
+ const stamp = stamps?.[slot] ?? 0;
334
+ let renderTime = 0, reckonTime = 0;
335
+ if (stamp > 0) {
336
+ if (this.#stampReckon && this.#stampRender) {
337
+ const rd = renderDeltas?.[slot] ?? 0;
338
+ reckonTime = stamp;
339
+ renderTime = stamp > rd ? stamp - rd : 0;
340
+ } else if (this.#stampReckon) {
341
+ reckonTime = stamp;
342
+ } else {
343
+ renderTime = stamp;
344
+ }
345
+ }
346
+ this.capture(client, renderTime, reckonTime, seq);
347
+ });
348
+ if (stamps !== undefined && count !== stamps.length) {
349
+ debugAndPrintError(new Error(
350
+ `@colyseus/core: unreliable input stamp block declared ${stamps.length} slots, decoded ${count}`
351
+ ));
352
+ }
280
353
  } catch (e: any) {
281
354
  debugAndPrintError(e);
282
355
  return;