From 4a0baec70b8963dcf2b630473b5b773b00aeda8a Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:56:40 -0500 Subject: [PATCH 1/5] chore: dev version bump (Prep for breaking change) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32c3a47..b4131f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1383,7 +1383,7 @@ dependencies = [ [[package]] name = "reqwest-cross" -version = "0.5.1" +version = "0.6.0-dev" dependencies = [ "anyhow", "document-features", diff --git a/Cargo.toml b/Cargo.toml index 70efc01..d4edf73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwest-cross" -version = "0.5.1" +version = "0.6.0-dev" authors = ["One "] categories = ["web-programming::http-client", "wasm"] documentation = "https://docs.rs/reqwest-cross" From 1540df2ca6ecfd6b37062d0cc63867dd29854b60 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:38:39 -0500 Subject: [PATCH 2/5] feat: add start_request function --- src/data_state.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/data_state.rs b/src/data_state.rs index 641f8dd..bcc1ccb 100644 --- a/src/data_state.rs +++ b/src/data_state.rs @@ -54,6 +54,37 @@ pub enum DataState { } impl DataState { + #[cfg(feature = "egui")] + /// Calls [Self::start_request] and adds a spinner if progress can be made + #[must_use] + pub fn egui_start_request(&mut self, ui: &mut egui::Ui, fetch_fn: F) -> CanMakeProgress + where + F: FnOnce() -> Awaiting, + { + let result = self.start_request(fetch_fn); + if result.is_able_to_make_progress() { + ui.spinner(); + } + result + } + + /// Starts a new request. Only intended to be on [Self::None] and if state + /// is any other value it returns + /// [CanMakeProgress::UnableToMakeProgress] + #[must_use] + pub fn start_request(&mut self, fetch_fn: F) -> CanMakeProgress + where + F: FnOnce() -> Awaiting, + { + if self.is_none() { + let result = self.get(fetch_fn); + assert!(result.is_able_to_make_progress()); + result + } else { + CanMakeProgress::UnableToMakeProgress + } + } + #[cfg(feature = "egui")] /// Attempts to load the data and displays appropriate UI if applicable. /// Some branches lead to no UI being displayed, in particular when the data From 504b1290aa27af419065d8c95d5b6cc19d5f15b8 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:14:54 -0500 Subject: [PATCH 3/5] feat: change api to make more use cases easier --- examples/loop_yield_data_state.rs | 50 +++++------ src/data_state.rs | 139 ++++++++++++++++-------------- src/data_state_retry.rs | 63 ++++++-------- 3 files changed, 126 insertions(+), 126 deletions(-) diff --git a/examples/loop_yield_data_state.rs b/examples/loop_yield_data_state.rs index dc777a0..7fcb3bc 100644 --- a/examples/loop_yield_data_state.rs +++ b/examples/loop_yield_data_state.rs @@ -3,7 +3,7 @@ // DataState type. use anyhow::Context; -use reqwest_cross::{fetch_plus, reqwest, Awaiting, DataState}; +use reqwest_cross::{fetch_plus, oneshot, reqwest, DataState}; #[cfg(all(not(target_arch = "wasm32"), feature = "native-tokio"))] #[tokio::main] @@ -22,10 +22,6 @@ fn main() { } async fn common_code() -> Result<(), Box> { - // Allows for one iteration where we see no progress but next loop should go - // into first branch - let mut seen_no_progress = false; - let client = reqwest::Client::new(); let mut state = DataState::None; @@ -34,34 +30,40 @@ async fn common_code() -> Result<(), Box> { // This loop would normally be a game loop, or the executor of an immediate mode // GUI. loop { - if let DataState::Present(status_code) = state.as_ref() { + if state.is_none() { + let client = client.clone(); + let can_make_progress = + state.start_request(|| make_request(client, "https://httpbin.org/get")); + assert!(can_make_progress.is_able_to_make_progress()); + } + if let Some(status_code) = state.poll().present() { println!("Response received"); assert_eq!(status_code, &200); break; - } else { - let outcome = state.get(|| { - let req = client.get("https://httpbin.org/get"); - let response_handler = |resp: reqwest::Result| async { - resp.map(|resp| resp.status()) - .context("Request failed, got an error back") - }; - let ui_notify = || { - println!("Request Completed, this is where you would wake up your UI thread"); - }; - Awaiting(fetch_plus(req, response_handler, ui_notify)) - }); - assert!(!seen_no_progress); - if outcome.is_unable_to_make_progress() { - // We should never get into this branch again - seen_no_progress = true; - } - reqwest_cross::yield_now().await; } + reqwest_cross::yield_now().await; } println!("Exited loop"); Ok(()) } +fn make_request( + client: reqwest::Client, + url: impl reqwest::IntoUrl, +) -> oneshot::Receiver> { + let req = client.get(url); + let response_handler = |resp: reqwest::Result| async { + resp.map(|resp| resp.status()) + .context("Request failed, got an error back") + }; + let ui_notify = || { + println!("Request Completed, this is where you would wake up your UI thread. +If using egui version of the functions the associated methods add spinners which will keep the loop going so no wake up is needed. +Passing an empty closure would suffice."); + }; + fetch_plus(req, response_handler, ui_notify) +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { diff --git a/src/data_state.rs b/src/data_state.rs index bcc1ccb..6034605 100644 --- a/src/data_state.rs +++ b/src/data_state.rs @@ -37,6 +37,11 @@ pub enum CanMakeProgress { /// Used to represent data that is pending being available #[derive(Debug)] pub struct Awaiting(pub oneshot::Receiver>); +impl From>> for Awaiting { + fn from(value: oneshot::Receiver>) -> Self { + Self(value) + } +} /// Used to store a type that is not always available and we need to keep /// polling it to get it ready @@ -57,9 +62,10 @@ impl DataState { #[cfg(feature = "egui")] /// Calls [Self::start_request] and adds a spinner if progress can be made #[must_use] - pub fn egui_start_request(&mut self, ui: &mut egui::Ui, fetch_fn: F) -> CanMakeProgress + pub fn egui_start_request(&mut self, ui: &mut egui::Ui, fetch_fn: F) -> CanMakeProgress where - F: FnOnce() -> Awaiting, + F: FnOnce() -> R, + R: Into>, { let result = self.start_request(fetch_fn); if result.is_able_to_make_progress() { @@ -69,97 +75,77 @@ impl DataState { } /// Starts a new request. Only intended to be on [Self::None] and if state - /// is any other value it returns - /// [CanMakeProgress::UnableToMakeProgress] + /// is any other value it returns [CanMakeProgress::UnableToMakeProgress] #[must_use] - pub fn start_request(&mut self, fetch_fn: F) -> CanMakeProgress + pub fn start_request(&mut self, fetch_fn: F) -> CanMakeProgress where - F: FnOnce() -> Awaiting, + F: FnOnce() -> R, + R: Into>, { if self.is_none() { - let result = self.get(fetch_fn); - assert!(result.is_able_to_make_progress()); - result + *self = DataState::AwaitingResponse(fetch_fn().into()); + CanMakeProgress::AbleToMakeProgress } else { + debug_assert!( + false, + "No known good reason this path should be hit other than logic error" + ); CanMakeProgress::UnableToMakeProgress } } + /// Convenience method that will try to make progress if in + /// [Self::AwaitingResponse] and does nothing otherwise. Returns a reference + /// to self for chaining + pub fn poll(&mut self) -> &mut Self { + if let DataState::AwaitingResponse(rx) = self { + if let Some(new_state) = Self::await_data(rx) { + *self = new_state; + } + } + self + } + #[cfg(feature = "egui")] - /// Attempts to load the data and displays appropriate UI if applicable. - /// Some branches lead to no UI being displayed, in particular when the data - /// or an error is received (On the expectation it will show next frame). - /// When in an error state the error messages will show as applicable. - /// If called an already has data present this function does nothing and - /// returns [CanMakeProgress::UnableToMakeProgress] + /// Meant to be a simple method to just provide the data if it's ready or + /// help with UI and polling to get it ready if it's not. /// - /// If a `retry_msg` is provided then it overrides the default + /// WARNING: Does nothing if `self` is [Self::None] /// - /// Note see [`Self::get`] for more info. + /// If a `error_btn_text` is provided then it overrides the default #[must_use] - pub fn egui_get( + pub fn egui_poll_mut( &mut self, ui: &mut egui::Ui, - retry_msg: Option<&str>, - fetch_fn: F, - ) -> CanMakeProgress - where - F: FnOnce() -> Awaiting, - { + error_btn_text: Option<&str>, + ) -> Option<&mut T> { match self { - DataState::None => { - ui.spinner(); - self.get(fetch_fn) - } + DataState::None => {} DataState::AwaitingResponse(_) => { ui.spinner(); - self.get(fetch_fn) + self.poll(); } - DataState::Present(_data) => { - // Does nothing as data is already present - CanMakeProgress::UnableToMakeProgress + DataState::Present(data) => { + return Some(data); } DataState::Failed(e) => { ui.colored_label(ui.visuals().error_fg_color, e.to_string()); - if ui.button(retry_msg.unwrap_or("Retry Request")).clicked() { + if ui + .button(error_btn_text.unwrap_or("Clear Error Status")) + .clicked() + { *self = DataState::default(); } - CanMakeProgress::AbleToMakeProgress } } + None } - /// Attempts to load the data and returns if it is able to make progress. - /// - /// Note: F needs to return `AwaitingType` and not T because it needs to - /// be able to be pending if T is not ready. + #[cfg(feature = "egui")] + /// Wraps [Self::egui_poll_mut] and returns an immutable reference #[must_use] - pub fn get(&mut self, fetch_fn: F) -> CanMakeProgress - where - F: FnOnce() -> Awaiting, - { - match self { - DataState::None => { - let rx = fetch_fn(); - *self = DataState::AwaitingResponse(rx); - CanMakeProgress::AbleToMakeProgress - } - DataState::AwaitingResponse(rx) => { - if let Some(new_state) = Self::await_data(rx) { - *self = new_state; - } - CanMakeProgress::AbleToMakeProgress - } - DataState::Present(_data) => { - // Does nothing data is already present - CanMakeProgress::UnableToMakeProgress - } - DataState::Failed(_e) => { - // Have no way to let the user know there is an error nothing we - // can do here - CanMakeProgress::UnableToMakeProgress - } - } + pub fn egui_poll(&mut self, ui: &mut egui::Ui, error_btn_text: Option<&str>) -> Option<&T> { + self.egui_poll_mut(ui, error_btn_text).map(|x| &*x) } /// Checks to see if the data is ready and if it is returns a new [`Self`] @@ -185,6 +171,31 @@ impl DataState { }) } + /// Returns a reference to the inner data if available otherwise None. + /// + /// NOTE: This function does not poll to get the data ready if the state is + /// still awaiting + pub fn present(&self) -> Option<&T> { + if let Self::Present(data) = self { + Some(data) + } else { + None + } + } + + /// Returns a mutable reference to the inner data if available otherwise + /// None + /// + /// NOTE: This function does not poll to get the data ready if the state is + /// still awaiting + pub fn present_mut(&mut self) -> Option<&mut T> { + if let Self::Present(data) = self { + Some(data) + } else { + None + } + } + /// Returns `true` if the data state is [`Present`]. /// /// [`Present`]: DataState::Present diff --git a/src/data_state_retry.rs b/src/data_state_retry.rs index e81576e..38dbe77 100644 --- a/src/data_state_retry.rs +++ b/src/data_state_retry.rs @@ -1,4 +1,4 @@ -use tracing::{error, warn}; +use tracing::warn; use crate::{data_state::CanMakeProgress, Awaiting, DataState, ErrorBounds}; use std::fmt::Debug; @@ -10,9 +10,11 @@ use std::time::{Duration, Instant}; pub struct DataStateRetry { /// Number of attempts that the retries get reset to pub max_attempts: u8, + /// The range of milliseconds to select a random value from to set the delay /// to retry pub retry_delay_millis: Range, + attempts_left: u8, inner: DataState, // Not public to ensure resets happen as they should next_allowed_attempt: Instant, @@ -74,19 +76,20 @@ impl DataStateRetry { /// /// Note see [`DataState::egui_get`] for more info. #[must_use] - pub fn egui_get( + pub fn egui_start_or_poll( &mut self, ui: &mut egui::Ui, retry_msg: Option<&str>, fetch_fn: F, ) -> CanMakeProgress where - F: FnOnce() -> Awaiting, + F: FnOnce() -> R, + R: Into>, { match self.inner.as_ref() { DataState::None | DataState::AwaitingResponse(_) => { self.ui_spinner_with_attempt_count(ui); - self.get(fetch_fn) + self.start_or_poll(fetch_fn) } DataState::Present(_data) => { // Does nothing as data is already present @@ -96,7 +99,7 @@ impl DataStateRetry { if self.attempts_left == 0 { ui.colored_label( ui.visuals().error_fg_color, - format!("{} attempts exhausted. {e}", self.max_attempts), + format!("No attempts left from {}. {e}", self.max_attempts), ); if ui.button(retry_msg.unwrap_or("Restart Requests")).clicked() { self.reset_attempts(); @@ -112,30 +115,26 @@ impl DataStateRetry { wait_left.as_secs() ), ); - let is_able_to_make_progress = self.get(fetch_fn).is_able_to_make_progress(); - assert!( - is_able_to_make_progress, - "if this is not true something is very wrong" - ); + if ui.button("Stop Trying").clicked() { + self.attempts_left = 0; + } } - CanMakeProgress::AbleToMakeProgress } } } /// Attempts to load the data and returns if it is able to make progress. - /// - /// See [`DataState::get`] for more info. #[must_use] - pub fn get(&mut self, fetch_fn: F) -> CanMakeProgress + pub fn start_or_poll(&mut self, fetch_fn: F) -> CanMakeProgress where - F: FnOnce() -> Awaiting, + F: FnOnce() -> R, + R: Into>, { match self.inner.as_mut() { DataState::None => { // Going to make an attempt, set when the next attempt is allowed - use rand::Rng; + use rand::Rng as _; let wait_time_in_millis = rand::thread_rng() .gen_range(self.retry_delay_millis.clone()) .into(); @@ -143,34 +142,19 @@ impl DataStateRetry { .checked_add(Duration::from_millis(wait_time_in_millis)) .expect("failed to get random delay, value was out of range"); - self.inner.get(fetch_fn) + self.inner.start_request(fetch_fn) } - DataState::AwaitingResponse(rx) => { - if let Some(new_state) = DataState::await_data(rx) { - // TODO 4: Add some tests to ensure await_data work as this function assumes - self.inner = match new_state.as_ref() { - DataState::None => { - error!("Unexpected new state received of DataState::None"); - unreachable!("Only expect Failed or Present variants to be returned but got None") - } - DataState::AwaitingResponse(_) => { - error!("Unexpected new state received of AwaitingResponse"); - unreachable!("Only expect Failed or Present variants to be returned bug got AwaitingResponse") - } - DataState::Present(_) => { - // Data was successfully received - self.reset_attempts(); - new_state - } - DataState::Failed(_) => new_state, - }; + DataState::AwaitingResponse(_) => { + if self.inner.poll().is_present() { + // Data was successfully received because before it was Awaiting + self.reset_attempts(); } CanMakeProgress::AbleToMakeProgress } - DataState::Present(_) => self.inner.get(fetch_fn), + DataState::Present(_) => CanMakeProgress::UnableToMakeProgress, DataState::Failed(err_msg) => { if self.attempts_left == 0 { - self.inner.get(fetch_fn) + CanMakeProgress::UnableToMakeProgress } else { let wait_left = wait_before_next_attempt(self.next_allowed_attempt); if wait_left.is_zero() { @@ -193,6 +177,7 @@ impl DataStateRetry { /// Clear stored data pub fn clear(&mut self) { self.inner = DataState::default(); + self.reset_attempts(); } /// Returns `true` if the internal data state is [`DataState::Present`]. @@ -245,3 +230,5 @@ impl AsMut> for DataStateRetry { fn wait_before_next_attempt(next_allowed_attempt: Instant) -> Duration { next_allowed_attempt.saturating_duration_since(Instant::now()) } + +// TODO 4: Use mocking to add tests ensuring retires are executed From a793805fb89a1538f9c5c8f48b41401545f8b756 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:21:02 -0500 Subject: [PATCH 4/5] chore: remove must use on on egui functions --- src/data_state.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/data_state.rs b/src/data_state.rs index 6034605..4cbe3f8 100644 --- a/src/data_state.rs +++ b/src/data_state.rs @@ -61,7 +61,6 @@ pub enum DataState { impl DataState { #[cfg(feature = "egui")] /// Calls [Self::start_request] and adds a spinner if progress can be made - #[must_use] pub fn egui_start_request(&mut self, ui: &mut egui::Ui, fetch_fn: F) -> CanMakeProgress where F: FnOnce() -> R, @@ -113,7 +112,6 @@ impl DataState { /// WARNING: Does nothing if `self` is [Self::None] /// /// If a `error_btn_text` is provided then it overrides the default - #[must_use] pub fn egui_poll_mut( &mut self, ui: &mut egui::Ui, @@ -143,7 +141,6 @@ impl DataState { #[cfg(feature = "egui")] /// Wraps [Self::egui_poll_mut] and returns an immutable reference - #[must_use] pub fn egui_poll(&mut self, ui: &mut egui::Ui, error_btn_text: Option<&str>) -> Option<&T> { self.egui_poll_mut(ui, error_btn_text).map(|x| &*x) } From c4a66c147d0027130dbd581185cc83f63e860106 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:48:39 -0500 Subject: [PATCH 5/5] chore: version bump 0.6.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4131f9..8633bf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1383,7 +1383,7 @@ dependencies = [ [[package]] name = "reqwest-cross" -version = "0.6.0-dev" +version = "0.6.0" dependencies = [ "anyhow", "document-features", diff --git a/Cargo.toml b/Cargo.toml index d4edf73..299e3a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwest-cross" -version = "0.6.0-dev" +version = "0.6.0" authors = ["One "] categories = ["web-programming::http-client", "wasm"] documentation = "https://docs.rs/reqwest-cross"