@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/native-engine.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|