import crypto from 'crypto' ; import { IncomingHttpHeaders } from 'http' ; import url from 'url' ; import LRUCache from 'lru-cache' ; import mime from 'mime-types' ; import { cacheAddition , connectFactor , fname , maybeSync , namespaceHash , valueFactory , withCoalescedInvoke , } from './util' ; import { createPageCaches } from './page-caches' ; import { PageCache , PageCachesInterface , CacheContent , CacheManagerOptions , CacheResult , CacheContext , CacheConfig , } from './type' ; const MAX_CACHE_EACH_REQ = Number ( process . env . ROUTE_CACHE_LIMIT ) || 10 ; const MAX_SIZE_EACH_CLUSTER = Number ( process . env . CLUSTER_CACHE_LIMIT ) || 100 ; const BASE_LEVEL = 0 ; const QUERY_LEVEL = 1 ; const HEADER_LEVEL = 2 ; const QUERY_HEADER_LEVEL = 3 ; class CacheManager { cache : LRUCache < string , CacheContent > ; cacheOptions : CacheManagerOptions ; constructor ( cacheOptions ) { this . cacheOptions = cacheOptions ; this . cache = new LRUCache ( { max : Math . min ( MAX_SIZE_EACH_CLUSTER , 600 ) * 1024 * 1024 , length ( n ) { const len = n . caches . keys ( ) . reduce ( ( total , cur ) => total + ( n . caches . peek ( cur ) ?. size || 0 ) , 1 ) ; return len ; } , } ) ; } private md5 ( content ) { const md5 = crypto . createHash ( 'md5' ) ; return md5 . update ( content ) . digest ( 'hex' ) ; } private generateRequestKey ( context ) { const { pathname , entry } = context ; return this . md5 ( `${ pathname }_${ entry }` ) ; } private replaceValue ( value , matcher ) { let final = value ; Object . keys ( matcher ) . some ( replacer => { const reg = new RegExp ( matcher [ replacer ] ) ; if ( reg . test ( value ) ) { final = replacer ; return true ; } return false ; } ) ; return final ; } private factor ( keys , obj , matches = { } ) { keys . sort ( ) ; const getValue = valueFactory ( obj ) ; const factorAry = keys . reduce ( ( ary , key ) => { let value = getValue ( key ) || '' ; const matcher = matches [ key ] ; if ( matcher ) { value = this . replaceValue ( value , matcher ) ; } return ary . concat ( [ key , value ] ) ; } , [ ] ) ; return factorAry . join ( ',' ) ; } private queryFactor ( context , data ) { const queryKeys = data . includes ?. query ; const queryMatches = data . matches ?. query ; if ( ! queryKeys || queryKeys . length === 0 ) { return null ; } const requestQuery = context . query ; const queryFactor = this . factor ( queryKeys , requestQuery , queryMatches ) ; return queryFactor ; } private headerFactor ( context , data ) { const headerKeys = data . includes ?. header ; const headerMatches = data . matches ?. header ; if ( ! headerKeys || headerKeys . length === 0 ) { return null ; } const requestHeader = context . headers ; const headerFactor = this . factor ( headerKeys , requestHeader , headerMatches ) ; return headerFactor ; } private readonly find : any = ( ( ) => { { const _this = this ; return { [ fname ( BASE_LEVEL ) ] ( context , cacheKey ) { return _this . md5 ( cacheKey ) ; } , [ fname ( QUERY_LEVEL ) ] ( context , cacheKey , data ) { const queryFactor = _this . queryFactor ( context , data ) ; if ( ! queryFactor ) { return null ; } return _this . md5 ( connectFactor ( cacheKey , queryFactor ) ) ; } , [ fname ( HEADER_LEVEL ) ] ( context , cacheKey , data ) { const headerFactor = _this . headerFactor ( context , data ) ; if ( ! headerFactor ) { return null ; } return _this . md5 ( connectFactor ( cacheKey , headerFactor ) ) ; } , [ fname ( QUERY_HEADER_LEVEL ) ] ( context , cacheKey , data ) { const queryFactor = _this . queryFactor ( context , data ) ; const headerFactor = _this . headerFactor ( context , data ) ; if ( ! queryFactor || ! headerFactor ) { return null ; } return _this . md5 ( connectFactor ( cacheKey , headerFactor , queryFactor ) ) ; } , } ; } } ) ( ) ; private async best ( context , cacheKey , data ) { const { level } = data ; const cacheHash = this . find [ fname ( level ) ] ( context , cacheKey , data ) ; if ( ! cacheHash ) { return null ; } return data . caches . get ( cacheHash ) ; } private createCacheContent ( config , caches ) { return { level : config . level , interval : config . interval , includes : config . includes || null , limit : config . staleLimit , matches : config . matches || null , caches , } ; } async get ( context ) { const cacheKey = this . generateRequestKey ( context ) ; const data = this . cache . get ( cacheKey ) ; if ( ! data ) { return null ; } const dest = await this . best ( context , cacheKey , data ) ; if ( ! dest ) { return null ; } const { expireTime , limitTime , html , cacheHash } = dest ; const isStale = Date . now ( ) - expireTime > 0 ; const isGarbage = limitTime ? Date . now ( ) - limitTime > 0 : false ; return { content : html || '' , contentType : mime . contentType ( 'html' ) as string , isStale , isGarbage , hash : cacheHash , } ; } async set ( context , html , cacheConfig , sync = false ) { if ( ! cacheConfig ) { return false ; } const cacheKey = this . generateRequestKey ( context ) ; let data = this . cache . get ( cacheKey ) ; if ( ! data ) { const caches = await createPageCaches ( MAX_CACHE_EACH_REQ ) ; data = this . createCacheContent ( cacheConfig , caches ) ; } const cacheHash = this . find [ fname ( cacheConfig . level ) ] ( context , cacheKey , data ) ; if ( ! cacheHash ) { return false ; } const cacheSyncOrAsync = async ( ) => { const next = data as CacheContent ; const limit = cacheConfig . staleLimit ; const storeHTML = cacheAddition ( html , cacheHash ) ; const size = storeHTML . length ; await next . caches . set ( cacheHash , { expireTime : Date . now ( ) + cacheConfig . interval * 1000 , limitTime : typeof limit === 'number' ? Date . now ( ) + limit * 1000 : null , cacheHash , html : storeHTML , size , } ) ; this . cache . set ( cacheKey , next ) ; return true ; } ; const doCache = withCoalescedInvoke ( cacheSyncOrAsync ) . bind ( null , namespaceHash ( 'stream' , cacheHash ) , [ ] ) ; return maybeSync ( doCache ) ( sync ) ; } async del ( context , cacheHash ) { const cacheKey = this . generateRequestKey ( context ) ; const data = this . cache . get ( cacheKey ) ; data ?. caches . del ( cacheHash ) ; } } let manager ; export function createCache ( ) { if ( manager ) { return manager ; } manager = new CacheManager ( { max : 0 } ) ; return manager ; } export function destroyCache ( ) { manager = null ! ; }