@colyseus/core 0.18.4 → 0.18.6

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 (122) hide show
  1. package/build/Debug.cjs +1 -0
  2. package/build/Debug.cjs.map +1 -1
  3. package/build/IPC.cjs +1 -0
  4. package/build/IPC.cjs.map +1 -1
  5. package/build/Logger.cjs +1 -0
  6. package/build/Logger.cjs.map +1 -1
  7. package/build/MatchMaker.cjs +11 -3
  8. package/build/MatchMaker.cjs.map +2 -2
  9. package/build/MatchMaker.mjs +10 -3
  10. package/build/MatchMaker.mjs.map +2 -2
  11. package/build/Protocol.cjs +1 -0
  12. package/build/Protocol.cjs.map +1 -1
  13. package/build/Rewind.cjs +1 -0
  14. package/build/Rewind.cjs.map +1 -1
  15. package/build/Room.cjs +102 -0
  16. package/build/Room.cjs.map +2 -2
  17. package/build/Room.d.ts +57 -0
  18. package/build/Room.mjs +101 -0
  19. package/build/Room.mjs.map +2 -2
  20. package/build/RoomMessages.cjs +1 -0
  21. package/build/RoomMessages.cjs.map +1 -1
  22. package/build/RoomPlugin.cjs +1 -0
  23. package/build/RoomPlugin.cjs.map +1 -1
  24. package/build/Server.cjs +1 -0
  25. package/build/Server.cjs.map +1 -1
  26. package/build/Stats.cjs +1 -0
  27. package/build/Stats.cjs.map +1 -1
  28. package/build/Transport.cjs +39 -0
  29. package/build/Transport.cjs.map +2 -2
  30. package/build/Transport.d.ts +66 -1
  31. package/build/Transport.mjs +37 -1
  32. package/build/Transport.mjs.map +2 -2
  33. package/build/errors/RoomExceptions.cjs +1 -0
  34. package/build/errors/RoomExceptions.cjs.map +1 -1
  35. package/build/errors/SeatReservationError.cjs +1 -0
  36. package/build/errors/SeatReservationError.cjs.map +1 -1
  37. package/build/errors/ServerError.cjs +1 -0
  38. package/build/errors/ServerError.cjs.map +1 -1
  39. package/build/index.cjs +5 -0
  40. package/build/index.cjs.map +2 -2
  41. package/build/index.d.ts +1 -1
  42. package/build/index.mjs +3 -1
  43. package/build/index.mjs.map +2 -2
  44. package/build/input/InputBuffer.cjs +1 -0
  45. package/build/input/InputBuffer.cjs.map +1 -1
  46. package/build/input/RoomInput.cjs +64 -3
  47. package/build/input/RoomInput.cjs.map +2 -2
  48. package/build/input/RoomInput.d.ts +16 -2
  49. package/build/input/RoomInput.mjs +63 -3
  50. package/build/input/RoomInput.mjs.map +2 -2
  51. package/build/input/types.cjs +1 -0
  52. package/build/input/types.cjs.map +1 -1
  53. package/build/internal.cjs +1 -0
  54. package/build/internal.cjs.map +1 -1
  55. package/build/matchmaker/Lobby.cjs +1 -0
  56. package/build/matchmaker/Lobby.cjs.map +1 -1
  57. package/build/matchmaker/LocalDriver/LocalDriver.cjs +1 -0
  58. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +1 -1
  59. package/build/matchmaker/LocalDriver/Query.cjs +1 -0
  60. package/build/matchmaker/LocalDriver/Query.cjs.map +1 -1
  61. package/build/matchmaker/RegisteredHandler.cjs +1 -0
  62. package/build/matchmaker/RegisteredHandler.cjs.map +1 -1
  63. package/build/matchmaker/controller.cjs +1 -0
  64. package/build/matchmaker/controller.cjs.map +1 -1
  65. package/build/matchmaker/driver.cjs +1 -0
  66. package/build/matchmaker/driver.cjs.map +2 -2
  67. package/build/matchmaker/driver.d.ts +4 -2
  68. package/build/matchmaker/driver.mjs.map +1 -1
  69. package/build/presence/LocalPresence.cjs +1 -0
  70. package/build/presence/LocalPresence.cjs.map +1 -1
  71. package/build/presence/Presence.cjs +1 -0
  72. package/build/presence/Presence.cjs.map +1 -1
  73. package/build/rooms/LobbyRoom.cjs +1 -0
  74. package/build/rooms/LobbyRoom.cjs.map +1 -1
  75. package/build/rooms/QueueRoom.cjs +1 -0
  76. package/build/rooms/QueueRoom.cjs.map +1 -1
  77. package/build/rooms/RelayRoom.cjs +1 -0
  78. package/build/rooms/RelayRoom.cjs.map +1 -1
  79. package/build/router/default_routes.cjs +2 -7
  80. package/build/router/default_routes.cjs.map +2 -2
  81. package/build/router/default_routes.mjs +2 -8
  82. package/build/router/default_routes.mjs.map +2 -2
  83. package/build/router/index.cjs +48 -1
  84. package/build/router/index.cjs.map +3 -3
  85. package/build/router/index.d.ts +6 -2
  86. package/build/router/index.mjs +47 -1
  87. package/build/router/index.mjs.map +2 -2
  88. package/build/router/node.cjs +1 -0
  89. package/build/router/node.cjs.map +1 -1
  90. package/build/serializer/NoneSerializer.cjs +1 -0
  91. package/build/serializer/NoneSerializer.cjs.map +1 -1
  92. package/build/serializer/SchemaSerializer.cjs +98 -0
  93. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  94. package/build/serializer/SchemaSerializer.d.ts +25 -0
  95. package/build/serializer/SchemaSerializer.mjs +98 -1
  96. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  97. package/build/serializer/Serializer.cjs +1 -0
  98. package/build/serializer/Serializer.cjs.map +2 -2
  99. package/build/serializer/Serializer.d.ts +15 -0
  100. package/build/utils/DevMode.cjs +1 -0
  101. package/build/utils/DevMode.cjs.map +1 -1
  102. package/build/utils/Env.cjs +1 -0
  103. package/build/utils/Env.cjs.map +1 -1
  104. package/build/utils/StandardSchema.cjs +1 -0
  105. package/build/utils/StandardSchema.cjs.map +1 -1
  106. package/build/utils/UserSessionIndex.cjs +1 -0
  107. package/build/utils/UserSessionIndex.cjs.map +1 -1
  108. package/build/utils/Utils.cjs +1 -0
  109. package/build/utils/Utils.cjs.map +1 -1
  110. package/build/utils/nanoevents.cjs +1 -0
  111. package/build/utils/nanoevents.cjs.map +1 -1
  112. package/package.json +5 -5
  113. package/src/MatchMaker.ts +20 -3
  114. package/src/Room.ts +122 -0
  115. package/src/Transport.ts +132 -4
  116. package/src/index.ts +1 -1
  117. package/src/input/RoomInput.ts +76 -3
  118. package/src/matchmaker/driver.ts +4 -2
  119. package/src/router/default_routes.ts +2 -8
  120. package/src/router/index.ts +72 -3
  121. package/src/serializer/SchemaSerializer.ts +146 -1
  122. package/src/serializer/Serializer.ts +17 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/index.ts"],
