{
  "version": 3,
  "sources": ["../src/WebSocketTransport.ts"],
  "sourcesContent": ["import http from 'http';\nimport { URL } from 'url';\nimport WebSocket, { type ServerOptions, WebSocketServer } from 'ws';\nimport express from 'express';\n\nimport { matchMaker, Protocol, Transport, debugAndPrintError, debugConnection, getBearerToken, CloseCode, connectClientToRoom, isDevMode } from '@colyseus/core';\nimport { WebSocketClient } from './WebSocketClient.ts';\n\nfunction noop() {}\nfunction heartbeat(this: any) { this.pingCount = 0; }\n\ntype RawWebSocketClient = WebSocket & { pingCount: number };\n\nexport interface TransportOptions extends ServerOptions {\n  pingInterval?: number;\n  pingMaxRetries?: number;\n}\n\n/**\n * Options for binding this transport to an existing HTTP server.\n *\n * This is primarily used by `colyseus/vite`, which shares Vite's dev HTTP server\n * and forwards only Colyseus websocket upgrade requests to this transport.\n */\nexport interface AttachToServerOptions {\n  /**\n   * Return `true` to let this transport handle the upgrade request.\n   * Requests that return `false` are left for the host HTTP server.\n   */\n  filter?: (req: http.IncomingMessage) => boolean;\n}\n\nexport class WebSocketTransport extends Transport {\n  protected wss: WebSocketServer;\n\n  protected pingInterval: NodeJS.Timeout;\n  protected pingIntervalMS: number;\n  protected pingMaxRetries: number;\n\n  // False when sharing an external HTTP server, such as Vite's dev server.\n  protected shouldShutdownServer: boolean = true;\n\n  private _originalSend: typeof WebSocketClient.prototype.raw | null = null;\n  private _expressApp?: express.Application;\n\n  constructor(options: TransportOptions = {}) {\n    super();\n\n    if (options.maxPayload === undefined) {\n      options.maxPayload = 4 * 1024; // 4Kb\n    }\n\n    // disable per-message deflate by default\n    if (options.perMessageDeflate === undefined) {\n      options.perMessageDeflate = false;\n    }\n\n    this.pingIntervalMS = (options.pingInterval !== undefined)\n      ? options.pingInterval\n      : 3000;\n\n    this.pingMaxRetries = (options.pingMaxRetries !== undefined)\n      ? options.pingMaxRetries\n      : 2;\n\n    // `noServer: true` lets callers attach later via `attachToServer()`.\n    // `colyseus/vite` uses this to share the Vite dev server instead of creating a new one.\n    if (!options.server && !options.noServer) {\n      options.server = http.createServer();\n    }\n\n    this.wss = new WebSocketServer(options);\n    this.wss.on('connection', this.onConnection);\n\n    // this is required to allow the ECONNRESET error to trigger on the `server` instance.\n    this.wss.on('error', (err) => debugAndPrintError(err));\n\n    this.server = options.server;\n\n    if (this.server && this.pingIntervalMS > 0 && this.pingMaxRetries > 0) {\n      this.server.on('listening', () =>\n        this.autoTerminateUnresponsiveClients(this.pingIntervalMS, this.pingMaxRetries));\n\n      this.server.on('close', () =>\n        clearInterval(this.pingInterval));\n    }\n  }\n\n  public getExpressApp(): express.Application {\n    if (!this.server) {\n      throw new Error('WebSocketTransport is not attached to an HTTP server.');\n    }\n\n    if (!this._expressApp) {\n      this._expressApp = express();\n      this.server.on('request', this._expressApp);\n    }\n    return this._expressApp;\n  }\n\n  public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n    if (!this.server) {\n      throw new Error('WebSocketTransport is not attached to an HTTP server.');\n    }\n\n    this.server.listen(port, hostname, backlog, listeningListener);\n    return this;\n  }\n\n  /**\n   * Attach this transport to an already-running HTTP server.\n   *\n   * `colyseus/vite` uses this in dev mode so Colyseus can reuse Vite's HTTP server\n   * instead of creating and owning a separate one.\n   */\n  public attachToServer(server: http.Server, options: AttachToServerOptions = {}) {\n    this.server = server;\n    this.shouldShutdownServer = false;\n\n    server.on('upgrade', (req, socket, head) => {\n      if (options.filter && !options.filter(req)) {\n        return;\n      }\n\n      this.wss.handleUpgrade(req, socket as any, head, (ws) => {\n        this.wss.emit('connection', ws, req);\n      });\n    });\n\n    if (this.pingIntervalMS > 0 && this.pingMaxRetries > 0 && !this.pingInterval) {\n      // An externally-managed server may already be listening, so start heartbeat here\n      // instead of waiting for a future \"listening\" event.\n      this.autoTerminateUnresponsiveClients(this.pingIntervalMS, this.pingMaxRetries);\n      server.on('close', () => clearInterval(this.pingInterval));\n    }\n\n    return this;\n  }\n\n  /**\n   * Close the websocket server and all active websocket connections.\n   *\n   * When attached through `attachToServer()`, keep the shared HTTP server alive.\n   * This is required for `colyseus/vite`, which does not own the Vite dev server.\n   */\n  public shutdown() {\n    this.wss.close();\n\n    if (this.shouldShutdownServer) {\n      this.server?.close();\n    }\n  }\n\n  public simulateLatency(milliseconds: number) {\n    if (this._originalSend == null) {\n      this._originalSend = WebSocketClient.prototype.raw;\n    }\n\n    const originalSend = this._originalSend;\n\n    WebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalSend : function (...args: any[]) {\n      // copy buffer\n      let [buf, ...rest] = args;\n      buf = Array.from(buf);\n      // @ts-ignore\n      setTimeout(() => originalSend.apply(this, [buf, ...rest]), milliseconds);\n    };\n  }\n\n  protected autoTerminateUnresponsiveClients(pingInterval: number, pingMaxRetries: number) {\n    // interval to detect broken connections\n    this.pingInterval = setInterval(() => {\n      this.wss.clients.forEach((client: WebSocket) => {\n        //\n        // if client hasn't responded after the interval, terminate its connection.\n        //\n        if ((client as RawWebSocketClient).pingCount >= pingMaxRetries) {\n          // debugConnection(`terminating unresponsive client ${client.sessionId}`);\n          debugConnection(`terminating unresponsive client`);\n          return client.terminate();\n        }\n\n        (client as RawWebSocketClient).pingCount++;\n        client.ping(noop);\n      });\n    }, pingInterval);\n  }\n\n  protected async onConnection(rawClient: RawWebSocketClient, req: http.IncomingMessage) {\n    // prevent server crashes if a single client had unexpected error\n    rawClient.on('error', (err) => debugAndPrintError(err.message + '\\n' + err.stack));\n    rawClient.on('pong', heartbeat);\n    rawClient.pingCount = 0;\n\n    const parsedURL = new URL(`ws://server/${req.url}`);\n\n    const sessionId = parsedURL.searchParams.get(\"sessionId\");\n    const processAndRoomId = parsedURL.pathname.match(/\\/[a-zA-Z0-9_\\-]+\\/([a-zA-Z0-9_\\-]+)$/);\n    const roomId = processAndRoomId && processAndRoomId[1];\n\n    // If sessionId is not provided, allow ping-pong utility.\n    if (!sessionId && !roomId) {\n      // Disconnect automatically after 1 second if no message is received.\n      const timeout = setTimeout(() => rawClient.close(CloseCode.NORMAL_CLOSURE), 1000);\n      rawClient.on('message', (_) => rawClient.send(new Uint8Array([Protocol.PING])));\n      rawClient.on('close', () => clearTimeout(timeout));\n      return;\n    }\n\n    const room = matchMaker.getLocalRoomById(roomId);\n\n    const client = new WebSocketClient(sessionId, rawClient);\n    const reconnectionToken = parsedURL.searchParams.get(\"reconnectionToken\");\n    const skipHandshake = (parsedURL.searchParams.has(\"skipHandshake\"));\n\n    try {\n      await connectClientToRoom(room, client, {\n        headers: new Headers(req.headers as Record<string, string>),\n        token: parsedURL.searchParams.get(\"_authToken\") ?? getBearerToken(req.headers.authorization),\n        ip: req.headers['x-real-ip'] ?? req.headers['x-forwarded-for'] ?? req.socket.remoteAddress,\n      }, {\n        reconnectionToken,\n        skipHandshake\n      });\n\n    } catch (e: any) {\n      debugAndPrintError(e);\n\n      // send error code to client then terminate.\n      // Use MAY_TRY_RECONNECT when a reconnection token is present so the\n      // SDK retries \u2014 the seat may not be reserved yet during devMode HMR.\n      client.error(e.code, e.message, () =>\n        rawClient.close(reconnectionToken\n          ? (isDevMode)\n            ? CloseCode.MAY_TRY_RECONNECT \n            : CloseCode.FAILED_TO_RECONNECT\n          : CloseCode.WITH_ERROR));\n    }\n  }\n\n}\n"],
  "mappings": "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",
  "names": ["http", "express"]
}
