app

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

commit 3c1f80c47cc4351ad65a77a6726886c27338459c
parent fc873b10183c6b0af47c7d48b0700501f946b42c
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 13:07:07 +0000

desktop: add async country lookup backend

- add a desktop country lookup tracker for async country list and center requests
- wire the desktop backend into the new core country lookup request and poll contract
- keep country queries on the existing offline geocoder boundary without reintroducing eager startup work
- add desktop tests for queued country list and center updates

Diffstat:
Acrates/desktop/src/country_lookup.rs | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/desktop/src/main.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 242 insertions(+), 0 deletions(-)

diff --git a/crates/desktop/src/country_lookup.rs b/crates/desktop/src/country_lookup.rs @@ -0,0 +1,187 @@ +use crate::offline_geocoder; +use radroots_app_core::{ + RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, + RadrootsLocationResolverError, RadrootsOfflineGeocoderState, +}; +#[cfg(target_os = "macos")] +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +#[derive(Clone, Default)] +pub(crate) struct DesktopCountryLookup { + country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>, + country_list_changed: Arc<AtomicBool>, + country_list_pending: Arc<AtomicBool>, + country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>, + country_center_changed: Arc<AtomicBool>, + country_center_pending: Arc<AtomicBool>, +} + +impl DesktopCountryLookup { + pub(crate) fn new() -> Self { + Self::default() + } + + #[cfg(target_os = "macos")] + pub(crate) fn begin_list( + &self, + app_data_root: PathBuf, + geocoder_state: RadrootsOfflineGeocoderState, + ) -> Result<(), RadrootsLocationResolverError> { + if self.country_list_pending.swap(true, Ordering::AcqRel) { + return Err(RadrootsLocationResolverError::QueryFailed { + message: "offline country list query is already running".to_owned(), + }); + } + + if let Ok(mut slot) = self.country_list_result.lock() { + *slot = None; + } + + let result = Arc::clone(&self.country_list_result); + let changed = Arc::clone(&self.country_list_changed); + let pending = Arc::clone(&self.country_list_pending); + std::thread::spawn(move || { + let lookup_result = + offline_geocoder::list_countries(app_data_root.as_path(), &geocoder_state); + if let Ok(mut slot) = result.lock() { + *slot = Some(lookup_result); + changed.store(true, Ordering::Release); + } + pending.store(false, Ordering::Release); + }); + + Ok(()) + } + + #[cfg(not(target_os = "macos"))] + pub(crate) fn begin_list( + &self, + _app_data_root: std::path::PathBuf, + _geocoder_state: RadrootsOfflineGeocoderState, + ) -> Result<(), RadrootsLocationResolverError> { + Err(RadrootsLocationResolverError::Unsupported) + } + + #[cfg(target_os = "macos")] + pub(crate) fn begin_center( + &self, + app_data_root: PathBuf, + geocoder_state: RadrootsOfflineGeocoderState, + country_id: String, + ) -> Result<(), RadrootsLocationResolverError> { + if self.country_center_pending.swap(true, Ordering::AcqRel) { + return Err(RadrootsLocationResolverError::QueryFailed { + message: "offline country center query is already running".to_owned(), + }); + } + + if let Ok(mut slot) = self.country_center_result.lock() { + *slot = None; + } + + let result = Arc::clone(&self.country_center_result); + let changed = Arc::clone(&self.country_center_changed); + let pending = Arc::clone(&self.country_center_pending); + std::thread::spawn(move || { + let lookup_result = offline_geocoder::country_center( + app_data_root.as_path(), + &geocoder_state, + &country_id, + ); + if let Ok(mut slot) = result.lock() { + *slot = Some(lookup_result); + changed.store(true, Ordering::Release); + } + pending.store(false, Ordering::Release); + }); + + Ok(()) + } + + #[cfg(not(target_os = "macos"))] + pub(crate) fn begin_center( + &self, + _app_data_root: std::path::PathBuf, + _geocoder_state: RadrootsOfflineGeocoderState, + _country_id: String, + ) -> Result<(), RadrootsLocationResolverError> { + Err(RadrootsLocationResolverError::Unsupported) + } + + pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> { + if !self.country_list_changed.swap(false, Ordering::AcqRel) { + return None; + } + + match self.country_list_result.lock() { + Ok(mut slot) => slot.take(), + Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { + message: "desktop country list result lock poisoned".to_owned(), + })), + } + } + + pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> { + if !self.country_center_changed.swap(false, Ordering::AcqRel) { + return None; + } + + match self.country_center_result.lock() { + Ok(mut slot) => slot.take(), + Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { + message: "desktop country center result lock poisoned".to_owned(), + })), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint}; + + fn sample_countries() -> RadrootsLocationCountryListResult { + Ok(vec![RadrootsLocationCountry { + country_id: "BR".to_owned(), + country_name: Some("Brazil".to_owned()), + center: RadrootsLocationPoint { + lat: -14.235, + lng: -51.9253, + }, + }]) + } + + #[test] + fn take_list_update_is_none_until_tracker_changes() { + let tracker = DesktopCountryLookup::new(); + + assert_eq!(tracker.take_list_update(), None); + } + + #[test] + fn take_list_update_returns_queued_result_once() { + let tracker = DesktopCountryLookup::new(); + *tracker.country_list_result.lock().unwrap() = Some(sample_countries()); + tracker.country_list_changed.store(true, Ordering::Release); + + assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1)); + assert_eq!(tracker.take_list_update(), None); + } + + #[test] + fn take_center_update_returns_queued_result_once() { + let tracker = DesktopCountryLookup::new(); + *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint { + lat: -14.235, + lng: -51.9253, + })); + tracker + .country_center_changed + .store(true, Ordering::Release); + + assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235)); + assert_eq!(tracker.take_center_update(), None); + } +} diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -11,6 +11,7 @@ use radroots_app_apple_security::{ use radroots_app_core::{ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, RadrootsApp, RadrootsAppBackend, RadrootsLocationCountry, + RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, @@ -28,9 +29,11 @@ use std::sync::Arc; #[cfg(target_os = "macos")] use zeroize::Zeroizing; +mod country_lookup; mod offline_geocoder; mod reverse_lookup; +use country_lookup::DesktopCountryLookup; use offline_geocoder::DesktopOfflineGeocoder; use reverse_lookup::DesktopReverseLookup; @@ -61,6 +64,7 @@ fn desktop_icon() -> Option<egui::IconData> { } struct DesktopBackend { + country_lookup: DesktopCountryLookup, offline_geocoder: DesktopOfflineGeocoder, reverse_lookup: DesktopReverseLookup, } @@ -88,6 +92,7 @@ impl DesktopBackend { )); Self { + country_lookup: DesktopCountryLookup::new(), offline_geocoder, reverse_lookup: DesktopReverseLookup::new(), } @@ -354,6 +359,56 @@ impl RadrootsAppBackend for DesktopBackend { Ok(self.reverse_lookup.take_update()) } + fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { + #[cfg(target_os = "macos")] + { + let app_data_root = Self::app_data_root() + .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; + return self + .country_lookup + .begin_list(app_data_root, self.offline_geocoder.current_state()); + } + + #[cfg(not(target_os = "macos"))] + { + Err(RadrootsLocationResolverError::Unsupported) + } + } + + fn poll_location_country_list_result( + &self, + ) -> Result<Option<RadrootsLocationCountryListResult>, String> { + Ok(self.country_lookup.take_list_update()) + } + + fn request_location_country_center_lookup( + &self, + country_id: &str, + ) -> Result<(), RadrootsLocationResolverError> { + #[cfg(target_os = "macos")] + { + let app_data_root = Self::app_data_root() + .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; + return self.country_lookup.begin_center( + app_data_root, + self.offline_geocoder.current_state(), + country_id.to_owned(), + ); + } + + #[cfg(not(target_os = "macos"))] + { + let _ = country_id; + Err(RadrootsLocationResolverError::Unsupported) + } + } + + fn poll_location_country_center_lookup_result( + &self, + ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> { + Ok(self.country_lookup.take_center_update()) + } + fn list_location_countries( &self, ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {