Просмотр исходного кода

Refactor and improve API calls caching process

There's now only one place where API calls are cached in the source code
and in the local storage.

A 15 minute default caching age is used for all cached API calls.

If a call is loaded from cache, a `console.log` appears stating that.
Sergiu Miclea 6 лет назад
Родитель
Сommit
c672a276e4

+ 3 - 3
src/components/organisms/EditReplica/EditReplica.jsx

@@ -119,7 +119,7 @@ class EditReplica extends React.Component<Props, State> {
     let loadAllOptions = async (type: 'source' | 'destination') => {
       let endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
       await this.loadOptions(endpoint, type, useCache)
-      this.loadExtraOptions(null, type)
+      this.loadExtraOptions(null, type, useCache)
     }
     if (this.hasSourceOptions()) {
       loadAllOptions('source')
@@ -142,7 +142,7 @@ class EditReplica extends React.Component<Props, State> {
     })
   }
 
-  loadExtraOptions(field?: ?Field, type: 'source' | 'destination') {
+  loadExtraOptions(field?: ?Field, type: 'source' | 'destination', useCache?: boolean) {
     let endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
     let env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
     let stateEnv = type === 'source' ? this.state.sourceData : this.state.destinationData
@@ -165,7 +165,7 @@ class EditReplica extends React.Component<Props, State> {
       optionsType: type,
       endpointId: endpoint.id,
       providerName: endpoint.type,
-      useCache: true,
+      useCache,
       envData,
     })
   }

+ 5 - 3
src/sources/InstanceSource.js

@@ -65,10 +65,10 @@ class InstanceSource {
     return response.data.instances
   }
 
