Skip to content

Commit 3971030

Browse files
committed
feat: integrate with subsecond
This allows us to implement hot patching for the request handlers. More details: https://docs.rs/subsecond/
1 parent 0643b2d commit 3971030

8 files changed

Lines changed: 499 additions & 69 deletions

File tree

Cargo.lock

Lines changed: 347 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ time = { version = "0.3.44", default-features = false }
132132
tokio = { version = "1.48", default-features = false }
133133
toml = { version = "0.9", default-features = false }
134134
tower = "0.5.2"
135-
tower-livereload = "0.9.6"
135+
#tower-livereload = { git = "https://github.com/leotaku/tower-livereload.git", rev = "05d1d9acf7a265b91e800a6dd3599dd6f0359c8e" }
136+
tower-livereload = "0.9"
136137
tower-sessions = { version = "0.14", default-features = false }
137138
tracing = { version = "0.1", default-features = false }
138139
tracing-subscriber = "0.3"

cot-macros/src/main_fn.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ pub(super) fn fn_to_cot_main(main_function_decl: ItemFn) -> syn::Result<TokenStr
1818
let crate_name = cot_ident();
1919
let result = quote! {
2020
fn main() {
21-
let body = async {
21+
async fn body() {
2222
let project = __cot_main();
2323
#crate_name::run_cli(project).await.expect(
2424
"failed to run the Cot project"
2525
);
2626

2727
#new_main_decl
28-
};
28+
}
2929
#[expect(clippy::expect_used)]
3030
{
3131
return #crate_name::__private::tokio::runtime::Builder::new_multi_thread()
3232
.enable_all()
3333
.build()
3434
.expect("Failed building the Runtime")
35-
.block_on(body);
35+
.block_on(::cot::__private::dioxus_devtools::serve_subsecond(body));
3636
}
3737
}
3838
};

cot/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ tower-sessions = { workspace = true, features = ["memory-store"] }
7070
tracing.workspace = true
7171
url = { workspace = true, features = ["serde"] }
7272

73+
dioxus-devtools = { version = "0.7.2", features = ["serve"] }
74+
subsecond = { version = "0.7.2" }
75+
7376
[dev-dependencies]
7477
async-stream.workspace = true
7578
criterion = { workspace = true, features = ["async_tokio"] }

cot/src/handler.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ pub(crate) fn into_box_request_handler<T, H: RequestHandler<T> + Send + Sync>(
6565
&self,
6666
request: Request,
6767
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + '_>> {
68-
Box::pin(self.0.handle(request))
68+
let mut hot_fn = subsecond::HotFn::current(|req| self.0.handle(req));
69+
Box::pin(hot_fn.call((request,)))
6970
}
7071
}
7172

@@ -98,7 +99,8 @@ macro_rules! impl_request_handler {
9899
let $ty = <$ty as FromRequestHead>::from_request_head(&head).await?;
99100
)*
100101

101-
self.clone()($($ty,)*).await.into_response()
102+
let mut hot_fn = subsecond::HotFn::current(move |$($ty,)*| self.clone()($($ty,)*));
103+
hot_fn.call(($($ty,)*)).await.into_response()
102104
}
103105
}
104106
};
@@ -136,7 +138,12 @@ macro_rules! impl_request_handler_from_request {
136138

137139
let $ty_from_request = $ty_from_request::from_request(&head, body).await?;
138140

139-
self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*).await.into_response()
141+
let mut hot_fn = subsecond::HotFn::current(
142+
move |($($ty_lhs,)* $ty_from_request, $($ty_rhs),*)| {
143+
self.clone()($($ty_lhs,)* $ty_from_request, $($ty_rhs),*)
144+
}
145+
);
146+
hot_fn.call((($($ty_lhs,)* $ty_from_request, $($ty_rhs),*),)).await.into_response()
140147
}
141148
}
142149
};
@@ -154,7 +161,7 @@ macro_rules! handle_all_parameters {
154161
$name!(P1, P2, P3, P4, P5, P6, P7);
155162
$name!(P1, P2, P3, P4, P5, P6, P7, P8);
156163
$name!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
157-
$name!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
164+
// $name!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
158165
};
159166
}
160167

cot/src/middleware/live_reload.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,24 @@ impl LiveReloadMiddleware {
131131
(
132132
IntoCotErrorLayer::new(),
133133
IntoCotResponseLayer::new(),
134-
tower_livereload::LiveReloadLayer::new(),
134+
Self::create_live_reload_layer(),
135135
)
136136
});
137137
Self(tower::util::option_layer(option_layer))
138138
}
139+
140+
fn create_live_reload_layer() -> tower_livereload::LiveReloadLayer {
141+
let layer = tower_livereload::LiveReloadLayer::new();
142+
let reloader = layer.reloader();
143+
subsecond::register_handler(std::sync::Arc::new(move || {
144+
reloader.reload();
145+
if let Some(notify) = crate::project::RELOAD_NOTIFY.get() {
146+
println!("reloading connected clients");
147+
notify.notify_waiters();
148+
}
149+
}));
150+
layer
151+
}
139152
}
140153

141154
#[cfg(feature = "live-reload")]

cot/src/private.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ pub use crate::utils::graph::apply_permutation;
2626
/// This is used in the CLI to specify the version of the crate to use in the
2727
/// `Cargo.toml` file when creating a new Cot project.
2828
pub const COT_VERSION: &str = env!("CARGO_PKG_VERSION");
29+
30+
pub use dioxus_devtools;

