@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.
- package/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -8
- package/build/accessibility-props.js +13 -16
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/host-binding.d.ts +1 -1
- package/build/animated/host-binding.js +19 -4
- package/build/animated/index.d.ts +1 -1
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +88 -40
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +116 -184
- package/build/fabric.d.ts +9 -0
- package/build/fabric.js +32 -0
- package/build/host-access.d.ts +125 -0
- package/build/host-access.js +280 -0
- package/build/host-behavior.d.ts +84 -21
- package/build/host-behavior.js +196 -30
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +14 -7
- package/build/index.js +53 -10
- package/build/mutation-buffer.d.ts +222 -0
- package/build/mutation-buffer.js +491 -0
- package/build/native-engine.d.ts +182 -0
- package/build/native-engine.js +178 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +66 -0
- package/build/node.d.ts +172 -57
- package/build/node.js +839 -383
- package/build/pan-responder/index.js +27 -52
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -56
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +307 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +232 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2478 -0
- package/cpp/SymbioteTree.h +257 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1058
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
package/build/fabric-props.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
|
-
// Fabric-prop translation: turn a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
1
|
+
// Fabric-prop translation: turn a node's logical props into the flat payload Fabric's C++ props
|
|
2
|
+
// expect. Color processing itself lives in ./platform-color (the stable leaf every color-touching
|
|
3
|
+
// module imports from); this file only decides WHICH props are color props and wires the structured
|
|
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`.
|
|
8
31
|
import { RAW_TEXT_COMPONENT } from './node.js';
|
|
9
|
-
import { registeredProcessor } from './registry.js';
|
|
10
32
|
import { isProcessableColor, processColor } from './platform-color/index.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { processTransformOrigin } from './process-transform-origin/index.js';
|
|
14
|
-
import { processTransform } from './process-transform/index.js';
|
|
15
|
-
import { processAspectRatio } from './process-aspect-ratio.js';
|
|
16
|
-
import { processFontVariant } from './process-font-variant.js';
|
|
17
|
-
import { processBackgroundImage } from './process-background-image/index.js';
|
|
18
|
-
import { isRecord, isString } from './type-guards.js';
|
|
33
|
+
import { configProcessedKeys } from './registry.js';
|
|
34
|
+
import { isRecord } from './type-guards.js';
|
|
19
35
|
// Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
|
|
20
36
|
// color parser silently drops strings. The actual conversion (processColor) is
|
|
21
37
|
// RN-platform-specific, so it is injected in platform-color.ts rather than imported,
|
|
@@ -63,103 +79,26 @@ const COLOR_PROPS = new Set([
|
|
|
63
79
|
'trackColorForFalse',
|
|
64
80
|
'trackTintColor',
|
|
65
81
|
]);
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// boxShadow accepts a CSS string or an array of shadow objects; anything else is
|
|
86
|
-
// undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
|
|
87
|
-
function asBoxShadowInput(value) {
|
|
88
|
-
if (typeof value === 'string')
|
|
89
|
-
return value;
|
|
90
|
-
if (Array.isArray(value))
|
|
91
|
-
return value.filter(isRecord);
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
// filter accepts a CSS string or an array of single-key filter objects; same narrowing.
|
|
95
|
-
function asFilterInput(value) {
|
|
96
|
-
if (typeof value === 'string')
|
|
97
|
-
return value;
|
|
98
|
-
if (Array.isArray(value))
|
|
99
|
-
return value.filter(isRecord);
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
// experimental_backgroundImage accepts a CSS string (gradient functions) or an array of
|
|
103
|
-
// structured gradient objects; same narrowing as boxShadow/filter.
|
|
104
|
-
function asBackgroundImageInput(value) {
|
|
105
|
-
if (typeof value === 'string')
|
|
106
|
-
return value;
|
|
107
|
-
if (Array.isArray(value))
|
|
108
|
-
return value.filter(isRecord);
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
// transformOrigin accepts a CSS string or a [x, y, z] array of strings/numbers; anything
|
|
112
|
-
// else is undefined to processTransformOrigin (which defaults to center/center/0).
|
|
113
|
-
function asTransformOriginInput(value) {
|
|
114
|
-
if (typeof value === 'string')
|
|
115
|
-
return value;
|
|
116
|
-
if (Array.isArray(value))
|
|
117
|
-
return value.filter(isStringOrNumber);
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
// aspectRatio accepts a number (the common, working form) or a ratio string; otherwise
|
|
121
|
-
// undefined, which processAspectRatio drops.
|
|
122
|
-
function asAspectRatioInput(value) {
|
|
123
|
-
if (typeof value === 'number' || typeof value === 'string')
|
|
124
|
-
return value;
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
// fontVariant accepts an array of variant strings (the common, working form) or a
|
|
128
|
-
// space-separated string; anything else becomes an empty string, which yields [].
|
|
129
|
-
function asFontVariantInput(value) {
|
|
130
|
-
if (typeof value === 'string')
|
|
82
|
+
// Convert a prop to the shape Fabric's C++ expects: a CSS-string color runs through the injected
|
|
83
|
+
// platform processor, because Fabric's C++ color parser silently drops strings.
|
|
84
|
+
//
|
|
85
|
+
// TWO FAMILIES OF PROCESSOR USED TO BE HERE AND BOTH MOVED, for one reason: on a device the payload
|
|
86
|
+
// is built in C++ and this function does not run, so anything resolved only here is resolved only
|
|
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
|
+
//
|
|
93
|
+
// 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 DIFFERENT color.
|
|
95
|
+
function processValue(key, value, alreadyProcessed) {
|
|
96
|
+
// A key the component's own config already converted is DONE. Running the engine's colour pass
|
|
97
|
+
// over it as well is not a no-op: processColor rotates, so a second rotation is a different
|
|
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.
|
|
100
|
+
if (alreadyProcessed?.has(key) === true)
|
|
131
101
|
return value;
|
|
132
|
-
if (Array.isArray(value))
|
|
133
|
-
return value.filter(isString);
|
|
134
|
-
return '';
|
|
135
|
-
}
|
|
136
|
-
// transform accepts a CSS string (processTransform parses it) or an array of single-key
|
|
137
|
-
// transform records (the hot animated / sticky-header path, passed through unchanged).
|
|
138
|
-
// A non-string non-array value is NOT dropped: it may already be processed, so it passes
|
|
139
|
-
// through verbatim rather than being coerced to [] (which would erase a valid transform).
|
|
140
|
-
function processTransformValue(value) {
|
|
141
|
-
if (typeof value === 'string')
|
|
142
|
-
return processTransform(value);
|
|
143
|
-
if (Array.isArray(value))
|
|
144
|
-
return processTransform(value.filter(isRecord));
|
|
145
|
-
return value;
|
|
146
|
-
}
|
|
147
|
-
function isStringOrNumber(value) {
|
|
148
|
-
return typeof value === 'string' || typeof value === 'number';
|
|
149
|
-
}
|
|
150
|
-
// Convert a prop to the shape Fabric's C++ expects. A third-party view contributes
|
|
151
|
-
// its own processors, auto-derived from its ViewConfig (validAttributes[*].process,
|
|
152
|
-
// e.g. processColor for a slider's track tints); those run first. Then the structured
|
|
153
|
-
// CSS-style processors (boxShadow/filter). Built-ins are never in the registry, so they
|
|
154
|
-
// fall through to the global color path, where any CSS-string color is run through the
|
|
155
|
-
// injected platform processor (Fabric's C++ color parser silently drops strings).
|
|
156
|
-
function processValue(component, key, value) {
|
|
157
|
-
const processor = registeredProcessor(component, key);
|
|
158
|
-
if (processor !== undefined)
|
|
159
|
-
return processor(value);
|
|
160
|
-
const styleProcessor = STYLE_PROCESSORS.get(key);
|
|
161
|
-
if (styleProcessor !== undefined)
|
|
162
|
-
return styleProcessor(value);
|
|
163
102
|
if (COLOR_PROPS.has(key) && isProcessableColor(value))
|
|
164
103
|
return processColor(value);
|
|
165
104
|
return value;
|
|
@@ -170,8 +109,10 @@ function processValue(component, key, value) {
|
|
|
170
109
|
// of style objects, and resolving each one per node costs O(nodes x styleKeys) to compute an
|
|
171
110
|
// answer that only varies with O(distinct styles). Cache it on the style object's identity.
|
|
172
111
|
//
|
|
173
|
-
// Keyed by
|
|
174
|
-
//
|
|
112
|
+
// Keyed by the style object ALONE. It used to carry a per-component dimension because processValue
|
|
113
|
+
// consulted that component's ViewConfig processors and one style object could resolve differently
|
|
114
|
+
// under two view names; those processors moved to `configPayloadFold`, which runs on the top-level
|
|
115
|
+
// bag before this, so what is left here is component-independent.
|
|
175
116
|
//
|
|
176
117
|
// The cache assumes a style object is not MUTATED IN PLACE, which is already the engine's contract:
|
|
177
118
|
// setProp compares with Object.is and skips a same-identity write, so an in-place style edit never
|
|
@@ -183,23 +124,22 @@ function processValue(component, key, value) {
|
|
|
183
124
|
// style entry, and addStyle below needs to see an explicit `undefined` to let a later entry clear
|
|
184
125
|
// an earlier one. Dropping them here would silently turn `[{flex:1},{flex:undefined}]` into
|
|
185
126
|
// `flex: 1`.
|
|
186
|
-
const styleCache = new
|
|
187
|
-
function processedStyle(
|
|
188
|
-
|
|
189
|
-
if (perComponent === undefined) {
|
|
190
|
-
perComponent = new WeakMap();
|
|
191
|
-
styleCache.set(component, perComponent);
|
|
192
|
-
}
|
|
193
|
-
const cached = perComponent.get(style);
|
|
127
|
+
const styleCache = new WeakMap();
|
|
128
|
+
function processedStyle(style) {
|
|
129
|
+
const cached = styleCache.get(style);
|
|
194
130
|
if (cached !== undefined)
|
|
195
131
|
return cached;
|
|
196
132
|
const resolved = {};
|
|
197
133
|
for (const key of Object.keys(style)) {
|
|
198
134
|
const value = style[key];
|
|
135
|
+
// `undefined` for the already-processed set, and it must stay that way: this cache is keyed on
|
|
136
|
+
// the style object ALONE (see the note above), so anything component-dependent read here would
|
|
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.
|
|
199
139
|
resolved[key] =
|
|
200
|
-
value === undefined ? undefined : processValue(
|
|
140
|
+
value === undefined ? undefined : processValue(key, value, undefined);
|
|
201
141
|
}
|
|
202
|
-
|
|
142
|
+
styleCache.set(style, resolved);
|
|
203
143
|
return resolved;
|
|
204
144
|
}
|
|
205
145
|
/**
|
|
@@ -229,15 +169,15 @@ function processedStyle(component, style) {
|
|
|
229
169
|
* overlap in practice (one is Yoga/visual, the other is testID/accessibility/source), so this is
|
|
230
170
|
* theoretical - but it is a difference, and `fabric-props.test.ts` pins both halves.
|
|
231
171
|
*/
|
|
232
|
-
function addStyle(out,
|
|
172
|
+
function addStyle(out, style) {
|
|
233
173
|
if (Array.isArray(style)) {
|
|
234
174
|
for (const entry of style)
|
|
235
|
-
addStyle(out,
|
|
175
|
+
addStyle(out, entry);
|
|
236
176
|
return;
|
|
237
177
|
}
|
|
238
178
|
if (!isRecord(style))
|
|
239
179
|
return;
|
|
240
|
-
const resolved = processedStyle(
|
|
180
|
+
const resolved = processedStyle(style);
|
|
241
181
|
for (const key of Object.keys(resolved)) {
|
|
242
182
|
const value = resolved[key];
|
|
243
183
|
if (value === undefined)
|
|
@@ -250,51 +190,24 @@ function addStyle(out, component, style) {
|
|
|
250
190
|
// props expect: `style` keys are hoisted to the top level, event handlers and
|
|
251
191
|
// undefined values are dropped.
|
|
252
192
|
// RN HAS NO `value` FABRIC PROP. A TextInput's controlled value rides as the private `text` prop,
|
|
253
|
-
// and the fold that produces it — `value ?? defaultValue` —
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// reading what the render function actually emits rather than trusting a header comment.
|
|
193
|
+
// and the fold that produces it — `value ?? defaultValue` — used to live in the component wrapper.
|
|
194
|
+
// A tag has no wrapper, so an author's `value={x}` would reach Fabric as a key no ViewConfig
|
|
195
|
+
// declares: silently dropped, `text` never set, and the field renders EMPTY. Nothing red anywhere —
|
|
196
|
+
// found 2026-08-31 by an agent reading what the render function actually emitted rather than
|
|
197
|
+
// trusting a header comment.
|
|
259
198
|
//
|
|
260
|
-
// So the fold
|
|
261
|
-
// is the third instance of one rule: a
|
|
262
|
-
//
|
|
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.
|
|
263
202
|
//
|
|
264
203
|
// GATED ON THE COMPONENT, NOT ON THE PROP. `value` is also a prop of `Switch` and `Slider`; a fold
|
|
265
204
|
// keyed on the prop name would write a bogus `text` onto both. Two string comparisons rather than a
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
// The engine may hold this because both views are in `BUILTIN_COMPONENTS`, whose hand-tuned tables
|
|
269
|
-
// (`view-config.ts`'s TEXT_INPUT_EVENTS, commit's COLOR_PROPS) already live here for the same
|
|
270
|
-
// reason. Routing it through `registerComponent` was tried first and is WRONG: `resolve()`
|
|
271
|
-
// short-circuits every builtin to EMPTY, so the registration would have been accepted and never
|
|
272
|
-
// applied.
|
|
273
|
-
const SINGLELINE_TEXT_INPUT = 'RCTSinglelineTextInputView';
|
|
274
|
-
const MULTILINE_TEXT_INPUT = 'RCTMultilineTextInputView';
|
|
275
|
-
function foldTextInputValue(props) {
|
|
276
|
-
const hasValue = props.value !== undefined;
|
|
277
|
-
const hasDefault = props.defaultValue !== undefined;
|
|
278
|
-
if (!hasValue && !hasDefault)
|
|
279
|
-
return props;
|
|
280
|
-
const folded = { ...props };
|
|
281
|
-
// `value` WINS over `defaultValue` — `foldText`'s rule, kept identical rather than re-derived.
|
|
282
|
-
// An explicit `text` is left alone: that is the component path, where the wrapper already folded,
|
|
283
|
-
// and re-folding there would let a stale `value` overwrite what the wrapper computed.
|
|
284
|
-
if (folded.text === undefined) {
|
|
285
|
-
folded.text = hasValue ? props.value : props.defaultValue;
|
|
286
|
-
}
|
|
287
|
-
// Blanked, not deleted: `fabricProps` skips undefined, and neither name is a real Fabric prop.
|
|
288
|
-
folded.value = undefined;
|
|
289
|
-
folded.defaultValue = undefined;
|
|
290
|
-
return folded;
|
|
291
|
-
}
|
|
292
|
-
export function fabricProps(node) {
|
|
205
|
+
export function fabricProps(node, nodeProps) {
|
|
293
206
|
if (node.component === RAW_TEXT_COMPONENT) {
|
|
294
207
|
// A raw-text node gets its behavior's fold too — it TRANSFORMS the text that is already there
|
|
295
208
|
// (Button uppercases its label on Android) and may not SUPPLY one, which is narrower than this
|
|
296
209
|
// comment claimed when it landed. `isEmptyRawText` (node.ts) decides whether the node commits
|
|
297
|
-
// at all from `
|
|
210
|
+
// at all from the node's own `text` prop, before any fold runs, so a text that exists only as a fold
|
|
298
211
|
// result is dropped by `renderableChildren` and the fold never executes. Reported by the hook's
|
|
299
212
|
// first consumer, within the hour.
|
|
300
213
|
//
|
|
@@ -304,8 +217,8 @@ export function fabricProps(node) {
|
|
|
304
217
|
// so the skip and the fold read the same source and an empty title still commits nothing.
|
|
305
218
|
return {
|
|
306
219
|
text: node.payloadFold !== undefined
|
|
307
|
-
? node.payloadFold(
|
|
308
|
-
:
|
|
220
|
+
? node.payloadFold(nodeProps).text
|
|
221
|
+
: nodeProps.text,
|
|
309
222
|
};
|
|
310
223
|
}
|
|
311
224
|
// This runs once per node per commit - 9 000 times on one benchmark press - so the two loops
|
|
@@ -324,26 +237,45 @@ export function fabricProps(node) {
|
|
|
324
237
|
const out = {};
|
|
325
238
|
// THE ONE POINT WHERE THE WHOLE BAG IS KNOWN ON EVERY PATH, which is what the aria fold needs:
|
|
326
239
|
// `aria-checked` has to be folded against a sibling `accessibilityState`, and `routeProp` sees
|
|
327
|
-
// one key at a time. Both commit paths — create and update — reach here, so a
|
|
328
|
-
//
|
|
240
|
+
// one key at a time. Both commit paths — create and update — reach here, so a tag gets the fold
|
|
241
|
+
// it has no wrapper to run.
|
|
329
242
|
//
|
|
330
|
-
// NOT memoised on
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
243
|
+
// NOT memoised on the bag's identity. The host mutates it IN PLACE, so an identity-keyed cache
|
|
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.
|
|
255
|
+
const aliasFolded = nodeProps;
|
|
256
|
+
// The behavior's own fold, keyed on the TAG — the two folds above are keyed on the resolved
|
|
257
|
+
// component name, which several tags share (`pressable` and a plain `view` are both `RCTView`),
|
|
258
|
+
// so neither could carry a per-primitive fold. See IPayloadFold.
|
|
259
|
+
//
|
|
260
|
+
// THE FOLD'S RETURN REPLACES THE BAG, and it costs more than it looks — see
|
|
261
|
+
// `payload-fold-merge.test.ts` for the measurement and for why the obvious fix does not fit yet.
|
|
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.
|
|
340
265
|
const behaviorFolded = node.payloadFold !== undefined
|
|
341
266
|
? node.payloadFold(aliasFolded)
|
|
342
267
|
: aliasFolded;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
268
|
+
// RN'S TWO TEXT DEFAULTS USED TO BE APPLIED HERE AND ARE GONE (2026-09-18) — the rule lives in
|
|
269
|
+
// `SymbioteFabricProps.cpp` alone, and its claims in `committed-payload.itest.ts`, read off the
|
|
270
|
+
// payload a commit actually sent. It had FIVE other implementations that day (`resolveTextProps`,
|
|
271
|
+
// Angular's `TextHost`, Vue's and Solid's renderers, and this one); the engine is the only layer
|
|
272
|
+
// that can see the authored bag for every adapter at once, so it is the only one that needs to.
|
|
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.
|
|
276
|
+
const props = behaviorFolded;
|
|
277
|
+
// Hoisted out of the loop: one cached lookup per node per commit, not one per key.
|
|
278
|
+
const alreadyProcessed = configProcessedKeys(node.component);
|
|
347
279
|
for (const key of Object.keys(props)) {
|
|
348
280
|
if (key === 'style')
|
|
349
281
|
continue;
|
|
@@ -352,10 +284,10 @@ export function fabricProps(node) {
|
|
|
352
284
|
continue;
|
|
353
285
|
if (value === undefined)
|
|
354
286
|
continue;
|
|
355
|
-
out[key] = processValue(
|
|
287
|
+
out[key] = processValue(key, value, alreadyProcessed);
|
|
356
288
|
}
|
|
357
289
|
// Hoist the style slot (object | array | nested arrays) into the SAME payload object - no
|
|
358
290
|
// intermediate flatten. See addStyle for the shape and for the two things this fixed.
|
|
359
|
-
addStyle(out,
|
|
291
|
+
addStyle(out, props.style);
|
|
360
292
|
return out;
|
|
361
293
|
}
|
package/build/fabric.d.ts
CHANGED
|
@@ -36,5 +36,14 @@ interface IFabricHost extends Omit<IFabricSlot, 'cloneNodeWithNewChildren' | 'cl
|
|
|
36
36
|
declare global {
|
|
37
37
|
var nativeFabricUIManager: IFabricHost | undefined;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Test seam: forget the bound slot, so a fixture can install a different host and be believed.
|
|
41
|
+
*
|
|
42
|
+
* `getSlot` caches the facade for the life of the module — the live binding re-mints a host function
|
|
43
|
+
* on every property read, so caching is not an optimisation but the difference between reading each
|
|
44
|
+
* method once and reading it per call. The cache has no invalidation in production because the
|
|
45
|
+
* global is installed once, before anything commits.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resetSlot(): void;
|
|
39
48
|
export declare function getSlot(): IFabricSlot;
|
|
40
49
|
export {};
|
package/build/fabric.js
CHANGED
|
@@ -5,7 +5,27 @@
|
|
|
5
5
|
// The live object is a lazy caching proxy: every property access mints a fresh
|
|
6
6
|
// host function, so we read each method once and cache a plain facade.
|
|
7
7
|
import { dlog } from './debug.js';
|
|
8
|
+
import { installNativeTreeHost } from './native-tree-host.js';
|
|
8
9
|
let cached;
|
|
10
|
+
// BATCHING IS GONE, and it is worth one paragraph because the idea recurs. `batching-slot.ts`
|
|
11
|
+
// recorded this slot's calls and replayed them once per commit — three ways, the last handing the
|
|
12
|
+
// bytes to `SymbioteApplier` in C++. It existed to remove per-call JSI crossings from a JS walk that
|
|
13
|
+
// worked out the Fabric operations. That walk no longer exists: adapters record their own mutations
|
|
14
|
+
// and the tree host derives everything, so there are no per-call crossings left to batch. Removed
|
|
15
|
+
// 2026-09-08 along with `setBatchedCommits`, the C++ applier and their differential. Root CLAUDE.md
|
|
16
|
+
// keeps the measurement that made it uninteresting even on the old path — Create 256.8 on / 258.5
|
|
17
|
+
// off, i.e. it demonstrably worked and bought nothing.
|
|
18
|
+
/**
|
|
19
|
+
* Test seam: forget the bound slot, so a fixture can install a different host and be believed.
|
|
20
|
+
*
|
|
21
|
+
* `getSlot` caches the facade for the life of the module — the live binding re-mints a host function
|
|
22
|
+
* on every property read, so caching is not an optimisation but the difference between reading each
|
|
23
|
+
* method once and reading it per call. The cache has no invalidation in production because the
|
|
24
|
+
* global is installed once, before anything commits.
|
|
25
|
+
*/
|
|
26
|
+
export function resetSlot() {
|
|
27
|
+
cached = undefined;
|
|
28
|
+
}
|
|
9
29
|
export function getSlot() {
|
|
10
30
|
if (cached)
|
|
11
31
|
return cached;
|
|
@@ -72,5 +92,17 @@ export function getSlot() {
|
|
|
72
92
|
measureLayout: (node, relativeToNode, onFail, onSuccess) => measureLayout(node, relativeToNode, onFail, onSuccess),
|
|
73
93
|
};
|
|
74
94
|
dlog('slot bound to nativeFabricUIManager');
|
|
95
|
+
// Resolve our own native module here, once, for its SIDE EFFECT: `RCTTurboModuleManager` runs
|
|
96
|
+
// `installJSIBindingsWithRuntime:` when it CREATES a module, so without this call the module is
|
|
97
|
+
// never created, the hook never runs, and `global.__symbioteEngineNative` is absent on a device
|
|
98
|
+
// carrying a perfectly working binary. A capability that is unreachable until its first consumer
|
|
99
|
+
// lands is indistinguishable from one that is broken, and the difference costs a build to find out.
|
|
100
|
+
//
|
|
101
|
+
// This is the right seam rather than a convenient one, and now for two reasons: binding the Fabric
|
|
102
|
+
// slot is the moment the engine has established it is on a native host at all, AND it is the last
|
|
103
|
+
// moment before a commit can happen — the tree host has to be in before `commitSurfaceOps` runs or
|
|
104
|
+
// the ops it names stay pending. It cannot throw: with no module `installNativeTreeHost()` is a
|
|
105
|
+
// no-op, which is most places (see `native-engine.ts`'s header).
|
|
106
|
+
installNativeTreeHost();
|
|
75
107
|
return cached;
|
|
76
108
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type ISymbioteNode } from './node';
|
|
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
|
+
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
|
+
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
|
+
export declare function parentsOf(nodes: readonly ISymbioteNode[]): readonly (ISymbioteNode | undefined)[];
|
|
41
|
+
/** Each root and every descendant, pre-order, anchors included — all of it in ONE crossing. */
|
|
42
|
+
export declare function subtreesOf(roots: readonly ISymbioteNode[]): readonly ISymbioteNode[];
|
|
43
|
+
/**
|
|
44
|
+
* The node and every ancestor above it, deepest first, in ONE crossing.
|
|
45
|
+
*
|
|
46
|
+
* `parentOf` per level is a crossing per level, and the event path needs this chain for every
|
|
47
|
+
* event — capture reads it reversed, bubble forward — plus again on every frame of a drag, for the
|
|
48
|
+
* responder's scope. Measured at 18 crossings per event on a depth-8 chain before the two phases
|
|
49
|
+
* shared a walk, 9 after, and 1 through here.
|
|
50
|
+
*
|
|
51
|
+
* Surfaces are dropped the same way `parentOf` drops them, by component, so a caller sees the same
|
|
52
|
+
* chain it would have built by walking.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ancestorsOf(node: ISymbioteNode): readonly ISymbioteNode[];
|
|
55
|
+
/** The first child, anchors included, or `undefined` for a leaf. */
|
|
56
|
+
export declare function firstChildOf(node: ISymbioteNode): ISymbioteNode | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The next sibling, or `undefined` at the end of the list.
|
|
59
|
+
*
|
|
60
|
+
* `surface` is required to answer for a TOP-LEVEL node, which has no parent to read the sibling
|
|
61
|
+
* list from — the surface owns that list instead. Passing it for a parented node is harmless and
|
|
62
|
+
* ignored, so a caller with one active surface can pass it unconditionally.
|
|
63
|
+
*/
|
|
64
|
+
export declare function nextSiblingOf(node: ISymbioteNode, surface?: SymbioteSurface): ISymbioteNode | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the node is a TEXT CONTAINER (`<Text>`), not whether it holds a string.
|
|
67
|
+
*
|
|
68
|
+
* The distinction is load-bearing for adapters that ask "can I write a string into this": a raw
|
|
69
|
+
* text node answers FALSE here, and an anchor does too. Use `isRawTextNode` for that question.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isTextContainer(node: ISymbioteNode): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the node is a RAW TEXT node — one a string can be written into.
|
|
74
|
+
*
|
|
75
|
+
* This is the question `solid-js/universal`'s `insertExpression` actually asks before calling
|
|
76
|
+
* replaceText, and answering it with `isTextContainer` would be wrong in both directions: a
|
|
77
|
+
* `<Text>` is a container that holds no string of its own, and the empty-string ANCHOR that
|
|
78
|
+
* cleanChildren leaves to hold a position is not writable either. An anchor is excluded here by
|
|
79
|
+
* construction, since its component is the `#anchor` sentinel.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isRawTextNode(node: ISymbioteNode): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* The Fabric view name this node currently resolves to (`RCTView`, `RCTText`, `RCTRawText`, the
|
|
84
|
+
* `#anchor` sentinel …). Exposed because two seams branch on it — Solid to answer `isTextNode`,
|
|
85
|
+
* Angular to recognise its own anchor hosts — and both read `node.component` directly today.
|
|
86
|
+
*
|
|
87
|
+
* NOT stable across a node's life: a primitive whose native view depends on a prop (`TextInput`'s
|
|
88
|
+
* `multiline`) changes view without changing identity. Read it, never cache it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function componentOf(node: ISymbioteNode): string;
|
|
91
|
+
/**
|
|
92
|
+
* The string a raw-text node currently holds, or `undefined` for any other node.
|
|
93
|
+
*
|
|
94
|
+
* Exists for the DIAGNOSTIC path rather than the render path: a seam that rejects a bare string
|
|
95
|
+
* outside a `<Text>` wants to name the offending text in its error, and reading `node.props.text`
|
|
96
|
+
* to do so is the last thing keeping that seam coupled to the node's shape. Vue's
|
|
97
|
+
* `setElementText` reads the same value for a real reason, so this is not a one-caller accessor.
|
|
98
|
+
*/
|
|
99
|
+
export declare function textOf(node: ISymbioteNode): string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Every prop standing on the node, function props included.
|
|
102
|
+
*
|
|
103
|
+
* The bag a payload fold reads. Two sources, because a function never crossed the wire (`writeProp`,
|
|
104
|
+
* node.ts): the host holds the values it could take, JS holds the callbacks it could not, and a fold
|
|
105
|
+
* asking for `onPress` must see the one the app wrote rather than the `undefined` the host was
|
|
106
|
+
* handed in its place.
|
|
107
|
+
*
|
|
108
|
+
* A COPY when anything was stashed, the host's own object when nothing was — which is nearly every
|
|
109
|
+
* node. Same caveat as `propOf`: `style` after a class merge is the `[classStyle, explicitStyle]`
|
|
110
|
+
* array, not the author's object.
|
|
111
|
+
*/
|
|
112
|
+
export declare function propsOf(node: ISymbioteNode): Readonly<Record<string, unknown>>;
|
|
113
|
+
/**
|
|
114
|
+
* A TEST read: the PAYLOAD the last commit handed Fabric for this node, `undefined` before one.
|
|
115
|
+
*
|
|
116
|
+
* `propsOf` above is the props AS THE OPS NAMED THEM — what the adapter said. This is what the
|
|
117
|
+
* payload builder MADE of them, after the aria fold, the behavior's own fold, the component-keyed
|
|
118
|
+
* folds and the style hoist. The two answer different questions and a test has to pick: "did the
|
|
119
|
+
* adapter write `inputMode`" is `propsOf`, and "did that reach native as `keyboardType`" is this.
|
|
120
|
+
*
|
|
121
|
+
* Only the native host can answer it — see `ITreeHost.committedPayloadOf`. Under the recording host
|
|
122
|
+
* it throws, on purpose, naming the itest suite as where the question belongs.
|
|
123
|
+
*/
|
|
124
|
+
export declare function committedPayloadOf(node: ISymbioteNode): Readonly<Record<string, unknown>> | undefined;
|
|
125
|
+
export declare function propOf(node: ISymbioteNode, key: string): unknown;
|