@symbiote-native/engine 0.5.0 → 1.0.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 (91) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -8
  10. package/build/accessibility-props.js +13 -16
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +125 -0
  28. package/build/host-access.js +280 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +196 -30
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +222 -0
  38. package/build/mutation-buffer.js +491 -0
  39. package/build/native-engine.d.ts +182 -0
  40. package/build/native-engine.js +178 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +66 -0
  43. package/build/node.d.ts +172 -57
  44. package/build/node.js +839 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +307 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +232 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2478 -0
  80. package/cpp/SymbioteTree.h +257 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
@@ -0,0 +1,280 @@
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.
37
+ import { flushOps, settleBeforeFlush, treeHost } from './tree-host.js';
38
+ import { hasPendingPlacement } from './mutation-buffer.js';
39
+ import { functionPropOf, functionPropsOf, isSymbioteNode, RAW_TEXT_COMPONENT, SURFACE_COMPONENT, } from './node.js';
40
+ // One frozen empty list rather than a fresh `[]`, because the fast path in `childrenOf` is the
41
+ // common answer during a build: solid asks 2 000 times on a 1 000-row create and every answer is
42
+ // this.
43
+ const NO_CHILDREN = [];
44
+ /**
45
+ * The node's parent, or `undefined` for a node that sits directly under a surface.
46
+ *
47
+ * A top-level node answers `undefined` even though a surface IS a node in the host's tree, and the
48
+ * three adapters depend on that exact miss: Angular reads `null` as "defer, `<ng-content>` will place
49
+ * this", while Vue and Solid spell `?? surface` at their call sites and compare the result against
50
+ * the `SymbioteSurface` object, which the surface's anchor node is not. `SURFACE_COMPONENT` is the
51
+ * sentinel that stops the answer there.
52
+ *
53
+ * So `undefined` is not the same question as "is this node attached".
54
+ *
55
+ * IT DOES NOT ALWAYS DRAIN, unlike its neighbours. A node's parent link changes only through an op
56
+ * that names it as the CHILD, so a node the pending batch has not placed already has its final
57
+ * answer standing in the host — see `hasPendingPlacement`. Angular's 1 000-row create asks this
58
+ * 1 000 times (its `addLViewToLContainer` calls `renderer.parentNode` once per embedded view) and
59
+ * exactly ONE of those reads was about a node the batch had touched.
60
+ */
61
+ export function parentOf(node) {
62
+ // Asked FIRST and outside the gate: an adapter holding a coalesced write has ops that are not in
63
+ // the buffer yet, so the gate cannot judge this node until they are. A listener that re-places
64
+ // this very node lands it in `hasPendingPlacement` below, which is then read after the fact.
65
+ settleBeforeFlush();
66
+ if (hasPendingPlacement(node))
67
+ flushOps();
68
+ const parent = treeHost()?.parentOf(node);
69
+ // A runtime guard, not a cast: the host stores handles as bare objects, and the brand is what says
70
+ // one of them is ours. It also refuses anything a foreign host might hand back.
71
+ if (!isSymbioteNode(parent))
72
+ return undefined;
73
+ return parent.component === SURFACE_COMPONENT ? undefined : parent;
74
+ }
75
+ /**
76
+ * The node's children, INCLUDING anchors.
77
+ *
78
+ * Anchors are structural bookkeeping — the commit skips them — but they are not invisible to
79
+ * traversal, and hiding them here would desync a framework runtime from the tree it built:
80
+ * solid-js/universal keeps its own record of what it inserted and re-derives positions through these
81
+ * lookups, so a node it placed must be a node it can find.
82
+ */
83
+ export function childrenOf(node) {
84
+ // A READ IS A BATCH BOUNDARY, which is what makes this guard worth a field. `flushOps` below
85
+ // drains the buffer into the host, so a question whose answer is EMPTY still cuts the op stream
86
+ // in two and costs a crossing. Measured on solid's 1 000-row create: 2 000 child-list reads, every
87
+ // one returning zero handles, and 2 002 drains of a buffer that should have crossed twice.
88
+ //
89
+ // `mayHaveChildren` is raised by the two structural recorders in `node.ts` and never lowered, so
90
+ // FALSE is a certainty and TRUE only means "ask". See its declaration for why it is not a tree.
91
+ if (!node.mayHaveChildren)
92
+ return NO_CHILDREN;
93
+ flushOps();
94
+ return treeHost()?.childrenOf(node).filter(isSymbioteNode) ?? [];
95
+ }
96
+ /**
97
+ * Every node's parent, positionally, in ONE crossing — `parentOf` for a list.
98
+ *
99
+ * Engine-internal, like `subtreesOf` below, and for the same reason: what a framework seam needs is
100
+ * the singular form, one step at a time. These two answer the question only TEARDOWN asks, and
101
+ * teardown is the one lifecycle event whose size is the tree's (see `ITreeHost`).
102
+ *
103
+ * A `SURFACE_COMPONENT` parent reads as `undefined` here exactly as it does in `parentOf`, so the
104
+ * two agree element for element.
105
+ */
106
+ export function parentsOf(nodes) {
107
+ flushOps();
108
+ const parents = treeHost()?.parentsOf(nodes);
109
+ if (parents === undefined)
110
+ return nodes.map(() => undefined);
111
+ return parents.map(parent => {
112
+ if (!isSymbioteNode(parent))
113
+ return undefined;
114
+ return parent.component === SURFACE_COMPONENT ? undefined : parent;
115
+ });
116
+ }
117
+ /** Each root and every descendant, pre-order, anchors included — all of it in ONE crossing. */
118
+ export function subtreesOf(roots) {
119
+ flushOps();
120
+ return treeHost()?.subtreesOf(roots).filter(isSymbioteNode) ?? [];
121
+ }
122
+ /**
123
+ * The node and every ancestor above it, deepest first, in ONE crossing.
124
+ *
125
+ * `parentOf` per level is a crossing per level, and the event path needs this chain for every
126
+ * event — capture reads it reversed, bubble forward — plus again on every frame of a drag, for the
127
+ * responder's scope. Measured at 18 crossings per event on a depth-8 chain before the two phases
128
+ * shared a walk, 9 after, and 1 through here.
129
+ *
130
+ * Surfaces are dropped the same way `parentOf` drops them, by component, so a caller sees the same
131
+ * chain it would have built by walking.
132
+ */
133
+ export function ancestorsOf(node) {
134
+ flushOps();
135
+ const chain = treeHost()?.ancestorsOf(node) ?? [];
136
+ const out = [];
137
+ for (const each of chain) {
138
+ // The same runtime guard `parentOf` applies, for the same two reasons: the host stores handles
139
+ // as bare objects, and a surface ends the chain rather than appearing in it.
140
+ if (!isSymbioteNode(each) || each.component === SURFACE_COMPONENT)
141
+ break;
142
+ out.push(each);
143
+ }
144
+ return out;
145
+ }
146
+ /** The first child, anchors included, or `undefined` for a leaf. */
147
+ export function firstChildOf(node) {
148
+ return childrenOf(node)[0];
149
+ }
150
+ /**
151
+ * The next sibling, or `undefined` at the end of the list.
152
+ *
153
+ * `surface` is required to answer for a TOP-LEVEL node, which has no parent to read the sibling
154
+ * list from — the surface owns that list instead. Passing it for a parented node is harmless and
155
+ * ignored, so a caller with one active surface can pass it unconditionally.
156
+ */
157
+ export function nextSiblingOf(node, surface) {
158
+ // ONE host call, not `parentOf` plus a whole child list.
159
+ //
160
+ // The previous spelling built every sibling to read one of them, and a keyed patch calls this
161
+ // once per row: measured on vue, appending 1 000 rows to 1 000 standing crossed 1 002 001 handles
162
+ // and removing them 2 002 002 — quadratic in the list, with every handle a host object built,
163
+ // filtered and discarded. The host holds the list and can scan it in place.
164
+ //
165
+ // `surface` is no longer read and stays in the signature because three adapters pass it: a
166
+ // top-level node's parent IS the surface node in the host's tree, so the host answers that case
167
+ // without being told which surface. The old code needed it only because `parentOf` masks a
168
+ // surface parent to `undefined` and there was then nothing left to ask.
169
+ flushOps();
170
+ const sibling = treeHost()?.nextSiblingOf(node);
171
+ return isSymbioteNode(sibling) ? sibling : undefined;
172
+ }
173
+ /**
174
+ * Whether the node is a TEXT CONTAINER (`<Text>`), not whether it holds a string.
175
+ *
176
+ * The distinction is load-bearing for adapters that ask "can I write a string into this": a raw
177
+ * text node answers FALSE here, and an anchor does too. Use `isRawTextNode` for that question.
178
+ */
179
+ export function isTextContainer(node) {
180
+ return node.isText;
181
+ }
182
+ /**
183
+ * Whether the node is a RAW TEXT node — one a string can be written into.
184
+ *
185
+ * This is the question `solid-js/universal`'s `insertExpression` actually asks before calling
186
+ * replaceText, and answering it with `isTextContainer` would be wrong in both directions: a
187
+ * `<Text>` is a container that holds no string of its own, and the empty-string ANCHOR that
188
+ * cleanChildren leaves to hold a position is not writable either. An anchor is excluded here by
189
+ * construction, since its component is the `#anchor` sentinel.
190
+ */
191
+ export function isRawTextNode(node) {
192
+ return node.component === RAW_TEXT_COMPONENT;
193
+ }
194
+ /**
195
+ * The Fabric view name this node currently resolves to (`RCTView`, `RCTText`, `RCTRawText`, the
196
+ * `#anchor` sentinel …). Exposed because two seams branch on it — Solid to answer `isTextNode`,
197
+ * Angular to recognise its own anchor hosts — and both read `node.component` directly today.
198
+ *
199
+ * NOT stable across a node's life: a primitive whose native view depends on a prop (`TextInput`'s
200
+ * `multiline`) changes view without changing identity. Read it, never cache it.
201
+ */
202
+ export function componentOf(node) {
203
+ return node.component;
204
+ }
205
+ /**
206
+ * The string a raw-text node currently holds, or `undefined` for any other node.
207
+ *
208
+ * Exists for the DIAGNOSTIC path rather than the render path: a seam that rejects a bare string
209
+ * outside a `<Text>` wants to name the offending text in its error, and reading `node.props.text`
210
+ * to do so is the last thing keeping that seam coupled to the node's shape. Vue's
211
+ * `setElementText` reads the same value for a real reason, so this is not a one-caller accessor.
212
+ */
213
+ export function textOf(node) {
214
+ if (node.component !== RAW_TEXT_COMPONENT)
215
+ return undefined;
216
+ flushOps();
217
+ const text = treeHost()?.propOf(node, 'text');
218
+ return typeof text === 'string' ? text : undefined;
219
+ }
220
+ /**
221
+ * The value currently standing on a prop, `undefined` if none is set.
222
+ *
223
+ * The one accessor here that reads a VALUE rather than the tree, and it earns its place from a
224
+ * real caller: Vue's `v-model` shim has to compose with whatever `onValueChange` the author
225
+ * already bound, so it must read the standing handler before writing its own. Without this the
226
+ * shim reaches for `el.props`, which is the last field read keeping a non-seam file coupled to the
227
+ * node's shape.
228
+ *
229
+ * Deliberately NOT a general property bag escape hatch: it answers what is on the node, which for
230
+ * `style` after a class merge is the `[classStyle, explicitStyle]` ARRAY rather than the author's
231
+ * object. `getExplicitStyle` exists for that question and this is not a substitute for it.
232
+ */
233
+ const NO_PROPS = {};
234
+ /**
235
+ * Every prop standing on the node, function props included.
236
+ *
237
+ * The bag a payload fold reads. Two sources, because a function never crossed the wire (`writeProp`,
238
+ * node.ts): the host holds the values it could take, JS holds the callbacks it could not, and a fold
239
+ * asking for `onPress` must see the one the app wrote rather than the `undefined` the host was
240
+ * handed in its place.
241
+ *
242
+ * A COPY when anything was stashed, the host's own object when nothing was — which is nearly every
243
+ * node. Same caveat as `propOf`: `style` after a class merge is the `[classStyle, explicitStyle]`
244
+ * array, not the author's object.
245
+ */
246
+ export function propsOf(node) {
247
+ flushOps();
248
+ const stored = treeHost()?.propsOf(node) ?? NO_PROPS;
249
+ const stashed = functionPropsOf(node);
250
+ if (stashed === undefined)
251
+ return stored;
252
+ const merged = { ...stored };
253
+ for (const [key, value] of stashed)
254
+ merged[key] = value;
255
+ return merged;
256
+ }
257
+ /**
258
+ * A TEST read: the PAYLOAD the last commit handed Fabric for this node, `undefined` before one.
259
+ *
260
+ * `propsOf` above is the props AS THE OPS NAMED THEM — what the adapter said. This is what the
261
+ * payload builder MADE of them, after the aria fold, the behavior's own fold, the component-keyed
262
+ * folds and the style hoist. The two answer different questions and a test has to pick: "did the
263
+ * adapter write `inputMode`" is `propsOf`, and "did that reach native as `keyboardType`" is this.
264
+ *
265
+ * Only the native host can answer it — see `ITreeHost.committedPayloadOf`. Under the recording host
266
+ * it throws, on purpose, naming the itest suite as where the question belongs.
267
+ */
268
+ export function committedPayloadOf(node) {
269
+ flushOps();
270
+ return treeHost()?.committedPayloadOf(node);
271
+ }
272
+ export function propOf(node, key) {
273
+ // Before the host, because a function prop never reached it — see `writeProp`. Cheap enough to be
274
+ // unconditional: this runs at gesture and lifecycle rate, never on a commit path.
275
+ const stashed = functionPropOf(node, key);
276
+ if (stashed !== undefined)
277
+ return stashed;
278
+ flushOps();
279
+ return treeHost()?.propOf(node, key);
280
+ }
@@ -2,12 +2,12 @@ import type { ISymbioteNode } from './node';
2
2
  /**
3
3
  * A pure props -> props mapping a behavior applies on the way to the Fabric payload.
4
4
  *
5
- * It exists because a lowered element has no component body, and a wrapper's body is where the
6
- * per-primitive prop FOLDS live — TextInput's W3C aliases (`inputMode` -> `keyboardType`,
7
- * `readOnly` -> `editable`), Pressable's `disabled` -> `accessibilityState`. Every one of those was
8
- * silently dropped the moment the primitive lowered: the raw alias reached Fabric as a key no
9
- * ViewConfig declares, so nothing threw and nothing rendered differently in a headless test —
10
- * only the device showed a numeric keyboard that never appeared.
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
11
  *
12
12
  * NOT a hook on `setProp`, for the same reason `afterCommit` is not: `setProp` is the hottest path
13
13
  * in the engine. This runs once per node per payload build, and only for a node whose behavior
@@ -29,23 +29,83 @@ export interface IHostBehavior {
29
29
  buildStructure?(node: ISymbioteNode): ISymbioteNode | undefined;
30
30
  attach(node: ISymbioteNode): void;
31
31
  attachAfterCommit?(node: ISymbioteNode): void;
32
- onWrapChange?(owner: ISymbioteNode, wrapper: ISymbioteNode | undefined): void;
33
32
  onOwnedListenerChange?(node: ISymbioteNode, name: string, wired: boolean): void;
34
33
  afterCommit?(node: ISymbioteNode): void;
35
34
  detach(node: ISymbioteNode): void;
35
+ /**
36
+ * This primitive's own prop folds. See IPayloadFold.
37
+ *
38
+ * NO PRODUCTION BEHAVIOR DECLARES ONE since 2026-09-18 — the sticky header's was the last, and it
39
+ * is `foldStickyHeaderProps` in `SymbioteFabricProps.cpp` now. So this reads as a leftover, and
40
+ * the question this codebase asks of one is what it REACHES rather than who uses it today. Two
41
+ * answers, and either alone would keep it:
42
+ *
43
+ * - It is the JS ARM of every cost measurement in `tag-rule-cost.itest.ts`. That file prices a
44
+ * rule by running it on both sides of the wire with the payloads asserted equal key by key
45
+ * first; without a fold there is no second arm, and the ~9-31 us-per-node figures every port in
46
+ * this migration was justified by could not be taken again.
47
+ * - It is the declared seam a third-party behavior would extend. A tag rule is ours to write in
48
+ * C++; a package that ships its own primitive has no such option.
49
+ *
50
+ * What it is NOT any more is where a Symbiote primitive puts its platform props. A new one that
51
+ * reaches for this is one whose rule belongs in the engine — read `SymbioteFabricProps.h` first.
52
+ */
36
53
  readonly foldPayload?: IPayloadFold;
