Skip to content

refactor(icon): fix SVG icons loading straight to memory#56

Open
balazs-szucs wants to merge 8 commits intogrimmory-tools:developfrom
balazs-szucs:SVG-fix
Open

refactor(icon): fix SVG icons loading straight to memory#56
balazs-szucs wants to merge 8 commits intogrimmory-tools:developfrom
balazs-szucs:SVG-fix

Conversation

@balazs-szucs
Copy link
Member

@balazs-szucs balazs-szucs commented Mar 18, 2026

📝 Description

Backend

  • The initialization logic that preloads all SVG icons into a cache at service startup is removed.
  • When saving a new SVG icon, the service now ensures the target directory exists before writing the file.
  • Removed the @PostConstruct initialization and icon cache loading from IconService, eliminating eager loading of all SVG icons at startup.
  • Ensured that the directory for SVG icons is created as needed when saving a new icon.

Frontend

  • The post-login initializer no longer preloads all icons after login.
  • The icon picker component now loads icon names and their contents only when the user opens the relevant tab, fetching missing icon content as needed.
  • The icon service is updated to provide paginated icon name retrieval and individual icon content fetches, removing the previous "preload all" method.

Linked Issue: Fixes #

Required. Every PR must reference an approved issue. If no issue exists, open one and wait for maintainer approval before submitting a PR. Unsolicited PRs without a linked issue will be closed.

🏷️ Type of Change

  • Bug fix
  • New feature
  • Enhancement to existing feature
  • Refactor (no behavior change)
  • Breaking change (existing functionality affected)
  • Documentation update

🔧 Changes

🧪 Testing (MANDATORY)

PRs without this section filled out will be closed. "Tests pass" or "Tested locally" is not sufficient. You must provide specifics.

Manual testing steps you performed:

Regression testing:

Edge cases covered:

Test output:

Backend test output (./gradlew test)
PASTE OUTPUT HERE
Frontend test output (ng test)
PASTE OUTPUT HERE

📸 Screen Recording / Screenshots (MANDATORY)

Every PR must include a screen recording or screenshots showing the change working end-to-end in a running local instance (both backend and frontend). This means you must have actually built, run, and tested the code yourself. PRs without visual proof will be closed without review.


✅ Pre-Submission Checklist

All boxes must be checked before requesting review. Incomplete PRs will be closed without review. No exceptions.

  • This PR is linked to an approved issue
  • Code follows project backend and frontend conventions
  • Branch is up to date with develop (merge conflicts resolved)
  • I ran the full stack locally (backend + frontend + database) and verified the change works
  • Automated tests added or updated to cover changes (backend and frontend)
  • All tests pass locally and output is pasted above
  • Screen recording or screenshots are attached above proving the change works
  • PR is a single focused change (one bug fix OR one feature, not multiple unrelated changes)
  • PR is reasonably scoped (PRs over 1000+ changed lines will be closed, split into smaller PRs)
  • No unsolicited refactors, cleanups, or "improvements" are bundled in
  • Flyway migration versioning is correct (if schema was modified)
  • Documentation PR submitted to booklore-docs (if user-facing changes)

🤖 AI-Assisted Contributions

