13 Commits

Author SHA1 Message Date
Nathan Nguyen f45fce00d5 fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings

Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses.

Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules.

Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known.

* fix(isr): address cache life review follow-ups

* chore: address latest review follow-ups

* ci: pin vite-plus setup version

* Revert "ci: pin vite-plus setup version"

This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1.

* fix: avoid blocking ISR page streams on cache metadata

* fix: preserve headers for speculative cacheLife probes

* fix: preserve prerender cacheLife metadata

* fix: preserve legacy ISR cache metadata behavior

* fix: preserve prerender seed revalidate context

* fix: harden app page cache policy metadata

* fix: resolve app router prerender conflict
2026-05-02 19:54:25 +01:00
Nathan Nguyen 598782e7b5 Make revalidatePath expire route-scoped fetch cache reads (#917)
Fetch cache entries without explicit next.tags could survive revalidatePath(). The page or route cache entry was invalidated, but the regenerated render could still read the old fetch response and reproduce stale data.

The missing boundary was Next.js soft tag semantics: path-derived implicit tags are read-time context for fetch cache lookups, not durable tags stored on the fetch entry.

Thread route-derived soft tags through App Router renders, pass them to fetch cache reads, and teach the memory and KV handlers to treat revalidated soft tags as cache misses without deleting shared fetch entries.

Regression coverage verifies revalidatePath invalidates untagged fetch cache reads and that KV soft-tag misses do not delete the shared entry.
2026-04-27 20:00:38 +01:00
Stephen Zhou 25f6e0fc87 chore: enable typeAware and typeCheck, use vp check (#551)
* chore: enable typeAware and typeCheck, use vp check

* Build

* Try cache false

* Revert "Try cache false"

This reverts commit 5f76ed02f1.

* Update

* workaround for vp check

* Try no workaround

* Fix check

* Update to 0.1.12

* denyWarnings
2026-03-16 11:00:47 -05:00
Nathan Nguyen 39791bf3cc feat: implement revalidateByPathPrefix on KVCacheHandler (#552)
* feat: implement revalidateByPathPrefix on KVCacheHandler

Uses KV list metadata to discover entry tags without extra get() calls.
set() now stores tags in KV metadata, and revalidateByPathPrefix reads
them back via kv.list() — making prefix invalidation O(list_pages)
instead of O(entries × get).

Entries written before metadata support are gracefully skipped.

* fix: guard against KV metadata 1024-byte limit

Cloudflare KV limits metadata to 1024 bytes per key. If an entry's
tags exceed this budget when JSON-serialized, omit metadata entirely
and fall back gracefully — prefix invalidation skips entries without
metadata, and exact-path invalidation via revalidateTag still works.

* test: add regression test for KV metadata 1024-byte overflow
2026-03-16 09:23:45 +00:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00
Nathan Nguyen 8d10bc64fd fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries (#503)
* fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries

MemoryCacheHandler.set had an inconsistent guard on data.revalidate:
the ctx path checked `revalidate > 0` but the data path only checked
`typeof data.revalidate === "number"`, allowing revalidate:0 to set
revalidateAt = Date.now() — making entries immediately stale.

This was inconsistent with KVCacheHandler which already had the > 0
guard on both paths. The two handlers would produce different staleness
behavior for the same input: Memory → immediately stale, KV → no TTL.

Add the missing `> 0` guard to align both code paths and both handlers.

* fix: revalidate:0 should skip cache storage entirely

Both MemoryCacheHandler and KVCacheHandler had inconsistent handling of
revalidate:0. In Next.js, revalidate:0 means "don't cache." But the
handlers would still store entries — Memory made them immediately stale
(data path lacked > 0 guard), KV cached them forever (both paths
rejected 0, so revalidateAt=null meant no expiry).

Resolve effective revalidate from both ctx and data paths (data
overrides ctx), and early-return without storing when it's 0. Both
handlers now use identical logic.

Fix existing test data that incidentally used data.revalidate:0 in
tests meant to exercise stale-while-revalidate and tag invalidation.
2026-03-12 17:34:12 +00:00
Divanshu Chauhan (divkix) 1b4a796a3f perf: startup and cache micro-optimizations (#436)
* perf: cache startup filesystem scans, optimize base64 and path normalization

- Cache hasMdxFiles() result per root directory to avoid redundant
  recursive filesystem walks when config() fires per Vite environment
- Cache resolvePostcssStringPlugins() result per project root to skip
  repeated existsSync checks across 17 PostCSS config file candidates
- Replace byte-by-byte base64 encode/decode in KV cache handler with
  Buffer APIs for significantly faster serialization
- Pass pre-normalized URL to nodeToWebRequest() in App Router prod
  server to eliminate redundant path normalization in the RSC handler

* fix: validate base64 structural length and improve test spy cleanup

Add length % 4 check to base64ToArrayBuffer to reject structurally
invalid base64 strings that pass the character-set regex but produce
empty/truncated buffers via Buffer.from(). Replace manual mockRestore()
calls with afterEach(vi.restoreAllMocks) to prevent spy leaks on
assertion failure.

* fix: address bonk review comments — composite mdx cache key, postcss comment order, hoist test import

* fix: cache Promise in resolvePostcssStringPlugins to prevent concurrent scan race; add POST urlOverride test

* fmt

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 16:22:58 +00:00
Divanshu Chauhan (divkix) 769a98ac85 perf(kv): local tag cache to reduce KV round-trips on cache hit (#433)
* perf(kv): add local in-memory tag cache to reduce KV round-trips

When KVCacheHandler.get() validates tags, it was issuing one kv.get()
per tag in parallel. For entries with N tags, that's N KV round-trips
per cache hit. Same pattern in revalidateTag() — N parallel PUTs.

Add a local Map<string, { timestamp, fetchedAt }> with a 5-second TTL
that caches tag invalidation timestamps. Within the TTL window, tag
checks are served from memory with zero I/O. After TTL expiry, the
next request re-fetches from KV.

Key behaviors:
- revalidateTag() updates the local cache immediately so invalidations
  are reflected without waiting for TTL expiry
- resetRequestCache() clears the local cache for per-request isolation
- NaN tag timestamps are cached and correctly treated as invalidation
- Only uncached/expired tags trigger KV reads (partial cache hits work)

* fix(test): guard fake timers with try/finally to prevent leaks

Wrap vi.useFakeTimers() in try/finally so vi.useRealTimers() runs even
if an assertion fails mid-test, preventing timer leaks into subsequent
tests.

* fix(kv): address bonk review comments on local tag cache

- Fix early return bug: split KV tag fetch loop into populate-cache pass
  then invalidation-check pass, so already-fetched tag results are always
  cached even when an earlier tag triggers an early return
- Fix unbounded _tagCache growth: delete stale entries when encountered
  during the TTL check (cheap eviction instead of accumulation forever)
- Make tag cache TTL configurable via tagCacheTtlMs constructor option
  (default 5000ms); eliminates fake timers in TTL test
- Clarify resetRequestCache() docstring: it is not called per-request by
  vinext, it is an opt-in escape hatch for callers that need explicit isolation
- Add resetRequestCache() test: verifies tags are re-fetched from KV after
  calling resetRequestCache()

* refactor(kv): reuse cached timestamps in invalidation check loop

The second pass over uncachedTags was re-calling Number(tagTime) and
re-reading tagResults[i]. Now it reads directly from _tagCache (which was
just populated in the first pass), eliminating the duplicate number
conversion and making the intent clearer.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 10:59:25 +00:00
Jared Stowell 1551e4406a fix: App Router ISR invalidation parity for fetch and path tags (#445)
* Fix App Router ISR tag parity

* Fix KV tag persistence

* Fix KV cache tag handling
2026-03-11 08:17:50 +00:00
James Anderson aa878d55ac fix(isr): KVCacheHandler.set() now awaits KV put to prevent perpetual STALE (#425)
* fix(isr): KVCacheHandler.set() now awaits KV put to prevent perpetual STALE

Background ISR regen was calling ctx.waitUntil(renderFn()) where renderFn()
resolved before the KV put network operation completed. set() returned
Promise.resolve() immediately, so await __isrSet() in the regen resolved early,
the waitUntil expired, and the Workers runtime killed the in-flight KV write —
leaving the cache entry perpetually STALE on every request after the TTL.

Fix: replace the fire-and-forget _putInBackground() with _put() which returns
the actual kv.put() promise. set() now returns that promise, so await __isrSet()
only resolves after the write is fully persisted. ctx.waitUntil() is also still
registered as belt-and-suspenders.

Adds three regression tests covering the STALE → regen → HIT lifecycle with a
delayed KV put mock to prove the set() promise is not an immediately-resolved stub.

* fix: remove unused variable flagged by lint
2026-03-10 23:11:24 +00:00
James Anderson 7ba1ab5ccf fix: register background KV ops and ISR regen with ctx.waitUntil on Workers (#391)
* fix: register background KV ops and ISR regen with ctx.waitUntil on Workers

On Cloudflare Workers, any async work started after a Response is returned
is killed immediately. This caused three silent failures:

1. ISR background regeneration (stale-while-revalidate) was never
   registered with ctx.waitUntil, so every revalidation on Workers was
   silently dropped.

2. KV deletes for corrupt/invalid/tag-invalidated cache entries were
   awaited on the critical path, blocking the response for pure cleanup work.

3. KV writes in KVCacheHandler.set() were awaited on the critical path;
   the client already has the rendered content so the write can happen
   after the response is sent.

Fixes:
- Thread ExecutionContext through triggerBackgroundRegeneration and call
  ctx.waitUntil(regenPromise) when ctx is present (isr-cache.ts and the
  inlined copy in pages-server-entry.ts).
- Add _deleteInBackground / _putInBackground helpers to KVCacheHandler
  that use ctx.waitUntil when ctx is present and fall back to
  fire-and-forget on Node.js.
- Convert all three delete callsites (corrupt JSON, invalid shape,
  tag-invalidation) and the set() KV write to use the new helpers.
- All changes are backward-compatible: ctx is optional everywhere;
  Node.js dev/prod behaviour is unchanged.

* regen snaps
2026-03-09 23:25:57 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00