import * as fs from "fs" ; import { api , id , utils , config } from "../index" ; export namespace cache { export enum CacheErrorMessages { locked = "Object locked" , notFound = "Object not found" , expired = "Object expired" } export interface CacheObject { key : string ; value : any ; expireTimestamp : number ; createdAt : number ; lastReadAt : number ; readAt ? : number ; } export interface CacheOptions { expireTimeMS ? : number ; retry ? : boolean | number ; } export const redisPrefix = config . general . cachePrefix ; export const lockPrefix = config . general . lockPrefix ; export const lockDuration = config . general . lockDuration ; export const scanCount = config . redis . scanCount || 1000 ; export const lockRetry = 100 ; export function client ( ) { if ( api . redis . clients && api . redis . clients . client ) { return api . redis . clients . client ; } else { throw new Error ( "redis not connected, cache cannot be used" ) ; } } let lockNameOverride ; export function lockName ( ) { if ( lockNameOverride ) return lockNameOverride ; return id ; } export function overrideLockName ( name ) { lockNameOverride = name ; } pattern export async function getKeys ( pattern , count = scanCount , keysAry = [ ] , cursor = 0 ) { const [ newCursor , matches ] = await client ( ) . scan ( cursor , "match" , pattern , "count" , count ) ; if ( matches && matches . length > 0 ) keysAry = keysAry . concat ( matches ) ; if ( newCursor === "0" ) return keysAry ; return cache . getKeys ( pattern , count , keysAry , parseInt ( newCursor ) ) ; } /**
     * Returns all the keys in redis which are under this Actionhero namespace.  Potentially very slow.
     */ export async function keys ( optionalScopePrefix = "" ) { return getKeys ( redisPrefix + optionalScopePrefix + "*" ) ; } /**
     * Returns all the locks in redis which are under this Actionhero namespace.  Potentially slow.
     */ export async function locks ( optionalScopePrefix = "" ) { return getKeys ( lockPrefix + optionalScopePrefix + "*" ) ; } /**
     * Returns the number of keys in redis which are under this Actionhero namespace.  Potentially very slow.
     */ export async function size ( pattern = redisPrefix + "*" ) { let length = 0 ; const keys = await cache . getKeys ( pattern ) ; if ( keys ) length = keys . length ; return length ; } /**
     * Removes all keys in redis which are under this Actionhero namespace.  Potentially very slow.
     * Returns the deleted keys.
     */ export async function clear ( pattern = redisPrefix + "*" ) { const keys = await cache . getKeys ( pattern ) ; const pipelineArgs = [ ] ; keys . forEach ( ( key ) => { pipelineArgs . push ( [ "del" , key ] ) ; } ) ; await client ( ) . pipeline ( pipelineArgs ) . exec ( ) ; return keys ; } /**
     * Write the current concents of redis (only the keys in Actionhero's namespace) to a file.
     */ export async function dumpWrite ( file ) { const data = { } ; const jobs = [ ] ; const keys = await cache . keys ( ) ; keys . forEach ( ( key ) => { jobs . push ( client ( ) . get ( key ) . then ( ( content ) => { data [ key ] = content ; } ) ) ; } ) ; await Promise . all ( jobs ) ; fs . writeFileSync ( file , JSON . stringify ( data ) ) ; return keys . length ; } /**
     * Load in contents for redis (and api.cache) saved to a file
     * Warning! Any existing keys in redis (under this Actionhero namespace) will be removed.
     */ export async function dumpRead ( file ) { const jobs = [ ] ; await cache . clear ( ) ; const fileData = fs . readFileSync ( file ) . toString ( ) ; const data = JSON . parse ( fileData ) ; const count = Object . keys ( data ) . length ; const saveDumpedElement = async ( key , content ) => { const parsedContent = JSON . parse ( content ) ; await client ( ) . set ( key , content ) ; if ( parsedContent . expireTimestamp ) { const expireTimeSeconds = Math . ceil ( ( parsedContent . expireTimestamp - new Date ( ) . getTime ( ) ) / 1000 ) ; await client ( ) . expire ( key , expireTimeSeconds ) ; } } ; Object . keys ( data ) . forEach ( ( key ) => { const content = data [ key ] ; jobs . push ( saveDumpedElement ( key , content ) ) ; } ) ; await Promise . all ( jobs ) ; return count ; } /**
     * Load an item from the cache.  Will throw an error if the item named by `key` cannot be found.
     * Automatically handles `api.cache.redisPrefix`
     */ export async function load ( key , options = { } ) { let cacheObj ; let lockOk = await cache . checkLock ( key , options . retry ) ; if ( lockOk !== true ) throw new Error ( CacheErrorMessages . locked ) ; let cachedStringifiedObjet = await client ( ) . get ( `${ redisPrefix }${ key }` ) ; try { cacheObj = JSON . parse ( cachedStringifiedObjet ) ; } catch ( e ) { } if ( ! cacheObj ) throw new Error ( CacheErrorMessages . notFound ) ; if ( cacheObj . expireTimestamp && cacheObj . expireTimestamp < new Date ( ) . getTime ( ) ) { throw new Error ( CacheErrorMessages . expired ) ; } const lastReadAt = cacheObj . readAt ; let expireTimeSeconds ; cacheObj . readAt = new Date ( ) . getTime ( ) ; if ( cacheObj . expireTimestamp ) { if ( options . expireTimeMS ) { cacheObj . expireTimestamp = new Date ( ) . getTime ( ) + options . expireTimeMS ; expireTimeSeconds = Math . ceil ( options . expireTimeMS / 1000 ) ; } else { expireTimeSeconds = Math . floor ( ( cacheObj . expireTimestamp - new Date ( ) . getTime ( ) ) / 1000 ) ; } } lockOk = await cache . checkLock ( key , options . retry ) ; if ( lockOk !== true ) throw new Error ( CacheErrorMessages . locked ) ; await client ( ) . set ( redisPrefix + key , JSON . stringify ( cacheObj ) ) ; if ( expireTimeSeconds ) { await client ( ) . expire ( redisPrefix + key , expireTimeSeconds ) ; return { key , value : cacheObj . value , expireTimestamp : cacheObj . expireTimestamp , createdAt : cacheObj . createdAt , lastReadAt , } ; } else { return { key , value : cacheObj . value , expireTimestamp : cacheObj . expireTimestamp , createdAt : cacheObj . createdAt , lastReadAt , } ; } } /**
     * Delete an item in the cache.  Will throw an error if the item named by `key` is locked.
     * Automatically handles `api.cache.redisPrefix`
     */ export async function destroy ( key ) { const lockOk = await cache . checkLock ( key , null ) ; if ( ! lockOk ) throw new Error ( CacheErrorMessages . locked ) ; const count = await client ( ) . del ( redisPrefix + key ) ; let response = true ; if ( count !== 1 ) { response = false ; } return response ; } /**
     * Save an item in the cache.  If an item is already in the cache with the same key, it will be overwritten.  Throws an error if the object is already in the cache and is locked.
     * Automatically handles `api.cache.redisPrefix`
     */ export async function save ( key , value , expireTimeMS ? ) { let expireTimeSeconds = null ; let expireTimestamp = null ; if ( expireTimeMS !== null ) { expireTimeSeconds = Math . ceil ( expireTimeMS / 1000 ) ; expireTimestamp = new Date ( ) . getTime ( ) + expireTimeMS ; } const cacheObj = { value : value , expireTimestamp : expireTimestamp , createdAt : new Date ( ) . getTime ( ) , readAt : null as number , } ; const lockOk = await cache . checkLock ( key , null ) ; if ( ! lockOk ) throw new Error ( CacheErrorMessages . locked ) ; await client ( ) . set ( redisPrefix + key , JSON . stringify ( cacheObj ) ) ; if ( expireTimeSeconds ) { await client ( ) . expire ( redisPrefix + key , expireTimeSeconds ) ; } return true ; } /**
     * Push an item to a shared queue/list in redis.
     * Automatically handles `api.cache.redisPrefix`
     */ export async function push ( key , item ) { const object = JSON . stringify ( { data : item } ) ; await client ( ) . rpush ( redisPrefix + key , object ) ; return true ; } /**
     * Pop (get) an item to a shared queue/list in redis.
     * Automatically handles `api.cache.redisPrefix`
     */ export async function pop ( key ) { const object = await client ( ) . lpop ( redisPrefix + key ) ; if ( ! object ) { return null ; } const item = JSON . parse ( object ) ; return item . data ; } /**
     * Check how many items are stored in a shared queue/list in redis.
     */ export async function listLength ( key ) { return client ( ) . llen ( redisPrefix + key ) ; } /**
     * Lock an item in redis (can be a list or a saved item) to this Actionhero process.
     */ export async function lock ( key , expireTimeMS = lockDuration ) { const lockOk = await cache . checkLock ( key , null ) ; if ( ! lockOk ) { return false ; } const result = await client ( ) . setnx ( lockPrefix + key , lockName ( ) ) ; if ( ! result ) { return false ; } await client ( ) . expire ( lockPrefix + key , Math . ceil ( expireTimeMS / 1000 ) ) ; return true ; } /**
     * Unlock an item in redis (can be a list or a saved item) which was previously locked by this Actionhero process.
     */ export async function unlock ( key ) { const lockOk = await cache . checkLock ( key , null ) ; if ( ! lockOk ) { return false ; } await client ( ) . del ( lockPrefix + key ) ; return true ; } export async function checkLock ( key , retry = false , startTime = new Date ( ) . getTime ( ) ) { const lockedBy = await client ( ) . get ( lockPrefix + key ) ; if ( lockedBy === lockName ( ) || lockedBy === null ) { return true ; } else { const delta = new Date ( ) . getTime ( ) - startTime ; if ( ! retry || delta > retry ) { return false ; } await utils . sleep ( lockRetry ) ; return cache . checkLock ( key , retry , startTime ) ; } } }