effect 3.18.5 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Graph.ts CHANGED
@@ -14,18 +14,6 @@ import type { Pipeable } from "./Pipeable.js"
14
14
  import { pipeArguments } from "./Pipeable.js"
15
15
  import type { Mutable } from "./Types.js"
16
16
 
17
- /**
18
- * Safely get a value from a Map, returning an Option.
19
- * Uses explicit key presence check with map.has() for better safety.
20
- * @internal
21
- */
22
- const getMapSafe = <K, V>(map: Map<K, V>, key: K): Option.Option<V> => {
23
- if (map.has(key)) {
24
- return Option.some(map.get(key)!)
25
- }
26
- return Option.none()
27
- }
28
-
29
17
  /**
30
18
  * Unique identifier for Graph instances.
31
19
  *
@@ -224,6 +212,23 @@ const ProtoGraph = {
224
212
  }
225
213
  }
226
214
 
215
+ // =============================================================================
216
+ // Errors
217
+ // =============================================================================
218
+
219
+ /**
220
+ * Error thrown when a graph operation fails.
221
+ *
222
+ * @since 3.18.0
223
+ * @category errors
224
+ */
225
+ export class GraphError extends Data.TaggedError("GraphError")<{
226
+ readonly message: string
227
+ }> {}
228
+
229
+ /** @internal */
230
+ const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` })
231
+
227
232
  // =============================================================================
228
233
  // Constructors
229
234
  // =============================================================================
@@ -523,7 +528,7 @@ export const addNode = <N, E, T extends Kind = "directed">(
523
528
  export const getNode = <N, E, T extends Kind = "directed">(
524
529
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
525
530
  nodeIndex: NodeIndex
526
- ): Option.Option<N> => getMapSafe(graph.nodes, nodeIndex)
531
+ ): Option.Option<N> => graph.nodes.has(nodeIndex) ? Option.some(graph.nodes.get(nodeIndex)!) : Option.none()
527
532
 
528
533
  /**
529
534
  * Checks if a node with the given index exists in the graph.
@@ -1174,10 +1179,10 @@ export const addEdge = <N, E, T extends Kind = "directed">(
1174
1179
  ): EdgeIndex => {
1175
1180
  // Validate that both nodes exist
1176
1181
  if (!mutable.nodes.has(source)) {
1177
- throw new Error(`Source node ${source} does not exist`)
1182
+ throw missingNode(source)
1178
1183
  }
1179
1184
  if (!mutable.nodes.has(target)) {
1180
- throw new Error(`Target node ${target} does not exist`)
1185
+ throw missingNode(target)
1181
1186
  }
1182
1187
 
1183
1188
  const edgeIndex = mutable.nextEdgeIndex
@@ -1187,26 +1192,26 @@ export const addEdge = <N, E, T extends Kind = "directed">(
1187
1192
  mutable.edges.set(edgeIndex, edgeData)
1188
1193
 
1189
1194
  // Update adjacency lists
1190
- const sourceAdjacency = getMapSafe(mutable.adjacency, source)
1191
- if (Option.isSome(sourceAdjacency)) {
1192
- sourceAdjacency.value.push(edgeIndex)
1195
+ const sourceAdjacency = mutable.adjacency.get(source)
1196
+ if (sourceAdjacency !== undefined) {
1197
+ sourceAdjacency.push(edgeIndex)
1193
1198
  }
1194
1199
 
1195
- const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target)
1196
- if (Option.isSome(targetReverseAdjacency)) {
1197
- targetReverseAdjacency.value.push(edgeIndex)
1200
+ const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
1201
+ if (targetReverseAdjacency !== undefined) {
1202
+ targetReverseAdjacency.push(edgeIndex)
1198
1203
  }
1199
1204
 
1200
1205
  // For undirected graphs, add reverse connections
1201
1206
  if (mutable.type === "undirected") {
1202
- const targetAdjacency = getMapSafe(mutable.adjacency, target)
1203
- if (Option.isSome(targetAdjacency)) {
1204
- targetAdjacency.value.push(edgeIndex)
1207
+ const targetAdjacency = mutable.adjacency.get(target)
1208
+ if (targetAdjacency !== undefined) {
1209
+ targetAdjacency.push(edgeIndex)
1205
1210
  }
1206
1211
 
1207
- const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source)
1208
- if (Option.isSome(sourceReverseAdjacency)) {
1209
- sourceReverseAdjacency.value.push(edgeIndex)
1212
+ const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
1213
+ if (sourceReverseAdjacency !== undefined) {
1214
+ sourceReverseAdjacency.push(edgeIndex)
1210
1215
  }
1211
1216
  }
1212
1217
 
@@ -1253,17 +1258,17 @@ export const removeNode = <N, E, T extends Kind = "directed">(
1253
1258
  const edgesToRemove: Array<EdgeIndex> = []
1254
1259
 
1255
1260
  // Get outgoing edges
1256
- const outgoingEdges = getMapSafe(mutable.adjacency, nodeIndex)
1257
- if (Option.isSome(outgoingEdges)) {
1258
- for (const edge of outgoingEdges.value) {
1261
+ const outgoingEdges = mutable.adjacency.get(nodeIndex)
1262
+ if (outgoingEdges !== undefined) {
1263
+ for (const edge of outgoingEdges) {
1259
1264
  edgesToRemove.push(edge)
1260
1265
  }
1261
1266
  }
1262
1267
 
1263
1268
  // Get incoming edges
1264
- const incomingEdges = getMapSafe(mutable.reverseAdjacency, nodeIndex)
1265
- if (Option.isSome(incomingEdges)) {
1266
- for (const edge of incomingEdges.value) {
1269
+ const incomingEdges = mutable.reverseAdjacency.get(nodeIndex)
1270
+ if (incomingEdges !== undefined) {
1271
+ for (const edge of incomingEdges) {
1267
1272
  edgesToRemove.push(edge)
1268
1273
  }
1269
1274
  }
@@ -1322,45 +1327,45 @@ const removeEdgeInternal = <N, E, T extends Kind = "directed">(
1322
1327
  edgeIndex: EdgeIndex
1323
1328
  ): boolean => {
1324
1329
  // Get edge data
1325
- const edge = getMapSafe(mutable.edges, edgeIndex)
1326
- if (Option.isNone(edge)) {
1330
+ const edge = mutable.edges.get(edgeIndex)
1331
+ if (edge === undefined) {
1327
1332
  return false // Edge doesn't exist, no mutation occurred
1328
1333
  }
1329
1334
 
1330
- const { source, target } = edge.value
1335
+ const { source, target } = edge
1331
1336
 
1332
1337
  // Remove from adjacency lists
1333
- const sourceAdjacency = getMapSafe(mutable.adjacency, source)
1334
- if (Option.isSome(sourceAdjacency)) {
1335
- const index = sourceAdjacency.value.indexOf(edgeIndex)
1338
+ const sourceAdjacency = mutable.adjacency.get(source)
1339
+ if (sourceAdjacency !== undefined) {
1340
+ const index = sourceAdjacency.indexOf(edgeIndex)
1336
1341
  if (index !== -1) {
1337
- sourceAdjacency.value.splice(index, 1)
1342
+ sourceAdjacency.splice(index, 1)
1338
1343
  }
1339
1344
  }
1340
1345
 
1341
- const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target)
1342
- if (Option.isSome(targetReverseAdjacency)) {
1343
- const index = targetReverseAdjacency.value.indexOf(edgeIndex)
1346
+ const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
1347
+ if (targetReverseAdjacency !== undefined) {
1348
+ const index = targetReverseAdjacency.indexOf(edgeIndex)
1344
1349
  if (index !== -1) {
1345
- targetReverseAdjacency.value.splice(index, 1)
1350
+ targetReverseAdjacency.splice(index, 1)
1346
1351
  }
1347
1352
  }
1348
1353
 
1349
1354
  // For undirected graphs, remove reverse connections
1350
1355
  if (mutable.type === "undirected") {
1351
- const targetAdjacency = getMapSafe(mutable.adjacency, target)
1352
- if (Option.isSome(targetAdjacency)) {
1353
- const index = targetAdjacency.value.indexOf(edgeIndex)
1356
+ const targetAdjacency = mutable.adjacency.get(target)
1357
+ if (targetAdjacency !== undefined) {
1358
+ const index = targetAdjacency.indexOf(edgeIndex)
1354
1359
  if (index !== -1) {
1355
- targetAdjacency.value.splice(index, 1)
1360
+ targetAdjacency.splice(index, 1)
1356
1361
  }
1357
1362
  }
1358
1363
 
1359
- const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source)
1360
- if (Option.isSome(sourceReverseAdjacency)) {
1361
- const index = sourceReverseAdjacency.value.indexOf(edgeIndex)
1364
+ const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
1365
+ if (sourceReverseAdjacency !== undefined) {
1366
+ const index = sourceReverseAdjacency.indexOf(edgeIndex)
1362
1367
  if (index !== -1) {
1363
- sourceReverseAdjacency.value.splice(index, 1)
1368
+ sourceReverseAdjacency.splice(index, 1)
1364
1369
  }
1365
1370
  }
1366
1371
  }
@@ -1404,7 +1409,7 @@ const removeEdgeInternal = <N, E, T extends Kind = "directed">(
1404
1409
  export const getEdge = <N, E, T extends Kind = "directed">(
1405
1410
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1406
1411
  edgeIndex: EdgeIndex
1407
- ): Option.Option<Edge<E>> => getMapSafe(graph.edges, edgeIndex)
1412
+ ): Option.Option<Edge<E>> => graph.edges.has(edgeIndex) ? Option.some(graph.edges.get(edgeIndex)!) : Option.none()
1408
1413
 
1409
1414
  /**
1410
1415
  * Checks if an edge exists between two nodes in the graph.
@@ -1439,15 +1444,15 @@ export const hasEdge = <N, E, T extends Kind = "directed">(
1439
1444
  source: NodeIndex,
1440
1445
  target: NodeIndex
1441
1446
  ): boolean => {
1442
- const adjacencyList = getMapSafe(graph.adjacency, source)
1443
- if (Option.isNone(adjacencyList)) {
1447
+ const adjacencyList = graph.adjacency.get(source)
1448
+ if (adjacencyList === undefined) {
1444
1449
  return false
1445
1450
  }
1446
1451
 
1447
1452
  // Check if any edge in the adjacency list connects to the target
1448
- for (const edgeIndex of adjacencyList.value) {
1449
- const edge = getMapSafe(graph.edges, edgeIndex)
1450
- if (Option.isSome(edge) && edge.value.target === target) {
1453
+ for (const edgeIndex of adjacencyList) {
1454
+ const edge = graph.edges.get(edgeIndex)
1455
+ if (edge !== undefined && edge.target === target) {
1451
1456
  return true
1452
1457
  }
1453
1458
  }
@@ -1522,16 +1527,16 @@ export const neighbors = <N, E, T extends Kind = "directed">(
1522
1527
  return getUndirectedNeighbors(graph as any, nodeIndex)
1523
1528
  }
1524
1529
 
1525
- const adjacencyList = getMapSafe(graph.adjacency, nodeIndex)
1526
- if (Option.isNone(adjacencyList)) {
1530
+ const adjacencyList = graph.adjacency.get(nodeIndex)
1531
+ if (adjacencyList === undefined) {
1527
1532
  return []
1528
1533
  }
1529
1534
 
1530
1535
  const result: Array<NodeIndex> = []
1531
- for (const edgeIndex of adjacencyList.value) {
1532
- const edge = getMapSafe(graph.edges, edgeIndex)
1533
- if (Option.isSome(edge)) {
1534
- result.push(edge.value.target)
1536
+ for (const edgeIndex of adjacencyList) {
1537
+ const edge = graph.edges.get(edgeIndex)
1538
+ if (edge !== undefined) {
1539
+ result.push(edge.target)
1535
1540
  }
1536
1541
  }
1537
1542
 
@@ -1573,19 +1578,19 @@ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1573
1578
  ? graph.reverseAdjacency
1574
1579
  : graph.adjacency
1575
1580
 
1576
- const adjacencyList = getMapSafe(adjacencyMap, nodeIndex)
1577
- if (Option.isNone(adjacencyList)) {
1581
+ const adjacencyList = adjacencyMap.get(nodeIndex)
1582
+ if (adjacencyList === undefined) {
1578
1583
  return []
1579
1584
  }
1580
1585
 
1581
1586
  const result: Array<NodeIndex> = []
1582
- for (const edgeIndex of adjacencyList.value) {
1583
- const edge = getMapSafe(graph.edges, edgeIndex)
1584
- if (Option.isSome(edge)) {
1587
+ for (const edgeIndex of adjacencyList) {
1588
+ const edge = graph.edges.get(edgeIndex)
1589
+ if (edge !== undefined) {
1585
1590
  // For incoming direction, we want the source node instead of target
1586
1591
  const neighborNode = direction === "incoming"
1587
- ? edge.value.source
1588
- : edge.value.target
1592
+ ? edge.source
1593
+ : edge.target
1589
1594
  result.push(neighborNode)
1590
1595
  }
1591
1596
  }
@@ -1597,6 +1602,18 @@ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1597
1602
  // GraphViz Export
1598
1603
  // =============================================================================
1599
1604
 
1605
+ /**
1606
+ * Configuration options for GraphViz DOT format generation from graphs.
1607
+ *
1608
+ * @since 3.18.0
1609
+ * @category models
1610
+ */
1611
+ export interface GraphVizOptions<N, E> {
1612
+ readonly nodeLabel?: (data: N) => string
1613
+ readonly edgeLabel?: (data: E) => string
1614
+ readonly graphName?: string
1615
+ }
1616
+
1600
1617
  /**
1601
1618
  * Exports a graph to GraphViz DOT format for visualization.
1602
1619
  *
@@ -1630,11 +1647,7 @@ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1630
1647
  */
1631
1648
  export const toGraphViz = <N, E, T extends Kind = "directed">(
1632
1649
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1633
- options?: {
1634
- readonly nodeLabel?: (data: N) => string
1635
- readonly edgeLabel?: (data: E) => string
1636
- readonly graphName?: string
1637
- }
1650
+ options?: GraphVizOptions<N, E>
1638
1651
  ): string => {
1639
1652
  const {
1640
1653
  edgeLabel = (data: E) => String(data),
@@ -1665,6 +1678,174 @@ export const toGraphViz = <N, E, T extends Kind = "directed">(
1665
1678
  return lines.join("\n")
1666
1679
  }
1667
1680
 
1681
+ // =============================================================================
1682
+ // Mermaid Export
1683
+ // =============================================================================
1684
+
1685
+ /**
1686
+ * Mermaid node shape types.
1687
+ *
1688
+ * @since 3.18.0
1689
+ * @category models
1690
+ */
1691
+ export type MermaidNodeShape =
1692
+ | "rectangle"
1693
+ | "rounded"
1694
+ | "circle"
1695
+ | "diamond"
1696
+ | "hexagon"
1697
+ | "stadium"
1698
+ | "subroutine"
1699
+ | "cylindrical"
1700
+
1701
+ /**
1702
+ * Mermaid diagram direction types.
1703
+ *
1704
+ * @since 3.18.0
1705
+ * @category models
1706
+ */
1707
+ export type MermaidDirection = "TB" | "TD" | "BT" | "LR" | "RL"
1708
+
1709
+ /**
1710
+ * Mermaid diagram type.
1711
+ *
1712
+ * @since 3.18.0
1713
+ * @category models
1714
+ */
1715
+ export type MermaidDiagramType = "flowchart" | "graph"
1716
+
1717
+ /**
1718
+ * Configuration options for Mermaid diagram generation.
1719
+ *
1720
+ * @since 3.18.0
1721
+ * @category models
1722
+ */
1723
+ export interface MermaidOptions<N, E> {
1724
+ readonly nodeLabel?: (data: N) => string
1725
+ readonly edgeLabel?: (data: E) => string
1726
+ readonly diagramType?: MermaidDiagramType
1727
+ readonly direction?: MermaidDirection
1728
+ readonly nodeShape?: (data: N) => MermaidNodeShape
1729
+ }
1730
+
1731
+ /** @internal */
1732
+ const escapeMermaidLabel = (label: string): string => {
1733
+ // Escape special characters for Mermaid using HTML entity codes
1734
+ // According to: https://mermaid.js.org/syntax/flowchart.html#special-characters-that-break-syntax
1735
+ return label
1736
+ .replace(/#/g, "#35;")
1737
+ .replace(/"/g, "#quot;")
1738
+ .replace(/</g, "#lt;")
1739
+ .replace(/>/g, "#gt;")
1740
+ .replace(/&/g, "#amp;")
1741
+ .replace(/\[/g, "#91;")
1742
+ .replace(/\]/g, "#93;")
1743
+ .replace(/\{/g, "#123;")
1744
+ .replace(/\}/g, "#125;")
1745
+ .replace(/\(/g, "#40;")
1746
+ .replace(/\)/g, "#41;")
1747
+ .replace(/\|/g, "#124;")
1748
+ .replace(/\\/g, "#92;")
1749
+ .replace(/\n/g, "<br/>");
1750
+ }
1751
+
1752
+ /** @internal */
1753
+ const formatMermaidNode = (nodeId: string, label: string, shape: MermaidNodeShape): string => {
1754
+ switch (shape) {
1755
+ case "rectangle":
1756
+ return `${nodeId}["${label}"]`
1757
+ case "rounded":
1758
+ return `${nodeId}("${label}")`
1759
+ case "circle":
1760
+ return `${nodeId}(("${label}"))`
1761
+ case "diamond":
1762
+ return `${nodeId}{"${label}"}`
1763
+ case "hexagon":
1764
+ return `${nodeId}{{"${label}"}}`
1765
+ case "stadium":
1766
+ return `${nodeId}(["${label}"])`
1767
+ case "subroutine":
1768
+ return `${nodeId}[["${label}"]]`
1769
+ case "cylindrical":
1770
+ return `${nodeId}[("${label}")]`
1771
+ }
1772
+ }
1773
+
1774
+ /**
1775
+ * Exports a graph to Mermaid diagram format for visualization.
1776
+ *
1777
+ * @example
1778
+ * ```ts
1779
+ * import { Graph } from "effect"
1780
+ *
1781
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1782
+ * const app = Graph.addNode(mutable, "App")
1783
+ * const db = Graph.addNode(mutable, "Database")
1784
+ * const cache = Graph.addNode(mutable, "Cache")
1785
+ * Graph.addEdge(mutable, app, db, 1)
1786
+ * Graph.addEdge(mutable, app, cache, 2)
1787
+ * })
1788
+ *
1789
+ * const mermaid = Graph.toMermaid(graph)
1790
+ * console.log(mermaid)
1791
+ * // flowchart TD
1792
+ * // 0["App"]
1793
+ * // 1["Database"]
1794
+ * // 2["Cache"]
1795
+ * // 0 -->|"1"| 1
1796
+ * // 0 -->|"2"| 2
1797
+ * ```
1798
+ *
1799
+ * @since 3.18.0
1800
+ * @category utils
1801
+ */
1802
+ export const toMermaid = <N, E, T extends Kind = "directed">(
1803
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1804
+ options?: MermaidOptions<N, E>
1805
+ ): string => {
1806
+ // Extract and validate options with defaults
1807
+ const {
1808
+ diagramType,
1809
+ direction = "TD",
1810
+ edgeLabel = (data: E) => String(data),
1811
+ nodeLabel = (data: N) => String(data),
1812
+ nodeShape = () => "rectangle" as const
1813
+ } = options ?? {}
1814
+
1815
+ // Auto-detect diagram type if not specified
1816
+ const finalDiagramType = diagramType ??
1817
+ (graph.type === "directed" ? "flowchart" : "graph")
1818
+
1819
+ // Generate diagram header
1820
+ const lines: Array<string> = []
1821
+ lines.push(`${finalDiagramType} ${direction}`)
1822
+
1823
+ // Add nodes
1824
+ for (const [nodeIndex, nodeData] of graph.nodes) {
1825
+ const nodeId = String(nodeIndex)
1826
+ const label = escapeMermaidLabel(nodeLabel(nodeData))
1827
+ const shape = nodeShape(nodeData)
1828
+ const formattedNode = formatMermaidNode(nodeId, label, shape)
1829
+ lines.push(` ${formattedNode}`)
1830
+ }
1831
+
1832
+ // Add edges
1833
+ const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---"
1834
+ for (const [, edgeData] of graph.edges) {
1835
+ const sourceId = String(edgeData.source)
1836
+ const targetId = String(edgeData.target)
1837
+ const label = escapeMermaidLabel(edgeLabel(edgeData.data))
1838
+
1839
+ if (label) {
1840
+ lines.push(` ${sourceId} ${edgeOperator}|"${label}"| ${targetId}`)
1841
+ } else {
1842
+ lines.push(` ${sourceId} ${edgeOperator} ${targetId}`)
1843
+ }
1844
+ }
1845
+
1846
+ return lines.join("\n")
1847
+ }
1848
+
1668
1849
  // =============================================================================
1669
1850
  // Direction Types for Bidirectional Traversal
1670
1851
  // =============================================================================
@@ -1683,10 +1864,10 @@ export const toGraphViz = <N, E, T extends Kind = "directed">(
1683
1864
  * })
1684
1865
  *
1685
1866
  * // Follow outgoing edges (normal direction)
1686
- * const outgoingNodes = Array.from(Graph.indices(Graph.dfs(graph, { startNodes: [0], direction: "outgoing" })))
1867
+ * const outgoingNodes = Array.from(Graph.indices(Graph.dfs(graph, { start: [0], direction: "outgoing" })))
1687
1868
  *
1688
1869
  * // Follow incoming edges (reverse direction)
1689
- * const incomingNodes = Array.from(Graph.indices(Graph.dfs(graph, { startNodes: [1], direction: "incoming" })))
1870
+ * const incomingNodes = Array.from(Graph.indices(Graph.dfs(graph, { start: [1], direction: "incoming" })))
1690
1871
  * ```
