@jbrowse/mobx-state-tree 5.13.0 → 6.1.0

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.
@@ -1,4 +1,4 @@
1
- import { isComputedProp, isObservableProp, createAtom, action, computed, _allowStateChangesInsideComputed, reaction, runInAction, observable, values, entries, isObservableArray, _getGlobalState, isObservableObject, defineProperty as defineProperty$1, _interceptReads, intercept, observe, ObservableMap, _getAdministration, set, makeObservable, getAtom, when } from 'mobx';
1
+ import { isComputedProp, isObservableProp, createAtom, action, computed, _allowStateChangesInsideComputed, reaction, runInAction, observable, values, entries, isObservableArray, isObservableObject, defineProperty as defineProperty$1, _interceptReads, intercept, observe, ObservableMap, _getAdministration, set, makeObservable, getAtom, when } from 'mobx';
2
2
 
3
3
  let livelinessChecking = "warn";
4
4
  /**
@@ -944,7 +944,6 @@ class BaseNode {
944
944
  constructor(type, parent, subpath, environment) {
945
945
  this.type = type;
946
946
  this.environment = environment;
947
- this.environment = environment;
948
947
  this.baseSetParent(parent, subpath);
949
948
  }
950
949
  getReconciliationType() {
@@ -1412,10 +1411,7 @@ class ObjectNode extends BaseNode {
1412
1411
  }
1413
1412
  _getCachedInitialSnapshot() {
1414
1413
  if (!this._cachedInitialSnapshotCreated) {
1415
- const type = this.type;
1416
- const childNodes = this._childNodes;
1417
- const snapshot = this._initialSnapshot;
1418
- this._cachedInitialSnapshot = type.processInitialSnapshot(childNodes, snapshot);
1414
+ this._cachedInitialSnapshot = this.type.processInitialSnapshot(this._childNodes);
1419
1415
  this._cachedInitialSnapshotCreated = true;
1420
1416
  }
1421
1417
  return this._cachedInitialSnapshot;
@@ -1808,9 +1804,6 @@ BaseType.prototype.create = action(BaseType.prototype.create);
1808
1804
  */
