field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit aa144b2f2f2afe3e9adc1afb5a41306c698b932c
parent 1665548581a8ee9582ff943ffa625bcc13d59960
Author: triesap <tyson@radroots.org>
Date:   Sat,  8 Aug 2026 01:34:58 +0000

feat(mobile): sync Today from bounded relay data

- propagate exact selectors through canonical sync pulls
- fetch and typed-admit Today events before projection
- expose relay sync outcomes through the native boundary
- honor public, simulator, and device context policies

Diffstat:
Mcore/crates/tera_ffi/src/dto.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcore/crates/tera_ffi/src/runtime.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcore/crates/tera_ffi/tests/runtime_delegation.rs | 13+++++++++++++
3 files changed, 131 insertions(+), 30 deletions(-)

diff --git a/core/crates/tera_ffi/src/dto.rs b/core/crates/tera_ffi/src/dto.rs @@ -19,12 +19,12 @@ use radroots_mobile_core::runtime::{ info::{AppInfo, RuntimeBuildInfo, RuntimeInfo}, product_surface::{ AddCommandType, CardLifecycleState, CreateAsk, CreateEvent, CreateFoodAvailability, - CreatePhotoUpdate, CreateUpdate, LocalNetwork, MeSnapshot, MediaReference, - MediaVerificationState, Phase1AddCommand, Phase1CancellationPolicy, Phase1DraftStatus, - Phase1MediaPrerequisite, Phase1MediaStage, Phase1OutboxState, Phase1QueuePolicy, - Phase1RelaySatisfaction, ProfileSummary, SearchResult, SearchResultType, SupportingProfile, - ThreadEntry, TodayCard, TodayCardType, TodayPage, TodayProjectionUpdate, - TodayRefreshReceipt, + CreatePhotoUpdate, CreateUpdate, LocalNetwork, LocalNetworkRelayPolicy, MeSnapshot, + MediaReference, MediaVerificationState, Phase1AddCommand, Phase1CancellationPolicy, + Phase1DraftStatus, Phase1MediaPrerequisite, Phase1MediaStage, Phase1OutboxState, + Phase1QueuePolicy, Phase1RelaySatisfaction, ProfileSummary, SearchResult, SearchResultType, + SupportingProfile, ThreadEntry, TodayCard, TodayCardType, TodayPage, TodayProjectionUpdate, + TodayRefreshReceipt, TodayRelaySyncState, TodaySyncReceipt, }, sdk::{ SdkCapabilityRecord, SdkRelayStatusRecord, SdkRelayStatusReportRecord, SdkShutdownRecord, @@ -210,14 +210,24 @@ impl TryFrom<FfiLocalNetworkRecord> for LocalNetwork { type Error = RadrootsAppError; fn try_from(value: FfiLocalNetworkRecord) -> Result<Self, Self::Error> { - require_schema(value.schema_version)?; - LocalNetwork::new( - value.id, - value.label, - value.relay_urls, - value.locality, - value.followed_authors, - value.generation, + value.try_into_with_relay_policy(LocalNetworkRelayPolicy::Public) + } +} + +impl FfiLocalNetworkRecord { + pub(crate) fn try_into_with_relay_policy( + self, + relay_policy: LocalNetworkRelayPolicy, + ) -> Result<LocalNetwork, RadrootsAppError> { + require_schema(self.schema_version)?; + LocalNetwork::new_for_relay_policy( + self.id, + self.label, + self.relay_urls, + self.locality, + self.followed_authors, + self.generation, + relay_policy, ) .map_err(|_| RadrootsAppError::invalid_argument("invalid_local_network")) } @@ -494,6 +504,43 @@ pub struct FfiTodayRefreshRecord { pub changed: bool, } +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum FfiTodayRelaySyncState { + Complete, + Partial, + Offline, +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct FfiTodaySyncRecord { + pub schema_version: u16, + pub relay_state: FfiTodayRelaySyncState, + pub pages_fetched: u16, + pub events_observed: u64, + pub events_admitted: u64, + pub events_rejected: u64, + pub projection: FfiTodayRefreshRecord, +} + +#[cfg_attr(coverage_nightly, coverage(off))] +impl From<TodaySyncReceipt> for FfiTodaySyncRecord { + fn from(value: TodaySyncReceipt) -> Self { + Self { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + relay_state: match value.relay_state { + TodayRelaySyncState::Complete => FfiTodayRelaySyncState::Complete, + TodayRelaySyncState::Partial => FfiTodayRelaySyncState::Partial, + TodayRelaySyncState::Offline => FfiTodayRelaySyncState::Offline, + }, + pages_fetched: value.pages_fetched, + events_observed: value.events_observed, + events_admitted: value.events_admitted, + events_rejected: value.events_rejected, + projection: value.projection.into(), + } + } +} + #[cfg_attr(coverage_nightly, coverage(off))] impl From<TodayRefreshReceipt> for FfiTodayRefreshRecord { fn from(value: TodayRefreshReceipt) -> Self { diff --git a/core/crates/tera_ffi/src/runtime.rs b/core/crates/tera_ffi/src/runtime.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use radroots_mobile_core::runtime::product_surface::LocalNetworkRelayPolicy; use radroots_mobile_core::runtime::product_surface::TodayPageRequest; use crate::dto::PreparedMedia; @@ -11,7 +12,8 @@ use crate::{ FfiMeRecord, FfiQueuePolicyRecord, FfiRelayStatusReportRecord, FfiRuntimeChangeKind, FfiRuntimeInfoRecord, FfiSearchResultRecord, FfiShutdownRecord, FfiStorageStatusRecord, FfiSubscriptionHandle, FfiTodayPageRecord, FfiTodayProjectionUpdate, FfiTodayRefreshRecord, - RadrootsAppError, RadrootsHostSigner, RadrootsRuntimeObserver, add_schemas, decode_id, + FfiTodaySyncRecord, RadrootsAppError, RadrootsHostSigner, RadrootsRuntimeObserver, add_schemas, + decode_id, }; #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] @@ -253,7 +255,7 @@ impl RadrootsRuntime { &self, context: FfiLocalNetworkRecord, ) -> Result<FfiLocalNetworkRecord, RadrootsAppError> { - LocalNetworkRecordConversion::round_trip(context) + self.local_network(context).map(Into::into) } pub async fn phase1_today_page( @@ -268,7 +270,7 @@ impl RadrootsRuntime { "invalid_today_page_request", )); } - let context = context.try_into()?; + let context = self.local_network(context)?; let request = match cursor { Some(cursor) => TodayPageRequest::after(limit, cursor), None => TodayPageRequest::first( @@ -290,7 +292,7 @@ impl RadrootsRuntime { now_unix_s: u64, update: FfiTodayProjectionUpdate, ) -> Result<FfiTodayRefreshRecord, RadrootsAppError> { - let context = context.try_into()?; + let context = self.local_network(context)?; let receipt = self .inner .phase1_refresh_today(&context, now_unix_s, update.into()) @@ -300,6 +302,22 @@ impl RadrootsRuntime { Ok(receipt.into()) } + pub async fn phase1_sync_today( + &self, + context: FfiLocalNetworkRecord, + now_unix_s: u64, + update: FfiTodayProjectionUpdate, + ) -> Result<FfiTodaySyncRecord, RadrootsAppError> { + let context = self.local_network(context)?; + let receipt = self + .inner + .phase1_sync_today(&context, now_unix_s, update.into()) + .await + .map_err(RadrootsAppError::from)?; + self.subscriptions.notify(FfiRuntimeChangeKind::Today, None); + Ok(receipt.into()) + } + pub async fn phase1_search( &self, context: FfiLocalNetworkRecord, @@ -307,7 +325,7 @@ impl RadrootsRuntime { limit: u16, as_of_unix_s: u64, ) -> Result<Vec<FfiSearchResultRecord>, RadrootsAppError> { - let context = context.try_into()?; + let context = self.local_network(context)?; self.inner .phase1_search(&context, &query, limit, as_of_unix_s) .await @@ -320,7 +338,7 @@ impl RadrootsRuntime { context: FfiLocalNetworkRecord, as_of_unix_s: u64, ) -> Result<FfiMeRecord, RadrootsAppError> { - let context = context.try_into()?; + let context = self.local_network(context)?; let public_key = self .inner .authenticated_store_public_key_hex() @@ -514,6 +532,39 @@ impl RadrootsRuntime { } } +impl RadrootsRuntime { + fn local_network( + &self, + context: FfiLocalNetworkRecord, + ) -> Result<radroots_mobile_core::runtime::product_surface::LocalNetwork, RadrootsAppError> + { + let profile = self.inner.sdk_relay_status()?.ok_or_else(|| { + RadrootsAppError::failure( + "relay_profile_unavailable", + "relay", + true, + &["configure_relay"], + "The relay profile is unavailable.", + ) + })?; + let relay_policy = match profile.profile.as_str() { + "public" => LocalNetworkRelayPolicy::Public, + "simulator_local" => LocalNetworkRelayPolicy::Simulator, + "device_development" => LocalNetworkRelayPolicy::Device, + _ => { + return Err(RadrootsAppError::failure( + "relay_profile_unsupported", + "relay", + false, + &["configure_relay"], + "The relay profile is unsupported.", + )); + } + }; + context.try_into_with_relay_policy(relay_policy) + } +} + async fn build_runtime( application_support_directory: String, public_key_hex: String, @@ -544,13 +595,3 @@ async fn build_runtime( }) .map_err(Into::into) } - -struct LocalNetworkRecordConversion; - -impl LocalNetworkRecordConversion { - fn round_trip(value: FfiLocalNetworkRecord) -> Result<FfiLocalNetworkRecord, RadrootsAppError> { - let context: radroots_mobile_core::runtime::product_surface::LocalNetwork = - value.try_into()?; - Ok(context.into()) - } -} diff --git a/core/crates/tera_ffi/tests/runtime_delegation.rs b/core/crates/tera_ffi/tests/runtime_delegation.rs @@ -69,6 +69,19 @@ async fn native_boundary_delegates_the_complete_core_surface() { assert_eq!(simulator.relays[0].access, "read_write"); assert!( runtime + .phase1_local_network(FfiLocalNetworkRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + id: "simulator".to_owned(), + label: "Simulator".to_owned(), + relay_urls: vec!["ws://127.0.0.1:7447".to_owned()], + locality: None, + followed_authors: vec![], + generation: 1, + }) + .is_ok() + ); + assert!( + runtime .configure_simulator_relays(vec!["wss://relay.example".to_owned()]) .is_err() );