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