ygo
Go bindings for the Rust y-crdt library, providing CRDT (Conflict-free Replicated Data Types) functionality for building collaborative applications.
Features
- Yjs CRDT Bindings - Go bindings to the Rust y-crdt library for manipulating Yjs documents (Text, Array, Map, XML types)
- y-sweet Integration - Sync documents in real-time with jamsocket/y-sweet servers via WebSocket
- Document Hooks - React to changes with
OnUpdatecallbacks for document modifications - Proxy Hooks - Intercept and act on connections when using ygo as a WebSocket proxy to y-sweet
Prerequisites
- Go 1.22 or later
- C compiler (gcc or clang)
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.