Skip to content

feat: add worklet bridge for Reanimated shared value support - #250

Open
mfazekas wants to merge 137 commits into
mainfrom
feat/worklet-bridge
Open

feat: add worklet bridge for Reanimated shared value support#250
mfazekas wants to merge 137 commits into
mainfrom
feat/worklet-bridge

Conversation

@mfazekas

@mfazekas mfazekas commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Port of #79 to the experimental repo. Adds worklet bridge that installs Nitro's dispatcher on Reanimated's UI runtime, enabling ViewModel property listeners to drive SharedValues on the UI thread.


Stack:

  1. feat!: experimental Rive runtime backend (iOS + Android) #134
  2. feat: add worklet bridge for Reanimated shared value support #250
  3. feat: add react-query + async APIs for exerciser #252

mfazekas and others added 26 commits June 1, 2026 11:19
Add new experimental iOS backend (ios/new/) with synchronous API,
move legacy backend files to ios/legacy/, add getEnums() support,
retry listener streams on missingData, and restore TestComponentOverlay.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
getEnums() in legacy now throws directing users to the experimental
backend instead of creating throwaway Worker+File instances.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
… binding

Each Worker has its own C++ command server with its own m_artboards handle map.
Creating separate Workers per file meant artboard handles from one file were
invalid on another file's server. Using a shared singleton Worker fixes cross-file
artboard property set. Also wires fit/alignment through experimental Fit enum and
improves asset type detection with audio/font magic bytes.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Happy <yesreply@happy.engineering>
Passing fit to the Rive() constructor breaks layout mode because
the MTKView drawable isn't ready yet. Set rive.fit after
setupRiveUIView() instead.
Port Flutter data binding tests for VM access, enums, creation
variants, list properties, artboard and image properties. Includes
new .riv test assets and react-native-harness upgrade to alpha.25.
…ceByIndex

Prevents fatal crash when passing negative numbers to Swift APIs
expecting unsigned integers.
CocoaPods doesn't embed SPM-resolved dynamic frameworks automatically.
Patches the embed script to include RiveRuntime when USE_RIVE_SPM=1.
Move 19 backend-specific files to src/legacy/java/, add Gradle sourceSets
switching via USE_RIVE_NEW_API property, prepare empty experimental dirs.
Uses the new handle-based Rive Android SDK (app.rive.*) with CommandQueue,
path-based ViewModelInstance property access, and Kotlin Flows for reactivity.
Throws UnsupportedOperationException for SMI inputs, text runs, and events.
Also fixes Gradle property resolution to use rootProject.findProperty.
Custom TextureView renderer with Choreographer render loop, CommandQueue
polling infrastructure, ViewModel resolution and property validation,
and example Compose test activities for manual testing.
Also fix viewModelByIndex/ByName on Android legacy to catch SDK
exceptions instead of letting them propagate as JniExceptions.
…fferences

Fix colorProperty setter Double→Int overflow on Android legacy
(toLong().toInt()). Make enum and replaceViewModel tests tolerant
of Android SDK behavioral differences.
Use .viewModelDefault(from: .name(vmName)) for default instance
creation instead of artboard-based lookup that only worked for
the default artboard's VM. Skip instanceName, color get/set, and
non-existent property checks on experimental iOS where the SDK
doesn't support them.
…anceName

Add async variants for viewModelByName, defaultArtboardViewModel, and
ViewModel create methods. Pass instanceName through at creation time on
both iOS and Android experimental backends as a workaround for the SDK
not exposing ViewModelInstance.name.
mfazekas added 10 commits June 1, 2026 11:19
Bump example app from RN 0.79.2 to 0.80.3. This unblocks upgrading to
reanimated 4.2+ and worklets 0.7+ for Nitro auto-box support.

