@jbrowse/mobx-state-tree 6.0.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.
package/dist/index.d.ts CHANGED
@@ -945,7 +945,6 @@ declare abstract class BaseType<C, S, T, N extends BaseNode<any, any, any> = Bas
945
945
  */
946
946
  declare abstract class ComplexType<C, S, T> extends BaseType<C, S, T, ObjectNode<C, S, T>> {
947
947
  identifierAttribute?: string;
948
- constructor(name: string);
949
948
  create(snapshot?: C, environment?: any): any;
950
949
  getValue(node: this["N"]): T;
951
950
  abstract getDefaultSnapshot(): C;
@@ -953,7 +952,7 @@ declare abstract class ComplexType<C, S, T> extends BaseType<C, S, T, ObjectNode
953
952
  abstract finalizeNewInstance(node: this["N"], instance: any): void;
954
953
  abstract applySnapshot(node: this["N"], snapshot: C): void;
955
954
  abstract applyPatchLocally(node: this["N"], subpath: string, patch: IJsonPatch): void;
956
- abstract processInitialSnapshot(childNodes: IChildNodesMap, snapshot: C): S;
955
+ abstract processInitialSnapshot(childNodes: IChildNodesMap): S;
957
956
  abstract getChildren(node: this["N"]): ReadonlyArray<AnyNode>;
958
957
  abstract getChildNode(node: this["N"], key: string): AnyNode;
959
958
  abstract getChildType(propertyName?: string): IAnyType;
@@ -1245,7 +1244,6 @@ declare class IdentifierCache {
1245
1244
  private cacheId;
1246
1245
  private cache;
1247
1246
  private lastCacheModificationPerId;
1248
- constructor();
1249
1247
  private updateLastCacheModificationPerId;
1250
1248
  getLastCacheModificationPerId(identifier: string): string;
1251
1249
  addNodeToCache(node: AnyObjectNode, lastCacheUpdate?: boolean): void;
@@ -946,7 +946,6 @@ class BaseNode {
946
946
  constructor(type, parent, subpath, environment) {
947
947
  this.type = type;
948
948
  this.environment = environment;
949
- this.environment = environment;
950
949
  this.baseSetParent(parent, subpath);
951
950
  }
952
951
  getReconciliationType() {
@@ -1414,10 +1413,7 @@ class ObjectNode extends BaseNode {
1414
1413
  }
1415
1414
  _getCachedInitialSnapshot() {
1416
1415
  if (!this._cachedInitialSnapshotCreated) {
1417
- const type = this.type;
1418
- const childNodes = this._childNodes;
1419
- const snapshot = this._initialSnapshot;
1420
- this._cachedInitialSnapshot = type.processInitialSnapshot(childNodes, snapshot);
1416
+ this._cachedInitialSnapshot = this.type.processInitialSnapshot(this._childNodes);
1421
1417
  this._cachedInitialSnapshotCreated = true;
1422
1418
  }
1423
1419
  return this._cachedInitialSnapshot;
@@ -1810,9 +1806,6 @@ BaseType.prototype.create = mobx.action(BaseType.prototype.create);
1810
1806
  */
1811
1807
  class ComplexType extends BaseType {
1812
1808
  identifierAttribute;
1813
- constructor(name) {
1814
- super(name);
1815
- }
1816
1809
  create(snapshot = this.getDefaultSnapshot(), environment) {
1817
1810
  return super.create(snapshot, environment);
1818
1811
  }
@@ -2714,13 +2707,6 @@ function typeCheckFailure(context, value, message) {
2714
2707
  // Clone context since it may be mutated after this error is created
2715
2708
  return [{ context: context.slice(), value, message }];
2716
2709
  }
2717
- /**
2718
- * @internal
2719
- * @hidden
2720
- */
2721
- function flattenTypeErrors(errors) {
2722
- return errors.flat();
2723
- }
2724
2710
  // TODO; doublecheck: typecheck should only needed to be invoked from: type.create and array / map / value.property will change
2725
2711
  /**
2726
2712
  * @internal
@@ -2782,7 +2768,6 @@ class IdentifierCache {
2782
2768
  // last time the cache (array) for a given time changed
2783
2769
  // n.b. it is not really the time, but just an integer that gets increased after each modification to the array
2784
2770
  lastCacheModificationPerId = mobx.observable.map();
2785
- constructor() { }
2786
2771
  updateLastCacheModificationPerId(identifier) {
2787
2772
  const lcm = this.lastCacheModificationPerId.get(identifier);
2788
2773
  // we start at 1 since 0 means no update since cache creation
@@ -2823,7 +2808,7 @@ class IdentifierCache {
2823
2808
  if (!set.length) {
2824
2809
  this.cache.delete(id);
2825
2810
  }
2826
- this.updateLastCacheModificationPerId(node.identifier);
2811
+ this.updateLastCacheModificationPerId(id);
2827
2812
  }
2828
2813
  }
2829
2814
  }
@@ -3055,15 +3040,8 @@ function convertChildNodesToArray(childNodes) {
3055
3040
  if (!childNodes) {
3056
3041
  return EMPTY_ARRAY;
3057
3042
  }
3058
- const keys = Object.keys(childNodes);
3059
- if (!keys.length) {
3060
- return EMPTY_ARRAY;
3061
- }
3062
- const result = new Array(keys.length);
3063
- keys.forEach((key, index) => {
3064
- result[index] = childNodes[key];
3065
- });
3066
- return result;
3043
+ const result = Object.values(childNodes);
3044
+ return result.length ? result : EMPTY_ARRAY;
3067
3045
  }
3068
3046
 
3069
3047
  const plainObjectString = Object.toString();
@@ -3279,10 +3257,19 @@ class EventHandler {
3279
3257
  this.handlers.length = 0;
3280
3258
  }
3281
3259
  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));
3260
+ const handlers = this.handlers;
3261
+ // 0 and 1 handlers are by far the common cases (a patch/snapshot listener, a
3262
+ // disposer) and need no copy: with a single handler, calling the value we
3263
+ // already read is exactly what iterating a copy of it would do.
3264
+ if (handlers.length <= 1) {
3265
+ handlers[0]?.(...args);
3266
+ return;
3267
+ }
3268
+ // otherwise iterate a copy so (un)registrations during emit don't disturb
3269
+ // this pass and reentrant emits stay correct
3270
+ for (const f of handlers.slice()) {
3271
+ f(...args);
3272
+ }
3286
3273
  }
3287
3274
  }
3288
3275
  /**
@@ -3663,6 +3650,11 @@ function escapeJsonPath(path) {
3663
3650
  * Unescape slashes and backslashes.
3664
3651
  */
3665
3652
  function unescapeJsonPath(path) {
3653
+ // every escape sequence starts with `~`, so bail before the two regex passes
3654
+ // for the overwhelmingly common unescaped part (mirrors escapeJsonPath above)
3655
+ if (!path.includes("~")) {
3656
+ return path;
3657
+ }
3666
3658
  return path.replace(/~1/g, "/").replace(/~0/g, "~");
3667
3659
  }
3668
3660
  /**
@@ -4153,22 +4145,21 @@ class MapType extends ComplexType {
4153
4145
  applySnapshot(node, snapshot) {
4154
4146
  typecheckInternal(this, snapshot);
4155
4147
  const target = node.storedValue;
4156
- const currentKeys = {};
4157
- Array.from(target.keys()).forEach(key => {
4158
- currentKeys[key] = false;
4159
- });
4148
+ // keys present before the apply, minus the ones the snapshot (re)sets — what
4149
+ // remains has to be deleted. A Set rather than a plain object: it needs no
4150
+ // sentinel values, no second pass to find the survivors, and it can hold keys
4151
+ // like "__proto__", which an object literal silently swallows.
4152
+ const staleKeys = new Set(target.keys());
4160
4153
  if (snapshot) {
4161
4154
  // Don't use target.replace, as it will throw away all existing items first
4162
4155
  for (const key in snapshot) {
4163
4156
  target.set(key, snapshot[key]);
4164
- currentKeys[`${key}`] = true;
4157
+ staleKeys.delete(key);
4165
4158
  }
4166
4159
  }
4167
- Object.keys(currentKeys).forEach(key => {
4168
- if (currentKeys[key] === false) {
4169
- target.delete(key);
4170
- }
4171
- });
4160
+ for (const key of staleKeys) {
4161
+ target.delete(key);
4162
+ }
4172
4163
  }
4173
4164
  getChildType() {
4174
4165
  return this._subType;
@@ -4308,7 +4299,7 @@ class ArrayType extends ComplexType {
4308
4299
  if (change.newValue === change.object[change.index]) {
4309
4300
  return null;
4310
4301
  }
4311
- const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], [change.index]);
4302
+ const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], change.index);
4312
4303
  if (!updatedNodes) {
4313
4304
  return null;
4314
4305
  }
@@ -4318,7 +4309,7 @@ class ArrayType extends ComplexType {
4318
4309
  case "splice":
4319
4310
  {
4320
4311
  const { index, removedCount, added } = change;
4321
- const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, added.map((_, i) => index + i));
4312
+ const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, index);
4322
4313
  if (!addedNodes) {
4323
4314
  return null;
4324
4315
  }
@@ -4444,7 +4435,11 @@ function array(subtype) {
4444
4435
  assertIsType(subtype, 1);
4445
4436
  return new ArrayType(`${subtype.name}[]`, subtype);
4446
4437
  }
4447
- function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths) {
4438
+ /**
4439
+ * @param firstNewPath index the reconciled slice starts at; both call sites
4440
+ * hand over a contiguous run, so subpath `i` is simply `firstNewPath + i`
4441
+ */
4442
+ function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNewPath) {
4448
4443
  let nothingChanged = true;
4449
4444
  // When the element type is a plain model with an identifier, index the old
4450
4445
  // nodes by id so a moved / replaced child is matched in O(1) instead of the
@@ -4467,7 +4462,7 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4467
4462
  const hasNewNode = i <= newValues.length - 1;
4468
4463
  const oldNode = oldNodes[i];
4469
4464
  let newValue = hasNewNode ? newValues[i] : undefined;
4470
- const newPath = `${newPaths[i]}`;
4465
+ const newPath = `${firstNewPath + i}`;
4471
4466
  // for some reason, instead of newValue we got a node, fallback to the storedValue
4472
4467
  // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681
4473
4468
  if (isNode(newValue)) {
@@ -4591,12 +4586,14 @@ function areSame(oldNode, newValue) {
4591
4586
  if (!(oldNode instanceof ObjectNode)) {
4592
4587
  return false;
4593
4588
  }
4589
+ if (oldNode.identifier === null ||
4590
+ !oldNode.identifierAttribute ||
4591
+ !isPlainObject(newValue)) {
4592
+ return false;
4593
+ }
4594
4594
  const oldNodeType = oldNode.getReconciliationType();
4595
4595
  // new value is a snapshot with the correct identifier
4596
- return (oldNode.identifier !== null &&
4597
- oldNode.identifierAttribute &&
4598
- isPlainObject(newValue) &&
4599
- oldNodeType.is(newValue) &&
4596
+ return (oldNodeType.is(newValue) &&
4600
4597
  oldNodeType.isMatchingSnapshotId(oldNode, newValue));
4601
4598
  }
4602
4599
  /**
@@ -4611,6 +4608,9 @@ function isArrayType(type) {
4611
4608
 
4612
4609
  const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot";
4613
4610
  const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
4611
+ function getPropObservable(storedValue, key) {
4612
+ return mobx.getAtom(storedValue, key);
4613
+ }
4614
4614
  function objectTypeToString() {
4615
4615
  return getStateTreeNode(this).toString();
4616
4616
  }
@@ -4620,16 +4620,8 @@ const defaultObjectOptions = {
4620
4620
  initializers: EMPTY_ARRAY
4621
4621
  };
4622
4622
  function toPropertiesObject(declaredProps) {
4623
- const keysList = Object.keys(declaredProps);
4624
- const alreadySeenKeys = new Set();
4625
- keysList.forEach(key => {
4626
- if (alreadySeenKeys.has(key)) {
4627
- throw fail(`${key} is declared twice in the model. Model should not contain the same keys`);
4628
- }
4629
- alreadySeenKeys.add(key);
4630
- });
4631
4623
  // loop through properties and ensures that all items are types
4632
- return keysList.reduce((props, key) => {
4624
+ return Object.keys(declaredProps).reduce((props, key) => {
4633
4625
  // warn if user intended a HOOK
4634
4626
  if (key in Hook) {
4635
4627
  throw fail(`Hook '${key}' was defined as property. Hooks should be defined as part of the actions`);
@@ -4669,7 +4661,10 @@ function toPropertiesObject(declaredProps) {
4669
4661
  throw fail(`Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})`);
4670
4662
  }
4671
4663
  return props;
4672
- }, { ...declaredProps });
4664
+ },
4665
+ // seeded with the raw declaration; every key is either replaced by its
4666
+ // converted type below, left alone because it already is one, or throws
4667
+ { ...declaredProps });
4673
4668
  }
4674
4669
  /**
4675
4670
  * @internal
@@ -4951,7 +4946,8 @@ class ModelType extends ComplexType {
4951
4946
  // success so a broken type keeps throwing on every create.
4952
4947
  if (!this.duplicateKeysChecked) {
4953
4948
  this.forAllProps(name => {
4954
- if (mobx.isComputedProp(instance, name) || !mobx.isObservableProp(instance, name)) {
4949
+ if (mobx.isComputedProp(instance, name) ||
4950
+ !mobx.isObservableProp(instance, name)) {
4955
4951
  throw fail(`${name} property is declared twice`);
4956
4952
  }
4957
4953
  });
@@ -4991,33 +4987,52 @@ class ModelType extends ComplexType {
4991
4987
  }
4992
4988
  }
4993
4989
  getChildren(node) {
4994
- const res = [];
4995
- this.forAllProps(name => {
4996
- res.push(this.getChildNode(node, name));
4997
- });
4990
+ const names = this.propertyNames;
4991
+ const res = new Array(names.length);
4992
+ for (let i = 0; i < names.length; i++) {
4993
+ res[i] = this.getPropertyNode(node, names[i]);
4994
+ }
4998
4995
  return res;
4999
4996
  }
5000
- getChildNode(node, key) {
5001
- if (!(key in this.properties)) {
5002
- throw fail(`Not a value property: ${key}`);
5003
- }
5004
- const adm = mobx._getAdministration(node.storedValue, key);
5005
- const childNode = adm.raw?.();
4997
+ /**
4998
+ * Same as {@link getChildNode} but for a key already known to be a declared
4999
+ * property (callers iterating `propertyNames`), so the membership guard is
5000
+ * skipped.
5001
+ */
5002
+ getPropertyNode(node, key) {
5003
+ const childNode = getPropObservable(node.storedValue, key).raw();
5006
5004
  if (!childNode) {
5007
5005
  throw fail(`Node not available for property ${key}`);
5008
5006
  }
5009
5007
  return childNode;
5010
5008
  }
5009
+ getChildNode(node, key) {
5010
+ if (!(key in this.properties)) {
5011
+ throw fail(`Not a value property: ${key}`);
5012
+ }
5013
+ return this.getPropertyNode(node, key);
5014
+ }
5011
5015
  getSnapshot(node, applyPostProcess = true) {
5012
5016
  const res = {};
5013
- this.forAllProps((name, type) => {
5014
- mobx.getAtom(node.storedValue, name).reportObserved();
5015
- const snapshot = this.getChildNode(node, name).snapshot;
5017
+ const storedValue = node.storedValue;
5018
+ const properties = this.properties;
5019
+ for (const name of this.propertyNames) {
5020
+ // One mobx lookup serves both purposes: reportObserved so the snapshot
5021
+ // computed recomputes when the child is reassigned (raw() below does not
5022
+ // track), and raw() to read the child node. Going through getChildNode
5023
+ // would repeat the same lookup for every property.
5024
+ const observable = getPropObservable(storedValue, name);
5025
+ observable.reportObserved();
5026
+ const childNode = observable.raw();
5027
+ if (!childNode) {
5028
+ throw fail(`Node not available for property ${name}`);
5029
+ }
5030
+ const snapshot = childNode.snapshot;
5016
5031
  // strip-default optionals omit their key when equal to the default
5017
- if (!shouldStripChildFromSnapshot(type, snapshot)) {
5032
+ if (!shouldStripChildFromSnapshot(properties[name], snapshot)) {
5018
5033
  res[name] = snapshot;
5019
5034
  }
5020
- });
5035
+ }
5021
5036
  if (applyPostProcess) {
5022
5037
  return this.applySnapshotPostProcessor(res);
5023
5038
  }
@@ -5081,7 +5096,10 @@ class ModelType extends ComplexType {
5081
5096
  return typeCheckSuccess();
5082
5097
  }
5083
5098
  forAllProps(fn) {
5084
- this.propertyNames.forEach(key => fn(key, this.properties[key]));
5099
+ const properties = this.properties;
5100
+ for (const key of this.propertyNames) {
5101
+ fn(key, properties[key]);
5102
+ }
5085
5103
  }
5086
5104
  describe() {
5087
5105
  // optimization: cache
@@ -5551,11 +5569,16 @@ class Union extends BaseType {
5551
5569
  _types;
5552
5570
  _dispatcher;
5553
5571
  _eager = true;
5572
+ // Deliberately recomputed rather than memoized: a `types.late` member reports
5573
+ // 0 for its subtype until the definition resolves, so the fold is not stable
5574
+ // over the type's lifetime and a memo would need a carve-out for exactly the
5575
+ // case that motivates it. Measured at ~4% of union creation, which is not
5576
+ // worth another cache.
5554
5577
  get flags() {
5555
5578
  let result = TypeFlags.Union;
5556
- this._types.forEach(type => {
5579
+ for (const type of this._types) {
5557
5580
  result |= type.flags;
5558
- });
5581
+ }
5559
5582
  return result;
5560
5583
  }
5561
5584
  constructor(name, _types, options) {
@@ -5697,13 +5720,9 @@ class Union extends BaseType {
5697
5720
  if (reconcileCurrentType.is(value)) {
5698
5721
  return reconcileCurrentType;
5699
5722
  }
5700
- return this._types
5701
- .filter(t => t !== reconcileCurrentType)
5702
- .find(type => type.is(value));
5703
- }
5704
- else {
5705
- return this._types.find(type => type.is(value));
5723
+ return this._types.find(type => type !== reconcileCurrentType && type.is(value));
5706
5724
  }
5725
+ return this._types.find(type => type.is(value));
5707
5726
  }
5708
5727
  tryQuickMatch(value, reconcileCurrentType) {
5709
5728
  // state tree nodes need full type compatibility checking
@@ -5715,15 +5734,17 @@ class Union extends BaseType {
5715
5734
  if (!isPlainObject(value)) {
5716
5735
  return this.tryMatchPrimitive(value);
5717
5736
  }
5718
- // for objects, try structural matching against model types
5719
- const typesToCheck = reconcileCurrentType
5720
- ? [
5721
- reconcileCurrentType,
5722
- ...this._types.filter(t => t !== reconcileCurrentType)
5723
- ]
5724
- : this._types;
5725
- for (const type of typesToCheck) {
5726
- if (this.snapshotLooksLikeType(value, type)) {
5737
+ // for objects, try structural matching against model types, preferring the
5738
+ // reconciliation type (checked first, then skipped in the main pass) so a
5739
+ // reconcile keeps its current member when it still fits — without building
5740
+ // the reordered candidate list this used to allocate on every call
5741
+ if (reconcileCurrentType &&
5742
+ this.snapshotLooksLikeType(value, reconcileCurrentType)) {
5743
+ return reconcileCurrentType;
5744
+ }
5745
+ for (const type of this._types) {
5746
+ if (type !== reconcileCurrentType &&
5747
+ this.snapshotLooksLikeType(value, type)) {
5727
5748
  return type;
5728
5749
  }
5729
5750
  }
@@ -5838,7 +5859,7 @@ class Union extends BaseType {
5838
5859
  if (applicableTypes === 1) {
5839
5860
  return typeCheckSuccess();
5840
5861
  }
5841
- return typeCheckFailure(context, value, "No type is applicable for the union").concat(flattenTypeErrors(allErrors));
5862
+ return typeCheckFailure(context, value, "No type is applicable for the union").concat(allErrors.flat());
5842
5863
  }
5843
5864
  getSubTypes() {
5844
5865
  return this._types;
@@ -5938,15 +5959,15 @@ class OptionalValue extends BaseType {
5938
5959
  : this.getDefaultInstanceOrSnapshot(), parent, subpath);
5939
5960
  }
5940
5961
  getDefaultInstanceOrSnapshot() {
5941
- const defaultInstanceOrSnapshot = typeof this._defaultValue === "function"
5942
- ? this._defaultValue()
5943
- : this._defaultValue;
5944
- // while static values are already snapshots and checked on types.optional
5945
- // generator functions must always be rechecked just in case
5946
- if (typeof this._defaultValue === "function") {
5947
- typecheckInternal(this, defaultInstanceOrSnapshot);
5962
+ const defaultValue = this._defaultValue;
5963
+ if (typeof defaultValue !== "function") {
5964
+ // static values are already snapshots, checked once by types.optional
5965
+ return defaultValue;
5948
5966
  }
5949
- return defaultInstanceOrSnapshot;
5967
+ const generated = defaultValue();
5968
+ // generator functions must always be rechecked just in case
5969
+ typecheckInternal(this, generated);
5970
+ return generated;
5950
5971
  }
5951
5972
  isValidSnapshot(value, context) {
5952
5973
  // defaulted values can be skipped
@@ -6038,33 +6059,70 @@ function isOptionalType(type) {
6038
6059
  return isType(type) && (type.flags & TypeFlags.Optional) > 0;
6039
6060
  }
6040
6061
  /**
6041
- * Compare a child snapshot to a stripped-default's reference snapshot. Mirrors
6042
- * the legacy hand-rolled comparison: identity for primitives, structural for
6043
- * objects/arrays.
6062
+ * Compare a child snapshot to a stripped-default's reference snapshot: identity
6063
+ * for primitives, structural for objects/arrays.
6064
+ *
6065
+ * Walks the two snapshots in parallel and bails at the first difference. The
6066
+ * previous implementation compared `JSON.stringify(a) === JSON.stringify(b)`
6067
+ * behind a size guard, which is O(whole snapshot) on both sides — plus two
6068
+ * string allocations — even when the very first key differs. That is the normal
6069
+ * case for an identified sub-model: its snapshot has exactly the same shape as
6070
+ * the default and differs only in the identifier, so the size guard never fires
6071
+ * and every `getSnapshot` of the parent serialized both trees just to answer
6072
+ * "no".
6073
+ *
6074
+ * Snapshots are frozen, acyclic, JSON-ish plain data, so a plain recursive walk
6075
+ * is safe. Key order is compared too, matching what stringify comparison did:
6076
+ * the point is to answer "is this still the default", and the only unsafe answer
6077
+ * is a false positive (a key gets dropped from the snapshot that shouldn't be),
6078
+ * so where this can't reproduce stringify exactly — `{a: undefined}` vs
6079
+ * `{b: undefined}`, or `NaN` nested in a frozen value, both of which stringify
6080
+ * flattened into equal text — it errs toward "not equal" and simply keeps the
6081
+ * key. Neither shape is reachable from a model snapshot, whose key set and order
6082
+ * are fixed by its type.
6044
6083
  */
6045
6084
  function defaultSnapshotEquals(a, b) {
6046
6085
  if (a === b) {
6047
6086
  return true;
6048
6087
  }
6049
- if (typeof a === "object" &&
6050
- a !== null &&
6051
- typeof b === "object" &&
6052
- b !== null) {
6053
- // Cheap structural short-circuit before the full stringify compare: a
6054
- // value of a different size can't equal the default, so the common strip
6055
- // case (non-empty value vs an empty `[]`/`{}` default) avoids stringifying
6056
- // a potentially large snapshot on every getSnapshot.
6057
- if (Array.isArray(a) !== Array.isArray(b)) {
6088
+ if (typeof a !== "object" ||
6089
+ a === null ||
6090
+ typeof b !== "object" ||
6091
+ b === null) {
6092
+ return false;
6093
+ }
6094
+ const aIsArray = Array.isArray(a);
6095
+ if (aIsArray !== Array.isArray(b)) {
6096
+ return false;
6097
+ }
6098
+ if (aIsArray) {
6099
+ const arrayA = a;
6100
+ const arrayB = b;
6101
+ if (arrayA.length !== arrayB.length) {
6058
6102
  return false;
6059
6103
  }
6060
- const aSize = Array.isArray(a) ? a.length : Object.keys(a).length;
6061
- const bSize = Array.isArray(b) ? b.length : Object.keys(b).length;
6062
- if (aSize !== bSize) {
6104
+ for (let i = 0; i < arrayA.length; i++) {
6105
+ if (!defaultSnapshotEquals(arrayA[i], arrayB[i])) {
6106
+ return false;
6107
+ }
6108
+ }
6109
+ return true;
6110
+ }
6111
+ const objectA = a;
6112
+ const objectB = b;
6113
+ const keysA = Object.keys(objectA);
6114
+ const keysB = Object.keys(objectB);
6115
+ if (keysA.length !== keysB.length) {
6116
+ return false;
6117
+ }
6118
+ for (let i = 0; i < keysA.length; i++) {
6119
+ const key = keysA[i];
6120
+ if (key !== keysB[i] ||
6121
+ !defaultSnapshotEquals(objectA[key], objectB[key])) {
6063
6122
  return false;
6064
6123
  }
6065
- return JSON.stringify(a) === JSON.stringify(b);
6066
6124
  }
6067
- return false;
6125
+ return true;
6068
6126
  }
6069
6127
  /**
6070
6128
  * An optional type that additionally omits its key from a parent model's