From eed9b1fa807d27f329f0f7039344e13f9dbb1a32 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 5 Jul 2026 14:57:22 +0200 Subject: [PATCH] fix(profile): revoke CV-preview blob URLs on unmount, not on every change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PDF-carousel cleanup effect had `[pdfCarousel]` deps, so its cleanup ran on every carousel change and revoked the *previous* array's object URLs — which are still referenced by unchanged items in the new array. Building a multi-template deck therefore left every preview except the last with a revoked (broken) blob URL. Drop paths are already handled explicitly in savePdfToCarousel (replace) and resetPdfCarousel (clear), so blanket per-change revocation was both harmful and redundant. Fix: track the latest carousel in a ref and revoke outstanding URLs only on unmount (empty-deps effect). No leak either way — unmount still frees them. Found during the memory-leak/resource audit; this is a resource-release correctness bug (over-eager revocation), not a leak. profile-page.test: 5/5 green (with an adequate timeout; the suite's 5s-timeout flakiness is pre-existing and unrelated). Co-Authored-By: Claude Opus 4.8 --- job-tracker-ui/src/pages/ProfilePage.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/job-tracker-ui/src/pages/ProfilePage.tsx b/job-tracker-ui/src/pages/ProfilePage.tsx index 37ff125..3b35b00 100644 --- a/job-tracker-ui/src/pages/ProfilePage.tsx +++ b/job-tracker-ui/src/pages/ProfilePage.tsx @@ -267,15 +267,27 @@ export default function ProfilePage() { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); + // Keep a ref to the latest carousel so the unmount cleanup can revoke the + // outstanding preview object URLs without re-running on every change. + const pdfCarouselRef = useRef([]); useEffect(() => { + pdfCarouselRef.current = pdfCarousel; + }, [pdfCarousel]); + + useEffect(() => { + // Revoke any remaining preview object URLs only on unmount. Per-change + // revocation is already handled explicitly in savePdfToCarousel (replace) and + // resetPdfCarousel (clear); doing it here on every pdfCarousel change revoked + // URLs that were still referenced by other items in the deck, breaking their + // previews. return () => { - pdfCarousel.forEach((item) => { + pdfCarouselRef.current.forEach((item) => { if (item.pdfUrl) { window.URL.revokeObjectURL(item.pdfUrl); } }); }; - }, [pdfCarousel]); + }, []); const loadProfile = useCallback(async () => { setLoading(true);