Skip to content

feat(web-reader): HTTP Caching & Cache Storage for content responses#75

Open
NamLe029 wants to merge 16 commits intogrimmory-tools:developfrom
NamLe029:web-reader-caching
Open

feat(web-reader): HTTP Caching & Cache Storage for content responses#75
NamLe029 wants to merge 16 commits intogrimmory-tools:developfrom
NamLe029:web-reader-caching

Conversation

@NamLe029
Copy link

@NamLe029 NamLe029 commented Mar 20, 2026

Description

This PR introduces:

  • HTTP Caching via Last-Modified at EPUB, PDF, audiobook content endpoints
  • Cache Storage for persistent content response caching on client side (only PDF, EPUB)

Improvements:
This feature helps reducing traffic to the server, especially for large book files.

How does Cache Storage work?
When enabled, all EPUB, PDF content responses will be cached. Subsequent requests will reuse that cached response as long as the response is deemed valid (via a HEAD request with the If-Modified-Since header to the server).

Additional changes:

  • LocalSettings for device-scoped settings (to control Cache Storage behaviors)
  • UI for LocalSettings

Testing

  • ./gradlew test passes
  • ng test passes
  • Cache Storage behaves as expected
  • Settings UI for Cache Storage behaves as expected

Screenshots

Local Settings UI
image

Summary by CodeRabbit

  • New Features

    • Browser Cache Storage: cache/revalidate fetched book and audiobook content, view usage, and clear cache.
    • "Local" settings tab and UI to toggle caching, refresh usage, and clear cache.
    • PDF viewer and file-fetching now use cached content when enabled.
  • Bug Fixes & Improvements

    • Server accepts HEAD and If-Modified-Since; responses expose Last-Modified.
    • Improved cache-control and dynamic content-type handling.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Backend streaming endpoints changed to return ResponseEntity with dynamic Content-Type and optional Last-Modified; CORS updated to allow HEAD and cache headers. Added FileUtils.getFileLastModified. Frontend: introduced CacheStorageService and LocalSettingsService, a Local Settings UI, and integrated cache-aware loading into BookFileService and PDF reader (blob/object-URL handling).

Changes

Cohort / File(s) Summary
Security / CORS
booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java
Added HEAD to allowed methods, added If-Modified-Since to allowed request headers, and exposed Last-Modified.
Controllers & Service: streaming → ResponseEntity
booklore-api/src/main/java/org/booklore/controller/AudiobookReaderController.java, booklore-api/src/main/java/org/booklore/controller/BookController.java, booklore-api/src/main/java/org/booklore/service/book/BookService.java
Replaced servlet-style void streaming methods with endpoints returning ResponseEntity<? extends Resource>/ResponseEntity<Resource>; removed servlet request/response params and range-based streaming delegation; introduced helper response construction with dynamic Content-Type, Cache-Control, and optional Last-Modified.
File utilities
booklore-api/src/main/java/org/booklore/util/FileUtils.java
Added public Long getFileLastModified(Path) — returns millis or null on failure and logs errors.
Frontend: Cache API & Local settings services
booklore-ui/src/app/shared/service/cache-storage.service.ts, booklore-ui/src/app/shared/service/local-settings.service.ts
Added CacheStorageService (wraps browser Cache Storage, supports validation via HEAD/If-Modified-Since, put/match/delete/clear/getCacheSizeInBytes) and LocalSettingsService (persist cacheStorageEnabled, defaults, load/commit helpers).
Frontend: Content loading & PDF reader
booklore-ui/src/app/features/book/service/book-file.service.ts, booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts
BookFileService.getFileContent now uses CacheStorageService when caching enabled (returns Blob/object-URL path via from(...)); PDF reader disables streaming (disableStream = true), resolves content via cache-aware helper, and revokes created blob URLs on destroy.
Frontend: Local Settings UI & wiring
booklore-ui/src/app/features/settings/local-settings/local-settings.component.ts, .../local-settings.component.html, .../local-settings.component.scss, booklore-ui/src/app/features/settings/settings.component.ts, booklore-ui/src/app/features/settings/settings.component.html
Added LocalSettingsComponent with template and styles to toggle cache, display/refresh usage, clear cache; added Local tab and registered component in settings imports.

Sequence Diagram

