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:
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) {