queryFn игнорировал TanStack v5 AbortSignal: first POST /analyze, poll GET /fetch-status и re-POST не передавали signal, цикл спал на non-abortable setTimeout без проверки signal.aborted. При unmount / смене cad|horizon цикл жил до 2-мин кэпа, дёргая GET /fetch-status каждые 2с + POST /analyze на каждый ready — зомби-поллинг. Проброс signal во все 3 fetch, sleep через shared abortableSleep (extracted из useSiteAnalysis #1242), throw AbortError перед итерацией. +3 vitest (abort + happy-path). Closes #1248
32 lines
1.2 KiB
TypeScript
32 lines
1.2 KiB
TypeScript
/**
|
||
* Sleep for `ms`, but resolve early if the AbortSignal fires.
|
||
*
|
||
* Используем в poll-циклах (analyze / fetch-status) чтобы цикл пробуждался
|
||
* сразу при abort (unmount, смена cad/horizon, cancel), а не ждал полные `ms`
|
||
* до следующей проверки `signal.aborted`. Без этого осиротевший цикл живёт до
|
||
* hard-cap (60 × 2s = 2 мин), сыпя GET /fetch-status + POST /analyze — зомби-
|
||
* поллинг (#1248, sibling #1242).
|
||
*
|
||
* Резолвится (не реджектится) — вызывающий сам решает бросать ли AbortError,
|
||
* проверив `signal.aborted` после await.
|
||
*/
|
||
export function abortableSleep(
|
||
ms: number,
|
||
signal?: AbortSignal,
|
||
): Promise<void> {
|
||
return new Promise((resolve) => {
|
||
if (signal?.aborted) {
|
||
resolve();
|
||
return;
|
||
}
|
||
const onAbort = () => {
|
||
clearTimeout(timer);
|
||
resolve();
|
||
};
|
||
const timer = setTimeout(() => {
|
||
signal?.removeEventListener("abort", onAbort);
|
||
resolve();
|
||
}, ms);
|
||
signal?.addEventListener("abort", onAbort, { once: true });
|
||
});
|
||
}
|