+0,
-1
1@@ -1,2 +1 @@
2 examples/load_document
3-lib/
M
Makefile
+68,
-31
1@@ -1,53 +1,90 @@
2-.PHONY: all build test clean libyrs update-yffi
3+.PHONY: all build test clean libyrs libyrs-local libyrs-cross \
4+ libyrs-linux-amd64 libyrs-linux-arm64 \
5+ libyrs-freebsd-amd64 libyrs-freebsd-arm64 \
6+ install-rust-targets update-yffi install-rust-default
7
8-# Rust toolchain and target
9-RUST_TARGET :=
10-ifdef TARGET
11- RUST_TARGET := --target $(TARGET)
12+# Detect platform and architecture
13+UNAME_S := $(shell uname -s)
14+UNAME_M := $(shell uname -m)
15+
16+# Map architecture names
17+ifeq ($(UNAME_M),x86_64)
18+ ARCH := amd64
19+endif
20+ifeq ($(UNAME_M),amd64)
21+ ARCH := amd64
22+endif
23+ifeq ($(UNAME_M),aarch64)
24+ ARCH := arm64
25+endif
26+ifeq ($(UNAME_M),arm64)
27+ ARCH := arm64
28 endif
29
30-# Library extensions by platform
31-UNAME_S := $(shell uname -s)
32+# Set platform name
33 ifeq ($(UNAME_S),Linux)
34- STATIC_LIB := libyrs.a
35- DYNAMIC_LIB := libyrs.so
36-endif
37-ifeq ($(UNAME_S),Darwin)
38- STATIC_LIB := libyrs.a
39- DYNAMIC_LIB := libyrs.dylib
40+ PLATFORM := linux
41 endif
42 ifeq ($(UNAME_S),FreeBSD)
43- STATIC_LIB := libyrs.a
44- DYNAMIC_LIB := libyrs.so
45-endif
46-ifeq ($(OS),Windows_NT)
47- STATIC_LIB := yrs.lib
48- DYNAMIC_LIB := yrs.dll
49+ PLATFORM := freebsd
50 endif
51
52-all: libyrs build
53+# Export CGO_LDFLAGS to use the correct pre-built library
54+export CGO_LDFLAGS := -L$(PWD)/lib/$(PLATFORM)-$(ARCH) -lyrs -ldl -lm
55+
56+all: build
57
58-# Build the Rust static library from y-crdt submodule
59-libyrs:
60- cd y-crdt && cargo build --release -p yffi $(RUST_TARGET)
61- mkdir -p lib
62- cp y-crdt/target/release/$(STATIC_LIB) lib/ 2>/dev/null || \
63- cp y-crdt/target/$(TARGET)/release/$(STATIC_LIB) lib/
64+# Install Rust cross-compilation targets for FreeBSD
65+install-rust-targets:
66+ rustup default nightly
67+
68+# Build all cross-platform libraries (requires install-rust-targets for FreeBSD)
69+libyrs-cross: install-rust-default libyrs-linux-amd64 libyrs-freebsd-amd64
70+ @echo "All cross-platform libraries built successfully"
71+
72+# Individual platform builds
73+libyrs-linux-amd64: install-rust-targets
74+ cd y-crdt && rustup run nightly cargo rustc --release -p yffi --target x86_64-unknown-linux-gnu --crate-type staticlib
75+ mkdir -p lib/linux-amd64
76+ cp y-crdt/target/x86_64-unknown-linux-gnu/release/libyrs.a lib/linux-amd64/
77+ cp y-crdt/tests-ffi/include/libyrs.h lib/include/
78+ @echo "Built libyrs.a for linux-amd64"
79+
80+libyrs-freebsd-amd64: install-rust-targets
81+ rustup target add x86_64-unknown-freebsd --toolchain nightly
82+ cd y-crdt && rustup run nightly cargo rustc --release -p yffi --target x86_64-unknown-freebsd --crate-type staticlib
83+ mkdir -p lib/freebsd-amd64
84+ cp y-crdt/target/x86_64-unknown-freebsd/release/libyrs.a lib/freebsd-amd64/
85 cp y-crdt/tests-ffi/include/libyrs.h lib/include/
86+ @echo "Built libyrs.a for freebsd-amd64"
87
88-build: libyrs
89+# Local build for current platform (overwrites prebuilt if exists)
90+libyrs-local:
91+ rustup default nightly
92+ cd y-crdt && rustup run nightly cargo build --release -p yffi
93+ mkdir -p lib/$(PLATFORM)-$(ARCH)
94+ cp y-crdt/target/release/libyrs.a lib/$(PLATFORM)-$(ARCH)/
95+ cp y-crdt/tests-ffi/include/libyrs.h lib/include/
96+ @echo "Built libyrs.a for local platform: $(PLATFORM)-$(ARCH)"
97+
98+# Alias for local build
99+libyrs: libyrs-local
100+
101+build:
102 go build .
103
104-test: libyrs
105+test:
106 go test -v .
107
108+examples:
109+ cd examples && CGO_LDFLAGS="$(CGO_LDFLAGS)" go build .
110+
111 clean:
112 cd y-crdt && cargo clean
113- rm -rf lib/
114+ rm -rf lib/*/libyrs.a
115 go clean -cache
116
117 # Update y-crdt submodule to the latest tagged release
118-# This is a manual target - not a precondition for building
119 update-yffi:
120 @echo "Updating y-crdt submodule to latest tagged release..."
121 git submodule update --init --recursive
122@@ -60,4 +97,4 @@ update-yffi:
123 cd .. && \
124 sed -i "s/const YFFIVersion = \"[^\"]*/const YFFIVersion = \"$$LATEST_TAG/" yjs.go && \
125 echo "Updated YFFIVersion constant in yjs.go to $$LATEST_TAG"
126- @echo "Run 'make libyrs' to rebuild with the updated version"
127+ @echo "Run 'make libyrs-cross' to rebuild all libraries"
+264,
-0
1@@ -0,0 +1,264 @@
2+# ygo
3+
4+Go bindings for the Rust [y-crdt](https://github.com/y-crdt/y-crdt) library, providing CRDT (Conflict-free Replicated Data Types) functionality for building collaborative applications.
5+
6+## Overview
7+
8+This library provides Go bindings to y-crdt's Yjs-compatible CRDT implementation via CGO. It enables real-time collaborative editing with support for:
9+
10+- **Documents** - The core unit of collaborative state
11+- **Text** - Rich text with formatting and attributes
12+- **Arrays** - Ordered collections with move operations
13+- **Maps** - Key-value stores with nested types
14+- **XML** - XML fragment and element types
15+- **Transactions** - Atomic read/write operations
16+- **Updates** - State synchronization between peers
17+- **Undo/Redo** - Operation history management
18+
19+## Installation
20+
21+```bash
22+go get github.com/BTBurke/ygo
23+```
24+
25+## Building
26+
27+### Prerequisites
28+
29+- Go 1.22 or later
30+- C compiler (gcc or clang)
31+
32+The library includes pre-built static libraries for Linux and FreeBSD on amd64 and arm64. No Rust toolchain is required for normal use.
33+
34+### Linux / FreeBSD
35+
36+**Simple usage (using pre-built libraries):**
37+
38+```bash
39+go get github.com/BTBurke/ygo
40+go build .
41+```
42+
43+**For static linking (recommended for distribution):**
44+
45+```bash
46+CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static"' .
47+```
48+
49+For a completely static binary (no dynamic library dependencies):
50+```bash
51+CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static -ldl -lm"' .
52+```
53+
54+### Building from Source (optional)
55+
56+If you need to build the Rust library from source (e.g., for a different platform or to apply patches):
57+
58+1. **Install Rust toolchain:**
59+ ```bash
60+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
61+ ```
62+
63+2. **Clone with submodules and build:**
64+ ```bash
65+ git clone --recursive https://github.com/BTBurke/ygo
66+ cd ygo
67+ make libyrs-local # Builds for current platform, overwrites prebuilt
68+ go build .
69+ ```
70+
71+3. **Cross-compile for all supported platforms:**
72+ ```bash
73+ make libyrs-cross # Builds for linux-amd64, linux-arm64, freebsd-amd64, freebsd-arm64
74+ ```
75+
76+### macOS
77+
78+macOS does not support fully static binaries due to system requirements. Use dynamic linking:
79+
80+```bash
81+make libyrs
82+go build .
83+```
84+
85+### Windows
86+
87+Windows support requires MinGW-w64:
88+
89+```bash
90+make libyrs
91+set CGO_ENABLED=1
92+go build .
93+```
94+
95+## Quick Start
96+
97+```go
98+package main
99+
100+import (
101+ "fmt"
102+ "log"
103+
104+ "github.com/BTBurke/ygo"
105+)
106+
107+func main() {
108+ // Create a new document
109+ doc, err := ygo.NewDoc()
110+ if err != nil {
111+ log.Fatal(err)
112+ }
113+ defer doc.Destroy()
114+
115+ // Get or create a text field
116+ txt, err := doc.GetText("content")
117+ if err != nil {
118+ log.Fatal(err)
119+ }
120+ defer txt.Destroy()
121+
122+ // Insert text within a transaction
123+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
124+ txt.Insert(txn, 0, "Hello, collaborative world!")
125+ return nil
126+ })
127+ if err != nil {
128+ log.Fatal(err)
129+ }
130+
131+ // Read the text back
132+ var content string
133+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
134+ var err error
135+ content, err = txt.String(txn)
136+ return err
137+ })
138+ if err != nil {
139+ log.Fatal(err)
140+ }
141+
142+ fmt.Println(content) // "Hello, collaborative world!"
143+}
144+```
145+
146+## Serialization
147+
148+Documents support standard Go binary marshaling:
149+
150+```go
151+// Serialize document to bytes
152+data, err := doc.MarshalBinary()
153+if err != nil {
154+ log.Fatal(err)
155+}
156+
157+// Save to file or send over network
158+os.WriteFile("document.yjs", data, 0644)
159+
160+// Later, load it back
161+data, _ := os.ReadFile("document.yjs")
162+doc2, _ := ygo.NewDoc()
163+txt2, _ := doc2.GetText("content")
164+if err := doc2.UnmarshalBinary(data); err != nil {
165+ log.Fatal(err)
166+}
167+```
168+
169+## Collaborative Updates
170+
171+Synchronize documents between clients:
172+
173+```go
174+// Get current state vector
175+var sv1 *ygo.StateVector
176+err = doc1.WithReadTransaction(func(txn *ygo.Transaction) error {
177+ sv1 = txn.GetStateVector()
178+ return nil
179+})
180+
181+// Get state diff (updates needed by remote)
182+var diff *ygo.Update
183+err = doc1.WithWriteTransaction(func(txn *ygo.Transaction) error {
184+ diff = txn.GetStateDiff(sv2) // sv2 is remote's state vector
185+ return nil
186+})
187+
188+// Apply diff on remote
189+doc2.WithWriteTransaction(func(txn *ygo.Transaction) error {
190+ return txn.ApplyUpdate(diff)
191+})
192+```
193+
194+## API Documentation
195+
196+The library implements standard Go interfaces for seamless integration:
197+
198+- `encoding.BinaryMarshaler` / `encoding.BinaryUnmarshaler` - Document serialization
199+- Callback-based transactions with automatic commit/rollback
200+- Idiomatic Go error handling throughout
201+
202+See the [examples](examples/) directory for more complete usage examples.
203+
204+## Architecture
205+
206+```
207+Go Application
208+ ↓ (CGO)
209+ygo (Go bindings)
210+ ↓ (FFI)
211+libyrs (Rust static library)
212+ ↓
213+y-crdt (Rust CRDT implementation)
214+```
215+
216+The native Rust library is built as a static library and linked into your Go binary.
217+
218+## Platform Support
219+
220+Pre-built static libraries are included for:
221+
222+| Platform | Architecture | Status |
223+|----------|--------------|--------|
224+| Linux | amd64, arm64 | ✅ Pre-built libraries included |
225+| FreeBSD | amd64, arm64 | ✅ Pre-built libraries included |
226+
227+The library automatically selects the correct pre-built library based on your platform. Cross-compilation is not supported - build on the target platform or use the provided pre-built libraries.
228+
229+## Updating the Native Library (for maintainers)
230+
231+To update the pre-built libraries to the latest y-crdt release:
232+
233+```bash
234+# Update submodule and version constant
235+make update-yffi
236+
237+# Build for all supported platforms (requires Rust toolchain)
238+make libyrs-cross
239+
240+# The lib/ directory now contains updated libraries for all platforms
241+# Commit the changes to lib/ directory
242+```
243+
244+For local development (overwrites pre-built library for current platform):
245+
246+```bash
247+make libyrs-local
248+go build .
249+```
250+
251+## License
252+
253+MIT License - See [LICENSE](LICENSE) for details.
254+
255+## Contributing
256+
257+Contributions are welcome! Please ensure:
258+
259+1. Code follows existing patterns and conventions
260+2. Tests pass: `go test ./...`
261+3. Examples build: `cd examples && go build .`
262+
263+## Acknowledgments
264+
265+This project is a Go wrapper around the excellent [y-crdt](https://github.com/y-crdt/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.
+2,
-1
1@@ -1,8 +1,9 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6 "testing"
7+
8+ "github.com/BTBurke/ygo"
9 )
10
11 func TestArrayBasic(t *testing.T) {
+3,
-2
1@@ -3,12 +3,13 @@
2 "packages": [
3 "go@latest",
4 "gnumake@latest",
5- "rustc@latest",
6 "cargo@latest",
7 "coreutils@latest",
8 "bash@latest",
9 "ripgrep@latest",
10- "yarn@latest"
11+ "yarn@latest",
12+ "rustup@latest",
13+ "rustc@latest"
14 ],
15 "shell": {
16 "init_hook": [
+49,
-0
1@@ -538,6 +538,55 @@
2 }
3 }
4 },
5+ "rustup@latest": {
6+ "last_modified": "2026-03-21T07:29:51Z",
7+ "plugin_version": "0.0.1",
8+ "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#rustup",
9+ "source": "devbox-search",
10+ "version": "1.28.2",
11+ "systems": {
12+ "aarch64-darwin": {
13+ "outputs": [
14+ {
15+ "name": "out",
16+ "path": "/nix/store/z151hcfq4djwzmxxfdiy5cih1nly05aq-rustup-1.28.2",
17+ "default": true
18+ }
19+ ],
20+ "store_path": "/nix/store/z151hcfq4djwzmxxfdiy5cih1nly05aq-rustup-1.28.2"
21+ },
22+ "aarch64-linux": {
23+ "outputs": [
24+ {
25+ "name": "out",
26+ "path": "/nix/store/l1bgj6d6p3n9868n91vhb8fvzj3l0n69-rustup-1.28.2",
27+ "default": true
28+ }
29+ ],
30+ "store_path": "/nix/store/l1bgj6d6p3n9868n91vhb8fvzj3l0n69-rustup-1.28.2"
31+ },
32+ "x86_64-darwin": {
33+ "outputs": [
34+ {
35+ "name": "out",
36+ "path": "/nix/store/3rczx3p8ayxam5xp1h1jpq3cwwrzpak4-rustup-1.28.2",
37+ "default": true
38+ }
39+ ],
40+ "store_path": "/nix/store/3rczx3p8ayxam5xp1h1jpq3cwwrzpak4-rustup-1.28.2"
41+ },
42+ "x86_64-linux": {
43+ "outputs": [
44+ {
45+ "name": "out",
46+ "path": "/nix/store/8bwaqs52z4830qljdx989b86lqjq5ah3-rustup-1.28.2",
47+ "default": true
48+ }
49+ ],
50+ "store_path": "/nix/store/8bwaqs52z4830qljdx989b86lqjq5ah3-rustup-1.28.2"
51+ }
52+ }
53+ },
54 "yarn@latest": {
55 "last_modified": "2026-03-21T07:29:51Z",
56 "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#yarn",
+0,
-595
1@@ -1,595 +0,0 @@
2-# BinaryMarshaler and BinaryUnmarshaler Implementation Plan
3-
4-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
5-
6-**Goal:** Implement the standard Go `encoding.BinaryMarshaler` and `encoding.BinaryUnmarshaler` interfaces for the `Doc` type to enable idiomatic binary serialization with V1 encoding.
7-
8-**Architecture:** Add `MarshalBinary() ([]byte, error)` and `UnmarshalBinary([]byte) error` methods to the `Doc` type. MarshalBinary will use a read transaction to capture the full document state. UnmarshalBinary will use a write transaction to apply the binary data as an update. This provides a standard Go interface that works seamlessly with encoding packages.
9-
10-**Tech Stack:** Go, CGO with yffi library
11-
12----
13-
14-## Task Overview
15-
16-1. Add `MarshalBinary()` method to Doc type in `document.go`
17-2. Add `UnmarshalBinary()` method to Doc type in `document.go`
18-3. Write comprehensive tests in `document_test.go`
19-4. Update `example_test.go` to demonstrate idiomatic usage
20-5. Update `examples/load_document.go` to use the new API
21-6. Verify all tests pass
22-
23----
24-
25-## Task 1: Implement MarshalBinary Method
26-
27-**Files:**
28-- Modify: `/home/btburke/projects/ygo/document.go`
29-
30-**Implementation Details:**
31-
32-Add the `MarshalBinary` method to the Doc type. This method should:
33-1. Check if the document pointer is valid (return error if nil)
34-2. Use `WithReadTransaction` to create a read-only transaction
35-3. Inside the transaction, call `txn.GetStateDiff(nil)` to get the full document state
36-4. Return the raw bytes from the Update's `Data()` method
37-
38-**Code to Add:**
39-
40-```go
41-// MarshalBinary implements encoding.BinaryMarshaler.
42-// Returns the document state as V1-encoded binary data.
43-// This enables idiomatic usage with Go's encoding packages.
44-func (d *Doc) MarshalBinary() ([]byte, error) {
45- if d.ptr == nil {
46- return nil, ErrNilDocument
47- }
48-
49- var data []byte
50- err := d.WithReadTransaction(func(txn *Transaction) error {
51- update := txn.GetStateDiff(nil)
52- if update == nil {
53- return fmt.Errorf("failed to get document state")
54- }
55- data = update.Data()
56- return nil
57- })
58-
59- if err != nil {
60- return nil, fmt.Errorf("failed to marshal document: %w", err)
61- }
62-
63- return data, nil
64-}
65-```
66-
67-**Step 1: Add the import for "encoding" if not present**
68-
69-Check if the encoding package is imported. If not, add it to the imports.
70-
71-**Step 2: Add the MarshalBinary method**
72-
73-Add the method after the existing methods in document.go (around line 100, after Clone).
74-
75-**Step 3: Verify build**
76-
77-Run: `go build ./...`
78-Expected: SUCCESS (no errors)
79-
80----
81-
82-## Task 2: Implement UnmarshalBinary Method
83-
84-**Files:**
85-- Modify: `/home/btburke/projects/ygo/document.go`
86-
87-**Implementation Details:**
88-
89-Add the `UnmarshalBinary` method to the Doc type. This method should:
90-1. Check if the document pointer is valid (return error if nil)
91-2. Check if data is empty (return nil/error as appropriate)
92-3. Create an Update from the bytes using `UpdateFromBytes(data)`
93-4. Use `WithWriteTransaction` to apply the update inside a transaction
94-5. Return any error from the application
95-
96-**Code to Add:**
97-
98-```go
99-// UnmarshalBinary implements encoding.BinaryUnmarshaler.
100-// Applies V1-encoded binary data to the document.
101-// This enables idiomatic usage with Go's encoding packages.
102-func (d *Doc) UnmarshalBinary(data []byte) error {
103- if d.ptr == nil {
104- return ErrNilDocument
105- }
106-
107- if len(data) == 0 {
108- return nil // Nothing to apply
109- }
110-
111- update := UpdateFromBytes(data)
112- if update == nil {
113- return fmt.Errorf("failed to create update from data")
114- }
115-
116- return d.WithWriteTransaction(func(txn *Transaction) error {
117- return txn.ApplyUpdate(update)
118- })
119-}
120-```
121-
122-**Step 1: Add the UnmarshalBinary method**
123-
124-Add the method immediately after MarshalBinary in document.go.
125-
126-**Step 2: Verify build**
127-
128-Run: `go build ./...`
129-Expected: SUCCESS (no errors)
130-
131----
132-
133-## Task 3: Write Tests for MarshalBinary and UnmarshalBinary
134-
135-**Files:**
136-- Modify: `/home/btburke/projects/ygo/document_test.go`
137-
138-**Tests to Add:**
139-
140-**Test 1: TestMarshalUnmarshalRoundTrip**
141-
142-Tests that marshaling and unmarshaling preserves document content.
143-
144-```go
145-func TestMarshalUnmarshalRoundTrip(t *testing.T) {
146- // Create document with content
147- doc1, err := yjs.NewDoc()
148- if err != nil {
149- t.Fatalf("failed to create doc1: %v", err)
150- }
151- defer doc1.Destroy()
152-
153- txt1, err := doc1.GetText("content")
154- if err != nil {
155- t.Fatalf("failed to get text: %v", err)
156- }
157- defer txt1.Destroy()
158-
159- // Add some content
160- err = doc1.WithWriteTransaction(func(txn *yjs.Transaction) error {
161- txt1.Insert(txn, 0, "Hello World!")
162- return nil
163- })
164- if err != nil {
165- t.Fatalf("failed to insert text: %v", err)
166- }
167-
168- // Marshal the document
169- data, err := doc1.MarshalBinary()
170- if err != nil {
171- t.Fatalf("failed to marshal: %v", err)
172- }
173- if len(data) == 0 {
174- t.Fatal("marshal returned empty data")
175- }
176-
177- // Create new document and unmarshal
178- doc2, err := yjs.NewDoc()
179- if err != nil {
180- t.Fatalf("failed to create doc2: %v", err)
181- }
182- defer doc2.Destroy()
183-
184- // IMPORTANT: Initialize shared types before unmarshaling
185- txt2, err := doc2.GetText("content")
186- if err != nil {
187- t.Fatalf("failed to get text2: %v", err)
188- }
189- defer txt2.Destroy()
190-
191- // Unmarshal the data
192- if err := doc2.UnmarshalBinary(data); err != nil {
193- t.Fatalf("failed to unmarshal: %v", err)
194- }
195-
196- // Verify content matches
197- var content string
198- err = doc2.WithReadTransaction(func(txn *yjs.Transaction) error {
199- var err error
200- content, err = txt2.String(txn)
201- return err
202- })
203- if err != nil {
204- t.Fatalf("failed to read content: %v", err)
205- }
206-
207- if content != "Hello World!" {
208- t.Errorf("expected 'Hello World!', got '%s'", content)
209- }
210-}
211-```
212-
213-**Test 2: TestMarshalNilDocument**
214-
215-Tests that marshaling a nil document returns an error.
216-
217-```go
218-func TestMarshalNilDocument(t *testing.T) {
219- var doc *yjs.Doc
220- data, err := doc.MarshalBinary()
221- if err == nil {
222- t.Error("expected error when marshaling nil document")
223- }
224- if data != nil {
225- t.Error("expected nil data when marshaling nil document")
226- }
227-}
228-```
229-
230-**Test 3: TestUnmarshalNilDocument**
231-
232-Tests that unmarshaling to a nil document returns an error.
233-
234-```go
235-func TestUnmarshalNilDocument(t *testing.T) {
236- var doc *yjs.Doc
237- err := doc.UnmarshalBinary([]byte{1, 2, 3})
238- if err == nil {
239- t.Error("expected error when unmarshaling to nil document")
240- }
241-}
242-```
243-
244-**Test 4: TestUnmarshalEmptyData**
245-
246-Tests that unmarshaling empty data is a no-op.
247-
248-```go
249-func TestUnmarshalEmptyData(t *testing.T) {
250- doc, err := yjs.NewDoc()
251- if err != nil {
252- t.Fatalf("failed to create doc: %v", err)
253- }
254- defer doc.Destroy()
255-
256- // Unmarshal empty data should succeed and be a no-op
257- if err := doc.UnmarshalBinary([]byte{}); err != nil {
258- t.Fatalf("failed to unmarshal empty data: %v", err)
259- }
260- if err := doc.UnmarshalBinary(nil); err != nil {
261- t.Fatalf("failed to unmarshal nil data: %v", err)
262- }
263-}
264-```
265-
266-**Test 5: TestMarshalEmptyDocument**
267-
268-Tests that marshaling an empty document returns valid data.
269-
270-```go
271-func TestMarshalEmptyDocument(t *testing.T) {
272- doc, err := yjs.NewDoc()
273- if err != nil {
274- t.Fatalf("failed to create doc: %v", err)
275- }
276- defer doc.Destroy()
277-
278- data, err := doc.MarshalBinary()
279- if err != nil {
280- t.Fatalf("failed to marshal empty doc: %v", err)
281- }
282- // Empty document still has some metadata, so data should not be empty
283- if len(data) == 0 {
284- t.Error("marshal of empty document should return some data (metadata)")
285- }
286-}
287-```
288-
289-**Step 1: Add imports if needed**
290-
291-Ensure the test file imports the testing package and yjs.
292-
293-**Step 2: Add all 5 test functions**
294-
295-Add these tests after the existing tests in document_test.go.
296-
297-**Step 3: Run tests**
298-
299-Run: `go test -v -run TestMarshal ./...`
300-Expected: All 5 tests pass
301-
302----
303-
304-## Task 4: Update example_test.go to Demonstrate Idiomatic Usage
305-
306-**Files:**
307-- Modify: `/home/btburke/projects/ygo/example_test.go`
308-
309-**Update TestExampleDocumentLoading**
310-
311-Replace the current implementation that manually uses transactions with the idiomatic MarshalBinary/UnmarshalBinary approach:
312-
313-**Current Pattern:**
314-```go
315-// Create sample document
316-var fullState *yjs.Update
317-err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
318- txt.Insert(txn, 0, "Hello from Yjs!")
319- fullState = txn.GetStateDiff(nil)
320- return nil
321-})
322-
323-// Write to file
324-os.WriteFile(filePath, fullState.Data(), 0644)
325-
326-// Load it back
327-data, _ := os.ReadFile(filePath)
328-doc2, _ := yjs.NewDoc()
329-txt2, _ := doc2.GetText("content")
330-err = doc2.WithWriteTransaction(func(txn *yjs.Transaction) error {
331- update := yjs.UpdateFromBytes(data)
332- return txn.ApplyUpdate(update)
333-})
334-```
335-
336-**New Idiomatic Pattern:**
337-```go
338-// Create sample document
339-err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
340- txt.Insert(txn, 0, "Hello from Yjs!")
341- return nil
342-})
343-
344-// Marshal document to bytes
345-data, err := doc.MarshalBinary()
346-if err != nil {
347- t.Fatalf("failed to marshal: %v", err)
348-}
349-
350-// Write to file
351-os.WriteFile(filePath, data, 0644)
352-
353-// Load it back
354-data, _ = os.ReadFile(filePath)
355-doc2, _ := yjs.NewDoc()
356-txt2, _ := doc2.GetText("content")
357-
358-// Unmarshal directly
359-doc2.UnmarshalBinary(data)
360-
361-// Verify content
362-var content string
363-err = doc2.WithReadTransaction(func(txn *yjs.Transaction) error {
364- content, err = txt2.String(txn)
365- return err
366-})
367-```
368-
369-**Step 1: Read the current example_test.go**
370-
371-**Step 2: Update the test to use MarshalBinary and UnmarshalBinary**
372-
373-Keep the t.Log() calls for progress tracking, but replace the transaction-heavy code with the simpler marshal/unmarshal approach.
374-
375-**Step 3: Run the test**
376-
377-Run: `go test -v -run TestExample ./...`
378-Expected: PASS
379-
380----
381-
382-## Task 5: Update examples/load_document.go
383-
384-**Files:**
385-- Modify: `/home/btburke/projects/ygo/examples/load_document.go`
386-
387-**Update Both Functions:**
388-
389-**loadAndDisplayDocument:**
390-- Replace the manual transaction-based loading with `doc.UnmarshalBinary(data)`
391-- Keep the transaction-based reading for display purposes (needed to access content)
392-
393-**createSampleDocument:**
394-- Replace the manual transaction-based state capture with `doc.MarshalBinary()`
395-
396-**Step 1: Read current examples/load_document.go**
397-
398-**Step 2: Update createSampleDocument**
399-
400-Old:
401-```go
402-func createSampleDocument(filePath string) error {
403- // ... create doc, get types ...
404-
405- txn, err := doc.WriteTransaction()
406- if err != nil {
407- return fmt.Errorf("failed to create transaction: %w", err)
408- }
409-
410- // Add content
411- txt.Insert(txn, 0, "Hello from Yjs!")
412- arr.InsertRange(txn, 0, items)
413-
414- fullState := txn.GetStateDiff(nil)
415- txn.Commit()
416-
417- os.WriteFile(filePath, fullState.Data(), 0644)
418- return nil
419-}
420-```
421-
422-New:
423-```go
424-func createSampleDocument(filePath string) error {
425- // ... create doc, get types ...
426-
427- // Add content
428- err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
429- txt.Insert(txn, 0, "Hello from Yjs!")
430- arr.InsertRange(txn, 0, items)
431- return nil
432- })
433- if err != nil {
434- return fmt.Errorf("failed to add content: %w", err)
435- }
436-
437- // Marshal document
438- data, err := doc.MarshalBinary()
439- if err != nil {
440- return fmt.Errorf("failed to marshal document: %w", err)
441- }
442-
443- os.WriteFile(filePath, data, 0644)
444- return nil
445-}
446-```
447-
448-**Step 3: Update loadAndDisplayDocument**
449-
450-Old:
451-```go
452-func loadAndDisplayDocument(filePath string) error {
453- data, _ := os.ReadFile(filePath)
454-
455- doc := yjs.NewDoc()
456- // ... initialize types ...
457-
458- txn, err := doc.WriteTransaction()
459- if err != nil {
460- return fmt.Errorf("failed to create transaction: %w", err)
461- }
462-
463- update := yjs.UpdateFromBytes(data)
464- if err := txn.ApplyUpdate(update); err != nil {
465- txn.Commit()
466- return fmt.Errorf("failed to apply update: %w", err)
467- }
468- txn.Commit()
469-
470- // ... read and display ...
471-}
472-```
473-
474-New:
475-```go
476-func loadAndDisplayDocument(filePath string) error {
477- data, err := os.ReadFile(filePath)
478- if err != nil {
479- return fmt.Errorf("failed to read file: %w", err)
480- }
481-
482- doc, err := yjs.NewDoc()
483- if err != nil {
484- return fmt.Errorf("failed to create doc: %w", err)
485- }
486- defer doc.Destroy()
487-
488- // IMPORTANT: Initialize shared types BEFORE unmarshaling
489- txt, err := doc.GetText("content")
490- if err != nil {
491- return fmt.Errorf("failed to get text: %w", err)
492- }
493- defer txt.Destroy()
494-
495- arr, err := doc.GetArray("items")
496- if err != nil {
497- return fmt.Errorf("failed to get array: %w", err)
498- }
499- defer arr.Destroy()
500-
501- // Unmarshal directly
502- if err := doc.UnmarshalBinary(data); err != nil {
503- return fmt.Errorf("failed to unmarshal document: %w", err)
504- }
505-
506- // ... read and display using transactions ...
507-}
508-```
509-
510-**Step 4: Build and test example**
511-
512-Run: `cd examples && go build -o load_document .`
513-Expected: SUCCESS
514-
515-Run: `./examples/load_document`
516-Expected: Displays document content successfully
517-
518----
519-
520-## Task 6: Final Verification
521-
522-**Step 1: Run full test suite**
523-
524-Run: `go test -v ./...`
525-Expected: All tests pass
526-
527-**Step 2: Verify example builds**
528-
529-Run: `go build ./... && cd examples && go build .`
530-Expected: Both build successfully
531-
532-**Step 3: Check interface compliance**
533-
534-Add a compile-time check to ensure Doc implements the interfaces:
535-
536-```go
537-// Compile-time interface compliance check
538-var (
539- _ encoding.BinaryMarshaler = (*Doc)(nil)
540- _ encoding.BinaryUnmarshaler = (*Doc)(nil)
541-)
542-```
543-
544-Add this to document.go after the imports.
545-
546-Run: `go build ./...`
547-Expected: SUCCESS (confirms interface compliance)
548-
549----
550-
551-## Summary
552-
553-**Files Modified:**
554-1. `/home/btburke/projects/ygo/document.go` - Add MarshalBinary and UnmarshalBinary methods
555-2. `/home/btburke/projects/ygo/document_test.go` - Add 5 comprehensive tests
556-3. `/home/btburke/projects/ygo/example_test.go` - Update to use idiomatic API
557-4. `/home/btburke/projects/ygo/examples/load_document.go` - Simplify using new API
558-
559-**Benefits of New API:**
560-
561-1. **Idiomatic Go** - Implements standard library interfaces
562-2. **Simpler usage** - No manual transaction management for serialization
563-3. **Works with encoding packages** - Can be used with `gob`, `json` (when embedded), etc.
564-4. **Cleaner code** - Reduces boilerplate significantly
565-5. **Composability** - Easy to serialize documents in larger data structures
566-
567-**Before/After Comparison:**
568-
569-```go
570-// BEFORE (old API)
571-data, err := doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
572- update := txn.GetStateDiff(nil)
573- data = update.Data()
574- return nil
575-})
576-os.WriteFile("doc.yjs", data, 0644)
577-
578-// Load back
579-data, _ := os.ReadFile("doc.yjs")
580-doc2.WithWriteTransaction(func(txn *yjs.Transaction) error {
581- update := yjs.UpdateFromBytes(data)
582- return txn.ApplyUpdate(update)
583-})
584-
585-// AFTER (new idiomatic API)
586-data, _ := doc.MarshalBinary()
587-os.WriteFile("doc.yjs", data, 0644)
588-
589-// Load back
590-data, _ = os.ReadFile("doc.yjs")
591-doc2.UnmarshalBinary(data)
592-```
593-
594-**Note on Backward Compatibility:**
595-
596-The existing `txn.GetStateDiff()`, `txn.ApplyUpdate()`, and `yjs.UpdateFromBytes()` functions remain available for advanced use cases (e.g., V2 encoding, partial updates, delta sync). The new MarshalBinary/UnmarshalBinary methods provide the idiomatic 90% use case.
1@@ -1,542 +0,0 @@
2-# Callback-Based Transaction API Implementation Plan
3-
4-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
5-
6-**Goal:** Replace manual transaction management with a callback-based API that automatically commits or rolls back based on callback return values.
7-
8-**Architecture:** Replace `WriteTransaction()` and `ReadTransaction()` methods with `WithWriteTransaction(fn)` and `WithReadTransaction(fn)` that take a callback function. The function creates the transaction, executes the callback, and handles commit/rollback automatically based on whether the callback returns an error.
9-
10-**Tech Stack:** Go, CGO with yffi library
11-
12----
13-
14-## Task Overview
15-
16-1. Update `transaction.go` - Replace existing methods with callback-based API
17-2. Update test files - All 55 transaction calls across 11 test files
18-3. Update example - 3 transaction calls in `examples/load_document.go`
19-4. Run full test suite to verify all changes work
20-
21----
22-
23-## Task 1: Update transaction.go with Callback API
24-
25-**Files:**
26-- Modify: `/home/btburke/projects/ygo/transaction.go`
27-
28-**Step 1: Add Rollback method for write transactions**
29-
30-Add a new `Rollback()` method to handle transaction rollback (needed for write transactions that fail):
31-
32-```go
33-// Rollback aborts the transaction without applying changes.
34-// This is used internally when a callback returns an error.
35-func (t *Transaction) Rollback() {
36- if t.ptr != nil {
37- C.ytransaction_commit(t.ptr) // yffi uses commit to end, even for rollback
38- t.ptr = nil
39- runtime.SetFinalizer(t, nil)
40- }
41-}
42-```
43-
44-**Step 2: Replace ReadTransaction with WithReadTransaction**
45-
46-Replace the existing `ReadTransaction()` method with:
47-
48-```go
49-// WithReadTransaction executes a callback within a read-only transaction.
50-// The transaction is automatically committed after the callback completes.
51-// Returns any error from the callback.
52-func (d *Doc) WithReadTransaction(fn func(*Transaction) error) error {
53- if d.ptr == nil {
54- return ErrNilDocument
55- }
56-
57- txn := C.ydoc_read_transaction(d.ptr)
58- if txn == nil {
59- return fmt.Errorf("failed to create read transaction: another transaction may be active")
60- }
61- t := &Transaction{ptr: txn, doc: d}
62-
63- // Execute callback
64- err := fn(t)
65-
66- // Always commit read transactions (they don't modify state)
67- t.Commit()
68-
69- return err
70-}
71-```
72-
73-**Step 3: Replace WriteTransaction and WriteTransactionWithOrigin**
74-
75-Replace both methods with:
76-
77-```go
78-// WithWriteTransaction executes a callback within a read-write transaction.
79-// If the callback returns nil, the transaction is committed.
80-// If the callback returns an error, the transaction is rolled back.
81-// Returns any error from the callback.
82-func (d *Doc) WithWriteTransaction(fn func(*Transaction) error) error {
83- return d.WithWriteTransactionWithOrigin(nil, fn)
84-}
85-
86-// WithWriteTransactionWithOrigin executes a callback within a read-write transaction with an origin marker.
87-// The origin can be used by event handlers and undo managers to identify change sources.
88-// If the callback returns nil, the transaction is committed.
89-// If the callback returns an error, the transaction is rolled back.
90-func (d *Doc) WithWriteTransactionWithOrigin(origin []byte, fn func(*Transaction) error) error {
91- if d.ptr == nil {
92- return ErrNilDocument
93- }
94-
95- var originPtr *C.char
96- var originLen C.uint32_t
97- if len(origin) > 0 {
98- originPtr = (*C.char)(unsafe.Pointer(&origin[0]))
99- originLen = C.uint32_t(len(origin))
100- }
101-
102- txn := C.ydoc_write_transaction(d.ptr, originLen, originPtr)
103- if txn == nil {
104- return fmt.Errorf("failed to create write transaction: another transaction may be active")
105- }
106- t := &Transaction{ptr: txn, doc: d}
107-
108- // Execute callback
109- err := fn(t)
110-
111- if err != nil {
112- // Rollback on error
113- t.Rollback()
114- return err
115- }
116-
117- // Commit on success
118- t.Commit()
119- return nil
120-}
121-```
122-
123-**Step 4: Remove the finalizer from transaction creation**
124-
125-Since we now handle commit/rollback explicitly in the With* methods, remove:
126-```go
127-runtime.SetFinalizer(t, (*Transaction).Commit) // REMOVE THIS LINE
128-```
129-
130-**Step 5: Verify build**
131-
132-Run: `go build ./...`
133-Expected: SUCCESS (no errors)
134-
135----
136-
137-## Task 2: Update transaction_test.go
138-
139-**Files:**
140-- Modify: `/home/btburke/projects/ygo/transaction_test.go`
141-
142-**Step 1: Update TestReadTransaction**
143-
144-Replace:
145-```go
146-func TestReadTransaction(t *testing.T) {
147- doc, err := yjs.NewDoc()
148- if err != nil {
149- t.Fatalf("failed to create doc: %v", err)
150- }
151- defer doc.Destroy()
152-
153- txn, err := doc.ReadTransaction()
154- if err != nil {
155- t.Fatalf("failed to create read transaction: %v", err)
156- }
157- defer txn.Commit()
158-
159- if txn.IsWriteable() {
160- t.Error("read transaction should not be writeable")
161- }
162-}
163-```
164-
165-With:
166-```go
167-func TestReadTransaction(t *testing.T) {
168- doc, err := yjs.NewDoc()
169- if err != nil {
170- t.Fatalf("failed to create doc: %v", err)
171- }
172- defer doc.Destroy()
173-
174- err = doc.WithReadTransaction(func(txn *yjs.Transaction) error {
175- if txn.IsWriteable() {
176- t.Error("read transaction should not be writeable")
177- }
178- return nil
179- })
180- if err != nil {
181- t.Fatalf("transaction failed: %v", err)
182- }
183-}
184-```
185-
186-**Step 2: Update TestWriteTransaction**
187-
188-Replace with callback pattern.
189-
190-**Step 3: Update TestTransactionConflict**
191-
192-This test needs special handling since it tests concurrent transactions. The first transaction should use the callback pattern, but the second transaction attempt (which should fail) remains as a simple call since it tests the failure case:
193-
194-```go
195-func TestTransactionConflict(t *testing.T) {
196- doc, err := yjs.NewDoc()
197- if err != nil {
198- t.Fatalf("failed to create doc: %v", err)
199- }
200- defer doc.Destroy()
201-
202- // First transaction uses callback pattern
203- err = doc.WithWriteTransaction(func(txn1 *yjs.Transaction) error {
204- if !txn1.IsWriteable() {
205- t.Error("write transaction should be writeable")
206- }
207-
208- // Try to create second concurrent transaction (should fail)
209- err := doc.WithWriteTransaction(func(txn2 *yjs.Transaction) error {
210- // This should not execute
211- t.Error("expected second write transaction to fail while first is open")
212- return nil
213- })
214- if err == nil {
215- t.Error("expected second write transaction to fail while first is open")
216- }
217-
218- return nil
219- })
220- if err != nil {
221- t.Fatalf("first transaction failed: %v", err)
222- }
223-}
224-```
225-
226-**Step 4: Update TestTransactionWithOrigin**
227-
228-Replace with callback pattern.
229-
230-**Step 5: Run tests**
231-
232-Run: `go test -v -run TestReadTransaction|TestWriteTransaction|TestTransaction ./...`
233-Expected: All transaction tests pass
234-
235----
236-
237-## Task 3: Update document_test.go
238-
239-**Files:**
240-- Modify: `/home/btburke/projects/ygo/document_test.go` (no transaction calls, just verify it still compiles)
241-
242-Run: `go test -v -run TestNewDoc ./...`
243-Expected: PASS
244-
245----
246-
247-## Task 4: Update text_test.go
248-
249-**Files:**
250-- Modify: `/home/btburke/projects/ygo/text_test.go`
251-
252-**Step 1: Update TestTextBasic**
253-
254-Replace:
255-```go
256- txn, err := doc.WriteTransaction()
257- if err != nil {
258- t.Fatalf("failed to create transaction: %v", err)
259- }
260- defer txn.Commit()
261-
262- txt.Insert(txn, 0, "hello")
263- txt.Insert(txn, 5, " world")
264- txt.RemoveRange(txn, 0, 6)
265-
266- length, err := txt.Len(txn)
267- ...
268-```
269-
270-With:
271-```go
272- var length uint32
273- var str string
274-
275- err = doc.WithWriteTransaction(func(txn *yjs.Transaction) error {
276- txt.Insert(txn, 0, "hello")
277- txt.Insert(txn, 5, " world")
278- txt.RemoveRange(txn, 0, 6)
279-
280- var err error
281- length, err = txt.Len(txn)
282- if err != nil {
283- return err
284- }
285-
286- str, err = txt.String(txn)
287- return err
288- })
289- if err != nil {
290- t.Fatalf("transaction failed: %v", err)
291- }
292-
293- if length != 5 {
294- t.Errorf("expected length 5, got %d", length)
295- }
296- if str != "world" {
297- t.Errorf("expected 'world', got '%s'", str)
298- }
299-```
300-
301-**Step 2: Update TestTextInsertWithAttributes**
302-
303-Similar pattern - move all operations inside callback, use closure variables for results.
304-
305-**Step 3: Update TestTextFormat**
306-
307-Same pattern.
308-
309-**Step 4: Update TestTextUnicode**
310-
311-Same pattern.
312-
313-**Step 5: Run tests**
314-
315-Run: `go test -v -run TestText ./...`
316-Expected: All text tests pass
317-
318----
319-
320-## Task 5: Update array_test.go
321-
322-**Files:**
323-- Modify: `/home/btburke/projects/ygo/array_test.go`
324-
325-**Step 1-3: Update TestArrayBasic, TestArrayPush, TestArrayMove**
326-
327-Replace each transaction pattern with callback style, using closure variables to capture results that need to be checked after the transaction.
328-
329-**Step 4: Run tests**
330-
331-Run: `go test -v -run TestArray ./...`
332-Expected: All array tests pass
333-
334----
335-
336-## Task 6: Update map_test.go
337-
338-**Files:**
339-- Modify: `/home/btburke/projects/ygo/map_test.go`
340-
341-**Step 1-2: Update TestMapBasic, TestMapNested**
342-
343-Replace transaction patterns with callbacks.
344-
345-**Step 3: Run tests**
346-
347-Run: `go test -v -run TestMap ./...`
348-Expected: All map tests pass
349-
350----
351-
352-## Task 7: Update xml_test.go
353-
354-**Files:**
355-- Modify: `/home/btburke/projects/ygo/xml_test.go`
356-
357-**Step 1-3: Update TestXmlElementBasic, TestXmlText, TestXmlNestedElements**
358-
359-Replace transaction patterns with callbacks. These tests have more complex assertions, so use closure variables effectively.
360-
361-**Step 4: Run tests**
362-
363-Run: `go test -v -run TestXml ./...`
364-Expected: All XML tests pass
365-
366----
367-
368-## Task 8: Update update_test.go
369-
370-**Files:**
371-- Modify: `/home/btburke/projects/ygo/update_test.go`
372-
373-**Step 1-2: Update TestUpdateExchange, TestFullStateSnapshot**
374-
375-These tests have two documents with concurrent transactions. Structure them carefully:
376-
377-For TestUpdateExchange:
378-```go
379-// Make concurrent edits using callbacks
380-var sv1, sv2 *yjs.StateVector
381-var diff1, diff2 *yjs.Update
382-
383-err = d1.WithWriteTransaction(func(txn1 *yjs.Transaction) error {
384- txt1.Insert(txn1, 0, "world")
385-
386- // Get state vector
387- sv1 = txn1.GetStateVector()
388-
389- // Get diff needs sv2, but it's not available yet
390- // We need to restructure this test...
391-
392- return nil
393-})
394-```
395-
396-This test is tricky because it exchanges state vectors between two concurrent transactions. We may need to restructure it to create one transaction at a time rather than trying to hold both open simultaneously.
397-
398-**Step 3: Run tests**
399-
400-Run: `go test -v -run TestUpdate ./...`
401-Expected: All update tests pass
402-
403----
404-
405-## Task 9: Update undo_test.go
406-
407-**Files:**
408-- Modify: `/home/btburke/projects/ygo/undo_test.go`
409-
410-**Step 1-3: Update TestUndoManagerBasic, TestUndoManagerStop, TestUndoManagerWithRemoteChanges**
411-
412-These tests involve undo manager with explicit `mgr.Stop()` calls between transactions. The callback pattern works well here since each transaction is independent.
413-
414-**Step 4: Run tests**
415-
416-Run: `go test -v -run TestUndo ./...`
417-Expected: All undo tests pass
418-
419----
420-
421-## Task 10: Update sticky_test.go
422-
423-**Files:**
424-- Modify: `/home/btburke/projects/ygo/sticky_test.go`
425-
426-**Step 1-3: Update TestStickyIndexBasic, TestStickyIndexSurvivesChanges, TestStickyIndexJSON**
427-
428-These tests mix write and read transactions. Replace each appropriately.
429-
430-**Step 4: Run tests**
431-
432-Run: `go test -v -run TestSticky ./...`
433-Expected: All sticky tests pass
434-
435----
436-
437-## Task 11: Update weak_test.go
438-
439-**Files:**
440-- Modify: `/home/btburke/projects/ygo/weak_test.go`
441-
442-**Step 1-4: Update TestWeakLinkText, TestWeakLinkArrayCreateOnly, TestWeakLinkArrayInputOnly, TestWeakLinkArrayFullWorkflow**
443-
444-Replace transaction patterns. Note that `TestWeakLinkArrayFullWorkflow` is already skipped due to known memory corruption.
445-
446-**Step 5: Run tests**
447-
448-Run: `go test -v -run TestWeak ./...`
449-Expected: All weak tests pass (except skipped one)
450-
451----
452-
453-## Task 12: Update example_test.go
454-
455-**Files:**
456-- Modify: `/home/btburke/projects/ygo/example_test.go`
457-
458-**Step 1: Update TestExampleDocumentLoading**
459-
460-This is the most complex test with multiple sequential transactions. Replace:
461-- First WriteTransaction for creating content
462-- Second WriteTransaction for applying update
463-- ReadTransaction for verifying content
464-
465-Use closure variables to capture `fullState` and `content` between transactions.
466-
467-**Step 2: Run tests**
468-
469-Run: `go test -v -run TestExample ./...`
470-Expected: Example test passes
471-
472----
473-
474-## Task 13: Update examples/load_document.go
475-
476-**Files:**
477-- Modify: `/home/btburke/projects/ygo/examples/load_document.go`
478-
479-**Step 1: Update loadAndDisplayDocument function**
480-
481-Replace the read transaction at lines 72-61 with callback pattern.
482-
483-**Step 2: Update createSampleDocument function**
484-
485-Replace the write transaction at lines 163-153 with callback pattern.
486-
487-**Step 3: Build example**
488-
489-Run: `cd examples && go build -o load_document .`
490-Expected: SUCCESS
491-
492-**Step 4: Run example**
493-
494-Run: `./examples/load_document`
495-Expected: Displays document content successfully (may crash at end due to known yffi issue)
496-
497----
498-
499-## Task 14: Final Verification
500-
501-**Step 1: Run full test suite**
502-
503-Run: `go test -v ./...`
504-Expected: All tests pass
505-
506-**Step 2: Verify build of main package and example**
507-
508-Run: `go build ./... && cd examples && go build .`
509-Expected: Both build successfully
510-
511-**Step 3: Document the breaking change**
512-
513-Add to CHANGELOG or README that this is a breaking API change:
514-- `doc.WriteTransaction()` → `doc.WithWriteTransaction(func(*Transaction) error) error`
515-- `doc.ReadTransaction()` → `doc.WithReadTransaction(func(*Transaction) error) error`
516-- `defer txn.Commit()` pattern no longer needed
517-
518----
519-
520-## Summary of Changes
521-
522-**Files Modified:**
523-1. `/home/btburke/projects/ygo/transaction.go` - Core API change
524-2. `/home/btburke/projects/ygo/transaction_test.go` - 4 tests
525-3. `/home/btburke/projects/ygo/text_test.go` - 4 tests
526-4. `/home/btburke/projects/ygo/array_test.go` - 3 tests
527-5. `/home/btburke/projects/ygo/map_test.go` - 2 tests
528-6. `/home/btburke/projects/ygo/xml_test.go` - 3 tests
529-7. `/home/btburke/projects/ygo/update_test.go` - 2 tests
530-8. `/home/btburke/projects/ygo/undo_test.go` - 3 tests
531-9. `/home/btburke/projects/ygo/sticky_test.go` - 3 tests
532-10. `/home/btburke/projects/ygo/weak_test.go` - 4 tests
533-11. `/home/btburke/projects/ygo/example_test.go` - 1 test
534-12. `/home/btburke/projects/ygo/examples/load_document.go` - 2 functions
535-
536-**Total: 58 transaction call sites updated**
537-
538-**Benefits of New API:**
539-- No more `defer txn.Commit()` footgun
540-- Automatic rollback on write errors
541-- Cleaner, more Go-idiomatic code
542-- Easier to reason about transaction lifecycle
543-- Reduced boilerplate (no explicit Commit calls)
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -2,7 +2,7 @@ package ygo_test
2
3 import (
4 "fmt"
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "os"
8 "testing"
9 )
+2,
-2
1@@ -2,6 +2,6 @@ module load_example
2
3 go 1.22
4
5-replace github.com/y-crdt/ygo => ../
6+replace github.com/BTBurke/ygo => ../
7
8-require github.com/y-crdt/ygo v0.0.0
9+require github.com/BTBurke/ygo v0.0.0
+1,
-1
1@@ -5,7 +5,7 @@ import (
2 "log"
3 "os"
4
5- ygo "github.com/y-crdt/ygo"
6+ ygo "github.com/BTBurke/ygo"
7 )
8
9 // loadAndDisplayDocument reads a Yjs document from a binary file and displays its contents
M
go.mod
+1,
-1
1@@ -1,3 +1,3 @@
2-module github.com/y-crdt/ygo
3+module github.com/BTBurke/ygo
4
5 go 1.22
+0,
-0
+0,
-0
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -3,7 +3,7 @@ package ygo_test
2 import (
3 "unsafe"
4
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "testing"
8 )
9
+1,
-1
1@@ -1,7 +1,7 @@
2 package ygo_test
3
4 import (
5- "github.com/y-crdt/ygo"
6+ "github.com/BTBurke/ygo"
7 "strings"
8 "testing"
9 )