Files
orca/src/main/git/fork-sync.ts
T
Neil af98a72f3f refactor(net): use AbortSignal.timeout for fetch timeouts, fix fork-sync clobbered cancel (#7504)
Replace the hand-rolled `AbortController` + `setTimeout(() => controller.abort())`
+ `clearTimeout` in `finally` pattern with `AbortSignal.timeout(ms)` across the
main-process fetchers, updaters, and hosted-provider clients. This removes a
timer-leak footgun (a thrown/early-returned path that skips the finally leaks the
timer) and ~3-4 lines of bookkeeping per site. `AbortSignal.timeout` is Node
17.3+ (Electron main is Node 22+).

Two sites compose a caller-cancel signal with the timeout via `AbortSignal.any`
(Node 20.3+) instead of a manual abort listener:
- git/fork-sync.ts: also fixes a latent bug — the caller's `options.signal` was
  spread into the git options then immediately clobbered by `signal:
  controller.signal`, so caller cancellation was silently dropped. `AbortSignal.any`
  restores it.
- rate-limits/claude-fetcher.ts (fetchViaOAuth external signal).

hosted-review-api-request.ts: `AbortSignal.timeout()` rejects with a
`TimeoutError`, not an `AbortError`, so the timeout-detection branch is updated
(otherwise `timedOut` would never be set).

minimax-fetcher.test.ts: its timeout test drove the abort with fake timers, which
cannot advance `AbortSignal.timeout`'s internal timer. Rewritten to fire the
timeout with an already-aborted signal so it genuinely exercises the abort path.

Deliberately NOT migrated:
- src/relay/git-handler.ts: the relay targets Node 18 (`build-relay.mjs`,
  MIN_NODE_MAJOR = 18); `AbortSignal.any` needs Node 20.3+, and timeout-only would
  drop the request context signal.
- ipc/feedback.ts: its timeout-driven fallback is verified with fake timers, which
  can't advance `AbortSignal.timeout`; kept on the manual pattern.
2026-07-05 23:09:14 -07:00

35 lines
1.2 KiB
TypeScript

import { normalizeGitErrorMessage } from '../../shared/git-remote-error'
import {
syncForkDefaultBranch,
type GitForkSyncExpectedUpstream,
type GitForkSyncResult
} from '../../shared/git-fork-sync'
import type { GitRuntimeOptions } from './git-runtime-options'
import { gitOptionsForWorktree } from './git-runtime-options'
import { gitExecFileAsync } from './runner'
export async function gitSyncForkDefaultBranch(
worktreePath: string,
expectedUpstream: GitForkSyncExpectedUpstream,
options: GitRuntimeOptions = {}
): Promise<GitForkSyncResult> {
// Compose the caller's cancel signal with the 60s timeout so neither is lost —
// the caller's signal was previously clobbered by the timeout controller.
const signal = options.signal
? AbortSignal.any([options.signal, AbortSignal.timeout(60_000)])
: AbortSignal.timeout(60_000)
try {
return await syncForkDefaultBranch(
(args) =>
gitExecFileAsync(args, {
...gitOptionsForWorktree(worktreePath, options),
timeout: 60_000,
signal
}),
{ expectedUpstream }
)
} catch (error) {
throw new Error(normalizeGitErrorMessage(error, 'push'))
}
}