mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
fix(browser): stop an abandoned wake from clearing a replacement page's lock
Round five. Lifecycle came back clean; one finding left. A wake cleared its entry from the waking map unconditionally. An abandoned wake can outlive a close and the replacement page's own wake, so that cleanup could drop the replacement's lock — and the next command would then see a live window with no wake in flight and drive a renderer still being rebuilt, defeating the coalescing fix from the round-three pass. Clear the entry only when it is still the one this wake installed, matching how releases already do it. Mutation-checked.
This commit is contained in:
@@ -552,6 +552,52 @@ describe('OffscreenBrowserBackend reclamation', () => {
|
||||
expect(await second).toBe(true)
|
||||
})
|
||||
|
||||
it('does not let an abandoned wake clear a replacement page wake lock', async () => {
|
||||
// Why: dropping the replacement's lock would let the next command see a
|
||||
// live window with no wake in flight and drive a half-rebuilt renderer.
|
||||
let releaseFirstSwap = (): void => {}
|
||||
let releaseSecondSwap = (): 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) => (releaseFirstSwap = resolve))
|
||||
)
|
||||
const abandoned = h.backend.wakeTab('a')
|
||||
await Promise.resolve()
|
||||
|
||||
await h.backend.closeTab('a')
|
||||
await h.backend.createTab({ url: 'https://replacement', browserPageId: 'a' })
|
||||
h.clock.value += 120_000
|
||||
await h.backend.reclaimIdlePages()
|
||||
|
||||
bridge.onProcessSwap.mockImplementationOnce(
|
||||
async () => new Promise<void>((resolve) => (releaseSecondSwap = resolve))
|
||||
)
|
||||
const replacementWake = h.backend.wakeTab('a')
|
||||
await Promise.resolve()
|
||||
|
||||
// The old wake finishing must not release the replacement's lock.
|
||||
releaseFirstSwap()
|
||||
expect(await abandoned).toBe(false)
|
||||
|
||||
let thirdSettled = false
|
||||
const third = h.backend.wakeTab('a').then((value) => {
|
||||
thirdSettled = true
|
||||
return value
|
||||
})
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(thirdSettled).toBe(false)
|
||||
|
||||
releaseSecondSwap()
|
||||
expect(await replacementWake).toBe(true)
|
||||
expect(await third).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)
|
||||
|
||||
@@ -181,7 +181,13 @@ export class OffscreenBrowserBackend implements BrowserBackend {
|
||||
try {
|
||||
return await wake
|
||||
} finally {
|
||||
this.waking.delete(browserPageId)
|
||||
// Why: an abandoned wake can outlive a close and a replacement page's own
|
||||
// wake. Clearing the entry blindly would drop the replacement's lock, and
|
||||
// the next command would see a live window with no wake in flight and
|
||||
// drive a renderer that is still being rebuilt.
|
||||
if (this.waking.get(browserPageId) === wake) {
|
||||
this.waking.delete(browserPageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user