This commit is contained in:
2026-09-18 20:00:12 +07:00
parent 70140b2454
commit f9e538257a
5 changed files with 352 additions and 518 deletions
+11 -111
View File
@@ -1,5 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use anyhow::Result;
use concord::cord02::ControlFold;
@@ -7,11 +6,10 @@ use concord::store::{ChannelKeyRef, CommunityState};
use concord::{ChannelId, CommunityId, Epoch};
use gpui::{AppContext, Context, EventEmitter, Task};
use nostr_sdk::prelude::*;
use state::NostrRegistry;
use crate::sync::{self, Snapshot};
/// Everything that decides which planes a community is subscribed to. The
/// registry re-subscribes only when this changes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubscriptionKey {
control_pks: BTreeMap<u64, PublicKey>,
@@ -31,6 +29,10 @@ impl SubscriptionKey {
relays: state.relays.clone(),
}
}
pub(crate) fn relays(&self) -> &[RelayUrl] {
&self.relays
}
}
#[derive(Debug, Clone)]
@@ -43,7 +45,6 @@ pub struct Community {
state: CommunityState,
control: ControlFold,
members: BTreeSet<PublicKey>,
database: Arc<dyn NostrDatabase>,
dirty: bool,
refresh_task: Option<Task<Result<()>>>,
}
@@ -51,12 +52,11 @@ pub struct Community {
impl EventEmitter<CommunityEvent> for Community {}
impl Community {
pub fn new(state: CommunityState, database: Arc<dyn NostrDatabase>) -> Self {
pub fn new(state: CommunityState) -> Self {
Self {
state,
control: ControlFold::default(),
members: BTreeSet::new(),
database,
dirty: false,
refresh_task: None,
}
@@ -86,18 +86,18 @@ impl Community {
SubscriptionKey::of(&self.state)
}
/// Rebuilds the community from the wraps in the local database. A burst of
/// signals produces at most two folds: one running, one owed.
/// Rebuilds the community from the wraps in the local database.
pub fn refresh(&mut self, cx: &mut Context<Self>) {
if self.refresh_task.is_some() {
self.dirty = true;
return;
}
let database = self.database.clone();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let state = self.state.clone();
let folded =
cx.background_spawn(async move { sync::fold(database.as_ref(), &state).await });
let folded = cx.background_spawn(async move { sync::fold(&client, &state).await });
self.refresh_task = Some(cx.spawn(async move |this, cx| {
let result = folded.await;
@@ -127,103 +127,3 @@ impl Community {
}
}
}
#[cfg(test)]
mod tests {
use concord::cord02::GENERAL_CHANNEL;
use concord::cord02::guestbook::{build_join, build_leave, seal_rumor};
use concord::derive::guestbook_group_key;
use concord::store::{load_state, save_state};
use nostr_memory::MemoryDatabase;
use super::*;
use crate::sync::fixtures::{AT_MS, community};
#[test]
fn genesis_folds_into_metadata_channels_and_the_owner() {
smol::block_on(async {
let owner = Keys::generate();
let (genesis, state) = community(&owner);
let database = MemoryDatabase::unbounded();
for wrap in &genesis.wraps {
database.save_event(wrap).await.expect("saves wrap");
}
save_state(&database, &state).await.expect("saves state");
let snapshot = sync::fold(&database, &state)
.await
.expect("folds")
.expect("genesis is a control edition");
let metadata = snapshot.control.community.as_ref().expect("metadata");
assert_eq!(metadata.name, "Room");
let channel = snapshot
.control
.channels
.get(&genesis.channel_id)
.expect("general channel");
assert_eq!(channel.name, GENERAL_CHANNEL);
assert!(!channel.private);
assert_eq!(snapshot.members, BTreeSet::from([owner.public_key()]));
let persisted = load_state(&database, &state.id)
.await
.expect("loads")
.expect("persisted");
assert_eq!(persisted.id, state.id);
});
}
#[test]
fn a_join_adds_a_member_and_a_later_leave_removes_them() {
smol::block_on(async {
let owner = Keys::generate();
let member = Keys::generate();
let (genesis, state) = community(&owner);
let database = MemoryDatabase::unbounded();
for wrap in &genesis.wraps {
database.save_event(wrap).await.expect("saves wrap");
}
save_state(&database, &state).await.expect("saves state");
let guestbook = guestbook_group_key(&state.community_root, &state.id, state.root_epoch)
.expect("guestbook key");
let join = seal_rumor(
&build_join(member.public_key(), None, AT_MS + 1_000),
&guestbook,
&member,
)
.expect("seals join")
.0;
database.save_event(&join).await.expect("saves join");
let joined = sync::fold(&database, &state)
.await
.expect("folds")
.expect("control is still held");
assert!(joined.members.contains(&member.public_key()));
assert_eq!(joined.members.len(), 2);
let leave = seal_rumor(
&build_leave(member.public_key(), AT_MS + 2_000),
&guestbook,
&member,
)
.expect("seals leave")
.0;
database.save_event(&leave).await.expect("saves leave");
let left = sync::fold(&database, &joined.state)
.await
.expect("folds")
.expect("control is still held");
assert!(!left.members.contains(&member.public_key()));
assert_eq!(left.members, BTreeSet::from([owner.public_key()]));
});
}
}