@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.
Files changed (47) hide show
  1. package/build/event-value-source.d.ts +4 -0
  2. package/build/event-value-source.js +3 -0
  3. package/build/events/delivery.d.ts +9 -0
  4. package/build/events/delivery.js +143 -0
  5. package/build/events/index.js +199 -660
  6. package/build/events/names.d.ts +24 -0
  7. package/build/events/names.js +80 -0
  8. package/build/events/press.d.ts +20 -0
  9. package/build/events/press.js +89 -0
  10. package/build/events/responder.d.ts +6 -0
  11. package/build/events/responder.js +124 -0
  12. package/build/expo-native-view.d.ts +2 -0
  13. package/build/expo-native-view.js +19 -0
  14. package/build/imperative.d.ts +2 -0
  15. package/build/imperative.js +18 -1
  16. package/build/index.d.ts +12 -2
  17. package/build/index.js +8 -2
  18. package/build/mutation-buffer.d.ts +3 -0
  19. package/build/mutation-buffer.js +25 -8
  20. package/build/native-engine.d.ts +6 -0
  21. package/build/native-engine.js +27 -48
  22. package/build/native-tree-host.js +1 -0
  23. package/build/node-events.d.ts +11 -0
  24. package/build/node-events.js +145 -0
  25. package/build/node-instance.d.ts +8 -0
  26. package/build/node-instance.js +168 -0
  27. package/build/node-props.d.ts +13 -0
  28. package/build/node-props.js +131 -0
  29. package/build/node-route.d.ts +2 -0
  30. package/build/node-route.js +151 -0
  31. package/build/node-style.d.ts +15 -0
  32. package/build/node-style.js +214 -0
  33. package/build/node-tree.d.ts +6 -0
  34. package/build/node-tree.js +159 -0
  35. package/build/node-types.d.ts +70 -0
  36. package/build/node-types.js +36 -0
  37. package/build/node.d.ts +7 -97
  38. package/build/node.js +9 -1128
  39. package/build/permission-hook-runtime.d.ts +27 -0
  40. package/build/permission-hook-runtime.js +37 -0
  41. package/build/resource-controller.d.ts +6 -0
  42. package/build/resource-controller.js +30 -0
  43. package/build/tree-host.d.ts +7 -0
  44. package/cpp/SymbioteEngineBindings.cpp +19 -18
  45. package/cpp/SymbioteTree.cpp +81 -156
  46. package/cpp/SymbioteTree.h +6 -0
  47. package/package.json +2 -2
@@ -25,59 +25,38 @@ import { isRecord } from './type-guards.js';
25
25
  // restate the number: a bump that leaves a fixture behind reads as "the binary is stale", which is
26
26
  // exactly the message this constant exists to produce.
27
27
  export const SUPPORTED_NATIVE_VERSION = 4;
