const db = uniCloud.database() function getType(value) { return Object.prototype.toString.call(value).slice(8, -1).toLowerCase() } const validator = { key: function(value) { const err = new Error('Invalid key') if (typeof value !== 'string') { throw err } const valueTrim = value.trim() if (!valueTrim || valueTrim !== value) { throw err } }, value: function(value) { // 仅作简单校验 const type = getType(value) const validValueType = ['null', 'number', 'string', 'array', 'object'] if (validValueType.indexOf(type) === -1) { throw new Error('Invalid value type') } }, duration: function(value) { const err = new Error('Invalid duration') if (value === undefined) { return } if (typeof value !== 'number' || value === 0) { throw err } } } /** * 入库时 expired 为过期时间对应的时间戳,永不过期用-1表示 * 返回结果时 与redis对齐,-1表示永不过期,-2表示已过期或不存在 */ class DatabaseCache { constructor({ collection = 'opendb-open-data' } = {}) { this.type = 'db' this.collection = db.collection(collection) } _serializeValue(value) { return value === undefined ? null : JSON.stringify(value) } _deserializeValue(value) { return value ? JSON.parse(value) : value } async set(key, value, duration) { validator.key(key) validator.value(value) validator.duration(duration) value = this._serializeValue(value) await this.collection.doc(key).set({ value, expired: duration && duration !== -1 ? Date.now() + (duration * 1000) : -1 }) } async _getWithDuration(key) { const getKeyRes = await this.collection.doc(key).get() const record = getKeyRes.data[0] if (!record) { return { value: null, duration: -2 } } const value = this._deserializeValue(record.value) const expired = record.expired if (expired === -1) { return { value, duration: -1 } } const duration = expired - Date.now() if (duration <= 0) { await this.remove(key) return { value: null, duration: -2 } } return { value, duration: Math.floor(duration / 1000) } } async get(key, { withDuration = true } = {}) { const result = await this._getWithDuration(key) if (!withDuration) { delete result.duration } return result } async remove(key) { await this.collection.doc(key).remove() } } class RedisCache { constructor() { this.type = 'redis' this.redis = uniCloud.redis() } _serializeValue(value) { return value === undefined ? null : JSON.stringify(value) } _deserializeValue(value) { return value ? JSON.parse(value) : value } async set(key, value, duration) { validator.key(key) validator.value(value) validator.duration(duration) value = this._serializeValue(value) if (!duration || duration === -1) { await this.redis.set(key, value) } else { await this.redis.set(key, value, 'EX', duration) } } async get(key, { withDuration = false } = {}) { let value = await this.redis.get(key) value = this._deserializeValue(value) if (!withDuration) { return { value } } const durationSecond = await this.redis.ttl(key) let duration switch (durationSecond) { case -1: duration = -1 break case -2: duration = -2 break default: duration = durationSecond break } return { value, duration } } async remove(key) { await this.redis.del(key) } } class Cache { constructor({ type, collection } = {}) { if (type === 'database') { return new DatabaseCache({ collection }) } else if (type === 'redis') { return new RedisCache() } else { throw new Error('Invalid cache type') } } } class CacheKey { constructor({ type, collection, cache, key, fallback } = {}) { this.cache = cache || new Cache({ type, collection }) this.key = key this.fallback = fallback } async set(value, duration) { await this.cache.set(this.key, value, duration) } async setWithSync(value, duration, syncMethod) { await Promise.all([ this.set(this.key, value, duration), syncMethod(value, duration) ]) } async get() { let { value, duration } = await this.cache.get(this.key) if (value !== null && value !== undefined) { return { value, duration } } if (!this.fallback) { return { value: null, duration: -2 } } const fallbackResult = await this.fallback() value = fallbackResult.value duration = fallbackResult.duration if (value !== null && duration !== undefined) { await this.cache.set(this.key, value, duration) } return { value, duration } } async remove() { await this.cache.remove(this.key) } } class CacheKeyCascade { constructor({ layers, // [{cache, type, collection, key}] 从低级到高级排序,[DbCacheKey, RedisCacheKey] fallback } = {}) { this.layers = layers this.cacheLayers = [] let lastCacheKey for (let i = 0; i < layers.length; i++) { const { type, cache, collection, key } = layers[i] const lastCacheKeyTemp = lastCacheKey try { const currentCacheKey = new CacheKey({ type, collection, cache, key, fallback: i === 0 ? fallback : function() { return lastCacheKeyTemp.get() } }) this.cacheLayers.push(currentCacheKey) lastCacheKey = currentCacheKey } catch (e) {} } this.highLevelCache = lastCacheKey } async set(value, duration) { return Promise.all( this.cacheLayers.map(item => { return item.set(value, duration) }) ) } async setWithSync(value, duration, syncMethod) { const setPromise = this.cacheLayers.map(item => { return item.set(value, duration) }) return Promise.all( [ ...setPromise, syncMethod(value, duration) ] ) } async get() { return this.highLevelCache.get() } async remove() { await Promise.all( this.cacheLayers.map(cacheKeyItem => { return cacheKeyItem.remove() }) ) } } module.exports = { Cache, DatabaseCache, RedisCache, CacheKey, CacheKeyCascade }