sequenceDiagram
    actor Browser as Client/Browser
    participant App as Frontend App
    participant Cache as CacheStorage (Browser)
    participant Http as HttpClient/Network
    participant API as Backend API

    Browser->>App: request book/audio URI
    App->>Cache: getCache(uri)
    alt cache hit
        Cache-->>App: cached Response
        alt validate freshness (noValidate = false)
            App->>Http: HEAD uri (If-Modified-Since: cached.last-modified)
            Http-->>App: 304 Not Modified
            App-->>Browser: serve cached blob / object-URL
            else 200 OK
            Http-->>App: 200 + body + headers
            App->>Cache: put(uri, Response.clone())
            App-->>Browser: serve fetched blob / object-URL
        end
    else cache miss
        App->>Http: GET uri
        Http->>API: GET request
        API-->>Http: Response (+ Last-Modified?)
        Http-->>App: Response
        App->>Cache: put(uri, Response.clone())
        App-->>Browser: serve fetched blob / object-URL
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
I tuck bytes in a secret lair,
HEAD taps check the cached-air,
Last-Modified hums the tune,
Blobs become a silver lune,
Hop — the reader wakes at noon.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically summarizes the main changes: introducing HTTP caching via Last-Modified headers and Cache Storage for persistent client-side caching of content responses.
Description check ✅ Passed The PR description covers the main objectives, improvements, how Cache Storage works, additional changes, and testing results. While somewhat informal in style, it provides sufficient detail about the feature's purpose and functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 9

🧹 Nitpick comments (2)
booklore-api/src/main/java/org/booklore/service/book/BookService.java (1)

31-31: Consider using explicit imports instead of wildcard.

Wildcard imports can obscure dependencies and potentially cause naming collisions. The explicit imports would be: CacheControl, MediaType, MediaTypeFactory, ResponseEntity.

