Comparing version 0.8.0 to 0.8.1
@@ -121,6 +121,5 @@ function defaultDispose() { } | ||
var Entry = /** @class */ (function () { | ||
function Entry(fn, args, key) { | ||
function Entry(fn, args) { | ||
this.fn = fn; | ||
this.args = args; | ||
this.key = key; | ||
this.parents = new Set(); | ||
@@ -138,3 +137,3 @@ this.childValues = new Map(); | ||
Entry.prototype.recompute = function () { | ||
if (!this.rememberParent() && this.maybeReportOrphan()) { | ||
if (!rememberParent(this) && maybeReportOrphan(this)) { | ||
// The recipient of the entry.reportOrphan callback decided to dispose | ||
@@ -145,3 +144,3 @@ // of this orphan entry by calling entry.dispose(), so we don't need to | ||
} | ||
return this.recomputeIfDirty(); | ||
return recomputeIfDirty(this); | ||
}; | ||
@@ -156,12 +155,12 @@ Entry.prototype.isOrphan = function () { | ||
this.value = UNKNOWN_VALUE; | ||
this.reportDirty(); | ||
reportDirty(this); | ||
// We can go ahead and unsubscribe here, since any further dirty | ||
// notifications we receive will be redundant, and unsubscribing may | ||
// free up some resources, e.g. file watchers. | ||
this.maybeUnsubscribe(); | ||
maybeUnsubscribe(this); | ||
}; | ||
Entry.prototype.dispose = function () { | ||
var _this = this; | ||
this.forgetChildren().forEach(function (child) { return child.maybeReportOrphan(); }); | ||
this.maybeUnsubscribe(); | ||
forgetChildren(this).forEach(maybeReportOrphan); | ||
maybeUnsubscribe(this); | ||
// Because this entry has been kicked out of the cache (in index.js), | ||
@@ -180,226 +179,221 @@ // we've lost the ability to find out if/when this entry becomes dirty, | ||
parent.setDirty(); | ||
parent.forgetChild(_this); | ||
forgetChild(parent, _this); | ||
}); | ||
}; | ||
Entry.prototype.rememberParent = function () { | ||
var local = get(); | ||
var parent = local.currentParentEntry; | ||
if (parent) { | ||
this.parents.add(parent); | ||
if (!parent.childValues.has(this)) { | ||
parent.childValues.set(this, UNKNOWN_VALUE); | ||
} | ||
if (this.mightBeDirty()) { | ||
parent.reportDirtyChild(this); | ||
} | ||
else { | ||
parent.reportCleanChild(this); | ||
} | ||
return parent; | ||
Entry.count = 0; | ||
Entry.POOL_TARGET_SIZE = 100; | ||
return Entry; | ||
}()); | ||
function rememberParent(child) { | ||
var local = get(); | ||
var parent = local.currentParentEntry; | ||
if (parent) { | ||
child.parents.add(parent); | ||
if (!parent.childValues.has(child)) { | ||
parent.childValues.set(child, UNKNOWN_VALUE); | ||
} | ||
}; | ||
// This is the most important method of the Entry API, because it | ||
// determines whether the cached entry.value can be returned immediately, | ||
// or must be recomputed. The overall performance of the caching system | ||
// depends on the truth of the following observations: (1) this.dirty is | ||
// usually false, (2) this.dirtyChildren is usually null/empty, and thus | ||
// (3) this.value is usally returned very quickly, without recomputation. | ||
Entry.prototype.recomputeIfDirty = function () { | ||
var _this = this; | ||
if (this.dirty) { | ||
// If this Entry is explicitly dirty because someone called | ||
// entry.setDirty(), recompute. | ||
return this.reallyRecompute(); | ||
if (mightBeDirty(child)) { | ||
reportDirtyChild(parent, child); | ||
} | ||
if (this.mightBeDirty()) { | ||
// 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)); | ||
try { | ||
child.recomputeIfDirty(); | ||
} | ||
catch (e) { | ||
_this.setDirty(); | ||
} | ||
}); | ||
if (this.dirty) { | ||
// If this Entry has become explicitly dirty after comparing the fresh | ||
// values of its dirty children against this.childValues, recompute. | ||
return this.reallyRecompute(); | ||
} | ||
else { | ||
reportCleanChild(parent, child); | ||
} | ||
assert(this.value !== UNKNOWN_VALUE); | ||
return this.value; | ||
}; | ||
Entry.prototype.reallyRecompute = function () { | ||
assert(!this.recomputing, "already recomputing"); | ||
this.recomputing = true; | ||
// Since this recomputation is likely to re-remember some of this | ||
// entry's children, we forget our children here but do not call | ||
// maybeReportOrphan until after the recomputation finishes. | ||
var originalChildren = this.forgetChildren(); | ||
var local = get(); | ||
var parent = local.currentParentEntry; | ||
local.currentParentEntry = this; | ||
var threw = true; | ||
try { | ||
this.value = this.fn.apply(null, this.args); | ||
threw = false; | ||
} | ||
finally { | ||
this.recomputing = false; | ||
assert(local.currentParentEntry === this); | ||
local.currentParentEntry = parent; | ||
if (threw || !this.maybeSubscribe()) { | ||
// 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 entry.value. | ||
this.setDirty(); | ||
return parent; | ||
} | ||
} | ||
// This is the most important method of the Entry API, because it | ||
// determines whether the cached entry.value can be returned immediately, | ||
// or must be recomputed. The overall performance of the caching system | ||
// depends on the truth of the following observations: (1) this.dirty is | ||
// usually false, (2) this.dirtyChildren is usually null/empty, and thus | ||
// (3) this.value is usally returned very quickly, without recomputation. | ||
function recomputeIfDirty(entry) { | ||
if (entry.dirty) { | ||
// If this Entry is explicitly dirty because someone called | ||
// entry.setDirty(), recompute. | ||
return reallyRecompute(entry); | ||
} | ||
if (mightBeDirty(entry)) { | ||
// Get fresh values for any dirty children, and if those values | ||
// disagree with this.childValues, mark this Entry explicitly dirty. | ||
entry.dirtyChildren.forEach(function (child) { | ||
assert(entry.childValues.has(child)); | ||
try { | ||
recomputeIfDirty(child); | ||
} | ||
else { | ||
// If we successfully recomputed entry.value and did not fail to | ||
// (re)subscribe, then this Entry is no longer explicitly dirty. | ||
this.setClean(); | ||
catch (e) { | ||
entry.setDirty(); | ||
} | ||
}); | ||
if (entry.dirty) { | ||
// If this Entry has become explicitly dirty after comparing the fresh | ||
// values of its dirty children against this.childValues, recompute. | ||
return reallyRecompute(entry); | ||
} | ||
// Now that we've had a chance to re-remember any children that were | ||
// involved in the recomputation, we can safely report any orphan | ||
// children that remain. | ||
originalChildren.forEach(function (child) { return child.maybeReportOrphan(); }); | ||
return this.value; | ||
}; | ||
Entry.prototype.mightBeDirty = function () { | ||
return this.dirty || !!(this.dirtyChildren && this.dirtyChildren.size); | ||
}; | ||
Entry.prototype.setClean = function () { | ||
this.dirty = false; | ||
if (this.mightBeDirty()) { | ||
// This Entry may still have dirty children, in which case we can't | ||
// let our parents know we're clean just yet. | ||
return; | ||
} | ||
assert(entry.value !== UNKNOWN_VALUE); | ||
return entry.value; | ||
} | ||
function reallyRecompute(entry) { | ||
assert(!entry.recomputing, "already recomputing"); | ||
entry.recomputing = true; | ||
// Since this recomputation is likely to re-remember some of this | ||
// entry's children, we forget our children here but do not call | ||
// maybeReportOrphan until after the recomputation finishes. | ||
var originalChildren = forgetChildren(entry); | ||
var local = get(); | ||
var parent = local.currentParentEntry; | ||
local.currentParentEntry = entry; | ||
var threw = true; | ||
try { | ||
entry.value = entry.fn.apply(null, entry.args); | ||
threw = false; | ||
} | ||
finally { | ||
entry.recomputing = false; | ||
assert(local.currentParentEntry === entry); | ||
local.currentParentEntry = parent; | ||
if (threw || !maybeSubscribe(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 entry.value. | ||
entry.setDirty(); | ||
} | ||
this.reportClean(); | ||
}; | ||
Entry.prototype.reportDirty = function () { | ||
var _this = this; | ||
this.parents.forEach(function (parent) { return parent.reportDirtyChild(_this); }); | ||
}; | ||
Entry.prototype.reportClean = function () { | ||
var _this = this; | ||
this.parents.forEach(function (parent) { return parent.reportCleanChild(_this); }); | ||
}; | ||
// Let a parent Entry know that one of its children may be dirty. | ||
Entry.prototype.reportDirtyChild = function (child) { | ||
// Must have called rememberParent(child) before calling | ||
// reportDirtyChild(parent, child). | ||
assert(this.childValues.has(child)); | ||
assert(child.mightBeDirty()); | ||
if (!this.dirtyChildren) { | ||
this.dirtyChildren = emptySetPool.pop() || new Set; | ||
else { | ||
// If we successfully recomputed entry.value and did not fail to | ||
// (re)subscribe, then this Entry is no longer explicitly dirty. | ||
setClean(entry); | ||
} | ||
else if (this.dirtyChildren.has(child)) { | ||
// If we already know this child is dirty, then we must have already | ||
// informed our own parents that we are dirty, so we can terminate | ||
// the recursion early. | ||
return; | ||
} | ||
this.dirtyChildren.add(child); | ||
this.reportDirty(); | ||
}; | ||
// Let a parent Entry know that one of its children is no longer dirty. | ||
Entry.prototype.reportCleanChild = function (child) { | ||
// Must have called rememberChild(child) before calling | ||
// reportCleanChild(parent, child). | ||
assert(this.childValues.has(child)); | ||
assert(!child.mightBeDirty()); | ||
var childValue = this.childValues.get(child); | ||
if (childValue === UNKNOWN_VALUE) { | ||
this.childValues.set(child, child.value); | ||
} | ||
else if (childValue !== child.value) { | ||
this.setDirty(); | ||
} | ||
this.removeDirtyChild(child); | ||
if (this.mightBeDirty()) { | ||
return; | ||
} | ||
this.reportClean(); | ||
}; | ||
Entry.prototype.removeDirtyChild = function (child) { | ||
var dc = this.dirtyChildren; | ||
if (dc) { | ||
dc["delete"](child); | ||
if (dc.size === 0) { | ||
if (emptySetPool.length < Entry.POOL_TARGET_SIZE) { | ||
emptySetPool.push(dc); | ||
} | ||
this.dirtyChildren = null; | ||
} | ||
// Now that we've had a chance to re-remember any children that were | ||
// involved in the recomputation, we can safely report any orphan | ||
// children that remain. | ||
originalChildren.forEach(maybeReportOrphan); | ||
return entry.value; | ||
} | ||
function mightBeDirty(entry) { | ||
return entry.dirty || !!(entry.dirtyChildren && entry.dirtyChildren.size); | ||
} | ||
function setClean(entry) { | ||
entry.dirty = false; | ||
if (mightBeDirty(entry)) { | ||
// This Entry may still have dirty children, in which case we can't | ||
// let our parents know we're clean just yet. | ||
return; | ||
} | ||
reportClean(entry); | ||
} | ||
function reportDirty(child) { | ||
child.parents.forEach(function (parent) { return reportDirtyChild(parent, child); }); | ||
} | ||
function reportClean(child) { | ||
child.parents.forEach(function (parent) { return reportCleanChild(parent, child); }); | ||
} | ||
// Let a parent Entry know that one of its children may be dirty. | ||
function reportDirtyChild(parent, child) { | ||
// Must have called rememberParent(child) before calling | ||
// reportDirtyChild(parent, child). | ||
assert(parent.childValues.has(child)); | ||
assert(mightBeDirty(child)); | ||
if (!parent.dirtyChildren) { | ||
parent.dirtyChildren = emptySetPool.pop() || new Set; | ||
} | ||
else if (parent.dirtyChildren.has(child)) { | ||
// If we already know this child is dirty, then we must have already | ||
// informed our own parents that we are dirty, so we can terminate | ||
// the recursion early. | ||
return; | ||
} | ||
parent.dirtyChildren.add(child); | ||
reportDirty(parent); | ||
} | ||
// Let a parent Entry know that one of its children is no longer dirty. | ||
function reportCleanChild(parent, child) { | ||
// Must have called rememberChild(child) before calling | ||
// reportCleanChild(parent, child). | ||
assert(parent.childValues.has(child)); | ||
assert(!mightBeDirty(child)); | ||
var childValue = parent.childValues.get(child); | ||
if (childValue === UNKNOWN_VALUE) { | ||
parent.childValues.set(child, child.value); | ||
} | ||
else if (childValue !== child.value) { | ||
parent.setDirty(); | ||
} | ||
removeDirtyChild(parent, child); | ||
if (mightBeDirty(parent)) { | ||
return; | ||
} | ||
reportClean(parent); | ||
} | ||
function removeDirtyChild(parent, child) { | ||
var dc = parent.dirtyChildren; | ||
if (dc) { | ||
dc["delete"](child); | ||
if (dc.size === 0) { | ||
if (emptySetPool.length < Entry.POOL_TARGET_SIZE) { | ||
emptySetPool.push(dc); | ||
} | ||
parent.dirtyChildren = null; | ||
} | ||
}; | ||
// If the given entry has a reportOrphan method, and no remaining parents, | ||
// call entry.reportOrphan and return true iff it returns true. The | ||
// reportOrphan function should return true to indicate entry.dispose() | ||
// has been called, and the entry has been removed from any other caches | ||
// (see index.js for the only current example). | ||
Entry.prototype.maybeReportOrphan = function () { | ||
var report = this.reportOrphan; | ||
return typeof report === "function" && | ||
this.parents.size === 0 && | ||
report(this) === true; | ||
}; | ||
// Removes all children from this entry and returns an array of the | ||
// removed children. | ||
Entry.prototype.forgetChildren = function () { | ||
var _this = this; | ||
var children = reusableEmptyArray; | ||
if (this.childValues.size > 0) { | ||
children = []; | ||
this.childValues.forEach(function (value, child) { | ||
_this.forgetChild(child); | ||
children.push(child); | ||
}); | ||
} | ||
} | ||
// If the given entry has a reportOrphan method, and no remaining parents, | ||
// call entry.reportOrphan and return true iff it returns true. The | ||
// reportOrphan function should return true to indicate entry.dispose() | ||
// has been called, and the entry has been removed from any other caches | ||
// (see index.js for the only current example). | ||
function maybeReportOrphan(entry) { | ||
return entry.parents.size === 0 && | ||
typeof entry.reportOrphan === "function" && | ||
entry.reportOrphan() === true; | ||
} | ||
// Removes all children from this entry and returns an array of the | ||
// removed children. | ||
function forgetChildren(parent) { | ||
var children = reusableEmptyArray; | ||
if (parent.childValues.size > 0) { | ||
children = []; | ||
parent.childValues.forEach(function (value, child) { | ||
forgetChild(parent, child); | ||
children.push(child); | ||
}); | ||
} | ||
// After we forget all our children, this.dirtyChildren must be empty | ||
// and therefore must have been reset to null. | ||
assert(parent.dirtyChildren === null); | ||
return children; | ||
} | ||
function forgetChild(parent, child) { | ||
child.parents["delete"](parent); | ||
parent.childValues["delete"](child); | ||
removeDirtyChild(parent, child); | ||
} | ||
function maybeSubscribe(entry) { | ||
if (typeof entry.subscribe === "function") { | ||
try { | ||
maybeUnsubscribe(entry); // Prevent double subscriptions. | ||
entry.unsubscribe = entry.subscribe.apply(null, entry.args); | ||
} | ||
// After we forget all our children, this.dirtyChildren must be empty | ||
// and therefore must have been reset to null. | ||
assert(this.dirtyChildren === null); | ||
return children; | ||
}; | ||
Entry.prototype.forgetChild = function (child) { | ||
child.parents["delete"](this); | ||
this.childValues["delete"](child); | ||
this.removeDirtyChild(child); | ||
}; | ||
Entry.prototype.maybeSubscribe = function () { | ||
if (typeof this.subscribe === "function") { | ||
try { | ||
this.maybeUnsubscribe(); // Prevent double subscriptions. | ||
this.unsubscribe = this.subscribe.apply(null, this.args); | ||
} | ||
catch (e) { | ||
// If this Entry has a subscribe function and it threw an exception | ||
// (or an unsubscribe function it previously returned now throws), | ||
// return false to indicate that we were not able to subscribe (or | ||
// unsubscribe), and this Entry should remain dirty. | ||
this.setDirty(); | ||
return false; | ||
} | ||
catch (e) { | ||
// If this Entry has a subscribe function and it threw an exception | ||
// (or an unsubscribe function it previously returned now throws), | ||
// return false to indicate that we were not able to subscribe (or | ||
// unsubscribe), and this Entry should remain dirty. | ||
entry.setDirty(); | ||
return false; | ||
} | ||
// Returning true indicates either that there was no entry.subscribe | ||
// function or that it succeeded. | ||
return true; | ||
}; | ||
Entry.prototype.maybeUnsubscribe = function () { | ||
var unsubscribe = this.unsubscribe; | ||
if (typeof unsubscribe === "function") { | ||
this.unsubscribe = void 0; | ||
unsubscribe(); | ||
} | ||
}; | ||
Entry.count = 0; | ||
Entry.POOL_TARGET_SIZE = 100; | ||
return Entry; | ||
}()); | ||
} | ||
// Returning true indicates either that there was no entry.subscribe | ||
// function or that it succeeded. | ||
return true; | ||
} | ||
function maybeUnsubscribe(entry) { | ||
var unsubscribe = entry.unsubscribe; | ||
if (typeof unsubscribe === "function") { | ||
entry.unsubscribe = void 0; | ||
unsubscribe(); | ||
} | ||
} | ||
@@ -465,6 +459,2 @@ // A trie data structure that holds object keys weakly, yet can also hold | ||
var makeCacheKey = options.makeCacheKey || defaultMakeCacheKey; | ||
function reportOrphan(entry) { | ||
// Triggers the entry.dispose() call above. | ||
return disposable && cache["delete"](entry.key); | ||
} | ||
function optimistic() { | ||
@@ -492,7 +482,7 @@ var args = []; | ||
else { | ||
entry = new Entry(originalFunction, args, key); | ||
entry = new Entry(originalFunction, args); | ||
cache.set(key, entry); | ||
entry.subscribe = options.subscribe; | ||
if (disposable) { | ||
entry.reportOrphan = reportOrphan; | ||
entry.reportOrphan = function () { return cache["delete"](key); }; | ||
} | ||
@@ -499,0 +489,0 @@ } |
import { OptimisticWrapOptions } from "./index"; | ||
export declare type AnyEntry = Entry<any, any, any>; | ||
export declare class Entry<TArgs extends any[], TValue, TKey> { | ||
export declare type AnyEntry = Entry<any, any>; | ||
export declare class Entry<TArgs extends any[], TValue> { | ||
readonly fn: (...args: TArgs) => TValue; | ||
args: Readonly<TArgs>; | ||
readonly key: TKey; | ||
args: TArgs; | ||
static count: number; | ||
@@ -11,10 +10,10 @@ static POOL_TARGET_SIZE: number; | ||
unsubscribe?: () => any; | ||
reportOrphan?: (entry: Entry<TArgs, TValue, TKey>) => any; | ||
private parents; | ||
private childValues; | ||
private dirtyChildren; | ||
private dirty; | ||
private recomputing; | ||
private value; | ||
constructor(fn: (...args: TArgs) => TValue, args: Readonly<TArgs>, key: TKey); | ||
reportOrphan?: (this: Entry<TArgs, TValue>) => any; | ||
readonly parents: Set<Entry<any, any>>; | ||
readonly childValues: Map<Entry<any, any>, any>; | ||
dirtyChildren: Set<AnyEntry> | null; | ||
dirty: boolean; | ||
recomputing: boolean; | ||
value: TValue; | ||
constructor(fn: (...args: TArgs) => TValue, args: TArgs); | ||
recompute(): TValue; | ||
@@ -24,17 +23,2 @@ isOrphan(): boolean; | ||
dispose(): void; | ||
private rememberParent; | ||
private recomputeIfDirty; | ||
private reallyRecompute; | ||
private mightBeDirty; | ||
private setClean; | ||
private reportDirty; | ||
private reportClean; | ||
private reportDirtyChild; | ||
private reportCleanChild; | ||
private removeDirtyChild; | ||
private maybeReportOrphan; | ||
private forgetChildren; | ||
private forgetChild; | ||
private maybeSubscribe; | ||
private maybeUnsubscribe; | ||
} |
432
lib/entry.js
@@ -15,6 +15,5 @@ "use strict"; | ||
var Entry = /** @class */ (function () { | ||
function Entry(fn, args, key) { | ||
function Entry(fn, args) { | ||
this.fn = fn; | ||
this.args = args; | ||
this.key = key; | ||
this.parents = new Set(); | ||
@@ -32,3 +31,3 @@ this.childValues = new Map(); | ||
Entry.prototype.recompute = function () { | ||
if (!this.rememberParent() && this.maybeReportOrphan()) { | ||
if (!rememberParent(this) && maybeReportOrphan(this)) { | ||
// The recipient of the entry.reportOrphan callback decided to dispose | ||
@@ -39,3 +38,3 @@ // of this orphan entry by calling entry.dispose(), so we don't need to | ||
} | ||
return this.recomputeIfDirty(); | ||
return recomputeIfDirty(this); | ||
}; | ||
@@ -50,12 +49,12 @@ Entry.prototype.isOrphan = function () { | ||
this.value = UNKNOWN_VALUE; | ||
this.reportDirty(); | ||
reportDirty(this); | ||
// We can go ahead and unsubscribe here, since any further dirty | ||
// notifications we receive will be redundant, and unsubscribing may | ||
// free up some resources, e.g. file watchers. | ||
this.maybeUnsubscribe(); | ||
maybeUnsubscribe(this); | ||
}; | ||
Entry.prototype.dispose = function () { | ||
var _this = this; | ||
this.forgetChildren().forEach(function (child) { return child.maybeReportOrphan(); }); | ||
this.maybeUnsubscribe(); | ||
forgetChildren(this).forEach(maybeReportOrphan); | ||
maybeUnsubscribe(this); | ||
// Because this entry has been kicked out of the cache (in index.js), | ||
@@ -74,227 +73,222 @@ // we've lost the ability to find out if/when this entry becomes dirty, | ||
parent.setDirty(); | ||
parent.forgetChild(_this); | ||
forgetChild(parent, _this); | ||
}); | ||
}; | ||
Entry.prototype.rememberParent = function () { | ||
var local = local_1.get(); | ||
var parent = local.currentParentEntry; | ||
if (parent) { | ||
this.parents.add(parent); | ||
if (!parent.childValues.has(this)) { | ||
parent.childValues.set(this, UNKNOWN_VALUE); | ||
} | ||
if (this.mightBeDirty()) { | ||
parent.reportDirtyChild(this); | ||
} | ||
else { | ||
parent.reportCleanChild(this); | ||
} | ||
return parent; | ||
Entry.count = 0; | ||
Entry.POOL_TARGET_SIZE = 100; | ||
return Entry; | ||
}()); | ||
exports.Entry = Entry; | ||
function rememberParent(child) { | ||
var local = local_1.get(); | ||
var parent = local.currentParentEntry; | ||
if (parent) { | ||
child.parents.add(parent); | ||
if (!parent.childValues.has(child)) { | ||
parent.childValues.set(child, UNKNOWN_VALUE); | ||
} | ||
}; | ||
// This is the most important method of the Entry API, because it | ||
// determines whether the cached entry.value can be returned immediately, | ||
// or must be recomputed. The overall performance of the caching system | ||
// depends on the truth of the following observations: (1) this.dirty is | ||
// usually false, (2) this.dirtyChildren is usually null/empty, and thus | ||
// (3) this.value is usally returned very quickly, without recomputation. | ||
Entry.prototype.recomputeIfDirty = function () { | ||
var _this = this; | ||
if (this.dirty) { | ||
// If this Entry is explicitly dirty because someone called | ||
// entry.setDirty(), recompute. | ||
return this.reallyRecompute(); | ||
if (mightBeDirty(child)) { | ||
reportDirtyChild(parent, child); | ||
} | ||
if (this.mightBeDirty()) { | ||
// 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)); | ||
try { | ||
child.recomputeIfDirty(); | ||
} | ||
catch (e) { | ||
_this.setDirty(); | ||
} | ||
}); | ||
if (this.dirty) { | ||
// If this Entry has become explicitly dirty after comparing the fresh | ||
// values of its dirty children against this.childValues, recompute. | ||
return this.reallyRecompute(); | ||
} | ||
else { | ||
reportCleanChild(parent, child); | ||
} | ||
assert(this.value !== UNKNOWN_VALUE); | ||
return this.value; | ||
}; | ||
Entry.prototype.reallyRecompute = function () { | ||
assert(!this.recomputing, "already recomputing"); | ||
this.recomputing = true; | ||
// Since this recomputation is likely to re-remember some of this | ||
// entry's children, we forget our children here but do not call | ||
// maybeReportOrphan until after the recomputation finishes. | ||
var originalChildren = this.forgetChildren(); | ||
var local = local_1.get(); | ||
var parent = local.currentParentEntry; | ||
local.currentParentEntry = this; | ||
var threw = true; | ||
try { | ||
this.value = this.fn.apply(null, this.args); | ||
threw = false; | ||
} | ||
finally { | ||
this.recomputing = false; | ||
assert(local.currentParentEntry === this); | ||
local.currentParentEntry = parent; | ||
if (threw || !this.maybeSubscribe()) { | ||
// 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 entry.value. | ||
this.setDirty(); | ||
return parent; | ||
} | ||
} | ||
// This is the most important method of the Entry API, because it | ||
// determines whether the cached entry.value can be returned immediately, | ||
// or must be recomputed. The overall performance of the caching system | ||
// depends on the truth of the following observations: (1) this.dirty is | ||
// usually false, (2) this.dirtyChildren is usually null/empty, and thus | ||
// (3) this.value is usally returned very quickly, without recomputation. | ||
function recomputeIfDirty(entry) { | ||
if (entry.dirty) { | ||
// If this Entry is explicitly dirty because someone called | ||
// entry.setDirty(), recompute. | ||
return reallyRecompute(entry); | ||
} | ||
if (mightBeDirty(entry)) { | ||
// Get fresh values for any dirty children, and if those values | ||
// disagree with this.childValues, mark this Entry explicitly dirty. | ||
entry.dirtyChildren.forEach(function (child) { | ||
assert(entry.childValues.has(child)); | ||
try { | ||
recomputeIfDirty(child); | ||
} | ||
else { | ||
// If we successfully recomputed entry.value and did not fail to | ||
// (re)subscribe, then this Entry is no longer explicitly dirty. | ||
this.setClean(); | ||
catch (e) { | ||
entry.setDirty(); | ||
} | ||
}); | ||
if (entry.dirty) { | ||
// If this Entry has become explicitly dirty after comparing the fresh | ||
// values of its dirty children against this.childValues, recompute. | ||
return reallyRecompute(entry); | ||
} | ||
// Now that we've had a chance to re-remember any children that were | ||
// involved in the recomputation, we can safely report any orphan | ||
// children that remain. | ||
originalChildren.forEach(function (child) { return child.maybeReportOrphan(); }); | ||
return this.value; | ||
}; | ||
Entry.prototype.mightBeDirty = function () { | ||
return this.dirty || !!(this.dirtyChildren && this.dirtyChildren.size); | ||
}; | ||
Entry.prototype.setClean = function () { | ||
this.dirty = false; | ||
if (this.mightBeDirty()) { | ||
// This Entry may still have dirty children, in which case we can't | ||
// let our parents know we're clean just yet. | ||
return; | ||
} | ||
assert(entry.value !== UNKNOWN_VALUE); | ||
return entry.value; | ||
} | ||
function reallyRecompute(entry) { | ||
assert(!entry.recomputing, "already recomputing"); | ||
entry.recomputing = true; | ||
// Since this recomputation is likely to re-remember some of this | ||
// entry's children, we forget our children here but do not call | ||
// maybeReportOrphan until after the recomputation finishes. | ||
var originalChildren = forgetChildren(entry); | ||
var local = local_1.get(); | ||
var parent = local.currentParentEntry; | ||
local.currentParentEntry = entry; | ||
var threw = true; | ||
try { | ||
entry.value = entry.fn.apply(null, entry.args); | ||
threw = false; | ||
} | ||
finally { | ||
entry.recomputing = false; | ||
assert(local.currentParentEntry === entry); | ||
local.currentParentEntry = parent; | ||
if (threw || !maybeSubscribe(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 entry.value. | ||
entry.setDirty(); | ||
} | ||
this.reportClean(); | ||
}; | ||
Entry.prototype.reportDirty = function () { | ||
var _this = this; | ||
this.parents.forEach(function (parent) { return parent.reportDirtyChild(_this); }); | ||
}; | ||
Entry.prototype.reportClean = function () { | ||
var _this = this; | ||
this.parents.forEach(function (parent) { return parent.reportCleanChild(_this); }); | ||
}; | ||
// Let a parent Entry know that one of its children may be dirty. | ||
Entry.prototype.reportDirtyChild = function (child) { | ||
// Must have called rememberParent(child) before calling | ||
// reportDirtyChild(parent, child). | ||
assert(this.childValues.has(child)); | ||
assert(child.mightBeDirty()); | ||
if (!this.dirtyChildren) { | ||
this.dirtyChildren = emptySetPool.pop() || new Set; | ||
else { | ||
// If we successfully recomputed entry.value and did not fail to | ||
// (re)subscribe, then this Entry is no longer explicitly dirty. | ||
setClean(entry); | ||
} | ||
else if (this.dirtyChildren.has(child)) { | ||
// If we already know this child is dirty, then we must have already | ||
// informed our own parents that we are dirty, so we can terminate | ||
// the recursion early. | ||
return; | ||
} | ||
this.dirtyChildren.add(child); | ||
this.reportDirty(); | ||
}; | ||
// Let a parent Entry know that one of its children is no longer dirty. | ||
Entry.prototype.reportCleanChild = function (child) { | ||
// Must have called rememberChild(child) before calling | ||
// reportCleanChild(parent, child). | ||
assert(this.childValues.has(child)); | ||
assert(!child.mightBeDirty()); | ||
var childValue = this.childValues.get(child); | ||
if (childValue === UNKNOWN_VALUE) { | ||
this.childValues.set(child, child.value); | ||
} | ||
else if (childValue !== child.value) { | ||
this.setDirty(); | ||
} | ||
this.removeDirtyChild(child); | ||
if (this.mightBeDirty()) { | ||
return; | ||
} | ||
this.reportClean(); | ||
}; | ||
Entry.prototype.removeDirtyChild = function (child) { | ||
var dc = this.dirtyChildren; | ||
if (dc) { | ||
dc.delete(child); | ||
if (dc.size === 0) { | ||
if (emptySetPool.length < Entry.POOL_TARGET_SIZE) { | ||
emptySetPool.push(dc); | ||
} | ||
this.dirtyChildren = null; | ||
} | ||
// Now that we've had a chance to re-remember any children that were | ||
// involved in the recomputation, we can safely report any orphan | ||
// children that remain. | ||
originalChildren.forEach(maybeReportOrphan); | ||
return entry.value; | ||
} | ||
function mightBeDirty(entry) { | ||
return entry.dirty || !!(entry.dirtyChildren && entry.dirtyChildren.size); | ||
} | ||
function setClean(entry) { | ||
entry.dirty = false; | ||
if (mightBeDirty(entry)) { | ||
// This Entry may still have dirty children, in which case we can't | ||
// let our parents know we're clean just yet. | ||
return; | ||
} | ||
reportClean(entry); | ||
} | ||
function reportDirty(child) { | ||
child.parents.forEach(function (parent) { return reportDirtyChild(parent, child); }); | ||
} | ||
function reportClean(child) { | ||
child.parents.forEach(function (parent) { return reportCleanChild(parent, child); }); | ||
} | ||
// Let a parent Entry know that one of its children may be dirty. | ||
function reportDirtyChild(parent, child) { | ||
// Must have called rememberParent(child) before calling | ||
// reportDirtyChild(parent, child). | ||
assert(parent.childValues.has(child)); | ||
assert(mightBeDirty(child)); | ||
if (!parent.dirtyChildren) { | ||
parent.dirtyChildren = emptySetPool.pop() || new Set; | ||
} | ||
else if (parent.dirtyChildren.has(child)) { | ||
// If we already know this child is dirty, then we must have already | ||
// informed our own parents that we are dirty, so we can terminate | ||
// the recursion early. | ||
return; | ||
} | ||
parent.dirtyChildren.add(child); | ||
reportDirty(parent); | ||
} | ||
// Let a parent Entry know that one of its children is no longer dirty. | ||
function reportCleanChild(parent, child) { | ||
// Must have called rememberChild(child) before calling | ||
// reportCleanChild(parent, child). | ||
assert(parent.childValues.has(child)); | ||
assert(!mightBeDirty(child)); | ||
var childValue = parent.childValues.get(child); | ||
if (childValue === UNKNOWN_VALUE) { | ||
parent.childValues.set(child, child.value); | ||
} | ||
else if (childValue !== child.value) { | ||
parent.setDirty(); | ||
} | ||
removeDirtyChild(parent, child); | ||
if (mightBeDirty(parent)) { | ||
return; | ||
} | ||
reportClean(parent); | ||
} | ||
function removeDirtyChild(parent, child) { | ||
var dc = parent.dirtyChildren; | ||
if (dc) { | ||
dc.delete(child); | ||
if (dc.size === 0) { | ||
if (emptySetPool.length < Entry.POOL_TARGET_SIZE) { | ||
emptySetPool.push(dc); | ||
} | ||
parent.dirtyChildren = null; | ||
} | ||
}; | ||
// If the given entry has a reportOrphan method, and no remaining parents, | ||
// call entry.reportOrphan and return true iff it returns true. The | ||
// reportOrphan function should return true to indicate entry.dispose() | ||
// has been called, and the entry has been removed from any other caches | ||
// (see index.js for the only current example). | ||
Entry.prototype.maybeReportOrphan = function () { | ||
var report = this.reportOrphan; | ||
return typeof report === "function" && | ||
this.parents.size === 0 && | ||
report(this) === true; | ||
}; | ||
// Removes all children from this entry and returns an array of the | ||
// removed children. | ||
Entry.prototype.forgetChildren = function () { | ||
var _this = this; | ||
var children = reusableEmptyArray; | ||
if (this.childValues.size > 0) { | ||
children = []; | ||
this.childValues.forEach(function (value, child) { | ||
_this.forgetChild(child); | ||
children.push(child); | ||
}); | ||
} | ||
} | ||
// If the given entry has a reportOrphan method, and no remaining parents, | ||
// call entry.reportOrphan and return true iff it returns true. The | ||
// reportOrphan function should return true to indicate entry.dispose() | ||
// has been called, and the entry has been removed from any other caches | ||
// (see index.js for the only current example). | ||
function maybeReportOrphan(entry) { | ||
return entry.parents.size === 0 && | ||
typeof entry.reportOrphan === "function" && | ||
entry.reportOrphan() === true; | ||
} | ||
// Removes all children from this entry and returns an array of the | ||
// removed children. | ||
function forgetChildren(parent) { | ||
var children = reusableEmptyArray; | ||
if (parent.childValues.size > 0) { | ||
children = []; | ||
parent.childValues.forEach(function (value, child) { | ||
forgetChild(parent, child); | ||
children.push(child); | ||
}); | ||
} | ||
// After we forget all our children, this.dirtyChildren must be empty | ||
// and therefore must have been reset to null. | ||
assert(parent.dirtyChildren === null); | ||
return children; | ||
} | ||
function forgetChild(parent, child) { | ||
child.parents.delete(parent); | ||
parent.childValues.delete(child); | ||
removeDirtyChild(parent, child); | ||
} | ||
function maybeSubscribe(entry) { | ||
if (typeof entry.subscribe === "function") { | ||
try { | ||
maybeUnsubscribe(entry); // Prevent double subscriptions. | ||
entry.unsubscribe = entry.subscribe.apply(null, entry.args); | ||
} | ||
// After we forget all our children, this.dirtyChildren must be empty | ||
// and therefore must have been reset to null. | ||
assert(this.dirtyChildren === null); | ||
return children; | ||
}; | ||
Entry.prototype.forgetChild = function (child) { | ||
child.parents.delete(this); | ||
this.childValues.delete(child); | ||
this.removeDirtyChild(child); | ||
}; | ||
Entry.prototype.maybeSubscribe = function () { | ||
if (typeof this.subscribe === "function") { | ||
try { | ||
this.maybeUnsubscribe(); // Prevent double subscriptions. | ||
this.unsubscribe = this.subscribe.apply(null, this.args); | ||
} | ||
catch (e) { | ||
// If this Entry has a subscribe function and it threw an exception | ||
// (or an unsubscribe function it previously returned now throws), | ||
// return false to indicate that we were not able to subscribe (or | ||
// unsubscribe), and this Entry should remain dirty. | ||
this.setDirty(); | ||
return false; | ||
} | ||
catch (e) { | ||
// If this Entry has a subscribe function and it threw an exception | ||
// (or an unsubscribe function it previously returned now throws), | ||
// return false to indicate that we were not able to subscribe (or | ||
// unsubscribe), and this Entry should remain dirty. | ||
entry.setDirty(); | ||
return false; | ||
} | ||
// Returning true indicates either that there was no entry.subscribe | ||
// function or that it succeeded. | ||
return true; | ||
}; | ||
Entry.prototype.maybeUnsubscribe = function () { | ||
var unsubscribe = this.unsubscribe; | ||
if (typeof unsubscribe === "function") { | ||
this.unsubscribe = void 0; | ||
unsubscribe(); | ||
} | ||
}; | ||
Entry.count = 0; | ||
Entry.POOL_TARGET_SIZE = 100; | ||
return Entry; | ||
}()); | ||
exports.Entry = Entry; | ||
} | ||
// Returning true indicates either that there was no entry.subscribe | ||
// function or that it succeeded. | ||
return true; | ||
} | ||
function maybeUnsubscribe(entry) { | ||
var unsubscribe = entry.unsubscribe; | ||
if (typeof unsubscribe === "function") { | ||
entry.unsubscribe = void 0; | ||
unsubscribe(); | ||
} | ||
} | ||
//# sourceMappingURL=entry.js.map |
@@ -28,6 +28,2 @@ "use strict"; | ||
var makeCacheKey = options.makeCacheKey || defaultMakeCacheKey; | ||
function reportOrphan(entry) { | ||
// Triggers the entry.dispose() call above. | ||
return disposable && cache.delete(entry.key); | ||
} | ||
function optimistic() { | ||
@@ -55,7 +51,7 @@ var args = []; | ||
else { | ||
entry = new entry_1.Entry(originalFunction, args, key); | ||
entry = new entry_1.Entry(originalFunction, args); | ||
cache.set(key, entry); | ||
entry.subscribe = options.subscribe; | ||
if (disposable) { | ||
entry.reportOrphan = reportOrphan; | ||
entry.reportOrphan = function () { return cache.delete(key); }; | ||
} | ||
@@ -62,0 +58,0 @@ } |
export declare function get(): { | ||
currentParentEntry: import("./entry").Entry<any, any, any>; | ||
currentParentEntry: import("./entry").Entry<any, any>; | ||
}; |
{ | ||
"name": "optimism", | ||
"version": "0.8.0", | ||
"version": "0.8.1", | ||
"author": "Ben Newman <ben@benjamn.com>", | ||
@@ -5,0 +5,0 @@ "description": "Composable reactive caching with efficient invalidation.", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
90280
1093