@@ -54,10 +54,19 @@ open class FirmwareReleaseRepositoryImpl(
5454 /* * Single-flight guard so concurrent collectors share one network refresh. */
5555 private val refreshMutex = Mutex ()
5656
57- /* * Guards [seedChecked] so concurrent collectors decode the bundled snapshot at most once per process. */
57+ /* *
58+ * Guards [bundledSnapshot] decode so concurrent collectors decode the bundled JSON at most once per process. The
59+ * apply/skip decision itself is re-evaluated every time against the CURRENT active DB — the active Room database
60+ * switches per selected device, so a one-shot seed gate would miss a freshly activated DB whose `firmware_release`
61+ * rows are empty.
62+ */
5863 private val seedMutex = Mutex ()
5964
60- @Volatile private var seedChecked = false
65+ /* * Decoded bundled snapshot cached for the process lifetime; the asset file never changes between launches. */
66+ @Volatile private var bundledSnapshot: NetworkFirmwareReleases ? = null
67+
68+ /* * Set when the bundled asset is missing or un-decodable, so we don't retry on every collection. */
69+ @Volatile private var bundleDecodeFailed = false
6170
6271 override val stableRelease: Flow <FirmwareRelease ?> = getLatestFirmware(FirmwareReleaseType .STABLE )
6372
@@ -87,34 +96,42 @@ open class FirmwareReleaseRepositoryImpl(
8796 /* *
8897 * Applies the bundled snapshot per release type whenever it is newer than what is cached for that type — not just
8998 * when the cache is empty. The bundle is refreshed weekly in CI, so an app update carries fresh data even for users
90- * whose network path to api.meshtastic.org chronically fails. Checked once per process; a cache that is already
91- * newer (from a successful network refresh) is never regressed, and a type the bundle doesn't ship is left
92- * untouched.
99+ * whose network path to api.meshtastic.org chronically fails. The decoded snapshot is cached for the process; the
100+ * apply/skip decision is re-evaluated every call against the CURRENT active DB, because that DB switches per
101+ * selected device and a one-shot seed gate would skip a freshly activated DB whose `firmware_release` rows are
102+ * empty. A cache that is already newer (from a successful network refresh) is never regressed, and a type the
103+ * bundle doesn't ship is left untouched.
93104 */
94105 private suspend fun ensureSeeded () {
95- if (seedChecked ) return
106+ if (bundleDecodeFailed ) return // don't retry the bundled asset on every collection once it has failed
96107 seedMutex.withLock {
97- if (seedChecked) return
98- safeCatching {
99- val bundled = assetReader.decode<NetworkFirmwareReleases >(" firmware_releases.json" , json)?.releases
100- if (bundled == null ) {
101- Logger .w { " FirmwareReleaseRepository: no bundled releases available to seed from" }
102- } else {
103- val toApply =
104- listOf (
105- FirmwareReleaseType .STABLE to bundled.stable,
106- FirmwareReleaseType .ALPHA to bundled.alpha,
107- )
108- .filter { (type, releases) -> isBundleNewerFor(type, releases) }
109- .toMap()
110- if (toApply.isNotEmpty()) {
111- Logger .i { " FirmwareReleaseRepository: applying bundled snapshot for ${toApply.keys} " }
112- localDataSource.replaceFirmwareReleases(toApply)
108+ // Decode the bundled JSON once per process; the asset never changes between launches.
109+ if (bundledSnapshot == null && ! bundleDecodeFailed) {
110+ safeCatching { assetReader.decode<NetworkFirmwareReleases >(" firmware_releases.json" , json) }
111+ .onSuccess { snapshot -> bundledSnapshot = snapshot }
112+ .onFailure { e ->
113+ Logger .w(e) { " FirmwareReleaseRepository: failed to decode bundled JSON" }
114+ bundleDecodeFailed = true
113115 }
116+ // Decode returning null (asset missing) also stops further retries.
117+ if (bundledSnapshot == null && ! bundleDecodeFailed) {
118+ Logger .w { " FirmwareReleaseRepository: no bundled releases available to seed from" }
119+ bundleDecodeFailed = true
114120 }
115121 }
116- .onSuccess { seedChecked = true }
117- .onFailure { e -> Logger .w(e) { " FirmwareReleaseRepository: failed to seed cache from bundled JSON" } }
122+
123+ val bundled = bundledSnapshot?.releases ? : return
124+
125+ // Re-evaluate against the current active DB on every call — it may have switched devices since the last
126+ // seed.
127+ val toApply =
128+ listOf (FirmwareReleaseType .STABLE to bundled.stable, FirmwareReleaseType .ALPHA to bundled.alpha)
129+ .filter { (type, releases) -> isBundleNewerFor(type, releases) }
130+ .toMap()
131+ if (toApply.isNotEmpty()) {
132+ Logger .i { " FirmwareReleaseRepository: applying bundled snapshot for ${toApply.keys} " }
133+ localDataSource.replaceFirmwareReleases(toApply)
134+ }
118135 }
119136 }
120137
0 commit comments