UNPKG

10.7 kBPlain TextView Raw
1import {
2 Lambda,
3 ComputedValue,
4 IDependencyTree,
5 IDerivation,
6 IDerivationState_,
7 TraceMode,
8 getDependencyTree,
9 globalState,
10 runReactions,
11 checkIfStateReadsAreAllowed
12} from "../internal"
13
14export interface IDepTreeNode {
15 name_: string
16 observing_?: IObservable[]
17}
18
19export interface IObservable extends IDepTreeNode {
20 diffValue_: number
21 /**
22 * Id of the derivation *run* that last accessed this observable.
23 * If this id equals the *run* id of the current derivation,
24 * the dependency is already established
25 */
26 lastAccessedBy_: number
27 isBeingObserved_: boolean
28
29 lowestObserverState_: IDerivationState_ // Used to avoid redundant propagations
30 isPendingUnobservation_: boolean // Used to push itself to global.pendingUnobservations at most once per batch.
31
32 observers_: Set<IDerivation>
33
34 onBUO(): void
35 onBO(): void
36
37 onBUOL: Set<Lambda> | undefined
38 onBOL: Set<Lambda> | undefined
39}
40
41export function hasObservers(observable: IObservable): boolean {
42 return observable.observers_ && observable.observers_.size > 0
43}
44
45export function getObservers(observable: IObservable): Set<IDerivation> {
46 return observable.observers_
47}
48
49// function invariantObservers(observable: IObservable) {
50// const list = observable.observers
51// const map = observable.observersIndexes
52// const l = list.length
53// for (let i = 0; i < l; i++) {
54// const id = list[i].__mapid
55// if (i) {
56// invariant(map[id] === i, "INTERNAL ERROR maps derivation.__mapid to index in list") // for performance
57// } else {
58// invariant(!(id in map), "INTERNAL ERROR observer on index 0 shouldn't be held in map.") // for performance
59// }
60// }
61// invariant(
62// list.length === 0 || Object.keys(map).length === list.length - 1,
63// "INTERNAL ERROR there is no junk in map"
64// )
65// }
66export function addObserver(observable: IObservable, node: IDerivation) {
67 // invariant(node.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1");
68 // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR add already added node");
69 // invariantObservers(observable);
70
71 observable.observers_.add(node)
72 if (observable.lowestObserverState_ > node.dependenciesState_)
73 observable.lowestObserverState_ = node.dependenciesState_
74
75 // invariantObservers(observable);
76 // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR didn't add node");
77}
78
79export function removeObserver(observable: IObservable, node: IDerivation) {
80 // invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
81 // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR remove already removed node");
82 // invariantObservers(observable);
83 observable.observers_.delete(node)
84 if (observable.observers_.size === 0) {
85 // deleting last observer
86 queueForUnobservation(observable)
87 }
88 // invariantObservers(observable);
89 // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR remove already removed node2");
90}
91
92export function queueForUnobservation(observable: IObservable) {
93 if (observable.isPendingUnobservation_ === false) {
94 // invariant(observable._observers.length === 0, "INTERNAL ERROR, should only queue for unobservation unobserved observables");
95 observable.isPendingUnobservation_ = true
96 globalState.pendingUnobservations.push(observable)
97 }
98}
99
100/**
101 * Batch starts a transaction, at least for purposes of memoizing ComputedValues when nothing else does.
102 * During a batch `onBecomeUnobserved` will be called at most once per observable.
103 * Avoids unnecessary recalculations.
104 */
105export function startBatch() {
106 globalState.inBatch++
107}
108
109export function endBatch() {
110 if (--globalState.inBatch === 0) {
111 runReactions()
112 // the batch is actually about to finish, all unobserving should happen here.
113 const list = globalState.pendingUnobservations
114 for (let i = 0; i < list.length; i++) {
115 const observable = list[i]
116 observable.isPendingUnobservation_ = false
117 if (observable.observers_.size === 0) {
118 if (observable.isBeingObserved_) {
119 // if this observable had reactive observers, trigger the hooks
120 observable.isBeingObserved_ = false
121 observable.onBUO()
122 }
123 if (observable instanceof ComputedValue) {
124 // computed values are automatically teared down when the last observer leaves
125 // this process happens recursively, this computed might be the last observabe of another, etc..
126 observable.suspend_()
127 }
128 }
129 }
130 globalState.pendingUnobservations = []
131 }
132}
133
134export function reportObserved(observable: IObservable): boolean {
135 checkIfStateReadsAreAllowed(observable)
136
137 const derivation = globalState.trackingDerivation
138 if (derivation !== null) {
139 /**
140 * Simple optimization, give each derivation run an unique id (runId)
141 * Check if last time this observable was accessed the same runId is used
142 * if this is the case, the relation is already known
143 */
144 if (derivation.runId_ !== observable.lastAccessedBy_) {
145 observable.lastAccessedBy_ = derivation.runId_
146 // Tried storing newObserving, or observing, or both as Set, but performance didn't come close...
147 derivation.newObserving_![derivation.unboundDepsCount_++] = observable
148 if (!observable.isBeingObserved_ && globalState.trackingContext) {
149 observable.isBeingObserved_ = true
150 observable.onBO()
151 }
152 }
153 return true
154 } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
155 queueForUnobservation(observable)
156 }
157
158 return false
159}
160
161// function invariantLOS(observable: IObservable, msg: string) {
162// // it's expensive so better not run it in produciton. but temporarily helpful for testing
163// const min = getObservers(observable).reduce((a, b) => Math.min(a, b.dependenciesState), 2)
164// if (min >= observable.lowestObserverState) return // <- the only assumption about `lowestObserverState`
165// throw new Error(
166// "lowestObserverState is wrong for " +
167// msg +
168// " because " +
169// min +
170// " < " +
171// observable.lowestObserverState
172// )
173// }
174
175/**
176 * NOTE: current propagation mechanism will in case of self reruning autoruns behave unexpectedly
177 * It will propagate changes to observers from previous run
178 * It's hard or maybe impossible (with reasonable perf) to get it right with current approach
179 * Hopefully self reruning autoruns aren't a feature people should depend on
180 * Also most basic use cases should be ok
181 */
182
183// Called by Atom when its value changes
184export function propagateChanged(observable: IObservable) {
185 // invariantLOS(observable, "changed start");
186 if (observable.lowestObserverState_ === IDerivationState_.STALE_) return
187 observable.lowestObserverState_ = IDerivationState_.STALE_
188
189 // Ideally we use for..of here, but the downcompiled version is really slow...
190 observable.observers_.forEach(d => {
191 if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
192 if (__DEV__ && d.isTracing_ !== TraceMode.NONE) {
193 logTraceInfo(d, observable)
194 }
195 d.onBecomeStale_()
196 }
197 d.dependenciesState_ = IDerivationState_.STALE_
198 })
199 // invariantLOS(observable, "changed end");
200}
201
202// Called by ComputedValue when it recalculate and its value changed
203export function propagateChangeConfirmed(observable: IObservable) {
204 // invariantLOS(observable, "confirmed start");
205 if (observable.lowestObserverState_ === IDerivationState_.STALE_) return
206 observable.lowestObserverState_ = IDerivationState_.STALE_
207
208 observable.observers_.forEach(d => {
209 if (d.dependenciesState_ === IDerivationState_.POSSIBLY_STALE_) {
210 d.dependenciesState_ = IDerivationState_.STALE_
211 if (__DEV__ && d.isTracing_ !== TraceMode.NONE) {
212 logTraceInfo(d, observable)
213 }
214 } else if (
215 d.dependenciesState_ === IDerivationState_.UP_TO_DATE_ // this happens during computing of `d`, just keep lowestObserverState up to date.
216 ) {
217 observable.lowestObserverState_ = IDerivationState_.UP_TO_DATE_
218 }
219 })
220 // invariantLOS(observable, "confirmed end");
221}
222
223// Used by computed when its dependency changed, but we don't wan't to immediately recompute.
224export function propagateMaybeChanged(observable: IObservable) {
225 // invariantLOS(observable, "maybe start");
226 if (observable.lowestObserverState_ !== IDerivationState_.UP_TO_DATE_) return
227 observable.lowestObserverState_ = IDerivationState_.POSSIBLY_STALE_
228
229 observable.observers_.forEach(d => {
230 if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
231 d.dependenciesState_ = IDerivationState_.POSSIBLY_STALE_
232 d.onBecomeStale_()
233 }
234 })
235 // invariantLOS(observable, "maybe end");
236}
237
238function logTraceInfo(derivation: IDerivation, observable: IObservable) {
239 console.log(
240 `[mobx.trace] '${derivation.name_}' is invalidated due to a change in: '${observable.name_}'`
241 )
242 if (derivation.isTracing_ === TraceMode.BREAK) {
243 const lines = []
244 printDepTree(getDependencyTree(derivation), lines, 1)
245
246 // prettier-ignore
247 new Function(
248`debugger;
249/*
250Tracing '${derivation.name_}'
251
252You are entering this break point because derivation '${derivation.name_}' is being traced and '${observable.name_}' is now forcing it to update.
253Just follow the stacktrace you should now see in the devtools to see precisely what piece of your code is causing this update
254The stackframe you are looking for is at least ~6-8 stack-frames up.
255
256${derivation instanceof ComputedValue ? derivation.derivation.toString().replace(/[*]\//g, "/") : ""}
257
258The dependencies for this derivation are:
259
260${lines.join("\n")}
261*/
262 `)()
263 }
264}
265
266function printDepTree(tree: IDependencyTree, lines: string[], depth: number) {
267 if (lines.length >= 1000) {
268 lines.push("(and many more)")
269 return
270 }
271 lines.push(`${"\t".repeat(depth - 1)}${tree.name}`)
272 if (tree.dependencies) tree.dependencies.forEach(child => printDepTree(child, lines, depth + 1))
273}