3.8 KiB
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 onopenand rejects onerroror caller-signal abort.send(frame: SocketFrame)— typed text or binary send; binary frames are copied into a freshArrayBufferso the underlying WS impl never sees a SharedArrayBuffer-backed view by accident.onMessage/onClose/onError— listeners returning unsubscribe functions.- Accepts
Uint8Array,ArrayBuffer, or anyArrayBufferViewon the wire and normalises them toUint8Arraybefore dispatching. - Sends on a closed socket throw
WebSocketProtocolErrorinstead of silently dropping the frame. - Pluggable
WebSocketConstructorso 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): voidonMessage(listener): () => voidonClose(listener): () => voidonError(listener): () => void
Implementation Notes
- The
xi-api-keyis sent as a query parameter (browsers can't set headers on the WS upgrade) — that's done inElevenLabsClient.openSocket, not here. - Binary
sendalways copies into a freshArrayBufferto defend against SharedArrayBuffer leakage; the cost is one heap allocation per outbound binary frame. - The internal
#wireOpenPromiseremoves itserrorlistener once the promise settles, so post-open transport errors flow only into the user-installedonErrorlisteners — they don't rejectwaitOpen()twice. bytesFromrejects unsupported binary payload types withWebSocketProtocolError. The error is dispatched toonErrorrather than thrown synchronously, since it originates inside an event handler callback.