mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-21 11:15:18 -04:00
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:
1 parent
e95cc396c5
commit
54756a3265
17 files changed
+354
-26
No files matched your search
@@ -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.
|
||||
}
|
||||
}
|
||||
+5
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+19
-23
@@ -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
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+38
@@ -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 {}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+32
@@ -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)
|
||||
}
|
||||
File renamed without changes.
+77
@@ -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()
|
||||
}
|
||||
+83
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user