UNPKG

phonic

Version:

[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2FPhonic-Co%2Fphonic-node) [![npm shield](htt

290 lines (289 loc) 13 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReconnectableConversationsSocket = void 0; const core = __importStar(require("../core/index.js")); const Socket_js_1 = require("../api/resources/conversations/client/Socket.js"); const json_js_1 = require("../core/json.js"); /** 1006 is the only close code that indicates an unexpected disconnect * worth reconnecting for. All other codes (1000, 4000, 4800, etc.) * are intentional server-side closes. */ const ABNORMAL_CLOSURE = 1006; const BASE_RECONNECT_DELAY_MS = 500; const MAX_RECONNECT_DELAY_MS = 5000; /** Safety cap: stop retrying if the server is completely unreachable. * In normal operation the server's terminal codes (4800/4801) stop * retries much sooner (within the 10s grace period). */ const MAX_RECONNECT_ATTEMPTS = 10; /** * Wraps ConversationsSocket with automatic reconnection on 1006. * * On abnormal closure, creates a brand new ReconnectingWebSocket (via the * createReconnectSocket factory) and wraps it in a fresh ConversationsSocket, * forwarding all events to user-registered handlers. * * Retries reconnection with exponential backoff until the server responds * with a terminal close code (4800 session expired, 4801 invalid state), * the safety cap is reached, or the user calls close(). * * Uses composition rather than inheritance to avoid coupling to the parent's * private event handler registration or ReconnectingWebSocket internals. */ class ReconnectableConversationsSocket { constructor(args) { this._conversationId = null; this._handlers = {}; this._reconnectAttempts = 0; this._isClosed = false; this._cleanupWireListeners = null; this._pendingReconnect = null; this._pendingReplacement = false; this._createReconnectSocket = args.createReconnectSocket; this._abortSignal = args.abortSignal != undefined ? args.abortSignal : null; this._inner = new Socket_js_1.ConversationsSocket({ socket: args.socket }); this._wireInner(this._inner, args.socket); } /** The conversation ID captured from the server's conversation_created message. */ get conversationId() { return this._conversationId; } get socket() { return this._inner.socket; } get readyState() { return this._inner.readyState; } on(event, callback) { this._handlers[event] = callback; } /** Drop outbound sends when we cannot talk to a live socket (no queue). */ _safeSend(op) { if (this._isClosed || this._pendingReplacement) { return; } if (this._inner.readyState !== core.ReconnectingWebSocket.ReadyState.OPEN) { return; } op(this._inner); } sendConfig(message) { this._safeSend((inner) => inner.sendConfig(message)); } sendAudioChunk(message) { this._safeSend((inner) => inner.sendAudioChunk(message)); } sendToolCallOutput(message) { this._safeSend((inner) => inner.sendToolCallOutput(message)); } sendUpdateSystemPrompt(message) { this._safeSend((inner) => inner.sendUpdateSystemPrompt(message)); } sendAddSystemMessage(message) { this._safeSend((inner) => inner.sendAddSystemMessage(message)); } sendSetExternalId(message) { this._safeSend((inner) => inner.sendSetExternalId(message)); } sendGenerateReply(message) { this._safeSend((inner) => inner.sendGenerateReply(message)); } sendSay(message) { this._safeSend((inner) => inner.sendSay(message)); } sendReset(message) { this._safeSend((inner) => inner.sendReset(message)); } sendMute(message) { this._safeSend((inner) => inner.sendMute(message)); } sendUnmute(message) { this._safeSend((inner) => inner.sendUnmute(message)); } /** * Not supported — reconnection after 1006 is handled automatically. * To start a new conversation, create a new socket via client.conversations.connect(). */ connect() { throw new Error("connect() is not supported on ReconnectableConversationsSocket. " + "Reconnection after 1006 is automatic. To start a new conversation, " + "call client.conversations.connect() again."); } close() { var _a; this._isClosed = true; this._pendingReplacement = false; if (this._pendingReconnect != null) { clearTimeout(this._pendingReconnect); this._pendingReconnect = null; } (_a = this._cleanupWireListeners) === null || _a === void 0 ? void 0 : _a.call(this); this._cleanupWireListeners = null; this._inner.close(); } waitForOpen() { return __awaiter(this, void 0, void 0, function* () { return this._inner.waitForOpen(); }); } _getReconnectDelay() { // Exponential backoff: 500ms, 1s, 2s, 4s, capped at 5s const delay = BASE_RECONNECT_DELAY_MS * Math.pow(2, this._reconnectAttempts - 1); return Math.min(delay, MAX_RECONNECT_DELAY_MS); } /** Schedule a reconnection attempt after backoff delay. */ _scheduleReconnect() { var _a, _b, _c; if (this._isClosed || this._conversationId === null || ((_a = this._abortSignal) === null || _a === void 0 ? void 0 : _a.aborted)) { return; } if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { this._pendingReplacement = false; (_c = (_b = this._handlers).error) === null || _c === void 0 ? void 0 : _c.call(_b, new Error("Max reconnect attempts reached")); return; } // Clear any existing timer to prevent orphaned timeouts if (this._pendingReconnect != null) { clearTimeout(this._pendingReconnect); } this._reconnectAttempts++; const delay = this._getReconnectDelay(); this._pendingReplacement = true; this._pendingReconnect = setTimeout(() => { this._pendingReconnect = null; if (this._isClosed) { this._pendingReplacement = false; return; } void this._doReconnect(); }, delay); } /** Perform the actual reconnection attempt. */ _doReconnect() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; try { const created = this._createReconnectSocket(this._conversationId); const newRawSocket = created instanceof Promise ? yield created : created; if (this._isClosed || ((_a = this._abortSignal) === null || _a === void 0 ? void 0 : _a.aborted)) { this._pendingReplacement = false; newRawSocket.close(); return; } // Clean up the old socket: remove our custom listeners and close // the ConversationsSocket wrapper (which removes its own listeners). (_b = this._cleanupWireListeners) === null || _b === void 0 ? void 0 : _b.call(this); this._cleanupWireListeners = null; try { this._inner.close(); } catch ( /* already closed from 1006 */_c) { /* already closed from 1006 */ } const newInner = new Socket_js_1.ConversationsSocket({ socket: newRawSocket }); this._inner = newInner; this._wireInner(newInner, newRawSocket); } catch (_d) { this._pendingReplacement = false; // Connection failed — schedule another attempt. // The server's grace period (10s) is the natural limit; // once it expires, the next attempt will get 4800 and stop. this._scheduleReconnect(); } }); } _wireInner(inner, rawSocket) { // Forward events from the inner ConversationsSocket to user handlers. // Clear _pendingReplacement before calling the user's open handler // so that sends inside the handler are not silently dropped. inner.on("open", () => { var _a, _b; this._pendingReplacement = false; (_b = (_a = this._handlers).open) === null || _b === void 0 ? void 0 : _b.call(_a); }); inner.on("message", (msg) => { var _a, _b; return (_b = (_a = this._handlers).message) === null || _b === void 0 ? void 0 : _b.call(_a, msg); }); inner.on("close", (ev) => { var _a, _b; return (_b = (_a = this._handlers).close) === null || _b === void 0 ? void 0 : _b.call(_a, ev); }); inner.on("error", (err) => { var _a, _b; return (_b = (_a = this._handlers).error) === null || _b === void 0 ? void 0 : _b.call(_a, err); }); // Intercept raw messages to capture conversation_id and reset reconnect counter const onMessage = (event) => { try { const data = (0, json_js_1.fromJson)(event.data); if (data && typeof data === "object" && "type" in data) { const type = data.type; if (type === "conversation_created") { this._conversationId = data.conversation_id; } if (type === "conversation_reconnected") { this._reconnectAttempts = 0; } } } catch (_a) { // ignore — inner socket handles parse errors } }; const onClose = (event) => { if (this._isClosed) { return; } if (event.code === ABNORMAL_CLOSURE && this._conversationId !== null) { // We have a conversation to resume — cancel RWS's built-in // auto-reconnect and handle it ourselves with reconnect_conv_id. // _handleClose calls _connect() before notifying listeners, // but _connect() awaits _wait() which is async. Calling // close() synchronously sets _closeCalled = true, which // _connect() checks before opening the new socket. rawSocket.close(); this._scheduleReconnect(); } // 1006 without conversationId: let RWS handle transport-level // reconnect normally (starts a fresh conversation). }; rawSocket.addEventListener("message", onMessage); rawSocket.addEventListener("close", onClose); this._cleanupWireListeners = () => { rawSocket.removeEventListener("message", onMessage); rawSocket.removeEventListener("close", onClose); }; } } exports.ReconnectableConversationsSocket = ReconnectableConversationsSocket;