@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.
- package/build/Room.cjs +9 -3
- package/build/Room.cjs.map +2 -2
- package/build/Room.d.ts +33 -0
- package/build/Room.mjs +9 -3
- package/build/Room.mjs.map +2 -2
- package/build/RoomMessages.cjs +31 -2
- package/build/RoomMessages.cjs.map +2 -2
- package/build/RoomMessages.d.ts +2 -2
- package/build/RoomMessages.mjs +31 -2
- package/build/RoomMessages.mjs.map +2 -2
- package/package.json +3 -3
- package/src/Room.ts +45 -5
- package/src/RoomMessages.ts +41 -4
package/build/RoomMessages.cjs
CHANGED
|
@@ -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,
|
|
278
|
+
#noHandler(client, messageType, message) {
|
|
274
279
|
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
275
|
-
|
|
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": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAsC;AACtC,sBAAuB;AAEvB,0BAAoF;AACpF,sBAAgC;AAChC,wBAAiC;AACjC,4BAAwD;AACxD,qBAA0B;AAC1B,mBAAiD;AACjD,4BAAmC;
|
|
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/build/RoomMessages.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Iterator } from '@colyseus/schema';
|
|
2
2
|
import { type StandardSchemaV1 } from './utils/StandardSchema.ts';
|
|
3
|
-
import type
|
|
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
|
*/
|
package/build/RoomMessages.mjs
CHANGED
|
@@ -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,
|
|
253
|
+
#noHandler(client, messageType, message) {
|
|
249
254
|
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
250
|
-
|
|
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": ";AAAA,SAAS,cAA6B;AACtC,SAAS,cAAc;AAEvB,SAAS,WAAW,WAAW,UAAU,sBAA2C;AACpF,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,wBAA+C;AACxD,SAAS,iBAAiB;AAC1B,SAAS,cAAc,0BAA0B;AACjD,SAAS,0BAA0B;
|
|
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.
|
|
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/
|
|
66
|
-
"@colyseus/
|
|
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
|
-
//
|
|
2390
|
+
// close once "onLeave" is fulfilled (#202), or earlier if allowReconnection() holds the seat
|
|
2353
2391
|
const ref = client.ref;
|
|
2354
|
-
|
|
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> {
|
package/src/RoomMessages.ts
CHANGED
|
@@ -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
|
|
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,
|
|
359
|
+
#noHandler(client: Client, messageType: string | number, message: unknown): void {
|
|
355
360
|
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
356
|
-
|
|
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
|
}
|