@jbrowse/mobx-state-tree 6.6.0 → 6.6.1

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.
@@ -4467,98 +4467,174 @@ function array(subtype) {
4467
4467
  */
4468
4468
  function reconcileArrayChildren(parent, childType, oldNodes, newValues, firstNewPath) {
4469
4469
  let nothingChanged = true;
4470
- // When the element type is a plain model with an identifier, index the old
4471
- // nodes by id so a moved / replaced child is matched in O(1) instead of the
4472
- // linear scan in the reorder branch below. This turns a full array
4473
- // replacement (the common "load new data" case) from O(n^2) into O(n). Types
4474
- // whose id extraction needs type-specific preprocessing (union,
4475
- // snapshotProcessor, late, ...) are intentionally excluded: areSame must run
4476
- // `is()` before their id check, so they stay on the scan path.
4477
- // With at most one old node the scan below is already O(1), so building the
4478
- // index would only add a Map allocation to every single-element write.
4479
- let idIndex;
4480
- if (oldNodes.length > 1 &&
4481
- childType instanceof ModelType &&
4482
- childType.identifierAttribute) {
4483
- const byId = new Map();
4484
- for (const n of oldNodes) {
4485
- if (n instanceof ObjectNode && n.identifier !== null) {
4486
- byId.set(n.identifier, n);
4487
- }
4488
- }
4489
- idIndex = { attr: childType.identifierAttribute, byId };
4490
- }
4491
- for (let i = 0;; i++) {
4492
- const hasNewNode = i <= newValues.length - 1;
4493
- const oldNode = oldNodes[i];
4494
- let newValue = hasNewNode ? newValues[i] : undefined;
4470
+ const result = [];
4471
+ // every old node before `next` is claimed
4472
+ const claimed = new Uint8Array(oldNodes.length);
4473
+ let next = 0;
4474
+ let finder;
4475
+ for (let i = 0; i < newValues.length; i++) {
4476
+ while (claimed[next]) {
4477
+ next++;
4478
+ }
4479
+ const oldNode = oldNodes[next];
4480
+ let newValue = newValues[i];
4495
4481
  const newPath = `${firstNewPath + i}`;
4496
4482
  // for some reason, instead of newValue we got a node, fallback to the storedValue
4497
4483
  // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681
4498
4484
  if (isNode(newValue)) {
4499
4485
  newValue = newValue.storedValue;
4500
4486
  }
4501
- if (!oldNode && !hasNewNode) {
4502
- // both are empty, end
4503
- break;
4487
+ if (!oldNode) {
4488
+ if (isStateTreeNode(newValue) &&
4489
+ getStateTreeNode(newValue).parent === parent) {
4490
+ // this node is owned by this parent, but not in the reconcilable set, so it must be double
4491
+ throw fail(`Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '${parent.path}/${newPath}', but it lives already at '${getStateTreeNode(newValue).path}'`);
4492
+ }
4493
+ nothingChanged = false;
4494
+ result.push(valueAsNode(childType, parent, newPath, newValue));
4495
+ }
4496
+ else if (areSame(oldNode, newValue)) {
4497
+ claimed[next] = 1;
4498
+ result.push(valueAsNode(childType, parent, newPath, newValue, oldNode));
4499
+ }
4500
+ else {
4501
+ nothingChanged = false;
4502
+ // a single-element write scans one node, cheaper than building an index
4503
+ const j = oldNodes.length > 1
4504
+ ? (finder ??= new ReuseFinder(childType, oldNodes, claimed)).find(newValue, next)
4505
+ : scanForSame(oldNodes, claimed, next, newValue);
4506
+ let oldMatch;
4507
+ if (j >= 0) {
4508
+ claimed[j] = 1;
4509
+ oldMatch = oldNodes[j];
4510
+ }
4511
+ result.push(valueAsNode(childType, parent, newPath, newValue, oldMatch));
4504
4512
  }
4505
- else if (oldNode && !hasNewNode) {
4506
- // new one does not exists
4513
+ }
4514
+ for (let j = next; j < oldNodes.length; j++) {
4515
+ if (!claimed[j]) {
4507
4516
  nothingChanged = false;
4508
- oldNodes.splice(i, 1);
4517
+ const oldNode = oldNodes[j];
4509
4518
  if (oldNode instanceof ObjectNode) {
4510
4519
  // since it is going to be returned by pop/splice/shift better create it before killing it
4511
4520
  // so it doesn't end up in an undead state
4512
4521
  oldNode.createObservableInstanceIfNeeded();
4513
4522
  }
4514
4523
  oldNode.die();
4515
- i--;
4516
4524
  }
4517
- else if (!oldNode) {
4518
- // there is no old node, create it
4519
- // check if already belongs to the same parent. if so, avoid pushing item in. only swapping can occur.
4520
- if (isStateTreeNode(newValue) &&
4521
- getStateTreeNode(newValue).parent === parent) {
4522
- // this node is owned by this parent, but not in the reconcilable set, so it must be double
4523
- throw fail(`Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '${parent.path}/${newPath}', but it lives already at '${getStateTreeNode(newValue).path}'`);
4524
- }
4525
- nothingChanged = false;
4526
- const newNode = valueAsNode(childType, parent, newPath, newValue);
4527
- oldNodes.splice(i, 0, newNode);
4525
+ }
4526
+ return nothingChanged ? null : result;
4527
+ }
4528
+ function scanForSame(oldNodes, claimed, from, value) {
4529
+ for (let j = from; j < oldNodes.length; j++) {
4530
+ if (!claimed[j] && areSame(oldNodes[j], value)) {
4531
+ return j;
4528
4532
  }
4529
- else if (areSame(oldNode, newValue)) {
4530
- // both are the same, reconcile
4531
- oldNodes[i] = valueAsNode(childType, parent, newPath, newValue, oldNode);
4533
+ }
4534
+ return -1;
4535
+ }
4536
+ /**
4537
+ * Picks the old node the reorder branch reuses: the first unclaimed one
4538
+ * `areSame` holds for. `areSame` reduces to an equality — node identity for a
4539
+ * live node, the old node's snapshot for anything else — except where the
4540
+ * value is a plain object and the old node carries an identifier, where it
4541
+ * runs `is()` before matching ids. So only that case scans, and an identified
4542
+ * plain model looks its id up instead, as ADR 0001 records.
4543
+ */
4544
+ class ReuseFinder {
4545
+ oldNodes;
4546
+ claimed;
4547
+ idAttribute;
4548
+ byId;
4549
+ byNode;
4550
+ bySnapshot;
4551
+ anyIdentified;
4552
+ constructor(childType, oldNodes, claimed) {
4553
+ this.oldNodes = oldNodes;
4554
+ this.claimed = claimed;
4555
+ this.idAttribute =
4556
+ childType instanceof ModelType ? childType.identifierAttribute : undefined;
4557
+ }
4558
+ find(value, next) {
4559
+ let j;
4560
+ if (this.idAttribute && isPlainObject(value)) {
4561
+ j =
4562
+ this.idIndex(next).get(normalizeIdentifier(value[this.idAttribute])) ??
4563
+ -1;
4564
+ }
4565
+ else if (isStateTreeNode(value)) {
4566
+ j = this.nodeIndex(next).get(getStateTreeNode(value)) ?? -1;
4567
+ }
4568
+ else if (!isPlainObject(value) || !this.hasIdentifiedNode(next)) {
4569
+ j = this.firstWithSnapshot(value, next);
4532
4570
  }
4533
4571
  else {
4534
- // nothing to do, try to reorder: find a candidate old node to reuse
4535
- let oldMatch = undefined;
4536
- if (idIndex && isPlainObject(newValue)) {
4537
- // for an identified element type a plain-object snapshot can only match
4538
- // by id (it is never a live node, and snapshot-reference equality still
4539
- // implies the same id), so a miss means "nothing to reuse" without
4540
- // scanning. areSame still verifies the single candidate.
4541
- const candidate = idIndex.byId.get(normalizeIdentifier(newValue[idIndex.attr]));
4542
- const j = candidate ? oldNodes.indexOf(candidate, i) : -1;
4543
- if (j >= i && areSame(oldNodes[j], newValue)) {
4544
- oldMatch = oldNodes.splice(j, 1)[0];
4572
+ return scanForSame(this.oldNodes, this.claimed, next, value);
4573
+ }
4574
+ return j >= 0 && !this.claimed[j] && areSame(this.oldNodes[j], value)
4575
+ ? j
4576
+ : -1;
4577
+ }
4578
+ // last one wins on a duplicate id, even when it is already claimed
4579
+ idIndex(next) {
4580
+ if (!this.byId) {
4581
+ this.byId = new Map();
4582
+ for (let j = next; j < this.oldNodes.length; j++) {
4583
+ const node = this.oldNodes[j];
4584
+ if (node instanceof ObjectNode && node.identifier !== null) {
4585
+ this.byId.set(node.identifier, j);
4545
4586
  }
4546
4587
  }
4547
- else {
4548
- // find a possible candidate to reuse
4549
- for (let j = i; j < oldNodes.length; j++) {
4550
- if (areSame(oldNodes[j], newValue)) {
4551
- oldMatch = oldNodes.splice(j, 1)[0];
4552
- break;
4553
- }
4588
+ }
4589
+ return this.byId;
4590
+ }
4591
+ nodeIndex(next) {
4592
+ if (!this.byNode) {
4593
+ this.byNode = new Map();
4594
+ for (let j = next; j < this.oldNodes.length; j++) {
4595
+ this.byNode.set(this.oldNodes[j], j);
4596
+ }
4597
+ }
4598
+ return this.byNode;
4599
+ }
4600
+ hasIdentifiedNode(next) {
4601
+ if (this.anyIdentified === undefined) {
4602
+ this.anyIdentified = this.oldNodes
4603
+ .slice(next)
4604
+ .some(node => node instanceof ObjectNode &&
4605
+ node.identifier !== null &&
4606
+ !!node.identifierAttribute);
4607
+ }
4608
+ return this.anyIdentified;
4609
+ }
4610
+ firstWithSnapshot(value, next) {
4611
+ if (!this.bySnapshot) {
4612
+ const first = new Map();
4613
+ const nextSame = new Int32Array(this.oldNodes.length);
4614
+ for (let j = this.oldNodes.length - 1; j >= next; j--) {
4615
+ const node = this.oldNodes[j];
4616
+ if (!this.claimed[j] && node.isAlive) {
4617
+ const snapshot = node.snapshot;
4618
+ nextSame[j] = first.get(snapshot) ?? -1;
4619
+ first.set(snapshot, j);
4554
4620
  }
4555
4621
  }
4556
- nothingChanged = false;
4557
- const newNode = valueAsNode(childType, parent, newPath, newValue, oldMatch);
4558
- oldNodes.splice(i, 0, newNode);
4622
+ this.bySnapshot = { first, nextSame };
4623
+ }
4624
+ const { first, nextSame } = this.bySnapshot;
4625
+ const head = first.get(value);
4626
+ if (head === undefined) {
4627
+ return -1;
4628
+ }
4629
+ let j = head;
4630
+ while (j >= 0 && (this.claimed[j] || !this.oldNodes[j].isAlive)) {
4631
+ j = nextSame[j];
4632
+ }
4633
+ if (j !== head) {
4634
+ first.set(value, j);
4559
4635
  }
4636
+ return j;
4560
4637
  }
4561
- return nothingChanged ? null : oldNodes;
4562
4638
  }
4563
4639
  /**
4564
4640
  * Convert a value to a node at given parent and subpath. Attempts to reuse old node if possible and given.