1691
1872
  *
1692
1873
  * @since 3.18.0
@@ -1907,13 +2088,13 @@ const getUndirectedNeighbors = <N, E>(
1907
2088
  const neighbors = new Set<NodeIndex>()
1908
2089
 
1909
2090
  // Check edges where this node is the source
1910
- const adjacencyList = getMapSafe(graph.adjacency, nodeIndex)
1911
- if (Option.isSome(adjacencyList)) {
1912
- for (const edgeIndex of adjacencyList.value) {
1913
- const edge = getMapSafe(graph.edges, edgeIndex)
1914
- if (Option.isSome(edge)) {
2091
+ const adjacencyList = graph.adjacency.get(nodeIndex)
2092
+ if (adjacencyList !== undefined) {
2093
+ for (const edgeIndex of adjacencyList) {
2094
+ const edge = graph.edges.get(edgeIndex)
2095
+ if (edge !== undefined) {
1915
2096
  // For undirected graphs, the neighbor is the other endpoint
1916
- const otherNode = edge.value.source === nodeIndex ? edge.value.target : edge.value.source
2097
+ const otherNode = edge.source === nodeIndex ? edge.target : edge.source
1917
2098
  neighbors.add(otherNode)
1918
2099
  }
1919
2100
  }
@@ -2077,12 +2258,12 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2077
2258
  scc.push(node)
2078
2259
 
2079
2260
  // Use reverse adjacency (transpose graph)
2080
- const reverseAdjacency = getMapSafe(graph.reverseAdjacency, node)
2081
- if (Option.isSome(reverseAdjacency)) {
2082
- for (const edgeIndex of reverseAdjacency.value) {
2083
- const edge = getMapSafe(graph.edges, edgeIndex)
2084
- if (Option.isSome(edge)) {
2085
- const predecessor = edge.value.source
2261
+ const reverseAdjacency = graph.reverseAdjacency.get(node)
2262
+ if (reverseAdjacency !== undefined) {
2263
+ for (const edgeIndex of reverseAdjacency) {
2264
+ const edge = graph.edges.get(edgeIndex)
2265
+ if (edge !== undefined) {
2266
+ const predecessor = edge.source
2086
2267
  if (!visited.has(predecessor)) {
2087
2268
  stack.push(predecessor)
2088
2269
  }
@@ -2110,7 +2291,44 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2110
2291
  export interface PathResult<E> {
2111
2292
  readonly path: Array<NodeIndex>
2112
2293
  readonly distance: number
2113
- readonly edgeWeights: Array<E>
2294
+ readonly costs: Array<E>
2295
+ }
2296
+
2297
+ /**
2298
+ * Configuration for Dijkstra's algorithm.
2299
+ *
2300
+ * @since 3.18.0
2301
+ * @category models
2302
+ */
2303
+ export interface DijkstraConfig<E> {
2304
+ source: NodeIndex
2305
+ target: NodeIndex
2306
+ cost: (edgeData: E) => number
2307
+ }
2308
+
2309
+ /**
2310
+ * Configuration for A* algorithm.
2311
+ *
2312
+ * @since 3.18.0
2313
+ * @category models
2314
+ */
2315
+ export interface AstarConfig<E, N> {
2316
+ source: NodeIndex
2317
+ target: NodeIndex
2318
+ cost: (edgeData: E) => number
2319
+ heuristic: (sourceNodeData: N, targetNodeData: N) => number
2320
+ }
2321
+
2322
+ /**
2323
+ * Configuration for Bellman-Ford algorithm.
2324
+ *
2325
+ * @since 3.18.0
2326
+ * @category models
2327
+ */
2328
+ export interface BellmanFordConfig<E> {
2329
+ source: NodeIndex
2330
+ target: NodeIndex
2331
+ cost: (edgeData: E) => number
2114
2332
  }
2115
2333
 
2116
2334
  /**
@@ -2132,7 +2350,7 @@ export interface PathResult<E> {
2132
2350
  * Graph.addEdge(mutable, b, c, 2)
2133
2351
  * })
2134
2352
  *
2135
- * const result = Graph.dijkstra(graph, 0, 2, (edgeData) => edgeData)
2353
+ * const result = Graph.dijkstra(graph, { source: 0, target: 2, cost: (edgeData) => edgeData })
2136
2354
  * if (Option.isSome(result)) {
2137
2355
  * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
2138
2356
  * console.log(result.value.distance) // 7 - total distance
@@ -2144,16 +2362,15 @@ export interface PathResult<E> {
2144
2362
  */
2145
2363
  export const dijkstra = <N, E, T extends Kind = "directed">(
2146
2364
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2147
- source: NodeIndex,
2148
- target: NodeIndex,
2149
- edgeWeight: (edgeData: E) => number
2365
+ config: DijkstraConfig<E>
2150
2366
  ): Option.Option<PathResult<E>> => {
2367
+ const { cost, source, target } = config
2151
2368
  // Validate that source and target nodes exist
2152
2369
  if (!graph.nodes.has(source)) {
2153
- throw new Error(`Source node ${source} does not exist`)
2370
+ throw missingNode(source)
2154
2371
  }
2155
2372
  if (!graph.nodes.has(target)) {
2156
- throw new Error(`Target node ${target} does not exist`)
2373
+ throw missingNode(target)
2157
2374
  }
2158
2375
 
2159
2376
  // Early return if source equals target
@@ -2161,7 +2378,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2161
2378
  return Option.some({
2162
2379
  path: [source],
2163
2380
  distance: 0,
2164
- edgeWeights: []
2381
+ costs: []
2165
2382
  })
2166
2383
  }
2167
2384
 
@@ -2210,13 +2427,13 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2210
2427
  const currentDistance = distances.get(currentNode)!
2211
2428
 
2212
2429
  // Examine all outgoing edges
2213
- const adjacencyList = getMapSafe(graph.adjacency, currentNode)
2214
- if (Option.isSome(adjacencyList)) {
2215
- for (const edgeIndex of adjacencyList.value) {
2216
- const edge = getMapSafe(graph.edges, edgeIndex)
2217
- if (Option.isSome(edge)) {
2218
- const neighbor = edge.value.target
2219
- const weight = edgeWeight(edge.value.data)
2430
+ const adjacencyList = graph.adjacency.get(currentNode)
2431
+ if (adjacencyList !== undefined) {
2432
+ for (const edgeIndex of adjacencyList) {
2433
+ const edge = graph.edges.get(edgeIndex)
2434
+ if (edge !== undefined) {
2435
+ const neighbor = edge.target
2436
+ const weight = cost(edge.data)
2220
2437
 
2221
2438
  // Validate non-negative weights
2222
2439
  if (weight < 0) {
@@ -2229,7 +2446,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2229
2446
  // Relaxation step
2230
2447
  if (newDistance < neighborDistance) {
2231
2448
  distances.set(neighbor, newDistance)
2232
- previous.set(neighbor, { node: currentNode, edgeData: edge.value.data })
2449
+ previous.set(neighbor, { node: currentNode, edgeData: edge.data })
2233
2450
 
2234
2451
  // Add to priority queue if not visited
2235
2452
  if (!visited.has(neighbor)) {
@@ -2249,14 +2466,14 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2249
2466
 
2250
2467
  // Reconstruct path
2251
2468
  const path: Array<NodeIndex> = []
2252
- const edgeWeights: Array<E> = []
2469
+ const costs: Array<E> = []
2253
2470
  let currentNode: NodeIndex | null = target
2254
2471
 
2255
2472
  while (currentNode !== null) {
2256
2473
  path.unshift(currentNode)
2257
2474
  const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2258
2475
  if (prev !== null) {
2259
- edgeWeights.unshift(prev.edgeData)
2476
+ costs.unshift(prev.edgeData)
2260
2477
  currentNode = prev.node
2261
2478
  } else {
2262
2479
  currentNode = null
@@ -2266,7 +2483,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2266
2483
  return Option.some({
2267
2484
  path,
2268
2485
  distance: targetDistance,
2269
- edgeWeights
2486
+ costs
2270
2487
  })
2271
2488
  }
2272
2489
 
@@ -2279,7 +2496,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2279
2496
  export interface AllPairsResult<E> {
2280
2497
  readonly distances: Map<NodeIndex, Map<NodeIndex, number>>
2281
2498
  readonly paths: Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>
2282
- readonly edgeWeights: Map<NodeIndex, Map<NodeIndex, Array<E>>>
2499
+ readonly costs: Map<NodeIndex, Map<NodeIndex, Array<E>>>
2283
2500
  }
2284
2501
 
2285
2502
  /**
@@ -2311,7 +2528,7 @@ export interface AllPairsResult<E> {
2311
2528
  */
2312
2529
  export const floydWarshall = <N, E, T extends Kind = "directed">(
2313
2530
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2314
- edgeWeight: (edgeData: E) => number
2531
+ cost: (edgeData: E) => number
2315
2532
  ): AllPairsResult<E> => {
2316
2533
  // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration)
2317
2534
  const allNodes = Array.from(graph.nodes.keys())
@@ -2336,7 +2553,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2336
2553
 
2337
2554
  // Set edge weights
2338
2555
  for (const [, edgeData] of graph.edges) {
2339
- const weight = edgeWeight(edgeData.data)
2556
+ const weight = cost(edgeData.data)
2340
2557
  const i = edgeData.source
2341
2558
  const j = edgeData.target
2342
2559
 
@@ -2374,19 +2591,19 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2374
2591
 
2375
2592
  // Build result paths and edge weights
2376
2593
  const paths = new Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>()
2377
- const resultEdgeWeights = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
2594
+ const resultCosts = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
2378
2595
 
2379
2596
  for (const i of allNodes) {
2380
2597
  paths.set(i, new Map())
2381
- resultEdgeWeights.set(i, new Map())
2598
+ resultCosts.set(i, new Map())
2382
2599
 
2383
2600
  for (const j of allNodes) {
2384
2601
  if (i === j) {
2385
2602
  paths.get(i)!.set(j, [i])
2386
- resultEdgeWeights.get(i)!.set(j, [])
2603
+ resultCosts.get(i)!.set(j, [])
2387
2604
  } else if (dist.get(i)!.get(j)! === Infinity) {
2388
2605
  paths.get(i)!.set(j, null)
2389
- resultEdgeWeights.get(i)!.set(j, [])
2606
+ resultCosts.get(i)!.set(j, [])
2390
2607
  } else {
2391
2608
  // Reconstruct path iteratively
2392
2609
  const path: Array<NodeIndex> = []
@@ -2408,7 +2625,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2408
2625
  }
2409
2626
 
2410
2627
  paths.get(i)!.set(j, path)
2411
- resultEdgeWeights.get(i)!.set(j, weights)
2628
+ resultCosts.get(i)!.set(j, weights)
2412
2629
  }
2413
2630
  }
2414
2631
  }
@@ -2416,7 +2633,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2416
2633
  return {
2417
2634
  distances: dist,
2418
2635
  paths,
2419
- edgeWeights: resultEdgeWeights
2636
+ costs: resultCosts
2420
2637
  }
2421
2638
  }
2422
2639
 
@@ -2443,7 +2660,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2443
2660
  * const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) =>
2444
2661
  * Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y)
2445
2662
  *
2446
- * const result = Graph.astar(graph, 0, 2, (edgeData) => edgeData, heuristic)
2663
+ * const result = Graph.astar(graph, { source: 0, target: 2, cost: (edgeData) => edgeData, heuristic })
2447
2664
  * if (Option.isSome(result)) {
2448
2665
  * console.log(result.value.path) // [0, 1, 2] - shortest path
2449
2666
  * console.log(result.value.distance) // 2 - total distance
@@ -2455,17 +2672,15 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2455
2672
  */
2456
2673
  export const astar = <N, E, T extends Kind = "directed">(
2457
2674
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2458
- source: NodeIndex,
2459
- target: NodeIndex,
2460
- edgeWeight: (edgeData: E) => number,
2461
- heuristic: (sourceNodeData: N, targetNodeData: N) => number
2675
+ config: AstarConfig<E, N>
2462
2676
  ): Option.Option<PathResult<E>> => {
2677
+ const { cost, heuristic, source, target } = config
2463
2678
  // Validate that source and target nodes exist
2464
2679
  if (!graph.nodes.has(source)) {
2465
- throw new Error(`Source node ${source} does not exist`)
2680
+ throw missingNode(source)
2466
2681
  }
2467
2682
  if (!graph.nodes.has(target)) {
2468
- throw new Error(`Target node ${target} does not exist`)
2683
+ throw missingNode(target)
2469
2684
  }
2470
2685
 
2471
2686
  // Early return if source equals target
@@ -2473,13 +2688,13 @@ export const astar = <N, E, T extends Kind = "directed">(
2473
2688
  return Option.some({
2474
2689
  path: [source],
2475
2690
  distance: 0,
2476
- edgeWeights: []
2691
+ costs: []
2477
2692
  })
2478
2693
  }
2479
2694
 
2480
2695
  // Get target node data for heuristic calculations
2481
- const targetNodeData = getMapSafe(graph.nodes, target)
2482
- if (Option.isNone(targetNodeData)) {
2696
+ const targetNodeData = graph.nodes.get(target)
2697
+ if (targetNodeData === undefined) {
2483
2698
  throw new Error(`Target node ${target} data not found`)
2484
2699
  }
2485
2700
 
@@ -2498,9 +2713,9 @@ export const astar = <N, E, T extends Kind = "directed">(
2498
2713
  }
2499
2714
 
2500
2715
  // Calculate initial f-score for source
2501
- const sourceNodeData = getMapSafe(graph.nodes, source)
2502
- if (Option.isSome(sourceNodeData)) {
2503
- const h = heuristic(sourceNodeData.value, targetNodeData.value)
2716
+ const sourceNodeData = graph.nodes.get(source)
2717
+ if (sourceNodeData !== undefined) {
2718
+ const h = heuristic(sourceNodeData, targetNodeData)
2504
2719
  fScore.set(source, h)
2505
2720
  }
2506
2721
 
@@ -2537,13 +2752,13 @@ export const astar = <N, E, T extends Kind = "directed">(
2537
2752
  const currentGScore = gScore.get(currentNode)!
2538
2753
 
2539
2754
  // Examine all outgoing edges
2540
- const adjacencyList = getMapSafe(graph.adjacency, currentNode)
2541
- if (Option.isSome(adjacencyList)) {
2542
- for (const edgeIndex of adjacencyList.value) {
2543
- const edge = getMapSafe(graph.edges, edgeIndex)
2544
- if (Option.isSome(edge)) {
2545
- const neighbor = edge.value.target
2546
- const weight = edgeWeight(edge.value.data)
2755
+ const adjacencyList = graph.adjacency.get(currentNode)
2756
+ if (adjacencyList !== undefined) {
2757
+ for (const edgeIndex of adjacencyList) {
2758
+ const edge = graph.edges.get(edgeIndex)
2759
+ if (edge !== undefined) {
2760
+ const neighbor = edge.target
2761
+ const weight = cost(edge.data)
2547
2762
 
2548
2763
  // Validate non-negative weights
2549
2764
  if (weight < 0) {
@@ -2557,12 +2772,12 @@ export const astar = <N, E, T extends Kind = "directed">(
2557
2772
  if (tentativeGScore < neighborGScore) {
2558
2773
  // Update g-score and previous
2559
2774
  gScore.set(neighbor, tentativeGScore)
2560
- previous.set(neighbor, { node: currentNode, edgeData: edge.value.data })
2775
+ previous.set(neighbor, { node: currentNode, edgeData: edge.data })
2561
2776
 
2562
2777
  // Calculate f-score using heuristic
2563
- const neighborNodeData = getMapSafe(graph.nodes, neighbor)
2564
- if (Option.isSome(neighborNodeData)) {
2565
- const h = heuristic(neighborNodeData.value, targetNodeData.value)
2778
+ const neighborNodeData = graph.nodes.get(neighbor)
2779
+ if (neighborNodeData !== undefined) {
2780
+ const h = heuristic(neighborNodeData, targetNodeData)
2566
2781
  const f = tentativeGScore + h
2567
2782
  fScore.set(neighbor, f)
2568
2783
 
@@ -2585,14 +2800,14 @@ export const astar = <N, E, T extends Kind = "directed">(
2585
2800
 
2586
2801
  // Reconstruct path
2587
2802
  const path: Array<NodeIndex> = []
2588
- const edgeWeights: Array<E> = []
2803
+ const costs: Array<E> = []
2589
2804
  let currentNode: NodeIndex | null = target
2590
2805
 
2591
2806
  while (currentNode !== null) {
2592
2807
  path.unshift(currentNode)
2593
2808
  const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2594
2809
  if (prev !== null) {
2595
- edgeWeights.unshift(prev.edgeData)
2810
+ costs.unshift(prev.edgeData)
2596
2811
  currentNode = prev.node
2597
2812
  } else {
2598
2813
  currentNode = null
@@ -2602,7 +2817,7 @@ export const astar = <N, E, T extends Kind = "directed">(
2602
2817
  return Option.some({
2603
2818
  path,
2604
2819
  distance: targetGScore,
2605
- edgeWeights
2820
+ costs
2606
2821
  })
2607
2822
  }
2608
2823
 
@@ -2626,7 +2841,7 @@ export const astar = <N, E, T extends Kind = "directed">(
2626
2841
  * Graph.addEdge(mutable, a, c, 5)
2627
2842
  * })
2628
2843
  *
2629
- * const result = Graph.bellmanFord(graph, 0, 2, (edgeData) => edgeData)
2844
+ * const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edgeData) => edgeData })
2630
2845
  * if (Option.isSome(result)) {
2631
2846
  * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
2632
2847
  * console.log(result.value.distance) // 2 - total distance
@@ -2638,16 +2853,15 @@ export const astar = <N, E, T extends Kind = "directed">(
2638
2853
  */
2639
2854
  export const bellmanFord = <N, E, T extends Kind = "directed">(
2640
2855
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2641
- source: NodeIndex,
2642
- target: NodeIndex,
2643
- edgeWeight: (edgeData: E) => number
2856
+ config: BellmanFordConfig<E>
2644
2857
  ): Option.Option<PathResult<E>> => {
2858
+ const { cost, source, target } = config
2645
2859
  // Validate that source and target nodes exist
2646
2860
  if (!graph.nodes.has(source)) {
2647
- throw new Error(`Source node ${source} does not exist`)
2861
+ throw missingNode(source)
2648
2862
  }
2649
2863
  if (!graph.nodes.has(target)) {
2650
- throw new Error(`Target node ${target} does not exist`)
2864
+ throw missingNode(target)
2651
2865
  }
2652
2866
 
2653
2867
  // Early return if source equals target
@@ -2655,7 +2869,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2655
2869
  return Option.some({
2656
2870
  path: [source],
2657
2871
  distance: 0,
2658
- edgeWeights: []
2872
+ costs: []
2659
2873
  })
2660
2874
  }
2661
2875
 
@@ -2672,7 +2886,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2672
2886
  // Collect all edges for relaxation
2673
2887
  const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = []
2674
2888
  for (const [, edgeData] of graph.edges) {
2675
- const weight = edgeWeight(edgeData.data)
2889
+ const weight = cost(edgeData.data)
2676
2890
  edges.push({
2677
2891
  source: edgeData.source,
2678
2892
  target: edgeData.target,
@@ -2720,12 +2934,12 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2720
2934
  affectedNodes.add(node)
2721
2935
 
2722
2936
  // Add all nodes reachable from this node
2723
- const adjacencyList = getMapSafe(graph.adjacency, node)
2724
- if (Option.isSome(adjacencyList)) {
2725
- for (const edgeIndex of adjacencyList.value) {
2726
- const edge = getMapSafe(graph.edges, edgeIndex)
2727
- if (Option.isSome(edge)) {
2728
- queue.push(edge.value.target)
2937
+ const adjacencyList = graph.adjacency.get(node)
2938
+ if (adjacencyList !== undefined) {
2939
+ for (const edgeIndex of adjacencyList) {
2940
+ const edge = graph.edges.get(edgeIndex)
2941
+ if (edge !== undefined) {
2942
+ queue.push(edge.target)
2729
2943
  }
2730
2944
  }
2731
2945
  }
@@ -2746,14 +2960,14 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2746
2960
 
2747
2961
  // Reconstruct path
2748
2962
  const path: Array<NodeIndex> = []
2749
- const edgeWeights: Array<E> = []
2963
+ const costs: Array<E> = []
2750
2964
  let currentNode: NodeIndex | null = target
2751
2965
 
2752
2966
  while (currentNode !== null) {
2753
2967
  path.unshift(currentNode)
2754
2968
  const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2755
2969
  if (prev !== null) {
2756
- edgeWeights.unshift(prev.edgeData)
2970
+ costs.unshift(prev.edgeData)
2757
2971
  currentNode = prev.node
2758
2972
  } else {
2759
2973
  currentNode = null
@@ -2763,7 +2977,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2763
2977
  return Option.some({
2764
2978
  path,
2765
2979
  distance: targetDistance,
2766
- edgeWeights
2980
+ costs
2767
2981
  })
2768
2982
  }
2769
2983
 
@@ -2785,7 +2999,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2785
2999
  * })
2786
3000
  *
2787
3001
  * // Both traversal and element iterators return NodeWalker
2788
- * const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { startNodes: [0] })
3002
+ * const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { start: [0] })
2789
3003
  * const allNodes: Graph.NodeWalker<string> = Graph.nodes(graph)
2790
3004
  *
2791
3005
  * // Common interface for working with node iterables
@@ -2822,7 +3036,7 @@ export class Walker<T, N> implements Iterable<[T, N]> {
2822
3036
  * Graph.addEdge(mutable, a, b, 1)
2823
3037
  * })
2824
3038
  *
2825
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3039
+ * const dfs = Graph.dfs(graph, { start: [0] })
2826
3040
  *
2827
3041
  * // Map to just the node data
2828
3042
  * const values = Array.from(dfs.visit((index, data) => data))
@@ -2856,7 +3070,7 @@ export class Walker<T, N> implements Iterable<[T, N]> {
2856
3070
  * Graph.addEdge(mutable, a, b, 1)
2857
3071
  * })
2858
3072
  *
2859
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3073
+ * const dfs = Graph.dfs(graph, { start: [0] })
2860
3074
  *
2861
3075
  * // Map to just the node data
2862
3076
  * const values = Array.from(dfs.visit((index, data) => data))
@@ -2908,7 +3122,7 @@ export type EdgeWalker<E> = Walker<EdgeIndex, Edge<E>>
2908
3122
  * Graph.addEdge(mutable, a, b, 1)
2909
3123
  * })
2910
3124
  *
2911
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3125
+ * const dfs = Graph.dfs(graph, { start: [0] })
2912
3126
  * const indices = Array.from(Graph.indices(dfs))
2913
3127
  * console.log(indices) // [0, 1]
2914
3128
  * ```
@@ -2931,7 +3145,7 @@ export const indices = <T, N>(walker: Walker<T, N>): Iterable<T> => walker.visit
2931
3145
  * Graph.addEdge(mutable, a, b, 1)
2932
3146
  * })
2933
3147
  *
2934
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3148
+ * const dfs = Graph.dfs(graph, { start: [0] })
2935
3149
  * const values = Array.from(Graph.values(dfs))
2936
3150
  * console.log(values) // ["A", "B"]
2937
3151
  * ```
@@ -2954,7 +3168,7 @@ export const values = <T, N>(walker: Walker<T, N>): Iterable<N> => walker.visit(
2954
3168
  * Graph.addEdge(mutable, a, b, 1)
2955
3169
  * })
2956
3170
  *
2957
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3171
+ * const dfs = Graph.dfs(graph, { start: [0] })
2958
3172
  * const entries = Array.from(Graph.entries(dfs))
2959
3173
  * console.log(entries) // [[0, "A"], [1, "B"]]
2960
3174
  * ```
@@ -2966,13 +3180,13 @@ export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
2966
3180
  walker.visit((index, data) => [index, data] as [T, N])
2967
3181
 
2968
3182
  /**
2969
- * Configuration options for DFS iterator.
3183
+ * Configuration for graph search iterators.
2970
3184
  *
2971
3185
  * @since 3.18.0
2972
3186
  * @category models
2973
3187
  */
2974
- export interface DfsConfig {
2975
- readonly startNodes?: Array<NodeIndex>
3188
+ export interface SearchConfig {
3189
+ readonly start?: Array<NodeIndex>
2976
3190
  readonly direction?: Direction
2977
3191
  }
2978
3192
 
@@ -2995,7 +3209,7 @@ export interface DfsConfig {
2995
3209
  * })
2996
3210
  *
2997
3211
  * // Start from a specific node
2998
- * const dfs1 = Graph.dfs(graph, { startNodes: [0] })
3212
+ * const dfs1 = Graph.dfs(graph, { start: [0] })
2999
3213
  * for (const nodeIndex of Graph.indices(dfs1)) {
3000
3214
  * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2
3001
3215
  * }
@@ -3010,21 +3224,21 @@ export interface DfsConfig {
3010
3224
  */
3011
3225
  export const dfs = <N, E, T extends Kind = "directed">(
3012
3226
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3013
- config: DfsConfig = {}
3227
+ config: SearchConfig = {}
3014
3228
  ): NodeWalker<N> => {
3015
- const startNodes = config.startNodes ?? []
3229
+ const start = config.start ?? []
3016
3230
  const direction = config.direction ?? "outgoing"
3017
3231
 
3018
3232
  // Validate that all start nodes exist
3019
- for (const nodeIndex of startNodes) {
3233
+ for (const nodeIndex of start) {
3020
3234
  if (!hasNode(graph, nodeIndex)) {
3021
- throw new Error(`Start node ${nodeIndex} does not exist`)
3235
+ throw missingNode(nodeIndex)
3022
3236
  }
3023
3237
  }
3024
3238
 
3025
3239
  return new Walker((f) => ({
3026
3240
  [Symbol.iterator]: () => {
3027
- const stack = [...startNodes]
3241
+ const stack = [...start]
3028
3242
  const discovered = new Set<NodeIndex>()
3029
3243
 
3030
3244
  const nextMapped = () => {
@@ -3037,8 +3251,8 @@ export const dfs = <N, E, T extends Kind = "directed">(
3037
3251
 
3038
3252
  discovered.add(current)
3039
3253
 
3040
- const nodeDataOption = getMapSafe(graph.nodes, current)
3041
- if (Option.isNone(nodeDataOption)) {
3254
+ const nodeDataOption = graph.nodes.get(current)
3255
+ if (nodeDataOption === undefined) {
3042
3256
  continue
3043
3257
  }
3044
3258
 
@@ -3050,7 +3264,7 @@ export const dfs = <N, E, T extends Kind = "directed">(
3050
3264
  }
3051
3265
  }
3052
3266
 
3053
- return { done: false, value: f(current, nodeDataOption.value) }
3267
+ return { done: false, value: f(current, nodeDataOption) }
3054
3268
  }
3055
3269
 
3056
3270
  return { done: true, value: undefined } as const
@@ -3061,17 +3275,6 @@ export const dfs = <N, E, T extends Kind = "directed">(
3061
3275
  }))
3062
3276
  }
3063
3277
 
3064
- /**
3065
- * Configuration options for BFS iterator.
3066
- *
3067
- * @since 3.18.0
3068
- * @category models
3069
- */
3070
- export interface BfsConfig {
3071
- readonly startNodes?: Array<NodeIndex>
3072
- readonly direction?: Direction
3073
- }
3074
-
3075
3278
  /**
3076
3279
  * Creates a new BFS iterator with optional configuration.
3077
3280
  *
@@ -3091,7 +3294,7 @@ export interface BfsConfig {
3091
3294
  * })
3092
3295
  *
3093
3296
  * // Start from a specific node
3094
- * const bfs1 = Graph.bfs(graph, { startNodes: [0] })
3297
+ * const bfs1 = Graph.bfs(graph, { start: [0] })
3095
3298
  * for (const nodeIndex of Graph.indices(bfs1)) {
3096
3299
  * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2
3097
3300
  * }
@@ -3106,21 +3309,21 @@ export interface BfsConfig {
3106
3309
  */
3107
3310
  export const bfs = <N, E, T extends Kind = "directed">(
3108
3311
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3109
- config: BfsConfig = {}
3312
+ config: SearchConfig = {}
3110
3313
  ): NodeWalker<N> => {
3111
- const startNodes = config.startNodes ?? []
3314
+ const start = config.start ?? []
3112
3315
  const direction = config.direction ?? "outgoing"
3113
3316
 
3114
3317
  // Validate that all start nodes exist
3115
- for (const nodeIndex of startNodes) {
3318
+ for (const nodeIndex of start) {
3116
3319
  if (!hasNode(graph, nodeIndex)) {
3117
- throw new Error(`Start node ${nodeIndex} does not exist`)
3320
+ throw missingNode(nodeIndex)
3118
3321
  }
3119
3322
  }
3120
3323
 
3121
3324
  return new Walker((f) => ({
3122
3325
  [Symbol.iterator]: () => {
3123
- const queue = [...startNodes]
3326
+ const queue = [...start]
3124
3327
  const discovered = new Set<NodeIndex>()
3125
3328
 
3126
3329
  const nextMapped = () => {
@@ -3222,7 +3425,7 @@ export const topo = <N, E, T extends Kind = "directed">(
3222
3425
  // Validate that all initial nodes exist
3223
3426
  for (const nodeIndex of initials) {
3224
3427
  if (!hasNode(graph, nodeIndex)) {
3225
- throw new Error(`Initial node ${nodeIndex} does not exist`)
3428
+ throw missingNode(nodeIndex)
3226
3429
  }
3227
3430
  }
3228
3431
 
@@ -3291,17 +3494,6 @@ export const topo = <N, E, T extends Kind = "directed">(
3291
3494
  }))
3292
3495
  }
3293
3496
 
3294
- /**
3295
- * Configuration options for DFS postorder iterator.
3296
- *
3297
- * @since 3.18.0
3298
- * @category models
3299
- */
3300
- export interface DfsPostOrderConfig {
3301
- readonly startNodes?: Array<NodeIndex>
3302
- readonly direction?: Direction
3303
- }
3304
-
3305
3497
  /**
3306
3498
  * Creates a new DFS postorder iterator with optional configuration.
3307
3499
  *
@@ -3322,7 +3514,7 @@ export interface DfsPostOrderConfig {
3322
3514
  * })
3323
3515
  *
3324
3516
  * // Postorder: children before parents
3325
- * const postOrder = Graph.dfsPostOrder(graph, { startNodes: [0] })
3517
+ * const postOrder = Graph.dfsPostOrder(graph, { start: [0] })
3326
3518
  * for (const node of postOrder) {
3327
3519
  * console.log(node) // 1, 2, 0
3328
3520
  * }
@@ -3333,15 +3525,15 @@ export interface DfsPostOrderConfig {
3333
3525
  */
3334
3526
  export const dfsPostOrder = <N, E, T extends Kind = "directed">(
3335
3527
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3336
- config: DfsPostOrderConfig = {}
3528
+ config: SearchConfig = {}
3337
3529
  ): NodeWalker<N> => {
3338
- const startNodes = config.startNodes ?? []
3530
+ const start = config.start ?? []
3339
3531
  const direction = config.direction ?? "outgoing"
3340
3532
 
3341
3533
  // Validate that all start nodes exist
3342
- for (const nodeIndex of startNodes) {
3534
+ for (const nodeIndex of start) {
3343
3535
  if (!hasNode(graph, nodeIndex)) {
3344
- throw new Error(`Start node ${nodeIndex} does not exist`)
3536
+ throw missingNode(nodeIndex)
3345
3537
  }
3346
3538
  }
3347
3539
 
@@ -3352,8 +3544,8 @@ export const dfsPostOrder = <N, E, T extends Kind = "directed">(
3352
3544
  const finished = new Set<NodeIndex>()
3353
3545
 
3354
3546
  // Initialize stack with start nodes
3355
- for (let i = startNodes.length - 1; i >= 0; i--) {
3356
- stack.push({ node: startNodes[i], visitedChildren: false })
3547
+ for (let i = start.length - 1; i >= 0; i--) {
3548
+ stack.push({ node: start[i], visitedChildren: false })
3357
3549
  }
3358
3550
 
3359
3551
  const nextMapped = () => {
@@ -3551,10 +3743,10 @@ export const externals = <N, E, T extends Kind = "directed">(
3551
3743
  let current = nodeIterator.next()
3552
3744
  while (!current.done) {
3553
3745
  const [nodeIndex, nodeData] = current.value
3554
- const adjacencyList = getMapSafe(adjacencyMap, nodeIndex)
3746
+ const adjacencyList = adjacencyMap.get(nodeIndex)
3555
3747
 
3556
3748
  // Node is external if it has no edges in the specified direction
3557
- if (Option.isNone(adjacencyList) || adjacencyList.value.length === 0) {
3749
+ if (adjacencyList === undefined || adjacencyList.length === 0) {
3558
3750
  return { done: false, value: f(nodeIndex, nodeData) }
3559
3751
  }
3560
3752
  current = nodeIterator.next()