Comparing version 0.5.1 to 0.5.2
316
lib/entry.js
"use strict"; | ||
var assert = require("assert"); | ||
var getLocal = require("./local.js").get; | ||
var UNKNOWN_VALUE = Object.create(null); | ||
var emptySetPool = []; | ||
var entryPool = []; | ||
function Entry(fn, key, args) { | ||
assert(this instanceof Entry); | ||
// Since this package might be used browsers, we should avoid using the | ||
// Node built-in assert module. | ||
function assert(condition, optionalMessage) { | ||
if (! condition) { | ||
throw new Error(optionalMessage || "assertion failure"); | ||
} | ||
} | ||
this.fn = fn; | ||
this.key = key; | ||
this.args = args; | ||
this.value = UNKNOWN_VALUE; | ||
this.dirty = true; | ||
function Entry(fn, args) { | ||
this.parents = new Set; | ||
@@ -24,6 +25,30 @@ this.childValues = new Map; | ||
this.subscribe = null; | ||
this.unsubscribe = null; | ||
reset(this, fn, args); | ||
} | ||
function reset(entry, fn, args) { | ||
entry.fn = fn; | ||
entry.args = args; | ||
entry.value = UNKNOWN_VALUE; | ||
entry.dirty = true; | ||
entry.subscribe = null; | ||
entry.unsubscribe = null; | ||
entry.recomputing = false; | ||
} | ||
Entry.acquire = function (fn, args) { | ||
var entry = entryPool.pop(); | ||
if (entry) { | ||
reset(entry, fn, args); | ||
return entry; | ||
} | ||
return new Entry(fn, args); | ||
}; | ||
function release(entry) { | ||
assert(entry.parents.size === 0); | ||
forgetChildren(entry); | ||
entryPool.push(entry); | ||
} | ||
exports.Entry = Entry; | ||
@@ -33,58 +58,52 @@ | ||
// The public API of Entry objects consists of the Entry constructor, | ||
// along with the recompute, setDirty, and dispose methods. | ||
Ep.recompute = function recompute() { | ||
rememberParent(this); | ||
return recomputeIfDirty(this); | ||
}; | ||
Ep.setDirty = function setDirty() { | ||
if (this.dirty) return; | ||
this.dirty = true; | ||
this.parents.forEach(reportDirty, this); | ||
this.value = UNKNOWN_VALUE; | ||
// Since we're explicitly setting this.dirty = true, we won't need to | ||
// examine any of our children in recomputeIfDirty, so we can go ahead | ||
// and forget them. | ||
forgetChildren(this); | ||
reportDirty(this); | ||
}; | ||
Ep.mightBeDirty = function mightBeDirty() { | ||
return this.dirty || | ||
(this.dirtyChildren && | ||
this.dirtyChildren.size > 0); | ||
}; | ||
Ep.recompute = function recompute() { | ||
var parent = this._rememberParent(); | ||
var value = this._recomputeIfDirty(); | ||
if (parent) { | ||
parent.childValues.set(this, this.value); | ||
parent._removeDirtyChild(this); | ||
} | ||
return value; | ||
}; | ||
Ep.dispose = function dispose() { | ||
// If we're no longer going to be subscribed to changes affecting this | ||
// Entry, then we'd better inform its parents that it needs to be | ||
// recomputed. | ||
this.setDirty(); | ||
this._forgetChildren(); | ||
unsubscribe(this); | ||
}; | ||
var entry = this; | ||
forgetChildren(entry); | ||
unsubscribe(entry); | ||
Ep._rememberChild = function _rememberChild(child) { | ||
child.parents.add(this); | ||
// Because this entry has been kicked out of the cache (in index.js), | ||
// we've lost the ability to find out if/when this entry becomes dirty, | ||
// whether that happens through a subscription, because of a direct call | ||
// to entry.setDirty(), or because one of its children becomes dirty. | ||
// Because of this loss of future information, we have to assume the | ||
// worst (that this entry might have become dirty very soon), so we must | ||
// immediately mark this entry's parents as dirty. Normally we could | ||
// just call entry.setDirty() rather than calling parent.setDirty() for | ||
// each parent, but that would leave this entry in parent.childValues | ||
// and parent.dirtyChildren, which would prevent the child from being | ||
// truly forgotten. | ||
entry.parents.forEach(function (parent) { | ||
// Causes parent to forget all of its children, including entry. | ||
parent.setDirty(); | ||
}); | ||
if (! this.childValues.has(child)) { | ||
this.childValues.set(child, UNKNOWN_VALUE); | ||
} | ||
if (child.mightBeDirty()) { | ||
this._reportDirtyChild(child); | ||
} else { | ||
this._reportCleanChild(child); | ||
} | ||
// Since this entry has no parents and no children anymore, and the | ||
// caller of Entry#dispose has indicated that entry.value no longer | ||
// matters, we can safely recycle this Entry object for later use. | ||
release(entry); | ||
}; | ||
Ep._forgetChild = function _forgetChild(child) { | ||
child.parents.delete(this); | ||
this._removeDirtyChild(child); | ||
this.childValues.delete(child); | ||
}; | ||
function setClean(entry) { | ||
entry.dirty = false; | ||
Ep._setClean = function _setClean() { | ||
this.dirty = false; | ||
if (this.dirtyChildren && | ||
this.dirtyChildren.size > 0) { | ||
if (mightBeDirty(entry)) { | ||
// This Entry may still have dirty children, in which case we can't | ||
@@ -95,26 +114,34 @@ // let our parents know we're clean just yet. | ||
this.parents.forEach(reportClean, this); | ||
}; | ||
reportClean(entry); | ||
} | ||
function reportDirty(parent) { | ||
parent._reportDirtyChild(this); | ||
function reportDirty(entry) { | ||
entry.parents.forEach(function (parent) { | ||
reportDirtyChild(parent, entry); | ||
}); | ||
} | ||
function reportClean(parent) { | ||
parent._reportCleanChild(this); | ||
function reportClean(entry) { | ||
entry.parents.forEach(function (parent) { | ||
reportCleanChild(parent, entry); | ||
}); | ||
} | ||
function mightBeDirty(entry) { | ||
return entry.dirty || | ||
(entry.dirtyChildren && | ||
entry.dirtyChildren.size); | ||
} | ||
// Let a parent Entry know that one of its children may be dirty. | ||
Ep._reportDirtyChild = function _reportDirtyChild(child) { | ||
// Must have called this._rememberChild(child) before calling | ||
// this._reportDirtyChild(child). | ||
assert(this.childValues.has(child)); | ||
assert(child.mightBeDirty()); | ||
function reportDirtyChild(entry, child) { | ||
// Must have called rememberParent(child) before calling | ||
// reportDirtyChild(parent, child). | ||
assert(entry.childValues.has(child)); | ||
assert(mightBeDirty(child)); | ||
if (! this.dirtyChildren) { | ||
// Initialize this.dirtyChildren with an empty set drawn from the | ||
// emptySetPool if possible. | ||
this.dirtyChildren = emptySetPool.pop() || new Set; | ||
if (! entry.dirtyChildren) { | ||
entry.dirtyChildren = emptySetPool.pop() || new Set; | ||
} else if (this.dirtyChildren.has(child)) { | ||
} else if (entry.dirtyChildren.has(child)) { | ||
// If we already know this child is dirty, then we must have already | ||
@@ -126,24 +153,28 @@ // informed our own parents that we are dirty, so we can terminate | ||
this.dirtyChildren.add(child); | ||
this.parents.forEach(reportDirty, this); | ||
}; | ||
entry.dirtyChildren.add(child); | ||
reportDirty(entry); | ||
} | ||
// Let a parent Entry know that one of its children is no longer dirty. | ||
Ep._reportCleanChild = function _reportCleanChild(child) { | ||
// Must have called this._rememberChild(child) before calling | ||
// this._reportCleanChild(child). | ||
assert(this.childValues.has(child)); | ||
assert(! child.mightBeDirty()); | ||
if (this.childValues.get(child) !== child.value) { | ||
this.setDirty(); | ||
function reportCleanChild(entry, child) { | ||
// Must have called rememberChild(child) before calling | ||
// reportCleanChild(parent, child). | ||
assert(entry.childValues.has(child)); | ||
assert(! mightBeDirty(child)); | ||
if (entry.childValues.get(child) !== child.value) { | ||
entry.setDirty(); | ||
} | ||
this._removeDirtyChild(child); | ||
}; | ||
// Often we are removing a child because it is no longer dirty, so | ||
// child.dirty is not a precondition for this method. Also note that the | ||
// child may remain in this.childValues, so we definitely do not want to | ||
// call child.parents.delete(this) here. | ||
Ep._removeDirtyChild = function _removeDirtyChild(child) { | ||
var dc = this.dirtyChildren; | ||
removeDirtyChild(entry, child); | ||
if (mightBeDirty(entry)) { | ||
return; | ||
} | ||
reportClean(entry); | ||
} | ||
function removeDirtyChild(entry, child) { | ||
var dc = entry.dirtyChildren; | ||
if (dc) { | ||
@@ -153,21 +184,26 @@ dc.delete(child); | ||
emptySetPool.push(dc); | ||
dc = this.dirtyChildren = null; | ||
entry.dirtyChildren = null; | ||
} | ||
} | ||
} | ||
if (this.dirty || dc) { | ||
return; | ||
} | ||
this.parents.forEach(reportClean, this); | ||
}; | ||
Ep._rememberParent = function _rememberParent() { | ||
function rememberParent(entry) { | ||
var local = getLocal(); | ||
var parent = local.currentParentEntry; | ||
if (parent) { | ||
parent._rememberChild(this); | ||
entry.parents.add(parent); | ||
if (! parent.childValues.has(entry)) { | ||
parent.childValues.set(entry, UNKNOWN_VALUE); | ||
} | ||
if (mightBeDirty(entry)) { | ||
reportDirtyChild(parent, entry); | ||
} else { | ||
reportCleanChild(parent, entry); | ||
} | ||
return parent; | ||
} | ||
}; | ||
} | ||
@@ -180,76 +216,86 @@ // This is the most important method of the Entry API, because it | ||
// (3) this.value is usally returned very quickly, without recomputation. | ||
Ep._recomputeIfDirty = function _recomputeIfDirty() { | ||
if (this.dirty) { | ||
function recomputeIfDirty(entry) { | ||
if (entry.dirty) { | ||
// If this Entry is explicitly dirty because someone called | ||
// entry.setDirty(), recompute. | ||
return this._reallyRecompute(); | ||
return reallyRecompute(entry); | ||
} | ||
if (this.dirtyChildren && | ||
this.dirtyChildren.size > 0) { | ||
if (mightBeDirty(entry)) { | ||
// Get fresh values for any dirty children, and if those values | ||
// disagree with this.childValues, mark this Entry explicitly dirty. | ||
this.dirtyChildren.forEach(function (child) { | ||
assert(this.childValues.has(child)); | ||
entry.dirtyChildren.forEach(function (child) { | ||
assert(entry.childValues.has(child)); | ||
try { | ||
child._recomputeIfDirty(); | ||
recomputeIfDirty(child); | ||
} catch (e) { | ||
this.setDirty(); | ||
entry.setDirty(); | ||
} | ||
}, this); | ||
}); | ||
if (this.dirty) { | ||
if (entry.dirty) { | ||
// If this Entry has become explicitly dirty after comparing the fresh | ||
// values of its dirty children against this.childValues, recompute. | ||
return this._reallyRecompute(); | ||
return reallyRecompute(entry); | ||
} | ||
} | ||
assert.notStrictEqual(this.value, UNKNOWN_VALUE); | ||
assert(entry.value !== UNKNOWN_VALUE); | ||
return this.value; | ||
}; | ||
return entry.value; | ||
} | ||
Ep._reallyRecompute = function _reallyRecompute() { | ||
this._forgetChildren(); | ||
function reallyRecompute(entry) { | ||
assert(! entry.recomputing, "already recomputing"); | ||
entry.recomputing = true; | ||
forgetChildren(entry); | ||
var local = getLocal(); | ||
var parent = local.currentParentEntry; | ||
local.currentParentEntry = this; | ||
local.currentParentEntry = entry; | ||
var threw = true; | ||
try { | ||
this.value = this.fn.apply(null, this.args); | ||
entry.value = entry.fn.apply(null, entry.args); | ||
threw = false; | ||
} finally { | ||
assert.strictEqual(local.currentParentEntry, this); | ||
entry.recomputing = false; | ||
assert(local.currentParentEntry === entry); | ||
local.currentParentEntry = parent; | ||
if (threw || ! subscribe(this)) { | ||
// Mark this Entry dirty if this.fn threw or we failed to | ||
if (threw || ! subscribe(entry)) { | ||
// Mark this Entry dirty if entry.fn threw or we failed to | ||
// resubscribe. This is important because, if we have a subscribe | ||
// function and it failed, then we're going to miss important | ||
// notifications about the potential dirtiness of this.value. | ||
this.setDirty(); | ||
// notifications about the potential dirtiness of entry.value. | ||
entry.setDirty(); | ||
} else { | ||
// If we successfully recomputed this.value and did not fail to | ||
// If we successfully recomputed entry.value and did not fail to | ||
// (re)subscribe, then this Entry is no longer explicitly dirty. | ||
this._setClean(); | ||
setClean(entry); | ||
} | ||
} | ||
return this.value; | ||
}; | ||
return entry.value; | ||
} | ||
Ep._forgetChildren = function _forgetChildren() { | ||
this.childValues.forEach(function (value, child) { | ||
this._forgetChild(child); | ||
}, this); | ||
function forgetChildren(entry) { | ||
entry.childValues.forEach(function (value, child) { | ||
forgetChild(entry, child); | ||
}); | ||
// After we forget all our children, this.dirtyChildren must be empty | ||
// and thus have been reset to null. | ||
assert.strictEqual(this.dirtyChildren, null); | ||
}; | ||
// and therefor must have been reset to null. | ||
assert(entry.dirtyChildren === null); | ||
} | ||
function forgetChild(entry, child) { | ||
child.parents.delete(entry); | ||
entry.childValues.delete(child); | ||
removeDirtyChild(entry, child); | ||
} | ||
function subscribe(entry) { | ||
@@ -256,0 +302,0 @@ if (typeof entry.subscribe === "function") { |
@@ -52,3 +52,3 @@ "use strict"; | ||
} else { | ||
cache.set(key, entry = new Entry(fn, key, args)); | ||
cache.set(key, entry = Entry.acquire(fn, args)); | ||
entry.subscribe = options.subscribe; | ||
@@ -55,0 +55,0 @@ } |
{ | ||
"name": "optimism", | ||
"version": "0.5.1", | ||
"version": "0.5.2", | ||
"author": "Ben Newman <ben@benjamn.com>", | ||
@@ -5,0 +5,0 @@ "description": "Composable reactive caching with efficient invalidation.", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
13631
346