ServerNodeManager.update() broadcast node changes to two independent streams — _nodeUpdates (SSE feed to UI clients) and nodeFlows (per-node observable flow for observer chains) — using two separate scope.launch blocks. Because both coroutines were enqueued independently, they could interleave with each other and with emissions from concurrent update() calls. With replay = 1 on nodeFlows, a stale emission winning the race became the persisted snapshot seen by any late-subscribing observer, potentially causing incorrect trigger evaluations or stale UI state.
The original code called scope.launch { _nodeUpdates.emit(node) } and then separately called publish() which also called scope.launch { flowOf(node.id).emit(...) }. Two coroutines, no ordering guarantee between them, no barrier between concurrent writes.
Coalesce both emissions into a single scope.launch in update(), with _nodeUpdates.emit followed by flowOf.emit in sequence. The propagation decision (shouldPublish, epoch, hopTtl) is resolved on the calling thread before the launch (because propagationContextTL is a ThreadLocal). The remote peer announce (tryPostEvent SOURCE_TRIGGERED) remains outside the launch since it is fire-and-forget and does not need to be sequenced with local emissions. publish() is unchanged and still used by succeeded() and EventMonitor for their own (non-update) paths.
scope.launch calls that emit to related flows from the same logical write point, ask whether they need to be in the same coroutine. If one stream’s replay or conflation means the last-emitted value becomes the “current” state for new subscribers, ordering is a correctness concern, not just a style one.ServerNodeManagerEmissionOrderTest verifies that both streams fire for a propagating update, that SSE-only writes do not reach the observer flow, and that within one write SSE precedes the observer flow.