README.md

ygo

Go bindings for the Rust y-crdt library, providing CRDT (Conflict-free Replicated Data Types) functionality for building collaborative applications.

Features

Prerequisites

The library uses Cgo and includes pre-built static libraries for Linux and FreeBSD on amd64 and arm64. No Rust toolchain is required for normal use.

Creating Documents

New Document

doc, _ := ygo.NewDoc()
// Cgo bindings require manually freeing memory (sorry!)
defer doc.Destroy()

txt, _ := doc.GetText("content")
defer txt.Destroy()

_ = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
    txt.Insert(txn, 0, "Hello, world!")
    return nil
})

Create a document from a Yjs V1 Update

data, _ := os.ReadFile("document.yjs")

doc, _ := ygo.NewDoc()
defer doc.Destroy()

_ = doc.UnmarshalBinary(data)

Create a document from the y-sweet format (similar to Yjs V1 format, but tracks changes)

// From file
doc, _ := ygo.NewDocFromYSweetFile("data.ysweet")
defer doc.Destroy()

// From reader
doc, _ := ygo.NewDocFromYSweet(reader)
defer doc.Destroy()

Transactions

When you want to make changes to a Y document, we offer two APIs:

Callback Pattern (Atomic)

For simple changes, callback pattern automatically creates a transaction for you. The underlying library has no concept of rollbacks, so a failed insert, push, etc. may leave you in an indeterminate state.

_ = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
    txt.Insert(txn, 0, "Hello")
    arr.Push(txn, ygo.String("item"))
    m.Insert(txn, "key", ygo.Int(42))
    return nil
})
_ = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
    content, _ := txt.String(txn)
    length := arr.Len()
    return nil
})

Explicit Pattern (Flat)

Use when you want flatter code with manual control. Rollback and Commit are basically the same thing, don’t assume that rollback fixes anything. This is a limitation of the underlying Rust library. Open an issue if this is a problem; the fix is to snapshot it before an attempted transaction and roll back to it.

txn, _ := doc.BeginWrite()
defer func() {
    if err != nil {
        txn.Rollback()
    } else {
        txn.Commit()
    }
}()

txt.Insert(txn, 0, "Hello")
arr.Push(txn, ygo.String("item"))
txn, _ := doc.BeginRead()
defer txn.Commit()

length := arr.Len()
content, _ := txt.String(txn)

With Origin Markers

Track change sources:

_ = doc.WithWriteTransactionWithOrigin([]byte("user-edit"), func(txn *ygo.Transaction) error {
    txt.Insert(txn, 0, "Hello")
    return nil
})

txn, _ := doc.BeginWriteWithOrigin([]byte("system"))
defer txn.Commit()

Working with Data Types

Text

txt, _ := doc.GetText("content")
defer txt.Destroy()

_ = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
    txt.Insert(txn, 0, "Hello, ")
    txt.Insert(txn, 7, "world!")
    txt.RemoveRange(txn, 0, 7)
    length := txt.Len(txn)
    content, _ := txt.String(txn)
    return nil
})

Array

arr, _ := doc.GetArray("items")
defer arr.Destroy()

_ = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
    arr.Push(txn, ygo.String("first"))
    arr.Insert(txn, 0, ygo.Int(42))
    length := arr.Len()
    return nil
})

Map

m, _ := doc.GetMap("data")
defer m.Destroy()

_ = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
    m.Insert(txn, "name", ygo.String("Alice"))
    m.Insert(txn, "count", ygo.Int(10))

    name, _ := m.GetString(txn, "name")
    count, _ := m.GetInt(txn, "count")
    return nil
})

Iteration

_ = m.ForEach(txn, func(key string, value *ygo.Output) error {
    // Process entry
    return nil
})

_ = arr.ForEach(txn, func(index uint32, value *ygo.Output) error {
    // Process element
    return nil
})

Sync to y-sweet Server

Simple Sync

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

_ = doc.Sync(ctx,
    ygo.WithSyncEndpoint("wss://y-sweet.example.com/doc/my-doc"),
    ygo.WithSyncAuthToken("token"),
    ygo.WithOnUpdate(func(d *ygo.Doc) error {
        // React to remote changes
        return nil
    }),
)

With Additional Lifecycle Hooks

doc, _ = ygo.NewDoc()

