Files
ComfyUI-OpenClaw/web/openclaw_fetch_wrappers.js

124 lines
4.0 KiB
JavaScript

/**
* R96: Fetch wrapper composition helpers with idempotence guard.
*
* Prevents duplicate wrapper stacking when bootstrap code runs multiple times.
*/
const WRAP_META = Symbol.for("openclaw.fetch_wrapper_meta");
const PRECONNECTED_ORIGINS = new Set();
function getDecoratorId(decorator, index) {
if (typeof decorator?.id === "string" && decorator.id) return decorator.id;
if (typeof decorator?.name === "string" && decorator.name) return decorator.name;
return `decorator_${index}`;
}
function arraysEqual(a = [], b = []) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}
export function getFetchWrapperMeta(fetchFn) {
return fetchFn?.[WRAP_META] || null;
}
export function composeFetchWrappersOnce(fetchFn, decorators = []) {
if (typeof fetchFn !== "function") {
throw new TypeError("fetchFn must be a function");
}
if (!Array.isArray(decorators)) {
throw new TypeError("decorators must be an array");
}
const chainIds = decorators.map(getDecoratorId);
const existingMeta = getFetchWrapperMeta(fetchFn);
if (existingMeta && arraysEqual(existingMeta.chainIds, chainIds)) {
return fetchFn;
}
let wrapped = fetchFn;
decorators.forEach((decorator, index) => {
if (typeof decorator !== "function") {
throw new TypeError(`decorator at index ${index} must be a function`);
}
wrapped = decorator(wrapped);
});
const baseMeta = existingMeta || { baseFetch: fetchFn, appliedCount: 0, chainIds: [] };
Object.defineProperty(wrapped, WRAP_META, {
value: {
baseFetch: baseMeta.baseFetch || fetchFn,
appliedCount: (baseMeta.appliedCount || 0) + 1,
chainIds,
},
enumerable: false,
configurable: false,
writable: false,
});
return wrapped;
}
/**
* Decorator: ensures signal is passed through when options contain one.
* (Useful when other decorators clone/normalize init objects.)
*/
export function withAbortPassthrough() {
const decorator = async (next, input, init = {}) => next(input, { ...init });
const wrapper = (next) => (input, init) => decorator(next, input, init);
wrapper.id = "abort_passthrough";
return wrapper;
}
/**
* Decorator: best-effort browser preconnect hint for HTTP(S) origins.
*/
export function withPreconnectHint() {
const wrapper = (next) => (input, init) => {
try {
const url = typeof input === "string" ? input : (input?.url || "");
if (url && typeof document !== "undefined" && /^https?:\/\//i.test(url)) {
const origin = new URL(url, window.location.href).origin;
if (!PRECONNECTED_ORIGINS.has(origin)) {
PRECONNECTED_ORIGINS.add(origin);
const link = document.createElement("link");
link.rel = "preconnect";
link.href = origin;
document.head.appendChild(link);
}
}
} catch {
// best-effort only
}
return next(input, init);
};
wrapper.id = "preconnect_hint";
return wrapper;
}
/**
* Decorator: single retry for idempotent GET requests on network failures.
*/
export function withGetRetry({ retries = 1 } = {}) {
const wrapper = (next) => async (input, init = {}) => {
const method = String(init?.method || "GET").toUpperCase();
const shouldRetry = method === "GET" && retries > 0;
let lastErr;
const attempts = shouldRetry ? retries + 1 : 1;
for (let i = 0; i < attempts; i += 1) {
try {
return await next(input, init);
} catch (err) {
const isAbort = err?.name === "AbortError";
if (isAbort || i >= attempts - 1) throw err;
lastErr = err;
}
}
throw lastErr || new Error("fetch_retry_exhausted");
};
wrapper.id = `retry_get_${retries}`;
return wrapper;
}