commit c998293000db6aa1c97bd83ed3259a1d3005246c
parent db4f3acda1eb67bc9a405b3c76962ca0f03d8dcb
Author: triesap <tyson@radroots.org>
Date: Fri, 19 Jun 2026 16:43:26 -0700
runtime: expose mobile read outcomes
- make profile reads return typed operational failures around optional data.
- make post-stream polling distinguish no event from stream failures.
- map disabled, missing identity, uninitialized relay, and stream errors explicitly.
- cover profile failure outcomes and no-stream no-data behavior in field core tests.
Diffstat:
3 files changed, 107 insertions(+), 34 deletions(-)
diff --git a/crates/field_core/src/runtime/nostr.rs b/crates/field_core/src/runtime/nostr.rs
@@ -76,6 +76,31 @@ fn map_post_event_metadata(
}
}
+#[cfg(feature = "nostr-client")]
+fn map_profile_event_metadata(
+ event: radroots_events_codec::parsed::RadrootsParsedData<
+ radroots_events_codec::profile::RadrootsProfileData,
+ >,
+) -> NostrProfileEventMetadata {
+ NostrProfileEventMetadata {
+ id: event.id,
+ author: event.author,
+ published_at: event.published_at as u64,
+ profile: NostrProfile {
+ name: event.data.profile.name.into(),
+ display_name: event.data.profile.display_name.into(),
+ nip05: event.data.profile.nip05.into(),
+ about: event.data.profile.about.into(),
+ website: event.data.profile.website,
+ picture: event.data.profile.picture,
+ banner: event.data.profile.banner,
+ lud06: event.data.profile.lud06,
+ lud16: event.data.profile.lud16,
+ bot: event.data.profile.bot,
+ },
+ }
+}
+
#[cfg_attr(not(coverage_nightly), uniffi::export)]
impl RadrootsRuntime {
pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> {
@@ -151,35 +176,31 @@ impl RadrootsRuntime {
}
}
- pub fn nostr_profile_for_self(&self) -> Option<NostrProfileEventMetadata> {
+ pub fn nostr_profile_for_self(
+ &self,
+ ) -> Result<Option<NostrProfileEventMetadata>, RadrootsAppError> {
#[cfg(feature = "nostr-client")]
{
- let guard = self.net.lock().ok()?;
- let keys = guard.selected_nostr_keys()?;
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
+ };
+ let keys = guard.selected_nostr_keys().ok_or_else(|| {
+ RadrootsAppError::identity("selected signing identity is not configured")
+ })?;
let pk = keys.public_key();
- let mgr = guard.nostr.as_ref()?;
- let out = mgr.fetch_profile_event_blocking(pk).ok()?;
- return out.map(|m| NostrProfileEventMetadata {
- id: m.id,
- author: m.author,
- published_at: m.published_at as u64,
- profile: NostrProfile {
- name: m.data.profile.name.into(),
- display_name: m.data.profile.display_name.into(),
- nip05: m.data.profile.nip05.into(),
- about: m.data.profile.about.into(),
- website: m.data.profile.website,
- picture: m.data.profile.picture,
- banner: m.data.profile.banner,
- lud06: m.data.profile.lud06,
- lud16: m.data.profile.lud16,
- bot: m.data.profile.bot,
- },
- });
+ let mgr = guard
+ .nostr
+ .as_ref()
+ .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))?;
+ let out = mgr
+ .fetch_profile_event_blocking(pk)
+ .map_err(|error| RadrootsAppError::relay(error.to_string()))?;
+ Ok(out.map(map_profile_event_metadata))
}
#[cfg(not(feature = "nostr-client"))]
{
- None
+ Err(RadrootsAppError::unsupported("nostr disabled"))
}
}
@@ -324,24 +345,33 @@ impl RadrootsRuntime {
}
}
- pub fn nostr_next_post_event(&self) -> Option<NostrPostEventMetadata> {
+ pub fn nostr_next_post_event(
+ &self,
+ ) -> Result<Option<NostrPostEventMetadata>, RadrootsAppError> {
#[cfg(feature = "nostr-client")]
{
- let mut rx_guard = self.post_events_rx.lock().ok()?;
- let rx = rx_guard.as_mut()?;
+ let mut rx_guard = match self.post_events_rx.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
+ };
+ let Some(rx) = rx_guard.as_mut() else {
+ return Ok(None);
+ };
match rx.try_recv() {
- Ok(event) => Some(map_post_event_metadata(event)),
- Err(TryRecvError::Empty) => None,
- Err(TryRecvError::Lagged(_)) => None,
+ Ok(event) => Ok(Some(map_post_event_metadata(event))),
+ Err(TryRecvError::Empty) => Ok(None),
+ Err(TryRecvError::Lagged(count)) => Err(RadrootsAppError::relay(format!(
+ "post event stream lagged by {count} events"
+ ))),
Err(TryRecvError::Closed) => {
*rx_guard = None;
- None
+ Err(RadrootsAppError::relay("post event stream closed"))
}
}
}
#[cfg(not(feature = "nostr-client"))]
{
- None
+ Err(RadrootsAppError::unsupported("nostr disabled"))
}
}
diff --git a/crates/field_core/tests/error_mapping.rs b/crates/field_core/tests/error_mapping.rs
@@ -28,6 +28,50 @@ fn uninitialized_nostr_publish_maps_to_relay_error() {
}
#[test]
+fn profile_read_without_identity_maps_to_identity_error() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+
+ let err = runtime
+ .nostr_profile_for_self()
+ .expect_err("missing identity should fail");
+
+ assert!(matches!(err, RadrootsAppError::Identity(_)));
+}
+
+#[test]
+fn profile_read_without_initialized_nostr_maps_to_relay_error() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+ let identity = radroots_identity::RadrootsIdentity::generate();
+ runtime
+ .nostr_identity_restore_host_custody_secret(
+ identity.secret_key_hex(),
+ Some("field".to_string()),
+ true,
+ )
+ .expect("restore identity");
+
+ let err = runtime
+ .nostr_profile_for_self()
+ .expect_err("uninitialized nostr should fail");
+
+ assert!(matches!(
+ err,
+ RadrootsAppError::Relay(message) if message == "nostr not initialized"
+ ));
+}
+
+#[test]
+fn post_stream_read_without_started_stream_returns_no_data() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+
+ let event = runtime
+ .nostr_next_post_event()
+ .expect("missing stream should be a no-data state");
+
+ assert!(event.is_none());
+}
+
+#[test]
fn retired_trade_operations_map_to_unsupported_error() {
let runtime = RadrootsRuntime::new().expect("runtime");
diff --git a/crates/field_core/tests/no_nostr_runtime.rs b/crates/field_core/tests/no_nostr_runtime.rs
@@ -55,9 +55,8 @@ fn nostr_disabled_paths_are_exercised() {
assert_eq!(status.connecting, 0);
assert!(status.last_error.is_none());
- assert!(runtime.nostr_profile_for_self().is_none());
- assert!(runtime.nostr_next_post_event().is_none());
-
+ expect_disabled(runtime.nostr_profile_for_self());
+ expect_disabled(runtime.nostr_next_post_event());
expect_disabled(runtime.nostr_set_default_relays(vec!["wss://relay.example.com".to_string()]));
expect_disabled(runtime.nostr_connect_if_key_present());
expect_disabled(runtime.nostr_post_profile(None, None, None, None));