@colyseus/core 0.18.7 → 0.18.9

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
@@ -58,14 +58,27 @@ var Server = class _Server {
58
58
  this._onTransportReady = new import_Utils.Deferred();
59
59
  this.onShutdownCallback = () => Promise.resolve();
60
60
  this.onBeforeShutdownCallback = () => Promise.resolve();
61
- const {
62
- gracefullyShutdown: gracefullyShutdown2 = true,
63
- greet: greet2 = true
64
- } = options;
65
- (0, import_DevMode.setDevMode)(options.devMode === true);
61
+ const { greet: greet2 = true } = options;
66
62
  this.options = options;
67
63
  this.greet = greet2;
68
64
  _Server.current = this;
65
+ if (options.logger) {
66
+ (0, import_Logger.setLogger)(options.logger);
67
+ }
68
+ this.bootRuntime();
69
+ }
70
+ /**
71
+ * Every side effect of construction: the dev-mode flag, the transport, the
72
+ * matchmaker and the process signal handlers. Distinct from
73
+ * {@link _bootServices}, which prepares user services once we're listening.
74
+ *
75
+ * Isolated so {@link DevServer} can opt out of all four — under
76
+ * `colyseus/vite` the plugin owns them, and user code still gets a real
77
+ * `Server` to call.
78
+ */
79
+ bootRuntime() {
80
+ const options = this.options;
81
+ (0, import_DevMode.setDevMode)(options.devMode === true);
69
82
  this.attach(options);
70
83
  matchMaker.setup(
71
84
  options.presence,
@@ -76,12 +89,9 @@ var Server = class _Server {
76
89
  this.presence = matchMaker.presence;
77
90
  this.driver = matchMaker.driver;
78
91
  });
79
- if (gracefullyShutdown2) {
92
+ if (options.gracefullyShutdown !== false) {
80
93
  (0, import_Utils.registerGracefulShutdown)((err) => this.gracefullyShutdown(true, err));
81
94
  }
82
- if (options.logger) {
83
- (0, import_Logger.setLogger)(options.logger);
84
- }
85
95
  }
86
96
  async attach(options) {
87
97
  this.transport = options.transport || await this.getDefaultTransport(options);
@@ -307,6 +317,30 @@ var Server = class _Server {
307
317
  }
308
318
  }
309
319
  };
320
+ var devWarnings = /* @__PURE__ */ new Set();
321
+ function warnDevOnce(message) {
322
+ if (devWarnings.has(message)) {
323
+ return;
324
+ }
325
+ devWarnings.add(message);
326
+ import_Logger.logger.warn(message);
327
+ }
328
+ var DevServer = class extends Server {
329
+ bootRuntime() {
330
+ this.transport = (0, import_Transport.getTransport)();
331
+ this._onTransportReady.resolve(this.transport);
332
+ this.presence = matchMaker.presence;
333
+ this.driver = matchMaker.driver;
334
+ if (this.options.transport) {
335
+ warnDevOnce("\u26A0\uFE0F 'transport' is ignored in dev mode \u2014 colyseus/vite serves WebSockets over Vite's own HTTP server.");
336
+ }
337
+ }
338
+ /** The plugin already serves Colyseus on Vite's own HTTP server and port. */
339
+ async listen(_port, _hostname, _backlog, listeningListener) {
340
+ warnDevOnce("\u26A0\uFE0F listen() is ignored in dev mode \u2014 connect to the Vite dev server's URL instead.");
341
+ listeningListener?.();
342
+ }
343
+ };
310
344
  var _originalRoomOnMessage = null;
311
345
  function applySimulatedLatency(transport, milliseconds) {
312
346
  if (milliseconds > 0) {
@@ -360,18 +394,12 @@ function unregisterRoomDefinitions(roomNames) {
360
394
  }
361
395
  function defineServer(options) {
362
396
  const { rooms, routes, ...serverOptions } = options;
363
- if (import_DevMode.isDevMode) {
364
- return {
365
- options: serverOptions,
366
- router: routes,
367
- "~rooms": rooms,
368
- // the vite plugin registers the transport before importing user code
369
- simulateLatency: (milliseconds) => applySimulatedLatency((0, import_Transport.getTransport)(), milliseconds)
370
- };
371
- }
372
- const server = new Server(serverOptions);
397
+ const server = import_DevMode.isDevMode ? new DevServer(serverOptions) : new Server(serverOptions);
373
398
  server.router = routes;
374
- registerRoomDefinitions(rooms);
399
+ server["~rooms"] = rooms;
400
+ if (!import_DevMode.isDevMode) {
401
+ registerRoomDefinitions(rooms);
402
+ }
375
403
  return server;
376
404
  }
377
405
  function defineRoom(roomKlass, defaultOptions) {
@@ -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';\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 { getTransport, setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport, createRouter } 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 * Booted before `matchMaker.accept()`. Structural so `@colyseus/core` keeps\n * no runtime dep on `@colyseus/database` \u2014 any `{ boot(): Promise<void> }`\n * works. The optional `applyRouterDefaults` is invoked after boot with the\n * user's router so the database can contribute endpoints (e.g. auth routes).\n */\n database?: {\n boot(): Promise<void>;\n applyRouterDefaults?(router: Router): Router | Promise<Router>;\n },\n\n /**\n * Mount `@colyseus/auth` routes into the router. Pass an options object\n * (forwarded to `auth.endpoints(...)`) to wire it explicitly \u2014 useful when\n * `database` is not in use. `false` disables auto-mounting even if a\n * `database` is present and would have provided defaults.\n */\n auth?: false | {\n settings?: Record<string, any>;\n oauth?: boolean | { cookieSecret?: string };\n prefix?: string;\n },\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 // Implicit default for callers that omit explicit Server reference \u2014 e.g.\n // `playground()` reads `Server.current.router.endpoints` at request time.\n // Last-construction wins; multi-server setups should reference instances\n // explicitly.\n static current: Server<any, any> | undefined;\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 (Server as { current: Server<any, any> | undefined }).current = this as Server<any, any>;\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 // `options.express` runs in `listen()` after `database.boot()` so user\n // code reading `database.auth.settings` etc. finds services instantiated.\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 await this._bootServices();\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 await this._bootServices();\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 *\n * May be called at any time \u2014 before or after `listen()`. Also available\n * via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which\n * overrides calls made before the server boots.\n *\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n // transport may still be resolving (constructor's attach() imports the default one)\n this._onTransportReady.then(\n (transport) => applySimulatedLatency(transport, milliseconds),\n () => {/* missing-transport error already surfaces via attach()/listen() */},\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 // Boot sequence shared by listen() and serverless(): user `beforeListen`\n // hook, database boot, framework router defaults (auth/db endpoints), then\n // the express app. serverless() needs this too \u2014 its exported server still\n // serves the express callback and auth/database routes.\n private async _bootServices(): Promise<void> {\n const { beforeListen, database, express } = this.options;\n\n // boot runs after module evaluation, so the env var wins over\n // simulateLatency() calls made at the top level of user code\n const envLatency = parseLatencyEnv();\n if (envLatency !== undefined) { this.simulateLatency(envLatency); }\n\n if (beforeListen) { await beforeListen(); }\n if (database) { await database.boot(); }\n\n await this._applyRouterDefaults();\n\n if (express) {\n await this._onTransportReady;\n if (this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await express(expressApp);\n }\n }\n }\n\n // Extend the user's router with framework-contributed endpoints. An explicit\n // `auth` option wins over the database's `applyRouterDefaults` so a non-DB\n // setup can still auto-mount @colyseus/auth, and a DB user can opt out.\n private async _applyRouterDefaults(): Promise<void> {\n const { auth, database } = this.options;\n const wantsAuth = auth !== undefined && auth !== false;\n const wantsDatabase = auth === undefined && !!database?.applyRouterDefaults;\n if (!wantsAuth && !wantsDatabase) { return; }\n\n // Boot an empty router if the user didn't pass `routes` \u2014 the framework's\n // default routes get layered on later in the `transport.listen()` callback.\n this.router ??= createRouter({}) as unknown as Routes;\n\n if (wantsAuth) {\n const authMod: any = await dynamicImport('@colyseus/auth').catch(() => undefined);\n const endpointsFn = authMod?.auth?.endpoints ?? authMod?.default?.auth?.endpoints;\n if (typeof endpointsFn === 'function') {\n this.router = (this.router as Router).extend(endpointsFn(auth)) as Routes;\n }\n } else {\n const updated = await database!.applyRouterDefaults!(this.router as Router);\n if (updated) { this.router = updated as Routes; }\n }\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\nlet _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n/**\n * Applies both halves of the round-trip latency simulation: delays the\n * transport's outgoing messages and the Room's incoming message handling.\n *\n * Prefer `server.simulateLatency()` \u2014 this is the shared core behind it, the\n * dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where\n * no `Server` instance (or transport, yet) may exist.\n */\nexport function applySimulatedLatency(transport: Transport | undefined, 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 transport?.simulateLatency(halfwayMS);\n\n if (_originalRoomOnMessage == null) {\n _originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = _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 * Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).\n * Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.\n */\nexport function parseLatencyEnv(): number | undefined {\n const value = process.env.COLYSEUS_LATENCY;\n if (value === undefined || value === '') { return undefined; }\n\n const milliseconds = Number(value);\n if (isNaN(milliseconds)) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);\n return undefined;\n }\n return milliseconds;\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 // the vite plugin registers the transport before importing user code\n simulateLatency: (milliseconds: number) => applySimulatedLatency(getTransport(), milliseconds),\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
- "names": ["gracefullyShutdown", "greet", "err", "module"]
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 { getTransport, setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport, createRouter } 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 * Booted before `matchMaker.accept()`. Structural so `@colyseus/core` keeps\n * no runtime dep on `@colyseus/database` \u2014 any `{ boot(): Promise<void> }`\n * works. The optional `applyRouterDefaults` is invoked after boot with the\n * user's router so the database can contribute endpoints (e.g. auth routes).\n */\n database?: {\n boot(): Promise<void>;\n applyRouterDefaults?(router: Router): Router | Promise<Router>;\n },\n\n /**\n * Mount `@colyseus/auth` routes into the router. Pass an options object\n * (forwarded to `auth.endpoints(...)`) to wire it explicitly \u2014 useful when\n * `database` is not in use. `false` disables auto-mounting even if a\n * `database` is present and would have provided defaults.\n */\n auth?: false | {\n settings?: Record<string, any>;\n oauth?: boolean | { cookieSecret?: string };\n prefix?: string;\n },\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 // Implicit default for callers that omit explicit Server reference \u2014 e.g.\n // `playground()` reads `Server.current.router.endpoints` at request time.\n // Last-construction wins; multi-server setups should reference instances\n // explicitly.\n static current: Server<any, any> | undefined;\n\n constructor(options: ServerOptions = {}) {\n const { greet = true } = options;\n\n this.options = options;\n this.greet = greet;\n\n (Server as { current: Server<any, any> | undefined }).current = this as Server<any, any>;\n\n if (options.logger) {\n setLogger(options.logger);\n }\n\n this.bootRuntime();\n }\n\n /**\n * Every side effect of construction: the dev-mode flag, the transport, the\n * matchmaker and the process signal handlers. Distinct from\n * {@link _bootServices}, which prepares user services once we're listening.\n *\n * Isolated so {@link DevServer} can opt out of all four \u2014 under\n * `colyseus/vite` the plugin owns them, and user code still gets a real\n * `Server` to call.\n */\n protected bootRuntime() {\n const options = this.options;\n\n setDevMode(options.devMode === true);\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 (options.gracefullyShutdown !== false) {\n registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));\n }\n }\n\n public async attach(options: ServerOptions) {\n this.transport = options.transport || await this.getDefaultTransport(options);\n // `options.express` runs in `listen()` after `database.boot()` so user\n // code reading `database.auth.settings` etc. finds services instantiated.\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 await this._bootServices();\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 await this._bootServices();\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 *\n * May be called at any time \u2014 before or after `listen()`. Also available\n * via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which\n * overrides calls made before the server boots.\n *\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n // transport may still be resolving (constructor's attach() imports the default one)\n this._onTransportReady.then(\n (transport) => applySimulatedLatency(transport, milliseconds),\n () => {/* missing-transport error already surfaces via attach()/listen() */},\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 // Boot sequence shared by listen() and serverless(): user `beforeListen`\n // hook, database boot, framework router defaults (auth/db endpoints), then\n // the express app. serverless() needs this too \u2014 its exported server still\n // serves the express callback and auth/database routes.\n private async _bootServices(): Promise<void> {\n const { beforeListen, database, express } = this.options;\n\n // boot runs after module evaluation, so the env var wins over\n // simulateLatency() calls made at the top level of user code\n const envLatency = parseLatencyEnv();\n if (envLatency !== undefined) { this.simulateLatency(envLatency); }\n\n if (beforeListen) { await beforeListen(); }\n if (database) { await database.boot(); }\n\n await this._applyRouterDefaults();\n\n if (express) {\n await this._onTransportReady;\n if (this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await express(expressApp);\n }\n }\n }\n\n // Extend the user's router with framework-contributed endpoints. An explicit\n // `auth` option wins over the database's `applyRouterDefaults` so a non-DB\n // setup can still auto-mount @colyseus/auth, and a DB user can opt out.\n private async _applyRouterDefaults(): Promise<void> {\n const { auth, database } = this.options;\n const wantsAuth = auth !== undefined && auth !== false;\n const wantsDatabase = auth === undefined && !!database?.applyRouterDefaults;\n if (!wantsAuth && !wantsDatabase) { return; }\n\n // Boot an empty router if the user didn't pass `routes` \u2014 the framework's\n // default routes get layered on later in the `transport.listen()` callback.\n this.router ??= createRouter({}) as unknown as Routes;\n\n if (wantsAuth) {\n const authMod: any = await dynamicImport('@colyseus/auth').catch(() => undefined);\n const endpointsFn = authMod?.auth?.endpoints ?? authMod?.default?.auth?.endpoints;\n if (typeof endpointsFn === 'function') {\n this.router = (this.router as Router).extend(endpointsFn(auth)) as Routes;\n }\n } else {\n const updated = await database!.applyRouterDefaults!(this.router as Router);\n if (updated) { this.router = updated as Routes; }\n }\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\n// the user module re-evaluates on every HMR reload \u2014 warn once per process\nconst devWarnings = new Set<string>();\nfunction warnDevOnce(message: string) {\n if (devWarnings.has(message)) { return; }\n devWarnings.add(message);\n logger.warn(message);\n}\n\n/**\n * The `Server` handed to user code under `colyseus/vite`.\n *\n * The plugin owns the HTTP server, the transport, the matchmaker and the\n * process lifecycle, so constructing one has no side effects \u2014 it only\n * carries the user's configuration back to the plugin. Everything else is\n * inherited, so the documented `Server` API keeps working in dev mode.\n */\nclass DevServer<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> extends Server<RoomTypes, Routes> {\n\n protected bootRuntime() {\n // the plugin creates the transport and runs matchMaker.setup() before\n // importing user code, so both are already available here\n this.transport = getTransport();\n this._onTransportReady.resolve(this.transport);\n this.presence = matchMaker.presence;\n this.driver = matchMaker.driver;\n\n if (this.options.transport) {\n warnDevOnce(\"\u26A0\uFE0F 'transport' is ignored in dev mode \u2014 colyseus/vite serves WebSockets over Vite's own HTTP server.\");\n }\n }\n\n /** The plugin already serves Colyseus on Vite's own HTTP server and port. */\n public async listen(_port?: number | string, _hostname?: string, _backlog?: number, listeningListener?: Function) {\n warnDevOnce(\"\u26A0\uFE0F listen() is ignored in dev mode \u2014 connect to the Vite dev server's URL instead.\");\n listeningListener?.();\n }\n}\n\nlet _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n/**\n * Applies both halves of the round-trip latency simulation: delays the\n * transport's outgoing messages and the Room's incoming message handling.\n *\n * Prefer `server.simulateLatency()` \u2014 this is the shared core behind it, the\n * dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where\n * no `Server` instance (or transport, yet) may exist.\n */\nexport function applySimulatedLatency(transport: Transport | undefined, 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 transport?.simulateLatency(halfwayMS);\n\n if (_originalRoomOnMessage == null) {\n _originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = _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 * Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).\n * Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.\n */\nexport function parseLatencyEnv(): number | undefined {\n const value = process.env.COLYSEUS_LATENCY;\n if (value === undefined || value === '') { return undefined; }\n\n const milliseconds = Number(value);\n if (isNaN(milliseconds)) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);\n return undefined;\n }\n return milliseconds;\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 const server = isDevMode\n ? new DevServer<T, R>(serverOptions)\n : new Server<T, R>(serverOptions);\n\n server.router = routes;\n server['~rooms'] = rooms;\n\n // in dev mode the plugin reads `~rooms` back and owns their\n // (re-)registration across HMR reloads\n if (!isDevMode) {\n registerRoomDefinitions(rooms);\n }\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
+ "names": ["greet", "err", "module"]
7
7
  }
package/build/Server.d.ts CHANGED
@@ -98,6 +98,16 @@ export declare class Server<RoomTypes extends Record<string, RegisteredHandler>
98
98
  protected _onTransportReady: Deferred<Transport>;
99
99
  static current: Server<any, any> | undefined;
100
100
  constructor(options?: ServerOptions);
101
+ /**
102
+ * Every side effect of construction: the dev-mode flag, the transport, the
103
+ * matchmaker and the process signal handlers. Distinct from
104
+ * {@link _bootServices}, which prepares user services once we're listening.
105
+ *
106
+ * Isolated so {@link DevServer} can opt out of all four — under
107
+ * `colyseus/vite` the plugin owns them, and user code still gets a real
108
+ * `Server` to call.
109
+ */
110
+ protected bootRuntime(): void;
101
111
  attach(options: ServerOptions): Promise<void>;
102
112
  /**
103
113
  * Bind the server into the port specified.
package/build/Server.mjs CHANGED
@@ -17,14 +17,27 @@ var Server = class _Server {
17
17
  this._onTransportReady = new Deferred();
18
18
  this.onShutdownCallback = () => Promise.resolve();
19
19
  this.onBeforeShutdownCallback = () => Promise.resolve();
20
- const {
21
- gracefullyShutdown: gracefullyShutdown2 = true,
22
- greet: greet2 = true
23
- } = options;
24
- setDevMode(options.devMode === true);
20
+ const { greet: greet2 = true } = options;
25
21
  this.options = options;
26
22
  this.greet = greet2;
27
23
  _Server.current = this;
24
+ if (options.logger) {
25
+ setLogger(options.logger);
26
+ }
27
+ this.bootRuntime();
28
+ }
29
+ /**
30
+ * Every side effect of construction: the dev-mode flag, the transport, the
31
+ * matchmaker and the process signal handlers. Distinct from
32
+ * {@link _bootServices}, which prepares user services once we're listening.
33
+ *
34
+ * Isolated so {@link DevServer} can opt out of all four — under
35
+ * `colyseus/vite` the plugin owns them, and user code still gets a real
36
+ * `Server` to call.
37
+ */
38
+ bootRuntime() {
39
+ const options = this.options;
40
+ setDevMode(options.devMode === true);
28
41
  this.attach(options);
29
42
  matchMaker.setup(
30
43
  options.presence,
@@ -35,12 +48,9 @@ var Server = class _Server {
35
48
  this.presence = matchMaker.presence;
36
49
  this.driver = matchMaker.driver;
37
50
  });
38
- if (gracefullyShutdown2) {
51
+ if (options.gracefullyShutdown !== false) {
39
52
  registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));
40
53
  }
41
- if (options.logger) {
42
- setLogger(options.logger);
43
- }
44
54
  }
