@jbrowse/mobx-state-tree 5.11.2 → 5.12.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)) {
@@ -1666,9 +1667,6 @@ ObjectNode.prototype.createObservableInstance = action(ObjectNode.prototype.crea
1666
1667
  ObjectNode.prototype.detach = action(ObjectNode.prototype.detach);
1667
1668
  ObjectNode.prototype.die = action(ObjectNode.prototype.die);
1668
1669
 
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
1670
  /**
1673
1671
  * @internal
1674
1672
  * @hidden
@@ -1744,27 +1742,7 @@ class BaseType {
1744
1742
  : typeCheckFailure(context, value);
1745
1743
  // it is tempting to compare snapshots, but in that case we should always clone on assignments...
1746
1744
  }
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;
1745
+ return this.isValidSnapshot(value, context);
1768
1746
  }
1769
1747
  is(thing) {
1770
1748
  return this.validate(thing, [{ path: "", type: this }]).length === 0;
@@ -3239,8 +3217,6 @@ function addHiddenWritableProp(object, propName, value) {
3239
3217
  */
3240
3218
  class EventHandler {
3241
3219
  handlers = [];
3242
- emitting = false;
3243
- pendingUnregisters = null;
3244
3220
  get hasSubscribers() {
3245
3221
  return this.handlers.length > 0;
3246
3222
  }
@@ -3259,14 +3235,6 @@ class EventHandler {
3259
3235
  return this.handlers.includes(fn);
3260
3236
  }
3261
3237
  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
3238
  const index = this.handlers.indexOf(fn);
3271
3239
  if (index >= 0) {
3272
3240
  this.handlers.splice(index, 1);
@@ -3276,26 +3244,10 @@ class EventHandler {
3276
3244
  this.handlers.length = 0;
3277
3245
  }
3278
3246
  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
- }
3247
+ // iterate a copy so (un)registrations during emit don't disturb this pass
3248
+ // and reentrant emits stay correct
3249
+ const handlers = this.handlers.slice();
3250
+ handlers.forEach(f => f(...args));
3299
3251
  }
3300
3252
  }
3301
3253
  /**
@@ -4448,6 +4400,23 @@ function array(subtype) {
4448
4400
  }
4449
4401
  function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths) {
4450
4402
  let nothingChanged = true;
4403
+ // When the element type is a plain model with an identifier, index the old
4404
+ // nodes by id so a moved / replaced child is matched in O(1) instead of the
4405
+ // linear scan in the reorder branch below. This turns a full array
4406
+ // replacement (the common "load new data" case) from O(n^2) into O(n). Types
4407
+ // whose id extraction needs type-specific preprocessing (union,
4408
+ // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4409
+ // `is()` before their id check, so they stay on the scan path.
4410
+ let idIndex;
4411
+ if (childType instanceof ModelType && childType.identifierAttribute) {
4412
+ const byId = new Map();
4413
+ for (const n of oldNodes) {
4414
+ if (n instanceof ObjectNode && n.identifier !== null) {
4415
+ byId.set(n.identifier, n);
4416
+ }
4417
+ }
4418
+ idIndex = { attr: childType.identifierAttribute, byId };
4419
+ }
4451
4420
  for (let i = 0;; i++) {
4452
4421
  const hasNewNode = i <= newValues.length - 1;
4453
4422
  const oldNode = oldNodes[i];
@@ -4491,13 +4460,26 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4491
4460
  oldNodes[i] = valueAsNode(childType, parent, newPath, newValue, oldNode);
4492
4461
  }
4493
4462
  else {
4494
- // nothing to do, try to reorder
4463
+ // nothing to do, try to reorder: find a candidate old node to reuse
4495
4464
  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)) {
4465
+ if (idIndex && isPlainObject(newValue)) {
4466
+ // for an identified element type a plain-object snapshot can only match
4467
+ // by id (it is never a live node, and snapshot-reference equality still
4468
+ // implies the same id), so a miss means "nothing to reuse" without
4469
+ // scanning. areSame still verifies the single candidate.
4470
+ const candidate = idIndex.byId.get(normalizeIdentifier(newValue[idIndex.attr]));
4471
+ const j = candidate ? oldNodes.indexOf(candidate, i) : -1;
4472
+ if (j >= i && areSame(oldNodes[j], newValue)) {
4499
4473
  oldMatch = oldNodes.splice(j, 1)[0];
4500
- break;
4474
+ }
4475
+ }
4476
+ else {
4477
+ // find a possible candidate to reuse
4478
+ for (let j = i; j < oldNodes.length; j++) {
4479
+ if (areSame(oldNodes[j], newValue)) {
4480
+ oldMatch = oldNodes.splice(j, 1)[0];
4481
+ break;
4482
+ }
4501
4483
  }
4502
4484
  }
4503
4485
  nothingChanged = false;
@@ -4657,6 +4639,9 @@ class ModelType extends ComplexType {
4657
4639
  preProcessor;
4658
4640
  postProcessor;
4659
4641
  propertyNames;
4642
+ // member/property name collisions are a property of the type, so we only need
4643
+ // to check the first instance we finalize (see finalizeNewInstance)
4644
+ duplicateKeysChecked = false;
4660
4645
  constructor(opts) {
4661
4646
  super(opts.name || defaultObjectOptions.name);
4662
4647
  Object.assign(this, defaultObjectOptions, opts);
@@ -4846,6 +4831,21 @@ class ModelType extends ComplexType {
4846
4831
  _interceptReads(instance, name, node.unbox);
4847
4832
  });
4848
4833
  this.initializers.reduce((self, fn) => fn(self), instance);
4834
+ // views, actions and volatile share the instance namespace with properties,
4835
+ // so a view/action reusing a property name silently clobbers that property's
4836
+ // observable value. It's a type-level mistake (identical for every instance),
4837
+ // so we check just the first one we finalize: a healthy property stays a
4838
+ // plain observable, a shadowing getter becomes a computed, and a shadowing
4839
+ // function/value stops being observable at all. The flag is only set on
4840
+ // success so a broken type keeps throwing on every create.
4841
+ if (!this.duplicateKeysChecked) {
4842
+ this.forAllProps(name => {
4843
+ if (isComputedProp(instance, name) || !isObservableProp(instance, name)) {
4844
+ throw fail(`${name} property is declared twice`);
4845
+ }
4846
+ });
4847
+ this.duplicateKeysChecked = true;
4848
+ }
4849
4849
  intercept(instance, this.willChange);
4850
4850
  observe(instance, this.didChange);
4851
4851
  }
@@ -4900,14 +4900,7 @@ class ModelType extends ComplexType {
4900
4900
  getSnapshot(node, applyPostProcess = true) {
4901
4901
  const res = {};
4902
4902
  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
- }
4903
+ getAtom(node.storedValue, name).reportObserved();
4911
4904
  const snapshot = this.getChildNode(node, name).snapshot;
4912
4905
  // strip-default optionals omit their key when equal to the default
4913
4906
  if (!shouldStripChildFromSnapshot(type, snapshot)) {
@@ -5519,22 +5512,14 @@ class Union extends BaseType {
5519
5512
  if (this._dispatcher) {
5520
5513
  return this._dispatcher(value);
5521
5514
  }
5522
- // fast path: when type checking is disabled, try quick structural matching first
5515
+ // fast path: when type checking is disabled, try quick structural matching
5516
+ // first. This skips full recursive validation of every property value, a
5517
+ // meaningful win for wide model members (e.g. jbrowse config schemas).
5523
5518
  if (!isTypeCheckingEnabled()) {
5524
5519
  const quickMatch = this.tryQuickMatch(value, reconcileCurrentType);
5525
5520
  if (quickMatch) {
5526
5521
  return quickMatch;
5527
5522
  }
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
5523
  }
5539
5524
  // find the most accomodating type
5540
5525
  // if we are using reconciliation try the current node type first (fix for #1045)