From 86a298e1c1251dc66bf7f17efcc63a0a8d0cf397 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 21 Sep 2026 03:42:41 -0400 Subject: [PATCH] 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 --- mobile/src/platform/native-wakelock.test.ts | 22 +++++++++++++++++++++ mobile/src/platform/native-wakelock.ts | 7 ++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/mobile/src/platform/native-wakelock.test.ts b/mobile/src/platform/native-wakelock.test.ts index 27dfbfeb77f..6ff03fc8139 100644 --- a/mobile/src/platform/native-wakelock.test.ts +++ b/mobile/src/platform/native-wakelock.test.ts @@ -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' + ]) + }) }) diff --git a/mobile/src/platform/native-wakelock.ts b/mobile/src/platform/native-wakelock.ts index 49e1da7cf49..0c785df0e67 100644 --- a/mobile/src/platform/native-wakelock.ts +++ b/mobile/src/platform/native-wakelock.ts @@ -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)