Conversation
🦋 Changeset detectedLatest commit: 17e1b18 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…time Adds the foundational building blocks for the route caching feature (RFC #1245): - CacheOptions, CacheProvider, CacheProviderFactory, InvalidateOptions types - Zod config schema (CacheSchema) registered under experimental.cache - AstroCache class with set() merge semantics, tags accumulation, invalidate(), _applyHeaders() - NoopAstroCache for dev mode - Utility functions: normalizeCacheDriverConfig, cacheConfigToManifest, defaultSetHeaders - Public type exports from astro package
Integrates the cache system into the full request lifecycle: - Virtual module vite-plugin for cache driver resolution (resolves from project root) - SSRManifest: cacheDriver and cacheConfig fields - BasePipeline: getCacheProvider() with lazy resolution - RenderContext: creates AstroCache/NoopAstroCache per request, exposes on Astro.cache and context.cache (API routes, actions, middleware) - App base: wraps onRequest for runtime providers, strips CDN headers after; applies _applyHeaders for CDN-based providers - Serialized manifest: cache driver import + config serialization - Throwing getter on Astro global for prerendered routes
Compiles cache.routes patterns using Astro's existing route parsing infrastructure (getParts, getPattern, routeComparator). Patterns use the same [param]/[...rest] syntax as file-based routing. Most specific route wins, computed once at startup via compileCacheRoutes().
72 unit tests covering: - AstroCache runtime: set() merge semantics, tags, invalidate, _applyHeaders, _isActive - Utils: defaultSetHeaders, normalizeCacheDriverConfig, cacheConfigToManifest, isCacheHint, isLiveDataEntry - NoopAstroCache: all methods callable and no-op - Route matching: exact paths, dynamic params, rest params, priority ordering 6 integration tests with mock CDN provider: - CDN-Cache-Control and Cache-Tag from context.cache.set() - cache.set(false) opt-out, tags-only, config-level routes - .astro page and API route support
Runtime cache provider for @astrojs/node with: - In-memory LRU cache (custom Map-based, no external deps) - SWR support via stale-while-revalidate with background revalidation - Tag-based and path-based invalidation - X-Astro-Cache response header (HIT/MISS/STALE) for observability - Auto-set as default when experimental.cache is enabled without a driver - Exported as @astrojs/node/cache subpath
8 tests covering the full runtime caching lifecycle: - Default driver auto-configuration - Cache hit/miss behavior with body verification - CDN header stripping for runtime providers - Tag-based and path-based invalidation - cache.set(false) opt-out - Uncached route passthrough
Full phased implementation plan for RFC #1245 covering: - Phases 1-3: Core types, pipeline wiring, route matching (complete) - Phase 4: Node adapter with in-memory LRU (complete) - Phase 5: Cloudflare adapter with CacheW - Phase 6: Vercel & Netlify adapters - Phase 7: Documentation
Replace _applyHeaders() and _isActive underscore convention with symbol-keyed methods behind applyCacheHeaders() and isCacheActive() helper functions. The symbols are not exported from the astro package, so users can't access them. Framework call sites import the helpers from the module directly. NoopAstroCache no longer needs the internal methods — the helpers handle the no-op case via 'in' checks.
Breaking: cache.routes moved to experimental.routeRules This commit addresses RFC #1245 feedback: 1. Terminology: Rename 'driver' to 'provider' throughout the cache API. Providers have a richer interface (headers, middleware, invalidation) than the simpler driver pattern used by sessions/unstorage. 2. Configuration: Move cache route config from cache.routes to experimental.routeRules with Nitro-style shortcuts: - Flat cache options: { maxAge: 600, swr: 60 } - Nested form: { cache: { maxAge: 600 } } - Prerender control: { prerender: true } 3. API: Add cache.options getter for full cache state access, complementing the existing cache.tags getter. Files renamed: - cache-driver.ts → cache-provider.ts (both astro + node adapter) - mock-cache-driver.mjs → mock-cache-provider.mjs (test fixture) All 109 tests pass (95 unit + 7 core + 7 node adapter).
routeRules is focused on cache configuration only. Prerender control will be handled separately if/when it's added to the feature.
1d9acb9 to
4c23661
Compare
4c23661 to
35aaf46
Compare
…tructor, fix test import)
…om SSR bundle - Move runtime cache code (AstroCache, noop, route-matching, utils) into cache/runtime/ directory following Astro conventions - Remove node:url and node:path imports that leaked into SSR bundle, breaking Cloudflare/edge deployments and verify-no-node-stuff test plugin - Guard getCacheProvider() call in hot path to skip async overhead when no cache provider is configured (addresses CodSpeed benchmark regression)
- Move in-memory LRU cache provider from @astrojs/node to astro core (it's runtime-agnostic, not Node-specific) - Export memoryCache() from astro/config (follows sharpImageService pattern) - Runtime entrypoint at astro/cache/memory (separate from config context) - Remove bare string form for cache provider config (object-only) - Export CacheProviderConfig from public types
The memory cache provider is now in core, so its tests belong here too. Uses testAdapter() + app.render() instead of a live Node server.
# Conflicts: # packages/astro/src/core/app/base.ts
sarah11918
left a comment
There was a problem hiding this comment.
I'll let @yanthomasdev tackle the commas, but just some thoughts from me re: error docs and changeset!
I'll note the changeset is... huge! I do not object, nor do I object to offloading some of that heavy lifting to the docs themselves, if you decide you want to just give more superficial overviews of the various parts of the feature.
| 'astro': minor | ||
| --- | ||
|
|
||
| Adds a new experimental Route Caching API and Route Rules for controlling SSR response caching. See the [RFC](https://github.com/withastro/roadmap/pull/1245) for full details. |
There was a problem hiding this comment.
| Adds a new experimental Route Caching API and Route Rules for controlling SSR response caching. See the [RFC](https://github.com/withastro/roadmap/pull/1245) for full details. | |
| Adds a new experimental Route Caching API and Route Rules for controlling SSR response caching |
Suggest removing this link since it's also at the bottom and the shorter the first paragraph is for a large PR, the easier it is to read (with all the other stuff like the author attribution)
|
|
||
| /** | ||
| * @docs | ||
| * @kind heading |
There was a problem hiding this comment.
Just noting that this leaves one errant SVG error at the end, also within this heading.
(Would argue it's that one that's in the wrong place, but if we don't fix it as out of scope in this PR, would be nice to get that error moved somewhere more sensible!
| title: 'Cache is not enabled.', | ||
| message: | ||
| '`Astro.cache` is not available because the cache feature is not enabled. To use caching, configure a cache provider in your Astro config under `experimental.cache`.', | ||
| hint: 'Use an adapter that provides a default cache provider, or set one explicitly: `experimental: { cache: { provider: "..." } }`.', |
There was a problem hiding this comment.
Could also be a good place to link to the experimental docs since someone who doesn't have this configured is going to need more than ... to know what to put for a provider.
| If you use `Astro.cache` or `context.cache` without enabling the feature, Astro throws an `AstroError` with the name `CacheNotEnabled` and a message explaining how to configure it. If the configured provider cannot be resolved, Astro throws `CacheProviderNotFound` at build time. | ||
|
|
||
| For more information on enabling and using this feature in your project, see the [Experimental Route Caching docs](https://docs.astro.build/en/reference/experimental-flags/route-caching/). | ||
| For a complete overview, and to give feedback on this experimental API, see the [Route Caching RFC](https://github.com/withastro/roadmap/pull/1245). |
There was a problem hiding this comment.
comma nit
| For a complete overview, and to give feedback on this experimental API, see the [Route Caching RFC](https://github.com/withastro/roadmap/pull/1245). | |
| For a complete overview and to give feedback on this experimental API, see the [Route Caching RFC](https://github.com/withastro/roadmap/pull/1245). |
| */ | ||
| const RouteRuleSchema = z.object({ | ||
| // Nested cache options (full form) | ||
| cache: CacheOptionsSchema.optional(), |
There was a problem hiding this comment.
I think it may make sense to not have cache and just keep the flat shortcuts. If you want to keep both, you'll probably need to use a union instead to not allow both at the same time
| const cache = new LRUMap<string, CachedEntry>(max); | ||
|
|
||
| return { | ||
| name: 'memory', |
There was a problem hiding this comment.
I did left a comment on the RFC about this but reiterating: I don't think it's useful to have name defined in the implementation. I think it'd be better in the config object alongside entrypoint instead so we can use it for logging in the build, eg. if we can't load the provider. One may argue that maybe we don't need a name at all and entrypoint is enough
There was a problem hiding this comment.
The reason I had it is for logging etc, so it's clear which provider it is. We're not using it at the moment, but I can see a time when we'd log that. We have the same pattern for content loaders.
There was a problem hiding this comment.
Np with keeping the name 👍 but my point on where to provider this name still stands
| } | ||
|
|
||
| const memoryProvider: CacheProviderFactory = ( | ||
| config: Record<string, any> | undefined, |
There was a problem hiding this comment.
Combined with satisfies CacheProviderFactory
| config: Record<string, any> | undefined, | |
| options: MemoryCacheProviderOptions = {}, |
| vitePluginActions({ fs, settings }), | ||
| vitePluginServerIslands({ settings, logger }), | ||
| vitePluginSessionDriver({ settings }), | ||
| ...(settings.config.experimental?.cache ? [vitePluginCacheProvider({ settings })] : []), |
There was a problem hiding this comment.
knit: I think it could be better to have the plugin return undefined so all the logic is confined there
| public partial: undefined | boolean = undefined, | ||
| public shouldInjectCspMetaTags = pipeline.manifest.shouldInjectCspMetaTags, | ||
| public session: AstroSession | undefined = undefined, | ||
| public cache: AstroCache | NoopAstroCache | DisabledAstroCache = disabledAstroCache, |
There was a problem hiding this comment.
Is there a good reasons to have different types VS just AstroCache?
| * }); | ||
| * ``` | ||
| */ | ||
| provider?: import('../../core/cache/types.js').CacheProviderConfig; |
There was a problem hiding this comment.
For consistency, should be a regular type import
| * } | ||
| * ``` | ||
| */ | ||
| routeRules?: Record< |
There was a problem hiding this comment.
If you want, you could instead export a RouteRules type from cache/types.js to keep things collocated. Other advantage is that you can then write a type test to make sure the type and the schema remain in sync
Changes
Adds a new experimental Route Caching API and Route Rules for controlling SSR response caching. See the RFC for full details.
Route caching gives you a platform-agnostic way to cache server-rendered responses, based on web standard cache headers. You set caching directives in your routes using
Astro.cache(in.astropages) orcontext.cache(in API routes and middleware), and Astro translates them into the appropriate headers or runtime behavior depending on your adapter. You can also define cache rules for routes declaratively in your config usingexperimental.routeRules, without modifying route code.Getting started
Enable the feature by configuring
experimental.cachewith a cache provider in your Astro config:Using
Astro.cacheandcontext.cacheIn
.astropages, useAstro.cache.set()to control caching:In API routes and middleware, use
context.cache:Cache options
cache.set()accepts the following options:maxAge(number): Time in seconds the response is considered fresh.swr(number): Stale-while-revalidate window in seconds. During this window, stale content is served while a fresh response is generated in the background.tags(string[]): Cache tags for targeted invalidation. Tags accumulate across multipleset()calls within a request.lastModified(Date): When multipleset()calls providelastModified, the most recent date wins.etag(string): Entity tag for conditional requests.Call
cache.set(false)to explicitly opt out of caching for a request.Multiple calls to
cache.set()within a single request are merged: scalar values use last-write-wins,lastModifieduses most-recent-wins, and tags accumulate.Invalidation
Purge cached entries by tag or path using
cache.invalidate():Config-level route rules
Use
experimental.routeRulesto set default cache options for routes without modifying route code. Supports Nitro-style shortcuts for ergonomic configuration:Route patterns support static paths, dynamic parameters (
[slug]), and rest parameters ([...path]). Per-routecache.set()calls merge with (and can override) the config-level defaults.You can also read the current cache state via
cache.options:Cache providers
Cache behavior is determined by the configured cache provider. There are two types:
CDN-Cache-Control,Cache-Tag) and let the CDN handle caching. Astro strips these headers before sending the response to the client.onRequest()to intercept and cache responses in-process, adding anX-Astro-Cacheheader (HIT/MISS/STALE) for observability.Built-in memory cache provider
Astro includes a built-in in-memory LRU cache provider. Import
memoryCachefromastro/configto configure it.Features:
X-Astro-Cacheresponse header:HIT,MISS, orSTALEWriting a custom cache provider
A cache provider is a module that exports a factory function as its default export:
Error handling
If you use
Astro.cacheorcontext.cachewithout enabling the feature, Astro throws anAstroErrorwith the nameCacheNotEnabledand a message explaining how to configure it. If the configured provider cannot be resolved, Astro throwsCacheProviderNotFoundat build time.Testing
Adds comprehensive test suite
Docs
withastro/docs#13305