Android app crashed at startup with Exception("Node not found: <installId>") inside
AvatarWithSpeechBubble, before the UI ever appeared.
AvatarWithSpeechBubble called nodeManager.readNodeState(installId()) directly, which
throws if the client node is absent. On Android, Compose can render this composable
before ClientNodeManager.init() has finished inserting the client node — the same race
does not manifest on desktop because the desktop startup path is faster.
readNodeStateOrNull already existed on ClientNodeManager and returns a null-emitting
flow rather than throwing, but the composable was using the throwing variant.
readNodeState(installId()).collectAsState() with
readNodeStateOrNull(installId()) keyed on swarmState.contains(installId()) inside
a remember { }, so the flow is re-derived when the client node arrives and the
composable receives an update via swarmState.?: return early exit so the composable renders nothing (blank, no crash) until
the client node is available.readNodeState(installId()).value call inside the TOS lambda with
the already-collected client value.ClientNodeManagerReadOrNullTest to pin the contract: readNodeState throws on
a missing id, readNodeStateOrNull returns a null-valued flow.readNodeState with an id that might not yet be in the
manager. Use readNodeStateOrNull and guard with ?: return or ?.let { }.installId()-based lookups are susceptible to this race on Android
because init() is async. Any composable that needs the client node should follow the
remember(swarmState.contains(installId())) { readNodeStateOrNull(installId()) }
pattern so recomposition is triggered when the node arrives.