Fix len API to align with Go expected behavior around nils
14 files changed,  +165, -82
M README.md
+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.
M array_test.go
+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 {
M compat_test.go
+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 			}
M examples/load_document.go
+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 
M examples/sync_demo.go
+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
A len_test.go
+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.
M map_test.go
+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")
M sticky_test.go
+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).
M text_test.go
+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.
M xml_test.go
+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 		}