diff --git a/README.md b/README.md index 02b393b..f81747a 100644 --- a/README.md +++ b/README.md @@ -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 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 diff --git a/src/index.js b/src/index.js index 7f33a2b..aeb52de 100644 --- a/src/index.js +++ b/src/index.js @@ -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 }) } @@ -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) { @@ -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) @@ -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) @@ -58,7 +61,9 @@ function strategyDefault (fn, options) { memoized = memoized.bind( this, fn, - options.cache.create(), + options.cache.create({ + ttl: options.ttl + }), options.serializer ) @@ -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] } @@ -94,5 +134,5 @@ class ObjectWithoutPrototypeCache { } const cacheDefault = { - create: () => new ObjectWithoutPrototypeCache() + create: (opts) => new ObjectWithoutPrototypeCache(opts) } diff --git a/test/index.js b/test/index.js index d98fede..9b8b122 100644 --- a/test/index.js +++ b/test/index.js @@ -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)