| import type { |
| ResponseCacheEntry, |
| ResponseGenerator, |
| ResponseCacheBase, |
| IncrementalResponseCacheEntry, |
| IncrementalResponseCache, |
| } from './types' |
|
|
| import { Batcher } from '../../lib/batcher' |
| import { scheduleOnNextTick } from '../../lib/scheduler' |
| import { |
| fromResponseCacheEntry, |
| routeKindToIncrementalCacheKind, |
| toResponseCacheEntry, |
| } from './utils' |
| import type { RouteKind } from '../route-kind' |
|
|
| export * from './types' |
|
|
| export default class ResponseCache implements ResponseCacheBase { |
| private readonly getBatcher = Batcher.create< |
| { key: string; isOnDemandRevalidate: boolean }, |
| IncrementalResponseCacheEntry | null, |
| string |
| >({ |
| |
| |
| cacheKeyFn: ({ key, isOnDemandRevalidate }) => |
| `${key}-${isOnDemandRevalidate ? '1' : '0'}`, |
| |
| |
| |
| schedulerFn: scheduleOnNextTick, |
| }) |
|
|
| private readonly revalidateBatcher = Batcher.create< |
| string, |
| IncrementalResponseCacheEntry | null |
| >({ |
| |
| |
| |
| schedulerFn: scheduleOnNextTick, |
| }) |
|
|
| private previousCacheItem?: { |
| key: string |
| entry: IncrementalResponseCacheEntry | null |
| expiresAt: number |
| } |
|
|
| |
| |
| |
| private minimal_mode?: boolean |
|
|
| constructor(minimal_mode: boolean) { |
| this.minimal_mode = minimal_mode |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public async get( |
| key: string | null, |
| responseGenerator: ResponseGenerator, |
| context: { |
| routeKind: RouteKind |
| isOnDemandRevalidate?: boolean |
| isPrefetch?: boolean |
| incrementalCache: IncrementalResponseCache |
| isRoutePPREnabled?: boolean |
| isFallback?: boolean |
| waitUntil?: (prom: Promise<any>) => void |
| } |
| ): Promise<ResponseCacheEntry | null> { |
| |
| |
| if (!key) { |
| return responseGenerator({ |
| hasResolved: false, |
| previousCacheEntry: null, |
| }) |
| } |
|
|
| |
| if ( |
| this.minimal_mode && |
| this.previousCacheItem?.key === key && |
| this.previousCacheItem.expiresAt > Date.now() |
| ) { |
| return toResponseCacheEntry(this.previousCacheItem.entry) |
| } |
|
|
| const { |
| incrementalCache, |
| isOnDemandRevalidate = false, |
| isFallback = false, |
| isRoutePPREnabled = false, |
| isPrefetch = false, |
| waitUntil, |
| routeKind, |
| } = context |
|
|
| const response = await this.getBatcher.batch( |
| { key, isOnDemandRevalidate }, |
| ({ resolve }) => { |
| const promise = this.handleGet( |
| key, |
| responseGenerator, |
| { |
| incrementalCache, |
| isOnDemandRevalidate, |
| isFallback, |
| isRoutePPREnabled, |
| isPrefetch, |
| routeKind, |
| }, |
| resolve |
| ) |
|
|
| |
| if (waitUntil) waitUntil(promise) |
|
|
| return promise |
| } |
| ) |
|
|
| return toResponseCacheEntry(response) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private async handleGet( |
| key: string, |
| responseGenerator: ResponseGenerator, |
| context: { |
| incrementalCache: IncrementalResponseCache |
| isOnDemandRevalidate: boolean |
| isFallback: boolean |
| isRoutePPREnabled: boolean |
| isPrefetch: boolean |
| routeKind: RouteKind |
| }, |
| resolve: (value: IncrementalResponseCacheEntry | null) => void |
| ): Promise<IncrementalResponseCacheEntry | null> { |
| let previousIncrementalCacheEntry: IncrementalResponseCacheEntry | null = |
| null |
| let resolved = false |
|
|
| try { |
| |
| previousIncrementalCacheEntry = !this.minimal_mode |
| ? await context.incrementalCache.get(key, { |
| kind: routeKindToIncrementalCacheKind(context.routeKind), |
| isRoutePPREnabled: context.isRoutePPREnabled, |
| isFallback: context.isFallback, |
| }) |
| : null |
|
|
| if (previousIncrementalCacheEntry && !context.isOnDemandRevalidate) { |
| resolve(previousIncrementalCacheEntry) |
| resolved = true |
|
|
| if (!previousIncrementalCacheEntry.isStale || context.isPrefetch) { |
| |
| return previousIncrementalCacheEntry |
| } |
| } |
|
|
| |
| const incrementalResponseCacheEntry = await this.revalidate( |
| key, |
| context.incrementalCache, |
| context.isRoutePPREnabled, |
| context.isFallback, |
| responseGenerator, |
| previousIncrementalCacheEntry, |
| previousIncrementalCacheEntry !== null && !context.isOnDemandRevalidate |
| ) |
|
|
| |
| if (!incrementalResponseCacheEntry) { |
| |
| if (this.minimal_mode) this.previousCacheItem = undefined |
| return null |
| } |
|
|
| |
| if (context.isOnDemandRevalidate && !resolved) { |
| return incrementalResponseCacheEntry |
| } |
|
|
| return incrementalResponseCacheEntry |
| } catch (err) { |
| |
| |
| if (resolved) { |
| console.error(err) |
| return null |
| } |
|
|
| throw err |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public async revalidate( |
| key: string, |
| incrementalCache: IncrementalResponseCache, |
| isRoutePPREnabled: boolean, |
| isFallback: boolean, |
| responseGenerator: ResponseGenerator, |
| previousIncrementalCacheEntry: IncrementalResponseCacheEntry | null, |
| hasResolved: boolean, |
| waitUntil?: (prom: Promise<any>) => void |
| ) { |
| return this.revalidateBatcher.batch(key, () => { |
| const promise = this.handleRevalidate( |
| key, |
| incrementalCache, |
| isRoutePPREnabled, |
| isFallback, |
| responseGenerator, |
| previousIncrementalCacheEntry, |
| hasResolved |
| ) |
|
|
| |
| if (waitUntil) waitUntil(promise) |
|
|
| return promise |
| }) |
| } |
|
|
| private async handleRevalidate( |
| key: string, |
| incrementalCache: IncrementalResponseCache, |
| isRoutePPREnabled: boolean, |
| isFallback: boolean, |
| responseGenerator: ResponseGenerator, |
| previousIncrementalCacheEntry: IncrementalResponseCacheEntry | null, |
| hasResolved: boolean |
| ) { |
| try { |
| |
| const responseCacheEntry = await responseGenerator({ |
| hasResolved, |
| previousCacheEntry: previousIncrementalCacheEntry, |
| isRevalidating: true, |
| }) |
| if (!responseCacheEntry) { |
| return null |
| } |
|
|
| |
| const incrementalResponseCacheEntry = await fromResponseCacheEntry({ |
| ...responseCacheEntry, |
| isMiss: !previousIncrementalCacheEntry, |
| }) |
|
|
| |
| |
| if (incrementalResponseCacheEntry.cacheControl) { |
| if (this.minimal_mode) { |
| this.previousCacheItem = { |
| key, |
| entry: incrementalResponseCacheEntry, |
| expiresAt: Date.now() + 1000, |
| } |
| } else { |
| await incrementalCache.set(key, incrementalResponseCacheEntry.value, { |
| cacheControl: incrementalResponseCacheEntry.cacheControl, |
| isRoutePPREnabled, |
| isFallback, |
| }) |
| } |
| } |
|
|
| return incrementalResponseCacheEntry |
| } catch (err) { |
| |
| |
| if (previousIncrementalCacheEntry?.cacheControl) { |
| const revalidate = Math.min( |
| Math.max( |
| previousIncrementalCacheEntry.cacheControl.revalidate || 3, |
| 3 |
| ), |
| 30 |
| ) |
| const expire = |
| previousIncrementalCacheEntry.cacheControl.expire === undefined |
| ? undefined |
| : Math.max( |
| revalidate + 3, |
| previousIncrementalCacheEntry.cacheControl.expire |
| ) |
|
|
| await incrementalCache.set(key, previousIncrementalCacheEntry.value, { |
| cacheControl: { revalidate: revalidate, expire: expire }, |
| isRoutePPREnabled, |
| isFallback, |
| }) |
| } |
|
|
| |
| throw err |
| } |
| } |
| } |
|
|