Skip to content

Commit

Permalink
Add TTL support
Browse files Browse the repository at this point in the history
  • Loading branch information
crazy4groovy committed Feb 11, 2017
1 parent e0c676b commit 69cb2c6
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 7 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ that represents it. It has to be a
[deterministic algorithm](https://en.wikipedia.org/wiki/Deterministic_algorithm)
meaning that, given one input, it always give the same output.

### TTL

To use a time-to-live:
```js
const memoized = memoize(fn, {
ttl: 100 // ms
})
```

`ttl` is used to expire/delete cache keys. Valid time range up to 24 hours.

Note: cache entries are 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.

## Benchmark

For an in depth explanation on how this library was created, go read
Expand Down
54 changes: 47 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ module.exports = function memoize (fn, options) {
? options.serializer
: serializerDefault

const ttl = options && +options.ttl
? +options.ttl
: ttlDefault

const strategy = options && options.strategy
? options.strategy
: strategyDefault

return strategy(fn, {
cache,
serializer
serializer,
ttl
})
}

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

const isPrimitive = (value) =>
value == null || (typeof value !== 'function' && typeof value !== 'object')
value === null || (typeof value !== 'function' && typeof value !== 'object')

function strategyDefault (fn, options) {
function monadic (fn, cache, serializer, arg) {
Expand All @@ -35,7 +40,6 @@ function strategyDefault (fn, options) {
if (!cache.has(cacheKey)) {
const computedValue = fn.call(this, arg)
cache.set(cacheKey, computedValue)
return computedValue
}

return cache.get(cacheKey)
Expand All @@ -47,7 +51,6 @@ function strategyDefault (fn, options) {
if (!cache.has(cacheKey)) {
const computedValue = fn.apply(this, args)
cache.set(cacheKey, computedValue)
return computedValue
}

return cache.get(cacheKey)
Expand All @@ -58,7 +61,9 @@ function strategyDefault (fn, options) {
memoized = memoized.bind(
this,
fn,
options.cache.create(),
options.cache.create({
ttl: options.ttl
}),
options.serializer
)

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

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

const ttlDefault = false

//
// Cache
//

class ObjectWithoutPrototypeCache {
constructor () {
constructor (opts) {
this.cache = Object.create(null)
this.preHas = () => {}
this.preGet = () => {}

if (opts.ttl) {
const ttl = Math.min(24 * 60 * 60 * 1000, Math.max(1, opts.ttl)) // max of 24 hours, min of 1 ms
const ttlKeyExpMap = {}

this.preHas = (key) => {
if (Date.now() > ttlKeyExpMap[key]) {
delete ttlKeyExpMap[key]
delete this.cache[key]
}
}
this.preGet = (key) => {
ttlKeyExpMap[key] = Date.now() + ttl
}

setInterval(() => {
const now = Date.now()
const keys = Object.keys(ttlKeyExpMap)
// The assumption here is that the order of keys is oldest -> most recently created,
// which coresponds to the order of closest exp -> farthest exp.
// So, keep looping thru expiration times until a key hasn't expired.
keys.every((key) => {
if (now > ttlKeyExpMap[key]) {
delete ttlKeyExpMap[key]
return true
}
})
}, opts.ttl)
}
}

has (key) {
this.preHas(key)
return (key in this.cache)
}

get (key) {
this.preGet(key)
return this.cache[key]
}

Expand All @@ -94,5 +134,5 @@ class ObjectWithoutPrototypeCache {
}

const cacheDefault = {
create: () => new ObjectWithoutPrototypeCache()
create: (opts) => new ObjectWithoutPrototypeCache(opts)
}
20 changes: 20 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ test('memoize functions with single non-primitive argument', () => {
expect(numberOfCalls).toBe(1)
})

test('memoize functions with single non-primitive argument and TTL', () => {
let numberOfCalls = 0
function plusPlus (obj) {
numberOfCalls += 1
return obj.number + 1
}

const memoizedPlusPlus = memoize(plusPlus, { ttl: 2 })

memoizedPlusPlus({number: 1})
memoizedPlusPlus({number: 1})
let i = 50000
/* a simple delay */ while (i--) Math.random() * Math.random()
memoizedPlusPlus({number: 1})
memoizedPlusPlus({number: 1})

// Assertions
expect(numberOfCalls).toBe(2)
})

test('memoize functions with N arguments', () => {
function nToThePower (n, power) {
return Math.pow(n, power)
Expand Down

0 comments on commit 69cb2c6

Please sign in to comment.