mobx 7.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # mobx
2
2
 
3
+ ## 7.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`9444c624b957b489875e1c6b45deb290034ce4e9`](https://github.com/mobxjs/mobx/commit/9444c624b957b489875e1c6b45deb290034ce4e9) [#4694](https://github.com/mobxjs/mobx/pull/4694) Thanks [@mrpmohiburrahman](https://github.com/mrpmohiburrahman)! - fix: `onBecomeObserved` is now called for the dependencies of a computed that becomes observed while serving a cached value. Previously, observation only cascaded when the newly observed computed also happened to recompute, so an observable with a live observer chain up to a running reaction could still report itself as unobserved and never fire its hook.
8
+
9
+ - [`53bb83fdf455a4e606bb721ea10040ded7c57796`](https://github.com/mobxjs/mobx/commit/53bb83fdf455a4e606bb721ea10040ded7c57796) [#4683](https://github.com/mobxjs/mobx/pull/4683) Thanks [@gesposito](https://github.com/gesposito)! - perf: fast-path primitives in `deepEnhancer`. Writing a primitive into a deep observable no longer runs the observable/array/plain-object/Map/Set/function type checks; primitives can never be made observable, so they are returned immediately. Creating an observable array of primitives is ~4x faster, and observable Set/Map writes are ~20-25% faster in the perf suite.
10
+
11
+ - [`030498d6b2bfe4cc27340fed8c706177cb3b28e1`](https://github.com/mobxjs/mobx/commit/030498d6b2bfe4cc27340fed8c706177cb3b28e1) [#4684](https://github.com/mobxjs/mobx/pull/4684) Thanks [@a-y-ibrahim](https://github.com/a-y-ibrahim)! - Fix a stack overflow ("Maximum call stack size exceeded") that could occur when an `onBecomeUnobserved` handler disposes a `Reaction`. Disposing a `Reaction` re-enters `endBatch()`, which used to recurse into the same `pendingUnobservations` drain loop instead of letting the already-running outer loop pick up the newly queued items, causing unbounded stack depth for long enough chains.
12
+
13
+ - [`a9086076b9ead1a9c933215bffaa9b57d44f6829`](https://github.com/mobxjs/mobx/commit/a9086076b9ead1a9c933215bffaa9b57d44f6829) [#4681](https://github.com/mobxjs/mobx/pull/4681) Thanks [@spokodev](https://github.com/spokodev)! - Fix ObservableSet union, intersection and symmetricDifference to return results in receiver order, matching native Set, when the argument is a plain Set.
14
+
15
+ - [`c65a4e14cf48b42cb792dc4c64edbcf56234b32d`](https://github.com/mobxjs/mobx/commit/c65a4e14cf48b42cb792dc4c64edbcf56234b32d) [#4682](https://github.com/mobxjs/mobx/pull/4682) Thanks [@gesposito](https://github.com/gesposito)! - perf: lazily allocate the internal `observers_` Set. Atoms and computed values no longer allocate an empty `Set` upfront; it is created on first observer instead. Most atoms in large stores are never observed, so this saves roughly 160 bytes per unobserved atom (e.g. ~35% lower heap usage when hydrating 50k instances with 10 observable fields each).
16
+
3
17
  ## 7.0.0
4
18
 
5
19
  ### Major Changes
@@ -7,7 +7,7 @@ export interface IAtom extends IObservable {
7
7
  export declare class Atom implements IAtom {
8
8
  name_: string;
9
9
  private flags_;
10
- observers_: Set<IDerivation>;
10
+ observers_: Set<IDerivation> | null;
11
11
  lastAccessedBy_: number;
12
12
  lowestObserverState_: IDerivationState_;
13
13
  /**
@@ -24,7 +24,7 @@ export declare class ComputedValue<T> implements IObservable, IComputedValue<T>,
24
24
  dependenciesState_: IDerivationState_;
25
25
  observing_: IObservable[];
26
26
  newObserving_: null;
27
- observers_: Set<IDerivation>;
27
+ observers_: Set<IDerivation> | null;
28
28
  runId_: number;
29
29
  lastAccessedBy_: number;
30
30
  lowestObserverState_: IDerivationState_;
@@ -52,6 +52,13 @@ export declare class MobXGlobals {
52
52
  * Are we currently processing reactions?
53
53
  */
54
54
  isRunningReactions: boolean;
55
+ /**
56
+ * Are we currently draining pendingUnobservations in endBatch?
57
+ * An onBecomeUnobserved handler can dispose a Reaction, which calls
58
+ * startBatch/endBatch again; this guards against re-entering the same
59
+ * drain loop recursively (see endBatch in observable.ts).
60
+ */
61
+ isRunningUnobservations: boolean;
55
62
  /**
56
63
  * Is it allowed to change observables at this point?
57
64
  * In general, MobX doesn't allow that when running computations and React.render.
@@ -14,7 +14,7 @@ export interface IObservable extends IDepTreeNode {
14
14
  isBeingObserved: boolean;
15
15
  lowestObserverState_: IDerivationState_;
16
16
  isPendingUnobservation: boolean;
17
- observers_: Set<IDerivation>;
17
+ observers_: Set<IDerivation> | null;
18
18
  onBUO(): void;
19
19
  onBO(): void;
20
20
  onBUOL: Set<Lambda> | undefined;
@@ -32,6 +32,14 @@ export declare function queueForUnobservation(observable: IObservable): void;
32
32
  */
33
33
  export declare function startBatch(): void;
34
34
  export declare function endBatch(): void;
35
+ /**
36
+ * Marks an observable as observed, cascading into the dependencies of a ComputedValue.
37
+ * Unobservation already cascades (`suspend_` -> `clearObserving`), observation normally
38
+ * only does so by accident: a newly observed computed usually recomputes and re-reports
39
+ * its dependencies. When it serves a cached value instead nothing re-reports them, so the
40
+ * transition has to be propagated by hand. See #4547.
41
+ */
42
+ export declare function markObserved(observable: IObservable): void;
35
43
  export declare function reportObserved(observable: IObservable): boolean;
36
44
  /**
37
45
  * NOTE: current propagation mechanism will in case of self reruning autoruns behave unexpectedly
@@ -263,7 +263,8 @@ class Atom {
263
263
  constructor(name_ = "Atom@" + getNextId() ) {
264
264
  this.name_ = void 0;
265
265
  this.flags_ = 0b000;
266
- this.observers_ = new Set();
266
+ // Allocated lazily on first observer to save memory.
267
+ this.observers_ = null;
267
268
  this.lastAccessedBy_ = 0;
268
269
  this.lowestObserverState_ = -1 /* IDerivationState_.NOT_TRACKING_ */;
269
270
  // onBecomeObservedListeners
@@ -345,6 +346,10 @@ function compareShallow(a, b) {
345
346
  const compareDefault = Object.is;
346
347
 
347
348
  function deepEnhancer(v, _, name) {
349
+ // primitives can never be made observable; skip the type checks below
350
+ if (v === null || typeof v !== "object" && typeof v !== "function") {
351
+ return v;
352
+ }
348
353
  // it is an observable already, done
349
354
  if (isObservable(v)) {
350
355
  return v;
@@ -1285,7 +1290,8 @@ class ComputedValue {
1285
1290
  // nodes we are looking at. Our value depends on these nodes
1286
1291
  this.newObserving_ = null;
1287
1292
  // during tracking it's an array with new observed observers
1288
- this.observers_ = new Set();
1293
+ // Lazily allocated on first observer - see Atom.observers_.
1294
+ this.observers_ = null;
1289
1295
  this.runId_ = 0;
1290
1296
  this.lastAccessedBy_ = 0;
1291
1297
  this.lowestObserverState_ = 0 /* IDerivationState_.UP_TO_DATE_ */;
@@ -1368,9 +1374,9 @@ class ComputedValue {
1368
1374
  if (this.isComputing) {
1369
1375
  die(32, this.name_, this.derivation);
1370
1376
  }
1371
- if (globalState.inBatch === 0 &&
1377
+ if (globalState.inBatch === 0 && (
1372
1378
  // !globalState.trackingDerivatpion &&
1373
- this.observers_.size === 0 && !this.keepAlive_) {
1379
+ !this.observers_ || this.observers_.size === 0) && !this.keepAlive_) {
1374
1380
  if (shouldCompute(this)) {
1375
1381
  this.warnAboutUntrackedRead_();
1376
1382
  startBatch(); // See perf test 'computed memoization'
@@ -1378,6 +1384,7 @@ class ComputedValue {
1378
1384
  endBatch();
1379
1385
  }
1380
1386
  } else {
1387
+ const wasBeingObserved = this.isBeingObserved;
1381
1388
  reportObserved(this);
1382
1389
  if (shouldCompute(this)) {
1383
1390
  let prevTrackingContext = globalState.trackingContext;
@@ -1388,6 +1395,10 @@ class ComputedValue {
1388
1395
  propagateChangeConfirmed(this);
1389
1396
  }
1390
1397
  globalState.trackingContext = prevTrackingContext;
1398
+ } else if (!wasBeingObserved && this.isBeingObserved) {
1399
+ // We just became observed while serving a cached value, so the getter
1400
+ // won't run and won't re-report our dependencies. Cascade to them. #4547
1401
+ this.observing_.forEach(markObserved);
1391
1402
  }
1392
1403
  }
1393
1404
  const result = this.value_;
@@ -1556,7 +1567,7 @@ function isComputingDerivation() {
1556
1567
  return globalState.trackingDerivation !== null; // filter out actions inside computations
1557
1568
  }
1558
1569
  function checkIfStateModificationsAreAllowed(atom) {
1559
- const hasObservers = atom.observers_.size > 0;
1570
+ const hasObservers = !!atom.observers_ && atom.observers_.size > 0;
1560
1571
  // Should not be possible to change observed state outside strict mode, except during initialization, see #563
1561
1572
  if (!globalState.allowStateChanges && (hasObservers || globalState.enforceActions === "always")) {
1562
1573
  console.warn("[MobX] " + (globalState.enforceActions ? "Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: " : "Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, a computed value or the render function of a React component? You can wrap side effects in 'runInAction' (or decorate functions with 'action') if needed. Tried to modify: ") + atom.name_);
@@ -1779,6 +1790,13 @@ class MobXGlobals {
1779
1790
  * Are we currently processing reactions?
1780
1791
  */
1781
1792
  this.isRunningReactions = false;
1793
+ /**
1794
+ * Are we currently draining pendingUnobservations in endBatch?
1795
+ * An onBecomeUnobserved handler can dispose a Reaction, which calls
1796
+ * startBatch/endBatch again; this guards against re-entering the same
1797
+ * drain loop recursively (see endBatch in observable.ts).
1798
+ */
1799
+ this.isRunningUnobservations = false;
1782
1800
  /**
1783
1801
  * Is it allowed to change observables at this point?
1784
1802
  * In general, MobX doesn't allow that when running computations and React.render.
@@ -1895,10 +1913,11 @@ function resetGlobalState() {
1895
1913
  }
1896
1914
 
1897
1915
  function hasObservers(observable) {
1898
- return observable.observers_ && observable.observers_.size > 0;
1916
+ return !!observable.observers_ && observable.observers_.size > 0;
1899
1917
  }
1900
1918
  function getObservers(observable) {
1901
- return observable.observers_;
1919
+ var _observable$observers;
1920
+ return (_observable$observers = observable.observers_) != null ? _observable$observers : new Set();
1902
1921
  }
1903
1922
  // function invariantObservers(observable: IObservable) {
1904
1923
  // const list = observable.observers
@@ -1918,10 +1937,8 @@ function getObservers(observable) {
1918
1937
  // )
1919
1938
  // }
1920
1939
  function addObserver(observable, node) {
1921
- // invariant(node.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1");
1922
- // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR add already added node");
1923
- // invariantObservers(observable);
1924
- observable.observers_.add(node);
1940
+ var _observable$observers2;
1941
+ ((_observable$observers2 = observable.observers_) != null ? _observable$observers2 : observable.observers_ = new Set()).add(node);
1925
1942
  if (observable.lowestObserverState_ > node.dependenciesState_) {
1926
1943
  observable.lowestObserverState_ = node.dependenciesState_;
1927
1944
  }
@@ -1932,8 +1949,12 @@ function removeObserver(observable, node) {
1932
1949
  // invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
1933
1950
  // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR remove already removed node");
1934
1951
  // invariantObservers(observable);
1935
- observable.observers_.delete(node);
1936
- if (observable.observers_.size === 0) {
1952
+ const observers = observable.observers_;
1953
+ if (!observers) {
1954
+ return;
1955
+ }
1956
+ observers.delete(node);
1957
+ if (observers.size === 0) {
1937
1958
  // deleting last observer
1938
1959
  queueForUnobservation(observable);
1939
1960
  }
@@ -1959,26 +1980,59 @@ function endBatch() {
1959
1980
  if (--globalState.inBatch === 0) {
1960
1981
  runReactions();
1961
1982
  // the batch is actually about to finish, all unobserving should happen here.
1962
- const list = globalState.pendingUnobservations;
1963
- for (let i = 0; i < list.length; i++) {
1964
- const observable = list[i];
1965
- observable.isPendingUnobservation = false;
1966
- if (observable.observers_.size === 0) {
1967
- if (observable.isBeingObserved) {
1968
- // if this observable had reactive observers, trigger the hooks
1969
- observable.isBeingObserved = false;
1970
- observable.onBUO();
1971
- }
1972
- if (observable instanceof ComputedValue) {
1973
- // computed values are automatically teared down when the last observer leaves
1974
- // this process happens recursively, this computed might be the last observabe of another, etc..
1975
- observable.suspend_();
1983
+ // Guard against re-entering this loop: an onBUO handler can dispose a Reaction,
1984
+ // which calls startBatch/endBatch again while we're still iterating. Bail out of
1985
+ // the nested call instead of recursing; the outer loop re-reads list.length on
1986
+ // every iteration, so it picks up anything the nested dispose() pushes onto the
1987
+ // same pendingUnobservations array.
1988
+ if (!globalState.isRunningUnobservations) {
1989
+ globalState.isRunningUnobservations = true;
1990
+ try {
1991
+ const list = globalState.pendingUnobservations;
1992
+ for (let i = 0; i < list.length; i++) {
1993
+ const observable = list[i];
1994
+ observable.isPendingUnobservation = false;
1995
+ if (!observable.observers_ || observable.observers_.size === 0) {
1996
+ if (observable.isBeingObserved) {
1997
+ // if this observable had reactive observers, trigger the hooks
1998
+ observable.isBeingObserved = false;
1999
+ observable.onBUO();
2000
+ }
2001
+ if (observable instanceof ComputedValue) {
2002
+ // computed values are automatically teared down when the last observer leaves
2003
+ // this process happens recursively, this computed might be the last observabe of another, etc..
2004
+ observable.suspend_();
2005
+ }
2006
+ }
1976
2007
  }
2008
+ globalState.pendingUnobservations = [];
2009
+ } finally {
2010
+ // Always release the guard, even if an onBUO handler (user code) threw,
2011
+ // otherwise every future endBatch() would see isRunningUnobservations
2012
+ // stuck true and silently stop draining pendingUnobservations forever.
2013
+ globalState.isRunningUnobservations = false;
1977
2014
  }
1978
2015
  }
1979
- globalState.pendingUnobservations = [];
1980
2016
  }
1981
2017
  }
2018
+ /**
2019
+ * Marks an observable as observed, cascading into the dependencies of a ComputedValue.
2020
+ * Unobservation already cascades (`suspend_` -> `clearObserving`), observation normally
2021
+ * only does so by accident: a newly observed computed usually recomputes and re-reports
2022
+ * its dependencies. When it serves a cached value instead nothing re-reports them, so the
2023
+ * transition has to be propagated by hand. See #4547.
2024
+ */
2025
+ function markObserved(observable) {
2026
+ var _observable$observing;
2027
+ if (observable.isBeingObserved) {
2028
+ return;
2029
+ }
2030
+ observable.isBeingObserved = true;
2031
+ observable.onBO();
2032
+ // No queueForUnobservation here: the observer links already exist, so the regular
2033
+ // suspend_ -> clearObserving -> removeObserver teardown still delivers the onBUO.
2034
+ (_observable$observing = observable.observing_) == null || _observable$observing.forEach(markObserved);
2035
+ }
1982
2036
  function reportObserved(observable) {
1983
2037
  checkIfStateReadsAreAllowed(observable);
1984
2038
  const derivation = globalState.trackingDerivation;
@@ -1998,7 +2052,7 @@ function reportObserved(observable) {
1998
2052
  }
1999
2053
  }
2000
2054
  return observable.isBeingObserved;
2001
- } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
2055
+ } else if ((!observable.observers_ || observable.observers_.size === 0) && globalState.inBatch > 0) {
2002
2056
  queueForUnobservation(observable);
2003
2057
  }
2004
2058
  return false;
@@ -2025,13 +2079,14 @@ function reportObserved(observable) {
2025
2079
  */
2026
2080
  // Called by Atom when its value changes
2027
2081
  function propagateChanged(observable) {
2082
+ var _observable$observers3;
2028
2083
  // invariantLOS(observable, "changed start");
2029
2084
  if (observable.lowestObserverState_ === 2 /* IDerivationState_.STALE_ */) {
2030
2085
  return;
2031
2086
  }
2032
2087
  observable.lowestObserverState_ = 2 /* IDerivationState_.STALE_ */;
2033
2088
  // Ideally we use for..of here, but the downcompiled version is really slow...
2034
- observable.observers_.forEach(d => {
2089
+ (_observable$observers3 = observable.observers_) == null || _observable$observers3.forEach(d => {
2035
2090
  if (d.dependenciesState_ === 0 /* IDerivationState_.UP_TO_DATE_ */) {
2036
2091
  d.onBecomeStale_();
2037
2092
  }
@@ -2041,12 +2096,13 @@ function propagateChanged(observable) {
2041
2096
  }
2042
2097
  // Called by ComputedValue when it recalculate and its value changed
2043
2098
  function propagateChangeConfirmed(observable) {
2099
+ var _observable$observers4;
2044
2100
  // invariantLOS(observable, "confirmed start");
2045
2101
  if (observable.lowestObserverState_ === 2 /* IDerivationState_.STALE_ */) {
2046
2102
  return;
2047
2103
  }
2048
2104
  observable.lowestObserverState_ = 2 /* IDerivationState_.STALE_ */;
2049
- observable.observers_.forEach(d => {
2105
+ (_observable$observers4 = observable.observers_) == null || _observable$observers4.forEach(d => {
2050
2106
  if (d.dependenciesState_ === 1 /* IDerivationState_.POSSIBLY_STALE_ */) {
2051
2107
  d.dependenciesState_ = 2 /* IDerivationState_.STALE_ */;
2052
2108
  } else if (d.dependenciesState_ === 0 /* IDerivationState_.UP_TO_DATE_ */ // this happens during computing of `d`, just keep lowestObserverState up to date.
@@ -2058,12 +2114,13 @@ function propagateChangeConfirmed(observable) {
2058
2114
  }
2059
2115
  // Used by computed when its dependency changed, but we don't wan't to immediately recompute.
2060
2116
  function propagateMaybeChanged(observable) {
2117
+ var _observable$observers5;
2061
2118
  // invariantLOS(observable, "maybe start");
2062
2119
  if (observable.lowestObserverState_ !== 0 /* IDerivationState_.UP_TO_DATE_ */) {
2063
2120
  return;
2064
2121
  }
2065
2122
  observable.lowestObserverState_ = 1 /* IDerivationState_.POSSIBLY_STALE_ */;
2066
- observable.observers_.forEach(d => {
2123
+ (_observable$observers5 = observable.observers_) == null || _observable$observers5.forEach(d => {
2067
2124
  if (d.dependenciesState_ === 0 /* IDerivationState_.UP_TO_DATE_ */) {
2068
2125
  d.dependenciesState_ = 1 /* IDerivationState_.POSSIBLY_STALE_ */;
2069
2126
  d.onBecomeStale_();
@@ -4318,31 +4375,16 @@ class ObservableSet {
4318
4375
  });
4319
4376
  }
4320
4377
  intersection(otherSet) {
4321
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4322
- return otherSet.intersection(this);
4323
- } else {
4324
- const dehancedSet = new Set(this);
4325
- return dehancedSet.intersection(otherSet);
4326
- }
4378
+ return new Set(this).intersection(otherSet);
4327
4379
  }
4328
4380
  union(otherSet) {
4329
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4330
- return otherSet.union(this);
4331
- } else {
4332
- const dehancedSet = new Set(this);
4333
- return dehancedSet.union(otherSet);
4334
- }
4381
+ return new Set(this).union(otherSet);
4335
4382
  }
4336
4383
  difference(otherSet) {
4337
4384
  return new Set(this).difference(otherSet);
4338
4385
  }
4339
4386
  symmetricDifference(otherSet) {
4340
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4341
- return otherSet.symmetricDifference(this);
4342
- } else {
4343
- const dehancedSet = new Set(this);
4344
- return dehancedSet.symmetricDifference(otherSet);
4345
- }
4387
+ return new Set(this).symmetricDifference(otherSet);
4346
4388
  }
4347
4389
  isSubsetOf(otherSet) {
4348
4390
  return new Set(this).isSubsetOf(otherSet);
@@ -4351,12 +4393,7 @@ class ObservableSet {
4351
4393
  return new Set(this).isSupersetOf(otherSet);
4352
4394
  }
4353
4395
  isDisjointFrom(otherSet) {
4354
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4355
- return otherSet.isDisjointFrom(this);
4356
- } else {
4357
- const dehancedSet = new Set(this);
4358
- return dehancedSet.isDisjointFrom(otherSet);
4359
- }
4396
+ return new Set(this).isDisjointFrom(otherSet);
4360
4397
  }
4361
4398
  replace(other) {
4362
4399
  if (isObservableSet(other)) {