import State from '../State' ; import Relation from '../utility/Relation' ; import Operation from './Operation' ; /** Abstract interface representing a subset of the {@linkcode Manager} class caching functionality that can be maintained in a simple key-value store. */ export class Cache { param state async setState ( state ) { return false ; } param query async unset ( query ) { return false ; } param query async getState ( query ) { return null ; } param paths async getQueries ( paths ) { return [ ] ; } } import LocalCache from '../cache/LocalCache' ; /** Singleton which manages state of all known _paths_. It provides two functionalities: 1) Executes {@linkcode Operation|Operations} to either cache the resulting {@linkcode State} in conjuction with its {@linkcode State.$query|_query_} or invalidate its dependent _paths_, and 2) Accepts subscriptions to cached {@linkcode State} via _queries_ and notifies relevant subscribers whenever cached {@linkcode State} change. */ export default class Manager { /** The instance of Manager (or derived class) currently accessible via the Manager singleton, using {@linkcode Mangager.access}. */ private static instance : Manager ; /** The maximum number of queries that can be cached before an {@linkcode Manager.evict|eviction} occurs. */ protected maxSize : number = 25000 ; /** An object that implements the {@linkcode Cache} interface, which will be used to store cached {@linkcode State} and dependency graphs. By defualt, this is an instance of {@linkcode LocalCache}, but when operating in a clustered environment, this should overriden with a global cache. */ private globals : Cache = new LocalCache ( ) ; /** Stores _queries_ which are actively being calculated, along with a promise resolving to their eventual state. */ private active : Map < string , Promise < State > > = new Map ( ) ; /** Maps _queries_ to cacheable {@linkcode Operation|operations} that will be invoked to recalculate their {@linkcode Manager.states|state}. */ private operations : Map < string , Operation > = new Map ( ) ; /** Maps _subscribers_ (represented by callback functions) to _queries_ and vice versa. Whenever a _query_ is {@linkcode Manager.cache|recalculated}, its associated _subscribers_ will be invoked with the resulting state. */ private subscriptions : Relation < Function , string > = new Relation ( ) ; /** A set containing functions ```(path, internal) => {...}``` which will be invoked when any _path_ is invalidated, with the invalidated _path_ string and a boolean denoting whether the invalidating initiated by a caller other than the {@linkcode Manager}. */ private listeners : Set < Function > = new Set ( ) ; constructor ( maxSize = null ) { if ( maxSize ) { this . maxSize = maxSize ; } } /** Executes the given {@linkcode Operation|operation} and stores it as well as the resulting {@linkcode State|state} in association with the operation's {@linkcode Operation.query|query}.
     */ private async cache ( operation ) { const { query } = operation ; if ( this . active . has ( query ) ) { return this . active . get ( query ) ; } this . operations . delete ( query ) ; this . active . set ( query , operation ( ) ) ; const state = await this . active . get ( query ) ; this . active . delete ( query ) ; if ( state . isError ( ) ) { await this . unset ( query ) ; return state ; } this . operations . set ( query , operation ) ; await this . globals . setState ( state ) ; await this . evict ( ) ; for ( const client of this . subscriptions . to ( query ) ) { client ( query , state ) ; } return state ; } param query private async evict ( ) { if ( this . operations . size > this . maxSize ) { for ( const query of this . operations . keys ( ) ) { if ( ! this . subscriptions . to ( query ) . next ( ) . value ) { await this . unset ( query ) ; return true ; } } } return false ; } param query private async unset ( query ) { this . operations . delete ( query ) ; this . globals . unset ( query ) ; for ( const client of this . subscriptions . to ( query ) ) { this . unsubscribe ( client , query ) ; client ( query , null ) ; } } param path param override async invalidate ( paths , override = false ) { const queries = await this . globals . getQueries ( paths ) ; await Promise . all ( queries . map ( async ( query ) => { if ( ! this . operations . has ( query ) ) { return null ; } if ( ! this . subscriptions . to ( query ) . next ( ) . value ) { return this . unset ( query ) ; } return this . cache ( this . operations . get ( query ) ) ; } ) ) ; if ( ! override ) { this . listeners . forEach ( ( client ) => { client ( paths , State . OK ( ) ) ; } ) ; } } /** Safely invokes an {@linkcode Operation|operation}, caching the resulting {@linkcode State|state} if applicable or otherwise invalidating dependent paths. */ async execute ( operation ) { const { query } = operation ; if ( operation . isCacheable ) { if ( this . operations . has ( query ) ) { return this . globals . getState ( query ) ; } return this . cache ( operation ) ; } const state = await operation ( ) ; await this . invalidate ( operation . dependents ) ; return state ; } param callback listen ( callback ) { this . listeners . add ( callback ) ; } /** Unregisters a listener function ```callback```. */ unlisten ( callback ) { this . listeners . delete ( callback ) ; } param callback subscribe ( client , query = null ) { if ( ! this . operations . has ( query ) ) { return false ; } this . subscriptions . link ( client , query ) ; return true ; } /** Unregisters a ```subscriber``` from the given ```query```, or from all subscribed queries if no ```query``` is provided. */ unsubscribe ( client , query = null ) { this . subscriptions . unlink ( client , query ) ; } /** Returns the Manager singleton instance. If the singleton instance has not been manually defined using {@linkcode Manager.initialize}, a default instance of {@linkcode Manager} will be created. */ static access ( ) { if ( ! this . instance ) { this . initialize ( new Manager ( ) ) ; } return this . instance ; } param instance static initialize ( instance ) { this . instance = instance ; } }