mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 11:30:41 +00:00
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { bindAbortRelay } from "../utils/fetch-timeout.js";
|
|
|
|
type FetchWithPreconnect = typeof fetch & {
|
|
preconnect: (url: string, init?: { credentials?: RequestCredentials }) => void;
|
|
};
|
|
|
|
type RequestInitWithDuplex = RequestInit & { duplex?: "half" };
|
|
|
|
const wrapFetchWithAbortSignalMarker = Symbol.for("openclaw.fetch.abort-signal-wrapped");
|
|
|
|
type FetchWithAbortSignalMarker = typeof fetch & {
|
|
[wrapFetchWithAbortSignalMarker]?: true;
|
|
};
|
|
|
|
function withDuplex(
|
|
init: RequestInit | undefined,
|
|
input: RequestInfo | URL,
|
|
): RequestInit | undefined {
|
|
const hasInitBody = init?.body != null;
|
|
const hasRequestBody =
|
|
!hasInitBody &&
|
|
typeof Request !== "undefined" &&
|
|
input instanceof Request &&
|
|
input.body != null;
|
|
if (!hasInitBody && !hasRequestBody) {
|
|
return init;
|
|
}
|
|
if (init && "duplex" in (init as Record<string, unknown>)) {
|
|
return init;
|
|
}
|
|
return init
|
|
? ({ ...init, duplex: "half" as const } as RequestInitWithDuplex)
|
|
: ({ duplex: "half" as const } as RequestInitWithDuplex);
|
|
}
|
|
|
|
export function wrapFetchWithAbortSignal(fetchImpl: typeof fetch): typeof fetch {
|
|
if ((fetchImpl as FetchWithAbortSignalMarker)[wrapFetchWithAbortSignalMarker]) {
|
|
return fetchImpl;
|
|
}
|
|
|
|
const wrapped = ((input: RequestInfo | URL, init?: RequestInit) => {
|
|
const patchedInit = withDuplex(init, input);
|
|
const signal = patchedInit?.signal;
|
|
if (!signal) {
|
|
return fetchImpl(input, patchedInit);
|
|
}
|
|
if (typeof AbortSignal !== "undefined" && signal instanceof AbortSignal) {
|
|
return fetchImpl(input, patchedInit);
|
|
}
|
|
if (typeof AbortController === "undefined") {
|
|
return fetchImpl(input, patchedInit);
|
|
}
|
|
if (typeof signal.addEventListener !== "function") {
|
|
return fetchImpl(input, patchedInit);
|
|
}
|
|
const controller = new AbortController();
|
|
const onAbort = bindAbortRelay(controller);
|
|
let listenerAttached = false;
|
|
if (signal.aborted) {
|
|
controller.abort();
|
|
} else {
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
listenerAttached = true;
|
|
}
|
|
const cleanup = () => {
|
|
if (!listenerAttached || typeof signal.removeEventListener !== "function") {
|
|
return;
|
|
}
|
|
listenerAttached = false;
|
|
try {
|
|
signal.removeEventListener("abort", onAbort);
|
|
} catch {
|
|
// Foreign/custom AbortSignal implementations may throw here.
|
|
// Never let cleanup mask the original fetch result/error.
|
|
}
|
|
};
|
|
try {
|
|
const response = fetchImpl(input, { ...patchedInit, signal: controller.signal });
|
|
return response.finally(cleanup);
|
|
} catch (error) {
|
|
cleanup();
|
|
throw error;
|
|
}
|
|
}) as FetchWithPreconnect;
|
|
|
|
const wrappedFetch = Object.assign(wrapped, fetchImpl) as FetchWithPreconnect;
|
|
const fetchWithPreconnect = fetchImpl as FetchWithPreconnect;
|
|
wrappedFetch.preconnect =
|
|
typeof fetchWithPreconnect.preconnect === "function"
|
|
? fetchWithPreconnect.preconnect.bind(fetchWithPreconnect)
|
|
: () => {};
|
|
|
|
Object.defineProperty(wrappedFetch, wrapFetchWithAbortSignalMarker, {
|
|
value: true,
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
});
|
|
|
|
return wrappedFetch;
|
|
}
|
|
|
|
export function resolveFetch(fetchImpl?: typeof fetch): typeof fetch | undefined {
|
|
const resolved = fetchImpl ?? globalThis.fetch;
|
|
if (!resolved) {
|
|
return undefined;
|
|
}
|
|
return wrapFetchWithAbortSignal(resolved);
|
|
}
|