Clean up CDP proxy client listeners

This commit is contained in:
Neil 2026-05-30 12:51:11 -07:00 committed by GitHub
parent 43714a2238
commit dffd1ac180
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 10 deletions

View File

@ -330,6 +330,28 @@ describe('CdpWsProxy', () => {
})
})
it('detaches client websocket listeners after client close', async () => {
const client = await connect()
const serverClient = (proxy as unknown as { client: WebSocket | null }).client
expect(serverClient).toBeTruthy()
const offSpy = vi.spyOn(serverClient!, 'off')
client.close()
const start = Date.now()
while (
(proxy as unknown as { client: WebSocket | null }).client &&
Date.now() - start < 2_000
) {
await new Promise((resolve) => setTimeout(resolve, 20))
}
expect((proxy as unknown as { client: WebSocket | null }).client).toBeNull()
const removedEvents = offSpy.mock.calls.map(([event]) => event)
expect(removedEvents).toEqual(expect.arrayContaining(['message', 'close']))
offSpy.mockRestore()
})
it('rejects inflight requests on stop', async () => {
let resolveCommand: (v: unknown) => void
mock.webContents.debugger.sendCommand.mockImplementation(

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines -- Why: this proxy owns HTTP discovery, websocket client lifecycle, and CDP debugger forwarding together. */
import { WebSocketServer, WebSocket } from 'ws'
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'http'
import type { WebContents } from 'electron'
@ -9,6 +10,7 @@ export class CdpWsProxy {
private httpServer: Server | null = null
private wss: WebSocketServer | null = null
private client: WebSocket | null = null
private detachClientListeners: (() => void) | null = null
private port = 0
private debuggerMessageHandler: ((...args: unknown[]) => void) | null = null
private debuggerDetachHandler: ((...args: unknown[]) => void) | null = null
@ -39,16 +41,27 @@ export class CdpWsProxy {
failStart(error)
}
this.wss.on('connection', (ws) => {
if (this.client) {
this.client.close()
}
this.closeClient()
this.client = ws
ws.on('message', (data) => this.handleClientMessage(ws, data.toString()))
ws.on('close', () => {
const onMessage = (data: WebSocket.RawData): void => {
this.handleClientMessage(ws, data.toString())
}
const onClose = (): void => {
detach()
if (this.client === ws) {
this.client = null
}
})
}
const detach = (): void => {
ws.off('message', onMessage)
ws.off('close', onClose)
if (this.detachClientListeners === detach) {
this.detachClientListeners = null
}
}
this.detachClientListeners = detach
ws.on('message', onMessage)
ws.on('close', onClose)
})
this.httpServer.listen(0, '127.0.0.1', () => {
this.httpServer?.removeListener('error', onListenError)
@ -66,10 +79,7 @@ export class CdpWsProxy {
async stop(): Promise<void> {
this.detachDebugger()
if (this.client) {
this.client.close()
this.client = null
}
this.closeClient()
if (this.wss) {
this.wss.close()
this.wss = null
@ -84,6 +94,14 @@ export class CdpWsProxy {
return this.port
}
private closeClient(): void {
const client = this.client
this.detachClientListeners?.()
this.detachClientListeners = null
this.client = null
client?.close()
}
private send(payload: unknown, client = this.client): void {
if (client?.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(payload))