You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
11 KiB
301 lines
11 KiB
import { parentEntrySlot } from "./context.js"; |
|
import { maybeUnsubscribe, arrayFromSet } from "./helpers.js"; |
|
const emptySetPool = []; |
|
const POOL_TARGET_SIZE = 100; |
|
// 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"); |
|
} |
|
} |
|
function valueIs(a, b) { |
|
const len = a.length; |
|
return ( |
|
// Unknown values are not equal to each other. |
|
len > 0 && |
|
// Both values must be ordinary (or both exceptional) to be equal. |
|
len === b.length && |
|
// The underlying value or exception must be the same. |
|
a[len - 1] === b[len - 1]); |
|
} |
|
function valueGet(value) { |
|
switch (value.length) { |
|
case 0: throw new Error("unknown value"); |
|
case 1: return value[0]; |
|
case 2: throw value[1]; |
|
} |
|
} |
|
function valueCopy(value) { |
|
return value.slice(0); |
|
} |
|
export class Entry { |
|
constructor(fn) { |
|
this.fn = fn; |
|
this.parents = new Set(); |
|
this.childValues = new Map(); |
|
// When this Entry has children that are dirty, this property becomes |
|
// a Set containing other Entry objects, borrowed from emptySetPool. |
|
// When the set becomes empty, it gets recycled back to emptySetPool. |
|
this.dirtyChildren = null; |
|
this.dirty = true; |
|
this.recomputing = false; |
|
this.value = []; |
|
this.deps = null; |
|
++Entry.count; |
|
} |
|
peek() { |
|
if (this.value.length === 1 && !mightBeDirty(this)) { |
|
rememberParent(this); |
|
return this.value[0]; |
|
} |
|
} |
|
// This is the most important method of the Entry API, because it |
|
// determines whether the cached this.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) valueGet(this.value) is usually returned without recomputation. |
|
recompute(args) { |
|
assert(!this.recomputing, "already recomputing"); |
|
rememberParent(this); |
|
return mightBeDirty(this) |
|
? reallyRecompute(this, args) |
|
: valueGet(this.value); |
|
} |
|
setDirty() { |
|
if (this.dirty) |
|
return; |
|
this.dirty = true; |
|
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. |
|
maybeUnsubscribe(this); |
|
} |
|
dispose() { |
|
this.setDirty(); |
|
// Sever any dependency relationships with our own children, so those |
|
// children don't retain this parent Entry in their child.parents sets, |
|
// thereby preventing it from being fully garbage collected. |
|
forgetChildren(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. |
|
eachParent(this, (parent, child) => { |
|
parent.setDirty(); |
|
forgetChild(parent, this); |
|
}); |
|
} |
|
forget() { |
|
// The code that creates Entry objects in index.ts will replace this method |
|
// with one that actually removes the Entry from the cache, which will also |
|
// trigger the entry.dispose method. |
|
this.dispose(); |
|
} |
|
dependOn(dep) { |
|
dep.add(this); |
|
if (!this.deps) { |
|
this.deps = emptySetPool.pop() || new Set(); |
|
} |
|
this.deps.add(dep); |
|
} |
|
forgetDeps() { |
|
if (this.deps) { |
|
arrayFromSet(this.deps).forEach(dep => dep.delete(this)); |
|
this.deps.clear(); |
|
emptySetPool.push(this.deps); |
|
this.deps = null; |
|
} |
|
} |
|
} |
|
Entry.count = 0; |
|
function rememberParent(child) { |
|
const parent = parentEntrySlot.getValue(); |
|
if (parent) { |
|
child.parents.add(parent); |
|
if (!parent.childValues.has(child)) { |
|
parent.childValues.set(child, []); |
|
} |
|
if (mightBeDirty(child)) { |
|
reportDirtyChild(parent, child); |
|
} |
|
else { |
|
reportCleanChild(parent, child); |
|
} |
|
return parent; |
|
} |
|
} |
|
function reallyRecompute(entry, args) { |
|
forgetChildren(entry); |
|
// Set entry as the parent entry while calling recomputeNewValue(entry). |
|
parentEntrySlot.withValue(entry, recomputeNewValue, [entry, args]); |
|
if (maybeSubscribe(entry, args)) { |
|
// If we successfully recomputed entry.value and did not fail to |
|
// (re)subscribe, then this Entry is no longer explicitly dirty. |
|
setClean(entry); |
|
} |
|
return valueGet(entry.value); |
|
} |
|
function recomputeNewValue(entry, args) { |
|
entry.recomputing = true; |
|
const { normalizeResult } = entry; |
|
let oldValueCopy; |
|
if (normalizeResult && entry.value.length === 1) { |
|
oldValueCopy = valueCopy(entry.value); |
|
} |
|
// Make entry.value an empty array, representing an unknown value. |
|
entry.value.length = 0; |
|
try { |
|
// If entry.fn succeeds, entry.value will become a normal Value. |
|
entry.value[0] = entry.fn.apply(null, args); |
|
// If we have a viable oldValueCopy to compare with the (successfully |
|
// recomputed) new entry.value, and they are not already === identical, give |
|
// normalizeResult a chance to pick/choose/reuse parts of oldValueCopy[0] |
|
// and/or entry.value[0] to determine the final cached entry.value. |
|
if (normalizeResult && oldValueCopy && !valueIs(oldValueCopy, entry.value)) { |
|
try { |
|
entry.value[0] = normalizeResult(entry.value[0], oldValueCopy[0]); |
|
} |
|
catch (_a) { |
|
// If normalizeResult throws, just use the newer value, rather than |
|
// saving the exception as entry.value[1]. |
|
} |
|
} |
|
} |
|
catch (e) { |
|
// If entry.fn throws, entry.value will hold that exception. |
|
entry.value[1] = e; |
|
} |
|
// Either way, this line is always reached. |
|
entry.recomputing = false; |
|
} |
|
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) { |
|
eachParent(child, reportDirtyChild); |
|
} |
|
function reportClean(child) { |
|
eachParent(child, reportCleanChild); |
|
} |
|
function eachParent(child, callback) { |
|
const parentCount = child.parents.size; |
|
if (parentCount) { |
|
const parents = arrayFromSet(child.parents); |
|
for (let i = 0; i < parentCount; ++i) { |
|
callback(parents[i], 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)); |
|
const parentWasClean = !mightBeDirty(parent); |
|
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); |
|
// If parent was clean before, it just became (possibly) dirty (according to |
|
// mightBeDirty), since we just added child to parent.dirtyChildren. |
|
if (parentWasClean) { |
|
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)); |
|
const childValue = parent.childValues.get(child); |
|
if (childValue.length === 0) { |
|
parent.childValues.set(child, valueCopy(child.value)); |
|
} |
|
else if (!valueIs(childValue, child.value)) { |
|
parent.setDirty(); |
|
} |
|
removeDirtyChild(parent, child); |
|
if (mightBeDirty(parent)) { |
|
return; |
|
} |
|
reportClean(parent); |
|
} |
|
function removeDirtyChild(parent, child) { |
|
const dc = parent.dirtyChildren; |
|
if (dc) { |
|
dc.delete(child); |
|
if (dc.size === 0) { |
|
if (emptySetPool.length < POOL_TARGET_SIZE) { |
|
emptySetPool.push(dc); |
|
} |
|
parent.dirtyChildren = null; |
|
} |
|
} |
|
} |
|
// Removes all children from this entry and returns an array of the |
|
// removed children. |
|
function forgetChildren(parent) { |
|
if (parent.childValues.size > 0) { |
|
parent.childValues.forEach((_value, child) => { |
|
forgetChild(parent, child); |
|
}); |
|
} |
|
// Remove this parent Entry from any sets to which it was added by the |
|
// addToSet method. |
|
parent.forgetDeps(); |
|
// After we forget all our children, this.dirtyChildren must be empty |
|
// and therefore must have been reset to null. |
|
assert(parent.dirtyChildren === null); |
|
} |
|
function forgetChild(parent, child) { |
|
child.parents.delete(parent); |
|
parent.childValues.delete(child); |
|
removeDirtyChild(parent, child); |
|
} |
|
function maybeSubscribe(entry, args) { |
|
if (typeof entry.subscribe === "function") { |
|
try { |
|
maybeUnsubscribe(entry); // Prevent double subscriptions. |
|
entry.unsubscribe = entry.subscribe.apply(null, 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. |
|
entry.setDirty(); |
|
return false; |
|
} |
|
} |
|
// Returning true indicates either that there was no entry.subscribe |
|
// function or that it succeeded. |
|
return true; |
|
} |
|
//# sourceMappingURL=entry.js.map
|