@tko/computed
Version:
TKO Computed Observables
426 lines (425 loc) • 15.2 kB
JavaScript
// @tko/computed 🥊 4.0.0-beta1.3 ESM
import {
addDisposeCallback,
arrayForEach,
createSymbolOrString,
domNodeIsAttachedToDocument,
extend,
options,
hasOwnProperty,
objectForEach,
options as koOptions,
removeDisposeCallback,
safeSetTimeout
} from "@tko/utils";
import {
dependencyDetection,
extenders,
valuesArePrimitiveAndEqual,
observable,
subscribable,
LATEST_VALUE
} from "@tko/observable";
const computedState = createSymbolOrString("_state");
const DISPOSED_STATE = {
dependencyTracking: null,
dependenciesCount: 0,
isDisposed: true,
isStale: false,
isDirty: false,
isSleeping: false,
disposeWhenNodeIsRemoved: null,
readFunction: null,
_options: null
};
export function computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget, options2) {
if (typeof evaluatorFunctionOrOptions === "object") {
options2 = evaluatorFunctionOrOptions;
} else {
options2 = options2 || {};
if (evaluatorFunctionOrOptions) {
options2.read = evaluatorFunctionOrOptions;
}
}
if (typeof options2.read !== "function") {
throw Error("Pass a function that returns the value of the computed");
}
var writeFunction = options2.write;
var state = {
latestValue: void 0,
isStale: true,
isDirty: true,
isBeingEvaluated: false,
suppressDisposalUntilDisposeWhenReturnsFalse: false,
isDisposed: false,
pure: false,
isSleeping: false,
readFunction: options2.read,
evaluatorFunctionTarget: evaluatorFunctionTarget || options2.owner,
disposeWhenNodeIsRemoved: options2.disposeWhenNodeIsRemoved || options2.disposeWhenNodeIsRemoved || null,
disposeWhen: options2.disposeWhen || options2.disposeWhen,
domNodeDisposalCallback: null,
dependencyTracking: {},
dependenciesCount: 0,
evaluationTimeoutInstance: null
};
function computedObservable() {
if (arguments.length > 0) {
if (typeof writeFunction === "function") {
writeFunction.apply(state.evaluatorFunctionTarget, arguments);
} else {
throw new Error("Cannot write a value to a computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
}
return this;
} else {
if (!state.isDisposed) {
dependencyDetection.registerDependency(computedObservable);
}
if (state.isDirty || state.isSleeping && computedObservable.haveDependenciesChanged()) {
computedObservable.evaluateImmediate();
}
return state.latestValue;
}
}
computedObservable[computedState] = state;
computedObservable.isWriteable = typeof writeFunction === "function";
subscribable.fn.init(computedObservable);
Object.setPrototypeOf(computedObservable, computed.fn);
if (options2.pure) {
state.pure = true;
state.isSleeping = true;
extend(computedObservable, pureComputedOverrides);
} else if (options2.deferEvaluation) {
extend(computedObservable, deferEvaluationOverrides);
}
if (koOptions.deferUpdates) {
extenders.deferred(computedObservable, true);
}
if (koOptions.debug) {
computedObservable._options = options2;
}
if (state.disposeWhenNodeIsRemoved) {
state.suppressDisposalUntilDisposeWhenReturnsFalse = true;
if (!state.disposeWhenNodeIsRemoved.nodeType) {
state.disposeWhenNodeIsRemoved = null;
}
}
if (!state.isSleeping && !options2.deferEvaluation) {
computedObservable.evaluateImmediate();
}
if (state.disposeWhenNodeIsRemoved && computedObservable.isActive()) {
addDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback = function() {
computedObservable.dispose();
});
}
return computedObservable;
}
function computedDisposeDependencyCallback(id, entryToDispose) {
if (entryToDispose !== null && entryToDispose.dispose) {
entryToDispose.dispose();
}
}
function computedBeginDependencyDetectionCallback(subscribable2, id) {
var computedObservable = this.computedObservable, state = computedObservable[computedState];
if (!state.isDisposed) {
if (this.disposalCount && this.disposalCandidates[id]) {
computedObservable.addDependencyTracking(id, subscribable2, this.disposalCandidates[id]);
this.disposalCandidates[id] = null;
--this.disposalCount;
} else if (!state.dependencyTracking[id]) {
computedObservable.addDependencyTracking(id, subscribable2, state.isSleeping ? { _target: subscribable2 } : computedObservable.subscribeToDependency(subscribable2));
}
if (subscribable2._notificationIsPending) {
subscribable2._notifyNextChangeIfValueIsDifferent();
}
}
}
computed.fn = {
equalityComparer: valuesArePrimitiveAndEqual,
getDependenciesCount() {
return this[computedState].dependenciesCount;
},
getDependencies() {
const dependencyTracking = this[computedState].dependencyTracking;
const dependentObservables = [];
objectForEach(dependencyTracking, function(id, dependency) {
dependentObservables[dependency._order] = dependency._target;
});
return dependentObservables;
},
addDependencyTracking(id, target, trackingObj) {
if (this[computedState].pure && target === this) {
throw Error("A 'pure' computed must not be called recursively");
}
this[computedState].dependencyTracking[id] = trackingObj;
trackingObj._order = this[computedState].dependenciesCount++;
trackingObj._version = target.getVersion();
},
haveDependenciesChanged() {
var id, dependency, dependencyTracking = this[computedState].dependencyTracking;
for (id in dependencyTracking) {
if (hasOwnProperty(dependencyTracking, id)) {
dependency = dependencyTracking[id];
if (this._evalDelayed && dependency._target._notificationIsPending || dependency._target.hasChanged(dependency._version)) {
return true;
}
}
}
},
markDirty() {
if (this._evalDelayed && !this[computedState].isBeingEvaluated) {
this._evalDelayed(false);
}
},
isActive() {
const state = this[computedState];
return state.isDirty || state.dependenciesCount > 0;
},
respondToChange() {
if (!this._notificationIsPending) {
this.evaluatePossiblyAsync();
} else if (this[computedState].isDirty) {
this[computedState].isStale = true;
}
},
subscribeToDependency(target) {
if (target._deferUpdates) {
var dirtySub = target.subscribe(this.markDirty, this, "dirty"), changeSub = target.subscribe(this.respondToChange, this);
return {
_target: target,
dispose() {
dirtySub.dispose();
changeSub.dispose();
}
};
} else {
return target.subscribe(this.evaluatePossiblyAsync, this);
}
},
evaluatePossiblyAsync() {
var computedObservable = this, throttleEvaluationTimeout = computedObservable.throttleEvaluation;
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
clearTimeout(this[computedState].evaluationTimeoutInstance);
this[computedState].evaluationTimeoutInstance = safeSetTimeout(function() {
computedObservable.evaluateImmediate(true);
}, throttleEvaluationTimeout);
} else if (computedObservable._evalDelayed) {
computedObservable._evalDelayed(true);
} else {
computedObservable.evaluateImmediate(true);
}
},
evaluateImmediate(notifyChange) {
var computedObservable = this, state = computedObservable[computedState], disposeWhen = state.disposeWhen, changed = false;
if (state.isBeingEvaluated) {
return;
}
if (state.isDisposed) {
return;
}
if (state.disposeWhenNodeIsRemoved && !domNodeIsAttachedToDocument(state.disposeWhenNodeIsRemoved) || disposeWhen && disposeWhen()) {
if (!state.suppressDisposalUntilDisposeWhenReturnsFalse) {
computedObservable.dispose();
return;
}
} else {
state.suppressDisposalUntilDisposeWhenReturnsFalse = false;
}
state.isBeingEvaluated = true;
try {
changed = this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange);
} finally {
state.isBeingEvaluated = false;
}
return changed;
},
evaluateImmediate_CallReadWithDependencyDetection(notifyChange) {
var computedObservable = this, state = computedObservable[computedState], changed = false;
var isInitial = state.pure ? void 0 : !state.dependenciesCount, dependencyDetectionContext = {
computedObservable,
disposalCandidates: state.dependencyTracking,
disposalCount: state.dependenciesCount
};
dependencyDetection.begin({
callbackTarget: dependencyDetectionContext,
callback: computedBeginDependencyDetectionCallback,
computed: computedObservable,
isInitial
});
state.dependencyTracking = {};
state.dependenciesCount = 0;
var newValue = this.evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext);
if (!state.dependenciesCount) {
computedObservable.dispose();
changed = true;
} else {
changed = computedObservable.isDifferent(state.latestValue, newValue);
}
if (changed) {
if (!state.isSleeping) {
computedObservable.notifySubscribers(state.latestValue, "beforeChange");
} else {
computedObservable.updateVersion();
}
state.latestValue = newValue;
if (options.debug) {
computedObservable._latestValue = newValue;
}
computedObservable.notifySubscribers(state.latestValue, "spectate");
if (!state.isSleeping && notifyChange) {
computedObservable.notifySubscribers(state.latestValue);
}
if (computedObservable._recordUpdate) {
computedObservable._recordUpdate();
}
}
if (isInitial) {
computedObservable.notifySubscribers(state.latestValue, "awake");
}
return changed;
},
evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext) {
try {
var readFunction = state.readFunction;
return state.evaluatorFunctionTarget ? readFunction.call(state.evaluatorFunctionTarget) : readFunction();
} finally {
dependencyDetection.end();
if (dependencyDetectionContext.disposalCount && !state.isSleeping) {
objectForEach(dependencyDetectionContext.disposalCandidates, computedDisposeDependencyCallback);
}
state.isStale = state.isDirty = false;
}
},
peek(forceEvaluate) {
const state = this[computedState];
if (state.isDirty && (forceEvaluate || !state.dependenciesCount) || state.isSleeping && this.haveDependenciesChanged()) {
this.evaluateImmediate();
}
return state.latestValue;
},
get [LATEST_VALUE]() {
return this.peek();
},
limit(limitFunction) {
const state = this[computedState];
subscribable.fn.limit.call(this, limitFunction);
Object.assign(this, {
_evalIfChanged() {
if (!this[computedState].isSleeping) {
if (this[computedState].isStale) {
this.evaluateImmediate();
} else {
this[computedState].isDirty = false;
}
}
return state.latestValue;
},
_evalDelayed(isChange) {
this._limitBeforeChange(state.latestValue);
state.isDirty = true;
if (isChange) {
state.isStale = true;
}
this._limitChange(this, !isChange);
}
});
},
dispose() {
var state = this[computedState];
if (!state.isSleeping && state.dependencyTracking) {
objectForEach(state.dependencyTracking, function(id, dependency) {
if (dependency.dispose) {
dependency.dispose();
}
});
}
if (state.disposeWhenNodeIsRemoved && state.domNodeDisposalCallback) {
removeDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback);
}
Object.assign(state, DISPOSED_STATE);
}
};
var pureComputedOverrides = {
beforeSubscriptionAdd(event) {
var computedObservable = this, state = computedObservable[computedState];
if (!state.isDisposed && state.isSleeping && event === "change") {
state.isSleeping = false;
if (state.isStale || computedObservable.haveDependenciesChanged()) {
state.dependencyTracking = null;
state.dependenciesCount = 0;
if (computedObservable.evaluateImmediate()) {
computedObservable.updateVersion();
}
} else {
var dependenciesOrder = [];
objectForEach(state.dependencyTracking, function(id, dependency) {
dependenciesOrder[dependency._order] = id;
});
arrayForEach(dependenciesOrder, function(id, order) {
var dependency = state.dependencyTracking[id], subscription = computedObservable.subscribeToDependency(dependency._target);
subscription._order = order;
subscription._version = dependency._version;
state.dependencyTracking[id] = subscription;
});
if (computedObservable.haveDependenciesChanged()) {
if (computedObservable.evaluateImmediate()) {
computedObservable.updateVersion();
}
}
}
if (!state.isDisposed) {
computedObservable.notifySubscribers(state.latestValue, "awake");
}
}
},
afterSubscriptionRemove(event) {
var state = this[computedState];
if (!state.isDisposed && event === "change" && !this.hasSubscriptionsForEvent("change")) {
objectForEach(state.dependencyTracking, function(id, dependency) {
if (dependency.dispose) {
state.dependencyTracking[id] = {
_target: dependency._target,
_order: dependency._order,
_version: dependency._version
};
dependency.dispose();
}
});
state.isSleeping = true;
this.notifySubscribers(void 0, "asleep");
}
},
getVersion() {
var state = this[computedState];
if (state.isSleeping && (state.isStale || this.haveDependenciesChanged())) {
this.evaluateImmediate();
}
return subscribable.fn.getVersion.call(this);
}
};
var deferEvaluationOverrides = {
beforeSubscriptionAdd(event) {
if (event === "change" || event === "beforeChange") {
this.peek();
}
}
};
Object.setPrototypeOf(computed.fn, subscribable.fn);
var protoProp = observable.protoProperty;
computed.fn[protoProp] = computed;
observable.observablePrototypes.add(computed);
export function isComputed(instance) {
return typeof instance === "function" && instance[protoProp] === computed;
}
export function isPureComputed(instance) {
return isComputed(instance) && instance[computedState] && instance[computedState].pure;
}
export function pureComputed(evaluatorFunctionOrOptions, evaluatorFunctionTarget) {
if (typeof evaluatorFunctionOrOptions === "function") {
return computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget, { "pure": true });
} else {
evaluatorFunctionOrOptions = extend({}, evaluatorFunctionOrOptions);
evaluatorFunctionOrOptions.pure = true;
return computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget);
}
}