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:
@@ -1076,7 +1076,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
},
|
||||
|
||||
async listDirectory(request) {
|
||||
async listDirectory(request, signal) {
|
||||
const capability = ctx.directoryPicker.capability()
|
||||
if (capability.kind !== 'browse') {
|
||||
return err(request, {
|
||||
@@ -1086,7 +1086,9 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
})
|
||||
}
|
||||
try {
|
||||
return ok(request, await capability.list(request.payload.path))
|
||||
// The carrier's signal follows the caller: a disconnect or timeout
|
||||
// stops the backend's directory scan instead of outliving it.
|
||||
return ok(request, await capability.list(request.payload.path, signal))
|
||||
} catch (error: unknown) {
|
||||
return err(request, directoryError(error))
|
||||
}
|
||||
|
||||
@@ -62,10 +62,13 @@ export interface HostApi {
|
||||
/**
|
||||
* List one directory level for the in-app browser; an absent path lists the
|
||||
* host account's home directory. Only served under the `browse` capability;
|
||||
* unreadable or missing targets fail with `directory-unreadable`.
|
||||
* unreadable or missing targets fail with `directory-unreadable`. The
|
||||
* carrier's request signal follows the caller, stopping the backend's scan
|
||||
* on disconnect or timeout.
|
||||
*/
|
||||
listDirectory(
|
||||
request: RpcRequest<{ path?: string }>,
|
||||
signal: AbortSignal,
|
||||
): Promise<RpcResponse<DirectoryListing>>
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,7 +64,7 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'session.cancel': { schema: sessionCancelRequestSchema, invoke: (api, r) => api.sessions.cancel(r) },
|
||||
'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },
|
||||
'host.pickDirectory': { schema: hostPickDirectoryRequestSchema, invoke: (api, r, signal) => api.host.pickDirectory(r, signal) },
|
||||
'host.listDirectory': { schema: hostListDirectoryRequestSchema, invoke: (api, r) => api.host.listDirectory(r) },
|
||||
'host.listDirectory': { schema: hostListDirectoryRequestSchema, invoke: (api, r, signal) => api.host.listDirectory(r, signal) },
|
||||
'host.createDirectory': { schema: hostCreateDirectoryRequestSchema, invoke: (api, r) => api.host.createDirectory(r) },
|
||||
'host.openPath': { schema: hostOpenPathRequestSchema, invoke: (api, r, signal) => api.host.openPath(r, signal) },
|
||||
'workspace.list': { schema: workspaceListRequestSchema, invoke: (api, r) => api.workspace.list(r) },
|
||||
|
||||
@@ -172,9 +172,9 @@ const BROWSE_STUB: DirectoryPickerCapability = {
|
||||
describe('host.listDirectory / host.createDirectory', () => {
|
||||
it('serves listings and creation through the browse capability, defaulting to home', async () => {
|
||||
const { api } = await harness(undefined, BROWSE_STUB)
|
||||
const home = await api.host.listDirectory(request({}))
|
||||
const home = await api.host.listDirectory(request({}), new AbortController().signal)
|
||||
expect(home.result).toMatchObject({ ok: true, value: { path: '/home/user', home: '/home/user' } })
|
||||
const listed = await api.host.listDirectory(request({ path: '/home/user/projects' }))
|
||||
const listed = await api.host.listDirectory(request({ path: '/home/user/projects' }), new AbortController().signal)
|
||||
expect(listed.result).toMatchObject({ ok: true, value: { path: '/home/user/projects' } })
|
||||
const created = await api.host.createDirectory(request({ path: '/home/user', name: 'fresh' }))
|
||||
expect(created.result).toEqual({ ok: true, value: { path: '/home/user/fresh' } })
|
||||
@@ -182,7 +182,7 @@ describe('host.listDirectory / host.createDirectory', () => {
|
||||
|
||||
it('maps typed picker failures onto the wire error codes and folds unknown throws to internal', async () => {
|
||||
const { api } = await harness(undefined, BROWSE_STUB)
|
||||
expect((await api.host.listDirectory(request({ path: '/denied' }))).result).toMatchObject({
|
||||
expect((await api.host.listDirectory(request({ path: '/denied' }), new AbortController().signal)).result).toMatchObject({
|
||||
ok: false, error: { code: 'directory-unreadable', details: { path: '/denied' } },
|
||||
})
|
||||
expect((await api.host.createDirectory(request({ path: '/home/user', name: 'taken' }))).result).toMatchObject({
|
||||
@@ -195,7 +195,7 @@ describe('host.listDirectory / host.createDirectory', () => {
|
||||
|
||||
it('refuses the browse RPCs under a native composition', async () => {
|
||||
const { api } = await harness()
|
||||
expect((await api.host.listDirectory(request({}))).result).toMatchObject({
|
||||
expect((await api.host.listDirectory(request({}), new AbortController().signal)).result).toMatchObject({
|
||||
ok: false, error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
|
||||
})
|
||||
expect((await api.host.createDirectory(request({ path: '/x', name: 'y' }))).result).toMatchObject({
|
||||
|
||||
Reference in New Issue
Block a user