-
Notifications
You must be signed in to change notification settings - Fork 4
/
CacheMap.js
99 lines (91 loc) · 3.41 KB
/
CacheMap.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import { createHub } from 'source/common/module/Event'
import { createDoublyLinkedList, createNode } from './LinkedList'
const DEFAULT_EXPIRE_TIME = 60 * 1000 // in msec, 1min
const createCache = (key, value, size, expireAt) => ({
...createNode(value), // value, prev, next
key,
size,
expireAt
})
// Time aware Least Recently Used (TLRU)
const createCacheMap = ({
valueSizeSumMax,
valueSizeSingleMax = Math.max(valueSizeSumMax * 0.05, 1), // limit big value for cache efficiently
eventHub = createHub() // set to null should be faster, if no event is needed
}) => {
if (__DEV__ && valueSizeSumMax <= 0) throw new Error(`invalid valueSizeSumMax: ${valueSizeSumMax}`)
const hasEventHub = Boolean(eventHub)
const {
clear: clearEventHub,
subscribe,
unsubscribe,
send
} = hasEventHub ? eventHub : {}
const map = new Map()
const linkedList = createDoublyLinkedList()
let valueSizeSum = 0
const cacheAdd = (cache) => {
map.set(cache.key, cache)
linkedList.unshift(cache)
valueSizeSum += cache.size
hasEventHub && send({ type: 'add', key: cache.key, payload: cache.value })
}
const cacheDelete = (cache) => {
map.delete(cache.key)
linkedList.remove(cache)
valueSizeSum -= cache.size
hasEventHub && send({ type: 'delete', key: cache.key, payload: cache.value })
}
return {
hasEventHub,
clearEventHub,
subscribe,
unsubscribe,
clear: () => map.forEach(cacheDelete), // use cacheDelete for event send // TODO: NOTE: not calling clearEventHub, so listener is kept
getSize: linkedList.getLength,
getValueSizeSum: () => valueSizeSum,
set: (key, value, size = 1, expireAt = Date.now() + DEFAULT_EXPIRE_TIME) => {
const prevCache = map.get(key)
prevCache && cacheDelete(prevCache) // drop prev cache
if (size > valueSizeSingleMax) return // size too big for cache
while (size + valueSizeSum > valueSizeSumMax) cacheDelete(linkedList.getTail().prev) // eslint-disable-line no-unmodified-loop-condition
cacheAdd(createCache(key, value, size, expireAt))
},
get: (key, time = Date.now()) => {
const cache = map.get(key)
if (!cache) return // miss
__DEV__ && cache.expireAt <= time && console.log('expired', cache.expireAt, time)
if (cache.expireAt <= time) return cacheDelete(cache) // expire
linkedList.moveToFirst(cache) // promote
return cache.value
},
touch: (key, expireAt = Date.now() + DEFAULT_EXPIRE_TIME) => {
const cache = map.get(key)
if (!cache) return
cache.expireAt = expireAt
linkedList.moveToFirst(cache) // promote
return cache.value
},
delete: (key) => {
const cache = map.get(key)
cache && cacheDelete(cache)
return cache && cache.value
},
saveCacheList: () => {
const cacheList = []
linkedList.forEachReverse(({ key, value, size, expireAt }) => cacheList.push({ key, value, size, expireAt })) // output with older cache first for simpler load & filter data
return cacheList
},
loadCacheList: (cacheList, time = Date.now()) => cacheList.forEach(({ key, value, size, expireAt }) => {
if (expireAt <= time) return // expired, drop
const cache = map.get(key)
if (cache && cache.expireAt >= expireAt) return // exist cache is good, skip
cache && cacheDelete(cache)
cacheAdd(createCache(key, value, size, expireAt))
})
}
}
export {
createCache,
createCacheMap
}