Yog.Builder.Live (YogEx v0.70.0)

Copy Markdown View Source

A live builder for incremental graph construction with label-to-ID registry.

Unlike the static Yog.Builder.Labeled which follows a "Build-Freeze-Analyze" pattern, Live provides a Transaction-style API that tracks pending changes. This allows efficient synchronization of an existing Graph with new labeled edges in O(ΔE) time, where ΔE is the number of new edges since last sync.

Use Cases

  • REPL environments: Incrementally build and analyze graphs
  • UI editors: Add nodes/edges interactively without rebuilding
  • Streaming data: Ingest new relationships as they arrive
  • Large graphs: Avoid O(E) rebuild for single-edge updates

Guarantees

  • ID Stability: Once a label is mapped to a NodeId, that mapping is immutable
  • Idempotency: Calling sync/2 with no pending changes is effectively free
  • Opaque Integration: Uses the same ID generation as static builders

Important: Managing the Pending Queue

The Live builder queues changes in memory until sync/2 is called. In streaming scenarios, if you add edges continuously without syncing, the pending queue will grow unbounded and consume memory.

Best Practice: Sync periodically based on your workload:

# For high-frequency streaming (e.g., Kafka consumer)
# Sync every N messages or every T seconds
{builder, graph} =
  if Yog.Builder.Live.pending_count(builder) > 1000 do
    Yog.Builder.Live.sync(builder, graph)
  else
    {builder, graph}
  end

# For batch processing
# Build up a batch, then sync once
builder = Enum.reduce(batch, builder, fn {from, to, weight}, b ->
  Yog.Builder.Live.add_edge(b, from, to, weight)
end)
{builder, graph} = Yog.Builder.Live.sync(builder, graph)

Recovery

If you need to discard pending changes without applying them:

Limitations

  • Memory: Pending changes are stored in memory until synced
  • No Persistence: The pending queue is lost if the process crashes
  • Single-threaded: Not designed for concurrent updates from multiple actors

Example Usage

# Initial setup - build base graph
builder = Yog.Builder.Live.new() |> Yog.Builder.Live.add_edge("A", "B", 10)
{builder, graph} = Yog.Builder.Live.sync(builder, Yog.directed())

# Incremental update - add new edge efficiently
builder = Yog.Builder.Live.add_edge(builder, "B", "C", 5)
{builder, graph} = Yog.Builder.Live.sync(builder, graph)  # O(1) for just this edge!

# Use with algorithms - get IDs from registry
{:ok, a_id} = Yog.Builder.Live.get_id(builder, "A")
{:ok, c_id} = Yog.Builder.Live.get_id(builder, "C")

Migration Note: This module was ported from Gleam to pure Elixir in v0.53.0. The API remains unchanged.

Summary

Types

Legacy builder type (deprecated)

Any type can be used as a label

t()

Live builder struct

A pending transition

Functions

Adds an edge between two labeled nodes with a weight.

Adds a simple edge with weight 1 between two labeled nodes.

Adds an unweighted edge (weight = nil) between two labeled nodes.

Returns all labels that have been registered.

Creates a checkpoint by clearing pending changes while preserving the registry.

Creates a new live builder for directed graphs.

Creates a live builder from an existing labeled builder.

Looks up the internal node ID for a given label.

Creates a new live builder with the specified graph type.

Returns the number of registered nodes.

Returns the number of pending changes.

Discards all pending changes without applying them.

Removes an edge between two labeled nodes.

Removes a node by its label.

Applies all pending changes to the graph.

Creates a new live builder for undirected graphs.

Types

builder()

@type builder() :: {:live_builder, map(), integer(), [transition()]} | t()

Legacy builder type (deprecated)

label()

@type label() :: term()

Any type can be used as a label

t()

@type t() :: %Yog.Builder.Live{
  next_id: integer(),
  pending: [transition()],
  registry: %{required(label()) => Yog.node_id()}
}

Live builder struct

transition()

@type transition() ::
  {:add_node, Yog.node_id(), label()}
  | {:add_edge, Yog.node_id(), Yog.node_id(), term()}
  | {:remove_edge, Yog.node_id(), Yog.node_id()}
  | {:remove_node, Yog.node_id()}

A pending transition

Functions

add_edge(builder, from, to, weight)

@spec add_edge(t(), label(), label(), term()) :: t()

Adds an edge between two labeled nodes with a weight.

The change is queued until sync/2 is called.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> Yog.Builder.Live.pending_count(builder) > 0
true

