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