uni-cloud-cache.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. const db = uniCloud.database()
  2. function getType(value) {
  3. return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
  4. }
  5. const validator = {
  6. key: function(value) {
  7. const err = new Error('Invalid key')
  8. if (typeof value !== 'string') {
  9. throw err
  10. }
  11. const valueTrim = value.trim()
  12. if (!valueTrim || valueTrim !== value) {
  13. throw err
  14. }
  15. },
  16. value: function(value) {
  17. // 仅作简单校验
  18. const type = getType(value)
  19. const validValueType = ['null', 'number', 'string', 'array', 'object']
  20. if (validValueType.indexOf(type) === -1) {
  21. throw new Error('Invalid value type')
  22. }
  23. },
  24. duration: function(value) {
  25. const err = new Error('Invalid duration')
  26. if (value === undefined) {
  27. return
  28. }
  29. if (typeof value !== 'number' || value === 0) {
  30. throw err
  31. }
  32. }
  33. }
  34. /**
  35. * 入库时 expired 为过期时间对应的时间戳,永不过期用-1表示
  36. * 返回结果时 与redis对齐,-1表示永不过期,-2表示已过期或不存在
  37. */
  38. class DatabaseCache {
  39. constructor({
  40. collection = 'opendb-open-data'
  41. } = {}) {
  42. this.type = 'db'
  43. this.collection = db.collection(collection)
  44. }
  45. _serializeValue(value) {
  46. return value === undefined ? null : JSON.stringify(value)
  47. }
  48. _deserializeValue(value) {
  49. return value ? JSON.parse(value) : value
  50. }
  51. async set(key, value, duration) {
  52. validator.key(key)
  53. validator.value(value)
  54. validator.duration(duration)
  55. value = this._serializeValue(value)
  56. await this.collection.doc(key).set({
  57. value,
  58. expired: duration && duration !== -1 ? Date.now() + (duration * 1000) : -1
  59. })
  60. }
  61. async _getWithDuration(key) {
  62. const getKeyRes = await this.collection.doc(key).get()
  63. const record = getKeyRes.data[0]
  64. if (!record) {
  65. return {
  66. value: null,
  67. duration: -2
  68. }
  69. }
  70. const value = this._deserializeValue(record.value)
  71. const expired = record.expired
  72. if (expired === -1) {
  73. return {
  74. value,
  75. duration: -1
  76. }
  77. }
  78. const duration = expired - Date.now()
  79. if (duration <= 0) {
  80. await this.remove(key)
  81. return {
  82. value: null,
  83. duration: -2
  84. }
  85. }
  86. return {
  87. value,
  88. duration: Math.floor(duration / 1000)
  89. }
  90. }
  91. async get(key, {
  92. withDuration = true
  93. } = {}) {
  94. const result = await this._getWithDuration(key)
  95. if (!withDuration) {
  96. delete result.duration
  97. }
  98. return result
  99. }
  100. async remove(key) {
  101. await this.collection.doc(key).remove()
  102. }
  103. }
  104. class RedisCache {
  105. constructor() {
  106. this.type = 'redis'
  107. this.redis = uniCloud.redis()
  108. }
  109. _serializeValue(value) {
  110. return value === undefined ? null : JSON.stringify(value)
  111. }
  112. _deserializeValue(value) {
  113. return value ? JSON.parse(value) : value
  114. }
  115. async set(key, value, duration) {
  116. validator.key(key)
  117. validator.value(value)
  118. validator.duration(duration)
  119. value = this._serializeValue(value)
  120. if (!duration || duration === -1) {
  121. await this.redis.set(key, value)
  122. } else {
  123. await this.redis.set(key, value, 'EX', duration)
  124. }
  125. }
  126. async get(key, {
  127. withDuration = false
  128. } = {}) {
  129. let value = await this.redis.get(key)
  130. value = this._deserializeValue(value)
  131. if (!withDuration) {
  132. return {
  133. value
  134. }
  135. }
  136. const durationSecond = await this.redis.ttl(key)
  137. let duration
  138. switch (durationSecond) {
  139. case -1:
  140. duration = -1
  141. break
  142. case -2:
  143. duration = -2
  144. break
  145. default:
  146. duration = durationSecond
  147. break
  148. }
  149. return {
  150. value,
  151. duration
  152. }
  153. }
  154. async remove(key) {
  155. await this.redis.del(key)
  156. }
  157. }
  158. class Cache {
  159. constructor({
  160. type,
  161. collection
  162. } = {}) {
  163. if (type === 'database') {
  164. return new DatabaseCache({
  165. collection
  166. })
  167. } else if (type === 'redis') {
  168. return new RedisCache()
  169. } else {
  170. throw new Error('Invalid cache type')
  171. }
  172. }
  173. }
  174. class CacheKey {
  175. constructor({
  176. type,
  177. collection,
  178. cache,
  179. key,
  180. fallback
  181. } = {}) {
  182. this.cache = cache || new Cache({
  183. type,
  184. collection
  185. })
  186. this.key = key
  187. this.fallback = fallback
  188. }
  189. async set(value, duration) {
  190. await this.cache.set(this.key, value, duration)
  191. }
  192. async setWithSync(value, duration, syncMethod) {
  193. await Promise.all([
  194. this.set(this.key, value, duration),
  195. syncMethod(value, duration)
  196. ])
  197. }
  198. async get() {
  199. let {
  200. value,
  201. duration
  202. } = await this.cache.get(this.key)
  203. if (value !== null && value !== undefined) {
  204. return {
  205. value,
  206. duration
  207. }
  208. }
  209. if (!this.fallback) {
  210. return {
  211. value: null,
  212. duration: -2
  213. }
  214. }
  215. const fallbackResult = await this.fallback()
  216. value = fallbackResult.value
  217. duration = fallbackResult.duration
  218. if (value !== null && duration !== undefined) {
  219. await this.cache.set(this.key, value, duration)
  220. }
  221. return {
  222. value,
  223. duration
  224. }
  225. }
  226. async remove() {
  227. await this.cache.remove(this.key)
  228. }
  229. }
  230. class CacheKeyCascade {
  231. constructor({
  232. layers, // [{cache, type, collection, key}] 从低级到高级排序,[DbCacheKey, RedisCacheKey]
  233. fallback
  234. } = {}) {
  235. this.layers = layers
  236. this.cacheLayers = []
  237. let lastCacheKey
  238. for (let i = 0; i < layers.length; i++) {
  239. const {
  240. type,
  241. cache,
  242. collection,
  243. key
  244. } = layers[i]
  245. const lastCacheKeyTemp = lastCacheKey
  246. try {
  247. const currentCacheKey = new CacheKey({
  248. type,
  249. collection,
  250. cache,
  251. key,
  252. fallback: i === 0 ? fallback : function() {
  253. return lastCacheKeyTemp.get()
  254. }
  255. })
  256. this.cacheLayers.push(currentCacheKey)
  257. lastCacheKey = currentCacheKey
  258. } catch (e) {}
  259. }
  260. this.highLevelCache = lastCacheKey
  261. }
  262. async set(value, duration) {
  263. return Promise.all(
  264. this.cacheLayers.map(item => {
  265. return item.set(value, duration)
  266. })
  267. )
  268. }
  269. async setWithSync(value, duration, syncMethod) {
  270. const setPromise = this.cacheLayers.map(item => {
  271. return item.set(value, duration)
  272. })
  273. return Promise.all(
  274. [
  275. ...setPromise,
  276. syncMethod(value, duration)
  277. ]
  278. )
  279. }
  280. async get() {
  281. return this.highLevelCache.get()
  282. }
  283. async remove() {
  284. await Promise.all(
  285. this.cacheLayers.map(cacheKeyItem => {
  286. return cacheKeyItem.remove()
  287. })
  288. )
  289. }
  290. }
  291. module.exports = {
  292. Cache,
  293. DatabaseCache,
  294. RedisCache,
  295. CacheKey,
  296. CacheKeyCascade
  297. }