lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 3147dbe18ae29b4a7513d57dff3c5ef2f9b2f18a
parent 9cb20b77ddcba28bb166836e2aed621bfe4cd717
Author: triesap <tyson@radroots.org>
Date:   Tue,  4 Aug 2026 00:07:17 +0000

profile: make overlapping refreshes monotonic

- admit completions only for the current bound session
- persist every correlated candidate through cache ordering
- render the deterministic cache winner after each completion
- cover newer-first and older-last completion ordering

Diffstat:
Mcrates/studio_application/src/profile_refresh.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcrates/studio_storage/src/runtime_actor.rs | 1-
2 files changed, 92 insertions(+), 14 deletions(-)

diff --git a/crates/studio_application/src/profile_refresh.rs b/crates/studio_application/src/profile_refresh.rs @@ -118,12 +118,16 @@ impl AppCore { profiles: &(impl ProfileRepository + ?Sized), clock: &(impl Clock + ?Sized), ) -> Result<AppSnapshot, SafeError> { - if !is_current_active(self, plan.public_key()) - || self.snapshot().revision() != plan.expected_revision() - { + if !is_current_active(self, plan.public_key()) { return Ok(self.snapshot()); } + let current_active = self + .snapshot() + .active_account() + .cloned() + .ok_or_else(invalid_profile_completion)?; + match result { Ok(Some(candidate)) => { let cached = CachedProfile::new( @@ -132,13 +136,17 @@ impl AppCore { ProfileRefreshStatus::Success, ); profiles.save_profile(&cached)?; + let winning_profile = profiles.load_profile(plan.public_key())?.map_or_else( + || candidate.metadata().clone(), + |profile| profile.candidate().metadata().clone(), + ); self.apply_transition(StateTransition::UpdateActiveAccount { expected: plan.public_key(), active_account: Box::new(ActiveAccountSnapshot::new( - plan.active_account().account().clone(), + current_active.account().clone(), RelayConnectionState::Connected, ProfileLoadState::Fresh, - Some(candidate.metadata().clone()), + Some(winning_profile), )), problem: None, }) @@ -146,14 +154,14 @@ impl AppCore { Ok(None) => self.apply_transition(StateTransition::UpdateActiveAccount { expected: plan.public_key(), active_account: Box::new(ActiveAccountSnapshot::new( - plan.active_account().account().clone(), + current_active.account().clone(), RelayConnectionState::Connected, - if plan.active_account().profile().is_some() { + if current_active.profile().is_some() { ProfileLoadState::Cached } else { ProfileLoadState::Empty }, - plan.active_account().profile().cloned(), + current_active.profile().cloned(), )), problem: None, }), @@ -163,10 +171,10 @@ impl AppCore { self.apply_transition(StateTransition::UpdateActiveAccount { expected: plan.public_key(), active_account: Box::new(ActiveAccountSnapshot::new( - plan.active_account().account().clone(), + current_active.account().clone(), RelayConnectionState::Degraded, ProfileLoadState::Error(error), - plan.active_account().profile().cloned(), + current_active.profile().cloned(), )), problem: Some(error), }) @@ -175,6 +183,13 @@ impl AppCore { } } +const fn invalid_profile_completion() -> SafeError { + SafeError::new( + SafeErrorCode::InvalidApplicationState, + radroots_studio_domain::SafeMessage::new("The active profile refresh is no longer valid."), + ) +} + fn is_current_active(core: &AppCore, public_key: PublicKey) -> bool { core.snapshot() .active_account() @@ -196,11 +211,11 @@ mod tests { use radroots_studio_domain::{ EventId, Kind0ProfileCandidate, ProfileMetadata, PublicKey, RelayUrl, SafeError, - SafeErrorCode, SafeMessage, SecretKeyInput, UnixTimestamp, + SafeErrorCode, SafeMessage, SecretKeyInput, UnixTimestamp, select_latest_kind0, }; use crate::{ - AppCore, BoxFuture, CachedProfile, Clock, InMemoryAccountRepository, + ActiveAccountSnapshot, AppCore, BoxFuture, CachedProfile, Clock, InMemoryAccountRepository, InMemoryOperationJournal, InMemorySecretStore, NostrClient, ProfileLoadState, ProfileRefreshStatus, ProfileRepository, RelayConfiguration, RelayConnectionState, }; @@ -213,7 +228,23 @@ mod tests { Ok(self.0.lock().expect("profiles").clone()) } fn save_profile(&self, profile: &CachedProfile) -> Result<(), SafeError> { - *self.0.lock().expect("profiles") = Some(profile.clone()); + let mut cached = self.0.lock().expect("profiles"); + let selected = cached.as_ref().map_or_else( + || profile.clone(), + |current| { + let winner = select_latest_kind0([ + current.candidate().clone(), + profile.candidate().clone(), + ]) + .expect("two candidates"); + if &winner == current.candidate() { + current.clone() + } else { + profile.clone() + } + }, + ); + *cached = Some(selected); Ok(()) } fn record_refresh_status( @@ -477,4 +508,52 @@ mod tests { .expect("signed-out no-op"); assert_eq!(no_op, signed_out); } + + #[test] + fn overlapping_refreshes_keep_the_newest_event_regardless_of_completion_order() { + let profiles = MemoryProfiles::default(); + let (core, public_key) = active_core(&profiles, Some("Cached")); + let first = core + .begin_profile_refresh() + .expect("first") + .expect("active"); + let second = core + .begin_profile_refresh() + .expect("second") + .expect("active"); + + core.complete_profile_refresh( + &second, + Ok(Some(profile(public_key, "Newest", 30))), + &profiles, + &FixedClock, + ) + .expect("newest completes first"); + let final_snapshot = core + .complete_profile_refresh( + &first, + Ok(Some(profile(public_key, "Older", 20))), + &profiles, + &FixedClock, + ) + .expect("older completes last"); + + assert_eq!( + final_snapshot + .active_account() + .and_then(ActiveAccountSnapshot::profile) + .and_then(ProfileMetadata::name), + Some("Newest") + ); + assert_eq!( + profiles + .load_profile(public_key) + .expect("cache") + .expect("profile") + .candidate() + .metadata() + .name(), + Some("Newest") + ); + } } diff --git a/crates/studio_storage/src/runtime_actor.rs b/crates/studio_storage/src/runtime_actor.rs @@ -945,7 +945,6 @@ impl RuntimeActor { .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); let correlated = task.correlation.session_generation() == self.session_generation - && task.correlation.expected_revision() == current.revision() && foreground.is_some_and(|binding| { binding.generation() == task.correlation.session_generation() && binding.identity().public_key() == task.correlation.account()