If any part of this PR was generated or assisted by AI tools (Copilot, Claude, ChatGPT, etc.), all items below are mandatory. You are fully responsible for every line you submit. "The AI wrote it" is not an excuse, and AI-generated PRs that clearly haven't been reviewed are the #1 reason PRs get closed.

  • I have read and understand every line of this PR and can explain any part of it during review
  • I personally ran the code and verified it works (not just trusted the AI's output)
  • PR is scoped to a single logical change, not a dump of everything the AI suggested
  • Tests validate actual behavior, not just coverage (AI-generated tests often assert nothing meaningful)
  • No dead code, placeholder comments, TODOs, or unused scaffolding left behind by AI
  • I did not submit refactors, style changes, or "improvements" the AI suggested beyond the scope of the issue

💬 Additional Context (optional)

Summary by CodeRabbit

  • Performance Improvements

    • Icons now load on-demand instead of preloading at startup, improving app startup time.
    • Icon picker loads icons asynchronously with concurrent fetching when the icon tab is opened and refreshes its list after saves.
  • Bug Fixes

    • Safer SVG saves: parent directories are created automatically before writing files.
    • Improved component teardown to prevent lingering subscriptions and potential memory leaks.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ca124c1-0843-4bb7-9458-f1660d00375b

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec99e1 and ff5285c.

📒 Files selected for processing (1)
  • booklore-api/src/test/java/org/booklore/service/IconServiceTest.java
💤 Files with no reviewable changes (1)
  • booklore-api/src/test/java/org/booklore/service/IconServiceTest.java

📝 Walkthrough

Walkthrough

Backend removed startup cache preloading and ensured parent directories are created when saving SVGs. Frontend removed automatic post-login icon preloading, introduced a paginated icon-name API call, and refactored the icon-picker to load SVG icons on-demand with concurrent per-icon fetches and error handling. Multiple component subscriptions were bound to teardown.

Changes

Cohort / File(s) Summary
Backend Icon Service
booklore-api/src/main/java/org/booklore/service/IconService.java
Deleted @PostConstruct init() / loadIconsIntoCache() startup cache population. Added Files.createDirectories(filePath.getParent()) in saveSvgIcon(...).
Backend Tests
booklore-api/src/test/java/org/booklore/service/IconServiceTest.java
Removed explicit iconService.init() call from test setup (reflects removal of startup init).
Frontend Post-Login Init
booklore-ui/src/app/core/services/post-login-initializer.service.ts
Removed injected IconService usage and preloadIcons() orchestration; initialize() now returns immediate of(undefined) (no icon preloading).
Frontend Icon Service
booklore-ui/src/app/shared/services/icon.service.ts
Removed preloadAllIcons() and bulk-cache population logic. Added getIconNames(page?: number, size?: number): Observable<string[]> to fetch paginated icon names from backend.
Frontend Icon Picker Component
booklore-ui/src/app/shared/components/icon-picker/icon-picker-component.ts
Added hasLoadedSvgIcons and new loadSvgIcons() that calls getIconNames(), handles errors, and concurrently fetches per-icon SVG content for uncached items (from, mergeMap(...,5), toArray). Removed cache-only loader and updated tab/init/post-save flows to trigger on-demand loading; added RxJS imports.
Frontend Book Browser
booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts
Bound multiple RxJS subscriptions to teardown via takeUntil(this.destroy$). Extracted filter-toggle handling into setupFilterToggleSubscription() to avoid repeated subscriptions.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant IconPicker as Icon Picker Component
    participant IconService as Frontend IconService
    participant Backend as Backend API

    User->>IconPicker: Open icon picker (activate SVG tab)
    IconPicker->>IconService: getIconNames(page, size)
    IconService->>Backend: GET /api/icons?page=...&size=...
    Backend-->>IconService: ["iconA","iconB",...]
    IconService-->>IconPicker: icon name list

    alt names empty
        IconPicker->>IconPicker: clear svgIcons, stop loading
    else names present
        IconPicker->>IconService: getSvgIconContent(name) [concurrent]
        IconService->>Backend: GET /api/icons/{name}/content
        Backend-->>IconService: SVG content
        IconService-->>IconPicker: sanitized SVG
        IconPicker->>IconPicker: aggregate results (toArray), update svgIcons
    end

    IconPicker-->>User: display icons
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped through folders, small and bright,
Names arrived by day, SVGs by night.
I fetch with gentle, concurrent paws,
No preload fuss — just orderly laws.
🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete. While it provides a clear summary of backend and frontend changes, it is missing the linked issue number, testing details, screenshots, and pre-submission checklist confirmation. Complete the required sections: add the linked issue number under 'Linked Issue', provide detailed manual testing steps with actual terminal output, include screenshots or screen recordings, and check all pre-submission checklist boxes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of removing eager icon cache initialization and switching to on-demand loading, which is the core theme across all backend and frontend changes.

✏️ 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.

Tip

CodeRabbit can use TruffleHog to scan for secrets in your code with verification capabilities.

Add a TruffleHog config file (e.g. trufflehog-config.yml, trufflehog.yml) to your project to customize detectors and scanning behavior. The tool runs only when a config file is present.

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

Caution

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

⚠️ Outside diff range comments (1)
booklore-api/src/main/java/org/booklore/service/IconService.java (1)

52-61: ⚠️ Potential issue | 🔴 Critical

Make icon creation atomic to prevent duplicate-write races.

The saveSvgIcon method checks file existence on line 52, then writes with CREATE + TRUNCATE_EXISTING on lines 59-61. This is a TOCTOU race condition: two concurrent requests can both pass the existence check and one can overwrite the other's file.

Use atomic creation (CREATE_NEW) instead, which atomically creates the file or throws FileAlreadyExistsException if it already exists. This eliminates the race window.

Proposed fix
+import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
@@
-        if (Files.exists(filePath)) {
-            log.warn("SVG icon already exists: {}", filename);
-            throw ApiError.ICON_ALREADY_EXISTS.createException(request.getSvgName());
-        }
-
         try {
             Files.createDirectories(filePath.getParent());
             Files.writeString(filePath, request.getSvgData(),
-                    StandardOpenOption.CREATE,
-                    StandardOpenOption.TRUNCATE_EXISTING);
+                    StandardOpenOption.CREATE_NEW);
 
             updateCache(filename, request.getSvgData());
 
             log.info("SVG icon saved successfully: {}", filename);
+        } catch (FileAlreadyExistsException e) {
+            log.warn("SVG icon already exists: {}", filename);
+            throw ApiError.ICON_ALREADY_EXISTS.createException(request.getSvgName());
         } catch (IOException e) {
             log.error("Failed to save SVG icon: {}", e.getMessage(), e);
             throw ApiError.FILE_READ_ERROR.createException("Failed to save SVG icon: " + e.getMessage());
         }
