-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #211 from optimizely/ll/cache-abstraction
Caching Abstraction
- Loading branch information
Showing
7 changed files
with
412 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
import { List, Map, OrderedSet, Record } from 'immutable' | ||
|
||
export const CacheEntry = Record({ | ||
value: null, | ||
storeStates: Map(), | ||
dispatchId: null, | ||
}) | ||
|
||
/******************************************************************************* | ||
* interface PersistentCache { | ||
* has(item) | ||
* lookup(item, notFoundValue) | ||
* hit(item) | ||
* miss(item, entry) | ||
* evict(item) | ||
* asMap() | ||
* } | ||
* | ||
* Inspired by clojure.core.cache/CacheProtocol | ||
*******************************************************************************/ | ||
|
||
/** | ||
* Plain map-based cache | ||
*/ | ||
export class BasicCache { | ||
|
||
/** | ||
* @param {Immutable.Map} cache | ||
*/ | ||
constructor(cache = Map()) { | ||
this.cache = cache; | ||
} | ||
|
||
/** | ||
* Retrieve the associated value, if it exists in this cache, otherwise | ||
* returns notFoundValue (or undefined if not provided) | ||
* @param {Object} item | ||
* @param {Object?} notFoundValue | ||
* @return {CacheEntry?} | ||
*/ | ||
lookup(item, notFoundValue) { | ||
return this.cache.get(item, notFoundValue) | ||
} | ||
|
||
/** | ||
* Checks if this cache contains an associated value | ||
* @param {Object} item | ||
* @return {boolean} | ||
*/ | ||
has(item) { | ||
return this.cache.has(item) | ||
} | ||
|
||
/** | ||
* Return cached items as map | ||
* @return {Immutable.Map} | ||
*/ | ||
asMap() { | ||
return this.cache | ||
} | ||
|
||
/** | ||
* Updates this cache when it is determined to contain the associated value | ||
* @param {Object} item | ||
* @return {BasicCache} | ||
*/ | ||
hit(item) { | ||
return this; | ||
} | ||
|
||
/** | ||
* Updates this cache when it is determined to **not** contain the associated value | ||
* @param {Object} item | ||
* @param {CacheEntry} entry | ||
* @return {BasicCache} | ||
*/ | ||
miss(item, entry) { | ||
return new BasicCache( | ||
this.cache.update(item, existingEntry => { | ||
if (existingEntry && existingEntry.dispatchId > entry.dispatchId) { | ||
throw new Error("Refusing to cache older value") | ||
} | ||
return entry | ||
}) | ||
) | ||
} | ||
|
||
/** | ||
* Removes entry from cache | ||
* @param {Object} item | ||
* @return {BasicCache} | ||
*/ | ||
evict(item) { | ||
return new BasicCache(this.cache.remove(item)) | ||
} | ||
} | ||
|
||
const DEFAULT_LRU_LIMIT = 1000 | ||
const DEFAULT_LRU_EVICT_COUNT = 1 | ||
|
||
/** | ||
* Implements caching strategy that evicts least-recently-used items in cache | ||
* when an item is being added to a cache that has reached a configured size | ||
* limit. | ||
*/ | ||
export class LRUCache { | ||
|
||
constructor(limit = DEFAULT_LRU_LIMIT, evictCount = DEFAULT_LRU_EVICT_COUNT, cache = new BasicCache(), lru = OrderedSet()) { | ||
this.limit = limit; | ||
this.evictCount = evictCount | ||
this.cache = cache; | ||
this.lru = lru; | ||
} | ||
|
||
/** | ||
* Retrieve the associated value, if it exists in this cache, otherwise | ||
* returns notFoundValue (or undefined if not provided) | ||
* @param {Object} item | ||
* @param {Object?} notFoundValue | ||
* @return {CacheEntry} | ||
*/ | ||
lookup(item, notFoundValue) { | ||
return this.cache.lookup(item, notFoundValue) | ||
} | ||
|
||
/** | ||
* Checks if this cache contains an associated value | ||
* @param {Object} item | ||
* @return {boolean} | ||
*/ | ||
has(item) { | ||
return this.cache.has(item) | ||
} | ||
|
||
/** | ||
* Return cached items as map | ||
* @return {Immutable.Map} | ||
*/ | ||
asMap() { | ||
return this.cache.asMap() | ||
} | ||
|
||
/** | ||
* Updates this cache when it is determined to contain the associated value | ||
* @param {Object} item | ||
* @return {LRUCache} | ||
*/ | ||
hit(item) { | ||
if (!this.cache.has(item)) { | ||
return this; | ||
} | ||
|
||
// remove it first to reorder in lru OrderedSet | ||
return new LRUCache(this.limit, this.evictCount, this.cache, this.lru.remove(item).add(item)) | ||
} | ||
|
||
/** | ||
* Updates this cache when it is determined to **not** contain the associated value | ||
* If cache has reached size limit, the LRU item is evicted. | ||
* @param {Object} item | ||
* @param {CacheEntry} entry | ||
* @return {LRUCache} | ||
*/ | ||
miss(item, entry) { | ||
if (this.lru.size >= this.limit) { | ||
if (this.has(item)) { | ||
return new LRUCache( | ||
this.limit, | ||
this.evictCount, | ||
this.cache.miss(item, entry), | ||
this.lru.remove(item).add(item) | ||
) | ||
} | ||
|
||
const cache = (this.lru | ||
.take(this.evictCount) | ||
.reduce((c, evictItem) => c.evict(evictItem), this.cache) | ||
.miss(item, entry)); | ||
|
||
return new LRUCache( | ||
this.limit, | ||
this.evictCount, | ||
cache, | ||
this.lru.skip(this.evictCount).add(item) | ||
) | ||
} else { | ||
return new LRUCache( | ||
this.limit, | ||
this.evictCount, | ||
this.cache.miss(item, entry), | ||
this.lru.add(item) | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Removes entry from cache | ||
* @param {Object} item | ||
* @return {LRUCache} | ||
*/ | ||
evict(item) { | ||
if (!this.cache.has(item)) { | ||
return this; | ||
} | ||
|
||
return new LRUCache( | ||
this.limit, | ||
this.evictCount, | ||
this.cache.evict(item), | ||
this.lru.remove(item) | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Returns default cache strategy | ||
* @return {BasicCache} | ||
*/ | ||
export function DefaultCache() { | ||
return new BasicCache() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.