import { Injectable } from '@angular/core' ; import { Headers , Response , RequestOptionsArgs , ResponseType , ResponseContentType } from '@angular/http' ; import { Observable } from 'rxjs/Observable' ; import 'rxjs/add/operator/catch' ; import 'rxjs/add/operator/map' ; import 'rxjs/add/operator/share' ; import 'rxjs/add/observable/of' ; import 'rxjs/add/observable/throw' ; import { ArmService } from './arm.service' ; import { ClearCache } from '../decorators/cache.decorator' ; import { Url } from './../../shared/Utilities/url' ; import { AiService } from './ai.service' ; import { BroadcastService } from './broadcast.service' ; import { PortalResources } from '../models/portal-resources' ; import { BroadcastEvent } from '../models/broadcast-event' ; import { errorIds } from './../models/error-ids' ; import { ErrorEvent } from './../models/error-event' ; import { TranslateService } from '@ngx-translate/core' ; import { Guid } from '../Utilities/Guid' ; import { Constants } from '../models/constants' ; export interface CacheItem { id : string ; value : any ; expireTime : number ; etag : string ; responseObservable : Observable < any > ; isRawResponse : boolean ; } export class Cache { [ key ] : CacheItem ; } @ Injectable ( ) export class CacheService { private _cache : Cache ; private _expireMS = parseInt ( Url . getParameterByName ( null , 'appsvc.debug.cacheinterval' ) , 10 ) || 60000 ; private _cleanUpMS = 3 * this . _expireMS ; public cleanUpEnabled = true ; constructor ( private _armService , private _aiService , private _broadcastService , private _translateService ) { this . _cache = new Cache ( ) ; setTimeout ( this . _cleanUp . bind ( this ) , this . _cleanUpMS ) ; } getArm ( resourceId , force ? , apiVersion ? , invokeApi ? ) { const url = this . _armService . getArmUrl ( resourceId , apiVersion ? apiVersion : this . _armService . antaresApiVersion20181101 ) ; return this . send ( url , 'GET' , force , null , null , invokeApi ) ; } deleteArm ( resourceId , apiVersion ? , invokeApi ? ) { const url = this . _armService . getArmUrl ( resourceId , apiVersion ? apiVersion : this . _armService . antaresApiVersion20181101 ) ; return this . send ( url , 'DELETE' , true , null , null , invokeApi ) ; } postArm ( resourceId , force ? , apiVersion ? , content ? , cacheKeyPrefix ? , responseContentType ? ) { const url = this . _armService . getArmUrl ( resourceId , apiVersion ? apiVersion : this . _armService . antaresApiVersion20181101 ) ; return this . send ( url , 'POST' , force , null , content , null , cacheKeyPrefix , responseContentType ) ; } putArm ( resourceId , apiVersion ? , content ? ) { const url = this . _armService . getArmUrl ( resourceId , apiVersion ? apiVersion : this . _armService . antaresApiVersion20181101 ) ; return this . _armService . send ( 'PUT' , url , content ) ; } patchArm ( resourceId , apiVersion ? , content ? ) { const url = this . _getArmUrl ( resourceId , apiVersion ) ; return this . _armService . send ( 'PATCH' , url , content ) ; } get ( url , force ? , headers ? , invokeApi ? , responseContentType ? ) { return this . send ( url , 'GET' , force , headers , null , invokeApi , null , responseContentType ) ; } post ( url , force ? , headers ? , content ? ) { return this . send ( url , 'POST' , force , headers , content ) ; } put ( url , headers ? , content ? ) { return this . send ( url , 'PUT' , true , headers , content ) ; } patch ( url , headers ? , content ? ) { return this . send ( url , 'PATCH' , true , headers , content ) ; } head ( url , force ? , headers ? ) { return this . send ( url , 'HEAD' , force , headers ) ; } delete ( url , headers ? ) { return this . send ( url , 'DELETE' , true , headers ) ; } options ( url , force ? , headers ? ) { return this . send ( url , 'OPTIONS' , force , headers ) ; } @ ClearCache ( 'clearAllCachedData' ) clearCache ( ) { this . _cache = new Cache ( ) ; } clearArmIdCachePrefix ( armIdPrefix ) { const prefix = `${ this . _armService . armUrl }${ armIdPrefix }` ; this . clearCachePrefix ( prefix ) ; } clearCachePrefix ( prefix ) { prefix = prefix . toLowerCase ( ) ; for ( const key in this . _cache ) { if ( key . startsWith ( prefix ) && this . _cache . hasOwnProperty ( key ) ) { delete this . _cache [ key ] ; } } } public send ( url , method , force , headers ? , content ? , invokeApi ? , keyPrefix ? , responseContentType ? ) { if ( url . startsWith ( 'http://' ) ) { url = url . replace ( 'http://' , 'https://' ) ; const parts = url . split ( '/' ) ; let domain = parts [ 2 ] ; if ( domain . indexOf ( '.' ) === - 1 ) { domain = domain + '.azurewebsites.net' ; } url = url . replace ( parts [ 2 ] , domain ) ; } if ( url . indexOf ( Constants . serviceHost ) > - 1 && ! headers ) { headers = new Headers ( ) ; } const key = keyPrefix ? keyPrefix . toLowerCase ( ) + url . toLowerCase ( ) : url . toLowerCase ( ) ; const item = this . _cache [ key ] ; if ( item && item . responseObservable ) { return item . responseObservable ; } else if ( ! force && item && Date . now ( ) < item . expireTime ) { return Observable . of ( item . isRawResponse ? item . value : this . _clone ( item . value ) ) ; } else { const etag = item && item . etag ; if ( etag && headers ) { headers . append ( 'If-None-Match' , etag ) ; } const responseObs = this . _armService . send ( method , url , content , etag , headers , invokeApi , responseContentType ) . retryWhen ( e => e . scan ( ( errorCount , err ) => { if ( errorCount >= 12 || err . status !== 429 ) { throw err ; } this . _aiService . trackEvent ( '429-retry-cache-service' , { retryCount : `${ errorCount }` , } ) ; return errorCount + 1 ; } , 0 ) . delay ( 5000 ) ) . map ( response => { return this . _mapAndCacheResponse ( method , response , key ) ; } ) . catch ( error => { if ( error . status === 304 ) { this . _cache [ key ] = this . createCacheItem ( key , item . value , error . headers . get ( 'ETag' ) , null , item . isRawResponse ) ; return Observable . of ( item . isRawResponse ? item . value : this . _clone ( item . value ) ) ; } else { delete this . _cache [ key ] ; return Observable . throw ( error ) ; } } ) . catch ( e => this . tryPassThroughController ( e , method , url , content , { headers : headers , responseType : responseContentType } ) . map ( response => { return this . _mapAndCacheResponse ( method , response , key ) ; } ) ) . share ( ) ; this . _cache [ key ] = this . createCacheItem ( key , item && item . value , etag , responseObs , false ) ; return responseObs ; } } public _mapAndCacheResponse ( method , response , key ) { if ( method === 'PUT' || method === 'PATCH' || method === 'DELETE' ) { delete this . _cache [ key ] ; } else { const responseETag = response . headers . get ( 'ETag' ) ; this . _cache [ key ] = this . createCacheItem ( key , response , responseETag , null , true ) ; } return response ; } private tryPassThroughController ( error , method , url , body , options ) { if ( error . status === 0 && error . type === ResponseType . Error ) { return this . _armService . send ( 'GET' , '/api/ping' , null , null , new Headers ( ) ) . catch ( _ => { this . _broadcastService . broadcast < ErrorEvent > ( BroadcastEvent . Error , { message : this . _translateService . instant ( PortalResources . error_appOffline ) , errorId : errorIds . applicationOffline , resourceId : url , } ) ; return Observable . throw ( error ) ; } ) . mergeMap ( _ => { const headers = { } ; if ( options && options . headers ) { options . headers . forEach ( ( v , n ) => { headers [ n ] = v . join ( ',' ) ; } ) ; } if ( typeof body !== 'string' ) { body = JSON . stringify ( body ) ; } const passThroughBody = { method : method , url : url , body : body , headers : headers , } ; const startTime = performance . now ( ) ; const logDependency = ( success , status ) => { const endTime = performance . now ( ) ; this . _aiService . trackDependency ( Guid . newGuid ( ) , passThroughBody . method , passThroughBody . url , passThroughBody . url , endTime - startTime , success , status ) ; } ; return this . _armService . send ( 'POST' , '/api/passthrough' , passThroughBody , null , new Headers ( ) , false , options . responseType ) . do ( r => logDependency ( true , r . status ) , e => logDependency ( false , e . status ) ) . catch ( ( e ) => { if ( e . status === 400 ) { let content = null ; try { content = e . json ( ) ; } catch ( e ) { content = null ; } if ( content && content . reason && content . reason === 'PassThrough' ) { this . _aiService . trackEvent ( errorIds . passThroughApiError , content ) ; return Observable . throw ( error ) ; } } return Observable . throw ( e ) ; } ) ; } ) ; } else { return Observable . throw ( error ) ; } } private _clone ( obj ) { return JSON . parse ( JSON . stringify ( obj ) ) ; } public createCacheItem ( id , value , etag , responseObs , isRawResponse ) { return { id : id , value : value , expireTime : Date . now ( ) + this . _expireMS , etag : etag , responseObservable : responseObs , isRawResponse : isRawResponse , } ; } private _cleanUp ( ) { if ( ! this . cleanUpEnabled ) { return ; } for ( const key in this . _cache ) { if ( this . _cache . hasOwnProperty ( key ) ) { const item = this . _cache [ key ] ; if ( Date . now ( ) >= item . expireTime ) { delete this . _cache [ key ] ; } } } setTimeout ( this . _cleanUp . bind ( this ) , this . _cleanUpMS ) ; } private _getArmUrl ( resourceId , apiVersion ? ) { const url = `${ this . _armService . armUrl }${ resourceId }` ; return this . _updateQueryString ( url , 'api-version' , apiVersion ? apiVersion : this . _armService . antaresApiVersion20181101 ) ; } private _updateQueryString ( uri , key , value ) { const re = new RegExp ( '([?&])' + key + '=.*?(&|$)' , 'i' ) ; const separator = uri . indexOf ( '?' ) !== - 1 ? '&' : '?' ; if ( uri . match ( re ) ) { return uri . replace ( re , '$1' + key + '=' + value + '$2' ) ; } else { return uri + separator + key + '=' + value ; } } }