Documentation
¶
Overview ¶
Package automerge provides the ability to interact with automerge documents. It is a featureful wrapper around automerge-rs that uses cgo to avoid reimplementing the core engine from scratch.
Document Structure and Types ¶
Automerge documents have a JSON-like structure, they consist of a root map which has string keys, and values of any supported types.
Supported types include several immutable primitive types:
- bool
- string, []byte
- float64, int64, uint64
- time.Time (in millisecond precision)
- nil (untyped)
And four mutable automerge types:
- Map - a mutable map[string]any
- List - a mutable []any
- Text – a mutable string
- Counter – an int64 that is incremented (instead of overwritten) by collaborators
If you read part of the doc that has no value set, automerge-go will return a Value with Kind() == KindVoid. You cannot create such a Value directly or write one to the document.
On write automerge-go will attempt to convert provided values to the most appropriate type, and error if that is not possible. For example structs are maps are converted to *Map, slices and arrays to *List, most numeric types are converted to float64 (the default number type for automerge), with the exception of int64 and uint64.
On read automerge-go will return a *Value, and you can use As to convert this to a more useful type.
Interacting with the Document ¶
Depending on your use-case there are a few ways to interact with the document, the recommended approach for reading is to cast the document to a go value:
doc, err := automerge.Load(bytes) if err != nil { return err } myVal, err := automerge.As[*myType](doc.Root()) if err != nil { return err }
If you wish to modify the document, or access just a subset, use a Path:
err := doc.Path("x", "y", 0).Set(&myStruct{Header: "h"}) v, err := automerge.As[*myStruct](doc.Path("x", "y", 0).Get())
It is always recommended to write the smallest change to the document, as this will improve the experience of other collaborative editors.
Writing to a path will create any intermediate Map or List objects needed, Reading from a path will not, but may return a void Value if the intermediate objects don't exist.
The automerge mutable types have additional methods. You can access these methods by calling Path.Map, Path.List, Path.Text or Path.Counter which assume the path is of the type you say it is:
values, err := doc.Path("collection").Map().Values() fmt.Println(values)
When you do this, any errors caused by traversing the path will be returned from methods called on the returned objects.
Controling formatting of structs ¶
By default automerge will convert your struct to a map. For each public field in the struct (the name starts with an uppercase letter) automerge will add an entry to the map with the name of the field and the fields value converted to an automerge value recursively.
You can control this behaviour using struct tags:
struct Example { Renamed bool `automerge:"newname"` Private bool `automerge:"-"` }
If the tag is present and equal to "-" the field will be ignored by automerge, otherwise the fields name will be set to the value of the tag.
Syncing and concurrency ¶
You can access methods on *Doc from multiple goroutines and access is mediated appropriately. For other types, you must provide your own syncronization, or only use them from one goroutine at a time.
If you retain a Map, List, Counter, or Text object while the document is being modified concurrently be aware that its value may change, or it may be deleted from the document. A safer pattern is to fork the document, make the changes you want, and then merge your changes back into the shared document.
There are a few different ways to keep distributed automerge docs in sync. If you're mostly making changes in one place and syncing them to another, you can use Doc.SaveIncremental and Doc.LoadIncremental
//* process 1 * initBytes, err := doc.Save() for { // ... make changes ... bytes, err := doc.SaveIncremental() ch <- bytes } //* process 2* err := automerge.Load(initBytes) for bytes := range ch { err := doc.LoadIncremental(bytes) }
If both peers are making changes you can use a SyncState object to keep them in sync. This wraps an underlying efficient sync protocol to minimize both round-trips and bandwidth used.
//* process 1 * syncState := automerge.NewSyncState(doc) for { m, valid := syncState.GenerateMessage() if valid { sendCh <- m.Bytes() } msg := <-recvCh _, err := syncState.ReceiveMessage(msg) if err != nil { panic(err) } } //* process 2 * syncState := automerge.NewSyncState(doc) for { msg := <-sendCh _, err := syncState.ReceiveMessage(msg) if err != nil { panic(err) } m, valid := syncState.GenerateMessage() if valid { recvCh <- m.Bytes() } }
If you need more flexibility, you can use Doc.Changes and Doc.Apply to manually track the changes you want to transfer. This puts more burden on the implementor to ensure an absense of bugs.
//* process 1 * heads := doc.Heads() for { // ... make changes ... changes, err := doc.Changes(heads) heads = doc.Heads() bytes := changes.Save() ch <- bytes } //* process 2 * for bytes := range ch { changes, err := automerge.LoadChanges(bytes) err := doc.Apply(changes) }
Index ¶
- func As[T any](v *Value, errs ...error) (ret T, err error)
- func NewActorID() string
- func SaveChanges(cs []*Change) []byte
- type Change
- type ChangeHash
- type CommitOptions
- type Counter
- type Doc
- func (d *Doc) ActorID() string
- func (d *Doc) Apply(chs ...*Change) error
- func (d *Doc) Change(ch ChangeHash) (*Change, error)
- func (d *Doc) Changes(since ...ChangeHash) ([]*Change, error)
- func (d *Doc) Commit(msg string, opts ...CommitOptions) (ChangeHash, error)
- func (d *Doc) Fork(asOf ...ChangeHash) (*Doc, error)
- func (d *Doc) Heads() []ChangeHash
- func (d *Doc) LoadIncremental(raw []byte) error
- func (d *Doc) Merge(d2 *Doc) ([]ChangeHash, error)
- func (d *Doc) Path(path ...any) *Path
- func (d *Doc) Root() *Value
- func (d *Doc) RootMap() *Map
- func (d *Doc) Save() []byte
- func (d *Doc) SaveIncremental() []byte
- func (d *Doc) SetActorID(id string) error
- type Kind
- type List
- func (l *List) Append(values ...any) error
- func (l *List) Delete(idx int) error
- func (l *List) Get(i int) (*Value, error)
- func (l *List) GoString() string
- func (l *List) Insert(idx int, value ...any) error
- func (l *List) Len() int
- func (l *List) Set(idx int, value any) error
- func (l *List) String() string
- func (l *List) Values() ([]*Value, error)
- type Map
- func (m *Map) Delete(key string) error
- func (m *Map) Get(key string) (*Value, error)
- func (m *Map) GoString() string
- func (m *Map) Keys() ([]string, error)
- func (m *Map) Len() int
- func (m *Map) Set(key string, value any) error
- func (m *Map) String() string
- func (m *Map) Values() (map[string]*Value, error)
- type Path
- func (p *Path) Counter() *Counter
- func (p *Path) Delete() error
- func (p *Path) Get() (*Value, error)
- func (p *Path) GoString() string
- func (p *Path) List() *List
- func (p *Path) Map() *Map
- func (p *Path) Path(path ...any) *Path
- func (p *Path) Set(v any) error
- func (p *Path) String() string
- func (p *Path) Text() *Text
- type SyncMessage
- type SyncState
- type Text
- func (t *Text) Append(s string) error
- func (t *Text) Delete(pos int, del int) error
- func (t *Text) Get() (string, error)
- func (t *Text) GoString() string
- func (t *Text) Insert(pos int, s string) error
- func (t *Text) Len() int
- func (t *Text) Set(s string) error
- func (t *Text) Splice(pos int, del int, s string) error
- func (t *Text) String() string
- type Value
- func (v *Value) Bool() bool
- func (v *Value) Bytes() []byte
- func (v *Value) Counter() *Counter
- func (v *Value) Float64() float64
- func (v *Value) GoString() string
- func (v *Value) Int64() int64
- func (v *Value) Interface() any
- func (v *Value) IsNull() bool
- func (v *Value) IsUnknown() bool
- func (v *Value) IsVoid() bool
- func (v *Value) Kind() Kind
- func (v *Value) List() *List
- func (v *Value) Map() *Map
- func (v *Value) Str() string
- func (v *Value) String() string
- func (v *Value) Text() *Text
- func (v *Value) Time() time.Time
- func (v *Value) Uint64() uint64
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func As ¶
As converts v to type T. If the v cannot be converted to T, an error will be returned. T can be any of the automerge builtin types (*Map, *List, *Counter, *Text), or any type that v's underlying value could be converted to. This conversion works generously, so that (for example) T could be any numeric type that v's value fits in, or if v.IsNull() then the 0-value of T will be returned.
To make it easier to wrap a call to Get(), when an not-nil error is passed as a second argument, As will return (nil, err)
Example ¶
doc := automerge.New() doc.Path("isValid").Set(true) doc.Path("foo", "bar").Set("baz") b, err := automerge.As[bool](doc.Path("isValid").Get()) if err != nil { panic(err) } fmt.Println("isValid:", b == true) v, err := automerge.As[string](doc.Path("foo", "bar").Get()) if err != nil { panic(err) } fmt.Println("foo-bar:", v) type S struct { IsValid bool `automerge:"isValid"` } s, err := automerge.As[*S](doc.Root()) if err != nil { panic(err) } fmt.Println("root valid:", s.IsValid == true)
Output: isValid: true foo-bar: baz root valid: true
func SaveChanges ¶
SaveChanges saves multiple changes to bytes (see also LoadChanges)
Types ¶
type Change ¶
type Change struct {
// contains filtered or unexported fields
}
Change is a set of mutations to the document. It is analagous to a commit in a version control system like Git.
func LoadChanges ¶
LoadChanges loads changes from bytes (see also SaveChanges and Change.Save)
func (*Change) ActorSeq ¶
ActorSeq is 1 for the first change by a given actor, 2 for the next, and so on.
func (*Change) Dependencies ¶
func (c *Change) Dependencies() []ChangeHash
Dependencies returns the hashes of all changes that this change directly depends on.
func (*Change) Hash ¶
func (c *Change) Hash() ChangeHash
Hash identifies the change by the SHA-256 of its binary representation
type ChangeHash ¶
type ChangeHash [32]byte
ChangeHash is a SHA-256 hash identifying an automerge change. Like a git commit, the hash encompasses both the changes made, any metadata (like commit message, or timestamp) and any changes on which this change depends.
func NewChangeHash ¶
func NewChangeHash(s string) (ChangeHash, error)
NewChangeHash creates a change has from its hex representation.
func (ChangeHash) String ¶
func (ch ChangeHash) String() string
String returns the hex-encoded form of the change hash
type CommitOptions ¶
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 Counter ¶
type Counter struct {
// contains filtered or unexported fields
}
Counter is a mutable int64 that collaborators can increment or decrement.
func NewCounter ¶
NewCounter returns a detached counter with the given starting value. Before you can call Counter.Get or Counter.Inc you must write it to the document.
type Doc ¶
type Doc struct {
// contains filtered or unexported fields
}
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.
func (*Doc) ActorID ¶
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 (*Doc) Change ¶
func (d *Doc) Change(ch ChangeHash) (*Change, error)
Change gets a specific change by hash.
func (*Doc) Changes ¶
func (d *Doc) Changes(since ...ChangeHash) ([]*Change, error)
Changes returns all changes made to the doc since the given heads. If since is empty, returns all changes to recreate the document.
func (*Doc) Commit ¶
func (d *Doc) Commit(msg string, opts ...CommitOptions) (ChangeHash, error)
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 (*Doc) Fork ¶
func (d *Doc) Fork(asOf ...ChangeHash) (*Doc, error)
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 (*Doc) Heads ¶
func (d *Doc) Heads() []ChangeHash
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 (*Doc) LoadIncremental ¶
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 (*Doc) Merge ¶
func (d *Doc) Merge(d2 *Doc) ([]ChangeHash, error)
Merge extracts all changes from d2 that are not in d and then applies them to d.
func (*Doc) Path ¶
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 (*Doc) SaveIncremental ¶
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 (*Doc) SetActorID ¶
SetActorID updates the current actorId of the doc. Valid actor IDs are a string with an even number of hex-digits.
type Kind ¶
type Kind uint
Kind represents the underlying type of a Value
var ( // KindVoid indicates the value was not present KindVoid Kind = C.AM_VAL_TYPE_VOID // KindBool indicates a bool KindBool Kind = C.AM_VAL_TYPE_BOOL // KindBytes indicates a []byte KindBytes Kind = C.AM_VAL_TYPE_BYTES // KindCounter indicates an *automerge.Counter KindCounter Kind = C.AM_VAL_TYPE_COUNTER // KindFloat64 indicates a float64 KindFloat64 Kind = C.AM_VAL_TYPE_F64 // KindInt indicates an int KindInt64 Kind = C.AM_VAL_TYPE_INT // KindUint indicates a uint KindUint64 Kind = C.AM_VAL_TYPE_UINT // KindNull indicates an explicit null was present KindNull Kind = C.AM_VAL_TYPE_NULL // KindStr indicates a string KindStr Kind = C.AM_VAL_TYPE_STR // KindTime indicates a time.Time KindTime Kind = C.AM_VAL_TYPE_TIMESTAMP // KindUnknown indicates an unknown type from a future version of automerge KindUnknown Kind = C.AM_VAL_TYPE_UNKNOWN // KindMap indicates an *automerge.Map KindMap Kind = kindObjType | C.AM_OBJ_TYPE_MAP // KindList indicates an *automerge.List KindList Kind = kindObjType | C.AM_OBJ_TYPE_LIST // KindText indicates an *automerge.Text KindText Kind = kindObjType | C.AM_OBJ_TYPE_TEXT )
type List ¶
type List struct {
// contains filtered or unexported fields
}
List is an automerge type that stores a list of Value's
func NewList ¶
func NewList() *List
NewList returns a detached list. Before you can read from or write to it you must write it to the document.
type Map ¶
type Map struct {
// contains filtered or unexported fields
}
Map is an automerge type that stores a map of strings to values
func NewMap ¶
func NewMap() *Map
NewMap returns a detached map. Before you can read from or write to it you must write it to the document.
func (*Map) Get ¶
Get retrieves the value from the map. This method will return an error if the underlying Get operation fails, or if this is the first attempt to access a Path.Map() and the path is not traverseable
func (*Map) Set ¶
Set sets a key in the map to a given value. This method may error if the underlying operation errors, the type you provide cannot be converted to an automerge type, or if this is the first write to a Path.Map and the path is not traverseable.
type Path ¶
type Path struct {
// contains filtered or unexported fields
}
Path is a cursor that lets you reach into the document
func (*Path) Counter ¶
Counter assumes there is a Counter at the given path. Calling methods on the counter will error if the path cannot be traversed or if the value at this path is not a counter.
func (*Path) List ¶
List assumes there is a List at the given path. Calling methods on the list will error if the path cannot be traversed or if the value at this path is not a list. If there is a void at this location, reading from the list will return void, and writing to the list will implicitly create it (and the path as necesasry).
func (*Path) Map ¶
Map assumes there is a Map at the given path. Calling methods on the map will error if the path cannot be traversed or if the value at this path is not a map. If there is a void at this location, writing to this map will implicitly create it (and the path as necessary).
func (*Path) Path ¶
Path extends the cursor with more path segments. It panics if any path segment is not a string or an int. It does not check that the path segment is traversable until you call a method that accesses the document.
func (*Path) Set ¶
Set sets the value at the given path, and creates any missing parent Maps or Lists needed.
type SyncMessage ¶
type SyncMessage struct {
// contains filtered or unexported fields
}
SyncMessage is sent between peers to keep copies of a document in sync.
func LoadSyncMessage ¶
func LoadSyncMessage(msg []byte) (*SyncMessage, error)
LoadSyncMessage decodes a sync message from a byte slice for inspection.
func (*SyncMessage) Bytes ¶
func (sm *SyncMessage) Bytes() []byte
Bytes returns a representation for sending over the network.
func (*SyncMessage) Changes ¶
func (sm *SyncMessage) Changes() []*Change
Changes returns any changes included in this SyncMessage
func (*SyncMessage) Heads ¶
func (sm *SyncMessage) Heads() []ChangeHash
Heads gives the heads of the peer that generated the SyncMessage
type SyncState ¶
type SyncState struct { Doc *Doc // contains filtered or unexported fields }
SyncState represents the state of syncing between a local copy of a doc and a peer; and lets you optimize bandwidth used to ensure two docs are always in sync.
Example ¶
doc := automerge.New() syncState := automerge.NewSyncState(doc) docUpdated := make(chan bool) recv := make(chan []byte) send := make(chan []byte) loop: // generate an initial message, and then do so again // after receiving updates from the peer or making local changes for { msg, valid := syncState.GenerateMessage() if valid { send <- msg.Bytes() } select { case msg, ok := <-recv: if !ok { break loop } _, err := syncState.ReceiveMessage(msg) if err != nil { panic(err) } case _, ok := <-docUpdated: if !ok { break loop } } }
Output:
func LoadSyncState ¶
LoadSyncState lets you resume syncing with a peer from where you left off.
func NewSyncState ¶
NewSyncState returns a new sync state to sync with a peer
func (*SyncState) GenerateMessage ¶
func (ss *SyncState) GenerateMessage() (sm *SyncMessage, valid bool)
GenerateMessage generates the next message to send to the client. If `valid` is false the clients are currently in sync and there are no more messages to send (until you either modify the underlying document)
func (*SyncState) ReceiveMessage ¶
func (ss *SyncState) ReceiveMessage(msg []byte) (*SyncMessage, error)
ReceiveMessage should be called with every message created by GenerateMessage on the peer side.
type Text ¶
type Text struct {
// contains filtered or unexported fields
}
Text is a mutable unicode string that can be edited collaboratively.
Note that automerge considers text to be a sequence of unicode codepoints while most go code treats strings as a sequence of bytes (that are hopefully valid utf8). In go programs unicode codepoints are stored as integers of type rune, and the unicode/utf8 package provides some helpers for common operations.
When editing Text you must pass positions and counts in terms of codepoints not bytes. For example if you wanted to replace the first instance of "🙃" you could do something like this:
s, _ := text.Get() => "😀🙃" byteIndex := strings.Index(s, "🙃") => 4 runeIndex := utf8.RuneCountInString(s[:byteIndex]) => 1 text.Splice(runeIndex, 1, "🧟") => "🙃🧟"
Although it is possible to represent invalid utf8 in a go string, automerge will error if you try to write invalid utf8 into a document.
If you are new to unicode it's worth pointing out that the number of codepoints does not necessarily correspond to the number of rendered glyphs (for example Text("👍🏼").Len() == 2). For more information consult the Unicode Consortium's FAQ.
func NewText ¶
NewText returns a detached Text with the given starting value. Before you can read or write it you must write it to the document.
func (*Text) Len ¶
Len returns number of unicode codepoints in the text, this may be less than the number of utf-8 bytes. For example Text("😀😀").Len() == 2, while len("😀😀") == 8.
func (*Text) Set ¶
Set overwrites the entire string with a new value, prefer to use Insert/Delete/Append/Splice as appropriate to preserves collaborators changes.
type Value ¶
type Value struct {
// contains filtered or unexported fields
}
Value represents a dynamically typed value read from a document. It can hold any of the supported primative types (bool, string, []byte, float64, int64, uint64, time.Time) the four mutable types (*Map, *List, *Text, *Counter), or it can be an explicit null, or a void to indicate that no value existed at all. You can convert from a Value to a go type using As, or call accessor methods directly.
func (*Value) Interface ¶
Interface returns the value as a go interface. It recursively converts automerge.Map to map[string]any, automerge.List to []any, automerge.Text to string, and automerge.Counter to int64.
func (*Value) String ¶
String returns a representation suitable for debugging. Use Value.Str to get the underlying string.
Source Files
¶
Directories
¶
Path | Synopsis |
---|---|
cmd
|
|
Package deps exists because shared libraries must be in the same directory as a go package for `go mod vendor` to work.
|
Package deps exists because shared libraries must be in the same directory as a go package for `go mod vendor` to work. |