Merge main and keep the read timeout armed without AbortSignal.any

When the caller passes its own AbortSignal and AbortSignal.any is
unavailable, the timeout was silently dropped — disabling the fix on
exactly the bootstrap reads it targets, since those carry a cancellation
signal. Compose the two signals manually through an AbortController in
that case; listeners detach when the request settles.
This commit is contained in:
Bohdan Triapitsyn
2026-08-26 02:23:21 +03:00
parent 90b6776e5b
commit 79b726451d
+29 -4
View File
@@ -240,10 +240,34 @@ export const createRuntimeOpencodeClient = (config: RuntimeOpencodeClientConfig)
const callerSignal = init?.signal;
const supportsAny = typeof AbortSignal !== 'undefined'
&& typeof (AbortSignal as { any?: unknown }).any === 'function';
const signal: AbortSignal = callerSignal && supportsAny
? (AbortSignal as typeof AbortSignal & { any: (signals: AbortSignal[]) => AbortSignal })
.any([callerSignal, timeout.signal])
: (callerSignal ?? timeout.signal);
let signal: AbortSignal;
let detachFallback: (() => void) | null = null;
if (callerSignal && supportsAny) {
signal = (AbortSignal as typeof AbortSignal & { any: (signals: AbortSignal[]) => AbortSignal })
.any([callerSignal, timeout.signal]);
} else if (callerSignal) {
// No AbortSignal.any: compose manually. Silently dropping the timeout
// here would disable the fix on exactly the bootstrap reads it
// targets, since those carry a cancellation signal.
const controller = new AbortController();
const abortFromCaller = () => controller.abort(callerSignal.reason);
const abortFromTimeout = () => controller.abort(timeout.signal.reason);
if (callerSignal.aborted) {
abortFromCaller();
} else if (timeout.signal.aborted) {
abortFromTimeout();
} else {
callerSignal.addEventListener('abort', abortFromCaller, { once: true });
timeout.signal.addEventListener('abort', abortFromTimeout, { once: true });
detachFallback = () => {
callerSignal.removeEventListener('abort', abortFromCaller);
timeout.signal.removeEventListener('abort', abortFromTimeout);
};
}
signal = controller.signal;
} else {
signal = timeout.signal;
}
try {
return await runtimeFetch(input, { ...init, signal });
} catch (error) {
@@ -252,6 +276,7 @@ export const createRuntimeOpencodeClient = (config: RuntimeOpencodeClientConfig)
}
throw error;
} finally {
detachFallback?.();
timeout.cleanup();
}
},