Skip to content

Commit 30a8b1e

Browse files
committed
refactor: move tags handling from cache-handler module to dedicated tags-handler to allow for reuse
1 parent 4c3aac1 commit 30a8b1e

File tree

2 files changed

+129
-94
lines changed

2 files changed

+129
-94
lines changed

src/run/handlers/cache.cts

+7-94
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@ import { Buffer } from 'node:buffer'
55
import { join } from 'node:path'
66
import { join as posixJoin } from 'node:path/posix'
77

8-
import { purgeCache } from '@netlify/functions'
98
import { type Span } from '@opentelemetry/api'
109
import type { PrerenderManifest } from 'next/dist/build/index.js'
1110
import { NEXT_CACHE_TAGS_HEADER } from 'next/dist/lib/constants.js'
1211

13-
import { name as nextRuntimePkgName, version as nextRuntimePkgVersion } from '../../../package.json'
14-
import { type TagManifest } from '../../shared/blob-types.cjs'
1512
import {
1613
type CacheHandlerContext,
1714
type CacheHandlerForMultipleVersions,
@@ -28,10 +25,9 @@ import {
2825
} from '../storage/storage.cjs'
2926

3027
import { getLogger, getRequestContext } from './request-context.cjs'
28+
import { isAnyTagStale, markTagsAsStaleAndPurgeEdgeCache, purgeEdgeCache } from './tags-handler.cjs'
3129
import { getTracer, recordWarning } from './tracer.cjs'
3230

33-
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
34-
3531
export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
3632
options: CacheHandlerContext
3733
revalidatedTags: string[]
@@ -427,70 +423,17 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
427423
if (requestContext?.didPagesRouterOnDemandRevalidate) {
428424
// encode here to deal with non ASCII characters in the key
429425
const tag = `_N_T_${key === '/index' ? '/' : encodeURI(key)}`
430-
const tags = tag.split(/,|%2c/gi).filter(Boolean)
431-
432-
if (tags.length === 0) {
433-
return
434-
}
435426

436427
getLogger().debug(`Purging CDN cache for: [${tag}]`)
437-
requestContext.trackBackgroundWork(
438-
purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
439-
// TODO: add reporting here
440-
getLogger()
441-
.withError(error)
442-
.error(`[NetlifyCacheHandler]: Purging the cache for tag ${tag} failed`)
443-
}),
444-
)
428+
429+
purgeEdgeCache(tag)
445430
}
446431
}
447432
})
448433
}
449434

450-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
451-
async revalidateTag(tagOrTags: string | string[], ...args: any) {
452-
const revalidateTagPromise = this.doRevalidateTag(tagOrTags, ...args)
453-
454-
const requestContext = getRequestContext()
455-
if (requestContext) {
456-
requestContext.trackBackgroundWork(revalidateTagPromise)
457-
}
458-
459-
return revalidateTagPromise
460-
}
461-
462-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
463-
private async doRevalidateTag(tagOrTags: string | string[], ...args: any) {
464-
getLogger().withFields({ tagOrTags, args }).debug('NetlifyCacheHandler.revalidateTag')
465-
466-
const tags = (Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags])
467-
.flatMap((tag) => tag.split(/,|%2c/gi))
468-
.filter(Boolean)
469-
470-
if (tags.length === 0) {
471-
return
472-
}
473-
474-
const data: TagManifest = {
475-
revalidatedAt: Date.now(),
476-
}
477-
478-
await Promise.all(
479-
tags.map(async (tag) => {
480-
try {
481-
await this.cacheStore.set(tag, data, 'tagManifest.set')
482-
} catch (error) {
483-
getLogger().withError(error).log(`Failed to update tag manifest for ${tag}`)
484-
}
485-
}),
486-
)
487-
488-
await purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
489-
// TODO: add reporting here
490-
getLogger()
491-
.withError(error)
492-
.error(`[NetlifyCacheHandler]: Purging the cache for tags ${tags.join(', ')} failed`)
493-
})
435+
async revalidateTag(tagOrTags: string | string[]) {
436+
return markTagsAsStaleAndPurgeEdgeCache(tagOrTags)
494437
}
495438

496439
resetRequestCache() {
@@ -501,7 +444,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
501444
/**
502445
* Checks if a cache entry is stale through on demand revalidated tags
503446
*/
504-
private async checkCacheEntryStaleByTags(
447+
private checkCacheEntryStaleByTags(
505448
cacheEntry: NetlifyCacheHandlerValue,
506449
tags: string[] = [],
507450
softTags: string[] = [],
@@ -542,37 +485,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
542485
// running in case future `CacheHandler.get` calls would be able to use results).
543486
// "Worst case" scenario is none of tag was invalidated in which case we need to wait
544487
// for all blob store checks to finish before we can be certain that no tag is stale.
545-
return new Promise<boolean>((resolve, reject) => {
546-
const tagManifestPromises: Promise<boolean>[] = []
547-
548-
for (const tag of cacheTags) {
549-
const tagManifestPromise: Promise<TagManifest | null> = this.cacheStore.get<TagManifest>(
550-
tag,
551-
'tagManifest.get',
552-
)
553-
554-
tagManifestPromises.push(
555-
tagManifestPromise.then((tagManifest) => {
556-
if (!tagManifest) {
557-
return false
558-
}
559-
const isStale = tagManifest.revalidatedAt >= (cacheEntry.lastModified || Date.now())
560-
if (isStale) {
561-
resolve(true)
562-
return true
563-
}
564-
return false
565-
}),
566-
)
567-
}
568-
569-
// make sure we resolve promise after all blobs are checked (if we didn't resolve as stale yet)
570-
Promise.all(tagManifestPromises)
571-
.then((tagManifestAreStale) => {
572-
resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale))
573-
})
574-
.catch(reject)
575-
})
488+
return isAnyTagStale(cacheTags, cacheEntry.lastModified)
576489
}
577490
}
578491

