/** * @experimental * @since 3.18.0 */ import * as Data from "./Data.js"; import * as Equal from "./Equal.js"; import { dual } from "./Function.js"; import * as Hash from "./Hash.js"; import { format, NodeInspectSymbol } from "./Inspectable.js"; import * as Option from "./Option.js"; import { pipeArguments } from "./Pipeable.js"; /** * Safely get a value from a Map, returning an Option. * Uses explicit key presence check with map.has() for better safety. * @internal */ const getMapSafe = (map, key) => { if (map.has(key)) { return Option.some(map.get(key)); } return Option.none(); }; /** * Unique identifier for Graph instances. * * @since 3.18.0 * @category symbol */ export const TypeId = "~effect/Graph"; /** * Edge data containing source, target, and user data. * * @since 3.18.0 * @category models */ export class Edge extends Data.Class {} // ============================================================================= // Proto Objects // ============================================================================= /** @internal */ const ProtoGraph = { [TypeId]: TypeId, [Symbol.iterator]() { return this.nodes[Symbol.iterator](); }, [NodeInspectSymbol]() { return this.toJSON(); }, [Equal.symbol](that) { if (isGraph(that)) { if (this.nodes.size !== that.nodes.size || this.edges.size !== that.edges.size || this.type !== that.type) { return false; } // Compare nodes for (const [nodeIndex, nodeData] of this.nodes) { if (!that.nodes.has(nodeIndex)) { return false; } const otherNodeData = that.nodes.get(nodeIndex); if (!Equal.equals(nodeData, otherNodeData)) { return false; } } // Compare edges for (const [edgeIndex, edgeData] of this.edges) { if (!that.edges.has(edgeIndex)) { return false; } const otherEdge = that.edges.get(edgeIndex); if (!Equal.equals(edgeData, otherEdge)) { return false; } } return true; } return false; }, [Hash.symbol]() { let hash = Hash.string("Graph"); hash = hash ^ Hash.string(this.type); hash = hash ^ Hash.number(this.nodes.size); hash = hash ^ Hash.number(this.edges.size); for (const [nodeIndex, nodeData] of this.nodes) { hash = hash ^ Hash.hash(nodeIndex) + Hash.hash(nodeData); } for (const [edgeIndex, edgeData] of this.edges) { hash = hash ^ Hash.hash(edgeIndex) + Hash.hash(edgeData); } return hash; }, toJSON() { return { _id: "Graph", nodeCount: this.nodes.size, edgeCount: this.edges.size, type: this.type }; }, toString() { return format(this); }, pipe() { return pipeArguments(this, arguments); } }; // ============================================================================= // Constructors // ============================================================================= /** @internal */ export const isGraph = u => typeof u === "object" && u !== null && TypeId in u; /** * Creates a directed graph, optionally with initial mutations. * * @example * ```ts * import { Graph } from "effect" * * // Directed graph with initial nodes and edges * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * }) * ``` * * @since 3.18.0 * @category constructors */ export const directed = mutate => { const graph = Object.create(ProtoGraph); graph.type = "directed"; graph.nodes = new Map(); graph.edges = new Map(); graph.adjacency = new Map(); graph.reverseAdjacency = new Map(); graph.nextNodeIndex = 0; graph.nextEdgeIndex = 0; graph.isAcyclic = Option.some(true); graph.mutable = false; if (mutate) { const mutable = beginMutation(graph); mutate(mutable); return endMutation(mutable); } return graph; }; /** * Creates an undirected graph, optionally with initial mutations. * * @example * ```ts * import { Graph } from "effect" * * // Undirected graph with initial nodes and edges * const graph = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A-B") * Graph.addEdge(mutable, b, c, "B-C") * }) * ``` * * @since 3.18.0 * @category constructors */ export const undirected = mutate => { const graph = Object.create(ProtoGraph); graph.type = "undirected"; graph.nodes = new Map(); graph.edges = new Map(); graph.adjacency = new Map(); graph.reverseAdjacency = new Map(); graph.nextNodeIndex = 0; graph.nextEdgeIndex = 0; graph.isAcyclic = Option.some(true); graph.mutable = false; if (mutate) { const mutable = beginMutation(graph); mutate(mutable); return endMutation(mutable); } return graph; }; // ============================================================================= // Scoped Mutable API // ============================================================================= /** * Creates a mutable scope for safe graph mutations by copying the data structure. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const mutable = Graph.beginMutation(graph) * // Now mutable can be safely modified without affecting original graph * ``` * * @since 3.18.0 * @category mutations */ export const beginMutation = graph => { // Copy adjacency maps with deep cloned arrays const adjacency = new Map(); const reverseAdjacency = new Map(); for (const [nodeIndex, edges] of graph.adjacency) { adjacency.set(nodeIndex, [...edges]); } for (const [nodeIndex, edges] of graph.reverseAdjacency) { reverseAdjacency.set(nodeIndex, [...edges]); } const mutable = Object.create(ProtoGraph); mutable.type = graph.type; mutable.nodes = new Map(graph.nodes); mutable.edges = new Map(graph.edges); mutable.adjacency = adjacency; mutable.reverseAdjacency = reverseAdjacency; mutable.nextNodeIndex = graph.nextNodeIndex; mutable.nextEdgeIndex = graph.nextEdgeIndex; mutable.isAcyclic = graph.isAcyclic; mutable.mutable = true; return mutable; }; /** * Converts a mutable graph back to an immutable graph, ending the mutation scope. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const mutable = Graph.beginMutation(graph) * // ... perform mutations on mutable ... * const newGraph = Graph.endMutation(mutable) * ``` * * @since 3.18.0 * @category mutations */ export const endMutation = mutable => { const graph = Object.create(ProtoGraph); graph.type = mutable.type; graph.nodes = new Map(mutable.nodes); graph.edges = new Map(mutable.edges); graph.adjacency = mutable.adjacency; graph.reverseAdjacency = mutable.reverseAdjacency; graph.nextNodeIndex = mutable.nextNodeIndex; graph.nextEdgeIndex = mutable.nextEdgeIndex; graph.isAcyclic = mutable.isAcyclic; graph.mutable = false; return graph; }; /** * Performs scoped mutations on a graph, automatically managing the mutation lifecycle. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const newGraph = Graph.mutate(graph, (mutable) => { * // Safe mutations go here * // mutable gets automatically converted back to immutable * }) * ``` * * @since 3.18.0 * @category mutations */ export const mutate = /*#__PURE__*/dual(2, (graph, f) => { const mutable = beginMutation(graph); f(mutable); return endMutation(mutable); }); // ============================================================================= // Basic Node Operations // ============================================================================= /** * Adds a new node to a mutable graph and returns its index. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * console.log(nodeA) // NodeIndex with value 0 * console.log(nodeB) // NodeIndex with value 1 * }) * ``` * * @since 3.18.0 * @category mutations */ export const addNode = (mutable, data) => { const nodeIndex = mutable.nextNodeIndex; // Add node data mutable.nodes.set(nodeIndex, data); // Initialize empty adjacency lists mutable.adjacency.set(nodeIndex, []); mutable.reverseAdjacency.set(nodeIndex, []); // Update graph allocators mutable.nextNodeIndex = mutable.nextNodeIndex + 1; return nodeIndex; }; /** * Gets the data associated with a node index, if it exists. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") * }) * * const nodeIndex = 0 * const nodeData = Graph.getNode(graph, nodeIndex) * * if (Option.isSome(nodeData)) { * console.log(nodeData.value) // "Node A" * } * ``` * * @since 3.18.0 * @category getters */ export const getNode = (graph, nodeIndex) => getMapSafe(graph.nodes, nodeIndex); /** * Checks if a node with the given index exists in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") * }) * * const nodeIndex = 0 * const exists = Graph.hasNode(graph, nodeIndex) * console.log(exists) // true * * const nonExistentIndex = 999 * const notExists = Graph.hasNode(graph, nonExistentIndex) * console.log(notExists) // false * ``` * * @since 3.18.0 * @category getters */ export const hasNode = (graph, nodeIndex) => graph.nodes.has(nodeIndex); /** * Returns the number of nodes in the graph. * * @example * ```ts * import { Graph } from "effect" * * const emptyGraph = Graph.directed() * console.log(Graph.nodeCount(emptyGraph)) // 0 * * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => { * Graph.addNode(mutable, "Node A") * Graph.addNode(mutable, "Node B") * Graph.addNode(mutable, "Node C") * }) * * console.log(Graph.nodeCount(graphWithNodes)) // 3 * ``` * * @since 3.18.0 * @category getters */ export const nodeCount = graph => graph.nodes.size; /** * Finds the first node that matches the given predicate. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") * Graph.addNode(mutable, "Node B") * Graph.addNode(mutable, "Node C") * }) * * const result = Graph.findNode(graph, (data) => data.startsWith("Node B")) * console.log(result) // Option.some(1) * * const notFound = Graph.findNode(graph, (data) => data === "Node D") * console.log(notFound) // Option.none() * ``` * * @since 3.18.0 * @category getters */ export const findNode = (graph, predicate) => { for (const [index, data] of graph.nodes) { if (predicate(data)) { return Option.some(index); } } return Option.none(); }; /** * Finds all nodes that match the given predicate. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Start A") * Graph.addNode(mutable, "Node B") * Graph.addNode(mutable, "Start C") * }) * * const result = Graph.findNodes(graph, (data) => data.startsWith("Start")) * console.log(result) // [0, 2] * * const empty = Graph.findNodes(graph, (data) => data === "Not Found") * console.log(empty) // [] * ``` * * @since 3.18.0 * @category getters */ export const findNodes = (graph, predicate) => { const results = []; for (const [index, data] of graph.nodes) { if (predicate(data)) { results.push(index); } } return results; }; /** * Finds the first edge that matches the given predicate. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 10) * Graph.addEdge(mutable, nodeB, nodeC, 20) * }) * * const result = Graph.findEdge(graph, (data) => data > 15) * console.log(result) // Option.some(1) * * const notFound = Graph.findEdge(graph, (data) => data > 100) * console.log(notFound) // Option.none() * ``` * * @since 3.18.0 * @category getters */ export const findEdge = (graph, predicate) => { for (const [edgeIndex, edgeData] of graph.edges) { if (predicate(edgeData.data, edgeData.source, edgeData.target)) { return Option.some(edgeIndex); } } return Option.none(); }; /** * Finds all edges that match the given predicate. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 10) * Graph.addEdge(mutable, nodeB, nodeC, 20) * Graph.addEdge(mutable, nodeC, nodeA, 30) * }) * * const result = Graph.findEdges(graph, (data) => data >= 20) * console.log(result) // [1, 2] * * const empty = Graph.findEdges(graph, (data) => data > 100) * console.log(empty) // [] * ``` * * @since 3.18.0 * @category getters */ export const findEdges = (graph, predicate) => { const results = []; for (const [edgeIndex, edgeData] of graph.edges) { if (predicate(edgeData.data, edgeData.source, edgeData.target)) { results.push(edgeIndex); } } return results; }; /** * Updates a single node's data by applying a transformation function. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "Node A") * Graph.addNode(mutable, "Node B") * Graph.updateNode(mutable, 0, (data) => data.toUpperCase()) * }) * * const nodeData = Graph.getNode(graph, 0) * console.log(nodeData) // Option.some("NODE A") * ``` * * @since 3.18.0 * @category transformations */ export const updateNode = (mutable, index, f) => { if (!mutable.nodes.has(index)) { return; } const currentData = mutable.nodes.get(index); const newData = f(currentData); mutable.nodes.set(index, newData); }; /** * Updates a single edge's data by applying a transformation function. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10) * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2) * }) * * const edgeData = Graph.getEdge(result, 0) * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 }) * ``` * * @since 3.18.0 * @category mutations */ export const updateEdge = (mutable, edgeIndex, f) => { if (!mutable.edges.has(edgeIndex)) { return; } const currentEdge = mutable.edges.get(edgeIndex); const newData = f(currentEdge.data); mutable.edges.set(edgeIndex, { ...currentEdge, data: newData }); }; /** * Creates a new graph with transformed node data using the provided mapping function. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "node a") * Graph.addNode(mutable, "node b") * Graph.addNode(mutable, "node c") * Graph.mapNodes(mutable, (data) => data.toUpperCase()) * }) * * const nodeData = Graph.getNode(graph, 0) * console.log(nodeData) // Option.some("NODE A") * ``` * * @since 3.18.0 * @category transformations */ export const mapNodes = (mutable, f) => { // Transform existing node data in place for (const [index, data] of mutable.nodes) { const newData = f(data); mutable.nodes.set(index, newData); } }; /** * Transforms all edge data in a mutable graph using the provided mapping function. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 10) * Graph.addEdge(mutable, b, c, 20) * Graph.mapEdges(mutable, (data) => data * 2) * }) * * const edgeData = Graph.getEdge(graph, 0) * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 }) * ``` * * @since 3.18.0 * @category transformations */ export const mapEdges = (mutable, f) => { // Transform existing edge data in place for (const [index, edgeData] of mutable.edges) { const newData = f(edgeData.data); mutable.edges.set(index, { ...edgeData, data: newData }); } }; /** * Reverses all edge directions in a mutable graph by swapping source and target nodes. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) // A -> B * Graph.addEdge(mutable, b, c, 2) // B -> C * Graph.reverse(mutable) // Now B -> A, C -> B * }) * * const edge0 = Graph.getEdge(graph, 0) * console.log(edge0) // Option.some({ source: 1, target: 0, data: 1 }) - B -> A * ``` * * @since 3.18.0 * @category transformations */ export const reverse = mutable => { // Reverse all edges by swapping source and target for (const [index, edgeData] of mutable.edges) { mutable.edges.set(index, { source: edgeData.target, target: edgeData.source, data: edgeData.data }); } // Clear and rebuild adjacency lists with reversed directions mutable.adjacency.clear(); mutable.reverseAdjacency.clear(); // Rebuild adjacency lists with reversed directions for (const [edgeIndex, edgeData] of mutable.edges) { // Add to forward adjacency (source -> target) const sourceEdges = mutable.adjacency.get(edgeData.source) || []; sourceEdges.push(edgeIndex); mutable.adjacency.set(edgeData.source, sourceEdges); // Add to reverse adjacency (target <- source) const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || []; targetEdges.push(edgeIndex); mutable.reverseAdjacency.set(edgeData.target, targetEdges); } // Invalidate cycle flag since edge directions changed mutable.isAcyclic = Option.none(); }; /** * Filters and optionally transforms nodes in a mutable graph using a predicate function. * Nodes that return Option.none are removed along with all their connected edges. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "active") * const b = Graph.addNode(mutable, "inactive") * const c = Graph.addNode(mutable, "active") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 2) * * // Keep only "active" nodes and transform to uppercase * Graph.filterMapNodes(mutable, (data) => * data === "active" ? Option.some(data.toUpperCase()) : Option.none() * ) * }) * * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) * ``` * * @since 3.18.0 * @category transformations */ export const filterMapNodes = (mutable, f) => { const nodesToRemove = []; // First pass: identify nodes to remove and transform data for nodes to keep for (const [index, data] of mutable.nodes) { const result = f(data); if (Option.isSome(result)) { // Transform node data mutable.nodes.set(index, result.value); } else { // Mark for removal nodesToRemove.push(index); } } // Second pass: remove filtered out nodes and their edges for (const nodeIndex of nodesToRemove) { removeNode(mutable, nodeIndex); } }; /** * Filters and optionally transforms edges in a mutable graph using a predicate function. * Edges that return Option.none are removed from the graph. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 5) * Graph.addEdge(mutable, b, c, 15) * Graph.addEdge(mutable, c, a, 25) * * // Keep only edges with weight >= 10 and double their weight * Graph.filterMapEdges(mutable, (data) => * data >= 10 ? Option.some(data * 2) : Option.none() * ) * }) * * console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed) * ``` * * @since 3.18.0 * @category transformations */ export const filterMapEdges = (mutable, f) => { const edgesToRemove = []; // First pass: identify edges to remove and transform data for edges to keep for (const [index, edgeData] of mutable.edges) { const result = f(edgeData.data); if (Option.isSome(result)) { // Transform edge data mutable.edges.set(index, { ...edgeData, data: result.value }); } else { // Mark for removal edgesToRemove.push(index); } } // Second pass: remove filtered out edges for (const edgeIndex of edgesToRemove) { removeEdge(mutable, edgeIndex); } }; /** * Filters nodes by removing those that don't match the predicate. * This function modifies the mutable graph in place. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "active") * Graph.addNode(mutable, "inactive") * Graph.addNode(mutable, "pending") * Graph.addNode(mutable, "active") * * // Keep only "active" nodes * Graph.filterNodes(mutable, (data) => data === "active") * }) * * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) * ``` * * @since 3.18.0 * @category transformations */ export const filterNodes = (mutable, predicate) => { const nodesToRemove = []; // Identify nodes to remove for (const [index, data] of mutable.nodes) { if (!predicate(data)) { nodesToRemove.push(index); } } // Remove filtered out nodes (this also removes connected edges) for (const nodeIndex of nodesToRemove) { removeNode(mutable, nodeIndex); } }; /** * Filters edges by removing those that don't match the predicate. * This function modifies the mutable graph in place. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * * Graph.addEdge(mutable, a, b, 5) * Graph.addEdge(mutable, b, c, 15) * Graph.addEdge(mutable, c, a, 25) * * // Keep only edges with weight >= 10 * Graph.filterEdges(mutable, (data) => data >= 10) * }) * * console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed) * ``` * * @since 3.18.0 * @category transformations */ export const filterEdges = (mutable, predicate) => { const edgesToRemove = []; // Identify edges to remove for (const [index, edgeData] of mutable.edges) { if (!predicate(edgeData.data)) { edgesToRemove.push(index); } } // Remove filtered out edges for (const edgeIndex of edgesToRemove) { removeEdge(mutable, edgeIndex); } }; // ============================================================================= // Cycle Flag Management (Internal) // ============================================================================= /** @internal */ const invalidateCycleFlagOnRemoval = mutable => { // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs) // If already unknown (null) or acyclic (true), no need to change if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === false) { mutable.isAcyclic = Option.none(); } }; /** @internal */ const invalidateCycleFlagOnAddition = mutable => { // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs) // If already unknown (null) or cyclic (false), no need to change if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === true) { mutable.isAcyclic = Option.none(); } }; // ============================================================================= // Edge Operations // ============================================================================= /** * Adds a new edge to a mutable graph and returns its index. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) * console.log(edge) // EdgeIndex with value 0 * }) * ``` * * @since 3.18.0 * @category mutations */ export const addEdge = (mutable, source, target, data) => { // Validate that both nodes exist if (!mutable.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`); } if (!mutable.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`); } const edgeIndex = mutable.nextEdgeIndex; // Create edge data const edgeData = new Edge({ source, target, data }); mutable.edges.set(edgeIndex, edgeData); // Update adjacency lists const sourceAdjacency = getMapSafe(mutable.adjacency, source); if (Option.isSome(sourceAdjacency)) { sourceAdjacency.value.push(edgeIndex); } const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target); if (Option.isSome(targetReverseAdjacency)) { targetReverseAdjacency.value.push(edgeIndex); } // For undirected graphs, add reverse connections if (mutable.type === "undirected") { const targetAdjacency = getMapSafe(mutable.adjacency, target); if (Option.isSome(targetAdjacency)) { targetAdjacency.value.push(edgeIndex); } const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source); if (Option.isSome(sourceReverseAdjacency)) { sourceReverseAdjacency.value.push(edgeIndex); } } // Update allocators mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1; // Only invalidate cycle flag if the graph was acyclic // Adding edges cannot remove cycles from cyclic graphs invalidateCycleFlagOnAddition(mutable); return edgeIndex; }; /** * Removes a node and all its incident edges from a mutable graph. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * Graph.addEdge(mutable, nodeA, nodeB, 42) * * // Remove nodeA and all edges connected to it * Graph.removeNode(mutable, nodeA) * }) * ``` * * @since 3.18.0 * @category mutations */ export const removeNode = (mutable, nodeIndex) => { // Check if node exists if (!mutable.nodes.has(nodeIndex)) { return; // Node doesn't exist, nothing to remove } // Collect all incident edges for removal const edgesToRemove = []; // Get outgoing edges const outgoingEdges = getMapSafe(mutable.adjacency, nodeIndex); if (Option.isSome(outgoingEdges)) { for (const edge of outgoingEdges.value) { edgesToRemove.push(edge); } } // Get incoming edges const incomingEdges = getMapSafe(mutable.reverseAdjacency, nodeIndex); if (Option.isSome(incomingEdges)) { for (const edge of incomingEdges.value) { edgesToRemove.push(edge); } } // Remove all incident edges for (const edgeIndex of edgesToRemove) { removeEdgeInternal(mutable, edgeIndex); } // Remove the node itself mutable.nodes.delete(nodeIndex); mutable.adjacency.delete(nodeIndex); mutable.reverseAdjacency.delete(nodeIndex); // Only invalidate cycle flag if the graph wasn't already known to be acyclic // Removing nodes cannot introduce cycles in an acyclic graph invalidateCycleFlagOnRemoval(mutable); }; /** * Removes an edge from a mutable graph. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) * * // Remove the edge * Graph.removeEdge(mutable, edge) * }) * ``` * * @since 3.18.0 * @category mutations */ export const removeEdge = (mutable, edgeIndex) => { const wasRemoved = removeEdgeInternal(mutable, edgeIndex); // Only invalidate cycle flag if an edge was actually removed // and only if the graph wasn't already known to be acyclic if (wasRemoved) { invalidateCycleFlagOnRemoval(mutable); } }; /** @internal */ const removeEdgeInternal = (mutable, edgeIndex) => { // Get edge data const edge = getMapSafe(mutable.edges, edgeIndex); if (Option.isNone(edge)) { return false; // Edge doesn't exist, no mutation occurred } const { source, target } = edge.value; // Remove from adjacency lists const sourceAdjacency = getMapSafe(mutable.adjacency, source); if (Option.isSome(sourceAdjacency)) { const index = sourceAdjacency.value.indexOf(edgeIndex); if (index !== -1) { sourceAdjacency.value.splice(index, 1); } } const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target); if (Option.isSome(targetReverseAdjacency)) { const index = targetReverseAdjacency.value.indexOf(edgeIndex); if (index !== -1) { targetReverseAdjacency.value.splice(index, 1); } } // For undirected graphs, remove reverse connections if (mutable.type === "undirected") { const targetAdjacency = getMapSafe(mutable.adjacency, target); if (Option.isSome(targetAdjacency)) { const index = targetAdjacency.value.indexOf(edgeIndex); if (index !== -1) { targetAdjacency.value.splice(index, 1); } } const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source); if (Option.isSome(sourceReverseAdjacency)) { const index = sourceReverseAdjacency.value.indexOf(edgeIndex); if (index !== -1) { sourceReverseAdjacency.value.splice(index, 1); } } } // Remove edge data mutable.edges.delete(edgeIndex); return true; // Edge was successfully removed }; // ============================================================================= // Edge Query Operations // ============================================================================= /** * Gets the edge data associated with an edge index, if it exists. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * Graph.addEdge(mutable, nodeA, nodeB, 42) * }) * * const edgeIndex = 0 * const edgeData = Graph.getEdge(graph, edgeIndex) * * if (Option.isSome(edgeData)) { * console.log(edgeData.value.data) // 42 * console.log(edgeData.value.source) // NodeIndex(0) * console.log(edgeData.value.target) // NodeIndex(1) * } * ``` * * @since 3.18.0 * @category getters */ export const getEdge = (graph, edgeIndex) => getMapSafe(graph.edges, edgeIndex); /** * Checks if an edge exists between two nodes in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 42) * }) * * const nodeA = 0 * const nodeB = 1 * const nodeC = 2 * * const hasAB = Graph.hasEdge(graph, nodeA, nodeB) * console.log(hasAB) // true * * const hasAC = Graph.hasEdge(graph, nodeA, nodeC) * console.log(hasAC) // false * ``` * * @since 3.18.0 * @category getters */ export const hasEdge = (graph, source, target) => { const adjacencyList = getMapSafe(graph.adjacency, source); if (Option.isNone(adjacencyList)) { return false; } // Check if any edge in the adjacency list connects to the target for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge) && edge.value.target === target) { return true; } } return false; }; /** * Returns the number of edges in the graph. * * @example * ```ts * import { Graph } from "effect" * * const emptyGraph = Graph.directed() * console.log(Graph.edgeCount(emptyGraph)) // 0 * * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 1) * Graph.addEdge(mutable, nodeB, nodeC, 2) * Graph.addEdge(mutable, nodeC, nodeA, 3) * }) * * console.log(Graph.edgeCount(graphWithEdges)) // 3 * ``` * * @since 3.18.0 * @category getters */ export const edgeCount = graph => graph.edges.size; /** * Returns the neighboring nodes (targets of outgoing edges) for a given node. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 1) * Graph.addEdge(mutable, nodeA, nodeC, 2) * }) * * const nodeA = 0 * const nodeB = 1 * const nodeC = 2 * * const neighborsA = Graph.neighbors(graph, nodeA) * console.log(neighborsA) // [NodeIndex(1), NodeIndex(2)] * * const neighborsB = Graph.neighbors(graph, nodeB) * console.log(neighborsB) // [] * ``` * * @since 3.18.0 * @category getters */ export const neighbors = (graph, nodeIndex) => { const adjacencyList = getMapSafe(graph.adjacency, nodeIndex); if (Option.isNone(adjacencyList)) { return []; } const result = []; for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { result.push(edge.value.target); } } return result; }; /** * Get neighbors of a node in a specific direction for bidirectional traversal. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, "A->B") * }) * * const nodeA = 0 * const nodeB = 1 * * // Get outgoing neighbors (nodes that nodeA points to) * const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing") * * // Get incoming neighbors (nodes that point to nodeB) * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming") * ``` * * @since 3.18.0 * @category queries */ export const neighborsDirected = (graph, nodeIndex, direction) => { const adjacencyMap = direction === "incoming" ? graph.reverseAdjacency : graph.adjacency; const adjacencyList = getMapSafe(adjacencyMap, nodeIndex); if (Option.isNone(adjacencyList)) { return []; } const result = []; for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { // For incoming direction, we want the source node instead of target const neighborNode = direction === "incoming" ? edge.value.source : edge.value.target; result.push(neighborNode); } } return result; }; // ============================================================================= // GraphViz Export // ============================================================================= /** * Exports a graph to GraphViz DOT format for visualization. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 1) * Graph.addEdge(mutable, nodeB, nodeC, 2) * Graph.addEdge(mutable, nodeC, nodeA, 3) * }) * * const dot = Graph.toGraphViz(graph) * console.log(dot) * // digraph G { * // "0" [label="Node A"]; * // "1" [label="Node B"]; * // "2" [label="Node C"]; * // "0" -> "1" [label="1"]; * // "1" -> "2" [label="2"]; * // "2" -> "0" [label="3"]; * // } * ``` * * @since 3.18.0 * @category utils */ export const toGraphViz = (graph, options) => { const { edgeLabel = data => String(data), graphName = "G", nodeLabel = data => String(data) } = options ?? {}; const isDirected = graph.type === "directed"; const graphType = isDirected ? "digraph" : "graph"; const edgeOperator = isDirected ? "->" : "--"; const lines = []; lines.push(`${graphType} ${graphName} {`); // Add nodes for (const [nodeIndex, nodeData] of graph.nodes) { const label = nodeLabel(nodeData).replace(/"/g, "\\\""); lines.push(` "${nodeIndex}" [label="${label}"];`); } // Add edges for (const [, edgeData] of graph.edges) { const label = edgeLabel(edgeData.data).replace(/"/g, "\\\""); lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`); } lines.push("}"); return lines.join("\n"); }; // ============================================================================= // ============================================================================= // Graph Structure Analysis Algorithms (Phase 5A) // ============================================================================= /** * Checks if the graph is acyclic (contains no cycles). * * Uses depth-first search to detect back edges, which indicate cycles. * For directed graphs, any back edge creates a cycle. For undirected graphs, * a back edge that doesn't go to the immediate parent creates a cycle. * * @example * ```ts * import { Graph } from "effect" * * // Acyclic directed graph (DAG) * const dag = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * }) * console.log(Graph.isAcyclic(dag)) // true * * // Cyclic directed graph * const cyclic = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle * }) * console.log(Graph.isAcyclic(cyclic)) // false * ``` * * @since 3.18.0 * @category algorithms */ export const isAcyclic = graph => { // Use existing cycle flag if available if (Option.isSome(graph.isAcyclic)) { return graph.isAcyclic.value; } // Stack-safe DFS cycle detection using iterative approach const visited = new Set(); const recursionStack = new Set(); // Get all nodes to handle disconnected components for (const startNode of graph.nodes.keys()) { if (visited.has(startNode)) { continue; // Already processed this component } // Iterative DFS with explicit stack const stack = [[startNode, [], 0, true]]; while (stack.length > 0) { const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1]; // First visit to this node if (isFirstVisit) { if (recursionStack.has(node)) { // Back edge found - cycle detected graph.isAcyclic = Option.some(false); return false; } if (visited.has(node)) { stack.pop(); continue; } visited.add(node); recursionStack.add(node); // Get neighbors for this node const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing")); stack[stack.length - 1] = [node, nodeNeighbors, 0, false]; continue; } // Process next neighbor if (neighborIndex < neighbors.length) { const neighbor = neighbors[neighborIndex]; stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false]; if (recursionStack.has(neighbor)) { // Back edge found - cycle detected graph.isAcyclic = Option.some(false); return false; } if (!visited.has(neighbor)) { stack.push([neighbor, [], 0, true]); } } else { // Done with this node - backtrack recursionStack.delete(node); stack.pop(); } } } // Cache the result graph.isAcyclic = Option.some(true); return true; }; /** * Checks if an undirected graph is bipartite. * * A bipartite graph is one whose vertices can be divided into two disjoint sets * such that no two vertices within the same set are adjacent. Uses BFS coloring * to determine bipartiteness. * * @example * ```ts * import { Graph } from "effect" * * // Bipartite graph (alternating coloring possible) * const bipartite = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * const d = Graph.addNode(mutable, "D") * Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D} * Graph.addEdge(mutable, b, c, "edge") * Graph.addEdge(mutable, c, d, "edge") * }) * console.log(Graph.isBipartite(bipartite)) // true * * // Non-bipartite graph (odd cycle) * const triangle = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "edge") * Graph.addEdge(mutable, b, c, "edge") * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle) * }) * console.log(Graph.isBipartite(triangle)) // false * ``` * * @since 3.18.0 * @category algorithms */ export const isBipartite = graph => { const coloring = new Map(); const discovered = new Set(); let isBipartiteGraph = true; // Get all nodes to handle disconnected components for (const startNode of graph.nodes.keys()) { if (!discovered.has(startNode)) { // Start BFS coloring from this component const queue = [startNode]; coloring.set(startNode, 0); // Color start node with 0 discovered.add(startNode); while (queue.length > 0 && isBipartiteGraph) { const current = queue.shift(); const currentColor = coloring.get(current); const neighborColor = currentColor === 0 ? 1 : 0; // Get all neighbors for undirected graph const nodeNeighbors = getUndirectedNeighbors(graph, current); for (const neighbor of nodeNeighbors) { if (!discovered.has(neighbor)) { // Color unvisited neighbor with opposite color coloring.set(neighbor, neighborColor); discovered.add(neighbor); queue.push(neighbor); } else { // Check if neighbor has the same color (conflict) if (coloring.get(neighbor) === currentColor) { isBipartiteGraph = false; break; } } } } // Early exit if not bipartite if (!isBipartiteGraph) { break; } } } return isBipartiteGraph; }; /** * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency. * For undirected graphs, we need to find the other endpoint of each edge incident to the node. */ const getUndirectedNeighbors = (graph, nodeIndex) => { const neighbors = new Set(); // Check edges where this node is the source const adjacencyList = getMapSafe(graph.adjacency, nodeIndex); if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { // For undirected graphs, the neighbor is the other endpoint const otherNode = edge.value.source === nodeIndex ? edge.value.target : edge.value.source; neighbors.add(otherNode); } } } return Array.from(neighbors); }; /** * Find connected components in an undirected graph. * Each component is represented as an array of node indices. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * const d = Graph.addNode(mutable, "D") * Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D * }) * * const components = Graph.connectedComponents(graph) * console.log(components) // [[0, 1], [2, 3]] * ``` * * @since 3.18.0 * @category algorithms */ export const connectedComponents = graph => { const visited = new Set(); const components = []; for (const startNode of graph.nodes.keys()) { if (!visited.has(startNode)) { // DFS to find all nodes in this component const component = []; const stack = [startNode]; while (stack.length > 0) { const current = stack.pop(); if (!visited.has(current)) { visited.add(current); component.push(current); // Add all unvisited neighbors to stack const nodeNeighbors = getUndirectedNeighbors(graph, current); for (const neighbor of nodeNeighbors) { if (!visited.has(neighbor)) { stack.push(neighbor); } } } } components.push(component); } } return components; }; /** * Find strongly connected components in a directed graph using Kosaraju's algorithm. * Each SCC is represented as an array of node indices. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C * }) * * const sccs = Graph.stronglyConnectedComponents(graph) * console.log(sccs) // [[0, 1, 2]] * ``` * * @since 3.18.0 * @category algorithms */ export const stronglyConnectedComponents = graph => { const visited = new Set(); const finishOrder = []; for (const startNode of graph.nodes.keys()) { if (visited.has(startNode)) { continue; } const stack = [[startNode, [], 0, true]]; while (stack.length > 0) { const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1]; if (isFirstVisit) { if (visited.has(node)) { stack.pop(); continue; } visited.add(node); const nodeNeighborsList = neighbors(graph, node); stack[stack.length - 1] = [node, nodeNeighborsList, 0, false]; continue; } // Process next neighbor if (neighborIndex < nodeNeighbors.length) { const neighbor = nodeNeighbors[neighborIndex]; stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false]; if (!visited.has(neighbor)) { stack.push([neighbor, [], 0, true]); } } else { // Done with this node - add to finish order (post-order) finishOrder.push(node); stack.pop(); } } } // Step 2: Stack-safe DFS on transpose graph in reverse finish order visited.clear(); const sccs = []; for (let i = finishOrder.length - 1; i >= 0; i--) { const startNode = finishOrder[i]; if (visited.has(startNode)) { continue; } const scc = []; const stack = [startNode]; while (stack.length > 0) { const node = stack.pop(); if (visited.has(node)) { continue; } visited.add(node); scc.push(node); // Use reverse adjacency (transpose graph) const reverseAdjacency = getMapSafe(graph.reverseAdjacency, node); if (Option.isSome(reverseAdjacency)) { for (const edgeIndex of reverseAdjacency.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { const predecessor = edge.value.source; if (!visited.has(predecessor)) { stack.push(predecessor); } } } } } sccs.push(scc); } return sccs; }; /** * Find the shortest path between two nodes using Dijkstra's algorithm. * * Dijkstra's algorithm works with non-negative edge weights and finds the shortest * path from a source node to a target node in O((V + E) log V) time complexity. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 5) * Graph.addEdge(mutable, a, c, 10) * Graph.addEdge(mutable, b, c, 2) * }) * * const result = Graph.dijkstra(graph, 0, 2, (edgeData) => edgeData) * if (Option.isSome(result)) { * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C * console.log(result.value.distance) // 7 - total distance * } * ``` * * @since 3.18.0 * @category algorithms */ export const dijkstra = (graph, source, target, edgeWeight) => { // Validate that source and target nodes exist if (!graph.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`); } if (!graph.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`); } // Early return if source equals target if (source === target) { return Option.some({ path: [source], distance: 0, edgeWeights: [] }); } // Distance tracking and priority queue simulation const distances = new Map(); const previous = new Map(); const visited = new Set(); // Initialize distances // Iterate directly over node keys for (const node of graph.nodes.keys()) { distances.set(node, node === source ? 0 : Infinity); previous.set(node, null); } // Simple priority queue using array (can be optimized with proper heap) const priorityQueue = [{ node: source, distance: 0 }]; while (priorityQueue.length > 0) { // Find minimum distance node (priority queue extract-min) let minIndex = 0; for (let i = 1; i < priorityQueue.length; i++) { if (priorityQueue[i].distance < priorityQueue[minIndex].distance) { minIndex = i; } } const current = priorityQueue.splice(minIndex, 1)[0]; const currentNode = current.node; // Skip if already visited (can happen with duplicate entries) if (visited.has(currentNode)) { continue; } visited.add(currentNode); // Early termination if we reached the target if (currentNode === target) { break; } // Get current distance const currentDistance = distances.get(currentNode); // Examine all outgoing edges const adjacencyList = getMapSafe(graph.adjacency, currentNode); if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { const neighbor = edge.value.target; const weight = edgeWeight(edge.value.data); // Validate non-negative weights if (weight < 0) { throw new Error(`Dijkstra's algorithm requires non-negative edge weights, found ${weight}`); } const newDistance = currentDistance + weight; const neighborDistance = distances.get(neighbor); // Relaxation step if (newDistance < neighborDistance) { distances.set(neighbor, newDistance); previous.set(neighbor, { node: currentNode, edgeData: edge.value.data }); // Add to priority queue if not visited if (!visited.has(neighbor)) { priorityQueue.push({ node: neighbor, distance: newDistance }); } } } } } } // Check if target is reachable const targetDistance = distances.get(target); if (targetDistance === Infinity) { return Option.none(); // No path exists } // Reconstruct path const path = []; const edgeWeights = []; let currentNode = target; while (currentNode !== null) { path.unshift(currentNode); const prev = previous.get(currentNode); if (prev !== null) { edgeWeights.unshift(prev.edgeData); currentNode = prev.node; } else { currentNode = null; } } return Option.some({ path, distance: targetDistance, edgeWeights }); }; /** * Find shortest paths between all pairs of nodes using Floyd-Warshall algorithm. * * Floyd-Warshall algorithm computes shortest paths between all pairs of nodes in O(V³) time. * It can handle negative edge weights and detect negative cycles. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 3) * Graph.addEdge(mutable, b, c, 2) * Graph.addEdge(mutable, a, c, 7) * }) * * const result = Graph.floydWarshall(graph, (edgeData) => edgeData) * const distanceAToC = result.distances.get(0)?.get(2) // 5 (A->B->C) * const pathAToC = result.paths.get(0)?.get(2) // [0, 1, 2] * ``` * * @since 3.18.0 * @category algorithms */ export const floydWarshall = (graph, edgeWeight) => { // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration) const allNodes = Array.from(graph.nodes.keys()); // Initialize distance matrix const dist = new Map(); const next = new Map(); const edgeMatrix = new Map(); // Initialize with infinity for all pairs for (const i of allNodes) { dist.set(i, new Map()); next.set(i, new Map()); edgeMatrix.set(i, new Map()); for (const j of allNodes) { dist.get(i).set(j, i === j ? 0 : Infinity); next.get(i).set(j, null); edgeMatrix.get(i).set(j, null); } } // Set edge weights for (const [, edgeData] of graph.edges) { const weight = edgeWeight(edgeData.data); const i = edgeData.source; const j = edgeData.target; // Use minimum weight if multiple edges exist const currentWeight = dist.get(i).get(j); if (weight < currentWeight) { dist.get(i).set(j, weight); next.get(i).set(j, j); edgeMatrix.get(i).set(j, edgeData.data); } } // Floyd-Warshall main loop for (const k of allNodes) { for (const i of allNodes) { for (const j of allNodes) { const distIK = dist.get(i).get(k); const distKJ = dist.get(k).get(j); const distIJ = dist.get(i).get(j); if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) { dist.get(i).set(j, distIK + distKJ); next.get(i).set(j, next.get(i).get(k)); } } } } // Check for negative cycles for (const i of allNodes) { if (dist.get(i).get(i) < 0) { throw new Error(`Negative cycle detected involving node ${i}`); } } // Build result paths and edge weights const paths = new Map(); const resultEdgeWeights = new Map(); for (const i of allNodes) { paths.set(i, new Map()); resultEdgeWeights.set(i, new Map()); for (const j of allNodes) { if (i === j) { paths.get(i).set(j, [i]); resultEdgeWeights.get(i).set(j, []); } else if (dist.get(i).get(j) === Infinity) { paths.get(i).set(j, null); resultEdgeWeights.get(i).set(j, []); } else { // Reconstruct path iteratively const path = []; const weights = []; let current = i; path.push(current); while (current !== j) { const nextNode = next.get(current).get(j); if (nextNode === null) break; const edgeData = edgeMatrix.get(current).get(nextNode); if (edgeData !== null) { weights.push(edgeData); } current = nextNode; path.push(current); } paths.get(i).set(j, path); resultEdgeWeights.get(i).set(j, weights); } } } return { distances: dist, paths, edgeWeights: resultEdgeWeights }; }; /** * Find the shortest path between two nodes using A* pathfinding algorithm. * * A* is an extension of Dijkstra's algorithm that uses a heuristic function to guide * the search towards the target, potentially finding paths faster than Dijkstra's. * The heuristic must be admissible (never overestimate the actual cost). * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed<{x: number, y: number}, number>((mutable) => { * const a = Graph.addNode(mutable, {x: 0, y: 0}) * const b = Graph.addNode(mutable, {x: 1, y: 0}) * const c = Graph.addNode(mutable, {x: 2, y: 0}) * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Manhattan distance heuristic * const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) => * Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y) * * const result = Graph.astar(graph, 0, 2, (edgeData) => edgeData, heuristic) * if (Option.isSome(result)) { * console.log(result.value.path) // [0, 1, 2] - shortest path * console.log(result.value.distance) // 2 - total distance * } * ``` * * @since 3.18.0 * @category algorithms */ export const astar = (graph, source, target, edgeWeight, heuristic) => { // Validate that source and target nodes exist if (!graph.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`); } if (!graph.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`); } // Early return if source equals target if (source === target) { return Option.some({ path: [source], distance: 0, edgeWeights: [] }); } // Get target node data for heuristic calculations const targetNodeData = getMapSafe(graph.nodes, target); if (Option.isNone(targetNodeData)) { throw new Error(`Target node ${target} data not found`); } // Distance tracking (g-score) and f-score (g + h) const gScore = new Map(); const fScore = new Map(); const previous = new Map(); const visited = new Set(); // Initialize scores // Iterate directly over node keys for (const node of graph.nodes.keys()) { gScore.set(node, node === source ? 0 : Infinity); fScore.set(node, Infinity); previous.set(node, null); } // Calculate initial f-score for source const sourceNodeData = getMapSafe(graph.nodes, source); if (Option.isSome(sourceNodeData)) { const h = heuristic(sourceNodeData.value, targetNodeData.value); fScore.set(source, h); } // Priority queue using f-score (total estimated cost) const openSet = [{ node: source, fScore: fScore.get(source) }]; while (openSet.length > 0) { // Find node with lowest f-score let minIndex = 0; for (let i = 1; i < openSet.length; i++) { if (openSet[i].fScore < openSet[minIndex].fScore) { minIndex = i; } } const current = openSet.splice(minIndex, 1)[0]; const currentNode = current.node; // Skip if already visited if (visited.has(currentNode)) { continue; } visited.add(currentNode); // Early termination if we reached the target if (currentNode === target) { break; } // Get current g-score const currentGScore = gScore.get(currentNode); // Examine all outgoing edges const adjacencyList = getMapSafe(graph.adjacency, currentNode); if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { const neighbor = edge.value.target; const weight = edgeWeight(edge.value.data); // Validate non-negative weights if (weight < 0) { throw new Error(`A* algorithm requires non-negative edge weights, found ${weight}`); } const tentativeGScore = currentGScore + weight; const neighborGScore = gScore.get(neighbor); // If this path to neighbor is better than any previous one if (tentativeGScore < neighborGScore) { // Update g-score and previous gScore.set(neighbor, tentativeGScore); previous.set(neighbor, { node: currentNode, edgeData: edge.value.data }); // Calculate f-score using heuristic const neighborNodeData = getMapSafe(graph.nodes, neighbor); if (Option.isSome(neighborNodeData)) { const h = heuristic(neighborNodeData.value, targetNodeData.value); const f = tentativeGScore + h; fScore.set(neighbor, f); // Add to open set if not visited if (!visited.has(neighbor)) { openSet.push({ node: neighbor, fScore: f }); } } } } } } } // Check if target is reachable const targetGScore = gScore.get(target); if (targetGScore === Infinity) { return Option.none(); // No path exists } // Reconstruct path const path = []; const edgeWeights = []; let currentNode = target; while (currentNode !== null) { path.unshift(currentNode); const prev = previous.get(currentNode); if (prev !== null) { edgeWeights.unshift(prev.edgeData); currentNode = prev.node; } else { currentNode = null; } } return Option.some({ path, distance: targetGScore, edgeWeights }); }; /** * Find the shortest path between two nodes using Bellman-Ford algorithm. * * Bellman-Ford algorithm can handle negative edge weights and detects negative cycles. * It has O(VE) time complexity, slower than Dijkstra's but more versatile. * Returns Option.none() if a negative cycle is detected that affects the path. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, -1) // Negative weight allowed * Graph.addEdge(mutable, b, c, 3) * Graph.addEdge(mutable, a, c, 5) * }) * * const result = Graph.bellmanFord(graph, 0, 2, (edgeData) => edgeData) * if (Option.isSome(result)) { * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C * console.log(result.value.distance) // 2 - total distance * } * ``` * * @since 3.18.0 * @category algorithms */ export const bellmanFord = (graph, source, target, edgeWeight) => { // Validate that source and target nodes exist if (!graph.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`); } if (!graph.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`); } // Early return if source equals target if (source === target) { return Option.some({ path: [source], distance: 0, edgeWeights: [] }); } // Initialize distances and predecessors const distances = new Map(); const previous = new Map(); // Iterate directly over node keys for (const node of graph.nodes.keys()) { distances.set(node, node === source ? 0 : Infinity); previous.set(node, null); } // Collect all edges for relaxation const edges = []; for (const [, edgeData] of graph.edges) { const weight = edgeWeight(edgeData.data); edges.push({ source: edgeData.source, target: edgeData.target, weight, edgeData: edgeData.data }); } // Relax edges up to V-1 times const nodeCount = graph.nodes.size; for (let i = 0; i < nodeCount - 1; i++) { let hasUpdate = false; for (const edge of edges) { const sourceDistance = distances.get(edge.source); const targetDistance = distances.get(edge.target); // Relaxation step if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { distances.set(edge.target, sourceDistance + edge.weight); previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData }); hasUpdate = true; } } // Early termination if no updates if (!hasUpdate) { break; } } // Check for negative cycles for (const edge of edges) { const sourceDistance = distances.get(edge.source); const targetDistance = distances.get(edge.target); if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { // Negative cycle detected - check if it affects the path to target const affectedNodes = new Set(); const queue = [edge.target]; while (queue.length > 0) { const node = queue.shift(); if (affectedNodes.has(node)) continue; affectedNodes.add(node); // Add all nodes reachable from this node const adjacencyList = getMapSafe(graph.adjacency, node); if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex); if (Option.isSome(edge)) { queue.push(edge.value.target); } } } } // If target is affected by negative cycle, return null if (affectedNodes.has(target)) { return Option.none(); } } } // Check if target is reachable const targetDistance = distances.get(target); if (targetDistance === Infinity) { return Option.none(); // No path exists } // Reconstruct path const path = []; const edgeWeights = []; let currentNode = target; while (currentNode !== null) { path.unshift(currentNode); const prev = previous.get(currentNode); if (prev !== null) { edgeWeights.unshift(prev.edgeData); currentNode = prev.node; } else { currentNode = null; } } return Option.some({ path, distance: targetDistance, edgeWeights }); }; /** * Concrete class for iterables that produce [NodeIndex, NodeData] tuples. * * This class provides a common abstraction for all iterables that return node data, * including traversal iterators (DFS, BFS, etc.) and element iterators (nodes, externals). * It uses a mapEntry function pattern for flexible iteration and transformation. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * // Both traversal and element iterators return NodeWalker * const dfsNodes: Graph.NodeWalker = Graph.dfs(graph, { startNodes: [0] }) * const allNodes: Graph.NodeWalker = Graph.nodes(graph) * * // Common interface for working with node iterables * function processNodes(nodeIterable: Graph.NodeWalker): Array { * return Array.from(Graph.indices(nodeIterable)) * } * * // Access node data using values() or entries() * const nodeData = Array.from(Graph.values(dfsNodes)) // ["A", "B"] * const nodeEntries = Array.from(Graph.entries(allNodes)) // [[0, "A"], [1, "B"]] * ``` * * @since 3.18.0 * @category models */ export class Walker { // @ts-ignore [Symbol.iterator]; /** * Visits each element and maps it to a value using the provided function. * * Takes a function that receives the index and data, * and returns an iterable of the mapped values. Skips elements that * no longer exist in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * * // Map to just the node data * const values = Array.from(dfs.visit((index, data) => data)) * console.log(values) // ["A", "B"] * * // Map to custom objects * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] * ``` * * @since 3.18.0 * @category iterators */ visit; constructor( /** * Visits each element and maps it to a value using the provided function. * * Takes a function that receives the index and data, * and returns an iterable of the mapped values. Skips elements that * no longer exist in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * * // Map to just the node data * const values = Array.from(dfs.visit((index, data) => data)) * console.log(values) // ["A", "B"] * * // Map to custom objects * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] * ``` * * @since 3.18.0 * @category iterators */ visit) { this.visit = visit; this[Symbol.iterator] = visit((index, data) => [index, data])[Symbol.iterator]; } } /** * Returns an iterator over the indices in the walker. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * const indices = Array.from(Graph.indices(dfs)) * console.log(indices) // [0, 1] * ``` * * @since 3.18.0 * @category utilities */ export const indices = walker => walker.visit((index, _) => index); /** * Returns an iterator over the values (data) in the walker. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * const values = Array.from(Graph.values(dfs)) * console.log(values) // ["A", "B"] * ``` * * @since 3.18.0 * @category utilities */ export const values = walker => walker.visit((_, data) => data); /** * Returns an iterator over [index, data] entries in the walker. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * const entries = Array.from(Graph.entries(dfs)) * console.log(entries) // [[0, "A"], [1, "B"]] * ``` * * @since 3.18.0 * @category utilities */ export const entries = walker => walker.visit((index, data) => [index, data]); /** * Creates a new DFS iterator with optional configuration. * * The iterator maintains a stack of nodes to visit and tracks discovered nodes. * It provides lazy evaluation of the depth-first search. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Start from a specific node * const dfs1 = Graph.dfs(graph, { startNodes: [0] }) * for (const nodeIndex of Graph.indices(dfs1)) { * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2 * } * * // Empty iterator (no starting nodes) * const dfs2 = Graph.dfs(graph) * // Can be used programmatically * ``` * * @since 3.18.0 * @category iterators */ export const dfs = (graph, config = {}) => { const startNodes = config.startNodes ?? []; const direction = config.direction ?? "outgoing"; // Validate that all start nodes exist for (const nodeIndex of startNodes) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Start node ${nodeIndex} does not exist`); } } return new Walker(f => ({ [Symbol.iterator]: () => { const stack = [...startNodes]; const discovered = new Set(); const nextMapped = () => { while (stack.length > 0) { const current = stack.pop(); if (discovered.has(current)) { continue; } discovered.add(current); const nodeDataOption = getMapSafe(graph.nodes, current); if (Option.isNone(nodeDataOption)) { continue; } const neighbors = neighborsDirected(graph, current, direction); for (let i = neighbors.length - 1; i >= 0; i--) { const neighbor = neighbors[i]; if (!discovered.has(neighbor)) { stack.push(neighbor); } } return { done: false, value: f(current, nodeDataOption.value) }; } return { done: true, value: undefined }; }; return { next: nextMapped }; } })); }; /** * Creates a new BFS iterator with optional configuration. * * The iterator maintains a queue of nodes to visit and tracks discovered nodes. * It provides lazy evaluation of the breadth-first search. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Start from a specific node * const bfs1 = Graph.bfs(graph, { startNodes: [0] }) * for (const nodeIndex of Graph.indices(bfs1)) { * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2 * } * * // Empty iterator (no starting nodes) * const bfs2 = Graph.bfs(graph) * // Can be used programmatically * ``` * * @since 3.18.0 * @category iterators */ export const bfs = (graph, config = {}) => { const startNodes = config.startNodes ?? []; const direction = config.direction ?? "outgoing"; // Validate that all start nodes exist for (const nodeIndex of startNodes) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Start node ${nodeIndex} does not exist`); } } return new Walker(f => ({ [Symbol.iterator]: () => { const queue = [...startNodes]; const discovered = new Set(); const nextMapped = () => { while (queue.length > 0) { const current = queue.shift(); if (!discovered.has(current)) { discovered.add(current); const neighbors = neighborsDirected(graph, current, direction); for (const neighbor of neighbors) { if (!discovered.has(neighbor)) { queue.push(neighbor); } } const nodeData = getNode(graph, current); if (Option.isSome(nodeData)) { return { done: false, value: f(current, nodeData.value) }; } return nextMapped(); } } return { done: true, value: undefined }; }; return { next: nextMapped }; } })); }; /** * Creates a new topological sort iterator with optional configuration. * * The iterator uses Kahn's algorithm to lazily produce nodes in topological order. * Throws an error if the graph contains cycles. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Standard topological sort * const topo1 = Graph.topo(graph) * for (const nodeIndex of Graph.indices(topo1)) { * console.log(nodeIndex) // 0, 1, 2 (topological order) * } * * // With initial nodes * const topo2 = Graph.topo(graph, { initials: [0] }) * * // Throws error for cyclic graph * const cyclicGraph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, a, 2) // Creates cycle * }) * * try { * Graph.topo(cyclicGraph) // Throws: "Cannot perform topological sort on cyclic graph" * } catch (error) { * console.log((error as Error).message) * } * ``` * * @since 3.18.0 * @category iterators */ export const topo = (graph, config = {}) => { // Check if graph is acyclic first if (!isAcyclic(graph)) { throw new Error("Cannot perform topological sort on cyclic graph"); } const initials = config.initials ?? []; // Validate that all initial nodes exist for (const nodeIndex of initials) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Initial node ${nodeIndex} does not exist`); } } return new Walker(f => ({ [Symbol.iterator]: () => { const inDegree = new Map(); const remaining = new Set(); const queue = [...initials]; // Initialize in-degree counts for (const [nodeIndex] of graph.nodes) { inDegree.set(nodeIndex, 0); remaining.add(nodeIndex); } // Calculate in-degrees for (const [, edgeData] of graph.edges) { const currentInDegree = inDegree.get(edgeData.target) || 0; inDegree.set(edgeData.target, currentInDegree + 1); } // Add nodes with zero in-degree to queue if no initials provided if (initials.length === 0) { for (const [nodeIndex, degree] of inDegree) { if (degree === 0) { queue.push(nodeIndex); } } } const nextMapped = () => { while (queue.length > 0) { const current = queue.shift(); if (remaining.has(current)) { remaining.delete(current); // Process outgoing edges, reducing in-degree of targets const neighbors = neighborsDirected(graph, current, "outgoing"); for (const neighbor of neighbors) { if (remaining.has(neighbor)) { const currentInDegree = inDegree.get(neighbor) || 0; const newInDegree = currentInDegree - 1; inDegree.set(neighbor, newInDegree); // If in-degree becomes 0, add to queue if (newInDegree === 0) { queue.push(neighbor); } } } const nodeData = getNode(graph, current); if (Option.isSome(nodeData)) { return { done: false, value: f(current, nodeData.value) }; } return nextMapped(); } } return { done: true, value: undefined }; }; return { next: nextMapped }; } })); }; /** * Creates a new DFS postorder iterator with optional configuration. * * The iterator maintains a stack with visit state tracking and emits nodes * in postorder (after all descendants have been processed). Essential for * dependency resolution and tree destruction algorithms. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const root = Graph.addNode(mutable, "root") * const child1 = Graph.addNode(mutable, "child1") * const child2 = Graph.addNode(mutable, "child2") * Graph.addEdge(mutable, root, child1, 1) * Graph.addEdge(mutable, root, child2, 1) * }) * * // Postorder: children before parents * const postOrder = Graph.dfsPostOrder(graph, { startNodes: [0] }) * for (const node of postOrder) { * console.log(node) // 1, 2, 0 * } * ``` * * @since 3.18.0 * @category iterators */ export const dfsPostOrder = (graph, config = {}) => { const startNodes = config.startNodes ?? []; const direction = config.direction ?? "outgoing"; // Validate that all start nodes exist for (const nodeIndex of startNodes) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Start node ${nodeIndex} does not exist`); } } return new Walker(f => ({ [Symbol.iterator]: () => { const stack = []; const discovered = new Set(); const finished = new Set(); // Initialize stack with start nodes for (let i = startNodes.length - 1; i >= 0; i--) { stack.push({ node: startNodes[i], visitedChildren: false }); } const nextMapped = () => { while (stack.length > 0) { const current = stack[stack.length - 1]; if (!discovered.has(current.node)) { discovered.add(current.node); current.visitedChildren = false; } if (!current.visitedChildren) { current.visitedChildren = true; const neighbors = neighborsDirected(graph, current.node, direction); for (let i = neighbors.length - 1; i >= 0; i--) { const neighbor = neighbors[i]; if (!discovered.has(neighbor) && !finished.has(neighbor)) { stack.push({ node: neighbor, visitedChildren: false }); } } } else { const nodeToEmit = stack.pop().node; if (!finished.has(nodeToEmit)) { finished.add(nodeToEmit); const nodeData = getNode(graph, nodeToEmit); if (Option.isSome(nodeData)) { return { done: false, value: f(nodeToEmit, nodeData.value) }; } return nextMapped(); } } } return { done: true, value: undefined }; }; return { next: nextMapped }; } })); }; /** * Creates an iterator over all node indices in the graph. * * The iterator produces node indices in the order they were added to the graph. * This provides access to all nodes regardless of connectivity. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * }) * * const indices = Array.from(Graph.indices(Graph.nodes(graph))) * console.log(indices) // [0, 1, 2] * ``` * * @since 3.18.0 * @category iterators */ export const nodes = graph => new Walker(f => ({ [Symbol.iterator]() { const nodeMap = graph.nodes; const iterator = nodeMap.entries(); return { next() { const result = iterator.next(); if (result.done) { return { done: true, value: undefined }; } const [nodeIndex, nodeData] = result.value; return { done: false, value: f(nodeIndex, nodeData) }; } }; } })); /** * Creates an iterator over all edge indices in the graph. * * The iterator produces edge indices in the order they were added to the graph. * This provides access to all edges regardless of connectivity. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 2) * }) * * const indices = Array.from(Graph.indices(Graph.edges(graph))) * console.log(indices) // [0, 1] * ``` * * @since 3.18.0 * @category iterators */ export const edges = graph => new Walker(f => ({ [Symbol.iterator]() { const edgeMap = graph.edges; const iterator = edgeMap.entries(); return { next() { const result = iterator.next(); if (result.done) { return { done: true, value: undefined }; } const [edgeIndex, edgeData] = result.value; return { done: false, value: f(edgeIndex, edgeData) }; } }; } })); /** * Creates an iterator over external nodes (nodes without edges in specified direction). * * External nodes are nodes that have no outgoing edges (direction="outgoing") or * no incoming edges (direction="incoming"). These are useful for finding * sources, sinks, or isolated nodes. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const source = Graph.addNode(mutable, "source") // 0 - no incoming * const middle = Graph.addNode(mutable, "middle") // 1 - has both * const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing * const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges * * Graph.addEdge(mutable, source, middle, 1) * Graph.addEdge(mutable, middle, sink, 2) * }) * * // Nodes with no outgoing edges (sinks + isolated) * const sinks = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) * console.log(sinks) // [2, 3] * * // Nodes with no incoming edges (sources + isolated) * const sources = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) * console.log(sources) // [0, 3] * ``` * * @since 3.18.0 * @category iterators */ export const externals = (graph, config = {}) => { const direction = config.direction ?? "outgoing"; return new Walker(f => ({ [Symbol.iterator]: () => { const nodeMap = graph.nodes; const adjacencyMap = direction === "incoming" ? graph.reverseAdjacency : graph.adjacency; const nodeIterator = nodeMap.entries(); const nextMapped = () => { let current = nodeIterator.next(); while (!current.done) { const [nodeIndex, nodeData] = current.value; const adjacencyList = getMapSafe(adjacencyMap, nodeIndex); // Node is external if it has no edges in the specified direction if (Option.isNone(adjacencyList) || adjacencyList.value.length === 0) { return { done: false, value: f(nodeIndex, nodeData) }; } current = nodeIterator.next(); } return { done: true, value: undefined }; }; return { next: nextMapped }; } })); }; //# sourceMappingURL=Graph.js.map