Skip to content

Conversation

@tchoumi313
Copy link
Contributor

@tchoumi313 tchoumi313 commented Oct 27, 2025

Summary by CodeRabbit

  • New Features

    • Attachment previews now lazy-load when scrolled into view and render inside a dedicated container.
    • Shows loading indicator, "no preview", and "could not find attachment" states for clearer UI.
  • Performance

    • Global in-memory cache for attachment previews to reduce redundant requests and speed display.
    • Prefers cached data on metadata changes and only refetches immediately when visible.
  • Bug Fixes / Stability

    • Improved lifecycle and cleanup to avoid resource leaks and handle aborted or failed fetches gracefully.

@tchoumi313 tchoumi313 self-assigned this Oct 27, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds an in-memory attachment cache and lazy-loading for EvidenceFilePreview using IntersectionObserver; previews fetch and cache blob URLs, store misses, show loading/no-preview states, and revoke blob URLs on replace/remove; displayPreview now accepts an att parameter.

Changes

Cohort / File(s) Summary
Evidence preview component
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte
Adds IntersectionObserver-based lazy loading (containerElement, isVisible, observer); integrates attachmentCache lookups and conditional fetches (caches hits and misses); updates signature to displayPreview(att: Attachment) and replaces direct attachment references with att; adds loading / no-preview UI branches; tracks mounted state and cleans up observer on destroy; binds container element and uses generateAttachmentCacheKey.
Attachment cache store
frontend/src/lib/stores/attachmentCache.ts
New Svelte writable store implementing a global in-memory cache with get, set, remove, clear, has, and subscribe; defines CachedAttachment and AttachmentCache types; exports attachmentCache singleton and generateAttachmentCacheKey; revokes blob URLs on update/remove/clear to prevent leaks.

Sequence Diagram(s)

sequenceDiagram
    participant Comp as EvidenceFilePreview
    participant Obs as IntersectionObserver
    participant Cache as attachmentCache
    participant API as Backend

    Comp->>Obs: observe(containerElement)
    Obs-->>Comp: intersection -> isVisible = true

    alt cache hit
        Comp->>Cache: get(cacheKey)
        Cache-->>Comp: CachedAttachment (url, fileExists)
        Comp->>Comp: displayPreview(att from cache)
    else cache miss
        Comp->>API: fetchAttachment(id, name)
        API-->>Comp: blob / 404 / error
        Comp->>Cache: set(cacheKey, CachedAttachment|miss)
        Comp->>Comp: displayPreview(att or NoPreview/Loading)
    end

    rect rgb(240,240,220)
    Note over Comp,Obs: component destroyed
    Comp->>Obs: disconnect()
    Note right of Cache: cache may revoke blob URLs on replace/remove/clear
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review IntersectionObserver setup, rootMargin, and onDestroy disconnect.
  • Verify cache key generation and consistent use of generateAttachmentCacheKey.
  • Inspect blob URL revoke logic in set / remove / clear to avoid double-revokes or leaks.
  • Check fetch abort/error handling and caching of misses to prevent stale or inconsistent UI states.

Possibly related PRs

Poem

🐰 I peek when the viewport wakes,
I stash the blob for speedy takes,
Lazy hops until sight's near,
Revoke the old, keep previews clear,
A rabbit cheers — cache and cheer!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: lazy loading for evidences preview with caching (faster)" directly corresponds to the primary changes in the pull request. The changeset implements lazy loading via IntersectionObserver and introduces a caching mechanism in a new attachmentCache module, both of which are explicitly mentioned in the title. The title clearly conveys the main objective—improving performance of evidence thumbnail display—through concise language that a teammate can quickly understand when scanning commit history. The inclusion of "(faster)" effectively communicates the performance benefit without being misleading.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch CA-1337-display-of-evidence-thumbnails-is-too-slow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (1)

120-134: Keyboard accessibility: don’t toggle on Tab; support Enter/Space; add aria-label.

Current handler toggles on Tab, which interferes with focus navigation.

