@symbiote-native/engine 1.3.1 → 1.5.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/build/event-value-source.d.ts +4 -0
- package/build/event-value-source.js +3 -0
- package/build/events/delivery.d.ts +9 -0
- package/build/events/delivery.js +143 -0
- package/build/events/index.js +199 -660
- package/build/events/names.d.ts +24 -0
- package/build/events/names.js +80 -0
- package/build/events/press.d.ts +20 -0
- package/build/events/press.js +89 -0
- package/build/events/responder.d.ts +6 -0
- package/build/events/responder.js +124 -0
- package/build/expo-native-view.d.ts +2 -0
- package/build/expo-native-view.js +19 -0
- package/build/imperative.d.ts +2 -0
- package/build/imperative.js +18 -1
- package/build/index.d.ts +12 -2
- package/build/index.js +8 -2
- package/build/mutation-buffer.d.ts +3 -0
- package/build/mutation-buffer.js +25 -8
- package/build/native-engine.d.ts +6 -0
- package/build/native-engine.js +27 -48
- package/build/native-tree-host.js +1 -0
- package/build/node-events.d.ts +11 -0
- package/build/node-events.js +145 -0
- package/build/node-instance.d.ts +8 -0
- package/build/node-instance.js +168 -0
- package/build/node-props.d.ts +13 -0
- package/build/node-props.js +131 -0
- package/build/node-route.d.ts +2 -0
- package/build/node-route.js +151 -0
- package/build/node-style.d.ts +15 -0
- package/build/node-style.js +214 -0
- package/build/node-tree.d.ts +6 -0
- package/build/node-tree.js +159 -0
- package/build/node-types.d.ts +70 -0
- package/build/node-types.js +36 -0
- package/build/node.d.ts +7 -97
- package/build/node.js +9 -1128
- package/build/permission-hook-runtime.d.ts +27 -0
- package/build/permission-hook-runtime.js +37 -0
- package/build/resource-controller.d.ts +6 -0
- package/build/resource-controller.js +30 -0
- package/build/tree-host.d.ts +7 -0
- package/cpp/SymbioteEngineBindings.cpp +19 -18
- package/cpp/SymbioteTree.cpp +81 -156
- package/cpp/SymbioteTree.h +6 -0
- package/package.json +2 -2
package/build/node.js
CHANGED
|
@@ -1,1128 +1,9 @@
|
|
|
1
|
-
// The mutation API
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { resolveStructuredStyle } from './structured-style.js';
|
|
11
|
-
import { IMAGE_SOURCE_PROPS, IMAGE_LOAD_EVENT_NAMES, anyImageLoadEventListenerWired, resolveImageSourceProp, } from './image-source-write.js';
|
|
12
|
-
import { Platform } from './platform';
|
|
13
|
-
// A cycle, deliberately: `imperative.ts` imports this module; the prototype methods below call
|
|
14
|
-
// back into it. Neither touches the other at module-evaluation time, only inside a function body,
|
|
15
|
-
// so every loader resolves it (CLAUDE.md's load-time side-effect rule).
|
|
16
|
-
import { measure as engineMeasure, measureInWindow as engineMeasureInWindow, measureLayout as engineMeasureLayout, setNativeProps as engineSetNativeProps, dispatchViewCommand, } from './imperative.js';
|
|
17
|
-
// The same deliberate cycle, for the same reason: `tree-host.ts` imports `takePropStats` from here
|
|
18
|
-
// and `censusRetainedTree` below asks it for the census. Function bodies only, on both sides.
|
|
19
|
-
import { EMPTY_CENSUS, flushOps, treeHost, } from './tree-host.js';
|
|
20
|
-
// The same deliberate cycle, for the same reason: `routeProp` resolves an AnimatedNode written
|
|
21
|
-
// into a prop, and the module that owns that resolution reaches back here for `setProp`. See
|
|
22
|
-
// `animated/host-binding.ts`'s header.
|
|
23
|
-
import { hasAnimatedNodes } from './animated/graph.js';
|
|
24
|
-
import { bindAnimatedEvent, bindAnimatedValue, hasAnimatedBindings, reattachAnimatedProps, } from './animated/host-binding.js';
|
|
25
|
-
const BRAND = Symbol('symbiote.node');
|
|
26
|
-
// A node carries the Fabric view name directly, so adding a primitive is just a new string from
|
|
27
|
-
// the adapter, no core change. The only name resolved at commit time is text: a <Text> nested
|
|
28
|
-
// inside another <Text> becomes a virtual span; `isText` marks a container so descendants pick it.
|
|
29
|
-
export const RAW_TEXT_COMPONENT = 'RCTRawText';
|
|
30
|
-
export const TEXT_COMPONENT = 'RCTText';
|
|
31
|
-
export const VIRTUAL_TEXT_COMPONENT = 'RCTVirtualText';
|
|
32
|
-
// Runtime guard narrowing `unknown` to ISymbioteEvent (no `as` cast). Lives with the interface
|
|
33
|
-
// it tests, so every adapter checking for a Symbiote event shares one guard instead of writing
|
|
34
|
-
// its own copy.
|
|
35
|
-
export function isSymbioteEvent(value) {
|
|
36
|
-
if (typeof value !== 'object' || value === null)
|
|
37
|
-
return false;
|
|
38
|
-
const nativeEvent = Reflect.get(value, 'nativeEvent');
|
|
39
|
-
return typeof nativeEvent === 'object' && nativeEvent !== null;
|
|
40
|
-
}
|
|
41
|
-
const FOCUS_COMMAND = 'focus';
|
|
42
|
-
const BLUR_COMMAND = 'blur';
|
|
43
|
-
// Names and arg order mirror RN's ScrollViewCommands.
|
|
44
|
-
const SCROLL_TO_COMMAND = 'scrollTo';
|
|
45
|
-
const SCROLL_TO_END_COMMAND = 'scrollToEnd';
|
|
46
|
-
const FLASH_SCROLL_INDICATORS_COMMAND = 'flashScrollIndicators';
|
|
47
|
-
// The one shape every retained node has: a class, so the imperative methods share a prototype
|
|
48
|
-
// instead of being allocated per node, and both factories below mint the same hidden class. Fields
|
|
49
|
-
// are `declare`d and assigned in the constructor, the shape V8/Hermes handle best.
|
|
50
|
-
class SymbioteNode {
|
|
51
|
-
constructor(component, isText) {
|
|
52
|
-
// Every field below is assigned here, not lazily: present from the constructor, they all keep
|
|
53
|
-
// ONE hidden class for every node. `attachHostBehavior` raises several of them later for the
|
|
54
|
-
// rare node whose behavior declares that capability.
|
|
55
|
-
this[BRAND] = true;
|
|
56
|
-
this.component = component;
|
|
57
|
-
this.isText = isText;
|
|
58
|
-
this.listeners = undefined;
|
|
59
|
-
this.hasCommitHook = false;
|
|
60
|
-
this.resolvesImageSources = false;
|
|
61
|
-
this.nativeIdWinsOverId = false;
|
|
62
|
-
this.styleParts = undefined;
|
|
63
|
-
this.payloadFold = undefined;
|
|
64
|
-
this.hostBehavior = undefined;
|
|
65
|
-
this.childHost = undefined;
|
|
66
|
-
this.wrapper = undefined;
|
|
67
|
-
this.mayHaveChildren = false;
|
|
68
|
-
this.isTornDown = false;
|
|
69
|
-
// `slotOf` reads this pair on EVERY handle operand of every op, so both must be stable slots.
|
|
70
|
-
// `slotBatch` starts at a value no real batch carries, so an untouched node reads as "not in
|
|
71
|
-
// this batch" with no separate flag.
|
|
72
|
-
this.slot = 0;
|
|
73
|
-
this.slotBatch = 0;
|
|
74
|
-
}
|
|
75
|
-
measure(callback) {
|
|
76
|
-
engineMeasure(this, callback);
|
|
77
|
-
}
|
|
78
|
-
measureInWindow(callback) {
|
|
79
|
-
engineMeasureInWindow(this, callback);
|
|
80
|
-
}
|
|
81
|
-
measureLayout(relativeToNativeNode, onSuccess, onFail) {
|
|
82
|
-
if (!isSymbioteNode(relativeToNativeNode)) {
|
|
83
|
-
dlog('measureLayout: relative target must be a host ref');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
engineMeasureLayout(this, relativeToNativeNode, onSuccess, onFail);
|
|
87
|
-
}
|
|
88
|
-
setNativeProps(nativeProps) {
|
|
89
|
-
engineSetNativeProps(this, nativeProps);
|
|
90
|
-
}
|
|
91
|
-
focus() {
|
|
92
|
-
dispatchViewCommand(this, FOCUS_COMMAND, []);
|
|
93
|
-
}
|
|
94
|
-
blur() {
|
|
95
|
-
dispatchViewCommand(this, BLUR_COMMAND, []);
|
|
96
|
-
}
|
|
97
|
-
// The defaults live HERE and nowhere else. `buildScrollViewHandle`
|
|
98
|
-
// (`@symbiote-native/components`) delegates here, so a built handle and a node cannot drift on
|
|
99
|
-
// what `scrollTo()` with no argument means.
|
|
100
|
-
scrollTo(options) {
|
|
101
|
-
const x = options?.x ?? 0;
|
|
102
|
-
const y = options?.y ?? 0;
|
|
103
|
-
const animated = options?.animated ?? true;
|
|
104
|
-
dlog(`ScrollView.scrollTo x=${x} y=${y} animated=${animated}`);
|
|
105
|
-
dispatchViewCommand(this, SCROLL_TO_COMMAND, [x, y, animated]);
|
|
106
|
-
}
|
|
107
|
-
scrollToEnd(options) {
|
|
108
|
-
const animated = options?.animated ?? true;
|
|
109
|
-
dlog(`ScrollView.scrollToEnd animated=${animated}`);
|
|
110
|
-
dispatchViewCommand(this, SCROLL_TO_END_COMMAND, [animated]);
|
|
111
|
-
}
|
|
112
|
-
flashScrollIndicators() {
|
|
113
|
-
dlog('ScrollView.flashScrollIndicators');
|
|
114
|
-
dispatchViewCommand(this, FLASH_SCROLL_INDICATORS_COMMAND, []);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// The committed record — handle, tag, rootTag — is the host's; `committedRecordOf` (tree-host.ts)
|
|
118
|
-
// answers for it, keyed on the handle OBJECT. A Vue Proxy around a host element misses that key,
|
|
119
|
-
// so a wrapped node's calls degrade to "not committed" — hold host nodes with `shallowRef`.
|
|
120
|
-
// Mint an element and record its creation. The node object IS the handle: what the ops address,
|
|
121
|
-
// what the host attaches its native node to, what Fabric hands back as an event target.
|
|
122
|
-
export function createElement(component, isText = false,
|
|
123
|
-
// The intrinsic tag this node came from, when it differs from the Fabric view name above. The
|
|
124
|
-
// behavior registry is keyed by tag, so an adapter creating `<pressable>` must hand it over here
|
|
125
|
-
// or the registration cannot fire. Nothing is stored — the lookup happens once, right below.
|
|
126
|
-
tag = component) {
|
|
127
|
-
const node = new SymbioteNode(component, isText);
|
|
128
|
-
// A primitive that commits NO VIEW resolves to the anchor component through `descriptorFor`
|
|
129
|
-
// (touchable-without-feedback, touchable-native-feedback), reaching this rather than
|
|
130
|
-
// `createAnchor` because the caller only knows it has a descriptor.
|
|
131
|
-
if (component === ANCHOR_COMPONENT)
|
|
132
|
-
recordCreateAnchor(node);
|
|
133
|
-
// A primitive whose ENTIRE subtree must vanish on this platform (input-accessory-view on
|
|
134
|
-
// Android) resolves to the void component the same way. An anchor hoists its children into
|
|
135
|
-
// Fabric in its place; a void node contributes neither itself nor them.
|
|
136
|
-
else if (component === VOID_COMPONENT)
|
|
137
|
-
recordCreateVoid(node);
|
|
138
|
-
// `instanceHandle` is the node itself: it round-trips through Fabric unchanged and comes back
|
|
139
|
-
// as the event target, and the BRAND below confirms it is one of ours.
|
|
140
|
-
else
|
|
141
|
-
recordCreateElement(node, component, isText, node);
|
|
142
|
-
// Gated on the boolean, not on the Map: this runs ~9 000 times per benchmark create, and an app
|
|
143
|
-
// that registers nothing must pay one boolean read rather than a hash lookup per node.
|
|
144
|
-
if (hasHostBehaviors())
|
|
145
|
-
attachHostBehavior(node, tag);
|
|
146
|
-
// A third-party view's own ViewConfig processors, as a fold, AFTER the behavior's — the behavior
|
|
147
|
-
// rewrites wrapper-body props, and `validAttributes[*].process` then converts what it produced.
|
|
148
|
-
// Costs one `Set.has` per node for a built-in, where `resolve` bails.
|
|
149
|
-
const configFold = configPayloadFold(component);
|
|
150
|
-
if (configFold !== undefined) {
|
|
151
|
-
const behaviorFold = node.payloadFold;
|
|
152
|
-
node.payloadFold =
|
|
153
|
-
behaviorFold === undefined
|
|
154
|
-
? configFold
|
|
155
|
-
: props => configFold(behaviorFold(props));
|
|
156
|
-
}
|
|
157
|
-
return node;
|
|
158
|
-
}
|
|
159
|
-
// `tag` mirrors `createElement`'s: the behavior registry is keyed by tag, and a raw text's CONTENT
|
|
160
|
-
// can still be a function of the platform (Button renders its title uppercased on Android), even
|
|
161
|
-
// with no props an app can write. Defaulted to the raw-text component for existing callers.
|
|
162
|
-
export function createRawText(text, tag = RAW_TEXT_COMPONENT) {
|
|
163
|
-
const node = new SymbioteNode(RAW_TEXT_COMPONENT, false);
|
|
164
|
-
recordCreateRawText(node, text);
|
|
165
|
-
// TAG check first, not `hasHostBehaviors()`: almost no raw text is tagged (thousands are leaves
|
|
166
|
-
// under a `<Text>`), so an untagged one pays a pointer-equality compare against the default,
|
|
167
|
-
// not a registry lookup.
|
|
168
|
-
if (tag !== RAW_TEXT_COMPONENT && hasHostBehaviors())
|
|
169
|
-
attachHostBehavior(node, tag);
|
|
170
|
-
return node;
|
|
171
|
-
}
|
|
172
|
-
// `instanceHandle` round-trips through Fabric unchanged: the object we pass to
|
|
173
|
-
// createNode comes back as the event target. We brand our nodes so the event
|
|
174
|
-
// handler can confirm a target is one of ours before dispatching.
|
|
175
|
-
export function isSymbioteNode(value) {
|
|
176
|
-
return typeof value === 'object' && value !== null && BRAND in value;
|
|
177
|
-
}
|
|
178
|
-
// A WeakMap can't be logged, so this gives every node a small human-readable id, assigned lazily
|
|
179
|
-
// on first call — lets a dlog at ref-attach time and one at commit/dispatch time be compared to
|
|
180
|
-
// prove whether they're the SAME node object. Kept behind DEBUG per <keep_logs_gate_behind_DEBUG>.
|
|
181
|
-
const debugIds = new WeakMap();
|
|
182
|
-
let nextDebugId = 1;
|
|
183
|
-
export function debugNodeId(node) {
|
|
184
|
-
let id = debugIds.get(node);
|
|
185
|
-
if (id === undefined) {
|
|
186
|
-
id = nextDebugId++;
|
|
187
|
-
debugIds.set(node, id);
|
|
188
|
-
}
|
|
189
|
-
return id;
|
|
190
|
-
}
|
|
191
|
-
// Vue's runtime-core needs comment/anchor nodes (fragments, v-if, v-for) to track sibling order;
|
|
192
|
-
// Fabric has no such concept. An anchor is a real retained node so insert/nextSibling/parentNode
|
|
193
|
-
// ordering stays correct, but the commit walk SKIPS it — no native view is ever created.
|
|
194
|
-
export const ANCHOR_COMPONENT = '#anchor';
|
|
195
|
-
export function createAnchor() {
|
|
196
|
-
const node = new SymbioteNode(ANCHOR_COMPONENT, false);
|
|
197
|
-
recordCreateAnchor(node);
|
|
198
|
-
return node;
|
|
199
|
-
}
|
|
200
|
-
// The sentinel a primitive resolves to when its ENTIRE subtree must vanish from Fabric on this
|
|
201
|
-
// platform (input-accessory-view on Android). Unlike ANCHOR_COMPONENT, whose node hoists children
|
|
202
|
-
// up, a void node's children never reach Fabric either — the commit walk stops there, recursively.
|
|
203
|
-
export const VOID_COMPONENT = '#void';
|
|
204
|
-
export function createVoid() {
|
|
205
|
-
const node = new SymbioteNode(VOID_COMPONENT, false);
|
|
206
|
-
recordCreateVoid(node);
|
|
207
|
-
return node;
|
|
208
|
-
}
|
|
209
|
-
// The sentinel a SURFACE's own root node carries, so `parentOf` can stop there. A top-level node
|
|
210
|
-
// must answer `undefined` for its parent — Angular reads `null` as "defer, ng-content will place
|
|
211
|
-
// this", while Vue/Solid spell `?? surface` and need the exact object to compare against.
|
|
212
|
-
// JS-side name only: what goes over the wire is `RCTView`, because this node is real.
|
|
213
|
-
export const SURFACE_COMPONENT = '#surface';
|
|
214
|
-
// One persistent root view per surface, mirroring RN's own AppContainer: `renderApplication` wraps
|
|
215
|
-
// the app in `<View style={{flex:1}} pointerEvents="box-none">`. Not decoration — without flex:1 a
|
|
216
|
-
// non-flex root collapses to content height, and without box-none an outside touch has no escape.
|
|
217
|
-
// Commits as ONE node rather than hoisting children into the child set, so the host materializes
|
|
218
|
-
// the node OP_COMMIT names instead of walking its children (an anchor there still hoists).
|
|
219
|
-
export function createSurfaceRoot() {
|
|
220
|
-
const node = new SymbioteNode(SURFACE_COMPONENT, false);
|
|
221
|
-
recordCreateElement(node, 'RCTView', false, node);
|
|
222
|
-
// Recorded straight, not through `routeProp`: these are literal Fabric props, not props an app
|
|
223
|
-
// authored, so they want none of the class merging or event routing that path exists for.
|
|
224
|
-
recordSetProp(node, 'style', { flex: 1 });
|
|
225
|
-
recordSetProp(node, 'pointerEvents', 'box-none');
|
|
226
|
-
return node;
|
|
227
|
-
}
|
|
228
|
-
export function isAnchor(node) {
|
|
229
|
-
return node.component === ANCHOR_COMPONENT;
|
|
230
|
-
}
|
|
231
|
-
// A raw text with no characters must not reach Fabric — AttributedString::appendFragment drops the
|
|
232
|
-
// fragment while the text walk has already flagged "last child was raw text", so the next raw
|
|
233
|
-
// sibling merges into an empty `fragments.back()` and aborts. Enforced by the host, not here.
|
|
234
|
-
// No dirty-marking here: an op names the node it changed, so the host marks that node and its
|
|
235
|
-
// ancestors. `node.listeners` never reaches Fabric, except `layout`, which raises `onLayout`
|
|
236
|
-
// through `setProp` below.
|
|
237
|
-
// Change which Fabric view a node commits as, keeping the node's identity. Policy stays out of the
|
|
238
|
-
// engine — which prop decides, and between which views, lives in HOST_PRIMITIVES; the engine only
|
|
239
|
-
// swaps the name. A no-op when unchanged, so a renderer may call it on every update.
|
|
240
|
-
// Both the JS field and the op move: `node.component` is what the aria fold and fabricProps key
|
|
241
|
-
// on, the op is what makes the host re-create the node, since no prop write moves it between views.
|
|
242
|
-
export function setNodeComponent(node, component) {
|
|
243
|
-
if (node.component === component)
|
|
244
|
-
return;
|
|
245
|
-
node.component = component;
|
|
246
|
-
recordSetComponent(node, component);
|
|
247
|
-
}
|
|
248
|
-
// Tree-staleness marking (which ancestor went stale, presence flips, anchor climbs) lives entirely
|
|
249
|
-
// on the host now, derived from the ops themselves — nothing here has to answer those questions.
|
|
250
|
-
// How many prop writes an adapter pushed at the engine. Read-and-zeroed through
|
|
251
|
-
// readCommitProfile() (tree-host.ts), which prices the layer ABOVE the host. No `noops` count: the
|
|
252
|
-
// `Object.is` guard lives in the host, which would need the previous value back over the wire.
|
|
253
|
-
// Not gated behind isDebug(): an integer increment is noise next to the prop write it counts, and
|
|
254
|
-
// the figure is only meaningful from a release build. No per-call dlog either — a log line per
|
|
255
|
-
// write would measure the logging rather than the code.
|
|
256
|
-
const propStats = { writes: 0 };
|
|
257
|
-
export function takePropStats() {
|
|
258
|
-
const snapshot = { writes: propStats.writes };
|
|
259
|
-
propStats.writes = 0;
|
|
260
|
-
return snapshot;
|
|
261
|
-
}
|
|
262
|
-
// `<component>.<key>` -> write count, gated behind `isDebug()`: a Map lookup per write is real
|
|
263
|
-
// cost on the hottest path, so it only runs when someone asked. Names exactly which (view, key)
|
|
264
|
-
// pair an adapter comparison's aggregate delta is hiding.
|
|
265
|
-
let propKeyTally;
|
|
266
|
-
export function takePropKeyTally() {
|
|
267
|
-
const snapshot = propKeyTally ?? new Map();
|
|
268
|
-
propKeyTally = undefined;
|
|
269
|
-
return snapshot;
|
|
270
|
-
}
|
|
271
|
-
// A pure prop set: no event inference. `onTintColor` is a Switch prop and reaches Fabric like any
|
|
272
|
-
// other; the event-vs-prop decision is made by routeProp, never by the key's name.
|
|
273
|
-
// `undefined` DELETES the key (`NO_VALUE` on the wire, mutation-buffer.ts). `null` is NOT the same:
|
|
274
|
-
// it's a legitimate Fabric value meaning "reset to default", and a merge-on-clone host must tell a
|
|
275
|
-
// removed key from one that was never there.
|
|
276
|
-
// No `Object.is` DEDUPE HERE: it needs the value the node already holds, which JS doesn't. The
|
|
277
|
-
// guard lives in the host's `OP_SET_PROP` instead, where the previous value is a local field.
|
|
278
|
-
export function setProp(node, key, value) {
|
|
279
|
-
// A composed primitive's slot — and its wrapper, where it has one — can carry a value DERIVED
|
|
280
|
-
// from an owner prop, so `markPropsDirty` bubbles up or neither ever learns. Here, not in
|
|
281
|
-
// `routeProp`: this is the one choke point every writer passes. See `IHostBehavior.slotDerived`.
|
|
282
|
-
if (node.childHost !== undefined && slotDerivesFrom(node, key)) {
|
|
283
|
-
markPropsDirty(node.childHost);
|
|
284
|
-
// Past the slot: a `buildStructure` that builds a CHAIN registers the deeper nodes here, and
|
|
285
|
-
// each keeps its own pure fold reading the owner. See `addDerivedNode`.
|
|
286
|
-
const derived = derivedNodesOf(node);
|
|
287
|
-
if (derived !== undefined)
|
|
288
|
-
for (const each of derived)
|
|
289
|
-
markPropsDirty(each);
|
|
290
|
-
if (node.wrapper !== undefined)
|
|
291
|
-
markPropsDirty(node.wrapper);
|
|
292
|
-
}
|
|
293
|
-
propStats.writes += 1;
|
|
294
|
-
if (isDebug()) {
|
|
295
|
-
propKeyTally ??= new Map();
|
|
296
|
-
const tallyKey = `${node.component}.${key}`;
|
|
297
|
-
propKeyTally.set(tallyKey, (propKeyTally.get(tallyKey) ?? 0) + 1);
|
|
298
|
-
}
|
|
299
|
-
writeProp(node, key, value);
|
|
300
|
-
}
|
|
301
|
-
// Function props that never left JS, keyed by node. A function CANNOT cross this wire:
|
|
302
|
-
// `jsi::dynamicFromValue` THROWS on a callable, killing the whole batch.
|
|
303
|
-
// Two paths reach here past `routeProp`'s registered-`on*` diversion: an unregistered `on*` that's
|
|
304
|
-
// an ordinary prop (`onValueChange`), and `setNativeProps`, which bypasses `routeProp` entirely —
|
|
305
|
-
// e.g. `Animated.View` spreading `panResponder.panHandlers` copies every key it holds.
|
|
306
|
-
// Live here like listeners do; `propOf` looks here first, keeping `onValueChange` readable.
|
|
307
|
-
// `fabricProps` drops function props on both hosts, so the payload is byte-identical either way.
|
|
308
|
-
const functionProps = new WeakMap();
|
|
309
|
-
// The one place a prop reaches the wire, and the only place that can keep a function off it.
|
|
310
|
-
// `setNativeProps` calls this rather than `recordSetProp` for that reason — the path with no
|
|
311
|
-
// `routeProp` in front of it.
|
|
312
|
-
export function writeProp(node, key, value) {
|
|
313
|
-
// The same strip `routeProp` does, repeated because THIS is the path with no `routeProp` in
|
|
314
|
-
// front of it: `AnimatedProps` re-sends its whole raw prop bag every frame, so on a JSX adapter
|
|
315
|
-
// `__self` rides straight past the declarative-path filter into the host.
|
|
316
|
-
if (REACT_JSX_DEV_PROPS.has(key))
|
|
317
|
-
return;
|
|
318
|
-
// Arms the node's recurring post-commit hook, for the rare node that has one. HERE, not in
|
|
319
|
-
// `setProp`: this is where both the declarative and `setNativeProps` paths meet, so a hook armed
|
|
320
|
-
// only by the former misses the imperative write entirely.
|
|
321
|
-
if (node.hasCommitHook)
|
|
322
|
-
noteCommitHookNodeChanged(node);
|
|
323
|
-
// `boxShadow`/`filter`/`transform` and Image's source props resolve on the way IN, at this same
|
|
324
|
-
// choke point: the C++ payload builder has no JS to do it headless. `structured-style.ts` hands
|
|
325
|
-
// back the same object when nothing needed resolving, keeping the host's identity guard intact.
|
|
326
|
-
let written = value;
|
|
327
|
-
if (key === 'style' || key === 'activeStyle') {
|
|
328
|
-
written = resolveStructuredStyle(value);
|
|
329
|
-
}
|
|
330
|
-
else if (node.resolvesImageSources && IMAGE_SOURCE_PROPS.has(key)) {
|
|
331
|
-
written = resolveImageSourceProp(value, key === 'source' && Platform.OS === 'android');
|
|
332
|
-
}
|
|
333
|
-
if (typeof written === 'function') {
|
|
334
|
-
let bag = functionProps.get(node);
|
|
335
|
-
if (bag === undefined) {
|
|
336
|
-
bag = new Map();
|
|
337
|
-
functionProps.set(node, bag);
|
|
338
|
-
}
|
|
339
|
-
bag.set(key, value);
|
|
340
|
-
// The host must not be left holding whatever stood under this key before — a stale value read
|
|
341
|
-
// back through `propOf` would beat the function this write just stashed.
|
|
342
|
-
recordSetProp(node, key, undefined);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
// Written over with a non-function: the stash must let go, or it keeps answering.
|
|
346
|
-
const bag = functionProps.get(node);
|
|
347
|
-
if (bag !== undefined)
|
|
348
|
-
bag.delete(key);
|
|
349
|
-
recordSetProp(node, key, written);
|
|
350
|
-
}
|
|
351
|
-
/** What `propOf` consults before asking the host. `undefined` when nothing was stashed. */
|
|
352
|
-
export function functionPropOf(node, key) {
|
|
353
|
-
return functionProps.get(node)?.get(key);
|
|
354
|
-
}
|
|
355
|
-
// The same stash, whole — what `propsOf` layers over the host's answer. `undefined` rather than an
|
|
356
|
-
// empty Map for a node that stashed nothing (nearly every node), so the caller hands back the
|
|
357
|
-
// host's own object instead of copying it.
|
|
358
|
-
export function functionPropsOf(node) {
|
|
359
|
-
return functionProps.get(node);
|
|
360
|
-
}
|
|
361
|
-
// "Rebuild this node's payload — the fold reads state I just changed." A behavior whose payload is
|
|
362
|
-
// DERIVED has no prop to write (the sticky header's debounced translateY lives in its own
|
|
363
|
-
// runtime), so this is the one route that dirties it directly. Pair with requestCommitFor.
|
|
364
|
-
export function markPropsDirty(node) {
|
|
365
|
-
flushOps();
|
|
366
|
-
// Announced to the buffer even though it writes no op: this is the one route that dirties a node
|
|
367
|
-
// without one, and a commit that cannot see it would skip itself as idle.
|
|
368
|
-
noteHostSideChange();
|
|
369
|
-
// The other way a node's payload is rebuilt, and the beat's population must cover both or a
|
|
370
|
-
// behavior whose payload is DERIVED — the sticky header's debounced translateY has no prop to
|
|
371
|
-
// write — would be armed by nothing.
|
|
372
|
-
if (node.hasCommitHook)
|
|
373
|
-
noteCommitHookNodeChanged(node);
|
|
374
|
-
treeHost()?.markPropsDirty(node);
|
|
375
|
-
}
|
|
376
|
-
// Fabric gates a handful of events behind a BOOLEAN prop: unlike scroll/touch/change, these fire
|
|
377
|
-
// only when the shadow node carries the flag. We drop function props from the payload, so a gated
|
|
378
|
-
// handler attaches on our side and the native event silently never arrives without this map.
|
|
379
|
-
// Exhaustive as of react-native 0.86: every `bool on*` field in Fabric's C++ props
|
|
380
|
-
// (ReactCommon/react/renderer/components/**), keyed by the post-`listenerName` event name.
|
|
381
|
-
// `magicTap` maps to `onMagicTap`, not the C++ member name `onAccessibilityMagicTap` — RN's own
|
|
382
|
-
// view config (BaseViewConfig.ios.js) disagrees with its C++ prop name, and matching stock is the
|
|
383
|
-
// only defensible choice until RN resolves it.
|
|
384
|
-
const GATED_EVENT_PROPS = new Map([
|
|
385
|
-
['layout', 'onLayout'],
|
|
386
|
-
['textLayout', 'onTextLayout'],
|
|
387
|
-
['accessibilityTap', 'onAccessibilityTap'],
|
|
388
|
-
['magicTap', 'onMagicTap'],
|
|
389
|
-
['accessibilityEscape', 'onAccessibilityEscape'],
|
|
390
|
-
['accessibilityAction', 'onAccessibilityAction'],
|
|
391
|
-
]);
|
|
392
|
-
// The explicit event channel. Structural adapters (Svelte addEventListener, Angular
|
|
393
|
-
// Renderer2.listen) call this directly with an already-known event name; flat-bag
|
|
394
|
-
// adapters reach it through routeProp. A non-function value clears the listener.
|
|
395
|
-
// Install a listener the BEHAVIOR owns, bypassing the ownership check: `setEventListener` diverts
|
|
396
|
-
// an owned name into the stash, which would be circular for the behavior's own dispatcher.
|
|
397
|
-
// `undefined` removes it, gate flag included — ScrollView takes the owner's `layout` only while
|
|
398
|
-
// something wants it, and a one-way installer would leave `onLayout: true` stuck in the payload.
|
|
399
|
-
export function setBehaviorListener(node, name, listener) {
|
|
400
|
-
if (listener === undefined)
|
|
401
|
-
node.listeners?.delete(name);
|
|
402
|
-
else
|
|
403
|
-
(node.listeners ??= new Map()).set(name, listener);
|
|
404
|
-
const flagProp = GATED_EVENT_PROPS.get(name);
|
|
405
|
-
if (flagProp !== undefined)
|
|
406
|
-
setProp(node, flagProp, listener === undefined ? undefined : true);
|
|
407
|
-
}
|
|
408
|
-
// The unowned names whose presence a platform rule reads. See `setEventListener`.
|
|
409
|
-
const PRESSABILITY_NAMES = new Set([
|
|
410
|
-
'press',
|
|
411
|
-
'longPress',
|
|
412
|
-
'startShouldSetResponder',
|
|
413
|
-
]);
|
|
414
|
-
export function setEventListener(node, name, value) {
|
|
415
|
-
const isHandler = typeof value === 'function';
|
|
416
|
-
// A name a host behavior OWNS never reaches `node.listeners`: the behavior's dispatcher holds
|
|
417
|
-
// that slot, and `node.listeners` is single-slot, so without this the app's callback would evict
|
|
418
|
-
// it with no diagnostic — on the keys a gesture STARTS on, silently pressless.
|
|
419
|
-
if (hasHostBehaviors() && ownsListener(node, name)) {
|
|
420
|
-
// The PRESENCE only, never the identity: a fresh closure nearly every render must not notify.
|
|
421
|
-
const wasWired = appListenerFor(node, name) !== undefined;
|
|
422
|
-
stashAppListener(node, name, isHandler ? value : undefined);
|
|
423
|
-
if (wasWired !== isHandler) {
|
|
424
|
-
// The BIT, on the flip only, so a platform rule (`focusable` on a touchable) can resolve a
|
|
425
|
-
// key depending on whether the app wired anything, without the closure leaving JS.
|
|
426
|
-
recordSetOwnedListener(node, name, isHandler);
|
|
427
|
-
notifyOwnedListenerChange(node, name, isHandler);
|
|
428
|
-
}
|
|
429
|
-
const flagged = GATED_EVENT_PROPS.get(name);
|
|
430
|
-
if (flagged !== undefined)
|
|
431
|
-
setProp(node, flagged, isHandler ? true : undefined);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
// A plain `<text>` presses through the engine's own synthesis, with no behavior owning the
|
|
435
|
-
// names, yet its payload depends on whether it is pressable (Android `accessible`, the `link`
|
|
436
|
-
// role). Same bit as the owned path, on the flip only.
|
|
437
|
-
if (PRESSABILITY_NAMES.has(name)) {
|
|
438
|
-
const wasWired = node.listeners?.has(name) === true;
|
|
439
|
-
if (wasWired !== isHandler)
|
|
440
|
-
recordSetOwnedListener(node, name, isHandler);
|
|
441
|
-
}
|
|
442
|
-
if (isHandler) {
|
|
443
|
-
const handler = value;
|
|
444
|
-
const listeners = (node.listeners ??= new Map());
|
|
445
|
-
listeners.set(name, (event) => handler(event));
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
node.listeners?.delete(name);
|
|
449
|
-
}
|
|
450
|
-
const flagProp = GATED_EVENT_PROPS.get(name);
|
|
451
|
-
if (flagProp !== undefined)
|
|
452
|
-
setProp(node, flagProp, isHandler ? true : undefined);
|
|
453
|
-
// `onLoad`/`onLoadStart`/`onLoadEnd`/`onError` are real Fabric events on `RCTImageView`, so they
|
|
454
|
-
// land here, not in `writeProp` — see image-source-write.ts for why Android's
|
|
455
|
-
// `shouldNotifyLoadEvents` is synthesized from the listener map, not a stashed function value.
|
|
456
|
-
if (node.resolvesImageSources && IMAGE_LOAD_EVENT_NAMES.has(name)) {
|
|
457
|
-
setProp(node, 'shouldNotifyLoadEvents', anyImageLoadEventListenerWired(node.listeners) ? true : undefined);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// `/^on[A-Z]/` spelled out: this runs on EVERY prop write, and a regex costs measurably more than
|
|
461
|
-
// the character reads (mutation-api-fill-cost.itest.ts). The boundary is pinned by node.test.ts.
|
|
462
|
-
// 111 is 'o', 110 is 'n', 65-90 is A-Z. `charCodeAt` past the end answers NaN, which fails every
|
|
463
|
-
// comparison, so a two-character `on` needs no length check.
|
|
464
|
-
function isOnEventName(key) {
|
|
465
|
-
if (key.charCodeAt(0) !== 111 || key.charCodeAt(1) !== 110)
|
|
466
|
-
return false;
|
|
467
|
-
const third = key.charCodeAt(2);
|
|
468
|
-
return third >= 65 && third <= 90;
|
|
469
|
-
}
|
|
470
|
-
// onChange -> change
|
|
471
|
-
function listenerName(propName) {
|
|
472
|
-
return propName.charAt(2).toLowerCase() + propName.slice(3);
|
|
473
|
-
}
|
|
474
|
-
// The responder-negotiation events (PanResponder's panHandlers): a JS-side protocol synthesized
|
|
475
|
-
// from raw touches, not Fabric ViewConfig events, so `isEventFor` never reports them. Treated as
|
|
476
|
-
// listeners on any node so the handlers attach instead of reaching Fabric as dead props.
|
|
477
|
-
const RESPONDER_EVENTS = new Set([
|
|
478
|
-
'startShouldSetResponder',
|
|
479
|
-
'startShouldSetResponderCapture',
|
|
480
|
-
'moveShouldSetResponder',
|
|
481
|
-
'moveShouldSetResponderCapture',
|
|
482
|
-
'responderGrant',
|
|
483
|
-
'responderReject',
|
|
484
|
-
'responderStart',
|
|
485
|
-
'responderMove',
|
|
486
|
-
'responderEnd',
|
|
487
|
-
'responderRelease',
|
|
488
|
-
'responderTerminate',
|
|
489
|
-
'responderTerminationRequest',
|
|
490
|
-
]);
|
|
491
|
-
// React's JSX dev transform annotates every element with __self (the component instance) and
|
|
492
|
-
// __source. React's own Fabric host config consumes both and never forwards them; a JSX-based
|
|
493
|
-
// adapter (Vue JSX, Solid JSX) instead carries them as ordinary props, reaching Fabric.
|
|
494
|
-
// Both platforms reject it, not just Android: iOS's `jsi::dynamicFromValue` keeps no visited set,
|
|
495
|
-
// and `__self` is a cyclic module `this` — an endless walk inside `applyOps` that allocates until
|
|
496
|
-
// RAM is exhausted, worse than Android's loud `folly::dynamic` rejection.
|
|
497
|
-
// SFC/template authoring never produces them. Strip them here, once, mirroring React's host config.
|
|
498
|
-
const REACT_JSX_DEV_PROPS = new Set([
|
|
499
|
-
'__self',
|
|
500
|
-
'__source',
|
|
501
|
-
]);
|
|
502
|
-
// All slots are present from the start rather than added as they are written: one hidden class
|
|
503
|
-
// for every styled node, instead of a shape transition per slot.
|
|
504
|
-
// Narrowed rather than cast: `routeProp` takes `unknown`, and a bare `typeof v === 'function'`
|
|
505
|
-
// leaves TS with `Function`, callable with anything. This states the shape the contract promises.
|
|
506
|
-
function isStyleCallback(value) {
|
|
507
|
-
return typeof value === 'function';
|
|
508
|
-
}
|
|
509
|
-
function stylePartsOf(node) {
|
|
510
|
-
return (node.styleParts ??= {
|
|
511
|
-
classStyle: undefined,
|
|
512
|
-
explicitStyle: undefined,
|
|
513
|
-
hiddenStyle: undefined,
|
|
514
|
-
className: undefined,
|
|
515
|
-
isPressed: false,
|
|
516
|
-
activeStyle: undefined,
|
|
517
|
-
activeStyleFromCallback: false,
|
|
518
|
-
published: undefined,
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
// The pressed variant is a complete REPLACEMENT, not an overlay: `resolveActiveClassName` resolves
|
|
522
|
-
// the element's tokens PLUS `:active` through the same matcher, so the result already contains
|
|
523
|
-
// everything the base class gave. No extra slot needed; the published array's shape stays put.
|
|
524
|
-
// Resolved LAZILY, at press time, never beside `classStyle` — eager would double the resolutions
|
|
525
|
-
// on every class WRITE to serve a state almost no node is ever in. A press is one event on one
|
|
526
|
-
// node, so the extra lookup there is invisible.
|
|
527
|
-
// `:active` applies only to a class reaching the engine as a STRING — true for nearly every
|
|
528
|
-
// producer: Vue/Angular/React all normalize `class` to a string before it reaches here, except
|
|
529
|
-
// Svelte's `normalizeSvelteClass`, the one live producer of a non-string class below.
|
|
530
|
-
// An OBJECT here is not a class map — `IClassNameValue` types it as an IResolvedStyle, the channel
|
|
531
|
-
// ScrollView/VirtualizedList/FlatList/ImageBackground use to hand a style through the class prop.
|
|
532
|
-
// Canonicalising it into tokens would silently break their styling — do not "simplify" it away.
|
|
533
|
-
// An ARRAY of plain strings: no adapter produces this today, and it reduces fresh every call, so
|
|
534
|
-
// it gets neither a pressed variant nor `isAlreadyPublished`.
|
|
535
|
-
// The registry memoises a class STRING to the same object; `isAlreadyPublished` compares slot 0
|
|
536
|
-
// with Object.is. A variant resolving fresh each call could never be turned away by that guard, so
|
|
537
|
-
// unpressed rows would republish and re-dirty forever — the storm the guard exists to stop.
|
|
538
|
-
// Slot 1's twin of `baseStyleOf`: the variant replaces slot 1, not slot 0, so it beats the class
|
|
539
|
-
// cascade the way the authored style does, while a `:active` rule can still win slot 0 underneath.
|
|
540
|
-
function explicitStyleOf(parts) {
|
|
541
|
-
return parts.isPressed && parts.activeStyle !== undefined
|
|
542
|
-
? parts.activeStyle
|
|
543
|
-
: parts.explicitStyle;
|
|
544
|
-
}
|
|
545
|
-
function baseStyleOf(parts) {
|
|
546
|
-
return parts.isPressed && typeof parts.className === 'string'
|
|
547
|
-
? resolveActiveClassName(parts.className)
|
|
548
|
-
: parts.classStyle;
|
|
549
|
-
}
|
|
550
|
-
// Republish the merged style after one half changed. Halves are written IN PLACE by the callers
|
|
551
|
-
// below — no patch object, no spread — because this is the hottest function in the mutation API.
|
|
552
|
-
// The fresh ARRAY allocation stays, DELIBERATELY: `setNativeProps` bypasses these parts, so an app
|
|
553
|
-
// handing over a hoisted style constant would get skipped by the Object.is guard and never
|
|
554
|
-
// restore the declarative style an animation overwrote. The re-push IS the restore path.
|
|
555
|
-
// Sound because `pushClassStyle` is the ONLY writer of `parts.published` — so a node that has
|
|
556
|
-
// published nothing holds `undefined`, and the first write can never be swallowed as "unchanged".
|
|
557
|
-
// What a node publishes when NOTHING resolves. Length 0 is the marker, needing no second field:
|
|
558
|
-
// `pushClassStyle` never otherwise publishes an empty array, and it's distinct from `undefined`
|
|
559
|
-
// ("nothing published yet", which must never be turned away).
|
|
560
|
-
const PUBLISHED_NOTHING = Object.freeze([]);
|
|
561
|
-
// ── ONE ARRAY PER DISTINCT PAIR, SHARED ACROSS NODES ─────────────────────────────────────────────
|
|
562
|
-
// `mutation-buffer.ts` interns values BY IDENTITY, so distinct-but-equal arrays across a list
|
|
563
|
-
// styled the same way cost distinct entries and separate JS -> `folly::dynamic` conversions.
|
|
564
|
-
// `WeakMap`, both levels, so nothing grows unbounded: a fresh style object per render gets a fresh
|
|
565
|
-
// cache entry that dies with the object, and correctly gets no sharing — two structurally equal
|
|
566
|
-
// objects are two values to whoever reads them.
|
|
567
|
-
// The three-slot (hidden) form is deliberately NOT cached: `display: 'none'` is rare by
|
|
568
|
-
// construction, so a third map would be paid for on every write to serve it.
|
|
569
|
-
const sharedPairByExplicit = new WeakMap();
|
|
570
|
-
const sharedPairByBase = new WeakMap();
|
|
571
|
-
// The published array for this pair — same object every time the same two parts are handed in.
|
|
572
|
-
// `undefined` when the pair can't be keyed (a primitive half, or the hidden form), and the caller
|
|
573
|
-
// then builds its own array; every reader compares slots by identity, never the array itself.
|
|
574
|
-
function sharedStylePair(base, explicit) {
|
|
575
|
-
const baseIsKeyable = typeof base === 'object' && base !== null;
|
|
576
|
-
const explicitIsKeyable = typeof explicit === 'object' && explicit !== null;
|
|
577
|
-
if (base === undefined && explicitIsKeyable) {
|
|
578
|
-
const cached = sharedPairByExplicit.get(explicit);
|
|
579
|
-
if (cached !== undefined)
|
|
580
|
-
return cached;
|
|
581
|
-
const made = [base, explicit];
|
|
582
|
-
sharedPairByExplicit.set(explicit, made);
|
|
583
|
-
return made;
|
|
584
|
-
}
|
|
585
|
-
if (!baseIsKeyable)
|
|
586
|
-
return undefined;
|
|
587
|
-
if (explicit === undefined) {
|
|
588
|
-
const cached = sharedPairByBase.get(base);
|
|
589
|
-
if (Array.isArray(cached))
|
|
590
|
-
return cached;
|
|
591
|
-
if (cached === undefined) {
|
|
592
|
-
const made = [base, explicit];
|
|
593
|
-
sharedPairByBase.set(base, made);
|
|
594
|
-
return made;
|
|
595
|
-
}
|
|
596
|
-
// A base that has already been seen WITH an explicit half holds the second-level map here, and
|
|
597
|
-
// the base-only array has nowhere to live beside it. Rare enough not to earn a third map.
|
|
598
|
-
return undefined;
|
|
599
|
-
}
|
|
600
|
-
if (!explicitIsKeyable)
|
|
601
|
-
return undefined;
|
|
602
|
-
const existing = sharedPairByBase.get(base);
|
|
603
|
-
const byExplicit = existing instanceof WeakMap ? existing : new WeakMap();
|
|
604
|
-
if (existing === undefined)
|
|
605
|
-
sharedPairByBase.set(base, byExplicit);
|
|
606
|
-
// Same clash as above, the other way round: this base is holding its base-only array. Leave it.
|
|
607
|
-
if (Array.isArray(existing))
|
|
608
|
-
return undefined;
|
|
609
|
-
const cached = byExplicit.get(explicit);
|
|
610
|
-
if (cached !== undefined)
|
|
611
|
-
return cached;
|
|
612
|
-
const made = [base, explicit];
|
|
613
|
-
byExplicit.set(explicit, made);
|
|
614
|
-
return made;
|
|
615
|
-
}
|
|
616
|
-
// A slot that contributes no keys to the payload: absent, or the registry's shared "this class
|
|
617
|
-
// styles nothing" object. An IDENTITY compare, not a key count — `Object.keys(x).length` allocates
|
|
618
|
-
// an array, and this runs on every class and style write.
|
|
619
|
-
// A plain style bag — not an array of styles, not a callback, not null.
|
|
620
|
-
function isStyleRecord(value) {
|
|
621
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
622
|
-
}
|
|
623
|
-
// Is this rebuilt style the same style, key for key? A component body writing its style inline
|
|
624
|
-
// hands over a fresh object every render, equal to the one already standing, which Object.is
|
|
625
|
-
// cannot see — without this the write crosses into the host and is only found unchanged there.
|
|
626
|
-
// Shallow and conservative, deliberately: a nested value (transform list, shadow, style array)
|
|
627
|
-
// reports "not the same" rather than being deep-compared, so being wrong here is slow, never
|
|
628
|
-
// incorrect — the host's own diffProps still refuses those exactly as before.
|
|
629
|
-
// `undefined` on either side also reports "not the same", which lets the key COUNT stand in for a
|
|
630
|
-
// key-set comparison: equal counts plus every key of `next` matching a defined value in `standing`
|
|
631
|
-
// cannot leave a key unaccounted for.
|
|
632
|
-
export function isSameShallowStyle(next, standing) {
|
|
633
|
-
// THE SAME OBJECT IS THE SAME STYLE, checked first: without it, a re-push of a hoisted constant
|
|
634
|
-
// (what Solid does on every signal change, having no diff) allocates two key arrays and walks
|
|
635
|
-
// them to reach the same answer the identity check gives for free.
|
|
636
|
-
// Changes nothing OBSERVABLE — break-tested, not assumed: inverting this line (identical object
|
|
637
|
-
// reporting "changed") leaves the full test suite green, because `pushClassStyle`'s own
|
|
638
|
-
// `isAlreadyPublished` catches the republish downstream via `sharedStylePair`'s memoization.
|
|
639
|
-
if (next === standing)
|
|
640
|
-
return isStyleRecord(next);
|
|
641
|
-
if (!isStyleRecord(next) || !isStyleRecord(standing))
|
|
642
|
-
return false;
|
|
643
|
-
const keys = Object.keys(next);
|
|
644
|
-
if (keys.length !== Object.keys(standing).length)
|
|
645
|
-
return false;
|
|
646
|
-
for (const key of keys) {
|
|
647
|
-
const value = next[key];
|
|
648
|
-
if (value === undefined || isStyleRecord(value) || Array.isArray(value)) {
|
|
649
|
-
return false;
|
|
650
|
-
}
|
|
651
|
-
if (!Object.is(value, standing[key]))
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
return true;
|
|
655
|
-
}
|
|
656
|
-
function contributesNothing(slot) {
|
|
657
|
-
return slot === undefined || slot === EMPTY_STYLE;
|
|
658
|
-
}
|
|
659
|
-
// Does this node have a style at all? Read through the same two resolvers as the publication, for
|
|
660
|
-
// the reason the guard below states: guard and publication disagreeing is a silent wrong screen.
|
|
661
|
-
function hasNothingToPublish(parts) {
|
|
662
|
-
return (parts.hiddenStyle === undefined &&
|
|
663
|
-
contributesNothing(baseStyleOf(parts)) &&
|
|
664
|
-
contributesNothing(explicitStyleOf(parts)));
|
|
665
|
-
}
|
|
666
|
-
function isAlreadyPublished(parts) {
|
|
667
|
-
const published = parts.published;
|
|
668
|
-
if (published === undefined)
|
|
669
|
-
return false;
|
|
670
|
-
// The delete is already standing. Asked before the slot comparisons because an empty array would
|
|
671
|
-
// otherwise pass both of them on `undefined` and then fail the length check, republishing a
|
|
672
|
-
// delete the host already performed.
|
|
673
|
-
if (published.length === 0)
|
|
674
|
-
return hasNothingToPublish(parts);
|
|
675
|
-
// `baseStyleOf`, not `parts.classStyle` — the guard and the publication must read slot 0 the
|
|
676
|
-
// same way or a press is turned away as already-published and silently does nothing on device
|
|
677
|
-
// while the behavior fires correctly and nothing goes red.
|
|
678
|
-
if (!Object.is(published[0], baseStyleOf(parts)))
|
|
679
|
-
return false;
|
|
680
|
-
// Through the resolver for the same reason as slot 0 above: guard and publication must agree, or
|
|
681
|
-
// a press is turned away as already-published and does nothing on device with nothing red.
|
|
682
|
-
if (!Object.is(published[1], explicitStyleOf(parts)))
|
|
683
|
-
return false;
|
|
684
|
-
return parts.hiddenStyle === undefined
|
|
685
|
-
? published.length === 2
|
|
686
|
-
: published.length === 3 && Object.is(published[2], parts.hiddenStyle);
|
|
687
|
-
}
|
|
688
|
-
function pushClassStyle(node, parts) {
|
|
689
|
-
// An unchanged class still reaches this write — Solid has no diff, so a list-wide signal
|
|
690
|
-
// re-pushes every row's class and dirties the whole tree. Guard keys off `published`, which
|
|
691
|
-
// setNativeProps clears, so an imperative restore still re-publishes correctly.
|
|
692
|
-
if (isAlreadyPublished(parts))
|
|
693
|
-
return;
|
|
694
|
-
// Emits NO_VALUE rather than `[undefined, undefined]` — the host skips a real array with one
|
|
695
|
-
// pointer check instead of building and diffing a `folly::dynamic`. Same published-marker guard
|
|
696
|
-
// as above keeps the restore path working after setNativeProps clears it.
|
|
697
|
-
if (hasNothingToPublish(parts)) {
|
|
698
|
-
parts.published = PUBLISHED_NOTHING;
|
|
699
|
-
setProp(node, 'style', undefined);
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
// Third slot only appended while hidden — a permanent 3-element array would add an allocation
|
|
703
|
-
// to every style write for a state most nodes never enter.
|
|
704
|
-
const base = baseStyleOf(parts);
|
|
705
|
-
const explicit = explicitStyleOf(parts);
|
|
706
|
-
const published = parts.hiddenStyle === undefined
|
|
707
|
-
? (sharedStylePair(base, explicit) ?? [base, explicit])
|
|
708
|
-
: [base, explicit, parts.hiddenStyle];
|
|
709
|
-
parts.published = published;
|
|
710
|
-
setProp(node, 'style', published);
|
|
711
|
-
}
|
|
712
|
-
// `display: 'none'` is a real RN style value (Yoga's DisplayNone), so a hidden node keeps its
|
|
713
|
-
// place in the tree, its state and its children — it just stops laying out and painting.
|
|
714
|
-
const HIDDEN_STYLE = { display: 'none' };
|
|
715
|
-
// Stop a node painting without unmounting it, or let it paint again — the seam React's
|
|
716
|
-
// Activity/Suspense reach for through hideInstance/unhideInstance. Lives in the engine, not an
|
|
717
|
-
// adapter, since restoring the author's style byte belongs to whoever owns the style merge.
|
|
718
|
-
export function setNodeHidden(node, hidden) {
|
|
719
|
-
const parts = stylePartsOf(node);
|
|
720
|
-
parts.hiddenStyle = hidden ? HIDDEN_STYLE : undefined;
|
|
721
|
-
pushClassStyle(node, parts);
|
|
722
|
-
}
|
|
723
|
-
// Put a node into (or out of) its pressed state, so `:active` rules apply. The engine-owned half:
|
|
724
|
-
// press state resolves below the framework, which is what lets a pressable stay an intrinsic tag
|
|
725
|
-
// instead of a component — a component is forced only when the template must read the state.
|
|
726
|
-
// Costs nothing when no `:active` rule is registered: resolveActiveClassName hands back the same
|
|
727
|
-
// object the unpressed path returns, so isAlreadyPublished turns the re-push away.
|
|
728
|
-
export function setNodePressed(node, pressed) {
|
|
729
|
-
const parts = stylePartsOf(node);
|
|
730
|
-
parts.isPressed = pressed;
|
|
731
|
-
pushClassStyle(node, parts);
|
|
732
|
-
}
|
|
733
|
-
// Tell the host a behavior's FEEDBACK is showing — TouchableHighlight's underlay, and only that.
|
|
734
|
-
// Deliberately not the same bit as setNodePressed: this drives a rule living in C++
|
|
735
|
-
// (foldTouchableHighlightUnderlay), crossing as one op, since `shown` lags `pressed` by a timer.
|
|
736
|
-
// No style computed here at all: the two props the rule reads (underlayColor, activeOpacity) are
|
|
737
|
-
// ones the engine already strips from the payload, so their defaults live in one place.
|
|
738
|
-
export function setNodeUnderlayShown(node, shown) {
|
|
739
|
-
recordSetUnderlayShown(node, shown);
|
|
740
|
-
}
|
|
741
|
-
// Forget what was last published, so the next pushClassStyle cannot be turned away. The one
|
|
742
|
-
// caller is setNativeProps, which writes the style slot past this file. A no-op for a node nobody
|
|
743
|
-
// has styled — that's why this isn't `stylePartsOf(node).published = undefined`.
|
|
744
|
-
export function clearPublishedStyle(node) {
|
|
745
|
-
if (node.styleParts !== undefined)
|
|
746
|
-
node.styleParts.published = undefined;
|
|
747
|
-
}
|
|
748
|
-
// The explicit (non-class) style half — an adapter that builds style key-by-key (Angular's
|
|
749
|
-
// ɵɵstyleProp/setStyle) merges onto this, not node.props.style directly, which may hold the
|
|
750
|
-
// [classStyle, explicitStyle] pair pushClassStyle publishes.
|
|
751
|
-
export function getExplicitStyle(node) {
|
|
752
|
-
return node.styleParts?.explicitStyle;
|
|
753
|
-
}
|
|
754
|
-
// The `[classStyle, explicitStyle]` pair the node currently publishes, same order pushClassStyle
|
|
755
|
-
// writes, so flattenStyle collapses it the way Fabric will.
|
|
756
|
-
// For a caller that wants the merged answer without a host: the pair reaches the payload as an op,
|
|
757
|
-
// and only a host holds ops — core/css-parser reads it here instead of reaching into styleParts.
|
|
758
|
-
export function getPublishedStyle(node) {
|
|
759
|
-
const parts = node.styleParts;
|
|
760
|
-
if (parts === undefined)
|
|
761
|
-
return [];
|
|
762
|
-
return [parts.classStyle, parts.explicitStyle];
|
|
763
|
-
}
|
|
764
|
-
const CLASS_PROP_KEYS = new Set(['class', 'className']);
|
|
765
|
-
// Flat-bag split (React/Vue/Solid): `onX` becomes a listener only when the component's ViewConfig
|
|
766
|
-
// declares `x` as an event — otherwise it's a plain prop, so `onTintColor` on a Switch (whose only
|
|
767
|
-
// event is `change`) routes to setProp and reaches Fabric untouched.
|
|
768
|
-
// `id` is RN's alias for `nativeID` and wins when both are set (View.js: `nativeID = id`). No
|
|
769
|
-
// ViewConfig declares raw `id`, so Fabric drops it — a half-working rename loses nativeID with
|
|
770
|
-
// nothing red anywhere.
|
|
771
|
-
const ID_ALIAS_FROM = 'id';
|
|
772
|
-
const ID_ALIAS_TO = 'nativeID';
|
|
773
|
-
// The ONE place this rename happens: HERE because every adapter's prop write ends at routeProp,
|
|
774
|
-
// whatever shape it starts in — a bag fold can't serve the per-key renderers and a per-key fold
|
|
775
|
-
// can't serve the bag ones, but the seam they share can serve both.
|
|
776
|
-
// Precedence needs state because upstream decides `id ?? nativeID` in one expression; a per-key
|
|
777
|
-
// writer never sees both, so unmemoized precedence would fall out of write order instead. The
|
|
778
|
-
// authored nativeID is remembered, so clearing `id` hands the slot back rather than latching.
|
|
779
|
-
// Precedence is per-component: View.js's `id ?? nativeID` is the default, but
|
|
780
|
-
// TouchableWithoutFeedback's clone unconditionally overwrites nativeID with the authored value
|
|
781
|
-
// when set. `node.nativeIdWinsOverId`, set for the one behavior that declares it, flips the winner.
|
|
782
|
-
const idAliased = new WeakMap();
|
|
783
|
-
function routeIdAlias(node, key, value) {
|
|
784
|
-
const state = idAliased.get(node) ?? {
|
|
785
|
-
idValue: undefined,
|
|
786
|
-
nativeIdValue: undefined,
|
|
787
|
-
};
|
|
788
|
-
if (key === ID_ALIAS_FROM)
|
|
789
|
-
state.idValue = value;
|
|
790
|
-
else
|
|
791
|
-
state.nativeIdValue = value;
|
|
792
|
-
idAliased.set(node, state);
|
|
793
|
-
const published = node.nativeIdWinsOverId
|
|
794
|
-
? (state.nativeIdValue ?? state.idValue)
|
|
795
|
-
: (state.idValue ?? state.nativeIdValue);
|
|
796
|
-
setProp(node, ID_ALIAS_TO, published);
|
|
797
|
-
}
|
|
798
|
-
export function routeProp(node, key, value) {
|
|
799
|
-
if (REACT_JSX_DEV_PROPS.has(key))
|
|
800
|
-
return;
|
|
801
|
-
// Prop twin of the child redirect in `appendChild` — a composed primitive's owner receives props
|
|
802
|
-
// that belong to its internal slot (`contentContainerStyle` on ScrollView styles the content
|
|
803
|
-
// view), same reason the owner is named for a child: that's where the app wrote it.
|
|
804
|
-
// Gated on the field, so a node with no slot pays one load+branch and never touches the registry.
|
|
805
|
-
// The redirect recurses into the slot's own routeProp, single-hop by construction — a slot has no
|
|
806
|
-
// slot of its own (`childHost` is documented single-hop).
|
|
807
|
-
if (node.childHost !== undefined) {
|
|
808
|
-
const slotKey = slotPropNameFor(node, key);
|
|
809
|
-
if (slotKey !== undefined) {
|
|
810
|
-
// A class name is a legal spelling of `contentContainerStyle`, so a string must land on the
|
|
811
|
-
// slot as `class`, not `style` — renamed verbatim it would publish a style holding a string,
|
|
812
|
-
// dropped with nothing red. React's wrapper resolves the name itself; this only matters here.
|
|
813
|
-
const slotValueFor = node.hostBehavior?.slotValueFor;
|
|
814
|
-
routeProp(node.childHost, slotKey === 'style' && typeof value === 'string' ? 'class' : slotKey, slotValueFor === undefined ? value : slotValueFor(slotKey, value));
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
// After the slot redirect on purpose: a composed primitive forwards most of its bag to an
|
|
819
|
-
// internal node (ImageBackground spreads everything but `style` onto its image), so an `id` on
|
|
820
|
-
// the owner belongs there. Resolved earlier, nativeID would land on the wrapper unseen.
|
|
821
|
-
if (key === ID_ALIAS_FROM || key === ID_ALIAS_TO) {
|
|
822
|
-
routeIdAlias(node, key, value);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
// An AnimatedNode in a prop (`style={{opacity: value}}`) resolves here to the value to publish,
|
|
826
|
-
// the engine holding the subscription. Returns its input by identity when nothing is animated,
|
|
827
|
-
// so every branch below is unchanged (animated/host-binding.ts).
|
|
828
|
-
// After the slot redirect, so an animated `contentContainerStyle` binds on the node that
|
|
829
|
-
// actually carries the style.
|
|
830
|
-
const resolved = hasAnimatedNodes()
|
|
831
|
-
? bindAnimatedValue(node, key, value)
|
|
832
|
-
: value;
|
|
833
|
-
if (CLASS_PROP_KEYS.has(key)) {
|
|
834
|
-
const parts = stylePartsOf(node);
|
|
835
|
-
// Canonicalised HERE so the stored value is what everything downstream keys on: an all-string
|
|
836
|
-
// array becomes one string, and then the pressed variant and isAlreadyPublished work on it
|
|
837
|
-
// exactly as on an authored string. One `typeof` for the common case.
|
|
838
|
-
parts.className = canonicalClassName(isClassNameValue(resolved) ? resolved : undefined);
|
|
839
|
-
parts.classStyle = resolveClassName(parts.className);
|
|
840
|
-
pushClassStyle(node, parts);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (key === 'style') {
|
|
844
|
-
const parts = stylePartsOf(node);
|
|
845
|
-
// A function `style` (`style={({pressed}) => …}`) arrives here intact and is resolved at both
|
|
846
|
-
// states — writing `style` + `activeStyle` as an explicit pair, cheaper by one call per
|
|
847
|
-
// recompute than the app doing it by hand.
|
|
848
|
-
// Without this the failure is silent: a function isn't an `on*` name, misses setEventListener,
|
|
849
|
-
// lands in setProp as a function value, and fabricProps drops function props — the node commits
|
|
850
|
-
// with no style at all.
|
|
851
|
-
// The callback must be pure in `pressed` — read once per state, here and under every
|
|
852
|
-
// transform's emission (core/components/src/state-style.ts carries the same contract).
|
|
853
|
-
if (isStyleCallback(resolved)) {
|
|
854
|
-
parts.explicitStyle = resolved({ pressed: false });
|
|
855
|
-
parts.activeStyle = resolved({ pressed: true });
|
|
856
|
-
parts.activeStyleFromCallback = true;
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
// A rebuilt literal equal to what is standing is not a change — see `isSameShallowStyle`.
|
|
860
|
-
// Gated on something being published, which keeps the restore path intact: a setNativeProps
|
|
861
|
-
// write clears `parts.published`, and after that this must never turn a write away — the
|
|
862
|
-
// re-push IS the restore, same mechanism isAlreadyPublished relies on.
|
|
863
|
-
// Gated on the previous write not coming from a callback, since that one owns
|
|
864
|
-
// `parts.activeStyle` and this branch must clear it — returning early would leave the old
|
|
865
|
-
// pressed look standing under a plain style.
|
|
866
|
-
if (parts.published !== undefined &&
|
|
867
|
-
!parts.activeStyleFromCallback &&
|
|
868
|
-
isSameShallowStyle(resolved, parts.explicitStyle)) {
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
parts.explicitStyle = resolved;
|
|
872
|
-
// Only a variant WE derived is stale now. `style` switching from a callback to a plain value
|
|
873
|
-
// must not leave the old pressed look standing, and an AUTHORED `activeStyle` must survive a
|
|
874
|
-
// `style` write, because the two arrive as independent props in an unspecified order.
|
|
875
|
-
if (parts.activeStyleFromCallback) {
|
|
876
|
-
parts.activeStyle = undefined;
|
|
877
|
-
parts.activeStyleFromCallback = false;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
pushClassStyle(node, parts);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
// Ours, never Fabric's — it is consumed here and must not reach the payload, or every pressable
|
|
884
|
-
// in the app carries an unknown key to native.
|
|
885
|
-
if (key === 'activeStyle') {
|
|
886
|
-
const parts = stylePartsOf(node);
|
|
887
|
-
parts.activeStyle = resolved;
|
|
888
|
-
// Slot 1 is no longer ours — whatever a callback derived has just been replaced. Without this
|
|
889
|
-
// the flag outlives its value: this branch overwrites the slot silently and a later plain
|
|
890
|
-
// `style` clears a variant the engine never derived, a sequence a flat-bag adapter can deliver.
|
|
891
|
-
parts.activeStyleFromCallback = false;
|
|
892
|
-
pushClassStyle(node, parts);
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
// RN's snapshot affordance (Pressable.js seeds usePressState with it): render the control
|
|
896
|
-
// pressed with no gesture. Selects activeStyle and any `:active` class — exactly what isPressed
|
|
897
|
-
// already decides, so it belongs beside activeStyle rather than in a behavior.
|
|
898
|
-
// Here rather than in attachAfterCommit: a behavior hook reading this prop costs a post-commit
|
|
899
|
-
// crossing per pressable node (budgeted in crossing-and-payload-census.probe.test.tsx). This
|
|
900
|
-
// branch is one string compare on the first commit — no crossing at all.
|
|
901
|
-
// TouchableHighlight's half of the same prop paints an underlay, so it's a separate rule in
|
|
902
|
-
// SymbioteFabricProps.cpp — a side effect AND a passthrough. Returning early here left that rule
|
|
903
|
-
// blind; keeping it out of the payload is kPressableMachineKeys's job, done separately.
|
|
904
|
-
// Same shape GATED_EVENT_PROPS uses above: act, then let the write continue.
|
|
905
|
-
if (key === 'testOnly_pressed')
|
|
906
|
-
setNodePressed(node, resolved === true);
|
|
907
|
-
if (isOnEventName(key)) {
|
|
908
|
-
// A native-driven `Animated.event` needs the native module as well as the listener map, and
|
|
909
|
-
// registers under the PROP name — see `bindAnimatedEvent`, which no-ops for anything else.
|
|
910
|
-
if (hasAnimatedNodes())
|
|
911
|
-
bindAnimatedEvent(node, key, resolved);
|
|
912
|
-
const name = listenerName(key);
|
|
913
|
-
const isRegisteredEvent = RESPONDER_EVENTS.has(name) || isEventFor(node.component, name);
|
|
914
|
-
// RNS* views derive events from react-native-screens' own codegen ViewConfig, so an
|
|
915
|
-
// unregistered event falls through to setProp as a dead prop Fabric ignores — indistinguishable
|
|
916
|
-
// from "the button did nothing" at the UI. Scoped to RNS* to avoid noise; gated behind DEBUG.
|
|
917
|
-
if (node.component.startsWith('RNS')) {
|
|
918
|
-
dlog(`routeProp: ${node.component} "${key}" -> listener "${name}" ` +
|
|
919
|
-
`registered=${isRegisteredEvent} at t=${Date.now()}`);
|
|
920
|
-
}
|
|
921
|
-
if (isRegisteredEvent) {
|
|
922
|
-
setEventListener(node, name, resolved);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
setProp(node, key, resolved);
|
|
927
|
-
}
|
|
928
|
-
// Counted in propStats — a text write IS a prop write, reaching Fabric as RCTRawText's only prop.
|
|
929
|
-
// Unguarded, same reason setProp is: comparing against the standing text means reading it back
|
|
930
|
-
// from the host, which holds it locally and dedupes there — including empty-string child-list
|
|
931
|
-
// transitions and reparenting under `<Text>` (RCTVirtualText vs RCTText).
|
|
932
|
-
export function setText(node, text) {
|
|
933
|
-
propStats.writes += 1;
|
|
934
|
-
recordSetText(node, text);
|
|
935
|
-
}
|
|
936
|
-
// Structural ops: each is one op and nothing else — the host detaches a child from whatever
|
|
937
|
-
// parent it currently has before linking it, true even when an adapter names a stale one (a MOVE
|
|
938
|
-
// spells as remove-then-insert). JS doesn't track the old parent and doesn't need to.
|
|
939
|
-
// What JS still decides is which node an op names — two redirects, a composed primitive's slot and
|
|
940
|
-
// a wrap claim, both read off a field so a plain node pays one load and one branch per op.
|
|
941
|
-
// The host's raw answer, surface INCLUDED — unlike `parentOf` (host-access.ts), which reports a
|
|
942
|
-
// top-level node as parentless by design. The two swaps below have to NAME the holder in an op, and
|
|
943
|
-
// for a wrapped node sitting directly under a surface that holder is the surface node.
|
|
944
|
-
function holderOf(node) {
|
|
945
|
-
flushOps();
|
|
946
|
-
const parent = treeHost()?.parentOf(node);
|
|
947
|
-
return isSymbioteNode(parent) ? parent : undefined;
|
|
948
|
-
}
|
|
949
|
-
// Which node a child actually lands on. See `ISymbioteNode.childHost`: the adapter always names the
|
|
950
|
-
// OWNER, and a node whose behavior built an internal subtree redirects the app's children into it —
|
|
951
|
-
// unless the behavior CLAIMS this particular child, which keeps it on the owner (`claimedChildren`).
|
|
952
|
-
// SINGLE HOP, not a loop, and the field's own comment says why — a chain would put a walk on the
|
|
953
|
-
// engine's hottest path to express a depth no primitive has. A behavior needing depth points
|
|
954
|
-
// `childHost` at the innermost node itself.
|
|
955
|
-
// Reads a field undefined on every node with no composed primitive — one load, one branch.
|
|
956
|
-
// Deliberately not behind hasHostBehaviors() (a second read for nothing); the claim check sits
|
|
957
|
-
// behind that branch, so only a slot-bearing node pays the registry probe.
|
|
958
|
-
function hostFor(parent, child) {
|
|
959
|
-
const slot = parent.childHost;
|
|
960
|
-
if (slot === undefined)
|
|
961
|
-
return parent;
|
|
962
|
-
// A slot that is a built SIBLING rather than a container — ImageBackground's absolutely-filled
|
|
963
|
-
// image — keeps the app's children on the owner. See `IHostBehavior.slotTakesNoChildren`.
|
|
964
|
-
if (!slotTakesChildren(parent))
|
|
965
|
-
return parent;
|
|
966
|
-
return claimModeFor(parent, child.component) === undefined ? slot : parent;
|
|
967
|
-
}
|
|
968
|
-
// The node a child must be inserted before, or `undefined` for an ordinary append.
|
|
969
|
-
// A host that still has a slot is an owner taking a claimed child, which goes before the slot
|
|
970
|
-
// whatever the framework asked — RN renders `{refreshControl}{content}` in that order, and
|
|
971
|
-
// `beforeChild` lives inside the slot anyway.
|
|
972
|
-
// A sibling slot is the opposite: RN paints the background image first and children over it
|
|
973
|
-
// (ImageBackground.js), so they append past it rather than in front — what `undefined` leaves
|
|
974
|
-
// alone.
|
|
975
|
-
function slotAnchorOf(host) {
|
|
976
|
-
const slot = host.childHost;
|
|
977
|
-
if (slot === undefined || !slotTakesChildren(host))
|
|
978
|
-
return undefined;
|
|
979
|
-
return slot;
|
|
980
|
-
}
|
|
981
|
-
// ── the two structural recorders, and why nothing here calls the raw ones ───────────────────────
|
|
982
|
-
// `mayHaveChildren` is only sound if every op that gives a node a child raises it — raised here
|
|
983
|
-
// rather than at each of the five call sites, since a forgotten one would make childrenOf answer
|
|
984
|
-
// "empty" for a node that has children: a wrong answer, not a slow one.
|
|
985
|
-
// Arm a parent's recurring post-commit hook for a STRUCTURAL change, not just a prop write: the
|
|
986
|
-
// ScrollView sticky-header machine drops a wrapper when the framework takes the wrapped child
|
|
987
|
-
// away, writing no prop on the wrapper's owner at all — narrowing the beat to props left it stuck.
|
|
988
|
-
function armCommitHookForChildChange(parent) {
|
|
989
|
-
if (parent.hasCommitHook)
|
|
990
|
-
noteCommitHookNodeChanged(parent);
|
|
991
|
-
}
|
|
992
|
-
function recordAppendInto(parent, child) {
|
|
993
|
-
parent.mayHaveChildren = true;
|
|
994
|
-
armCommitHookForChildChange(parent);
|
|
995
|
-
recordAppendChild(parent, child);
|
|
996
|
-
}
|
|
997
|
-
function recordInsertInto(parent, child, beforeChild) {
|
|
998
|
-
parent.mayHaveChildren = true;
|
|
999
|
-
armCommitHookForChildChange(parent);
|
|
1000
|
-
recordInsertBefore(parent, child, beforeChild);
|
|
1001
|
-
}
|
|
1002
|
-
// What actually occupies this node's place in its parent's child list. See `ISymbioteNode.wrapper`:
|
|
1003
|
-
// a wrapped owner is what the adapter names and the wrapper is what the tree holds, so every
|
|
1004
|
-
// structural op takes the owner and moves the wrapper.
|
|
1005
|
-
function placedNode(node) {
|
|
1006
|
-
return node.wrapper ?? node;
|
|
1007
|
-
}
|
|
1008
|
-
// Make `child` the owner's parent, in place. Returns false when this is not a wrap claim, so the
|
|
1009
|
-
// two inserts fall through to the ordinary path on one call.
|
|
1010
|
-
// The owner being unattached is the normal case: every adapter fills a node's children before
|
|
1011
|
-
// appending it to its own parent, so the wrap usually happens with no holder yet and only the
|
|
1012
|
-
// second op runs — the later appendChild(root, owner) inserts the wrapper via placedNode.
|
|
1013
|
-
function wrapsOwner(owner, child) {
|
|
1014
|
-
if (owner.childHost === undefined)
|
|
1015
|
-
return false;
|
|
1016
|
-
if (claimModeFor(owner, child.component) !== 'wrap')
|
|
1017
|
-
return false;
|
|
1018
|
-
if (hasHostBehaviors())
|
|
1019
|
-
reattachHostBehaviors(child);
|
|
1020
|
-
if (hasAnimatedBindings())
|
|
1021
|
-
reattachAnimatedProps(child);
|
|
1022
|
-
const holder = holderOf(owner);
|
|
1023
|
-
// Wrapper takes the owner's place first, then the owner moves under it — the host's own detach
|
|
1024
|
-
// on link is what unlinks the owner from `holder`, so no removal op is needed.
|
|
1025
|
-
if (holder !== undefined)
|
|
1026
|
-
recordInsertInto(holder, child, owner);
|
|
1027
|
-
owner.wrapper = child;
|
|
1028
|
-
recordAppendInto(child, owner);
|
|
1029
|
-
return true;
|
|
1030
|
-
}
|
|
1031
|
-
// Put the owner back where its wrapper stood — the mirror of `wrapsOwner`. It must leave the owner
|
|
1032
|
-
// ATTACHED: the framework is removing the RefreshControl, not the ScrollView.
|
|
1033
|
-
function unwrapsOwner(owner, child) {
|
|
1034
|
-
if (owner.wrapper !== child)
|
|
1035
|
-
return false;
|
|
1036
|
-
owner.wrapper = undefined;
|
|
1037
|
-
const holder = holderOf(child);
|
|
1038
|
-
if (holder === undefined) {
|
|
1039
|
-
// The wrapper never reached a parent, so there is no place to take back — the owner simply
|
|
1040
|
-
// stops hanging off it.
|
|
1041
|
-
recordRemoveChild(child, owner);
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
recordInsertInto(holder, owner, child);
|
|
1045
|
-
recordRemoveChild(holder, child);
|
|
1046
|
-
}
|
|
1047
|
-
return true;
|
|
1048
|
-
}
|
|
1049
|
-
export function appendChild(requestedParent, child) {
|
|
1050
|
-
if (wrapsOwner(requestedParent, child))
|
|
1051
|
-
return;
|
|
1052
|
-
const parent = hostFor(requestedParent, child);
|
|
1053
|
-
// A node the sweep tore down can be put back — Svelte parks live subtrees offscreen across
|
|
1054
|
-
// commits. A WeakSet miss for anything freshly built, so the create path pays nothing.
|
|
1055
|
-
if (hasHostBehaviors())
|
|
1056
|
-
reattachHostBehaviors(child);
|
|
1057
|
-
if (hasAnimatedBindings())
|
|
1058
|
-
reattachAnimatedProps(child);
|
|
1059
|
-
const placed = placedNode(child);
|
|
1060
|
-
const anchor = slotAnchorOf(parent);
|
|
1061
|
-
if (anchor === undefined)
|
|
1062
|
-
recordAppendInto(parent, placed);
|
|
1063
|
-
else
|
|
1064
|
-
recordInsertInto(parent, placed, anchor);
|
|
1065
|
-
if (hasHostBehaviors())
|
|
1066
|
-
notifyChildInserted(parent, placed);
|
|
1067
|
-
}
|
|
1068
|
-
// No anchor means append, and it's a real case, not a defensive guard: solid-js/universal spells
|
|
1069
|
-
// "insert at end" as `insertNode(parent, node, null)`, and Vue passes `anchor` through as `null`.
|
|
1070
|
-
// A slot has to name a node, so an unanchored insert IS an append and is recorded as one.
|
|
1071
|
-
export function insertBefore(requestedParent, child, beforeChild) {
|
|
1072
|
-
if (wrapsOwner(requestedParent, child))
|
|
1073
|
-
return;
|
|
1074
|
-
const parent = hostFor(requestedParent, child);
|
|
1075
|
-
if (hasHostBehaviors())
|
|
1076
|
-
reattachHostBehaviors(child);
|
|
1077
|
-
if (hasAnimatedBindings())
|
|
1078
|
-
reattachAnimatedProps(child);
|
|
1079
|
-
const placed = placedNode(child);
|
|
1080
|
-
const anchor = slotAnchorOf(parent) ??
|
|
1081
|
-
(beforeChild === null || beforeChild === undefined
|
|
1082
|
-
? undefined
|
|
1083
|
-
: placedNode(beforeChild));
|
|
1084
|
-
if (anchor === undefined)
|
|
1085
|
-
recordAppendInto(parent, placed);
|
|
1086
|
-
else
|
|
1087
|
-
recordInsertInto(parent, placed, anchor);
|
|
1088
|
-
if (hasHostBehaviors())
|
|
1089
|
-
notifyChildInserted(parent, placed);
|
|
1090
|
-
}
|
|
1091
|
-
// Removal only NOMINATES a behavior for teardown; the commit sweep decides. A framework may spell
|
|
1092
|
-
// a move as remove-then-reinsert (Solid does), so tearing down here kills the machine of a node
|
|
1093
|
-
// that comes back alive in the same batch — see host-behavior.ts's markDetachCandidate.
|
|
1094
|
-
export function removeChild(requestedParent, child) {
|
|
1095
|
-
// A wrap claim leaving: the owner takes its own place back and stays in the tree. Nominated for
|
|
1096
|
-
// teardown like any other removed node, because the wrapper IS leaving.
|
|
1097
|
-
if (unwrapsOwner(requestedParent, child)) {
|
|
1098
|
-
if (hasAttachedBehaviors() || hasAnimatedBindings())
|
|
1099
|
-
markDetachCandidate(child);
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
// A slot that IS the child being removed stops being one. Only a behavior that adopts an app
|
|
1103
|
-
// child as its slot reaches this; without the clear, hostFor below redirects the removal into
|
|
1104
|
-
// the node being removed, and the next child appended nests inside the orphan.
|
|
1105
|
-
if (requestedParent.childHost === child)
|
|
1106
|
-
requestedParent.childHost = undefined;
|
|
1107
|
-
// Redirected for the same reason the two inserts are: the adapter removes from the node it
|
|
1108
|
-
// appended to, which is the OWNER, while the child actually lives in the slot.
|
|
1109
|
-
const parent = hostFor(requestedParent, child);
|
|
1110
|
-
// hasAttachedBehaviors, not hasHostBehaviors: the latter is on from module load in every app just
|
|
1111
|
-
// from registering Pressable as a type. Nominating a candidate crosses every removed node into JS
|
|
1112
|
-
// on the commit sweep, which can't matter before a behavior has actually attached to anything.
|
|
1113
|
-
if (hasAttachedBehaviors() || hasAnimatedBindings())
|
|
1114
|
-
markDetachCandidate(child);
|
|
1115
|
-
// BOTH, and the owner is the one that matters: a composed primitive's behavior lives on the node
|
|
1116
|
-
// the adapter named, while `hostFor` redirects the mutation into its internal slot. Arming only
|
|
1117
|
-
// the slot arms a node that has no behavior at all.
|
|
1118
|
-
armCommitHookForChildChange(requestedParent);
|
|
1119
|
-
armCommitHookForChildChange(parent);
|
|
1120
|
-
recordRemoveChild(parent, placedNode(child));
|
|
1121
|
-
}
|
|
1122
|
-
// A structural census of the tree the HOST holds — see ITreeCensus (tree-host.ts). Walks nothing
|
|
1123
|
-
// here: the walk needs props.text and a child list, which JS has neither. `undefined` from
|
|
1124
|
-
// treeHost() means nothing installed, so the empty census can't be mistaken for a real zero.
|
|
1125
|
-
export function censusRetainedTree(roots) {
|
|
1126
|
-
flushOps();
|
|
1127
|
-
return treeHost()?.census(roots) ?? EMPTY_CENSUS;
|
|
1128
|
-
}
|
|
1
|
+
// The mutation API, as a barrel so `from './node'` keeps naming the whole of it. Adapters call it
|
|
2
|
+
// and every call appends an OPCODE to `mutation-buffer.ts`, т.к. the tree is the HOST's
|
|
3
|
+
export { ANCHOR_COMPONENT, RAW_TEXT_COMPONENT, SURFACE_COMPONENT, TEXT_COMPONENT, VIRTUAL_TEXT_COMPONENT, VOID_COMPONENT, isAnchor, isSymbioteEvent, isSymbioteNode, } from './node-types.js';
|
|
4
|
+
export { createAnchor, createElement, createRawText, createSurfaceRoot, createVoid, debugNodeId, setNodeComponent, } from './node-instance.js';
|
|
5
|
+
export { functionPropOf, functionPropsOf, markPropsDirty, setProp, setText, takePropKeyTally, takePropStats, writeProp, } from './node-props.js';
|
|
6
|
+
export { clearPublishedStyle, getExplicitStyle, getPublishedStyle, isSameShallowStyle, setNodeHidden, setNodePressed, setNodeUnderlayShown, } from './node-style.js';
|
|
7
|
+
export { hasListenerFor, listenerFor, setBehaviorListener, setEventListener, setNodeDispatch, } from './node-events.js';
|
|
8
|
+
export { routeProp } from './node-route.js';
|
|
9
|
+
export { appendChild, censusRetainedTree, insertBefore, removeChild, } from './node-tree.js';
|