@jbrowse/mobx-state-tree 6.5.2 → 6.6.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.
@@ -1,4 +1,4 @@
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';
1
+ import { isComputedProp, isObservableProp, action, createAtom, 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
  /**
@@ -565,7 +565,9 @@ function getRelativePath(base, target) {
565
565
  }
566
566
  /**
567
567
  * Returns a deep copy of the given state tree node as new tree.
568
- * Shorthand for `snapshot(x) = getType(x).create(getSnapshot(x))`
568
+ * Like `getType(x).create(getSnapshot(x))`, except that a node wrapped in a
569
+ * `types.snapshotProcessor` is recreated through the processor, which
570
+ * `getType(x)` (the wrapped type) would skip.
569
571
  *
570
572
  * _Tip: clone will create a literal copy, including the same identifiers. To modify identifiers etc. during cloning, don't use clone but take a snapshot of the tree, modify it, and create new instance_
571
573
  *
@@ -577,11 +579,11 @@ function clone(source, keepEnvironment = true) {
577
579
  // check all arguments
578
580
  assertIsStateTreeNode(source, 1);
579
581
  const node = getStateTreeNode(source);
580
- return node.type.create(node.snapshot, keepEnvironment === true
582
+ return (node.snapshotProcessorType ?? node.type).create(node.snapshot, keepEnvironment === true
581
583
  ? node.root.environment
582
584
  : keepEnvironment === false
583
585
  ? undefined
584
- : keepEnvironment); // it's an object or something else
586
+ : keepEnvironment);
585
587
  }
586
588
  /**
587
589
  * Removes a model element from the state tree, and let it live on as a new state tree
@@ -700,17 +702,15 @@ function walk(target, processor) {
700
702
  * @returns
701
703
  */
702
704
  function getPropertyMembers(typeOrNode) {
703
- let type;
704
- if (isStateTreeNode(typeOrNode)) {
705
- type = getType(typeOrNode);
706
- }
707
- else {
708
- type = typeOrNode;
709
- }
710
- assertArg(type, t => isModelType(t), "model type or model instance", 1);
705
+ const type = isStateTreeNode(typeOrNode)
706
+ ? getType(typeOrNode)
707
+ : typeOrNode;
708
+ assertArg(type, isModelType, "model type or model instance", 1);
709
+ // a wrapped model reports the model's properties; a union of models has no
710
+ // single set and reports none
711
711
  return {
712
712
  name: type.name,
713
- properties: { ...type.properties }
713
+ properties: { ...asModelType(type)?.properties }
714
714
  };
715
715
  }
716
716
  /**
@@ -912,16 +912,28 @@ class BaseNode {
912
912
  }
913
913
  }
914
914
  _hookSubscribers;
915
- fireInternalHook(name) {
916
- if (this._hookSubscribers) {
917
- this._hookSubscribers.emit(name, this, name);
915
+ fireInternalHook(name, subject = this) {
916
+ this._hookSubscribers?.emit(name, subject, name);
917
+ }
918
+ /**
919
+ * Detaching a node takes its whole subtree out of the tree, but only the
920
+ * detached node's parent changes, so only it fires `beforeDetach` for real.
921
+ * Internal subscribers below it (reference watchers on a target) still need
922
+ * to know their node is leaving.
923
+ */
924
+ notifyDetachOf(subject) {
925
+ this.fireInternalHook(Hook.beforeDetach, subject);
926
+ }
927
+ isWithin(ancestor) {
928
+ for (let node = this.parent; node; node = node.parent) {
929
+ if (node === ancestor) {
930
+ return true;
931
+ }
918
932
  }
933
+ return false;
919
934
  }
920
935
  registerHook(hook, hookHandler) {
921
- if (!this._hookSubscribers) {
922
- this._hookSubscribers = new EventHandlers();
923
- }
924
- return this._hookSubscribers.register(hook, hookHandler);
936
+ return (this._hookSubscribers ??= new EventHandlers()).register(hook, hookHandler);
925
937
  }
926
938
  _parent;
927
939
  get parent() {
@@ -982,6 +994,13 @@ class BaseNode {
982
994
  this.aliveAtom.reportObserved();
983
995
  return this.isAlive;
984
996
  }
997
+ die() {
998
+ if (!this.isAlive || this.isDetaching) {
999
+ return;
1000
+ }
1001
+ this.aboutToDie();
1002
+ this.finalizeDeath();
1003
+ }
985
1004
  baseFinalizeCreation(whenFinalized) {
986
1005
  if (devMode()) {
987
1006
  if (!this.isAlive) {
@@ -1017,6 +1036,7 @@ class BaseNode {
1017
1036
  this.fireHook(Hook.beforeDestroy);
1018
1037
  }
1019
1038
  }
1039
+ BaseNode.prototype.die = action(BaseNode.prototype.die);
1020
1040
 
1021
1041
  /**
1022
1042
  * @internal
@@ -1034,9 +1054,6 @@ class ScalarNode extends BaseNode {
1034
1054
  throw e;
1035
1055
  }
1036
1056
  this.state = NodeLifeCycle.CREATED;
1037
- // for scalar nodes there's no point in firing this event since it would fire on the constructor, before
1038
- // anybody can actually register for/listen to it
1039
- // this.fireHook(Hook.AfterCreate)
1040
1057
  this.finalizeCreation();
1041
1058
  }
1042
1059
  get root() {
@@ -1079,13 +1096,6 @@ class ScalarNode extends BaseNode {
1079
1096
  const path = (this.isAlive ? this.path : this.pathUponDeath) || "<root>";
1080
1097
  return `${this.type.name}@${path}${this.isAlive ? "" : " [dead]"}`;
1081
1098
  }
1082
- die() {
1083
- if (!this.isAlive || this.state === NodeLifeCycle.DETACHING) {
1084
- return;
1085
- }
1086
- this.aboutToDie();
1087
- this.finalizeDeath();
1088
- }
1089
1099
  finalizeCreation() {
1090
1100
  this.baseFinalizeCreation();
1091
1101
  }
@@ -1099,7 +1109,6 @@ class ScalarNode extends BaseNode {
1099
1109
  this.fireInternalHook(name);
1100
1110
  }
1101
1111
  }
1102
- ScalarNode.prototype.die = action(ScalarNode.prototype.die);
1103
1112
 
1104
1113
  let nextNodeId = 1;
1105
1114
  var ObservableInstanceLifecycle;
@@ -1287,8 +1296,8 @@ class ObjectNode extends BaseNode {
1287
1296
  if (!this.parent) {
1288
1297
  return;
1289
1298
  }
1290
- // detach if attached
1291
1299
  this.fireHook(Hook.beforeDetach);
1300
+ this.notifyChildrenOfDetach(this);
1292
1301
  const previousState = this.state;
1293
1302
  this.state = NodeLifeCycle.DETACHING;
1294
1303
  const root = this.root;
@@ -1304,6 +1313,21 @@ class ObjectNode extends BaseNode {
1304
1313
  this.state = previousState;
1305
1314
  }
1306
1315
  }
1316
+ notifyDetachOf(subject) {
1317
+ super.notifyDetachOf(subject);
1318
+ this.notifyChildrenOfDetach(subject);
1319
+ }
1320
+ notifyChildrenOfDetach(subject) {
1321
+ // a handler notified earlier in the walk may have destroyed this node.
1322
+ // A node never read still has to be walked: an instance created elsewhere
1323
+ // can have been moved under it, subscribers and all.
1324
+ if (!this.isAlive) {
1325
+ return;
1326
+ }
1327
+ for (const child of this.getChildren()) {
1328
+ child.notifyDetachOf(subject);
1329
+ }
1330
+ }
1307
1331
  setParent(newParent, subpath) {
1308
1332
  const parentChanged = newParent !== this.parent;
1309
1333
  const subpathChanged = subpath !== this.subpath;
@@ -1508,37 +1532,27 @@ class ObjectNode extends BaseNode {
1508
1532
  this.clearParent();
1509
1533
  }
1510
1534
  preboot() {
1511
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1512
- const self = this;
1513
1535
  this._applyPatches = createActionInvoker(this.storedValue, "@APPLY_PATCHES", (patches) => {
1514
1536
  patches.forEach(patch => {
1515
1537
  if (!patch.path) {
1516
- self.type.applySnapshot(self, patch.value);
1538
+ this.type.applySnapshot(this, patch.value);
1517
1539
  return;
1518
1540
  }
1519
1541
  const parts = splitJsonPath(patch.path);
1520
- const node = resolveNodeByPathParts(self, parts.slice(0, -1));
1542
+ const node = resolveNodeByPathParts(this, parts.slice(0, -1));
1521
1543
  node.applyPatchLocally(parts[parts.length - 1], patch);
1522
1544
  });
1523
1545
  });
1524
1546
  this._applySnapshot = createActionInvoker(this.storedValue, "@APPLY_SNAPSHOT", (snapshot) => {
1525
- // if the snapshot is the same as the current one, avoid performing a reconcile
1526
- if (snapshot === self.snapshot) {
1547
+ // the current snapshot needs no reconcile
1548
+ if (snapshot === this.snapshot) {
1527
1549
  return;
1528
1550
  }
1529
- // else, apply it by calling the type logic
1530
- return self.type.applySnapshot(self, snapshot);
1551
+ this.type.applySnapshot(this, snapshot);
1531
1552
  });
1532
1553
  addHiddenFinalProp(this.storedValue, "$treenode", this);
1533
1554
  addHiddenFinalProp(this.storedValue, "toJSON", toJSON);
1534
1555
  }
1535
- die() {
1536
- if (!this.isAlive || this.state === NodeLifeCycle.DETACHING) {
1537
- return;
1538
- }
1539
- this.aboutToDie();
1540
- this.finalizeDeath();
1541
- }
1542
1556
  aboutToDie() {
1543
1557
  if (this._observableInstanceState ===
1544
1558
  ObservableInstanceLifecycle.UNINITIALIZED) {
@@ -1550,8 +1564,8 @@ class ObjectNode extends BaseNode {
1550
1564
  // beforeDestroy should run before the disposers since else we could end up in a situation where
1551
1565
  // a disposer added with addDisposer at this stage (beforeDestroy) is actually never released
1552
1566
  this.baseAboutToDie();
1553
- this._internalEventsEmit(InternalEvents.Dispose);
1554
- this._internalEventsClear(InternalEvents.Dispose);
1567
+ this._internalEvents?.emit(InternalEvents.Dispose);
1568
+ this._internalEvents?.clear(InternalEvents.Dispose);
1555
1569
  }
1556
1570
  finalizeDeath() {
1557
1571
  // invariant: not called directly but from "die"
@@ -1562,57 +1576,57 @@ class ObjectNode extends BaseNode {
1562
1576
  // "kill" the computed prop and just store the last snapshot
1563
1577
  const snapshot = this.snapshot;
1564
1578
  this._snapshotUponDeath = snapshot;
1565
- this._internalEventsClearAll();
1579
+ this._internalEvents?.clearAll();
1566
1580
  this.baseFinalizeDeath();
1567
1581
  }
1568
1582
  onSnapshot(onChange) {
1569
1583
  this._addSnapshotReaction();
1570
- const unregister = this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1584
+ const unregister = this.internalEvents().register(InternalEvents.Snapshot, onChange);
1571
1585
  return () => {
1572
1586
  unregister();
1573
1587
  // The reaction re-serializes the whole subtree on every change, so leaving
1574
1588
  // it running once the last listener is gone would keep charging the node
1575
1589
  // for a snapshot nobody receives. Referential stability of getSnapshot()
1576
1590
  // does not depend on it — that comes from keepAlive on _snapshotComputed.
1577
- if (!this._internalEventsHasSubscribers(InternalEvents.Snapshot)) {
1591
+ if (!this._internalEvents?.hasSubscribers(InternalEvents.Snapshot)) {
1578
1592
  this._removeSnapshotReaction();
1579
1593
  }
1580
1594
  };
1581
1595
  }
1582
1596
  emitSnapshot(snapshot) {
1583
- this._internalEventsEmit(InternalEvents.Snapshot, snapshot);
1597
+ this._internalEvents?.emit(InternalEvents.Snapshot, snapshot);
1584
1598
  }
1585
1599
  onPatch(handler) {
1586
- return this._internalEventsRegister(InternalEvents.Patch, handler);
1600
+ return this.internalEvents().register(InternalEvents.Patch, handler);
1587
1601
  }
1588
1602
  emitPatch(basePatch, source) {
1589
- if (this._internalEventsHasSubscribers(InternalEvents.Patch)) {
1603
+ if (this._internalEvents?.hasSubscribers(InternalEvents.Patch)) {
1590
1604
  const localizedPatch = {
1591
1605
  ...basePatch,
1592
1606
  path: `${source.path.slice(this.path.length)}/${basePatch.path}` // calculate the relative path of the patch
1593
1607
  };
1594
1608
  const [patch, reversePatch] = splitPatch(localizedPatch);
1595
- this._internalEventsEmit(InternalEvents.Patch, patch, reversePatch);
1609
+ this._internalEvents?.emit(InternalEvents.Patch, patch, reversePatch);
1596
1610
  }
1597
1611
  if (this.parent) {
1598
1612
  this.parent.emitPatch(basePatch, source);
1599
1613
  }
1600
1614
  }
1601
1615
  hasDisposer(disposer) {
1602
- return this._internalEventsHas(InternalEvents.Dispose, disposer);
1616
+ return !!this._internalEvents?.has(InternalEvents.Dispose, disposer);
1603
1617
  }
1604
1618
  addDisposer(disposer) {
1605
1619
  if (!this.hasDisposer(disposer)) {
1606
- this._internalEventsRegister(InternalEvents.Dispose, disposer, true);
1620
+ this.internalEvents().register(InternalEvents.Dispose, disposer, true);
1607
1621
  return;
1608
1622
  }
1609
1623
  throw fail("cannot add a disposer when it is already registered for execution");
1610
1624
  }
1611
1625
  removeDisposer(disposer) {
1612
- if (!this._internalEventsHas(InternalEvents.Dispose, disposer)) {
1626
+ if (!this._internalEvents?.has(InternalEvents.Dispose, disposer)) {
1613
1627
  throw fail("cannot remove a disposer which was never registered for execution");
1614
1628
  }
1615
- this._internalEventsUnregister(InternalEvents.Dispose, disposer);
1629
+ this._internalEvents?.unregister(InternalEvents.Dispose, disposer);
1616
1630
  }
1617
1631
  removeMiddleware(middleware) {
1618
1632
  if (this.middlewares) {
@@ -1657,48 +1671,17 @@ class ObjectNode extends BaseNode {
1657
1671
  // not removeDisposer(), which throws when the registration is already gone —
1658
1672
  // the last listener can be disposed from within a disposer, i.e. after
1659
1673
  // aboutToDie has cleared them
1660
- this._internalEventsUnregister(InternalEvents.Dispose, disposer);
1674
+ this._internalEvents?.unregister(InternalEvents.Dispose, disposer);
1661
1675
  disposer();
1662
1676
  }
1663
- // #region internal event handling
1677
+ // created on first registration: most nodes never get a listener
1664
1678
  _internalEvents;
1665
- // we proxy the methods to avoid creating an EventHandlers instance when it is not needed
1666
- _internalEventsHasSubscribers(event) {
1667
- return !!this._internalEvents && this._internalEvents.hasSubscribers(event);
1668
- }
1669
- _internalEventsRegister(event, eventHandler, atTheBeginning = false) {
1670
- if (!this._internalEvents) {
1671
- this._internalEvents = new EventHandlers();
1672
- }
1673
- return this._internalEvents.register(event, eventHandler, atTheBeginning);
1674
- }
1675
- _internalEventsHas(event, eventHandler) {
1676
- return (!!this._internalEvents && this._internalEvents.has(event, eventHandler));
1677
- }
1678
- _internalEventsUnregister(event, eventHandler) {
1679
- if (this._internalEvents) {
1680
- this._internalEvents.unregister(event, eventHandler);
1681
- }
1682
- }
1683
- _internalEventsEmit(event, ...args) {
1684
- if (this._internalEvents) {
1685
- this._internalEvents.emit(event, ...args);
1686
- }
1687
- }
1688
- _internalEventsClear(event) {
1689
- if (this._internalEvents) {
1690
- this._internalEvents.clear(event);
1691
- }
1692
- }
1693
- _internalEventsClearAll() {
1694
- if (this._internalEvents) {
1695
- this._internalEvents.clearAll();
1696
- }
1679
+ internalEvents() {
1680
+ return (this._internalEvents ??= new EventHandlers());
1697
1681
  }
1698
1682
  }
1699
1683
  ObjectNode.prototype.createObservableInstance = action(ObjectNode.prototype.createObservableInstance);
1700
1684
  ObjectNode.prototype.detach = action(ObjectNode.prototype.detach);
1701
- ObjectNode.prototype.die = action(ObjectNode.prototype.die);
1702
1685
 
1703
1686
  /**
1704
1687
  * @internal
@@ -1890,7 +1873,6 @@ class ComplexType extends BaseType {
1890
1873
  return null;
1891
1874
  }
1892
1875
  }
1893
- ComplexType.prototype.create = action(ComplexType.prototype.create);
1894
1876
  /**
1895
1877
  * @internal
1896
1878
  * @hidden
@@ -1900,11 +1882,6 @@ class SimpleType extends BaseType {
1900
1882
  return snapshot;
1901
1883
  }
1902
1884
  getValue(node) {
1903
- // if we ever find a case where scalar nodes can be accessed without iterating through its parent
1904
- // uncomment this to make sure the parent chain is created when this is accessed
1905
- // if (node.parent) {
1906
- // node.parent.createObservableInstanceIfNeeded()
1907
- // }
1908
1885
  return node.storedValue;
1909
1886
  }
1910
1887
  getSnapshot(node) {
@@ -1934,6 +1911,30 @@ class SimpleType extends BaseType {
1934
1911
  function isType(value) {
1935
1912
  return (typeof value === "object" && value?.isType === true);
1936
1913
  }
1914
+ /**
1915
+ * The one type a wrapper hands everything to: what an `optional`,
1916
+ * `stripDefault`, `refinement`, `snapshotProcessor` or (resolved) `late`
1917
+ * wraps. `undefined` for any other type, including a union or a `resilient`,
1918
+ * which pick a type per value.
1919
+ *
1920
+ * Resolves a `late` type's definition if it has not been read yet.
1921
+ */
1922
+ function getWrappedType(type) {
1923
+ const subtype = type.getSubTypes();
1924
+ return isType(subtype) ? subtype : undefined;
1925
+ }
1926
+ /**
1927
+ * Strips every wrapper {@link getWrappedType} sees through, down to the type
1928
+ * that actually builds the value: for a complex type `T`,
1929
+ * `getType(T.create(snapshot)) === unwrapType(T)`.
1930
+ *
1931
+ * `types.model({ xs: types.array(X) }).properties.xs`, for instance, is an
1932
+ * `optional` whose unwrapped type is the array type.
1933
+ */
1934
+ function unwrapType(type) {
1935
+ const wrapped = getWrappedType(type);
1936
+ return wrapped ? unwrapType(wrapped) : type;
1937
+ }
1937
1938
  /**
1938
1939
  * @internal
1939
1940
  * @hidden
@@ -2932,7 +2933,7 @@ function createScalarNode(type, parent, subpath, environment, initialValue) {
2932
2933
  * @hidden
2933
2934
  */
2934
2935
  function isNode(value) {
2935
- return value instanceof ScalarNode || value instanceof ObjectNode;
2936
+ return value instanceof BaseNode;
2936
2937
  }
2937
2938
 
2938
2939
  /**
@@ -3111,25 +3112,6 @@ function fail(message = "Illegal state") {
3111
3112
  function identity(_) {
3112
3113
  return _;
3113
3114
  }
3114
- /**
3115
- * @internal
3116
- * @hidden
3117
- */
3118
- const isInteger = Number.isInteger;
3119
- /**
3120
- * @internal
3121
- * @hidden
3122
- */
3123
- function isFloat(val) {
3124
- return Number(val) === val && val % 1 !== 0;
3125
- }
3126
- /**
3127
- * @internal
3128
- * @hidden
3129
- */
3130
- function isFinite(val) {
3131
- return Number.isFinite(val);
3132
- }
3133
3115
  /**
3134
3116
  * @internal
3135
3117
  * @hidden
@@ -3777,11 +3759,8 @@ class SnapshotProcessor extends BaseType {
3777
3759
  return sn;
3778
3760
  }
3779
3761
  _fixNode(node) {
3780
- // the node's type is the *inner* type, so `getType(instance).create(...)`
3781
- // would bypass the processors — point it at ours instead
3782
- const nodeType = node.type;
3783
- nodeType.create = this.create.bind(this);
3784
3762
  if (node instanceof ObjectNode) {
3763
+ node.snapshotProcessorType = this;
3785
3764
  node.hasSnapshotPostProcessor = !!this._processors.postProcessor;
3786
3765
  }
3787
3766
  const oldGetSnapshot = node.getSnapshot;
@@ -4265,14 +4244,20 @@ function map(subtype) {
4265
4244
  return new MapType(subtype);
4266
4245
  }
4267
4246
  /**
4268
- * Returns if a given value represents a map type.
4269
- *
4270
- * @param type
4271
- * @returns `true` if it is a map type.
4247
+ * Returns if a type is a map type, or wraps or unions one. For the map type
4248
+ * itself, use {@link asMapType}.
4272
4249
  */
4273
4250
  function isMapType(type) {
4274
4251
  return isType(type) && (type.flags & TypeFlags.Map) > 0;
4275
4252
  }
4253
+ /**
4254
+ * The map type `type` builds its values with, seeing through the wrappers
4255
+ * {@link unwrapType} does, or `undefined` if that is not a map type.
4256
+ */
4257
+ function asMapType(type) {
4258
+ const unwrapped = unwrapType(type);
4259
+ return unwrapped instanceof MapType ? unwrapped : undefined;
4260
+ }
4276
4261
 
4277
4262
  /**
4278
4263
  * @internal
@@ -4482,98 +4467,174 @@ function array(subtype) {
4482
4467
  */
4483
4468
  function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNewPath) {
4484
4469
  let nothingChanged = true;
4485
- // When the element type is a plain model with an identifier, index the old
4486
- // nodes by id so a moved / replaced child is matched in O(1) instead of the
4487
- // linear scan in the reorder branch below. This turns a full array
4488
- // replacement (the common "load new data" case) from O(n^2) into O(n). Types
4489
- // whose id extraction needs type-specific preprocessing (union,
4490
- // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4491
- // `is()` before their id check, so they stay on the scan path.
4492
- // With at most one old node the scan below is already O(1), so building the
4493
- // index would only add a Map allocation to every single-element write.
4494
- let idIndex;
4495
- if (oldNodes.length > 1 &&
4496
- childType instanceof ModelType &&
4497
- childType.identifierAttribute) {
4498
- const byId = new Map();
4499
- for (const n of oldNodes) {
4500
- if (n instanceof ObjectNode && n.identifier !== null) {
4501
- byId.set(n.identifier, n);
4502
- }
4503
- }
4504
- idIndex = { attr: childType.identifierAttribute, byId };
4505
- }
4506
- for (let i = 0;; i++) {
4507
- const hasNewNode = i <= newValues.length - 1;
4508
- const oldNode = oldNodes[i];
4509
- let newValue = hasNewNode ? newValues[i] : undefined;
4470
+ const result = [];
4471
+ // every old node before `next` is claimed
4472
+ const claimed = new Uint8Array(oldNodes.length);
4473
+ let next = 0;
4474
+ let finder;
4475
+ for (let i = 0; i < newValues.length; i++) {
4476
+ while (claimed[next]) {
4477
+ next++;
4478
+ }
4479
+ const oldNode = oldNodes[next];
4480
+ let newValue = newValues[i];
4510
4481
  const newPath = `${firstNewPath + i}`;
4511
4482
  // for some reason, instead of newValue we got a node, fallback to the storedValue
4512
4483
  // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681
4513
4484
  if (isNode(newValue)) {
4514
4485
  newValue = newValue.storedValue;
4515
4486
  }
4516
- if (!oldNode && !hasNewNode) {
4517
- // both are empty, end
4518
- break;
4487
+ if (!oldNode) {
4488
+ if (isStateTreeNode(newValue) &&
4489
+ getStateTreeNode(newValue).parent === parent) {
4490
+ // this node is owned by this parent, but not in the reconcilable set, so it must be double
4491
+ throw fail(`Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '${parent.path}/${newPath}', but it lives already at '${getStateTreeNode(newValue).path}'`);
4492
+ }
4493
+ nothingChanged = false;
4494
+ result.push(valueAsNode(childType, parent, newPath, newValue));
4495
+ }
4496
+ else if (areSame(oldNode, newValue)) {
4497
+ claimed[next] = 1;
4498
+ result.push(valueAsNode(childType, parent, newPath, newValue, oldNode));
4519
4499
  }
4520
- else if (oldNode && !hasNewNode) {
4521
- // new one does not exists
4500
+ else {
4522
4501
  nothingChanged = false;
4523
- oldNodes.splice(i, 1);
4502
+ // a single-element write scans one node, cheaper than building an index
4503
+ const j = oldNodes.length > 1
4504
+ ? (finder ??= new ReuseFinder(childType, oldNodes, claimed)).find(newValue, next)
4505
+ : scanForSame(oldNodes, claimed, next, newValue);
4506
+ let oldMatch;
4507
+ if (j >= 0) {
4508
+ claimed[j] = 1;
4509
+ oldMatch = oldNodes[j];
4510
+ }
4511
+ result.push(valueAsNode(childType, parent, newPath, newValue, oldMatch));
4512
+ }
4513
+ }
4514
+ for (let j = next; j < oldNodes.length; j++) {
4515
+ if (!claimed[j]) {
4516
+ nothingChanged = false;
4517
+ const oldNode = oldNodes[j];
4524
4518
  if (oldNode instanceof ObjectNode) {
4525
4519
  // since it is going to be returned by pop/splice/shift better create it before killing it
4526
4520
  // so it doesn't end up in an undead state
4527
4521
  oldNode.createObservableInstanceIfNeeded();
4528
4522
  }
4529
4523
  oldNode.die();
4530
- i--;
4531
4524
  }
4532
- else if (!oldNode) {
4533
- // there is no old node, create it
4534
- // check if already belongs to the same parent. if so, avoid pushing item in. only swapping can occur.
4535
- if (isStateTreeNode(newValue) &&
4536
- getStateTreeNode(newValue).parent === parent) {
4537
- // this node is owned by this parent, but not in the reconcilable set, so it must be double
4538
- throw fail(`Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '${parent.path}/${newPath}', but it lives already at '${getStateTreeNode(newValue).path}'`);
4539
- }
4540
- nothingChanged = false;
4541
- const newNode = valueAsNode(childType, parent, newPath, newValue);
4542
- oldNodes.splice(i, 0, newNode);
4525
+ }
4526
+ return nothingChanged ? null : result;
4527
+ }
4528
+ function scanForSame(oldNodes, claimed, from, value) {
4529
+ for (let j = from; j < oldNodes.length; j++) {
4530
+ if (!claimed[j] && areSame(oldNodes[j], value)) {
4531
+ return j;
4543
4532
  }
4544
- else if (areSame(oldNode, newValue)) {
4545
- // both are the same, reconcile
4546
- oldNodes[i] = valueAsNode(childType, parent, newPath, newValue, oldNode);
4533
+ }
4534
+ return -1;
4535
+ }
4536
+ /**
4537
+ * Picks the old node the reorder branch reuses: the first unclaimed one
4538
+ * `areSame` holds for. `areSame` reduces to an equality — node identity for a
4539
+ * live node, the old node's snapshot for anything else — except where the
4540
+ * value is a plain object and the old node carries an identifier, where it
4541
+ * runs `is()` before matching ids. So only that case scans, and an identified
4542
+ * plain model looks its id up instead, as ADR 0001 records.
4543
+ */
4544
+ class ReuseFinder {
4545
+ oldNodes;
4546
+ claimed;
4547
+ idAttribute;
4548
+ byId;
4549
+ byNode;
4550
+ bySnapshot;
4551
+ anyIdentified;
4552
+ constructor(childType, oldNodes, claimed) {
4553
+ this.oldNodes = oldNodes;
4554
+ this.claimed = claimed;
4555
+ this.idAttribute =
4556
+ childType instanceof ModelType ? childType.identifierAttribute : undefined;
4557
+ }
4558
+ find(value, next) {
4559
+ let j;
4560
+ if (this.idAttribute && isPlainObject(value)) {
4561
+ j =
4562
+ this.idIndex(next).get(normalizeIdentifier(value[this.idAttribute])) ??
4563
+ -1;
4564
+ }
4565
+ else if (isStateTreeNode(value)) {
4566
+ j = this.nodeIndex(next).get(getStateTreeNode(value)) ?? -1;
4567
+ }
4568
+ else if (!isPlainObject(value) || !this.hasIdentifiedNode(next)) {
4569
+ j = this.firstWithSnapshot(value, next);
4547
4570
  }
4548
4571
  else {
4549
- // nothing to do, try to reorder: find a candidate old node to reuse
4550
- let oldMatch = undefined;
4551
- if (idIndex && isPlainObject(newValue)) {
4552
- // for an identified element type a plain-object snapshot can only match
4553
- // by id (it is never a live node, and snapshot-reference equality still
4554
- // implies the same id), so a miss means "nothing to reuse" without
4555
- // scanning. areSame still verifies the single candidate.
4556
- const candidate = idIndex.byId.get(normalizeIdentifier(newValue[idIndex.attr]));
4557
- const j = candidate ? oldNodes.indexOf(candidate, i) : -1;
4558
- if (j >= i && areSame(oldNodes[j], newValue)) {
4559
- oldMatch = oldNodes.splice(j, 1)[0];
4572
+ return scanForSame(this.oldNodes, this.claimed, next, value);
4573
+ }
4574
+ return j >= 0 && !this.claimed[j] && areSame(this.oldNodes[j], value)
4575
+ ? j
4576
+ : -1;
4577
+ }
4578
+ // last one wins on a duplicate id, even when it is already claimed
4579
+ idIndex(next) {
4580
+ if (!this.byId) {
4581
+ this.byId = new Map();
4582
+ for (let j = next; j < this.oldNodes.length; j++) {
4583
+ const node = this.oldNodes[j];
4584
+ if (node instanceof ObjectNode && node.identifier !== null) {
4585
+ this.byId.set(node.identifier, j);
4560
4586
  }
4561
4587
  }
4562
- else {
4563
- // find a possible candidate to reuse
4564
- for (let j = i; j < oldNodes.length; j++) {
4565
- if (areSame(oldNodes[j], newValue)) {
4566
- oldMatch = oldNodes.splice(j, 1)[0];
4567
- break;
4568
- }
4588
+ }
4589
+ return this.byId;
4590
+ }
4591
+ nodeIndex(next) {
4592
+ if (!this.byNode) {
4593
+ this.byNode = new Map();
4594
+ for (let j = next; j < this.oldNodes.length; j++) {
4595
+ this.byNode.set(this.oldNodes[j], j);
4596
+ }
4597
+ }
4598
+ return this.byNode;
4599
+ }
4600
+ hasIdentifiedNode(next) {
4601
+ if (this.anyIdentified === undefined) {
4602
+ this.anyIdentified = this.oldNodes
4603
+ .slice(next)
4604
+ .some(node => node instanceof ObjectNode &&
4605
+ node.identifier !== null &&
4606
+ !!node.identifierAttribute);
4607
+ }
4608
+ return this.anyIdentified;
4609
+ }
4610
+ firstWithSnapshot(value, next) {
4611
+ if (!this.bySnapshot) {
4612
+ const first = new Map();
4613
+ const nextSame = new Int32Array(this.oldNodes.length);
4614
+ for (let j = this.oldNodes.length - 1; j >= next; j--) {
4615
+ const node = this.oldNodes[j];
4616
+ if (!this.claimed[j] && node.isAlive) {
4617
+ const snapshot = node.snapshot;
4618
+ nextSame[j] = first.get(snapshot) ?? -1;
4619
+ first.set(snapshot, j);
4569
4620
  }
4570
4621
  }
4571
- nothingChanged = false;
4572
- const newNode = valueAsNode(childType, parent, newPath, newValue, oldMatch);
4573
- oldNodes.splice(i, 0, newNode);
4622
+ this.bySnapshot = { first, nextSame };
4623
+ }
4624
+ const { first, nextSame } = this.bySnapshot;
4625
+ const head = first.get(value);
4626
+ if (head === undefined) {
4627
+ return -1;
4628
+ }
4629
+ let j = head;
4630
+ while (j >= 0 && (this.claimed[j] || !this.oldNodes[j].isAlive)) {
4631
+ j = nextSame[j];
4574
4632
  }
4633
+ if (j !== head) {
4634
+ first.set(value, j);
4635
+ }
4636
+ return j;
4575
4637
  }
4576
- return nothingChanged ? null : oldNodes;
4577
4638
  }
4578
4639
  /**
4579
4640
  * Convert a value to a node at given parent and subpath. Attempts to reuse old node if possible and given.
@@ -4642,14 +4703,20 @@ function areSame(oldNode, newValue) {
4642
4703
  oldNodeType.isMatchingSnapshotId(oldNode, newValue));
4643
4704
  }
4644
4705
  /**
4645
- * Returns if a given value represents an array type.
4646
- *
4647
- * @param type
4648
- * @returns `true` if the type is an array type.
4706
+ * Returns if a type is an array type, or wraps or unions one. For the array
4707
+ * type itself, use {@link asArrayType}.
4649
4708
  */
4650
4709
  function isArrayType(type) {
4651
4710
  return isType(type) && (type.flags & TypeFlags.Array) > 0;
4652
4711
  }
4712
+ /**
4713
+ * The array type `type` builds its values with, seeing through the wrappers
4714
+ * {@link unwrapType} does, or `undefined` if that is not an array type.
4715
+ */
4716
+ function asArrayType(type) {
4717
+ const unwrapped = unwrapType(type);
4718
+ return unwrapped instanceof ArrayType ? unwrapped : undefined;
4719
+ }
4653
4720
 
4654
4721
  const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot";
4655
4722
  const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
@@ -4684,6 +4751,12 @@ function getPropObservables(storedValue) {
4684
4751
  function objectTypeToString() {
4685
4752
  return getStateTreeNode(this).toString();
4686
4753
  }
4754
+ /** `second` applied to `first`'s result, allocating only when both exist */
4755
+ function chainProcessors(first, second) {
4756
+ return first && second
4757
+ ? snapshot => second(first(snapshot))
4758
+ : (first ?? second);
4759
+ }
4687
4760
  const ANONYMOUS_MODEL_NAME = "AnonymousModel";
4688
4761
  /**
4689
4762
  * A plain loop rather than a callback per property, which allocated a closure
@@ -4972,8 +5045,6 @@ class ModelType extends ComplexType {
4972
5045
  ? initialValue
4973
5046
  : this.applySnapshotPreProcessor(initialValue);
4974
5047
  return createObjectNode(this, parent, subpath, environment, value);
4975
- // Optimization: record all prop- view- and action names after first construction, and generate an optimal base class
4976
- // that pre-reserves all these fields for fast object-member lookups
4977
5048
  }
4978
5049
  initializeChildNodes(objNode, initialSnapshot = {}) {
4979
5050
  const type = objNode.type;
@@ -5137,15 +5208,12 @@ class ModelType extends ComplexType {
5137
5208
  }
5138
5209
  }
5139
5210
  applySnapshotPreProcessor(snapshot) {
5140
- const processor = this.preProcessor;
5141
- return processor ? processor.call(null, snapshot) : snapshot;
5211
+ const preProcessor = this.preProcessor;
5212
+ return preProcessor ? preProcessor(snapshot) : snapshot;
5142
5213
  }
5143
5214
  applySnapshotPostProcessor(snapshot) {
5144
5215
  const postProcessor = this.postProcessor;
5145
- if (postProcessor) {
5146
- return postProcessor.call(null, snapshot);
5147
- }
5148
- return snapshot;
5216
+ return postProcessor ? postProcessor(snapshot) : snapshot;
5149
5217
  }
5150
5218
  getChildType(propertyName) {
5151
5219
  assertIsString(propertyName, 1);
@@ -5168,7 +5236,6 @@ class ModelType extends ComplexType {
5168
5236
  return typeCheckSuccess();
5169
5237
  }
5170
5238
  describe() {
5171
- // optimization: cache
5172
5239
  return `{ ${this.propertyNames
5173
5240
  .map(key => `${key}: ${this.properties[key].describe()}`)
5174
5241
  .join("; ")} }`;
@@ -5190,27 +5257,17 @@ Object.assign(ModelType.prototype, {
5190
5257
  props(properties) {
5191
5258
  return this.cloneAndEnhance({ properties });
5192
5259
  },
5260
+ // a newly added pre-processor sees the raw snapshot first; a newly added
5261
+ // post-processor sees what the existing one produced
5193
5262
  preProcessSnapshot(preProcessor) {
5194
- const currentPreprocessor = this.preProcessor;
5195
- if (!currentPreprocessor) {
5196
- return this.cloneAndEnhance({ preProcessor });
5197
- }
5198
- else {
5199
- return this.cloneAndEnhance({
5200
- preProcessor: snapshot => currentPreprocessor(preProcessor(snapshot))
5201
- });
5202
- }
5263
+ return this.cloneAndEnhance({
5264
+ preProcessor: chainProcessors(preProcessor, this.preProcessor)
5265
+ });
5203
5266
  },
5204
5267
  postProcessSnapshot(postProcessor) {
5205
- const currentPostprocessor = this.postProcessor;
5206
- if (!currentPostprocessor) {
5207
- return this.cloneAndEnhance({ postProcessor });
5208
- }
5209
- else {
5210
- return this.cloneAndEnhance({
5211
- postProcessor: snapshot => postProcessor(currentPostprocessor(snapshot))
5212
- });
5213
- }
5268
+ return this.cloneAndEnhance({
5269
+ postProcessor: chainProcessors(this.postProcessor, postProcessor)
5270
+ });
5214
5271
  }
5215
5272
  });
5216
5273
  /**
@@ -5234,39 +5291,44 @@ function model(...args) {
5234
5291
  * the types are composed into a new Type with the given name
5235
5292
  */
5236
5293
  function compose(...args) {
5237
- // TODO: just join the base type names if no name is provided
5238
5294
  const hasTypename = typeof args[0] === "string";
5239
5295
  const typeName = hasTypename ? args[0] : ANONYMOUS_MODEL_NAME;
5240
5296
  if (hasTypename) {
5241
5297
  args.shift();
5242
5298
  }
5243
- // check all parameters
5299
+ // exactly a model type: each part's own properties and initializers are
5300
+ // read below, and a wrapper around a model has neither
5244
5301
  if (devMode()) {
5245
5302
  args.forEach((type, i) => {
5246
- assertArg(type, isModelType, "mobx-state-tree model type", hasTypename ? i + 2 : i + 1);
5303
+ assertArg(type, t => t instanceof ModelType, "mobx-state-tree model type", hasTypename ? i + 2 : i + 1);
5247
5304
  });
5248
5305
  }
5249
5306
  return args
5250
5307
  .reduce((prev, cur) => prev.cloneAndEnhance({
5251
- name: `${prev.name}_${cur.name}`,
5252
5308
  properties: cur.properties,
5253
5309
  // cur.properties is another ModelType's already-converted+frozen bag
5254
5310
  propertiesAreConverted: true,
5255
5311
  initializers: cur.initializers,
5256
- preProcessor: (snapshot) => cur.applySnapshotPreProcessor(prev.applySnapshotPreProcessor(snapshot)),
5257
- postProcessor: (snapshot) => cur.applySnapshotPostProcessor(prev.applySnapshotPostProcessor(snapshot))
5312
+ preProcessor: chainProcessors(prev.preProcessor, cur.preProcessor),
5313
+ postProcessor: chainProcessors(prev.postProcessor, cur.postProcessor)
5258
5314
  }))
5259
5315
  .named(typeName);
5260
5316
  }
5261
5317
  /**
5262
- * Returns if a given value represents a model type.
5263
- *
5264
- * @param type
5265
- * @returns
5318
+ * Returns if a type is a model type, or wraps or unions one. For the model
5319
+ * type itself, use {@link asModelType}.
5266
5320
  */
5267
5321
  function isModelType(type) {
5268
5322
  return isType(type) && (type.flags & TypeFlags.Object) > 0;
5269
5323
  }
5324
+ /**
5325
+ * The model type `type` builds its values with, seeing through the wrappers
5326
+ * {@link unwrapType} does, or `undefined` if that is not a model type.
5327
+ */
5328
+ function asModelType(type) {
5329
+ const unwrapped = unwrapType(type);
5330
+ return unwrapped instanceof ModelType ? unwrapped : undefined;
5331
+ }
5270
5332
  /**
5271
5333
  * `extendInstance` - attaches additional actions, views and volatile state to an
5272
5334
  * already-created model instance, using the same instantiation machinery as
@@ -5292,7 +5354,7 @@ function extendInstance(instance, fn) {
5292
5354
  throw fail("extendInstance expects a mobx-state-tree node");
5293
5355
  }
5294
5356
  const type = getStateTreeNode(instance).type;
5295
- if (!isModelType(type)) {
5357
+ if (!(type instanceof ModelType)) {
5296
5358
  throw fail("extendInstance can only be used on model instances");
5297
5359
  }
5298
5360
  // Views/actions/volatile are installed via defineProperty + makeObservable — the
@@ -5300,9 +5362,8 @@ function extendInstance(instance, fn) {
5300
5362
  // write-protection interceptor is attached (see finalizeNewInstance). On a live
5301
5363
  // instance that interceptor is already active, so run the attach inside an action
5302
5364
  // context: isRunningAction() then short-circuits assertWritable.
5303
- const modelType = type;
5304
5365
  const attach = createActionInvoker(instance, "@@extendInstance", (() => {
5305
- modelType.applyExtensionToInstance(instance, fn(instance));
5366
+ type.applyExtensionToInstance(instance, fn(instance));
5306
5367
  }));
5307
5368
  attach();
5308
5369
  return instance;
@@ -5374,7 +5435,7 @@ const number = new CoreType("number", TypeFlags.Number, v => typeof v === "numbe
5374
5435
  * })
5375
5436
  * ```
5376
5437
  */
5377
- const integer = new CoreType("integer", TypeFlags.Integer, v => isInteger(v));
5438
+ const integer = new CoreType("integer", TypeFlags.Integer, Number.isInteger);
5378
5439
  /**
5379
5440
  * `types.float` - Creates a type that can only contain an float value.
5380
5441
  *
@@ -5386,7 +5447,7 @@ const integer = new CoreType("integer", TypeFlags.Integer, v => isInteger(v));
5386
5447
  * })
5387
5448
  * ```
5388
5449
  */
5389
- const float = new CoreType("float", TypeFlags.Float, v => isFloat(v));
5450
+ const float = new CoreType("float", TypeFlags.Float, v => Number(v) === v && v % 1 !== 0);
5390
5451
  /**
5391
5452
  * `types.finite` - Creates a type that can only contain an finite value.
5392
5453
  *
@@ -5398,7 +5459,7 @@ const float = new CoreType("float", TypeFlags.Float, v => isFloat(v));
5398
5459
  * })
5399
5460
  * ```
5400
5461
  */
5401
- const finite = new CoreType("finite", TypeFlags.Finite, v => isFinite(v));
5462
+ const finite = new CoreType("finite", TypeFlags.Finite, Number.isFinite);
5402
5463
  /**
5403
5464
  * `types.boolean` - Creates a type that can only contain a boolean value.
5404
5465
  * This type is used for boolean values by default
@@ -5459,11 +5520,9 @@ function getPrimitiveFactoryFromValue(value) {
5459
5520
  /**
5460
5521
  * Returns if a given value represents a primitive type.
5461
5522
  *
5462
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
5463
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
5464
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
5465
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
5466
- * `isMapType`, `isModelType`) keep their predicate.
5523
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
5524
+ * unions inherit from what they hold, so it is also true for a type that wraps
5525
+ * or unions one. Use {@link unwrapType} to get at the type itself.
5467
5526
  *
5468
5527
  * @param type
5469
5528
  * @returns
@@ -5529,11 +5588,9 @@ function literal(value) {
5529
5588
  /**
5530
5589
  * Returns if a given value represents a literal type.
5531
5590
  *
5532
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
5533
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
5534
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
5535
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
5536
- * `isMapType`, `isModelType`) keep their predicate.
5591
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
5592
+ * unions inherit from what they hold, so it is also true for a type that wraps
5593
+ * or unions one. Use {@link unwrapType} to get at the type itself.
5537
5594
  *
5538
5595
  * @param type
5539
5596
  * @returns
@@ -5611,11 +5668,9 @@ function refinement(...args) {
5611
5668
  /**
5612
5669
  * Returns if a given value is a refinement type.
5613
5670
  *
5614
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
5615
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
5616
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
5617
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
5618
- * `isMapType`, `isModelType`) keep their predicate.
5671
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
5672
+ * unions inherit from what they hold, so it is also true for a type that wraps
5673
+ * or unions one. Use {@link unwrapType} to get at the type itself.
5619
5674
  *
5620
5675
  * @param type
5621
5676
  * @returns
@@ -5628,6 +5683,10 @@ function isRefinementType(type) {
5628
5683
  * `types.enumeration` - Can be used to create an string based enumeration.
5629
5684
  * (note: this methods is just sugar for a union of string literals)
5630
5685
  *
5686
+ * The member type is inferred from the options, so a literal array, an
5687
+ * `as const` array and `Object.values(SomeStringEnum)` all produce the exact
5688
+ * union; only an array already typed `string[]` falls back to `string`.
5689
+ *
5631
5690
  * Example:
5632
5691
  * ```ts
5633
5692
  * const TrafficLight = types.model({
@@ -5639,52 +5698,47 @@ function isRefinementType(type) {
5639
5698
  * @param options possible values this enumeration can have
5640
5699
  * @returns
5641
5700
  */
5642
- function enumeration(name, options) {
5643
- const realOptions = typeof name === "string" ? options : name;
5644
- // check all options
5701
+ function enumeration(nameOrOptions, maybeOptions) {
5702
+ const name = typeof nameOrOptions === "string" ? nameOrOptions : undefined;
5703
+ const options = typeof nameOrOptions === "string" ? maybeOptions : nameOrOptions;
5645
5704
  if (devMode()) {
5646
- realOptions.forEach((option, i) => {
5705
+ options.forEach((option, i) => {
5647
5706
  assertIsString(option, i + 1);
5648
5707
  });
5649
5708
  }
5650
- const type = union(...realOptions.map(option => literal(`${option}`)));
5651
- if (typeof name === "string") {
5652
- type.name = name;
5653
- }
5654
- return type;
5709
+ // built directly rather than through union(): its members are fresh
5710
+ // literals, so interning could never share it, and the name belongs to this
5711
+ // union alone
5712
+ return new Union(options.map(option => literal(`${option}`)), name === undefined ? undefined : { name });
5655
5713
  }
5656
5714
 
5657
- // Drill through single-subtype wrappers — optional(), refinement(),
5658
- // snapshotProcessor(), late() — to the underlying ModelType. Discriminated-
5659
- // union scoping keys on a member's literal `type` property, but real-world
5660
- // members are rarely bare models (jbrowse config schemas, for instance, are
5661
- // always optional(model) or optional(snapshotProcessor(model))). Without this
5662
- // the scoping never engages and every failure prints every member's full
5663
- // structure. Wrappers expose their child as `_subtype` (optional/refinement/
5664
- // snapshotProcessor) or via `getSubType()` (late); bounded to avoid cycles.
5665
- // Only *successful* resolutions are cached. A wrapper chain's shape is fixed at
5666
- // construction, so once a member resolves to a ModelType it always will; but a
5667
- // `late` member reports no subtype until its definition evaluates, and that
5668
- // miss must stay retryable.
5669
- const resolvedModelTypes = new WeakMap();
5715
+ // The model a member is meant for: the one under its wrappers, and for a
5716
+ // `resilient` member the one it tries first. Matching a snapshot to a member
5717
+ // (quick-match dispatch, discriminator scoping) keys on that model's props,
5718
+ // and real-world members are rarely bare models (jbrowse config schemas are
5719
+ // always optional(model) or optional(snapshotProcessor(model))).
5720
+ // Misses are cached too, unless the member holds a `late` type: that has no
5721
+ // subtype until its definition evaluates, so its miss must stay retryable.
5722
+ const intendedModelTypes = new WeakMap();
5670
5723
  function resolveModelType(type) {
5671
- if (!type) {
5672
- return undefined;
5724
+ const cached = intendedModelTypes.get(type);
5725
+ if (cached !== undefined) {
5726
+ return cached ?? undefined;
5673
5727
  }
5674
- const cached = resolvedModelTypes.get(type);
5675
- if (cached) {
5676
- return cached;
5728
+ const model = findIntendedModelType(type);
5729
+ if (model || !(type.flags & TypeFlags.Late)) {
5730
+ intendedModelTypes.set(type, model ?? null);
5677
5731
  }
5678
- let current = type;
5679
- for (let depth = 0; current && depth < 20; depth++) {
5680
- if (current instanceof ModelType) {
5681
- resolvedModelTypes.set(type, current);
5682
- return current;
5683
- }
5684
- const wrapper = current;
5685
- current = wrapper._subtype ?? wrapper.getSubType?.(false);
5732
+ return model;
5733
+ }
5734
+ function findIntendedModelType(type) {
5735
+ const unwrapped = unwrapType(type);
5736
+ if (unwrapped instanceof ModelType) {
5737
+ return unwrapped;
5686
5738
  }
5687
- return undefined;
5739
+ return unwrapped instanceof Resilient
5740
+ ? findIntendedModelType(unwrapped.primaryType)
5741
+ : undefined;
5688
5742
  }
5689
5743
  // The quick-match paths below already know a type carries TypeFlags.Literal,
5690
5744
  // but `is()` still routes through BaseType.validate — a context array, an entry
@@ -5722,13 +5776,17 @@ class Union extends BaseType {
5722
5776
  if (cached !== undefined) {
5723
5777
  return cached;
5724
5778
  }
5779
+ const result = this.foldFlags();
5780
+ if (!(result & TypeFlags.Late)) {
5781
+ this._flags = result;
5782
+ }
5783
+ return result;
5784
+ }
5785
+ foldFlags() {
5725
5786
  let result = TypeFlags.Union;
5726
5787
  for (const type of this.members()) {
5727
5788
  result |= type.flags;
5728
5789
  }
5729
- if (!(result & TypeFlags.Late)) {
5730
- this._flags = result;
5731
- }
5732
5790
  return result;
5733
5791
  }
5734
5792
  computeName() {
@@ -6063,11 +6121,7 @@ class DynamicUnion extends Union {
6063
6121
  return this._members();
6064
6122
  }
6065
6123
  get flags() {
6066
- let result = TypeFlags.Union;
6067
- for (const type of this.members()) {
6068
- result |= type.flags;
6069
- }
6070
- return result;
6124
+ return this.foldFlags();
6071
6125
  }
6072
6126
  get name() {
6073
6127
  return this._explicitName !== undefined
@@ -6146,11 +6200,9 @@ function union(...args) {
6146
6200
  /**
6147
6201
  * Returns if a given value represents a union type.
6148
6202
  *
6149
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6150
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6151
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6152
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
6153
- * `isMapType`, `isModelType`) keep their predicate.
6203
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
6204
+ * unions inherit from what they hold, so it is also true for a type that wraps
6205
+ * or unions one. Use {@link unwrapType} to get at the type itself.
6154
6206
  *
6155
6207
  * @param type
6156
6208
  * @returns
@@ -6159,13 +6211,9 @@ function isUnionType(type) {
6159
6211
  return isType(type) && (type.flags & TypeFlags.Union) > 0;
6160
6212
  }
6161
6213
  /**
6162
- * Returns the member types of a union.
6163
- *
6164
- * Wrapper types (`optional`, `refinement`, `late`) inherit the union flag from
6165
- * the type they wrap, so `isUnionType` is true for e.g. an optional-of-union,
6166
- * but their `getSubTypes()` reports the single wrapped type rather than the
6167
- * union's members. This drills through those wrappers until the union's member
6168
- * array surfaces.
6214
+ * Returns the member types of a union, seeing through the wrappers
6215
+ * {@link unwrapType} does: `isUnionType` is also true for e.g. an
6216
+ * optional-of-union.
6169
6217
  *
6170
6218
  * @param type a type for which `isUnionType` is true
6171
6219
  * @returns the array of member types of the underlying union
@@ -6174,16 +6222,11 @@ function getUnionSubtypes(type) {
6174
6222
  if (!isUnionType(type)) {
6175
6223
  throw fail("expected a union type");
6176
6224
  }
6177
- let subtypes = type.getSubTypes();
6178
- while (typeof subtypes === "object" &&
6179
- subtypes !== null &&
6180
- !Array.isArray(subtypes)) {
6181
- subtypes = subtypes.getSubTypes();
6182
- }
6183
- if (!Array.isArray(subtypes)) {
6225
+ const union = unwrapType(type);
6226
+ if (!(union instanceof Union)) {
6184
6227
  throw fail("could not extract subtypes from union type");
6185
6228
  }
6186
- return subtypes;
6229
+ return union.getSubTypes();
6187
6230
  }
6188
6231
 
6189
6232
  /**
@@ -6330,11 +6373,9 @@ const undefinedAsOptionalValues = [undefined];
6330
6373
  /**
6331
6374
  * Returns if a value represents an optional type.
6332
6375
  *
6333
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6334
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6335
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6336
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
6337
- * `isMapType`, `isModelType`) keep their predicate.
6376
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
6377
+ * unions inherit from what they hold, so it is also true for a type that wraps
6378
+ * or unions one. Use {@link unwrapType} to get at the type itself.
6338
6379
  *
6339
6380
  * @param type
6340
6381
  * @returns
@@ -6345,26 +6386,6 @@ function isOptionalType(type) {
6345
6386
  function isRecord(value) {
6346
6387
  return typeof value === "object" && value !== null;
6347
6388
  }
6348
- /**
6349
- * The identifier attribute of the model a stripDefault ultimately wraps, or
6350
- * `null` when there is none. Drills through the single-subtype wrappers the way
6351
- * `resolveModelType` in union.ts does — that one is module-private there, and
6352
- * this file may not reach into it.
6353
- *
6354
- * `null` is also the answer for an unresolved `types.late`, which only costs the
6355
- * short-circuit, never correctness: the full structural walk still runs.
6356
- */
6357
- function resolveIdentifierAttribute(type) {
6358
- let current = type;
6359
- for (let depth = 0; current && depth < 20; depth++) {
6360
- if (current instanceof ModelType) {
6361
- return current.identifierAttribute ? current.identifierAttribute : null;
6362
- }
6363
- const wrapper = current;
6364
- current = wrapper._subtype ?? wrapper.getSubType?.(false);
6365
- }
6366
- return null;
6367
- }
6368
6389
  /**
6369
6390
  * Compare a child snapshot to a stripped-default's reference snapshot: identity
6370
6391
  * for primitives, structural for objects/arrays.
@@ -6446,7 +6467,10 @@ class StripDefaultValue extends OptionalValue {
6446
6467
  equalsDefault(snapshot, defaultSnapshot) {
6447
6468
  let identifierAttribute = this._identifierAttribute;
6448
6469
  if (identifierAttribute === undefined) {
6449
- identifierAttribute = resolveIdentifierAttribute(this.getSubTypes());
6470
+ // null when not wrapping an identified model; an unresolved late member
6471
+ // also lands here, which only costs the short-circuit below
6472
+ identifierAttribute =
6473
+ asModelType(this.getSubTypes())?.identifierAttribute ?? null;
6450
6474
  this._identifierAttribute = identifierAttribute;
6451
6475
  }
6452
6476
  // an identified model's snapshot normally has the same shape as the default
@@ -6556,16 +6580,14 @@ class Late extends BaseType {
6556
6580
  }
6557
6581
  getSubType(mustSucceed) {
6558
6582
  if (!this._subType) {
6559
- let t = undefined;
6583
+ let t;
6560
6584
  try {
6561
6585
  t = this._definition();
6562
6586
  }
6563
6587
  catch (e) {
6564
- if (e instanceof ReferenceError) // can happen in strict ES5 code when a definition is self refering
6565
- {
6566
- t = undefined;
6567
- }
6568
- else {
6588
+ // a self-referencing definition can read its binding before it is
6589
+ // initialized; that is "not defined yet", like returning undefined
6590
+ if (!(e instanceof ReferenceError)) {
6569
6591
  throw e;
6570
6592
  }
6571
6593
  }
@@ -6581,10 +6603,15 @@ class Late extends BaseType {
6581
6603
  }
6582
6604
  return this._subType;
6583
6605
  }
6584
- constructor(name, _definition) {
6606
+ constructor(_definition, name) {
6585
6607
  super(name);
6586
6608
  this._definition = _definition;
6587
6609
  }
6610
+ // the unnamed form is named after the definition's source text, which is
6611
+ // only worth building if something reads it
6612
+ computeName() {
6613
+ return `late(${this._definition.toString()})`;
6614
+ }
6588
6615
  instantiate(parent, subpath, environment, initialValue) {
6589
6616
  return this.getSubType(true).instantiate(parent, subpath, environment, initialValue);
6590
6617
  }
@@ -6593,7 +6620,7 @@ class Late extends BaseType {
6593
6620
  }
6594
6621
  describe() {
6595
6622
  const t = this.getSubType(false);
6596
- return t ? t.name : "<uknown late type>";
6623
+ return t ? t.name : "<unknown late type>";
6597
6624
  }
6598
6625
  isValidSnapshot(value, context) {
6599
6626
  const t = this.getSubType(false);
@@ -6608,8 +6635,7 @@ class Late extends BaseType {
6608
6635
  return t ? t.isAssignableFrom(type) : false;
6609
6636
  }
6610
6637
  getSubTypes() {
6611
- const subtype = this.getSubType(false);
6612
- return subtype ? subtype : cannotDetermineSubtype;
6638
+ return this.getSubType(false) ?? cannotDetermineSubtype;
6613
6639
  }
6614
6640
  }
6615
6641
  /**
@@ -6629,26 +6655,21 @@ class Late extends BaseType {
6629
6655
  * @returns
6630
6656
  */
6631
6657
  function late(nameOrType, maybeType) {
6632
- const name = typeof nameOrType === "string"
6633
- ? nameOrType
6634
- : `late(${nameOrType.toString()})`;
6658
+ const name = typeof nameOrType === "string" ? nameOrType : undefined;
6635
6659
  const type = typeof nameOrType === "string" ? maybeType : nameOrType;
6636
- // checks that the type is actually a late type
6637
6660
  if (devMode()) {
6638
6661
  if (!(typeof type === "function" && type.length === 0)) {
6639
6662
  throw fail(`Invalid late type, expected a function with zero arguments that returns a type, got: ${type}`);
6640
6663
  }
6641
6664
  }
6642
- return new Late(name, type);
6665
+ return new Late(type, name);
6643
6666
  }
6644
6667
  /**
6645
6668
  * Returns if a given value represents a late type.
6646
6669
  *
6647
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6648
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6649
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6650
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
6651
- * `isMapType`, `isModelType`) keep their predicate.
6670
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
6671
+ * unions inherit from what they hold, so it is also true for a type that wraps
6672
+ * or unions one. Use {@link unwrapType} to get at the type itself.
6652
6673
  *
6653
6674
  * @param type
6654
6675
  * @returns
@@ -6658,7 +6679,8 @@ function isLateType(type) {
6658
6679
  }
6659
6680
 
6660
6681
  function lazy(name, options) {
6661
- // TODO: fix this unknown casting to be stricter
6682
+ // a lazy type stands in for the type it loads, down to that type's own
6683
+ // methods (a model's `.props`, say), which the placeholder does not have
6662
6684
  return new Lazy(name, options);
6663
6685
  }
6664
6686
  /**
@@ -6825,11 +6847,9 @@ function frozen(arg) {
6825
6847
  /**
6826
6848
  * Returns if a given value represents a frozen type.
6827
6849
  *
6828
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6829
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6830
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6831
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
6832
- * `isMapType`, `isModelType`) keep their predicate.
6850
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
6851
+ * unions inherit from what they hold, so it is also true for a type that wraps
6852
+ * or unions one. Use {@link unwrapType} to get at the type itself.
6833
6853
  *
6834
6854
  * @param type
6835
6855
  * @returns
@@ -6973,9 +6993,10 @@ class BaseReferenceType extends SimpleType {
6973
6993
  return undefined;
6974
6994
  }
6975
6995
  const refTargetNode = getStateTreeNode(refTargetValue);
6976
- const hookHandler = (_, refTargetNodeHook) => {
6996
+ const hookHandler = (subject, refTargetNodeHook) => {
6977
6997
  const cause = getInvalidationCause(refTargetNodeHook);
6978
- if (!cause) {
6998
+ // a reference detached along with its target still resolves it
6999
+ if (!cause || (cause === "detach" && storedRefNode.isWithin(subject))) {
6979
7000
  return;
6980
7001
  }
6981
7002
  this.fireInvalidated(cause, storedRefNode, referenceId, refTargetNode);
@@ -7173,11 +7194,9 @@ function reference(subType, options) {
7173
7194
  /**
7174
7195
  * Returns if a given value represents a reference type.
7175
7196
  *
7176
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
7177
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
7178
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
7179
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
7180
- * `isMapType`, `isModelType`) keep their predicate.
7197
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
7198
+ * unions inherit from what they hold, so it is also true for a type that wraps
7199
+ * or unions one. Use {@link unwrapType} to get at the type itself.
7181
7200
  *
7182
7201
  * @param type
7183
7202
  * @returns
@@ -7302,11 +7321,9 @@ const identifierNumber = new IdentifierNumberType();
7302
7321
  /**
7303
7322
  * Returns if a given value represents an identifier type.
7304
7323
  *
7305
- * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
7306
- * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
7307
- * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
7308
- * to compile. Guards with a distinct narrowing target (`isArrayType`,
7309
- * `isMapType`, `isModelType`) keep their predicate.
7324
+ * Like every `isXType` guard it reads the type's flags, which wrappers and
7325
+ * unions inherit from what they hold, so it is also true for a type that wraps
7326
+ * or unions one. Use {@link unwrapType} to get at the type itself.
7310
7327
  *
7311
7328
  * @param type
7312
7329
  * @returns
@@ -7437,6 +7454,10 @@ class CustomType extends SimpleType {
7437
7454
  }
7438
7455
  }
7439
7456
 
7457
+ /**
7458
+ * @internal
7459
+ * @hidden
7460
+ */
7440
7461
  class Resilient extends BaseType {
7441
7462
  _subtype;
7442
7463
  _fallbackType;
@@ -7444,6 +7465,10 @@ class Resilient extends BaseType {
7444
7465
  get flags() {
7445
7466
  return this._subtype.flags;
7446
7467
  }
7468
+ /** the type tried first; the fallback takes over only when it fails */
7469
+ get primaryType() {
7470
+ return this._subtype;
7471
+ }
7447
7472
  constructor(_subtype, _fallbackType, _createFallbackSnapshot) {
7448
7473
  super();
7449
7474
  this._subtype = _subtype;
@@ -7599,5 +7624,5 @@ const types = {
7599
7624
  resilient
7600
7625
  };
7601
7626
 
7602
- export { addDisposer, addMiddleware, applyAction, applyPatch, applySnapshot, cannotDetermineSubtype, cast, castFlowReturn, castToReferenceSnapshot, castToSnapshot, clone, createActionTrackingMiddleware, createActionTrackingMiddleware2, decorate, destroy, detach, escapeJsonPath, extendInstance, flow, getChildType, getEnv, getIdentifier, getLivelinessChecking, getMembers, getNodeId, getParent, getParentOfType, getPath, getPathParts, getPropertyMembers, getRelativePath, getRoot, getRunningActionContext, getSnapshot, getType, getUnionSubtypes, hasParent, hasParentOfType, isActionContextChildOf, isActionContextThisOrChildOf, isAlive, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isProtected, isReferenceType, isRefinementType, isRoot, isStateTreeNode, isType, isUnionType, isValidReference, joinJsonPath, onAction, onPatch, onSnapshot, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, setTypeChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, walk };
7627
+ export { addDisposer, addMiddleware, applyAction, applyPatch, applySnapshot, asArrayType, asMapType, asModelType, cannotDetermineSubtype, cast, castFlowReturn, castToReferenceSnapshot, castToSnapshot, clone, createActionTrackingMiddleware, createActionTrackingMiddleware2, decorate, destroy, detach, escapeJsonPath, extendInstance, flow, getChildType, getEnv, getIdentifier, getLivelinessChecking, getMembers, getNodeId, getParent, getParentOfType, getPath, getPathParts, getPropertyMembers, getRelativePath, getRoot, getRunningActionContext, getSnapshot, getType, getUnionSubtypes, getWrappedType, hasParent, hasParentOfType, isActionContextChildOf, isActionContextThisOrChildOf, isAlive, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isProtected, isReferenceType, isRefinementType, isRoot, isStateTreeNode, isType, isUnionType, isValidReference, joinJsonPath, onAction, onPatch, onSnapshot, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, setTypeChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, unwrapType, walk };
7603
7628
  //# sourceMappingURL=mobx-state-tree.mjs.map