diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 06:09:29 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 06:09:29 +0000 |
| commit | a43cc7b8f007a86742756bd4b67f9ba1c204bbd0 (patch) | |
| tree | 0d9bbbe0c4856c2992bed41ad1d1c9964fbc9f91 /android | |
| parent | 9a87f9db5af943ea253b814d4196020341e3c2ba (diff) | |
fix(widget): dedup rapid completeTask taps on the same task
CompleteWorker.enqueue used a plain WorkManager.enqueue(), which allows
unlimited concurrent OneTimeWorkRequests. A rapid double-tap on the same
row (plausible since the checkbox doesn't visually update until the full
async round-trip -- complete() -> fetchAndPersist() -> updateAll() --
finishes) could spawn two independent, unordered CompleteWorker runs for
the same task, each doing its own fetchAndPersist(); a second worker's
fetch started before the first worker's complete() call had actually
landed server-side could persist a stale snapshot after the first
worker's correct one.
Now uses enqueueUniqueWork("complete_$id", KEEP, ...) so a tap on a task
that already has a completion in flight is dropped rather than racing a
second worker. Different task ids remain independent.
Diffstat (limited to 'android')
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/work/CompleteWorker.kt | 21 |
1 files changed, 20 insertions, 1 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/work/CompleteWorker.kt b/android/app/src/main/java/org/terst/doot/widget/work/CompleteWorker.kt index d3017ff..51ad727 100644 --- a/android/app/src/main/java/org/terst/doot/widget/work/CompleteWorker.kt +++ b/android/app/src/main/java/org/terst/doot/widget/work/CompleteWorker.kt @@ -35,12 +35,31 @@ class CompleteWorker(context: Context, params: WorkerParameters) : const val KEY_ID = "item_id" const val KEY_SOURCE = "item_source" + // enqueueUniqueWork(id, KEEP, ...) instead of a plain enqueue(): a + // rapid double-tap on the same row (plausible since the checkbox + // doesn't visually update until this worker's full async round-trip + // -- complete() -> fetchAndPersist() -> updateAll() -- finishes) + // previously spawned two independent, unordered CompleteWorker runs + // for the same task. Each does its own fetchAndPersist(), so a + // second worker's fetch (started before the first worker's + // complete() call had actually landed server-side) could persist a + // stale snapshot after the first worker's correct one, leaving the + // widget showing outdated data. KEEP means a tap on a task that + // already has a completion in flight is simply dropped -- the first + // request's result (including its fetchAndPersist/updateAll) is what + // takes effect, with no race between two workers touching the same + // task. Different task ids still run independently/concurrently, + // which is fine since they don't share a row. fun enqueue(context: Context, id: String, source: String) { val data = workDataOf(KEY_ID to id, KEY_SOURCE to source) val request = OneTimeWorkRequestBuilder<CompleteWorker>() .setInputData(data) .build() - WorkManager.getInstance(context).enqueue(request) + WorkManager.getInstance(context).enqueueUniqueWork( + "complete_$id", + ExistingWorkPolicy.KEEP, + request + ) } } } |
