Skip to content

fix(download): prune stale entries from the persisted queue on restore#231

Open
anishgoyal1108 wants to merge 1 commit into
IReaderorg:masterfrom
anishgoyal1108:chore/desktop-download-resume
Open

fix(download): prune stale entries from the persisted queue on restore#231
anishgoyal1108 wants to merge 1 commit into
IReaderorg:masterfrom
anishgoyal1108:chore/desktop-download-resume

Conversation

@anishgoyal1108

@anishgoyal1108 anishgoyal1108 commented Jun 6, 2026

Copy link
Copy Markdown

Description

DownloadManager restores its queue from JSON on startup but never pruned entries
the legacy runDownloadService path had already fulfilled — that path writes chapter
content to the DB and removes the DB download rows on completion without telling
DownloadManager to drop the matching persisted-queue entries. Over time the JSON
queue accumulated orphans that kept the "Downloading…" notification stuck at zero
real progress.

On restore we now cross-check each queued entry against the DB and drop ones whose
chapter no longer exists (book deleted) or that already have content. Includes the
chapterMapper / ChapterRepository content-length helpers the prune relies on and
the related BookDetailViewModel download-state wiring.

Type of Change

  • Bug fix

Testing

  • Compiled successfully (desktop) ← reviewer to confirm
  • Verified a queue with stale entries is pruned on next launch and the
    notification no longer sticks at 0%

Additional Notes

Applied cleanly onto current master. Desktop-focused but the prune logic lives in
commonMain.

Summary by CodeRabbit

  • Bug Fixes

    • Prevents temporary placeholder content from being permanently saved to the database
    • Removes stale download queue entries for chapters that no longer exist or are already complete
  • Improvements

    • Enhanced download queue management with better status synchronization and data validation
    • Smarter download detection to avoid unnecessary operations
    • Improved user messaging for download status and availability

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Lost in the diff? Review this PR in Change Stack to follow the change map from intent to exact ranges.

Review Change Stack

📝 Walkthrough

Walkthrough

This PR strengthens download queue integrity by detecting placeholder-string content corruption, validating the persistent queue on startup, reconciling legacy and current download progress tracking, and adding UI-level guards to prevent queuing already-downloaded chapters.

Changes

Download Queue Lifecycle and Placeholder Content Cleanup

Layer / File(s) Summary
Placeholder detection and stripping mechanism
data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt
Introduces a new List<Page>.stripDownloadedPlaceholder() extension function that detects when chapter content consists only of placeholder-marker Text pages and returns emptyList() to strip them; otherwise returns the original content unchanged.
Data persistence cleanup with placeholder stripping
data/src/commonMain/kotlin/ireader/data/chapter/ChapterRepositoryImpl.kt, data/src/commonMain/kotlin/ireader/data/repository/consolidated/ChapterRepositoryImpl.kt
Applies stripDownloadedPlaceholder() transformation to chapter.content in all database upsert paths: single insertChapter(), batch insertChapters(), and addAll() operations to prevent placeholder content from persisting to the database.
Download queue validation and pruning on initialization
domain/src/commonMain/kotlin/ireader/domain/services/download/DownloadManager.kt
Replaces direct queue restoration with validated pruning that drops entries for missing chapters and chapters with existing meaningful content, then re-persists the trimmed queue so disk state matches the pruned in-memory view.
Download progress state merging
domain/src/desktopMain/kotlin/ireader/domain/services/common/DesktopDownloadService.kt
Changes the progress bridge from full-map replacement to merge semantics: queue entries are upserted while preserving existing non-QUEUED legacy status/progress, and legacy progress unconditionally overwrites while retaining queue-sourced chapter/book names.
UI-level pre-download validation
presentation/src/commonMain/kotlin/ireader/presentation/ui/book/viewmodel/BookDetailViewModel.kt
Adds a pre-check in startDownloadService() to inspect chapter content length and show an "already downloaded" snackbar when all chapters have content, avoiding unnecessary queue entries and providing conditional success messaging based on chapters queued.

🎯 3 (Moderate) | ⏱️ ~25 minutes


🐰 A placeholder stripped, a queue refined,
Download streams pure, no corruption to find,
Progress now merged with legacy grace,
UI guards prevent a download's false race.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: pruning stale entries from the persisted download queue on restore, which is the primary objective and the focus of DownloadManager.init().
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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.

DownloadManager restores its queue from JSON on startup but never pruned
entries the legacy runDownloadService path had already fulfilled (that path
writes content to the DB and removes DB rows without telling DownloadManager).
Orphans accumulated and kept the "Downloading..." notification stuck at zero
progress. On restore we now cross-check each queued entry against the DB and
drop ones whose chapter no longer exists or already has content.

Includes the chapterMapper / ChapterRepository content-length helpers the prune
relies on and BookDetailViewModel download-state wiring.
@anishgoyal1108 anishgoyal1108 force-pushed the chore/desktop-download-resume branch from eff5255 to 71c8db0 Compare June 6, 2026 03:18
@anishgoyal1108 anishgoyal1108 marked this pull request as ready for review June 6, 2026 03:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
data/src/commonMain/kotlin/ireader/data/chapter/ChapterRepositoryImpl.kt (1)

142-146: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Notification uses wrong content check.

The notification checks chapter.content.isNotEmpty() instead of contentForInsert.isNotEmpty(). When incoming content is the placeholder marker, it will be stripped to empty but the original chapter.content remains non-empty, triggering a ContentFetched notification even though no new content was written (the SQL COALESCE preserves existing DB content).

🐛 Proposed fix
         // Notify observers that chapter content was updated
         // This allows the reader screen to reload content when chapters are downloaded
         // by the downloader or other background processes
