Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/smogon/pokemon-showdown-client/llms.txt

Use this file to discover all available pages before exploring further.

Every message that flows between the browser and the Pokémon Showdown game server passes through PSConnection, defined in client-connection.ts. The class is intentionally thin: it owns the socket, manages the reconnect loop, and delegates all higher-level message handling to PS.receive(). HTTP requests — for login, team storage, and the REST API — go through two separate helpers: the Net fetch wrapper and PSLoginServer.

PSConnection

export class PSConnection {
  socket: WebSocket | null;
  connected: boolean;
  lastMessageTimeBeforeReconnect: number;
  queue: string[];
  reconnectDelay: number;        // starts at 1 000 ms, doubles on each failure
  reconnectTimer: ReturnType<typeof setTimeout> | null;
}

socket

The underlying WebSocket (or SockJS shim). null when a Web Worker is handling the connection, or when disconnected.

connected

true once the socket handshake completes. While false, outgoing messages are queued in queue[].

queue

Messages buffered before connection is established. Flushed in FIFO order immediately on connect.

reconnectDelay

Exponential back-off delay (ms). Starts at 1 000, doubles on each failed attempt, capped at 15 000.

Connection lifecycle

1

Initialise

PSConnection.connect() is called at module load. It creates a new PSConnection instance (or calls reconnect() on an existing one) and triggers PSStorage.init() to load cross-origin localStorage data before opening the socket.
2

Prefer a Web Worker

initConnection() calls tryConnectInWorker() first. If the browser supports Worker, a client-connection-worker.js worker is spun up and sent a { type: 'connect', server: PS.server } message. All socket I/O then runs off the main thread.
3

Fall back to direct connect

If the worker fails (e.g. CSP, no Worker support), directConnect() opens a SockJS or WebSocket connection on the main thread.
4

Receive messages

Incoming data is forwarded to PS.receive(data), which parses the room prefix and dispatches each protocol line to the appropriate PSRoom.
5

Reconnect on disconnect

handleDisconnect() marks all rooms as 'autoreconnect' and schedules a retry via retryConnection(). canReconnect() blocks reconnection if the tab has been open for more than 24 hours — the user is prompted to refresh instead.

tryConnectInWorker and directConnect

// Preferred path — non-blocking socket I/O
// Returns false if this.socket already exists (direct connection is active)
tryConnectInWorker(): boolean {
  if (this.socket) return false; // must be one or the other
  if (this.connected) return true;

  // Reuse an existing worker if present
  if (this.worker) {
    this.worker.postMessage({ type: 'connect', server: PS.server });
    return true;
  }

  const worker = new Worker('/js/client-connection-worker.js');
  worker.postMessage({ type: 'connect', server: PS.server });

  worker.onmessage = event => {
    const { type, data } = event.data;
    switch (type) {
      case 'connected':    // socket open, flush queue
      case 'message':      // PS.receive(data)
      case 'disconnected': // handleDisconnect()
      case 'error':        // fall back to directConnect()
    }
  };
  return true;
}

// Fallback path — SockJS or WebSocket on the main thread
directConnect(): void {
  const server = PS.server;
  const port = server.protocol === 'https' ? `:${server.port}` : `:${server.httpport!}`;
  const url = `${server.protocol}://${server.host}${port}${server.prefix}`;
  this.socket = new SockJS(url, [], { timeout: 5 * 60 * 1000 });
  // or: new WebSocket(url.replace('http', 'ws') + '/websocket')

  this.socket.onopen    = () => { /* flush queue, PS.update() */ };
  this.socket.onmessage = (ev) => PS.receive('' + ev.data);
  this.socket.onclose   = () => { /* handleDisconnect() */ };
}

Web Worker message protocol

typedata fieldPurpose
'connect'PS.server objectOpen the socket to this server
'send'message stringSend a message over the socket

canReconnect

canReconnect(): boolean {
  const uptime = Date.now() - PS.startTime;
  if (uptime > 24 * 60 * 60 * 1000) {
    PS.confirm(`It's been over a day since you first connected. Please refresh.`, {
      okButton: 'Refresh',
    }).then(confirmed => {
      if (confirmed) PS.room?.send(`/refresh`);
    });
    return false;
  }
  return this.shouldReconnect; // private — set to false by disconnect()
}
If the tab has been open for more than 24 hours, canReconnect() returns false and prompts the user to refresh. This prevents stale authentication tokens from causing silent failures.

Sending messages

PS.send(msg, roomid?) wraps PSConnection.send():
// In PS (client-main.ts)
send(msg: string, roomid?: RoomID) {
  this.connection.send(`${roomid || ''}|${msg}`);
}

// In PSConnection
send(msg: string) {
  if (!this.connected) {
    this.queue.push(msg);  // buffered until connected
    return;
  }
  if (this.worker) {
    this.worker.postMessage({ type: 'send', data: msg });
  } else if (this.socket) {
    this.socket.send(msg);
  }
}
Messages are prefixed with the room ID separated by |. A message to the global room omits the prefix:
PS.send('/search gen9randombattle');         // → "|/search gen9randombattle"
PS.send('/forfeit', 'battle-gen9-12345' as RoomID); // → "battle-gen9-12345|/forfeit"

ServerInfo: the server config structure

The ServerInfo interface (defined in client-main.ts) describes which server to connect to. It is populated either from Config.defaultserver or from the cross-origin PSStorage iframe handshake:
export interface ServerInfo {
  id: ID;           // e.g. 'showdown' | 'smogtours'
  protocol: string; // 'https' or 'http'
  host: string;     // e.g. 'sim3.psim.us'
  port: number;     // typically 443
  httpport?: number;
  altport?: number;
  prefix: string;   // URL path prefix, e.g. ''
  afd?: boolean;    // April Fools mode
  registered?: boolean;
}

PSLoginServer

PSLoginServer is an HTTP client for the PS authentication endpoint (/~~<serverid>/action.php). It handles the login flow (challenge-response with challstr) and other account actions.
export const PSLoginServer = new class {
  // Low-level: returns raw response string
  rawQuery(act: string, data: PostData): Promise<string | null>;

  // High-level: JSON-parses the response
  query(act: string, data?: PostData): Promise<{ [k: string]: any } | null>;
};

Common actions

const result = await PSLoginServer.query('getassertion', {
  userid: 'ash',
  challstr: PS.user.challstr,
});

Net: the fetch wrapper

Net is a lightweight XMLHttpRequest wrapper used throughout the client when a full fetch API is unavailable or undesirable.
export function Net(uri: string): NetRequest;

class NetRequest {
  uri: string;

  // GET (or POST with method override)
  get(opts?: NetRequestOptions): Promise<string>;

  // POST
  post(opts?: NetRequestOptions, body?: PostData | string): Promise<string>;
}

Usage examples

const html = await Net('https://pokemonshowdown.com/news.json').get();
Net.defaultRoute can be set to a base URL so that paths starting with / are automatically prefixed. When running under file: protocol (the test client), Net automatically upgrades // URLs to https:.

Helper utilities on Net

MethodSignaturePurpose
Net.encodeQuery(data: string | PostData) => stringURL-encodes a key-value object into application/x-www-form-urlencoded format
Net.formData(form: HTMLFormElement) => Record<string, string | boolean>Reads all named inputs from a form element

Build docs developers (and LLMs) love