Files
orca/src/preload/runtime-environment-subscriptions.ts
T
Neil 661372d514 Cancel abandoned file searches and prevent stale results (#25370)
* Bound relay git-grep records and clean up capacity failures

Credits @OrcaWin for the original bounded-record proposal.

* Bound filesystem listing and transfer metadata at the execution host

Apply mobile limits before transport, list Markdown through its semantic producer, and retain complete directory results only within explicit capacity budgets. Stream SFTP directory packets and reject oversized transfer plans before reporting success; preserve narrow older-peer fallbacks and validate streamed response retention.

* Release inactive Markdown candidates and support folder scopes

Keep one current document snapshot per consumer and attach completion candidates to the actual editor model lifetime. Resolve folder workspace roots through existing workspace identities so their previews and completions receive the same authorized listing as worktrees.

* Preserve full runtime inventories under aggregate byte budgets

Leave unqualified inventories complete beyond 20,000 files. Charge retained paths and serialized bytes at local and relay producers, reject oversized full inventories explicitly, and bound old-peer response reassembly; keep caller-specific mobile and explicit result limits.

* Bound SFTP directory handle cleanup waits

Send CLOSE even after cancellation, stop waiting after five seconds without an acknowledgement, and ignore late callbacks. Preserve the original capacity or cancellation error and close handles returned after an aborted OPENDIR.

* Preserve registered root spelling in Markdown document paths

* Reject incomplete SFTP directory cleanup and final cancellation

* Bound pending response bytes before stream ownership arrives

* Preserve bounded directory reads on legacy SSH hosts

* Allow bounded per-chunk padding at response byte boundaries

* Bound response payload retention after stream metadata

* Bound complete legacy Quick Open inventories and cache retention by bytes

* Localize Markdown document listing fallback

* Exercise relay search decoding with real byte streams

* Preserve concrete relay stream fixture types

* test: compare distinct bundled ripgrep platforms on Windows

* Preserve Markdown discovery across Windows and legacy SSH hosts

* Make WSL process termination available to the bounds layer

* Cancel abandoned file searches and preserve search keyboard navigation

* Invalidate searches when workspace roots or owners change

* Cover root replacement and legacy search cancellation together

* Exercise search cancellation through the current desktop access seam

* Keep search ownership and cancellation contracts current

* Share local text-search execution and close canceled subscriptions
2026-10-05 21:46:37 -07:00

171 lines
6.1 KiB
TypeScript

import type { RuntimeRpcResponse } from '../shared/runtime-rpc-envelope'
type RuntimeEnvironmentSubscribeArgs = {
subscriptionId?: string
selector: string
method: string
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedEnvironmentRuntimeId?: string
}
type RuntimeEnvironmentSubscriptionCallbacks = {
onResponse: (response: RuntimeRpcResponse<unknown>) => void
onBinary?: (bytes: Uint8Array<ArrayBufferLike>) => void
onError?: (error: { code: string; message: string }) => void
onClose?: () => void
}
export type RuntimeEnvironmentSubscriptionHandle = {
unsubscribe: () => void
sendBinary: (bytes: Uint8Array<ArrayBufferLike>) => void
}
type RuntimeEnvironmentSubscriptionEvent =
| { subscriptionId: string; type: 'response'; response: RuntimeRpcResponse<unknown> }
| { subscriptionId: string; type: 'binary'; bytes: Uint8Array<ArrayBufferLike> }
| { subscriptionId: string; type: 'error'; code: string; message: string }
| { subscriptionId: string; type: 'close' }
type RuntimeEnvironmentSubscriptionIpc = {
invoke: (channel: string, args: unknown) => Promise<unknown>
send: (channel: string, args: unknown) => void
on: (
channel: string,
listener: (event: unknown, payload: RuntimeEnvironmentSubscriptionEvent) => void
) => void
removeListener: (
channel: string,
listener: (event: unknown, payload: RuntimeEnvironmentSubscriptionEvent) => void
) => void
}
const SUBSCRIPTION_EVENT_CHANNEL = 'runtimeEnvironments:subscriptionEvent'
// Why: each subscribe() previously attached its own channel listener that the
// 'error' branch never detached, so reconnecting shared-control subscriptions
// leaked listener closures that pinned renderer state. One active dispatcher per
// ipc instance keeps listener retention O(1) while per-subscription state lives
// only in this map.
type RuntimeEnvironmentSubscriptionDispatcher = {
callbacks: Map<string, RuntimeEnvironmentSubscriptionCallbacks>
listener: (event: unknown, payload: RuntimeEnvironmentSubscriptionEvent) => void
}
const subscriptionDispatchers = new WeakMap<
RuntimeEnvironmentSubscriptionIpc,
RuntimeEnvironmentSubscriptionDispatcher
>()
function releaseIdleDispatcher(
ipc: RuntimeEnvironmentSubscriptionIpc,
callbacks: Map<string, RuntimeEnvironmentSubscriptionCallbacks>,
listener: RuntimeEnvironmentSubscriptionDispatcher['listener']
): void {
if (callbacks.size > 0) {
return
}
ipc.removeListener(SUBSCRIPTION_EVENT_CHANNEL, listener)
subscriptionDispatchers.delete(ipc)
}
function releaseSubscription(
ipc: RuntimeEnvironmentSubscriptionIpc,
dispatcher: RuntimeEnvironmentSubscriptionDispatcher,
subscriptionId: string
): void {
if (!dispatcher.callbacks.delete(subscriptionId)) {
return
}
releaseIdleDispatcher(ipc, dispatcher.callbacks, dispatcher.listener)
}
function getOrCreateDispatcher(
ipc: RuntimeEnvironmentSubscriptionIpc
): RuntimeEnvironmentSubscriptionDispatcher {
const existing = subscriptionDispatchers.get(ipc)
if (existing) {
return existing
}
const callbacks = new Map<string, RuntimeEnvironmentSubscriptionCallbacks>()
const listener = (_event: unknown, event: RuntimeEnvironmentSubscriptionEvent): void => {
const subscriptionCallbacks = callbacks.get(event.subscriptionId)
if (!subscriptionCallbacks) {
return
}
if (event.type === 'response') {
subscriptionCallbacks.onResponse(event.response)
} else if (event.type === 'binary') {
subscriptionCallbacks.onBinary?.(event.bytes)
} else if (event.type === 'error') {
// Why: errors are non-terminal for shared-control subscriptions (they
// survive reconnects), so the entry stays mapped until the consumer
// unsubscribes.
subscriptionCallbacks.onError?.({ code: event.code, message: event.message })
} else {
// Why: close is terminal; release before the callback so a throwing or
// re-entrant onClose cannot keep renderer state mapped.
callbacks.delete(event.subscriptionId)
releaseIdleDispatcher(ipc, callbacks, listener)
subscriptionCallbacks.onClose?.()
}
}
const dispatcher: RuntimeEnvironmentSubscriptionDispatcher = { callbacks, listener }
subscriptionDispatchers.set(ipc, dispatcher)
ipc.on(SUBSCRIPTION_EVENT_CHANNEL, listener)
return dispatcher
}
function createRuntimeEnvironmentSubscriptionId(): string {
const randomUuid = globalThis.crypto?.randomUUID
if (typeof randomUuid === 'function') {
return randomUuid.call(globalThis.crypto)
}
return `sub-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
export async function subscribeRuntimeEnvironmentFromPreload(
ipc: RuntimeEnvironmentSubscriptionIpc,
args: RuntimeEnvironmentSubscribeArgs,
callbacks: RuntimeEnvironmentSubscriptionCallbacks,
createSubscriptionId = createRuntimeEnvironmentSubscriptionId
): Promise<RuntimeEnvironmentSubscriptionHandle> {
const subscriptionId = args.subscriptionId ?? createSubscriptionId()
// Why: streaming RPCs can emit their first frame before ipcMain.handle()
// resolves, so the dispatcher must be routing this id before invoking.
const dispatcher = getOrCreateDispatcher(ipc)
if (dispatcher.callbacks.has(subscriptionId)) {
throw new Error('Runtime environment subscription id already exists')
}
dispatcher.callbacks.set(subscriptionId, callbacks)
const releaseCurrentSubscription = (): void => {
if (dispatcher.callbacks.get(subscriptionId) === callbacks) {
releaseSubscription(ipc, dispatcher, subscriptionId)
}
}
try {
const result = (await ipc.invoke('runtimeEnvironments:subscribe', {
...args,
subscriptionId
})) as { subscriptionId: string; requestId: string }
if (result.subscriptionId !== subscriptionId) {
releaseCurrentSubscription()
throw new Error('Runtime environment subscription id mismatch')
}
} catch (error) {
releaseCurrentSubscription()
throw error
}
return {
unsubscribe: () => {
releaseCurrentSubscription()
void ipc.invoke('runtimeEnvironments:unsubscribe', { subscriptionId })
},
sendBinary: (bytes) => {
ipc.send('runtimeEnvironments:subscriptionBinary', { subscriptionId, bytes })
}
}
}