3 files changed,
+387,
-0
+51,
-0
1@@ -181,6 +181,57 @@ input, _ := ygo.MapInput(data)
2 m.Insert(txn, "user", input)
3 ```
4
5+### Explicit Transactions
6+
7+Use explicit Begin/Commit for flatter code with less nesting:
8+
9+```go
10+// Old: Callback-based API (nested)
11+err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
12+ txt.Insert(txn, 0, "Hello")
13+ arr.Push(txn, ygo.Int(42))
14+ return nil
15+})
16+
17+// New: Explicit transaction API (flatter)
18+txn, err := doc.BeginWrite()
19+if err != nil {
20+ return err
21+}
22+defer func() {
23+ if r := recover(); r != nil {
24+ txn.Rollback()
25+ panic(r)
26+ }
27+ if err != nil {
28+ txn.Rollback()
29+ } else {
30+ txn.Commit()
31+ }
32+}()
33+
34+// Perform operations (no nesting!)
35+txt.Insert(txn, 0, "Hello")
36+arr.Push(txn, ygo.Int(42))
37+
38+// Read transactions work similarly
39+txn, err = doc.BeginRead()
40+if err != nil {
41+ return err
42+}
43+defer txn.Commit()
44+
45+length := arr.Len()
46+content, _ := txt.String(txn)
47+```
48+
49+The explicit API gives you full control over transaction lifecycle:
50+- **BeginRead()** → **Commit()** for read-only transactions
51+- **BeginWrite()** → **Commit()** or **Rollback()** for write transactions
52+- **BeginWriteWithOrigin(origin)** to mark changes with a source identifier
53+
54+**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.
55+
56 ## Serialization
57
58 Documents support standard Go binary marshaling:
+261,
-0
1@@ -0,0 +1,261 @@
2+package ygo_test
3+
4+import (
5+ "testing"
6+
7+ "github.com/BTBurke/ygo"
8+)
9+
10+func TestBeginRead(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+ // Create a text field
18+ txt, err := doc.GetText("content")
19+ if err != nil {
20+ t.Fatalf("failed to get text: %v", err)
21+ }
22+ defer txt.Destroy()
23+
24+ // Insert some content first
25+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
26+ txt.Insert(txn, 0, "Hello, World!")
27+ return nil
28+ })
29+ if err != nil {
30+ t.Fatalf("failed to insert text: %v", err)
31+ }
32+
33+ // Now test explicit read transaction
34+ txn, err := doc.BeginRead()
35+ if err != nil {
36+ t.Fatalf("failed to begin read transaction: %v", err)
37+ }
38+
39+ // Read content
40+ content, err := txt.String(txn)
41+ if err != nil {
42+ txn.Commit()
43+ t.Fatalf("failed to get string: %v", err)
44+ }
45+
46+ // Commit the transaction
47+ txn.Commit()
48+
49+ if content != "Hello, World!" {
50+ t.Errorf("expected 'Hello, World!', got '%s'", content)
51+ }
52+}
53+
54+func TestBeginWrite(t *testing.T) {
55+ doc, err := ygo.NewDoc()
56+ if err != nil {
57+ t.Fatalf("failed to create doc: %v", err)
58+ }
59+ defer doc.Destroy()
60+
61+ // Create a text field
62+ txt, err := doc.GetText("content")
63+ if err != nil {
64+ t.Fatalf("failed to get text: %v", err)
65+ }
66+ defer txt.Destroy()
67+
68+ // Test explicit write transaction
69+ txn, err := doc.BeginWrite()
70+ if err != nil {
71+ t.Fatalf("failed to begin write transaction: %v", err)
72+ }
73+
74+ // Write content
75+ err = txt.Insert(txn, 0, "Hello from explicit transaction!")
76+ if err != nil {
77+ txn.Rollback()
78+ t.Fatalf("failed to insert text: %v", err)
79+ }
80+
81+ // Commit the transaction
82+ txn.Commit()
83+
84+ // Verify the write was applied
85+ var content string
86+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
87+ var err error
88+ content, err = txt.String(txn)
89+ return err
90+ })
91+ if err != nil {
92+ t.Fatalf("failed to read text: %v", err)
93+ }
94+
95+ if content != "Hello from explicit transaction!" {
96+ t.Errorf("expected 'Hello from explicit transaction!', got '%s'", content)
97+ }
98+}
99+
100+func TestBeginWriteRollback(t *testing.T) {
101+ doc, err := ygo.NewDoc()
102+ if err != nil {
103+ t.Fatalf("failed to create doc: %v", err)
104+ }
105+ defer doc.Destroy()
106+
107+ // Create a text field
108+ txt, err := doc.GetText("content")
109+ if err != nil {
110+ t.Fatalf("failed to get text: %v", err)
111+ }
112+ defer txt.Destroy()
113+
114+ // Insert some content first
115+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
116+ txt.Insert(txn, 0, "Original content")
117+ return nil
118+ })
119+ if err != nil {
120+ t.Fatalf("failed to insert text: %v", err)
121+ }
122+
123+ // Test rollback
124+ txn, err := doc.BeginWrite()
125+ if err != nil {
126+ t.Fatalf("failed to begin write transaction: %v", err)
127+ }
128+
129+ // Write content
130+ err = txt.Insert(txn, 16, " - this is a test")
131+ if err != nil {
132+ txn.Rollback()
133+ t.Fatalf("failed to insert text: %v", err)
134+ }
135+
136+ // Rollback instead of commit
137+ txn.Rollback()
138+
139+ // In y-crdt, changes are applied immediately within the transaction scope.
140+ // Rollback() only closes the transaction without syncing to remote peers.
141+ // The local document state is already modified. This is a limitation of the
142+ // underlying y-crdt library.
143+ //
144+ // For true atomic operations that can be rolled back, use the callback-based
145+ // WithWriteTransaction API which only commits on success.
146+
147+ // Verify that content was modified (y-crdt behavior)
148+ var content string
149+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
150+ var err error
151+ content, err = txt.String(txn)
152+ return err
153+ })
154+ if err != nil {
155+ t.Fatalf("failed to read text: %v", err)
156+ }
157+
158+ // Note: This demonstrates that BeginWrite + Rollback doesn't undo changes
159+ // Use WithWriteTransaction for atomic operations with automatic rollback
160+ if content == "Original content" {
161+ t.Log("Note: Rollback did undo changes (this may vary by y-crdt version)")
162+ } else {
163+ t.Logf("Note: Content after rollback: '%s' (changes are not undone in y-crdt)", content)
164+ }
165+}
166+
167+func TestBeginWriteWithOrigin(t *testing.T) {
168+ doc, err := ygo.NewDoc()
169+ if err != nil {
170+ t.Fatalf("failed to create doc: %v", err)
171+ }
172+ defer doc.Destroy()
173+
174+ // Create a text field
175+ txt, err := doc.GetText("content")
176+ if err != nil {
177+ t.Fatalf("failed to get text: %v", err)
178+ }
179+ defer txt.Destroy()
180+
181+ // Test explicit write transaction with origin
182+ origin := []byte("test-origin")
183+ txn, err := doc.BeginWriteWithOrigin(origin)
184+ if err != nil {
185+ t.Fatalf("failed to begin write transaction with origin: %v", err)
186+ }
187+
188+ // Write content
189+ err = txt.Insert(txn, 0, "Content with origin")
190+ if err != nil {
191+ txn.Rollback()
192+ t.Fatalf("failed to insert text: %v", err)
193+ }
194+
195+ // Commit the transaction
196+ txn.Commit()
197+
198+ // Verify the write was applied
199+ var content string
200+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
201+ var err error
202+ content, err = txt.String(txn)
203+ return err
204+ })
205+ if err != nil {
206+ t.Fatalf("failed to read text: %v", err)
207+ }
208+
209+ if content != "Content with origin" {
210+ t.Errorf("expected 'Content with origin', got '%s'", content)
211+ }
212+}
213+
214+func TestBeginReadWhileWriteActive(t *testing.T) {
215+ doc, err := ygo.NewDoc()
216+ if err != nil {
217+ t.Fatalf("failed to create doc: %v", err)
218+ }
219+ defer doc.Destroy()
220+
221+ // Start a write transaction
222+ writeTxn, err := doc.BeginWrite()
223+ if err != nil {
224+ t.Fatalf("failed to begin write transaction: %v", err)
225+ }
226+
227+ // Attempt to start a read transaction while write is active
228+ // This should fail because only one transaction can be active at a time
229+ _, err = doc.BeginRead()
230+ if err == nil {
231+ writeTxn.Rollback()
232+ t.Error("expected error when starting read transaction while write transaction is active")
233+ }
234+
235+ // Clean up the write transaction
236+ writeTxn.Rollback()
237+}
238+
239+func TestBeginWriteWhileReadActive(t *testing.T) {
240+ doc, err := ygo.NewDoc()
241+ if err != nil {
242+ t.Fatalf("failed to create doc: %v", err)
243+ }
244+ defer doc.Destroy()
245+
246+ // Start a read transaction
247+ readTxn, err := doc.BeginRead()
248+ if err != nil {
249+ t.Fatalf("failed to begin read transaction: %v", err)
250+ }
251+
252+ // Attempt to start a write transaction while read is active
253+ // This should fail because only one transaction can be active at a time
254+ _, err = doc.BeginWrite()
255+ if err == nil {
256+ readTxn.Commit()
257+ t.Error("expected error when starting write transaction while read transaction is active")
258+ }
259+
260+ // Clean up the read transaction
261+ readTxn.Commit()
262+}
+75,
-0
1@@ -40,6 +40,33 @@ func (d *Doc) WithReadTransaction(fn func(*Transaction) error) error {
2 return err
3 }
4
5+// BeginRead starts a read-only transaction.
6+// The caller must call Commit() when done to release resources.
7+// Returns an error if another transaction is already active.
8+//
9+// Example:
10+//
11+// txn, err := doc.BeginRead()
12+// if err != nil {
13+// return err
14+// }
15+// defer txn.Commit()
16+//
17+// length := arr.Len()
18+// // ... read operations ...
19+func (d *Doc) BeginRead() (*Transaction, error) {
20+ if d.ptr == nil {
21+ return nil, ErrNilDocument
22+ }
23+
24+ txn := C.ydoc_read_transaction(d.ptr)
25+ if txn == nil {
26+ return nil, fmt.Errorf("failed to create read transaction: another transaction may be active")
27+ }
28+
29+ return &Transaction{ptr: txn, doc: d}, nil
30+}
31+
32 // WithWriteTransaction executes a callback within a read-write transaction.
33 // If the callback returns nil, the transaction is committed.
34 // If the callback returns an error, the transaction is rolled back.
35@@ -110,6 +137,54 @@ func (d *Doc) WithWriteTransactionWithOrigin(origin []byte, fn func(*Transaction
36 return nil
37 }
38
39+// BeginWrite starts a read-write transaction.
40+// The caller must call Commit() to apply changes or Rollback() to abort.
41+// Returns an error if another transaction is already active.
42+//
43+// Example:
44+//
45+// txn, err := doc.BeginWrite()
46+// if err != nil {
47+// return err
48+// }
49+// defer func() {
50+// if err != nil {
51+// txn.Rollback()
52+// } else {
53+// txn.Commit()
54+// }
55+// }()
56+//
57+// txt.Insert(txn, 0, "Hello")
58+// // ... more write operations ...
59+func (d *Doc) BeginWrite() (*Transaction, error) {
60+ return d.BeginWriteWithOrigin(nil)
61+}
62+
63+// BeginWriteWithOrigin starts a read-write transaction with an origin marker.
64+// The origin can be used by event handlers and undo managers to identify change sources.
65+// The caller must call Commit() to apply changes or Rollback() to abort.
66+// Returns an error if another transaction is already active.
67+func (d *Doc) BeginWriteWithOrigin(origin []byte) (*Transaction, error) {
68+ if d.ptr == nil {
69+ return nil, ErrNilDocument
70+ }
71+
72+ var originPtr *C.char
73+ var originLen C.uint32_t
74+ if len(origin) > 0 {
75+ originPtr = (*C.char)(unsafe.Pointer(&origin[0]))
76+ originLen = C.uint32_t(len(origin))
77+ }
78+
79+ txn := C.ydoc_write_transaction(d.ptr, originLen, originPtr)
80+ if txn == nil {
81+ return nil, fmt.Errorf("failed to create write transaction: another transaction may be active")
82+ }
83+
84+ return &Transaction{ptr: txn, doc: d}, nil
85+}
86+
87 // IsWriteable returns true if this is a read-write transaction.
88 func (t *Transaction) IsWriteable() bool {
89 if t.ptr == nil {