app

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

commit 1960461dbd0357404f7d84a7bce6e9949363de9b
parent beeec5f90967f30716a61ed3ecc1ecb663a60ede
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 14:59:31 +0000

app: bootstrap init types and wasm web fixes

- add app init error/result types and backend container with tests
- wire radroots-app-core dependency and enable uuid js feature for wasm
- gate sql/tangle deps and modules behind non-wasm cfg
- update web adapters for request bodies, idb, geolocation, notifications, and random bytes

Diffstat:
MCargo.lock | 1+
MCargo.toml | 2+-
Mapp/Cargo.toml | 1+
Aapp/src/init.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/lib.rs | 2++
Mcrates/core/Cargo.toml | 4+++-
Mcrates/core/src/crypto/random.rs | 4+---
Mcrates/core/src/crypto/service.rs | 2++
Mcrates/core/src/geolocation/web.rs | 14++++++++------
Mcrates/core/src/idb/keyval.rs | 2+-
Mcrates/core/src/idb/store.rs | 18+++++++++++++-----
Mcrates/core/src/idb/value.rs | 3+++
Mcrates/core/src/lib.rs | 2++
Mcrates/core/src/notifications/web.rs | 22+++++++++++++++-------
Mcrates/core/src/radroots/web.rs | 18+++++++++---------
15 files changed, 133 insertions(+), 33 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1541,6 +1541,7 @@ name = "radroots-app" version = "0.1.0" dependencies = [ "leptos", + "radroots-app-core", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml @@ -86,7 +86,7 @@ url = "2" chrono = "0.4" hex = "0.4" sha2 = "0.10" -uuid = { version = "1.8", features = ["v4", "v7"] } +uuid = { version = "1.8", features = ["v4", "v7", "js"] } regex = "1" once_cell = "1" radroots-nostr = { path = "refs/crates/nostr" } diff --git a/app/Cargo.toml b/app/Cargo.toml @@ -12,3 +12,4 @@ crate-type = ["cdylib", "rlib"] [dependencies] leptos = { workspace = true, features = ["csr"] } wasm-bindgen.workspace = true +radroots-app-core = { path = "../crates/core" } diff --git a/app/src/init.rs b/app/src/init.rs @@ -0,0 +1,71 @@ +#![forbid(unsafe_code)] + +use std::fmt; + +use radroots_app_core::datastore::{RadrootsClientDatastoreError, RadrootsClientWebDatastore}; +use radroots_app_core::idb::RadrootsClientIdbStoreError; +use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientWebKeystoreNostr}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppInitError { + Idb(RadrootsClientIdbStoreError), + Datastore(RadrootsClientDatastoreError), + Keystore(RadrootsClientKeystoreError), +} + +pub type AppInitErrorMessage = &'static str; + +impl AppInitError { + pub const fn message(&self) -> AppInitErrorMessage { + match self { + AppInitError::Idb(_) => "error.app.init.idb", + AppInitError::Datastore(_) => "error.app.init.datastore", + AppInitError::Keystore(_) => "error.app.init.keystore", + } + } +} + +impl fmt::Display for AppInitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.message()) + } +} + +impl std::error::Error for AppInitError {} + +pub struct AppBackends { + pub datastore: RadrootsClientWebDatastore, + pub nostr_keystore: RadrootsClientWebKeystoreNostr, +} + +pub type AppInitResult<T> = Result<T, AppInitError>; + +#[cfg(test)] +mod tests { + use super::{AppInitError, AppInitErrorMessage}; + use radroots_app_core::datastore::RadrootsClientDatastoreError; + use radroots_app_core::idb::RadrootsClientIdbStoreError; + use radroots_app_core::keystore::RadrootsClientKeystoreError; + + #[test] + fn app_init_error_messages_match_spec() { + let cases: &[(AppInitError, AppInitErrorMessage)] = &[ + ( + AppInitError::Idb(RadrootsClientIdbStoreError::IdbUndefined), + "error.app.init.idb", + ), + ( + AppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined), + "error.app.init.datastore", + ), + ( + AppInitError::Keystore(RadrootsClientKeystoreError::IdbUndefined), + "error.app.init.keystore", + ), + ]; + for (err, expected) in cases { + assert_eq!(err.message(), *expected); + assert_eq!(err.to_string(), *expected); + } + } +} diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -1,6 +1,8 @@ #![forbid(unsafe_code)] mod app; +mod init; mod entry; pub use app::App; +pub use init::{AppBackends, AppInitError, AppInitErrorMessage, AppInitResult}; diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml @@ -16,11 +16,13 @@ serde_json = { workspace = true } getrandom = { workspace = true } base64 = { workspace = true } radroots-nostr = { workspace = true, features = ["events"] } -rusqlite = { workspace = true, features = ["bundled", "serialize"] } url = { workspace = true } chrono = { workspace = true } hex = { workspace = true } sha2 = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rusqlite = { workspace = true, features = ["bundled", "serialize"] } radroots-sql-core = { workspace = true, features = ["native"] } radroots-tangle-db = { workspace = true } radroots-tangle-db-schema = { workspace = true } diff --git a/crates/core/src/crypto/random.rs b/crates/core/src/crypto/random.rs @@ -11,11 +11,9 @@ pub fn fill_random(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> { fn fill_random_inner(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> { let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?; let crypto = window.crypto().map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?; - let array = js_sys::Uint8Array::from(bytes); crypto - .get_random_values_with_u8_array(&array) + .get_random_values_with_u8_array(bytes) .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?; - array.copy_to(bytes); Ok(()) } diff --git a/crates/core/src/crypto/service.rs b/crates/core/src/crypto/service.rs @@ -27,6 +27,8 @@ use crate::crypto::{ use crate::crypto::random::fill_random; #[cfg(target_arch = "wasm32")] use crate::idb::{idb_get, idb_store_ensure, idb_store_exists, idb_value_as_bytes}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; use super::{ RadrootsClientCryptoDecryptOutcome, diff --git a/crates/core/src/geolocation/web.rs b/crates/core/src/geolocation/web.rs @@ -56,15 +56,16 @@ impl RadrootsClientWebGeolocation { let _ = resolve.call1(&JsValue::NULL, &position); }, ); + let reject_failure = reject.clone(); let failure = wasm_bindgen::closure::Closure::once( move |error: web_sys::PositionError| { - let _ = reject.call1(&JsValue::NULL, &error); + let _ = reject_failure.call1(&JsValue::NULL, &error); }, ); - let mut options = web_sys::PositionOptions::new(); - options.enable_high_accuracy(true); - options.timeout(10000.0); - options.maximum_age(30000.0); + let options = web_sys::PositionOptions::new(); + options.set_enable_high_accuracy(true); + options.set_timeout(10_000); + options.set_maximum_age(30_000); if geolocation .get_current_position_with_error_callback_and_options( success.as_ref().unchecked_ref(), @@ -78,7 +79,8 @@ impl RadrootsClientWebGeolocation { success.forget(); failure.forget(); }); - JsFuture::from(promise).await + let value = JsFuture::from(promise).await?; + value.dyn_into::<web_sys::Position>() } #[cfg(target_arch = "wasm32")] diff --git a/crates/core/src/idb/keyval.rs b/crates/core/src/idb/keyval.rs @@ -39,7 +39,7 @@ async fn idb_request(request: IdbRequest) -> Result<JsValue, RadrootsClientIdbSt let err = request_error .error() .map(JsValue::from) - .unwrap_or_else(|| JsValue::from_str("idb_request_failed")); + .unwrap_or_else(|_| JsValue::from_str("idb_request_failed")); let _ = reject_error.call1(&JsValue::UNDEFINED, &err); }) as Box<dyn FnMut(_)>); request.set_onerror(Some(on_error.as_ref().unchecked_ref())); diff --git a/crates/core/src/idb/store.rs b/crates/core/src/idb/store.rs @@ -4,7 +4,7 @@ use crate::idb::{RADROOTS_IDB_DATABASE, RADROOTS_IDB_STORES}; use super::RadrootsClientIdbStoreError; #[cfg(target_arch = "wasm32")] -use js_sys::{Array, Promise, Reflect}; +use js_sys::{Array, Function, Promise, Reflect}; #[cfg(target_arch = "wasm32")] use wasm_bindgen::closure::Closure; #[cfg(target_arch = "wasm32")] @@ -28,10 +28,18 @@ async fn idb_database_exists( factory: &IdbFactory, database: &str, ) -> Result<bool, RadrootsClientIdbStoreError> { - let promise = match factory.databases() { - Ok(promise) => promise, - Err(_) => return Ok(true), + let databases = Reflect::get(factory.as_ref(), &JsValue::from_str("databases")) + .ok() + .and_then(|value| value.dyn_into::<Function>().ok()); + let Some(databases) = databases else { + return Ok(true); }; + let promise = databases + .call0(factory.as_ref()) + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; + let promise: Promise = promise + .dyn_into() + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; let value = JsFuture::from(promise) .await .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; @@ -108,7 +116,7 @@ pub(crate) async fn idb_open( let err = request_error .error() .map(JsValue::from) - .unwrap_or_else(|| JsValue::from_str("idb_open_failed")); + .unwrap_or_else(|_| JsValue::from_str("idb_open_failed")); let _ = reject_error.call1(&JsValue::UNDEFINED, &err); }) as Box<dyn FnMut(_)>); request.set_onerror(Some(on_error.as_ref().unchecked_ref())); diff --git a/crates/core/src/idb/value.rs b/crates/core/src/idb/value.rs @@ -4,6 +4,9 @@ pub type RadrootsClientIdbValue = wasm_bindgen::JsValue; pub type RadrootsClientIdbValue = (); #[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; + +#[cfg(target_arch = "wasm32")] pub fn idb_value_as_bytes(value: &RadrootsClientIdbValue) -> Option<Vec<u8>> { if value.is_instance_of::<js_sys::Uint8Array>() || value.is_instance_of::<js_sys::ArrayBuffer>() diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -10,5 +10,7 @@ pub mod idb; pub mod keystore; pub mod notifications; pub mod radroots; +#[cfg(not(target_arch = "wasm32"))] pub mod sql; +#[cfg(not(target_arch = "wasm32"))] pub mod tangle; diff --git a/crates/core/src/notifications/web.rs b/crates/core/src/notifications/web.rs @@ -83,16 +83,21 @@ impl RadrootsClientWebNotifications { let reader_load = reader.clone(); let reader_error = reader.clone(); let promise = js_sys::Promise::new(&mut |resolve, reject| { + let reader_load = reader_load.clone(); + let resolve_load = resolve.clone(); + let reject_load = reject.clone(); let onload = wasm_bindgen::closure::Closure::once(move |_event: web_sys::Event| { match reader_load.result() { Ok(value) => { - let _ = resolve.call1(&JsValue::NULL, &value); + let _ = resolve_load.call1(&JsValue::NULL, &value); } Err(err) => { - let _ = reject.call1(&JsValue::NULL, &err); + let _ = reject_load.call1(&JsValue::NULL, &err); } } }); + let reader_error = reader_error.clone(); + let reject_error = reject.clone(); let onerror = wasm_bindgen::closure::Closure::once(move |_event: web_sys::Event| { let err = reader_error .error() @@ -100,7 +105,7 @@ impl RadrootsClientWebNotifications { .unwrap_or_else(|| { JsValue::from_str(RadrootsClientNotificationsError::ReadFailure.message()) }); - let _ = reject.call1(&JsValue::NULL, &err); + let _ = reject_error.call1(&JsValue::NULL, &err); }); reader.set_onload(Some(onload.as_ref().unchecked_ref())); reader.set_onerror(Some(onerror.as_ref().unchecked_ref())); @@ -137,13 +142,16 @@ impl RadrootsClientWebNotifications { input.set_accept("image/png,image/jpg"); let input_handle = input.clone(); let promise = js_sys::Promise::new(&mut |resolve, _reject| { + let input_handle = input_handle.clone(); + let input_set = input_handle.clone(); + let resolve_change = resolve.clone(); let onchange = wasm_bindgen::closure::Closure::once(move |_event: web_sys::Event| { let files = input_handle.files(); let value = files.map(JsValue::from).unwrap_or(JsValue::NULL); - let _ = resolve.call1(&JsValue::NULL, &value); + let _ = resolve_change.call1(&JsValue::NULL, &value); }); - input_handle.set_onchange(Some(onchange.as_ref().unchecked_ref())); - input_handle.click(); + input_set.set_onchange(Some(onchange.as_ref().unchecked_ref())); + input_set.click(); onchange.forget(); }); let value = JsFuture::from(promise) @@ -259,7 +267,7 @@ impl RadrootsClientNotifications for RadrootsClientWebNotifications { .as_deref() .unwrap_or(&self.config.app_name); if let Some(body) = opts.body.as_deref() { - let mut options = web_sys::NotificationOptions::new(); + let options = web_sys::NotificationOptions::new(); options.set_body(body); web_sys::Notification::new_with_options(title, &options) .map_err(|_| RadrootsClientNotificationsError::Unavailable)?; diff --git a/crates/core/src/radroots/web.rs b/crates/core/src/radroots/web.rs @@ -89,8 +89,8 @@ impl RadrootsClientWebRadroots { body: Option<Value>, ) -> RadrootsClientRadrootsResult<Option<Value>> { let window = web_sys::window().ok_or(RadrootsClientRadrootsError::RequestFailure)?; - let mut init = web_sys::RequestInit::new(); - init.method(method); + let init = web_sys::RequestInit::new(); + init.set_method(method); let header_map = web_sys::Headers::new() .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?; for (key, value) in headers { @@ -101,9 +101,9 @@ impl RadrootsClientWebRadroots { if let Some(body) = body { let body = serde_json::to_string(&body) .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?; - init.body(Some(&JsValue::from_str(&body))); + init.set_body(&JsValue::from_str(&body)); } - init.headers(&header_map); + init.set_headers(&header_map); let request = web_sys::Request::new_with_str_and_init(url, &init) .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?; let response = JsFuture::from(window.fetch_with_request(&request)) @@ -127,8 +127,8 @@ impl RadrootsClientWebRadroots { body: &[u8], ) -> RadrootsClientRadrootsResult<Option<Value>> { let window = web_sys::window().ok_or(RadrootsClientRadrootsError::RequestFailure)?; - let mut init = web_sys::RequestInit::new(); - init.method(method); + let init = web_sys::RequestInit::new(); + init.set_method(method); let header_map = web_sys::Headers::new() .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?; for (key, value) in headers { @@ -137,8 +137,8 @@ impl RadrootsClientWebRadroots { .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?; } let bytes = js_sys::Uint8Array::from(body); - init.body(Some(&bytes.into())); - init.headers(&header_map); + init.set_body(&bytes.into()); + init.set_headers(&header_map); let request = web_sys::Request::new_with_str_and_init(url, &init) .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?; let response = JsFuture::from(window.fetch_with_request(&request)) @@ -335,7 +335,7 @@ fn encode_bearer_token(value: &str) -> String { async fn parse_response( response: web_sys::Response, ) -> RadrootsClientRadrootsResult<Option<Value>> { - let json_response = response.clone().json(); + let json_response = response.json(); if let Ok(json_response) = json_response { if let Ok(value) = JsFuture::from(json_response).await { if let Ok(value) = serde_wasm_bindgen::from_value::<Value>(value) {