@colyseus/core 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/build/MatchMaker.cjs +2 -2
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -4
  4. package/build/MatchMaker.mjs +2 -2
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Room.cjs +11 -3
  7. package/build/Room.cjs.map +2 -2
  8. package/build/Room.d.ts +1 -1
  9. package/build/Room.mjs +11 -3
  10. package/build/Room.mjs.map +2 -2
  11. package/build/RoomMessages.cjs +2 -1
  12. package/build/RoomMessages.cjs.map +2 -2
  13. package/build/RoomMessages.d.ts +3 -1
  14. package/build/RoomMessages.mjs +2 -1
  15. package/build/RoomMessages.mjs.map +2 -2
  16. package/build/router/default_routes.cjs +2 -1
  17. package/build/router/default_routes.cjs.map +2 -2
  18. package/build/router/default_routes.mjs +2 -1
  19. package/build/router/default_routes.mjs.map +2 -2
  20. package/build/router/index.cjs +6 -1
  21. package/build/router/index.cjs.map +2 -2
  22. package/build/router/index.d.ts +1 -1
  23. package/build/router/index.mjs +6 -1
  24. package/build/router/index.mjs.map +2 -2
  25. package/build/router/node.cjs +22 -2
  26. package/build/router/node.cjs.map +2 -2
  27. package/build/router/node.mjs +22 -2
  28. package/build/router/node.mjs.map +2 -2
  29. package/build/utils/Utils.cjs.map +2 -2
  30. package/build/utils/Utils.d.ts +12 -0
  31. package/build/utils/Utils.mjs.map +2 -2
  32. package/build/utils/nanoevents.cjs +4 -1
  33. package/build/utils/nanoevents.cjs.map +2 -2
  34. package/build/utils/nanoevents.d.ts +3 -1
  35. package/build/utils/nanoevents.mjs +4 -1
  36. package/build/utils/nanoevents.mjs.map +2 -2
  37. package/package.json +9 -8
  38. package/src/MatchMaker.ts +33 -12
  39. package/src/Room.ts +23 -4
  40. package/src/RoomMessages.ts +2 -1
  41. package/src/router/default_routes.ts +2 -1
  42. package/src/router/index.ts +16 -1
  43. package/src/router/node.ts +26 -2
  44. package/src/utils/Utils.ts +18 -0
  45. package/src/utils/nanoevents.ts +4 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/node.ts"],
