import {reactive} from 'vue' import {createGlobalState, hasOwn} from "@vueuse/core"; import {empty, func} from "@/uni_modules/uv-ui-tools/libs/function/test"; import {sleep} from "@/uni_modules/uv-ui-tools/libs/function"; import {getSizeOfObject} from "@/utils"; import _ from 'lodash'; function CacheResult(data, options) { if (hasOwn(data, 'dataSize')) { // 如果对象内部有循环钩子,getSizeOfObject无法计算,则预先缓存大小 this.data = data.data this.dataSize = data.dataSize } else { this.data = data } this.expires = options?.timeout ? (options.timeout + Date.now()) : null this.obsoleted = () => { return this.expires && this.expires < Date.now() } } export const useCacheStore = createGlobalState(() => { // data const container = reactive(new Map()) const defaultOptions = { timeout: 2 * 3600 * 1000, // default cache 2 hours emptyIgnore: false // default cache empty values } // hooks // functions const checkActionDefine = (action) => { if (!action) throw new Error('action is required') if (func(action)) { return { type: action.toString(), handler: async (...args) => await action(...args) } } else { if (!action.type) throw new Error('action.type is required') if (!action.handler) throw new Error('action.handler is required') if (!func(action.handler)) throw new Error('action.handler must be a function') } return action } const generateCacheKey = function (payload) { return JSON.stringify({payload}) } const executeAction = async (action, payload) => { let result = action.handler(payload) if (result instanceof Promise) result = await result return result } /* * 获取缓存结果。 * 虽然改进后缓存动作传入function也可以,但用defineCacheAction的方式调试时更直观。 * @param {Object|Function} action 缓存动作。action是Function时尤其要注意用payload区隔结果! * @param {Object} payload 缓存参数,区隔缓存结果的关键! * @param {Object} options 缓存选项 * @returns {Object} 缓存结果 * */ const dispatchCache = async (action, payload = undefined, options = undefined) => { // TODO:并发锁, 并发机会不高且没有致命性影响,以后再优化 const formatAction = checkActionDefine(action) let typeNode = container.get(formatAction.type) if (!typeNode) { typeNode = new Map() container.set(formatAction.type, typeNode) } const key = generateCacheKey(payload) let cacheData = typeNode.get(key) // fire cached result if (cacheData instanceof CacheResult && !cacheData.obsoleted()) { // call `sleep` to simulate api reaction. // NOTE: 这个模拟对于mx-condition.config.isImmediate有关键作用,否则需要对缓存和API实时做不同配置 await sleep(0) return cacheData.data } // cache miss, execute action and cache result const result = await executeAction(formatAction, payload) const cacheOptions = _.merge(defaultOptions, options) cacheData = new CacheResult(result, cacheOptions) if (!(empty(result) && cacheOptions.emptyIgnore)) { typeNode.set(key, cacheData) } return cacheData.data } /* * 是否存在缓存 * @param {Object} action 缓存动作 * @param {Object} payload 缓存参数 * @returns {Boolean} 是否存在缓存 * */ const hasCache = (action, payload) => { const formatAction = checkActionDefine(action) const typeNode = container.get(formatAction.type) if (!typeNode) return false const key = generateCacheKey(payload) return typeNode.has(key) } /* * 删除缓存 * @param {Object} action 缓存动作 * @param {Object} payload 缓存参数 * */ const removeCache = (action, payload = null) => { if (payload === null || payload === undefined) { clearCache(action) return; } const formatAction = checkActionDefine(action) const typeNode = container.get(formatAction.type) if (!typeNode) return const key = generateCacheKey(payload) typeNode.delete(key) if (!typeNode.size) container.delete(formatAction.type) } /* * 清理缓存 * @param {Object} action 缓存动作,action = null时清空所有缓存 * */ const clearCache = (action = null) => { if (!action) { container.clear() return } const formatAction = checkActionDefine(action) container.delete(formatAction.type) } /* * 清理过期缓存 * */ const gcCache = () => { if (!container.size) return console.log('run useCacheStore: gc') for (const [type, typeNode] of container.entries()) { for (const [key, cacheData] of typeNode.entries()) { if (cacheData.obsoleted()) { typeNode.delete(key) console.log('run useCacheStore: gc: remove cache', type, key) } } if (!typeNode.size) { container.delete(type) console.log('run useCacheStore: gc: remove type', type) } } } const getSize = function () { // then calculate let total = 0 container.forEach(typeNode => { typeNode.forEach(i => { if (hasOwn(i, 'dataSize')) { total += i.dataSize } else { total += getSizeOfObject(i) } }) }) return total } // window.cacheContainer = container return { container, // open for test dispatchCache, hasCache, removeCache, clearCache, gcCache, getSize } })