45
55
  async attach(options) {
46
56
  this.transport = options.transport || await this.getDefaultTransport(options);
@@ -266,6 +276,30 @@ var Server = class _Server {
266
276
  }
267
277
  }
268
278
  };
279
+ var devWarnings = /* @__PURE__ */ new Set();
280
+ function warnDevOnce(message) {
281
+ if (devWarnings.has(message)) {
282
+ return;
283
+ }
284
+ devWarnings.add(message);
285
+ logger.warn(message);
286
+ }
287
+ var DevServer = class extends Server {
288
+ bootRuntime() {
289
+ this.transport = getTransport();
290
+ this._onTransportReady.resolve(this.transport);
291
+ this.presence = matchMaker.presence;
292
+ this.driver = matchMaker.driver;
293
+ if (this.options.transport) {
294
+ warnDevOnce("\u26A0\uFE0F 'transport' is ignored in dev mode \u2014 colyseus/vite serves WebSockets over Vite's own HTTP server.");
295
+ }
296
+ }
297
+ /** The plugin already serves Colyseus on Vite's own HTTP server and port. */
298
+ async listen(_port, _hostname, _backlog, listeningListener) {
299
+ warnDevOnce("\u26A0\uFE0F listen() is ignored in dev mode \u2014 connect to the Vite dev server's URL instead.");
300
+ listeningListener?.();
301
+ }
302
+ };
269
303
  var _originalRoomOnMessage = null;
