@symbiote-native/engine 1.2.0 → 1.3.1
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/android/CMakeLists.txt +31 -1
- package/android/build.gradle +4 -1
- package/build/accessibility-info/index.android.js +21 -17
- package/build/accessibility-info/index.ios.js +2 -0
- package/build/accessibility-info/shared.d.ts +1 -1
- package/build/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/alert/shared.d.ts +1 -1
- package/build/alert/shared.js +3 -7
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/app-state/index.d.ts +1 -1
- package/build/app-state/index.js +35 -27
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +7 -6
- package/build/back-handler/index.js +19 -16
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- package/build/events/index.js +12 -3
- package/build/fabric-props.js +75 -180
- package/build/fabric.d.ts +2 -8
- package/build/fabric.js +27 -33
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +1 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.d.ts +4 -2
- package/build/image-loader.js +24 -48
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +1 -12
- package/build/image-source-write.js +28 -36
- package/build/imperative.d.ts +0 -28
- package/build/imperative.js +42 -92
- package/build/index.d.ts +3 -2
- package/build/index.js +30 -43
- package/build/invariant.d.ts +1 -0
- package/build/invariant.js +10 -0
- package/build/keyboard/index.js +11 -32
- package/build/linking/index.android.js +5 -3
- package/build/linking/shared.d.ts +1 -1
- package/build/linking/shared.js +16 -19
- package/build/mutation-buffer.d.ts +0 -177
- package/build/mutation-buffer.js +137 -308
- package/build/native-engine.d.ts +0 -102
- package/build/native-engine.js +38 -98
- package/build/native-events.d.ts +5 -0
- package/build/native-events.js +30 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +14 -31
- package/build/node.d.ts +0 -212
- package/build/node.js +354 -774
- package/build/permissions-android/index.android.d.ts +59 -0
- package/build/permissions-android/index.android.js +47 -0
- package/build/permissions-android/index.d.ts +1 -115
- package/build/permissions-android/index.ios.d.ts +59 -0
- package/build/permissions-android/index.ios.js +31 -0
- package/build/permissions-android/index.js +3 -184
- package/build/permissions-android/shared.d.ts +63 -0
- package/build/permissions-android/shared.js +66 -0
- package/build/platform/index.android.js +3 -5
- package/build/platform/index.ios.js +3 -3
- package/build/platform/shared.d.ts +4 -0
- package/build/platform/shared.js +9 -0
- package/build/platform-color/index.android.d.ts +4 -0
- package/build/platform-color/index.android.js +7 -0
- package/build/platform-color/index.d.ts +2 -20
- package/build/platform-color/index.js +1 -45
- package/build/platform-color/shared.d.ts +21 -0
- package/build/platform-color/shared.js +41 -0
- package/build/post-commit.js +3 -8
- package/build/process-aspect-ratio.js +3 -7
- package/build/process-background-longhands.js +10 -19
- package/build/process-filter.js +11 -19
- package/build/process-font-variant.js +3 -7
- package/build/registry.d.ts +0 -33
- package/build/registry.js +22 -57
- package/build/report-error.js +4 -18
- package/build/settings/index.android.d.ts +6 -0
- package/build/settings/index.android.js +21 -0
- package/build/settings/index.d.ts +1 -8
- package/build/settings/index.ios.d.ts +8 -0
- package/build/settings/index.ios.js +122 -0
- package/build/settings/index.js +3 -122
- package/build/share/index.android.js +9 -32
- package/build/share/index.ios.js +14 -15
- package/build/share/shared.d.ts +4 -2
- package/build/share/shared.js +7 -10
- package/build/status-bar/index.android.js +1 -1
- package/build/status-bar/index.ios.js +4 -3
- package/build/structured-style.d.ts +0 -9
- package/build/structured-style.js +16 -31
- package/build/styles.js +3 -6
- package/build/surface.d.ts +0 -26
- package/build/surface.js +29 -76
- package/build/text-input-state.js +4 -8
- package/build/toast-android/index.android.d.ts +10 -0
- package/build/toast-android/index.android.js +108 -0
- package/build/toast-android/index.d.ts +1 -10
- package/build/toast-android/index.ios.d.ts +10 -0
- package/build/toast-android/index.ios.js +19 -0
- package/build/toast-android/index.js +3 -108
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +0 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- package/cpp/SymbioteFabricProps.cpp +204 -120
- package/cpp/SymbioteFabricProps.h +5 -0
- package/cpp/SymbioteTree.cpp +20 -6
- package/package.json +2 -2
package/build/mutation-buffer.js
CHANGED
|
@@ -1,70 +1,29 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// Nine operations. It is the mutation API `node.ts` already exports, minus the four that were never
|
|
29
|
-
// primitive:
|
|
30
|
-
//
|
|
31
|
-
// setNodeHidden / setNodePressed resolve a CSS class to a style IN JS and then call setProp.
|
|
32
|
-
// The class registry stays in JS, so what crosses is the result.
|
|
33
|
-
// setEventListener / a listener is a closure and cannot be `folly::dynamic`. It
|
|
34
|
-
// setBehaviorListener stays in a JS registry; what crosses is the boolean gate flag
|
|
35
|
-
// (`GATED_EVENT_PROPS`), which is an ordinary setProp.
|
|
36
|
-
//
|
|
37
|
-
// ── WHAT STAYS IN JS, AND IT IS NOT A TREE ───────────────────────────────────────────────────────
|
|
38
|
-
//
|
|
39
|
-
// the handle per node a bare `{}` carrying native's node on `NativeState`. No parent, no
|
|
40
|
-
// children, no order — an address. Its lifetime IS the node's lifetime,
|
|
41
|
-
// which is how `nativeFabricUIManager` and a browser both work.
|
|
42
|
-
// listeners Map<handle, Map<name, fn>>. Flat.
|
|
43
|
-
// the CSS class registry class -> style, resolved before any op is written. Native never sees a
|
|
44
|
-
// class name.
|
|
45
|
-
// host behaviors the press / text-input / switch machines. They read props back over
|
|
46
|
-
// JSI at GESTURE rate — ~10 reads per touch, not 19 009 per commit.
|
|
47
|
-
//
|
|
48
|
-
// ── WHY PROPS GO NATIVE, WHICH WAS DECIDED THE OTHER WAY FIRST ───────────────────────────────────
|
|
49
|
-
//
|
|
50
|
-
// The first draft of this contract kept prop VALUES in JS and gave native only the structure, on
|
|
51
|
-
// the reasoning that `RawProps` is lazy so handing Fabric a JS object costs nothing. That is wrong.
|
|
52
|
-
// `RawProps` is lazy only until `parse()`, and `ConcreteComponentDescriptor::cloneProps` calls it
|
|
53
|
-
// UNCONDITIONALLY. In `Mode::JSI` the preparse walks every key of every node —
|
|
54
|
-
// `getPropertyNames`, then per key `getValueAtIndex` + `getString` + `utf8()` (a `std::string`
|
|
55
|
-
// allocation) + `getProperty` — and `enableCppPropsIteratorSetter()` defaults to FALSE in 0.86, so
|
|
56
|
-
// that is the live path. `Mode::Dynamic` does the identical work with zero JSI.
|
|
57
|
-
//
|
|
58
|
-
// So a 1 000-row create already pays ~44 001 JSI property reads and as many string allocations
|
|
59
|
-
// INSIDE `createNode`. Props held natively as `folly::dynamic`, built incrementally as `setProp`
|
|
60
|
-
// ops arrive, are cheaper than what ships today, not more expensive.
|
|
61
|
-
/**
|
|
62
|
-
* The opcodes. These numbers ARE the contract: renumbering here without renumbering the C++ commits
|
|
63
|
-
* a different tree, silently, and no test in either language can see across the boundary.
|
|
64
|
-
*
|
|
65
|
-
* Stride is fixed so the ops array is addressable as memory rather than parsed. Operands are either
|
|
66
|
-
* a SLOT (an index into the batch's `handles` array — see below) or an index into a side table.
|
|
67
|
-
*/
|
|
1
|
+
// The contract: native owns the tree, JS emits nothing but a command buffer. This file is the
|
|
2
|
+
// spec — core/engine/cpp/SymbioteTree.cpp must agree with it, since a wire format described in
|
|
3
|
+
// prose gets reimplemented, written as code gets exercised.
|
|
4
|
+
// ── what changed, and why it is smaller than it sounds ───────────────────────────────────────────
|
|
5
|
+
// The adapter used to mutate a JS tree that commit.ts walked to re-derive a diff the framework
|
|
6
|
+
// already knew. The buffer now carries the adapter's alphabet directly: the walk, the diffing,
|
|
7
|
+
// and the JS shadow tree (IMirror) all disappear rather than move.
|
|
8
|
+
// ── the alphabet ─────────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Nine operations — the mutation API node.ts already exports, minus what was never primitive:
|
|
10
|
+
// setNodeHidden/setNodePressed resolve a CSS class to a style in JS then call setProp; a listener
|
|
11
|
+
// can't be folly::dynamic, so setEventListener crosses only its boolean gate flag as a setProp.
|
|
12
|
+
// ── what stays in JS, and it is not a tree ──────────────────────────────────────────────────────
|
|
13
|
+
// The handle per node: a bare {} carrying native's node on NativeState — no parent/children/order,
|
|
14
|
+
// just an address. Listeners and the CSS class registry stay flat maps; host behaviors read props
|
|
15
|
+
// back over JSI at gesture rate, not commit rate.
|
|
16
|
+
// ── why props go native, decided the other way first ────────────────────────────────────────────
|
|
17
|
+
// Keeping prop values in JS assumed RawProps is lazy so handing Fabric a JS object costs nothing —
|
|
18
|
+
// wrong: ConcreteComponentDescriptor::cloneProps calls parse() unconditionally, and the preparse
|
|
19
|
+
// walks every key of every node through JSI regardless of mode.
|
|
20
|
+
// So a create already pays a JSI property read and a string allocation per prop, per node, inside
|
|
21
|
+
// createNode. Props held natively as folly::dynamic, built incrementally as setProp ops arrive,
|
|
22
|
+
// are cheaper than what shipped before, not more expensive.
|
|
23
|
+
// The opcodes. These numbers ARE the contract: renumbering here without renumbering the C++ side
|
|
24
|
+
// commits a different tree, silently, and no test in either language can see across the boundary.
|
|
25
|
+
// Stride is fixed so the ops array is addressable as memory rather than parsed. Operands are
|
|
26
|
+
// either a slot (an index into the batch's handles array) or an index into a side table.
|
|
68
27
|
export const OP_STRIDE = 6;
|
|
69
28
|
export const OP_CREATE_ELEMENT = 0; // [slot, viewName, isText, tag, instanceHandle]
|
|
70
29
|
export const OP_CREATE_RAW_TEXT = 1; // [slot, text]
|
|
@@ -76,107 +35,54 @@ export const OP_SET_PROP = 6; // [slot, key, value] — value = NO_VALUE deletes
|
|
|
76
35
|
export const OP_SET_TEXT = 7; // [slot, text]
|
|
77
36
|
export const OP_COMMIT = 8; // [rootTag, surface]
|
|
78
37
|
export const OP_SET_COMPONENT = 9; // [slot, viewName]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*
|
|
85
|
-
* WHY THE TAG HAS TO CROSS AT ALL. A tag's platform props are resolved natively now, and the native
|
|
86
|
-
* side keys them off what it knows the node IS. For `<text-input>` the Fabric view name answers
|
|
87
|
-
* that by itself (`RCTSinglelineTextInputView` names nothing else); for `<pressable>` it does not —
|
|
88
|
-
* it commits as `RCTView`, byte-identical to a plain view. The tag is the only fact that separates
|
|
89
|
-
* them, and it is the same fact a browser keys user-agent behavior off.
|
|
90
|
-
*/
|
|
38
|
+
// The intrinsic tag this node came from — pressable, not RCTView. [slot, tag]. Emitted only for a
|
|
39
|
+
// node a host behavior actually attached to; every other node pays nothing.
|
|
40
|
+
// Native's platform-prop rules key off what the node IS, and the Fabric view name alone doesn't
|
|
41
|
+
// always answer that — pressable commits as RCTView, byte-identical to a plain view, so the tag
|
|
42
|
+
// is the only fact that separates them.
|
|
91
43
|
export const OP_SET_TAG = 10; // [slot, tag]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* `[slot, name, present]`. See `recordSetOwnedListener` for why the bit crosses and the closure
|
|
95
|
-
* does not.
|
|
96
|
-
*/
|
|
44
|
+
// An app callback appeared on, or disappeared from, an event name the behavior owns.
|
|
45
|
+
// [slot, name, present]. See recordSetOwnedListener for why the bit crosses, not the closure.
|
|
97
46
|
export const OP_SET_OWNED_LISTENER = 11; // [slot, name, present]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* the one control whose feedback does NOT track the press exactly: RN holds the underlay past
|
|
107
|
-
* release so a fast tap still flashes (`TouchableHighlight.js:270-293`).
|
|
108
|
-
*
|
|
109
|
-
* THE TIMER STAYS IN JS and that is the boundary, not an omission. WHEN the bit flips is Pressability
|
|
110
|
-
* plus a `delayPressOut` hold, running at gesture rate and calling back into app code
|
|
111
|
-
* (`onShowUnderlay` / `onHideUnderlay`). WHAT a showing underlay looks like — a background colour and
|
|
112
|
-
* a dimmed child, from two props no ViewConfig declares — is the platform's, and it is
|
|
113
|
-
* `foldTouchableHighlightUnderlay` now.
|
|
114
|
-
*
|
|
115
|
-
* A flip is a GESTURE-rate event rather than a per-render one: twice a tap, against a `payloadFold`
|
|
116
|
-
* that was charged on every commit the node was dirty in for the life of the screen.
|
|
117
|
-
*/
|
|
47
|
+
// A behavior's feedback state flipped — TouchableHighlight's underlay is showing, or stopped.
|
|
48
|
+
// [slot, shown]. The second bit to cross for the same reason the first did: OP_SET_OWNED_LISTENER
|
|
49
|
+
// carries whether the app wired a handler, this carries whether the control is giving feedback.
|
|
50
|
+
// The timer stays in JS, and that's the boundary, not an omission: WHEN the bit flips is
|
|
51
|
+
// Pressability plus a delayPressOut hold at gesture rate; WHAT a showing underlay looks like is
|
|
52
|
+
// the platform's (foldTouchableHighlightUnderlay).
|
|
53
|
+
// A flip is a gesture-rate event rather than a per-render one — twice a tap, against a payloadFold
|
|
54
|
+
// that used to be charged on every commit the node was dirty in for the life of the screen.
|
|
118
55
|
export const OP_SET_UNDERLAY_SHOWN = 12; // [slot, shown]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* contributes nothing to the host tree. An anchor cannot express that — it exists precisely to hand
|
|
125
|
-
* its children up in its own place — so a node whose entire subtree must vanish from Fabric needs
|
|
126
|
-
* its own kind. Everything else about it (the retained JS tree, `appendChild`, adapter bookkeeping)
|
|
127
|
-
* is unaffected; only the commit walk treats it as contributing zero Fabric nodes, recursively.
|
|
128
|
-
*/
|
|
56
|
+
// A void node: commits nothing of its own and does not hoist its children into Fabric either —
|
|
57
|
+
// unlike an anchor, which hoists. [slot]. InputAccessoryView.js renders null on Android, so the
|
|
58
|
+
// whole component (children included) must vanish, which an anchor can't express.
|
|
59
|
+
// Everything else about it (the retained JS tree, appendChild, adapter bookkeeping) is unaffected;
|
|
60
|
+
// only the commit walk treats it as contributing zero Fabric nodes, recursively.
|
|
129
61
|
export const OP_CREATE_VOID = 13; // [slot]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* `undefined` cannot carry it: `null` is a legitimate Fabric prop value meaning "reset to the
|
|
134
|
-
* default", and the two must stay distinguishable — `cloneNodeWithNewProps` merges, so a removed
|
|
135
|
-
* key has to be sent as an explicit `null` while a key that was never there must not be sent at
|
|
136
|
-
* all. The JS `setProp` already collapses `undefined` to a delete before anything is encoded.
|
|
137
|
-
*/
|
|
62
|
+
// A setProp whose value slot is this deletes the key. undefined can't carry it: null is a
|
|
63
|
+
// legitimate Fabric prop value meaning "reset to default", and the two must stay distinguishable
|
|
64
|
+
// since cloneNodeWithNewProps merges — a removed key must be sent as explicit null, never omitted.
|
|
138
65
|
export const NO_VALUE = -1;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* an empty one would paint.
|
|
149
|
-
* ANCHOR a position marker the frameworks insert (`{#if}`, a fragment, a `{@render}` slot).
|
|
150
|
-
* It never becomes a Fabric node at all: native keeps it in ITS OWN structure so
|
|
151
|
-
* `insertBefore(parent, node, anchor)` resolves, and hoists its children into its
|
|
152
|
-
* parent's child list when building the child set. This is why our own store cannot BE
|
|
153
|
-
* the Fabric tree — there is no Fabric node to hold an anchor, which is the one thing
|
|
154
|
-
* `IMirror` was genuinely for.
|
|
155
|
-
*
|
|
156
|
-
* A SURFACE is an anchor too, and that is not a trick: an anchor is a node whose children belong to
|
|
157
|
-
* its parent's list, and a surface is a node whose children belong to the root's child set. Same
|
|
158
|
-
* shape, so `OP_COMMIT` names one and the native side needs no `rootTag -> node` map — which keeps
|
|
159
|
-
* the applier stateless, the property the whole lifetime design rests on.
|
|
160
|
-
*/
|
|
66
|
+
// A node kind. Native needs it because two of the three never become a Fabric node the same way,
|
|
67
|
+
// decided at insert or child-set build, never by a walk. ELEMENT is an ordinary view, except a
|
|
68
|
+
// text inside another text commits as RCTVirtualText, resolved when it acquires a parent.
|
|
69
|
+
// RAW_TEXT is an RCTRawText leaf, skipped from its parent's child set when empty. ANCHOR never
|
|
70
|
+
// becomes a Fabric node: native hoists its children into its parent's child set instead, which is
|
|
71
|
+
// why our own store cannot BE the Fabric tree.
|
|
72
|
+
// A surface is an anchor too: its children belong to the root's child set the same way an
|
|
73
|
+
// anchor's belong to its parent's list, so OP_COMMIT names one and native needs no rootTag -> node
|
|
74
|
+
// map — keeping the applier stateless.
|
|
161
75
|
export const KIND_ELEMENT = 0;
|
|
162
76
|
export const KIND_RAW_TEXT = 1;
|
|
163
77
|
export const KIND_ANCHOR = 2;
|
|
164
|
-
// ──
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// This was a `number[]` and `takeBatch` ended in `Int32Array.from(ops)`. Measured on a 1 000-row
|
|
173
|
-
// create through the work ledger: **210 042 slots**, every one of them converted element by element
|
|
174
|
-
// through the ITERATOR PROTOCOL on every commit. `new Int32Array(array)` is not the escape — it
|
|
175
|
-
// takes the same path, checked rather than assumed (F-55). The escape is not building an Array.
|
|
176
|
-
//
|
|
177
|
-
// Capacity DOUBLES and is never given back: a commit that needed 210 042 slots once will need them
|
|
178
|
-
// again, and re-growing from a small start would pay the same copies every commit for the memory of
|
|
179
|
-
// a single benchmark row list.
|
|
78
|
+
// ── the recorder ─────────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// One buffer per process, drained by takeBatch() at commit. Not per surface: a node belongs to
|
|
80
|
+
// exactly one surface, so ops for another surface are inert until that surface commits.
|
|
81
|
+
// Typed from the start, not a number[] converted at drain time — a plain array walks every element
|
|
82
|
+
// through the iterator protocol on every commit, and Int32Array.from takes the same path. The
|
|
83
|
+
// escape is not building an Array at all.
|
|
84
|
+
// Capacity doubles and is never given back: a commit that once needed this many slots will need
|
|
85
|
+
// them again, and re-growing from a small start would pay the same copies every commit.
|
|
180
86
|
const INITIAL_OP_CAPACITY = 1_024;
|
|
181
87
|
let ops = new Int32Array(INITIAL_OP_CAPACITY);
|
|
182
88
|
let opCount = 0;
|
|
@@ -187,16 +93,14 @@ let handles = [];
|
|
|
187
93
|
// Interning matters more here than it looks: a 1 000-row create emits about a dozen distinct view
|
|
188
94
|
// names across 10 000 elements, and every prop KEY is drawn from a set of a few hundred.
|
|
189
95
|
const stringIds = new Map();
|
|
190
|
-
// Which batch the
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// live batch and needs no separate "is it in this batch" flag.
|
|
96
|
+
// Which batch the slot standing on a handle belongs to. Bumped by takeBatch, which invalidates
|
|
97
|
+
// every slot at once without walking the handles that hold them.
|
|
98
|
+
// Starts at 1 because a fresh node's slotBatch is 0, so an untouched handle can never match a live
|
|
99
|
+
// batch and needs no separate "is it in this batch" flag.
|
|
195
100
|
let batchId = 1;
|
|
196
|
-
// The same table for prop
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// 12 005 `setProp` ops spent 20-29 ms converting inside a 35 ms `applyOps`.
|
|
101
|
+
// The same table for prop values, and it pays off on the far side: the host converts each entry
|
|
102
|
+
// to folly::dynamic when the op is applied, so a style object reused across many rows was many
|
|
103
|
+
// conversions of the same object without it.
|
|
200
104
|
const valueIds = new Map();
|
|
201
105
|
// Booleans skip that table entirely — see `internValue`. Two slots, reset with the batch alongside
|
|
202
106
|
// everything else the tables hold.
|
|
@@ -211,31 +115,17 @@ function intern(text) {
|
|
|
211
115
|
stringIds.set(text, strings.length - 1);
|
|
212
116
|
return strings.length - 1;
|
|
213
117
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* holds. Measured on the create fixture, three adapters seed `allowFontScaling: true` at
|
|
226
|
-
* `createElement`, which alone wrote one entry per text node on the screen.
|
|
227
|
-
*
|
|
228
|
-
* NUMBERS STAY OUT. They would need the `Map`, whose keys compare by SameValueZero — that folds
|
|
229
|
-
* `-0` into `0` and `NaN` into itself, and a value this cheap to convert is not worth opening the
|
|
230
|
-
* question for.
|
|
231
|
-
*
|
|
232
|
-
* Identity, never structural equality: comparing deeply would make the buffer's cost depend on the
|
|
233
|
-
* size of what it is handed, which is the opposite of the point.
|
|
234
|
-
*
|
|
235
|
-
* Reusing an index is safe against MUTATION of the value between two ops, and not by luck — the host
|
|
236
|
-
* converts at apply time, after the batch has closed, so both ops already saw the object's final
|
|
237
|
-
* state whether they shared an index or not.
|
|
238
|
-
*/
|
|
118
|
+
// The index the ops address this value by — deduplicated when it is worth deduplicating. Objects,
|
|
119
|
+
// functions and strings go through the Map: they repeat (one StyleSheet.create object per screen)
|
|
120
|
+
// and their conversion costs something.
|
|
121
|
+
// Booleans are folded without the Map, since there are only two of them — a dedicated slot each is
|
|
122
|
+
// a branch rather than a hash. The conversion was never the whole cost either: values is a JSI
|
|
123
|
+
// array the host reads entry by entry, so a duplicate is a crossing whatever it holds.
|
|
124
|
+
// Numbers stay out: they'd need the Map, whose keys compare by SameValueZero (folding -0 into 0,
|
|
125
|
+
// NaN into itself), and a value this cheap to convert isn't worth opening that question for.
|
|
126
|
+
// Identity, never structural equality — comparing deeply would make the buffer's cost depend on
|
|
127
|
+
// the size of what it's handed. Reusing an index is safe against mutation between two ops since
|
|
128
|
+
// the host converts at apply time, after the batch closes, when both already reflect final state.
|
|
239
129
|
function internValue(value) {
|
|
240
130
|
if (value === true) {
|
|
241
131
|
if (trueId === NOT_INTERNED)
|
|
@@ -264,41 +154,22 @@ function pushValue(value) {
|
|
|
264
154
|
values.push(value);
|
|
265
155
|
return values.length - 1;
|
|
266
156
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* two, so a thousand-row create mentions handles about forty thousand times — and a `Map<object,
|
|
277
|
-
* number>` charges a hash for each one, plus a second for the `set` on a miss. Two fields on a shape
|
|
278
|
-
* the node already carries turn that into a compare.
|
|
279
|
-
*
|
|
280
|
-
* Measured on `build-release` by `mutation-api-fill-cost.itest.ts`, both arms in one sitting, three
|
|
281
|
-
* runs each: `createRawText` 0.70 -> 0.51 us, `appendChild` 0.65 -> 0.58, `recordSetProp` 0.34 ->
|
|
282
|
-
* 0.30, and the whole `fill` phase of a 10 001-node create 25.0 -> 23.2 ms. `createElement` barely
|
|
283
|
-
* moved (0.72 -> 0.70), which is the control: it does enough else that one lookup is noise in it.
|
|
284
|
-
*
|
|
285
|
-
* WHY AN EPOCH RATHER THAN CLEARING: a slot is meaningless outside its batch (see `IMutationBatch`),
|
|
286
|
-
* so every slot must die when the batch drains. Walking the handles to reset them would cost exactly
|
|
287
|
-
* what the `Map.clear` cost; bumping one counter invalidates all of them at once, and a handle that
|
|
288
|
-
* is never mentioned again is never touched.
|
|
289
|
-
*/
|
|
157
|
+
// The index the ops address this handle by, for the duration of THIS batch. Assigned on first
|
|
158
|
+
// mention rather than at creation, so a batch carries exactly the nodes it names — a handle from
|
|
159
|
+
// an earlier batch arrives already owning its native node.
|
|
160
|
+
// The slot lives on the handle, not in a Map keyed by it, a measured decision: this runs once per
|
|
161
|
+
// handle operand, and a Map<object, number> charges a hash per lookup plus a set on a miss. Two
|
|
162
|
+
// fields on a shape the node already carries turn that into a compare.
|
|
163
|
+
// An epoch rather than clearing: a slot is meaningless outside its batch, so every slot must die
|
|
164
|
+
// on drain. Walking handles to reset them costs what Map.clear costs; bumping one counter
|
|
165
|
+
// invalidates all of them at once, and an unmentioned handle is never touched.
|
|
290
166
|
function slotOf(handle) {
|
|
291
|
-
//
|
|
292
|
-
// -1
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
// Deliberately only "an object", not "one of OUR nodes": a handle is an identity to this file and
|
|
299
|
-
// nothing more, and the applier's own fixtures address bare `{}`. The wrong-KIND-of-object case
|
|
300
|
-
// (Angular handing over a `SymbioteSurface` at teardown) is caught one layer on, where the host
|
|
301
|
-
// fails to resolve it and can name the opcode.
|
|
167
|
+
// A buffer cannot silently absorb a handle that isn't a node the way the old retained tree did
|
|
168
|
+
// (children.indexOf(x) === -1, no-op) — the op is recorded, and the failure surfaces in the
|
|
169
|
+
// host, on a later op, as a node whose create it never saw.
|
|
170
|
+
// Deliberately only "an object", not "one of our nodes": a handle is an identity to this file and
|
|
171
|
+
// nothing more. The wrong-kind-of-object case is caught one layer on, where the host fails to
|
|
172
|
+
// resolve it and can name the opcode.
|
|
302
173
|
if (typeof handle !== 'object' || handle === null) {
|
|
303
174
|
throw new Error(`symbiote engine: a mutation named ${String(handle)}, which is not a node. An adapter is ` +
|
|
304
175
|
`passing a framework sentinel straight through — an absent insert anchor is spelled by ` +
|
|
@@ -311,15 +182,10 @@ function slotOf(handle) {
|
|
|
311
182
|
handle.slotBatch = batchId;
|
|
312
183
|
return handle.slot;
|
|
313
184
|
}
|
|
314
|
-
// Has anything changed the
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// empties the buffer many times between commits. `hasPendingOps()` then answers "no" for a surface
|
|
319
|
-
// with a screen's worth of unpublished work. This survives the drain and is cleared only by a
|
|
320
|
-
// commit, which is what "is there anything to publish" actually means.
|
|
321
|
-
//
|
|
322
|
-
// `OP_COMMIT` is excluded deliberately: recording a commit is not a change to the tree, and counting
|
|
185
|
+
// Has anything changed the tree since the last commit drained? Not the same question as
|
|
186
|
+
// hasPendingOps(): every structural read calls flushOps, which empties the buffer many times
|
|
187
|
+
// between commits, so hasPendingOps() alone would answer "no" for unpublished work.
|
|
188
|
+
// OP_COMMIT is excluded deliberately: recording a commit is not a change to the tree, and counting
|
|
323
189
|
// it would make every commit look like it had work.
|
|
324
190
|
let changedSinceCommit = false;
|
|
325
191
|
function push(op, a = 0, b = 0, c = 0, d = 0, e = 0) {
|
|
@@ -338,44 +204,32 @@ function push(op, a = 0, b = 0, c = 0, d = 0, e = 0) {
|
|
|
338
204
|
ops[opCount + 5] = e;
|
|
339
205
|
opCount += OP_STRIDE;
|
|
340
206
|
}
|
|
341
|
-
|
|
207
|
+
// Whether a commit would publish anything. See changedSinceCommit.
|
|
342
208
|
export function hasChangedSinceCommit() {
|
|
343
209
|
return changedSinceCommit;
|
|
344
210
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
* already been published.
|
|
349
|
-
*/
|
|
211
|
+
// Called by commitSurfaceOps once it has drained. Separate from takeBatch because a read drains
|
|
212
|
+
// too, and a read is not a commit — clearing there would make the next commit believe its work had
|
|
213
|
+
// already been published.
|
|
350
214
|
export function noteCommitDrained() {
|
|
351
215
|
changedSinceCommit = false;
|
|
352
216
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
* be skipped, and the derived payload would sit unpublished until something unrelated changed.
|
|
357
|
-
*/
|
|
217
|
+
// The one dirtying route that writes no op: a behavior with a derived payload asks the host to
|
|
218
|
+
// rebuild a node the buffer never named. Without this the commit that follows would look idle and
|
|
219
|
+
// be skipped, and the derived payload would sit unpublished until something unrelated changed.
|
|
358
220
|
export function noteHostSideChange() {
|
|
359
221
|
changedSinceCommit = true;
|
|
360
222
|
}
|
|
361
|
-
// Nodes whose
|
|
362
|
-
// structural op.
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// same parent in the host as it has here.
|
|
372
|
-
//
|
|
373
|
-
// Creation is in it because the host cannot answer about a node it has never been told exists, which
|
|
374
|
-
// is the other half — and the half a "was it re-parented" set alone would get wrong.
|
|
375
|
-
//
|
|
376
|
-
// NOT A TREE, and that is the line this has to stay on the right side of (`node.ts:2` — no parent,
|
|
377
|
-
// no children, no mirror). It answers about the BUFFER: is this node's placement unpublished. It is
|
|
378
|
-
// emptied by `takeBatch`, so it never outlives one batch and can never disagree with the host.
|
|
223
|
+
// Nodes whose placement this batch has not published yet — created, or named as the child of a
|
|
224
|
+
// structural op. Exists so a read doesn't have to drain: parentOf is unconditional otherwise, and
|
|
225
|
+
// most reads never touch a node the pending batch has.
|
|
226
|
+
// Sound because a node's parent link changes only through an op that names that node as the child
|
|
227
|
+
// — `before` on an insert is a position reference and moves nothing, so a handle absent from here
|
|
228
|
+
// has the same parent in the host as it has here.
|
|
229
|
+
// Creation is in it too, since the host can't answer about a node it's never been told exists — the
|
|
230
|
+
// half a "was it re-parented" set alone would get wrong.
|
|
231
|
+
// Not a tree: it answers about the buffer, whether this node's placement is unpublished. Emptied
|
|
232
|
+
// by takeBatch, so it never outlives one batch and can never disagree with the host.
|
|
379
233
|
let placementPending = new Set();
|
|
380
234
|
/** Does the pending batch hold anything that could change what the host says this node's parent is? */
|
|
381
235
|
export function hasPendingPlacement(handle) {
|
|
@@ -421,42 +275,24 @@ export function recordSetProp(handle, key, value) {
|
|
|
421
275
|
export function recordSetText(handle, text) {
|
|
422
276
|
push(OP_SET_TEXT, slotOf(handle), intern(text));
|
|
423
277
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
* moves a node between native views, so the host has to RE-CREATE the node under the new name and
|
|
430
|
-
* re-parent its children — which is the same path a reparent already takes, and why this needs no
|
|
431
|
-
* new machinery on the far side beyond honouring the name.
|
|
432
|
-
*
|
|
433
|
-
* A JS-only workaround was the alternative and it is the one thing this design rules out: to know
|
|
434
|
-
* what to rebuild, JS would have to hold the tree.
|
|
435
|
-
*/
|
|
278
|
+
// Change a node's Fabric view name after creation. Exists for one thing: TextInput's multiline
|
|
279
|
+
// decides between RCTSinglelineTextInputView and RCTMultilineTextInputView, and an app can flip it.
|
|
280
|
+
// No prop write moves a node between native views, so the host re-creates it under the new name
|
|
281
|
+
// and re-parents its children. A JS-only workaround would need JS to hold the tree — the one thing
|
|
282
|
+
// this design rules out.
|
|
436
283
|
export function recordSetComponent(handle, viewName) {
|
|
437
284
|
push(OP_SET_COMPONENT, slotOf(handle), intern(viewName));
|
|
438
285
|
}
|
|
439
286
|
export function recordSetTag(handle, tag) {
|
|
440
287
|
push(OP_SET_TAG, slotOf(handle), intern(tag));
|
|
441
288
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
* (`TouchableOpacity.js:336-339`), two props and one thing only JS knew. This is the one bit that
|
|
450
|
-
* closes that gap.
|
|
451
|
-
*
|
|
452
|
-
* The browser is the argument rather than convenience: a UA computes focusability itself and CAN,
|
|
453
|
-
* because `addEventListener` is its own API — it knows which elements carry a click handler, while
|
|
454
|
-
* the handler's body stays the application's. Same split.
|
|
455
|
-
*
|
|
456
|
-
* Emitted on a FLIP only, from `setEventListener`, which already refuses to notify on listener
|
|
457
|
-
* identity because a framework hands a fresh closure nearly every render. So this is a mount-time
|
|
458
|
-
* op, not a per-render one — against the per-commit fold it replaces.
|
|
459
|
-
*/
|
|
289
|
+
// Whether an app callback is currently wired to an event name the behavior owns. [slot, name,
|
|
290
|
+
// present]. The existence, never the function — a name a behavior owns is diverted into a JS
|
|
291
|
+
// stash and never becomes a prop, so native needs this bit to compute focusable itself.
|
|
292
|
+
// The browser is the precedent: a UA computes focusability itself because addEventListener is its
|
|
293
|
+
// own API, so it knows which elements carry a handler while the handler's body stays the app's.
|
|
294
|
+
// Emitted on a flip only, from setEventListener, which already ignores listener identity since a
|
|
295
|
+
// framework hands a fresh closure nearly every render — a mount-time op, not a per-render one.
|
|
460
296
|
export function recordSetOwnedListener(handle, name, isPresent) {
|
|
461
297
|
push(OP_SET_OWNED_LISTENER, slotOf(handle), intern(name), isPresent ? 1 : 0);
|
|
462
298
|
}
|
|
@@ -471,18 +307,11 @@ export function recordCommit(rootTag, surface) {
|
|
|
471
307
|
export function hasPendingOps() {
|
|
472
308
|
return opCount > 0;
|
|
473
309
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
* mutation the adapter makes.
|
|
480
|
-
*
|
|
481
|
-
* `slice` for the ops and not `subarray` for exactly that reason — a subarray would share the
|
|
482
|
-
* backing store the very next `push` writes into. It is a typed-array copy rather than the
|
|
483
|
-
* element-by-element iterator walk this used to be, and the ops buffer itself is KEPT so its
|
|
484
|
-
* capacity survives the drain.
|
|
485
|
-
*/
|
|
310
|
+
// Drain the buffer. Fresh arrays rather than reused ones: the batch outlives this call on the
|
|
311
|
+
// native path (applyOps reads handles while attaching state), and a recycled array would be
|
|
312
|
+
// mutated under it by the next mutation the adapter makes.
|
|
313
|
+
// slice for the ops, not subarray, for the same reason — subarray would share the backing store
|
|
314
|
+
// the very next push writes into. The ops buffer itself is kept so its capacity survives the drain.
|
|
486
315
|
export function takeBatch() {
|
|
487
316
|
const batch = {
|
|
488
317
|
ops: ops.slice(0, opCount),
|