From 47abe009938c493ba10c57ce6a50ddd4387dcdde Mon Sep 17 00:00:00 2001 From: rjmac Date: Mon, 19 Feb 2024 18:17:26 -0800 Subject: [PATCH] fix: don't leak canceled timeouts (#2331) Instead of using `Closure::once_into_js`, this uses `into_js_value`, which uses weak references to clean up the closure when Javascript no longer has need of it. It would be nice to make this (and the similar interval function) drop the callback promptly when cancelled, but I don't think that's possible while keeping the handles Copy. Fixes #2330 Co-authored-by: Robert Macomber --- leptos_dom/src/helpers.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/leptos_dom/src/helpers.rs b/leptos_dom/src/helpers.rs index 3dbb269b25..99fe8a4e28 100644 --- a/leptos_dom/src/helpers.rs +++ b/leptos_dom/src/helpers.rs @@ -103,6 +103,25 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) { _ = request_animation_frame_with_handle(cb); } +// Closure::once_into_js only frees the callback when it's actually +// called, so this instead uses into_js_value, which can be freed by +// the host JS engine's GC if it supports weak references (which all +// modern brower engines do). The way this works is that the provided +// callback's captured data is dropped immediately after being called, +// as before, but it leaves behind a small stub closure rust-side that +// will be freed "eventually" by the JS GC. If the function is never +// called (e.g., it's a cancelled timeout or animation frame callback) +// then it will also be freed eventually. +fn closure_once(cb: impl FnOnce() + 'static) -> JsValue { + let mut wrapped_cb: Option> = Some(Box::new(cb)); + let closure = Closure::new(move || { + if let Some(cb) = wrapped_cb.take() { + cb() + } + }); + closure.into_js_value() +} + /// Runs the given function between the next repaint using /// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame), /// returning a cancelable handle. @@ -128,7 +147,7 @@ pub fn request_animation_frame_with_handle( .map(AnimationFrameRequestHandle) } - raf(Closure::once_into_js(cb)) + raf(closure_once(cb)) } /// Handle that is generated by [request_idle_callback_with_handle] and can be @@ -237,7 +256,7 @@ pub fn set_timeout_with_handle( .map(TimeoutHandle) } - st(Closure::once_into_js(cb), duration) + st(closure_once(cb), duration) } /// "Debounce" a callback function. This will cause it to wait for a period of `delay`