# `socket` Wrapper around any `WebSocketLike` (the browser global, `ws` from Node, or a fake from tests). Exposes a typed frame-level API and a `Promise` for the open handshake. ## Purpose Raw WebSocket APIs are awkward — open / message / close / error are delivered as untyped events, binary frames arrive as `Blob` / `ArrayBuffer` / typed-array views depending on platform, and there's no built-in way to await the open handshake. `ElevenLabsSocket` papers over all of that so adapters that need WebSocket transport (e.g. the streaming-input TTS endpoint) can `await sock.waitOpen()` and then send typed `SocketFrame` values without touching the raw event surface. ## Features - `waitOpen()` — promise that resolves on `open` and rejects on `error` or caller-signal abort. - `send(frame: SocketFrame)` — typed text or binary send; binary frames are copied into a fresh `ArrayBuffer` so the underlying WS impl never sees a SharedArrayBuffer-backed view by accident. - `onMessage` / `onClose` / `onError` — listeners returning unsubscribe functions. - Accepts `Uint8Array`, `ArrayBuffer`, or any `ArrayBufferView` on the wire and normalises them to `Uint8Array` before dispatching. - Sends on a closed socket throw `WebSocketProtocolError` instead of silently dropping the frame. - Pluggable `WebSocketConstructor` so tests inject a fake. ## Usage: Basic Example ```ts import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs"; const client = new ElevenLabsClient({ apiKey: process.env.KEY! }); const sock = client.openSocket({ path: "/v1/text-to-speech/voice-id/stream-input", feature: "tts", operation: "stream_input", query: { model_id: "eleven_flash_v2" }, }); await sock.waitOpen(); sock.onMessage((f) => process.stdout.write(f.data)); sock.send({ kind: "text", data: JSON.stringify({ text: "hello" }) }); ``` ## Usage: Advanced Example ```ts import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs"; const ctrl = new AbortController(); const client = new ElevenLabsClient({ apiKey: KEY, webSocketImpl: WS, // 'ws' on Node }); const sock = client.openSocket({ path: "/v1/text-to-speech/abc/stream-input", feature: "tts", operation: "stream_input", }); const unsubMsg = sock.onMessage((f) => sink.write(f.data)); const unsubClose = sock.onClose((e) => log.info("closed", { code: e.code, clean: e.wasClean }) ); sock.onError((err) => log.warn("ws error", { err })); try { await sock.waitOpen(); for (const chunk of corpus) { sock.send({ kind: "text", data: JSON.stringify({ text: chunk }) }); } } finally { unsubMsg(); unsubClose(); sock.close(1000, "done"); } ctrl.abort(); ``` ## API Reference - `new ElevenLabsSocket(ws: WebSocketLike, endpoint: string, signal?: AbortSignal)` - `waitOpen(): Promise` - `readyState: number` — 0 connecting / 1 open / 2 closing / 3 closed. - `send(frame: SocketFrame): void` — `{ kind: "text" | "binary", data }` - `close(code?: number, reason?: string): void` - `onMessage(listener): () => void` - `onClose(listener): () => void` - `onError(listener): () => void` ## Implementation Notes - The `xi-api-key` is sent as a query parameter (browsers can't set headers on the WS upgrade) — that's done in `ElevenLabsClient.openSocket`, not here. - Binary `send` always copies into a fresh `ArrayBuffer` to defend against SharedArrayBuffer leakage; the cost is one heap allocation per outbound binary frame. - The internal `#wireOpenPromise` removes its `error` listener once the promise settles, so post-open transport errors flow only into the user-installed `onError` listeners — they don't reject `waitOpen()` twice. - `bytesFrom` rejects unsupported binary payload types with `WebSocketProtocolError`. The error is dispatched to `onError` rather than thrown synchronously, since it originates inside an event handler callback.