From c895024686cd0a0b878e7e1cd66c78adf0d4c940 Mon Sep 17 00:00:00 2001 From: Robert Macomber Date: Sat, 17 Feb 2024 09:25:47 -0800 Subject: [PATCH] Make it so that cancelled timeouts don't leak 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 --- 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`