Skip to content

fix(ui): prevent console error when leaving reader#956

Merged
imnotjames merged 1 commit into
grimmory-tools:developfrom
imnotjames:fix/954/leaving-reader-error
Apr 28, 2026
Merged

fix(ui): prevent console error when leaving reader#956
imnotjames merged 1 commit into
grimmory-tools:developfrom
imnotjames:fix/954/leaving-reader-error

Conversation

@imnotjames

@imnotjames imnotjames commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

the reader was not properly cleaning up after itself after being closed. instead of using the destroy helper, we use the custom element callback disconnectedCallback() to ensure clean up happens

Description

Linked Issue: Fixes #954

Changes

  • actually destroy the foliate Paginator when disconnected from the document

Summary by CodeRabbit

  • Refactor
    • Improved component lifecycle handling to ensure observers and resources are cleanly disconnected when views change or are removed.
    • Safer view handling to avoid errors if embedded content is unavailable, resulting in more stable navigation and smoother view replacements.
    • Reduces potential resource leaks and improves overall UI reliability.

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 01252320-3601-4161-b1be-71fe220c91fd

📥 Commits

Reviewing files that changed from the base of the PR and between f539b14 and 57950d3.

📒 Files selected for processing (1)
  • frontend/src/assets/foliate/paginator.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/assets/foliate/paginator.js
📜 Recent review details
⏰ 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). (3)
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (java-kotlin)

📝 Walkthrough

Walkthrough

Replaced explicit destroy() teardown with DOM lifecycle disconnectedCallback() for View and Paginator; observers are fully disconnected. View.expand() now guards against missing iframe/document to avoid null access. Paginator forwards disconnected cleanup to the current View and calls disconnectedCallback() when replacing views.

Changes

Cohort / File(s) Summary
Lifecycle & safety fixes
frontend/src/assets/foliate/paginator.js
Removed View.destroy() and Paginator.destroy(); added disconnectedCallback() implementations that call ResizeObserver.disconnect() and forward cleanup. Replaced calls to destroy() with disconnectedCallback() when swapping views. Added null-safety guard in View.expand() to avoid accessing a detached iframe/document.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

frontend, enhancement

Suggested reviewers

  • imajes
  • zachyale
  • balazs-szucs

Poem

🐰 I nudge the iframe, soft and sly,

