@colyseus/core 0.18.25 → 0.18.26

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.
@@ -32,7 +32,9 @@ var import_nanoevents = require("./utils/nanoevents.cjs");
32
32
  var import_StandardSchema = require("./utils/StandardSchema.cjs");
33
33
  var import_DevMode = require("./utils/DevMode.cjs");
34
34
  var import_Debug = require("./Debug.cjs");
35
+ var import_Logger = require("./Logger.cjs");
35
36
  var import_RoomExceptions = require("./errors/RoomExceptions.cjs");
37
+ var import_Transport = require("./Transport.cjs");
36
38
  function toResponseError(e) {
37
39
  if (e instanceof Error) {
38
40
  const code = e.code;
@@ -203,6 +205,9 @@ var RoomMessages = class {
203
205
  }
204
206
  const handler = this.events.events[messageType]?.[0];
205
207
  if (handler === void 0) {
208
+ if (this.room.onUnhandledMessage !== void 0) {
209
+ this.#reportUnhandled(client, messageType, message);
210
+ }
206
211
  this.#replyToRequest(client, requestId, import_shared_types.ResponseStatus.ERROR, {
207
212
  name: "no_handler",
208
213
  message: `room "${this.room.roomName}" has no onMessage("${messageType}") handler to answer this request.`
@@ -270,15 +275,39 @@ var RoomMessages = class {
270
275
  client.enqueueRaw(import_Protocol.getMessageBytes[import_shared_types.Protocol.ROOM_RESPONSE](requestId, status, payload));
271
276
  }
272
277
  /** No handler for the type: error the client in dev, drop it in production. */
273
- #noHandler(client, messageType, _message) {
278
+ #noHandler(client, messageType, message) {
274
279
  const errorMessage = `room onMessage for "${messageType}" not registered.`;
275
- (0, import_Debug.debugMessage)(`${errorMessage} (roomId: ${this.room.roomId})`);
280
+ if (this.room.onUnhandledMessage !== void 0) {
281
+ this.#reportUnhandled(client, messageType, message);
282
+ } else {
283
+ import_Logger.logger.warn(`room "${this.room.roomName}" (roomId: ${this.room.roomId}): client '${client.sessionId}' sent "${messageType}", which has no onMessage() handler. ${import_DevMode.isDevMode ? "Replied with an INVALID_PAYLOAD error (outside devMode, the client is disconnected)." : "Disconnecting the client."}`);
284
+ }
276
285
  if (import_DevMode.isDevMode) {
277
286
  client.error(import_shared_types.ErrorCode.INVALID_PAYLOAD, errorMessage);
278
287
  } else {
288
+ client.state = import_Transport.ClientState.LEAVING;
279
289
  client.leave(import_shared_types.CloseCode.WITH_ERROR, errorMessage);
280
290
  }
281
291
  }
292
+ /** Call `room.onUnhandledMessage`. A throw or rejection goes to
293
+ * `onUncaughtException` when set, or is logged; it never stops the default. */
294
+ #reportUnhandled(client, messageType, message) {
295
+ const report = (e) => {
296
+ if (this.room.onUncaughtException !== void 0) {
297
+ this.room.onUncaughtException(new import_RoomExceptions.OnMessageException(e, e?.message, client, message, messageType), "onMessage");
298
+ } else {
299
+ (0, import_Debug.debugAndPrintError)(e);
300
+ }
301
+ };
302
+ try {
303
+ const result = this.room.onUnhandledMessage(client, messageType, message);
304
+ if (typeof result?.catch === "function") {
305
+ result.catch(report);
306
+ }
307
+ } catch (e) {
308
+ report(e);
309
+ }
310
+ }
282
311
  };
283
312
  // Annotate the CommonJS export names for ESM import in node:
