@symbiote-native/engine 0.5.0 → 1.1.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 (91) 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 -8
  10. package/build/accessibility-props.js +13 -16
  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/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +145 -0
  28. package/build/host-access.js +315 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +236 -51
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +238 -0
  38. package/build/mutation-buffer.js +513 -0
  39. package/build/native-engine.d.ts +185 -0
  40. package/build/native-engine.js +182 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +68 -0
  43. package/build/node.d.ts +195 -58
  44. package/build/node.js +852 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +322 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +234 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2593 -0
  80. package/cpp/SymbioteTree.h +294 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
package/README.md CHANGED
@@ -1,12 +1,21 @@
1
1
  # @symbiote-native/engine
2
2
 
3
- The **retained shadow-tree engine** at the bottom of [SymbioteNative](../../README.md) — the one
4
- package every framework adapter (`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/angular`, …)
5
- drives, and the only place the mutation→clone-on-write translation into React Native's Fabric
6
- exists. It holds a retained, mutable tree of nodes that an adapter mutates cheaply
7
- (`appendChild` / `setProp` / `removeChild` …), then on commit diffs that tree against Fabric's
8
- current one, clones only what changed, and calls `completeRoot` — the persistent, clone-on-write
9
- dance Fabric requires, done **once**, for every framework.
3
+ The package at the bottom of [SymbioteNative](../../README.md) that every framework adapter
4
+ (`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/angular`, …) drives, and the
5
+ only place the mutation→clone-on-write translation into React Native's Fabric exists. **The
6
+ retained tree itself lives in C++ now**, in `cpp/SymbioteTree.cpp`, reached through this package's
7
+ `src/` — a node handle returned to JS is a thin reference into that native tree, not a JS object
8
+ holding the tree's state.
9
+
10
+ An adapter still mutates a node cheaply (`appendChild` / `setProp` / `removeChild` …), but each
11
+ call now appends an op to the current commit's buffer instead of touching a tree directly. On
12
+ commit, the whole buffer crosses into C++ in **one** JSI call; `SymbioteTree` applies the ops
13
+ against the retained tree, clones only what changed, resolves the platform-parity rules keyed on
14
+ each node's Fabric tag (`cpp/SymbioteFabricProps.cpp` — RN's own component-level behavior, like
15
+ `Pressable`'s `disabled` fold or a `<Text>`'s default `ellipsizeMode`, ported once instead of per
16
+ adapter), and calls into Fabric's C++ `UIManager` directly to commit — the persistent,
17
+ clone-on-write dance Fabric requires, done **once**, for every framework, with one crossing per
18
+ commit instead of one per mutation.
10
19
 
11
20
  > New to SymbioteNative? The [root README](../../README.md) has the architecture and the one fact it
12
21
  > rests on — React is just _one client_ of `nativeFabricUIManager`. This package is what sits
@@ -101,10 +110,10 @@ before a tag is guaranteed to exist.
101
110
  `ActionSheetIOS`, `Linking`, `Vibration`, `ToastAndroid`, `Settings`, `I18nManager`.
102
111
  - **Host behaviors** (`registerHostBehavior` / `IHostBehavior` / `hasHostBehaviors` /
103
112
  `clearHostBehaviors` / `appListenerFor` / `setBehaviorListener` / `requestCommitFor`) — the
104
- registry that lets a primitive's state machine live directly on the engine node instead of
105
- inside a framework component, so a `Pressable`/`Switch`/`TextInput`/`Image` can compile to a
106
- bare intrinsic tag ("host-primitive lowering"). `@symbiote-native/components` registers its
107
- behaviors against this seam; the engine never imports them back.
113
+ registry that lets a primitive's state machine live directly on the engine node instead of inside
114
+ a framework component, so `pressable`/`switch`/`text-input`/`image` are bare intrinsic tags.
115
+ `@symbiote-native/components` registers its behaviors against this seam; the engine never imports
116
+ them back.
108
117
  - **`Animated`** — both the JS and native driver (`timing` / `spring` / `decay` / `loop` /
109
118
  `ValueXY` / tracking / `diffClamp` / `Easing`), including the native-event attachment path
110
119
  (`attachNativeEvent`, `AnimatedEvent`).
@@ -123,9 +132,11 @@ before a tag is guaranteed to exist.
123
132
 
124
133
  - It does not know about React, Vue, Angular, JSX, templates, or reactivity — an adapter maps its
