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:
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> {