react-native-automerge-generated
v0.1.0
Published
Native Automerge bindings for React Native via UniFFI
Readme
react-native-automerge-generated
Native Automerge bindings for React Native via compiled Rust + UniFFI.
Replaces the WebAssembly-based Automerge backend with a native Rust implementation exposed through JSI, enabling:
- Hermes support — no longer requires JSC for WebAssembly
- Android support — WASM-based automerge only worked reliably on iOS
- No JSC patches — eliminates the
createArrayBufferpatch for JSCRuntime.cpp - No Metro hacks — no more slim→fullfat resolver redirects
- Better performance — direct native calls via JSI instead of WASM interpretation
Architecture
App TypeScript
│
@automerge/automerge/slim ← UseApi(nativeApi)
│
useapi-adapter.ts (type conversion + API bridge)
│
src/generated/automerge.ts (auto-generated by ubrn)
│ JSI (C++)
cpp/generated/ (auto-generated by ubrn)
│ FFI (C ABI)
uniffi_automerge Rust crate (UniFFI 0.29)
│
automerge core (v0.7.3)Hand-written components:
rust/— UniFFI-annotated Rust wrapper (ported from automerge-swift)src/useapi-adapter.ts— bridges generated API to@automerge/automerge/slim
Everything else (C++, ObjC++, Kotlin, generated TypeScript) is auto-generated by uniffi-bindgen-react-native.
Installation
npm install react-native-automerge-generated
# or
yarn add react-native-automerge-generatediOS
cd ios && pod installAndroid
Gradle sync should pick up the native module automatically.
Usage
import { UseApi } from '@automerge/automerge/slim';
import { nativeApi } from 'react-native-automerge-generated';
// Initialize once at app startup
UseApi(nativeApi);
// Then use automerge normally
import * as Automerge from '@automerge/automerge/slim';
let doc = Automerge.init();
doc = Automerge.change(doc, d => {
d.key = 'value';
});
const saved = Automerge.save(doc);
const loaded = Automerge.load(saved);Building from Source
Prerequisites
- Rust toolchain (1.89+):
rustup install stable - iOS targets:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim - Android targets:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android - Android NDK (via Android Studio)
- uniffi-bindgen-react-native:
npm install(dev dependency)
Build
# iOS (builds universal static lib + generates bindings)
npm run build:ios
# Android (builds for all ABIs + generates bindings)
npm run build:android
# Check Rust compiles
cd rust && cargo checkProject Structure
rust/
src/
automerge.udl UniFFI interface definition (50+ methods)
doc.rs Doc wrapper around automerge::AutoCommit
sync_state.rs SyncState wrapper
scalar_value.rs ScalarValue enum (String, Int, Uint, F64, etc.)
value.rs Value enum (Object | Scalar)
patches.rs Patch/PatchAction types
obj_id.rs ObjId custom type (ArrayBuffer)
actor_id.rs ActorId custom type (ArrayBuffer)
change_hash.rs ChangeHash custom type (ArrayBuffer)
cursor.rs Cursor custom type (ArrayBuffer)
...
Cargo.toml
src/
useapi-adapter.ts UseApi bridge (NativeAutomerge + nativeApi)
generated/
automerge.ts Auto-generated TypeScript bindings
automerge-ffi.ts Auto-generated FFI layer
index.ts Package entry pointType Mapping
The adapter converts between the generated API types and what @automerge/automerge/slim expects:
| automerge/slim | Generated API | Conversion |
|---|---|---|
| ObjId (string "_root") | ArrayBuffer | base64 with o: prefix |
| ChangeHash (hex string) | ArrayBuffer | hex encode/decode |
| ActorId (hex string) | ArrayBuffer | hex encode/decode |
| Cursor (string) | ArrayBuffer | base64 with c: prefix |
| Uint8Array | Array<number> | Array.from() / new Uint8Array() |
| JS primitives | ScalarValue tagged union | tag-based dispatch |
Current Limitations
- Stubbed methods:
unmark(),spans(),getBlock(),updateBlock(),saveBundle(),encodeChange()throw or return empty values. These are not needed for core automerge operations. getChanges()returns concatenated bytes — the Rustencode_changes_since()returns all changes as a single byte array rather than individually split changes. This works forloadIncremental()/applyChanges()but callers expecting individual change objects should be aware.decodeChange()returns partial data — the native implementation returns change metadata (hash, actor, timestamp, message, deps) but not the fullopsarray orseq/startOpfields.- No WASM fallback — this is a complete replacement, not a supplement.
Development Notes
Post-Build Hook
The build process includes a post-build script (scripts/add-useapi-export.sh) that automatically injects the nativeApi export into index.ts after ubrn generates the bindings. This is necessary because ubrn regenerates index.ts from a template on each build.
The export line:
export { nativeApi, NativeAutomerge, NativeSyncState } from './useapi-adapter';This runs automatically after npm run build:ios or npm run build:android.
Testing
The package includes a comprehensive test suite with 40+ test cases covering all major Automerge operations.
# Install dependencies
npm install
# Build the native module first (required)
npm run build:ios # or build:android
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm test -- --coverageTest coverage includes:
- Document creation, cloning, and manipulation
- Map, List, Text, and Counter operations
- Save/load persistence
- Merge operations and conflict resolution
- Change history and decodeChange()
- Sync protocol
- Complex nested structures
See tests/README.md for detailed test documentation.
License
Apache 2.0