125
134
  own framework idioms onto this API, never the other way around.
126
- - It does not touch Fabric C++, JSI, or Yoga directly — it calls `nativeFabricUIManager`
127
- (`createNode` / `cloneNodeWithNewProps` / `appendChildToSet` / `completeRoot`), the same
128
- framework-agnostic seam React's own renderer uses.
135
+ - The JS side (`src/`) does not touch Fabric, JSI, or Yoga directly — it only builds the
136
+ command buffer and hands it to `SymbioteTree` in one crossing. That C++ tree is the layer
137
+ that actually calls Fabric's `UIManager` (`createNode` / `cloneNodeWithNewProps` /
138
+ `appendChildToSet` / `completeRoot`), the same framework-agnostic seam React's own renderer
139
+ uses further up, from JS, through `nativeFabricUIManager`.
129
140
  - It is not a component library — visual components (Switch, Modal, the lists, …) live in
130
141
  [`@symbiote-native/components`](../components), built on top of this package's `Descriptor`-free
131
142
  mutation API.
@@ -145,3 +156,17 @@ before a tag is guaranteed to exist.
145
156
  pnpm test # vitest, from the workspace root — headless, against a fake Fabric slot
146
157
  DEBUG=1 pnpm test # same, with diagnostic logs on
147
158
  ```
159
+
160
+ That covers the JS side (`src/`) against a fake `nativeFabricUIManager`. `SymbioteTree` itself
161
+ (`cpp/`) has its own test tiers, run from the workspace root:
162
+
163
+ ```bash
164
+ pnpm test:cpp # gtest unit tests over SymbioteTree, asserts on — the build every PR runs
165
+ pnpm test:itest # the JS-driven correctness suite (core/engine/cpp/tests/js/*.itest.ts*),
166
+ # a real six-adapter integration harness against the C++ tree, asserts on
167
+ pnpm bench:itest # the same itest harness, Release build (NDEBUG + -O) — for timings, never asserts
168
+ pnpm test:android # test:itest built with -DSYMBIOTE_PLATFORM_ANDROID=ON, for the #ifdef ANDROID rules
169
+ ```
170
+
171
+ Never read `test:itest`'s timings — that build has asserts on and is measurably slower than what
172
+ ships; use `bench:itest` for anything you intend to quote a millisecond from.
@@ -0,0 +1,51 @@
1
+ # The Android half of the engine's native module. It compiles `../cpp` — the SAME sources the
2
+ # podspec globs — plus the JNI registration shim beside this file. Nothing under `cpp/` is copied
3
+ # or forked; the whole point of the split is that only the registration is per-platform.
4
+ #
5
+ # Shaped after the one third-party precedent that is already in this repo's own app trees and
6
+ # already builds against RN 0.86: `react-native-screens/android/CMakeLists.txt`.
7
+
8
+ cmake_minimum_required(VERSION 3.13)
9
+
10
+ project(symbiote_engine)
11
+
12
+ set(CMAKE_VERBOSE_MAKEFILE ON)
13
+
14
+ # Globbed, not listed, and that is deliberate: the iOS podspec globs `cpp/**/*.{h,cpp}`, so a listed
15
+ # set here would mean a new shared source compiles on one platform and not the other — a link error
16
+ # on Android for a symbol iOS resolves fine, arriving whenever somebody adds a file and forgets this
17
+ # line. RN's own fantom tester globs its sources for the same reason.
18
+ file(GLOB SYMBIOTE_SHARED_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../cpp/*.cpp)
19
+
20
+ add_library(symbiote_engine
21
+ SHARED
22
+ ${SYMBIOTE_SHARED_SOURCES}
23
+ src/main/cpp/SymbioteEngineJni.cpp
24
+ )
25
+
26
+ # So the JNI shim can say `#include "SymbioteEngineBindings.h"` rather than walk four levels up.
27
+ target_include_directories(symbiote_engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../cpp)
28
+
29
+ set_target_properties(symbiote_engine PROPERTIES
30
+ CXX_STANDARD 20
31
+ CXX_STANDARD_REQUIRED ON
32
+ CXX_EXTENSIONS OFF
33
+ POSITION_INDEPENDENT_CODE ON
34
+ )
35
+
36
+ target_compile_definitions(symbiote_engine PRIVATE -DFOLLY_NO_CONFIG=1)
37
+
38
+ # `jsi` and `reactnative` are the only two prefab modules ReactAndroid publishes
39
+ # (ReactAndroid/build.gradle.kts, the `prefab { }` block — the third is `hermestooling`).
40
+ # `reactnative` is the merged library that carries react/renderer/uimanager, react/renderer/mounting
41
+ # and the turbomodule JNI headers our C++ includes; there is no separate
42
+ # `react_renderer_uimanager` prefab target to link.
43
+ find_package(ReactAndroid REQUIRED CONFIG)
44
+ find_package(fbjni REQUIRED CONFIG)
45
+
46
+ target_link_libraries(symbiote_engine
47
+ ReactAndroid::reactnative
48
+ ReactAndroid::jsi
49
+ fbjni::fbjni
50
+ android
51
+ )
@@ -0,0 +1,90 @@
1
+ // @symbiote-native/engine's Android native half — the twin of `symbiote-engine.podspec`.
2
+ //
3
+ // `@symbiote-native/engine` is a DIRECT dependency of every example app, so RN's autolinking finds
4
+ // this project with no entry in anybody's `settings.gradle` — the same reason the podspec needs no
5
+ // Podfile line. Built as a subproject of the consuming app, so it reads the app's pinned SDK/NDK
6
+ // versions from `rootProject.ext` (see `examples/*/android/build.gradle`), falling back to the same
7
+ // defaults `packages/android` uses when built in isolation.
8
+
9
+ apply plugin: "com.android.library"
10
+ apply plugin: "org.jetbrains.kotlin.android"
11
+ // Runs codegen over `../codegen-specs`, producing `NativeSymbioteEngineSpec` (Java) and the JNI spec
12
+ // the app's autolinking links. Needed for the module to be a real TURBO module: the legacy-interop
13
+ // path (`TurboModuleManager::getLegacyModule`) never consults `TurboModuleWithJSIBindings`, so a
14
+ // plain `ReactContextBaseJavaModule` — the shape `packages/android` uses — would install nothing.
15
+ // The plugin supplies `root`/`reactNativeDir`/`jsRootDir` for a library itself and adds the generated
16
+ // java dir to `sourceSets.main` (ReactPlugin.kt, `configureCodegen(isLibrary = true)`), so no
17
+ // `react { }` block and no `sourceSets` line are needed here.
18
+ apply plugin: "com.facebook.react"
19
+
20
+ def safeExtGet(prop, fallback) {
21
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
22
+ }
23
+
24
+ android {
25
+ namespace "dev.symbiotenative.engine"
26
+ compileSdk safeExtGet("compileSdkVersion", 36)
27
+
28
+ if (rootProject.hasProperty("ndkPath")) {
29
+ ndkPath rootProject.ext.ndkPath
30
+ }
31
+ if (rootProject.hasProperty("ndkVersion")) {
32
+ ndkVersion rootProject.ext.ndkVersion
33
+ }
34
+
35
+ defaultConfig {
36
+ minSdk safeExtGet("minSdkVersion", 24)
37
+
38
+ externalNativeBuild {
39
+ cmake {
40
+ arguments "-DANDROID_STL=c++_shared"
41
+ }
42
+ }
43
+ }
44
+
45
+ buildFeatures {
46
+ // Consumes ReactAndroid's published `jsi` / `reactnative` prefab modules.
47
+ prefab true
48
+ }
49
+
50
+ externalNativeBuild {
51
+ cmake {
52
+ path "CMakeLists.txt"
53
+ }
54
+ }
55
+
56
+ compileOptions {
57
+ sourceCompatibility JavaVersion.VERSION_17
58
+ targetCompatibility JavaVersion.VERSION_17
59
+ }
60
+
61
+ kotlinOptions {
62
+ jvmTarget = "17"
63
+ }
64
+
65
+ packagingOptions {
66
+ // RN's own .so files get copied into this library's intermediates by prefab and would collide
67
+ // when the app merges native libs. Only `libsymbiote_engine.so` should ship from here. Since
68
+ // RN 0.76 the renderer/jni libraries are merged into `libreactnative.so`, so that one name
69
+ // covers all of them (same list as `react-native-screens`).
70
+ excludes = [
71
+ "META-INF",
72
+ "META-INF/**",
73
+ "**/libjsi.so",
74
+ "**/libc++_shared.so",
75
+ "**/libfbjni.so",
76
+ "**/libreactnative.so",
77
+ ]
78
+ }
79
+ }
80
+
81
+ repositories {
82
+ google()
83
+ mavenCentral()
84
+ }
85
+
86
+ dependencies {
87
+ // Version-less: the consuming app's React Native Gradle plugin pins react-android, and it brings
88
+ // fbjni's prefab in transitively (which is what `find_package(fbjni)` resolves).
89
+ implementation "com.facebook.react:react-android"
90
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,72 @@
1
+ // Android's registration shim — the twin of `core/engine/ios/SymbioteEngineModule.mm`, and the only
2
+ // file in this folder that is not build configuration. Everything it does is hand the platform-shared
3
+ // `symbiote::installBindings` to whatever runtime React Native offers it.
4
+ //
5
+ // The seam is `TurboModuleWithJSIBindings`
6
+ // (ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/TurboModuleWithJSIBindings.kt).
7
+ // `TurboModuleManager::getTurboModule` checks `isInstanceOf(JTurboModuleWithJSIBindings)` right after
8
+ // it creates the Java module and immediately calls `installer->cthis()->installBindings(runtime, ...)`
9
+ // (ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/TurboModuleManager.cpp:205-216). That is
10
+ // exactly what `installJSIBindingsWithRuntime:callInvoker:` is on iOS: a hook nobody can skip who
11
+ // reaches the module at all, so JS resolves the module for its SIDE EFFECT and then reads the global.
12
+ //
13
+ // RN's own sample of this shape is
14
+ // ReactCommon/react/nativemodule/samples/platform/android/{SampleTurboModule.kt,ReactCommon/SampleTurboModuleJSIBindings.cpp}
15
+ // — a `jni::JavaClass` whose descriptor names the Kotlin module, `registerNatives` binding
16
+ // `getBindingsInstaller`, and a `BindingsInstallerHolder::newObjectCxxArgs` lambda. This file is that,
17
+ // with our installer inside the lambda.
18
+
19
+ #include <ReactCommon/BindingsInstallerHolder.h>
20
+ #include <ReactCommon/CallInvoker.h>
21
+ #include <fbjni/fbjni.h>
22
+ #include <jsi/jsi.h>
23
+
24
+ #include "SymbioteEngineBindings.h"
25
+
26
+ namespace symbiote {
27
+
28
+ using namespace facebook;
29
+
30
+ namespace {
31
+
32
+ /**
33
+ * The C++ side of `dev.symbiotenative.engine.SymbioteEngineModule`.
34
+ *
35
+ * Both of the Kotlin class's `external` members are bound here. `getVersion` is native rather than a
36
+ * Kotlin constant on purpose: `kNativeVersion` is the ABI gate `native-engine.ts` reads, and a second
37
+ * spelling of it in Kotlin is the kind of pair that drifts without anything going red.
38
+ */
39
+ class SymbioteEngineJni : public jni::JavaClass<SymbioteEngineJni> {
40
+ public:
41
+ static constexpr const char *kJavaDescriptor = "Ldev/symbiotenative/engine/SymbioteEngineModule;";
42
+
43
+ static void registerNatives() {
44
+ javaClassLocal()->registerNatives({
45
+ makeNativeMethod("getBindingsInstaller", SymbioteEngineJni::getBindingsInstaller),
46
+ makeNativeMethod("getVersion", SymbioteEngineJni::getVersion),
47
+ });
48
+ }
49
+
50
+ private:
51
+ static jni::local_ref<react::BindingsInstallerHolder::javaobject> getBindingsInstaller(
52
+ jni::alias_ref<SymbioteEngineJni> /*self*/) {
53
+ return react::BindingsInstallerHolder::newObjectCxxArgs(
54
+ [](jsi::Runtime &runtime, const std::shared_ptr<react::CallInvoker> & /*callInvoker*/) {
55
+ installBindings(runtime);
56
+ });
57
+ }
58
+
59
+ static jdouble getVersion(jni::alias_ref<SymbioteEngineJni> /*self*/) {
60
+ return static_cast<jdouble>(kNativeVersion);
61
+ }
62
+ };
63
+
64
+ } // namespace
65
+
66
+ } // namespace symbiote
67
+
68
+ // `System.loadLibrary("symbiote_engine")` from the module's own initializer is what gets here, so the
69
+ // natives are bound before anything can call them.
70
+ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) {
71
+ return facebook::jni::initialize(vm, [] { symbiote::SymbioteEngineJni::registerNatives(); });
72
+ }
@@ -0,0 +1,43 @@
1
+ package dev.symbiotenative.engine
2
+
3
+ import com.facebook.proguard.annotations.DoNotStrip
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.module.annotations.ReactModule
6
+ import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
7
+ import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
8
+
9
+ // Android's registration shim, and the twin of `core/engine/ios/SymbioteEngineModule.mm`. It carries
10
+ // no behaviour: creating it is what installs `global.__symbioteEngineNative`, and the payload lives
11
+ // in `core/engine/cpp/`, compiled unchanged by `../CMakeLists.txt`.
12
+ //
13
+ // `TurboModuleWithJSIBindings` is the Android analogue of iOS's `RCTTurboModuleWithJSIBindings` —
14
+ // `TurboModuleManager::getTurboModule` calls `getBindingsInstaller()` the moment it creates the Java
15
+ // module and applies the returned installer to the runtime. So there is no `install()` for JS to
16
+ // call, deliberately: an explicit install has an ordering requirement nothing enforces, while this
17
+ // hook cannot be skipped by anyone who reaches the module at all.
18
+ //
19
+ // Both members below are `external` and bound in `src/main/cpp/SymbioteEngineJni.cpp`. `getVersion`
20
+ // in particular: it is the ABI gate `native-engine.ts` reads, and its value is `kNativeVersion` in
21
+ // the shared C++ — restating it as a Kotlin constant would be a second spelling that drifts silently.
22
+ @DoNotStrip
23
+ @ReactModule(name = SymbioteEngineModule.NAME)
24
+ class SymbioteEngineModule(reactContext: ReactApplicationContext) :
25
+ NativeSymbioteEngineSpec(reactContext), TurboModuleWithJSIBindings {
26
+
27
+ // Not in a `companion object` initializer: the companion is only touched when something reads
28
+ // `NAME`, and `getBindingsInstaller` is called on the INSTANCE. Loading here runs before either
29
+ // native member can be reached, and a repeat `loadLibrary` of an already-loaded name is a no-op.
30
+ init {
31
+ System.loadLibrary("symbiote_engine")
32
+ }
33
+
34
+ @DoNotStrip external override fun getBindingsInstaller(): BindingsInstallerHolder
35
+
36
+ @DoNotStrip external override fun getVersion(): Double
37
+
38
+ companion object {
39
+ // Declared here rather than read off the generated spec's own `NAME`, so this file and the
40
+ // package beside it do not depend on a field of a class that exists only after codegen runs.
41
+ const val NAME: String = "SymbioteEngine"
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ package dev.symbiotenative.engine
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ // The one ReactPackage for `@symbiote-native/engine`'s native half. Autolinking registers it (see
10
+ // `../../../../../../react-native.config.cjs`), so no app touches its `MainApplication`.
11
+ //
12
+ // `BaseReactPackage` rather than the plain `ReactPackage` that `packages/android` uses, and
13
+ // `isTurboModule = true` rather than the legacy-interop default: those two together are what put the
14
+ // module on `TurboModuleManager::getTurboModule`, which is the ONLY path that consults
15
+ // `TurboModuleWithJSIBindings`. On the legacy path the module would be created, work, and install
16
+ // nothing — with no error anywhere.
17
+ class SymbioteEnginePackage : BaseReactPackage() {
18
+
19
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
20
+ if (name == SymbioteEngineModule.NAME) SymbioteEngineModule(reactContext) else null
21
+
22
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
23
+ mapOf(
24
+ SymbioteEngineModule.NAME to
25
+ ReactModuleInfo(
26
+ SymbioteEngineModule.NAME,
27
+ SymbioteEngineModule::class.java.name,
28
+ /* canOverrideExistingModule = */ false,
29
+ /* needsEagerInit = */ false,
30
+ /* isCxxModule = */ false,
31
+ /* isTurboModule = */ true,
32
+ )
33
+ )
34
+ }
35
+ }
@@ -10,7 +10,7 @@
10
10
  // files, not derived from this file's name. Mirrors
11
11
  // RN's Libraries/Components/AccessibilityInfo/AccessibilityInfo.js.
12
12
  import { isSymbioteNode } from '../node.js';
13
- import { sendAccessibilityEvent as dispatchAccessibilityEvent } from '../commit.js';
13
+ import { sendAccessibilityEvent as dispatchAccessibilityEvent } from '../imperative.js';
14
14
  import { dlog } from '../debug.js';
15
15
  export { isBoolean } from '../type-guards.js';
16
16
  // The routing behind IAccessibilityInfoStatic.sendAccessibilityEvent, shared by both
@@ -1,11 +1,4 @@
1
- export declare const ARIA_ALIAS_KEYS: readonly string[];
2
- /**
3
- * Whether one key is an alias this fold consumes. `startsWith` rather than a Set lookup: this runs
4
- * on `setProp`, the hottest write path in the engine (32 001 writes on one benchmark create), and
5
- * it is guarded by the node's sticky flag so it is reached at most once per node per key. The
6
- * `role` comparison comes first because it is the one alias with no prefix.
7
- */
8
- export declare function isAriaAliasKey(key: string): boolean;
1
+ export declare const ARIA_ALIAS_KEYS: readonly ["role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"];
9
2
  /**
10
3
  * Fold the web-alias `aria-*` / `role` props into RN's canonical `accessibility*` props.
11
4
  *
@@ -1,12 +1,9 @@
1
1
  // RN's `aria-*` / `role` -> `accessibility*` fold, at the layer every path goes through.
2
2
  //
3
- // WHY IT IS HERE AND NOT IN A WRAPPER. It used to run inside each primitive's COMPONENT, which is
4
- // exactly the layer host-primitive lowering removes. A per-attribute element path cannot do it —
5
- // `aria-checked` has to be folded against a sibling `accessibilityState` — so the four lowering
6
- // transforms REFUSED any element carrying `role` or an `aria-*` attribute
7
- // (`REFUSAL_CATEGORIES.bagFold`). Accessibility props are ordinary in real code, so that refusal
8
- // cost lowering coverage on every primitive, including the three already lowered. Moving the fold
9
- // down deletes the refusal instead of teaching four transforms a bag operation they cannot express.
3
+ // WHY IT IS HERE AND NOT IN A WRAPPER. It used to run inside each primitive's COMPONENT, and a tag
4
+ // has none. It also cannot run per attribute — `aria-checked` has to be folded against a sibling
5
+ // `accessibilityState` — so it belongs at the one point where the whole bag is known, which is the
6
+ // payload build.
10
7
  //
11
8
  // IT IS A MOVE, NOT A REWRITE, AND THAT IS DELIBERATE. The function carries TWO CONTRADICTORY
12
9
  // PRECEDENCE RULES: for the scalars an explicit `accessibility*` WINS and the alias only fills a
@@ -59,6 +56,10 @@ const ROLE_TO_ACCESSIBILITY_ROLE = {
59
56
  // list. `slotDerived` (host-behavior.ts) takes prop NAMES, so a primitive whose payload derives
60
57
  // from an owner's aria props has to enumerate them — and a second hand-written copy is exactly what
61
58
  // `.claude/rules/adapter-parity-audit.md` records going stale one member at a time.
59
+ // `as const` rather than `readonly string[]`, so the members are LITERALS. That is what lets a
60
+ // consumer index a prop type with them — `pickAccessibilityProps` in the Svelte adapter forwards the
61
+ // aria half by looping this list — and it makes the two sides check each other: a name here that is
62
+ // not a key of `IAriaProps` stops compiling at the use site rather than going quietly unforwarded.
62
63
  export const ARIA_ALIAS_KEYS = [
63
64
  'role',
64
65
  'aria-label',
@@ -85,15 +86,11 @@ function hasAnyAriaKey(props) {
85
86
  }
86
87
  return false;
87
88
  }
88
- /**
89
- * Whether one key is an alias this fold consumes. `startsWith` rather than a Set lookup: this runs
90
- * on `setProp`, the hottest write path in the engine (32 001 writes on one benchmark create), and
91
- * it is guarded by the node's sticky flag so it is reached at most once per node per key. The
92
- * `role` comparison comes first because it is the one alias with no prefix.
93
- */
94
- export function isAriaAliasKey(key) {
95
- return key === 'role' || key.startsWith('aria-');
96
- }
89
+ // `isAriaAliasKey` WAS HERE AND IS GONE (2026-09-18). Its whole reason was to maintain
90
+ // `node.hasAriaAlias` from `setProp` — the hottest write path in the engine, 32 001 writes on one
91
+ // benchmark create — and that flag existed only to gate the aria fold inside the headless payload
92
+ // builder. The fold left for `SymbioteFabricProps.cpp`, which recomputes presence from the bag it
93
+ // holds, so the flag became write-only and this function became its only maintainer. Both went.
97
94
  function isRecord(value) {
98
95
  return typeof value === 'object' && value !== null;
99
96
  }
@@ -26,7 +26,7 @@ export declare function decay(value: AnimatedValue, config: IDecayConfig): IComp
26
26
  export interface IParallelConfig {
27
27
  stopTogether?: boolean;
28
28
  }
29
- export declare function parallel(animations: ICompositeAnimation[], config?: IParallelConfig): ICompositeAnimation;
29
+ export declare function parallel(animations: (ICompositeAnimation | undefined)[], config?: IParallelConfig): ICompositeAnimation;
30
30
  export declare function sequence(animations: ICompositeAnimation[]): ICompositeAnimation;
31
31
  export declare function delay(time: number): ICompositeAnimation;
32
32
  export declare function stagger(time: number, animations: ICompositeAnimation[]): ICompositeAnimation;
@@ -99,7 +99,10 @@ export function decay(value, config) {
99
99
  },
100
100
  };