1809
1805
  class ComplexType extends BaseType {
1810
1806
  identifierAttribute;
1811
- constructor(name) {
1812
- super(name);
1813
- }
1814
1807
  create(snapshot = this.getDefaultSnapshot(), environment) {
1815
1808
  return super.create(snapshot, environment);
1816
1809
  }
@@ -2712,13 +2705,6 @@ function typeCheckFailure(context, value, message) {
2712
2705
  // Clone context since it may be mutated after this error is created
2713
2706
  return [{ context: context.slice(), value, message }];
2714
2707
  }
2715
- /**
2716
- * @internal
2717
- * @hidden
2718
- */
2719
- function flattenTypeErrors(errors) {
2720
- return errors.flat();
2721
- }
2722
2708
  // TODO; doublecheck: typecheck should only needed to be invoked from: type.create and array / map / value.property will change
2723
2709
  /**
2724
2710
  * @internal
@@ -2780,7 +2766,6 @@ class IdentifierCache {
2780
2766
  // last time the cache (array) for a given time changed
2781
2767
  // n.b. it is not really the time, but just an integer that gets increased after each modification to the array
2782
2768
  lastCacheModificationPerId = observable.map();
2783
- constructor() { }
2784
2769
  updateLastCacheModificationPerId(identifier) {
2785
2770
  const lcm = this.lastCacheModificationPerId.get(identifier);
2786
2771
  // we start at 1 since 0 means no update since cache creation
@@ -2821,7 +2806,7 @@ class IdentifierCache {
2821
2806
  if (!set.length) {
2822
2807
  this.cache.delete(id);
2823
2808
  }
2824
- this.updateLastCacheModificationPerId(node.identifier);
2809
+ this.updateLastCacheModificationPerId(id);
2825
2810
  }
2826
2811
  }
2827
2812
  }
@@ -3053,15 +3038,8 @@ function convertChildNodesToArray(childNodes) {
3053
3038
  if (!childNodes) {
3054
3039
  return EMPTY_ARRAY;
3055
3040
  }
3056
- const keys = Object.keys(childNodes);
3057
- if (!keys.length) {
3058
- return EMPTY_ARRAY;
3059
- }
3060
- const result = new Array(keys.length);
3061
- keys.forEach((key, index) => {
3062
- result[index] = childNodes[key];
3063
- });
3064
- return result;
3041
+ const result = Object.values(childNodes);
3042
+ return result.length ? result : EMPTY_ARRAY;
3065
3043
  }
3066
3044
 
3067
3045
  const plainObjectString = Object.toString();
@@ -3079,9 +3057,7 @@ const EMPTY_OBJECT = Object.freeze({});
3079
3057
  * @internal
3080
3058
  * @hidden
3081
3059
  */
3082
- const mobxShallow = _getGlobalState().useProxies
3083
- ? { deep: false }
3084
- : { deep: false, proxy: false };
3060
+ const mobxShallow = { deep: false };
3085
3061
  Object.freeze(mobxShallow);
3086
3062
  /**
3087
3063
  * @internal
@@ -3279,10 +3255,19 @@ class EventHandler {
3279
3255
  this.handlers.length = 0;
3280
3256
  }
3281
3257
  emit(...args) {
3282
- // iterate a copy so (un)registrations during emit don't disturb this pass
3283
- // and reentrant emits stay correct
3284
- const handlers = this.handlers.slice();
3285
- handlers.forEach(f => f(...args));
3258
+ const handlers = this.handlers;
3259
+ // 0 and 1 handlers are by far the common cases (a patch/snapshot listener, a
3260
+ // disposer) and need no copy: with a single handler, calling the value we
3261
+ // already read is exactly what iterating a copy of it would do.
3262
+ if (handlers.length <= 1) {
3263
+ handlers[0]?.(...args);
3264
+ return;
3265
+ }
3266
+ // otherwise iterate a copy so (un)registrations during emit don't disturb
3267
+ // this pass and reentrant emits stay correct
3268
+ for (const f of handlers.slice()) {
3269
+ f(...args);
3270
+ }
3286
3271
  }
3287
3272
  }
3288
3273
  /**
@@ -3663,6 +3648,11 @@ function escapeJsonPath(path) {
3663
3648
  * Unescape slashes and backslashes.
3664
3649
  */
3665
3650
  function unescapeJsonPath(path) {
3651
+ // every escape sequence starts with `~`, so bail before the two regex passes
3652
+ // for the overwhelmingly common unescaped part (mirrors escapeJsonPath above)
3653
+ if (!path.includes("~")) {
3654
+ return path;
3655
+ }
3666
3656
  return path.replace(/~1/g, "/").replace(/~0/g, "~");
3667
3657
  }
3668
3658
  /**
@@ -3909,7 +3899,11 @@ var MapIdentifierMode;
3909
3899
  })(MapIdentifierMode || (MapIdentifierMode = {}));
3910
3900
  class MSTMap extends ObservableMap {
3911
3901
  constructor(initialData, name) {
3912
- super(initialData, observable.ref.enhancer, name);
3902
+ // mobx's default (deep) enhancer. The old `observable.ref.enhancer` arg
3903
+ // read undefined ever since mobx 6 renamed the field to `options_.enhancer`,
3904
+ // so deep has been the effective behavior all along; mobx 7 dropped
3905
+ // `observable.ref` entirely.
3906
+ super(initialData, undefined, name);
3913
3907
  }
3914
3908
  get(key) {
3915
3909
  // maybe this is over-enthousiastic? normalize numeric keys to strings
@@ -4149,22 +4143,21 @@ class MapType extends ComplexType {
4149
4143
  applySnapshot(node, snapshot) {
4150
4144
  typecheckInternal(this, snapshot);
4151
4145
  const target = node.storedValue;
4152
- const currentKeys = {};
4153
- Array.from(target.keys()).forEach(key => {
4154
- currentKeys[key] = false;
4155
- });
4146
+ // keys present before the apply, minus the ones the snapshot (re)sets — what
4147
+ // remains has to be deleted. A Set rather than a plain object: it needs no
4148
+ // sentinel values, no second pass to find the survivors, and it can hold keys
4149
+ // like "__proto__", which an object literal silently swallows.
4150
+ const staleKeys = new Set(target.keys());
4156
4151
  if (snapshot) {
4157
4152
  // Don't use target.replace, as it will throw away all existing items first
4158
4153
  for (const key in snapshot) {
4159
4154
  target.set(key, snapshot[key]);
4160
- currentKeys[`${key}`] = true;
4155
+ staleKeys.delete(key);
4161
4156
  }
4162
4157
  }
4163
- Object.keys(currentKeys).forEach(key => {
4164
- if (currentKeys[key] === false) {
4165
- target.delete(key);
4166
- }
4167
- });
4158
+ for (const key of staleKeys) {
4159
+ target.delete(key);
4160
+ }
4168
4161
  }
4169
4162
  getChildType() {
4170
4163
  return this._subType;
@@ -4304,7 +4297,7 @@ class ArrayType extends ComplexType {
4304
4297
  if (change.newValue === change.object[change.index]) {
4305
4298
  return null;
4306
4299
  }
4307
- const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], [change.index]);
4300
+ const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], change.index);
4308
4301
  if (!updatedNodes) {
4309
4302
  return null;
4310
4303
  }
@@ -4314,7 +4307,7 @@ class ArrayType extends ComplexType {
4314
4307
  case "splice":
4315
4308
  {
4316
4309
  const { index, removedCount, added } = change;
4317
- const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, added.map((_, i) => index + i));
4310
+ const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, index);
4318
4311
  if (!addedNodes) {
4319
4312
  return null;
4320
4313
  }
@@ -4440,7 +4433,11 @@ function array(subtype) {
4440
4433
  assertIsType(subtype, 1);
4441
4434
  return new ArrayType(`${subtype.name}[]`, subtype);
4442
4435
  }
4443
- function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths) {
4436
+ /**
4437
+ * @param firstNewPath index the reconciled slice starts at; both call sites
4438
+ * hand over a contiguous run, so subpath `i` is simply `firstNewPath + i`
4439
+ */
4440
+ function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNewPath) {
4444
4441
  let nothingChanged = true;
4445
4442
  // When the element type is a plain model with an identifier, index the old
4446
4443
  // nodes by id so a moved / replaced child is matched in O(1) instead of the
@@ -4463,7 +4460,7 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4463
4460
  const hasNewNode = i <= newValues.length - 1;
4464
4461
  const oldNode = oldNodes[i];
4465
4462
  let newValue = hasNewNode ? newValues[i] : undefined;
4466
- const newPath = `${newPaths[i]}`;
4463
+ const newPath = `${firstNewPath + i}`;
4467
4464
  // for some reason, instead of newValue we got a node, fallback to the storedValue
4468
4465
  // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681
4469
4466
  if (isNode(newValue)) {
@@ -4587,12 +4584,14 @@ function areSame(oldNode, newValue) {
4587
4584
  if (!(oldNode instanceof ObjectNode)) {
4588
4585
  return false;
4589
4586
  }
4587
+ if (oldNode.identifier === null ||
4588
+ !oldNode.identifierAttribute ||
4589
+ !isPlainObject(newValue)) {
4590
+ return false;
4591
+ }
4590
4592
  const oldNodeType = oldNode.getReconciliationType();
4591
4593
  // new value is a snapshot with the correct identifier
4592
- return (oldNode.identifier !== null &&
4593
- oldNode.identifierAttribute &&
4594
- isPlainObject(newValue) &&
4595
- oldNodeType.is(newValue) &&
4594
+ return (oldNodeType.is(newValue) &&
4596
4595
  oldNodeType.isMatchingSnapshotId(oldNode, newValue));
4597
4596
  }
4598
4597
  /**
@@ -4607,6 +4606,9 @@ function isArrayType(type) {
4607
4606
 
4608
4607
  const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot";
4609
4608
  const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
4609
+ function getPropObservable(storedValue, key) {
4610
+ return getAtom(storedValue, key);
4611
+ }
4610
4612
  function objectTypeToString() {
4611
4613
  return getStateTreeNode(this).toString();
4612
4614
  }
@@ -4616,16 +4618,8 @@ const defaultObjectOptions = {
4616
4618
  initializers: EMPTY_ARRAY
4617
4619
  };
4618
4620
  function toPropertiesObject(declaredProps) {
4619
- const keysList = Object.keys(declaredProps);
4620
- const alreadySeenKeys = new Set();
4621
- keysList.forEach(key => {
4622
- if (alreadySeenKeys.has(key)) {
4623
- throw fail(`${key} is declared twice in the model. Model should not contain the same keys`);
4624
- }
4625
- alreadySeenKeys.add(key);
4626
- });
4627
4621
  // loop through properties and ensures that all items are types
4628
- return keysList.reduce((props, key) => {
4622
+ return Object.keys(declaredProps).reduce((props, key) => {
4629
4623
  // warn if user intended a HOOK
4630
4624
  if (key in Hook) {
4631
4625
  throw fail(`Hook '${key}' was defined as property. Hooks should be defined as part of the actions`);
@@ -4665,7 +4659,10 @@ function toPropertiesObject(declaredProps) {
4665
4659
  throw fail(`Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})`);
4666
4660
  }
4667
4661
  return props;
4668
- }, { ...declaredProps });
4662
+ },
4663
+ // seeded with the raw declaration; every key is either replaced by its
4664
+ // converted type below, left alone because it already is one, or throws
4665
+ { ...declaredProps });
4669
4666
  }
4670
4667
  /**
4671
4668
  * @internal
@@ -4947,7 +4944,8 @@ class ModelType extends ComplexType {
4947
4944
  // success so a broken type keeps throwing on every create.
4948
4945
  if (!this.duplicateKeysChecked) {
4949
4946
  this.forAllProps(name => {
4950
- if (isComputedProp(instance, name) || !isObservableProp(instance, name)) {
4947
+ if (isComputedProp(instance, name) ||
4948
+ !isObservableProp(instance, name)) {
4951
4949
  throw fail(`${name} property is declared twice`);
4952
4950
  }
4953
4951
  });
@@ -4987,33 +4985,52 @@ class ModelType extends ComplexType {
4987
4985
  }
4988
4986
  }
4989
4987
  getChildren(node) {
4990
- const res = [];
4991
- this.forAllProps(name => {
4992
- res.push(this.getChildNode(node, name));
4993
- });
4988
+ const names = this.propertyNames;
4989
+ const res = new Array(names.length);
4990
+ for (let i = 0; i < names.length; i++) {
4991
+ res[i] = this.getPropertyNode(node, names[i]);
4992
+ }
4994
4993
  return res;
4995
4994
  }
4996
- getChildNode(node, key) {
4997
- if (!(key in this.properties)) {
4998
- throw fail(`Not a value property: ${key}`);
4999
- }
5000
- const adm = _getAdministration(node.storedValue, key);
5001
- const childNode = adm.raw?.();
4995
+ /**
4996
+ * Same as {@link getChildNode} but for a key already known to be a declared
4997
+ * property (callers iterating `propertyNames`), so the membership guard is
4998
+ * skipped.
4999
+ */
5000
+ getPropertyNode(node, key) {
5001
+ const childNode = getPropObservable(node.storedValue, key).raw();
5002
5002
  if (!childNode) {
5003
5003
  throw fail(`Node not available for property ${key}`);
5004
5004
  }
5005
5005
  return childNode;
5006
5006
  }
5007
+ getChildNode(node, key) {
5008
+ if (!(key in this.properties)) {
5009
+ throw fail(`Not a value property: ${key}`);
5010
+ }
5011
+ return this.getPropertyNode(node, key);
5012
+ }
5007
5013
  getSnapshot(node, applyPostProcess = true) {
5008
5014
  const res = {};
5009
- this.forAllProps((name, type) => {
5010
- getAtom(node.storedValue, name).reportObserved();
5011
- const snapshot = this.getChildNode(node, name).snapshot;
5015
+ const storedValue = node.storedValue;
5016
+ const properties = this.properties;
5017
+ for (const name of this.propertyNames) {
5018
+ // One mobx lookup serves both purposes: reportObserved so the snapshot
5019
+ // computed recomputes when the child is reassigned (raw() below does not
5020
+ // track), and raw() to read the child node. Going through getChildNode
5021
+ // would repeat the same lookup for every property.
5022
+ const observable = getPropObservable(storedValue, name);
5023
+ observable.reportObserved();
5024
+ const childNode = observable.raw();
5025
+ if (!childNode) {
5026
+ throw fail(`Node not available for property ${name}`);
5027
+ }
5028
+ const snapshot = childNode.snapshot;
5012
5029
  // strip-default optionals omit their key when equal to the default
5013
- if (!shouldStripChildFromSnapshot(type, snapshot)) {
5030
+ if (!shouldStripChildFromSnapshot(properties[name], snapshot)) {
5014
5031
  res[name] = snapshot;
5015
5032
  }
5016
- });
5033
+ }
5017
5034
  if (applyPostProcess) {
5018
5035
  return this.applySnapshotPostProcessor(res);
5019
5036
  }
@@ -5077,7 +5094,10 @@ class ModelType extends ComplexType {
5077
5094
  return typeCheckSuccess();
5078
5095
  }
5079
5096
  forAllProps(fn) {
5080
- this.propertyNames.forEach(key => fn(key, this.properties[key]));
5097
+ const properties = this.properties;
5098
+ for (const key of this.propertyNames) {
5099
+ fn(key, properties[key]);
5100
+ }
5081
5101
  }
5082
5102
  describe() {
5083
5103
  // optimization: cache
@@ -5547,11 +5567,16 @@ class Union extends BaseType {
5547
5567
  _types;
5548
5568
  _dispatcher;
5549
5569
  _eager = true;
5570
+ // Deliberately recomputed rather than memoized: a `types.late` member reports
5571
+ // 0 for its subtype until the definition resolves, so the fold is not stable
5572
+ // over the type's lifetime and a memo would need a carve-out for exactly the
5573
+ // case that motivates it. Measured at ~4% of union creation, which is not
5574
+ // worth another cache.
5550
5575
  get flags() {
5551
5576
  let result = TypeFlags.Union;
5552
- this._types.forEach(type => {
5577
+ for (const type of this._types) {
5553
5578
  result |= type.flags;
5554
- });
5579
+ }
5555
5580
  return result;
5556
5581
  }
5557
5582
  constructor(name, _types, options) {
@@ -5693,13 +5718,9 @@ class Union extends BaseType {
5693
5718
  if (reconcileCurrentType.is(value)) {
5694
5719
  return reconcileCurrentType;
5695
5720
  }
5696
- return this._types
5697
- .filter(t => t !== reconcileCurrentType)
5698
- .find(type => type.is(value));
5699
- }
5700
- else {
5701
- return this._types.find(type => type.is(value));
5721
+ return this._types.find(type => type !== reconcileCurrentType && type.is(value));
5702
5722
  }
5723
+ return this._types.find(type => type.is(value));
5703
5724
  }
5704
5725
  tryQuickMatch(value, reconcileCurrentType) {
5705
5726
  // state tree nodes need full type compatibility checking
@@ -5711,15 +5732,17 @@ class Union extends BaseType {
5711
5732
  if (!isPlainObject(value)) {
5712
5733
  return this.tryMatchPrimitive(value);
5713
5734
  }
5714
- // for objects, try structural matching against model types
5715
- const typesToCheck = reconcileCurrentType
5716
- ? [
5717
- reconcileCurrentType,
5718
- ...this._types.filter(t => t !== reconcileCurrentType)
5719
- ]
5720
- : this._types;
5721
- for (const type of typesToCheck) {
5722
- if (this.snapshotLooksLikeType(value, type)) {
5735
+ // for objects, try structural matching against model types, preferring the
5736
+ // reconciliation type (checked first, then skipped in the main pass) so a
5737
+ // reconcile keeps its current member when it still fits — without building
5738
+ // the reordered candidate list this used to allocate on every call
5739
+ if (reconcileCurrentType &&
5740
+ this.snapshotLooksLikeType(value, reconcileCurrentType)) {
5741
+ return reconcileCurrentType;
5742
+ }
5743
+ for (const type of this._types) {
5744
+ if (type !== reconcileCurrentType &&
5745
+ this.snapshotLooksLikeType(value, type)) {
5723
5746
  return type;
5724
5747
  }
5725
5748
  }
@@ -5834,7 +5857,7 @@ class Union extends BaseType {
5834
5857
  if (applicableTypes === 1) {
5835
5858
  return typeCheckSuccess();
5836
5859
  }
5837
- return typeCheckFailure(context, value, "No type is applicable for the union").concat(flattenTypeErrors(allErrors));
5860
+ return typeCheckFailure(context, value, "No type is applicable for the union").concat(allErrors.flat());
5838
5861
  }
5839
5862
  getSubTypes() {
5840
5863
  return this._types;
@@ -5934,15 +5957,15 @@ class OptionalValue extends BaseType {
5934
5957
  : this.getDefaultInstanceOrSnapshot(), parent, subpath);
5935
5958
  }
5936
5959
  getDefaultInstanceOrSnapshot() {
5937
- const defaultInstanceOrSnapshot = typeof this._defaultValue === "function"
5938
- ? this._defaultValue()
5939
- : this._defaultValue;
5940
- // while static values are already snapshots and checked on types.optional
5941
- // generator functions must always be rechecked just in case
5942
- if (typeof this._defaultValue === "function") {
5943
- typecheckInternal(this, defaultInstanceOrSnapshot);
5960
+ const defaultValue = this._defaultValue;
5961
+ if (typeof defaultValue !== "function") {
5962
+ // static values are already snapshots, checked once by types.optional
5963
+ return defaultValue;
5944
5964
  }
5945
- return defaultInstanceOrSnapshot;
5965
+ const generated = defaultValue();
5966
+ // generator functions must always be rechecked just in case
5967
+ typecheckInternal(this, generated);
5968
+ return generated;
5946
5969
  }
5947
5970
  isValidSnapshot(value, context) {
5948
5971
  // defaulted values can be skipped
@@ -6034,33 +6057,70 @@ function isOptionalType(type) {
6034
6057
  return isType(type) && (type.flags & TypeFlags.Optional) > 0;
6035
6058
  }
6036
6059
  /**
6037
- * Compare a child snapshot to a stripped-default's reference snapshot. Mirrors
6038
- * the legacy hand-rolled comparison: identity for primitives, structural for
6039
- * objects/arrays.
6060
+ * Compare a child snapshot to a stripped-default's reference snapshot: identity
6061
+ * for primitives, structural for objects/arrays.
6062
+ *
6063
+ * Walks the two snapshots in parallel and bails at the first difference. The
6064
+ * previous implementation compared `JSON.stringify(a) === JSON.stringify(b)`
6065
+ * behind a size guard, which is O(whole snapshot) on both sides — plus two
6066
+ * string allocations — even when the very first key differs. That is the normal
6067
+ * case for an identified sub-model: its snapshot has exactly the same shape as
6068
+ * the default and differs only in the identifier, so the size guard never fires
6069
+ * and every `getSnapshot` of the parent serialized both trees just to answer
6070
+ * "no".
6071
+ *
6072
+ * Snapshots are frozen, acyclic, JSON-ish plain data, so a plain recursive walk
6073
+ * is safe. Key order is compared too, matching what stringify comparison did:
6074
+ * the point is to answer "is this still the default", and the only unsafe answer
6075
+ * is a false positive (a key gets dropped from the snapshot that shouldn't be),
6076
+ * so where this can't reproduce stringify exactly — `{a: undefined}` vs
6077
+ * `{b: undefined}`, or `NaN` nested in a frozen value, both of which stringify
6078
+ * flattened into equal text — it errs toward "not equal" and simply keeps the
6079
+ * key. Neither shape is reachable from a model snapshot, whose key set and order
6080
+ * are fixed by its type.
6040
6081
  */
6041
6082
  function defaultSnapshotEquals(a, b) {
6042
6083
  if (a === b) {
6043
6084
  return true;
6044
6085
  }
6045
- if (typeof a === "object" &&
6046
- a !== null &&
6047
- typeof b === "object" &&
6048
- b !== null) {
6049
- // Cheap structural short-circuit before the full stringify compare: a
6050
- // value of a different size can't equal the default, so the common strip
6051
- // case (non-empty value vs an empty `[]`/`{}` default) avoids stringifying
6052
- // a potentially large snapshot on every getSnapshot.
6053
- if (Array.isArray(a) !== Array.isArray(b)) {
6086
+ if (typeof a !== "object" ||
6087
+ a === null ||
6088
+ typeof b !== "object" ||
6089
+ b === null) {
6090
+ return false;
6091
+ }
6092
+ const aIsArray = Array.isArray(a);
6093
+ if (aIsArray !== Array.isArray(b)) {
6094
+ return false;
6095
+ }
6096
+ if (aIsArray) {
6097
+ const arrayA = a;
6098
+ const arrayB = b;
6099
+ if (arrayA.length !== arrayB.length) {
6054
6100
  return false;
6055
6101
  }
6056
- const aSize = Array.isArray(a) ? a.length : Object.keys(a).length;
6057
- const bSize = Array.isArray(b) ? b.length : Object.keys(b).length;
6058
- if (aSize !== bSize) {
6102
+ for (let i = 0; i < arrayA.length; i++) {
6103
+ if (!defaultSnapshotEquals(arrayA[i], arrayB[i])) {
6104
+ return false;
6105
+ }
6106
+ }
6107
+ return true;
6108
+ }
6109
+ const objectA = a;
6110
+ const objectB = b;
6111
+ const keysA = Object.keys(objectA);
6112
+ const keysB = Object.keys(objectB);
6113
+ if (keysA.length !== keysB.length) {
6114
+ return false;
6115
+ }
6116
+ for (let i = 0; i < keysA.length; i++) {
6117
+ const key = keysA[i];
6118
+ if (key !== keysB[i] ||
6119
+ !defaultSnapshotEquals(objectA[key], objectB[key])) {
6059
6120
  return false;
6060
6121
  }
6061
- return JSON.stringify(a) === JSON.stringify(b);
6062
6122
  }
6063
- return false;
6123
+ return true;
6064
6124
  }
6065
6125
  /**
6066
6126
  * An optional type that additionally omits its key from a parent model's