Merge #1ed68583: Debounce relay-status counter updates (notification ra…

Debounce relay-status counter updates (notification rate limit)

nostr:nevent1qqspa459s04qpe5h3k6m0dg2amne9fl247pxjk3w6ylnq7m4rw3pk2cpz3mhxue69uhhyetvv9ujumn8d96zuer9wc4mf4tk

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Fixes the relay-status notification (id=2) hitting Android's per-app notification rate limit during relay activity bursts.

addSent/addFailed called notify(2) synchronously for every relay event (NostrClientLoggerListener.onSent), so bursts of 7-34 notifies/sec against the ~5/sec limit were shed by NotificationManagerService and the status notification froze on stale text.

The counter path now emits Unit ticks into a MutableSharedFlow (capacity 1, DROP_OLDEST) collected with debounce(300): a burst collapses into one notification update, the last tick always renders, and tryEmit never suspends or fails on Quartz IO threads. No numeric counter, so nothing overflows or wraps no matter how long the process runs. Connection-state updates keep their existing debounce so connect/disconnect still shows promptly.

Issue: nevent1qy28wumn8ghj7un9d3shjtnwva5hgtnyv4mqqg80gw6ushd8jyj6rt8qseup7hngj0xhqa2ms3dvam60lhc760zvkyq6uts9

Validated: ktlintCheck, lint, :app:testFreeDebugUnitTest.
This commit is contained in:
greenart7c3
2026-08-18 08:27:54 -03:00
@@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.onEach
@@ -39,6 +41,23 @@ class AmberRelayStats(
var available = emptySet<NormalizedRelayUrl>()
var connected = emptySet<NormalizedRelayUrl>()
// Counter updates arrive per relay event (NostrClientLoggerListener.onSent),
// which can burst at 30+ events/sec and trip Android's per-app notification
// rate limit (~5/sec): NotificationManager sheds every update and the status
// notification freezes on stale text until the burst ends. Emitting a tick
// defers notify() to the debounced collector started in
// createNotificationChannel(), so a burst collapses into one notification
// update. Connection-state changes keep their own (also debounced)
// collectors so connect/disconnect still shows promptly.
// Unit events instead of a numeric counter: nothing to overflow or wrap no
// matter how long the app runs. DROP_OLDEST + capacity 1 keeps only the
// newest tick, so tryEmit never fails and the last event of a burst always
// renders.
private val counterTick = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
@OptIn(FlowPreview::class)
@SuppressLint("MissingPermission")
val relayStatus = combine(client.availableRelaysFlow(), client.connectedRelaysFlow()) { available, connected ->
@@ -107,6 +126,14 @@ class AmberRelayStats(
updateNotification()
}
}
Amber.instance.applicationIOScope.launch {
// Trailing-edge debounce for the counter path (addSent/addFailed):
// the last tick of a burst always renders, at most one notify per
// 300ms window.
counterTick.debounce(300).collect {
updateNotification()
}
}
}
private val innerCache = mutableMapOf<NormalizedRelayUrl, AmberRelayStat>()
@@ -253,12 +280,16 @@ class AmberRelayStats(
fun addSent(url: NormalizedRelayUrl) {
get(url).addSent()
updateNotification()
scheduleNotificationUpdate()
}
fun addFailed(url: NormalizedRelayUrl) {
get(url).addFailed()
updateNotification()
scheduleNotificationUpdate()
}
private fun scheduleNotificationUpdate() {
counterTick.tryEmit(Unit)
}
}