If document's gone, I hop on by.
Disconnect my whiskers, tidy and quick,
No dangling observers, no unexpected flick.
Hooray — the reader rests, calm and slick!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows conventional commit format with 'fix(ui)' prefix and clearly describes preventing console error when leaving reader.
Description check ✅ Passed The PR description includes all required template sections: Description explaining the issue and solution, Linked Issue reference (#954), and Changes section.
Linked Issues check ✅ Passed The PR directly addresses issue #954 by replacing destroy() with disconnectedCallback() to properly clean up the Paginator when disconnected, preventing the 'this.document is null' console error.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing resource cleanup in paginator.js by implementing proper lifecycle management, directly related to resolving the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

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

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.

@imnotjames imnotjames marked this pull request as ready for review April 28, 2026 04:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
frontend/src/assets/foliate/paginator.js (2)

1120-1126: ⚠️ Potential issue | 🟡 Minor

Guard unload path when open() hasn’t initialized sections yet.

At Line 1124, this.sections[this.#index]?.unload?.() throws if this.sections is undefined (e.g., element disconnected before open(book)).

✅ Minimal safe fix
-        this.sections[this.#index]?.unload?.()
+        this.sections?.[this.#index]?.unload?.()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/assets/foliate/paginator.js` around lines 1120 - 1126,
disconnectedCallback currently accesses this.sections[this.#index]?.unload?.()
which throws if this.sections is undefined (e.g., element disconnected before
open(book) ran); update disconnectedCallback (method name) to guard the path by
checking this.sections is truthy (and that this.#index is a valid index) before
calling unload on the chosen section, e.g., only call section.unload if
this.sections exists and the indexed section is defined; ensure you still
disconnect this.#observer, call this.#view?.disconnectedCallback(), set
this.#view = null, and remove the media query listener as before.

452-626: ⚠️ Potential issue | 🟠 Major

Prevent duplicate handlers on reconnect in connectedCallback().

connectedCallback() can run more than once per instance (Web Components lifecycle), and lines 567–574 add new bound listeners each time since .bind(this) creates a fresh function reference. disconnectedCallback() (lines 1120–1126) only removes the mediaQuery listener but leaves the host listeners intact. Reattaching the element adds duplicate touchstart, touchmove, and touchend handlers, causing navigation/selection to fire multiple times.

The same applies to the 'load' and 'relocate' listeners (lines 583–603) and the container scroll listeners (lines 567–572).

Fix by storing stable handler references as fields and guarding setup with an initialization flag:

♻️ Suggested fix
export class Paginator extends HTMLElement {
+    `#initialized` = false
+    `#onHostTouchStart` = e => this.#onTouchStart(e)
+    `#onHostTouchMove` = e => this.#onTouchMove(e)
+    `#onHostTouchEnd` = e => this.#onTouchEnd(e)
+    `#onMediaQueryChange` = () => {
+        if (!this.#view) return
+        this.#background.style.background = getBackground(this.#view.document)
+    }
+
     connectedCallback() {
+        if (this.#initialized) {
+            this.#observer.observe(this.#container)
+            this.#mediaQuery.addEventListener('change', this.#onMediaQueryChange)
+            return
+        }
+        this.#initialized = true
         // ... existing setup ...
-        this.addEventListener('touchstart', this.#onTouchStart.bind(this), opts)
-        this.addEventListener('touchmove', this.#onTouchMove.bind(this), opts)
-        this.addEventListener('touchend', this.#onTouchEnd.bind(this))
+        this.addEventListener('touchstart', this.#onHostTouchStart, opts)
+        this.addEventListener('touchmove', this.#onHostTouchMove, opts)
+        this.addEventListener('touchend', this.#onHostTouchEnd)
         // ... other listeners ...
-        this.#mediaQueryListener = () => {
-            if (!this.#view) return
-            this.#background.style.background = getBackground(this.#view.document)
-        }
-        this.#mediaQuery.addEventListener('change', this.#mediaQueryListener)
+        this.#mediaQuery.addEventListener('change', this.#onMediaQueryChange)
     }

     disconnectedCallback() {
         this.#observer.disconnect()
+        this.removeEventListener('touchstart', this.#onHostTouchStart)
+        this.removeEventListener('touchmove', this.#onHostTouchMove)
+        this.removeEventListener('touchend', this.#onHostTouchEnd)
+        this.removeEventListener('load', this.#onLoad)
+        this.removeEventListener('relocate', this.#onRelocate)
+        this.#container.removeEventListener('scroll', this.#onContainerScroll)
+        this.#container.removeEventListener('scroll', this.#onContainerScrollDebounced)
+        this.#mediaQuery.removeEventListener('change', this.#onMediaQueryChange)
         // ... existing cleanup ...
     }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/assets/foliate/paginator.js` around lines 452 - 626,
connectedCallback currently binds and adds new anonymous listeners each time
(touchstart/touchmove/touchend, container scroll handlers, 'load' and 'relocate'
handlers and mediaQuery listener) which creates duplicates on reconnect; fix by
creating persistent bound handler fields (e.g. this._boundOnTouchStart =
this.#onTouchStart.bind(this), this._boundOnTouchMove, this._boundOnTouchEnd,
this._boundOnContainerScroll, this._boundOnContainerDebouncedScroll,
this._boundOnLoad, this._boundOnRelocate, this.#mediaQueryListener) and use
those fields when calling addEventListener in connectedCallback, add a guard
flag (e.g. this._initialized) to skip reattaching when already initialized, and
ensure disconnectedCallback removes the same stored handlers
(removeEventListener for touch, container scroll, load, relocate, and mediaQuery
change) and clears the flag so reattachment is safe.
🧹 Nitpick comments (1)
frontend/src/assets/foliate/paginator.js (1)

667-669: Prefer a neutral disposer name over manual disconnectedCallback() invocation.

Line 667 manually calls this.#view.disconnectedCallback(), but View is not a custom element. Renaming this to dispose() improves lifecycle clarity and avoids confusion with platform-managed callbacks.

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

In `@frontend/src/assets/foliate/paginator.js` around lines 667 - 669, Replace the
manual call to the platform-style disconnectedCallback on the View instance with
a neutral disposer method: add/rename the View class method from
disconnectedCallback to dispose (or add a dispose that calls the existing
disconnectedCallback implementation), then update the call site in paginator to
call this.#view.dispose() instead of this.#view.disconnectedCallback(); ensure
any other references to disconnectedCallback on the View class are updated to
the new dispose method to keep lifecycle semantics consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/assets/foliate/paginator.js`:
- Around line 418-420: Disconnected cleanup currently only disconnects
this.#observer but doesn't prevent the pending doc.fonts.ready.then(() =>
this.expand()) from running after teardown; add a disposed boolean property
(e.g. this.#disposed) set to true in disconnectedCallback alongside
this.#observer.disconnect(), store or wrap the fonts.ready then-handler so it
checks the disposed flag before calling this.expand(), and make expand()
defensive by early-returning if this.#disposed or if this.document is
null/undefined to avoid accessing this.document after teardown.

---

Outside diff comments:
In `@frontend/src/assets/foliate/paginator.js`:
- Around line 1120-1126: disconnectedCallback currently accesses
this.sections[this.#index]?.unload?.() which throws if this.sections is
undefined (e.g., element disconnected before open(book) ran); update
disconnectedCallback (method name) to guard the path by checking this.sections
is truthy (and that this.#index is a valid index) before calling unload on the
chosen section, e.g., only call section.unload if this.sections exists and the
indexed section is defined; ensure you still disconnect this.#observer, call
this.#view?.disconnectedCallback(), set this.#view = null, and remove the media
query listener as before.
- Around line 452-626: connectedCallback currently binds and adds new anonymous
listeners each time (touchstart/touchmove/touchend, container scroll handlers,
'load' and 'relocate' handlers and mediaQuery listener) which creates duplicates
on reconnect; fix by creating persistent bound handler fields (e.g.
this._boundOnTouchStart = this.#onTouchStart.bind(this), this._boundOnTouchMove,
this._boundOnTouchEnd, this._boundOnContainerScroll,
this._boundOnContainerDebouncedScroll, this._boundOnLoad, this._boundOnRelocate,
this.#mediaQueryListener) and use those fields when calling addEventListener in
connectedCallback, add a guard flag (e.g. this._initialized) to skip reattaching
when already initialized, and ensure disconnectedCallback removes the same
stored handlers (removeEventListener for touch, container scroll, load,
relocate, and mediaQuery change) and clears the flag so reattachment is safe.

---

Nitpick comments:
In `@frontend/src/assets/foliate/paginator.js`:
- Around line 667-669: Replace the manual call to the platform-style
disconnectedCallback on the View instance with a neutral disposer method:
add/rename the View class method from disconnectedCallback to dispose (or add a
dispose that calls the existing disconnectedCallback implementation), then
update the call site in paginator to call this.#view.dispose() instead of
this.#view.disconnectedCallback(); ensure any other references to
disconnectedCallback on the View class are updated to the new dispose method to
keep lifecycle semantics consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: a785ebc4-cc7c-4558-ae5d-a002ab429c6d

📥 Commits

Reviewing files that changed from the base of the PR and between bf7c2d8 and f539b14.

📒 Files selected for processing (1)
  • frontend/src/assets/foliate/paginator.js
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 334
File: booklore-api/src/main/java/org/booklore/service/reader/EpubReaderService.java:402-407
Timestamp: 2026-04-02T09:25:48.330Z
Learning: In grimmory-tools/grimmory, before commenting on any file processing code (epub or pdf), always verify the current state of the relevant upstream grimmory libraries: `grimmory-tools/epub4j` (for epub) and `grimmory-tools/PDFium4j` (for pdf). These custom libraries may have different APIs, capabilities, and limitations compared to the third-party libraries they replace. Issues about streaming, buffering, or API surface may need to be filed/addressed in those upstream repos rather than in grimmory itself.

Comment thread frontend/src/assets/foliate/paginator.js
the reader was not properly cleaning up after itself after being closed.
instead of using the `destroy` helper, we use the custom element
callback `disconnectedCallback()` to ensure clean up happens
@imnotjames imnotjames force-pushed the fix/954/leaving-reader-error branch from f539b14 to 57950d3 Compare April 28, 2026 12:34
@imnotjames imnotjames merged commit f359e49 into grimmory-tools:develop Apr 28, 2026
16 checks passed
@imnotjames imnotjames deleted the fix/954/leaving-reader-error branch April 28, 2026 20:46
dsmouse pushed a commit to dsmouse/grimmory that referenced this pull request May 11, 2026
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.

Errors when interacting with and leaving reader

2 participants