Skip to content

Commit d8bfba8

Browse files
committed
Add TTL support
1 parent e0c676b commit d8bfba8

File tree

3 files changed

+80
-7
lines changed

3 files changed

+80
-7
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ that represents it. It has to be a
7070
[deterministic algorithm](https://en.wikipedia.org/wiki/Deterministic_algorithm)
7171
meaning that, given one input, it always give the same output.
7272

73+
### TTL
74+
75+
To use a time-to-live:
76+
```js
77+
const memoized = memoize(fn, {
78+
ttl: 100 // ms
79+
})
80+
```
81+
82+
`ttl` is used to expire/delete cache keys. Valid time range up to 24 hours.
83+
84+
Note: cache entries not groomed aggressively for performance reasons, so a cache entry may reside in memory for up to `ttl * 2` before actually being purged. However, if a cache entry is accessed anytime after its expiration, it will then be immediately deleted and re-calculated.
85+
7386
## Benchmark
7487

7588
For an in depth explanation on how this library was created, go read

src/index.js

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ module.exports = function memoize (fn, options) {
1111
? options.serializer
1212
: serializerDefault
1313

14+
const ttl = options && +options.ttl
15+
? +options.ttl
16+
: ttlDefault
17+
1418
const strategy = options && options.strategy
1519
? options.strategy
1620
: strategyDefault
1721

1822
return strategy(fn, {
1923
cache,
20-
serializer
24+
serializer,
25+
ttl
2126
})
2227
}
2328

@@ -26,7 +31,7 @@ module.exports = function memoize (fn, options) {
2631
//
2732

2833
const isPrimitive = (value) =>
29-
value == null || (typeof value !== 'function' && typeof value !== 'object')
34+
value === null || (typeof value !== 'function' && typeof value !== 'object')
3035

3136
function strategyDefault (fn, options) {
3237
function monadic (fn, cache, serializer, arg) {
@@ -35,7 +40,6 @@ function strategyDefault (fn, options) {
3540
if (!cache.has(cacheKey)) {
3641
const computedValue = fn.call(this, arg)
3742
cache.set(cacheKey, computedValue)
38-
return computedValue
3943
}
4044

4145
return cache.get(cacheKey)
@@ -47,7 +51,6 @@ function strategyDefault (fn, options) {
4751
if (!cache.has(cacheKey)) {
4852
const computedValue = fn.apply(this, args)
4953
cache.set(cacheKey, computedValue)
50-
return computedValue
5154
}
5255

5356
return cache.get(cacheKey)
@@ -58,7 +61,9 @@ function strategyDefault (fn, options) {
5861
memoized = memoized.bind(
5962
this,
6063
fn,
61-
options.cache.create(),
64+
options.cache.create({
65+
ttl: options.ttl
66+
}),
6267
options.serializer
6368
)
6469

@@ -71,20 +76,55 @@ function strategyDefault (fn, options) {
7176

7277
const serializerDefault = (...args) => JSON.stringify(args)
7378

79+
const ttlDefault = false
80+
7481
//
7582
// Cache
7683
//
7784

7885
class ObjectWithoutPrototypeCache {
79-
constructor () {
86+
constructor (opts) {
8087
this.cache = Object.create(null)
88+
this.preHas = () => {}
89+
this.preGet = () => {}
90+
91+
if (opts.ttl) {
92+
const ttl = Math.min(24 * 60 * 60 * 1000, Math.max(1, opts.ttl)) // max of 24 hours, min of 1 ms
93+
const ttlKeyExpMap = {}
94+
95+
this.preHas = (key) => {
96+
if (Date.now() > ttlKeyExpMap[key]) {
97+
delete ttlKeyExpMap[key]
98+
delete this.cache[key]
99+
}
100+
}
101+
this.preGet = (key) => {
102+
ttlKeyExpMap[key] = Date.now() + ttl
103+
}
104+
105+
setInterval(() => {
106+
const now = Date.now()
107+
const keys = Object.keys(ttlKeyExpMap)
108+
// The assumption here is that the order of keys is oldest -> most recently created,
109+
// which coresponds to the order of closest exp -> farthest exp.
110+
// So, keep looping thru expiration times until a key hasn't expired.
111+
keys.every((key) => {
112+
if (now > ttlKeyExpMap[key]) {
113+
delete ttlKeyExpMap[key]
114+
return true
115+
}
116+
})
117+
}, opts.ttl)
118+
}
81119
}
82120

83121
has (key) {
122+
this.preHas(key)
84123
return (key in this.cache)
85124
}
86125

87126
get (key) {
127+
this.preGet(key)
88128
return this.cache[key]
89129
}
90130

@@ -94,5 +134,5 @@ class ObjectWithoutPrototypeCache {
94134
}
95135

96136
const cacheDefault = {
97-
create: () => new ObjectWithoutPrototypeCache()
137+
create: (opts) => new ObjectWithoutPrototypeCache(opts)
98138
}

test/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ test('memoize functions with single non-primitive argument', () => {
5858
expect(numberOfCalls).toBe(1)
5959
})
6060

61+
test('memoize functions with single non-primitive argument and TTL', () => {
62+
let numberOfCalls = 0
63+
function plusPlus (obj) {
64+
numberOfCalls += 1
65+
return obj.number + 1
66+
}
67+
68+
const memoizedPlusPlus = memoize(plusPlus, { ttl: 2 })
69+
70+
memoizedPlusPlus({number: 1})
71+
memoizedPlusPlus({number: 1})
72+
let i = 50000
73+
/* a simple delay */ while (i--) Math.random() * Math.random()
74+
memoizedPlusPlus({number: 1})
75+
memoizedPlusPlus({number: 1})
76+
77+
// Assertions
78+
expect(numberOfCalls).toBe(2)
79+
})
80+
6181
test('memoize functions with N arguments', () => {
6282
function nToThePower (n, power) {
6383
return Math.pow(n, power)

0 commit comments

Comments
 (0)