| | import type { DeepReadonly } from '../../shared/lib/deep-readonly' |
| | |
| | import { |
| | renderToReadableStream, |
| | decodeReply, |
| | decodeReplyFromAsyncIterable, |
| | createTemporaryReferenceSet as createServerTemporaryReferenceSet, |
| | } from 'react-server-dom-webpack/server' |
| | import { |
| | createFromReadableStream, |
| | encodeReply, |
| | createTemporaryReferenceSet as createClientTemporaryReferenceSet, |
| | } from 'react-server-dom-webpack/client' |
| | import { prerender } from 'react-server-dom-webpack/static' |
| | |
| |
|
| | import type { WorkStore } from '../app-render/work-async-storage.external' |
| | import { workAsyncStorage } from '../app-render/work-async-storage.external' |
| | import type { |
| | PrerenderStoreModernClient, |
| | PrerenderStoreModernRuntime, |
| | PrivateUseCacheStore, |
| | RequestStore, |
| | RevalidateStore, |
| | UseCacheStore, |
| | WorkUnitStore, |
| | } from '../app-render/work-unit-async-storage.external' |
| | import { |
| | getHmrRefreshHash, |
| | getRenderResumeDataCache, |
| | getPrerenderResumeDataCache, |
| | workUnitAsyncStorage, |
| | getDraftModeProviderForCacheScope, |
| | getCacheSignal, |
| | isHmrRefresh, |
| | getServerComponentsHmrCache, |
| | getRuntimeStagePromise, |
| | } from '../app-render/work-unit-async-storage.external' |
| |
|
| | import { |
| | makeDevtoolsIOAwarePromise, |
| | makeHangingPromise, |
| | } from '../dynamic-rendering-utils' |
| |
|
| | import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' |
| |
|
| | import { |
| | getClientReferenceManifest, |
| | getServerModuleMap, |
| | } from '../app-render/manifests-singleton' |
| | import type { CacheEntry } from '../lib/cache-handlers/types' |
| | import type { CacheSignal } from '../app-render/cache-signal' |
| | import { decryptActionBoundArgs } from '../app-render/encryption' |
| | import { InvariantError } from '../../shared/lib/invariant-error' |
| | import { createReactServerErrorHandler } from '../app-render/create-error-handler' |
| | import { DYNAMIC_EXPIRE, RUNTIME_PREFETCH_DYNAMIC_STALE } from './constants' |
| | import { getCacheHandler } from './handlers' |
| | import { UseCacheTimeoutError } from './use-cache-errors' |
| | import { |
| | createHangingInputAbortSignal, |
| | postponeWithTracking, |
| | throwToInterruptStaticGeneration, |
| | } from '../app-render/dynamic-rendering' |
| | import { |
| | makeErroringSearchParamsForUseCache, |
| | type SearchParams, |
| | } from '../request/search-params' |
| | import type { Params } from '../request/params' |
| | import { createLazyResult, isResolvedLazyResult } from '../lib/lazy-result' |
| | import { dynamicAccessAsyncStorage } from '../app-render/dynamic-access-async-storage.external' |
| | import type { CacheLife } from './cache-life' |
| | import { RenderStage } from '../app-render/staged-rendering' |
| | import * as Log from '../../build/output/log' |
| |
|
| | interface PrivateCacheContext { |
| | readonly kind: 'private' |
| | readonly outerWorkUnitStore: |
| | | RequestStore |
| | | PrivateUseCacheStore |
| | | PrerenderStoreModernRuntime |
| | } |
| |
|
| | interface PublicCacheContext { |
| | readonly kind: 'public' |
| | |
| | readonly outerWorkUnitStore: |
| | | Exclude<WorkUnitStore, PrerenderStoreModernClient> |
| | | undefined |
| | } |
| |
|
| | type CacheContext = PrivateCacheContext | PublicCacheContext |
| |
|
| | type CacheKeyParts = |
| | | [buildId: string, id: string, args: unknown[]] |
| | | [buildId: string, id: string, args: unknown[], hmrRefreshHash: string] |
| |
|
| | interface UseCachePageInnerProps { |
| | params: Promise<Params> |
| | searchParams?: Promise<SearchParams> |
| | } |
| |
|
| | export interface UseCachePageProps { |
| | params: Promise<Params> |
| | searchParams: Promise<SearchParams> |
| | $$isPage: true |
| | } |
| |
|
| | export type UseCacheLayoutProps = { |
| | params: Promise<Params> |
| | $$isLayout: true |
| | } & { |
| | |
| | |
| | [slot: string]: any |
| | } |
| |
|
| | const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' |
| |
|
| | const debug = process.env.NEXT_PRIVATE_DEBUG_CACHE |
| | ? console.debug.bind(console, 'use-cache:') |
| | : undefined |
| |
|
| | const filterStackFrame = |
| | process.env.NODE_ENV !== 'production' |
| | ? (require('../lib/source-maps') as typeof import('../lib/source-maps')) |
| | .filterStackFrameDEV |
| | : undefined |
| | const findSourceMapURL = |
| | process.env.NODE_ENV !== 'production' |
| | ? (require('../lib/source-maps') as typeof import('../lib/source-maps')) |
| | .findSourceMapURLDEV |
| | : undefined |
| |
|
| | function generateCacheEntry( |
| | workStore: WorkStore, |
| | cacheContext: CacheContext, |
| | clientReferenceManifest: DeepReadonly<ClientReferenceManifest>, |
| | encodedArguments: FormData | string, |
| | fn: (...args: unknown[]) => Promise<unknown>, |
| | timeoutError: UseCacheTimeoutError |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | return workStore.runInCleanSnapshot( |
| | generateCacheEntryWithRestoredWorkStore, |
| | workStore, |
| | cacheContext, |
| | clientReferenceManifest, |
| | encodedArguments, |
| | fn, |
| | timeoutError |
| | ) |
| | } |
| |
|
| | function generateCacheEntryWithRestoredWorkStore( |
| | workStore: WorkStore, |
| | cacheContext: CacheContext, |
| | clientReferenceManifest: DeepReadonly<ClientReferenceManifest>, |
| | encodedArguments: FormData | string, |
| | fn: (...args: unknown[]) => Promise<unknown>, |
| | timeoutError: UseCacheTimeoutError |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | return workAsyncStorage.run( |
| | workStore, |
| | generateCacheEntryWithCacheContext, |
| | workStore, |
| | cacheContext, |
| | clientReferenceManifest, |
| | encodedArguments, |
| | fn, |
| | timeoutError |
| | ) |
| | } |
| |
|
| | function createUseCacheStore( |
| | workStore: WorkStore, |
| | cacheContext: CacheContext, |
| | defaultCacheLife: Required<CacheLife> |
| | ): UseCacheStore { |
| | if (cacheContext.kind === 'private') { |
| | const outerWorkUnitStore = cacheContext.outerWorkUnitStore |
| |
|
| | return { |
| | type: 'private-cache', |
| | phase: 'render', |
| | implicitTags: outerWorkUnitStore?.implicitTags, |
| | revalidate: defaultCacheLife.revalidate, |
| | expire: defaultCacheLife.expire, |
| | stale: defaultCacheLife.stale, |
| | explicitRevalidate: undefined, |
| | explicitExpire: undefined, |
| | explicitStale: undefined, |
| | tags: null, |
| | hmrRefreshHash: getHmrRefreshHash(workStore, outerWorkUnitStore), |
| | isHmrRefresh: isHmrRefresh(workStore, outerWorkUnitStore), |
| | serverComponentsHmrCache: getServerComponentsHmrCache( |
| | workStore, |
| | outerWorkUnitStore |
| | ), |
| | forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore), |
| | runtimeStagePromise: getRuntimeStagePromise(outerWorkUnitStore), |
| | draftMode: getDraftModeProviderForCacheScope( |
| | workStore, |
| | outerWorkUnitStore |
| | ), |
| | rootParams: outerWorkUnitStore.rootParams, |
| | headers: outerWorkUnitStore.headers, |
| | cookies: outerWorkUnitStore.cookies, |
| | } |
| | } else { |
| | let useCacheOrRequestStore: RequestStore | UseCacheStore | undefined |
| | const outerWorkUnitStore = cacheContext.outerWorkUnitStore |
| |
|
| | if (outerWorkUnitStore) { |
| | switch (outerWorkUnitStore?.type) { |
| | case 'cache': |
| | case 'private-cache': |
| | case 'request': |
| | useCacheOrRequestStore = outerWorkUnitStore |
| | break |
| | case 'prerender-runtime': |
| | case 'prerender': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | outerWorkUnitStore satisfies never |
| | } |
| | } |
| |
|
| | return { |
| | type: 'cache', |
| | phase: 'render', |
| | implicitTags: outerWorkUnitStore?.implicitTags, |
| | revalidate: defaultCacheLife.revalidate, |
| | expire: defaultCacheLife.expire, |
| | stale: defaultCacheLife.stale, |
| | explicitRevalidate: undefined, |
| | explicitExpire: undefined, |
| | explicitStale: undefined, |
| | tags: null, |
| | hmrRefreshHash: |
| | outerWorkUnitStore && getHmrRefreshHash(workStore, outerWorkUnitStore), |
| | isHmrRefresh: useCacheOrRequestStore?.isHmrRefresh ?? false, |
| | serverComponentsHmrCache: |
| | useCacheOrRequestStore?.serverComponentsHmrCache, |
| | forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore), |
| | draftMode: |
| | outerWorkUnitStore && |
| | getDraftModeProviderForCacheScope(workStore, outerWorkUnitStore), |
| | } |
| | } |
| | } |
| |
|
| | function assertDefaultCacheLife( |
| | defaultCacheLife: CacheLife | undefined |
| | ): asserts defaultCacheLife is Required<CacheLife> { |
| | if ( |
| | !defaultCacheLife || |
| | defaultCacheLife.revalidate == null || |
| | defaultCacheLife.expire == null || |
| | defaultCacheLife.stale == null |
| | ) { |
| | throw new InvariantError( |
| | 'A default cacheLife profile must always be provided.' |
| | ) |
| | } |
| | } |
| |
|
| | function generateCacheEntryWithCacheContext( |
| | workStore: WorkStore, |
| | cacheContext: CacheContext, |
| | clientReferenceManifest: DeepReadonly<ClientReferenceManifest>, |
| | encodedArguments: FormData | string, |
| | fn: (...args: unknown[]) => Promise<unknown>, |
| | timeoutError: UseCacheTimeoutError |
| | ) { |
| | if (!workStore.cacheLifeProfiles) { |
| | throw new InvariantError('cacheLifeProfiles should always be provided.') |
| | } |
| | const defaultCacheLife = workStore.cacheLifeProfiles['default'] |
| | assertDefaultCacheLife(defaultCacheLife) |
| |
|
| | |
| | const cacheStore = createUseCacheStore( |
| | workStore, |
| | cacheContext, |
| | defaultCacheLife |
| | ) |
| |
|
| | return workUnitAsyncStorage.run(cacheStore, () => |
| | dynamicAccessAsyncStorage.run( |
| | { abortController: new AbortController() }, |
| | generateCacheEntryImpl, |
| | workStore, |
| | cacheContext, |
| | cacheStore, |
| | clientReferenceManifest, |
| | encodedArguments, |
| | fn, |
| | timeoutError |
| | ) |
| | ) |
| | } |
| |
|
| | function propagateCacheLifeAndTagsToRevalidateStore( |
| | revalidateStore: RevalidateStore, |
| | entry: CacheEntry |
| | ): void { |
| | const outerTags = (revalidateStore.tags ??= []) |
| |
|
| | for (const tag of entry.tags) { |
| | if (!outerTags.includes(tag)) { |
| | outerTags.push(tag) |
| | } |
| | } |
| |
|
| | if (revalidateStore.stale > entry.stale) { |
| | revalidateStore.stale = entry.stale |
| | } |
| |
|
| | if (revalidateStore.revalidate > entry.revalidate) { |
| | revalidateStore.revalidate = entry.revalidate |
| | } |
| |
|
| | if (revalidateStore.expire > entry.expire) { |
| | revalidateStore.expire = entry.expire |
| | } |
| | } |
| |
|
| | function propagateCacheLifeAndTags( |
| | cacheContext: CacheContext, |
| | entry: CacheEntry |
| | ): void { |
| | if (cacheContext.kind === 'private') { |
| | switch (cacheContext.outerWorkUnitStore.type) { |
| | case 'prerender-runtime': |
| | case 'private-cache': |
| | propagateCacheLifeAndTagsToRevalidateStore( |
| | cacheContext.outerWorkUnitStore, |
| | entry |
| | ) |
| | break |
| | case 'request': |
| | case undefined: |
| | break |
| | default: |
| | cacheContext.outerWorkUnitStore satisfies never |
| | } |
| | } else { |
| | switch (cacheContext.outerWorkUnitStore?.type) { |
| | case 'cache': |
| | case 'private-cache': |
| | case 'prerender': |
| | case 'prerender-runtime': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | propagateCacheLifeAndTagsToRevalidateStore( |
| | cacheContext.outerWorkUnitStore, |
| | entry |
| | ) |
| | break |
| | case 'request': |
| | case 'unstable-cache': |
| | case undefined: |
| | break |
| | default: |
| | cacheContext.outerWorkUnitStore satisfies never |
| | } |
| | } |
| | } |
| |
|
| | async function collectResult( |
| | savedStream: ReadableStream<Uint8Array>, |
| | workStore: WorkStore, |
| | cacheContext: CacheContext, |
| | innerCacheStore: UseCacheStore, |
| | startTime: number, |
| | errors: Array<unknown> |
| | ): Promise<CacheEntry> { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | const buffer: Uint8Array[] = [] |
| | const reader = savedStream.getReader() |
| |
|
| | try { |
| | for (let entry; !(entry = await reader.read()).done; ) { |
| | buffer.push(entry.value) |
| | } |
| | } catch (error) { |
| | errors.push(error) |
| | } |
| |
|
| | let idx = 0 |
| | const bufferStream = new ReadableStream<Uint8Array>({ |
| | pull(controller) { |
| | if (workStore.invalidDynamicUsageError) { |
| | controller.error(workStore.invalidDynamicUsageError) |
| | } else if (idx < buffer.length) { |
| | controller.enqueue(buffer[idx++]) |
| | } else if (errors.length > 0) { |
| | |
| | controller.error(errors[0]) |
| | } else { |
| | controller.close() |
| | } |
| | }, |
| | }) |
| |
|
| | const collectedTags = innerCacheStore.tags |
| | |
| | |
| | |
| | const collectedRevalidate = |
| | innerCacheStore.explicitRevalidate !== undefined |
| | ? innerCacheStore.explicitRevalidate |
| | : innerCacheStore.revalidate |
| | const collectedExpire = |
| | innerCacheStore.explicitExpire !== undefined |
| | ? innerCacheStore.explicitExpire |
| | : innerCacheStore.expire |
| | const collectedStale = |
| | innerCacheStore.explicitStale !== undefined |
| | ? innerCacheStore.explicitStale |
| | : innerCacheStore.stale |
| |
|
| | const entry: CacheEntry = { |
| | value: bufferStream, |
| | timestamp: startTime, |
| | revalidate: collectedRevalidate, |
| | expire: collectedExpire, |
| | stale: collectedStale, |
| | tags: collectedTags === null ? [] : collectedTags, |
| | } |
| |
|
| | if (cacheContext.outerWorkUnitStore) { |
| | const outerWorkUnitStore = cacheContext.outerWorkUnitStore |
| |
|
| | |
| | switch (outerWorkUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-runtime': { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | break |
| | } |
| | case 'request': { |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | outerWorkUnitStore.cacheSignal |
| | ) { |
| | |
| | |
| | break |
| | } |
| | |
| | } |
| |
|
| | case 'private-cache': |
| | case 'cache': |
| | case 'unstable-cache': |
| | case 'prerender-legacy': |
| | case 'prerender-ppr': { |
| | propagateCacheLifeAndTags(cacheContext, entry) |
| | break |
| | } |
| | default: { |
| | outerWorkUnitStore satisfies never |
| | } |
| | } |
| |
|
| | const cacheSignal = getCacheSignal(outerWorkUnitStore) |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | } |
| | } |
| |
|
| | return entry |
| | } |
| |
|
| | type GenerateCacheEntryResult = |
| | | { |
| | readonly type: 'cached' |
| | readonly stream: ReadableStream |
| | readonly pendingCacheEntry: Promise<CacheEntry> |
| | } |
| | | { |
| | readonly type: 'prerender-dynamic' |
| | readonly hangingPromise: Promise<never> |
| | } |
| |
|
| | async function generateCacheEntryImpl( |
| | workStore: WorkStore, |
| | cacheContext: CacheContext, |
| | innerCacheStore: UseCacheStore, |
| | clientReferenceManifest: DeepReadonly<ClientReferenceManifest>, |
| | encodedArguments: FormData | string, |
| | fn: (...args: unknown[]) => Promise<unknown>, |
| | timeoutError: UseCacheTimeoutError |
| | ): Promise<GenerateCacheEntryResult> { |
| | const temporaryReferences = createServerTemporaryReferenceSet() |
| | const outerWorkUnitStore = cacheContext.outerWorkUnitStore |
| |
|
| | const [, , args] = |
| | typeof encodedArguments === 'string' |
| | ? await decodeReply<CacheKeyParts>( |
| | encodedArguments, |
| | getServerModuleMap(), |
| | { temporaryReferences } |
| | ) |
| | : await decodeReplyFromAsyncIterable<CacheKeyParts>( |
| | { |
| | async *[Symbol.asyncIterator]() { |
| | for (const entry of encodedArguments) { |
| | yield entry |
| | } |
| |
|
| | if (outerWorkUnitStore) { |
| | switch (outerWorkUnitStore.type) { |
| | case 'prerender-runtime': |
| | case 'prerender': |
| | |
| | |
| | |
| | |
| | |
| | await new Promise<void>((resolve) => { |
| | if (outerWorkUnitStore.renderSignal.aborted) { |
| | resolve() |
| | } else { |
| | outerWorkUnitStore.renderSignal.addEventListener( |
| | 'abort', |
| | () => resolve(), |
| | { once: true } |
| | ) |
| | } |
| | }) |
| | break |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | outerWorkUnitStore satisfies never |
| | } |
| | } |
| | }, |
| | }, |
| | getServerModuleMap(), |
| | { temporaryReferences } |
| | ) |
| |
|
| | |
| | const startTime = performance.timeOrigin + performance.now() |
| |
|
| | |
| | |
| | |
| | |
| | const resultPromise = createLazyResult(fn.bind(null, ...args)) |
| |
|
| | let errors: Array<unknown> = [] |
| |
|
| | |
| | |
| | |
| | |
| | const handleError = createReactServerErrorHandler( |
| | workStore.dev, |
| | workStore.isBuildTimePrerendering ?? false, |
| | workStore.reactServerErrorsByDigest, |
| | (error) => { |
| | |
| | |
| | |
| | |
| | |
| | if (process.env.NODE_ENV === 'production') { |
| | Log.error(error) |
| | } |
| |
|
| | errors.push(error) |
| | } |
| | ) |
| |
|
| | let stream: ReadableStream<Uint8Array> |
| |
|
| | switch (outerWorkUnitStore?.type) { |
| | case 'prerender-runtime': |
| | case 'prerender': |
| | const timeoutAbortController = new AbortController() |
| |
|
| | |
| | |
| | |
| | const timer = setTimeout(() => { |
| | workStore.invalidDynamicUsageError = timeoutError |
| | timeoutAbortController.abort(timeoutError) |
| | }, 50000) |
| |
|
| | const dynamicAccessAbortSignal = |
| | dynamicAccessAsyncStorage.getStore()?.abortController.signal |
| |
|
| | const abortSignal = dynamicAccessAbortSignal |
| | ? AbortSignal.any([ |
| | dynamicAccessAbortSignal, |
| | outerWorkUnitStore.renderSignal, |
| | timeoutAbortController.signal, |
| | ]) |
| | : timeoutAbortController.signal |
| |
|
| | const { prelude } = await prerender( |
| | resultPromise, |
| | clientReferenceManifest.clientModules, |
| | { |
| | environmentName: 'Cache', |
| | filterStackFrame, |
| | signal: abortSignal, |
| | temporaryReferences, |
| | onError(error) { |
| | if (abortSignal.aborted && abortSignal.reason === error) { |
| | return undefined |
| | } |
| |
|
| | return handleError(error) |
| | }, |
| | } |
| | ) |
| |
|
| | clearTimeout(timer) |
| |
|
| | if (timeoutAbortController.signal.aborted) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | stream = new ReadableStream({ |
| | start(controller) { |
| | controller.error(timeoutAbortController.signal.reason) |
| | }, |
| | }) |
| | } else if (dynamicAccessAbortSignal?.aborted) { |
| | |
| | |
| | |
| | const hangingPromise = makeHangingPromise<never>( |
| | outerWorkUnitStore.renderSignal, |
| | workStore.route, |
| | 'dynamic "use cache"' |
| | ) |
| |
|
| | if (outerWorkUnitStore.cacheSignal) { |
| | outerWorkUnitStore.cacheSignal.endRead() |
| | } |
| |
|
| | return { type: 'prerender-dynamic', hangingPromise } |
| | } else { |
| | stream = prelude |
| | } |
| | break |
| | case 'request': |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if ( |
| | process.env.NODE_ENV === 'development' && |
| | outerWorkUnitStore.cacheSignal |
| | ) { |
| | await new Promise((resolve) => setTimeout(resolve)) |
| | } |
| | |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | case undefined: |
| | stream = renderToReadableStream( |
| | resultPromise, |
| | clientReferenceManifest.clientModules, |
| | { |
| | environmentName: 'Cache', |
| | filterStackFrame, |
| | temporaryReferences, |
| | onError: handleError, |
| | } |
| | ) |
| | break |
| | default: |
| | return outerWorkUnitStore satisfies never |
| | } |
| |
|
| | const [returnStream, savedStream] = stream.tee() |
| |
|
| | const pendingCacheEntry = collectResult( |
| | savedStream, |
| | workStore, |
| | cacheContext, |
| | innerCacheStore, |
| | startTime, |
| | errors |
| | ) |
| |
|
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | returnStream.name = 'use cache' |
| | } |
| |
|
| | return { |
| | type: 'cached', |
| | |
| | |
| | |
| | stream: returnStream, |
| | pendingCacheEntry, |
| | } |
| | } |
| |
|
| | function cloneCacheEntry(entry: CacheEntry): [CacheEntry, CacheEntry] { |
| | const [streamA, streamB] = entry.value.tee() |
| | entry.value = streamA |
| | const clonedEntry: CacheEntry = { |
| | value: streamB, |
| | timestamp: entry.timestamp, |
| | revalidate: entry.revalidate, |
| | expire: entry.expire, |
| | stale: entry.stale, |
| | tags: entry.tags, |
| | } |
| | return [entry, clonedEntry] |
| | } |
| |
|
| | async function clonePendingCacheEntry( |
| | pendingCacheEntry: Promise<CacheEntry> |
| | ): Promise<[CacheEntry, CacheEntry]> { |
| | const entry = await pendingCacheEntry |
| | return cloneCacheEntry(entry) |
| | } |
| |
|
| | async function getNthCacheEntry( |
| | split: Promise<[CacheEntry, CacheEntry]>, |
| | i: number |
| | ): Promise<CacheEntry> { |
| | return (await split)[i] |
| | } |
| |
|
| | async function encodeFormData(formData: FormData): Promise<string> { |
| | let result = '' |
| | for (let [key, value] of formData) { |
| | |
| | |
| | |
| | |
| | |
| | result += key.length.toString(16) + ':' + key |
| | let stringValue |
| | if (typeof value === 'string') { |
| | stringValue = value |
| | } else { |
| | |
| | |
| | |
| | const arrayBuffer = await value.arrayBuffer() |
| | if (arrayBuffer.byteLength % 2 === 0) { |
| | stringValue = String.fromCodePoint(...new Uint16Array(arrayBuffer)) |
| | } else { |
| | stringValue = |
| | String.fromCodePoint( |
| | ...new Uint16Array(arrayBuffer, 0, (arrayBuffer.byteLength - 1) / 2) |
| | ) + |
| | String.fromCodePoint( |
| | new Uint8Array(arrayBuffer, arrayBuffer.byteLength - 1, 1)[0] |
| | ) |
| | } |
| | } |
| | result += stringValue.length.toString(16) + ':' + stringValue |
| | } |
| | return result |
| | } |
| |
|
| | function createTrackedReadableStream( |
| | stream: ReadableStream, |
| | cacheSignal: CacheSignal |
| | ) { |
| | const reader = stream.getReader() |
| | return new ReadableStream({ |
| | async pull(controller) { |
| | const { done, value } = await reader.read() |
| | if (done) { |
| | controller.close() |
| | cacheSignal.endRead() |
| | } else { |
| | controller.enqueue(value) |
| | } |
| | }, |
| | }) |
| | } |
| |
|
| | export async function cache( |
| | kind: string, |
| | id: string, |
| | boundArgsLength: number, |
| | originalFn: (...args: unknown[]) => Promise<unknown>, |
| | args: unknown[] |
| | ) { |
| | const isPrivate = kind === 'private' |
| |
|
| | |
| | |
| | const cacheHandler = isPrivate ? undefined : getCacheHandler(kind) |
| |
|
| | if (!isPrivate && !cacheHandler) { |
| | throw new Error('Unknown cache handler: ' + kind) |
| | } |
| |
|
| | const timeoutError = new UseCacheTimeoutError() |
| | Error.captureStackTrace(timeoutError, cache) |
| |
|
| | const wrapAsInvalidDynamicUsageError = ( |
| | error: Error, |
| | workStore: WorkStore |
| | ) => { |
| | Error.captureStackTrace(error, cache) |
| | workStore.invalidDynamicUsageError ??= error |
| |
|
| | return error |
| | } |
| |
|
| | const workStore = workAsyncStorage.getStore() |
| | if (workStore === undefined) { |
| | throw new Error( |
| | '"use cache" cannot be used outside of App Router. Expected a WorkStore.' |
| | ) |
| | } |
| |
|
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| | const name = originalFn.name |
| | let fn = originalFn |
| | let cacheContext: CacheContext |
| |
|
| | if (isPrivate) { |
| | const expression = '"use cache: private"' |
| |
|
| | switch (workUnitStore?.type) { |
| | |
| | case 'prerender': |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | expression |
| | ) |
| | case 'prerender-ppr': |
| | return postponeWithTracking( |
| | workStore.route, |
| | expression, |
| | workUnitStore.dynamicTracking |
| | ) |
| | case 'prerender-legacy': |
| | return throwToInterruptStaticGeneration( |
| | expression, |
| | workStore, |
| | workUnitStore |
| | ) |
| | case 'prerender-client': |
| | throw new InvariantError( |
| | `${expression} must not be used within a client component. Next.js should be preventing ${expression} from being allowed in client components statically, but did not in this case.` |
| | ) |
| | case 'unstable-cache': { |
| | throw wrapAsInvalidDynamicUsageError( |
| | new Error( |
| | |
| | `${expression} must not be used within \`unstable_cache()\`.` |
| | ), |
| | workStore |
| | ) |
| | } |
| | case 'cache': { |
| | throw wrapAsInvalidDynamicUsageError( |
| | new Error( |
| | |
| | `${expression} must not be used within "use cache". It can only be nested inside of another ${expression}.` |
| | ), |
| | workStore |
| | ) |
| | } |
| | case 'request': |
| | case 'prerender-runtime': |
| | case 'private-cache': |
| | cacheContext = { |
| | kind: 'private', |
| | outerWorkUnitStore: workUnitStore, |
| | } |
| | break |
| | case undefined: |
| | throw wrapAsInvalidDynamicUsageError( |
| | new Error( |
| | |
| | `${expression} cannot be used outside of a request context.` |
| | ), |
| | workStore |
| | ) |
| | default: |
| | workUnitStore satisfies never |
| | |
| | |
| | throw new InvariantError(`Unexpected work unit store.`) |
| | } |
| | } else { |
| | switch (workUnitStore?.type) { |
| | case 'prerender-client': |
| | const expression = '"use cache"' |
| | throw new InvariantError( |
| | `${expression} must not be used within a client component. Next.js should be preventing ${expression} from being allowed in client components statically, but did not in this case.` |
| | ) |
| | case 'prerender': |
| | case 'prerender-runtime': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | |
| | |
| | case 'unstable-cache': |
| | case undefined: |
| | cacheContext = { |
| | kind: 'public', |
| | outerWorkUnitStore: workUnitStore, |
| | } |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | |
| | |
| | throw new InvariantError(`Unexpected work unit store.`) |
| | } |
| | } |
| |
|
| | |
| | |
| | const clientReferenceManifest = getClientReferenceManifest() |
| |
|
| | |
| | |
| | |
| | |
| | const buildId = workStore.buildId |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const hmrRefreshHash = |
| | workUnitStore && getHmrRefreshHash(workStore, workUnitStore) |
| |
|
| | const hangingInputAbortSignal = workUnitStore |
| | ? createHangingInputAbortSignal(workUnitStore) |
| | : undefined |
| |
|
| | if (cacheContext.kind === 'private') { |
| | const { outerWorkUnitStore } = cacheContext |
| | switch (outerWorkUnitStore.type) { |
| | case 'prerender-runtime': { |
| | |
| | |
| | if (outerWorkUnitStore.runtimeStagePromise) { |
| | await outerWorkUnitStore.runtimeStagePromise |
| | } |
| | break |
| | } |
| | case 'request': { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | await makeDevtoolsIOAwarePromise( |
| | undefined, |
| | outerWorkUnitStore, |
| | RenderStage.Runtime |
| | ) |
| | } |
| | break |
| | } |
| | case 'private-cache': |
| | break |
| | default: { |
| | outerWorkUnitStore satisfies never |
| | } |
| | } |
| | } |
| |
|
| | let isPageOrLayoutSegmentFunction = false |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (isPageSegmentFunction(args)) { |
| | isPageOrLayoutSegmentFunction = true |
| |
|
| | const [ |
| | { params: outerParams, searchParams: outerSearchParams }, |
| | ...otherOuterArgs |
| | ] = args |
| |
|
| | const props: UseCachePageInnerProps = { |
| | params: outerParams, |
| | |
| | } |
| |
|
| | if (isPrivate) { |
| | |
| | |
| | props.searchParams = outerSearchParams |
| | } |
| |
|
| | args = [props, ...otherOuterArgs] |
| |
|
| | fn = { |
| | [name]: async ( |
| | { |
| | params: _innerParams, |
| | searchParams: innerSearchParams, |
| | }: UseCachePageInnerProps, |
| | ...otherInnerArgs: unknown[] |
| | ) => |
| | originalFn.apply(null, [ |
| | { |
| | params: outerParams, |
| | searchParams: |
| | innerSearchParams ?? |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | makeErroringSearchParamsForUseCache(workStore), |
| | }, |
| | ...otherInnerArgs, |
| | ]), |
| | }[name] as (...args: unknown[]) => Promise<unknown> |
| | } else if (isLayoutSegmentFunction(args)) { |
| | isPageOrLayoutSegmentFunction = true |
| |
|
| | const [ |
| | { params: outerParams, $$isLayout, ...outerSlots }, |
| | ...otherOuterArgs |
| | ] = args |
| |
|
| | |
| | |
| | |
| | |
| | |
| | args = [{ params: outerParams, ...outerSlots }, ...otherOuterArgs] |
| |
|
| | fn = { |
| | [name]: async ( |
| | { |
| | params: _innerParams, |
| | ...innerSlots |
| | }: Omit<UseCacheLayoutProps, '$$isLayout'>, |
| | ...otherInnerArgs: unknown[] |
| | ) => |
| | originalFn.apply(null, [ |
| | { params: outerParams, ...innerSlots }, |
| | ...otherInnerArgs, |
| | ]), |
| | }[name] as (...args: unknown[]) => Promise<unknown> |
| | } |
| |
|
| | if (boundArgsLength > 0) { |
| | if (args.length === 0) { |
| | throw new InvariantError( |
| | `Expected the "use cache" function ${JSON.stringify(fn.name)} to receive its encrypted bound arguments as the first argument.` |
| | ) |
| | } |
| |
|
| | const encryptedBoundArgs = args.shift() as Promise<string> |
| | const boundArgs = await decryptActionBoundArgs(id, encryptedBoundArgs) |
| |
|
| | if (!Array.isArray(boundArgs)) { |
| | throw new InvariantError( |
| | `Expected the bound arguments of "use cache" function ${JSON.stringify(fn.name)} to deserialize into an array, got ${typeof boundArgs} instead.` |
| | ) |
| | } |
| |
|
| | if (boundArgsLength !== boundArgs.length) { |
| | throw new InvariantError( |
| | `Expected the "use cache" function ${JSON.stringify(fn.name)} to receive ${boundArgsLength} bound arguments, got ${boundArgs.length} instead.` |
| | ) |
| | } |
| |
|
| | args.unshift(boundArgs) |
| | } |
| |
|
| | const temporaryReferences = createClientTemporaryReferenceSet() |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const cacheKeyParts: CacheKeyParts = hmrRefreshHash |
| | ? [buildId, id, args, hmrRefreshHash] |
| | : [buildId, id, args] |
| |
|
| | const encodeCacheKeyParts = () => |
| | encodeReply(cacheKeyParts, { |
| | temporaryReferences, |
| | signal: hangingInputAbortSignal, |
| | }) |
| |
|
| | let encodedCacheKeyParts: FormData | string |
| |
|
| | switch (workUnitStore?.type) { |
| | case 'prerender-runtime': |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | case 'prerender': |
| | if (!isPageOrLayoutSegmentFunction) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const dynamicAccessAbortController = new AbortController() |
| |
|
| | encodedCacheKeyParts = await dynamicAccessAsyncStorage.run( |
| | { abortController: dynamicAccessAbortController }, |
| | encodeCacheKeyParts |
| | ) |
| |
|
| | if (dynamicAccessAbortController.signal.aborted) { |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'dynamic "use cache"' |
| | ) |
| | } |
| | break |
| | } |
| | |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | |
| | |
| | |
| | |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | case undefined: |
| | encodedCacheKeyParts = await encodeCacheKeyParts() |
| | break |
| | default: |
| | return workUnitStore satisfies never |
| | } |
| |
|
| | const serializedCacheKey = |
| | typeof encodedCacheKeyParts === 'string' |
| | ? |
| | |
| | encodedCacheKeyParts |
| | : await encodeFormData(encodedCacheKeyParts) |
| |
|
| | let stream: undefined | ReadableStream = undefined |
| |
|
| | |
| | const prerenderResumeDataCache = workUnitStore |
| | ? getPrerenderResumeDataCache(workUnitStore) |
| | : null |
| | const renderResumeDataCache = workUnitStore |
| | ? getRenderResumeDataCache(workUnitStore) |
| | : null |
| |
|
| | if (renderResumeDataCache) { |
| | const cacheSignal = workUnitStore ? getCacheSignal(workUnitStore) : null |
| |
|
| | if (cacheSignal) { |
| | cacheSignal.beginRead() |
| | } |
| | const cachedEntry = renderResumeDataCache.cache.get(serializedCacheKey) |
| | if (cachedEntry !== undefined) { |
| | const existingEntry = await cachedEntry |
| | if (workUnitStore !== undefined && existingEntry !== undefined) { |
| | if ( |
| | existingEntry.revalidate === 0 || |
| | existingEntry.expire < DYNAMIC_EXPIRE |
| | ) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (existingEntry.revalidate === 0) { |
| | debug?.( |
| | 'omitting entry', |
| | serializedCacheKey, |
| | 'from static shell due to revalidate: 0' |
| | ) |
| | } else { |
| | debug?.( |
| | 'omitting entry', |
| | serializedCacheKey, |
| | 'from static shell due to short expire value:', |
| | existingEntry.expire |
| | ) |
| | } |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | } |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'dynamic "use cache"' |
| | ) |
| | case 'prerender-runtime': { |
| | |
| | |
| | |
| | if (workUnitStore.runtimeStagePromise) { |
| | await workUnitStore.runtimeStagePromise |
| | } |
| | break |
| | } |
| | case 'request': { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | |
| | |
| | |
| | |
| | await makeDevtoolsIOAwarePromise( |
| | undefined, |
| | workUnitStore, |
| | RenderStage.Runtime |
| | ) |
| | } |
| | break |
| | } |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | if (existingEntry.stale < RUNTIME_PREFETCH_DYNAMIC_STALE) { |
| | switch (workUnitStore.type) { |
| | case 'prerender-runtime': |
| | |
| | |
| | |
| | |
| | debug?.( |
| | 'omitting entry', |
| | serializedCacheKey, |
| | 'from runtime shell due to short stale value:', |
| | existingEntry.stale |
| | ) |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | } |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'dynamic "use cache"' |
| | ) |
| | case 'request': { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | |
| | |
| | |
| | |
| | await makeDevtoolsIOAwarePromise( |
| | undefined, |
| | workUnitStore, |
| | RenderStage.Dynamic |
| | ) |
| | } |
| | break |
| | } |
| | case 'prerender': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | } |
| |
|
| | debug?.('Resume Data Cache entry found', serializedCacheKey) |
| |
|
| | |
| | |
| | |
| | propagateCacheLifeAndTags(cacheContext, existingEntry) |
| |
|
| | const [streamA, streamB] = existingEntry.value.tee() |
| | existingEntry.value = streamB |
| |
|
| | if (cacheSignal) { |
| | |
| | |
| | stream = createTrackedReadableStream(streamA, cacheSignal) |
| | } else { |
| | stream = streamA |
| | } |
| | } else { |
| | debug?.('Resume Data Cache entry not found', serializedCacheKey) |
| |
|
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | } |
| |
|
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (workUnitStore.allowEmptyStaticShell) { |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'dynamic "use cache"' |
| | ) |
| | } |
| | break |
| | case 'prerender-runtime': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | } |
| | } |
| |
|
| | if (stream === undefined) { |
| | const cacheSignal = workUnitStore ? getCacheSignal(workUnitStore) : null |
| | if (cacheSignal) { |
| | |
| | |
| | cacheSignal.beginRead() |
| | } |
| |
|
| | const lazyRefreshTags = workStore.refreshTagsByCacheKind.get(kind) |
| |
|
| | if (lazyRefreshTags && !isResolvedLazyResult(lazyRefreshTags)) { |
| | await lazyRefreshTags |
| | } |
| |
|
| | let entry: CacheEntry | undefined |
| |
|
| | |
| | if (cacheHandler && !shouldForceRevalidate(workStore, workUnitStore)) { |
| | entry = await cacheHandler.get( |
| | serializedCacheKey, |
| | workUnitStore?.implicitTags?.tags ?? [] |
| | ) |
| | } |
| |
|
| | if (entry) { |
| | const implicitTags = workUnitStore?.implicitTags?.tags ?? [] |
| | let implicitTagsExpiration = 0 |
| |
|
| | if (workUnitStore?.implicitTags) { |
| | const lazyExpiration = |
| | workUnitStore.implicitTags.expirationsByCacheKind.get(kind) |
| |
|
| | if (lazyExpiration) { |
| | const expiration = isResolvedLazyResult(lazyExpiration) |
| | ? lazyExpiration.value |
| | : await lazyExpiration |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if (expiration < Infinity) { |
| | implicitTagsExpiration = expiration |
| | } |
| | } |
| | } |
| |
|
| | if ( |
| | shouldDiscardCacheEntry( |
| | entry, |
| | workStore, |
| | workUnitStore, |
| | implicitTags, |
| | implicitTagsExpiration |
| | ) |
| | ) { |
| | debug?.('discarding expired entry', serializedCacheKey) |
| | entry = undefined |
| | } |
| | } |
| |
|
| | const currentTime = performance.timeOrigin + performance.now() |
| | if ( |
| | workUnitStore !== undefined && |
| | entry !== undefined && |
| | (entry.revalidate === 0 || entry.expire < DYNAMIC_EXPIRE) |
| | ) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (entry.revalidate === 0) { |
| | debug?.( |
| | 'omitting entry', |
| | serializedCacheKey, |
| | 'from static shell due to revalidate: 0' |
| | ) |
| | } else { |
| | debug?.( |
| | 'omitting entry', |
| | serializedCacheKey, |
| | 'from static shell due to short expire value:', |
| | entry.expire |
| | ) |
| | } |
| | if (cacheSignal) { |
| | cacheSignal.endRead() |
| | } |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | 'dynamic "use cache"' |
| | ) |
| | case 'request': { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | |
| | |
| | |
| | |
| | await makeDevtoolsIOAwarePromise( |
| | undefined, |
| | workUnitStore, |
| | RenderStage.Runtime |
| | ) |
| | } |
| | break |
| | } |
| | case 'prerender-runtime': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | if ( |
| | entry === undefined || |
| | currentTime > entry.timestamp + entry.expire * 1000 || |
| | (workStore.isStaticGeneration && |
| | currentTime > entry.timestamp + entry.revalidate * 1000) |
| | ) { |
| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | if (entry) { |
| | if (currentTime > entry.timestamp + entry.expire * 1000) { |
| | debug?.('entry is expired', serializedCacheKey) |
| | } |
| |
|
| | if ( |
| | workStore.isStaticGeneration && |
| | currentTime > entry.timestamp + entry.revalidate * 1000 |
| | ) { |
| | debug?.('static generation, entry is stale', serializedCacheKey) |
| | } |
| | } |
| |
|
| | const result = await generateCacheEntry( |
| | workStore, |
| | cacheContext, |
| | clientReferenceManifest, |
| | encodedCacheKeyParts, |
| | fn, |
| | timeoutError |
| | ) |
| |
|
| | if (result.type === 'prerender-dynamic') { |
| | return result.hangingPromise |
| | } |
| |
|
| | const { stream: newStream, pendingCacheEntry } = result |
| |
|
| | |
| | if (!workStore.isDraftMode) { |
| | let savedCacheEntry |
| |
|
| | if (prerenderResumeDataCache) { |
| | |
| | const split = clonePendingCacheEntry(pendingCacheEntry) |
| | savedCacheEntry = getNthCacheEntry(split, 0) |
| | prerenderResumeDataCache.cache.set( |
| | serializedCacheKey, |
| | getNthCacheEntry(split, 1) |
| | ) |
| | } else { |
| | savedCacheEntry = pendingCacheEntry |
| | } |
| |
|
| | if (cacheHandler) { |
| | const promise = cacheHandler.set(serializedCacheKey, savedCacheEntry) |
| |
|
| | workStore.pendingRevalidateWrites ??= [] |
| | workStore.pendingRevalidateWrites.push(promise) |
| | } |
| | } |
| |
|
| | stream = newStream |
| | } else { |
| | |
| | |
| | if (cacheContext.kind === 'private') { |
| | throw new InvariantError( |
| | `A private cache entry must not be retrieved from the cache handler.` |
| | ) |
| | } |
| |
|
| | propagateCacheLifeAndTags(cacheContext, entry) |
| |
|
| | |
| | stream = entry.value |
| |
|
| | |
| | |
| | if (prerenderResumeDataCache) { |
| | const [entryLeft, entryRight] = cloneCacheEntry(entry) |
| | if (cacheSignal) { |
| | stream = createTrackedReadableStream(entryLeft.value, cacheSignal) |
| | } else { |
| | stream = entryLeft.value |
| | } |
| |
|
| | prerenderResumeDataCache.cache.set( |
| | serializedCacheKey, |
| | Promise.resolve(entryRight) |
| | ) |
| | } else { |
| | |
| | |
| | |
| | cacheSignal?.endRead() |
| | } |
| |
|
| | if (currentTime > entry.timestamp + entry.revalidate * 1000) { |
| | |
| | |
| | |
| | const result = await generateCacheEntry( |
| | workStore, |
| | |
| | { kind: cacheContext.kind, outerWorkUnitStore: undefined }, |
| | clientReferenceManifest, |
| | encodedCacheKeyParts, |
| | fn, |
| | timeoutError |
| | ) |
| |
|
| | if (result.type === 'cached') { |
| | const { stream: ignoredStream, pendingCacheEntry } = result |
| | let savedCacheEntry: Promise<CacheEntry> |
| |
|
| | if (prerenderResumeDataCache) { |
| | const split = clonePendingCacheEntry(pendingCacheEntry) |
| | savedCacheEntry = getNthCacheEntry(split, 0) |
| | prerenderResumeDataCache.cache.set( |
| | serializedCacheKey, |
| | getNthCacheEntry(split, 1) |
| | ) |
| | } else { |
| | savedCacheEntry = pendingCacheEntry |
| | } |
| |
|
| | if (cacheHandler) { |
| | const promise = cacheHandler.set( |
| | serializedCacheKey, |
| | savedCacheEntry |
| | ) |
| |
|
| | workStore.pendingRevalidateWrites ??= [] |
| | workStore.pendingRevalidateWrites.push(promise) |
| | } |
| |
|
| | await ignoredStream.cancel() |
| | } |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const replayConsoleLogs = true |
| |
|
| | const serverConsumerManifest = { |
| | |
| | |
| | |
| | moduleLoading: null, |
| | moduleMap: isEdgeRuntime |
| | ? clientReferenceManifest.edgeRscModuleMapping |
| | : clientReferenceManifest.rscModuleMapping, |
| | serverModuleMap: getServerModuleMap(), |
| | } |
| |
|
| | return createFromReadableStream(stream, { |
| | findSourceMapURL, |
| | serverConsumerManifest, |
| | temporaryReferences, |
| | replayConsoleLogs, |
| | environmentName: 'Cache', |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function isPageSegmentFunction( |
| | args: any[] |
| | ): args is [UseCachePageProps, ...unknown[]] { |
| | const [maybeProps] = args |
| |
|
| | return ( |
| | maybeProps !== null && |
| | typeof maybeProps === 'object' && |
| | (maybeProps as UseCachePageProps).$$isPage === true |
| | ) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function isLayoutSegmentFunction( |
| | args: any[] |
| | ): args is [UseCacheLayoutProps, ...unknown[]] { |
| | const [maybeProps] = args |
| |
|
| | return ( |
| | maybeProps !== null && |
| | typeof maybeProps === 'object' && |
| | (maybeProps as UseCacheLayoutProps).$$isLayout === true |
| | ) |
| | } |
| |
|
| | function shouldForceRevalidate( |
| | workStore: WorkStore, |
| | workUnitStore: WorkUnitStore | undefined |
| | ): boolean { |
| | if (workStore.isOnDemandRevalidate || workStore.isDraftMode) { |
| | return true |
| | } |
| |
|
| | if (workStore.dev && workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'request': |
| | return workUnitStore.headers.get('cache-control') === 'no-cache' |
| | case 'cache': |
| | case 'private-cache': |
| | return workUnitStore.forceRevalidate |
| | case 'prerender-runtime': |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | return false |
| | } |
| |
|
| | function shouldDiscardCacheEntry( |
| | entry: CacheEntry, |
| | workStore: WorkStore, |
| | workUnitStore: WorkUnitStore | undefined, |
| | implicitTags: string[], |
| | implicitTagsExpiration: number |
| | ): boolean { |
| | |
| | |
| | if (entry.timestamp <= implicitTagsExpiration) { |
| | debug?.( |
| | 'entry was created at', |
| | entry.timestamp, |
| | 'before implicit tags were revalidated at', |
| | implicitTagsExpiration |
| | ) |
| |
|
| | return true |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | return false |
| | case 'prerender-runtime': |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | |
| | |
| | if (entry.tags.some((tag) => isRecentlyRevalidatedTag(tag, workStore))) { |
| | return true |
| | } |
| |
|
| | |
| | |
| | if (implicitTags.some((tag) => isRecentlyRevalidatedTag(tag, workStore))) { |
| | return true |
| | } |
| |
|
| | return false |
| | } |
| |
|
| | function isRecentlyRevalidatedTag(tag: string, workStore: WorkStore): boolean { |
| | const { previouslyRevalidatedTags, pendingRevalidatedTags } = workStore |
| |
|
| | |
| | if (previouslyRevalidatedTags.includes(tag)) { |
| | debug?.('tag', tag, 'was previously revalidated') |
| |
|
| | return true |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if (pendingRevalidatedTags?.some((item) => item.tag === tag)) { |
| | debug?.('tag', tag, 'was just revalidated') |
| |
|
| | return true |
| | } |
| |
|
| | return false |
| | } |
| |
|