🤖 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/IconService.java` around
lines 52 - 61, The saveSvgIcon method currently does a TOCTOU check with
Files.exists(filePath) then writes using CREATE + TRUNCATE_EXISTING; change it
to attempt an atomic creation using StandardOpenOption.CREATE_NEW (and remove
TRUNCATE_EXISTING) when calling Files.writeString(filePath,
request.getSvgData(), StandardOpenOption.CREATE_NEW) after ensuring parent
directories exist, and catch FileAlreadyExistsException to throw
ApiError.ICON_ALREADY_EXISTS.createException(request.getSvgName()) so concurrent
requests cannot overwrite each other; keep other IO exceptions
propagated/handled as before.
🤖 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/components/icon-picker/icon-picker-component.ts`:
- Around line 145-150: The current implementation in loadSvgIcons builds
contentRequests via names.map and passes them to forkJoin, causing up to size
(default 1000) parallel HTTP requests; replace this with a stream-based approach
using from(names) piped through mergeMap to call
iconService.getSvgIconContent(name) only for names that are not cached (use
iconCache.getCachedSanitized(name) to skip), and set mergeMap's concurrency
parameter to a reasonable limit (e.g., 5–20) so requests run bounded
concurrently; collect results (e.g., toArray or accumulating side effects) and
subscribe, preserving the same post-load behavior previously in the forkJoin
subscription.

---

Outside diff comments:
In `@booklore-api/src/main/java/org/booklore/service/IconService.java`:
- Around line 52-61: The saveSvgIcon method currently does a TOCTOU check with
Files.exists(filePath) then writes using CREATE + TRUNCATE_EXISTING; change it
to attempt an atomic creation using StandardOpenOption.CREATE_NEW (and remove
TRUNCATE_EXISTING) when calling Files.writeString(filePath,
request.getSvgData(), StandardOpenOption.CREATE_NEW) after ensuring parent
directories exist, and catch FileAlreadyExistsException to throw
ApiError.ICON_ALREADY_EXISTS.createException(request.getSvgName()) so concurrent
requests cannot overwrite each other; keep other IO exceptions
propagated/handled as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e98bacb-b3c9-44b6-adb3-81f6f7f58c85

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0c4aa and 36d9f51.

