From 688c2fbc99cfc146761ecb93acbc37bb0d2df575 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:59:32 -0500 Subject: [PATCH] feat(connections): enable wasmJs, unblocking all v0 feature modules The v0 web slice (feature:connections/messaging/node/settings, per this effort's own architecture decision) all share KmpFeatureConventionPlugin. Its apply() wired core:testing directly into every consumer's commonTest, unconditionally, at plugin-apply time -- before the consuming module's own build.gradle.kts kotlin {} block (and any nonWebTest source set it creates) has even run. core:testing has no wasmJs target, so every v0 feature module would have hit the identical compileTestKotlinWasmJs failure the moment it opted in, no matter what its own build.gradle.kts did to try to route around it -- a shared-build-logic problem, not a per-module one. Fixed by deferring the wiring to target.afterEvaluate, which checks whether the consuming module ended up with a wasmJs target and a nonWebTest source set and routes core:testing there instead when both exist, with a fail-fast check() if a module has one but not the other. Every non-wasmJs feature module keeps resolving core:testing via commonTest exactly as before -- verified with a real compile+test run across all nine other consumers (messaging, node, settings, map-maplibre, intro, discovery, docs, firmware, wifi-provision), zero regression. core:domain (a feature:connections dependency, zero expect/actual, zero java.*/android.* imports, every dependency already wasmJs-clean) gets a bare wasmJs() -- mechanical. feature:connections surfaced a sharper version of the screening test this session has used for every prior module: "no expect/actual, no java.*/android.* imports" is necessary but not sufficient. ScannerViewModel.kt/CommonGetDiscoveredDevicesUseCase.kt directly referenced core:datastore's RecentAddressesDataSource/ FirmwareRecoveryDataSource -- concrete classes that live in that module's own nonWebMain (Preferences-backed, no wasmJs variant), reached transitively rather than through any local expect/actual. Two new feature-local interfaces (RecentAddressesSource, PendingFirmwareRecoverySource) seam this off: a nonWebMain adapter delegates to the real DataStore-backed sources unchanged, and wasmJs gets an honest no-op (no recent-address history, no firmware-recovery banner on web this pass) -- same shape as core:service's TakServerIntegration seam. A real localStorage-backed implementation is deferred until a webApp module exists to wire one in. Also fixes an unrelated, pre-existing detekt violation (NoUnusedImports on ProjectExtensions.kt) surfaced while re-running build-logic/convention's own lint as part of this pass's verification -- unrelated to this change's own logic, folded in since it was already in front of us. Co-Authored-By: Claude Sonnet 5 --- .../main/kotlin/KmpFeatureConventionPlugin.kt | 25 ++++++++++- .../buildlogic/ProjectExtensions.kt | 1 - core/domain/build.gradle.kts | 21 ++++++++- .../settings/AdminActionsUseCaseTest.kt | 0 .../settings/CleanNodeDatabaseUseCaseTest.kt | 0 .../usecase/settings/ExportDataUseCaseTest.kt | 0 .../settings/ExportNodeDatabaseUseCaseTest.kt | 0 .../settings/InstallProfileUseCaseTest.kt | 0 .../settings/RadioConfigUseCaseTest.kt | 0 .../settings/SetMeshLogSettingsUseCaseTest.kt | 0 feature/connections/build.gradle.kts | 31 +++++++++++++ .../connections/AndroidScannerViewModel.kt | 8 ++-- .../AndroidGetDiscoveredDevicesUseCase.kt | 4 +- .../feature/connections/ScannerViewModel.kt | 8 ++-- .../data/PendingFirmwareRecoverySource.kt | 27 +++++++++++ .../connections/data/RecentAddressesSource.kt | 34 ++++++++++++++ .../CommonGetDiscoveredDevicesUseCase.kt | 4 +- .../connections/JvmScannerViewModel.kt | 8 ++-- .../usecase/JvmGetDiscoveredDevicesUseCase.kt | 4 +- .../connections/data/DataSourceAdapters.kt | 41 +++++++++++++++++ .../connections/ScannerViewModelHarness.kt | 8 ++-- .../connections/ScannerViewModelTest.kt | 0 .../CommonGetDiscoveredDevicesUseCaseTest.kt | 4 +- .../domain/usecase/TcpDiscoveryHelpersTest.kt | 0 .../connections/data/NoopDataSources.kt | 45 +++++++++++++++++++ 25 files changed, 246 insertions(+), 27 deletions(-) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt (100%) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt (100%) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt (100%) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt (100%) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt (100%) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt (100%) rename core/domain/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt (100%) create mode 100644 feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.kt create mode 100644 feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt create mode 100644 feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt rename feature/connections/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt (96%) rename feature/connections/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt (100%) rename feature/connections/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt (99%) rename feature/connections/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt (100%) create mode 100644 feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt index 012c98e36f..258ec11470 100644 --- a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt @@ -69,8 +69,31 @@ class KmpFeatureConventionPlugin : Plugin { implementation(libs.library("compose-multiplatform-ui")) } + } - sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) } + // core:testing has no wasmJs target (same gap every core/* module hit while gaining one this + // session). Wiring it into commonTest directly — as this plugin used to, unconditionally — + // breaks compileTestKotlinWasmJs for every feature module that opts into wasmJs, since the + // dependency is added by this shared plugin's apply(), which runs *before* the consuming + // module's own build.gradle.kts `kotlin {}` block (and any nonWebTest source set it creates) + // has executed. Deferring to afterEvaluate — which fires only after the whole build script has + // run — lets us check what the consuming module actually set up and route accordingly: + // - a module with a `nonWebTest` source set (wasmJs opted in, core:testing hoisted out of + // commonMain the same way every core/* module did) gets core:testing wired there instead. + // - every other feature module (no wasmJs, no nonWebTest) is wired into commonTest exactly as + // before — fully backward-compatible, verified against every other v0/non-wasmJs consumer. + target.afterEvaluate { + extensions.configure { + val hasWasmJsTarget = targets.findByName("wasmJs") != null + val nonWebTest = sourceSets.findByName("nonWebTest") + check(!hasWasmJsTarget || nonWebTest != null) { + "${target.path} registers wasmJs() but has no `nonWebTest` source set — " + + "core:testing has no wasmJs target, so it must be routed away from commonTest. " + + "See feature/connections/build.gradle.kts for the pattern." + } + val testSourceSet = nonWebTest ?: sourceSets.getByName("commonTest") + testSourceSet.dependencies { implementation(project(":core:testing")) } + } } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt index 980c5adc33..fd20d891ef 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt @@ -26,7 +26,6 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.testing.AbstractTestTask import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType import org.gradle.plugin.use.PluginDependency diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index ef45d55ff0..4552abf0f7 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -24,6 +24,14 @@ plugins { kotlin { android { withHostTest { isIncludeAndroidResources = true } } + // Library module: bare wasmJs(), no browser(). No custom hierarchy group is needed for MAIN — zero + // expect/actual declarations and zero java.*/android.* imports in commonMain (confirmed via grep), + // and every commonMain dependency (core:repository/model/common/database/datastore/resources, + // protobufs, kermit/okio/kotlinx-datetime/kotlinx-serialization-json(-okio)) already publishes a + // wasmJs variant — same shape as core:repository/core:service, unlike core:ble/core:database. + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs() + sourceSets { commonMain.dependencies { implementation(projects.core.repository) @@ -40,6 +48,17 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json.okio) } - commonTest.dependencies { implementation(projects.core.testing) } + + // TEST only: core:testing has no wasmJs target (same gap every other module this session hit). + // 7 of 13 commonTest files depend on it (confirmed via grep for the import, not assumed) — moved + // to a nonWebTest source set; the other 6 stay in commonTest and compile for wasmJs. + val nonWebTest by creating { + dependsOn(commonTest.get()) + dependencies { implementation(projects.core.testing) } + } + getByName("jvmTest") { dependsOn(nonWebTest) } + getByName("androidHostTest") { dependsOn(nonWebTest) } + matching { it.name == "iosArm64Test" || it.name == "iosSimulatorArm64Test" } + .configureEach { dependsOn(nonWebTest) } } } diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt b/core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt similarity index 100% rename from core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt rename to core/domain/src/nonWebTest/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 696882eca6..72f2b8e537 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -15,11 +15,33 @@ * 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.feature) } kotlin { android { withHostTest { isIncludeAndroidResources = true } } + // Feature module: bare wasmJs(), no browser() (that's for the eventual webApp executable). + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + + // nonWebMain: ScannerViewModel/CommonGetDiscoveredDevicesUseCase depend on RecentAddressesSource/ + // PendingFirmwareRecoverySource (feature-local interfaces, commonMain) — but the real, Preferences-backed + // adapters (DataSourceAdapters.kt) delegate to core:datastore's RecentAddressesDataSource/ + // FirmwareRecoveryDataSource, which have no wasmJs target (androidx.datastore.preferences publishes none + // — see core:datastore's own wasmJs milestone). Predicate, not withAndroidTarget()/withApple() — those + // silently drop androidMain under com.android.kotlin.multiplatform.library (KT-80409), same as core:ble. + @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), same gap core:ble hit. + sourceSets.getByName("iosMain") { dependsOn(sourceSets.getByName("nonWebMain")) } + sourceSets { commonMain.dependencies { implementation(projects.core.common) @@ -47,5 +69,14 @@ kotlin { implementation(libs.compose.multiplatform.ui.test) implementation(compose.desktop.currentOs) } + + // TEST only: 4 of 6 commonTest files depend on core:testing (no wasmJs target — same gap every + // other module this session hit), confirmed via grep for the import, not assumed: + // ScannerViewModelHarness.kt/ScannerViewModelTest.kt/TcpDiscoveryHelpersTest.kt/ + // CommonGetDiscoveredDevicesUseCaseTest.kt moved to the nonWebTest source set the hierarchy + // template above already creates (android/jvm/iOS only); the other 2 stay in commonTest and + // compile for wasmJs. core:testing itself is wired into nonWebTest by KmpFeatureConventionPlugin + // (afterEvaluate, routes to nonWebTest when present) — not added here. + getByName("nonWebTest") { dependsOn(commonTest.get()) } } } diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt index 9e4473cf3b..52ae892756 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt @@ -26,8 +26,6 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.ble.BluetoothRepository -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.network.repository.UsbRepository @@ -39,6 +37,8 @@ import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.bonding_failed_retry import org.meshtastic.core.resources.usb_permission_denied +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.AndroidUsbDeviceData import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -50,14 +50,14 @@ class AndroidScannerViewModel( radioController: RadioController, radioInterfaceService: RadioInterfaceService, radioPrefs: RadioPrefs, - recentAddressesDataSource: RecentAddressesDataSource, + recentAddressesDataSource: RecentAddressesSource, getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, networkRepository: NetworkRepository, dispatchers: org.meshtastic.core.di.CoroutineDispatchers, private val bluetoothRepository: BluetoothRepository, private val usbRepository: UsbRepository, uiPrefs: UiPrefs, - firmwareRecoveryDataSource: FirmwareRecoveryDataSource, + firmwareRecoveryDataSource: PendingFirmwareRecoverySource, bleScanner: org.meshtastic.core.ble.BleScanner? = null, ) : ScannerViewModel( serviceRepository, diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt index 8e7d74a300..6db0330ce7 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt @@ -24,7 +24,6 @@ import org.jetbrains.compose.resources.getString import org.koin.core.annotation.Single import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.Node import org.meshtastic.core.network.repository.DiscoveredService @@ -33,6 +32,7 @@ import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.meshtastic +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.AndroidUsbDeviceData import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices @@ -44,7 +44,7 @@ import java.util.Locale @Single(binds = [GetDiscoveredDevicesUseCase::class]) class AndroidGetDiscoveredDevicesUseCase( private val bluetoothRepository: BluetoothRepository, - private val recentAddressesDataSource: RecentAddressesDataSource, + private val recentAddressesDataSource: RecentAddressesSource, private val nodeRepository: NodeRepository, private val databaseManager: DatabaseManager, private val usbRepository: UsbRepository, diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt index 2fa7e8750f..8e100508d6 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt @@ -44,8 +44,6 @@ import org.meshtastic.core.ble.BleScanStartFailureReason import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.ble.MeshtasticBleConstants import org.meshtastic.core.common.util.safeCatchingAll -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.PendingFirmwareRecovery import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.di.CoroutineDispatchers @@ -65,6 +63,8 @@ import org.meshtastic.core.resources.getPluralStringSuspend import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.ui.viewmodel.safeLaunch import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -143,12 +143,12 @@ open class ScannerViewModel( private val radioController: RadioController, private val radioInterfaceService: RadioInterfaceService, private val radioPrefs: RadioPrefs, - private val recentAddressesDataSource: RecentAddressesDataSource, + private val recentAddressesDataSource: RecentAddressesSource, private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, private val networkRepository: NetworkRepository, private val dispatchers: CoroutineDispatchers, private val uiPrefs: UiPrefs, - private val firmwareRecoveryDataSource: FirmwareRecoveryDataSource, + private val firmwareRecoveryDataSource: PendingFirmwareRecoverySource, private val bleScanner: BleScanner? = null, ) : ViewModel() { diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.kt new file mode 100644 index 0000000000..e30730f042 --- /dev/null +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/PendingFirmwareRecoverySource.kt @@ -0,0 +1,27 @@ +/* + * 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.feature.connections.data + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.datastore.model.PendingFirmwareRecovery + +/** Feature-local seam over `core:datastore`'s `FirmwareRecoveryDataSource` — see [RecentAddressesSource]. */ +interface PendingFirmwareRecoverySource { + val pending: Flow + + suspend fun clear() +} diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt new file mode 100644 index 0000000000..7a4225f25b --- /dev/null +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/data/RecentAddressesSource.kt @@ -0,0 +1,34 @@ +/* + * 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.feature.connections.data + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.datastore.model.RecentAddress + +/** + * Feature-local seam over `core:datastore`'s `RecentAddressesDataSource`, which wraps `androidx.datastore.preferences` + * and so has no wasmJs target (that library publishes zero js/wasmJs variants — see core:datastore's own wasmJs + * milestone). Keeps [org.meshtastic.feature.connections.ScannerViewModel]/`CommonGetDiscoveredDevicesUseCase` in + * commonMain: only the adapter delegating to the real DataSource is hoisted to `nonWebMain`, and wasmJs gets a no-op. + */ +interface RecentAddressesSource { + val recentAddresses: Flow> + + suspend fun add(address: RecentAddress) + + suspend fun remove(address: String) +} diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt index 9069e37f9f..7008db1a5d 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt @@ -21,12 +21,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.common.util.safeCatchingAll -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.network.repository.DiscoveredService import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.resources.meshtastic +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -39,7 +39,7 @@ import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase * target registers its own `@Single` wrapper (see `JvmGetDiscoveredDevicesUseCase`). */ open class CommonGetDiscoveredDevicesUseCase( - private val recentAddressesDataSource: RecentAddressesDataSource, + private val recentAddressesDataSource: RecentAddressesSource, private val nodeRepository: NodeRepository, private val databaseManager: DatabaseManager, private val usbScanner: UsbScanner? = null, diff --git a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt index 97d2f02a0c..8158474bb5 100644 --- a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt +++ b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/JvmScannerViewModel.kt @@ -17,14 +17,14 @@ package org.meshtastic.feature.connections import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.repository.RadioController import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase /** @@ -40,12 +40,12 @@ class JvmScannerViewModel( radioController: RadioController, radioInterfaceService: RadioInterfaceService, radioPrefs: RadioPrefs, - recentAddressesDataSource: RecentAddressesDataSource, + recentAddressesDataSource: RecentAddressesSource, getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, networkRepository: NetworkRepository, dispatchers: org.meshtastic.core.di.CoroutineDispatchers, uiPrefs: UiPrefs, - firmwareRecoveryDataSource: FirmwareRecoveryDataSource, + firmwareRecoveryDataSource: PendingFirmwareRecoverySource, bleScanner: org.meshtastic.core.ble.BleScanner? = null, ) : ScannerViewModel( serviceRepository, diff --git a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt index 8c1adc3ce6..b200181b5f 100644 --- a/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmGetDiscoveredDevicesUseCase.kt @@ -18,8 +18,8 @@ package org.meshtastic.feature.connections.domain.usecase import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase /** @@ -35,7 +35,7 @@ import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase */ @Single(binds = [GetDiscoveredDevicesUseCase::class]) class JvmGetDiscoveredDevicesUseCase( - recentAddressesDataSource: RecentAddressesDataSource, + recentAddressesDataSource: RecentAddressesSource, nodeRepository: NodeRepository, databaseManager: DatabaseManager, usbScanner: UsbScanner? = null, diff --git a/feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt b/feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt new file mode 100644 index 0000000000..49d5d59eef --- /dev/null +++ b/feature/connections/src/nonWebMain/kotlin/org/meshtastic/feature/connections/data/DataSourceAdapters.kt @@ -0,0 +1,41 @@ +/* + * 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.feature.connections.data + +import org.koin.core.annotation.Single +import org.meshtastic.core.datastore.FirmwareRecoveryDataSource +import org.meshtastic.core.datastore.RecentAddressesDataSource +import org.meshtastic.core.datastore.model.RecentAddress + +/** android/jvm/iOS binding: delegates to the real, Preferences-backed `RecentAddressesDataSource`. */ +@Single +class RecentAddressesSourceAdapter(private val delegate: RecentAddressesDataSource) : RecentAddressesSource { + override val recentAddresses = delegate.recentAddresses + + override suspend fun add(address: RecentAddress) = delegate.add(address) + + override suspend fun remove(address: String) = delegate.remove(address) +} + +/** android/jvm/iOS binding: delegates to the real, Preferences-backed `FirmwareRecoveryDataSource`. */ +@Single +class PendingFirmwareRecoverySourceAdapter(private val delegate: FirmwareRecoveryDataSource) : + PendingFirmwareRecoverySource { + override val pending = delegate.pending + + override suspend fun clear() = delegate.clear() +} diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt similarity index 96% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt index 55351bdb36..8e55901d66 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt +++ b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelHarness.kt @@ -35,8 +35,6 @@ import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.meshtastic.core.ble.BleDevice import org.meshtastic.core.ble.BleScanner -import org.meshtastic.core.datastore.FirmwareRecoveryDataSource -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.network.repository.DiscoveredService import org.meshtastic.core.network.repository.NetworkRepository @@ -47,6 +45,8 @@ import org.meshtastic.core.testing.FakeBluetoothRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.FakeServiceRepository import org.meshtastic.core.testing.FakeUiPrefs +import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase @@ -73,8 +73,8 @@ class ScannerViewModelHarness(val testDispatcher: TestDispatcher = UnconfinedTes val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill) val radioPrefs: RadioPrefs = mock(MockMode.autofill) - val recentAddressesDataSource: RecentAddressesDataSource = mock(MockMode.autofill) - val firmwareRecoveryDataSource: FirmwareRecoveryDataSource = mock(MockMode.autofill) + val recentAddressesDataSource: RecentAddressesSource = mock(MockMode.autofill) + val firmwareRecoveryDataSource: PendingFirmwareRecoverySource = mock(MockMode.autofill) val networkRepository: NetworkRepository = mock(MockMode.autofill) val bleScanner: BleScanner = mock(MockMode.autofill) diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt similarity index 100% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt similarity index 99% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt index 0efe552c3c..46d99875af 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt +++ b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCaseTest.kt @@ -26,11 +26,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.network.repository.DiscoveredService import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.TestDataFactory +import org.meshtastic.feature.connections.data.RecentAddressesSource import org.meshtastic.feature.connections.model.DeviceListEntry import kotlin.test.Test import kotlin.test.assertNotNull @@ -41,7 +41,7 @@ import kotlin.test.assertTrue class CommonGetDiscoveredDevicesUseCaseTest { private lateinit var useCase: CommonGetDiscoveredDevicesUseCase private lateinit var nodeRepository: FakeNodeRepository - private lateinit var recentAddressesDataSource: RecentAddressesDataSource + private lateinit var recentAddressesDataSource: RecentAddressesSource private lateinit var databaseManager: DatabaseManager private val recentAddressesFlow = MutableStateFlow>(emptyList()) private val resolvedServicesFlow = MutableStateFlow>(emptyList()) diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt b/feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt similarity index 100% rename from feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt rename to feature/connections/src/nonWebTest/kotlin/org/meshtastic/feature/connections/domain/usecase/TcpDiscoveryHelpersTest.kt diff --git a/feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt b/feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt new file mode 100644 index 0000000000..9bf137bdb3 --- /dev/null +++ b/feature/connections/src/wasmJsMain/kotlin/org/meshtastic/feature/connections/data/NoopDataSources.kt @@ -0,0 +1,45 @@ +/* + * 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.feature.connections.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.koin.core.annotation.Single +import org.meshtastic.core.datastore.model.PendingFirmwareRecovery +import org.meshtastic.core.datastore.model.RecentAddress + +/** + * wasmJs binding: no recent-TCP-address persistence on web (the real DataSource is Preferences-backed with no wasmJs + * target — see [RecentAddressesSource]). [DEFERRED]: a real implementation could be written against `localStorage`, + * mirroring core:datastore's own `LocalStorageStore`, once a webApp module exists to wire it in. + */ +@Single +class NoopRecentAddressesSource : RecentAddressesSource { + override val recentAddresses: Flow> = flowOf(emptyList()) + + override suspend fun add(address: RecentAddress) = Unit + + override suspend fun remove(address: String) = Unit +} + +/** wasmJs binding: no firmware-recovery banner on web — see [NoopRecentAddressesSource]. */ +@Single +class NoopPendingFirmwareRecoverySource : PendingFirmwareRecoverySource { + override val pending: Flow = flowOf(null) + + override suspend fun clear() = Unit +}