@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.
Files changed (96) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -7
  10. package/build/accessibility-props.js +22 -21
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/graph.d.ts +2 -0
  17. package/build/animated/graph.js +14 -0
  18. package/build/animated/host-binding.d.ts +39 -0
  19. package/build/animated/host-binding.js +278 -0
  20. package/build/animated/index.d.ts +1 -1
  21. package/build/animated/leaf-lifecycle.js +10 -22
  22. package/build/animated/mock.d.ts +1 -19
  23. package/build/animated/props.js +1 -1
  24. package/build/animated/rgba.js +16 -50
  25. package/build/events/index.js +123 -33
  26. package/build/fabric-props.d.ts +1 -1
  27. package/build/fabric-props.js +129 -182
  28. package/build/fabric.d.ts +10 -0
  29. package/build/fabric.js +40 -0
  30. package/build/host-access.d.ts +125 -0
  31. package/build/host-access.js +280 -0
  32. package/build/host-behavior.d.ts +107 -7
  33. package/build/host-behavior.js +309 -31
  34. package/build/image-source-write.d.ts +16 -0
  35. package/build/image-source-write.js +65 -0
  36. package/build/imperative.d.ts +49 -0
  37. package/build/imperative.js +258 -0
  38. package/build/index.d.ts +18 -10
  39. package/build/index.js +65 -10
  40. package/build/mutation-buffer.d.ts +222 -0
  41. package/build/mutation-buffer.js +491 -0
  42. package/build/native-engine.d.ts +182 -0
  43. package/build/native-engine.js +178 -0
  44. package/build/native-tree-host.d.ts +25 -0
  45. package/build/native-tree-host.js +66 -0
  46. package/build/node.d.ts +191 -60
  47. package/build/node.js +1034 -327
  48. package/build/pan-responder/index.d.ts +2 -2
  49. package/build/pan-responder/index.js +37 -56
  50. package/build/platform-color/index.d.ts +1 -1
  51. package/build/platform-color/index.js +11 -4
  52. package/build/process-background-image/index.js +30 -566
  53. package/build/process-background-longhands.d.ts +4 -0
  54. package/build/process-background-longhands.js +44 -0
  55. package/build/process-box-shadow/index.js +23 -187
  56. package/build/process-filter.js +27 -300
  57. package/build/process-transform/index.d.ts +1 -1
  58. package/build/process-transform/index.js +25 -107
  59. package/build/process-transform-origin/index.d.ts +1 -1
  60. package/build/process-transform-origin/index.js +29 -102
  61. package/build/registry.d.ts +36 -0
  62. package/build/registry.js +73 -0
  63. package/build/sound-manager/index.d.ts +3 -0
  64. package/build/sound-manager/index.js +36 -0
  65. package/build/structured-style.d.ts +10 -0
  66. package/build/structured-style.js +180 -0
  67. package/build/style-registry/index.d.ts +14 -0
  68. package/build/style-registry/index.js +60 -11
  69. package/build/styles.d.ts +5 -1
  70. package/build/surface.d.ts +31 -2
  71. package/build/surface.js +138 -42
  72. package/build/text-input-state.d.ts +1 -0
  73. package/build/text-input-state.js +17 -3
  74. package/build/tree-host.d.ts +307 -0
  75. package/build/tree-host.js +211 -0
  76. package/build/view-config.js +4 -4
  77. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  78. package/cpp/SymbioteDebug.cpp +51 -0
  79. package/cpp/SymbioteDebug.h +54 -0
  80. package/cpp/SymbioteEngineBindings.cpp +232 -0
  81. package/cpp/SymbioteEngineBindings.h +59 -0
  82. package/cpp/SymbioteFabricProps.cpp +2619 -0
  83. package/cpp/SymbioteFabricProps.h +223 -0
  84. package/cpp/SymbioteTree.cpp +2478 -0
  85. package/cpp/SymbioteTree.h +257 -0
  86. package/ios/SymbioteEngineModule.h +25 -0
  87. package/ios/SymbioteEngineModule.mm +44 -0
  88. package/package.json +31 -3
  89. package/react-native.config.cjs +23 -0
  90. package/symbiote-engine.podspec +42 -0
  91. package/build/animated/bezier.d.ts +0 -1
  92. package/build/animated/bezier.js +0 -102
  93. package/build/commit.d.ts +0 -49
  94. package/build/commit.js +0 -1030
  95. package/build/tags.d.ts +0 -2
  96. 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.4.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.0"
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;
@@ -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;