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