Grove (Grove v0.1.1)

View Source

Grove - Conflict-free replicated trees for Elixir.

Grove provides CRDT-based tree structures for collaborative editing of hierarchical data like documents, forms, and ASTs.

Quick Start

# Convert plain data to a CRDT tree
plain_data = %{
  id: "form_1",
  type: :form,
  attrs: %{title: "Survey"},
  children: [
    %{id: "field_1", type: :text, attrs: %{label: "Name"}}
  ]
}

tree = Grove.from_data(plain_data, replica_id: "node_a")

# Convert back to plain data
plain_data = Grove.to_data(tree)

Round-Trip Guarantee

When all nodes have explicit IDs:

data == Grove.to_data(Grove.from_data(data, replica_id: "a"))

Summary

Functions

Applies multiple CRDT operations atomically.

Like batch/2 but raises on error.

Buffers an update for debounced broadcast.

Clears the cursor position for this replica.

Clears the selection for this replica.

Flushes the update buffer, broadcasting all pending deltas.

Converts plain hierarchical data to a Grove tree.

Gets the current replica's presence metadata.

Gets all presences for the current document.

Updates the cursor position for this replica.

Updates which node has focus for this replica.

Updates the selection range for this replica.

Converts a Grove tree back to plain hierarchical data.

Functions

batch(crdt, fun)

@spec batch(
  struct(),
  (struct() -> struct())
) :: Grove.Batch.batch_result(struct())

Applies multiple CRDT operations atomically.

The batch function receives the CRDT and should return the modified CRDT. All operations within the batch accumulate into a single delta buffer.

Returns {:ok, updated_crdt} on success or {:error, reason, original_crdt} on failure.

Example

alias Grove.Set.ORSet

{:ok, set} = Grove.batch(ORSet.new(:node_a), fn s ->
  s
  |> ORSet.add("apple")
  |> ORSet.add("banana")
end)

See Grove.Batch.run/2 for details.

batch!(crdt, fun)

@spec batch!(
  struct(),
  (struct() -> struct())
) :: struct()

Like batch/2 but raises on error.

See Grove.Batch.run!/2 for details.

buffer_update(socket, fun, opts \\ [])

@spec buffer_update(map(), (Grove.Tree.t() -> {Grove.Tree.t(), term()}), keyword()) ::
  map()

Buffers an update for debounced broadcast.

See Grove.LiveView.buffer_update/3 for details.

clear_cursor(socket)

Clears the cursor position for this replica.

See Grove.LiveView.clear_cursor/1 for details.

clear_selection(socket)

Clears the selection for this replica.

See Grove.LiveView.clear_selection/1 for details.

flush_buffer(socket)

Flushes the update buffer, broadcasting all pending deltas.

See Grove.LiveView.flush_buffer/1 for details.

from_data(data, opts)

Converts plain hierarchical data to a Grove tree.

Options

  • :replica_id - Required. Unique identifier for this replica.

Example

tree = Grove.from_data(data, replica_id: "node_a")

See Grove.Data.from_data/2 for details.

get_my_presence(socket)

Gets the current replica's presence metadata.

See Grove.LiveView.get_my_presence/1 for details.

get_presences(socket)

Gets all presences for the current document.

See Grove.LiveView.get_presences/1 for details.

set_cursor(socket, node_id, offset \\ nil)

@spec set_cursor(map(), String.t(), non_neg_integer() | nil) :: map()

Updates the cursor position for this replica.

See Grove.LiveView.set_cursor/3 for details.

set_focus(socket, node_id)

Updates which node has focus for this replica.

See Grove.LiveView.set_focus/2 for details.

set_selection(socket, start_node, end_node, opts \\ [])

@spec set_selection(map(), String.t(), String.t(), keyword()) :: map()

Updates the selection range for this replica.

See Grove.LiveView.set_selection/4 for details.

to_data(tree)

Converts a Grove tree back to plain hierarchical data.

Deleted nodes are excluded from the output.

Example

plain_data = Grove.to_data(tree)

See Grove.Data.to_data/1 for details.