From 0cba706b01e2e0fef620893d441e272cdac7894e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:33:19 -0700 Subject: [PATCH] fix(ports): coalesce advertised URL refresh bursts (#19150) --- .../ports/WorkspacePortScanner.test.tsx | 110 ++++++++++++++++++ .../components/ports/WorkspacePortScanner.tsx | 14 ++- 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/ports/WorkspacePortScanner.test.tsx b/src/renderer/src/components/ports/WorkspacePortScanner.test.tsx index 0ae53fee931..7741368d025 100644 --- a/src/renderer/src/components/ports/WorkspacePortScanner.test.tsx +++ b/src/renderer/src/components/ports/WorkspacePortScanner.test.tsx @@ -602,3 +602,113 @@ describe('WorkspacePortScanner', () => { expect(getPublishedRemoteWorktreePorts()).toBeUndefined() }) }) + +describe('advertised URL refresh bursts', () => { + async function mountLocalScanner(): Promise<() => void> { + useAppStore.setState({ settings: getDefaultSettings('/tmp/orca-workspaces') }) + await act(async () => { + root?.render() + await flushPromises() + }) + localScan.mockClear() + return vi.mocked(window.api.workspacePorts.onAdvertisedUrlChanged).mock + .calls[0][0] as () => void + } + + it('coalesces sequential URL changes into one immediate scan and one settled scan', async () => { + const changed = await mountLocalScanner() + for (let index = 0; index < 5; index++) { + await act(async () => { + changed() + await flushPromises() + await vi.advanceTimersByTimeAsync(100) + }) + } + expect(localScan).toHaveBeenCalledTimes(1) + await act(async () => { + await vi.advanceTimersByTimeAsync(1_000) + }) + expect(localScan).toHaveBeenCalledTimes(2) + await act(async () => { + changed() + await flushPromises() + }) + expect(localScan).toHaveBeenCalledTimes(3) + }) + + it('cancels the settled scan on unmount', async () => { + const changed = await mountLocalScanner() + await act(async () => { + changed() + await flushPromises() + }) + act(() => root?.unmount()) + root = null + await vi.advanceTimersByTimeAsync(2_000) + expect(localScan).toHaveBeenCalledTimes(1) + }) + + it('skips the settled scan while hidden and accepts the next visible URL change', async () => { + let visibility: DocumentVisibilityState = 'visible' + const restore = overrideDocumentVisibilityState(() => visibility) + try { + const changed = await mountLocalScanner() + await act(async () => { + changed() + await flushPromises() + }) + visibility = 'hidden' + await act(async () => { + await vi.advanceTimersByTimeAsync(2_000) + }) + expect(localScan).toHaveBeenCalledTimes(1) + visibility = 'visible' + await act(async () => { + changed() + await flushPromises() + }) + expect(localScan).toHaveBeenCalledTimes(2) + } finally { + restore() + } + }) +}) + +it('releases the URL burst when its leading scan finishes while hidden', async () => { + let visibility: DocumentVisibilityState = 'visible' + const restore = overrideDocumentVisibilityState(() => visibility) + try { + useAppStore.setState({ settings: getDefaultSettings('/tmp/orca-workspaces') }) + await act(async () => { + root?.render() + await flushPromises() + }) + const changed = vi.mocked(window.api.workspacePorts.onAdvertisedUrlChanged).mock + .calls[0][0] as () => void + let finish!: (scan: WorkspacePortScanResult) => void + localScan.mockClear() + localScan.mockImplementationOnce( + () => + new Promise((resolve) => { + finish = resolve + }) + ) + await act(async () => { + changed() + await flushPromises() + }) + visibility = 'hidden' + await act(async () => { + finish(emptyScan) + await flushPromises() + }) + visibility = 'visible' + await act(async () => { + changed() + await flushPromises() + }) + expect(localScan).toHaveBeenCalledTimes(2) + } finally { + restore() + } +}) diff --git a/src/renderer/src/components/ports/WorkspacePortScanner.tsx b/src/renderer/src/components/ports/WorkspacePortScanner.tsx index 2b12bd86cd8..f8f2d11ee40 100644 --- a/src/renderer/src/components/ports/WorkspacePortScanner.tsx +++ b/src/renderer/src/components/ports/WorkspacePortScanner.tsx @@ -280,6 +280,7 @@ export function WorkspacePortScanner({ enabled = true }: { enabled?: boolean }): return } + let burstRefresh: Promise | null = null let eventSequence = 0 let disposed = false let retryTimer: ReturnType | null = null @@ -296,15 +297,24 @@ export function WorkspacePortScanner({ enabled = true }: { enabled?: boolean }): const sequence = eventSequence clearRetryTimer() if (!isWindowVisible()) { + burstRefresh = null return } - void refresh({ force: true, targets: [runtimeTarget] }).finally(() => { - if (disposed || sequence !== eventSequence || !isWindowVisible()) { + // Keep the leading scan through the quiet window so sequential events share it too. + burstRefresh ??= refresh({ force: true, targets: [runtimeTarget] }) + void burstRefresh.finally(() => { + if (disposed || sequence !== eventSequence) { + return + } + if (!isWindowVisible()) { + burstRefresh = null return } // Why: some dev servers print their URL just before the listener is // visible to lsof/netstat. One quiet settle scan catches that startup race. retryTimer = setTimeout(() => { + retryTimer = null + burstRefresh = null if (disposed || sequence !== eventSequence || !isWindowVisible()) { return }