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

91.42% Statements 32/35
76.47% Branches 13/17
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              64x 64x 64x                                                   19x     64x 64x 64x 64x 64x 2x 2x 2x     62x 62x 1x     1x     61x                         61x   48x 48x   2x 2x   4x 4x   3x 3x   2x 2x   1x 1x   1x 1x        
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);
      this.params.logger.debug(`[WS-IN] ${msgText}`);
      const data = decodeClientMessage(msgText);
      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;
    }
  }
}