Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
60 changes: 53 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,61 @@ 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()
// The assumption here is that the order of keys is oldest -> newest,
// which corresponds to the order of soonest exp -> latest exp.
// So, keep looping thru expiration times *until* a key that hasn't expired (via .every()).
Object.keys(ttlKeyExpMap)
.every((key) => {
// note: key has "_" prefix, which helps object key ordering remain as expected, even for stringified numbers
if (now > ttlKeyExpMap[key]) {
delete ttlKeyExpMap[key]
return true
}
// short circuit here -- end looping now
})
},
ttl
)
}
}

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

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

Expand All @@ -94,5 +140,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