refactor(service): harden KMP service layer — database init, connection reliability, handler decomposition (#4992)

This commit is contained in:
James Rich 2026-04-04 13:07:44 -05:00 committed by GitHub
parent e111b61e4e
commit 6af3ad6f0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 3808 additions and 735 deletions

View file

@ -61,6 +61,7 @@ import okio.Path.Companion.toPath
import org.jetbrains.skia.Image
import org.koin.core.context.startKoin
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.database.desktopDataDir
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.navigation.rememberMultiBackstack
@ -248,7 +249,7 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
},
) {
setSingletonImageLoaderFactory { context ->
val cacheDir = System.getProperty("user.home") + "/.meshtastic/image_cache_v3"
val cacheDir = desktopDataDir() + "/image_cache_v3"
ImageLoader.Builder(context)
.components {
add(KtorNetworkFetcherFactory(httpClient = httpClient))

View file

@ -34,6 +34,7 @@ import okio.Path.Companion.toPath
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.database.desktopDataDir
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
@ -43,16 +44,6 @@ import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
/**
* Resolves the desktop data directory for persistent storage (DataStore files, Room database). Defaults to
* `~/.meshtastic/`. Override via `MESHTASTIC_DATA_DIR` environment variable.
*/
private fun desktopDataDir(): String {
val override = System.getenv("MESHTASTIC_DATA_DIR")
if (!override.isNullOrBlank()) return override
return System.getProperty("user.home") + "/.meshtastic"
}
/** Creates a file-backed [DataStore]<[Preferences]> at the given path under the data directory. */
private fun createPreferencesDataStore(name: String, scope: CoroutineScope): DataStore<Preferences> {
val dir = desktopDataDir() + "/datastore"
@ -90,7 +81,14 @@ private class DesktopProcessLifecycleOwner : LifecycleOwner {
*/
@Suppress("InjectDispatcher")
fun desktopPlatformModule() = module {
includes(desktopPreferencesDataStoreModule(), desktopProtoDataStoreModule())
// Application-lifetime scope shared by all DataStore instances. Per the DataStore docs:
// "The Job within this context dictates the lifecycle of the DataStore's internal operations.
// Ensure it is an application-scoped context that is not canceled by UI lifecycle events."
// DataStore has no close() API — the in-memory cache is released only when this Job is cancelled
// (at process exit). Using SupervisorJob so a single store's failure doesn't cascade.
val dataStoreScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
includes(desktopPreferencesDataStoreModule(dataStoreScope), desktopProtoDataStoreModule(dataStoreScope))
// -- Build config --
single<BuildConfigProvider> {
@ -109,10 +107,7 @@ fun desktopPlatformModule() = module {
}
/** Named [DataStore]<[Preferences]> instances for all preference domains. */
@Suppress("InjectDispatcher")
private fun desktopPreferencesDataStoreModule() = module {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private fun desktopPreferencesDataStoreModule(scope: CoroutineScope) = module {
single<DataStore<Preferences>>(named("AnalyticsDataStore")) { createPreferencesDataStore("analytics", scope) }
single<DataStore<Preferences>>(named("HomoglyphEncodingDataStore")) {
createPreferencesDataStore("homoglyph_encoding", scope)
@ -135,9 +130,7 @@ private fun desktopPreferencesDataStoreModule() = module {
}
/** Proto [DataStore] instances (OkioStorage-backed). */
@Suppress("InjectDispatcher")
private fun desktopProtoDataStoreModule() = module {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private fun desktopProtoDataStoreModule(scope: CoroutineScope) = module {
val protoDir = desktopDataDir() + "/datastore"
single<DataStore<LocalConfig>>(named("CoreLocalConfigDataStore")) {

View file

@ -51,7 +51,10 @@ class DesktopRadioTransportFactory(
TCPInterface(service, dispatchers, address.removePrefix(InterfaceId.TCP.id.toString()))
}
address.startsWith(InterfaceId.SERIAL.id) -> {
SerialTransport(portName = address.removePrefix(InterfaceId.SERIAL.id.toString()), service = service)
SerialTransport.open(
portName = address.removePrefix(InterfaceId.SERIAL.id.toString()),
service = service,
)
}
else -> error("Unsupported transport for address: $address")
}

View file

@ -75,6 +75,7 @@ class NoopRadioInterfaceService : RadioInterfaceService {
override val receivedData = MutableSharedFlow<ByteArray>()
override val meshActivity = MutableSharedFlow<MeshActivity>()
override val connectionError = MutableSharedFlow<String>()
override fun sendToRadio(bytes: ByteArray) {
logWarn("NoopRadioInterfaceService.sendToRadio(${bytes.size} bytes)")