fix(host): cancellable listing scans and O(log window) insertion

capability.list gains an optional AbortSignal threaded from the RPC
carrier's request signal (the pickDirectory pattern): a disconnected or
timed-out caller stops the opendir loop instead of the scan outliving
its caller, and the abort surfaces as its own reason rather than a
directory-unreadable dressing. boundedInsert rejects a full window's
at-or-beyond-tail candidate on one comparison and binary-inserts
retained candidates, so an oversized level no longer pays a window scan
per dirent.
This commit is contained in:
creatixchu
2026-07-29 04:42:13 +08:00
parent da970ea269
commit 7503390590
17 changed files with 68 additions and 27 deletions
@@ -83,6 +83,19 @@ describe('BrowseDirectoryPicker', () => {
}
})
it('stops the scan with the caller: an aborted signal rejects with its own reason', async () => {
const gone = new AbortController()
gone.abort(new Error('caller left'))
// The abort surfaces as-is, not dressed as an unreadable directory.
await expect(capability.list(root, gone.signal)).rejects.toThrow('caller left')
// A live signal changes nothing about ordinary failures.
const live = new AbortController()
const missing = join(root, 'no-such-dir')
const failure = await capability.list(missing, live.signal).catch((error: unknown) => error)
expect(failure).toBeInstanceOf(DirectoryPickerError)
expect((failure as DirectoryPickerError).code).toBe('directory-unreadable')
})
it('boundedInsert keeps the window name-sorted and bounded, reporting evictions', () => {
const candidate = (name: string): ListingCandidate => ({ name, isDirectory: true, isSymbolicLink: false })
const window: ListingCandidate[] = []
@@ -91,9 +104,11 @@ describe('BrowseDirectoryPicker', () => {
// A smaller name lands in place and pushes the current largest out.
expect(boundedInsert(window, candidate('a'), 2)).toBe(true)
expect(window.map(entry => entry.name)).toEqual(['a', 'm'])
// A name beyond the window's tail enters last and leaves immediately.
// A name at or beyond the full window's tail rejects on one comparison.
expect(boundedInsert(window, candidate('t'), 2)).toBe(true)
expect(window.map(entry => entry.name)).toEqual(['a', 'm'])
expect(boundedInsert(window, candidate('m'), 2)).toBe(true)
expect(window.map(entry => entry.name)).toEqual(['a', 'm'])
})
it('reports the ancestry as jump-target crumbs ending at the listed directory', async () => {