Skip to content

Commit da2f27a

Browse files
committed
update RFC to match implementation (provider naming, routeRules, DisabledAstroCache, node LRU details)
1 parent b30df02 commit da2f27a

1 file changed

Lines changed: 58 additions & 45 deletions

File tree

proposals/0056-route-caching.md

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ Caching is essential for performant server-rendered applications, but current so
236236

237237
**Important:** This API only works in on-demand SSR routes. Static and prerendered routes are already cached and don't support route-level cache control. Caching is not active during development — the API is available but calls are no-ops, ensuring the dev server always serves fresh content.
238238

239+
**When cache is not configured:** If no cache provider is configured (i.e. no `cache` in config and the adapter does not provide a default), accessing `Astro.cache` throws a `CacheNotEnabled` error with an actionable message directing the user to configure a cache provider.
240+
239241
### `Astro.cache.set(options | cacheHint | entry | false)`
240242

241243
Declares caching behavior for the current route. Sets the appropriate headers for the current adapter.
@@ -331,10 +333,11 @@ Invalidates cached responses. Available in `.astro` pages, API routes, and middl
331333
**Invalidate by path:**
332334

333335
```ts
334-
cache.invalidate({ path: "/products/laptop" });
335-
cache.invalidate({ path: "/blog/*" }); // Pattern matching
336+
cache.invalidate({ path: "/products/laptop" }); // Exact path
336337
```
337338

339+
Note: The current implementation supports exact-path invalidation. Wildcard patterns are not currently supported.
340+
338341
**Invalidate by tag:**
339342