cot/src/project.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,7 @@ impl Bootstrapper<WithCache> {
13861386
let auth_backend = self.project.auth_backend(&self.context);
13871387
let context = self.context.with_auth(auth_backend);
13881388

1389+
// dioxus_devtools::
13891390
Ok(Bootstrapper {
13901391
project: self.project,
13911392
context,
@@ -2140,7 +2141,7 @@ pub async fn run_at_with_shutdown(
21402141
};
21412142
std::panic::set_hook(Box::new(new_hook));
21422143
}
2143-
axum::serve(listener, handler.into_make_service())
2144+
axum::serve(ResetListener::new(listener), handler.into_make_service())
21442145
.with_graceful_shutdown(shutdown_signal)
21452146
.await
21462147
.map_err(StartServerError)?;
@@ -2155,6 +2156,121 @@ pub async fn run_at_with_shutdown(
21552156
Ok(())
21562157
}
21572158

2159+
pub(crate) static RELOAD_NOTIFY: std::sync::OnceLock<Arc<tokio::sync::Notify>> =
2160+
std::sync::OnceLock::new();
2161+
2162+
#[derive(Debug)]
2163+
struct ResetListener {
2164+
inner: tokio::net::TcpListener,
2165+
}
2166+
2167+
impl ResetListener {
2168+
fn new(inner: tokio::net::TcpListener) -> Self {
2169+
Self { inner }
2170+
}
2171+
}
2172+
2173+
impl axum::serve::Listener for ResetListener {
2174+
type Io = ResetStream;
2175+
type Addr = std::net::SocketAddr;
2176+
2177+
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
2178+
loop {
2179+
match self.inner.accept().await {
2180+
Ok((stream, addr)) => {
2181+
let notify = RELOAD_NOTIFY.get_or_init(|| Arc::new(tokio::sync::Notify::new()));
2182+
let notify = notify.clone();
2183+
return (
2184+
ResetStream {
2185+
inner: stream,
2186+
reset_fut: Box::pin(async move { notify.notified().await }),
2187+
},
2188+
addr,
2189+
);
2190+
}
2191+
Err(_err) => {
2192+
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2193+
}
2194+
}
2195+
}
2196+
}
2197+
2198+
fn local_addr(&self) -> std::io::Result<Self::Addr> {
2199+
self.inner.local_addr()
2200+
}
2201+
}
2202+
2203+
struct ResetStream {
2204+
inner: tokio::net::TcpStream,
2205+
reset_fut: std::pin::Pin<Box<dyn Future<Output = ()> + Send>>,
2206+
}
2207+
2208+
impl Debug for ResetStream {
2209+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2210+
f.debug_struct("ResetStream")
2211+
.field("inner", &self.inner)
2212+
.finish()
2213+
}
2214+
}
2215+
2216+
impl tokio::io::AsyncRead for ResetStream {
2217+
fn poll_read(
2218+
mut self: std::pin::Pin<&mut Self>,
2219+
cx: &mut std::task::Context<'_>,
2220+
buf: &mut tokio::io::ReadBuf<'_>,
2221+
) -> std::task::Poll<std::io::Result<()>> {
2222+
if self.reset_fut.as_mut().poll(cx).is_ready() {
2223+
return std::task::Poll::Ready(Err(std::io::Error::new(
2224+
std::io::ErrorKind::ConnectionReset,
2225+
"connection reset by live reload",
2226+
)));
2227+
}
2228+
std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
2229+
}
2230+
}
2231+
2232+
impl tokio::io::AsyncWrite for ResetStream {
2233+
fn poll_write(
2234+
mut self: std::pin::Pin<&mut Self>,
2235+
cx: &mut std::task::Context<'_>,
2236+
buf: &[u8],
2237+
) -> std::task::Poll<std::io::Result<usize>> {
2238+
if self.reset_fut.as_mut().poll(cx).is_ready() {
2239+
return std::task::Poll::Ready(Err(std::io::Error::new(
2240+
std::io::ErrorKind::ConnectionReset,
2241+
"connection reset by live reload",
2242+
)));
2243+
}
2244+
std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
2245+
}
2246+
2247+
fn poll_flush(
2248+
mut self: std::pin::Pin<&mut Self>,
2249+
cx: &mut std::task::Context<'_>,
2250+
) -> std::task::Poll<std::io::Result<()>> {
2251+
if self.reset_fut.as_mut().poll(cx).is_ready() {
2252+
return std::task::Poll::Ready(Err(std::io::Error::new(
2253+
std::io::ErrorKind::ConnectionReset,
2254+
"connection reset by live reload",
2255+
)));
2256+
}
2257+
std::pin::Pin::new(&mut self.inner).poll_flush(cx)
2258+
}
2259+
2260+
fn poll_shutdown(
2261+
mut self: std::pin::Pin<&mut Self>,
2262+
cx: &mut std::task::Context<'_>,
2263+
) -> std::task::Poll<std::io::Result<()>> {
2264+
if self.reset_fut.as_mut().poll(cx).is_ready() {
2265+
return std::task::Poll::Ready(Err(std::io::Error::new(
2266+
std::io::ErrorKind::ConnectionReset,
2267+
"connection reset by live reload",
2268+
)));
2269+
}
2270+
std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
2271+
}
2272+
}
2273+
21582274
#[derive(Debug, Error)]
21592275
#[error("failed to start the server: {0}")]
21602276
pub(crate) struct StartServerError(#[from] pub(crate) std::io::Error);

0 commit comments

Comments
 (0)