add a sse demo
This commit is contained in:
parent
6ed24fb4bc
commit
d0cc4f33a6
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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(
|
||||||
|
@ -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::*;
|
||||||
|
@ -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 /> },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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
114
src/frontend/pages/echo.rs
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user