diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index ae6d013ef..9dee631e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2241,6 +2241,11 @@ object LocalCache : ILocalCache, ICacheProvider { } } + // 2× the 10-min `PRESENCE_FRESHNESS_WINDOW_SECONDS` used by + // `NestsFeedFilter` so a presence still inside any feed's window + // can never be pruned. + private val PRESENCE_PRUNE_AGE_SECONDS = 20L * 60L + fun pruneOldMessagesChannel(channel: Channel) { val toBeRemoved = channel.pruneOldMessages() @@ -2254,6 +2259,14 @@ object LocalCache : ILocalCache, ICacheProvider { removeFromCache(childrenToBeRemoved) + // Audio-room presence is keyed separately from `notes` and + // never gets reaped by the top-N rule. Drop entries older + // than 2× the 10-min freshness window so the index doesn't + // grow unbounded with every author who ever heartbeat here. + if (channel is LiveActivitiesChannel) { + channel.pruneStalePresence(TimeUtils.now() - PRESENCE_PRUNE_AGE_SECONDS) + } + if (toBeRemoved.size > 100 || channel.notes.size() > 100) { println( "PRUNE: ${toBeRemoved.size} old messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept", diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt index 75b202dae..1638b54d6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -80,6 +80,27 @@ class LiveActivitiesChannel( flowSet?.notes?.invalidateData() } + /** + * Drop presence entries older than [cutoff]. Without this, every + * author who ever heartbeats in this room would leave an entry + * here forever, even though the freshness check in + * `NestsFeedFilter` already treats anything past + * `PRESENCE_FRESHNESS_WINDOW_SECONDS` (10 min) as dead. Run on the + * same schedule as [pruneOldMessages] with a generous cutoff (2× + * the freshness window) so a presence still inside any feed's + * window can never be pruned. + */ + fun pruneStalePresence(cutoff: Long): Int { + val toRemove = mutableListOf() + presenceNotes.forEach { author, note -> + val createdAt = note.event?.createdAt + if (createdAt == null || createdAt < cutoff) toRemove.add(author) + } + toRemove.forEach { presenceNotes.remove(it) } + if (toRemove.isNotEmpty()) flowSet?.notes?.invalidateData() + return toRemove.size + } + fun address() = address override fun relays() = info?.allRelayUrls()?.toSet()?.ifEmpty { null } ?: super.relays()