From 1236729b24dde7f50f6c270f9aa87f2116a55cf0 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 29 May 2026 02:00:53 -0700 Subject: [PATCH] fix: abort relay requests on dispatcher dispose --- src/relay/client-request-aborts.ts | 7 +++++++ src/relay/dispatcher.test.ts | 22 ++++++++++++++++++++++ src/relay/dispatcher.ts | 3 +++ 3 files changed, 32 insertions(+) diff --git a/src/relay/client-request-aborts.ts b/src/relay/client-request-aborts.ts index 636be796c..12d44363d 100644 --- a/src/relay/client-request-aborts.ts +++ b/src/relay/client-request-aborts.ts @@ -27,6 +27,13 @@ export class ClientRequestAborts { } } + abortAll(): void { + for (const [, controller] of this.controllers) { + controller.abort() + } + this.controllers.clear() + } + private key(clientId: number, requestId: number): string { return `${clientId}:${requestId}` } diff --git a/src/relay/dispatcher.test.ts b/src/relay/dispatcher.test.ts index 3e76c7055..5ca845be3 100644 --- a/src/relay/dispatcher.test.ts +++ b/src/relay/dispatcher.test.ts @@ -310,6 +310,28 @@ describe('RelayDispatcher', () => { await vi.advanceTimersByTimeAsync(0) }) + it('aborts in-flight request contexts on dispose', async () => { + let observedSignal: AbortSignal | undefined + let resolveHandler!: () => void + dispatcher.onRequest( + 'slow.method', + (_params, context) => + new Promise((resolve) => { + observedSignal = context.signal + resolveHandler = () => resolve(null) + }) + ) + + const req: JsonRpcRequest = { jsonrpc: '2.0', id: 101, method: 'slow.method' } + dispatcher.feed(encodeJsonRpcFrame(req, 1, 0)) + await vi.advanceTimersByTimeAsync(0) + dispatcher.dispose() + + expect(observedSignal?.aborted).toBe(true) + resolveHandler() + await vi.advanceTimersByTimeAsync(0) + }) + it('notifies listeners when the primary client is invalidated', () => { const listener = vi.fn() dispatcher.onClientDetached(listener) diff --git a/src/relay/dispatcher.ts b/src/relay/dispatcher.ts index b1ce857c2..e34928513 100644 --- a/src/relay/dispatcher.ts +++ b/src/relay/dispatcher.ts @@ -162,6 +162,9 @@ export class RelayDispatcher { clearInterval(this.keepaliveTimer) this.keepaliveTimer = null } + // Why: dispose means this relay instance cannot send responses anymore; + // abort in-flight request work so stale SSH-side scans/watchers release. + this.requestAborts.abortAll() } private createClient(write: (data: Buffer) => void): RelayClient {