All files / server/routes/simulation ws-message-router.ts

91.42% Statements 32/35
78.94% Branches 15/19
100% Functions 4/4
96.96% Lines 32/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96              151x 151x 151x                                                   50x     151x 151x 151x   151x 151x 2x 2x 2x     149x 149x 1x     1x     148x                         148x   93x 93x   2x 2x   19x 19x   9x 9x   2x 2x   3x 3x   20x 20x        
import type { RawData, WebSocket } from "ws";
import type { Logger } from "@shared/logger";
import type { ClientToServerWSMessage } from "@shared/schema";
import { decodeClientMessage } from "../../services/ws-message-decoder";
import type { ClientState } from "./ws-session-manager";
 
function rawDataToString(data: RawData): string {
  Iif (Array.isArray(data)) return Buffer.concat(data).toString();
  Iif (data instanceof ArrayBuffer) return Buffer.from(data).toString();
  return data.toString();
}
 
type MessageHandler<T extends ClientToServerWSMessage> = (
  ws: WebSocket,
  data: T,
  clientState: ClientState,
) => void | Promise<void>;
 
export interface WsMessageRouterHandlers {
  startSimulation: MessageHandler<Extract<ClientToServerWSMessage, { type: "start_simulation" }>>;
  codeChanged: MessageHandler<Extract<ClientToServerWSMessage, { type: "code_changed" }>>;
  stopSimulation: MessageHandler<Extract<ClientToServerWSMessage, { type: "stop_simulation" }>>;
  pauseSimulation: MessageHandler<Extract<ClientToServerWSMessage, { type: "pause_simulation" }>>;
  resumeSimulation: MessageHandler<Extract<ClientToServerWSMessage, { type: "resume_simulation" }>>;
  serialInput: MessageHandler<Extract<ClientToServerWSMessage, { type: "serial_input" }>>;
  setPinValue: MessageHandler<Extract<ClientToServerWSMessage, { type: "set_pin_value" }>>;
}
 
interface WsMessageRouterParams {
  logger: Logger;
  getClientState: (ws: WebSocket) => ClientState | undefined;
  handlers: WsMessageRouterHandlers;
}
 
export class WsMessageRouter {
  constructor(private readonly params: WsMessageRouterParams) {}
 
  async route(ws: WebSocket, message: RawData): Promise<void> {
    try {
      const msgText = rawDataToString(message);
      const data = decodeClientMessage(msgText);
      // Type and size only: messages carry sketch source and serial input.
      this.params.logger.debug(`[WS-IN] ${data?.type ?? "invalid"} (${msgText.length} chars)`);
      if (!data) {
        this.params.logger.warn("[WS] Rejected invalid client message");
        ws.close(1008, "Invalid message");
        return;
      }
 
      const clientState = this.params.getClientState(ws);
      if (!clientState) {
        this.params.logger.warn(
          `[WS] Message received but clientState not found for type: ${data.type}`,
        );
        return;
      }
 
      await this.dispatch(ws, data, clientState);
    } catch (error) {
      this.params.logger.error(
        `Invalid WebSocket message: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }
 
  private async dispatch(
    ws: WebSocket,
    data: ClientToServerWSMessage,
    clientState: ClientState,
  ): Promise<void> {
    switch (data.type) {
      case "start_simulation":
        await this.params.handlers.startSimulation(ws, data, clientState);
        break;
      case "code_changed":
        await this.params.handlers.codeChanged(ws, data, clientState);
        break;
      case "stop_simulation":
        await this.params.handlers.stopSimulation(ws, data, clientState);
        break;
      case "pause_simulation":
        await this.params.handlers.pauseSimulation(ws, data, clientState);
        break;
      case "resume_simulation":
        await this.params.handlers.resumeSimulation(ws, data, clientState);
        break;
      case "serial_input":
        await this.params.handlers.serialInput(ws, data, clientState);
        break;
      case "set_pin_value":
        await this.params.handlers.setPinValue(ws, data, clientState);
        break;
    }
  }
}