storage.js

/**
 * @file 离线存储控制器
 * @author lisfan <goolisfan@gmail.com>
 * @version 1.0.0
 * @licence MIT
 */

import localforage from 'localforage'
import validation from '@~lisfan/validation'
import Logger from '@~lisfan/logger'
import _ from './utils/utils'

import sessionStorageWrapper from './utils/localforage-sessionstoragewrapper'
import DataItem from './models/data-item'

import DATA_TYPES from './enums/data-types'
import STORAGES from './enums/storages'
import STORAGE_DRIVERS from './enums/storage-drivers'
import LOCALFORAGE_DRIVERS from './enums/localforage-drivers'
import DRIVERS_REFLECTOR from './enums/drivers-reflector'

// 增加新的sessionStorage驱动器
const definedSessionDriverPromise = localforage.defineDriver(sessionStorageWrapper)

// localForage默认配置项
const localForageDefaultConfig = localforage._defaultConfig

/* eslint-disable max-len */
// localForage默认驱动器列表,同时优先选择sessionStorage存储
const localForageDefaultDriver = [LOCALFORAGE_DRIVERS.SESSIONSTORAGE].concat(localForageDefaultConfig._driver)
/* eslint-enable max-len */

/**
 * 私有方法
 *
 * @private
 */
const _actions = {
  /**
   * localforage实例工厂
   *
   * @since 1.0.0
   *
   * @param {Storage} self - 实例自身
   * @param {object} options - 配置项
   *
   * @returns {LocalForage}
   */
  localforageFactory(self, options) {
    // 验证驱动器列表是否至少有一个支持
    const drivers = options.driver.length > 0
      ? options.driver
      : localForageDefaultDriver

    let supportDriver = drivers.some((driver) => {
      return localforage.supports(driver)
    })

    if (!supportDriver) {
      const unSupportDrivers = drivers.map((driver) => {
        return STORAGES[driver]
      })

      self._logger.error(`current browser cant\'t support ${unSupportDrivers.join(',')}  storage, please use another one`)
    }

    return localforage.createInstance({
      ...options,
      driver: drivers
    })
  },
  /**
   * storage实例初始化
   * 1. 创建localforage实例
   * 2. 填充localforage实例初始化完成后的storage实例数据
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Storage} self - 实例自身
   *
   * @returns {Promise}
   */
  async init(self) {
    /* eslint-disable max-len */
    const storageDrivers = this.transformDriver(_.castArray(self.$options.driver))
    /* eslint-enable max-len */

    self._storeMapStorage = await this.createStoreMapStorage(self, storageDrivers)

    self._storage = await this.createStorage(self, storageDrivers)

    return this.readyInit(self)
  },
  /**
   * 创建存储storeMap的localforage实例
   * - 用来存储各个DataItem实例
   * - 该离线存储器的实例配置driver值参考用户自定义的驱动器
   * - 目的是避免使用自身数据库进行存储storeMap,而引来的一些不必要的麻烦
   * - 比如,如果直接放置在自身属性上,会占用一个存储项,且部分api调用时,还需要排除该项
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Storage} self - 实例自身
   * @param {string[]} storageDrivers - 存储驱动器列表
   *
   * @returns {Promise}
   */
  createStoreMapStorage(self, storageDrivers) {
    // 异步加载,确保自定义driver已经加载完毕
    return definedSessionDriverPromise.then(() => {
      return this.localforageFactory(self, {
        name: 'STORE_MAP',
        driver: storageDrivers
      })
    })
  },
  /**
   * 创建存储数据的localforage实例
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Storage} self - 实例自身
   * @param {string[]} storageDrivers - 存储驱动器列表
   *
   * @returns {Promise}
   */
  createStorage(self, storageDrivers) {
    // 异步加载,确保自定义driver已经加载完毕
    return definedSessionDriverPromise.then(() => {
      return this.localforageFactory(self, {
        ...self.$options,
        driver: storageDrivers
      })
    })
  },
  /**
   * localforage实例完全初始化后,对storage实例进行完全初始化处理
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Storage} self - 实例自身
   *
   * @returns {Promise}
   */
  async readyInit(self) {
    await self._storage.ready().then(async () => {
      const localforageConfig = self._storage._config
      self.$driver = DRIVERS_REFLECTOR[localforageConfig.driver]
      self.$name = localforageConfig.name
      self.$description = localforageConfig.description
      self.$storeName = localforageConfig.storeName
      self.$size = localforageConfig.size

      self.$maxAge = self.$options.maxAge

      // (优先)等待数据长度计算
      await this.computedLength(self)
      // 解析离线存储中的storemap
      await this.parseStoreMap(self)

      // 绑定事件(必须等待上两个完成)
      this.bindRecordStoreMapEvent(self)
    })

    return Promise.resolve()
  },
  /**
   * 计算数据长度长度
   * [误]每次调用会影响数据项长度的API后,重新计算length的长度
   * [误]由于计算长度是调用其他异步api的,为了提升性能,采用手动计算方式处理
   * 因为会发生setItem覆盖数据值的情况,手动计算不会准确
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Storage} self - 实例自身
   *
   * @returns {Promise}
   */
  computedLength(self) {
    return self._storage.length().then((length) => {
      self.$length = length
    })
  },
  /**
   * 调用该方法时,确保localforage实例已完全初始化
   * 解析默认存在的storeMap,且存在数据项长度大于0
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Storage} self - 实例自身
   *
   * @returns {Promise}
   */
  async parseStoreMap(self) {
    return self._storeMapStorage.ready(() => {
      return self._storeMapStorage.getItem(self.$name).then((data) => {
        // 若以存在,且数据项长度大于0
        if (data && self.length > 0) {
          self.$storeMap = _.mapValues(data, (options) => {
            return new DataItem(options)
          })
        } else {
          self.$storeMap = {}
        }
      })
    })
  },
  /**
   * 过滤梳理storeMap的数据单元实例列表
   *
   * @since 1.0.0
   *
   * @param {Storage} self - 实例自身
   *
   * @returns {DataItem[]}
   */
  filterStoreMap(self) {
    let storeMap = {}
    Object.entries(self.$storeMap).forEach(([key, dataItem]) => {
      // 过滤maxAge不存在的DataItem实例
      if (validation.isNumber(dataItem.$maxAge)) {
        storeMap[key] = {
          description: dataItem.$description,
          timeStamp: dataItem.$timeStamp,
          maxAge: dataItem.$maxAge,
        }
      }
    })

    return storeMap
  },
  /**
   * 绑定标签页离开事件
   * [旧]每次调用会影响数据项长度的API后,重新记录一次storeMap
   * 需要排除数据的记录,只保存对象上的一些数据而已
   * [新]优化性能,只有在离开页面的时候才进行一次存储
   *
   * @since 1.0.0
   *
   * @param {Storage} self - 实例自身
   */
  bindRecordStoreMapEvent(self) {
    window.addEventListener('beforeunload', () => {
      const filteredStoreMap = this.filterStoreMap(self)
      const filteredStoreMapLength = Object.keys(filteredStoreMap).length
      if (filteredStoreMapLength > 0) {
        self._storeMapStorage.ready(() => {
          self._storeMapStorage.setItem(self.$name, filteredStoreMap)
        })
      }
    })
  },
  /**
   * 转换storage驱动器和localforage驱动器的映射关系
   *
   * @since 1.0.0
   *
   * @param {string[]} drivers - storage驱动器列表或localforage驱动器列表
   *
   * @returns {string[]}
   */
  transformDriver(drivers) {
    const transformedDriver = drivers.map((driver) => {
      return DRIVERS_REFLECTOR[driver]
    })

    // 移除假值
    return _.compact(transformedDriver)
  },

  /**
   * 转换成可离线存储的格式
   *
   * @since 1.0.0
   *
   * @param {*} data - 任意数据
   *
   * @returns {*}
   */
  transformStorageDate(data) {
    switch (validation.typeof(data)) {
      case 'undefined':
        return DATA_TYPES.UNDEFINED + 'undefined'
      case 'date':
        return DATA_TYPES.DATE + data.getTime()
      case 'regexp':
        return DATA_TYPES.REGEXP + data.toString()
      case 'function':
        return DATA_TYPES.FUNCTION + data.toString()
      case 'number':
        if (validation.isNaN(data)) {
          // 处理是NaN的情况
          return DATA_TYPES.NAN + 'NaN'
        } else if (!validation.isFinite(data) && data > 0) {
          // 处理是NaN的情况
          return DATA_TYPES.INFINITY + 'Infinity'
        } else if (!validation.isFinite(data) && data < 0) {
          // 处理是NaN的情况
          return DATA_TYPES.INFINITY + '-Infinity'
        }

        // 其他情况的number直接返回
        return data
      default:
        return data
    }
  },
  /**
   * 解析数据时的正则匹配模式
   *
   * @since 1.0.0
   */
  PARSE_DATA_REGEXP: /^\[storage ([^\]#]+)\]#([\s\S]+)$/,
  /**
   * 解析要存储的值
   *
   * @since 1.0.0
   *
   * @param {*} data - 任意数据
   *
   * @returns {*}
   */
  parseStorageDate(data) {
    let type
    let value

    if (validation.isString(data) && data.startsWith('[storage')) {
      const matched = data.match(this.PARSE_DATA_REGEXP)

      if (matched) {
        type = matched[1]
        value = matched[2]
      }
    }

    /* eslint-disable no-eval*/
    switch (type) {
      case 'undefined':
        return undefined
      case 'date':
        return new Date(Number(value))
      case 'regexp':
        return eval(`(${value})`)
      case 'function':
        return eval(`(${value})`)
      case 'nan':
        return eval(`(${value})`)
      case 'infinity':
        return eval(`(${value})`)
      default:
        return data
    }
  },
  /* eslint-enable no-eval*/
  /**
   * 过滤时效还未超时的数据
   *
   * @since 1.0.0
   *
   * @param {Storage} self - 实例自身
   *
   * @returns {Promise}
   */
  filterInvalidData(self) {
    let storeMap = {}

    // 导师步解析数据,过滤已过期数据
    return self._storage.iterate((data, name) => {
      const dataItem = self.$storeMap[name]
      if (dataItem.isOutdated()) {
        self.removeItem(name)
      } else {
        // 解析数据类型
        dataItem.fillData(_actions.parseStorageDate(data))
        storeMap[name] = dataItem
      }
    }).then(() => {
      return storeMap
    })
  }
}

/**
 * @description
 * - storage只会管理存储通过其 API 创建的离线数据,不管理其他自定义的存储数据(数据项会加上命名空间前缀)
 * - storage实例会单独建立一个映射表来管理数据单元与数据的映射关系
 *
 * @classdesc 离线存储类
 *
 * @class
 */
class Storage {
  /**
   * sessionStorage驱动器
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Storage
   *
   * @type {string}
   */
  static SESSIONSTORAGE = STORAGE_DRIVERS.SESSIONSTORAGE
  /**
   * indexedDB驱动器
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Storage
   *
   * @type {string}
   */
  static INDEXEDDB = STORAGE_DRIVERS.INDEXEDDB
  /**
   * webSQL驱动器
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Storage
   *
   * @type {string}
   */
  static WEBSQL = STORAGE_DRIVERS.WEBSQL
  /**
   * localStorage驱动器
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Storage
   *
   * @type {string}
   */
  static LOCALSTORAGE = STORAGE_DRIVERS.LOCALSTORAGE

  /**
   * 默认配置选项
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Storage
   *
   * @type {object}
   * @property {number} maxAge=-1 - 数据可存活时间(毫秒单位),可选值有:0=不缓存,小于0的值=永久缓存(默认),大于0的值=可存活时间
   * @property {boolean} debug=false - 调试日志输出模式
   * @property {array} driver=[Storage.SESSIONSTORAGE,Storage.INDEXEDDB,Storage.WEBSQL,Storage.LOCALSTORAGE] -
   *   离线存储器的驱动器优先选择列表
   * @property {string} name='storage' - 离线存储器命名空间
   * @property {string} description='' - 离线存储器描述,取localforage的默认值
   * @property {number} size=4980736 - 离线存储器的大小,仅webSQL有效,取localforage的默认值
   * @property {string} storeName=4980736 - 离线存储器的数据库名称,仅indexedDB和WebSQL有效,取localforage的默认值
   */
  static options = {
    name: 'storage',
    debug: false,
    maxAge: -1,
    driver: _actions.transformDriver(localForageDefaultDriver),
    description: localForageDefaultConfig.description,
    size: localForageDefaultConfig.size,
    storeName: localForageDefaultConfig.storeName,
  }

  /**
   * 更新默认配置选项
   *
   * @since 1.0.0
   *
   * @see Storage.options
   *
   * @param {object} options - 配置选项见{@link Storage.options}
   *
   * @returns {Storage}
   */
  static config(options) {
    // 不调用localforage.config,希望这里的config只是针对Storage类的配置更新
    Storage.options = {
      ...Storage.options,
      options
    }

    return this
  }

  /**
   * 判断浏览器是否支持对应的离线存储驱动器
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {Symbol} driver - 驱动器常量
   *
   * @returns {Promise}
   */
  static supports(driver) {
    return localforage.supports(DRIVERS_REFLECTOR[driver])
  }

  /**
   * 构造函数
   *
   * @see Storage.options
   *
   * @param {object} options - 配置选项见{@link Storage.options}
   */
  constructor(options) {
    this.$options = {
      ...Storage.options,
      ...options
    }

    this._logger = new Logger({
      name: this.$options.name,
      debug: this.$options.debug
    })

    this._ready = _actions.init(this)
  }

  /**
   * 实例关联的存储storeMap的localforage实例
   *
   * @since 1.0.0
   *
   * @private
   */
  _storeMapStorage = undefined

  /**
   * 实例关联的localforage实例
   *
   * @since 1.0.0
   *
   * @private
   */
  _storage = undefined

  /**
   * 日志打印器,方便调试
   *
   * @since 1.1.0
   *
   * @private
   */
  _logger = undefined

  /**
   * 实例的完全初始化
   *
   * @since 1.0.0
   *
   * @private
   */
  _ready = undefined

  /**
   * 实例的数据与存活时间映射关系表
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {object}
   */
  $storeMap = {}

  /**
   * 实例初始配置项
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {object}
   */
  $options = undefined

  /**
   * 实例的数据存活时长
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $maxAge = undefined

  /**
   * 实例的驱动器类型
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {string}
   */
  $driver = undefined

  /**
   * 实例的命名空间
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {string}
   */
  $name = undefined

  /**
   * 实例的描述
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {string}
   */
  $description = undefined

  /**
   * 实例的数据库大小
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $size = undefined

  /**
   * 实例的数据库名称
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {string}
   */
  $storeName = undefined

  /**
   * 实例的数据项长度
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $length = 0

  /**
   * 获取实例的数据项长度,实例$length属性的别名属性
   *
   * @since 1.0.0
   *
   * @getter
   * @readonly
   *
   * @type {number}
   */
  get length() {
    return this.$length || 0
  }

  /**
   * 确保实例已初始化完成
   *
   * @since 1.0.0
   *
   * @async
   *
   * @returns {Promise}
   */
  ready() {
    return this._ready
  }

  /**
   * 获取当前实例的驱动器常量
   * [注] 确保实例已初始化完成,否则取不到值
   *
   * @since 1.0.0
   *
   * @async
   *
   * @returns {Promise}
   */
  driver() {
    return this.$driver
  }

  /**
   * 存储数据项到离线存储器
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {string} name - 数据项名称
   * @param {*} data - 任意数据
   * @param {object} options - 自定义存储单元实例的配置选项
   * @param {number} [options.maxAge] - 数据单元项可存活时间(毫秒单位)
   * @param {string} [options.description]- 数据单元项描述
   *
   * @returns {Promise}
   */
  setItem(name, data, options = {}) {
    const maxAge = validation.isNumber(options.maxAge) ? options.maxAge : this.$maxAge

    // 若时效性为0,则不存储到store中,直接返回结果
    // 同时删除原本已存在的数据项
    if (maxAge === 0) {
      this._logger.warn(`maxAge is (0), (${name}) no need to set!`, 'data is: (', data, ')')
      this.removeItem(name)
      return Promise.resolve(data)
    }

    // 往storeMap中插入一条记录
    // [误]判断$store是否已存在,若已存在,则进行更新数据,若不存在,则初始化
    // [新]该方法将进行覆盖,重新需实例化
    this.$storeMap[name] = new DataItem({
      ...options,
      maxAge, // 默认取storage上的存活时间
      data,
      name: name // 名称强制指定为setItem的name参数
    })

    // 针对几种数据类型,进行转换
    // 几种localforage不支持的值进行转换

    return this._storage.setItem(name, _actions.transformStorageDate(data)).then(async () => {
      this._logger.log(`set (${name}) success! data can live (${maxAge / 1000}s)`, 'data is: (', data, ')')

      await _actions.computedLength(this)
      // 将当前的原始值返回
      return data
    })
  }

  // /**
  //  * 更新数据项数据
  //  * 会更新数据单元项实例的更新时间戳
  //  *
  //  * @deprecated 1.1.0
  //  * @since 1.0.0
  //  * @param {string} name - 数据项名称
  //  * @param {*} data - 任意数据
  //  * @returns {Promise}
  //  */
  // updateItem(name, data) {
  //   // 判断是否已存在该项
  //   const dataItem = this.$storeMap[name]
  //
  //   // 若未存在,则进行初始化
  //   if (!(dataItem instanceof DataItem)) {
  //     return this.setItem(name, data)
  //   }
  //
  //   // 若已存在,则只更新数据
  //   dataItem.updateData(data)
  //
  //   return this._storage.setItem(name, _actions.transformStorageDate(data)).then(() => {
  //     return data
  //   })
  // }

  /**
   * 获取指定数据项数据
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {string} name - 数据项名称
   *
   * @returns {Promise}
   */
  getItem(name) {
    const dataItem = this.$storeMap[name]

    // 如果DataItem实例不存在,或者DataItem实例的maxAge属性不存在
    if (!dataItem || !validation.isNumber(dataItem.$maxAge)) {
      return Promise.reject('outdate')
    }

    // maxAge的值大于0
    // 移除数据
    if (dataItem.isOutdated()) {
      this.removeItem(name)
      return Promise.reject('outdate')
    }

    // 优化性能
    // 若数据还在存活期,且已绑定在了storeMap上,则忽略从离线存储中取出再解析的过程
    if (!validation.isUndefined(dataItem.$data)) {
      this._logger.log(`get (${name}) success!`, 'data is: (', dataItem.$data, ')')
      return Promise.resolve(dataItem.$data)
    }

    return this._storage.getItem(name).then((data) => {
      // 解析数据类型
      data = _actions.parseStorageDate(data)
      dataItem.fillData(data)

      this._logger.log(`get (${name}) success!`, 'data is: (', data, ')')
      return data
    })
  }

  /**
   * 移除数据项
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {string} name - 数据项名称
   *
   * @returns {Promise}
   */
  removeItem(name) {
    this.$storeMap[name] = null
    delete this.$storeMap[name]

    return this._storage.removeItem(name).then(async () => {
      this._logger.warn(`remove (${name}) success!`)

      await _actions.computedLength(this)
    })
  }

  /**
   * 清空所有数据项
   *
   * @since 1.0.0
   *
   * @async
   *
   * @returns {Promise}
   */
  clear() {
    this.$storeMap = {}
    this.$length = 0

    this._logger.warn(`clear (${name}) success!`)

    return this._storage.clear()
  }

  // /**
  //  * 获取指定序号的数据项键名
  //  * [注]该API在使用localStorage存储时行为会有点怪异,不准确
  //  *
  //  * @deprecated 1.1.0
  //  * @since 1.0.0
  //  * @param {number} keyIndex - 序号
  //  * @returns {Promise}
  //  */
  // key(keyIndex) {
  //   return this._storage.key(keyIndex)
  // }

  /**
   * 获取所有时效性活的数据的键列表
   *
   * @since 1.0.0
   *
   * @async
   *
   * @returns {Promise}
   */
  keys() {
    this._logger.warn(`this operate is inefficient! avoid use it.`)

    return Promise.resolve().then(() => {
      let keys = []
      Object.entries(this.$storeMap).forEach(([name, dataItem]) => {
        if (dataItem.isOutdated()) {
          this.removeItem(name)
        } else {
          keys.push(name)
        }
      })

      return keys
    })
  }

  /**
   * 迭代所有数据项
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {function} iteratorCallback - 迭代函数,迭代函数若返回了具体的值,则提前退出,且返回值将作为resolved的结果值
   *
   * @returns {Promise}
   */
  iterate(iteratorCallback) {
    this._logger.warn(`this operate is inefficient! avoid use it.`)

    return _actions.filterInvalidData(this).then((storeMap) => {
      let result

      Object.entries(storeMap).every(([name, dataItem], index) => {
        result = iteratorCallback(dataItem.$data, name, index + 1)

        return validation.isUndefined(result)
      })

      return result
    })
  }
}

export { Storage }

export default new Storage()