54
+ /**
55
+ * Resolve `source` / `defaultSource` / `loadingIndicatorSource` on the way IN — Image's, and only
56
+ * Image's. See `image-source-write.ts`.
57
+ *
58
+ * It is declared here rather than inferred from the tag because it is a statement about what the
59
+ * primitive's props MEAN, which is exactly what a behavior is for. And it is a flag rather than a
60
+ * function: `routeProp` is the hottest path in the engine, so what it can afford per write is a
61
+ * boolean read on the node, not a call into a behavior.
62
+ */
63
+ readonly resolvesImageSources?: boolean;
64
+ /**
65
+ * Flips `id`/`nativeID` precedence to nativeID-over-id — TouchableWithoutFeedback's, and only
66
+ * its. See `routeIdAlias` in `node.ts` for why the flip exists and which vendor file demands it.
67
+ */
68
+ readonly nativeIdWinsOverId?: boolean;
37
69
  }
38
70
  export declare function registerHostBehavior(component: string, behavior: IHostBehavior): void;
39
71
  export declare function hostBehaviorFor(tag: string): IHostBehavior | undefined;
40
72
  export declare function hasHostBehaviors(): boolean;
73
+ /**
74
+ * Has a behavior ever ATTACHED to a node, as opposed to a behavior TYPE having been registered?
75
+ *
76
+ * `hasBehaviors` answers the second, and it is on from module load in every app: registering
77
+ * `Pressable` arms it whether or not one is ever mounted. It gates `createElement`'s attach probe
78
+ * correctly — a node has to be offered to the registry to find out. It gates the TEARDOWN SWEEP
79
+ * wrongly, and that is expensive: the sweep crosses every removed node into JS, which on a
80
+ * 1 000-row clear is ten thousand handles. Measured on `build-release`
81
+ * (`teardown-sweep-cost.itest.ts`): 1.8 ms with the sweep off against 6.3 ms with it on, i.e. 3.2x,
82
+ * all of it inside the commit. `Clear` is the one row where stock React Native beats every adapter.
83
+ *
84
+ * Nothing the sweep does can matter before the first attach, and the four collections say so:
85
+ * `attached` is written only by `attachHostBehavior`; `awaitingCommit` and `committedEachTime` are
86
+ * written only inside a `behavior.` branch; `parked` only by `detachAnimatedProps`, which has its
87
+ * own gate. The one remaining effect is marking `tornDown`, which exists so a later re-insert knows
88
+ * to re-arm — and there is nothing to re-arm.
89
+ *
90
+ * MONOTONE, deliberately: it turns on and never off, so it needs no accounting on a `WeakMap` that
91
+ * has no size and no destructor. Being late is the only way it can be wrong, and it cannot be late.
92
+ */
93
+ export declare function hasAttachedBehaviors(): boolean;
41
94
  export declare function slotPropNameFor(node: ISymbioteNode, key: string): string | undefined;
