@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.
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();
@@ -3081,9 +3059,7 @@ const EMPTY_OBJECT = Object.freeze({});
3081
3059
  * @internal
3082
3060
  * @hidden
3083
3061
  */
3084
- const mobxShallow = mobx._getGlobalState().useProxies
3085
- ? { deep: false }
3086
- : { deep: false, proxy: false };
3062
+ const mobxShallow = { deep: false };
3087
3063
  Object.freeze(mobxShallow);
3088
3064
  /**
3089
3065
  * @internal
@@ -3281,10 +3257,19 @@ class EventHandler {
3281
3257
  this.handlers.length = 0;
3282
3258
  }
3283
3259
  emit(...args) {
3284
- // iterate a copy so (un)registrations during emit don't disturb this pass
3285
- // and reentrant emits stay correct
3286
- const handlers = this.handlers.slice();
3287
- 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
+ }
3288
3273
  }
3289
3274
  }
3290
3275
  /**
@@ -3665,6 +3650,11 @@ function escapeJsonPath(path) {
3665
3650
  * Unescape slashes and backslashes.
3666
3651
  */
3667
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
+ }
3668
3658
  return path.replace(/~1/g, "/").replace(/~0/g, "~");
3669
3659
  }
3670
3660
  /**
@@ -3911,7 +3901,11 @@ var MapIdentifierMode;
3911
3901
  })(MapIdentifierMode || (MapIdentifierMode = {}));
3912
3902
  class MSTMap extends mobx.ObservableMap {
3913
3903
  constructor(initialData, name) {
3914
- super(initialData, mobx.observable.ref.enhancer, name);
3904
+ // mobx's default (deep) enhancer. The old `observable.ref.enhancer` arg
3905
+ // read undefined ever since mobx 6 renamed the field to `options_.enhancer`,
3906
+ // so deep has been the effective behavior all along; mobx 7 dropped
3907
+ // `observable.ref` entirely.
3908
+ super(initialData, undefined, name);
3915
3909
  }
3916
3910
  get(key) {
3917
3911
  // maybe this is over-enthousiastic? normalize numeric keys to strings
@@ -4151,22 +4145,21 @@ class MapType extends ComplexType {
4151
4145
  applySnapshot(node, snapshot) {
4152
4146
  typecheckInternal(this, snapshot);
4153
4147
  const target = node.storedValue;
4154
- const currentKeys = {};
4155
- Array.from(target.keys()).forEach(key => {
4156
- currentKeys[key] = false;
4157
- });
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());
4158
4153
  if (snapshot) {
4159
4154
  // Don't use target.replace, as it will throw away all existing items first
4160
4155
  for (const key in snapshot) {
4161
4156
  target.set(key, snapshot[key]);
4162
- currentKeys[`${key}`] = true;
4157
+ staleKeys.delete(key);
4163
4158
  }
4164
4159
  }
4165
- Object.keys(currentKeys).forEach(key => {
4166
- if (currentKeys[key] === false) {
4167
- target.delete(key);
4168
- }
4169
- });
4160
+ for (const key of staleKeys) {
4161
+ target.delete(key);
4162
+ }
4170
4163
  }
4171
4164
  getChildType() {
4172
4165
  return this._subType;
@@ -4306,7 +4299,7 @@ class ArrayType extends ComplexType {
4306
4299
  if (change.newValue === change.object[change.index]) {
4307
4300
  return null;
4308
4301
  }
4309
- 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);
4310
4303
  if (!updatedNodes) {
4311
4304
  return null;
4312
4305
  }
@@ -4316,7 +4309,7 @@ class ArrayType extends ComplexType {
4316
4309
  case "splice":
4317
4310
  {
4318
4311
  const { index, removedCount, added } = change;
4319
- 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);
4320
4313
  if (!addedNodes) {
4321
4314
  return null;
4322
4315
  }
@@ -4442,7 +4435,11 @@ function array(subtype) {
4442
4435
  assertIsType(subtype, 1);
4443
4436
  return new ArrayType(`${subtype.name}[]`, subtype);
4444
4437
  }
4445
- 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) {
4446
4443
  let nothingChanged = true;
4447
4444
  // When the element type is a plain model with an identifier, index the old
4448
4445
  // nodes by id so a moved / replaced child is matched in O(1) instead of the
@@ -4465,7 +4462,7 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4465
4462
  const hasNewNode = i <= newValues.length - 1;
4466
4463
  const oldNode = oldNodes[i];
4467
4464
  let newValue = hasNewNode ? newValues[i] : undefined;
4468
- const newPath = `${newPaths[i]}`;
4465
+ const newPath = `${firstNewPath + i}`;
4469
4466
  // for some reason, instead of newValue we got a node, fallback to the storedValue
4470
4467
  // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681
4471
4468
  if (isNode(newValue)) {
@@ -4589,12 +4586,14 @@ function areSame(oldNode, newValue) {
4589
4586
  if (!(oldNode instanceof ObjectNode)) {
4590
4587
  return false;
4591
4588
  }
4589
+ if (oldNode.identifier === null ||
4590
+ !oldNode.identifierAttribute ||
4591
+ !isPlainObject(newValue)) {
4592
+ return false;
4593
+ }
4592
4594
  const oldNodeType = oldNode.getReconciliationType();
4593
4595
  // new value is a snapshot with the correct identifier
4594
- return (oldNode.identifier !== null &&
4595
- oldNode.identifierAttribute &&
4596
- isPlainObject(newValue) &&
4597
- oldNodeType.is(newValue) &&
4596
+ return (oldNodeType.is(newValue) &&
4598
4597
  oldNodeType.isMatchingSnapshotId(oldNode, newValue));
4599
4598
  }
4600
4599
  /**
@@ -4609,6 +4608,9 @@ function isArrayType(type) {
4609
4608
 
4610
4609
  const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot";
4611
4610
  const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
4611
+ function getPropObservable(storedValue, key) {
4612
+ return mobx.getAtom(storedValue, key);
4613
+ }
4612
4614
  function objectTypeToString() {
4613
4615
  return getStateTreeNode(this).toString();
4614
4616
  }
@@ -4618,16 +4620,8 @@ const defaultObjectOptions = {
4618
4620
  initializers: EMPTY_ARRAY
4619
4621
  };
4620
4622
  function toPropertiesObject(declaredProps) {
4621
- const keysList = Object.keys(declaredProps);
4622
- const alreadySeenKeys = new Set();
4623
- keysList.forEach(key => {
4624
- if (alreadySeenKeys.has(key)) {
4625
- throw fail(`${key} is declared twice in the model. Model should not contain the same keys`);
4626
- }
4627
- alreadySeenKeys.add(key);
4628
- });
4629
4623
  // loop through properties and ensures that all items are types
4630
- return keysList.reduce((props, key) => {
4624
+ return Object.keys(declaredProps).reduce((props, key) => {
4631
4625
  // warn if user intended a HOOK
4632
4626
  if (key in Hook) {
4633
4627
  throw fail(`Hook '${key}' was defined as property. Hooks should be defined as part of the actions`);
@@ -4667,7 +4661,10 @@ function toPropertiesObject(declaredProps) {
4667
4661
  throw fail(`Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})`);
4668
4662
  }
4669
4663
  return props;
4670
- }, { ...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 });
4671
4668
  }
4672
4669
  /**
4673
4670
  * @internal
@@ -4949,7 +4946,8 @@ class ModelType extends ComplexType {
4949
4946
  // success so a broken type keeps throwing on every create.
4950
4947
  if (!this.duplicateKeysChecked) {
4951
4948
  this.forAllProps(name => {
4952
- if (mobx.isComputedProp(instance, name) || !mobx.isObservableProp(instance, name)) {
4949
+ if (mobx.isComputedProp(instance, name) ||
4950
+ !mobx.isObservableProp(instance, name)) {
4953
4951
  throw fail(`${name} property is declared twice`);
4954
4952
  }
4955
4953
  });
@@ -4989,33 +4987,52 @@ class ModelType extends ComplexType {
4989
4987
  }
4990
4988
  }
4991
4989
  getChildren(node) {
4992
- const res = [];
4993
- this.forAllProps(name => {
4994
- res.push(this.getChildNode(node, name));
4995
- });
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
+ }
4996
4995
  return res;
4997
4996
  }
4998
- getChildNode(node, key) {
4999
- if (!(key in this.properties)) {
5000
- throw fail(`Not a value property: ${key}`);
5001
- }
5002
- const adm = mobx._getAdministration(node.storedValue, key);
5003
- 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();
5004
5004
  if (!childNode) {
5005
5005
  throw fail(`Node not available for property ${key}`);
5006
5006
  }
5007
5007
  return childNode;
5008
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
+ }
5009
5015
  getSnapshot(node, applyPostProcess = true) {
5010
5016
  const res = {};
5011
- this.forAllProps((name, type) => {
5012
- mobx.getAtom(node.storedValue, name).reportObserved();
5013
- 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;
5014
5031
  // strip-default optionals omit their key when equal to the default
5015
- if (!shouldStripChildFromSnapshot(type, snapshot)) {
5032
+ if (!shouldStripChildFromSnapshot(properties[name], snapshot)) {
5016
5033
  res[name] = snapshot;
5017
5034
  }
5018
- });
5035
+ }
5019
5036
  if (applyPostProcess) {
5020
5037
  return this.applySnapshotPostProcessor(res);
5021
5038
  }
@@ -5079,7 +5096,10 @@ class ModelType extends ComplexType {
5079
5096
  return typeCheckSuccess();
5080
5097
  }
5081
5098
  forAllProps(fn) {
5082
- 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
+ }
5083
5103
  }
5084
5104
  describe() {
5085
5105
  // optimization: cache
@@ -5549,11 +5569,16 @@ class Union extends BaseType {
5549
5569
  _types;
5550
5570
  _dispatcher;
5551
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.
5552
5577
  get flags() {
5553
5578
  let result = TypeFlags.Union;
5554
- this._types.forEach(type => {
5579
+ for (const type of this._types) {
5555
5580
  result |= type.flags;
5556
- });
5581
+ }
5557
5582
  return result;
5558
5583
  }
5559
5584
  constructor(name, _types, options) {
@@ -5695,13 +5720,9 @@ class Union extends BaseType {
5695
5720
  if (reconcileCurrentType.is(value)) {
5696
5721
  return reconcileCurrentType;
5697
5722
  }
5698
- return this._types
5699
- .filter(t => t !== reconcileCurrentType)
5700
- .find(type => type.is(value));
5701
- }
5702
- else {
5703
- return this._types.find(type => type.is(value));
5723
+ return this._types.find(type => type !== reconcileCurrentType && type.is(value));
5704
5724
  }
5725
+ return this._types.find(type => type.is(value));
5705
5726
  }
5706
5727
  tryQuickMatch(value, reconcileCurrentType) {
5707
5728
  // state tree nodes need full type compatibility checking
@@ -5713,15 +5734,17 @@ class Union extends BaseType {
5713
5734
  if (!isPlainObject(value)) {
5714
5735
  return this.tryMatchPrimitive(value);
5715
5736
  }
5716
- // for objects, try structural matching against model types
5717
- const typesToCheck = reconcileCurrentType
5718
- ? [
5719
- reconcileCurrentType,
5720
- ...this._types.filter(t => t !== reconcileCurrentType)
5721
- ]
5722
- : this._types;
5723
- for (const type of typesToCheck) {
5724
- 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)) {
5725
5748
  return type;
5726
5749
  }
5727
5750
  }
@@ -5836,7 +5859,7 @@ class Union extends BaseType {
5836
5859
  if (applicableTypes === 1) {
5837
5860
  return typeCheckSuccess();
5838
5861
  }
5839
- 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());
5840
5863
  }
5841
5864
  getSubTypes() {
5842
5865
  return this._types;
@@ -5936,15 +5959,15 @@ class OptionalValue extends BaseType {
5936
5959
  : this.getDefaultInstanceOrSnapshot(), parent, subpath);
5937
5960
  }
5938
5961
  getDefaultInstanceOrSnapshot() {
5939
- const defaultInstanceOrSnapshot = typeof this._defaultValue === "function"
5940
- ? this._defaultValue()
5941
- : this._defaultValue;
5942
- // while static values are already snapshots and checked on types.optional
5943
- // generator functions must always be rechecked just in case
5944
- if (typeof this._defaultValue === "function") {
5945
- 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;
5946
5966
  }
5947
- return defaultInstanceOrSnapshot;
5967
+ const generated = defaultValue();
5968
+ // generator functions must always be rechecked just in case
5969
+ typecheckInternal(this, generated);
5970
+ return generated;
5948
5971
  }
5949
5972
  isValidSnapshot(value, context) {
5950
5973
  // defaulted values can be skipped
@@ -6036,33 +6059,70 @@ function isOptionalType(type) {
6036
6059
  return isType(type) && (type.flags & TypeFlags.Optional) > 0;
6037
6060
  }
6038
6061
  /**
6039
- * Compare a child snapshot to a stripped-default's reference snapshot. Mirrors
6040
- * the legacy hand-rolled comparison: identity for primitives, structural for
6041
- * 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.
6042
6083
  */
6043
6084
  function defaultSnapshotEquals(a, b) {
6044
6085
  if (a === b) {
6045
6086
  return true;
6046
6087
  }
6047
- if (typeof a === "object" &&
6048
- a !== null &&
6049
- typeof b === "object" &&
6050
- b !== null) {
6051
- // Cheap structural short-circuit before the full stringify compare: a
6052
- // value of a different size can't equal the default, so the common strip
6053
- // case (non-empty value vs an empty `[]`/`{}` default) avoids stringifying
6054
- // a potentially large snapshot on every getSnapshot.
6055
- 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) {
6056
6102
  return false;
6057
6103
  }
6058
- const aSize = Array.isArray(a) ? a.length : Object.keys(a).length;
6059
- const bSize = Array.isArray(b) ? b.length : Object.keys(b).length;
6060
- 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])) {
6061
6122
  return false;
6062
6123
  }
6063
- return JSON.stringify(a) === JSON.stringify(b);
6064
6124
  }
6065
- return false;
6125
+ return true;
6066
6126
  }
6067
6127
  /**
6068
6128
  * An optional type that additionally omits its key from a parent model's