Skip to content

fix(cache): propagate deduplicated nested "use cache" metadata before writes settle (#3321) - #3333

Open
Divkix wants to merge 1 commit into
cloudflare:mainfrom
Divkix:issue-3321
Open

Divkix wants to merge 1 commit into
cloudflare:mainfrom
Divkix:issue-3321

Conversation

@Divkix

@Divkix Divkix commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #3321

Summary

  • Publish a "use cache" invocation's value, tags, lifetime, and root-param dependencies to the enclosing request as soon as collection completes; persistence is handed to the request lifecycle instead of being awaited on the value path.
  • Serve a repeat lookup for a key collected earlier in the same request from a request-scoped retained record until its handler write settles, instead of re-executing the cached function because the entry is not on disk yet.
  • Keep cross-request behavior unchanged: a later request still reads the handler and follows the root-param redirect marker to rebuild a root-param-specific key.

Cause

Two outer cache scopes that share an inner "use cache" call must inherit the inner scope's root-param dependencies while the inner cache-handler write is still pending. Vinext awaited that write inside the value path, so an outer scope either stalled behind a slow handler or re-executed the inner function and keyed its own entry without the root params the inner cache read — letting a later request with a different root-param value reuse the entry produced for another value.

Upstream fixed the same problem in vercel/next.js#98808 (Fix metadata propagation for deduplicated nested caches) by replacing the pending-metadata promise consumers awaited with a shared cache entry that forks at collection time. This change applies the same ordering to vinext: metadata leaves the inner scope at collection, and the write is deferred through waitUntil on Workers or the pending cache-work queue that action and route-handler boundaries drain. Where no lifecycle owns the work, the write is still awaited, so "a resolved value implies its write settled" continues to hold there. The retained record is dropped once the write settles.

Red/green proof

vp test run --project unit tests/use-cache-root-params.test.ts — with the fix removed the new test times out, because the render never finishes while the inner write is held; with the fix, 3 passed.

The test is ported from upstream test/e2e/app-dir/app-root-params-getters/use-cache.test.ts (the use-cache-dedup fixture): a delaying handler holds the English inner write until both outer entries reach set(). It asserts the render completes, the inner function executes once, both outer entries are stored under root-param-specific keys, and a later French request does not reuse the English entry.

Validation

  • vp run build, vp run check, vp run knip, bash -n scripts/*.sh, node scripts/ci-integration-shard.mjs --check --shard-total=10 — all pass.
  • vp test run (unit + integration): 12,249 passed. The remaining failures (action-runtime-security, dev-route-discovery ×2, pages-router) reproduce on the clean baseline in this environment (macOS /var vs /private/var path handling); parallel-run-only failures pass in isolation.
  • Playwright, all 30 CI projects: app-router 207/217/194, app-router-isr-prod 30 (including app-router-prod/use-cache.spec.ts), app-router-client-cache 22, cloudflare-dev 19, cloudflare-workers 86, pages-router 144, pages-router-prod 77, remaining projects green. @cloudflare/workers-response-store#test 51, @vinext/cloudflare#test 31.
  • Scaffold jobs reproduced locally: create-next-app (node init + dev server HTTP 200), create-next-app (Cloudflare init + vinext build + expected outputs), create-vinext-app (Cloudflare build).

… writes settle (cloudflare#3321)

An inner `"use cache"` invocation now publishes its value, tags, lifetime, and
root-param dependencies to the enclosing request as soon as collection
completes. Its handler write is handed to the request lifecycle (`waitUntil`
plus the pending cache-work queue that action and route-handler boundaries
drain) instead of being awaited on the value path, and a repeat lookup for the
same key is served from a request-scoped retained record until that write
settles.

Two outer cache scopes that share an inner cache therefore inherit the inner
metadata while persistence is still in flight, instead of re-executing the
inner function and keying themselves without the root params it read. The
retained record is dropped once the write settles, and cross-request reads are
unchanged: a later request still reads the handler and follows the root-param
redirect marker to rebuild a root-param-specific key, so an entry produced for
one root-param value is never reused for another.

Upstream: vercel/next.js#98808.
@pkg-pr-new

pkg-pr-new Bot commented Sep 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vinext/cloudflare

npm i https://pkg.pr.new/@vinext/cloudflare@3333

create-vinext-app

npm i https://pkg.pr.new/create-vinext-app@3333

@vinext/types

npm i https://pkg.pr.new/@vinext/types@3333

vinext

npm i https://pkg.pr.new/vinext@3333

@cloudflare/workers-response-store

npm i https://pkg.pr.new/@cloudflare/workers-response-store@3333

commit: f986e65

@github-actions

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared f986e65 against base e97de29 using alternating same-runner rounds. Next.js was unchanged and skipped.

1 improved · 0 regressed · 5 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 142.2 KB 142.2 KB ⚫ -0.0%
Client entry size (gzip) vinext 129.6 KB 129.6 KB ⚫ -0.0%
Dev server cold start vinext 3.06 s 3.02 s 🟢 -1.6%
Production build time vinext 3.33 s 3.30 s ⚫ -0.8%
RSC entry closure size (gzip) vinext 121.0 KB 120.8 KB ⚫ -0.2%
Server bundle size (gzip) vinext 218.3 KB 218.0 KB ⚫ -0.1%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

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.

Cache Components: propagate deduplicated nested cache metadata before cache-handler writes settle

1 participant