@symbiote-native/engine 1.3.0 → 1.4.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.
Files changed (79) hide show
  1. package/build/accessibility-props.d.ts +0 -11
  2. package/build/accessibility-props.js +30 -68
  3. package/build/animated/graph.js +1 -1
  4. package/build/animated/leaf-lifecycle.js +2 -2
  5. package/build/asset-source-resolver.d.ts +2 -0
  6. package/build/asset-source-resolver.js +13 -0
  7. package/build/back-handler/index.d.ts +1 -5
  8. package/build/back-handler/index.js +0 -6
  9. package/build/debug.js +8 -22
  10. package/build/dispatch.js +3 -9
  11. package/build/events/delivery.d.ts +9 -0
  12. package/build/events/delivery.js +143 -0
  13. package/build/events/index.js +199 -660
  14. package/build/events/names.d.ts +24 -0
  15. package/build/events/names.js +80 -0
  16. package/build/events/press.d.ts +20 -0
  17. package/build/events/press.js +89 -0
  18. package/build/events/responder.d.ts +6 -0
  19. package/build/events/responder.js +124 -0
  20. package/build/fabric-props.js +74 -179
  21. package/build/fabric.d.ts +0 -13
  22. package/build/fabric.js +18 -38
  23. package/build/host-access.d.ts +1 -128
  24. package/build/host-access.js +96 -205
  25. package/build/host-behavior.d.ts +0 -100
  26. package/build/host-behavior.js +125 -311
  27. package/build/image-loader.js +10 -23
  28. package/build/image-source-resolver.js +3 -7
  29. package/build/image-source-write.d.ts +0 -11
  30. package/build/image-source-write.js +14 -34
  31. package/build/imperative.d.ts +2 -28
  32. package/build/imperative.js +60 -93
  33. package/build/index.d.ts +6 -2
  34. package/build/index.js +29 -39
  35. package/build/mutation-buffer.d.ts +3 -177
  36. package/build/mutation-buffer.js +162 -316
  37. package/build/native-engine.d.ts +6 -102
  38. package/build/native-engine.js +60 -141
  39. package/build/native-events.js +9 -18
  40. package/build/native-tree-host.d.ts +0 -21
  41. package/build/native-tree-host.js +15 -31
  42. package/build/node-events.d.ts +11 -0
  43. package/build/node-events.js +145 -0
  44. package/build/node-instance.d.ts +8 -0
  45. package/build/node-instance.js +168 -0
  46. package/build/node-props.d.ts +13 -0
  47. package/build/node-props.js +131 -0
  48. package/build/node-route.d.ts +2 -0
  49. package/build/node-route.js +151 -0
  50. package/build/node-style.d.ts +15 -0
  51. package/build/node-style.js +214 -0
  52. package/build/node-tree.d.ts +6 -0
  53. package/build/node-tree.js +159 -0
  54. package/build/node-types.d.ts +70 -0
  55. package/build/node-types.js +36 -0
  56. package/build/node.d.ts +7 -309
  57. package/build/node.js +9 -1564
  58. package/build/post-commit.js +3 -8
  59. package/build/process-aspect-ratio.js +3 -7
  60. package/build/process-background-longhands.js +10 -19
  61. package/build/process-filter.js +11 -19
  62. package/build/process-font-variant.js +3 -7
  63. package/build/registry.d.ts +0 -33
  64. package/build/registry.js +22 -57
  65. package/build/report-error.js +4 -18
  66. package/build/structured-style.d.ts +0 -9
  67. package/build/structured-style.js +16 -31
  68. package/build/styles.js +3 -6
  69. package/build/surface.d.ts +0 -26
  70. package/build/surface.js +29 -76
  71. package/build/text-input-state.js +4 -8
  72. package/build/touch-history.js +5 -11
  73. package/build/tree-host.d.ts +7 -270
  74. package/build/tree-host.js +63 -153
  75. package/build/view-config.js +17 -37
  76. package/cpp/SymbioteEngineBindings.cpp +19 -18
  77. package/cpp/SymbioteTree.cpp +81 -156
  78. package/cpp/SymbioteTree.h +6 -0
  79. package/package.json +2 -2
