Fix unread counter stuck after reading all articles #2

Merged
mathias merged 1 commits from hotfix/unread-counter-stuck into master 2026-09-11 17:22:29 +02:00
Owner

markRead() fired its PUT fire-and-forget with no retry and silently
swallowed failures. If one PUT among a burst (paging through article
view fires one per page turn) failed or lost a race with the app's own
background sync()->fetchData() refetch, that article's read-mark was
permanently lost: the header's unreadCount (derived live from
allItems) would show it again mid-session, and a reload reproduced it
identically since the DB genuinely still had it unread.

  • markRead() now retries transient failures (no response / 5xx / 429)
    with backoff and a request timeout, skips retrying non-retryable
    4xx, and surfaces a message on final failure instead of only
    console.log.
  • A pendingReadCounts refcount (keyed by article id) tracks in-flight/
    retrying marks; fetchData() excludes those ids - snapshotting before
    its GET is dispatched, not just once the response lands - so a
    concurrent refetch can't resurrect an article whose read-confirmation
    is still in flight or unsettled at request time.
  • markAllRead() now reports a partial-failure summary instead of
    unconditionally claiming full success when some marks fail.

Extends vue/src/composables/tests/useFeeds.spec.js with coverage
for retry/backoff, non-retryable errors, the dispatch-before-response
race, overlapping concurrent marks for the same id, and markAllRead's
partial-failure message.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01CVw1XNQjodp4B7qiZqjtDE

markRead() fired its PUT fire-and-forget with no retry and silently swallowed failures. If one PUT among a burst (paging through article view fires one per page turn) failed or lost a race with the app's own background sync()->fetchData() refetch, that article's read-mark was permanently lost: the header's unreadCount (derived live from allItems) would show it again mid-session, and a reload reproduced it identically since the DB genuinely still had it unread. - markRead() now retries transient failures (no response / 5xx / 429) with backoff and a request timeout, skips retrying non-retryable 4xx, and surfaces a message on final failure instead of only console.log. - A pendingReadCounts refcount (keyed by article id) tracks in-flight/ retrying marks; fetchData() excludes those ids - snapshotting before its GET is dispatched, not just once the response lands - so a concurrent refetch can't resurrect an article whose read-confirmation is still in flight or unsettled at request time. - markAllRead() now reports a partial-failure summary instead of unconditionally claiming full success when some marks fail. Extends vue/src/composables/__tests__/useFeeds.spec.js with coverage for retry/backoff, non-retryable errors, the dispatch-before-response race, overlapping concurrent marks for the same id, and markAllRead's partial-failure message. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVw1XNQjodp4B7qiZqjtDE
mathias added 1 commit 2026-09-11 17:22:18 +02:00
markRead() fired its PUT fire-and-forget with no retry and silently
swallowed failures. If one PUT among a burst (paging through article
view fires one per page turn) failed or lost a race with the app's own
background sync()->fetchData() refetch, that article's read-mark was
permanently lost: the header's unreadCount (derived live from
allItems) would show it again mid-session, and a reload reproduced it
identically since the DB genuinely still had it unread.

- markRead() now retries transient failures (no response / 5xx / 429)
  with backoff and a request timeout, skips retrying non-retryable
  4xx, and surfaces a message on final failure instead of only
  console.log.
- A pendingReadCounts refcount (keyed by article id) tracks in-flight/
  retrying marks; fetchData() excludes those ids - snapshotting before
  its GET is dispatched, not just once the response lands - so a
  concurrent refetch can't resurrect an article whose read-confirmation
  is still in flight or unsettled at request time.
- markAllRead() now reports a partial-failure summary instead of
  unconditionally claiming full success when some marks fail.

Extends vue/src/composables/__tests__/useFeeds.spec.js with coverage
for retry/backoff, non-retryable errors, the dispatch-before-response
race, overlapping concurrent marks for the same id, and markAllRead's
partial-failure message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVw1XNQjodp4B7qiZqjtDE
mathias merged commit e2df60d112 into master 2026-09-11 17:22:28 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/rss-reader#2