101
101
  }
102
- export function parallel(animations, config) {
102
+ export function parallel(
103
+ // `| undefined` entries: `AnimatedImplementation.js`'s `parallelImpl` treats a falsy array
104
+ // entry (e.g. `cond && timing(...)`) as already-finished rather than crashing.
105
+ animations, config) {
103
106
  let doneCount = 0;
104
107
  // Track per-animation completion so stop() calls each at most once.
105
108
  const hasEnded = {};
@@ -123,19 +126,24 @@ export function parallel(animations, config) {
123
126
  result.stop();
124
127
  }
125
128
  };
126
- animation.start(cb, isLooping);
129
+ if (animation === undefined) {
130
+ cb({ finished: true });
131
+ }
132
+ else {
133
+ animation.start(cb, isLooping);
134
+ }
127
135
  });
128
136
  },
129
137
  stop() {
130
138
  animations.forEach((animation, idx) => {
131
139
  if (!hasEnded[idx])
132
- animation.stop();
140
+ animation?.stop();
133
141
  hasEnded[idx] = true;
134
142
  });
135
143
  },
136
144
  reset() {
137
145
  animations.forEach((animation, idx) => {
138
- animation.reset();
146
+ animation?.reset();
139
147
  hasEnded[idx] = false;
140
148
  doneCount = 0;
141
149
  });
@@ -192,6 +200,12 @@ export function delay(time) {
192
200
  export function stagger(time, animations) {
193
201
  return parallel(animations.map((animation, i) => sequence([delay(time * i), animation])));
194
202
  }
203
+ // TODO(rn-parity, low priority): vendor's `loopImpl`/`sequenceImpl`/`parallelImpl` each throw
204
+ // ("Loops run using the native driver cannot contain Animated.X animations") from a
205
+ // `_startNativeLoop` reachable only by nesting `loop(loop(...))` around a native-driven leaf. Ours
206
+ // has no such throw: the nesting silently falls back to the JS restart loop instead of erroring.
207
+ // Judged non-actionable (misuse-detection guard, no runtime-visible effect for a correct caller,
208
+ // same class as the `numColumns` invariant) — see the audit skill for the full reasoning.
195
209
  export function loop(animation, config = {}) {
196
210
  const iterations = config.iterations ?? -1;
197
211
  const resetBeforeIteration = config.resetBeforeIteration ?? true;
@@ -1,5 +1,5 @@
1
1
  export type IEasingFunction = (t: number) => number;
2
- export declare const Easing: {
2
+ export interface IEasing {
3
3
  step0(n: number): number;
4
4
  step1(n: number): number;
5
5
  linear(t: number): number;
@@ -17,4 +17,5 @@ export declare const Easing: {
17
17
  in(easing: IEasingFunction): IEasingFunction;
18
18
  out(easing: IEasingFunction): IEasingFunction;
19
19
  inOut(easing: IEasingFunction): IEasingFunction;
20
- };
20
+ }
21
+ export declare const Easing: IEasing;
@@ -1,88 +1,17 @@
1
- // Common easing functions, ported from React Native's Libraries/Animated/Easing.js.
2
- // Pure math (the only dependency is the equally-pure bezier solver), so it lives
3
- // unchanged in shared and every adapter re-exports it.
4
- import { bezier } from './bezier.js';
5
- let cachedEase;
6
- export const Easing = {
7
- step0(n) {
8
- return n > 0 ? 1 : 0;
9
- },
10
- step1(n) {
11
- return n >= 1 ? 1 : 0;
12
- },
13
- linear(t) {
14
- return t;
15
- },
16
- // A simple inertial interaction (object slowly accelerating to speed).
17
- ease(t) {
18
- if (cachedEase === undefined) {
19
- cachedEase = bezier(0.42, 0, 1, 1);
20
- }
21
- return cachedEase(t);
22
- },
23
- quad(t) {
24
- return t * t;
25
- },
26
- cubic(t) {
27
- return t * t * t;
28
- },
29
- // Nth-power: position is the Nth power of elapsed time.
30
- poly(n) {
31
- return t => Math.pow(t, n);
32
- },
33
- sin(t) {
34
- return 1 - Math.cos((t * Math.PI) / 2);
35
- },
36
- circle(t) {
37
- return 1 - Math.sqrt(1 - t * t);
38
- },
39
- exp(t) {
40
- return Math.pow(2, 10 * (t - 1));
41
- },
42
- // Elastic: spring-like oscillation. bounciness 1 overshoots once; 0 not at all.
43
- elastic(bounciness = 1) {
44
- const p = bounciness * Math.PI;
45
- return t => 1 - Math.pow(Math.cos((t * Math.PI) / 2), 3) * Math.cos(t * p);
46
- },
47
- // Animates back slightly before moving forward.
48
- back(s = 1.70158) {
49
- return t => t * t * ((s + 1) * t - s);
50
- },
51
- // A simple bouncing effect.
52
- bounce(t) {
53
- if (t < 1 / 2.75) {
54
- return 7.5625 * t * t;
55
- }
56
- if (t < 2 / 2.75) {
57
- const t2 = t - 1.5 / 2.75;
58
- return 7.5625 * t2 * t2 + 0.75;
59
- }
60
- if (t < 2.5 / 2.75) {
61
- const t2 = t - 2.25 / 2.75;
62
- return 7.5625 * t2 * t2 + 0.9375;
63
- }
64
- const t2 = t - 2.625 / 2.75;
65
- return 7.5625 * t2 * t2 + 0.984375;
66
- },
67
- // A cubic bezier curve, same as CSS transition-timing-function.
68
- bezier(x1, y1, x2, y2) {
69
- return bezier(x1, y1, x2, y2);
70
- },
71
- // Runs an easing function forwards.
72
- in(easing) {
73
- return easing;
74
- },
75
- // Runs an easing function backwards.
76
- out(easing) {
77
- return t => 1 - easing(1 - t);
78
- },
79
- // Makes any easing function symmetrical (forwards then backwards).
80
- inOut(easing) {
81
- return t => {
82
- if (t < 0.5) {
83
- return easing(t * 2) / 2;
84
- }
85
- return 1 - easing((1 - t) * 2) / 2;
86
- };
87
- },
88
- };
1
+ // RN's own Easing, imported rather than ported.
2
+ //
3
+ // The port this replaces was 109 lines beside a 142-line `bezier.ts`, and it reproduced upstream
4
+ // formula for formula. Swept at 20 001 points per function before the swap: 15 of the 17 agree to
5
+ // the bit, and the two bezier-backed ones (`ease`, `bezier`) differ by 2.9e-10, because upstream
6
+ // samples its spline into a Float32Array where the port used a plain (float64) array. That is
7
+ // below a sub-pixel on any animation this drives.
8
+ //
9
+ // The TYPE is ours and stays ours. `Easing` is on the engine barrel and re-exported by all five
10
+ // adapters, and the upstream module ships no types - so importing it untyped would hand every
11
+ // consumer `any`. Twenty lines of interface in exchange for 251 lines of implementation.
12
+ import EasingUpstream from 'react-native/Libraries/Animated/Easing';
13
+ // tsc resolves this deep path but reads the Flow source through allowJs, so the default export
14
+ // arrives shapeless rather than untyped - the mismatch is the only thing the suppression covers,
15
+ // and IEasing above is what every consumer actually sees.
16
+ // @ts-expect-error - upstream ships no usable types for this path.
17
+ export const Easing = EasingUpstream;