@jbrowse/mobx-state-tree 6.1.0 → 6.3.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.
@@ -482,30 +482,20 @@ function getIdentifier(target) {
482
482
  return getStateTreeNode(target).identifier;
483
483
  }
484
484
  /**
485
- * Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns such reference if the check passes,
486
- * else it returns undefined.
487
- *
488
- * @param getter Function to access the reference.
489
- * @param checkIfAlive true to also make sure the referenced node is alive (default), false to skip this check.
490
- * @returns
485
+ * Resolves a reference getter to the referenced node, or `undefined` when the
486
+ * reference is empty, dangling, or (with `checkIfAlive`) points at a dead node.
487
+ * Backs both {@link tryReference} and {@link isValidReference}.
491
488
  */
492
- function tryReference(getter, checkIfAlive = true) {
489
+ function resolveReference(getter, checkIfAlive) {
493
490
  try {
494
491
  const node = getter();
495
492
  if (node === undefined || node === null) {
496
493
  return undefined;
497
494
  }
498
- else if (isStateTreeNode(node)) {
499
- if (!checkIfAlive) {
500
- return node;
501
- }
502
- else {
503
- return isAlive(node) ? node : undefined;
504
- }
505
- }
506
- else {
495
+ if (!isStateTreeNode(node)) {
507
496
  throw fail("The reference to be checked is not one of node, null or undefined");
508
497
  }
498
+ return !checkIfAlive || isAlive(node) ? node : undefined;
509
499
  }
510
500
  catch (e) {
511
501
  if (e instanceof InvalidReferenceError) {
@@ -514,6 +504,17 @@ function tryReference(getter, checkIfAlive = true) {
514
504
  throw e;
515
505
  }
516
506
  }
507
+ /**
508
+ * Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns such reference if the check passes,
509
+ * else it returns undefined.
510
+ *
511
+ * @param getter Function to access the reference.
512
+ * @param checkIfAlive true to also make sure the referenced node is alive (default), false to skip this check.
513
+ * @returns
514
+ */
515
+ function tryReference(getter, checkIfAlive = true) {
516
+ return resolveReference(getter, checkIfAlive);
517
+ }
517
518
  /**
518
519
  * Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns if the check passes or not.
519
520
  *
@@ -522,24 +523,7 @@ function tryReference(getter, checkIfAlive = true) {
522
523
  * @returns
523
524
  */
524
525
  function isValidReference(getter, checkIfAlive = true) {
525
- try {
526
- const node = getter();
527
- if (node === undefined || node === null) {
528
- return false;
529
- }
530
- else if (isStateTreeNode(node)) {
531
- return checkIfAlive ? isAlive(node) : true;
532
- }
533
- else {
534
- throw fail("The reference to be checked is not one of node, null or undefined");
535
- }
536
- }
537
- catch (e) {
538
- if (e instanceof InvalidReferenceError) {
539
- return false;
540
- }
541
- throw e;
542
- }
526
+ return resolveReference(getter, checkIfAlive) !== undefined;
543
527
  }
544
528
  /**
545
529
  * Try to resolve a given path relative to a given node.
@@ -1167,7 +1151,7 @@ class ObjectNode extends BaseNode {
1167
1151
  }
1168
1152
  _autoUnbox = true; // unboxing is disabled when reading child nodes
1169
1153
  _isRunningAction = false; // only relevant for root
1170
- _hasSnapshotReaction = false;
1154
+ _snapshotReactionDisposer;
1171
1155
  _observableInstanceState = ObservableInstanceLifecycle.UNINITIALIZED;
1172
1156
  _childNodes;
1173
1157
  _initialSnapshot;
@@ -1589,7 +1573,17 @@ class ObjectNode extends BaseNode {
1589
1573
  }
1590
1574
  onSnapshot(onChange) {
1591
1575
  this._addSnapshotReaction();
1592
- return this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1576
+ const unregister = this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1577
+ return () => {
1578
+ unregister();
1579
+ // The reaction re-serializes the whole subtree on every change, so leaving
1580
+ // it running once the last listener is gone would keep charging the node
1581
+ // for a snapshot nobody receives. Referential stability of getSnapshot()
1582
+ // does not depend on it — that comes from keepAlive on _snapshotComputed.
1583
+ if (!this._internalEventsHasSubscribers(InternalEvents.Snapshot)) {
1584
+ this._removeSnapshotReaction();
1585
+ }
1586
+ };
1593
1587
  }
1594
1588
  emitSnapshot(snapshot) {
1595
1589
  this._internalEventsEmit(InternalEvents.Snapshot, snapshot);
@@ -1654,11 +1648,23 @@ class ObjectNode extends BaseNode {
1654
1648
  this.type.applyPatchLocally(this, subpath, patch);
1655
1649
  }
1656
1650
  _addSnapshotReaction() {
1657
- if (!this._hasSnapshotReaction) {
1651
+ if (!this._snapshotReactionDisposer) {
1658
1652
  const snapshotDisposer = mobx.reaction(() => this.snapshot, snapshot => this.emitSnapshot(snapshot), snapshotReactionOptions);
1659
1653
  this.addDisposer(snapshotDisposer);
1660
- this._hasSnapshotReaction = true;
1654
+ this._snapshotReactionDisposer = snapshotDisposer;
1655
+ }
1656
+ }
1657
+ _removeSnapshotReaction() {
1658
+ const disposer = this._snapshotReactionDisposer;
1659
+ if (!disposer) {
1660
+ return;
1661
1661
  }
1662
+ this._snapshotReactionDisposer = undefined;
1663
+ // not removeDisposer(), which throws when the registration is already gone —
1664
+ // the last listener can be disposed from within a disposer, i.e. after
1665
+ // aboutToDie has cleared them
1666
+ this._internalEventsUnregister(InternalEvents.Dispose, disposer);
1667
+ disposer();
1662
1668
  }
1663
1669
  // #region internal event handling
1664
1670
  _internalEvents;
@@ -1735,8 +1741,6 @@ var TypeFlags;
1735
1741
  * @hidden
1736
1742
  */
1737
1743
  const cannotDetermineSubtype = "cannotDetermine";
1738
- /** @hidden */
1739
- const $type = Symbol("$type");
1740
1744
  /**
1741
1745
  * A base type produces a MST node (Node in the state tree)
1742
1746
  *
@@ -1744,16 +1748,39 @@ const $type = Symbol("$type");
1744
1748
  * @hidden
1745
1749
  */
1746
1750
  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;
1751
+ /**
1752
+ * Builds the name of a type that does not get one handed to it. Composite
1753
+ * types (`union`, `array`, `map`, `reference`, ...) override this to fold
1754
+ * their members' names; see {@link name}. It runs after construction, so
1755
+ * unlike a `super(...)` argument it can read the subclass's own fields.
1756
+ */
1757
+ computeName() {
1758
+ // istanbul ignore next
1759
+ throw fail(`${this.constructor.name} has neither a name nor a computeName`);
1760
+ }
1761
+ /**
1762
+ * Friendly type name.
1763
+ *
1764
+ * A composite type folds its members' names, which for a union is a map +
1765
+ * join over every member — and the result is read only by error messages and
1766
+ * `describe()`. So those types leave it unset and it is built on first read
1767
+ * and cached: a union that never fails a typecheck never builds one, and
1768
+ * jbrowse builds a union per config slot, tens of thousands per session load.
1769
+ *
1770
+ * Deliberately not a lambda passed to the constructor: that allocated a
1771
+ * closure per type (the very cost being avoided) and made two separately
1772
+ * built but equivalent types compare unequal, since the closures differ.
1773
+ */
1774
+ get name() {
1775
+ return (this._name ??= this.computeName());
1776
+ }
1777
+ set name(value) {
1778
+ this._name = value;
1779
+ }
1755
1780
  constructor(name) {
1756
- this.name = name;
1781
+ if (name !== undefined) {
1782
+ this._name = name;
1783
+ }
1757
1784
  }
1758
1785
  create(snapshot, environment) {
1759
1786
  typecheckInternal(this, snapshot);
@@ -1798,6 +1825,9 @@ class BaseType {
1798
1825
  }
1799
1826
  }
1800
1827
  BaseType.prototype.create = mobx.action(BaseType.prototype.create);
1828
+ // Defaults that every type shares, kept off the individual type objects. See
1829
+ // `isType` and `_name` in the class body.
1830
+ Object.assign(BaseType.prototype, { isType: true, _name: undefined });
1801
1831
  /**
1802
1832
  * A complex type produces a MST node (Node in the state tree)
1803
1833
  *
@@ -1915,7 +1945,6 @@ function assertIsType(type, argNumber) {
1915
1945
  assertArg(type, isType, "mobx-state-tree type", argNumber);
1916
1946
  }
1917
1947
 
1918
- const runningActions = new Map();
1919
1948
  /**
1920
1949
  * Note: Consider migrating to `createActionTrackingMiddleware2`, it is easier to use.
1921
1950
  *
@@ -1931,6 +1960,12 @@ const runningActions = new Map();
1931
1960
  * @returns
1932
1961
  */
1933
1962
  function createActionTrackingMiddleware(hooks) {
1963
+ // per middleware, not per module: the entries hold the `context` this
1964
+ // middleware's own onStart returned, so two middlewares on one tree would
1965
+ // otherwise overwrite each other's context and then delete the shared entry —
1966
+ // whichever saw `flow_return` second dereferenced `undefined`.
1967
+ // createActionTrackingMiddleware2 scopes its map the same way.
1968
+ const runningActions = new Map();
1934
1969
  return function actionTrackingMiddleware(call, next, _abort) {
1935
1970
  switch (call.type) {
1936
1971
  case "action": {
@@ -2123,7 +2158,7 @@ function createActionTrackingMiddleware2(middlewareHooks) {
2123
2158
  };
2124
2159
  }
2125
2160
 
2126
- function serializeArgument(node, actionName, index, arg) {
2161
+ function serializeArgument(arg) {
2127
2162
  if (arg instanceof Date) {
2128
2163
  return { $MST_DATE: arg.getTime() };
2129
2164
  }
@@ -2151,7 +2186,7 @@ function serializeArgument(node, actionName, index, arg) {
2151
2186
  return serializeTheUnserializable(`${e}`);
2152
2187
  }
2153
2188
  }
2154
- function deserializeArgument(adm, value) {
2189
+ function deserializeArgument(value) {
2155
2190
  if (value && typeof value === "object" && "$MST_DATE" in value) {
2156
2191
  return new Date(value["$MST_DATE"]);
2157
2192
  }
@@ -2195,7 +2230,7 @@ function baseApplyAction(target, action) {
2195
2230
  if (!(typeof resolvedTarget[action.name] === "function")) {
2196
2231
  throw fail(`Action '${action.name}' does not exist in '${node.path}'`);
2197
2232
  }
2198
- return resolvedTarget[action.name](...(action.args ? action.args.map(v => deserializeArgument(node, v)) : []));
2233
+ return resolvedTarget[action.name](...(action.args ? action.args.map(deserializeArgument) : []));
2199
2234
  }
2200
2235
  /**
2201
2236
  * Small abstraction around `onAction` and `applyAction`, attaches an action listener to a tree and records all the actions emitted.
@@ -2312,7 +2347,7 @@ function onAction(target, listener, attachAfter = false) {
2312
2347
  const info = {
2313
2348
  name: rawCall.name,
2314
2349
  path: getRelativePathBetweenNodes(getStateTreeNode(target), sourceNode),
2315
- args: rawCall.args.map((arg, index) => serializeArgument(sourceNode, rawCall.name, index, arg))
2350
+ args: rawCall.args.map(serializeArgument)
2316
2351
  };
2317
2352
  if (attachAfter) {
2318
2353
  const res = next(rawCall);
@@ -2653,6 +2688,8 @@ function shortenPrintValue(valueInString) {
2653
2688
  }
2654
2689
  function toErrorString(error) {
2655
2690
  const { value } = error;
2691
+ // every context entry carries a type: they are built by getContextForPath and
2692
+ // by typecheck's initial `[{ path: "", type }]`
2656
2693
  const type = error.context[error.context.length - 1].type;
2657
2694
  const fullPath = error.context
2658
2695
  .map(({ path }) => path)
@@ -2664,14 +2701,21 @@ function toErrorString(error) {
2664
2701
  : isPrimitive(value)
2665
2702
  ? "value"
2666
2703
  : "snapshot";
2667
- const isSnapshotCompatible = type && isStateTreeNode(value) && type.is(getStateTreeNode(value).snapshot);
2668
- return `${pathPrefix}${currentTypename} ${shortenPrintValue(prettyPrintValue(value))} is not assignable ${type ? `to type: \`${type.name}\`` : ``}${error.message ? ` (${error.message})` : ""}${type
2669
- ? isPrimitiveType(type) || isPrimitive(value)
2670
- ? `.`
2671
- : `, expected an instance of \`${type.name}\` or a snapshot like \`${shortenPrintValue(type.describe())}\` instead.${isSnapshotCompatible
2672
- ? " (Note that a snapshot of the provided value is compatible with the targeted type)"
2673
- : ""}`
2674
- : `.`}`;
2704
+ let expectation;
2705
+ if (isPrimitiveType(type) || isPrimitive(value)) {
2706
+ expectation = ".";
2707
+ }
2708
+ else {
2709
+ // `isPrimitiveType` is declared `(type: IT) => type is IT`, so its negative
2710
+ // branch narrows to `never` rather than to "some non-primitive type"; the
2711
+ // annotation restores the type it actually has here.
2712
+ const complexType = type;
2713
+ const isSnapshotCompatible = isStateTreeNode(value) && complexType.is(getStateTreeNode(value).snapshot);
2714
+ expectation = `, expected an instance of \`${complexType.name}\` or a snapshot like \`${shortenPrintValue(complexType.describe())}\` instead.${isSnapshotCompatible
2715
+ ? " (Note that a snapshot of the provided value is compatible with the targeted type)"
2716
+ : ""}`;
2717
+ }
2718
+ return `${pathPrefix}${currentTypename} ${shortenPrintValue(prettyPrintValue(value))} is not assignable to type: \`${type.name}\`${error.message ? ` (${error.message})` : ""}${expectation}`;
2675
2719
  }
2676
2720
  /**
2677
2721
  * @internal
@@ -2780,10 +2824,13 @@ class IdentifierCache {
2780
2824
  addNodeToCache(node, lastCacheUpdate = true) {
2781
2825
  if (node.identifierAttribute) {
2782
2826
  const identifier = node.identifier;
2783
- if (!this.cache.has(identifier)) {
2784
- this.cache.set(identifier, mobx.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 = mobx.observable.array([], mobxShallow);
2832
+ this.cache.set(identifier, set);
2785
2833
  }
2786
- const set = this.cache.get(identifier);
2787
2834
  if (set.includes(node)) {
2788
2835
  throw fail(`Already registered`);
2789
2836
  }
@@ -3123,7 +3170,11 @@ function isPlainObject(value) {
3123
3170
  return false;
3124
3171
  }
3125
3172
  const proto = Object.getPrototypeOf(value);
3126
- 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) {
3127
3178
  return true;
3128
3179
  }
3129
3180
  return proto.constructor?.toString() === plainObjectString;
@@ -3493,6 +3544,8 @@ function createFlowSpawner(name, generator) {
3493
3544
  const spawner = function flowSpawner(...flowArgs) {
3494
3545
  // Implementation based on https://github.com/tj/co/blob/master/index.js
3495
3546
  const runId = getNextActionId();
3547
+ // no `!`: the guard below is the point, and asserting non-null first made it
3548
+ // dead to the checker while leaving it live at runtime
3496
3549
  const parentContext = getCurrentActionContext();
3497
3550
  if (!parentContext) {
3498
3551
  throw fail("a mst flow must always have a parent context");
@@ -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
  }
@@ -3745,8 +3814,10 @@ class SnapshotProcessor extends BaseType {
3745
3814
  return sn;
3746
3815
  }
3747
3816
  _fixNode(node) {
3748
- // the node has to use these methods rather than the original type ones
3749
- proxyNodeTypeMethods(node.type, this, "create");
3817
+ // the node's type is the *inner* type, so `getType(instance).create(...)`
3818
+ // would bypass the processors — point it at ours instead
3819
+ const nodeType = node.type;
3820
+ nodeType.create = this.create.bind(this);
3750
3821
  if (node instanceof ObjectNode) {
3751
3822
  node.hasSnapshotPostProcessor = !!this._processors.postProcessor;
3752
3823
  }
@@ -3815,11 +3886,6 @@ class SnapshotProcessor extends BaseType {
3815
3886
  return this._subtype.isMatchingSnapshotId(current, processedSn);
3816
3887
  }
3817
3888
  }
3818
- function proxyNodeTypeMethods(nodeType, snapshotProcessorType, ...methods) {
3819
- for (const method of methods) {
3820
- nodeType[method] = snapshotProcessorType[method].bind(snapshotProcessorType);
3821
- }
3822
- }
3823
3889
  /**
3824
3890
  * `types.snapshotProcessor` - Runs a pre/post snapshot processor before/after serializing a given type.
3825
3891
  *
@@ -3870,6 +3936,41 @@ function snapshotProcessor(type, processors, name) {
3870
3936
  return new SnapshotProcessor(type, processors, name);
3871
3937
  }
3872
3938
 
3939
+ /**
3940
+ * Append `hooks` to a collection type's initializer list. `EMPTY_ARRAY` is the
3941
+ * shared default, so a type that never calls `.hooks()` allocates nothing.
3942
+ *
3943
+ * @internal
3944
+ * @hidden
3945
+ */
3946
+ function appendHookInitializer(current, hooks) {
3947
+ return current.length > 0 ? [...current, hooks] : [hooks];
3948
+ }
3949
+ /**
3950
+ * Install every hook the type's initializers produce onto `instance` as an MST
3951
+ * action, mirroring how a model's `.actions()` members are attached.
3952
+ *
3953
+ * @internal
3954
+ * @hidden
3955
+ */
3956
+ function installHookInitializers(hookInitializers, instance) {
3957
+ const addProp = !devMode() ? addHiddenFinalProp : addHiddenWritableProp;
3958
+ for (const initializer of hookInitializers) {
3959
+ const hooks = initializer(instance);
3960
+ for (const name of Object.keys(hooks)) {
3961
+ const hook = hooks[name];
3962
+ addProp(instance, name, createActionInvoker(instance, name, hook));
3963
+ }
3964
+ }
3965
+ }
3966
+ /**
3967
+ * The empty initializer list every collection type starts with.
3968
+ *
3969
+ * @internal
3970
+ * @hidden
3971
+ */
3972
+ const NO_HOOK_INITIALIZERS = EMPTY_ARRAY;
3973
+
3873
3974
  const needsIdentifierError = `Map.put can only be used to store complex values that have an identifier type attribute`;
3874
3975
  function tryCollectModelTypes(type, modelTypes) {
3875
3976
  const subtypes = type.getSubTypes();
@@ -3966,21 +4067,21 @@ class MSTMap extends mobx.ObservableMap {
3966
4067
  */
3967
4068
  class MapType extends ComplexType {
3968
4069
  _subType;
4070
+ hookInitializers;
3969
4071
  identifierMode = MapIdentifierMode.UNKNOWN;
3970
4072
  mapIdentifierAttribute = undefined;
3971
4073
  flags = TypeFlags.Map;
3972
- hookInitializers = [];
3973
- constructor(name, _subType, hookInitializers = []) {
3974
- super(name);
4074
+ constructor(_subType, hookInitializers = NO_HOOK_INITIALIZERS) {
4075
+ super();
3975
4076
  this._subType = _subType;
3976
- this._determineIdentifierMode();
3977
4077
  this.hookInitializers = hookInitializers;
4078
+ this._determineIdentifierMode();
4079
+ }
4080
+ computeName() {
4081
+ return `Map<string, ${this._subType.name}>`;
3978
4082
  }
3979
4083
  hooks(hooks) {
3980
- const hookInitializers = this.hookInitializers.length > 0
3981
- ? this.hookInitializers.concat(hooks)
3982
- : [hooks];
3983
- return new MapType(this.name, this._subType, hookInitializers);
4084
+ return new MapType(this._subType, appendHookInitializer(this.hookInitializers, hooks));
3984
4085
  }
3985
4086
  instantiate(parent, subpath, environment, initialValue) {
3986
4087
  this._determineIdentifierMode();
@@ -4023,15 +4124,7 @@ class MapType extends ComplexType {
4023
4124
  }
4024
4125
  finalizeNewInstance(node, instance) {
4025
4126
  mobx._interceptReads(instance, node.unbox);
4026
- const type = node.type;
4027
- type.hookInitializers.forEach(initializer => {
4028
- const hooks = initializer(instance);
4029
- Object.keys(hooks).forEach(name => {
4030
- const hook = hooks[name];
4031
- const actionInvoker = createActionInvoker(instance, name, hook);
4032
- (!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(instance, name, actionInvoker);
4033
- });
4034
- });
4127
+ installHookInitializers(node.type.hookInitializers, instance);
4035
4128
  mobx.intercept(instance, this.willChange);
4036
4129
  mobx.observe(instance, this.didChange);
4037
4130
  }
@@ -4214,7 +4307,7 @@ MapType.prototype.applySnapshot = mobx.action(MapType.prototype.applySnapshot);
4214
4307
  * @returns
4215
4308
  */
4216
4309
  function map(subtype) {
4217
- return new MapType(`Map<string, ${subtype.name}>`, subtype);
4310
+ return new MapType(subtype);
4218
4311
  }
4219
4312
  /**
4220
4313
  * Returns if a given value represents a map type.
@@ -4232,18 +4325,18 @@ function isMapType(type) {
4232
4325
  */
4233
4326
  class ArrayType extends ComplexType {
4234
4327
  _subType;
4328
+ hookInitializers;
4235
4329
  flags = TypeFlags.Array;
4236
- hookInitializers = [];
4237
- constructor(name, _subType, hookInitializers = []) {
4238
- super(name);
4330
+ constructor(_subType, hookInitializers = NO_HOOK_INITIALIZERS) {
4331
+ super();
4239
4332
  this._subType = _subType;
4240
4333
  this.hookInitializers = hookInitializers;
4241
4334
  }
4335
+ computeName() {
4336
+ return `${this._subType.name}[]`;
4337
+ }
4242
4338
  hooks(hooks) {
4243
- const hookInitializers = this.hookInitializers.length > 0
4244
- ? this.hookInitializers.concat(hooks)
4245
- : [hooks];
4246
- return new ArrayType(this.name, this._subType, hookInitializers);
4339
+ return new ArrayType(this._subType, appendHookInitializer(this.hookInitializers, hooks));
4247
4340
  }
4248
4341
  instantiate(parent, subpath, environment, initialValue) {
4249
4342
  return createObjectNode(this, parent, subpath, environment, initialValue);
@@ -4263,15 +4356,7 @@ class ArrayType extends ComplexType {
4263
4356
  }
4264
4357
  finalizeNewInstance(node, instance) {
4265
4358
  mobx._getAdministration(instance).dehancer = node.unbox;
4266
- const type = node.type;
4267
- type.hookInitializers.forEach(initializer => {
4268
- const hooks = initializer(instance);
4269
- Object.keys(hooks).forEach(name => {
4270
- const hook = hooks[name];
4271
- const actionInvoker = createActionInvoker(instance, name, hook);
4272
- (!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(instance, name, actionInvoker);
4273
- });
4274
- });
4359
+ installHookInitializers(node.type.hookInitializers, instance);
4275
4360
  mobx.intercept(instance, this.willChange);
4276
4361
  mobx.observe(instance, this.didChange);
4277
4362
  }
@@ -4292,14 +4377,17 @@ class ArrayType extends ComplexType {
4292
4377
  const node = getStateTreeNode(change.object);
4293
4378
  node.assertWritable({ subpath: `${change.index}` });
4294
4379
  const subType = node.type._subType;
4295
- const childNodes = node.getChildren();
4296
4380
  switch (change.type) {
4297
4381
  case "update":
4298
4382
  {
4299
4383
  if (change.newValue === change.object[change.index]) {
4300
4384
  return null;
4301
4385
  }
4302
- const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], change.index);
4386
+ const updatedNodes = reconcileArrayChildren(node, subType,
4387
+ // only the replaced child is reconciled, so read that one node
4388
+ // directly — `node.getChildren()` copies the whole backing array,
4389
+ // which made a single-element assignment cost O(array length)
4390
+ [node.getChildNode(`${change.index}`)], [change.newValue], change.index);
4303
4391
  if (!updatedNodes) {
4304
4392
  return null;
4305
4393
  }
@@ -4309,6 +4397,7 @@ class ArrayType extends ComplexType {
4309
4397
  case "splice":
4310
4398
  {
4311
4399
  const { index, removedCount, added } = change;
4400
+ const childNodes = node.getChildren();
4312
4401
  const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, index);
4313
4402
  if (!addedNodes) {
4314
4403
  return null;
@@ -4433,7 +4522,7 @@ ArrayType.prototype.applySnapshot = mobx.action(ArrayType.prototype.applySnapsho
4433
4522
  */
4434
4523
  function array(subtype) {
4435
4524
  assertIsType(subtype, 1);
4436
- return new ArrayType(`${subtype.name}[]`, subtype);
4525
+ return new ArrayType(subtype);
4437
4526
  }
4438
4527
  /**
4439
4528
  * @param firstNewPath index the reconciled slice starts at; both call sites
@@ -4448,8 +4537,12 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNew
4448
4537
  // whose id extraction needs type-specific preprocessing (union,
4449
4538
  // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4450
4539
  // `is()` before their id check, so they stay on the scan path.
4540
+ // With at most one old node the scan below is already O(1), so building the
4541
+ // index would only add a Map allocation to every single-element write.
4451
4542
  let idIndex;
4452
- if (childType instanceof ModelType && childType.identifierAttribute) {
4543
+ if (oldNodes.length > 1 &&
4544
+ childType instanceof ModelType &&
4545
+ childType.identifierAttribute) {
4453
4546
  const byId = new Map();
4454
4547
  for (const n of oldNodes) {
4455
4548
  if (n instanceof ObjectNode && n.identifier !== null) {
@@ -4611,14 +4704,52 @@ const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
4611
4704
  function getPropObservable(storedValue, key) {
4612
4705
  return mobx.getAtom(storedValue, key);
4613
4706
  }
4707
+ /**
4708
+ * All of an instance's per-property `ObservableValue`s in one map, or
4709
+ * `undefined` if this mobx does not expose them.
4710
+ *
4711
+ * `observable.object` returns a **Proxy**, and `getAtom(storedValue, key)`
4712
+ * probes it with four `isObservableArray/Set/Map/Object` guards plus a `$mobx`
4713
+ * read — each one a marker-property read that goes through the proxy's `get`
4714
+ * trap. Paying that per property made the trap machinery the bulk of a wide
4715
+ * model's `getSnapshot`; resolving the administration once and reading its
4716
+ * property map directly reduces the per-property cost to a `Map.get`.
4717
+ *
4718
+ * `values_` is mobx-internal, hence the undefined result: every caller keeps a
4719
+ * path built on supported API, so a mobx that drops it degrades to the old
4720
+ * speed rather than breaking.
4721
+ *
4722
+ * The map is complete for our instances. `getAtom` falls back to
4723
+ * `materializeLazy{Computed,Observable}_` because mobx defers constructing an
4724
+ * `ObservableValue` for *decorator* annotations; `createNewInstance` builds
4725
+ * these through `observable.object`, which populates `values_` eagerly for
4726
+ * every declared property.
4727
+ */
4728
+ function getPropObservables(storedValue) {
4729
+ const adm = mobx._getAdministration(storedValue);
4730
+ return adm.values_;
4731
+ }
4614
4732
  function objectTypeToString() {
4615
4733
  return getStateTreeNode(this).toString();
4616
4734
  }
4617
- const defaultObjectOptions = {
4618
- name: "AnonymousModel",
4619
- properties: {},
4620
- initializers: EMPTY_ARRAY
4621
- };
4735
+ const ANONYMOUS_MODEL_NAME = "AnonymousModel";
4736
+ /**
4737
+ * A plain loop rather than `forAllProps`, which allocated a closure and made an
4738
+ * indirect call per property. This runs once per `types.model()` over every
4739
+ * declared property, and jbrowse builds a ~40-slot schema per track.
4740
+ */
4741
+ function findIdentifierAttribute(properties, propertyNames) {
4742
+ let identifierAttribute = undefined;
4743
+ for (const propName of propertyNames) {
4744
+ if (properties[propName].flags & TypeFlags.Identifier) {
4745
+ if (identifierAttribute) {
4746
+ throw fail(`Cannot define property '${propName}' as object identifier, property '${identifierAttribute}' is already defined as identifier property`);
4747
+ }
4748
+ identifierAttribute = propName;
4749
+ }
4750
+ }
4751
+ return identifierAttribute;
4752
+ }
4622
4753
  function toPropertiesObject(declaredProps) {
4623
4754
  // loop through properties and ensures that all items are types
4624
4755
  return Object.keys(declaredProps).reduce((props, key) => {
@@ -4684,14 +4815,29 @@ class ModelType extends ComplexType {
4684
4815
  // to check the first instance we finalize (see finalizeNewInstance)
4685
4816
  duplicateKeysChecked = false;
4686
4817
  constructor(opts) {
4687
- super(opts.name || defaultObjectOptions.name);
4688
- Object.assign(this, defaultObjectOptions, opts);
4818
+ // `??`, not `||`: `types.model("", {})` names the model "". The old
4819
+ // `Object.assign(this, defaults, opts)` below overwrote the `||` fallback
4820
+ // with opts.name afterwards, so that only worked by accident.
4821
+ super(opts.name ?? ANONYMOUS_MODEL_NAME);
4822
+ // Every field is assigned here, unconditionally and in a fixed order,
4823
+ // rather than by `Object.assign(this, defaultObjectOptions, opts)`. `opts`
4824
+ // carries a different key set at each of the three call sites — `model()`,
4825
+ // and cloneAndEnhance's preprocessed / converted paths — so copying it
4826
+ // wholesale gave ModelType three hidden classes, making every later read of
4827
+ // `type.properties` / `type.propertyNames` (getSnapshot's inner loop, among
4828
+ // others) polymorphic. It also left the internal `propertiesArePreProcessed`
4829
+ // / `propertiesAreConverted` plumbing on the type for its whole lifetime.
4830
+ this.initializers = opts.initializers ?? EMPTY_ARRAY;
4831
+ this.preProcessor = opts.preProcessor;
4832
+ this.postProcessor = opts.postProcessor;
4833
+ const declaredProperties = (opts.properties ?? EMPTY_OBJECT);
4689
4834
  if (opts.propertiesArePreProcessed) {
4690
4835
  // `properties` is a parent type's already-converted + frozen output
4691
4836
  // (chain step with no new props), so its derived propertyNames and
4692
4837
  // identifierAttribute are identical to the parent's — reuse them verbatim
4693
4838
  // (cloneAndEnhance passed them in) instead of re-running Object.keys and
4694
4839
  // the per-prop identifier scan, both O(props), on every step.
4840
+ this.properties = declaredProperties;
4695
4841
  this.propertyNames = opts.propertyNames;
4696
4842
  this.identifierAttribute = opts.identifierAttribute;
4697
4843
  }
@@ -4700,26 +4846,16 @@ class ModelType extends ComplexType {
4700
4846
  // means every value is already a type — the parent's converted bag merged
4701
4847
  // with a freshly converted delta — so skip re-converting. Only raw entry
4702
4848
  // 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();
4849
+ const properties = (opts.propertiesAreConverted
4850
+ ? declaredProperties
4851
+ : toPropertiesObject(declaredProperties));
4852
+ this.properties = properties;
4853
+ freeze(properties); // make sure nobody messes with it
4854
+ const propertyNames = Object.keys(properties);
4855
+ this.propertyNames = propertyNames;
4856
+ this.identifierAttribute = findIdentifierAttribute(properties, propertyNames);
4709
4857
  }
4710
4858
  }
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
4859
  cloneAndEnhance(opts) {
4724
4860
  // Fast path: a chain step that adds no new properties (.actions/.views/
4725
4861
  // .volatile/.named/pre-postProcessor) reuses this type's already-converted +
@@ -4822,19 +4958,7 @@ class ModelType extends ComplexType {
4822
4958
  }
4823
4959
  extend(fn) {
4824
4960
  const initializer = (self) => {
4825
- const { actions, views, state, ...rest } = fn(self);
4826
- for (const key in rest) {
4827
- throw fail(`The \`extend\` function should return an object with a subset of the fields 'actions', 'views' and 'state'. Found invalid key '${key}'`);
4828
- }
4829
- if (state) {
4830
- this.instantiateVolatileState(self, state);
4831
- }
4832
- if (views) {
4833
- this.instantiateViews(self, views);
4834
- }
4835
- if (actions) {
4836
- this.instantiateActions(self, actions);
4837
- }
4961
+ this.applyExtension(self, fn(self), "The `extend` function");
4838
4962
  return self;
4839
4963
  };
4840
4964
  return this.cloneAndEnhance({ initializers: [initializer] });
@@ -4855,9 +4979,17 @@ class ModelType extends ComplexType {
4855
4979
  * are permitted (see extendInstance, which runs this in an action context).
4856
4980
  */
4857
4981
  applyExtensionToInstance(self, extension) {
4982
+ this.applyExtension(self, extension, "extendInstance");
4983
+ }
4984
+ /**
4985
+ * Materializes an `{ actions, views, state }` bundle onto an instance. Shared
4986
+ * by `.extend()` (at creation time) and `applyExtensionToInstance` (on a live
4987
+ * instance); `subject` only names the caller in the invalid-key error.
4988
+ */
4989
+ applyExtension(self, extension, subject) {
4858
4990
  const { actions, views, state, ...rest } = extension;
4859
4991
  for (const key in rest) {
4860
- throw fail(`extendInstance should return an object with a subset of the fields 'actions', 'views' and 'state'. Found invalid key '${key}'`);
4992
+ throw fail(`${subject} should return an object with a subset of the fields 'actions', 'views' and 'state'. Found invalid key '${key}'`);
4861
4993
  }
4862
4994
  if (state) {
4863
4995
  this.instantiateVolatileState(self, state);
@@ -4933,9 +5065,20 @@ class ModelType extends ComplexType {
4933
5065
  }
4934
5066
  finalizeNewInstance(node, instance) {
4935
5067
  addHiddenFinalProp(instance, "toString", objectTypeToString);
4936
- this.forAllProps(name => {
4937
- mobx._interceptReads(instance, name, node.unbox);
4938
- });
5068
+ // `_interceptReads(instance, name, ...)` assigns exactly this `dehancer`,
5069
+ // but reaches the ObservableValue via getAtom — several proxy-trap reads
5070
+ // per property (see getPropObservables). Resolve the map once instead.
5071
+ const observables = getPropObservables(instance);
5072
+ if (observables) {
5073
+ for (const name of this.propertyNames) {
5074
+ observables.get(name).dehancer = node.unbox;
5075
+ }
5076
+ }
5077
+ else {
5078
+ this.forAllProps(name => {
5079
+ mobx._interceptReads(instance, name, node.unbox);
5080
+ });
5081
+ }
4939
5082
  this.initializers.reduce((self, fn) => fn(self), instance);
4940
5083
  // views, actions and volatile share the instance namespace with properties,
4941
5084
  // so a view/action reusing a property name silently clobbers that property's
@@ -4988,9 +5131,18 @@ class ModelType extends ComplexType {
4988
5131
  }
4989
5132
  getChildren(node) {
4990
5133
  const names = this.propertyNames;
5134
+ const storedValue = node.storedValue;
5135
+ const observables = getPropObservables(storedValue);
4991
5136
  const res = new Array(names.length);
4992
5137
  for (let i = 0; i < names.length; i++) {
4993
- res[i] = this.getPropertyNode(node, names[i]);
5138
+ const name = names[i];
5139
+ const childNode = (observables
5140
+ ? observables.get(name)
5141
+ : getPropObservable(storedValue, name))?.raw();
5142
+ if (!childNode) {
5143
+ throw fail(`Node not available for property ${name}`);
5144
+ }
5145
+ res[i] = childNode;
4994
5146
  }
4995
5147
  return res;
4996
5148
  }
@@ -5016,14 +5168,17 @@ class ModelType extends ComplexType {
5016
5168
  const res = {};
5017
5169
  const storedValue = node.storedValue;
5018
5170
  const properties = this.properties;
5171
+ const observables = getPropObservables(storedValue);
5019
5172
  for (const name of this.propertyNames) {
5020
5173
  // One mobx lookup serves both purposes: reportObserved so the snapshot
5021
5174
  // computed recomputes when the child is reassigned (raw() below does not
5022
5175
  // track), and raw() to read the child node. Going through getChildNode
5023
5176
  // would repeat the same lookup for every property.
5024
- const observable = getPropObservable(storedValue, name);
5025
- observable.reportObserved();
5026
- const childNode = observable.raw();
5177
+ const observable = observables
5178
+ ? observables.get(name)
5179
+ : getPropObservable(storedValue, name);
5180
+ observable?.reportObserved();
5181
+ const childNode = observable?.raw();
5027
5182
  if (!childNode) {
5028
5183
  throw fail(`Node not available for property ${name}`);
5029
5184
  }
@@ -5124,7 +5279,7 @@ function model(...args) {
5124
5279
  if (devMode() && typeof args[0] !== "string" && args[1]) {
5125
5280
  throw fail("Model creation failed. First argument must be a string when two arguments are provided");
5126
5281
  }
5127
- const name = typeof args[0] === "string" ? args.shift() : "AnonymousModel";
5282
+ const name = typeof args[0] === "string" ? args.shift() : ANONYMOUS_MODEL_NAME;
5128
5283
  const properties = args.shift() || {};
5129
5284
  return new ModelType({ name, properties });
5130
5285
  }
@@ -5138,7 +5293,7 @@ function model(...args) {
5138
5293
  function compose(...args) {
5139
5294
  // TODO: just join the base type names if no name is provided
5140
5295
  const hasTypename = typeof args[0] === "string";
5141
- const typeName = hasTypename ? args[0] : "AnonymousModel";
5296
+ const typeName = hasTypename ? args[0] : ANONYMOUS_MODEL_NAME;
5142
5297
  if (hasTypename) {
5143
5298
  args.shift();
5144
5299
  }
@@ -5364,6 +5519,12 @@ function getPrimitiveFactoryFromValue(value) {
5364
5519
  /**
5365
5520
  * Returns if a given value represents a primitive type.
5366
5521
  *
5522
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
5523
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
5524
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
5525
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
5526
+ * `isMapType`, `isModelType`) keep their predicate.
5527
+ *
5367
5528
  * @param type
5368
5529
  * @returns
5369
5530
  */
@@ -5373,6 +5534,8 @@ function isPrimitiveType(type) {
5373
5534
  (TypeFlags.String |
5374
5535
  TypeFlags.Number |
5375
5536
  TypeFlags.Integer |
5537
+ TypeFlags.Float |
5538
+ TypeFlags.Finite |
5376
5539
  TypeFlags.Boolean |
5377
5540
  TypeFlags.Date)) >
5378
5541
  0);
@@ -5426,6 +5589,12 @@ function literal(value) {
5426
5589
  /**
5427
5590
  * Returns if a given value represents a literal type.
5428
5591
  *
5592
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
5593
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
5594
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
5595
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
5596
+ * `isMapType`, `isModelType`) keep their predicate.
5597
+ *
5429
5598
  * @param type
5430
5599
  * @returns
5431
5600
  */
@@ -5505,11 +5674,17 @@ function refinement(...args) {
5505
5674
  /**
5506
5675
  * Returns if a given value is a refinement type.
5507
5676
  *
5677
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
5678
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
5679
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
5680
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
5681
+ * `isMapType`, `isModelType`) keep their predicate.
5682
+ *
5508
5683
  * @param type
5509
5684
  * @returns
5510
5685
  */
5511
5686
  function isRefinementType(type) {
5512
- return (type.flags & TypeFlags.Refinement) > 0;
5687
+ return isType(type) && (type.flags & TypeFlags.Refinement) > 0;
5513
5688
  }
5514
5689
 
5515
5690
  /**
@@ -5550,10 +5725,23 @@ function enumeration(name, options) {
5550
5725
  // the scoping never engages and every failure prints every member's full
5551
5726
  // structure. Wrappers expose their child as `_subtype` (optional/refinement/
5552
5727
  // snapshotProcessor) or via `getSubType()` (late); bounded to avoid cycles.
5728
+ // Only *successful* resolutions are cached. A wrapper chain's shape is fixed at
5729
+ // construction, so once a member resolves to a ModelType it always will; but a
5730
+ // `late` member reports no subtype until its definition evaluates, and that
5731
+ // miss must stay retryable.
5732
+ const resolvedModelTypes = new WeakMap();
5553
5733
  function resolveModelType(type) {
5734
+ if (!type) {
5735
+ return undefined;
5736
+ }
5737
+ const cached = resolvedModelTypes.get(type);
5738
+ if (cached) {
5739
+ return cached;
5740
+ }
5554
5741
  let current = type;
5555
5742
  for (let depth = 0; current && depth < 20; depth++) {
5556
5743
  if (current instanceof ModelType) {
5744
+ resolvedModelTypes.set(type, current);
5557
5745
  return current;
5558
5746
  }
5559
5747
  const wrapper = current;
@@ -5567,31 +5755,51 @@ function resolveModelType(type) {
5567
5755
  */
5568
5756
  class Union extends BaseType {
5569
5757
  _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.
5758
+ _flags;
5759
+ // Memoized, but only once the fold is known to be stable. A `types.late`
5760
+ // member reports 0 for its subtype until its definition resolves, so a union
5761
+ // containing one must keep recomputing — and every wrapper ORs its subtype's
5762
+ // flags upward, so `Late` in the *result* is an exact test for "some member
5763
+ // may still change" however deeply it is nested. A resolved late still
5764
+ // reports Late, so such a union simply never caches; that is conservative in
5765
+ // the safe direction and no real-world union is built out of late members.
5766
+ //
5767
+ // This reverses an earlier decision to leave it uncached, which was sized at
5768
+ // ~4% "on union creation alone". That understated it: the reads are what
5769
+ // cost, not the creation. `ModelType._getIdentifierAttribute` folds every
5770
+ // property's flags on each `types.model()`, and jbrowse's config slots are
5771
+ // unions under a stripDefault, so building one schema re-folded every slot's
5772
+ // union. See agent-docs/adr/0003.
5577
5773
  get flags() {
5774
+ const cached = this._flags;
5775
+ if (cached !== undefined) {
5776
+ return cached;
5777
+ }
5578
5778
  let result = TypeFlags.Union;
5579
5779
  for (const type of this._types) {
5580
5780
  result |= type.flags;
5581
5781
  }
5782
+ if (!(result & TypeFlags.Late)) {
5783
+ this._flags = result;
5784
+ }
5582
5785
  return result;
5583
5786
  }
5584
- constructor(name, _types, options) {
5585
- super(name);
5787
+ computeName() {
5788
+ return `(${this._types.map(type => type.name).join(" | ")})`;
5789
+ }
5790
+ constructor(_types, options) {
5791
+ super();
5586
5792
  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;
5793
+ // read the two options directly rather than spreading defaults into a fresh
5794
+ // object: this constructor runs once per config slot in jbrowse, and the
5795
+ // merged object was allocated only to be read twice and dropped
5796
+ if (options !== undefined) {
5797
+ if (options.dispatcher !== undefined) {
5798
+ this._dispatcher = options.dispatcher;
5799
+ }
5800
+ if (options.eager === false) {
5801
+ this._eager = false;
5802
+ }
5595
5803
  }
5596
5804
  }
5597
5805
  isAssignableFrom(type) {
@@ -5642,14 +5850,6 @@ class Union extends BaseType {
5642
5850
  }
5643
5851
  return `${baseWithDiscriminator}:\n ${formatValidationErrorLines(errors).join("\n ")}`;
5644
5852
  }
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
5853
  _findCandidateByTypeDiscriminator(discriminator) {
5654
5854
  const cache = (this._discriminatorCache ??= new Map());
5655
5855
  if (cache.has(discriminator)) {
@@ -5684,11 +5884,6 @@ class Union extends BaseType {
5684
5884
  }
5685
5885
  return found;
5686
5886
  }
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
5887
  allMembersDiscriminated() {
5693
5888
  if (this._allMembersDiscriminated === undefined) {
5694
5889
  this._allMembersDiscriminated = this._types.every(t => {
@@ -5786,18 +5981,20 @@ class Union extends BaseType {
5786
5981
  // use cached propertyNames from ModelType instead of Object.keys()
5787
5982
  for (const key of model.propertyNames) {
5788
5983
  const propType = props[key];
5789
- const isOptional = propType.flags & TypeFlags.Optional;
5984
+ // `flags` is a recomputed getter on the wrapper types (optional,
5985
+ // snapshotProcessor, late, union), so read it once per property
5986
+ const flags = propType.flags;
5790
5987
  const propValue = value[key];
5791
5988
  // check required properties exist and are not undefined
5792
5989
  // (unless the type accepts undefined, which Optional types do)
5793
- if (!isOptional) {
5990
+ if (!(flags & TypeFlags.Optional)) {
5794
5991
  if (!(key in value) || propValue === undefined) {
5795
5992
  return false;
5796
5993
  }
5797
5994
  }
5798
5995
  // for literal types, verify the value matches exactly
5799
5996
  // this is critical for discriminated unions
5800
- if (propType.flags & TypeFlags.Literal) {
5997
+ if (flags & TypeFlags.Literal) {
5801
5998
  if (!propType.is(propValue)) {
5802
5999
  return false;
5803
6000
  }
@@ -5820,7 +6017,8 @@ class Union extends BaseType {
5820
6017
  // - A failure is the definitive, scoped error only when every member is
5821
6018
  // discriminated; otherwise a catch-all could still accept the value, so
5822
6019
  // fall through to full validation.
5823
- if (isPlainObject(value) && !isStateTreeNode(value)) {
6020
+ const isSnapshotObject = isPlainObject(value) && !isStateTreeNode(value);
6021
+ if (isSnapshotObject) {
5824
6022
  const discriminator = value.type;
5825
6023
  if (typeof discriminator === "string") {
5826
6024
  const candidate = this._findCandidateByTypeDiscriminator(discriminator);
@@ -5836,7 +6034,7 @@ class Union extends BaseType {
5836
6034
  // for plain-object snapshots, prefer union members whose literal-typed
5837
6035
  // discriminator properties match the value (e.g. {type: "MsaView"})
5838
6036
  // so error output is scoped to the intended branch instead of every member
5839
- const candidates = isPlainObject(value) && !isStateTreeNode(value)
6037
+ const candidates = isSnapshotObject
5840
6038
  ? this._types.filter(t => this.snapshotLooksLikeType(value, t))
5841
6039
  : [];
5842
6040
  const typesToValidate = candidates.length > 0 ? candidates : this._types;
@@ -5865,6 +6063,14 @@ class Union extends BaseType {
5865
6063
  return this._types;
5866
6064
  }
5867
6065
  }
6066
+ // Defaults shared by every union; see the field declarations at the top of the
6067
+ // class for why they are not own slots.
6068
+ Object.assign(Union.prototype, {
6069
+ _dispatcher: undefined,
6070
+ _eager: true,
6071
+ _discriminatorCache: undefined,
6072
+ _allMembersDiscriminated: undefined
6073
+ });
5868
6074
  /**
5869
6075
  * `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
6076
  *
@@ -5872,12 +6078,18 @@ class Union extends BaseType {
5872
6078
  * @param otherTypes
5873
6079
  * @returns
5874
6080
  */
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(" | ")})`;
6081
+ function union(...args) {
6082
+ // One rest array, handed straight to the Union in the common case. Splitting
6083
+ // the leading argument out — whether by a `(first, ...rest)` signature that
6084
+ // rebuilds `[first, ...rest]`, or by `rest.unshift(first)` — allocates a
6085
+ // second array per union, and jbrowse builds one union per config slot. (The
6086
+ // `unshift` form is worse still: V8 inlines the spread and calls out to the
6087
+ // builtin, so it measured 3x the spread on a two-member union.) Only the
6088
+ // options overload, which nothing hot uses, pays for a copy.
6089
+ const firstIsType = isType(args[0]);
6090
+ const options = firstIsType ? undefined : args[0];
6091
+ const types = (firstIsType ? args : args.slice(1));
6092
+ // the name is folded from the members on demand — see Union.computeName
5881
6093
  // check all options
5882
6094
  if (devMode()) {
5883
6095
  if (options) {
@@ -5887,16 +6099,22 @@ function union(optionsOrType, ...otherTypes) {
5887
6099
  assertIsType(type, options ? i + 2 : i + 1);
5888
6100
  });
5889
6101
  }
5890
- return new Union(name, types, options);
6102
+ return new Union(types, options);
5891
6103
  }
5892
6104
  /**
5893
6105
  * Returns if a given value represents a union type.
5894
6106
  *
6107
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6108
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6109
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6110
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
6111
+ * `isMapType`, `isModelType`) keep their predicate.
6112
+ *
5895
6113
  * @param type
5896
6114
  * @returns
5897
6115
  */
5898
6116
  function isUnionType(type) {
5899
- return (type.flags & TypeFlags.Union) > 0;
6117
+ return isType(type) && (type.flags & TypeFlags.Union) > 0;
5900
6118
  }
5901
6119
  /**
5902
6120
  * Returns the member types of a union.
@@ -5934,15 +6152,31 @@ class OptionalValue extends BaseType {
5934
6152
  _subtype;
5935
6153
  _defaultValue;
5936
6154
  optionalValues;
6155
+ _flags;
6156
+ // memoized once stable; see the same guard on Union.flags for why `Late` in
6157
+ // the folded result is the exact test for "may still change"
5937
6158
  get flags() {
5938
- return this._subtype.flags | TypeFlags.Optional;
6159
+ const cached = this._flags;
6160
+ if (cached !== undefined) {
6161
+ return cached;
6162
+ }
6163
+ const result = this._subtype.flags | TypeFlags.Optional;
6164
+ if (!(result & TypeFlags.Late)) {
6165
+ this._flags = result;
6166
+ }
6167
+ return result;
5939
6168
  }
5940
6169
  constructor(_subtype, _defaultValue, optionalValues) {
5941
- super(_subtype.name);
6170
+ // no name argument: reading `_subtype.name` here would force the name of
6171
+ // whatever is wrapped, and jbrowse wraps a union per config slot
6172
+ super();
5942
6173
  this._subtype = _subtype;
5943
6174
  this._defaultValue = _defaultValue;
5944
6175
  this.optionalValues = optionalValues;
5945
6176
  }
6177
+ computeName() {
6178
+ return this._subtype.name;
6179
+ }
5946
6180
  describe() {
5947
6181
  return `${this._subtype.describe()}?`;
5948
6182
  }
@@ -5985,8 +6219,11 @@ class OptionalValue extends BaseType {
5985
6219
  }
5986
6220
  }
5987
6221
  function checkOptionalPreconditions(type, defaultValueOrFunction) {
5988
- // make sure we never pass direct instances
5989
- if (typeof defaultValueOrFunction !== "function" &&
6222
+ // make sure we never pass direct instances. A node is always an object, so
6223
+ // the typeof narrows first: most defaults are primitives, and reading
6224
+ // `$treenode` off a string or a number is a megamorphic miss that this runs
6225
+ // once per config slot.
6226
+ if (typeof defaultValueOrFunction === "object" &&
5990
6227
  isStateTreeNode(defaultValueOrFunction)) {
5991
6228
  throw fail("default value cannot be an instance, pass a snapshot or a function that creates an instance/snapshot instead");
5992
6229
  }
@@ -6051,6 +6288,12 @@ const undefinedAsOptionalValues = [undefined];
6051
6288
  /**
6052
6289
  * Returns if a value represents an optional type.
6053
6290
  *
6291
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6292
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6293
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6294
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
6295
+ * `isMapType`, `isModelType`) keep their predicate.
6296
+ *
6054
6297
  * @template IT
6055
6298
  * @param type
6056
6299
  * @returns
@@ -6136,7 +6379,6 @@ function defaultSnapshotEquals(a, b) {
6136
6379
  * @internal
6137
6380
  */
6138
6381
  class StripDefaultValue extends OptionalValue {
6139
- _defaultSnapshot;
6140
6382
  shouldStripFromSnapshot(snapshot) {
6141
6383
  if (!this._defaultSnapshot) {
6142
6384
  // instantiate the subtype detached with the default and read the node's
@@ -6148,6 +6390,9 @@ class StripDefaultValue extends OptionalValue {
6148
6390
  return defaultSnapshotEquals(snapshot, this._defaultSnapshot.value);
6149
6391
  }
6150
6392
  }
6393
+ Object.assign(StripDefaultValue.prototype, {
6394
+ _defaultSnapshot: undefined
6395
+ });
6151
6396
  /**
6152
6397
  * Whether `type` is a strip-default optional whose current child `snapshot`
6153
6398
  * equals its default and should therefore be omitted from the parent model's
@@ -6295,6 +6540,12 @@ function late(nameOrType, maybeType) {
6295
6540
  /**
6296
6541
  * Returns if a given value represents a late type.
6297
6542
  *
6543
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6544
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6545
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6546
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
6547
+ * `isMapType`, `isModelType`) keep their predicate.
6548
+ *
6298
6549
  * @param type
6299
6550
  * @returns
6300
6551
  */
@@ -6350,7 +6601,13 @@ class Lazy extends SimpleType {
6350
6601
  }
6351
6602
  const node = createScalarNode(this, parent, subpath, environment, deepFreeze(value));
6352
6603
  this.pendingNodeList.push(node);
6353
- mobx.when(() => !node.isAlive, () => this.pendingNodeList.splice(this.pendingNodeList.indexOf(node), 1));
6604
+ mobx.when(() => !node.isAlive, () => {
6605
+ // guard the index: splice(-1, 1) would drop an unrelated pending node
6606
+ const index = this.pendingNodeList.indexOf(node);
6607
+ if (index >= 0) {
6608
+ this.pendingNodeList.splice(index, 1);
6609
+ }
6610
+ });
6354
6611
  return node;
6355
6612
  }
6356
6613
  isValidSnapshot(value, context) {
@@ -6379,9 +6636,12 @@ class Frozen extends SimpleType {
6379
6636
  subType;
6380
6637
  flags = TypeFlags.Frozen;
6381
6638
  constructor(subType) {
6382
- super(subType ? `frozen(${subType.name})` : "frozen");
6639
+ super(subType ? undefined : "frozen");
6383
6640
  this.subType = subType;
6384
6641
  }
6642
+ computeName() {
6643
+ return `frozen(${this.subType.name})`;
6644
+ }
6385
6645
  describe() {
6386
6646
  return "<any immutable value>";
6387
6647
  }
@@ -6454,6 +6714,12 @@ function frozen(arg) {
6454
6714
  /**
6455
6715
  * Returns if a given value represents a frozen type.
6456
6716
  *
6717
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
6718
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
6719
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
6720
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
6721
+ * `isMapType`, `isModelType`) keep their predicate.
6722
+ *
6457
6723
  * @param type
6458
6724
  * @returns
6459
6725
  */
@@ -6537,10 +6803,13 @@ class BaseReferenceType extends SimpleType {
6537
6803
  onInvalidated;
6538
6804
  flags = TypeFlags.Reference;
6539
6805
  constructor(targetType, onInvalidated) {
6540
- super(`reference(${targetType.name})`);
6806
+ super();
6541
6807
  this.targetType = targetType;
6542
6808
  this.onInvalidated = onInvalidated;
6543
6809
  }
6810
+ computeName() {
6811
+ return `reference(${this.targetType.name})`;
6812
+ }
6544
6813
  describe() {
6545
6814
  return this.name;
6546
6815
  }
@@ -6681,9 +6950,6 @@ class BaseReferenceType extends SimpleType {
6681
6950
  * @hidden
6682
6951
  */
6683
6952
  class IdentifierReferenceType extends BaseReferenceType {
6684
- constructor(targetType, onInvalidated) {
6685
- super(targetType, onInvalidated);
6686
- }
6687
6953
  getValue(storedRefNode) {
6688
6954
  if (!storedRefNode.isAlive) {
6689
6955
  return undefined;
@@ -6799,11 +7065,17 @@ function reference(subType, options) {
6799
7065
  /**
6800
7066
  * Returns if a given value represents a reference type.
6801
7067
  *
7068
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
7069
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
7070
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
7071
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
7072
+ * `isMapType`, `isModelType`) keep their predicate.
7073
+ *
6802
7074
  * @param type
6803
7075
  * @returns
6804
7076
  */
6805
7077
  function isReferenceType(type) {
6806
- return (type.flags & TypeFlags.Reference) > 0;
7078
+ return isType(type) && (type.flags & TypeFlags.Reference) > 0;
6807
7079
  }
6808
7080
  /**
6809
7081
  * `types.safeReference` - A safe reference is like a standard reference, except that it accepts the undefined value by default
@@ -6874,7 +7146,6 @@ class BaseIdentifierType extends SimpleType {
6874
7146
  * @hidden
6875
7147
  */
6876
7148
  class IdentifierType extends BaseIdentifierType {
6877
- flags = TypeFlags.Identifier;
6878
7149
  constructor() {
6879
7150
  super(`identifier`, "string");
6880
7151
  }
@@ -6932,6 +7203,12 @@ const identifierNumber = new IdentifierNumberType();
6932
7203
  /**
6933
7204
  * Returns if a given value represents an identifier type.
6934
7205
  *
7206
+ * Returns a plain `boolean`, not a `type is IT` predicate: with the parameter
7207
+ * typed as `IT`, narrowing to `IT` was a no-op in the positive branch while
7208
+ * collapsing the negative one to `never`, so `if (!isX(t)) { t.name }` failed
7209
+ * to compile. Guards with a distinct narrowing target (`isArrayType`,
7210
+ * `isMapType`, `isModelType`) keep their predicate.
7211
+ *
6935
7212
  * @param type
6936
7213
  * @returns
6937
7214
  */
@@ -7072,11 +7349,14 @@ class Resilient extends BaseType {
7072
7349
  return this._subtype.flags;
7073
7350
  }
7074
7351
  constructor(_subtype, _fallbackType, _createFallbackSnapshot) {
7075
- super(`resilient(${_subtype.name})`);
7352
+ super();
7076
7353
  this._subtype = _subtype;
7077
7354
  this._fallbackType = _fallbackType;
7078
7355
  this._createFallbackSnapshot = _createFallbackSnapshot;
7079
7356
  }
7357
+ computeName() {
7358
+ return `resilient(${this._subtype.name})`;
7359
+ }
7080
7360
  describe() {
7081
7361
  return `resilient(${this._subtype.describe()})`;
7082
7362
  }
@@ -7111,12 +7391,20 @@ class Resilient extends BaseType {
7111
7391
  return this._subtype.reconcile(current, newValue, parent, subpath);
7112
7392
  }
7113
7393
  if (this._fallbackType.isAssignableFrom(current.type)) {
7394
+ // `current` holds the fallback. Try the real type again, but keep the
7395
+ // fallback node around while doing so — it is what the catch reconciles.
7396
+ let recovered;
7114
7397
  try {
7115
- return this._subtype.instantiate(parent, subpath, undefined, newValue);
7398
+ recovered = this._subtype.instantiate(parent, subpath, undefined, newValue);
7116
7399
  }
7117
7400
  catch (e) {
7118
7401
  return this._fallbackType.reconcile(current, this._createFallbackSnapshot(e, newValue), parent, subpath);
7119
7402
  }
7403
+ // the fallback node has been replaced, so it has to die: otherwise it
7404
+ // stays alive in the tree and its identifier stays in the root's cache,
7405
+ // where it collides with the recovered node's
7406
+ current.die();
7407
+ return recovered;
7120
7408
  }
7121
7409
  try {
7122
7410
  return this._subtype.reconcile(current, newValue, parent, subpath);