feat(datastore): enable wasmJs via a platform-neutral Store abstraction

core:datastore wraps androidx.datastore.core.DataStore<T> for five
payload types (four proto messages plus a Preferences-backed store).
androidx.datastore:datastore -- the core artifact providing
DataStore/OkioSerializer/CorruptionException, not just the
-preferences extension core:prefs already hit -- publishes no wasmJs
variant at any version, confirmed against its Gradle Module Metadata.

Introduces Store<T> (core/datastore/store/Store.kt), a direct analog
to core:prefs's PrefsStore but simpler: each store here already holds
one whole serializable value, so no key/snapshot abstraction is
needed, just data/updateData mirroring DataStore<T>'s own two
members. nonWebMain's DataStoreAdapter wraps a real DataStore<T>
unchanged; wasmJsMain's LocalStorageStore is backed by localStorage,
running each payload's Wire ADAPTER.decode/.encode against an
in-memory okio.Buffer and base64-encoding the resulting bytes
(localStorage is string-only). Corruption policy mirrors the existing
Android/JVM ReplaceFileCorruptionHandler: no value yet or a value that
fails to decode both fall back to the default, with a decode failure
also logged and the recovered default written back so it isn't
re-hit on every access.

The four proto-payload DataSources (ChannelSetDataSource and friends)
needed zero logic changes -- they already depended only on
data/updateData, not on any other DataStore<T> member.

CorePreferencesDataStore (DataStore<Preferences>) has no wasmJs
counterpart: androidx.datastore.preferences's Preferences type itself
has no wasmJs variant, so there's no way to construct a Store<Preferences>
there at all -- this is the payload type being unavailable, not a
missing adapter. It and its three consumers
(RecentAddressesDataSource/BootloaderWarningDataSource/
FirmwareRecoveryDataSource) move to nonWebMain unchanged, deferred
rather than dropped -- a future pass could rewrite them against
core:prefs's own PrefsStore instead.

Caught before shipping: hoisting androidx.datastore/
androidx.datastore.preferences to nonWebMain-only initially demoted
them from api to implementation, mirroring core:prefs's precedent --
but androidApp's google flavor (GoogleMapsDataStore.kt/
GoogleMapsPrefs.kt) imports those packages directly with no dependency
of its own, relying entirely on this module's transitive api exposure.
Kept as api at the nonWebMain/commonMain level instead; verified via a
real compileGoogleDebugKotlin + testGoogleDebugUnitTest run, not just
the fdroid flavor the first pass checked.

CoreDatastoreWasmJsModule is not registered anywhere yet -- no webApp
module exists in this repo pass, same state as core:database's
SingleDatabaseProvider and core:prefs's CorePrefsWasmJsModule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
James RichandClaude Sonnet 5 committed 2026-08-30 22:43:26 -05:00
1 parent e95cc396c5
commit 54756a3265
17 files changed
+354 -26

No files matched your search

