From 4e45dda7689c8692c9471eaad881be02a33dfe47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 8 Dec 2025 08:16:28 +0100 Subject: [PATCH 1/3] feat: integrate with subsecond This allows us to implement hot patching for the request handlers. More details: https://docs.rs/subsecond/ --- Cargo.lock | 303 ++++++++++++++++-- Cargo.toml | 3 +- cot-cli/src/new_project.rs | 3 +- cot-cli/src/project_template/bacon.toml | 5 - cot-macros/src/main_fn.rs | 6 +- cot/Cargo.toml | 5 +- cot/src/handler.rs | 21 +- cot/src/hot_patching.rs | 42 +++ cot/src/lib.rs | 1 + cot/src/middleware.rs | 2 +- cot/src/middleware/live_reload.rs | 37 ++- .../middleware/live_reload/reset_listener.rs | 111 +++++++ cot/src/private.rs | 4 + cot/src/project.rs | 14 +- 14 files changed, 516 insertions(+), 41 deletions(-) delete mode 100644 cot-cli/src/project_template/bacon.toml create mode 100644 cot/src/hot_patching.rs create mode 100644 cot/src/middleware/live_reload/reset_listener.rs diff --git a/Cargo.lock b/Cargo.lock index 42de5670..bc5ec976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -428,9 +428,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.50" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -616,6 +616,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -692,6 +712,7 @@ dependencies = [ "derive_builder", "derive_more", "digest", + "dioxus-devtools", "email_address", "fake", "fantoccini", @@ -726,6 +747,7 @@ dependencies = [ "serde_urlencoded", "sha2", "sqlx", + "subsecond", "subtle", "swagger-ui-redist", "sync_wrapper", @@ -984,6 +1006,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "deadpool" version = "0.12.3" @@ -1114,6 +1142,87 @@ dependencies = [ "subtle", ] +[[package]] +name = "dioxus-cli-config" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e9d9da2e7334fdae5d77e3989207aa549062f74ff1ca2171393bbdd7fda90" + +[[package]] +name = "dioxus-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7400cbd21a98e585a13f8c29574da9b8afb2fd343f712618042b6c71761f0933" +dependencies = [ + "anyhow", + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond", + "tracing", +] + +[[package]] +name = "dioxus-core-types" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0652ab5f9c2c32261d44a3155debbfd909ed03d03434d7f70f5a796bf255c519" + +[[package]] +name = "dioxus-devtools" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9748128bcd102b10e58c765939807053ccab542206a939b8bab228077455c259" +dependencies = [ + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "futures-channel", + "futures-util", + "serde", + "serde_json", + "subsecond", + "thiserror 2.0.17", + "tracing", + "tungstenite", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48540ca8a0ab1ec81cd4db35f0c9713d43b158647fc1dcb0d79965fc3b41d96c" +dependencies = [ + "dioxus-core", + "serde", + "subsecond-types", +] + +[[package]] +name = "dioxus-signals" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27fc4df7a31a7f02e5a0b40884bb66ee165226a05d75fce03baa44029e438762" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "parking_lot", + "rustc-hash", + "tracing", + "warnings", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1344,9 +1453,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -1515,6 +1624,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generational-box" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e658d10252a15200ca4a1c67c7180fc0baffa3f92869bbd903025daf6f70fd65" +dependencies = [ + "parking_lot", + "tracing", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2088,9 +2207,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -2126,6 +2245,16 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.15" @@ -2134,13 +2263,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -2188,6 +2317,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + [[package]] name = "matchers" version = "0.2.0" @@ -2219,6 +2354,24 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -2615,6 +2768,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2754,9 +2927,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -2887,9 +3060,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags", ] @@ -3043,9 +3216,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -3203,9 +3376,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", @@ -3285,10 +3458,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3320,6 +3494,16 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -3573,6 +3757,34 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subsecond" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09bc2c9ef0381b403ab8b58122961cb83266d16b1f55f9486d5857ba4a9ae26" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07aa455c66ddfdbb51507537402b961e027846468954ef8d974bce65dff9eb0" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4130,6 +4342,23 @@ dependencies = [ "toml", ] +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" @@ -4193,6 +4422,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4251,6 +4486,28 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4860,6 +5117,6 @@ dependencies = [ [[package]] name = "zmij" -version = "0.1.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e404bcd8afdaf006e529269d3e85a743f9480c3cef60034d77860d02964f3ba" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" diff --git a/Cargo.toml b/Cargo.toml index 9fc024c5..eb4668e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,7 +140,8 @@ time = { version = "0.3.44", default-features = false } tokio = { version = "1.48", default-features = false } toml = { version = "0.9", default-features = false } tower = "0.5.2" -tower-livereload = "0.9.6" +#tower-livereload = { git = "https://github.com/leotaku/tower-livereload.git", rev = "05d1d9acf7a265b91e800a6dd3599dd6f0359c8e" } +tower-livereload = "0.9" tower-sessions = { version = "0.14", default-features = false } tracing = { version = "0.1", default-features = false } tracing-subscriber = "0.3" diff --git a/cot-cli/src/new_project.rs b/cot-cli/src/new_project.rs index 1241bc15..254f2090 100644 --- a/cot-cli/src/new_project.rs +++ b/cot-cli/src/new_project.rs @@ -13,10 +13,9 @@ macro_rules! project_file { }; } -const PROJECT_FILES: [(&str, &str); 10] = [ +const PROJECT_FILES: [(&str, &str); 9] = [ project_file!("Cargo.toml.template"), project_file!("Cargo.lock.template"), - project_file!("bacon.toml"), project_file!(".gitignore"), project_file!("src/main.rs"), project_file!("src/migrations.rs"), diff --git a/cot-cli/src/project_template/bacon.toml b/cot-cli/src/project_template/bacon.toml deleted file mode 100644 index fbbca7d5..00000000 --- a/cot-cli/src/project_template/bacon.toml +++ /dev/null @@ -1,5 +0,0 @@ -[jobs.serve] -command = ["cargo", "run"] -background = false -on_change_strategy = "kill_then_restart" -watch = ["templates", "static"] diff --git a/cot-macros/src/main_fn.rs b/cot-macros/src/main_fn.rs index 804b3f3a..4d39a61f 100644 --- a/cot-macros/src/main_fn.rs +++ b/cot-macros/src/main_fn.rs @@ -18,21 +18,21 @@ pub(super) fn fn_to_cot_main(main_function_decl: ItemFn) -> syn::Result + Send + Sync>( &self, request: Request, ) -> Pin> + Send + '_>> { - Box::pin(self.0.handle(request)) + Box::pin(crate::hot_patching::call_hot( + |req| self.0.handle(req), + request, + )) } } @@ -98,7 +101,12 @@ macro_rules! impl_request_handler { let $ty = <$ty as FromRequestHead>::from_request_head(&head).await?; )* - self.clone()($($ty,)*).await.into_response() + $crate::__private::hot_patching::call_hot( + move |($($ty,)*)| self.clone()($($ty,)*), + ($($ty,)*), + ) + .await + .into_response() } } }; @@ -136,7 +144,14 @@ macro_rules! impl_request_handler_from_request { let $ty_from_request = $ty_from_request::from_request(&head, body).await?; - self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*).await.into_response() + $crate::__private::hot_patching::call_hot( + move |($($ty_lhs,)* $ty_from_request, $($ty_rhs),*)| { + self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*) + }, + ($($ty_lhs,)* $ty_from_request, $($ty_rhs),*), + ) + .await + .into_response() } } }; diff --git a/cot/src/hot_patching.rs b/cot/src/hot_patching.rs new file mode 100644 index 00000000..7334d4aa --- /dev/null +++ b/cot/src/hot_patching.rs @@ -0,0 +1,42 @@ +#[allow( + clippy::allow_attributes, + reason = "Only happens when hot-patching is enabled" +)] +#[allow( + clippy::future_not_send, + reason = "Send not needed; serve/Bootstrapper is run async in a single thread" +)] +#[doc(hidden)] +pub async fn serve(callback: impl FnMut() -> F) +where + F: Future + 'static, +{ + #[cfg(feature = "hot-patching")] + { + dioxus_devtools::serve_subsecond(callback).await; + } + + #[cfg(not(feature = "hot-patching"))] + { + let mut callback = callback; // avoid "variable does not need to be mutable" warnings + callback().await; + } +} + +#[doc(hidden)] +pub fn call_hot(func: F, args: A) -> R +where + F: FnMut(A) -> R, +{ + #[cfg(feature = "hot-patching")] + { + let mut hot_fn = subsecond::HotFn::current(func); + hot_fn.call((args,)) + } + + #[cfg(not(feature = "hot-patching"))] + { + let mut func = func; // avoid "variable does not need to be mutable" warnings + func(args) + } +} diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 3cfb5994..6d1e0e68 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -76,6 +76,7 @@ pub mod config; mod error_page; #[macro_use] pub(crate) mod handler; +pub(crate) mod hot_patching; pub mod html; #[cfg(feature = "json")] pub mod json; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index b9e6e27c..e3b10ccd 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -35,7 +35,7 @@ use crate::session::store::redis::RedisStore; use crate::{Body, Error}; #[cfg(feature = "live-reload")] -mod live_reload; +pub(crate) mod live_reload; #[cfg(feature = "live-reload")] pub use live_reload::LiveReloadMiddleware; diff --git a/cot/src/middleware/live_reload.rs b/cot/src/middleware/live_reload.rs index 9ea31521..bde3eaa1 100644 --- a/cot/src/middleware/live_reload.rs +++ b/cot/src/middleware/live_reload.rs @@ -1,5 +1,12 @@ +mod reset_listener; + +use std::sync::Mutex; + use cot::middleware::{IntoCotErrorLayer, IntoCotResponseLayer}; use cot::project::MiddlewareContext; +pub(crate) use reset_listener::ResetListener; +use tower_livereload::Reloader; +use tracing::trace; #[cfg(feature = "live-reload")] type LiveReloadLayerType = tower::util::Either< @@ -131,13 +138,41 @@ impl LiveReloadMiddleware { ( IntoCotErrorLayer::new(), IntoCotResponseLayer::new(), - tower_livereload::LiveReloadLayer::new(), + Self::create_live_reload_layer(), ) }); Self(tower::util::option_layer(option_layer)) } + + fn create_live_reload_layer() -> tower_livereload::LiveReloadLayer { + let layer = tower_livereload::LiveReloadLayer::new(); + + let mut reloaders = RELOADERS + .lock() + .expect("reloaders mutex was poisoned; please restart the server"); + if reloaders.is_empty() { + subsecond::register_handler(std::sync::Arc::new(reload_clients)); + } + reloaders.push(layer.reloader()); + layer + } } +fn reload_clients() { + let reloaders = RELOADERS + .lock() + .expect("reloaders mutex was poisoned; please restart the server"); + for reloader in &*reloaders { + reloader.reload(); + } + if let Some(notify) = reset_listener::RELOAD_NOTIFY.get() { + trace!("reloading connected clients"); + notify.notify_waiters(); + } +} + +static RELOADERS: Mutex> = Mutex::new(Vec::new()); + #[cfg(feature = "live-reload")] impl Default for LiveReloadMiddleware { fn default() -> Self { diff --git a/cot/src/middleware/live_reload/reset_listener.rs b/cot/src/middleware/live_reload/reset_listener.rs new file mode 100644 index 00000000..7b210c5a --- /dev/null +++ b/cot/src/middleware/live_reload/reset_listener.rs @@ -0,0 +1,111 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use derive_more::with_trait::Debug; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::{TcpListener, TcpStream}; + +pub(super) static RELOAD_NOTIFY: std::sync::OnceLock> = + std::sync::OnceLock::new(); + +/// A wrapper over [`TcpListener`] that can reset existing TCP connections when +/// a globally defined signal is set. +/// +/// This is useful for hot-patching, so that the clients that are already +/// connected won't use the connection to the old code and will use the +/// hotpatched versions of the handlers instead. +#[derive(Debug)] +pub(crate) struct ResetListener { + inner: TcpListener, +} + +impl ResetListener { + pub(crate) fn new(inner: TcpListener) -> Self { + Self { inner } + } +} + +impl axum::serve::Listener for ResetListener { + type Io = ResetStream; + type Addr = std::net::SocketAddr; + + async fn accept(&mut self) -> (Self::Io, Self::Addr) { + loop { + match self.inner.accept().await { + Ok((stream, addr)) => { + let notify = RELOAD_NOTIFY.get_or_init(|| Arc::new(tokio::sync::Notify::new())); + let notify = notify.clone(); + return ( + ResetStream { + inner: stream, + reset_fut: Box::pin(async move { notify.notified().await }), + }, + addr, + ); + } + Err(_err) => { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + } + } + } + + fn local_addr(&self) -> std::io::Result { + self.inner.local_addr() + } +} + +#[derive(Debug)] +pub(crate) struct ResetStream { + inner: TcpStream, + #[debug("..")] + reset_fut: Pin + Send>>, +} + +impl ResetStream { + fn forward_to_inner( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + f: F, + ) -> Poll> + where + F: FnOnce(Pin<&mut TcpStream>, &mut Context<'_>) -> Poll>, + { + if self.reset_fut.as_mut().poll(cx).is_ready() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "connection reset by live reload", + ))); + } + f(Pin::new(&mut self.inner), cx) + } +} + +impl AsyncRead for ResetStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + self.forward_to_inner(cx, |inner, cx| inner.poll_read(cx, buf)) + } +} + +impl AsyncWrite for ResetStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.forward_to_inner(cx, |inner, cx| inner.poll_write(cx, buf)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.forward_to_inner(cx, AsyncWrite::poll_flush) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.forward_to_inner(cx, AsyncWrite::poll_shutdown) + } +} diff --git a/cot/src/private.rs b/cot/src/private.rs index 8fe02bdd..f34470dd 100644 --- a/cot/src/private.rs +++ b/cot/src/private.rs @@ -26,3 +26,7 @@ pub use crate::utils::graph::apply_permutation; /// This is used in the CLI to specify the version of the crate to use in the /// `Cargo.toml` file when creating a new Cot project. pub const COT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub mod hot_patching { + pub use crate::hot_patching::{call_hot, serve}; +} diff --git a/cot/src/project.rs b/cot/src/project.rs index c4014cb0..75725d0f 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -1386,6 +1386,7 @@ impl Bootstrapper { let auth_backend = self.project.auth_backend(&self.context); let context = self.context.with_auth(auth_backend); + // dioxus_devtools:: Ok(Bootstrapper { project: self.project, context, @@ -2140,7 +2141,7 @@ pub async fn run_at_with_shutdown( }; std::panic::set_hook(Box::new(new_hook)); } - axum::serve(listener, handler.into_make_service()) + axum::serve(make_reset_listener(listener), handler.into_make_service()) .with_graceful_shutdown(shutdown_signal) .await .map_err(StartServerError)?; @@ -2155,6 +2156,17 @@ pub async fn run_at_with_shutdown( Ok(()) } +#[cfg(feature = "live-reload")] +fn make_reset_listener( + listener: tokio::net::TcpListener, +) -> crate::middleware::live_reload::ResetListener { + crate::middleware::live_reload::ResetListener::new(listener) +} +#[cfg(not(feature = "live-reload"))] +fn make_reset_listener(listener: tokio::net::TcpListener) -> tokio::net::TcpListener { + listener +} + #[derive(Debug, Error)] #[error("failed to start the server: {0}")] pub(crate) struct StartServerError(#[from] pub(crate) std::io::Error); From 373033b3e0133f39898d2b84856ed6d4482839de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 29 Dec 2025 12:20:11 +0100 Subject: [PATCH 2/3] address review comment --- cot/src/project.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cot/src/project.rs b/cot/src/project.rs index 75725d0f..db123b6d 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -2156,15 +2156,18 @@ pub async fn run_at_with_shutdown( Ok(()) } -#[cfg(feature = "live-reload")] fn make_reset_listener( listener: tokio::net::TcpListener, -) -> crate::middleware::live_reload::ResetListener { - crate::middleware::live_reload::ResetListener::new(listener) -} -#[cfg(not(feature = "live-reload"))] -fn make_reset_listener(listener: tokio::net::TcpListener) -> tokio::net::TcpListener { - listener +) -> impl axum::serve::Listener { + #[cfg(feature = "live-reload")] + { + crate::middleware::live_reload::ResetListener::new(listener) + } + + #[cfg(not(feature = "live-reload"))] + { + listener + } } #[derive(Debug, Error)] From a18eb8a09be1397fe20d5ba20836bd5a0f92b963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Tue, 30 Dec 2025 19:28:58 +0100 Subject: [PATCH 3/3] more work --- Cargo.lock | 18 ----- ...ot_testing__new__create_new_project-5.snap | 4 +- ...ot_testing__new__create_new_project-6.snap | 4 +- ...create_new_project_with_custom_name-5.snap | 4 +- ...create_new_project_with_custom_name-6.snap | 4 +- cot/Cargo.toml | 6 +- cot/src/handler.rs | 17 +++-- cot/src/hot_patching.rs | 72 ++++++++++++++++++- cot/src/private.rs | 2 +- 9 files changed, 92 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ac61c7c..2427a0dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,14 +1145,10 @@ dependencies = [ [[package]] name = "dioxus-cli-config" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e9d9da2e7334fdae5d77e3989207aa549062f74ff1ca2171393bbdd7fda90" [[package]] name = "dioxus-core" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7400cbd21a98e585a13f8c29574da9b8afb2fd343f712618042b6c71761f0933" dependencies = [ "anyhow", "const_format", @@ -1173,14 +1169,10 @@ dependencies = [ [[package]] name = "dioxus-core-types" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0652ab5f9c2c32261d44a3155debbfd909ed03d03434d7f70f5a796bf255c519" [[package]] name = "dioxus-devtools" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9748128bcd102b10e58c765939807053ccab542206a939b8bab228077455c259" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -1199,8 +1191,6 @@ dependencies = [ [[package]] name = "dioxus-devtools-types" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48540ca8a0ab1ec81cd4db35f0c9713d43b158647fc1dcb0d79965fc3b41d96c" dependencies = [ "dioxus-core", "serde", @@ -1210,8 +1200,6 @@ dependencies = [ [[package]] name = "dioxus-signals" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27fc4df7a31a7f02e5a0b40884bb66ee165226a05d75fce03baa44029e438762" dependencies = [ "dioxus-core", "futures-channel", @@ -1627,8 +1615,6 @@ dependencies = [ [[package]] name = "generational-box" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e658d10252a15200ca4a1c67c7180fc0baffa3f92869bbd903025daf6f70fd65" dependencies = [ "parking_lot", "tracing", @@ -3760,8 +3746,6 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subsecond" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09bc2c9ef0381b403ab8b58122961cb83266d16b1f55f9486d5857ba4a9ae26" dependencies = [ "js-sys", "libc", @@ -3779,8 +3763,6 @@ dependencies = [ [[package]] name = "subsecond-types" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07aa455c66ddfdbb51507537402b961e027846468954ef8d974bce65dff9eb0" dependencies = [ "serde", ] diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap index 28005faf..d4b8e043 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-5.snap @@ -1,19 +1,19 @@ --- source: cot-cli/tests/snapshot_testing/new/mod.rs +assertion_line: 19 description: "Verbosity level: debug" info: program: cot args: - new - "-vvvv" - - /tmp/cot-test-o4uWVf/project + - /tmp/cot-test-cGGhse/project --- success: true exit_code: 0 ----- stdout ----- TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock" -TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs" diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap index f1a8e8da..c584c528 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project-6.snap @@ -1,19 +1,19 @@ --- source: cot-cli/tests/snapshot_testing/new/mod.rs +assertion_line: 19 description: "Verbosity level: trace" info: program: cot args: - new - "-vvvvv" - - /tmp/cot-test-QUOaBC/project + - /tmp/cot-test-npgw5S/project --- success: true exit_code: 0 ----- stdout ----- TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock" -TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs" diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap index e9fb34e5..df02b840 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-5.snap @@ -1,5 +1,6 @@ --- source: cot-cli/tests/snapshot_testing/new/mod.rs +assertion_line: 39 description: "Verbosity level: debug" info: program: cot @@ -8,14 +9,13 @@ info: - "--name" - my_project - "-vvvv" - - /tmp/cot-test-BEJYfS/project + - /tmp/cot-test-3W8pnv/project --- success: true exit_code: 0 ----- stdout ----- TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock" -TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs" diff --git a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap index 2d703e8f..d0b9ad96 100644 --- a/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap +++ b/cot-cli/tests/snapshot_testing/new/snapshots/cli__snapshot_testing__new__create_new_project_with_custom_name-6.snap @@ -1,5 +1,6 @@ --- source: cot-cli/tests/snapshot_testing/new/mod.rs +assertion_line: 39 description: "Verbosity level: trace" info: program: cot @@ -8,14 +9,13 @@ info: - "--name" - my_project - "-vvvvv" - - /tmp/cot-test-IWoQbg/project + - /tmp/cot-test-fHh4vf/project --- success: true exit_code: 0 ----- stdout ----- TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/Cargo.lock" -TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/bacon.toml" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/.gitignore" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/main.rs" TIMESTAMP TRACE cot_cli::new_project: Writing file: "/tmp/TEMP_PATH/project/src/migrations.rs" diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 8ff6c87f..c9097435 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -30,7 +30,8 @@ deadpool-redis = { workspace = true, features = ["tokio-comp", "rt_tokio_1"], op derive_builder.workspace = true derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } digest.workspace = true -dioxus-devtools = { version = "0.7.2", features = ["serve"], optional = true } +#dioxus-devtools = { version = "0.7.2", features = ["serve"], optional = true } +dioxus-devtools = { path = "/home/m4tx/projects/dioxus/packages/devtools/", features = ["serve"], optional = true } email_address.workspace = true fake = { workspace = true, optional = true, features = ["derive", "chrono"] } form_urlencoded.workspace = true @@ -58,7 +59,8 @@ serde_json = { workspace = true, optional = true } serde_path_to_error = { workspace = true } sha2.workspace = true sqlx = { workspace = true, features = ["runtime-tokio", "chrono"], optional = true } -subsecond = { version = "0.7.2", optional = true } +#subsecond = { version = "0.7.2", optional = true } +subsecond = { path = "/home/m4tx/projects/dioxus/packages/subsecond/subsecond/", optional = true } subtle = { workspace = true, features = ["std"] } swagger-ui-redist = { workspace = true, optional = true } sync_wrapper.workspace = true diff --git a/cot/src/handler.rs b/cot/src/handler.rs index 225eaf39..98eddd8f 100644 --- a/cot/src/handler.rs +++ b/cot/src/handler.rs @@ -66,7 +66,10 @@ pub(crate) fn into_box_request_handler + Send + Sync>( request: Request, ) -> Pin> + Send + '_>> { Box::pin(crate::hot_patching::call_hot( - |req| self.0.handle(req), + |req| { + Box::pin(self.0.handle(req)) + as Pin> + Send>> + }, request, )) } @@ -81,8 +84,8 @@ macro_rules! impl_request_handler { where Func: FnOnce($($ty,)*) -> Fut + Clone + Send + Sync + 'static, $($ty: FromRequestHead + Send,)* - Fut: Future + Send, - R: IntoResponse, + Fut: Future + Send + 'static, + R: IntoResponse + 'static, { #[allow( clippy::allow_attributes, @@ -101,8 +104,8 @@ macro_rules! impl_request_handler { let $ty = <$ty as FromRequestHead>::from_request_head(&head).await?; )* - $crate::__private::hot_patching::call_hot( - move |($($ty,)*)| self.clone()($($ty,)*), + $crate::__private::hot_patching::call_async( + move |($($ty,)*)| Box::pin(self.clone()($($ty,)*)) as Pin + Send>>, ($($ty,)*), ) .await @@ -120,7 +123,7 @@ macro_rules! impl_request_handler_from_request { $($ty_lhs: FromRequestHead + Send,)* $ty_from_request: FromRequest + Send, $($ty_rhs: FromRequestHead + Send,)* - Fut: Future + Send, + Fut: Future + Send + 'static, R: IntoResponse, { #[allow( @@ -144,7 +147,7 @@ macro_rules! impl_request_handler_from_request { let $ty_from_request = $ty_from_request::from_request(&head, body).await?; - $crate::__private::hot_patching::call_hot( + $crate::__private::hot_patching::call_async( move |($($ty_lhs,)* $ty_from_request, $($ty_rhs),*)| { self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*) }, diff --git a/cot/src/hot_patching.rs b/cot/src/hot_patching.rs index 7334d4aa..2287241b 100644 --- a/cot/src/hot_patching.rs +++ b/cot/src/hot_patching.rs @@ -1,3 +1,13 @@ +use std::panic::AssertUnwindSafe; +use std::pin::Pin; + +use subsecond::{HotFn, HotFnPanic}; + +/// Runs given future with [`subsecond`], dropping the future and re-running it +/// when the code changes. +/// +/// When the hot-patching feature is not enabled, the function just runs the +/// future once. #[allow( clippy::allow_attributes, reason = "Only happens when hot-patching is enabled" @@ -6,14 +16,19 @@ clippy::future_not_send, reason = "Send not needed; serve/Bootstrapper is run async in a single thread" )] -#[doc(hidden)] pub async fn serve(callback: impl FnMut() -> F) where F: Future + 'static, { + println!("1"); + #[cfg(feature = "hot-patching")] { - dioxus_devtools::serve_subsecond(callback).await; + println!("2"); + // dioxus_devtools::serve_subsecond(callback).await; + dioxus_devtools::connect_subsecond(); + let mut callback = callback; + callback().await; } #[cfg(not(feature = "hot-patching"))] @@ -23,7 +38,10 @@ where } } -#[doc(hidden)] +/// Calls the function using [`subsecond::HotFn`]. +/// +/// This causes the function passed to be hot-reloadable. If the hot-reloading +/// feature is not enabled, the function is called directly. pub fn call_hot(func: F, args: A) -> R where F: FnMut(A) -> R, @@ -40,3 +58,51 @@ where func(args) } } + +pub fn call_async(f: F, args: A) -> Pin + Send>> +where + F: FnOnce(A) -> Fut, + Fut: Future + Send + 'static, +{ + // return Box::pin(f(args)); + + // For FnOnce, we need to handle this differently since we can only call it once + // We'll store the closure in an Option and take it when needed + let mut f_option = Some(f); + + // Create a wrapper function that boxes the future + let wrapper = move |args| -> Pin + Send>> { + if let Some(closure) = f_option.take() { + Box::pin(closure(args)) + } else { + // This shouldn't happen in normal hot reload scenarios since each + // hot reload creates a new call_async invocation + panic!( + "Hot reload closure already consumed - this indicates a problem with the hot reload system" + ) + } + }; + + let mut hotfn = HotFn::current(wrapper); + loop { + let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call((args,)))); + + // If the call succeeds just return the result, otherwise we try to handle the + // panic if its our own. + let err = match res { + Ok(res) => return res, + Err(err) => err, + }; + + // If this is our panic then let's handle it, otherwise we just resume unwinding + let Some(_hot_payload) = err.downcast_ref::() else { + std::panic::resume_unwind(err); + }; + + // For hot reload with FnOnce, we can't retry with the same closure + // The hot reload system should create a new function call entirely + panic!( + "Hot reload detected but cannot retry with FnOnce closure - hot reload should create new function instance" + ); + } +} diff --git a/cot/src/private.rs b/cot/src/private.rs index f34470dd..fffc3dec 100644 --- a/cot/src/private.rs +++ b/cot/src/private.rs @@ -28,5 +28,5 @@ pub use crate::utils::graph::apply_permutation; pub const COT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub mod hot_patching { - pub use crate::hot_patching::{call_hot, serve}; + pub use crate::hot_patching::{call_async, call_hot, serve}; }