@symbiote-native/engine 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/build/accessibility-props.d.ts +0 -11
  2. package/build/accessibility-props.js +30 -68
  3. package/build/animated/graph.js +1 -1
  4. package/build/animated/leaf-lifecycle.js +2 -2
  5. package/build/asset-source-resolver.d.ts +2 -0
  6. package/build/asset-source-resolver.js +13 -0
  7. package/build/back-handler/index.d.ts +1 -5
  8. package/build/back-handler/index.js +0 -6
  9. package/build/debug.js +8 -22
  10. package/build/dispatch.js +3 -9
  11. package/build/events/delivery.d.ts +9 -0
  12. package/build/events/delivery.js +143 -0
  13. package/build/events/index.js +199 -660
  14. package/build/events/names.d.ts +24 -0
  15. package/build/events/names.js +80 -0
  16. package/build/events/press.d.ts +20 -0
  17. package/build/events/press.js +89 -0
  18. package/build/events/responder.d.ts +6 -0
  19. package/build/events/responder.js +124 -0
  20. package/build/fabric-props.js +74 -179
  21. package/build/fabric.d.ts +0 -13
  22. package/build/fabric.js +18 -38
  23. package/build/host-access.d.ts +1 -128
  24. package/build/host-access.js +96 -205
  25. package/build/host-behavior.d.ts +0 -100
  26. package/build/host-behavior.js +125 -311
  27. package/build/image-loader.js +10 -23
  28. package/build/image-source-resolver.js +3 -7
  29. package/build/image-source-write.d.ts +0 -11
  30. package/build/image-source-write.js +14 -34
  31. package/build/imperative.d.ts +2 -28
  32. package/build/imperative.js +60 -93
  33. package/build/index.d.ts +6 -2
  34. package/build/index.js +29 -39
  35. package/build/mutation-buffer.d.ts +3 -177
  36. package/build/mutation-buffer.js +162 -316
  37. package/build/native-engine.d.ts +6 -102
  38. package/build/native-engine.js +60 -141
  39. package/build/native-events.js +9 -18
  40. package/build/native-tree-host.d.ts +0 -21
  41. package/build/native-tree-host.js +15 -31
  42. package/build/node-events.d.ts +11 -0
  43. package/build/node-events.js +145 -0
  44. package/build/node-instance.d.ts +8 -0
  45. package/build/node-instance.js +168 -0
  46. package/build/node-props.d.ts +13 -0
  47. package/build/node-props.js +131 -0
  48. package/build/node-route.d.ts +2 -0
  49. package/build/node-route.js +151 -0
  50. package/build/node-style.d.ts +15 -0
  51. package/build/node-style.js +214 -0
  52. package/build/node-tree.d.ts +6 -0
  53. package/build/node-tree.js +159 -0
  54. package/build/node-types.d.ts +70 -0
  55. package/build/node-types.js +36 -0
  56. package/build/node.d.ts +7 -309
  57. package/build/node.js +9 -1564
  58. package/build/post-commit.js +3 -8
  59. package/build/process-aspect-ratio.js +3 -7
  60. package/build/process-background-longhands.js +10 -19
  61. package/build/process-filter.js +11 -19
  62. package/build/process-font-variant.js +3 -7
  63. package/build/registry.d.ts +0 -33
  64. package/build/registry.js +22 -57
  65. package/build/report-error.js +4 -18
  66. package/build/structured-style.d.ts +0 -9
  67. package/build/structured-style.js +16 -31
  68. package/build/styles.js +3 -6
  69. package/build/surface.d.ts +0 -26
  70. package/build/surface.js +29 -76
  71. package/build/text-input-state.js +4 -8
  72. package/build/touch-history.js +5 -11
  73. package/build/tree-host.d.ts +7 -270
  74. package/build/tree-host.js +63 -153
  75. package/build/view-config.js +17 -37
  76. package/cpp/SymbioteEngineBindings.cpp +19 -18
  77. package/cpp/SymbioteTree.cpp +81 -156
  78. package/cpp/SymbioteTree.h +6 -0
  79. package/package.json +2 -2
