From 0103953953933faddce48935b53f3e3e39a2c28e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 18 Aug 2026 10:00:43 -0400 Subject: [PATCH] refactor(diagnostics): split the flap window and episode rule apart detekt ReturnCount. Extracting the pruning and the is-this-an-episode predicate reads better than suppressing it, and the cooldown rule now has a name and a docstring of its own. --- .../diagnostics/TransportFlapDetector.kt | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/diagnostics/TransportFlapDetector.kt b/android/app/src/main/java/com/fabledsword/minstrel/diagnostics/TransportFlapDetector.kt index 2297914a..dfcda283 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/diagnostics/TransportFlapDetector.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/diagnostics/TransportFlapDetector.kt @@ -33,20 +33,30 @@ class TransportFlapDetector( */ fun onChange(observation: TransportObservation): List? { recent.addLast(observation) - while (recent.isNotEmpty() && - observation.atElapsedMs - recent.first().atElapsedMs > windowMs - ) { - recent.removeFirst() - } - if (recent.size < minChanges) return null - // One episode, one summary. A sustained fault would otherwise emit a - // summary per reading and bury the per-change events underneath them. - val since = lastSummaryAtMs - if (since != null && observation.atElapsedMs - since < summaryCooldownMs) return null + dropReadingsOlderThan(observation.atElapsedMs) + if (!isEpisode(observation.atElapsedMs)) return null lastSummaryAtMs = observation.atElapsedMs return recent.toList() } + private fun dropReadingsOlderThan(nowMs: Long) { + while (recent.isNotEmpty() && nowMs - recent.first().atElapsedMs > windowMs) { + recent.removeFirst() + } + } + + /** + * Enough changes packed together, and far enough from the last thing we + * wrote down. The cooldown is what keeps one episode to one summary: a + * sustained fault produces a change every poll, and a summary per reading + * would bury the per-change events underneath them. + */ + private fun isEpisode(nowMs: Long): Boolean { + val since = lastSummaryAtMs + val cooled = since == null || nowMs - since >= summaryCooldownMs + return recent.size >= minChanges && cooled + } + /** Forget everything — call when the route changes or casting ends. */ fun reset() { recent.clear()