IrohEx.Automerge (iroh_ex v0.0.15)

High-level API for automerge CRDT documents.

Automerge is a JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.

Features

  • Maps: Nested key-value structures
  • Lists: Ordered sequences with concurrent insert support
  • Text: Collaborative text editing with character-level merging
  • Counters: Distributed counters that always converge correctly

Basic Usage

# Create a node first
node = IrohEx.Native.create_node(self(), IrohEx.NodeConfig.build())

# Create a document
doc_id = IrohEx.Automerge.new(node)

# Put some data
:ok = IrohEx.Automerge.put(node, doc_id, [], "name", "Alice")
:ok = IrohEx.Automerge.put(node, doc_id, [], "age", 30)

# Create nested structures
IrohEx.Automerge.create_map(node, doc_id, [], "profile")
:ok = IrohEx.Automerge.put(node, doc_id, ["profile"], "email", "alice@example.com")

# Read data
"Alice" = IrohEx.Automerge.get(node, doc_id, [], "name")

Merging Documents

# Fork a document to simulate concurrent edits
doc2 = IrohEx.Automerge.fork(node, doc_id)

# Make changes to both
:ok = IrohEx.Automerge.put(node, doc_id, [], "field_a", "value_a")
:ok = IrohEx.Automerge.put(node, doc2, [], "field_b", "value_b")

# Merge doc2 into doc_id
{:ok, bytes} = IrohEx.Automerge.save(node, doc2)
:ok = IrohEx.Automerge.merge(node, doc_id, bytes)

# Now doc_id has both changes!

Summary

Functions

Get the current counter value.

