Skip to content

fix(feed): abort the in-flight load-more when the feed filter changes - #110

Open
lopatnov wants to merge 3 commits into
mainfrom
refactor/maintain-feed-pagination-race
Open

fix(feed): abort the in-flight load-more when the feed filter changes#110
lopatnov wants to merge 3 commits into
mainfrom
refactor/maintain-feed-pagination-race

Conversation

@lopatnov

Copy link
Copy Markdown
Owner

Summary

  • useFeedPage's handleLoadMore() called loadFeed(cursor) with no AbortSignal (the parameter was optional and defaulted to undefined), while the filter-change effect used its own separate AbortController. If a user triggered "load more" and then changed the source/unread filter before the load-more response came back, there was no way to cancel the stale request. When it resolved late, it appended its (now-stale) items onto the list the filter change had already replaced, and overwrote nextCursor with a cursor belonging to the wrong filter. The finally block's if (!signal?.aborted) setLoading(false) was also unconditionally true for an unabortable call, so it could clear the replacement request's loading flag too.
  • Extracted the abort discipline useCursorPaginatedList already had correctly into a new shared hook, useLatestRequest.ts (tracks one "latest request" AbortController, aborting whatever's in flight when a new one starts). Both useFeedPage and useCursorPaginatedList now route through it, so the two hooks can't drift out of sync again. loadFeed's signal parameter is now required, not optional, so a future caller can't skip the discipline the way handleLoadMore did.

Found by architect during the periodic /maintain refactor-analysis pass (2026-08-15), off-signal from repo-scout's file-size/component-count report — a genuine SOLID/duplication read of the hooks, not a line-count trigger. Independently verified before merging: reproduced the bug against the pre-fix code (confirmed the new test fails there — nextCursor ends up as the stale cursor) and confirmed it passes against the fix.

Related issue

Part of the periodic /maintain cycle (2026-08-15) — step 3 (refactor analysis: real bug found along the way).

Type of change

  • Bug fix
  • Refactor / code quality
  • Documentation update (CHANGELOG)

Checklist

  • dotnet build --configuration Release passes with 0 errors (unaffected by this branch)
  • cd src/pressmark-web && npm run build passes with 0 TypeScript errors
  • New behavior is covered by tests — added FeedPage — load-more race condition, verified it fails against the pre-fix hook and passes against the fix
  • All UI strings use t('ns:key') — no strings touched

Notes for reviewers

No design ambiguity — useCursorPaginatedList already had the correct pattern (shared AbortController across loadMore and the reset effect); useFeedPage had drifted from it by hand-rolling its own version that forgot to wire load-more through the abort logic at all. The fix is a straight extraction of the already-correct pattern into a shared hook, applied uniformly to both.


Generated by Claude Code

claude added 3 commits August 15, 2026 06:44
useFeedPage hand-rolled its own cursor pagination instead of going through
useCursorPaginatedList, and never received the shared-AbortController fix that
7060fef applied to that hook. Its load-more path called loadFeed(cursor) with
no signal at all, so nothing could cancel it:

  - switching the source filter or the unread toggle while a load-more was in
    flight let the stale page resolve into appendItems(), splicing articles of
    the previous filter into the list that replaced them;
  - the same response overwrote nextCursor, so every subsequent page was pulled
    from the wrong filter's cursor;
  - its finally block ran `if (!signal?.aborted)` against an undefined signal,
    which is always falsy, so it cleared the loading flag the replacement
    request had just set — dropping the skeleton and re-arming load-more early.

Extract the "one request in flight, starting a new one aborts the old" rule
that useCursorPaginatedList already implemented correctly into useLatestRequest
and use it from both hooks, so the invariant cannot drift out of sync in one of
them again. loadFeed's signal is now required rather than optional, which is
what let an unabortable call be written in the first place.

No behaviour change for useCursorPaginatedList — same controller discipline,
just relocated.

Needs test coverage (follow-up tester pass): switching the unread filter and
the ?sub= source filter while a load-more is in flight must leave neither a
stale item nor a stale nextCursor in the feed store; FeedPage.test.tsx
currently mocks useIntersectionLoader out entirely, so the load-more path has
no coverage at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
Verified the test fails against the pre-fix useFeedPage.ts (nextCursor
gets overwritten with the stale load-more response's cursor), and passes
against the fix — confirms both that the bug was real and that the fix
in the previous commit resolves it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@lopatnov, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1acadc65-949f-4df1-8a4f-a6719a0a7c2b

📥 Commits

Reviewing files that changed from the base of the PR and between 76eae30 and 038b402.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/pressmark-web/src/hooks/useCursorPaginatedList.ts
  • src/pressmark-web/src/hooks/useFeedPage.ts
  • src/pressmark-web/src/hooks/useLatestRequest.ts
  • src/pressmark-web/src/pages/FeedPage.test.tsx

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.

@gitar-bot

gitar-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@sonarqubecloud

Copy link
Copy Markdown

@lopatnov
lopatnov marked this pull request as ready for review August 16, 2026 12:52
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

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