refactor(concord): move per-relay filter assembly to shared planner

The Android assembler was hand-rolling the plane-address -> per-relay
kind-1059 RelayBasedFilter collapse (with per-relay since). That logic is
platform-agnostic — RelayBasedFilter and SincePerRelayMap are both
commons/quartz types — so lift it to ConcordSubscriptionPlanner.relayBasedFilters
alongside the existing filtersByRelay one-shot variant. The assembler now only
does the account-dependent step (deriving channel planes from folded session
state) and delegates the collapse. Covered by a new planner unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
Claude
2026-07-11 00:16:05 +00:00
parent bafa0cca9a
commit b4fe6dffcd
3 changed files with 71 additions and 22 deletions
@@ -26,11 +26,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.concord.events.ConcordKinds
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.nip01Core.relay.normalizer.NormalizedRelayUrl
/** One screen's request to keep the user's joined Concord Channels live. */
class ConcordChannelQueryState(
@@ -76,7 +73,9 @@ class ConcordChannelSubAssembler(
if (entries.isEmpty()) return null
// Control planes for every joined community, plus channel planes for the
// ones whose Control Plane has already folded.
// ones whose Control Plane has already folded. Deriving the channel planes
// is the only account-dependent step; collapsing planes into per-relay
// kind-1059 filters lives in the shared planner.
val subs = ArrayList<ConcordPlaneSub>()
subs += ConcordSubscriptionPlanner.controlPlaneSubs(entries)
for (entry in entries) {
@@ -88,24 +87,7 @@ class ConcordChannelSubAssembler(
subs += ConcordSubscriptionPlanner.channelPlaneSubs(entry, state)
}
// One kind-1059 filter per host relay, carrying every plane address on it.
val authorsByRelay = HashMap<NormalizedRelayUrl, MutableSet<String>>()
for (sub in subs) {
for (relay in sub.relays) authorsByRelay.getOrPut(relay) { HashSet() }.add(sub.pubKeyHex)
}
if (authorsByRelay.isEmpty()) return null
return authorsByRelay.map { (relay, authors) ->
RelayBasedFilter(
relay = relay,
filter =
Filter(
kinds = listOf(ConcordKinds.WRAP),
authors = authors.toList(),
since = since?.get(relay)?.time,
),
)
}
return ConcordSubscriptionPlanner.relayBasedFilters(subs, since)
}
override fun id(key: ConcordChannelQueryState) = key.account
@@ -20,10 +20,13 @@
*/
package com.vitorpamplona.amethyst.commons.actions
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityState
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
@@ -89,5 +92,37 @@ object ConcordSubscriptionPlanner {
return authorsByRelay.mapValues { (_, authors) -> listOf(ConcordActions.planeFilterFor(authors)) }
}
/**
* Collapses [subs] into one [RelayBasedFilter] per host relay for a live
* subscription: each relay gets a single `{kinds:[1059], authors:[…all plane
* pks on it…], since}` filter, with [since] applied per relay from the EOSE
* map. Returns null when no plane resolves to a relay (nothing to subscribe).
*
* This is the assembler-facing shape (what a `PerUniqueIdEoseManager` returns);
* [filtersByRelay] is the one-shot drain shape (no `since`).
*/
fun relayBasedFilters(
subs: List<ConcordPlaneSub>,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
val authorsByRelay = HashMap<NormalizedRelayUrl, MutableSet<String>>()
for (sub in subs) {
for (relay in sub.relays) authorsByRelay.getOrPut(relay) { HashSet() }.add(sub.pubKeyHex)
}
if (authorsByRelay.isEmpty()) return null
return authorsByRelay.map { (relay, authors) ->
RelayBasedFilter(
relay = relay,
filter =
Filter(
kinds = listOf(ConcordKinds.WRAP),
authors = authors.toList(),
since = since?.get(relay)?.time,
),
)
}
}
private fun normalize(urls: List<String>): Set<NormalizedRelayUrl> = urls.mapNotNullTo(mutableSetOf()) { RelayUrlNormalizer.normalizeOrNull(it) }
}
@@ -20,13 +20,16 @@
*/
package com.vitorpamplona.amethyst.commons.actions
import com.vitorpamplona.amethyst.commons.relays.MutableTime
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityFactory
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ConcordSubscriptionPlannerTest {
@@ -71,4 +74,33 @@ class ConcordSubscriptionPlannerTest {
assertTrue(filter.authors!!.contains(community.controlPlane.publicKeyHex))
assertTrue(filter.authors!!.contains(general.pubKeyHex))
}
@Test
fun relayBasedFiltersCollapsePerRelayAndApplySince() =
runTest {
val community = ConcordCommunityFactory.create(owner, "Nostrichs", createdAt = 1L, relays = listOf("wss://r.example"))
val entry =
com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry(
id = community.communityIdHex,
owner = community.ownerPubKey,
ownerSalt = community.ownerSalt.toHexKey(),
root = community.communityRoot.toHexKey(),
rootEpoch = community.rootEpoch,
relays = listOf("wss://r.example"),
name = "Nostrichs",
)
val subs = ConcordSubscriptionPlanner.controlPlaneSubs(listOf(entry))
val relay = RelayUrlNormalizer.normalizeOrNull("wss://r.example")!!
// One kind-1059 filter for the single relay, carrying the derived since.
val filters = ConcordSubscriptionPlanner.relayBasedFilters(subs, mutableMapOf(relay to MutableTime(1234L)))!!
assertEquals(1, filters.size)
assertEquals(relay, filters[0].relay)
assertEquals(listOf(1059), filters[0].filter.kinds)
assertEquals(1234L, filters[0].filter.since)
assertTrue(filters[0].filter.authors!!.contains(community.controlPlane.publicKeyHex))
// No planes resolve to a relay -> nothing to subscribe.
assertNull(ConcordSubscriptionPlanner.relayBasedFilters(emptyList(), null))
}
}