import Path from 'path' ; import { Writable } from 'stream' ; import chalk from 'chalk' ; import * as LmdbStore from 'lmdb-store' ; const GLOBAL_ATIME = `${ Date . now ( ) }` ; const MINUTE = 1000 * 60 ; const HOUR = MINUTE * 60 ; const DAY = HOUR * 24 ; const dbName = ( db ) => db . name ; export class Cache { private readonly codes : LmdbStore . RootDatabase < string , string > ; private readonly atimes : LmdbStore . Database < string , string > ; private readonly mtimes : LmdbStore . Database < string , string > ; private readonly sourceMaps : LmdbStore . Database < string , string > ; private readonly pathRoot : string ; private readonly prefix : string ; private readonly log ? : Writable ; private readonly timer : NodeJS . Timer ; constructor ( config ) { if ( ! Path . isAbsolute ( config . pathRoot ) ) { throw new Error ( 'cache requires an absolute path to resolve paths relative to' ) ; } this . pathRoot = config . pathRoot ; this . prefix = config . prefix ; this . log = config . log ; this . codes = LmdbStore . open ( config . dir , { name : 'codes' , encoding : 'string' , maxReaders : 500 , } ) ; this . atimes = this . codes . openDB ( 'atimes' , { name : 'atimes' , encoding : 'string' , } ) ; this . mtimes = this . codes . openDB ( 'mtimes' , { name : 'mtimes' , encoding : 'string' , } ) ; this . sourceMaps = this . codes . openDB ( 'sourceMaps' , { name : 'sourceMaps' , encoding : 'string' , } ) ; this . timer = setTimeout ( ( ) => { this . pruneOldKeys ( ) ; } , 30 * MINUTE ) ; if ( typeof this . timer . unref === 'function' ) { this . timer . unref ( ) ; } } getMtime ( path ) { return this . safeGet ( this . mtimes , this . getKey ( path ) ) ; } getCode ( path ) { const key = this . getKey ( path ) ; const code = this . safeGet ( this . codes , key ) ; if ( code !== undefined ) { this . safePut ( this . atimes , key , GLOBAL_ATIME ) ; } return code ; } getSourceMap ( path ) { const map = this . safeGet ( this . sourceMaps , this . getKey ( path ) ) ; if ( typeof map === 'string' ) { return JSON . parse ( map ) ; } } async update ( path , file ) { const key = this . getKey ( path ) ; await Promise . all ( [ this . safePut ( this . atimes , key , GLOBAL_ATIME ) , this . safePut ( this . mtimes , key , file . mtime ) , this . safePut ( this . codes , key , file . code ) , this . safePut ( this . sourceMaps , key , JSON . stringify ( file . map ) ) , ] ) ; } close ( ) { clearTimeout ( this . timer ) ; } private getKey ( path ) { const normalizedPath = Path . sep !== '/' ? Path . relative ( this . pathRoot , path ) . split ( Path . sep ) . join ( '/' ) : Path . relative ( this . pathRoot , path ) ; return `${ this . prefix }${ normalizedPath }` ; } private safeGet < V > ( db , key ) { try { const value = db . get ( key ) ; this . debug ( value === undefined ? 'MISS' : 'HIT' , db , key ) ; return value ; } catch ( error ) { this . logError ( 'GET' , db , key , error ) ; } } private async safePut < V > ( db , key , value ) { try { await db . put ( key , value ) ; this . debug ( 'PUT' , db , key ) ; } catch ( error ) { this . logError ( 'PUT' , db , key , error ) ; } } private debug ( type , db , key ) { if ( this . log ) { this . log . write ( `${ type }   [${ dbName ( db ) }]   ${ String ( key ) }\n` ) ; } } private logError ( type , db , key , error ) { this . debug ( `ERROR/${ type }` , db , `${ String ( key ) }: ${ error . stack }` ) ; process . stderr . write ( chalk . red ( `[@kbn/optimizer/node] ${ type } error [${ dbName ( db ) }/${ String ( key ) }]: ${ error . stack }\n` ) ) ; } private async pruneOldKeys ( ) { try { const ATIME_LIMIT = Date . now ( ) - 30 * DAY ; const BATCH_SIZE = 1000 ; const validKeys = [ ] ; const invalidKeys = [ ] ; for ( const { key , value } of this . atimes . getRange ( ) ) { const atime = parseInt ( `${ value }` , 10 ) ; if ( Number . isNaN ( atime ) || atime < ATIME_LIMIT ) { invalidKeys . push ( key ) ; } else { validKeys . push ( key ) ; } if ( validKeys . length + invalidKeys . length >= BATCH_SIZE ) { const promises = new Set ( ) ; if ( invalidKeys . length ) { for ( const k of invalidKeys ) { promises . add ( this . atimes . remove ( k ) ) ; promises . add ( this . mtimes . remove ( k ) ) ; promises . add ( this . codes . remove ( k ) ) ; promises . add ( this . sourceMaps . remove ( k ) ) ; } } else { promises . add ( new Promise ( ( resolve ) => setTimeout ( resolve , 1 ) ) ) ; } invalidKeys . length = 0 ; validKeys . length = 0 ; await Promise . all ( Array . from ( promises ) ) ; } } } catch { } } }