fix(browser): make a second wake wait for the first to finish rebuilding

A wake materializes the window before it swaps the helper session and reloads
the retained address, so the page reads as resident well before it is usable.
The coalescing promise was consulted after that resident check, so a second
command targeting the same page returned immediately and ran against a blank
renderer whose session was still being torn down underneath it. Join the
in-flight wake first.

Mutation-checked: restoring the old order turns the new test red.
This commit is contained in:
Jinwoo-H
2026-08-19 19:07:12 -04:00
parent de4fef9c5a
commit 95c303bf5f
2 changed files with 39 additions and 4 deletions
@@ -499,6 +499,37 @@ describe('OffscreenBrowserBackend reclamation', () => {
})
})
it('makes a second wake wait for the first to finish rebuilding', async () => {
// Why: materialize sets the window before the session swap and the reload,
// so the page looks resident long before it is usable.
let releaseSwap = (): void => {}
const h = createHarness()
await h.backend.createTab({ url: 'https://a', browserPageId: 'a' })
h.clock.value += 120_000
await h.backend.reclaimIdlePages()
const bridge = h.bridge as unknown as { onProcessSwap: ReturnType<typeof vi.fn> }
bridge.onProcessSwap.mockImplementationOnce(
async () => new Promise<void>((resolve) => (releaseSwap = resolve))
)
const first = h.backend.wakeTab('a')
await Promise.resolve()
await Promise.resolve()
let secondSettled = false
const second = h.backend.wakeTab('a').then((value) => {
secondSettled = true
return value
})
await Promise.resolve()
await Promise.resolve()
expect(secondSettled).toBe(false)
releaseSwap()
expect(await first).toBe(true)
expect(await second).toBe(true)
})
it('reports false when waking a page it does not own', async () => {
const h = createHarness()
expect(await h.backend.wakeTab('nope')).toBe(false)
@@ -130,14 +130,18 @@ export class OffscreenBrowserBackend implements BrowserBackend {
return false
}
page.lastActivityAt = this.now()
const releasing = this.releasing.get(browserPageId)
if (!releasing && page.window && !page.window.isDestroyed()) {
return true
}
// Why: a wake materializes the window before it swaps the helper session
// and reloads the address, so the page looks resident well before it is
// usable. Join an in-flight wake first or a second command runs against a
// blank page whose session is still being torn down underneath it.
const inFlight = this.waking.get(browserPageId)
if (inFlight) {
return inFlight
}
const releasing = this.releasing.get(browserPageId)
if (!releasing && page.window && !page.window.isDestroyed()) {
return true
}
const wake = (async (): Promise<boolean> => {
await releasing
if (!this.pages.has(browserPageId)) {