Grove.Tree.EventGraph (Grove v0.1.1)

View Source

DAG operations on event history.

Provides traversal, sorting, and concurrency detection for the event graph formed by parent references in events. Events form a directed acyclic graph (DAG) where edges point from child events to their parent events.

This module operates on lists of events without maintaining additional state, building indexes on-demand for efficient operations.

Event Graph Structure

Events form a DAG where:

  • Root events have no parents (created independently on different replicas)
  • Sequential events have exactly one parent (linear editing)
  • Merge events have multiple parents (concurrent branches joining)

Performance

Most operations are O(V + E) where V = number of events and E = number of edges. For typical collaborative editing with linear history, fast paths provide O(n) performance.

Summary

Functions

Returns all ancestors of an event (excluding the event itself).

Builds a map from parent ID to list of child IDs for reverse traversal.

Builds a map from event ID to Event for O(1) lookups.

Determines if two events are concurrent (neither is an ancestor of the other).

Returns all descendants of an event (excluding the event itself).

Computes the difference between two versions.

Returns events between two versions for linear forward movement.

Returns all events that occurred after the given version.

Finds the most recent common ancestor of two events.

Finds the last event where the version was critical (single frontier).

Sorts events in topological (causal) order using Kahn's algorithm.

Types

children_map()

@type children_map() :: %{required(event_id()) => [event_id()]}

event_id()

@type event_id() :: Grove.Tree.Event.event_id()

event_index()

@type event_index() :: %{required(event_id()) => Grove.Tree.Event.t()}

Functions

ancestors(events, event_id)

@spec ancestors([Grove.Tree.Event.t()], event_id()) :: MapSet.t(event_id())

Returns all ancestors of an event (excluding the event itself).

Ancestors are all events that are reachable by following parent edges.

Examples

iex> ancestors = EventGraph.ancestors(events, {"r1", 5})
iex> MapSet.member?(ancestors, {"r1", 1})
true

build_children_map(events)

@spec build_children_map([Grove.Tree.Event.t()]) :: children_map()

Builds a map from parent ID to list of child IDs for reverse traversal.

This enables efficient descendant queries by inverting the parent edges.

build_index(events)

@spec build_index([Grove.Tree.Event.t()]) :: event_index()

Builds a map from event ID to Event for O(1) lookups.

Examples

iex> events = [event1, event2, event3]
iex> index = EventGraph.build_index(events)
iex> Map.get(index, {"replica_1", 1})
%Event{id: {"replica_1", 1}, ...}

concurrent?(events, event_id, event_id)

@spec concurrent?([Grove.Tree.Event.t()], event_id(), event_id()) :: boolean()

Determines if two events are concurrent (neither is an ancestor of the other).

Two events are concurrent if they were created independently without knowledge of each other. This is determined by computing the vector clock for each event from its ancestry and checking if they are comparable.

Complexity

O(V) where V is the total number of ancestors to traverse.

Examples

iex> # Sequential events are not concurrent
iex> EventGraph.concurrent?(events, {"r1", 1}, {"r1", 2})
false

iex> # Events on different branches are concurrent
iex> EventGraph.concurrent?(events, {"r1", 2}, {"r2", 2})
true

iex> # Same event is not concurrent with itself
iex> EventGraph.concurrent?(events, {"r1", 1}, {"r1", 1})
false

descendants(events, event_id)

@spec descendants([Grove.Tree.Event.t()], event_id()) :: MapSet.t(event_id())

Returns all descendants of an event (excluding the event itself).

Descendants are all events that are reachable by following child edges.

Examples

iex> descendants = EventGraph.descendants(events, {"r1", 1})
iex> MapSet.member?(descendants, {"r1", 5})
true

diff(events, from_version, to_version)

@spec diff([Grove.Tree.Event.t()], Grove.Tree.Version.t(), Grove.Tree.Version.t()) ::
  %{
    retreat: [Grove.Tree.Event.t()],
    advance: [Grove.Tree.Event.t()]
  }

Computes the difference between two versions.

Returns events that need to be retreated (undone) and advanced (applied) to move from from_version to to_version.

Retreat events are in reverse topological order (children first, for safe undo). Advance events are in topological order (parents first, for safe apply).

Examples

iex> # Moving forward in linear history
iex> diff = EventGraph.diff(events, v1, v2)
iex> diff.retreat
[]
iex> length(diff.advance)
3