270
304
  function applySimulatedLatency(transport, milliseconds) {
271
305
  if (milliseconds > 0) {
@@ -319,18 +353,12 @@ function unregisterRoomDefinitions(roomNames) {
319
353
  }
320
354
  function defineServer(options) {
321
355
  const { rooms, routes, ...serverOptions } = options;
322
- if (isDevMode) {
323
- return {
324
- options: serverOptions,
325
- router: routes,
326
- "~rooms": rooms,
327
- // the vite plugin registers the transport before importing user code
328
- simulateLatency: (milliseconds) => applySimulatedLatency(getTransport(), milliseconds)
329
- };
330
- }
331
- const server = new Server(serverOptions);
356
+ const server = isDevMode ? new DevServer(serverOptions) : new Server(serverOptions);
332
357
  server.router = routes;
333
- registerRoomDefinitions(rooms);
358
+ server["~rooms"] = rooms;
359
+ if (!isDevMode) {
360
+ registerRoomDefinitions(rooms);
361
+ }
334
362
  return server;
335
363
  }
336
364
  function defineRoom(roomKlass, defaultOptions) {
@@ -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';\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 { getTransport, setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport, createRouter } 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 * Booted before `matchMaker.accept()`. Structural so `@colyseus/core` keeps\n * no runtime dep on `@colyseus/database` \u2014 any `{ boot(): Promise<void> }`\n * works. The optional `applyRouterDefaults` is invoked after boot with the\n * user's router so the database can contribute endpoints (e.g. auth routes).\n */\n database?: {\n boot(): Promise<void>;\n applyRouterDefaults?(router: Router): Router | Promise<Router>;\n },\n\n /**\n * Mount `@colyseus/auth` routes into the router. Pass an options object\n * (forwarded to `auth.endpoints(...)`) to wire it explicitly \u2014 useful when\n * `database` is not in use. `false` disables auto-mounting even if a\n * `database` is present and would have provided defaults.\n */\n auth?: false | {\n settings?: Record<string, any>;\n oauth?: boolean | { cookieSecret?: string };\n prefix?: string;\n },\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 // Implicit default for callers that omit explicit Server reference \u2014 e.g.\n // `playground()` reads `Server.current.router.endpoints` at request time.\n // Last-construction wins; multi-server setups should reference instances\n // explicitly.\n static current: Server<any, any> | undefined;\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 (Server as { current: Server<any, any> | undefined }).current = this as Server<any, any>;\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 // `options.express` runs in `listen()` after `database.boot()` so user\n // code reading `database.auth.settings` etc. finds services instantiated.\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 await this._bootServices();\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 await this._bootServices();\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 *\n * May be called at any time \u2014 before or after `listen()`. Also available\n * via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which\n * overrides calls made before the server boots.\n *\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n // transport may still be resolving (constructor's attach() imports the default one)\n this._onTransportReady.then(\n (transport) => applySimulatedLatency(transport, milliseconds),\n () => {/* missing-transport error already surfaces via attach()/listen() */},\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 // Boot sequence shared by listen() and serverless(): user `beforeListen`\n // hook, database boot, framework router defaults (auth/db endpoints), then\n // the express app. serverless() needs this too \u2014 its exported server still\n // serves the express callback and auth/database routes.\n private async _bootServices(): Promise<void> {\n const { beforeListen, database, express } = this.options;\n\n // boot runs after module evaluation, so the env var wins over\n // simulateLatency() calls made at the top level of user code\n const envLatency = parseLatencyEnv();\n if (envLatency !== undefined) { this.simulateLatency(envLatency); }\n\n if (beforeListen) { await beforeListen(); }\n if (database) { await database.boot(); }\n\n await this._applyRouterDefaults();\n\n if (express) {\n await this._onTransportReady;\n if (this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await express(expressApp);\n }\n }\n }\n\n // Extend the user's router with framework-contributed endpoints. An explicit\n // `auth` option wins over the database's `applyRouterDefaults` so a non-DB\n // setup can still auto-mount @colyseus/auth, and a DB user can opt out.\n private async _applyRouterDefaults(): Promise<void> {\n const { auth, database } = this.options;\n const wantsAuth = auth !== undefined && auth !== false;\n const wantsDatabase = auth === undefined && !!database?.applyRouterDefaults;\n if (!wantsAuth && !wantsDatabase) { return; }\n\n // Boot an empty router if the user didn't pass `routes` \u2014 the framework's\n // default routes get layered on later in the `transport.listen()` callback.\n this.router ??= createRouter({}) as unknown as Routes;\n\n if (wantsAuth) {\n const authMod: any = await dynamicImport('@colyseus/auth').catch(() => undefined);\n const endpointsFn = authMod?.auth?.endpoints ?? authMod?.default?.auth?.endpoints;\n if (typeof endpointsFn === 'function') {\n this.router = (this.router as Router).extend(endpointsFn(auth)) as Routes;\n }\n } else {\n const updated = await database!.applyRouterDefaults!(this.router as Router);\n if (updated) { this.router = updated as Routes; }\n }\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\nlet _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n/**\n * Applies both halves of the round-trip latency simulation: delays the\n * transport's outgoing messages and the Room's incoming message handling.\n *\n * Prefer `server.simulateLatency()` \u2014 this is the shared core behind it, the\n * dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where\n * no `Server` instance (or transport, yet) may exist.\n */\nexport function applySimulatedLatency(transport: Transport | undefined, 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 transport?.simulateLatency(halfwayMS);\n\n if (_originalRoomOnMessage == null) {\n _originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = _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 * Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).\n * Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.\n */\nexport function parseLatencyEnv(): number | undefined {\n const value = process.env.COLYSEUS_LATENCY;\n if (value === undefined || value === '') { return undefined; }\n\n const milliseconds = Number(value);\n if (isNaN(milliseconds)) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);\n return undefined;\n }\n return milliseconds;\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 // the vite plugin registers the transport before importing user code\n simulateLatency: (milliseconds: number) => applySimulatedLatency(getTransport(), milliseconds),\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
- "names": ["gracefullyShutdown", "greet", "err"]
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 { getTransport, setTransport, Transport } from './Transport.ts';\nimport { logger, setLogger } from './Logger.ts';\nimport { setDevMode, isDevMode } from './utils/DevMode.ts';\nimport { type Router, bindRouterToTransport, createRouter } 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 * Booted before `matchMaker.accept()`. Structural so `@colyseus/core` keeps\n * no runtime dep on `@colyseus/database` \u2014 any `{ boot(): Promise<void> }`\n * works. The optional `applyRouterDefaults` is invoked after boot with the\n * user's router so the database can contribute endpoints (e.g. auth routes).\n */\n database?: {\n boot(): Promise<void>;\n applyRouterDefaults?(router: Router): Router | Promise<Router>;\n },\n\n /**\n * Mount `@colyseus/auth` routes into the router. Pass an options object\n * (forwarded to `auth.endpoints(...)`) to wire it explicitly \u2014 useful when\n * `database` is not in use. `false` disables auto-mounting even if a\n * `database` is present and would have provided defaults.\n */\n auth?: false | {\n settings?: Record<string, any>;\n oauth?: boolean | { cookieSecret?: string };\n prefix?: string;\n },\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 // Implicit default for callers that omit explicit Server reference \u2014 e.g.\n // `playground()` reads `Server.current.router.endpoints` at request time.\n // Last-construction wins; multi-server setups should reference instances\n // explicitly.\n static current: Server<any, any> | undefined;\n\n constructor(options: ServerOptions = {}) {\n const { greet = true } = options;\n\n this.options = options;\n this.greet = greet;\n\n (Server as { current: Server<any, any> | undefined }).current = this as Server<any, any>;\n\n if (options.logger) {\n setLogger(options.logger);\n }\n\n this.bootRuntime();\n }\n\n /**\n * Every side effect of construction: the dev-mode flag, the transport, the\n * matchmaker and the process signal handlers. Distinct from\n * {@link _bootServices}, which prepares user services once we're listening.\n *\n * Isolated so {@link DevServer} can opt out of all four \u2014 under\n * `colyseus/vite` the plugin owns them, and user code still gets a real\n * `Server` to call.\n */\n protected bootRuntime() {\n const options = this.options;\n\n setDevMode(options.devMode === true);\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 (options.gracefullyShutdown !== false) {\n registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));\n }\n }\n\n public async attach(options: ServerOptions) {\n this.transport = options.transport || await this.getDefaultTransport(options);\n // `options.express` runs in `listen()` after `database.boot()` so user\n // code reading `database.auth.settings` etc. finds services instantiated.\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 await this._bootServices();\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 await this._bootServices();\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 *\n * May be called at any time \u2014 before or after `listen()`. Also available\n * via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which\n * overrides calls made before the server boots.\n *\n * @param milliseconds round trip latency in milliseconds.\n */\n public simulateLatency(milliseconds: number) {\n // transport may still be resolving (constructor's attach() imports the default one)\n this._onTransportReady.then(\n (transport) => applySimulatedLatency(transport, milliseconds),\n () => {/* missing-transport error already surfaces via attach()/listen() */},\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 // Boot sequence shared by listen() and serverless(): user `beforeListen`\n // hook, database boot, framework router defaults (auth/db endpoints), then\n // the express app. serverless() needs this too \u2014 its exported server still\n // serves the express callback and auth/database routes.\n private async _bootServices(): Promise<void> {\n const { beforeListen, database, express } = this.options;\n\n // boot runs after module evaluation, so the env var wins over\n // simulateLatency() calls made at the top level of user code\n const envLatency = parseLatencyEnv();\n if (envLatency !== undefined) { this.simulateLatency(envLatency); }\n\n if (beforeListen) { await beforeListen(); }\n if (database) { await database.boot(); }\n\n await this._applyRouterDefaults();\n\n if (express) {\n await this._onTransportReady;\n if (this.transport.getExpressApp) {\n const expressApp = await this.transport.getExpressApp();\n await express(expressApp);\n }\n }\n }\n\n // Extend the user's router with framework-contributed endpoints. An explicit\n // `auth` option wins over the database's `applyRouterDefaults` so a non-DB\n // setup can still auto-mount @colyseus/auth, and a DB user can opt out.\n private async _applyRouterDefaults(): Promise<void> {\n const { auth, database } = this.options;\n const wantsAuth = auth !== undefined && auth !== false;\n const wantsDatabase = auth === undefined && !!database?.applyRouterDefaults;\n if (!wantsAuth && !wantsDatabase) { return; }\n\n // Boot an empty router if the user didn't pass `routes` \u2014 the framework's\n // default routes get layered on later in the `transport.listen()` callback.\n this.router ??= createRouter({}) as unknown as Routes;\n\n if (wantsAuth) {\n const authMod: any = await dynamicImport('@colyseus/auth').catch(() => undefined);\n const endpointsFn = authMod?.auth?.endpoints ?? authMod?.default?.auth?.endpoints;\n if (typeof endpointsFn === 'function') {\n this.router = (this.router as Router).extend(endpointsFn(auth)) as Routes;\n }\n } else {\n const updated = await database!.applyRouterDefaults!(this.router as Router);\n if (updated) { this.router = updated as Routes; }\n }\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\n// the user module re-evaluates on every HMR reload \u2014 warn once per process\nconst devWarnings = new Set<string>();\nfunction warnDevOnce(message: string) {\n if (devWarnings.has(message)) { return; }\n devWarnings.add(message);\n logger.warn(message);\n}\n\n/**\n * The `Server` handed to user code under `colyseus/vite`.\n *\n * The plugin owns the HTTP server, the transport, the matchmaker and the\n * process lifecycle, so constructing one has no side effects \u2014 it only\n * carries the user's configuration back to the plugin. Everything else is\n * inherited, so the documented `Server` API keeps working in dev mode.\n */\nclass DevServer<\n RoomTypes extends Record<string, RegisteredHandler> = any,\n Routes extends Router = any\n> extends Server<RoomTypes, Routes> {\n\n protected bootRuntime() {\n // the plugin creates the transport and runs matchMaker.setup() before\n // importing user code, so both are already available here\n this.transport = getTransport();\n this._onTransportReady.resolve(this.transport);\n this.presence = matchMaker.presence;\n this.driver = matchMaker.driver;\n\n if (this.options.transport) {\n warnDevOnce(\"\u26A0\uFE0F 'transport' is ignored in dev mode \u2014 colyseus/vite serves WebSockets over Vite's own HTTP server.\");\n }\n }\n\n /** The plugin already serves Colyseus on Vite's own HTTP server and port. */\n public async listen(_port?: number | string, _hostname?: string, _backlog?: number, listeningListener?: Function) {\n warnDevOnce(\"\u26A0\uFE0F listen() is ignored in dev mode \u2014 connect to the Vite dev server's URL instead.\");\n listeningListener?.();\n }\n}\n\nlet _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;\n\n/**\n * Applies both halves of the round-trip latency simulation: delays the\n * transport's outgoing messages and the Room's incoming message handling.\n *\n * Prefer `server.simulateLatency()` \u2014 this is the shared core behind it, the\n * dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where\n * no `Server` instance (or transport, yet) may exist.\n */\nexport function applySimulatedLatency(transport: Transport | undefined, 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 transport?.simulateLatency(halfwayMS);\n\n if (_originalRoomOnMessage == null) {\n _originalRoomOnMessage = Room.prototype['_onMessage'];\n }\n\n const originalOnMessage = _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 * Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).\n * Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.\n */\nexport function parseLatencyEnv(): number | undefined {\n const value = process.env.COLYSEUS_LATENCY;\n if (value === undefined || value === '') { return undefined; }\n\n const milliseconds = Number(value);\n if (isNaN(milliseconds)) {\n logger.warn(`\uD83D\uDCF6\uFE0F\u2757 Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);\n return undefined;\n }\n return milliseconds;\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 const server = isDevMode\n ? new DevServer<T, R>(serverOptions)\n : new Server<T, R>(serverOptions);\n\n server.router = routes;\n server['~rooms'] = rooms;\n\n // in dev mode the plugin reads `~rooms` back and owns their\n // (re-)registration across HMR reloads\n if (!isDevMode) {\n registerRoomDefinitions(rooms);\n }\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
+ "names": ["greet", "err"]
7
7
  }
@@ -30,10 +30,11 @@ var import_DevMode = require("../utils/DevMode.cjs");
30
30
  var LocalPresence = class {
31
31
  constructor() {
32
32
  this.subscriptions = new import_events.EventEmitter();
33
- this.data = {};
34
- this.hash = {};
35
- this.keys = {};
36
- this.timeouts = {};
33
+ // null-proto: keys are caller-controlled, must never resolve through Object.prototype
34
+ this.data = /* @__PURE__ */ Object.create(null);
35
+ this.hash = /* @__PURE__ */ Object.create(null);
36
+ this.keys = /* @__PURE__ */ Object.create(null);
37
+ this.timeouts = /* @__PURE__ */ Object.create(null);
37
38
  if (import_DevMode.isDevMode && (0, import_DevMode.hasDevModeCache)()) {
38
39
  const cache = (0, import_DevMode.getDevModeCache)();
39
40
  if (cache.data) {
@@ -124,7 +125,7 @@ var LocalPresence = class {
124
125
  return (this.data[key] || []).length;
125
126
  }
126
127
  async sinter(...keys) {
127
- const intersection = {};
128
+ const intersection = /* @__PURE__ */ Object.create(null);
128
129
  for (let i = 0, l = keys.length; i < l; i++) {
129
130
  (await this.smembers(keys[i])).forEach((member) => {
130
131
  if (!intersection[member]) {
@@ -142,14 +143,14 @@ var LocalPresence = class {
142
143
  }
143
144
  hset(key, field, value) {
144
145
  if (!this.hash[key]) {
145
- this.hash[key] = {};
146
+ this.hash[key] = /* @__PURE__ */ Object.create(null);
146
147
  }
147
148
  this.hash[key][field] = value;
148
149
  return Promise.resolve(true);
149
150
  }
150
151
  hincrby(key, field, incrBy) {
151
152
  if (!this.hash[key]) {
152
- this.hash[key] = {};
153
+ this.hash[key] = /* @__PURE__ */ Object.create(null);
153
154
  }
154
155
  let value = Number(this.hash[key][field] || "0");
155
156
  value += incrBy;
@@ -158,7 +159,7 @@ var LocalPresence = class {
158
159
  }
159
160
  hincrbyex(key, field, incrBy, expireInSeconds) {
160
161
  if (!this.hash[key]) {
161
- this.hash[key] = {};
162
+ this.hash[key] = /* @__PURE__ */ Object.create(null);
162
163
  }
163
164
  let value = Number(this.hash[key][field] || "0");
164
165
  value += incrBy;
@@ -176,7 +177,7 @@ var LocalPresence = class {
176
177
  return typeof this.hash[key] === "object" ? this.hash[key][field] ?? null : null;
177
178
  }
178
179
  async hgetall(key) {
179
- return this.hash[key] || {};
180
+ return { ...this.hash[key] };
180
181
  }
181
182
  hdel(key, field) {
182
183
  const success = this.hash?.[key]?.[field] !== void 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presence/LocalPresence.ts"],
4
- "sourcesContent": ["\nimport { EventEmitter } from 'events';\nimport { spliceOne } from '../utils/Utils.ts';\nimport type { Presence } from './Presence.ts';\n\nimport { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '../utils/DevMode.ts';\n\ntype Callback = (...args: any[]) => void;\n\nexport class LocalPresence implements Presence {\n public subscriptions: EventEmitter = new EventEmitter();\n\n public data: {[roomName: string]: string[]} = {};\n public hash: {[roomName: string]: {[key: string]: string}} = {};\n\n public keys: {[name: string]: string | number} = {};\n\n private timeouts: {[name: string]: NodeJS.Timeout} = {};\n\n constructor() {\n //\n // reload from local cache on devMode\n //\n if (\n isDevMode &&\n hasDevModeCache()\n ) {\n const cache = getDevModeCache();\n if (cache.data) { this.data = cache.data; }\n if (cache.hash) { this.hash = cache.hash; }\n if (cache.keys) { this.keys = cache.keys; }\n }\n }\n\n public subscribe(topic: string, callback: (...args: any[]) => void) {\n this.subscriptions.on(topic, callback);\n return Promise.resolve(this);\n }\n\n public unsubscribe(topic: string, callback?: Callback) {\n if (callback) {\n this.subscriptions.removeListener(topic, callback);\n\n } else {\n this.subscriptions.removeAllListeners(topic);\n }\n\n return this;\n }\n\n public publish(topic: string, data: any) {\n this.subscriptions.emit(topic, data);\n return this;\n }\n\n public async channels (pattern?: string) {\n let eventNames = this.subscriptions.eventNames() as string[];\n if (pattern) {\n //\n // This is a limited glob pattern to regexp implementation.\n // If needed, we can use a full implementation like picomatch: https://github.com/micromatch/picomatch/\n //\n const regexp = new RegExp(\n pattern.\n replaceAll(\".\", \"\\\\.\").\n replaceAll(\"$\", \"\\\\$\").\n replaceAll(\"*\", \".*\").\n replaceAll(\"?\", \".\"),\n \"i\"\n );\n eventNames = eventNames.filter((eventName) => regexp.test(eventName));\n }\n return eventNames;\n }\n\n public async exists(key: string): Promise<boolean> {\n return (\n this.keys[key] !== undefined ||\n this.data[key] !== undefined ||\n this.hash[key] !== undefined\n );\n }\n\n public set(key: string, value: string) {\n this.keys[key] = value;\n }\n\n public setex(key: string, value: string, seconds: number) {\n this.keys[key] = value;\n this.expire(key, seconds);\n }\n\n public expire(key: string, seconds: number) {\n // ensure previous timeout is clear before setting another one.\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.keys[key];\n delete this.timeouts[key];\n }, seconds * 1000);\n }\n\n public get(key: string) {\n return this.keys[key];\n }\n\n public del(key: string) {\n delete this.keys[key];\n delete this.data[key];\n delete this.hash[key];\n }\n\n public sadd(key: string, value: any) {\n if (!this.data[key]) {\n this.data[key] = [];\n }\n\n if (this.data[key].indexOf(value) === -1) {\n this.data[key].push(value);\n }\n }\n\n public async smembers(key: string): Promise<string[]> {\n return this.data[key] || [];\n }\n\n public async sismember(key: string, field: string) {\n return this.data[key] && this.data[key].includes(field) ? 1 : 0;\n }\n\n public srem(key: string, value: any) {\n if (this.data[key]) {\n spliceOne(this.data[key], this.data[key].indexOf(value));\n }\n }\n\n public scard(key: string) {\n return (this.data[key] || []).length;\n }\n\n public async sinter(...keys: string[]) {\n const intersection: {[value: string]: number} = {};\n\n for (let i = 0, l = keys.length; i < l; i++) {\n (await this.smembers(keys[i])).forEach((member) => {\n if (!intersection[member]) {\n intersection[member] = 0;\n }\n\n intersection[member]++;\n });\n }\n\n return Object.keys(intersection).reduce((prev, curr) => {\n if (intersection[curr] > 1) {\n prev.push(curr);\n }\n return prev;\n }, []);\n }\n\n public hset(key: string, field: string, value: string) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n this.hash[key][field] = value;\n return Promise.resolve(true);\n }\n\n public hincrby(key: string, field: string, incrBy: number) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n return Promise.resolve(value);\n }\n\n public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n\n //\n // FIXME: delete only hash[key][field]\n // (we can't use \"HEXPIRE\" in Redis because it's only available since Redis version 7.4.0+)\n //\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.hash[key];\n delete this.timeouts[key];\n }, expireInSeconds * 1000);\n\n return Promise.resolve(value);\n }\n\n public async hget(key: string, field: string) {\n return (typeof(this.hash[key]) === 'object')\n ? this.hash[key][field] ?? null\n : null;\n }\n\n public async hgetall(key: string) {\n return this.hash[key] || {};\n }\n\n public hdel(key: string, field: any) {\n const success = this.hash?.[key]?.[field] !== undefined;\n if (success) {\n delete this.hash[key][field];\n }\n return Promise.resolve(success);\n }\n\n public async hlen(key: string) {\n return this.hash[key] && Object.keys(this.hash[key]).length || 0;\n }\n\n public async incr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)++;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public async decr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)--;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public llen(key: string) {\n return Promise.resolve((this.data[key] && this.data[key].length) || 0);\n }\n\n public rpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].push(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].unshift(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpop(key: string): Promise<string> {\n return Promise.resolve(Array.isArray(this.data[key])\n ? this.data[key].shift()\n : null);\n }\n\n public rpop(key: string): Promise<string | null> {\n return Promise.resolve(this.data[key].pop());\n }\n\n public brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null> {\n const keys = args.slice(0, -1) as string[];\n const timeoutInSeconds = args[args.length - 1] as number;\n\n const getFirstPopulated = (): [string, string] | null => {\n const keyWithValue = keys.find(key => this.data[key] && this.data[key].length > 0);\n if (keyWithValue) {\n return [keyWithValue, this.data[keyWithValue].pop()];\n } else {\n return null;\n }\n }\n\n const firstPopulated = getFirstPopulated();\n\n if (firstPopulated) {\n // return first populated key + item\n return Promise.resolve(firstPopulated);\n\n } else {\n // 8 retries per second\n const maxRetries = timeoutInSeconds * 8;\n\n let tries = 0;\n return new Promise((resolve) => {\n const interval = setInterval(() => {\n tries++;\n\n const firstPopulated = getFirstPopulated();\n if (firstPopulated) {\n clearInterval(interval);\n return resolve(firstPopulated);\n\n } else if (tries >= maxRetries) {\n clearInterval(interval);\n return resolve(null);\n }\n\n }, (timeoutInSeconds * 1000) / maxRetries);\n });\n }\n }\n\n public setMaxListeners(number: number) {\n this.subscriptions.setMaxListeners(number);\n }\n\n public shutdown() {\n if (isDevMode) {\n writeDevModeCache({\n data: this.data,\n hash: this.hash,\n keys: this.keys\n });\n }\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\nimport { EventEmitter } from 'events';\nimport { spliceOne } from '../utils/Utils.ts';\nimport type { Presence } from './Presence.ts';\n\nimport { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '../utils/DevMode.ts';\n\ntype Callback = (...args: any[]) => void;\n\nexport class LocalPresence implements Presence {\n public subscriptions: EventEmitter = new EventEmitter();\n\n // null-proto: keys are caller-controlled, must never resolve through Object.prototype\n public data: {[roomName: string]: string[]} = Object.create(null);\n public hash: {[roomName: string]: {[key: string]: string}} = Object.create(null);\n\n public keys: {[name: string]: string | number} = Object.create(null);\n\n private timeouts: {[name: string]: NodeJS.Timeout} = Object.create(null);\n\n constructor() {\n //\n // reload from local cache on devMode\n //\n if (\n isDevMode &&\n hasDevModeCache()\n ) {\n const cache = getDevModeCache();\n if (cache.data) { this.data = cache.data; }\n if (cache.hash) { this.hash = cache.hash; }\n if (cache.keys) { this.keys = cache.keys; }\n }\n }\n\n public subscribe(topic: string, callback: (...args: any[]) => void) {\n this.subscriptions.on(topic, callback);\n return Promise.resolve(this);\n }\n\n public unsubscribe(topic: string, callback?: Callback) {\n if (callback) {\n this.subscriptions.removeListener(topic, callback);\n\n } else {\n this.subscriptions.removeAllListeners(topic);\n }\n\n return this;\n }\n\n public publish(topic: string, data: any) {\n this.subscriptions.emit(topic, data);\n return this;\n }\n\n public async channels (pattern?: string) {\n let eventNames = this.subscriptions.eventNames() as string[];\n if (pattern) {\n //\n // This is a limited glob pattern to regexp implementation.\n // If needed, we can use a full implementation like picomatch: https://github.com/micromatch/picomatch/\n //\n const regexp = new RegExp(\n pattern.\n replaceAll(\".\", \"\\\\.\").\n replaceAll(\"$\", \"\\\\$\").\n replaceAll(\"*\", \".*\").\n replaceAll(\"?\", \".\"),\n \"i\"\n );\n eventNames = eventNames.filter((eventName) => regexp.test(eventName));\n }\n return eventNames;\n }\n\n public async exists(key: string): Promise<boolean> {\n return (\n this.keys[key] !== undefined ||\n this.data[key] !== undefined ||\n this.hash[key] !== undefined\n );\n }\n\n public set(key: string, value: string) {\n this.keys[key] = value;\n }\n\n public setex(key: string, value: string, seconds: number) {\n this.keys[key] = value;\n this.expire(key, seconds);\n }\n\n public expire(key: string, seconds: number) {\n // ensure previous timeout is clear before setting another one.\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.keys[key];\n delete this.timeouts[key];\n }, seconds * 1000);\n }\n\n public get(key: string) {\n return this.keys[key];\n }\n\n public del(key: string) {\n delete this.keys[key];\n delete this.data[key];\n delete this.hash[key];\n }\n\n public sadd(key: string, value: any) {\n if (!this.data[key]) {\n this.data[key] = [];\n }\n\n if (this.data[key].indexOf(value) === -1) {\n this.data[key].push(value);\n }\n }\n\n public async smembers(key: string): Promise<string[]> {\n return this.data[key] || [];\n }\n\n public async sismember(key: string, field: string) {\n return this.data[key] && this.data[key].includes(field) ? 1 : 0;\n }\n\n public srem(key: string, value: any) {\n if (this.data[key]) {\n spliceOne(this.data[key], this.data[key].indexOf(value));\n }\n }\n\n public scard(key: string) {\n return (this.data[key] || []).length;\n }\n\n public async sinter(...keys: string[]) {\n const intersection: {[value: string]: number} = Object.create(null);\n\n for (let i = 0, l = keys.length; i < l; i++) {\n (await this.smembers(keys[i])).forEach((member) => {\n if (!intersection[member]) {\n intersection[member] = 0;\n }\n\n intersection[member]++;\n });\n }\n\n return Object.keys(intersection).reduce((prev, curr) => {\n if (intersection[curr] > 1) {\n prev.push(curr);\n }\n return prev;\n }, []);\n }\n\n public hset(key: string, field: string, value: string) {\n if (!this.hash[key]) { this.hash[key] = Object.create(null); }\n this.hash[key][field] = value;\n return Promise.resolve(true);\n }\n\n public hincrby(key: string, field: string, incrBy: number) {\n if (!this.hash[key]) { this.hash[key] = Object.create(null); }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n return Promise.resolve(value);\n }\n\n public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {\n if (!this.hash[key]) { this.hash[key] = Object.create(null); }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n\n //\n // FIXME: delete only hash[key][field]\n // (we can't use \"HEXPIRE\" in Redis because it's only available since Redis version 7.4.0+)\n //\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.hash[key];\n delete this.timeouts[key];\n }, expireInSeconds * 1000);\n\n return Promise.resolve(value);\n }\n\n public async hget(key: string, field: string) {\n return (typeof(this.hash[key]) === 'object')\n ? this.hash[key][field] ?? null\n : null;\n }\n\n public async hgetall(key: string) {\n return { ...this.hash[key] }; // fresh plain object, like redis\n }\n\n public hdel(key: string, field: any) {\n const success = this.hash?.[key]?.[field] !== undefined;\n if (success) {\n delete this.hash[key][field];\n }\n return Promise.resolve(success);\n }\n\n public async hlen(key: string) {\n return this.hash[key] && Object.keys(this.hash[key]).length || 0;\n }\n\n public async incr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)++;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public async decr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)--;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public llen(key: string) {\n return Promise.resolve((this.data[key] && this.data[key].length) || 0);\n }\n\n public rpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].push(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].unshift(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpop(key: string): Promise<string> {\n return Promise.resolve(Array.isArray(this.data[key])\n ? this.data[key].shift()\n : null);\n }\n\n public rpop(key: string): Promise<string | null> {\n return Promise.resolve(this.data[key].pop());\n }\n\n public brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null> {\n const keys = args.slice(0, -1) as string[];\n const timeoutInSeconds = args[args.length - 1] as number;\n\n const getFirstPopulated = (): [string, string] | null => {\n const keyWithValue = keys.find(key => this.data[key] && this.data[key].length > 0);\n if (keyWithValue) {\n return [keyWithValue, this.data[keyWithValue].pop()];\n } else {\n return null;\n }\n }\n\n const firstPopulated = getFirstPopulated();\n\n if (firstPopulated) {\n // return first populated key + item\n return Promise.resolve(firstPopulated);\n\n } else {\n // 8 retries per second\n const maxRetries = timeoutInSeconds * 8;\n\n let tries = 0;\n return new Promise((resolve) => {\n const interval = setInterval(() => {\n tries++;\n\n const firstPopulated = getFirstPopulated();\n if (firstPopulated) {\n clearInterval(interval);\n return resolve(firstPopulated);\n\n } else if (tries >= maxRetries) {\n clearInterval(interval);\n return resolve(null);\n }\n\n }, (timeoutInSeconds * 1000) / maxRetries);\n });\n }\n }\n\n public setMaxListeners(number: number) {\n this.subscriptions.setMaxListeners(number);\n }\n\n public shutdown() {\n if (isDevMode) {\n writeDevModeCache({\n data: this.data,\n hash: this.hash,\n keys: this.keys\n });\n }\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["firstPopulated"]
7
7
  }
@@ -5,10 +5,11 @@ import { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from "
5
5
  var LocalPresence = class {
6
6
  constructor() {
7
7
  this.subscriptions = new EventEmitter();
8
- this.data = {};
9
- this.hash = {};
10
- this.keys = {};
11
- this.timeouts = {};
8
+ // null-proto: keys are caller-controlled, must never resolve through Object.prototype
9
+ this.data = /* @__PURE__ */ Object.create(null);
10
+ this.hash = /* @__PURE__ */ Object.create(null);
11
+ this.keys = /* @__PURE__ */ Object.create(null);
12
+ this.timeouts = /* @__PURE__ */ Object.create(null);
12
13
  if (isDevMode && hasDevModeCache()) {
13
14
  const cache = getDevModeCache();
14
15
  if (cache.data) {
@@ -99,7 +100,7 @@ var LocalPresence = class {
99
100
  return (this.data[key] || []).length;
100
101
  }
101
102
  async sinter(...keys) {
102
- const intersection = {};
103
+ const intersection = /* @__PURE__ */ Object.create(null);
103
104
  for (let i = 0, l = keys.length; i < l; i++) {
104
105
  (await this.smembers(keys[i])).forEach((member) => {
105
106
  if (!intersection[member]) {
@@ -117,14 +118,14 @@ var LocalPresence = class {
117
118
  }
118
119
  hset(key, field, value) {
119
120
  if (!this.hash[key]) {
120
- this.hash[key] = {};
121
+ this.hash[key] = /* @__PURE__ */ Object.create(null);
121
122
  }
122
123
  this.hash[key][field] = value;
123
124
  return Promise.resolve(true);
124
125
  }
125
126
  hincrby(key, field, incrBy) {
126
127
  if (!this.hash[key]) {
127
- this.hash[key] = {};
128
+ this.hash[key] = /* @__PURE__ */ Object.create(null);
128
129
  }
129
130
  let value = Number(this.hash[key][field] || "0");
130
131
  value += incrBy;
@@ -133,7 +134,7 @@ var LocalPresence = class {
133
134
  }
134
135
  hincrbyex(key, field, incrBy, expireInSeconds) {
135
136
  if (!this.hash[key]) {
136
- this.hash[key] = {};
137
+ this.hash[key] = /* @__PURE__ */ Object.create(null);
137
138
  }
138
139
  let value = Number(this.hash[key][field] || "0");
139
140
  value += incrBy;
@@ -151,7 +152,7 @@ var LocalPresence = class {
151
152
  return typeof this.hash[key] === "object" ? this.hash[key][field] ?? null : null;
152
153
  }
153
154
  async hgetall(key) {
154
- return this.hash[key] || {};
155
+ return { ...this.hash[key] };
155
156
  }
156
157
  hdel(key, field) {
157
158
  const success = this.hash?.[key]?.[field] !== void 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presence/LocalPresence.ts"],
4
- "sourcesContent": ["\nimport { EventEmitter } from 'events';\nimport { spliceOne } from '../utils/Utils.ts';\nimport type { Presence } from './Presence.ts';\n\nimport { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '../utils/DevMode.ts';\n\ntype Callback = (...args: any[]) => void;\n\nexport class LocalPresence implements Presence {\n public subscriptions: EventEmitter = new EventEmitter();\n\n public data: {[roomName: string]: string[]} = {};\n public hash: {[roomName: string]: {[key: string]: string}} = {};\n\n public keys: {[name: string]: string | number} = {};\n\n private timeouts: {[name: string]: NodeJS.Timeout} = {};\n\n constructor() {\n //\n // reload from local cache on devMode\n //\n if (\n isDevMode &&\n hasDevModeCache()\n ) {\n const cache = getDevModeCache();\n if (cache.data) { this.data = cache.data; }\n if (cache.hash) { this.hash = cache.hash; }\n if (cache.keys) { this.keys = cache.keys; }\n }\n }\n\n public subscribe(topic: string, callback: (...args: any[]) => void) {\n this.subscriptions.on(topic, callback);\n return Promise.resolve(this);\n }\n\n public unsubscribe(topic: string, callback?: Callback) {\n if (callback) {\n this.subscriptions.removeListener(topic, callback);\n\n } else {\n this.subscriptions.removeAllListeners(topic);\n }\n\n return this;\n }\n\n public publish(topic: string, data: any) {\n this.subscriptions.emit(topic, data);\n return this;\n }\n\n public async channels (pattern?: string) {\n let eventNames = this.subscriptions.eventNames() as string[];\n if (pattern) {\n //\n // This is a limited glob pattern to regexp implementation.\n // If needed, we can use a full implementation like picomatch: https://github.com/micromatch/picomatch/\n //\n const regexp = new RegExp(\n pattern.\n replaceAll(\".\", \"\\\\.\").\n replaceAll(\"$\", \"\\\\$\").\n replaceAll(\"*\", \".*\").\n replaceAll(\"?\", \".\"),\n \"i\"\n );\n eventNames = eventNames.filter((eventName) => regexp.test(eventName));\n }\n return eventNames;\n }\n\n public async exists(key: string): Promise<boolean> {\n return (\n this.keys[key] !== undefined ||\n this.data[key] !== undefined ||\n this.hash[key] !== undefined\n );\n }\n\n public set(key: string, value: string) {\n this.keys[key] = value;\n }\n\n public setex(key: string, value: string, seconds: number) {\n this.keys[key] = value;\n this.expire(key, seconds);\n }\n\n public expire(key: string, seconds: number) {\n // ensure previous timeout is clear before setting another one.\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.keys[key];\n delete this.timeouts[key];\n }, seconds * 1000);\n }\n\n public get(key: string) {\n return this.keys[key];\n }\n\n public del(key: string) {\n delete this.keys[key];\n delete this.data[key];\n delete this.hash[key];\n }\n\n public sadd(key: string, value: any) {\n if (!this.data[key]) {\n this.data[key] = [];\n }\n\n if (this.data[key].indexOf(value) === -1) {\n this.data[key].push(value);\n }\n }\n\n public async smembers(key: string): Promise<string[]> {\n return this.data[key] || [];\n }\n\n public async sismember(key: string, field: string) {\n return this.data[key] && this.data[key].includes(field) ? 1 : 0;\n }\n\n public srem(key: string, value: any) {\n if (this.data[key]) {\n spliceOne(this.data[key], this.data[key].indexOf(value));\n }\n }\n\n public scard(key: string) {\n return (this.data[key] || []).length;\n }\n\n public async sinter(...keys: string[]) {\n const intersection: {[value: string]: number} = {};\n\n for (let i = 0, l = keys.length; i < l; i++) {\n (await this.smembers(keys[i])).forEach((member) => {\n if (!intersection[member]) {\n intersection[member] = 0;\n }\n\n intersection[member]++;\n });\n }\n\n return Object.keys(intersection).reduce((prev, curr) => {\n if (intersection[curr] > 1) {\n prev.push(curr);\n }\n return prev;\n }, []);\n }\n\n public hset(key: string, field: string, value: string) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n this.hash[key][field] = value;\n return Promise.resolve(true);\n }\n\n public hincrby(key: string, field: string, incrBy: number) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n return Promise.resolve(value);\n }\n\n public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {\n if (!this.hash[key]) { this.hash[key] = {}; }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n\n //\n // FIXME: delete only hash[key][field]\n // (we can't use \"HEXPIRE\" in Redis because it's only available since Redis version 7.4.0+)\n //\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.hash[key];\n delete this.timeouts[key];\n }, expireInSeconds * 1000);\n\n return Promise.resolve(value);\n }\n\n public async hget(key: string, field: string) {\n return (typeof(this.hash[key]) === 'object')\n ? this.hash[key][field] ?? null\n : null;\n }\n\n public async hgetall(key: string) {\n return this.hash[key] || {};\n }\n\n public hdel(key: string, field: any) {\n const success = this.hash?.[key]?.[field] !== undefined;\n if (success) {\n delete this.hash[key][field];\n }\n return Promise.resolve(success);\n }\n\n public async hlen(key: string) {\n return this.hash[key] && Object.keys(this.hash[key]).length || 0;\n }\n\n public async incr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)++;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public async decr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)--;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public llen(key: string) {\n return Promise.resolve((this.data[key] && this.data[key].length) || 0);\n }\n\n public rpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].push(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].unshift(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpop(key: string): Promise<string> {\n return Promise.resolve(Array.isArray(this.data[key])\n ? this.data[key].shift()\n : null);\n }\n\n public rpop(key: string): Promise<string | null> {\n return Promise.resolve(this.data[key].pop());\n }\n\n public brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null> {\n const keys = args.slice(0, -1) as string[];\n const timeoutInSeconds = args[args.length - 1] as number;\n\n const getFirstPopulated = (): [string, string] | null => {\n const keyWithValue = keys.find(key => this.data[key] && this.data[key].length > 0);\n if (keyWithValue) {\n return [keyWithValue, this.data[keyWithValue].pop()];\n } else {\n return null;\n }\n }\n\n const firstPopulated = getFirstPopulated();\n\n if (firstPopulated) {\n // return first populated key + item\n return Promise.resolve(firstPopulated);\n\n } else {\n // 8 retries per second\n const maxRetries = timeoutInSeconds * 8;\n\n let tries = 0;\n return new Promise((resolve) => {\n const interval = setInterval(() => {\n tries++;\n\n const firstPopulated = getFirstPopulated();\n if (firstPopulated) {\n clearInterval(interval);\n return resolve(firstPopulated);\n\n } else if (tries >= maxRetries) {\n clearInterval(interval);\n return resolve(null);\n }\n\n }, (timeoutInSeconds * 1000) / maxRetries);\n });\n }\n }\n\n public setMaxListeners(number: number) {\n this.subscriptions.setMaxListeners(number);\n }\n\n public shutdown() {\n if (isDevMode) {\n writeDevModeCache({\n data: this.data,\n hash: this.hash,\n keys: this.keys\n });\n }\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\nimport { EventEmitter } from 'events';\nimport { spliceOne } from '../utils/Utils.ts';\nimport type { Presence } from './Presence.ts';\n\nimport { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '../utils/DevMode.ts';\n\ntype Callback = (...args: any[]) => void;\n\nexport class LocalPresence implements Presence {\n public subscriptions: EventEmitter = new EventEmitter();\n\n // null-proto: keys are caller-controlled, must never resolve through Object.prototype\n public data: {[roomName: string]: string[]} = Object.create(null);\n public hash: {[roomName: string]: {[key: string]: string}} = Object.create(null);\n\n public keys: {[name: string]: string | number} = Object.create(null);\n\n private timeouts: {[name: string]: NodeJS.Timeout} = Object.create(null);\n\n constructor() {\n //\n // reload from local cache on devMode\n //\n if (\n isDevMode &&\n hasDevModeCache()\n ) {\n const cache = getDevModeCache();\n if (cache.data) { this.data = cache.data; }\n if (cache.hash) { this.hash = cache.hash; }\n if (cache.keys) { this.keys = cache.keys; }\n }\n }\n\n public subscribe(topic: string, callback: (...args: any[]) => void) {\n this.subscriptions.on(topic, callback);\n return Promise.resolve(this);\n }\n\n public unsubscribe(topic: string, callback?: Callback) {\n if (callback) {\n this.subscriptions.removeListener(topic, callback);\n\n } else {\n this.subscriptions.removeAllListeners(topic);\n }\n\n return this;\n }\n\n public publish(topic: string, data: any) {\n this.subscriptions.emit(topic, data);\n return this;\n }\n\n public async channels (pattern?: string) {\n let eventNames = this.subscriptions.eventNames() as string[];\n if (pattern) {\n //\n // This is a limited glob pattern to regexp implementation.\n // If needed, we can use a full implementation like picomatch: https://github.com/micromatch/picomatch/\n //\n const regexp = new RegExp(\n pattern.\n replaceAll(\".\", \"\\\\.\").\n replaceAll(\"$\", \"\\\\$\").\n replaceAll(\"*\", \".*\").\n replaceAll(\"?\", \".\"),\n \"i\"\n );\n eventNames = eventNames.filter((eventName) => regexp.test(eventName));\n }\n return eventNames;\n }\n\n public async exists(key: string): Promise<boolean> {\n return (\n this.keys[key] !== undefined ||\n this.data[key] !== undefined ||\n this.hash[key] !== undefined\n );\n }\n\n public set(key: string, value: string) {\n this.keys[key] = value;\n }\n\n public setex(key: string, value: string, seconds: number) {\n this.keys[key] = value;\n this.expire(key, seconds);\n }\n\n public expire(key: string, seconds: number) {\n // ensure previous timeout is clear before setting another one.\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.keys[key];\n delete this.timeouts[key];\n }, seconds * 1000);\n }\n\n public get(key: string) {\n return this.keys[key];\n }\n\n public del(key: string) {\n delete this.keys[key];\n delete this.data[key];\n delete this.hash[key];\n }\n\n public sadd(key: string, value: any) {\n if (!this.data[key]) {\n this.data[key] = [];\n }\n\n if (this.data[key].indexOf(value) === -1) {\n this.data[key].push(value);\n }\n }\n\n public async smembers(key: string): Promise<string[]> {\n return this.data[key] || [];\n }\n\n public async sismember(key: string, field: string) {\n return this.data[key] && this.data[key].includes(field) ? 1 : 0;\n }\n\n public srem(key: string, value: any) {\n if (this.data[key]) {\n spliceOne(this.data[key], this.data[key].indexOf(value));\n }\n }\n\n public scard(key: string) {\n return (this.data[key] || []).length;\n }\n\n public async sinter(...keys: string[]) {\n const intersection: {[value: string]: number} = Object.create(null);\n\n for (let i = 0, l = keys.length; i < l; i++) {\n (await this.smembers(keys[i])).forEach((member) => {\n if (!intersection[member]) {\n intersection[member] = 0;\n }\n\n intersection[member]++;\n });\n }\n\n return Object.keys(intersection).reduce((prev, curr) => {\n if (intersection[curr] > 1) {\n prev.push(curr);\n }\n return prev;\n }, []);\n }\n\n public hset(key: string, field: string, value: string) {\n if (!this.hash[key]) { this.hash[key] = Object.create(null); }\n this.hash[key][field] = value;\n return Promise.resolve(true);\n }\n\n public hincrby(key: string, field: string, incrBy: number) {\n if (!this.hash[key]) { this.hash[key] = Object.create(null); }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n return Promise.resolve(value);\n }\n\n public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {\n if (!this.hash[key]) { this.hash[key] = Object.create(null); }\n let value = Number(this.hash[key][field] || '0');\n value += incrBy;\n this.hash[key][field] = value.toString();\n\n //\n // FIXME: delete only hash[key][field]\n // (we can't use \"HEXPIRE\" in Redis because it's only available since Redis version 7.4.0+)\n //\n if (this.timeouts[key]) {\n clearTimeout(this.timeouts[key]);\n }\n this.timeouts[key] = setTimeout(() => {\n delete this.hash[key];\n delete this.timeouts[key];\n }, expireInSeconds * 1000);\n\n return Promise.resolve(value);\n }\n\n public async hget(key: string, field: string) {\n return (typeof(this.hash[key]) === 'object')\n ? this.hash[key][field] ?? null\n : null;\n }\n\n public async hgetall(key: string) {\n return { ...this.hash[key] }; // fresh plain object, like redis\n }\n\n public hdel(key: string, field: any) {\n const success = this.hash?.[key]?.[field] !== undefined;\n if (success) {\n delete this.hash[key][field];\n }\n return Promise.resolve(success);\n }\n\n public async hlen(key: string) {\n return this.hash[key] && Object.keys(this.hash[key]).length || 0;\n }\n\n public async incr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)++;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public async decr(key: string) {\n if (!this.keys[key]) {\n this.keys[key] = 0;\n }\n (this.keys[key] as number)--;\n return Promise.resolve(this.keys[key] as number);\n }\n\n public llen(key: string) {\n return Promise.resolve((this.data[key] && this.data[key].length) || 0);\n }\n\n public rpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].push(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpush(key: string, ...values: string[]): Promise<number> {\n if (!this.data[key]) { this.data[key] = []; }\n\n let lastLength: number = 0;\n\n values.forEach(value => {\n lastLength = this.data[key].unshift(value);\n });\n\n return Promise.resolve(lastLength);\n }\n\n public lpop(key: string): Promise<string> {\n return Promise.resolve(Array.isArray(this.data[key])\n ? this.data[key].shift()\n : null);\n }\n\n public rpop(key: string): Promise<string | null> {\n return Promise.resolve(this.data[key].pop());\n }\n\n public brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null> {\n const keys = args.slice(0, -1) as string[];\n const timeoutInSeconds = args[args.length - 1] as number;\n\n const getFirstPopulated = (): [string, string] | null => {\n const keyWithValue = keys.find(key => this.data[key] && this.data[key].length > 0);\n if (keyWithValue) {\n return [keyWithValue, this.data[keyWithValue].pop()];\n } else {\n return null;\n }\n }\n\n const firstPopulated = getFirstPopulated();\n\n if (firstPopulated) {\n // return first populated key + item\n return Promise.resolve(firstPopulated);\n\n } else {\n // 8 retries per second\n const maxRetries = timeoutInSeconds * 8;\n\n let tries = 0;\n return new Promise((resolve) => {\n const interval = setInterval(() => {\n tries++;\n\n const firstPopulated = getFirstPopulated();\n if (firstPopulated) {\n clearInterval(interval);\n return resolve(firstPopulated);\n\n } else if (tries >= maxRetries) {\n clearInterval(interval);\n return resolve(null);\n }\n\n }, (timeoutInSeconds * 1000) / maxRetries);\n });\n }\n }\n\n public setMaxListeners(number: number) {\n this.subscriptions.setMaxListeners(number);\n }\n\n public shutdown() {\n if (isDevMode) {\n writeDevModeCache({\n data: this.data,\n hash: this.hash,\n keys: this.keys\n });\n }\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["firstPopulated"]
7
7
  }
@@ -52,8 +52,9 @@ var isDevMode = false;
52
52
  function hasDevModeCache() {
53
53
  return import_fs.default.existsSync(DEVMODE_CACHE_FILE_PATH);
54
54
  }
55
+ var nullProto = (_, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.assign(/* @__PURE__ */ Object.create(null), value) : value;
55
56
  function getDevModeCache() {
56
- return JSON.parse(import_fs.default.readFileSync(DEVMODE_CACHE_FILE_PATH, "utf8")) || {};
57
+ return JSON.parse(import_fs.default.readFileSync(DEVMODE_CACHE_FILE_PATH, "utf8"), nullProto) || {};
57
58
  }
58
59
  function writeDevModeCache(cache) {
59
60
  import_fs.default.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), "utf8");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/DevMode.ts"],
4
- "sourcesContent": ["import fs from 'fs';\nimport path from 'path';\nimport { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';\nimport { logger } from '../Logger.ts';\nimport { debugAndPrintError, debugDevMode } from '../Debug.ts';\nimport { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';\nimport type { Room } from '../Room.ts';\n\nconst DEVMODE_CACHE_FILE_PATH = path.resolve(\".devmode.json\");\n\nexport let isDevMode: boolean = false;\n\nexport function hasDevModeCache() {\n return fs.existsSync(DEVMODE_CACHE_FILE_PATH);\n}\n\nexport function getDevModeCache() {\n return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8')) || {};\n}\n\nexport function writeDevModeCache(cache: any) {\n fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');\n}\n\nexport function setDevMode(bool: boolean) {\n isDevMode = bool;\n}\n\nexport async function reloadFromCache() {\n const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));\n debugDevMode(\"rooms to restore: %i\", roomHistoryList.length);\n\n for (const [roomId, value] of roomHistoryList) {\n let roomHistory: any;\n let recreatedRoom: Room;\n let recreatedRoomListing: any;\n\n try {\n roomHistory = JSON.parse(value);\n debugDevMode(\"restoring room %s (%s)\", roomHistory.roomName, roomId);\n\n recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);\n recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);\n\n } catch (e: any) {\n // unrestorable entry (e.g. room type renamed/removed) \u2014 drop it\n // instead of wedging the boot\n debugAndPrintError(`\u274C couldn't restore room '${roomId}':\\n${e.stack}`);\n await presence.hdel(getRoomRestoreListKey(), roomId);\n continue;\n }\n\n // Restore previous state\n if (roomHistory.hasOwnProperty(\"state\")) {\n try {\n const rawState = JSON.parse(roomHistory.state);\n logger.debug(`\uD83D\uDCCB room '${roomId}' state =>`, rawState);\n\n (recreatedRoom.state as Schema).restore(rawState);\n\n // Restore the encoder's nextUniqueId so refIds increase\n // monotonically across HMR cycles. Without this, restore()\n // always produces the same refIds (0,1,2,3...) and onJoin()\n // always assigns the same next refIds (4,5...), causing the\n // client decoder to reuse stale instances on the 2nd+ cycle.\n if (roomHistory.nextRefId !== undefined) {\n const encoderRoot = recreatedRoom.state[$changes]?.root;\n if (encoderRoot && roomHistory.nextRefId > encoderRoot['nextUniqueId']) {\n encoderRoot['nextUniqueId'] = roomHistory.nextRefId;\n }\n }\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't restore room '${roomId}' state:\\n${e.stack}`);\n }\n }\n\n // Reserve seats for clients from cached history.\n // Skip entries without a reconnectionToken \u2014 these are stale\n // seats from allowReconnection() where the client already left\n // (e.g. page refresh). Restoring them would block room disposal.\n if (roomHistory.clients) {\n for (const clientData of roomHistory.clients) {\n const { sessionId, reconnectionToken } = clientData;\n if (!reconnectionToken) { continue; }\n // TODO: need to restore each client's StateView as well\n await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, recreatedRoom.seatReservationTimeout, false, reconnectionToken]);\n }\n }\n\n // call `onRestoreRoom` with custom 'cache'd property.\n recreatedRoom.onRestoreRoom?.(roomHistory[\"cache\"]);\n\n logger.debug(`\uD83D\uDD04 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(\", \")}`);\n }\n\n if (roomHistoryList.length > 0) {\n logger.debug(\"\u2705\", roomHistoryList.length, \"room(s) have been restored.\");\n }\n}\n\nexport async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {\n for (const room of Object.values(rooms)) {\n const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);\n if (roomHistoryResult) {\n try {\n const roomHistory = JSON.parse(roomHistoryResult);\n\n // custom cache method\n roomHistory[\"cache\"] = room.onCacheRoom?.();\n\n // encode state\n debugDevMode(\"caching room %s (%s)\", room.roomName, room.roomId);\n\n if (room.state) {\n roomHistory[\"state\"] = JSON.stringify(room.state);\n\n // Cache the encoder's nextUniqueId so it can be restored.\n // This ensures refIds increase monotonically across HMR cycles,\n // preventing the client decoder from reusing stale refs that\n // happen to have the same refId as newly created instances.\n const encoderRoot = room.state[$changes]?.root;\n if (encoderRoot) {\n roomHistory[\"nextRefId\"] = encoderRoot['nextUniqueId'];\n }\n }\n\n // cache active clients with their reconnection tokens\n // TODO: need to cache each client's StateView as well\n const activeClients = room.clients.map((client) => ({\n sessionId: client.sessionId,\n reconnectionToken: client.reconnectionToken,\n }));\n\n // collect active client sessionIds to avoid duplicates\n const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));\n\n // also cache reserved seats (they don't have reconnectionTokens yet)\n // filter out reserved seats that are already active clients (from devMode reconnection)\n const reservedSeats = Object.keys(room['_reservedSeats'])\n .filter((sessionId) => !activeSessionIds.has(sessionId))\n .map((sessionId) => ({\n sessionId,\n reconnectionToken: undefined,\n }));\n\n roomHistory[\"clients\"] = activeClients.concat(reservedSeats);\n\n await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));\n\n // Rewrite updated room history\n logger.debug(`\uD83D\uDCBE caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory[\"state\"] !== undefined ? \"yes\" : \"no\"})`);\n\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't cache room '${room.roomId}', due to:\\n${e.stack}`);\n }\n }\n }\n}\n\nexport function getRoomRestoreListKey() {\n return 'roomhistory';\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAe;AACf,kBAAiB;AACjB,oBAAuG;AACvG,oBAAuB;AACvB,mBAAiD;AACjD,wBAA6E;AAG7E,IAAM,0BAA0B,YAAAA,QAAK,QAAQ,eAAe;AAErD,IAAI,YAAqB;AAEzB,SAAS,kBAAkB;AAChC,SAAO,UAAAC,QAAG,WAAW,uBAAuB;AAC9C;AAEO,SAAS,kBAAkB;AAChC,SAAO,KAAK,MAAM,UAAAA,QAAG,aAAa,yBAAyB,MAAM,CAAC,KAAK,CAAC;AAC1E;AAEO,SAAS,kBAAkB,OAAY;AAC5C,YAAAA,QAAG,cAAc,yBAAyB,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,MAAM;AAClF;AAEO,SAAS,WAAW,MAAe;AACxC,cAAY;AACd;AAEA,eAAsB,kBAAkB;AACtC,QAAM,kBAAkB,OAAO,QAAQ,MAAM,2BAAS,QAAQ,sBAAsB,CAAC,CAAC;AACtF,iCAAa,wBAAwB,gBAAgB,MAAM;AAE3D,aAAW,CAAC,QAAQ,KAAK,KAAK,iBAAiB;AAC7C,QAAI;AACJ,QAAI;AACJ,QAAI;AAEJ,QAAI;AACF,oBAAc,KAAK,MAAM,KAAK;AAC9B,qCAAa,0BAA0B,YAAY,UAAU,MAAM;AAEnE,6BAAuB,UAAM,oCAAiB,YAAY,UAAU,YAAY,eAAe,MAAM;AACrG,0BAAgB,oCAAiB,qBAAqB,MAAM;AAAA,IAE9D,SAAS,GAAQ;AAGf,2CAAmB,iCAA4B,MAAM;AAAA,EAAO,EAAE,KAAK,EAAE;AACrE,YAAM,2BAAS,KAAK,sBAAsB,GAAG,MAAM;AACnD;AAAA,IACF;AAGA,QAAI,YAAY,eAAe,OAAO,GAAG;AACvC,UAAI;AACF,cAAM,WAAW,KAAK,MAAM,YAAY,KAAK;AAC7C,6BAAO,MAAM,mBAAY,MAAM,cAAc,QAAQ;AAErD,QAAC,cAAc,MAAiB,QAAQ,QAAQ;AAOhD,YAAI,YAAY,cAAc,QAAW;AACvC,gBAAM,cAAc,cAAc,MAAM,sBAAQ,GAAG;AACnD,cAAI,eAAe,YAAY,YAAY,YAAY,cAAc,GAAG;AACtE,wBAAY,cAAc,IAAI,YAAY;AAAA,UAC5C;AAAA,QACF;AAAA,MACF,SAAS,GAAQ;AACf,6CAAmB,iCAA4B,MAAM;AAAA,EAAa,EAAE,KAAK,EAAE;AAAA,MAC7E;AAAA,IACF;AAMA,QAAI,YAAY,SAAS;AACvB,iBAAW,cAAc,YAAY,SAAS;AAC5C,cAAM,EAAE,WAAW,kBAAkB,IAAI;AACzC,YAAI,CAAC,mBAAmB;AAAE;AAAA,QAAU;AAEpC,kBAAM,kCAAe,qBAAqB,QAAQ,gBAAgB,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,cAAc,wBAAwB,OAAO,iBAAiB,CAAC;AAAA,MACvJ;AAAA,IACF;AAGA,kBAAc,gBAAgB,YAAY,OAAO,CAAC;AAElD,yBAAO,MAAM,mBAAY,MAAM,4BAA4B,YAAY,SAAS,UAAU,CAAC,oBAAoB,YAAY,SAAS,IAAI,CAAC,MAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,EAC/K;AAEA,MAAI,gBAAgB,SAAS,GAAG;AAC9B,yBAAO,MAAM,UAAK,gBAAgB,QAAQ,6BAA6B;AAAA,EACzE;AACF;AAEA,eAAsB,iBAAiB,OAAmC;AACxE,aAAW,QAAQ,OAAO,OAAO,KAAK,GAAG;AACvC,UAAM,oBAAoB,MAAM,2BAAS,KAAK,sBAAsB,GAAG,KAAK,MAAM;AAClF,QAAI,mBAAmB;AACrB,UAAI;AACF,cAAM,cAAc,KAAK,MAAM,iBAAiB;AAGhD,oBAAY,OAAO,IAAI,KAAK,cAAc;AAG1C,uCAAa,wBAAwB,KAAK,UAAU,KAAK,MAAM;AAE/D,YAAI,KAAK,OAAO;AACd,sBAAY,OAAO,IAAI,KAAK,UAAU,KAAK,KAAK;AAMhD,gBAAM,cAAc,KAAK,MAAM,sBAAQ,GAAG;AAC1C,cAAI,aAAa;AACf,wBAAY,WAAW,IAAI,YAAY,cAAc;AAAA,UACvD;AAAA,QACF;AAIA,cAAM,gBAAgB,KAAK,QAAQ,IAAI,CAAC,YAAY;AAAA,UAClD,WAAW,OAAO;AAAA,UAClB,mBAAmB,OAAO;AAAA,QAC5B,EAAE;AAGF,cAAM,mBAAmB,IAAI,IAAI,cAAc,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC;AAItE,cAAM,gBAAgB,OAAO,KAAK,KAAK,gBAAgB,CAAC,EACrD,OAAO,CAAC,cAAc,CAAC,iBAAiB,IAAI,SAAS,CAAC,EACtD,IAAI,CAAC,eAAe;AAAA,UACnB;AAAA,UACA,mBAAmB;AAAA,QACrB,EAAE;AAEJ,oBAAY,SAAS,IAAI,cAAc,OAAO,aAAa;AAE3D,cAAM,2BAAS,KAAK,sBAAsB,GAAG,KAAK,QAAQ,KAAK,UAAU,WAAW,CAAC;AAGrF,6BAAO,MAAM,2BAAoB,KAAK,MAAM,eAAe,KAAK,QAAQ,MAAM,gBAAgB,YAAY,OAAO,MAAM,SAAY,QAAQ,IAAI,GAAG;AAAA,MAEpJ,SAAS,GAAQ;AACf,6CAAmB,+BAA0B,KAAK,MAAM;AAAA,EAAe,EAAE,KAAK,EAAE;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,wBAAwB;AACtC,SAAO;AACT;",
4
+ "sourcesContent": ["import fs from 'fs';\nimport path from 'path';\nimport { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';\nimport { logger } from '../Logger.ts';\nimport { debugAndPrintError, debugDevMode } from '../Debug.ts';\nimport { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';\nimport type { Room } from '../Room.ts';\n\nconst DEVMODE_CACHE_FILE_PATH = path.resolve(\".devmode.json\");\n\nexport let isDevMode: boolean = false;\n\nexport function hasDevModeCache() {\n return fs.existsSync(DEVMODE_CACHE_FILE_PATH);\n}\n\n// presence dictionaries are keyed by caller-controlled strings: rebuild them null-proto\nconst nullProto = (_: string, value: any) =>\n (value && typeof value === 'object' && !Array.isArray(value))\n ? Object.assign(Object.create(null), value)\n : value;\n\nexport function getDevModeCache() {\n return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8'), nullProto) || {};\n}\n\nexport function writeDevModeCache(cache: any) {\n fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');\n}\n\nexport function setDevMode(bool: boolean) {\n isDevMode = bool;\n}\n\nexport async function reloadFromCache() {\n const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));\n debugDevMode(\"rooms to restore: %i\", roomHistoryList.length);\n\n for (const [roomId, value] of roomHistoryList) {\n let roomHistory: any;\n let recreatedRoom: Room;\n let recreatedRoomListing: any;\n\n try {\n roomHistory = JSON.parse(value);\n debugDevMode(\"restoring room %s (%s)\", roomHistory.roomName, roomId);\n\n recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);\n recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);\n\n } catch (e: any) {\n // unrestorable entry (e.g. room type renamed/removed) \u2014 drop it\n // instead of wedging the boot\n debugAndPrintError(`\u274C couldn't restore room '${roomId}':\\n${e.stack}`);\n await presence.hdel(getRoomRestoreListKey(), roomId);\n continue;\n }\n\n // Restore previous state\n if (roomHistory.hasOwnProperty(\"state\")) {\n try {\n const rawState = JSON.parse(roomHistory.state);\n logger.debug(`\uD83D\uDCCB room '${roomId}' state =>`, rawState);\n\n (recreatedRoom.state as Schema).restore(rawState);\n\n // Restore the encoder's nextUniqueId so refIds increase\n // monotonically across HMR cycles. Without this, restore()\n // always produces the same refIds (0,1,2,3...) and onJoin()\n // always assigns the same next refIds (4,5...), causing the\n // client decoder to reuse stale instances on the 2nd+ cycle.\n if (roomHistory.nextRefId !== undefined) {\n const encoderRoot = recreatedRoom.state[$changes]?.root;\n if (encoderRoot && roomHistory.nextRefId > encoderRoot['nextUniqueId']) {\n encoderRoot['nextUniqueId'] = roomHistory.nextRefId;\n }\n }\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't restore room '${roomId}' state:\\n${e.stack}`);\n }\n }\n\n // Reserve seats for clients from cached history.\n // Skip entries without a reconnectionToken \u2014 these are stale\n // seats from allowReconnection() where the client already left\n // (e.g. page refresh). Restoring them would block room disposal.\n if (roomHistory.clients) {\n for (const clientData of roomHistory.clients) {\n const { sessionId, reconnectionToken } = clientData;\n if (!reconnectionToken) { continue; }\n // TODO: need to restore each client's StateView as well\n await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, recreatedRoom.seatReservationTimeout, false, reconnectionToken]);\n }\n }\n\n // call `onRestoreRoom` with custom 'cache'd property.\n recreatedRoom.onRestoreRoom?.(roomHistory[\"cache\"]);\n\n logger.debug(`\uD83D\uDD04 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(\", \")}`);\n }\n\n if (roomHistoryList.length > 0) {\n logger.debug(\"\u2705\", roomHistoryList.length, \"room(s) have been restored.\");\n }\n}\n\nexport async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {\n for (const room of Object.values(rooms)) {\n const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);\n if (roomHistoryResult) {\n try {\n const roomHistory = JSON.parse(roomHistoryResult);\n\n // custom cache method\n roomHistory[\"cache\"] = room.onCacheRoom?.();\n\n // encode state\n debugDevMode(\"caching room %s (%s)\", room.roomName, room.roomId);\n\n if (room.state) {\n roomHistory[\"state\"] = JSON.stringify(room.state);\n\n // Cache the encoder's nextUniqueId so it can be restored.\n // This ensures refIds increase monotonically across HMR cycles,\n // preventing the client decoder from reusing stale refs that\n // happen to have the same refId as newly created instances.\n const encoderRoot = room.state[$changes]?.root;\n if (encoderRoot) {\n roomHistory[\"nextRefId\"] = encoderRoot['nextUniqueId'];\n }\n }\n\n // cache active clients with their reconnection tokens\n // TODO: need to cache each client's StateView as well\n const activeClients = room.clients.map((client) => ({\n sessionId: client.sessionId,\n reconnectionToken: client.reconnectionToken,\n }));\n\n // collect active client sessionIds to avoid duplicates\n const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));\n\n // also cache reserved seats (they don't have reconnectionTokens yet)\n // filter out reserved seats that are already active clients (from devMode reconnection)\n const reservedSeats = Object.keys(room['_reservedSeats'])\n .filter((sessionId) => !activeSessionIds.has(sessionId))\n .map((sessionId) => ({\n sessionId,\n reconnectionToken: undefined,\n }));\n\n roomHistory[\"clients\"] = activeClients.concat(reservedSeats);\n\n await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));\n\n // Rewrite updated room history\n logger.debug(`\uD83D\uDCBE caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory[\"state\"] !== undefined ? \"yes\" : \"no\"})`);\n\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't cache room '${room.roomId}', due to:\\n${e.stack}`);\n }\n }\n }\n}\n\nexport function getRoomRestoreListKey() {\n return 'roomhistory';\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["path", "fs"]
7
7
  }
@@ -10,8 +10,9 @@ var isDevMode = false;
10
10
  function hasDevModeCache() {
11
11
  return fs.existsSync(DEVMODE_CACHE_FILE_PATH);
12
12
  }
13
+ var nullProto = (_, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.assign(/* @__PURE__ */ Object.create(null), value) : value;
13
14
  function getDevModeCache() {
14
- return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, "utf8")) || {};
15
+ return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, "utf8"), nullProto) || {};
15
16
  }
16
17
  function writeDevModeCache(cache) {
17
18
  fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), "utf8");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/DevMode.ts"],
4
- "sourcesContent": ["import fs from 'fs';\nimport path from 'path';\nimport { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';\nimport { logger } from '../Logger.ts';\nimport { debugAndPrintError, debugDevMode } from '../Debug.ts';\nimport { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';\nimport type { Room } from '../Room.ts';\n\nconst DEVMODE_CACHE_FILE_PATH = path.resolve(\".devmode.json\");\n\nexport let isDevMode: boolean = false;\n\nexport function hasDevModeCache() {\n return fs.existsSync(DEVMODE_CACHE_FILE_PATH);\n}\n\nexport function getDevModeCache() {\n return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8')) || {};\n}\n\nexport function writeDevModeCache(cache: any) {\n fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');\n}\n\nexport function setDevMode(bool: boolean) {\n isDevMode = bool;\n}\n\nexport async function reloadFromCache() {\n const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));\n debugDevMode(\"rooms to restore: %i\", roomHistoryList.length);\n\n for (const [roomId, value] of roomHistoryList) {\n let roomHistory: any;\n let recreatedRoom: Room;\n let recreatedRoomListing: any;\n\n try {\n roomHistory = JSON.parse(value);\n debugDevMode(\"restoring room %s (%s)\", roomHistory.roomName, roomId);\n\n recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);\n recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);\n\n } catch (e: any) {\n // unrestorable entry (e.g. room type renamed/removed) \u2014 drop it\n // instead of wedging the boot\n debugAndPrintError(`\u274C couldn't restore room '${roomId}':\\n${e.stack}`);\n await presence.hdel(getRoomRestoreListKey(), roomId);\n continue;\n }\n\n // Restore previous state\n if (roomHistory.hasOwnProperty(\"state\")) {\n try {\n const rawState = JSON.parse(roomHistory.state);\n logger.debug(`\uD83D\uDCCB room '${roomId}' state =>`, rawState);\n\n (recreatedRoom.state as Schema).restore(rawState);\n\n // Restore the encoder's nextUniqueId so refIds increase\n // monotonically across HMR cycles. Without this, restore()\n // always produces the same refIds (0,1,2,3...) and onJoin()\n // always assigns the same next refIds (4,5...), causing the\n // client decoder to reuse stale instances on the 2nd+ cycle.\n if (roomHistory.nextRefId !== undefined) {\n const encoderRoot = recreatedRoom.state[$changes]?.root;\n if (encoderRoot && roomHistory.nextRefId > encoderRoot['nextUniqueId']) {\n encoderRoot['nextUniqueId'] = roomHistory.nextRefId;\n }\n }\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't restore room '${roomId}' state:\\n${e.stack}`);\n }\n }\n\n // Reserve seats for clients from cached history.\n // Skip entries without a reconnectionToken \u2014 these are stale\n // seats from allowReconnection() where the client already left\n // (e.g. page refresh). Restoring them would block room disposal.\n if (roomHistory.clients) {\n for (const clientData of roomHistory.clients) {\n const { sessionId, reconnectionToken } = clientData;\n if (!reconnectionToken) { continue; }\n // TODO: need to restore each client's StateView as well\n await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, recreatedRoom.seatReservationTimeout, false, reconnectionToken]);\n }\n }\n\n // call `onRestoreRoom` with custom 'cache'd property.\n recreatedRoom.onRestoreRoom?.(roomHistory[\"cache\"]);\n\n logger.debug(`\uD83D\uDD04 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(\", \")}`);\n }\n\n if (roomHistoryList.length > 0) {\n logger.debug(\"\u2705\", roomHistoryList.length, \"room(s) have been restored.\");\n }\n}\n\nexport async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {\n for (const room of Object.values(rooms)) {\n const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);\n if (roomHistoryResult) {\n try {\n const roomHistory = JSON.parse(roomHistoryResult);\n\n // custom cache method\n roomHistory[\"cache\"] = room.onCacheRoom?.();\n\n // encode state\n debugDevMode(\"caching room %s (%s)\", room.roomName, room.roomId);\n\n if (room.state) {\n roomHistory[\"state\"] = JSON.stringify(room.state);\n\n // Cache the encoder's nextUniqueId so it can be restored.\n // This ensures refIds increase monotonically across HMR cycles,\n // preventing the client decoder from reusing stale refs that\n // happen to have the same refId as newly created instances.\n const encoderRoot = room.state[$changes]?.root;\n if (encoderRoot) {\n roomHistory[\"nextRefId\"] = encoderRoot['nextUniqueId'];\n }\n }\n\n // cache active clients with their reconnection tokens\n // TODO: need to cache each client's StateView as well\n const activeClients = room.clients.map((client) => ({\n sessionId: client.sessionId,\n reconnectionToken: client.reconnectionToken,\n }));\n\n // collect active client sessionIds to avoid duplicates\n const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));\n\n // also cache reserved seats (they don't have reconnectionTokens yet)\n // filter out reserved seats that are already active clients (from devMode reconnection)\n const reservedSeats = Object.keys(room['_reservedSeats'])\n .filter((sessionId) => !activeSessionIds.has(sessionId))\n .map((sessionId) => ({\n sessionId,\n reconnectionToken: undefined,\n }));\n\n roomHistory[\"clients\"] = activeClients.concat(reservedSeats);\n\n await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));\n\n // Rewrite updated room history\n logger.debug(`\uD83D\uDCBE caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory[\"state\"] !== undefined ? \"yes\" : \"no\"})`);\n\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't cache room '${room.roomId}', due to:\\n${e.stack}`);\n }\n }\n }\n}\n\nexport function getRoomRestoreListKey() {\n return 'roomhistory';\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from 'fs';\nimport path from 'path';\nimport { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';\nimport { logger } from '../Logger.ts';\nimport { debugAndPrintError, debugDevMode } from '../Debug.ts';\nimport { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';\nimport type { Room } from '../Room.ts';\n\nconst DEVMODE_CACHE_FILE_PATH = path.resolve(\".devmode.json\");\n\nexport let isDevMode: boolean = false;\n\nexport function hasDevModeCache() {\n return fs.existsSync(DEVMODE_CACHE_FILE_PATH);\n}\n\n// presence dictionaries are keyed by caller-controlled strings: rebuild them null-proto\nconst nullProto = (_: string, value: any) =>\n (value && typeof value === 'object' && !Array.isArray(value))\n ? Object.assign(Object.create(null), value)\n : value;\n\nexport function getDevModeCache() {\n return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8'), nullProto) || {};\n}\n\nexport function writeDevModeCache(cache: any) {\n fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');\n}\n\nexport function setDevMode(bool: boolean) {\n isDevMode = bool;\n}\n\nexport async function reloadFromCache() {\n const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));\n debugDevMode(\"rooms to restore: %i\", roomHistoryList.length);\n\n for (const [roomId, value] of roomHistoryList) {\n let roomHistory: any;\n let recreatedRoom: Room;\n let recreatedRoomListing: any;\n\n try {\n roomHistory = JSON.parse(value);\n debugDevMode(\"restoring room %s (%s)\", roomHistory.roomName, roomId);\n\n recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);\n recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);\n\n } catch (e: any) {\n // unrestorable entry (e.g. room type renamed/removed) \u2014 drop it\n // instead of wedging the boot\n debugAndPrintError(`\u274C couldn't restore room '${roomId}':\\n${e.stack}`);\n await presence.hdel(getRoomRestoreListKey(), roomId);\n continue;\n }\n\n // Restore previous state\n if (roomHistory.hasOwnProperty(\"state\")) {\n try {\n const rawState = JSON.parse(roomHistory.state);\n logger.debug(`\uD83D\uDCCB room '${roomId}' state =>`, rawState);\n\n (recreatedRoom.state as Schema).restore(rawState);\n\n // Restore the encoder's nextUniqueId so refIds increase\n // monotonically across HMR cycles. Without this, restore()\n // always produces the same refIds (0,1,2,3...) and onJoin()\n // always assigns the same next refIds (4,5...), causing the\n // client decoder to reuse stale instances on the 2nd+ cycle.\n if (roomHistory.nextRefId !== undefined) {\n const encoderRoot = recreatedRoom.state[$changes]?.root;\n if (encoderRoot && roomHistory.nextRefId > encoderRoot['nextUniqueId']) {\n encoderRoot['nextUniqueId'] = roomHistory.nextRefId;\n }\n }\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't restore room '${roomId}' state:\\n${e.stack}`);\n }\n }\n\n // Reserve seats for clients from cached history.\n // Skip entries without a reconnectionToken \u2014 these are stale\n // seats from allowReconnection() where the client already left\n // (e.g. page refresh). Restoring them would block room disposal.\n if (roomHistory.clients) {\n for (const clientData of roomHistory.clients) {\n const { sessionId, reconnectionToken } = clientData;\n if (!reconnectionToken) { continue; }\n // TODO: need to restore each client's StateView as well\n await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, recreatedRoom.seatReservationTimeout, false, reconnectionToken]);\n }\n }\n\n // call `onRestoreRoom` with custom 'cache'd property.\n recreatedRoom.onRestoreRoom?.(roomHistory[\"cache\"]);\n\n logger.debug(`\uD83D\uDD04 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(\", \")}`);\n }\n\n if (roomHistoryList.length > 0) {\n logger.debug(\"\u2705\", roomHistoryList.length, \"room(s) have been restored.\");\n }\n}\n\nexport async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {\n for (const room of Object.values(rooms)) {\n const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);\n if (roomHistoryResult) {\n try {\n const roomHistory = JSON.parse(roomHistoryResult);\n\n // custom cache method\n roomHistory[\"cache\"] = room.onCacheRoom?.();\n\n // encode state\n debugDevMode(\"caching room %s (%s)\", room.roomName, room.roomId);\n\n if (room.state) {\n roomHistory[\"state\"] = JSON.stringify(room.state);\n\n // Cache the encoder's nextUniqueId so it can be restored.\n // This ensures refIds increase monotonically across HMR cycles,\n // preventing the client decoder from reusing stale refs that\n // happen to have the same refId as newly created instances.\n const encoderRoot = room.state[$changes]?.root;\n if (encoderRoot) {\n roomHistory[\"nextRefId\"] = encoderRoot['nextUniqueId'];\n }\n }\n\n // cache active clients with their reconnection tokens\n // TODO: need to cache each client's StateView as well\n const activeClients = room.clients.map((client) => ({\n sessionId: client.sessionId,\n reconnectionToken: client.reconnectionToken,\n }));\n\n // collect active client sessionIds to avoid duplicates\n const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));\n\n // also cache reserved seats (they don't have reconnectionTokens yet)\n // filter out reserved seats that are already active clients (from devMode reconnection)\n const reservedSeats = Object.keys(room['_reservedSeats'])\n .filter((sessionId) => !activeSessionIds.has(sessionId))\n .map((sessionId) => ({\n sessionId,\n reconnectionToken: undefined,\n }));\n\n roomHistory[\"clients\"] = activeClients.concat(reservedSeats);\n\n await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));\n\n // Rewrite updated room history\n logger.debug(`\uD83D\uDCBE caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory[\"state\"] !== undefined ? \"yes\" : \"no\"})`);\n\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't cache room '${room.roomId}', due to:\\n${e.stack}`);\n }\n }\n }\n}\n\nexport function getRoomRestoreListKey() {\n return 'roomhistory';\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.7",
3
+ "version": "0.18.9",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -52,25 +52,25 @@
52
52
  "msgpackr": "^2.0.1",
53
53
  "nanoid": "^3.3.11",
54
54
  "@colyseus/better-call": "^1.3.1",
55
- "@colyseus/greeting-banner": "^4.0.1",
56
- "@colyseus/shared-types": "^0.18.1"
55
+ "@colyseus/greeting-banner": "^4.0.2",
56
+ "@colyseus/shared-types": "^0.18.2"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@colyseus/schema": "^5.0.8",
60
60
  "express": "^5.0.0",
61
61
  "vitest": "^3.1.1",
62
62
  "@colyseus/redis-driver": "^0.18.2",
63
- "@colyseus/redis-presence": "^0.18.2",
64
- "@colyseus/tools": "^0.18.2"
63
+ "@colyseus/redis-presence": "^0.18.3",
64
+ "@colyseus/tools": "^0.18.3"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@colyseus/schema": "^5.0.8",
68
68
  "@pm2/io": "^6.1.0",
69
69
  "express": "^4.16.0 || ^5.0.0",
70
70
  "zod": "^4.1.12",
71
- "@colyseus/auth": "^0.18.1",
72
- "@colyseus/better-call": "^1.3.1",
73
- "@colyseus/ws-transport": "^0.18.1"
71
+ "@colyseus/auth": "^0.18.2",
72
+ "@colyseus/ws-transport": "^0.18.2",
73
+ "@colyseus/better-call": "^1.3.1"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@colyseus/auth": {
package/src/Server.ts CHANGED
@@ -132,18 +132,34 @@ export class Server<
132
132
  static current: Server<any, any> | undefined;
133
133
 
134
134
  constructor(options: ServerOptions = {}) {
135
- const {
136
- gracefullyShutdown = true,
137
- greet = true
138
- } = options;
139
-
140
- setDevMode(options.devMode === true);
135
+ const { greet = true } = options;
141
136
 
142
137
  this.options = options;
143
138
  this.greet = greet;
144
139
 
145
140
  (Server as { current: Server<any, any> | undefined }).current = this as Server<any, any>;
146
141
 
142
+ if (options.logger) {
143
+ setLogger(options.logger);
144
+ }
145
+
146
+ this.bootRuntime();
147
+ }
148
+
149
+ /**
150
+ * Every side effect of construction: the dev-mode flag, the transport, the
151
+ * matchmaker and the process signal handlers. Distinct from
152
+ * {@link _bootServices}, which prepares user services once we're listening.
153
+ *
154
+ * Isolated so {@link DevServer} can opt out of all four — under
155
+ * `colyseus/vite` the plugin owns them, and user code still gets a real
156
+ * `Server` to call.
157
+ */
158
+ protected bootRuntime() {
159
+ const options = this.options;
160
+
161
+ setDevMode(options.devMode === true);
162
+
147
163
  this.attach(options);
148
164
 
149
165
  // Pass options.presence/driver through as-is (possibly undefined).
@@ -159,13 +175,9 @@ export class Server<
159
175
  this.driver = matchMaker.driver;
160
176
  });
161
177
 
162
- if (gracefullyShutdown) {
178
+ if (options.gracefullyShutdown !== false) {
163
179
  registerGracefulShutdown((err) => this.gracefullyShutdown(true, err));
164
180
  }
165
-
166
- if (options.logger) {
167
- setLogger(options.logger);
168
- }
169
181
  }
170
182
 
171
183
  public async attach(options: ServerOptions) {
@@ -498,6 +510,47 @@ export class Server<
498
510
  () => Promise.resolve()
499
511
  }
500
512
 
513
+ // the user module re-evaluates on every HMR reload — warn once per process
514
+ const devWarnings = new Set<string>();
515
+ function warnDevOnce(message: string) {
516
+ if (devWarnings.has(message)) { return; }
517
+ devWarnings.add(message);
518
+ logger.warn(message);
519
+ }
520
+
521
+ /**
522
+ * The `Server` handed to user code under `colyseus/vite`.
523
+ *
524
+ * The plugin owns the HTTP server, the transport, the matchmaker and the
525
+ * process lifecycle, so constructing one has no side effects — it only
526
+ * carries the user's configuration back to the plugin. Everything else is
527
+ * inherited, so the documented `Server` API keeps working in dev mode.
528
+ */
529
+ class DevServer<
530
+ RoomTypes extends Record<string, RegisteredHandler> = any,
531
+ Routes extends Router = any
532
+ > extends Server<RoomTypes, Routes> {
533
+
534
+ protected bootRuntime() {
535
+ // the plugin creates the transport and runs matchMaker.setup() before
536
+ // importing user code, so both are already available here
537
+ this.transport = getTransport();
538
+ this._onTransportReady.resolve(this.transport);
539
+ this.presence = matchMaker.presence;
540
+ this.driver = matchMaker.driver;
541
+
542
+ if (this.options.transport) {
543
+ warnDevOnce("⚠️ 'transport' is ignored in dev mode — colyseus/vite serves WebSockets over Vite's own HTTP server.");
544
+ }
545
+ }
546
+
547
+ /** The plugin already serves Colyseus on Vite's own HTTP server and port. */
548
+ public async listen(_port?: number | string, _hostname?: string, _backlog?: number, listeningListener?: Function) {
549
+ warnDevOnce("⚠️ listen() is ignored in dev mode — connect to the Vite dev server's URL instead.");
550
+ listeningListener?.();
551
+ }
552
+ }
553
+
501
554
  let _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
502
555
 
503
556
  /**
@@ -597,22 +650,18 @@ export function defineServer<
597
650
  ): Server<T, R> {
598
651
  const { rooms, routes, ...serverOptions } = options;
599
652
 
600
- if (isDevMode) {
601
- // In dev mode, the Vite plugin manages Server/matchMaker lifecycle.
602
- // Return a config-only object — no Server instance, no matchMaker.setup().
603
- return {
604
- options: serverOptions,
605
- router: routes,
606
- '~rooms': rooms,
607
- // the vite plugin registers the transport before importing user code
608
- simulateLatency: (milliseconds: number) => applySimulatedLatency(getTransport(), milliseconds),
609
- } as unknown as Server<T, R>;
610
- }
653
+ const server = isDevMode
654
+ ? new DevServer<T, R>(serverOptions)
655
+ : new Server<T, R>(serverOptions);
611
656
 
612
- const server = new Server<T, R>(serverOptions);
613
657
  server.router = routes;
658
+ server['~rooms'] = rooms;
614
659
 
615
- registerRoomDefinitions(rooms);
660
+ // in dev mode the plugin reads `~rooms` back and owns their
661
+ // (re-)registration across HMR reloads
662
+ if (!isDevMode) {
663
+ registerRoomDefinitions(rooms);
664
+ }
616
665
 
617
666
  return server;
618
667
  }
@@ -10,12 +10,13 @@ type Callback = (...args: any[]) => void;
10
10
  export class LocalPresence implements Presence {
11
11
  public subscriptions: EventEmitter = new EventEmitter();
12
12
 
13
- public data: {[roomName: string]: string[]} = {};
14
- public hash: {[roomName: string]: {[key: string]: string}} = {};
13
+ // null-proto: keys are caller-controlled, must never resolve through Object.prototype
14
+ public data: {[roomName: string]: string[]} = Object.create(null);
15
+ public hash: {[roomName: string]: {[key: string]: string}} = Object.create(null);
15
16
 
16
- public keys: {[name: string]: string | number} = {};
17
+ public keys: {[name: string]: string | number} = Object.create(null);
17
18
 
18
- private timeouts: {[name: string]: NodeJS.Timeout} = {};
19
+ private timeouts: {[name: string]: NodeJS.Timeout} = Object.create(null);
19
20
 
20
21
  constructor() {
21
22
  //
@@ -140,7 +141,7 @@ export class LocalPresence implements Presence {
140
141
  }
141
142
 
142
143
  public async sinter(...keys: string[]) {
143
- const intersection: {[value: string]: number} = {};
144
+ const intersection: {[value: string]: number} = Object.create(null);
144
145
 
145
146
  for (let i = 0, l = keys.length; i < l; i++) {
146
147
  (await this.smembers(keys[i])).forEach((member) => {
@@ -161,13 +162,13 @@ export class LocalPresence implements Presence {
161
162
  }
162
163
 
163
164
  public hset(key: string, field: string, value: string) {
164
- if (!this.hash[key]) { this.hash[key] = {}; }
165
+ if (!this.hash[key]) { this.hash[key] = Object.create(null); }
165
166
  this.hash[key][field] = value;
166
167
  return Promise.resolve(true);
167
168
  }
168
169
 
169
170
  public hincrby(key: string, field: string, incrBy: number) {
170
- if (!this.hash[key]) { this.hash[key] = {}; }
171
+ if (!this.hash[key]) { this.hash[key] = Object.create(null); }
171
172
  let value = Number(this.hash[key][field] || '0');
172
173
  value += incrBy;
173
174
  this.hash[key][field] = value.toString();
@@ -175,7 +176,7 @@ export class LocalPresence implements Presence {
175
176
  }
176
177
 
177
178
  public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {
178
- if (!this.hash[key]) { this.hash[key] = {}; }
179
+ if (!this.hash[key]) { this.hash[key] = Object.create(null); }
179
180
  let value = Number(this.hash[key][field] || '0');
180
181
  value += incrBy;
181
182
  this.hash[key][field] = value.toString();
@@ -202,7 +203,7 @@ export class LocalPresence implements Presence {
202
203
  }
203
204
 
204
205
  public async hgetall(key: string) {
205
- return this.hash[key] || {};
206
+ return { ...this.hash[key] }; // fresh plain object, like redis
206
207
  }
207
208
 
208
209
  public hdel(key: string, field: any) {
@@ -14,8 +14,14 @@ export function hasDevModeCache() {
14
14
  return fs.existsSync(DEVMODE_CACHE_FILE_PATH);
15
15
  }
16
16
 
17
+ // presence dictionaries are keyed by caller-controlled strings: rebuild them null-proto
18
+ const nullProto = (_: string, value: any) =>
19
+ (value && typeof value === 'object' && !Array.isArray(value))
20
+ ? Object.assign(Object.create(null), value)
21
+ : value;
22
+
17
23
  export function getDevModeCache() {
18
- return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8')) || {};
24
+ return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8'), nullProto) || {};
19
25
  }
20
26
 
21
27
  export function writeDevModeCache(cache: any) {