Skip to main content

Overview

The outgoing WebSocket server on port 5200 broadcasts processed match data to frontend applications.
  • No authentication required: Simple logon with groupCode
  • Room-based broadcasting: Clients join rooms and receive data for their match
  • Filtered data: Internal fields and secrets removed before emission
  • Real-time updates: Instant match_data events as game state changes

Server Initialization

The server is created in src/connector/websocketOutgoing.ts:19-85:
let serverInstance;

if (process.env.INSECURE == "true") {
  serverInstance = createInsecureServer();
} else {
  if (!process.env.SERVER_KEY || !process.env.SERVER_CERT) {
    Log.error(
      `Missing TLS key or certificate! Please provide the paths to the key and certificate in the .env file. (SERVER_KEY and SERVER_CERT)`,
    );
  }

  const options = {
    key: readFileSync(process.env.SERVER_KEY!),
    cert: readFileSync(process.env.SERVER_CERT!),
  };

  serverInstance = createServer(options);
}

this.wss = new Server(serverInstance, {
  perMessageDeflate: {
    zlibDeflateOptions: {
      chunkSize: 1024,
      memLevel: 7,
      level: 3,
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024,
    },
    threshold: 1024,
  },
  cors: { origin: "*" },
});

serverInstance.listen(5200);
The outgoing server uses identical Socket.IO configuration as the incoming server, including perMessageDeflate compression and CORS wildcard.

Connection Flow

Frontend clients connect and join rooms based on their groupCode.

Initial Connection

From websocketOutgoing.ts:54-76:
this.wss.on(`connection`, (ws) => {
  ws.on("error", (e) => {
    Log.error(`Someone in ${ws.rooms} encountered a Websocket error: ${e}`);
  });

  ws.once("logon", (msg: string) => {
    try {
      const json = JSON.parse(msg);
      ws.join(json.groupCode);
      ws.emit(
        "logon_success",
        JSON.stringify({
          groupCode: json.groupCode,
          msg: `Logon succeeded for group code ${json.groupCode}`,
        }),
      );
      Log.info(`Received output logon using Group Code ${json.groupCode}`);
      MatchController.getInstance().sendMatchDataForLogon(json.groupCode);
    } catch (e) {
      Log.error(`Error parsing outgoing logon request: ${e}`);
    }
  });
});

Logon Event

Clients must emit a logon event with their groupCode to join the room:
socket.emit("logon", JSON.stringify({ groupCode: "MATCH123" }));
Logon payload:
{
  groupCode: string; // The match identifier
}

Logon Success Response

On successful logon, the server:
  1. Joins the client to the groupCode room
  2. Emits logon_success confirmation
  3. Sends current match data via MatchController.sendMatchDataForLogon()
logon_success payload:
{
  groupCode: string;
  msg: string; // "Logon succeeded for group code {groupCode}"
}
After logon_success, the client immediately receives the current match state via a match_data event.

Room-Based Broadcasting

Socket.IO rooms enable efficient broadcasting to all clients watching the same match.

Joining Rooms

ws.join(json.groupCode);
Each groupCode becomes a Socket.IO room. Clients in the same room receive identical match_data events.

Broadcasting to Rooms

From websocketOutgoing.ts:87-116:
sendMatchData(groupCode: string, data: any) {
  const {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    replayLog,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    eventNumber,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    groupSecret,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    playercamUrl,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    timeoutEndTimeout,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    timeoutRemainingLoop,

    ...formattedData
  } = data;

  const deepMod: any = structuredClone(formattedData);
  if (
    deepMod.tools &&
    deepMod.tools.playercamsInfo &&
    typeof deepMod.tools.playercamsInfo === "object"
  ) {
    delete deepMod.tools.playercamsInfo.secret;
    delete deepMod.tools.playercamsInfo.endTime;
  }

  this.wss.to(groupCode).emit("match_data", JSON.stringify(deepMod));
}

Data Filtering

Before emitting match_data, the server removes internal fields to prevent exposing sensitive information:

