import { observable , makeObservable , action } from 'mobx' ; import { Dependency } from '@cloudbeaver/core-di' ; import { Executor , ExecutorInterrupter , IExecutor , IExecutorHandler , ISyncExecutor , SyncExecutor , TaskScheduler } from '@cloudbeaver/core-executor' ; import { MetadataMap } from '@cloudbeaver/core-utils' ; export interface ICachedResourceMetadata { outdated : boolean ; loading : boolean ; includes : string [ ] ; exception : Error | null ; } export interface IDataError < TParam > { param : TParam ; exception : Error ; } export interface IParamAlias < TParam > { param : TParam ; getAlias : ( param : TParam ) => TParam ; } export type CachedResourceData < TResource > = TResource extends CachedResource < infer T , any , any , any > ? T : never ; export type CachedResourceParam < TResource > = TResource extends CachedResource < any , infer T , any , any > ? T : never ; export type CachedResourceKey < TResource > = TResource extends CachedResource < any , any , infer T , any > ? T : never ; export type CachedResourceContext < TResource > = TResource extends CachedResource < any , any , any , infer T > ? T : void ; export abstract class CachedResource < TData , TParam , TKey = TParam , TContext extends string [ ] | void = void > extends Dependency { data : TData ; readonly onDataOutdated : ISyncExecutor < TParam > ; readonly onDataUpdate : ISyncExecutor < TParam > ; readonly onDataError : ISyncExecutor < IDataError < TParam > > ; readonly beforeLoad : IExecutor < TParam > ; protected metadata : MetadataMap < TKey , ICachedResourceMetadata > ; protected loadedKeys : TParam [ ] ; protected defaultIncludes : string [ ] ; protected get loading ( ) : boolean { return this . scheduler . executing ; } protected scheduler : TaskScheduler < TParam > ; protected paramAliases : Array < IParamAlias < TParam > > ; protected logActivity : boolean ; protected outdateWaitList : TParam [ ] ; constructor ( defaultValue , defaultIncludes = [ ] ) { super ( ) ; this . lock = this . lock . bind ( this ) ; this . includes = this . includes . bind ( this ) ; this . loadingTask = this . loadingTask . bind ( this ) ; this . logActivity = false ; this . loadedKeys = [ ] ; this . defaultIncludes = defaultIncludes ; this . paramAliases = [ ] ; this . outdateWaitList = [ ] ; this . metadata = new MetadataMap ( ( ) => ( observable ( { outdated : true , loading : false , exception : null , includes : observable ( [ ... this . defaultIncludes ] ) , } , undefined , { deep : false } ) ) ) ; this . scheduler = new TaskScheduler ( this . lock ) ; this . data = defaultValue ; this . beforeLoad = new Executor ( null , this . lock ) ; this . onDataOutdated = new SyncExecutor < TParam > ( null ) ; this . onDataUpdate = new SyncExecutor < TParam > ( null ) ; this . onDataError = new SyncExecutor < IDataError < TParam > > ( null ) ; if ( this . logActivity ) { this . spy ( this . onDataUpdate , 'onDataUpdate' ) ; this . spy ( this . onDataError , 'onDataError' ) ; } makeObservable < CachedResource < TData , TParam , TKey , TContext > , 'loader' | 'loadedKeys' > ( this , { loadedKeys : observable , data : observable , loader : action , markDataLoading : action , markDataLoaded : action , markDataError : action , markOutdated : action , markUpdated : action , } ) ; } sync ( resource ) { resource . outdateResource ( this ) ; if ( this . logActivity ) { } if ( this . logActivity ) { } this . preloadResource ( resource ) ; } updateResource < T = TParam > ( resource , map ? ) { this . onDataUpdate . addHandler ( param => { try { if ( this . logActivity ) { console . group ( this . getActionPrefixedName ( ' update - ' + resource . getName ( ) ) ) ; } if ( map ) { param = map ( param ) as any as TParam ; } resource . markUpdated ( param as any as T ) ; } finally { if ( this . logActivity ) { console . groupEnd ( ) ; } } } ) ; return this ; } outdateResource < T = TParam > ( resource , map ? ) { this . onDataOutdated . addHandler ( param => { try { if ( this . logActivity ) { console . group ( this . getActionPrefixedName ( ' outdate - ' + resource . getName ( ) ) ) ; } if ( map ) { param = map ( param ) as any as TParam ; } resource . markOutdated ( param as any as T ) ; } finally { if ( this . logActivity ) { console . groupEnd ( ) ; } } } ) ; return this ; } preloadResource < T = TParam > ( resource , map ? ) { this . beforeLoad . addHandler ( async ( param ) => { try { if ( this . logActivity ) { console . group ( this . getActionPrefixedName ( ' preload - ' + resource . getName ( ) ) ) ; } if ( map ) { param = map ( param ) as any as TParam ; } await resource . load ( param as any as T , undefined ) ; } finally { if ( this . logActivity ) { console . groupEnd ( ) ; } } } ) ; return this ; } before ( handler ) { this . beforeLoad . addHandler ( async ( param , contexts ) => { try { if ( this . logActivity ) { console . group ( this . getActionPrefixedName ( ' preload - ' + handler . name ) ) ; } await handler ( param , contexts ) ; } finally { if ( this . logActivity ) { console . groupEnd ( ) ; } } } ) ; return this ; } abstract isLoaded ( param , context ) ; isAlias ( key ) { return this . paramAliases . some ( alias => this . includes ( key , alias . param ) ) ; } waitLoad ( ) { return this . scheduler . wait ( ) ; } isLoading ( ) { return this . loading ; } isAliasLoaded ( key ) { return this . loadedKeys . some ( loadedKey => this . includes ( key , loadedKey ) ) ; } isIncludes ( key , includes ) { key = this . transformParam ( key ) ; const metadata = this . getMetadata ( key ) ; if ( includes ) { return includes . every ( include => metadata . includes . includes ( include ) ) ; } return true ; } getMetadata ( param ) { const metadata = this . metadata . get ( param as any as TKey ) ; return metadata ; } getException ( param ) { param = this . transformParam ( param ) ; return this . getMetadata ( param ) . exception ; } isOutdated ( param ) { if ( this . isAlias ( param ) && ! this . isAliasLoaded ( param ) ) { return true ; } param = this . transformParam ( param ) ; const metadata = this . getMetadata ( param ) ; return metadata . outdated ; } isDataLoading ( param ) { param = this . transformParam ( param ) ; return this . getMetadata ( param ) . loading ; } markDataLoading ( param , context ) { param = this . transformParam ( param ) ; const metadata = this . getMetadata ( param ) ; metadata . loading = true ; } markDataLoaded ( param , includes ) { param = this . transformParam ( param ) ; if ( includes ) { this . commitIncludes ( param , includes ) ; } const metadata = this . getMetadata ( param ) ; metadata . loading = false ; } markDataError ( exception , param , context ) { if ( this . isAlias ( param ) && ! this . isAliasLoaded ( param ) ) { this . loadedKeys . push ( param ) ; } param = this . transformParam ( param ) ; const metadata = this . getMetadata ( param ) ; metadata . exception = exception ; metadata . outdated = false ; this . onDataError . execute ( { param , exception } ) ; } cleanError ( param ) { param = this . transformParam ( param ) ; const metadata = this . getMetadata ( param ) ; metadata . exception = null ; } markOutdated ( param ) { if ( this . scheduler . isExecuting ( param ) && ! this . outdateWaitList . some ( key => this . includes ( param , key ) ) ) { this . outdateWaitList . push ( param ) ; return ; } this . markOutdatedSync ( param ) ; } markUpdated ( param ) { if ( this . isAlias ( param ) && ! this . isAliasLoaded ( param ) ) { this . loadedKeys . push ( param ) ; } param = this . transformParam ( param ) ; const metadata = this . getMetadata ( param ) ; metadata . outdated = false ; } addAlias ( param , getAlias ) { this . paramAliases . push ( { param , getAlias } ) ; } transformParam ( param ) { let deep = 0 ; transform : if ( deep < 10 ) { for ( const alias of this . paramAliases ) { if ( this . includes ( alias . param , param ) ) { param = alias . getAlias ( param ) ; deep ++ ; break transform ; } } } else { console . warn ( 'CachedResource: parameter transform was stopped' ) ; } return param ; } async refresh ( param , context ) { await this . loadData ( param , true , context ) ; return this . data ; } async load ( param , context ) { await this . loadData ( param , false , context ) ; return this . data ; } getIncludes ( key ? ) { if ( key === undefined ) { return this . defaultIncludes ; } key = this . transformParam ( key ) ; const metadata = this . getMetadata ( key ) ; return metadata . includes ; } getIncludesMap ( key ? , includes = this . defaultIncludes ) { const keyIncludes = this . getIncludes ( key ) ; return [ 'customIncludeBase' , ... includes , ... keyIncludes ] . reduce < any > ( ( map , key ) => { map [ key ] = true ; return map ; } , { } ) ; } protected resetIncludes ( ) { for ( const metadata of this . metadata . values ( ) ) { metadata . includes = [ ... this . defaultIncludes ] ; } } protected commitIncludes ( key , includes ) { key = this . transformParam ( key ) ; const metadata = this . getMetadata ( key ) ; for ( const include of includes ) { if ( ! metadata . includes . includes ( include ) ) { metadata . includes . push ( include ) ; } } } protected setData ( data ) { this . data = data ; } protected markOutdatedSync ( param ) { if ( this . isAlias ( param ) ) { const index = this . loadedKeys . findIndex ( key => this . includes ( param , key ) ) ; if ( index >= 0 ) { this . loadedKeys . splice ( index , 1 ) ; } } param = this . transformParam ( param ) ; const metadata = this . getMetadata ( param ) ; metadata . outdated = true ; this . onDataOutdated . execute ( param ) ; } protected lock ( param , second ) { if ( this . isAlias ( param ) || this . isAlias ( second ) ) { return true ; } return this . includes ( param , second ) ; } protected includes ( param , second ) { if ( param === second ) { return true ; } param = this . transformParam ( param ) ; second = this . transformParam ( second ) ; return param === second ; } protected abstract loader ( param , context ) ; protected async performUpdate < T > ( param , context , update ) ; protected async performUpdate < T > ( param , context , update , exitCheck ) ; protected async performUpdate < T > ( param , context , update , exitCheck ? ) { const contexts = await this . beforeLoad . execute ( param ) ; if ( ExecutorInterrupter . isInterrupted ( contexts ) ) { return ; } await this . scheduler . waitRelease ( param ) ; if ( exitCheck ?. ( ) ) { return ; } let loaded = false ; return this . scheduler . schedule ( param , async ( ) => { if ( exitCheck ?. ( ) ) { return ; } this . markDataLoading ( param , context ) ; try { const result = await this . taskWrapper ( param , context , update ) ; loaded = true ; return result ; } finally { this . markDataLoaded ( param , context ) ; } } , { before : ( ) => { this . markOutdatedSync ( param ) ; } , success : ( ) => { if ( loaded ) { this . cleanError ( param ) ; this . onDataUpdate . execute ( param ) ; } } , error : exception => this . markDataError ( exception , param , context ) , } ) ; } protected async loadData ( param , refresh , context ) { const contexts = await this . beforeLoad . execute ( param ) ; if ( ExecutorInterrupter . isInterrupted ( contexts ) && ! refresh ) { return ; } if ( ! refresh ) { await this . scheduler . waitRelease ( param ) ; if ( this . isLoaded ( param , context ) && ! this . isOutdated ( param ) ) { return ; } } let loaded = false ; await this . scheduler . schedule ( param , async ( ) => { if ( this . isLoaded ( param , context ) && ! this . isOutdated ( param ) ) { return ; } this . markDataLoading ( param , context ) ; try { const result = await this . taskWrapper ( param , context , this . loadingTask ) ; loaded = true ; return result ; } finally { this . markDataLoaded ( param , context ) ; } } , { before : ( ) => { if ( refresh ) { this . markOutdatedSync ( param ) ; } } , success : async ( ) => { if ( loaded ) { this . cleanError ( param ) ; this . onDataUpdate . execute ( param ) ; } } , error : exception => this . markDataError ( exception , param , context ) , } ) ; } private async loadingTask ( param , context ) { this . setData ( await this . loader ( param , context ) ) ; } private async taskWrapper < T > ( param , context , promise ) { if ( this . logActivity ) { console . log ( this . getActionPrefixedName ( 'loading' ) ) ; } const value = await promise ( param , context ) ; this . markUpdated ( param ) ; for ( let i = 0 ; i < this . outdateWaitList . length ; i ++ ) { const key = this . outdateWaitList [ i ] ; this . markOutdatedSync ( key ) ; } this . outdateWaitList = [ ] ; return value ; } public getName ( ) { return this . constructor . name ; } protected logLock = ( action ) : IExecutorHandler < any > => ( ) => { console . log ( this . getActionPrefixedName ( action ) ) ; } ; private logName = ( action ) : IExecutorHandler < any > => ( ) => { console . log ( this . getActionPrefixedName ( action ) ) ; } ; protected getActionPrefixedName ( action ) { return this . constructor . name + ': ' + action ; } private logInterrupted = ( action ) : IExecutorHandler < any > => ( data , contexts ) => { if ( ExecutorInterrupter . isInterrupted ( contexts ) ) { console . log ( this . getActionPrefixedName ( action ) + 'interrupted' ) ; } } ; private spy ( executor , action ) { executor . addHandler ( this . logName ( action ) ) . addPostHandler ( this . logInterrupted ( action ) ) ; } }