add_simple_edge(builder, from, to)

@spec add_simple_edge(t(), label(), label()) :: t()

Adds a simple edge with weight 1 between two labeled nodes.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_simple_edge("A", "B")
iex> is_struct(builder, Yog.Builder.Live)
true

add_unweighted_edge(builder, from, to)

@spec add_unweighted_edge(t(), label(), label()) :: t()

Adds an unweighted edge (weight = nil) between two labeled nodes.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_unweighted_edge("A", "B")
iex> is_struct(builder, Yog.Builder.Live)
true

all_labels(arg1)

@spec all_labels(t() | builder()) :: [label()]

Returns all labels that have been registered.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> labels = Yog.Builder.Live.all_labels(builder)
iex> Enum.sort(labels)
["A", "B"]

checkpoint(builder)

@spec checkpoint(t() | builder()) :: t()

Creates a checkpoint by clearing pending changes while preserving the registry.

Similar to purge_pending/1 but conceptually marks a save point.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> builder = Yog.Builder.Live.checkpoint(builder)
iex> Yog.Builder.Live.pending_count(builder)
0

directed()

@spec directed() :: t()

Creates a new live builder for directed graphs.

Examples

iex> builder = Yog.Builder.Live.directed()
iex> is_struct(builder, Yog.Builder.Live)
true

from_labeled(labeled_builder)

@spec from_labeled(Yog.Builder.Labeled.t() | Yog.Builder.Labeled.builder()) :: t()

Creates a live builder from an existing labeled builder.

This is useful for transitioning from static to incremental building.

Examples

iex> labeled = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 5)
iex> builder = Yog.Builder.Live.from_labeled(labeled)
iex> is_struct(builder, Yog.Builder.Live)
true

get_id(arg1, label)

@spec get_id(t() | builder(), label()) :: {:ok, Yog.node_id()} | {:error, nil}

Looks up the internal node ID for a given label.

Returns {:ok, id} if the label exists in the registry, {:error, nil} otherwise.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
...> |> Yog.Builder.Live.sync(Yog.directed())
...> |> elem(0)
iex> Yog.Builder.Live.get_id(builder, "A")
{:ok, 0}

new()

@spec new() :: t()

Creates a new live builder with the specified graph type.

Examples

iex> builder = Yog.Builder.Live.new()
iex> is_struct(builder, Yog.Builder.Live)
true

node_count(arg1)

@spec node_count(t() | builder()) :: integer()

Returns the number of registered nodes.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> Yog.Builder.Live.node_count(builder)
2

pending_count(arg1)

@spec pending_count(t() | builder()) :: integer()

Returns the number of pending changes.

Use this to monitor queue growth and trigger syncs when needed.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> Yog.Builder.Live.pending_count(builder) > 0
true

purge_pending(builder)

@spec purge_pending(t() | builder()) :: t()

Discards all pending changes without applying them.

The registry (label-to-ID mappings) is preserved.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> builder = Yog.Builder.Live.purge_pending(builder)
iex> Yog.Builder.Live.pending_count(builder)
0

remove_edge(builder, from, to)

@spec remove_edge(t() | builder(), label(), label()) :: t()

Removes an edge between two labeled nodes.

The change is queued until sync/2 is called.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
...> |> Yog.Builder.Live.remove_edge("A", "B")
iex> is_struct(builder, Yog.Builder.Live)
true

remove_node(builder, label)

@spec remove_node(t() | builder(), label()) :: t()

Removes a node by its label.

Also removes all edges connected to this node. The change is queued until sync/2 is called.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
...> |> Yog.Builder.Live.remove_node("A")
iex> is_struct(builder, Yog.Builder.Live)
true

sync(builder, graph)

@spec sync(t() | builder(), Yog.graph()) :: {t(), Yog.graph()}

Applies all pending changes to the graph.

Returns {builder, updated_graph} where the builder has cleared its pending queue. This is an O(ΔE) operation where ΔE is the number of pending edges.

Examples

iex> builder = Yog.Builder.Live.new()
...> |> Yog.Builder.Live.add_edge("A", "B", 10)
iex> {_builder, graph} = Yog.Builder.Live.sync(builder, Yog.directed())
iex> length(Yog.all_nodes(graph))
2

undirected()

@spec undirected() :: t()

Creates a new live builder for undirected graphs.

Examples

iex> builder = Yog.Builder.Live.undirected()
iex> is_struct(builder, Yog.Builder.Live)
true