Skip to content

Commit 32a415d

Browse files
authored
#147 feat: add automatic cleanup for EventHandle via Drop trait (#151)
Implement RAII pattern for event handler cleanup to prevent memory leaks: - Add Drop impl for EventHandle that automatically unregisters callbacks - Add unregistered flag to prevent double cleanup - Log errors during automatic cleanup instead of panicking - Update EventHandle documentation with automatic cleanup examples This eliminates the need for manual cleanup in most cases: - Event handlers automatically unregister when handle drops - Prevents memory leaks from forgotten callbacks - Prevents stale callbacks executing after component destruction - Still supports manual cleanup via off_event() if needed Benefits: - Idiomatic Rust lifecycle management - Follows RAII principles - Simplifies framework integration (Yew, Leptos) - All existing tests pass (208 tests)
1 parent 1bcad6d commit 32a415d

File tree

1 file changed

+82
-8
lines changed

1 file changed

+82
-8
lines changed

src/webapp/types.rs

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,35 @@ use js_sys::{Function, Object, Reflect};
55
use serde::Serialize;
66
use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
77

8+
use crate::logger;
9+
810
/// Handle returned when registering callbacks.
11+
///
12+
/// Automatically unregisters the callback when dropped, implementing RAII
13+
/// cleanup pattern to prevent memory leaks.
14+
///
15+
/// # Examples
16+
///
17+
/// ```no_run
18+
/// use telegram_webapp_sdk::TelegramWebApp;
19+
///
20+
/// if let Some(app) = TelegramWebApp::instance() {
21+
/// // Handle is automatically cleaned up when scope ends
22+
/// let handle = app
23+
/// .on_theme_changed(|| {
24+
/// println!("Theme changed!");
25+
/// })
26+
/// .expect("subscribe");
27+
///
28+
/// // No manual cleanup needed - Drop handles it
29+
/// } // <- handle dropped here, callback unregistered automatically
30+
/// ```
931
pub struct EventHandle<T: ?Sized> {
10-
pub(super) target: Object,
11-
pub(super) method: &'static str,
12-
pub(super) event: Option<String>,
13-
pub(super) callback: Closure<T>
32+
pub(super) target: Object,
33+
pub(super) method: &'static str,
34+
pub(super) event: Option<String>,
35+
pub(super) callback: Closure<T>,
36+
pub(super) unregistered: bool
1437
}
1538

1639
impl<T: ?Sized> EventHandle<T> {
@@ -24,27 +47,78 @@ impl<T: ?Sized> EventHandle<T> {
2447
target,
2548
method,
2649
event,
27-
callback
50+
callback,
51+
unregistered: false
2852
}
2953
}
3054

31-
pub(crate) fn unregister(self) -> Result<(), JsValue> {
55+
pub(crate) fn unregister(mut self) -> Result<(), JsValue> {
56+
if self.unregistered {
57+
return Ok(());
58+
}
59+
3260
let f = Reflect::get(&self.target, &self.method.into())?;
3361
let func = f
3462
.dyn_ref::<Function>()
3563
.ok_or_else(|| JsValue::from_str(&format!("{} is not a function", self.method)))?;
36-
match self.event {
64+
match &self.event {
3765
Some(event) => func.call2(
3866
&self.target,
39-
&event.into(),
67+
&event.clone().into(),
4068
self.callback.as_ref().unchecked_ref()
4169
)?,
4270
None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())?
4371
};
72+
73+
self.unregistered = true;
4474
Ok(())
4575
}
4676
}
4777

78+
impl<T: ?Sized> Drop for EventHandle<T> {
79+
/// Automatically unregisters the event callback when the handle is dropped.
80+
///
81+
/// This implements the RAII pattern, ensuring that event handlers are
82+
/// properly cleaned up even if the user forgets to manually unregister.
83+
/// Errors during unregistration are logged but do not panic.
84+
fn drop(&mut self) {
85+
if self.unregistered {
86+
return;
87+
}
88+
89+
let f = match Reflect::get(&self.target, &self.method.into()) {
90+
Ok(f) => f,
91+
Err(_) => {
92+
logger::error("Failed to get unregister method");
93+
return;
94+
}
95+
};
96+
97+
let func = match f.dyn_ref::<Function>() {
98+
Some(func) => func,
99+
None => {
100+
logger::error(&format!("{} is not a function", self.method));
101+
return;
102+
}
103+
};
104+
105+
let result = match &self.event {
106+
Some(event) => func.call2(
107+
&self.target,
108+
&event.clone().into(),
109+
self.callback.as_ref().unchecked_ref()
110+
),
111+
None => func.call1(&self.target, self.callback.as_ref().unchecked_ref())
112+
};
113+
114+
if result.is_err() {
115+
logger::error("Failed to unregister event callback");
116+
}
117+
118+
self.unregistered = true;
119+
}
120+
}
121+
48122
/// Identifies which bottom button to operate on.
49123
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50124
pub enum BottomButton {

0 commit comments

Comments
 (0)