rsnext/packages/next/server/response-cache/web.ts
Shu Ding 0c95a93858
Eliminate path polyfill and incremental-cache from base server (#39548)
Continue to optimize the base server to make it leaner. These are only needed by the Node.js server currently.

Related:
- #39433
- #39045
- #39044
- #39037

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)
2022-08-12 15:25:47 +00:00

129 lines
3.7 KiB
TypeScript

import type { ResponseCacheEntry, ResponseGenerator } from './types'
/**
* In the web server, there is currently no incremental cache provided and we
* always SSR the page.
*/
export default class WebResponseCache {
pendingResponses: Map<string, Promise<ResponseCacheEntry | null>>
previousCacheItem?: {
key: string
entry: ResponseCacheEntry | null
expiresAt: number
}
minimalMode?: boolean
constructor(minimalMode: boolean) {
this.pendingResponses = new Map()
this.minimalMode = minimalMode
}
public get(
key: string | null,
responseGenerator: ResponseGenerator,
context: {
isManualRevalidate?: boolean
isPrefetch?: boolean
}
): Promise<ResponseCacheEntry | null> {
// ensure manual revalidate doesn't block normal requests
const pendingResponseKey = key
? `${key}-${context.isManualRevalidate ? '1' : '0'}`
: null
const pendingResponse = pendingResponseKey
? this.pendingResponses.get(pendingResponseKey)
: 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 (pendingResponseKey) {
this.pendingResponses.set(pendingResponseKey, promise)
}
let resolved = false
const resolve = (cacheEntry: ResponseCacheEntry | null) => {
if (pendingResponseKey) {
// Ensure all reads from the cache get the latest value.
this.pendingResponses.set(
pendingResponseKey,
Promise.resolve(cacheEntry)
)
}
if (!resolved) {
resolved = true
resolver(cacheEntry)
}
}
// we keep the previous cache entry around to leverage
// when the incremental cache is disabled in minimal mode
if (
pendingResponseKey &&
this.minimalMode &&
this.previousCacheItem?.key === pendingResponseKey &&
this.previousCacheItem.expiresAt > Date.now()
) {
resolve(this.previousCacheItem.entry)
this.pendingResponses.delete(pendingResponseKey)
return promise
}
// 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 cacheEntry = await responseGenerator(resolved, false)
const resolveValue =
cacheEntry === null
? null
: {
...cacheEntry,
isMiss: true,
}
// for manual revalidate wait to resolve until cache is set
if (!context.isManualRevalidate) {
resolve(resolveValue)
}
if (key && cacheEntry && typeof cacheEntry.revalidate !== 'undefined') {
this.previousCacheItem = {
key: pendingResponseKey || key,
entry: cacheEntry,
expiresAt: Date.now() + 1000,
}
} else {
this.previousCacheItem = undefined
}
if (context.isManualRevalidate) {
resolve(resolveValue)
}
} 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 (pendingResponseKey) {
this.pendingResponses.delete(pendingResponseKey)
}
}
})()
return promise
}
}