code review improvements - simplify API, remove runtime hacks
12 files changed,  +615, -52
M README.md
+57, -0
 1@@ -15,6 +15,8 @@ This library provides Go bindings to y-crdt's Yjs-compatible CRDT implementation
 2 - **Updates** - State synchronization between peers
 3 - **Undo/Redo** - Operation history management
 4 
 5+**Resource Management:** All types (`Doc`, `Text`, `Array`, `Map`, `XmlFragment`, etc.) require explicit cleanup. Always use `defer obj.Destroy()` when creating these objects to prevent memory leaks. See the Quick Start example below.
 6+
 7 ## Installation
 8 
 9 ```bash
10@@ -124,6 +126,61 @@ func main() {
11 }
12 ```
13 
14+## Convenience Features
15+
16+### Typed Map Getters
17+
18+Get typed values from maps without manual type checking:
19+
20+```go
21+// Instead of verbose Output handling:
22+// out, _ := m.Get(txn, "name")
23+// defer out.Destroy()
24+// str, _ := out.String()
25+
26+// Use convenience getters:
27+name, _ := m.GetString(txn, "name")
28+age, _ := m.GetInt(txn, "age")
29+pi, _ := m.GetFloat(txn, "pi")
30+active, _ := m.GetBool(txn, "active")
31+```
32+
33+### Map/Array Iteration
34+
35+Iterate with callbacks instead of manual iterator management:
36+
37+```go
38+// Map iteration
39+err = m.ForEach(txn, func(key string, value *ygo.Output) error {
40+    // Process each entry
41+    // Return error to stop iteration early
42+    return nil
43+})
44+
45+// Array iteration  
46+err = arr.ForEach(txn, func(index uint32, value *ygo.Output) error {
47+    // Process each element
48+    return nil
49+})
50+```
51+
52+### Map Input from Go Maps
53+
54+Create inputs from Go maps instead of parallel slices:
55+
56+```go
57+// Old: parallel slices (error-prone)
58+// ygo.YMap([]string{"name", "age"}, []ygo.Input{ygo.String("Alice"), ygo.Int(30)})
59+
60+// New: Go map syntax
61+data := map[string]ygo.Input{
62+    "name": ygo.String("Alice"),
63+    "age":  ygo.Int(30),
64+}
65+input, _ := ygo.MapInput(data)
66+m.Insert(txn, "user", input)
67+```
68+
69 ## Serialization
70 
71 Documents support standard Go binary marshaling:
M array.go
+31, -3
 1@@ -8,7 +8,6 @@ import "C"
 2 import (
 3 	"encoding/json"
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -31,13 +30,11 @@ func (d *Doc) GetArray(name string) (*Array, error) {
10 	}
11 
12 	a := &Array{branch: branch}
13-	runtime.SetFinalizer(a, (*Array).Destroy)
14 	return a, nil
15 }
16 
17 // Destroy releases resources.
18 func (a *Array) Destroy() {
19-	runtime.SetFinalizer(a, nil)
20 }
21 
22 // Len returns the number of elements.
23@@ -142,6 +139,37 @@ func (a *Array) Iter(txn *Transaction) (*ArrayIter, error) {
24 	return &ArrayIter{ptr: ptr}, nil
25 }
26 
27+// ForEach iterates over all elements in the array, calling fn for each element.
28+// The index is passed as the first argument to fn.
29+// If fn returns an error, iteration stops and the error is returned.
30+// The Output is automatically destroyed after fn returns.
31+func (a *Array) ForEach(txn *Transaction, fn func(index uint32, value *Output) error) error {
32+	iter, err := a.Iter(txn)
33+	if err != nil {
34+		return err
35+	}
36+	defer iter.Destroy()
37+
38+	var index uint32
39+	for {
40+		out, err := iter.Next()
41+		if err != nil {
42+			return err
43+		}
44+		if out == nil {
45+			break
46+		}
47+
48+		if err := fn(index, out); err != nil {
49+			out.Destroy()
50+			return err
51+		}
52+		out.Destroy()
53+		index++
54+	}
55+	return nil
56+}
57+
58 // ArrayIter iterates over array elements.
59 type ArrayIter struct {
60 	ptr *C.YArrayIter
A convenience_test.go
+380, -0
  1@@ -0,0 +1,380 @@
  2+package ygo_test
  3+
  4+import (
  5+	"testing"
  6+
  7+	"github.com/BTBurke/ygo"
  8+)
  9+
 10+func TestMapGetString(t *testing.T) {
 11+	doc, err := ygo.NewDoc()
 12+	if err != nil {
 13+		t.Fatalf("failed to create doc: %v", err)
 14+	}
 15+	defer doc.Destroy()
 16+
 17+	m, err := doc.GetMap("test")
 18+	if err != nil {
 19+		t.Fatalf("failed to get map: %v", err)
 20+	}
 21+	defer m.Destroy()
 22+
 23+	// Insert test data
 24+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 25+		m.Insert(txn, "name", ygo.String("Alice"))
 26+		return nil
 27+	})
 28+	if err != nil {
 29+		t.Fatalf("transaction failed: %v", err)
 30+	}
 31+
 32+	// Test GetString
 33+	var name string
 34+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 35+		var err error
 36+		name, err = m.GetString(txn, "name")
 37+		return err
 38+	})
 39+	if err != nil {
 40+		t.Fatalf("GetString failed: %v", err)
 41+	}
 42+	if name != "Alice" {
 43+		t.Errorf("expected Alice, got %s", name)
 44+	}
 45+}
 46+
 47+func TestMapGetInt(t *testing.T) {
 48+	doc, err := ygo.NewDoc()
 49+	if err != nil {
 50+		t.Fatalf("failed to create doc: %v", err)
 51+	}
 52+	defer doc.Destroy()
 53+
 54+	m, err := doc.GetMap("test")
 55+	if err != nil {
 56+		t.Fatalf("failed to get map: %v", err)
 57+	}
 58+	defer m.Destroy()
 59+
 60+	// Insert test data
 61+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 62+		m.Insert(txn, "age", ygo.Int(30))
 63+		return nil
 64+	})
 65+	if err != nil {
 66+		t.Fatalf("transaction failed: %v", err)
 67+	}
 68+
 69+	// Test GetInt
 70+	var age int64
 71+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 72+		var err error
 73+		age, err = m.GetInt(txn, "age")
 74+		return err
 75+	})
 76+	if err != nil {
 77+		t.Fatalf("GetInt failed: %v", err)
 78+	}
 79+	if age != 30 {
 80+		t.Errorf("expected 30, got %d", age)
 81+	}
 82+}
 83+
 84+func TestMapGetFloat(t *testing.T) {
 85+	doc, err := ygo.NewDoc()
 86+	if err != nil {
 87+		t.Fatalf("failed to create doc: %v", err)
 88+	}
 89+	defer doc.Destroy()
 90+
 91+	m, err := doc.GetMap("test")
 92+	if err != nil {
 93+		t.Fatalf("failed to get map: %v", err)
 94+	}
 95+	defer m.Destroy()
 96+
 97+	// Insert test data
 98+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 99+		m.Insert(txn, "pi", ygo.Float(3.14))
100+		return nil
101+	})
102+	if err != nil {
103+		t.Fatalf("transaction failed: %v", err)
104+	}
105+
106+	// Test GetFloat
107+	var pi float64
108+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
109+		var err error
110+		pi, err = m.GetFloat(txn, "pi")
111+		return err
112+	})
113+	if err != nil {
114+		t.Fatalf("GetFloat failed: %v", err)
115+	}
116+	if pi != 3.14 {
117+		t.Errorf("expected 3.14, got %f", pi)
118+	}
119+}
120+
121+func TestMapGetBool(t *testing.T) {
122+	doc, err := ygo.NewDoc()
123+	if err != nil {
124+		t.Fatalf("failed to create doc: %v", err)
125+	}
126+	defer doc.Destroy()
127+
128+	m, err := doc.GetMap("test")
129+	if err != nil {
130+		t.Fatalf("failed to get map: %v", err)
131+	}
132+	defer m.Destroy()
133+
134+	// Insert test data
135+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
136+		m.Insert(txn, "active", ygo.Bool(true))
137+		return nil
138+	})
139+	if err != nil {
140+		t.Fatalf("transaction failed: %v", err)
141+	}
142+
143+	// Test GetBool
144+	var active bool
145+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
146+		var err error
147+		active, err = m.GetBool(txn, "active")
148+		return err
149+	})
150+	if err != nil {
151+		t.Fatalf("GetBool failed: %v", err)
152+	}
153+	if !active {
154+		t.Errorf("expected true, got %v", active)
155+	}
156+}
157+
158+func TestMapForEach(t *testing.T) {
159+	doc, err := ygo.NewDoc()
160+	if err != nil {
161+		t.Fatalf("failed to create doc: %v", err)
162+	}
163+	defer doc.Destroy()
164+
165+	m, err := doc.GetMap("test")
166+	if err != nil {
167+		t.Fatalf("failed to get map: %v", err)
168+	}
169+	defer m.Destroy()
170+
171+	// Insert test data
172+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
173+		m.Insert(txn, "a", ygo.String("1"))
174+		m.Insert(txn, "b", ygo.String("2"))
175+		m.Insert(txn, "c", ygo.String("3"))
176+		return nil
177+	})
178+	if err != nil {
179+		t.Fatalf("transaction failed: %v", err)
180+	}
181+
182+	// Test ForEach
183+	var count int
184+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
185+		return m.ForEach(txn, func(key string, value *ygo.Output) error {
186+			count++
187+			return nil
188+		})
189+	})
190+	if err != nil {
191+		t.Fatalf("ForEach failed: %v", err)
192+	}
193+	if count != 3 {
194+		t.Errorf("expected 3 entries, got %d", count)
195+	}
196+}
197+
198+func TestMapForEachEarlyExit(t *testing.T) {
199+	doc, err := ygo.NewDoc()
200+	if err != nil {
201+		t.Fatalf("failed to create doc: %v", err)
202+	}
203+	defer doc.Destroy()
204+
205+	m, err := doc.GetMap("test")
206+	if err != nil {
207+		t.Fatalf("failed to get map: %v", err)
208+	}
209+	defer m.Destroy()
210+
211+	// Insert test data
212+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
213+		m.Insert(txn, "a", ygo.String("1"))
214+		m.Insert(txn, "b", ygo.String("2"))
215+		m.Insert(txn, "c", ygo.String("3"))
216+		return nil
217+	})
218+	if err != nil {
219+		t.Fatalf("transaction failed: %v", err)
220+	}
221+
222+	// Test ForEach with early exit
223+	var count int
224+	testErr := ygo.ErrKeyNotFound // Just use any error for testing
225+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
226+		return m.ForEach(txn, func(key string, value *ygo.Output) error {
227+			count++
228+			if count >= 2 {
229+				return testErr
230+			}
231+			return nil
232+		})
233+	})
234+	if err != testErr {
235+		t.Fatalf("expected ForEach to return test error, got: %v", err)
236+	}
237+	if count != 2 {
238+		t.Errorf("expected iteration to stop at 2, got %d", count)
239+	}
240+}
241+
242+func TestArrayForEach(t *testing.T) {
243+	doc, err := ygo.NewDoc()
244+	if err != nil {
245+		t.Fatalf("failed to create doc: %v", err)
246+	}
247+	defer doc.Destroy()
248+
249+	arr, err := doc.GetArray("test")
250+	if err != nil {
251+		t.Fatalf("failed to get array: %v", err)
252+	}
253+	defer arr.Destroy()
254+
255+	// Insert test data
256+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
257+		arr.Push(txn, ygo.Int(10))
258+		arr.Push(txn, ygo.Int(20))
259+		arr.Push(txn, ygo.Int(30))
260+		return nil
261+	})
262+	if err != nil {
263+		t.Fatalf("transaction failed: %v", err)
264+	}
265+
266+	// Test ForEach
267+	var sum int64
268+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
269+		return arr.ForEach(txn, func(index uint32, value *ygo.Output) error {
270+			if v, ok := value.Int(); ok {
271+				sum += v
272+			}
273+			return nil
274+		})
275+	})
276+	if err != nil {
277+		t.Fatalf("ForEach failed: %v", err)
278+	}
279+	if sum != 60 {
280+		t.Errorf("expected sum 60, got %d", sum)
281+	}
282+}
283+
284+func TestMapInput(t *testing.T) {
285+	// Test MapInput with data
286+	data := map[string]ygo.Input{
287+		"name": ygo.String("Alice"),
288+		"age":  ygo.Int(30),
289+	}
290+	input, err := ygo.MapInput(data)
291+	if err != nil {
292+		t.Fatalf("MapInput failed: %v", err)
293+	}
294+
295+	doc, err := ygo.NewDoc()
296+	if err != nil {
297+		t.Fatalf("failed to create doc: %v", err)
298+	}
299+	defer doc.Destroy()
300+
301+	m, err := doc.GetMap("test")
302+	if err != nil {
303+		t.Fatalf("failed to get map: %v", err)
304+	}
305+	defer m.Destroy()
306+
307+	// Insert the map input
308+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
309+		return m.Insert(txn, "data", input)
310+	})
311+	if err != nil {
312+		t.Fatalf("transaction failed: %v", err)
313+	}
314+
315+	// Verify we can read it back
316+	var name string
317+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
318+		dataMap, err := m.GetNestedMap(txn, "data")
319+		if err != nil {
320+			return err
321+		}
322+		defer dataMap.Destroy()
323+		name, err = dataMap.GetString(txn, "name")
324+		return err
325+	})
326+	if err != nil {
327+		t.Fatalf("GetNestedMap failed: %v", err)
328+	}
329+	if name != "Alice" {
330+		t.Errorf("expected Alice, got %s", name)
331+	}
332+}
333+
334+func TestJSONObject(t *testing.T) {
335+	// Test JSONObject with data
336+	data := map[string]ygo.Input{
337+		"name": ygo.String("Bob"),
338+		"age":  ygo.Int(25),
339+	}
340+	input, err := ygo.JSONObject(data)
341+	if err != nil {
342+		t.Fatalf("JSONObject failed: %v", err)
343+	}
344+
345+	doc, err := ygo.NewDoc()
346+	if err != nil {
347+		t.Fatalf("failed to create doc: %v", err)
348+	}
349+	defer doc.Destroy()
350+
351+	m, err := doc.GetMap("test")
352+	if err != nil {
353+		t.Fatalf("failed to get map: %v", err)
354+	}
355+	defer m.Destroy()
356+
357+	// Insert the JSON object input
358+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
359+		return m.Insert(txn, "data", input)
360+	})
361+	if err != nil {
362+		t.Fatalf("transaction failed: %v", err)
363+	}
364+
365+	// Verify we can read it back as a JSON map
366+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
367+		out, err := m.Get(txn, "data")
368+		if err != nil {
369+			return err
370+		}
371+		defer out.Destroy()
372+
373+		if out.Tag() != ygo.TagJSONMap {
374+			t.Errorf("expected TagJSONMap, got %v", out.Tag())
375+		}
376+		return nil
377+	})
378+	if err != nil {
379+		t.Fatalf("Get failed: %v", err)
380+	}
381+}
M document.go
+1, -20
 1@@ -9,7 +9,6 @@ import (
 2 	"encoding"
 3 	"encoding/json"
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 
 8 	"github.com/BTBurke/ygo/sync"
 9@@ -84,7 +83,6 @@ func NewDoc() (*Doc, error) {
10 	if d.ptr == nil {
11 		return nil, fmt.Errorf("failed to create document: C.ydoc_new() returned nil")
12 	}
13-	runtime.SetFinalizer(d, (*Doc).Destroy)
14 	return d, nil
15 }
16 
17@@ -105,22 +103,6 @@ func NewDocWithOptions(opts DocOptions) (*Doc, error) {
18 		return nil, fmt.Errorf("failed to create document with options: C.ydoc_new_with_options() returned nil")
19 	}
20 
21-	runtime.SetFinalizer(d, (*Doc).Destroy)
22-	return d, nil
23-}
24-
25-// NewDocWithSync creates a new document with a sync client attached.
26-// The returned document will be synchronized with the sync server.
27-func NewDocWithSync(client *sync.SyncClient, opts ...DocOption) (*Doc, error) {
28-	docOpts := DefaultOptions()
29-	for _, opt := range opts {
30-		opt(&docOpts)
31-	}
32-	d, err := NewDocWithOptions(docOpts)
33-	if err != nil {
34-		return nil, err
35-	}
36-	d.sync = client
37 	return d, nil
38 }
39 
40@@ -136,7 +118,6 @@ func (d *Doc) Clone() (*Doc, error) {
41 	if cloned.ptr == nil {
42 		return nil, fmt.Errorf("failed to clone document: C.ydoc_clone() returned nil")
43 	}
44-	runtime.SetFinalizer(cloned, (*Doc).Destroy)
45 	return cloned, nil
46 }
47 
48@@ -259,6 +240,7 @@ func (d *Doc) AutoLoad() bool {
49 // If a sync client is attached, it will send any pending updates,
50 // gracefully disconnect from the server, then destroy the document.
51 // Safe to call from within the OnUpdate callback.
52+// Callers must call Destroy() explicitly when done with the document.
53 func (d *Doc) Destroy() {
54 	// Close sync connection first (sends pending updates)
55 	if d.sync != nil {
56@@ -269,7 +251,6 @@ func (d *Doc) Destroy() {
57 	if d.ptr != nil {
58 		C.ydoc_destroy(d.ptr)
59 		d.ptr = nil
60-		runtime.SetFinalizer(d, nil)
61 	}
62 }
63 
M input.go
+41, -0
 1@@ -170,6 +170,47 @@ func YMap(keys []string, values []Input) (Input, error) {
 2 	return result, nil
 3 }
 4 
 5+// MapInput creates a YMap shared type input from a Go map.
 6+// Usage: ygo.MapInput(map[string]ygo.Input{"name": ygo.String("Alice"), "age": ygo.Int(30)})
 7+//
 8+// Note: The iteration order of Go maps is not guaranteed, so the resulting
 9+// YMap may have keys in any order.
10+func MapInput(items map[string]Input) (Input, error) {
11+	if len(items) == 0 {
12+		return YMap(nil, nil)
13+	}
14+
15+	// Extract keys and values
16+	keys := make([]string, 0, len(items))
17+	values := make([]Input, 0, len(items))
18+	for k, v := range items {
19+		keys = append(keys, k)
20+		values = append(values, v)
21+	}
22+
23+	return YMap(keys, values)
24+}
25+
26+// JSONObject creates a JSON map input from a Go map.
27+// Usage: ygo.JSONObject(map[string]ygo.Input{"name": ygo.String("Alice"), "age": ygo.Int(30)})
28+//
29+// Note: The iteration order of Go maps is not guaranteed, so the resulting
30+// JSON object may have keys in any order.
31+func JSONObject(items map[string]Input) (Input, error) {
32+	if len(items) == 0 {
33+		return JSONMap(nil, nil)
34+	}
35+
36+	keys := make([]string, 0, len(items))
37+	values := make([]Input, 0, len(items))
38+	for k, v := range items {
39+		keys = append(keys, k)
40+		values = append(values, v)
41+	}
42+
43+	return JSONMap(keys, values)
44+}
45+
46 // YText creates a YText shared type input with initial content.
47 func YText(initial string) Input {
48 	cStr := C.CString(initial)
M map.go
+100, -3
  1@@ -8,7 +8,6 @@ import "C"
  2 import (
  3 	"encoding/json"
  4 	"fmt"
  5-	"runtime"
  6 	"unsafe"
  7 )
  8 
  9@@ -31,13 +30,11 @@ func (d *Doc) GetMap(name string) (*Map, error) {
 10 	}
 11 
 12 	m := &Map{branch: branch}
 13-	runtime.SetFinalizer(m, (*Map).Destroy)
 14 	return m, nil
 15 }
 16 
 17 // Destroy releases resources.
 18 func (m *Map) Destroy() {
 19-	runtime.SetFinalizer(m, nil)
 20 }
 21 
 22 // Len returns the number of entries.
 23@@ -166,6 +163,78 @@ func (m *Map) GetNestedArray(txn *Transaction, key string) (*Array, error) {
 24 	return &Array{branch: branch}, nil
 25 }
 26 
 27+// GetString retrieves a string value by key.
 28+// Returns ErrKeyNotFound if key doesn't exist.
 29+// Returns error if value is not a string.
 30+func (m *Map) GetString(txn *Transaction, key string) (string, error) {
 31+	out, err := m.Get(txn, key)
 32+	if err != nil {
 33+		return "", err
 34+	}
 35+	defer out.Destroy()
 36+
 37+	str, ok := out.String()
 38+	if !ok {
 39+		return "", fmt.Errorf("value at key %q is not a string (got %v)", key, out.Tag())
 40+	}
 41+	return str, nil
 42+}
 43+
 44+// GetInt retrieves an integer value by key.
 45+// Returns ErrKeyNotFound if key doesn't exist.
 46+// Returns error if value is not an integer or float.
 47+func (m *Map) GetInt(txn *Transaction, key string) (int64, error) {
 48+	out, err := m.Get(txn, key)
 49+	if err != nil {
 50+		return 0, err
 51+	}
 52+	defer out.Destroy()
 53+
 54+	if val, ok := out.Int(); ok {
 55+		return val, nil
 56+	}
 57+	if val, ok := out.Float(); ok {
 58+		return int64(val), nil
 59+	}
 60+	return 0, fmt.Errorf("value at key %q is not a number (got %v)", key, out.Tag())
 61+}
 62+
 63+// GetFloat retrieves a float value by key.
 64+// Returns ErrKeyNotFound if key doesn't exist.
 65+// Returns error if value is not a number.
 66+func (m *Map) GetFloat(txn *Transaction, key string) (float64, error) {
 67+	out, err := m.Get(txn, key)
 68+	if err != nil {
 69+		return 0, err
 70+	}
 71+	defer out.Destroy()
 72+
 73+	if val, ok := out.Float(); ok {
 74+		return val, nil
 75+	}
 76+	if val, ok := out.Int(); ok {
 77+		return float64(val), nil
 78+	}
 79+	return 0, fmt.Errorf("value at key %q is not a number (got %v)", key, out.Tag())
 80+}
 81+
 82+// GetBool retrieves a boolean value by key.
 83+// Returns ErrKeyNotFound if key doesn't exist.
 84+// Returns error if value is not a boolean.
 85+func (m *Map) GetBool(txn *Transaction, key string) (bool, error) {
 86+	out, err := m.Get(txn, key)
 87+	if err != nil {
 88+		return false, err
 89+	}
 90+	defer out.Destroy()
 91+
 92+	val, ok := out.Bool()
 93+	if !ok {
 94+		return false, fmt.Errorf("value at key %q is not a bool (got %v)", key, out.Tag())
 95+	}
 96+	return val, nil
 97+}
 98+
 99 // MapEntry represents a key-value pair from a map.
100 type MapEntry struct {
101 	Key   string
102@@ -221,6 +290,34 @@ func (m *Map) Iter(txn *Transaction) (*MapIter, error) {
103 	return &MapIter{ptr: ptr}, nil
104 }
105 
106+// ForEach iterates over all entries in the map, calling fn for each key-value pair.
107+// If fn returns an error, iteration stops and the error is returned.
108+// The Output is automatically destroyed after fn returns.
109+func (m *Map) ForEach(txn *Transaction, fn func(key string, value *Output) error) error {
110+	iter, err := m.Iter(txn)
111+	if err != nil {
112+		return err
113+	}
114+	defer iter.Destroy()
115+
116+	for {
117+		entry, err := iter.Next()
118+		if err != nil {
119+			return err
120+		}
121+		if entry == nil {
122+			break
123+		}
124+
125+		if err := fn(entry.Key, entry.Value); err != nil {
126+			entry.Value.Destroy()
127+			return err
128+		}
129+		entry.Value.Destroy()
130+	}
131+	return nil
132+}
133+
134 // MarshalJSONWithTxn implements JSON marshaling for Map.
135 // Returns a JSON object with string keys and JSON values.
136 func (m *Map) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
M sticky.go
+0, -5
 1@@ -6,7 +6,6 @@ package ygo
 2 import "C"
 3 import (
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -50,7 +49,6 @@ func NewStickyIndexFromIndex(t interface{ Branch() unsafe.Pointer }, txn *Transa
10 	}
11 
12 	si := &StickyIndex{ptr: ptr}
13-	runtime.SetFinalizer(si, (*StickyIndex).Destroy)
14 	return si, nil
15 }
16 
17@@ -59,7 +57,6 @@ func (si *StickyIndex) Destroy() {
18 	if si.ptr != nil {
19 		C.ysticky_index_destroy(si.ptr)
20 		si.ptr = nil
21-		runtime.SetFinalizer(si, nil)
22 	}
23 }
24 
25@@ -101,7 +98,6 @@ func DecodeStickyIndex(data []byte) (*StickyIndex, error) {
26 	}
27 
28 	si := &StickyIndex{ptr: ptr}
29-	runtime.SetFinalizer(si, (*StickyIndex).Destroy)
30 	return si, nil
31 }
32 
33@@ -124,7 +120,6 @@ func ParseStickyIndexJSON(jsonStr string) (*StickyIndex, error) {
34 	}
35 
36 	si := &StickyIndex{ptr: ptr}
37-	runtime.SetFinalizer(si, (*StickyIndex).Destroy)
38 	return si, nil
39 }
40 
M text.go
+0, -3
 1@@ -8,7 +8,6 @@ import "C"
 2 import (
 3 	"encoding/json"
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -31,14 +30,12 @@ func (d *Doc) GetText(name string) (*Text, error) {
10 	}
11 
12 	t := &Text{branch: branch}
13-	runtime.SetFinalizer(t, (*Text).Destroy)
14 	return t, nil
15 }
16 
17 // Destroy releases resources (does not delete the text from document).
18 func (t *Text) Destroy() {
19 	// Text is managed by document, no explicit destroy needed
20-	runtime.SetFinalizer(t, nil)
21 }
22 
23 // Len returns the length of the text in UTF code units (based on doc encoding).
M transaction.go
+0, -3
 1@@ -7,7 +7,6 @@ package ygo
 2 import "C"
 3 import (
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -125,7 +124,6 @@ func (t *Transaction) Commit() {
10 	if t.ptr != nil {
11 		C.ytransaction_commit(t.ptr)
12 		t.ptr = nil
13-		runtime.SetFinalizer(t, nil)
14 	}
15 }
16 
17@@ -135,7 +133,6 @@ func (t *Transaction) Rollback() {
18 	if t.ptr != nil {
19 		C.ytransaction_commit(t.ptr) // yffi uses commit to end, even for rollback
20 		t.ptr = nil
21-		runtime.SetFinalizer(t, nil)
22 	}
23 }
24 
M undo.go
+0, -3
 1@@ -7,7 +7,6 @@ package ygo
 2 import "C"
 3 import (
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -43,7 +42,6 @@ func NewUndoManager(doc *Doc, opts *UndoManagerOptions) (*UndoManager, error) {
10 	}
11 
12 	m := &UndoManager{ptr: ptr}
13-	runtime.SetFinalizer(m, (*UndoManager).Destroy)
14 	return m, nil
15 }
16 
17@@ -52,7 +50,6 @@ func (m *UndoManager) Destroy() {
18 	if m.ptr != nil {
19 		C.yundo_manager_destroy(m.ptr)
20 		m.ptr = nil
21-		runtime.SetFinalizer(m, nil)
22 	}
23 }
24 
M weak.go
+0, -3
 1@@ -7,7 +7,6 @@ package ygo
 2 import "C"
 3 import (
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -57,7 +56,6 @@ func (w *Weak) Destroy() {
10 	if w.ptr != nil {
11 		C.yweak_destroy(w.ptr)
12 		w.ptr = nil
13-		runtime.SetFinalizer(w, nil)
14 	}
15 }
16 
17@@ -213,7 +211,6 @@ func (m *Map) LinkMap(txn *Transaction, key string) (*Weak, error) {
18 	}
19 
20 	w := &Weak{ptr: ptr}
21-	runtime.SetFinalizer(w, (*Weak).Destroy)
22 	return w, nil
23 }
24 
M xml.go
+5, -9
 1@@ -8,7 +8,6 @@ import "C"
 2 import (
 3 	"encoding/json"
 4 	"fmt"
 5-	"runtime"
 6 	"unsafe"
 7 )
 8 
 9@@ -31,13 +30,11 @@ func (d *Doc) GetXmlFragment(name string) (*XmlFragment, error) {
10 	}
11 
12 	f := &XmlFragment{branch: branch}
13-	runtime.SetFinalizer(f, (*XmlFragment).Destroy)
14 	return f, nil
15 }
16 
17 // Destroy releases resources.
18 func (f *XmlFragment) Destroy() {
19-	runtime.SetFinalizer(f, nil)
20 }
21 
22 // InsertElement creates and inserts an XML element at index.
23@@ -60,7 +57,7 @@ func (f *XmlFragment) InsertElement(txn *Transaction, index uint32, tag string)
24 	}
25 
26 	e := &XmlElement{branch: branch}
27-	runtime.SetFinalizer(e, (*XmlElement).Destroy)
28+	// REMOVED: runtime.SetFinalizer
29 	return e, nil
30 }
31 
32@@ -82,7 +79,7 @@ func (f *XmlFragment) InsertText(txn *Transaction, index uint32) (*XmlText, erro
33 	}
34 
35 	t := &XmlText{branch: branch}
36-	runtime.SetFinalizer(t, (*XmlText).Destroy)
37+	// REMOVED: runtime.SetFinalizer
38 	return t, nil
39 }
40 
41@@ -93,7 +90,7 @@ type XmlElement struct {
42 
43 // Destroy releases resources.
44 func (e *XmlElement) Destroy() {
45-	runtime.SetFinalizer(e, nil)
46+	// REMOVED: runtime.SetFinalizer
47 }
48 
49 // Tag returns the element's tag name.
50@@ -182,7 +179,6 @@ func (e *XmlElement) InsertElement(txn *Transaction, index uint32, tag string) (
51 	}
52 
53 	elem := &XmlElement{branch: branch}
54-	runtime.SetFinalizer(elem, (*XmlElement).Destroy)
55 	return elem, nil
56 }
57 
58@@ -204,7 +200,7 @@ func (e *XmlElement) InsertText(txn *Transaction, index uint32) (*XmlText, error
59 	}
60 
61 	t := &XmlText{branch: branch}
62-	runtime.SetFinalizer(t, (*XmlText).Destroy)
63+	// REMOVED: runtime.SetFinalizer
64 	return t, nil
65 }
66 
67@@ -230,7 +226,7 @@ type XmlText struct {
68 
69 // Destroy releases resources.
70 func (t *XmlText) Destroy() {
71-	runtime.SetFinalizer(t, nil)
72+	// REMOVED: runtime.SetFinalizer
73 }
74 
75 // Len returns the text length.