sync/race problems + dropped frames on exit problems
This commit is contained in:
6 files changed
+187
-34
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Inbox, MoreHorizontal, RefreshCw, Search, Zap } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -69,8 +69,12 @@ export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [wakeBusyKey, setWakeBusyKey] = useState("");
|
||||
const [details, setDetails] = useState<FleetDevice | null>(null);
|
||||
const loadSeqRef = useRef(0);
|
||||
|
||||
async function loadFleet() {
|
||||
// Latest request wins: filter changes and debounced searches overlap, and
|
||||
// an older response must never overwrite fresher state.
|
||||
const seq = ++loadSeqRef.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [nextDevices, nextKnownDevices] = await Promise.all([
|
||||
@@ -84,12 +88,14 @@ export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
|
||||
}),
|
||||
fetchKnownDevices(),
|
||||
]);
|
||||
if (seq !== loadSeqRef.current) return;
|
||||
setDevices(nextDevices);
|
||||
setKnownDevices(nextKnownDevices);
|
||||
} catch (err) {
|
||||
if (seq !== loadSeqRef.current) return;
|
||||
toast.error("Failed to load fleet", { description: String(err) });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (seq === loadSeqRef.current) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +158,13 @@ export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [presence, known, agentId]);
|
||||
|
||||
// The filter effect above already loads on mount; only react to query edits.
|
||||
const mountedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!mountedRef.current) {
|
||||
mountedRef.current = true;
|
||||
return;
|
||||
}
|
||||
const timer = window.setTimeout(() => void loadFleet(), 220);
|
||||
return () => window.clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -105,6 +105,7 @@ export function TerminalPage({
|
||||
const modifiersRef = useRef<TerminalModifiers>(NO_TERMINAL_MODIFIERS);
|
||||
const terminalTouchRef = useRef<TerminalTouch | null>(null);
|
||||
const terminalInputFocusedBeforeAccessoryRef = useRef(false);
|
||||
const refreshSessionsRef = useRef<() => void>(() => {});
|
||||
const [sessions, setSessions] = useState<TerminalSession[]>([]);
|
||||
const [session, setSession] = useState<TerminalSession | null>(null);
|
||||
const [connection, setConnection] = useState<TerminalConnectionState>("idle");
|
||||
@@ -348,6 +349,7 @@ export function TerminalPage({
|
||||
};
|
||||
if (control.type === "ready") {
|
||||
setConnection("ready");
|
||||
refreshSessionsRef.current();
|
||||
window.requestAnimationFrame(() => {
|
||||
fitRef.current?.fit();
|
||||
lastTerminalSizeRef.current = { rows: 0, cols: 0 };
|
||||
@@ -387,6 +389,8 @@ export function TerminalPage({
|
||||
if (current === "exited") return current;
|
||||
return "disconnected";
|
||||
});
|
||||
// The agent may have reaped or reported sessions while we were away.
|
||||
refreshSessionsRef.current();
|
||||
};
|
||||
},
|
||||
[detachTransport, operatorId, sendResize],
|
||||
@@ -572,7 +576,7 @@ export function TerminalPage({
|
||||
});
|
||||
resizeObserver.observe(hostRef.current);
|
||||
void restoreTerminalSession();
|
||||
const sessionRefresh = window.setInterval(() => {
|
||||
const refreshSessions = () => {
|
||||
void listTerminals()
|
||||
.then((listed) => {
|
||||
if (!cancelled) {
|
||||
@@ -585,15 +589,20 @@ export function TerminalPage({
|
||||
// The attached terminal transport remains authoritative while a
|
||||
// background list refresh is temporarily unavailable.
|
||||
});
|
||||
}, 5000);
|
||||
};
|
||||
refreshSessionsRef.current = refreshSessions;
|
||||
// Session lifecycle only changes at connect/disconnect moments or when the
|
||||
// operator returns to the tab; refresh on those instead of polling.
|
||||
window.addEventListener("focus", refreshSessions);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
input.dispose();
|
||||
titleChange.dispose();
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener("focus", refreshSessions);
|
||||
refreshSessionsRef.current = () => {};
|
||||
window.cancelAnimationFrame(resizeFrame);
|
||||
window.clearInterval(sessionRefresh);
|
||||
detachTransport();
|
||||
terminal.dispose();
|
||||
terminalRef.current = null;
|
||||
|
||||
@@ -150,10 +150,14 @@ export function TerminalSessionTabs({
|
||||
<button
|
||||
type="button"
|
||||
className="terminal-tab-close"
|
||||
disabled={locked}
|
||||
disabled={locked || connection === "connecting"}
|
||||
aria-label={`Close ${agentLabel} terminal session`}
|
||||
title={
|
||||
locked ? "Attached in another browser" : "Close session"
|
||||
locked
|
||||
? "Attached in another browser"
|
||||
: connection === "connecting"
|
||||
? "Connecting — wait before closing"
|
||||
: "Close session"
|
||||
}
|
||||
onClick={() => onClose(item)}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user