Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combined caching strategy #212

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
2 changes: 0 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
"nuclear-js": "^1.0.5",
"webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0",
"grunt-concurrent": "^1.0.0",
"grunt-contrib-connect": "^0.10.1",
"remarkable": "^1.6.0",
"front-matter": "^1.0.0",
"glob": "^5.0.10",
Expand Down
16 changes: 8 additions & 8 deletions src/console-polyfill.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
try {
if (!(window.console && console.log)) {
if (!(window.console && console.log)) { // eslint-disable-line
console = {
log: function(){},
debug: function(){},
info: function(){},
warn: function(){},
error: function(){}
};
log: function() {},
debug: function() {},
info: function() {},
warn: function() {},
error: function() {},
}
}
} catch(e) {}
} catch(e) {} // eslint-disable-line
2 changes: 1 addition & 1 deletion src/create-react-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function(reactor) {
each(this.getDataBindings(), (getter, key) => {
const unwatchFn = reactor.observe(getter, (val) => {
this.setState({
[key]: val
[key]: val,
})
})

Expand Down
39 changes: 39 additions & 0 deletions src/getter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,50 @@ import Immutable, { List } from 'immutable'
import { isFunction, isArray } from './utils'
import { isKeyPath } from './key-path'

const CACHE_OPTIONS = ['default', 'always', 'never']

/**
* Getter helper functions
* A getter is an array with the form:
* [<KeyPath>, ...<KeyPath>, <function>]
*/
const identity = (x) => x

/**
* Add override options to a getter
* @param {getter} getter
* @param {object} options
* @param {boolean} options.cache
* @param {*} options.cacheKey
* @returns {getter}
*/
function Getter(getter, options={}) {
if (!isKeyPath(getter) && !isGetter(getter)) {
throw new Error('createGetter must be passed a keyPath or Getter')
}

if (getter.hasOwnProperty('__options')) {
throw new Error('Cannot reassign options to getter')
}

getter.__options = {}
getter.__options.cache = CACHE_OPTIONS.indexOf(options.cache) > -1 ? options.cache : 'default'
getter.__options.cacheKey = options.cacheKey !== undefined ? options.cacheKey : null
return getter
}

/**
* Retrieve an option from getter options
* @param {getter} getter
* @param {string} Name of option to retrieve
* @returns {*}
*/
function getGetterOption(getter, option) {
if (getter.__options) {
return getter.__options[option]
}
}

/**
* Checks if something is a getter literal, ex: ['dep1', 'dep2', function(dep1, dep2) {...}]
* @param {*} toTest
Expand Down Expand Up @@ -106,7 +143,9 @@ export default {
isGetter,
getComputeFn,
getFlattenedDeps,
getGetterOption,
getStoreDeps,
getDeps,
fromKeyPath,
Getter,
}
3 changes: 2 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Reactor from './reactor'
import Immutable from 'immutable'
import { toJS, toImmutable, isImmutable } from './immutable-helpers'
import { isKeyPath } from './key-path'
import { isGetter } from './getter'
import { isGetter, Getter } from './getter'
import createReactMixin from './create-react-mixin'

export default {
Expand All @@ -17,4 +17,5 @@ export default {
toImmutable,
isImmutable,
createReactMixin,
Getter,
}
31 changes: 29 additions & 2 deletions src/reactor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Immutable from 'immutable'
import createReactMixin from './create-react-mixin'
import * as fns from './reactor/fns'
import { BasicCache, LRUCache } from './reactor/cache'
import { isKeyPath } from './key-path'
import { isGetter } from './getter'
import { toJS } from './immutable-helpers'
Expand All @@ -24,11 +25,33 @@ import {
class Reactor {
constructor(config = {}) {
const debug = !!config.debug
const useCache = config.useCache === undefined ? true : !!config.useCache

let Cache = LRUCache

if (config.cache !== undefined) {
Cache = config.cache
} else if (config.maxItemsToCache !== undefined) {
const maxItemsToCache = Number(config.maxItemsToCache)
if (maxItemsToCache > 0) {
Cache = LRUCache.bind(LRUCache, maxItemsToCache)
} else {
Cache = BasicCache
}
}

const cacheFactory = () => {
return new Cache()
}

const baseOptions = debug ? DEBUG_OPTIONS : PROD_OPTIONS
const initialReactorState = new ReactorState({
debug: debug,
cacheFactory: cacheFactory,
cache: cacheFactory(),
// merge config options with the defaults
options: baseOptions.merge(config.options || {}),
useCache: useCache,
})

this.prevReactorState = initialReactorState
Expand Down Expand Up @@ -88,7 +111,9 @@ class Reactor {
let { observerState, entry } = fns.addObserver(this.observerState, getter, handler)
this.observerState = observerState
return () => {
this.observerState = fns.removeObserverByEntry(this.observerState, entry)
let { observerState, reactorState } = fns.removeObserverByEntry(this.observerState, this.reactorState, entry)
this.observerState = observerState
this.reactorState = reactorState
}
}

Expand All @@ -100,7 +125,9 @@ class Reactor {
throw new Error('Must call unobserve with a Getter')
}

this.observerState = fns.removeObserver(this.observerState, getter, handler)
const { observerState, reactorState } = fns.removeObserver(this.observerState, this.reactorState, getter, handler)
this.observerState = observerState
this.reactorState = reactorState
}

/**
Expand Down
220 changes: 220 additions & 0 deletions src/reactor/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { 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))
}
}

export const DEFAULT_LRU_LIMIT = 1000
export 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)
)
}
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 LRUCache()
}
Loading