284
313
  0 && (module.exports = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/RoomMessages.ts"],
4
- "sourcesContent": ["import { decode, type Iterator } from '@colyseus/schema';\nimport { unpack } from 'msgpackr';\n\nimport { CloseCode, ErrorCode, Protocol, ResponseStatus, type MessageContext } from '@colyseus/shared-types';\nimport { getMessageBytes } from './Protocol.ts';\nimport { createNanoEvents } from './utils/nanoevents.ts';\nimport { standardValidate, type StandardSchemaV1 } from './utils/StandardSchema.ts';\nimport { isDevMode } from './utils/DevMode.ts';\nimport { debugMessage, debugAndPrintError } from './Debug.ts';\nimport { OnMessageException } from './errors/RoomExceptions.ts';\nimport type { Client, ClientPrivate } from './Transport.ts';\nimport type { Room } from './Room.ts';\n\n/** Normalize a thrown value into the `{ name, message, code? }` shape echoed in a\n * ROOM_RESPONSE error reply. */\nfunction toResponseError(e: any): { name: string; message: string; code?: any } {\n if (e instanceof Error) {\n const code = (e as any).code;\n return (code !== undefined)\n ? { name: e.name, message: e.message, code }\n : { name: e.name, message: e.message };\n }\n return { name: \"Error\", message: String(e) };\n}\n\n// The handler's terminal action \u2014 NOT the wire ResponseStatus: `none` and\n// `resolved` both reply OK (plain return value vs `ctx.resolve(value)`), a\n// distinction a single OK can't carry; ERROR is never a handler decision\n// (thrown/no-handler). Projected onto ResponseStatus once, in onRequest's reply.\nconst OUTCOME_NONE = 0, OUTCOME_REJECTED = 1, OUTCOME_RESOLVED = 2;\n\n/**\n * Runtime {@link MessageContext} for a {@link Protocol.ROOM_REQUEST} dispatch.\n * `reject`/`resolve` record the decision on the ctx; `onRequest` reads it after the\n * handler returns, so a bare side-effecting call works as well as `return\n * ctx.reject(r)` (the branded return is compile-time only \u2014 the runtime return is\n * unused).\n */\nclass DispatchContext implements MessageContext {\n readonly id: number | undefined;\n _outcome = OUTCOME_NONE;\n _reason: any = undefined;\n _value: any = undefined;\n\n constructor(id: number | undefined) {\n this.id = id;\n }\n\n reject(reason?: any): any {\n this._outcome = OUTCOME_REJECTED;\n this._reason = reason;\n return undefined;\n }\n\n resolve(value?: any): any {\n this._outcome = OUTCOME_RESOLVED;\n this._value = value;\n return undefined;\n }\n}\n\n/** Shared no-op context for fire-and-forget `ROOM_DATA` (no reply channel, so\n * `reject`/`resolve` go nowhere). `id` is `undefined`; zero per-message allocation. */\nconst SEND_CONTEXT: MessageContext = Object.freeze({\n id: undefined,\n reject: () => undefined as any,\n resolve: () => undefined as any,\n});\n\n/**\n * Per-room message-routing layer, owned by {@link Room}. Owns the user-message\n * handler registry (`onMessage`/`onMessageBytes` + per-type validators) and\n * decodes/dispatches the user-message wire frames (ROOM_DATA / ROOM_REQUEST /\n * ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames\n * (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the\n * original dispatch order. Reads back into its Room only for `onUncaughtException`\n * (handler wrapping) and `roomId`/`roomName` (logging).\n *\n * @internal\n */\nexport class RoomMessages {\n private room: Room<any>;\n\n /** Handler registry (nanoevents). Public so Room can re-expose it as\n * `onMessageEvents` for @colyseus/playground introspection and\n * @colyseus/testing handler-swapping. */\n events = createNanoEvents();\n\n /** Per-type StandardSchema validators. Public for the same reason as\n * {@link events} (`onMessageValidators`). */\n // null-prototype: keyed by client-supplied message type (colyseus/colyseus#951)\n validators: { [type: string]: StandardSchemaV1 } = Object.create(null);\n\n constructor(room: Room<any>) {\n this.room = room;\n }\n\n /**\n * Register a handler (body of {@link Room.onMessage}). Wraps the callback via\n * the room's `onUncaughtException` when set, stores the validator, and returns\n * the unbind closure.\n */\n on(\n _messageType: '*' | string | number,\n _validationSchema: StandardSchemaV1 | ((...args: any[]) => void),\n _callback?: (...args: any[]) => void,\n ): () => void {\n const messageType = _messageType.toString();\n\n const validationSchema = (typeof _callback === 'function')\n ? _validationSchema as StandardSchemaV1\n : undefined;\n\n const callback = (validationSchema === undefined)\n ? _validationSchema as (...args: any[]) => void\n : _callback;\n\n const removeListener = this.events.on(messageType, (this.room.onUncaughtException !== undefined)\n ? this.#wrapMessageHandler(callback, _messageType)\n : callback);\n\n if (validationSchema !== undefined) {\n this.validators[messageType] = validationSchema;\n }\n\n // returns a method to unbind the callback\n return () => {\n removeListener();\n if (this.events.events[messageType].length === 0) {\n delete this.validators[messageType];\n }\n };\n }\n\n /**\n * Wrap a user message handler so a sync throw or async rejection is reported to\n * `room.onUncaughtException` as an {@link OnMessageException} carrying the correct\n * `client` / `payload` / `type`. The handler is invoked with a different argument\n * shape per dispatch path, so those fields are normalized here rather than relying\n * on positional forwarding (which silently mis-mapped `type` once a context arg was\n * added \u2014 see ROOM_DATA/ROOM_REQUEST passing `SEND_CONTEXT`/`ctx`):\n *\n * - typed handler (DATA / REQUEST / BYTES): `(client, payload, ctx?)` \u2192 `type` is\n * the registered `_messageType`.\n * - wildcard `'*'` handler: `(client, type, payload)` \u2192 `type` is\n * the received message type.\n *\n * The error is swallowed (never rethrown) so a faulty handler can't break dispatch;\n * the handler's return value (or promise) is passed through for the REQUEST path.\n */\n #wrapMessageHandler(callback: (...args: any[]) => any, registeredType: '*' | string | number) {\n const onError = this.room.onUncaughtException!.bind(this.room);\n const isWildcard = (registeredType === '*');\n\n return (...args: any[]): any => {\n const report = (e: Error) => onError(\n new OnMessageException(\n e, e.message,\n args[0], // client\n isWildcard ? args[2] : args[1], // payload\n isWildcard ? args[1] : registeredType, // type\n ),\n 'onMessage',\n );\n\n try {\n const result = callback(...args);\n return (typeof result?.catch === 'function') ? result.catch(report) : result;\n } catch (e: any) {\n report(e);\n }\n };\n }\n\n /** Dispatch a `ROOM_DATA` frame: decode type + msgpack payload, validate, emit. */\n onData(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n // custom message validation\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[messageType]) {\n this.#dispatch(this.events.events[messageType], client, message, SEND_CONTEXT);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /**\n * Call the handlers of a fire-and-forget frame. With no reply to carry an error\n * and nothing above this to catch one, a throw would reach the socket's event\n * listener and shut the process down, along with every other room in it. So\n * errors are logged, and the remaining handlers still run. Handlers wrapped for\n * `onUncaughtException` report their own errors and never throw here.\n */\n #dispatch(handlers: Array<(...args: any[]) => any>, ...args: any[]): void {\n for (let i = 0, length = handlers.length; i < length; i++) {\n try {\n const result = handlers[i](...args);\n if (typeof result?.catch === 'function') {\n result.catch(debugAndPrintError);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n }\n }\n }\n\n /** Dispatch a `ROOM_REQUEST` frame: same handlers as ROOM_DATA, but the client\n * opted into a reply, so echo the `requestId` with the outcome. The handler's\n * reply is decided by what it returns / does to `ctx`: `ctx.reject(reason)` \u2192\n * `REJECTED(reason)`, `ctx.resolve(value)` \u2192 `OK(value)`, a thrown error or\n * missing handler \u2192 `ERROR`, else `OK(return)`. */\n onRequest(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const requestId = decode.number(buffer, it);\n\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"request #%d: '%s' -> %j (roomId: %s)\", requestId, messageType, message, this.room.roomId);\n\n // custom message validation (shared with the ROOM_DATA path)\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n // Reply with an error so the caller's pending request resolves instead of timing out.\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n // Answered by the FIRST handler registered for the type (emit would discard\n // returns); wildcard handlers have no response contract, so they're ineligible.\n const handler = this.events.events[messageType as string]?.[0];\n\n if (handler === undefined) {\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {\n name: \"no_handler\",\n message: `room \"${this.room.roomName}\" has no onMessage(\"${messageType}\") handler to answer this request.`,\n });\n return;\n }\n\n const ctx = new DispatchContext(requestId);\n\n // Sync handlers reply in this tick with no promise machinery; only a thenable\n // return defers to the microtask queue. A sync throw (unwrapped handler) and an\n // async rejection both fall to the ERROR reply. With onUncaughtException set the\n // handler is wrapped (swallows its own errors), so it never throws / its promise\n // resolves undefined, and the error reports there.\n let response: any;\n try {\n response = handler(client, message, ctx);\n if (response !== null && typeof response === 'object' && typeof response.then === 'function') {\n response.then(\n (resolved: any) => this.#finalizeRequest(client, requestId, ctx, resolved),\n (e: any) => {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n },\n );\n return;\n }\n } catch (e: any) {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n this.#finalizeRequest(client, requestId, ctx, response);\n }\n\n /** Finalize a request: project the handler's outcome onto a ROOM_RESPONSE reply \u2014\n * `ctx.reject` \u2192 REJECTED(reason), `ctx.resolve(value)` \u2192 OK(value), else OK(return). */\n #finalizeRequest(client: Client, requestId: number, ctx: DispatchContext, response: any): void {\n if (ctx._outcome === OUTCOME_REJECTED) {\n this.#replyToRequest(client, requestId, ResponseStatus.REJECTED, ctx._reason);\n } else if (ctx._outcome === OUTCOME_RESOLVED) {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, ctx._value);\n } else {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, response);\n }\n }\n\n /** Dispatch a `ROOM_DATA_BYTES` frame: raw bytes routed to `_$b`-prefixed handlers. */\n onDataBytes(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message: any = buffer.subarray(it.offset, buffer.byteLength);\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n const bytesMessageType = `_$b${messageType}`;\n\n // custom message validation\n try {\n if (this.validators[bytesMessageType] !== undefined) {\n message = standardValidate(this.validators[bytesMessageType], message);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[bytesMessageType]) {\n this.#dispatch(this.events.events[bytesMessageType], client, message);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /** Emit a ROOM_RESPONSE reply immediately. */\n #replyToRequest(client: Client, requestId: number, status: ResponseStatus, payload?: any): void {\n debugMessage(\"response #%d: status=%d -> %j (roomId: %s)\", requestId, status, payload, this.room.roomId);\n client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));\n }\n\n /** No handler for the type: error the client in dev, drop it in production. */\n #noHandler(client: Client, messageType: string | number, _message: unknown): void {\n const errorMessage = `room onMessage for \"${messageType}\" not registered.`;\n debugMessage(`${errorMessage} (roomId: ${this.room.roomId})`);\n if (isDevMode) {\n client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);\n } else {\n client.leave(CloseCode.WITH_ERROR, errorMessage);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { decode, type Iterator } from '@colyseus/schema';\nimport { unpack } from 'msgpackr';\n\nimport { CloseCode, ErrorCode, Protocol, ResponseStatus, type MessageContext } from '@colyseus/shared-types';\nimport { getMessageBytes } from './Protocol.ts';\nimport { createNanoEvents } from './utils/nanoevents.ts';\nimport { standardValidate, type StandardSchemaV1 } from './utils/StandardSchema.ts';\nimport { isDevMode } from './utils/DevMode.ts';\nimport { debugMessage, debugAndPrintError } from './Debug.ts';\nimport { logger } from './Logger.ts';\nimport { OnMessageException } from './errors/RoomExceptions.ts';\nimport { ClientState, type Client, type ClientPrivate } from './Transport.ts';\nimport type { Room } from './Room.ts';\n\n/** Normalize a thrown value into the `{ name, message, code? }` shape echoed in a\n * ROOM_RESPONSE error reply. */\nfunction toResponseError(e: any): { name: string; message: string; code?: any } {\n if (e instanceof Error) {\n const code = (e as any).code;\n return (code !== undefined)\n ? { name: e.name, message: e.message, code }\n : { name: e.name, message: e.message };\n }\n return { name: \"Error\", message: String(e) };\n}\n\n// The handler's terminal action \u2014 NOT the wire ResponseStatus: `none` and\n// `resolved` both reply OK (plain return value vs `ctx.resolve(value)`), a\n// distinction a single OK can't carry; ERROR is never a handler decision\n// (thrown/no-handler). Projected onto ResponseStatus once, in onRequest's reply.\nconst OUTCOME_NONE = 0, OUTCOME_REJECTED = 1, OUTCOME_RESOLVED = 2;\n\n/**\n * Runtime {@link MessageContext} for a {@link Protocol.ROOM_REQUEST} dispatch.\n * `reject`/`resolve` record the decision on the ctx; `onRequest` reads it after the\n * handler returns, so a bare side-effecting call works as well as `return\n * ctx.reject(r)` (the branded return is compile-time only \u2014 the runtime return is\n * unused).\n */\nclass DispatchContext implements MessageContext {\n readonly id: number | undefined;\n _outcome = OUTCOME_NONE;\n _reason: any = undefined;\n _value: any = undefined;\n\n constructor(id: number | undefined) {\n this.id = id;\n }\n\n reject(reason?: any): any {\n this._outcome = OUTCOME_REJECTED;\n this._reason = reason;\n return undefined;\n }\n\n resolve(value?: any): any {\n this._outcome = OUTCOME_RESOLVED;\n this._value = value;\n return undefined;\n }\n}\n\n/** Shared no-op context for fire-and-forget `ROOM_DATA` (no reply channel, so\n * `reject`/`resolve` go nowhere). `id` is `undefined`; zero per-message allocation. */\nconst SEND_CONTEXT: MessageContext = Object.freeze({\n id: undefined,\n reject: () => undefined as any,\n resolve: () => undefined as any,\n});\n\n/**\n * Per-room message-routing layer, owned by {@link Room}. Owns the user-message\n * handler registry (`onMessage`/`onMessageBytes` + per-type validators) and\n * decodes/dispatches the user-message wire frames (ROOM_DATA / ROOM_REQUEST /\n * ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames\n * (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the\n * original dispatch order. Reads back into its Room only for `onUncaughtException`\n * (handler wrapping), `onUnhandledMessage` and `roomId`/`roomName` (logging).\n *\n * @internal\n */\nexport class RoomMessages {\n private room: Room<any>;\n\n /** Handler registry (nanoevents). Public so Room can re-expose it as\n * `onMessageEvents` for @colyseus/playground introspection and\n * @colyseus/testing handler-swapping. */\n events = createNanoEvents();\n\n /** Per-type StandardSchema validators. Public for the same reason as\n * {@link events} (`onMessageValidators`). */\n // null-prototype: keyed by client-supplied message type (colyseus/colyseus#951)\n validators: { [type: string]: StandardSchemaV1 } = Object.create(null);\n\n constructor(room: Room<any>) {\n this.room = room;\n }\n\n /**\n * Register a handler (body of {@link Room.onMessage}). Wraps the callback via\n * the room's `onUncaughtException` when set, stores the validator, and returns\n * the unbind closure.\n */\n on(\n _messageType: '*' | string | number,\n _validationSchema: StandardSchemaV1 | ((...args: any[]) => void),\n _callback?: (...args: any[]) => void,\n ): () => void {\n const messageType = _messageType.toString();\n\n const validationSchema = (typeof _callback === 'function')\n ? _validationSchema as StandardSchemaV1\n : undefined;\n\n const callback = (validationSchema === undefined)\n ? _validationSchema as (...args: any[]) => void\n : _callback;\n\n const removeListener = this.events.on(messageType, (this.room.onUncaughtException !== undefined)\n ? this.#wrapMessageHandler(callback, _messageType)\n : callback);\n\n if (validationSchema !== undefined) {\n this.validators[messageType] = validationSchema;\n }\n\n // returns a method to unbind the callback\n return () => {\n removeListener();\n if (this.events.events[messageType].length === 0) {\n delete this.validators[messageType];\n }\n };\n }\n\n /**\n * Wrap a user message handler so a sync throw or async rejection is reported to\n * `room.onUncaughtException` as an {@link OnMessageException} carrying the correct\n * `client` / `payload` / `type`. The handler is invoked with a different argument\n * shape per dispatch path, so those fields are normalized here rather than relying\n * on positional forwarding (which silently mis-mapped `type` once a context arg was\n * added \u2014 see ROOM_DATA/ROOM_REQUEST passing `SEND_CONTEXT`/`ctx`):\n *\n * - typed handler (DATA / REQUEST / BYTES): `(client, payload, ctx?)` \u2192 `type` is\n * the registered `_messageType`.\n * - wildcard `'*'` handler: `(client, type, payload)` \u2192 `type` is\n * the received message type.\n *\n * The error is swallowed (never rethrown) so a faulty handler can't break dispatch;\n * the handler's return value (or promise) is passed through for the REQUEST path.\n */\n #wrapMessageHandler(callback: (...args: any[]) => any, registeredType: '*' | string | number) {\n const onError = this.room.onUncaughtException!.bind(this.room);\n const isWildcard = (registeredType === '*');\n\n return (...args: any[]): any => {\n const report = (e: Error) => onError(\n new OnMessageException(\n e, e.message,\n args[0], // client\n isWildcard ? args[2] : args[1], // payload\n isWildcard ? args[1] : registeredType, // type\n ),\n 'onMessage',\n );\n\n try {\n const result = callback(...args);\n return (typeof result?.catch === 'function') ? result.catch(report) : result;\n } catch (e: any) {\n report(e);\n }\n };\n }\n\n /** Dispatch a `ROOM_DATA` frame: decode type + msgpack payload, validate, emit. */\n onData(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n // custom message validation\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[messageType]) {\n this.#dispatch(this.events.events[messageType], client, message, SEND_CONTEXT);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /**\n * Call the handlers of a fire-and-forget frame. With no reply to carry an error\n * and nothing above this to catch one, a throw would reach the socket's event\n * listener and shut the process down, along with every other room in it. So\n * errors are logged, and the remaining handlers still run. Handlers wrapped for\n * `onUncaughtException` report their own errors and never throw here.\n */\n #dispatch(handlers: Array<(...args: any[]) => any>, ...args: any[]): void {\n for (let i = 0, length = handlers.length; i < length; i++) {\n try {\n const result = handlers[i](...args);\n if (typeof result?.catch === 'function') {\n result.catch(debugAndPrintError);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n }\n }\n }\n\n /** Dispatch a `ROOM_REQUEST` frame: same handlers as ROOM_DATA, but the client\n * opted into a reply, so echo the `requestId` with the outcome. The handler's\n * reply is decided by what it returns / does to `ctx`: `ctx.reject(reason)` \u2192\n * `REJECTED(reason)`, `ctx.resolve(value)` \u2192 `OK(value)`, a thrown error or\n * missing handler \u2192 `ERROR`, else `OK(return)`. */\n onRequest(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const requestId = decode.number(buffer, it);\n\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"request #%d: '%s' -> %j (roomId: %s)\", requestId, messageType, message, this.room.roomId);\n\n // custom message validation (shared with the ROOM_DATA path)\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n // Reply with an error so the caller's pending request resolves instead of timing out.\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n // Answered by the FIRST handler registered for the type (emit would discard\n // returns); wildcard handlers have no response contract, so they're ineligible.\n const handler = this.events.events[messageType as string]?.[0];\n\n if (handler === undefined) {\n // no warning without the hook: the client already gets an explicit error\n if (this.room.onUnhandledMessage !== undefined) {\n this.#reportUnhandled(client, messageType, message);\n }\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {\n name: \"no_handler\",\n message: `room \"${this.room.roomName}\" has no onMessage(\"${messageType}\") handler to answer this request.`,\n });\n return;\n }\n\n const ctx = new DispatchContext(requestId);\n\n // Sync handlers reply in this tick with no promise machinery; only a thenable\n // return defers to the microtask queue. A sync throw (unwrapped handler) and an\n // async rejection both fall to the ERROR reply. With onUncaughtException set the\n // handler is wrapped (swallows its own errors), so it never throws / its promise\n // resolves undefined, and the error reports there.\n let response: any;\n try {\n response = handler(client, message, ctx);\n if (response !== null && typeof response === 'object' && typeof response.then === 'function') {\n response.then(\n (resolved: any) => this.#finalizeRequest(client, requestId, ctx, resolved),\n (e: any) => {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n },\n );\n return;\n }\n } catch (e: any) {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n this.#finalizeRequest(client, requestId, ctx, response);\n }\n\n /** Finalize a request: project the handler's outcome onto a ROOM_RESPONSE reply \u2014\n * `ctx.reject` \u2192 REJECTED(reason), `ctx.resolve(value)` \u2192 OK(value), else OK(return). */\n #finalizeRequest(client: Client, requestId: number, ctx: DispatchContext, response: any): void {\n if (ctx._outcome === OUTCOME_REJECTED) {\n this.#replyToRequest(client, requestId, ResponseStatus.REJECTED, ctx._reason);\n } else if (ctx._outcome === OUTCOME_RESOLVED) {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, ctx._value);\n } else {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, response);\n }\n }\n\n /** Dispatch a `ROOM_DATA_BYTES` frame: raw bytes routed to `_$b`-prefixed handlers. */\n onDataBytes(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message: any = buffer.subarray(it.offset, buffer.byteLength);\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n const bytesMessageType = `_$b${messageType}`;\n\n // custom message validation\n try {\n if (this.validators[bytesMessageType] !== undefined) {\n message = standardValidate(this.validators[bytesMessageType], message);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[bytesMessageType]) {\n this.#dispatch(this.events.events[bytesMessageType], client, message);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /** Emit a ROOM_RESPONSE reply immediately. */\n #replyToRequest(client: Client, requestId: number, status: ResponseStatus, payload?: any): void {\n debugMessage(\"response #%d: status=%d -> %j (roomId: %s)\", requestId, status, payload, this.room.roomId);\n client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));\n }\n\n /** No handler for the type: error the client in dev, drop it in production. */\n #noHandler(client: Client, messageType: string | number, message: unknown): void {\n const errorMessage = `room onMessage for \"${messageType}\" not registered.`;\n\n if (this.room.onUnhandledMessage !== undefined) {\n this.#reportUnhandled(client, messageType, message);\n\n } else {\n logger.warn(`room \"${this.room.roomName}\" (roomId: ${this.room.roomId}): client '${client.sessionId}' sent \"${messageType}\", which has no onMessage() handler. ${(isDevMode)\n ? \"Replied with an INVALID_PAYLOAD error (outside devMode, the client is disconnected).\"\n : \"Disconnecting the client.\"}`);\n }\n\n if (isDevMode) {\n client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);\n } else {\n // frames already received still arrive after leave(); Room#_onMessage skips them\n client.state = ClientState.LEAVING;\n client.leave(CloseCode.WITH_ERROR, errorMessage);\n }\n }\n\n /** Call `room.onUnhandledMessage`. A throw or rejection goes to\n * `onUncaughtException` when set, or is logged; it never stops the default. */\n #reportUnhandled(client: Client, messageType: string | number, message: unknown): void {\n const report = (e: any) => {\n if (this.room.onUncaughtException !== undefined) {\n this.room.onUncaughtException(new OnMessageException<Room<any>, any>(e, e?.message, client, message, messageType), 'onMessage');\n } else {\n debugAndPrintError(e);\n }\n };\n\n try {\n const result = this.room.onUnhandledMessage!(client, messageType, message) as any;\n if (typeof result?.catch === 'function') {\n result.catch(report);\n }\n } catch (e: any) {\n report(e);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { type Iterator } from '@colyseus/schema';
2
2
  import { type StandardSchemaV1 } from './utils/StandardSchema.ts';
3
- import type { Client, ClientPrivate } from './Transport.ts';
3
+ import { type Client, type ClientPrivate } from './Transport.ts';
4
4
  import type { Room } from './Room.ts';
5
5
  /**
6
6
  * Per-room message-routing layer, owned by {@link Room}. Owns the user-message
@@ -9,7 +9,7 @@ import type { Room } from './Room.ts';
9
9
  * ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames
10
10
  * (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the
11
11
  * original dispatch order. Reads back into its Room only for `onUncaughtException`
12
- * (handler wrapping) and `roomId`/`roomName` (logging).
12
+ * (handler wrapping), `onUnhandledMessage` and `roomId`/`roomName` (logging).
13
13
  *
14
14
  * @internal
15
15
  */
@@ -7,7 +7,9 @@ import { createNanoEvents } from "./utils/nanoevents.mjs";
7
7
  import { standardValidate } from "./utils/StandardSchema.mjs";
8
8
  import { isDevMode } from "./utils/DevMode.mjs";
9
9
  import { debugMessage, debugAndPrintError } from "./Debug.mjs";
10
+ import { logger } from "./Logger.mjs";
10
11
  import { OnMessageException } from "./errors/RoomExceptions.mjs";
12
+ import { ClientState } from "./Transport.mjs";
11
13
  function toResponseError(e) {
12
14
  if (e instanceof Error) {
13
15
  const code = e.code;
@@ -178,6 +180,9 @@ var RoomMessages = class {
178
180
  }
179
181
  const handler = this.events.events[messageType]?.[0];
180
182
  if (handler === void 0) {
183
+ if (this.room.onUnhandledMessage !== void 0) {
184
+ this.#reportUnhandled(client, messageType, message);
185
+ }
181
186
  this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {
182
187
  name: "no_handler",
183
188
  message: `room "${this.room.roomName}" has no onMessage("${messageType}") handler to answer this request.`
@@ -245,15 +250,39 @@ var RoomMessages = class {
245
250
  client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));
246
251
  }
247
252
  /** No handler for the type: error the client in dev, drop it in production. */
248
- #noHandler(client, messageType, _message) {
253
+ #noHandler(client, messageType, message) {
249
254
  const errorMessage = `room onMessage for "${messageType}" not registered.`;
250
- debugMessage(`${errorMessage} (roomId: ${this.room.roomId})`);
255
+ if (this.room.onUnhandledMessage !== void 0) {
256
+ this.#reportUnhandled(client, messageType, message);
257
+ } else {
258
+ logger.warn(`room "${this.room.roomName}" (roomId: ${this.room.roomId}): client '${client.sessionId}' sent "${messageType}", which has no onMessage() handler. ${isDevMode ? "Replied with an INVALID_PAYLOAD error (outside devMode, the client is disconnected)." : "Disconnecting the client."}`);
259
+ }
251
260
  if (isDevMode) {
252
261
  client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
253
262
  } else {
263
+ client.state = ClientState.LEAVING;
254
264
  client.leave(CloseCode.WITH_ERROR, errorMessage);
255
265
  }
256
266
  }
267
+ /** Call `room.onUnhandledMessage`. A throw or rejection goes to
268
+ * `onUncaughtException` when set, or is logged; it never stops the default. */
269
+ #reportUnhandled(client, messageType, message) {
270
+ const report = (e) => {
271
+ if (this.room.onUncaughtException !== void 0) {
272
+ this.room.onUncaughtException(new OnMessageException(e, e?.message, client, message, messageType), "onMessage");
273
+ } else {
274
+ debugAndPrintError(e);
275
+ }
276
+ };
277
+ try {
278
+ const result = this.room.onUnhandledMessage(client, messageType, message);
279
+ if (typeof result?.catch === "function") {
280
+ result.catch(report);
281
+ }
282
+ } catch (e) {
283
+ report(e);
284
+ }
285
+ }
257
286
  };
258
287
  export {
259
288
  RoomMessages
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/RoomMessages.ts"],
4
- "sourcesContent": ["import { decode, type Iterator } from '@colyseus/schema';\nimport { unpack } from 'msgpackr';\n\nimport { CloseCode, ErrorCode, Protocol, ResponseStatus, type MessageContext } from '@colyseus/shared-types';\nimport { getMessageBytes } from './Protocol.ts';\nimport { createNanoEvents } from './utils/nanoevents.ts';\nimport { standardValidate, type StandardSchemaV1 } from './utils/StandardSchema.ts';\nimport { isDevMode } from './utils/DevMode.ts';\nimport { debugMessage, debugAndPrintError } from './Debug.ts';\nimport { OnMessageException } from './errors/RoomExceptions.ts';\nimport type { Client, ClientPrivate } from './Transport.ts';\nimport type { Room } from './Room.ts';\n\n/** Normalize a thrown value into the `{ name, message, code? }` shape echoed in a\n * ROOM_RESPONSE error reply. */\nfunction toResponseError(e: any): { name: string; message: string; code?: any } {\n if (e instanceof Error) {\n const code = (e as any).code;\n return (code !== undefined)\n ? { name: e.name, message: e.message, code }\n : { name: e.name, message: e.message };\n }\n return { name: \"Error\", message: String(e) };\n}\n\n// The handler's terminal action \u2014 NOT the wire ResponseStatus: `none` and\n// `resolved` both reply OK (plain return value vs `ctx.resolve(value)`), a\n// distinction a single OK can't carry; ERROR is never a handler decision\n// (thrown/no-handler). Projected onto ResponseStatus once, in onRequest's reply.\nconst OUTCOME_NONE = 0, OUTCOME_REJECTED = 1, OUTCOME_RESOLVED = 2;\n\n/**\n * Runtime {@link MessageContext} for a {@link Protocol.ROOM_REQUEST} dispatch.\n * `reject`/`resolve` record the decision on the ctx; `onRequest` reads it after the\n * handler returns, so a bare side-effecting call works as well as `return\n * ctx.reject(r)` (the branded return is compile-time only \u2014 the runtime return is\n * unused).\n */\nclass DispatchContext implements MessageContext {\n readonly id: number | undefined;\n _outcome = OUTCOME_NONE;\n _reason: any = undefined;\n _value: any = undefined;\n\n constructor(id: number | undefined) {\n this.id = id;\n }\n\n reject(reason?: any): any {\n this._outcome = OUTCOME_REJECTED;\n this._reason = reason;\n return undefined;\n }\n\n resolve(value?: any): any {\n this._outcome = OUTCOME_RESOLVED;\n this._value = value;\n return undefined;\n }\n}\n\n/** Shared no-op context for fire-and-forget `ROOM_DATA` (no reply channel, so\n * `reject`/`resolve` go nowhere). `id` is `undefined`; zero per-message allocation. */\nconst SEND_CONTEXT: MessageContext = Object.freeze({\n id: undefined,\n reject: () => undefined as any,\n resolve: () => undefined as any,\n});\n\n/**\n * Per-room message-routing layer, owned by {@link Room}. Owns the user-message\n * handler registry (`onMessage`/`onMessageBytes` + per-type validators) and\n * decodes/dispatches the user-message wire frames (ROOM_DATA / ROOM_REQUEST /\n * ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames\n * (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the\n * original dispatch order. Reads back into its Room only for `onUncaughtException`\n * (handler wrapping) and `roomId`/`roomName` (logging).\n *\n * @internal\n */\nexport class RoomMessages {\n private room: Room<any>;\n\n /** Handler registry (nanoevents). Public so Room can re-expose it as\n * `onMessageEvents` for @colyseus/playground introspection and\n * @colyseus/testing handler-swapping. */\n events = createNanoEvents();\n\n /** Per-type StandardSchema validators. Public for the same reason as\n * {@link events} (`onMessageValidators`). */\n // null-prototype: keyed by client-supplied message type (colyseus/colyseus#951)\n validators: { [type: string]: StandardSchemaV1 } = Object.create(null);\n\n constructor(room: Room<any>) {\n this.room = room;\n }\n\n /**\n * Register a handler (body of {@link Room.onMessage}). Wraps the callback via\n * the room's `onUncaughtException` when set, stores the validator, and returns\n * the unbind closure.\n */\n on(\n _messageType: '*' | string | number,\n _validationSchema: StandardSchemaV1 | ((...args: any[]) => void),\n _callback?: (...args: any[]) => void,\n ): () => void {\n const messageType = _messageType.toString();\n\n const validationSchema = (typeof _callback === 'function')\n ? _validationSchema as StandardSchemaV1\n : undefined;\n\n const callback = (validationSchema === undefined)\n ? _validationSchema as (...args: any[]) => void\n : _callback;\n\n const removeListener = this.events.on(messageType, (this.room.onUncaughtException !== undefined)\n ? this.#wrapMessageHandler(callback, _messageType)\n : callback);\n\n if (validationSchema !== undefined) {\n this.validators[messageType] = validationSchema;\n }\n\n // returns a method to unbind the callback\n return () => {\n removeListener();\n if (this.events.events[messageType].length === 0) {\n delete this.validators[messageType];\n }\n };\n }\n\n /**\n * Wrap a user message handler so a sync throw or async rejection is reported to\n * `room.onUncaughtException` as an {@link OnMessageException} carrying the correct\n * `client` / `payload` / `type`. The handler is invoked with a different argument\n * shape per dispatch path, so those fields are normalized here rather than relying\n * on positional forwarding (which silently mis-mapped `type` once a context arg was\n * added \u2014 see ROOM_DATA/ROOM_REQUEST passing `SEND_CONTEXT`/`ctx`):\n *\n * - typed handler (DATA / REQUEST / BYTES): `(client, payload, ctx?)` \u2192 `type` is\n * the registered `_messageType`.\n * - wildcard `'*'` handler: `(client, type, payload)` \u2192 `type` is\n * the received message type.\n *\n * The error is swallowed (never rethrown) so a faulty handler can't break dispatch;\n * the handler's return value (or promise) is passed through for the REQUEST path.\n */\n #wrapMessageHandler(callback: (...args: any[]) => any, registeredType: '*' | string | number) {\n const onError = this.room.onUncaughtException!.bind(this.room);\n const isWildcard = (registeredType === '*');\n\n return (...args: any[]): any => {\n const report = (e: Error) => onError(\n new OnMessageException(\n e, e.message,\n args[0], // client\n isWildcard ? args[2] : args[1], // payload\n isWildcard ? args[1] : registeredType, // type\n ),\n 'onMessage',\n );\n\n try {\n const result = callback(...args);\n return (typeof result?.catch === 'function') ? result.catch(report) : result;\n } catch (e: any) {\n report(e);\n }\n };\n }\n\n /** Dispatch a `ROOM_DATA` frame: decode type + msgpack payload, validate, emit. */\n onData(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n // custom message validation\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[messageType]) {\n this.#dispatch(this.events.events[messageType], client, message, SEND_CONTEXT);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /**\n * Call the handlers of a fire-and-forget frame. With no reply to carry an error\n * and nothing above this to catch one, a throw would reach the socket's event\n * listener and shut the process down, along with every other room in it. So\n * errors are logged, and the remaining handlers still run. Handlers wrapped for\n * `onUncaughtException` report their own errors and never throw here.\n */\n #dispatch(handlers: Array<(...args: any[]) => any>, ...args: any[]): void {\n for (let i = 0, length = handlers.length; i < length; i++) {\n try {\n const result = handlers[i](...args);\n if (typeof result?.catch === 'function') {\n result.catch(debugAndPrintError);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n }\n }\n }\n\n /** Dispatch a `ROOM_REQUEST` frame: same handlers as ROOM_DATA, but the client\n * opted into a reply, so echo the `requestId` with the outcome. The handler's\n * reply is decided by what it returns / does to `ctx`: `ctx.reject(reason)` \u2192\n * `REJECTED(reason)`, `ctx.resolve(value)` \u2192 `OK(value)`, a thrown error or\n * missing handler \u2192 `ERROR`, else `OK(return)`. */\n onRequest(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const requestId = decode.number(buffer, it);\n\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"request #%d: '%s' -> %j (roomId: %s)\", requestId, messageType, message, this.room.roomId);\n\n // custom message validation (shared with the ROOM_DATA path)\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n // Reply with an error so the caller's pending request resolves instead of timing out.\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n // Answered by the FIRST handler registered for the type (emit would discard\n // returns); wildcard handlers have no response contract, so they're ineligible.\n const handler = this.events.events[messageType as string]?.[0];\n\n if (handler === undefined) {\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {\n name: \"no_handler\",\n message: `room \"${this.room.roomName}\" has no onMessage(\"${messageType}\") handler to answer this request.`,\n });\n return;\n }\n\n const ctx = new DispatchContext(requestId);\n\n // Sync handlers reply in this tick with no promise machinery; only a thenable\n // return defers to the microtask queue. A sync throw (unwrapped handler) and an\n // async rejection both fall to the ERROR reply. With onUncaughtException set the\n // handler is wrapped (swallows its own errors), so it never throws / its promise\n // resolves undefined, and the error reports there.\n let response: any;\n try {\n response = handler(client, message, ctx);\n if (response !== null && typeof response === 'object' && typeof response.then === 'function') {\n response.then(\n (resolved: any) => this.#finalizeRequest(client, requestId, ctx, resolved),\n (e: any) => {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n },\n );\n return;\n }\n } catch (e: any) {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n this.#finalizeRequest(client, requestId, ctx, response);\n }\n\n /** Finalize a request: project the handler's outcome onto a ROOM_RESPONSE reply \u2014\n * `ctx.reject` \u2192 REJECTED(reason), `ctx.resolve(value)` \u2192 OK(value), else OK(return). */\n #finalizeRequest(client: Client, requestId: number, ctx: DispatchContext, response: any): void {\n if (ctx._outcome === OUTCOME_REJECTED) {\n this.#replyToRequest(client, requestId, ResponseStatus.REJECTED, ctx._reason);\n } else if (ctx._outcome === OUTCOME_RESOLVED) {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, ctx._value);\n } else {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, response);\n }\n }\n\n /** Dispatch a `ROOM_DATA_BYTES` frame: raw bytes routed to `_$b`-prefixed handlers. */\n onDataBytes(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message: any = buffer.subarray(it.offset, buffer.byteLength);\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n const bytesMessageType = `_$b${messageType}`;\n\n // custom message validation\n try {\n if (this.validators[bytesMessageType] !== undefined) {\n message = standardValidate(this.validators[bytesMessageType], message);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[bytesMessageType]) {\n this.#dispatch(this.events.events[bytesMessageType], client, message);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /** Emit a ROOM_RESPONSE reply immediately. */\n #replyToRequest(client: Client, requestId: number, status: ResponseStatus, payload?: any): void {\n debugMessage(\"response #%d: status=%d -> %j (roomId: %s)\", requestId, status, payload, this.room.roomId);\n client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));\n }\n\n /** No handler for the type: error the client in dev, drop it in production. */\n #noHandler(client: Client, messageType: string | number, _message: unknown): void {\n const errorMessage = `room onMessage for \"${messageType}\" not registered.`;\n debugMessage(`${errorMessage} (roomId: ${this.room.roomId})`);\n if (isDevMode) {\n client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);\n } else {\n client.leave(CloseCode.WITH_ERROR, errorMessage);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { decode, type Iterator } from '@colyseus/schema';\nimport { unpack } from 'msgpackr';\n\nimport { CloseCode, ErrorCode, Protocol, ResponseStatus, type MessageContext } from '@colyseus/shared-types';\nimport { getMessageBytes } from './Protocol.ts';\nimport { createNanoEvents } from './utils/nanoevents.ts';\nimport { standardValidate, type StandardSchemaV1 } from './utils/StandardSchema.ts';\nimport { isDevMode } from './utils/DevMode.ts';\nimport { debugMessage, debugAndPrintError } from './Debug.ts';\nimport { logger } from './Logger.ts';\nimport { OnMessageException } from './errors/RoomExceptions.ts';\nimport { ClientState, type Client, type ClientPrivate } from './Transport.ts';\nimport type { Room } from './Room.ts';\n\n/** Normalize a thrown value into the `{ name, message, code? }` shape echoed in a\n * ROOM_RESPONSE error reply. */\nfunction toResponseError(e: any): { name: string; message: string; code?: any } {\n if (e instanceof Error) {\n const code = (e as any).code;\n return (code !== undefined)\n ? { name: e.name, message: e.message, code }\n : { name: e.name, message: e.message };\n }\n return { name: \"Error\", message: String(e) };\n}\n\n// The handler's terminal action \u2014 NOT the wire ResponseStatus: `none` and\n// `resolved` both reply OK (plain return value vs `ctx.resolve(value)`), a\n// distinction a single OK can't carry; ERROR is never a handler decision\n// (thrown/no-handler). Projected onto ResponseStatus once, in onRequest's reply.\nconst OUTCOME_NONE = 0, OUTCOME_REJECTED = 1, OUTCOME_RESOLVED = 2;\n\n/**\n * Runtime {@link MessageContext} for a {@link Protocol.ROOM_REQUEST} dispatch.\n * `reject`/`resolve` record the decision on the ctx; `onRequest` reads it after the\n * handler returns, so a bare side-effecting call works as well as `return\n * ctx.reject(r)` (the branded return is compile-time only \u2014 the runtime return is\n * unused).\n */\nclass DispatchContext implements MessageContext {\n readonly id: number | undefined;\n _outcome = OUTCOME_NONE;\n _reason: any = undefined;\n _value: any = undefined;\n\n constructor(id: number | undefined) {\n this.id = id;\n }\n\n reject(reason?: any): any {\n this._outcome = OUTCOME_REJECTED;\n this._reason = reason;\n return undefined;\n }\n\n resolve(value?: any): any {\n this._outcome = OUTCOME_RESOLVED;\n this._value = value;\n return undefined;\n }\n}\n\n/** Shared no-op context for fire-and-forget `ROOM_DATA` (no reply channel, so\n * `reject`/`resolve` go nowhere). `id` is `undefined`; zero per-message allocation. */\nconst SEND_CONTEXT: MessageContext = Object.freeze({\n id: undefined,\n reject: () => undefined as any,\n resolve: () => undefined as any,\n});\n\n/**\n * Per-room message-routing layer, owned by {@link Room}. Owns the user-message\n * handler registry (`onMessage`/`onMessageBytes` + per-type validators) and\n * decodes/dispatches the user-message wire frames (ROOM_DATA / ROOM_REQUEST /\n * ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames\n * (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the\n * original dispatch order. Reads back into its Room only for `onUncaughtException`\n * (handler wrapping), `onUnhandledMessage` and `roomId`/`roomName` (logging).\n *\n * @internal\n */\nexport class RoomMessages {\n private room: Room<any>;\n\n /** Handler registry (nanoevents). Public so Room can re-expose it as\n * `onMessageEvents` for @colyseus/playground introspection and\n * @colyseus/testing handler-swapping. */\n events = createNanoEvents();\n\n /** Per-type StandardSchema validators. Public for the same reason as\n * {@link events} (`onMessageValidators`). */\n // null-prototype: keyed by client-supplied message type (colyseus/colyseus#951)\n validators: { [type: string]: StandardSchemaV1 } = Object.create(null);\n\n constructor(room: Room<any>) {\n this.room = room;\n }\n\n /**\n * Register a handler (body of {@link Room.onMessage}). Wraps the callback via\n * the room's `onUncaughtException` when set, stores the validator, and returns\n * the unbind closure.\n */\n on(\n _messageType: '*' | string | number,\n _validationSchema: StandardSchemaV1 | ((...args: any[]) => void),\n _callback?: (...args: any[]) => void,\n ): () => void {\n const messageType = _messageType.toString();\n\n const validationSchema = (typeof _callback === 'function')\n ? _validationSchema as StandardSchemaV1\n : undefined;\n\n const callback = (validationSchema === undefined)\n ? _validationSchema as (...args: any[]) => void\n : _callback;\n\n const removeListener = this.events.on(messageType, (this.room.onUncaughtException !== undefined)\n ? this.#wrapMessageHandler(callback, _messageType)\n : callback);\n\n if (validationSchema !== undefined) {\n this.validators[messageType] = validationSchema;\n }\n\n // returns a method to unbind the callback\n return () => {\n removeListener();\n if (this.events.events[messageType].length === 0) {\n delete this.validators[messageType];\n }\n };\n }\n\n /**\n * Wrap a user message handler so a sync throw or async rejection is reported to\n * `room.onUncaughtException` as an {@link OnMessageException} carrying the correct\n * `client` / `payload` / `type`. The handler is invoked with a different argument\n * shape per dispatch path, so those fields are normalized here rather than relying\n * on positional forwarding (which silently mis-mapped `type` once a context arg was\n * added \u2014 see ROOM_DATA/ROOM_REQUEST passing `SEND_CONTEXT`/`ctx`):\n *\n * - typed handler (DATA / REQUEST / BYTES): `(client, payload, ctx?)` \u2192 `type` is\n * the registered `_messageType`.\n * - wildcard `'*'` handler: `(client, type, payload)` \u2192 `type` is\n * the received message type.\n *\n * The error is swallowed (never rethrown) so a faulty handler can't break dispatch;\n * the handler's return value (or promise) is passed through for the REQUEST path.\n */\n #wrapMessageHandler(callback: (...args: any[]) => any, registeredType: '*' | string | number) {\n const onError = this.room.onUncaughtException!.bind(this.room);\n const isWildcard = (registeredType === '*');\n\n return (...args: any[]): any => {\n const report = (e: Error) => onError(\n new OnMessageException(\n e, e.message,\n args[0], // client\n isWildcard ? args[2] : args[1], // payload\n isWildcard ? args[1] : registeredType, // type\n ),\n 'onMessage',\n );\n\n try {\n const result = callback(...args);\n return (typeof result?.catch === 'function') ? result.catch(report) : result;\n } catch (e: any) {\n report(e);\n }\n };\n }\n\n /** Dispatch a `ROOM_DATA` frame: decode type + msgpack payload, validate, emit. */\n onData(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n // custom message validation\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[messageType]) {\n this.#dispatch(this.events.events[messageType], client, message, SEND_CONTEXT);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /**\n * Call the handlers of a fire-and-forget frame. With no reply to carry an error\n * and nothing above this to catch one, a throw would reach the socket's event\n * listener and shut the process down, along with every other room in it. So\n * errors are logged, and the remaining handlers still run. Handlers wrapped for\n * `onUncaughtException` report their own errors and never throw here.\n */\n #dispatch(handlers: Array<(...args: any[]) => any>, ...args: any[]): void {\n for (let i = 0, length = handlers.length; i < length; i++) {\n try {\n const result = handlers[i](...args);\n if (typeof result?.catch === 'function') {\n result.catch(debugAndPrintError);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n }\n }\n }\n\n /** Dispatch a `ROOM_REQUEST` frame: same handlers as ROOM_DATA, but the client\n * opted into a reply, so echo the `requestId` with the outcome. The handler's\n * reply is decided by what it returns / does to `ctx`: `ctx.reject(reason)` \u2192\n * `REJECTED(reason)`, `ctx.resolve(value)` \u2192 `OK(value)`, a thrown error or\n * missing handler \u2192 `ERROR`, else `OK(return)`. */\n onRequest(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const requestId = decode.number(buffer, it);\n\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"request #%d: '%s' -> %j (roomId: %s)\", requestId, messageType, message, this.room.roomId);\n\n // custom message validation (shared with the ROOM_DATA path)\n if (this.validators[messageType] !== undefined) {\n message = standardValidate(this.validators[messageType], message);\n }\n\n } catch (e: any) {\n // Reply with an error so the caller's pending request resolves instead of timing out.\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n // Answered by the FIRST handler registered for the type (emit would discard\n // returns); wildcard handlers have no response contract, so they're ineligible.\n const handler = this.events.events[messageType as string]?.[0];\n\n if (handler === undefined) {\n // no warning without the hook: the client already gets an explicit error\n if (this.room.onUnhandledMessage !== undefined) {\n this.#reportUnhandled(client, messageType, message);\n }\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {\n name: \"no_handler\",\n message: `room \"${this.room.roomName}\" has no onMessage(\"${messageType}\") handler to answer this request.`,\n });\n return;\n }\n\n const ctx = new DispatchContext(requestId);\n\n // Sync handlers reply in this tick with no promise machinery; only a thenable\n // return defers to the microtask queue. A sync throw (unwrapped handler) and an\n // async rejection both fall to the ERROR reply. With onUncaughtException set the\n // handler is wrapped (swallows its own errors), so it never throws / its promise\n // resolves undefined, and the error reports there.\n let response: any;\n try {\n response = handler(client, message, ctx);\n if (response !== null && typeof response === 'object' && typeof response.then === 'function') {\n response.then(\n (resolved: any) => this.#finalizeRequest(client, requestId, ctx, resolved),\n (e: any) => {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n },\n );\n return;\n }\n } catch (e: any) {\n debugAndPrintError(e);\n this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));\n return;\n }\n\n this.#finalizeRequest(client, requestId, ctx, response);\n }\n\n /** Finalize a request: project the handler's outcome onto a ROOM_RESPONSE reply \u2014\n * `ctx.reject` \u2192 REJECTED(reason), `ctx.resolve(value)` \u2192 OK(value), else OK(return). */\n #finalizeRequest(client: Client, requestId: number, ctx: DispatchContext, response: any): void {\n if (ctx._outcome === OUTCOME_REJECTED) {\n this.#replyToRequest(client, requestId, ResponseStatus.REJECTED, ctx._reason);\n } else if (ctx._outcome === OUTCOME_RESOLVED) {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, ctx._value);\n } else {\n this.#replyToRequest(client, requestId, ResponseStatus.OK, response);\n }\n }\n\n /** Dispatch a `ROOM_DATA_BYTES` frame: raw bytes routed to `_$b`-prefixed handlers. */\n onDataBytes(client: Client & ClientPrivate, buffer: Buffer, it: Iterator): void {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n\n let message: any = buffer.subarray(it.offset, buffer.byteLength);\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.room.roomId);\n\n const bytesMessageType = `_$b${messageType}`;\n\n // custom message validation\n try {\n if (this.validators[bytesMessageType] !== undefined) {\n message = standardValidate(this.validators[bytesMessageType], message);\n }\n } catch (e: any) {\n debugAndPrintError(e);\n client.leave(CloseCode.WITH_ERROR);\n return;\n }\n\n if (this.events.events[bytesMessageType]) {\n this.#dispatch(this.events.events[bytesMessageType], client, message);\n\n } else if (this.events.events['*']) {\n this.#dispatch(this.events.events['*'], client, messageType, message);\n\n } else {\n this.#noHandler(client, messageType, message);\n }\n }\n\n /** Emit a ROOM_RESPONSE reply immediately. */\n #replyToRequest(client: Client, requestId: number, status: ResponseStatus, payload?: any): void {\n debugMessage(\"response #%d: status=%d -> %j (roomId: %s)\", requestId, status, payload, this.room.roomId);\n client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));\n }\n\n /** No handler for the type: error the client in dev, drop it in production. */\n #noHandler(client: Client, messageType: string | number, message: unknown): void {\n const errorMessage = `room onMessage for \"${messageType}\" not registered.`;\n\n if (this.room.onUnhandledMessage !== undefined) {\n this.#reportUnhandled(client, messageType, message);\n\n } else {\n logger.warn(`room \"${this.room.roomName}\" (roomId: ${this.room.roomId}): client '${client.sessionId}' sent \"${messageType}\", which has no onMessage() handler. ${(isDevMode)\n ? \"Replied with an INVALID_PAYLOAD error (outside devMode, the client is disconnected).\"\n : \"Disconnecting the client.\"}`);\n }\n\n if (isDevMode) {\n client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);\n } else {\n // frames already received still arrive after leave(); Room#_onMessage skips them\n client.state = ClientState.LEAVING;\n client.leave(CloseCode.WITH_ERROR, errorMessage);\n }\n }\n\n /** Call `room.onUnhandledMessage`. A throw or rejection goes to\n * `onUncaughtException` when set, or is logged; it never stops the default. */\n #reportUnhandled(client: Client, messageType: string | number, message: unknown): void {\n const report = (e: any) => {\n if (this.room.onUncaughtException !== undefined) {\n this.room.onUncaughtException(new OnMessageException<Room<any>, any>(e, e?.message, client, message, messageType), 'onMessage');\n } else {\n debugAndPrintError(e);\n }\n };\n\n try {\n const result = this.room.onUnhandledMessage!(client, messageType, message) as any;\n if (typeof result?.catch === 'function') {\n result.catch(report);\n }\n } catch (e: any) {\n report(e);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.25",
3
+ "version": "0.18.26",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -62,8 +62,8 @@
62
62
  "express": "^5.0.0",
63
63
  "vitest": "^3.1.1",
64
64
  "@colyseus/redis-driver": "^0.18.7",
65
- "@colyseus/redis-presence": "^0.18.6",
66
- "@colyseus/tools": "^0.18.11"
65
+ "@colyseus/tools": "^0.18.11",
66
+ "@colyseus/redis-presence": "^0.18.6"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@colyseus/auth": "0.18.x",
package/src/Room.ts CHANGED
@@ -255,6 +255,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
255
255
  #_roomId: string;
256
256
  #_roomName: string;
257
257
  #_onLeaveConcurrent: number = 0; // number of onLeave calls in progress
258
+ #_pendingCloses = new WeakMap<Client, () => void>(); // server-initiated closes waiting on onLeave
258
259
 
259
260
  /**
260
261
  * Maximum number of clients allowed to connect into the room. When room reaches this limit,
@@ -804,6 +805,40 @@ export class Room<T extends RoomOptions = RoomOptions> {
804
805
  */
805
806
  public onUncaughtException?(error: RoomException, methodName: RoomMethodName): void;
806
807
 
808
+ /**
809
+ * Called when a client sends a message type that has no `onMessage()` handler.
810
+ * Use it to report the message, for example to an error tracker.
811
+ *
812
+ * It only observes. After it returns, the server still handles the message the
813
+ * usual way:
814
+ * - `room.send()`/`room.sendBytes()`: the client is disconnected with
815
+ * `CloseCode.WITH_ERROR` (4002). In `devMode`, the client receives an
816
+ * `ErrorCode.INVALID_PAYLOAD` error instead and stays connected.
817
+ * - `room.request()`: the request rejects with a `no_handler` error.
818
+ *
819
+ * To accept every message type without disconnecting the client, register a
820
+ * fallback handler with `onMessage("*", ...)` instead. Messages it receives
821
+ * don't reach this method, except requests: the fallback can't answer those.
822
+ *
823
+ * Without this method, the server logs a warning for unhandled `send()`/`sendBytes()`
824
+ * messages. An error thrown here goes to `onUncaughtException` (as an
825
+ * `OnMessageException`) when defined, or is logged.
826
+ *
827
+ * @param client - The client that sent the message.
828
+ * @param type - The message type it sent.
829
+ * @param message - The decoded payload (a `Buffer` for `sendBytes()`).
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * onUnhandledMessage(client: Client, type: string | number, message: unknown) {
834
+ * Sentry.captureMessage(`unhandled message "${type}"`, {
835
+ * extra: { roomId: this.roomId, sessionId: client.sessionId, message },
836
+ * });
837
+ * }
838
+ * ```
839
+ */
840
+ public onUnhandledMessage?(client: ExtractRoomClient<T>, type: string | number, message: unknown): void | Promise<any>;
841
+
807
842
  /**
808
843
  * This method is called before onJoin() - this is where you should authenticate the client
809
844
  * @param client - The client that is authenticating.
@@ -1999,6 +2034,9 @@ export class Room<T extends RoomOptions = RoomOptions> {
1999
2034
 
2000
2035
  this._reserveSeat(sessionId, true, previousClient.auth, seconds, true);
2001
2036
 
2037
+ // the seat is held now: run a server-initiated close that's still waiting on onLeave
2038
+ this.#_pendingCloses.get(previousClient)?.();
2039
+
2002
2040
  // keep reconnection reference in case the user reconnects into this room.
2003
2041
  const reconnection = new Deferred<Client & ClientPrivate>();
2004
2042
  this._reconnections[reconnectionToken] = [sessionId, reconnection];
@@ -2349,14 +2387,16 @@ export class Room<T extends RoomOptions = RoomOptions> {
2349
2387
  // prevent "onLeave" from being called twice if player asks to leave
2350
2388
  client.ref.removeListener('close', client.ref['onleave']);
2351
2389
 
2352
- // only effectively close connection when "onLeave" is fulfilled
2390
+ // close once "onLeave" is fulfilled (#202), or earlier if allowReconnection() holds the seat
2353
2391
  const ref = client.ref;
2354
- this._onLeave(client, closeCode).then(() => {
2355
- // skip if a successful reconnection has transplanted a new ref (#950)
2356
- if (client.ref === ref) {
2392
+ const close = () => {
2393
+ // once only; skip if a successful reconnection has transplanted a new ref (#950)
2394
+ if (this.#_pendingCloses.delete(client) && client.ref === ref) {
2357
2395
  (client as any).leave(closeCode, reason);
2358
2396
  }
2359
- });
2397
+ };
2398
+ this.#_pendingCloses.set(client, close);
2399
+ this._onLeave(client, closeCode).then(close);
2360
2400
  }
2361
2401
 
2362
2402
  private async _onLeave(client: ExtractRoomClient<T>, code?: number): Promise<any> {
@@ -7,8 +7,9 @@ import { createNanoEvents } from './utils/nanoevents.ts';
7
7
  import { standardValidate, type StandardSchemaV1 } from './utils/StandardSchema.ts';
8
8
  import { isDevMode } from './utils/DevMode.ts';
9
9
  import { debugMessage, debugAndPrintError } from './Debug.ts';
10
+ import { logger } from './Logger.ts';
10
11
  import { OnMessageException } from './errors/RoomExceptions.ts';
11
- import type { Client, ClientPrivate } from './Transport.ts';
12
+ import { ClientState, type Client, type ClientPrivate } from './Transport.ts';
12
13
  import type { Room } from './Room.ts';
13
14
 
14
15
  /** Normalize a thrown value into the `{ name, message, code? }` shape echoed in a
@@ -74,7 +75,7 @@ const SEND_CONTEXT: MessageContext = Object.freeze({
74
75
  * ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames
75
76
  * (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the
76
77
  * original dispatch order. Reads back into its Room only for `onUncaughtException`
77
- * (handler wrapping) and `roomId`/`roomName` (logging).
78
+ * (handler wrapping), `onUnhandledMessage` and `roomId`/`roomName` (logging).
78
79
  *
79
80
  * @internal
80
81
  */
@@ -263,6 +264,10 @@ export class RoomMessages {
263
264
  const handler = this.events.events[messageType as string]?.[0];
264
265
 
265
266
  if (handler === undefined) {
267
+ // no warning without the hook: the client already gets an explicit error
268
+ if (this.room.onUnhandledMessage !== undefined) {
269
+ this.#reportUnhandled(client, messageType, message);
270
+ }
266
271
  this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {
267
272
  name: "no_handler",
268
273
  message: `room "${this.room.roomName}" has no onMessage("${messageType}") handler to answer this request.`,
@@ -351,13 +356,45 @@ export class RoomMessages {
351
356
  }
352
357
 
353
358
  /** No handler for the type: error the client in dev, drop it in production. */
354
- #noHandler(client: Client, messageType: string | number, _message: unknown): void {
359
+ #noHandler(client: Client, messageType: string | number, message: unknown): void {
355
360
  const errorMessage = `room onMessage for "${messageType}" not registered.`;
356
- debugMessage(`${errorMessage} (roomId: ${this.room.roomId})`);
361
+
362
+ if (this.room.onUnhandledMessage !== undefined) {
363
+ this.#reportUnhandled(client, messageType, message);
364
+
365
+ } else {
366
+ logger.warn(`room "${this.room.roomName}" (roomId: ${this.room.roomId}): client '${client.sessionId}' sent "${messageType}", which has no onMessage() handler. ${(isDevMode)
367
+ ? "Replied with an INVALID_PAYLOAD error (outside devMode, the client is disconnected)."
368
+ : "Disconnecting the client."}`);
369
+ }
370
+
357
371
  if (isDevMode) {
358
372
  client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
359
373
  } else {
374
+ // frames already received still arrive after leave(); Room#_onMessage skips them
375
+ client.state = ClientState.LEAVING;
360
376
  client.leave(CloseCode.WITH_ERROR, errorMessage);
361
377
  }
362
378
  }
379
+
380
+ /** Call `room.onUnhandledMessage`. A throw or rejection goes to
381
+ * `onUncaughtException` when set, or is logged; it never stops the default. */
382
+ #reportUnhandled(client: Client, messageType: string | number, message: unknown): void {
383
+ const report = (e: any) => {
384
+ if (this.room.onUncaughtException !== undefined) {
385
+ this.room.onUncaughtException(new OnMessageException<Room<any>, any>(e, e?.message, client, message, messageType), 'onMessage');
386
+ } else {
387
+ debugAndPrintError(e);
388
+ }
389
+ };
390
+
391
+ try {
392
+ const result = this.room.onUnhandledMessage!(client, messageType, message) as any;
393
+ if (typeof result?.catch === 'function') {
394
+ result.catch(report);
395
+ }
396
+ } catch (e: any) {
397
+ report(e);
398
+ }
399
+ }
363
400
  }