app

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

commit 0624030217a5543e8b1edd1156592509e3538779
parent 336e2c34e6caf8a1b6411c1e6669b1d3e0a0559e
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 21:15:57 +0000

app: wire init stages for asset bootstrap

- add asset bootstrap runner with stage callbacks
- invoke asset bootstrap before backend init
- surface asset errors as init failures
- add tests for asset bootstrap behavior

Diffstat:
Mapp/src/app.rs | 41+++++++++++++++++++++++++++++++++++++----
Mapp/src/init.rs | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mapp/src/lib.rs | 1+
3 files changed, 103 insertions(+), 5 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -2,10 +2,16 @@ use leptos::prelude::*; use leptos::task::spawn_local; use crate::{ + app_init_assets, app_init_backends, + app_init_has_completed, app_init_state_default, app_init_mark_completed, app_init_reset, + app_init_progress_add, + app_init_stage_set, + app_init_total_add, + app_init_total_unknown, app_config_default, app_datastore_read_app_data, app_health_check_all, @@ -86,16 +92,43 @@ pub fn App() -> impl IntoView { provide_context(init_state); Effect::new(move || { spawn_local(async move { - init_state.update(|state| state.stage = AppInitStage::Storage); - match app_init_backends(app_config_default()).await { + init_state.update(|state| app_init_stage_set(state, AppInitStage::Storage)); + let config = app_config_default(); + if !app_init_has_completed() { + init_state.update(|state| { + state.loaded_bytes = 0; + state.total_bytes = Some(0); + }); + let assets_result = app_init_assets( + &config, + |stage| init_state.update(|state| app_init_stage_set(state, stage)), + |loaded, total| { + init_state.update(|state| { + app_init_progress_add(state, loaded); + match total { + Some(value) => app_init_total_add(state, value), + None => app_init_total_unknown(state), + } + }); + }, + ) + .await; + if let Err(err) = assets_result { + init_error.set(Some(AppInitError::Assets(err))); + init_state.update(|state| app_init_stage_set(state, AppInitStage::Error)); + return; + } + init_state.update(|state| app_init_stage_set(state, AppInitStage::Storage)); + } + match app_init_backends(config).await { Ok(value) => { backends.set(Some(value)); app_init_mark_completed(); - init_state.update(|state| state.stage = AppInitStage::Ready); + init_state.update(|state| app_init_stage_set(state, AppInitStage::Ready)); } Err(err) => { init_error.set(Some(err)); - init_state.update(|state| state.stage = AppInitStage::Error); + init_state.update(|state| app_init_stage_set(state, AppInitStage::Error)); } } }) diff --git a/app/src/init.rs b/app/src/init.rs @@ -26,6 +26,8 @@ use crate::{ app_datastore_read_app_data, app_datastore_write_app_data, app_datastore_write_config, + app_assets_geocoder_db_url, + app_assets_sql_wasm_url, app_keystore_nostr_ensure_key, AppAppData, AppConfig, @@ -224,6 +226,7 @@ pub enum AppInitError { Datastore(RadrootsClientDatastoreError), Keystore(RadrootsClientKeystoreError), Config(AppConfigError), + Assets(AppInitAssetError), } pub type AppInitErrorMessage = &'static str; @@ -235,6 +238,7 @@ impl AppInitError { AppInitError::Datastore(_) => "error.app.init.datastore", AppInitError::Keystore(_) => "error.app.init.keystore", AppInitError::Config(_) => "error.app.init.config", + AppInitError::Assets(_) => "error.app.init.assets", } } } @@ -255,6 +259,32 @@ pub struct AppBackends { pub type AppInitResult<T> = Result<T, AppInitError>; +pub async fn app_init_assets<F, G>( + config: &AppConfig, + mut on_stage: F, + mut on_progress: G, +) -> Result<(), AppInitAssetError> +where + F: FnMut(AppInitStage), + G: FnMut(u64, Option<u64>), +{ + if let Some(url) = app_assets_sql_wasm_url(config).filter(|value| !value.is_empty()) { + on_stage(AppInitStage::DownloadSql); + app_init_fetch_asset(url, |loaded, total| { + on_progress(loaded, total); + }) + .await?; + } + if let Some(url) = app_assets_geocoder_db_url(config).filter(|value| !value.is_empty()) { + on_stage(AppInitStage::DownloadGeo); + app_init_fetch_asset(url, |loaded, total| { + on_progress(loaded, total); + }) + .await?; + } + Ok(()) +} + pub fn app_init_has_completed() -> bool { #[cfg(target_arch = "wasm32")] { @@ -370,6 +400,7 @@ pub async fn app_init_backends(config: AppConfig) -> AppInitResult<AppBackends> mod tests { use super::{ app_init_backends, + app_init_assets, app_init_progress_add, app_init_state_default, app_init_stage_set, @@ -378,8 +409,9 @@ mod tests { AppInitError, AppInitErrorMessage, AppInitStage, + AppInitAssetError, }; - use crate::app_config_default; + use crate::{app_config_default, AppConfig}; use radroots_app_core::datastore::RadrootsClientDatastoreError; use radroots_app_core::idb::RadrootsClientIdbStoreError; use radroots_app_core::keystore::{ @@ -409,6 +441,10 @@ mod tests { AppInitError::Config(AppConfigError::MissingKeyMap("nostr_key")), "error.app.init.config", ), + ( + AppInitError::Assets(AppInitAssetError::FetchUnavailable), + "error.app.init.assets", + ), ]; for (err, expected) in cases { assert_eq!(err.message(), *expected); @@ -518,4 +554,32 @@ mod tests { app_init_total_add(&mut state, 5); assert_eq!(state.total_bytes, None); } + + #[test] + fn app_init_assets_skips_when_empty() { + let config = app_config_default(); + let mut stages = Vec::new(); + let mut progress = Vec::new(); + let result = futures::executor::block_on(app_init_assets( + &config, + |stage| stages.push(stage), + |loaded, total| progress.push((loaded, total)), + )); + assert!(result.is_ok()); + assert!(stages.is_empty()); + assert!(progress.is_empty()); + } + + #[test] + fn app_init_assets_reports_unavailable_on_native() { + let mut config = AppConfig::empty(); + config.assets.sql_wasm_url = Some("http://example.com/sql.wasm".to_string()); + let result = futures::executor::block_on(app_init_assets( + &config, + |_stage| {}, + |_loaded, _total| {}, + )) + .expect_err("asset fetch should error on native"); + assert_eq!(result, AppInitAssetError::FetchUnavailable); + } } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -79,6 +79,7 @@ pub use config::{ APP_KEYSTORE_KEY_NOSTR_DEFAULT, }; pub use init::{ + app_init_assets, app_init_backends, app_init_fetch_asset, app_init_has_completed,