diff --git a/app/src/terminal/component.rs b/app/src/terminal/component.rs index ffb152d..8728d20 100644 --- a/app/src/terminal/component.rs +++ b/app/src/terminal/component.rs @@ -70,6 +70,35 @@ impl Default for TerminalViewport { } } +struct ResizeObserverHandle { + observer: ResizeObserver, + _callback: Closure, +} + +impl Drop for ResizeObserverHandle { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +struct WebSocketHandle { + socket: Rc, + _on_open: Closure, + _on_message: Closure, + _on_close: Closure, + _on_error: Closure, +} + +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>| { + 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::::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::(&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::::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::::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>| { + 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::::new( move |entries: Array, _observer: ResizeObserver| { + let Some(options) = options_signal.try_get_untracked() else { + return; + }; let Some(entry) = entries.get(0).dyn_into::().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::()) else { - return; + return None; }; observer.observe(&element); - callback.forget(); - std::mem::forget(observer); + Some(ResizeObserverHandle { + observer, + _callback: callback, + }) }); // -- Keyboard input -----------------------------------------------------