document.go

  1package ygo
  2
  3/*
  4#include "libyrs.h"
  5#include <stdlib.h>
  6*/
  7import "C"
  8import (
  9	"encoding"
 10	"encoding/json"
 11	"fmt"
 12	"unsafe"
 13
 14	"git.kilimanjaro.io/ygo/sync"
 15)
 16
 17// Compile-time interface compliance check
 18var (
 19	_ encoding.BinaryMarshaler   = (*Doc)(nil)
 20	_ encoding.BinaryUnmarshaler = (*Doc)(nil)
 21)
 22
 23// MarshalOption configures Document JSON marshaling.
 24type MarshalOption func(*marshalConfig)
 25
 26type marshalConfig struct {
 27	roots []rootSpec
 28}
 29
 30type rootSpec struct {
 31	name string
 32	typ  RootType
 33}
 34
 35// RootType identifies the type of shared root to marshal.
 36type RootType int
 37
 38const (
 39	RootTypeMap RootType = iota
 40	RootTypeArray
 41	RootTypeXmlFragment
 42	RootTypeText
 43)
 44
 45// WithMap includes the named Map in the marshaled output.
 46func WithMap(name string) MarshalOption {
 47	return func(cfg *marshalConfig) {
 48		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeMap})
 49	}
 50}
 51
 52// WithArray includes the named Array in the marshaled output.
 53func WithArray(name string) MarshalOption {
 54	return func(cfg *marshalConfig) {
 55		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeArray})
 56	}
 57}
 58
 59// WithXmlFragment includes the named XmlFragment in the marshaled output.
 60func WithXmlFragment(name string) MarshalOption {
 61	return func(cfg *marshalConfig) {
 62		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeXmlFragment})
 63	}
 64}
 65
 66// WithText includes the named Text in the marshaled output.
 67func WithText(name string) MarshalOption {
 68	return func(cfg *marshalConfig) {
 69		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeText})
 70	}
 71}
 72
 73// Doc represents a Yjs document - the core unit of collaborative resources.
 74// All shared collections live within a document scope.
 75type Doc struct {
 76	ptr  *C.YDoc
 77	sync *sync.SyncClient
 78}
 79
 80// NewDoc creates a new document with a randomized client ID.
 81func NewDoc() (*Doc, error) {
 82	d := &Doc{ptr: C.ydoc_new()}
 83	if d.ptr == nil {
 84		return nil, fmt.Errorf("failed to create document: C.ydoc_new() returned nil")
 85	}
 86	return d, nil
 87}
 88
 89// NewDocWithOptions creates a new document with specific options.
 90func NewDocWithOptions(opts DocOptions) (*Doc, error) {
 91	cOpts := opts.toC()
 92	d := &Doc{ptr: C.ydoc_new_with_options(cOpts)}
 93
 94	// Free allocated C strings
 95	if cOpts.guid != nil {
 96		C.free(unsafe.Pointer(cOpts.guid))
 97	}
 98	if cOpts.collection_id != nil {
 99		C.free(unsafe.Pointer(cOpts.collection_id))
100	}
101
102	if d.ptr == nil {
103		return nil, fmt.Errorf("failed to create document with options: C.ydoc_new_with_options() returned nil")
104	}
105
106	return d, nil
107}
108
109// Clone creates a shallow, reference-counted clone of the document.
110// Both the original and clone share the same underlying data - changes to one
111// are immediately visible to the other. Use DeepClone() if you need an
112// independent copy with no shared state.
113func (d *Doc) Clone() (*Doc, error) {
114	if d.ptr == nil {
115		return nil, ErrNilDocument
116	}
117	cloned := &Doc{ptr: C.ydoc_clone(d.ptr)}
118	if cloned.ptr == nil {
119		return nil, fmt.Errorf("failed to clone document: C.ydoc_clone() returned nil")
120	}
121	return cloned, nil
122}
123
124// DeepClone creates an independent copy of the document by serializing its
125// current state and deserializing into a new document. The returned document
126// has no shared state with the original - changes to one do not affect the
127// other. This is useful for creating checkpoints, backups, or isolated copies
128// that can be modified independently.
129func (d *Doc) DeepClone() (*Doc, error) {
130	data, err := d.MarshalBinary()
131	if err != nil {
132		return nil, fmt.Errorf("failed to marshal document: %w", err)
133	}
134
135	clone, err := NewDoc()
136	if err != nil {
137		return nil, fmt.Errorf("failed to create new document: %w", err)
138	}
139
140	if err := clone.UnmarshalBinary(data); err != nil {
141		clone.Destroy()
142		return nil, fmt.Errorf("failed to unmarshal document: %w", err)
143	}
144
145	return clone, nil
146}
147
148// MarshalBinary implements encoding.BinaryMarshaler.
149// Returns the document state as V1-encoded binary data.
150// This enables idiomatic usage with Go's encoding packages.
151func (d *Doc) MarshalBinary() ([]byte, error) {
152	if d == nil || d.ptr == nil {
153		return nil, ErrNilDocument
154	}
155
156	var data []byte
157	err := d.WithReadTransaction(func(txn *Transaction) error {
158		update := txn.GetStateDiff(nil)
159		if update == nil {
160			return fmt.Errorf("failed to get document state")
161		}
162		data = update.Data()
163		return nil
164	})
165
166	if err != nil {
167		return nil, fmt.Errorf("failed to marshal document: %w", err)
168	}
169
170	return data, nil
171}
172
173// UnmarshalBinary implements encoding.BinaryUnmarshaler.
174// Applies V1-encoded binary data to the document.
175// This enables idiomatic usage with Go's encoding packages.
176func (d *Doc) UnmarshalBinary(data []byte) error {
177	if d == nil || d.ptr == nil {
178		return ErrNilDocument
179	}
180
181	if len(data) == 0 {
182		return nil // Nothing to apply
183	}
184
185	update := UpdateFromBytes(data)
186	if update == nil {
187		return fmt.Errorf("failed to create update from data")
188	}
189
190	return d.WithWriteTransaction(func(txn *Transaction) error {
191		return txn.ApplyUpdate(update)
192	})
193}
194
195// ClientID returns the unique client identifier.
196func (d *Doc) ClientID() uint64 {
197	if d.ptr == nil {
198		return 0
199	}
200	return uint64(C.ydoc_id(d.ptr))
201}
202
203// GUID returns the document's globally unique identifier.
204func (d *Doc) GUID() string {
205	if d.ptr == nil {
206		return ""
207	}
208	return cStringToGoAndFree(C.ydoc_guid(d.ptr))
209}
210
211// CollectionID returns the collection identifier or empty string if none.
212func (d *Doc) CollectionID() string {
213	if d.ptr == nil {
214		return ""
215	}
216	cStr := C.ydoc_collection_id(d.ptr)
217	if cStr == nil {
218		return ""
219	}
220	return cStringToGoAndFree(cStr)
221}
222
223// ShouldLoad returns whether the document requests a data load.
224func (d *Doc) ShouldLoad() bool {
225	if d.ptr == nil {
226		return false
227	}
228	return C.ydoc_should_load(d.ptr) != 0
229}
230
231// AutoLoad returns whether subdocuments are auto-loaded.
232func (d *Doc) AutoLoad() bool {
233	if d.ptr == nil {
234		return false
235	}
236	return C.ydoc_auto_load(d.ptr) != 0
237}
238
239// Destroy releases all memory allocated by the document.
240// If a sync client is attached, it will send any pending updates,
241// gracefully disconnect from the server, then destroy the document.
242// Safe to call from within the OnUpdate callback.
243// Callers must call Destroy() explicitly when done with the document.
244func (d *Doc) Destroy() {
245	// Close sync connection first (sends pending updates)
246	if d.sync != nil {
247		d.sync.SendPendingAndClose()
248		d.sync = nil
249	}
250
251	if d.ptr != nil {
252		C.ydoc_destroy(d.ptr)
253		d.ptr = nil
254	}
255}
256
257// MarshalJSON implements json.Marshaler for Document.
258// Returns the specified top-level shared types as JSON.
259// If no options are provided, returns an empty JSON object "{}".
260//
261// Example:
262//
263//	doc.MarshalJSON(ygo.WithMap("users"), ygo.WithArray("items"))
264//	// Returns: {"users": {...}, "items": [...]}
265func (d *Doc) MarshalJSON(opts ...MarshalOption) ([]byte, error) {
266	if d.ptr == nil {
267		return nil, ErrNilDocument
268	}
269
270	cfg := &marshalConfig{}
271	for _, opt := range opts {
272		opt(cfg)
273	}
274
275	if len(cfg.roots) == 0 {
276		return []byte("{}"), nil
277	}
278
279	roots := make(map[string]struct {
280		typ RootType
281		m   *Map
282		a   *Array
283		f   *XmlFragment
284		t   *Text
285	}, len(cfg.roots))
286
287	for _, spec := range cfg.roots {
288		switch spec.typ {
289		case RootTypeMap:
290			m, err := d.GetMap(spec.name)
291			if err != nil {
292				return nil, fmt.Errorf("map %q: %w", spec.name, err)
293			}
294			roots[spec.name] = struct {
295				typ RootType
296				m   *Map
297				a   *Array
298				f   *XmlFragment
299				t   *Text
300			}{typ: spec.typ, m: m}
301
302		case RootTypeArray:
303			a, err := d.GetArray(spec.name)
304			if err != nil {
305				return nil, fmt.Errorf("array %q: %w", spec.name, err)
306			}
307			roots[spec.name] = struct {
308				typ RootType
309				m   *Map
310				a   *Array
311				f   *XmlFragment
312				t   *Text
313			}{typ: spec.typ, a: a}
314
315		case RootTypeXmlFragment:
316			f, err := d.GetXmlFragment(spec.name)
317			if err != nil {
318				return nil, fmt.Errorf("xml fragment %q: %w", spec.name, err)
319			}
320			roots[spec.name] = struct {
321				typ RootType
322				m   *Map
323				a   *Array
324				f   *XmlFragment
325				t   *Text
326			}{typ: spec.typ, f: f}
327
328		case RootTypeText:
329			t, err := d.GetText(spec.name)
330			if err != nil {
331				return nil, fmt.Errorf("text %q: %w", spec.name, err)
332			}
333			roots[spec.name] = struct {
334				typ RootType
335				m   *Map
336				a   *Array
337				f   *XmlFragment
338				t   *Text
339			}{typ: spec.typ, t: t}
340		}
341	}
342
343	defer func() {
344		for _, r := range roots {
345			if r.m != nil {
346				r.m.Destroy()
347			}
348			if r.a != nil {
349				r.a.Destroy()
350			}
351			if r.f != nil {
352				r.f.Destroy()
353			}
354			if r.t != nil {
355				r.t.Destroy()
356			}
357		}
358	}()
359
360	result := make(map[string]interface{})
361	err := d.WithReadTransaction(func(txn *Transaction) error {
362		for name, r := range roots {
363			var jsonBytes []byte
364			var err error
365
366			switch r.typ {
367			case RootTypeMap:
368				jsonBytes, err = r.m.MarshalJSONWithTxn(txn)
369			case RootTypeArray:
370				jsonBytes, err = r.a.MarshalJSONWithTxn(txn)
371			case RootTypeXmlFragment:
372				jsonBytes, err = r.f.MarshalJSONWithTxn(txn)
373			case RootTypeText:
374				jsonBytes, err = r.t.MarshalJSONWithTxn(txn)
375			}
376
377			if err != nil {
378				return fmt.Errorf("%s: %w", name, err)
379			}
380
381			var val interface{}
382			if err := json.Unmarshal(jsonBytes, &val); err != nil {
383				return fmt.Errorf("%s: %w", name, err)
384			}
385			result[name] = val
386		}
387		return nil
388	})
389
390	if err != nil {
391		return nil, err
392	}
393
394	return json.Marshal(result)
395}