app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 1268c21df613f5ecd4de828011f68b20f427a063
parent d9ed403689a8864e2b4cae7fb7b1e5a69fe99e27
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 18:14:18 +0000

core(state): add application service ports

- isolate account, profile, namespace, and app-state storage
- define async Nostr profile and deterministic clock boundaries
- reserve a send-safe secret-store application port
- prove all service contracts accept isolated test fakes

Diffstat:
Mcore/crates/application/src/lib.rs | 5+++++
Acore/crates/application/src/ports.rs | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 4++--
3 files changed, 232 insertions(+), 2 deletions(-)

diff --git a/core/crates/application/src/lib.rs b/core/crates/application/src/lib.rs @@ -1,7 +1,12 @@ #![doc = "Radroots Studio application runtime."] +pub mod ports; pub mod snapshot; +pub use ports::{ + AccountNamespaceRepository, AccountRepository, AppStateRepository, BoxFuture, Clock, + NostrClient, ProfileRepository, SecretStore, +}; pub use snapshot::{ ActiveAccountSnapshot, AppLifecycle, AppSnapshot, ProfileLoadState, RelayConfiguration, RelayConnectionState, SessionState, SnapshotRevision, diff --git a/core/crates/application/src/ports.rs b/core/crates/application/src/ports.rs @@ -0,0 +1,225 @@ +use std::future::Future; +use std::pin::Pin; + +use radroots_studio_domain::{ + AccountSummary, Kind0ProfileCandidate, ProfileMetadata, PublicKey, RelayUrl, SafeError, + UnixTimestamp, +}; + +pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>; + +pub trait AccountRepository: Send + Sync { + /// Lists saved public account records in deterministic order. + /// + /// # Errors + /// + /// Returns a safe storage error when records cannot be read. + fn list_accounts(&self) -> Result<Vec<AccountSummary>, SafeError>; + /// Finds one saved public account record. + /// + /// # Errors + /// + /// Returns a safe storage error when the lookup cannot complete. + fn find_account(&self, public_key: PublicKey) -> Result<Option<AccountSummary>, SafeError>; + /// Inserts or updates one public account record. + /// + /// # Errors + /// + /// Returns a safe storage error when the durable write fails. + fn save_account(&self, account: &AccountSummary) -> Result<(), SafeError>; + /// Removes one public account record. + /// + /// # Errors + /// + /// Returns a safe storage error when the durable delete fails. + fn remove_account(&self, public_key: PublicKey) -> Result<(), SafeError>; +} + +pub trait ProfileRepository: Send + Sync { + /// Loads cached public profile metadata. + /// + /// # Errors + /// + /// Returns a safe storage error when the cache cannot be read. + fn load_profile(&self, public_key: PublicKey) -> Result<Option<ProfileMetadata>, SafeError>; + /// Saves a verified kind-0 profile candidate. + /// + /// # Errors + /// + /// Returns a safe storage error when the cache cannot be committed. + fn save_profile(&self, candidate: &Kind0ProfileCandidate) -> Result<(), SafeError>; + /// Removes cached profile metadata for an account. + /// + /// # Errors + /// + /// Returns a safe storage error when the cache cannot be deleted. + fn remove_profile(&self, public_key: PublicKey) -> Result<(), SafeError>; +} + +pub trait AccountNamespaceRepository: Send + Sync { + /// Reads one internal non-secret account-scoped value. + /// + /// # Errors + /// + /// Returns a safe storage error when the value cannot be read. + fn get_value(&self, owner: PublicKey, key: &str) -> Result<Option<String>, SafeError>; + /// Writes one internal non-secret account-scoped value. + /// + /// # Errors + /// + /// Returns a safe storage error when the value cannot be committed. + fn set_value(&self, owner: PublicKey, key: &str, value: &str) -> Result<(), SafeError>; + /// Removes all internal values owned by an account. + /// + /// # Errors + /// + /// Returns a safe storage error when cleanup cannot be committed. + fn clear_owner(&self, owner: PublicKey) -> Result<(), SafeError>; +} + +pub trait AppStateRepository: Send + Sync { + /// Loads the persisted selected account. + /// + /// # Errors + /// + /// Returns a safe storage error when application state cannot be read. + fn load_selected_account(&self) -> Result<Option<PublicKey>, SafeError>; + /// Persists the selected account or the empty selection. + /// + /// # Errors + /// + /// Returns a safe storage error when application state cannot be committed. + fn save_selected_account(&self, public_key: Option<PublicKey>) -> Result<(), SafeError>; +} + +pub trait SecretStore: Send + Sync {} + +pub trait NostrClient: Send + Sync { + fn fetch_profile<'a>( + &'a self, + public_key: PublicKey, + relays: &'a [RelayUrl], + ) -> BoxFuture<'a, Result<Option<Kind0ProfileCandidate>, SafeError>>; +} + +pub trait Clock: Send + Sync { + fn now(&self) -> UnixTimestamp; +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use radroots_studio_domain::{ + AccountSummary, Kind0ProfileCandidate, ProfileMetadata, PublicKey, RelayUrl, SafeError, + UnixTimestamp, + }; + + use super::{ + AccountNamespaceRepository, AccountRepository, AppStateRepository, BoxFuture, Clock, + NostrClient, ProfileRepository, SecretStore, + }; + + #[derive(Default)] + struct FakePorts { + selected: Mutex<Option<PublicKey>>, + } + + impl AccountRepository for FakePorts { + fn list_accounts(&self) -> Result<Vec<AccountSummary>, SafeError> { + Ok(Vec::new()) + } + + fn find_account( + &self, + _public_key: PublicKey, + ) -> Result<Option<AccountSummary>, SafeError> { + Ok(None) + } + + fn save_account(&self, _account: &AccountSummary) -> Result<(), SafeError> { + Ok(()) + } + + fn remove_account(&self, _public_key: PublicKey) -> Result<(), SafeError> { + Ok(()) + } + } + + impl ProfileRepository for FakePorts { + fn load_profile( + &self, + _public_key: PublicKey, + ) -> Result<Option<ProfileMetadata>, SafeError> { + Ok(None) + } + + fn save_profile(&self, _candidate: &Kind0ProfileCandidate) -> Result<(), SafeError> { + Ok(()) + } + + fn remove_profile(&self, _public_key: PublicKey) -> Result<(), SafeError> { + Ok(()) + } + } + + impl AccountNamespaceRepository for FakePorts { + fn get_value(&self, _owner: PublicKey, _key: &str) -> Result<Option<String>, SafeError> { + Ok(None) + } + + fn set_value(&self, _owner: PublicKey, _key: &str, _value: &str) -> Result<(), SafeError> { + Ok(()) + } + + fn clear_owner(&self, _owner: PublicKey) -> Result<(), SafeError> { + Ok(()) + } + } + + impl AppStateRepository for FakePorts { + fn load_selected_account(&self) -> Result<Option<PublicKey>, SafeError> { + Ok(*self.selected.lock().expect("selected lock")) + } + + fn save_selected_account(&self, public_key: Option<PublicKey>) -> Result<(), SafeError> { + *self.selected.lock().expect("selected lock") = public_key; + Ok(()) + } + } + + impl SecretStore for FakePorts {} + + impl NostrClient for FakePorts { + fn fetch_profile<'a>( + &'a self, + _public_key: PublicKey, + _relays: &'a [RelayUrl], + ) -> BoxFuture<'a, Result<Option<Kind0ProfileCandidate>, SafeError>> { + Box::pin(async { Ok(None) }) + } + } + + impl Clock for FakePorts { + fn now(&self) -> UnixTimestamp { + UnixTimestamp::from_seconds(1).expect("valid fake time") + } + } + + fn assert_send_sync<T: Send + Sync>() {} + + #[test] + fn ports_accept_send_sync_test_fakes() { + assert_send_sync::<FakePorts>(); + + let ports = FakePorts::default(); + ports + .save_selected_account(Some(PublicKey::from_bytes([1_u8; 32]))) + .expect("save selection"); + assert_eq!( + ports.load_selected_account().expect("load selection"), + Some(PublicKey::from_bytes([1_u8; 32])) + ); + assert_eq!(ports.now().as_seconds(), 1); + } +} diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -278,7 +278,7 @@ and focused domain tests. ### RCLD-03: Application state machine -Status: pending. +Status: in progress. Scope: checkpoints 12 through 14. Define repository, secret, clock, and Nostr ports; implement in-memory AppCore bootstrap, command serialization, monotonic @@ -467,7 +467,7 @@ handoff commit sequence. ### RCLD-03 -- [ ] 12. Add application ports for repositories, secrets, clock, and Nostr +- [x] 12. Add application ports for repositories, secrets, clock, and Nostr client. - [ ] 13. Implement in-memory AppCore bootstrap and observer registry. - [ ] 14. Add state transition helpers and command trace tests.