+51 -3
View File
@@ -14,6 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
@@ -23,17 +28,60 @@ plugins {
kotlin {
android { withHostTest {} }
// Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable).
@OptIn(ExperimentalWasmDsl::class)
wasmJs()
// nonWebMain: androidx.datastore:datastore (DataStore<T>/OkioSerializer/CorruptionException) and
// androidx.datastore.preferences (the Preferences type) both publish no wasmJs variant at all — confirmed
// against their real Gradle Module Metadata, the same absence core:prefs hit for datastore-preferences alone
// (see core/prefs/build.gradle.kts). So the four proto serializers, CorePreferencesDataStore, and its three
// Preferences-backed consumers (RecentAddressesDataSource/BootloaderWarningDataSource/
// FirmwareRecoveryDataSource) all live here instead of commonMain. The four proto DataSources themselves
// (ChannelSetDataSource etc.) stay in commonMain unchanged — they depend only on the platform-neutral Store<T>
// abstraction (see core/datastore/store/Store.kt), never on DataStore<T> directly. Predicate, not
// withAndroidTarget()/withApple() — those silently drop androidMain under
// com.android.kotlin.multiplatform.library (KT-80409). See core/ble/build.gradle.kts for the same pattern.
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyHierarchyTemplate(KotlinHierarchyTemplate.default) {
common { group("nonWeb") { withCompilations { it.target.targetName != "wasmJs" } } }
}
// The predicate above misses iosMain itself (only reaches the two leaf iOS compilations), so any
// nonWebMain-only actual/declaration can't see nonWebMain's members without this explicit edge.
sourceSets.getByName("iosMain") { dependsOn(sourceSets.getByName("nonWebMain")) }
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
implementation(projects.core.model)
implementation(libs.meshtastic.protobufs)
api(libs.androidx.datastore)
api(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kermit)
// api, not implementation: androidApp (e.g. the google flavor's GoogleMapsDataStore/GoogleMapsPrefs) and
// other consumers import okio.* directly today with no dependency of their own, relying entirely on this
// module exposing it transitively (previously via androidx.datastore's own api-exposed okio dependency,
// now directly since androidx.datastore itself moved to nonWebMain — see below).
api(libs.okio)
}
commonTest.dependencies { implementation(libs.okio) }
// android/jvm/ios only (see hierarchy template above) — androidx.datastore has no wasmJs variant, and the
// serializers/CorePreferencesDataStore (the sole consumers) live here. api, not implementation: androidApp's
// google flavor (GoogleMapsDataStore.kt/GoogleMapsPrefs.kt) imports androidx.datastore.* directly with no
// dependency of its own, relying entirely on this module's transitive exposure — confirmed by grepping
// androidApp/build.gradle.kts for a direct dependency (none exists). An intermediate source set's `api`
// dependency still propagates to every consumer of the targets under it (android/jvm/iOS here), while
// correctly staying invisible to wasmJs consumers, exactly preserving the original commonMain-level `api`'s
// effective reach.
getByName("nonWebMain").dependencies {
api(libs.androidx.datastore)
api(libs.androidx.datastore.preferences)
}
wasmJsMain.dependencies { implementation(libs.kotlinx.browser) }
// The only commonTest file (RecentAddressesDataSourceTest) constructs a real DataStore<Preferences> via
// PreferenceDataStoreFactory, so it moved to nonWebTest wholesale (same split core:prefs/core:database's
// DataStore-backed tests got) — commonTest is empty now.
}
}
@@ -36,6 +36,7 @@ import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
import org.meshtastic.core.datastore.store.asStore
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
@@ -68,6 +69,7 @@ class LocalConfigDataStoreModule {
produceNewData = { LocalConfig() },
scope = scope,
)
.asStore()
.asCoreLocalConfigDataStore()
}
@@ -80,6 +82,7 @@ class ModuleConfigDataStoreModule {
produceNewData = { LocalModuleConfig() },
scope = scope,
)
.asStore()
.asCoreModuleConfigDataStore()
}
@@ -92,6 +95,7 @@ class ChannelSetDataStoreModule {
produceNewData = { ChannelSet() },
scope = scope,
)
.asStore()
.asCoreChannelSetDataStore()
}
@@ -104,6 +108,7 @@ class LocalStatsDataStoreModule {
produceNewData = { LocalStats() },
scope = scope,
)
.asStore()
.asCoreLocalStatsDataStore()
}
@@ -16,52 +16,48 @@
*/
package org.meshtastic.core.datastore.di
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import org.meshtastic.core.datastore.store.Store
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
// One type per store, so the compiler distinguishes them instead of a Koin string qualifier. Each is a transparent
// `DataStore` of its payload — inject and use it exactly like one.
// `Store` of its payload — inject and use it exactly like one. `CorePreferencesDataStore` (DataStore<Preferences>)
// is not here: `androidx.datastore.preferences`'s `Preferences` type has no wasmJs variant, so it can't be ported
// behind `Store<T>` at all — see nonWebMain's `CorePreferencesDataStore.kt`.
/**
* Application-lifetime scope shared by every [DataStore]. Per the DataStore docs this must not be cancelled by UI
* lifecycle events: `DataStore` has no `close()`, so its in-memory cache is released only when this job ends.
* Application-lifetime scope shared by every real `DataStore` (android/jvm/iOS only). Per the DataStore docs this must
* not be cancelled by UI lifecycle events: `DataStore` has no `close()`, so its in-memory cache is released only when
* this job ends. wasmJs's `LocalStorageStore` needs no scope — `localStorage` access is synchronous.
*/
interface DataStoreScope : CoroutineScope
/** Presents an existing scope as [DataStoreScope]; the wrapper adds nothing but identity. */
fun CoroutineScope.asDataStoreScope(): DataStoreScope = object : DataStoreScope, CoroutineScope by this {}
interface CorePreferencesDataStore : DataStore<Preferences>
/** Presents an existing store as [CorePreferencesDataStore]; the wrapper adds nothing but identity. */
fun DataStore<Preferences>.asCorePreferencesDataStore(): CorePreferencesDataStore =
object : CorePreferencesDataStore, DataStore<Preferences> by this {}
interface CoreChannelSetDataStore : DataStore<ChannelSet>
interface CoreChannelSetDataStore : Store<ChannelSet>
/** Presents an existing store as [CoreChannelSetDataStore]; the wrapper adds nothing but identity. */
fun DataStore<ChannelSet>.asCoreChannelSetDataStore(): CoreChannelSetDataStore =
object : CoreChannelSetDataStore, DataStore<ChannelSet> by this {}
fun Store<ChannelSet>.asCoreChannelSetDataStore(): CoreChannelSetDataStore =
object : CoreChannelSetDataStore, Store<ChannelSet> by this {}
interface CoreLocalConfigDataStore : DataStore<LocalConfig>
interface CoreLocalConfigDataStore : Store<LocalConfig>
/** Presents an existing store as [CoreLocalConfigDataStore]; the wrapper adds nothing but identity. */
fun DataStore<LocalConfig>.asCoreLocalConfigDataStore(): CoreLocalConfigDataStore =
object : CoreLocalConfigDataStore, DataStore<LocalConfig> by this {}
fun Store<LocalConfig>.asCoreLocalConfigDataStore(): CoreLocalConfigDataStore =
object : CoreLocalConfigDataStore, Store<LocalConfig> by this {}
interface CoreLocalStatsDataStore : DataStore<LocalStats>
interface CoreLocalStatsDataStore : Store<LocalStats>
/** Presents an existing store as [CoreLocalStatsDataStore]; the wrapper adds nothing but identity. */
fun DataStore<LocalStats>.asCoreLocalStatsDataStore(): CoreLocalStatsDataStore =
object : CoreLocalStatsDataStore, DataStore<LocalStats> by this {}
fun Store<LocalStats>.asCoreLocalStatsDataStore(): CoreLocalStatsDataStore =
object : CoreLocalStatsDataStore, Store<LocalStats> by this {}
interface CoreModuleConfigDataStore : DataStore<LocalModuleConfig>
interface CoreModuleConfigDataStore : Store<LocalModuleConfig>
/** Presents an existing store as [CoreModuleConfigDataStore]; the wrapper adds nothing but identity. */
fun DataStore<LocalModuleConfig>.asCoreModuleConfigDataStore(): CoreModuleConfigDataStore =
object : CoreModuleConfigDataStore, DataStore<LocalModuleConfig> by this {}
fun Store<LocalModuleConfig>.asCoreModuleConfigDataStore(): CoreModuleConfigDataStore =
object : CoreModuleConfigDataStore, Store<LocalModuleConfig> by this {}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.datastore.store
import kotlinx.coroutines.flow.Flow
/**
* Platform-neutral replacement for `androidx.datastore.core.DataStore<T>`, mirroring its two members exactly.
* `androidx.datastore:datastore` (the core artifact providing `DataStore`/`OkioSerializer`/`CorruptionException`)
* publishes no wasmJs variant at all — confirmed against its Gradle Module Metadata, the same absence
* `androidx.datastore.preferences` hit for core:prefs (see `core/prefs/store/PrefsStore.kt`) — so this module's
* proto-payload stores (ChannelSet/LocalConfig/LocalStats/LocalModuleConfig) can't reference the real type from
* commonMain at all. Two implementations exist:
* - nonWebMain's `asStore()` — a thin wrapper over a real `DataStore<T>`; android/jvm/iOS keep using the real DataStore
* machinery underneath, unchanged.
* - wasmJsMain's `LocalStorageStore` — backed by the browser's `localStorage`, encoding each value's proto bytes as
* base64 (localStorage is string-only).
*
* Unlike core:prefs's `PrefsStore` (many keys under one store), each `Store<T>` here already holds one whole
* serializable value, so no key/snapshot abstraction is needed — this interface is a direct, simpler analog.
*
* `updateData`'s [transform] is deliberately non-suspend (androidx's is `suspend`) — no DataSource in this module needs
* suspension inside a transform, and a non-suspend lambda already satisfies a `suspend` function type, so nonWebMain's
* real-DataStore adapter passes it straight through with no wrapping needed.
*/
interface Store<T> {
val data: Flow<T>
suspend fun updateData(transform: (T) -> T): T
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.datastore.di
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
/**
* `androidx.datastore.preferences`'s `Preferences` type has no wasmJs variant at all (the same absence `core:prefs` hit
* — see `core/prefs/store/PrefsStore.kt`), so unlike the four proto-payload stores in `CoreDataStores.kt`, this one
* cannot be ported behind the generic `Store<T>` abstraction: there is no way to construct a `Store<Preferences>` on
* wasmJs, because the `Preferences` type itself doesn't resolve there — this is not a missing-adapter gap, it's the
* payload type being unavailable. Stays directly typed against the real `DataStore<Preferences>`, android/jvm/iOS only.
* Its three consumers — `RecentAddressesDataSource`, `BootloaderWarningDataSource`, `FirmwareRecoveryDataSource` — move
* here with it, since they use `androidx.datastore.preferences`'s typed-key API directly. [DEFERRED]: a future pass
* could give these three web support by rewriting them against core:prefs's own `PrefsStore`/`PrefsKey` abstraction
* instead (which already solves "key-value settings on wasmJs" for the rest of the app) — not attempted here, out of
* this module's scope.
*/
interface CorePreferencesDataStore : DataStore<Preferences>
/** Presents an existing store as [CorePreferencesDataStore]; the wrapper adds nothing but identity. */
fun DataStore<Preferences>.asCorePreferencesDataStore(): CorePreferencesDataStore =
object : CorePreferencesDataStore, DataStore<Preferences> by this {}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.datastore.store
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow
/**
* Adapts a real `DataStore<T>` (android/jvm/iOS only — `androidx.datastore:datastore` has no wasmJs variant) to the
* platform-neutral [Store]. Every `CoreDatastoreAndroidModule`/`DesktopPlatformModule` call site that used to hand a
* `DataStore<T>` straight to an `asCoreXDataStore()` wrapper now inserts this adapter first:
* `protoStore(...).asStore().asCoreXDataStore()`.
*/
fun <T> DataStore<T>.asStore(): Store<T> = object : Store<T> {
override val data: Flow<T> = this@asStore.data
override suspend fun updateData(transform: (T) -> T): T = this@asStore.updateData(transform)
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.datastore.di
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.store.LocalStorageStore
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
/**
* Koin module providing wasmJs [org.meshtastic.core.datastore.store.Store]-backed proto stores, one per payload type,
* backed by the browser's `localStorage`. Mirrors `CoreDatastoreAndroidModule`/desktopApp's
* `desktopProtoDataStoreModule` — one singleton store per proto payload, minus the `DataStoreScope`/on-disk-path
* plumbing (`localStorage` access is synchronous, no scope or file path needed).
*
* `CorePreferencesDataStore` has no counterpart here: see its own KDoc in nonWebMain for why (the `Preferences` type
* itself has no wasmJs variant), which is also why `RecentAddressesDataSource`/`BootloaderWarningDataSource`/
* `FirmwareRecoveryDataSource` have no web binding this pass.
*
* Not yet registered anywhere: like `CorePrefsWasmJsModule`/core:database's `SingleDatabaseProvider`, nothing on wasmJs
* composes a Koin graph yet (no `webApp` module exists in this repo pass).
*/
@Module
class CoreDatastoreWasmJsModule {
@Single
fun provideChannelSetDataStore(): CoreChannelSetDataStore = LocalStorageStore(
storageKey = "channel_set_ds",
defaultValue = ChannelSet(),
decode = ChannelSet.ADAPTER::decode,
encode = { value, sink -> ChannelSet.ADAPTER.encode(sink, value) },
)
.asCoreChannelSetDataStore()
@Single
fun provideLocalConfigDataStore(): CoreLocalConfigDataStore = LocalStorageStore(
storageKey = "local_config_ds",
defaultValue = LocalConfig(),
decode = LocalConfig.ADAPTER::decode,
encode = { value, sink -> LocalConfig.ADAPTER.encode(sink, value) },
)
.asCoreLocalConfigDataStore()
@Single
fun provideLocalStatsDataStore(): CoreLocalStatsDataStore = LocalStorageStore(
storageKey = "local_stats_ds",
defaultValue = LocalStats(),
decode = LocalStats.ADAPTER::decode,
encode = { value, sink -> LocalStats.ADAPTER.encode(sink, value) },
)
.asCoreLocalStatsDataStore()
@Single
fun provideModuleConfigDataStore(): CoreModuleConfigDataStore = LocalStorageStore(
storageKey = "module_config_ds",
defaultValue = LocalModuleConfig(),
decode = LocalModuleConfig.ADAPTER::decode,
encode = { value, sink -> LocalModuleConfig.ADAPTER.encode(sink, value) },
)
.asCoreModuleConfigDataStore()
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.datastore.store
import co.touchlab.kermit.Logger
import kotlinx.browser.localStorage
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import okio.Buffer
import okio.BufferedSink
import okio.BufferedSource
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
/**
* Backed by the browser's `localStorage`. [decode]/[encode] run against an in-memory [Buffer] — it implements both
* `BufferedSource` and `BufferedSink` purely in memory (no file, works on every target) — to get/put the value's raw
* proto bytes, which are then [Base64]-encoded since `localStorage` only stores strings.
*
* Reads are live (no caching), matching `LocalStoragePrefsStore`: [revision] exists purely to give [data] a fresh
* emission after every [updateData] — [Revision] has no `equals` override, so each freshly-constructed instance is
* unequal by reference to the last, exactly what [MutableStateFlow] needs to avoid conflating the update away.
*
* Corruption policy mirrors this module's Android/JVM `ReplaceFileCorruptionHandler` (see
* `CoreDatastoreAndroidModule`'s `protoStore`): no value yet (first launch) or a value that fails to decode both fall
* back to [defaultValue]. A decode failure is also logged and the recovered default written back, so a corrupted read
* is not repeated on every subsequent access — "first launch" is not logged, since it isn't corruption.
*/
@OptIn(ExperimentalEncodingApi::class)
internal class LocalStorageStore<T>(
private val storageKey: String,
private val defaultValue: T,
private val decode: (BufferedSource) -> T,
private val encode: (T, BufferedSink) -> Unit,
) : Store<T> {
private class Revision
private val revision = MutableStateFlow(Revision())
override val data: Flow<T> = revision.map { readCurrent() }
override suspend fun updateData(transform: (T) -> T): T {
val updated = transform(readCurrent())
writeCurrent(updated)
revision.value = Revision()
return updated
}
// Heterogeneous failure modes (Base64's IllegalArgumentException, Wire/okio's various decode-time exceptions) are
// deliberately all treated the same way: recover to defaultValue.
@Suppress("TooGenericExceptionCaught")
private fun readCurrent(): T {
val raw = localStorage.getItem(storageKey) ?: return defaultValue
return try {
decode(Buffer().apply { write(Base64.Default.decode(raw)) })
} catch (e: Exception) {
Logger.w(e) { "Corrupt localStorage value for '$storageKey', resetting to default" }
writeCurrent(defaultValue)
defaultValue
}
}
private fun writeCurrent(value: T) {
val buffer = Buffer()
encode(value, buffer)
localStorage.setItem(storageKey, Base64.Default.encode(buffer.readByteArray()))
}
}
@@ -52,6 +52,7 @@ import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
import org.meshtastic.core.datastore.store.asStore
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.AnalyticsDataStore
import org.meshtastic.core.prefs.di.AppDataStore
@@ -192,21 +193,25 @@ private fun desktopProtoDataStoreModule() = module {
single<CoreLocalConfigDataStore> {
protoStore(LocalConfigSerializer, "$protoDir/local_config.pb", { LocalConfig() }, get())
.asStore()
.asCoreLocalConfigDataStore()
}
single<CoreModuleConfigDataStore> {
protoStore(ModuleConfigSerializer, "$protoDir/module_config.pb", { LocalModuleConfig() }, get())
.asStore()
.asCoreModuleConfigDataStore()
}
single<CoreChannelSetDataStore> {
protoStore(ChannelSetSerializer, "$protoDir/channel_set.pb", { ChannelSet() }, get())
.asStore()
.asCoreChannelSetDataStore()
}
single<CoreLocalStatsDataStore> {
protoStore(LocalStatsSerializer, "$protoDir/local_stats.pb", { LocalStats() }, get())
.asStore()
.asCoreLocalStatsDataStore()
}
}