diff --git a/src/components/os/apps/BlogApp.tsx b/src/components/os/apps/BlogApp.tsx index a0c0450..3f146ea 100644 --- a/src/components/os/apps/BlogApp.tsx +++ b/src/components/os/apps/BlogApp.tsx @@ -15,6 +15,11 @@ type BlogPayload = { fetchedAt: string; }; +// Module-level cache to prevent duplicate fetches when multiple components +// mount simultaneously and use this hook, or when the window is opened/closed frequently. +let fetchPromise: Promise | null = null; +let cachedPayload: BlogPayload | null = null; + const dateFormatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', @@ -22,20 +27,34 @@ const dateFormatter = new Intl.DateTimeFormat('en-US', { }); export default function BlogApp() { - const [payload, setPayload] = useState(null); + const [payload, setPayload] = useState(cachedPayload); const [error, setError] = useState(null); const [query, setQuery] = useState(''); useEffect(() => { let cancelled = false; - fetch('/api/blog.json') - .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))) + + if (cachedPayload) { + setPayload(cachedPayload); + return; + } + + if (!fetchPromise) { + // Security: use an AbortSignal timeout to prevent application hangs + fetchPromise = fetch('/api/blog.json', { signal: AbortSignal.timeout(10000) }) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))); + } + + fetchPromise .then((data: BlogPayload) => { + cachedPayload = data; if (!cancelled) setPayload(data); }) .catch((err) => { + fetchPromise = null; // Allow retry on error if (!cancelled) setError(err instanceof Error ? err.message : 'failed to load'); }); + return () => { cancelled = true; };