iex> # Switching branches
iex> diff = EventGraph.diff(events, branch_a_version, branch_b_version)
iex> length(diff.retreat)
2
iex> length(diff.advance)
1

events_between(events, from_version, to_version)

@spec events_between(
  [Grove.Tree.Event.t()],
  Grove.Tree.Version.t(),
  Grove.Tree.Version.t()
) ::
  {:ok, [Grove.Tree.Event.t()]} | {:error, :not_linear}

Returns events between two versions for linear forward movement.

This is a convenience function that returns only advance events when moving forward in a linear history. Returns an error if the movement requires retreating (branch switch).

Returns

  • {:ok, events} - Events to advance in topological order
  • {:error, :not_linear} - Movement requires retreat (not a linear forward)

Examples

iex> # Moving forward in linear history
iex> {:ok, events} = EventGraph.events_between(history, v1, v2)
iex> length(events)
3

iex> # Branch switch requires retreat
iex> EventGraph.events_between(history, branch_a, branch_b)
{:error, :not_linear}

events_since(events, frontier)

@spec events_since(
  [Grove.Tree.Event.t()],
  Grove.Tree.Version.t() | MapSet.t(event_id())
) ::
  {:ok, [Grove.Tree.Event.t()]} | {:error, :version_pruned}

Returns all events that occurred after the given version.

The version's frontier identifies the "tips" of the DAG at that point in time. This function returns all events that are descendants of the frontier.

Fast Path

When the frontier has exactly one event (critical version / linear history), this uses a simple list traversal instead of graph search.

Returns

  • {:ok, events} - List of events since the version
  • {:error, :version_pruned} - Frontier events are no longer in history

Examples

iex> # Get all events since a saved version
iex> {:ok, new_events} = EventGraph.events_since(tree.history, saved_version)

iex> # Empty frontier means return all events
iex> {:ok, all} = EventGraph.events_since(events, %Version{frontier: MapSet.new()})

find_common_ancestor(events, id, id)

@spec find_common_ancestor([Grove.Tree.Event.t()], event_id(), event_id()) ::
  {:ok, event_id()} | {:error, :no_common_ancestor}

Finds the most recent common ancestor of two events.

The common ancestor is the latest event that is an ancestor of both given events. This is useful for finding the point where two branches diverged.

Returns

  • {:ok, event_id} - The ID of the common ancestor
  • {:error, :no_common_ancestor} - Events have no common ancestor (e.g., different roots)

Examples

iex> # Same event is its own ancestor
iex> EventGraph.find_common_ancestor(events, {"r1", 1}, {"r1", 1})
{:ok, {"r1", 1}}

iex> # Find where two branches diverged
iex> EventGraph.find_common_ancestor(events, {"r1", 5}, {"r2", 3})
{:ok, {"r1", 2}}

find_last_critical(events)

@spec find_last_critical([Grove.Tree.Event.t()]) :: Grove.Tree.Event.t() | nil

Finds the last event where the version was critical (single frontier).

This is useful for replay optimization - we can skip events before the last critical point since state is known to be consistent.

Returns the most recent event that maintains critical version status, or nil if no such event exists.

Examples

iex> # Linear history - all events are critical
iex> event = EventGraph.find_last_critical(events)
iex> event.id
{"r1", 5}

iex> # After branching - finds last critical before branch
iex> event = EventGraph.find_last_critical(branched_events)
iex> event.id
{"r1", 2}

topological_sort(events, opts \\ [])

@spec topological_sort(
  [Grove.Tree.Event.t()],
  keyword()
) :: [Grove.Tree.Event.t()]

Sorts events in topological (causal) order using Kahn's algorithm.

Returns events ordered such that all parents appear before their children. This is the order in which events should be replayed to reconstruct state.

Options

  • :assume_linear - If true, assumes history is linear and just reverses the list. Use when Grove.Tree.Version.critical?/1 is true for better performance.

Complexity

  • O(V + E) for general case using Kahn's algorithm
  • O(n) when :assume_linear is true

Examples

iex> events = Tree.history(tree)
iex> sorted = EventGraph.topological_sort(events)
iex> # First event has no parents, last event is most recent

# Fast path for sequential history
iex> sorted = EventGraph.topological_sort(events, assume_linear: Version.critical?(tree.version))