@@ -1540,33 +1540,16 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1540
1540
 
1541
1541
  size_t opsLength = 0;
1542
1542
  const int32_t *ops = int32ArrayData(runtime, arguments[0], opsLength);
1543
- // ── THE PROLOGUE WAS THE COST, AND THIS LINE WAS THE PROLOGUE ──────────────────────────────────
1544
- //
1545
- // `getObject`/`getArray` rather than `asObject`/`asArray`. The checking pair runs an `isObject`
1546
- // and an `isArray` per table — eight JSI round trips for four arguments — and they were 2.8 us of
1547
- // a 4.4 us fixed entry cost. Measured on an EMPTY batch against a 0.13 us bare host call
1548
- // (`small-batch-crossing-cost.itest.ts`): prologue 4.38 -> 1.54 us, and a whole small drain
1549
- // 5.54 -> 2.76 us.
1550
- //
1551
- // WHY IT IS SAFE TO DROP THEM, and it is the harness's own split rather than a shrug: `takeBatch`
1552
- // is the only producer on this wire and always hands over four arrays, and `jsi::Value::getObject`
1553
- // / `Object::getArray` carry `assert`s that are LIVE in the correctness build — `core/engine/cpp/
1554
- // tests/build` is Debug with `NDEBUG` off, which is the whole reason it exists. So a fixture that
1555
- // hand-builds a malformed batch aborts there and the build that ships pays nothing for the check.
1556
- //
1557
- // WHAT IT COSTS ANYONE: a framework that navigates between mutations pays this entry per
1558
- // mutation, not per commit. Solid's `cleanChildren` enters `applyOps` 2 000 times to clear a
1559
- // thousand rows.
1543
+ // `getObject`/`getArray` and never the checking pair, which was 2.8 us of a 4.4 us entry. Why
1544
+ // that is safe: `symbiote-engine-op-application` skill, "The prologue"
1560
1545
  auto strings = arguments[1].getObject(runtime).getArray(runtime);
1561
1546
  auto values = arguments[2].getObject(runtime).getArray(runtime);
1562
1547
  auto instanceHandles = arguments[3].getObject(runtime).getArray(runtime);
1563
1548
  auto handles = arguments[4].getObject(runtime).getArray(runtime);
1564
1549
  const size_t slotCount = handles.size(runtime);
1565
1550
 
1566
- // Slot -> node, resolved at most ONCE per batch and usually not at all: a slot this batch creates
1567
- // is written by its own create op and never read from JS, which on a create-shaped commit is
1568
- // nearly every slot. Only a slot naming a node an EARLIER batch created costs a read, and it costs
1569
- // exactly one however many ops go on to name it.
1551
+ // Slot -> node, resolved at most ONCE per batch and usually not at all. See the skill,
1552
+ // "The three per-batch tables"
1570
1553
  std::vector<NodePtr> bySlot(slotCount);
1571
1554
 
1572
1555
  auto checkSlot = [&](int32_t slot) -> size_t {
@@ -1606,18 +1589,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1606
1589
  walkCost_.publishNs += nanosSince(publishStartedAt);
1607
1590
  };
1608
1591
 
1609
- // Decoded ONCE per batch, not once per op that names a string.
1610
- //
1611
- // This used to read the JSI array and allocate a fresh `std::string` inside the op loop, which
1612
- // spent exactly the saving `mutation-buffer.ts` interns for: its own comment says a 1 000-row
1613
- // create emits about a dozen distinct view names across 10 000 elements and draws every prop key
1614
- // from a set of a few hundred, and none of that reached here. Counted through a real adapter
1615
- // (`adapters/solid/src/batch-decode-census.probe.test.tsx`): 16 005 decodes against a table of
1616
- // 2 008 entries on a create, and the same 8.0x on an append.
1617
- //
1618
- // Two costs go, and only one of them is measurable without a device. The allocation half a bench
1619
- // puts at 3.26x for the whole path (`core/engine/bench/batch-string-decode.cpp`); the other half
1620
- // is 13 997 JSI crossings that simply stop happening, and nothing headless can price those.
1592
+ // Decoded ONCE per batch, not once per op that names a string. The 8.0x this turns on, and what
1593
+ // is not measurable headless: the skill, "The three per-batch tables"
1621
1594
  std::vector<std::string> decodedStrings;
