diff --git a/src/main/browser/offscreen-browser-backend-reclaim.test.ts b/src/main/browser/offscreen-browser-backend-reclaim.test.ts index 6bf12e15ea4..69122f18b5d 100644 --- a/src/main/browser/offscreen-browser-backend-reclaim.test.ts +++ b/src/main/browser/offscreen-browser-backend-reclaim.test.ts @@ -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 } + bridge.onProcessSwap.mockImplementationOnce( + async () => new Promise((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) diff --git a/src/main/browser/offscreen-browser-backend.ts b/src/main/browser/offscreen-browser-backend.ts index 206dcc0eadf..271a7ea1cb1 100644 --- a/src/main/browser/offscreen-browser-backend.ts +++ b/src/main/browser/offscreen-browser-backend.ts @@ -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 => { await releasing if (!this.pages.has(browserPageId)) {