app

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

commit c184c0c247305b56f353cdc30841d926125d6bae
parent 4e47ad1a9b2c119d92f8bb4a53bc29521535b68e
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 01:14:47 +0000

core: add revision-aware offline geocoder diagnostics

- extend offline geocoder unavailable state with release-safe platform and asset revision context
- export platform and stamped revision details through the shared offline geocoder diagnostic contract without leaking raw debug paths
- attach the new diagnostic context across desktop ios android and web geocoder initialization paths
- log one-line unavailable events on desktop and ios to match the existing android geocoder failure reporting

Diffstat:
MCargo.lock | 2++
Mcrates/android/src/lib.rs | 1+
Mcrates/android/src/offline_geocoder.rs | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcrates/core/src/lib.rs | 13+++++++++++--
Mcrates/core/src/offline_geocoder.rs | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/desktop/Cargo.toml | 1+
Mcrates/desktop/src/main.rs | 21+++++++++++----------
Mcrates/desktop/src/offline_geocoder.rs | 71+++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mcrates/ios/Cargo.toml | 1+
Mcrates/ios/src/lib.rs | 5+++--
Mcrates/ios/src/offline_geocoder.rs | 71+++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mcrates/web/src/lib.rs | 11++++++-----
12 files changed, 289 insertions(+), 81 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2885,6 +2885,7 @@ dependencies = [ "eframe", "egui", "image", + "log", "objc2-foundation 0.3.2", "radroots-app-apple-security", "radroots-app-core", @@ -2900,6 +2901,7 @@ name = "radroots-app-ios" version = "0.1.0" dependencies = [ "eframe", + "log", "radroots-app-apple-security", "radroots-app-core", "radroots-geocoder", diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs @@ -204,6 +204,7 @@ impl AndroidBackend { let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::from_state( RadrootsOfflineGeocoderState::unavailable( radroots_app_core::RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + radroots_app_core::RadrootsOfflineGeocoderPlatform::Android, "android offline geocoder initialization is only wired on android targets", ), ); diff --git a/crates/android/src/offline_geocoder.rs b/crates/android/src/offline_geocoder.rs @@ -1,6 +1,9 @@ #![cfg_attr(not(target_os = "android"), allow(dead_code))] -use radroots_app_core::{RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind}; +use radroots_app_core::{ + RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, + RadrootsOfflineGeocoderUnavailableKind, +}; #[cfg(target_os = "android")] use radroots_geocoder::Geocoder; use std::path::Path; @@ -60,6 +63,7 @@ impl AndroidOfflineGeocoder { .unwrap_or_else(|_| { RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Android, "android offline geocoder state lock poisoned", ) }) @@ -78,19 +82,44 @@ impl AndroidOfflineGeocoder { fn initialize_offline_geocoder() -> RadrootsOfflineGeocoderState { match initialize_offline_geocoder_inner() { Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err((kind, debug_message)) => { - RadrootsOfflineGeocoderState::unavailable(kind, debug_message) - } + Err((kind, asset_revision, debug_message)) => match asset_revision { + Some(asset_revision) => RadrootsOfflineGeocoderState::unavailable_with_revision( + kind, + RadrootsOfflineGeocoderPlatform::Android, + asset_revision, + debug_message, + ), + None => RadrootsOfflineGeocoderState::unavailable( + kind, + RadrootsOfflineGeocoderPlatform::Android, + debug_message, + ), + }, } } #[cfg(target_os = "android")] -fn initialize_offline_geocoder_inner() --> Result<(), (RadrootsOfflineGeocoderUnavailableKind, String)> { - let staged_path = stage_offline_geocoder_asset()?; +fn initialize_offline_geocoder_inner() -> Result< + (), + ( + RadrootsOfflineGeocoderUnavailableKind, + Option<String>, + String, + ), +> { + let staged_path = stage_offline_geocoder_asset() + .map_err(|(kind, debug_message)| (kind, None, debug_message))?; + let asset_revision = staged_asset_revision(staged_path.as_str()).map_err(|debug_message| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + None, + debug_message, + ) + })?; Geocoder::open_path(staged_path.as_str()).map_err(|source| { ( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + Some(asset_revision.clone()), format!("failed to open staged android geocoder db: {source}"), ) })?; @@ -304,6 +333,24 @@ fn prune_stale_revisions(staged_path: &str) -> Result<(), String> { Ok(()) } +fn staged_asset_revision(staged_path: &str) -> Result<String, String> { + let staged_path = Path::new(staged_path); + let Some(active_revision_dir) = staged_path.parent() else { + return Err("android staged geocoder path did not have a revision directory".to_owned()); + }; + let Some(active_revision) = active_revision_dir.file_name() else { + return Err("android staged geocoder revision directory did not have a name".to_owned()); + }; + let revision = active_revision.to_string_lossy(); + if revision.len() != 64 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err( + "android staged geocoder revision directory name was not a sha256 hex revision" + .to_owned(), + ); + } + Ok(revision.into_owned()) +} + #[cfg(test)] mod tests { use super::*; @@ -313,6 +360,7 @@ mod tests { fn missing_asset_maps_to_build_unavailable_message() { let state = RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Android, "android bundled geocoder asset missing at assets/geocoder/geonames.db", ); @@ -320,6 +368,8 @@ mod tests { state, RadrootsOfflineGeocoderState::Unavailable { kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + platform: RadrootsOfflineGeocoderPlatform::Android, + asset_revision: None, debug_message: "android bundled geocoder asset missing at assets/geocoder/geonames.db" .to_owned(), @@ -328,6 +378,17 @@ mod tests { } #[test] + fn staged_asset_revision_reads_sha256_directory_name() { + let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"; + let staged_path = format!("/tmp/radroots/android/geocoder/{revision}/geonames.db"); + + assert_eq!( + staged_asset_revision(staged_path.as_str()).unwrap(), + revision + ); + } + + #[test] fn prune_stale_revisions_keeps_active_revision_only() { let temp_root = std::env::temp_dir().join(format!( "radroots-android-geocoder-prune-test-{}", diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -9,8 +9,8 @@ mod offline_geocoder; pub const APP_NAME: &str = "Rad Roots"; pub use offline_geocoder::{ - RadrootsOfflineGeocoderDiagnostic, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, + RadrootsOfflineGeocoderDiagnostic, RadrootsOfflineGeocoderPlatform, + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -298,6 +298,11 @@ impl RadrootsApp { if let Some(diagnostic) = state.diagnostic() { ui.label(diagnostic.technical_message); ui.add_space(6.0); + ui.monospace(format!("platform: {}", diagnostic.platform_code)); + ui.monospace(format!( + "asset revision: {}", + diagnostic.asset_revision.as_deref().unwrap_or("unknown") + )); ui.monospace(format!("diagnostic code: {}", diagnostic.code)); if ui.button("Copy Offline Geocoder Diagnostic").clicked() { ui.ctx().copy_text(diagnostic.export_text()); @@ -1169,6 +1174,7 @@ mod tests { RadrootsOfflineGeocoderState::Initializing, vec![Ok(Some(RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Desktop, "failed to open staged geocoder db", )))], ), @@ -1180,6 +1186,7 @@ mod tests { app.offline_geocoder_state, Some(RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Desktop, "failed to open staged geocoder db", )) ); @@ -1200,6 +1207,8 @@ mod tests { .as_ref() .and_then(RadrootsOfflineGeocoderState::diagnostic) .unwrap(); + assert_eq!(diagnostic.platform_code, "desktop"); + assert_eq!(diagnostic.asset_revision, None); assert_eq!(diagnostic.code, "initialization_failed"); assert!( !diagnostic diff --git a/crates/core/src/offline_geocoder.rs b/crates/core/src/offline_geocoder.rs @@ -1,4 +1,23 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsOfflineGeocoderPlatform { + Desktop, + Ios, + Android, + Web, +} + +impl RadrootsOfflineGeocoderPlatform { + pub fn code(self) -> &'static str { + match self { + Self::Desktop => "desktop", + Self::Ios => "ios", + Self::Android => "android", + Self::Web => "web", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RadrootsOfflineGeocoderUnavailableKind { MissingBuildAsset, InitializationFailed, @@ -40,6 +59,8 @@ impl RadrootsOfflineGeocoderUnavailableKind { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RadrootsOfflineGeocoderDiagnostic { + pub platform_code: &'static str, + pub asset_revision: Option<String>, pub code: &'static str, pub summary_label: &'static str, pub user_message: &'static str, @@ -49,8 +70,13 @@ pub struct RadrootsOfflineGeocoderDiagnostic { impl RadrootsOfflineGeocoderDiagnostic { pub fn export_text(&self) -> String { format!( - "offline geocoder diagnostic\ncode: {}\nstatus: {}\nuser: {}\ntechnical: {}", - self.code, self.summary_label, self.user_message, self.technical_message + "offline geocoder diagnostic\nplatform: {}\nasset_revision: {}\ncode: {}\nstatus: {}\nuser: {}\ntechnical: {}", + self.platform_code, + self.asset_revision.as_deref().unwrap_or("unknown"), + self.code, + self.summary_label, + self.user_message, + self.technical_message ) } } @@ -61,6 +87,8 @@ pub enum RadrootsOfflineGeocoderState { Ready, Unavailable { kind: RadrootsOfflineGeocoderUnavailableKind, + platform: RadrootsOfflineGeocoderPlatform, + asset_revision: Option<String>, debug_message: String, }, } @@ -68,10 +96,27 @@ pub enum RadrootsOfflineGeocoderState { impl RadrootsOfflineGeocoderState { pub fn unavailable( kind: RadrootsOfflineGeocoderUnavailableKind, + platform: RadrootsOfflineGeocoderPlatform, debug_message: impl Into<String>, ) -> Self { Self::Unavailable { kind, + platform, + asset_revision: None, + debug_message: debug_message.into(), + } + } + + pub fn unavailable_with_revision( + kind: RadrootsOfflineGeocoderUnavailableKind, + platform: RadrootsOfflineGeocoderPlatform, + asset_revision: impl Into<String>, + debug_message: impl Into<String>, + ) -> Self { + Self::Unavailable { + kind, + platform, + asset_revision: Some(asset_revision.into()), debug_message: debug_message.into(), } } @@ -85,7 +130,14 @@ impl RadrootsOfflineGeocoderState { pub fn diagnostic(&self) -> Option<RadrootsOfflineGeocoderDiagnostic> { match self { - Self::Unavailable { kind, .. } => Some(RadrootsOfflineGeocoderDiagnostic { + Self::Unavailable { + kind, + platform, + asset_revision, + .. + } => Some(RadrootsOfflineGeocoderDiagnostic { + platform_code: platform.code(), + asset_revision: asset_revision.clone(), code: kind.code(), summary_label: self.summary_label(), user_message: kind.user_message(), @@ -103,6 +155,20 @@ impl RadrootsOfflineGeocoderState { } } + pub fn platform(&self) -> Option<RadrootsOfflineGeocoderPlatform> { + match self { + Self::Unavailable { platform, .. } => Some(*platform), + Self::Initializing | Self::Ready => None, + } + } + + pub fn asset_revision(&self) -> Option<&str> { + match self { + Self::Unavailable { asset_revision, .. } => asset_revision.as_deref(), + Self::Initializing | Self::Ready => None, + } + } + pub fn technical_message(&self) -> Option<&'static str> { match self { Self::Unavailable { kind, .. } => Some(kind.technical_message()), @@ -126,10 +192,13 @@ mod tests { fn unavailable_state_exposes_release_safe_diagnostic() { let state = RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Desktop, "failed to open staged geocoder db: /tmp/geonames.db", ); let diagnostic = state.diagnostic().unwrap(); + assert_eq!(diagnostic.platform_code, "desktop"); + assert_eq!(diagnostic.asset_revision, None); assert_eq!(diagnostic.code, "initialization_failed"); assert_eq!(diagnostic.summary_label, "Offline geocoder unavailable"); assert_eq!( @@ -142,4 +211,27 @@ mod tests { ); assert!(!diagnostic.export_text().contains("/tmp/geonames.db")); } + + #[test] + fn unavailable_state_with_revision_exports_release_safe_platform_context() { + let state = RadrootsOfflineGeocoderState::unavailable_with_revision( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Android, + "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c", + "failed to open staged android geocoder db: /data/user/0/org.radroots.app.android/files/geocoder.db", + ); + let diagnostic = state.diagnostic().unwrap(); + let export_text = diagnostic.export_text(); + + assert_eq!(diagnostic.platform_code, "android"); + assert_eq!( + diagnostic.asset_revision.as_deref(), + Some("6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c") + ); + assert!(export_text.contains("platform: android")); + assert!(export_text.contains( + "asset_revision: 6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c" + )); + assert!(!export_text.contains("/data/user/0/org.radroots.app.android/files/geocoder.db")); + } } diff --git a/crates/desktop/Cargo.toml b/crates/desktop/Cargo.toml @@ -19,6 +19,7 @@ directories.workspace = true eframe = { workspace = true, features = ["wgpu", "wayland", "x11"] } egui.workspace = true image.workspace = true +log.workspace = true radroots-app-core = { path = "../core" } radroots-geocoder.workspace = true radroots-nostr-accounts.workspace = true diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -10,8 +10,8 @@ use radroots_app_apple_security::{ }; use radroots_app_core::{ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, - ImportActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, SetupActionState, + ImportActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderPlatform, + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, SetupActionState, }; #[cfg(target_os = "macos")] use radroots_identity::RadrootsIdentity; @@ -64,21 +64,22 @@ impl DesktopBackend { #[cfg(target_os = "macos")] let offline_geocoder = match Self::app_data_root() { Ok(app_data_root) => DesktopOfflineGeocoder::start(app_data_root), - Err(debug_message) => DesktopOfflineGeocoder::from_state( - RadrootsOfflineGeocoderState::unavailable( + Err(debug_message) => { + DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Desktop, debug_message, - ), - ), + )) + } }; #[cfg(not(target_os = "macos"))] - let offline_geocoder = DesktopOfflineGeocoder::from_state( - RadrootsOfflineGeocoderState::unavailable( + let offline_geocoder = + DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Desktop, "desktop offline geocoder initialization is only wired for macos", - ), - ); + )); Self { offline_geocoder } } diff --git a/crates/desktop/src/offline_geocoder.rs b/crates/desktop/src/offline_geocoder.rs @@ -1,4 +1,7 @@ -use radroots_app_core::{RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind}; +use radroots_app_core::{ + RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, + RadrootsOfflineGeocoderUnavailableKind, +}; use radroots_geocoder::Geocoder; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -28,6 +31,9 @@ impl DesktopOfflineGeocoder { let changed = Arc::clone(&tracker.changed); std::thread::spawn(move || { let state = initialize_offline_geocoder(app_data_root.as_path()); + if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state { + log::warn!("desktop offline geocoder unavailable: {debug_message}"); + } if let Ok(mut slot) = current.lock() { *slot = state; changed.store(true, Ordering::Release); @@ -44,6 +50,7 @@ impl DesktopOfflineGeocoder { .unwrap_or_else(|_| { RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Desktop, "desktop offline geocoder state lock poisoned", ) }) @@ -59,49 +66,58 @@ impl DesktopOfflineGeocoder { } fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { - match initialize_offline_geocoder_inner(app_data_root) { - Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err((kind, debug_message)) => { - RadrootsOfflineGeocoderState::unavailable(kind, debug_message) - } - } -} - -fn initialize_offline_geocoder_inner( - app_data_root: &Path, -) -> Result<(), (RadrootsOfflineGeocoderUnavailableKind, String)> { let source_path = runtime_asset_path().map_err(|debug_message| { - ( + RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Desktop, debug_message, ) - })?; + }); + let source_path = match source_path { + Ok(source_path) => source_path, + Err(state) => return state, + }; if !source_path.is_file() { - return Err(( + return RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Desktop, format!( "desktop bundled geocoder asset missing at {}", source_path.display() ), - )); + ); } - let revision = runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new(".")))?; + let revision = + match runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) { + Ok(revision) => revision, + Err((kind, debug_message)) => { + return RadrootsOfflineGeocoderState::unavailable( + kind, + RadrootsOfflineGeocoderPlatform::Desktop, + debug_message, + ); + } + }; let staged_path = staged_db_path(app_data_root, revision.as_str()); - stage_runtime_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { - ( + if let Err(debug_message) = stage_runtime_asset(source_path.as_path(), staged_path.as_path()) { + return RadrootsOfflineGeocoderState::unavailable_with_revision( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Desktop, + revision, debug_message, - ) - })?; - Geocoder::open_path(staged_path.as_path()).map_err(|source| { - ( + ); + } + if let Err(source) = Geocoder::open_path(staged_path.as_path()) { + return RadrootsOfflineGeocoderState::unavailable_with_revision( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Desktop, + revision, format!("failed to open staged geocoder db: {source}"), - ) - })?; + ); + } let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str()); - Ok(()) + RadrootsOfflineGeocoderState::Ready } fn runtime_asset_path() -> Result<PathBuf, String> { @@ -239,6 +255,7 @@ mod tests { fn missing_asset_maps_to_build_unavailable_message() { let state = RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Desktop, "desktop bundled geocoder asset missing at /tmp/geonames.db", ); @@ -246,6 +263,8 @@ mod tests { state, RadrootsOfflineGeocoderState::Unavailable { kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + platform: RadrootsOfflineGeocoderPlatform::Desktop, + asset_revision: None, debug_message: "desktop bundled geocoder asset missing at /tmp/geonames.db" .to_owned(), } diff --git a/crates/ios/Cargo.toml b/crates/ios/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["staticlib", "rlib"] [dependencies] eframe = { workspace = true, features = ["wgpu"] } +log.workspace = true radroots-app-apple-security.workspace = true radroots-app-core = { path = "../core" } radroots-geocoder.workspace = true diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -7,8 +7,8 @@ use radroots_app_core::IdentityGateState; #[cfg(target_os = "ios")] use radroots_app_core::{ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, ImportActionState, - PasteActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, SetupActionState, + PasteActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderPlatform, + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, SetupActionState, }; #[cfg(any(target_os = "ios", test))] use radroots_identity::RadrootsIdentity; @@ -48,6 +48,7 @@ impl IosBackend { Err(debug_message) => offline_geocoder::IosOfflineGeocoder::from_state( RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Ios, debug_message, ), ), diff --git a/crates/ios/src/offline_geocoder.rs b/crates/ios/src/offline_geocoder.rs @@ -1,6 +1,9 @@ #![cfg_attr(not(target_os = "ios"), allow(dead_code))] -use radroots_app_core::{RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind}; +use radroots_app_core::{ + RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, + RadrootsOfflineGeocoderUnavailableKind, +}; use radroots_geocoder::Geocoder; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -30,6 +33,9 @@ impl IosOfflineGeocoder { std::thread::spawn(move || { let state = initialize_offline_geocoder(app_data_root.as_path()); + if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state { + log::warn!("ios offline geocoder unavailable: {debug_message}"); + } if let Ok(mut slot) = current.lock() { *slot = state; changed.store(true, Ordering::Release); @@ -46,6 +52,7 @@ impl IosOfflineGeocoder { .unwrap_or_else(|_| { RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Ios, "ios offline geocoder state lock poisoned", ) }) @@ -61,49 +68,58 @@ impl IosOfflineGeocoder { } fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { - match initialize_offline_geocoder_inner(app_data_root) { - Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err((kind, debug_message)) => { - RadrootsOfflineGeocoderState::unavailable(kind, debug_message) - } - } -} - -fn initialize_offline_geocoder_inner( - app_data_root: &Path, -) -> Result<(), (RadrootsOfflineGeocoderUnavailableKind, String)> { let source_path = bundled_asset_path().map_err(|debug_message| { - ( + RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::InternalError, + RadrootsOfflineGeocoderPlatform::Ios, debug_message, ) - })?; + }); + let source_path = match source_path { + Ok(source_path) => source_path, + Err(state) => return state, + }; if !source_path.is_file() { - return Err(( + return RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Ios, format!( "ios bundled geocoder asset missing at {}", source_path.display() ), - )); + ); } - let revision = bundled_asset_revision(source_path.parent().unwrap_or_else(|| Path::new(".")))?; + let revision = + match bundled_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) { + Ok(revision) => revision, + Err((kind, debug_message)) => { + return RadrootsOfflineGeocoderState::unavailable( + kind, + RadrootsOfflineGeocoderPlatform::Ios, + debug_message, + ); + } + }; let staged_path = staged_db_path(app_data_root, revision.as_str()); - stage_bundled_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { - ( + if let Err(debug_message) = stage_bundled_asset(source_path.as_path(), staged_path.as_path()) { + return RadrootsOfflineGeocoderState::unavailable_with_revision( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Ios, + revision, debug_message, - ) - })?; - Geocoder::open_path(staged_path.as_path()).map_err(|source| { - ( + ); + } + if let Err(source) = Geocoder::open_path(staged_path.as_path()) { + return RadrootsOfflineGeocoderState::unavailable_with_revision( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + RadrootsOfflineGeocoderPlatform::Ios, + revision, format!("failed to open staged ios geocoder db: {source}"), - ) - })?; + ); + } let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str()); - Ok(()) + RadrootsOfflineGeocoderState::Ready } fn bundled_asset_path() -> Result<PathBuf, String> { @@ -242,6 +258,7 @@ mod tests { fn missing_asset_maps_to_build_unavailable_message() { let state = RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Ios, "ios bundled geocoder asset missing at /tmp/geonames.db", ); @@ -249,6 +266,8 @@ mod tests { state, RadrootsOfflineGeocoderState::Unavailable { kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + platform: RadrootsOfflineGeocoderPlatform::Ios, + asset_revision: None, debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db".to_owned(), } ); diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs @@ -13,20 +13,22 @@ use nostr::nips::nip19::ToBech32; use nostr::signer::NostrSigner; #[cfg(target_arch = "wasm32")] use nostr_browser_signer::{BrowserSigner, Error as BrowserSignerError}; -#[cfg(any(target_arch = "wasm32", test))] -use radroots_app_core::{ - RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, -}; #[cfg(target_arch = "wasm32")] use radroots_app_core::{ HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, RadrootsApp, RadrootsAppBackend, SetupActionState, }; +#[cfg(any(target_arch = "wasm32", test))] +use radroots_app_core::{ + RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, + RadrootsOfflineGeocoderUnavailableKind, +}; #[cfg(any(target_arch = "wasm32", test))] fn offline_geocoder_unavailable_state() -> RadrootsOfflineGeocoderState { RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + RadrootsOfflineGeocoderPlatform::Web, "radroots-geocoder currently depends on rusqlite and is not wired for wasm runtime initialization.", ) } @@ -92,7 +94,6 @@ impl WebBackend { state.pending_result = None; IdentityGateState::Missing } - } #[cfg(target_arch = "wasm32")]