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.livestreams.datasource.LiveStreamsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
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.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.nests.datasource.NestsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
||||||
@@ -109,6 +110,7 @@ class RelaySubscriptionsCoordinator(
|
|||||||
val liveStreams = LiveStreamsFilterAssembler(client)
|
val liveStreams = LiveStreamsFilterAssembler(client)
|
||||||
val nests = NestsFilterAssembler(client)
|
val nests = NestsFilterAssembler(client)
|
||||||
val nestRoom = NestRoomFilterAssembler(client)
|
val nestRoom = NestRoomFilterAssembler(client)
|
||||||
|
val nestRoomLiveness = NestRoomLivenessAssembler(client)
|
||||||
val longs = LongsFilterAssembler(client)
|
val longs = LongsFilterAssembler(client)
|
||||||
val articles = ArticlesFilterAssembler(client)
|
val articles = ArticlesFilterAssembler(client)
|
||||||
val badges = BadgesFilterAssembler(client)
|
val badges = BadgesFilterAssembler(client)
|
||||||
@@ -135,6 +137,7 @@ class RelaySubscriptionsCoordinator(
|
|||||||
liveStreams,
|
liveStreams,
|
||||||
nests,
|
nests,
|
||||||
nestRoom,
|
nestRoom,
|
||||||
|
nestRoomLiveness,
|
||||||
longs,
|
longs,
|
||||||
articles,
|
articles,
|
||||||
badges,
|
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.PrivateFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag
|
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.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.NestActivity
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestBridge
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestBridge
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -300,14 +300,14 @@ private fun RenderLiveOrEndedFromPresence(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Most recent kind-10312 presence `createdAt` cached for [address],
|
* Most recent kind-10312 presence `createdAt` cached for [address],
|
||||||
* or null until one arrives. Reuses the room's existing assembler
|
* or null until one arrives. Uses the dedicated
|
||||||
* subscription (`NestRoomFilterAssemblerSubscription`) — same
|
* [NestRoomLivenessProbeSubscription] — `kinds=[10312], #a=[roomA],
|
||||||
* subscription that warms up when the user actually opens the room
|
* limit=1` per outbox relay — so a feed of N rooms doesn't pay
|
||||||
* — and reads version notes off the room's [LocalCache] channel,
|
* each popular room's chat-history pull just to render a LIVE /
|
||||||
* which `LocalCache.consume(MeetingRoomPresenceEvent, …)` attaches
|
* ENDED badge. The assembler reads version notes off the room's
|
||||||
* by `#a=room`. Trade-off: that REQ also pulls chat + reactions, so
|
* [LocalCache] channel, which `LocalCache.consume(MeetingRoomPresenceEvent, …)`
|
||||||
* a thumbnail probe pays a popular room's full message history. If
|
* attaches by `#a=room`. The full chat / reaction / admin REQs
|
||||||
* that load matters, swap to a dedicated limit:1 assembler.
|
* stay gated behind the join flow (see [NestRoomFilterAssemblerSubscription]).
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -315,7 +315,7 @@ private fun observeRoomLatestPresence(
|
|||||||
note: AddressableNote,
|
note: AddressableNote,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
): State<Long?> {
|
): State<Long?> {
|
||||||
NestRoomFilterAssemblerSubscription(note, accountViewModel)
|
NestRoomLivenessProbeSubscription(note, accountViewModel)
|
||||||
|
|
||||||
val channel = remember(note.idHex) { LocalCache.getOrCreateLiveChannel(note.address) }
|
val channel = remember(note.idHex) { LocalCache.getOrCreateLiveChannel(note.address) }
|
||||||
val flow =
|
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