Files
skills/docs/media/audio/third-party-elevenlabs/src/client/socket.md
2026-05-29 08:20:56 +01:00

3.8 KiB

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<void> 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

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

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<void>
  • 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.