Skip to content

fix: invalidate shelf cache when book status changes#405

Merged
masonfox merged 4 commits intodevelopfrom
fix/shelf-cache-invalidation
Mar 19, 2026
Merged

fix: invalidate shelf cache when book status changes#405
masonfox merged 4 commits intodevelopfrom
fix/shelf-cache-invalidation

Conversation

@masonfox
Copy link
Copy Markdown
Owner

Summary

Fixes a cache invalidation bug where changing a book's status on /books/:id doesn't invalidate the cache for /shelves/:id, causing shelves to show stale book status for up to 30 seconds.

Problem

When a user changes a book's status on the book detail page, the invalidateBookQueries() helper function invalidates several React Query caches (book, sessions, progress, dashboard, library, read-next), but does not invalidate shelf queries. This means:

  • Shelf pages continue showing the old status until the 30-second staleTime expires
  • Users see inconsistent data between the book detail page and shelf pages
  • Manual refresh or waiting 30s is required to see updated status on shelves

Solution

Modified invalidateBookQueries() in hooks/useBookStatus.ts to also invalidate shelf caches:

Cache-Based Approach (Option A)

  1. Check cache first: Query React Query cache for the book's shelves using queryKeys.book.shelves(bookId)
  2. Surgical invalidation: If cached shelves exist, invalidate only those specific shelf queries using queryKeys.shelf.byId(shelfId)
  3. Nuclear fallback: If no cached shelves, invalidate all shelf queries using queryKeys.shelf.base() to ensure correctness

This approach balances performance (surgical when possible) with correctness (nuclear when cache unavailable).

Changes

Core Changes

  • hooks/useBookStatus.ts (lines 42-74):

    • Enhanced invalidateBookQueries() to check cached book shelves
    • Invalidate affected shelf queries (surgical) or all shelves (nuclear)
    • Added JSDoc comments documenting the shelf invalidation logic
  • app/api/books/[id]/status/route.ts (line 69):

    • Removed misleading comment about SessionService handling cache invalidation

Testing

  • ✅ All 4007 existing tests pass
  • ✅ No breaking changes to function signature
  • ✅ Compatible with all existing uses of invalidateBookQueries()
  • ✅ Server running and responsive during testing

Implementation Pattern

This follows the same pattern already used in app/books/[id]/page.tsx (lines 245-261) for the updateShelves() function, which demonstrates proper shelf invalidation when books are added/removed from shelves.

Related Files

  • Investigation details: docs/plans/fix-shelf-cache-invalidation.md
  • Query keys definition: lib/query-keys.ts (lines 162-172)
  • Book detail page: app/books/[id]/page.tsx (lines 229-242 for shelf caching)

When a book's status was changed on /books/:id, the shelf pages at
/shelves/:id would show stale status for up to 30 seconds until the
React Query cache expired. This was because invalidateBookQueries()
invalidated book, session, progress, dashboard, and library queries,
but did not invalidate shelf queries.

Changes:
- Modified invalidateBookQueries() to check React Query cache for the
  book's shelves and invalidate those shelf caches
- Uses cache-based approach: if shelves are cached, invalidates only
  affected shelves (surgical); otherwise invalidates all shelves
  (nuclear) to ensure correctness
- Removed misleading comment in status route about SessionService
  handling cache invalidation

All 4007 tests pass.
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #405      +/-   ##
===========================================
+ Coverage    78.62%   78.66%   +0.04%     
===========================================
  Files          167      167              
  Lines         7540     7556      +16     
  Branches      1846     1850       +4     
===========================================
+ Hits          5928     5944      +16     
  Misses        1127     1127              
  Partials       485      485              