4
- "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
5
- "mappings": "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",
6
- "names": ["import_better_call", "pkg", "createBetterCallRouter"]
4
+ "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */\n prefix?: string;\n /** SPA dist dir \u2014 the default middleware dispatches catch-all requests only for files that exist here. */\n staticDir?: string;\n /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */\n buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? '', opts.staticDir);\n const middleware = buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n\n/**\n * Default express middleware for a single-prefix SPA panel (monitor,\n * playground). Express and the endpoint map use different coordinates: a path\n * mount strips its prefix into `req.baseUrl`, while the endpoints keep the\n * configured `prefix` baked into their paths. This rebases every request onto\n * the endpoint namespace, so the panel works at any mount path \u2014 `prefix` only\n * matters in router mode and at root mounts.\n */\nfunction panelMiddleware(prefix: string, staticDir?: string) {\n const staticRoot = staticDir && path.resolve(staticDir);\n return ({ specificRouter, specificHandler, fullHandler }: DualModeHelpers): ExpressMiddleware =>\n (req, res, next) => {\n const r = req as IncomingMessage & { baseUrl?: string; originalUrl?: string };\n const raw = r.url ?? '';\n const q = raw.indexOf('?');\n const url = q < 0 ? raw : raw.slice(0, q);\n const query = q < 0 ? '' : raw.slice(q);\n\n // Path mounts arrive stripped (`req.url` is mount-relative); root and\n // pathless mounts arrive unstripped and already match the endpoints.\n const dispatchPath = r.baseUrl ? prefix + url : url;\n\n // Canonicalize the index: the SPA references its assets relatively, so\n // they only resolve from the trailing-slash URL. The browser's address\n // is originalUrl \u2014 302 (not 301: browsers cache 301s across remounts).\n if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {\n const original = (r.originalUrl ?? raw).split('?')[0]!;\n if (r.method === 'GET' && !original.endsWith('/')) {\n res.writeHead(302, { location: `${original}/${query}` });\n res.end();\n return;\n }\n }\n\n const dispatch = (handler: NodeHandler) => {\n // better-call's getRequest resolves the path as baseUrl + url\n const wrapped = Object.create(req, {\n url: { value: dispatchPath + query, enumerable: true, configurable: true },\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true },\n });\n handler(wrapped as any, res).catch(next);\n };\n\n const route = specificRouter.findRoute(r.method ?? 'GET', dispatchPath);\n // rou3 normalizes trailing slashes in findRoute but processRequest\n // exact-matches \u2014 only trust the hit when the slashes agree.\n if (route && (!route.data?.path?.endsWith('/') || dispatchPath.endsWith('/'))) {\n return dispatch(specificHandler);\n }\n\n // Asset request \u2014 only delegate when the file exists on disk, so the\n // catch-all's SPA fallback can't mask sibling express routes.\n if (r.method !== 'GET' || !staticRoot || !dispatchPath.startsWith(prefix)) { return next(); }\n const rel = dispatchPath.slice(prefix.length).replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(staticRoot, rel);\n if (!filePath.startsWith(staticRoot + path.sep)) { return next(); }\n fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["import_better_call", "pkg", "createBetterCallRouter", "path", "fs"]
7
7
  }
