-
Notifications
You must be signed in to change notification settings - Fork 7
/
doc.go
316 lines (266 loc) · 8.16 KB
/
doc.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package automerge
// #include "automerge.h"
import "C"
import (
"fmt"
"runtime"
"sync"
"time"
)
type actorID struct {
item *item
cActorID *C.AMactorId
}
func (ai *actorID) String() string {
defer runtime.KeepAlive(ai)
return fromByteSpanStr(C.AMactorIdStr(ai.cActorID))
}
// NewActorID generates a new unique actor id.
func NewActorID() string {
return must(wrap(C.AMactorIdInit()).item()).actorID().String()
}
// Doc represents an automerge document. You can read and write the
// values of the document with [Doc.Root], [Doc.RootMap] or [Doc.Path],
// and other methods are provided to enable collaboration and accessing
// historical data.
// After writing to the document you should immediately call [Doc.Commit] to
// explicitly create a [Change], though if you forget to do this most methods
// on a document will create an anonymous change on your behalf.
type Doc struct {
item *item
cDoc *C.AMdoc
m sync.Mutex
}
func (d *Doc) lock() (*C.AMdoc, func()) {
d.m.Lock()
locked := true
return d.cDoc, func() {
if locked {
locked = false
d.m.Unlock()
}
}
}
// New creates a new empty document
func New() *Doc {
return must(wrap(C.AMcreate(nil)).item()).doc()
}
// Load loads a document from its serialized form
func Load(b []byte) (*Doc, error) {
cbytes, free := toByteSpan(b)
defer free()
item, err := wrap(C.AMload(cbytes.src, cbytes.count)).item()
if err != nil {
return nil, err
}
return item.doc(), nil
}
// Save exports a document to its serialized form
func (d *Doc) Save() []byte {
cDoc, unlock := d.lock()
defer unlock()
return must(wrap(C.AMsave(cDoc)).item()).bytes()
}
// RootMap returns the root of the document as a Map
func (d *Doc) RootMap() *Map {
return &Map{doc: d, objID: &objID{cObjID: (*C.AMobjId)(C.AM_ROOT)}}
}
// Root returns the root of the document as a Value
// of [KindMap]
func (d *Doc) Root() *Value {
return &Value{kind: KindMap, doc: d, val: d.RootMap()}
}
// Path returns a [*Path] that points to a position in the doc.
// Path will panic unless each path component is a string or an int.
// Calling Path with no arguments returns a path to the [Doc.Root].
func (d *Doc) Path(path ...any) *Path {
return (&Path{d: d}).Path(path...)
}
// CommitOptions are (rarer) options passed to commit.
// If Time is not set then time.Now() is used. To omit a timestamp pass a pointer to the zero time: &time.Time{}
// If AllowEmpty is not set then commits with no operations will error.
type CommitOptions struct {
Time *time.Time
AllowEmpty bool
}
// Commit adds a new version to the document with all operations so far.
// The returned ChangeHash is the new head of the document.
// Note: You should call commit immediately after modifying the document
// as most methods that inspect or modify the documents' history
// will automatically commit any outstanding changes.
func (d *Doc) Commit(msg string, opts ...CommitOptions) (ChangeHash, error) {
cDoc, unlock := d.lock()
defer unlock()
allowEmpty := false
time := time.Now()
for _, o := range opts {
if o.AllowEmpty {
allowEmpty = true
}
if o.Time != nil {
time = *o.Time
}
}
millis := (*C.int64_t)(C.NULL)
if !time.IsZero() {
m := time.UnixMilli()
millis = (*C.int64_t)(&m)
}
cMsg := C.AMbyteSpan{src: (*C.uchar)(C.NULL), count: 0}
if msg != "" {
var free func()
cMsg, free = toByteSpanStr(msg)
defer free()
}
item, err := wrap(C.AMcommit(cDoc, cMsg, millis)).item()
if err != nil {
return ChangeHash{}, err
}
if err == nil && item.Kind() == KindVoid {
if !allowEmpty {
return ChangeHash{}, fmt.Errorf("Commit is empty")
}
item, err = wrap(C.AMemptyChange(cDoc, cMsg, millis)).item()
if err != nil {
return ChangeHash{}, err
}
}
return item.changeHash(), nil
}
// Heads returns the hashes of the current heads for the document.
// For a new document with no changes, this will have length zero.
// If you have just created a commit, this will have length one. If
// you have applied independent changes from multiple actors, then the
// length will be greater that one.
// If you'd like to merge independent changes together call [Doc.Commit]
// passing a [CommitOptions] with AllowEmpty set to true.
func (d *Doc) Heads() []ChangeHash {
cDoc, unlock := d.lock()
defer unlock()
items := must(wrap(C.AMgetHeads(cDoc)).items())
return mapItems(items, func(i *item) ChangeHash {
return i.changeHash()
})
}
// Change gets a specific change by hash.
func (d *Doc) Change(ch ChangeHash) (*Change, error) {
cDoc, unlock := d.lock()
defer unlock()
byteSpan, free := toByteSpan(ch[:])
defer free()
item, err := wrap(C.AMgetChangeByHash(cDoc, byteSpan.src, byteSpan.count)).item()
if err != nil {
return nil, err
}
if item.Kind() == KindVoid {
return nil, fmt.Errorf("hash %s does not correspond to a change in this document", ch)
}
return item.change(), nil
}
// Changes returns all changes made to the doc since the given heads.
// If since is empty, returns all changes to recreate the document.
func (d *Doc) Changes(since ...ChangeHash) ([]*Change, error) {
cDoc, unlock := d.lock()
defer unlock()
items, err := itemsFromChangeHashes(since)
if err != nil {
return nil, err
}
cSince, free := createItems(items)
defer free()
items, err = wrap(C.AMgetChanges(cDoc, cSince)).items()
if err != nil {
return nil, err
}
return mapItems(items, func(i *item) *Change {
return i.change()
}), nil
}
// Apply the given change(s) to the document
func (d *Doc) Apply(chs ...*Change) error {
if len(chs) == 0 {
return nil
}
items := []*item{}
for _, ch := range chs {
items = append(items, ch.item)
}
cDoc, unlock := d.lock()
defer unlock()
cChs, free := createItems(items)
defer free()
return wrap(C.AMapplyChanges(cDoc, cChs)).void()
}
// SaveIncremental exports the changes since the last call to [Doc.Save] or
// [Doc.SaveIncremental] for passing to [Doc.LoadIncremental] on a different doc.
// See also [SyncState] for a more managed approach to syncing.
func (d *Doc) SaveIncremental() []byte {
cDoc, unlock := d.lock()
defer unlock()
return must(wrap(C.AMsaveIncremental(cDoc)).item()).bytes()
}
// LoadIncremental applies the changes exported by [Doc.SaveIncremental].
// It is the callers responsibility to ensure that every incremental change
// is applied to keep the documents in sync.
// See also [SyncState] for a more managed approach to syncing.
func (d *Doc) LoadIncremental(raw []byte) error {
cDoc, unlock := d.lock()
defer unlock()
cBytes, free := toByteSpan(raw)
defer free()
// returns the number of bytes read...
_, err := wrap(C.AMloadIncremental(cDoc, cBytes.src, cBytes.count)).item()
return err
}
// Fork returns a new, independent, copy of the document
// if asOf is empty then it is forked in its current state.
// otherwise it returns a version as of the given heads.
func (d *Doc) Fork(asOf ...ChangeHash) (*Doc, error) {
items, err := itemsFromChangeHashes(asOf)
if err != nil {
return nil, err
}
cDoc, unlock := d.lock()
defer unlock()
cAsOf, free := createItems(items)
defer free()
item, err := wrap(C.AMfork(cDoc, cAsOf)).item()
if err != nil {
return nil, err
}
return item.doc(), nil
}
// Merge extracts all changes from d2 that are not in d
// and then applies them to d.
func (d *Doc) Merge(d2 *Doc) ([]ChangeHash, error) {
cDoc, unlock := d.lock()
defer unlock()
cDoc2, unlock2 := d2.lock()
defer unlock2()
items, err := wrap(C.AMmerge(cDoc, cDoc2)).items()
if err != nil {
return nil, err
}
return mapItems(items, func(i *item) ChangeHash { return i.changeHash() }), nil
}
// ActorID returns the current actorId of the doc hex-encoded
// This is used for all operations that write to the document.
// By default a random ActorID is generated, but you can customize
// this with [Doc.SetActorID].
func (d *Doc) ActorID() string {
cDoc, unlock := d.lock()
defer unlock()
return must(wrap(C.AMgetActorId(cDoc)).item()).actorID().String()
}
// SetActorID updates the current actorId of the doc.
// Valid actor IDs are a string with an even number of hex-digits.
func (d *Doc) SetActorID(id string) error {
ai, err := itemFromActorID(id)
if err != nil {
return err
}
cDoc, unlock := d.lock()
defer unlock()
defer runtime.KeepAlive(ai)
return wrap(C.AMsetActorId(cDoc, ai.actorID().cActorID)).void()
}