From 7ea7f6099260034d2ecf07e3b432a7faaebeded2 Mon Sep 17 00:00:00 2001 From: Wes Roberts Date: Tue, 9 Jun 2020 19:02:17 -0400 Subject: [PATCH] RrMapCache and RrObjectCache --- .../bundles-snapshot.test.js.snap | 123 ++++++++++++++++++ src/cache/README.md | 7 +- src/cache/RrMapCache.js | 41 ++++++ src/cache/RrObjectCache.js | 44 +++++++ src/cache/__tests__/RrMapCache.spec.js | 12 ++ src/cache/__tests__/RrObjectCache.spec.js | 12 ++ src/cache/__util__/testBasicBehavior.js | 5 + src/cache/__util__/testRrBehavior.js | 30 +++++ src/index.d.ts | 17 +++ src/index.js | 2 + 10 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 src/cache/RrMapCache.js create mode 100644 src/cache/RrObjectCache.js create mode 100644 src/cache/__tests__/RrMapCache.spec.js create mode 100644 src/cache/__tests__/RrObjectCache.spec.js create mode 100644 src/cache/__util__/testRrBehavior.js diff --git a/jest/__snapshots__/bundles-snapshot.test.js.snap b/jest/__snapshots__/bundles-snapshot.test.js.snap index b6bb64c3..2b47a918 100644 --- a/jest/__snapshots__/bundles-snapshot.test.js.snap +++ b/jest/__snapshots__/bundles-snapshot.test.js.snap @@ -370,12 +370,135 @@ exports[`Dist bundle is unchanged 1`] = ` return LruMapCache; }(); + var RrObjectCache = /*#__PURE__*/function () { + function RrObjectCache(_temp) { + var _ref = _temp === void 0 ? {} : _temp, + cacheSize = _ref.cacheSize; + + validateCacheSize(cacheSize); + this._cache = {}; + this._cacheKeys = []; + this._cacheLength = 0; + this._cacheSize = cacheSize; + } + + var _proto = RrObjectCache.prototype; + + _proto.set = function set(key, selectorFn) { + if (this._cacheLength >= this._cacheSize) { + this._randomReplace(key, selectorFn); + } else { + this._cache[key] = selectorFn; + this._cacheKeys[this._cacheLength] = key; + this._cacheLength++; + } + }; + + _proto.get = function get(key) { + return this._cache[key]; + }; + + _proto.remove = function remove(key) { + var index = this._cacheKeys.indexOf(key); // O(1) + + + if (index > -1) { + delete this._cache[key]; + var lastIndex = this._cacheLength - 1; + var lastKey = this._cacheKeys[lastIndex]; + this._cacheKeys[index] = lastKey; + this._cacheLength--; + } + }; + + _proto.clear = function clear() { + this._cache = {}; + this._cacheKeys = []; + this._cacheLength = 0; + }; + + _proto._randomReplace = function _randomReplace(newKey, newValue) { + var index = Math.floor(Math.random() * this._cacheLength); + var key = this._cacheKeys[index]; + delete this._cache[key]; + this._cacheKeys[index] = newKey; + this._cache[newKey] = newValue; + }; + + _proto.isValidCacheKey = function isValidCacheKey(cacheKey) { + return isStringOrNumber(cacheKey); + }; + + return RrObjectCache; + }(); + + var RrMapCache = /*#__PURE__*/function () { + function RrMapCache(_temp) { + var _ref = _temp === void 0 ? {} : _temp, + cacheSize = _ref.cacheSize; + + validateCacheSize(cacheSize); + this.clear(); + this._cacheSize = cacheSize; + } + + var _proto = RrMapCache.prototype; + + _proto.set = function set(key, selectorFn) { + if (this._cacheLength >= this._cacheSize) { + this._randomReplace(key, selectorFn); + } else { + this._cache.set(key, selectorFn); + + this._cacheKeys[this._cacheLength] = key; + this._cacheLength++; + } + }; + + _proto.get = function get(key) { + return this._cache.get(key); + }; + + _proto.remove = function remove(key) { + var index = this._cacheKeys.indexOf(key); + + if (index > -1) { + delete this._cache[\\"delete\\"](key); + var lastIndex = this._cacheLength - 1; + var lastKey = this._cacheKeys[lastIndex]; + this._cacheKeys[index] = lastKey; + this._cacheLength--; + } + }; + + _proto.clear = function clear() { + this._cache = new Map(); + this._cacheKeys = []; + this._cacheLength = 0; + }; + + _proto._randomReplace = function _randomReplace(newKey, newValue) { + var index = Math.floor(Math.random() * this._cacheLength); + var key = this._cacheKeys[index]; + + this._cache[\\"delete\\"](key); + + this._cacheKeys[index] = newKey; + + this._cache.set(newKey, newValue); + }; + + return RrMapCache; + }(); + exports.FifoMapCache = FifoMapCache; exports.FifoObjectCache = FifoObjectCache; exports.FlatMapCache = FlatMapCache; exports.FlatObjectCache = FlatObjectCache; exports.LruMapCache = LruMapCache; exports.LruObjectCache = LruObjectCache; + exports.RrMapCache = RrMapCache; + exports.RrObjectCache = RrObjectCache; exports.createCachedSelector = createCachedSelector; exports.createStructuredCachedSelector = createStructuredCachedSelector; exports.default = createCachedSelector; diff --git a/src/cache/README.md b/src/cache/README.md index 3c67c94e..014f8660 100644 --- a/src/cache/README.md +++ b/src/cache/README.md @@ -4,16 +4,18 @@ ## Available cache objects -`re-reselect` ships with **6 ready-to-use cache object constructors**: +`re-reselect` ships with several **ready-to-use cache object constructors**: | name | accepted cacheKey | type | storage | | :---------------------------------------: | :---------------: | :-----------------------------------: | :----------------------------: | | [`FlatObjectCache`](./FlatObjectCache.js) | `number` `string` | flat unlimited | JS object | | [`FifoObjectCache`](./FifoObjectCache.js) | `number` `string` | [first in first out][docs-fifo-cache] | JS object | | [`LruObjectCache`](./LruObjectCache.js) | `number` `string` | [least recently used][docs-lru-cache] | JS object | +| [`RrObjectCache`](./RrObjectCache.js) | `number` `string` | [random replacement][docs-rr-cache] | JS object | | [`FlatMapCache`](./FlatMapCache.js) | any | flat unlimited | [Map object][docs-mozilla-map] | -| [`FifoMapCache`](./FifoMapCache.js) | any | [first in first out][docs-fifo-cache] | [Map object][docs-mozilla-map] | +| [`FifoMapCache`](./FifoMapCache.js) | any | [first in first out][docs-fifo-cache] | [Map object][docs-mozilla-map] | | [`LruMapCache`](./LruMapCache.js) | any | [least recently used][docs-lru-cache] | [Map object][docs-mozilla-map] | +| [`RrMapCache`](./RrMapCache.js) | any | [random replacement][docs-rr-cache] | [Map object][docs-mozilla-map] | ```js @@ -53,4 +55,5 @@ interface ICacheObject { [wiki-strategy-pattern]: https://en.wikipedia.org/wiki/Strategy_pattern [docs-fifo-cache]: https://en.wikipedia.org/wiki/Cache_replacement_policies#First_in_first_out_(FIFO) [docs-lru-cache]: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) +[docs-rr-cache]: https://en.wikipedia.org/wiki/Cache_replacement_policies#Random_replacement_(RR) [docs-mozilla-map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map diff --git a/src/cache/RrMapCache.js b/src/cache/RrMapCache.js new file mode 100644 index 00000000..cde4ad65 --- /dev/null +++ b/src/cache/RrMapCache.js @@ -0,0 +1,41 @@ +import validateCacheSize from './util/validateCacheSize'; +import isStringOrNumber from './util/isStringOrNumber'; + +export default class RrMapCache { + constructor({cacheSize} = {}) { + validateCacheSize(cacheSize); + this.clear(); + this._cacheSize = cacheSize; + } + set(key, selectorFn) { + if (this._cacheLength >= this._cacheSize) { + this._randomReplace(key, selectorFn); + } else { + this._cache.set(key, selectorFn); + this._cacheKeys[this._cacheLength] = key; + this._cacheLength++; + } + } + get(key) { + return this._cache.get(key); + } + remove(key) { + const index = this._cacheKeys.indexOf(key); // O(1) + if (index > -1) { + delete this._cache.delete(key); + this._cacheLength--; + this._cacheKeys[index] = this._cacheKeys[this._cacheLength]; + } + } + clear() { + this._cache = new Map(); + this._cacheKeys = []; + this._cacheLength = 0; + } + _randomReplace(newKey, newValue) { + const index = Math.floor(Math.random() * this._cacheLength); + this._cache.delete(this._cacheKeys[index]); + this._cacheKeys[index] = newKey; + this._cache.set(newKey, newValue); + } +} diff --git a/src/cache/RrObjectCache.js b/src/cache/RrObjectCache.js new file mode 100644 index 00000000..6440ed48 --- /dev/null +++ b/src/cache/RrObjectCache.js @@ -0,0 +1,44 @@ +import validateCacheSize from './util/validateCacheSize'; +import isStringOrNumber from './util/isStringOrNumber'; + +export default class RrObjectCache { + constructor({cacheSize} = {}) { + validateCacheSize(cacheSize); + this.clear(); + this._cacheSize = cacheSize; + } + set(key, selectorFn) { + if (this._cacheLength >= this._cacheSize) { + this._randomReplace(key, selectorFn); + } else { + this._cache[key] = selectorFn; + this._cacheKeys[this._cacheLength] = key; + this._cacheLength++; + } + } + get(key) { + return this._cache[key]; + } + remove(key) { + const index = this._cacheKeys.indexOf(key); // O(1) + if (index > -1) { + delete this._cache[key]; + this._cacheLength--; + this._cacheKeys[index] = this._cacheKeys[this._cacheLength]; + } + } + clear() { + this._cache = {}; + this._cacheKeys = []; + this._cacheLength = 0; + } + _randomReplace(newKey, newValue) { + const index = Math.floor(Math.random() * this._cacheLength); + delete this._cache[this._cacheKeys[index]]; + this._cacheKeys[index] = newKey; + this._cache[newKey] = newValue; + } + isValidCacheKey(cacheKey) { + return isStringOrNumber(cacheKey); + } +} diff --git a/src/cache/__tests__/RrMapCache.spec.js b/src/cache/__tests__/RrMapCache.spec.js new file mode 100644 index 00000000..f553b2cc --- /dev/null +++ b/src/cache/__tests__/RrMapCache.spec.js @@ -0,0 +1,12 @@ +import {RrMapCache as CacheObject} from '../../../src/index'; +import testBasicBehavior from '../__util__/testBasicBehavior'; +import testRrBehavior from '../__util__/testRrBehavior'; +import testCacheSizeOptionValidation from '../__util__/testCacheSizeOptionValidation'; +import testMapCacheKeyBehavior from '../__util__/testMapCacheKeyBehavior'; + +describe('RrMapCache', () => { + testBasicBehavior(CacheObject, {cacheSize: 10}); + testRrBehavior(CacheObject); + testCacheSizeOptionValidation(CacheObject); + testMapCacheKeyBehavior(CacheObject, {cacheSize: 10}); +}); diff --git a/src/cache/__tests__/RrObjectCache.spec.js b/src/cache/__tests__/RrObjectCache.spec.js new file mode 100644 index 00000000..4d69bf55 --- /dev/null +++ b/src/cache/__tests__/RrObjectCache.spec.js @@ -0,0 +1,12 @@ +import {RrObjectCache as CacheObject} from '../../../src/index'; +import testBasicBehavior from '../__util__/testBasicBehavior'; +import testRrBehavior from '../__util__/testRrBehavior'; +import testCacheSizeOptionValidation from '../__util__/testCacheSizeOptionValidation'; +import testObjectCacheKeyBehavior from '../__util__/testObjectCacheKeyBehavior'; + +describe('RrObjectCache', () => { + testBasicBehavior(CacheObject, {cacheSize: 10}); + testRrBehavior(CacheObject); + testCacheSizeOptionValidation(CacheObject); + testObjectCacheKeyBehavior(CacheObject, {cacheSize: 10}); +}); diff --git a/src/cache/__util__/testBasicBehavior.js b/src/cache/__util__/testBasicBehavior.js index 34d1284f..020c9c73 100644 --- a/src/cache/__util__/testBasicBehavior.js +++ b/src/cache/__util__/testBasicBehavior.js @@ -36,6 +36,11 @@ function testBasicBehavior(CacheObject, options) { expect(cache.get(entry)).toBe(undefined); }); }); + + it('removes non-existant keys', () => { + const cache = new CacheObject({cacheSize: 5}); + expect(() => cache.remove('foo')).not.toThrow(); + }); }); } diff --git a/src/cache/__util__/testRrBehavior.js b/src/cache/__util__/testRrBehavior.js new file mode 100644 index 00000000..30efc15d --- /dev/null +++ b/src/cache/__util__/testRrBehavior.js @@ -0,0 +1,30 @@ +import fillCacheWith from './fillCacheWith'; + +function testRrBehavior(CacheObject) { + describe('RR cache behavior', () => { + it('limits cache size by removing a random item', () => { + const cache = new CacheObject({cacheSize: 5}); + const entries = [1, 2, 3, 4, 5, 6]; + const get = cache.get.bind(cache); + fillCacheWith(cache, entries); + expect(entries.every(get)).toBe(false); + }); + + it('shrinks when removing existing items manually', () => { + const cache = new CacheObject({cacheSize: 5}); + const entries = [1, 2, 3, 4, 5]; + const get = cache.get.bind(cache); + fillCacheWith(cache, entries); + expect(entries.every(get)).toBe(true); + cache.remove('non-existant'); + cache.remove(5); + expect(cache.get(5)).toBeUndefined(); + cache.set(5, 5); + expect(entries.every(get)).toBe(true); + cache.set(6, 6); + expect(entries.every(get)).toBe(false); + }); + }); +} + +export default testRrBehavior; diff --git a/src/index.d.ts b/src/index.d.ts index 46682d07..c102f0a6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -4327,6 +4327,15 @@ export class LruObjectCache implements ICacheObject { isValidCacheKey(key: ObjectCacheKey): boolean; } +export class RrObjectCache implements ICacheObject { + constructor(options: {cacheSize: number}); + set(key: ObjectCacheKey, selectorFn: any): void; + get(key: ObjectCacheKey): any; + remove(key: ObjectCacheKey): void; + clear(): void; + isValidCacheKey(key: ObjectCacheKey): boolean; +} + export class FlatMapCache implements ICacheObject { set(key: any, selectorFn: any): void; get(key: any): any; @@ -4350,6 +4359,14 @@ export class LruMapCache implements ICacheObject { clear(): void; } +export class RrMapCache implements ICacheObject { + constructor(options: {cacheSize: number}); + set(key: any, selectorFn: any): void; + get(key: any): any; + remove(key: any): void; + clear(): void; +} + /* * Key selector creators */ diff --git a/src/index.js b/src/index.js index f32b037d..c0f19317 100644 --- a/src/index.js +++ b/src/index.js @@ -11,3 +11,5 @@ export {default as LruObjectCache} from './cache/LruObjectCache'; export {default as FlatMapCache} from './cache/FlatMapCache'; export {default as FifoMapCache} from './cache/FifoMapCache'; export {default as LruMapCache} from './cache/LruMapCache'; +export {default as RrObjectCache} from './cache/RrObjectCache'; +export {default as RrMapCache} from './cache/RrMapCache';