mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
fix(concord): only bump session revision on structural change, not per message
Every inbound plane wrap that a session claimed — including a plain chat message — bumped ConcordSessionManager.revision, and the always-on preload, the channel subscription, and the open-channel history subscription each call invalidateFilters() on every bump. So every message re-derived and re-REQ'd every community's control + channel planes. On a cold load of hundreds of buffered messages that is hundreds of re-subscriptions, which the relays answer with "there is a bug in the client, no one should be making so many requests" and close the plane subs mid-load (each needing a fresh NIP-42 AUTH). The result: channels load only their last few messages, or none. ingest() now reports a ConcordIngestOutcome (NOT_MINE / NON_STRUCTURAL / STRUCTURAL). Only a STRUCTURAL wrap — a Control-Plane fold, a guestbook membership change, or a buffered base-rekey — bumps the revision. Chat messages are NON_STRUCTURAL: they still reach the feed via the rumor sink → LocalCache, but no longer churn the subscriptions. The manager keeps its Boolean contract (claimed) for DecryptAndIndexProcessor. Verified on-device: the "so many requests" rate-limit is gone and the plane subscription stays open and drains steadily instead of being closed and reopened per message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
413bf726fd
commit
9f55794e06
@@ -5022,9 +5022,9 @@ class Account(
|
||||
|
||||
// Keep Concord channel metadata (community name/icon, membership) live across the whole
|
||||
// app — not just the hub screen — so the Messages tab renders each channel's community
|
||||
// chip, and per-community bans apply, as soon as a Control Plane folds. The revision bumps
|
||||
// on every ingested message, so sample() coalesces bursts into at most one full re-index
|
||||
// per window instead of re-scanning every channel's notes per message.
|
||||
// chip, and per-community bans apply, as soon as a Control Plane folds. The revision now
|
||||
// bumps only on *structural* change (a fold / membership / rekey, never a plain message),
|
||||
// so this fires rarely; sample() stays as a cheap coalescer for a burst of folds.
|
||||
scope.launch {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
concordSessions.revision.sample(500).collect {
|
||||
|
||||
+42
-10
@@ -41,6 +41,29 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
*/
|
||||
typealias ConcordRumorSink = (communityId: HexKey, channelIdHex: HexKey, rumor: Event) -> Unit
|
||||
|
||||
/**
|
||||
* The result of feeding one wrap to a session's [ConcordCommunitySession.ingest]. It separates
|
||||
* "was it ours" from "did it change structure", so only structure-changing wraps bump the session
|
||||
* revision (and thus re-derive plane subscriptions). Landing every chat message as a revision bump
|
||||
* re-REQs every plane per message and gets the client rate-limited off the relays.
|
||||
*/
|
||||
enum class ConcordIngestOutcome {
|
||||
/** The wrap is not addressed to any plane this session knows. Keep routing it elsewhere. */
|
||||
NOT_MINE,
|
||||
|
||||
/** Ours and applied, but nothing the subscription set / folded structure depends on changed —
|
||||
* a chat/reaction/reply/delete message landing, or a duplicate wrap. Must NOT bump the revision. */
|
||||
NON_STRUCTURAL,
|
||||
|
||||
/** Ours and changed structure: a Control-Plane fold (metadata/channels/membership/authority), a
|
||||
* guestbook membership change, or a buffered base-rekey. Bumps the revision. */
|
||||
STRUCTURAL,
|
||||
;
|
||||
|
||||
/** True when the wrap belonged to this session (whether or not it changed structure). */
|
||||
val claimed get() = this != NOT_MINE
|
||||
}
|
||||
|
||||
/**
|
||||
* The live read-model of one joined Concord community, driven by inbound stream
|
||||
* wraps fed via [ingest].
|
||||
@@ -155,38 +178,47 @@ class ConcordCommunitySession(
|
||||
/**
|
||||
* Ingests a stream [wrap]. If it belongs to this community's Control Plane it
|
||||
* re-folds; if it belongs to a known channel plane it re-projects that
|
||||
* channel's messages. Returns true if the wrap was recognized and applied.
|
||||
* channel's messages. The [ConcordIngestOutcome] tells the caller both whether
|
||||
* the wrap was ours and — crucially — whether it changed *structure* (a fold that
|
||||
* moves the subscription set / metadata) versus just landing a chat message. Only
|
||||
* a [ConcordIngestOutcome.STRUCTURAL] result should bump the session revision;
|
||||
* bumping on every message re-derives every plane's REQ per message and rate-limits
|
||||
* the relays (they close the plane subs mid-load, so channels appear empty).
|
||||
*/
|
||||
fun ingest(wrap: Event): Boolean {
|
||||
fun ingest(wrap: Event): ConcordIngestOutcome {
|
||||
when (wrap.pubKey) {
|
||||
controlPlaneAddress -> {
|
||||
lock.withLock {
|
||||
if (controlWraps.put(wrap.id, wrap) != null) return true // dup
|
||||
if (controlWraps.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup
|
||||
}
|
||||
refold()
|
||||
return true
|
||||
return ConcordIngestOutcome.STRUCTURAL
|
||||
}
|
||||
guestbookAddress -> {
|
||||
lock.withLock {
|
||||
if (guestbookWraps.put(wrap.id, wrap) != null) return true // dup
|
||||
if (guestbookWraps.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup
|
||||
}
|
||||
refoldGuestbook()
|
||||
return true
|
||||
return ConcordIngestOutcome.STRUCTURAL
|
||||
}
|
||||
nextBaseRekeyAddress -> {
|
||||
// Buffer only — decrypting a base-rotation blob needs the account signer, so the
|
||||
// app layer drains [pendingBaseRekeyWraps] with it and authorizes the rotator.
|
||||
// app layer drains [pendingBaseRekeyWraps] with it and authorizes the rotator. That
|
||||
// drain runs off the revision tick, so a buffered rekey must bump (rare — a rekey,
|
||||
// not a message).
|
||||
lock.withLock { baseRekeyWraps[wrap.id] = wrap }
|
||||
return true
|
||||
return ConcordIngestOutcome.STRUCTURAL
|
||||
}
|
||||
else -> {
|
||||
val channelRef = lock.withLock { channelKeysByAddress[wrap.pubKey] } ?: return false
|
||||
val channelRef = lock.withLock { channelKeysByAddress[wrap.pubKey] } ?: return ConcordIngestOutcome.NOT_MINE
|
||||
val (channelIdHex, _) = channelRef
|
||||
lock.withLock {
|
||||
channelWrapsById.getOrPut(channelIdHex) { LinkedHashMap() }.put(wrap.id, wrap)
|
||||
}
|
||||
reprojectChannel(channelIdHex)
|
||||
return true
|
||||
// A chat message lands in the feed via [onRumor] → LocalCache, independent of the
|
||||
// revision; it changes no plane address, so it must NOT bump (see the storm note above).
|
||||
return ConcordIngestOutcome.NON_STRUCTURAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -123,11 +123,17 @@ class ConcordSessionManager(
|
||||
return out
|
||||
}
|
||||
|
||||
/** Route an inbound stream wrap; true if it was a Concord plane wrap we applied. */
|
||||
/**
|
||||
* Route an inbound stream wrap; true if it was a Concord plane wrap we applied. Only a wrap that
|
||||
* changed community *structure* (a fold, a membership/rekey change — not a plain chat message)
|
||||
* bumps the revision: bumping per message re-derives every plane subscription per message and
|
||||
* gets the client rate-limited off the relays (which then close the plane subs mid-load). Chat
|
||||
* messages still reach the feed via the rumor sink → LocalCache, independent of the revision.
|
||||
*/
|
||||
fun ingest(wrap: Event): Boolean {
|
||||
val applied = registry.ingest(wrap)
|
||||
if (applied) bumpRevision()
|
||||
return applied
|
||||
val outcome = registry.ingest(wrap)
|
||||
if (outcome == ConcordIngestOutcome.STRUCTURAL) bumpRevision()
|
||||
return outcome.claimed
|
||||
}
|
||||
|
||||
fun sessions() = registry.sessions()
|
||||
|
||||
+7
-6
@@ -98,16 +98,17 @@ class ConcordSessionRegistry(
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes an inbound stream [wrap] to whichever session recognizes it. Returns
|
||||
* true if some session applied it. A wrap belongs to at most one plane, so the
|
||||
* first accepting session wins.
|
||||
* Routes an inbound stream [wrap] to whichever session recognizes it, returning that session's
|
||||
* [ConcordIngestOutcome] (or [ConcordIngestOutcome.NOT_MINE] if none claim it). A wrap belongs to
|
||||
* at most one plane, so the first accepting session wins.
|
||||
*/
|
||||
fun ingest(wrap: Event): Boolean {
|
||||
fun ingest(wrap: Event): ConcordIngestOutcome {
|
||||
val snapshot = lock.withLock { sessions.values.toList() }
|
||||
for (session in snapshot) {
|
||||
if (session.ingest(wrap)) return true
|
||||
val outcome = session.ingest(wrap)
|
||||
if (outcome != ConcordIngestOutcome.NOT_MINE) return outcome
|
||||
}
|
||||
return false
|
||||
return ConcordIngestOutcome.NOT_MINE
|
||||
}
|
||||
|
||||
fun clear() = lock.withLock { sessions.clear() }
|
||||
|
||||
+9
-6
@@ -54,8 +54,9 @@ class ConcordCommunitySessionTest {
|
||||
val session = ConcordCommunitySession(entry, owner.pubKey) { communityId, channelIdHex, rumor -> captured += Triple(communityId, channelIdHex, rumor) }
|
||||
assertEquals(community.controlPlane.publicKeyHex, session.controlPlaneAddress)
|
||||
|
||||
// Feed the genesis control wraps → state folds, channels + membership resolve.
|
||||
community.genesisWraps.forEach { assertTrue(session.ingest(it)) }
|
||||
// Feed the genesis control wraps → state folds, channels + membership resolve. A fold is
|
||||
// STRUCTURAL (it moves the subscription set), so it's allowed to bump the revision.
|
||||
community.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL, session.ingest(it)) }
|
||||
val state = session.state.value
|
||||
assertEquals("Nostrichs", state?.metadata?.name)
|
||||
assertTrue(state!!.channels.containsKey(community.generalChannelIdHex))
|
||||
@@ -67,7 +68,9 @@ class ConcordCommunitySessionTest {
|
||||
|
||||
// A channel message wrap decrypts and is emitted to the sink for #general.
|
||||
val msgWrap = ConcordActions.buildChannelMessage(owner, general, community.generalChannelIdHex, community.rootEpoch, "gm all", 2L)
|
||||
assertTrue(session.ingest(msgWrap))
|
||||
// A chat message lands in the feed but is NON_STRUCTURAL: it must never bump the revision
|
||||
// (per-message re-subscription is what rate-limited the plane REQs and emptied channels).
|
||||
assertEquals(ConcordIngestOutcome.NON_STRUCTURAL, session.ingest(msgWrap))
|
||||
val general9 = captured.filter { it.second == community.generalChannelIdHex && it.third.content == "gm all" }
|
||||
assertEquals(1, general9.size)
|
||||
assertEquals(community.communityIdHex, general9[0].first)
|
||||
@@ -76,7 +79,7 @@ class ConcordCommunitySessionTest {
|
||||
|
||||
// A reaction to that message decrypts as a kind-7 bound to the channel, e-tagging the target.
|
||||
val reactionWrap = ConcordActions.buildChannelReaction(owner, general, community.generalChannelIdHex, community.rootEpoch, message, "🤙", 3L)
|
||||
assertTrue(session.ingest(reactionWrap))
|
||||
assertEquals(ConcordIngestOutcome.NON_STRUCTURAL, session.ingest(reactionWrap))
|
||||
val reaction = captured.map { it.third }.first { it.kind == 7 }
|
||||
assertEquals("🤙", reaction.content)
|
||||
assertEquals(message.id, reaction.tags.first { it[0] == "e" }[1])
|
||||
@@ -85,7 +88,7 @@ class ConcordCommunitySessionTest {
|
||||
// root and lowercase `e` at the immediate parent (both the message here), still bound
|
||||
// to the channel so it groups into the message's thread — the shape Armada threads.
|
||||
val replyWrap = ConcordActions.buildChannelReply(owner, general, community.generalChannelIdHex, community.rootEpoch, message, "gm back", 4L)
|
||||
assertTrue(session.ingest(replyWrap))
|
||||
assertEquals(ConcordIngestOutcome.NON_STRUCTURAL, session.ingest(replyWrap))
|
||||
val reply = captured.map { it.third }.first { it.content == "gm back" }
|
||||
assertEquals(1111, reply.kind)
|
||||
assertEquals(message.id, reply.tags.first { it[0] == "E" }[1])
|
||||
@@ -94,6 +97,6 @@ class ConcordCommunitySessionTest {
|
||||
|
||||
// A stray wrap from a different community is ignored.
|
||||
val outsider = ConcordCommunityFactory.create(owner, "Other", createdAt = 1L, relays = listOf("wss://r.example"))
|
||||
assertTrue(!session.ingest(outsider.genesisWraps.first()))
|
||||
assertEquals(ConcordIngestOutcome.NOT_MINE, session.ingest(outsider.genesisWraps.first()))
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
@@ -70,8 +69,8 @@ class ConcordSessionRegistryTest {
|
||||
assertTrue(registry.subscribeAddresses().contains(alpha.controlPlane.publicKeyHex))
|
||||
assertTrue(registry.subscribeAddresses().contains(beta.controlPlane.publicKeyHex))
|
||||
|
||||
// A genesis control wrap routes to Alpha's session and folds it.
|
||||
alpha.genesisWraps.forEach { assertTrue(registry.ingest(it)) }
|
||||
// A genesis control wrap routes to Alpha's session and folds it (STRUCTURAL).
|
||||
alpha.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL, registry.ingest(it)) }
|
||||
val alphaState = registry.sessionFor(alpha.communityIdHex)!!.state.value
|
||||
assertEquals("Alpha", alphaState?.metadata?.name)
|
||||
|
||||
@@ -81,7 +80,7 @@ class ConcordSessionRegistryTest {
|
||||
|
||||
// A channel message decrypts and is emitted to the sink for Alpha's #general.
|
||||
val msg = ConcordActions.buildChannelMessage(owner, general, alpha.generalChannelIdHex, alpha.rootEpoch, "gm", 2L)
|
||||
assertTrue(registry.ingest(msg))
|
||||
assertEquals(ConcordIngestOutcome.NON_STRUCTURAL, registry.ingest(msg))
|
||||
val general9 = captured.filter { it.first == alpha.communityIdHex && it.second == alpha.generalChannelIdHex && it.third.content == "gm" }
|
||||
assertEquals(1, general9.size)
|
||||
|
||||
@@ -101,6 +100,6 @@ class ConcordSessionRegistryTest {
|
||||
|
||||
// A wrap from an unknown community is routed nowhere.
|
||||
val gamma = ConcordCommunityFactory.create(owner, "Gamma", createdAt = 1L, relays = listOf("wss://r.example"))
|
||||
assertFalse(registry.ingest(gamma.genesisWraps.first()))
|
||||
assertEquals(ConcordIngestOutcome.NOT_MINE, registry.ingest(gamma.genesisWraps.first()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user