@@ -0,0 +1,145 @@
1
+ // The listener channel: which `on*` prop becomes a listener, who owns the slot, and the handful of
2
+ // events Fabric gates behind a boolean prop
3
+ import { recordSetOwnedListener } from './mutation-buffer.js';
4
+ import { appListenerFor, hasHostBehaviors, notifyOwnedListenerChange, ownsListener, stashAppListener, } from './host-behavior.js';
5
+ import { IMAGE_LOAD_EVENT_NAMES, anyImageLoadEventListenerWired, } from './image-source-write.js';
6
+ import { setProp } from './node-props.js';
7
+ // Fabric gates a handful of events behind a BOOLEAN prop: unlike scroll/touch/change these fire
8
+ // only when the shadow node carries the flag, and a gated handler would otherwise attach on our
9
+ // side while the native event silently never arrives
10
+ // `magicTap` maps to `onMagicTap`, not the C++ member name `onAccessibilityMagicTap`: RN's own
11
+ // `BaseViewConfig.ios.js` disagrees with its C++ prop name, and matching stock is the only choice
12
+ const GATED_EVENT_PROPS = new Map([
13
+ ['layout', 'onLayout'],
14
+ ['textLayout', 'onTextLayout'],
15
+ ['accessibilityTap', 'onAccessibilityTap'],
16
+ ['magicTap', 'onMagicTap'],
17
+ ['accessibilityEscape', 'onAccessibilityEscape'],
18
+ ['accessibilityAction', 'onAccessibilityAction'],
19
+ ]);
20
+ // Install a listener the BEHAVIOR owns, bypassing the ownership check, т.к. `setEventListener`
21
+ // diverts an owned name into the stash and that would be circular for the behavior's dispatcher
22
+ // `undefined` removes it, gate flag included: ScrollView takes the owner's `layout` only while
23
+ // something wants it, and a one-way installer would leave `onLayout: true` stuck in the payload
24
+ export function setBehaviorListener(node, name, listener) {
25
+ if (listener === undefined)
26
+ node.listeners?.delete(name);
27
+ else
28
+ (node.listeners ??= new Map()).set(name, listener);
29
+ // The flag is written UNCONDITIONALLY. Writing it only on the FLIP was measured and rejected: it
30
+ // saves 1 KB per item on the two anchor-backed touchables and costs 10 ms on their teardown
31
+ // (`gated-listener-writes.test.ts`, symbiote-perf-measurement §24)
32
+ const flagProp = GATED_EVENT_PROPS.get(name);
33
+ if (flagProp !== undefined)
34
+ setProp(node, flagProp, listener === undefined ? undefined : true);
35
+ }
36
+ // Arm (or release, with `undefined`) the shared handler a behavior dispatches through, instead of
37
+ // installing one closure per name. See `IEventDispatch`
38
+ export function setNodeDispatch(node, dispatch) {
39
+ node.dispatch = dispatch;
40
+ }
41
+ /** Whether anything at all would receive `name` on this node. */
42
+ export function hasListenerFor(node, name) {
43
+ if (node.listeners?.has(name) === true)
44
+ return true;
45
+ return node.dispatch?.names.has(name) === true;
46
+ }
47
+ // The app's own listener first: a behavior owns its names, so the two never collide, and an
48
+ // unarmed node stops at one `Map` probe
49
+ export function listenerFor(node, name) {
50
+ const own = node.listeners?.get(name);
51
+ if (own !== undefined)
52
+ return own;
53
+ const dispatch = node.dispatch;
54
+ if (dispatch === undefined || !dispatch.names.has(name))
55
+ return undefined;
56
+ // ONE closure per delivered event, where a dispatcher per name built seven per mounted node.
57
+ // `name` is bound here rather than read off `event.type`, so a caller building its own event
58
+ // (every test driving a behavior by hand) reaches the same handler
59
+ return event => dispatch.deliver(node, name, event);
60
+ }
61
+ // The unowned names whose presence a platform rule reads
62
+ const PRESSABILITY_NAMES = new Set([
63
+ 'press',
64
+ 'longPress',
65
+ 'startShouldSetResponder',
66
+ ]);
67
+ // A name a host behavior OWNS never reaches `node.listeners`: the behavior's dispatcher holds that
68
+ // slot, and the map is single-slot, so the app's callback would evict it with no diagnostic
69
+ function stashOwnedListener(node, name, value, isHandler) {
70
+ // The PRESENCE only, never the identity: a fresh closure nearly every render must not notify
71
+ const wasWired = appListenerFor(node, name) !== undefined;
72
+ stashAppListener(node, name, isHandler ? value : undefined);
73
+ if (wasWired !== isHandler) {
74
+ // The BIT, on the flip only, so a platform rule can resolve a key depending on whether the app
75
+ // wired anything without the closure leaving JS
76
+ recordSetOwnedListener(node, name, isHandler);
77
+ notifyOwnedListenerChange(node, name, isHandler);
78
+ }
79
+ const flagged = GATED_EVENT_PROPS.get(name);
80
+ if (flagged !== undefined)
81
+ setProp(node, flagged, isHandler ? true : undefined);
82
+ }
83
+ // The explicit event channel. Structural adapters (Svelte `addEventListener`, Angular
84
+ // `Renderer2.listen`) call this with an already-known event name, flat-bag adapters reach it
85
+ // through `routeProp`, and a non-function value clears the listener
86
+ export function setEventListener(node, name, value) {
87
+ const isHandler = typeof value === 'function';
88
+ if (hasHostBehaviors() && ownsListener(node, name)) {
89
+ stashOwnedListener(node, name, value, isHandler);
90
+ return;
91
+ }
92
+ // A plain `<text>` presses through the engine's own synthesis with no behavior owning the names,
93
+ // yet its payload depends on whether it is pressable. Same bit as the owned path, on the flip
94
+ if (PRESSABILITY_NAMES.has(name)) {
95
+ const wasWired = node.listeners?.has(name) === true;
96
+ if (wasWired !== isHandler)
97
+ recordSetOwnedListener(node, name, isHandler);
98
+ }
99
+ if (isHandler) {
100
+ const handler = value;
101
+ const listeners = (node.listeners ??= new Map());
102
+ listeners.set(name, (event) => handler(event));
103
+ }
104
+ else {
105
+ node.listeners?.delete(name);
106
+ }
107
+ const flagProp = GATED_EVENT_PROPS.get(name);
108
+ if (flagProp !== undefined)
109
+ setProp(node, flagProp, isHandler ? true : undefined);
110
+ // `onLoad` and friends are real Fabric events on `RCTImageView`, so they land here rather than in
111
+ // `writeProp`. See `image-source-write.ts` for Android's synthesized `shouldNotifyLoadEvents`
112
+ if (node.resolvesImageSources && IMAGE_LOAD_EVENT_NAMES.has(name)) {
113
+ setProp(node, 'shouldNotifyLoadEvents', anyImageLoadEventListenerWired(node.listeners) ? true : undefined);
114
+ }
115
+ }
116
+ // `/^on[A-Z]/` spelled out, т.к. this runs on EVERY prop write and a regex costs measurably more
117
+ // than the character reads. 111 is 'o', 110 is 'n', 65-90 is A-Z, and `charCodeAt` past the end
118
+ // answers NaN, which fails every comparison
119
+ export function isOnEventName(key) {
120
+ if (key.charCodeAt(0) !== 111 || key.charCodeAt(1) !== 110)
121
+ return false;
122
+ const third = key.charCodeAt(2);
123
+ return third >= 65 && third <= 90;
124
+ }
125
+ /** `onChange` to `change`. */
126
+ export function listenerName(propName) {
127
+ return propName.charAt(2).toLowerCase() + propName.slice(3);
128
+ }
129
+ // PanResponder's `panHandlers`: a JS-side protocol synthesized from raw touches, not Fabric
130
+ // ViewConfig events, so `isEventFor` never reports them. Treated as listeners on any node so the
131
+ // handlers attach instead of reaching Fabric as dead props
132
+ export const RESPONDER_EVENTS = new Set([
133
+ 'startShouldSetResponder',
134
+ 'startShouldSetResponderCapture',
135
+ 'moveShouldSetResponder',
136
+ 'moveShouldSetResponderCapture',
137
+ 'responderGrant',
138
+ 'responderReject',
139
+ 'responderStart',
140
+ 'responderMove',
141
+ 'responderEnd',
142
+ 'responderRelease',
143
+ 'responderTerminate',
144
+ 'responderTerminationRequest',
145
+ ]);
@@ -0,0 +1,8 @@
1
+ import { type ISymbioteNode } from './node-types';
2
+ export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
3
+ export declare function createRawText(text: string, tag?: string): ISymbioteNode;
4
+ export declare function debugNodeId(node: ISymbioteNode): number;
5
+ export declare function createAnchor(): ISymbioteNode;
6
+ export declare function createVoid(): ISymbioteNode;
7
+ export declare function createSurfaceRoot(): ISymbioteNode;
8
+ export declare function setNodeComponent(node: ISymbioteNode, component: string): void;
@@ -0,0 +1,168 @@
1
+ // The one shape every retained node has, and the factories that mint it
2
+ import { recordCreateAnchor, recordCreateVoid, recordCreateElement, recordCreateRawText, recordSetComponent, recordSetProp, } from './mutation-buffer.js';
3
+ import { attachHostBehavior, hasHostBehaviors, } from './host-behavior.js';
4
+ import { configPayloadFold } from './registry.js';
5
+ import { dlog } from './debug.js';
6
+ // A cycle, deliberately: `imperative.ts` imports this module and the prototype methods below call
7
+ // back into it. Neither touches the other at module-evaluation time, only inside a function body
8
+ import { measure as engineMeasure, measureInWindow as engineMeasureInWindow, measureLayout as engineMeasureLayout, setNativeProps as engineSetNativeProps, dispatchViewCommand, } from './imperative.js';
9
+ import { ANCHOR_COMPONENT, BRAND, RAW_TEXT_COMPONENT, SURFACE_COMPONENT, VOID_COMPONENT, isSymbioteNode, } from './node-types.js';
10
+ const FOCUS_COMMAND = 'focus';
11
+ const BLUR_COMMAND = 'blur';
12
+ // Names and arg order mirror RN's `ScrollViewCommands`
13
+ const SCROLL_TO_COMMAND = 'scrollTo';
14
+ const SCROLL_TO_END_COMMAND = 'scrollToEnd';
15
+ const FLASH_SCROLL_INDICATORS_COMMAND = 'flashScrollIndicators';
16
+ // A class, so the imperative methods share a prototype instead of being allocated per node, and
17
+ // both factories below mint the same hidden class. Fields are `declare`d and assigned in the
18
+ // constructor, the shape V8 and Hermes handle best
19
+ class SymbioteNode {
20
+ constructor(component, isText) {
21
+ // Every field is assigned here, not lazily: present from the constructor, they all keep ONE
22
+ // hidden class for every node
23
+ this[BRAND] = true;
24
+ this.component = component;
25
+ this.isText = isText;
26
+ this.listeners = undefined;
27
+ this.dispatch = undefined;
28
+ this.hasCommitHook = false;
29
+ this.resolvesImageSources = false;
30
+ this.nativeIdWinsOverId = false;
31
+ this.styleParts = undefined;
32
+ this.payloadFold = undefined;
33
+ this.hostBehavior = undefined;
34
+ this.childHost = undefined;
35
+ this.wrapper = undefined;
36
+ this.mayHaveChildren = false;
37
+ this.isTornDown = false;
38
+ // `slotOf` reads this pair on EVERY handle operand of every op, so both must be stable slots.
39
+ // `slotBatch` starts at a value no real batch carries, so an untouched node needs no flag
40
+ this.slot = 0;
41
+ this.slotBatch = 0;
42
+ this.createdBatch = 0;
43
+ }
44
+ measure(callback) {
45
+ engineMeasure(this, callback);
46
+ }
47
+ measureInWindow(callback) {
48
+ engineMeasureInWindow(this, callback);
49
+ }
50
+ measureLayout(relativeToNativeNode, onSuccess, onFail) {
51
+ if (!isSymbioteNode(relativeToNativeNode)) {
52
+ dlog('measureLayout: relative target must be a host ref');
53
+ return;
54
+ }
55
+ engineMeasureLayout(this, relativeToNativeNode, onSuccess, onFail);
56
+ }
57
+ setNativeProps(nativeProps) {
58
+ engineSetNativeProps(this, nativeProps);
59
+ }
60
+ focus() {
61
+ dispatchViewCommand(this, FOCUS_COMMAND, []);
62
+ }
63
+ blur() {
64
+ dispatchViewCommand(this, BLUR_COMMAND, []);
65
+ }
66
+ // The defaults live HERE and nowhere else, so `buildScrollViewHandle` delegating here cannot
67
+ // drift on what `scrollTo()` with no argument means
68
+ scrollTo(options) {
69
+ const x = options?.x ?? 0;
70
+ const y = options?.y ?? 0;
71
+ const animated = options?.animated ?? true;
72
+ dlog(`ScrollView.scrollTo x=${x} y=${y} animated=${animated}`);
73
+ dispatchViewCommand(this, SCROLL_TO_COMMAND, [x, y, animated]);
74
+ }
75
+ scrollToEnd(options) {
76
+ const animated = options?.animated ?? true;
77
+ dlog(`ScrollView.scrollToEnd animated=${animated}`);
78
+ dispatchViewCommand(this, SCROLL_TO_END_COMMAND, [animated]);
79
+ }
80
+ flashScrollIndicators() {
81
+ dlog('ScrollView.flashScrollIndicators');
82
+ dispatchViewCommand(this, FLASH_SCROLL_INDICATORS_COMMAND, []);
83
+ }
84
+ }
85
+ // Mint an element and record its creation. The node object IS the handle: what the ops address,
86
+ // what the host attaches its native node to, and what Fabric hands back as an event target
87
+ export function createElement(component, isText = false,
88
+ // The intrinsic tag this node came from, when it differs from the Fabric view name above. The
89
+ // behavior registry is keyed by tag, so an adapter creating `<pressable>` must hand it over
90
+ tag = component) {
91
+ const node = new SymbioteNode(component, isText);
92
+ // A primitive that commits NO VIEW resolves to the anchor component through `descriptorFor`, and
93
+ // a primitive whose ENTIRE subtree must vanish resolves to the void one the same way
94
+ if (component === ANCHOR_COMPONENT)
95
+ recordCreateAnchor(node);
96
+ else if (component === VOID_COMPONENT)
97
+ recordCreateVoid(node);
98
+ else
99
+ recordCreateElement(node, component, isText, node);
100
+ // Gated on the boolean, not on the `Map`: this runs ~9 000 times per benchmark create, and an app
101
+ // that registers nothing must pay one boolean read rather than a hash lookup per node
102
+ if (hasHostBehaviors())
103
+ attachHostBehavior(node, tag);
104
+ // A third-party view's own ViewConfig processors, as a fold, AFTER the behavior's: the behavior
105
+ // rewrites wrapper-body props and `validAttributes[*].process` then converts what it produced
106
+ const configFold = configPayloadFold(component);
107
+ if (configFold !== undefined) {
108
+ const behaviorFold = node.payloadFold;
109
+ node.payloadFold =
110
+ behaviorFold === undefined
111
+ ? configFold
112
+ : props => configFold(behaviorFold(props));
113
+ }
114
+ return node;
115
+ }
116
+ // `tag` mirrors `createElement`'s, т.к. a raw text's CONTENT can still be a function of the
117
+ // platform (Button renders its title uppercased on Android) even with no props an app can write
118
+ export function createRawText(text, tag = RAW_TEXT_COMPONENT) {
119
+ const node = new SymbioteNode(RAW_TEXT_COMPONENT, false);
120
+ recordCreateRawText(node, text);
121
+ // TAG check first, not `hasHostBehaviors()`: almost no raw text is tagged, so an untagged one
122
+ // pays a pointer-equality compare against the default rather than a registry lookup
123
+ if (tag !== RAW_TEXT_COMPONENT && hasHostBehaviors())
124
+ attachHostBehavior(node, tag);
125
+ return node;
126
+ }
127
+ // A `WeakMap` cannot be logged, so this gives every node a small human-readable id assigned lazily,
128
+ // which lets a `dlog` at ref-attach time and one at dispatch time prove they are the SAME object
129
+ const debugIds = new WeakMap();
130
+ let nextDebugId = 1;
131
+ export function debugNodeId(node) {
132
+ let id = debugIds.get(node);
133
+ if (id === undefined) {
134
+ id = nextDebugId++;
135
+ debugIds.set(node, id);
136
+ }
137
+ return id;
138
+ }
139
+ export function createAnchor() {
140
+ const node = new SymbioteNode(ANCHOR_COMPONENT, false);
141
+ recordCreateAnchor(node);
142
+ return node;
143
+ }
144
+ export function createVoid() {
145
+ const node = new SymbioteNode(VOID_COMPONENT, false);
146
+ recordCreateVoid(node);
147
+ return node;
148
+ }
149
+ // One persistent root view per surface, mirroring RN's own `AppContainer`. Not decoration: without
150
+ // `flex: 1` a non-flex root collapses to content height, and without `box-none` a touch outside the
151
+ // app has no escape
152
+ export function createSurfaceRoot() {
153
+ const node = new SymbioteNode(SURFACE_COMPONENT, false);
154
+ recordCreateElement(node, 'RCTView', false, node);
155
+ // Recorded straight, not through `routeProp`: these are literal Fabric props, not props an app
156
+ // authored, so they want none of the class merging or event routing that path exists for
157
+ recordSetProp(node, 'style', { flex: 1 });
158
+ recordSetProp(node, 'pointerEvents', 'box-none');
159
+ return node;
160
+ }
161
+ // Change which Fabric view a node commits as, keeping the node's identity. Both the JS field and
162
+ // the op move, т.к. no prop write moves a node between views and the host must re-create it
163
+ export function setNodeComponent(node, component) {
164
+ if (node.component === component)
165
+ return;
166
+ node.component = component;
167
+ recordSetComponent(node, component);
168
+ }
@@ -0,0 +1,13 @@
1
+ import type { ISymbioteNode } from './node-types';
2
+ export declare function takePropStats(): {
3
+ writes: number;
4
+ };
5
+ export declare function takePropKeyTally(): ReadonlyMap<string, number>;
6
+ export declare const REACT_JSX_DEV_PROPS: ReadonlySet<string>;
7
+ export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
8
+ export declare function writeProp(node: ISymbioteNode, key: string, value: unknown): void;
9
+ /** What `propOf` consults before asking the host, `undefined` when nothing was stashed. */
10
+ export declare function functionPropOf(node: ISymbioteNode, key: string): unknown;
11
+ export declare function functionPropsOf(node: ISymbioteNode): ReadonlyMap<string, unknown> | undefined;
12
+ export declare function markPropsDirty(node: ISymbioteNode): void;
13
+ export declare function setText(node: ISymbioteNode, text: string): void;
@@ -0,0 +1,131 @@
1
+ // The prop write path: the one choke point every writer passes on its way to an op, plus the
2
+ // function-prop stash and the two counters `readCommitProfile` drains
3
+ import { isPendingCreate, noteHostSideChange, recordSetProp, recordSetText, } from './mutation-buffer.js';
4
+ import { derivedNodesOf, noteCommitHookNodeChanged, slotDerivesFrom, } from './host-behavior.js';
5
+ import { resolveStructuredStyle } from './structured-style.js';
6
+ import { IMAGE_SOURCE_PROPS, resolveImageSourceProp, } from './image-source-write.js';
7
+ import { Platform } from './platform';
8
+ import { isDebug } from './debug.js';
9
+ import { flushOps, treeHost } from './tree-host.js';
10
+ // How many prop writes an adapter pushed at the engine, read and zeroed through
11
+ // `readCommitProfile`. Not gated behind `isDebug()`, т.к. an integer increment is noise next to the
12
+ // prop write it counts
13
+ const propStats = { writes: 0 };
14
+ export function takePropStats() {
15
+ const snapshot = { writes: propStats.writes };
16
+ propStats.writes = 0;
17
+ return snapshot;
18
+ }
19
+ // `<component>.<key>` to write count, gated behind `isDebug()` т.к. a `Map` lookup per write is
20
+ // real cost on the hottest path. Names which (view, key) pair an aggregate delta is hiding
21
+ let propKeyTally;
22
+ export function takePropKeyTally() {
23
+ const snapshot = propKeyTally ?? new Map();
24
+ propKeyTally = undefined;
25
+ return snapshot;
26
+ }
27
+ // React's JSX dev transform annotates every element with `__self` and `__source`, which React's own
28
+ // host config consumes and never forwards. Both platforms reject them: `__self` is a cyclic module
29
+ // `this`, and `jsi::dynamicFromValue` keeps no visited set
30
+ export const REACT_JSX_DEV_PROPS = new Set([
31
+ '__self',
32
+ '__source',
33
+ ]);
34
+ // Function props that never left JS, keyed by node. A function CANNOT cross this wire, т.к.
35
+ // `jsi::dynamicFromValue` THROWS on a callable and kills the whole batch
36
+ const functionProps = new WeakMap();
37
+ // A pure prop set with no event inference: the event-vs-prop decision is `routeProp`'s, never the
38
+ // key's name. `undefined` DELETES the key, where `null` is a legitimate Fabric "reset to default"
39
+ // No `Object.is` dedupe here, т.к. it needs the value the node already holds, which JS does not.
40
+ // The guard lives in the host's `OP_SET_PROP`, where the previous value is a local field
41
+ export function setProp(node, key, value) {
42
+ // A composed primitive's slot, and its wrapper where it has one, can carry a value DERIVED from
43
+ // an owner prop, so `markPropsDirty` bubbles up or neither ever learns
44
+ if (node.childHost !== undefined && slotDerivesFrom(node, key)) {
45
+ markPropsDirty(node.childHost);
46
+ // Past the slot: a `buildStructure` that builds a CHAIN registers the deeper nodes here, and
47
+ // each keeps its own pure fold reading the owner. See `addDerivedNode`
48
+ const derived = derivedNodesOf(node);
49
+ if (derived !== undefined)
50
+ for (const each of derived)
51
+ markPropsDirty(each);
52
+ if (node.wrapper !== undefined)
53
+ markPropsDirty(node.wrapper);
54
+ }
55
+ propStats.writes += 1;
56
+ if (isDebug()) {
57
+ propKeyTally ??= new Map();
58
+ const tallyKey = `${node.component}.${key}`;
59
+ propKeyTally.set(tallyKey, (propKeyTally.get(tallyKey) ?? 0) + 1);
60
+ }
61
+ writeProp(node, key, value);
62
+ }
63
+ // The one place a prop reaches the wire, and the only place that can keep a function off it.
64
+ // `setNativeProps` calls this rather than `recordSetProp`, т.к. it has no `routeProp` in front
65
+ export function writeProp(node, key, value) {
66
+ // Repeated from `routeProp` т.к. THIS is the path with nothing in front of it: `AnimatedProps`
67
+ // re-sends its whole raw bag every frame, so on a JSX adapter `__self` rides straight past
68
+ if (REACT_JSX_DEV_PROPS.has(key))
69
+ return;
70
+ // Arms the node's recurring post-commit hook. HERE т.к. this is where the declarative and
71
+ // `setNativeProps` paths meet, and a hook armed only by the former misses the imperative write
72
+ if (node.hasCommitHook)
73
+ noteCommitHookNodeChanged(node);
74
+ // `boxShadow` / `filter` / `transform` and Image's source props resolve on the way IN, at this
75
+ // same choke point, т.к. the C++ payload builder has no JS to do it headless
76
+ let written = value;
77
+ if (key === 'style' || key === 'activeStyle') {
78
+ written = resolveStructuredStyle(value);
79
+ }
80
+ else if (node.resolvesImageSources && IMAGE_SOURCE_PROPS.has(key)) {
81
+ written = resolveImageSourceProp(value, key === 'source' && Platform.OS === 'android');
82
+ }
83
+ if (typeof written === 'function') {
84
+ let bag = functionProps.get(node);
85
+ if (bag === undefined) {
86
+ bag = new Map();
87
+ functionProps.set(node, bag);
88
+ }
89
+ bag.set(key, value);
90
+ // The host must not be left holding whatever stood under this key before: a stale value read
91
+ // back through `propOf` would beat the function this write just stashed
92
+ recordSetProp(node, key, undefined);
93
+ return;
94
+ }
95
+ // Written over with a non-function, so the stash must let go or it keeps answering
96
+ const bag = functionProps.get(node);
97
+ if (bag !== undefined)
98
+ bag.delete(key);
99
+ recordSetProp(node, key, written);
100
+ }
101
+ /** What `propOf` consults before asking the host, `undefined` when nothing was stashed. */
102
+ export function functionPropOf(node, key) {
103
+ return functionProps.get(node)?.get(key);
104
+ }
105
+ // The same stash, whole, for `propsOf` to layer over the host's answer. `undefined` rather than an
106
+ // empty `Map` for a node that stashed nothing, so the caller hands back the host's own object
107
+ export function functionPropsOf(node) {
108
+ return functionProps.get(node);
109
+ }
110
+ // "Rebuild this node's payload, the fold reads state I just changed". A behavior whose payload is
111
+ // DERIVED has no prop to write, so this is the one route that dirties it directly
112
+ export function markPropsDirty(node) {
113
+ // Announced to the buffer even though it writes no op, т.к. a commit that cannot see this change
114
+ // would skip itself as idle
115
+ noteHostSideChange();
116
+ if (node.hasCommitHook)
117
+ noteCommitHookNodeChanged(node);
118
+ // A node this batch created has no committed payload and the host cannot name it yet, so the mark
119
+ // is a no-op and the `flushOps` it would force is the whole of its cost
120
+ if (isPendingCreate(node))
121
+ return;
122
+ // The host call takes a HANDLE, so it has to be ORDERED after the ops that built the node
123
+ flushOps();
124
+ treeHost()?.markPropsDirty(node);
125
+ }
126
+ // Counted in `propStats`, т.к. a text write IS a prop write, reaching Fabric as `RCTRawText`'s only
127
+ // prop. Unguarded for the same reason `setProp` is: the host holds the standing text and dedupes
128
+ export function setText(node, text) {
129
+ propStats.writes += 1;
130
+ recordSetText(node, text);
131
+ }
@@ -0,0 +1,2 @@
1
+ import type { ISymbioteNode } from './node-types';
2
+ export declare function routeProp(node: ISymbioteNode, key: string, value: unknown): void;
@@ -0,0 +1,151 @@
1
+ // Where every adapter's prop write lands, whatever shape it started in: the slot redirect, the
2
+ // `id` alias, the class/style slots, the event split, and `setProp` for everything else
3
+ import { isEventFor } from './view-config.js';
4
+ import { canonicalClassName, isClassNameValue, resolveClassName, } from './style-registry/index.js';
5
+ import { dlog } from './debug.js';
6
+ import { slotPropNameFor } from './host-behavior.js';
7
+ import { hasAnimatedNodes } from './animated/graph.js';
8
+ import { bindAnimatedEvent, bindAnimatedValue } from './animated/host-binding.js';
9
+ import { REACT_JSX_DEV_PROPS, setProp } from './node-props.js';
10
+ import { isStyleCallback, isSameShallowStyle, pushClassStyle, setNodePressed, stylePartsOf, } from './node-style.js';
11
+ import { RESPONDER_EVENTS, isOnEventName, listenerName, setEventListener, } from './node-events.js';
12
+ const CLASS_PROP_KEYS = new Set(['class', 'className']);
13
+ // `id` is RN's alias for `nativeID` and wins when both are set (`View.js`: `nativeID = id`). No
14
+ // ViewConfig declares raw `id`, so Fabric drops it and a half-working rename loses `nativeID`
15
+ const ID_ALIAS_FROM = 'id';
16
+ const ID_ALIAS_TO = 'nativeID';
17
+ // Precedence needs state т.к. upstream decides `id ?? nativeID` in one expression while a per-key
18
+ // writer never sees both. The authored `nativeID` is remembered, so clearing `id` hands the slot
19
+ // back rather than latching
20
+ const idAliased = new WeakMap();
21
+ function routeIdAlias(node, key, value) {
22
+ const state = idAliased.get(node) ?? {
23
+ idValue: undefined,
24
+ nativeIdValue: undefined,
25
+ };
26
+ if (key === ID_ALIAS_FROM)
27
+ state.idValue = value;
28
+ else
29
+ state.nativeIdValue = value;
30
+ idAliased.set(node, state);
31
+ const published = node.nativeIdWinsOverId
32
+ ? (state.nativeIdValue ?? state.idValue)
33
+ : (state.idValue ?? state.nativeIdValue);
34
+ setProp(node, ID_ALIAS_TO, published);
35
+ }
36
+ // A composed primitive's owner receives props that belong to its internal slot
37
+ // (`contentContainerStyle` on ScrollView styles the content view), the same reason the owner is
38
+ // named for a child. Returns whether the write was taken
39
+ function routeToSlot(node, key, value) {
40
+ const slotKey = slotPropNameFor(node, key);
41
+ if (slotKey === undefined)
42
+ return false;
43
+ // A class name is a legal spelling of `contentContainerStyle`, so a string lands on the slot as
44
+ // `class`: renamed verbatim it would publish a style holding a string, dropped with nothing red
45
+ const slotValueFor = node.hostBehavior?.slotValueFor;
46
+ routeProp(node.childHost ?? node, slotKey === 'style' && typeof value === 'string' ? 'class' : slotKey, slotValueFor === undefined ? value : slotValueFor(slotKey, value));
47
+ return true;
48
+ }
49
+ // A function `style` arrives intact and is resolved at BOTH states, written as an explicit
50
+ // `style` + `activeStyle` pair. Without this it misses `setEventListener`, lands in `setProp` as a
51
+ // function and is dropped from the payload, so the node commits with no style at all
52
+ function routeStyleValue(node, resolved) {
53
+ const parts = stylePartsOf(node);
54
+ if (isStyleCallback(resolved)) {
55
+ parts.explicitStyle = resolved({ pressed: false });
56
+ parts.activeStyle = resolved({ pressed: true });
57
+ parts.activeStyleFromCallback = true;
58
+ pushClassStyle(node, parts);
59
+ return;
60
+ }
61
+ // Gated on something being published, which keeps the restore path intact, and on the previous
62
+ // write not coming from a callback, т.к. that one owns `parts.activeStyle`
63
+ if (parts.published !== undefined &&
64
+ !parts.activeStyleFromCallback &&
65
+ isSameShallowStyle(resolved, parts.explicitStyle)) {
66
+ return;
67
+ }
68
+ parts.explicitStyle = resolved;
69
+ // Only a variant WE derived is stale now: an AUTHORED `activeStyle` must survive a `style` write,
70
+ // т.к. the two arrive as independent props in an unspecified order
71
+ if (parts.activeStyleFromCallback) {
72
+ parts.activeStyle = undefined;
73
+ parts.activeStyleFromCallback = false;
74
+ }
75
+ pushClassStyle(node, parts);
76
+ }
77
+ // The three keys that reach the style slots rather than the payload. Returns whether one did
78
+ function routeStyleSlot(node, key, resolved) {
79
+ if (CLASS_PROP_KEYS.has(key)) {
80
+ const parts = stylePartsOf(node);
81
+ // Canonicalised HERE so the stored value is what everything downstream keys on: an all-string
82
+ // array becomes one string, and the pressed variant then works on it as on an authored string
83
+ parts.className = canonicalClassName(isClassNameValue(resolved) ? resolved : undefined);
84
+ parts.classStyle = resolveClassName(parts.className);
85
+ pushClassStyle(node, parts);
86
+ return true;
87
+ }
88
+ if (key === 'style') {
89
+ routeStyleValue(node, resolved);
90
+ return true;
91
+ }
92
+ // Ours, never Fabric's: it is consumed here and must not reach the payload
93
+ if (key === 'activeStyle') {
94
+ const parts = stylePartsOf(node);
95
+ parts.activeStyle = resolved;
96
+ // Slot 1 is no longer ours, т.к. whatever a callback derived has just been replaced. Without
97
+ // this the flag outlives its value and a later plain `style` clears a variant we never derived
98
+ parts.activeStyleFromCallback = false;
99
+ pushClassStyle(node, parts);
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ // An `on*` name becomes a listener only when the component's ViewConfig declares the event,
105
+ // so `onTintColor` on a Switch routes to `setProp` and reaches Fabric untouched
106
+ function routeEvent(node, key, resolved) {
107
+ // A native-driven `Animated.event` needs the native module as well as the listener map and
108
+ // registers under the PROP name, see `bindAnimatedEvent`
109
+ if (hasAnimatedNodes())
110
+ bindAnimatedEvent(node, key, resolved);
111
+ const name = listenerName(key);
112
+ const isRegisteredEvent = RESPONDER_EVENTS.has(name) || isEventFor(node.component, name);
113
+ // RNS* views derive events from react-native-screens' own codegen ViewConfig, so an unregistered
114
+ // event falls through to `setProp` as a dead prop and looks like "the button did nothing"
115
+ if (node.component.startsWith('RNS')) {
116
+ dlog(`routeProp: ${node.component} "${key}" -> listener "${name}" ` +
117
+ `registered=${isRegisteredEvent} at t=${Date.now()}`);
118
+ }
119
+ if (!isRegisteredEvent)
120
+ return false;
121
+ setEventListener(node, name, resolved);
122
+ return true;
123
+ }
124
+ export function routeProp(node, key, value) {
125
+ if (REACT_JSX_DEV_PROPS.has(key))
126
+ return;
127
+ // Gated on the field, so a node with no slot pays one load and a branch. Single-hop by
128
+ // construction, т.к. a slot has no slot of its own
129
+ if (node.childHost !== undefined && routeToSlot(node, key, value))
130
+ return;
131
+ // After the slot redirect on purpose: a composed primitive forwards most of its bag to an
132
+ // internal node, so an `id` on the owner belongs there
133
+ if (key === ID_ALIAS_FROM || key === ID_ALIAS_TO) {
134
+ routeIdAlias(node, key, value);
135
+ return;
136
+ }
137
+ // An AnimatedNode in a prop resolves here to the value to publish, the engine holding the
138
+ // subscription. Returns its input by identity when nothing is animated
139
+ const resolved = hasAnimatedNodes()
140
+ ? bindAnimatedValue(node, key, value)
141
+ : value;
142
+ if (routeStyleSlot(node, key, resolved))
143
+ return;
144
+ // RN's snapshot affordance: render the control pressed with no gesture. One string compare on the
145
+ // first commit, where a behavior hook reading it would cost a post-commit crossing per node
146
+ if (key === 'testOnly_pressed')
147
+ setNodePressed(node, resolved === true);
148
+ if (isOnEventName(key) && routeEvent(node, key, resolved))
149
+ return;
150
+ setProp(node, key, resolved);
151
+ }
@@ -0,0 +1,15 @@
1
+ import { type IClassNameValue } from './style-registry';
2
+ import type { IClassStyleParts, ISymbioteNode } from './node-types';
3
+ export declare function isStyleCallback(value: unknown): value is (state: {
4
+ pressed: boolean;
5
+ }) => unknown;
6
+ export declare function stylePartsOf(node: ISymbioteNode): IClassStyleParts;
7
+ export declare function isSameShallowStyle(next: unknown, standing: unknown): boolean;
8
+ export declare function pushClassStyle(node: ISymbioteNode, parts: IClassStyleParts): void;
9
+ export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): void;
10
+ export declare function setNodePressed(node: ISymbioteNode, pressed: boolean): void;
11
+ export declare function setNodeUnderlayShown(node: ISymbioteNode, shown: boolean): void;
12
+ export declare function clearPublishedStyle(node: ISymbioteNode): void;
13
+ export declare function getExplicitStyle(node: ISymbioteNode): unknown;
14
+ export declare function getPublishedStyle(node: ISymbioteNode): readonly unknown[];
15
+ export type { IClassNameValue };