42
95
  export declare function slotTakesChildren(node: ISymbioteNode): boolean;
43
96
  export declare function addDerivedNode(owner: ISymbioteNode, node: ISymbioteNode): void;
44
97
  export declare function derivedNodesOf(owner: ISymbioteNode): readonly ISymbioteNode[] | undefined;
45
98
  export declare function notifyOwnedListenerChange(node: ISymbioteNode, name: string, wired: boolean): void;
46
99
  export declare function notifyChildInserted(node: ISymbioteNode, child: ISymbioteNode): void;
47
- export declare function notifyWrapChange(owner: ISymbioteNode, wrapper: ISymbioteNode | undefined): void;
48
100
  export declare function claimModeFor(node: ISymbioteNode, component: string): IClaimMode | undefined;
101
+ /**
102
+ * Every owner key feeds the slot. The spelling for a `cloneElement` primitive, whose slot is not
103
+ * derived from a named set at all — see `IHostBehavior.slotDerived`.
104
+ *
105
+ * A sentinel in the SAME array rather than a second field, so `slotDerivesFrom` stays one lookup and
106
+ * a behavior that wants both spellings cannot express a contradiction.
107
+ */
108
+ export declare const SLOT_DERIVED_ALL = "*";
49
109
  export declare function slotDerivesFrom(node: ISymbioteNode, key: string): boolean;