1622
1595
  {
1623
1596
  const auto stringsStartedAt = ISteadyClock::now();
@@ -1630,17 +1603,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1630
1603
  walkCost_.stringDecodeNs += nanosSince(stringsStartedAt);
1631
1604
  }
1632
1605
 
1633
- // Prop VALUES, converted at most once per entry per batch — the other half of the buffer's
1634
- // interning, and useless without it. `mutation-buffer.ts` gives one entry to one object however
1635
- // many nodes were handed it, so a `StyleSheet.create` style shared by a thousand rows arrives as
1636
- // one entry; this is what turns that into one conversion instead of a thousand identical ones.
1637
- //
1638
- // LAZY rather than eager, unlike the strings above: a batch's value table can hold entries no
1639
- // surviving op names — a prop written and then overwritten in the same batch — and converting one
1640
- // eagerly would charge for work the ops do not ask for. The strings table has no such shape.
1641
- //
1642
- // One consequence worth knowing when a conversion throws: `boundedDynamicFrom`'s message names the
1643
- // prop and view of the FIRST op to reach a given entry, not every op that shares it.
1606
+ // Prop VALUES, converted at most once per entry per batch, and LAZILY unlike the strings above.
1607
+ // Why lazily, and what that costs a thrown conversion: the skill, "The three per-batch tables"
1644
1608
  std::vector<folly::dynamic> convertedValues(values.size(runtime));
1645
1609
  std::vector<bool> valueIsConverted(convertedValues.size(), false);
1646
1610
  walkCost_.valueEntries += convertedValues.size();
@@ -1657,10 +1621,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1657
1621
  return decodedStrings[static_cast<size_t>(index)];
1658
1622
  };
1659
1623
 
