import { Observable , of , throwError } from 'rxjs' ; import { startWith , tap } from 'rxjs/operators' ; import { CACHE_KEY_SEPARATOR , DEFAULT_TIMEOUT } from '../constants' ; import { ODataBatchResource , ODataRequest , ODataResponse } from '../resources' ; import { Cache , PathSegmentNames } from '../types' ; export interface ODataCacheEntry < T > { payload : T ; lastRead : number ; timeout : number ; tags : string [ ] ; } export abstract class ODataCache implements Cache { timeout : number ; entries : Map < string , ODataCacheEntry < any > > ; constructor ( { timeout = DEFAULT_TIMEOUT } ) { this . timeout = timeout ; this . entries = new Map < string , ODataCacheEntry < any > > ( ) ; } abstract getResponse ( req ) ; abstract putResponse ( req , res ) ; scope ( req ) { const segments = req . resource . cloneSegments ( ) ; return segments . segments ( { key : true } ) . reduce ( ( acc , s ) => { if ( s . name === PathSegmentNames . entitySet ) acc = [ ... acc , s . path ( ) as string ] ; return acc ; } , [ 'request' ] ) ; } tags ( req , res ) { const tags = [ ] ; const context = res . context ; if ( context . entitySet ) { tags . push ( context . key ? `${ context . entitySet }(${ context . key })` : context . entitySet ) ; } if ( context . type ) tags . push ( context . type ) ; return tags ; } buildEntry < T > ( payload , { timeout , tags } ) { return { payload , lastRead : Date . now ( ) , timeout : timeout || this . timeout , tags : tags || [ ] , } ; } buildKey ( names ) { return names . join ( CACHE_KEY_SEPARATOR ) ; } put < T > ( name , payload , { timeout , scope , tags , } = { } ) { const entry = this . buildEntry < T > ( payload , { timeout , tags } ) ; const key = this . buildKey ( [ ... ( scope || [ ] ) , name ] ) ; this . entries . set ( key , entry ) ; this . forget ( ) ; } get < T > ( name , { scope } = { } ) { const key = this . buildKey ( [ ... ( scope || [ ] ) , name ] ) ; const entry = this . entries . get ( key ) ; return entry !== undefined && ! this . isExpired ( entry ) ? entry . payload : undefined ; } forget ( { name , scope = [ ] , tags = [ ] , } = { } ) { if ( name !== undefined ) scope . push ( name ) ; const key = scope . length > 0 ? this . buildKey ( scope ) : undefined ; this . entries . forEach ( ( entry , k ) => { if ( this . isExpired ( entry ) || ( key !== undefined && k . startsWith ( key ) ) || ( tags . length > 0 && tags . some ( ( t ) => entry . tags . indexOf ( t ) !== - 1 ) ) ) { this . entries . delete ( k ) ; } } ) ; } flush ( ) { this . entries = new Map < string , ODataCacheEntry < any > > ( ) ; } isExpired ( entry ) { return entry . lastRead < Date . now ( ) - ( entry . timeout || this . timeout ) * 1000 ; } handleRequest ( req , res$ ) { return req . isFetch ( ) ? this . handleFetch ( req , res$ ) : req . isMutate ( ) ? this . handleMutate ( req , res$ ) : res$ ; } private handleFetch ( req , res$ ) { const policy = req . fetchPolicy ; const cached = this . getResponse ( req ) ; if ( policy === 'no-cache' ) { return res$ ; } if ( policy === 'cache-only' ) { if ( cached ) { return of ( cached ) ; } else { return throwError ( 'No Cached' ) ; } } if ( policy === 'cache-first' || policy === 'cache-and-network' || policy === 'network-only' ) { res$ = res$ . pipe ( tap ( ( res ) => { if ( res . options . cacheability !== 'no-store' ) this . putResponse ( req , res ) ; } ) ) ; } return cached !== undefined && policy !== 'network-only' ? policy === 'cache-and-network' ? res$ . pipe ( startWith ( cached ) ) : of ( cached ) : res$ ; } private handleMutate ( req , res$ ) { const requests = req . isBatch ( ) ? ( req . resource as ODataBatchResource ) . requests ( ) . filter ( ( r ) => r . isMutate ( ) ) : [ req ] ; for ( var r of requests ) { const scope = this . scope ( r ) ; this . forget ( { scope } ) ; } return res$ ; } }