50
110
  export declare function ownsListener(node: ISymbioteNode, name: string): boolean;
51
111
  export declare function stashAppListener(node: ISymbioteNode, name: string, listener: unknown): void;
@@ -61,23 +121,26 @@ export declare function attachHostBehavior(node: ISymbioteNode, tag: string): vo
61
121
  */
62
122
  export declare function runDeferredAttaches(isCommitted: (node: ISymbioteNode) => boolean): void;
63
123
  /**
64
- * The recurring beat. Split from `runDeferredAttaches` because the two answer different questions:
65
- * `attachAfterCommit` needs a FRESH FABRIC TAG, so it belongs below `completeRoot` and must not run
66
- * on a commit that made no native call; `afterCommit` needs only "props were published", which a
67
- * no-op commit satisfies just as well.
68
- *
69
- * Keeping them together made `afterCommit` unreachable for exactly the props a behavior owns: a fold
70
- * that STRIPS a prop makes its own commit byte-identical, `commitContainer` returns above the drain,
71
- * and the hook never sees the flip. TouchableOpacity's re-settle on `disabled` is the case
72
- * (`disabled` is a MACHINE_ONLY key), Button's `title`/`color` the other.
124
+ * Arm a node's recurring hook for the next commit.
73
125
  *
74
- * SETUP STILL RUNS BEFORE THE BEAT on the first commit, and the order is load-bearing: a node
75
- * carrying both hooks has `attachAfterCommit` seed the mirrors `afterCommit` compares against. The
76
- * caller preserves it by calling this AFTER `runDeferredAttaches` on the changed path — the no-op
77
- * path has no setup to run, since a node with no Fabric tag has not committed at all.
126
+ * Called from `setProp` — gated there on `node.hasCommitHook`, a boolean field beside
127
+ * `hasAriaAlias` on the same hidden class, so a node without a recurring hook pays one load and
128
+ * one branch per write and never reaches this.
78
129
  */
