@@ -4,7 +4,6 @@ import type { StoredComposableCacheEntry } from "@/types/cache";
44import type { InternalEvent , InternalResult } from "@/types/open-next" ;
55import type {
66 CacheEntryType ,
7- CachedFile ,
87 CachedFetchValue ,
98 CacheValue ,
109 OpenNextHandlerOptions ,
@@ -13,6 +12,7 @@ import type {
1312
1413import { createGenericHandler } from "../core/createGenericHandler.js" ;
1514import { resolveCdnInvalidation , resolveIncrementalCache , resolveTagCache } from "../core/resolve.js" ;
15+ import { computeEntryCacheControl } from "../utils/cache-control.js" ;
1616import { getTagsFromValue , isStale , writeTags } from "../utils/cache.js" ;
1717import { runWithOpenNextRequestContext } from "../utils/promise.js" ;
1818import { toReadableStream } from "../utils/stream.js" ;
@@ -130,19 +130,23 @@ async function handleGet(
130130 } ;
131131 }
132132
133- if ( result . value && ! result . shouldBypassTagCache ) {
134- let tags : string [ ] = [ ...additionalTags ] ;
133+ // The tags are also used to make the response purgeable, so they are derived for every hit,
134+ // including the ones bypassing the tag cache.
135+ let tags : string [ ] = [ ...additionalTags ] ;
136+
137+ if ( cacheType === "cache" ) {
138+ tags = [ ...tags , ...getTagsFromValue ( result . value as CacheValue < "cache" > ) ] ;
139+ } else if ( cacheType === "fetch" ) {
140+ const fetchValue = result . value as CachedFetchValue ;
141+ tags = [ ...tags , ...( fetchValue . tags ?? [ ] ) , ...( fetchValue . data ?. tags ?? [ ] ) ] ;
142+ } else if ( cacheType === "composable" ) {
143+ const composableValue = result . value as StoredComposableCacheEntry ;
144+ tags = [ ...tags , ...( composableValue . tags ?? [ ] ) ] ;
145+ }
135146
136- if ( cacheType === "cache" ) {
137- tags = [ ...tags , ...getTagsFromValue ( result . value as CacheValue < "cache" > ) ] ;
138- } else if ( cacheType === "fetch" ) {
139- const fetchValue = result . value as CachedFetchValue ;
140- tags = [ ...tags , ...( fetchValue . tags ?? [ ] ) , ...( fetchValue . data ?. tags ?? [ ] ) ] ;
141- } else if ( cacheType === "composable" ) {
142- const composableValue = result . value as StoredComposableCacheEntry ;
143- tags = [ ...tags , ...( composableValue . tags ?? [ ] ) ] ;
144- }
147+ let isEntryStale = false ;
145148
149+ if ( ! result . shouldBypassTagCache ) {
146150 const lastModified = result . lastModified ?? Date . now ( ) ;
147151
148152 if ( tags . length > 0 ) {
@@ -163,13 +167,15 @@ async function handleGet(
163167 }
164168
165169 // Check if the cache entry is stale (valid but needs background revalidation)
166- const _isStale = tags . length > 0 ? await isStale ( key , tags , lastModified ) : false ;
167- if ( _isStale ) {
170+ isEntryStale = tags . length > 0 ? await isStale ( key , tags , lastModified ) : false ;
171+ if ( isEntryStale ) {
168172 result . lastModified = 1 ;
169173 }
170174 }
171175
172- return buildCacheGetResponse ( result ) ;
176+ // We default to the entry key when no tag is found, so that page router based entries can also
177+ // be purged this way.
178+ return buildCacheGetResponse ( result , isEntryStale , tags . length > 0 ? tags : [ key ] ) ;
173179 } catch ( e ) {
174180 error ( "Failed to get cache entry" , e ) ;
175181 return buildErrorResponse ( "Failed to get cache entry" , 500 ) ;
@@ -399,14 +405,24 @@ async function handleRevalidateTags(body?: Buffer): Promise<InternalResult> {
399405// Cache GET response builder //
400406/////////////////////////////
401407
402- function buildCacheGetResponse ( result : WithLastModified < CacheValue < CacheEntryType > > ) : InternalResult {
408+ function buildCacheGetResponse (
409+ result : WithLastModified < CacheValue < CacheEntryType > > ,
410+ isStaleFromTagCache : boolean ,
411+ tags : string [ ]
412+ ) : InternalResult {
403413 const value = result . value ! ;
404414
405415 const headers : Record < string , string | string [ ] > = {
406416 "x-opennext-cache-found" : "true" ,
407- "Cache-Control" : "no-store" ,
417+ "Cache-Control" : computeEntryCacheControl ( value , result . lastModified , isStaleFromTagCache ) ,
408418 } ;
409419
420+ // The `Cache-Control` above lets an HTTP cache store this response, it can only be invalidated
421+ // through a purge keyed on these tags. See `computeEntryCacheControl`.
422+ if ( tags . length > 0 ) {
423+ headers [ "cache-tag" ] = tags . join ( "," ) ;
424+ }
425+
410426 if ( result . lastModified !== undefined ) {
411427 headers [ "x-opennext-cache-last-modified" ] = String ( result . lastModified ) ;
412428 }
@@ -415,24 +431,28 @@ function buildCacheGetResponse(result: WithLastModified<CacheValue<CacheEntryTyp
415431 }
416432
417433 if ( "kind" in value && value . kind === "FETCH" ) {
418- return buildFetchResponse ( value as CachedFetchValue , headers ) ;
434+ return buildFetchResponse ( value as CacheValue < "fetch" > , headers ) ;
419435 }
420436
421437 if ( "type" in value ) {
422- return buildCachedFileResponse ( value as CachedFile , headers ) ;
438+ return buildCachedFileResponse ( value as CacheValue < "cache" > , headers ) ;
423439 }
424440
425441 return buildComposableResponse ( value as StoredComposableCacheEntry , headers ) ;
426442}
427443
428444function buildFetchResponse (
429- value : CachedFetchValue ,
445+ value : CacheValue < "fetch" > ,
430446 headers : Record < string , string | string [ ] >
431447) : InternalResult {
432448 headers [ "x-opennext-cache-type" ] = "fetch" ;
433449 headers [ "x-opennext-cache-fetch-kind" ] = "FETCH" ;
434450 headers [ "x-opennext-cache-fetch-data-url" ] = value . data . url ;
435451
452+ if ( value . revalidate !== undefined ) {
453+ headers [ "x-opennext-cache-revalidate" ] = String ( value . revalidate ) ;
454+ }
455+
436456 if ( value . data . status !== undefined ) {
437457 headers [ "x-opennext-cache-fetch-data-status" ] = String ( value . data . status ) ;
438458 }
@@ -458,12 +478,16 @@ function buildFetchResponse(
458478}
459479
460480function buildCachedFileResponse (
461- value : CachedFile ,
481+ value : CacheValue < "cache" > ,
462482 headers : Record < string , string | string [ ] >
463483) : InternalResult {
464484 headers [ "x-opennext-cache-type" ] = "cache" ;
465485 headers [ "x-opennext-cache-sub-type" ] = value . type ;
466486
487+ if ( value . revalidate !== undefined ) {
488+ headers [ "x-opennext-cache-revalidate" ] = String ( value . revalidate ) ;
489+ }
490+
467491 if ( value . meta ?. status !== undefined ) {
468492 headers [ "x-opennext-cache-meta-status" ] = String ( value . meta . status ) ;
469493 }
0 commit comments