[PR #734] [CLOSED] feat(prerender): seed KV cache with pre-rendered routes at deploy time #805

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

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/734
Author: @raed04
Created: 4/1/2026
Status: Closed

Base: mainHead: feat/seed-kv-cache-562


📝 Commits (1)

  • fc51d75 feat(prerender): seed KV cache with pre-rendered routes at deploy time

📊 Changes

4 files changed (+1024 additions, -1 deletions)

View changed files

📝 packages/vinext/src/cli.ts (+1 -0)
packages/vinext/src/cloudflare/seed-kv-cache.ts (+299 -0)
📝 packages/vinext/src/deploy.ts (+39 -1)
tests/seed-kv-cache.test.ts (+685 -0)

📄 Description

Summary

  • Adds automatic KV cache population during vinext deploy so pre-rendered pages are cache HITs from the first request
  • New deploy-time step reads vinext-prerender.json and uploads HTML/RSC entries to Workers KV via the Cloudflare bulk REST API
  • Correct cache key format matching runtime KVCacheHandler.get(): cache:app:<buildId>:<pathname>:html

How it works

  1. After pre-rendering completes (Step 6a) and before wrangler deploys the worker (Step 7), a new Step 6c runs automatically
  2. Reads vinext-prerender.json manifest and pre-rendered HTML/RSC files from dist/server/prerendered-routes/
  3. Constructs ISR cache keys via isrCacheKey() (same function the runtime uses)
  4. Base64-encodes RSC data to match KVCacheHandler serialization format
  5. Uploads in batches of 10,000 pairs via Cloudflare KV bulk REST API

Automatic when: CLOUDFLARE_API_TOKEN env var is set and VINEXT_CACHE KV namespace exists in wrangler config.
Opt-out: vinext deploy --no-seed-cache
Non-fatal: Seeding failures log a warning but never block deployment.

Key design decisions

  • Correct key formatcache:app:<buildId>:<pathname>:html matches what KVCacheHandler.get() reads at runtime (unlike TPR which uses cache:<pathname> — a known pre-existing bug)
  • Static routes omit expiration_ttl — matches runtime KVCacheHandler.set() behavior (no expiry for static pages)
  • ISR routes get 10x revalidate TTL clamped to [60s, 30d]
  • Deploy-time only — this module runs in Node.js during deploy, never bundled into the worker (addresses the bundle contamination concern from #562)
  • App Router only — Pages Router seeding is left for future work

Security

  • Path traversal guard: resolved file paths must stay within prerenderDir
  • TOCTOU-safe: uses try/catch on readFileSync (no existsSync + readFileSync race)
  • Error messages capped to 500 chars to prevent sensitive data leakage
  • Checks Cloudflare API success: false responses (HTTP 200 with semantic errors)

Files

File Change
packages/vinext/src/cloudflare/seed-kv-cache.ts New — core seeding module (300 lines)
packages/vinext/src/deploy.ts Step 6c integration + --no-seed-cache flag
packages/vinext/src/cli.ts Wire noSeedCache flag to deploy options
tests/seed-kv-cache.test.ts New — 27 unit tests

Test plan

  • 27 unit tests covering:
    • Correct KV key format with isrCacheKey()
    • Binary RSC base64 round-trip
    • FNV1a hash for long pathnames (>200 chars)
    • Static routes: null revalidateAt, no expiration_ttl
    • ISR routes: correct revalidateAt, TTL clamping (min 60s)
    • revalidate=0 treated as static
    • Unicode pathnames
    • Empty HTML files
    • Mixed manifest (App + Pages + skipped + error routes)
    • Batch splitting at 10,000 pairs with order preservation
    • API success:false rejection
    • API HTTP error handling
  • All existing tests pass
  • Formatting passes vp fmt --check

Closes #562


🔄 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/734 **Author:** [@raed04](https://github.com/raed04) **Created:** 4/1/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `feat/seed-kv-cache-562` --- ### 📝 Commits (1) - [`fc51d75`](https://github.com/cloudflare/vinext/commit/fc51d752e8b7a4d7b29220d975b653a18a9e57a9) feat(prerender): seed KV cache with pre-rendered routes at deploy time ### 📊 Changes **4 files changed** (+1024 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/cli.ts` (+1 -0) ➕ `packages/vinext/src/cloudflare/seed-kv-cache.ts` (+299 -0) 📝 `packages/vinext/src/deploy.ts` (+39 -1) ➕ `tests/seed-kv-cache.test.ts` (+685 -0) </details> ### 📄 Description ## Summary - Adds automatic KV cache population during `vinext deploy` so pre-rendered pages are cache HITs from the first request - New deploy-time step reads `vinext-prerender.json` and uploads HTML/RSC entries to Workers KV via the Cloudflare bulk REST API - Correct cache key format matching runtime `KVCacheHandler.get()`: `cache:app:<buildId>:<pathname>:html` ## How it works 1. After pre-rendering completes (Step 6a) and before wrangler deploys the worker (Step 7), a new **Step 6c** runs automatically 2. Reads `vinext-prerender.json` manifest and pre-rendered HTML/RSC files from `dist/server/prerendered-routes/` 3. Constructs ISR cache keys via `isrCacheKey()` (same function the runtime uses) 4. Base64-encodes RSC data to match `KVCacheHandler` serialization format 5. Uploads in batches of 10,000 pairs via [Cloudflare KV bulk REST API](https://developers.cloudflare.com/api/resources/kv/subresources/namespaces/methods/bulk_update/) **Automatic when:** `CLOUDFLARE_API_TOKEN` env var is set and `VINEXT_CACHE` KV namespace exists in wrangler config. **Opt-out:** `vinext deploy --no-seed-cache` **Non-fatal:** Seeding failures log a warning but never block deployment. ## Key design decisions - **Correct key format** — `cache:app:<buildId>:<pathname>:html` matches what `KVCacheHandler.get()` reads at runtime (unlike TPR which uses `cache:<pathname>` — a known pre-existing bug) - **Static routes omit `expiration_ttl`** — matches runtime `KVCacheHandler.set()` behavior (no expiry for static pages) - **ISR routes get `10x revalidate` TTL** clamped to `[60s, 30d]` - **Deploy-time only** — this module runs in Node.js during deploy, never bundled into the worker (addresses the bundle contamination concern from #562) - **App Router only** — Pages Router seeding is left for future work ## Security - Path traversal guard: resolved file paths must stay within `prerenderDir` - TOCTOU-safe: uses try/catch on `readFileSync` (no `existsSync` + `readFileSync` race) - Error messages capped to 500 chars to prevent sensitive data leakage - Checks Cloudflare API `success: false` responses (HTTP 200 with semantic errors) ## Files | File | Change | |------|--------| | `packages/vinext/src/cloudflare/seed-kv-cache.ts` | **New** — core seeding module (300 lines) | | `packages/vinext/src/deploy.ts` | Step 6c integration + `--no-seed-cache` flag | | `packages/vinext/src/cli.ts` | Wire `noSeedCache` flag to deploy options | | `tests/seed-kv-cache.test.ts` | **New** — 27 unit tests | ## Test plan - [x] 27 unit tests covering: - Correct KV key format with `isrCacheKey()` - Binary RSC base64 round-trip - FNV1a hash for long pathnames (>200 chars) - Static routes: null `revalidateAt`, no `expiration_ttl` - ISR routes: correct `revalidateAt`, TTL clamping (min 60s) - `revalidate=0` treated as static - Unicode pathnames - Empty HTML files - Mixed manifest (App + Pages + skipped + error routes) - Batch splitting at 10,000 pairs with order preservation - API `success:false` rejection - API HTTP error handling - [x] All existing tests pass - [x] Formatting passes `vp fmt --check` Closes #562 --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:10:11 +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#805
No description provided.