@symbiote-native/engine 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +1 -5
- package/build/back-handler/index.js +0 -6
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- package/build/events/index.js +2 -2
- package/build/fabric-props.js +74 -179
- package/build/fabric.d.ts +0 -13
- package/build/fabric.js +18 -38
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +0 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.js +10 -23
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +0 -11
- package/build/image-source-write.js +14 -34
- package/build/imperative.d.ts +0 -28
- package/build/imperative.js +42 -92
- package/build/index.d.ts +1 -0
- package/build/index.js +24 -37
- package/build/mutation-buffer.d.ts +0 -177
- package/build/mutation-buffer.js +137 -308
- package/build/native-engine.d.ts +0 -102
- package/build/native-engine.js +38 -98
- package/build/native-events.js +9 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +14 -31
- package/build/node.d.ts +0 -212
- package/build/node.js +338 -774
- package/build/post-commit.js +3 -8
- package/build/process-aspect-ratio.js +3 -7
- package/build/process-background-longhands.js +10 -19
- package/build/process-filter.js +11 -19
- package/build/process-font-variant.js +3 -7
- package/build/registry.d.ts +0 -33
- package/build/registry.js +22 -57
- package/build/report-error.js +4 -18
- package/build/structured-style.d.ts +0 -9
- package/build/structured-style.js +16 -31
- package/build/styles.js +3 -6
- package/build/surface.d.ts +0 -26
- package/build/surface.js +29 -76
- package/build/text-input-state.js +4 -8
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +0 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- package/package.json +2 -2
package/build/fabric-props.js
CHANGED
|
@@ -1,41 +1,13 @@
|
|
|
1
1
|
// Fabric-prop translation: turn a node's logical props into the flat payload Fabric's C++ props
|
|
2
|
-
// expect. Color processing
|
|
3
|
-
//
|
|
4
|
-
// CSS-style processors.
|
|
5
|
-
//
|
|
6
|
-
// IT IS CALLED BY THE HOST, and that is what the `props` parameter is for. The NODE still comes in
|
|
7
|
-
// beside it for its authored component name and for a behavior's own `payloadFold`.
|
|
8
|
-
//
|
|
9
|
-
// **THIS IS THE HEADLESS BUILDER, and the device one is `SymbioteFabricProps.cpp`.** The header used
|
|
10
|
-
// to say the C++ side "does not have this yet", which was true mid-branch and stopped being true
|
|
11
|
-
// when the payload builder was ported; a reader who believed it would go looking for a blank screen.
|
|
12
|
-
//
|
|
13
|
-
// What follows from that, and it is the file's main rule: **no platform rule may live here.** The ten
|
|
14
|
-
// tag rules never got a copy, and RN's two Text defaults lost theirs on 2026-09-18. A rule with a
|
|
15
|
-
// copy on this side is a rule whose only test runs on this side, and the device copy can then break
|
|
16
|
-
// with everything green — not hypothetical: it is how a disabled `touchable-highlight` shipped
|
|
17
|
-
// `focusable: true`.
|
|
18
|
-
//
|
|
19
|
-
// **NONE IS LEFT, as of 2026-09-18.** Three went, one per commit, in that order: RN's Text defaults,
|
|
20
|
-
// `value ?? defaultValue -> text`, and the aria fold. Separately on purpose — they had different test
|
|
21
|
-
// topologies, and one commit removing several could not be attributed to any of them.
|
|
22
|
-
//
|
|
23
|
-
// SO THE HEADLESS PAYLOAD DIVERGES FROM THE DEVICE'S, deliberately and in named places: a text
|
|
24
|
-
// input's carries `value` where the device's carries `text`, a text node's is missing two defaults,
|
|
25
|
-
// and a bare tag's `aria-*` keys arrive unfolded. **That asymmetry is the harness working as
|
|
26
|
-
// designed** — it is what forces a claim about a platform rule to be made where the rule runs. Do
|
|
27
|
-
// not close it by adding a rule back.
|
|
28
|
-
//
|
|
29
|
-
// What is left is the framework-agnostic half: colour processing, the style hoist, and a node's own
|
|
30
|
-
// `payloadFold`.
|
|
2
|
+
// expect. Color processing lives in ./platform-color (the stable leaf every color-touching module
|
|
3
|
+
// imports from); this file only decides WHICH props are color props.
|
|
31
4
|
import { RAW_TEXT_COMPONENT } from './node.js';
|
|
32
5
|
import { isProcessableColor, processColor } from './platform-color';
|
|
33
6
|
import { configProcessedKeys } from './registry.js';
|
|
34
7
|
import { isRecord } from './type-guards.js';
|
|
35
|
-
// Color props must reach Fabric as platform ints, not CSS strings
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// keeping shared free of a react-native dependency (and the headless harness working).
|
|
8
|
+
// Color props must reach Fabric as platform ints, not CSS strings — Fabric's C++ color parser
|
|
9
|
+
// silently drops strings. processColor is RN-platform-specific, so it's injected from
|
|
10
|
+
// platform-color.ts rather than imported, keeping this module free of a react-native dependency.
|
|
39
11
|
const COLOR_PROPS = new Set([
|
|
40
12
|
'backgroundColor',
|
|
41
13
|
'color',
|
|
@@ -68,11 +40,9 @@ const COLOR_PROPS = new Set([
|
|
|
68
40
|
// Text decoration color (underline/strike): same Fabric strictness as any color.
|
|
69
41
|
'textDecorationColor',
|
|
70
42
|
'selectionHandleColor',
|
|
71
|
-
// Switch track/thumb colors. RN processColors each via the Switch ViewConfig
|
|
72
|
-
// (
|
|
73
|
-
//
|
|
74
|
-
// trackTintColor, and Android's ColorPropConverter is strict ("the value must be a
|
|
75
|
-
// number or Object"), so a raw CSS string crashes. thumbTintColor reaches both.
|
|
43
|
+
// Switch track/thumb colors. RN processColors each via the Switch ViewConfig; Android's
|
|
44
|
+
// ColorPropConverter is strict ("must be a number or Object"), so a raw CSS string crashes.
|
|
45
|
+
// thumbTintColor reaches both platforms.
|
|
76
46
|
'onTintColor',
|
|
77
47
|
'thumbTintColor',
|
|
78
48
|
'trackColorForTrue',
|
|
@@ -80,50 +50,33 @@ const COLOR_PROPS = new Set([
|
|
|
80
50
|
'trackTintColor',
|
|
81
51
|
]);
|
|
82
52
|
// Convert a prop to the shape Fabric's C++ expects: a CSS-string color runs through the injected
|
|
83
|
-
// platform processor,
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// headless. A third-party view's own `validAttributes[*].process` now runs in `configPayloadFold`
|
|
88
|
-
// (installed on the node, called by the C++ through `payloadFold`); the structured style keys
|
|
89
|
-
// — boxShadow, filter, transform, transformOrigin, aspectRatio, fontVariant,
|
|
90
|
-
// experimental_backgroundImage — now run at WRITE time in `structured-style.ts`, so `node.props`
|
|
91
|
-
// already holds the structured value whichever builder reads it.
|
|
92
|
-
//
|
|
53
|
+
// platform processor, since Fabric's C++ color parser silently drops strings.
|
|
54
|
+
// Any other processor must NOT live here: on a device the payload is built in C++ and this
|
|
55
|
+
// function never runs, so anything resolved only here is resolved only headless. A third-party
|
|
56
|
+
// view's own config processors run in configPayloadFold; structured style keys run at write time.
|
|
93
57
|
// Neither may come back here. Applying a processor twice is not a no-op: a color already converted
|
|
94
|
-
// to a platform int, run through processColor again, is a
|
|
58
|
+
// to a platform int, run through processColor again, is a different color.
|
|
95
59
|
function processValue(key, value, alreadyProcessed) {
|
|
96
|
-
// A key the component's own config already converted is
|
|
97
|
-
//
|
|
98
|
-
// colour. This used to be prevented by accident — a processed colour is a number, and numbers
|
|
99
|
-
// were not processable — until a numeric colour became an author's own rrggbbaa literal.
|
|
60
|
+
// A key the component's own config already converted is done. Running the colour pass over it
|
|
61
|
+
// too is not a no-op: processColor rotates, so a second rotation is a different colour.
|
|
100
62
|
if (alreadyProcessed?.has(key) === true)
|
|
101
63
|
return value;
|
|
102
64
|
if (COLOR_PROPS.has(key) && isProcessableColor(value))
|
|
103
65
|
return processColor(value);
|
|
104
66
|
return value;
|
|
105
67
|
}
|
|
106
|
-
// A style object is
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// setProp compares with Object.is and skips a same-identity write, so an in-place style edit never
|
|
119
|
-
// marks the node dirty and never reaches Fabric today either. What narrows slightly is the case
|
|
120
|
-
// where some OTHER prop on the same node changed in the same commit - that used to pick the
|
|
121
|
-
// mutation up as a side effect, and now does not.
|
|
122
|
-
//
|
|
123
|
-
// KEEPING undefined-valued keys is deliberate: the resolved object is a faithful picture of ONE
|
|
124
|
-
// style entry, and addStyle below needs to see an explicit `undefined` to let a later entry clear
|
|
125
|
-
// an earlier one. Dropping them here would silently turn `[{flex:1},{flex:undefined}]` into
|
|
126
|
-
// `flex: 1`.
|
|
68
|
+
// A style object is shared: StyleSheet.create hands out one frozen object per rule, and the CSS
|
|
69
|
+
// class registry resolves a class name to one cached object, so a thousand rows carry the same
|
|
70
|
+
// handful of style objects. Cache resolution on the style object's identity.
|
|
71
|
+
// Keyed by the style object alone: the per-component ViewConfig processors that once made one
|
|
72
|
+
// style object resolve differently under two view names moved to configPayloadFold, which runs on
|
|
73
|
+
// the top-level bag before this, so what's left here is component-independent.
|
|
74
|
+
// Assumes a style object is not mutated in place, which is already the engine's contract: setProp
|
|
75
|
+
// compares with Object.is and skips a same-identity write, so an in-place edit never marks the
|
|
76
|
+
// node dirty and never reaches Fabric either way.
|
|
77
|
+
// Keeping undefined-valued keys is deliberate: the resolved object is a faithful picture of one
|
|
78
|
+
// style entry, and addStyle below needs to see an explicit undefined to let a later entry clear an
|
|
79
|
+
// earlier one — dropping them would silently turn [{flex:1},{flex:undefined}] into `flex: 1`.
|
|
127
80
|
const styleCache = new WeakMap();
|
|
128
81
|
function processedStyle(style) {
|
|
129
82
|
const cached = styleCache.get(style);
|
|
@@ -132,43 +85,23 @@ function processedStyle(style) {
|
|
|
132
85
|
const resolved = {};
|
|
133
86
|
for (const key of Object.keys(style)) {
|
|
134
87
|
const value = style[key];
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// be shared with every other component using the same style object. A config's processors are
|
|
138
|
-
// keyed on top-level prop names and never reach inside a style anyway.
|
|
88
|
+
// undefined for the already-processed set: this cache is keyed on the style object alone, so
|
|
89
|
+
// anything component-dependent read here would be shared with every user of that object.
|
|
139
90
|
resolved[key] =
|
|
140
91
|
value === undefined ? undefined : processValue(key, value, undefined);
|
|
141
92
|
}
|
|
142
93
|
styleCache.set(style, resolved);
|
|
143
94
|
return resolved;
|
|
144
95
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
* This is also the shape React Native itself uses. `ReactNativeAttributePayload.addNestedProperty`
|
|
155
|
-
* (.vendors/react/packages/react-native-renderer/src/ReactNativeAttributePayload.js:208) recurses
|
|
156
|
-
* over the style array writing into a single `updatePayload`; upstream's `flattenStyle` appears
|
|
157
|
-
* ONLY in `diffNestedProperty`, i.e. the update path where an array meets an object - never on
|
|
158
|
-
* create. Our previous version flattened first and hoisted second, which allocated one merged
|
|
159
|
-
* object per node per commit that nothing else ever read.
|
|
160
|
-
*
|
|
161
|
-
* It also fixed a dead cache. `processedStyle` used to be reachable only when `props.style` was a
|
|
162
|
-
* bare object, and it never is: `commitClassStyle` (node.ts) always writes the two-element
|
|
163
|
-
* `[classStyle, explicitStyle]` array, by design. So the memo existed, was correct, and never ran.
|
|
164
|
-
*
|
|
165
|
-
* Later entries win, because a later write overwrites the same key on `out`. An explicit
|
|
166
|
-
* `undefined` CLEARS the key instead, matching the flatten path it replaces. One narrow
|
|
167
|
-
* divergence, recorded rather than hidden: the `delete` also clears a same-named TOP-LEVEL prop
|
|
168
|
-
* hoisted before the style pass, which flattening did not. Style keys and native prop keys do not
|
|
169
|
-
* overlap in practice (one is Yoga/visual, the other is testID/accessibility/source), so this is
|
|
170
|
-
* theoretical - but it is a difference, and `fabric-props.test.ts` pins both halves.
|
|
171
|
-
*/
|
|
96
|
+
// Hoist one style slot's keys into the payload being built, recursing on position only — the same
|
|
97
|
+
// rule flattenStyle follows, and for the same reason: `transform: [{translateX: 5}]` is an
|
|
98
|
+
// array-valued prop, not a nested style.
|
|
99
|
+
// No intermediate object: every style entry's resolution is memoized on its own identity and its
|
|
100
|
+
// keys are written straight into `out`, so a thousand rows sharing one class-resolved style
|
|
101
|
+
// resolve it once and each node only pays a copy loop.
|
|
102
|
+
// Later entries win, because a later write overwrites the same key on `out`. An explicit
|
|
103
|
+
// undefined clears the key instead — the one narrow divergence from flattenStyle is that this also
|
|
104
|
+
// clears a same-named top-level prop hoisted before the style pass, pinned by fabric-props.test.ts.
|
|
172
105
|
function addStyle(out, style) {
|
|
173
106
|
if (Array.isArray(style)) {
|
|
174
107
|
for (const entry of style)
|
|
@@ -186,93 +119,55 @@ function addStyle(out, style) {
|
|
|
186
119
|
out[key] = value;
|
|
187
120
|
}
|
|
188
121
|
}
|
|
189
|
-
// Translate the retained node's logical props into the flat payload Fabric's C++
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// found 2026-08-31 by an agent reading what the render function actually emitted rather than
|
|
197
|
-
// trusting a header comment.
|
|
198
|
-
//
|
|
199
|
-
// So the fold lives in the layer every path goes through, exactly like the aria fold above it. This
|
|
200
|
-
// is the third instance of one rule: a tag inherits NOTHING a wrapper did, and the repair belongs
|
|
201
|
-
// below the fork.
|
|
202
|
-
//
|
|
203
|
-
// GATED ON THE COMPONENT, NOT ON THE PROP. `value` is also a prop of `Switch` and `Slider`; a fold
|
|
204
|
-
// keyed on the prop name would write a bogus `text` onto both. Two string comparisons rather than a
|
|
122
|
+
// Translate the retained node's logical props into the flat payload Fabric's C++ props expect:
|
|
123
|
+
// style keys are hoisted to the top level, event handlers and undefined values are dropped.
|
|
124
|
+
// RN has no `value` Fabric prop: a TextInput's controlled value rides as the private `text` prop,
|
|
125
|
+
// via a `value ?? defaultValue` fold a wrapper used to run. A tag has no wrapper, so this fold
|
|
126
|
+
// lives here instead — the same "a tag inherits nothing a wrapper did" rule as the aria fold above.
|
|
127
|
+
// Gated on the component, not the prop: `value` is also a prop of Switch and Slider, and a fold
|
|
128
|
+
// keyed on the prop name would write a bogus `text` onto both.
|
|
205
129
|
export function fabricProps(node, nodeProps) {
|
|
206
130
|
if (node.component === RAW_TEXT_COMPONENT) {
|
|
207
|
-
// A raw-text node gets its behavior's fold too — it
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
// Which is why the skip is NOT the thing to change: it runs for every raw-text node in every
|
|
215
|
-
// app, and consulting a fold there would put one on that walk. Get the value into `props.text`
|
|
216
|
-
// instead — Button routes the owner's `title` onto this node with `slotProps: {title: 'text'}`,
|
|
217
|
-
// so the skip and the fold read the same source and an empty title still commits nothing.
|
|
131
|
+
// A raw-text node gets its behavior's fold too — it transforms text already there (Button
|
|
132
|
+
// uppercases its label) but may not supply one: isEmptyRawText decides whether the node
|
|
133
|
+
// commits at all from the node's own text prop, before any fold runs.
|
|
134
|
+
// So the skip is not the thing to change — it runs for every raw-text node in every app. Get
|
|
135
|
+
// the value into props.text instead: Button routes its owner's title here via slotProps, so
|
|
136
|
+
// the skip and the fold read the same source.
|
|
218
137
|
return {
|
|
219
138
|
text: node.payloadFold !== undefined
|
|
220
139
|
? node.payloadFold(nodeProps).text
|
|
221
140
|
: nodeProps.text,
|
|
222
141
|
};
|
|
223
142
|
}
|
|
224
|
-
// This runs once per node per commit
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
// strictly cheaper shape - Object.keys allocates one array per call, 10 007 of them on a
|
|
231
|
-
// 1 000-row create (counted), and for...in allocates none - and the headless V8 bench agreed,
|
|
232
|
-
// 12-13% off create/replace `min`. On DEVICE it lost: with the Fabric call counts and prop-key
|
|
233
|
-
// payload byte-identical either way (9000/5000/1009, 32001 keys), Release Create went 217.8 ->
|
|
234
|
-
// 243.2 ms while stock moved only inside its 4% noise floor. Hermes' for-in is not V8's enum
|
|
235
|
-
// cache. The general rule this bought: an allocation-count win measured on V8 is not a Hermes
|
|
236
|
-
// win, and only the on-device number decides (`perf-claims-need-numbers`).
|
|
143
|
+
// This runs once per node per commit, so the two loops below iterate with Object.keys rather
|
|
144
|
+
// than Object.entries: entries allocates a fresh two-element array per key on top of the outer
|
|
145
|
+
// array, measured as a real share of the create path's garbage.
|
|
146
|
+
// Do not "improve" this to for...in: tried and reverted. It wins on allocation count and even on
|
|
147
|
+
// headless V8 benchmarks, but loses on device — Hermes' for-in is not V8's enum cache, and only
|
|
148
|
+
// the on-device number decides.
|
|
237
149
|
const out = {};
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// it
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// (the `processedStyle` pattern below) would be stale forever. The gate is the node's sticky flag
|
|
245
|
-
// instead: one boolean read for a node with no alias, which is nearly all of them, and the fold's
|
|
246
|
-
// own fast path returns by identity for the rest.
|
|
247
|
-
// THE ARIA FOLD IS NOT CALLED HERE ANY MORE (2026-09-18), and it is the last platform rule to
|
|
248
|
-
// leave this builder. `foldAriaProps` itself STAYS in JS and is not a mirror: `pickAccessibilityProps`
|
|
249
|
-
// (`@symbiote-native/components`) folds a bag and then picks fields BY NAME, which it cannot do
|
|
250
|
-
// from a bag holding only `aria-label`. So the function has a real, load-bearing caller — what was
|
|
251
|
-
// wrong was this CALL, which put a rule the device runs in C++ back into the headless payload and
|
|
252
|
-
// invited 27 cases to assert it where the device copy is invisible.
|
|
253
|
-
//
|
|
254
|
-
// Where the claims live now: `core/engine/cpp/tests/js/aria-payload.itest.ts`, off a real payload.
|
|
150
|
+
// The one point where the whole bag is known on every path, which the aria fold needs:
|
|
151
|
+
// aria-checked must fold against a sibling accessibilityState, and routeProp sees one key at a
|
|
152
|
+
// time. Both commit paths reach here, so a tag gets the fold it has no wrapper to run.
|
|
153
|
+
// Not memoised on the bag's identity — the host mutates it in place, so an identity-keyed cache
|
|
154
|
+
// would be stale forever. The gate is the node's sticky flag instead, one boolean read for a
|
|
155
|
+
// node with no alias (nearly all of them); the fold's own fast path handles the rest.
|
|
255
156
|
const aliasFolded = nodeProps;
|
|
256
|
-
// The behavior's own fold, keyed on the
|
|
257
|
-
// component name, which several tags share (
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// Briefly: every fold returns `{ ...props, ...whatItChanged }`, and on device reading that back is
|
|
263
|
-
// `jsi::dynamicFromValue`, 13.3 ms of a 17.8 ms fold phase against 1.6 ms to send the bag out and
|
|
264
|
-
// 1.6 ms to run the fold.
|
|
157
|
+
// The behavior's own fold, keyed on the tag — the two folds above are keyed on the resolved
|
|
158
|
+
// component name, which several tags share (pressable and a plain view are both RCTView), so
|
|
159
|
+
// neither could carry a per-primitive fold. See IPayloadFold.
|
|
160
|
+
// The fold's return REPLACES the bag, which costs more than it looks (payload-fold-merge.test.ts
|
|
161
|
+
// has the measurement): every fold returns `{ ...props, ...whatItChanged }`, and reading that
|
|
162
|
+
// back on device dominates the fold phase.
|
|
265
163
|
const behaviorFolded = node.payloadFold !== undefined
|
|
266
164
|
? node.payloadFold(aliasFolded)
|
|
267
165
|
: aliasFolded;
|
|
268
|
-
// RN'
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// So a text node's payload here is missing two keys the device's carries. That is a PROPERTY of
|
|
275
|
-
// this harness rather than a gap in it — do not close it by adding the rule back.
|
|
166
|
+
// RN's two text defaults are NOT applied here — the rule lives in SymbioteFabricProps.cpp alone,
|
|
167
|
+
// pinned by committed-payload.itest.ts off a real payload. The engine is the only layer that can
|
|
168
|
+
// see the authored bag for every adapter at once, so it's the only one that needs the rule.
|
|
169
|
+
// So a text node's payload here is missing two keys the device's carries. That is a property of
|
|
170
|
+
// this harness, not a gap in it — do not close it by adding the rule back.
|
|
276
171
|
const props = behaviorFolded;
|
|
277
172
|
// Hoisted out of the loop: one cached lookup per node per commit, not one per key.
|
|
278
173
|
const alreadyProcessed = configProcessedKeys(node.component);
|
package/build/fabric.d.ts
CHANGED
|
@@ -34,23 +34,10 @@ interface IFabricHost extends Omit<IFabricSlot, 'cloneNodeWithNewChildren' | 'cl
|
|
|
34
34
|
cloneNodeWithNewChildrenAndProps(node: IFabricNode, children: readonly IFabricNode[], newProps: IFabricProps): IFabricNode;
|
|
35
35
|
findShadowNodeByTag_DEPRECATED?(tag: number): IFabricNode | null;
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
38
|
-
* An accessibility event addressed by a bare native TAG, the way RN's bridgeless
|
|
39
|
-
* `UIManager.sendAccessibilityEvent` does it: resolve the tag to its committed shadow node, then send.
|
|
40
|
-
* An unknown tag is dropped, as RN drops it (with a log rather than a throw).
|
|
41
|
-
*/
|
|
42
37
|
export declare function sendAccessibilityEventByTag(tag: number, eventType: string): void;
|
|
43
38
|
declare global {
|
|
44
39
|
var nativeFabricUIManager: IFabricHost | undefined;
|
|
45
40
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Test seam: forget the bound slot, so a fixture can install a different host and be believed.
|
|
48
|
-
*
|
|
49
|
-
* `getSlot` caches the facade for the life of the module — the live binding re-mints a host function
|
|
50
|
-
* on every property read, so caching is not an optimisation but the difference between reading each
|
|
51
|
-
* method once and reading it per call. The cache has no invalidation in production because the
|
|
52
|
-
* global is installed once, before anything commits.
|
|
53
|
-
*/
|
|
54
41
|
export declare function resetSlot(): void;
|
|
55
42
|
export declare function getSlot(): IFabricSlot;
|
|
56
43
|
export {};
|
package/build/fabric.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
// The one seam symbiote drives.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// The live object is a lazy caching proxy: every property access mints a fresh
|
|
6
|
-
// host function, so we read each method once and cache a plain facade.
|
|
1
|
+
// The one seam symbiote drives. global.nativeFabricUIManager is the framework-agnostic, JSI-bound
|
|
2
|
+
// mutation API Fabric exposes; React's renderer is just one client of it. We bind to it directly.
|
|
3
|
+
// The live object is a lazy caching proxy, so we read each method once and cache a plain facade.
|
|
7
4
|
import { dlog } from './debug.js';
|
|
8
5
|
import { installNativeTreeHost } from './native-tree-host.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* An unknown tag is dropped, as RN drops it (with a log rather than a throw).
|
|
13
|
-
*/
|
|
6
|
+
// An accessibility event addressed by a bare native tag, the way RN's bridgeless
|
|
7
|
+
// UIManager.sendAccessibilityEvent does it: resolve the tag to its committed shadow node, then
|
|
8
|
+
// send. An unknown tag is dropped, as RN drops it (with a log rather than a throw).
|
|
14
9
|
export function sendAccessibilityEventByTag(tag, eventType) {
|
|
15
10
|
const host = globalThis.nativeFabricUIManager;
|
|
16
11
|
const node = host?.findShadowNodeByTag_DEPRECATED?.(tag);
|
|
@@ -21,22 +16,12 @@ export function sendAccessibilityEventByTag(tag, eventType) {
|
|
|
21
16
|
host.sendAccessibilityEvent(node, eventType);
|
|
22
17
|
}
|
|
23
18
|
let cached;
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// keeps the measurement that made it uninteresting even on the old path — Create 256.8 on / 258.5
|
|
31
|
-
// off, i.e. it demonstrably worked and bought nothing.
|
|
32
|
-
/**
|
|
33
|
-
* Test seam: forget the bound slot, so a fixture can install a different host and be believed.
|
|
34
|
-
*
|
|
35
|
-
* `getSlot` caches the facade for the life of the module — the live binding re-mints a host function
|
|
36
|
-
* on every property read, so caching is not an optimisation but the difference between reading each
|
|
37
|
-
* method once and reading it per call. The cache has no invalidation in production because the
|
|
38
|
-
* global is installed once, before anything commits.
|
|
39
|
-
*/
|
|
19
|
+
// Batching is gone: this slot's calls used to be recorded and replayed once per commit, to remove
|
|
20
|
+
// per-call JSI crossings from a JS walk that worked out the Fabric operations. That walk no longer
|
|
21
|
+
// exists — adapters record their own mutations — so there is nothing left to batch.
|
|
22
|
+
// Test seam: forget the bound slot, so a fixture can install a different host and be believed.
|
|
23
|
+
// getSlot caches the facade for the life of the module — the live binding re-mints a host function
|
|
24
|
+
// on every property read, so caching means reading each method once instead of per call.
|
|
40
25
|
export function resetSlot() {
|
|
41
26
|
cached = undefined;
|
|
42
27
|
}
|
|
@@ -106,17 +91,12 @@ export function getSlot() {
|
|
|
106
91
|
measureLayout: (node, relativeToNode, onFail, onSuccess) => measureLayout(node, relativeToNode, onFail, onSuccess),
|
|
107
92
|
};
|
|
108
93
|
dlog('slot bound to nativeFabricUIManager');
|
|
109
|
-
// Resolve our own native module here, once, for its
|
|
110
|
-
//
|
|
111
|
-
// never created
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// This is the right seam rather than a convenient one, and now for two reasons: binding the Fabric
|
|
116
|
-
// slot is the moment the engine has established it is on a native host at all, AND it is the last
|
|
117
|
-
// moment before a commit can happen — the tree host has to be in before `commitSurfaceOps` runs or
|
|
118
|
-
// the ops it names stay pending. It cannot throw: with no module `installNativeTreeHost()` is a
|
|
119
|
-
// no-op, which is most places (see `native-engine.ts`'s header).
|
|
94
|
+
// Resolve our own native module here, once, for its side effect: RCTTurboModuleManager runs
|
|
95
|
+
// installJSIBindingsWithRuntime: when it creates a module, so without this call the module is
|
|
96
|
+
// never created and the global bindings stay absent even on a working binary.
|
|
97
|
+
// The right seam, not just a convenient one: binding the Fabric slot is the moment the engine has
|
|
98
|
+
// established it's on a native host, and the last moment before commitSurfaceOps can run. Cannot
|
|
99
|
+
// throw: with no module, installNativeTreeHost() is a no-op (see native-engine.ts's header).
|
|
120
100
|
installNativeTreeHost();
|
|
121
101
|
return cached;
|
|
122
102
|
}
|
package/build/host-access.d.ts
CHANGED
|
@@ -1,145 +1,18 @@
|
|
|
1
1
|
import { type ISymbioteNode } from './node';
|
|
2
2
|
import type { SymbioteSurface } from './surface';
|
|
3
|
-
/**
|
|
4
|
-
* The node's parent, or `undefined` for a node that sits directly under a surface.
|
|
5
|
-
*
|
|
6
|
-
* A top-level node answers `undefined` even though a surface IS a node in the host's tree, and the
|
|
7
|
-
* three adapters depend on that exact miss: Angular reads `null` as "defer, `<ng-content>` will place
|
|
8
|
-
* this", while Vue and Solid spell `?? surface` at their call sites and compare the result against
|
|
9
|
-
* the `SymbioteSurface` object, which the surface's anchor node is not. `SURFACE_COMPONENT` is the
|
|
10
|
-
* sentinel that stops the answer there.
|
|
11
|
-
*
|
|
12
|
-
* So `undefined` is not the same question as "is this node attached".
|
|
13
|
-
*
|
|
14
|
-
* IT DOES NOT ALWAYS DRAIN, unlike its neighbours. A node's parent link changes only through an op
|
|
15
|
-
* that names it as the CHILD, so a node the pending batch has not placed already has its final
|
|
16
|
-
* answer standing in the host — see `hasPendingPlacement`. Angular's 1 000-row create asks this
|
|
17
|
-
* 1 000 times (its `addLViewToLContainer` calls `renderer.parentNode` once per embedded view) and
|
|
18
|
-
* exactly ONE of those reads was about a node the batch had touched.
|
|
19
|
-
*/
|
|
20
3
|
export declare function parentOf(node: ISymbioteNode): ISymbioteNode | undefined;
|
|
21
|
-
/**
|
|
22
|
-
* The node's children, INCLUDING anchors.
|
|
23
|
-
*
|
|
24
|
-
* Anchors are structural bookkeeping — the commit skips them — but they are not invisible to
|
|
25
|
-
* traversal, and hiding them here would desync a framework runtime from the tree it built:
|
|
26
|
-
* solid-js/universal keeps its own record of what it inserted and re-derives positions through these
|
|
27
|
-
* lookups, so a node it placed must be a node it can find.
|
|
28
|
-
*/
|
|
29
4
|
export declare function childrenOf(node: ISymbioteNode): readonly ISymbioteNode[];
|
|
30
|
-
/**
|
|
31
|
-
* Every node's parent, positionally, in ONE crossing — `parentOf` for a list.
|
|
32
|
-
*
|
|
33
|
-
* Engine-internal, like `subtreesOf` below, and for the same reason: what a framework seam needs is
|
|
34
|
-
* the singular form, one step at a time. These two answer the question only TEARDOWN asks, and
|
|
35
|
-
* teardown is the one lifecycle event whose size is the tree's (see `ITreeHost`).
|
|
36
|
-
*
|
|
37
|
-
* A `SURFACE_COMPONENT` parent reads as `undefined` here exactly as it does in `parentOf`, so the
|
|
38
|
-
* two agree element for element.
|
|
39
|
-
*/
|
|
40
5
|
export declare function parentsOf(nodes: readonly ISymbioteNode[]): readonly (ISymbioteNode | undefined)[];
|
|
41
6
|
/** Each root and every descendant, pre-order, anchors included — all of it in ONE crossing. */
|
|
42
7
|
export declare function subtreesOf(roots: readonly ISymbioteNode[]): readonly ISymbioteNode[];
|
|
43
|
-
/**
|
|
44
|
-
* The same walk narrowed to what a teardown must visit — see `ITreeHost.teardownSubtreesOf`.
|
|
45
|
-
*
|
|
46
|
-
* The gate is the ANIMATED one, not a behavior one: a binding is per node and carries no tag, so an
|
|
47
|
-
* app that animates needs every node back and gets the full walk. Nothing else narrows, because
|
|
48
|
-
* nothing else is charged per node of a removed subtree.
|
|
49
|
-
*/
|
|
50
8
|
export declare function teardownSubtreesOf(roots: readonly ISymbioteNode[]): readonly ISymbioteNode[];
|
|
51
|
-
/**
|
|
52
|
-
* The node and every ancestor above it, deepest first, in ONE crossing.
|
|
53
|
-
*
|
|
54
|
-
* `parentOf` per level is a crossing per level, and the event path needs this chain for every
|
|
55
|
-
* event — capture reads it reversed, bubble forward — plus again on every frame of a drag, for the
|
|
56
|
-
* responder's scope. Measured at 18 crossings per event on a depth-8 chain before the two phases
|
|
57
|
-
* shared a walk, 9 after, and 1 through here.
|
|
58
|
-
*
|
|
59
|
-
* Surfaces are dropped the same way `parentOf` drops them, by component, so a caller sees the same
|
|
60
|
-
* chain it would have built by walking.
|
|
61
|
-
*/
|
|
62
9
|
export declare function ancestorsOf(node: ISymbioteNode): readonly ISymbioteNode[];
|
|
63
|
-
/**
|
|
64
|
-
* The first child, anchors included, or `undefined` for a leaf.
|
|
65
|
-
*
|
|
66
|
-
* ONE HOST CALL, not `childrenOf(node)[0]`, and the difference is a complexity class rather than a
|
|
67
|
-
* constant. `solid-js/universal`'s `cleanChildren` empties a parent with
|
|
68
|
-
* `while (removed = getFirstChild(parent)) removeNode(parent, removed)` — so the old spelling read a
|
|
69
|
-
* list of N, then N-1, then N-2, building and discarding every handle each time. Measured on a
|
|
70
|
-
* 2 000-row Solid `Clear` (`solid-clear-scaling.itest.tsx`): **2 001 001 handles** crossed to remove
|
|
71
|
-
* two thousand children, N(N+1)/2 to the unit, against the ~2 000 the work needs.
|
|
72
|
-
*
|
|
73
|
-
* The `mayHaveChildren` fast path is kept for the same reason `childrenOf` has it: FALSE is a
|
|
74
|
-
* certainty, so a leaf answers without a drain and without a crossing.
|
|
75
|
-
*/
|
|
76
10
|
export declare function firstChildOf(node: ISymbioteNode): ISymbioteNode | undefined;
|
|
77
|
-
|
|
78
|
-
* The next sibling, or `undefined` at the end of the list.
|
|
79
|
-
*
|
|
80
|
-
* `surface` is required to answer for a TOP-LEVEL node, which has no parent to read the sibling
|
|
81
|
-
* list from — the surface owns that list instead. Passing it for a parented node is harmless and
|
|
82
|
-
* ignored, so a caller with one active surface can pass it unconditionally.
|
|
83
|
-
*/
|
|
84
|
-
export declare function nextSiblingOf(node: ISymbioteNode, surface?: SymbioteSurface): ISymbioteNode | undefined;
|
|
85
|
-
/**
|
|
86
|
-
* Whether the node is a TEXT CONTAINER (`<Text>`), not whether it holds a string.
|
|
87
|
-
*
|
|
88
|
-
* The distinction is load-bearing for adapters that ask "can I write a string into this": a raw
|
|
89
|
-
* text node answers FALSE here, and an anchor does too. Use `isRawTextNode` for that question.
|
|
90
|
-
*/
|
|
11
|
+
export declare function nextSiblingOf(node: ISymbioteNode, _surface?: SymbioteSurface): ISymbioteNode | undefined;
|
|
91
12
|
export declare function isTextContainer(node: ISymbioteNode): boolean;
|
|
92
|
-
/**
|
|
93
|
-
* Whether the node is a RAW TEXT node — one a string can be written into.
|
|
94
|
-
*
|
|
95
|
-
* This is the question `solid-js/universal`'s `insertExpression` actually asks before calling
|
|
96
|
-
* replaceText, and answering it with `isTextContainer` would be wrong in both directions: a
|
|
97
|
-
* `<Text>` is a container that holds no string of its own, and the empty-string ANCHOR that
|
|
98
|
-
* cleanChildren leaves to hold a position is not writable either. An anchor is excluded here by
|
|
99
|
-
* construction, since its component is the `#anchor` sentinel.
|
|
100
|
-
*/
|
|
101
13
|
export declare function isRawTextNode(node: ISymbioteNode): boolean;
|
|
102
|
-
/**
|
|
103
|
-
* The Fabric view name this node currently resolves to (`RCTView`, `RCTText`, `RCTRawText`, the
|
|
104
|
-
* `#anchor` sentinel …). Exposed because two seams branch on it — Solid to answer `isTextNode`,
|
|
105
|
-
* Angular to recognise its own anchor hosts — and both read `node.component` directly today.
|
|
106
|
-
*
|
|
107
|
-
* NOT stable across a node's life: a primitive whose native view depends on a prop (`TextInput`'s
|
|
108
|
-
* `multiline`) changes view without changing identity. Read it, never cache it.
|
|
109
|
-
*/
|
|
110
14
|
export declare function componentOf(node: ISymbioteNode): string;
|
|
111
|
-
/**
|
|
112
|
-
* The string a raw-text node currently holds, or `undefined` for any other node.
|
|
113
|
-
*
|
|
114
|
-
* Exists for the DIAGNOSTIC path rather than the render path: a seam that rejects a bare string
|
|
115
|
-
* outside a `<Text>` wants to name the offending text in its error, and reading `node.props.text`
|
|
116
|
-
* to do so is the last thing keeping that seam coupled to the node's shape. Vue's
|
|
117
|
-
* `setElementText` reads the same value for a real reason, so this is not a one-caller accessor.
|
|
118
|
-
*/
|
|
119
15
|
export declare function textOf(node: ISymbioteNode): string | undefined;
|
|
120
|
-
/**
|
|
121
|
-
* Every prop standing on the node, function props included.
|
|
122
|
-
*
|
|
123
|
-
* The bag a payload fold reads. Two sources, because a function never crossed the wire (`writeProp`,
|
|
124
|
-
* node.ts): the host holds the values it could take, JS holds the callbacks it could not, and a fold
|
|
125
|
-
* asking for `onPress` must see the one the app wrote rather than the `undefined` the host was
|
|
126
|
-
* handed in its place.
|
|
127
|
-
*
|
|
128
|
-
* A COPY when anything was stashed, the host's own object when nothing was — which is nearly every
|
|
129
|
-
* node. Same caveat as `propOf`: `style` after a class merge is the `[classStyle, explicitStyle]`
|
|
130
|
-
* array, not the author's object.
|
|
131
|
-
*/
|
|
132
16
|
export declare function propsOf(node: ISymbioteNode): Readonly<Record<string, unknown>>;
|
|
133
|
-
/**
|
|
134
|
-
* A TEST read: the PAYLOAD the last commit handed Fabric for this node, `undefined` before one.
|
|
135
|
-
*
|
|
136
|
-
* `propsOf` above is the props AS THE OPS NAMED THEM — what the adapter said. This is what the
|
|
137
|
-
* payload builder MADE of them, after the aria fold, the behavior's own fold, the component-keyed
|
|
138
|
-
* folds and the style hoist. The two answer different questions and a test has to pick: "did the
|
|
139
|
-
* adapter write `inputMode`" is `propsOf`, and "did that reach native as `keyboardType`" is this.
|
|
140
|
-
*
|
|
141
|
-
* Only the native host can answer it — see `ITreeHost.committedPayloadOf`. Under the recording host
|
|
142
|
-
* it throws, on purpose, naming the itest suite as where the question belongs.
|
|
143
|
-
*/
|
|
144
17
|
export declare function committedPayloadOf(node: ISymbioteNode): Readonly<Record<string, unknown>> | undefined;
|
|
145
18
|
export declare function propOf(node: ISymbioteNode, key: string): unknown;
|