Grove.LiveView (Grove v0.1.1)
View SourceLiveView integration for Grove CRDT trees.
This module provides helper functions for using Grove trees in Phoenix LiveView applications with real-time synchronization across connected clients.
Usage
defmodule MyAppWeb.FormLive do
use MyAppWeb, :live_view
def mount(%{"id" => doc_id}, _session, socket) do
socket = Grove.LiveView.mount_tree(socket, doc_id,
replica_id: socket.id,
pubsub: MyApp.PubSub
)
{:ok, socket}
end
def handle_info({:grove_delta, delta, from_pid}, socket) when from_pid != self() do
socket = Grove.LiveView.apply_remote(socket, delta)
{:noreply, socket}
end
def handle_event("update_field", %{"node_id" => node_id, "value" => value}, socket) do
socket = Grove.LiveView.apply_and_broadcast(socket, fn tree ->
updated_tree = Grove.Tree.update_node(tree, node_id, fn node ->
Grove.Node.update_attrs(node, %{value: value})
end)
delta = {:update_attrs, node_id, %{value: value}}
{updated_tree, delta}
end)
{:noreply, socket}
end
endSocket Assigns
This module stores the following assigns in the socket (all prefixed with grove_):
:grove_tree- The currentGrove.Treestruct:grove_replica_id- Unique identifier for this replica:grove_doc_id- Document identifier for the tree:grove_topic- PubSub topic for synchronization:grove_pubsub- PubSub module being used:grove_update_buffer- List of pending deltas for debounced broadcast:grove_buffer_timer- Timer reference for debounce flush
Configuration
You can configure a default PubSub module in your config:
config :grove, pubsub: MyApp.PubSubOr pass it explicitly to mount_tree/3.
Summary
Functions
Applies a local operation and broadcasts the delta to peers.
Applies a remote delta received from PubSub.
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.
Gets the document ID for this socket.
Gets the current replica's presence metadata.
Gets all presences for the current document.
Gets the replica ID for this socket.
Gets the current tree from the socket.
Mounts a Grove tree into the socket and subscribes to updates.
Updates the cursor position for this replica.
Updates which node has focus for this replica.
Updates the selection range for this replica.
Updates the tree in the socket without broadcasting.
Types
@type delta() :: term()
@type socket() :: Phoenix.LiveView.Socket.t() | map()
Functions
@spec apply_and_broadcast(socket(), (Grove.Tree.t() -> {Grove.Tree.t(), delta()})) :: socket()
Applies a local operation and broadcasts the delta to peers.
The function receives the current tree and should return a tuple of
{updated_tree, delta} where delta is the change to broadcast.
If there are pending buffered updates, they are flushed first to maintain operation order.
Example
socket = Grove.LiveView.apply_and_broadcast(socket, fn tree ->
updated_tree = Grove.Tree.update_node(tree, node_id, &update_fn/1)
delta = {:update, node_id, changes}
{updated_tree, delta}
end)
@spec apply_remote(socket(), delta(), (Grove.Tree.t(), delta() -> Grove.Tree.t())) :: socket()
Applies a remote delta received from PubSub.
This function should be called from your handle_info/2 callback
when receiving :grove_delta messages.
The apply_fn receives the current tree and the delta, and should
return the updated tree.
Batch deltas ({:batch, [deltas]}) are automatically handled by
applying each delta sequentially in order.
Example
def handle_info({:grove_delta, delta, from_pid}, socket) when from_pid != self() do
socket = Grove.LiveView.apply_remote(socket, delta, fn tree, delta ->
# Apply the delta to your tree
apply_delta(tree, delta)
end)
{:noreply, socket}
endIf no apply function is provided, the delta is expected to be a tree and will replace the current tree directly.
@spec buffer_update( socket(), (Grove.Tree.t() -> {Grove.Tree.t(), delta()}), keyword() ) :: socket()
Buffers an update for debounced broadcast.
The update is applied to the tree immediately for local responsiveness, but the delta is buffered and only broadcast after the debounce period.
This is useful for high-frequency operations like typing or cursor moves where you want immediate local feedback but reduced network traffic.
Options
:debounce- Debounce period in milliseconds. Defaults to 100ms.
Example
# High-frequency typing events
socket = Grove.LiveView.buffer_update(socket, fn tree ->
updated_tree = Grove.Tree.update_node(tree, node_id, fn node ->
Grove.Node.update_attrs(node, %{value: value})
end)
delta = {:update_value, node_id, value}
{updated_tree, delta}
end, debounce: 150)Timer Handling
You must handle the :grove_flush_buffer message in your LiveView:
def handle_info(:grove_flush_buffer, socket) do
{:noreply, Grove.flush_buffer(socket)}
end
Clears the cursor position for this replica.
Example
socket = Grove.LiveView.clear_cursor(socket)
Clears the selection for this replica.
Example
socket = Grove.LiveView.clear_selection(socket)
Flushes the update buffer, broadcasting all pending deltas.
If the buffer is empty, this is a no-op.
Pending deltas are wrapped in {:batch, [deltas]} and broadcast
to all subscribers. The batch preserves the order of operations.
Example
def handle_info(:grove_flush_buffer, socket) do
{:noreply, Grove.flush_buffer(socket)}
end
Gets the document ID for this socket.
Gets the current replica's presence metadata.
Returns the metadata map or nil if not tracking presence.
Example
case Grove.LiveView.get_my_presence(socket) do
%{cursor: cursor} -> # do something with cursor
nil -> # not tracking presence
end
Gets all presences for the current document.
Returns a map of %{replica_id => %{metas: [meta]}}.
Example
presences = Grove.LiveView.get_presences(socket)
# %{
# "replica_1" => %{metas: [%{cursor: %{node_id: "f1", offset: 10}, ...}]},
# "replica_2" => %{metas: [%{cursor: nil, ...}]}
# }
Gets the replica ID for this socket.
@spec get_tree(socket()) :: Grove.Tree.t()
Gets the current tree from the socket.
Example
tree = Grove.LiveView.get_tree(socket)
nodes = Grove.Tree.find_nodes(tree, type: :text)
Mounts a Grove tree into the socket and subscribes to updates.
This function initializes the socket with a Grove tree and subscribes to the PubSub topic for real-time synchronization.
Options
:replica_id- Required. Unique identifier for this replica (e.g.,socket.id):tree- Optional. Initial tree. Defaults to an empty tree.:pubsub- Optional. PubSub module. Defaults to configured value.
Example
socket = Grove.LiveView.mount_tree(socket, "doc_123",
replica_id: socket.id,
pubsub: MyApp.PubSub
)
@spec set_cursor(socket(), String.t(), non_neg_integer() | nil) :: socket()
Updates the cursor position for this replica.
The cursor represents the caret position within a node.
Parameters
socket- LiveView socket with presence tracking enablednode_id- The node ID where the cursor is positionedoffset- Optional character offset within the node
Example
socket = Grove.LiveView.set_cursor(socket, "field_123", 42)
Updates which node has focus for this replica.
Focus indicates which field or node the user is currently interacting with.
Example
socket = Grove.LiveView.set_focus(socket, "field_123")
Updates the selection range for this replica.
A selection spans from a start position to an end position, potentially across multiple nodes.
Parameters
socket- LiveView socket with presence tracking enabledstart_node- Node ID where selection startsend_node- Node ID where selection endsstart_offset- Optional character offset at startend_offset- Optional character offset at end
Example
socket = Grove.LiveView.set_selection(socket,
"field_1", "field_2",
start_offset: 10,
end_offset: 25
)
@spec update_tree(socket(), (Grove.Tree.t() -> Grove.Tree.t())) :: socket()
Updates the tree in the socket without broadcasting.
Use this for local-only updates that don't need to sync.