add a sse demo

This commit is contained in:
2024-10-16 17:48:15 +08:00
parent 6ed24fb4bc
commit d0cc4f33a6
8 changed files with 180 additions and 22 deletions

View File

@ -10,6 +10,8 @@ pub enum Route {
Home,
#[at("/counter")]
Counter,
#[at("/echo")]
Echo,
#[not_found]
#[at("/404")]
NotFound,
@ -22,6 +24,7 @@ fn switch(routes: Route) -> Html {
Route::Home => html! { <IndexPage /> },
Route::Counter => html! { <CounterPage /> },
Route::NotFound => html! { <NotFound /> },
Route::Echo => html! { <EchoPage /> },
}
}

View File

@ -42,6 +42,7 @@ pub fn nav_bar() -> Html {
for (route, title) in [
(Route::Home, "Home"),
(Route::Counter, "Counter"),
(Route::Echo, "Echo"),
] {
let active_item = if let Some(ref location) = maybe_location {
let path = location.path();

View File

@ -1,7 +1,9 @@
pub mod index;
pub mod counter;
pub mod echo;
pub mod _404;
pub use counter::CounterPage;
pub use echo::EchoPage;
pub use index::IndexPage;
pub use _404::NotFound;

114
src/frontend/pages/echo.rs Normal file
View File

@ -0,0 +1,114 @@
use futures::{FutureExt, StreamExt};
use std::rc::Rc;
use std::time::Duration;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew::{function_component, html, use_state_eq, Callback, Html, InputEvent};
#[derive(Clone, PartialEq, Default)]
struct ReducibleString(String);
enum StringReducer {
Append(String),
}
impl Reducible for ReducibleString {
type Action = StringReducer;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
match action {
StringReducer::Append(string) => Rc::new(Self(format!("{}{}", self.0, string))),
}
}
}
#[function_component]
pub fn EchoPage() -> Html {
let response = use_reducer(ReducibleString::default);
let name = use_state_eq(|| String::new());
let name_setter_for_input = {
let name = name.clone();
Callback::from(move |ev: InputEvent| {
let name_writer = name.setter();
let Some(target) = ev.target() else {
return;
};
// Events can bubble so this listener might catch events from child
// elements which are not of type HtmlInputElement
let input = target.dyn_into::<HtmlInputElement>().ok();
if let Some(input) = input {
name_writer.set(input.value());
}
})
};
let start_sse = {
let name = name.clone();
let response = response.clone();
Callback::from(move |_| {
let response = response.clone();
let url = format!("/sse/echo?s={}", *name);
let mut eventsource = match gloo::net::eventsource::futures::EventSource::new(&url) {
Ok(eventsource) => eventsource,
Err(err) => {
gloo::console::error!(format!("failed to open event source: {}", err));
return;
}
};
let mut subscription = match eventsource.subscribe("message") {
Ok(subscription) => subscription,
Err(err) => {
gloo::console::error!(format!("failed to subscribe on event source: {}", err));
return;
}
};
wasm_bindgen_futures::spawn_local(async move {
loop {
futures::select_biased! {
message = subscription.next().fuse() => {
let Some(data) = message else {
continue
};
let (_, event) = match data {
Ok(data) => data,
Err(err) => {
gloo::console::error!(format!("failed to fetch data from event source: {}", err));
continue;
}
};
let Some(data) = event.data().as_string() else {
continue
};
response.dispatch(StringReducer::Append(data));
}, // subscription.next
_ = gloo::timers::future::sleep(Duration::from_secs(5)).fuse() => {
break
},
}
}
gloo::console::log!("eventsource 5: ", format!("{:?}", eventsource.state()));
});
})
};
if (*response).0.len() > 0 {
let response = (*response).clone().0;
html! {
<>
{response}
</>
}
} else {
let name = (*name).clone();
html! {
<>
<input value={name} oninput={name_setter_for_input} />
<button onclick={start_sse}>{"Hi"}</button>
</>
}
}
}