fix(terminal): clean up browser callbacks on unmount

This commit is contained in:
zhangheng
2026-06-24 11:21:06 +08:00
parent f68ee93c6d
commit 05bd106f06

View File

@@ -70,6 +70,35 @@ impl Default for TerminalViewport {
}
}
struct ResizeObserverHandle {
observer: ResizeObserver,
_callback: Closure<dyn FnMut(Array, ResizeObserver)>,
}
impl Drop for ResizeObserverHandle {
fn drop(&mut self) {
self.observer.disconnect();
}
}
struct WebSocketHandle {
socket: Rc<WebSocket>,
_on_open: Closure<dyn Fn(Event)>,
_on_message: Closure<dyn Fn(MessageEvent)>,
_on_close: Closure<dyn Fn(Event)>,
_on_error: Closure<dyn Fn(ErrorEvent)>,
}
impl Drop for WebSocketHandle {
fn drop(&mut self) {
self.socket.set_onopen(None);
self.socket.set_onmessage(None);
self.socket.set_onclose(None);
self.socket.set_onerror(None);
let _ = self.socket.close();
}
}
// ---------------------------------------------------------------------------
// TerminalPanel component
// ---------------------------------------------------------------------------
@@ -944,11 +973,13 @@ pub fn TerminalPanel(
// -- WebSocket lifecycle ------------------------------------------------
Effect::new(move |_| {
Effect::new(move |prev: Option<Option<WebSocketHandle>>| {
drop(prev);
let url = websocket_url();
let Ok(socket) = WebSocket::new(&url) else {
connection_status.set("Failed to connect".to_owned());
return;
let _ = connection_status.try_set("Failed to connect".to_owned());
return None;
};
socket.set_binary_type(BinaryType::Arraybuffer);
@@ -967,17 +998,36 @@ pub fn TerminalPanel(
let open_core = core_signal;
let open_tick = render_tick;
let on_open = Closure::<dyn Fn(Event)>::new(move |_| {
open_status.set("Connected".to_owned());
open_ws.set(Some((*open_socket).clone()));
if open_status.try_set("Connected".to_owned()).is_some() {
return;
}
if open_ws.try_set(Some((*open_socket).clone())).is_some() {
return;
}
let options = options_signal.get_untracked();
let Some(options) = options_signal.try_get_untracked() else {
return;
};
let viewport = measure_terminal_viewport(open_ref, open_measure, &options);
open_viewport.set(viewport);
cell_size.set(measure_cell_size(open_measure));
if open_viewport.try_set(viewport).is_some() {
return;
}
if cell_size.try_set(measure_cell_size(open_measure)).is_some() {
return;
}
open_core.update_untracked(|core| core.resize(viewport.rows, viewport.cols));
open_tick.update(|t| *t += 1);
open_mode.set(ViewMode::Live);
if open_core
.try_update_untracked(|core| core.resize(viewport.rows, viewport.cols))
.is_none()
{
return;
}
if open_tick.try_update(|t| *t += 1).is_none() {
return;
}
if open_mode.try_set(ViewMode::Live).is_some() {
return;
}
send_resize_message(&open_socket, viewport);
if let Some(on_resize) = on_resize {
@@ -987,7 +1037,6 @@ pub fn TerminalPanel(
focus_ime_textarea(&open_ime);
});
socket.set_onopen(Some(on_open.as_ref().unchecked_ref()));
on_open.forget();
// onmessage
let message_core = core_signal;
@@ -1006,7 +1055,7 @@ pub fn TerminalPanel(
};
let Ok(message) = serde_json::from_str::<ServerTerminalMessage>(&text) else {
message_status.set("Protocol error".to_owned());
let _ = message_status.try_set("Protocol error".to_owned());
return;
};
@@ -1032,47 +1081,54 @@ pub fn TerminalPanel(
);
}
ServerTerminalMessage::Exit { code } => {
message_status.set(match code {
let _ = message_status.try_set(match code {
Some(code) => format!("Exited ({code})"),
None => "Exited".to_owned(),
});
}
ServerTerminalMessage::Error { message } => {
message_status.set(format!("Error: {message}"));
let _ = message_status.try_set(format!("Error: {message}"));
}
ServerTerminalMessage::Pong => {}
}
});
socket.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
on_message.forget();
// onclose
let close_status = connection_status;
let close_ws = ws_signal;
let on_close = Closure::<dyn Fn(Event)>::new(move |_| {
close_status.set("Disconnected".to_owned());
close_ws.set(None);
let _ = close_status.try_set("Disconnected".to_owned());
let _ = close_ws.try_set(None);
});
socket.set_onclose(Some(on_close.as_ref().unchecked_ref()));
on_close.forget();
// onerror
let error_status = connection_status;
let on_error = Closure::<dyn Fn(ErrorEvent)>::new(move |_| {
error_status.set("Connection error".to_owned());
let _ = error_status.try_set("Connection error".to_owned());
});
socket.set_onerror(Some(on_error.as_ref().unchecked_ref()));
on_error.forget();
Some(WebSocketHandle {
socket,
_on_open: on_open,
_on_message: on_message,
_on_close: on_close,
_on_error: on_error,
})
});
// -- ResizeObserver -----------------------------------------------------
Effect::new(move |_| {
Effect::new(move |prev: Option<Option<ResizeObserverHandle>>| {
drop(prev);
let Some(element) = terminal_ref.get() else {
return;
return None;
};
let Some(socket) = ws_signal.get() else {
return;
return None;
};
let resize_socket = socket.clone();
@@ -1087,11 +1143,13 @@ pub fn TerminalPanel(
let resize_ime = ime_ref;
let callback = Closure::<dyn FnMut(Array, ResizeObserver)>::new(
move |entries: Array, _observer: ResizeObserver| {
let Some(options) = options_signal.try_get_untracked() else {
return;
};
let Some(entry) = entries.get(0).dyn_into::<ResizeObserverEntry>().ok() else {
return;
};
let rect = entry.content_rect();
let options = options_signal.get_untracked();
let viewport = measure_terminal_viewport_from_rect(
rect.width(),
rect.height(),
@@ -1099,17 +1157,38 @@ pub fn TerminalPanel(
&options,
);
if viewport != resize_viewport.get_untracked() {
resize_viewport.set(viewport);
cell_size.set(measure_cell_size(resize_measure));
let Some(current_viewport) = resize_viewport.try_get_untracked() else {
return;
};
if viewport != current_viewport {
if resize_viewport.try_set(viewport).is_some() {
return;
}
if cell_size
.try_set(measure_cell_size(resize_measure))
.is_some()
{
return;
}
let mode_val = resize_mode.get_untracked();
let st = resize_st.get_untracked();
let Some(mode_val) = resize_mode.try_get_untracked() else {
return;
};
let Some(st) = resize_st.try_get_untracked() else {
return;
};
resize_core.update_untracked(|core| {
core.resize(viewport.rows, viewport.cols);
});
resize_tick.update(|t| *t += 1);
if resize_core
.try_update_untracked(|core| {
core.resize(viewport.rows, viewport.cols);
})
.is_none()
{
return;
}
if resize_tick.try_update(|t| *t += 1).is_none() {
return;
}
if mode_val == ViewMode::Live {
sync_scroll_to_bottom(resize_ref, resize_prog_scroll);
@@ -1129,12 +1208,14 @@ pub fn TerminalPanel(
let Ok(observer) = ResizeObserver::new(callback.as_ref().unchecked_ref::<Function>())
else {
return;
return None;
};
observer.observe(&element);
callback.forget();
std::mem::forget(observer);
Some(ResizeObserverHandle {
observer,
_callback: callback,
})
});
// -- Keyboard input -----------------------------------------------------