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