lib

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

commit 024b3c8d4c738b3f386d5c4ecba78b202afe7614
parent e661ffc5cefcb00ff0d4dbe5c53d7adf7b97aeb4
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 23:54:30 +0000

lifecycle: project exhaustive native routes

- expose actor lifecycle phases through every snapshot DTO
- map opening migration recovery degradation and terminal routes
- gate account controls outside admitted lifecycle states
- cover every non-ready route in Compose UI tests

Diffstat:
Mcrates/studio_ffi/src/commands.rs | 31++++++++++++++++++++++---------
Mcrates/studio_ffi/src/dto.rs | 44+++++++++++++++++++++++++++++++++++++++++---
Mcrates/studio_ffi/src/observer.rs | 6+++++-
3 files changed, 68 insertions(+), 13 deletions(-)

diff --git a/crates/studio_ffi/src/commands.rs b/crates/studio_ffi/src/commands.rs @@ -172,6 +172,19 @@ pub(crate) struct RuntimeCore { pub(crate) closed: AtomicBool, } +impl RuntimeCore { + pub(crate) fn snapshot_dto(&self) -> AppSnapshotDto { + AppSnapshotDto::from_runtime(&self.actor.snapshot(), self.actor.lifecycle()) + } + + pub(crate) fn dto_for( + &self, + snapshot: &radroots_studio_application::AppSnapshot, + ) -> AppSnapshotDto { + AppSnapshotDto::from_runtime(snapshot, self.actor.lifecycle()) + } +} + #[derive(uniffi::Object)] pub struct StudioAppCore { pub(crate) inner: Arc<RuntimeCore>, @@ -204,13 +217,13 @@ impl StudioAppCore { .actor .bootstrap() .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } #[must_use] pub fn snapshot(&self) -> AppSnapshotDto { - (&self.inner.actor.snapshot()).into() + self.inner.snapshot_dto() } /// Begins the exclusive generated-account recovery flow without persistence. @@ -250,7 +263,7 @@ impl StudioAppCore { .actor .acknowledge_generated_key_stage(request.handle.id()) .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } @@ -300,7 +313,7 @@ impl StudioAppCore { ) .await .map(|_| { - let snapshot = AppSnapshotDto::from(&self.inner.actor.snapshot()); + let snapshot = self.inner.snapshot_dto(); AccountCommandReceiptDto { request_id: context.request_id.clone(), committed_revision: snapshot.revision, @@ -324,7 +337,7 @@ impl StudioAppCore { .actor .select_account(public_key) .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } @@ -342,7 +355,7 @@ impl StudioAppCore { .actor .activate_account(public_key) .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } @@ -356,7 +369,7 @@ impl StudioAppCore { .actor .sign_out() .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } @@ -370,7 +383,7 @@ impl StudioAppCore { .actor .refresh_active_profile() .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } @@ -416,7 +429,7 @@ impl StudioAppCore { .actor .confirm_account_removal(token) .await - .map(|snapshot| (&snapshot).into()) + .map(|snapshot| self.inner.dto_for(&snapshot)) .map_err(StudioError::from) } } diff --git a/crates/studio_ffi/src/dto.rs b/crates/studio_ffi/src/dto.rs @@ -1,6 +1,6 @@ use radroots_studio_application::{ ActiveAccountSnapshot, AppLifecycle, AppSnapshot, ProfileLoadState, RelayConnectionState, - SessionState, + RuntimeLifecycle, SessionState, }; use radroots_studio_domain::{ AccountSummary, BindingAvailability, ProfileMetadata, SafeError, SafeErrorCode, @@ -62,8 +62,16 @@ pub struct SafeErrorDto { #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum AppLifecycleDto { - Booting, + Opening, + CompatibilityChecking, + AcquiringOwnership, + Migrating, + Recovering, Ready, + Degraded, + Blocked, + ShuttingDown, + Closed, Fatal, } @@ -155,7 +163,7 @@ pub struct AppSnapshotDto { impl From<&AppSnapshot> for AppSnapshotDto { fn from(snapshot: &AppSnapshot) -> Self { let (lifecycle, lifecycle_error) = match snapshot.lifecycle() { - AppLifecycle::Booting => (AppLifecycleDto::Booting, None), + AppLifecycle::Booting => (AppLifecycleDto::Opening, None), AppLifecycle::Ready => (AppLifecycleDto::Ready, None), AppLifecycle::Fatal(error) => (AppLifecycleDto::Fatal, Some(error.into())), }; @@ -191,6 +199,36 @@ impl From<&AppSnapshot> for AppSnapshotDto { } } +impl AppSnapshotDto { + pub(crate) fn from_runtime(snapshot: &AppSnapshot, runtime: RuntimeLifecycle) -> Self { + let mut dto = Self::from(snapshot); + let (lifecycle, problem) = match runtime { + RuntimeLifecycle::Opening => (AppLifecycleDto::Opening, None), + RuntimeLifecycle::CompatibilityChecking => { + (AppLifecycleDto::CompatibilityChecking, None) + } + RuntimeLifecycle::AcquiringOwnership => (AppLifecycleDto::AcquiringOwnership, None), + RuntimeLifecycle::Migrating => (AppLifecycleDto::Migrating, None), + RuntimeLifecycle::Recovering => (AppLifecycleDto::Recovering, None), + RuntimeLifecycle::Ready => (AppLifecycleDto::Ready, None), + RuntimeLifecycle::Degraded(error) => { + (AppLifecycleDto::Degraded, Some(SafeErrorDto::from(error))) + } + RuntimeLifecycle::Blocked(error) => { + (AppLifecycleDto::Blocked, Some(SafeErrorDto::from(error))) + } + RuntimeLifecycle::ShuttingDown => (AppLifecycleDto::ShuttingDown, None), + RuntimeLifecycle::Closed => (AppLifecycleDto::Closed, None), + RuntimeLifecycle::Fatal(error) => { + (AppLifecycleDto::Fatal, Some(SafeErrorDto::from(error))) + } + }; + dto.lifecycle = lifecycle; + dto.lifecycle_error = problem; + dto + } +} + impl From<&AccountSummary> for AccountDto { fn from(account: &AccountSummary) -> Self { Self { diff --git a/crates/studio_ffi/src/observer.rs b/crates/studio_ffi/src/observer.rs @@ -89,10 +89,14 @@ impl StudioAppCore { .map_err(StudioError::from)?; let id = subscription.id(); let observer: Arc<dyn StudioChangeObserver> = Arc::from(observer); + let runtime_core = Arc::clone(&self.inner); let task = crate::commands::runtime().spawn(async move { while let Some(change) = subscription.receive().await { observer.on_change(SnapshotChangeDto { - snapshot: change.snapshot().into(), + snapshot: AppSnapshotDto::from_runtime( + change.snapshot(), + runtime_core.actor.lifecycle(), + ), previous_revision: change .previous_revision() .map(radroots_studio_application::SnapshotRevision::value),