You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
6.6 KiB
324 lines
6.6 KiB
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
|
|
}
|