-module(yog@builder@live). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/yog/builder/live.gleam"). -export([new/0, directed/0, undirected/0, add_edge/4, add_unweighted_edge/3, add_simple_edge/3, remove_edge/3, remove_node/2, sync/2, purge_pending/1, checkpoint/1, get_id/2, all_labels/1, node_count/1, pending_count/1, from_labeled/1]). -export_type([transition/2, live_builder/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " A live builder for incremental graph construction with label-to-ID registry.\n" "\n" " Unlike the static `labeled` builder which follows a \"Build-Freeze-Analyze\" pattern,\n" " `LiveBuilder` provides a **Transaction-style API** that tracks pending changes.\n" " This allows efficient synchronization of an existing `Graph` with new labeled edges\n" " in $O(\\Delta E)$ time, where $\\Delta E$ is the number of new edges since last sync.\n" "\n" " ## Use Cases\n" "\n" " - **REPL environments**: Incrementally build and analyze graphs\n" " - **UI editors**: Add nodes/edges interactively without rebuilding\n" " - **Streaming data**: Ingest new relationships as they arrive\n" " - **Large graphs**: Avoid $O(E)$ rebuild for single-edge updates\n" "\n" " ## Guarantees\n" "\n" " - **ID Stability:** Once a label is mapped to a `NodeId`, that mapping is immutable\n" " - **Idempotency:** Calling `sync` with no pending changes is effectively free\n" " - **Opaque Integration:** Uses the same ID generation as static builders\n" "\n" " ## Important: Managing the Pending Queue\n" "\n" " The `LiveBuilder` queues changes in memory until `sync()` is called. In streaming\n" " scenarios, if you add edges continuously without syncing, the pending queue will\n" " grow unbounded and consume memory.\n" "\n" " **Best Practice:** Sync periodically based on your workload:\n" "\n" " ```gleam\n" " // For high-frequency streaming (e.g., Kafka consumer)\n" " // Sync every N messages or every T seconds\n" " let #(builder, graph) = case live.pending_count(builder) > 1000 {\n" " True -> live.sync(builder, graph)\n" " False -> #(builder, graph)\n" " }\n" "\n" " // For batch processing\n" " // Build up a batch, then sync once\n" " let builder = list.fold(batch, builder, fn(b, edge) {\n" " live.add_edge(b, edge.0, edge.1, edge.2)\n" " })\n" " let #(builder, graph) = live.sync(builder, graph)\n" " ```\n" "\n" " **Recovery:** If you need to discard pending changes without applying them,\n" " use `purge_pending()` (abandon changes) or `checkpoint()` (keep registry).\n" "\n" " ## Limitations\n" "\n" " - **Memory:** Pending changes are stored in memory until synced\n" " - **No Persistence:** The pending queue is lost if the process crashes\n" " - **Single-threaded:** Not designed for concurrent updates from multiple actors\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import yog/builder/live\n" " import yog/pathfinding/dijkstra as pathfinding\n" " import gleam/int\n" "\n" " // Initial setup - build base graph\n" " let builder = live.new() |> live.add_edge(\"A\", \"B\", 10)\n" " let #(builder, graph) = live.sync(builder, yog.directed())\n" "\n" " // Incremental update - add new edge efficiently\n" " let builder = builder |> live.add_edge(\"B\", \"C\", 5)\n" " let #(builder, graph) = live.sync(builder, graph) // O(1) for just this edge!\n" "\n" " // Use with algorithms - get IDs from registry\n" " let assert Ok(a_id) = live.get_id(builder, \"A\")\n" " let assert Ok(c_id) = live.get_id(builder, \"C\")\n" " let path = pathfinding.shortest_path(graph, a_id, c_id, ...)\n" " ```\n" ). -type transition(NBB, NBC) :: {add_node, integer(), NBB} | {add_edge, integer(), integer(), NBC} | {remove_edge, integer(), integer()} | {remove_node, integer()}. -opaque live_builder(NBD, NBE) :: {live_builder, gleam@dict:dict(NBD, integer()), integer(), list(transition(NBD, NBE))}. -file("src/yog/builder/live.gleam", 115). ?DOC( " Creates a new empty live builder.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder = live.new()\n" " ```\n" ). -spec new() -> live_builder(any(), any()). new() -> {live_builder, maps:new(), 0, []}. -file("src/yog/builder/live.gleam", 129). ?DOC( " Creates a new live builder with a directed graph type in mind.\n" "\n" " This is a convenience function - the builder itself doesn't store\n" " the graph type, but it helps document intent.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder = live.directed()\n" " ```\n" ). -spec directed() -> live_builder(any(), any()). directed() -> new(). -file("src/yog/builder/live.gleam", 143). ?DOC( " Creates a new live builder with an undirected graph type in mind.\n" "\n" " This is a convenience function - the builder itself doesn't store\n" " the graph type, but it helps document intent.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder = live.undirected()\n" " ```\n" ). -spec undirected() -> live_builder(any(), any()). undirected() -> new(). -file("src/yog/builder/live.gleam", 158). ?DOC( " Gets or creates a node ID for the given label.\n" "\n" " If the label already exists in the registry, returns its existing ID.\n" " If not, assigns a new ID and queues an `AddNode` transition.\n" "\n" " > **Note:** This function is idempotent - calling it multiple times with the\n" " > same label always returns the same ID without adding duplicate transitions.\n" "\n" " ## Complexity\n" "\n" " - $O(\\log N)$ for dict lookup/insert where N is number of registered labels\n" ). -spec ensure_node(live_builder(NBR, NBS), NBR) -> {live_builder(NBR, NBS), integer()}. ensure_node(Builder, Label) -> case gleam_stdlib:map_get(erlang:element(2, Builder), Label) of {ok, Id} -> {Builder, Id}; {error, _} -> Id@1 = erlang:element(3, Builder), New_registry = gleam@dict:insert( erlang:element(2, Builder), Label, Id@1 ), Transition = {add_node, Id@1, Label}, New_pending = [Transition | erlang:element(4, Builder)], {{live_builder, New_registry, Id@1 + 1, New_pending}, Id@1} end. -file("src/yog/builder/live.gleam", 198). ?DOC( " Adds an edge between two labeled nodes.\n" "\n" " If either label doesn't exist in the registry, new nodes are created.\n" " The edge is queued as a pending transition and will be applied on next `sync`.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder =\n" " live.new()\n" " |> live.add_edge(\"home\", \"work\", 10)\n" " |> live.add_edge(\"work\", \"gym\", 5)\n" " ```\n" "\n" " ## Complexity\n" "\n" " - $O(\\log N)$ per label lookup/insert\n" ). -spec add_edge(live_builder(NBX, NBY), NBX, NBX, NBY) -> live_builder(NBX, NBY). add_edge(Builder, Src_label, Dst_label, Weight) -> {Builder@1, Src_id} = ensure_node(Builder, Src_label), {Builder@2, Dst_id} = ensure_node(Builder@1, Dst_label), Transition = {add_edge, Src_id, Dst_id, Weight}, {live_builder, erlang:element(2, Builder@2), erlang:element(3, Builder@2), [Transition | erlang:element(4, Builder@2)]}. -file("src/yog/builder/live.gleam", 223). ?DOC( " Adds an unweighted edge between two labeled nodes.\n" "\n" " This is a convenience function for graphs where edges have no meaningful weight.\n" " Uses `Nil` as the edge data type.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder: live.LiveBuilder(String, Nil) =\n" " live.directed()\n" " |> live.add_unweighted_edge(\"A\", \"B\")\n" " ```\n" ). -spec add_unweighted_edge(live_builder(NCD, nil), NCD, NCD) -> live_builder(NCD, nil). add_unweighted_edge(Builder, Src_label, Dst_label) -> add_edge(Builder, Src_label, Dst_label, nil). -file("src/yog/builder/live.gleam", 244). ?DOC( " Adds a simple edge with weight 1 between two labeled nodes.\n" "\n" " This is a convenience function for graphs with integer weights where\n" " a default weight of 1 is appropriate (e.g., unweighted graphs, hop counts).\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder =\n" " live.directed()\n" " |> live.add_simple_edge(\"A\", \"B\")\n" " |> live.add_simple_edge(\"B\", \"C\")\n" " ```\n" ). -spec add_simple_edge(live_builder(NCI, integer()), NCI, NCI) -> live_builder(NCI, integer()). add_simple_edge(Builder, Src_label, Dst_label) -> add_edge(Builder, Src_label, Dst_label, 1). -file("src/yog/builder/live.gleam", 268). ?DOC( " Removes an edge between two labeled nodes.\n" "\n" " If either label doesn't exist, no transition is queued.\n" " The removal is queued as a pending transition.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder =\n" " builder\n" " |> live.remove_edge(\"A\", \"B\")\n" " ```\n" "\n" " ## Complexity\n" "\n" " - $O(\\log N)$ per label lookup\n" ). -spec remove_edge(live_builder(NCN, NCO), NCN, NCN) -> live_builder(NCN, NCO). remove_edge(Builder, Src_label, Dst_label) -> case {gleam_stdlib:map_get(erlang:element(2, Builder), Src_label), gleam_stdlib:map_get(erlang:element(2, Builder), Dst_label)} of {{ok, Src_id}, {ok, Dst_id}} -> Transition = {remove_edge, Src_id, Dst_id}, {live_builder, erlang:element(2, Builder), erlang:element(3, Builder), [Transition | erlang:element(4, Builder)]}; {_, _} -> Builder end. -file("src/yog/builder/live.gleam", 305). ?DOC( " Removes a node by its label.\n" "\n" " The node and all its connected edges are removed. The ID is NOT reused.\n" " The label is removed from the registry so a future add would get a new ID.\n" "\n" " > **Warning:** Removing a node invalidates any cached IDs for that label.\n" " > After sync, `get_id(builder, label)` will return `Error(Nil)`.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder =\n" " builder\n" " |> live.remove_node(\"obsolete_node\")\n" " ```\n" "\n" " ## Complexity\n" "\n" " - $O(\\log N)$ for dict lookup/removal\n" ). -spec remove_node(live_builder(NCT, NCU), NCT) -> live_builder(NCT, NCU). remove_node(Builder, Label) -> case gleam_stdlib:map_get(erlang:element(2, Builder), Label) of {ok, Id} -> New_registry = gleam@dict:delete(erlang:element(2, Builder), Label), Transition = {remove_node, Id}, {live_builder, New_registry, erlang:element(3, Builder), [Transition | erlang:element(4, Builder)]}; {error, _} -> Builder end. -file("src/yog/builder/live.gleam", 370). ?DOC(" Applies a list of transitions to a graph.\n"). -spec apply_transitions(yog@model:graph(NDJ, NDK), list(transition(NDJ, NDK))) -> yog@model:graph(NDJ, NDK). apply_transitions(Graph, Transitions) -> gleam@list:fold(Transitions, Graph, fun(G, Transition) -> case Transition of {add_node, Id, Label} -> yog@model:add_node(G, Id, Label); {add_edge, Src, Dst, Weight} -> yog@model:add_edge(G, Src, Dst, Weight); {remove_edge, Src@1, Dst@1} -> yog@model:remove_edge(G, Src@1, Dst@1); {remove_node, Id@1} -> yog@model:remove_node(G, Id@1) end end). -file("src/yog/builder/live.gleam", 349). ?DOC( " Synchronizes pending changes to the given graph.\n" "\n" " Applies all pending transitions (in order) to the provided graph,\n" " then returns a new builder with an empty pending list and the updated graph.\n" "\n" " > **Note:** If there are no pending changes, this returns immediately\n" " > with the same builder and graph (effectively O(1)).\n" "\n" " > **Warning:** The graph type (directed/undirected) is determined by the\n" " > input graph. Make sure to provide a graph of the correct type on first sync.\n" "\n" " > **Performance:** For large pending queues (>1000 items), consider syncing\n" " > more frequently to avoid memory pressure and reduce sync latency.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " // Initial build\n" " let builder = live.new() |> live.add_edge(\"A\", \"B\", 10)\n" " let #(builder, graph) = live.sync(builder, yog.directed())\n" "\n" " // Incremental update\n" " let builder = builder |> live.add_edge(\"B\", \"C\", 5)\n" " let #(builder, graph) = live.sync(builder, graph) // Only applies B->C!\n" " ```\n" "\n" " ## Complexity\n" "\n" " - $O(\\Delta E)$ where $\\Delta E$ is the number of pending transitions\n" ). -spec sync(live_builder(NCZ, NDA), yog@model:graph(NCZ, NDA)) -> {live_builder(NCZ, NDA), yog@model:graph(NCZ, NDA)}. sync(Builder, Graph) -> case erlang:element(4, Builder) of [] -> {Builder, Graph}; Pending -> Transitions = lists:reverse(Pending), New_graph = apply_transitions(Graph, Transitions), {{live_builder, erlang:element(2, Builder), erlang:element(3, Builder), []}, New_graph} end. -file("src/yog/builder/live.gleam", 411). ?DOC( " Discards all pending transitions without applying them.\n" "\n" " This is a \"hard reset\" that abandons all queued changes. The registry\n" " (label→ID mappings) is preserved. Use this when you detect an error\n" " in your batch and want to start fresh.\n" "\n" " > **Compare:** `checkpoint()` also clears pending but is intended for\n" " > marking progress. `purge_pending()` is for abandoning work.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " // Queue some changes\n" " let builder = live.add_edge(builder, \"A\", \"B\", 10)\n" "\n" " // Oops, wrong data! Purge and start over.\n" " let builder = live.purge_pending(builder)\n" " // builder has no pending changes, registry still has A and B\n" " ```\n" ). -spec purge_pending(live_builder(NDS, NDT)) -> live_builder(NDS, NDT). purge_pending(Builder) -> {live_builder, erlang:element(2, Builder), erlang:element(3, Builder), []}. -file("src/yog/builder/live.gleam", 435). ?DOC( " Marks a checkpoint by discarding pending transitions.\n" "\n" " This is useful when you want to \"commit\" progress and discard the\n" " pending queue without applying it to a graph. The registry is preserved.\n" "\n" " > **Compare:** `purge_pending()` has the same effect but different intent.\n" " > Use `checkpoint()` for progress tracking, `purge_pending()` for error recovery.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let builder =\n" " live.new()\n" " |> live.add_edge(\"A\", \"B\", 10)\n" " |> live.checkpoint() // Discard the pending edge\n" "\n" " // builder now has no pending changes\n" " let #(builder, graph) = live.sync(builder, yog.directed())\n" " // graph is empty - the edge was discarded\n" " ```\n" ). -spec checkpoint(live_builder(NDY, NDZ)) -> live_builder(NDY, NDZ). checkpoint(Builder) -> {live_builder, erlang:element(2, Builder), erlang:element(3, Builder), []}. -file("src/yog/builder/live.gleam", 456). ?DOC( " Looks up the node ID for a given label.\n" "\n" " Returns `Ok(id)` if the label has been registered, `Error(Nil)` if not.\n" " Use this to get node IDs for use with graph algorithms.\n" "\n" " > **Note:** After `remove_node`, this will return `Error(Nil)` for that label.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let assert Ok(home_id) = live.get_id(builder, \"home\")\n" " let path = dijkstra.shortest_path(graph, home_id, ...)\n" " ```\n" "\n" " ## Complexity\n" "\n" " - $O(\\log N)$ for dict lookup\n" ). -spec get_id(live_builder(NEE, any()), NEE) -> {ok, integer()} | {error, nil}. get_id(Builder, Label) -> gleam_stdlib:map_get(erlang:element(2, Builder), Label). -file("src/yog/builder/live.gleam", 468). ?DOC( " Returns all labels that have been registered.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let labels = live.all_labels(builder)\n" " // [\"A\", \"B\", \"C\"]\n" " ```\n" ). -spec all_labels(live_builder(NEK, any())) -> list(NEK). all_labels(Builder) -> maps:keys(erlang:element(2, Builder)). -file("src/yog/builder/live.gleam", 479). ?DOC( " Returns the number of registered labels (nodes).\n" "\n" " ## Example\n" "\n" " ```gleam\n" " live.node_count(builder) // 5\n" " ```\n" ). -spec node_count(live_builder(any(), any())) -> integer(). node_count(Builder) -> maps:size(erlang:element(2, Builder)). -file("src/yog/builder/live.gleam", 496). ?DOC( " Returns the number of pending transitions.\n" "\n" " Useful for debugging or deciding whether to sync based on batch size.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " // Sync every 1000 changes to avoid unbounded growth\n" " case live.pending_count(builder) > 1000 {\n" " True -> live.sync(builder, graph)\n" " False -> #(builder, graph)\n" " }\n" " ```\n" ). -spec pending_count(live_builder(any(), any())) -> integer(). pending_count(Builder) -> erlang:length(erlang:element(4, Builder)). -file("src/yog/builder/live.gleam", 522). ?DOC( " Creates a live builder from an existing labeled builder.\n" "\n" " This allows migration from static to incremental building.\n" " The pending list starts empty - call `sync` separately to apply changes.\n" "\n" " > **Note:** This uses the labeled builder's exported registry. The label→ID\n" " > mappings are preserved exactly.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import yog/builder/labeled\n" "\n" " // Start with static builder\n" " let static = labeled.directed() |> labeled.add_edge(\"A\", \"B\", 10)\n" " let graph = labeled.to_graph(static)\n" "\n" " // Convert to live for incremental updates\n" " let live_builder = live.from_labeled(static)\n" " let live_builder = live.add_edge(live_builder, \"B\", \"C\", 5)\n" " let #(live_builder, graph) = live.sync(live_builder, graph)\n" " ```\n" ). -spec from_labeled(yog@builder@labeled:builder(NEX, NEY)) -> live_builder(NEX, NEY). from_labeled(Labeled_builder) -> Registry = yog@builder@labeled:to_registry(Labeled_builder), Next_id = yog@builder@labeled:next_id(Labeled_builder), {live_builder, Registry, Next_id, []}.