Suggested explicit imports
-import org.springframework.http.*;
+import org.springframework.http.CacheControl;
+import org.springframework.http.MediaType;
+import org.springframework.http.MediaTypeFactory;
+import org.springframework.http.ResponseEntity;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/main/java/org/booklore/service/book/BookService.java` at
line 31, Replace the wildcard import "import org.springframework.http.*;" in
BookService.java with explicit imports for the symbols used so imports are clear
and avoid collisions; specifically add explicit imports for CacheControl,
MediaType, MediaTypeFactory, and ResponseEntity (remove the wildcard and
organize/import only the classes referenced by methods in BookService).
booklore-ui/src/app/shared/service/cache-storage.service.ts (1)

43-43: Handle cache write failures from put() to avoid unhandled async errors.

this.put(uri, res.clone()) is fire-and-forget; if caches.open()/cache.put() fails, the rejection is unobserved.

Proposed fix
-    this.put(uri, res.clone());
+    await this.put(uri, res.clone()).catch((error) => {
+      console.error("Error writing cache:", error);
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-ui/src/app/shared/service/cache-storage.service.ts` at line 43, The
call this.put(uri, res.clone()) is currently fire-and-forget and can produce
unobserved promise rejections if caches.open()/cache.put() fails; update the
caller (or put method) to await the promise or attach a .catch handler to it and
log or handle errors to prevent unhandled async errors. Specifically, locate the
invocation this.put(uri, res.clone()) in CacheStorageService and change it to
either await this.put(uri, res.clone()) inside an async function or append
.catch(err => {/* log via your logger or console.error with context */}) so
failures from put() (and internals like caches.open()/cache.put()) are observed
and handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-ui/src/app/features/book/service/book-file.service.ts`:
- Around line 34-36: The cache path in BookFileService (when
localSettingsService.get().cacheStorageEnabled is true) must handle rejections
and falsy responses from cacheStorageService.getCache(url) and fall back to
this.http.get; update the logic around cacheStorageService.getCache(url) (the
Promise wrapped with from(...)) to catch Promise rejections and check for a
falsy response/blob, and on error or missing data return the HTTP Observable
(this.http.get<Blob>(url, { responseType: 'blob' as 'json' })); implement this
using RxJS operators (e.g., from(...).pipe(catchError(...)) or
switchMap/mergeMap to choose the HTTP fallback) so cache failures do not
propagate to callers.

In `@booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts`:
- Around line 209-211: The constructed URI in pdf-reader.component.ts uses
direct concatenation of fileType into the query string (const uri = ... ?
`${API_CONFIG.BASE_URL}/api/v1/books/${bookId}/content?bookType=${fileType}` :
...), which can break for reserved characters; fix it by encoding the bookType
value with encodeURIComponent(fileType) when building the query parameter (keep
bookId and base URL unchanged) so the resulting uri is safe for HTTP requests.
- Around line 205-217: getBookData currently creates blob URLs via
URL.createObjectURL(blob) and assigns them to this.bookData without revoking
previous ones, and ngOnDestroy doesn't revoke the active blob URL; to fix,
introduce a property (e.g., currentBlobUrl) to track the active object URL,
ensure before assigning a new URL in getBookData you revoke the existing one via
URL.revokeObjectURL(currentBlobUrl) (and set currentBlobUrl to the new URL after
assignment), and revoke currentBlobUrl inside ngOnDestroy to release memory;
update any places that read/write this.bookData to use the tracked URL
accordingly (functions: getBookData, ngOnDestroy, and where this.bookData is
set/used).

In
`@booklore-ui/src/app/features/settings/local-settings/local-settings.component.html`:
- Around line 4-57: The template in local-settings.component.html is using
*transloco="let t; prefix: 'settingsNaming'" but still contains hardcoded
strings; replace each literal (header title/description, section
title/description, labels/descriptions for "Enable Cache Storage", "Cache
Storage Usage (MB)", "Clear Cache", and button labels "Refresh"/"Clear Cache")
with the appropriate transloco lookup using the t function (e.g. {{
t('localSettings.title') }}, {{ t('localSettings.description') }}, {{
t('cacheStorage.title') }}, etc.) while keeping bindings and event handlers
intact (settings.cacheStorageEnabled, cacheStorageUsageMb,
isLoadingCacheStorageUsage, isClearingCacheStorage, loadCacheStorageUsage(),
clearCacheStorage()); ensure keys use the existing prefix 'settingsNaming' so
strings localize consistently with other settings components.

In
`@booklore-ui/src/app/features/settings/local-settings/local-settings.component.scss`:
- Around line 9-45: Stylelint is flagging declaration-empty-line-before
violations in the SCSS blocks; open the .scss and for each selector shown in the
diff (.settings-title, .settings-description, .settings-card, .section-header,
.section-title, .section-description, .settings-content, .setting-item) adjust
blank lines so they follow the project's declaration-empty-line-before rule
(remove unexpected empty lines before declaration lists or add the single
required blank line where configured), ensure there are no extra blank lines
between declarations inside each block, and re-run stylelint (or apply stylelint
--fix) to verify the violations are resolved.

In
`@booklore-ui/src/app/features/settings/local-settings/local-settings.component.ts`:
- Around line 59-69: Wrap the body of the accept async handler in a
try/catch/finally: call await this.cacheStorageService.clear() and await
this.loadCacheStorageUsage() inside try, catch errors to call
this.messageService.add with a severity "error" and useful detail, and ensure
this.isClearingCacheStorage is always reset to false in finally; keep the
existing success message in the try path and reference the accept method,
isClearingCacheStorage flag, cacheStorageService.clear, loadCacheStorageUsage,
and messageService.add when making the changes.

In `@booklore-ui/src/app/features/settings/settings.component.html`:
- Around line 13-15: The "Local" tab label is hardcoded; update the p-tab
content that uses SettingsTab.LocalSettings to use the translation helper (e.g.,
{{ t('settings.tabs.local') }}) instead of the literal "Local", and add the
corresponding "settings.tabs.local" key with value "Local" to the translation
files (e.g., en.json) so the tab is internationalized; ensure you reference the
same key pattern used by other tabs like {{ t('reader') }}.

In `@booklore-ui/src/app/shared/service/cache-storage.service.ts`:
- Around line 97-101: The clear() method currently maps cache.keys() to an array
of this.delete(...) promises but doesn't wait for them, so fix clear() to await
all deletions before resolving: after obtaining keys from cache() and mapping to
this.delete(key.url), return or await Promise.all(...) of those delete promises
(or convert clear() to async and await the deletions). Ensure you still resolve
to void after all this.delete(...) calls complete, keeping references to the
cache() and delete(...) methods.
- Line 70: The catch clause in CacheStorageService uses an invalid union
annotation `catch (error: HttpErrorResponse | any)` which violates TS catch
typing rules; change the catch parameter to `catch (error: unknown)` in the
method where this appears (look for the catch in cache-storage.service.ts, e.g.,
inside methods of class CacheStorageService), then inside the catch block narrow
the error with runtime checks (for example use `if (error instanceof
HttpErrorResponse)` or type guard helpers) before accessing
HttpErrorResponse-specific properties, otherwise treat it as a generic error.

---

Nitpick comments:
In `@booklore-api/src/main/java/org/booklore/service/book/BookService.java`:
- Line 31: Replace the wildcard import "import org.springframework.http.*;" in
BookService.java with explicit imports for the symbols used so imports are clear
and avoid collisions; specifically add explicit imports for CacheControl,
MediaType, MediaTypeFactory, and ResponseEntity (remove the wildcard and
organize/import only the classes referenced by methods in BookService).

In `@booklore-ui/src/app/shared/service/cache-storage.service.ts`:
- Line 43: The call this.put(uri, res.clone()) is currently fire-and-forget and
can produce unobserved promise rejections if caches.open()/cache.put() fails;
update the caller (or put method) to await the promise or attach a .catch
handler to it and log or handle errors to prevent unhandled async errors.
Specifically, locate the invocation this.put(uri, res.clone()) in
CacheStorageService and change it to either await this.put(uri, res.clone())
inside an async function or append .catch(err => {/* log via your logger or
console.error with context */}) so failures from put() (and internals like
caches.open()/cache.put()) are observed and handled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 129b7314-91e8-4f5b-8d52-9964d60c56da

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef4448 and a4e1a80.

📒 Files selected for processing (14)
  • booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java
  • booklore-api/src/main/java/org/booklore/controller/AudiobookReaderController.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/util/FileUtils.java
  • booklore-ui/src/app/features/book/service/book-file.service.ts
  • booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.html
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.scss
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.ts
  • booklore-ui/src/app/features/settings/settings.component.html
  • booklore-ui/src/app/features/settings/settings.component.ts
  • booklore-ui/src/app/shared/service/cache-storage.service.ts
  • booklore-ui/src/app/shared/service/local-settings.service.ts

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-ui/src/app/shared/service/cache-storage.service.ts`:
- Around line 100-106: There is an extra closing brace that prematurely ends the
CacheStorageService class (after the clear() method) causing
getCacheSizeInBytes() and cache() to be parsed as top-level statements; remove
the spurious '}' so the class remains open and those methods
(getCacheSizeInBytes, cache, clear, etc.) are defined as members of
CacheStorageService.
- Around line 38-45: The call to this.put(uri, res.clone()) is not awaited so
getCache() can return before the entry is persisted and write failures are
ignored; update the code to await the cache write (await this.put(uri,
res.clone())) and wrap it in a try/catch so write errors are logged/handled (or
rethrown) rather than silently dropped; ensure put(uri, Response) returns a
Promise if it doesn't already and that callers of getCache()/put() can properly
observe failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d01da3b-d2e2-48ea-af14-560ed6801367