- react 19.0.0 → 19.1.0
- react-native 0.79.2 → 0.80.3
- @react-native-community/cli 18.0.0 → 19.1.2
- @react-native/* packages 0.79.2 → 0.80.3
- Kotlin 2.0.21 → 2.1.20, Gradle 8.13 → 8.14.1
- Removed compose-compiler-gradle-plugin (no longer needed)
Bump react-native-harness and platform packages from 1.0.0 to 1.1.0.
## Summary

Adds 31 harness tests for the non-deprecated async API that previously
had zero test coverage. Existing tests only exercise the deprecated sync
methods (`.value`, `viewModelByName`, `createInstanceByName`, etc.) —
this fills the gap for the intended async replacements.

## What's covered

- `createDefaultInstanceAsync`, `createInstanceByNameAsync`,
`createBlankInstanceAsync`
- `defaultArtboardViewModelAsync`, `getArtboardCountAsync`,
`getArtboardNamesAsync`
- `getPropertiesAsync`, `getPropertyCountAsync`, `getInstanceCountAsync`
- `getValueAsync()` + `set()` round-trip for all 5 property types
(number, string, boolean, color, enum)
- `viewModelAsync` for nested ViewModel access
- Async list operations: `getLengthAsync`, `getInstanceAtAsync`,
`addInstanceAsync`, `removeInstanceAtAsync`, `swapAsync`

## Test plan

- [x] `yarn test:harness:ios -- __tests__/async-api.harness.ts` — 31/31
pass
- [x] Full suite `yarn test:harness:ios` — 136 pass (no regressions)
coverage-ios is now integrated in the harness packages directly,
removing the local file: dependency.
Fixes flaky autoplay harness tests. The 6.20.1 fix for "massive
timestamp delta on first frame" was causing the animation to jump
through its entire cycle in one frame, making ypos property change
detection unreliable. Verified with 100 consecutive test runs (0
failures).
Adds 15 new harness tests covering listener callback invocation, set() +
getValueAsync() round-trips, and removeListeners() for all ViewModel
property types. iOS native coverage goes from 41% to 50%.

Target types: BooleanProperty (16→84%), ColorProperty (14→84%),
EnumProperty (16→84%), TriggerProperty (0→81%), StringProperty (37→84%),
NumberProperty (85→90%).
Adds harness tests for two previously untested areas:

**Font config** (6 tests): loadFont by system name, loadFont by URL,
systemFallback, setFallbackFonts with default/weight-specific fonts,
clearFallbackFonts, error on invalid font name.

**Asset loading** (4 tests): referencedAssets with font type, image URL
type, multiple asset types, and undefined assets.

Covers HybridRiveFontConfig.swift (was 0%), AssetLoader.swift (was 1%),
DataSourceResolver.swift (was 0%), HTTPDataLoader.swift (was 43%).
Adds 7 new tests covering previously untested native code paths:

- **fromBytes**: load .riv from raw ArrayBuffer (HybridRiveFileFactory)
- **RiveImages.loadFromURLAsync**: load image and verify byteSize
(HybridRiveImageFactory + HybridRiveImage)
- **RiveLog.setLogLevel**: exercise all 4 log levels (HybridRiveLogger)
- **RiveView pause/play/reset**: exercise view control methods with a
rendered RiveView (HybridRiveView)
Upgrades harness from 1.2.0 to 1.3.0. Key improvements from harness PRs
#129 and #132:

- Bridge RPC closes immediately on app disconnect instead of waiting for
timeout
- New internal RPC transport with heartbeat-based liveness detection
- Dead app connections fail fast with `AppBridgeDisconnectedError`
instead of ambiguous "device did not respond" after 90s

Should reduce the intermittent CI flakiness we've been seeing on legacy
iOS.
@mfazekas
mfazekas force-pushed the feat/rive-ios-experimental branch from a5bf096 to 42e7d76 Compare June 1, 2026 09:21
mfazekas added 12 commits June 10, 2026 19:17
…ntModificationException (#286)

`onChanged` iterates `listeners.values` on a `Dispatchers.Default`
thread (the flow-collection coroutine), while
`addListener`/`removeListener` mutate the same map from the JS thread. A
plain `LinkedHashMap` fails fast with `ConcurrentModificationException`
when this overlap occurs.

`ConcurrentHashMap` iterates safely with concurrent mutations — no CME,
no locks needed at call sites.
…oid) (#285)

## Problem
On the experimental backend, `play()`/`pause()`/`reset()` were no-ops —
animations couldn't be paused/resumed and `autoPlay={false}` was
ignored.
- **iOS**: only mutated a private `isPaused` flag never forwarded to
`RiveUIView`.
- **Android**: empty bodies; the render loop advanced unconditionally
and `autoPlay` was never consumed.

Additionally, `useRiveTrigger` never fired on Android experimental —
three separate issues:
1. `awaitViewReady()` resolved at end of `configure()`, before the
TextureView surface existed. Triggers fired before the surface was up
were lost.
2. `getTriggerFlow` has `replay=0` (no initial value on subscription);
`drop=1` was silently eating the first real trigger.
3. The listener coroutine was launched with `DEFAULT` start, so it was
merely queued on `Dispatchers.Default` and could miss a trigger fired
before it reached `collect()`.

Also, `dataBind={{ byName }}` on Android always bound from
`vmNames.first()` (first VM in file) instead of the artboard's default
ViewModel.

## Fix
- **iOS** (`ios/new/RiveReactNativeView.swift`): forward `isPaused` to
`RiveUIView` in play/pause/playIfNeeded, pass it into
`RiveUIView(rive:isPaused:)`, set it from `autoPlay`.
- **Android** (`android/src/new/.../RiveReactNativeView.kt`): gate
`advanceStateMachine` on a `paused` flag toggled by play/pause, honor
`autoPlay`. Signal `awaitViewReady()` on first rendered frame (not end
of configure).
- **Android triggers**: use `drop=0` for trigger flow (no initial value
to skip); launch listener coroutine with `CoroutineStart.UNDISPATCHED`.
- **Android byName**: use `ViewModelSource.DefaultForArtboard(art)`
instead of `Named(vmNames.first())`, matching iOS and the Auto path.
- **reset()**: deprecated. Neither experimental backend has a reset
primitive — marked `@deprecated` in the spec, experimental impl logs an
error and does nothing.

## Verification
- `autoplay.harness.tsx`: pause freezes `ypos`, play resumes it,
`autoPlay={false}` holds. Green on both platforms.
- `use-rive-trigger.harness.tsx`: both stable and unstable-callback
tests now pass on Android.
- `viewmodel-instance-lookup.harness.tsx`: byName regression test
verifies `artboard2 + byName("vmi1") → _id="vm2.vmi1.id"` (not vm1).
- CI: legacy iOS `pod install` step clears `Podfile.lock` to avoid
external `fast_float` checksum rejection.
`asyncExecuteOnUiThread` posts to the UI thread and returns immediately,
so the Promise resolves before the work runs. If the action throws, it
crashes on the UI thread instead of rejecting the Promise.

`play()`/`pause()`/`reset()` on the experimental backend are just flag
toggles or a deprecation log — no UI thread requirement, no throw.
Replace with `Promise.async { view.play() }` etc.
Property accessors (`numberProperty`, `stringProperty`, etc.) were using
`runBlocking { flow.first() }` as an existence probe before returning
the wrapper. This blocks the calling thread and can deadlock if called
from the main thread (e.g. via a worklet).

The iOS experimental backend already does the right thing — it just
creates and returns the wrapper directly. This PR aligns Android
experimental to the same behaviour: return a wrapper always, fail lazily
when `getValueAsync()` is called on an invalid path.

Also removes the `propertyNames` lazy val (used for `hasProperty`) which
had its own one-time `runBlocking` call.

Test: updated guard for the "non-existent returns undefined" test (skips
on all experimental backends), added new test asserting
`getValueAsync()` rejects for invalid paths on experimental.
Removes unreachable and never-populated code from the experimental iOS
backend.

- Drop `eventListeners`, `addEventListener`, `removeEventListeners` from
`RiveReactNativeView` — the Hybrid layer throws before these are ever
called
- Drop `listenerTasks` from `HybridViewModelListProperty` and
`HybridViewModelImageProperty` — the dict is declared and torn down but
never populated
…iew reuse, bind-before-ready (#289)

Addresses several correctness issues from the experimental runtime
review.

- **Android**: cancel auto-bind `CoroutineScope` on dispose; adds
`disposed` guard inside the Main-thread hop to prevent post-teardown
binding against a stale/reconfigured handle
- **iOS**: reuse existing `RiveUIView` on reconfigure by assigning
`riveUIView.rive = rive` instead of tearing down and recreating the view
— eliminates orphaned MTKView draw calls on file switch
- **iOS**: queue `bindViewModelInstance` calls that arrive before
`configTask` completes, then apply them once `riveInstance` is set —
previously the bind was silently dropped

Includes harness tests for the iOS fixes:
- `reconfigure.harness.tsx` — verifies animation still plays after
switching file prop and back
- `bind-before-ready.harness.tsx` — reproduces and verifies the
bind-before-ready race (failed before fix, passes after)
Stacked on #289.

Adds `setValueAsync(value)` to Number, String, Boolean, Color, and Enum
property types across all 4 backends (iOS experimental, iOS legacy,
Android experimental, Android legacy).

On iOS experimental, this is a proper async write — the promise resolves
only after the SDK `setValue` call completes on the main actor, giving
ordering guarantees and error propagation. On legacy and Android,
`set()` is already synchronous so `setValueAsync` is a thin wrapper that
makes the API consistent.

Includes harness tests covering all 5 property types.
…ental

# Conflicts:
#	example/package.json
#	package.json
#	yarn.lock
Adds getPropertiesAsync() to ViewModel and ViewModelInstance for runtime
introspection. Returns ViewModelPropertyInfo[] with name and type for each
property.
Port from #79. Adds RiveWorkletBridge
HybridObject that installs Nitro's dispatcher on Reanimated's UI runtime,
enabling ViewModel property listeners to drive SharedValues on the UI
thread without blocking when JS is busy.

Also bumps rive-ios to 6.20.2 and adds bouncing ball exerciser demo.
Suppress eslint no-deprecated warnings for runOnUI,
defaultArtboardViewModel, and createDefaultInstance.
These are properly fixed in the next stacked PR.
@mfazekas
mfazekas force-pushed the feat/worklet-bridge branch from 0b0d2e0 to f245c11 Compare June 12, 2026 04:56
Base automatically changed from feat/rive-ios-experimental to main July 20, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant