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
|
// 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 -----------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user