Skip to content

Commit 66cbf45

Browse files
committed
Add test of server sent events
1 parent ba52291 commit 66cbf45

File tree

9 files changed

+409
-3
lines changed

9 files changed

+409
-3
lines changed

Cargo.lock

+46
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
22
resolver = "2"
3-
members = ["app", "frontend", "server", "fnord_ui", "qbittorrent_rs", "qbittorrent_rs_proto"]
3+
members = ["app", "frontend", "server", "fnord_ui", "qbittorrent_rs", "qbittorrent_rs_proto", "leptos_sse"]
44

55
# need to be applied only to wasm build
66
[profile.release]

app/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ bincode = { version = "1.3.3", optional = true }
2828
base64 = { version = "0.22.1", optional = true }
2929
anyhow.workspace = true
3030

31+
leptos_sse = { path = "../leptos_sse" }
32+
3133
[features]
3234
default = []
3335
hydrate = ["leptos/hydrate", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "fnord-ui/hydrate"]
34-
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum", "dep:simple_crypt", "dep:bincode", "dep:base64", "fnord-ui/ssr", "qbittorrent-rs"]
36+
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum", "dep:simple_crypt", "dep:bincode", "dep:base64", "fnord-ui/ssr", "qbittorrent-rs", "leptos_sse/ssr"]
3537

app/src/lib.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use leptos_meta::*;
99
use leptos_router::{components::*, StaticSegment};
1010

1111
use fnord_ui::components::{Navbar, NavbarBrand};
12+
use leptos_sse::sse_signal;
13+
use serde::{Deserialize, Serialize};
1214

1315
pub mod error_template;
1416

