From 79b726451d5c52718cef01ccc2af75da6438abaa Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 26 Aug 2026 02:23:21 +0300 Subject: [PATCH] Merge main and keep the read timeout armed without AbortSignal.any MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/ui/src/lib/opencode/client.ts | 33 ++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/lib/opencode/client.ts b/packages/ui/src/lib/opencode/client.ts index fa201397..f7058bed 100644 --- a/packages/ui/src/lib/opencode/client.ts +++ b/packages/ui/src/lib/opencode/client.ts @@ -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(); } },