@@ -183,6 +183,10 @@ export interface DualModeHelpers {
183
183
  export declare function dualModeEndpoints<E extends Record<string, Endpoint>>(endpoints: E, opts: {
184
184
  /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */
185
185
  catchAllKey?: keyof E;
186
- /** Build the express middleware given the pre-built routers + node handlers. */
187
- buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;
186
+ /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */
187
+ prefix?: string;
188
+ /** SPA dist dir — the default middleware dispatches catch-all requests only for files that exist here. */
189
+ staticDir?: string;
190
+ /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */
191
+ buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;
188
192
  }): ExpressMiddleware & E;
@@ -1,4 +1,6 @@
1
1
  // packages/core/src/router/index.ts
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import { createHash, timingSafeEqual } from "node:crypto";
3
5
  import { createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from "@colyseus/better-call";
4
6
  import { toNodeHandler, getRequest, setResponse } from "@colyseus/better-call/node";
@@ -141,7 +143,8 @@ function dualModeEndpoints(endpoints, opts) {
141
143
  ) : endpoints;
142
144
  const specificRouter = createRouter(specificEndpoints);
143
145
  const specificHandler = toNodeHandler(specificRouter.handler);
144
- const middleware = opts.buildMiddleware({
146
+ const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? "", opts.staticDir);
147
+ const middleware = buildMiddleware({
145
148
  specificRouter,
146
149
  specificHandler,
147
150
  fullRouter,
@@ -149,6 +152,49 @@ function dualModeEndpoints(endpoints, opts) {
149
152
  });
150
153
  return Object.assign(middleware, endpoints);
151
154
  }
155
+ function panelMiddleware(prefix, staticDir) {
156
+ const staticRoot = staticDir && path.resolve(staticDir);
157
+ return ({ specificRouter, specificHandler, fullHandler }) => (req, res, next) => {
158
+ const r = req;
159
+ const raw = r.url ?? "";
160
+ const q = raw.indexOf("?");
161
+ const url = q < 0 ? raw : raw.slice(0, q);
162
+ const query = q < 0 ? "" : raw.slice(q);
163
+ const dispatchPath = r.baseUrl ? prefix + url : url;
164
+ if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {
165
+ const original = (r.originalUrl ?? raw).split("?")[0];
166
+ if (r.method === "GET" && !original.endsWith("/")) {
167
+ res.writeHead(302, { location: `${original}/${query}` });
168
+ res.end();
169
+ return;
170
+ }
171
+ }
172
+ const dispatch = (handler) => {
173
+ const wrapped = Object.create(req, {
174
+ url: { value: dispatchPath + query, enumerable: true, configurable: true },
175
+ baseUrl: { value: "", enumerable: true, configurable: true },
176
+ originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true }
177
+ });
178
+ handler(wrapped, res).catch(next);
179
+ };
180
+ const route = specificRouter.findRoute(r.method ?? "GET", dispatchPath);
181
+ if (route && (!route.data?.path?.endsWith("/") || dispatchPath.endsWith("/"))) {
182
+ return dispatch(specificHandler);
183
+ }
184
+ if (r.method !== "GET" || !staticRoot || !dispatchPath.startsWith(prefix)) {
185
+ return next();
186
+ }
187
+ const rel = dispatchPath.slice(prefix.length).replace(/^\/+/, "");
188
+ if (!rel || rel.includes("..")) {
189
+ return next();
190
+ }
191
+ const filePath = path.resolve(staticRoot, rel);
192
+ if (!filePath.startsWith(staticRoot + path.sep)) {
193
+ return next();
194
+ }
195
+ fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());
196
+ };
197
+ }
152
198
  export {
153
199
  basicAuth,
154
200
  bindRouterToTransport,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/index.ts"],
4
- "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */\n prefix?: string;\n /** SPA dist dir \u2014 the default middleware dispatches catch-all requests only for files that exist here. */\n staticDir?: string;\n /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */\n buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? '', opts.staticDir);\n const middleware = buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n\n/**\n * Default express middleware for a single-prefix SPA panel (monitor,\n * playground). Express and the endpoint map use different coordinates: a path\n * mount strips its prefix into `req.baseUrl`, while the endpoints keep the\n * configured `prefix` baked into their paths. This rebases every request onto\n * the endpoint namespace, so the panel works at any mount path \u2014 `prefix` only\n * matters in router mode and at root mounts.\n */\nfunction panelMiddleware(prefix: string, staticDir?: string) {\n const staticRoot = staticDir && path.resolve(staticDir);\n return ({ specificRouter, specificHandler, fullHandler }: DualModeHelpers): ExpressMiddleware =>\n (req, res, next) => {\n const r = req as IncomingMessage & { baseUrl?: string; originalUrl?: string };\n const raw = r.url ?? '';\n const q = raw.indexOf('?');\n const url = q < 0 ? raw : raw.slice(0, q);\n const query = q < 0 ? '' : raw.slice(q);\n\n // Path mounts arrive stripped (`req.url` is mount-relative); root and\n // pathless mounts arrive unstripped and already match the endpoints.\n const dispatchPath = r.baseUrl ? prefix + url : url;\n\n // Canonicalize the index: the SPA references its assets relatively, so\n // they only resolve from the trailing-slash URL. The browser's address\n // is originalUrl \u2014 302 (not 301: browsers cache 301s across remounts).\n if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {\n const original = (r.originalUrl ?? raw).split('?')[0]!;\n if (r.method === 'GET' && !original.endsWith('/')) {\n res.writeHead(302, { location: `${original}/${query}` });\n res.end();\n return;\n }\n }\n\n const dispatch = (handler: NodeHandler) => {\n // better-call's getRequest resolves the path as baseUrl + url\n const wrapped = Object.create(req, {\n url: { value: dispatchPath + query, enumerable: true, configurable: true },\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true },\n });\n handler(wrapped as any, res).catch(next);\n };\n\n const route = specificRouter.findRoute(r.method ?? 'GET', dispatchPath);\n // rou3 normalizes trailing slashes in findRoute but processRequest\n // exact-matches \u2014 only trust the hit when the slashes agree.\n if (route && (!route.data?.path?.endsWith('/') || dispatchPath.endsWith('/'))) {\n return dispatch(specificHandler);\n }\n\n // Asset request \u2014 only delegate when the file exists on disk, so the\n // catch-all's SPA fallback can't mask sibling express routes.\n if (r.method !== 'GET' || !staticRoot || !dispatchPath.startsWith(prefix)) { return next(); }\n const rel = dispatchPath.slice(prefix.length).replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(staticRoot, rel);\n if (!filePath.startsWith(staticRoot + path.sep)) { return next(); }\n fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["createEndpoint", "createMiddleware"]
7
7
  }
@@ -1,3 +1,4 @@
1
+ const __cjsImportMetaUrl = require('node:url').pathToFileURL(__filename).href;
1
2
  "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/node.ts"],
4
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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",
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ const __cjsImportMetaUrl = require('node:url').pathToFileURL(__filename).href;
1
2
  "use strict";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/serializer/NoneSerializer.ts"],
