lib

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

commit e661ffc5cefcb00ff0d4dbe5c53d7adf7b97aeb4
parent 839129e2387492fd1bba94ad1be66bd1c3a87745
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 23:39:39 +0000

ffi: remove superseded v1 command surface

- remove unversioned open generate import observe and shutdown APIs
- migrate Kotlin to compatibility-gated v2 bindings
- adopt native staged recovery and ordered change consumers
- guard generated contracts against legacy API reintroduction

Diffstat:
Mcrates/studio_ffi/src/commands.rs | 72++++++++++++++++--------------------------------------------------------
Mcrates/studio_ffi/src/lib.rs | 5++---
Mcrates/studio_ffi/src/observer.rs | 101++++++++++++++++++++-----------------------------------------------------------
3 files changed, 43 insertions(+), 135 deletions(-)

diff --git a/crates/studio_ffi/src/commands.rs b/crates/studio_ffi/src/commands.rs @@ -122,13 +122,6 @@ impl StudioError { } } -#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] -pub struct GeneratedAccountDto { - pub account: AccountDto, - pub snapshot: AppSnapshotDto, - pub nsec: String, -} - #[derive(uniffi::Object)] pub struct GeneratedRecoveryRequest { handle: GeneratedKeyRecoveryHandle, @@ -201,19 +194,6 @@ impl StudioAppCore { Self::open_path_compatible(&path, &expectation, development_mode) } - /// Opens the canonical application database and runtime services. - /// - /// # Errors - /// - /// Returns a safe configuration or storage error. - #[uniffi::constructor] - pub fn open(development_mode: bool) -> Result<Arc<Self>, StudioError> { - let path = canonical_database_path()?; - std::fs::create_dir_all(path.parent().ok_or_else(path_unavailable)?) - .map_err(|_| path_unavailable())?; - Self::open_path(&path, development_mode) - } - /// Restores durable public application state. /// /// # Errors @@ -233,24 +213,6 @@ impl StudioAppCore { (&self.inner.actor.snapshot()).into() } - /// Generates and stores one local account with a one-time backup receipt. - /// - /// # Errors - /// - /// Returns a safe keyring, storage, or account error. - pub async fn generate_account(&self) -> Result<GeneratedAccountDto, StudioError> { - self.inner - .actor - .generate_account() - .await - .map(|receipt| GeneratedAccountDto { - account: receipt.account().into(), - snapshot: (&self.inner.actor.snapshot()).into(), - nsec: receipt.generated_nsec().with_exposed_secret(str::to_owned), - }) - .map_err(StudioError::from) - } - /// Begins the exclusive generated-account recovery flow without persistence. /// /// # Errors @@ -311,24 +273,6 @@ impl StudioAppCore { .map_err(StudioError::from) } - /// Imports one nsec or canonical secret-key hex value. - /// - /// # Errors - /// - /// Returns a safe validation, keyring, storage, or account error. - pub async fn import_secret_key( - &self, - secret_key: Vec<u8>, - ) -> Result<AppSnapshotDto, StudioError> { - let input = SecretKeyInput::parse_bytes(secret_key).map_err(StudioError::from)?; - self.inner - .actor - .import_secret_key(input) - .await - .map(|_| (&self.inner.actor.snapshot()).into()) - .map_err(StudioError::from) - } - /// Imports or repairs an account using a caller-owned idempotency key. /// /// # Errors @@ -786,4 +730,20 @@ mod tests { Some("canonical-lowercase-public-key-hex") ); } + + #[test] + fn superseded_v1_ffi_commands_are_absent() { + let commands = include_str!("commands.rs"); + let observer = include_str!("observer.rs"); + for forbidden in [ + format!("pub async fn {}_account(", "generate"), + format!("pub async fn {}_secret_key(", "import"), + format!("pub fn {}(development_mode", "open"), + format!("pub async fn {}(", "subscribe"), + format!("pub fn {}(&self)", "shutdown"), + ] { + assert!(!commands.contains(&forbidden)); + assert!(!observer.contains(&forbidden)); + } + } } diff --git a/crates/studio_ffi/src/lib.rs b/crates/studio_ffi/src/lib.rs @@ -5,8 +5,8 @@ mod dto; mod observer; pub use commands::{ - AccountCommandReceiptDto, GeneratedAccountDto, GeneratedRecoveryRequest, RemovalRequest, - RequestContextDto, StudioAppCore, StudioError, + AccountCommandReceiptDto, GeneratedRecoveryRequest, RemovalRequest, RequestContextDto, + StudioAppCore, StudioError, }; pub use dto::{ AccountDto, ActiveAccountDto, AppLifecycleDto, AppSnapshotDto, KeyAvailabilityDto, ProfileDto, @@ -15,7 +15,6 @@ pub use dto::{ }; pub use observer::{ ObserverSubscription, ShutdownReceiptDto, SnapshotChangeDto, StudioChangeObserver, - StudioObserver, }; uniffi::setup_scaffolding!(); diff --git a/crates/studio_ffi/src/observer.rs b/crates/studio_ffi/src/observer.rs @@ -12,11 +12,6 @@ const OBSERVER_CHANGE_CAPACITY: NonZeroUsize = match NonZeroUsize::new(64) { None => unreachable!(), }; -#[uniffi::export(callback_interface)] -pub trait StudioObserver: Send + Sync { - fn on_snapshot_changed(&self, snapshot: AppSnapshotDto); -} - #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] pub struct SnapshotChangeDto { pub snapshot: AppSnapshotDto, @@ -115,62 +110,6 @@ impl StudioAppCore { })) } - /// Subscribes to revisioned snapshots and immediately delivers the current value. - /// - /// # Errors - /// - /// Returns a safe observer or lifecycle error. - pub async fn subscribe( - &self, - observer: Box<dyn StudioObserver>, - ) -> Result<Arc<ObserverSubscription>, StudioError> { - if self.inner.closed.load(Ordering::Acquire) { - return Err(closed_error()); - } - let mut subscription = self - .inner - .actor - .subscribe_changes(OBSERVER_CHANGE_CAPACITY) - .await - .map_err(StudioError::from)?; - let id = subscription.id(); - let observer: Arc<dyn StudioObserver> = Arc::from(observer); - let task = crate::commands::runtime().spawn(async move { - while let Some(change) = subscription.receive().await { - observer.on_snapshot_changed(change.snapshot().into()); - } - }); - self.inner - .observers - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(id, task); - Ok(Arc::new(ObserverSubscription { - core: Arc::downgrade(&self.inner), - id: Mutex::new(Some(id)), - })) - } - - pub fn shutdown(&self) { - if self.inner.closed.swap(true, Ordering::AcqRel) { - return; - } - let handles = std::mem::take( - &mut *self - .inner - .observers - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner), - ); - for (_, task) in handles { - task.abort(); - } - let actor = self.inner.actor.clone(); - crate::commands::runtime().spawn(async move { - let _ = actor.close().await; - }); - } - /// Stops observer delivery and waits for actor-owned shutdown. /// /// # Errors @@ -223,7 +162,9 @@ mod tests { use radroots_studio_storage::RuntimeActorHandle; use crate::commands::{ACTOR_MAILBOX_CAPACITY, RuntimeCore, SystemClock, runtime}; - use crate::{AppSnapshotDto, ProfileLoadStateDto, StudioAppCore, StudioObserver}; + use crate::{ + AppSnapshotDto, ProfileLoadStateDto, SnapshotChangeDto, StudioAppCore, StudioChangeObserver, + }; const SECRET_HEX: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; @@ -233,8 +174,9 @@ mod tests { core: Mutex<Option<Arc<StudioAppCore>>>, } - impl StudioObserver for RecordingObserver { - fn on_snapshot_changed(&self, snapshot: AppSnapshotDto) { + impl StudioChangeObserver for RecordingObserver { + fn on_change(&self, change: SnapshotChangeDto) { + let snapshot = change.snapshot; if let Some(core) = self.core.lock().expect("core").as_ref() { assert_eq!(core.snapshot().revision, snapshot.revision); } @@ -272,7 +214,7 @@ mod tests { let observer = Arc::new(RecordingObserver::default()); *observer.core.lock().expect("core") = Some(Arc::clone(&core)); let subscription = core - .subscribe(Box::new(ArcObserver(observer.clone()))) + .subscribe_changes_v2(Box::new(ArcObserver(observer.clone()))) .await .expect("subscribe"); @@ -294,15 +236,14 @@ mod tests { let core = core(); let observer = Arc::new(RecordingObserver::default()); let _subscription = runtime() - .block_on(core.subscribe(Box::new(ArcObserver(observer.clone())))) + .block_on(core.subscribe_changes_v2(Box::new(ArcObserver(observer.clone())))) .expect("subscribe"); - core.shutdown(); - core.shutdown(); + runtime().block_on(core.shutdown_v2()).expect("shutdown"); assert!( runtime() - .block_on(core.subscribe(Box::new(ArcObserver(observer)))) + .block_on(core.subscribe_changes_v2(Box::new(ArcObserver(observer)))) .is_err() ); assert!(core.inner.observers.lock().expect("observers").is_empty()); @@ -333,13 +274,21 @@ mod tests { let observer = Arc::new(RecordingObserver::default()); *observer.core.lock().expect("core") = Some(Arc::clone(&core)); let subscription = core - .subscribe(Box::new(ArcObserver(observer.clone()))) + .subscribe_changes_v2(Box::new(ArcObserver(observer.clone()))) .await .expect("subscribe"); let imported = core - .import_secret_key(SECRET_HEX.as_bytes().to_vec()) + .import_account_v2( + crate::RequestContextDto { + request_id: "observer-import".to_owned(), + expected_revision: core.snapshot().revision, + deadline_millis: 5_000, + }, + SECRET_HEX.as_bytes().to_vec(), + ) .await - .expect("import"); + .expect("import") + .snapshot; let public_key = imported.selected_public_key_hex.expect("selection"); core.activate_account(public_key).await.expect("activate"); core.refresh_active_profile().await.expect("refresh"); @@ -361,16 +310,16 @@ mod tests { core.sign_out().await.expect("sign out"); assert_eq!(observer.snapshots.lock().expect("snapshots").len(), count); - core.shutdown(); + core.shutdown_v2().await.expect("shutdown"); publisher.shutdown().await; local_relay.shutdown(); } struct ArcObserver(Arc<RecordingObserver>); - impl StudioObserver for ArcObserver { - fn on_snapshot_changed(&self, snapshot: AppSnapshotDto) { - self.0.on_snapshot_changed(snapshot); + impl StudioChangeObserver for ArcObserver { + fn on_change(&self, change: SnapshotChangeDto) { + self.0.on_change(change); } }