340343
```ts
@@ -376,9 +379,17 @@ interface CacheProvider {
376379
* - Read cache headers from response to determine caching behavior
377380
* - Store response in cache
378381
* - Return response
382+
*
383+
* The context includes an optional `waitUntil` function, which runtime
384+
* providers can use for SWR background revalidation without blocking
385+
* the response.
379386
*/
380387
onRequest?(
381-
context: MiddlewareContext,
388+
context: {
389+
request: Request;
390+
url: URL;
391+
waitUntil?: (p: Promise<unknown>) => void;
392+
},
382393
next: () => Promise<Response>,
383394
): Promise<Response>;
384395

@@ -420,6 +431,11 @@ interface AstroCache {
420431
*/
421432
readonly tags: string[];
422433

434+
/**
435+
* Current accumulated cache options for this request (read-only).
436+
*/
437+
readonly options: Readonly<CacheOptions>;
438+
423439
/**
424440
* Invalidate cached content by path, tags, or entry.
425441
*/
@@ -449,6 +465,7 @@ There are two fundamental types of cache providers:
449465
**Runtime provider middleware flow:**
450466

451467
```ts
468+
// context: { request: Request; url: URL; waitUntil?: (p: Promise<unknown>) => void }
452469
async onRequest(context, next) {
453470
// 1. Check cache using context.request. Assume this.cache is a simple key-value store.
454471
const cached = await this.cache.get(context.url.pathname);
@@ -487,28 +504,30 @@ async onRequest(context, next) {
487504

488505
This design ensures consistent behavior: runtime providers honour the same cache headers that external CDNs use.
489506

507+
**Observability:** Runtime providers set an `X-Astro-Cache` response header with the value `HIT`, `MISS`, or `STALE` to indicate whether the response was served from cache. This is useful for debugging and monitoring cache behavior.
508+
490509
### Example Runtime Provider Implementation
491510

492511
Here's a complete example of a Node memory provider implementation:
493512

494513
```ts
495-
import { LRUCache } from "lru-cache";
496-
514+
// Zero-dependency in-memory LRU cache implementation
497515
class NodeMemoryProvider implements CacheProvider {
498516
name = "node-memory";
499-
private cache: LRUCache<string, CachedEntry>;
500-
501-
constructor(options?: { max?: number; ttl?: number }) {
502-
this.cache = new LRUCache({
503-
max: options?.max ?? 500,
504-
ttl: options?.ttl ?? 1000 * 60 * 5, // 5 minutes default
505-
allowStale: true,
506-
updateAgeOnGet: true,
507-
});
517+
private cache: Map<string, CachedEntry>;
518+
private max: number;
519+
520+
constructor(options?: { max?: number }) {
521+
this.max = options?.max ?? 1000;
522+
this.cache = new Map();
508523
}
509524

510525
async onRequest(
511-
context: CacheMiddlewareContext,
526+
context: {
527+
request: Request;
528+
url: URL;
529+
waitUntil?: (p: Promise<unknown>) => void;
530+
},
512531
next: () => Promise<Response>,
513532
): Promise<Response> {
514533
// In real implementation, normalise the URL params etc
@@ -523,13 +542,13 @@ class NodeMemoryProvider implements CacheProvider {
523542
// Get response from route
524543
const response = await next();
525544

526-
// Parse cache headers
545+
// Parse cache headers — TTL is per-entry from response headers
527546
const cacheControl = response.headers.get("CDN-Cache-Control");
528547
if (cacheControl) {
529548
const { maxAge, swr } = this.parseCacheControl(cacheControl);
530549
const tags = response.headers.get("Cache-Tag")?.split(", ") ?? [];
531550

532-
// Store in cache
551+
// Store in cache (evict LRU entries if at capacity)
533552
this.cache.set(cacheKey, {
534553
response: response.clone(),
535554
tags,
@@ -543,16 +562,9 @@ class NodeMemoryProvider implements CacheProvider {
543562
}
544563

545564
async invalidate(options: InvalidateOptions): Promise<void> {
546-
// Naive implementation iterates all keys
547-
548565
if (options.path) {
549-
// Invalidate by path (with wildcard support)
550-
const pattern = new URLPattern(options.path);
551-
for (const key of this.cache.keys()) {
552-
if (pattern.test(key)) {
553-
this.cache.delete(key);
554-
}
555-
}
566+
// Exact-path invalidation (wildcard patterns not currently supported)
567+
this.cache.delete(options.path);
556568
}
557569

558570
if (options.tags) {
@@ -572,8 +584,8 @@ This example demonstrates:
572584

573585
- Middleware-style `onRequest()` that checks cache before calling `next()`
574586
- Reading cache headers from the response to determine caching behavior
575-
- Storing responses with metadata (tags, TTL, timestamp)
576-
- Invalidation by both path patterns and cache tags
587+
- Per-entry TTL derived from response cache headers (not a global default)
588+
- Exact-path invalidation and tag-based invalidation
577589
- Proper response cloning to avoid consuming the stream
578590

579591
### Default Header Generation
@@ -586,15 +598,17 @@ function defaultSetHeaders(options: CacheOptions): Headers {
586598
const headers = new Headers();
587599

588600
// Build Cache-Control value
589-
const directives = ["public"];
601+
const directives: string[] = [];
590602
if (options.maxAge !== undefined) {
591603
directives.push(`max-age=${options.maxAge}`);
592604
}
593605
if (options.swr !== undefined) {
594606
directives.push(`stale-while-revalidate=${options.swr}`);
595607
}
596608

597-
headers.set("CDN-Cache-Control", directives.join(", "));
609+
if (directives.length) {
610+
headers.set("CDN-Cache-Control", directives.join(", "));
611+
}
598612

599613
if (options.tags?.length) {
600614
headers.set("Cache-Tag", options.tags.join(", "));
@@ -649,7 +663,7 @@ Provider packages export a function that accepts provider-specific options and r
649663
export function cacheFastly(options: FastlyOptions) {
650664
return {
651665
entrypoint: new URL("./provider.js", import.meta.url),
652-
options,
666+
config: options,
653667
};
654668
}
655669
```
@@ -665,15 +679,15 @@ Each cache provider maps `Astro.cache.set()` options to platform-specific header
665679
**Vercel Provider (`vercel`):**
666680

667681
```
668-
maxAge + swr → CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600
682+
maxAge + swr → CDN-Cache-Control: max-age=300, stale-while-revalidate=3600
669683
tags → Cache-Tag: products, product:123
670684
lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
671685
```
672686

673687
**Netlify Provider:**
674688

675689
```
676-
maxAge + swr → Netlify-CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600, durable
690+
maxAge + swr → Netlify-CDN-Cache-Control: max-age=300, stale-while-revalidate=3600, durable
677691
tags → Netlify-Cache-Tag: products, product:123
678692
lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
679693
```
@@ -685,7 +699,7 @@ Uses Cloudflare's upcoming support for caching Worker responses with native tag-
685699
For use when Cloudflare acts as a CDN/proxy in front of another origin (like Node.js):
686700

687701
```
688-
maxAge + swr → Cache-Control: public, s-maxage=300, stale-while-revalidate=3600
702+
maxAge + swr → Cache-Control: s-maxage=300, stale-while-revalidate=3600
689703
tags → Cache-Tag: products, product:123
690704
lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
691705
```
@@ -705,7 +719,7 @@ Note: Fastly uses `Surrogate-Control` and `Surrogate-Key` headers (proprietary F
705719
**Akamai Provider:**
706720

707721
```
708-
maxAge + swr → CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600
722+
maxAge + swr → CDN-Cache-Control: max-age=300, stale-while-revalidate=3600
709723
tags → Edge-Cache-Tag: products,product:123
710724
lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
711725
```
@@ -714,7 +728,7 @@ Note: Akamai supports the standardized `CDN-Cache-Control` header (RFC 9213) for
714728

715729
**Node Memory Provider:**
716730

717-
In-memory LRU cache using `lru-cache` library with stale-while-revalidate support. As a runtime provider, it reads the `CDN-Cache-Control` and `Cache-Tag` headers set by the default header generation to determine caching behavior.
731+
Zero-dependency in-memory LRU cache with stale-while-revalidate support. As a runtime provider, it reads the `CDN-Cache-Control` and `Cache-Tag` headers set by the default header generation to determine caching behavior. TTL is determined per-entry from the response's cache headers rather than a global default.
718732

719733
### Invalidation Implementation
720734

@@ -852,7 +866,7 @@ Provider packages export a configuration function that accepts type-safe options
852866
export function cacheFastly(options: FastlyOptions) {
853867
return {
854868
entrypoint: new URL("./provider.js", import.meta.url),
855-
options,
869+
config: options,
856870
};
857871
}
858872

@@ -997,8 +1011,7 @@ export default defineConfig({
9971011
adapter: node(),
9981012
cache: {
9991013
provider: cacheMemory({
1000-
max: 1000, // Max cache entries
1001-
ttl: 300000, // Default TTL in ms
1014+
max: 1000, // Max cache entries (default: 1000)
10021015
}),
10031016
},
10041017
});
@@ -1114,12 +1127,13 @@ Cache set in middleware is treated like config-level defaults: `Astro.cache.set(
11141127

11151128
## Node.js Implementation Details
11161129

1117-
The Node adapter maintains an in-memory LRU cache which provides:
1130+
The Node adapter maintains a zero-dependency in-memory LRU cache (default max: 1000 entries) which provides:
11181131

11191132
- Automatic eviction of least-recently-used entries
1120-
- Built-in stale-while-revalidate support via `fetchMethod`
1121-
- TTL management
1133+
- Built-in stale-while-revalidate support
1134+
- Per-entry TTL derived from response cache headers
11221135
- Size limits to prevent memory issues
1136+
- Exact-path invalidation (wildcard patterns not currently supported)
11231137

11241138
**Important limitations:**
11251139

@@ -1225,7 +1239,6 @@ export default defineConfig({
12251239
cache: {
12261240
provider: cacheMemory({
12271241
max: 1000,
1228-
ttl: 300000,
12291242
}),
12301243
},
12311244
});
@@ -1328,8 +1341,8 @@ Users currently setting cache headers manually can migrate incrementally:
13281341

13291342
```astro
13301343
---
1331-
Astro.response.headers.set('Cache-Control', 'public, max-age=300');
1332-
Astro.response.headers.set('CDN-Cache-Control', 'public, max-age=300, stale-while-revalidate=3600');
1344+
Astro.response.headers.set('Cache-Control', 'max-age=300');
1345+
Astro.response.headers.set('CDN-Cache-Control', 'max-age=300, stale-while-revalidate=3600');
13331346
---
13341347
```
13351348

0 commit comments

Comments
 (0)