1660
- // `auto &&describe` and not a `std::function`: the description must stay a lambda the compiler can
1661
- // inline away, for the reason `boundedDynamicFrom`'s own comment gives — building the string
1662
- // eagerly was 48.8% of the decode path once, and a `std::function` per op would allocate to
1663
- // reintroduce half of it.
1624
+ // `auto &&describe` and not a `std::function`: the description must stay a lambda the compiler
1625
+ // can inline away. See `boundedDynamicFrom`, and the skill for the 48.8% behind it
1664
1626
  auto valueAt = [&](int32_t index, auto &&describe) -> const folly::dynamic & {
1665
1627
  if (index < 0 || static_cast<size_t>(index) >= convertedValues.size()) {
1666
1628
  throw jsi::JSError(
@@ -1729,8 +1691,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1729
1691
  const auto holdStartedAt = ISteadyClock::now();
1730
1692
  holdHandle(runtime, *child);
1731
1693
  walkCost_.holdHandleNs += nanosSince(holdStartedAt);
1732
- // The hint the detach path reads back. Appending past a hole is harmless — the hole keeps
1733
- // its place until the next read compacts, and order is preserved either way.
1694
+ // A HINT the detach path reads back, and appending past a hole is harmless: the hole keeps
1695
+ // its place until the next read compacts
1734
1696
  child->slotInParent = parent->children.size();
1735
1697
  parent->children.push_back(std::move(child));
1736
1698
  markDirty(*parent);
@@ -1741,10 +1703,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1741
1703
  const auto &parent = nodeAt(ops[at + 1]);
1742
1704
  auto child = nodeAt(ops[at + 2]);
1743
1705
  const auto &before = nodeAt(ops[at + 3]);
1744
- // A MOVE WITHIN THE SAME PARENT ERASES RATHER THAN PUNCHING A HOLE, and the reason is that
1745
- // an insert shifts this vector anyway: a hole would force a compaction pass on top of the
1746
- // shift, which measured 2.4x worse on a 4 000-row reorder than simply erasing. A move to a
1747
- // DIFFERENT parent holes the old one as usual — nothing is about to shift it.
1706
+ // A MOVE WITHIN THE SAME PARENT ERASES RATHER THAN PUNCHING A HOLE, 2.4x better on a
1707
+ // 4 000-row reorder. See the skill, "Child-vector maintenance"
1748
1708
  if (child->parent == parent.get()) {
1749
1709
  auto &standing = parent->children;
1750
1710
  const size_t hinted = child->slotInParent;
@@ -1770,60 +1730,34 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1770
1730
  std::find(siblings.begin(), siblings.end(), before) - siblings.begin());
1771
1731
  siblings.insert(siblings.begin() + static_cast<std::ptrdiff_t>(index), std::move(child));
1772
1732
  siblings[index]->slotInParent = index;
1773
- // THE TAIL'S HINTS ARE NOW ONE TOO LOW, AND THEY ARE DELIBERATELY LEFT THAT WAY.
1774
- //
1775
- // Renumbering them is O(width) per insert, which was tried and made a reorder of 4 000 rows
1776
- // 34.6 ms against 6.7 — five times worse, to keep a hint exact that nothing requires to be.
1777
- // `detachFromParent` validates before it believes (`siblings[hinted] == child`) and falls
1778
- // back to a scan, so a stale hint costs one detach its old price and never costs correctness.
1779
- //
1780
- // What IS still linear here is the vector insert itself. Finding the anchor is a load now;
1781
- // making the insert a load needs a different container, not a different search.
1733
+ // THE TAIL'S HINTS ARE NOW ONE TOO LOW AND ARE LEFT THAT WAY: renumbering was five times
1734
+ // worse on a 4 000-row reorder. See the skill, "Child-vector maintenance"
1782
1735
  markDirty(*parent);
1783
1736
  break;
1784
1737
  }
1785
1738
  case kOpRemoveChild: {
1786
1739
  const auto &parent = nodeAt(ops[at + 1]);
1787
1740
  auto child = nodeAt(ops[at + 2]);
1788
- // Named rather than implied: `detachFromParent` reads the child's OWN parent pointer, which
1789
- // is the truth even when the adapter names a stale parent — frameworks spell a move as
1790
- // remove-then-insert and can arrive here after the insert already re-parented the node.
1741
+ // The child's OWN parent pointer, which is the truth even when the adapter names a stale
1742
+ // one. See the skill, "Child-vector maintenance"
1791
1743
  if (child->parent == parent.get()) {
1792
1744
  detachFromParent(child);
1793
- // Out of the tree, so nothing pins its placeholder any more. Released HERE and not inside
1794
- // `detachFromParent`, which the two attach ops also call to spell a MOVE: dropping the
1795
- // pin there would leave a window, mid-batch, where the node is in no tree and a
1796
- // collection could take the handle a re-attach is about to need.
1745
+ // Released HERE and not inside `detachFromParent`, which the attach ops also call to
1746
+ // spell a MOVE. Why that window would matter: the skill
1797
1747
  child->attachedHandle.reset();
1798
1748
  }
1799
1749
  break;
1800
1750
  }
1801
- // Writing a value the node already holds is a NO-OP and returns before `markDirty`. Fabric
1802
- // never saw a difference either way — `diffProps` would find the key unchanged and drop it —
1803
- // but the mark is not free: it climbs to the first already-dirty ancestor and strips every one
1804
- // of them of the reuse fast path, so an otherwise untouched subtree gets rebuilt purely to
1805
- // prove it is untouched. Measured: Angular's Pressable host bag pushed 104 000 setProp calls
1806
- // for a screen Solid built in 12 000, 90 000 of them writing `undefined` over an absent key.
1807
- //
1808
- // The guard lives HERE and not in the engine's `setProp` because it needs the value the node
1809
- // already holds — a read JS would have to make over the wire, ~44 001 times on a 1 000-row
1810
- // create, which is exactly the traffic this design removes.
1811
- //
1812
- // ONE DELIBERATE ASYMMETRY with the reference applier, and it is in the safe direction. TS
1813
- // compares with `Object.is`, so for a style object or a handler the guard simply never fires:
1814
- // an adapter may hand back the SAME reference with mutated contents, and identity cannot see
1815
- // that. Here the value is a fresh `folly::dynamic` copied off the JSI value, so nothing can
1816
- // mutate it behind us and a deep compare is both available and correct. It therefore turns
1817
- // away strictly MORE writes than TS does. That changes the work, never the committed tree —
1818
- // `diffProps` drops an unchanged key either way — so the two still agree on output.
1751
+ // Writing a value the node already holds returns before `markDirty`, т.к. the MARK is what
1752
+ // costs: it strips every ancestor of the reuse fast path. Why the guard is here and not in
1753
+ // JS, and the one asymmetry with the TS applier: the skill, "The two guards"
1819
1754
  case kOpSetProp: {
1820
1755
  const auto setPropStartedAt = ISteadyClock::now();
1821
1756
  const auto &node = nodeAt(ops[at + 1]);
1822
1757
  const auto &key = stringAt(ops[at + 2]);
1823
1758
  if (ops[at + 3] == kNoValue) {
1824
- // An absent key is not a key holding null: deleting one that is not there changes nothing,
1825
- // while deleting one that is there changes what the next `diffProps` sends, since a
1826
- // vanished key has to go out as an explicit null.
1759
+ // An absent key is not a key holding null, and a vanished one goes out as an explicit
1760
+ // null. See the skill, "The two guards"
1827
1761
  if (node->props.get_ptr(key) == nullptr) {
1828
1762
  walkCost_.deletesOfAbsent += 1;
1829
1763
  break;
@@ -1838,9 +1772,7 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1838
1772
  walkCost_.writesOfUnchanged += 1;
1839
1773
  break;
1840
1774
  }
1841
- // A COPY, where this used to move: the entry is shared by every node the same object was
1842
- // handed to, so it has to survive this op. One `folly::dynamic` copy against one JS ->
1843
- // dynamic conversion, and the conversion is the JSI crossing.
1775
+ // A COPY and not a move: the entry is shared by every node the same object was handed to
1844
1776
  node->props[key] = value;
1845
1777
  }
1846
1778
  markDirty(*node);
@@ -1848,14 +1780,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1848
1780
  walkCost_.setProps += 1;
1849
1781
  break;
1850
1782
  }
1851
- // The same guard, and here it is strictly stronger than a reference check even in TS: `text`
1852
- // is a string, so this is a real value comparison. A framework that re-renders a subtree and
1853
- // hands back an unchanged label — every list row whose text did not move, on every update —
1854
- // stops dirtying its ancestors.
1855
- // The one op that changes what a node IS rather than what it holds. `materialize`'s
1856
- // `needsFreshFamily` already covers the consequence — a name differing from
1857
- // `committedViewName` re-creates the node and re-parents its children — so this only moves the
1858
- // name and marks. `TextInput`'s `multiline` flip is the whole reason it exists.
1783
+ // The one op that changes what a node IS rather than what it holds, and `materialize`'s
1784
+ // `needsFreshFamily` covers the consequence. `TextInput`'s `multiline` flip is why it exists
1859
1785
  case kOpSetComponent: {
1860
1786
  const auto &node = nodeAt(ops[at + 1]);
1861
1787
  const auto &viewName = stringAt(ops[at + 2]);
@@ -1864,19 +1790,15 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1864
1790
  markDirty(*node);
1865
1791
  break;
1866
1792
  }
1867
- // No `markDirty`: this arrives at `createElement`, before any prop is routed and long before
1868
- // the node's first commit, so the payload it changes has not been built yet.
1793
+ // No `markDirty`: this arrives at `createElement`, before the node's first commit, so there
1794
+ // is no payload yet. The table of which ops mark and why is in the skill
1869
1795
  case kOpSetTag: {
1870
1796
  const auto &node = nodeAt(ops[at + 1]);
1871
1797
  node->tagName = stringAt(ops[at + 2]);
1872
1798
  break;
1873
1799
  }
1874
- // `markDirty`, unlike `kOpSetTag` above, and the difference is WHEN each arrives. A tag is set
1875
- // at `attachHostBehavior`, before any prop is routed and before the node's first commit, so
1876
- // there is no payload yet to invalidate. A listener can flip at any point in a screen's life —
1877
- // a row that becomes pressable once its data loads — and the key it decides is already
1878
- // committed by then. Without this the control renders permanently unfocusable while visibly
1879
- // interactive, and nothing else in the batch would mark it: a listener is not a prop write.
1800
+ // `markDirty`, unlike `kOpSetTag`, т.к. a listener can flip at any point in a screen's life
1801
+ // and the key it decides is committed by then. See the skill
1880
1802
  case kOpSetOwnedListener: {
1881
1803
  const auto &node = nodeAt(ops[at + 1]);
1882
1804
  // Only the names a platform rule actually reads. Anything else is a JS-side concern that
@@ -1898,13 +1820,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1898
1820
  if (node->underlayShown == shown) break;
1899
1821
  node->underlayShown = shown;
1900
1822
  markDirty(*node);
1901
- // AND THE CHILD, because this bit drives a rule on BOTH nodes: the container takes the
1902
- // background and the child takes the opacity (`foldTouchableHighlightChild`). A descendant
1903
- // rule runs when ITS node is dirty, so without this the child would freeze in its unpressed
1904
- // shape and never dim — the hazard `ownerProps` already carries for ScrollView's content.
1905
- //
1906
- // FIRST child and no walk: RN takes `React.Children.only` (`TouchableHighlight.js:306`), so
1907
- // one is the whole population rather than a simplification. Twice per tap, not per frame.
1823
+ // AND THE CHILD, т.к. the bit drives a rule on BOTH (`foldTouchableHighlightChild`), and
1824
+ // the FIRST child is the whole population since RN takes `React.Children.only`
1908
1825
  if (!node->children.empty()) {
1909
1826
  compactChildren(*node);
1910
1827
  if (!node->children.empty() && node->children.front() != nullptr)
@@ -1917,16 +1834,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1917
1834
  const auto &text = stringAt(ops[at + 2]);
1918
1835
  const auto *existing = node->props.get_ptr("text");
1919
1836
  if (existing != nullptr && existing->isString() && existing->asString() == text) break;
1920
- // Read BEFORE the write, and only a FLIP marks the parent.
1921
- //
1922
- // A write to or from '' takes this node out of its parent's renderable child list or puts it
1923
- // back, which is a structural change to the PARENT that nothing else here would record.
1924
- // Marking unconditionally made every ordinary relabel do it too, and `markDirty` sets the
1925
- // parent's SELF-dirty bit — which forces a full `fabricProps` + `diffProps` on a node whose
1926
- // own props did not move. Counted through three adapters on a 1 000-row relabel
1927
- // (`adapters/*/src/work-ledger.probe.test.*`): 3 000 payload keys rebuilt to send 1 000.
1928
- // The walk still reaches this node either way, because `markDirty(*node)` raises
1929
- // `pathDirty` on every ancestor.
1837
+ // Read BEFORE the write, and only a FLIP to or from `''` marks the parent, т.к. that is
1838
+ // what moves the node in or out of its parent's renderable list. See the skill
1930
1839
  const bool wasEmpty =
1931
1840
  existing == nullptr || !existing->isString() || existing->asString().empty();
1932
1841
  node->props["text"] = text;
@@ -1938,43 +1847,25 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1938
1847
  const auto surfaceId = static_cast<react::SurfaceId>(ops[at + 1]);
1939
1848
  const auto &surface = nodeAt(ops[at + 2]);
1940
1849
  auto childSet = std::make_shared<ChildSet>();
1941
- // The surface NODE is contributed, not its children — it is the AppContainer view
1942
- // (`createSurfaceRoot`, `flex: 1` + `box-none`) and it commits. Routing it through the same
1943
- // call keeps the two shapes one path: an ANCHOR in this position hoists its children.
1944
- //
1945
- // `nullptr` as the Fabric parent: the root CHILD SET is not a node. So a top-level node
1946
- // moving between two surfaces is NOT caught by the parent comparison — both sides are
1947
- // `nullptr` — and the surface id is what separates them, which is why `materialize`
1948
- // compares that too.
1949
- // The root child set goes to `completeSurface`, which commits it through a transaction we
1950
- // never see the result of — so there is nothing to adopt back here, and these owners are
1951
- // collected only because `appendRenderable` needs somewhere to put them.
1850
+ // The surface NODE is contributed, not its children, and `nullptr` stands for the root
1851
+ // child set т.к. it is not a node. What that costs a cross-surface move: the skill
1952
1852
  IOwnerTally rootOwners;
1953
- // THE ONE TIMER THAT IS NOT PER NODE, and it has to be here rather than inside
1954
- // `materialize`: the walk is recursive, so a timer around the recursive call would count
1955
- // every ancestor's time again for every descendant. This is the walk's single entry point.
1853
+ // THE ONE TIMER THAT IS NOT PER NODE, т.к. the walk is recursive and a timer inside
1854
+ // `materialize` would count every ancestor again for every descendant
1956
1855
  const auto walkStartedAt = ISteadyClock::now();
1957
1856
  appendRenderable(
1958
1857
  runtime, uiManager, *childSet, rootOwners, *surface, false, surfaceId, nullptr);
1959
1858
  walkCost_.walkNs += nanosSince(walkStartedAt);
1960
- // SKIPPED when the root child set comes back identical. `materialize` already declines to
1961
- // clone a node nothing changed, so an unchanged tree produces the same handles — and
1962
- // `completeSurface` on them is a full `ShadowTree::commit`, with layout and a mount pass,
1963
- // for no change at all.
1964
- //
1965
- // This is what makes the JS side's commit fan-out free: every commit names every live root,
1966
- // because a cross-surface mutation dirties a surface whose renderer nobody is holding
1967
- // (`commitSurfaceOps` in tree-host.ts). An untouched root reaches here with an identical
1968
- // list and stops.
1859
+ // SKIPPED when the root child set comes back identical, which is what makes the JS side's
1860
+ // commit fan-out free. See the skill, "kOpCommit"
1969
1861
  if (surface->hasCommittedRenderable &&
1970
1862
  sameNodes(surface->committedRenderable, *childSet)) {
1971
1863
  break;
1972
1864
  }
1973
1865
  surface->committedRenderable = *childSet;
1974
1866
  surface->hasCommittedRenderable = true;
1975
- // `completeSurface` runs `ShadowTree::commit` itself, with a lambda that REPLACES the root's
1976
- // children outright — so a retry against a moved root is harmless and there is nothing to
1977
- // rebase. That is why this needs neither a commit hook nor a retained pending root.
1867
+ // `completeSurface` runs `ShadowTree::commit` with a lambda that REPLACES the root's
1868
+ // children, so this needs neither a commit hook nor a retained pending root
1978
1869
  uiManager.completeSurface(
1979
1870
  surfaceId,
1980
1871
  childSet,
@@ -1983,9 +1874,8 @@ jsi::Value Tree::applyOps(jsi::Runtime &runtime, const jsi::Value *arguments, si
1983
1874
  .source = react::ShadowTree::CommitSource::React});
1984
1875
  uiManager.getShadowTreeRegistry().visit(
1985
1876
  surfaceId, [&rootOwners](const react::ShadowTree &shadowTree) {
1986
- // THE REPAIR, and it must run here rather than in `materialize`: substitution happens
1987
- // INSIDE the commit, so the only tree that can be believed is the one the registry
1988
- // holds once `completeSurface` has returned. See `adoptCommitted`.
1877
+ // THE REPAIR, here and not in `materialize` т.к. substitution happens INSIDE the
1878
+ // commit. See `adoptCommitted`
1989
1879
  const ChildSet &landedRoot =
1990
1880
  shadowTree.getCurrentRevision().rootShadowNode->getChildren();
1991
1881
  const size_t rootCount =
@@ -2408,6 +2298,10 @@ jsi::Value Tree::measureLayout(jsi::Runtime &runtime, const jsi::Value *, size_t
2408
2298
  throw jsi::JSError(runtime, "symbiote engine: measureLayout is not built on this platform");
2409
2299
  }
2410
2300
 
2301
+ jsi::Value Tree::getBoundingClientRect(jsi::Runtime &runtime, const jsi::Value *, size_t) {
2302
+ throw jsi::JSError(runtime, "symbiote engine: getBoundingClientRect is not built on this platform");
2303
+ }
2304
+
2411
2305
  #else
2412
2306
 
2413
2307
  jsi::Value Tree::measure(jsi::Runtime &runtime, const jsi::Value *arguments, size_t count) {
@@ -2471,6 +2365,37 @@ jsi::Value Tree::measureInWindow(
2471
2365
  return jsi::Value::undefined();
2472
2366
  }
2473
2367
 
2368
+ // No callback, unlike its siblings above - the JS caller (imperative.ts) expects a direct return.
2369
+ // `undefined` (no revision yet) reads the same as an uncommitted node to that caller.
2370
+ jsi::Value Tree::getBoundingClientRect(
2371
+ jsi::Runtime &runtime,
2372
+ const jsi::Value *arguments,
2373
+ size_t count) {
2374
+ if (count < 2) {
2375
+ throw jsi::JSError(
2376
+ runtime, "symbiote engine: expected getBoundingClientRect(handle, includeTransform)");
2377
+ }
2378
+ const auto node = nodeFrom(runtime, arguments[0].asObject(runtime), "getBoundingClientRect");
2379
+ const bool includeTransform = arguments[1].getBool();
2380
+
2381
+ auto revision = node->committed == nullptr
2382
+ ? nullptr
2383
+ : uiManagerFor(runtime, "getBoundingClientRect")
2384
+ .getShadowTreeRevisionProvider()
2385
+ ->getCurrentRevision(node->committed->getSurfaceId());
2386
+ if (revision == nullptr) {
2387
+ return jsi::Value::undefined();
2388
+ }
2389
+
2390
+ auto rect = react::dom::getBoundingClientRect(revision, *node->committed, includeTransform);
2391
+ auto result = jsi::Object(runtime);
2392
+ result.setProperty(runtime, "x", jsi::Value(rect.x));
2393
+ result.setProperty(runtime, "y", jsi::Value(rect.y));
2394
+ result.setProperty(runtime, "width", jsi::Value(rect.width));
2395
+ result.setProperty(runtime, "height", jsi::Value(rect.height));
2396
+ return result;
2397
+ }
2398
+
2474
2399
  jsi::Value Tree::measureLayout(
2475
2400
  jsi::Runtime &runtime,
2476
2401
  const jsi::Value *arguments,
@@ -239,6 +239,12 @@ class Tree {
239
239
  facebook::jsi::Runtime &runtime,
240
240
  const facebook::jsi::Value *arguments,
241
241
  size_t count);
242
+ // Synchronous, unlike the four above - no callback. Throws where SYMBIOTE_HAS_DOM_MEASURE is
243
+ // undetected, same as they do; JS callers must catch it (see imperative.ts).
244
+ facebook::jsi::Value getBoundingClientRect(
245
+ facebook::jsi::Runtime &runtime,
246
+ const facebook::jsi::Value *arguments,
247
+ size_t count);
242
248
  facebook::jsi::Value setIsJSResponder(
243
249
  facebook::jsi::Runtime &runtime,
244
250
  const facebook::jsi::Value *arguments,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/engine",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "SymbioteNative's retained shadow-tree engine — clone-on-write commit path + event normalization over React Native Fabric, shared by every framework adapter.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "peerDependenciesMeta": {},
73
73
  "devDependencies": {
74
- "@symbiote-native/test-utils": "0.4.2"
74
+ "@symbiote-native/test-utils": "0.4.5"
75
75
  },
76
76
  "scripts": {
77
77
  "typecheck": "tsc --build",