洛阳学员端
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

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
}