-  onkeydown={(event) => {
-    if ((display && event.key === 'Escape') || event.key === 'Tab') {
-      display = !display;
-    }
-  }}
+  onkeydown={(event) => {
+    if (!display && (event.key === 'Enter' || event.key === ' ')) {
+      display = true;
+    } else if (display && event.key === 'Escape') {
+      display = false;
+    }
+  }}
+  aria-label="Preview attachment"
🧹 Nitpick comments (12)
frontend/src/lib/stores/attachmentCache.ts (4)

65-68: Use hasOwnProperty for membership checks.

Avoid prototype-chain hits with the in operator.

-    has: (key: string): boolean => {
-      const cache = get({ subscribe });
-      return key in cache;
-    }
+    has: (key: string): boolean => {
+      const cache = get({ subscribe });
+      return Object.prototype.hasOwnProperty.call(cache, key);
+    }

19-19: Remove unused destructured set.

It isn’t used and shadows your custom set API name.

-  const { subscribe, set, update } = writable<AttachmentCache>({});
+  const { subscribe, update } = writable<AttachmentCache>({});

8-12: Export the CachedAttachment type and reuse it in consumers.

Prevents type drift with the component’s duplicate Attachment interface.

-interface CachedAttachment {
+export interface CachedAttachment {
   type: string;
   url: string;
   fileExists: boolean;
 }

77-79: Strengthen cache key to avoid collisions (evidence vs. revisions).

Current key omits the scope, risking collisions for identical id/name across endpoints.

-export function generateAttachmentCacheKey(id: string, attachmentName: string): string {
-  return `${id}-${attachmentName}`;
-}
+export function generateAttachmentCacheKey(
+  id: string,
+  attachmentName: string,
+  variant?: 'evidence' | 'revision'
+): string {
+  const scope = variant ?? 'evidence';
+  return `${scope}:${id}:${attachmentName}`;
+}

Please update call sites accordingly (see suggested changes in EvidenceFilePreview.svelte).

frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (8)

27-31: Support cancellation to prevent stale updates and wasted work.

Use AbortController to cancel in-flight fetches on prop changes or unmount.

 const fetchAttachment = async () => {
+  // Cancel any previous fetch and create a new controller
+  abortCtl?.abort();
+  abortCtl = new AbortController();
   // Generate cache key
   cacheKey = generateAttachmentCacheKey(meta.id, meta.attachment);

21-26: Wire up AbortController lifecycle.

Declare controller and abort on destroy.

- let attachment: Attachment | undefined = $state();
+ let attachment: Attachment | undefined = $state();
+ let abortCtl: AbortController | undefined = $state();

 onDestroy(() => {
   // Clean up observer
   if (observer) {
     observer.disconnect();
   }
+  abortCtl?.abort();
 });

Also applies to: 83-88


114-118: Fix Tailwind z-index utility.

Use an arbitrary value with brackets.

-const wrapperClasses =
-  'fixed w-full h-full inset-0 flex justify-center items-center backdrop-blur-sm backdrop-brightness-40 z-999';
+const wrapperClasses =
+  'fixed w-full h-full inset-0 flex justify-center items-center backdrop-blur-sm backdrop-brightness-40 z-[999]';

136-141: Avoid conflicting height classes when expanded.

When display is true, h-24 and h-[90%] both render; prefer a single branch-specific class.

-  <img
-    src={att.url}
-    alt="attachment"
-    class="h-24 object-contain {display ? imageElementClasses : ''}"
-  />
+  <img
+    src={att.url}
+    alt="attachment"
+    class={display ? imageElementClasses : 'h-24 object-contain'}
+  />

-  <embed
-    src={att.url}
-    type="application/pdf"
-    class="h-24 object-contain {display ? embedElementClasses : ''}"
-  />
+  <embed
+    src={att.url}
+    type="application/pdf"
+    class={display ? embedElementClasses : 'h-24 object-contain'}
+  />

Also applies to: 147-150


4-7: Reuse the exported type instead of duplicating the interface.

Import the type from the cache module to keep things in sync.

 import { m } from '$paraglide/messages';
 import { attachmentCache, generateAttachmentCacheKey } from '$lib/stores/attachmentCache';
+import type { CachedAttachment as Attachment } from '$lib/stores/attachmentCache';
@@
-interface Attachment {
-  type: string;
-  url: string;
-  fileExists: boolean;
-}

Also applies to: 15-19


29-29: Include variant in cache key to avoid cross-endpoint collisions.

Pass a scope to the key generator (requires the companion change in attachmentCache.ts).

-  cacheKey = generateAttachmentCacheKey(meta.id, meta.attachment);
+  cacheKey = generateAttachmentCacheKey(
+    meta.id,
+    meta.attachment,
+    meta.evidence ? 'revision' : 'evidence'
+  );
@@
-  const key = generateAttachmentCacheKey(meta.id, meta.attachment);
+  const key = generateAttachmentCacheKey(
+    meta.id,
+    meta.attachment,
+    meta.evidence ? 'revision' : 'evidence'
+  );

Also applies to: 93-99


59-81: Optional: preload earlier for smoother UX.

Consider a larger rootMargin (e.g., 200px) to start fetch slightly earlier on fast scroll.


27-53: Optional: dedupe in-flight requests across components.

Two components can request the same key before the first write completes. A simple in-flight Promise map in the cache layer would coalesce concurrent fetches.

I can sketch an in-flight map pattern if you’d like.

Also applies to: 59-81, 102-107

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e9752c and 9ed9550.

📒 Files selected for processing (2)
  • frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (4 hunks)
  • frontend/src/lib/stores/attachmentCache.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: build (22)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: lint-community (22)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: startup-functional-test (3.12)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (3)

25-25: Remove redundant state variable.

The cacheKey state variable is unnecessary since it's recalculated on lines 29 and 103. Cache keys should be computed inline where needed rather than stored in reactive state.

Apply this diff:

-let cacheKey = $state('');

Then inline the computation where used.


66-91: Consider checking initial visibility.

The observer setup doesn't check if containerElement is already in the viewport on mount. For elements that start visible, the intersection callback may not fire immediately, delaying the fetch. Consider checking entry.intersectionRatio or using observer.takeRecords() after observation starts.

Alternative approach:

 		observer = new IntersectionObserver(
 			(entries) => {
 				entries.forEach((entry) => {
 					if (entry.isIntersecting && !isVisible) {
 						isVisible = true;
-						// Fetch attachment when element becomes visible
 						fetchAttachment().then((_attachment) => {
 							attachment = _attachment;
 						});
-						// Disconnect after first load
 						observer?.disconnect();
 					}
 				});
 			},
 			{
-				rootMargin: '50px' // Start loading slightly before element is visible
+				rootMargin: '50px',
+				threshold: 0
 			}
 		);
 		observer.observe(containerElement);
+		// Check if already visible
+		if (containerElement.getBoundingClientRect().top < window.innerHeight) {
+			isVisible = true;
+			fetchAttachment().then((_attachment) => {
+				attachment = _attachment;
+			});
+			observer.disconnect();
+		}
 	}

100-121: Simplify coordination between run block and observer.

The run block (lines 112-117) and onMount observer (lines 77-79) both call fetchAttachment(), creating duplicate fetch paths. Consider:

  • Let the observer handle all initial loads
  • Use the run block only for cache lookups when meta changes
  • Or remove the isVisible check and always fetch in the run block (simpler but less lazy)

Current logic may skip fetching if meta changes before the element is observed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ed9550 and 65784f3.

📒 Files selected for processing (2)
  • frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (4 hunks)
  • frontend/src/lib/stores/attachmentCache.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/lib/stores/attachmentCache.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: build_community_frontend
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: Analyze (python)
🔇 Additional comments (4)
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (4)

4-6: LGTM!

The imports correctly add lifecycle hooks and cache utilities needed for the lazy loading implementation.


93-98: LGTM!

Observer cleanup is properly implemented. Note: blob URL cleanup still needed (flagged separately).


130-163: LGTM!

The refactored snippet signature with the att parameter improves reusability and clarity. The implementation correctly handles image and PDF previews.


165-181: LGTM!

The template correctly implements lazy loading container binding and provides clear loading, error, and no-preview states for better UX.

Comment on lines +49 to +56
const blob = await res.blob();
const result = {
type: blob.type,
url: URL.createObjectURL(blob),
fileExists: true
} satisfies Attachment;
attachmentCache.set(cacheKey, result);
return result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Revoke blob URLs to prevent memory leaks.

Blob URLs created on line 52 are never revoked, causing memory leaks as attachments accumulate. Implement cleanup:

  1. Revoke URLs in onDestroy
  2. Revoke old URL before caching a replacement
  3. Consider tracking URLs for batch cleanup

Example cleanup in onDestroy:

onDestroy(() => {
	if (observer) {
		observer.disconnect();
	}
	// Revoke blob URL to free memory
	if (attachment?.url) {
		URL.revokeObjectURL(attachment.url);
	}
});
🤖 Prompt for AI Agents
In frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte around
lines 49 to 56, the created blob URL (URL.createObjectURL) is never revoked
causing memory leaks; update the component to revoke URLs: before caching a new
result, call URL.revokeObjectURL on any previous cached attachment.url for the
same cacheKey, store/tracked created URLs so they can be revoked in batch, and
add an onDestroy handler that disconnects any observer and revokes the current
attachment.url (and any tracked URLs) to free memory.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (3)

25-25: Consider removing the cacheKey state variable.

The cacheKey state variable is set in fetchAttachment (line 29) but is never read elsewhere. The run block generates its own key inline (line 110). This state variable appears unused and can be removed.

Apply this diff:

 	let containerElement: HTMLElement | undefined = $state();
 	let isVisible = $state(false);
 	let observer: IntersectionObserver | undefined;
-	let cacheKey = $state('');

And remove the assignment on line 29:

 	const fetchAttachment = async () => {
-		// Generate cache key
-		cacheKey = generateAttachmentCacheKey(meta.id, meta.attachment);
+		const cacheKey = generateAttachmentCacheKey(meta.id, meta.attachment);
 
 		// Check cache first

77-84: Simplify observer logic.

The !isVisible check on line 77 is redundant since the observer disconnects immediately after first load (line 84). Additionally, the promise chain could be more concise.

Apply this diff to simplify:

 				entries.forEach((entry) => {
-					if (entry.isIntersecting && !isVisible) {
+					if (entry.isIntersecting) {
 						isVisible = true;
-						// Fetch attachment when element becomes visible
-						fetchAttachment().then((_attachment) => {
-							attachment = _attachment;
-						});
-						// Disconnect after first load
+						fetchAttachment().then((att) => (attachment = att));
 						observer?.disconnect();
 					}
 				});

107-128: Consider consolidating cache-check logic.

The run block duplicates cache-checking logic that's already in fetchAttachment. While the current implementation works correctly, you could simplify by always calling fetchAttachment (which handles caching internally):

 	run(() => {
 		if (mounted && meta.attachment) {
-			// Check cache first when meta changes
-			const key = generateAttachmentCacheKey(meta.id, meta.attachment);
-			if (attachmentCache.has(key)) {
-				const cached = attachmentCache.get(key);
-				if (cached) {
-					attachment = cached;
-					return;
-				}
-			}
-
-			// If visible, fetch immediately
 			if (isVisible) {
-				fetchAttachment().then((_attachment) => {
-					attachment = _attachment;
-				});
+				fetchAttachment().then((att) => (attachment = att));
+			} else {
+				// Check if already cached
+				const key = generateAttachmentCacheKey(meta.id, meta.attachment);
+				attachment = attachmentCache.get(key);
 			}
 		} else {
 			attachment = undefined;
 		}
 	});

This reduces duplication while preserving the optimization of not fetching when not visible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65784f3 and affe686.

📒 Files selected for processing (1)
  • frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: build_community_frontend
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: Analyze (python)
🔇 Additional comments (3)
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (3)

96-105: LGTM! Proper cleanup with clear documentation.

The cleanup logic correctly disconnects the observer, and the comment clearly explains why blob URLs are not revoked here (shared cache responsibility). This properly addresses the previous memory leak concerns.


137-170: LGTM! Good refactoring of the snippet.

Parameterizing the displayPreview snippet with att: Attachment makes the dependency explicit and the snippet more reusable. All references are correctly updated.


172-188: LGTM! Clear loading states and error handling.

The template properly handles all states: loading, preview available, no preview, and missing attachment. The container binding enables the lazy-loading functionality.

Comment on lines +58 to +60
if ((err as any)?.name === 'AbortError') {
const miss = { type: '', url: '', fileExists: false } satisfies Attachment;
return miss;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent error caching on AbortError.

The AbortError path returns a miss without caching it (lines 58-60), unlike the generic error path which caches the miss (lines 62-64). This inconsistency could trigger unnecessary refetches if the user scrolls away during a fetch and then returns.

However, I notice there's no AbortController in this code. If abort functionality isn't needed, consider removing the special AbortError handling:

 	} catch (err) {
-		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;

Alternatively, if you plan to add abort support later, make the caching consistent:

 	} catch (err) {
 		if ((err as any)?.name === 'AbortError') {
 			const miss = { type: '', url: '', fileExists: false } satisfies Attachment;
+			attachmentCache.set(cacheKey, miss);
 			return miss;
 		}
 		const miss = { type: '', url: '', fileExists: false } satisfies Attachment;
 		attachmentCache.set(cacheKey, miss);
 		return miss;
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ((err as any)?.name === 'AbortError') {
const miss = { type: '', url: '', fileExists: false } satisfies Attachment;
return miss;
} catch (err) {
if ((err as any)?.name === 'AbortError') {
const miss = { type: '', url: '', fileExists: false } satisfies Attachment;
attachmentCache.set(cacheKey, miss);
return miss;
}
const miss = { type: '', url: '', fileExists: false } satisfies Attachment;
attachmentCache.set(cacheKey, miss);
return miss;
}
🤖 Prompt for AI Agents
In frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte around
lines 58 to 60, the AbortError branch returns a miss without caching it while
the generic error path caches the miss; either remove the AbortError-specific
branch if abort behavior isn't used (so all errors follow the same cache path),
or update the AbortError path to mirror the generic error handling by creating
the same miss object and writing it to the cache before returning; if you intend
to support aborts later, also add an AbortController to the fetch call and wire
it into the caller so aborts are intentional and handled consistently.

@ab-smith
Copy link
Contributor

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 31, 2025

✅ Actions performed

Full review triggered.

@ab-smith ab-smith changed the title feat: Lazy Loading with Intersection Observer and Cache Integration feat: lazy loading for evidences preview (faster) Oct 31, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (3)

57-65: Remove dead AbortError handling or make it consistent.

The AbortError check is inconsistent (doesn't cache the miss) and is dead code since no AbortController is passed to fetch. The network request on lines 41-43 provides no signal parameter, so AbortError will never be thrown.

Apply this diff to remove the dead code path and simplify error handling:

 	} catch (err) {
-		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;
 	}

Alternatively, if you plan to add abort support later, cache the miss consistently in the AbortError path as well.


28-37: Consider optimizing cache lookup.

The code calls has() then get(), resulting in two cache lookups. You can simplify this by calling get() once and checking its result.

Apply this diff:

 		// Generate cache key
 		cacheKey = generateAttachmentCacheKey(meta.id, meta.attachment);
 
 		// Check cache first
-		if (attachmentCache.has(cacheKey)) {
-			const cached = attachmentCache.get(cacheKey);
-			if (cached) {
-				return cached;
-			}
+		const cached = attachmentCache.get(cacheKey);
+		if (cached) {
+			return cached;
 		}

107-128: Consider optimizing cache lookup pattern (same as in fetchAttachment).

The cache lookup on lines 110-117 uses the same has() then get() pattern that can be simplified to a single get() call.

Apply this diff:

 		if (mounted && meta.attachment) {
 			// Check cache first when meta changes
 			const key = generateAttachmentCacheKey(meta.id, meta.attachment);
-			if (attachmentCache.has(key)) {
-				const cached = attachmentCache.get(key);
-				if (cached) {
-					attachment = cached;
-					return;
-				}
+			const cached = attachmentCache.get(key);
+			if (cached) {
+				attachment = cached;
+				return;
 			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e9752c and affe686.

📒 Files selected for processing (2)
  • frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (4 hunks)
  • frontend/src/lib/stores/attachmentCache.ts (1 hunks)
🔇 Additional comments (5)
frontend/src/lib/stores/attachmentCache.ts (1)

75-82: LGTM!

The singleton export and cache key generator are clean and well-structured.

frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte (4)

1-26: LGTM!

The imports and state setup properly support lazy loading with IntersectionObserver and cache integration. All necessary pieces are in place.


69-94: LGTM!

The IntersectionObserver setup is well-implemented with proper lifecycle management. The 50px rootMargin provides good UX by preloading slightly before the element enters the viewport, and disconnecting after first load prevents duplicate fetches.


96-105: LGTM!

The cleanup logic is correct, and the comment clearly explains why blob URLs are not revoked here. Since the cache is shared across components, per-component revocation would break other instances using the same attachment.


137-188: LGTM!

The rendering logic is well-structured. Making displayPreview accept an explicit att parameter is cleaner than relying on closure, and the template properly handles all states (loading, cached, error, no preview).

Comment on lines +45 to +53
remove: (key: string) => {
update((cache) => {
if (cache[key]) {
URL.revokeObjectURL(cache[key].url);
delete cache[key];
}
return cache;
});
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix immutability: remove mutates cache in-place.

The remove method uses delete cache[key] and returns the mutated object, inconsistent with set which returns a new object via spread. This breaks the immutability pattern and may cause Svelte reactivity issues.

Apply this diff to use destructuring for immutable removal:

 		remove: (key: string) => {
 			update((cache) => {
 				if (cache[key]) {
 					URL.revokeObjectURL(cache[key].url);
-					delete cache[key];
 				}
-				return cache;
+				const { [key]: _, ...rest } = cache;
+				return rest;
 			});
 		},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
remove: (key: string) => {
update((cache) => {
if (cache[key]) {
URL.revokeObjectURL(cache[key].url);
delete cache[key];
}
return cache;
});
},
remove: (key: string) => {
update((cache) => {
if (cache[key]) {
URL.revokeObjectURL(cache[key].url);
}
const { [key]: _, ...rest } = cache;
return rest;
});
},
🤖 Prompt for AI Agents
In frontend/src/lib/stores/attachmentCache.ts around lines 45 to 53, the remove
function currently mutates the cache in-place using delete; change it to be
immutable by creating a new object without the removed key (using object
destructuring to omit the key or by shallow-cloning and deleting from the clone)
and return that new object from update; make sure to call URL.revokeObjectURL
for the removed entry's url before returning the new cache so revocation still
occurs but the original cache object is not mutated.

@ab-smith ab-smith self-requested a review October 31, 2025 15:58
@ab-smith ab-smith changed the title feat: lazy loading for evidences preview (faster) feat: lazy loading for evidences preview with caching (faster) Oct 31, 2025
Copy link
Contributor

@ab-smith ab-smith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we invalidate the cache and avoid memory exhaustion?
not sure about the cache keys and management neither.

@ab-smith ab-smith marked this pull request as draft October 31, 2025 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants