fix: normalize abort signals for telegram fetch

This commit is contained in:
Peter Steinberger
2026-01-21 16:46:49 +00:00
parent fb164b321e
commit 0e003cb7f1
7 changed files with 88 additions and 24 deletions

36
src/infra/fetch.test.ts Normal file
View File

@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from "vitest";
import { wrapFetchWithAbortSignal } from "./fetch.js";
describe("wrapFetchWithAbortSignal", () => {
it("converts foreign abort signals to native controllers", async () => {
let seenSignal: AbortSignal | undefined;
const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
seenSignal = init?.signal as AbortSignal | undefined;
return {} as Response;
});
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
let abortHandler: (() => void) | null = null;
const fakeSignal = {
aborted: false,
addEventListener: (event: string, handler: () => void) => {
if (event === "abort") abortHandler = handler;
},
removeEventListener: (event: string, handler: () => void) => {
if (event === "abort" && abortHandler === handler) abortHandler = null;
},
} as AbortSignal;
const promise = wrapped("https://example.com", { signal: fakeSignal });
expect(fetchImpl).toHaveBeenCalledOnce();
expect(seenSignal).toBeInstanceOf(AbortSignal);
expect(seenSignal).not.toBe(fakeSignal);
abortHandler?.();
expect(seenSignal?.aborted).toBe(true);
await promise;
});
});

29
src/infra/fetch.ts Normal file
View File

@@ -0,0 +1,29 @@
export function wrapFetchWithAbortSignal(fetchImpl: typeof fetch): typeof fetch {
return (input: RequestInfo | URL, init?: RequestInit) => {
const signal = init?.signal;
if (!signal) return fetchImpl(input, init);
if (typeof AbortSignal !== "undefined" && signal instanceof AbortSignal) {
return fetchImpl(input, init);
}
if (typeof AbortController === "undefined") {
return fetchImpl(input, init);
}
if (typeof signal.addEventListener !== "function") {
return fetchImpl(input, init);
}
const controller = new AbortController();
const onAbort = () => controller.abort();
if (signal.aborted) {
controller.abort();
} else {
signal.addEventListener("abort", onAbort, { once: true });
}
const response = fetchImpl(input, { ...init, signal: controller.signal });
if (typeof signal.removeEventListener === "function") {
void response.finally(() => {
signal.removeEventListener("abort", onAbort);
});
}
return response;
};
}