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}