@jbrowse/mobx-state-tree 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1165,7 +1165,7 @@ class ObjectNode extends BaseNode {
1165
1165
  }
1166
1166
  _autoUnbox = true; // unboxing is disabled when reading child nodes
1167
1167
  _isRunningAction = false; // only relevant for root
1168
- _hasSnapshotReaction = false;
1168
+ _snapshotReactionDisposer;
1169
1169
  _observableInstanceState = ObservableInstanceLifecycle.UNINITIALIZED;
1170
1170
  _childNodes;
1171
1171
  _initialSnapshot;
@@ -1587,7 +1587,17 @@ class ObjectNode extends BaseNode {
1587
1587
  }
1588
1588
  onSnapshot(onChange) {
1589
1589
  this._addSnapshotReaction();
1590
- return this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1590
+ const unregister = this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1591
+ return () => {
1592
+ unregister();
1593
+ // The reaction re-serializes the whole subtree on every change, so leaving
1594
+ // it running once the last listener is gone would keep charging the node
1595
+ // for a snapshot nobody receives. Referential stability of getSnapshot()
1596
+ // does not depend on it — that comes from keepAlive on _snapshotComputed.
1597
+ if (!this._internalEventsHasSubscribers(InternalEvents.Snapshot)) {
1598
+ this._removeSnapshotReaction();
1599
+ }
1600
+ };
1591
1601
  }
