@jbrowse/mobx-state-tree 5.4.7 → 5.6.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.
@@ -1654,6 +1654,9 @@ ObjectNode.prototype.createObservableInstance = action(ObjectNode.prototype.crea
1654
1654
  ObjectNode.prototype.detach = action(ObjectNode.prototype.detach);
1655
1655
  ObjectNode.prototype.die = action(ObjectNode.prototype.die);
1656
1656
 
1657
+ // Cache for validation results to avoid re-validating the same object against the same type
1658
+ // Uses WeakMap so cached objects can be garbage collected
1659
+ const validationCache = new WeakMap();
1657
1660
  /**
1658
1661
  * @internal
1659
1662
  * @hidden
@@ -1729,7 +1732,27 @@ class BaseType {
1729
1732
  : typeCheckFailure(context, value);
1730
1733
  // it is tempting to compare snapshots, but in that case we should always clone on assignments...
1731
1734
  }
1732
- return this.isValidSnapshot(value, context);
1735
+ // check cache for object values (only at root level to avoid context mismatches)
1736
+ if (typeof value === "object" && value !== null && context.length === 1) {
1737
+ const typeCache = validationCache.get(value);
1738
+ if (typeCache) {
1739
+ const cached = typeCache.get(this);
1740
+ if (cached !== undefined) {
1741
+ return cached;
1742
+ }
1743
+ }
1744
+ }
1745
+ const result = this.isValidSnapshot(value, context);
1746
+ // cache result for object values (only at root level)
1747
+ if (typeof value === "object" && value !== null && context.length === 1) {
1748
+ let typeCache = validationCache.get(value);
1749
+ if (!typeCache) {
1750
+ typeCache = new WeakMap();
1751
+ validationCache.set(value, typeCache);
1752
+ }
1753
+ typeCache.set(this, result);
1754
+ }
1755
+ return result;
1733
1756
  }
1734
1757
  is(thing) {
1735
1758
  return this.validate(thing, [{ path: "", type: this }]).length === 0;
@@ -2594,9 +2617,21 @@ function toErrorString(error) {
2594
2617
  /**
2595
2618
  * @internal
2596
2619
  * @hidden
2620
+ * Pushes a new entry onto the context array (mutates in place for performance).
2621
+ * Returns the same context array for chaining.
2597
2622
  */
2598
2623
  function getContextForPath(context, path, type) {
2599
- return context.concat([{ path, type }]);
2624
+ context.push({ path, type });
2625
+ return context;
2626
+ }
2627
+ /**
2628
+ * @internal
2629
+ * @hidden
2630
+ * Pops the last entry from the context array (mutates in place).
2631
+ * Must be called after validation to restore context state.
2632
+ */
2633
+ function popContext(context) {
2634
+ context.pop();
2600
2635
  }
2601
2636
  /**
2602
2637
  * @internal
@@ -2610,14 +2645,15 @@ function typeCheckSuccess() {
2610
2645
  * @hidden
2611
2646
  */
2612
2647
  function typeCheckFailure(context, value, message) {
2613
- return [{ context, value, message }];
2648
+ // Clone context since it may be mutated after this error is created
2649
+ return [{ context: context.slice(), value, message }];
2614
2650
  }
2615
2651
  /**
2616
2652
  * @internal
2617
2653
  * @hidden
2618
2654
  */
2619
2655
  function flattenTypeErrors(errors) {
2620
- return errors.reduce((a, i) => a.concat(i), []);
2656
+ return errors.flat();
2621
2657
  }
2622
2658
  // TODO; doublecheck: typecheck should only needed to be invoked from: type.create and array / map / value.property will change
2623
2659
  /**
@@ -3172,6 +3208,8 @@ function addHiddenWritableProp(object, propName, value) {
3172
3208
  */
3173
3209
  class EventHandler {
3174
3210
  handlers = [];
3211
+ emitting = false;
3212
+ pendingUnregisters = null;
3175
3213
  get hasSubscribers() {
3176
3214
  return this.handlers.length > 0;
3177
3215
  }
@@ -3190,6 +3228,14 @@ class EventHandler {
3190
3228
  return this.handlers.indexOf(fn) >= 0;
3191
3229
  }
3192
3230
  unregister(fn) {
3231
+ if (this.emitting) {
3232
+ // defer unregistration until emit is done
3233
+ if (!this.pendingUnregisters) {
3234
+ this.pendingUnregisters = [];
3235
+ }
3236
+ this.pendingUnregisters.push(fn);
3237
+ return;
3238
+ }
3193
3239
  const index = this.handlers.indexOf(fn);
3194
3240
  if (index >= 0) {
3195
3241
  this.handlers.splice(index, 1);
@@ -3199,9 +3245,26 @@ class EventHandler {
3199
3245
  this.handlers.length = 0;
3200
3246
  }
3201
3247
  emit(...args) {
3202
- // make a copy just in case it changes
3203
- const handlers = this.handlers.slice();
3204
- handlers.forEach(f => f(...args));
3248
+ // use emitting flag to defer unregistrations instead of copying array
3249
+ this.emitting = true;
3250
+ try {
3251
+ for (const f of this.handlers) {
3252
+ f(...args);
3253
+ }
3254
+ }
3255
+ finally {
3256
+ this.emitting = false;
3257
+ // process any deferred unregistrations
3258
+ if (this.pendingUnregisters) {
3259
+ for (const fn of this.pendingUnregisters) {
3260
+ const index = this.handlers.indexOf(fn);
3261
+ if (index >= 0) {
3262
+ this.handlers.splice(index, 1);
3263
+ }
3264
+ }
3265
+ this.pendingUnregisters = null;
3266
+ }
3267
+ }
3205
3268
  }
3206
3269
  }
3207
3270
  /**
@@ -4121,7 +4184,15 @@ class MapType extends ComplexType {
4121
4184
  if (!isPlainObject(value)) {
4122
4185
  return typeCheckFailure(context, value, "Value is not a plain object");
4123
4186
  }
4124
- return flattenTypeErrors(Object.keys(value).map(path => this._subType.validate(value[path], getContextForPath(context, path, this._subType))));
4187
+ for (const key of Object.keys(value)) {
4188
+ getContextForPath(context, key, this._subType);
4189
+ const errors = this._subType.validate(value[key], context);
4190
+ popContext(context);
4191
+ if (errors.length > 0) {
4192
+ return errors;
4193
+ }
4194
+ }
4195
+ return typeCheckSuccess();
4125
4196
  }
4126
4197
  getDefaultSnapshot() {
4127
4198
  return EMPTY_OBJECT;
@@ -4334,7 +4405,15 @@ class ArrayType extends ComplexType {
4334
4405
  if (!isArray(value)) {
4335
4406
  return typeCheckFailure(context, value, "Value is not an array");
4336
4407
  }
4337
- return flattenTypeErrors(value.map((item, index) => this._subType.validate(item, getContextForPath(context, "" + index, this._subType))));
4408
+ for (let i = 0; i < value.length; i++) {
4409
+ getContextForPath(context, "" + i, this._subType);
4410
+ const errors = this._subType.validate(value[i], context);
4411
+ popContext(context);
4412
+ if (errors.length > 0) {
4413
+ return errors;
4414
+ }
4415
+ }
4416
+ return typeCheckSuccess();
4338
4417
  }
4339
4418
  getDefaultSnapshot() {
4340
4419
  return EMPTY_ARRAY;
@@ -4884,7 +4963,16 @@ class ModelType extends ComplexType {
4884
4963
  if (!isPlainObject(snapshot)) {
4885
4964
  return typeCheckFailure(context, snapshot, "Value is not a plain object");
4886
4965
  }
4887
- return flattenTypeErrors(this.propertyNames.map(key => this.properties[key].validate(snapshot[key], getContextForPath(context, key, this.properties[key]))));
4966
+ for (const key of this.propertyNames) {
4967
+ const propType = this.properties[key];
4968
+ getContextForPath(context, key, propType);
4969
+ const errors = propType.validate(snapshot[key], context);
4970
+ popContext(context);
4971
+ if (errors.length > 0) {
4972
+ return errors;
4973
+ }
4974
+ }
4975
+ return typeCheckSuccess();
4888
4976
  }
4889
4977
  forAllProps(fn) {
4890
4978
  this.propertyNames.forEach(key => fn(key, this.properties[key]));
@@ -5350,6 +5438,23 @@ class Union extends BaseType {
5350
5438
  if (this._dispatcher) {
5351
5439
  return this._dispatcher(value);
5352
5440
  }
5441
+ // fast path: when type checking is disabled, try quick structural matching first
5442
+ if (!isTypeCheckingEnabled()) {
5443
+ const quickMatch = this.tryQuickMatch(value, reconcileCurrentType);
5444
+ if (quickMatch) {
5445
+ return quickMatch;
5446
+ }
5447
+ // for plain object snapshots that didn't match via quick path, try all types
5448
+ // with quick matching before falling back to full validation
5449
+ // (state tree nodes must go through full validation for type identity checks)
5450
+ if (isPlainObject(value) && !isStateTreeNode(value)) {
5451
+ for (const type of this._types) {
5452
+ if (this.snapshotLooksLikeType(value, type)) {
5453
+ return type;
5454
+ }
5455
+ }
5456
+ }
5457
+ }
5353
5458
  // find the most accomodating type
5354
5459
  // if we are using reconciliation try the current node type first (fix for #1045)
5355
5460
  if (reconcileCurrentType) {
@@ -5364,6 +5469,84 @@ class Union extends BaseType {
5364
5469
  return this._types.find(type => type.is(value));
5365
5470
  }
5366
5471
  }
5472
+ tryQuickMatch(value, reconcileCurrentType) {
5473
+ // state tree nodes need full type compatibility checking
5474
+ // (e.g., A.is(B.create()) must return false even if snapshots are compatible)
5475
+ if (isStateTreeNode(value)) {
5476
+ return undefined;
5477
+ }
5478
+ // for non-object values, try primitive matching
5479
+ if (!isPlainObject(value)) {
5480
+ return this.tryMatchPrimitive(value);
5481
+ }
5482
+ // for objects, try structural matching against model types
5483
+ const typesToCheck = reconcileCurrentType
5484
+ ? [
5485
+ reconcileCurrentType,
5486
+ ...this._types.filter(t => t !== reconcileCurrentType)
5487
+ ]
5488
+ : this._types;
5489
+ for (const type of typesToCheck) {
5490
+ if (this.snapshotLooksLikeType(value, type)) {
5491
+ return type;
5492
+ }
5493
+ }
5494
+ return undefined;
5495
+ }
5496
+ tryMatchPrimitive(value) {
5497
+ const valueType = typeof value;
5498
+ for (const type of this._types) {
5499
+ const flags = type.flags;
5500
+ if ((valueType === "string" && flags & TypeFlags.String) ||
5501
+ (valueType === "number" &&
5502
+ flags &
5503
+ (TypeFlags.Number |
5504
+ TypeFlags.Integer |
5505
+ TypeFlags.Float |
5506
+ TypeFlags.Finite)) ||
5507
+ (valueType === "boolean" && flags & TypeFlags.Boolean) ||
5508
+ (value === null && flags & TypeFlags.Null) ||
5509
+ (value === undefined && flags & TypeFlags.Undefined)) {
5510
+ return type;
5511
+ }
5512
+ // for literals, check exact value match
5513
+ if (flags & TypeFlags.Literal) {
5514
+ if (type.is(value)) {
5515
+ return type;
5516
+ }
5517
+ }
5518
+ }
5519
+ return undefined;
5520
+ }
5521
+ snapshotLooksLikeType(value, type) {
5522
+ // for model types, check if snapshot has all the required property keys
5523
+ // and that any literal-typed properties match exactly
5524
+ if (type instanceof ModelType) {
5525
+ const props = type.properties;
5526
+ // use cached propertyNames from ModelType instead of Object.keys()
5527
+ for (const key of type.propertyNames) {
5528
+ const propType = props[key];
5529
+ const isOptional = propType.flags & TypeFlags.Optional;
5530
+ const propValue = value[key];
5531
+ // check required properties exist and are not undefined
5532
+ // (unless the type accepts undefined, which Optional types do)
5533
+ if (!isOptional) {
5534
+ if (!(key in value) || propValue === undefined) {
5535
+ return false;
5536
+ }
5537
+ }
5538
+ // for literal types, verify the value matches exactly
5539
+ // this is critical for discriminated unions
5540
+ if (propType.flags & TypeFlags.Literal) {
5541
+ if (!propType.is(propValue)) {
5542
+ return false;
5543
+ }
5544
+ }
5545
+ }
5546
+ return true;
5547
+ }
5548
+ return false;
5549
+ }
5367
5550
  isValidSnapshot(value, context) {
5368
5551
  if (this._dispatcher) {
5369
5552
  return this._dispatcher(value).validate(value, context);
@@ -6453,6 +6636,122 @@ class CustomType extends SimpleType {
6453
6636
  }
6454
6637
  }
6455
6638
 
6639
+ class Resilient extends BaseType {
6640
+ _subtype;
6641
+ _fallbackType;
6642
+ _createFallbackSnapshot;
6643
+ get flags() {
6644
+ return this._subtype.flags;
6645
+ }
6646
+ constructor(_subtype, _fallbackType, _createFallbackSnapshot) {
6647
+ super(`resilient(${_subtype.name})`);
6648
+ this._subtype = _subtype;
6649
+ this._fallbackType = _fallbackType;
6650
+ this._createFallbackSnapshot = _createFallbackSnapshot;
6651
+ }
6652
+ describe() {
6653
+ return `resilient(${this._subtype.describe()})`;
6654
+ }
6655
+ _instantiateFallback(parent, subpath, environment, originalSnapshot, originalError) {
6656
+ let fallbackSnapshot;
6657
+ try {
6658
+ fallbackSnapshot = this._createFallbackSnapshot(originalError, originalSnapshot);
6659
+ }
6660
+ catch (e) {
6661
+ throw fail(`resilient: createFallbackSnapshot threw while handling error for '${this._subtype.name}': ${originalError}. createFallbackSnapshot error: ${e}`);
6662
+ }
6663
+ try {
6664
+ return this._fallbackType.instantiate(parent, subpath, environment, fallbackSnapshot);
6665
+ }
6666
+ catch (e) {
6667
+ throw fail(`resilient: fallback type '${this._fallbackType.name}' failed to instantiate while handling error for '${this._subtype.name}': ${originalError}. Fallback error: ${e}`);
6668
+ }
6669
+ }
6670
+ instantiate(parent, subpath, environment, initialValue) {
6671
+ if (isStateTreeNode(initialValue)) {
6672
+ return this._subtype.instantiate(parent, subpath, environment, initialValue);
6673
+ }
6674
+ try {
6675
+ return this._subtype.instantiate(parent, subpath, environment, initialValue);
6676
+ }
6677
+ catch (e) {
6678
+ return this._instantiateFallback(parent, subpath, environment, initialValue, e);
6679
+ }
6680
+ }
6681
+ reconcile(current, newValue, parent, subpath) {
6682
+ if (isStateTreeNode(newValue)) {
6683
+ return this._subtype.reconcile(current, newValue, parent, subpath);
6684
+ }
6685
+ if (this._fallbackType.isAssignableFrom(current.type)) {
6686
+ try {
6687
+ return this._subtype.instantiate(parent, subpath, undefined, newValue);
6688
+ }
6689
+ catch (e) {
6690
+ return this._fallbackType.reconcile(current, this._createFallbackSnapshot(e, newValue), parent, subpath);
6691
+ }
6692
+ }
6693
+ try {
6694
+ return this._subtype.reconcile(current, newValue, parent, subpath);
6695
+ }
6696
+ catch (e) {
6697
+ current.die();
6698
+ return this._instantiateFallback(parent, subpath, undefined, newValue, e);
6699
+ }
6700
+ }
6701
+ isValidSnapshot(_value, _context) {
6702
+ return typeCheckSuccess();
6703
+ }
6704
+ is(thing) {
6705
+ if (isType(thing)) {
6706
+ return (this._subtype.isAssignableFrom(thing) ||
6707
+ this._fallbackType.isAssignableFrom(thing));
6708
+ }
6709
+ try {
6710
+ if (this._subtype.is(thing)) {
6711
+ return true;
6712
+ }
6713
+ }
6714
+ catch (_e) {
6715
+ // subtype.is() may throw (e.g. union dispatcher)
6716
+ }
6717
+ try {
6718
+ return this._fallbackType.is(thing);
6719
+ }
6720
+ catch (_e) {
6721
+ return false;
6722
+ }
6723
+ }
6724
+ isAssignableFrom(type) {
6725
+ return (this._subtype.isAssignableFrom(type) ||
6726
+ this._fallbackType.isAssignableFrom(type));
6727
+ }
6728
+ getSubTypes() {
6729
+ return [this._subtype, this._fallbackType];
6730
+ }
6731
+ getSnapshot(node) {
6732
+ return node.snapshot;
6733
+ }
6734
+ }
6735
+ /**
6736
+ * `types.resilient` - Wraps a type so that instantiation errors are caught
6737
+ * and a fallback type is used instead. This is useful for loading data that
6738
+ * may contain unknown or invalid subtrees (e.g., plugin types that are not
6739
+ * installed) without crashing the entire state tree.
6740
+ *
6741
+ * The `createFallbackSnapshot` callback receives the caught error and the
6742
+ * original snapshot, and must return a valid snapshot for the fallback type.
6743
+ *
6744
+ * @param type The type to wrap.
6745
+ * @param fallbackType The fallback type to use when instantiation fails.
6746
+ * @param createFallbackSnapshot Callback that produces a fallback snapshot from the error and original snapshot.
6747
+ * @returns A resilient type.
6748
+ */
6749
+ function resilient(type, fallbackType, createFallbackSnapshot) {
6750
+ assertIsType(type, 1);
6751
+ assertIsType(fallbackType, 2);
6752
+ return new Resilient(type, fallbackType, createFallbackSnapshot);
6753
+ }
6754
+
6456
6755
  // we import the types to re-export them inside types.
6457
6756
  const types = {
6458
6757
  enumeration,
@@ -6483,7 +6782,8 @@ const types = {
6483
6782
  lazy,
6484
6783
  undefined: undefinedType,
6485
6784
  null: nullType,
6486
- snapshotProcessor
6785
+ snapshotProcessor,
6786
+ resilient
6487
6787
  };
6488
6788
 
6489
6789
  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, 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, process$1 as process, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, walk };