@colyseus/core 0.17.42 → 0.17.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/Server.cjs CHANGED
@@ -43,12 +43,11 @@ var matchMaker = __toESM(require("./MatchMaker.cjs"), 1);
43
43
  var import_RegisteredHandler = require("./matchmaker/RegisteredHandler.cjs");
44
44
  var import_Room = require("./Room.cjs");
45
45
  var import_Utils = require("./utils/Utils.cjs");
46
- var import_LocalPresence = require("./presence/LocalPresence.cjs");
47
- var import_LocalDriver = require("./matchmaker/LocalDriver/LocalDriver.cjs");
48
46
  var import_Transport = require("./Transport.cjs");
49
47
  var import_Logger = require("./Logger.cjs");
50
48
  var import_DevMode = require("./utils/DevMode.cjs");
51
49
  var import_router = require("./router/index.cjs");
50
+ var import_node = require("./router/node.cjs");
52
51
  var import_shared_types = require("@colyseus/shared-types");
53
52
  var import_default_routes = require("./router/default_routes.cjs");
54
53
  var Server = class {
@@ -62,17 +61,18 @@ var Server = class {
62
61
  greet: greet2 = true
63
62
  } = options;
64
63
  (0, import_DevMode.setDevMode)(options.devMode === true);
65
- this.presence = options.presence || new import_LocalPresence.LocalPresence();
66
- this.driver = options.driver || new import_LocalDriver.LocalDriver();
67
64
  this.options = options;
68
65
  this.greet = greet2;
69
66
  this.attach(options);
70
67
  matchMaker.setup(
71
- this.presence,
72
- this.driver,
68
+ options.presence,
69
+ options.driver,
73
70
  options.publicAddress,
74
71
  options.selectProcessIdToCreateRoom
75
- );
72
+ ).then(() => {
73
+ this.presence = matchMaker.presence;
74
+ this.driver = matchMaker.driver;
75
+ });
76
76
  if (gracefullyShutdown2) {
77
77
  (0, import_Utils.registerGracefulShutdown)((err) => this.gracefullyShutdown(true, err));
78
78
  }
@@ -120,17 +120,11 @@ var Server = class {
120
120
  }
121
121
  await this._onTransportReady;
122
122
  return new Promise((resolve, reject) => {
123
- (0, import_Transport.setTransport)(this.transport);
124
123
  this.transport.listen(port, hostname, backlog, (err) => {
125
124
  if (this.transport.server) {
126
125
  this.transport.server.on("error", (err2) => reject(err2));
127
126
  }
128
- if (!this.router) {
129
- this.router = (0, import_default_routes.getDefaultRouter)();
130
- } else {
131
- this.router = this.router.extend({ ...(0, import_default_routes.getDefaultRouter)().endpoints });
132
- }
133
- (0, import_router.bindRouterToTransport)(this.transport, this.router, this.options.express !== void 0);
127
+ this.bindRoutes();
134
128
  if (listeningListener) {
135
129
  listeningListener(err);
136
130
  }
@@ -142,6 +136,64 @@ var Server = class {
142
136
  });
143
137
  });
144
138
  }
