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