Flag Coverage Δ
unittests 78.66% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
app/api/books/[id]/status/route.ts 92.10% <ø> (ø)
hooks/useBookStatus.ts 85.53% <100.00%> (+1.61%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…lidation

- Replace type assertion with runtime type guard for safer cache validation
- Add debug logging for surgical vs nuclear invalidation paths
- Add 6 comprehensive tests covering all cache invalidation scenarios
- All 4013 tests passing

Addresses @review feedback: test coverage, type safety, and observability
Repository owner deleted a comment from Copilot AI Mar 19, 2026
@masonfox masonfox requested a review from Copilot March 19, 2026 13:53

This comment was marked as outdated.

- Add logger for structured logging (replace console.debug)
- Fix empty shelves logic to treat [] as no-op
- Update test assertions for better verification
- Update empty shelves test expectation

Fixes suggested by GitHub Copilot code review.
@masonfox masonfox requested a review from Copilot March 19, 2026 14:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a TanStack Query cache invalidation gap so that changing a book’s status on /books/:id also updates any affected /shelves/:id pages immediately, instead of waiting for staleTime to expire.

Changes:

  • Extend invalidateBookQueries() to invalidate shelf queries for shelves containing the updated book (surgical when cache is present; fallback to invalidating all shelves).
  • Add unit tests covering the new shelf invalidation behaviors.
  • Remove a misleading cache invalidation comment from the book status API route.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
hooks/useBookStatus.ts Adds shelf-aware cache invalidation logic inside invalidateBookQueries().
app/api/books/[id]/status/route.ts Removes an inaccurate comment about where cache invalidation occurs.
__tests__/hooks/useBookStatus.test.ts Adds tests validating both surgical and fallback shelf invalidation paths.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +398 to +402
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
Comment on lines +47 to +62
export function invalidateBookQueries(queryClient: any, bookId: string): void {
queryClient.invalidateQueries({ queryKey: queryKeys.book.detail(parseInt(bookId)) });
queryClient.invalidateQueries({ queryKey: queryKeys.sessions.byBook(parseInt(bookId)) });
queryClient.invalidateQueries({ queryKey: queryKeys.progress.byBook(parseInt(bookId)) });
const numericBookId = parseInt(bookId);

// Invalidate book-related queries
queryClient.invalidateQueries({ queryKey: queryKeys.book.detail(numericBookId) });
queryClient.invalidateQueries({ queryKey: queryKeys.sessions.byBook(numericBookId) });
queryClient.invalidateQueries({ queryKey: queryKeys.progress.byBook(numericBookId) });
queryClient.invalidateQueries({ queryKey: queryKeys.dashboard.all() });
queryClient.invalidateQueries({ queryKey: queryKeys.library.books() });
queryClient.invalidateQueries({ queryKey: queryKeys.readNext.base() });

// Invalidate shelves containing this book
// Try to get shelves from cache; if available, invalidate only those shelves (surgical)
// If not cached, invalidate all shelves (nuclear) to ensure correctness
const bookShelvesKey = queryKeys.book.shelves(numericBookId);
const cachedShelves = queryClient.getQueryData(bookShelvesKey);
- Use createTestQueryClient() for consistent test configuration
- Type invalidateBookQueries queryClient parameter as QueryClient

Addresses Copilot suggestions:
1. Test configuration consistency - use helper that disables caching
2. Type safety - replace 'any' with proper QueryClient type

Co-authored-by: GitHub Copilot <copilot@github.com>
@masonfox masonfox requested a review from Copilot March 19, 2026 14:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes stale shelf data after changing a book’s status by extending the existing React Query invalidation helper to also invalidate relevant shelf queries, ensuring /shelves/:id reflects status updates immediately.

Changes:

  • Extend invalidateBookQueries() to invalidate shelf caches (surgical when book-shelf membership is cached; otherwise fallback to invalidating all shelves).
  • Remove a misleading cache invalidation comment from the status API route.
  • Add unit tests covering the new shelf invalidation behavior (including fallback paths).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
hooks/useBookStatus.ts Enhances invalidateBookQueries() to invalidate shelf query caches based on cached book→shelves membership, with a fallback to invalidating all shelves.
app/api/books/[id]/status/route.ts Removes a misleading comment about where cache invalidation occurs.
tests/hooks/useBookStatus.test.ts Adds test coverage for invalidateBookQueries() including shelf invalidation scenarios.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +88 to +93
// Cache unavailable or invalid - invalidate all shelves to be safe
logger.debug(
{ bookId: numericBookId, cacheState: cachedShelves === null ? 'null' : 'invalid' },
'Nuclear shelf invalidation (cache unavailable)'
);
queryClient.invalidateQueries({ queryKey: queryKeys.shelf.base() });
@masonfox masonfox merged commit 7b3cf28 into develop Mar 19, 2026
8 checks passed
@masonfox masonfox deleted the fix/shelf-cache-invalidation branch March 19, 2026 14:35
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