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