fix(terminal): clean up browser callbacks on unmount
This commit is contained in:
@@ -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 -----------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user