Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a1575b0

Browse filesBrowse files
committed
refactor: move tags handling from cache-handler module to dedicated tags-handler to allow for reuse
1 parent 6b3a40a commit a1575b0
Copy full SHA for a1575b0

File tree

Expand file treeCollapse file tree

2 files changed

+154
-102
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+154
-102
lines changed

‎src/run/handlers/cache.cts

Copy file name to clipboardExpand all lines: src/run/handlers/cache.cts
+8-102Lines changed: 8 additions & 102 deletions
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[] = [],
@@ -534,45 +477,8 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
534477
}
535478

536479
// 2. If any in-memory tags don't indicate that any of tags was invalidated
537-
// we will check blob store. Full-route cache and fetch caches share a lot of tags
538-
// but we will only do actual blob read once withing a single request due to cacheStore
539-
// memoization.
540-
// Additionally, we will resolve the promise as soon as we find first
541-
// stale tag, so that we don't wait for all of them to resolve (but keep all
542-
// running in case future `CacheHandler.get` calls would be able to use results).
543-
// "Worst case" scenario is none of tag was invalidated in which case we need to wait
544-
// 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-
})
480+
// we will check blob store.
481+
return isAnyTagStale(cacheTags, cacheEntry.lastModified)
576482
}
577483
}
578484

‎src/run/handlers/tags-handler.cts

Copy file name to clipboard
+146Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 {
6+
getMemoizedKeyValueStoreBackedByRegionalBlobStore,
7+
MemoizedKeyValueStoreBackedByRegionalBlobStore,
8+
} from '../storage/storage.cjs'
9+
10+
import { getLogger, getRequestContext } from './request-context.cjs'
11+
12+
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
13+
14+
/**
15+
* Get timestamp of the last revalidation for a tag
16+
*/
17+
async function lastTagRevalidationTimestamp(
18+
tag: string,
19+
cacheStore: MemoizedKeyValueStoreBackedByRegionalBlobStore,
20+
): Promise<number | null> {
21+
const tagManifest = await cacheStore.get<TagManifest>(tag, 'tagManifest.get')
22+
if (!tagManifest) {
23+
return null
24+
}
25+
return tagManifest.revalidatedAt
26+
}
27+
28+
/**
29+
* Check if any of the tags were invalidated since the given timestamp
30+
*/
31+
export function isAnyTagStale(tags: string[], timestamp: number): Promise<boolean> {
32+
if (tags.length === 0 || !timestamp) {
33+
return Promise.resolve(false)
34+
}
35+
36+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
37+
38+
// Full-route cache and fetch caches share a lot of tags
39+
// but we will only do actual blob read once withing a single request due to cacheStore
40+
// memoization.
41+
// Additionally, we will resolve the promise as soon as we find first
42+
// stale tag, so that we don't wait for all of them to resolve (but keep all
43+
// running in case future `CacheHandler.get` calls would be able to use results).
44+
// "Worst case" scenario is none of tag was invalidated in which case we need to wait
45+
// for all blob store checks to finish before we can be certain that no tag is stale.
46+
return new Promise<boolean>((resolve, reject) => {
47+
const tagManifestPromises: Promise<boolean>[] = []
48+
49+
for (const tag of tags) {
50+
const lastRevalidationTimestampPromise = lastTagRevalidationTimestamp(tag, cacheStore)
51+
52+
tagManifestPromises.push(
53+
lastRevalidationTimestampPromise.then((lastRevalidationTimestamp) => {
54+
if (!lastRevalidationTimestamp) {
55+
// tag was never revalidated
56+
return false
57+
}
58+
const isStale = lastRevalidationTimestamp >= timestamp
59+
if (isStale) {
60+
// resolve outer promise immediately if any of the tags is stale
61+
resolve(true)
62+
return true
63+
}
64+
return false
65+
}),
66+
)
67+
}
68+
69+
// make sure we resolve promise after all blobs are checked (if we didn't resolve as stale yet)
70+
Promise.all(tagManifestPromises)
71+
.then((tagManifestAreStale) => {
72+
resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale))
73+
})
74+
.catch(reject)
75+
})
76+
}
77+
78+
/**
79+
* Transform a tag or tags into an array of tags and handle white space splitting and encoding
80+
*/
81+
function getCacheTagsFromTagOrTags(tagOrTags: string | string[]): string[] {
82+
return (Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags])
83+
.flatMap((tag) => tag.split(/,|%2c/gi))
84+
.filter(Boolean)
85+
}
86+
87+
export function purgeEdgeCache(tagOrTags: string | string[]): void {
88+
const tags = getCacheTagsFromTagOrTags(tagOrTags)
89+
90+
if (tags.length === 0) {
91+
return
92+
}
93+
94+
const purgeCachePromise = purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
95+
// TODO: add reporting here
96+
getLogger()
97+
.withError(error)
98+
.error(`[NetlifyCacheHandler]: Purging the cache for tags [${tags.join(',')}] failed`)
99+
})
100+
101+
getRequestContext()?.trackBackgroundWork(purgeCachePromise)
102+
}
103+
104+
async function doRevalidateTag(tags: string[]): Promise<void> {
105+
getLogger().withFields({ tags }).debug('NetlifyCacheHandler.revalidateTag')
106+
107+
if (tags.length === 0) {
108+
return
109+
}
110+
111+
const data: TagManifest = {
112+
revalidatedAt: Date.now(),
113+
}
114+
115+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
116+
117+
await Promise.all(
118+
tags.map(async (tag) => {
119+
try {
120+
await cacheStore.set(tag, data, 'tagManifest.set')
121+
} catch (error) {
122+
getLogger().withError(error).log(`Failed to update tag manifest for ${tag}`)
123+
}
124+
}),
125+
)
126+
127+
await purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
128+
// TODO: add reporting here
129+
getLogger()
130+
.withError(error)
131+
.error(`[NetlifyCacheHandler]: Purging the cache for tags ${tags.join(', ')} failed`)
132+
})
133+
}
134+
135+
export function markTagsAsStaleAndPurgeEdgeCache(tagOrTags: string | string[]) {
136+
const tags = getCacheTagsFromTagOrTags(tagOrTags)
137+
138+
const revalidateTagPromise = doRevalidateTag(tags)
139+
140+
const requestContext = getRequestContext()
141+
if (requestContext) {
142+
requestContext.trackBackgroundWork(revalidateTagPromise)
143+
}
144+
145+
return revalidateTagPromise
146+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.