Source

services/cache-service.js

/**
 * @namespace Services
 * @category Services
 * @module cache-service
 * */

import localforageWrapper from '@/utils/cache'
import cacheTimes from '@/config/cache-times.json'
import { getQueryStringValue } from '@/utils/querystring'

const shouldLog =
  getQueryStringValue('cache_verbose') === '1' ||
  getQueryStringValue('cache_debug') === '1' ||
  getQueryStringValue('verbose') === '1'

export function createLocalStorage(namespacePrefix = '') {
  namespacePrefix = namespacePrefix ? namespacePrefix + '_' : ''
  let storage = {
    name: namespacePrefix,
    localforage: localforageWrapper,
    keys: async () =>
      (await localforageWrapper.keys()).filter((key) =>
        key.includes(namespacePrefix)
      ),
    setItem(key, value) {
      return localforageWrapper.setItem(`${namespacePrefix}${key}`, value)
    },
    async getItem(key, defaultValue = null) {
      let value = await localforageWrapper.getItem(`${namespacePrefix}${key}`)
      return value === null ? defaultValue : value
    },
    async clear() {
      let keys = await this.localforage.keys()
      await Promise.all(
        keys
          .filter((k) => k.indexOf(namespacePrefix) === 0)
          .map((k) => this.localforage.removeItem(k))
      )
    },
    /**
     * Simple cache invalidation by string match
     * i.g: this.$localStorage.invalidateCacheByKeyInclude('cdm/messages_alertes') will remove cache for Alerts module main search
     */
    async invalidateCacheByKeyInclude(str) {
      let keys = await this.keys()
      let promises = keys
        .filter((k) => k.includes(str))
        .map((key) => {
          console.log('invalidateCacheByKeyInclude::invalidate', key)
          return this.localforage.removeItem(key)
        })
      await Promise.all(promises)
      if (promises.length === 0) {
        shouldLog &&
          console.log('invalidateCacheByKeyInclude::no-match', {
            keys,
          })
      }
      return promises.length > 0
    },
  }
  storage.cache = createCache(storage, {})
  return storage
}

/**
 * Cache wrapper on top of localforage (Used for API calls caching)
 *
 * @param {*} storage
 * @param {*} options
 */
