fix(data): snapshot last-known-good config and recover on decode failure instead of wiping
This commit is contained in:
parent
a5681608ed
commit
fbeb15be88
1 changed files with 38 additions and 5 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package chat.vojo.proxy.data
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
|
|
@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
|
@ -32,7 +34,25 @@ data class ProxyState(
|
|||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vojo_proxy")
|
||||
private val STATE_KEY = stringPreferencesKey("state")
|
||||
// Last known-good encoded state, snapshotted before each successful write. If STATE_KEY
|
||||
// later fails to decode (disk corruption / incompatible schema), both the read path and
|
||||
// update() recover from this instead of silently wiping every saved server.
|
||||
private val STATE_BACKUP_KEY = stringPreferencesKey("state_backup")
|
||||
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||
private const val TAG = "ConfigStore"
|
||||
|
||||
/**
|
||||
* Decode persisted state, treating only a genuine decode failure (corruption / incompatible
|
||||
* schema) as "corrupt" → null. Error/OOM propagate so a transient failure never masquerades
|
||||
* as missing data and triggers a wipe.
|
||||
*/
|
||||
private fun decodeStateOrNull(raw: String): ProxyState? = try {
|
||||
json.decodeFromString<ProxyState>(raw)
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
/** Single source of truth for persisted configuration, backed by Preferences DataStore. */
|
||||
class ConfigStore(context: Context) {
|
||||
|
|
@ -43,7 +63,8 @@ class ConfigStore(context: Context) {
|
|||
// rethrow anything else so a real bug isn't hidden as "no data".
|
||||
.catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e }
|
||||
.map { prefs ->
|
||||
prefs[STATE_KEY]?.let { runCatching { json.decodeFromString<ProxyState>(it) }.getOrNull() }
|
||||
prefs[STATE_KEY]?.let { decodeStateOrNull(it) }
|
||||
?: prefs[STATE_BACKUP_KEY]?.let { decodeStateOrNull(it) }
|
||||
?: ProxyState()
|
||||
}
|
||||
|
||||
|
|
@ -51,10 +72,22 @@ class ConfigStore(context: Context) {
|
|||
|
||||
suspend fun update(transform: (ProxyState) -> ProxyState) {
|
||||
store.edit { prefs ->
|
||||
val cur = prefs[STATE_KEY]?.let {
|
||||
runCatching { json.decodeFromString<ProxyState>(it) }.getOrNull()
|
||||
} ?: ProxyState()
|
||||
prefs[STATE_KEY] = json.encodeToString(transform(cur))
|
||||
val raw = prefs[STATE_KEY]
|
||||
val cur = raw?.let { decodeStateOrNull(it) }
|
||||
val base = if (cur != null) {
|
||||
// Current state is good: snapshot it as last-known-good before overwriting,
|
||||
// so a future on-disk corruption of STATE_KEY can be auto-recovered on read.
|
||||
prefs[STATE_BACKUP_KEY] = raw
|
||||
cur
|
||||
} else {
|
||||
// STATE_KEY present but undecodable (disk corruption or a forward-incompatible
|
||||
// schema after a downgrade). Recover from the last known-good snapshot if we
|
||||
// have one — the original bug here wiped every saved server. Leave the backup
|
||||
// untouched so the good snapshot survives. Start fresh only if there is none.
|
||||
if (raw != null) Log.w(TAG, "persisted state did not decode — recovering from last-known-good backup")
|
||||
prefs[STATE_BACKUP_KEY]?.let { decodeStateOrNull(it) } ?: ProxyState()
|
||||
}
|
||||
prefs[STATE_KEY] = json.encodeToString(transform(base))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue