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