diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts
index a93b99a725..6491511d5d 100644
--- a/core/prefs/build.gradle.kts
+++ b/core/prefs/build.gradle.kts
@@ -15,6 +15,10 @@
* along with this program. If not, see .
*/
+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.koin)
@@ -23,16 +27,48 @@ plugins {
kotlin {
android { withHostTest {} }
+ // Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable).
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs()
+
+ // nonWebMain: androidx.datastore.preferences has no wasmJs variant (the `Preferences` type itself doesn't
+ // resolve there), so DataStorePrefsStore — the adapter wrapping a real DataStore — and the
+ // Android-only CorePrefsAndroidModule live here/in androidMain instead of commonMain. All 17 `*PrefsImpl`
+ // classes themselves stay in commonMain: they depend only on the platform-neutral PrefsStore/PrefsSnapshot/
+ // PrefsKey abstraction (see core/prefs/store/PrefsStore.kt), not on Preferences directly, so unlike
+ // core:database's DatabaseManager split, nothing about their own code needs to move. 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.repository)
implementation(projects.core.common)
implementation(projects.core.di)
- implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.atomicfu)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core)
}
+
+ // android/jvm/ios only (see hierarchy template above) — androidx.datastore.preferences has no wasmJs
+ // variant, and DataStorePrefsStore (the sole consumer) lives here.
+ getByName("nonWebMain").dependencies { implementation(libs.androidx.datastore.preferences) }
+
+ wasmJsMain.dependencies { implementation(libs.kotlinx.browser) }
+
+ // All 7 existing commonTest files construct a real DataStore via PreferenceDataStoreFactory,
+ // so they move to nonWebTest wholesale (same nonWebTest split core:database's DataStore/DAO tests got) —
+ // nothing wasmJs-reachable in this module's tests needs a real DataStore today. No extra dependency
+ // needed here: kotlin("test")/kotest/turbine/coroutines-test come from configureKmpTestDependencies()'s
+ // commonTest additions, and nonWebTest inherits them via the hierarchy template's dependsOn edge.
}
}
diff --git a/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt
index 5ff3af23cb..f49726c2bc 100644
--- a/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt
+++ b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt
@@ -27,9 +27,11 @@ import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
+import org.meshtastic.core.prefs.store.PrefsStore
+import org.meshtastic.core.prefs.store.asPrefsStore
/**
- * Koin module providing Android [DataStore] instances for each preference domain.
+ * Koin module providing Android [PrefsStore] instances (each backed by a real [DataStore]) for each preference domain.
*
* Each DataStore is a singleton backed by its own [CoroutineScope] using the injected [CoroutineDispatchers.io]
* dispatcher, and includes a [SharedPreferencesMigration] to migrate legacy SharedPreferences data on first access.
@@ -97,14 +99,21 @@ class CorePrefsAndroidModule {
/**
* [legacyName] is the SharedPreferences file this domain migrates from, [fileName] the DataStore file it lives in now.
* Both are on-disk identities — changing either orphans existing user data.
+ *
+ * Returns [PrefsStore] (not the underlying `DataStore` directly) so every `provideXDataStore()` call site
+ * above can keep calling `.asXDataStore()` unchanged — `asPrefsStore()` is the one line doing the adaptation.
*/
private fun store(
context: Context,
dispatchers: CoroutineDispatchers,
legacyName: String,
fileName: String,
-): DataStore = PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, legacyName)),
- scope = CoroutineScope(dispatchers.io + SupervisorJob()),
- produceFile = { context.preferencesDataStoreFile(fileName) },
-)
+): PrefsStore {
+ val dataStore: DataStore =
+ PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, legacyName)),
+ scope = CoroutineScope(dispatchers.io + SupervisorJob()),
+ produceFile = { context.preferencesDataStoreFile(fileName) },
+ )
+ return dataStore.asPrefsStore()
+}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt
index a55e3d46c6..2e1ce4344e 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.analytics
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -30,6 +27,8 @@ import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.AnalyticsDataStore
import org.meshtastic.core.prefs.di.AppDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.AnalyticsPrefs
import kotlin.uuid.Uuid
@@ -67,7 +66,7 @@ class AnalyticsPrefsImpl(
const val KEY_ANALYTICS_ALLOWED = "allowed"
const val KEY_INSTALL_ID = "appPrefs_install_id"
- val KEY_ANALYTICS_ALLOWED_PREF = booleanPreferencesKey(KEY_ANALYTICS_ALLOWED)
- val KEY_INSTALL_ID_PREF = stringPreferencesKey(KEY_INSTALL_ID)
+ val KEY_ANALYTICS_ALLOWED_PREF = booleanPrefsKey(KEY_ANALYTICS_ALLOWED)
+ val KEY_INSTALL_ID_PREF = stringPrefsKey(KEY_INSTALL_ID)
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt
index bf2e6be0a1..1e0e22cfdf 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/appfunctions/AppFunctionsPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.appfunctions
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.AppDataStore
+import org.meshtastic.core.prefs.store.PrefsKey
+import org.meshtastic.core.prefs.store.booleanPrefsKey
import org.meshtastic.core.repository.AppFunctionsPrefs
@Single
@@ -68,23 +67,23 @@ class AppFunctionsPrefsImpl(private val dataStore: AppDataStore, dispatchers: Co
override fun setGetUnreadSummaryEnabled(enabled: Boolean) = set(KEY_GET_UNREAD_SUMMARY, enabled)
- private fun booleanPref(key: Preferences.Key, default: Boolean): StateFlow =
+ private fun booleanPref(key: PrefsKey, default: Boolean): StateFlow =
dataStore.data.map { it[key] ?: default }.stateIn(scope, SharingStarted.Eagerly, default)
- private fun set(key: Preferences.Key, value: Boolean) {
+ private fun set(key: PrefsKey, value: Boolean) {
scope.launch { dataStore.edit { prefs -> prefs[key] = value } }
}
companion object {
- private val KEY_MASTER = booleanPreferencesKey("appfn_master_enabled")
- private val KEY_SEND_MESSAGE = booleanPreferencesKey("appfn_send_message")
- private val KEY_GET_MESH_STATUS = booleanPreferencesKey("appfn_get_mesh_status")
- private val KEY_GET_NODE_LIST = booleanPreferencesKey("appfn_get_node_list")
- private val KEY_GET_CHANNEL_INFO = booleanPreferencesKey("appfn_get_channel_info")
- private val KEY_GET_DEVICE_STATUS = booleanPreferencesKey("appfn_get_device_status")
- private val KEY_GET_NODE_DETAILS = booleanPreferencesKey("appfn_get_node_details")
- private val KEY_GET_MESH_METRICS = booleanPreferencesKey("appfn_get_mesh_metrics")
- private val KEY_GET_RECENT_MESSAGES = booleanPreferencesKey("appfn_get_recent_messages")
- private val KEY_GET_UNREAD_SUMMARY = booleanPreferencesKey("appfn_get_unread_summary")
+ private val KEY_MASTER = booleanPrefsKey("appfn_master_enabled")
+ private val KEY_SEND_MESSAGE = booleanPrefsKey("appfn_send_message")
+ private val KEY_GET_MESH_STATUS = booleanPrefsKey("appfn_get_mesh_status")
+ private val KEY_GET_NODE_LIST = booleanPrefsKey("appfn_get_node_list")
+ private val KEY_GET_CHANNEL_INFO = booleanPrefsKey("appfn_get_channel_info")
+ private val KEY_GET_DEVICE_STATUS = booleanPrefsKey("appfn_get_device_status")
+ private val KEY_GET_NODE_DETAILS = booleanPrefsKey("appfn_get_node_details")
+ private val KEY_GET_MESH_METRICS = booleanPrefsKey("appfn_get_mesh_metrics")
+ private val KEY_GET_RECENT_MESSAGES = booleanPrefsKey("appfn_get_recent_messages")
+ private val KEY_GET_UNREAD_SUMMARY = booleanPrefsKey("appfn_get_unread_summary")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/PrefsDataStores.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/PrefsDataStores.kt
index 97c66164c7..8c5f43b0df 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/PrefsDataStores.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/PrefsDataStores.kt
@@ -18,76 +18,72 @@
package org.meshtastic.core.prefs.di
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
+import org.meshtastic.core.prefs.store.PrefsStore
// One type per preference domain, so the compiler distinguishes them instead of a Koin string qualifier. Each is a
-// transparent `DataStore` — inject and use it exactly like one.
+// transparent PrefsStore — inject and use it exactly like one. PrefsStore itself (not `DataStore`
+// directly — androidx.datastore.preferences has no wasmJs variant) is what makes these usable from every target:
+// nonWebMain's DataStorePrefsStore adapts a real DataStore, wasmJsMain's LocalStoragePrefsStore is
+// backed by localStorage. See core/prefs/store/PrefsStore.kt.
-interface AnalyticsDataStore : DataStore
+interface AnalyticsDataStore : PrefsStore
/** Presents an existing store as [AnalyticsDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asAnalyticsDataStore(): AnalyticsDataStore =
- object : AnalyticsDataStore, DataStore by this {}
+fun PrefsStore.asAnalyticsDataStore(): AnalyticsDataStore = object : AnalyticsDataStore, PrefsStore by this {}
-interface AppDataStore : DataStore
+interface AppDataStore : PrefsStore
/** Presents an existing store as [AppDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asAppDataStore(): AppDataStore = object : AppDataStore, DataStore by this {}
+fun PrefsStore.asAppDataStore(): AppDataStore = object : AppDataStore, PrefsStore by this {}
-interface CustomEmojiDataStore : DataStore
+interface CustomEmojiDataStore : PrefsStore
/** Presents an existing store as [CustomEmojiDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asCustomEmojiDataStore(): CustomEmojiDataStore =
- object : CustomEmojiDataStore, DataStore by this {}
+fun PrefsStore.asCustomEmojiDataStore(): CustomEmojiDataStore = object : CustomEmojiDataStore, PrefsStore by this {}
-interface FilterDataStore : DataStore
+interface FilterDataStore : PrefsStore
/** Presents an existing store as [FilterDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asFilterDataStore(): FilterDataStore =
- object : FilterDataStore, DataStore by this {}
+fun PrefsStore.asFilterDataStore(): FilterDataStore = object : FilterDataStore, PrefsStore by this {}
-interface HomoglyphEncodingDataStore : DataStore
+interface HomoglyphEncodingDataStore : PrefsStore
/** Presents an existing store as [HomoglyphEncodingDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asHomoglyphEncodingDataStore(): HomoglyphEncodingDataStore =
- object : HomoglyphEncodingDataStore, DataStore by this {}
+fun PrefsStore.asHomoglyphEncodingDataStore(): HomoglyphEncodingDataStore =
+ object : HomoglyphEncodingDataStore, PrefsStore by this {}
-interface MapConsentDataStore : DataStore
+interface MapConsentDataStore : PrefsStore
/** Presents an existing store as [MapConsentDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asMapConsentDataStore(): MapConsentDataStore =
- object : MapConsentDataStore, DataStore by this {}
+fun PrefsStore.asMapConsentDataStore(): MapConsentDataStore = object : MapConsentDataStore, PrefsStore by this {}
-interface MapDataStore : DataStore
+interface MapDataStore : PrefsStore
/** Presents an existing store as [MapDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asMapDataStore(): MapDataStore = object : MapDataStore, DataStore by this {}
+fun PrefsStore.asMapDataStore(): MapDataStore = object : MapDataStore, PrefsStore by this {}
-interface MapTileProviderDataStore : DataStore
+interface MapTileProviderDataStore : PrefsStore
/** Presents an existing store as [MapTileProviderDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asMapTileProviderDataStore(): MapTileProviderDataStore =
- object : MapTileProviderDataStore, DataStore by this {}
+fun PrefsStore.asMapTileProviderDataStore(): MapTileProviderDataStore =
+ object : MapTileProviderDataStore, PrefsStore by this {}
-interface MeshDataStore : DataStore
+interface MeshDataStore : PrefsStore
/** Presents an existing store as [MeshDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asMeshDataStore(): MeshDataStore = object : MeshDataStore, DataStore by this {}
+fun PrefsStore.asMeshDataStore(): MeshDataStore = object : MeshDataStore, PrefsStore by this {}
-interface MeshLogDataStore : DataStore
+interface MeshLogDataStore : PrefsStore
/** Presents an existing store as [MeshLogDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asMeshLogDataStore(): MeshLogDataStore =
- object : MeshLogDataStore, DataStore by this {}
+fun PrefsStore.asMeshLogDataStore(): MeshLogDataStore = object : MeshLogDataStore, PrefsStore by this {}
-interface RadioDataStore : DataStore
+interface RadioDataStore : PrefsStore
/** Presents an existing store as [RadioDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asRadioDataStore(): RadioDataStore =
- object : RadioDataStore, DataStore by this {}
+fun PrefsStore.asRadioDataStore(): RadioDataStore = object : RadioDataStore, PrefsStore by this {}
-interface UiDataStore : DataStore
+interface UiDataStore : PrefsStore
/** Presents an existing store as [UiDataStore]; the wrapper adds nothing but identity. */
-fun DataStore.asUiDataStore(): UiDataStore = object : UiDataStore, DataStore by this {}
+fun PrefsStore.asUiDataStore(): UiDataStore = object : UiDataStore, PrefsStore by this {}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/DiscoveryPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/DiscoveryPrefsImpl.kt
index e93bc20b2d..b8ccd37a7a 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/DiscoveryPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/DiscoveryPrefsImpl.kt
@@ -16,10 +16,6 @@
*/
package org.meshtastic.core.prefs.discovery
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -30,6 +26,9 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.UiDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.intPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.DiscoveryPrefs
@Single
@@ -72,10 +71,10 @@ class DiscoveryPrefsImpl(private val dataStore: UiDataStore, dispatchers: Corout
}
companion object {
- private val KEY_DWELL_MINUTES = intPreferencesKey("discovery_dwell_minutes")
- private val KEY_SELECTED_PRESETS = stringPreferencesKey("discovery_selected_presets")
- private val KEY_AI_ENABLED = booleanPreferencesKey("discovery_ai_enabled")
- private val KEY_TOPOLOGY_OVERLAY = booleanPreferencesKey("discovery_topology_overlay")
+ private val KEY_DWELL_MINUTES = intPrefsKey("discovery_dwell_minutes")
+ private val KEY_SELECTED_PRESETS = stringPrefsKey("discovery_selected_presets")
+ private val KEY_AI_ENABLED = booleanPrefsKey("discovery_ai_enabled")
+ private val KEY_TOPOLOGY_OVERLAY = booleanPrefsKey("discovery_topology_overlay")
private const val PRESET_DELIMITER = ","
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt
index 7322dd9b54..c24236188c 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/discovery/MeshBeaconPrefsImpl.kt
@@ -16,8 +16,6 @@
*/
package org.meshtastic.core.prefs.discovery
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
@@ -31,6 +29,7 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.UiDataStore
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.MeshBeaconPrefs
@Single
@@ -64,7 +63,7 @@ class MeshBeaconPrefsImpl(private val dataStore: UiDataStore, dispatchers: Corou
}
private companion object {
- val KEY_STORED_BEACONS = stringPreferencesKey("mesh_beacon_stored_offers")
+ val KEY_STORED_BEACONS = stringPrefsKey("mesh_beacon_stored_offers")
// Newline can never appear in a beacon record (fields are numeric + base64), so it is a safe row separator.
const val RECORD_DELIMITER = "\n"
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
index ae9bb8faf9..1b34e219b4 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.emoji
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.CustomEmojiDataStore
+import org.meshtastic.core.prefs.store.intPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.CustomEmojiPrefs
@Single
@@ -60,7 +59,7 @@ class CustomEmojiPrefsImpl(private val dataStore: CustomEmojiDataStore, dispatch
companion object {
const val KEY_EMOJI_FREQ = "pref_key_custom_emoji_freq"
- val KEY_EMOJI_FREQ_PREF = stringPreferencesKey(KEY_EMOJI_FREQ)
- val KEY_SKIN_TONE_PREF = intPreferencesKey("pref_key_skin_tone")
+ val KEY_EMOJI_FREQ_PREF = stringPrefsKey(KEY_EMOJI_FREQ)
+ val KEY_SKIN_TONE_PREF = intPrefsKey("pref_key_skin_tone")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt
index be5dd819d1..cc64a9ccbf 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.filter
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.FilterDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.stringSetPrefsKey
import org.meshtastic.core.repository.FilterPrefs
@Single
@@ -56,7 +55,7 @@ class FilterPrefsImpl(private val dataStore: FilterDataStore, dispatchers: Corou
const val KEY_FILTER_WORDS = "filter_words"
const val FILTER_PREFS_NAME = "filter-prefs"
- val KEY_FILTER_ENABLED_PREF = booleanPreferencesKey(KEY_FILTER_ENABLED)
- val KEY_FILTER_WORDS_PREF = stringSetPreferencesKey(KEY_FILTER_WORDS)
+ val KEY_FILTER_ENABLED_PREF = booleanPrefsKey(KEY_FILTER_ENABLED)
+ val KEY_FILTER_WORDS_PREF = stringSetPrefsKey(KEY_FILTER_WORDS)
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt
index 7e5693ff9e..7ce0638200 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt
@@ -16,8 +16,6 @@
*/
package org.meshtastic.core.prefs.homoglyph
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -28,6 +26,7 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
import org.meshtastic.core.repository.HomoglyphPrefs
@Single
@@ -44,6 +43,6 @@ class HomoglyphPrefsImpl(private val dataStore: HomoglyphEncodingDataStore, disp
companion object {
const val KEY_ENABLED = "enabled"
- val KEY_ENABLED_PREF = booleanPreferencesKey(KEY_ENABLED)
+ val KEY_ENABLED_PREF = booleanPrefsKey(KEY_ENABLED)
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt
index cf6e630014..bcf87e0329 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt
@@ -16,8 +16,6 @@
*/
package org.meshtastic.core.prefs.map
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
import kotlinx.atomicfu.atomic
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.CoroutineScope
@@ -31,6 +29,7 @@ import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.cachedFlow
import org.meshtastic.core.prefs.di.MapConsentDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
import org.meshtastic.core.repository.MapConsentPrefs
@Single
@@ -41,11 +40,11 @@ class MapConsentPrefsImpl(private val dataStore: MapConsentDataStore, dispatcher
private val consentFlows = atomic(persistentMapOf>>())
override fun shouldReportLocation(nodeNum: Int?): StateFlow = cachedFlow(consentFlows, nodeNum) {
- val key = booleanPreferencesKey(nodeNum.toString())
+ val key = booleanPrefsKey(nodeNum.toString())
dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
}
override fun setShouldReportLocation(nodeNum: Int?, report: Boolean) {
- scope.launch { dataStore.edit { prefs -> prefs[booleanPreferencesKey(nodeNum.toString())] = report } }
+ scope.launch { dataStore.edit { prefs -> prefs[booleanPrefsKey(nodeNum.toString())] = report } }
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
index a500b6f70c..b40d08001a 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
@@ -16,12 +16,6 @@
*/
package org.meshtastic.core.prefs.map
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.doublePreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.longPreferencesKey
-import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -33,6 +27,11 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.MapDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.doublePrefsKey
+import org.meshtastic.core.prefs.store.intPrefsKey
+import org.meshtastic.core.prefs.store.longPrefsKey
+import org.meshtastic.core.prefs.store.stringSetPrefsKey
import org.meshtastic.core.repository.MapCameraPosition
import org.meshtastic.core.repository.MapPrefs
@@ -190,23 +189,23 @@ class MapPrefsImpl(private val dataStore: MapDataStore, dispatchers: CoroutineDi
.first()
companion object {
- val KEY_MAP_STYLE_PREF = intPreferencesKey("map_style_id")
- val KEY_SHOW_ONLY_FAVORITES_PREF = booleanPreferencesKey("show_only_favorites")
- val KEY_SHOW_WAYPOINTS_PREF = booleanPreferencesKey("show_waypoints")
- val KEY_SHOW_PRECISION_CIRCLE_PREF = booleanPreferencesKey("show_precision_circle")
- val KEY_LAST_HEARD_FILTER_PREF = longPreferencesKey("last_heard_filter")
- val KEY_LAST_HEARD_TRACK_FILTER_PREF = longPreferencesKey("last_heard_track_filter")
- val KEY_HIDDEN_LAYER_URLS_PREF = stringSetPreferencesKey("hidden_layer_urls")
- val KEY_NETWORK_MAP_LAYERS_PREF = stringSetPreferencesKey("network_map_layers")
- val KEY_LAYER_OPACITY_PREF = stringSetPreferencesKey("layer_opacity")
- val KEY_ONLY_ONLINE_PREF = booleanPreferencesKey("map_only_online")
- val KEY_ONLY_DIRECT_PREF = booleanPreferencesKey("map_only_direct")
- val KEY_EXCLUDE_MQTT_PREF = booleanPreferencesKey("map_exclude_mqtt")
- val KEY_SHOW_IGNORED_PREF = booleanPreferencesKey("map_show_ignored")
- val KEY_INCLUDE_UNKNOWN_PREF = booleanPreferencesKey("map_include_unknown")
- val KEY_EXCLUDED_ROLES_PREF = stringSetPreferencesKey("map_excluded_roles")
- val KEY_CAMERA_LATITUDE = doublePreferencesKey("camera_latitude")
- val KEY_CAMERA_LONGITUDE = doublePreferencesKey("camera_longitude")
- val KEY_CAMERA_ZOOM = doublePreferencesKey("camera_zoom")
+ val KEY_MAP_STYLE_PREF = intPrefsKey("map_style_id")
+ val KEY_SHOW_ONLY_FAVORITES_PREF = booleanPrefsKey("show_only_favorites")
+ val KEY_SHOW_WAYPOINTS_PREF = booleanPrefsKey("show_waypoints")
+ val KEY_SHOW_PRECISION_CIRCLE_PREF = booleanPrefsKey("show_precision_circle")
+ val KEY_LAST_HEARD_FILTER_PREF = longPrefsKey("last_heard_filter")
+ val KEY_LAST_HEARD_TRACK_FILTER_PREF = longPrefsKey("last_heard_track_filter")
+ val KEY_HIDDEN_LAYER_URLS_PREF = stringSetPrefsKey("hidden_layer_urls")
+ val KEY_NETWORK_MAP_LAYERS_PREF = stringSetPrefsKey("network_map_layers")
+ val KEY_LAYER_OPACITY_PREF = stringSetPrefsKey("layer_opacity")
+ val KEY_ONLY_ONLINE_PREF = booleanPrefsKey("map_only_online")
+ val KEY_ONLY_DIRECT_PREF = booleanPrefsKey("map_only_direct")
+ val KEY_EXCLUDE_MQTT_PREF = booleanPrefsKey("map_exclude_mqtt")
+ val KEY_SHOW_IGNORED_PREF = booleanPrefsKey("map_show_ignored")
+ val KEY_INCLUDE_UNKNOWN_PREF = booleanPrefsKey("map_include_unknown")
+ val KEY_EXCLUDED_ROLES_PREF = stringSetPrefsKey("map_excluded_roles")
+ val KEY_CAMERA_LATITUDE = doublePrefsKey("camera_latitude")
+ val KEY_CAMERA_LONGITUDE = doublePrefsKey("camera_longitude")
+ val KEY_CAMERA_ZOOM = doublePrefsKey("camera_zoom")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt
index 1787ffc618..bed29bac3d 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt
@@ -16,8 +16,6 @@
*/
package org.meshtastic.core.prefs.map
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -28,6 +26,7 @@ import kotlinx.coroutines.flow.stateIn
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.MapTileProviderDataStore
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.MapTileProviderPrefs
@Single
@@ -69,7 +68,7 @@ class MapTileProviderPrefsImpl(private val dataStore: MapTileProviderDataStore,
companion object {
const val KEY_CUSTOM_PROVIDERS = "custom_tile_providers"
const val KEY_SELECTED_CUSTOM_PROVIDER_ID = "selected_custom_tile_provider_id"
- val KEY_CUSTOM_PROVIDERS_PREF = stringPreferencesKey(KEY_CUSTOM_PROVIDERS)
- val KEY_SELECTED_CUSTOM_PROVIDER_ID_PREF = stringPreferencesKey(KEY_SELECTED_CUSTOM_PROVIDER_ID)
+ val KEY_CUSTOM_PROVIDERS_PREF = stringPrefsKey(KEY_CUSTOM_PROVIDERS)
+ val KEY_SELECTED_CUSTOM_PROVIDER_ID_PREF = stringPrefsKey(KEY_SELECTED_CUSTOM_PROVIDER_ID)
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt
index 61a15b0e8b..a8c8a51541 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.mesh
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.atomicfu.atomic
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.CoroutineScope
@@ -34,6 +31,8 @@ import org.meshtastic.core.common.util.normalizeAddress
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.cachedFlow
import org.meshtastic.core.prefs.di.MeshDataStore
+import org.meshtastic.core.prefs.store.intPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.MeshPrefs
@Single
@@ -63,14 +62,14 @@ class MeshPrefsImpl(private val dataStore: MeshDataStore, dispatchers: Coroutine
dataStore.data.first()[KEY_DEVICE_ADDRESS_PREF] ?: NO_DEVICE_SELECTED
override fun getStoreForwardLastRequest(address: String?): StateFlow = cachedFlow(storeForwardFlows, address) {
- val key = intPreferencesKey(storeForwardKey(address))
+ val key = intPrefsKey(storeForwardKey(address))
dataStore.data.map { it[key] ?: 0 }.stateIn(scope, SharingStarted.Eagerly, 0)
}
override fun setStoreForwardLastRequest(address: String?, timestamp: Int) {
scope.launch {
dataStore.edit { prefs ->
- val key = intPreferencesKey(storeForwardKey(address))
+ val key = intPrefsKey(storeForwardKey(address))
if (timestamp <= 0) {
prefs.remove(key)
} else {
@@ -83,7 +82,7 @@ class MeshPrefsImpl(private val dataStore: MeshDataStore, dispatchers: Coroutine
private fun storeForwardKey(address: String?): String = "store-forward-last-request-${normalizeAddress(address)}"
companion object {
- val KEY_DEVICE_ADDRESS_PREF = stringPreferencesKey("device_address")
+ val KEY_DEVICE_ADDRESS_PREF = stringPrefsKey("device_address")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt
index f2d8868410..74eee1238f 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.meshlog
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -30,6 +27,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.MeshLogDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.intPrefsKey
import org.meshtastic.core.repository.MeshLogCleanupPolicy
import org.meshtastic.core.repository.MeshLogPrefs
@@ -68,7 +67,7 @@ class MeshLogPrefsImpl(private val dataStore: MeshLogDataStore, dispatchers: Cor
const val DEFAULT_RETENTION_DAYS = 30
const val DEFAULT_LOGGING_ENABLED = true
- val KEY_RETENTION_DAYS_PREF = intPreferencesKey(RETENTION_DAYS_KEY)
- val KEY_LOGGING_ENABLED_PREF = booleanPreferencesKey(LOGGING_ENABLED_KEY)
+ val KEY_RETENTION_DAYS_PREF = intPrefsKey(RETENTION_DAYS_KEY)
+ val KEY_LOGGING_ENABLED_PREF = booleanPrefsKey(LOGGING_ENABLED_KEY)
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsImpl.kt
index cbf73b407d..df44ee4b2d 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.notification
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.UiDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.NotificationPrefs
@Single
@@ -116,10 +115,10 @@ class NotificationPrefsImpl(private val dataStore: UiDataStore, dispatchers: Cor
private fun parseOptInIds(csv: String?): List =
csv?.split(',')?.mapNotNull { it.toIntOrNull() }?.distinct() ?: emptyList()
- private val KEY_MESSAGES_ENABLED = booleanPreferencesKey("notif_messages_enabled")
- private val KEY_NODE_EVENTS_ENABLED = booleanPreferencesKey("notif_node_events_enabled")
- private val KEY_NODE_EVENTS_AUTO_DISABLED = booleanPreferencesKey("notif_node_events_auto_disabled_event")
- private val KEY_LOW_BATTERY_ENABLED = booleanPreferencesKey("notif_low_battery_enabled")
- private val KEY_GEOFENCE_ALERT_OPT_INS = stringPreferencesKey("notif_geofence_alert_opt_ins")
+ private val KEY_MESSAGES_ENABLED = booleanPrefsKey("notif_messages_enabled")
+ private val KEY_NODE_EVENTS_ENABLED = booleanPrefsKey("notif_node_events_enabled")
+ private val KEY_NODE_EVENTS_AUTO_DISABLED = booleanPrefsKey("notif_node_events_auto_disabled_event")
+ private val KEY_LOW_BATTERY_ENABLED = booleanPrefsKey("notif_low_battery_enabled")
+ private val KEY_GEOFENCE_ALERT_OPT_INS = stringPrefsKey("notif_geofence_alert_opt_ins")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt
index 6f4cf80ebc..2a7e0f506e 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt
@@ -16,8 +16,6 @@
*/
package org.meshtastic.core.prefs.radio
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -28,6 +26,7 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.RadioDataStore
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.RadioPrefs
@Single
@@ -65,7 +64,7 @@ class RadioPrefsImpl(private val dataStore: RadioDataStore, dispatchers: Corouti
}
companion object {
- val KEY_DEV_ADDR_PREF = stringPreferencesKey("devAddr2")
- val KEY_DEV_NAME_PREF = stringPreferencesKey("devName")
+ val KEY_DEV_ADDR_PREF = stringPrefsKey("devAddr2")
+ val KEY_DEV_NAME_PREF = stringPrefsKey("devName")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/store/PrefsStore.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/store/PrefsStore.kt
new file mode 100644
index 0000000000..304d68d0a9
--- /dev/null
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/store/PrefsStore.kt
@@ -0,0 +1,101 @@
+/*
+ * 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 .
+ */
+package org.meshtastic.core.prefs.store
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * The value shapes a [PrefsKey] can address — every case a real preference in this module actually uses. `internal`:
+ * only the two platform-specific [PrefsStore] implementations (nonWebMain's `DataStorePrefsStore`, wasmJsMain's
+ * `LocalStoragePrefsStore`) ever need to switch on it; every `*PrefsImpl` consumer only ever calls
+ * [PrefsSnapshot.get]/[PrefsSnapshot.Editor.set] generically and never inspects a key's shape.
+ */
+internal enum class PrefsKeyType {
+ BOOLEAN,
+ INT,
+ LONG,
+ DOUBLE,
+ STRING,
+ STRING_SET,
+}
+
+/**
+ * A single, platform-neutral key into a [PrefsStore]. Mirrors `androidx.datastore.preferences.core.Preferences.Key`
+ * closely enough that porting a `*PrefsImpl` off DataStore is a mechanical rename, not a rewrite: swap the
+ * `androidx.datastore.preferences.core.*PreferencesKey` import/call for the matching `*PrefsKey` factory below, and
+ * `Preferences`/`MutablePreferences` for [PrefsSnapshot]/[PrefsSnapshot.Editor].
+ *
+ * Equality/hashing is by [name] alone (matching `Preferences.Key`), so two keys constructed for the same name — even
+ * from different call sites, e.g. a dynamic per-node key built fresh on every read and write — address the same
+ * underlying value.
+ */
+class PrefsKey internal constructor(internal val name: String, internal val type: PrefsKeyType) {
+ override fun equals(other: Any?): Boolean = other is PrefsKey<*> && name == other.name
+
+ override fun hashCode(): Int = name.hashCode()
+
+ override fun toString(): String = name
+}
+
+fun booleanPrefsKey(name: String): PrefsKey = PrefsKey(name, PrefsKeyType.BOOLEAN)
+
+fun intPrefsKey(name: String): PrefsKey = PrefsKey(name, PrefsKeyType.INT)
+
+fun longPrefsKey(name: String): PrefsKey = PrefsKey(name, PrefsKeyType.LONG)
+
+fun doublePrefsKey(name: String): PrefsKey = PrefsKey(name, PrefsKeyType.DOUBLE)
+
+fun stringPrefsKey(name: String): PrefsKey = PrefsKey(name, PrefsKeyType.STRING)
+
+fun stringSetPrefsKey(name: String): PrefsKey> = PrefsKey(name, PrefsKeyType.STRING_SET)
+
+/** Read-only view of a [PrefsStore]'s current values. Mirrors `androidx.datastore.preferences.core.Preferences`. */
+interface PrefsSnapshot {
+ operator fun get(key: PrefsKey): T?
+
+ operator fun contains(key: PrefsKey<*>): Boolean
+
+ /**
+ * Mutable view passed to [PrefsStore.edit]'s transform block. Mirrors `MutablePreferences` — including reading back
+ * values already written earlier in the same transform, which several `*PrefsImpl` read-modify-write call sites
+ * rely on (e.g. bumping an insertion-ordered CSV, or defaulting a value only if absent).
+ */
+ interface Editor : PrefsSnapshot {
+ operator fun set(key: PrefsKey, value: T)
+
+ fun remove(key: PrefsKey)
+ }
+}
+
+/**
+ * Platform-neutral replacement for `DataStore`. `androidx.datastore.preferences` itself publishes no
+ * wasmJs (or even plain JS) variant at any version — the `Preferences` type doesn't exist for that target — so this
+ * module can't reference it from commonMain at all. Two implementations exist:
+ * - nonWebMain's `DataStorePrefsStore` — a thin wrapper over a real `DataStore`; android/jvm/iOS keep
+ * using the real DataStore machinery underneath, unchanged.
+ * - wasmJsMain's `LocalStoragePrefsStore` — backed by the browser's `localStorage`, a synchronous, built-in key-value
+ * string store. Unlike core:database's OPFS story, no Worker or npm dependency is needed here at all.
+ *
+ * `edit`'s [transform] is deliberately non-suspend (androidx's `DataStore.edit`'s is `suspend`) and returns `Unit`
+ * (androidx's returns the resulting `Preferences`) — no call site in this module uses either capability, and keeping it
+ * synchronous means no suspension point can interleave between a transform's reads and its writes on any platform.
+ */
+interface PrefsStore {
+ val data: Flow
+
+ suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit)
+}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/tak/TakPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/tak/TakPrefsImpl.kt
index a80cb215e8..fa75f0f1e2 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/tak/TakPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/tak/TakPrefsImpl.kt
@@ -16,9 +16,6 @@
*/
package org.meshtastic.core.prefs.tak
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.UiDataStore
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.intPrefsKey
import org.meshtastic.core.repository.TakPrefs
@Single(binds = [TakPrefs::class])
@@ -57,8 +56,8 @@ class TakPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDis
}
companion object {
- val KEY_TAK_SERVER_ENABLED = booleanPreferencesKey("tak_server_enabled")
- val KEY_TAK_MESH_TO_COT = booleanPreferencesKey("tak_mesh_to_cot")
- val KEY_TAK_SERVER_CHANNEL = intPreferencesKey("tak_server_channel")
+ val KEY_TAK_SERVER_ENABLED = booleanPrefsKey("tak_server_enabled")
+ val KEY_TAK_MESH_TO_COT = booleanPrefsKey("tak_mesh_to_cot")
+ val KEY_TAK_SERVER_CHANNEL = intPrefsKey("tak_server_channel")
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPreferences.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPreferences.kt
index c4f3b19233..adf74ee6a6 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPreferences.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPreferences.kt
@@ -16,9 +16,9 @@
*/
package org.meshtastic.core.prefs.ui
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
import org.meshtastic.core.model.NodeListDensity
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
/**
* DataStore preference keys for node list layout configuration. Key strings are used directly by DataStore — do not
@@ -40,15 +40,15 @@ enum class NodeListLayoutPreferences(val key: String, val defaultBoolean: Boolea
private const val DENSITY_KEY = "node-list-density"
val DEFAULT_DENSITY = NodeListDensity.COMPLETE.name
- val KEY_DENSITY = stringPreferencesKey(DENSITY_KEY)
- val KEY_SHOW_POWER = booleanPreferencesKey(SHOULD_SHOW_POWER.key)
- val KEY_SHOW_LAST_HEARD = booleanPreferencesKey(SHOULD_SHOW_LAST_HEARD.key)
- val KEY_LAST_HEARD_RELATIVE = booleanPreferencesKey(LAST_HEARD_IS_RELATIVE.key)
- val KEY_SHOW_LOCATION = booleanPreferencesKey(SHOULD_SHOW_LOCATION.key)
- val KEY_SHOW_HOPS = booleanPreferencesKey(SHOULD_SHOW_HOPS.key)
- val KEY_SHOW_SIGNAL = booleanPreferencesKey(SHOULD_SHOW_SIGNAL.key)
- val KEY_SHOW_CHANNEL = booleanPreferencesKey(SHOULD_SHOW_CHANNEL.key)
- val KEY_SHOW_ROLE = booleanPreferencesKey(SHOULD_SHOW_ROLE.key)
- val KEY_SHOW_TELEMETRY = booleanPreferencesKey(SHOULD_SHOW_TELEMETRY.key)
+ val KEY_DENSITY = stringPrefsKey(DENSITY_KEY)
+ val KEY_SHOW_POWER = booleanPrefsKey(SHOULD_SHOW_POWER.key)
+ val KEY_SHOW_LAST_HEARD = booleanPrefsKey(SHOULD_SHOW_LAST_HEARD.key)
+ val KEY_LAST_HEARD_RELATIVE = booleanPrefsKey(LAST_HEARD_IS_RELATIVE.key)
+ val KEY_SHOW_LOCATION = booleanPrefsKey(SHOULD_SHOW_LOCATION.key)
+ val KEY_SHOW_HOPS = booleanPrefsKey(SHOULD_SHOW_HOPS.key)
+ val KEY_SHOW_SIGNAL = booleanPrefsKey(SHOULD_SHOW_SIGNAL.key)
+ val KEY_SHOW_CHANNEL = booleanPrefsKey(SHOULD_SHOW_CHANNEL.key)
+ val KEY_SHOW_ROLE = booleanPrefsKey(SHOULD_SHOW_ROLE.key)
+ val KEY_SHOW_TELEMETRY = booleanPrefsKey(SHOULD_SHOW_TELEMETRY.key)
}
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt
index 7e7e72a258..aee605e5d6 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt
@@ -16,11 +16,6 @@
*/
package org.meshtastic.core.prefs.ui
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.atomicfu.atomic
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.CoroutineScope
@@ -35,6 +30,10 @@ import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.prefs.cachedFlow
import org.meshtastic.core.prefs.di.UiDataStore
+import org.meshtastic.core.prefs.store.PrefsSnapshot
+import org.meshtastic.core.prefs.store.booleanPrefsKey
+import org.meshtastic.core.prefs.store.intPrefsKey
+import org.meshtastic.core.prefs.store.stringPrefsKey
import org.meshtastic.core.repository.UiPrefs
@Single
@@ -209,12 +208,12 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp
override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow =
cachedFlow(provideNodeLocationFlows, nodeNum) {
- val key = booleanPreferencesKey(provideLocationKey(nodeNum))
+ val key = booleanPrefsKey(provideLocationKey(nodeNum))
dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
}
override fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean) {
- scope.launch { dataStore.edit { it[booleanPreferencesKey(provideLocationKey(nodeNum))] = provide } }
+ scope.launch { dataStore.edit { it[booleanPrefsKey(provideLocationKey(nodeNum))] = provide } }
}
private fun provideLocationKey(nodeNum: Int) = "provide-location-$nodeNum"
@@ -312,34 +311,34 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp
}
companion object {
- val KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF = booleanPreferencesKey("has_shown_not_paired_warning")
- val KEY_SHOW_QUICK_CHAT_PREF = booleanPreferencesKey("show-quick-chat")
- val KEY_SHOW_FULL_MESSAGE_TIMESTAMPS = booleanPreferencesKey("show-full-message-timestamps")
- val KEY_EVENT_THEME_ENABLED = booleanPreferencesKey("event-theme-enabled")
+ val KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF = booleanPrefsKey("has_shown_not_paired_warning")
+ val KEY_SHOW_QUICK_CHAT_PREF = booleanPrefsKey("show-quick-chat")
+ val KEY_SHOW_FULL_MESSAGE_TIMESTAMPS = booleanPrefsKey("show-full-message-timestamps")
+ val KEY_EVENT_THEME_ENABLED = booleanPrefsKey("event-theme-enabled")
- val KEY_APP_INTRO_COMPLETED = booleanPreferencesKey("app_intro_completed")
- val KEY_THEME = intPreferencesKey("theme")
- private val KEY_UNITS_OVERRIDE = intPreferencesKey("units_override")
- val KEY_LOCALE = stringPreferencesKey("locale")
- val KEY_NODE_SORT = intPreferencesKey("node-sort-option")
- val KEY_INCLUDE_UNKNOWN = booleanPreferencesKey("include-unknown")
- val KEY_EXCLUDE_INFRASTRUCTURE = booleanPreferencesKey("exclude-infrastructure")
- val KEY_ONLY_ONLINE = booleanPreferencesKey("only-online")
- val KEY_ONLY_DIRECT = booleanPreferencesKey("only-direct")
- val KEY_SHOW_IGNORED = booleanPreferencesKey("show-ignored")
- val KEY_EXCLUDE_MQTT = booleanPreferencesKey("exclude-mqtt")
- val KEY_BLE_AUTO_SCAN = booleanPreferencesKey("ble-auto-scan")
- val KEY_NETWORK_AUTO_SCAN = booleanPreferencesKey("network-auto-scan")
- val KEY_SELECTED_CONNECTION_TRANSPORT = stringPreferencesKey("selected-connection-transport")
- val KEY_FIRMWARE_UPDATE_NOTIFICATION_KEYS = stringPreferencesKey("firmware-update-notification-keys")
- val KEY_SHOW_BLE_TRANSPORT = booleanPreferencesKey("show-ble-transport")
- val KEY_SHOW_NETWORK_TRANSPORT = booleanPreferencesKey("show-network-transport")
- val KEY_SHOW_USB_TRANSPORT = booleanPreferencesKey("show-usb-transport")
+ val KEY_APP_INTRO_COMPLETED = booleanPrefsKey("app_intro_completed")
+ val KEY_THEME = intPrefsKey("theme")
+ private val KEY_UNITS_OVERRIDE = intPrefsKey("units_override")
+ val KEY_LOCALE = stringPrefsKey("locale")
+ val KEY_NODE_SORT = intPrefsKey("node-sort-option")
+ val KEY_INCLUDE_UNKNOWN = booleanPrefsKey("include-unknown")
+ val KEY_EXCLUDE_INFRASTRUCTURE = booleanPrefsKey("exclude-infrastructure")
+ val KEY_ONLY_ONLINE = booleanPrefsKey("only-online")
+ val KEY_ONLY_DIRECT = booleanPrefsKey("only-direct")
+ val KEY_SHOW_IGNORED = booleanPrefsKey("show-ignored")
+ val KEY_EXCLUDE_MQTT = booleanPrefsKey("exclude-mqtt")
+ val KEY_BLE_AUTO_SCAN = booleanPrefsKey("ble-auto-scan")
+ val KEY_NETWORK_AUTO_SCAN = booleanPrefsKey("network-auto-scan")
+ val KEY_SELECTED_CONNECTION_TRANSPORT = stringPrefsKey("selected-connection-transport")
+ val KEY_FIRMWARE_UPDATE_NOTIFICATION_KEYS = stringPrefsKey("firmware-update-notification-keys")
+ val KEY_SHOW_BLE_TRANSPORT = booleanPrefsKey("show-ble-transport")
+ val KEY_SHOW_NETWORK_TRANSPORT = booleanPrefsKey("show-network-transport")
+ val KEY_SHOW_USB_TRANSPORT = booleanPrefsKey("show-usb-transport")
private const val MAX_FIRMWARE_UPDATE_NOTIFICATION_KEYS = 100
private fun parseDeviceType(name: String): DeviceType? = DeviceType.entries.firstOrNull { it.name == name }
- private fun legacySelectedConnectionTransport(preferences: Preferences): DeviceType? {
+ private fun legacySelectedConnectionTransport(preferences: PrefsSnapshot): DeviceType? {
val hasLegacyTransportPreference =
KEY_SHOW_BLE_TRANSPORT in preferences ||
KEY_SHOW_NETWORK_TRANSPORT in preferences ||
diff --git a/core/prefs/src/nonWebMain/kotlin/org/meshtastic/core/prefs/store/DataStorePrefsStore.kt b/core/prefs/src/nonWebMain/kotlin/org/meshtastic/core/prefs/store/DataStorePrefsStore.kt
new file mode 100644
index 0000000000..480d901d43
--- /dev/null
+++ b/core/prefs/src/nonWebMain/kotlin/org/meshtastic/core/prefs/store/DataStorePrefsStore.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 .
+ */
+package org.meshtastic.core.prefs.store
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.MutablePreferences
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.doublePreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.core.stringSetPreferencesKey
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Adapts a real `DataStore` (android/jvm/iOS only — `androidx.datastore.preferences` has no wasmJs
+ * variant) to the platform-neutral [PrefsStore]. Every `CorePrefsAndroidModule`/`DesktopPlatformModule` call site that
+ * used to hand a `DataStore` straight to an `asXDataStore()` wrapper now inserts this adapter first:
+ * `store(...).asPrefsStore().asXDataStore()`.
+ */
+fun DataStore.asPrefsStore(): PrefsStore = DataStorePrefsStore(this)
+
+private class DataStorePrefsStore(private val delegate: DataStore) : PrefsStore {
+ override val data: Flow = delegate.data.map { DataStorePrefsSnapshot(it) }
+
+ override suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit) {
+ delegate.edit { prefs -> transform(DataStorePrefsEditor(prefs)) }
+ }
+}
+
+private class DataStorePrefsSnapshot(private val prefs: Preferences) : PrefsSnapshot {
+ override fun get(key: PrefsKey): T? = prefs[key.toPreferencesKey()]
+
+ override fun contains(key: PrefsKey<*>): Boolean = key.toRawPreferencesKey() in prefs
+}
+
+private class DataStorePrefsEditor(private val prefs: MutablePreferences) : PrefsSnapshot.Editor {
+ override fun get(key: PrefsKey): T? = prefs[key.toPreferencesKey()]
+
+ override fun contains(key: PrefsKey<*>): Boolean = key.toRawPreferencesKey() in prefs
+
+ override fun set(key: PrefsKey, value: T) {
+ prefs[key.toPreferencesKey()] = value
+ }
+
+ override fun remove(key: PrefsKey) {
+ prefs.remove(key.toPreferencesKey())
+ }
+}
+
+private fun PrefsKey<*>.toRawPreferencesKey(): Preferences.Key<*> = when (type) {
+ PrefsKeyType.BOOLEAN -> booleanPreferencesKey(name)
+ PrefsKeyType.INT -> intPreferencesKey(name)
+ PrefsKeyType.LONG -> longPreferencesKey(name)
+ PrefsKeyType.DOUBLE -> doublePreferencesKey(name)
+ PrefsKeyType.STRING -> stringPreferencesKey(name)
+ PrefsKeyType.STRING_SET -> stringSetPreferencesKey(name)
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun PrefsKey.toPreferencesKey(): Preferences.Key = toRawPreferencesKey() as Preferences.Key
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt
similarity index 95%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt
index 13174ff612..a7e34176db 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt
@@ -26,6 +26,7 @@ import okio.FileSystem
import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.asFilterDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import org.meshtastic.core.repository.FilterPrefs
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -55,7 +56,7 @@ class FilterPrefsTest {
produceFile = { tmpDir / "test.preferences_pb" },
)
dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
- filterPrefs = FilterPrefsImpl(dataStore.asFilterDataStore(), dispatchers)
+ filterPrefs = FilterPrefsImpl(dataStore.asPrefsStore().asFilterDataStore(), dispatchers)
}
@AfterTest
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt
similarity index 96%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt
index c6c0f05951..5d07259e70 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/map/MapPrefsImplTest.kt
@@ -27,6 +27,7 @@ import okio.FileSystem
import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.asMapDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import org.meshtastic.core.repository.MapCameraPosition
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -54,7 +55,7 @@ class MapPrefsImplTest {
)
prefs =
MapPrefsImpl(
- dataStore.asMapDataStore(),
+ dataStore.asPrefsStore().asMapDataStore(),
CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher),
)
}
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImplTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImplTest.kt
similarity index 93%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImplTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImplTest.kt
index 3779e31ef0..b0aefd6b3b 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImplTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImplTest.kt
@@ -19,7 +19,6 @@ package org.meshtastic.core.prefs.mesh
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
@@ -32,6 +31,7 @@ import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.MeshDataStore
import org.meshtastic.core.prefs.di.asMeshDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -68,9 +68,9 @@ class MeshPrefsImplTest {
@Test
fun `await device address waits for persisted data instead of returning the flow default`() = testScope.runTest {
val persistedAddress = "xAA:BB:CC:DD:EE:FF"
- dataStore.edit { preferences -> preferences[MeshPrefsImpl.KEY_DEVICE_ADDRESS_PREF] = persistedAddress }
+ dataStore.asPrefsStore().edit { editor -> editor[MeshPrefsImpl.KEY_DEVICE_ADDRESS_PREF] = persistedAddress }
val loadGate = CompletableDeferred()
- val delegate = dataStore.asMeshDataStore()
+ val delegate = dataStore.asPrefsStore().asMeshDataStore()
val delayedDataStore =
object : MeshDataStore by delegate {
override val data = delegate.data.onStart { loadGate.await() }
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt
similarity index 98%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt
index 2080402186..7a46620860 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt
@@ -26,6 +26,7 @@ import okio.FileSystem
import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.asUiDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import org.meshtastic.core.repository.NotificationPrefs
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -55,7 +56,7 @@ class NotificationPrefsTest {
produceFile = { tmpDir / "test.preferences_pb" },
)
dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
- notificationPrefs = NotificationPrefsImpl(dataStore.asUiDataStore(), dispatchers)
+ notificationPrefs = NotificationPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
}
@AfterTest
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/tak/TakPrefsTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/tak/TakPrefsTest.kt
similarity index 96%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/tak/TakPrefsTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/tak/TakPrefsTest.kt
index 496e2695c3..1ebb78f2d9 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/tak/TakPrefsTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/tak/TakPrefsTest.kt
@@ -26,6 +26,7 @@ import okio.FileSystem
import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.asUiDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import org.meshtastic.core.repository.TakPrefs
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -54,7 +55,7 @@ class TakPrefsTest {
produceFile = { tmpDir / "test.preferences_pb" },
)
dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
- takPrefs = TakPrefsImpl(dataStore.asUiDataStore(), dispatchers)
+ takPrefs = TakPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
}
@AfterTest
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPrefsTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPrefsTest.kt
similarity index 97%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPrefsTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPrefsTest.kt
index c75e659a78..1a0d6eede7 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPrefsTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPrefsTest.kt
@@ -27,6 +27,7 @@ import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.NodeListDensity
import org.meshtastic.core.prefs.di.asUiDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -52,7 +53,7 @@ class NodeListLayoutPrefsTest {
produceFile = { tmpDir / "test.preferences_pb" },
)
val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
- prefs = UiPrefsImpl(dataStore.asUiDataStore(), dispatchers)
+ prefs = UiPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
}
@AfterTest
diff --git a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImplTest.kt b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImplTest.kt
similarity index 92%
rename from core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImplTest.kt
rename to core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImplTest.kt
index 084cac9a40..ac9c411f2b 100644
--- a/core/prefs/src/commonTest/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImplTest.kt
+++ b/core/prefs/src/nonWebTest/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImplTest.kt
@@ -19,7 +19,6 @@ package org.meshtastic.core.prefs.ui
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestDispatcher
@@ -31,6 +30,7 @@ import okio.Path
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.prefs.di.asUiDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@@ -59,7 +59,7 @@ class UiPrefsImplTest {
produceFile = { tmpDir / "test.preferences_pb" },
)
val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
- prefs = UiPrefsImpl(dataStore.asUiDataStore(), dispatchers)
+ prefs = UiPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
}
@AfterTest
@@ -70,7 +70,7 @@ class UiPrefsImplTest {
@Test
fun `explicit selected connection transport wins over legacy booleans`() = testScope.runTest {
- dataStore.edit {
+ dataStore.asPrefsStore().edit {
it[UiPrefsImpl.KEY_SELECTED_CONNECTION_TRANSPORT] = DeviceType.USB.name
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = true
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
@@ -82,7 +82,7 @@ class UiPrefsImplTest {
@Test
fun `invalid selected connection transport falls back to legacy booleans`() = testScope.runTest {
- dataStore.edit {
+ dataStore.asPrefsStore().edit {
it[UiPrefsImpl.KEY_SELECTED_CONNECTION_TRANSPORT] = "WIFI"
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = false
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
@@ -98,7 +98,7 @@ class UiPrefsImplTest {
@Test
fun `legacy selected connection transport defaults to BLE when all transports are visible`() = testScope.runTest {
- dataStore.edit {
+ dataStore.asPrefsStore().edit {
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = true
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
it[UiPrefsImpl.KEY_SHOW_USB_TRANSPORT] = true
@@ -109,7 +109,7 @@ class UiPrefsImplTest {
@Test
fun `legacy selected connection transport chooses TCP when BLE is hidden`() = testScope.runTest {
- dataStore.edit {
+ dataStore.asPrefsStore().edit {
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = false
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
it[UiPrefsImpl.KEY_SHOW_USB_TRANSPORT] = true
@@ -120,7 +120,7 @@ class UiPrefsImplTest {
@Test
fun `legacy selected connection transport chooses USB when only USB is visible`() = testScope.runTest {
- dataStore.edit {
+ dataStore.asPrefsStore().edit {
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = false
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = false
it[UiPrefsImpl.KEY_SHOW_USB_TRANSPORT] = true
@@ -137,7 +137,8 @@ class UiPrefsImplTest {
fun `full message timestamps persist when enabled`() = testScope.runTest {
prefs.setShowFullMessageTimestamps(true)
- val stored = dataStore.data.first { it[UiPrefsImpl.KEY_SHOW_FULL_MESSAGE_TIMESTAMPS] == true }
+ val stored =
+ dataStore.asPrefsStore().data.first { it[UiPrefsImpl.KEY_SHOW_FULL_MESSAGE_TIMESTAMPS] == true }
assertTrue(prefs.showFullMessageTimestamps.value)
assertEquals(true, stored[UiPrefsImpl.KEY_SHOW_FULL_MESSAGE_TIMESTAMPS])
}
diff --git a/core/prefs/src/wasmJsMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsWasmJsModule.kt b/core/prefs/src/wasmJsMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsWasmJsModule.kt
new file mode 100644
index 0000000000..b53a01a28d
--- /dev/null
+++ b/core/prefs/src/wasmJsMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsWasmJsModule.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 .
+ */
+package org.meshtastic.core.prefs.di
+
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+import org.meshtastic.core.prefs.store.LocalStoragePrefsStore
+
+/**
+ * Koin module providing wasmJs [org.meshtastic.core.prefs.store.PrefsStore] instances for each preference domain,
+ * backed by the browser's `localStorage`. Mirrors `CorePrefsAndroidModule`/desktopApp's `DesktopPlatformModule` — one
+ * singleton store per domain, same `_ds`-suffixed names — minus the `SharedPreferencesMigration` (nothing to migrate
+ * from on web) and the `CoroutineScope`/dispatcher plumbing (localStorage reads and writes are synchronous).
+ *
+ * Not yet registered anywhere: like core:database's `SingleDatabaseProvider`, nothing on wasmJs composes a Koin graph
+ * yet (no `webApp` module exists in this repo pass) — a future one wires this in explicitly, the same way
+ * `CorePrefsAndroidModule` is listed by name in androidApp's `AppKoinModule` despite already sitting inside
+ * `CorePrefsModule`'s `@ComponentScan("org.meshtastic.core.prefs")`.
+ */
+@Suppress("TooManyFunctions")
+@Module
+class CorePrefsWasmJsModule {
+ @Single
+ fun provideAnalyticsDataStore(): AnalyticsDataStore = LocalStoragePrefsStore("analytics_ds").asAnalyticsDataStore()
+
+ @Single
+ fun provideHomoglyphEncodingDataStore(): HomoglyphEncodingDataStore =
+ LocalStoragePrefsStore("homoglyph_encoding_ds").asHomoglyphEncodingDataStore()
+
+ @Single fun provideAppDataStore(): AppDataStore = LocalStoragePrefsStore("app_ds").asAppDataStore()
+
+ @Single
+ fun provideCustomEmojiDataStore(): CustomEmojiDataStore =
+ LocalStoragePrefsStore("custom_emoji_ds").asCustomEmojiDataStore()
+
+ @Single fun provideMapDataStore(): MapDataStore = LocalStoragePrefsStore("map_ds").asMapDataStore()
+
+ @Single
+ fun provideMapConsentDataStore(): MapConsentDataStore =
+ LocalStoragePrefsStore("map_consent_ds").asMapConsentDataStore()
+
+ @Single
+ fun provideMapTileProviderDataStore(): MapTileProviderDataStore =
+ LocalStoragePrefsStore("map_tile_provider_ds").asMapTileProviderDataStore()
+
+ @Single fun provideMeshDataStore(): MeshDataStore = LocalStoragePrefsStore("mesh_ds").asMeshDataStore()
+
+ @Single fun provideRadioDataStore(): RadioDataStore = LocalStoragePrefsStore("radio_ds").asRadioDataStore()
+
+ @Single fun provideUiDataStore(): UiDataStore = LocalStoragePrefsStore("ui_ds").asUiDataStore()
+
+ @Single fun provideMeshLogDataStore(): MeshLogDataStore = LocalStoragePrefsStore("meshlog_ds").asMeshLogDataStore()
+
+ @Single fun provideFilterDataStore(): FilterDataStore = LocalStoragePrefsStore("filter_ds").asFilterDataStore()
+}
diff --git a/core/prefs/src/wasmJsMain/kotlin/org/meshtastic/core/prefs/store/LocalStoragePrefsStore.kt b/core/prefs/src/wasmJsMain/kotlin/org/meshtastic/core/prefs/store/LocalStoragePrefsStore.kt
new file mode 100644
index 0000000000..d3785a5795
--- /dev/null
+++ b/core/prefs/src/wasmJsMain/kotlin/org/meshtastic/core/prefs/store/LocalStoragePrefsStore.kt
@@ -0,0 +1,97 @@
+/*
+ * 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 .
+ */
+package org.meshtastic.core.prefs.store
+
+import kotlinx.browser.localStorage
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Backed by the browser's `localStorage` — a synchronous, built-in key-value string store, so (unlike core:database's
+ * OPFS story) no Worker or npm dependency is needed here at all.
+ *
+ * Every domain's store gets its own [namespace] prefix on every localStorage key, matching the file-per-domain
+ * isolation the real `DataStore` instances get on android/jvm/iOS (see CorePrefsAndroidModule /
+ * DesktopPlatformModule's per-domain `fileName`).
+ *
+ * Reads are live: [LocalStorageSnapshot] holds no cached state, it re-reads `localStorage` on every
+ * [PrefsSnapshot.get]/[PrefsSnapshot.contains] call. [revision] exists purely to give [data] a *new* emission after
+ * every [edit] — `LocalStorageSnapshot` has no `equals` override, so each freshly-constructed instance is unequal by
+ * reference to the last, which is exactly what [MutableStateFlow] needs to avoid conflating the update away.
+ *
+ * [DEFERRED]: same-tab writes are reflected immediately (every `edit()` publishes a new revision, and every read goes
+ * straight to localStorage), but a write from another browser tab is not observed — that would need a
+ * `window.onstorage` listener re-publishing a revision when this namespace's keys change, not attempted in this pass.
+ */
+internal class LocalStoragePrefsStore(private val namespace: String) : PrefsStore {
+ private val revision = MutableStateFlow(LocalStorageSnapshot(namespace))
+
+ override val data: Flow = revision
+
+ override suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit) {
+ transform(LocalStorageEditor(namespace))
+ revision.value = LocalStorageSnapshot(namespace)
+ }
+}
+
+private class LocalStorageSnapshot(private val namespace: String) : PrefsSnapshot {
+ override fun get(key: PrefsKey): T? = decode(localStorage.getItem(storageKey(namespace, key)), key.type)
+
+ override fun contains(key: PrefsKey<*>): Boolean = localStorage.getItem(storageKey(namespace, key)) != null
+}
+
+private class LocalStorageEditor(private val namespace: String) : PrefsSnapshot.Editor {
+ override fun get(key: PrefsKey): T? = decode(localStorage.getItem(storageKey(namespace, key)), key.type)
+
+ override fun contains(key: PrefsKey<*>): Boolean = localStorage.getItem(storageKey(namespace, key)) != null
+
+ override fun set(key: PrefsKey, value: T) {
+ localStorage.setItem(storageKey(namespace, key), encode(value, key.type))
+ }
+
+ override fun remove(key: PrefsKey) {
+ localStorage.removeItem(storageKey(namespace, key))
+ }
+}
+
+private fun storageKey(namespace: String, key: PrefsKey<*>): String = "$namespace:${key.name}"
+
+// Matches the '|'-joined convention UiPrefsImpl's own firmwareUpdateNotificationKeys already uses for a
+// Set-shaped preference — reusing it here rather than inventing a new serialization format. As there, a set
+// element containing '|' itself would corrupt round-tripping; no current preference stores one.
+private const val SET_DELIMITER = "|"
+
+@Suppress("UNCHECKED_CAST")
+private fun decode(raw: String?, type: PrefsKeyType): T? {
+ if (raw == null) return null
+ val value: Any =
+ when (type) {
+ PrefsKeyType.BOOLEAN -> raw.toBooleanStrictOrNull() ?: return null
+ PrefsKeyType.INT -> raw.toIntOrNull() ?: return null
+ PrefsKeyType.LONG -> raw.toLongOrNull() ?: return null
+ PrefsKeyType.DOUBLE -> raw.toDoubleOrNull() ?: return null
+ PrefsKeyType.STRING -> raw
+ PrefsKeyType.STRING_SET -> raw.split(SET_DELIMITER).filter(String::isNotEmpty).toSet()
+ }
+ return value as T
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun encode(value: T, type: PrefsKeyType): String = when (type) {
+ PrefsKeyType.STRING_SET -> (value as Set).joinToString(SET_DELIMITER)
+ else -> value.toString()
+}
diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts
index ad6f61a88f..c8dc4d2624 100644
--- a/core/repository/build.gradle.kts
+++ b/core/repository/build.gradle.kts
@@ -23,6 +23,13 @@ plugins {
kotlin {
android { withHostTest {} }
+ // Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable). Unlike
+ // core:ble/core:database/core:prefs, no custom hierarchy group is needed: Location's android/jvm/ios
+ // actuals already each live in their own independent source set (no shared nonWeb code to hide from
+ // wasmJs), so a 4th, wasmJsMain/Location.kt, is all this module needs.
+ @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+ wasmJs()
+
sourceSets {
commonMain.dependencies {
api(projects.core.model)
diff --git a/core/repository/src/wasmJsMain/kotlin/org/meshtastic/core/repository/Location.kt b/core/repository/src/wasmJsMain/kotlin/org/meshtastic/core/repository/Location.kt
new file mode 100644
index 0000000000..907529b6bb
--- /dev/null
+++ b/core/repository/src/wasmJsMain/kotlin/org/meshtastic/core/repository/Location.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 .
+ */
+package org.meshtastic.core.repository
+
+/**
+ * wasmJs placeholder location type, mirroring jvmMain's — no consumer needs real location on web yet, same as desktop.
+ * A real one would wrap the browser Geolocation API (`navigator.geolocation`); deferred until something actually calls
+ * [LocationService]/[LocationRepository] on this target.
+ */
+actual class Location
diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt
index d21d2c1f97..044df38513 100644
--- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt
+++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshLogCleanupWorkerTest.kt
@@ -17,8 +17,6 @@
package org.meshtastic.core.service
import android.content.Context
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.preferencesOf
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
@@ -41,6 +39,8 @@ import org.junit.runner.RunWith
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.di.MeshLogDataStore
import org.meshtastic.core.prefs.meshlog.MeshLogPrefsImpl
+import org.meshtastic.core.prefs.store.PrefsKey
+import org.meshtastic.core.prefs.store.PrefsSnapshot
import org.meshtastic.core.service.worker.MeshLogCleanupWorker
import org.meshtastic.core.testing.FakeMeshLogRepository
import org.robolectric.RobolectricTestRunner
@@ -169,23 +169,28 @@ class MeshLogCleanupWorkerTest {
private val loadGate = CompletableDeferred()
private val persisted =
MutableStateFlow(
- preferencesOf(
- MeshLogPrefsImpl.KEY_LOGGING_ENABLED_PREF to loggingEnabled,
- MeshLogPrefsImpl.KEY_RETENTION_DAYS_PREF to retentionDays,
+ FakePrefsSnapshot(
+ mapOf(
+ MeshLogPrefsImpl.KEY_LOGGING_ENABLED_PREF to loggingEnabled,
+ MeshLogPrefsImpl.KEY_RETENTION_DAYS_PREF to retentionDays,
+ ),
),
)
var collectionCount: Int = 0
private set
- override val data: Flow = flow {
+ override val data: Flow = flow {
collectionCount += 1
loadGate.await()
emitAll(persisted)
}
- override suspend fun updateData(transform: suspend (Preferences) -> Preferences): Preferences =
- transform(persisted.value).also { persisted.value = it }
+ override suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit) {
+ val editor = FakePrefsEditor(persisted.value.values)
+ transform(editor)
+ persisted.value = FakePrefsSnapshot(editor.values)
+ }
fun releaseLoad() {
loadGate.complete(Unit)
@@ -195,4 +200,29 @@ class MeshLogCleanupWorkerTest {
loadGate.completeExceptionally(cause)
}
}
+
+ /** Minimal in-memory [PrefsSnapshot]/[PrefsSnapshot.Editor] test double — no real DataStore/localStorage. */
+ private class FakePrefsSnapshot(val values: Map, Any?>) : PrefsSnapshot {
+ @Suppress("UNCHECKED_CAST")
+ override fun get(key: PrefsKey): T? = values[key] as T?
+
+ override fun contains(key: PrefsKey<*>): Boolean = values.containsKey(key)
+ }
+
+ private class FakePrefsEditor(initial: Map, Any?>) : PrefsSnapshot.Editor {
+ val values: MutableMap, Any?> = initial.toMutableMap()
+
+ @Suppress("UNCHECKED_CAST")
+ override fun get(key: PrefsKey): T? = values[key] as T?
+
+ override fun contains(key: PrefsKey<*>): Boolean = values.containsKey(key)
+
+ override fun set(key: PrefsKey, value: T) {
+ values[key] = value
+ }
+
+ override fun remove(key: PrefsKey) {
+ values.remove(key)
+ }
+ }
}
diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt
index ea79c13596..5ff8befa38 100644
--- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt
+++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt
@@ -77,6 +77,7 @@ import org.meshtastic.core.prefs.di.asMeshDataStore
import org.meshtastic.core.prefs.di.asMeshLogDataStore
import org.meshtastic.core.prefs.di.asRadioDataStore
import org.meshtastic.core.prefs.di.asUiDataStore
+import org.meshtastic.core.prefs.store.asPrefsStore
import org.meshtastic.desktop.DesktopBuildConfig
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
@@ -144,20 +145,31 @@ fun desktopPlatformModule() = module {
single(named(PROCESS_LIFECYCLE)) { DesktopProcessLifecycleOwner().lifecycle }
}
-/** Typed preference-datastore singletons for each preference domain. */
+/**
+ * Typed preference-datastore singletons for each preference domain.
+ *
+ * Every core:prefs store (all but the last line) inserts [asPrefsStore] before its `asXDataStore()` call: those marker
+ * types are now `PrefsStore`-backed (androidx.datastore.preferences has no wasmJs variant, so core:prefs abstracts over
+ * it — see core/prefs/store/PrefsStore.kt), not `DataStore` directly. `CorePreferencesDataStore` belongs
+ * to core:datastore, a separate, unrelated module still typed directly against `DataStore` — left as-is.
+ */
private fun desktopPreferencesDataStoreModule() = module {
- single { prefsStore("analytics", get()).asAnalyticsDataStore() }
- single { prefsStore("homoglyph_encoding", get()).asHomoglyphEncodingDataStore() }
- single { prefsStore("app", get()).asAppDataStore() }
- single { prefsStore("custom_emoji", get()).asCustomEmojiDataStore() }
- single { prefsStore("map", get()).asMapDataStore() }
- single { prefsStore("map_consent", get()).asMapConsentDataStore() }
- single { prefsStore("map_tile_provider", get()).asMapTileProviderDataStore() }
- single { prefsStore("mesh", get()).asMeshDataStore() }
- single { prefsStore("radio", get()).asRadioDataStore() }
- single { prefsStore("ui", get()).asUiDataStore() }
- single { prefsStore("meshlog", get()).asMeshLogDataStore() }
- single { prefsStore("filter", get()).asFilterDataStore() }
+ single { prefsStore("analytics", get()).asPrefsStore().asAnalyticsDataStore() }
+ single {
+ prefsStore("homoglyph_encoding", get()).asPrefsStore().asHomoglyphEncodingDataStore()
+ }
+ single { prefsStore("app", get()).asPrefsStore().asAppDataStore() }
+ single { prefsStore("custom_emoji", get()).asPrefsStore().asCustomEmojiDataStore() }
+ single { prefsStore("map", get()).asPrefsStore().asMapDataStore() }
+ single { prefsStore("map_consent", get()).asPrefsStore().asMapConsentDataStore() }
+ single {
+ prefsStore("map_tile_provider", get()).asPrefsStore().asMapTileProviderDataStore()
+ }
+ single { prefsStore("mesh", get()).asPrefsStore().asMeshDataStore() }
+ single { prefsStore("radio", get()).asPrefsStore().asRadioDataStore() }
+ single { prefsStore("ui", get()).asPrefsStore().asUiDataStore() }
+ single { prefsStore("meshlog", get()).asPrefsStore().asMeshLogDataStore() }
+ single { prefsStore("filter", get()).asPrefsStore().asFilterDataStore() }
single { prefsStore("core_preferences", get()).asCorePreferencesDataStore() }
}