From e5d15c920fe3f97f02ad995c25129f86accf3363 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 14 Jul 2026 11:21:57 -0400 Subject: [PATCH] Serialize app-scope writes via Main.immediate (Codex hardening note) Non-blocking follow-up from the audit: applicationScope ran on Dispatchers.Default, so two very fast conflicting persistence writes (recalibrate then immediately Reset) had no ordering guarantee. Main.immediate launches the coroutine body synchronously in call order, so the DataStore edits enqueue deterministically; DataStore still does the IO on its own dispatcher. Only lightweight persistence uses this scope. 64 tests passing; assembleDebug clean; runs on device. Co-Authored-By: Claude Fable 5 --- app/src/main/java/com/onthelevel/OnTheLevelApp.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/onthelevel/OnTheLevelApp.kt b/app/src/main/java/com/onthelevel/OnTheLevelApp.kt index 038ea21..50aad57 100644 --- a/app/src/main/java/com/onthelevel/OnTheLevelApp.kt +++ b/app/src/main/java/com/onthelevel/OnTheLevelApp.kt @@ -22,8 +22,13 @@ class AppContainer(context: Context) { * Outlives any single screen. Used for durable fire-and-forget persistence * (e.g. saving a calibration): a screen-scoped coroutine would be cancelled if * the user leaves the moment the write is launched. + * + * Main.immediate (not Default) so writes launched from UI callbacks enqueue in + * call order — two fast conflicting writes (recalibrate then Reset) then reach + * DataStore deterministically. Only lightweight persistence runs here; DataStore + * still performs the actual IO on its own dispatcher. */ - val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) val sensorSource: SensorSource by lazy { AndroidSensorSource(appContext) } val settings: SettingsRepository by lazy { SettingsRepository(appContext) }