mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #3658 from vitorpamplona/fix/concord-messages-cold-boot
fix: Concord groups missing or slow on the Messages tab after a cold boot
This commit is contained in:
@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.model.UiSettings
|
|||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||||
@@ -170,6 +171,7 @@ private object PrefKeys {
|
|||||||
const val LATEST_GEOHASH_LIST = "latestGeohashList"
|
const val LATEST_GEOHASH_LIST = "latestGeohashList"
|
||||||
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
|
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
|
||||||
const val LATEST_RELAY_GROUP_LIST = "latestRelayGroupList"
|
const val LATEST_RELAY_GROUP_LIST = "latestRelayGroupList"
|
||||||
|
const val LATEST_CONCORD_LIST = "latestConcordList"
|
||||||
const val LATEST_TRUST_PROVIDER_LIST = "latestTrustProviderList"
|
const val LATEST_TRUST_PROVIDER_LIST = "latestTrustProviderList"
|
||||||
const val LATEST_KEY_PACKAGE_RELAY_LIST = "latestKeyPackageRelayList"
|
const val LATEST_KEY_PACKAGE_RELAY_LIST = "latestKeyPackageRelayList"
|
||||||
const val LATEST_FAVORITE_ALGO_FEEDS_LIST = "latestFavoriteAlgoFeedsList"
|
const val LATEST_FAVORITE_ALGO_FEEDS_LIST = "latestFavoriteAlgoFeedsList"
|
||||||
@@ -581,6 +583,7 @@ object LocalPreferences {
|
|||||||
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
|
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
|
||||||
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
|
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
|
||||||
putOrRemove(PrefKeys.LATEST_RELAY_GROUP_LIST, settings.backupRelayGroupList)
|
putOrRemove(PrefKeys.LATEST_RELAY_GROUP_LIST, settings.backupRelayGroupList)
|
||||||
|
putOrRemove(PrefKeys.LATEST_CONCORD_LIST, settings.backupConcordList)
|
||||||
putOrRemove(PrefKeys.LATEST_TRUST_PROVIDER_LIST, settings.backupTrustProviderList)
|
putOrRemove(PrefKeys.LATEST_TRUST_PROVIDER_LIST, settings.backupTrustProviderList)
|
||||||
putOrRemove(PrefKeys.LATEST_KEY_PACKAGE_RELAY_LIST, settings.backupKeyPackageRelayList)
|
putOrRemove(PrefKeys.LATEST_KEY_PACKAGE_RELAY_LIST, settings.backupKeyPackageRelayList)
|
||||||
putOrRemove(PrefKeys.LATEST_FAVORITE_ALGO_FEEDS_LIST, settings.backupFavoriteAlgoFeedsList)
|
putOrRemove(PrefKeys.LATEST_FAVORITE_ALGO_FEEDS_LIST, settings.backupFavoriteAlgoFeedsList)
|
||||||
@@ -769,6 +772,7 @@ object LocalPreferences {
|
|||||||
val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null)
|
val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null)
|
||||||
val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null)
|
val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null)
|
||||||
val latestRelayGroupListStr = getString(PrefKeys.LATEST_RELAY_GROUP_LIST, null)
|
val latestRelayGroupListStr = getString(PrefKeys.LATEST_RELAY_GROUP_LIST, null)
|
||||||
|
val latestConcordListStr = getString(PrefKeys.LATEST_CONCORD_LIST, null)
|
||||||
val latestTrustProviderListStr = getString(PrefKeys.LATEST_TRUST_PROVIDER_LIST, null)
|
val latestTrustProviderListStr = getString(PrefKeys.LATEST_TRUST_PROVIDER_LIST, null)
|
||||||
val latestKeyPackageRelayListStr = getString(PrefKeys.LATEST_KEY_PACKAGE_RELAY_LIST, null)
|
val latestKeyPackageRelayListStr = getString(PrefKeys.LATEST_KEY_PACKAGE_RELAY_LIST, null)
|
||||||
val latestFavoriteAlgoFeedsListStr = getString(PrefKeys.LATEST_FAVORITE_ALGO_FEEDS_LIST, null)
|
val latestFavoriteAlgoFeedsListStr = getString(PrefKeys.LATEST_FAVORITE_ALGO_FEEDS_LIST, null)
|
||||||
@@ -831,6 +835,7 @@ object LocalPreferences {
|
|||||||
val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) }
|
val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) }
|
||||||
val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) }
|
val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) }
|
||||||
val latestRelayGroupList = async { parseEventOrNull<SimpleGroupListEvent>(latestRelayGroupListStr) }
|
val latestRelayGroupList = async { parseEventOrNull<SimpleGroupListEvent>(latestRelayGroupListStr) }
|
||||||
|
val latestConcordList = async { parseEventOrNull<ConcordCommunityListEvent>(latestConcordListStr) }
|
||||||
val latestTrustProviderList = async { parseEventOrNull<TrustProviderListEvent>(latestTrustProviderListStr) }
|
val latestTrustProviderList = async { parseEventOrNull<TrustProviderListEvent>(latestTrustProviderListStr) }
|
||||||
val latestKeyPackageRelayList = async { parseEventOrNull<KeyPackageRelayListEvent>(latestKeyPackageRelayListStr) }
|
val latestKeyPackageRelayList = async { parseEventOrNull<KeyPackageRelayListEvent>(latestKeyPackageRelayListStr) }
|
||||||
val latestFavoriteAlgoFeedsList = async { parseEventOrNull<FavoriteAlgoFeedsListEvent>(latestFavoriteAlgoFeedsListStr) }
|
val latestFavoriteAlgoFeedsList = async { parseEventOrNull<FavoriteAlgoFeedsListEvent>(latestFavoriteAlgoFeedsListStr) }
|
||||||
@@ -885,6 +890,7 @@ object LocalPreferences {
|
|||||||
val latestGeohashListResolved = latestGeohashList.await()
|
val latestGeohashListResolved = latestGeohashList.await()
|
||||||
val latestEphemeralListResolved = latestEphemeralList.await()
|
val latestEphemeralListResolved = latestEphemeralList.await()
|
||||||
val latestRelayGroupListResolved = latestRelayGroupList.await()
|
val latestRelayGroupListResolved = latestRelayGroupList.await()
|
||||||
|
val latestConcordListResolved = latestConcordList.await()
|
||||||
val latestTrustProviderListResolved = latestTrustProviderList.await()
|
val latestTrustProviderListResolved = latestTrustProviderList.await()
|
||||||
val latestKeyPackageRelayListResolved = latestKeyPackageRelayList.await()
|
val latestKeyPackageRelayListResolved = latestKeyPackageRelayList.await()
|
||||||
val latestFavoriteAlgoFeedsListResolved = latestFavoriteAlgoFeedsList.await()
|
val latestFavoriteAlgoFeedsListResolved = latestFavoriteAlgoFeedsList.await()
|
||||||
@@ -981,6 +987,7 @@ object LocalPreferences {
|
|||||||
backupGeohashList = latestGeohashListResolved,
|
backupGeohashList = latestGeohashListResolved,
|
||||||
backupEphemeralChatList = latestEphemeralListResolved,
|
backupEphemeralChatList = latestEphemeralListResolved,
|
||||||
backupRelayGroupList = latestRelayGroupListResolved,
|
backupRelayGroupList = latestRelayGroupListResolved,
|
||||||
|
backupConcordList = latestConcordListResolved,
|
||||||
backupTrustProviderList = latestTrustProviderListResolved,
|
backupTrustProviderList = latestTrustProviderListResolved,
|
||||||
backupKeyPackageRelayList = latestKeyPackageRelayListResolved,
|
backupKeyPackageRelayList = latestKeyPackageRelayListResolved,
|
||||||
backupFavoriteAlgoFeedsList = latestFavoriteAlgoFeedsListResolved,
|
backupFavoriteAlgoFeedsList = latestFavoriteAlgoFeedsListResolved,
|
||||||
|
|||||||
+22
@@ -77,7 +77,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal.WebBookmar
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.dal.WorkoutFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.dal.WorkoutFeedFilter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AccountFeedContentStates(
|
class AccountFeedContentStates(
|
||||||
@@ -201,6 +203,26 @@ class AccountFeedContentStates(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Concord control-plane fold is what first reveals a community's channels (and what makes
|
||||||
|
// ConcordCommunitySession.state non-null, without which ChatroomListKnownFeedFilter emits
|
||||||
|
// nothing at all for that community). None of it flows through LocalCache.newEventBundles,
|
||||||
|
// so the additive path can't see it: a folded channel reaches the Messages tab only if a
|
||||||
|
// message for it happens to arrive afterwards. Cold boot therefore shows a *subset* of a
|
||||||
|
// community's channels, or omits a quiet community entirely, until some unrelated
|
||||||
|
// invalidation fires. Rebuild on every structural change instead. `revision` bumps only on
|
||||||
|
// fold/membership/rekey (never a plain message), and sample() coalesces the burst of folds
|
||||||
|
// that lands as each control plane catches up — the same pairing Account.kt uses to drive
|
||||||
|
// refreshConcordChannelIndex off this flow.
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
account.concordSessions.revision
|
||||||
|
.drop(1)
|
||||||
|
.sample(500)
|
||||||
|
.collect {
|
||||||
|
dmKnown.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Same for the Concord view mode (inline channels vs one row per community).
|
// Same for the Concord view mode (inline channels vs one row per community).
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
account.settings.concordViewMode
|
account.settings.concordViewMode
|
||||||
|
|||||||
+50
-7
@@ -62,8 +62,17 @@ enum class ConcordIngestOutcome {
|
|||||||
* a chat/reaction/reply/delete message landing, or a duplicate wrap. Must NOT bump the revision. */
|
* a chat/reaction/reply/delete message landing, or a duplicate wrap. Must NOT bump the revision. */
|
||||||
NON_STRUCTURAL,
|
NON_STRUCTURAL,
|
||||||
|
|
||||||
/** Ours and changed structure: a Control-Plane fold (metadata/channels/membership/authority), a
|
/** Ours and re-folded the Control Plane. The fold republishes [ConcordCommunitySession.state],
|
||||||
* guestbook membership change, or a buffered base-rekey. Bumps the revision. */
|
* so the session's own state watcher is what bumps the revision — and, because the folded state
|
||||||
|
* compares by value, only when the fold actually *changed* something. A control wrap that folds
|
||||||
|
* to an identical state (a prior-epoch wrap that doesn't move the anti-rollback floor, a role
|
||||||
|
* edition that touches nothing we subscribe on) therefore costs no bump at all. The manager must
|
||||||
|
* NOT bump on this outcome as well, or every control wrap counts twice. */
|
||||||
|
STRUCTURAL_FOLD,
|
||||||
|
|
||||||
|
/** Ours and changed structure *without* touching [ConcordCommunitySession.state]: a guestbook
|
||||||
|
* membership change (which republishes `members`) or a buffered base-rekey. No state watcher
|
||||||
|
* covers these, so the manager bumps the revision directly. */
|
||||||
STRUCTURAL,
|
STRUCTURAL,
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -145,6 +154,22 @@ class ConcordCommunitySession(
|
|||||||
// Deduped inbound wraps.
|
// Deduped inbound wraps.
|
||||||
private val controlWraps = LinkedHashMap<HexKey, Event>()
|
private val controlWraps = LinkedHashMap<HexKey, Event>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypted control editions memoized by wrap id.
|
||||||
|
*
|
||||||
|
* Both [refold] and [controlFloorsLocked] fold their WHOLE buffer on every inbound control
|
||||||
|
* wrap, and turning a wrap into an edition is a NIP-44 open + parse. Re-deriving them each
|
||||||
|
* time made a cold-boot backfill quadratic in decryptions — one measured boot did ~8.6k opens
|
||||||
|
* to ingest 93 control wraps for a single community. Memoizing makes it one open per wrap.
|
||||||
|
*
|
||||||
|
* Wrap ids are unique and a wrap only ever belongs to one plane (it is routed by `pubKey`), so
|
||||||
|
* a single id-keyed map is safe across the current and prior-epoch Control Planes even though
|
||||||
|
* they open under different keys. A wrap that fails to open caches `null` so it is not retried
|
||||||
|
* on every subsequent fold. The wrap buffers are only ever added to, so this tracks their
|
||||||
|
* lifetime exactly and needs no separate eviction.
|
||||||
|
*/
|
||||||
|
private val editionByWrapId = HashMap<HexKey, ControlEdition?>()
|
||||||
|
|
||||||
// Prior-epoch Control Plane address -> (wrapId -> wrap). Kept apart from [controlWraps]: these
|
// Prior-epoch Control Plane address -> (wrapId -> wrap). Kept apart from [controlWraps]: these
|
||||||
// never join the live fold, they only produce the anti-rollback floor.
|
// never join the live fold, they only produce the anti-rollback floor.
|
||||||
private val historicalControlWraps = HashMap<HexKey, LinkedHashMap<HexKey, Event>>()
|
private val historicalControlWraps = HashMap<HexKey, LinkedHashMap<HexKey, Event>>()
|
||||||
@@ -280,7 +305,7 @@ class ConcordCommunitySession(
|
|||||||
fun auxStreamKeys(): List<GroupKey> = listOf(guestbookKey, nextBaseRekeyKey)
|
fun auxStreamKeys(): List<GroupKey> = listOf(guestbookKey, nextBaseRekeyKey)
|
||||||
|
|
||||||
/** The community's current Control Plane editions — the input a moderation edition chains onto. */
|
/** The community's current Control Plane editions — the input a moderation edition chains onto. */
|
||||||
fun controlEditions(): List<ControlEdition> = lock.withLock { ConcordActions.controlEditions(controlWraps.values.toList(), controlPlaneKey) }
|
fun controlEditions(): List<ControlEdition> = lock.withLock { editionsLocked(controlWraps.values.toList(), controlPlaneKey) }
|
||||||
|
|
||||||
/** The raw Control Plane wraps buffered so far — the input a Refounding compacts (CORD-06 §3). */
|
/** The raw Control Plane wraps buffered so far — the input a Refounding compacts (CORD-06 §3). */
|
||||||
fun controlPlaneWraps(): List<Event> = lock.withLock { controlWraps.values.toList() }
|
fun controlPlaneWraps(): List<Event> = lock.withLock { controlWraps.values.toList() }
|
||||||
@@ -314,7 +339,7 @@ class ConcordCommunitySession(
|
|||||||
if (controlWraps.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup
|
if (controlWraps.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup
|
||||||
}
|
}
|
||||||
refold()
|
refold()
|
||||||
return ConcordIngestOutcome.STRUCTURAL
|
return ConcordIngestOutcome.STRUCTURAL_FOLD
|
||||||
}
|
}
|
||||||
guestbookAddress -> {
|
guestbookAddress -> {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
@@ -341,7 +366,7 @@ class ConcordCommunitySession(
|
|||||||
if (buffer.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup
|
if (buffer.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup
|
||||||
}
|
}
|
||||||
refold()
|
refold()
|
||||||
return ConcordIngestOutcome.STRUCTURAL
|
return ConcordIngestOutcome.STRUCTURAL_FOLD
|
||||||
}
|
}
|
||||||
val current = lock.withLock { channelKeysByAddress[wrap.pubKey] }
|
val current = lock.withLock { channelKeysByAddress[wrap.pubKey] }
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
@@ -422,7 +447,7 @@ class ConcordCommunitySession(
|
|||||||
val wraps = controlWraps.values.toList()
|
val wraps = controlWraps.values.toList()
|
||||||
val folded =
|
val folded =
|
||||||
ConcordCommunityState.fold(
|
ConcordCommunityState.fold(
|
||||||
ConcordActions.controlEditions(wraps, controlPlaneKey),
|
editionsLocked(wraps, controlPlaneKey),
|
||||||
entry.owner,
|
entry.owner,
|
||||||
controlFloorsLocked(),
|
controlFloorsLocked(),
|
||||||
)
|
)
|
||||||
@@ -455,6 +480,24 @@ class ConcordCommunitySession(
|
|||||||
for (channelIdHex in newChannels) reprojectChannel(channelIdHex)
|
for (channelIdHex in newChannels) reprojectChannel(channelIdHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [wraps] opened into editions through [editionByWrapId], so a wrap is only ever decrypted
|
||||||
|
* once no matter how many folds it participates in. Caller must hold [lock].
|
||||||
|
*/
|
||||||
|
private fun editionsLocked(
|
||||||
|
wraps: Collection<Event>,
|
||||||
|
planeKey: GroupKey,
|
||||||
|
): List<ControlEdition> =
|
||||||
|
wraps.mapNotNull { wrap ->
|
||||||
|
if (editionByWrapId.containsKey(wrap.id)) {
|
||||||
|
editionByWrapId[wrap.id]
|
||||||
|
} else {
|
||||||
|
val edition = ConcordStreamEnvelope.openOrNull(wrap, planeKey)?.let { ControlEdition.fromRumor(it.rumor) }
|
||||||
|
editionByWrapId[wrap.id] = edition
|
||||||
|
edition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The per-entity anti-rollback floor: the authority-gated heads of every prior epoch's
|
* The per-entity anti-rollback floor: the authority-gated heads of every prior epoch's
|
||||||
* Control Plane we still hold a root for, folded **oldest epoch first** so each epoch is
|
* Control Plane we still hold a root for, folded **oldest epoch first** so each epoch is
|
||||||
@@ -472,7 +515,7 @@ class ConcordCommunitySession(
|
|||||||
var floors = emptyMap<String, EntityFloor>()
|
var floors = emptyMap<String, EntityFloor>()
|
||||||
for ((address, keyAtEpoch) in historicalControlKeys.entries.sortedBy { it.value.second }) {
|
for ((address, keyAtEpoch) in historicalControlKeys.entries.sortedBy { it.value.second }) {
|
||||||
val wraps = historicalControlWraps[address]?.values?.toList() ?: continue
|
val wraps = historicalControlWraps[address]?.values?.toList() ?: continue
|
||||||
val editions = ConcordActions.controlEditions(wraps, keyAtEpoch.first)
|
val editions = editionsLocked(wraps, keyAtEpoch.first)
|
||||||
if (editions.isEmpty()) continue
|
if (editions.isEmpty()) continue
|
||||||
floors = ConcordCommunityState.authorizedHeads(editions, entry.owner, floors)
|
floors = ConcordCommunityState.authorizedHeads(editions, entry.owner, floors)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -150,6 +150,9 @@ class ConcordSessionManager(
|
|||||||
seenOnRelays: Set<NormalizedRelayUrl> = emptySet(),
|
seenOnRelays: Set<NormalizedRelayUrl> = emptySet(),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val outcome = registry.ingest(wrap, seenOnRelays)
|
val outcome = registry.ingest(wrap, seenOnRelays)
|
||||||
|
// Only the planes that change structure *without* republishing `state`. A control-plane
|
||||||
|
// fold returns STRUCTURAL_FOLD and is bumped by the per-session state watcher instead, which
|
||||||
|
// (since the folded state compares by value) fires only when the fold genuinely changed.
|
||||||
if (outcome == ConcordIngestOutcome.STRUCTURAL) bumpRevision()
|
if (outcome == ConcordIngestOutcome.STRUCTURAL) bumpRevision()
|
||||||
return outcome.claimed
|
return outcome.claimed
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -161,7 +161,14 @@ class FeedContentState(
|
|||||||
if (noteEvent != null) {
|
if (noteEvent != null) {
|
||||||
!cacheProvider.hasBeenDeleted(noteEvent)
|
!cacheProvider.hasBeenDeleted(noteEvent)
|
||||||
} else {
|
} else {
|
||||||
false
|
// An event-less row is a placeholder the filter synthesized for a room
|
||||||
|
// that has no message yet — a just-joined Concord channel, NIP-29 group,
|
||||||
|
// Marmot group or geohash cell. It carries no event, so it cannot have
|
||||||
|
// been deleted, and dropping it here deleted every such row from the
|
||||||
|
// Messages list the moment ANY kind-5 landed in an unrelated batch. The
|
||||||
|
// row then stayed gone until the next full rebuild, which is why a quiet
|
||||||
|
// community looked like it had never loaded at all.
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@ class ConcordCommunitySessionTest {
|
|||||||
|
|
||||||
// Feed the genesis control wraps → state folds, channels + membership resolve. A fold is
|
// 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.
|
// STRUCTURAL (it moves the subscription set), so it's allowed to bump the revision.
|
||||||
community.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL, session.ingest(it)) }
|
community.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL_FOLD, session.ingest(it)) }
|
||||||
val state = session.state.value
|
val state = session.state.value
|
||||||
assertEquals("Nostrichs", state?.metadata?.name)
|
assertEquals("Nostrichs", state?.metadata?.name)
|
||||||
assertTrue(state!!.channels.containsKey(community.generalChannelIdHex))
|
assertTrue(state!!.channels.containsKey(community.generalChannelIdHex))
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ class ConcordSessionRegistryTest {
|
|||||||
assertTrue(registry.subscribeAddresses().contains(beta.controlPlane.publicKeyHex))
|
assertTrue(registry.subscribeAddresses().contains(beta.controlPlane.publicKeyHex))
|
||||||
|
|
||||||
// A genesis control wrap routes to Alpha's session and folds it (STRUCTURAL).
|
// A genesis control wrap routes to Alpha's session and folds it (STRUCTURAL).
|
||||||
alpha.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL, registry.ingest(it)) }
|
alpha.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL_FOLD, registry.ingest(it)) }
|
||||||
val alphaState = registry.sessionFor(alpha.communityIdHex)!!.state.value
|
val alphaState = registry.sessionFor(alpha.communityIdHex)!!.state.value
|
||||||
assertEquals("Alpha", alphaState?.metadata?.name)
|
assertEquals("Alpha", alphaState?.metadata?.name)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.concord.cord04Roles.RoleEntity
|
|||||||
import com.vitorpamplona.quartz.concord.cord04Roles.asFloor
|
import com.vitorpamplona.quartz.concord.cord04Roles.asFloor
|
||||||
|
|
||||||
/** A channel id paired with its current folded definition. */
|
/** A channel id paired with its current folded definition. */
|
||||||
class ConcordChannel(
|
data class ConcordChannel(
|
||||||
val channelIdHex: String,
|
val channelIdHex: String,
|
||||||
val definition: ChannelEntity,
|
val definition: ChannelEntity,
|
||||||
)
|
)
|
||||||
@@ -49,7 +49,7 @@ class ConcordChannel(
|
|||||||
* "Every member keeps the entire Control Plane in sync — it is small and must
|
* "Every member keeps the entire Control Plane in sync — it is small and must
|
||||||
* stay complete." Recompute this whenever the known editions change.
|
* stay complete." Recompute this whenever the known editions change.
|
||||||
*/
|
*/
|
||||||
class ConcordCommunityState(
|
data class ConcordCommunityState(
|
||||||
val ownerPubKey: String,
|
val ownerPubKey: String,
|
||||||
val metadata: MetadataEntity?,
|
val metadata: MetadataEntity?,
|
||||||
val channels: Map<String, ConcordChannel>,
|
val channels: Map<String, ConcordChannel>,
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
|||||||
* assigned Role and holds [ConcordPermissions.MANAGE_ROLES]. Cycles that never
|
* assigned Role and holds [ConcordPermissions.MANAGE_ROLES]. Cycles that never
|
||||||
* touch the owner can never bootstrap themselves.
|
* touch the owner can never bootstrap themselves.
|
||||||
*/
|
*/
|
||||||
class AuthorityResolver private constructor(
|
data class AuthorityResolver private constructor(
|
||||||
private val ownerLower: String,
|
private val ownerLower: String,
|
||||||
private val roles: Map<String, RoleEntity>,
|
private val roles: Map<String, RoleEntity>,
|
||||||
private val memberRoles: Map<String, Set<String>>,
|
private val memberRoles: Map<String, Set<String>>,
|
||||||
|
|||||||
+5
-5
@@ -64,7 +64,7 @@ object ConcordJson {
|
|||||||
* drops the role, and with it every authority (grant) that depends on it.
|
* drops the role, and with it every authority (grant) that depends on it.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class RoleScope(
|
data class RoleScope(
|
||||||
val kind: String = "server",
|
val kind: String = "server",
|
||||||
@SerialName("channel_id") val channelId: String? = null,
|
@SerialName("channel_id") val channelId: String? = null,
|
||||||
)
|
)
|
||||||
@@ -75,7 +75,7 @@ class RoleScope(
|
|||||||
* ranks higher; no role may claim position 0 (reserved for the owner).
|
* ranks higher; no role may claim position 0 (reserved for the owner).
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class RoleEntity(
|
data class RoleEntity(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val position: Long = 0,
|
val position: Long = 0,
|
||||||
/** u64 permission bitfield as a decimal string. */
|
/** u64 permission bitfield as a decimal string. */
|
||||||
@@ -94,7 +94,7 @@ class RoleEntity(
|
|||||||
* terminates at the owner (see [AuthorityResolver]).
|
* terminates at the owner (see [AuthorityResolver]).
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class GrantEntity(
|
data class GrantEntity(
|
||||||
val member: String = "",
|
val member: String = "",
|
||||||
@SerialName("role_ids") val roleIds: List<String> = emptyList(),
|
@SerialName("role_ids") val roleIds: List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
@@ -105,7 +105,7 @@ class GrantEntity(
|
|||||||
* A [deleted] channel is terminal — its id is never reused.
|
* A [deleted] channel is terminal — its id is never reused.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class ChannelEntity(
|
data class ChannelEntity(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val private: Boolean = false,
|
val private: Boolean = false,
|
||||||
val voice: Boolean = false,
|
val voice: Boolean = false,
|
||||||
@@ -122,7 +122,7 @@ class ChannelEntity(
|
|||||||
* community name too.
|
* community name too.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class MetadataEntity(
|
data class MetadataEntity(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val icon: ImagePointer? = null,
|
val icon: ImagePointer? = null,
|
||||||
val banner: ImagePointer? = null,
|
val banner: ImagePointer? = null,
|
||||||
|
|||||||
Reference in New Issue
Block a user