@symbiote-native/engine 0.4.0 → 1.0.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/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -7
- package/build/accessibility-props.js +22 -21
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/graph.d.ts +2 -0
- package/build/animated/graph.js +14 -0
- package/build/animated/host-binding.d.ts +39 -0
- package/build/animated/host-binding.js +278 -0
- package/build/animated/index.d.ts +1 -1
- package/build/animated/leaf-lifecycle.js +10 -22
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +123 -33
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +129 -182
- package/build/fabric.d.ts +10 -0
- package/build/fabric.js +40 -0
- package/build/host-access.d.ts +125 -0
- package/build/host-access.js +280 -0
- package/build/host-behavior.d.ts +107 -7
- package/build/host-behavior.js +309 -31
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +18 -10
- package/build/index.js +65 -10
- package/build/mutation-buffer.d.ts +222 -0
- package/build/mutation-buffer.js +491 -0
- package/build/native-engine.d.ts +182 -0
- package/build/native-engine.js +178 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +66 -0
- package/build/node.d.ts +191 -60
- package/build/node.js +1034 -327
- package/build/pan-responder/index.d.ts +2 -2
- package/build/pan-responder/index.js +37 -56
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/styles.d.ts +5 -1
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -42
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +307 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +232 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2478 -0
- package/cpp/SymbioteTree.h +257 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1030
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
|
|
5
|
+
namespace symbiote {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The shadow tree, owned by NATIVE. JS emits nothing but a command buffer.
|
|
9
|
+
*
|
|
10
|
+
* The spec is `core/engine/src/mutation-buffer.ts` and this file must agree with it — the opcode
|
|
11
|
+
* numbers are the contract, and renumbering on one side commits a different tree in silence.
|
|
12
|
+
*
|
|
13
|
+
* ── WHAT THIS REPLACES, AND WHY IT IS SMALLER THAN WHAT IT REPLACES ──────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* `SymbioteApplier` sat beside this file until 2026-09-08 and is deleted. It replayed FABRIC
|
|
16
|
+
* operations that a 2 041-line JS walk worked out — and that walk existed to re-derive a diff the
|
|
17
|
+
* framework had already computed and thrown away: a reconciler's whole job is knowing what changed,
|
|
18
|
+
* and every adapter tells us call by call.
|
|
19
|
+
*
|
|
20
|
+
* So this takes the ADAPTER's alphabet instead, and the derivation does not move here — it stops
|
|
21
|
+
* existing. What was 2 041 lines of JS diffing is, on this side, a child vector and a dirty flag.
|
|
22
|
+
* Three rules survive the collapse, and all three are two lines each rather than a walk:
|
|
23
|
+
*
|
|
24
|
+
* a text element inside a text element commits as `RCTVirtualText`. Decided when a node acquires
|
|
25
|
+
* a PARENT, because that is when the answer first exists, and re-decided on a reparent.
|
|
26
|
+
*
|
|
27
|
+
* an anchor and an empty raw-text node are skipped from their parent's child set — an anchor
|
|
28
|
+
* hoisting its own children up in its place. Decided when the set is built.
|
|
29
|
+
*
|
|
30
|
+
* `cloneNodeWithNewProps` MERGES rather than replaces, so a clone's payload must be a minimal
|
|
31
|
+
* diff with vanished keys sent as `null`. Cheaper here than in JS: we already know which keys an
|
|
32
|
+
* op touched, so nothing has to be compared.
|
|
33
|
+
*
|
|
34
|
+
* ── LIFETIME: THIS CLASS HOLDS NOTHING ───────────────────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* A node's owner is the JS handle. `applyOps` attaches each created node to the placeholder object
|
|
37
|
+
* the adapter is already holding, as JSI `NativeState`, so the node lives exactly as long as that
|
|
38
|
+
* object and Hermes' collector is what frees it. A node also stays alive while a PARENT holds it,
|
|
39
|
+
* which is the browser's rule exactly: alive while in the tree or referenced from script.
|
|
40
|
+
*
|
|
41
|
+
* That is not a preference. The previous applier kept an `unordered_map<int32_t, shared_ptr>` and
|
|
42
|
+
* addressed nodes by a monotonic id — correct, and a second owner with nothing to tell it when the
|
|
43
|
+
* first one let go. One benchmark suite took it from 792 MB to 1492 MB. A `weak_ptr` table was
|
|
44
|
+
* tried and died on device on the first commit (`node 1 is gone`): Fabric's state reconciliation
|
|
45
|
+
* clones a stateful node AND THE PATH TO THE ROOT, so any `ScrollView` re-creates the root every
|
|
46
|
+
* commit while the JS side legitimately still names it. The repair is not a better table. It is
|
|
47
|
+
* having one owner.
|
|
48
|
+
*
|
|
49
|
+
* Hence: no members. `Tree` exists to give the host functions a `this`, and a per-call instance
|
|
50
|
+
* would behave identically.
|
|
51
|
+
*/
|
|
52
|
+
class Tree {
|
|
53
|
+
public:
|
|
54
|
+
/**
|
|
55
|
+
* `applyOps(ops, strings, values, instanceHandles, handles)`.
|
|
56
|
+
*
|
|
57
|
+
* `ops` is an `Int32Array` read as MEMORY — the commands never become JS values. The side tables
|
|
58
|
+
* carry what JSI has to marshal either way, and `handles` travels OUT: it holds the placeholder
|
|
59
|
+
* object for every slot the ops address, and a created node is attached to the object at its own
|
|
60
|
+
* slot. A slot is an index into THIS batch and is meaningless outside it, deliberately — an id
|
|
61
|
+
* that outlives a batch is what forces a table on this side.
|
|
62
|
+
*
|
|
63
|
+
* Prop values are converted to `folly::dynamic` ONCE, here, and never marshalled again. That is
|
|
64
|
+
* strictly cheaper than what ships today: `ConcreteComponentDescriptor::cloneProps` calls
|
|
65
|
+
* `RawProps::parse` unconditionally, and in `Mode::JSI` the preparse walks every key of every
|
|
66
|
+
* node with a `std::string` allocation per key — ~44 001 of each on a 1 000-row create, inside
|
|
67
|
+
* `createNode`. `Mode::Dynamic` does the identical work with no JSI at all.
|
|
68
|
+
*/
|
|
69
|
+
facebook::jsi::Value applyOps(
|
|
70
|
+
facebook::jsi::Runtime &runtime,
|
|
71
|
+
const facebook::jsi::Value *arguments,
|
|
72
|
+
size_t count);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The five READS JS makes, and their oracle is `core/test-utils/src/tree-applier.ts` — the
|
|
76
|
+
* TypeScript half of this file, which ~5 500 tests drive. Each one below mirrors an export there.
|
|
77
|
+
*
|
|
78
|
+
* Two are VALUE reads. The host behaviors (the press, text-input and switch machines) run in JS
|
|
79
|
+
* and must see the props they react to; `getViewName` exists on top of that because it may answer
|
|
80
|
+
* something the adapter did not choose — see the virtual-text rule above.
|
|
81
|
+
*
|
|
82
|
+
* Three are STRUCTURAL, and they are their seams' contract rather than our choice: Solid's nodeOps
|
|
83
|
+
* declare getParentNode / getFirstChild / getNextSibling, Vue's RendererOptions and Angular's
|
|
84
|
+
* Renderer2 declare a parent/sibling pair, and Svelte's compiled output reaches `firstChild` as a
|
|
85
|
+
* real prototype getter. Fabric answers none of them — `nativeFabricUIManager` exposes no
|
|
86
|
+
* structural read, and `NativeDOM` answers against the CURRENT REVISION, which is never the tree a
|
|
87
|
+
* reconciler is mid-way through building.
|
|
88
|
+
*
|
|
89
|
+
* All five run at GESTURE or lifecycle rate rather than commit rate, which is what makes the
|
|
90
|
+
* crossing irrelevant: ~10 reads per touch against the 19 009 per commit this design exists to
|
|
91
|
+
* remove.
|
|
92
|
+
*
|
|
93
|
+
* `census` is deliberately NOT here. It has one JS caller, it is diagnostics, and answering it
|
|
94
|
+
* would cost a full native walk of the very tree this design exists to stop walking.
|
|
95
|
+
*/
|
|
96
|
+
facebook::jsi::Value getProp(
|
|
97
|
+
facebook::jsi::Runtime &runtime,
|
|
98
|
+
const facebook::jsi::Value *arguments,
|
|
99
|
+
size_t count);
|
|
100
|
+
/** `getProps(handle)` — every prop standing on the node, in one crossing. */
|
|
101
|
+
facebook::jsi::Value getProps(
|
|
102
|
+
facebook::jsi::Runtime &runtime,
|
|
103
|
+
const facebook::jsi::Value *arguments,
|
|
104
|
+
size_t count);
|
|
105
|
+
/** `markPropsDirty(handle)` — the dirtying route a DERIVED payload has, since it writes no prop. */
|
|
106
|
+
facebook::jsi::Value markPropsDirty(
|
|
107
|
+
facebook::jsi::Runtime &runtime,
|
|
108
|
+
const facebook::jsi::Value *arguments,
|
|
109
|
+
size_t count);
|
|
110
|
+
facebook::jsi::Value getViewName(
|
|
111
|
+
facebook::jsi::Runtime &runtime,
|
|
112
|
+
const facebook::jsi::Value *arguments,
|
|
113
|
+
size_t count);
|
|
114
|
+
/** `parentOf(handle)` — the node's OWN parent, a surface included. */
|
|
115
|
+
facebook::jsi::Value parentOf(
|
|
116
|
+
facebook::jsi::Runtime &runtime,
|
|
117
|
+
const facebook::jsi::Value *arguments,
|
|
118
|
+
size_t count);
|
|
119
|
+
/**
|
|
120
|
+
* `nextSiblingOf(handle)` — the next entry in the parent's child list, anchors included.
|
|
121
|
+
*
|
|
122
|
+
* Its own call because the JS spelling was `parentOf` plus a whole `childrenOf`, which a keyed
|
|
123
|
+
* patch makes quadratic: 1 002 001 handles crossed on a 1 000-row Vue append.
|
|
124
|
+
*/
|
|
125
|
+
facebook::jsi::Value nextSiblingOf(
|
|
126
|
+
facebook::jsi::Runtime &runtime,
|
|
127
|
+
const facebook::jsi::Value *arguments,
|
|
128
|
+
size_t count);
|
|
129
|
+
/** `childrenOf(handle)` — in order, ANCHORS INCLUDED. */
|
|
130
|
+
facebook::jsi::Value childrenOf(
|
|
131
|
+
facebook::jsi::Runtime &runtime,
|
|
132
|
+
const facebook::jsi::Value *arguments,
|
|
133
|
+
size_t count);
|
|
134
|
+
/**
|
|
135
|
+
* `parentsOf(handles)` / `subtreesOf(roots)` — the batched twins of the two reads above, each
|
|
136
|
+
* answering for a whole list in ONE crossing.
|
|
137
|
+
*
|
|
138
|
+
* They are members rather than a JS loop because the teardown sweep is the one structural read
|
|
139
|
+
* whose size is the TREE's rather than one node's: it asks for every removed node's parent and
|
|
140
|
+
* then walks everything the removal took with it, which at a thousand rows was eleven thousand
|
|
141
|
+
* crossings inside the timed step.
|
|
142
|
+
*
|
|
143
|
+
* `subtreesOf` answers PRE-ORDER, each root followed by its descendants, concatenated in root
|
|
144
|
+
* order. Anchors included, exactly as `childrenOf` includes them.
|
|
145
|
+
*/
|
|
146
|
+
facebook::jsi::Value parentsOf(
|
|
147
|
+
facebook::jsi::Runtime &runtime,
|
|
148
|
+
const facebook::jsi::Value *arguments,
|
|
149
|
+
size_t count);
|
|
150
|
+
facebook::jsi::Value subtreesOf(
|
|
151
|
+
facebook::jsi::Runtime &runtime,
|
|
152
|
+
const facebook::jsi::Value *arguments,
|
|
153
|
+
size_t count);
|
|
154
|
+
/**
|
|
155
|
+
* `ancestorsOf(handle)` — the node and every ancestor above it, DEEPEST FIRST, in one crossing.
|
|
156
|
+
*
|
|
157
|
+
* The upward twin of `subtreesOf`, and it exists for the same reason the batched pair above does:
|
|
158
|
+
* a walk that asks per LEVEL pays a crossing per level. Event dispatch needs this chain for every
|
|
159
|
+
* event (capture reads it reversed, bubble forward) and the responder negotiation needs it again
|
|
160
|
+
* on every frame of every drag — measured at 18 crossings per event on a depth-8 chain before the
|
|
161
|
+
* two phases shared a walk, and 9 after.
|
|
162
|
+
*
|
|
163
|
+
* A SURFACE is included, exactly as `parentOf`'s answer includes one: stopping at a surface is
|
|
164
|
+
* `host-access.ts`'s job, and it reads the answer's `component` to do it.
|
|
165
|
+
*/
|
|
166
|
+
facebook::jsi::Value ancestorsOf(
|
|
167
|
+
facebook::jsi::Runtime &runtime,
|
|
168
|
+
const facebook::jsi::Value *arguments,
|
|
169
|
+
size_t count);
|
|
170
|
+
/** `committedRecordOf(handle)` — `{handle, tag, rootTag}`, or undefined before a first commit. */
|
|
171
|
+
facebook::jsi::Value committedRecordOf(
|
|
172
|
+
facebook::jsi::Runtime &runtime,
|
|
173
|
+
const facebook::jsi::Value *arguments,
|
|
174
|
+
size_t count);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The imperative six, taking the same placeholder object `applyOps` attached the node to.
|
|
178
|
+
*
|
|
179
|
+
* Each is the matching branch of `UIManagerBinding::get` with one substitution: where the binding
|
|
180
|
+
* unwraps a handle IT minted, these unwrap ours and then take the `ShadowNode` the last commit
|
|
181
|
+
* left on the node. They live HERE rather than beside `Applier` because a handle carries exactly
|
|
182
|
+
* one `NativeState` — under this tree that state is a `Node`, which nothing in `Applier` can read.
|
|
183
|
+
*
|
|
184
|
+
* A node with no committed `ShadowNode` gets the SAME answer as a surface with no revision: six
|
|
185
|
+
* zeroes, four zeroes, `onFail`, and a no-op for the two that go straight to `UIManager`. It is
|
|
186
|
+
* the ordinary state under an async-batched commit, and an app that only asked where something is
|
|
187
|
+
* must not be thrown into.
|
|
188
|
+
*
|
|
189
|
+
* dispatchCommand(handle, name, args) sendAccessibilityEvent(handle, eventType)
|
|
190
|
+
* measure(handle, cb) measureInWindow(handle, cb)
|
|
191
|
+
* measureLayout(handle, relativeTo, onFail, onSuccess)
|
|
192
|
+
* setIsJSResponder(handle, isResponder, blockNativeResponder)
|
|
193
|
+
*/
|
|
194
|
+
facebook::jsi::Value dispatchCommand(
|
|
195
|
+
facebook::jsi::Runtime &runtime,
|
|
196
|
+
const facebook::jsi::Value *arguments,
|
|
197
|
+
size_t count);
|
|
198
|
+
facebook::jsi::Value sendAccessibilityEvent(
|
|
199
|
+
facebook::jsi::Runtime &runtime,
|
|
200
|
+
const facebook::jsi::Value *arguments,
|
|
201
|
+
size_t count);
|
|
202
|
+
facebook::jsi::Value measure(
|
|
203
|
+
facebook::jsi::Runtime &runtime,
|
|
204
|
+
const facebook::jsi::Value *arguments,
|
|
205
|
+
size_t count);
|
|
206
|
+
facebook::jsi::Value measureInWindow(
|
|
207
|
+
facebook::jsi::Runtime &runtime,
|
|
208
|
+
const facebook::jsi::Value *arguments,
|
|
209
|
+
size_t count);
|
|
210
|
+
facebook::jsi::Value measureLayout(
|
|
211
|
+
facebook::jsi::Runtime &runtime,
|
|
212
|
+
const facebook::jsi::Value *arguments,
|
|
213
|
+
size_t count);
|
|
214
|
+
facebook::jsi::Value setIsJSResponder(
|
|
215
|
+
facebook::jsi::Runtime &runtime,
|
|
216
|
+
const facebook::jsi::Value *arguments,
|
|
217
|
+
size_t count);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* RN's own commit telemetry for ANY surface, read on demand — `{ layoutMs, textMs, commitMs,
|
|
221
|
+
* layoutNodes, textMeasures }` for whichever commit produced that surface's current revision.
|
|
222
|
+
*
|
|
223
|
+
* It takes a surface id, so it can be pointed at a surface REACT drove — the only way to answer
|
|
224
|
+
* whether a tree-wide text re-measure is ours or something a Fabric commit costs.
|
|
225
|
+
*/
|
|
226
|
+
facebook::jsi::Value readSurfaceTelemetry(
|
|
227
|
+
facebook::jsi::Runtime &runtime,
|
|
228
|
+
const facebook::jsi::Value *arguments,
|
|
229
|
+
size_t count);
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The PAYLOAD the last commit handed Fabric for this node, or `undefined` before its first.
|
|
233
|
+
*
|
|
234
|
+
* A TEST READ, and the only complete one there is. What a committed node can otherwise report is
|
|
235
|
+
* `Props::getDebugProps()`, a hand-written SELECTION per component — `RCTView` answers seven keys
|
|
236
|
+
* and `RCTSinglelineTextInputView` answers `testID` and nothing else, because RN implements none
|
|
237
|
+
* for `TextInputProps`. So a test asking "did the payload builder resolve `inputMode` into
|
|
238
|
+
* `keyboardType`" had no way to look, and every rule in `SymbioteFabricProps.cpp` was verifiable
|
|
239
|
+
* only through its TypeScript twin.
|
|
240
|
+
*
|
|
241
|
+
* RN's own complete read is `Props::rawProps`, behind `RN_SERIALIZABLE_STATE` — which drags
|
|
242
|
+
* `fbjni/fbjni.h` into `State`'s virtual interface and cannot compile on a host build (tried
|
|
243
|
+
* 2026-09-15, recorded in `core/engine/cpp/tests/CMakeLists.txt`). This needs no flag: the bag is
|
|
244
|
+
* already retained per node as the next commit's diff baseline, so the read costs a conversion and
|
|
245
|
+
* nothing else, and no commit path changes at all.
|
|
246
|
+
*
|
|
247
|
+
* WHAT IT IS NOT: proof that Fabric PARSED any of it. A key no ViewConfig declares sits in here
|
|
248
|
+
* exactly as it was sent — `processor-refusal.itest.ts` is the test for that question, and
|
|
249
|
+
* `getDebugProps` stays the read that proves the round trip. This answers what we SENT.
|
|
250
|
+
*/
|
|
251
|
+
facebook::jsi::Value committedPayloadOf(
|
|
252
|
+
facebook::jsi::Runtime &runtime,
|
|
253
|
+
const facebook::jsi::Value *arguments,
|
|
254
|
+
size_t count);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
} // namespace symbiote
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The first native code this project owns. Everything else under `packages/*` with a podspec is a
|
|
2
|
+
// WRAPPER that vendors somebody else's sources; this compiles our own.
|
|
3
|
+
//
|
|
4
|
+
// It exists for item 8c of the `symbiote-fabric-cxx-surface` skill: the engine's structure must live
|
|
5
|
+
// in memory a native module owns, so that JS reads it as a typed-array view rather than calling for
|
|
6
|
+
// it. This header is the iOS entry point — the module whose creation installs the host object.
|
|
7
|
+
//
|
|
8
|
+
// It does NOT touch React Native's own native sources, which `<native_core_is_untouched>` forbids.
|
|
9
|
+
// Everything it uses is public: `RCTTurboModuleWithJSIBindings` is a declared protocol, and RN's own
|
|
10
|
+
// `AnimatedModule` reaches the runtime through the identical hook.
|
|
11
|
+
|
|
12
|
+
#import <Foundation/Foundation.h>
|
|
13
|
+
#import <React/RCTBridgeModule.h>
|
|
14
|
+
|
|
15
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registered as `SymbioteEngine`. Resolving it from JS is what CREATES it, and creation is what runs
|
|
19
|
+
* `installJSIBindingsWithRuntime:callInvoker:` — so `core/engine/src/native-engine.ts` touches the
|
|
20
|
+
* module for its side effect and then reads the global the hook installed.
|
|
21
|
+
*/
|
|
22
|
+
@interface SymbioteEngineModule : NSObject <RCTBridgeModule>
|
|
23
|
+
@end
|
|
24
|
+
|
|
25
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#import "SymbioteEngineModule.h"
|
|
2
|
+
|
|
3
|
+
#import <ReactCommon/RCTTurboModule.h>
|
|
4
|
+
#import <ReactCommon/RCTTurboModuleWithJSIBindings.h>
|
|
5
|
+
#import <SymbioteEngineSpec/SymbioteEngineSpec.h>
|
|
6
|
+
|
|
7
|
+
#include "../cpp/SymbioteEngineBindings.h"
|
|
8
|
+
|
|
9
|
+
using namespace facebook;
|
|
10
|
+
|
|
11
|
+
@interface SymbioteEngineModule () <NativeSymbioteEngineSpec, RCTTurboModuleWithJSIBindings>
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
@implementation SymbioteEngineModule
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_MODULE(SymbioteEngine)
|
|
17
|
+
|
|
18
|
+
- (std::shared_ptr<react::TurboModule>)getTurboModule:(const react::ObjCTurboModule::InitParams &)params
|
|
19
|
+
{
|
|
20
|
+
return std::make_shared<react::NativeSymbioteEngineSpecJSI>(params);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (NSNumber *)getVersion
|
|
24
|
+
{
|
|
25
|
+
return @(symbiote::kNativeVersion);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#pragma mark - RCTTurboModuleWithJSIBindings
|
|
29
|
+
|
|
30
|
+
// Called ONCE, by `RCTTurboModuleManager`, at the moment this module is first created — which is why
|
|
31
|
+
// `native-engine.ts` resolves the module for its side effect and then reads the global. There is no
|
|
32
|
+
// `install()` for JS to call, deliberately: an explicit install has to happen before the first use
|
|
33
|
+
// and nothing enforces that ordering, while this hook cannot be skipped by anyone who reaches the
|
|
34
|
+
// module at all.
|
|
35
|
+
//
|
|
36
|
+
// Everything below the next line is platform-shared. When Android's shim lands it calls the same
|
|
37
|
+
// `installBindings` from JNI, and this file stays this length.
|
|
38
|
+
- (void)installJSIBindingsWithRuntime:(jsi::Runtime &)runtime
|
|
39
|
+
callInvoker:(const std::shared_ptr<react::CallInvoker> &)callInvoker
|
|
40
|
+
{
|
|
41
|
+
symbiote::installBindings(runtime);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "SymbioteNative's retained shadow-tree engine — clone-on-write commit path + event normalization over React Native Fabric, shared by every framework adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,19 +31,47 @@
|
|
|
31
31
|
".": {
|
|
32
32
|
"types": "./build/index.d.ts",
|
|
33
33
|
"default": "./build/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./mutation-buffer": {
|
|
36
|
+
"types": "./build/mutation-buffer.d.ts",
|
|
37
|
+
"default": "./build/mutation-buffer.js"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"files": [
|
|
37
|
-
"build"
|
|
41
|
+
"build",
|
|
42
|
+
"cpp",
|
|
43
|
+
"!cpp/tests",
|
|
44
|
+
"ios",
|
|
45
|
+
"android",
|
|
46
|
+
"codegen-specs",
|
|
47
|
+
"symbiote-engine.podspec",
|
|
48
|
+
"react-native.config.cjs"
|
|
38
49
|
],
|
|
50
|
+
"codegenConfig": {
|
|
51
|
+
"name": "SymbioteEngineSpec",
|
|
52
|
+
"type": "modules",
|
|
53
|
+
"jsSrcsDir": "codegen-specs",
|
|
54
|
+
"android": {
|
|
55
|
+
"javaPackageName": "dev.symbiotenative.engine"
|
|
56
|
+
},
|
|
57
|
+
"ios": {
|
|
58
|
+
"modulesProvider": {
|
|
59
|
+
"SymbioteEngine": "SymbioteEngineModule"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
39
63
|
"publishConfig": {
|
|
40
64
|
"access": "public"
|
|
41
65
|
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@react-native/normalize-colors": "0.86.2"
|
|
68
|
+
},
|
|
42
69
|
"peerDependencies": {
|
|
43
70
|
"react-native": ">=0.86"
|
|
44
71
|
},
|
|
72
|
+
"peerDependenciesMeta": {},
|
|
45
73
|
"devDependencies": {
|
|
46
|
-
"@symbiote-native/test-utils": "0.3.
|
|
74
|
+
"@symbiote-native/test-utils": "0.3.1"
|
|
47
75
|
},
|
|
48
76
|
"scripts": {
|
|
49
77
|
"typecheck": "tsc --build",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Declares this package's Android native project to @react-native-community/cli autolinking.
|
|
2
|
+
// Explicit rather than relying on auto-detection, so the Kotlin ReactPackage is registered
|
|
3
|
+
// deterministically — same reasoning as `packages/android/react-native.config.js`.
|
|
4
|
+
//
|
|
5
|
+
// `ios` is deliberately ABSENT rather than null: the iOS half is a real podspec
|
|
6
|
+
// (`symbiote-engine.podspec`) that CocoaPods autolinking already finds, and naming the key would
|
|
7
|
+
// only give us a chance to get it wrong.
|
|
8
|
+
//
|
|
9
|
+
// `.cjs`, not `.js`: this package is `"type": "module"`, so a `.js` file using `module.exports`
|
|
10
|
+
// would be loaded as ESM and throw. The CLI searches `react-native.config.js`,
|
|
11
|
+
// `react-native.config.cjs` and `react-native.config.ts` (cli-config's `searchPlacesForCJS`), so the
|
|
12
|
+
// extension costs nothing.
|
|
13
|
+
module.exports = {
|
|
14
|
+
dependency: {
|
|
15
|
+
platforms: {
|
|
16
|
+
android: {
|
|
17
|
+
sourceDir: './android',
|
|
18
|
+
packageImportPath: 'import dev.symbiotenative.engine.SymbioteEnginePackage;',
|
|
19
|
+
packageInstance: 'new SymbioteEnginePackage()',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
# `@symbiote-native/engine` is a DIRECT dependency of every example app, so RN's autolinking finds
|
|
6
|
+
# this podspec with no entry in anybody's Podfile — which is the whole reason the native module lives
|
|
7
|
+
# here instead of in a package of its own. A new package would have needed a release, an entry in
|
|
8
|
+
# every example's manifest, and a row in three parity audits; this needs a `pod install`.
|
|
9
|
+
#
|
|
10
|
+
# Note the two vendoring workarounds that `packages/{slider,navigation}` carry are absent here, on
|
|
11
|
+
# purpose: those podspecs glob files out of a THIRD-PARTY package that pnpm puts behind a symlink,
|
|
12
|
+
# and CocoaPods' recursive glob never descends into one. Our sources sit under this pod's own
|
|
13
|
+
# directory, so a plain downward pattern reaches them.
|
|
14
|
+
Pod::Spec.new do |s|
|
|
15
|
+
s.name = 'symbiote-engine'
|
|
16
|
+
s.version = package['version']
|
|
17
|
+
s.summary = 'Native store and commit hook for the SymbioteNative engine.'
|
|
18
|
+
s.license = package['license'] || 'MIT'
|
|
19
|
+
s.authors = package['author'] || 'symbiote'
|
|
20
|
+
s.homepage = package['homepage'] || 'https://github.com/symbiote/symbiote'
|
|
21
|
+
s.platforms = { :ios => '15.1', :visionos => '1.0' }
|
|
22
|
+
s.source = { :git => 'https://github.com/symbiote/symbiote.git', :tag => "v#{s.version}" }
|
|
23
|
+
|
|
24
|
+
# `cpp/` is the platform-shared half and `ios/` is the registration shim. Android's shim will glob
|
|
25
|
+
# the same `cpp/` from its own build file rather than copying anything.
|
|
26
|
+
s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}'
|
|
27
|
+
|
|
28
|
+
# Pulls in React-Core, the codegen'd spec pod, ReactCommon/turbomodule and the folly flags that
|
|
29
|
+
# every New Architecture module needs. Defined by react-native's `react_native_pods.rb`, which the
|
|
30
|
+
# app's Podfile requires before it reaches this file; the fallback keeps `pod spec lint` and any
|
|
31
|
+
# non-RN evaluation from dying on an undefined method.
|
|
32
|
+
if defined?(install_modules_dependencies)
|
|
33
|
+
install_modules_dependencies(s)
|
|
34
|
+
else
|
|
35
|
+
s.dependency 'React-Core'
|
|
36
|
+
s.dependency 'React-jsi'
|
|
37
|
+
s.dependency 'ReactCommon/turbomodule/core'
|
|
38
|
+
s.pod_target_xcconfig = {
|
|
39
|
+
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20'
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function bezier(mX1: number, mY1: number, mX2: number, mY2: number): (x: number) => number;
|
package/build/animated/bezier.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
// Cubic-bezier easing solver, ported verbatim from React Native's
|
|
2
|
-
// Libraries/Animated/bezier.js (originally bezier-easing by Gaëtan Renaudeau,
|
|
3
|
-
// MIT). Pure math (no React, no native), so it lives unchanged in shared.
|
|
4
|
-
// Established by empiricism (tradeoff: performance vs precision).
|
|
5
|
-
const NEWTON_ITERATIONS = 4;
|
|
6
|
-
const NEWTON_MIN_SLOPE = 0.001;
|
|
7
|
-
const SUBDIVISION_PRECISION = 0.0000001;
|
|
8
|
-
const SUBDIVISION_MAX_ITERATIONS = 10;
|
|
9
|
-
const SPLINE_TABLE_SIZE = 11;
|
|
10
|
-
const SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0);
|
|
11
|
-
function a(a1, a2) {
|
|
12
|
-
return 1.0 - 3.0 * a2 + 3.0 * a1;
|
|
13
|
-
}
|
|
14
|
-
function b(a1, a2) {
|
|
15
|
-
return 3.0 * a2 - 6.0 * a1;
|
|
16
|
-
}
|
|
17
|
-
function c(a1) {
|
|
18
|
-
return 3.0 * a1;
|
|
19
|
-
}
|
|
20
|
-
// x(t) given t, x1, x2, or y(t) given t, y1, y2.
|
|
21
|
-
function calcBezier(t, a1, a2) {
|
|
22
|
-
return ((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t;
|
|
23
|
-
}
|
|
24
|
-
// dx/dt given t, x1, x2, or dy/dt given t, y1, y2.
|
|
25
|
-
function getSlope(t, a1, a2) {
|
|
26
|
-
return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1);
|
|
27
|
-
}
|
|
28
|
-
function binarySubdivide(x, lower, upper, mX1, mX2) {
|
|
29
|
-
let currentX;
|
|
30
|
-
let currentT;
|
|
31
|
-
let lo = lower;
|
|
32
|
-
let hi = upper;
|
|
33
|
-
let i = 0;
|
|
34
|
-
do {
|
|
35
|
-
currentT = lo + (hi - lo) / 2.0;
|
|
36
|
-
currentX = calcBezier(currentT, mX1, mX2) - x;
|
|
37
|
-
if (currentX > 0.0) {
|
|
38
|
-
hi = currentT;
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
lo = currentT;
|
|
42
|
-
}
|
|
43
|
-
} while (Math.abs(currentX) > SUBDIVISION_PRECISION &&
|
|
44
|
-
++i < SUBDIVISION_MAX_ITERATIONS);
|
|
45
|
-
return currentT;
|
|
46
|
-
}
|
|
47
|
-
function newtonRaphsonIterate(x, guess, mX1, mX2) {
|
|
48
|
-
let guessT = guess;
|
|
49
|
-
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
|
|
50
|
-
const currentSlope = getSlope(guessT, mX1, mX2);
|
|
51
|
-
if (currentSlope === 0.0) {
|
|
52
|
-
return guessT;
|
|
53
|
-
}
|
|
54
|
-
const currentX = calcBezier(guessT, mX1, mX2) - x;
|
|
55
|
-
guessT -= currentX / currentSlope;
|
|
56
|
-
}
|
|
57
|
-
return guessT;
|
|
58
|
-
}
|
|
59
|
-
export function bezier(mX1, mY1, mX2, mY2) {
|
|
60
|
-
if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) {
|
|
61
|
-
throw new Error('bezier x values must be in [0, 1] range');
|
|
62
|
-
}
|
|
63
|
-
// Precompute the spline samples once.
|
|
64
|
-
const sampleValues = new Array(SPLINE_TABLE_SIZE);
|
|
65
|
-
if (mX1 !== mY1 || mX2 !== mY2) {
|
|
66
|
-
for (let i = 0; i < SPLINE_TABLE_SIZE; ++i) {
|
|
67
|
-
sampleValues[i] = calcBezier(i * SAMPLE_STEP_SIZE, mX1, mX2);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function getTForX(x) {
|
|
71
|
-
let intervalStart = 0.0;
|
|
72
|
-
let currentSample = 1;
|
|
73
|
-
const lastSample = SPLINE_TABLE_SIZE - 1;
|
|
74
|
-
for (; currentSample !== lastSample && sampleValues[currentSample] <= x; ++currentSample) {
|
|
75
|
-
intervalStart += SAMPLE_STEP_SIZE;
|
|
76
|
-
}
|
|
77
|
-
--currentSample;
|
|
78
|
-
// Interpolate to provide an initial guess for t.
|
|
79
|
-
const dist = (x - sampleValues[currentSample]) /
|
|
80
|
-
(sampleValues[currentSample + 1] - sampleValues[currentSample]);
|
|
81
|
-
const guessForT = intervalStart + dist * SAMPLE_STEP_SIZE;
|
|
82
|
-
const initialSlope = getSlope(guessForT, mX1, mX2);
|
|
83
|
-
if (initialSlope >= NEWTON_MIN_SLOPE) {
|
|
84
|
-
return newtonRaphsonIterate(x, guessForT, mX1, mX2);
|
|
85
|
-
}
|
|
86
|
-
if (initialSlope === 0.0) {
|
|
87
|
-
return guessForT;
|
|
88
|
-
}
|
|
89
|
-
return binarySubdivide(x, intervalStart, intervalStart + SAMPLE_STEP_SIZE, mX1, mX2);
|
|
90
|
-
}
|
|
91
|
-
return function bezierEasing(x) {
|
|
92
|
-
if (mX1 === mY1 && mX2 === mY2) {
|
|
93
|
-
return x; // linear
|
|
94
|
-
}
|
|
95
|
-
// JS numbers are imprecise; pin the extremes.
|
|
96
|
-
if (x === 0)
|
|
97
|
-
return 0;
|
|
98
|
-
if (x === 1)
|
|
99
|
-
return 1;
|
|
100
|
-
return calcBezier(getTForX(x), mY1, mY2);
|
|
101
|
-
};
|
|
102
|
-
}
|
package/build/commit.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { type IFabricNode, type IRootTag, type IMeasureOnSuccess, type IMeasureInWindowOnSuccess, type IMeasureLayoutOnSuccess } from './fabric';
|
|
2
|
-
import { type ISymbioteNode } from './node';
|
|
3
|
-
export { processColor, setColorProcessor } from './platform-color';
|
|
4
|
-
declare global {
|
|
5
|
-
var __SYMBIOTE_BATCH_CREATE__: boolean | undefined;
|
|
6
|
-
}
|
|
7
|
-
export declare function disposeRoot(rootTag: IRootTag): void;
|
|
8
|
-
export declare function commitChildren(rootTag: IRootTag, children: readonly ISymbioteNode[]): void;
|
|
9
|
-
export interface ICommitProfile {
|
|
10
|
-
commits: number;
|
|
11
|
-
walkMs: number;
|
|
12
|
-
nodesVisited: number;
|
|
13
|
-
/** Update-path nodes that rebuilt their Fabric payload and deep-compared it. */
|
|
14
|
-
propsBuilt: number;
|
|
15
|
-
/** Update-path nodes that re-cloned for a child but reused the committed payload by reference. */
|
|
16
|
-
propsReused: number;
|
|
17
|
-
propWrites: number;
|
|
18
|
-
propNoops: number;
|
|
19
|
-
childScans: number;
|
|
20
|
-
childScanProbed: number;
|
|
21
|
-
childFlattens: number;
|
|
22
|
-
childFlattenProbed: number;
|
|
23
|
-
childFlattenWidest: number;
|
|
24
|
-
}
|
|
25
|
-
export declare function readCommitProfile(): ICommitProfile;
|
|
26
|
-
export declare function flushNativeProps(): void;
|
|
27
|
-
export declare function setNativeProps(node: ISymbioteNode, partial: Record<string, unknown>): void;
|
|
28
|
-
/**
|
|
29
|
-
* Publish a node whose props were changed OUTSIDE any renderer mutation.
|
|
30
|
-
*
|
|
31
|
-
* Dirtying is not publishing. Every other write reaches Fabric because the framework's own commit
|
|
32
|
-
* follows it; a change driven by a NATIVE EVENT has no such follow-up — `native-events.ts`
|
|
33
|
-
* requests no commit and no adapter does either, so a node marked dirty from an event handler
|
|
34
|
-
* simply stays dirty. The host-behavior press path (`setNodePressed` for `:active`) is the first
|
|
35
|
-
* caller that is not `setNativeProps`, and `setNodeHidden`'s React twin never needed it because
|
|
36
|
-
* the reconciler is already in its commit phase when it calls.
|
|
37
|
-
*
|
|
38
|
-
* Queued rather than committed on the spot, sharing `setNativeProps`' batch: several writes in one
|
|
39
|
-
* task publish together at the microtask boundary, one commit per surface.
|
|
40
|
-
*/
|
|
41
|
-
export declare function requestCommitFor(node: ISymbioteNode): void;
|
|
42
|
-
export declare function getNativeTag(node: ISymbioteNode): number | undefined;
|
|
43
|
-
export declare function whenCommitted(node: ISymbioteNode, action: () => void): () => void;
|
|
44
|
-
export declare function getNativeNode(node: ISymbioteNode): IFabricNode | undefined;
|
|
45
|
-
export declare function dispatchViewCommand(node: ISymbioteNode, commandName: string, args: readonly unknown[]): void;
|
|
46
|
-
export declare function sendAccessibilityEvent(node: ISymbioteNode, eventType: string): void;
|
|
47
|
-
export declare function measure(node: ISymbioteNode, callback: IMeasureOnSuccess): void;
|
|
48
|
-
export declare function measureInWindow(node: ISymbioteNode, callback: IMeasureInWindowOnSuccess): void;
|
|
49
|
-
export declare function measureLayout(node: ISymbioteNode, relativeTo: ISymbioteNode, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
|