Context
launchWhileAttentive (added in #4406) is the shared helper for gating background pollers on per-surface UI visibility. It collects an attention flow with collectLatest and calls block() immediately on resume plus on every interval tick while attention is true.
Problem
If block() throws, the collectLatest lambda terminates, the outer launch Job enters Cancelled, and the poller is permanently dead — subsequent attention transitions (true → false → true) cannot revive it. There is no signal to the user that polling has stopped.
The current sole consumer, DesktopNetworkProfileService.runDetection, wraps its body in runCatching, so the issue is latent. As soon as a second consumer (sync, cleanup, notifications) lands without that exact defensive boilerplate, the first transient failure silently kills its poller for the rest of the process lifetime.
Proposed fix
Move per-tick exception handling into launchWhileAttentive itself:
- Catch
Throwable around each block() invocation, log via KotlinLogging, continue to the next tick.
- Rethrow
CancellationException to keep structured concurrency semantics intact (so flow cancellation / parent scope cancellation still propagate).
The helper's name implies "polling gated by attention", and the intuitive contract is that a single bad tick should not stop future ticks. Consumers should only have to decide what to do; the scheduler owns the "what if it throws" question.
Context
launchWhileAttentive(added in #4406) is the shared helper for gating background pollers on per-surface UI visibility. It collects an attention flow withcollectLatestand callsblock()immediately on resume plus on every interval tick while attention is true.Problem
If
block()throws, thecollectLatestlambda terminates, the outerlaunchJob entersCancelled, and the poller is permanently dead — subsequentattentiontransitions (true → false → true) cannot revive it. There is no signal to the user that polling has stopped.The current sole consumer,
DesktopNetworkProfileService.runDetection, wraps its body inrunCatching, so the issue is latent. As soon as a second consumer (sync, cleanup, notifications) lands without that exact defensive boilerplate, the first transient failure silently kills its poller for the rest of the process lifetime.Proposed fix
Move per-tick exception handling into
launchWhileAttentiveitself:Throwablearound eachblock()invocation, log viaKotlinLogging, continue to the next tick.CancellationExceptionto keep structured concurrency semantics intact (so flow cancellation / parent scope cancellation still propagate).The helper's name implies "polling gated by attention", and the intuitive contract is that a single bad tick should not stop future ticks. Consumers should only have to decide what to do; the scheduler owns the "what if it throws" question.