From 9ed95506ca014218da7b7a3422daa09370f2be7c Mon Sep 17 00:00:00 2001 From: tchoumi313 Date: Mon, 27 Oct 2025 15:43:34 +0100 Subject: [PATCH 1/3] Lazy Loading with Intersection Observer and Cache Integration --- .../ModelTable/EvidenceFilePreview.svelte | 113 ++++++++++++++---- frontend/src/lib/stores/attachmentCache.ts | 79 ++++++++++++ 2 files changed, 168 insertions(+), 24 deletions(-) create mode 100644 frontend/src/lib/stores/attachmentCache.ts diff --git a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte index b761444871..7ae384fd24 100644 --- a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte +++ b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte @@ -1,8 +1,9 @@ -{#snippet displayPreview()} +{#snippet displayPreview(att: Attachment)}
- {#if attachment.type.startsWith('image')} + {#if att.type.startsWith('image')} attachment - {:else if attachment.type === 'application/pdf'} + {:else if att.type === 'application/pdf'} {#if !display}
{/if} @@ -89,18 +152,20 @@
{/snippet} -{#if cell} - {#if attachment} - {#if attachment.type.startsWith('image') || attachment.type === 'application/pdf'} - {@render displayPreview(attachment)} - {:else if !attachment.fileExists} -

{m.couldNotFindAttachmentMessage()}

+
+ {#if cell} + {#if attachment} + {#if attachment.type.startsWith('image') || attachment.type === 'application/pdf'} + {@render displayPreview(attachment)} + {:else if !attachment.fileExists} +

{m.couldNotFindAttachmentMessage()}

+ {:else} +

{m.NoPreviewMessage()}

+ {/if} {:else} -

{m.NoPreviewMessage()}

+ + {m.loading()}... + {/if} - {:else} - - {m.loading()}... - {/if} -{/if} +
diff --git a/frontend/src/lib/stores/attachmentCache.ts b/frontend/src/lib/stores/attachmentCache.ts new file mode 100644 index 0000000000..9ef7702779 --- /dev/null +++ b/frontend/src/lib/stores/attachmentCache.ts @@ -0,0 +1,79 @@ +import { writable, get } from 'svelte/store'; + +/** + * Global cache for attachment blob URLs + * Prevents duplicate downloads of the same evidence attachment + */ + +interface CachedAttachment { + type: string; + url: string; + fileExists: boolean; +} + +interface AttachmentCache { + [key: string]: CachedAttachment; +} + +function createAttachmentCache() { + const { subscribe, set, update } = writable({}); + + return { + subscribe, + /** + * Get a cached attachment by key + */ + get: (key: string): CachedAttachment | undefined => { + const cache = get({ subscribe }); + return cache[key]; + }, + /** + * Store an attachment in the cache + */ + set: (key: string, value: CachedAttachment) => { + update((cache) => { + cache[key] = value; + return cache; + }); + }, + /** + * Remove an attachment from the cache and revoke its blob URL + */ + remove: (key: string) => { + update((cache) => { + if (cache[key]) { + URL.revokeObjectURL(cache[key].url); + delete cache[key]; + } + return cache; + }); + }, + /** + * Clear all cached attachments and revoke all blob URLs + */ + clear: () => { + update((cache) => { + Object.values(cache).forEach((attachment) => { + URL.revokeObjectURL(attachment.url); + }); + return {}; + }); + }, + /** + * Check if an attachment is in the cache + */ + has: (key: string): boolean => { + const cache = get({ subscribe }); + return key in cache; + } + }; +} + +export const attachmentCache = createAttachmentCache(); + +/** + * Generate a cache key for an attachment + */ +export function generateAttachmentCacheKey(id: string, attachmentName: string): string { + return `${id}-${attachmentName}`; +} From 65784f30ca955ed60ab3eb996fcbf685834b7f53 Mon Sep 17 00:00:00 2001 From: tchoumi313 Date: Tue, 28 Oct 2025 08:29:49 +0100 Subject: [PATCH 2/3] update --- .../ModelTable/EvidenceFilePreview.svelte | 36 ++++++++++++------- frontend/src/lib/stores/attachmentCache.ts | 7 ++-- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte index 7ae384fd24..bf989506e9 100644 --- a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte +++ b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte @@ -37,19 +37,29 @@ } // Fetch from server if not in cache - const res = await fetch( - `/${meta.evidence ? 'evidence-revisions' : 'evidences'}/${meta.id}/attachment` - ); - const blob = await res.blob(); - const result = { - type: blob.type, - url: URL.createObjectURL(blob), - fileExists: res.ok - }; - - // Store in cache - attachmentCache.set(cacheKey, result); - return result; + try { + const res = await fetch( + `/${meta.evidence ? 'evidence-revisions' : 'evidences'}/${meta.id}/attachment` + ); + if (!res.ok) { + const miss = { type: '', url: '', fileExists: false } satisfies Attachment; + attachmentCache.set(cacheKey, miss); + return miss; + } + const blob = await res.blob(); + const result = { + type: blob.type, + url: URL.createObjectURL(blob), + fileExists: true + } satisfies Attachment; + attachmentCache.set(cacheKey, result); + return result; + } catch (err) { + if ((err as any)?.name === 'AbortError') return attachment!; + const miss = { type: '', url: '', fileExists: false } satisfies Attachment; + attachmentCache.set(cacheKey, miss); + return miss; + } }; let mounted = $state(false); diff --git a/frontend/src/lib/stores/attachmentCache.ts b/frontend/src/lib/stores/attachmentCache.ts index 9ef7702779..5c2be5b5d2 100644 --- a/frontend/src/lib/stores/attachmentCache.ts +++ b/frontend/src/lib/stores/attachmentCache.ts @@ -32,8 +32,11 @@ function createAttachmentCache() { */ set: (key: string, value: CachedAttachment) => { update((cache) => { - cache[key] = value; - return cache; + const prev = cache[key]; + if (prev?.url && prev.url !== value.url) { + URL.revokeObjectURL(prev.url); + } + return { ...cache, [key]: value }; }); }, /** From affe6863c4049c1cf0e559bd0be5eccc0ab2a97d Mon Sep 17 00:00:00 2001 From: tchoumi313 Date: Tue, 28 Oct 2025 10:23:16 +0100 Subject: [PATCH 3/3] update --- .../lib/components/ModelTable/EvidenceFilePreview.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte index bf989506e9..7bb83f382b 100644 --- a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte +++ b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte @@ -55,7 +55,10 @@ attachmentCache.set(cacheKey, result); return result; } catch (err) { - if ((err as any)?.name === 'AbortError') return attachment!; + if ((err as any)?.name === 'AbortError') { + const miss = { type: '', url: '', fileExists: false } satisfies Attachment; + return miss; + } const miss = { type: '', url: '', fileExists: false } satisfies Attachment; attachmentCache.set(cacheKey, miss); return miss; @@ -95,6 +98,10 @@ if (observer) { observer.disconnect(); } + // Note: We do NOT revoke blob URLs here because the attachmentCache + // is shared across multiple components. The cache itself handles + // URL revocation when URLs are replaced or when cache.clear() is called. + // Revoking URLs here would break other components using the same attachment. }); run(() => {