/
response-cache.ts
131 lines (118 loc) · 3.78 KB
/
response-cache.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import { IncrementalCache } from './incremental-cache'
import RenderResult from './render-result'
interface CachedRedirectValue {
kind: 'REDIRECT'
props: Object
}
interface CachedPageValue {
kind: 'PAGE'
html: RenderResult
pageData: Object
}
export type ResponseCacheValue = CachedRedirectValue | CachedPageValue
export type ResponseCacheEntry = {
revalidate?: number | false
value: ResponseCacheValue | null
}
type ResponseGenerator = (
hasResolved: boolean,
hadCache: boolean
) => Promise<ResponseCacheEntry | null>
export default class ResponseCache {
incrementalCache: IncrementalCache
pendingResponses: Map<string, Promise<ResponseCacheEntry | null>>
constructor(incrementalCache: IncrementalCache) {
this.incrementalCache = incrementalCache
this.pendingResponses = new Map()
}
public get(
key: string | null,
responseGenerator: ResponseGenerator,
context: { isManualRevalidate?: boolean }
): Promise<ResponseCacheEntry | null> {
const pendingResponse = key ? this.pendingResponses.get(key) : null
if (pendingResponse) {
return pendingResponse
}
let resolver: (cacheEntry: ResponseCacheEntry | null) => void = () => {}
let rejecter: (error: Error) => void = () => {}
const promise: Promise<ResponseCacheEntry | null> = new Promise(
(resolve, reject) => {
resolver = resolve
rejecter = reject
}
)
if (key) {
this.pendingResponses.set(key, promise)
}
let resolved = false
const resolve = (cacheEntry: ResponseCacheEntry | null) => {
if (key) {
// Ensure all reads from the cache get the latest value.
this.pendingResponses.set(key, Promise.resolve(cacheEntry))
}
if (!resolved) {
resolved = true
resolver(cacheEntry)
}
}
// We wait to do any async work until after we've added our promise to
// `pendingResponses` to ensure that any any other calls will reuse the
// same promise until we've fully finished our work.
;(async () => {
try {
const cachedResponse = key ? await this.incrementalCache.get(key) : null
if (
cachedResponse &&
(!context.isManualRevalidate ||
cachedResponse.revalidateAfter === false)
) {
resolve({
revalidate: cachedResponse.curRevalidate,
value:
cachedResponse.value?.kind === 'PAGE'
? {
kind: 'PAGE',
html: RenderResult.fromStatic(cachedResponse.value.html),
pageData: cachedResponse.value.pageData,
}
: cachedResponse.value,
})
if (!cachedResponse.isStale) {
// The cached value is still valid, so we don't need
// to update it yet.
return
}
}
const cacheEntry = await responseGenerator(resolved, !!cachedResponse)
resolve(cacheEntry)
if (key && cacheEntry && typeof cacheEntry.revalidate !== 'undefined') {
await this.incrementalCache.set(
key,
cacheEntry.value?.kind === 'PAGE'
? {
kind: 'PAGE',
html: cacheEntry.value.html.toUnchunkedString(),
pageData: cacheEntry.value.pageData,
}
: cacheEntry.value,
cacheEntry.revalidate
)
}
} catch (err) {
// while revalidating in the background we can't reject as
// we already resolved the cache entry so log the error here
if (resolved) {
console.error(err)
} else {
rejecter(err as Error)
}
} finally {
if (key) {
this.pendingResponses.delete(key)
}
}
})()
return promise
}
}