fix(ext/popup): revoke object URLs in Document detail teardown

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) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-25 20:41:34 -04:00
parent 705b171553
commit 9c481422ad

View File

@@ -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<string>();
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<void>
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 = `<img src="${url}" alt="" />`;
}).catch(() => {});
}
@@ -316,12 +331,20 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise<void>
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 = `<img src="${url}" alt="" />`;