Files
orca/src/main/ipc/worktree-git-common-narrow-watch.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

274 lines
8.6 KiB
TypeScript

import { stat } from 'node:fs/promises'
import { join } from 'node:path'
import { subscribeViaWatcherProcess } from './parcel-watcher-process'
import { isWatcherProcessFailure } from './parcel-watcher-process-failure'
import type { WorktreeBaseWatchTarget } from './worktree-base-directory-event-filter'
import type {
WorktreeBasePollEvent,
WorktreeBaseSubscription,
WorktreePollerWindowVisibility
} from './worktree-base-directory-poller'
import { createSingleFlight } from './single-flight-promise'
import { createGitCommonWatchReconciliation } from './worktree-git-common-watch-reconciliation'
import { startGitCommonPolling } from './worktree-git-common-polling'
// The native stream is hosted in the crash-isolated watcher child, never the
// Electron main process: watcher.node teardown races heap-corrupt the hosting
// process when unsubscribe overlaps in-flight callbacks (issue #8732), and
// root deletion via `git worktree prune` makes that overlap routine here.
export async function startGitCommonNarrowWatch(
target: WorktreeBaseWatchTarget,
onEvents: (events: WorktreeBasePollEvent[]) => void,
pollIntervalMs: number,
platform: NodeJS.Platform,
visibility: WorktreePollerWindowVisibility,
onFullScan?: () => void,
onWatchError?: (error: Error) => void
): Promise<WorktreeBaseSubscription> {
const worktreesDir = join(target.path, 'worktrees')
const watcherOptions = platform === 'win32' ? { backend: 'windows' as const } : {}
let disposed = false
let subscription: WorktreeBaseSubscription | null = null
let existenceTimer: ReturnType<typeof setInterval> | null = null
const pollingFallback = createSingleFlight()
let subscribing = false
let parkedWhileHidden = false
let usingPollingFallback = false
let nativeSubscriptionGeneration = 0
const reconciliation = createGitCommonWatchReconciliation({
commonDirPath: target.path,
pollIntervalMs,
visibility,
canStart: () => !disposed && !usingPollingFallback && subscription !== null,
shouldKeep: () => !disposed && !usingPollingFallback,
onRootReplacement: () => {
nativeSubscriptionGeneration++
const current = subscription
subscription = null
if (current) {
void current.unsubscribe().catch(() => {})
}
armExistencePoll()
},
onEvents
})
const stopExistencePoll = (): void => {
if (existenceTimer) {
clearInterval(existenceTimer)
existenceTimer = null
}
}
const shouldUsePollingFallback = (error: unknown): boolean =>
isWatcherProcessFailure(error) &&
(error.code === 'supervisor_crash_fuse' || error.code === 'process_unavailable')
const ensurePollingFallback = (): Promise<void> =>
pollingFallback.run(() => {
stopExistencePoll()
usingPollingFallback = true
return reconciliation
.unsubscribe()
.catch(() => {})
.then(() =>
startGitCommonPolling(
target.path,
onEvents,
pollIntervalMs,
visibility,
onFullScan,
false
)
)
.then(async (fallback) => {
if (disposed || subscription) {
await fallback.unsubscribe()
return
}
subscription = fallback
})
})
const tryUpgradeToNarrowWatch = async (): Promise<void> => {
if (disposed || subscribing || subscription) {
return
}
subscribing = true
try {
const installed = await trySubscribe()
if (installed && !disposed) {
stopExistencePoll()
await reconciliation.ensureStarted()
// The dir appearing means a first linked worktree was just
// registered; surface it so the repo's worktree list refreshes.
onEvents([{ type: 'create', path: worktreesDir }])
}
} finally {
subscribing = false
}
}
const armExistencePoll = (): void => {
if (disposed || existenceTimer || subscription) {
return
}
if (!visibility.isWindowVisible()) {
parkedWhileHidden = true
return
}
existenceTimer = setInterval(() => {
if (disposed) {
return
}
if (!visibility.isWindowVisible()) {
parkedWhileHidden = true
stopExistencePoll()
return
}
void tryUpgradeToNarrowWatch()
}, pollIntervalMs)
existenceTimer.unref?.()
}
const unsubscribeVisibility = visibility.onWindowBecameVisible(() => {
if (!disposed && parkedWhileHidden) {
parkedWhileHidden = false
void tryUpgradeToNarrowWatch().finally(() => {
armExistencePoll()
})
}
reconciliation.notifyWindowBecameVisible()
})
const trySubscribe = async (): Promise<boolean> => {
try {
const s = await stat(worktreesDir)
if (!s.isDirectory()) {
return false
}
} catch {
return false
}
const generation = ++nativeSubscriptionGeneration
let errored = false
let active = true
// Why: parcel tears its native stream down when the watched root is
// deleted (e.g. `git worktree prune` removing an empty worktrees dir) —
// sometimes surfaced as an error, sometimes as a delete event for the
// root. Either way: notify, drop the dead stream, and let the existence
// poll re-arm when a future worktree add recreates the dir.
const teardown = (): void => {
active = false
errored = true
if (generation === nativeSubscriptionGeneration) {
nativeSubscriptionGeneration++
}
const current = subscription
subscription = null
if (current) {
void current.unsubscribe().catch(() => {})
}
}
const teardownAndRearm = (): void => {
teardown()
armExistencePoll()
}
try {
const sub = await subscribeViaWatcherProcess(
worktreesDir,
(error, events) => {
if (disposed || !active || generation !== nativeSubscriptionGeneration) {
return
}
if (error) {
if (onWatchError) {
onWatchError(error)
} else {
onEvents([{ type: 'update', path: worktreesDir }])
}
if (shouldUsePollingFallback(error)) {
teardown()
void ensurePollingFallback().catch(() => {
if (!disposed) {
armExistencePoll()
}
})
} else {
teardownAndRearm()
}
return
}
if (events.length > 0) {
const rootGone = events.some(
(event) => event.type === 'delete' && event.path === worktreesDir
)
onEvents(events.map((event) => ({ type: event.type, path: event.path })))
if (rootGone) {
teardownAndRearm()
}
}
},
watcherOptions,
{
// Why: a watcher-child crash drops events during the automatic
// resubscribe gap; report a structural change so worktrees re-sync.
onInterruption: () => {
if (!disposed && active && generation === nativeSubscriptionGeneration) {
if (onWatchError) {
onWatchError(new Error('Git common watcher interrupted'))
} else {
onEvents([{ type: 'update', path: worktreesDir }])
}
}
}
}
)
if (generation !== nativeSubscriptionGeneration) {
void sub.unsubscribe().catch(() => {})
return false
}
if (disposed || errored) {
void sub.unsubscribe().catch(() => {})
await pollingFallback.pending()?.catch(() => {})
return !errored || subscription !== null
}
subscription = { unsubscribe: () => sub.unsubscribe() }
return true
} catch (error) {
if (disposed || generation !== nativeSubscriptionGeneration) {
return false
}
if (shouldUsePollingFallback(error)) {
await ensurePollingFallback()
return subscription !== null
}
return false
}
}
if (!(await trySubscribe())) {
// Why: repos commonly start without linked worktrees; retrying the narrow
// subscription lets macOS upgrade to native events when the directory appears.
armExistencePoll()
}
await reconciliation.ensureStarted()
return {
unsubscribe: async () => {
disposed = true
stopExistencePoll()
unsubscribeVisibility()
await pollingFallback.pending()?.catch(() => {})
nativeSubscriptionGeneration++
const current = subscription
subscription = null
await Promise.all([
current?.unsubscribe().catch(() => {}),
reconciliation.unsubscribe().catch(() => {})
])
}
}
}