clean up
This commit is contained in:
@@ -227,105 +227,3 @@ fn state_document_of(event: &Event) -> Option<CommunityId> {
|
|||||||
let hex = identifier.strip_prefix(STATE_PREFIX)?;
|
let hex = identifier.strip_prefix(STATE_PREFIX)?;
|
||||||
hex.parse().ok()
|
hex.parse().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use concord::Epoch;
|
|
||||||
use nostr_memory::MemoryDatabase;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn client() -> Client {
|
|
||||||
ClientBuilder::default()
|
|
||||||
.database(MemoryDatabase::unbounded())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_states_reads_one_document_per_community_and_ignores_other_documents() {
|
|
||||||
smol::block_on(async {
|
|
||||||
let client = client();
|
|
||||||
|
|
||||||
let state = CommunityState {
|
|
||||||
id: CommunityId::from_bytes([0x42; 32]),
|
|
||||||
name: Some("Anime and Manga".to_owned()),
|
|
||||||
owner: Keys::generate().public_key(),
|
|
||||||
owner_salt: [0x01; 32],
|
|
||||||
community_root: [0x02; 32],
|
|
||||||
root_epoch: Epoch(0),
|
|
||||||
control_root: None,
|
|
||||||
control_pks: BTreeMap::new(),
|
|
||||||
channels: Vec::new(),
|
|
||||||
relays: Vec::new(),
|
|
||||||
heads: Vec::new(),
|
|
||||||
banned: BTreeSet::new(),
|
|
||||||
cursors: BTreeMap::new(),
|
|
||||||
held_roots: Vec::new(),
|
|
||||||
channel_cuts: BTreeMap::new(),
|
|
||||||
refounders: BTreeSet::new(),
|
|
||||||
removed_at: None,
|
|
||||||
stranded: false,
|
|
||||||
dissolved: false,
|
|
||||||
added_at_ms: 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
save_state(&client, &state).await.expect("saves");
|
|
||||||
|
|
||||||
// A cached rumor is also an application-specific document, but not a
|
|
||||||
// state document, so the prefix keeps it out of the state scan.
|
|
||||||
let other = EventBuilder::new(Kind::ApplicationSpecificData, "{}")
|
|
||||||
.tags([Tag::identifier("deadbeef")])
|
|
||||||
.finalize(&*LOCAL_KEYS)
|
|
||||||
.expect("builds");
|
|
||||||
client.database().save_event(&other).await.expect("saves");
|
|
||||||
|
|
||||||
let loaded = load_states(&client).await.expect("loads");
|
|
||||||
|
|
||||||
assert_eq!(loaded, vec![state]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn caching_the_same_rumor_twice_leaves_one_row() {
|
|
||||||
smol::block_on(async {
|
|
||||||
let client = client();
|
|
||||||
let channel = ChannelId::from_bytes([0x9c; 32]);
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let group = concord::derive::channel_group_key(&[0x07; 32], &channel, Epoch(0))
|
|
||||||
.expect("a group key");
|
|
||||||
|
|
||||||
let rumor = concord::cord03::build_message(
|
|
||||||
keys.public_key(),
|
|
||||||
&channel,
|
|
||||||
Epoch(0),
|
|
||||||
"twice",
|
|
||||||
None,
|
|
||||||
1_700_000_000_000,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let (wrap, _) = concord::cord03::seal_rumor(&rumor, &group, &keys, false)
|
|
||||||
.await
|
|
||||||
.expect("seals");
|
|
||||||
let (opened, _) =
|
|
||||||
concord::cord03::open(&wrap, &group, &channel, Epoch(0)).expect("opens");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
cache_rumor(&client, &channel, &opened)
|
|
||||||
.await
|
|
||||||
.expect("caches")
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
cache_rumor(&client, &channel, &opened)
|
|
||||||
.await
|
|
||||||
.expect("caches")
|
|
||||||
);
|
|
||||||
|
|
||||||
let cached = query_rumors(&client, &channel, None, 10, None)
|
|
||||||
.await
|
|
||||||
.expect("reads");
|
|
||||||
assert_eq!(cached.len(), 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use state::NostrRegistry;
|
|||||||
use crate::cache;
|
use crate::cache;
|
||||||
use crate::history::{self, PageRegistry, Window, WrapPage};
|
use crate::history::{self, PageRegistry, Window, WrapPage};
|
||||||
use crate::rekey::{self, Adoptions};
|
use crate::rekey::{self, Adoptions};
|
||||||
use crate::sync::{self, Snapshot};
|
use crate::sync::{self};
|
||||||
|
|
||||||
/// Wraps one relay returns for one page request.
|
/// Wraps one relay returns for one page request.
|
||||||
const PAGE_WRAPS: usize = 50;
|
const PAGE_WRAPS: usize = 50;
|
||||||
@@ -124,10 +124,6 @@ pub struct Community {
|
|||||||
icon_ref: Option<ImageRef>,
|
icon_ref: Option<ImageRef>,
|
||||||
banner: Option<PathBuf>,
|
banner: Option<PathBuf>,
|
||||||
banner_ref: Option<ImageRef>,
|
banner_ref: Option<ImageRef>,
|
||||||
dirty: bool,
|
|
||||||
refresh_task: Option<Task<Result<()>>>,
|
|
||||||
icon_task: Option<Task<Result<()>>>,
|
|
||||||
banner_task: Option<Task<Result<()>>>,
|
|
||||||
rounds: HashMap<ChannelId, Round>,
|
rounds: HashMap<ChannelId, Round>,
|
||||||
/// The last completed round per channel, for the panel's honest states.
|
/// The last completed round per channel, for the panel's honest states.
|
||||||
progress: HashMap<ChannelId, Progress>,
|
progress: HashMap<ChannelId, Progress>,
|
||||||
@@ -137,8 +133,6 @@ pub struct Community {
|
|||||||
last_round: HashMap<ChannelId, Instant>,
|
last_round: HashMap<ChannelId, Instant>,
|
||||||
pages: PageRegistry,
|
pages: PageRegistry,
|
||||||
rekey_task: Option<Task<Result<()>>>,
|
rekey_task: Option<Task<Result<()>>>,
|
||||||
rekey_dirty: bool,
|
|
||||||
/// Spawned folds, round bookkeeping and publishes, cancelled on drop
|
|
||||||
tasks: SmallVec<[Task<Result<()>>; 2]>,
|
tasks: SmallVec<[Task<Result<()>>; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,17 +149,12 @@ impl Community {
|
|||||||
icon_ref: None,
|
icon_ref: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
banner_ref: None,
|
banner_ref: None,
|
||||||
dirty: false,
|
|
||||||
refresh_task: None,
|
|
||||||
icon_task: None,
|
|
||||||
banner_task: None,
|
|
||||||
rounds: HashMap::new(),
|
rounds: HashMap::new(),
|
||||||
progress: HashMap::new(),
|
progress: HashMap::new(),
|
||||||
unreadable: BTreeMap::new(),
|
unreadable: BTreeMap::new(),
|
||||||
last_round: HashMap::new(),
|
last_round: HashMap::new(),
|
||||||
pages,
|
pages,
|
||||||
rekey_task: None,
|
rekey_task: None,
|
||||||
rekey_dirty: false,
|
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -620,7 +609,6 @@ impl Community {
|
|||||||
let (opened, _) = cord03::open(&wrap, &group, &channel, epoch)?;
|
let (opened, _) = cord03::open(&wrap, &group, &channel, epoch)?;
|
||||||
cache::cache_rumor(&client, &channel, &opened).await?;
|
cache::cache_rumor(&client, &channel, &opened).await?;
|
||||||
|
|
||||||
sync::connect_relays(&client, &relays).await;
|
|
||||||
sync::publish_wrap(&client, &wrap, &relays).await;
|
sync::publish_wrap(&client, &wrap, &relays).await;
|
||||||
|
|
||||||
Ok(opened.rumor_id)
|
Ok(opened.rumor_id)
|
||||||
@@ -631,16 +619,12 @@ impl Community {
|
|||||||
///
|
///
|
||||||
/// The caller re-folds afterwards; this only seeds the new planes.
|
/// The caller re-folds afterwards; this only seeds the new planes.
|
||||||
pub(crate) fn adopt(&mut self, state: CommunityState) {
|
pub(crate) fn adopt(&mut self, state: CommunityState) {
|
||||||
// A fold already in flight would write its pre-adoption state back.
|
|
||||||
self.refresh_task = None;
|
|
||||||
self.dirty = false;
|
|
||||||
self.state = state;
|
self.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adopt whatever the rekey watch has delivered, then re-page what it moved.
|
/// Adopt whatever the rekey watch has delivered, then re-page what it moved.
|
||||||
pub fn rekey(&mut self, cx: &mut Context<Self>) {
|
pub fn rekey(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.rekey_task.is_some() {
|
if self.rekey_task.is_some() {
|
||||||
self.rekey_dirty = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,6 +635,7 @@ impl Community {
|
|||||||
|
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let roles = self.control.roles.clone();
|
let roles = self.control.roles.clone();
|
||||||
|
|
||||||
@@ -746,11 +731,6 @@ impl Community {
|
|||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(error) => cx.emit(CommunityEvent::Error(error.to_string())),
|
Err(error) => cx.emit(CommunityEvent::Error(error.to_string())),
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.rekey_dirty {
|
|
||||||
self.rekey_dirty = false;
|
|
||||||
self.rekey(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fold an adoption into the held state, persist it, and re-page what moved.
|
/// Fold an adoption into the held state, persist it, and re-page what moved.
|
||||||
@@ -818,8 +798,6 @@ impl Community {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// `stepped` carries the key held before the walk plus every epoch it
|
|
||||||
// passed through, each with the cutoff its superseding rotation set.
|
|
||||||
for key in channel.stepped {
|
for key in channel.stepped {
|
||||||
if !held.priors.iter().any(|prior| prior.epoch == key.epoch) {
|
if !held.priors.iter().any(|prior| prior.epoch == key.epoch) {
|
||||||
held.priors.push(key);
|
held.priors.push(key);
|
||||||
@@ -889,67 +867,53 @@ impl Community {
|
|||||||
|
|
||||||
/// Rebuilds the community from the wraps in the local database.
|
/// Rebuilds the community from the wraps in the local database.
|
||||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.refresh_task.is_some() {
|
|
||||||
self.dirty = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let folded = cx.background_spawn(async move { sync::fold(&client, &state).await });
|
let folded = cx.background_spawn(async move { sync::fold(&client, &state).await });
|
||||||
|
|
||||||
self.refresh_task = Some(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
let result = folded.await;
|
let result = folded.await;
|
||||||
this.update(cx, |this, cx| this.apply(result, cx))?;
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(Some(snapshot)) => {
|
||||||
|
let mut state = snapshot.state;
|
||||||
|
state.cursors = std::mem::take(&mut this.state.cursors);
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
this.control = snapshot.control;
|
||||||
|
this.members = snapshot.members;
|
||||||
|
this.load_images(cx);
|
||||||
|
|
||||||
|
let reported = !snapshot.unreadable.is_empty();
|
||||||
|
this.unreadable = snapshot.unreadable;
|
||||||
|
|
||||||
|
if reported {
|
||||||
|
cx.emit(CommunityEvent::Unreadable(this.state.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit(CommunityEvent::Updated(this.state.id));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
cx.emit(CommunityEvent::Error(error.to_string()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply(&mut self, result: Result<Option<Snapshot>>, cx: &mut Context<Self>) {
|
|
||||||
self.refresh_task = None;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(Some(snapshot)) => {
|
|
||||||
let mut state = snapshot.state;
|
|
||||||
state.cursors = std::mem::take(&mut self.state.cursors);
|
|
||||||
self.state = state;
|
|
||||||
self.control = snapshot.control;
|
|
||||||
self.members = snapshot.members;
|
|
||||||
self.load_images(cx);
|
|
||||||
|
|
||||||
// A fold reads every plane, so its count is the truth for every
|
|
||||||
// channel rather than a sample of the region one round read.
|
|
||||||
// Replacing it is what lets the count fall again once a key is
|
|
||||||
// adopted; a partial round only ever raises it.
|
|
||||||
let reported = !snapshot.unreadable.is_empty();
|
|
||||||
self.unreadable = snapshot.unreadable;
|
|
||||||
|
|
||||||
if reported {
|
|
||||||
cx.emit(CommunityEvent::Unreadable(self.state.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.emit(CommunityEvent::Updated(self.state.id));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
Ok(None) => {}
|
|
||||||
Err(error) => cx.emit(CommunityEvent::Error(error.to_string())),
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.dirty {
|
|
||||||
self.dirty = false;
|
|
||||||
self.refresh(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve the folded icon and banner into local files.
|
/// Resolve the folded icon and banner into local files.
|
||||||
fn load_images(&mut self, cx: &mut Context<Self>) {
|
fn load_images(&mut self, cx: &mut Context<Self>) {
|
||||||
let (icon, banner) = match self.control.community.as_ref() {
|
let (icon, banner) = match self.control.community.as_ref() {
|
||||||
Some(metadata) => (metadata.icon.clone(), metadata.banner.clone()),
|
Some(metadata) => (metadata.icon.clone(), metadata.banner.clone()),
|
||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.load_icon(icon, cx);
|
self.load_icon(icon, cx);
|
||||||
self.load_banner(banner, cx);
|
self.load_banner(banner, cx);
|
||||||
}
|
}
|
||||||
@@ -966,7 +930,7 @@ impl Community {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.icon_task = Some(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match sync::resolve_image(&icon, cx).await {
|
match sync::resolve_image(&icon, cx).await {
|
||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
@@ -992,7 +956,7 @@ impl Community {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.banner_task = Some(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match sync::resolve_image(&banner, cx).await {
|
match sync::resolve_image(&banner, cx).await {
|
||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ mod sync;
|
|||||||
pub use community::*;
|
pub use community::*;
|
||||||
pub use sync::*;
|
pub use sync::*;
|
||||||
|
|
||||||
/// How long a burst of relay notifications is collected before it is folded.
|
|
||||||
const PUMP_WINDOW: Duration = Duration::from_millis(200);
|
const PUMP_WINDOW: Duration = Duration::from_millis(200);
|
||||||
/// How long a community's relays may deliver nothing before
|
|
||||||
/// its standing subscription is torn down and re-issued.
|
|
||||||
const LIVE_ROTATE: Duration = Duration::from_secs(90);
|
const LIVE_ROTATE: Duration = Duration::from_secs(90);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ pub struct Watch {
|
|||||||
pub epoch: Epoch,
|
pub epoch: Epoch,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The permission a rotation of `scope` is judged under, by whoever reads it and
|
|
||||||
/// by this client when it publishes one.
|
|
||||||
fn permissions(scope: RekeyScope) -> &'static [u64] {
|
fn permissions(scope: RekeyScope) -> &'static [u64] {
|
||||||
match scope {
|
match scope {
|
||||||
RekeyScope::Base => &[Permissions::BAN],
|
RekeyScope::Base => &[Permissions::BAN],
|
||||||
@@ -39,11 +37,6 @@ fn permissions(scope: RekeyScope) -> &'static [u64] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Every address a community's rotations can arrive at.
|
/// Every address a community's rotations can arrive at.
|
||||||
///
|
|
||||||
/// The base scope watches the epoch after *every* held root, not only the
|
|
||||||
/// current one: a rotation published while this client was away is read from
|
|
||||||
/// the root it stepped off, and the npub that minted an epoch is only knowable
|
|
||||||
/// from the rotation that minted it.
|
|
||||||
pub fn watches(state: &CommunityState) -> Result<Vec<Watch>> {
|
pub fn watches(state: &CommunityState) -> Result<Vec<Watch>> {
|
||||||
let mut watches = Vec::new();
|
let mut watches = Vec::new();
|
||||||
let roots = state.roots();
|
let roots = state.roots();
|
||||||
@@ -185,8 +178,8 @@ where
|
|||||||
S: AsyncNip44 + ?Sized,
|
S: AsyncNip44 + ?Sized,
|
||||||
{
|
{
|
||||||
let watches = watches(state)?;
|
let watches = watches(state)?;
|
||||||
|
|
||||||
let mut authors: BTreeSet<PublicKey> = BTreeSet::new();
|
let mut authors: BTreeSet<PublicKey> = BTreeSet::new();
|
||||||
|
|
||||||
for watch in &watches {
|
for watch in &watches {
|
||||||
authors.insert(watch.address);
|
authors.insert(watch.address);
|
||||||
}
|
}
|
||||||
@@ -666,12 +659,6 @@ where
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|rotation| rotation.continuity(held_epoch, &held_key) == Continuity::Extends)
|
.filter(|rotation| rotation.continuity(held_epoch, &held_key) == Continuity::Extends)
|
||||||
{
|
{
|
||||||
// Who minted an epoch is proven by the rotation itself, not by a blob:
|
|
||||||
// a rotation is only a candidate after continuity against a key we
|
|
||||||
// hold, so its rotator minted this epoch whether or not it addressed
|
|
||||||
// us. A member who joined on a stale bundle never held the epochs
|
|
||||||
// between, and the snapshot that seeds them is only honored on this
|
|
||||||
// npub's authority (CORD-02 §5).
|
|
||||||
if scope == RekeyScope::Base {
|
if scope == RekeyScope::Base {
|
||||||
step.refounders.insert(rotation.rotator);
|
step.refounders.insert(rotation.rotator);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,8 +214,6 @@ where
|
|||||||
|
|
||||||
/// Best-effort publication of the genesis wraps to the community's relays.
|
/// Best-effort publication of the genesis wraps to the community's relays.
|
||||||
pub(crate) async fn publish_wraps(client: &Client, wraps: &[Event], relays: &[RelayUrl]) {
|
pub(crate) async fn publish_wraps(client: &Client, wraps: &[Event], relays: &[RelayUrl]) {
|
||||||
connect_relays(client, relays).await;
|
|
||||||
|
|
||||||
for wrap in wraps {
|
for wrap in wraps {
|
||||||
publish_wrap(client, wrap, relays).await;
|
publish_wrap(client, wrap, relays).await;
|
||||||
}
|
}
|
||||||
@@ -232,6 +230,8 @@ pub(crate) async fn connect_relays(client: &Client, relays: &[RelayUrl]) {
|
|||||||
|
|
||||||
/// Best-effort publication of a single wrap to the community's relays.
|
/// Best-effort publication of a single wrap to the community's relays.
|
||||||
pub(crate) async fn publish_wrap(client: &Client, wrap: &Event, relays: &[RelayUrl]) {
|
pub(crate) async fn publish_wrap(client: &Client, wrap: &Event, relays: &[RelayUrl]) {
|
||||||
|
connect_relays(client, relays).await;
|
||||||
|
|
||||||
let sent = if relays.is_empty() {
|
let sent = if relays.is_empty() {
|
||||||
client.send_event(wrap).broadcast().await
|
client.send_event(wrap).broadcast().await
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user