139
+ /**
140
+ * Prepare matchmaking and HTTP routes, then return the underlying
141
+ * `http.Server` *without* binding to a port.
142
+ *
143
+ * Use this on serverless platforms that consume an exported HTTP server
144
+ * instead of a listening one. Vercel, for example, drives the request handler
145
+ * of a default-exported server (its Express/Hono WebSocket examples use
146
+ * `export default server`), whereas calling `listen()` there selects Vercel's
147
+ * "captured server" path, which does not invoke Express-style app handlers.
148
+ *
149
+ * The transport must be created with an `http.Server` so it can be exported.
150
+ * Vercel additionally needs `package.json` `"main"` pointing at the entrypoint:
151
+ *
152
+ * ```ts
153
+ * import { createServer } from "node:http";
154
+ * const httpServer = createServer();
155
+ * const server = new Server({
156
+ * transport: new WebSocketTransport({ server: httpServer }),
157
+ * express: (app) => { app.get("/hello", (req, res) => res.json({ ok: true })); },
158
+ * });
159
+ * server.define("my_room", MyRoom);
160
+ * export default await server.serverless(); // no listen()
161
+ * ```
162
+ */
163
+ async serverless() {
164
+ if (this.options.beforeListen) {
165
+ await this.options.beforeListen();
166
+ }
167
+ await this._onTransportReady;
168
+ const server = this.transport.server;
169
+ if (!server) {
170
+ throw new Error("serverless() requires a transport backed by an http.Server (pass `{ server }` to the transport).");
171
+ }
172
+ this.bindRoutes();
173
+ (0, import_node.prereadRequestBodies)(server);
174
+ await matchMaker.accept(this.options.isStandaloneMatchMaker);
175
+ if (this.greet) {
176
+ (0, import_greeting_banner.greet)();
177
+ }
178
+ return server;
179
+ }
180
+ /**
181
+ * Set the active transport globally and bind the matchmaking/HTTP routes.
182
+ * Shared by {@link listen} and {@link serverless}.
183
+ *
184
+ * `setTransport()` runs here rather than before `transport.listen()`: the
185
+ * global transport is only read by the matchmaking route handler, which can't
186
+ * run until `bindRouterToTransport()` below registers the route.
187
+ */
188
+ bindRoutes() {
189
+ (0, import_Transport.setTransport)(this.transport);
190
+ if (!this.router) {
191
+ this.router = (0, import_default_routes.getDefaultRouter)();
192
+ } else {
193
+ this.router = this.router.extend({ ...(0, import_default_routes.getDefaultRouter)().endpoints });
194
+ }
195
+ (0, import_router.bindRouterToTransport)(this.transport, this.router, this.options.express !== void 0);
196
+ }
145
197
  define(nameOrHandler, handlerOrOptions, defaultOptions) {
146
198
  const name = typeof nameOrHandler === "string" ? nameOrHandler : nameOrHandler.name;
147
199
  const roomClass = typeof nameOrHandler === "string" ? handlerOrOptions : nameOrHandler;
@@ -163,8 +215,8 @@ var Server = class {
163
215
  await this.onBeforeShutdownCallback();
164
216
  await matchMaker.gracefullyShutdown();
165
217
  this.transport.shutdown();
166
- this.presence.shutdown();
167
- await this.driver.shutdown();
218
+ this.presence?.shutdown();
219
+ await this.driver?.shutdown();
168
220
  await this.onShutdownCallback();
169
221
  } catch (e) {
170
222
  (0, import_Debug.debugAndPrintError)(`error during shutdown: ${e}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/Server.ts"],
4
- "sourcesContent": ["import { greet } from \"@colyseus/greeting-banner\";\nimport type express from 'express';\n\nimport { debugAndPrintError } from './Debug.ts';\nimport * as matchMaker from './MatchMaker.ts';\nimport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\n\nimport { type OnCreateOptions, Room } from './Room.ts';\nimport { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './utils/Utils.ts';\n\nimport type { Presence } from \"./presence/Presence.ts\";\nimport { LocalPresence } from './presence/LocalPresence.ts';\nimport { LocalDriver } from './matchmaker/LocalDriver/LocalDriver.ts';\n\nimport { setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport } from './router/index.ts';\nimport { type SDKTypes as SharedSDKTypes } from '@colyseus/shared-types';\nimport { getDefaultRouter } from './router/default_routes.ts';\n\nexport type ServerOptions = {\n publicAddress?: string,\n presence?: Presence,\n driver?: matchMaker.MatchMakerDriver,\n transport?: Transport,\n gracefullyShutdown?: boolean,\n logger?: any;\n\n /**\n * Optional callback to execute before the server listens.\n * This is useful for example to connect into a database or other services before the server listens.\n */\n beforeListen?: () => Promise<void> | void,\n\n /**\n * Optional callback to configure Express routes.\n * When provided, the transport layer will initialize an Express-compatible app\n * and pass it to this callback for custom route configuration.\n *\n * For uWebSockets transport, this uses the uwebsockets-express module.\n */\n express?: (app: express.Application) => Promise<void> | void,\n\n /**\n * Custom function to determine which process should handle room creation.\n * Default: assign new rooms the process with least amount of rooms created\n */\n selectProcessIdToCreateRoom?: matchMaker.SelectProcessIdCallback;\n\n /**\n * Whether this process is running as a standalone match-maker or not. (default: false)\n * When enabled, this process will not spawn rooms and will only be responsible for matchmaking.\n */\n isStandaloneMatchMaker?: boolean; \n\n /**\n * If enabled, rooms are going to be restored in the server-side upon restart,\n * clients are going to automatically re-connect when server reboots.\n *\n * Beware of \"schema mismatch\" issues. When updating Schema structures and\n * reloading existing data, you may see \"schema mismatch\" errors in the\n * client-side.\n *\n * (This operation is costly and should not be used in a production\n * environment)\n */\n devMode?: boolean,\n\n /**\n * Display greeting message on server start.\n * Default: true\n */\n greet?: boolean,\n};\n\n/**\n * Exposed types for the client-side SDK.\n * Re-exported from @colyseus/shared-types with specific type constraints.\n */\nexport interface SDKTypes<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> extends SharedSDKTypes<RoomTypes, Routes> {}\n\nexport class Server<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> implements SDKTypes<RoomTypes, Routes> {\n '~rooms': RoomTypes;\n '~routes': Routes;\n\n public transport: Transport;\n public router: Routes;\n public options: ServerOptions;\n\n protected presence: Presence;\n protected driver: matchMaker.MatchMakerDriver;\n\n protected port: number | string;\n protected greet: boolean;\n\n protected _onTransportReady = new Deferred<Transport>();\n\n private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n constructor(options: ServerOptions = {}) {\n const {\n gracefullyShutdown = true,\n greet = true\n } = options;\n\n setDevMode(options.devMode === true);\n\n this.presence = options.presence || new LocalPresence();\n this.driver = options.driver || new LocalDriver();\n this.options = options;\n this.greet = greet;\n\n this.attach(options);\n\n matchMaker.setup(\n this.presence,\n this.driver,\n options.publicAddress,\n options.selectProcessIdToCreateRoom,\n );\n\n if (gracefullyShutdown) {\n registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));\n }\n\n if (options.logger) {\n setLogger(options.logger);\n }\n }\n\n public async attach(options: ServerOptions) {\n this.transport = options.transport || await this.getDefaultTransport(options);\n\n // Initialize Express if callback is provided\n if (options.express && this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await options.express(expressApp);\n }\n\n // Resolve the promise when the transport is ready\n this._onTransportReady.resolve(this.transport);\n }\n\n /**\n * Bind the server into the port specified.\n *\n * @param port - Port number or Unix socket path\n * @param hostname\n * @param backlog\n * @param listeningListener\n */\n public async listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: Function) {\n if (this.options.beforeListen) {\n await this.options.beforeListen();\n }\n\n //\n // if Colyseus Cloud is detected, use @colyseus/tools to listen\n //\n if (process.env.COLYSEUS_CLOUD !== undefined ) {\n if (typeof(hostname) === \"number\") {\n //\n // workaround, @colyseus/tools calls server.listen() again with the port as a string\n //\n hostname = undefined;\n\n } else {\n try {\n return (await dynamicImport(\"@colyseus/tools\")).listen(this);\n } catch (error) {\n const err = new Error(\"Please install @colyseus/tools to be able to host on Colyseus Cloud.\");\n err.cause = error;\n throw err;\n }\n }\n }\n\n //\n // otherwise, listen on the port directly\n //\n this.port = port;\n\n //\n // Make sure matchmaker is ready before accepting connections\n // (isDevMode: matchmaker may take extra milliseconds to restore the rooms)\n //\n await matchMaker.accept(this.options.isStandaloneMatchMaker);\n\n /**\n * Greetings!\n */\n if (this.greet) {\n greet();\n }\n\n // Wait for the transport to be ready\n await this._onTransportReady;\n\n return new Promise<void>((resolve, reject) => {\n // TODO: refactor me!\n // set transport globally, to be used by matchmaking route\n setTransport(this.transport);\n\n this.transport.listen(port, hostname, backlog, (err) => {\n if (this.transport.server) {\n this.transport.server.on('error', (err) => reject(err));\n }\n\n // default router is used if no router is provided\n if (!this.router) {\n this.router = getDefaultRouter() as unknown as Routes;\n\n } else {\n // make sure default routes are included\n // https://github.com/Bekacru/better-call/pull/67\n this.router = this.router.extend({ ...getDefaultRouter().endpoints }) as unknown as Routes;\n }\n\n bindRouterToTransport(this.transport, this.router, this.options.express !== undefined);\n\n if (listeningListener) {\n listeningListener(err);\n }\n\n if (err) {\n reject(err);\n\n } else {\n resolve();\n }\n });\n });\n }\n\n /**\n * Define a new type of room for matchmaking.\n *\n * @param name public room identifier for match-making.\n * @param roomClass Room class definition\n * @param defaultOptions default options for `onCreate`\n */\n public define<T extends Type<Room>>(\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n name: string,\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n nameOrHandler: string | T,\n handlerOrOptions: T | OnCreateOptions<T>,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler {\n const name = (typeof(nameOrHandler) === \"string\")\n ? nameOrHandler\n : nameOrHandler.name;\n\n const roomClass = (typeof(nameOrHandler) === \"string\")\n ? handlerOrOptions\n : nameOrHandler;\n\n const options = (typeof(nameOrHandler) === \"string\")\n ? defaultOptions\n : handlerOrOptions;\n\n return matchMaker.defineRoomType(name, roomClass, options);\n }\n\n /**\n * Remove a room definition from matchmaking.\n * This method does not destroy any room. It only dissallows matchmaking\n */\n public removeRoomType(name: string): void {\n matchMaker.removeRoomType(name);\n }\n\n public async gracefullyShutdown(exit: boolean = true, err?: Error) {\n if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) {\n return;\n }\n\n try {\n // custom \"before shutdown\" method\n await this.onBeforeShutdownCallback();\n\n // this is going to lock all rooms and wait for them to be disposed\n await matchMaker.gracefullyShutdown();\n\n this.transport.shutdown();\n this.presence.shutdown();\n await this.driver.shutdown();\n\n // custom \"after shutdown\" method\n await this.onShutdownCallback();\n\n } catch (e) {\n debugAndPrintError(`error during shutdown: ${e}`);\n\n } finally {\n if (exit) {\n process.exit((err && !isDevMode) ? 1 : 0);\n }\n }\n }\n\n /**\n * Add simulated latency between client and server.\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n if (milliseconds > 0) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`);\n } else {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation disabled.`);\n }\n\n const halfwayMS = (milliseconds / 2);\n this.transport.simulateLatency(halfwayMS);\n\n if (this._originalRoomOnMessage == null) {\n this._originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = this._originalRoomOnMessage;\n\n Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {\n // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.\n const cachedBuffer = Buffer.from(buffer);\n setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);\n };\n }\n\n /**\n * Register a callback that is going to be executed before the server shuts down.\n * @param callback\n */\n public onShutdown(callback: () => void | Promise<any>) {\n this.onShutdownCallback = callback;\n }\n\n public onBeforeShutdown(callback: () => void | Promise<any>) {\n this.onBeforeShutdownCallback = callback;\n }\n\n protected async getDefaultTransport(options: any): Promise<Transport> {\n try {\n const module = await dynamicImport('@colyseus/ws-transport');\n const WebSocketTransport = module.WebSocketTransport;\n return new WebSocketTransport(options);\n\n } catch (error) {\n this._onTransportReady.reject(error);\n throw new Error(\"Please provide a 'transport' layer. Default transport not set.\");\n }\n }\n\n protected onShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n\n protected onBeforeShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n}\n\nexport type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;\n\nfunction isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {\n return value instanceof RegisteredHandler || (\n typeof(value) === \"object\" &&\n value !== null &&\n 'klass' in (value as object)\n );\n}\n\nexport function registerRoomDefinitions<T extends RoomDefinitions>(rooms: T): string[] {\n const roomNames: string[] = [];\n\n for (const [name, value] of Object.entries(rooms)) {\n if (isRegisteredHandler(value)) {\n value.name = name;\n matchMaker.addRoomType(value);\n\n } else {\n matchMaker.defineRoomType(name, value);\n }\n\n roomNames.push(name);\n }\n\n return roomNames;\n}\n\nexport function unregisterRoomDefinitions(roomNames: Iterable<string>) {\n for (const roomName of roomNames) {\n matchMaker.removeRoomType(roomName);\n }\n}\n\nexport type DefineServerOptions<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n> = ServerOptions & {\n rooms: T,\n routes?: R,\n};\n\nexport function defineServer<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n>(\n options: DefineServerOptions<T, R>,\n): Server<T, R> {\n const { rooms, routes, ...serverOptions } = options;\n\n if (isDevMode) {\n // In dev mode, the Vite plugin manages Server/matchMaker lifecycle.\n // Return a config-only object \u2014 no Server instance, no matchMaker.setup().\n return {\n options: serverOptions,\n router: routes,\n '~rooms': rooms,\n } as unknown as Server<T, R>;\n }\n\n const server = new Server<T, R>(serverOptions);\n server.router = routes;\n\n registerRoomDefinitions(rooms);\n\n return server;\n}\n\nexport function defineRoom<T extends Type<Room>>(\n roomKlass: T,\n defaultOptions?: Parameters<NonNullable<InstanceType<T>['onCreate']>>[0],\n): RegisteredHandler<InstanceType<T>> {\n return new RegisteredHandler(roomKlass, defaultOptions) as unknown as RegisteredHandler<InstanceType<T>>;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { greet } from \"@colyseus/greeting-banner\";\nimport type express from 'express';\nimport type { Server as HttpServer } from 'node:http';\n\nimport { debugAndPrintError } from './Debug.ts';\nimport * as matchMaker from './MatchMaker.ts';\nimport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\n\nimport { type OnCreateOptions, Room } from './Room.ts';\nimport { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './utils/Utils.ts';\n\nimport type { Presence } from \"./presence/Presence.ts\";\n\nimport { setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport } from './router/index.ts';\nimport { prereadRequestBodies } from './router/node.ts';\nimport { type SDKTypes as SharedSDKTypes } from '@colyseus/shared-types';\nimport { getDefaultRouter } from './router/default_routes.ts';\n\nexport type ServerOptions = {\n publicAddress?: string,\n presence?: Presence,\n driver?: matchMaker.MatchMakerDriver,\n transport?: Transport,\n gracefullyShutdown?: boolean,\n logger?: any;\n\n /**\n * Optional callback to execute before the server listens.\n * This is useful for example to connect into a database or other services before the server listens.\n */\n beforeListen?: () => Promise<void> | void,\n\n /**\n * Optional callback to configure Express routes.\n * When provided, the transport layer will initialize an Express-compatible app\n * and pass it to this callback for custom route configuration.\n *\n * For uWebSockets transport, this uses the uwebsockets-express module.\n */\n express?: (app: express.Application) => Promise<void> | void,\n\n /**\n * Custom function to determine which process should handle room creation.\n * Default: assign new rooms the process with least amount of rooms created\n */\n selectProcessIdToCreateRoom?: matchMaker.SelectProcessIdCallback;\n\n /**\n * Whether this process is running as a standalone match-maker or not. (default: false)\n * When enabled, this process will not spawn rooms and will only be responsible for matchmaking.\n */\n isStandaloneMatchMaker?: boolean; \n\n /**\n * If enabled, rooms are going to be restored in the server-side upon restart,\n * clients are going to automatically re-connect when server reboots.\n *\n * Beware of \"schema mismatch\" issues. When updating Schema structures and\n * reloading existing data, you may see \"schema mismatch\" errors in the\n * client-side.\n *\n * (This operation is costly and should not be used in a production\n * environment)\n */\n devMode?: boolean,\n\n /**\n * Display greeting message on server start.\n * Default: true\n */\n greet?: boolean,\n};\n\n/**\n * Exposed types for the client-side SDK.\n * Re-exported from @colyseus/shared-types with specific type constraints.\n */\nexport interface SDKTypes<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> extends SharedSDKTypes<RoomTypes, Routes> {}\n\nexport class Server<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> implements SDKTypes<RoomTypes, Routes> {\n '~rooms': RoomTypes;\n '~routes': Routes;\n\n public transport: Transport;\n public router: Routes;\n public options: ServerOptions;\n\n protected presence: Presence;\n protected driver: matchMaker.MatchMakerDriver;\n\n protected port: number | string;\n protected greet: boolean;\n\n protected _onTransportReady = new Deferred<Transport>();\n\n private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n constructor(options: ServerOptions = {}) {\n const {\n gracefullyShutdown = true,\n greet = true\n } = options;\n\n setDevMode(options.devMode === true);\n\n this.options = options;\n this.greet = greet;\n\n this.attach(options);\n\n // Pass options.presence/driver through as-is (possibly undefined).\n // matchMaker.setup() falls back to getDefaultPresence/getDefaultDriver,\n // which auto-select RedisPresence/RedisDriver on Colyseus Cloud.\n matchMaker.setup(\n options.presence,\n options.driver,\n options.publicAddress,\n options.selectProcessIdToCreateRoom,\n ).then(() => {\n this.presence = matchMaker.presence;\n this.driver = matchMaker.driver;\n });\n\n if (gracefullyShutdown) {\n registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));\n }\n\n if (options.logger) {\n setLogger(options.logger);\n }\n }\n\n public async attach(options: ServerOptions) {\n this.transport = options.transport || await this.getDefaultTransport(options);\n\n // Initialize Express if callback is provided\n if (options.express && this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await options.express(expressApp);\n }\n\n // Resolve the promise when the transport is ready\n this._onTransportReady.resolve(this.transport);\n }\n\n /**\n * Bind the server into the port specified.\n *\n * @param port - Port number or Unix socket path\n * @param hostname\n * @param backlog\n * @param listeningListener\n */\n public async listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: Function) {\n if (this.options.beforeListen) {\n await this.options.beforeListen();\n }\n\n //\n // if Colyseus Cloud is detected, use @colyseus/tools to listen\n //\n if (process.env.COLYSEUS_CLOUD !== undefined ) {\n if (typeof(hostname) === \"number\") {\n //\n // workaround, @colyseus/tools calls server.listen() again with the port as a string\n //\n hostname = undefined;\n\n } else {\n try {\n return (await dynamicImport(\"@colyseus/tools\")).listen(this);\n } catch (error) {\n const err = new Error(\"Please install @colyseus/tools to be able to host on Colyseus Cloud.\");\n err.cause = error;\n throw err;\n }\n }\n }\n\n //\n // otherwise, listen on the port directly\n //\n this.port = port;\n\n //\n // Make sure matchmaker is ready before accepting connections\n // (isDevMode: matchmaker may take extra milliseconds to restore the rooms)\n //\n await matchMaker.accept(this.options.isStandaloneMatchMaker);\n\n /**\n * Greetings!\n */\n if (this.greet) {\n greet();\n }\n\n // Wait for the transport to be ready\n await this._onTransportReady;\n\n return new Promise<void>((resolve, reject) => {\n this.transport.listen(port, hostname, backlog, (err) => {\n if (this.transport.server) {\n this.transport.server.on('error', (err) => reject(err));\n }\n\n this.bindRoutes();\n\n if (listeningListener) {\n listeningListener(err);\n }\n\n if (err) {\n reject(err);\n\n } else {\n resolve();\n }\n });\n });\n }\n\n /**\n * Prepare matchmaking and HTTP routes, then return the underlying\n * `http.Server` *without* binding to a port.\n *\n * Use this on serverless platforms that consume an exported HTTP server\n * instead of a listening one. Vercel, for example, drives the request handler\n * of a default-exported server (its Express/Hono WebSocket examples use\n * `export default server`), whereas calling `listen()` there selects Vercel's\n * \"captured server\" path, which does not invoke Express-style app handlers.\n *\n * The transport must be created with an `http.Server` so it can be exported.\n * Vercel additionally needs `package.json` `\"main\"` pointing at the entrypoint:\n *\n * ```ts\n * import { createServer } from \"node:http\";\n * const httpServer = createServer();\n * const server = new Server({\n * transport: new WebSocketTransport({ server: httpServer }),\n * express: (app) => { app.get(\"/hello\", (req, res) => res.json({ ok: true })); },\n * });\n * server.define(\"my_room\", MyRoom);\n * export default await server.serverless(); // no listen()\n * ```\n */\n public async serverless(): Promise<HttpServer> {\n if (this.options.beforeListen) {\n await this.options.beforeListen();\n }\n\n // transport (and its HTTP server) must be ready before binding routes\n await this._onTransportReady;\n\n const server = this.transport.server as HttpServer | undefined;\n if (!server) {\n throw new Error(\"serverless() requires a transport backed by an http.Server (pass `{ server }` to the transport).\");\n }\n\n this.bindRoutes();\n\n // When the server is consumed via `export default` (rather than listen()),\n // the request body must be available synchronously \u2014 buffer it up-front so\n // `req.body` is set before the router runs.\n prereadRequestBodies(server);\n\n await matchMaker.accept(this.options.isStandaloneMatchMaker);\n\n if (this.greet) {\n greet();\n }\n\n return server;\n }\n\n /**\n * Set the active transport globally and bind the matchmaking/HTTP routes.\n * Shared by {@link listen} and {@link serverless}.\n *\n * `setTransport()` runs here rather than before `transport.listen()`: the\n * global transport is only read by the matchmaking route handler, which can't\n * run until `bindRouterToTransport()` below registers the route.\n */\n private bindRoutes() {\n // set transport globally, to be used by the matchmaking route\n setTransport(this.transport);\n\n // default router is used if no router is provided\n if (!this.router) {\n this.router = getDefaultRouter() as unknown as Routes;\n\n } else {\n // make sure default routes are included\n // https://github.com/Bekacru/better-call/pull/67\n this.router = this.router.extend({ ...getDefaultRouter().endpoints }) as unknown as Routes;\n }\n\n bindRouterToTransport(this.transport, this.router, this.options.express !== undefined);\n }\n\n /**\n * Define a new type of room for matchmaking.\n *\n * @param name public room identifier for match-making.\n * @param roomClass Room class definition\n * @param defaultOptions default options for `onCreate`\n */\n public define<T extends Type<Room>>(\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n name: string,\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n nameOrHandler: string | T,\n handlerOrOptions: T | OnCreateOptions<T>,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler {\n const name = (typeof(nameOrHandler) === \"string\")\n ? nameOrHandler\n : nameOrHandler.name;\n\n const roomClass = (typeof(nameOrHandler) === \"string\")\n ? handlerOrOptions\n : nameOrHandler;\n\n const options = (typeof(nameOrHandler) === \"string\")\n ? defaultOptions\n : handlerOrOptions;\n\n return matchMaker.defineRoomType(name, roomClass, options);\n }\n\n /**\n * Remove a room definition from matchmaking.\n * This method does not destroy any room. It only dissallows matchmaking\n */\n public removeRoomType(name: string): void {\n matchMaker.removeRoomType(name);\n }\n\n public async gracefullyShutdown(exit: boolean = true, err?: Error) {\n if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) {\n return;\n }\n\n try {\n // custom \"before shutdown\" method\n await this.onBeforeShutdownCallback();\n\n // this is going to lock all rooms and wait for them to be disposed\n await matchMaker.gracefullyShutdown();\n\n this.transport.shutdown();\n this.presence?.shutdown();\n await this.driver?.shutdown();\n\n // custom \"after shutdown\" method\n await this.onShutdownCallback();\n\n } catch (e) {\n debugAndPrintError(`error during shutdown: ${e}`);\n\n } finally {\n if (exit) {\n process.exit((err && !isDevMode) ? 1 : 0);\n }\n }\n }\n\n /**\n * Add simulated latency between client and server.\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n if (milliseconds > 0) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`);\n } else {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation disabled.`);\n }\n\n const halfwayMS = (milliseconds / 2);\n this.transport.simulateLatency(halfwayMS);\n\n if (this._originalRoomOnMessage == null) {\n this._originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = this._originalRoomOnMessage;\n\n Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {\n // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.\n const cachedBuffer = Buffer.from(buffer);\n setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);\n };\n }\n\n /**\n * Register a callback that is going to be executed before the server shuts down.\n * @param callback\n */\n public onShutdown(callback: () => void | Promise<any>) {\n this.onShutdownCallback = callback;\n }\n\n public onBeforeShutdown(callback: () => void | Promise<any>) {\n this.onBeforeShutdownCallback = callback;\n }\n\n protected async getDefaultTransport(options: any): Promise<Transport> {\n try {\n const module = await dynamicImport('@colyseus/ws-transport');\n const WebSocketTransport = module.WebSocketTransport;\n return new WebSocketTransport(options);\n\n } catch (error) {\n this._onTransportReady.reject(error);\n throw new Error(\"Please provide a 'transport' layer. Default transport not set.\");\n }\n }\n\n protected onShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n\n protected onBeforeShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n}\n\nexport type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;\n\nfunction isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {\n return value instanceof RegisteredHandler || (\n typeof(value) === \"object\" &&\n value !== null &&\n 'klass' in (value as object)\n );\n}\n\nexport function registerRoomDefinitions<T extends RoomDefinitions>(rooms: T): string[] {\n const roomNames: string[] = [];\n\n for (const [name, value] of Object.entries(rooms)) {\n if (isRegisteredHandler(value)) {\n value.name = name;\n matchMaker.addRoomType(value);\n\n } else {\n matchMaker.defineRoomType(name, value);\n }\n\n roomNames.push(name);\n }\n\n return roomNames;\n}\n\nexport function unregisterRoomDefinitions(roomNames: Iterable<string>) {\n for (const roomName of roomNames) {\n matchMaker.removeRoomType(roomName);\n }\n}\n\nexport type DefineServerOptions<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n> = ServerOptions & {\n rooms: T,\n routes?: R,\n};\n\nexport function defineServer<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n>(\n options: DefineServerOptions<T, R>,\n): Server<T, R> {\n const { rooms, routes, ...serverOptions } = options;\n\n if (isDevMode) {\n // In dev mode, the Vite plugin manages Server/matchMaker lifecycle.\n // Return a config-only object \u2014 no Server instance, no matchMaker.setup().\n return {\n options: serverOptions,\n router: routes,\n '~rooms': rooms,\n } as unknown as Server<T, R>;\n }\n\n const server = new Server<T, R>(serverOptions);\n server.router = routes;\n\n registerRoomDefinitions(rooms);\n\n return server;\n}\n\nexport function defineRoom<T extends Type<Room>>(\n roomKlass: T,\n defaultOptions?: Parameters<NonNullable<InstanceType<T>['onCreate']>>[0],\n): RegisteredHandler<InstanceType<T>> {\n return new RegisteredHandler(roomKlass, defaultOptions) as unknown as RegisteredHandler<InstanceType<T>>;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["gracefullyShutdown", "greet", "err", "module"]
7
7
  }
package/build/Server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type express from 'express';
2
+ import type { Server as HttpServer } from 'node:http';
2
3
  import * as matchMaker from './MatchMaker.ts';
3
4
  import { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
4
5
  import { type OnCreateOptions, Room } from './Room.ts';
@@ -84,6 +85,40 @@ export declare class Server<RoomTypes extends Record<string, RegisteredHandler>
84
85
  * @param listeningListener
85
86
  */
86
87
  listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: Function): Promise<any>;
88
+ /**
89
+ * Prepare matchmaking and HTTP routes, then return the underlying
90
+ * `http.Server` *without* binding to a port.
91
+ *
92
+ * Use this on serverless platforms that consume an exported HTTP server
93
+ * instead of a listening one. Vercel, for example, drives the request handler
94
+ * of a default-exported server (its Express/Hono WebSocket examples use
95
+ * `export default server`), whereas calling `listen()` there selects Vercel's
96
+ * "captured server" path, which does not invoke Express-style app handlers.
97
+ *
98
+ * The transport must be created with an `http.Server` so it can be exported.
99
+ * Vercel additionally needs `package.json` `"main"` pointing at the entrypoint:
100
+ *
101
+ * ```ts
102
+ * import { createServer } from "node:http";
103
+ * const httpServer = createServer();
104
+ * const server = new Server({
105
+ * transport: new WebSocketTransport({ server: httpServer }),
106
+ * express: (app) => { app.get("/hello", (req, res) => res.json({ ok: true })); },
107
+ * });
108
+ * server.define("my_room", MyRoom);
109
+ * export default await server.serverless(); // no listen()
110
+ * ```
111
+ */
112
+ serverless(): Promise<HttpServer>;
113
+ /**
114
+ * Set the active transport globally and bind the matchmaking/HTTP routes.
115
+ * Shared by {@link listen} and {@link serverless}.
116
+ *
117
+ * `setTransport()` runs here rather than before `transport.listen()`: the
118
+ * global transport is only read by the matchmaking route handler, which can't
119
+ * run until `bindRouterToTransport()` below registers the route.
120
+ */
121
+ private bindRoutes;
87
122
  /**
88
123
  * Define a new type of room for matchmaking.
89
124
  *
package/build/Server.mjs CHANGED
@@ -5,12 +5,11 @@ import * as matchMaker from "./MatchMaker.mjs";
5
5
  import { RegisteredHandler } from "./matchmaker/RegisteredHandler.mjs";
6
6
  import { Room } from "./Room.mjs";
7
7
  import { Deferred, registerGracefulShutdown, dynamicImport } from "./utils/Utils.mjs";
8
- import { LocalPresence } from "./presence/LocalPresence.mjs";
9
- import { LocalDriver } from "./matchmaker/LocalDriver/LocalDriver.mjs";
10
8
  import { setTransport } from "./Transport.mjs";
11
9
  import { logger, setLogger } from "./Logger.mjs";
12
10
  import { setDevMode, isDevMode } from "./utils/DevMode.mjs";
13
11
  import { bindRouterToTransport } from "./router/index.mjs";
12
+ import { prereadRequestBodies } from "./router/node.mjs";
14
13
  import "@colyseus/shared-types";
15
14
  import { getDefaultRouter } from "./router/default_routes.mjs";
16
15
  var Server = class {
@@ -24,17 +23,18 @@ var Server = class {
24
23
  greet: greet2 = true
25
24
  } = options;
26
25
  setDevMode(options.devMode === true);
27
- this.presence = options.presence || new LocalPresence();
28
- this.driver = options.driver || new LocalDriver();
29
26
  this.options = options;
30
27
  this.greet = greet2;
31
28
  this.attach(options);
32
29
  matchMaker.setup(
33
- this.presence,
34
- this.driver,
30
+ options.presence,
31
+ options.driver,
35
32
  options.publicAddress,
36
33
  options.selectProcessIdToCreateRoom
37
- );
34
+ ).then(() => {
35
+ this.presence = matchMaker.presence;
36
+ this.driver = matchMaker.driver;
37
+ });
38
38
  if (gracefullyShutdown2) {
39
39
  registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));
40
40
  }
@@ -82,17 +82,11 @@ var Server = class {
82
82
  }
83
83
  await this._onTransportReady;
84
84
  return new Promise((resolve, reject) => {
85
- setTransport(this.transport);
86
85
  this.transport.listen(port, hostname, backlog, (err) => {
87
86
  if (this.transport.server) {
88
87
  this.transport.server.on("error", (err2) => reject(err2));
89
88
  }
90
- if (!this.router) {
91
- this.router = getDefaultRouter();
92
- } else {
93
- this.router = this.router.extend({ ...getDefaultRouter().endpoints });
94
- }
95
- bindRouterToTransport(this.transport, this.router, this.options.express !== void 0);
89
+ this.bindRoutes();
96
90
  if (listeningListener) {
97
91
  listeningListener(err);
98
92
  }
@@ -104,6 +98,64 @@ var Server = class {
104
98
  });
105
99
  });
106
100
  }
101
+ /**
102
+ * Prepare matchmaking and HTTP routes, then return the underlying
103
+ * `http.Server` *without* binding to a port.
104
+ *
105
+ * Use this on serverless platforms that consume an exported HTTP server
106
+ * instead of a listening one. Vercel, for example, drives the request handler
107
+ * of a default-exported server (its Express/Hono WebSocket examples use
108
+ * `export default server`), whereas calling `listen()` there selects Vercel's
109
+ * "captured server" path, which does not invoke Express-style app handlers.
110
+ *
111
+ * The transport must be created with an `http.Server` so it can be exported.
112
+ * Vercel additionally needs `package.json` `"main"` pointing at the entrypoint:
113
+ *
114
+ * ```ts
115
+ * import { createServer } from "node:http";
116
+ * const httpServer = createServer();
117
+ * const server = new Server({
118
+ * transport: new WebSocketTransport({ server: httpServer }),
119
+ * express: (app) => { app.get("/hello", (req, res) => res.json({ ok: true })); },
120
+ * });
121
+ * server.define("my_room", MyRoom);
122
+ * export default await server.serverless(); // no listen()
123
+ * ```
124
+ */
125
+ async serverless() {
126
+ if (this.options.beforeListen) {
127
+ await this.options.beforeListen();
128
+ }
129
+ await this._onTransportReady;
130
+ const server = this.transport.server;
131
+ if (!server) {
132
+ throw new Error("serverless() requires a transport backed by an http.Server (pass `{ server }` to the transport).");
133
+ }
134
+ this.bindRoutes();
135
+ prereadRequestBodies(server);
136
+ await matchMaker.accept(this.options.isStandaloneMatchMaker);
137
+ if (this.greet) {
138
+ greet();
139
+ }
140
+ return server;
141
+ }
142
+ /**
143
+ * Set the active transport globally and bind the matchmaking/HTTP routes.
144
+ * Shared by {@link listen} and {@link serverless}.
145
+ *
146
+ * `setTransport()` runs here rather than before `transport.listen()`: the
147
+ * global transport is only read by the matchmaking route handler, which can't
148
+ * run until `bindRouterToTransport()` below registers the route.
149
+ */
150
+ bindRoutes() {
151
+ setTransport(this.transport);
152
+ if (!this.router) {
153
+ this.router = getDefaultRouter();
154
+ } else {
155
+ this.router = this.router.extend({ ...getDefaultRouter().endpoints });
156
+ }
157
+ bindRouterToTransport(this.transport, this.router, this.options.express !== void 0);
158
+ }
107
159
  define(nameOrHandler, handlerOrOptions, defaultOptions) {
108
160
  const name = typeof nameOrHandler === "string" ? nameOrHandler : nameOrHandler.name;
109
161
  const roomClass = typeof nameOrHandler === "string" ? handlerOrOptions : nameOrHandler;
@@ -125,8 +177,8 @@ var Server = class {
125
177
  await this.onBeforeShutdownCallback();
126
178
  await matchMaker.gracefullyShutdown();
127
179
  this.transport.shutdown();
128
- this.presence.shutdown();
129
- await this.driver.shutdown();
180
+ this.presence?.shutdown();
181
+ await this.driver?.shutdown();
130
182
  await this.onShutdownCallback();
131
183
  } catch (e) {
132
184
  debugAndPrintError(`error during shutdown: ${e}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/Server.ts"],
4
- "sourcesContent": ["import { greet } from \"@colyseus/greeting-banner\";\nimport type express from 'express';\n\nimport { debugAndPrintError } from './Debug.ts';\nimport * as matchMaker from './MatchMaker.ts';\nimport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\n\nimport { type OnCreateOptions, Room } from './Room.ts';\nimport { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './utils/Utils.ts';\n\nimport type { Presence } from \"./presence/Presence.ts\";\nimport { LocalPresence } from './presence/LocalPresence.ts';\nimport { LocalDriver } from './matchmaker/LocalDriver/LocalDriver.ts';\n\nimport { setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport } from './router/index.ts';\nimport { type SDKTypes as SharedSDKTypes } from '@colyseus/shared-types';\nimport { getDefaultRouter } from './router/default_routes.ts';\n\nexport type ServerOptions = {\n publicAddress?: string,\n presence?: Presence,\n driver?: matchMaker.MatchMakerDriver,\n transport?: Transport,\n gracefullyShutdown?: boolean,\n logger?: any;\n\n /**\n * Optional callback to execute before the server listens.\n * This is useful for example to connect into a database or other services before the server listens.\n */\n beforeListen?: () => Promise<void> | void,\n\n /**\n * Optional callback to configure Express routes.\n * When provided, the transport layer will initialize an Express-compatible app\n * and pass it to this callback for custom route configuration.\n *\n * For uWebSockets transport, this uses the uwebsockets-express module.\n */\n express?: (app: express.Application) => Promise<void> | void,\n\n /**\n * Custom function to determine which process should handle room creation.\n * Default: assign new rooms the process with least amount of rooms created\n */\n selectProcessIdToCreateRoom?: matchMaker.SelectProcessIdCallback;\n\n /**\n * Whether this process is running as a standalone match-maker or not. (default: false)\n * When enabled, this process will not spawn rooms and will only be responsible for matchmaking.\n */\n isStandaloneMatchMaker?: boolean; \n\n /**\n * If enabled, rooms are going to be restored in the server-side upon restart,\n * clients are going to automatically re-connect when server reboots.\n *\n * Beware of \"schema mismatch\" issues. When updating Schema structures and\n * reloading existing data, you may see \"schema mismatch\" errors in the\n * client-side.\n *\n * (This operation is costly and should not be used in a production\n * environment)\n */\n devMode?: boolean,\n\n /**\n * Display greeting message on server start.\n * Default: true\n */\n greet?: boolean,\n};\n\n/**\n * Exposed types for the client-side SDK.\n * Re-exported from @colyseus/shared-types with specific type constraints.\n */\nexport interface SDKTypes<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> extends SharedSDKTypes<RoomTypes, Routes> {}\n\nexport class Server<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> implements SDKTypes<RoomTypes, Routes> {\n '~rooms': RoomTypes;\n '~routes': Routes;\n\n public transport: Transport;\n public router: Routes;\n public options: ServerOptions;\n\n protected presence: Presence;\n protected driver: matchMaker.MatchMakerDriver;\n\n protected port: number | string;\n protected greet: boolean;\n\n protected _onTransportReady = new Deferred<Transport>();\n\n private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n constructor(options: ServerOptions = {}) {\n const {\n gracefullyShutdown = true,\n greet = true\n } = options;\n\n setDevMode(options.devMode === true);\n\n this.presence = options.presence || new LocalPresence();\n this.driver = options.driver || new LocalDriver();\n this.options = options;\n this.greet = greet;\n\n this.attach(options);\n\n matchMaker.setup(\n this.presence,\n this.driver,\n options.publicAddress,\n options.selectProcessIdToCreateRoom,\n );\n\n if (gracefullyShutdown) {\n registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));\n }\n\n if (options.logger) {\n setLogger(options.logger);\n }\n }\n\n public async attach(options: ServerOptions) {\n this.transport = options.transport || await this.getDefaultTransport(options);\n\n // Initialize Express if callback is provided\n if (options.express && this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await options.express(expressApp);\n }\n\n // Resolve the promise when the transport is ready\n this._onTransportReady.resolve(this.transport);\n }\n\n /**\n * Bind the server into the port specified.\n *\n * @param port - Port number or Unix socket path\n * @param hostname\n * @param backlog\n * @param listeningListener\n */\n public async listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: Function) {\n if (this.options.beforeListen) {\n await this.options.beforeListen();\n }\n\n //\n // if Colyseus Cloud is detected, use @colyseus/tools to listen\n //\n if (process.env.COLYSEUS_CLOUD !== undefined ) {\n if (typeof(hostname) === \"number\") {\n //\n // workaround, @colyseus/tools calls server.listen() again with the port as a string\n //\n hostname = undefined;\n\n } else {\n try {\n return (await dynamicImport(\"@colyseus/tools\")).listen(this);\n } catch (error) {\n const err = new Error(\"Please install @colyseus/tools to be able to host on Colyseus Cloud.\");\n err.cause = error;\n throw err;\n }\n }\n }\n\n //\n // otherwise, listen on the port directly\n //\n this.port = port;\n\n //\n // Make sure matchmaker is ready before accepting connections\n // (isDevMode: matchmaker may take extra milliseconds to restore the rooms)\n //\n await matchMaker.accept(this.options.isStandaloneMatchMaker);\n\n /**\n * Greetings!\n */\n if (this.greet) {\n greet();\n }\n\n // Wait for the transport to be ready\n await this._onTransportReady;\n\n return new Promise<void>((resolve, reject) => {\n // TODO: refactor me!\n // set transport globally, to be used by matchmaking route\n setTransport(this.transport);\n\n this.transport.listen(port, hostname, backlog, (err) => {\n if (this.transport.server) {\n this.transport.server.on('error', (err) => reject(err));\n }\n\n // default router is used if no router is provided\n if (!this.router) {\n this.router = getDefaultRouter() as unknown as Routes;\n\n } else {\n // make sure default routes are included\n // https://github.com/Bekacru/better-call/pull/67\n this.router = this.router.extend({ ...getDefaultRouter().endpoints }) as unknown as Routes;\n }\n\n bindRouterToTransport(this.transport, this.router, this.options.express !== undefined);\n\n if (listeningListener) {\n listeningListener(err);\n }\n\n if (err) {\n reject(err);\n\n } else {\n resolve();\n }\n });\n });\n }\n\n /**\n * Define a new type of room for matchmaking.\n *\n * @param name public room identifier for match-making.\n * @param roomClass Room class definition\n * @param defaultOptions default options for `onCreate`\n */\n public define<T extends Type<Room>>(\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n name: string,\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n nameOrHandler: string | T,\n handlerOrOptions: T | OnCreateOptions<T>,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler {\n const name = (typeof(nameOrHandler) === \"string\")\n ? nameOrHandler\n : nameOrHandler.name;\n\n const roomClass = (typeof(nameOrHandler) === \"string\")\n ? handlerOrOptions\n : nameOrHandler;\n\n const options = (typeof(nameOrHandler) === \"string\")\n ? defaultOptions\n : handlerOrOptions;\n\n return matchMaker.defineRoomType(name, roomClass, options);\n }\n\n /**\n * Remove a room definition from matchmaking.\n * This method does not destroy any room. It only dissallows matchmaking\n */\n public removeRoomType(name: string): void {\n matchMaker.removeRoomType(name);\n }\n\n public async gracefullyShutdown(exit: boolean = true, err?: Error) {\n if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) {\n return;\n }\n\n try {\n // custom \"before shutdown\" method\n await this.onBeforeShutdownCallback();\n\n // this is going to lock all rooms and wait for them to be disposed\n await matchMaker.gracefullyShutdown();\n\n this.transport.shutdown();\n this.presence.shutdown();\n await this.driver.shutdown();\n\n // custom \"after shutdown\" method\n await this.onShutdownCallback();\n\n } catch (e) {\n debugAndPrintError(`error during shutdown: ${e}`);\n\n } finally {\n if (exit) {\n process.exit((err && !isDevMode) ? 1 : 0);\n }\n }\n }\n\n /**\n * Add simulated latency between client and server.\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n if (milliseconds > 0) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`);\n } else {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation disabled.`);\n }\n\n const halfwayMS = (milliseconds / 2);\n this.transport.simulateLatency(halfwayMS);\n\n if (this._originalRoomOnMessage == null) {\n this._originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = this._originalRoomOnMessage;\n\n Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {\n // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.\n const cachedBuffer = Buffer.from(buffer);\n setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);\n };\n }\n\n /**\n * Register a callback that is going to be executed before the server shuts down.\n * @param callback\n */\n public onShutdown(callback: () => void | Promise<any>) {\n this.onShutdownCallback = callback;\n }\n\n public onBeforeShutdown(callback: () => void | Promise<any>) {\n this.onBeforeShutdownCallback = callback;\n }\n\n protected async getDefaultTransport(options: any): Promise<Transport> {\n try {\n const module = await dynamicImport('@colyseus/ws-transport');\n const WebSocketTransport = module.WebSocketTransport;\n return new WebSocketTransport(options);\n\n } catch (error) {\n this._onTransportReady.reject(error);\n throw new Error(\"Please provide a 'transport' layer. Default transport not set.\");\n }\n }\n\n protected onShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n\n protected onBeforeShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n}\n\nexport type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;\n\nfunction isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {\n return value instanceof RegisteredHandler || (\n typeof(value) === \"object\" &&\n value !== null &&\n 'klass' in (value as object)\n );\n}\n\nexport function registerRoomDefinitions<T extends RoomDefinitions>(rooms: T): string[] {\n const roomNames: string[] = [];\n\n for (const [name, value] of Object.entries(rooms)) {\n if (isRegisteredHandler(value)) {\n value.name = name;\n matchMaker.addRoomType(value);\n\n } else {\n matchMaker.defineRoomType(name, value);\n }\n\n roomNames.push(name);\n }\n\n return roomNames;\n}\n\nexport function unregisterRoomDefinitions(roomNames: Iterable<string>) {\n for (const roomName of roomNames) {\n matchMaker.removeRoomType(roomName);\n }\n}\n\nexport type DefineServerOptions<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n> = ServerOptions & {\n rooms: T,\n routes?: R,\n};\n\nexport function defineServer<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n>(\n options: DefineServerOptions<T, R>,\n): Server<T, R> {\n const { rooms, routes, ...serverOptions } = options;\n\n if (isDevMode) {\n // In dev mode, the Vite plugin manages Server/matchMaker lifecycle.\n // Return a config-only object \u2014 no Server instance, no matchMaker.setup().\n return {\n options: serverOptions,\n router: routes,\n '~rooms': rooms,\n } as unknown as Server<T, R>;\n }\n\n const server = new Server<T, R>(serverOptions);\n server.router = routes;\n\n registerRoomDefinitions(rooms);\n\n return server;\n}\n\nexport function defineRoom<T extends Type<Room>>(\n roomKlass: T,\n defaultOptions?: Parameters<NonNullable<InstanceType<T>['onCreate']>>[0],\n): RegisteredHandler<InstanceType<T>> {\n return new RegisteredHandler(roomKlass, defaultOptions) as unknown as RegisteredHandler<InstanceType<T>>;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { greet } from \"@colyseus/greeting-banner\";\nimport type express from 'express';\nimport type { Server as HttpServer } from 'node:http';\n\nimport { debugAndPrintError } from './Debug.ts';\nimport * as matchMaker from './MatchMaker.ts';\nimport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\n\nimport { type OnCreateOptions, Room } from './Room.ts';\nimport { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './utils/Utils.ts';\n\nimport type { Presence } from \"./presence/Presence.ts\";\n\nimport { setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport } from './router/index.ts';\nimport { prereadRequestBodies } from './router/node.ts';\nimport { type SDKTypes as SharedSDKTypes } from '@colyseus/shared-types';\nimport { getDefaultRouter } from './router/default_routes.ts';\n\nexport type ServerOptions = {\n publicAddress?: string,\n presence?: Presence,\n driver?: matchMaker.MatchMakerDriver,\n transport?: Transport,\n gracefullyShutdown?: boolean,\n logger?: any;\n\n /**\n * Optional callback to execute before the server listens.\n * This is useful for example to connect into a database or other services before the server listens.\n */\n beforeListen?: () => Promise<void> | void,\n\n /**\n * Optional callback to configure Express routes.\n * When provided, the transport layer will initialize an Express-compatible app\n * and pass it to this callback for custom route configuration.\n *\n * For uWebSockets transport, this uses the uwebsockets-express module.\n */\n express?: (app: express.Application) => Promise<void> | void,\n\n /**\n * Custom function to determine which process should handle room creation.\n * Default: assign new rooms the process with least amount of rooms created\n */\n selectProcessIdToCreateRoom?: matchMaker.SelectProcessIdCallback;\n\n /**\n * Whether this process is running as a standalone match-maker or not. (default: false)\n * When enabled, this process will not spawn rooms and will only be responsible for matchmaking.\n */\n isStandaloneMatchMaker?: boolean; \n\n /**\n * If enabled, rooms are going to be restored in the server-side upon restart,\n * clients are going to automatically re-connect when server reboots.\n *\n * Beware of \"schema mismatch\" issues. When updating Schema structures and\n * reloading existing data, you may see \"schema mismatch\" errors in the\n * client-side.\n *\n * (This operation is costly and should not be used in a production\n * environment)\n */\n devMode?: boolean,\n\n /**\n * Display greeting message on server start.\n * Default: true\n */\n greet?: boolean,\n};\n\n/**\n * Exposed types for the client-side SDK.\n * Re-exported from @colyseus/shared-types with specific type constraints.\n */\nexport interface SDKTypes<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> extends SharedSDKTypes<RoomTypes, Routes> {}\n\nexport class Server<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> implements SDKTypes<RoomTypes, Routes> {\n '~rooms': RoomTypes;\n '~routes': Routes;\n\n public transport: Transport;\n public router: Routes;\n public options: ServerOptions;\n\n protected presence: Presence;\n protected driver: matchMaker.MatchMakerDriver;\n\n protected port: number | string;\n protected greet: boolean;\n\n protected _onTransportReady = new Deferred<Transport>();\n\n private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n constructor(options: ServerOptions = {}) {\n const {\n gracefullyShutdown = true,\n greet = true\n } = options;\n\n setDevMode(options.devMode === true);\n\n this.options = options;\n this.greet = greet;\n\n this.attach(options);\n\n // Pass options.presence/driver through as-is (possibly undefined).\n // matchMaker.setup() falls back to getDefaultPresence/getDefaultDriver,\n // which auto-select RedisPresence/RedisDriver on Colyseus Cloud.\n matchMaker.setup(\n options.presence,\n options.driver,\n options.publicAddress,\n options.selectProcessIdToCreateRoom,\n ).then(() => {\n this.presence = matchMaker.presence;\n this.driver = matchMaker.driver;\n });\n\n if (gracefullyShutdown) {\n registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));\n }\n\n if (options.logger) {\n setLogger(options.logger);\n }\n }\n\n public async attach(options: ServerOptions) {\n this.transport = options.transport || await this.getDefaultTransport(options);\n\n // Initialize Express if callback is provided\n if (options.express && this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await options.express(expressApp);\n }\n\n // Resolve the promise when the transport is ready\n this._onTransportReady.resolve(this.transport);\n }\n\n /**\n * Bind the server into the port specified.\n *\n * @param port - Port number or Unix socket path\n * @param hostname\n * @param backlog\n * @param listeningListener\n */\n public async listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: Function) {\n if (this.options.beforeListen) {\n await this.options.beforeListen();\n }\n\n //\n // if Colyseus Cloud is detected, use @colyseus/tools to listen\n //\n if (process.env.COLYSEUS_CLOUD !== undefined ) {\n if (typeof(hostname) === \"number\") {\n //\n // workaround, @colyseus/tools calls server.listen() again with the port as a string\n //\n hostname = undefined;\n\n } else {\n try {\n return (await dynamicImport(\"@colyseus/tools\")).listen(this);\n } catch (error) {\n const err = new Error(\"Please install @colyseus/tools to be able to host on Colyseus Cloud.\");\n err.cause = error;\n throw err;\n }\n }\n }\n\n //\n // otherwise, listen on the port directly\n //\n this.port = port;\n\n //\n // Make sure matchmaker is ready before accepting connections\n // (isDevMode: matchmaker may take extra milliseconds to restore the rooms)\n //\n await matchMaker.accept(this.options.isStandaloneMatchMaker);\n\n /**\n * Greetings!\n */\n if (this.greet) {\n greet();\n }\n\n // Wait for the transport to be ready\n await this._onTransportReady;\n\n return new Promise<void>((resolve, reject) => {\n this.transport.listen(port, hostname, backlog, (err) => {\n if (this.transport.server) {\n this.transport.server.on('error', (err) => reject(err));\n }\n\n this.bindRoutes();\n\n if (listeningListener) {\n listeningListener(err);\n }\n\n if (err) {\n reject(err);\n\n } else {\n resolve();\n }\n });\n });\n }\n\n /**\n * Prepare matchmaking and HTTP routes, then return the underlying\n * `http.Server` *without* binding to a port.\n *\n * Use this on serverless platforms that consume an exported HTTP server\n * instead of a listening one. Vercel, for example, drives the request handler\n * of a default-exported server (its Express/Hono WebSocket examples use\n * `export default server`), whereas calling `listen()` there selects Vercel's\n * \"captured server\" path, which does not invoke Express-style app handlers.\n *\n * The transport must be created with an `http.Server` so it can be exported.\n * Vercel additionally needs `package.json` `\"main\"` pointing at the entrypoint:\n *\n * ```ts\n * import { createServer } from \"node:http\";\n * const httpServer = createServer();\n * const server = new Server({\n * transport: new WebSocketTransport({ server: httpServer }),\n * express: (app) => { app.get(\"/hello\", (req, res) => res.json({ ok: true })); },\n * });\n * server.define(\"my_room\", MyRoom);\n * export default await server.serverless(); // no listen()\n * ```\n */\n public async serverless(): Promise<HttpServer> {\n if (this.options.beforeListen) {\n await this.options.beforeListen();\n }\n\n // transport (and its HTTP server) must be ready before binding routes\n await this._onTransportReady;\n\n const server = this.transport.server as HttpServer | undefined;\n if (!server) {\n throw new Error(\"serverless() requires a transport backed by an http.Server (pass `{ server }` to the transport).\");\n }\n\n this.bindRoutes();\n\n // When the server is consumed via `export default` (rather than listen()),\n // the request body must be available synchronously \u2014 buffer it up-front so\n // `req.body` is set before the router runs.\n prereadRequestBodies(server);\n\n await matchMaker.accept(this.options.isStandaloneMatchMaker);\n\n if (this.greet) {\n greet();\n }\n\n return server;\n }\n\n /**\n * Set the active transport globally and bind the matchmaking/HTTP routes.\n * Shared by {@link listen} and {@link serverless}.\n *\n * `setTransport()` runs here rather than before `transport.listen()`: the\n * global transport is only read by the matchmaking route handler, which can't\n * run until `bindRouterToTransport()` below registers the route.\n */\n private bindRoutes() {\n // set transport globally, to be used by the matchmaking route\n setTransport(this.transport);\n\n // default router is used if no router is provided\n if (!this.router) {\n this.router = getDefaultRouter() as unknown as Routes;\n\n } else {\n // make sure default routes are included\n // https://github.com/Bekacru/better-call/pull/67\n this.router = this.router.extend({ ...getDefaultRouter().endpoints }) as unknown as Routes;\n }\n\n bindRouterToTransport(this.transport, this.router, this.options.express !== undefined);\n }\n\n /**\n * Define a new type of room for matchmaking.\n *\n * @param name public room identifier for match-making.\n * @param roomClass Room class definition\n * @param defaultOptions default options for `onCreate`\n */\n public define<T extends Type<Room>>(\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n name: string,\n roomClass: T,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler\n public define<T extends Type<Room>>(\n nameOrHandler: string | T,\n handlerOrOptions: T | OnCreateOptions<T>,\n defaultOptions?: OnCreateOptions<T>,\n ): RegisteredHandler {\n const name = (typeof(nameOrHandler) === \"string\")\n ? nameOrHandler\n : nameOrHandler.name;\n\n const roomClass = (typeof(nameOrHandler) === \"string\")\n ? handlerOrOptions\n : nameOrHandler;\n\n const options = (typeof(nameOrHandler) === \"string\")\n ? defaultOptions\n : handlerOrOptions;\n\n return matchMaker.defineRoomType(name, roomClass, options);\n }\n\n /**\n * Remove a room definition from matchmaking.\n * This method does not destroy any room. It only dissallows matchmaking\n */\n public removeRoomType(name: string): void {\n matchMaker.removeRoomType(name);\n }\n\n public async gracefullyShutdown(exit: boolean = true, err?: Error) {\n if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) {\n return;\n }\n\n try {\n // custom \"before shutdown\" method\n await this.onBeforeShutdownCallback();\n\n // this is going to lock all rooms and wait for them to be disposed\n await matchMaker.gracefullyShutdown();\n\n this.transport.shutdown();\n this.presence?.shutdown();\n await this.driver?.shutdown();\n\n // custom \"after shutdown\" method\n await this.onShutdownCallback();\n\n } catch (e) {\n debugAndPrintError(`error during shutdown: ${e}`);\n\n } finally {\n if (exit) {\n process.exit((err && !isDevMode) ? 1 : 0);\n }\n }\n }\n\n /**\n * Add simulated latency between client and server.\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n if (milliseconds > 0) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`);\n } else {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Colyseus latency simulation disabled.`);\n }\n\n const halfwayMS = (milliseconds / 2);\n this.transport.simulateLatency(halfwayMS);\n\n if (this._originalRoomOnMessage == null) {\n this._originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = this._originalRoomOnMessage;\n\n Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {\n // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.\n const cachedBuffer = Buffer.from(buffer);\n setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);\n };\n }\n\n /**\n * Register a callback that is going to be executed before the server shuts down.\n * @param callback\n */\n public onShutdown(callback: () => void | Promise<any>) {\n this.onShutdownCallback = callback;\n }\n\n public onBeforeShutdown(callback: () => void | Promise<any>) {\n this.onBeforeShutdownCallback = callback;\n }\n\n protected async getDefaultTransport(options: any): Promise<Transport> {\n try {\n const module = await dynamicImport('@colyseus/ws-transport');\n const WebSocketTransport = module.WebSocketTransport;\n return new WebSocketTransport(options);\n\n } catch (error) {\n this._onTransportReady.reject(error);\n throw new Error(\"Please provide a 'transport' layer. Default transport not set.\");\n }\n }\n\n protected onShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n\n protected onBeforeShutdownCallback: () => void | Promise<any> =\n () => Promise.resolve()\n}\n\nexport type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;\n\nfunction isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {\n return value instanceof RegisteredHandler || (\n typeof(value) === \"object\" &&\n value !== null &&\n 'klass' in (value as object)\n );\n}\n\nexport function registerRoomDefinitions<T extends RoomDefinitions>(rooms: T): string[] {\n const roomNames: string[] = [];\n\n for (const [name, value] of Object.entries(rooms)) {\n if (isRegisteredHandler(value)) {\n value.name = name;\n matchMaker.addRoomType(value);\n\n } else {\n matchMaker.defineRoomType(name, value);\n }\n\n roomNames.push(name);\n }\n\n return roomNames;\n}\n\nexport function unregisterRoomDefinitions(roomNames: Iterable<string>) {\n for (const roomName of roomNames) {\n matchMaker.removeRoomType(roomName);\n }\n}\n\nexport type DefineServerOptions<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n> = ServerOptions & {\n rooms: T,\n routes?: R,\n};\n\nexport function defineServer<\n T extends Record<string, RegisteredHandler>,\n R extends Router\n>(\n options: DefineServerOptions<T, R>,\n): Server<T, R> {\n const { rooms, routes, ...serverOptions } = options;\n\n if (isDevMode) {\n // In dev mode, the Vite plugin manages Server/matchMaker lifecycle.\n // Return a config-only object \u2014 no Server instance, no matchMaker.setup().\n return {\n options: serverOptions,\n router: routes,\n '~rooms': rooms,\n } as unknown as Server<T, R>;\n }\n\n const server = new Server<T, R>(serverOptions);\n server.router = routes;\n\n registerRoomDefinitions(rooms);\n\n return server;\n}\n\nexport function defineRoom<T extends Type<Room>>(\n roomKlass: T,\n defaultOptions?: Parameters<NonNullable<InstanceType<T>['onCreate']>>[0],\n): RegisteredHandler<InstanceType<T>> {\n return new RegisteredHandler(roomKlass, defaultOptions) as unknown as RegisteredHandler<InstanceType<T>>;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["gracefullyShutdown", "greet", "err"]
7
7
  }
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // packages/core/src/router/node.ts
31
31
  var node_exports = {};
32
32
  __export(node_exports, {
33
- createNodeMatchmakingMiddleware: () => createNodeMatchmakingMiddleware
33
+ createNodeMatchmakingMiddleware: () => createNodeMatchmakingMiddleware,
34
+ prereadRequestBodies: () => prereadRequestBodies
34
35
  });
35
36
  module.exports = __toCommonJS(node_exports);
36
37
  var import_url = require("url");
@@ -47,6 +48,39 @@ function readBody(req) {
47
48
  req.on("error", reject);
48
49
  });
49
50
  }