-  async loadInstances(endpointId: string): Promise<Instance[]> {
+  async loadInstances(endpointId: string, cache?: ?boolean): Promise<Instance[]> {
     Api.cancelRequests(endpointId)
     let url = `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances`
-    let response = await Api.send({ url, cancelId: endpointId })
+    let response = await Api.send({ url, cancelId: endpointId, cache })
     return response.data.instances
   }
 
@@ -77,7 +77,8 @@ class InstanceSource {
     instanceName: string,
     reqId: number,
     quietError?: boolean,
-    env?: any
+    env?: any,
+    cache?: ?boolean,
   ): Promise<{ instance: Instance, reqId: number }> {
     let url = `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances/${btoa(instanceName)}`
     if (env) {
@@ -87,6 +88,7 @@ class InstanceSource {
       url,
       cancelId: `instanceDetail-${reqId}`,
       quietError,
+      cache,
     })
     return { instance: response.data.instance, reqId }
   }

+ 6 - 1
src/sources/NetworkSource.js

@@ -22,12 +22,17 @@ import { servicesUrl } from '../constants'
 class NetworkSource {
   async loadNetworks(enpointId: string, environment: ?{ [string]: mixed }, options?: {
     quietError?: boolean,
+    useLocalStorage?: boolean,
   }): Promise<Network[]> {
     let url = `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${enpointId}/networks`
     if (environment) {
       url = `${url}?env=${btoa(JSON.stringify(environment))}`
     }
-    let response = await Api.send({ url, quietError: options && options.quietError })
+    let response = await Api.send({
+      url,
+      quietError: options && options.quietError,
+      cache: options && options.useLocalStorage,
+    })
     let networks: Network[] = response.data.networks.filter(n => n.name.indexOf('coriolis-migrnet') === -1)
     networks.sort((a, b) => a.name.localeCompare(b.name))
     return networks

+ 10 - 4
src/sources/ProviderSource.js

@@ -34,18 +34,21 @@ class ProviderSource {
     return response.data.providers
   }
 
-  async loadOptionsSchema(providerName: string, schemaType: 'migration' | 'replica', optionsType: 'source' | 'destination'): Promise<Field[]> {
+  async loadOptionsSchema(providerName: string, schemaType: 'migration' | 'replica', optionsType: 'source' | 'destination', useCache?: ?boolean): Promise<Field[]> {
     let schemaTypeInt = schemaType === 'migration' ?
       optionsType === 'source' ? providerTypes.SOURCE_MIGRATION : providerTypes.TARGET_MIGRATION :
       optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
 
-    let response = await Api.get(`${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`)
+    let response = await Api.send({
+      url: `${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`,
+      cache: useCache,
+    })
     let schema = optionsType === 'source' ? response.data.schemas.source_environment_schema : response.data.schemas.destination_environment_schema
     let fields = SchemaParser.optionsSchemaToFields(providerName, schema)
     return fields
   }
 
-  async getOptionsValues(optionsType: 'source' | 'destination', endpointId: string, envData: ?{ [string]: mixed }): Promise<OptionValues[]> {
+  async getOptionsValues(optionsType: 'source' | 'destination', endpointId: string, envData: ?{ [string]: mixed }, cache?: ?boolean): Promise<OptionValues[]> {
     let envString = ''
     if (envData) {
       envString = `?env=${btoa(JSON.stringify(envData))}`
@@ -53,7 +56,10 @@ class ProviderSource {
     let callName = optionsType === 'source' ? 'source-options' : 'destination-options'
     let fieldName = optionsType === 'source' ? 'source_options' : 'destination_options'
 
-    let response = await Api.get(`${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/${callName}${envString}`)
+    let response = await Api.send({
+      url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/${callName}${envString}`,
+      cache,
+    })
     return response.data[fieldName]
   }
 }

+ 2 - 88
src/stores/InstanceStore.js

@@ -22,66 +22,6 @@ import InstanceSource from '../sources/InstanceSource'
 import ApiCaller from '../utils/ApiCaller'
 import configLoader from '../utils/Config'
 
-class InstanceLocalStorage {
-  static saveInstancesToLocalStorage(endpointId: string, instances: Instance[]) {
-    let instancesLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instances') || '[]')
-    let endpointIndex = instancesLocalStorage.findIndex(i => i.endpointId === endpointId)
-    if (endpointIndex > -1) {
-      instancesLocalStorage.splice(endpointIndex, 1)
-    }
-    instancesLocalStorage.push({ endpointId, instances })
-    localStorage.setItem('instances', JSON.stringify(instancesLocalStorage))
-  }
-
-  static loadInstancesFromLocalStorage(endpointId: string): ?Instance[] {
-    let instancesLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instances') || '[]')
-    let endpointInstances = instancesLocalStorage.find(i => i.endpointId === endpointId)
-    if (!endpointInstances) {
-      return null
-    }
-    return endpointInstances.instances
-  }
-
-  static saveDetailsToLocalStorage(endpointId: string, instance: Instance) {
-    let instancesDetailsLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instancesDetails') || '[]')
-    let endpointInstancesIndex = instancesDetailsLocalStorage.findIndex(i => i.endpointId === endpointId)
-    let endpointDetails = { endpointId, instances: [] }
-    if (endpointInstancesIndex > -1) {
-      endpointDetails = instancesDetailsLocalStorage[endpointInstancesIndex]
-      instancesDetailsLocalStorage.splice(endpointInstancesIndex, 1)
-    }
-
-    let localInstanceIndex = endpointDetails.instances.findIndex(i => i.id === instance.id)
-    if (localInstanceIndex > -1) {
-      endpointDetails.instances.splice(localInstanceIndex, 1)
-    }
-    endpointDetails.instances.push(instance)
-    instancesDetailsLocalStorage.push(endpointDetails)
-    localStorage.setItem('instancesDetails', JSON.stringify(instancesDetailsLocalStorage))
-  }
-
-  static loadDetailsFromLocalStorage(endpointId: string, instancesInfo: Instance[]): ?Instance[] {
-    let instancesDetailsLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instancesDetails') || '[]')
-    let endpointStorage = instancesDetailsLocalStorage.find(i => i.endpointId === endpointId)
-    if (!endpointStorage || !endpointStorage.instances) {
-      return null
-    }
-    let isValid = true
-    let instances: Instance[] = []
-    instancesInfo.forEach(instance => {
-      let storageInstance = endpointStorage.instances.find(i => instance.id === i.id)
-      if (storageInstance) {
-        instances.push(storageInstance)
-      } else {
-        isValid = false
-      }
-    })
-    if (isValid) {
-      return instances
-    }
-    return null
-  }
-}
 
 class InstanceStore {
   @observable instancesLoading = false
@@ -163,20 +103,12 @@ class InstanceStore {
     this.instancesLoading = true
     this.lastEndpointId = endpointId
 
-    let endpointInstances = InstanceLocalStorage.loadInstancesFromLocalStorage(endpointId)
-    if (endpointInstances) {
-      this.backgroundInstances = endpointInstances
-      this.instancesLoading = false
-      return
-    }
-
     try {
-      let instances = await InstanceSource.loadInstances(endpointId)
+      let instances = await InstanceSource.loadInstances(endpointId, true)
       if (endpointId !== this.lastEndpointId) {
         return
       }
       this.loadInstancesSuccess(instances)
-      InstanceLocalStorage.saveInstancesToLocalStorage(endpointId, instances)
     } catch (ex) {
       if (endpointId !== this.lastEndpointId) {
         return
@@ -287,10 +219,6 @@ class InstanceStore {
     InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
 
     instancesInfo.sort((a, b) => (a.instance_name || a.name).localeCompare(b.instance_name || b.name))
-    let hash = i => `${i.instance_name || i.name}-${i.id || endpointId}`
-    if (useLocalStorage && this.instancesDetails.map(hash).join('_') === instancesInfo.map(hash).join('_')) {
-      return
-    }
 
     let count = instancesInfo.length
     this.loadingInstancesDetails = true
@@ -299,22 +227,12 @@ class InstanceStore {
     this.instancesDetailsCount = count
     this.instancesDetailsRemaining = count
 
-    if (useLocalStorage) {
-      let storageInstances = InstanceLocalStorage.loadDetailsFromLocalStorage(endpointId, instancesInfo)
-      if (storageInstances) {
-        this.loadingInstancesDetails = false
-        this.instancesDetails = storageInstances
-        this.loadingInstancesDetails = false
-        this.instancesDetailsRemaining = 0
-        return
-      }
-    }
     await new Promise(resolve => {
       Promise.all(instancesInfo.map(async instanceInfo => {
         try {
           let resp: { instance: Instance, reqId: number } =
             await InstanceSource.loadInstanceDetails(endpointId, instanceInfo.instance_name || instanceInfo.name,
-              this.reqId, quietError, env)
+              this.reqId, quietError, env, useLocalStorage)
           if (resp.reqId !== this.reqId) {
             return
           }
@@ -328,8 +246,6 @@ class InstanceStore {
             }
           })
 
-          InstanceLocalStorage.saveDetailsToLocalStorage(endpointId, resp.instance)
-
           runInAction(() => {
             this.instancesDetails = [
               ...this.instancesDetails,
@@ -345,11 +261,9 @@ class InstanceStore {
             this.instancesDetailsRemaining -= 1
             this.loadingInstancesDetails = this.instancesDetailsRemaining > 0
           })
-
           if (!err || err.reqId !== this.reqId) {
             return
           }
-
           if (count === 0) {
             resolve()
           }

+ 1 - 39
src/stores/NetworkStore.js

@@ -18,27 +18,6 @@ import { observable, action, runInAction } from 'mobx'
 import type { Network } from '../types/Network'
 import NetworkSource from '../sources/NetworkSource'
 
-class NetworkLocalStorage {
-  static loadNetworksFromStorage(id: string): ?Network[] {
-    let networkStorage: { id: string, networks: Network[] }[] = JSON.parse(localStorage.getItem('networks') || '[]')
-    let endpointNetworks = networkStorage.find(n => n.id === id)
-    if (!endpointNetworks) {
-      return null
-    }
-    return endpointNetworks.networks
-  }
-
-  static saveNetworksToLocalStorage(id: string, networks: Network[]) {
-    let networkStorage: { id: string, networks: Network[] }[] = JSON.parse(localStorage.getItem('networks') || '[]')
-    let endpointNetworksIndex = networkStorage.findIndex(n => n.id === id)
-    if (endpointNetworksIndex > -1) {
-      networkStorage.splice(endpointNetworksIndex, 1)
-    }
-    networkStorage.push({ id, networks })
-    localStorage.setItem('networks', JSON.stringify(networkStorage))
-  }
-}
-
 class NetworkStore {
   @observable networks: Network[] = []
   @observable loading: boolean = false
@@ -49,32 +28,15 @@ class NetworkStore {
     useLocalStorage?: boolean,
     quietError?: boolean,
   }) {
-    let id = `${endpointId}-${btoa(JSON.stringify(environment))}`
-    if (this.cachedId === id && options && options.useLocalStorage) {
-      return
-    }
-
     this.loading = true
-
-    if (options && options.useLocalStorage) {
-      let networkStorage = NetworkLocalStorage.loadNetworksFromStorage(id)
-      if (networkStorage) {
-        this.loading = false
-        this.networks = networkStorage
-        this.cachedId = id
-        return
-      }
-    }
-
     this.networks = []
+
     try {
       let networks = await NetworkSource.loadNetworks(endpointId, environment, options)
       runInAction(() => {
         this.loading = false
         this.networks = networks
-        this.cachedId = id
       })
-      NetworkLocalStorage.saveNetworksToLocalStorage(id, networks)
     } catch (e) {
       this.loading = false
     }

+ 4 - 41
src/stores/ProviderStore.js

@@ -139,9 +139,6 @@ class ProviderStore {
     }
   }
 
-  destinationSchemaCache: { [string]: Field[] } = {}
-  sourceSchemaCache: { [string]: Field[] } = {}
-
   @action async loadOptionsSchema(options: {
     providerName: string,
     schemaType: 'migration' | 'replica',
@@ -149,24 +146,11 @@ class ProviderStore {
     useCache?: boolean,
   }): Promise<void> {
     let { schemaType, providerName, optionsType, useCache } = options
-    let cacheData: any
-
-    let cacheKey = `${providerName}-${schemaType}`
     if (optionsType === 'source') {
-      cacheData = this.sourceSchemaCache[cacheKey]
       this.lastSourceSchemaType = schemaType
     } else {
-      cacheData = this.destinationSchemaCache[cacheKey]
       this.lastDestinationSchemaType = schemaType
     }
-    if (useCache && cacheData) {
-      if (optionsType === 'source') {
-        this.sourceSchema = [...cacheData]
-      } else {
-        this.destinationSchema = [...cacheData]
-      }
-      return
-    }
 
     if (optionsType === 'source') {
       this.sourceSchemaLoading = true
@@ -175,8 +159,8 @@ class ProviderStore {
     }
 
     try {
-      let fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, schemaType, optionsType)
-      this.loadOptionsSchemaSuccess(fields, cacheKey, optionsType)
+      let fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, schemaType, optionsType, useCache)
+      this.loadOptionsSchemaSuccess(fields, optionsType)
     } catch (err) {
       throw err
     } finally {
@@ -184,13 +168,11 @@ class ProviderStore {
     }
   }
 
-  @action loadOptionsSchemaSuccess(fields: Field[], cacheKey: string, optionsType: 'source' | 'destination') {
+  @action loadOptionsSchemaSuccess(fields: Field[], optionsType: 'source' | 'destination') {
     if (optionsType === 'source') {
       this.sourceSchema = fields
-      this.sourceSchemaCache[cacheKey] = fields
     } else {
       this.destinationSchema = fields
-      this.destinationSchemaCache[cacheKey] = fields
     }
   }
 
@@ -202,8 +184,6 @@ class ProviderStore {
     }
   }
 
-  cache: { key: string, data: OptionValues[] }[] = []
-
   async getOptionsValues(config: {
     optionsType: 'source' | 'destination',
     endpointId: string,
@@ -223,28 +203,11 @@ class ProviderStore {
       return []
     }
 
-    if (useCache) {
-      let key = `${endpointId}-${providerName}-${optionsType}-${JSON.stringify(envData)}`
-      let cacheItem = this.cache.find(c => c.key === key)
-      if (cacheItem) {
-        this.getOptionsValuesSuccess(optionsType, providerName, cacheItem.data)
-        this.getOptionsValuesDone(optionsType)
-        return cacheItem.data
-      }
-    }
-
     this.getOptionsValuesStart(optionsType)
 
     try {
-      let options = await ProviderSource.getOptionsValues(optionsType, endpointId, envData)
+      let options = await ProviderSource.getOptionsValues(optionsType, endpointId, envData, useCache)
       this.getOptionsValuesSuccess(optionsType, providerName, options)
-      if (useCache) {
-        let key = `${endpointId}-${providerName}-${optionsType}-${JSON.stringify(envData)}`
-        if (this.cache.length > 20) {
-          this.cache.splice(0)
-        }
-        this.cache.push({ key, data: options })
-      }
       return options
     } catch (err) {
       console.error(err)

+ 8 - 0
src/types/Cache.js

@@ -0,0 +1,8 @@
+// @flow
+
+export type Cache = {
+  [key: string]: {
+    data: any,
+    createdAt: string,
+  }
+}

+ 14 - 3
src/utils/ApiCaller.js

@@ -15,9 +15,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 
 import axios from 'axios'
-import type { AxiosXHRConfig, $AxiosXHR } from 'axios'
+import type { AxiosXHRConfig } from 'axios'
 import cookie from 'js-cookie'
 
+import cacher from './Cacher'
 import logger from './ApiLogger'
 import notificationStore from '../stores/NotificationStore'
 
@@ -35,6 +36,8 @@ type RequestOptions = {
   responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream',
   quietError?: boolean,
   skipLog?: ?boolean,
+  cache?: ?boolean,
+  cacheFor?: ?number,
 }
 
 let cancelables: Cancelable[] = []
@@ -71,11 +74,16 @@ class ApiCaller {
     cancelables = cancelables.filter(r => r.requestId !== cancelRequestId)
   }
 
-  get(url: string): Promise<$AxiosXHR<any>> {
+  get(url: string): Promise<any> {
     return this.send({ url })
   }
 
-  send(options: RequestOptions): Promise<$AxiosXHR<any>> {
+  send(options: RequestOptions): Promise<any> {
+    let cachedData = options.cache ? cacher.load({ key: options.url, maxAge: options.cacheFor }) : null
+    if (cachedData) {
+      return Promise.resolve({ data: cachedData })
+    }
+
     return new Promise((resolve, reject) => {
       const axiosOptions: AxiosXHRConfig<any> = {
         url: options.url,
@@ -111,6 +119,9 @@ class ApiCaller {
             requestStatus: 200,
           })
         }
+        if (options.cache) {
+          cacher.save({ key: options.url, data: response.data })
+        }
         resolve(response)
       }).catch(error => {
         if (error.response) {

+ 44 - 0
src/utils/Cacher.js

@@ -0,0 +1,44 @@
+// @flow
+
+import type { Cache } from '../types/Cache'
+
+const DEFAULT_MAX_AGE = 15 * 60 * 1000 // 15 minutes
+const STORE = 'api-cacher'
+
+class Cacher {
+  load(options: {
+    key: string,
+    maxAge?: ?number,
+  }): ?any {
+    let { key, maxAge } = options
+    let storage: Cache = JSON.parse(localStorage.getItem(STORE) || '{}')
+    let item = storage[key]
+    if (!item) {
+      return null
+    }
+    let createdAt = new Date(item.createdAt).getTime()
+    let actualMaxAge = (maxAge || DEFAULT_MAX_AGE)
+    if (new Date().getTime() - createdAt > actualMaxAge) {
+      delete storage[key]
+      localStorage.setItem(STORE, JSON.stringify(storage))
+      return null
+    }
+    console.log(`%cFrom cache ${key}`, 'color: #777A8B', item.data)
+    return item.data
+  }
+
+  save(options: {
+    key: string,
+    data: any,
+  }) {
+    let { key, data } = options
+    let storage: Cache = JSON.parse(localStorage.getItem(STORE) || '{}')
+    storage[key] = {
+      data,
+      createdAt: new Date().toISOString(),
+    }
+    localStorage.setItem(STORE, JSON.stringify(storage))
+  }
+}
+
+export default new Cacher()