4
- "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n });\n req.on('end', () => resolve(data ? JSON.parse(data) : {}));\n req.on('error', reject);\n });\n}\n\n/**\n * Buffer incoming request bodies and expose them as `req.body` before the\n * server's existing \"request\" listeners run.\n *\n * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)\n * rather than `listen()`: the matchmaking router reads the body from `req.body`\n * when present, otherwise from a lazy request stream that does not drain in that\n * mode \u2014 which would stall matchmaking POSTs.\n */\nexport function prereadRequestBodies(server: http.Server) {\n type WithBody = http.IncomingMessage & { body?: unknown };\n const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;\n const run = (req: http.IncomingMessage, res: http.ServerResponse) => {\n for (const listener of listeners) { listener.call(server, req, res); }\n };\n\n server.removeAllListeners('request');\n server.on('request', (req: WithBody, res) => {\n const method = req.method ?? 'GET';\n const needsBody =\n method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&\n req.body === undefined &&\n Number(req.headers['content-length']) > 0;\n\n if (!needsBody) {\n run(req, res);\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf8');\n if ((req.headers['content-type'] || '').includes('application/json')) {\n try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }\n } else {\n req.body = raw;\n }\n run(req, res);\n });\n req.on('error', () => run(req, res));\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body: await readBody(req),\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\n/** Matchmaking options are small \u2014 the cap only stops unbounded buffering. */\nconst MAX_BODY_SIZE = 1024 * 1024;\n\nconst badRequest = (status: number, message: string) =>\n Object.assign(new Error(message), { status });\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n if (data.length > MAX_BODY_SIZE) {\n reject(badRequest(413, 'request body too large'));\n req.destroy();\n }\n });\n // JSON.parse throws on a later tick \u2014 uncaught here it kills the process.\n req.on('end', () => {\n try { resolve(data ? JSON.parse(data) : {}); }\n catch { reject(badRequest(400, 'malformed JSON body')); }\n });\n req.on('error', reject);\n });\n}\n\n/**\n * Buffer incoming request bodies and expose them as `req.body` before the\n * server's existing \"request\" listeners run.\n *\n * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)\n * rather than `listen()`: the matchmaking router reads the body from `req.body`\n * when present, otherwise from a lazy request stream that does not drain in that\n * mode \u2014 which would stall matchmaking POSTs.\n */\nexport function prereadRequestBodies(server: http.Server) {\n type WithBody = http.IncomingMessage & { body?: unknown };\n const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;\n const run = (req: http.IncomingMessage, res: http.ServerResponse) => {\n for (const listener of listeners) { listener.call(server, req, res); }\n };\n\n server.removeAllListeners('request');\n server.on('request', (req: WithBody, res) => {\n const method = req.method ?? 'GET';\n const needsBody =\n method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&\n req.body === undefined &&\n Number(req.headers['content-length']) > 0;\n\n if (!needsBody) {\n run(req, res);\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf8');\n if ((req.headers['content-type'] || '').includes('application/json')) {\n try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }\n } else {\n req.body = raw;\n }\n run(req, res);\n });\n req.on('error', () => run(req, res));\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n let body: any;\n try {\n body = await readBody(req);\n } catch (e: any) {\n // answer here \u2014 next() would report a misleading 404 for a bad body\n res.writeHead(e.status ?? 400, { ...corsHeaders, 'content-type': 'application/json' });\n res.end(JSON.stringify({ error: e.message }));\n return;\n }\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body,\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,iBAAoB;AACpB,iBAA4B;AAC5B,kBAA4B;AAC5B,4BAAoC;AAGpC,IAAM,gBAAgB,OAAO;AAE7B,IAAM,aAAa,CAAC,QAAgB,YAClC,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,EAAE,OAAO,CAAC;AAE9C,SAAS,SAAS,KAAyC;AACzD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AAEX,QAAI,GAAG,QAAQ,CAAC,UAA2B;AACzC,cAAQ,MAAM,SAAS;AACvB,UAAI,KAAK,SAAS,eAAe;AAC/B,eAAO,WAAW,KAAK,wBAAwB,CAAC;AAChD,YAAI,QAAQ;AAAA,MACd;AAAA,IACF,CAAC;AAED,QAAI,GAAG,OAAO,MAAM;AAClB,UAAI;AAAE,gBAAQ,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC;AAAA,MAAG,QACvC;AAAE,eAAO,WAAW,KAAK,qBAAqB,CAAC;AAAA,MAAG;AAAA,IAC1D,CAAC;AACD,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAWO,SAAS,qBAAqB,QAAqB;AAExD,QAAM,YAAY,OAAO,UAAU,SAAS;AAC5C,QAAM,MAAM,CAAC,KAA2B,QAA6B;AACnE,eAAW,YAAY,WAAW;AAAE,eAAS,KAAK,QAAQ,KAAK,GAAG;AAAA,IAAG;AAAA,EACvE;AAEA,SAAO,mBAAmB,SAAS;AACnC,SAAO,GAAG,WAAW,CAAC,KAAe,QAAQ;AAC3C,UAAM,SAAS,IAAI,UAAU;AAC7B,UAAM,YACJ,WAAW,SAAS,WAAW,UAAU,WAAW,aACpD,IAAI,SAAS,UACb,OAAO,IAAI,QAAQ,gBAAgB,CAAC,IAAI;AAE1C,QAAI,CAAC,WAAW;AACd,UAAI,KAAK,GAAG;AACZ;AAAA,IACF;AAEA,UAAM,SAAmB,CAAC;AAC1B,QAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,QAAI,GAAG,OAAO,MAAM;AAClB,YAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AACjD,WAAK,IAAI,QAAQ,cAAc,KAAK,IAAI,SAAS,kBAAkB,GAAG;AACpE,YAAI;AAAE,cAAI,OAAO,KAAK,MAAM,OAAO,IAAI;AAAA,QAAG,QAAQ;AAAE,cAAI,OAAO;AAAA,QAAK;AAAA,MACtE,OAAO;AACL,YAAI,OAAO;AAAA,MACb;AACA,UAAI,KAAK,GAAG;AAAA,IACd,CAAC;AACD,QAAI,GAAG,SAAS,MAAM,IAAI,KAAK,GAAG,CAAC;AAAA,EACrC,CAAC;AACH;AAEA,SAAS,eAAe,KAA2B,SAA2C;AAC5F,SAAO;AAAA,IACL,GAAc,sBAAW;AAAA,IACzB,GAAc,sBAAW,eAAe,OAAO;AAAA,EACjD;AACF;AAEO,SAAS,kCAAkC;AAChD,SAAO,OACL,KACA,KACA,SACG;AACH,UAAM,MAAM,IAAI,eAAI,IAAI,OAAO,KAAK,kBAAkB;AACtD,UAAM,mBAAmB,IAAI,SAAS,WAAW,IAAe,sBAAW,cAAc,GAAG;AAE5F,QAAI,CAAC,kBAAkB;AACrB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,QAAQ,IAAI,OAAiC;AACjE,UAAM,cAAc,eAAe,KAAK,OAAO;AAE/C,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,UAAU,KAAK,WAAW;AAC9B,UAAI,IAAI;AACR;AAAA,IACF;AAEA,QAAI,IAAI,WAAW,QAAQ;AACzB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,SAAS,MAAM,2BAA2B;AAC5D,QAAI,CAAC,OAAO;AACV,WAAK;AACL;AAAA,IACF;AAEA,UAAM,CAAC,EAAE,QAAQ,QAAQ,IAAI;AAE7B,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,SAAS,GAAG;AAAA,IAC3B,SAAS,GAAQ;AAEf,UAAI,UAAU,EAAE,UAAU,KAAK,EAAE,GAAG,aAAa,gBAAgB,mBAAmB,CAAC;AACrF,UAAI,IAAI,KAAK,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC5C;AAAA,IACF;AAEA,QAAI;AACF,YAAM,WAAW,UAAM,2CAAoB;AAAA,QACzC,QAAQ,EAAE,QAAQ,SAAS;AAAA,QAC3B;AAAA,QACA,SAAS,IAAI;AAAA,QACb,SAAS,EAAE,QAAQ;AAAA,QACnB,YAAY;AAAA,MACd,CAAC;AAED,gBAAM,yBAAY,KAAK,QAAQ;AAAA,IAEjC,QAAQ;AAGN,WAAK;AAAA,IACP;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -3,13 +3,25 @@ import { URL } from "url";
3
3
  import * as matchMaker from "../MatchMaker.mjs";
