All files / src translate.ts

100% Statements 24/24
100% Branches 24/24
100% Functions 2/2
100% Lines 16/16

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                                                                                            33x 31x     29x 29x   1x     28x 26x   25x   22x 22x   19x 19x 17x   15x                       23x       23x            
/**
 * Pure translation between socket.io v0.9 event frames and EtherCalc's
 * native WS message types.
 *
 * Scope: we only bridge `type:5` (Event) frames — that's the single event
 * channel the legacy server ever used (`@on data` in the LiveScript). Other
 * packet types (connect/disconnect/heartbeat) are managed elsewhere and are
 * not user-data-bearing.
 *
 * Legacy wire shape for events:
 *   Frame:  `5::<endpoint>:{"name":"data","args":[{…payload}]}`
 *   Meaning: emit event named "data" with one argument = the EtherCalc
 *           message object.
 *
 * Direction:
 *   client → server: unwrap `args[0]` and validate it matches the
 *                    ClientMessage discriminated union.
 *   server → client: wrap the ServerMessage into an `args[0]` slot.
 */
import {
  CLIENT_MESSAGE_TYPES,
  type ClientMessage,
  type ServerMessage,
} from '@ethercalc/shared/messages';
import { encodeFrame, PacketType, type Packet } from './framing.ts';
 
interface SocketIoEventPayload {
  name?: unknown;
  args?: unknown;
}
 
/**
 * Translate an inbound socket.io event packet to a native ClientMessage.
 *
 * Returns `null` (never throws) when:
 *   - packet is not of type Event (5)
 *   - data is missing or malformed JSON
 *   - `name` is not "data"
 *   - `args` is missing, empty, or the first arg isn't a ClientMessage
 *
 * We deliberately don't validate payload shape beyond the `type` field —
 * the native WS layer owes deeper validation for *its* input, and the
 * shim's job is to be a thin translator. Duplicating type checks here
 * would drift from the canonical parser in shared/messages.ts.
 */
export function socketIoEventToNative(packet: Packet): ClientMessage | null {
  if (packet.type !== PacketType.Event) return null;
  if (packet.data === undefined || packet.data === '') return null;
 
  let parsed: SocketIoEventPayload;
  try {
    parsed = JSON.parse(packet.data) as SocketIoEventPayload;
  } catch {
    return null;
  }
 
  if (!parsed || typeof parsed !== 'object') return null;
  if (parsed.name !== 'data') return null;
 
  if (!Array.isArray(parsed.args) || parsed.args.length === 0) return null;
 
  const first = parsed.args[0];
  if (!first || typeof first !== 'object') return null;
 
  const type = (first as { type?: unknown }).type;
  if (typeof type !== 'string') return null;
  if (!(CLIENT_MESSAGE_TYPES as readonly string[]).includes(type)) return null;
 
  return first as ClientMessage;
}
 
/**
 * Wrap an outbound ServerMessage as a socket.io event frame string ready
 * to hand to a WebSocket client's `send()` call.
 *
 * Produces `5::/:{"name":"data","args":[{…msg}]}` — the event name is
 * fixed to `"data"` to match the legacy server's single broadcast channel.
 * Endpoint is empty (root namespace) which the v0.9 client expects.
 */
export function nativeToSocketIoEvent(msg: ServerMessage): string {
  const payload: SocketIoEventPayload = {
    name: 'data',
    args: [msg],
  };
  return encodeFrame({
    type: PacketType.Event,
    endpoint: '',
    data: JSON.stringify(payload),
  });
}