fix(mobile): let dispose re-read the tag set once its turn comes

Queueing dispose behind each tag's own operations introduced a call the module
says it does not make: a release already in flight can give the tag back before
dispose runs, and deactivating an unheld tag is a native call whose failure would
read to the page as a lock it could not drop.

Re-reads the set when the queued action runs rather than trusting what it held
when dispose was called.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-21 03:42:41 -04:00
parent f14959def3
commit 86a298e1c1
2 changed files with 28 additions and 1 deletions
@@ -115,4 +115,26 @@ describe('a release issued while its own activate is still in flight', () => {
gated.settle('activate:orca-mobile-dictation:1:c')
await expect(first).resolves.toEqual({ active: true })
})
it('does not ask the device again for a tag a queued release already gave back', async () => {
// `dispose` queues behind the tag's own operations, so by the time it runs the release ahead
// of it may have returned the tag. Deactivating an unheld tag is a native call this module
// does not make: its failure would read to the page as a lock it could not drop.
const gated = createGatedDevice()
const server = createNativeWakelockServer(gated.device)
const activated = server.serve({ active: true, tag: 'orca-mobile-dictation:1:e' })
await settleMicrotasks()
gated.settle('activate:orca-mobile-dictation:1:e')
await expect(activated).resolves.toEqual({ active: true })
const released = server.serve({ active: false, tag: 'orca-mobile-dictation:1:e' })
await settleMicrotasks()
server.dispose()
gated.settle('deactivate:orca-mobile-dictation:1:e')
await expect(released).resolves.toEqual({ active: false })
await settleMicrotasks()
expect(gated.calls).toEqual([
'activate:orca-mobile-dictation:1:e',
'deactivate:orca-mobile-dictation:1:e'
])
})
})
+6 -1
View File
@@ -104,8 +104,13 @@ export function createNativeWakelockServer(device: WakelockDevice): NativeWakelo
// Quiet, for the reason every other dispose here is: this runs while a screen is going
// away, and a device that would not drop a tag is not something the page can be told about.
// Forgotten only on success, so one this device refused stays recorded and a later release
// still reaches it. Queued behind that tag's own operations rather than racing them.
// still reaches it. Queued behind that tag's own operations rather than racing them, and
// re-reading the set once it runs: a release already in flight may have given it back, and
// deactivating an unheld tag is a native call this module does not make.
void enqueue(tag, async () => {
if (!held.has(tag)) {
return
}
await device.deactivate(tag)
held.delete(tag)
}).catch(() => undefined)