diff --git a/Cargo.lock b/Cargo.lock index 1d5639404..fe8e75818 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5398,6 +5398,8 @@ dependencies = [ "dioxus-core", "dioxus-devtools-types", "dioxus-signals", + "futures-channel", + "futures-util", "serde", "serde_json", "subsecond", @@ -7236,9 +7238,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", diff --git a/packages/devtools/Cargo.toml b/packages/devtools/Cargo.toml index 79f701179..0780c23f3 100644 --- a/packages/devtools/Cargo.toml +++ b/packages/devtools/Cargo.toml @@ -19,6 +19,9 @@ serde_json = { workspace = true } subsecond = { workspace = true } thiserror = { workspace = true } +futures-util = { workspace = true, features = ["sink", "async-await-macro"], optional = true } +futures-channel = { workspace = true, optional = true } + # hot reloading serve tracing = { workspace = true } warnings = { workspace = true } @@ -32,3 +35,7 @@ serde_json = { workspace = true } [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] + +[features] +default = [] +serve = ["dep:futures-util", "dep:futures-channel"] diff --git a/packages/devtools/src/lib.rs b/packages/devtools/src/lib.rs index 53aa73200..7e2e106c6 100644 --- a/packages/devtools/src/lib.rs +++ b/packages/devtools/src/lib.rs @@ -109,3 +109,104 @@ pub fn connect_at(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Sen } }); } + +/// Run this asynchronous future to completion. +/// +/// Whenever your code changes, the future is dropped and a new one is created using the new function. +/// +/// This is useful for using subsecond outside of dioxus, like with axum. To pass args to the underlying +/// function, you can use the `serve_subsecond_with_args` function. +/// +/// ```rust, ignore +/// #[tokio::main] +/// async fn main() { +/// dioxus_devtools::serve_subsecond(router_main).await; +/// } +/// +/// async fn router_main() { +/// use axum::{Router, routing::get}; +/// +/// let app = Router::new().route("/", get(test_route)); +/// +/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); +/// println!("Server running on http://localhost:3000"); +/// +/// axum::serve(listener, app.clone()).await.unwrap() +/// } +/// +/// async fn test_route() -> axum::response::Html<&'static str> { +/// "axum works!!!!!".into() +/// } +/// ``` +#[cfg(feature = "serve")] +#[cfg(not(target_family = "wasm"))] +pub async fn serve_subsecond(mut callback: impl FnMut() -> F) +where + F: std::future::Future + 'static, +{ + serve_subsecond_with_args((), move |_args| callback()).await +} + +/// Run this asynchronous future to completion. +/// +/// Whenever your code changes, the future is dropped and a new one is created using the new function. +/// +/// ```rust, ignore +/// #[tokio::main] +/// async fn main() { +/// let args = ("abc".to_string(),); +/// dioxus_devtools::serve_subsecond_with_args(args, router_main).await; +/// } +/// +/// async fn router_main(args: (String,)) { +/// use axum::{Router, routing::get}; +/// +/// let app = Router::new().route("/", get(test_route)); +/// +/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); +/// println!("Server running on http://localhost:3000 -> {}", args.0); +/// +/// axum::serve(listener, app.clone()).await.unwrap() +/// } +/// +/// async fn test_route() -> axum::response::Html<&'static str> { +/// "axum works!!!!!".into() +/// } +/// ``` +#[cfg(feature = "serve")] +pub async fn serve_subsecond_with_args(args: A, mut callback: impl FnMut(A) -> F) +where + F: std::future::Future + 'static, +{ + let (tx, mut rx) = futures_channel::mpsc::unbounded(); + + connect(move |msg| { + if let DevserverMsg::HotReload(hot_reload_msg) = msg { + if let Some(jumptable) = hot_reload_msg.jump_table { + if hot_reload_msg.for_pid == Some(std::process::id()) { + unsafe { subsecond::apply_patch(jumptable).unwrap() }; + tx.unbounded_send(()).unwrap(); + } + } + } + }); + + let wrapped = move |args| -> std::pin::Pin>> { + Box::pin(callback(args)) + }; + + let mut hotfn = subsecond::HotFn::current(wrapped); + let mut cur_future = hotfn.call((args.clone(),)); + + loop { + use futures_util::StreamExt; + let res = futures_util::future::select(cur_future, rx.next()).await; + + match res { + futures_util::future::Either::Left(_completed) => _ = rx.next().await, + futures_util::future::Either::Right(_reload) => {} + } + + cur_future = hotfn.call((args.clone(),)); + } +}