4
4
  "sourcesContent": ["import type { Client } from '../Transport.ts';\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\n\nexport class NoneSerializer<T= any> implements Serializer<T> {\n public id: string = 'none';\n\n public reset(data: any) {}\n\n public getFullState(_client?: Client, _timing?: PatchTimingContext) {\n return null;\n }\n\n public applyPatches(_clients: Client[], _state: T, _timing?: PatchTimingContext): boolean {\n return false;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,IAAM,iBAAN,MAAsD;AAAA,EAAtD;AACL,SAAO,KAAa;AAAA;AAAA,EAEb,MAAM,MAAW;AAAA,EAAC;AAAA,EAElB,aAAa,SAAkB,SAA8B;AAClE,WAAO;AAAA,EACT;AAAA,EAEO,aAAa,UAAoB,QAAW,SAAuC;AACxF,WAAO;AAAA,EACT;AACF;",
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,IAAM,iBAAN,MAAsD;AAAA,EAAtD;AACL,SAAO,KAAa;AAAA;AAAA,EAEb,MAAM,MAAW;AAAA,EAAC;AAAA,EAElB,aAAa,SAAkB,SAA8B;AAClE,WAAO;AAAA,EACT;AAAA,EAEO,aAAa,UAAoB,QAAW,SAAuC;AACxF,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ const __cjsImportMetaUrl = require('node:url').pathToFileURL(__filename).href;
1
2
  "use strict";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -27,6 +28,7 @@ var import_shared_types = require("@colyseus/shared-types");
27
28
  var import_Transport = require("../Transport.cjs");
28
29
  var import_schema = require("@colyseus/schema");
29
30
  var import_Debug = require("../Debug.cjs");
31
+ var import_Logger = require("../Logger.cjs");
30
32
  var TIMED_PREFIX_SIZE = 8;
31
33
  function buildTimedFrame(encodedBody, sNow, inputSeq) {
32
34
  const schemaPayload = encodedBody.subarray(1);
@@ -48,6 +50,24 @@ function sendClientFrame(client, encodedBody, timing) {
48
50
  client.raw(encodedBody);
49
51
  }
50
52
  }
53
+ var UNRELIABLE_HEADER_SIZE = 2;
54
+ function stateHasUnreliableFields(context) {
55
+ for (const klass of context.schemas.keys()) {
56
+ const metadata = klass[Symbol.metadata];
57
+ if (metadata === void 0) {
58
+ continue;
59
+ }
60
+ for (let i = 0; i <= metadata[import_schema.$numFields]; i++) {
61
+ if (import_schema.Metadata.hasUnreliableAtIndex(metadata, i)) {
62
+ return true;
63
+ }
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+ function canReceiveUnreliable(client) {
69
+ return client.state === import_Transport.ClientState.JOINED && client.rawUnreliable !== void 0;
70
+ }
51
71
  var SHARED_VIEW = {};
52
72
  var SchemaSerializer = class {
53
73
  constructor() {
@@ -58,13 +78,24 @@ var SchemaSerializer = class {
58
78
  // TODO: make this optional. allocating a new buffer for each room may not be always necessary.
59
79
  this.fullEncodeBuffer = new Uint8Array(import_schema.Encoder.BUFFER_SIZE);
60
80
  this.sharedOffsetCache = { offset: 0 };
81
+ /** @see {@link Serializer.hasUnreliableFields} */
82
+ this.hasUnreliableFields = false;
83
+ /** Room-wide counter stamped into every unreliable frame so the client can
84
+ * drop reordered datagrams. Wraps at 65536 — see {@link ProtocolModifier.UNRELIABLE}. */
85
+ this.unreliableSeq = 0;
86
+ /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */
87
+ this.warnedNoUnreliableTransport = false;
61
88
  }
62
89
  reset(newState) {
63
90
  this.encoder = new import_schema.Encoder(newState);
64
91
  this.hasFilters = this.encoder.context.hasFilters;
92
+ this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);
65
93
  this.fullEncodeBuffer[0] = import_shared_types.Protocol.ROOM_STATE;
66
94
  if (this.hasFilters) {
67
95
  this.encodedViews = /* @__PURE__ */ new Map();
96
+ if (this.hasUnreliableFields) {
97
+ this.encodedUnreliableViews = /* @__PURE__ */ new Map();
98
+ }
68
99
  }
69
100
  }
70
101
  getFullState(client, timing) {
@@ -161,6 +192,73 @@ var SchemaSerializer = class {
161
192
  }
162
193
  return true;
163
194
  }
195
+ /**
196
+ * Encode and send the UNRELIABLE channel — the `@unreliable` fields, which
197
+ * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}
198
+ * (a WebTransport datagram); clients whose transport has no unreliable
199
+ * channel are skipped entirely.
200
+ *
201
+ * Runs on its own cadence, independent of `applyPatches`. Only ever called on
202
+ * rooms where {@link hasUnreliableFields} is true.
203
+ */
204
+ applyUnreliablePatches(clients) {
205
+ if (!this.encoder.hasUnreliableChanges) {
206
+ return false;
207
+ }
208
+ this.needFullEncode = true;
209
+ if (!clients.some(canReceiveUnreliable)) {
210
+ this.warnNoUnreliableTransport(clients);
211
+ this.encoder.discardUnreliableChanges();
212
+ return false;
213
+ }
214
+ this.unreliableSeq = this.unreliableSeq + 1 & 65535;
215
+ const it = { offset: 1 + UNRELIABLE_HEADER_SIZE };
216
+ this.encoder.sharedBuffer[0] = import_shared_types.Protocol.ROOM_STATE_PATCH | import_shared_types.ProtocolModifier.UNRELIABLE;
217
+ import_schema.encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });
218
+ const encodedChanges = this.encoder.encodeUnreliable(it);
219
+ let numClients = clients.length;
220
+ if (!this.hasFilters) {
221
+ while (numClients--) {
222
+ const client = clients[numClients];
223
+ if (canReceiveUnreliable(client)) {
224
+ client.rawUnreliable(encodedChanges);
225
+ }
226
+ }
227
+ } else {
228
+ const sharedOffset = it.offset;
229
+ while (numClients--) {
230
+ const client = clients[numClients];
231
+ if (!canReceiveUnreliable(client)) {
232
+ continue;
233
+ }
234
+ const view = client.view || SHARED_VIEW;
235
+ let encodedView = this.encodedUnreliableViews.get(view);
236
+ if (encodedView === void 0) {
237
+ encodedView = view === SHARED_VIEW ? encodedChanges : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);
238
+ this.encodedUnreliableViews.set(view, encodedView);
239
+ }
240
+ client.rawUnreliable(encodedView);
241
+ }
242
+ this.encodedUnreliableViews.clear();
243
+ }
244
+ this.encoder.discardUnreliableChanges();
245
+ return true;
246
+ }
247
+ /**
248
+ * The state declares `@unreliable` fields but not one connected client can
249
+ * receive them — every WebSocket transport lacks a datagram channel. Those
250
+ * fields keep their join-time value and never update, which is silent
251
+ * otherwise. Warns once per room.
252
+ */
253
+ warnNoUnreliableTransport(clients) {
254
+ if (this.warnedNoUnreliableTransport || clients.length === 0) {
255
+ return;
256
+ }
257
+ this.warnedNoUnreliableTransport = true;
258
+ import_Logger.logger.warn(
259
+ "@colyseus/core: state has @unreliable fields, but no connected client's transport provides an unreliable channel \u2014 those fields will not be patched. Use @colyseus/h3-transport (WebTransport) to enable them."
260
+ );
261
+ }
164
262
  handshake() {
165
263
  if (!this.handshakeCache) {
166
264
  this.handshakeCache = this.encoder.state && import_schema.Reflection.encode(this.encoder);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/serializer/SchemaSerializer.ts"],
4
- "sourcesContent": ["import { Protocol, ProtocolModifier } from '@colyseus/shared-types';\n\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\nimport { type Client, type ClientPrivate, ClientState } from '../Transport.ts';\n\nimport { type Iterator, encode, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';\nimport { debugPatch } from '../Debug.ts';\n\n/**\n * Size of the {@link ProtocolModifier.TIMED} prefix that follows the\n * protocol byte:\n * `[uint32 sNow][uint32 inputSeq]`\n *\n * - `sNow` \u2014 server clock as **milliseconds since room start**\n * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,\n * language-agnostic integer (C/C#/Lua have no `performance.now()`); the\n * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the\n * client's reconstructed `serverNow()` stays in phase. Wraps at u32\n * (~49.7 days uptime) \u2014 irrelevant per session. Drives the client clock\n * offset / `serverNow()`.\n * - `inputSeq` \u2014 seq VALUE of the last input CONSUMED into the authoritative\n * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:\n * equals the consumed count (inputs are sequenced implicitly by receive\n * order). Unreliable: the framework wire seq, so a fully-dropped input\n * doesn't make the ack lag the client's sent seq by the lost count. This\n * single number is the canonical reconciliation ack (\"the sequence number\n * of the last input the server processed\"); the client prunes its\n * pending-input buffer against it AND derives RTT from its round-trip\n * (`now \u2212 sendTime(inputSeq)`). No separate received-seq / received-time \u2014\n * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.\n */\nconst TIMED_PREFIX_SIZE = 8;\n\n/**\n * Construct a per-recipient TIMED buffer:\n * `[code|TIMED][sNow][inputSeq][...schema bytes]`\n *\n * `encodedBody` is the shared encoded buffer whose byte 0 already holds the\n * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the\n * schema payload. We construct a fresh per-client Buffer so the transport\n * never sees a buffer that might mutate between queue and send.\n *\n * Uses the schema `encode.*` helpers to keep the wire layout symmetric with\n * the SDK's `decode.*`-driven reader.\n */\nfunction buildTimedFrame(\n encodedBody: Uint8Array,\n sNow: number,\n inputSeq: number,\n): Buffer {\n const schemaPayload = encodedBody.subarray(1);\n const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);\n out[0] = encodedBody[0] | ProtocolModifier.TIMED;\n const it: Iterator = { offset: 1 };\n encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)\n encode.uint32(out, inputSeq >>> 0, it);\n out.set(schemaPayload, it.offset);\n return out;\n}\n\n/** Seq VALUE of the last input consumed from the client's buffer \u2014 the\n * reconciliation ack (and RTT correlation key). Reliable: equals the consumed\n * count; unreliable: the framework wire seq (so packet loss doesn't skew it).\n * 0 when the room doesn't buffer input. */\nfunction clientProcessedInputSeq(client: Client): number {\n return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;\n}\n\n/**\n * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,\n * else the shared `encodedBody` passed straight through (no per-client copy). Any\n * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here\n * \u2014 they go out standalone right after the patch via `Room._flushPendingClientFrames`.\n */\nfunction sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {\n if (timing) {\n client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));\n } else {\n client.raw(encodedBody);\n }\n}\n\nconst SHARED_VIEW = {};\n\nexport class SchemaSerializer<T extends Schema> implements Serializer<T> {\n public id = 'schema';\n\n protected encoder: Encoder<T>;\n protected hasFilters: boolean = false;\n\n protected handshakeCache: Uint8Array;\n\n // flag to avoid re-encoding full state if no changes were made\n protected needFullEncode: boolean = true;\n\n // TODO: make this optional. allocating a new buffer for each room may not be always necessary.\n protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);\n protected fullEncodeCache: Uint8Array;\n protected sharedOffsetCache: Iterator = { offset: 0 };\n\n protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n public reset(newState: T & Schema) {\n this.encoder = new Encoder(newState);\n this.hasFilters = this.encoder.context.hasFilters;\n\n // cache ROOM_STATE byte as part of the encoded buffer\n this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;\n\n if (this.hasFilters) {\n this.encodedViews = new Map();\n }\n }\n\n public getFullState(client?: Client, timing?: PatchTimingContext) {\n if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {\n this.sharedOffsetCache = { offset: 1 };\n this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);\n this.needFullEncode = false;\n }\n\n const body = (this.hasFilters && client?.view)\n ? this.encoder.encodeAllView(\n client.view,\n this.sharedOffsetCache.offset,\n { ...this.sharedOffsetCache },\n this.fullEncodeBuffer\n )\n : this.fullEncodeCache;\n\n if (timing && client) {\n return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));\n }\n return body;\n }\n\n public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {\n let numClients = clients.length;\n\n if (numClients === 0) {\n if (this.encoder.hasChanges) {\n // if there are changes but no clients, we need to encode full state on next patch\n this.needFullEncode = true;\n }\n // skip patching and clear changes\n this.encoder.discardChanges();\n return false;\n }\n\n if (!this.encoder.hasChanges) {\n\n // check if views have changes (manual add() or remove() items)\n if (this.hasFilters) {\n //\n // FIXME: refactor this to avoid duplicating code.\n //\n // it's probably better to have 2 different 'applyPatches' methods.\n // (one for handling state with filters, and another for handling state without filters)\n //\n const clientsWithViewChange = clients.filter((client) => {\n return client.state === ClientState.JOINED && client.view?.changes.size > 0\n });\n\n if (clientsWithViewChange.length > 0) {\n const it: Iterator = { offset: 1 };\n\n const sharedOffset = it.offset;\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n clientsWithViewChange.forEach((client) => {\n const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);\n sendClientFrame(client, encodedView, timing);\n });\n }\n }\n\n // Heartbeat: `defineInput()` rooms still need periodic timing samples\n // to keep client clock offset / RTT estimates fresh during quiet\n // ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema\n // body \u2014 1 byte protocol + 8 bytes prefix.\n // No-op for rooms without `defineInput()` (timing is undefined).\n if (timing) {\n const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);\n for (let i = 0; i < clients.length; i++) {\n const client = clients[i];\n if (client.state !== ClientState.JOINED) continue;\n sendClientFrame(client, heartbeatBody, timing);\n }\n }\n\n // skip patching state if:\n // - no clients are connected\n // - no changes were made\n // - no \"filtered changes\" were made when using filters\n return false;\n }\n\n this.needFullEncode = true;\n\n // dump changes for patch debugging\n if (debugPatch.enabled) {\n (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);\n }\n\n // get patch bytes\n const it: Iterator = { offset: 1 };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encode(it);\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n sendClientFrame(client, encodedChanges, timing);\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n // encode state multiple times, for each client\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeView(client.view, sharedOffset, it);\n this.encodedViews.set(view, encodedView);\n }\n\n sendClientFrame(client, encodedView, timing);\n }\n\n // clear views\n this.encodedViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardChanges();\n\n // debug patches\n if (debugPatch.enabled) {\n debugPatch(\n '%d bytes sent to %d clients, %j',\n encodedChanges.length,\n clients.length,\n (debugPatch as any).dumpChanges,\n );\n }\n\n return true;\n }\n\n public handshake() {\n /**\n * Cache handshake to avoid encoding it for each client joining\n */\n if (!this.handshakeCache) {\n //\n // TODO: re-use handshake buffer for all rooms of same type (?)\n //\n this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));\n }\n\n return this.handshakeCache;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Protocol, ProtocolModifier } from '@colyseus/shared-types';\n\nimport type { PatchTimingContext, Serializer } from './Serializer.ts';\nimport { type Client, type ClientPrivate, ClientState } from '../Transport.ts';\n\nimport { type Iterator, $numFields, encode, Encoder, dumpChanges, Metadata, Reflection, Schema, StateView, TypeContext } from '@colyseus/schema';\nimport { debugPatch } from '../Debug.ts';\nimport { logger } from '../Logger.ts';\n\n/**\n * Size of the {@link ProtocolModifier.TIMED} prefix that follows the\n * protocol byte:\n * `[uint32 sNow][uint32 inputSeq]`\n *\n * - `sNow` \u2014 server clock as **milliseconds since room start**\n * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,\n * language-agnostic integer (C/C#/Lua have no `performance.now()`); the\n * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the\n * client's reconstructed `serverNow()` stays in phase. Wraps at u32\n * (~49.7 days uptime) \u2014 irrelevant per session. Drives the client clock\n * offset / `serverNow()`.\n * - `inputSeq` \u2014 seq VALUE of the last input CONSUMED into the authoritative\n * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:\n * equals the consumed count (inputs are sequenced implicitly by receive\n * order). Unreliable: the framework wire seq, so a fully-dropped input\n * doesn't make the ack lag the client's sent seq by the lost count. This\n * single number is the canonical reconciliation ack (\"the sequence number\n * of the last input the server processed\"); the client prunes its\n * pending-input buffer against it AND derives RTT from its round-trip\n * (`now \u2212 sendTime(inputSeq)`). No separate received-seq / received-time \u2014\n * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.\n */\nconst TIMED_PREFIX_SIZE = 8;\n\n/**\n * Construct a per-recipient TIMED buffer:\n * `[code|TIMED][sNow][inputSeq][...schema bytes]`\n *\n * `encodedBody` is the shared encoded buffer whose byte 0 already holds the\n * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the\n * schema payload. We construct a fresh per-client Buffer so the transport\n * never sees a buffer that might mutate between queue and send.\n *\n * Uses the schema `encode.*` helpers to keep the wire layout symmetric with\n * the SDK's `decode.*`-driven reader.\n */\nfunction buildTimedFrame(\n encodedBody: Uint8Array,\n sNow: number,\n inputSeq: number,\n): Buffer {\n const schemaPayload = encodedBody.subarray(1);\n const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);\n out[0] = encodedBody[0] | ProtocolModifier.TIMED;\n const it: Iterator = { offset: 1 };\n encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)\n encode.uint32(out, inputSeq >>> 0, it);\n out.set(schemaPayload, it.offset);\n return out;\n}\n\n/** Seq VALUE of the last input consumed from the client's buffer \u2014 the\n * reconciliation ack (and RTT correlation key). Reliable: equals the consumed\n * count; unreliable: the framework wire seq (so packet loss doesn't skew it).\n * 0 when the room doesn't buffer input. */\nfunction clientProcessedInputSeq(client: Client): number {\n return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;\n}\n\n/**\n * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,\n * else the shared `encodedBody` passed straight through (no per-client copy). Any\n * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here\n * \u2014 they go out standalone right after the patch via `Room._flushPendingClientFrames`.\n */\nfunction sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {\n if (timing) {\n client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));\n } else {\n client.raw(encodedBody);\n }\n}\n\n/**\n * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the\n * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for\n * the wrap-safe freshness rule the client applies to `seq`.\n */\nconst UNRELIABLE_HEADER_SIZE = 2;\n\n/**\n * Does any Schema class reachable from the state root declare an `@unreliable`\n * field? Walked once per room, in {@link SchemaSerializer.reset} \u2014 the result\n * gates whether the unreliable channel is wired up at all, so rooms that don't\n * use it never reach any of the code below.\n */\nfunction stateHasUnreliableFields(context: TypeContext): boolean {\n for (const klass of context.schemas.keys()) {\n const metadata = klass[Symbol.metadata];\n if (metadata === undefined) { continue; }\n\n // `$numFields` is the last index, inclusive (matches `Metadata.getFields`).\n for (let i = 0; i <= metadata[$numFields]; i++) {\n if (Metadata.hasUnreliableAtIndex(metadata, i)) { return true; }\n }\n }\n return false;\n}\n\n/** Can this client receive the unreliable channel right now? Its transport\n * exposing {@link Client.rawUnreliable} IS the capability check. */\nfunction canReceiveUnreliable(client: Client): boolean {\n return client.state === ClientState.JOINED && client.rawUnreliable !== undefined;\n}\n\nconst SHARED_VIEW = {};\n\nexport class SchemaSerializer<T extends Schema> implements Serializer<T> {\n public id = 'schema';\n\n protected encoder: Encoder<T>;\n protected hasFilters: boolean = false;\n\n protected handshakeCache: Uint8Array;\n\n // flag to avoid re-encoding full state if no changes were made\n protected needFullEncode: boolean = true;\n\n // TODO: make this optional. allocating a new buffer for each room may not be always necessary.\n protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);\n protected fullEncodeCache: Uint8Array;\n protected sharedOffsetCache: Iterator = { offset: 0 };\n\n protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** @see {@link Serializer.hasUnreliableFields} */\n public hasUnreliableFields: boolean = false;\n\n /** Room-wide counter stamped into every unreliable frame so the client can\n * drop reordered datagrams. Wraps at 65536 \u2014 see {@link ProtocolModifier.UNRELIABLE}. */\n protected unreliableSeq: number = 0;\n\n protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;\n\n /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */\n protected warnedNoUnreliableTransport = false;\n\n public reset(newState: T & Schema) {\n this.encoder = new Encoder(newState);\n this.hasFilters = this.encoder.context.hasFilters;\n this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);\n\n // cache ROOM_STATE byte as part of the encoded buffer\n this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;\n\n if (this.hasFilters) {\n this.encodedViews = new Map();\n\n if (this.hasUnreliableFields) {\n // Separate from `encodedViews`: the two channels encode on independent\n // cadences, so a reliable tick's cache must never be served to one.\n this.encodedUnreliableViews = new Map();\n }\n }\n }\n\n public getFullState(client?: Client, timing?: PatchTimingContext) {\n if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {\n this.sharedOffsetCache = { offset: 1 };\n this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);\n this.needFullEncode = false;\n }\n\n const body = (this.hasFilters && client?.view)\n ? this.encoder.encodeAllView(\n client.view,\n this.sharedOffsetCache.offset,\n { ...this.sharedOffsetCache },\n this.fullEncodeBuffer\n )\n : this.fullEncodeCache;\n\n if (timing && client) {\n return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));\n }\n return body;\n }\n\n public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {\n let numClients = clients.length;\n\n if (numClients === 0) {\n if (this.encoder.hasChanges) {\n // if there are changes but no clients, we need to encode full state on next patch\n this.needFullEncode = true;\n }\n // skip patching and clear changes\n this.encoder.discardChanges();\n return false;\n }\n\n if (!this.encoder.hasChanges) {\n\n // check if views have changes (manual add() or remove() items)\n if (this.hasFilters) {\n //\n // FIXME: refactor this to avoid duplicating code.\n //\n // it's probably better to have 2 different 'applyPatches' methods.\n // (one for handling state with filters, and another for handling state without filters)\n //\n const clientsWithViewChange = clients.filter((client) => {\n return client.state === ClientState.JOINED && client.view?.changes.size > 0\n });\n\n if (clientsWithViewChange.length > 0) {\n const it: Iterator = { offset: 1 };\n\n const sharedOffset = it.offset;\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n clientsWithViewChange.forEach((client) => {\n const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);\n sendClientFrame(client, encodedView, timing);\n });\n }\n }\n\n // Heartbeat: `defineInput()` rooms still need periodic timing samples\n // to keep client clock offset / RTT estimates fresh during quiet\n // ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema\n // body \u2014 1 byte protocol + 8 bytes prefix.\n // No-op for rooms without `defineInput()` (timing is undefined).\n if (timing) {\n const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);\n for (let i = 0; i < clients.length; i++) {\n const client = clients[i];\n if (client.state !== ClientState.JOINED) continue;\n sendClientFrame(client, heartbeatBody, timing);\n }\n }\n\n // skip patching state if:\n // - no clients are connected\n // - no changes were made\n // - no \"filtered changes\" were made when using filters\n return false;\n }\n\n this.needFullEncode = true;\n\n // dump changes for patch debugging\n if (debugPatch.enabled) {\n (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);\n }\n\n // get patch bytes\n const it: Iterator = { offset: 1 };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encode(it);\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n sendClientFrame(client, encodedChanges, timing);\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n // encode state multiple times, for each client\n while (numClients--) {\n const client = clients[numClients];\n\n //\n // FIXME: avoid this check for each client\n //\n if (client.state !== ClientState.JOINED) {\n continue;\n }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeView(client.view, sharedOffset, it);\n this.encodedViews.set(view, encodedView);\n }\n\n sendClientFrame(client, encodedView, timing);\n }\n\n // clear views\n this.encodedViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardChanges();\n\n // debug patches\n if (debugPatch.enabled) {\n debugPatch(\n '%d bytes sent to %d clients, %j',\n encodedChanges.length,\n clients.length,\n (debugPatch as any).dumpChanges,\n );\n }\n\n return true;\n }\n\n /**\n * Encode and send the UNRELIABLE channel \u2014 the `@unreliable` fields, which\n * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}\n * (a WebTransport datagram); clients whose transport has no unreliable\n * channel are skipped entirely.\n *\n * Runs on its own cadence, independent of `applyPatches`. Only ever called on\n * rooms where {@link hasUnreliableFields} is true.\n */\n public applyUnreliablePatches(clients: Client[]): boolean {\n if (!this.encoder.hasUnreliableChanges) { return false; }\n\n // These mutations are visible to `encodeAll`, so the cached full state is\n // stale \u2014 even though `root.changes` (the reliable queue) is empty and\n // `getFullState`'s own guard would therefore miss it.\n this.needFullEncode = true;\n\n if (!clients.some(canReceiveUnreliable)) {\n this.warnNoUnreliableTransport(clients);\n // Drain regardless: the queue is not self-clearing, and an un-discarded\n // tick would grow it without bound.\n this.encoder.discardUnreliableChanges();\n return false;\n }\n\n this.unreliableSeq = (this.unreliableSeq + 1) & 0xFFFF;\n\n // Stamp the header before encoding, as the reliable path does. Safe across\n // a buffer overflow: `Encoder._resizeBuffer` copies the old bytes forward.\n const it: Iterator = { offset: 1 + UNRELIABLE_HEADER_SIZE };\n this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH | ProtocolModifier.UNRELIABLE;\n encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });\n\n // encode changes once, for all clients\n const encodedChanges = this.encoder.encodeUnreliable(it);\n\n let numClients = clients.length;\n\n if (!this.hasFilters) {\n while (numClients--) {\n const client = clients[numClients];\n if (canReceiveUnreliable(client)) { client.rawUnreliable!(encodedChanges); }\n }\n\n } else {\n // cache shared offset\n const sharedOffset = it.offset;\n\n while (numClients--) {\n const client = clients[numClients];\n if (!canReceiveUnreliable(client)) { continue; }\n\n const view = client.view || SHARED_VIEW;\n\n let encodedView = this.encodedUnreliableViews.get(view);\n\n // allow to pass the same encoded view for multiple clients\n if (encodedView === undefined) {\n encodedView = (view === SHARED_VIEW)\n ? encodedChanges\n : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);\n this.encodedUnreliableViews.set(view, encodedView);\n }\n\n client.rawUnreliable!(encodedView);\n }\n\n // clear views\n this.encodedUnreliableViews.clear();\n }\n\n // discard changes after sending\n this.encoder.discardUnreliableChanges();\n\n return true;\n }\n\n /**\n * The state declares `@unreliable` fields but not one connected client can\n * receive them \u2014 every WebSocket transport lacks a datagram channel. Those\n * fields keep their join-time value and never update, which is silent\n * otherwise. Warns once per room.\n */\n private warnNoUnreliableTransport(clients: Client[]) {\n if (this.warnedNoUnreliableTransport || clients.length === 0) { return; }\n this.warnedNoUnreliableTransport = true;\n logger.warn(\n \"@colyseus/core: state has @unreliable fields, but no connected client's transport\" +\n \" provides an unreliable channel \u2014 those fields will not be patched.\" +\n \" Use @colyseus/h3-transport (WebTransport) to enable them.\"\n );\n }\n\n public handshake() {\n /**\n * Cache handshake to avoid encoding it for each client joining\n */\n if (!this.handshakeCache) {\n //\n // TODO: re-use handshake buffer for all rooms of same type (?)\n //\n this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));\n }\n\n return this.handshakeCache;\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["it"]
7
7
  }
@@ -12,9 +12,34 @@ export declare class SchemaSerializer<T extends Schema> implements Serializer<T>
12
12
  protected fullEncodeCache: Uint8Array;
13
13
  protected sharedOffsetCache: Iterator;
14
14
  protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
15
+ /** @see {@link Serializer.hasUnreliableFields} */
16
+ hasUnreliableFields: boolean;
17
+ /** Room-wide counter stamped into every unreliable frame so the client can
18
+ * drop reordered datagrams. Wraps at 65536 — see {@link ProtocolModifier.UNRELIABLE}. */
19
+ protected unreliableSeq: number;
20
+ protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
21
+ /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */
22
+ protected warnedNoUnreliableTransport: boolean;
15
23
  reset(newState: T & Schema): void;
16
24
  getFullState(client?: Client, timing?: PatchTimingContext): Uint8Array<ArrayBufferLike> | Buffer<ArrayBufferLike>;
17
25
  applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext): boolean;
26
+ /**
27
+ * Encode and send the UNRELIABLE channel — the `@unreliable` fields, which
28
+ * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}
29
+ * (a WebTransport datagram); clients whose transport has no unreliable
30
+ * channel are skipped entirely.
31
+ *
32
+ * Runs on its own cadence, independent of `applyPatches`. Only ever called on
33
+ * rooms where {@link hasUnreliableFields} is true.
34
+ */
35
+ applyUnreliablePatches(clients: Client[]): boolean;
36
+ /**
37
+ * The state declares `@unreliable` fields but not one connected client can
38
+ * receive them — every WebSocket transport lacks a datagram channel. Those
39
+ * fields keep their join-time value and never update, which is silent
40
+ * otherwise. Warns once per room.
41
+ */
42
+ private warnNoUnreliableTransport;
18
43
  handshake(): Uint8Array<ArrayBufferLike>;
19
44
  }
20
45
  export {};