src/run/handlers/tags-handler.cts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { purgeCache } from '@netlify/functions'
2+
3+
import { name as nextRuntimePkgName, version as nextRuntimePkgVersion } from '../../../package.json'
4+
import { TagManifest } from '../../shared/blob-types.cjs'
5+
import { getMemoizedKeyValueStoreBackedByRegionalBlobStore } from '../storage/storage.cjs'
6+
7+
import { getLogger, getRequestContext } from './request-context.cjs'
8+
9+
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
10+
11+
/**
12+
* Check if any of the tags were invalidated since the given timestamp
13+
*/
14+
export function isAnyTagStale(tags: string[], timestamp: number): Promise<boolean> {
15+
if (tags.length === 0) {
16+
return Promise.resolve(false)
17+
}
18+
19+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
20+
21+
return new Promise<boolean>((resolve, reject) => {
22+
const tagManifestPromises: Promise<boolean>[] = []
23+
24+
for (const tag of tags) {
25+
const tagManifestPromise: Promise<TagManifest | null> = cacheStore.get<TagManifest>(
26+
tag,
27+
'tagManifest.get',
28+
)
29+
30+
tagManifestPromises.push(
31+
tagManifestPromise.then((tagManifest) => {
32+
if (!tagManifest) {
33+
return false
34+
}
35+
const isStale = tagManifest.revalidatedAt >= (timestamp || Date.now())
36+
if (isStale) {
37+
resolve(true)
38+
return true
39+
}
40+
return false
41+
}),
42+
)
43+
}
44+
45+
// make sure we resolve promise after all blobs are checked (if we didn't resolve as stale yet)
46+
Promise.all(tagManifestPromises)
47+
.then((tagManifestAreStale) => {
48+
resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale))
49+
})
50+
.catch(reject)
51+
})
52+
}
53+
54+
/**
55+
* Transform a tag or tags into an array of tags and handle white space splitting and encoding
56+
*/
57+
function getCacheTagsFromTagOrTags(tagOrTags: string | string[]): string[] {
58+
return (Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags])
59+
.flatMap((tag) => tag.split(/,|%2c/gi))
60+
.filter(Boolean)
61+
}
62+
63+
export function purgeEdgeCache(tagOrTags: string | string[]): void {
64+
const tags = getCacheTagsFromTagOrTags(tagOrTags)
65+
66+
if (tags.length === 0) {
67+
return
68+
}
69+
70+
const purgeCachePromise = purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
71+
// TODO: add reporting here
72+
getLogger()
73+
.withError(error)
74+
.error(`[NetlifyCacheHandler]: Purging the cache for tags [${tags.join(',')}] failed`)
75+
})
76+
77+
getRequestContext()?.trackBackgroundWork(purgeCachePromise)
78+
}
79+
80+
async function doRevalidateTag(tags: string[]): Promise<void> {
81+
getLogger().withFields({ tags }).debug('NetlifyCacheHandler.revalidateTag')
82+
83+
if (tags.length === 0) {
84+
return
85+
}
86+
87+
const data: TagManifest = {
88+
revalidatedAt: Date.now(),
89+
}
90+
91+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
92+
93+
await Promise.all(
94+
tags.map(async (tag) => {
95+
try {
96+
await cacheStore.set(tag, data, 'tagManifest.set')
97+
} catch (error) {
98+
getLogger().withError(error).log(`Failed to update tag manifest for ${tag}`)
99+
}
100+
}),
101+
)
102+
103+
await purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
104+
// TODO: add reporting here
105+
getLogger()
106+
.withError(error)
107+
.error(`[NetlifyCacheHandler]: Purging the cache for tags ${tags.join(', ')} failed`)
108+
})
109+
}
110+
111+
export function markTagsAsStaleAndPurgeEdgeCache(tagOrTags: string | string[]) {
112+
const tags = getCacheTagsFromTagOrTags(tagOrTags)
113+
114+
const revalidateTagPromise = doRevalidateTag(tags)
115+
116+
const requestContext = getRequestContext()
117+
if (requestContext) {
118+
requestContext.trackBackgroundWork(revalidateTagPromise)
119+
}
120+
121+
return revalidateTagPromise
122+
}

0 commit comments

Comments
 (0)