@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
@@ -1,39 +1,21 @@
1
- // Host access — the READ half of a DOM, which Fabric does not ship.
2
- //
3
- // Mostly navigation, plus the two value reads a seam needs (`textOf`, `propOf`). Named for the
4
- // whole rather than for navigation alone: this is the surface an adapter is allowed to ask a node
5
- // about, and keeping value reads out of it would only push them back to raw field access, which is
6
- // the thing being removed.
7
- //
8
- // Four of five framework renderer seams navigate the host on their hot paths, and it is their
9
- // contract, not our choice: Solid's nodeOps declare getParentNode / getFirstChild / getNextSibling,
10
- // Vue's RendererOptions declare parentNode / nextSibling, Angular's Renderer2 declares the same
11
- // pair, and Svelte's compiled output reaches firstChild / nextSibling as real prototype getters.
12
- // React is the only one that needs none, because it navigates its own fibers.
13
- //
14
- // In a browser the DOM answers these. Fabric cannot: `nativeFabricUIManager` exposes no structural
15
- // read at all, and RN's `NativeDOM` (which does expose getChildNodes / getParentNode) answers
16
- // against the CURRENT REVISION — so a node that is created, moved or removed but not yet committed
17
- // answers empty or null. A reconciler navigates the tree it is mid-way through BUILDING, which is
18
- // exactly the state no committed revision holds. See the `symbiote-fabric-cxx-surface` skill, §1a
19
- // and §7b.
20
- //
21
- // So these accessors exist to give adapters the navigation their seams require WITHOUT handing them
22
- // the node's fields. There are no fields left to hand them: since 2026-09-08 every one of these is a
23
- // read into the TREE HOST (`tree-host.ts`) — native on device, the TypeScript applier headlessly —
24
- // because JS holds no tree at all. The historical note: every seam used to read `node.parent` /
25
- // `node.children` directly (8 reads in Solid's renderer, 12 in Vue's, 13 in Angular's), which
26
- // couples each adapter to a shape that no longer exists on this side of the wire.
27
- //
28
- // Each read FLUSHES the mutation buffer first. A reconciler navigates the tree it is mid-way through
29
- // BUILDING, so the host has to be told about the ops recorded since the last commit before it can
30
- // answer. That is what `flushOps` is for, and it is why these are not simply the host's own methods.
31
- //
32
- // A survey of the three adapters that navigate (Vue, Angular, Solid) confirmed none can answer from
33
- // state it already holds — Vue's `RendererOptions` callbacks cannot see the vnode tree, Solid's
34
- // `universal.js` deliberately re-derives from the host rather than trust its own record, and Angular
35
- // calls `parentNode` exactly where its TNode/LView does not know. Pushing this into the adapters
36
- // would build three JS trees instead of the one being removed.
1
+ // Host access — the read half of a DOM, which Fabric does not ship. Mostly navigation, plus the
2
+ // two value reads a seam needs (textOf, propOf) — named for the whole because this is the surface
3
+ // an adapter may ask a node about, and keeping value reads out would push them back to raw fields.
4
+ // Four of five framework renderer seams navigate the host on their hot paths, and it's their
5
+ // contract, not our choice: Solid, Vue and Angular all declare parent/sibling navigation
6
+ // callbacks. Only React needs none — it navigates its own fibers.
7
+ // In a browser the DOM answers these; Fabric cannot. nativeFabricUIManager exposes no structural
8
+ // read, and RN's NativeDOM answers against the current revision — a node created, moved or removed
9
+ // but not yet committed answers empty or null, exactly the state a mid-build reconciler is in.
10
+ // So these accessors give adapters the navigation their seams require without handing them the
11
+ // node's fields: every one reads into the tree host (tree-host.ts) — native on device, a
12
+ // TypeScript applier headlessly — because JS holds no tree at all.
13
+ // Each read flushes the mutation buffer first: a reconciler navigates the tree it is mid-way
14
+ // through building, so the host must be told about ops recorded since the last commit before it
15
+ // can answer — that's what flushOps is for.
16
+ // None of the three navigating adapters (Vue, Angular, Solid) can answer from state they already
17
+ // hold on their own side — pushing this into the adapters would build three JS trees instead of
18
+ // the one being removed.
37
19
  import { flushOps, settleBeforeFlush, treeHost } from './tree-host.js';
