add a sse demo

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

33
Cargo.lock generated
View File

@ -1102,9 +1102,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.70" version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1231,13 +1231,18 @@ dependencies = [
"log", "log",
"mime", "mime",
"mime_guess", "mime_guess",
"serde",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream",
"tower 0.5.1", "tower 0.5.1",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-logger", "wasm-logger",
"web-sys",
"yew", "yew",
"yew-agent", "yew-agent",
"yew-router", "yew-router",
@ -2150,9 +2155,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.93" version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -2161,9 +2166,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.93" version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@ -2176,9 +2181,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.43" version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@ -2188,9 +2193,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.93" version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -2198,9 +2203,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.93" version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2211,9 +2216,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.93" version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]] [[package]]
name = "wasm-logger" name = "wasm-logger"

View File

@ -24,12 +24,17 @@ yew-router = "0.18"
gloo = "0.10" gloo = "0.10"
yew-agent = "0.3.0" yew-agent = "0.3.0"
futures = "0.3" futures = "0.3"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.45"
serde = { version = "1", features = ["derive"]}
web-sys = "0.3.70"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# server side # server side
axum = { version = "0.7", features = ["multipart", "tracing", "ws"] } axum = { version = "0.7", features = ["multipart", "tracing", "ws"] }
console-subscriber = { version = "0.4.0", optional = true } console-subscriber = { version = "0.4.0", optional = true }
tokio = { version = "1", features = ["full", "tracing"] } tokio = { version = "1", features = ["full", "tracing"] }
tokio-stream = { version = "0.1", features = ["time"] }
tower = { version = "0.5", features = ["tracing", "util"] } tower = { version = "0.5", features = ["tracing", "util"] }
tower-http = { version = "0.6", features = ["util", "trace", "catch-panic"] } tower-http = { version = "0.6", features = ["util", "trace", "catch-panic"] }
tracing = "0.1" tracing = "0.1"

View File

@ -1,14 +1,18 @@
use std::{collections::HashMap, str::FromStr}; use axum::response::sse::{Event, KeepAlive};
use axum::{ use axum::{
extract::Path, extract::{Path, Query},
http::{HeaderName, HeaderValue, Uri}, http::{HeaderName, HeaderValue, Uri},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use networkd::{ServerApp, ServerAppProps, Route}; use networkd::{Route, ServerApp, ServerAppProps};
use serde::Deserialize;
use std::convert::Infallible;
use std::{collections::HashMap, str::FromStr};
use tokio_stream::StreamExt as _;
use tower_http::{ use tower_http::{
trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse}, ServiceBuilderExt} trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse},
; ServiceBuilderExt,
};
use tracing::Level; use tracing::Level;
use yew::ServerRenderer; use yew::ServerRenderer;
@ -17,6 +21,28 @@ use yew_router::Routable;
static INDEX_PAGE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dist/index.html")); static INDEX_PAGE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dist/index.html"));
static DIST_DIR: include_dir::Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/dist"); static DIST_DIR: include_dir::Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/dist");
#[derive(Debug, Deserialize)]
struct SSEEchoParams {
#[serde(alias = "s", default)]
content: String,
}
// echo characters one by one
async fn sse_echo(Query(query): Query<SSEEchoParams>) -> impl IntoResponse {
let items: Vec<String> = query.content.chars().map(|c| c.to_string()).collect();
let stream = futures::stream::iter(items)
.map(|s| Ok::<_, Infallible>(Event::default().data(s.clone())))
.chain(futures::stream::pending())
.throttle(std::time::Duration::from_secs(1));
axum::response::sse::Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(std::time::Duration::from_secs(1))
.text("keepalive"),
)
}
#[tracing::instrument] #[tracing::instrument]
async fn rendering_pages( async fn rendering_pages(
url: Uri, url: Uri,
@ -70,6 +96,8 @@ async fn main() -> anyhow::Result<()> {
let api_router = axum::Router::new(); let api_router = axum::Router::new();
let oauth_router = axum::Router::new(); let oauth_router = axum::Router::new();
let sse_router = axum::Router::new().route("/echo", axum::routing::get(sse_echo));
let router = axum::Router::new() let router = axum::Router::new()
// not required to login // not required to login
.route("/static/*path", axum::routing::get(static_assets)) .route("/static/*path", axum::routing::get(static_assets))
@ -81,7 +109,7 @@ async fn main() -> anyhow::Result<()> {
// nesting router // nesting router
.nest("/api/", api_router) .nest("/api/", api_router)
.nest("/o/", oauth_router) .nest("/o/", oauth_router)
.nest("/sse", sse_router)
// fallback // fallback
.fallback(rendering_pages) .fallback(rendering_pages)
.layer( .layer(

View File

@ -1,6 +1,6 @@
pub mod app; pub mod app;
pub mod pages;
pub mod components; pub mod components;
pub mod pages;
pub mod worker; pub mod worker;
pub use app::*; pub use app::*;

View File

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

View File

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

View File

@ -1,7 +1,9 @@
pub mod index; pub mod index;
pub mod counter; pub mod counter;
pub mod echo;
pub mod _404; pub mod _404;
pub use counter::CounterPage; pub use counter::CounterPage;
pub use echo::EchoPage;
pub use index::IndexPage; pub use index::IndexPage;
pub use _404::NotFound; 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>
</>
}
}
}