-        if (chapterNotifier != null && chapter.content.isNotEmpty()) {
+        if (chapterNotifier != null && contentForInsert.isNotEmpty()) {
             chapterNotifier.tryNotifyChange(
                 ChapterNotifier.ChangeType.ContentFetched(chapter.id, chapter.bookId)
             )
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/src/commonMain/kotlin/ireader/data/chapter/ChapterRepositoryImpl.kt`
around lines 142 - 146, The notification currently checks
chapter.content.isNotEmpty() which can be non-empty even when no new content was
written; change the condition that triggers
chapterNotifier.tryNotifyChange(ChapterNotifier.ChangeType.ContentFetched(chapter.id,
chapter.bookId)) to test contentForInsert.isNotEmpty() instead so notifications
only fire when the incoming content actually contains non-placeholder data.
🧹 Nitpick comments (1)
data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt (1)

88-88: ⚡ Quick win

Use imported type alias for consistency.

The function signature uses the fully qualified type name ireader.core.source.model.Page despite importing Page on line 4. The rest of the file uses the short form (e.g., line 18).

♻️ Simplify type reference
-fun List<ireader.core.source.model.Page>.stripDownloadedPlaceholder(): List<ireader.core.source.model.Page> {
+fun List<Page>.stripDownloadedPlaceholder(): List<Page> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt` at line 88,
The extension function signature uses the fully-qualified type name instead of
the imported alias; update the signature of stripDownloadedPlaceholder (the
function declared as fun
List<ireader.core.source.model.Page>.stripDownloadedPlaceholder()) to use the
imported Page type (fun List<Page>.stripDownloadedPlaceholder()) so it matches
the rest of the file's short-form references and maintains consistency with
other usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt`:
- Around line 90-92: Replace the hardcoded marker string in the onlyPlaceholder
check with the shared constant (e.g., DOWNLOADED_CHAPTER_PLACEHOLDER) or a new
private constant used by both stripDownloadedPlaceholder and Chapter.isEmpty;
update the expression in chapterMapper.kt where onlyPlaceholder is computed (the
lambda checking page.text.contains(...)) to reference that constant instead of
the literal so both locations share the same marker.

---

Outside diff comments:
In `@data/src/commonMain/kotlin/ireader/data/chapter/ChapterRepositoryImpl.kt`:
- Around line 142-146: The notification currently checks
chapter.content.isNotEmpty() which can be non-empty even when no new content was
written; change the condition that triggers
chapterNotifier.tryNotifyChange(ChapterNotifier.ChangeType.ContentFetched(chapter.id,
chapter.bookId)) to test contentForInsert.isNotEmpty() instead so notifications
only fire when the incoming content actually contains non-placeholder data.

---

Nitpick comments:
In `@data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt`:
- Line 88: The extension function signature uses the fully-qualified type name
instead of the imported alias; update the signature of
stripDownloadedPlaceholder (the function declared as fun
List<ireader.core.source.model.Page>.stripDownloadedPlaceholder()) to use the
imported Page type (fun List<Page>.stripDownloadedPlaceholder()) so it matches
the rest of the file's short-form references and maintains consistency with
other usages.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67bb2527-301f-4638-ab87-2dd8e0bbd65e

📥 Commits

Reviewing files that changed from the base of the PR and between 1a6a203 and 71c8db0.

📒 Files selected for processing (6)
  • data/src/commonMain/kotlin/ireader/data/chapter/ChapterRepositoryImpl.kt
  • data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt
  • data/src/commonMain/kotlin/ireader/data/repository/consolidated/ChapterRepositoryImpl.kt
  • domain/src/commonMain/kotlin/ireader/domain/services/download/DownloadManager.kt
  • domain/src/desktopMain/kotlin/ireader/domain/services/common/DesktopDownloadService.kt
  • presentation/src/commonMain/kotlin/ireader/presentation/ui/book/viewmodel/BookDetailViewModel.kt

Comment on lines +90 to +92
val onlyPlaceholder = all { page ->
page is ireader.core.source.model.Text && page.text.contains("PLACEHOLDER_DO_NOT_DISPLAY_THIS_TEXT_TO_USER")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hardcoded substring creates maintenance risk.

The placeholder detection uses a hardcoded substring "PLACEHOLDER_DO_NOT_DISPLAY_THIS_TEXT_TO_USER" instead of referencing DOWNLOADED_CHAPTER_PLACEHOLDER or extracting the shared substring. The same substring appears in Chapter.isEmpty(), suggesting duplication. If the constant changes, both locations must be updated manually.

🔧 Proposed fix using the constant
     val onlyPlaceholder = all { page ->
-        page is ireader.core.source.model.Text && page.text.contains("PLACEHOLDER_DO_NOT_DISPLAY_THIS_TEXT_TO_USER")
+        page is ireader.core.source.model.Text && page.text.contains(DOWNLOADED_CHAPTER_PLACEHOLDER)
     }

Alternatively, extract the substring as a shared constant:

private const val PLACEHOLDER_MARKER = "PLACEHOLDER_DO_NOT_DISPLAY_THIS_TEXT_TO_USER"

then reference it in both stripDownloadedPlaceholder() and Chapter.isEmpty().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/src/commonMain/kotlin/ireader/data/chapter/chapterMapper.kt` around
lines 90 - 92, Replace the hardcoded marker string in the onlyPlaceholder check
with the shared constant (e.g., DOWNLOADED_CHAPTER_PLACEHOLDER) or a new private
constant used by both stripDownloadedPlaceholder and Chapter.isEmpty; update the
expression in chapterMapper.kt where onlyPlaceholder is computed (the lambda
checking page.text.contains(...)) to reference that constant instead of the
literal so both locations share the same marker.

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.

1 participant