Removed Fields

  1. replayLog: Internal replay buffer
  2. eventNumber: Internal event counter
  3. groupSecret: Secret token for match validation
  4. playercamUrl: Internal player camera URL
  5. timeoutEndTimeout: Internal timeout handler reference
  6. timeoutRemainingLoop: Internal interval reference
  7. tools.playercamsInfo.secret: Player camera authentication secret
  8. tools.playercamsInfo.endTime: Internal timeout reference

Destructuring and Cloning

The server uses object destructuring to remove fields:
const {
  replayLog,
  eventNumber,
  groupSecret,
  playercamUrl,
  timeoutEndTimeout,
  timeoutRemainingLoop,
  ...formattedData // All remaining fields
} = data;
Then creates a deep clone to safely delete nested properties:
const deepMod: any = structuredClone(formattedData);
if (
  deepMod.tools &&
  deepMod.tools.playercamsInfo &&
  typeof deepMod.tools.playercamsInfo === "object"
) {
  delete deepMod.tools.playercamsInfo.secret;
  delete deepMod.tools.playercamsInfo.endTime;
}
structuredClone creates a deep copy of the object, preventing mutations to the original match data stored by the server.

match_data Event

The primary event emitted to frontend clients is match_data.

Emission

this.wss.to(groupCode).emit("match_data", JSON.stringify(deepMod));
  • Event name: match_data
  • Payload: JSON string of filtered match data
  • Target: All clients in the groupCode room

Event Frequency

The server emits match_data whenever:
  • A client joins (via sendMatchDataForLogon)
  • Observer/auxiliary clients send new data to port 5100
  • Match state changes (scoreboard, killfeed, round info, etc.)

Payload Structure

The match_data payload contains the complete match state:
{
  matchId: string;
  groupCode: string;
  scoreboard: IFormattedScoreboard[];
  killfeed: IFormattedKillfeed[];
  roster: IFormattedRoster[];
  roundInfo: IFormattedRoundInfo;
  score: IFormattedScore;
  map: string;
  gameMode: string;
  observing: string;
  teamIsAttacker: boolean;
  spikeDetonated: boolean;
  spikeDefused: boolean;
  tools: ToolsData;
  // ... and more fields
}
See the Data Events page for complete data schemas.

Singleton Pattern

The WebsocketOutgoing class uses a singleton pattern:
private static instance: WebsocketOutgoing;

public static getInstance(): WebsocketOutgoing {
  if (WebsocketOutgoing.instance == null) WebsocketOutgoing.instance = new WebsocketOutgoing();
  return WebsocketOutgoing.instance;
}
The MatchController calls WebsocketOutgoing.getInstance().sendMatchData() to broadcast updates.

Error Handling

Connection Errors

ws.on("error", (e) => {
  Log.error(`Someone in ${ws.rooms} encountered a Websocket error: ${e}`);
});

Engine Errors

this.wss.engine.on("connection_error", (err) => {
  Log.error("Socket.IO error: " + err);
});
Errors are logged but do not disconnect clients or crash the server.

Connection Example

import { io } from "socket.io-client";

const socket = io("wss://server:5200");

socket.on("connect", () => {
  console.log("Connected to port 5200");
  
  // Join room for match
  socket.emit("logon", JSON.stringify({ groupCode: "MATCH123" }));
});

socket.on("logon_success", (msg) => {
  const response = JSON.parse(msg);
  console.log(response.msg); // "Logon succeeded for group code MATCH123"
});

socket.on("match_data", (msg) => {
  const matchData = JSON.parse(msg);
  console.log("Received match data:", matchData);
  
  // Update UI with scoreboard, killfeed, etc.
  updateScoreboard(matchData.scoreboard);
  updateKillfeed(matchData.killfeed);
  updateRoundInfo(matchData.roundInfo);
});

socket.on("disconnect", () => {
  console.log("Disconnected from server");
});

Performance Considerations

Compression

The server compresses messages over 1KB using perMessageDeflate, reducing bandwidth usage for large match data payloads.

Room Broadcasting

Socket.IO rooms use efficient internal data structures to broadcast to multiple clients without serializing the payload multiple times.

Deep Cloning

The server uses structuredClone instead of JSON.parse(JSON.stringify()) for better performance when filtering data.

Next Steps

Data Events

Explore match_data payload structure and schemas

Incoming Connection

Learn how data arrives at port 5100

Build docs developers (and LLMs) love