perf: optimize polling with caching and event-driven approach (P1-2)

- CLI binary detection cached with 60s TTL
- Settings poll replaced with visibility/focus events
- Health check uses adaptive backoff (30s → 60s when stable)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-13 01:24:43 +08:00
1 parent a5805cb380
commit e86f0cfd8f
3 files changed
+76 -11

No files matched your search

+12 -3
View File
@@ -150,11 +150,20 @@ export function AppShell({ children }: { children: React.ReactNode }) {
}
}, []);
// Poll periodically so the indicator stays in sync when settings change
// Re-fetch when window gains focus / becomes visible instead of polling every 5s
useEffect(() => {
fetchSkipPermissions();
const id = setInterval(fetchSkipPermissions, 5000);
return () => clearInterval(id);
const handleVisibility = () => {
if (document.visibilityState === "visible") {
fetchSkipPermissions();
}
};
document.addEventListener("visibilitychange", handleVisibility);
window.addEventListener("focus", fetchSkipPermissions);
return () => {
document.removeEventListener("visibilitychange", handleVisibility);
window.removeEventListener("focus", fetchSkipPermissions);
};
}, [fetchSkipPermissions]);
// --- Update check state ---
+46 -7
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useEffect, useState, useCallback, useRef } from "react";
import {
Dialog,
DialogContent,
@@ -17,26 +17,67 @@ interface ClaudeStatus {
version: string | null;
}
const BASE_INTERVAL = 30_000; // 30s
const BACKED_OFF_INTERVAL = 60_000; // 60s after 3 consecutive stable results
const STABLE_THRESHOLD = 3;
export function ConnectionStatus() {
const [status, setStatus] = useState<ClaudeStatus | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const stableCountRef = useRef(0);
const lastConnectedRef = useRef<boolean | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Use a ref-based approach to avoid circular deps between check and schedule
const checkRef = useRef<() => void>(() => {});
const schedule = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
const interval = stableCountRef.current >= STABLE_THRESHOLD
? BACKED_OFF_INTERVAL
: BASE_INTERVAL;
timerRef.current = setTimeout(() => checkRef.current(), interval);
}, []);
const checkStatus = useCallback(async () => {
try {
const res = await fetch("/api/claude-status");
if (res.ok) {
const data: ClaudeStatus = await res.json();
if (lastConnectedRef.current === data.connected) {
stableCountRef.current++;
} else {
stableCountRef.current = 0;
}
lastConnectedRef.current = data.connected;
setStatus(data);
}
} catch {
if (lastConnectedRef.current === false) {
stableCountRef.current++;
} else {
stableCountRef.current = 0;
}
lastConnectedRef.current = false;
setStatus({ connected: false, version: null });
}
}, []);
schedule();
}, [schedule]);
useEffect(() => {
checkRef.current = checkStatus;
}, [checkStatus]);
useEffect(() => {
checkStatus(); // eslint-disable-line react-hooks/set-state-in-effect -- setState is called asynchronously after fetch
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [checkStatus]);
const handleManualRefresh = useCallback(() => {
stableCountRef.current = 0;
checkStatus();
const interval = setInterval(checkStatus, 30000);
return () => clearInterval(interval);
}, [checkStatus]);
const connected = status?.connected ?? false;
@@ -127,9 +168,7 @@ export function ConnectionStatus() {
<DialogFooter>
<Button
variant="outline"
onClick={() => {
checkStatus();
}}
onClick={handleManualRefresh}
>
Refresh
</Button>
+18 -1
View File
@@ -95,11 +95,28 @@ export function getExpandedPath(): string {
return parts.join(path.delimiter);
}
// TTL cache for findClaudeBinary to avoid repeated filesystem probes
let _cachedBinaryPath: string | undefined | null = null; // null = not cached
let _cachedBinaryTimestamp = 0;
const BINARY_CACHE_TTL = 60_000; // 60 seconds
/**
* Find and validate the Claude CLI binary.
* Tests each candidate with --version before returning.
* Results are cached for 60s to avoid redundant filesystem probes on every poll.
*/
export function findClaudeBinary(): string | undefined {
const now = Date.now();
if (_cachedBinaryPath !== null && now - _cachedBinaryTimestamp < BINARY_CACHE_TTL) {
return _cachedBinaryPath;
}
const found = _findClaudeBinaryUncached();
_cachedBinaryPath = found;
_cachedBinaryTimestamp = now;
return found;
}
function _findClaudeBinaryUncached(): string | undefined {
// Try known candidate paths first
for (const p of getClaudeCandidatePaths()) {
try {