@jbrowse/mobx-state-tree 5.11.2 → 5.13.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.
@@ -764,10 +764,11 @@ function getMembers(target) {
764
764
  }
765
765
  return;
766
766
  }
767
- if (descriptor.value._isFlowAction === true) {
767
+ const value = descriptor.value;
768
+ if (value?._isFlowAction === true) {
768
769
  reflected.flowActions.push(key);
769
770
  }
770
- if (descriptor.value._isMSTAction === true) {
771
+ if (value?._isMSTAction === true) {
771
772
  reflected.actions.push(key);
772
773
  }
773
774
  else if (isObservableProp(target, key)) {
@@ -1136,6 +1137,10 @@ const snapshotReactionOptions = {
1136
1137
  throw e;
1137
1138
  }
1138
1139
  };
1140
+ // keepAlive lets the snapshot computed memoize its value even with no observer,
1141
+ // so getSnapshot() stays referentially stable without an eager snapshot
1142
+ // reaction serializing the whole tree at create time. Shared: mobx only reads it.
1143
+ const snapshotComputedOptions = { keepAlive: true };
1139
1144
  /**
1140
1145
  * @internal
1141
1146
  * @hidden
@@ -1168,9 +1173,18 @@ class ObjectNode extends BaseNode {
1168
1173
  _cachedInitialSnapshot;
1169
1174
  _cachedInitialSnapshotCreated = false;
1170
1175
  _snapshotComputed;
1176
+ // whether _snapshotComputed has ever run — see createObservableInstance
1177
+ _snapshotComputedEvaluated = false;
1171
1178
  constructor(complexType, parent, subpath, environment, initialValue) {
1172
1179
  super(complexType, parent, subpath, environment);
1173
- this._snapshotComputed = computed(() => freeze(this.getSnapshot()));
1180
+ // keepAlive: memoize the snapshot after the first read even with no
1181
+ // observer, so getSnapshot() is referentially stable (and reconcile-by-
1182
+ // snapshot works) without eagerly serializing the whole tree at create
1183
+ // time — the previous cost, driven by the eager root reaction below.
1184
+ this._snapshotComputed = computed(() => {
1185
+ this._snapshotComputedEvaluated = true;
1186
+ return freeze(this.getSnapshot());
1187
+ }, snapshotComputedOptions);
1174
1188
  this.unbox = this.unbox.bind(this);
1175
1189
  this._initialSnapshot = freeze(initialValue);
1176
1190
  this.identifierAttribute = complexType.identifierAttribute;
@@ -1254,10 +1268,17 @@ class ObjectNode extends BaseNode {
1254
1268
  this._isRunningAction = false;
1255
1269
  }
1256
1270
  this._observableInstanceState = ObservableInstanceLifecycle.CREATED;
1257
- this._snapshotComputed.trackAndCompute();
1258
- if (this.isRoot) {
1259
- this._addSnapshotReaction();
1260
- }
1271
+ // The snapshot computed branches on the non-observable
1272
+ // "_observableInstanceState" flag we just flipped, so a value already
1273
+ // memoized from the initial snapshot is now stale (mobx can't see the
1274
+ // change) — refresh it. When it was never read there is nothing to refresh,
1275
+ // and forcing it here would serialize the whole subtree (the dominant cost
1276
+ // of creating a root) for a value nobody has asked for yet, so we skip it.
1277
+ this.refreshMemoizedSnapshot();
1278
+ // The root's snapshot reaction only drives onSnapshot; set it up lazily
1279
+ // (onSnapshot already does) rather than eagerly here, so a root nobody
1280
+ // subscribes to never pays for a full-tree snapshot at creation. keepAlive
1281
+ // on _snapshotComputed preserves snapshot referential stability without it.
1261
1282
  this._childNodes = EMPTY_OBJECT;
1262
1283
  this.state = NodeLifeCycle.CREATED;
1263
1284
  if (fireHooks) {
@@ -1362,6 +1383,21 @@ class ObjectNode extends BaseNode {
1362
1383
  }
1363
1384
  return this._snapshotComputed.get();
1364
1385
  }
1386
+ /**
1387
+ * @internal
1388
+ * @hidden
1389
+ * Recompute+retrack the keepAlive snapshot computed if it has already run.
1390
+ * Needed whenever something changes what the snapshot would produce without
1391
+ * an observable mobx would track: flipping the (non-observable) observable-
1392
+ * instance state, or SnapshotProcessor rewriting getSnapshot after
1393
+ * construction (see snapshotProcessor._fixNode). If it never ran, there is
1394
+ * nothing cached to go stale, so this is a no-op.
1395
+ */
1396
+ refreshMemoizedSnapshot() {
1397
+ if (this._snapshotComputedEvaluated) {
1398
+ this._snapshotComputed.trackAndCompute();
1399
+ }
1400
+ }
1365
1401
  // NOTE: we use this method to get snapshot without creating @computed overhead
1366
1402
  getSnapshot() {
1367
1403
  if (!this.isAlive) {
@@ -1666,9 +1702,6 @@ ObjectNode.prototype.createObservableInstance = action(ObjectNode.prototype.crea
1666
1702
  ObjectNode.prototype.detach = action(ObjectNode.prototype.detach);
1667
1703
  ObjectNode.prototype.die = action(ObjectNode.prototype.die);
1668
1704
 
1669
- // Cache for validation results to avoid re-validating the same object against the same type
1670
- // Uses WeakMap so cached objects can be garbage collected
1671
- const validationCache = new WeakMap();
1672
1705
  /**
1673
1706
  * @internal
1674
1707
  * @hidden
@@ -1744,27 +1777,7 @@ class BaseType {
1744
1777
  : typeCheckFailure(context, value);
1745
1778
  // it is tempting to compare snapshots, but in that case we should always clone on assignments...
1746
1779
  }
1747
- // check cache for object values (only at root level to avoid context mismatches)
1748
- if (typeof value === "object" && value !== null && context.length === 1) {
1749
- const typeCache = validationCache.get(value);
1750
- if (typeCache) {
1751
- const cached = typeCache.get(this);
1752
- if (cached !== undefined) {
1753
- return cached;
1754
- }
1755
- }
1756
- }
1757
- const result = this.isValidSnapshot(value, context);
1758
- // cache result for object values (only at root level)
1759
- if (typeof value === "object" && value !== null && context.length === 1) {
1760
- let typeCache = validationCache.get(value);
1761
- if (!typeCache) {
1762
- typeCache = new WeakMap();
1763
- validationCache.set(value, typeCache);
1764
- }
1765
- typeCache.set(this, result);
1766
- }
1767
- return result;
1780
+ return this.isValidSnapshot(value, context);
1768
1781
  }
1769
1782
  is(thing) {
1770
1783
  return this.validate(thing, [{ path: "", type: this }]).length === 0;
@@ -3239,8 +3252,6 @@ function addHiddenWritableProp(object, propName, value) {
3239
3252
  */
3240
3253
  class EventHandler {
3241
3254
  handlers = [];
3242
- emitting = false;
3243
- pendingUnregisters = null;
3244
3255
  get hasSubscribers() {
3245
3256
  return this.handlers.length > 0;
3246
3257
  }
@@ -3259,14 +3270,6 @@ class EventHandler {
3259
3270
  return this.handlers.includes(fn);
3260
3271
  }
3261
3272
  unregister(fn) {
3262
- if (this.emitting) {
3263
- // defer unregistration until emit is done
3264
- if (!this.pendingUnregisters) {
3265
- this.pendingUnregisters = [];
3266
- }
3267
- this.pendingUnregisters.push(fn);
3268
- return;
3269
- }
3270
3273
  const index = this.handlers.indexOf(fn);
3271
3274
  if (index >= 0) {
3272
3275
  this.handlers.splice(index, 1);
@@ -3276,26 +3279,10 @@ class EventHandler {
3276
3279
  this.handlers.length = 0;
3277
3280
  }
3278
3281
  emit(...args) {
3279
- // use emitting flag to defer unregistrations instead of copying array
3280
- this.emitting = true;
3281
- try {
3282
- for (const f of this.handlers) {
3283
- f(...args);
3284
- }
3285
- }
3286
- finally {
3287
- this.emitting = false;
3288
- // process any deferred unregistrations
3289
- if (this.pendingUnregisters) {
3290
- for (const fn of this.pendingUnregisters) {
3291
- const index = this.handlers.indexOf(fn);
3292
- if (index >= 0) {
3293
- this.handlers.splice(index, 1);
3294
- }
3295
- }
3296
- this.pendingUnregisters = null;
3297
- }
3298
- }
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));
3299
3286
  }
3300
3287
  }
3301
3288
  /**
@@ -3358,6 +3345,11 @@ let _typeChecking;
3358
3345
  function setTypeChecking(enabled) {
3359
3346
  _typeChecking = enabled;
3360
3347
  }
3348
+ // the ENABLE_TYPE_CHECK env var is fixed at process start, so read it once:
3349
+ // process.env access is a comparatively expensive lookup and isTypeCheckingEnabled
3350
+ // runs on every create() / typed write. The setTypeChecking() override and
3351
+ // dev-mode default stay live below.
3352
+ const _envTypeCheck = typeof process !== "undefined" && process.env?.ENABLE_TYPE_CHECK === "true";
3361
3353
  /**
3362
3354
  * @internal
3363
3355
  * @hidden
@@ -3365,10 +3357,7 @@ function setTypeChecking(enabled) {
3365
3357
  function isTypeCheckingEnabled() {
3366
3358
  // an explicit setTypeChecking() override (incl. `false`) wins over the
3367
3359
  // dev-mode / env-var default, hence ?? rather than ||
3368
- return (_typeChecking ??
3369
- (devMode() ||
3370
- (typeof process !== "undefined" &&
3371
- process.env?.ENABLE_TYPE_CHECK === "true")));
3360
+ return _typeChecking ?? (devMode() || _envTypeCheck);
3372
3361
  }
3373
3362
  let _devMode = process.env.NODE_ENV !== "production";
3374
3363
  /**
@@ -3771,6 +3760,11 @@ class SnapshotProcessor extends BaseType {
3771
3760
  }
3772
3761
  const oldGetSnapshot = node.getSnapshot;
3773
3762
  node.getSnapshot = () => this.postProcessSnapshot(oldGetSnapshot.call(node), node);
3763
+ // getSnapshot was just rewritten; refresh any snapshot already memoized for
3764
+ // this node (e.g. it was serialized before being moved under this processor)
3765
+ if (node instanceof ObjectNode) {
3766
+ node.refreshMemoizedSnapshot();
3767
+ }
3774
3768
  if (!isUnionType(this._subtype)) {
3775
3769
  node.getReconciliationType = () => {
3776
3770
  return this;
@@ -4448,6 +4442,23 @@ function array(subtype) {
4448
4442
  }
4449
4443
  function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths) {
4450
4444
  let nothingChanged = true;
4445
+ // When the element type is a plain model with an identifier, index the old
4446
+ // nodes by id so a moved / replaced child is matched in O(1) instead of the
4447
+ // linear scan in the reorder branch below. This turns a full array
4448
+ // replacement (the common "load new data" case) from O(n^2) into O(n). Types
4449
+ // whose id extraction needs type-specific preprocessing (union,
4450
+ // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4451
+ // `is()` before their id check, so they stay on the scan path.
4452
+ let idIndex;
4453
+ if (childType instanceof ModelType && childType.identifierAttribute) {
4454
+ const byId = new Map();
4455
+ for (const n of oldNodes) {
4456
+ if (n instanceof ObjectNode && n.identifier !== null) {
4457
+ byId.set(n.identifier, n);
4458
+ }
4459
+ }
4460
+ idIndex = { attr: childType.identifierAttribute, byId };
4461
+ }
4451
4462
  for (let i = 0;; i++) {
4452
4463
  const hasNewNode = i <= newValues.length - 1;
4453
4464
  const oldNode = oldNodes[i];
@@ -4491,13 +4502,26 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4491
4502
  oldNodes[i] = valueAsNode(childType, parent, newPath, newValue, oldNode);
4492
4503
  }
4493
4504
  else {
4494
- // nothing to do, try to reorder
4505
+ // nothing to do, try to reorder: find a candidate old node to reuse
4495
4506
  let oldMatch = undefined;
4496
- // find a possible candidate to reuse
4497
- for (let j = i; j < oldNodes.length; j++) {
4498
- if (areSame(oldNodes[j], newValue)) {
4507
+ if (idIndex && isPlainObject(newValue)) {
4508
+ // for an identified element type a plain-object snapshot can only match
4509
+ // by id (it is never a live node, and snapshot-reference equality still
4510
+ // implies the same id), so a miss means "nothing to reuse" without
4511
+ // scanning. areSame still verifies the single candidate.
4512
+ const candidate = idIndex.byId.get(normalizeIdentifier(newValue[idIndex.attr]));
4513
+ const j = candidate ? oldNodes.indexOf(candidate, i) : -1;
4514
+ if (j >= i && areSame(oldNodes[j], newValue)) {
4499
4515
  oldMatch = oldNodes.splice(j, 1)[0];
4500
- break;
4516
+ }
4517
+ }
4518
+ else {
4519
+ // find a possible candidate to reuse
4520
+ for (let j = i; j < oldNodes.length; j++) {
4521
+ if (areSame(oldNodes[j], newValue)) {
4522
+ oldMatch = oldNodes.splice(j, 1)[0];
4523
+ break;
4524
+ }
4501
4525
  }
4502
4526
  }
4503
4527
  nothingChanged = false;
@@ -4657,14 +4681,33 @@ class ModelType extends ComplexType {
4657
4681
  preProcessor;
4658
4682
  postProcessor;
4659
4683
  propertyNames;
4684
+ // member/property name collisions are a property of the type, so we only need
4685
+ // to check the first instance we finalize (see finalizeNewInstance)
4686
+ duplicateKeysChecked = false;
4660
4687
  constructor(opts) {
4661
4688
  super(opts.name || defaultObjectOptions.name);
4662
4689
  Object.assign(this, defaultObjectOptions, opts);
4663
- // ensures that any default value gets converted to its related type
4664
- this.properties = toPropertiesObject(this.properties);
4665
- freeze(this.properties); // make sure nobody messes with it
4666
- this.propertyNames = Object.keys(this.properties);
4667
- this.identifierAttribute = this._getIdentifierAttribute();
4690
+ if (opts.propertiesArePreProcessed) {
4691
+ // `properties` is a parent type's already-converted + frozen output
4692
+ // (chain step with no new props), so its derived propertyNames and
4693
+ // identifierAttribute are identical to the parent's — reuse them verbatim
4694
+ // (cloneAndEnhance passed them in) instead of re-running Object.keys and
4695
+ // the per-prop identifier scan, both O(props), on every step.
4696
+ this.propertyNames = opts.propertyNames;
4697
+ this.identifierAttribute = opts.identifierAttribute;
4698
+ }
4699
+ else {
4700
+ // `propertiesAreConverted` (set by cloneAndEnhance when props are added)
4701
+ // means every value is already a type — the parent's converted bag merged
4702
+ // with a freshly converted delta — so skip re-converting. Only raw entry
4703
+ // points (`model()`) still need the full toPropertiesObject pass.
4704
+ if (!opts.propertiesAreConverted) {
4705
+ this.properties = toPropertiesObject(this.properties);
4706
+ }
4707
+ freeze(this.properties); // make sure nobody messes with it
4708
+ this.propertyNames = Object.keys(this.properties);
4709
+ this.identifierAttribute = this._getIdentifierAttribute();
4710
+ }
4668
4711
  }
4669
4712
  _getIdentifierAttribute() {
4670
4713
  let identifierAttribute = undefined;
@@ -4679,9 +4722,35 @@ class ModelType extends ComplexType {
4679
4722
  return identifierAttribute;
4680
4723
  }
4681
4724
  cloneAndEnhance(opts) {
4725
+ // Fast path: a chain step that adds no new properties (.actions/.views/
4726
+ // .volatile/.named/pre-postProcessor) reuses this type's already-converted +
4727
+ // frozen properties (and their derived names/identifier) verbatim.
4728
+ const hasNewProps = opts.properties !== undefined && Object.keys(opts.properties).length > 0;
4729
+ if (!hasNewProps) {
4730
+ return new ModelType({
4731
+ name: opts.name || this.name,
4732
+ properties: this.properties,
4733
+ propertiesArePreProcessed: true,
4734
+ // safe to share: propertyNames is never mutated after construction
4735
+ propertyNames: this.propertyNames,
4736
+ identifierAttribute: this.identifierAttribute,
4737
+ initializers: this.initializers.concat(opts.initializers || []),
4738
+ preProcessor: opts.preProcessor || this.preProcessor,
4739
+ postProcessor: opts.postProcessor || this.postProcessor
4740
+ });
4741
+ }
4742
+ // Adding props: this type's `properties` is already converted+frozen, so only
4743
+ // the delta needs conversion. compose passes already-converted props
4744
+ // (cur.properties) and flags them; `.props()` passes a raw declaration. Either
4745
+ // way the merged bag is fully converted, so the constructor skips re-converting
4746
+ // the (potentially large) inherited set — the previous hot spot in compose.
4747
+ const convertedNewProps = opts.propertiesAreConverted
4748
+ ? opts.properties
4749
+ : toPropertiesObject(opts.properties);
4682
4750
  return new ModelType({
4683
4751
  name: opts.name || this.name,
4684
- properties: Object.assign({}, this.properties, opts.properties),
4752
+ properties: Object.assign({}, this.properties, convertedNewProps),
4753
+ propertiesAreConverted: true,
4685
4754
  initializers: this.initializers.concat(opts.initializers || []),
4686
4755
  preProcessor: opts.preProcessor || this.preProcessor,
4687
4756
  postProcessor: opts.postProcessor || this.postProcessor
@@ -4778,6 +4847,29 @@ class ModelType extends ComplexType {
4778
4847
  };
4779
4848
  return this.cloneAndEnhance({ initializers: [viewInitializer] });
4780
4849
  }
4850
+ /**
4851
+ * @internal
4852
+ * @hidden
4853
+ * Materializes an additional actions/views/state extension onto an
4854
+ * already-created instance, using the same machinery as `.extend()` runs at
4855
+ * creation time. Powers {@link extendInstance}; callers must ensure the writes
4856
+ * are permitted (see extendInstance, which runs this in an action context).
4857
+ */
4858
+ applyExtensionToInstance(self, extension) {
4859
+ const { actions, views, state, ...rest } = extension;
4860
+ for (const key in rest) {
4861
+ throw fail(`extendInstance should return an object with a subset of the fields 'actions', 'views' and 'state'. Found invalid key '${key}'`);
4862
+ }
4863
+ if (state) {
4864
+ this.instantiateVolatileState(self, state);
4865
+ }
4866
+ if (views) {
4867
+ this.instantiateViews(self, views);
4868
+ }
4869
+ if (actions) {
4870
+ this.instantiateActions(self, actions);
4871
+ }
4872
+ }
4781
4873
  instantiateViews(self, views) {
4782
4874
  // check views return
4783
4875
  if (!isPlainObject(views)) {
@@ -4846,6 +4938,21 @@ class ModelType extends ComplexType {
4846
4938
  _interceptReads(instance, name, node.unbox);
4847
4939
  });
4848
4940
  this.initializers.reduce((self, fn) => fn(self), instance);
4941
+ // views, actions and volatile share the instance namespace with properties,
4942
+ // so a view/action reusing a property name silently clobbers that property's
4943
+ // observable value. It's a type-level mistake (identical for every instance),
4944
+ // so we check just the first one we finalize: a healthy property stays a
4945
+ // plain observable, a shadowing getter becomes a computed, and a shadowing
4946
+ // function/value stops being observable at all. The flag is only set on
4947
+ // success so a broken type keeps throwing on every create.
4948
+ if (!this.duplicateKeysChecked) {
4949
+ this.forAllProps(name => {
4950
+ if (isComputedProp(instance, name) || !isObservableProp(instance, name)) {
4951
+ throw fail(`${name} property is declared twice`);
4952
+ }
4953
+ });
4954
+ this.duplicateKeysChecked = true;
4955
+ }
4849
4956
  intercept(instance, this.willChange);
4850
4957
  observe(instance, this.didChange);
4851
4958
  }
@@ -4900,14 +5007,7 @@ class ModelType extends ComplexType {
4900
5007
  getSnapshot(node, applyPostProcess = true) {
4901
5008
  const res = {};
4902
5009
  this.forAllProps((name, type) => {
4903
- try {
4904
- // TODO: FIXME, make sure the observable ref is used!
4905
- const atom = getAtom(node.storedValue, name);
4906
- atom.reportObserved();
4907
- }
4908
- catch (_e) {
4909
- throw fail(`${name} property is declared twice`);
4910
- }
5010
+ getAtom(node.storedValue, name).reportObserved();
4911
5011
  const snapshot = this.getChildNode(node, name).snapshot;
4912
5012
  // strip-default optionals omit their key when equal to the default
4913
5013
  if (!shouldStripChildFromSnapshot(type, snapshot)) {
@@ -5030,6 +5130,8 @@ function compose(...args) {
5030
5130
  .reduce((prev, cur) => prev.cloneAndEnhance({
5031
5131
  name: `${prev.name}_${cur.name}`,
5032
5132
  properties: cur.properties,
5133
+ // cur.properties is another ModelType's already-converted+frozen bag
5134
+ propertiesAreConverted: true,
5033
5135
  initializers: cur.initializers,
5034
5136
  preProcessor: (snapshot) => cur.applySnapshotPreProcessor(prev.applySnapshotPreProcessor(snapshot)),
5035
5137
  postProcessor: (snapshot) => cur.applySnapshotPostProcessor(prev.applySnapshotPostProcessor(snapshot))
@@ -5045,6 +5147,46 @@ function compose(...args) {
5045
5147
  function isModelType(type) {
5046
5148
  return isType(type) && (type.flags & TypeFlags.Object) > 0;
5047
5149
  }
5150
+ /**
5151
+ * `extendInstance` - attaches additional actions, views and volatile state to an
5152
+ * already-created model instance, using the same instantiation machinery as
5153
+ * `.extend()`. This enables lazily loading a model's views/actions chain: create
5154
+ * the model with only its base props (plus any critical-path members), then attach
5155
+ * the rest at runtime — e.g. from a dynamically imported module — without rebuilding
5156
+ * the type or re-hydrating the tree.
5157
+ *
5158
+ * The extension is applied inside an action context, so it works on protected trees.
5159
+ * Attached views are computeds (reactive, memoized) and attached actions get a full
5160
+ * MST action context, exactly like members declared in the original chain.
5161
+ *
5162
+ * Note: the attached members are NOT part of the instance's *static* type. If you
5163
+ * need them typed, declare the augmented shape separately (an `import type` of the
5164
+ * chain's return type is erased at build time, so it costs nothing in the bundle).
5165
+ *
5166
+ * @param instance a live model instance
5167
+ * @param fn receives the instance and returns `{ actions?, views?, state? }`
5168
+ * @returns the same instance
5169
+ */
5170
+ function extendInstance(instance, fn) {
5171
+ if (!isStateTreeNode(instance)) {
5172
+ throw fail("extendInstance expects a mobx-state-tree node");
5173
+ }
5174
+ const type = getStateTreeNode(instance).type;
5175
+ if (!isModelType(type)) {
5176
+ throw fail("extendInstance can only be used on model instances");
5177
+ }
5178
+ // Views/actions/volatile are installed via defineProperty + makeObservable — the
5179
+ // same "add" operations the initializers perform at creation, before the
5180
+ // write-protection interceptor is attached (see finalizeNewInstance). On a live
5181
+ // instance that interceptor is already active, so run the attach inside an action
5182
+ // context: isRunningAction() then short-circuits assertWritable.
5183
+ const modelType = type;
5184
+ const attach = createActionInvoker(instance, "@@extendInstance", (() => {
5185
+ modelType.applyExtensionToInstance(instance, fn(instance));
5186
+ }));
5187
+ attach();
5188
+ return instance;
5189
+ }
5048
5190
 
5049
5191
  // TODO: implement CoreType using types.custom ?
5050
5192
  /**
@@ -5473,7 +5615,24 @@ class Union extends BaseType {
5473
5615
  }
5474
5616
  return `${baseWithDiscriminator}:\n ${formatValidationErrorLines(errors).join("\n ")}`;
5475
5617
  }
5618
+ // Memoizes the discriminator -> member scan below. Union membership is fixed
5619
+ // at construction, so the result for a given `type` string never changes.
5620
+ // Without this, validating a config with many elements drawn from a wide
5621
+ // pluggable union (e.g. jbrowse's 30+ track/adapter types) re-scans every
5622
+ // member — and calls resolveModelType + literal.is() on each — once per
5623
+ // element. With it, each distinct discriminator scans once; the rest are
5624
+ // O(1) map hits. `undefined` (no match OR ambiguous) is cached too.
5625
+ _discriminatorCache;
5476
5626
  _findCandidateByTypeDiscriminator(discriminator) {
5627
+ const cache = (this._discriminatorCache ??= new Map());
5628
+ if (cache.has(discriminator)) {
5629
+ return cache.get(discriminator);
5630
+ }
5631
+ const found = this._scanForTypeDiscriminator(discriminator);
5632
+ cache.set(discriminator, found);
5633
+ return found;
5634
+ }
5635
+ _scanForTypeDiscriminator(discriminator) {
5477
5636
  let found;
5478
5637
  for (const t of this._types) {
5479
5638
  const model = resolveModelType(t);
@@ -5519,22 +5678,14 @@ class Union extends BaseType {
5519
5678
  if (this._dispatcher) {
5520
5679
  return this._dispatcher(value);
5521
5680
  }
5522
- // fast path: when type checking is disabled, try quick structural matching first
5681
+ // fast path: when type checking is disabled, try quick structural matching
5682
+ // first. This skips full recursive validation of every property value, a
5683
+ // meaningful win for wide model members (e.g. jbrowse config schemas).
5523
5684
  if (!isTypeCheckingEnabled()) {
5524
5685
  const quickMatch = this.tryQuickMatch(value, reconcileCurrentType);
5525
5686
  if (quickMatch) {
5526
5687
  return quickMatch;
5527
5688
  }
5528
- // for plain object snapshots that didn't match via quick path, try all types
5529
- // with quick matching before falling back to full validation
5530
- // (state tree nodes must go through full validation for type identity checks)
5531
- if (isPlainObject(value) && !isStateTreeNode(value)) {
5532
- for (const type of this._types) {
5533
- if (this.snapshotLooksLikeType(value, type)) {
5534
- return type;
5535
- }
5536
- }
5537
- }
5538
5689
  }
5539
5690
  // find the most accomodating type
5540
5691
  // if we are using reconciliation try the current node type first (fix for #1045)
@@ -7002,5 +7153,5 @@ const types = {
7002
7153
  resilient
7003
7154
  };
7004
7155
 
7005
- export { addDisposer, addMiddleware, applyAction, applyPatch, applySnapshot, cast, castFlowReturn, castToReferenceSnapshot, castToSnapshot, clone, createActionTrackingMiddleware, createActionTrackingMiddleware2, decorate, destroy, detach, escapeJsonPath, flow, getChildType, getEnv, getIdentifier, getLivelinessChecking, getMembers, getNodeId, getParent, getParentOfType, getPath, getPathParts, getPropertyMembers, getRelativePath, getRoot, getRunningActionContext, getSnapshot, getType, getUnionSubtypes, hasParent, hasParentOfType, isActionContextChildOf, isActionContextThisOrChildOf, isAlive, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isProtected, isReferenceType, isRefinementType, isRoot, isStateTreeNode, isType, isUnionType, isValidReference, joinJsonPath, onAction, onPatch, onSnapshot, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, setTypeChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, walk };
7156
+ export { addDisposer, addMiddleware, applyAction, applyPatch, applySnapshot, cast, castFlowReturn, castToReferenceSnapshot, castToSnapshot, clone, createActionTrackingMiddleware, createActionTrackingMiddleware2, decorate, destroy, detach, escapeJsonPath, extendInstance, flow, getChildType, getEnv, getIdentifier, getLivelinessChecking, getMembers, getNodeId, getParent, getParentOfType, getPath, getPathParts, getPropertyMembers, getRelativePath, getRoot, getRunningActionContext, getSnapshot, getType, getUnionSubtypes, hasParent, hasParentOfType, isActionContextChildOf, isActionContextThisOrChildOf, isAlive, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isProtected, isReferenceType, isRefinementType, isRoot, isStateTreeNode, isType, isUnionType, isValidReference, joinJsonPath, onAction, onPatch, onSnapshot, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, setTypeChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, walk };
7006
7157
  //# sourceMappingURL=mobx-state-tree.mjs.map