app

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

commit 336e2c34e6caf8a1b6411c1e6669b1d3e0a0559e
parent d2052a98723de2f7bf09daeaff43d423219ae5a1
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 21:13:38 +0000

app: add init asset fetch helpers

- add asset fetch helper with progress callback
- add init stage and progress update helpers
- gate fetch helper for wasm with fallback error
- add unit tests for progress helper behavior

Diffstat:
MCargo.lock | 3+++
Mapp/Cargo.toml | 3+++
Mapp/src/init.rs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/lib.rs | 7+++++++
4 files changed, 156 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1542,11 +1542,14 @@ version = "0.1.0" dependencies = [ "async-trait", "futures", + "js-sys", "leptos", "radroots-app-core", "serde", "serde_json", "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/app/Cargo.toml b/app/Cargo.toml @@ -12,6 +12,9 @@ crate-type = ["cdylib", "rlib"] [dependencies] leptos = { workspace = true, features = ["csr"] } wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +js-sys.workspace = true +web-sys.workspace = true radroots-app-core = { path = "../crates/core" } serde.workspace = true diff --git a/app/src/init.rs b/app/src/init.rs @@ -37,6 +37,14 @@ use crate::{ #[cfg(target_arch = "wasm32")] use leptos::prelude::window; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::JsFuture; +#[cfg(target_arch = "wasm32")] +use js_sys::Uint8Array; +#[cfg(target_arch = "wasm32")] +use web_sys::Response; pub const APP_INIT_STORAGE_KEY: &str = "radroots.app.init.ready"; @@ -96,6 +104,120 @@ pub const fn app_init_state_default() -> AppInitState { } } +pub fn app_init_stage_set(state: &mut AppInitState, stage: AppInitStage) { + state.stage = stage; +} + +pub fn app_init_progress_add(state: &mut AppInitState, bytes: u64) { + if bytes == 0 { + return; + } + state.loaded_bytes = state.loaded_bytes.saturating_add(bytes); +} + +pub fn app_init_total_add(state: &mut AppInitState, bytes: u64) { + if bytes == 0 { + return; + } + let Some(total) = state.total_bytes else { + return; + }; + state.total_bytes = Some(total.saturating_add(bytes)); +} + +pub fn app_init_total_unknown(state: &mut AppInitState) { + state.total_bytes = None; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AppInitAssetProgress { + pub loaded_bytes: u64, + pub total_bytes: Option<u64>, +} + +impl AppInitAssetProgress { + pub const fn empty() -> Self { + Self { + loaded_bytes: 0, + total_bytes: Some(0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppInitAssetError { + MissingUrl, + FetchUnavailable, + FetchFailed, +} + +impl AppInitAssetError { + pub const fn message(self) -> &'static str { + match self { + AppInitAssetError::MissingUrl => "error.app.init.asset_missing_url", + AppInitAssetError::FetchUnavailable => "error.app.init.asset_unavailable", + AppInitAssetError::FetchFailed => "error.app.init.asset_fetch_failed", + } + } +} + +impl fmt::Display for AppInitAssetError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.message()) + } +} + +impl std::error::Error for AppInitAssetError {} + +#[cfg(target_arch = "wasm32")] +pub async fn app_init_fetch_asset<F>( + url: &str, + mut on_progress: F, +) -> Result<AppInitAssetProgress, AppInitAssetError> +where + F: FnMut(u64, Option<u64>), +{ + if url.is_empty() { + return Err(AppInitAssetError::MissingUrl); + } + let response_value = JsFuture::from(window().fetch_with_str(url)) + .await + .map_err(|_| AppInitAssetError::FetchFailed)?; + let response: Response = response_value + .dyn_into() + .map_err(|_| AppInitAssetError::FetchFailed)?; + let total_bytes = response + .headers() + .get("content-length") + .ok() + .flatten() + .and_then(|value| value.parse::<u64>().ok()); + let buffer_value = JsFuture::from(response.array_buffer().map_err(|_| AppInitAssetError::FetchFailed)?) + .await + .map_err(|_| AppInitAssetError::FetchFailed)?; + let buffer = Uint8Array::new(&buffer_value); + let loaded_bytes = buffer.length() as u64; + on_progress(loaded_bytes, total_bytes); + Ok(AppInitAssetProgress { + loaded_bytes, + total_bytes, + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn app_init_fetch_asset<F>( + url: &str, + _on_progress: F, +) -> Result<AppInitAssetProgress, AppInitAssetError> +where + F: FnMut(u64, Option<u64>), +{ + if url.is_empty() { + return Err(AppInitAssetError::MissingUrl); + } + Err(AppInitAssetError::FetchUnavailable) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppInitError { Idb(RadrootsClientIdbStoreError), @@ -248,7 +370,11 @@ pub async fn app_init_backends(config: AppConfig) -> AppInitResult<AppBackends> mod tests { use super::{ app_init_backends, + app_init_progress_add, app_init_state_default, + app_init_stage_set, + app_init_total_add, + app_init_total_unknown, AppInitError, AppInitErrorMessage, AppInitStage, @@ -375,4 +501,21 @@ mod tests { assert_eq!(state.loaded_bytes, 0); assert_eq!(state.total_bytes, Some(0)); } + + #[test] + fn app_init_progress_helpers_update_state() { + let mut state = app_init_state_default(); + app_init_stage_set(&mut state, AppInitStage::Storage); + assert_eq!(state.stage, AppInitStage::Storage); + app_init_progress_add(&mut state, 0); + assert_eq!(state.loaded_bytes, 0); + app_init_progress_add(&mut state, 5); + assert_eq!(state.loaded_bytes, 5); + app_init_total_add(&mut state, 10); + assert_eq!(state.total_bytes, Some(10)); + app_init_total_unknown(&mut state); + assert_eq!(state.total_bytes, None); + app_init_total_add(&mut state, 5); + assert_eq!(state.total_bytes, None); + } } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -80,11 +80,18 @@ pub use config::{ }; pub use init::{ app_init_backends, + app_init_fetch_asset, app_init_has_completed, app_init_mark_completed, + app_init_progress_add, app_init_reset, app_init_state_default, + app_init_stage_set, + app_init_total_add, + app_init_total_unknown, AppBackends, + AppInitAssetError, + AppInitAssetProgress, AppInitError, AppInitErrorMessage, AppInitResult,