@jbrowse/mobx-state-tree 6.0.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -946,7 +946,6 @@ class BaseNode {
946
946
  constructor(type, parent, subpath, environment) {
947
947
  this.type = type;
948
948
  this.environment = environment;
949
- this.environment = environment;
950
949
  this.baseSetParent(parent, subpath);
951
950
  }
952
951
  getReconciliationType() {
@@ -1168,7 +1167,7 @@ class ObjectNode extends BaseNode {
1168
1167
  }
1169
1168
  _autoUnbox = true; // unboxing is disabled when reading child nodes
1170
1169
  _isRunningAction = false; // only relevant for root
1171
- _hasSnapshotReaction = false;
1170
+ _snapshotReactionDisposer;
1172
1171
  _observableInstanceState = ObservableInstanceLifecycle.UNINITIALIZED;
1173
1172
  _childNodes;
1174
1173
  _initialSnapshot;
@@ -1414,10 +1413,7 @@ class ObjectNode extends BaseNode {
1414
1413
  }
1415
1414
  _getCachedInitialSnapshot() {
1416
1415
  if (!this._cachedInitialSnapshotCreated) {
1417
- const type = this.type;
1418
- const childNodes = this._childNodes;
1419
- const snapshot = this._initialSnapshot;
1420
- this._cachedInitialSnapshot = type.processInitialSnapshot(childNodes, snapshot);
1416
+ this._cachedInitialSnapshot = this.type.processInitialSnapshot(this._childNodes);
1421
1417
  this._cachedInitialSnapshotCreated = true;
1422
1418
  }
1423
1419
  return this._cachedInitialSnapshot;
@@ -1593,7 +1589,17 @@ class ObjectNode extends BaseNode {
1593
1589
  }
1594
1590
  onSnapshot(onChange) {
1595
1591
  this._addSnapshotReaction();
1596
- return this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1592
+ const unregister = this._internalEventsRegister(InternalEvents.Snapshot, onChange);
1593
+ return () => {
1594
+ unregister();
1595
+ // The reaction re-serializes the whole subtree on every change, so leaving
1596
+ // it running once the last listener is gone would keep charging the node
1597
+ // for a snapshot nobody receives. Referential stability of getSnapshot()
1598
+ // does not depend on it — that comes from keepAlive on _snapshotComputed.
1599
+ if (!this._internalEventsHasSubscribers(InternalEvents.Snapshot)) {
1600
+ this._removeSnapshotReaction();
1601
+ }
1602
+ };
1597
1603
  }
1598
1604
  emitSnapshot(snapshot) {
1599
1605
  this._internalEventsEmit(InternalEvents.Snapshot, snapshot);
@@ -1658,12 +1664,24 @@ class ObjectNode extends BaseNode {
1658
1664
  this.type.applyPatchLocally(this, subpath, patch);
1659
1665
  }
1660
1666
  _addSnapshotReaction() {
1661
- if (!this._hasSnapshotReaction) {
1667
+ if (!this._snapshotReactionDisposer) {
1662
1668
  const snapshotDisposer = mobx.reaction(() => this.snapshot, snapshot => this.emitSnapshot(snapshot), snapshotReactionOptions);
1663
1669
  this.addDisposer(snapshotDisposer);
1664
- this._hasSnapshotReaction = true;
1670
+ this._snapshotReactionDisposer = snapshotDisposer;
1665
1671
  }
1666
1672
  }
1673
+ _removeSnapshotReaction() {
1674
+ const disposer = this._snapshotReactionDisposer;
1675
+ if (!disposer) {
1676
+ return;
1677
+ }
1678
+ this._snapshotReactionDisposer = undefined;
1679
+ // not removeDisposer(), which throws when the registration is already gone —
1680
+ // the last listener can be disposed from within a disposer, i.e. after
1681
+ // aboutToDie has cleared them
1682
+ this._internalEventsUnregister(InternalEvents.Dispose, disposer);
1683
+ disposer();
1684
+ }
1667
1685
  // #region internal event handling
1668
1686
  _internalEvents;
1669
1687
  // we proxy the methods to avoid creating an EventHandlers instance when it is not needed
@@ -1739,8 +1757,6 @@ var TypeFlags;
1739
1757
  * @hidden
1740
1758
  */
1741
1759
  const cannotDetermineSubtype = "cannotDetermine";
1742
- /** @hidden */
1743
- const $type = Symbol("$type");
1744
1760
  /**
1745
1761
  * A base type produces a MST node (Node in the state tree)
1746
1762
  *
@@ -1748,16 +1764,39 @@ const $type = Symbol("$type");
1748
1764
  * @hidden
1749
1765
  */
1750
1766
  class BaseType {
1751
- [$type];
1752
- // these are just to make inner types avaialable to inherited classes
1753
- C;
1754
- S;
1755
- T;
1756
- N;
1757
- isType = true;
1758
- name;
1767
+ /**
1768
+ * Builds the name of a type that does not get one handed to it. Composite
1769
+ * types (`union`, `array`, `map`, `reference`, ...) override this to fold
1770
+ * their members' names; see {@link name}. It runs after construction, so
1771
+ * unlike a `super(...)` argument it can read the subclass's own fields.
1772
+ */
1773
+ computeName() {
1774
+ // istanbul ignore next
1775
+ throw fail(`${this.constructor.name} has neither a name nor a computeName`);
1776
+ }
1777
+ /**
1778
+ * Friendly type name.
1779
+ *
1780
+ * A composite type folds its members' names, which for a union is a map +
1781
+ * join over every member — and the result is read only by error messages and
1782
+ * `describe()`. So those types leave it unset and it is built on first read
1783
+ * and cached: a union that never fails a typecheck never builds one, and
1784
+ * jbrowse builds a union per config slot, tens of thousands per session load.
1785
+ *
1786
+ * Deliberately not a lambda passed to the constructor: that allocated a
1787
+ * closure per type (the very cost being avoided) and made two separately
1788
+ * built but equivalent types compare unequal, since the closures differ.
1789
+ */
1790
+ get name() {
1791
+ return (this._name ??= this.computeName());
1792
+ }
1793
+ set name(value) {
1794
+ this._name = value;
1795
+ }
1759
1796
  constructor(name) {
1760
- this.name = name;
1797
+ if (name !== undefined) {
1798
+ this._name = name;
1799
+ }
1761
1800
  }
1762
1801
  create(snapshot, environment) {
1763
1802
  typecheckInternal(this, snapshot);
@@ -1802,6 +1841,9 @@ class BaseType {
1802
1841
  }
1803
1842
  }
1804
1843
  BaseType.prototype.create = mobx.action(BaseType.prototype.create);
1844
+ // Defaults that every type shares, kept off the individual type objects. See
1845
+ // `isType` and `_name` in the class body.
1846
+ Object.assign(BaseType.prototype, { isType: true, _name: undefined });
1805
1847
  /**
1806
1848
  * A complex type produces a MST node (Node in the state tree)
1807
1849
  *
@@ -1810,9 +1852,6 @@ BaseType.prototype.create = mobx.action(BaseType.prototype.create);
1810
1852
  */
1811
1853
  class ComplexType extends BaseType {
1812
1854
  identifierAttribute;
1813
- constructor(name) {
1814
- super(name);
1815
- }
1816
1855
  create(snapshot = this.getDefaultSnapshot(), environment) {
1817
1856
  return super.create(snapshot, environment);
1818
1857
  }
@@ -2130,7 +2169,7 @@ function createActionTrackingMiddleware2(middlewareHooks) {
2130
2169
  };
2131
2170
  }
2132
2171
 
2133
- function serializeArgument(node, actionName, index, arg) {
2172
+ function serializeArgument(arg) {
2134
2173
  if (arg instanceof Date) {
2135
2174
  return { $MST_DATE: arg.getTime() };
2136
2175
  }
@@ -2158,7 +2197,7 @@ function serializeArgument(node, actionName, index, arg) {
2158
2197
  return serializeTheUnserializable(`${e}`);
2159
2198
  }
2160
2199
  }
2161
- function deserializeArgument(adm, value) {
2200
+ function deserializeArgument(value) {
2162
2201
  if (value && typeof value === "object" && "$MST_DATE" in value) {
2163
2202
  return new Date(value["$MST_DATE"]);
2164
2203
  }
@@ -2202,7 +2241,7 @@ function baseApplyAction(target, action) {
2202
2241
  if (!(typeof resolvedTarget[action.name] === "function")) {
2203
2242
  throw fail(`Action '${action.name}' does not exist in '${node.path}'`);
2204
2243
  }
2205
- return resolvedTarget[action.name](...(action.args ? action.args.map(v => deserializeArgument(node, v)) : []));
2244
+ return resolvedTarget[action.name](...(action.args ? action.args.map(deserializeArgument) : []));
2206
2245
  }
2207
2246
  /**
2208
2247
  * Small abstraction around `onAction` and `applyAction`, attaches an action listener to a tree and records all the actions emitted.
@@ -2319,7 +2358,7 @@ function onAction(target, listener, attachAfter = false) {
2319
2358
  const info = {
2320
2359
  name: rawCall.name,
2321
2360
  path: getRelativePathBetweenNodes(getStateTreeNode(target), sourceNode),
2322
- args: rawCall.args.map((arg, index) => serializeArgument(sourceNode, rawCall.name, index, arg))
2361
+ args: rawCall.args.map(serializeArgument)
2323
2362
  };
2324
2363
  if (attachAfter) {
2325
2364
  const res = next(rawCall);
@@ -2714,13 +2753,6 @@ function typeCheckFailure(context, value, message) {
2714
2753
  // Clone context since it may be mutated after this error is created
2715
2754
  return [{ context: context.slice(), value, message }];
2716
2755
  }
2717
- /**
2718
- * @internal
2719
- * @hidden
2720
- */
2721
- function flattenTypeErrors(errors) {
2722
- return errors.flat();
2723
- }
2724
2756
  // TODO; doublecheck: typecheck should only needed to be invoked from: type.create and array / map / value.property will change
2725
2757
  /**
2726
2758
  * @internal
@@ -2782,7 +2814,6 @@ class IdentifierCache {
2782
2814
  // last time the cache (array) for a given time changed
2783
2815
  // n.b. it is not really the time, but just an integer that gets increased after each modification to the array
2784
2816
  lastCacheModificationPerId = mobx.observable.map();
2785
- constructor() { }
2786
2817
  updateLastCacheModificationPerId(identifier) {
2787
2818
  const lcm = this.lastCacheModificationPerId.get(identifier);
2788
2819
  // we start at 1 since 0 means no update since cache creation
@@ -2795,10 +2826,13 @@ class IdentifierCache {
2795
2826
  addNodeToCache(node, lastCacheUpdate = true) {
2796
2827
  if (node.identifierAttribute) {
2797
2828
  const identifier = node.identifier;
2798
- if (!this.cache.has(identifier)) {
2799
- this.cache.set(identifier, mobx.observable.array([], mobxShallow));
2829
+ // one observable-map read, not a has() plus a get(): this runs for every
2830
+ // identified node created
2831
+ let set = this.cache.get(identifier);
2832
+ if (!set) {
2833
+ set = mobx.observable.array([], mobxShallow);
2834
+ this.cache.set(identifier, set);
2800
2835
  }
2801
- const set = this.cache.get(identifier);
2802
2836
  if (set.includes(node)) {
2803
2837
  throw fail(`Already registered`);
2804
2838
  }
@@ -2823,7 +2857,7 @@ class IdentifierCache {
2823
2857
  if (!set.length) {
2824
2858
  this.cache.delete(id);
2825
2859
  }
2826
- this.updateLastCacheModificationPerId(node.identifier);
2860
+ this.updateLastCacheModificationPerId(id);
2827
2861
  }
2828
2862
  }
2829
2863
  }
@@ -3055,15 +3089,8 @@ function convertChildNodesToArray(childNodes) {
3055
3089
  if (!childNodes) {
3056
3090
  return EMPTY_ARRAY;
3057
3091
  }
3058
- const keys = Object.keys(childNodes);
3059
- if (!keys.length) {
3060
- return EMPTY_ARRAY;
3061
- }
3062
- const result = new Array(keys.length);
3063
- keys.forEach((key, index) => {
3064
- result[index] = childNodes[key];
3065
- });
3066
- return result;
3092
+ const result = Object.values(childNodes);
3093
+ return result.length ? result : EMPTY_ARRAY;
3067
3094
  }
3068
3095
 
3069
3096
  const plainObjectString = Object.toString();
@@ -3145,7 +3172,11 @@ function isPlainObject(value) {
3145
3172
  return false;
3146
3173
  }
3147
3174
  const proto = Object.getPrototypeOf(value);
3148
- if (proto == null) {
3175
+ // Fast path for an object literal from this realm — nearly every snapshot MST
3176
+ // inspects. The fallback compares the constructor's *source text* so that an
3177
+ // `Object` from another realm (iframe, vm context) still counts as plain; it
3178
+ // is ~6x slower, so only cross-realm and null-prototype values pay for it.
3179
+ if (proto === Object.prototype || proto == null) {
3149
3180
  return true;
3150
3181
  }
3151
3182
  return proto.constructor?.toString() === plainObjectString;
@@ -3279,10 +3310,19 @@ class EventHandler {
3279
3310
  this.handlers.length = 0;
3280
3311
  }
3281
3312
  emit(...args) {
3282
- // iterate a copy so (un)registrations during emit don't disturb this pass
3283
- // and reentrant emits stay correct
3284
- const handlers = this.handlers.slice();
3285
- handlers.forEach(f => f(...args));
3313
+ const handlers = this.handlers;
3314
+ // 0 and 1 handlers are by far the common cases (a patch/snapshot listener, a
3315
+ // disposer) and need no copy: with a single handler, calling the value we
3316
+ // already read is exactly what iterating a copy of it would do.
3317
+ if (handlers.length <= 1) {
3318
+ handlers[0]?.(...args);
3319
+ return;
3320
+ }
3321
+ // otherwise iterate a copy so (un)registrations during emit don't disturb
3322
+ // this pass and reentrant emits stay correct
3323
+ for (const f of handlers.slice()) {
3324
+ f(...args);
3325
+ }
3286
3326
  }
3287
3327
  }
3288
3328
  /**
@@ -3663,6 +3703,11 @@ function escapeJsonPath(path) {
3663
3703
  * Unescape slashes and backslashes.
3664
3704
  */
3665
3705
  function unescapeJsonPath(path) {
3706
+ // every escape sequence starts with `~`, so bail before the two regex passes
3707
+ // for the overwhelmingly common unescaped part (mirrors escapeJsonPath above)
3708
+ if (!path.includes("~")) {
3709
+ return path;
3710
+ }
3666
3711
  return path.replace(/~1/g, "/").replace(/~0/g, "~");
3667
3712
  }
3668
3713
  /**
@@ -3721,14 +3766,30 @@ const $preProcessorFailed = Symbol("$preProcessorFailed");
3721
3766
  class SnapshotProcessor extends BaseType {
3722
3767
  _subtype;
3723
3768
  _processors;
3769
+ _flags;
3770
+ // memoized once stable; see the guard on Union.flags
3724
3771
  get flags() {
3725
- 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;
3726
3781
  }
3727
3782
  constructor(_subtype, _processors, name) {
3728
- 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);
3729
3787
  this._subtype = _subtype;
3730
3788
  this._processors = _processors;
3731
3789
  }
3790
+ computeName() {
3791
+ return this._subtype.name;
3792
+ }
3732
3793
  describe() {
3733
3794
  return `snapshotProcessor(${this._subtype.describe()})`;
3734
3795
  }
@@ -3978,17 +4039,20 @@ class MapType extends ComplexType {
3978
4039
  mapIdentifierAttribute = undefined;
3979
4040
  flags = TypeFlags.Map;
3980
4041
  hookInitializers = [];
3981
- constructor(name, _subType, hookInitializers = []) {
3982
- super(name);
4042
+ constructor(_subType, hookInitializers = []) {
4043
+ super();
3983
4044
  this._subType = _subType;
3984
4045
  this._determineIdentifierMode();
3985
4046
  this.hookInitializers = hookInitializers;
3986
4047
  }
4048
+ computeName() {
4049
+ return `Map<string, ${this._subType.name}>`;
4050
+ }
3987
4051
  hooks(hooks) {
3988
4052
  const hookInitializers = this.hookInitializers.length > 0
3989
4053
  ? this.hookInitializers.concat(hooks)
3990
4054
  : [hooks];
3991
- return new MapType(this.name, this._subType, hookInitializers);
4055
+ return new MapType(this._subType, hookInitializers);
3992
4056
  }
3993
4057
  instantiate(parent, subpath, environment, initialValue) {
3994
4058
  this._determineIdentifierMode();
@@ -4153,22 +4217,21 @@ class MapType extends ComplexType {
4153
4217
  applySnapshot(node, snapshot) {
4154
4218
  typecheckInternal(this, snapshot);
4155
4219
  const target = node.storedValue;
4156
- const currentKeys = {};
4157
- Array.from(target.keys()).forEach(key => {
4158
- currentKeys[key] = false;
4159
- });
4220
+ // keys present before the apply, minus the ones the snapshot (re)sets — what
4221
+ // remains has to be deleted. A Set rather than a plain object: it needs no
4222
+ // sentinel values, no second pass to find the survivors, and it can hold keys
4223
+ // like "__proto__", which an object literal silently swallows.
4224
+ const staleKeys = new Set(target.keys());
4160
4225
  if (snapshot) {
4161
4226
  // Don't use target.replace, as it will throw away all existing items first
4162
4227
  for (const key in snapshot) {
4163
4228
  target.set(key, snapshot[key]);
4164
- currentKeys[`${key}`] = true;
4229
+ staleKeys.delete(key);
4165
4230
  }
4166
4231
  }
4167
- Object.keys(currentKeys).forEach(key => {
4168
- if (currentKeys[key] === false) {
4169
- target.delete(key);
4170
- }
4171
- });
4232
+ for (const key of staleKeys) {
4233
+ target.delete(key);
4234
+ }
4172
4235
  }
4173
4236
  getChildType() {
4174
4237
  return this._subType;
@@ -4223,7 +4286,7 @@ MapType.prototype.applySnapshot = mobx.action(MapType.prototype.applySnapshot);
4223
4286
  * @returns
4224
4287
  */
4225
4288
  function map(subtype) {
4226
- return new MapType(`Map<string, ${subtype.name}>`, subtype);
4289
+ return new MapType(subtype);
4227
4290
  }
4228
4291
  /**
4229
4292
  * Returns if a given value represents a map type.
@@ -4243,16 +4306,19 @@ class ArrayType extends ComplexType {
4243
4306
  _subType;
4244
4307
  flags = TypeFlags.Array;
4245
4308
  hookInitializers = [];
4246
- constructor(name, _subType, hookInitializers = []) {
4247
- super(name);
4309
+ constructor(_subType, hookInitializers = []) {
4310
+ super();
4248
4311
  this._subType = _subType;
4249
4312
  this.hookInitializers = hookInitializers;
4250
4313
  }
4314
+ computeName() {
4315
+ return `${this._subType.name}[]`;
4316
+ }
4251
4317
  hooks(hooks) {
4252
4318
  const hookInitializers = this.hookInitializers.length > 0
4253
4319
  ? this.hookInitializers.concat(hooks)
4254
4320
  : [hooks];
4255
- return new ArrayType(this.name, this._subType, hookInitializers);
4321
+ return new ArrayType(this._subType, hookInitializers);
4256
4322
  }
4257
4323
  instantiate(parent, subpath, environment, initialValue) {
4258
4324
  return createObjectNode(this, parent, subpath, environment, initialValue);
@@ -4301,14 +4367,17 @@ class ArrayType extends ComplexType {
4301
4367
  const node = getStateTreeNode(change.object);
4302
4368
  node.assertWritable({ subpath: `${change.index}` });
4303
4369
  const subType = node.type._subType;
4304
- const childNodes = node.getChildren();
4305
4370
  switch (change.type) {
4306
4371
  case "update":
4307
4372
  {
4308
4373
  if (change.newValue === change.object[change.index]) {
4309
4374
  return null;
4310
4375
  }
4311
- const updatedNodes = reconcileArrayChildren(node, subType, [childNodes[change.index]], [change.newValue], [change.index]);
4376
+ const updatedNodes = reconcileArrayChildren(node, subType,
4377
+ // only the replaced child is reconciled, so read that one node
4378
+ // directly — `node.getChildren()` copies the whole backing array,
4379
+ // which made a single-element assignment cost O(array length)
4380
+ [node.getChildNode(`${change.index}`)], [change.newValue], change.index);
4312
4381
  if (!updatedNodes) {
4313
4382
  return null;
4314
4383
  }
@@ -4318,7 +4387,8 @@ class ArrayType extends ComplexType {
4318
4387
  case "splice":
4319
4388
  {
4320
4389
  const { index, removedCount, added } = change;
4321
- const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, added.map((_, i) => index + i));
4390
+ const childNodes = node.getChildren();
4391
+ const addedNodes = reconcileArrayChildren(node, subType, childNodes.slice(index, index + removedCount), added, index);
4322
4392
  if (!addedNodes) {
4323
4393
  return null;
4324
4394
  }
@@ -4442,9 +4512,13 @@ ArrayType.prototype.applySnapshot = mobx.action(ArrayType.prototype.applySnapsho
4442
4512
  */
4443
4513
  function array(subtype) {
4444
4514
  assertIsType(subtype, 1);
4445
- return new ArrayType(`${subtype.name}[]`, subtype);
4515
+ return new ArrayType(subtype);
4446
4516
  }
4447
- function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths) {
4517
+ /**
4518
+ * @param firstNewPath index the reconciled slice starts at; both call sites
4519
+ * hand over a contiguous run, so subpath `i` is simply `firstNewPath + i`
4520
+ */
4521
+ function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNewPath) {
4448
4522
  let nothingChanged = true;
4449
4523
  // When the element type is a plain model with an identifier, index the old
4450
4524
  // nodes by id so a moved / replaced child is matched in O(1) instead of the
@@ -4453,8 +4527,12 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4453
4527
  // whose id extraction needs type-specific preprocessing (union,
4454
4528
  // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4455
4529
  // `is()` before their id check, so they stay on the scan path.
4530
+ // With at most one old node the scan below is already O(1), so building the
4531
+ // index would only add a Map allocation to every single-element write.
4456
4532
  let idIndex;
4457
- if (childType instanceof ModelType && childType.identifierAttribute) {
4533
+ if (oldNodes.length > 1 &&
4534
+ childType instanceof ModelType &&
4535
+ childType.identifierAttribute) {
4458
4536
  const byId = new Map();
4459
4537
  for (const n of oldNodes) {
4460
4538
  if (n instanceof ObjectNode && n.identifier !== null) {
@@ -4467,7 +4545,7 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4467
4545
  const hasNewNode = i <= newValues.length - 1;
4468
4546
  const oldNode = oldNodes[i];
4469
4547
  let newValue = hasNewNode ? newValues[i] : undefined;
4470
- const newPath = `${newPaths[i]}`;
4548
+ const newPath = `${firstNewPath + i}`;
4471
4549
  // for some reason, instead of newValue we got a node, fallback to the storedValue
4472
4550
  // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681
4473
4551
  if (isNode(newValue)) {
@@ -4591,12 +4669,14 @@ function areSame(oldNode, newValue) {
4591
4669
  if (!(oldNode instanceof ObjectNode)) {
4592
4670
  return false;
4593
4671
  }
4672
+ if (oldNode.identifier === null ||
4673
+ !oldNode.identifierAttribute ||
4674
+ !isPlainObject(newValue)) {
4675
+ return false;
4676
+ }
4594
4677
  const oldNodeType = oldNode.getReconciliationType();
4595
4678
  // new value is a snapshot with the correct identifier
4596
- return (oldNode.identifier !== null &&
4597
- oldNode.identifierAttribute &&
4598
- isPlainObject(newValue) &&
4599
- oldNodeType.is(newValue) &&
4679
+ return (oldNodeType.is(newValue) &&
4600
4680
  oldNodeType.isMatchingSnapshotId(oldNode, newValue));
4601
4681
  }
4602
4682
  /**
@@ -4611,25 +4691,59 @@ function isArrayType(type) {
4611
4691
 
4612
4692
  const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot";
4613
4693
  const POST_PROCESS_SNAPSHOT = "postProcessSnapshot";
4694
+ function getPropObservable(storedValue, key) {
4695
+ return mobx.getAtom(storedValue, key);
4696
+ }
4697
+ /**
4698
+ * All of an instance's per-property `ObservableValue`s in one map, or
4699
+ * `undefined` if this mobx does not expose them.
4700
+ *
4701
+ * `observable.object` returns a **Proxy**, and `getAtom(storedValue, key)`
4702
+ * probes it with four `isObservableArray/Set/Map/Object` guards plus a `$mobx`
4703
+ * read — each one a marker-property read that goes through the proxy's `get`
4704
+ * trap. Paying that per property made the trap machinery the bulk of a wide
4705
+ * model's `getSnapshot`; resolving the administration once and reading its
4706
+ * property map directly reduces the per-property cost to a `Map.get`.
4707
+ *
4708
+ * `values_` is mobx-internal, hence the undefined result: every caller keeps a
4709
+ * path built on supported API, so a mobx that drops it degrades to the old
4710
+ * speed rather than breaking.
4711
+ *
4712
+ * The map is complete for our instances. `getAtom` falls back to
4713
+ * `materializeLazy{Computed,Observable}_` because mobx defers constructing an
4714
+ * `ObservableValue` for *decorator* annotations; `createNewInstance` builds
4715
+ * these through `observable.object`, which populates `values_` eagerly for
4716
+ * every declared property.
4717
+ */
4718
+ function getPropObservables(storedValue) {
4719
+ const adm = mobx._getAdministration(storedValue);
4720
+ return adm.values_;
4721
+ }
4614
4722
  function objectTypeToString() {
4615
4723
  return getStateTreeNode(this).toString();
4616
4724
  }
4617
4725
  const defaultObjectOptions = {
4618
- name: "AnonymousModel",
4619
- properties: {},
4620
- initializers: EMPTY_ARRAY
4621
- };
4622
- function toPropertiesObject(declaredProps) {
4623
- const keysList = Object.keys(declaredProps);
4624
- const alreadySeenKeys = new Set();
4625
- keysList.forEach(key => {
4626
- if (alreadySeenKeys.has(key)) {
4627
- throw fail(`${key} is declared twice in the model. Model should not contain the same keys`);
4726
+ name: "AnonymousModel"};
4727
+ /**
4728
+ * A plain loop rather than `forAllProps`, which allocated a closure and made an
4729
+ * indirect call per property. This runs once per `types.model()` over every
4730
+ * declared property, and jbrowse builds a ~40-slot schema per track.
4731
+ */
4732
+ function findIdentifierAttribute(properties, propertyNames) {
4733
+ let identifierAttribute = undefined;
4734
+ for (const propName of propertyNames) {
4735
+ if (properties[propName].flags & TypeFlags.Identifier) {
4736
+ if (identifierAttribute) {
4737
+ throw fail(`Cannot define property '${propName}' as object identifier, property '${identifierAttribute}' is already defined as identifier property`);
4738
+ }
4739
+ identifierAttribute = propName;
4628
4740
  }
4629
- alreadySeenKeys.add(key);
4630
- });
4741
+ }
4742
+ return identifierAttribute;
4743
+ }
4744
+ function toPropertiesObject(declaredProps) {
4631
4745
  // loop through properties and ensures that all items are types
4632
- return keysList.reduce((props, key) => {
4746
+ return Object.keys(declaredProps).reduce((props, key) => {
4633
4747
  // warn if user intended a HOOK
4634
4748
  if (key in Hook) {
4635
4749
  throw fail(`Hook '${key}' was defined as property. Hooks should be defined as part of the actions`);
@@ -4669,7 +4783,10 @@ function toPropertiesObject(declaredProps) {
4669
4783
  throw fail(`Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})`);
4670
4784
  }
4671
4785
  return props;
4672
- }, { ...declaredProps });
4786
+ },
4787
+ // seeded with the raw declaration; every key is either replaced by its
4788
+ // converted type below, left alone because it already is one, or throws
4789
+ { ...declaredProps });
4673
4790
  }
4674
4791
  /**
4675
4792
  * @internal
@@ -4689,14 +4806,29 @@ class ModelType extends ComplexType {
4689
4806
  // to check the first instance we finalize (see finalizeNewInstance)
4690
4807
  duplicateKeysChecked = false;
4691
4808
  constructor(opts) {
4692
- super(opts.name || defaultObjectOptions.name);
4693
- Object.assign(this, defaultObjectOptions, opts);
4809
+ // `??`, not `||`: `types.model("", {})` names the model "". The old
4810
+ // `Object.assign(this, defaults, opts)` below overwrote the `||` fallback
4811
+ // with opts.name afterwards, so that only worked by accident.
4812
+ super(opts.name ?? defaultObjectOptions.name);
4813
+ // Every field is assigned here, unconditionally and in a fixed order,
4814
+ // rather than by `Object.assign(this, defaultObjectOptions, opts)`. `opts`
4815
+ // carries a different key set at each of the three call sites — `model()`,
4816
+ // and cloneAndEnhance's preprocessed / converted paths — so copying it
4817
+ // wholesale gave ModelType three hidden classes, making every later read of
4818
+ // `type.properties` / `type.propertyNames` (getSnapshot's inner loop, among
4819
+ // others) polymorphic. It also left the internal `propertiesArePreProcessed`
4820
+ // / `propertiesAreConverted` plumbing on the type for its whole lifetime.
4821
+ this.initializers = opts.initializers ?? EMPTY_ARRAY;
4822
+ this.preProcessor = opts.preProcessor;
4823
+ this.postProcessor = opts.postProcessor;
4824
+ const declaredProperties = (opts.properties ?? EMPTY_OBJECT);
4694
4825
  if (opts.propertiesArePreProcessed) {
4695
4826
  // `properties` is a parent type's already-converted + frozen output
4696
4827
  // (chain step with no new props), so its derived propertyNames and
4697
4828
  // identifierAttribute are identical to the parent's — reuse them verbatim
4698
4829
  // (cloneAndEnhance passed them in) instead of re-running Object.keys and
4699
4830
  // the per-prop identifier scan, both O(props), on every step.
4831
+ this.properties = declaredProperties;
4700
4832
  this.propertyNames = opts.propertyNames;
4701
4833
  this.identifierAttribute = opts.identifierAttribute;
4702
4834
  }
@@ -4705,26 +4837,16 @@ class ModelType extends ComplexType {
4705
4837
  // means every value is already a type — the parent's converted bag merged
4706
4838
  // with a freshly converted delta — so skip re-converting. Only raw entry
4707
4839
  // points (`model()`) still need the full toPropertiesObject pass.
4708
- if (!opts.propertiesAreConverted) {
4709
- this.properties = toPropertiesObject(this.properties);
4710
- }
4711
- freeze(this.properties); // make sure nobody messes with it
4712
- this.propertyNames = Object.keys(this.properties);
4713
- this.identifierAttribute = this._getIdentifierAttribute();
4840
+ const properties = (opts.propertiesAreConverted
4841
+ ? declaredProperties
4842
+ : toPropertiesObject(declaredProperties));
4843
+ this.properties = properties;
4844
+ freeze(properties); // make sure nobody messes with it
4845
+ const propertyNames = Object.keys(properties);
4846
+ this.propertyNames = propertyNames;
4847
+ this.identifierAttribute = findIdentifierAttribute(properties, propertyNames);
4714
4848
  }
4715
4849
  }
4716
- _getIdentifierAttribute() {
4717
- let identifierAttribute = undefined;
4718
- this.forAllProps((propName, propType) => {
4719
- if (propType.flags & TypeFlags.Identifier) {
4720
- if (identifierAttribute) {
4721
- throw fail(`Cannot define property '${propName}' as object identifier, property '${identifierAttribute}' is already defined as identifier property`);
4722
- }
4723
- identifierAttribute = propName;
4724
- }
4725
- });
4726
- return identifierAttribute;
4727
- }
4728
4850
  cloneAndEnhance(opts) {
4729
4851
  // Fast path: a chain step that adds no new properties (.actions/.views/
4730
4852
  // .volatile/.named/pre-postProcessor) reuses this type's already-converted +
@@ -4938,9 +5060,20 @@ class ModelType extends ComplexType {
4938
5060
  }
4939
5061
  finalizeNewInstance(node, instance) {
4940
5062
  addHiddenFinalProp(instance, "toString", objectTypeToString);
4941
- this.forAllProps(name => {
4942
- mobx._interceptReads(instance, name, node.unbox);
4943
- });
5063
+ // `_interceptReads(instance, name, ...)` assigns exactly this `dehancer`,
5064
+ // but reaches the ObservableValue via getAtom — several proxy-trap reads
5065
+ // per property (see getPropObservables). Resolve the map once instead.
5066
+ const observables = getPropObservables(instance);
5067
+ if (observables) {
5068
+ for (const name of this.propertyNames) {
5069
+ observables.get(name).dehancer = node.unbox;
5070
+ }
5071
+ }
5072
+ else {
5073
+ this.forAllProps(name => {
5074
+ mobx._interceptReads(instance, name, node.unbox);
5075
+ });
5076
+ }
4944
5077
  this.initializers.reduce((self, fn) => fn(self), instance);
4945
5078
  // views, actions and volatile share the instance namespace with properties,
4946
5079
  // so a view/action reusing a property name silently clobbers that property's
@@ -4951,7 +5084,8 @@ class ModelType extends ComplexType {
4951
5084
  // success so a broken type keeps throwing on every create.
4952
5085
  if (!this.duplicateKeysChecked) {
4953
5086
  this.forAllProps(name => {
4954
- if (mobx.isComputedProp(instance, name) || !mobx.isObservableProp(instance, name)) {
5087
+ if (mobx.isComputedProp(instance, name) ||
5088
+ !mobx.isObservableProp(instance, name)) {
4955
5089
  throw fail(`${name} property is declared twice`);
4956
5090
  }
4957
5091
  });
@@ -4991,33 +5125,64 @@ class ModelType extends ComplexType {
4991
5125
  }
4992
5126
  }
4993
5127
  getChildren(node) {
4994
- const res = [];
4995
- this.forAllProps(name => {
4996
- res.push(this.getChildNode(node, name));
4997
- });
5128
+ const names = this.propertyNames;
5129
+ const storedValue = node.storedValue;
5130
+ const observables = getPropObservables(storedValue);
5131
+ const res = new Array(names.length);
5132
+ for (let i = 0; i < names.length; i++) {
5133
+ const name = names[i];
5134
+ const childNode = (observables
5135
+ ? observables.get(name)
5136
+ : getPropObservable(storedValue, name))?.raw();
5137
+ if (!childNode) {
5138
+ throw fail(`Node not available for property ${name}`);
5139
+ }
5140
+ res[i] = childNode;
5141
+ }
4998
5142
  return res;
4999
5143
  }
5000
- getChildNode(node, key) {
5001
- if (!(key in this.properties)) {
5002
- throw fail(`Not a value property: ${key}`);
5003
- }
5004
- const adm = mobx._getAdministration(node.storedValue, key);
5005
- const childNode = adm.raw?.();
5144
+ /**
5145
+ * Same as {@link getChildNode} but for a key already known to be a declared
5146
+ * property (callers iterating `propertyNames`), so the membership guard is
5147
+ * skipped.
5148
+ */
5149
+ getPropertyNode(node, key) {
5150
+ const childNode = getPropObservable(node.storedValue, key).raw();
5006
5151
  if (!childNode) {
5007
5152
  throw fail(`Node not available for property ${key}`);
5008
5153
  }
5009
5154
  return childNode;
5010
5155
  }
5156
+ getChildNode(node, key) {
5157
+ if (!(key in this.properties)) {
5158
+ throw fail(`Not a value property: ${key}`);
5159
+ }
5160
+ return this.getPropertyNode(node, key);
5161
+ }
5011
5162
  getSnapshot(node, applyPostProcess = true) {
5012
5163
  const res = {};
5013
- this.forAllProps((name, type) => {
5014
- mobx.getAtom(node.storedValue, name).reportObserved();
5015
- const snapshot = this.getChildNode(node, name).snapshot;
5164
+ const storedValue = node.storedValue;
5165
+ const properties = this.properties;
5166
+ const observables = getPropObservables(storedValue);
5167
+ for (const name of this.propertyNames) {
5168
+ // One mobx lookup serves both purposes: reportObserved so the snapshot
5169
+ // computed recomputes when the child is reassigned (raw() below does not
5170
+ // track), and raw() to read the child node. Going through getChildNode
5171
+ // would repeat the same lookup for every property.
5172
+ const observable = observables
5173
+ ? observables.get(name)
5174
+ : getPropObservable(storedValue, name);
5175
+ observable?.reportObserved();
5176
+ const childNode = observable?.raw();
5177
+ if (!childNode) {
5178
+ throw fail(`Node not available for property ${name}`);
5179
+ }
5180
+ const snapshot = childNode.snapshot;
5016
5181
  // strip-default optionals omit their key when equal to the default
5017
- if (!shouldStripChildFromSnapshot(type, snapshot)) {
5182
+ if (!shouldStripChildFromSnapshot(properties[name], snapshot)) {
5018
5183
  res[name] = snapshot;
5019
5184
  }
5020
- });
5185
+ }
5021
5186
  if (applyPostProcess) {
5022
5187
  return this.applySnapshotPostProcessor(res);
5023
5188
  }
@@ -5081,7 +5246,10 @@ class ModelType extends ComplexType {
5081
5246
  return typeCheckSuccess();
5082
5247
  }
5083
5248
  forAllProps(fn) {
5084
- this.propertyNames.forEach(key => fn(key, this.properties[key]));
5249
+ const properties = this.properties;
5250
+ for (const key of this.propertyNames) {
5251
+ fn(key, properties[key]);
5252
+ }
5085
5253
  }
5086
5254
  describe() {
5087
5255
  // optimization: cache
@@ -5355,6 +5523,8 @@ function isPrimitiveType(type) {
5355
5523
  (TypeFlags.String |
5356
5524
  TypeFlags.Number |
5357
5525
  TypeFlags.Integer |
5526
+ TypeFlags.Float |
5527
+ TypeFlags.Finite |
5358
5528
  TypeFlags.Boolean |
5359
5529
  TypeFlags.Date)) >
5360
5530
  0);
@@ -5491,7 +5661,7 @@ function refinement(...args) {
5491
5661
  * @returns
5492
5662
  */
5493
5663
  function isRefinementType(type) {
5494
- return (type.flags & TypeFlags.Refinement) > 0;
5664
+ return isType(type) && (type.flags & TypeFlags.Refinement) > 0;
5495
5665
  }
5496
5666
 
5497
5667
  /**
@@ -5532,10 +5702,23 @@ function enumeration(name, options) {
5532
5702
  // the scoping never engages and every failure prints every member's full
5533
5703
  // structure. Wrappers expose their child as `_subtype` (optional/refinement/
5534
5704
  // snapshotProcessor) or via `getSubType()` (late); bounded to avoid cycles.
5705
+ // Only *successful* resolutions are cached. A wrapper chain's shape is fixed at
5706
+ // construction, so once a member resolves to a ModelType it always will; but a
5707
+ // `late` member reports no subtype until its definition evaluates, and that
5708
+ // miss must stay retryable.
5709
+ const resolvedModelTypes = new WeakMap();
5535
5710
  function resolveModelType(type) {
5711
+ if (!type) {
5712
+ return undefined;
5713
+ }
5714
+ const cached = resolvedModelTypes.get(type);
5715
+ if (cached) {
5716
+ return cached;
5717
+ }
5536
5718
  let current = type;
5537
5719
  for (let depth = 0; current && depth < 20; depth++) {
5538
5720
  if (current instanceof ModelType) {
5721
+ resolvedModelTypes.set(type, current);
5539
5722
  return current;
5540
5723
  }
5541
5724
  const wrapper = current;
@@ -5549,26 +5732,51 @@ function resolveModelType(type) {
5549
5732
  */
5550
5733
  class Union extends BaseType {
5551
5734
  _types;
5552
- _dispatcher;
5553
- _eager = true;
5735
+ _flags;
5736
+ // Memoized, but only once the fold is known to be stable. A `types.late`
5737
+ // member reports 0 for its subtype until its definition resolves, so a union
5738
+ // containing one must keep recomputing — and every wrapper ORs its subtype's
5739
+ // flags upward, so `Late` in the *result* is an exact test for "some member
5740
+ // may still change" however deeply it is nested. A resolved late still
5741
+ // reports Late, so such a union simply never caches; that is conservative in
5742
+ // the safe direction and no real-world union is built out of late members.
5743
+ //
5744
+ // This reverses an earlier decision to leave it uncached, which was sized at
5745
+ // ~4% "on union creation alone". That understated it: the reads are what
5746
+ // cost, not the creation. `ModelType._getIdentifierAttribute` folds every
5747
+ // property's flags on each `types.model()`, and jbrowse's config slots are
5748
+ // unions under a stripDefault, so building one schema re-folded every slot's
5749
+ // union. See agent-docs/adr/0003.
5554
5750
  get flags() {
5751
+ const cached = this._flags;
5752
+ if (cached !== undefined) {
5753
+ return cached;
5754
+ }
5555
5755
  let result = TypeFlags.Union;
5556
- this._types.forEach(type => {
5756
+ for (const type of this._types) {
5557
5757
  result |= type.flags;
5558
- });
5758
+ }
5759
+ if (!(result & TypeFlags.Late)) {
5760
+ this._flags = result;
5761
+ }
5559
5762
  return result;
5560
5763
  }
5561
- constructor(name, _types, options) {
5562
- super(name);
5764
+ computeName() {
5765
+ return `(${this._types.map(type => type.name).join(" | ")})`;
5766
+ }
5767
+ constructor(_types, options) {
5768
+ super();
5563
5769
  this._types = _types;
5564
- options = {
5565
- eager: true,
5566
- dispatcher: undefined,
5567
- ...options
5568
- };
5569
- this._dispatcher = options.dispatcher;
5570
- if (!options.eager) {
5571
- this._eager = false;
5770
+ // read the two options directly rather than spreading defaults into a fresh
5771
+ // object: this constructor runs once per config slot in jbrowse, and the
5772
+ // merged object was allocated only to be read twice and dropped
5773
+ if (options !== undefined) {
5774
+ if (options.dispatcher !== undefined) {
5775
+ this._dispatcher = options.dispatcher;
5776
+ }
5777
+ if (options.eager === false) {
5778
+ this._eager = false;
5779
+ }
5572
5780
  }
5573
5781
  }
5574
5782
  isAssignableFrom(type) {
@@ -5619,14 +5827,6 @@ class Union extends BaseType {
5619
5827
  }
5620
5828
  return `${baseWithDiscriminator}:\n ${formatValidationErrorLines(errors).join("\n ")}`;
5621
5829
  }
5622
- // Memoizes the discriminator -> member scan below. Union membership is fixed
5623
- // at construction, so the result for a given `type` string never changes.
5624
- // Without this, validating a config with many elements drawn from a wide
5625
- // pluggable union (e.g. jbrowse's 30+ track/adapter types) re-scans every
5626
- // member — and calls resolveModelType + literal.is() on each — once per
5627
- // element. With it, each distinct discriminator scans once; the rest are
5628
- // O(1) map hits. `undefined` (no match OR ambiguous) is cached too.
5629
- _discriminatorCache;
5630
5830
  _findCandidateByTypeDiscriminator(discriminator) {
5631
5831
  const cache = (this._discriminatorCache ??= new Map());
5632
5832
  if (cache.has(discriminator)) {
@@ -5661,11 +5861,6 @@ class Union extends BaseType {
5661
5861
  }
5662
5862
  return found;
5663
5863
  }
5664
- // True when every member resolves to a model carrying a literal `type`
5665
- // discriminator — i.e. a fully discriminated union, where a snapshot's `type`
5666
- // uniquely identifies the intended member and no untagged catch-all member
5667
- // could also accept it. Cached: membership is fixed at construction.
5668
- _allMembersDiscriminated;
5669
5864
  allMembersDiscriminated() {
5670
5865
  if (this._allMembersDiscriminated === undefined) {
5671
5866
  this._allMembersDiscriminated = this._types.every(t => {
@@ -5697,13 +5892,9 @@ class Union extends BaseType {
5697
5892
  if (reconcileCurrentType.is(value)) {
5698
5893
  return reconcileCurrentType;
5699
5894
  }
5700
- return this._types
5701
- .filter(t => t !== reconcileCurrentType)
5702
- .find(type => type.is(value));
5703
- }
5704
- else {
5705
- return this._types.find(type => type.is(value));
5895
+ return this._types.find(type => type !== reconcileCurrentType && type.is(value));
5706
5896
  }
5897
+ return this._types.find(type => type.is(value));
5707
5898
  }
5708
5899
  tryQuickMatch(value, reconcileCurrentType) {
5709
5900
  // state tree nodes need full type compatibility checking
@@ -5715,15 +5906,17 @@ class Union extends BaseType {
5715
5906
  if (!isPlainObject(value)) {
5716
5907
  return this.tryMatchPrimitive(value);
5717
5908
  }
5718
- // for objects, try structural matching against model types
5719
- const typesToCheck = reconcileCurrentType
5720
- ? [
5721
- reconcileCurrentType,
5722
- ...this._types.filter(t => t !== reconcileCurrentType)
5723
- ]
5724
- : this._types;
5725
- for (const type of typesToCheck) {
5726
- if (this.snapshotLooksLikeType(value, type)) {
5909
+ // for objects, try structural matching against model types, preferring the
5910
+ // reconciliation type (checked first, then skipped in the main pass) so a
5911
+ // reconcile keeps its current member when it still fits — without building
5912
+ // the reordered candidate list this used to allocate on every call
5913
+ if (reconcileCurrentType &&
5914
+ this.snapshotLooksLikeType(value, reconcileCurrentType)) {
5915
+ return reconcileCurrentType;
5916
+ }
5917
+ for (const type of this._types) {
5918
+ if (type !== reconcileCurrentType &&
5919
+ this.snapshotLooksLikeType(value, type)) {
5727
5920
  return type;
5728
5921
  }
5729
5922
  }
@@ -5765,18 +5958,20 @@ class Union extends BaseType {
5765
5958
  // use cached propertyNames from ModelType instead of Object.keys()
5766
5959
  for (const key of model.propertyNames) {
5767
5960
  const propType = props[key];
5768
- const isOptional = propType.flags & TypeFlags.Optional;
5961
+ // `flags` is a recomputed getter on the wrapper types (optional,
5962
+ // snapshotProcessor, late, union), so read it once per property
5963
+ const flags = propType.flags;
5769
5964
  const propValue = value[key];
5770
5965
  // check required properties exist and are not undefined
5771
5966
  // (unless the type accepts undefined, which Optional types do)
5772
- if (!isOptional) {
5967
+ if (!(flags & TypeFlags.Optional)) {
5773
5968
  if (!(key in value) || propValue === undefined) {
5774
5969
  return false;
5775
5970
  }
5776
5971
  }
5777
5972
  // for literal types, verify the value matches exactly
5778
5973
  // this is critical for discriminated unions
5779
- if (propType.flags & TypeFlags.Literal) {
5974
+ if (flags & TypeFlags.Literal) {
5780
5975
  if (!propType.is(propValue)) {
5781
5976
  return false;
5782
5977
  }
@@ -5838,12 +6033,20 @@ class Union extends BaseType {
5838
6033
  if (applicableTypes === 1) {
5839
6034
  return typeCheckSuccess();
5840
6035
  }
5841
- return typeCheckFailure(context, value, "No type is applicable for the union").concat(flattenTypeErrors(allErrors));
6036
+ return typeCheckFailure(context, value, "No type is applicable for the union").concat(allErrors.flat());
5842
6037
  }
5843
6038
  getSubTypes() {
5844
6039
  return this._types;
5845
6040
  }
5846
6041
  }
6042
+ // Defaults shared by every union; see the field declarations at the top of the
6043
+ // class for why they are not own slots.
6044
+ Object.assign(Union.prototype, {
6045
+ _dispatcher: undefined,
6046
+ _eager: true,
6047
+ _discriminatorCache: undefined,
6048
+ _allMembersDiscriminated: undefined
6049
+ });
5847
6050
  /**
5848
6051
  * `types.union` - Create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function of the form `(snapshot) => Type`.
5849
6052
  *
@@ -5851,12 +6054,18 @@ class Union extends BaseType {
5851
6054
  * @param otherTypes
5852
6055
  * @returns
5853
6056
  */
5854
- function union(optionsOrType, ...otherTypes) {
5855
- const options = isType(optionsOrType) ? undefined : optionsOrType;
5856
- const types = isType(optionsOrType)
5857
- ? [optionsOrType, ...otherTypes]
5858
- : otherTypes;
5859
- const name = `(${types.map(type => type.name).join(" | ")})`;
6057
+ function union(...args) {
6058
+ // One rest array, handed straight to the Union in the common case. Splitting
6059
+ // the leading argument out — whether by a `(first, ...rest)` signature that
6060
+ // rebuilds `[first, ...rest]`, or by `rest.unshift(first)` — allocates a
6061
+ // second array per union, and jbrowse builds one union per config slot. (The
6062
+ // `unshift` form is worse still: V8 inlines the spread and calls out to the
6063
+ // builtin, so it measured 3x the spread on a two-member union.) Only the
6064
+ // options overload, which nothing hot uses, pays for a copy.
6065
+ const firstIsType = isType(args[0]);
6066
+ const options = firstIsType ? undefined : args[0];
6067
+ const types = (firstIsType ? args : args.slice(1));
6068
+ // the name is folded from the members on demand — see Union.computeName
5860
6069
  // check all options
5861
6070
  if (devMode()) {
5862
6071
  if (options) {
@@ -5866,7 +6075,7 @@ function union(optionsOrType, ...otherTypes) {
5866
6075
  assertIsType(type, options ? i + 2 : i + 1);
5867
6076
  });
5868
6077
  }
5869
- return new Union(name, types, options);
6078
+ return new Union(types, options);
5870
6079
  }
5871
6080
  /**
5872
6081
  * Returns if a given value represents a union type.
@@ -5875,7 +6084,7 @@ function union(optionsOrType, ...otherTypes) {
5875
6084
  * @returns
5876
6085
  */
5877
6086
  function isUnionType(type) {
5878
- return (type.flags & TypeFlags.Union) > 0;
6087
+ return isType(type) && (type.flags & TypeFlags.Union) > 0;
5879
6088
  }
5880
6089
  /**
5881
6090
  * Returns the member types of a union.
@@ -5913,15 +6122,31 @@ class OptionalValue extends BaseType {
5913
6122
  _subtype;
5914
6123
  _defaultValue;
5915
6124
  optionalValues;
6125
+ _flags;
6126
+ // memoized once stable; see the same guard on Union.flags for why `Late` in
6127
+ // the folded result is the exact test for "may still change"
5916
6128
  get flags() {
5917
- return this._subtype.flags | TypeFlags.Optional;
6129
+ const cached = this._flags;
6130
+ if (cached !== undefined) {
6131
+ return cached;
6132
+ }
6133
+ const result = this._subtype.flags | TypeFlags.Optional;
6134
+ if (!(result & TypeFlags.Late)) {
6135
+ this._flags = result;
6136
+ }
6137
+ return result;
5918
6138
  }
5919
6139
  constructor(_subtype, _defaultValue, optionalValues) {
5920
- super(_subtype.name);
6140
+ // no name argument: reading `_subtype.name` here would force the name of
6141
+ // whatever is wrapped, and jbrowse wraps a union per config slot
6142
+ super();
5921
6143
  this._subtype = _subtype;
5922
6144
  this._defaultValue = _defaultValue;
5923
6145
  this.optionalValues = optionalValues;
5924
6146
  }
6147
+ computeName() {
6148
+ return this._subtype.name;
6149
+ }
5925
6150
  describe() {
5926
6151
  return `${this._subtype.describe()}?`;
5927
6152
  }
@@ -5938,15 +6163,15 @@ class OptionalValue extends BaseType {
5938
6163
  : this.getDefaultInstanceOrSnapshot(), parent, subpath);
5939
6164
  }
5940
6165
  getDefaultInstanceOrSnapshot() {
5941
- const defaultInstanceOrSnapshot = typeof this._defaultValue === "function"
5942
- ? this._defaultValue()
5943
- : this._defaultValue;
5944
- // while static values are already snapshots and checked on types.optional
5945
- // generator functions must always be rechecked just in case
5946
- if (typeof this._defaultValue === "function") {
5947
- typecheckInternal(this, defaultInstanceOrSnapshot);
6166
+ const defaultValue = this._defaultValue;
6167
+ if (typeof defaultValue !== "function") {
6168
+ // static values are already snapshots, checked once by types.optional
6169
+ return defaultValue;
5948
6170
  }
5949
- return defaultInstanceOrSnapshot;
6171
+ const generated = defaultValue();
6172
+ // generator functions must always be rechecked just in case
6173
+ typecheckInternal(this, generated);
6174
+ return generated;
5950
6175
  }
5951
6176
  isValidSnapshot(value, context) {
5952
6177
  // defaulted values can be skipped
@@ -5964,8 +6189,11 @@ class OptionalValue extends BaseType {
5964
6189
  }
5965
6190
  }
5966
6191
  function checkOptionalPreconditions(type, defaultValueOrFunction) {
5967
- // make sure we never pass direct instances
5968
- if (typeof defaultValueOrFunction !== "function" &&
6192
+ // make sure we never pass direct instances. A node is always an object, so
6193
+ // the typeof narrows first: most defaults are primitives, and reading
6194
+ // `$treenode` off a string or a number is a megamorphic miss that this runs
6195
+ // once per config slot.
6196
+ if (typeof defaultValueOrFunction === "object" &&
5969
6197
  isStateTreeNode(defaultValueOrFunction)) {
5970
6198
  throw fail("default value cannot be an instance, pass a snapshot or a function that creates an instance/snapshot instead");
5971
6199
  }
@@ -6038,33 +6266,70 @@ function isOptionalType(type) {
6038
6266
  return isType(type) && (type.flags & TypeFlags.Optional) > 0;
6039
6267
  }
6040
6268
  /**
6041
- * Compare a child snapshot to a stripped-default's reference snapshot. Mirrors
6042
- * the legacy hand-rolled comparison: identity for primitives, structural for
6043
- * objects/arrays.
6269
+ * Compare a child snapshot to a stripped-default's reference snapshot: identity
6270
+ * for primitives, structural for objects/arrays.
6271
+ *
6272
+ * Walks the two snapshots in parallel and bails at the first difference. The
6273
+ * previous implementation compared `JSON.stringify(a) === JSON.stringify(b)`
6274
+ * behind a size guard, which is O(whole snapshot) on both sides — plus two
6275
+ * string allocations — even when the very first key differs. That is the normal
6276
+ * case for an identified sub-model: its snapshot has exactly the same shape as
6277
+ * the default and differs only in the identifier, so the size guard never fires
6278
+ * and every `getSnapshot` of the parent serialized both trees just to answer
6279
+ * "no".
6280
+ *
6281
+ * Snapshots are frozen, acyclic, JSON-ish plain data, so a plain recursive walk
6282
+ * is safe. Key order is compared too, matching what stringify comparison did:
6283
+ * the point is to answer "is this still the default", and the only unsafe answer
6284
+ * is a false positive (a key gets dropped from the snapshot that shouldn't be),
6285
+ * so where this can't reproduce stringify exactly — `{a: undefined}` vs
6286
+ * `{b: undefined}`, or `NaN` nested in a frozen value, both of which stringify
6287
+ * flattened into equal text — it errs toward "not equal" and simply keeps the
6288
+ * key. Neither shape is reachable from a model snapshot, whose key set and order
6289
+ * are fixed by its type.
6044
6290
  */
6045
6291
  function defaultSnapshotEquals(a, b) {
6046
6292
  if (a === b) {
6047
6293
  return true;
6048
6294
  }
6049
- if (typeof a === "object" &&
6050
- a !== null &&
6051
- typeof b === "object" &&
6052
- b !== null) {
6053
- // Cheap structural short-circuit before the full stringify compare: a
6054
- // value of a different size can't equal the default, so the common strip
6055
- // case (non-empty value vs an empty `[]`/`{}` default) avoids stringifying
6056
- // a potentially large snapshot on every getSnapshot.
6057
- if (Array.isArray(a) !== Array.isArray(b)) {
6295
+ if (typeof a !== "object" ||
6296
+ a === null ||
6297
+ typeof b !== "object" ||
6298
+ b === null) {
6299
+ return false;
6300
+ }
6301
+ const aIsArray = Array.isArray(a);
6302
+ if (aIsArray !== Array.isArray(b)) {
6303
+ return false;
6304
+ }
6305
+ if (aIsArray) {
6306
+ const arrayA = a;
6307
+ const arrayB = b;
6308
+ if (arrayA.length !== arrayB.length) {
6058
6309
  return false;
6059
6310
  }
6060
- const aSize = Array.isArray(a) ? a.length : Object.keys(a).length;
6061
- const bSize = Array.isArray(b) ? b.length : Object.keys(b).length;
6062
- if (aSize !== bSize) {
6311
+ for (let i = 0; i < arrayA.length; i++) {
6312
+ if (!defaultSnapshotEquals(arrayA[i], arrayB[i])) {
6313
+ return false;
6314
+ }
6315
+ }
6316
+ return true;
6317
+ }
6318
+ const objectA = a;
6319
+ const objectB = b;
6320
+ const keysA = Object.keys(objectA);
6321
+ const keysB = Object.keys(objectB);
6322
+ if (keysA.length !== keysB.length) {
6323
+ return false;
6324
+ }
6325
+ for (let i = 0; i < keysA.length; i++) {
6326
+ const key = keysA[i];
6327
+ if (key !== keysB[i] ||
6328
+ !defaultSnapshotEquals(objectA[key], objectB[key])) {
6063
6329
  return false;
6064
6330
  }
6065
- return JSON.stringify(a) === JSON.stringify(b);
6066
6331
  }
6067
- return false;
6332
+ return true;
6068
6333
  }
6069
6334
  /**
6070
6335
  * An optional type that additionally omits its key from a parent model's
@@ -6078,7 +6343,6 @@ function defaultSnapshotEquals(a, b) {
6078
6343
  * @internal
6079
6344
  */
6080
6345
  class StripDefaultValue extends OptionalValue {
6081
- _defaultSnapshot;
6082
6346
  shouldStripFromSnapshot(snapshot) {
6083
6347
  if (!this._defaultSnapshot) {
6084
6348
  // instantiate the subtype detached with the default and read the node's
@@ -6090,6 +6354,9 @@ class StripDefaultValue extends OptionalValue {
6090
6354
  return defaultSnapshotEquals(snapshot, this._defaultSnapshot.value);
6091
6355
  }
6092
6356
  }
6357
+ Object.assign(StripDefaultValue.prototype, {
6358
+ _defaultSnapshot: undefined
6359
+ });
6093
6360
  /**
6094
6361
  * Whether `type` is a strip-default optional whose current child `snapshot`
6095
6362
  * equals its default and should therefore be omitted from the parent model's
@@ -6292,7 +6559,13 @@ class Lazy extends SimpleType {
6292
6559
  }
6293
6560
  const node = createScalarNode(this, parent, subpath, environment, deepFreeze(value));
6294
6561
  this.pendingNodeList.push(node);
6295
- mobx.when(() => !node.isAlive, () => this.pendingNodeList.splice(this.pendingNodeList.indexOf(node), 1));
6562
+ mobx.when(() => !node.isAlive, () => {
6563
+ // guard the index: splice(-1, 1) would drop an unrelated pending node
6564
+ const index = this.pendingNodeList.indexOf(node);
6565
+ if (index >= 0) {
6566
+ this.pendingNodeList.splice(index, 1);
6567
+ }
6568
+ });
6296
6569
  return node;
6297
6570
  }
6298
6571
  isValidSnapshot(value, context) {
@@ -6321,9 +6594,12 @@ class Frozen extends SimpleType {
6321
6594
  subType;
6322
6595
  flags = TypeFlags.Frozen;
6323
6596
  constructor(subType) {
6324
- super(subType ? `frozen(${subType.name})` : "frozen");
6597
+ super(subType ? undefined : "frozen");
6325
6598
  this.subType = subType;
6326
6599
  }
6600
+ computeName() {
6601
+ return `frozen(${this.subType.name})`;
6602
+ }
6327
6603
  describe() {
6328
6604
  return "<any immutable value>";
6329
6605
  }
@@ -6479,10 +6755,13 @@ class BaseReferenceType extends SimpleType {
6479
6755
  onInvalidated;
6480
6756
  flags = TypeFlags.Reference;
6481
6757
  constructor(targetType, onInvalidated) {
6482
- super(`reference(${targetType.name})`);
6758
+ super();
6483
6759
  this.targetType = targetType;
6484
6760
  this.onInvalidated = onInvalidated;
6485
6761
  }
6762
+ computeName() {
6763
+ return `reference(${this.targetType.name})`;
6764
+ }
6486
6765
  describe() {
6487
6766
  return this.name;
6488
6767
  }
@@ -6745,7 +7024,7 @@ function reference(subType, options) {
6745
7024
  * @returns
6746
7025
  */
6747
7026
  function isReferenceType(type) {
6748
- return (type.flags & TypeFlags.Reference) > 0;
7027
+ return isType(type) && (type.flags & TypeFlags.Reference) > 0;
6749
7028
  }
6750
7029
  /**
6751
7030
  * `types.safeReference` - A safe reference is like a standard reference, except that it accepts the undefined value by default
@@ -6816,7 +7095,6 @@ class BaseIdentifierType extends SimpleType {
6816
7095
  * @hidden
6817
7096
  */
6818
7097
  class IdentifierType extends BaseIdentifierType {
6819
- flags = TypeFlags.Identifier;
6820
7098
  constructor() {
6821
7099
  super(`identifier`, "string");
6822
7100
  }
@@ -7014,11 +7292,14 @@ class Resilient extends BaseType {
7014
7292
  return this._subtype.flags;
7015
7293
  }
7016
7294
  constructor(_subtype, _fallbackType, _createFallbackSnapshot) {
7017
- super(`resilient(${_subtype.name})`);
7295
+ super();
7018
7296
  this._subtype = _subtype;
7019
7297
  this._fallbackType = _fallbackType;
7020
7298
  this._createFallbackSnapshot = _createFallbackSnapshot;
7021
7299
  }
7300
+ computeName() {
7301
+ return `resilient(${this._subtype.name})`;
7302
+ }
7022
7303
  describe() {
7023
7304
  return `resilient(${this._subtype.describe()})`;
7024
7305
  }
@@ -7053,12 +7334,20 @@ class Resilient extends BaseType {
7053
7334
  return this._subtype.reconcile(current, newValue, parent, subpath);
7054
7335
  }
7055
7336
  if (this._fallbackType.isAssignableFrom(current.type)) {
7337
+ // `current` holds the fallback. Try the real type again, but keep the
7338
+ // fallback node around while doing so — it is what the catch reconciles.
7339
+ let recovered;
7056
7340
  try {
7057
- return this._subtype.instantiate(parent, subpath, undefined, newValue);
7341
+ recovered = this._subtype.instantiate(parent, subpath, undefined, newValue);
7058
7342
  }
7059
7343
  catch (e) {
7060
7344
  return this._fallbackType.reconcile(current, this._createFallbackSnapshot(e, newValue), parent, subpath);
7061
7345
  }
7346
+ // the fallback node has been replaced, so it has to die: otherwise it
7347
+ // stays alive in the tree and its identifier stays in the root's cache,
7348
+ // where it collides with the recovered node's
7349
+ current.die();
7350
+ return recovered;
7062
7351
  }
7063
7352
  try {
7064
7353
  return this._subtype.reconcile(current, newValue, parent, subpath);