export const MAX_CLIENT_WS_PAYLOAD_BYTES = 10 * 1024 * 1024; export class WsPayloadTooLargeError extends Error { constructor(byteLength: number) { super(`WebSocket message too large: ${byteLength} bytes`); this.name = "WsPayloadTooLargeError"; } } export interface JsonWsMessage { type: string; payload?: unknown; [key: string]: unknown; } function assertPayloadSize(byteLength: number): void { if (byteLength > MAX_CLIENT_WS_PAYLOAD_BYTES) { throw new WsPayloadTooLargeError(byteLength); } } function decodeWsText(data: unknown): string { if (typeof data === "string") { assertPayloadSize(Buffer.byteLength(data, "utf8")); return data; } if (data instanceof ArrayBuffer) { assertPayloadSize(data.byteLength); return new TextDecoder().decode(new Uint8Array(data)); } if (ArrayBuffer.isView(data)) { assertPayloadSize(data.byteLength); return new TextDecoder().decode( new Uint8Array(data.buffer, data.byteOffset, data.byteLength), ); } if (Array.isArray(data) && data.every(Buffer.isBuffer)) { const byteLength = data.reduce((total, chunk) => total + chunk.byteLength, 0); assertPayloadSize(byteLength); return Buffer.concat(data, byteLength).toString("utf8"); } throw new Error("Unsupported WebSocket message payload"); } export function decodeJsonWsMessage(data: unknown): JsonWsMessage { const parsed = JSON.parse(decodeWsText(data)) as unknown; if ( typeof parsed !== "object" || parsed === null || !("type" in parsed) || typeof parsed.type !== "string" ) { throw new Error("Invalid WebSocket message payload"); } return parsed as JsonWsMessage; }