mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
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:
+4
-22
@@ -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
|
||||
|
||||
+35
@@ -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) }
|
||||
}
|
||||
|
||||
+32
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user