📥 Commits

Reviewing files that changed from the base of the PR and between a4e1a80 and d9708a6.

📒 Files selected for processing (2)
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.ts
  • booklore-ui/src/app/shared/service/cache-storage.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.ts

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

♻️ Duplicate comments (1)
booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts (1)

101-117: ⚠️ Potential issue | 🟠 Major

Revoke prior blob URL before replacing bookData during in-component navigation.

Line 196 cleanup only runs on destroy; Line 117 can overwrite an existing blob: URL with a new one while the component is still alive (route param changes), leaving the old URL unreleased.

Proposed fix
         next: ({book, bookSetting, myself, bookData}) => {
+          if (this.bookData?.startsWith('blob:') && this.bookData !== bookData) {
+            URL.revokeObjectURL(this.bookData);
+          }
           const pdfMeta = book;
           const pdfPrefs = bookSetting;
@@
           this.page = pdfMeta.pdfProgress?.page || 1;
           this.bookData = bookData;

Also applies to: 196-198, 217-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts`
around lines 101 - 117, Before overwriting this.bookData with a new blob URL in
the next handler, revoke the previous blob URL to avoid leaking object URLs: if
this.bookData is a non-empty string that starts with "blob:" call
URL.revokeObjectURL(this.bookData) (guarding for null/undefined), then assign
the new bookData; apply the same revoke-before-assign pattern wherever
this.bookData is set (including the component's ngOnDestroy cleanup and any
other assignments) so previous blob URLs are always released.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-ui/src/app/shared/service/cache-storage.service.ts`:
- Around line 70-76: In the catch block inside CacheStorageService
(cache-storage.service.ts) remove the duplicated check for HttpErrorResponse
with status === 304 so the branch appears only once; locate the catch handling
(the function where the try/catch checks "error instanceof HttpErrorResponse &&
error.status === 304") and delete the redundant repeated if statement so the
not-modified return true path is present a single time.

---

Duplicate comments:
In `@booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts`:
- Around line 101-117: Before overwriting this.bookData with a new blob URL in
the next handler, revoke the previous blob URL to avoid leaking object URLs: if
this.bookData is a non-empty string that starts with "blob:" call
URL.revokeObjectURL(this.bookData) (guarding for null/undefined), then assign
the new bookData; apply the same revoke-before-assign pattern wherever
this.bookData is set (including the component's ngOnDestroy cleanup and any
other assignments) so previous blob URLs are always released.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd4a1d51-c9f8-403d-825c-589f85a0c968

📥 Commits

Reviewing files that changed from the base of the PR and between d9708a6 and 454279c.

📒 Files selected for processing (3)
  • booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.scss
  • booklore-ui/src/app/shared/service/cache-storage.service.ts
✅ Files skipped from review due to trivial changes (1)
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.scss

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.

🧹 Nitpick comments (2)
booklore-ui/src/app/shared/service/cache-storage.service.ts (2)

104-111: Cache size calculation may be inaccurate when content-length header is missing.

The method relies on the content-length header which may not always be present (e.g., chunked responses, server omission). For responses without this header, size defaults to 0, under-reporting actual cache usage. Since this is displayed to users in the UI, consider using blob.size for accurate measurement.

♻️ Proposed fix using blob size
   getCacheSizeInBytes(): Promise<number> {
     return this.cache()
       .then((cache) => cache.keys())
       .then((keys) => keys.map((key) => this.match(key.url)))
       .then((promises) => Promise.all(promises))
-      .then((responses) => responses.map(response => parseInt(response?.headers.get("content-length") || "0")))
+      .then((responses) => Promise.all(
+        responses.map(response => response?.blob().then(blob => blob.size) ?? Promise.resolve(0))
+      ))
       .then((sizes) => sizes.reduce((total, size) => total + size, 0));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-ui/src/app/shared/service/cache-storage.service.ts` around lines 104
- 111, getCacheSizeInBytes currently sums Content-Length headers which can be
missing; update it to fall back to the actual response body size by calling
response.blob() and using blob.size when headers lack content-length (and handle
null/undefined responses and non-OK responses). In the promise chain starting
from cache() -> cache.keys() -> this.match(key.url) (and inside the responses
array), for each response check response?.headers.get("content-length") first,
parse it if present, otherwise await response.blob() and use blob.size (treat
missing response as size 0), then sum those sizes and return the total; keep
Promise.all for the blob async calls so behavior remains asynchronous.

26-28: Replace @ts-ignore with a safer type narrowing approach.

The @ts-ignore directive is too broad and suppresses all TypeScript errors on the line. The logic guarantees entry is defined here, but TypeScript cannot infer this. Use a more targeted approach.

♻️ Proposed fix
-    if (!stale)
-      //@ts-ignore
-      return entry;
+    if (!stale && entry) {
+      return entry;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-ui/src/app/shared/service/cache-storage.service.ts` around lines 26
- 28, Remove the broad `@ts-ignore` and narrow the type explicitly: instead of
suppressing TypeScript, check that entry is not null/undefined (e.g. if (!stale
&& entry !== undefined) return entry;) or use a non-null assertion only on the
return (return entry!) if you are certain entry exists; update the code around
the stale and entry variables so TypeScript can infer the defined type without
`@ts-ignore`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@booklore-ui/src/app/shared/service/cache-storage.service.ts`:
- Around line 104-111: getCacheSizeInBytes currently sums Content-Length headers
which can be missing; update it to fall back to the actual response body size by
calling response.blob() and using blob.size when headers lack content-length
(and handle null/undefined responses and non-OK responses). In the promise chain
starting from cache() -> cache.keys() -> this.match(key.url) (and inside the
responses array), for each response check
response?.headers.get("content-length") first, parse it if present, otherwise
await response.blob() and use blob.size (treat missing response as size 0), then
sum those sizes and return the total; keep Promise.all for the blob async calls
so behavior remains asynchronous.
- Around line 26-28: Remove the broad `@ts-ignore` and narrow the type explicitly:
instead of suppressing TypeScript, check that entry is not null/undefined (e.g.
if (!stale && entry !== undefined) return entry;) or use a non-null assertion
only on the return (return entry!) if you are certain entry exists; update the
code around the stale and entry variables so TypeScript can infer the defined
type without `@ts-ignore`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7831c9f7-4bd3-4c59-b3c4-defbdd6ed117

📥 Commits

Reviewing files that changed from the base of the PR and between 454279c and 8fac12f.

📒 Files selected for processing (1)
  • booklore-ui/src/app/shared/service/cache-storage.service.ts

@imajes
Copy link
Member

imajes commented Mar 20, 2026

@NamLe029 thanks for this -- can you rebase, and also give me a bit of an idea for what this improvement helps with - and what the risks are? usually when adding caching, we need to be thoughtful of areas that might require the latest / non-cached version... so is that afforded throughout?

@NamLe029
Copy link
Author

Oops, apologies for lack of details.

Improvements:
This feature helps reducing traffic to the server, especially for large book files, by caching responses and reusing them later.
Risks:

  • There is a chance that retrieving from cache storage fails. It's already addressed by fallback to fetching the server for response.
  • For possibility of outdated response/cache, every time the cache is requested, its validated by sending a HEAD request to server, checking if its been modified. So, we can be sure that the cache is up-to-date every request.

@NamLe029 NamLe029 force-pushed the web-reader-caching branch from 8fac12f to b551207 Compare March 21, 2026 03:58
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: 3

♻️ Duplicate comments (1)
booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts (1)

101-118: ⚠️ Potential issue | 🟠 Major

Revoke the previous blob URL before overwriting bookData.

When route params change in the same component instance, Lines 217-219 create a new object URL and Line 117 replaces this.bookData. Lines 196-198 only revoke the last URL on destroy, so earlier blob URLs stay pinned for the rest of the session.

Suggested change
         next: ({book, bookSetting, myself, bookData}) => {
+          if (this.bookData?.startsWith('blob:')) {
+            URL.revokeObjectURL(this.bookData);
+          }
+
           const pdfMeta = book;
           const pdfPrefs = bookSetting;
 
           this.pageTitle.setBookPageTitle(pdfMeta);
@@
           this.canPrint = myself.permissions.canDownload || myself.permissions.admin;
           this.page = pdfMeta.pdfProgress?.page || 1;
           this.bookData = bookData;

Also applies to: 196-198, 217-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts`
around lines 101 - 118, When assigning a new blob URL to this.bookData (the code
path that calls URL.createObjectURL around lines where createObjectURL is used),
revoke any previously-created URL first to avoid leaking pinned blobs; e.g., if
this.bookData?.blobUrl (or whatever property holds the URL) exists call
URL.revokeObjectURL(prevUrl) before overwriting this.bookData, and ensure
ngOnDestroy still revokes the final URL (the existing revoke logic in
ngOnDestroy should remain but confirm it uses the same property). Update the
assignment sites (the createObjectURL block and wherever this.bookData is
replaced) to perform the revoke and then set the new blob URL.
🧹 Nitpick comments (1)
booklore-api/src/main/java/org/booklore/util/FileUtils.java (1)

86-91: Treat missing files as a warn-and-null path.

This helper logs every missing-path case as an error, while getFileSizeInKb(Path) in the same class already treats that case as a non-fatal miss. Reusing that pattern here will keep transient cache-validation misses from looking like server faults.

♻️ Proposed fix
     public Long getFileLastModified(Path filePath) {
         try {
+            if (!Files.exists(filePath)) {
+                log.warn(FILE_NOT_FOUND_MESSAGE + "{}", filePath.toAbsolutePath());
+                return null;
+            }
             return Files.getLastModifiedTime(filePath).toMillis();
         } catch (IOException e) {
             log.error("Failed to get last modified for path [{}]: {}", filePath, e.getMessage(), e);
             return null;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/main/java/org/booklore/util/FileUtils.java` around lines 86
- 91, The getFileLastModified(Path) method currently logs all IOExceptions as
errors; change it to mirror getFileSizeInKb(Path) by detecting a missing-file
condition (e.g., NoSuchFileException or Files.notExists(filePath)) and logging
that case at WARN level while returning null, and only log other IOExceptions as
errors; update the log call in getFileLastModified to use log.warn(...) for
missing-paths and keep log.error(...) for unexpected IOExceptions so transient
cache misses aren't treated as server faults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-api/src/main/java/org/booklore/service/book/BookService.java`:
- Around line 349-350: The response currently sets CacheControl via
CacheControl.noCache().cachePrivate() in BookService (the
builder.cacheControl(...) call) which allows HTTP caching alongside the new
Cache Storage layer; update the backend to make Cache Storage the only persisted
cache by replacing the CacheControl invocation to use
CacheControl.noStore().cachePrivate() (i.e., change CacheControl.noCache() →
CacheControl.noStore() in the builder.cacheControl(...) call), so the server
sends no-store and avoids creating an unmanaged HTTP cache copy.

In
`@booklore-ui/src/app/features/settings/local-settings/local-settings.component.html`:
- Around line 28-33: The label for the "Enable Cache Storage" control is not
associated with the p-checkbox, so clicking the label won't toggle the checkbox
and screen readers lose the relationship; update the label element to include
for="cache-storage-enabled" and set the p-checkbox prop
inputId="cache-storage-enabled" (the p-checkbox bound to
settings.cacheStorageEnabled and using (onChange)="onSettingChange()") so the
visible label is correctly tied to the checkbox input for accessibility.

In `@booklore-ui/src/app/shared/service/local-settings.service.ts`:
- Around line 15-17: The default LocalSettings currently enables persistent
cache by setting defaultSettings.cacheStorageEnabled to true; change this
default to false so cacheStorageEnabled is opt-in via the Local Settings UI
(update the defaultSettings: LocalSettings object and any code that relies on
its initial value such as consumers of LocalSettings to handle false as
default). Ensure references to defaultSettings and the cacheStorageEnabled
property are updated so the app treats persistence as disabled until the user
explicitly enables it.

---

Duplicate comments:
In `@booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts`:
- Around line 101-118: When assigning a new blob URL to this.bookData (the code
path that calls URL.createObjectURL around lines where createObjectURL is used),
revoke any previously-created URL first to avoid leaking pinned blobs; e.g., if
this.bookData?.blobUrl (or whatever property holds the URL) exists call
URL.revokeObjectURL(prevUrl) before overwriting this.bookData, and ensure
ngOnDestroy still revokes the final URL (the existing revoke logic in
ngOnDestroy should remain but confirm it uses the same property). Update the
assignment sites (the createObjectURL block and wherever this.bookData is
replaced) to perform the revoke and then set the new blob URL.

---

Nitpick comments:
In `@booklore-api/src/main/java/org/booklore/util/FileUtils.java`:
- Around line 86-91: The getFileLastModified(Path) method currently logs all
IOExceptions as errors; change it to mirror getFileSizeInKb(Path) by detecting a
missing-file condition (e.g., NoSuchFileException or Files.notExists(filePath))
and logging that case at WARN level while returning null, and only log other
IOExceptions as errors; update the log call in getFileLastModified to use
log.warn(...) for missing-paths and keep log.error(...) for unexpected
IOExceptions so transient cache misses aren't treated as server faults.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9450181f-547e-4d95-a1c5-da725e1ab3ca

📥 Commits

Reviewing files that changed from the base of the PR and between 8fac12f and b551207.

📒 Files selected for processing (14)
  • booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java
  • booklore-api/src/main/java/org/booklore/controller/AudiobookReaderController.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/util/FileUtils.java
  • booklore-ui/src/app/features/book/service/book-file.service.ts
  • booklore-ui/src/app/features/readers/pdf-reader/pdf-reader.component.ts
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.html
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.scss
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.ts
  • booklore-ui/src/app/features/settings/settings.component.html
  • booklore-ui/src/app/features/settings/settings.component.ts
  • booklore-ui/src/app/shared/service/cache-storage.service.ts
  • booklore-ui/src/app/shared/service/local-settings.service.ts
✅ Files skipped from review due to trivial changes (2)
  • booklore-ui/src/app/features/settings/local-settings/local-settings.component.scss
  • booklore-ui/src/app/shared/service/cache-storage.service.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java
  • booklore-ui/src/app/features/settings/settings.component.html
  • booklore-ui/src/app/features/settings/settings.component.ts
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-ui/src/app/features/book/service/book-file.service.ts
  • booklore-api/src/main/java/org/booklore/controller/AudiobookReaderController.java

Comment on lines +28 to +33
<label class="setting-label">Enable Cache Storage</label>
<span class="setting-description">Store data locally in your browser for faster access</span>
</div>
<div class="setting-control">
<p-checkbox [(ngModel)]="settings.cacheStorageEnabled" [binary]="true" (onChange)="onSettingChange()">
</p-checkbox>
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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "local-settings.component.html" | head -5

Repository: grimmory-tools/grimmory

Length of output: 151


🌐 Web query:

PrimeNG p-checkbox inputId label association documentation

💡 Result:

To associate an HTML <label> with a PrimeNG p-checkbox, set an inputId on the checkbox and point the label’s for attribute to the same value (this links to the hidden native input PrimeNG renders). [1]

<p-checkbox inputId="ny" [(ngModel)]="checked" [binary]="true"></p-checkbox>
<label for="ny">New York</label>

PrimeNG also documents accessibility alternatives if you can’t/won’t use a <label>: ariaLabelledBy (reference another element’s id) or ariaLabel (string label). [1]

Source: PrimeNG Checkbox docs + Accessibility section. [1]


🏁 Script executed:

cat -n ./booklore-ui/src/app/features/settings/local-settings/local-settings.component.html | sed -n '20,40p'

Repository: grimmory-tools/grimmory

Length of output: 1119


🏁 Script executed:

# Also check if there are other label-checkbox patterns in the same file
grep -n "p-checkbox\|<label" ./booklore-ui/src/app/features/settings/local-settings/local-settings.component.html

Repository: grimmory-tools/grimmory

Length of output: 427


Bind the visible label to the checkbox input for accessibility.

Line 28 renders a <label>, but the p-checkbox at Line 32 has no input association. Clicking the label text won't toggle the control, and assistive technology loses the label relationship. Add inputId="cache-storage-enabled" to the checkbox and for="cache-storage-enabled" to the label.

Suggested change
-            <label class="setting-label">Enable Cache Storage</label>
+            <label class="setting-label" for="cache-storage-enabled">Enable Cache Storage</label>
             <span class="setting-description">Store data locally in your browser for faster access</span>
@@
-            <p-checkbox [(ngModel)]="settings.cacheStorageEnabled" [binary]="true" (onChange)="onSettingChange()">
+            <p-checkbox inputId="cache-storage-enabled" [(ngModel)]="settings.cacheStorageEnabled" [binary]="true" (onChange)="onSettingChange()">
             </p-checkbox>
📝 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
<label class="setting-label">Enable Cache Storage</label>
<span class="setting-description">Store data locally in your browser for faster access</span>
</div>
<div class="setting-control">
<p-checkbox [(ngModel)]="settings.cacheStorageEnabled" [binary]="true" (onChange)="onSettingChange()">
</p-checkbox>
<label class="setting-label" for="cache-storage-enabled">Enable Cache Storage</label>
<span class="setting-description">Store data locally in your browser for faster access</span>
</div>
<div class="setting-control">
<p-checkbox inputId="cache-storage-enabled" [(ngModel)]="settings.cacheStorageEnabled" [binary]="true" (onChange)="onSettingChange()">
</p-checkbox>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@booklore-ui/src/app/features/settings/local-settings/local-settings.component.html`
around lines 28 - 33, The label for the "Enable Cache Storage" control is not
associated with the p-checkbox, so clicking the label won't toggle the checkbox
and screen readers lose the relationship; update the label element to include
for="cache-storage-enabled" and set the p-checkbox prop
inputId="cache-storage-enabled" (the p-checkbox bound to
settings.cacheStorageEnabled and using (onChange)="onSettingChange()") so the
visible label is correctly tied to the checkbox input for accessibility.

Comment on lines +15 to +17
public readonly defaultSettings: LocalSettings = {
cacheStorageEnabled: true,
};
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

Make persistent content caching opt-in.

Line 16 enables Cache Storage for every device by default, and the downstream readers/services use that flag to persist full EPUB/PDF responses locally. That makes retained content and disk usage opt-out, which is risky on shared browsers. Please default this to false and let the new Local Settings UI enable it explicitly.

Suggested change
   public readonly defaultSettings: LocalSettings = {
-    cacheStorageEnabled: true,
+    cacheStorageEnabled: false,
   };
📝 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
public readonly defaultSettings: LocalSettings = {
cacheStorageEnabled: true,
};
public readonly defaultSettings: LocalSettings = {
cacheStorageEnabled: false,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-ui/src/app/shared/service/local-settings.service.ts` around lines 15
- 17, The default LocalSettings currently enables persistent cache by setting
defaultSettings.cacheStorageEnabled to true; change this default to false so
cacheStorageEnabled is opt-in via the Local Settings UI (update the
defaultSettings: LocalSettings object and any code that relies on its initial
value such as consumers of LocalSettings to handle false as default). Ensure
references to defaultSettings and the cacheStorageEnabled property are updated
so the app treats persistence as disabled until the user explicitly enables it.

+ avoid double caching
+ detach get & validate logic
+ error handling for cache insert
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants