fix: isolate per-chain mapping failures in currentChains, not just register/unregister

mapChainLocalToChain() runs as a single mapList{} transform over the entire
chain list. If it throws for even one malformed row (e.g. a JSON parse
failure on the chain's `additional` blob), the whole transform throws,
killing the Eagerly-shared currentChains/chainsById flows for every chain -
permanently, since Eagerly sharing never restarts once its upstream dies.

This is the same class of bug already fixed for registerChain/unregisterChain
in the diff-processing step below, but one level upstream of it: a mapping
failure here never even reaches that per-chain isolation, since it happens
before the diff is computed at all.

Device logs on a fresh install showed exactly this shape: 8 chains (including
Pezkuwi, Pezkuwi Asset Hub, Polkadot Asset Hub) successfully completed
"Constructed runtime" within the first ~2.5 seconds, then all activity for
every remaining chain stopped completely for the rest of the session - no
crash, no battery-saver kill (confirmed off), the process simply went silent,
consistent with the shared flow dying mid-registration and never recovering.
This commit is contained in:
2026-07-09 02:10:40 -07:00
parent 99c9fd9db5
commit 1d00f78fa9
@@ -7,7 +7,7 @@ import io.novafoundation.nova.common.utils.RuntimeContext
import io.novafoundation.nova.common.utils.diffed import io.novafoundation.nova.common.utils.diffed
import io.novafoundation.nova.common.utils.filterList import io.novafoundation.nova.common.utils.filterList
import io.novafoundation.nova.common.utils.inBackground import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.mapList import io.novafoundation.nova.common.utils.mapListNotNull
import io.novafoundation.nova.common.utils.mapNotNullToSet import io.novafoundation.nova.common.utils.mapNotNullToSet
import io.novafoundation.nova.common.utils.provideContext import io.novafoundation.nova.common.utils.provideContext
import io.novafoundation.nova.common.utils.removeHexPrefix import io.novafoundation.nova.common.utils.removeHexPrefix
@@ -75,7 +75,16 @@ class ChainRegistry(
) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) { ) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
val currentChains = chainDao.joinChainInfoFlow() val currentChains = chainDao.joinChainInfoFlow()
.mapList { mapChainLocalToChain(it, gson) } // mapListNotNull, not mapList: mapChainLocalToChain() can throw on a single malformed row (e.g. a
// gson.fromJson() failure on the chain's `additional` JSON blob) - since this whole step runs as ONE
// transform over the ENTIRE chain list, one bad chain would previously throw out of this operator and
// permanently kill this Eagerly-shared flow for every chain, not just the offending one. Skip and log
// instead, matching the per-chain isolation already applied to registerChain/unregisterChain below.
.mapListNotNull { chainLocal ->
runCatching { mapChainLocalToChain(chainLocal, gson) }
.onFailure { Log.e(LOG_TAG, "Failed to map chain ${chainLocal.chain.id} (${chainLocal.chain.name}) from local DB", it) }
.getOrNull()
}
.diffed() .diffed()
.map { diff -> .map { diff ->
// Each chain's register/unregister is isolated: one malformed/leftover row (e.g. a chain persisted // Each chain's register/unregister is isolated: one malformed/leftover row (e.g. a chain persisted