function createCache(storage, options = {}) {
  /**
   * Retrieves and env value if available
   * @TODO: Unit test
   *
   * @param {String} configValue
   * @param {Mixed} defaultValue
   *
   */
  function getCacheTimeValueFromConfig(configValue, defaultValue = null) {
    defaultValue = !isNaN(defaultValue) ? parseInt(defaultValue) : null
    if (!defaultValue && configValue.indexOf('||')) {
      defaultValue = !isNaN(configValue.split('||')[1])
        ? parseInt(configValue.split('||')[1]) || defaultValue
        : defaultValue
      configValue = configValue.split('||')[0]
      shouldLog &&
        console.log('getCacheTimeValueFromConfig', {
          defaultValue,
          configValue,
        })
    }
    let timeValue =
      (process.env[configValue] &&
        ![undefined, ''].includes(process.env[configValue]) &&
        !isNaN(parseInt(process.env[configValue])) &&
        parseInt(process.env[configValue])) ||
      defaultValue
    return (!isNaN(timeValue) && timeValue) || defaultValue
  }

  return {
    /**
     * Checks if cache should be disabled via an special URL parameter
     */
    shouldDisableCache: () =>
      getQueryStringValue('nocache') === '1' ||
      getQueryStringValue('cache') === '0',
    /**
     * Retrieves cache times per key (normalized)
     *
     */
    getCacheTimes() {
      let table = cacheTimes || {}
      return Object.keys(table).reduce((acum, key) => {
        let cacheTime = getCacheTimeValueFromConfig(
          table[key],
          getCacheTimeValueFromConfig(
            'process.env.VUE_APP_CACHE_TIME_DEFAULT',
            null
          )
        )
        if (cacheTime !== null) {
          acum[key.split('/').join('_')] = cacheTime
        }
        return acum
      }, {})
    },
    /**
     * Retrieve data from cache if key has an expiration time and cache is enabled and cached data is not expired
     * @param {*} url
     */
    async get(key) {
      let storageKey = `cache__${key}`
      key = key.split('/').join('_')
      let keyTimes = key.split('__uniq__')[0]

      if (this.shouldDisableCache()) {
        shouldLog && console.log(`${storage.name}:cache:get`, key, 'disabled')
        return null
      }

      const shouldUseCache = this.getCacheTime(keyTimes)
      if (!shouldUseCache) {
        shouldLog && console.log(`${storage.name}:cache:get`, key, 'skip')
        return null
      }

      let item = await storage.getItem(storageKey, null)
      const isExpired = item
        ? Date.now() - item.date > parseInt(this.getCacheTime(keyTimes))
        : true
      if (item) {
        if (isExpired) {
          shouldLog &&
            console.log(`${storage.name}:cache:get`, key, 'expired', {
              storageKey,
              item,
              cacheAge: Date.now() - item.date,
              cacheRetention: parseInt(this.getCacheTime(keyTimes)),
            })
          storage.localforage.removeItem(storageKey)
        } else {
          shouldLog && console.log(`${storage.name}:cache:get`, key, 'success')
          return item.value
        }
      } else {
        shouldLog &&
          console.log(`${storage.name}:cache:get`, key, 'not-found', {
            storageKey,
          })
      }
      return null
    },
    /**
     * Get cache time value from object
     * Supports retrieving value by matching a key who contains a wildcard
     * @example
     *
     *    {
     *      "/v2/temps_reel/by_vehicule**":"1000"
     *    }
     *    getCacheTime(times, "/v2/temps_reel/by_vehicule?vehicule_id=3125")
     *
     *    //Will return 1000
     *
     * @param {*} times
     * @param {*} key
     * @returns
     */
    getCacheTime(key) {
      this._times = this._times || this.getCacheTimes()

      let cacheKeys = Object.keys(this._times).map((_key) =>
        _key.split('/').join('_')
      )

      let keyToCompare = key.split('/').join('_')
      let originAsKey = window.location.origin.split('/').join('_')
      if (key.includes(originAsKey)) {
        keyToCompare = key.split(originAsKey).join('').split('/').join('_')
      }

      let firstMatchedCacheKey = cacheKeys.find((cacheKey) => {
        cacheKey = cacheKey.split('**')[0] //If wildcard, the the left part
        return keyToCompare.includes(cacheKey)
      })
      let cacheTime = firstMatchedCacheKey
        ? this._times[firstMatchedCacheKey]
        : null
      const isCacheTimeFound = !!firstMatchedCacheKey
      const isWildcardCacheKey = firstMatchedCacheKey
        ? firstMatchedCacheKey.includes('**')
        : false

      shouldLog &&
        console.log(
          `cache:time:${
            isCacheTimeFound
              ? 'found' + `${isWildcardCacheKey ? '(wildcard)' : ''}`
              : 'not-found'
          }`,
          {
            keyToCompare,
            cacheKeys,
          }
        )
      return cacheTime
    },
    /**
     * Check if a key has an expiration time and if cache is enabled
     * @param {*} key
     */
    shouldCache(key) {
      key = key.split('/').join('_')
      key = key.split('__uniq__')[0]
      shouldLog &&
        console.log(`${storage.name}:cache:shouldCache`, key, {
          shouldCache: this.getCacheTime(key) && !this.shouldDisableCache(),
          isCacheDisabled: this.shouldDisableCache(),
        })
      return !!this.getCacheTime(key) && !this.shouldDisableCache()
    },
    /**
     * Store data if the key has an expiration time and cache is enabled
     *
     * @param {*} key
     * @param {*} value
     */
    set(key, value) {
      if (this.shouldDisableCache()) {
        shouldLog &&
          console.log(`${storage.name}:cache:set (skip, disabled)`, key)
        return
      }

      let storageKey = `cache__${key}`
      key = key.split('/').join('_')
      let keyTimes = key.split('__uniq__')[0]
      let timeCount = this.getCacheTime(keyTimes)
      if (timeCount) {
        let payload = {
          date: Date.now(),
          value,
        }
        shouldLog && console.log(`${storage.name}:cache:set`, key)
        return storage.setItem(storageKey, payload)
      } else {
        shouldLog &&
          console.log(`${storage.name}:cache:set (skip)`, key, {
            keyTimes,
            timeCount,
          })
      }
    },
  }
}