perf(concord): stop rebuilding Notifications on every fold while an invite is pending

The pending Direct Invites fold in every session revision, and
`visible()` mints new views each time. The views are plain classes, so
the StateFlow never saw an equal value and re-emitted on every fold. Each
emission clears and rebuilds all three Notifications feeds and Messages'
New Requests. So an unanswered invite forced a full notification rescan
on every Control Plane fold, back to back during boot.

Emit only when what a row shows or ranks by changed: wrap id, catch-up,
expired, followed sender, new channels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-09-30 10:11:15 -04:00
co-authored by Claude Opus 5.5
parent d8dba40e5b
commit 6fdbe44288
@@ -126,6 +126,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -949,8 +950,20 @@ class AccountConcordActions(
visibleConcordDirectInvites(),
acceptingConcordDirectInvites,
) { visible, accepting -> if (accepting.isEmpty()) visible else visible.filterNot { it.wrapId in accepting } }
// `visible()` re-runs on every session revision and mints new (identity-equal only) views,
// and each emission clears and rebuilds every Notifications feed. Emit only when something
// a row shows or ranks by actually moved.
.distinctUntilChangedBy { list -> list.map { InviteRowKey(it.wrapId, it.catchUp, it.expired, it.followedSender, it.newChannelIds) } }
.stateIn(account.scope, SharingStarted.Eagerly, emptyList())
private data class InviteRowKey(
val wrapId: HexKey,
val catchUp: Boolean,
val expired: Boolean,
val followedSender: Boolean,
val newChannelIds: List<HexKey>,
)
private fun visibleConcordDirectInvites() =
combine(
directInviteInbox.pending,