14 files changed,
+165,
-82
+37,
-0
1@@ -232,6 +232,43 @@ The explicit API gives you full control over transaction lifecycle:
2
3 **Note on Rollback:** The y-crdt library applies changes immediately during the transaction. Calling `Rollback()` only closes the transaction without syncing to remote peers - the local document state is already modified. For true atomic operations that automatically rollback on error, use `WithWriteTransaction()` which only commits if the callback succeeds.
4
5+### Breaking API Changes (v0.x → v1.0)
6+
7+**Len() Methods Simplified:**
8+
9+All `Len()` methods have been updated for consistency:
10+
11+```go
12+// Old API - returned (uint32, error)
13+length, err := arr.Len()
14+if err != nil {
15+ return err
16+}
17+
18+// New API - returns just uint32, panics on nil (like Go's len())
19+length := arr.Len() // Array.Len() - no txn needed
20+length := m.Len(txn) // Map.Len() - txn still required
21+length := txt.Len(txn) // Text.Len() - txn still required
22+```
23+
24+**Error Handling - Nil Checks Now Panic:**
25+
26+Methods now panic on programming errors (nil pointers) instead of returning errors:
27+
28+```go
29+// Old API
30+length, err := arr.Len()
31+if err != nil {
32+ // Handle nil array error
33+}
34+
35+// New API - panics like Go's built-in len()
36+var arr *ygo.Array
37+length := arr.Len() // panic: "ygo: Array.Len called on nil Array"
38+```
39+
40+This aligns with Go best practices where programming errors (nil dereferences) should fail fast. Only runtime errors (key not found, index out of bounds) return errors.
41+
42 ## Serialization
43
44 Documents support standard Go binary marshaling:
M
array.go
+5,
-8
1@@ -38,11 +38,12 @@ func (a *Array) Destroy() {
2 }
3
4 // Len returns the number of elements.
5-func (a *Array) Len() (uint32, error) {
6+// Panics if the Array is nil (like len() on a nil slice).
7+func (a *Array) Len() uint32 {
8 if a.branch == nil {
9- return 0, ErrNilBranch
10+ panic("ygo: Array.Len called on nil Array")
11 }
12- return uint32(C.yarray_len(a.branch)), nil
13+ return uint32(C.yarray_len(a.branch))
14 }
15
16 // Get returns the element at index.
17@@ -87,11 +88,7 @@ func (a *Array) InsertRange(txn *Transaction, index uint32, items []Input) error
18
19 // Push adds an item to the end.
20 func (a *Array) Push(txn *Transaction, item Input) error {
21- len, err := a.Len()
22- if err != nil {
23- return err
24- }
25- return a.InsertRange(txn, len, []Input{item})
26+ return a.InsertRange(txn, a.Len(), []Input{item})
27 }
28
29 // RemoveRange removes elements starting at index.
+6,
-8
1@@ -34,9 +34,8 @@ func TestArrayBasic(t *testing.T) {
2 }
3 arr.InsertRange(txn, 0, items)
4
5- var err error
6- length, err = arr.Len()
7- return err
8+ length = arr.Len()
9+ return nil
10 })
11 if err != nil {
12 t.Fatalf("transaction failed: %v", err)
13@@ -50,9 +49,8 @@ func TestArrayBasic(t *testing.T) {
14 err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
15 arr.RemoveRange(txn, 1, 1)
16
17- var err error
18- length, err = arr.Len()
19- return err
20+ length = arr.Len()
21+ return nil
22 })
23 if err != nil {
24 t.Fatalf("transaction failed: %v", err)
25@@ -102,7 +100,7 @@ func TestArrayPush(t *testing.T) {
26 arr.Push(txn, ygo.Int(2))
27
28 var err error
29- length, err = arr.Len()
30+ length = arr.Len()
31 return err
32 })
33 if err != nil {
34@@ -139,7 +137,7 @@ func TestArrayMove(t *testing.T) {
35 // Array should now have [2, 3, 1]
36 // Just verify length is preserved
37 var err error
38- length, err = arr.Len()
39+ length = arr.Len()
40 return err
41 })
42 if err != nil {
+4,
-10
1@@ -158,7 +158,7 @@ func TestYjsCompatibility(t *testing.T) {
2 var length uint32
3 err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
4 var err error
5- length, err = arr.Len()
6+ length = arr.Len()
7 return err
8 })
9 if err != nil {
10@@ -318,7 +318,7 @@ func TestYjsCompatibility(t *testing.T) {
11 var length uint32
12 err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
13 var err error
14- length, err = arr.Len()
15+ length = arr.Len()
16 return err
17 })
18 if err != nil {
19@@ -489,10 +489,7 @@ func TestYjsCompatibility(t *testing.T) {
20 }
21 defer arr.Destroy()
22
23- length, err := arr.Len()
24- if err != nil {
25- t.Fatalf("failed to get array length: %v", err)
26- }
27+ length := arr.Len()
28 if length != 3 {
29 t.Errorf("expected length 3, got %d", length)
30 }
31@@ -553,10 +550,7 @@ func TestYjsCompatibility(t *testing.T) {
32 defer arr.Destroy()
33
34 err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
35- length, err := arr.Len()
36- if err != nil {
37- t.Fatalf("failed to get array length: %v", err)
38- }
39+ length := arr.Len()
40 if length != 2 {
41 t.Errorf("expected length 2, got %d", length)
42 }
+2,
-8
1@@ -70,10 +70,7 @@ func loadAndDisplayDocument(filePath string) error {
2
3 // Access array field
4 if arr != nil {
5- arrLen, err := arr.Len()
6- if err != nil {
7- return fmt.Errorf("failed to get array length: %w", err)
8- }
9+ arrLen := arr.Len()
10 fmt.Printf("Array \"items\" length: %d\n", arrLen)
11
12 // Read first few items
13@@ -106,10 +103,7 @@ func loadAndDisplayDocument(filePath string) error {
14
15 // Access map field
16 if m != nil {
17- mLen, err := m.Len(txn)
18- if err != nil {
19- return fmt.Errorf("failed to get map length: %w", err)
20- }
21+ mLen := m.Len(txn)
22 fmt.Printf("Map \"metadata\" entries: %d\n", mLen)
23 }
24
+1,
-4
1@@ -89,10 +89,7 @@ func main() {
2 // add this text to the end
3 if !fired {
4 if err := d.WithWriteTransaction(func(txn *ygo.Transaction) error {
5- length, err := txt2.Len(txn)
6- if err != nil {
7- return err
8- }
9+ length := txt2.Len(txn)
10 txt2.Insert(txn, length+1, " Right back at you from client 2")
11 fired = true
12 return nil
+85,
-0
1@@ -0,0 +1,85 @@
2+package ygo_test
3+
4+import (
5+ "testing"
6+
7+ "github.com/BTBurke/ygo"
8+)
9+
10+func TestArrayLenPanicsOnNil(t *testing.T) {
11+ defer func() {
12+ if r := recover(); r == nil {
13+ t.Error("expected panic for nil Array")
14+ }
15+ }()
16+ var arr *ygo.Array
17+ _ = arr.Len() // should panic
18+}
19+
20+func TestMapLenPanicsOnNil(t *testing.T) {
21+ defer func() {
22+ if r := recover(); r == nil {
23+ t.Error("expected panic for nil Map")
24+ }
25+ }()
26+ var m *ygo.Map
27+ _ = m.Len(nil) // should panic
28+}
29+
30+func TestTextLenPanicsOnNil(t *testing.T) {
31+ defer func() {
32+ if r := recover(); r == nil {
33+ t.Error("expected panic for nil Text")
34+ }
35+ }()
36+ var txt *ygo.Text
37+ _ = txt.Len(nil) // should panic
38+}
39+
40+func TestArrayLen(t *testing.T) {
41+ doc, err := ygo.NewDoc()
42+ if err != nil {
43+ t.Fatalf("failed to create doc: %v", err)
44+ }
45+ defer doc.Destroy()
46+
47+ arr, err := doc.GetArray("test")
48+ if err != nil {
49+ t.Fatalf("failed to get array: %v", err)
50+ }
51+ defer arr.Destroy()
52+
53+ // Initially empty
54+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
55+ length := arr.Len()
56+ if length != 0 {
57+ t.Errorf("expected length 0, got %d", length)
58+ }
59+ return nil
60+ })
61+ if err != nil {
62+ t.Fatalf("transaction failed: %v", err)
63+ }
64+
65+ // Add items
66+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
67+ arr.Push(txn, ygo.Int(1))
68+ arr.Push(txn, ygo.Int(2))
69+ return nil
70+ })
71+ if err != nil {
72+ t.Fatalf("transaction failed: %v", err)
73+ }
74+
75+ // Check length again
76+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
77+ length := arr.Len()
78+ if length != 2 {
79+ t.Errorf("expected length 2, got %d", length)
80+ }
81+ return nil
82+ })
83+ if err != nil {
84+ t.Fatalf("transaction failed: %v", err)
85+ }
86+}
M
map.go
+5,
-4
1@@ -38,14 +38,15 @@ func (m *Map) Destroy() {
2 }
3
4 // Len returns the number of entries.
5-func (m *Map) Len(txn *Transaction) (uint32, error) {
6+// Panics if the Map is nil or if txn is nil.
7+func (m *Map) Len(txn *Transaction) uint32 {
8 if m.branch == nil {
9- return 0, ErrNilBranch
10+ panic("ygo: Map.Len called on nil Map")
11 }
12 if txn == nil || txn.ptr == nil {
13- return 0, ErrNilTransaction
14+ panic("ygo: Map.Len called with nil transaction")
15 }
16- return uint32(C.ymap_len(m.branch, txn.ptr)), nil
17+ return uint32(C.ymap_len(m.branch, txn.ptr))
18 }
19
20 // Insert adds or updates a key-value pair.
+4,
-11
1@@ -31,11 +31,7 @@ func TestMapBasic(t *testing.T) {
2 arrayItems := []ygo.Input{ygo.Int(11), ygo.Int(22)}
3 m.Insert(txn, "b", ygo.JSONArray(arrayItems))
4
5- var err error
6- length, err = m.Len(txn)
7- if err != nil {
8- return err
9- }
10+ length = m.Len(txn)
11
12 // Remove key twice
13 removed1, err = m.Remove(txn, "a")
14@@ -55,8 +51,8 @@ func TestMapBasic(t *testing.T) {
15
16 // Clear map
17 m.Clear(txn)
18- length, err = m.Len(txn)
19- return err
20+ length = m.Len(txn)
21+ return nil
22 })
23 if err != nil {
24 t.Fatalf("transaction failed: %v", err)
25@@ -111,10 +107,7 @@ func TestMapNested(t *testing.T) {
26
27 m.Insert(txn, "outerMap", outerMap)
28
29- length, mapErr = m.Len(txn)
30- if mapErr != nil {
31- return mapErr
32- }
33+ length = m.Len(txn)
34
35 // Retrieve and verify nested structure
36 out, err = m.Get(txn, "outerMap")
+1,
-5
1@@ -28,11 +28,7 @@ func TestStickyIndexBasic(t *testing.T) {
2 txt.Insert(txn, 0, "y")
3 txt.Insert(txn, 0, "x")
4
5- var err error
6- length, err = txt.Len(txn)
7- if err != nil {
8- return err
9- }
10+ length = txt.Len(txn)
11
12 for i := uint32(0); i < length; i++ {
13 for _, assoc := range []ygo.Assoc{ygo.AssocBefore, ygo.AssocAfter} {
M
text.go
+6,
-9
1@@ -39,14 +39,15 @@ func (t *Text) Destroy() {
2 }
3
4 // Len returns the length of the text in UTF code units (based on doc encoding).
5-func (t *Text) Len(txn *Transaction) (uint32, error) {
6+// Panics if the Text is nil or if txn is nil.
7+func (t *Text) Len(txn *Transaction) uint32 {
8 if t.branch == nil {
9- return 0, ErrNilBranch
10+ panic("ygo: Text.Len called on nil Text")
11 }
12 if txn == nil || txn.ptr == nil {
13- return 0, ErrNilTransaction
14+ panic("ygo: Text.Len called with nil transaction")
15 }
16- return uint32(C.ytext_len(t.branch, txn.ptr)), nil
17+ return uint32(C.ytext_len(t.branch, txn.ptr))
18 }
19
20 // String returns the text content as a Go string.
21@@ -126,11 +127,7 @@ func (t *Text) Format(txn *Transaction, index, length uint32, attrs Input) error
22
23 // Push appends text to the end.
24 func (t *Text) Push(txn *Transaction, text string) error {
25- len, err := t.Len(txn)
26- if err != nil {
27- return err
28- }
29- return t.Insert(txn, len, text)
30+ return t.Insert(txn, t.Len(txn), text)
31 }
32
33 // Branch returns the underlying branch pointer (for advanced use).
+3,
-7
1@@ -28,11 +28,7 @@ func TestTextBasic(t *testing.T) {
2 txt.Insert(txn, 5, " world")
3 txt.RemoveRange(txn, 0, 6)
4
5- var err error
6- length, err = txt.Len(txn)
7- if err != nil {
8- return err
9- }
10+ length = txt.Len(txn)
11
12 str, err = txt.String(txn)
13 return err
14@@ -72,8 +68,8 @@ func TestTextInsertWithAttributes(t *testing.T) {
15 }
16 txt.InsertWithAttributes(txn, 0, "bold text", attrs)
17
18- length, jsonErr = txt.Len(txn)
19- return jsonErr
20+ length = txt.Len(txn)
21+ return nil
22 })
23 if err != nil {
24 t.Fatalf("transaction failed: %v", err)
M
xml.go
+5,
-4
1@@ -230,14 +230,15 @@ func (t *XmlText) Destroy() {
2 }
3
4 // Len returns the text length.
5-func (t *XmlText) Len(txn *Transaction) (uint32, error) {
6+// Panics if the XmlText is nil or if txn is nil.
7+func (t *XmlText) Len(txn *Transaction) uint32 {
8 if t.branch == nil {
9- return 0, ErrNilBranch
10+ panic("ygo: XmlText.Len called on nil XmlText")
11 }
12 if txn == nil || txn.ptr == nil {
13- return 0, ErrNilTransaction
14+ panic("ygo: XmlText.Len called with nil transaction")
15 }
16- return uint32(C.yxmltext_len(t.branch, txn.ptr)), nil
17+ return uint32(C.yxmltext_len(t.branch, txn.ptr))
18 }
19
20 // String returns the text content.
+1,
-4
1@@ -107,10 +107,7 @@ func TestXmlText(t *testing.T) {
2
3 txt.Insert(txn, 0, "hello world")
4
5- length, err = txt.Len(txn)
6- if err != nil {
7- return err
8- }
9+ length = txt.Len(txn)
10 if length != 11 {
11 t.Errorf("expected length 11, got %d", length)
12 }