130
+ export declare function noteCommitHookNodeChanged(node: ISymbioteNode): void;
79
131
  export declare function runCommittedHooks(isCommitted: (node: ISymbioteNode) => boolean): void;
80
132
  export declare function markDetachCandidate(node: ISymbioteNode): void;
133
+ /**
134
+ * Whether the sweep has anything to do — asked BEFORE its arguments are built.
135
+ *
136
+ * The sweep's own first line already returns on an empty candidate set, and that was not enough:
137
+ * its caller passes `surface.children`, which is a GETTER that crosses to the host, allocates the
138
+ * whole top-level list and filters it into a second array. On a surface holding four thousand rows
139
+ * that ran on every commit, including the ones with nothing to sweep, because an argument is
140
+ * evaluated before the guard inside the callee can decline. Same shape as the `dlog` arguments that
141
+ * cost Angular 5-10% while emitting nothing.
142
+ */
143
+ export declare function hasDetachCandidates(): boolean;
81
144
  export declare function sweepDetachedBehaviors(topLevel: readonly ISymbioteNode[], onDetached: (node: ISymbioteNode) => void): void;
82
145
  export declare function teardownSubtree(node: ISymbioteNode, onDetached: (node: ISymbioteNode) => void): void;
83
146
  export declare function reattachHostBehaviors(node: ISymbioteNode): void;