mobx
Version:
Simple, scalable state management.
275 lines (247 loc) • 11.2 kB
text/typescript
import {
Lambda,
ComputedValue,
IDerivation,
IDerivationState_,
globalState,
runReactions,
checkIfStateReadsAreAllowed
} from "../internal"
export interface IDepTreeNode {
name_: string
observing_?: IObservable[]
}
export interface IObservable extends IDepTreeNode {
diffValue: number
/**
* Id of the derivation *run* that last accessed this observable.
* If this id equals the *run* id of the current derivation,
* the dependency is already established
*/
lastAccessedBy_: number
isBeingObserved: boolean
lowestObserverState_: IDerivationState_ // Used to avoid redundant propagations
isPendingUnobservation: boolean // Used to push itself to global.pendingUnobservations at most once per batch.
observers_: Set<IDerivation> | null
onBUO(): void
onBO(): void
onBUOL: Set<Lambda> | undefined
onBOL: Set<Lambda> | undefined
}
export function hasObservers(observable: IObservable): boolean {
return !!observable.observers_ && observable.observers_.size > 0
}
export function getObservers(observable: IObservable): Set<IDerivation> {
return observable.observers_ ?? new Set()
}
// function invariantObservers(observable: IObservable) {
// const list = observable.observers
// const map = observable.observersIndexes
// const l = list.length
// for (let i = 0; i < l; i++) {
// const id = list[i].__mapid
// if (i) {
// invariant(map[id] === i, "INTERNAL ERROR maps derivation.__mapid to index in list") // for performance
// } else {
// invariant(!(id in map), "INTERNAL ERROR observer on index 0 shouldn't be held in map.") // for performance
// }
// }
// invariant(
// list.length === 0 || Object.keys(map).length === list.length - 1,
// "INTERNAL ERROR there is no junk in map"
// )
// }
export function addObserver(observable: IObservable, node: IDerivation) {
// invariant(node.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1");
// invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR add already added node");
// invariantObservers(observable);
;(observable.observers_ ??= new Set()).add(node)
if (observable.lowestObserverState_ > node.dependenciesState_) {
observable.lowestObserverState_ = node.dependenciesState_
}
// invariantObservers(observable);
// invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR didn't add node");
}
export function removeObserver(observable: IObservable, node: IDerivation) {
// invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
// invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR remove already removed node");
// invariantObservers(observable);
const observers = observable.observers_
if (!observers) {
return
}
observers.delete(node)
if (observers.size === 0) {
// deleting last observer
queueForUnobservation(observable)
}
// invariantObservers(observable);
// invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR remove already removed node2");
}
export function queueForUnobservation(observable: IObservable) {
if (observable.isPendingUnobservation === false) {
// invariant(observable._observers.length === 0, "INTERNAL ERROR, should only queue for unobservation unobserved observables");
observable.isPendingUnobservation = true
globalState.pendingUnobservations.push(observable)
}
}
/**
* Batch starts a transaction, at least for purposes of memoizing ComputedValues when nothing else does.
* During a batch `onBecomeUnobserved` will be called at most once per observable.
* Avoids unnecessary recalculations.
*/
export function startBatch() {
globalState.inBatch++
}
export function endBatch() {
if (--globalState.inBatch === 0) {
runReactions()
// the batch is actually about to finish, all unobserving should happen here.
// Guard against re-entering this loop: an onBUO handler can dispose a Reaction,
// which calls startBatch/endBatch again while we're still iterating. Bail out of
// the nested call instead of recursing; the outer loop re-reads list.length on
// every iteration, so it picks up anything the nested dispose() pushes onto the
// same pendingUnobservations array.
if (!globalState.isRunningUnobservations) {
globalState.isRunningUnobservations = true
try {
const list = globalState.pendingUnobservations
for (let i = 0; i < list.length; i++) {
const observable = list[i]
observable.isPendingUnobservation = false
if (!observable.observers_ || observable.observers_.size === 0) {
if (observable.isBeingObserved) {
// if this observable had reactive observers, trigger the hooks
observable.isBeingObserved = false
observable.onBUO()
}
if (observable instanceof ComputedValue) {
// computed values are automatically teared down when the last observer leaves
// this process happens recursively, this computed might be the last observabe of another, etc..
observable.suspend_()
}
}
}
globalState.pendingUnobservations = []
} finally {
// Always release the guard, even if an onBUO handler (user code) threw,
// otherwise every future endBatch() would see isRunningUnobservations
// stuck true and silently stop draining pendingUnobservations forever.
globalState.isRunningUnobservations = false
}
}
}
}
/**
* Marks an observable as observed, cascading into the dependencies of a ComputedValue.
* Unobservation already cascades (`suspend_` -> `clearObserving`), observation normally
* only does so by accident: a newly observed computed usually recomputes and re-reports
* its dependencies. When it serves a cached value instead nothing re-reports them, so the
* transition has to be propagated by hand. See #4547.
*/
export function markObserved(observable: IObservable) {
if (observable.isBeingObserved) {
return
}
observable.isBeingObserved = true
observable.onBO()
// No queueForUnobservation here: the observer links already exist, so the regular
// suspend_ -> clearObserving -> removeObserver teardown still delivers the onBUO.
observable.observing_?.forEach(markObserved)
}
export function reportObserved(observable: IObservable): boolean {
checkIfStateReadsAreAllowed(observable)
const derivation = globalState.trackingDerivation
if (derivation !== null) {
/**
* Simple optimization, give each derivation run an unique id (runId)
* Check if last time this observable was accessed the same runId is used
* if this is the case, the relation is already known
*/
if (derivation.runId_ !== observable.lastAccessedBy_) {
observable.lastAccessedBy_ = derivation.runId_
// Tried storing newObserving, or observing, or both as Set, but performance didn't come close...
derivation.newObserving_![derivation.unboundDepsCount_++] = observable
if (!observable.isBeingObserved && globalState.trackingContext) {
observable.isBeingObserved = true
observable.onBO()
}
}
return observable.isBeingObserved
} else if (
(!observable.observers_ || observable.observers_.size === 0) &&
globalState.inBatch > 0
) {
queueForUnobservation(observable)
}
return false
}
// function invariantLOS(observable: IObservable, msg: string) {
// // it's expensive so better not run it in produciton. but temporarily helpful for testing
// const min = getObservers(observable).reduce((a, b) => Math.min(a, b.dependenciesState), 2)
// if (min >= observable.lowestObserverState) return // <- the only assumption about `lowestObserverState`
// throw new Error(
// "lowestObserverState is wrong for " +
// msg +
// " because " +
// min +
// " < " +
// observable.lowestObserverState
// )
// }
/**
* NOTE: current propagation mechanism will in case of self reruning autoruns behave unexpectedly
* It will propagate changes to observers from previous run
* It's hard or maybe impossible (with reasonable perf) to get it right with current approach
* Hopefully self reruning autoruns aren't a feature people should depend on
* Also most basic use cases should be ok
*/
// Called by Atom when its value changes
export function propagateChanged(observable: IObservable) {
// invariantLOS(observable, "changed start");
if (observable.lowestObserverState_ === IDerivationState_.STALE_) {
return
}
observable.lowestObserverState_ = IDerivationState_.STALE_
// Ideally we use for..of here, but the downcompiled version is really slow...
observable.observers_?.forEach(d => {
if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
d.onBecomeStale_()
}
d.dependenciesState_ = IDerivationState_.STALE_
})
// invariantLOS(observable, "changed end");
}
// Called by ComputedValue when it recalculate and its value changed
export function propagateChangeConfirmed(observable: IObservable) {
// invariantLOS(observable, "confirmed start");
if (observable.lowestObserverState_ === IDerivationState_.STALE_) {
return
}
observable.lowestObserverState_ = IDerivationState_.STALE_
observable.observers_?.forEach(d => {
if (d.dependenciesState_ === IDerivationState_.POSSIBLY_STALE_) {
d.dependenciesState_ = IDerivationState_.STALE_
} else if (
d.dependenciesState_ === IDerivationState_.UP_TO_DATE_ // this happens during computing of `d`, just keep lowestObserverState up to date.
) {
observable.lowestObserverState_ = IDerivationState_.UP_TO_DATE_
}
})
// invariantLOS(observable, "confirmed end");
}
// Used by computed when its dependency changed, but we don't wan't to immediately recompute.
export function propagateMaybeChanged(observable: IObservable) {
// invariantLOS(observable, "maybe start");
if (observable.lowestObserverState_ !== IDerivationState_.UP_TO_DATE_) {
return
}
observable.lowestObserverState_ = IDerivationState_.POSSIBLY_STALE_
observable.observers_?.forEach(d => {
if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
d.dependenciesState_ = IDerivationState_.POSSIBLY_STALE_
d.onBecomeStale_()
}
})
// invariantLOS(observable, "maybe end");
}