leptos_axum_socket/lib.rs
1//! Realtime pub/sub communication for Leptos + Axum applications.
2//!
3//! ## Usage
4//!
5//! ```
6//! # use leptos::prelude::*;
7//! # use leptos_axum_socket::{expect_socket_context, ServerSocket, SocketMsg};
8//! # use serde::{Serialize, Deserialize};
9//! # use axum::extract::{State, FromRef};
10//! #
11//! # #[derive(FromRef, Clone)]
12//! # pub struct AppState {
13//! # pub socket: ServerSocket,
14//! # }
15//! #
16//! // Define the key and message types
17//! #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
18//! pub struct MyKey {
19//! pub bla: String,
20//! }
21//!
22//! #[derive(Clone, Serialize, Deserialize, Debug)]
23//! pub struct MyMsg {
24//! pub awesome_msg: String,
25//! }
26//!
27//! // Implement the SocketMsg trait for MyMsg to link the key and message types
28//! impl SocketMsg for MyMsg {
29//! type Key = MyKey;
30//! #[cfg(feature = "ssr")]
31//! type AppState = AppState;
32//! }
33//!
34//! #[component]
35//! pub fn MyComponent() -> impl IntoView {
36//! let socket = expect_socket_context();
37//!
38//! // Subscribe to receive messages that are sent with the given key
39//! socket.subscribe(
40//! MyKey {
41//! bla: "bla".to_string(),
42//! },
43//! |msg: &MyMsg| {
44//! // Simply log the message
45//! leptos::logging::log!("message: {msg:#?}");
46//! },
47//! );
48//!
49//! let on_click = move || {
50//! // Send a message with the given key
51//! socket.send(
52//! MyKey {
53//! bla: "bla".to_string(),
54//! },
55//! MyMsg {
56//! awesome_msg: "awesome message".to_string(),
57//! },
58//! );
59//! };
60//!
61//! view! { "..." }
62//! }
63//!
64//! #[server]
65//! pub async fn my_server_function() -> Result<(), ServerFnError> {
66//! // Send from the server
67//! leptos_axum_socket::send(
68//! &MyKey {
69//! bla: "bla".to_string(),
70//! },
71//! &MyMsg {
72//! awesome_msg: "Hello, world!".to_string(),
73//! },
74//! ).await;
75//!
76//! Ok(())
77//! }
78//! ```
79//!
80//! For this to work you have to prepare a little bit.
81//!
82//! Define your app state in your lib.rs:
83//!
84//! ```
85//! use leptos::prelude::*;
86//!
87//! #[cfg(feature = "ssr")]
88//! #[derive(Clone, axum::extract::FromRef)]
89//! pub struct AppState {
90//! // This is required for Leptos Axum Socket to work
91//! pub socket: leptos_axum_socket::ServerSocket,
92//!
93//! // this is required for Leptos to work with axum
94//! pub leptos_options: LeptosOptions,
95//! }
96//! ```
97//!
98//! Initialize your Axum app (probably in main.rs):
99//!
100//! ```
101//! # use leptos::prelude::*;
102//! # use leptos_axum_socket::{ServerSocket, SocketMsg, SocketRoute, handlers::upgrade_websocket};
103//! # use serde::{Deserialize, Serialize};
104//! # use axum::{Router, extract::{State, WebSocketUpgrade, FromRef}, response::Response};
105//! # use leptos_axum::{generate_route_list, LeptosRoutes};
106//! #
107//! # #[derive(Clone, FromRef)]
108//! # pub struct AppState {
109//! # pub server_socket: ServerSocket,
110//! # pub leptos_options: LeptosOptions,
111//! # }
112//! #
113//! # fn shell(options: LeptosOptions) -> impl IntoView {
114//! # ()
115//! # }
116//! # fn App() -> impl IntoView {
117//! # ()
118//! # }
119//! #
120//! # #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
121//! # pub struct MyKey {
122//! # pub bla: String,
123//! # }
124//! #
125//! # #[derive(Clone, Serialize, Deserialize, Debug)]
126//! # pub struct MyMsg {
127//! # pub awesome_msg: String,
128//! # }
129//! #
130//! # impl SocketMsg for MyMsg {
131//! # type Key = MyKey;
132//! # #[cfg(feature = "ssr")]
133//! # type AppState = AppState;
134//! # }
135//! #
136//! #[tokio::main]
137//! async fn main() {
138//! let conf = get_configuration(None).unwrap();
139//! let addr = conf.leptos_options.site_addr;
140//!
141//! let routes = generate_route_list(App);
142//!
143//! // Construct the Axum app state
144//! let state = AppState {
145//! leptos_options: conf.leptos_options,
146//! server_socket: ServerSocket::new(),
147//! };
148//!
149//! // Optional: add subscription filters and message mappers
150//! {
151//! let mut server_socket = state.server_socket.lock().await;
152//! server_socket.add_subscribe_filter(async |key: MyKey, _ctx: &()| { key.bla == "bla" });
153//! server_socket.add_send_mapper(|key: MyKey, msg: MyMsg, _ctx: &()| {
154//! if key.bla == "bla" {
155//! Some(MyMsg {
156//! awesome_msg: msg.awesome_msg.replace("old", "new"),
157//! })
158//! } else {
159//! None
160//! }
161//! });
162//! }
163//!
164//! // Init the Axum app
165//! let app: Router<AppState> = Router::new()
166//! .leptos_routes(&state, routes, {
167//! let leptos_options = state.leptos_options.clone();
168//! move || shell(leptos_options.clone())
169//! })
170//! .socket_route(connect_to_websocket) // Register the socket route (implementation below)
171//! .fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
172//! .with_state(state); // Register the state
173//!
174//! let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
175//! // axum::serve(listener, app.into_make_service())
176//! // .await
177//! // .unwrap();
178//! }
179//!
180//! // Implement the `connect_to_websocket` handler:
181//! #[cfg(feature = "ssr")]
182//! pub async fn connect_to_websocket(
183//! ws: WebSocketUpgrade,
184//! State(socket): State<ServerSocket>,
185//! ) -> Response {
186//! // You could do authentication here
187//!
188//! // Provide extra context like the user's ID for example that is passed to the permission filters
189//! let ctx = ();
190//!
191//! upgrade_websocket(ws, socket, ctx)
192//! }
193//! ```
194//!
195//! And finally provide the context in your root Leptos component:
196//!
197//! ```
198//! # use leptos::prelude::*;
199//! # use leptos_axum_socket::provide_socket_context;
200//! #
201//! #[component]
202//! pub fn App() -> impl IntoView {
203//! provide_socket_context();
204//!
205//! view! { "..." }
206//! }
207//! ```
208//!
209//! ### Axum Handlers
210//!
211//! You can also send messages from inside axum handlers.
212//! Checkout [`ServerSocketInner::send`] and [`ServerSocketInner::send_to_self`].
213
214pub mod channel;
215#[cfg(feature = "ssr")]
216pub mod handlers;
217
218pub use crate::channel::*;
219
220/// Implement this trait to link your socket message types to your key types.
221/// In order to use this crate you have to implement this trait for your socket messages.
222///
223/// On the server you have to provide the application state as well.
224///
225/// ```
226/// # use leptos_axum_socket::{ServerSocket, SocketMsg};
227/// # use serde::{Serialize, Deserialize};
228/// # use axum::extract::FromRef;
229/// #
230/// # #[derive(FromRef, Clone)]
231/// # pub struct AppState {
232/// # pub socket: ServerSocket,
233/// # }
234/// #
235/// // Define the key and message types
236/// #[derive(Clone, Serialize, Deserialize)]
237/// pub struct MyKey {
238/// pub bla: String,
239/// }
240///
241/// #[derive(Clone, Serialize, Deserialize, Debug)]
242/// pub struct MyMsg {
243/// pub awesome_msg: String,
244/// }
245///
246/// // Implement the SocketMsg trait for MyMsg to link the key and message types
247/// impl SocketMsg for MyMsg {
248/// type Key = MyKey;
249/// #[cfg(feature = "ssr")]
250/// type AppState = AppState;
251/// }
252/// ```
253pub trait SocketMsg {
254 type Key;
255 #[cfg(feature = "ssr")]
256 type AppState;
257}
258
259/// Trait to extend the Axum router
260#[cfg(feature = "ssr")]
261pub trait SocketRoute<S>
262where
263 S: Clone + Send + Sync + 'static,
264{
265 /// Add the necessary websocket route to the Axum router
266 fn socket_route<H, T>(self, handler: H) -> Self
267 where
268 H: axum::handler::Handler<T, S>,
269 T: 'static;
270}
271
272#[cfg(feature = "ssr")]
273impl<S> SocketRoute<S> for axum::Router<S>
274where
275 S: Clone + Send + Sync + 'static,
276 ServerSocket: axum::extract::FromRef<S>,
277{
278 fn socket_route<H, T>(self, handler: H) -> Self
279 where
280 H: axum::handler::Handler<T, S>,
281 T: 'static,
282 {
283 use axum::routing::get;
284 use tracing::debug;
285
286 debug!("Adding websocket route to {WEBSOCKET_CHANNEL_URL}");
287
288 self.route(WEBSOCKET_CHANNEL_URL, get(handler))
289 }
290}