perf(nests): dedicated kinds=[10312] limit=1 feed liveness probe
Feed thumbnails were reusing NestRoomFilterAssemblerSubscription to flip the LIVE/ENDED badge based on the freshest kind-10312 presence. That assembler subscribes to chat + presence + reactions (kinds 1311, 10312, 7) — so a feed of N rooms paid each popular room's full chat-history pull per outbox relay just to render a status pill. Add NestRoomLivenessAssembler, scoped to a single kinds=[10312], #a=[room], limit=1 filter per relay, and switch the feed thumbnail to it. The full chat / reaction / admin command REQ stays gated behind the join flow. https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4
This commit is contained in:
+3
@@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterA
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomLivenessAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
||||
@@ -109,6 +110,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val liveStreams = LiveStreamsFilterAssembler(client)
|
||||
val nests = NestsFilterAssembler(client)
|
||||
val nestRoom = NestRoomFilterAssembler(client)
|
||||
val nestRoomLiveness = NestRoomLivenessAssembler(client)
|
||||
val longs = LongsFilterAssembler(client)
|
||||
val articles = ArticlesFilterAssembler(client)
|
||||
val badges = BadgesFilterAssembler(client)
|
||||
@@ -135,6 +137,7 @@ class RelaySubscriptionsCoordinator(
|
||||
liveStreams,
|
||||
nests,
|
||||
nestRoom,
|
||||
nestRoomLiveness,
|
||||
longs,
|
||||
articles,
|
||||
badges,
|
||||
|
||||
+10
-10
@@ -78,7 +78,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53L
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.PrivateFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomLivenessProbeSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestBridge
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -300,14 +300,14 @@ private fun RenderLiveOrEndedFromPresence(
|
||||
|
||||
/**
|
||||
* Most recent kind-10312 presence `createdAt` cached for [address],
|
||||
* or null until one arrives. Reuses the room's existing assembler
|
||||
* subscription (`NestRoomFilterAssemblerSubscription`) — same
|
||||
* subscription that warms up when the user actually opens the room
|
||||
* — and reads version notes off the room's [LocalCache] channel,
|
||||
* which `LocalCache.consume(MeetingRoomPresenceEvent, …)` attaches
|
||||
* by `#a=room`. Trade-off: that REQ also pulls chat + reactions, so
|
||||
* a thumbnail probe pays a popular room's full message history. If
|
||||
* that load matters, swap to a dedicated limit:1 assembler.
|
||||
* or null until one arrives. Uses the dedicated
|
||||
* [NestRoomLivenessProbeSubscription] — `kinds=[10312], #a=[roomA],
|
||||
* limit=1` per outbox relay — so a feed of N rooms doesn't pay
|
||||
* each popular room's chat-history pull just to render a LIVE /
|
||||
* ENDED badge. The assembler reads version notes off the room's
|
||||
* [LocalCache] channel, which `LocalCache.consume(MeetingRoomPresenceEvent, …)`
|
||||
* attaches by `#a=room`. The full chat / reaction / admin REQs
|
||||
* stay gated behind the join flow (see [NestRoomFilterAssemblerSubscription]).
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
@@ -315,7 +315,7 @@ private fun observeRoomLatestPresence(
|
||||
note: AddressableNote,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Long?> {
|
||||
NestRoomFilterAssemblerSubscription(note, accountViewModel)
|
||||
NestRoomLivenessProbeSubscription(note, accountViewModel)
|
||||
|
||||
val channel = remember(note.idHex) { LocalCache.getOrCreateLiveChannel(note.address) }
|
||||
val flow =
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
|
||||
|
||||
/**
|
||||
* Per-room state for the lightweight liveness probe — used by feed
|
||||
* thumbnail cards to flip the LIVE / ENDED badge based on whether
|
||||
* any speaker has published a fresh kind-10312 presence.
|
||||
*
|
||||
* Distinct from [NestRoomQueryState] (which fans out to chat,
|
||||
* presence, reactions, and admin commands for an actively-joined
|
||||
* room) — the feed should not pay a popular room's full chat
|
||||
* history just to render a status pill.
|
||||
*/
|
||||
@Stable
|
||||
class NestRoomLivenessQueryState(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
/**
|
||||
* Single per-room sub-assembler for the feed thumbnail liveness
|
||||
* probe. Issues ONE narrow [Filter] per outbox relay:
|
||||
*
|
||||
* `kinds=[10312]`, `#a=[roomATag]`, `limit=1`
|
||||
*
|
||||
* Net wire change: the previous code reused [NestRoomFilterSubAssembler]
|
||||
* for the badge probe, which subscribes to chat + presence + reactions
|
||||
* (kinds 1311, 10312, 7) — so a feed of N rooms cost N popular-room
|
||||
* chat-history pulls per relay. This probe asks for the single
|
||||
* latest presence per room and nothing else.
|
||||
*/
|
||||
class NestRoomLivenessSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<NestRoomLivenessQueryState>,
|
||||
) : PerUniqueIdEoseManager<NestRoomLivenessQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: NestRoomLivenessQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val relays =
|
||||
key.account.outboxRelays.flow.value +
|
||||
key.note.relays +
|
||||
((key.note.event as? MeetingSpaceEvent)?.relays()?.toSet() ?: emptySet())
|
||||
|
||||
return relays.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(MeetingRoomPresenceEvent.KIND),
|
||||
tags = mapOf("a" to listOf(key.note.idHex)),
|
||||
// limit=1: the badge only needs the freshest
|
||||
// heartbeat — older ones don't change the
|
||||
// verdict. since is still threaded so the
|
||||
// EOSE manager can resume after a reconnect.
|
||||
limit = 1,
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id(key: NestRoomLivenessQueryState): String = key.note.idHex
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NestRoomLivenessAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<NestRoomLivenessQueryState>() {
|
||||
private val sub = NestRoomLivenessSubAssembler(client, ::allKeys)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = sub.invalidateFilters()
|
||||
|
||||
override fun destroy() = sub.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle-aware subscription. Stays open while the feed card is in
|
||||
* the tree (LazyColumn auto-disposes off-screen cards); closes on
|
||||
* dispose. Use this from feed thumbnails — the join flow uses
|
||||
* [NestRoomFilterAssemblerSubscription] which is heavier.
|
||||
*/
|
||||
@Composable
|
||||
fun NestRoomLivenessProbeSubscription(
|
||||
note: AddressableNote,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = NestRoomLivenessProbeSubscription(
|
||||
note,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().nestRoomLiveness,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NestRoomLivenessProbeSubscription(
|
||||
note: AddressableNote,
|
||||
account: Account,
|
||||
filterAssembler: NestRoomLivenessAssembler,
|
||||
) {
|
||||
val state =
|
||||
remember(note, account.pubKey) {
|
||||
NestRoomLivenessQueryState(note, account)
|
||||
}
|
||||
KeyDataSourceSubscription(state, filterAssembler)
|
||||
}
|
||||
Reference in New Issue
Block a user