38
20
  import { hasPendingPlacement } from './mutation-buffer.js';
39
21
  import { hasAnimatedBindings } from './animated/host-binding.js';
@@ -42,68 +24,45 @@ import { functionPropOf, functionPropsOf, isSymbioteNode, RAW_TEXT_COMPONENT, SU
42
24
  // common answer during a build: solid asks 2 000 times on a 1 000-row create and every answer is
43
25
  // this.
44
26
  const NO_CHILDREN = [];
45
- /**
46
- * The node's parent, or `undefined` for a node that sits directly under a surface.
47
- *
48
- * A top-level node answers `undefined` even though a surface IS a node in the host's tree, and the
49
- * three adapters depend on that exact miss: Angular reads `null` as "defer, `<ng-content>` will place
50
- * this", while Vue and Solid spell `?? surface` at their call sites and compare the result against
51
- * the `SymbioteSurface` object, which the surface's anchor node is not. `SURFACE_COMPONENT` is the
52
- * sentinel that stops the answer there.
53
- *
54
- * So `undefined` is not the same question as "is this node attached".
55
- *
56
- * IT DOES NOT ALWAYS DRAIN, unlike its neighbours. A node's parent link changes only through an op
57
- * that names it as the CHILD, so a node the pending batch has not placed already has its final
58
- * answer standing in the host — see `hasPendingPlacement`. Angular's 1 000-row create asks this
59
- * 1 000 times (its `addLViewToLContainer` calls `renderer.parentNode` once per embedded view) and
60
- * exactly ONE of those reads was about a node the batch had touched.
61
- */
27
+ // The node's parent, or undefined for a node that sits directly under a surface. A top-level node
28
+ // answers undefined even though a surface IS a node in the host's tree — three adapters depend on
29
+ // that exact miss, comparing the result against the SymbioteSurface object it is not.
30
+ // Does not always drain, unlike its neighbours: a node's parent link changes only through an op
31
+ // that names it as the child, so a node the pending batch hasn't placed already has its final
32
+ // answer standing in the host (see hasPendingPlacement).
62
33
  export function parentOf(node) {
63
- // Asked FIRST and outside the gate: an adapter holding a coalesced write has ops that are not in
64
- // the buffer yet, so the gate cannot judge this node until they are. A listener that re-places
65
- // this very node lands it in `hasPendingPlacement` below, which is then read after the fact.
34
+ // Asked first and outside the gate: an adapter holding a coalesced write has ops not in the
35
+ // buffer yet, so the gate can't judge this node until they are. A listener re-placing this very
36
+ // node lands it in hasPendingPlacement below, read after the fact.
66
37
  settleBeforeFlush();
67
38
  if (hasPendingPlacement(node))
68
39
  flushOps();
69
40
  const parent = treeHost()?.parentOf(node);
70
- // A runtime guard, not a cast: the host stores handles as bare objects, and the brand is what says
71
- // one of them is ours. It also refuses anything a foreign host might hand back.
41
+ // A runtime guard, not a cast: the host stores handles as bare objects, and the brand is what
42
+ // says one of them is ours. It also refuses anything a foreign host might hand back.
72
43
  if (!isSymbioteNode(parent))
73
44
  return undefined;
74
45
  return parent.component === SURFACE_COMPONENT ? undefined : parent;
75
46
  }
76
- /**
77
- * The node's children, INCLUDING anchors.
78
- *
79
- * Anchors are structural bookkeeping — the commit skips them — but they are not invisible to
80
- * traversal, and hiding them here would desync a framework runtime from the tree it built:
81
- * solid-js/universal keeps its own record of what it inserted and re-derives positions through these
82
- * lookups, so a node it placed must be a node it can find.
83
- */
47
+ // The node's children, including anchors. Anchors are structural bookkeeping (the commit skips
48
+ // them) but not invisible to traversal — solid-js/universal re-derives positions through these
49
+ // lookups, so a node it placed must be a node it can find.
84
50
  export function childrenOf(node) {
85
- // A READ IS A BATCH BOUNDARY, which is what makes this guard worth a field. `flushOps` below
86
- // drains the buffer into the host, so a question whose answer is EMPTY still cuts the op stream
87
- // in two and costs a crossing. Measured on solid's 1 000-row create: 2 000 child-list reads, every
88
- // one returning zero handles, and 2 002 drains of a buffer that should have crossed twice.
89
- //
90
- // `mayHaveChildren` is raised by the two structural recorders in `node.ts` and never lowered, so
91
- // FALSE is a certainty and TRUE only means "ask". See its declaration for why it is not a tree.
51
+ // A read is a batch boundary, which is what makes this guard worth a field: flushOps below
52
+ // drains the buffer into the host, so a question whose answer is empty still cuts the op stream
53
+ // in two and costs a crossing.
54
+ // mayHaveChildren is raised by the two structural recorders in node.ts and never lowered, so
55
+ // false is a certainty and true only means "ask". See its declaration for why it's not a tree.
92
56
  if (!node.mayHaveChildren)
93
57
  return NO_CHILDREN;
94
58
  flushOps();
95
59
  return treeHost()?.childrenOf(node).filter(isSymbioteNode) ?? [];
96
60
  }
97
- /**
98
- * Every node's parent, positionally, in ONE crossing — `parentOf` for a list.
99
- *
100
- * Engine-internal, like `subtreesOf` below, and for the same reason: what a framework seam needs is
101
- * the singular form, one step at a time. These two answer the question only TEARDOWN asks, and
102
- * teardown is the one lifecycle event whose size is the tree's (see `ITreeHost`).
103
- *
104
- * A `SURFACE_COMPONENT` parent reads as `undefined` here exactly as it does in `parentOf`, so the
105
- * two agree element for element.
106
- */
61
+ // Every node's parent, positionally, in one crossing — parentOf for a list. Engine-internal, like
62
+ // subtreesOf below: a framework seam only ever needs the singular form, one step at a time. These
63
+ // two answer the question only teardown asks, whose size is the tree's (see ITreeHost).
64
+ // A SURFACE_COMPONENT parent reads as undefined here exactly as it does in parentOf, so the two
65
+ // agree element for element.
107
66
  export function parentsOf(nodes) {
108
67
  flushOps();
109
68
  const parents = treeHost()?.parentsOf(nodes);
@@ -118,37 +77,25 @@ export function parentsOf(nodes) {
118
77
  /** Each root and every descendant, pre-order, anchors included — all of it in ONE crossing. */
119
78
  export function subtreesOf(roots) {
120
79
  flushOps();
121
- // The `.filter` is the type NARROWING, not a defensive check, and it costs ~0.6 ms of the 5.5 ms
122
- // a thousand-row clear spends in the engine — measured on `build-release` by returning the host's
123
- // array unnarrowed (`teardown-sweep-cost.itest.ts`, rest of the sweep 2.75 -> 2.16 ms). It stays,
124
- // because removing it means either an `as` or declaring `ITreeHost.subtreesOf` to hand back our
125
- // own type, and a pluggable host is exactly what that `object` boundary is for.
80
+ // The .filter is type narrowing, not a defensive check, and it stays: removing it means either
81
+ // an `as` or declaring ITreeHost.subtreesOf to hand back our own type, and a pluggable host is
82
+ // exactly what that object boundary is for.
126
83
  return treeHost()?.subtreesOf(roots).filter(isSymbioteNode) ?? [];
127
84
  }
128
- /**
129
- * The same walk narrowed to what a teardown must visit — see `ITreeHost.teardownSubtreesOf`.
130
- *
131
- * The gate is the ANIMATED one, not a behavior one: a binding is per node and carries no tag, so an
132
- * app that animates needs every node back and gets the full walk. Nothing else narrows, because
133
- * nothing else is charged per node of a removed subtree.
134
- */
85
+ // The same walk narrowed to what a teardown must visit — see ITreeHost.teardownSubtreesOf. The
86
+ // gate is the animated one, not a behavior one: a binding is per node and carries no tag, so an
87
+ // app that animates needs every node back and gets the full walk.
135
88
  export function teardownSubtreesOf(roots) {
136
89
  if (hasAnimatedBindings())
137
90
  return subtreesOf(roots);
138
91
  flushOps();
139
92
  return treeHost()?.teardownSubtreesOf(roots).filter(isSymbioteNode) ?? [];
140
93
  }
141
- /**
142
- * The node and every ancestor above it, deepest first, in ONE crossing.
143
- *
144
- * `parentOf` per level is a crossing per level, and the event path needs this chain for every
145
- * event — capture reads it reversed, bubble forward — plus again on every frame of a drag, for the
146
- * responder's scope. Measured at 18 crossings per event on a depth-8 chain before the two phases
147
- * shared a walk, 9 after, and 1 through here.
148
- *
149
- * Surfaces are dropped the same way `parentOf` drops them, by component, so a caller sees the same
150
- * chain it would have built by walking.
151
- */
94
+ // The node and every ancestor above it, deepest first, in one crossing. parentOf per level is a
95
+ // crossing per level, and the event path needs this chain for every event (capture reversed,
96
+ // bubble forward), plus again on every drag frame for the responder's scope.
97
+ // Surfaces are dropped the same way parentOf drops them, by component, so a caller sees the same
98
+ // chain it would have built by walking.
152
99
  export function ancestorsOf(node) {
153
100
  flushOps();
154
101
  const chain = treeHost()?.ancestorsOf(node) ?? [];
@@ -162,19 +109,11 @@ export function ancestorsOf(node) {
162
109
  }
163
110
  return out;
164
111
  }
165
- /**
166
- * The first child, anchors included, or `undefined` for a leaf.
167
- *
168
- * ONE HOST CALL, not `childrenOf(node)[0]`, and the difference is a complexity class rather than a
169
- * constant. `solid-js/universal`'s `cleanChildren` empties a parent with
170
- * `while (removed = getFirstChild(parent)) removeNode(parent, removed)` — so the old spelling read a
171
- * list of N, then N-1, then N-2, building and discarding every handle each time. Measured on a
172
- * 2 000-row Solid `Clear` (`solid-clear-scaling.itest.tsx`): **2 001 001 handles** crossed to remove
173
- * two thousand children, N(N+1)/2 to the unit, against the ~2 000 the work needs.
174
- *
175
- * The `mayHaveChildren` fast path is kept for the same reason `childrenOf` has it: FALSE is a
176
- * certainty, so a leaf answers without a drain and without a crossing.
177
- */
112
+ // The first child, anchors included, or undefined for a leaf. One host call, not
113
+ // childrenOf(node)[0] — that spelling is quadratic: solid-js/universal's cleanChildren empties a
114
+ // parent in a loop, so reading the list to take its head crosses a handle per remaining child.
115
+ // The mayHaveChildren fast path is kept for the same reason childrenOf has it: false is a
116
+ // certainty, so a leaf answers without a drain and without a crossing.
178
117
  export function firstChildOf(node) {
179
118
  if (!node.mayHaveChildren)
180
119
  return undefined;
@@ -182,69 +121,41 @@ export function firstChildOf(node) {
182
121
  const child = treeHost()?.firstChildOf(node);
183
122
  return isSymbioteNode(child) ? child : undefined;
184
123
  }
185
- /**
186
- * The next sibling, or `undefined` at the end of the list.
187
- *
188
- * `surface` is required to answer for a TOP-LEVEL node, which has no parent to read the sibling
189
- * list from — the surface owns that list instead. Passing it for a parented node is harmless and
190
- * ignored, so a caller with one active surface can pass it unconditionally.
191
- */
192
- export function nextSiblingOf(node, surface) {
193
- // ONE host call, not `parentOf` plus a whole child list.
194
- //
195
- // The previous spelling built every sibling to read one of them, and a keyed patch calls this
196
- // once per row: measured on vue, appending 1 000 rows to 1 000 standing crossed 1 002 001 handles
197
- // and removing them 2 002 002 — quadratic in the list, with every handle a host object built,
198
- // filtered and discarded. The host holds the list and can scan it in place.
199
- //
200
- // `surface` is no longer read and stays in the signature because three adapters pass it: a
201
- // top-level node's parent IS the surface node in the host's tree, so the host answers that case
202
- // without being told which surface. The old code needed it only because `parentOf` masks a
203
- // surface parent to `undefined` and there was then nothing left to ask.
124
+ // The next sibling, or undefined at the end of the list. `_surface` stays in the signature only
125
+ // because three adapters still pass it, though it's no longer read (see below).
126
+ export function nextSiblingOf(node, _surface) {
127
+ // One host call, not parentOf plus a whole child list — the old spelling built every sibling to
128
+ // read one of them, quadratic on a keyed patch calling this once per row. The host holds the
129
+ // list and can scan it in place.
130
+ // `surface` is unread because a top-level node's parent IS the surface node in the host's tree,
131
+ // so the host answers without being told which surface — needed only because parentOf masks a
132
+ // surface parent to undefined, leaving nothing else to ask.
204
133
  flushOps();
205
134
  const sibling = treeHost()?.nextSiblingOf(node);
206
135
  return isSymbioteNode(sibling) ? sibling : undefined;
207
136
  }
208
- /**
209
- * Whether the node is a TEXT CONTAINER (`<Text>`), not whether it holds a string.
210
- *
211
- * The distinction is load-bearing for adapters that ask "can I write a string into this": a raw
212
- * text node answers FALSE here, and an anchor does too. Use `isRawTextNode` for that question.
213
- */
137
+ // Whether the node is a text container (<Text>), not whether it holds a string. Load-bearing for
138
+ // "can I write a string into this": a raw text node answers false here, and so does an anchor —
139
+ // use isRawTextNode for that question.
214
140
  export function isTextContainer(node) {
215
141
  return node.isText;
216
142
  }
217
- /**
218
- * Whether the node is a RAW TEXT node — one a string can be written into.
219
- *
220
- * This is the question `solid-js/universal`'s `insertExpression` actually asks before calling
221
- * replaceText, and answering it with `isTextContainer` would be wrong in both directions: a
222
- * `<Text>` is a container that holds no string of its own, and the empty-string ANCHOR that
223
- * cleanChildren leaves to hold a position is not writable either. An anchor is excluded here by
224
- * construction, since its component is the `#anchor` sentinel.
225
- */
143
+ // Whether the node is a raw text node — one a string can be written into. The question
144
+ // solid-js/universal's insertExpression actually asks before replaceText: isTextContainer would
145
+ // answer wrong both ways, since a <Text> holds no string of its own and an anchor isn't writable.
226
146
  export function isRawTextNode(node) {
227
147
  return node.component === RAW_TEXT_COMPONENT;
228
148
  }
229
- /**
230
- * The Fabric view name this node currently resolves to (`RCTView`, `RCTText`, `RCTRawText`, the
231
- * `#anchor` sentinel …). Exposed because two seams branch on it — Solid to answer `isTextNode`,
232
- * Angular to recognise its own anchor hosts — and both read `node.component` directly today.
233
- *
234
- * NOT stable across a node's life: a primitive whose native view depends on a prop (`TextInput`'s
235
- * `multiline`) changes view without changing identity. Read it, never cache it.
236
- */
149
+ // The Fabric view name this node currently resolves to (RCTView, RCTText, RCTRawText, the #anchor
150
+ // sentinel...). Exposed because Solid and Angular both branch on it directly.
151
+ // Not stable across a node's life — a primitive whose native view depends on a prop changes view
152
+ // without changing identity, so read it, never cache it.
237
153
  export function componentOf(node) {
238
154
  return node.component;
239
155
  }
240
- /**
241
- * The string a raw-text node currently holds, or `undefined` for any other node.
242
- *
243
- * Exists for the DIAGNOSTIC path rather than the render path: a seam that rejects a bare string
244
- * outside a `<Text>` wants to name the offending text in its error, and reading `node.props.text`
245
- * to do so is the last thing keeping that seam coupled to the node's shape. Vue's
246
- * `setElementText` reads the same value for a real reason, so this is not a one-caller accessor.
247
- */
156
+ // The string a raw-text node currently holds, or undefined for any other node. Exists for the
157
+ // diagnostic path rather than the render path: a seam rejecting a bare string outside a <Text>
158
+ // wants to name the offending text without coupling to node.props.text's shape.
248
159
  export function textOf(node) {
249
160
  if (node.component !== RAW_TEXT_COMPONENT)
250
161
  return undefined;
@@ -252,32 +163,18 @@ export function textOf(node) {
252
163
  const text = treeHost()?.propOf(node, 'text');
253
164
  return typeof text === 'string' ? text : undefined;
254
165
  }
255
- /**
256
- * The value currently standing on a prop, `undefined` if none is set.
257
- *
258
- * The one accessor here that reads a VALUE rather than the tree, and it earns its place from a
259
- * real caller: Vue's `v-model` shim has to compose with whatever `onValueChange` the author
260
- * already bound, so it must read the standing handler before writing its own. Without this the
261
- * shim reaches for `el.props`, which is the last field read keeping a non-seam file coupled to the
262
- * node's shape.
263
- *
264
- * Deliberately NOT a general property bag escape hatch: it answers what is on the node, which for
265
- * `style` after a class merge is the `[classStyle, explicitStyle]` ARRAY rather than the author's
266
- * object. `getExplicitStyle` exists for that question and this is not a substitute for it.
267
- */
166
+ // The value currently standing on a prop, undefined if none is set. The one accessor here that
167
+ // reads a value rather than the tree: Vue's v-model shim must read the standing onValueChange
168
+ // handler before composing its own, without coupling to el.props's shape.
169
+ // Deliberately not a general property bag escape hatch: it answers what is on the node, which for
170
+ // style after a class merge is the [classStyle, explicitStyle] array, not the author's object —
171
+ // getExplicitStyle exists for that question and this is not a substitute for it.
268
172
  const NO_PROPS = {};
269
- /**
270
- * Every prop standing on the node, function props included.
271
- *
272
- * The bag a payload fold reads. Two sources, because a function never crossed the wire (`writeProp`,
273
- * node.ts): the host holds the values it could take, JS holds the callbacks it could not, and a fold
274
- * asking for `onPress` must see the one the app wrote rather than the `undefined` the host was
275
- * handed in its place.
276
- *
277
- * A COPY when anything was stashed, the host's own object when nothing was — which is nearly every
278
- * node. Same caveat as `propOf`: `style` after a class merge is the `[classStyle, explicitStyle]`
279
- * array, not the author's object.
280
- */
173
+ // Every prop standing on the node, function props included — the bag a payload fold reads. Two
174
+ // sources, since a function never crossed the wire: the host holds values, JS holds the callbacks
175
+ // it couldn't, and a fold asking for onPress must see what the app wrote, not host-side undefined.
176
+ // A copy when anything was stashed, the host's own object when nothing was (nearly every node).
177
+ // Same caveat as propOf: style after a class merge is the array, not the author's object.
281
178
  export function propsOf(node) {
282
179
  flushOps();
283
180
  const stored = treeHost()?.propsOf(node) ?? NO_PROPS;
@@ -289,17 +186,11 @@ export function propsOf(node) {
289
186
  merged[key] = value;
290
187
  return merged;
291
188
  }
292
- /**
293
- * A TEST read: the PAYLOAD the last commit handed Fabric for this node, `undefined` before one.
294
- *
295
- * `propsOf` above is the props AS THE OPS NAMED THEM — what the adapter said. This is what the
296
- * payload builder MADE of them, after the aria fold, the behavior's own fold, the component-keyed
297
- * folds and the style hoist. The two answer different questions and a test has to pick: "did the
298
- * adapter write `inputMode`" is `propsOf`, and "did that reach native as `keyboardType`" is this.
299
- *
300
- * Only the native host can answer it — see `ITreeHost.committedPayloadOf`. Under the recording host
301
- * it throws, on purpose, naming the itest suite as where the question belongs.
302
- */
189
+ // A test read: the payload the last commit handed Fabric for this node, undefined before one.
190
+ // propsOf above is the props as the ops named them — what the adapter said. This is what the
191
+ // payload builder made of them, after every fold and the style hoist.
192
+ // Only the native host can answer it — see ITreeHost.committedPayloadOf. Under the recording host
193
+ // it throws, on purpose, naming the itest suite as where the question belongs.
303
194
  export function committedPayloadOf(node) {
304
195
  flushOps();
305
196
  return treeHost()?.committedPayloadOf(node);
@@ -1,21 +1,4 @@
1
1
  import type { ISymbioteNode } from './node';
2
- /**
3
- * A pure props -> props mapping a behavior applies on the way to the Fabric payload.
4
- *
5
- * It exists because a tag has no component body, and a wrapper's body is where the per-primitive
6
- * prop FOLDS live — TextInput's W3C aliases (`inputMode` -> `keyboardType`, `readOnly` ->
7
- * `editable`), Pressable's `disabled` -> `accessibilityState`. Every one of those was silently
8
- * dropped the moment the wrapper went: the raw alias reached Fabric as a key no ViewConfig
9
- * declares, so nothing threw and nothing rendered differently in a headless test — only the device
10
- * showed a numeric keyboard that never appeared.
11
- *
12
- * NOT a hook on `setProp`, for the same reason `afterCommit` is not: `setProp` is the hottest path
13
- * in the engine. This runs once per node per payload build, and only for a node whose behavior
14
- * supplied one.
15
- *
16
- * MUST be pure and MUST NOT mutate its input — `node.props` is the live bag, and the folds beside
17
- * this one return their input by identity when there is nothing to do.
18
- */
19
2
  export type IPayloadFold = (props: Readonly<Record<string, unknown>>) => Record<string, unknown>;
20
3
  export type IClaimMode = 'beside' | 'wrap';
21
4
  export interface IHostBehavior {
@@ -33,64 +16,13 @@ export interface IHostBehavior {
33
16
  onOwnedListenerChange?(node: ISymbioteNode, name: string, wired: boolean): void;
34
17
  afterCommit?(node: ISymbioteNode): void;
35
18
  detach(node: ISymbioteNode): void;
36
- /**
37
- * This primitive's own prop folds. See IPayloadFold.
38
- *
39
- * NO PRODUCTION BEHAVIOR DECLARES ONE since 2026-09-18 — the sticky header's was the last, and it
40
- * is `foldStickyHeaderProps` in `SymbioteFabricProps.cpp` now. So this reads as a leftover, and
41
- * the question this codebase asks of one is what it REACHES rather than who uses it today. Two
42
- * answers, and either alone would keep it:
43
- *
44
- * - It is the JS ARM of every cost measurement in `tag-rule-cost.itest.ts`. That file prices a
45
- * rule by running it on both sides of the wire with the payloads asserted equal key by key
46
- * first; without a fold there is no second arm, and the ~9-31 us-per-node figures every port in
47
- * this migration was justified by could not be taken again.
48
- * - It is the declared seam a third-party behavior would extend. A tag rule is ours to write in
49
- * C++; a package that ships its own primitive has no such option.
50
- *
51
- * What it is NOT any more is where a Symbiote primitive puts its platform props. A new one that
52
- * reaches for this is one whose rule belongs in the engine — read `SymbioteFabricProps.h` first.
53
- */
54
19
  readonly foldPayload?: IPayloadFold;
55
- /**
56
- * Resolve `source` / `defaultSource` / `loadingIndicatorSource` on the way IN — Image's, and only
57
- * Image's. See `image-source-write.ts`.
58
- *
59
- * It is declared here rather than inferred from the tag because it is a statement about what the
60
- * primitive's props MEAN, which is exactly what a behavior is for. And it is a flag rather than a
61
- * function: `routeProp` is the hottest path in the engine, so what it can afford per write is a
62
- * boolean read on the node, not a call into a behavior.
63
- */
64
20
  readonly resolvesImageSources?: boolean;
65
- /**
66
- * Flips `id`/`nativeID` precedence to nativeID-over-id — TouchableWithoutFeedback's, and only
67
- * its. See `routeIdAlias` in `node.ts` for why the flip exists and which vendor file demands it.
68
- */
69
21
  readonly nativeIdWinsOverId?: boolean;
70
22
  }
71
23
  export declare function registerHostBehavior(component: string, behavior: IHostBehavior): void;
72
24
  export declare function hostBehaviorFor(tag: string): IHostBehavior | undefined;
73
25
  export declare function hasHostBehaviors(): boolean;
74
- /**
75
- * Has a behavior ever ATTACHED to a node, as opposed to a behavior TYPE having been registered?
76
- *
77
- * `hasBehaviors` answers the second, and it is on from module load in every app: registering
78
- * `Pressable` arms it whether or not one is ever mounted. It gates `createElement`'s attach probe
79
- * correctly — a node has to be offered to the registry to find out. It gates the TEARDOWN SWEEP
80
- * wrongly, and that is expensive: the sweep crosses every removed node into JS, which on a
81
- * 1 000-row clear is ten thousand handles. Measured on `build-release`
82
- * (`teardown-sweep-cost.itest.ts`): 1.8 ms with the sweep off against 6.3 ms with it on, i.e. 3.2x,
83
- * all of it inside the commit. `Clear` is the one row where stock React Native beats every adapter.
84
- *
85
- * Nothing the sweep does can matter before the first attach, and the four collections say so:
86
- * `node.hostBehavior` is written only by `attachHostBehavior`; `awaitingCommit` and `committedEachTime` are
87
- * written only inside a `behavior.` branch; `parked` only by `detachAnimatedProps`, which has its
88
- * own gate. The one remaining effect is marking `isTornDown`, which exists so a later re-insert knows
89
- * to re-arm — and there is nothing to re-arm.
90
- *
91
- * MONOTONE, deliberately: it turns on and never off, so it needs no accounting on a `WeakMap` that
92
- * has no size and no destructor. Being late is the only way it can be wrong, and it cannot be late.
93
- */
94
26
  export declare function hasAttachedBehaviors(): boolean;
95
27
  export declare function slotPropNameFor(node: ISymbioteNode, key: string): string | undefined;
96
28
  export declare function slotTakesChildren(node: ISymbioteNode): boolean;
@@ -99,48 +31,16 @@ export declare function derivedNodesOf(owner: ISymbioteNode): readonly ISymbiote
99
31
  export declare function notifyOwnedListenerChange(node: ISymbioteNode, name: string, wired: boolean): void;
100
32
  export declare function notifyChildInserted(node: ISymbioteNode, child: ISymbioteNode): void;
101
33
  export declare function claimModeFor(node: ISymbioteNode, component: string): IClaimMode | undefined;
102
- /**
103
- * Every owner key feeds the slot. The spelling for a `cloneElement` primitive, whose slot is not
104
- * derived from a named set at all — see `IHostBehavior.slotDerived`.
105
- *
106
- * A sentinel in the SAME array rather than a second field, so `slotDerivesFrom` stays one lookup and
107
- * a behavior that wants both spellings cannot express a contradiction.
108
- */
109
34
  export declare const SLOT_DERIVED_ALL = "*";
110
35
  export declare function slotDerivesFrom(node: ISymbioteNode, key: string): boolean;
111
36
  export declare function ownsListener(node: ISymbioteNode, name: string): boolean;
112
37
  export declare function stashAppListener(node: ISymbioteNode, name: string, listener: unknown): void;
113
38
  export declare function appListenerFor(node: ISymbioteNode, name: string): unknown;
114
39
  export declare function attachHostBehavior(node: ISymbioteNode, tag: string): void;
115
- /**
116
- * Run the deferred half of every behavior whose node has now been committed. Called from the commit
117
- * path immediately after `completeRoot`, where fresh Fabric tags have just been assigned.
118
- *
119
- * `isCommitted` is passed in for the same no-cycle reason — `committedOf` lives in `commit.ts`. A
120
- * still-uncommitted node stays in the set: a create superseded before it ever reached Fabric waits
121
- * for the commit that lands it, and `detachSubtree` drops it if that commit never comes.
122
- */
123
40
  export declare function runDeferredAttaches(isCommitted: (node: ISymbioteNode) => boolean): void;
124
- /**
125
- * Arm a node's recurring hook for the next commit.
126
- *
127
- * Called from `setProp` — gated there on `node.hasCommitHook`, a boolean field beside
128
- * `hasAriaAlias` on the same hidden class, so a node without a recurring hook pays one load and
129
- * one branch per write and never reaches this.
130
- */
131
41
  export declare function noteCommitHookNodeChanged(node: ISymbioteNode): void;
132
42
  export declare function runCommittedHooks(isCommitted: (node: ISymbioteNode) => boolean): void;
133
43
  export declare function markDetachCandidate(node: ISymbioteNode): void;
134
- /**
135
- * Whether the sweep has anything to do — asked BEFORE its arguments are built.
136
- *
137
- * The sweep's own first line already returns on an empty candidate set, and that was not enough:
138
- * its caller passes `surface.children`, which is a GETTER that crosses to the host, allocates the
139
- * whole top-level list and filters it into a second array. On a surface holding four thousand rows
140
- * that ran on every commit, including the ones with nothing to sweep, because an argument is
141
- * evaluated before the guard inside the callee can decline. Same shape as the `dlog` arguments that
142
- * cost Angular 5-10% while emitting nothing.
143
- */
144
44
  export declare function hasDetachCandidates(): boolean;
145
45
  export declare function sweepDetachedBehaviors(topLevel: readonly ISymbioteNode[], onDetached: (node: ISymbioteNode) => void): void;
146
46
  export declare function teardownSubtree(node: ISymbioteNode, onDetached: (node: ISymbioteNode) => void): void;