4
4
  import { setResponse } from "@colyseus/better-call/node";
5
5
  import { postMatchmakeMethod } from "./default_routes.mjs";
6
+ var MAX_BODY_SIZE = 1024 * 1024;
7
+ var badRequest = (status, message) => Object.assign(new Error(message), { status });
6
8
  function readBody(req) {
7
9
  return new Promise((resolve, reject) => {
8
10
  let data = "";
9
11
  req.on("data", (chunk) => {
10
12
  data += chunk.toString();
13
+ if (data.length > MAX_BODY_SIZE) {
14
+ reject(badRequest(413, "request body too large"));
15
+ req.destroy();
16
+ }
17
+ });
18
+ req.on("end", () => {
19
+ try {
20
+ resolve(data ? JSON.parse(data) : {});
21
+ } catch {
22
+ reject(badRequest(400, "malformed JSON body"));
23
+ }
11
24
  });
12
- req.on("end", () => resolve(data ? JSON.parse(data) : {}));
13
25
  req.on("error", reject);
14
26
  });
15
27
  }
@@ -77,10 +89,18 @@ function createNodeMatchmakingMiddleware() {
77
89
  return;
78
90
  }
79
91
  const [, method, roomName] = match;
92
+ let body;
93
+ try {
94
+ body = await readBody(req);
95
+ } catch (e) {
96
+ res.writeHead(e.status ?? 400, { ...corsHeaders, "content-type": "application/json" });
97
+ res.end(JSON.stringify({ error: e.message }));
98
+ return;
99
+ }
80
100
  try {
81
101
  const response = await postMatchmakeMethod({
82
102
  params: { method, roomName },
83
- body: await readBody(req),
103
+ body,
84
104
  headers: req.headers,
85
105
  request: { headers },
86
106
  asResponse: true
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/node.ts"],
4
- "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n });\n req.on('end', () => resolve(data ? JSON.parse(data) : {}));\n req.on('error', reject);\n });\n}\n\n/**\n * Buffer incoming request bodies and expose them as `req.body` before the\n * server's existing \"request\" listeners run.\n *\n * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)\n * rather than `listen()`: the matchmaking router reads the body from `req.body`\n * when present, otherwise from a lazy request stream that does not drain in that\n * mode \u2014 which would stall matchmaking POSTs.\n */\nexport function prereadRequestBodies(server: http.Server) {\n type WithBody = http.IncomingMessage & { body?: unknown };\n const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;\n const run = (req: http.IncomingMessage, res: http.ServerResponse) => {\n for (const listener of listeners) { listener.call(server, req, res); }\n };\n\n server.removeAllListeners('request');\n server.on('request', (req: WithBody, res) => {\n const method = req.method ?? 'GET';\n const needsBody =\n method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&\n req.body === undefined &&\n Number(req.headers['content-length']) > 0;\n\n if (!needsBody) {\n run(req, res);\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf8');\n if ((req.headers['content-type'] || '').includes('application/json')) {\n try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }\n } else {\n req.body = raw;\n }\n run(req, res);\n });\n req.on('error', () => run(req, res));\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body: await readBody(req),\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
- "mappings": ";AAUA,SAAS,WAAW;AACpB,YAAY,gBAAgB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AAEpC,SAAS,SAAS,KAAyC;AACzD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AAEX,QAAI,GAAG,QAAQ,CAAC,UAA2B;AACzC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AACD,QAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;AACzD,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAWO,SAAS,qBAAqB,QAAqB;AAExD,QAAM,YAAY,OAAO,UAAU,SAAS;AAC5C,QAAM,MAAM,CAAC,KAA2B,QAA6B;AACnE,eAAW,YAAY,WAAW;AAAE,eAAS,KAAK,QAAQ,KAAK,GAAG;AAAA,IAAG;AAAA,EACvE;AAEA,SAAO,mBAAmB,SAAS;AACnC,SAAO,GAAG,WAAW,CAAC,KAAe,QAAQ;AAC3C,UAAM,SAAS,IAAI,UAAU;AAC7B,UAAM,YACJ,WAAW,SAAS,WAAW,UAAU,WAAW,aACpD,IAAI,SAAS,UACb,OAAO,IAAI,QAAQ,gBAAgB,CAAC,IAAI;AAE1C,QAAI,CAAC,WAAW;AACd,UAAI,KAAK,GAAG;AACZ;AAAA,IACF;AAEA,UAAM,SAAmB,CAAC;AAC1B,QAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,QAAI,GAAG,OAAO,MAAM;AAClB,YAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AACjD,WAAK,IAAI,QAAQ,cAAc,KAAK,IAAI,SAAS,kBAAkB,GAAG;AACpE,YAAI;AAAE,cAAI,OAAO,KAAK,MAAM,OAAO,IAAI;AAAA,QAAG,QAAQ;AAAE,cAAI,OAAO;AAAA,QAAK;AAAA,MACtE,OAAO;AACL,YAAI,OAAO;AAAA,MACb;AACA,UAAI,KAAK,GAAG;AAAA,IACd,CAAC;AACD,QAAI,GAAG,SAAS,MAAM,IAAI,KAAK,GAAG,CAAC;AAAA,EACrC,CAAC;AACH;AAEA,SAAS,eAAe,KAA2B,SAA2C;AAC5F,SAAO;AAAA,IACL,GAAc,sBAAW;AAAA,IACzB,GAAc,sBAAW,eAAe,OAAO;AAAA,EACjD;AACF;AAEO,SAAS,kCAAkC;AAChD,SAAO,OACL,KACA,KACA,SACG;AACH,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;AACtD,UAAM,mBAAmB,IAAI,SAAS,WAAW,IAAe,sBAAW,cAAc,GAAG;AAE5F,QAAI,CAAC,kBAAkB;AACrB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,QAAQ,IAAI,OAAiC;AACjE,UAAM,cAAc,eAAe,KAAK,OAAO;AAE/C,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,UAAU,KAAK,WAAW;AAC9B,UAAI,IAAI;AACR;AAAA,IACF;AAEA,QAAI,IAAI,WAAW,QAAQ;AACzB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,SAAS,MAAM,2BAA2B;AAC5D,QAAI,CAAC,OAAO;AACV,WAAK;AACL;AAAA,IACF;AAEA,UAAM,CAAC,EAAE,QAAQ,QAAQ,IAAI;AAE7B,QAAI;AACF,YAAM,WAAW,MAAM,oBAAoB;AAAA,QACzC,QAAQ,EAAE,QAAQ,SAAS;AAAA,QAC3B,MAAM,MAAM,SAAS,GAAG;AAAA,QACxB,SAAS,IAAI;AAAA,QACb,SAAS,EAAE,QAAQ;AAAA,QACnB,YAAY;AAAA,MACd,CAAC;AAED,YAAM,YAAY,KAAK,QAAQ;AAAA,IAEjC,QAAQ;AAGN,WAAK;AAAA,IACP;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\n/** Matchmaking options are small \u2014 the cap only stops unbounded buffering. */\nconst MAX_BODY_SIZE = 1024 * 1024;\n\nconst badRequest = (status: number, message: string) =>\n Object.assign(new Error(message), { status });\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n if (data.length > MAX_BODY_SIZE) {\n reject(badRequest(413, 'request body too large'));\n req.destroy();\n }\n });\n // JSON.parse throws on a later tick \u2014 uncaught here it kills the process.\n req.on('end', () => {\n try { resolve(data ? JSON.parse(data) : {}); }\n catch { reject(badRequest(400, 'malformed JSON body')); }\n });\n req.on('error', reject);\n });\n}\n\n/**\n * Buffer incoming request bodies and expose them as `req.body` before the\n * server's existing \"request\" listeners run.\n *\n * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)\n * rather than `listen()`: the matchmaking router reads the body from `req.body`\n * when present, otherwise from a lazy request stream that does not drain in that\n * mode \u2014 which would stall matchmaking POSTs.\n */\nexport function prereadRequestBodies(server: http.Server) {\n type WithBody = http.IncomingMessage & { body?: unknown };\n const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;\n const run = (req: http.IncomingMessage, res: http.ServerResponse) => {\n for (const listener of listeners) { listener.call(server, req, res); }\n };\n\n server.removeAllListeners('request');\n server.on('request', (req: WithBody, res) => {\n const method = req.method ?? 'GET';\n const needsBody =\n method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&\n req.body === undefined &&\n Number(req.headers['content-length']) > 0;\n\n if (!needsBody) {\n run(req, res);\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf8');\n if ((req.headers['content-type'] || '').includes('application/json')) {\n try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }\n } else {\n req.body = raw;\n }\n run(req, res);\n });\n req.on('error', () => run(req, res));\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n let body: any;\n try {\n body = await readBody(req);\n } catch (e: any) {\n // answer here \u2014 next() would report a misleading 404 for a bad body\n res.writeHead(e.status ?? 400, { ...corsHeaders, 'content-type': 'application/json' });\n res.end(JSON.stringify({ error: e.message }));\n return;\n }\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body,\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Utils.ts"],
4
- "sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n/**\n * Return type of `remoteRoomCall()`.\n *\n * Resolves to the method's awaited return type (or the property's type) when\n * the method name was captured as a literal type. Falls back to `any` when it\n * wasn't \u2014 e.g. `remoteRoomCall<MyRoom>(...)` with only the room type given:\n * TypeScript applies the `TMethod` default instead of inferring the literal\n * once an explicit type argument list is present (microsoft/TypeScript#26242).\n * Pass both type arguments (`remoteRoomCall<MyRoom, 'myMethod'>`) for a\n * precise return type.\n */\nexport type RemoteRoomCallReturn<\n TRoom,\n TMethod extends keyof TRoom\n> = keyof TRoom extends TMethod\n ? any\n : ExtractMethodOrPropertyType<TRoom, TMethod>;\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -11,6 +11,18 @@ export type MethodName<T> = string & {
11
11
  * - If the key is a property, returns the type of that property
12
12
  */
13
13
  export type ExtractMethodOrPropertyType<TClass, TKey extends keyof TClass> = TClass[TKey] extends (...args: any[]) => infer R ? Awaited<R> : TClass[TKey];
14
+ /**
15
+ * Return type of `remoteRoomCall()`.
16
+ *
17
+ * Resolves to the method's awaited return type (or the property's type) when
18
+ * the method name was captured as a literal type. Falls back to `any` when it
19
+ * wasn't — e.g. `remoteRoomCall<MyRoom>(...)` with only the room type given:
20
+ * TypeScript applies the `TMethod` default instead of inferring the literal
21
+ * once an explicit type argument list is present (microsoft/TypeScript#26242).
22
+ * Pass both type arguments (`remoteRoomCall<MyRoom, 'myMethod'>`) for a
23
+ * precise return type.
24
+ */
25
+ export type RemoteRoomCallReturn<TRoom, TMethod extends keyof TRoom> = keyof TRoom extends TMethod ? any : ExtractMethodOrPropertyType<TRoom, TMethod>;
14
26
  export declare const REMOTE_ROOM_SHORT_TIMEOUT: number;
15
27
  export declare const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME: number;
16
28
  /**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Utils.ts"],
4
- "sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n/**\n * Return type of `remoteRoomCall()`.\n *\n * Resolves to the method's awaited return type (or the property's type) when\n * the method name was captured as a literal type. Falls back to `any` when it\n * wasn't \u2014 e.g. `remoteRoomCall<MyRoom>(...)` with only the room type given:\n * TypeScript applies the `TMethod` default instead of inferring the literal\n * once an explicit type argument list is present (microsoft/TypeScript#26242).\n * Pass both type arguments (`remoteRoomCall<MyRoom, 'myMethod'>`) for a\n * precise return type.\n */\nexport type RemoteRoomCallReturn<\n TRoom,\n TMethod extends keyof TRoom\n> = keyof TRoom extends TMethod\n ? any\n : ExtractMethodOrPropertyType<TRoom, TMethod>;\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -29,7 +29,10 @@ var createNanoEvents = () => ({
29
29
  callbacks[i](...args);
30
30
  }
31
31
  },
32
- events: {},
32
+ // null-prototype: event names are client-supplied (message types), and on a
33
+ // plain object "__proto__" / "constructor" / "toString" resolve to inherited
34
+ // members instead of missing (colyseus/colyseus#951)
35
+ events: /* @__PURE__ */ Object.create(null),
33
36
  on(event, cb) {
34
37
  ;
35
38
  (this.events[event] ||= []).push(cb);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/nanoevents.ts"],
4
- "sourcesContent": ["export const createNanoEvents = () => ({\n emit(event: string, ...args: any[]) {\n for (\n let callbacks = this.events[event] || [],\n i = 0,\n length = callbacks.length;\n i < length;\n i++\n ) {\n callbacks[i](...args)\n }\n },\n events: {},\n on(event: string, cb: (...args: any[]) => void) {\n ;(this.events[event] ||= []).push(cb)\n return () => {\n this.events[event] = this.events[event]?.filter(i => cb !== i)\n }\n }\n })"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,mBAAmB,OAAO;AAAA,EACnC,KAAK,UAAkB,MAAa;AAClC,aACM,YAAY,KAAK,OAAO,KAAK,KAAK,CAAC,GACrC,IAAI,GACJ,SAAS,UAAU,QACrB,IAAI,QACJ,KACA;AACA,gBAAU,CAAC,EAAE,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AAAA,EACA,QAAQ,CAAC;AAAA,EACT,GAAG,OAAe,IAA8B;AAC9C;AAAC,KAAC,KAAK,OAAO,KAAK,MAAM,CAAC,GAAG,KAAK,EAAE;AACpC,WAAO,MAAM;AACX,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,OAAO,OAAK,OAAO,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["export const createNanoEvents = () => ({\n emit(event: string, ...args: any[]) {\n for (\n let callbacks = this.events[event] || [],\n i = 0,\n length = callbacks.length;\n i < length;\n i++\n ) {\n callbacks[i](...args)\n }\n },\n // null-prototype: event names are client-supplied (message types), and on a\n // plain object \"__proto__\" / \"constructor\" / \"toString\" resolve to inherited\n // members instead of missing (colyseus/colyseus#951)\n events: Object.create(null) as { [event: string]: Array<(...args: any[]) => void> },\n on(event: string, cb: (...args: any[]) => void) {\n ;(this.events[event] ||= []).push(cb)\n return () => {\n this.events[event] = this.events[event]?.filter(i => cb !== i)\n }\n }\n })"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,mBAAmB,OAAO;AAAA,EACnC,KAAK,UAAkB,MAAa;AAClC,aACM,YAAY,KAAK,OAAO,KAAK,KAAK,CAAC,GACrC,IAAI,GACJ,SAAS,UAAU,QACrB,IAAI,QACJ,KACA;AACA,gBAAU,CAAC,EAAE,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAIA,QAAQ,uBAAO,OAAO,IAAI;AAAA,EAC1B,GAAG,OAAe,IAA8B;AAC9C;AAAC,KAAC,KAAK,OAAO,KAAK,MAAM,CAAC,GAAG,KAAK,EAAE;AACpC,WAAO,MAAM;AACX,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,OAAO,OAAK,OAAO,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,7 @@
1
1
  export declare const createNanoEvents: () => {
2
2
  emit(event: string, ...args: any[]): void;
3
- events: {};
3
+ events: {
4
+ [event: string]: Array<(...args: any[]) => void>;
5
+ };
4
6
  on(event: string, cb: (...args: any[]) => void): () => void;
5
7
  };
@@ -5,7 +5,10 @@ var createNanoEvents = () => ({
5
5
  callbacks[i](...args);
6
6
  }
7
7
  },
8
- events: {},
8
+ // null-prototype: event names are client-supplied (message types), and on a
9
+ // plain object "__proto__" / "constructor" / "toString" resolve to inherited
10
+ // members instead of missing (colyseus/colyseus#951)
11
+ events: /* @__PURE__ */ Object.create(null),
9
12
  on(event, cb) {
10
13
  ;
11
14
  (this.events[event] ||= []).push(cb);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/nanoevents.ts"],
4
- "sourcesContent": ["export const createNanoEvents = () => ({\n emit(event: string, ...args: any[]) {\n for (\n let callbacks = this.events[event] || [],\n i = 0,\n length = callbacks.length;\n i < length;\n i++\n ) {\n callbacks[i](...args)\n }\n },\n events: {},\n on(event: string, cb: (...args: any[]) => void) {\n ;(this.events[event] ||= []).push(cb)\n return () => {\n this.events[event] = this.events[event]?.filter(i => cb !== i)\n }\n }\n })"],
5
- "mappings": ";AAAO,IAAM,mBAAmB,OAAO;AAAA,EACnC,KAAK,UAAkB,MAAa;AAClC,aACM,YAAY,KAAK,OAAO,KAAK,KAAK,CAAC,GACrC,IAAI,GACJ,SAAS,UAAU,QACrB,IAAI,QACJ,KACA;AACA,gBAAU,CAAC,EAAE,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AAAA,EACA,QAAQ,CAAC;AAAA,EACT,GAAG,OAAe,IAA8B;AAC9C;AAAC,KAAC,KAAK,OAAO,KAAK,MAAM,CAAC,GAAG,KAAK,EAAE;AACpC,WAAO,MAAM;AACX,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,OAAO,OAAK,OAAO,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["export const createNanoEvents = () => ({\n emit(event: string, ...args: any[]) {\n for (\n let callbacks = this.events[event] || [],\n i = 0,\n length = callbacks.length;\n i < length;\n i++\n ) {\n callbacks[i](...args)\n }\n },\n // null-prototype: event names are client-supplied (message types), and on a\n // plain object \"__proto__\" / \"constructor\" / \"toString\" resolve to inherited\n // members instead of missing (colyseus/colyseus#951)\n events: Object.create(null) as { [event: string]: Array<(...args: any[]) => void> },\n on(event: string, cb: (...args: any[]) => void) {\n ;(this.events[event] ||= []).push(cb)\n return () => {\n this.events[event] = this.events[event]?.filter(i => cb !== i)\n }\n }\n })"],
5
+ "mappings": ";AAAO,IAAM,mBAAmB,OAAO;AAAA,EACnC,KAAK,UAAkB,MAAa;AAClC,aACM,YAAY,KAAK,OAAO,KAAK,KAAK,CAAC,GACrC,IAAI,GACJ,SAAS,UAAU,QACrB,IAAI,QACJ,KACA;AACA,gBAAU,CAAC,EAAE,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAIA,QAAQ,uBAAO,OAAO,IAAI;AAAA,EAC1B,GAAG,OAAe,IAA8B;AAC9C;AAAC,KAAC,KAAK,OAAO,KAAK,MAAM,CAAC,GAAG,KAAK,EAAE;AACpC,WAAO,MAAM;AACX,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,OAAO,OAAK,OAAO,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -52,24 +52,25 @@
52
52
  "msgpackr": "^2.0.1",
53
53
  "nanoid": "^3.3.11",
54
54
  "@colyseus/better-call": "^1.3.1",
55
- "@colyseus/shared-types": "^0.18.1",
56
- "@colyseus/greeting-banner": "^4.0.1"
55
+ "@colyseus/greeting-banner": "^4.0.1",
56
+ "@colyseus/shared-types": "^0.18.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@colyseus/schema": "^5.0.8",
60
60
  "express": "^5.0.0",
61
- "@colyseus/redis-presence": "^0.18.1",
61
+ "vitest": "^3.1.1",
62
62
  "@colyseus/redis-driver": "^0.18.1",
63
- "@colyseus/tools": "^0.18.1"
63
+ "@colyseus/redis-presence": "^0.18.2",
64
+ "@colyseus/tools": "^0.18.2"
64
65
  },
65
66
  "peerDependencies": {
66
67
  "@colyseus/schema": "^5.0.8",
67
68
  "@pm2/io": "^6.1.0",
68
69
  "express": "^4.16.0 || ^5.0.0",
69
70
  "zod": "^4.1.12",
71
+ "@colyseus/auth": "^0.18.1",
70
72
  "@colyseus/better-call": "^1.3.1",
71
- "@colyseus/ws-transport": "^0.18.1",
72
- "@colyseus/auth": "^0.18.1"
73
+ "@colyseus/ws-transport": "^0.18.1"
73
74
  },
74
75
  "peerDependenciesMeta": {
75
76
  "@colyseus/auth": {
@@ -99,6 +100,6 @@
99
100
  },
100
101
  "gitHead": "c45b410e99eadffff4b74e701339992e2faa15f8",
101
102
  "scripts": {
102
- "test": "tsc -p test"
103
+ "test": "vitest run"
103
104
  }
104
105
  }
package/src/MatchMaker.ts CHANGED
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
2
2
 
3
3
  import { requestFromIPC, subscribeIPC, subscribeWithTimeout } from './IPC.ts';
4
4
 
5
- import { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, type MethodName, type ExtractMethodOrPropertyType } from './utils/Utils.ts';
5
+ import { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, type MethodName, type RemoteRoomCallReturn } from './utils/Utils.ts';
6
6
  import { isDevMode, cacheRoomHistory, getRoomRestoreListKey, reloadFromCache } from './utils/DevMode.ts';
7
7
 
8
8
  import { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
@@ -32,7 +32,8 @@ export { controller, stats, type MatchMakerDriver };
32
32
  export type ClientOptions = any;
33
33
  export type SelectProcessIdCallback = (roomName: string, clientOptions: ClientOptions) => Promise<string>;
34
34
 
35
- const handlers: {[id: string]: RegisteredHandler} = {};
35
+ // null-prototype: keyed by client-supplied room name (colyseus/colyseus#951)
36
+ const handlers: {[id: string]: RegisteredHandler} = Object.create(null);
36
37
  const rooms: {[roomId: string]: Room} = {};
37
38
  const events = new EventEmitter();
38
39
 
@@ -337,7 +338,7 @@ export async function findRoomsByIds(roomIds: string[]): Promise<Map<string, IRo
337
338
  *
338
339
  * @param roomName - The Id of the specific room.
339
340
  * @param filterOptions - Filter options.
340
- * @param sortOptions - Sorting options.
341
+ * @param additionalSortOptions - Sorting options, merged over the ones declared on the room handler.
341
342
  *
342
343
  * @returns Promise<IRoomCache> - A promise contaning an object which includes room metadata and configurations.
343
344
  */
@@ -364,23 +365,43 @@ export async function findOneRoomAvailable(
364
365
  /**
365
366
  * Call a method or return a property on a remote room.
366
367
  *
368
+ * Provide both type arguments for a precisely typed result, e.g.
369
+ * `remoteRoomCall<MyRoom, 'myMethod'>(roomId, 'myMethod')`. With only the
370
+ * room type given, TypeScript cannot infer the method literal
371
+ * (microsoft/TypeScript#26242) and the result is typed `any`.
372
+ *
367
373
  * @param roomId - The Id of the specific room instance.
368
374
  * @param method - Method or attribute to call or retrive.
369
375
  * @param args - Array of arguments for the method
370
376
  *
371
- * @returns Promise<any> - Returned value from the called or retrieved method/attribute.
377
+ * @returns Promise - Returned value from the called or retrieved method/attribute.
372
378
  */
373
- export async function remoteRoomCall<TRoom = Room>(
379
+ export function remoteRoomCall<TRoom = Room, TMethod extends keyof TRoom = keyof TRoom>(
380
+ roomId: string,
381
+ method: TMethod,
382
+ args?: any[],
383
+ rejectionTimeout?: number,
384
+ ): Promise<RemoteRoomCallReturn<TRoom, TMethod>>;
385
+
386
+ // fallback for dynamic method names not present on the room type
387
+ export function remoteRoomCall<TRoom = Room>(
374
388
  roomId: string,
375
389
  method: keyof TRoom,
376
390
  args?: any[],
391
+ rejectionTimeout?: number,
392
+ ): Promise<any>;
393
+
394
+ export async function remoteRoomCall(
395
+ roomId: string,
396
+ method: any,
397
+ args?: any[],
377
398
  rejectionTimeout = REMOTE_ROOM_SHORT_TIMEOUT,
378
- ): Promise<ExtractMethodOrPropertyType<TRoom, typeof method>> {
379
- const room = rooms[roomId] as TRoom;
399
+ ): Promise<any> {
400
+ const room = rooms[roomId];
380
401
 
381
402
  if (!room) {
382
403
  try {
383
- return await requestFromIPC(presence, getRoomChannel(roomId), method as string, args, rejectionTimeout);
404
+ return await requestFromIPC(presence, getRoomChannel(roomId), method, args, rejectionTimeout);
384
405
 
385
406
  } catch (e: any) {
386
407
 
@@ -395,7 +416,7 @@ export async function remoteRoomCall<TRoom = Room>(
395
416
 
396
417
  // TODO: for 1.0, consider always throwing previous error directly.
397
418
 
398
- const request = `${String(method)}${args && ' with args ' + JSON.stringify(args) || ''}`;
419
+ const request = `${method}${args && ' with args ' + JSON.stringify(args) || ''}`;
399
420
  throw new ServerError(
400
421
  ErrorCode.MATCHMAKE_UNHANDLED,
401
422
  `remote room (${roomId}) timed out, requesting "${request}". (${rejectionTimeout}ms exceeded)`,
@@ -403,9 +424,9 @@ export async function remoteRoomCall<TRoom = Room>(
403
424
  }
404
425
 
405
426
  } else {
406
- return (!args && typeof (room[method]) !== 'function')
407
- ? room[method as string]
408
- : (await room[method as string].apply(room, args && JSON.parse(JSON.stringify(args))));
427
+ return (!args && typeof ((room as any)[method]) !== 'function')
428
+ ? (room as any)[method]
429
+ : (await (room as any)[method].apply(room, args && JSON.parse(JSON.stringify(args))));
409
430
  }
410
431
  }
411
432
 
package/src/Room.ts CHANGED
@@ -1778,7 +1778,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
1778
1778
  * Allow the specified client to reconnect into the room. Must be used inside `onLeave()` method.
1779
1779
  * If seconds is provided, the reconnection is going to be cancelled after the provided amount of seconds.
1780
1780
  *
1781
- * @param client - The client that is allowed to reconnect into the room.
1781
+ * @param previousClient - The client that is allowed to reconnect into the room.
1782
1782
  * @param seconds - The time in seconds that the client is allowed to reconnect into the room.
1783
1783
  *
1784
1784
  * @returns Deferred<Client> - The differed is a promise like type.
@@ -2170,7 +2170,13 @@ export class Room<T extends RoomOptions = RoomOptions> {
2170
2170
  client.ref.removeListener('close', client.ref['onleave']);
2171
2171
 
2172
2172
  // only effectively close connection when "onLeave" is fulfilled
2173
- this._onLeave(client, closeCode).then(() => (client as any).leave(closeCode, reason));
2173
+ const ref = client.ref;
2174
+ this._onLeave(client, closeCode).then(() => {
2175
+ // skip if a successful reconnection has transplanted a new ref (#950)
2176
+ if (client.ref === ref) {
2177
+ (client as any).leave(closeCode, reason);
2178
+ }
2179
+ });
2174
2180
  }
2175
2181
 
2176
2182
  private async _onLeave(client: ExtractRoomClient<T>, code?: number): Promise<any> {
@@ -2208,14 +2214,27 @@ export class Room<T extends RoomOptions = RoomOptions> {
2208
2214
  }
2209
2215
  }
2210
2216
 
2217
+ //
2218
+ // A successful reconnection has already replaced this client: the replacement
2219
+ // owns the 'leave' accounting from here on.
2220
+ //
2221
+ // This must be checked before looking up `_reconnections`: allowReconnection()
2222
+ // reassigns `previousClient.reconnectionToken` to the replacement's token, so a
2223
+ // late-resuming _onLeave() would otherwise attach a second #_onAfterLeave() to
2224
+ // the replacement's pending reconnection and decrement ccu twice for one join.
2225
+ //
2226
+ // @ts-ignore (client.state may be modified at onLeave())
2227
+ if (client.state === ClientState.RECONNECTED) {
2228
+ return;
2229
+ }
2230
+
2211
2231
  // check for manual "reconnection" flow
2212
2232
  if (this._reconnections[client.reconnectionToken]) {
2213
2233
  this._reconnections[client.reconnectionToken][1].catch(async () => {
2214
2234
  await this.#_onAfterLeave(client, code, method === this.onDrop);
2215
2235
  });
2216
2236
 
2217
- // @ts-ignore (client.state may be modified at onLeave())
2218
- } else if (client.state !== ClientState.RECONNECTED) {
2237
+ } else {
2219
2238
  await this.#_onAfterLeave(client, code, method === this.onDrop);
2220
2239
  }
2221
2240
  }
@@ -88,7 +88,8 @@ export class RoomMessages {
88
88
 
89
89
  /** Per-type StandardSchema validators. Public for the same reason as
90
90
  * {@link events} (`onMessageValidators`). */
91
- validators: { [type: string]: StandardSchemaV1 } = {};
91
+ // null-prototype: keyed by client-supplied message type (colyseus/colyseus#951)
92
+ validators: { [type: string]: StandardSchemaV1 } = Object.create(null);
92
93
 
93
94
  constructor(room: Room<any>) {
94
95
  this.room = room;
@@ -1,4 +1,5 @@
1
- import { createEndpoint, createRouter } from "@colyseus/better-call";
1
+ import { createEndpoint } from "@colyseus/better-call";
2
+ import { createRouter } from "./index.ts";
2
3
  import * as matchMaker from "../MatchMaker.ts";
3
4
  import { getBearerToken } from "../utils/Utils.ts";
4
5
  import { getTransport } from "../Transport.ts";
@@ -12,7 +12,9 @@ export {
12
12
  createMiddleware,
13
13
  createInternalContext,
14
14
 
15
- // Re-export types needed for declaration emit
15
+ // Re-export every type reachable from an inferred type below — consumers
16
+ // depend on @colyseus/core, not @colyseus/better-call, and cannot name it
17
+ // under pnpm's isolated node_modules (TS2742/TS2883).
16
18
  type Router,
17
19
  type RouterConfig,
18
20
  type Endpoint,
@@ -20,6 +22,12 @@ export {
20
22
  type EndpointOptions,
21
23
  type EndpointContext,
22
24
  type StrictEndpoint,
25
+ type StandardSchemaV1,
26
+ type MiddlewareOptions,
27
+ type MiddlewareInputContext,
28
+ type CookieOptions,
29
+ type CookiePrefixOptions,
30
+ type Status,
23
31
  } from "@colyseus/better-call";
24
32
 
25
33
  export { toNodeHandler };
@@ -138,11 +146,18 @@ export function createRouter<
138
146
  E extends Record<string, Endpoint>,
139
147
  Config extends RouterConfig
140
148
  >(endpoints: E, config: Config = {} as Config) {
149
+ const onError = config?.onError;
141
150
  return createBetterCallRouter({ ...endpoints }, {
142
151
  // better-call's /api/reference page dumps the full API surface
143
152
  // unauthenticated — opt back in by passing `openapi` explicitly.
144
153
  openapi: { disabled: true },
145
154
  ...config,
155
+ // Otherwise a malformed body is a 500 plus a stack trace on stderr: log
156
+ // noise any anonymous client can trigger at will. Matched on the message
157
+ // because `onError` receives no request context to test against.
158
+ onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))
159
+ ? Response.json({ error: 'malformed request body' }, { status: 400 })
160
+ : await onError?.(error),
146
161
  });
147
162
  }
148
163