[PR #498] [MERGED] fix: dedup stale background refetches in fetch cache #620

Closed
opened 2026-05-06 13:09:07 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/498
Author: @NathanDrake2406
Created: 3/12/2026
Status: Merged
Merged: 3/12/2026
Merged by: @james-elicx

Base: mainHead: fix/fetch-cache-stale-dedup


📝 Commits (7)

  • d4be71d fix: dedup stale background refetches in fetch cache
  • f6d0063 test: add fetch cache stale dedup concurrency tests
  • e57c05e Update packages/vinext/src/shims/fetch-cache.ts
  • 2faabaa fix: repair broken setTimeout structure from review suggestion
  • 8c7baaf fix: wrap fake timer tests in try/finally for safety
  • 5caf79e fix: guard background revalidation against caching error responses
  • b4840c0 fix: tighten cache guard to status === 200

📊 Changes

2 files changed (+423 additions, -39 deletions)

View changed files

📝 packages/vinext/src/shims/fetch-cache.ts (+94 -37)
📝 tests/fetch-cache.test.ts (+329 -2)

📄 Description

Summary

  • Every concurrent request hitting a stale fetch cache entry was independently calling originalFetch(), creating a thundering herd on popular endpoints
  • Added a per-cache-key dedup map (pendingRefetches) shared across RSC/SSR environments via Symbol.for(), matching the existing ISR regeneration dedup pattern in isr-cache.ts
  • Added a 60s timeout safety net that force-cleans dedup entries when upstream fetches hang, with identity checks in both .finally() and setTimeout to prevent slot-ownership races
  • Improved error log to include URL and truncated cache key for production observability

Test plan

  • Concurrent stale hits trigger only one background refetch (deferred promise gate ensures deterministic timing)
  • Completed refetches allow new ones (lifecycle test)
  • Failed refetches clean up dedup entry, allowing retry (error-path test)
  • Hung fetches are force-cleaned after 60s timeout (vi.useFakeTimers)
  • Late-settling hung fetch does not evict a replacement refetch's slot (race test)
  • _resetPendingRefetches() called in beforeEach for test isolation
  • CI: full Vitest suite + Playwright E2E

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/498 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 3/12/2026 **Status:** ✅ Merged **Merged:** 3/12/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/fetch-cache-stale-dedup` --- ### 📝 Commits (7) - [`d4be71d`](https://github.com/cloudflare/vinext/commit/d4be71d1a34501f6caf226592111857ed39faff2) fix: dedup stale background refetches in fetch cache - [`f6d0063`](https://github.com/cloudflare/vinext/commit/f6d006340ec18e29f5b4e4f9511b74ae86a97244) test: add fetch cache stale dedup concurrency tests - [`e57c05e`](https://github.com/cloudflare/vinext/commit/e57c05e51085c7c81f53ce1f43de5da8a1f15834) Update packages/vinext/src/shims/fetch-cache.ts - [`2faabaa`](https://github.com/cloudflare/vinext/commit/2faabaa6639368681ba85b6b7bd1ba37c77e8fe7) fix: repair broken setTimeout structure from review suggestion - [`8c7baaf`](https://github.com/cloudflare/vinext/commit/8c7baaf6929412dc27ba2b8b8ad9bda56b55f21b) fix: wrap fake timer tests in try/finally for safety - [`5caf79e`](https://github.com/cloudflare/vinext/commit/5caf79ee62e94fb11b4ed15256b2e90161f5f571) fix: guard background revalidation against caching error responses - [`b4840c0`](https://github.com/cloudflare/vinext/commit/b4840c00f152e245942e4ed9373bce80ff6627d8) fix: tighten cache guard to status === 200 ### 📊 Changes **2 files changed** (+423 additions, -39 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/shims/fetch-cache.ts` (+94 -37) 📝 `tests/fetch-cache.test.ts` (+329 -2) </details> ### 📄 Description ## Summary - Every concurrent request hitting a stale fetch cache entry was independently calling `originalFetch()`, creating a thundering herd on popular endpoints - Added a per-cache-key dedup map (`pendingRefetches`) shared across RSC/SSR environments via `Symbol.for()`, matching the existing ISR regeneration dedup pattern in `isr-cache.ts` - Added a 60s timeout safety net that force-cleans dedup entries when upstream fetches hang, with identity checks in both `.finally()` and `setTimeout` to prevent slot-ownership races - Improved error log to include URL and truncated cache key for production observability ## Test plan - [x] Concurrent stale hits trigger only one background refetch (deferred promise gate ensures deterministic timing) - [x] Completed refetches allow new ones (lifecycle test) - [x] Failed refetches clean up dedup entry, allowing retry (error-path test) - [x] Hung fetches are force-cleaned after 60s timeout (`vi.useFakeTimers`) - [x] Late-settling hung fetch does not evict a replacement refetch's slot (race test) - [x] `_resetPendingRefetches()` called in `beforeEach` for test isolation - [ ] CI: full Vitest suite + Playwright E2E --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:09:07 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#620
No description provided.