UNPKG

@ai-sdk/provider-utils

Version:
117 lines (108 loc) 3.67 kB
import { removeUndefinedEntries } from './remove-undefined-entries'; import { getWebSocketConstructor, readWebSocketMessageText, type WebSocketConstructor, type WebSocketLike, } from './websocket'; export interface WebSocketConnection { /** Undefined when the constructor threw or the signal was already aborted. */ socket: WebSocketLike | undefined; /** * Unregisters the abort listener and closes the socket. Never throws. * Handlers stay attached; callers guard their own terminal state. */ close: (code?: number) => void; } /** * Opens a WebSocket for a provider model, owning the transport-generic layer * (analogous to `postToApi` for HTTP): constructor resolution, header hygiene, * abort wiring, and message decoding. Callers own the URL, the auth channel * (subprotocols vs headers), and the wire protocol. */ export function connectToWebSocket({ url, protocols, headers, webSocket, abortSignal, onOpen, onMessageText, onProcessingError, onSocketError, onClose, onAbort, }: { url: string | URL; protocols?: string | string[]; headers?: Record<string, string | undefined>; webSocket?: WebSocketConstructor; abortSignal?: AbortSignal; onOpen?: (socket: WebSocketLike) => void; /** One decoded message. Throws and rejections go to `onProcessingError`. */ onMessageText: (text: string) => void | PromiseLike<void>; /** Constructor throws and message decoding/processing failures. */ onProcessingError: (error: unknown) => void; onSocketError?: () => void; onClose?: () => void; /** Also called (without opening a socket) when the signal is already aborted. */ onAbort?: (reason: unknown) => void; }): WebSocketConnection { let socket: WebSocketLike | undefined; let abortListener: (() => void) | undefined; const close = (code?: number) => { if (abortListener != null) { abortSignal?.removeEventListener('abort', abortListener); abortListener = undefined; } try { socket?.close(code); } catch { // socket may already be closed } }; if (abortSignal?.aborted) { onAbort?.(abortSignal.reason ?? new Error('Aborted')); return { socket: undefined, close }; } try { const WebSocketConstructor = getWebSocketConstructor(webSocket); // native `WebSocket` ignores the headers option; header-capable // implementations like `ws` forward it and throw on undefined values: socket = new WebSocketConstructor(url, protocols, { headers: removeUndefinedEntries(headers ?? {}), }); } catch (error) { onProcessingError(error); return { socket: undefined, close }; } if (abortSignal != null && onAbort != null) { abortListener = () => onAbort(abortSignal.reason ?? new Error('Aborted')); abortSignal.addEventListener('abort', abortListener, { once: true }); } const openedSocket = socket; socket.onopen = () => { try { onOpen?.(openedSocket); } catch (error) { onProcessingError(error); } }; // Messages are processed through a promise tail so async decoding (e.g. // Blob frames) cannot reorder them, and error/close handling cannot // overtake a still-decoding terminal frame. let tail: Promise<void> = Promise.resolve(); socket.onmessage = event => { tail = tail .then(() => readWebSocketMessageText(event.data)) .then(text => onMessageText(text)) .catch(onProcessingError); }; socket.onerror = () => { tail = tail.then(() => onSocketError?.()).catch(onProcessingError); }; socket.onclose = () => { tail = tail.then(() => onClose?.()).catch(onProcessingError); }; return { socket, close }; }