1592
1602
  emitSnapshot(snapshot) {
1593
1603
  this._internalEventsEmit(InternalEvents.Snapshot, snapshot);
@@ -1652,11 +1662,23 @@ class ObjectNode extends BaseNode {
1652
1662
  this.type.applyPatchLocally(this, subpath, patch);
1653
1663
  }
1654
1664
  _addSnapshotReaction() {
1655
- if (!this._hasSnapshotReaction) {
1665
+ if (!this._snapshotReactionDisposer) {
1656
1666
  const snapshotDisposer = reaction(() => this.snapshot, snapshot => this.emitSnapshot(snapshot), snapshotReactionOptions);
1657
1667
  this.addDisposer(snapshotDisposer);
1658
- this._hasSnapshotReaction = true;
1668
+ this._snapshotReactionDisposer = snapshotDisposer;
1669
+ }
1670
+ }
1671
+ _removeSnapshotReaction() {
1672
+ const disposer = this._snapshotReactionDisposer;
1673
+ if (!disposer) {
1674
+ return;
1659
1675
  }
1676
+ this._snapshotReactionDisposer = undefined;
1677
+ // not removeDisposer(), which throws when the registration is already gone —
1678
+ // the last listener can be disposed from within a disposer, i.e. after
1679
+ // aboutToDie has cleared them
1680
+ this._internalEventsUnregister(InternalEvents.Dispose, disposer);
1681
+ disposer();
1660
1682
  }
1661
1683
  // #region internal event handling
1662
1684
  _internalEvents;
@@ -1733,8 +1755,6 @@ var TypeFlags;
1733
1755
  * @hidden
1734
1756
  */
1735
1757
  const cannotDetermineSubtype = "cannotDetermine";
1736
- /** @hidden */
1737
- const $type = Symbol("$type");
1738
1758
  /**
1739
1759
  * A base type produces a MST node (Node in the state tree)
1740
1760
  *
@@ -1742,16 +1762,39 @@ const $type = Symbol("$type");
1742
1762
  * @hidden
1743
1763
  */
1744
1764
  class BaseType {
1745
- [$type];
1746
- // these are just to make inner types avaialable to inherited classes
1747
- C;
1748
- S;
1749
- T;
1750
- N;
1751
- isType = true;
1752
- name;
1765
+ /**
1766
+ * Builds the name of a type that does not get one handed to it. Composite
1767
+ * types (`union`, `array`, `map`, `reference`, ...) override this to fold
1768
+ * their members' names; see {@link name}. It runs after construction, so
1769
+ * unlike a `super(...)` argument it can read the subclass's own fields.
1770
+ */
1771
+ computeName() {
1772
+ // istanbul ignore next
1773
+ throw fail(`${this.constructor.name} has neither a name nor a computeName`);
1774
+ }
1775
+ /**
1776
+ * Friendly type name.
1777
+ *
1778
+ * A composite type folds its members' names, which for a union is a map +
1779
+ * join over every member — and the result is read only by error messages and
1780
+ * `describe()`. So those types leave it unset and it is built on first read
1781
+ * and cached: a union that never fails a typecheck never builds one, and
1782
+ * jbrowse builds a union per config slot, tens of thousands per session load.
1783
+ *
1784
+ * Deliberately not a lambda passed to the constructor: that allocated a
1785
+ * closure per type (the very cost being avoided) and made two separately
1786
+ * built but equivalent types compare unequal, since the closures differ.
1787
+ */
1788
+ get name() {
1789
+ return (this._name ??= this.computeName());
1790
+ }
1791
+ set name(value) {
1792
+ this._name = value;
1793
+ }
1753
1794
  constructor(name) {
1754
- this.name = name;
1795
+ if (name !== undefined) {
1796
+ this._name = name;
1797
+ }
1755
1798
  }
1756
1799
  create(snapshot, environment) {
1757
1800
  typecheckInternal(this, snapshot);
@@ -1796,6 +1839,9 @@ class BaseType {
1796
1839
  }
1797
1840
  }
1798
1841
  BaseType.prototype.create = action(BaseType.prototype.create);
1842
+ // Defaults that every type shares, kept off the individual type objects. See
1843
+ // `isType` and `_name` in the class body.
1844
+ Object.assign(BaseType.prototype, { isType: true, _name: undefined });
1799
1845
  /**
1800
1846
  * A complex type produces a MST node (Node in the state tree)
1801
1847
  *
@@ -2121,7 +2167,7 @@ function createActionTrackingMiddleware2(middlewareHooks) {
2121
2167
  };
2122
2168
  }
2123
2169
 
2124
- function serializeArgument(node, actionName, index, arg) {
2170
+ function serializeArgument(arg) {
2125
2171
  if (arg instanceof Date) {
2126
2172
  return { $MST_DATE: arg.getTime() };
2127
2173
  }
@@ -2149,7 +2195,7 @@ function serializeArgument(node, actionName, index, arg) {
2149
2195
  return serializeTheUnserializable(`${e}`);
2150
2196
  }
2151
2197
  }
2152
- function deserializeArgument(adm, value) {
2198
+ function deserializeArgument(value) {
2153
2199
  if (value && typeof value === "object" && "$MST_DATE" in value) {
2154
2200
  return new Date(value["$MST_DATE"]);
2155
2201
  }
@@ -2193,7 +2239,7 @@ function baseApplyAction(target, action) {
2193
2239
  if (!(typeof resolvedTarget[action.name] === "function")) {
2194
2240
  throw fail(`Action '${action.name}' does not exist in '${node.path}'`);
2195
2241
  }
2196
- return resolvedTarget[action.name](...(action.args ? action.args.map(v => deserializeArgument(node, v)) : []));
2242
+ return resolvedTarget[action.name](...(action.args ? action.args.map(deserializeArgument) : []));
2197
2243
  }
2198
2244
  /**
2199
2245
  * Small abstraction around `onAction` and `applyAction`, attaches an action listener to a tree and records all the actions emitted.
@@ -2310,7 +2356,7 @@ function onAction(target, listener, attachAfter = false) {
2310
2356
  const info = {
2311
2357
  name: rawCall.name,
2312
2358
  path: getRelativePathBetweenNodes(getStateTreeNode(target), sourceNode),
2313
- args: rawCall.args.map((arg, index) => serializeArgument(sourceNode, rawCall.name, index, arg))
2359
+ args: rawCall.args.map(serializeArgument)
2314
2360
  };
2315
2361
  if (attachAfter) {
2316
2362
  const res = next(rawCall);
@@ -2778,10 +2824,13 @@ class IdentifierCache {
2778
2824
  addNodeToCache(node, lastCacheUpdate = true) {
2779
2825
  if (node.identifierAttribute) {
2780
2826
  const identifier = node.identifier;
2781
- if (!this.cache.has(identifier)) {
2782
- this.cache.set(identifier, observable.array([], mobxShallow));
2827
+ // one observable-map read, not a has() plus a get(): this runs for every
2828
+ // identified node created
2829
+ let set = this.cache.get(identifier);
2830
+ if (!set) {
2831
+ set = observable.array([], mobxShallow);
2832
+ this.cache.set(identifier, set);
2783
2833
  }
2784
- const set = this.cache.get(identifier);
2785
2834
  if (set.includes(node)) {
2786
2835
  throw fail(`Already registered`);
2787
2836
  }
@@ -3121,7 +3170,11 @@ function isPlainObject(value) {
3121
3170
  return false;
3122
3171
  }
3123
3172
  const proto = Object.getPrototypeOf(value);
3124
- if (proto == null) {
3173
+ // Fast path for an object literal from this realm — nearly every snapshot MST
3174
+ // inspects. The fallback compares the constructor's *source text* so that an
3175
+ // `Object` from another realm (iframe, vm context) still counts as plain; it
3176
+ // is ~6x slower, so only cross-realm and null-prototype values pay for it.
3177
+ if (proto === Object.prototype || proto == null) {
3125
3178
  return true;
3126
3179
  }
3127
3180
  return proto.constructor?.toString() === plainObjectString;
@@ -3711,14 +3764,30 @@ const $preProcessorFailed = Symbol("$preProcessorFailed");
3711
3764
  class SnapshotProcessor extends BaseType {
3712
3765
  _subtype;
3713
3766
  _processors;
3767
+ _flags;
3768
+ // memoized once stable; see the guard on Union.flags
3714
3769
  get flags() {
3715
- return this._subtype.flags | TypeFlags.SnapshotProcessor;
3770
+ const cached = this._flags;
3771
+ if (cached !== undefined) {
3772
+ return cached;
3773
+ }
3774
+ const result = this._subtype.flags | TypeFlags.SnapshotProcessor;
3775
+ if (!(result & TypeFlags.Late)) {
3776
+ this._flags = result;
3777
+ }
3778
+ return result;
3716
3779
  }
3717
3780
  constructor(_subtype, _processors, name) {
3718
- super(name || _subtype.name);
3781
+ // `|| undefined`, so an empty name still falls through to the subtype's, as
3782
+ // it did when this read `name || _subtype.name`. Passing `""` straight to
3783
+ // `super` would keep it: `_name ??= computeName()` only fills a nullish one.
3784
+ super(name || undefined);
3719
3785
  this._subtype = _subtype;
3720
3786
  this._processors = _processors;
3721
3787
  }
3788
+ computeName() {
3789
+ return this._subtype.name;
3790
+ }
3722
3791
  describe() {
3723
3792
  return `snapshotProcessor(${this._subtype.describe()})`;
3724
3793
  }
@@ -3968,17 +4037,20 @@ class MapType extends ComplexType {
3968
4037
  mapIdentifierAttribute = undefined;
3969
4038
  flags = TypeFlags.Map;
3970
4039
  hookInitializers = [];
3971
- constructor(name, _subType, hookInitializers = []) {
3972
- super(name);
4040
+ constructor(_subType, hookInitializers = []) {
4041
+ super();
3973
4042
  this._subType = _subType;
3974
4043
  this._determineIdentifierMode();
3975
4044
  this.hookInitializers = hookInitializers;
3976
4045
  }
4046
+ computeName() {
4047
+ return `Map<string, ${this._subType.name}>`;
4048
+ }
3977
4049
  hooks(hooks) {
3978
4050
  const hookInitializers = this.hookInitializers.length > 0
3979
4051
  ? this.hookInitializers.concat(hooks)
3980
4052
  : [hooks];
3981
- return new MapType(this.name, this._subType, hookInitializers);
4053
+ return new MapType(this._subType, hookInitializers);
3982
4054
  }
3983
4055
  instantiate(parent, subpath, environment, initialValue) {
3984
4056
  this._determineIdentifierMode();
@@ -4212,7 +4284,7 @@ MapType.prototype.applySnapshot = action(MapType.prototype.applySnapshot);
4212
4284
  * @returns
4213
4285
  */
4214
4286
  function map(subtype) {
4215
- return new MapType(`Map<string, ${subtype.name}>`, subtype);
4287
+ return new MapType(subtype);
4216
4288
  }
4217
4289
  /**
4218
4290
  * Returns if a given value represents a map type.
@@ -4232,16 +4304,19 @@ class ArrayType extends ComplexType {
4232
4304
  _subType;
4233
4305
  flags = TypeFlags.Array;
4234
4306
  hookInitializers = [];
4235
- constructor(name, _subType, hookInitializers = []) {
4236
- super(name);
4307
+ constructor(_subType, hookInitializers = []) {
4308
+ super();
4237
4309
  this._subType = _subType;
4238
4310
  this.hookInitializers = hookInitializers;
4239
4311
  }
4312
+ computeName() {
4313
+ return `${this._subType.name}[]`;
4314
+ }
4240
4315
  hooks(hooks) {
4241
4316
  const hookInitializers = this.hookInitializers.length > 0
4242
4317
  ? this.hookInitializers.concat(hooks)
4243
4318
  : [hooks];
4244
- return new ArrayType(this.name, this._subType, hookInitializers);
4319
+ return new ArrayType(this._subType, hookInitializers);
4245
4320
  }
4246
4321
  instantiate(parent, subpath, environment, initialValue) {
4247
4322
  return createObjectNode(this, parent, subpath, environment, initialValue);
@@ -4290,14 +4365,17 @@ class ArrayType extends ComplexType {
4290
4365
  const node = getStateTreeNode(change.object);
4291
4366
  node.assertWritable({ subpath: `${change.index}` });
4292
4367
  const subType = node.type._subType;
4293
- const childNodes = node.getChildren();
4294
4368
  switch (change.type) {
4295
4369
  case "update":
4296
4370
  {
4297
4371
  if (change.newValue === change.object[change.index]) {
4298
4372
  return null;
4299
4373
  }
4300
- const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], change.index);
4374
+ const updatedNodes = reconcileArrayChildren(node, subType,
4375
+ // only the replaced child is reconciled, so read that one node
4376
+ // directly — `node.getChildren()` copies the whole backing array,
4377
+ // which made a single-element assignment cost O(array length)
4378
+ [node.getChildNode(`${change.index}`)], [change.newValue], change.index);
4301
4379
  if (!updatedNodes) {
4302
4380
  return null;
4303
4381
  }
@@ -4307,6 +4385,7 @@ class ArrayType extends ComplexType {
4307
4385
  case "splice":
4308
4386
  {
4309
4387
  const { index, removedCount, added } = change;
4388
+ const childNodes = node.getChildren();
4310
4389
  const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, index);
4311
4390
  if (!addedNodes) {
4312
4391
  return null;
@@ -4431,7 +4510,7 @@ ArrayType.prototype.applySnapshot = action(ArrayType.prototype.applySnapshot);
4431
4510
  */
4432
4511
  function array(subtype) {
4433
4512
  assertIsType(subtype, 1);
4434
- return new ArrayType(`${subtype.name}[]`, subtype);
4513
+ return new ArrayType(subtype);
4435
4514
  }
4436
4515
  /**
4437
4516
  * @param firstNewPath index the reconciled slice starts at; both call sites
@@ -4446,8 +4525,12 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNew
4446
4525
  // whose id extraction needs type-specific preprocessing (union,
4447
4526
  // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4448
4527
  // `is()` before their id check, so they stay on the scan path.
4528
+ // With at most one old node the scan below is already O(1), so building the
4529
+ // index would only add a Map allocation to every single-element write.
4449
4530
  let idIndex;
4450
- if (childType instanceof ModelType && childType.identifierAttribute) {
4531
+ if (oldNodes.length > 1 &&
4532
+ childType instanceof ModelType &&
4533
+ childType.identifierAttribute) {
4451
4534
  const byId = new Map();
4452
4535
  for (const n of oldNodes) {
4453
4536
  if (n instanceof ObjectNode && n.identifier !== null) {
@@ -4609,14 +4692,53 @@ const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
4609
4692
  function getPropObservable(storedValue, key) {
4610
4693
  return getAtom(storedValue, key);
4611
4694
  }
4695
+ /**
4696
+ * All of an instance's per-property `ObservableValue`s in one map, or
4697
+ * `undefined` if this mobx does not expose them.
4698
+ *
4699
+ * `observable.object` returns a **Proxy**, and `getAtom(storedValue, key)`
4700
+ * probes it with four `isObservableArray/Set/Map/Object` guards plus a `$mobx`
4701
+ * read — each one a marker-property read that goes through the proxy's `get`
4702
+ * trap. Paying that per property made the trap machinery the bulk of a wide
4703
+ * model's `getSnapshot`; resolving the administration once and reading its
4704
+ * property map directly reduces the per-property cost to a `Map.get`.
4705
+ *
4706
+ * `values_` is mobx-internal, hence the undefined result: every caller keeps a
4707
+ * path built on supported API, so a mobx that drops it degrades to the old
4708
+ * speed rather than breaking.
4709
+ *
4710
+ * The map is complete for our instances. `getAtom` falls back to
4711
+ * `materializeLazy{Computed,Observable}_` because mobx defers constructing an
4712
+ * `ObservableValue` for *decorator* annotations; `createNewInstance` builds
4713
+ * these through `observable.object`, which populates `values_` eagerly for
4714
+ * every declared property.
4715
+ */
4716
+ function getPropObservables(storedValue) {
4717
+ const adm = _getAdministration(storedValue);
4718
+ return adm.values_;
4719
+ }
4612
4720
  function objectTypeToString() {
4613
4721
  return getStateTreeNode(this).toString();
4614
4722
  }
4615
4723
  const defaultObjectOptions = {
4616
- name: "AnonymousModel",
4617
- properties: {},
4618
- initializers: EMPTY_ARRAY
4619
- };
4724
+ name: "AnonymousModel"};
4725
+ /**
4726
+ * A plain loop rather than `forAllProps`, which allocated a closure and made an
4727
+ * indirect call per property. This runs once per `types.model()` over every
4728
+ * declared property, and jbrowse builds a ~40-slot schema per track.
4729
+ */
4730
+ function findIdentifierAttribute(properties, propertyNames) {
4731
+ let identifierAttribute = undefined;
4732
+ for (const propName of propertyNames) {
4733
+ if (properties[propName].flags & TypeFlags.Identifier) {
4734
+ if (identifierAttribute) {
4735
+ throw fail(`Cannot define property '${propName}' as object identifier, property '${identifierAttribute}' is already defined as identifier property`);
4736
+ }
4737
+ identifierAttribute = propName;
4738
+ }
4739
+ }
4740
+ return identifierAttribute;
4741
+ }
4620
4742
  function toPropertiesObject(declaredProps) {
4621
4743
  // loop through properties and ensures that all items are types
4622
4744
  return Object.keys(declaredProps).reduce((props, key) => {
@@ -4682,14 +4804,29 @@ class ModelType extends ComplexType {
4682
4804
  // to check the first instance we finalize (see finalizeNewInstance)
4683
4805
  duplicateKeysChecked = false;
4684
4806
  constructor(opts) {
4685
- super(opts.name || defaultObjectOptions.name);
4686
- Object.assign(this, defaultObjectOptions, opts);
4807
+ // `??`, not `||`: `types.model("", {})` names the model "". The old
4808
+ // `Object.assign(this, defaults, opts)` below overwrote the `||` fallback
4809
+ // with opts.name afterwards, so that only worked by accident.
4810
+ super(opts.name ?? defaultObjectOptions.name);
4811
+ // Every field is assigned here, unconditionally and in a fixed order,
4812
+ // rather than by `Object.assign(this, defaultObjectOptions, opts)`. `opts`
4813
+ // carries a different key set at each of the three call sites — `model()`,
4814
+ // and cloneAndEnhance's preprocessed / converted paths — so copying it
4815
+ // wholesale gave ModelType three hidden classes, making every later read of
4816
+ // `type.properties` / `type.propertyNames` (getSnapshot's inner loop, among
4817
+ // others) polymorphic. It also left the internal `propertiesArePreProcessed`
4818
+ // / `propertiesAreConverted` plumbing on the type for its whole lifetime.
4819
+ this.initializers = opts.initializers ?? EMPTY_ARRAY;
4820
+ this.preProcessor = opts.preProcessor;
4821
+ this.postProcessor = opts.postProcessor;
4822
+ const declaredProperties = (opts.properties ?? EMPTY_OBJECT);
4687
4823
  if (opts.propertiesArePreProcessed) {
4688
4824
  // `properties` is a parent type's already-converted + frozen output
4689
4825
  // (chain step with no new props), so its derived propertyNames and
4690
4826
  // identifierAttribute are identical to the parent's — reuse them verbatim
4691
4827
  // (cloneAndEnhance passed them in) instead of re-running Object.keys and
4692
4828
  // the per-prop identifier scan, both O(props), on every step.
4829
+ this.properties = declaredProperties;
4693
4830
  this.propertyNames = opts.propertyNames;
4694
4831
  this.identifierAttribute = opts.identifierAttribute;
4695
4832
  }
@@ -4698,26 +4835,16 @@ class ModelType extends ComplexType {
4698
4835
  // means every value is already a type — the parent's converted bag merged
4699
4836
  // with a freshly converted delta — so skip re-converting. Only raw entry
4700
4837
  // points (`model()`) still need the full toPropertiesObject pass.
4701
- if (!opts.propertiesAreConverted) {
4702
- this.properties = toPropertiesObject(this.properties);
4703
- }
4704
- freeze(this.properties); // make sure nobody messes with it
4705
- this.propertyNames = Object.keys(this.properties);
4706
- this.identifierAttribute = this._getIdentifierAttribute();
4838
+ const properties = (opts.propertiesAreConverted
4839
+ ? declaredProperties
4840
+ : toPropertiesObject(declaredProperties));
4841
+ this.properties = properties;
4842
+ freeze(properties); // make sure nobody messes with it
4843
+ const propertyNames = Object.keys(properties);
4844
+ this.propertyNames = propertyNames;
4845
+ this.identifierAttribute = findIdentifierAttribute(properties, propertyNames);
4707
4846
  }
4708
4847
  }
4709
- _getIdentifierAttribute() {
4710
- let identifierAttribute = undefined;
4711
- this.forAllProps((propName, propType) => {
4712
- if (propType.flags & TypeFlags.Identifier) {
4713
- if (identifierAttribute) {
4714
- throw fail(`Cannot define property '${propName}' as object identifier, property '${identifierAttribute}' is already defined as identifier property`);
4715
- }
4716
- identifierAttribute = propName;
4717
- }
4718
- });
4719
- return identifierAttribute;
4720
- }
4721
4848
  cloneAndEnhance(opts) {
4722
4849
  // Fast path: a chain step that adds no new properties (.actions/.views/
4723
4850
  // .volatile/.named/pre-postProcessor) reuses this type's already-converted +
@@ -4931,9 +5058,20 @@ class ModelType extends ComplexType {
4931
5058
  }
4932
5059
  finalizeNewInstance(node, instance) {
4933
5060
  addHiddenFinalProp(instance, "toString", objectTypeToString);
4934
- this.forAllProps(name => {
4935
- _interceptReads(instance, name, node.unbox);
4936
- });
5061
+ // `_interceptReads(instance, name, ...)` assigns exactly this `dehancer`,
5062
+ // but reaches the ObservableValue via getAtom — several proxy-trap reads
5063
+ // per property (see getPropObservables). Resolve the map once instead.
5064
+ const observables = getPropObservables(instance);
5065
+ if (observables) {
5066
+ for (const name of this.propertyNames) {
5067
+ observables.get(name).dehancer = node.unbox;
5068
+ }
5069
+ }
5070
+ else {
5071
+ this.forAllProps(name => {
5072
+ _interceptReads(instance, name, node.unbox);
5073
+ });
5074
+ }
4937
5075
  this.initializers.reduce((self, fn) => fn(self), instance);
4938
5076
  // views, actions and volatile share the instance namespace with properties,
4939
5077
  // so a view/action reusing a property name silently clobbers that property's
@@ -4986,9 +5124,18 @@ class ModelType extends ComplexType {
4986
5124
  }
4987
5125
  getChildren(node) {
4988
5126
  const names = this.propertyNames;
5127
+ const storedValue = node.storedValue;
5128
+ const observables = getPropObservables(storedValue);
4989
5129
  const res = new Array(names.length);
4990
5130
  for (let i = 0; i < names.length; i++) {
4991
- res[i] = this.getPropertyNode(node, names[i]);
5131
+ const name = names[i];
5132
+ const childNode = (observables
5133
+ ? observables.get(name)
5134
+ : getPropObservable(storedValue, name))?.raw();
5135
+ if (!childNode) {
5136
+ throw fail(`Node not available for property ${name}`);
5137
+ }
5138
+ res[i] = childNode;
4992
5139
  }
4993
5140
  return res;
4994
5141
  }
@@ -5014,14 +5161,17 @@ class ModelType extends ComplexType {
5014
5161
  const res = {};
5015
5162
  const storedValue = node.storedValue;
5016
5163
  const properties = this.properties;
5164
+ const observables = getPropObservables(storedValue);
5017
5165
  for (const name of this.propertyNames) {
5018
5166
  // One mobx lookup serves both purposes: reportObserved so the snapshot
5019
5167
  // computed recomputes when the child is reassigned (raw() below does not
5020
5168
  // track), and raw() to read the child node. Going through getChildNode
5021
5169
  // would repeat the same lookup for every property.
5022
- const observable = getPropObservable(storedValue, name);
5023
- observable.reportObserved();
5024
- const childNode = observable.raw();
5170
+ const observable = observables
5171
+ ? observables.get(name)
5172
+ : getPropObservable(storedValue, name);
5173
+ observable?.reportObserved();
5174
+ const childNode = observable?.raw();
5025
5175
  if (!childNode) {
5026
5176
  throw fail(`Node not available for property ${name}`);
5027
5177
  }
@@ -5371,6 +5521,8 @@ function isPrimitiveType(type) {
5371
5521
  (TypeFlags.String |
5372
5522
  TypeFlags.Number |
5373
5523
  TypeFlags.Integer |
5524
+ TypeFlags.Float |
5525
+ TypeFlags.Finite |
5374
5526
  TypeFlags.Boolean |
5375
5527
  TypeFlags.Date)) >
5376
5528
  0);
@@ -5507,7 +5659,7 @@ function refinement(...args) {
5507
5659
  * @returns
5508
5660
  */
5509
5661
  function isRefinementType(type) {
5510
- return (type.flags & TypeFlags.Refinement) > 0;
5662
+ return isType(type) && (type.flags & TypeFlags.Refinement) > 0;
5511
5663
  }
5512
5664
 
5513
5665
  /**
@@ -5548,10 +5700,23 @@ function enumeration(name, options) {
5548
5700
  // the scoping never engages and every failure prints every member's full
5549
5701
  // structure. Wrappers expose their child as `_subtype` (optional/refinement/
5550
5702
  // snapshotProcessor) or via `getSubType()` (late); bounded to avoid cycles.
5703
+ // Only *successful* resolutions are cached. A wrapper chain's shape is fixed at
5704
+ // construction, so once a member resolves to a ModelType it always will; but a
5705
+ // `late` member reports no subtype until its definition evaluates, and that
5706
+ // miss must stay retryable.
5707
+ const resolvedModelTypes = new WeakMap();
5551
5708
  function resolveModelType(type) {
5709
+ if (!type) {
5710
+ return undefined;
5711
+ }
5712
+ const cached = resolvedModelTypes.get(type);
5713
+ if (cached) {
5714
+ return cached;
5715
+ }
5552
5716
  let current = type;
5553
5717
  for (let depth = 0; current && depth < 20; depth++) {
5554
5718
  if (current instanceof ModelType) {
5719
+ resolvedModelTypes.set(type, current);
5555
5720
  return current;
5556
5721
  }
5557
5722
  const wrapper = current;
@@ -5565,31 +5730,51 @@ function resolveModelType(type) {
5565
5730
  */
5566
5731
  class Union extends BaseType {
5567
5732
  _types;
5568
- _dispatcher;
5569
- _eager = true;
5570
- // Deliberately recomputed rather than memoized: a `types.late` member reports
5571
- // 0 for its subtype until the definition resolves, so the fold is not stable
5572
- // over the type's lifetime and a memo would need a carve-out for exactly the
5573
- // case that motivates it. Measured at ~4% of union creation, which is not
5574
- // worth another cache.
5733
+ _flags;
5734
+ // Memoized, but only once the fold is known to be stable. A `types.late`
5735
+ // member reports 0 for its subtype until its definition resolves, so a union
5736
+ // containing one must keep recomputing — and every wrapper ORs its subtype's
5737
+ // flags upward, so `Late` in the *result* is an exact test for "some member
5738
+ // may still change" however deeply it is nested. A resolved late still
5739
+ // reports Late, so such a union simply never caches; that is conservative in
5740
+ // the safe direction and no real-world union is built out of late members.
5741
+ //
5742
+ // This reverses an earlier decision to leave it uncached, which was sized at
5743
+ // ~4% "on union creation alone". That understated it: the reads are what
5744
+ // cost, not the creation. `ModelType._getIdentifierAttribute` folds every
5745
+ // property's flags on each `types.model()`, and jbrowse's config slots are
5746
+ // unions under a stripDefault, so building one schema re-folded every slot's
5747
+ // union. See agent-docs/adr/0003.
5575
5748
  get flags() {
5749
+ const cached = this._flags;
5750
+ if (cached !== undefined) {
5751
+ return cached;
5752
+ }
5576
5753
  let result = TypeFlags.Union;
5577
5754
  for (const type of this._types) {
5578
5755
  result |= type.flags;
5579
5756
  }
5757
+ if (!(result & TypeFlags.Late)) {
5758
+ this._flags = result;
5759
+ }
5580
5760
  return result;
5581
5761
  }
5582
- constructor(name, _types, options) {
5583
- super(name);
5762
+ computeName() {
5763
+ return `(${this._types.map(type => type.name).join(" | ")})`;
5764
+ }
5765
+ constructor(_types, options) {
5766
+ super();
5584
5767
  this._types = _types;
5585
- options = {
5586
- eager: true,
5587
- dispatcher: undefined,
5588
- ...options
5589
- };
5590
- this._dispatcher = options.dispatcher;
5591
- if (!options.eager) {
5592
- this._eager = false;
5768
+ // read the two options directly rather than spreading defaults into a fresh
5769
+ // object: this constructor runs once per config slot in jbrowse, and the
5770
+ // merged object was allocated only to be read twice and dropped
5771
+ if (options !== undefined) {
5772
+ if (options.dispatcher !== undefined) {
5773
+ this._dispatcher = options.dispatcher;
5774
+ }
5775
+ if (options.eager === false) {
5776
+ this._eager = false;
5777
+ }
5593
5778
  }
5594
5779
  }
5595
5780
  isAssignableFrom(type) {
@@ -5640,14 +5825,6 @@ class Union extends BaseType {
5640
5825
  }
5641
5826
  return `${baseWithDiscriminator}:\n ${formatValidationErrorLines(errors).join("\n ")}`;
5642
5827
  }
5643
- // Memoizes the discriminator -> member scan below. Union membership is fixed
5644
- // at construction, so the result for a given `type` string never changes.
5645
- // Without this, validating a config with many elements drawn from a wide
5646
- // pluggable union (e.g. jbrowse's 30+ track/adapter types) re-scans every
5647
- // member — and calls resolveModelType + literal.is() on each — once per
5648
- // element. With it, each distinct discriminator scans once; the rest are
5649
- // O(1) map hits. `undefined` (no match OR ambiguous) is cached too.
5650
- _discriminatorCache;
5651
5828
  _findCandidateByTypeDiscriminator(discriminator) {
5652
5829
  const cache = (this._discriminatorCache ??= new Map());
5653
5830
  if (cache.has(discriminator)) {
@@ -5682,11 +5859,6 @@ class Union extends BaseType {
5682
5859
  }
5683
5860
  return found;
5684
5861
  }
5685
- // True when every member resolves to a model carrying a literal `type`
5686
- // discriminator — i.e. a fully discriminated union, where a snapshot's `type`
5687
- // uniquely identifies the intended member and no untagged catch-all member
5688
- // could also accept it. Cached: membership is fixed at construction.
5689
- _allMembersDiscriminated;
5690
5862
  allMembersDiscriminated() {
5691
5863
  if (this._allMembersDiscriminated === undefined) {
5692
5864
  this._allMembersDiscriminated = this._types.every(t => {
@@ -5784,18 +5956,20 @@ class Union extends BaseType {
5784
5956
  // use cached propertyNames from ModelType instead of Object.keys()
5785
5957
  for (const key of model.propertyNames) {
5786
5958
  const propType = props[key];
5787
- const isOptional = propType.flags & TypeFlags.Optional;
5959
+ // `flags` is a recomputed getter on the wrapper types (optional,
5960
+ // snapshotProcessor, late, union), so read it once per property
5961
+ const flags = propType.flags;
5788
5962
  const propValue = value[key];
5789
5963
  // check required properties exist and are not undefined
5790
5964
  // (unless the type accepts undefined, which Optional types do)
5791
- if (!isOptional) {
5965
+ if (!(flags & TypeFlags.Optional)) {
5792
5966
  if (!(key in value) || propValue === undefined) {
5793
5967
  return false;
5794
5968
  }
5795
5969
  }
5796
5970
  // for literal types, verify the value matches exactly
5797
5971
  // this is critical for discriminated unions
5798
- if (propType.flags & TypeFlags.Literal) {
5972
+ if (flags & TypeFlags.Literal) {
5799
5973
  if (!propType.is(propValue)) {
5800
5974
  return false;
5801
5975
  }
@@ -5863,6 +6037,14 @@ class Union extends BaseType {
5863
6037
  return this._types;
5864
6038
  }
5865
6039
  }
6040
+ // Defaults shared by every union; see the field declarations at the top of the
6041
+ // class for why they are not own slots.
6042
+ Object.assign(Union.prototype, {
6043
+ _dispatcher: undefined,
6044
+ _eager: true,
6045
+ _discriminatorCache: undefined,
6046
+ _allMembersDiscriminated: undefined
6047
+ });
5866
6048
  /**
5867
6049
  * `types.union` - Create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function of the form `(snapshot) => Type`.
5868
6050
  *
@@ -5870,12 +6052,18 @@ class Union extends BaseType {
5870
6052
  * @param otherTypes
5871
6053
  * @returns
5872
6054
  */
5873
- function union(optionsOrType, ...otherTypes) {
5874
- const options = isType(optionsOrType) ? undefined : optionsOrType;
5875
- const types = isType(optionsOrType)
5876
- ? [optionsOrType, ...otherTypes]
5877
- : otherTypes;
5878
- const name = `(${types.map(type => type.name).join(" | ")})`;
6055
+ function union(...args) {
6056
+ // One rest array, handed straight to the Union in the common case. Splitting
6057
+ // the leading argument out — whether by a `(first, ...rest)` signature that
6058
+ // rebuilds `[first, ...rest]`, or by `rest.unshift(first)` — allocates a
6059
+ // second array per union, and jbrowse builds one union per config slot. (The
6060
+ // `unshift` form is worse still: V8 inlines the spread and calls out to the
6061
+ // builtin, so it measured 3x the spread on a two-member union.) Only the
6062
+ // options overload, which nothing hot uses, pays for a copy.
6063
+ const firstIsType = isType(args[0]);
6064
+ const options = firstIsType ? undefined : args[0];
6065
+ const types = (firstIsType ? args : args.slice(1));
6066
+ // the name is folded from the members on demand — see Union.computeName
5879
6067
  // check all options
5880
6068
  if (devMode()) {
5881
6069
  if (options) {
@@ -5885,7 +6073,7 @@ function union(optionsOrType, ...otherTypes) {
5885
6073
  assertIsType(type, options ? i + 2 : i + 1);
5886
6074
  });
5887
6075
  }
5888
- return new Union(name, types, options);
6076
+ return new Union(types, options);
5889
6077
  }
5890
6078
  /**
5891
6079
  * Returns if a given value represents a union type.
@@ -5894,7 +6082,7 @@ function union(optionsOrType, ...otherTypes) {
5894
6082
  * @returns
5895
6083
  */
5896
6084
  function isUnionType(type) {
5897
- return (type.flags & TypeFlags.Union) > 0;
6085
+ return isType(type) && (type.flags & TypeFlags.Union) > 0;
5898
6086
  }
5899
6087
  /**
5900
6088
  * Returns the member types of a union.
@@ -5932,15 +6120,31 @@ class OptionalValue extends BaseType {
5932
6120
  _subtype;
5933
6121
  _defaultValue;
5934
6122
  optionalValues;
6123
+ _flags;
6124
+ // memoized once stable; see the same guard on Union.flags for why `Late` in
6125
+ // the folded result is the exact test for "may still change"
5935
6126
  get flags() {
5936
- return this._subtype.flags | TypeFlags.Optional;
6127
+ const cached = this._flags;
6128
+ if (cached !== undefined) {
6129
+ return cached;
6130
+ }
6131
+ const result = this._subtype.flags | TypeFlags.Optional;
6132
+ if (!(result & TypeFlags.Late)) {
6133
+ this._flags = result;
6134
+ }
6135
+ return result;
5937
6136
  }
5938
6137
  constructor(_subtype, _defaultValue, optionalValues) {
5939
- super(_subtype.name);
6138
+ // no name argument: reading `_subtype.name` here would force the name of
6139
+ // whatever is wrapped, and jbrowse wraps a union per config slot
6140
+ super();
5940
6141
  this._subtype = _subtype;
5941
6142
  this._defaultValue = _defaultValue;
5942
6143
  this.optionalValues = optionalValues;
5943
6144
  }
6145
+ computeName() {
6146
+ return this._subtype.name;
6147
+ }
5944
6148
  describe() {
5945
6149
  return `${this._subtype.describe()}?`;
5946
6150
  }
@@ -5983,8 +6187,11 @@ class OptionalValue extends BaseType {
5983
6187
  }
5984
6188
  }
5985
6189
  function checkOptionalPreconditions(type, defaultValueOrFunction) {
5986
- // make sure we never pass direct instances
5987
- if (typeof defaultValueOrFunction !== "function" &&
6190
+ // make sure we never pass direct instances. A node is always an object, so
6191
+ // the typeof narrows first: most defaults are primitives, and reading
6192
+ // `$treenode` off a string or a number is a megamorphic miss that this runs
6193
+ // once per config slot.
6194
+ if (typeof defaultValueOrFunction === "object" &&
5988
6195
  isStateTreeNode(defaultValueOrFunction)) {
5989
6196
  throw fail("default value cannot be an instance, pass a snapshot or a function that creates an instance/snapshot instead");
5990
6197
  }
@@ -6134,7 +6341,6 @@ function defaultSnapshotEquals(a, b) {
6134
6341
  * @internal
6135
6342
  */
6136
6343
  class StripDefaultValue extends OptionalValue {
6137
- _defaultSnapshot;
6138
6344
  shouldStripFromSnapshot(snapshot) {
6139
6345
  if (!this._defaultSnapshot) {
6140
6346
  // instantiate the subtype detached with the default and read the node's
@@ -6146,6 +6352,9 @@ class StripDefaultValue extends OptionalValue {
6146
6352
  return defaultSnapshotEquals(snapshot, this._defaultSnapshot.value);
6147
6353
  }
6148
6354
  }
6355
+ Object.assign(StripDefaultValue.prototype, {
6356
+ _defaultSnapshot: undefined
6357
+ });
6149
6358
  /**
6150
6359
  * Whether `type` is a strip-default optional whose current child `snapshot`
6151
6360
  * equals its default and should therefore be omitted from the parent model's
@@ -6348,7 +6557,13 @@ class Lazy extends SimpleType {
6348
6557
  }
6349
6558
  const node = createScalarNode(this, parent, subpath, environment, deepFreeze(value));
6350
6559
  this.pendingNodeList.push(node);
6351
- when(() => !node.isAlive, () => this.pendingNodeList.splice(this.pendingNodeList.indexOf(node), 1));
6560
+ when(() => !node.isAlive, () => {
6561
+ // guard the index: splice(-1, 1) would drop an unrelated pending node
6562
+ const index = this.pendingNodeList.indexOf(node);
6563
+ if (index >= 0) {
6564
+ this.pendingNodeList.splice(index, 1);
6565
+ }
6566
+ });
6352
6567
  return node;
6353
6568
  }
6354
6569
  isValidSnapshot(value, context) {
@@ -6377,9 +6592,12 @@ class Frozen extends SimpleType {
6377
6592
  subType;
6378
6593
  flags = TypeFlags.Frozen;
6379
6594
  constructor(subType) {
6380
- super(subType ? `frozen(${subType.name})` : "frozen");
6595
+ super(subType ? undefined : "frozen");
6381
6596
  this.subType = subType;
6382
6597
  }
6598
+ computeName() {
6599
+ return `frozen(${this.subType.name})`;
6600
+ }
6383
6601
  describe() {
6384
6602
  return "<any immutable value>";
6385
6603
  }
@@ -6535,10 +6753,13 @@ class BaseReferenceType extends SimpleType {
6535
6753
  onInvalidated;
6536
6754
  flags = TypeFlags.Reference;
6537
6755
  constructor(targetType, onInvalidated) {
6538
- super(`reference(${targetType.name})`);
6756
+ super();
6539
6757
  this.targetType = targetType;
6540
6758
  this.onInvalidated = onInvalidated;
6541
6759
  }
6760
+ computeName() {
6761
+ return `reference(${this.targetType.name})`;
6762
+ }
6542
6763
  describe() {
6543
6764
  return this.name;
6544
6765
  }
@@ -6801,7 +7022,7 @@ function reference(subType, options) {
6801
7022
  * @returns
6802
7023
  */
6803
7024
  function isReferenceType(type) {
6804
- return (type.flags & TypeFlags.Reference) > 0;
7025
+ return isType(type) && (type.flags & TypeFlags.Reference) > 0;
6805
7026
  }
6806
7027
  /**
6807
7028
  * `types.safeReference` - A safe reference is like a standard reference, except that it accepts the undefined value by default
@@ -6872,7 +7093,6 @@ class BaseIdentifierType extends SimpleType {
6872
7093
  * @hidden
6873
7094
  */
6874
7095
  class IdentifierType extends BaseIdentifierType {
6875
- flags = TypeFlags.Identifier;
6876
7096
  constructor() {
6877
7097
  super(`identifier`, "string");
6878
7098
  }
@@ -7070,11 +7290,14 @@ class Resilient extends BaseType {
7070
7290
  return this._subtype.flags;
7071
7291
  }
7072
7292
  constructor(_subtype, _fallbackType, _createFallbackSnapshot) {
7073
- super(`resilient(${_subtype.name})`);
7293
+ super();
7074
7294
  this._subtype = _subtype;
7075
7295
  this._fallbackType = _fallbackType;
7076
7296
  this._createFallbackSnapshot = _createFallbackSnapshot;
7077
7297
  }
7298
+ computeName() {
7299
+ return `resilient(${this._subtype.name})`;
7300
+ }
7078
7301
  describe() {
7079
7302
  return `resilient(${this._subtype.describe()})`;
7080
7303
  }
@@ -7109,12 +7332,20 @@ class Resilient extends BaseType {
7109
7332
  return this._subtype.reconcile(current, newValue, parent, subpath);
7110
7333
  }
7111
7334
  if (this._fallbackType.isAssignableFrom(current.type)) {
7335
+ // `current` holds the fallback. Try the real type again, but keep the
7336
+ // fallback node around while doing so — it is what the catch reconciles.
7337
+ let recovered;
7112
7338
  try {
7113
- return this._subtype.instantiate(parent, subpath, undefined, newValue);
7339
+ recovered = this._subtype.instantiate(parent, subpath, undefined, newValue);
7114
7340
  }
7115
7341
  catch (e) {
7116
7342
  return this._fallbackType.reconcile(current, this._createFallbackSnapshot(e, newValue), parent, subpath);
7117
7343
  }
7344
+ // the fallback node has been replaced, so it has to die: otherwise it
7345
+ // stays alive in the tree and its identifier stays in the root's cache,
7346
+ // where it collides with the recovered node's
7347
+ current.die();
7348
+ return recovered;
7118
7349
  }
7119
7350
  try {
7120
7351
  return this._subtype.reconcile(current, newValue, parent, subpath);