summaryrefslogtreecommitdiff
path: root/android
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-12 06:09:29 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-12 06:09:29 +0000
commita43cc7b8f007a86742756bd4b67f9ba1c204bbd0 (patch)
tree0d9bbbe0c4856c2992bed41ad1d1c9964fbc9f91 /android
parent9a87f9db5af943ea253b814d4196020341e3c2ba (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.kt21
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
+ )
}
}
}