fix(profile): revoke CV-preview blob URLs on unmount, not on every change

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 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-05 14:57:22 +02:00
parent e5e2c65709
commit eed9b1fa80
+14 -2
View File
@@ -267,15 +267,27 @@ export default function ProfilePage() {
const [currentPassword, setCurrentPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = 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<PdfCarouselItem[]>([]);
useEffect(() => { 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 () => { return () => {
pdfCarousel.forEach((item) => { pdfCarouselRef.current.forEach((item) => {
if (item.pdfUrl) { if (item.pdfUrl) {
window.URL.revokeObjectURL(item.pdfUrl); window.URL.revokeObjectURL(item.pdfUrl);
} }
}); });
}; };
}, [pdfCarousel]); }, []);
const loadProfile = useCallback(async () => { const loadProfile = useCallback(async () => {
setLoading(true); setLoading(true);