commit bad2c1f2aafab17b8f2155eb10f7941f73ee00bf
parent 379a2a506832868fcb1192d05e522eec63e02018
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:
2 files changed, 230 insertions(+), 0 deletions(-)
diff --git a/crates/studio_application/src/lib.rs b/crates/studio_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/crates/studio_application/src/ports.rs b/crates/studio_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);
+ }
+}