11import { getDeployStore , GetWithMetadataOptions , Store } from '@netlify/blobs'
2+ import { LRUCache } from 'lru-cache'
23
3- import type { BlobType } from '../shared/cache-types.cjs'
4+ import { type BlobType , estimateBlobSize } from '../shared/cache-types.cjs'
45
6+ import { getRequestContext } from './handlers/request-context.cjs'
57import { getTracer } from './handlers/tracer.cjs'
68
79const FETCH_BEFORE_NEXT_PATCHED_IT = Symbol . for ( 'nf-not-patched-fetch' )
@@ -70,6 +72,54 @@ const encodeBlobKey = async (key: string) => {
7072 return await encodeBlobKeyImpl ( key )
7173}
7274
75+ // lru-cache types don't like using `null` for values, so we use a symbol to represent it and do conversion
76+ // so it doesn't leak outside
77+ const NullValue = Symbol . for ( 'null-value' )
78+ const inMemoryLRUCache = new LRUCache <
79+ string ,
80+ BlobType | typeof NullValue | Promise < BlobType | null >
81+ > ( {
82+ max : 1000 ,
83+ // TODO: get value from CacheHandler configuration
84+ maxEntrySize : 50 * 1024 * 1024 , // 50MB
85+ sizeCalculation : ( valueToStore ) => {
86+ return estimateBlobSize ( valueToStore === NullValue ? null : valueToStore )
87+ } ,
88+ } )
89+
90+ interface RequestSpecificInMemoryCache {
91+ get ( key : string ) : BlobType | null | Promise < BlobType | null > | undefined
92+ set ( key : string , value : BlobType | null | Promise < BlobType | null > ) : void
93+ }
94+
95+ const getRequestSpecificInMemoryCache = ( ) : RequestSpecificInMemoryCache => {
96+ const requestContext = getRequestContext ( )
97+ if ( ! requestContext ) {
98+ // Fallback to a no-op store if we can't find request context
99+ return {
100+ get ( ) : undefined {
101+ // no-op
102+ } ,
103+ set ( ) {
104+ // no-op
105+ } ,
106+ }
107+ }
108+
109+ return {
110+ get ( key ) {
111+ const inMemoryValue = inMemoryLRUCache . get ( `${ requestContext . requestID } :${ key } ` )
112+ if ( inMemoryValue === NullValue ) {
113+ return null
114+ }
115+ return inMemoryValue
116+ } ,
117+ set ( key , value ) {
118+ inMemoryLRUCache . set ( `${ requestContext . requestID } :${ key } ` , value ?? NullValue )
119+ } ,
120+ }
121+ }
122+
73123export const getMemoizedKeyValueStoreBackedByRegionalBlobStore = (
74124 args : GetWithMetadataOptions = { } ,
75125) => {
@@ -78,18 +128,30 @@ export const getMemoizedKeyValueStoreBackedByRegionalBlobStore = (
78128
79129 return {
80130 async get < T extends BlobType > ( key : string , otelSpanTitle : string ) : Promise < T | null > {
81- const blobKey = await encodeBlobKey ( key )
131+ const inMemoryCache = getRequestSpecificInMemoryCache ( )
82132
83- return tracer . withActiveSpan ( otelSpanTitle , async ( span ) => {
133+ const memoizedValue = inMemoryCache . get ( key )
134+ if ( typeof memoizedValue !== 'undefined' ) {
135+ return memoizedValue as T | null | Promise < T | null >
136+ }
137+
138+ const blobKey = await encodeBlobKey ( key )
139+ const getPromise = tracer . withActiveSpan ( otelSpanTitle , async ( span ) => {
84140 span . setAttributes ( { key, blobKey } )
85141 const blob = ( await store . get ( blobKey , { type : 'json' } ) ) as T | null
142+ inMemoryCache . set ( key , blob )
86143 span . addEvent ( blob ? 'Hit' : 'Miss' )
87144 return blob
88145 } )
146+ inMemoryCache . set ( key , getPromise )
147+ return getPromise
89148 } ,
90149 async set ( key : string , value : BlobType , otelSpanTitle : string ) {
91- const blobKey = await encodeBlobKey ( key )
150+ const inMemoryCache = getRequestSpecificInMemoryCache ( )
92151
152+ inMemoryCache . set ( key , value )
153+
154+ const blobKey = await encodeBlobKey ( key )
93155 return tracer . withActiveSpan ( otelSpanTitle , async ( span ) => {
94156 span . setAttributes ( { key, blobKey } )
95157 return await store . setJSON ( blobKey , value )
0 commit comments