client, _ := ygo.NewSyncClient(doc,
    ygo.WithSyncEndpoint("wss://y-sweet.example.com/doc/my-doc"),
    ygo.WithSyncAuthToken("token"),
)

client.OnConnect(func(doc *ygo.Doc, stats ygo.SyncStats) error {
    log.Printf("Connected with %d peers", stats.PeerCount)
    return nil
})

client.OnDisconnect(func(doc *ygo.Doc, stats ygo.SyncStats, reason ygo.DisconnectReason) error {
    log.Printf("Disconnected: %v", reason)
    return nil
})

client.OnUpdate(func(doc *ygo.Doc, stats ygo.SyncStats) error {
    // Handle remote update
    return nil
})

_ = client.Connect(ctx)

y-sweet API Client

The ysweet package contains a client for interacting with an upstream y-sweet server. This is needed when you need to create new documents and generate authentication credentials for the y-sweet Yjs sync provider.

import "github.com/BTBurke/ygo/ysweet"

client, _ := ysweet.NewClient("http://localhost:8080")

// Create new document
docID, _ := client.NewDoc("")

// Or with specific ID
docID, _ := client.NewDoc("my-document")

// Get authentication for sync
auth, _ := client.AuthDoc(docID)
// you can return this auth struct in JSON to configure the y-sweet Yjs provider

//use it to configure a Go client to connect to the y-sweet server
wsURL := auth.WebsocketURL()

Proxy Server

This library provides a proxy server that you can put in front of y-sweet to proxy the websocket connections from a Yjs document. The advantage of this is that you don’t need to protect server-only routes on the y-sweet server that are meant to create documents and generate authorization credentials. It also adds hooks so you can also react to connection/disconnection events when Yjs clients connect to your y-sweet server.

Default URL Pattern

The default y-sweet routes returns URLs like /d/{docID}/ws/{docID}. You can optionally change this, see the custom URL scheme below.

handler := ysweet.ProxyHandler("ws://upstream:8080",
    ysweet.WithTargetAuthToken("secret"),
    ysweet.WithOnConnect(func(ctx context.Context, docID string, w http.ResponseWriter, r *http.Request) error {
        // Validate connection
        return nil
    }),
    ysweet.WithOnDisconnect(func(ctx context.Context, docID string, reason ysweet.DisconnectReason) error {
        // Cleanup on disconnect
        return nil
    }),
)
http.Handle("/d/", handler)

Custom URL Scheme

When you want a custom URL scheme, provide a function to extract the document ID. Internally, this is changed to the URL scheme that y-sweet expects.

handler := ysweet.ProxyHandler("ws://upstream:8080",
    ysweet.WithDocIDFunc(func(r *http.Request) string {
        // Extract docID from custom URL: /ws/{docID}
        return r.PathValue("docID")
    }),
)
http.Handle("/ws/{docID}", handler)

Serialization

Implements the binary encoding interfaces using the Yjs V1 format.

// Serialize
data, _ := doc.MarshalBinary()
os.WriteFile("document.yjs", data, 0644)

// Deserialize
data, _ := os.ReadFile("document.yjs")
doc2, _ := ygo.NewDoc()
_ = doc2.UnmarshalBinary(data)

State Vectors and Updates

// Get state vector
var sv *ygo.StateVector
_ = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
    sv = txn.GetStateVector()
    return nil
})

// Get diff for remote
doc.WithReadTransaction(func(txn *ygo.Transaction) error {
    // this is the same as the Yjs function encodeStateAsUpdate V1 algorithm
    diff := txn.GetStateDiff(sv)
    // Send diff to remote
    return nil
})

// Apply update from remote
_ = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
    update := ygo.UpdateFromBytes(data)
    return txn.ApplyUpdate(update)
})

Platform Support

Pre-built static libraries are included for:

Platform Architecture Status
Linux amd64 ✅ Included
FreeBSD amd64 ✅ Included

Updating the Native Library (for maintainers)

# Update submodule and version constant
make update-yffi

# Build for all supported platforms
make libyrs-cross

# For local development (current platform only)
make libyrs-local

License

MIT License - See LICENSE for details.

Acknowledgments

This project is a Go wrapper around y-crdt Rust library by the Yjs team. All CRDT logic and algorithms are implemented in y-crdt; ygo provides the Go bindings and API layer.