@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.
package/dist/index.d.ts CHANGED
@@ -347,7 +347,7 @@ declare function getRelativePath(base: IAnyStateTreeNode, target: IAnyStateTreeN
347
347
  * @param keepEnvironment indicates whether the clone should inherit the same environment (`true`, the default), or not have an environment (`false`). If an object is passed in as second argument, that will act as the environment for the cloned tree.
348
348
  * @returns
349
349
  */
350
- declare function clone<T extends IAnyStateTreeNode>(source: T, keepEnvironment?: boolean | any): T;
350
+ declare function clone<T extends IAnyStateTreeNode>(source: T, keepEnvironment?: boolean | object): T;
351
351
  /**
352
352
  * Removes a model element from the state tree, and let it live on as a new state tree
353
353
  */
@@ -766,10 +766,11 @@ function getMembers(target) {
766
766
  }
767
767
  return;
768
768
  }
769
- if (descriptor.value._isFlowAction === true) {
769
+ const value = descriptor.value;
770
+ if (value?._isFlowAction === true) {
770
771
  reflected.flowActions.push(key);
771
772
  }
772
- if (descriptor.value._isMSTAction === true) {
773
+ if (value?._isMSTAction === true) {
773
774
  reflected.actions.push(key);
774
775
  }
775
776
  else if (mobx.isObservableProp(target, key)) {
@@ -1668,9 +1669,6 @@ ObjectNode.prototype.createObservableInstance = mobx.action(ObjectNode.prototype
1668
1669
  ObjectNode.prototype.detach = mobx.action(ObjectNode.prototype.detach);
1669
1670
  ObjectNode.prototype.die = mobx.action(ObjectNode.prototype.die);
1670
1671
 
1671
- // Cache for validation results to avoid re-validating the same object against the same type
1672
- // Uses WeakMap so cached objects can be garbage collected
1673
- const validationCache = new WeakMap();
1674
1672
  /**
1675
1673
  * @internal
1676
1674
  * @hidden
@@ -1746,27 +1744,7 @@ class BaseType {
1746
1744
  : typeCheckFailure(context, value);
1747
1745
  // it is tempting to compare snapshots, but in that case we should always clone on assignments...
1748
1746
  }
1749
- // check cache for object values (only at root level to avoid context mismatches)
1750
- if (typeof value === "object" && value !== null && context.length === 1) {
1751
- const typeCache = validationCache.get(value);
1752
- if (typeCache) {
1753
- const cached = typeCache.get(this);
1754
- if (cached !== undefined) {
1755
- return cached;
1756
- }
1757
- }
1758
- }
1759
- const result = this.isValidSnapshot(value, context);
1760
- // cache result for object values (only at root level)
1761
- if (typeof value === "object" && value !== null && context.length === 1) {
1762
- let typeCache = validationCache.get(value);
1763
- if (!typeCache) {
1764
- typeCache = new WeakMap();
1765
- validationCache.set(value, typeCache);
1766
- }
1767
- typeCache.set(this, result);
1768
- }
1769
- return result;
1747
+ return this.isValidSnapshot(value, context);
1770
1748
  }
1771
1749
  is(thing) {
1772
1750
  return this.validate(thing, [{ path: "", type: this }]).length === 0;
@@ -3241,8 +3219,6 @@ function addHiddenWritableProp(object, propName, value) {
3241
3219
  */
3242
3220
  class EventHandler {
3243
3221
  handlers = [];
3244
- emitting = false;
3245
- pendingUnregisters = null;
3246
3222
  get hasSubscribers() {
3247
3223
  return this.handlers.length > 0;
3248
3224
  }
@@ -3261,14 +3237,6 @@ class EventHandler {
3261
3237
  return this.handlers.includes(fn);
3262
3238
  }
3263
3239
  unregister(fn) {
3264
- if (this.emitting) {
3265
- // defer unregistration until emit is done
3266
- if (!this.pendingUnregisters) {
3267
- this.pendingUnregisters = [];
3268
- }
3269
- this.pendingUnregisters.push(fn);
3270
- return;
3271
- }
3272
3240
  const index = this.handlers.indexOf(fn);
3273
3241
  if (index >= 0) {
3274
3242
  this.handlers.splice(index, 1);
@@ -3278,26 +3246,10 @@ class EventHandler {
3278
3246
  this.handlers.length = 0;
3279
3247
  }
3280
3248
  emit(...args) {
3281
- // use emitting flag to defer unregistrations instead of copying array
3282
- this.emitting = true;
3283
- try {
3284
- for (const f of this.handlers) {
3285
- f(...args);
3286
- }
3287
- }
3288
- finally {
3289
- this.emitting = false;
3290
- // process any deferred unregistrations
3291
- if (this.pendingUnregisters) {
3292
- for (const fn of this.pendingUnregisters) {
3293
- const index = this.handlers.indexOf(fn);
3294
- if (index >= 0) {
3295
- this.handlers.splice(index, 1);
3296
- }
3297
- }
3298
- this.pendingUnregisters = null;
3299
- }
3300
- }
3249
+ // iterate a copy so (un)registrations during emit don't disturb this pass
3250
+ // and reentrant emits stay correct
3251
+ const handlers = this.handlers.slice();
3252
+ handlers.forEach(f => f(...args));
3301
3253
  }
3302
3254
  }
3303
3255
  /**
@@ -4450,6 +4402,23 @@ function array(subtype) {
4450
4402
  }
4451
4403
  function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths) {
4452
4404
  let nothingChanged = true;
4405
+ // When the element type is a plain model with an identifier, index the old
4406
+ // nodes by id so a moved / replaced child is matched in O(1) instead of the
4407
+ // linear scan in the reorder branch below. This turns a full array
4408
+ // replacement (the common "load new data" case) from O(n^2) into O(n). Types
4409
+ // whose id extraction needs type-specific preprocessing (union,
4410
+ // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4411
+ // `is()` before their id check, so they stay on the scan path.
4412
+ let idIndex;
4413
+ if (childType instanceof ModelType && childType.identifierAttribute) {
4414
+ const byId = new Map();
4415
+ for (const n of oldNodes) {
4416
+ if (n instanceof ObjectNode && n.identifier !== null) {
4417
+ byId.set(n.identifier, n);
4418
+ }
4419
+ }
4420
+ idIndex = { attr: childType.identifierAttribute, byId };
4421
+ }
4453
4422
  for (let i = 0;; i++) {
4454
4423
  const hasNewNode = i <= newValues.length - 1;
4455
4424
  const oldNode = oldNodes[i];
@@ -4493,13 +4462,26 @@ function reconcileArrayChildren(parent, childType, oldNodes, newValues, newPaths
4493
4462
  oldNodes[i] = valueAsNode(childType, parent, newPath, newValue, oldNode);
4494
4463
  }
4495
4464
  else {
4496
- // nothing to do, try to reorder
4465
+ // nothing to do, try to reorder: find a candidate old node to reuse
4497
4466
  let oldMatch = undefined;
4498
- // find a possible candidate to reuse
4499
- for (let j = i; j < oldNodes.length; j++) {
4500
- if (areSame(oldNodes[j], newValue)) {
4467
+ if (idIndex && isPlainObject(newValue)) {
4468
+ // for an identified element type a plain-object snapshot can only match
4469
+ // by id (it is never a live node, and snapshot-reference equality still
4470
+ // implies the same id), so a miss means "nothing to reuse" without
4471
+ // scanning. areSame still verifies the single candidate.
4472
+ const candidate = idIndex.byId.get(normalizeIdentifier(newValue[idIndex.attr]));
4473
+ const j = candidate ? oldNodes.indexOf(candidate, i) : -1;
4474
+ if (j >= i && areSame(oldNodes[j], newValue)) {
4501
4475
  oldMatch = oldNodes.splice(j, 1)[0];
4502
- break;
4476
+ }
4477
+ }
4478
+ else {
4479
+ // find a possible candidate to reuse
4480
+ for (let j = i; j < oldNodes.length; j++) {
4481
+ if (areSame(oldNodes[j], newValue)) {
4482
+ oldMatch = oldNodes.splice(j, 1)[0];
4483
+ break;
4484
+ }
4503
4485
  }
4504
4486
  }
4505
4487
  nothingChanged = false;
@@ -4659,6 +4641,9 @@ class ModelType extends ComplexType {
4659
4641
  preProcessor;
4660
4642
  postProcessor;
4661
4643
  propertyNames;
4644
+ // member/property name collisions are a property of the type, so we only need
4645
+ // to check the first instance we finalize (see finalizeNewInstance)
4646
+ duplicateKeysChecked = false;
4662
4647
  constructor(opts) {
4663
4648
  super(opts.name || defaultObjectOptions.name);
4664
4649
  Object.assign(this, defaultObjectOptions, opts);
@@ -4848,6 +4833,21 @@ class ModelType extends ComplexType {
4848
4833
  mobx._interceptReads(instance, name, node.unbox);
4849
4834
  });
4850
4835
  this.initializers.reduce((self, fn) => fn(self), instance);
4836
+ // views, actions and volatile share the instance namespace with properties,
4837
+ // so a view/action reusing a property name silently clobbers that property's
4838
+ // observable value. It's a type-level mistake (identical for every instance),
4839
+ // so we check just the first one we finalize: a healthy property stays a
4840
+ // plain observable, a shadowing getter becomes a computed, and a shadowing
4841
+ // function/value stops being observable at all. The flag is only set on
4842
+ // success so a broken type keeps throwing on every create.
4843
+ if (!this.duplicateKeysChecked) {
4844
+ this.forAllProps(name => {
4845
+ if (mobx.isComputedProp(instance, name) || !mobx.isObservableProp(instance, name)) {
4846
+ throw fail(`${name} property is declared twice`);
4847
+ }
4848
+ });
4849
+ this.duplicateKeysChecked = true;
4850
+ }
4851
4851
  mobx.intercept(instance, this.willChange);
4852
4852
  mobx.observe(instance, this.didChange);
4853
4853
  }
@@ -4902,14 +4902,7 @@ class ModelType extends ComplexType {
4902
4902
  getSnapshot(node, applyPostProcess = true) {
4903
4903
  const res = {};
4904
4904
  this.forAllProps((name, type) => {
4905
- try {
4906
- // TODO: FIXME, make sure the observable ref is used!
4907
- const atom = mobx.getAtom(node.storedValue, name);
4908
- atom.reportObserved();
4909
- }
4910
- catch (_e) {
4911
- throw fail(`${name} property is declared twice`);
4912
- }
4905
+ mobx.getAtom(node.storedValue, name).reportObserved();
4913
4906
  const snapshot = this.getChildNode(node, name).snapshot;
4914
4907
  // strip-default optionals omit their key when equal to the default
4915
4908
  if (!shouldStripChildFromSnapshot(type, snapshot)) {
@@ -5521,22 +5514,14 @@ class Union extends BaseType {
5521
5514
  if (this._dispatcher) {
5522
5515
  return this._dispatcher(value);
5523
5516
  }
5524
- // fast path: when type checking is disabled, try quick structural matching first
5517
+ // fast path: when type checking is disabled, try quick structural matching
5518
+ // first. This skips full recursive validation of every property value, a
5519
+ // meaningful win for wide model members (e.g. jbrowse config schemas).
5525
5520
  if (!isTypeCheckingEnabled()) {
5526
5521
  const quickMatch = this.tryQuickMatch(value, reconcileCurrentType);
5527
5522
  if (quickMatch) {
5528
5523
  return quickMatch;
5529
5524
  }
5530
- // for plain object snapshots that didn't match via quick path, try all types
5531
- // with quick matching before falling back to full validation
5532
- // (state tree nodes must go through full validation for type identity checks)
5533
- if (isPlainObject(value) && !isStateTreeNode(value)) {
5534
- for (const type of this._types) {
5535
- if (this.snapshotLooksLikeType(value, type)) {
5536
- return type;
5537
- }
5538
- }
5539
- }
5540
5525
  }
5541
5526
  // find the most accomodating type
5542
5527
  // if we are using reconciliation try the current node type first (fix for #1045)