/**
 * Three supported expiry policies:
 * - indefinite: entries don't expire and must be explicitly removed
 * - absolute: entries expire after the given duration in MS, even if accessed multiple times in the mean time
 * - sliding: entries expire after the given duration in MS of inactivity (i.e. get resets the clock)
 */ export type PromiseCacheExpiry = { policy : "indefinite" ; } | { policy : "absolute" | "sliding" ; durationMs : number ; } ; /**
 * Options for configuring the PromiseCache
 */ export interface PromiseCacheOptions { /** Common expiration policy for all items added to this cache */ expiry ? : PromiseCacheExpiry ; /** If the stored Promise is rejected with a particular error, should the given key be removed? */ removeOnError ? : ( e : any ) => boolean ; } /**
 * Handles garbage collection of expiring cache entries.
 * Not exported.
 */ class GarbageCollector < TKey > { private readonly gcTimeouts = new Map < TKey , ReturnType < typeof setTimeout > > ( ) ; constructor ( private readonly expiry , private readonly cleanup ) { } /**
     * Schedule GC for the given key, as applicable
     */ public schedule ( key ) { if ( this . expiry . policy !== "indefinite" ) { this . gcTimeouts . set ( key , setTimeout ( ( ) => { this . cleanup ( key ) ; this . cancel ( key ) ; } , this . expiry . durationMs ) ) ; } } /**
     * Cancel any pending GC for the given key
     */ public cancel ( key ) { const timeout = this . gcTimeouts . get ( key ) ; if ( timeout !== undefined ) { clearTimeout ( timeout ) ; this . gcTimeouts . delete ( key ) ; } } /**
     * Update any pending GC for the given key, as applicable
     */ public update ( key ) { if ( this . expiry . policy === "sliding" ) { this . cancel ( key ) ; this . schedule ( key ) ; } } } /**
* A specialized cache for async work, allowing you to safely cache the promised result of some async work
* without fear of running it multiple times or losing track of errors.
*/ export class PromiseCache < TKey , TResult > { private readonly cache = new Map < TKey , Promise < TResult > > ( ) ; private readonly gc : GarbageCollector < TKey > ; private readonly removeOnError : ( error : any ) => boolean ; /**
     * Create the PromiseCache with the given options, with the following defaults:
     *
     * expiry: indefinite, removeOnError: true for all errors
     */ constructor ( { expiry = { policy : "indefinite" } , removeOnError = ( ) => true , } = { } ) { this . removeOnError = removeOnError ; this . gc = new GarbageCollector < TKey > ( expiry , ( key ) => this . remove ( key ) ) ; } /**
     * Check if there's anything cached at the given key
     */ public has ( key ) { return this . cache . has ( key ) ; } /**
     * Get the Promise for the given key, or undefined if it's not found.
     * Extend expiry if applicable.
     */ public get ( key ) { if ( this . has ( key ) ) { this . gc . update ( key ) ; } return this . cache . get ( key ) ; } /**
     * Remove the Promise for the given key, returning true if it was found and removed
     */ public remove ( key ) { this . gc . cancel ( key ) ; return this . cache . delete ( key ) ; } param key param asyncFn public async addOrGet ( key , asyncFn ) { let promise = this . get ( key ) ; if ( promise === undefined ) { const safeAsyncFn = async ( ) => asyncFn ( ) ; promise = safeAsyncFn ( ) ; this . cache . set ( key , promise ) ; promise . catch ( ( error ) => { if ( this . removeOnError ( error ) ) { this . remove ( key ) ; } } ) ; this . gc . schedule ( key ) ; } return promise ; } param key param asyncFn public add ( key , asyncFn ) { const alreadyPresent = this . has ( key ) ; this . addOrGet ( key , asyncFn ) . catch ( ( ) => { } ) ; return ! alreadyPresent ; } param key param value public async addValueOrGet ( key , value ) { return this . addOrGet ( key , async ( ) => value ) ; } param key param value public addValue ( key , value ) { return this . add ( key , async ( ) => value ) ; } }