Increment a counter (creates it if it doesn't exist).

Create a list at the given path.

Create a nested map structure.

Create a collaborative text object.

Delete a document from memory.

Delete a key from a map.

Fork a document (create a copy with shared history).

Get a value at a nested path.

Get all keys from a map.

List all document IDs.

Delete a value from a list by index.

Get a value from a list by index.

Insert a value at a specific index.

Get the length of a list.

Push a value to the end of a list.

Load a document from binary data.

Merge another document's changes into this one.

Create a new empty automerge document.

Put a value at a nested path.

Save a document to binary format.

Sync document with all connected peers via gossip.

Get the full text content.

Get document as JSON string for debugging.

Types

doc_id()

@type doc_id() :: binary()

node_ref()

@type node_ref() :: reference()

path()

@type path() :: [binary()]

Functions

counter_get(node, doc_id, path, key)

@spec counter_get(node_ref(), doc_id(), path(), binary()) :: integer()

Get the current counter value.

Examples

42 = IrohEx.Automerge.counter_get(node, doc_id, [], "views")

counter_increment(node, doc_id, path, key, delta)

@spec counter_increment(node_ref(), doc_id(), path(), binary(), integer()) ::
  integer()

Increment a counter (creates it if it doesn't exist).

Counters are a special CRDT type that always converge correctly, even with concurrent increments from multiple nodes.

Examples

1 = IrohEx.Automerge.counter_increment(node, doc_id, [], "views", 1)
6 = IrohEx.Automerge.counter_increment(node, doc_id, [], "views", 5)
4 = IrohEx.Automerge.counter_increment(node, doc_id, [], "views", -2)

create_list(node, doc_id, path, key)

@spec create_list(node_ref(), doc_id(), path(), binary()) ::
  {:ok, binary()} | {:error, term()}

Create a list at the given path.

Examples

IrohEx.Automerge.create_list(node, doc_id, [], "items")

create_map(node, doc_id, path, key)

@spec create_map(node_ref(), doc_id(), path(), binary()) ::
  {:ok, binary()} | {:error, term()}

Create a nested map structure.

Examples

IrohEx.Automerge.create_map(node, doc_id, [], "users")
IrohEx.Automerge.create_map(node, doc_id, ["users"], "alice")
:ok = IrohEx.Automerge.put(node, doc_id, ["users", "alice"], "name", "Alice")

create_text(node, doc_id, path, key, initial_text \\ "")

@spec create_text(node_ref(), doc_id(), path(), binary(), binary()) ::
  {:ok, binary()} | {:error, term()}

Create a collaborative text object.

Text objects support character-level concurrent editing with automatic conflict resolution.

Examples

IrohEx.Automerge.create_text(node, doc_id, [], "content", "Hello World")

delete(node, doc_id)

@spec delete(node_ref(), doc_id()) :: boolean()

Delete a document from memory.

Examples

true = IrohEx.Automerge.delete(node, doc_id)

delete_key(node, doc_id, path, key)

@spec delete_key(node_ref(), doc_id(), path(), binary()) :: :ok | {:error, term()}

Delete a key from a map.

Examples

:ok = IrohEx.Automerge.delete_key(node, doc_id, [], "old_field")

fork(node, doc_id)

@spec fork(node_ref(), doc_id()) :: doc_id()

Fork a document (create a copy with shared history).

Forking is useful for:

  • Simulating concurrent edits in tests
  • Creating branches of a document
  • Offline editing that will be merged later

Examples

doc2 = IrohEx.Automerge.fork(node, doc_id)

get(node, doc_id, path, key)

@spec get(node_ref(), doc_id(), path(), binary()) :: term()

Get a value at a nested path.

Examples

"Alice" = IrohEx.Automerge.get(node, doc_id, [], "name")
:not_found = IrohEx.Automerge.get(node, doc_id, [], "nonexistent")

keys(node, doc_id, path)

@spec keys(node_ref(), doc_id(), path()) :: [binary()]

Get all keys from a map.

Examples

["name", "age", "email"] = IrohEx.Automerge.keys(node, doc_id, [])

list(node)

@spec list(node_ref()) :: [doc_id()]

List all document IDs.

Examples

doc_ids = IrohEx.Automerge.list(node)

list_delete(node, doc_id, path, index)

@spec list_delete(node_ref(), doc_id(), path(), non_neg_integer()) ::
  :ok | {:error, term()}

Delete a value from a list by index.

Examples

:ok = IrohEx.Automerge.list_delete(node, doc_id, ["items"], 0)

list_get(node, doc_id, path, index)

@spec list_get(node_ref(), doc_id(), path(), non_neg_integer()) :: term()

Get a value from a list by index.

Examples

"first" = IrohEx.Automerge.list_get(node, doc_id, ["items"], 0)

list_insert(node, doc_id, path, index, value)

@spec list_insert(node_ref(), doc_id(), path(), non_neg_integer(), term()) ::
  :ok | {:error, term()}

Insert a value at a specific index.

Examples

:ok = IrohEx.Automerge.list_insert(node, doc_id, ["items"], 0, "at_beginning")

list_length(node, doc_id, path)

@spec list_length(node_ref(), doc_id(), path()) :: non_neg_integer()

Get the length of a list.

Examples

3 = IrohEx.Automerge.list_length(node, doc_id, ["items"])

list_push(node, doc_id, path, value)

@spec list_push(node_ref(), doc_id(), path(), term()) :: :ok | {:error, term()}

Push a value to the end of a list.

Examples

IrohEx.Automerge.create_list(node, doc_id, [], "items")
:ok = IrohEx.Automerge.list_push(node, doc_id, ["items"], "first")
:ok = IrohEx.Automerge.list_push(node, doc_id, ["items"], "second")

load(node, data)

@spec load(node_ref(), binary()) :: {:ok, doc_id()} | {:error, term()}

Load a document from binary data.

Examples

{:ok, doc_id} = IrohEx.Automerge.load(node, saved_bytes)

merge(node, doc_id, other_doc_bytes)

@spec merge(node_ref(), doc_id(), binary()) :: :ok | {:error, term()}

Merge another document's changes into this one.

Examples

{:ok, other_bytes} = IrohEx.Automerge.save(node, other_doc_id)
:ok = IrohEx.Automerge.merge(node, doc_id, other_bytes)

new(node)

@spec new(node_ref()) :: doc_id()

Create a new empty automerge document.

Examples

doc_id = IrohEx.Automerge.new(node)

put(node, doc_id, path, key, value)

@spec put(node_ref(), doc_id(), path(), binary(), term()) :: :ok | {:error, term()}

Put a value at a nested path.

Parameters

  • node - The node reference
  • doc_id - The document ID
  • path - List of keys to navigate to (empty for root)
  • key - The key to set
  • value - The value (string, integer, float, boolean, or binary)

Examples

# Set at root level
:ok = IrohEx.Automerge.put(node, doc_id, [], "name", "Alice")

# Set at nested path (path must exist)
:ok = IrohEx.Automerge.put(node, doc_id, ["users", "alice"], "email", "alice@example.com")

save(node, doc_id)

@spec save(node_ref(), doc_id()) :: {:ok, binary()} | {:error, term()}

Save a document to binary format.

The binary format is compact and can be stored or transmitted.

Examples

{:ok, bytes} = IrohEx.Automerge.save(node, doc_id)
File.write!("document.automerge", bytes)

sync(node, doc_id)

@spec sync(node_ref(), doc_id()) :: :ok | {:error, term()}

Sync document with all connected peers via gossip.

This broadcasts the full document to all peers in the gossip network.

Examples

:ok = IrohEx.Automerge.sync(node, doc_id)

text_delete(node, doc_id, path, position, length)

@spec text_delete(node_ref(), doc_id(), path(), non_neg_integer(), non_neg_integer()) ::
  :ok | {:error, term()}

Delete text at a position.

Examples

:ok = IrohEx.Automerge.text_delete(node, doc_id, ["content"], 5, 10)
# Deletes 10 characters starting at position 5

text_get(node, doc_id, path)

@spec text_get(node_ref(), doc_id(), path()) :: binary()

Get the full text content.

Examples

"Hello World" = IrohEx.Automerge.text_get(node, doc_id, ["content"])

text_insert(node, doc_id, path, position, text)

@spec text_insert(node_ref(), doc_id(), path(), non_neg_integer(), binary()) ::
  :ok | {:error, term()}

Insert text at a position.

Examples

:ok = IrohEx.Automerge.text_insert(node, doc_id, ["content"], 5, " Beautiful")
# "Hello Beautiful World"

to_json(node, doc_id)

@spec to_json(node_ref(), doc_id()) :: binary()

Get document as JSON string for debugging.

Examples

json = IrohEx.Automerge.to_json(node, doc_id)
IO.puts(json)