effect
Version:
The missing standard library for TypeScript, for writing production-grade software.
1,817 lines (1,682 loc) • 107 kB
text/typescript
/**
* @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 type { Inspectable } from "./Inspectable.js"
import { format, NodeInspectSymbol } from "./Inspectable.js"
import * as Option from "./Option.js"
import type { Pipeable } from "./Pipeable.js"
import { pipeArguments } from "./Pipeable.js"
import type { Mutable } from "./Types.js"
/**
* Unique identifier for Graph instances.
*
* @since 3.18.0
* @category symbol
*/
export const TypeId: "~effect/Graph" = "~effect/Graph" as const
/**
* Type identifier for Graph instances.
*
* @since 3.18.0
* @category symbol
*/
export type TypeId = typeof TypeId
/**
* Node index for node identification using plain numbers.
*
* @since 3.18.0
* @category models
*/
export type NodeIndex = number
/**
* Edge index for edge identification using plain numbers.
*
* @since 3.18.0
* @category models
*/
export type EdgeIndex = number
/**
* Edge data containing source, target, and user data.
*
* @since 3.18.0
* @category models
*/
export class Edge<E> extends Data.Class<{
readonly source: NodeIndex
readonly target: NodeIndex
readonly data: E
}> {}
/**
* Graph type for distinguishing directed and undirected graphs.
*
* @since 3.18.0
* @category models
*/
export type Kind = "directed" | "undirected"
/**
* Graph prototype interface.
*
* @since 3.18.0
* @category models
*/
export interface Proto<out N, out E> extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable {
readonly [TypeId]: TypeId
readonly nodes: Map<NodeIndex, N>
readonly edges: Map<EdgeIndex, Edge<E>>
readonly adjacency: Map<NodeIndex, Array<EdgeIndex>>
readonly reverseAdjacency: Map<NodeIndex, Array<EdgeIndex>>
nextNodeIndex: NodeIndex
nextEdgeIndex: EdgeIndex
isAcyclic: Option.Option<boolean>
}
/**
* Immutable graph interface.
*
* @since 3.18.0
* @category models
*/
export interface Graph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
readonly type: T
readonly mutable: false
}
/**
* Mutable graph interface.
*
* @since 3.18.0
* @category models
*/
export interface MutableGraph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
readonly type: T
readonly mutable: true
}
/**
* Directed graph type alias.
*
* @since 3.18.0
* @category models
*/
export type DirectedGraph<N, E> = Graph<N, E, "directed">
/**
* Undirected graph type alias.
*
* @since 3.18.0
* @category models
*/
export type UndirectedGraph<N, E> = Graph<N, E, "undirected">
/**
* Mutable directed graph type alias.
*
* @since 3.18.0
* @category models
*/
export type MutableDirectedGraph<N, E> = MutableGraph<N, E, "directed">
/**
* Mutable undirected graph type alias.
*
* @since 3.18.0
* @category models
*/
export type MutableUndirectedGraph<N, E> = MutableGraph<N, E, "undirected">
// =============================================================================
// Proto Objects
// =============================================================================
/** @internal */
const ProtoGraph = {
[TypeId]: TypeId,
[Symbol.iterator](this: Graph<any, any>) {
return this.nodes[Symbol.iterator]()
},
[NodeInspectSymbol](this: Graph<any, any>) {
return this.toJSON()
},
[Equal.symbol](this: Graph<any, any>, that: Equal.Equal): boolean {
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](this: Graph<any, any>): number {
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(this: Graph<any, any>) {
return {
_id: "Graph",
nodeCount: this.nodes.size,
edgeCount: this.edges.size,
type: this.type
}
},
toString(this: Graph<any, any>) {
return format(this)
},
pipe() {
return pipeArguments(this, arguments)
}
}
// =============================================================================
// Errors
// =============================================================================
/**
* Error thrown when a graph operation fails.
*
* @since 3.18.0
* @category errors
*/
export class GraphError extends Data.TaggedError("GraphError")<{
readonly message: string
}> {}
/** @internal */
const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` })
// =============================================================================
// Constructors
// =============================================================================
/** @internal */
export const isGraph = (u: unknown): u is Graph<unknown, unknown> => 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<string, string>((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 = <N, E>(mutate?: (mutable: MutableDirectedGraph<N, E>) => void): DirectedGraph<N, E> => {
const graph: Mutable<DirectedGraph<N, E>> = 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 as DirectedGraph<N, E>)
mutate(mutable as MutableDirectedGraph<N, E>)
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<string, string>((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 = <N, E>(mutate?: (mutable: MutableUndirectedGraph<N, E>) => void): UndirectedGraph<N, E> => {
const graph: Mutable<UndirectedGraph<N, E>> = 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 as MutableUndirectedGraph<N, E>)
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<string, number>()
* const mutable = Graph.beginMutation(graph)
* // Now mutable can be safely modified without affecting original graph
* ```
*
* @since 3.18.0
* @category mutations
*/
export const beginMutation = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T>
): MutableGraph<N, E, T> => {
// Copy adjacency maps with deep cloned arrays
const adjacency = new Map<NodeIndex, Array<EdgeIndex>>()
const reverseAdjacency = new Map<NodeIndex, Array<EdgeIndex>>()
for (const [nodeIndex, edges] of graph.adjacency) {
adjacency.set(nodeIndex, [...edges])
}
for (const [nodeIndex, edges] of graph.reverseAdjacency) {
reverseAdjacency.set(nodeIndex, [...edges])
}
const mutable: Mutable<MutableGraph<N, E, T>> = 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<string, number>()
* const mutable = Graph.beginMutation(graph)
* // ... perform mutations on mutable ...
* const newGraph = Graph.endMutation(mutable)
* ```
*
* @since 3.18.0
* @category mutations
*/
export const endMutation = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>
): Graph<N, E, T> => {
const graph: Mutable<Graph<N, E, T>> = 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<string, number>()
* 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: {
/**
* Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const graph = Graph.directed<string, number>()
* const newGraph = Graph.mutate(graph, (mutable) => {
* // Safe mutations go here
* // mutable gets automatically converted back to immutable
* })
* ```
*
* @since 3.18.0
* @category mutations
*/
<N, E, T extends Kind = "directed">(f: (mutable: MutableGraph<N, E, T>) => void): (graph: Graph<N, E, T>) => Graph<N, E, T>
/**
* Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const graph = Graph.directed<string, number>()
* const newGraph = Graph.mutate(graph, (mutable) => {
* // Safe mutations go here
* // mutable gets automatically converted back to immutable
* })
* ```
*
* @since 3.18.0
* @category mutations
*/
<N, E, T extends Kind = "directed">(graph: Graph<N, E, T>, f: (mutable: MutableGraph<N, E, T>) => void): Graph<N, E, T>
} = dual(2, <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T>,
f: (mutable: MutableGraph<N, E, T>) => void
): Graph<N, E, T> => {
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
data: N
): NodeIndex => {
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
nodeIndex: NodeIndex
): Option.Option<N> => graph.nodes.has(nodeIndex) ? Option.some(graph.nodes.get(nodeIndex)!) : Option.none()
/**
* Checks if a node with the given index exists in the graph.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const graph = Graph.mutate(Graph.directed<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
nodeIndex: NodeIndex
): boolean => graph.nodes.has(nodeIndex)
/**
* Returns the number of nodes in the graph.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const emptyGraph = Graph.directed<string, number>()
* 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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>
): number => 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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
predicate: (data: N) => boolean
): Option.Option<NodeIndex> => {
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
predicate: (data: N) => boolean
): Array<NodeIndex> => {
const results: Array<NodeIndex> = []
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
): Option.Option<EdgeIndex> => {
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
): Array<EdgeIndex> => {
const results: Array<EdgeIndex> = []
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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
index: NodeIndex,
f: (data: N) => N
): void => {
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
edgeIndex: EdgeIndex,
f: (data: E) => E
): void => {
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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
f: (data: N) => N
): void => {
// 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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
f: (data: E) => E
): void => {
// 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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>
): void => {
// 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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
f: (data: N) => Option.Option<N>
): void => {
const nodesToRemove: Array<NodeIndex> = []
// 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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
f: (data: E) => Option.Option<E>
): void => {
const edgesToRemove: Array<EdgeIndex> = []
// 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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
predicate: (data: N) => boolean
): void => {
const nodesToRemove: Array<NodeIndex> = []
// 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<string, number>((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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
predicate: (data: E) => boolean
): void => {
const edgesToRemove: Array<EdgeIndex> = []
// 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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>
): void => {
// 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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>
): void => {
// 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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
source: NodeIndex,
target: NodeIndex,
data: E
): EdgeIndex => {
// Validate that both nodes exist
if (!mutable.nodes.has(source)) {
throw missingNode(source)
}
if (!mutable.nodes.has(target)) {
throw missingNode(target)
}
const edgeIndex = mutable.nextEdgeIndex
// Create edge data
const edgeData = new Edge({ source, target, data })
mutable.edges.set(edgeIndex, edgeData)
// Update adjacency lists
const sourceAdjacency = mutable.adjacency.get(source)
if (sourceAdjacency !== undefined) {
sourceAdjacency.push(edgeIndex)
}
const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
if (targetReverseAdjacency !== undefined) {
targetReverseAdjacency.push(edgeIndex)
}
// For undirected graphs, add reverse connections
if (mutable.type === "undirected") {
const targetAdjacency = mutable.adjacency.get(target)
if (targetAdjacency !== undefined) {
targetAdjacency.push(edgeIndex)
}
const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
if (sourceReverseAdjacency !== undefined) {
sourceReverseAdjacency.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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
nodeIndex: NodeIndex
): void => {
// 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: Array<EdgeIndex> = []
// Get outgoing edges
const outgoingEdges = mutable.adjacency.get(nodeIndex)
if (outgoingEdges !== undefined) {
for (const edge of outgoingEdges) {
edgesToRemove.push(edge)
}
}
// Get incoming edges
const incomingEdges = mutable.reverseAdjacency.get(nodeIndex)
if (incomingEdges !== undefined) {
for (const edge of incomingEdges) {
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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
edgeIndex: EdgeIndex
): void => {
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 = <N, E, T extends Kind = "directed">(
mutable: MutableGraph<N, E, T>,
edgeIndex: EdgeIndex
): boolean => {
// Get edge data
const edge = mutable.edges.get(edgeIndex)
if (edge === undefined) {
return false // Edge doesn't exist, no mutation occurred
}
const { source, target } = edge
// Remove from adjacency lists
const sourceAdjacency = mutable.adjacency.get(source)
if (sourceAdjacency !== undefined) {
const index = sourceAdjacency.indexOf(edgeIndex)
if (index !== -1) {
sourceAdjacency.splice(index, 1)
}
}
const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
if (targetReverseAdjacency !== undefined) {
const index = targetReverseAdjacency.indexOf(edgeIndex)
if (index !== -1) {
targetReverseAdjacency.splice(index, 1)
}
}
// For undirected graphs, remove reverse connections
if (mutable.type === "undirected") {
const targetAdjacency = mutable.adjacency.get(target)
if (targetAdjacency !== undefined) {
const index = targetAdjacency.indexOf(edgeIndex)
if (index !== -1) {
targetAdjacency.splice(index, 1)
}
}
const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
if (sourceReverseAdjacency !== undefined) {
const index = sourceReverseAdjacency.indexOf(edgeIndex)
if (index !== -1) {
sourceReverseAdjacency.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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
edgeIndex: EdgeIndex
): Option.Option<Edge<E>> => graph.edges.has(edgeIndex) ? Option.some(graph.edges.get(edgeIndex)!) : Option.none()
/**
* Checks if an edge exists between two nodes in the graph.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const graph = Graph.mutate(Graph.directed<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
source: NodeIndex,
target: NodeIndex
): boolean => {
const adjacencyList = graph.adjacency.get(source)
if (adjacencyList === undefined) {
return false
}
// Check if any edge in the adjacency list connects to the target
for (const edgeIndex of adjacencyList) {
const edge = graph.edges.get(edgeIndex)
if (edge !== undefined && edge.target === target) {
return true
}
}
return false
}
/**
* Returns the number of edges in the graph.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const emptyGraph = Graph.directed<string, number>()
* 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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>
): number => 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<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
nodeIndex: NodeIndex
): Array<NodeIndex> => {
// For undirected graphs, use the specialized helper that returns the other endpoint
if (graph.type === "undirected") {
return getUndirectedNeighbors(graph as any, nodeIndex)
}
const adjacencyList = graph.adjacency.get(nodeIndex)
if (adjacencyList === undefined) {
return []
}
const result: Array<NodeIndex> = []
for (const edgeIndex of adjacencyList) {
const edge = graph.edges.get(edgeIndex)
if (edge !== undefined) {
result.push(edge.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<string, string>((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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
nodeIndex: NodeIndex,
direction: Direction
): Array<NodeIndex> => {
const adjacencyMap = direction === "incoming"
? graph.reverseAdjacency
: graph.adjacency
const adjacencyList = adjacencyMap.get(nodeIndex)
if (adjacencyList === undefined) {
return []
}
const result: Array<NodeIndex> = []
for (const edgeIndex of adjacencyList) {
const edge = graph.edges.get(edgeIndex)
if (edge !== undefined) {
// For incoming direction, we want the source node instead of target
const neighborNode = direction === "incoming"
? edge.source
: edge.target
result.push(neighborNode)
}
}
return result
}
// =============================================================================
// GraphViz Export
// =============================================================================
/**
* Configuration options for GraphViz DOT format generation from graphs.
*
* @since 3.18.0
* @category models
*/
export interface GraphVizOptions<N, E> {
readonly nodeLabel?: (data: N) => string
readonly edgeLabel?: (data: E) => string
readonly graphName?: string
}
/**
* Exports a graph to GraphViz DOT format for visualization.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const graph = Graph.mutate(Graph.directed<string, number>(), (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 = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
options?: GraphVizOptions<N, E>
): string => {
const {
edgeLabel = (data: E) => String(data),
graphName = "G",
nodeLabel = (data: N) => String(data)
} = options ?? {}
const isDirected = graph.type === "directed"
const graphType = isDirected ? "digraph" : "graph"
const edgeOperator = isDirected ? "->" : "--"
const lines: Array<string> = []
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")
}
// =============================================================================
// Mermaid Export
// =============================================================================
/**
* Mermaid node shape types.
*
* @since 3.18.0
* @category models
*/
export type MermaidNodeShape =
| "rectangle"
| "rounded"
| "circle"
| "diamond"
| "hexagon"
| "stadium"
| "subroutine"
| "cylindrical"
/**
* Mermaid diagram direction types.
*
* @since 3.18.0
* @category models
*/
export type MermaidDirection = "TB" | "TD" | "BT" | "LR" | "RL"
/**
* Mermaid diagram type.
*
* @since 3.18.0
* @category models
*/
export type MermaidDiagramType = "flowchart" | "graph"
/**
* Configuration options for Mermaid diagram generation.
*
* @since 3.18.0
* @category models
*/
export interface MermaidOptions<N, E> {
readonly nodeLabel?: (data: N) => string
readonly edgeLabel?: (data: E) => string
readonly diagramType?: MermaidDiagramType
readonly direction?: MermaidDirection
readonly nodeShape?: (data: N) => MermaidNodeShape
}
/** @internal */
const escapeMermaidLabel = (label: string): string => {
// Escape special characters for Mermaid using HTML entity codes
// According to: https://mermaid.js.org/syntax/flowchart.html#special-characters-that-break-syntax
return label
.replace(/#/g, "#35;")
.replace(/"/g, "#quot;")
.replace(/</g, "#lt;")
.replace(/>/g, "#gt;")
.replace(/&/g, "#amp;")
.replace(/\[/g, "#91;")
.replace(/\]/g, "#93;")
.replace(/\{/g, "#123;")
.replace(/\}/g, "#125;")
.replace(/\(/g, "#40;")
.replace(/\)/g, "#41;")
.replace(/\|/g, "#124;")
.replace(/\\/g, "#92;")
.replace(/\n/g, "<br/>");
}
/** @internal */
const formatMermaidNode = (nodeId: string, label: string, shape: MermaidNodeShape): string => {
switch (shape) {
case "rectangle":
return `${nodeId}["${label}"]`
case "rounded":
return `${nodeId}("${label}")`
case "circle":
return `${nodeId}(("${label}"))`
case "diamond":
return `${nodeId}{"${label}"}`
case "hexagon":
return `${nodeId}{{"${label}"}}`
case "stadium":
return `${nodeId}(["${label}"])`
case "subroutine":
return `${nodeId}[["${label}"]]`
case "cylindrical":
return `${nodeId}[("${label}")]`
}
}
/**
* Exports a graph to Mermaid diagram format for visualization.
*
* @example
* ```ts
* import { Graph } from "effect"
*
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
* const app = Graph.addNode(mutable, "App")
* const db = Graph.addNode(mutable, "Database")
* const cache = Graph.addNode(mutable, "Cache")
* Graph.addEdge(mutable, app, db, 1)
* Graph.addEdge(mutable, app, cache, 2)
* })
*
* const mermaid = Graph.toMermaid(graph)
* console.log(mermaid)
* // flowchart TD
* // 0["App"]
* // 1["Database"]
* // 2["Cache"]
* // 0 -->|"1"| 1
* // 0 -->|"2"| 2
* ```
*
* @since 3.18.0
* @category utils
*/
export const toMermaid = <N, E, T extends Kind = "directed">(
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
options?: MermaidOptions<N, E>
): string => {
// Extract and validate options with defaults
const {
diagramType,
direction = "TD",
edgeLabel = (data: E) => String(data),
nodeLabel = (data: N) => String(data),
nodeShape = () => "rectangle" as const
} = options ?? {}
// Auto-detect diagram type if not specified
const finalDiagramType = diagramType ??