28
+ // A member missing from this list resolves fine at bring-up and throws at the first call site
29
+ // instead. `getBoundingClientRect` stays out on purpose: it is optional on the type.
30
+ const REQUIRED_METHOD_NAMES = [
31
+ 'allocInt32Array',
32
+ 'probeUIManager',
33
+ 'applyOps',
34
+ 'getProp',
35
+ 'getProps',
36
+ 'markPropsDirty',
37
+ 'getViewName',
38
+ 'parentOf',
39
+ 'childrenOf',
40
+ 'firstChildOf',
41
+ 'nextSiblingOf',
42
+ 'parentsOf',
43
+ 'subtreesOf',
44
+ 'teardownSubtreesOf',
45
+ 'ancestorsOf',
46
+ 'committedRecordOf',
47
+ 'dispatchCommand',
48
+ 'sendAccessibilityEvent',
49
+ 'measure',
50
+ 'measureInWindow',
51
+ 'measureLayout',
52
+ 'setIsJSResponder',
53
+ ];
28
54
  function isBindings(value) {
29
55
  if (!isRecord(value))
30
56
  return false;
31
57
  if (typeof value.version !== 'number')
32
58
  return false;
33
- if (typeof value.allocInt32Array !== 'function')
34
- return false;
35
- if (typeof value.probeUIManager !== 'function')
36
- return false;
37
- if (typeof value.applyOps !== 'function')
38
- return false;
39
- // The tree reads, checked one by one: a member added to the type without a line here resolves
40
- // fine at bring-up and throws at the first call site — a gesture or a measure() — one language
41
- // and several seconds away from the install that caused it.
42
- if (typeof value.getProp !== 'function')
43
- return false;
44
- if (typeof value.getProps !== 'function')
45
- return false;
46
- if (typeof value.markPropsDirty !== 'function')
47
- return false;
48
- if (typeof value.getViewName !== 'function')
49
- return false;
50
- if (typeof value.parentOf !== 'function')
51
- return false;
52
- if (typeof value.childrenOf !== 'function')
53
- return false;
54
- if (typeof value.firstChildOf !== 'function')
55
- return false;
56
- if (typeof value.nextSiblingOf !== 'function')
57
- return false;
58
- if (typeof value.parentsOf !== 'function')
59
- return false;
60
- if (typeof value.subtreesOf !== 'function')
61
- return false;
62
- if (typeof value.teardownSubtreesOf !== 'function')
63
- return false;
64
- if (typeof value.ancestorsOf !== 'function')
65
- return false;
66
- if (typeof value.committedRecordOf !== 'function')
67
- return false;
68
- // The imperative six, checked by name: a pod with applyOps but not these is an older binary,
69
- // and accepting it means measure() reaches a missing method at gesture time, not bring-up.
70
- if (typeof value.dispatchCommand !== 'function')
71
- return false;
72
- if (typeof value.sendAccessibilityEvent !== 'function')
73
- return false;
74
- if (typeof value.measure !== 'function')
75
- return false;
76
- if (typeof value.measureInWindow !== 'function')
77
- return false;
78
- if (typeof value.measureLayout !== 'function')
79
- return false;
80
- return typeof value.setIsJSResponder === 'function';
59
+ return REQUIRED_METHOD_NAMES.every(name => typeof value[name] === 'function');
81
60
  }
82
61
  // SUPPORTED_NATIVE_VERSION guards a memory layout, not a member list: the shape guard above
83
62
  // already refuses an older pod lacking a new function's name, so bumping for that alone would
@@ -36,6 +36,7 @@ export function nativeTreeHost(bindings) {
36
36
  measureInWindow: bindings.measureInWindow,
37
37
  measureLayout: bindings.measureLayout,
38
38
  setIsJSResponder: bindings.setIsJSResponder,
39
+ getBoundingClientRect: bindings.getBoundingClientRect,
39
40
  };
40
41
  }
41
42
  // Install it, if this runtime has a native module and nothing has claimed the seam already.
@@ -0,0 +1,11 @@
1
+ import type { IEventDispatch, IListener, ISymbioteNode } from './node-types';
2
+ export declare function setBehaviorListener(node: ISymbioteNode, name: string, listener: IListener | undefined): void;
3
+ export declare function setNodeDispatch(node: ISymbioteNode, dispatch: IEventDispatch | undefined): void;
4
+ /** Whether anything at all would receive `name` on this node. */
5
+ export declare function hasListenerFor(node: ISymbioteNode, name: string): boolean;
6
+ export declare function listenerFor(node: ISymbioteNode, name: string): IListener | undefined;
7
+ export declare function setEventListener(node: ISymbioteNode, name: string, value: unknown): void;
8
+ export declare function isOnEventName(key: string): boolean;
9
+ /** `onChange` to `change`. */
10
+ export declare function listenerName(propName: string): string;
11
+ export declare const RESPONDER_EVENTS: ReadonlySet<string>;
@@ -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;