UNPKG

@tko/computed

Version:

TKO Computed Observables

426 lines (425 loc) 15.2 kB
// @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); } }