51
+ function prereadRequestBodies(server) {
52
+ const listeners = server.listeners("request");
53
+ const run = (req, res) => {
54
+ for (const listener of listeners) {
55
+ listener.call(server, req, res);
56
+ }
57
+ };
58
+ server.removeAllListeners("request");
59
+ server.on("request", (req, res) => {
60
+ const method = req.method ?? "GET";
61
+ const needsBody = method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && req.body === void 0 && Number(req.headers["content-length"]) > 0;
62
+ if (!needsBody) {
63
+ run(req, res);
64
+ return;
65
+ }
66
+ const chunks = [];
67
+ req.on("data", (chunk) => chunks.push(chunk));
68
+ req.on("end", () => {
69
+ const raw = Buffer.concat(chunks).toString("utf8");
70
+ if ((req.headers["content-type"] || "").includes("application/json")) {
71
+ try {
72
+ req.body = JSON.parse(raw || "{}");
73
+ } catch {
74
+ req.body = raw;
75
+ }
76
+ } else {
77
+ req.body = raw;
78
+ }
79
+ run(req, res);
80
+ });
81
+ req.on("error", () => run(req, res));
82
+ });
83
+ }
50
84
  function getCorsHeaders(req, headers) {
51
85
  return {
52
86
  ...matchMaker.controller.DEFAULT_CORS_HEADERS,
@@ -94,5 +128,6 @@ function createNodeMatchmakingMiddleware() {
94
128
  }
95
129
  // Annotate the CommonJS export names for ESM import in node:
96
130
  0 && (module.exports = {
97
- createNodeMatchmakingMiddleware
131
+ createNodeMatchmakingMiddleware,
132
+ prereadRequestBodies
98
133
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/node.ts"],
4
- "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n });\n req.on('end', () => resolve(data ? JSON.parse(data) : {}));\n req.on('error', reject);\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body: await readBody(req),\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,iBAAoB;AACpB,iBAA4B;AAC5B,kBAA4B;AAC5B,4BAAoC;AAEpC,SAAS,SAAS,KAAyC;AACzD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AAEX,QAAI,GAAG,QAAQ,CAAC,UAA2B;AACzC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AACD,QAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;AACzD,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAEA,SAAS,eAAe,KAA2B,SAA2C;AAC5F,SAAO;AAAA,IACL,GAAc,sBAAW;AAAA,IACzB,GAAc,sBAAW,eAAe,OAAO;AAAA,EACjD;AACF;AAEO,SAAS,kCAAkC;AAChD,SAAO,OACL,KACA,KACA,SACG;AACH,UAAM,MAAM,IAAI,eAAI,IAAI,OAAO,KAAK,kBAAkB;AACtD,UAAM,mBAAmB,IAAI,SAAS,WAAW,IAAe,sBAAW,cAAc,GAAG;AAE5F,QAAI,CAAC,kBAAkB;AACrB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,QAAQ,IAAI,OAAiC;AACjE,UAAM,cAAc,eAAe,KAAK,OAAO;AAE/C,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,UAAU,KAAK,WAAW;AAC9B,UAAI,IAAI;AACR;AAAA,IACF;AAEA,QAAI,IAAI,WAAW,QAAQ;AACzB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,SAAS,MAAM,2BAA2B;AAC5D,QAAI,CAAC,OAAO;AACV,WAAK;AACL;AAAA,IACF;AAEA,UAAM,CAAC,EAAE,QAAQ,QAAQ,IAAI;AAE7B,QAAI;AACF,YAAM,WAAW,UAAM,2CAAoB;AAAA,QACzC,QAAQ,EAAE,QAAQ,SAAS;AAAA,QAC3B,MAAM,MAAM,SAAS,GAAG;AAAA,QACxB,SAAS,IAAI;AAAA,QACb,SAAS,EAAE,QAAQ;AAAA,QACnB,YAAY;AAAA,MACd,CAAC;AAED,gBAAM,yBAAY,KAAK,QAAQ;AAAA,IAEjC,QAAQ;AAGN,WAAK;AAAA,IACP;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n });\n req.on('end', () => resolve(data ? JSON.parse(data) : {}));\n req.on('error', reject);\n });\n}\n\n/**\n * Buffer incoming request bodies and expose them as `req.body` before the\n * server's existing \"request\" listeners run.\n *\n * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)\n * rather than `listen()`: the matchmaking router reads the body from `req.body`\n * when present, otherwise from a lazy request stream that does not drain in that\n * mode \u2014 which would stall matchmaking POSTs.\n */\nexport function prereadRequestBodies(server: http.Server) {\n type WithBody = http.IncomingMessage & { body?: unknown };\n const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;\n const run = (req: http.IncomingMessage, res: http.ServerResponse) => {\n for (const listener of listeners) { listener.call(server, req, res); }\n };\n\n server.removeAllListeners('request');\n server.on('request', (req: WithBody, res) => {\n const method = req.method ?? 'GET';\n const needsBody =\n method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&\n req.body === undefined &&\n Number(req.headers['content-length']) > 0;\n\n if (!needsBody) {\n run(req, res);\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf8');\n if ((req.headers['content-type'] || '').includes('application/json')) {\n try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }\n } else {\n req.body = raw;\n }\n run(req, res);\n });\n req.on('error', () => run(req, res));\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body: await readBody(req),\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,4 +8,14 @@
8
8
  * this file only as the thin raw Node/Express adapter around it.
9
9
  */
10
10
  import type http from 'http';
11
+ /**
12
+ * Buffer incoming request bodies and expose them as `req.body` before the
13
+ * server's existing "request" listeners run.
14
+ *
15
+ * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)
16
+ * rather than `listen()`: the matchmaking router reads the body from `req.body`
17
+ * when present, otherwise from a lazy request stream that does not drain in that
18
+ * mode — which would stall matchmaking POSTs.
19
+ */
20
+ export declare function prereadRequestBodies(server: http.Server): void;
11
21
  export declare function createNodeMatchmakingMiddleware(): (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => Promise<void>;
@@ -13,6 +13,39 @@ function readBody(req) {
13
13
  req.on("error", reject);
14
14
  });
15
15
  }
16
+ function prereadRequestBodies(server) {
17
+ const listeners = server.listeners("request");
18
+ const run = (req, res) => {
19
+ for (const listener of listeners) {
20
+ listener.call(server, req, res);
21
+ }
22
+ };
23
+ server.removeAllListeners("request");
24
+ server.on("request", (req, res) => {
25
+ const method = req.method ?? "GET";
26
+ const needsBody = method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && req.body === void 0 && Number(req.headers["content-length"]) > 0;
27
+ if (!needsBody) {
28
+ run(req, res);
29
+ return;
30
+ }
31
+ const chunks = [];
32
+ req.on("data", (chunk) => chunks.push(chunk));
33
+ req.on("end", () => {
34
+ const raw = Buffer.concat(chunks).toString("utf8");
35
+ if ((req.headers["content-type"] || "").includes("application/json")) {
36
+ try {
37
+ req.body = JSON.parse(raw || "{}");
38
+ } catch {
39
+ req.body = raw;
40
+ }
41
+ } else {
42
+ req.body = raw;
43
+ }
44
+ run(req, res);
45
+ });
46
+ req.on("error", () => run(req, res));
47
+ });
48
+ }
16
49
  function getCorsHeaders(req, headers) {
17
50
  return {
18
51
  ...matchMaker.controller.DEFAULT_CORS_HEADERS,
@@ -59,5 +92,6 @@ function createNodeMatchmakingMiddleware() {
59
92
  };
60
93
  }
61
94
  export {
62
- createNodeMatchmakingMiddleware
95
+ createNodeMatchmakingMiddleware,
96
+ prereadRequestBodies
63
97
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/node.ts"],
4
- "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n });\n req.on('end', () => resolve(data ? JSON.parse(data) : {}));\n req.on('error', reject);\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body: await readBody(req),\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
- "mappings": ";AAUA,SAAS,WAAW;AACpB,YAAY,gBAAgB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AAEpC,SAAS,SAAS,KAAyC;AACzD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AAEX,QAAI,GAAG,QAAQ,CAAC,UAA2B;AACzC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AACD,QAAI,GAAG,OAAO,MAAM,QAAQ,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;AACzD,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAEA,SAAS,eAAe,KAA2B,SAA2C;AAC5F,SAAO;AAAA,IACL,GAAc,sBAAW;AAAA,IACzB,GAAc,sBAAW,eAAe,OAAO;AAAA,EACjD;AACF;AAEO,SAAS,kCAAkC;AAChD,SAAO,OACL,KACA,KACA,SACG;AACH,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;AACtD,UAAM,mBAAmB,IAAI,SAAS,WAAW,IAAe,sBAAW,cAAc,GAAG;AAE5F,QAAI,CAAC,kBAAkB;AACrB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,QAAQ,IAAI,OAAiC;AACjE,UAAM,cAAc,eAAe,KAAK,OAAO;AAE/C,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,UAAU,KAAK,WAAW;AAC9B,UAAI,IAAI;AACR;AAAA,IACF;AAEA,QAAI,IAAI,WAAW,QAAQ;AACzB,WAAK;AACL;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,SAAS,MAAM,2BAA2B;AAC5D,QAAI,CAAC,OAAO;AACV,WAAK;AACL;AAAA,IACF;AAEA,UAAM,CAAC,EAAE,QAAQ,QAAQ,IAAI;AAE7B,QAAI;AACF,YAAM,WAAW,MAAM,oBAAoB;AAAA,QACzC,QAAQ,EAAE,QAAQ,SAAS;AAAA,QAC3B,MAAM,MAAM,SAAS,GAAG;AAAA,QACxB,SAAS,IAAI;AAAA,QACb,SAAS,EAAE,QAAQ;AAAA,QACnB,YAAY;AAAA,MACd,CAAC;AAED,YAAM,YAAY,KAAK,QAAQ;AAAA,IAEjC,QAAQ;AAGN,WAAK;AAAA,IACP;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Raw Node.js adapter for Colyseus matchmaking routes used by `colyseus/vite`.\n *\n * This file exists specifically so the Vite plugin can share Vite's dev HTTP\n * server while still exposing the Colyseus `/matchmake/*` endpoints.\n *\n * Keep the matchmaking behavior itself in `router/default_routes.ts` and use\n * this file only as the thin raw Node/Express adapter around it.\n */\nimport type http from 'http';\nimport { URL } from 'url';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { setResponse } from '@colyseus/better-call/node';\nimport { postMatchmakeMethod } from './default_routes.ts';\n\nfunction readBody(req: http.IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = '';\n\n req.on('data', (chunk: Buffer | string) => {\n data += chunk.toString();\n });\n req.on('end', () => resolve(data ? JSON.parse(data) : {}));\n req.on('error', reject);\n });\n}\n\n/**\n * Buffer incoming request bodies and expose them as `req.body` before the\n * server's existing \"request\" listeners run.\n *\n * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)\n * rather than `listen()`: the matchmaking router reads the body from `req.body`\n * when present, otherwise from a lazy request stream that does not drain in that\n * mode \u2014 which would stall matchmaking POSTs.\n */\nexport function prereadRequestBodies(server: http.Server) {\n type WithBody = http.IncomingMessage & { body?: unknown };\n const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;\n const run = (req: http.IncomingMessage, res: http.ServerResponse) => {\n for (const listener of listeners) { listener.call(server, req, res); }\n };\n\n server.removeAllListeners('request');\n server.on('request', (req: WithBody, res) => {\n const method = req.method ?? 'GET';\n const needsBody =\n method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&\n req.body === undefined &&\n Number(req.headers['content-length']) > 0;\n\n if (!needsBody) {\n run(req, res);\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf8');\n if ((req.headers['content-type'] || '').includes('application/json')) {\n try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }\n } else {\n req.body = raw;\n }\n run(req, res);\n });\n req.on('error', () => run(req, res));\n });\n}\n\nfunction getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {\n return {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(headers),\n };\n}\n\nexport function createNodeMatchmakingMiddleware() {\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: () => void,\n ) => {\n const url = new URL(req.url || '/', 'http://localhost');\n const isMatchmakeRoute = url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}/`);\n\n if (!isMatchmakeRoute) {\n next();\n return;\n }\n\n const headers = new Headers(req.headers as Record<string, string>);\n const corsHeaders = getCorsHeaders(req, headers);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n if (req.method !== 'POST') {\n next();\n return;\n }\n\n const match = url.pathname.match(/^\\/matchmake\\/(\\w+)\\/(.+)/);\n if (!match) {\n next();\n return;\n }\n\n const [, method, roomName] = match;\n\n try {\n const response = await postMatchmakeMethod({\n params: { method, roomName },\n body: await readBody(req),\n headers: req.headers as Record<string, string>,\n request: { headers } as any,\n asResponse: true,\n });\n\n await setResponse(res, response);\n\n } catch {\n // Endpoint-level failures are returned as Response when `asResponse` is true.\n // Any thrown error here is unexpected, so let the next middleware decide.\n next();\n }\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -36,29 +36,33 @@ __export(Env_exports, {
36
36
  isColyseusCloud: () => isColyseusCloud
37
37
  });
38
38
  module.exports = __toCommonJS(Env_exports);
39
+ var import_os = __toESM(require("os"), 1);
39
40
  var import_LocalPresence = require("../presence/LocalPresence.cjs");
40
41
  var import_LocalDriver = require("../matchmaker/LocalDriver/LocalDriver.cjs");
41
42
  var import_Logger = require("../Logger.cjs");
42
43
  function isColyseusCloud() {
43
44
  return process.env.COLYSEUS_CLOUD !== void 0;
44
45
  }
46
+ function shouldUseRedisOnCloud() {
47
+ return import_os.default.cpus().length > 1 || process.env.REDIS_URI !== void 0;
48
+ }
45
49
  async function getDefaultPresence() {
46
- if (isColyseusCloud()) {
50
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
47
51
  try {
48
52
  const RedisPresence = await import("@colyseus/redis-presence");
49
53
  return new RedisPresence.RedisPresence(process.env.REDIS_URI);
50
54
  } catch (e) {
51
55
  console.error(e);
52
56
  import_Logger.logger.warn("");
53
- import_Logger.logger.warn("\u274C could not initialize RedisDriver.");
54
- import_Logger.logger.warn("\u{1F449} npm install --save @colyseus/redis-driver");
57
+ import_Logger.logger.warn("\u274C could not initialize RedisPresence.");
58
+ import_Logger.logger.warn("\u{1F449} npm install --save @colyseus/redis-presence");
55
59
  import_Logger.logger.warn("");
56
60
  }
57
61
  }
58
62
  return new import_LocalPresence.LocalPresence();
59
63
  }
60
64
  async function getDefaultDriver() {
61
- if (isColyseusCloud()) {
65
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
62
66
  try {
63
67
  const RedisDriver = await import("@colyseus/redis-driver");
64
68
  return new RedisDriver.RedisDriver(process.env.REDIS_URI);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Env.ts"],
4
- "sourcesContent": ["import { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA8B;AAC9B,yBAAmD;AACnD,oBAAuB;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,0CAAqC;AACjD,2BAAO,KAAK,qDAA8C;AAC1D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,mCAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,0CAAqC;AACjD,2BAAO,KAAK,qDAA8C;AAC1D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,+BAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
6
- "names": []
4
+ "sourcesContent": ["import os from 'os';\nimport { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\n// On Colyseus Cloud, only opt into Redis when there's more than one CPU\n// (multi-process deploys) or when REDIS_URI is explicitly provided.\n// Single-CPU instances without REDIS_URI keep using Local presence/driver.\nfunction shouldUseRedisOnCloud(): boolean {\n return os.cpus().length > 1 || process.env.REDIS_URI !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAe;AACf,2BAA8B;AAC9B,yBAAmD;AACnD,oBAAuB;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAKA,SAAS,wBAAiC;AACxC,SAAO,UAAAA,QAAG,KAAK,EAAE,SAAS,KAAK,QAAQ,IAAI,cAAc;AAC3D;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,4CAAuC;AACnD,2BAAO,KAAK,uDAAgD;AAC5D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,mCAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,0CAAqC;AACjD,2BAAO,KAAK,qDAA8C;AAC1D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,+BAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
6
+ "names": ["os"]
7
7
  }
@@ -1,27 +1,31 @@
1
1
  // packages/core/src/utils/Env.ts
2
+ import os from "os";
2
3
  import { LocalPresence } from "../presence/LocalPresence.mjs";
3
4
  import { LocalDriver } from "../matchmaker/LocalDriver/LocalDriver.mjs";
4
5
  import { logger } from "../Logger.mjs";
5
6
  function isColyseusCloud() {
6
7
  return process.env.COLYSEUS_CLOUD !== void 0;
7
8
  }
9
+ function shouldUseRedisOnCloud() {
10
+ return os.cpus().length > 1 || process.env.REDIS_URI !== void 0;
11
+ }
8
12
  async function getDefaultPresence() {
9
- if (isColyseusCloud()) {
13
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
10
14
  try {
11
15
  const RedisPresence = await import("@colyseus/redis-presence");
12
16
  return new RedisPresence.RedisPresence(process.env.REDIS_URI);
13
17
  } catch (e) {
14
18
  console.error(e);
15
19
  logger.warn("");
16
- logger.warn("\u274C could not initialize RedisDriver.");
17
- logger.warn("\u{1F449} npm install --save @colyseus/redis-driver");
20
+ logger.warn("\u274C could not initialize RedisPresence.");
21
+ logger.warn("\u{1F449} npm install --save @colyseus/redis-presence");
18
22
  logger.warn("");
19
23
  }
20
24
  }
21
25
  return new LocalPresence();
22
26
  }
23
27
  async function getDefaultDriver() {
24
- if (isColyseusCloud()) {
28
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
25
29
  try {
26
30
  const RedisDriver = await import("@colyseus/redis-driver");
27
31
  return new RedisDriver.RedisDriver(process.env.REDIS_URI);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Env.ts"],
4
- "sourcesContent": ["import { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
- "mappings": ";AAAA,SAAS,qBAAqB;AAC9B,SAAS,mBAA0C;AACnD,SAAS,cAAc;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,0CAAqC;AACjD,aAAO,KAAK,qDAA8C;AAC1D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,cAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,0CAAqC;AACjD,aAAO,KAAK,qDAA8C;AAC1D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,YAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import os from 'os';\nimport { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\n// On Colyseus Cloud, only opt into Redis when there's more than one CPU\n// (multi-process deploys) or when REDIS_URI is explicitly provided.\n// Single-CPU instances without REDIS_URI keep using Local presence/driver.\nfunction shouldUseRedisOnCloud(): boolean {\n return os.cpus().length > 1 || process.env.REDIS_URI !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
+ "mappings": ";AAAA,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,mBAA0C;AACnD,SAAS,cAAc;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAKA,SAAS,wBAAiC;AACxC,SAAO,GAAG,KAAK,EAAE,SAAS,KAAK,QAAQ,IAAI,cAAc;AAC3D;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,4CAAuC;AACnD,aAAO,KAAK,uDAAgD;AAC5D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,cAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,0CAAqC;AACjD,aAAO,KAAK,qDAA8C;AAC1D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,YAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.17.42",
3
+ "version": "0.17.44",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -46,20 +46,20 @@
46
46
  "node": ">= 22.x"
47
47
  },
48
48
  "dependencies": {
49
- "@colyseus/msgpackr": "^1.11.2",
49
+ "@colyseus/msgpackr": "^1.11.3",
50
50
  "@colyseus/timer": "^2.0.0",
51
51
  "@standard-schema/spec": "^1.0.0",
52
52
  "debug": "^4.3.4",
53
53
  "nanoid": "^3.3.11",
54
- "@colyseus/shared-types": "^0.17.6",
55
54
  "@colyseus/better-call": "^1.3.1",
56
- "@colyseus/greeting-banner": "^3.0.8"
55
+ "@colyseus/greeting-banner": "^3.0.8",
56
+ "@colyseus/shared-types": "^0.17.6"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@colyseus/schema": "^4.0.7",
60
60
  "express": "^5.0.0",
61
- "@colyseus/redis-driver": "^0.17.7",
62
61
  "@colyseus/redis-presence": "^0.17.7",
62
+ "@colyseus/redis-driver": "^0.17.7",
63
63
  "@colyseus/tools": "^0.17.19"
64
64
  },
65
65
  "peerDependencies": {
package/src/Server.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { greet } from "@colyseus/greeting-banner";
2
2
  import type express from 'express';
3
+ import type { Server as HttpServer } from 'node:http';
3
4
 
4
5
  import { debugAndPrintError } from './Debug.ts';
5
6
  import * as matchMaker from './MatchMaker.ts';
@@ -9,13 +10,12 @@ import { type OnCreateOptions, Room } from './Room.ts';
9
10
  import { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './utils/Utils.ts';
10
11
 
11
12
  import type { Presence } from "./presence/Presence.ts";
12
- import { LocalPresence } from './presence/LocalPresence.ts';
13
- import { LocalDriver } from './matchmaker/LocalDriver/LocalDriver.ts';
14
13
 
15
14
  import { setTransport, Transport } from './Transport.ts';
16
15
  import { logger, setLogger } from './Logger.ts';
17
16
  import { setDevMode, isDevMode } from './utils/DevMode.ts';
18
17
  import { type Router, bindRouterToTransport } from './router/index.ts';
18
+ import { prereadRequestBodies } from './router/node.ts';
19
19
  import { type SDKTypes as SharedSDKTypes } from '@colyseus/shared-types';
20
20
  import { getDefaultRouter } from './router/default_routes.ts';
21
21
 
@@ -112,19 +112,23 @@ export class Server<
112
112
 
113
113
  setDevMode(options.devMode === true);
114
114
 
115
- this.presence = options.presence || new LocalPresence();
116
- this.driver = options.driver || new LocalDriver();
117
115
  this.options = options;
118
116
  this.greet = greet;
119
117
 
120
118
  this.attach(options);
121
119
 
120
+ // Pass options.presence/driver through as-is (possibly undefined).
121
+ // matchMaker.setup() falls back to getDefaultPresence/getDefaultDriver,
122
+ // which auto-select RedisPresence/RedisDriver on Colyseus Cloud.
122
123
  matchMaker.setup(
123
- this.presence,
124
- this.driver,
124
+ options.presence,
125
+ options.driver,
125
126
  options.publicAddress,
126
127
  options.selectProcessIdToCreateRoom,
127
- );
128
+ ).then(() => {
129
+ this.presence = matchMaker.presence;
130
+ this.driver = matchMaker.driver;
131
+ });
128
132
 
129
133
  if (gracefullyShutdown) {
130
134
  registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));
@@ -204,26 +208,12 @@ export class Server<
204
208
  await this._onTransportReady;
205
209
 
206
210
  return new Promise<void>((resolve, reject) => {
207
- // TODO: refactor me!
208
- // set transport globally, to be used by matchmaking route
209
- setTransport(this.transport);
210
-
211
211
  this.transport.listen(port, hostname, backlog, (err) => {
212
212
  if (this.transport.server) {
213
213
  this.transport.server.on('error', (err) => reject(err));
214
214
  }
215
215
 
216
- // default router is used if no router is provided
217
- if (!this.router) {
218
- this.router = getDefaultRouter() as unknown as Routes;
219
-
220
- } else {
221
- // make sure default routes are included
222
- // https://github.com/Bekacru/better-call/pull/67
223
- this.router = this.router.extend({ ...getDefaultRouter().endpoints }) as unknown as Routes;
224
- }
225
-
226
- bindRouterToTransport(this.transport, this.router, this.options.express !== undefined);
216
+ this.bindRoutes();
227
217
 
228
218
  if (listeningListener) {
229
219
  listeningListener(err);
@@ -239,6 +229,84 @@ export class Server<
239
229
  });
240
230
  }
241
231
 
232
+ /**
233
+ * Prepare matchmaking and HTTP routes, then return the underlying
234
+ * `http.Server` *without* binding to a port.
235
+ *
236
+ * Use this on serverless platforms that consume an exported HTTP server
237
+ * instead of a listening one. Vercel, for example, drives the request handler
238
+ * of a default-exported server (its Express/Hono WebSocket examples use
239
+ * `export default server`), whereas calling `listen()` there selects Vercel's
240
+ * "captured server" path, which does not invoke Express-style app handlers.
241
+ *
242
+ * The transport must be created with an `http.Server` so it can be exported.
243
+ * Vercel additionally needs `package.json` `"main"` pointing at the entrypoint:
244
+ *
245
+ * ```ts
246
+ * import { createServer } from "node:http";
247
+ * const httpServer = createServer();
248
+ * const server = new Server({
249
+ * transport: new WebSocketTransport({ server: httpServer }),
250
+ * express: (app) => { app.get("/hello", (req, res) => res.json({ ok: true })); },
251
+ * });
252
+ * server.define("my_room", MyRoom);
253
+ * export default await server.serverless(); // no listen()
254
+ * ```
255
+ */
256
+ public async serverless(): Promise<HttpServer> {
257
+ if (this.options.beforeListen) {
258
+ await this.options.beforeListen();
259
+ }
260
+
261
+ // transport (and its HTTP server) must be ready before binding routes
262
+ await this._onTransportReady;
263
+
264
+ const server = this.transport.server as HttpServer | undefined;
265
+ if (!server) {
266
+ throw new Error("serverless() requires a transport backed by an http.Server (pass `{ server }` to the transport).");
267
+ }
268
+
269
+ this.bindRoutes();
270
+
271
+ // When the server is consumed via `export default` (rather than listen()),
272
+ // the request body must be available synchronously — buffer it up-front so
273
+ // `req.body` is set before the router runs.
274
+ prereadRequestBodies(server);
275
+
276
+ await matchMaker.accept(this.options.isStandaloneMatchMaker);
277
+
278
+ if (this.greet) {
279
+ greet();
280
+ }
281
+
282
+ return server;
283
+ }
284
+
285
+ /**
286
+ * Set the active transport globally and bind the matchmaking/HTTP routes.
287
+ * Shared by {@link listen} and {@link serverless}.
288
+ *
289
+ * `setTransport()` runs here rather than before `transport.listen()`: the
290
+ * global transport is only read by the matchmaking route handler, which can't
291
+ * run until `bindRouterToTransport()` below registers the route.
292
+ */
293
+ private bindRoutes() {
294
+ // set transport globally, to be used by the matchmaking route
295
+ setTransport(this.transport);
296
+
297
+ // default router is used if no router is provided
298
+ if (!this.router) {
299
+ this.router = getDefaultRouter() as unknown as Routes;
300
+
301
+ } else {
302
+ // make sure default routes are included
303
+ // https://github.com/Bekacru/better-call/pull/67
304
+ this.router = this.router.extend({ ...getDefaultRouter().endpoints }) as unknown as Routes;
305
+ }
306
+
307
+ bindRouterToTransport(this.transport, this.router, this.options.express !== undefined);
308
+ }
309
+
242
310
  /**
243
311
  * Define a new type of room for matchmaking.
244
312
  *
@@ -296,8 +364,8 @@ export class Server<
296
364
  await matchMaker.gracefullyShutdown();
297
365
 
298
366
  this.transport.shutdown();
299
- this.presence.shutdown();
300
- await this.driver.shutdown();
367
+ this.presence?.shutdown();
368
+ await this.driver?.shutdown();
301
369
 
302
370
  // custom "after shutdown" method
303
371
  await this.onShutdownCallback();
@@ -25,6 +25,50 @@ function readBody(req: http.IncomingMessage): Promise<any> {
25
25
  });
26
26
  }
27
27
 
28
+ /**
29
+ * Buffer incoming request bodies and expose them as `req.body` before the
30
+ * server's existing "request" listeners run.
31
+ *
32
+ * Needed when the HTTP server is consumed via `export default` (e.g. on Vercel)
33
+ * rather than `listen()`: the matchmaking router reads the body from `req.body`
34
+ * when present, otherwise from a lazy request stream that does not drain in that
35
+ * mode — which would stall matchmaking POSTs.
36
+ */
37
+ export function prereadRequestBodies(server: http.Server) {
38
+ type WithBody = http.IncomingMessage & { body?: unknown };
39
+ const listeners = server.listeners('request') as Array<(req: http.IncomingMessage, res: http.ServerResponse) => void>;
40
+ const run = (req: http.IncomingMessage, res: http.ServerResponse) => {
41
+ for (const listener of listeners) { listener.call(server, req, res); }
42
+ };
43
+
44
+ server.removeAllListeners('request');
45
+ server.on('request', (req: WithBody, res) => {
46
+ const method = req.method ?? 'GET';
47
+ const needsBody =
48
+ method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' &&
49
+ req.body === undefined &&
50
+ Number(req.headers['content-length']) > 0;
51
+
52
+ if (!needsBody) {
53
+ run(req, res);
54
+ return;
55
+ }
56
+
57
+ const chunks: Buffer[] = [];
58
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
59
+ req.on('end', () => {
60
+ const raw = Buffer.concat(chunks).toString('utf8');
61
+ if ((req.headers['content-type'] || '').includes('application/json')) {
62
+ try { req.body = JSON.parse(raw || '{}'); } catch { req.body = raw; }
63
+ } else {
64
+ req.body = raw;
65
+ }
66
+ run(req, res);
67
+ });
68
+ req.on('error', () => run(req, res));
69
+ });
70
+ }
71
+
28
72
  function getCorsHeaders(req: http.IncomingMessage, headers?: Headers): Record<string, string> {
29
73
  return {
30
74
  ...matchMaker.controller.DEFAULT_CORS_HEADERS,
package/src/utils/Env.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import os from 'os';
1
2
  import { LocalPresence } from '../presence/LocalPresence.ts';
2
3
  import { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';
3
4
  import { logger } from '../Logger.ts';
@@ -7,16 +8,23 @@ export function isColyseusCloud(): boolean {
7
8
  return process.env.COLYSEUS_CLOUD !== undefined;
8
9
  }
9
10
 
11
+ // On Colyseus Cloud, only opt into Redis when there's more than one CPU
12
+ // (multi-process deploys) or when REDIS_URI is explicitly provided.
13
+ // Single-CPU instances without REDIS_URI keep using Local presence/driver.
14
+ function shouldUseRedisOnCloud(): boolean {
15
+ return os.cpus().length > 1 || process.env.REDIS_URI !== undefined;
16
+ }
17
+
10
18
  export async function getDefaultPresence(): Promise<Presence> {
11
- if (isColyseusCloud()) {
19
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
12
20
  try {
13
21
  const RedisPresence = await import('@colyseus/redis-presence');
14
22
  return new RedisPresence.RedisPresence(process.env.REDIS_URI);
15
23
  } catch (e) {
16
24
  console.error(e);
17
25
  logger.warn("");
18
- logger.warn("❌ could not initialize RedisDriver.");
19
- logger.warn("👉 npm install --save @colyseus/redis-driver");
26
+ logger.warn("❌ could not initialize RedisPresence.");
27
+ logger.warn("👉 npm install --save @colyseus/redis-presence");
20
28
  logger.warn("");
21
29
  }
22
30
  }
@@ -24,7 +32,7 @@ export async function getDefaultPresence(): Promise<Presence> {
24
32
  }
25
33
 
26
34
  export async function getDefaultDriver(): Promise<MatchMakerDriver> {
27
- if (isColyseusCloud()) {
35
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
28
36
  try {
29
37
  const RedisDriver = await import('@colyseus/redis-driver');
30
38
  return new RedisDriver.RedisDriver(process.env.REDIS_URI);