From 8707c4fd09a09945763a8127ffb2fa2d2116faa2 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 6 Aug 2025 16:50:05 -0700 Subject: [PATCH 01/33] rearrange deps by feature flag --- Cargo.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b0c323e..d4322bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ alloy = { version = "0.8.1", features = [ anyhow = "1.0" base64 = "0.22.1" bincode = "1.3.3" -color-eyre = { version = "0.6", features = ["capture-spantrace"], optional = true } http = "1.0.0" mime_guess = "2.0" serde = { version = "1.0", features = ["derive"] } @@ -33,8 +32,10 @@ rand = "0.8" regex = "1.11.1" rmp-serde = "1.1.2" thiserror = "1.0" +url = "2.4.1" +wit-bindgen = "0.42.1" + +color-eyre = { version = "0.6", features = ["capture-spantrace"], optional = true } tracing = { version = "0.1", optional = true } tracing-error = { version = "0.2", optional = true } tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "std"], optional = true } -url = "2.4.1" -wit-bindgen = "0.42.1" From 6fdf671c896dd5e487b805e6d8061e9e961f152f Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 6 Aug 2025 16:58:58 -0700 Subject: [PATCH 02/33] hyperapp: add from https://github.com/hyperware-ai/hyperprocess-macro/commit/c0d0bb9bafeacd9033eadc4821cd070f223268b3 --- Cargo.lock | 57 ++++- Cargo.toml | 8 +- src/hyperapp.rs | 539 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 4 files changed, 600 insertions(+), 7 deletions(-) create mode 100644 src/hyperapp.rs diff --git a/Cargo.lock b/Cargo.lock index e07156b..0276415 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -1364,7 +1364,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1556,6 +1568,7 @@ dependencies = [ "base64", "bincode", "color-eyre", + "futures-util", "http", "mime_guess", "rand", @@ -1568,6 +1581,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "url", + "uuid", "wit-bindgen", ] @@ -1933,7 +1947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2332,6 +2346,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -2366,7 +2386,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -3314,6 +3334,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "valuable" version = "0.1.0" @@ -3356,6 +3385,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt 0.39.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.99" @@ -3630,7 +3668,7 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa5b79cd8cb4b27a9be3619090c03cbb87fe7b1c6de254b4c9b4477188828af8" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen-rt 0.42.1", "wit-bindgen-rust-macro", ] @@ -3645,6 +3683,15 @@ dependencies = [ "wit-parser", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "wit-bindgen-rt" version = "0.42.1" diff --git a/Cargo.toml b/Cargo.toml index d4322bc..ac6f59c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/hyperware-ai/process_lib" license = "Apache-2.0" [features] +hyperapp = ["dep:futures-util", "dep:uuid"] logging = ["dep:color-eyre", "dep:tracing", "dep:tracing-error", "dep:tracing-subscriber"] simulation-mode = [] @@ -26,15 +27,18 @@ base64 = "0.22.1" bincode = "1.3.3" http = "1.0.0" mime_guess = "2.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.120" rand = "0.8" regex = "1.11.1" rmp-serde = "1.1.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.120" thiserror = "1.0" url = "2.4.1" wit-bindgen = "0.42.1" +futures-util = { version = "0.3", optional = true } +uuid = { version = "1.0", features = ["v4"], optional = true } + color-eyre = { version = "0.6", features = ["capture-spantrace"], optional = true } tracing = { version = "0.1", optional = true } tracing-error = { version = "0.2", optional = true } diff --git a/src/hyperapp.rs b/src/hyperapp.rs new file mode 100644 index 0000000..d652db8 --- /dev/null +++ b/src/hyperapp.rs @@ -0,0 +1,539 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures_util::task::noop_waker_ref; +use hyperware_process_lib::{ + http::server::{HttpServer, IncomingHttpRequest}, + logging::info, + get_state, http, set_state, timer, BuildError, LazyLoadBlob, Message, Request, SendError, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use uuid::Uuid; + +pub mod prelude { + pub use crate::APP_CONTEXT; + pub use crate::RESPONSE_REGISTRY; + // Add other commonly used items here +} + +thread_local! { + pub static APP_CONTEXT: RefCell = RefCell::new(AppContext { + hidden_state: None, + executor: Executor::new(), + }); + + pub static RESPONSE_REGISTRY: RefCell>> = RefCell::new(HashMap::new()); + + pub static APP_HELPERS: RefCell = RefCell::new(AppHelpers { + current_server: None, + current_message: None, + current_http_context: None, + }); +} + +#[derive(Clone)] +pub struct HttpRequestContext { + pub request: IncomingHttpRequest, + pub response_headers: HashMap, +} + +pub struct AppContext { + pub hidden_state: Option, + pub executor: Executor, +} + +pub struct AppHelpers { + pub current_server: Option<*mut HttpServer>, + pub current_message: Option, + pub current_http_context: Option, +} + +// Access function for the current path +pub fn get_path() -> Option { + APP_HELPERS.with(|helpers| { + helpers.borrow().current_http_context.as_ref() + .and_then(|ctx| ctx.request.path().ok()) + }) +} + +// Access function for the current server +pub fn get_server() -> Option<&'static mut HttpServer> { + APP_HELPERS.with(|ctx| ctx.borrow().current_server.map(|ptr| unsafe { &mut *ptr })) +} + +pub fn get_http_method() -> Option { + APP_HELPERS.with(|helpers| { + helpers.borrow().current_http_context.as_ref() + .and_then(|ctx| ctx.request.method().ok()) + .map(|m| m.to_string()) + }) +} + +// Set response headers that will be included in the HTTP response +pub fn set_response_headers(headers: HashMap) { + APP_HELPERS.with(|helpers| { + if let Some(ctx) = &mut helpers.borrow_mut().current_http_context { + ctx.response_headers = headers; + } + }) +} + +// Add a single response header +pub fn add_response_header(key: String, value: String) { + APP_HELPERS.with(|helpers| { + if let Some(ctx) = &mut helpers.borrow_mut().current_http_context { + ctx.response_headers.insert(key, value); + } + }) +} + + +pub fn clear_http_request_context() { + APP_HELPERS.with(|helpers| { + helpers.borrow_mut().current_http_context = None; + }) +} + +// Access function for the source address of the current message +pub fn source() -> hyperware_process_lib::Address { + APP_HELPERS.with(|ctx| { + ctx.borrow() + .current_message + .as_ref() + .expect("No message in current context") + .source() + .clone() + }) +} + +/// Get query parameters from the current HTTP request path +/// Returns None if not in an HTTP context or no query parameters present +pub fn get_query_params() -> Option> { + get_path().map(|path| { + let mut params = HashMap::new(); + if let Some(query_start) = path.find('?') { + let query = &path[query_start + 1..]; + for pair in query.split('&') { + if let Some(eq_pos) = pair.find('=') { + let key = pair[..eq_pos].to_string(); + let value = pair[eq_pos + 1..].to_string(); + params.insert(key, value); + } + } + } + params + }) +} + +pub struct Executor { + tasks: Vec>>>, +} + +impl Executor { + pub fn new() -> Self { + Self { tasks: Vec::new() } + } + + pub fn spawn(&mut self, fut: impl Future + 'static) { + self.tasks.push(Box::pin(fut)); + } + + pub fn poll_all_tasks(&mut self) { + let mut ctx = Context::from_waker(noop_waker_ref()); + let mut completed = Vec::new(); + + for i in 0..self.tasks.len() { + if let Poll::Ready(()) = self.tasks[i].as_mut().poll(&mut ctx) { + completed.push(i); + } + } + + for idx in completed.into_iter().rev() { + let _ = self.tasks.remove(idx); + } + } +} +struct ResponseFuture { + correlation_id: String, + // Capture HTTP context at creation time + http_context: Option, +} + +impl ResponseFuture { + fn new(correlation_id: String) -> Self { + // Capture current HTTP context when future is created (at .await point) + let http_context = APP_HELPERS.with(|helpers| { + helpers.borrow().current_http_context.clone() + }); + + Self { + correlation_id, + http_context, + } + } +} + +impl Future for ResponseFuture { + type Output = Vec; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let correlation_id = &self.correlation_id; + + let maybe_bytes = RESPONSE_REGISTRY.with(|registry| { + let mut registry_mut = registry.borrow_mut(); + registry_mut.remove(correlation_id) + }); + + if let Some(bytes) = maybe_bytes { + // Restore this future's captured context + if let Some(ref context) = self.http_context { + APP_HELPERS.with(|helpers| { + helpers.borrow_mut().current_http_context = Some(context.clone()); + }); + } + + Poll::Ready(bytes) + } else { + Poll::Pending + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Error)] +pub enum AppSendError { + #[error("SendError: {0}")] + SendError(SendError), + #[error("BuildError: {0}")] + BuildError(BuildError), +} + +pub async fn sleep(sleep_ms: u64) -> Result<(), AppSendError> { + let request = Request::to(("our", "timer", "distro", "sys")) + .body(timer::TimerAction::SetTimer(sleep_ms)) + .expects_response((sleep_ms / 1_000) + 1); + + let correlation_id = Uuid::new_v4().to_string(); + if let Err(e) = request.context(correlation_id.as_bytes().to_vec()).send() { + return Err(AppSendError::BuildError(e)); + } + + let _ = ResponseFuture::new(correlation_id).await; + + return Ok(()); +} + +pub async fn send(request: Request) -> Result +where + R: serde::de::DeserializeOwned, +{ + let request = if request.timeout.is_some() { + request + } else { + request.expects_response(30) + }; + + let correlation_id = Uuid::new_v4().to_string(); + if let Err(e) = request.context(correlation_id.as_bytes().to_vec()).send() { + return Err(AppSendError::BuildError(e)); + } + + let response_bytes = ResponseFuture::new(correlation_id).await; + if let Ok(r) = serde_json::from_slice::(&response_bytes) { + return Ok(r); + } + + let e = serde_json::from_slice::(&response_bytes) + .expect("Failed to deserialize response to send()"); + return Err(AppSendError::SendError(e)); +} + +pub async fn send_rmp(request: Request) -> Result +where + R: serde::de::DeserializeOwned, +{ + let request = if request.timeout.is_some() { + request + } else { + request.expects_response(30) + }; + + let correlation_id = Uuid::new_v4().to_string(); + if let Err(e) = request.context(correlation_id.as_bytes().to_vec()).send() { + return Err(AppSendError::BuildError(e)); + } + + let response_bytes = ResponseFuture::new(correlation_id).await; + if let Ok(r) = rmp_serde::from_slice::(&response_bytes) { + return Ok(r); + } + + let e = rmp_serde::from_slice::(&response_bytes) + .expect("Failed to deserialize response to send()"); + return Err(AppSendError::SendError(e)); +} + +#[macro_export] +macro_rules! hyper { + ($($code:tt)*) => { + $crate::APP_CONTEXT.with(|ctx| { + ctx.borrow_mut().executor.spawn(async move { + $($code)* + }) + }) + }; +} + + +// Enum defining the state persistance behaviour +#[derive(Clone)] +pub enum SaveOptions { + // Never Persist State + Never, + // Persist State Every Message + EveryMessage, + // Persist State Every N Messages + EveryNMessage(u64), + // Persist State Every N Seconds + EveryNSeconds(u64), + // Persist State Only If Changed + OnDiff, +} +pub struct HiddenState { + save_config: SaveOptions, + message_count: u64, + old_state: Option>, // Stores the serialized state from before message processing +} + +impl HiddenState { + pub fn new(save_config: SaveOptions) -> Self { + Self { + save_config, + message_count: 0, + old_state: None, + } + } + + fn should_save_state(&mut self) -> bool { + match self.save_config { + SaveOptions::Never => false, + SaveOptions::EveryMessage => true, + SaveOptions::EveryNMessage(n) => { + self.message_count += 1; + if self.message_count >= n { + self.message_count = 0; + true + } else { + false + } + } + SaveOptions::EveryNSeconds(_) => false, // Handled by timer instead + SaveOptions::OnDiff => false, // Will be handled separately with state comparison + } + } +} + +// TODO: We need a timer macro again. + +/// Store a snapshot of the current state before processing a message +/// This is used for OnDiff save option to compare state before and after +/// Only stores if old_state is None (i.e., first time or after a save) +pub fn store_old_state(state: &S) +where + S: serde::Serialize, +{ + APP_CONTEXT.with(|ctx| { + let mut ctx_mut = ctx.borrow_mut(); + if let Some(ref mut hidden_state) = ctx_mut.hidden_state { + if matches!(hidden_state.save_config, SaveOptions::OnDiff) && hidden_state.old_state.is_none() { + if let Ok(s_bytes) = rmp_serde::to_vec(state) { + hidden_state.old_state = Some(s_bytes); + } + } + } + }); +} + +/// Trait that must be implemented by application state types +pub trait State { + /// Creates a new instance of the state. + fn new() -> Self; +} + +/// Initialize state from persisted storage or create new if none exists +/// TODO: Delete? +pub fn initialize_state() -> S +where + S: serde::de::DeserializeOwned + Default, +{ + match get_state() { + Some(bytes) => match rmp_serde::from_slice::(&bytes) { + Ok(state) => state, + Err(e) => { + panic!("error deserializing existing state: {e}. We're panicking because we don't want to nuke state by setting it to a new instance."); + } + }, + None => { + info!("no existing state, creating new one"); + S::default() + } + } +} + +pub fn setup_server( + ui_config: Option<&hyperware_process_lib::http::server::HttpBindingConfig>, + endpoints: &[Binding], +) -> http::server::HttpServer { + let mut server = http::server::HttpServer::new(5); + + if let Some(ui) = ui_config { + if let Err(e) = server.serve_ui("ui", vec!["/"], ui.clone()) { + panic!("failed to serve UI: {e}. Make sure that a ui folder is in /pkg"); + } + } + + // Verify no duplicate paths + let mut seen_paths = std::collections::HashSet::new(); + for endpoint in endpoints.iter() { + let path = match endpoint { + Binding::Http { path, .. } => path, + Binding::Ws { path, .. } => path, + }; + if !seen_paths.insert(path) { + panic!("duplicate path found: {}", path); + } + } + + for endpoint in endpoints { + match endpoint { + Binding::Http { path, config } => { + server + .bind_http_path(path.to_string(), config.clone()) + .expect("failed to serve API path"); + } + Binding::Ws { path, config } => { + server + .bind_ws_path(path.to_string(), config.clone()) + .expect("failed to bind WS path"); + } + } + } + + server +} + +/// Pretty prints a SendError in a more readable format +pub fn pretty_print_send_error(error: &SendError) { + let kind = &error.kind; + let target = &error.target; + + // Try to decode body as UTF-8 string, fall back to showing as bytes + let body = String::from_utf8(error.message.body().to_vec()) + .map(|s| format!("\"{}\"", s)) + .unwrap_or_else(|_| format!("{:?}", error.message.body())); + + // Try to decode context as UTF-8 string + let context = error + .context + .as_ref() + .map(|bytes| String::from_utf8_lossy(bytes).into_owned()); + + hyperware_process_lib::logging::error!( + "SendError {{ + kind: {:?}, + target: {}, + body: {}, + context: {} +}}", + kind, + target, + body, + context + .map(|s| format!("\"{}\"", s)) + .unwrap_or("None".to_string()) + ); +} + +// For demonstration, we'll define them all in one place. +// Make sure the signatures match the real function signatures you require! +pub fn no_init_fn(_state: &mut S) { + // does nothing +} + +pub fn no_ws_handler( + _state: &mut S, + _server: &mut http::server::HttpServer, + _channel_id: u32, + _msg_type: http::server::WsMessageType, + _blob: LazyLoadBlob, +) { + // does nothing +} + +pub fn no_http_api_call(_state: &mut S, _req: ()) { + // does nothing +} + +pub fn no_local_request(_msg: &Message, _state: &mut S, _req: ()) { + // does nothing +} + +pub fn no_remote_request(_msg: &Message, _state: &mut S, _req: ()) { + // does nothing +} + +#[derive(Clone, Debug)] +pub enum Binding { + Http { + path: &'static str, + config: hyperware_process_lib::http::server::HttpBindingConfig, + }, + Ws { + path: &'static str, + config: hyperware_process_lib::http::server::WsBindingConfig, + }, +} + +pub fn maybe_save_state(state: &S) +where + S: serde::Serialize, +{ + APP_CONTEXT.with(|ctx| { + let mut ctx_mut = ctx.borrow_mut(); + if let Some(ref mut hidden_state) = ctx_mut.hidden_state { + let should_save = if matches!(hidden_state.save_config, SaveOptions::OnDiff) { + // For OnDiff, compare current state with old state + if let Ok(current_bytes) = rmp_serde::to_vec(state) { + let state_changed = match &hidden_state.old_state { + Some(old_bytes) => old_bytes != ¤t_bytes, + None => true, // If no old state, consider it changed + }; + + if state_changed { + true + } else { + false + } + } else { + false + } + } else { + hidden_state.should_save_state() + }; + + if should_save { + if let Ok(s_bytes) = rmp_serde::to_vec(state) { + let _ = set_state(&s_bytes); + + // Clear old_state after saving so it can be set again on next message + if matches!(hidden_state.save_config, SaveOptions::OnDiff) { + hidden_state.old_state = None; + } + } + } + } + }); +} diff --git a/src/lib.rs b/src/lib.rs index 4134715..b0ab330 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,9 @@ pub mod vfs; /// A set of types and macros for writing "script" processes. pub mod scripting; +#[cfg(feature = "hyperapp")] +pub mod hyperapp; + mod types; pub use types::{ address::{Address, AddressParseError}, From 393aa1bf9deaaffa334f5156d00e8eaba0628c72 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 6 Aug 2025 17:17:15 -0700 Subject: [PATCH 03/33] hyperapp: fix compiler errors --- Cargo.toml | 2 +- src/hyperapp.rs | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac6f59c..0dc4fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/hyperware-ai/process_lib" license = "Apache-2.0" [features] -hyperapp = ["dep:futures-util", "dep:uuid"] +hyperapp = ["dep:futures-util", "dep:uuid", "logging"] logging = ["dep:color-eyre", "dep:tracing", "dep:tracing-error", "dep:tracing-subscriber"] simulation-mode = [] diff --git a/src/hyperapp.rs b/src/hyperapp.rs index d652db8..7b44e9c 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -5,21 +5,15 @@ use std::pin::Pin; use std::task::{Context, Poll}; use futures_util::task::noop_waker_ref; -use hyperware_process_lib::{ - http::server::{HttpServer, IncomingHttpRequest}, - logging::info, - get_state, http, set_state, timer, BuildError, LazyLoadBlob, Message, Request, SendError, +use crate::{ + http::server::{HttpBindingConfig, HttpServer, IncomingHttpRequest, WsBindingConfig}, + logging::{info, error}, + get_state, http, set_state, timer, Address, BuildError, LazyLoadBlob, Message, Request, SendError, }; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; -pub mod prelude { - pub use crate::APP_CONTEXT; - pub use crate::RESPONSE_REGISTRY; - // Add other commonly used items here -} - thread_local! { pub static APP_CONTEXT: RefCell = RefCell::new(AppContext { hidden_state: None, @@ -99,7 +93,7 @@ pub fn clear_http_request_context() { } // Access function for the source address of the current message -pub fn source() -> hyperware_process_lib::Address { +pub fn source() -> Address { APP_HELPERS.with(|ctx| { ctx.borrow() .current_message @@ -384,7 +378,7 @@ where } pub fn setup_server( - ui_config: Option<&hyperware_process_lib::http::server::HttpBindingConfig>, + ui_config: Option<&HttpBindingConfig>, endpoints: &[Binding], ) -> http::server::HttpServer { let mut server = http::server::HttpServer::new(5); @@ -441,7 +435,7 @@ pub fn pretty_print_send_error(error: &SendError) { .as_ref() .map(|bytes| String::from_utf8_lossy(bytes).into_owned()); - hyperware_process_lib::logging::error!( + error!( "SendError {{ kind: {:?}, target: {}, @@ -489,11 +483,11 @@ pub fn no_remote_request(_msg: &Message, _state: &mut S, _req: ()) { pub enum Binding { Http { path: &'static str, - config: hyperware_process_lib::http::server::HttpBindingConfig, + config: HttpBindingConfig, }, Ws { path: &'static str, - config: hyperware_process_lib::http::server::WsBindingConfig, + config: WsBindingConfig, }, } From 604edfdb6e065c37102bdd64bda0ea2979a8e042 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 00:17:59 +0000 Subject: [PATCH 04/33] Format Rust code using rustfmt --- src/hyperapp.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 7b44e9c..501af47 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -4,12 +4,13 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use futures_util::task::noop_waker_ref; use crate::{ + get_state, http, http::server::{HttpBindingConfig, HttpServer, IncomingHttpRequest, WsBindingConfig}, - logging::{info, error}, - get_state, http, set_state, timer, Address, BuildError, LazyLoadBlob, Message, Request, SendError, + logging::{error, info}, + set_state, timer, Address, BuildError, LazyLoadBlob, Message, Request, SendError, }; +use futures_util::task::noop_waker_ref; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; @@ -49,7 +50,10 @@ pub struct AppHelpers { // Access function for the current path pub fn get_path() -> Option { APP_HELPERS.with(|helpers| { - helpers.borrow().current_http_context.as_ref() + helpers + .borrow() + .current_http_context + .as_ref() .and_then(|ctx| ctx.request.path().ok()) }) } @@ -61,7 +65,10 @@ pub fn get_server() -> Option<&'static mut HttpServer> { pub fn get_http_method() -> Option { APP_HELPERS.with(|helpers| { - helpers.borrow().current_http_context.as_ref() + helpers + .borrow() + .current_http_context + .as_ref() .and_then(|ctx| ctx.request.method().ok()) .map(|m| m.to_string()) }) @@ -85,7 +92,6 @@ pub fn add_response_header(key: String, value: String) { }) } - pub fn clear_http_request_context() { APP_HELPERS.with(|helpers| { helpers.borrow_mut().current_http_context = None; @@ -160,9 +166,8 @@ struct ResponseFuture { impl ResponseFuture { fn new(correlation_id: String) -> Self { // Capture current HTTP context when future is created (at .await point) - let http_context = APP_HELPERS.with(|helpers| { - helpers.borrow().current_http_context.clone() - }); + let http_context = + APP_HELPERS.with(|helpers| helpers.borrow().current_http_context.clone()); Self { correlation_id, @@ -281,7 +286,6 @@ macro_rules! hyper { }; } - // Enum defining the state persistance behaviour #[derive(Clone)] pub enum SaveOptions { @@ -342,7 +346,9 @@ where APP_CONTEXT.with(|ctx| { let mut ctx_mut = ctx.borrow_mut(); if let Some(ref mut hidden_state) = ctx_mut.hidden_state { - if matches!(hidden_state.save_config, SaveOptions::OnDiff) && hidden_state.old_state.is_none() { + if matches!(hidden_state.save_config, SaveOptions::OnDiff) + && hidden_state.old_state.is_none() + { if let Ok(s_bytes) = rmp_serde::to_vec(state) { hidden_state.old_state = Some(s_bytes); } From 11d23b25b1fdd3121957a802ce9f938e1d0b59d7 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 8 Aug 2025 16:47:03 -0700 Subject: [PATCH 05/33] hyperapp: reexport hyper macro in hyperapp (since macro_export exports it from root) --- src/hyperapp.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 501af47..3dac2b3 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -15,6 +15,13 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +// macro_export puts it in the root, +// so we re-export here so you can use as either +// hyperware_process_lib::hyper +// or +// hyperware_process_lib::hyperapp:hyper +pub use crate::hyper; + thread_local! { pub static APP_CONTEXT: RefCell = RefCell::new(AppContext { hidden_state: None, From 4f7513ea59858d1bbf5e5a4f39906e8a6c232694 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 8 Aug 2025 16:50:33 -0700 Subject: [PATCH 06/33] hyperapp: try to fix hyper macro --- src/hyperapp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 3dac2b3..fd5dadb 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -285,7 +285,7 @@ where #[macro_export] macro_rules! hyper { ($($code:tt)*) => { - $crate::APP_CONTEXT.with(|ctx| { + hyperware_process_lib::hyperapp::APP_CONTEXT.with(|ctx| { ctx.borrow_mut().executor.spawn(async move { $($code)* }) From b07c8a06be7178443e03efeebce0915b1899c34c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 8 Aug 2025 16:54:35 -0700 Subject: [PATCH 07/33] hyperapp: rename `hyper!` to `run_async!` --- src/hyperapp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index fd5dadb..47e4658 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -283,7 +283,7 @@ where } #[macro_export] -macro_rules! hyper { +macro_rules! run_async { ($($code:tt)*) => { hyperware_process_lib::hyperapp::APP_CONTEXT.with(|ctx| { ctx.borrow_mut().executor.spawn(async move { From 108543b792f149ac45448d24bec6fb5123c3082d Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 8 Aug 2025 16:58:44 -0700 Subject: [PATCH 08/33] hyperapp: fix the run_async reexport --- src/hyperapp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 47e4658..7cd0439 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -17,10 +17,10 @@ use uuid::Uuid; // macro_export puts it in the root, // so we re-export here so you can use as either -// hyperware_process_lib::hyper +// hyperware_process_lib::run_async // or -// hyperware_process_lib::hyperapp:hyper -pub use crate::hyper; +// hyperware_process_lib::hyperapp::run_async +pub use crate::run_async; thread_local! { pub static APP_CONTEXT: RefCell = RefCell::new(AppContext { From d353cfd6b119ba609be2fed01fa34d046584cb03 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 11 Aug 2025 16:15:28 -0700 Subject: [PATCH 09/33] work towards async hyperapp versions --- src/http/client.rs | 119 +++++ src/http/server.rs | 664 +-------------------------- src/http/server/server_async.rs | 546 ++++++++++++++++++++++ src/http/server/server_sync.rs | 641 ++++++++++++++++++++++++++ src/hyperapp.rs | 46 ++ src/hypermap.rs | 267 +++++++++++ src/kv.rs | 410 +---------------- src/kv/kv_async.rs | 250 ++++++++++ src/kv/kv_sync.rs | 413 +++++++++++++++++ src/net.rs | 105 +++++ src/sign.rs | 53 +++ src/sqlite.rs | 203 +------- src/sqlite/sqlite_async.rs | 154 +++++++ src/sqlite/sqlite_sync.rs | 202 ++++++++ src/timer.rs | 11 + src/vfs/directory.rs | 10 + src/vfs/directory/directory_async.rs | 23 + src/vfs/directory_async.rs | 100 ++++ src/vfs/file.rs | 10 + src/vfs/file/file_async.rs | 23 + src/vfs/file_async.rs | 388 ++++++++++++++++ src/vfs/mod.rs | 37 ++ 22 files changed, 3426 insertions(+), 1249 deletions(-) create mode 100644 src/http/server/server_async.rs create mode 100644 src/http/server/server_sync.rs create mode 100644 src/kv/kv_async.rs create mode 100644 src/kv/kv_sync.rs create mode 100644 src/sqlite/sqlite_async.rs create mode 100644 src/sqlite/sqlite_sync.rs create mode 100644 src/vfs/directory/directory_async.rs create mode 100644 src/vfs/directory_async.rs create mode 100644 src/vfs/file/file_async.rs create mode 100644 src/vfs/file_async.rs diff --git a/src/http/client.rs b/src/http/client.rs index bf06b8c..6747d19 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -6,6 +6,9 @@ use std::collections::HashMap; use std::str::FromStr; use thiserror::Error; +#[cfg(feature = "hyperapp")] +use crate::hyperapp; + /// [`crate::Request`] type sent to the `http-client:distro:sys` service in order to open a /// WebSocket connection, send a WebSocket message on an existing connection, or /// send an HTTP request. @@ -131,6 +134,7 @@ pub fn send_request( /// Make an HTTP request using http-client and await its response. /// /// Returns HTTP response from the `http` crate if successful, with the body type as bytes. +#[cfg(not(feature = "hyperapp"))] pub fn send_request_await_response( method: Method, url: url::Url, @@ -190,6 +194,69 @@ pub fn send_request_await_response( .unwrap()) } +/// Make an HTTP request using http-client and await its response. +/// +/// Returns HTTP response from the `http` crate if successful, with the body type as bytes. +#[cfg(feature = "hyperapp")] +pub async fn send_request_await_response( + method: Method, + url: url::Url, + headers: Option>, + timeout: u64, + body: Vec, +) -> std::result::Result>, HttpClientError> { + let request = KiRequest::to(("our", "http-client", "distro", "sys")) + .body( + serde_json::to_vec(&HttpClientAction::Http(OutgoingHttpRequest { + method: method.to_string(), + version: None, + url: url.to_string(), + headers: headers.unwrap_or_default(), + })) + .map_err(|_| HttpClientError::MalformedRequest)?, + ) + .blob_bytes(body) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| HttpClientError::ExecuteRequestFailed("http-client timed out".to_string()))?; + + let resp = match serde_json::from_slice::< + std::result::Result, + >(&resp_bytes) + { + Ok(Ok(HttpClientResponse::Http(resp))) => resp, + Ok(Ok(HttpClientResponse::WebSocketAck)) => { + return Err(HttpClientError::ExecuteRequestFailed( + "http-client gave unexpected response".to_string(), + )) + } + Ok(Err(e)) => return Err(e), + Err(e) => { + return Err(HttpClientError::ExecuteRequestFailed(format!( + "http-client gave invalid response: {e:?}" + ))) + } + }; + let mut http_response = http::Response::builder() + .status(http::StatusCode::from_u16(resp.status).unwrap_or_default()); + let headers = http_response.headers_mut().unwrap(); + for (key, value) in &resp.headers { + let Ok(key) = http::header::HeaderName::from_str(key) else { + continue; + }; + let Ok(value) = http::header::HeaderValue::from_str(value) else { + continue; + }; + headers.insert(key, value); + } + Ok(http_response + .body(get_blob().unwrap_or_default().bytes) + .unwrap()) +} + +#[cfg(not(feature = "hyperapp"))] pub fn open_ws_connection( url: String, headers: Option>, @@ -231,7 +298,36 @@ pub fn send_ws_client_push(channel_id: u32, message_type: WsMessageType, blob: K .unwrap() } +#[cfg(feature = "hyperapp")] +pub async fn open_ws_connection( + url: String, + headers: Option>, + channel_id: u32, +) -> std::result::Result<(), HttpClientError> { + let request = KiRequest::to(("our", "http-client", "distro", "sys")) + .body( + serde_json::to_vec(&HttpClientAction::WebSocketOpen { + url: url.clone(), + headers: headers.unwrap_or(HashMap::new()), + channel_id, + }) + .unwrap(), + ) + .expects_response(5); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| HttpClientError::WsOpenFailed { url: url.clone() })?; + + match serde_json::from_slice(&resp_bytes) { + Ok(Ok(HttpClientResponse::WebSocketAck)) => Ok(()), + Ok(Err(e)) => Err(e), + _ => Err(HttpClientError::WsOpenFailed { url }), + } +} + /// Close a WebSocket connection. +#[cfg(not(feature = "hyperapp"))] pub fn close_ws_connection(channel_id: u32) -> std::result::Result<(), HttpClientError> { let Ok(Ok(Message::Response { body, .. })) = KiRequest::to(("our", "http-client", "distro", "sys")) @@ -251,3 +347,26 @@ pub fn close_ws_connection(channel_id: u32) -> std::result::Result<(), HttpClien _ => Err(HttpClientError::WsCloseFailed { channel_id }), } } + +/// Close a WebSocket connection. +#[cfg(feature = "hyperapp")] +pub async fn close_ws_connection(channel_id: u32) -> std::result::Result<(), HttpClientError> { + let request = KiRequest::to(("our", "http-client", "distro", "sys")) + .body( + serde_json::json!(HttpClientAction::WebSocketClose { channel_id }) + .to_string() + .as_bytes() + .to_vec(), + ) + .expects_response(5); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| HttpClientError::WsCloseFailed { channel_id })?; + + match serde_json::from_slice(&resp_bytes) { + Ok(Ok(HttpClientResponse::WebSocketAck)) => Ok(()), + Ok(Err(e)) => Err(e), + _ => Err(HttpClientError::WsCloseFailed { channel_id }), + } +} diff --git a/src/http/server.rs b/src/http/server.rs index 18bb940..8fcdc26 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -1,14 +1,15 @@ -use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; -use crate::{ - get_blob, last_blob, LazyLoadBlob as KiBlob, Message, Request as KiRequest, - Response as KiResponse, -}; +use crate::{LazyLoadBlob as KiBlob, Request as KiRequest, Response as KiResponse}; pub use http::StatusCode; use http::{HeaderMap, HeaderName, HeaderValue}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use thiserror::Error; +#[cfg(not(feature = "hyperapp"))] +mod server_sync; +#[cfg(feature = "hyperapp")] +mod server_async; + /// [`crate::Request`] received from the `http-server:distro:sys` service as a /// result of either an HTTP or WebSocket binding, created via [`HttpServerAction`]. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -296,10 +297,10 @@ pub enum MessageType { /// A representation of the HTTP server as configured by your process. #[derive(Clone, Debug)] pub struct HttpServer { - http_paths: HashMap, - ws_paths: HashMap, + pub(crate) http_paths: HashMap, + pub(crate) ws_paths: HashMap, /// A mapping of WebSocket paths to the channels that are open on them. - ws_channels: HashMap>, + pub(crate) ws_channels: HashMap>, /// The timeout given for `http-server:distro:sys` to respond to a configuration request. pub timeout: u64, } @@ -324,10 +325,10 @@ pub struct HttpServer { /// will require the user to be logged in separately to the general domain authentication. #[derive(Clone, Debug)] pub struct HttpBindingConfig { - authenticated: bool, - local_only: bool, - secure_subdomain: bool, - static_content: Option, + pub(crate) authenticated: bool, + pub(crate) local_only: bool, + pub(crate) secure_subdomain: bool, + pub(crate) static_content: Option, } impl HttpBindingConfig { @@ -399,9 +400,9 @@ impl HttpBindingConfig { /// not use the WebSocket extension protocol to connect with a runtime extension. #[derive(Clone, Copy, Debug)] pub struct WsBindingConfig { - authenticated: bool, - secure_subdomain: bool, - extension: bool, + pub(crate) authenticated: bool, + pub(crate) secure_subdomain: bool, + pub(crate) extension: bool, } impl WsBindingConfig { @@ -444,639 +445,6 @@ impl WsBindingConfig { } } -impl HttpServer { - /// Create a new HttpServer with the given timeout. - pub fn new(timeout: u64) -> Self { - Self { - http_paths: HashMap::new(), - ws_paths: HashMap::new(), - ws_channels: HashMap::new(), - timeout, - } - } - - /// Register a new path with the HTTP server configured using [`HttpBindingConfig`]. - pub fn bind_http_path( - &mut self, - path: T, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let cache = config.static_content.is_some(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")).body( - serde_json::to_vec(&if config.secure_subdomain { - HttpServerAction::SecureBind { - path: path.clone(), - cache, - } - } else { - HttpServerAction::Bind { - path: path.clone(), - authenticated: config.authenticated, - local_only: config.local_only, - cache, - } - }) - .unwrap(), - ); - let res = match config.static_content.clone() { - Some(static_content) => req - .blob(static_content) - .send_and_await_response(self.timeout), - None => req.send_and_await_response(self.timeout), - }; - let Ok(Message::Response { body, .. }) = res.unwrap() else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.insert(path, config); - } - resp - } - - /// Register a new path with the HTTP server configured using [`WsBindingConfig`]. - pub fn bind_ws_path( - &mut self, - path: T, - config: WsBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(if config.secure_subdomain { - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.clone(), - extension: config.extension, - }) - .unwrap() - } else { - serde_json::to_vec(&HttpServerAction::WebSocketBind { - path: path.clone(), - authenticated: config.authenticated, - extension: config.extension, - }) - .unwrap() - }) - .send_and_await_response(self.timeout); - let Ok(Message::Response { body, .. }) = res.unwrap() else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.ws_paths.insert(path, config); - } - resp - } - - /// Register a new path with the HTTP server, and serve a static file from it. - /// The server will respond to GET requests on this path with the given file. - pub fn bind_http_static_path( - &mut self, - path: T, - authenticated: bool, - local_only: bool, - content_type: Option, - content: Vec, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::Bind { - path: path.clone(), - authenticated, - local_only, - cache: true, - }) - .unwrap(), - ) - .blob(crate::hyperware::process::standard::LazyLoadBlob { - mime: content_type.clone(), - bytes: content.clone(), - }) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.insert( - path, - HttpBindingConfig { - authenticated, - local_only, - secure_subdomain: false, - static_content: Some(KiBlob { - mime: content_type, - bytes: content, - }), - }, - ); - } - resp - } - - /// Register a new path with the HTTP server. This will cause the HTTP server to - /// forward any requests on this path to the calling process. - /// - /// Instead of binding at just a path, this function tells the HTTP server to - /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric - /// characters converted to `-`, although will not be needed if package ID is - /// a genuine hypermap entry) and bind at that subdomain. - pub fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::SecureBind { - path: path.clone(), - cache: false, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.insert( - path, - HttpBindingConfig { - authenticated: true, - local_only: false, - secure_subdomain: true, - static_content: None, - }, - ); - } - resp - } - - /// Register a new WebSocket path with the HTTP server. Any client connections - /// made on this path will be forwarded to this process. - /// - /// Instead of binding at just a path, this function tells the HTTP server to - /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric - /// characters converted to `-`, although will not be needed if package ID is - /// a genuine hypermap entry) and bind at that subdomain. - pub fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.clone(), - extension: false, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout); - let Ok(Message::Response { body, .. }) = res.unwrap() else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.ws_paths.insert( - path, - WsBindingConfig { - authenticated: true, - secure_subdomain: true, - extension: false, - }, - ); - } - resp - } - - /// Modify a previously-bound HTTP path. - pub fn modify_http_path( - &mut self, - path: &str, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let entry = self - .http_paths - .get_mut(path) - .ok_or(HttpServerError::MalformedRequest)?; - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::Bind { - path: path.to_string(), - authenticated: config.authenticated, - local_only: config.local_only, - cache: true, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - entry.authenticated = config.authenticated; - entry.local_only = config.local_only; - entry.secure_subdomain = config.secure_subdomain; - entry.static_content = config.static_content; - } - resp - } - - /// Modify a previously-bound WS path - pub fn modify_ws_path( - &mut self, - path: &str, - config: WsBindingConfig, - ) -> Result<(), HttpServerError> { - let entry = self - .ws_paths - .get_mut(path) - .ok_or(HttpServerError::MalformedRequest)?; - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(if entry.secure_subdomain { - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.to_string(), - extension: config.extension, - }) - .unwrap() - } else { - serde_json::to_vec(&HttpServerAction::WebSocketBind { - path: path.to_string(), - authenticated: config.authenticated, - extension: config.extension, - }) - .unwrap() - }) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - entry.authenticated = config.authenticated; - entry.secure_subdomain = config.secure_subdomain; - entry.extension = config.extension; - } - resp - } - - /// Unbind a previously-bound HTTP path. - pub fn unbind_http_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.remove(&path); - } - resp - } - - /// Unbind a previously-bound WebSocket path. - pub fn unbind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::WebSocketUnbind { path: path.clone() }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.ws_paths.remove(&path); - } - resp - } - - /// Serve a file from the given directory within our package drive at the given paths. - /// - /// The directory is relative to the `pkg` folder within this package's drive. - /// - /// The config `static_content` field will be ignored in favor of the file content. - /// An error will be returned if the file does not exist. - pub fn serve_file( - &mut self, - file_path: &str, - paths: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - let our = crate::our(); - let _res = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path: format!( - "/{}/pkg/{}", - our.package_id(), - file_path.trim_start_matches('/') - ), - action: VfsAction::Read, - }) - .map_err(|_| HttpServerError::MalformedRequest)?, - ) - .send_and_await_response(self.timeout) - .unwrap(); - - let Some(mut blob) = get_blob() else { - return Err(HttpServerError::NoBlob); - }; - - let content_type = get_mime_type(&file_path); - blob.mime = Some(content_type); - - for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; - } - - Ok(()) - } - - /// Serve a file from the given absolute directory. - /// - /// The config `static_content` field will be ignored in favor of the file content. - /// An error will be returned if the file does not exist. - pub fn serve_file_raw_path( - &mut self, - file_path: &str, - paths: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - let _res = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path: file_path.to_string(), - action: VfsAction::Read, - }) - .map_err(|_| HttpServerError::MalformedRequest)?, - ) - .send_and_await_response(self.timeout) - .unwrap(); - - let Some(mut blob) = get_blob() else { - return Err(HttpServerError::NoBlob); - }; - - let content_type = get_mime_type(&file_path); - blob.mime = Some(content_type); - - for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; - } - - Ok(()) - } - - /// Helper function to traverse a UI directory and apply an operation to each file. - /// This is used by both serve_ui and unserve_ui to avoid code duplication. - fn traverse_ui_directory( - &mut self, - directory: &str, - roots: &[&str], - mut file_handler: F, - ) -> Result<(), HttpServerError> - where - F: FnMut(&mut Self, &str, &[&str], bool) -> Result<(), HttpServerError>, - { - let our = crate::our(); - let initial_path = format!("{}/pkg/{}", our.package_id(), directory); - - let mut queue = std::collections::VecDeque::new(); - queue.push_back(initial_path.clone()); - - while let Some(path) = queue.pop_front() { - let Ok(directory_response) = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path, - action: VfsAction::ReadDir, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap() - else { - return Err(HttpServerError::MalformedRequest); - }; - - let directory_body = serde_json::from_slice::(directory_response.body()) - .map_err(|_e| HttpServerError::UnexpectedResponse)?; - - // determine if it's a file or a directory and handle appropriately - let VfsResponse::ReadDir(directory_info) = directory_body else { - return Err(HttpServerError::UnexpectedResponse); - }; - - for entry in directory_info { - match entry.file_type { - FileType::Directory => { - // push the directory onto the queue - queue.push_back(entry.path); - } - FileType::File => { - let relative_path = entry.path.replace(&initial_path, ""); - let is_index = entry.path.ends_with("index.html"); - - // Call the handler with the file path and whether it's an index file - file_handler(self, &entry.path, &[relative_path.as_str()], is_index)?; - - // If it's an index file, also handle the root paths - if is_index { - for root in roots { - file_handler(self, &entry.path, &[root], true)?; - } - } - } - _ => { - // ignore symlinks and other - } - } - } - } - - Ok(()) - } - - /// Serve static files from a given directory by binding all of them - /// in http-server to their filesystem path. - /// - /// The directory is relative to the `pkg` folder within this package's drive. - /// - /// The config `static_content` field will be ignored in favor of the files' contents. - /// An error will be returned if the file does not exist. - pub fn serve_ui( - &mut self, - directory: &str, - roots: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - self.traverse_ui_directory(directory, &roots, |server, file_path, paths, _is_index| { - server.serve_file_raw_path(file_path, paths.to_vec(), config.clone()) - }) - } - - /// Unserve static files from a given directory by unbinding all of them - /// from http-server that were previously bound by serve_ui. - /// - /// The directory is relative to the `pkg` folder within this package's drive. - /// - /// This mirrors the logic of serve_ui but calls unbind_http_path instead. - pub fn unserve_ui(&mut self, directory: &str, roots: Vec<&str>) -> Result<(), HttpServerError> { - self.traverse_ui_directory(directory, &roots, |server, _file_path, paths, _is_index| { - // Unbind each path that was bound - for path in paths { - server.unbind_http_path(*path)?; - } - Ok(()) - }) - } - - /// Handle a WebSocket open event from the HTTP server. - pub fn handle_websocket_open(&mut self, path: &str, channel_id: u32) { - self.ws_channels - .entry(path.to_string()) - .or_insert(HashSet::new()) - .insert(channel_id); - } - - /// Handle a WebSocket close event from the HTTP server. - pub fn handle_websocket_close(&mut self, channel_id: u32) { - self.ws_channels.iter_mut().for_each(|(_, channels)| { - channels.remove(&channel_id); - }); - } - - pub fn parse_request(&self, body: &[u8]) -> Result { - let request = serde_json::from_slice::(body) - .map_err(|_| HttpServerError::MalformedRequest)?; - Ok(request) - } - - /// Handle an incoming request from the HTTP server. - pub fn handle_request( - &mut self, - server_request: HttpServerRequest, - mut http_handler: impl FnMut(IncomingHttpRequest) -> (HttpResponse, Option), - mut ws_handler: impl FnMut(u32, WsMessageType, KiBlob), - ) { - match server_request { - HttpServerRequest::Http(http_request) => { - let (response, blob) = http_handler(http_request); - let response = KiResponse::new().body(serde_json::to_vec(&response).unwrap()); - if let Some(blob) = blob { - response.blob(blob).send().unwrap(); - } else { - response.send().unwrap(); - } - } - HttpServerRequest::WebSocketPush { - channel_id, - message_type, - } => ws_handler(channel_id, message_type, last_blob().unwrap_or_default()), - HttpServerRequest::WebSocketOpen { path, channel_id } => { - self.handle_websocket_open(&path, channel_id); - } - HttpServerRequest::WebSocketClose(channel_id) => { - self.handle_websocket_close(channel_id); - } - } - } - - /// Push a WebSocket message to all channels on a given path. - pub fn ws_push_all_channels(&self, path: &str, message_type: WsMessageType, blob: KiBlob) { - ws_push_all_channels(&self.ws_channels, path, message_type, blob); - } - - pub fn get_ws_channels(&self) -> HashMap> { - self.ws_channels.clone() - } - - /// Register multiple paths with the HTTP server using the same configuration. - /// The security setting is determined by the `secure_subdomain` field in `HttpBindingConfig`. - /// All paths must be bound successfully, or none will be bound. If any path - /// fails to bind, all previously bound paths will be unbound before returning - /// the error. - pub fn bind_multiple_http_paths>( - &mut self, - paths: Vec, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - let mut bound_paths = Vec::new(); - - for path in paths { - let path_str = path.into(); - let result = match config.secure_subdomain { - true => self.secure_bind_http_path(path_str.clone()), - false => self.bind_http_path(path_str.clone(), config.clone()), - }; - - match result { - // If binding succeeds, add the path to the list of bound paths - Ok(_) => bound_paths.push(path_str), - // If binding fails, unbind all previously bound paths - Err(e) => { - for bound_path in bound_paths { - let _ = self.unbind_http_path(&bound_path); - } - return Err(e); - } - } - } - - Ok(()) - } -} /// Send an HTTP response to an incoming HTTP request ([`HttpServerRequest::Http`]). pub fn send_response(status: StatusCode, headers: Option>, body: Vec) { diff --git a/src/http/server/server_async.rs b/src/http/server/server_async.rs new file mode 100644 index 0000000..9441ba9 --- /dev/null +++ b/src/http/server/server_async.rs @@ -0,0 +1,546 @@ +use crate::{ + http::server::{ + HttpBindingConfig, HttpServer, HttpServerAction, HttpServerError, + WsBindingConfig, + }, + hyperapp, LazyLoadBlob as KiBlob, Request as KiRequest, +}; +use std::collections::{HashMap, HashSet}; + +impl HttpServer { + pub fn new(timeout: u64) -> Self { + Self { + http_paths: HashMap::new(), + ws_paths: HashMap::new(), + ws_channels: HashMap::new(), + timeout, + } + } + + pub async fn bind_http_path( + &mut self, + path: T, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let cache = config.static_content.is_some(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&if config.secure_subdomain { + HttpServerAction::SecureBind { + path: path.clone(), + cache, + } + } else { + HttpServerAction::Bind { + path: path.clone(), + authenticated: config.authenticated, + local_only: config.local_only, + cache, + } + }) + .unwrap(), + ) + .expects_response(self.timeout); + + let req = match config.static_content.clone() { + Some(static_content) => req.blob(static_content), + None => req, + }; + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.http_paths.insert(path, config); + } + resp + } + + pub async fn bind_ws_path( + &mut self, + path: T, + config: WsBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(if config.secure_subdomain { + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.clone(), + extension: config.extension, + }) + .unwrap() + } else { + serde_json::to_vec(&HttpServerAction::WebSocketBind { + path: path.clone(), + authenticated: config.authenticated, + extension: config.extension, + }) + .unwrap() + }) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.ws_paths.insert(path, config); + } + resp + } + + pub async fn bind_http_static_path( + &mut self, + path: T, + authenticated: bool, + local_only: bool, + content_type: Option, + content: Vec, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::Bind { + path: path.clone(), + authenticated, + local_only, + cache: true, + }) + .unwrap(), + ) + .blob(crate::hyperware::process::standard::LazyLoadBlob { + mime: content_type.clone(), + bytes: content.clone(), + }) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.http_paths.insert( + path, + HttpBindingConfig { + authenticated, + local_only, + secure_subdomain: false, + static_content: Some(KiBlob { + mime: content_type, + bytes: content, + }), + }, + ); + } + resp + } + + pub async fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::SecureBind { + path: path.clone(), + cache: false, + }) + .unwrap(), + ) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.http_paths.insert( + path, + HttpBindingConfig { + authenticated: true, + local_only: false, + secure_subdomain: true, + static_content: None, + }, + ); + } + resp + } + + pub async fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.clone(), + extension: false, + }) + .unwrap(), + ) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.ws_paths.insert( + path, + WsBindingConfig { + authenticated: true, + secure_subdomain: true, + extension: false, + }, + ); + } + resp + } + + pub async fn modify_http_path( + &mut self, + path: &str, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let entry = self + .http_paths + .get_mut(path) + .ok_or(HttpServerError::MalformedRequest)?; + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::Bind { + path: path.to_string(), + authenticated: config.authenticated, + local_only: config.local_only, + cache: true, + }) + .unwrap(), + ) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + entry.authenticated = config.authenticated; + entry.local_only = config.local_only; + entry.secure_subdomain = config.secure_subdomain; + entry.static_content = config.static_content; + } + resp + } + + pub async fn modify_ws_path( + &mut self, + path: &str, + config: WsBindingConfig, + ) -> Result<(), HttpServerError> { + let entry = self + .ws_paths + .get_mut(path) + .ok_or(HttpServerError::MalformedRequest)?; + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(if entry.secure_subdomain { + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.to_string(), + extension: config.extension, + }) + .unwrap() + } else { + serde_json::to_vec(&HttpServerAction::WebSocketBind { + path: path.to_string(), + authenticated: config.authenticated, + extension: config.extension, + }) + .unwrap() + }) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + entry.authenticated = config.authenticated; + entry.secure_subdomain = config.secure_subdomain; + entry.extension = config.extension; + } + resp + } + + pub async fn unbind_http_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.http_paths.remove(&path); + } + resp + } + + pub async fn unbind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::WebSocketUnbind { path: path.clone() }) + .unwrap(), + ) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = serde_json::from_slice::>(&resp_bytes) + .map_err(|_| HttpServerError::UnexpectedResponse)?; + + if resp.is_ok() { + self.ws_paths.remove(&path); + } + resp + } + + pub async fn serve_file( + &mut self, + file_path: &str, + paths: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + use crate::vfs::{VfsAction, VfsRequest}; + use crate::get_blob; + + let our = crate::our(); + let req = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path: format!( + "/{}/pkg/{}", + our.package_id(), + file_path.trim_start_matches('/') + ), + action: VfsAction::Read, + }) + .map_err(|_| HttpServerError::MalformedRequest)?, + ) + .expects_response(self.timeout); + + let _res = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + + let Some(mut blob) = get_blob() else { + return Err(HttpServerError::NoBlob); + }; + + let content_type = get_mime_type(&file_path); + blob.mime = Some(content_type); + + for path in paths { + self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))).await?; + } + + Ok(()) + } + + pub async fn serve_file_raw_path( + &mut self, + file_path: &str, + paths: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + use crate::vfs::{VfsAction, VfsRequest}; + use crate::get_blob; + + let req = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path: file_path.to_string(), + action: VfsAction::Read, + }) + .map_err(|_| HttpServerError::MalformedRequest)?, + ) + .expects_response(self.timeout); + + let _res = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + + let Some(mut blob) = get_blob() else { + return Err(HttpServerError::NoBlob); + }; + + let content_type = get_mime_type(&file_path); + blob.mime = Some(content_type); + + for path in paths { + self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))).await?; + } + + Ok(()) + } + + + pub async fn serve_ui( + &mut self, + directory: &str, + roots: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; + + let our = crate::our(); + let initial_path = format!("{}/pkg/{}", our.package_id(), directory); + + let mut queue = std::collections::VecDeque::new(); + queue.push_back(initial_path.clone()); + + while let Some(path) = queue.pop_front() { + let req = crate::Request::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path, + action: VfsAction::ReadDir, + }) + .unwrap(), + ) + .expects_response(self.timeout); + + let directory_response = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let directory_body = serde_json::from_slice::(&directory_response) + .map_err(|_e| HttpServerError::UnexpectedResponse)?; + + let VfsResponse::ReadDir(directory_info) = directory_body else { + return Err(HttpServerError::UnexpectedResponse); + }; + + for entry in directory_info { + match entry.file_type { + FileType::Directory => { + queue.push_back(entry.path); + } + FileType::File => { + let relative_path = entry.path.replace(&initial_path, ""); + let is_index = entry.path.ends_with("index.html"); + + self.serve_file_raw_path(&entry.path, vec![relative_path.as_str()], config.clone()).await?; + + if is_index { + for root in &roots { + self.serve_file_raw_path(&entry.path, vec![root], config.clone()).await?; + } + } + } + _ => {} + } + } + } + + Ok(()) + } + + pub async fn unserve_ui(&mut self, directory: &str, roots: Vec<&str>) -> Result<(), HttpServerError> { + use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; + + let our = crate::our(); + let initial_path = format!("{}/pkg/{}", our.package_id(), directory); + + let mut queue = std::collections::VecDeque::new(); + queue.push_back(initial_path.clone()); + + while let Some(path) = queue.pop_front() { + let req = crate::Request::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path, + action: VfsAction::ReadDir, + }) + .unwrap(), + ) + .expects_response(self.timeout); + + let directory_response = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let directory_body = serde_json::from_slice::(&directory_response) + .map_err(|_e| HttpServerError::UnexpectedResponse)?; + + let VfsResponse::ReadDir(directory_info) = directory_body else { + return Err(HttpServerError::UnexpectedResponse); + }; + + for entry in directory_info { + match entry.file_type { + FileType::Directory => { + queue.push_back(entry.path); + } + FileType::File => { + let relative_path = entry.path.replace(&initial_path, ""); + let is_index = entry.path.ends_with("index.html"); + + self.unbind_http_path(relative_path.as_str()).await?; + + if is_index { + for root in &roots { + self.unbind_http_path(*root).await?; + } + } + } + _ => {} + } + } + } + + Ok(()) + } + + pub fn handle_websocket_open(&mut self, path: &str, channel_id: u32) { + self.ws_channels + .entry(path.to_string()) + .or_insert(HashSet::new()) + .insert(channel_id); + } + + pub fn handle_websocket_close(&mut self, channel_id: u32) { + self.ws_channels.iter_mut().for_each(|(_, channels)| { + channels.remove(&channel_id); + }); + } +} + +fn get_mime_type(path: &str) -> String { + let ext = path.split('.').last().unwrap_or(""); + match ext { + "html" => "text/html", + "css" => "text/css", + "js" => "application/javascript", + "json" => "application/json", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "svg" => "image/svg+xml", + "ico" => "image/x-icon", + "wasm" => "application/wasm", + _ => "application/octet-stream", + }.to_string() +} \ No newline at end of file diff --git a/src/http/server/server_sync.rs b/src/http/server/server_sync.rs new file mode 100644 index 0000000..61ce9ac --- /dev/null +++ b/src/http/server/server_sync.rs @@ -0,0 +1,641 @@ +use crate::{ + get_blob, + http::server::{HttpBindingConfig, HttpResponse, HttpServer, HttpServerAction, HttpServerError, HttpServerRequest, IncomingHttpRequest, WsBindingConfig, WsMessageType, get_mime_type, ws_push_all_channels}, + last_blob, LazyLoadBlob as KiBlob, Message, Request as KiRequest, Response as KiResponse, + vfs::{FileType, VfsAction, VfsRequest, VfsResponse}, +}; +use std::collections::{HashMap, HashSet}; + +impl HttpServer { + /// Create a new HttpServer with the given timeout. + pub fn new(timeout: u64) -> Self { + Self { + http_paths: HashMap::new(), + ws_paths: HashMap::new(), + ws_channels: HashMap::new(), + timeout, + } + } + + /// Register a new path with the HTTP server configured using [`HttpBindingConfig`]. + pub fn bind_http_path( + &mut self, + path: T, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let cache = config.static_content.is_some(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")).body( + serde_json::to_vec(&if config.secure_subdomain { + HttpServerAction::SecureBind { + path: path.clone(), + cache, + } + } else { + HttpServerAction::Bind { + path: path.clone(), + authenticated: config.authenticated, + local_only: config.local_only, + cache, + } + }) + .unwrap(), + ); + let res = match config.static_content.clone() { + Some(static_content) => req + .blob(static_content) + .send_and_await_response(self.timeout), + None => req.send_and_await_response(self.timeout), + }; + let Ok(Message::Response { body, .. }) = res.unwrap() else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.insert(path, config); + } + resp + } + + /// Register a new path with the HTTP server configured using [`WsBindingConfig`]. + pub fn bind_ws_path( + &mut self, + path: T, + config: WsBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(if config.secure_subdomain { + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.clone(), + extension: config.extension, + }) + .unwrap() + } else { + serde_json::to_vec(&HttpServerAction::WebSocketBind { + path: path.clone(), + authenticated: config.authenticated, + extension: config.extension, + }) + .unwrap() + }) + .send_and_await_response(self.timeout); + let Ok(Message::Response { body, .. }) = res.unwrap() else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.ws_paths.insert(path, config); + } + resp + } + + /// Register a new path with the HTTP server, and serve a static file from it. + /// The server will respond to GET requests on this path with the given file. + pub fn bind_http_static_path( + &mut self, + path: T, + authenticated: bool, + local_only: bool, + content_type: Option, + content: Vec, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::Bind { + path: path.clone(), + authenticated, + local_only, + cache: true, + }) + .unwrap(), + ) + .blob(crate::hyperware::process::standard::LazyLoadBlob { + mime: content_type.clone(), + bytes: content.clone(), + }) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.insert( + path, + HttpBindingConfig { + authenticated, + local_only, + secure_subdomain: false, + static_content: Some(KiBlob { + mime: content_type, + bytes: content, + }), + }, + ); + } + resp + } + + /// Register a new path with the HTTP server. This will cause the HTTP server to + /// forward any requests on this path to the calling process. + /// + /// Instead of binding at just a path, this function tells the HTTP server to + /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric + /// characters converted to `-`, although will not be needed if package ID is + /// a genuine hypermap entry) and bind at that subdomain. + pub fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::SecureBind { + path: path.clone(), + cache: false, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.insert( + path, + HttpBindingConfig { + authenticated: true, + local_only: false, + secure_subdomain: true, + static_content: None, + }, + ); + } + resp + } + + /// Register a new WebSocket path with the HTTP server. Any client connections + /// made on this path will be forwarded to this process. + /// + /// Instead of binding at just a path, this function tells the HTTP server to + /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric + /// characters converted to `-`, although will not be needed if package ID is + /// a genuine hypermap entry) and bind at that subdomain. + pub fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.clone(), + extension: false, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout); + let Ok(Message::Response { body, .. }) = res.unwrap() else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.ws_paths.insert( + path, + WsBindingConfig { + authenticated: true, + secure_subdomain: true, + extension: false, + }, + ); + } + resp + } + + /// Modify a previously-bound HTTP path. + pub fn modify_http_path( + &mut self, + path: &str, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let entry = self + .http_paths + .get_mut(path) + .ok_or(HttpServerError::MalformedRequest)?; + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::Bind { + path: path.to_string(), + authenticated: config.authenticated, + local_only: config.local_only, + cache: true, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + entry.authenticated = config.authenticated; + entry.local_only = config.local_only; + entry.secure_subdomain = config.secure_subdomain; + entry.static_content = config.static_content; + } + resp + } + + /// Modify a previously-bound WS path + pub fn modify_ws_path( + &mut self, + path: &str, + config: WsBindingConfig, + ) -> Result<(), HttpServerError> { + let entry = self + .ws_paths + .get_mut(path) + .ok_or(HttpServerError::MalformedRequest)?; + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(if entry.secure_subdomain { + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.to_string(), + extension: config.extension, + }) + .unwrap() + } else { + serde_json::to_vec(&HttpServerAction::WebSocketBind { + path: path.to_string(), + authenticated: config.authenticated, + extension: config.extension, + }) + .unwrap() + }) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + entry.authenticated = config.authenticated; + entry.secure_subdomain = config.secure_subdomain; + entry.extension = config.extension; + } + resp + } + + /// Unbind a previously-bound HTTP path. + pub fn unbind_http_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.remove(&path); + } + resp + } + + /// Unbind a previously-bound WebSocket path. + pub fn unbind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::WebSocketUnbind { path: path.clone() }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.ws_paths.remove(&path); + } + resp + } + + /// Serve a file from the given directory within our package drive at the given paths. + /// + /// The directory is relative to the `pkg` folder within this package's drive. + /// + /// The config `static_content` field will be ignored in favor of the file content. + /// An error will be returned if the file does not exist. + pub fn serve_file( + &mut self, + file_path: &str, + paths: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + let our = crate::our(); + let _res = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path: format!( + "/{}/pkg/{}", + our.package_id(), + file_path.trim_start_matches('/') + ), + action: VfsAction::Read, + }) + .map_err(|_| HttpServerError::MalformedRequest)?, + ) + .send_and_await_response(self.timeout) + .unwrap(); + + let Some(mut blob) = get_blob() else { + return Err(HttpServerError::NoBlob); + }; + + let content_type = get_mime_type(&file_path); + blob.mime = Some(content_type); + + for path in paths { + self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; + } + + Ok(()) + } + + /// Serve a file from the given absolute directory. + /// + /// The config `static_content` field will be ignored in favor of the file content. + /// An error will be returned if the file does not exist. + pub fn serve_file_raw_path( + &mut self, + file_path: &str, + paths: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + let _res = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path: file_path.to_string(), + action: VfsAction::Read, + }) + .map_err(|_| HttpServerError::MalformedRequest)?, + ) + .send_and_await_response(self.timeout) + .unwrap(); + + let Some(mut blob) = get_blob() else { + return Err(HttpServerError::NoBlob); + }; + + let content_type = get_mime_type(&file_path); + blob.mime = Some(content_type); + + for path in paths { + self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; + } + + Ok(()) + } + + /// Helper function to traverse a UI directory and apply an operation to each file. + /// This is used by both serve_ui and unserve_ui to avoid code duplication. + fn traverse_ui_directory( + &mut self, + directory: &str, + roots: &[&str], + mut file_handler: F, + ) -> Result<(), HttpServerError> + where + F: FnMut(&mut Self, &str, &[&str], bool) -> Result<(), HttpServerError>, + { + let our = crate::our(); + let initial_path = format!("{}/pkg/{}", our.package_id(), directory); + + let mut queue = std::collections::VecDeque::new(); + queue.push_back(initial_path.clone()); + + while let Some(path) = queue.pop_front() { + let Ok(directory_response) = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path, + action: VfsAction::ReadDir, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap() + else { + return Err(HttpServerError::MalformedRequest); + }; + + let directory_body = serde_json::from_slice::(directory_response.body()) + .map_err(|_e| HttpServerError::UnexpectedResponse)?; + + // determine if it's a file or a directory and handle appropriately + let VfsResponse::ReadDir(directory_info) = directory_body else { + return Err(HttpServerError::UnexpectedResponse); + }; + + for entry in directory_info { + match entry.file_type { + FileType::Directory => { + // push the directory onto the queue + queue.push_back(entry.path); + } + FileType::File => { + let relative_path = entry.path.replace(&initial_path, ""); + let is_index = entry.path.ends_with("index.html"); + + // Call the handler with the file path and whether it's an index file + file_handler(self, &entry.path, &[relative_path.as_str()], is_index)?; + + // If it's an index file, also handle the root paths + if is_index { + for root in roots { + file_handler(self, &entry.path, &[root], true)?; + } + } + } + _ => { + // ignore symlinks and other + } + } + } + } + + Ok(()) + } + + /// Serve static files from a given directory by binding all of them + /// in http-server to their filesystem path. + /// + /// The directory is relative to the `pkg` folder within this package's drive. + /// + /// The config `static_content` field will be ignored in favor of the files' contents. + /// An error will be returned if the file does not exist. + pub fn serve_ui( + &mut self, + directory: &str, + roots: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + self.traverse_ui_directory(directory, &roots, |server, file_path, paths, _is_index| { + server.serve_file_raw_path(file_path, paths.to_vec(), config.clone()) + }) + } + + /// Unserve static files from a given directory by unbinding all of them + /// from http-server that were previously bound by serve_ui. + /// + /// The directory is relative to the `pkg` folder within this package's drive. + /// + /// This mirrors the logic of serve_ui but calls unbind_http_path instead. + pub fn unserve_ui(&mut self, directory: &str, roots: Vec<&str>) -> Result<(), HttpServerError> { + self.traverse_ui_directory(directory, &roots, |server, _file_path, paths, _is_index| { + // Unbind each path that was bound + for path in paths { + server.unbind_http_path(*path)?; + } + Ok(()) + }) + } + + /// Handle a WebSocket open event from the HTTP server. + pub fn handle_websocket_open(&mut self, path: &str, channel_id: u32) { + self.ws_channels + .entry(path.to_string()) + .or_insert(HashSet::new()) + .insert(channel_id); + } + + /// Handle a WebSocket close event from the HTTP server. + pub fn handle_websocket_close(&mut self, channel_id: u32) { + self.ws_channels.iter_mut().for_each(|(_, channels)| { + channels.remove(&channel_id); + }); + } + + pub fn parse_request(&self, body: &[u8]) -> Result { + let request = serde_json::from_slice::(body) + .map_err(|_| HttpServerError::MalformedRequest)?; + Ok(request) + } + + /// Handle an incoming request from the HTTP server. + pub fn handle_request( + &mut self, + server_request: HttpServerRequest, + mut http_handler: impl FnMut(IncomingHttpRequest) -> (HttpResponse, Option), + mut ws_handler: impl FnMut(u32, WsMessageType, KiBlob), + ) { + match server_request { + HttpServerRequest::Http(http_request) => { + let (response, blob) = http_handler(http_request); + let response = KiResponse::new().body(serde_json::to_vec(&response).unwrap()); + if let Some(blob) = blob { + response.blob(blob).send().unwrap(); + } else { + response.send().unwrap(); + } + } + HttpServerRequest::WebSocketPush { + channel_id, + message_type, + } => ws_handler(channel_id, message_type, last_blob().unwrap_or_default()), + HttpServerRequest::WebSocketOpen { path, channel_id } => { + self.handle_websocket_open(&path, channel_id); + } + HttpServerRequest::WebSocketClose(channel_id) => { + self.handle_websocket_close(channel_id); + } + } + } + + /// Push a WebSocket message to all channels on a given path. + pub fn ws_push_all_channels(&self, path: &str, message_type: WsMessageType, blob: KiBlob) { + ws_push_all_channels(&self.ws_channels, path, message_type, blob); + } + + pub fn get_ws_channels(&self) -> HashMap> { + self.ws_channels.clone() + } + + /// Register multiple paths with the HTTP server using the same configuration. + /// The security setting is determined by the `secure_subdomain` field in `HttpBindingConfig`. + /// All paths must be bound successfully, or none will be bound. If any path + /// fails to bind, all previously bound paths will be unbound before returning + /// the error. + pub fn bind_multiple_http_paths>( + &mut self, + paths: Vec, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + let mut bound_paths = Vec::new(); + + for path in paths { + let path_str = path.into(); + let result = match config.secure_subdomain { + true => self.secure_bind_http_path(path_str.clone()), + false => self.bind_http_path(path_str.clone(), config.clone()), + }; + + match result { + // If binding succeeds, add the path to the list of bound paths + Ok(_) => bound_paths.push(path_str), + // If binding fails, unbind all previously bound paths + Err(e) => { + for bound_path in bound_paths { + let _ = self.unbind_http_path(&bound_path); + } + return Err(e); + } + } + } + + Ok(()) + } +} \ No newline at end of file diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 7cd0439..15a4328 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -390,6 +390,7 @@ where } } +#[cfg(not(feature = "hyperapp"))] pub fn setup_server( ui_config: Option<&HttpBindingConfig>, endpoints: &[Binding], @@ -432,6 +433,51 @@ pub fn setup_server( server } +#[cfg(feature = "hyperapp")] +pub async fn setup_server( + ui_config: Option<&HttpBindingConfig>, + endpoints: &[Binding], +) -> http::server::HttpServer { + let mut server = http::server::HttpServer::new(5); + + if let Some(ui) = ui_config { + if let Err(e) = server.serve_ui("ui", vec!["/"], ui.clone()).await { + panic!("failed to serve UI: {e}. Make sure that a ui folder is in /pkg"); + } + } + + // Verify no duplicate paths + let mut seen_paths = std::collections::HashSet::new(); + for endpoint in endpoints.iter() { + let path = match endpoint { + Binding::Http { path, .. } => path, + Binding::Ws { path, .. } => path, + }; + if !seen_paths.insert(path) { + panic!("duplicate path found: {}", path); + } + } + + for endpoint in endpoints { + match endpoint { + Binding::Http { path, config } => { + server + .bind_http_path(path.to_string(), config.clone()) + .await + .expect("failed to serve API path"); + } + Binding::Ws { path, config } => { + server + .bind_ws_path(path.to_string(), config.clone()) + .await + .expect("failed to bind WS path"); + } + } + } + + server +} + /// Pretty prints a SendError in a more readable format pub fn pretty_print_send_error(error: &SendError) { let kind = &error.kind; diff --git a/src/hypermap.rs b/src/hypermap.rs index 5e7b0af..be920e7 100644 --- a/src/hypermap.rs +++ b/src/hypermap.rs @@ -372,6 +372,7 @@ pub fn namehash(name: &str) -> String { /// Decode a mint log from the hypermap into a 'resolved' format. /// /// Uses [`valid_name()`] to check if the name is valid. +#[cfg(not(feature = "hyperapp"))] pub fn decode_mint_log(log: &crate::eth::Log) -> Result { let contract::Note::SIGNATURE_HASH = log.topics()[0] else { return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); @@ -388,9 +389,30 @@ pub fn decode_mint_log(log: &crate::eth::Log) -> Result { } } +/// Decode a mint log from the hypermap into a 'resolved' format. +/// +/// Uses [`valid_name()`] to check if the name is valid. +#[cfg(feature = "hyperapp")] +pub async fn decode_mint_log(log: &crate::eth::Log) -> Result { + let contract::Note::SIGNATURE_HASH = log.topics()[0] else { + return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); + }; + let decoded = contract::Mint::decode_log_data(log.data(), true) + .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; + let name = String::from_utf8_lossy(&decoded.label).to_string(); + if !valid_name(&name) { + return Err(DecodeLogError::InvalidName(name)); + } + match resolve_parent(log, None).await { + Some(parent_path) => Ok(Mint { name, parent_path }), + None => Err(DecodeLogError::UnresolvedParent(name)), + } +} + /// Decode a note log from the hypermap into a 'resolved' format. /// /// Uses [`valid_name()`] to check if the name is valid. +#[cfg(not(feature = "hyperapp"))] pub fn decode_note_log(log: &crate::eth::Log) -> Result { let contract::Note::SIGNATURE_HASH = log.topics()[0] else { return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); @@ -411,6 +433,31 @@ pub fn decode_note_log(log: &crate::eth::Log) -> Result { } } +/// Decode a note log from the hypermap into a 'resolved' format. +/// +/// Uses [`valid_name()`] to check if the name is valid. +#[cfg(feature = "hyperapp")] +pub async fn decode_note_log(log: &crate::eth::Log) -> Result { + let contract::Note::SIGNATURE_HASH = log.topics()[0] else { + return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); + }; + let decoded = contract::Note::decode_log_data(log.data(), true) + .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; + let note = String::from_utf8_lossy(&decoded.label).to_string(); + if !valid_note(¬e) { + return Err(DecodeLogError::InvalidName(note)); + } + match resolve_parent(log, None).await { + Some(parent_path) => Ok(Note { + note, + parent_path, + data: decoded.data, + }), + None => Err(DecodeLogError::UnresolvedParent(note)), + } +} + +#[cfg(not(feature = "hyperapp"))] pub fn decode_fact_log(log: &crate::eth::Log) -> Result { let contract::Fact::SIGNATURE_HASH = log.topics()[0] else { return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); @@ -431,17 +478,48 @@ pub fn decode_fact_log(log: &crate::eth::Log) -> Result { } } +#[cfg(feature = "hyperapp")] +pub async fn decode_fact_log(log: &crate::eth::Log) -> Result { + let contract::Fact::SIGNATURE_HASH = log.topics()[0] else { + return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); + }; + let decoded = contract::Fact::decode_log_data(log.data(), true) + .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; + let fact = String::from_utf8_lossy(&decoded.label).to_string(); + if !valid_fact(&fact) { + return Err(DecodeLogError::InvalidName(fact)); + } + match resolve_parent(log, None).await { + Some(parent_path) => Ok(Fact { + fact, + parent_path, + data: decoded.data, + }), + None => Err(DecodeLogError::UnresolvedParent(fact)), + } +} + /// Given a [`crate::eth::Log`] (which must be a log from hypermap), resolve the parent name /// of the new entry or note. +#[cfg(not(feature = "hyperapp"))] pub fn resolve_parent(log: &crate::eth::Log, timeout: Option) -> Option { let parent_hash = log.topics()[1].to_string(); net::get_name(&parent_hash, log.block_number, timeout) } +/// Given a [`crate::eth::Log`] (which must be a log from hypermap), resolve the parent name +/// of the new entry or note. +#[cfg(feature = "hyperapp")] +pub async fn resolve_parent(log: &crate::eth::Log, timeout: Option) -> Option { + let parent_hash = log.topics()[1].to_string(); + net::get_name(&parent_hash, log.block_number, timeout).await +} + /// Given a [`crate::eth::Log`] (which must be a log from hypermap), resolve the full name /// of the new entry or note. /// /// Uses [`valid_name()`] to check if the name is valid. +#[cfg(not(feature = "hyperapp"))] pub fn resolve_full_name(log: &crate::eth::Log, timeout: Option) -> Option { let parent_hash = log.topics()[1].to_string(); let parent_name = net::get_name(&parent_hash, log.block_number, timeout)?; @@ -471,6 +549,40 @@ pub fn resolve_full_name(log: &crate::eth::Log, timeout: Option) -> Option< Some(format!("{name}.{parent_name}")) } +/// Given a [`crate::eth::Log`] (which must be a log from hypermap), resolve the full name +/// of the new entry or note. +/// +/// Uses [`valid_name()`] to check if the name is valid. +#[cfg(feature = "hyperapp")] +pub async fn resolve_full_name(log: &crate::eth::Log, timeout: Option) -> Option { + let parent_hash = log.topics()[1].to_string(); + let parent_name = net::get_name(&parent_hash, log.block_number, timeout).await?; + let log_name = match log.topics()[0] { + contract::Mint::SIGNATURE_HASH => { + let decoded = contract::Mint::decode_log_data(log.data(), true).unwrap(); + decoded.label + } + contract::Note::SIGNATURE_HASH => { + let decoded = contract::Note::decode_log_data(log.data(), true).unwrap(); + decoded.label + } + contract::Fact::SIGNATURE_HASH => { + let decoded = contract::Fact::decode_log_data(log.data(), true).unwrap(); + decoded.label + } + _ => return None, + }; + let name = String::from_utf8_lossy(&log_name); + if !valid_entry( + &name, + log.topics()[0] == contract::Note::SIGNATURE_HASH, + log.topics()[0] == contract::Fact::SIGNATURE_HASH, + ) { + return None; + } + Some(format!("{name}.{parent_name}")) +} + pub fn eth_apply_filter(logs: &[EthLog], filter: &EthFilter) -> Vec { let mut matched_logs = Vec::new(); @@ -947,6 +1059,7 @@ impl Hypermap { )) } + #[cfg(not(feature = "hyperapp"))] pub fn validate_log_cache(&self, log_cache: &LogCache) -> anyhow::Result { let from_block = log_cache.metadata.from_block.parse::().map_err(|_| { anyhow::anyhow!( @@ -978,6 +1091,39 @@ impl Hypermap { )?) } + #[cfg(feature = "hyperapp")] + pub async fn validate_log_cache(&self, log_cache: &LogCache) -> anyhow::Result { + let from_block = log_cache.metadata.from_block.parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid from_block in metadata: {}", + log_cache.metadata.from_block + ) + })?; + let to_block = log_cache.metadata.to_block.parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid to_block in metadata: {}", + log_cache.metadata.to_block + ) + })?; + + let mut bytes_to_verify = serde_json::to_vec(&log_cache.logs) + .map_err(|e| anyhow::anyhow!("Failed to serialize logs for validation: {:?}", e))?; + bytes_to_verify.extend_from_slice(&from_block.to_be_bytes()); + bytes_to_verify.extend_from_slice(&to_block.to_be_bytes()); + let hashed_data = keccak256(&bytes_to_verify); + + let signature_hex = log_cache.metadata.signature.trim_start_matches("0x"); + let signature_bytes = hex::decode(signature_hex) + .map_err(|e| anyhow::anyhow!("Failed to decode hex signature: {:?}", e))?; + + Ok(sign::net_key_verify( + hashed_data.to_vec(), + &log_cache.metadata.created_by.parse::()?, + signature_bytes, + ).await?) + } + + #[cfg(not(feature = "hyperapp"))] pub fn get_bootstrap( &self, from_block: Option, @@ -1057,6 +1203,89 @@ impl Hypermap { Ok((block, unique_logs)) } + #[cfg(feature = "hyperapp")] + pub async fn get_bootstrap( + &self, + from_block: Option, + retry_params: Option<(u64, Option)>, + chain: Option, + ) -> anyhow::Result<(u64, Vec)> { + print_to_terminal( + 2, + &format!( + "get_bootstrap: from_block={:?}, retry_params={:?}, chain={:?}", + from_block, retry_params, chain, + ), + ); + let (block, log_caches) = self.get_bootstrap_log_cache(from_block, retry_params, chain)?; + + let mut all_valid_logs: Vec = Vec::new(); + let request_from_block_val = from_block.unwrap_or(0); + + for log_cache in log_caches { + match self.validate_log_cache(&log_cache).await { + Ok(true) => { + for log in log_cache.logs { + if let Some(log_block_number) = log.block_number { + if log_block_number >= request_from_block_val { + all_valid_logs.push(log); + } + } else { + if from_block.is_none() { + all_valid_logs.push(log); + } + } + } + } + Ok(false) => { + print_to_terminal( + 1, + &format!("LogCache validation failed for cache created by {}. Discarding {} logs.", + log_cache.metadata.created_by, + log_cache.logs.len()) + ); + } + Err(e) => { + print_to_terminal( + 1, + &format!( + "Error validating LogCache from {}: {:?}. Discarding {} logs.", + log_cache.metadata.created_by, + e, + log_cache.logs.len() + ), + ); + } + } + } + + all_valid_logs.sort_by(|a, b| { + let block_cmp = a.block_number.cmp(&b.block_number); + if block_cmp == std::cmp::Ordering::Equal { + std::cmp::Ordering::Equal + } else { + block_cmp + } + }); + + let mut unique_logs = Vec::new(); + for log in all_valid_logs { + if !unique_logs.contains(&log) { + unique_logs.push(log); + } + } + + print_to_terminal( + 2, + &format!( + "get_bootstrap: Consolidated {} unique logs.", + unique_logs.len(), + ), + ); + Ok((block, unique_logs)) + } + + #[cfg(not(feature = "hyperapp"))] pub fn bootstrap( &self, from_block: Option, @@ -1097,6 +1326,44 @@ impl Hypermap { ); Ok((block, results_per_filter)) } + + #[cfg(feature = "hyperapp")] + pub async fn bootstrap( + &self, + from_block: Option, + filters: Vec, + retry_params: Option<(u64, Option)>, + chain: Option, + ) -> anyhow::Result<(u64, Vec>)> { + print_to_terminal( + 2, + &format!( + "bootstrap: from_block={:?}, filters={:?}, retry_params={:?}, chain={:?}", + from_block, filters, retry_params, chain, + ), + ); + let (block, consolidated_logs) = self.get_bootstrap(from_block, retry_params, chain).await?; + + if consolidated_logs.is_empty() { + print_to_terminal(2,"bootstrap: No logs retrieved after consolidation. Returning empty results for filters."); + return Ok((block, filters.iter().map(|_| Vec::new()).collect())); + } + + let mut results_per_filter: Vec> = Vec::new(); + for filter in filters { + let filtered_logs = eth_apply_filter(&consolidated_logs, &filter); + results_per_filter.push(filtered_logs); + } + + print_to_terminal( + 2, + &format!( + "bootstrap: Applied {} filters to bootstrapped logs.", + results_per_filter.len(), + ), + ); + Ok((block, results_per_filter)) + } } impl Serialize for ManifestItem { diff --git a/src/kv.rs b/src/kv.rs index 71611b4..a668e92 100644 --- a/src/kv.rs +++ b/src/kv.rs @@ -1,8 +1,13 @@ -use crate::{get_blob, Message, PackageId, Request}; +use crate::PackageId; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::marker::PhantomData; use thiserror::Error; +#[cfg(not(feature = "hyperapp"))] +mod kv_sync; +#[cfg(feature = "hyperapp")] +mod kv_async; + /// Actions are sent to a specific key value database. `db` is the name, /// `package_id` is the [`PackageId`] that created the database. Capabilities /// are checked: you can access another process's database if it has given @@ -158,408 +163,5 @@ pub struct Kv { _marker: PhantomData<(K, V)>, } -impl Kv -where - K: Serialize + DeserializeOwned, - V: Serialize + DeserializeOwned, -{ - /// Get a value. - pub fn get(&self, key: &K) -> anyhow::Result { - let key = serde_json::to_vec(key)?; - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Get(key), - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Get { .. } => { - let bytes = match get_blob() { - Some(bytes) => bytes.bytes, - None => return Err(anyhow::anyhow!("kv: no blob")), - }; - let value = serde_json::from_slice::(&bytes) - .map_err(|e| anyhow::anyhow!("Failed to deserialize value: {}", e))?; - Ok(value) - } - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Get a value as a different type T - pub fn get_as(&self, key: &K) -> anyhow::Result - where - T: DeserializeOwned, - { - let key = serde_json::to_vec(key)?; - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Get(key), - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Get { .. } => { - let bytes = match get_blob() { - Some(bytes) => bytes.bytes, - None => return Err(anyhow::anyhow!("kv: no blob")), - }; - let value = serde_json::from_slice::(&bytes) - .map_err(|e| anyhow::anyhow!("Failed to deserialize value: {}", e))?; - Ok(value) - } - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Set a value, optionally in a transaction. - pub fn set(&self, key: &K, value: &V, tx_id: Option) -> anyhow::Result<()> { - let key = serde_json::to_vec(key)?; - let value = serde_json::to_vec(value)?; - - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Set { key, tx_id }, - })?) - .blob_bytes(value) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Set a value as a different type T - pub fn set_as(&self, key: &K, value: &T, tx_id: Option) -> anyhow::Result<()> - where - T: Serialize, - { - let key = serde_json::to_vec(key)?; - let value = serde_json::to_vec(value)?; - - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Set { key, tx_id }, - })?) - .blob_bytes(value) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Delete a value, optionally in a transaction. - pub fn delete(&self, key: &K, tx_id: Option) -> anyhow::Result<()> { - let key = serde_json::to_vec(key)?; - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Delete { key, tx_id }, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Delete a value with a different key type - pub fn delete_as(&self, key: &T, tx_id: Option) -> anyhow::Result<()> - where - T: Serialize, - { - let key = serde_json::to_vec(key)?; - - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Delete { key, tx_id }, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Begin a transaction. - pub fn begin_tx(&self) -> anyhow::Result { - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::BeginTx, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::BeginTx { tx_id } => Ok(tx_id), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Commit a transaction. - pub fn commit_tx(&self, tx_id: u64) -> anyhow::Result<()> { - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Commit { tx_id }, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } -} - -impl Kv, Vec> { - /// Get raw bytes directly - pub fn get_raw(&self, key: &[u8]) -> anyhow::Result> { - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Get(key.to_vec()), - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Get { .. } => { - let bytes = match get_blob() { - Some(bytes) => bytes.bytes, - None => return Err(anyhow::anyhow!("kv: no blob")), - }; - Ok(bytes) - } - KvResponse::Err { 0: error } => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Set raw bytes directly - pub fn set_raw(&self, key: &[u8], value: &[u8], tx_id: Option) -> anyhow::Result<()> { - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Set { - key: key.to_vec(), - tx_id, - }, - })?) - .blob_bytes(value.to_vec()) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err { 0: error } => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } - - /// Delete raw bytes directly - pub fn delete_raw(&self, key: &[u8], tx_id: Option) -> anyhow::Result<()> { - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: KvAction::Delete { - key: key.to_vec(), - tx_id, - }, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err { 0: error } => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } - } -} - -/// Helper function to open a raw bytes key-value store -pub fn open_raw( - package_id: PackageId, - db: &str, - timeout: Option, -) -> anyhow::Result, Vec>> { - open(package_id, db, timeout) -} - -/// Opens or creates a kv db. -pub fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result> -where - K: Serialize + DeserializeOwned, - V: Serialize + DeserializeOwned, -{ - let timeout = timeout.unwrap_or(5); - - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: package_id.clone(), - db: db.to_string(), - action: KvAction::Open, - })?) - .send_and_await_response(timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - KvResponse::Ok => Ok(Kv { - package_id, - db: db.to_string(), - timeout, - _marker: PhantomData, - }), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } -} - -/// Removes and deletes a kv db. -pub fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { - let timeout = timeout.unwrap_or(5); - - let res = Request::new() - .target(("our", "kv", "distro", "sys")) - .body(serde_json::to_vec(&KvRequest { - package_id: package_id.clone(), - db: db.to_string(), - action: KvAction::RemoveDb, - })?) - .send_and_await_response(timeout)?; - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - match response { - KvResponse::Ok => Ok(()), - KvResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), - } - } - _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), - } -} diff --git a/src/kv/kv_async.rs b/src/kv/kv_async.rs new file mode 100644 index 0000000..ba07429 --- /dev/null +++ b/src/kv/kv_async.rs @@ -0,0 +1,250 @@ +use crate::{ + get_blob, hyperapp, + kv::{Kv, KvAction, KvError, KvRequest, KvResponse}, + PackageId, Request, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::marker::PhantomData; + +impl Kv +where + K: Serialize + DeserializeOwned, + V: Serialize + DeserializeOwned, +{ + /// Get a value. + pub async fn get(&self, key: &K) -> anyhow::Result { + let key = serde_json::to_vec(key)?; + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Get(key), + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Get { .. } => { + let bytes = match get_blob() { + Some(bytes) => bytes.bytes, + None => return Err(anyhow::anyhow!("kv: no blob")), + }; + let value = serde_json::from_slice::(&bytes) + .map_err(|e| anyhow::anyhow!("Failed to deserialize value: {}", e))?; + Ok(value) + } + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } + + /// Get a value as a different type T + pub async fn get_as(&self, key: &K) -> anyhow::Result + where + T: DeserializeOwned, + { + let key = serde_json::to_vec(key)?; + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Get(key), + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Get { .. } => { + let bytes = match get_blob() { + Some(bytes) => bytes.bytes, + None => return Err(anyhow::anyhow!("kv: no blob")), + }; + let value = serde_json::from_slice::(&bytes) + .map_err(|e| anyhow::anyhow!("Failed to deserialize value: {}", e))?; + Ok(value) + } + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } + + /// Set a value, optionally in a transaction. + pub async fn set(&self, key: &K, value: &V, tx_id: Option) -> anyhow::Result<()> { + let key = serde_json::to_vec(key)?; + let value = serde_json::to_vec(value)?; + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Set { key, tx_id }, + })?) + .blob_bytes(value) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } + + /// Set a value as a different type T + pub async fn set_as(&self, key: &K, value: &T, tx_id: Option) -> anyhow::Result<()> + where + T: Serialize, + { + let key = serde_json::to_vec(key)?; + let value = serde_json::to_vec(value)?; + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Set { key, tx_id }, + })?) + .blob_bytes(value) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } + + /// Delete a value, optionally in a transaction. + pub async fn delete(&self, key: &K, tx_id: Option) -> anyhow::Result<()> { + let key = serde_json::to_vec(key)?; + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Delete { key, tx_id }, + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } + + /// Delete a value with a different key type + pub async fn delete_as(&self, key: &T, tx_id: Option) -> anyhow::Result<()> + where + T: Serialize, + { + let key = serde_json::to_vec(key)?; + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Delete { key, tx_id }, + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } + + /// Begin a transaction. + pub async fn begin_tx(&self) -> anyhow::Result { + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::BeginTx, + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::BeginTx { tx_id } => Ok(tx_id), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } + } +} + +/// Removes and deletes a kv db. +pub async fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { + let timeout = timeout.unwrap_or(5); + + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: KvAction::RemoveDb, + })?) + .expects_response(timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } +} + +/// Helper function to open a raw bytes key-value store +pub async fn open_raw( + package_id: PackageId, + db: &str, + timeout: Option, +) -> anyhow::Result, Vec>> { + open(package_id, db, timeout).await +} + +/// Opens or creates a kv db. +pub async fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result> +where + K: Serialize + DeserializeOwned, + V: Serialize + DeserializeOwned, +{ + let timeout = timeout.unwrap_or(5); + + let request = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: KvAction::Open, + })?) + .expects_response(timeout); + + let response = hyperapp::send::(request).await?; + + match response { + KvResponse::Ok => Ok(Kv { + package_id, + db: db.to_string(), + timeout, + _marker: PhantomData, + }), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response")), + } +} \ No newline at end of file diff --git a/src/kv/kv_sync.rs b/src/kv/kv_sync.rs new file mode 100644 index 0000000..6ac70b3 --- /dev/null +++ b/src/kv/kv_sync.rs @@ -0,0 +1,413 @@ +use crate::{ + get_blob, + kv::{Kv, KvAction, KvError, KvRequest, KvResponse}, + Message, PackageId, Request, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::marker::PhantomData; + +impl Kv +where + K: Serialize + DeserializeOwned, + V: Serialize + DeserializeOwned, +{ + /// Get a value. + pub fn get(&self, key: &K) -> anyhow::Result { + let key = serde_json::to_vec(key)?; + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Get(key), + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Get { .. } => { + let bytes = match get_blob() { + Some(bytes) => bytes.bytes, + None => return Err(anyhow::anyhow!("kv: no blob")), + }; + let value = serde_json::from_slice::(&bytes) + .map_err(|e| anyhow::anyhow!("Failed to deserialize value: {}", e))?; + Ok(value) + } + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Get a value as a different type T + pub fn get_as(&self, key: &K) -> anyhow::Result + where + T: DeserializeOwned, + { + let key = serde_json::to_vec(key)?; + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Get(key), + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Get { .. } => { + let bytes = match get_blob() { + Some(bytes) => bytes.bytes, + None => return Err(anyhow::anyhow!("kv: no blob")), + }; + let value = serde_json::from_slice::(&bytes) + .map_err(|e| anyhow::anyhow!("Failed to deserialize value: {}", e))?; + Ok(value) + } + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Set a value, optionally in a transaction. + pub fn set(&self, key: &K, value: &V, tx_id: Option) -> anyhow::Result<()> { + let key = serde_json::to_vec(key)?; + let value = serde_json::to_vec(value)?; + + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Set { key, tx_id }, + })?) + .blob_bytes(value) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Set a value as a different type T + pub fn set_as(&self, key: &K, value: &T, tx_id: Option) -> anyhow::Result<()> + where + T: Serialize, + { + let key = serde_json::to_vec(key)?; + let value = serde_json::to_vec(value)?; + + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Set { key, tx_id }, + })?) + .blob_bytes(value) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Delete a value, optionally in a transaction. + pub fn delete(&self, key: &K, tx_id: Option) -> anyhow::Result<()> { + let key = serde_json::to_vec(key)?; + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Delete { key, tx_id }, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Delete a value with a different key type + pub fn delete_as(&self, key: &T, tx_id: Option) -> anyhow::Result<()> + where + T: Serialize, + { + let key = serde_json::to_vec(key)?; + + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Delete { key, tx_id }, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Begin a transaction. + pub fn begin_tx(&self) -> anyhow::Result { + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::BeginTx, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::BeginTx { tx_id } => Ok(tx_id), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Commit a transaction. + pub fn commit_tx(&self, tx_id: u64) -> anyhow::Result<()> { + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Commit { tx_id }, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } +} + +impl Kv, Vec> { + /// Get raw bytes directly + pub fn get_raw(&self, key: &[u8]) -> anyhow::Result> { + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Get(key.to_vec()), + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Get { .. } => { + let bytes = match get_blob() { + Some(bytes) => bytes.bytes, + None => return Err(anyhow::anyhow!("kv: no blob")), + }; + Ok(bytes) + } + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Set raw bytes directly + pub fn set_raw(&self, key: &[u8], value: &[u8], tx_id: Option) -> anyhow::Result<()> { + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Set { + key: key.to_vec(), + tx_id, + }, + })?) + .blob_bytes(value.to_vec()) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } + + /// Delete raw bytes directly + pub fn delete_raw(&self, key: &[u8], tx_id: Option) -> anyhow::Result<()> { + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: KvAction::Delete { + key: key.to_vec(), + tx_id, + }, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } + } +} + +/// Helper function to open a raw bytes key-value store +pub fn open_raw( + package_id: PackageId, + db: &str, + timeout: Option, +) -> anyhow::Result, Vec>> { + open(package_id, db, timeout) +} + +/// Opens or creates a kv db. +pub fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result> +where + K: Serialize + DeserializeOwned, + V: Serialize + DeserializeOwned, +{ + let timeout = timeout.unwrap_or(5); + + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: KvAction::Open, + })?) + .send_and_await_response(timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(Kv { + package_id, + db: db.to_string(), + timeout, + _marker: PhantomData, + }), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } +} + +/// Removes and deletes a kv db. +pub fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { + let timeout = timeout.unwrap_or(5); + + let res = Request::new() + .target(("our", "kv", "distro", "sys")) + .body(serde_json::to_vec(&KvRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: KvAction::RemoveDb, + })?) + .send_and_await_response(timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + KvResponse::Ok => Ok(()), + KvResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("kv: unexpected response {:?}", response)), + } + } + _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), + } +} \ No newline at end of file diff --git a/src/net.rs b/src/net.rs index 371e465..a679468 100644 --- a/src/net.rs +++ b/src/net.rs @@ -2,6 +2,9 @@ use crate::{get_blob, Address, NodeId, Request, SendError}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +#[cfg(feature = "hyperapp")] +use crate::hyperapp; + // // Networking protocol types and functions for interacting with it // @@ -166,6 +169,7 @@ impl HnsUpdate { /// /// This function uses a 30-second timeout to reach `net:distro:sys`. If more /// control over the timeout is needed, create a [`Request`] directly. +#[cfg(not(feature = "hyperapp"))] pub fn sign(message: T) -> Result, SendError> where T: Into>, @@ -178,6 +182,37 @@ where .map(|_resp| get_blob().unwrap().bytes) } +/// Verify a signature on a message. +/// +/// The receiver of a signature created using [`sign`] should use this function +/// to verify the signature, which takes a `from` address to match against +/// the prepended signing [`Address`] of the source process. +/// +/// Sign a message with the node's networking key. This may be used to prove +/// identity to other parties outside of using the networking protocol. +/// +/// Note that the given message will be prepended with the source [`Address`] +/// of this message. This is done in order to not allow different processes +/// on the same node to sign messages for/as one another. The receiver of +/// the signed message should use [`verify()`] to verify the signature, which +/// takes a `from` address to match against that prepended signing [`Address`]. +/// +/// This function uses a 30-second timeout to reach `net:distro:sys`. If more +/// control over the timeout is needed, create a [`Request`] directly. +#[cfg(feature = "hyperapp")] +pub async fn sign(message: T) -> Result, hyperapp::AppSendError> +where + T: Into>, +{ + let request = Request::to(("our", "net", "distro", "sys")) + .body(rmp_serde::to_vec(&NetAction::Sign).unwrap()) + .blob_bytes(message.into()) + .expects_response(30); + + hyperapp::send_rmp::>(request).await?; + Ok(get_blob().unwrap().bytes) +} + /// Verify a signature on a message. /// /// The receiver of a signature created using [`sign`] should use this function @@ -186,6 +221,7 @@ where /// /// This function uses a 30-second timeout to reach `net:distro:sys`. If more /// control over the timeout is needed, create a [`Request`] directly. +#[cfg(not(feature = "hyperapp"))] pub fn verify(from: T, message: U, signature: V) -> Result where T: Into
, @@ -213,11 +249,49 @@ where }) } +/// Get a [`crate::hypermap::Hypermap`] entry name from its namehash. +/// +/// Verify a signature on a message. +/// +/// The receiver of a signature created using [`sign`] should use this function +/// to verify the signature, which takes a `from` address to match against +/// the prepended signing [`Address`] of the source process. +/// +/// This function uses a 30-second timeout to reach `net:distro:sys`. If more +/// control over the timeout is needed, create a [`Request`] directly. +#[cfg(feature = "hyperapp")] +pub async fn verify(from: T, message: U, signature: V) -> Result +where + T: Into
, + U: Into>, + V: Into>, +{ + let request = Request::to(("our", "net", "distro", "sys")) + .body( + rmp_serde::to_vec(&NetAction::Verify { + from: from.into(), + signature: signature.into(), + }) + .unwrap(), + ) + .blob_bytes(message.into()) + .expects_response(30); + + let resp_bytes = hyperapp::send_rmp::>(request).await?; + let Ok(NetResponse::Verified(valid)) = + rmp_serde::from_slice::(&resp_bytes) + else { + return Ok(false); + }; + Ok(valid) +} + /// Get a [`crate::hypermap::Hypermap`] entry name from its namehash. /// /// Default timeout is 30 seconds. Note that the responsiveness of the indexer /// will depend on the block option used. The indexer will wait until it has /// seen the block given to respond. +#[cfg(not(feature = "hyperapp"))] pub fn get_name(namehash: T, block: Option, timeout: Option) -> Option where T: Into, @@ -242,3 +316,34 @@ where maybe_name } + +/// Get a [`crate::hypermap::Hypermap`] entry name from its namehash. +/// +/// Default timeout is 30 seconds. Note that the responsiveness of the indexer +/// will depend on the block option used. The indexer will wait until it has +/// seen the block given to respond. +#[cfg(feature = "hyperapp")] +pub async fn get_name(namehash: T, block: Option, timeout: Option) -> Option +where + T: Into, +{ + let request = Request::to(("our", "hns-indexer", "hns-indexer", "sys")) + .body( + serde_json::to_vec(&IndexerRequests::NamehashToName(NamehashToNameRequest { + hash: namehash.into(), + block: block.unwrap_or(0), + })) + .unwrap(), + ) + .expects_response(timeout.unwrap_or(30)); + + let resp_bytes = hyperapp::send::>(request).await.ok()?; + + let Ok(IndexerResponses::Name(maybe_name)) = + serde_json::from_slice::(&resp_bytes) + else { + return None; + }; + + maybe_name +} diff --git a/src/sign.rs b/src/sign.rs index 7611425..a6b7aea 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -11,6 +11,10 @@ use crate::hyperware::process::sign::{ }; use crate::{last_blob, Address, Request}; +#[cfg(feature = "hyperapp")] +use crate::hyperapp; + +#[cfg(not(feature = "hyperapp"))] pub fn net_key_sign(message: Vec) -> anyhow::Result> { let response = Request::to(("our", "sign", "sign", "sys")) .body(serde_json::to_vec(&SignRequest::NetKeySign).unwrap()) @@ -27,6 +31,26 @@ pub fn net_key_sign(message: Vec) -> anyhow::Result> { Ok(last_blob().unwrap().bytes) } +#[cfg(feature = "hyperapp")] +pub async fn net_key_sign(message: Vec) -> anyhow::Result> { + let request = Request::to(("our", "sign", "sign", "sys")) + .body(serde_json::to_vec(&SignRequest::NetKeySign).unwrap()) + .blob_bytes(message) + .expects_response(10); + + let response_bytes = hyperapp::send::>(request).await?; + + let SignResponse::NetKeySign = serde_json::from_slice(&response_bytes)? else { + return Err(anyhow::anyhow!( + "unexpected response from sign:sign:sys: {}", + String::from_utf8(response_bytes).unwrap_or_default(), + )); + }; + + Ok(last_blob().unwrap().bytes) +} + +#[cfg(not(feature = "hyperapp"))] pub fn net_key_verify( message: Vec, signer: &Address, @@ -53,6 +77,35 @@ pub fn net_key_verify( Ok(response) } +#[cfg(feature = "hyperapp")] +pub async fn net_key_verify( + message: Vec, + signer: &Address, + signature: Vec, +) -> anyhow::Result { + let request = Request::to(("our", "sign", "sign", "sys")) + .body( + serde_json::to_vec(&SignRequest::NetKeyVerify(NetKeyVerifyRequest { + node: signer.to_string(), + signature, + })) + .unwrap(), + ) + .blob_bytes(message) + .expects_response(10); + + let response_bytes = hyperapp::send::>(request).await?; + + let SignResponse::NetKeyVerify(response) = serde_json::from_slice(&response_bytes)? else { + return Err(anyhow::anyhow!( + "unexpected response from sign:sign:sys: {}", + String::from_utf8(response_bytes).unwrap_or_default(), + )); + }; + + Ok(response) +} + impl Serialize for NetKeyVerifyRequest { fn serialize(&self, serializer: S) -> Result where diff --git a/src/sqlite.rs b/src/sqlite.rs index 30c5bdd..f8c8b5b 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -1,8 +1,12 @@ -use crate::{get_blob, Message, PackageId, Request}; +use crate::PackageId; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use thiserror::Error; +#[cfg(not(feature = "hyperapp"))] +mod sqlite_sync; +#[cfg(feature = "hyperapp")] +mod sqlite_async; + /// Actions are sent to a specific SQLite database. `db` is the name, /// `package_id` is the [`PackageId`] that created the database. Capabilities /// are checked: you can access another process's database if it has given @@ -177,198 +181,3 @@ pub struct Sqlite { pub timeout: u64, } -impl Sqlite { - /// Query database. Only allows sqlite read keywords. - pub fn read( - &self, - query: String, - params: Vec, - ) -> anyhow::Result>> { - let res = Request::new() - .target(("our", "sqlite", "distro", "sys")) - .body(serde_json::to_vec(&SqliteRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: SqliteAction::Query(query), - })?) - .blob_bytes(serde_json::to_vec(¶ms)?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - SqliteResponse::Read => { - let blob = get_blob().ok_or_else(|| SqliteError::MalformedRequest)?; - let values = serde_json::from_slice::< - Vec>, - >(&blob.bytes) - .map_err(|_| SqliteError::MalformedRequest)?; - Ok(values) - } - SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!( - "sqlite: unexpected response {:?}", - response - )), - } - } - _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), - } - } - - /// Execute a statement. Only allows sqlite write keywords. - pub fn write( - &self, - statement: String, - params: Vec, - tx_id: Option, - ) -> anyhow::Result<()> { - let res = Request::new() - .target(("our", "sqlite", "distro", "sys")) - .body(serde_json::to_vec(&SqliteRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: SqliteAction::Write { statement, tx_id }, - })?) - .blob_bytes(serde_json::to_vec(¶ms)?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - SqliteResponse::Ok => Ok(()), - SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!( - "sqlite: unexpected response {:?}", - response - )), - } - } - _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), - } - } - - /// Begin a transaction. - pub fn begin_tx(&self) -> anyhow::Result { - let res = Request::new() - .target(("our", "sqlite", "distro", "sys")) - .body(serde_json::to_vec(&SqliteRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: SqliteAction::BeginTx, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - SqliteResponse::BeginTx { tx_id } => Ok(tx_id), - SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!( - "sqlite: unexpected response {:?}", - response - )), - } - } - _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), - } - } - - /// Commit a transaction. - pub fn commit_tx(&self, tx_id: u64) -> anyhow::Result<()> { - let res = Request::new() - .target(("our", "sqlite", "distro", "sys")) - .body(serde_json::to_vec(&SqliteRequest { - package_id: self.package_id.clone(), - db: self.db.clone(), - action: SqliteAction::Commit { tx_id }, - })?) - .send_and_await_response(self.timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - SqliteResponse::Ok => Ok(()), - SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!( - "sqlite: unexpected response {:?}", - response - )), - } - } - _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), - } - } -} - -/// Open or create sqlite database. -pub fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result { - let timeout = timeout.unwrap_or(5); - - let res = Request::new() - .target(("our", "sqlite", "distro", "sys")) - .body(serde_json::to_vec(&SqliteRequest { - package_id: package_id.clone(), - db: db.to_string(), - action: SqliteAction::Open, - })?) - .send_and_await_response(timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - SqliteResponse::Ok => Ok(Sqlite { - package_id, - db: db.to_string(), - timeout, - }), - SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!( - "sqlite: unexpected response {:?}", - response - )), - } - } - _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), - } -} - -/// Remove and delete sqlite database. -pub fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { - let timeout = timeout.unwrap_or(5); - - let res = Request::new() - .target(("our", "sqlite", "distro", "sys")) - .body(serde_json::to_vec(&SqliteRequest { - package_id: package_id.clone(), - db: db.to_string(), - action: SqliteAction::RemoveDb, - })?) - .send_and_await_response(timeout)?; - - match res { - Ok(Message::Response { body, .. }) => { - let response = serde_json::from_slice::(&body)?; - - match response { - SqliteResponse::Ok => Ok(()), - SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!( - "sqlite: unexpected response {:?}", - response - )), - } - } - _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), - } -} diff --git a/src/sqlite/sqlite_async.rs b/src/sqlite/sqlite_async.rs new file mode 100644 index 0000000..2645fc0 --- /dev/null +++ b/src/sqlite/sqlite_async.rs @@ -0,0 +1,154 @@ +use crate::{ + get_blob, hyperapp, + sqlite::{Sqlite, SqliteAction, SqliteError, SqliteRequest, SqliteResponse}, + PackageId, Request, +}; +use std::collections::HashMap; + +impl Sqlite { + /// Query database. Only allows sqlite read keywords. + pub async fn read( + &self, + query: String, + params: Vec, + ) -> anyhow::Result>> { + let request = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::Query(query), + })?) + .blob_bytes(serde_json::to_vec(¶ms)?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + SqliteResponse::Read => { + let blob = get_blob().ok_or_else(|| SqliteError::MalformedRequest)?; + let values = serde_json::from_slice::< + Vec>, + >(&blob.bytes) + .map_err(|_| SqliteError::MalformedRequest)?; + Ok(values) + } + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + } + } + + /// Execute a statement. Only allows sqlite write keywords. + pub async fn write( + &self, + statement: String, + params: Vec, + tx_id: Option, + ) -> anyhow::Result<()> { + let request = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::Write { statement, tx_id }, + })?) + .blob_bytes(serde_json::to_vec(¶ms)?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + SqliteResponse::Ok => Ok(()), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + } + } + + /// Begin a transaction. + pub async fn begin_tx(&self) -> anyhow::Result { + let request = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::BeginTx, + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + SqliteResponse::BeginTx { tx_id } => Ok(tx_id), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + } + } + + /// Commit a transaction. + pub async fn commit_tx(&self, tx_id: u64) -> anyhow::Result<()> { + let request = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::Commit { tx_id }, + })?) + .expects_response(self.timeout); + + let response = hyperapp::send::(request).await?; + + match response { + SqliteResponse::Ok => Ok(()), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + } + } +} + +/// Open or create sqlite database. +pub async fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result { + let timeout = timeout.unwrap_or(5); + + let request = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: SqliteAction::Open, + })?) + .expects_response(timeout); + + let response = hyperapp::send::(request).await?; + + match response { + SqliteResponse::Ok => Ok(Sqlite { + package_id, + db: db.to_string(), + timeout, + }), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + } +} + +/// Remove and delete sqlite database. +pub async fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { + let timeout = timeout.unwrap_or(5); + + let request = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: SqliteAction::RemoveDb, + })?) + .expects_response(timeout); + + let response = hyperapp::send::(request).await?; + + match response { + SqliteResponse::Ok => Ok(()), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + } +} \ No newline at end of file diff --git a/src/sqlite/sqlite_sync.rs b/src/sqlite/sqlite_sync.rs new file mode 100644 index 0000000..d1b4cb7 --- /dev/null +++ b/src/sqlite/sqlite_sync.rs @@ -0,0 +1,202 @@ +use crate::{ + get_blob, + sqlite::{Sqlite, SqliteAction, SqliteError, SqliteRequest, SqliteResponse}, + Message, PackageId, Request, +}; +use std::collections::HashMap; + +impl Sqlite { + /// Query database. Only allows sqlite read keywords. + pub fn read( + &self, + query: String, + params: Vec, + ) -> anyhow::Result>> { + let res = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::Query(query), + })?) + .blob_bytes(serde_json::to_vec(¶ms)?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + SqliteResponse::Read => { + let blob = get_blob().ok_or_else(|| SqliteError::MalformedRequest)?; + let values = serde_json::from_slice::< + Vec>, + >(&blob.bytes) + .map_err(|_| SqliteError::MalformedRequest)?; + Ok(values) + } + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), + } + } + _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), + } + } + + /// Execute a statement. Only allows sqlite write keywords. + pub fn write( + &self, + statement: String, + params: Vec, + tx_id: Option, + ) -> anyhow::Result<()> { + let res = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::Write { statement, tx_id }, + })?) + .blob_bytes(serde_json::to_vec(¶ms)?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + SqliteResponse::Ok => Ok(()), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), + } + } + _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), + } + } + + /// Begin a transaction. + pub fn begin_tx(&self) -> anyhow::Result { + let res = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::BeginTx, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + SqliteResponse::BeginTx { tx_id } => Ok(tx_id), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), + } + } + _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), + } + } + + /// Commit a transaction. + pub fn commit_tx(&self, tx_id: u64) -> anyhow::Result<()> { + let res = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: self.package_id.clone(), + db: self.db.clone(), + action: SqliteAction::Commit { tx_id }, + })?) + .send_and_await_response(self.timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + SqliteResponse::Ok => Ok(()), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), + } + } + _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), + } + } +} + +/// Open or create sqlite database. +pub fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result { + let timeout = timeout.unwrap_or(5); + + let res = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: SqliteAction::Open, + })?) + .send_and_await_response(timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + SqliteResponse::Ok => Ok(Sqlite { + package_id, + db: db.to_string(), + timeout, + }), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), + } + } + _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), + } +} + +/// Remove and delete sqlite database. +pub fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { + let timeout = timeout.unwrap_or(5); + + let res = Request::new() + .target(("our", "sqlite", "distro", "sys")) + .body(serde_json::to_vec(&SqliteRequest { + package_id: package_id.clone(), + db: db.to_string(), + action: SqliteAction::RemoveDb, + })?) + .send_and_await_response(timeout)?; + + match res { + Ok(Message::Response { body, .. }) => { + let response = serde_json::from_slice::(&body)?; + + match response { + SqliteResponse::Ok => Ok(()), + SqliteResponse::Err(error) => Err(error.into()), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), + } + } + _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), + } +} \ No newline at end of file diff --git a/src/timer.rs b/src/timer.rs index a946800..96994cb 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -1,6 +1,9 @@ use crate::{Context, Message, Request, SendError}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "hyperapp")] +use crate::hyperapp; + /// The [`Request::body()`] field for requests to `timer:distro:sys`, a runtime module /// that allows processes to set timers with a duration specified in milliseconds. /// @@ -33,6 +36,7 @@ pub fn set_timer(duration: u64, context: Option) { /// Set a timer using the runtime that will return a [`crate::Response`] after the specified duration, /// then wait for that timer to resolve. The duration should be a number of milliseconds. +#[cfg(not(feature = "hyperapp"))] pub fn set_and_await_timer(duration: u64) -> Result { Request::to(("our", "timer", "distro", "sys")) .body(TimerAction::SetTimer(duration)) @@ -40,3 +44,10 @@ pub fn set_and_await_timer(duration: u64) -> Result { // safe to unwrap this call when we know we've set both target and body .unwrap() } + +/// Set a timer using the runtime that will return a [`crate::Response`] after the specified duration, +/// then wait for that timer to resolve. The duration should be a number of milliseconds. +#[cfg(feature = "hyperapp")] +pub async fn set_and_await_timer(duration: u64) -> Result<(), hyperapp::AppSendError> { + hyperapp::sleep(duration).await +} diff --git a/src/vfs/directory.rs b/src/vfs/directory.rs index ceebbf9..e23cec1 100644 --- a/src/vfs/directory.rs +++ b/src/vfs/directory.rs @@ -1,5 +1,8 @@ use super::{parse_response, vfs_request, DirEntry, FileType, VfsAction, VfsError, VfsResponse}; +#[cfg(feature = "hyperapp")] +pub mod directory_async; + /// VFS (Virtual File System) helper struct for a directory. /// Opening or creating a directory will give you a `Result`. /// You can call it's impl functions to interact with it. @@ -79,6 +82,7 @@ pub fn open_dir(path: &str, create: bool, timeout: Option) -> Result) -> Result<(), VfsError> { let timeout = timeout.unwrap_or(5); @@ -96,3 +100,9 @@ pub fn remove_dir(path: &str, timeout: Option) -> Result<(), VfsError> { }), } } + +/// Removes a dir at path, errors if path not found or path is not a `Directory`. +#[cfg(feature = "hyperapp")] +pub async fn remove_dir(path: &str, timeout: Option) -> Result<(), VfsError> { + directory_async::remove_dir_async(path, timeout).await +} diff --git a/src/vfs/directory/directory_async.rs b/src/vfs/directory/directory_async.rs new file mode 100644 index 0000000..7b3f794 --- /dev/null +++ b/src/vfs/directory/directory_async.rs @@ -0,0 +1,23 @@ +use crate::{ + hyperapp, + vfs::{vfs_request, VfsAction, VfsError, VfsResponse, parse_response}, + Request, +}; + +/// Removes a dir at path, errors if path not found or path is not a `Directory`. +pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), VfsError> { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::RemoveDir).expects_response(timeout); + + let response = hyperapp::send::(request).await.map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match response { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} \ No newline at end of file diff --git a/src/vfs/directory_async.rs b/src/vfs/directory_async.rs new file mode 100644 index 0000000..0540f84 --- /dev/null +++ b/src/vfs/directory_async.rs @@ -0,0 +1,100 @@ +use super::{parse_response, vfs_request, DirEntry, FileType, VfsAction, VfsError, VfsResponse}; +use crate::hyperapp; + +pub struct DirectoryAsync { + pub path: String, + pub timeout: u64, +} + +impl DirectoryAsync { + pub async fn read(&self) -> Result, VfsError> { + let request = vfs_request(&self.path, VfsAction::ReadDir) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::ReadDir(entries) => Ok(entries), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } +} + +pub async fn open_dir_async(path: &str, create: bool, timeout: Option) -> Result { + let timeout = timeout.unwrap_or(5); + if !create { + let request = vfs_request(path, VfsAction::Metadata) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Metadata(m) => { + if m.file_type != FileType::Directory { + return Err(VfsError::IOError( + "entry at path is not a directory".to_string(), + )); + } + } + VfsResponse::Err(e) => return Err(e), + _ => { + return Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }) + } + } + + return Ok(DirectoryAsync { + path: path.to_string(), + timeout, + }); + } + + let request = vfs_request(path, VfsAction::CreateDirAll) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(DirectoryAsync { + path: path.to_string(), + timeout, + }), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} + +pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), VfsError> { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::RemoveDir) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} \ No newline at end of file diff --git a/src/vfs/file.rs b/src/vfs/file.rs index 4a2b716..e0d6cb4 100644 --- a/src/vfs/file.rs +++ b/src/vfs/file.rs @@ -3,6 +3,9 @@ use super::{ }; use crate::{get_blob, PackageId}; +#[cfg(feature = "hyperapp")] +pub mod file_async; + /// VFS (Virtual File System) helper struct for a file. /// Opening or creating a `File` will give you a `Result`. /// You can call its impl functions to interact with it. @@ -366,6 +369,7 @@ pub fn create_file(path: &str, timeout: Option) -> Result { } /// Removes a file at path, errors if path not found or path is not a file. +#[cfg(not(feature = "hyperapp"))] pub fn remove_file(path: &str, timeout: Option) -> Result<(), VfsError> { let timeout = timeout.unwrap_or(5); @@ -383,3 +387,9 @@ pub fn remove_file(path: &str, timeout: Option) -> Result<(), VfsError> { }), } } + +/// Removes a file at path, errors if path not found or path is not a file. +#[cfg(feature = "hyperapp")] +pub async fn remove_file(path: &str, timeout: Option) -> Result<(), VfsError> { + file_async::remove_file_async(path, timeout).await +} diff --git a/src/vfs/file/file_async.rs b/src/vfs/file/file_async.rs new file mode 100644 index 0000000..2e403b6 --- /dev/null +++ b/src/vfs/file/file_async.rs @@ -0,0 +1,23 @@ +use crate::{ + hyperapp, + vfs::{vfs_request, VfsAction, VfsError, VfsResponse, parse_response}, + Request, +}; + +/// Removes a file at path, errors if path not found or path is not a file. +pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), VfsError> { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::RemoveFile).expects_response(timeout); + + let response = hyperapp::send::(request).await.map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match response { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e.into()), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} \ No newline at end of file diff --git a/src/vfs/file_async.rs b/src/vfs/file_async.rs new file mode 100644 index 0000000..94e92dd --- /dev/null +++ b/src/vfs/file_async.rs @@ -0,0 +1,388 @@ +use super::{ + parse_response, vfs_request, FileMetadata, SeekFrom, VfsAction, VfsError, VfsResponse, +}; +use crate::{get_blob, hyperapp, PackageId}; + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct FileAsync { + pub path: String, + pub timeout: u64, +} + +impl FileAsync { + pub fn new>(path: T, timeout: u64) -> Self { + Self { + path: path.into(), + timeout, + } + } + + pub async fn read(&self) -> Result, VfsError> { + let request = vfs_request(&self.path, VfsAction::Read) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Read => { + let data = match get_blob() { + Some(bytes) => bytes.bytes, + None => { + return Err(VfsError::ParseError { + error: "no blob".to_string(), + path: self.path.clone(), + }) + } + }; + Ok(data) + } + VfsResponse::Err(e) => Err(e.into()), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn read_into(&self, buffer: &mut [u8]) -> Result { + let request = vfs_request(&self.path, VfsAction::Read) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Read => { + let data = get_blob().unwrap_or_default().bytes; + let len = std::cmp::min(data.len(), buffer.len()); + buffer[..len].copy_from_slice(&data[..len]); + Ok(len) + } + VfsResponse::Err(e) => Err(e.into()), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn read_at(&self, buffer: &mut [u8]) -> Result { + let length = buffer.len() as u64; + + let request = vfs_request(&self.path, VfsAction::ReadExact { length }) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Read => { + let data = get_blob().unwrap_or_default().bytes; + let len = std::cmp::min(data.len(), buffer.len()); + buffer[..len].copy_from_slice(&data[..len]); + Ok(len) + } + VfsResponse::Err(e) => Err(e.into()), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn read_to_end(&self) -> Result, VfsError> { + let request = vfs_request(&self.path, VfsAction::ReadToEnd) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Read => Ok(get_blob().unwrap_or_default().bytes), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn read_to_string(&self) -> Result { + let request = vfs_request(&self.path, VfsAction::ReadToString) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::ReadToString(s) => Ok(s), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn write(&self, buffer: &[u8]) -> Result<(), VfsError> { + let request = vfs_request(&self.path, VfsAction::Write) + .blob_bytes(buffer) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn write_all(&mut self, buffer: &[u8]) -> Result<(), VfsError> { + let request = vfs_request(&self.path, VfsAction::WriteAll) + .blob_bytes(buffer) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn append(&mut self, buffer: &[u8]) -> Result<(), VfsError> { + let request = vfs_request(&self.path, VfsAction::Append) + .blob_bytes(buffer) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn seek(&mut self, pos: SeekFrom) -> Result { + let request = vfs_request(&self.path, VfsAction::Seek(pos)) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::SeekFrom { + new_offset: new_pos, + } => Ok(new_pos), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn copy(&mut self, path: &str) -> Result { + let request = vfs_request( + &self.path, + VfsAction::CopyFile { + new_path: path.to_string(), + }, + ) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(FileAsync { + path: path.to_string(), + timeout: self.timeout, + }), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn set_len(&mut self, size: u64) -> Result<(), VfsError> { + let request = vfs_request(&self.path, VfsAction::SetLen(size)) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn metadata(&self) -> Result { + let request = vfs_request(&self.path, VfsAction::Metadata) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Metadata(metadata) => Ok(metadata), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } + + pub async fn sync_all(&self) -> Result<(), VfsError> { + let request = vfs_request(&self.path, VfsAction::SyncAll) + .expects_response(self.timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: self.path.clone(), + }), + } + } +} + +impl Drop for FileAsync { + fn drop(&mut self) { + vfs_request(&self.path, VfsAction::CloseFile) + .send() + .unwrap(); + } +} + +pub async fn create_drive_async( + package_id: PackageId, + drive: &str, + timeout: Option, +) -> Result { + let timeout = timeout.unwrap_or(5); + let path = format!("/{}/{}", package_id, drive); + + let request = vfs_request(&path, VfsAction::CreateDrive) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(path), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path, + }), + } +} + +pub async fn open_file_async(path: &str, create: bool, timeout: Option) -> Result { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::OpenFile { create }) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(FileAsync { + path: path.to_string(), + timeout, + }), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} + +pub async fn create_file_async(path: &str, timeout: Option) -> Result { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::CreateFile) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(FileAsync { + path: path.to_string(), + timeout, + }), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} + +pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), VfsError> { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::RemoveFile) + .expects_response(timeout); + + let resp_bytes = hyperapp::send_rmp::>(request) + .await + .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match parse_response(&resp_bytes)? { + VfsResponse::Ok => Ok(()), + VfsResponse::Err(e) => Err(e.into()), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} \ No newline at end of file diff --git a/src/vfs/mod.rs b/src/vfs/mod.rs index 0022731..e2908bd 100644 --- a/src/vfs/mod.rs +++ b/src/vfs/mod.rs @@ -124,6 +124,7 @@ where } /// Metadata of a path, returns file type and length. +#[cfg(not(feature = "hyperapp"))] pub fn metadata(path: &str, timeout: Option) -> Result { let timeout = timeout.unwrap_or(5); @@ -142,7 +143,28 @@ pub fn metadata(path: &str, timeout: Option) -> Result) -> Result { + let timeout = timeout.unwrap_or(5); + + let request = vfs_request(path, VfsAction::Metadata).expects_response(timeout); + + let response = crate::hyperapp::send::(request).await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + match response { + VfsResponse::Metadata(metadata) => Ok(metadata), + VfsResponse::Err(e) => Err(e), + _ => Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), + } +} + /// Removes a path, if it's either a directory or a file. +#[cfg(not(feature = "hyperapp"))] pub fn remove_path(path: &str, timeout: Option) -> Result<(), VfsError> { let meta = metadata(path, timeout)?; @@ -156,6 +178,21 @@ pub fn remove_path(path: &str, timeout: Option) -> Result<(), VfsError> { } } +/// Removes a path, if it's either a directory or a file. +#[cfg(feature = "hyperapp")] +pub async fn remove_path(path: &str, timeout: Option) -> Result<(), VfsError> { + let meta = metadata(path, timeout).await?; + + match meta.file_type { + FileType::Directory => directory::remove_dir(path, timeout).await, + FileType::File => file::remove_file(path, timeout).await, + _ => Err(VfsError::ParseError { + error: "path is not a file or directory".to_string(), + path: path.to_string(), + }), + } +} + pub fn parse_response(body: &[u8]) -> Result { serde_json::from_slice::(body).map_err(|_| VfsError::MalformedRequest) } From 38ce8dae2c636554a4821be2a243ad0466c43cd2 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 11 Aug 2025 16:29:13 -0700 Subject: [PATCH 10/33] fix all compiler warnings --- src/http/client.rs | 38 +++++++++----------- src/http/server/server_async.rs | 52 ++++++++-------------------- src/kv.rs | 7 +++- src/kv/kv_async.rs | 2 +- src/kv/kv_sync.rs | 2 +- src/net.rs | 20 +++++------ src/sign.rs | 18 +++++----- src/sqlite.rs | 5 +++ src/timer.rs | 4 ++- src/vfs/directory/directory_async.rs | 3 +- src/vfs/file/file_async.rs | 3 +- 11 files changed, 66 insertions(+), 88 deletions(-) diff --git a/src/http/client.rs b/src/http/client.rs index 6747d19..a7d83f2 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,5 +1,7 @@ pub use super::server::{HttpResponse, WsMessageType}; -use crate::{get_blob, LazyLoadBlob as KiBlob, Message, Request as KiRequest}; +use crate::{get_blob, LazyLoadBlob as KiBlob, Request as KiRequest}; +#[cfg(not(feature = "hyperapp"))] +use crate::Message; use http::Method; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -218,26 +220,18 @@ pub async fn send_request_await_response( .blob_bytes(body) .expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let resp_result = hyperapp::send::>(request) .await .map_err(|_| HttpClientError::ExecuteRequestFailed("http-client timed out".to_string()))?; - let resp = match serde_json::from_slice::< - std::result::Result, - >(&resp_bytes) - { - Ok(Ok(HttpClientResponse::Http(resp))) => resp, - Ok(Ok(HttpClientResponse::WebSocketAck)) => { + let resp = match resp_result { + Ok(HttpClientResponse::Http(resp)) => resp, + Ok(HttpClientResponse::WebSocketAck) => { return Err(HttpClientError::ExecuteRequestFailed( "http-client gave unexpected response".to_string(), )) } - Ok(Err(e)) => return Err(e), - Err(e) => { - return Err(HttpClientError::ExecuteRequestFailed(format!( - "http-client gave invalid response: {e:?}" - ))) - } + Err(e) => return Err(e), }; let mut http_response = http::Response::builder() .status(http::StatusCode::from_u16(resp.status).unwrap_or_default()); @@ -315,13 +309,13 @@ pub async fn open_ws_connection( ) .expects_response(5); - let resp_bytes = hyperapp::send_rmp::>(request) + let resp_result = hyperapp::send::>(request) .await .map_err(|_| HttpClientError::WsOpenFailed { url: url.clone() })?; - match serde_json::from_slice(&resp_bytes) { - Ok(Ok(HttpClientResponse::WebSocketAck)) => Ok(()), - Ok(Err(e)) => Err(e), + match resp_result { + Ok(HttpClientResponse::WebSocketAck) => Ok(()), + Err(e) => Err(e), _ => Err(HttpClientError::WsOpenFailed { url }), } } @@ -360,13 +354,13 @@ pub async fn close_ws_connection(channel_id: u32) -> std::result::Result<(), Htt ) .expects_response(5); - let resp_bytes = hyperapp::send_rmp::>(request) + let resp_result = hyperapp::send::>(request) .await .map_err(|_| HttpClientError::WsCloseFailed { channel_id })?; - match serde_json::from_slice(&resp_bytes) { - Ok(Ok(HttpClientResponse::WebSocketAck)) => Ok(()), - Ok(Err(e)) => Err(e), + match resp_result { + Ok(HttpClientResponse::WebSocketAck) => Ok(()), + Err(e) => Err(e), _ => Err(HttpClientError::WsCloseFailed { channel_id }), } } diff --git a/src/http/server/server_async.rs b/src/http/server/server_async.rs index 9441ba9..667272c 100644 --- a/src/http/server/server_async.rs +++ b/src/http/server/server_async.rs @@ -51,9 +51,7 @@ impl HttpServer { None => req, }; - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.insert(path, config); @@ -87,9 +85,7 @@ impl HttpServer { }) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.ws_paths.insert(path, config); @@ -125,9 +121,7 @@ impl HttpServer { }) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.insert( @@ -161,9 +155,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.insert( @@ -194,9 +186,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.ws_paths.insert( @@ -235,9 +225,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { entry.authenticated = config.authenticated; @@ -274,9 +262,7 @@ impl HttpServer { }) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { entry.authenticated = config.authenticated; @@ -295,9 +281,7 @@ impl HttpServer { .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.remove(&path); @@ -317,9 +301,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let resp = serde_json::from_slice::>(&resp_bytes) - .map_err(|_| HttpServerError::UnexpectedResponse)?; + let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.ws_paths.remove(&path); @@ -333,7 +315,7 @@ impl HttpServer { paths: Vec<&str>, config: HttpBindingConfig, ) -> Result<(), HttpServerError> { - use crate::vfs::{VfsAction, VfsRequest}; + use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; use crate::get_blob; let our = crate::our(); @@ -351,7 +333,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let _res = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let _res = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; let Some(mut blob) = get_blob() else { return Err(HttpServerError::NoBlob); @@ -373,7 +355,7 @@ impl HttpServer { paths: Vec<&str>, config: HttpBindingConfig, ) -> Result<(), HttpServerError> { - use crate::vfs::{VfsAction, VfsRequest}; + use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; use crate::get_blob; let req = KiRequest::to(("our", "vfs", "distro", "sys")) @@ -386,7 +368,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let _res = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let _res = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; let Some(mut blob) = get_blob() else { return Err(HttpServerError::NoBlob); @@ -428,9 +410,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let directory_response = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let directory_body = serde_json::from_slice::(&directory_response) - .map_err(|_e| HttpServerError::UnexpectedResponse)?; + let directory_body = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; let VfsResponse::ReadDir(directory_info) = directory_body else { return Err(HttpServerError::UnexpectedResponse); @@ -481,9 +461,7 @@ impl HttpServer { ) .expects_response(self.timeout); - let directory_response = hyperapp::send_rmp::>(req).await.map_err(|_| HttpServerError::Timeout)?; - let directory_body = serde_json::from_slice::(&directory_response) - .map_err(|_e| HttpServerError::UnexpectedResponse)?; + let directory_body = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; let VfsResponse::ReadDir(directory_info) = directory_body else { return Err(HttpServerError::UnexpectedResponse); diff --git a/src/kv.rs b/src/kv.rs index a668e92..bb1f406 100644 --- a/src/kv.rs +++ b/src/kv.rs @@ -1,12 +1,17 @@ use crate::PackageId; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use thiserror::Error; #[cfg(not(feature = "hyperapp"))] mod kv_sync; +#[cfg(not(feature = "hyperapp"))] +pub use kv_sync::{open, open_raw, remove_db}; + #[cfg(feature = "hyperapp")] mod kv_async; +#[cfg(feature = "hyperapp")] +pub use kv_async::{open, open_raw, remove_db}; /// Actions are sent to a specific key value database. `db` is the name, /// `package_id` is the [`PackageId`] that created the database. Capabilities diff --git a/src/kv/kv_async.rs b/src/kv/kv_async.rs index ba07429..bde64a9 100644 --- a/src/kv/kv_async.rs +++ b/src/kv/kv_async.rs @@ -1,6 +1,6 @@ use crate::{ get_blob, hyperapp, - kv::{Kv, KvAction, KvError, KvRequest, KvResponse}, + kv::{Kv, KvAction, KvRequest, KvResponse}, PackageId, Request, }; use serde::{de::DeserializeOwned, Serialize}; diff --git a/src/kv/kv_sync.rs b/src/kv/kv_sync.rs index 6ac70b3..2ab3dcf 100644 --- a/src/kv/kv_sync.rs +++ b/src/kv/kv_sync.rs @@ -1,6 +1,6 @@ use crate::{ get_blob, - kv::{Kv, KvAction, KvError, KvRequest, KvResponse}, + kv::{Kv, KvAction, KvRequest, KvResponse}, Message, PackageId, Request, }; use serde::{de::DeserializeOwned, Serialize}; diff --git a/src/net.rs b/src/net.rs index a679468..abbf2e9 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,4 +1,6 @@ -use crate::{get_blob, Address, NodeId, Request, SendError}; +use crate::{get_blob, Address, NodeId, Request}; +#[cfg(not(feature = "hyperapp"))] +use crate::SendError; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -209,7 +211,7 @@ where .blob_bytes(message.into()) .expects_response(30); - hyperapp::send_rmp::>(request).await?; + let _response = hyperapp::send_rmp::(request).await?; Ok(get_blob().unwrap().bytes) } @@ -277,10 +279,8 @@ where .blob_bytes(message.into()) .expects_response(30); - let resp_bytes = hyperapp::send_rmp::>(request).await?; - let Ok(NetResponse::Verified(valid)) = - rmp_serde::from_slice::(&resp_bytes) - else { + let response = hyperapp::send_rmp::(request).await?; + let NetResponse::Verified(valid) = response else { return Ok(false); }; Ok(valid) @@ -337,13 +337,9 @@ where ) .expects_response(timeout.unwrap_or(30)); - let resp_bytes = hyperapp::send::>(request).await.ok()?; + let response = hyperapp::send::(request).await.ok()?; - let Ok(IndexerResponses::Name(maybe_name)) = - serde_json::from_slice::(&resp_bytes) - else { - return None; - }; + let IndexerResponses::Name(maybe_name) = response; maybe_name } diff --git a/src/sign.rs b/src/sign.rs index a6b7aea..92a5186 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -38,12 +38,12 @@ pub async fn net_key_sign(message: Vec) -> anyhow::Result> { .blob_bytes(message) .expects_response(10); - let response_bytes = hyperapp::send::>(request).await?; + let response = hyperapp::send::(request).await?; - let SignResponse::NetKeySign = serde_json::from_slice(&response_bytes)? else { + let SignResponse::NetKeySign = response else { return Err(anyhow::anyhow!( - "unexpected response from sign:sign:sys: {}", - String::from_utf8(response_bytes).unwrap_or_default(), + "unexpected response from sign:sign:sys: {:?}", + response, )); }; @@ -94,16 +94,16 @@ pub async fn net_key_verify( .blob_bytes(message) .expects_response(10); - let response_bytes = hyperapp::send::>(request).await?; + let response = hyperapp::send::(request).await?; - let SignResponse::NetKeyVerify(response) = serde_json::from_slice(&response_bytes)? else { + let SignResponse::NetKeyVerify(verified) = response else { return Err(anyhow::anyhow!( - "unexpected response from sign:sign:sys: {}", - String::from_utf8(response_bytes).unwrap_or_default(), + "unexpected response from sign:sign:sys: {:?}", + response, )); }; - Ok(response) + Ok(verified) } impl Serialize for NetKeyVerifyRequest { diff --git a/src/sqlite.rs b/src/sqlite.rs index f8c8b5b..5e4e9d5 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -4,8 +4,13 @@ use thiserror::Error; #[cfg(not(feature = "hyperapp"))] mod sqlite_sync; +#[cfg(not(feature = "hyperapp"))] +pub use sqlite_sync::{open, remove_db}; + #[cfg(feature = "hyperapp")] mod sqlite_async; +#[cfg(feature = "hyperapp")] +pub use sqlite_async::{open, remove_db}; /// Actions are sent to a specific SQLite database. `db` is the name, /// `package_id` is the [`PackageId`] that created the database. Capabilities diff --git a/src/timer.rs b/src/timer.rs index 96994cb..574095a 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -1,4 +1,6 @@ -use crate::{Context, Message, Request, SendError}; +use crate::{Context, Request}; +#[cfg(not(feature = "hyperapp"))] +use crate::{Message, SendError}; use serde::{Deserialize, Serialize}; #[cfg(feature = "hyperapp")] diff --git a/src/vfs/directory/directory_async.rs b/src/vfs/directory/directory_async.rs index 7b3f794..373a173 100644 --- a/src/vfs/directory/directory_async.rs +++ b/src/vfs/directory/directory_async.rs @@ -1,7 +1,6 @@ use crate::{ hyperapp, - vfs::{vfs_request, VfsAction, VfsError, VfsResponse, parse_response}, - Request, + vfs::{vfs_request, VfsAction, VfsError, VfsResponse}, }; /// Removes a dir at path, errors if path not found or path is not a `Directory`. diff --git a/src/vfs/file/file_async.rs b/src/vfs/file/file_async.rs index 2e403b6..a84aed2 100644 --- a/src/vfs/file/file_async.rs +++ b/src/vfs/file/file_async.rs @@ -1,7 +1,6 @@ use crate::{ hyperapp, - vfs::{vfs_request, VfsAction, VfsError, VfsResponse, parse_response}, - Request, + vfs::{vfs_request, VfsAction, VfsError, VfsResponse}, }; /// Removes a file at path, errors if path not found or path is not a file. From 8703703e5d9b64d2924045ebb8546f949f576ca5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:44:16 +0000 Subject: [PATCH 11/33] Format Rust code using rustfmt --- src/http/client.rs | 25 +++++--- src/http/server.rs | 5 +- src/http/server/server_async.rs | 91 +++++++++++++++++++--------- src/http/server/server_sync.rs | 13 ++-- src/hypermap.rs | 6 +- src/kv.rs | 3 - src/kv/kv_async.rs | 14 ++++- src/kv/kv_sync.rs | 4 +- src/net.rs | 8 ++- src/sqlite.rs | 1 - src/sqlite/sqlite_async.rs | 45 ++++++++++---- src/sqlite/sqlite_sync.rs | 4 +- src/vfs/directory/directory_async.rs | 8 ++- src/vfs/file/file_async.rs | 8 ++- src/vfs/mod.rs | 5 +- 15 files changed, 161 insertions(+), 79 deletions(-) diff --git a/src/http/client.rs b/src/http/client.rs index a7d83f2..9065c43 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,7 +1,7 @@ pub use super::server::{HttpResponse, WsMessageType}; -use crate::{get_blob, LazyLoadBlob as KiBlob, Request as KiRequest}; #[cfg(not(feature = "hyperapp"))] use crate::Message; +use crate::{get_blob, LazyLoadBlob as KiBlob, Request as KiRequest}; use http::Method; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -220,9 +220,12 @@ pub async fn send_request_await_response( .blob_bytes(body) .expects_response(timeout); - let resp_result = hyperapp::send::>(request) - .await - .map_err(|_| HttpClientError::ExecuteRequestFailed("http-client timed out".to_string()))?; + let resp_result = + hyperapp::send::>(request) + .await + .map_err(|_| { + HttpClientError::ExecuteRequestFailed("http-client timed out".to_string()) + })?; let resp = match resp_result { Ok(HttpClientResponse::Http(resp)) => resp, @@ -309,9 +312,10 @@ pub async fn open_ws_connection( ) .expects_response(5); - let resp_result = hyperapp::send::>(request) - .await - .map_err(|_| HttpClientError::WsOpenFailed { url: url.clone() })?; + let resp_result = + hyperapp::send::>(request) + .await + .map_err(|_| HttpClientError::WsOpenFailed { url: url.clone() })?; match resp_result { Ok(HttpClientResponse::WebSocketAck) => Ok(()), @@ -354,9 +358,10 @@ pub async fn close_ws_connection(channel_id: u32) -> std::result::Result<(), Htt ) .expects_response(5); - let resp_result = hyperapp::send::>(request) - .await - .map_err(|_| HttpClientError::WsCloseFailed { channel_id })?; + let resp_result = + hyperapp::send::>(request) + .await + .map_err(|_| HttpClientError::WsCloseFailed { channel_id })?; match resp_result { Ok(HttpClientResponse::WebSocketAck) => Ok(()), diff --git a/src/http/server.rs b/src/http/server.rs index 8fcdc26..b9382d6 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -5,10 +5,10 @@ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use thiserror::Error; -#[cfg(not(feature = "hyperapp"))] -mod server_sync; #[cfg(feature = "hyperapp")] mod server_async; +#[cfg(not(feature = "hyperapp"))] +mod server_sync; /// [`crate::Request`] received from the `http-server:distro:sys` service as a /// result of either an HTTP or WebSocket binding, created via [`HttpServerAction`]. @@ -445,7 +445,6 @@ impl WsBindingConfig { } } - /// Send an HTTP response to an incoming HTTP request ([`HttpServerRequest::Http`]). pub fn send_response(status: StatusCode, headers: Option>, body: Vec) { KiResponse::new() diff --git a/src/http/server/server_async.rs b/src/http/server/server_async.rs index 667272c..1d11dc2 100644 --- a/src/http/server/server_async.rs +++ b/src/http/server/server_async.rs @@ -1,7 +1,6 @@ use crate::{ http::server::{ - HttpBindingConfig, HttpServer, HttpServerAction, HttpServerError, - WsBindingConfig, + HttpBindingConfig, HttpServer, HttpServerAction, HttpServerError, WsBindingConfig, }, hyperapp, LazyLoadBlob as KiBlob, Request as KiRequest, }; @@ -51,7 +50,9 @@ impl HttpServer { None => req, }; - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.insert(path, config); @@ -85,7 +86,9 @@ impl HttpServer { }) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.ws_paths.insert(path, config); @@ -121,7 +124,9 @@ impl HttpServer { }) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.insert( @@ -155,7 +160,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.insert( @@ -186,7 +193,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.ws_paths.insert( @@ -225,7 +234,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { entry.authenticated = config.authenticated; @@ -262,7 +273,9 @@ impl HttpServer { }) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { entry.authenticated = config.authenticated; @@ -281,7 +294,9 @@ impl HttpServer { .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.http_paths.remove(&path); @@ -301,7 +316,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let resp = hyperapp::send::>(req).await.map_err(|_| HttpServerError::Timeout)?; + let resp = hyperapp::send::>(req) + .await + .map_err(|_| HttpServerError::Timeout)?; if resp.is_ok() { self.ws_paths.remove(&path); @@ -315,9 +332,9 @@ impl HttpServer { paths: Vec<&str>, config: HttpBindingConfig, ) -> Result<(), HttpServerError> { - use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; use crate::get_blob; - + use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; + let our = crate::our(); let req = KiRequest::to(("our", "vfs", "distro", "sys")) .body( @@ -333,7 +350,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let _res = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; + let _res = hyperapp::send::(req) + .await + .map_err(|_| HttpServerError::Timeout)?; let Some(mut blob) = get_blob() else { return Err(HttpServerError::NoBlob); @@ -343,7 +362,8 @@ impl HttpServer { blob.mime = Some(content_type); for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))).await?; + self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))) + .await?; } Ok(()) @@ -355,9 +375,9 @@ impl HttpServer { paths: Vec<&str>, config: HttpBindingConfig, ) -> Result<(), HttpServerError> { - use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; use crate::get_blob; - + use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; + let req = KiRequest::to(("our", "vfs", "distro", "sys")) .body( serde_json::to_vec(&VfsRequest { @@ -368,7 +388,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let _res = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; + let _res = hyperapp::send::(req) + .await + .map_err(|_| HttpServerError::Timeout)?; let Some(mut blob) = get_blob() else { return Err(HttpServerError::NoBlob); @@ -378,13 +400,13 @@ impl HttpServer { blob.mime = Some(content_type); for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))).await?; + self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))) + .await?; } Ok(()) } - pub async fn serve_ui( &mut self, directory: &str, @@ -410,7 +432,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let directory_body = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; + let directory_body = hyperapp::send::(req) + .await + .map_err(|_| HttpServerError::Timeout)?; let VfsResponse::ReadDir(directory_info) = directory_body else { return Err(HttpServerError::UnexpectedResponse); @@ -425,11 +449,17 @@ impl HttpServer { let relative_path = entry.path.replace(&initial_path, ""); let is_index = entry.path.ends_with("index.html"); - self.serve_file_raw_path(&entry.path, vec![relative_path.as_str()], config.clone()).await?; + self.serve_file_raw_path( + &entry.path, + vec![relative_path.as_str()], + config.clone(), + ) + .await?; if is_index { for root in &roots { - self.serve_file_raw_path(&entry.path, vec![root], config.clone()).await?; + self.serve_file_raw_path(&entry.path, vec![root], config.clone()) + .await?; } } } @@ -441,7 +471,11 @@ impl HttpServer { Ok(()) } - pub async fn unserve_ui(&mut self, directory: &str, roots: Vec<&str>) -> Result<(), HttpServerError> { + pub async fn unserve_ui( + &mut self, + directory: &str, + roots: Vec<&str>, + ) -> Result<(), HttpServerError> { use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; let our = crate::our(); @@ -461,7 +495,9 @@ impl HttpServer { ) .expects_response(self.timeout); - let directory_body = hyperapp::send::(req).await.map_err(|_| HttpServerError::Timeout)?; + let directory_body = hyperapp::send::(req) + .await + .map_err(|_| HttpServerError::Timeout)?; let VfsResponse::ReadDir(directory_info) = directory_body else { return Err(HttpServerError::UnexpectedResponse); @@ -520,5 +556,6 @@ fn get_mime_type(path: &str) -> String { "ico" => "image/x-icon", "wasm" => "application/wasm", _ => "application/octet-stream", - }.to_string() -} \ No newline at end of file + } + .to_string() +} diff --git a/src/http/server/server_sync.rs b/src/http/server/server_sync.rs index 61ce9ac..c551264 100644 --- a/src/http/server/server_sync.rs +++ b/src/http/server/server_sync.rs @@ -1,8 +1,13 @@ use crate::{ - get_blob, - http::server::{HttpBindingConfig, HttpResponse, HttpServer, HttpServerAction, HttpServerError, HttpServerRequest, IncomingHttpRequest, WsBindingConfig, WsMessageType, get_mime_type, ws_push_all_channels}, - last_blob, LazyLoadBlob as KiBlob, Message, Request as KiRequest, Response as KiResponse, + get_blob, + http::server::{ + get_mime_type, ws_push_all_channels, HttpBindingConfig, HttpResponse, HttpServer, + HttpServerAction, HttpServerError, HttpServerRequest, IncomingHttpRequest, WsBindingConfig, + WsMessageType, + }, + last_blob, vfs::{FileType, VfsAction, VfsRequest, VfsResponse}, + LazyLoadBlob as KiBlob, Message, Request as KiRequest, Response as KiResponse, }; use std::collections::{HashMap, HashSet}; @@ -638,4 +643,4 @@ impl HttpServer { Ok(()) } -} \ No newline at end of file +} diff --git a/src/hypermap.rs b/src/hypermap.rs index be920e7..5bf4aa5 100644 --- a/src/hypermap.rs +++ b/src/hypermap.rs @@ -1120,7 +1120,8 @@ impl Hypermap { hashed_data.to_vec(), &log_cache.metadata.created_by.parse::()?, signature_bytes, - ).await?) + ) + .await?) } #[cfg(not(feature = "hyperapp"))] @@ -1342,7 +1343,8 @@ impl Hypermap { from_block, filters, retry_params, chain, ), ); - let (block, consolidated_logs) = self.get_bootstrap(from_block, retry_params, chain).await?; + let (block, consolidated_logs) = + self.get_bootstrap(from_block, retry_params, chain).await?; if consolidated_logs.is_empty() { print_to_terminal(2,"bootstrap: No logs retrieved after consolidation. Returning empty results for filters."); diff --git a/src/kv.rs b/src/kv.rs index bb1f406..f4178b6 100644 --- a/src/kv.rs +++ b/src/kv.rs @@ -167,6 +167,3 @@ pub struct Kv { pub timeout: u64, _marker: PhantomData<(K, V)>, } - - - diff --git a/src/kv/kv_async.rs b/src/kv/kv_async.rs index bde64a9..f6d64c3 100644 --- a/src/kv/kv_async.rs +++ b/src/kv/kv_async.rs @@ -188,7 +188,11 @@ where } /// Removes and deletes a kv db. -pub async fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { +pub async fn remove_db( + package_id: PackageId, + db: &str, + timeout: Option, +) -> anyhow::Result<()> { let timeout = timeout.unwrap_or(5); let request = Request::new() @@ -219,7 +223,11 @@ pub async fn open_raw( } /// Opens or creates a kv db. -pub async fn open(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result> +pub async fn open( + package_id: PackageId, + db: &str, + timeout: Option, +) -> anyhow::Result> where K: Serialize + DeserializeOwned, V: Serialize + DeserializeOwned, @@ -247,4 +255,4 @@ where KvResponse::Err(error) => Err(error.into()), _ => Err(anyhow::anyhow!("kv: unexpected response")), } -} \ No newline at end of file +} diff --git a/src/kv/kv_sync.rs b/src/kv/kv_sync.rs index 2ab3dcf..a4fb404 100644 --- a/src/kv/kv_sync.rs +++ b/src/kv/kv_sync.rs @@ -1,5 +1,5 @@ use crate::{ - get_blob, + get_blob, kv::{Kv, KvAction, KvRequest, KvResponse}, Message, PackageId, Request, }; @@ -410,4 +410,4 @@ pub fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyho } _ => Err(anyhow::anyhow!("kv: unexpected message: {:?}", res)), } -} \ No newline at end of file +} diff --git a/src/net.rs b/src/net.rs index abbf2e9..6a10b5d 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,6 +1,6 @@ -use crate::{get_blob, Address, NodeId, Request}; #[cfg(not(feature = "hyperapp"))] use crate::SendError; +use crate::{get_blob, Address, NodeId, Request}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -262,7 +262,11 @@ where /// This function uses a 30-second timeout to reach `net:distro:sys`. If more /// control over the timeout is needed, create a [`Request`] directly. #[cfg(feature = "hyperapp")] -pub async fn verify(from: T, message: U, signature: V) -> Result +pub async fn verify( + from: T, + message: U, + signature: V, +) -> Result where T: Into
, U: Into>, diff --git a/src/sqlite.rs b/src/sqlite.rs index 5e4e9d5..f2d692d 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -185,4 +185,3 @@ pub struct Sqlite { pub db: String, pub timeout: u64, } - diff --git a/src/sqlite/sqlite_async.rs b/src/sqlite/sqlite_async.rs index 2645fc0..8caef73 100644 --- a/src/sqlite/sqlite_async.rs +++ b/src/sqlite/sqlite_async.rs @@ -27,14 +27,16 @@ impl Sqlite { match response { SqliteResponse::Read => { let blob = get_blob().ok_or_else(|| SqliteError::MalformedRequest)?; - let values = serde_json::from_slice::< - Vec>, - >(&blob.bytes) - .map_err(|_| SqliteError::MalformedRequest)?; + let values = + serde_json::from_slice::>>(&blob.bytes) + .map_err(|_| SqliteError::MalformedRequest)?; Ok(values) } SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), } } @@ -60,7 +62,10 @@ impl Sqlite { match response { SqliteResponse::Ok => Ok(()), SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), } } @@ -80,7 +85,10 @@ impl Sqlite { match response { SqliteResponse::BeginTx { tx_id } => Ok(tx_id), SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), } } @@ -100,7 +108,10 @@ impl Sqlite { match response { SqliteResponse::Ok => Ok(()), SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), } } } @@ -127,12 +138,19 @@ pub async fn open(package_id: PackageId, db: &str, timeout: Option) -> anyh timeout, }), SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), } } /// Remove and delete sqlite database. -pub async fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyhow::Result<()> { +pub async fn remove_db( + package_id: PackageId, + db: &str, + timeout: Option, +) -> anyhow::Result<()> { let timeout = timeout.unwrap_or(5); let request = Request::new() @@ -149,6 +167,9 @@ pub async fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> match response { SqliteResponse::Ok => Ok(()), SqliteResponse::Err(error) => Err(error.into()), - _ => Err(anyhow::anyhow!("sqlite: unexpected response {:?}", response)), + _ => Err(anyhow::anyhow!( + "sqlite: unexpected response {:?}", + response + )), } -} \ No newline at end of file +} diff --git a/src/sqlite/sqlite_sync.rs b/src/sqlite/sqlite_sync.rs index d1b4cb7..303bbaa 100644 --- a/src/sqlite/sqlite_sync.rs +++ b/src/sqlite/sqlite_sync.rs @@ -1,5 +1,5 @@ use crate::{ - get_blob, + get_blob, sqlite::{Sqlite, SqliteAction, SqliteError, SqliteRequest, SqliteResponse}, Message, PackageId, Request, }; @@ -199,4 +199,4 @@ pub fn remove_db(package_id: PackageId, db: &str, timeout: Option) -> anyho } _ => Err(anyhow::anyhow!("sqlite: unexpected message: {:?}", res)), } -} \ No newline at end of file +} diff --git a/src/vfs/directory/directory_async.rs b/src/vfs/directory/directory_async.rs index 373a173..3bc54d6 100644 --- a/src/vfs/directory/directory_async.rs +++ b/src/vfs/directory/directory_async.rs @@ -8,8 +8,10 @@ pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), Vf let timeout = timeout.unwrap_or(5); let request = vfs_request(path, VfsAction::RemoveDir).expects_response(timeout); - - let response = hyperapp::send::(request).await.map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + let response = hyperapp::send::(request) + .await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; match response { VfsResponse::Ok => Ok(()), @@ -19,4 +21,4 @@ pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), Vf path: path.to_string(), }), } -} \ No newline at end of file +} diff --git a/src/vfs/file/file_async.rs b/src/vfs/file/file_async.rs index a84aed2..2d361c0 100644 --- a/src/vfs/file/file_async.rs +++ b/src/vfs/file/file_async.rs @@ -8,8 +8,10 @@ pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), V let timeout = timeout.unwrap_or(5); let request = vfs_request(path, VfsAction::RemoveFile).expects_response(timeout); - - let response = hyperapp::send::(request).await.map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; + + let response = hyperapp::send::(request) + .await + .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; match response { VfsResponse::Ok => Ok(()), @@ -19,4 +21,4 @@ pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), V path: path.to_string(), }), } -} \ No newline at end of file +} diff --git a/src/vfs/mod.rs b/src/vfs/mod.rs index e2908bd..069eb8a 100644 --- a/src/vfs/mod.rs +++ b/src/vfs/mod.rs @@ -149,8 +149,9 @@ pub async fn metadata(path: &str, timeout: Option) -> Result(request).await + + let response = crate::hyperapp::send::(request) + .await .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; match response { From 6a6b413bf224b76fa93399fd543a262714f8eeaf Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 12 Aug 2025 15:26:14 -0700 Subject: [PATCH 12/33] hyperapp: remove an annoying `.await`er --- src/hyperapp.rs | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 15a4328..7cd0439 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -390,7 +390,6 @@ where } } -#[cfg(not(feature = "hyperapp"))] pub fn setup_server( ui_config: Option<&HttpBindingConfig>, endpoints: &[Binding], @@ -433,51 +432,6 @@ pub fn setup_server( server } -#[cfg(feature = "hyperapp")] -pub async fn setup_server( - ui_config: Option<&HttpBindingConfig>, - endpoints: &[Binding], -) -> http::server::HttpServer { - let mut server = http::server::HttpServer::new(5); - - if let Some(ui) = ui_config { - if let Err(e) = server.serve_ui("ui", vec!["/"], ui.clone()).await { - panic!("failed to serve UI: {e}. Make sure that a ui folder is in /pkg"); - } - } - - // Verify no duplicate paths - let mut seen_paths = std::collections::HashSet::new(); - for endpoint in endpoints.iter() { - let path = match endpoint { - Binding::Http { path, .. } => path, - Binding::Ws { path, .. } => path, - }; - if !seen_paths.insert(path) { - panic!("duplicate path found: {}", path); - } - } - - for endpoint in endpoints { - match endpoint { - Binding::Http { path, config } => { - server - .bind_http_path(path.to_string(), config.clone()) - .await - .expect("failed to serve API path"); - } - Binding::Ws { path, config } => { - server - .bind_ws_path(path.to_string(), config.clone()) - .await - .expect("failed to bind WS path"); - } - } - } - - server -} - /// Pretty prints a SendError in a more readable format pub fn pretty_print_send_error(error: &SendError) { let kind = &error.kind; From c27a881e764920636ac025112533a714d622793c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 12 Aug 2025 15:37:44 -0700 Subject: [PATCH 13/33] de-async-ify http server --- src/http/server.rs | 665 +++++++++++++++++++++++++++++++- src/http/server/server_async.rs | 561 --------------------------- src/http/server/server_sync.rs | 646 ------------------------------- 3 files changed, 649 insertions(+), 1223 deletions(-) delete mode 100644 src/http/server/server_async.rs delete mode 100644 src/http/server/server_sync.rs diff --git a/src/http/server.rs b/src/http/server.rs index b9382d6..18bb940 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -1,15 +1,14 @@ -use crate::{LazyLoadBlob as KiBlob, Request as KiRequest, Response as KiResponse}; +use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; +use crate::{ + get_blob, last_blob, LazyLoadBlob as KiBlob, Message, Request as KiRequest, + Response as KiResponse, +}; pub use http::StatusCode; use http::{HeaderMap, HeaderName, HeaderValue}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use thiserror::Error; -#[cfg(feature = "hyperapp")] -mod server_async; -#[cfg(not(feature = "hyperapp"))] -mod server_sync; - /// [`crate::Request`] received from the `http-server:distro:sys` service as a /// result of either an HTTP or WebSocket binding, created via [`HttpServerAction`]. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -297,10 +296,10 @@ pub enum MessageType { /// A representation of the HTTP server as configured by your process. #[derive(Clone, Debug)] pub struct HttpServer { - pub(crate) http_paths: HashMap, - pub(crate) ws_paths: HashMap, + http_paths: HashMap, + ws_paths: HashMap, /// A mapping of WebSocket paths to the channels that are open on them. - pub(crate) ws_channels: HashMap>, + ws_channels: HashMap>, /// The timeout given for `http-server:distro:sys` to respond to a configuration request. pub timeout: u64, } @@ -325,10 +324,10 @@ pub struct HttpServer { /// will require the user to be logged in separately to the general domain authentication. #[derive(Clone, Debug)] pub struct HttpBindingConfig { - pub(crate) authenticated: bool, - pub(crate) local_only: bool, - pub(crate) secure_subdomain: bool, - pub(crate) static_content: Option, + authenticated: bool, + local_only: bool, + secure_subdomain: bool, + static_content: Option, } impl HttpBindingConfig { @@ -400,9 +399,9 @@ impl HttpBindingConfig { /// not use the WebSocket extension protocol to connect with a runtime extension. #[derive(Clone, Copy, Debug)] pub struct WsBindingConfig { - pub(crate) authenticated: bool, - pub(crate) secure_subdomain: bool, - pub(crate) extension: bool, + authenticated: bool, + secure_subdomain: bool, + extension: bool, } impl WsBindingConfig { @@ -445,6 +444,640 @@ impl WsBindingConfig { } } +impl HttpServer { + /// Create a new HttpServer with the given timeout. + pub fn new(timeout: u64) -> Self { + Self { + http_paths: HashMap::new(), + ws_paths: HashMap::new(), + ws_channels: HashMap::new(), + timeout, + } + } + + /// Register a new path with the HTTP server configured using [`HttpBindingConfig`]. + pub fn bind_http_path( + &mut self, + path: T, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let cache = config.static_content.is_some(); + let req = KiRequest::to(("our", "http-server", "distro", "sys")).body( + serde_json::to_vec(&if config.secure_subdomain { + HttpServerAction::SecureBind { + path: path.clone(), + cache, + } + } else { + HttpServerAction::Bind { + path: path.clone(), + authenticated: config.authenticated, + local_only: config.local_only, + cache, + } + }) + .unwrap(), + ); + let res = match config.static_content.clone() { + Some(static_content) => req + .blob(static_content) + .send_and_await_response(self.timeout), + None => req.send_and_await_response(self.timeout), + }; + let Ok(Message::Response { body, .. }) = res.unwrap() else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.insert(path, config); + } + resp + } + + /// Register a new path with the HTTP server configured using [`WsBindingConfig`]. + pub fn bind_ws_path( + &mut self, + path: T, + config: WsBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(if config.secure_subdomain { + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.clone(), + extension: config.extension, + }) + .unwrap() + } else { + serde_json::to_vec(&HttpServerAction::WebSocketBind { + path: path.clone(), + authenticated: config.authenticated, + extension: config.extension, + }) + .unwrap() + }) + .send_and_await_response(self.timeout); + let Ok(Message::Response { body, .. }) = res.unwrap() else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.ws_paths.insert(path, config); + } + resp + } + + /// Register a new path with the HTTP server, and serve a static file from it. + /// The server will respond to GET requests on this path with the given file. + pub fn bind_http_static_path( + &mut self, + path: T, + authenticated: bool, + local_only: bool, + content_type: Option, + content: Vec, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::Bind { + path: path.clone(), + authenticated, + local_only, + cache: true, + }) + .unwrap(), + ) + .blob(crate::hyperware::process::standard::LazyLoadBlob { + mime: content_type.clone(), + bytes: content.clone(), + }) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.insert( + path, + HttpBindingConfig { + authenticated, + local_only, + secure_subdomain: false, + static_content: Some(KiBlob { + mime: content_type, + bytes: content, + }), + }, + ); + } + resp + } + + /// Register a new path with the HTTP server. This will cause the HTTP server to + /// forward any requests on this path to the calling process. + /// + /// Instead of binding at just a path, this function tells the HTTP server to + /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric + /// characters converted to `-`, although will not be needed if package ID is + /// a genuine hypermap entry) and bind at that subdomain. + pub fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::SecureBind { + path: path.clone(), + cache: false, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.insert( + path, + HttpBindingConfig { + authenticated: true, + local_only: false, + secure_subdomain: true, + static_content: None, + }, + ); + } + resp + } + + /// Register a new WebSocket path with the HTTP server. Any client connections + /// made on this path will be forwarded to this process. + /// + /// Instead of binding at just a path, this function tells the HTTP server to + /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric + /// characters converted to `-`, although will not be needed if package ID is + /// a genuine hypermap entry) and bind at that subdomain. + pub fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.clone(), + extension: false, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout); + let Ok(Message::Response { body, .. }) = res.unwrap() else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.ws_paths.insert( + path, + WsBindingConfig { + authenticated: true, + secure_subdomain: true, + extension: false, + }, + ); + } + resp + } + + /// Modify a previously-bound HTTP path. + pub fn modify_http_path( + &mut self, + path: &str, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> + where + T: Into, + { + let entry = self + .http_paths + .get_mut(path) + .ok_or(HttpServerError::MalformedRequest)?; + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::Bind { + path: path.to_string(), + authenticated: config.authenticated, + local_only: config.local_only, + cache: true, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + entry.authenticated = config.authenticated; + entry.local_only = config.local_only; + entry.secure_subdomain = config.secure_subdomain; + entry.static_content = config.static_content; + } + resp + } + + /// Modify a previously-bound WS path + pub fn modify_ws_path( + &mut self, + path: &str, + config: WsBindingConfig, + ) -> Result<(), HttpServerError> { + let entry = self + .ws_paths + .get_mut(path) + .ok_or(HttpServerError::MalformedRequest)?; + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(if entry.secure_subdomain { + serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { + path: path.to_string(), + extension: config.extension, + }) + .unwrap() + } else { + serde_json::to_vec(&HttpServerAction::WebSocketBind { + path: path.to_string(), + authenticated: config.authenticated, + extension: config.extension, + }) + .unwrap() + }) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + entry.authenticated = config.authenticated; + entry.secure_subdomain = config.secure_subdomain; + entry.extension = config.extension; + } + resp + } + + /// Unbind a previously-bound HTTP path. + pub fn unbind_http_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.http_paths.remove(&path); + } + resp + } + + /// Unbind a previously-bound WebSocket path. + pub fn unbind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> + where + T: Into, + { + let path: String = path.into(); + let res = KiRequest::to(("our", "http-server", "distro", "sys")) + .body( + serde_json::to_vec(&HttpServerAction::WebSocketUnbind { path: path.clone() }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap(); + let Ok(Message::Response { body, .. }) = res else { + return Err(HttpServerError::Timeout); + }; + let Ok(resp) = serde_json::from_slice::>(&body) else { + return Err(HttpServerError::UnexpectedResponse); + }; + if resp.is_ok() { + self.ws_paths.remove(&path); + } + resp + } + + /// Serve a file from the given directory within our package drive at the given paths. + /// + /// The directory is relative to the `pkg` folder within this package's drive. + /// + /// The config `static_content` field will be ignored in favor of the file content. + /// An error will be returned if the file does not exist. + pub fn serve_file( + &mut self, + file_path: &str, + paths: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + let our = crate::our(); + let _res = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path: format!( + "/{}/pkg/{}", + our.package_id(), + file_path.trim_start_matches('/') + ), + action: VfsAction::Read, + }) + .map_err(|_| HttpServerError::MalformedRequest)?, + ) + .send_and_await_response(self.timeout) + .unwrap(); + + let Some(mut blob) = get_blob() else { + return Err(HttpServerError::NoBlob); + }; + + let content_type = get_mime_type(&file_path); + blob.mime = Some(content_type); + + for path in paths { + self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; + } + + Ok(()) + } + + /// Serve a file from the given absolute directory. + /// + /// The config `static_content` field will be ignored in favor of the file content. + /// An error will be returned if the file does not exist. + pub fn serve_file_raw_path( + &mut self, + file_path: &str, + paths: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + let _res = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path: file_path.to_string(), + action: VfsAction::Read, + }) + .map_err(|_| HttpServerError::MalformedRequest)?, + ) + .send_and_await_response(self.timeout) + .unwrap(); + + let Some(mut blob) = get_blob() else { + return Err(HttpServerError::NoBlob); + }; + + let content_type = get_mime_type(&file_path); + blob.mime = Some(content_type); + + for path in paths { + self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; + } + + Ok(()) + } + + /// Helper function to traverse a UI directory and apply an operation to each file. + /// This is used by both serve_ui and unserve_ui to avoid code duplication. + fn traverse_ui_directory( + &mut self, + directory: &str, + roots: &[&str], + mut file_handler: F, + ) -> Result<(), HttpServerError> + where + F: FnMut(&mut Self, &str, &[&str], bool) -> Result<(), HttpServerError>, + { + let our = crate::our(); + let initial_path = format!("{}/pkg/{}", our.package_id(), directory); + + let mut queue = std::collections::VecDeque::new(); + queue.push_back(initial_path.clone()); + + while let Some(path) = queue.pop_front() { + let Ok(directory_response) = KiRequest::to(("our", "vfs", "distro", "sys")) + .body( + serde_json::to_vec(&VfsRequest { + path, + action: VfsAction::ReadDir, + }) + .unwrap(), + ) + .send_and_await_response(self.timeout) + .unwrap() + else { + return Err(HttpServerError::MalformedRequest); + }; + + let directory_body = serde_json::from_slice::(directory_response.body()) + .map_err(|_e| HttpServerError::UnexpectedResponse)?; + + // determine if it's a file or a directory and handle appropriately + let VfsResponse::ReadDir(directory_info) = directory_body else { + return Err(HttpServerError::UnexpectedResponse); + }; + + for entry in directory_info { + match entry.file_type { + FileType::Directory => { + // push the directory onto the queue + queue.push_back(entry.path); + } + FileType::File => { + let relative_path = entry.path.replace(&initial_path, ""); + let is_index = entry.path.ends_with("index.html"); + + // Call the handler with the file path and whether it's an index file + file_handler(self, &entry.path, &[relative_path.as_str()], is_index)?; + + // If it's an index file, also handle the root paths + if is_index { + for root in roots { + file_handler(self, &entry.path, &[root], true)?; + } + } + } + _ => { + // ignore symlinks and other + } + } + } + } + + Ok(()) + } + + /// Serve static files from a given directory by binding all of them + /// in http-server to their filesystem path. + /// + /// The directory is relative to the `pkg` folder within this package's drive. + /// + /// The config `static_content` field will be ignored in favor of the files' contents. + /// An error will be returned if the file does not exist. + pub fn serve_ui( + &mut self, + directory: &str, + roots: Vec<&str>, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + self.traverse_ui_directory(directory, &roots, |server, file_path, paths, _is_index| { + server.serve_file_raw_path(file_path, paths.to_vec(), config.clone()) + }) + } + + /// Unserve static files from a given directory by unbinding all of them + /// from http-server that were previously bound by serve_ui. + /// + /// The directory is relative to the `pkg` folder within this package's drive. + /// + /// This mirrors the logic of serve_ui but calls unbind_http_path instead. + pub fn unserve_ui(&mut self, directory: &str, roots: Vec<&str>) -> Result<(), HttpServerError> { + self.traverse_ui_directory(directory, &roots, |server, _file_path, paths, _is_index| { + // Unbind each path that was bound + for path in paths { + server.unbind_http_path(*path)?; + } + Ok(()) + }) + } + + /// Handle a WebSocket open event from the HTTP server. + pub fn handle_websocket_open(&mut self, path: &str, channel_id: u32) { + self.ws_channels + .entry(path.to_string()) + .or_insert(HashSet::new()) + .insert(channel_id); + } + + /// Handle a WebSocket close event from the HTTP server. + pub fn handle_websocket_close(&mut self, channel_id: u32) { + self.ws_channels.iter_mut().for_each(|(_, channels)| { + channels.remove(&channel_id); + }); + } + + pub fn parse_request(&self, body: &[u8]) -> Result { + let request = serde_json::from_slice::(body) + .map_err(|_| HttpServerError::MalformedRequest)?; + Ok(request) + } + + /// Handle an incoming request from the HTTP server. + pub fn handle_request( + &mut self, + server_request: HttpServerRequest, + mut http_handler: impl FnMut(IncomingHttpRequest) -> (HttpResponse, Option), + mut ws_handler: impl FnMut(u32, WsMessageType, KiBlob), + ) { + match server_request { + HttpServerRequest::Http(http_request) => { + let (response, blob) = http_handler(http_request); + let response = KiResponse::new().body(serde_json::to_vec(&response).unwrap()); + if let Some(blob) = blob { + response.blob(blob).send().unwrap(); + } else { + response.send().unwrap(); + } + } + HttpServerRequest::WebSocketPush { + channel_id, + message_type, + } => ws_handler(channel_id, message_type, last_blob().unwrap_or_default()), + HttpServerRequest::WebSocketOpen { path, channel_id } => { + self.handle_websocket_open(&path, channel_id); + } + HttpServerRequest::WebSocketClose(channel_id) => { + self.handle_websocket_close(channel_id); + } + } + } + + /// Push a WebSocket message to all channels on a given path. + pub fn ws_push_all_channels(&self, path: &str, message_type: WsMessageType, blob: KiBlob) { + ws_push_all_channels(&self.ws_channels, path, message_type, blob); + } + + pub fn get_ws_channels(&self) -> HashMap> { + self.ws_channels.clone() + } + + /// Register multiple paths with the HTTP server using the same configuration. + /// The security setting is determined by the `secure_subdomain` field in `HttpBindingConfig`. + /// All paths must be bound successfully, or none will be bound. If any path + /// fails to bind, all previously bound paths will be unbound before returning + /// the error. + pub fn bind_multiple_http_paths>( + &mut self, + paths: Vec, + config: HttpBindingConfig, + ) -> Result<(), HttpServerError> { + let mut bound_paths = Vec::new(); + + for path in paths { + let path_str = path.into(); + let result = match config.secure_subdomain { + true => self.secure_bind_http_path(path_str.clone()), + false => self.bind_http_path(path_str.clone(), config.clone()), + }; + + match result { + // If binding succeeds, add the path to the list of bound paths + Ok(_) => bound_paths.push(path_str), + // If binding fails, unbind all previously bound paths + Err(e) => { + for bound_path in bound_paths { + let _ = self.unbind_http_path(&bound_path); + } + return Err(e); + } + } + } + + Ok(()) + } +} + /// Send an HTTP response to an incoming HTTP request ([`HttpServerRequest::Http`]). pub fn send_response(status: StatusCode, headers: Option>, body: Vec) { KiResponse::new() diff --git a/src/http/server/server_async.rs b/src/http/server/server_async.rs deleted file mode 100644 index 1d11dc2..0000000 --- a/src/http/server/server_async.rs +++ /dev/null @@ -1,561 +0,0 @@ -use crate::{ - http::server::{ - HttpBindingConfig, HttpServer, HttpServerAction, HttpServerError, WsBindingConfig, - }, - hyperapp, LazyLoadBlob as KiBlob, Request as KiRequest, -}; -use std::collections::{HashMap, HashSet}; - -impl HttpServer { - pub fn new(timeout: u64) -> Self { - Self { - http_paths: HashMap::new(), - ws_paths: HashMap::new(), - ws_channels: HashMap::new(), - timeout, - } - } - - pub async fn bind_http_path( - &mut self, - path: T, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let cache = config.static_content.is_some(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&if config.secure_subdomain { - HttpServerAction::SecureBind { - path: path.clone(), - cache, - } - } else { - HttpServerAction::Bind { - path: path.clone(), - authenticated: config.authenticated, - local_only: config.local_only, - cache, - } - }) - .unwrap(), - ) - .expects_response(self.timeout); - - let req = match config.static_content.clone() { - Some(static_content) => req.blob(static_content), - None => req, - }; - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.http_paths.insert(path, config); - } - resp - } - - pub async fn bind_ws_path( - &mut self, - path: T, - config: WsBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(if config.secure_subdomain { - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.clone(), - extension: config.extension, - }) - .unwrap() - } else { - serde_json::to_vec(&HttpServerAction::WebSocketBind { - path: path.clone(), - authenticated: config.authenticated, - extension: config.extension, - }) - .unwrap() - }) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.ws_paths.insert(path, config); - } - resp - } - - pub async fn bind_http_static_path( - &mut self, - path: T, - authenticated: bool, - local_only: bool, - content_type: Option, - content: Vec, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::Bind { - path: path.clone(), - authenticated, - local_only, - cache: true, - }) - .unwrap(), - ) - .blob(crate::hyperware::process::standard::LazyLoadBlob { - mime: content_type.clone(), - bytes: content.clone(), - }) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.http_paths.insert( - path, - HttpBindingConfig { - authenticated, - local_only, - secure_subdomain: false, - static_content: Some(KiBlob { - mime: content_type, - bytes: content, - }), - }, - ); - } - resp - } - - pub async fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::SecureBind { - path: path.clone(), - cache: false, - }) - .unwrap(), - ) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.http_paths.insert( - path, - HttpBindingConfig { - authenticated: true, - local_only: false, - secure_subdomain: true, - static_content: None, - }, - ); - } - resp - } - - pub async fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.clone(), - extension: false, - }) - .unwrap(), - ) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.ws_paths.insert( - path, - WsBindingConfig { - authenticated: true, - secure_subdomain: true, - extension: false, - }, - ); - } - resp - } - - pub async fn modify_http_path( - &mut self, - path: &str, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let entry = self - .http_paths - .get_mut(path) - .ok_or(HttpServerError::MalformedRequest)?; - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::Bind { - path: path.to_string(), - authenticated: config.authenticated, - local_only: config.local_only, - cache: true, - }) - .unwrap(), - ) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - entry.authenticated = config.authenticated; - entry.local_only = config.local_only; - entry.secure_subdomain = config.secure_subdomain; - entry.static_content = config.static_content; - } - resp - } - - pub async fn modify_ws_path( - &mut self, - path: &str, - config: WsBindingConfig, - ) -> Result<(), HttpServerError> { - let entry = self - .ws_paths - .get_mut(path) - .ok_or(HttpServerError::MalformedRequest)?; - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(if entry.secure_subdomain { - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.to_string(), - extension: config.extension, - }) - .unwrap() - } else { - serde_json::to_vec(&HttpServerAction::WebSocketBind { - path: path.to_string(), - authenticated: config.authenticated, - extension: config.extension, - }) - .unwrap() - }) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - entry.authenticated = config.authenticated; - entry.secure_subdomain = config.secure_subdomain; - entry.extension = config.extension; - } - resp - } - - pub async fn unbind_http_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.http_paths.remove(&path); - } - resp - } - - pub async fn unbind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::WebSocketUnbind { path: path.clone() }) - .unwrap(), - ) - .expects_response(self.timeout); - - let resp = hyperapp::send::>(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - if resp.is_ok() { - self.ws_paths.remove(&path); - } - resp - } - - pub async fn serve_file( - &mut self, - file_path: &str, - paths: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - use crate::get_blob; - use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; - - let our = crate::our(); - let req = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path: format!( - "/{}/pkg/{}", - our.package_id(), - file_path.trim_start_matches('/') - ), - action: VfsAction::Read, - }) - .map_err(|_| HttpServerError::MalformedRequest)?, - ) - .expects_response(self.timeout); - - let _res = hyperapp::send::(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - let Some(mut blob) = get_blob() else { - return Err(HttpServerError::NoBlob); - }; - - let content_type = get_mime_type(&file_path); - blob.mime = Some(content_type); - - for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))) - .await?; - } - - Ok(()) - } - - pub async fn serve_file_raw_path( - &mut self, - file_path: &str, - paths: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - use crate::get_blob; - use crate::vfs::{VfsAction, VfsRequest, VfsResponse}; - - let req = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path: file_path.to_string(), - action: VfsAction::Read, - }) - .map_err(|_| HttpServerError::MalformedRequest)?, - ) - .expects_response(self.timeout); - - let _res = hyperapp::send::(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - let Some(mut blob) = get_blob() else { - return Err(HttpServerError::NoBlob); - }; - - let content_type = get_mime_type(&file_path); - blob.mime = Some(content_type); - - for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone()))) - .await?; - } - - Ok(()) - } - - pub async fn serve_ui( - &mut self, - directory: &str, - roots: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; - - let our = crate::our(); - let initial_path = format!("{}/pkg/{}", our.package_id(), directory); - - let mut queue = std::collections::VecDeque::new(); - queue.push_back(initial_path.clone()); - - while let Some(path) = queue.pop_front() { - let req = crate::Request::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path, - action: VfsAction::ReadDir, - }) - .unwrap(), - ) - .expects_response(self.timeout); - - let directory_body = hyperapp::send::(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - let VfsResponse::ReadDir(directory_info) = directory_body else { - return Err(HttpServerError::UnexpectedResponse); - }; - - for entry in directory_info { - match entry.file_type { - FileType::Directory => { - queue.push_back(entry.path); - } - FileType::File => { - let relative_path = entry.path.replace(&initial_path, ""); - let is_index = entry.path.ends_with("index.html"); - - self.serve_file_raw_path( - &entry.path, - vec![relative_path.as_str()], - config.clone(), - ) - .await?; - - if is_index { - for root in &roots { - self.serve_file_raw_path(&entry.path, vec![root], config.clone()) - .await?; - } - } - } - _ => {} - } - } - } - - Ok(()) - } - - pub async fn unserve_ui( - &mut self, - directory: &str, - roots: Vec<&str>, - ) -> Result<(), HttpServerError> { - use crate::vfs::{FileType, VfsAction, VfsRequest, VfsResponse}; - - let our = crate::our(); - let initial_path = format!("{}/pkg/{}", our.package_id(), directory); - - let mut queue = std::collections::VecDeque::new(); - queue.push_back(initial_path.clone()); - - while let Some(path) = queue.pop_front() { - let req = crate::Request::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path, - action: VfsAction::ReadDir, - }) - .unwrap(), - ) - .expects_response(self.timeout); - - let directory_body = hyperapp::send::(req) - .await - .map_err(|_| HttpServerError::Timeout)?; - - let VfsResponse::ReadDir(directory_info) = directory_body else { - return Err(HttpServerError::UnexpectedResponse); - }; - - for entry in directory_info { - match entry.file_type { - FileType::Directory => { - queue.push_back(entry.path); - } - FileType::File => { - let relative_path = entry.path.replace(&initial_path, ""); - let is_index = entry.path.ends_with("index.html"); - - self.unbind_http_path(relative_path.as_str()).await?; - - if is_index { - for root in &roots { - self.unbind_http_path(*root).await?; - } - } - } - _ => {} - } - } - } - - Ok(()) - } - - pub fn handle_websocket_open(&mut self, path: &str, channel_id: u32) { - self.ws_channels - .entry(path.to_string()) - .or_insert(HashSet::new()) - .insert(channel_id); - } - - pub fn handle_websocket_close(&mut self, channel_id: u32) { - self.ws_channels.iter_mut().for_each(|(_, channels)| { - channels.remove(&channel_id); - }); - } -} - -fn get_mime_type(path: &str) -> String { - let ext = path.split('.').last().unwrap_or(""); - match ext { - "html" => "text/html", - "css" => "text/css", - "js" => "application/javascript", - "json" => "application/json", - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "svg" => "image/svg+xml", - "ico" => "image/x-icon", - "wasm" => "application/wasm", - _ => "application/octet-stream", - } - .to_string() -} diff --git a/src/http/server/server_sync.rs b/src/http/server/server_sync.rs deleted file mode 100644 index c551264..0000000 --- a/src/http/server/server_sync.rs +++ /dev/null @@ -1,646 +0,0 @@ -use crate::{ - get_blob, - http::server::{ - get_mime_type, ws_push_all_channels, HttpBindingConfig, HttpResponse, HttpServer, - HttpServerAction, HttpServerError, HttpServerRequest, IncomingHttpRequest, WsBindingConfig, - WsMessageType, - }, - last_blob, - vfs::{FileType, VfsAction, VfsRequest, VfsResponse}, - LazyLoadBlob as KiBlob, Message, Request as KiRequest, Response as KiResponse, -}; -use std::collections::{HashMap, HashSet}; - -impl HttpServer { - /// Create a new HttpServer with the given timeout. - pub fn new(timeout: u64) -> Self { - Self { - http_paths: HashMap::new(), - ws_paths: HashMap::new(), - ws_channels: HashMap::new(), - timeout, - } - } - - /// Register a new path with the HTTP server configured using [`HttpBindingConfig`]. - pub fn bind_http_path( - &mut self, - path: T, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let cache = config.static_content.is_some(); - let req = KiRequest::to(("our", "http-server", "distro", "sys")).body( - serde_json::to_vec(&if config.secure_subdomain { - HttpServerAction::SecureBind { - path: path.clone(), - cache, - } - } else { - HttpServerAction::Bind { - path: path.clone(), - authenticated: config.authenticated, - local_only: config.local_only, - cache, - } - }) - .unwrap(), - ); - let res = match config.static_content.clone() { - Some(static_content) => req - .blob(static_content) - .send_and_await_response(self.timeout), - None => req.send_and_await_response(self.timeout), - }; - let Ok(Message::Response { body, .. }) = res.unwrap() else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.insert(path, config); - } - resp - } - - /// Register a new path with the HTTP server configured using [`WsBindingConfig`]. - pub fn bind_ws_path( - &mut self, - path: T, - config: WsBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(if config.secure_subdomain { - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.clone(), - extension: config.extension, - }) - .unwrap() - } else { - serde_json::to_vec(&HttpServerAction::WebSocketBind { - path: path.clone(), - authenticated: config.authenticated, - extension: config.extension, - }) - .unwrap() - }) - .send_and_await_response(self.timeout); - let Ok(Message::Response { body, .. }) = res.unwrap() else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.ws_paths.insert(path, config); - } - resp - } - - /// Register a new path with the HTTP server, and serve a static file from it. - /// The server will respond to GET requests on this path with the given file. - pub fn bind_http_static_path( - &mut self, - path: T, - authenticated: bool, - local_only: bool, - content_type: Option, - content: Vec, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::Bind { - path: path.clone(), - authenticated, - local_only, - cache: true, - }) - .unwrap(), - ) - .blob(crate::hyperware::process::standard::LazyLoadBlob { - mime: content_type.clone(), - bytes: content.clone(), - }) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.insert( - path, - HttpBindingConfig { - authenticated, - local_only, - secure_subdomain: false, - static_content: Some(KiBlob { - mime: content_type, - bytes: content, - }), - }, - ); - } - resp - } - - /// Register a new path with the HTTP server. This will cause the HTTP server to - /// forward any requests on this path to the calling process. - /// - /// Instead of binding at just a path, this function tells the HTTP server to - /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric - /// characters converted to `-`, although will not be needed if package ID is - /// a genuine hypermap entry) and bind at that subdomain. - pub fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::SecureBind { - path: path.clone(), - cache: false, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.insert( - path, - HttpBindingConfig { - authenticated: true, - local_only: false, - secure_subdomain: true, - static_content: None, - }, - ); - } - resp - } - - /// Register a new WebSocket path with the HTTP server. Any client connections - /// made on this path will be forwarded to this process. - /// - /// Instead of binding at just a path, this function tells the HTTP server to - /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric - /// characters converted to `-`, although will not be needed if package ID is - /// a genuine hypermap entry) and bind at that subdomain. - pub fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.clone(), - extension: false, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout); - let Ok(Message::Response { body, .. }) = res.unwrap() else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.ws_paths.insert( - path, - WsBindingConfig { - authenticated: true, - secure_subdomain: true, - extension: false, - }, - ); - } - resp - } - - /// Modify a previously-bound HTTP path. - pub fn modify_http_path( - &mut self, - path: &str, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> - where - T: Into, - { - let entry = self - .http_paths - .get_mut(path) - .ok_or(HttpServerError::MalformedRequest)?; - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::Bind { - path: path.to_string(), - authenticated: config.authenticated, - local_only: config.local_only, - cache: true, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - entry.authenticated = config.authenticated; - entry.local_only = config.local_only; - entry.secure_subdomain = config.secure_subdomain; - entry.static_content = config.static_content; - } - resp - } - - /// Modify a previously-bound WS path - pub fn modify_ws_path( - &mut self, - path: &str, - config: WsBindingConfig, - ) -> Result<(), HttpServerError> { - let entry = self - .ws_paths - .get_mut(path) - .ok_or(HttpServerError::MalformedRequest)?; - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(if entry.secure_subdomain { - serde_json::to_vec(&HttpServerAction::WebSocketSecureBind { - path: path.to_string(), - extension: config.extension, - }) - .unwrap() - } else { - serde_json::to_vec(&HttpServerAction::WebSocketBind { - path: path.to_string(), - authenticated: config.authenticated, - extension: config.extension, - }) - .unwrap() - }) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - entry.authenticated = config.authenticated; - entry.secure_subdomain = config.secure_subdomain; - entry.extension = config.extension; - } - resp - } - - /// Unbind a previously-bound HTTP path. - pub fn unbind_http_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body(serde_json::to_vec(&HttpServerAction::Unbind { path: path.clone() }).unwrap()) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.http_paths.remove(&path); - } - resp - } - - /// Unbind a previously-bound WebSocket path. - pub fn unbind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> - where - T: Into, - { - let path: String = path.into(); - let res = KiRequest::to(("our", "http-server", "distro", "sys")) - .body( - serde_json::to_vec(&HttpServerAction::WebSocketUnbind { path: path.clone() }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap(); - let Ok(Message::Response { body, .. }) = res else { - return Err(HttpServerError::Timeout); - }; - let Ok(resp) = serde_json::from_slice::>(&body) else { - return Err(HttpServerError::UnexpectedResponse); - }; - if resp.is_ok() { - self.ws_paths.remove(&path); - } - resp - } - - /// Serve a file from the given directory within our package drive at the given paths. - /// - /// The directory is relative to the `pkg` folder within this package's drive. - /// - /// The config `static_content` field will be ignored in favor of the file content. - /// An error will be returned if the file does not exist. - pub fn serve_file( - &mut self, - file_path: &str, - paths: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - let our = crate::our(); - let _res = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path: format!( - "/{}/pkg/{}", - our.package_id(), - file_path.trim_start_matches('/') - ), - action: VfsAction::Read, - }) - .map_err(|_| HttpServerError::MalformedRequest)?, - ) - .send_and_await_response(self.timeout) - .unwrap(); - - let Some(mut blob) = get_blob() else { - return Err(HttpServerError::NoBlob); - }; - - let content_type = get_mime_type(&file_path); - blob.mime = Some(content_type); - - for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; - } - - Ok(()) - } - - /// Serve a file from the given absolute directory. - /// - /// The config `static_content` field will be ignored in favor of the file content. - /// An error will be returned if the file does not exist. - pub fn serve_file_raw_path( - &mut self, - file_path: &str, - paths: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - let _res = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path: file_path.to_string(), - action: VfsAction::Read, - }) - .map_err(|_| HttpServerError::MalformedRequest)?, - ) - .send_and_await_response(self.timeout) - .unwrap(); - - let Some(mut blob) = get_blob() else { - return Err(HttpServerError::NoBlob); - }; - - let content_type = get_mime_type(&file_path); - blob.mime = Some(content_type); - - for path in paths { - self.bind_http_path(path, config.clone().static_content(Some(blob.clone())))?; - } - - Ok(()) - } - - /// Helper function to traverse a UI directory and apply an operation to each file. - /// This is used by both serve_ui and unserve_ui to avoid code duplication. - fn traverse_ui_directory( - &mut self, - directory: &str, - roots: &[&str], - mut file_handler: F, - ) -> Result<(), HttpServerError> - where - F: FnMut(&mut Self, &str, &[&str], bool) -> Result<(), HttpServerError>, - { - let our = crate::our(); - let initial_path = format!("{}/pkg/{}", our.package_id(), directory); - - let mut queue = std::collections::VecDeque::new(); - queue.push_back(initial_path.clone()); - - while let Some(path) = queue.pop_front() { - let Ok(directory_response) = KiRequest::to(("our", "vfs", "distro", "sys")) - .body( - serde_json::to_vec(&VfsRequest { - path, - action: VfsAction::ReadDir, - }) - .unwrap(), - ) - .send_and_await_response(self.timeout) - .unwrap() - else { - return Err(HttpServerError::MalformedRequest); - }; - - let directory_body = serde_json::from_slice::(directory_response.body()) - .map_err(|_e| HttpServerError::UnexpectedResponse)?; - - // determine if it's a file or a directory and handle appropriately - let VfsResponse::ReadDir(directory_info) = directory_body else { - return Err(HttpServerError::UnexpectedResponse); - }; - - for entry in directory_info { - match entry.file_type { - FileType::Directory => { - // push the directory onto the queue - queue.push_back(entry.path); - } - FileType::File => { - let relative_path = entry.path.replace(&initial_path, ""); - let is_index = entry.path.ends_with("index.html"); - - // Call the handler with the file path and whether it's an index file - file_handler(self, &entry.path, &[relative_path.as_str()], is_index)?; - - // If it's an index file, also handle the root paths - if is_index { - for root in roots { - file_handler(self, &entry.path, &[root], true)?; - } - } - } - _ => { - // ignore symlinks and other - } - } - } - } - - Ok(()) - } - - /// Serve static files from a given directory by binding all of them - /// in http-server to their filesystem path. - /// - /// The directory is relative to the `pkg` folder within this package's drive. - /// - /// The config `static_content` field will be ignored in favor of the files' contents. - /// An error will be returned if the file does not exist. - pub fn serve_ui( - &mut self, - directory: &str, - roots: Vec<&str>, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - self.traverse_ui_directory(directory, &roots, |server, file_path, paths, _is_index| { - server.serve_file_raw_path(file_path, paths.to_vec(), config.clone()) - }) - } - - /// Unserve static files from a given directory by unbinding all of them - /// from http-server that were previously bound by serve_ui. - /// - /// The directory is relative to the `pkg` folder within this package's drive. - /// - /// This mirrors the logic of serve_ui but calls unbind_http_path instead. - pub fn unserve_ui(&mut self, directory: &str, roots: Vec<&str>) -> Result<(), HttpServerError> { - self.traverse_ui_directory(directory, &roots, |server, _file_path, paths, _is_index| { - // Unbind each path that was bound - for path in paths { - server.unbind_http_path(*path)?; - } - Ok(()) - }) - } - - /// Handle a WebSocket open event from the HTTP server. - pub fn handle_websocket_open(&mut self, path: &str, channel_id: u32) { - self.ws_channels - .entry(path.to_string()) - .or_insert(HashSet::new()) - .insert(channel_id); - } - - /// Handle a WebSocket close event from the HTTP server. - pub fn handle_websocket_close(&mut self, channel_id: u32) { - self.ws_channels.iter_mut().for_each(|(_, channels)| { - channels.remove(&channel_id); - }); - } - - pub fn parse_request(&self, body: &[u8]) -> Result { - let request = serde_json::from_slice::(body) - .map_err(|_| HttpServerError::MalformedRequest)?; - Ok(request) - } - - /// Handle an incoming request from the HTTP server. - pub fn handle_request( - &mut self, - server_request: HttpServerRequest, - mut http_handler: impl FnMut(IncomingHttpRequest) -> (HttpResponse, Option), - mut ws_handler: impl FnMut(u32, WsMessageType, KiBlob), - ) { - match server_request { - HttpServerRequest::Http(http_request) => { - let (response, blob) = http_handler(http_request); - let response = KiResponse::new().body(serde_json::to_vec(&response).unwrap()); - if let Some(blob) = blob { - response.blob(blob).send().unwrap(); - } else { - response.send().unwrap(); - } - } - HttpServerRequest::WebSocketPush { - channel_id, - message_type, - } => ws_handler(channel_id, message_type, last_blob().unwrap_or_default()), - HttpServerRequest::WebSocketOpen { path, channel_id } => { - self.handle_websocket_open(&path, channel_id); - } - HttpServerRequest::WebSocketClose(channel_id) => { - self.handle_websocket_close(channel_id); - } - } - } - - /// Push a WebSocket message to all channels on a given path. - pub fn ws_push_all_channels(&self, path: &str, message_type: WsMessageType, blob: KiBlob) { - ws_push_all_channels(&self.ws_channels, path, message_type, blob); - } - - pub fn get_ws_channels(&self) -> HashMap> { - self.ws_channels.clone() - } - - /// Register multiple paths with the HTTP server using the same configuration. - /// The security setting is determined by the `secure_subdomain` field in `HttpBindingConfig`. - /// All paths must be bound successfully, or none will be bound. If any path - /// fails to bind, all previously bound paths will be unbound before returning - /// the error. - pub fn bind_multiple_http_paths>( - &mut self, - paths: Vec, - config: HttpBindingConfig, - ) -> Result<(), HttpServerError> { - let mut bound_paths = Vec::new(); - - for path in paths { - let path_str = path.into(); - let result = match config.secure_subdomain { - true => self.secure_bind_http_path(path_str.clone()), - false => self.bind_http_path(path_str.clone(), config.clone()), - }; - - match result { - // If binding succeeds, add the path to the list of bound paths - Ok(_) => bound_paths.push(path_str), - // If binding fails, unbind all previously bound paths - Err(e) => { - for bound_path in bound_paths { - let _ = self.unbind_http_path(&bound_path); - } - return Err(e); - } - } - } - - Ok(()) - } -} From 13a4bda39bf2f17889fa150198076f853439b1e6 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 12 Aug 2025 20:36:01 -0700 Subject: [PATCH 14/33] bump version to 2.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69ece44..87c55d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,7 +1617,7 @@ dependencies = [ [[package]] name = "hyperware_process_lib" -version = "2.1.0" +version = "2.2.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 73bac30..c5d4d8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperware_process_lib" authors = ["Sybil Technologies AG"] -version = "2.1.0" +version = "2.2.0" edition = "2021" description = "A library for writing Hyperware processes in Rust." homepage = "https://hyperware.ai" From 253575e0596ee07937232f2f33491496dc1df425 Mon Sep 17 00:00:00 2001 From: Hallmane Date: Sat, 23 Aug 2025 05:44:17 +0200 Subject: [PATCH 15/33] increase await timer to not cut hyperwallet operations off --- src/hyperwallet_client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperwallet_client/mod.rs b/src/hyperwallet_client/mod.rs index adad227..4a1f38c 100644 --- a/src/hyperwallet_client/mod.rs +++ b/src/hyperwallet_client/mod.rs @@ -159,7 +159,7 @@ pub(crate) fn send_message( // Use local address pattern like HTTP client - hyperwallet is always local let response = Request::to(("our", "hyperwallet", "hyperwallet", "sys")) .body(serde_json::to_vec(&message).map_err(HyperwalletClientError::Serialization)?) - .send_and_await_response(5) // 5s timeout + .send_and_await_response(45) // 45s timeout .map_err(|e| HyperwalletClientError::Communication(e.into()))? .map_err(|e| HyperwalletClientError::Communication(e.into()))?; From fbefdbd07a0327b58ce02730abe4f9b92485adc7 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 29 Aug 2025 15:11:18 -0700 Subject: [PATCH 16/33] hyperapp: add spawn() --- src/hyperapp.rs | 56 +++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 7cd0439..88b00d9 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -15,14 +15,10 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; -// macro_export puts it in the root, -// so we re-export here so you can use as either -// hyperware_process_lib::run_async -// or -// hyperware_process_lib::hyperapp::run_async -pub use crate::run_async; - thread_local! { + static SPAWN_QUEUE: RefCell>>>> = RefCell::new(Vec::new()); + + pub static APP_CONTEXT: RefCell = RefCell::new(AppContext { hidden_state: None, executor: Executor::new(), @@ -140,22 +136,39 @@ pub struct Executor { tasks: Vec>>>, } +pub fn spawn(fut: impl Future + 'static) { + SPAWN_QUEUE.with(|queue| { + queue.borrow_mut().push(Box::pin(fut)); + }) +} + + impl Executor { pub fn new() -> Self { Self { tasks: Vec::new() } } - pub fn spawn(&mut self, fut: impl Future + 'static) { - self.tasks.push(Box::pin(fut)); - } - pub fn poll_all_tasks(&mut self) { - let mut ctx = Context::from_waker(noop_waker_ref()); - let mut completed = Vec::new(); + loop { + SPAWN_QUEUE.with(|queue| { + self.tasks.append(&mut queue.borrow_mut()); + }); + + let mut ctx = Context::from_waker(noop_waker_ref()); + let mut completed = Vec::new(); + + for i in 0..self.tasks.len() { + if let Poll::Ready(()) = self.tasks[i].as_mut().poll(&mut ctx) { + completed.push(i); + } + } - for i in 0..self.tasks.len() { - if let Poll::Ready(()) = self.tasks[i].as_mut().poll(&mut ctx) { - completed.push(i); + // tasks can spawn more tasks + let should_break = SPAWN_QUEUE.with(|queue| { + queue.is_empty() + }); + if should_break { + break; } } @@ -282,17 +295,6 @@ where return Err(AppSendError::SendError(e)); } -#[macro_export] -macro_rules! run_async { - ($($code:tt)*) => { - hyperware_process_lib::hyperapp::APP_CONTEXT.with(|ctx| { - ctx.borrow_mut().executor.spawn(async move { - $($code)* - }) - }) - }; -} - // Enum defining the state persistance behaviour #[derive(Clone)] pub enum SaveOptions { From b3217db1f02b2e8581b3506f45d4c11866854e84 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 29 Aug 2025 15:36:06 -0700 Subject: [PATCH 17/33] hyperapp: fix spawn bugs --- src/hyperapp.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 88b00d9..9d9b93e 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -149,13 +149,14 @@ impl Executor { } pub fn poll_all_tasks(&mut self) { + let mut completed = Vec::new(); + loop { SPAWN_QUEUE.with(|queue| { self.tasks.append(&mut queue.borrow_mut()); }); let mut ctx = Context::from_waker(noop_waker_ref()); - let mut completed = Vec::new(); for i in 0..self.tasks.len() { if let Poll::Ready(()) = self.tasks[i].as_mut().poll(&mut ctx) { @@ -165,6 +166,7 @@ impl Executor { // tasks can spawn more tasks let should_break = SPAWN_QUEUE.with(|queue| { + let queue = queue.borrow(); queue.is_empty() }); if should_break { From a16d47a2bfae7864e97d70a3914829b4e54a4033 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 22:36:39 +0000 Subject: [PATCH 18/33] Format Rust code using rustfmt --- src/hyperapp.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 9d9b93e..86f2ebc 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -142,7 +142,6 @@ pub fn spawn(fut: impl Future + 'static) { }) } - impl Executor { pub fn new() -> Self { Self { tasks: Vec::new() } From b9f1ead63356bfd4b60b337a380fef1be81d81c6 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 29 Aug 2025 20:37:53 -0700 Subject: [PATCH 19/33] hyperapp: fix polling --- src/hyperapp.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 86f2ebc..f0b8e88 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -148,13 +148,14 @@ impl Executor { } pub fn poll_all_tasks(&mut self) { - let mut completed = Vec::new(); - loop { + // Drain any newly spawned tasks into our task list SPAWN_QUEUE.with(|queue| { self.tasks.append(&mut queue.borrow_mut()); }); + // Poll all tasks, collecting completed ones + let mut completed = Vec::new(); let mut ctx = Context::from_waker(noop_waker_ref()); for i in 0..self.tasks.len() { @@ -163,18 +164,18 @@ impl Executor { } } - // tasks can spawn more tasks - let should_break = SPAWN_QUEUE.with(|queue| { - let queue = queue.borrow(); - queue.is_empty() - }); - if should_break { - break; + // Remove completed tasks immediately to prevent re-polling + for idx in completed.into_iter().rev() { + let _ = self.tasks.remove(idx); } - } - for idx in completed.into_iter().rev() { - let _ = self.tasks.remove(idx); + // Check if there are new tasks spawned during polling + let has_new_tasks = SPAWN_QUEUE.with(|queue| !queue.borrow().is_empty()); + + // Continue if new tasks were spawned, otherwise we're done + if !has_new_tasks { + break; + } } } } From 4c4d434cac72f69a927f93b611ea4257b14c9d3c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 30 Aug 2025 07:35:19 -0700 Subject: [PATCH 20/33] vfs: cleanup --- src/vfs/directory/directory_async.rs | 24 ------------------------ src/vfs/file/file_async.rs | 24 ------------------------ 2 files changed, 48 deletions(-) delete mode 100644 src/vfs/directory/directory_async.rs delete mode 100644 src/vfs/file/file_async.rs diff --git a/src/vfs/directory/directory_async.rs b/src/vfs/directory/directory_async.rs deleted file mode 100644 index 3bc54d6..0000000 --- a/src/vfs/directory/directory_async.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{ - hyperapp, - vfs::{vfs_request, VfsAction, VfsError, VfsResponse}, -}; - -/// Removes a dir at path, errors if path not found or path is not a `Directory`. -pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), VfsError> { - let timeout = timeout.unwrap_or(5); - - let request = vfs_request(path, VfsAction::RemoveDir).expects_response(timeout); - - let response = hyperapp::send::(request) - .await - .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; - - match response { - VfsResponse::Ok => Ok(()), - VfsResponse::Err(e) => Err(e), - _ => Err(VfsError::ParseError { - error: "unexpected response".to_string(), - path: path.to_string(), - }), - } -} diff --git a/src/vfs/file/file_async.rs b/src/vfs/file/file_async.rs deleted file mode 100644 index 2d361c0..0000000 --- a/src/vfs/file/file_async.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{ - hyperapp, - vfs::{vfs_request, VfsAction, VfsError, VfsResponse}, -}; - -/// Removes a file at path, errors if path not found or path is not a file. -pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), VfsError> { - let timeout = timeout.unwrap_or(5); - - let request = vfs_request(path, VfsAction::RemoveFile).expects_response(timeout); - - let response = hyperapp::send::(request) - .await - .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; - - match response { - VfsResponse::Ok => Ok(()), - VfsResponse::Err(e) => Err(e.into()), - _ => Err(VfsError::ParseError { - error: "unexpected response".to_string(), - path: path.to_string(), - }), - } -} From f9582d76f8c13e843d2edc7b51464049ebc89511 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:35:42 +0000 Subject: [PATCH 21/33] Format Rust code using rustfmt --- src/vfs/directory_async.rs | 20 +++++++-------- src/vfs/file_async.rs | 50 +++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/vfs/directory_async.rs b/src/vfs/directory_async.rs index 0540f84..dd2e534 100644 --- a/src/vfs/directory_async.rs +++ b/src/vfs/directory_async.rs @@ -8,8 +8,7 @@ pub struct DirectoryAsync { impl DirectoryAsync { pub async fn read(&self) -> Result, VfsError> { - let request = vfs_request(&self.path, VfsAction::ReadDir) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::ReadDir).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -26,11 +25,14 @@ impl DirectoryAsync { } } -pub async fn open_dir_async(path: &str, create: bool, timeout: Option) -> Result { +pub async fn open_dir_async( + path: &str, + create: bool, + timeout: Option, +) -> Result { let timeout = timeout.unwrap_or(5); if !create { - let request = vfs_request(path, VfsAction::Metadata) - .expects_response(timeout); + let request = vfs_request(path, VfsAction::Metadata).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -59,8 +61,7 @@ pub async fn open_dir_async(path: &str, create: bool, timeout: Option) -> R }); } - let request = vfs_request(path, VfsAction::CreateDirAll) - .expects_response(timeout); + let request = vfs_request(path, VfsAction::CreateDirAll).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -82,8 +83,7 @@ pub async fn open_dir_async(path: &str, create: bool, timeout: Option) -> R pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), VfsError> { let timeout = timeout.unwrap_or(5); - let request = vfs_request(path, VfsAction::RemoveDir) - .expects_response(timeout); + let request = vfs_request(path, VfsAction::RemoveDir).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -97,4 +97,4 @@ pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), Vf path: path.to_string(), }), } -} \ No newline at end of file +} diff --git a/src/vfs/file_async.rs b/src/vfs/file_async.rs index 94e92dd..bfb3e97 100644 --- a/src/vfs/file_async.rs +++ b/src/vfs/file_async.rs @@ -18,8 +18,7 @@ impl FileAsync { } pub async fn read(&self) -> Result, VfsError> { - let request = vfs_request(&self.path, VfsAction::Read) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::Read).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -47,8 +46,7 @@ impl FileAsync { } pub async fn read_into(&self, buffer: &mut [u8]) -> Result { - let request = vfs_request(&self.path, VfsAction::Read) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::Read).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -72,8 +70,8 @@ impl FileAsync { pub async fn read_at(&self, buffer: &mut [u8]) -> Result { let length = buffer.len() as u64; - let request = vfs_request(&self.path, VfsAction::ReadExact { length }) - .expects_response(self.timeout); + let request = + vfs_request(&self.path, VfsAction::ReadExact { length }).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -95,8 +93,7 @@ impl FileAsync { } pub async fn read_to_end(&self) -> Result, VfsError> { - let request = vfs_request(&self.path, VfsAction::ReadToEnd) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::ReadToEnd).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -113,8 +110,8 @@ impl FileAsync { } pub async fn read_to_string(&self) -> Result { - let request = vfs_request(&self.path, VfsAction::ReadToString) - .expects_response(self.timeout); + let request = + vfs_request(&self.path, VfsAction::ReadToString).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -188,8 +185,7 @@ impl FileAsync { } pub async fn seek(&mut self, pos: SeekFrom) -> Result { - let request = vfs_request(&self.path, VfsAction::Seek(pos)) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::Seek(pos)).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -234,8 +230,8 @@ impl FileAsync { } pub async fn set_len(&mut self, size: u64) -> Result<(), VfsError> { - let request = vfs_request(&self.path, VfsAction::SetLen(size)) - .expects_response(self.timeout); + let request = + vfs_request(&self.path, VfsAction::SetLen(size)).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -252,8 +248,7 @@ impl FileAsync { } pub async fn metadata(&self) -> Result { - let request = vfs_request(&self.path, VfsAction::Metadata) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::Metadata).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -270,8 +265,7 @@ impl FileAsync { } pub async fn sync_all(&self) -> Result<(), VfsError> { - let request = vfs_request(&self.path, VfsAction::SyncAll) - .expects_response(self.timeout); + let request = vfs_request(&self.path, VfsAction::SyncAll).expects_response(self.timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -304,8 +298,7 @@ pub async fn create_drive_async( let timeout = timeout.unwrap_or(5); let path = format!("/{}/{}", package_id, drive); - let request = vfs_request(&path, VfsAction::CreateDrive) - .expects_response(timeout); + let request = vfs_request(&path, VfsAction::CreateDrive).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -321,11 +314,14 @@ pub async fn create_drive_async( } } -pub async fn open_file_async(path: &str, create: bool, timeout: Option) -> Result { +pub async fn open_file_async( + path: &str, + create: bool, + timeout: Option, +) -> Result { let timeout = timeout.unwrap_or(5); - let request = vfs_request(path, VfsAction::OpenFile { create }) - .expects_response(timeout); + let request = vfs_request(path, VfsAction::OpenFile { create }).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -347,8 +343,7 @@ pub async fn open_file_async(path: &str, create: bool, timeout: Option) -> pub async fn create_file_async(path: &str, timeout: Option) -> Result { let timeout = timeout.unwrap_or(5); - let request = vfs_request(path, VfsAction::CreateFile) - .expects_response(timeout); + let request = vfs_request(path, VfsAction::CreateFile).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -370,8 +365,7 @@ pub async fn create_file_async(path: &str, timeout: Option) -> Result) -> Result<(), VfsError> { let timeout = timeout.unwrap_or(5); - let request = vfs_request(path, VfsAction::RemoveFile) - .expects_response(timeout); + let request = vfs_request(path, VfsAction::RemoveFile).expects_response(timeout); let resp_bytes = hyperapp::send_rmp::>(request) .await @@ -385,4 +379,4 @@ pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), V path: path.to_string(), }), } -} \ No newline at end of file +} From 19978badc5db783d9d05b4d47fe0e2312616c19f Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 2 Sep 2025 13:35:30 -0700 Subject: [PATCH 22/33] fix hyperwallet --- Cargo.lock | 2 ++ Cargo.toml | 5 ++++- src/hyperwallet_client/mod.rs | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 398dc64..c81b1eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1628,6 +1628,7 @@ dependencies = [ "bincode", "color-eyre", "futures-util", + "hex", "http", "mime_guess", "rand 0.8.5", @@ -1635,6 +1636,7 @@ dependencies = [ "rmp-serde", "serde", "serde_json", + "sha3", "thiserror 1.0.69", "tracing", "tracing-error", diff --git a/Cargo.toml b/Cargo.toml index 7e15bf2..3898dc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" [features] hyperapp = ["dep:futures-util", "dep:uuid", "logging"] logging = ["dep:color-eyre", "dep:tracing", "dep:tracing-error", "dep:tracing-subscriber"] -hyperwallet = [] +hyperwallet = ["dep:hex", "dep:sha3"] simulation-mode = [] [dependencies] @@ -48,3 +48,6 @@ color-eyre = { version = "0.6", features = ["capture-spantrace"], optional = tru tracing = { version = "0.1", optional = true } tracing-error = { version = "0.2", optional = true } tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "std"], optional = true } + +hex = { version = "0.4.3", optional = true } +sha3 = { version = "0.10.8", optional = true } diff --git a/src/hyperwallet_client/mod.rs b/src/hyperwallet_client/mod.rs index adad227..f7cb33e 100644 --- a/src/hyperwallet_client/mod.rs +++ b/src/hyperwallet_client/mod.rs @@ -1,4 +1,3 @@ -use crate::println as kiprintln; use crate::Request; use thiserror::Error; From acd381cecd673215ff1c054445616a0f9577b6d0 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 2 Sep 2025 13:50:30 -0700 Subject: [PATCH 23/33] fix vfs async (compiling at least -- functionality tbd) --- src/vfs/{ => directory}/directory_async.rs | 0 src/vfs/{ => file}/file_async.rs | 104 ++++++++++----------- 2 files changed, 52 insertions(+), 52 deletions(-) rename src/vfs/{ => directory}/directory_async.rs (100%) rename src/vfs/{ => file}/file_async.rs (75%) diff --git a/src/vfs/directory_async.rs b/src/vfs/directory/directory_async.rs similarity index 100% rename from src/vfs/directory_async.rs rename to src/vfs/directory/directory_async.rs diff --git a/src/vfs/file_async.rs b/src/vfs/file/file_async.rs similarity index 75% rename from src/vfs/file_async.rs rename to src/vfs/file/file_async.rs index bfb3e97..670827c 100644 --- a/src/vfs/file_async.rs +++ b/src/vfs/file/file_async.rs @@ -1,5 +1,5 @@ use super::{ - parse_response, vfs_request, FileMetadata, SeekFrom, VfsAction, VfsError, VfsResponse, + vfs_request, FileMetadata, SeekFrom, VfsAction, VfsError, VfsResponse, }; use crate::{get_blob, hyperapp, PackageId}; @@ -20,11 +20,11 @@ impl FileAsync { pub async fn read(&self) -> Result, VfsError> { let request = vfs_request(&self.path, VfsAction::Read).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Read => { let data = match get_blob() { Some(bytes) => bytes.bytes, @@ -48,11 +48,11 @@ impl FileAsync { pub async fn read_into(&self, buffer: &mut [u8]) -> Result { let request = vfs_request(&self.path, VfsAction::Read).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Read => { let data = get_blob().unwrap_or_default().bytes; let len = std::cmp::min(data.len(), buffer.len()); @@ -73,11 +73,11 @@ impl FileAsync { let request = vfs_request(&self.path, VfsAction::ReadExact { length }).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Read => { let data = get_blob().unwrap_or_default().bytes; let len = std::cmp::min(data.len(), buffer.len()); @@ -95,11 +95,11 @@ impl FileAsync { pub async fn read_to_end(&self) -> Result, VfsError> { let request = vfs_request(&self.path, VfsAction::ReadToEnd).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Read => Ok(get_blob().unwrap_or_default().bytes), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -113,11 +113,11 @@ impl FileAsync { let request = vfs_request(&self.path, VfsAction::ReadToString).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::ReadToString(s) => Ok(s), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -132,11 +132,11 @@ impl FileAsync { .blob_bytes(buffer) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -151,11 +151,11 @@ impl FileAsync { .blob_bytes(buffer) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -170,11 +170,11 @@ impl FileAsync { .blob_bytes(buffer) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -187,11 +187,11 @@ impl FileAsync { pub async fn seek(&mut self, pos: SeekFrom) -> Result { let request = vfs_request(&self.path, VfsAction::Seek(pos)).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::SeekFrom { new_offset: new_pos, } => Ok(new_pos), @@ -212,11 +212,11 @@ impl FileAsync { ) .expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(FileAsync { path: path.to_string(), timeout: self.timeout, @@ -233,11 +233,11 @@ impl FileAsync { let request = vfs_request(&self.path, VfsAction::SetLen(size)).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -250,11 +250,11 @@ impl FileAsync { pub async fn metadata(&self) -> Result { let request = vfs_request(&self.path, VfsAction::Metadata).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Metadata(metadata) => Ok(metadata), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -267,11 +267,11 @@ impl FileAsync { pub async fn sync_all(&self) -> Result<(), VfsError> { let request = vfs_request(&self.path, VfsAction::SyncAll).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -300,11 +300,11 @@ pub async fn create_drive_async( let request = vfs_request(&path, VfsAction::CreateDrive).expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(path), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { @@ -323,11 +323,11 @@ pub async fn open_file_async( let request = vfs_request(path, VfsAction::OpenFile { create }).expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(FileAsync { path: path.to_string(), timeout, @@ -345,11 +345,11 @@ pub async fn create_file_async(path: &str, timeout: Option) -> Result>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(FileAsync { path: path.to_string(), timeout, @@ -367,11 +367,11 @@ pub async fn remove_file_async(path: &str, timeout: Option) -> Result<(), V let request = vfs_request(path, VfsAction::RemoveFile).expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await - .map_err(|e| VfsError::SendError(crate::SendErrorKind::Timeout))?; + .map_err(|_e| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e.into()), _ => Err(VfsError::ParseError { From 8ae18fdb34202c6d6edbca41505589a45ec9d3e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:50:50 +0000 Subject: [PATCH 24/33] Format Rust code using rustfmt --- src/vfs/file/file_async.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vfs/file/file_async.rs b/src/vfs/file/file_async.rs index 670827c..a23ae0a 100644 --- a/src/vfs/file/file_async.rs +++ b/src/vfs/file/file_async.rs @@ -1,6 +1,4 @@ -use super::{ - vfs_request, FileMetadata, SeekFrom, VfsAction, VfsError, VfsResponse, -}; +use super::{vfs_request, FileMetadata, SeekFrom, VfsAction, VfsError, VfsResponse}; use crate::{get_blob, hyperapp, PackageId}; #[derive(serde::Deserialize, serde::Serialize)] From 0e1aaccc99a76355be747036b5acdf17fdf47b5c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 24 Sep 2025 13:39:12 -0700 Subject: [PATCH 25/33] vfs: fix directory async --- src/vfs/directory/directory_async.rs | 30 +++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/vfs/directory/directory_async.rs b/src/vfs/directory/directory_async.rs index dd2e534..8195cd6 100644 --- a/src/vfs/directory/directory_async.rs +++ b/src/vfs/directory/directory_async.rs @@ -1,4 +1,4 @@ -use super::{parse_response, vfs_request, DirEntry, FileType, VfsAction, VfsError, VfsResponse}; +use super::{vfs_request, DirEntry, FileType, VfsAction, VfsError, VfsResponse}; use crate::hyperapp; pub struct DirectoryAsync { @@ -10,16 +10,16 @@ impl DirectoryAsync { pub async fn read(&self) -> Result, VfsError> { let request = vfs_request(&self.path, VfsAction::ReadDir).expects_response(self.timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::ReadDir(entries) => Ok(entries), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { error: "unexpected response".to_string(), - path: self.path.clone(), + path: self.path.to_string(), }), } } @@ -34,11 +34,11 @@ pub async fn open_dir_async( if !create { let request = vfs_request(path, VfsAction::Metadata).expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Metadata(m) => { if m.file_type != FileType::Directory { return Err(VfsError::IOError( @@ -47,12 +47,10 @@ pub async fn open_dir_async( } } VfsResponse::Err(e) => return Err(e), - _ => { - return Err(VfsError::ParseError { - error: "unexpected response".to_string(), - path: path.to_string(), - }) - } + _ => return Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }), } return Ok(DirectoryAsync { @@ -63,11 +61,11 @@ pub async fn open_dir_async( let request = vfs_request(path, VfsAction::CreateDirAll).expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(DirectoryAsync { path: path.to_string(), timeout, @@ -85,11 +83,11 @@ pub async fn remove_dir_async(path: &str, timeout: Option) -> Result<(), Vf let request = vfs_request(path, VfsAction::RemoveDir).expects_response(timeout); - let resp_bytes = hyperapp::send_rmp::>(request) + let response = hyperapp::send::(request) .await .map_err(|_| VfsError::SendError(crate::SendErrorKind::Timeout))?; - match parse_response(&resp_bytes)? { + match response { VfsResponse::Ok => Ok(()), VfsResponse::Err(e) => Err(e), _ => Err(VfsError::ParseError { From 820debd6fd625f7de226356fead62b61b9764fae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:39:38 +0000 Subject: [PATCH 26/33] Format Rust code using rustfmt --- src/vfs/directory/directory_async.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vfs/directory/directory_async.rs b/src/vfs/directory/directory_async.rs index 8195cd6..16a65f8 100644 --- a/src/vfs/directory/directory_async.rs +++ b/src/vfs/directory/directory_async.rs @@ -47,10 +47,12 @@ pub async fn open_dir_async( } } VfsResponse::Err(e) => return Err(e), - _ => return Err(VfsError::ParseError { - error: "unexpected response".to_string(), - path: path.to_string(), - }), + _ => { + return Err(VfsError::ParseError { + error: "unexpected response".to_string(), + path: path.to_string(), + }) + } } return Ok(DirectoryAsync { From 232fe2526f383c88efc2ef3a289deba2e193d68f Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 24 Sep 2025 15:08:22 -0700 Subject: [PATCH 27/33] hyperapp: allow serving ui from non-default dir --- src/hyperapp.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index f0b8e88..6c8a43c 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -396,12 +396,17 @@ where pub fn setup_server( ui_config: Option<&HttpBindingConfig>, + ui_path: Option, endpoints: &[Binding], ) -> http::server::HttpServer { let mut server = http::server::HttpServer::new(5); if let Some(ui) = ui_config { - if let Err(e) = server.serve_ui("ui", vec!["/"], ui.clone()) { + if let Err(e) = server.serve_ui( + &ui_path.unwrap_or_else(|| "ui".to_string()), + vec!["/"], + ui.clone(), + ) { panic!("failed to serve UI: {e}. Make sure that a ui folder is in /pkg"); } } From 9b58c1a3e2de0110e47e7457cba465c1f446a321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Mon, 6 Oct 2025 14:53:05 +0200 Subject: [PATCH 28/33] wip --- src/hyperapp.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index f0b8e88..1eb37b2 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -37,6 +37,7 @@ thread_local! { pub struct HttpRequestContext { pub request: IncomingHttpRequest, pub response_headers: HashMap, + pub response_status: u16, } pub struct AppContext { @@ -95,6 +96,15 @@ pub fn add_response_header(key: String, value: String) { }) } +// Set the HTTP response status code +pub fn set_response_status(status: u16) { + APP_HELPERS.with(|helpers| { + if let Some(ctx) = &mut helpers.borrow_mut().current_http_context { + ctx.response_status = status; + } + }) +} + pub fn clear_http_request_context() { APP_HELPERS.with(|helpers| { helpers.borrow_mut().current_http_context = None; From e8b065179ce5d15893a23142416e59c87e0f31f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Mon, 6 Oct 2025 16:48:18 +0200 Subject: [PATCH 29/33] custom error code --- src/hyperapp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 1eb37b2..7bfe9ce 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -37,7 +37,7 @@ thread_local! { pub struct HttpRequestContext { pub request: IncomingHttpRequest, pub response_headers: HashMap, - pub response_status: u16, + pub response_status: http::StatusCode, } pub struct AppContext { @@ -97,7 +97,7 @@ pub fn add_response_header(key: String, value: String) { } // Set the HTTP response status code -pub fn set_response_status(status: u16) { +pub fn set_response_status(status: http::StatusCode) { APP_HELPERS.with(|helpers| { if let Some(ctx) = &mut helpers.borrow_mut().current_http_context { ctx.response_status = status; From b33ce448690b0976da465cd4d1063526b926c0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Tue, 7 Oct 2025 15:27:25 +0200 Subject: [PATCH 30/33] prevent crashing on invalid error reponse --- src/hyperapp.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index f0b8e88..2986029 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -267,9 +267,16 @@ where return Ok(r); } - let e = serde_json::from_slice::(&response_bytes) - .expect("Failed to deserialize response to send()"); - return Err(AppSendError::SendError(e)); + match serde_json::from_slice::(&response_bytes) { + Ok(e) => Err(AppSendError::SendError(e)), + Err(err) => { + error!( + "Failed to deserialize response in send(): {} (payload: {:?})", + err, response_bytes + ); + Err(AppSendError::BuildError(BuildError::NoBody)) + } + } } pub async fn send_rmp(request: Request) -> Result @@ -292,9 +299,16 @@ where return Ok(r); } - let e = rmp_serde::from_slice::(&response_bytes) - .expect("Failed to deserialize response to send()"); - return Err(AppSendError::SendError(e)); + match rmp_serde::from_slice::(&response_bytes) { + Ok(e) => Err(AppSendError::SendError(e)), + Err(err) => { + error!( + "Failed to deserialize response in send_rmp(): {} (payload: {:?})", + err, response_bytes + ); + Err(AppSendError::BuildError(BuildError::NoBody)) + } + } } // Enum defining the state persistance behaviour From 876b0c26367db2554059180f0339150faf972897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Fri, 17 Oct 2025 14:08:34 +0200 Subject: [PATCH 31/33] implement join handle for tasks and implement waker --- Cargo.lock | 1 + Cargo.toml | 3 +- src/hyperapp.rs | 77 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c81b1eb..ceba34b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1627,6 +1627,7 @@ dependencies = [ "base64", "bincode", "color-eyre", + "futures-channel", "futures-util", "hex", "http", diff --git a/Cargo.toml b/Cargo.toml index 3898dc3..fac64fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/hyperware-ai/process_lib" license = "Apache-2.0" [features] -hyperapp = ["dep:futures-util", "dep:uuid", "logging"] +hyperapp = ["dep:futures-util", "dep:futures-channel", "dep:uuid", "logging"] logging = ["dep:color-eyre", "dep:tracing", "dep:tracing-error", "dep:tracing-subscriber"] hyperwallet = ["dep:hex", "dep:sha3"] simulation-mode = [] @@ -42,6 +42,7 @@ url = "2.4.1" wit-bindgen = "0.42.1" futures-util = { version = "0.3", optional = true } +futures-channel = { version = "0.3", optional = true } uuid = { version = "1.0", features = ["v4"], optional = true } color-eyre = { version = "0.6", features = ["capture-spantrace"], optional = true } diff --git a/src/hyperapp.rs b/src/hyperapp.rs index 29a4dd0..a474ed2 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -2,6 +2,10 @@ use std::cell::RefCell; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use std::task::{Context, Poll}; use crate::{ @@ -10,7 +14,8 @@ use crate::{ logging::{error, info}, set_state, timer, Address, BuildError, LazyLoadBlob, Message, Request, SendError, }; -use futures_util::task::noop_waker_ref; +use futures_util::task::{waker_ref, ArcWake}; +use futures_channel::{mpsc, oneshot}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; @@ -18,7 +23,6 @@ use uuid::Uuid; thread_local! { static SPAWN_QUEUE: RefCell>>>> = RefCell::new(Vec::new()); - pub static APP_CONTEXT: RefCell = RefCell::new(AppContext { hidden_state: None, executor: Executor::new(), @@ -146,10 +150,53 @@ pub struct Executor { tasks: Vec>>>, } -pub fn spawn(fut: impl Future + 'static) { +struct ExecutorWakeFlag { + triggered: AtomicBool, +} + +impl ExecutorWakeFlag { + fn new() -> Self { + Self { + triggered: AtomicBool::new(false), + } + } + + fn take(&self) -> bool { + self.triggered.swap(false, Ordering::SeqCst) + } +} + +impl ArcWake for ExecutorWakeFlag { + fn wake_by_ref(arc_self: &Arc) { + arc_self.triggered.store(true, Ordering::SeqCst); + } +} + +pub struct JoinHandle { + receiver: oneshot::Receiver, +} + +impl Future for JoinHandle { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let receiver = &mut self.get_mut().receiver; + Pin::new(receiver).poll(cx) + } +} + +pub fn spawn(fut: impl Future + 'static) -> JoinHandle +where + T: 'static, +{ + let (sender, receiver) = oneshot::channel(); SPAWN_QUEUE.with(|queue| { - queue.borrow_mut().push(Box::pin(fut)); - }) + queue.borrow_mut().push(Box::pin(async move { + let result = fut.await; + let _ = sender.send(result); + })); + }); + JoinHandle { receiver } } impl Executor { @@ -158,19 +205,24 @@ impl Executor { } pub fn poll_all_tasks(&mut self) { + let wake_flag = Arc::new(ExecutorWakeFlag::new()); loop { // Drain any newly spawned tasks into our task list SPAWN_QUEUE.with(|queue| { self.tasks.append(&mut queue.borrow_mut()); }); - // Poll all tasks, collecting completed ones + // Poll all tasks, collecting completed ones. + // Put waker into context so tasks can wake the executor if needed. let mut completed = Vec::new(); - let mut ctx = Context::from_waker(noop_waker_ref()); + { + let waker = waker_ref(&wake_flag); + let mut ctx = Context::from_waker(&waker); - for i in 0..self.tasks.len() { - if let Poll::Ready(()) = self.tasks[i].as_mut().poll(&mut ctx) { - completed.push(i); + for i in 0..self.tasks.len() { + if let Poll::Ready(()) = self.tasks[i].as_mut().poll(&mut ctx) { + completed.push(i); + } } } @@ -181,9 +233,10 @@ impl Executor { // Check if there are new tasks spawned during polling let has_new_tasks = SPAWN_QUEUE.with(|queue| !queue.borrow().is_empty()); + // Check if any task woke the executor that needs to be re-polled + let was_woken = wake_flag.take(); - // Continue if new tasks were spawned, otherwise we're done - if !has_new_tasks { + if !has_new_tasks && !was_woken { break; } } From 76b81721d5ab2a479dc341d410326db00553ae7b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:09:06 +0000 Subject: [PATCH 32/33] Format Rust code using rustfmt --- src/hyperapp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index a474ed2..b5de920 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -14,8 +14,8 @@ use crate::{ logging::{error, info}, set_state, timer, Address, BuildError, LazyLoadBlob, Message, Request, SendError, }; -use futures_util::task::{waker_ref, ArcWake}; use futures_channel::{mpsc, oneshot}; +use futures_util::task::{waker_ref, ArcWake}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; From 78ea492c8fc8ca1bfb0138de20cec80e281c07b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Mon, 20 Oct 2025 14:45:52 +0200 Subject: [PATCH 33/33] drop edge case handling --- src/hyperapp.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/hyperapp.rs b/src/hyperapp.rs index a474ed2..0f887b6 100644 --- a/src/hyperapp.rs +++ b/src/hyperapp.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; use std::sync::{ @@ -14,8 +14,8 @@ use crate::{ logging::{error, info}, set_state, timer, Address, BuildError, LazyLoadBlob, Message, Request, SendError, }; -use futures_util::task::{waker_ref, ArcWake}; use futures_channel::{mpsc, oneshot}; +use futures_util::task::{waker_ref, ArcWake}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; @@ -29,6 +29,7 @@ thread_local! { }); pub static RESPONSE_REGISTRY: RefCell>> = RefCell::new(HashMap::new()); + pub static CANCELLED_RESPONSES: RefCell> = RefCell::new(HashSet::new()); pub static APP_HELPERS: RefCell = RefCell::new(AppHelpers { current_server: None, @@ -246,6 +247,7 @@ struct ResponseFuture { correlation_id: String, // Capture HTTP context at creation time http_context: Option, + resolved: bool, } impl ResponseFuture { @@ -257,6 +259,7 @@ impl ResponseFuture { Self { correlation_id, http_context, + resolved: false, } } } @@ -265,16 +268,18 @@ impl Future for ResponseFuture { type Output = Vec; fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - let correlation_id = &self.correlation_id; + let this = self.get_mut(); let maybe_bytes = RESPONSE_REGISTRY.with(|registry| { let mut registry_mut = registry.borrow_mut(); - registry_mut.remove(correlation_id) + registry_mut.remove(&this.correlation_id) }); if let Some(bytes) = maybe_bytes { + this.resolved = true; + // Restore this future's captured context - if let Some(ref context) = self.http_context { + if let Some(ref context) = this.http_context { APP_HELPERS.with(|helpers| { helpers.borrow_mut().current_http_context = Some(context.clone()); }); @@ -287,6 +292,23 @@ impl Future for ResponseFuture { } } +impl Drop for ResponseFuture { + fn drop(&mut self) { + // We want to avoid cleaning up after successful responses + if self.resolved { + return; + } + + RESPONSE_REGISTRY.with(|registry| { + registry.borrow_mut().remove(&self.correlation_id); + }); + + CANCELLED_RESPONSES.with(|set| { + set.borrow_mut().insert(self.correlation_id.clone()); + }); + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Error)] pub enum AppSendError { #[error("SendError: {0}")]