feat: populate Workers KV with pre-rendered pages at deploy time#670
feat: populate Workers KV with pre-rendered pages at deploy time#670NathanDrake2406 wants to merge 7 commits intocloudflare:mainfrom
Conversation
Add populate-kv.ts with cache key construction, tag generation, entry serialization, bulk upload, and end-to-end populateKV orchestrator. Key format matches runtime __isrCacheKey exactly: [appPrefix:]cache:app:<buildId>:<pathname>:<suffix> Produces two entries per route (:html + :rsc) with full tag hierarchy for revalidatePath() support. Uploads via Cloudflare REST bulk API with count-based (10k) and byte-based (95MB) batching. 38 tests covering key parity, tag parity, entry shape, batching, and end-to-end flow with temp directory fixtures.
commit: |
Add step 6c between prerender/TPR and wrangler deploy. Automatically populates KV when CLOUDFLARE_API_TOKEN + VINEXT_CACHE KV namespace + prerender manifest all exist. Silent skip when prerequisites missing. New CLI flags: --no-populate-kv Disable automatic KV population --app-prefix KVCacheHandler appPrefix for key construction Export resolveAccountId from tpr.ts for shared use.
Replace TPR's inline uploadToKV with shared buildRouteEntries and uploadBulkToKV from populate-kv.ts. This fixes three TPR bugs: - Key format: was cache:<path>, now cache:app:<buildId>:<path>:html - TTL: was 10x revalidate clamped, now fixed 30-day (matching runtime) - Tags: was empty [], now full hierarchy for revalidatePath() support BuildId is resolved from vinext-prerender.json when available.
Replace `as string` casts with type-safe parseFetchBody helper. Reorder imports: builtins → framework → project modules.
- Only seed routes with router: "app" — Pages Router uses different keys (pages:...) and value shape (kind: "PAGES"), so seeding them as APP_PAGE entries would be unreachable at runtime. - Routes without a router field are skipped defensively (pre-cloudflare#653 manifests lack this field, but also lack buildId so populateKV would already skip). - Remove --app-prefix CLI flag — it writes prefixed keys without wiring the same prefix into the generated runtime, creating silently unreadable cache entries. Keep appPrefix on the programmatic PopulateKVOptions for advanced users who configure both sides manually.
32e6c66 to
68f3762
Compare
| // Step 6c: Populate KV with pre-rendered pages | ||
| if (!options.noPopulateKv) { | ||
| const apiToken = process.env.CLOUDFLARE_API_TOKEN; | ||
| const wranglerConfig = parseWranglerConfig(root); | ||
| const kvNamespaceId = wranglerConfig?.kvNamespaceId; | ||
|
|
||
| if (apiToken && kvNamespaceId) { | ||
| const accountId = wranglerConfig?.accountId ?? (await resolveAccountId(apiToken)); | ||
|
|
||
| if (accountId) { | ||
| const kvResult = await populateKV({ | ||
| root, | ||
| accountId, | ||
| namespaceId: kvNamespaceId, | ||
| apiToken, | ||
| }); | ||
|
|
||
| if (kvResult.skipped) { | ||
| console.log(`\n KV populate: Skipped (${kvResult.skipped})`); | ||
| } else if (kvResult.entriesUploaded > 0) { | ||
| console.log( | ||
| `\n KV populate: ${kvResult.routesProcessed} routes → ${kvResult.entriesUploaded} entries (${(kvResult.durationMs / 1000).toFixed(1)}s)`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
I think we need to better think about the API for this before implementing it.
There shouldn't be an implicit assumption that KV is the cache handler people use.
I have been thinking about this over the weekend - we need to ensure the solution we arrive at will scale to any cache handler rather than being KV-specific.
I'm not still not 100% on what that should look like. Some things I was thinking about at the weekend were having a static property on the cache handler itself that can define a method for build/deploy-time bulk-population, and then provide the handler using the next.config.js cacheHandler option.
That could work, but I think it might have an issue with bundling dependencies that we don't want bundled if it were to use wrangler for the population logic for example.
Lets continue a conversation about how this can be implemented in a way that scales to any cache handler on the issue before iterating on impl
There was a problem hiding this comment.
Sorry for zoomin! I'll refactor it out of deploy and turn this into a KV adapter instead. Do you think the foundation part is good enough tho?
There was a problem hiding this comment.
Yeah i think a config is best here. I'll get back to u w the details. Need to nerd out a bit
There was a problem hiding this comment.
Lets discuss on the issue what good looks like for a long-term solution 👍
There was a problem hiding this comment.
Lets discuss on the issue what good looks like for a long-term solution 👍
instead of a static method, what about a named export? It feels cleaner and more modular, less coupled.
I can refactor this PR to use the seeding core and the KV part to a KV adapter
There was a problem hiding this comment.
As the decision we make will have to be supported for a long time ideally without needing breaking changes, it would probably be good for us to come up with a few different options that we can weigh up pros/cons for and hopefully get some engagement from others in the community on, with a caveat that whatever approach we take should still be fully compatible with the cache handler Next.js config option.
There was a problem hiding this comment.
Yeah I think you should open an issue and pin it, maybe a discussion?
There was a problem hiding this comment.
There's a placeholder issue for remote cache population - we can use that one perhaps?
Summary
Closes #562. Depends on #653 — requires
buildId,trailingSlash, androuterfields in the prerender manifest. Should be merged after #653.cloudflare/populate-kv.ts— reads prerender manifest + HTML/RSC files, constructs cache entries matching the runtime format exactly, uploads via Cloudflare REST bulk APICLOUDFLARE_API_TOKEN+VINEXT_CACHEKV namespace + prerender manifest all exist.--no-populate-kvto opt out.uploadToKV(80 lines) with shared helpers, fixing three bugs: wrong key format (cache:<path>→cache:app:<buildId>:<path>:html), wrong TTL (10x revalidate → fixed 30-day), missing tags (empty[]→ full hierarchy forrevalidatePath())Key format parity
Deploy-time keys match the runtime
__isrCacheKeyinapp-rsc-entry.tsexactly:Two entries per route (
:html+:rsc) with full tag hierarchy (_N_T_/layout, intermediate layouts, leaf page tag) sorevalidatePath()works on seeded entries.Design decisions
pages:...) and value shape (kind: "PAGES"). Routes withoutrouter: "app"are skipped.KVCacheHandler.set()default--app-prefixflag — removed after review; it would write prefixed keys without wiring the prefix into the generated runtime, creating silently unreadable entries.appPrefixremains on the programmaticPopulateKVOptionsfor advanced users who configure both sides.Test plan
vp checkpasses (0 warnings, 0 errors)CLOUDFLARE_API_TOKEN=... vinext deploy --prerender-allon a test project with ISR routeswrangler kv key list --binding VINEXT_CACHEX-Vinext-Cache: HIT