This project provides Go bindings for the Yrs CRDT library, enabling JSON Patch-based synchronization for Go applications. The core component is the Doc Go type, which wraps a Yrs document and exposes methods for manipulation and state management.
The project is set up to build static C-compatible libraries from the Rust yffi crate for multiple target architectures, which are then consumed by the Go package using cgo.
DocGo Type: A Go struct that manages an underlying Yrs document.- JSON Patch Synchronization: Apply JSON patches to update the document state.
- State Serialization: Get the document state as a JSON-compatible
map[string]interface{}. - State Vector Management:
GetStateVector(): Serialize the Yrs document state into a compact byte vector (Yrs update format v1).ApplyStateVector(): Restore a document from a previously obtained state vector.
- Cross-Platform Static Libraries: Builds
.astatic libraries for:- Linux x86_64 (
x86_64-unknown-linux-gnu) - Linux ARM64 (
aarch64-unknown-linux-gnu) - macOS x86_64 (Intel) (
x86_64-apple-darwin) - macOS ARM64 (Apple Silicon) (
aarch64-apple-darwin) - Windows x86_64 (MinGW) (
x86_64-pc-windows-gnu)
- Linux x86_64 (
Setting up the build environment requires Go, Rust, and several tools for cross-compilation if you intend to build for all target architectures.
Install Go (version 1.18 or later recommended for cgo improvements).
- Follow the official instructions: golang.org/doc/install
Install Rust using rustup.
- Follow the official instructions: rustup.rs
Add the Rust target toolchains for the architectures you intend to build. The Makefile is configured for all five listed above.
rustup target add x86_64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-gnu
rustup target add x86_64-apple-darwin # Usually present if on macOS Intel
rustup target add aarch64-apple-darwin # Usually present if on macOS ARM
rustup target add x86_64-pc-windows-gnuIf the project uses Git submodules (e.g., for thirdParty/y-crdt), you'll need to initialize and update them:
git submodule update --init --recursiveInstall cbindgen to generate the C header file (libyrs.h) from the Rust code.
brew install cbindgenTo build the Rust static libraries for non-native targets (e.g., building for Linux or Windows from macOS), you need appropriate C cross-compiler toolchains. The Rust build process (Cargo) needs these to link the static libraries correctly.
For macOS users (using Homebrew):
- Linux x86_64:
brew tap messense/macos-cross-toolchains brew install x86_64-unknown-linux-gnu
- Linux ARM64:
# brew tap messense/macos-cross-toolchains brew install aarch64-unknown-linux-gnu - Windows x86_64 (MinGW):
This provides
brew install mingw-w64
x86_64-w64-mingw32-gccand related tools.
Ensure the installed cross-compilers are in your PATH.
The Makefile provides several targets to manage the build process:
-
Clean Artifacts:
make clean
Removes previously built Rust libraries, Go binaries, and the
yrs_packagedirectory. -
Build Static Libraries and Header:
make yrs
This is the primary target for preparing the C-compatible artifacts. It will:
- Compile the
yffiRust crate intolibyrs.afor all target architectures defined inTARGET_TRIPLES. - Copy these static libraries to
yrs_package/lib/<target_triple>/libyrs.a. - Generate
libyrs.husingcbindgenfromyffi. - Copy and patch
libyrs.hintoyrs_package/include/libyrs.h.
- Compile the
-
Run Go Package Tests:
make build_go
This depends on
make yrsand then runs the Go tests for theautosyncpackage (go test . -v), linking against the host architecture's static library. -
Build All (Alias for
build_go):make all
Typical Build Workflow:
make clean
make yrs # Prepare library artifacts
make build_go # Run tests for the autosync packageThe autosync package (defined in the root directory) provides the Doc type.
-
Ensure
cgois Enabled:cgois required for Go to interface with C libraries. It's enabled by default but ensureCGO_ENABLED=1if you've changed it. -
Import the Package: Assuming this module (
github.com/ProlificLabs/autosync) is a dependency:import ( "fmt" "github.com/ProlificLabs/autosync" )
If using it locally, you might use a
replacedirective in the consuming module'sgo.modfile.
d := autosync.NewDoc(): Creates a newDoc.d.Destroy(): Frees the underlying Yrs C resources. Crucial to call this when done to prevent memory leaks.jsonState, err := d.ToJSON(): Gets the current document state asmap[string]interface{}.err := d.ApplyOperations(patchList): Applies ajsonpatch.JSONPatchListto the document.stateVec, err := d.GetStateVector(): Serializes the document state to a byte slice.err := d.ApplyStateVector(stateVec): Applies a previously obtained state vector to the document.appliedPatches, err := d.UpdateToState(newStateMap): Calculates the JSON patch needed to transform the document's current state tonewStateMap, applies it, and returns the patches.
This demonstrates basic usage within a Go program. You would integrate this logic into your application where needed.
package main
import (
"fmt"
"log"
"github.com/ProlificLabs/autosync"
"github.com/snorwin/jsonpatch" // For creating patch objects
)
func main() {
doc := autosync.NewDoc()
// IMPORTANT: Ensure Destroy is called eventually, e.g., using defer in a relevant scope
defer doc.Destroy()
// Initial state
initialJSON, _ := doc.ToJSON()
fmt.Println("Initial state:", initialJSON)
// Apply an "add" operation
// Corresponds to: {"op": "add", "path": "/foo", "value": "bar"}
patch1, err := jsonpatch.ParsePatch([]byte(`[{"op": "add", "path": "/foo", "value": "bar"}]`))
if err != nil {
log.Fatal("Failed to parse patch1:", err)
}
err = doc.ApplyOperations(patch1)
if err != nil {
log.Fatal("Failed to apply patch1:", err)
}
state1, _ := doc.ToJSON()
fmt.Println("State after patch1:", state1) // Should be map[foo:bar]
// Update to a new state
newState := map[string]interface{}{
"foo": "baz",
"newKey": 123,
}
applied, err := doc.UpdateToState(newState)
if err != nil {
log.Fatal("Failed to update to state:", err)
}
fmt.Println("Applied patches for UpdateToState:", applied.String())
finalState, _ := doc.ToJSON()
fmt.Println("Final state:", finalState) // Should be map[foo:baz newKey:123]
// Get state vector
stateVec, err := doc.GetStateVector()
if err != nil {
log.Fatal("Failed to get state vector:", err)
}
fmt.Printf("State vector length: %d bytes\n", len(stateVec))
// Create a new doc and apply state vector
doc2 := autosync.NewDoc()
defer doc2.Destroy()
err = doc2.ApplyStateVector(stateVec)
if err != nil {
log.Fatal("Failed to apply state vector to doc2:", err)
}
stateDoc2, _ := doc2.ToJSON()
fmt.Println("State of doc2 from vector:", stateDoc2) // Should match finalState
}./Makefile: Main build script../go.mod,./go.sum: Go module definition files../autosync.go,./autosync_test.go: The Go package source and test files../.cargo/config.toml: Cargo configuration for cross-compilation linkers../yrs_package/: Output directory created bymake yrs../yrs_package/include/libyrs.h: The generated C header file../yrs_package/lib/<target_triple>/libyrs.a: The compiled static libraries for each architecture.
./thirdParty/y-crdt/: Submodule or vendored code for the Yrs Rust library../thirdParty/y-crdt/yffi/: The Rust FFI crate that is compiled.