Repository: vercel/next.js
Pull request: fix: mark file system incremental cache as external so it's memory is shared

This moves the in-memory LRU cache on the file system cache handler as external so that it's shared between different instances of the file system cache module.

Changed files:
filename: packages/next/src/server/lib/incremental-cache/file-system-cache.ts
patch: @@ -1,5 +1,5 @@
 import type { RouteMetadata } from '../../../export/routes/types'
-import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from './'
+import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from '.'
 import type { CacheFs } from '../../../shared/lib/utils'
 import {
   CachedRouteKind,
@@ -10,7 +10,7 @@ import {
   type SetIncrementalResponseCacheContext,
 } from '../../response-cache'
 
-import { LRUCache } from '../lru-cache'
+import type { LRUCache } from '../lru-cache'
 import path from '../../../shared/lib/isomorphic/path'
 import {
   NEXT_CACHE_TAGS_HEADER,
@@ -23,6 +23,7 @@ import {
 } from '../../../lib/constants'
 import { isStale, tagsManifest } from './tags-manifest.external'
 import { MultiFileWriter } from '../../../lib/multi-file-writer'
+import { getMemoryCache } from './memory-cache.external'
 
 type FileSystemCacheContext = Omit<
   CacheHandlerContext,
@@ -32,54 +33,31 @@ type FileSystemCacheContext = Omit<
   serverDistDir: string
 }
 
-let memoryCache: LRUCache<CacheHandlerValue> | undefined
-
 export default class FileSystemCache implements CacheHandler {
   private fs: FileSystemCacheContext['fs']
   private flushToDisk?: FileSystemCacheContext['flushToDisk']
   private serverDistDir: FileSystemCacheContext['serverDistDir']
   private revalidatedTags: string[]
-  private debug: boolean
+  private static debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
+  private static memoryCache: LRUCache<CacheHandlerValue> | undefined
 
   constructor(ctx: FileSystemCacheContext) {
     this.fs = ctx.fs
     this.flushToDisk = ctx.flushToDisk
     this.serverDistDir = ctx.serverDistDir
     this.revalidatedTags = ctx.revalidatedTags
-    this.debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
 
     if (ctx.maxMemoryCacheSize) {
-      if (!memoryCache) {
-        if (this.debug) {
+      if (!FileSystemCache.memoryCache) {
+        if (FileSystemCache.debug) {
           console.log('using memory store for fetch cache')
         }
 
-        memoryCache = new LRUCache(ctx.maxMemoryCacheSize, function length({
-          value,
-        }) {
-          if (!value) {
-            return 25
-          } else if (value.kind === CachedRouteKind.REDIRECT) {
-            return JSON.stringify(value.props).length
-          } else if (value.kind === CachedRouteKind.IMAGE) {
-            throw new Error('invariant image should not be incremental-cache')
-          } else if (value.kind === CachedRouteKind.FETCH) {
-            return JSON.stringify(value.data || '').length
-          } else if (value.kind === CachedRouteKind.APP_ROUTE) {
-            return value.body.length
-          }
-          // rough estimate of size of cache value
-          return (
-            value.html.length +
-            (JSON.stringify(
-              value.kind === CachedRouteKind.APP_PAGE
-                ? value.rscData
-                : value.pageData
-            )?.length || 0)
-          )
-        })
+        FileSystemCache.memoryCache = getMemoryCache(ctx.maxMemoryCacheSize)
+      } else if (FileSystemCache.debug) {
+        console.log('memory store already initialized')
       }
-    } else if (this.debug) {
+    } else if (FileSystemCache.debug) {
       console.log('not using memory store for fetch cache')
     }
   }
@@ -92,7 +70,7 @@ export default class FileSystemCache implements CacheHandler {
     let [tags] = args
     tags = typeof tags === 'string' ? [tags] : tags
 
-    if (this.debug) {
+    if (FileSystemCache.debug) {
       console.log('revalidateTag', tags)
     }
 
@@ -111,9 +89,9 @@ export default class FileSystemCache implements CacheHandler {
     const [key, ctx] = args
     const { kind } = ctx
 
-    let data = memoryCache?.get(key)
+    let data = FileSystemCache.memoryCache?.get(key)
 
-    if (this.debug) {
+    if (FileSystemCache.debug) {
       if (kind === IncrementalCacheKind.FETCH) {
         console.log('get', key, ctx.tags, kind, !!data)
       } else {
@@ -182,7 +160,7 @@ export default class FileSystemCache implements CacheHandler {
             // TODO: remove this when we can send the tags
             // via header on GET same as SET
             if (!tags?.every((tag) => storedTags?.includes(tag))) {
-              if (this.debug) {
+              if (FileSystemCache.debug) {
                 console.log('tags vs storedTags mismatch', tags, storedTags)
               }
               await this.set(key, data.value, {
@@ -291,7 +269,7 @@ export default class FileSystemCache implements CacheHandler {
         }
 
         if (data) {
-          memoryCache?.set(key, data)
+          FileSystemCache.memoryCache?.set(key, data)
         }
       } catch {
         return null
@@ -345,12 +323,12 @@ export default class FileSystemCache implements CacheHandler {
     data: IncrementalCacheValue | null,
     ctx: SetIncrementalFetchCacheContext | SetIncrementalResponseCacheContext
   ) {
-    memoryCache?.set(key, {
+    FileSystemCache.memoryCache?.set(key, {
       value: data,
       lastModified: Date.now(),
     })
 
-    if (this.debug) {
+    if (FileSystemCache.debug) {
       console.log('set', key)
     }
 

filename: packages/next/src/server/lib/incremental-cache/memory-cache.external.ts
patch: @@ -0,0 +1,34 @@
+import type { CacheHandlerValue } from '.'
+import { CachedRouteKind } from '../../response-cache/types'
+import { LRUCache } from '../lru-cache'
+
+let memoryCache: LRUCache<CacheHandlerValue> | undefined
+
+export function getMemoryCache(maxMemoryCacheSize: number) {
+  if (!memoryCache) {
+    memoryCache = new LRUCache(maxMemoryCacheSize, function length({ value }) {
+      if (!value) {
+        return 25
+      } else if (value.kind === CachedRouteKind.REDIRECT) {
+        return JSON.stringify(value.props).length
+      } else if (value.kind === CachedRouteKind.IMAGE) {
+        throw new Error('invariant image should not be incremental-cache')
+      } else if (value.kind === CachedRouteKind.FETCH) {
+        return JSON.stringify(value.data || '').length
+      } else if (value.kind === CachedRouteKind.APP_ROUTE) {
+        return value.body.length
+      }
+      // rough estimate of size of cache value
+      return (
+        value.html.length +
+        (JSON.stringify(
+          value.kind === CachedRouteKind.APP_PAGE
+            ? value.rscData
+            : value.pageData
+        )?.length || 0)
+      )
+    })
+  }
+
+  return memoryCache
+}