From 9c481422ada1475822a40492de65761022f63a91 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 25 Apr 2026 20:41:34 -0400 Subject: [PATCH] fix(ext/popup): revoke object URLs in Document detail teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two leaks from 705b171: 1. Lazy-load thumb for image-mime primary attachments created URL.createObjectURL but never revoked. Now tracked in a module-level registry, revoked on teardown. 2. 🔍 preview toggle's object URL same issue. Now tracked, revoked on teardown + on toggle-off (when user clicks the preview button to collapse). Download button's URL (already self-cleaning via setTimeout) left untracked — no change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/popup/components/types/document.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/extension/src/popup/components/types/document.ts b/extension/src/popup/components/types/document.ts index 57822dd..df25aee 100644 --- a/extension/src/popup/components/types/document.ts +++ b/extension/src/popup/components/types/document.ts @@ -17,8 +17,23 @@ let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null; let sectionsExpanded = false; +const detailObjectUrls = new Set(); + +function trackDetailUrl(url: string): string { + detailObjectUrls.add(url); + return url; +} + +function revokeAllDetailUrls(): void { + for (const url of detailObjectUrls) { + URL.revokeObjectURL(url); + } + detailObjectUrls.clear(); +} + export function teardown(): void { teardownAttachmentsDisclosure(); + revokeAllDetailUrls(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); activeKeyHandler = null; @@ -296,7 +311,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise if (!resp || !resp.ok) return; const data = resp.data as { bytes: ArrayBuffer }; const blob = new Blob([data.bytes], { type: primaryRef.mime_type }); - const url = URL.createObjectURL(blob); + const url = trackDetailUrl(URL.createObjectURL(blob)); if (thumb) thumb.innerHTML = ``; }).catch(() => {}); } @@ -316,12 +331,20 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise document.getElementById('doc-preview')?.addEventListener('click', async () => { const sigblock = document.getElementById('doc-sigblock')!; const existingPreview = sigblock.querySelector('.document-signature-block__preview'); - if (existingPreview) { existingPreview.remove(); return; } + if (existingPreview) { + const img = existingPreview.querySelector('img'); + if (img?.src) { + URL.revokeObjectURL(img.src); + detailObjectUrls.delete(img.src); + } + existingPreview.remove(); + return; + } const resp = await sendMessage({ type: 'download_attachment', itemId: item.id, attachmentId: primaryRef.id }); if (!resp || !resp.ok) return; const data = resp.data as { bytes: ArrayBuffer }; const blob = new Blob([data.bytes], { type: primaryRef.mime_type }); - const url = URL.createObjectURL(blob); + const url = trackDetailUrl(URL.createObjectURL(blob)); const preview = document.createElement('div'); preview.className = 'document-signature-block__preview'; preview.innerHTML = ``;