📒 Files selected for processing (4)
  • booklore-api/src/main/java/org/booklore/service/IconService.java
  • booklore-ui/src/app/core/services/post-login-initializer.service.ts
  • booklore-ui/src/app/shared/components/icon-picker/icon-picker-component.ts
  • booklore-ui/src/app/shared/services/icon.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/components/icon-picker/icon-picker-component.ts`:
- Around line 146-157: The current subscription delays setting
svgIcons/isLoadingSvgIcons until every SVG body is fetched; instead immediately
assign this.svgIcons = names and this.isLoadingSvgIcons = false so the SVG tab
can render names right away, and remove the toArray()-based blocking pipeline;
change the from(names).pipe(...) logic to either kick off non-blocking
background warming (subscribe without awaiting toArray) or move
getSvgIconContent calls to a lazy loader used by the renderer/selection code
(e.g., a method like loadSvgFor(name) that checks
iconCache.getCachedSanitized(name) and calls
iconService.getSvgIconContent(name).pipe(catchError(()=>of(null))) when an item
becomes visible or selected), ensuring background fetches don’t prevent
publishing svgIcons.
- Around line 83-87: Replace the current "load if svgIcons.length === 0" logic
with explicit load-state flags: add booleans isLoadingSvgIcons and
isSvgIconsLoaded, set isLoadingSvgIcons=true at start of loadSvgIcons() and
false in finally, and set isSvgIconsLoaded=true when the fetch completes (even
if the returned array is empty); then update the activeTabIndex setter and any
init logic (e.g., ngOnInit / tab-activation code) to call loadSvgIcons() only
when value === '1' && !isSvgIconsLoaded && !isLoadingSvgIcons to prevent
repeated fetches and duplicate loads when the tab is already active during
initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e8edebf-69c8-4f60-b834-b36de042b14a

📥 Commits

Reviewing files that changed from the base of the PR and between 36d9f51 and 969644b.

📒 Files selected for processing (1)
  • booklore-ui/src/app/shared/components/icon-picker/icon-picker-component.ts

imajes
imajes previously approved these changes Mar 20, 2026
@imajes imajes dismissed their stale review March 20, 2026 22:13

removing review to make sure it passes tests against develop etc.

@imajes
Copy link
Member

imajes commented Mar 21, 2026

@balazs-szucs can you rebase this against develop please :)

github-actions bot and others added 2 commits March 21, 2026 02:17
## [2.3.0](grimmory-tools/grimmory@v2.2.6...v2.3.0) (2026-03-21)

### Features

* **release:** document develop-based stable release previews ([930e526](grimmory-tools@930e526))

### Bug Fixes

* **api:** fix potential memory leaks in file processing ([031e8ae](grimmory-tools@031e8ae))
* **ci:** correct artifact download action pin ([37ca101](grimmory-tools@37ca101))
* **ci:** publish PR test results from workflow_run ([11a76bf](grimmory-tools@11a76bf))
* **ci:** repair release preview and test result publishing ([afa5b81](grimmory-tools@afa5b81))
* drop telemetry from app ([grimmory-tools#52](grimmory-tools#52)) ([4d82cb7](grimmory-tools@4d82cb7))
* **ui:** repair frontend compile after rebrand ([fea1ec6](grimmory-tools@fea1ec6))

### Refactors

* **build:** rename frontend dist output to grimmory ([ecf388f](grimmory-tools@ecf388f))
* **i18n:** rename booklore translation keys to grimmory ([eb94afa](grimmory-tools@eb94afa))
* **metadata:** move default parser from Amazon to Goodreads ([e252122](grimmory-tools@e252122))
* pull kepubify & ffprobe during build ([grimmory-tools#50](grimmory-tools#50)) ([1c15629](grimmory-tools@1c15629))
* **ui:** rebrand frontend surfaces to grimmory ([d786dd8](grimmory-tools@d786dd8))

### Chores

* **api:** remove the custom startup banner ([98c9b1a](grimmory-tools@98c9b1a))
* **deps:** bump flatted from 3.4.1 to 3.4.2 in /booklore-ui ([grimmory-tools#73](grimmory-tools#73)) ([c4bd0c7](grimmory-tools@c4bd0c7))
* **funding:** point support links at opencollective ([55c0ac0](grimmory-tools@55c0ac0))
* **release:** 2.2.7 [skip ci] ([0b5e24c](grimmory-tools@0b5e24c))
* remove old verbose PR template, replace with temporary more low-key one. ([grimmory-tools#84](grimmory-tools#84)) ([b868526](grimmory-tools@b868526))
* **ui:** drop financial support dialog ([grimmory-tools#21](grimmory-tools#21)) ([62be6b1](grimmory-tools@62be6b1))

### Documentation

* updated supported file formats in README.md ([grimmory-tools#68](grimmory-tools#68)) ([f912e80](grimmory-tools@f912e80))

### Style

* **i18n:** normalize translation json formatting ([grimmory-tools#89](grimmory-tools#89)) ([857290d](grimmory-tools@857290d))
* **ui:** simplify the topbar logo branding ([0416d48](grimmory-tools@0416d48))
@balazs-szucs balazs-szucs changed the base branch from main to develop March 21, 2026 11:47
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

🤖 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/components/book-browser/book-browser.component.ts`:
- Line 299: The subscription to coverScalePreferenceService.scaleChange$
currently discards values
(this.coverScalePreferenceService.scaleChange$.pipe(debounceTime(1000),
takeUntil(this.destroy$)).subscribe();) and should be removed or given an
explicit handler and comment; either delete this line entirely if the component
has no side-effects on scale changes (the service already persists via
saveScalePreference()), or replace subscribe() with a documented callback that
performs the intended component-side work (e.g., update local UI state or call a
refresh method) and keep the debounceTime(1000) and takeUntil(this.destroy$) for
cleanup; refer to coverScalePreferenceService, scaleChange$,
saveScalePreference(), debounceTime and destroy$ when locating the code to
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: adc9cf4d-0136-43d1-9bda-7083d6252c3e

📥 Commits

Reviewing files that changed from the base of the PR and between 7fc9f70 and 8ec99e1.

📒 Files selected for processing (1)
  • booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts

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.

3 participants