UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

174 lines (173 loc) 6.61 kB
import { F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js"; import { t as decodeTextPrefix } from "./src-vebZIeLe.js"; import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; //#region src/infra/http-response-body-timeout.ts function createResponseBodyTimeoutError(message) { const error = new Error(message); error.name = "TimeoutError"; return error; } async function withResponseBodyTimeout(params) { if (params.timeoutMs === void 0) return await params.read(); const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 1); let timeoutId; let timeoutError; return await new Promise((resolve, reject) => { const clear = () => { if (timeoutId !== void 0) { clearTimeout(timeoutId); timeoutId = void 0; } }; timeoutId = setTimeout(() => { const error = params.onTimeout?.({ timeoutMs }) ?? createResponseBodyTimeoutError(`Response body timed out after ${timeoutMs}ms`); timeoutError = error; clear(); params.cancel(error).catch(() => void 0); reject(error); }, timeoutMs); if (typeof timeoutId === "object" && "unref" in timeoutId) timeoutId.unref(); Promise.resolve().then(() => params.read(() => { if (timeoutError) throw timeoutError; timeoutId?.refresh(); })).then((value) => { clear(); if (!timeoutError) resolve(value); }, (error) => { clear(); if (!timeoutError) reject(toErrorObject(error, "Non-Error rejection")); }); }); } /** Owns one refreshable idle deadline for a bounded response-body operation. */ function withResponseBodyIdleTimeout(reader, chunkTimeoutMs, onIdleTimeout, read) { if (chunkTimeoutMs === void 0) return read(); return withResponseBodyTimeout({ timeoutMs: chunkTimeoutMs, onTimeout: ({ timeoutMs }) => onIdleTimeout?.({ chunkTimeoutMs: timeoutMs }) ?? createResponseBodyTimeoutError(`Media download stalled: no data received for ${timeoutMs}ms`), cancel: async (error) => await reader.cancel(error), read }); } /** Reads one chunk, rejecting and cancelling the reader after an idle timeout. */ async function readChunkWithIdleTimeout(reader, chunkTimeoutMs, onIdleTimeout) { return await withResponseBodyIdleTimeout(reader, chunkTimeoutMs, onIdleTimeout, () => reader.read()); } //#endregion //#region src/infra/http-response-body.ts /** Requests cancellation only when no consumer has started reading the body. */ async function cancelUnreadResponseBody(response) { if (response && !response.bodyUsed) response.body?.cancel().catch(() => void 0); } async function readResponsePrefixFromReader(reader, maxBytes, options) { const chunks = []; let size = 0; let truncated = false; try { await withResponseBodyIdleTimeout(reader, options?.chunkTimeoutMs || void 0, options?.onIdleTimeout, async (refreshTimeout) => { while (true) { refreshTimeout?.(); const { done, value } = await reader.read(); if (done) break; if (!value?.length) continue; const remaining = maxBytes - size; size += value.length; if (size > maxBytes || options?.stopAtLimit && size === maxBytes) { if (remaining > 0) chunks.push(value.subarray(0, remaining)); truncated = true; reader.cancel().catch(() => void 0); break; } chunks.push(value); } }); } finally { try { reader.releaseLock(); } catch {} } return { materializeBuffer: () => Buffer.concat(chunks, Math.floor(Math.min(size, maxBytes))), size, truncated }; } async function readResponsePrefix(response, maxBytes, options) { if (!Number.isFinite(maxBytes) || maxBytes < 0) throw new RangeError(`maxBytes must be a non-negative finite number: ${maxBytes}`); let timeoutMs; try { timeoutMs = typeof options?.timeoutMs === "function" ? options.timeoutMs() : options?.timeoutMs; } catch (error) { response.body?.cancel(error).catch(() => void 0); throw error; } const body = response.body; if (!body || typeof body.getReader !== "function") return await withResponseBodyTimeout({ timeoutMs, onTimeout: options?.onTimeout, cancel: async (error) => await body?.cancel(error), read: async () => { const fallback = Buffer.from(await response.arrayBuffer()); const truncated = fallback.length > maxBytes; return { materializeBuffer: () => truncated ? fallback.subarray(0, maxBytes) : fallback, size: fallback.length, truncated }; } }); const reader = body.getReader(); return await withResponseBodyTimeout({ timeoutMs, onTimeout: options?.onTimeout, cancel: async (error) => await reader.cancel(error), read: async () => await readResponsePrefixFromReader(reader, maxBytes, options) }); } /** Reads and decodes a bounded text prefix while cancelling unread overflow. */ async function readResponseTextPrefix(response, maxBytes, options) { const prefix = await readResponsePrefix(response, maxBytes, { ...options, stopAtLimit: true }); return { text: decodeTextPrefix(prefix.materializeBuffer(), { truncated: prefix.truncated }), size: prefix.size, truncated: prefix.truncated }; } /** Reads a response body under byte, idle, and overall timeout bounds. */ async function readResponseWithLimit(response, maxBytes, options) { const onOverflow = options?.onOverflow; const prefix = await readResponsePrefix(response, maxBytes, { chunkTimeoutMs: options?.chunkTimeoutMs, onIdleTimeout: options?.onIdleTimeout, timeoutMs: options?.timeoutMs, onTimeout: options?.onTimeout }); if (prefix.truncated) throw onOverflow ? onOverflow({ size: prefix.size, maxBytes, res: response }) : /* @__PURE__ */ new Error(`Content too large: ${prefix.size} bytes (limit: ${maxBytes} bytes)`); return prefix.materializeBuffer(); } /** Reads a small collapsed text prefix from a response body for diagnostics/errors. */ async function readResponseTextSnippet(response, options) { const maxBytes = options?.maxBytes ?? 8192; const maxChars = options?.maxChars ?? 200; const prefix = await readResponseTextPrefix(response, maxBytes, { chunkTimeoutMs: options?.chunkTimeoutMs, onIdleTimeout: options?.onIdleTimeout, timeoutMs: options?.timeoutMs, onTimeout: options?.onTimeout }); if (!prefix.text) return; const collapsed = prefix.text.replace(/\s+/g, " ").trim(); if (!collapsed) return; if (collapsed.length > maxChars) return `${truncateUtf16Safe(collapsed, maxChars)}…`; return prefix.truncated ? `${collapsed}…` : collapsed; } //#endregion export { readChunkWithIdleTimeout as a, readResponseWithLimit as i, readResponseTextPrefix as n, readResponseTextSnippet as r, cancelUnreadResponseBody as t };