@@ -73,7 +75,7 @@ fn HomePage(is_auth: Signal<bool>, action: ServerAction<Login>) -> impl IntoView
7375
let res = move || {
7476
if is_auth() {
7577
Either::Left(view! {
76-
<div>"Hello"</div>
78+
<div><Dashboard /></div>
7779
})
7880
} else {
7981
Either::Right(view! {
@@ -105,3 +107,20 @@ fn HomePage(is_auth: Signal<bool>, action: ServerAction<Login>) -> impl IntoView
105107
<div>{res()}</div>
106108
}
107109
}
110+
111+
#[component]
112+
fn Dashboard() -> impl IntoView {
113+
// Provide websocket connection
114+
// leptos_sse::provide_sse("http://localhost:3000/sse").unwrap();
115+
116+
// Create sse signal
117+
let count = sse_signal::<Count>("http://localhost:3010/sse");
118+
view! {
119+
<div>Count: {move || { view! { <span>{count.get().value.to_string()}</span>}}}</div>
120+
}
121+
}
122+
123+
#[derive(Clone, Default, Serialize, Deserialize)]
124+
pub struct Count {
125+
pub value: i32,
126+
}

leptos_sse/Cargo.toml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[package]
2+
name = "leptos_sse"
3+
version = "0.4.0"
4+
edition = "2021"
5+
description = "Leptos server signals synced through server-sent-events (SSE)"
6+
repository = "https://github.com/messense/leptos_sse"
7+
license = "MIT"
8+
keywords = ["leptos", "server", "signal", "sse"]
9+
categories = [
10+
"wasm",
11+
"web-programming",
12+
"web-programming::http-client",
13+
"web-programming::http-server",
14+
]
15+
16+
[dependencies]
17+
cfg-if = "1"
18+
js-sys = "0.3.61"
19+
json-patch = "1.0.0"
20+
leptos.workspace = true
21+
serde = { version = "1.0.160", features = ["derive"] }
22+
serde_json = "1.0"
23+
wasm-bindgen = { version = "0.2.84", default-features = false }
24+
web-sys = { version = "0.3.61", features = ["EventSource", "MessageEvent"] }
25+
pin-project-lite = "0.2.12"
26+
tokio = { version = "1.36.0", optional = true }
27+
tokio-stream = { version = "0.1.14", optional = true }
28+
tracing.workspace = true
29+
30+
axum = { version = "0.7", default-features = false, features = [
31+
"tokio",
32+
"json",
33+
], optional = true }
34+
futures = { version = "0.3.28", default-features = false, optional = true }
35+
36+
[features]
37+
default = []
38+
ssr = ["dep:axum", "dep:futures", "dep:tokio", "dep:tokio-stream"]
39+
40+
[package.metadata.docs.rs]
41+
features = ["ssr"]
42+
rustdoc-args = ["--cfg", "docsrs"]

leptos_sse/src/axum.rs

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use std::borrow::Cow;
2+
use std::pin::Pin;
3+
use std::task::Poll;
4+
5+
use axum::response::sse::Event;
6+
use futures::stream::{Stream, StreamExt, TryStream};
7+
use pin_project_lite::pin_project;
8+
use serde::Serialize;
9+
use serde_json::Value;
10+
use tokio::sync::mpsc;
11+
pub use tokio::sync::mpsc::error::{SendError, TrySendError};
12+
use tokio_stream::wrappers::ReceiverStream;
13+
14+
use crate::ServerSignalUpdate;
15+
16+
pin_project! {
17+
/// A signal owned by the server which writes to the SSE when mutated.
18+
#[derive(Clone, Debug)]
19+
pub struct ServerSentEvents<S> {
20+
name: Cow<'static, str>,
21+
#[pin]
22+
stream: S,
23+
json_value: Value,
24+
}
25+
}
26+
27+
impl<S> ServerSentEvents<S> {
28+
/// Create a new [`ServerSentEvents`] a stream, initializing `T` to default.
29+
///
30+
/// This function can fail if serilization of `T` fails.
31+
pub fn new<T>(name: impl Into<Cow<'static, str>>, stream: S) -> Result<Self, serde_json::Error>
32+
where
33+
T: Default + Serialize,
34+
S: TryStream<Ok = T, Error = axum::BoxError>,
35+
{
36+
Ok(ServerSentEvents {
37+
name: name.into(),
38+
stream,
39+
json_value: serde_json::to_value(T::default())?,
40+
})
41+
}
42+
43+
/// Create a server-sent-events (SSE) channel pair.
44+
///
45+
/// The `buffer` argument controls how many unsent messages can be stored without waiting.
46+
///
47+
/// The first item in the tuple is the MPSC channel sender half.
48+
pub fn channel<T>(
49+
name: impl Into<Cow<'static, str>>,
50+
buffer: usize,
51+
) -> Result<
52+
(
53+
Sender<T>,
54+
ServerSentEvents<impl TryStream<Ok = T, Error = axum::BoxError>>,
55+
),
56+
serde_json::Error,
57+
>
58+
where
59+
T: Default + Serialize,
60+
{
61+
let (sender, receiver) = mpsc::channel::<T>(buffer);
62+
let stream = ReceiverStream::new(receiver).map(Ok);
63+
Ok((Sender(sender), ServerSentEvents::new(name, stream)?))
64+
}
65+
}
66+
67+
impl<S> Stream for ServerSentEvents<S>
68+
where
69+
S: TryStream<Error = axum::BoxError>,
70+
S::Ok: Serialize,
71+
{
72+
type Item = Result<Event, axum::BoxError>;
73+
74+
fn poll_next(
75+
self: Pin<&mut Self>,
76+
cx: &mut std::task::Context<'_>,
77+
) -> Poll<Option<Self::Item>> {
78+
let this = self.project();
79+
match this.stream.try_poll_next(cx) {
80+
Poll::Ready(Some(Ok(value))) => {
81+
let new_json = serde_json::to_value(value)?;
82+
let update = ServerSignalUpdate::new_from_json::<S::Item>(
83+
this.name.clone(),
84+
this.json_value,
85+
&new_json,
86+
);
87+
*this.json_value = new_json;
88+
let event = Event::default().json_data(update)?;
89+
Poll::Ready(Some(Ok(event)))
90+
}
91+
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
92+
Poll::Ready(None) => Poll::Ready(None),
93+
Poll::Pending => Poll::Pending,
94+
}
95+
}
96+
}
97+
98+
/// Sender half of a server-sent events stream.
99+
#[derive(Clone, Debug)]
100+
pub struct Sender<T>(mpsc::Sender<T>);
101+
102+
impl<T> Sender<T> {
103+
/// Send an SSE message.
104+
pub async fn send(&self, value: T) -> Result<(), SendError<T>>
105+
where
106+
T: Serialize,
107+
{
108+
self.0.send(value).await
109+
}
110+
111+
/// Attempts to immediately send an SSE message.
112+
pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>>
113+
where
114+
T: Serialize,
115+
{
116+
self.0.try_send(value)
117+
}
118+
}

0 commit comments

Comments
 (0)