effect 3.18.4 → 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.
Files changed (51) hide show
  1. package/HashRing/package.json +6 -0
  2. package/dist/cjs/Array.js.map +1 -1
  3. package/dist/cjs/Effect.js.map +1 -1
  4. package/dist/cjs/Graph.js +290 -177
  5. package/dist/cjs/Graph.js.map +1 -1
  6. package/dist/cjs/HashRing.js +257 -0
  7. package/dist/cjs/HashRing.js.map +1 -0
  8. package/dist/cjs/JSONSchema.js +39 -8
  9. package/dist/cjs/JSONSchema.js.map +1 -1
  10. package/dist/cjs/TestClock.js +8 -8
  11. package/dist/cjs/TestClock.js.map +1 -1
  12. package/dist/cjs/index.js +4 -2
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/internal/version.js +1 -1
  15. package/dist/dts/Array.d.ts +3 -3
  16. package/dist/dts/Array.d.ts.map +1 -1
  17. package/dist/dts/Effect.d.ts +6 -1
  18. package/dist/dts/Effect.d.ts.map +1 -1
  19. package/dist/dts/Graph.d.ts +147 -49
  20. package/dist/dts/Graph.d.ts.map +1 -1
  21. package/dist/dts/HashRing.d.ts +158 -0
  22. package/dist/dts/HashRing.d.ts.map +1 -0
  23. package/dist/dts/JSONSchema.d.ts +3 -2
  24. package/dist/dts/JSONSchema.d.ts.map +1 -1
  25. package/dist/dts/Types.d.ts +1 -1
  26. package/dist/dts/Types.d.ts.map +1 -1
  27. package/dist/dts/index.d.ts +5 -0
  28. package/dist/dts/index.d.ts.map +1 -1
  29. package/dist/esm/Array.js.map +1 -1
  30. package/dist/esm/Effect.js.map +1 -1
  31. package/dist/esm/Graph.js +286 -175
  32. package/dist/esm/Graph.js.map +1 -1
  33. package/dist/esm/HashRing.js +245 -0
  34. package/dist/esm/HashRing.js.map +1 -0
  35. package/dist/esm/JSONSchema.js +35 -6
  36. package/dist/esm/JSONSchema.js.map +1 -1
  37. package/dist/esm/TestClock.js +8 -8
  38. package/dist/esm/TestClock.js.map +1 -1
  39. package/dist/esm/index.js +5 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/internal/version.js +1 -1
  42. package/package.json +9 -1
  43. package/src/Array.ts +4 -4
  44. package/src/Effect.ts +6 -1
  45. package/src/Graph.ts +415 -218
  46. package/src/HashRing.ts +387 -0
  47. package/src/JSONSchema.ts +39 -9
  48. package/src/TestClock.ts +9 -9
  49. package/src/Types.ts +3 -1
  50. package/src/index.ts +6 -0
  51. package/src/internal/version.ts +1 -1
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
  }
@@ -1517,16 +1522,21 @@ export const neighbors = <N, E, T extends Kind = "directed">(
1517
1522
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1518
1523
  nodeIndex: NodeIndex
1519
1524
  ): Array<NodeIndex> => {
1520
- const adjacencyList = getMapSafe(graph.adjacency, nodeIndex)
1521
- if (Option.isNone(adjacencyList)) {
1525
+ // For undirected graphs, use the specialized helper that returns the other endpoint
1526
+ if (graph.type === "undirected") {
1527
+ return getUndirectedNeighbors(graph as any, nodeIndex)
1528
+ }
1529
+
1530
+ const adjacencyList = graph.adjacency.get(nodeIndex)
1531
+ if (adjacencyList === undefined) {
1522
1532
  return []
1523
1533
  }
1524
1534
 
1525
1535
  const result: Array<NodeIndex> = []
1526
- for (const edgeIndex of adjacencyList.value) {
1527
- const edge = getMapSafe(graph.edges, edgeIndex)
1528
- if (Option.isSome(edge)) {
1529
- 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)
1530
1540
  }
1531
1541
  }
1532
1542
 
@@ -1568,19 +1578,19 @@ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1568
1578
  ? graph.reverseAdjacency
1569
1579
  : graph.adjacency
1570
1580
 
1571
- const adjacencyList = getMapSafe(adjacencyMap, nodeIndex)
1572
- if (Option.isNone(adjacencyList)) {
1581
+ const adjacencyList = adjacencyMap.get(nodeIndex)
1582
+ if (adjacencyList === undefined) {
1573
1583
  return []
1574
1584
  }
1575
1585
 
1576
1586
  const result: Array<NodeIndex> = []
1577
- for (const edgeIndex of adjacencyList.value) {
1578
- const edge = getMapSafe(graph.edges, edgeIndex)
1579
- if (Option.isSome(edge)) {
1587
+ for (const edgeIndex of adjacencyList) {
1588
+ const edge = graph.edges.get(edgeIndex)
1589
+ if (edge !== undefined) {
1580
1590
  // For incoming direction, we want the source node instead of target
1581
1591
  const neighborNode = direction === "incoming"
1582
- ? edge.value.source
1583
- : edge.value.target
1592
+ ? edge.source
1593
+ : edge.target
1584
1594
  result.push(neighborNode)
1585
1595
  }
1586
1596
  }
@@ -1592,6 +1602,18 @@ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1592
1602
  // GraphViz Export
1593
1603
  // =============================================================================
1594
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
+
1595
1617
  /**
1596
1618
  * Exports a graph to GraphViz DOT format for visualization.
1597
1619
  *
@@ -1625,11 +1647,7 @@ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1625
1647
  */
1626
1648
  export const toGraphViz = <N, E, T extends Kind = "directed">(
1627
1649
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1628
- options?: {
1629
- readonly nodeLabel?: (data: N) => string
1630
- readonly edgeLabel?: (data: E) => string
1631
- readonly graphName?: string
1632
- }
1650
+ options?: GraphVizOptions<N, E>
1633
1651
  ): string => {
1634
1652
  const {
1635
1653
  edgeLabel = (data: E) => String(data),
@@ -1660,6 +1678,174 @@ export const toGraphViz = <N, E, T extends Kind = "directed">(
1660
1678
  return lines.join("\n")
1661
1679
  }
1662
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
+
1663
1849
  // =============================================================================
1664
1850
  // Direction Types for Bidirectional Traversal
1665
1851
  // =============================================================================
@@ -1678,10 +1864,10 @@ export const toGraphViz = <N, E, T extends Kind = "directed">(
1678
1864
  * })
1679
1865
  *
1680
1866
  * // Follow outgoing edges (normal direction)
1681
- * 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" })))
1682
1868
  *
1683
1869
  * // Follow incoming edges (reverse direction)
1684
- * 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" })))
1685
1871
  * ```
1686
1872
  *
1687
1873
  * @since 3.18.0
@@ -1902,13 +2088,13 @@ const getUndirectedNeighbors = <N, E>(
1902
2088
  const neighbors = new Set<NodeIndex>()
1903
2089
 
1904
2090
  // Check edges where this node is the source
1905
- const adjacencyList = getMapSafe(graph.adjacency, nodeIndex)
1906
- if (Option.isSome(adjacencyList)) {
1907
- for (const edgeIndex of adjacencyList.value) {
1908
- const edge = getMapSafe(graph.edges, edgeIndex)
1909
- 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) {
1910
2096
  // For undirected graphs, the neighbor is the other endpoint
1911
- const otherNode = edge.value.source === nodeIndex ? edge.value.target : edge.value.source
2097
+ const otherNode = edge.source === nodeIndex ? edge.target : edge.source
1912
2098
  neighbors.add(otherNode)
1913
2099
  }
1914
2100
  }
@@ -2072,12 +2258,12 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2072
2258
  scc.push(node)
2073
2259
 
2074
2260
  // Use reverse adjacency (transpose graph)
2075
- const reverseAdjacency = getMapSafe(graph.reverseAdjacency, node)
2076
- if (Option.isSome(reverseAdjacency)) {
2077
- for (const edgeIndex of reverseAdjacency.value) {
2078
- const edge = getMapSafe(graph.edges, edgeIndex)
2079
- if (Option.isSome(edge)) {
2080
- 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
2081
2267
  if (!visited.has(predecessor)) {
2082
2268
  stack.push(predecessor)
2083
2269
  }
@@ -2105,7 +2291,44 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2105
2291
  export interface PathResult<E> {
2106
2292
  readonly path: Array<NodeIndex>
2107
2293
  readonly distance: number
2108
- 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
2109
2332
  }
2110
2333
 
2111
2334
  /**
@@ -2127,7 +2350,7 @@ export interface PathResult<E> {
2127
2350
  * Graph.addEdge(mutable, b, c, 2)
2128
2351
  * })
2129
2352
  *
2130
- * const result = Graph.dijkstra(graph, 0, 2, (edgeData) => edgeData)
2353
+ * const result = Graph.dijkstra(graph, { source: 0, target: 2, cost: (edgeData) => edgeData })
2131
2354
  * if (Option.isSome(result)) {
2132
2355
  * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
2133
2356
  * console.log(result.value.distance) // 7 - total distance
@@ -2139,16 +2362,15 @@ export interface PathResult<E> {
2139
2362
  */
2140
2363
  export const dijkstra = <N, E, T extends Kind = "directed">(
2141
2364
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2142
- source: NodeIndex,
2143
- target: NodeIndex,
2144
- edgeWeight: (edgeData: E) => number
2365
+ config: DijkstraConfig<E>
2145
2366
  ): Option.Option<PathResult<E>> => {
2367
+ const { cost, source, target } = config
2146
2368
  // Validate that source and target nodes exist
2147
2369
  if (!graph.nodes.has(source)) {
2148
- throw new Error(`Source node ${source} does not exist`)
2370
+ throw missingNode(source)
2149
2371
  }
2150
2372
  if (!graph.nodes.has(target)) {
2151
- throw new Error(`Target node ${target} does not exist`)
2373
+ throw missingNode(target)
2152
2374
  }
2153
2375
 
2154
2376
  // Early return if source equals target
@@ -2156,7 +2378,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2156
2378
  return Option.some({
2157
2379
  path: [source],
2158
2380
  distance: 0,
2159
- edgeWeights: []
2381
+ costs: []
2160
2382
  })
2161
2383
  }
2162
2384
 
@@ -2205,13 +2427,13 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2205
2427
  const currentDistance = distances.get(currentNode)!
2206
2428
 
2207
2429
  // Examine all outgoing edges
2208
- const adjacencyList = getMapSafe(graph.adjacency, currentNode)
2209
- if (Option.isSome(adjacencyList)) {
2210
- for (const edgeIndex of adjacencyList.value) {
2211
- const edge = getMapSafe(graph.edges, edgeIndex)
2212
- if (Option.isSome(edge)) {
2213
- const neighbor = edge.value.target
2214
- 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)
2215
2437
 
2216
2438
  // Validate non-negative weights
2217
2439
  if (weight < 0) {
@@ -2224,7 +2446,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2224
2446
  // Relaxation step
2225
2447
  if (newDistance < neighborDistance) {
2226
2448
  distances.set(neighbor, newDistance)
2227
- previous.set(neighbor, { node: currentNode, edgeData: edge.value.data })
2449
+ previous.set(neighbor, { node: currentNode, edgeData: edge.data })
2228
2450
 
2229
2451
  // Add to priority queue if not visited
2230
2452
  if (!visited.has(neighbor)) {
@@ -2244,14 +2466,14 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2244
2466
 
2245
2467
  // Reconstruct path
2246
2468
  const path: Array<NodeIndex> = []
2247
- const edgeWeights: Array<E> = []
2469
+ const costs: Array<E> = []
2248
2470
  let currentNode: NodeIndex | null = target
2249
2471
 
2250
2472
  while (currentNode !== null) {
2251
2473
  path.unshift(currentNode)
2252
2474
  const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2253
2475
  if (prev !== null) {
2254
- edgeWeights.unshift(prev.edgeData)
2476
+ costs.unshift(prev.edgeData)
2255
2477
  currentNode = prev.node
2256
2478
  } else {
2257
2479
  currentNode = null
@@ -2261,7 +2483,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2261
2483
  return Option.some({
2262
2484
  path,
2263
2485
  distance: targetDistance,
2264
- edgeWeights
2486
+ costs
2265
2487
  })
2266
2488
  }
2267
2489
 
@@ -2274,7 +2496,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2274
2496
  export interface AllPairsResult<E> {
2275
2497
  readonly distances: Map<NodeIndex, Map<NodeIndex, number>>
2276
2498
  readonly paths: Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>
2277
- readonly edgeWeights: Map<NodeIndex, Map<NodeIndex, Array<E>>>
2499
+ readonly costs: Map<NodeIndex, Map<NodeIndex, Array<E>>>
2278
2500
  }
2279
2501
 
2280
2502
  /**
@@ -2306,7 +2528,7 @@ export interface AllPairsResult<E> {
2306
2528
  */
2307
2529
  export const floydWarshall = <N, E, T extends Kind = "directed">(
2308
2530
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2309
- edgeWeight: (edgeData: E) => number
2531
+ cost: (edgeData: E) => number
2310
2532
  ): AllPairsResult<E> => {
2311
2533
  // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration)
2312
2534
  const allNodes = Array.from(graph.nodes.keys())
@@ -2331,7 +2553,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2331
2553
 
2332
2554
  // Set edge weights
2333
2555
  for (const [, edgeData] of graph.edges) {
2334
- const weight = edgeWeight(edgeData.data)
2556
+ const weight = cost(edgeData.data)
2335
2557
  const i = edgeData.source
2336
2558
  const j = edgeData.target
2337
2559
 
@@ -2369,19 +2591,19 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2369
2591
 
2370
2592
  // Build result paths and edge weights
2371
2593
  const paths = new Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>()
2372
- const resultEdgeWeights = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
2594
+ const resultCosts = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
2373
2595
 
2374
2596
  for (const i of allNodes) {
2375
2597
  paths.set(i, new Map())
2376
- resultEdgeWeights.set(i, new Map())
2598
+ resultCosts.set(i, new Map())
2377
2599
 
2378
2600
  for (const j of allNodes) {
2379
2601
  if (i === j) {
2380
2602
  paths.get(i)!.set(j, [i])
2381
- resultEdgeWeights.get(i)!.set(j, [])
2603
+ resultCosts.get(i)!.set(j, [])
2382
2604
  } else if (dist.get(i)!.get(j)! === Infinity) {
2383
2605
  paths.get(i)!.set(j, null)
2384
- resultEdgeWeights.get(i)!.set(j, [])
2606
+ resultCosts.get(i)!.set(j, [])
2385
2607
  } else {
2386
2608
  // Reconstruct path iteratively
2387
2609
  const path: Array<NodeIndex> = []
@@ -2403,7 +2625,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2403
2625
  }
2404
2626
 
2405
2627
  paths.get(i)!.set(j, path)
2406
- resultEdgeWeights.get(i)!.set(j, weights)
2628
+ resultCosts.get(i)!.set(j, weights)
2407
2629
  }
2408
2630
  }
2409
2631
  }
@@ -2411,7 +2633,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2411
2633
  return {
2412
2634
  distances: dist,
2413
2635
  paths,
2414
- edgeWeights: resultEdgeWeights
2636
+ costs: resultCosts
2415
2637
  }
2416
2638
  }
2417
2639
 
@@ -2438,7 +2660,7 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2438
2660
  * const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) =>
2439
2661
  * Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y)
2440
2662
  *
2441
- * const result = Graph.astar(graph, 0, 2, (edgeData) => edgeData, heuristic)
2663
+ * const result = Graph.astar(graph, { source: 0, target: 2, cost: (edgeData) => edgeData, heuristic })
2442
2664
  * if (Option.isSome(result)) {
2443
2665
  * console.log(result.value.path) // [0, 1, 2] - shortest path
2444
2666
  * console.log(result.value.distance) // 2 - total distance
@@ -2450,17 +2672,15 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2450
2672
  */
2451
2673
  export const astar = <N, E, T extends Kind = "directed">(
2452
2674
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2453
- source: NodeIndex,
2454
- target: NodeIndex,
2455
- edgeWeight: (edgeData: E) => number,
2456
- heuristic: (sourceNodeData: N, targetNodeData: N) => number
2675
+ config: AstarConfig<E, N>
2457
2676
  ): Option.Option<PathResult<E>> => {
2677
+ const { cost, heuristic, source, target } = config
2458
2678
  // Validate that source and target nodes exist
2459
2679
  if (!graph.nodes.has(source)) {
2460
- throw new Error(`Source node ${source} does not exist`)
2680
+ throw missingNode(source)
2461
2681
  }
2462
2682
  if (!graph.nodes.has(target)) {
2463
- throw new Error(`Target node ${target} does not exist`)
2683
+ throw missingNode(target)
2464
2684
  }
2465
2685
 
2466
2686
  // Early return if source equals target
@@ -2468,13 +2688,13 @@ export const astar = <N, E, T extends Kind = "directed">(
2468
2688
  return Option.some({
2469
2689
  path: [source],
2470
2690
  distance: 0,
2471
- edgeWeights: []
2691
+ costs: []
2472
2692
  })
2473
2693
  }
2474
2694
 
2475
2695
  // Get target node data for heuristic calculations
2476
- const targetNodeData = getMapSafe(graph.nodes, target)
2477
- if (Option.isNone(targetNodeData)) {
2696
+ const targetNodeData = graph.nodes.get(target)
2697
+ if (targetNodeData === undefined) {
2478
2698
  throw new Error(`Target node ${target} data not found`)
2479
2699
  }
2480
2700
 
@@ -2493,9 +2713,9 @@ export const astar = <N, E, T extends Kind = "directed">(
2493
2713
  }
2494
2714
 
2495
2715
  // Calculate initial f-score for source
2496
- const sourceNodeData = getMapSafe(graph.nodes, source)
2497
- if (Option.isSome(sourceNodeData)) {
2498
- const h = heuristic(sourceNodeData.value, targetNodeData.value)
2716
+ const sourceNodeData = graph.nodes.get(source)
2717
+ if (sourceNodeData !== undefined) {
2718
+ const h = heuristic(sourceNodeData, targetNodeData)
2499
2719
  fScore.set(source, h)
2500
2720
  }
2501
2721
 
@@ -2532,13 +2752,13 @@ export const astar = <N, E, T extends Kind = "directed">(
2532
2752
  const currentGScore = gScore.get(currentNode)!
2533
2753
 
2534
2754
  // Examine all outgoing edges
2535
- const adjacencyList = getMapSafe(graph.adjacency, currentNode)
2536
- if (Option.isSome(adjacencyList)) {
2537
- for (const edgeIndex of adjacencyList.value) {
2538
- const edge = getMapSafe(graph.edges, edgeIndex)
2539
- if (Option.isSome(edge)) {
2540
- const neighbor = edge.value.target
2541
- 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)
2542
2762
 
2543
2763
  // Validate non-negative weights
2544
2764
  if (weight < 0) {
@@ -2552,12 +2772,12 @@ export const astar = <N, E, T extends Kind = "directed">(
2552
2772
  if (tentativeGScore < neighborGScore) {
2553
2773
  // Update g-score and previous
2554
2774
  gScore.set(neighbor, tentativeGScore)
2555
- previous.set(neighbor, { node: currentNode, edgeData: edge.value.data })
2775
+ previous.set(neighbor, { node: currentNode, edgeData: edge.data })
2556
2776
 
2557
2777
  // Calculate f-score using heuristic
2558
- const neighborNodeData = getMapSafe(graph.nodes, neighbor)
2559
- if (Option.isSome(neighborNodeData)) {
2560
- const h = heuristic(neighborNodeData.value, targetNodeData.value)
2778
+ const neighborNodeData = graph.nodes.get(neighbor)
2779
+ if (neighborNodeData !== undefined) {
2780
+ const h = heuristic(neighborNodeData, targetNodeData)
2561
2781
  const f = tentativeGScore + h
2562
2782
  fScore.set(neighbor, f)
2563
2783
 
@@ -2580,14 +2800,14 @@ export const astar = <N, E, T extends Kind = "directed">(
2580
2800
 
2581
2801
  // Reconstruct path
2582
2802
  const path: Array<NodeIndex> = []
2583
- const edgeWeights: Array<E> = []
2803
+ const costs: Array<E> = []
2584
2804
  let currentNode: NodeIndex | null = target
2585
2805
 
2586
2806
  while (currentNode !== null) {
2587
2807
  path.unshift(currentNode)
2588
2808
  const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2589
2809
  if (prev !== null) {
2590
- edgeWeights.unshift(prev.edgeData)
2810
+ costs.unshift(prev.edgeData)
2591
2811
  currentNode = prev.node
2592
2812
  } else {
2593
2813
  currentNode = null
@@ -2597,7 +2817,7 @@ export const astar = <N, E, T extends Kind = "directed">(
2597
2817
  return Option.some({
2598
2818
  path,
2599
2819
  distance: targetGScore,
2600
- edgeWeights
2820
+ costs
2601
2821
  })
2602
2822
  }
2603
2823
 
@@ -2621,7 +2841,7 @@ export const astar = <N, E, T extends Kind = "directed">(
2621
2841
  * Graph.addEdge(mutable, a, c, 5)
2622
2842
  * })
2623
2843
  *
2624
- * const result = Graph.bellmanFord(graph, 0, 2, (edgeData) => edgeData)
2844
+ * const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edgeData) => edgeData })
2625
2845
  * if (Option.isSome(result)) {
2626
2846
  * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
2627
2847
  * console.log(result.value.distance) // 2 - total distance
@@ -2633,16 +2853,15 @@ export const astar = <N, E, T extends Kind = "directed">(
2633
2853
  */
2634
2854
  export const bellmanFord = <N, E, T extends Kind = "directed">(
2635
2855
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2636
- source: NodeIndex,
2637
- target: NodeIndex,
2638
- edgeWeight: (edgeData: E) => number
2856
+ config: BellmanFordConfig<E>
2639
2857
  ): Option.Option<PathResult<E>> => {
2858
+ const { cost, source, target } = config
2640
2859
  // Validate that source and target nodes exist
2641
2860
  if (!graph.nodes.has(source)) {
2642
- throw new Error(`Source node ${source} does not exist`)
2861
+ throw missingNode(source)
2643
2862
  }
2644
2863
  if (!graph.nodes.has(target)) {
2645
- throw new Error(`Target node ${target} does not exist`)
2864
+ throw missingNode(target)
2646
2865
  }
2647
2866
 
2648
2867
  // Early return if source equals target
@@ -2650,7 +2869,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2650
2869
  return Option.some({
2651
2870
  path: [source],
2652
2871
  distance: 0,
2653
- edgeWeights: []
2872
+ costs: []
2654
2873
  })
2655
2874
  }
2656
2875
 
@@ -2667,7 +2886,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2667
2886
  // Collect all edges for relaxation
2668
2887
  const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = []
2669
2888
  for (const [, edgeData] of graph.edges) {
2670
- const weight = edgeWeight(edgeData.data)
2889
+ const weight = cost(edgeData.data)
2671
2890
  edges.push({
2672
2891
  source: edgeData.source,
2673
2892
  target: edgeData.target,
@@ -2715,12 +2934,12 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2715
2934
  affectedNodes.add(node)
2716
2935
 
2717
2936
  // Add all nodes reachable from this node
2718
- const adjacencyList = getMapSafe(graph.adjacency, node)
2719
- if (Option.isSome(adjacencyList)) {
2720
- for (const edgeIndex of adjacencyList.value) {
2721
- const edge = getMapSafe(graph.edges, edgeIndex)
2722
- if (Option.isSome(edge)) {
2723
- 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)
2724
2943
  }
2725
2944
  }
2726
2945
  }
@@ -2741,14 +2960,14 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2741
2960
 
2742
2961
  // Reconstruct path
2743
2962
  const path: Array<NodeIndex> = []
2744
- const edgeWeights: Array<E> = []
2963
+ const costs: Array<E> = []
2745
2964
  let currentNode: NodeIndex | null = target
2746
2965
 
2747
2966
  while (currentNode !== null) {
2748
2967
  path.unshift(currentNode)
2749
2968
  const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2750
2969
  if (prev !== null) {
2751
- edgeWeights.unshift(prev.edgeData)
2970
+ costs.unshift(prev.edgeData)
2752
2971
  currentNode = prev.node
2753
2972
  } else {
2754
2973
  currentNode = null
@@ -2758,7 +2977,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2758
2977
  return Option.some({
2759
2978
  path,
2760
2979
  distance: targetDistance,
2761
- edgeWeights
2980
+ costs
2762
2981
  })
2763
2982
  }
2764
2983
 
@@ -2780,7 +2999,7 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2780
2999
  * })
2781
3000
  *
2782
3001
  * // Both traversal and element iterators return NodeWalker
2783
- * const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { startNodes: [0] })
3002
+ * const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { start: [0] })
2784
3003
  * const allNodes: Graph.NodeWalker<string> = Graph.nodes(graph)
2785
3004
  *
2786
3005
  * // Common interface for working with node iterables
@@ -2817,7 +3036,7 @@ export class Walker<T, N> implements Iterable<[T, N]> {
2817
3036
  * Graph.addEdge(mutable, a, b, 1)
2818
3037
  * })
2819
3038
  *
2820
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3039
+ * const dfs = Graph.dfs(graph, { start: [0] })
2821
3040
  *
2822
3041
  * // Map to just the node data
2823
3042
  * const values = Array.from(dfs.visit((index, data) => data))
@@ -2851,7 +3070,7 @@ export class Walker<T, N> implements Iterable<[T, N]> {
2851
3070
  * Graph.addEdge(mutable, a, b, 1)
2852
3071
  * })
2853
3072
  *
2854
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3073
+ * const dfs = Graph.dfs(graph, { start: [0] })
2855
3074
  *
2856
3075
  * // Map to just the node data
2857
3076
  * const values = Array.from(dfs.visit((index, data) => data))
@@ -2903,7 +3122,7 @@ export type EdgeWalker<E> = Walker<EdgeIndex, Edge<E>>
2903
3122
  * Graph.addEdge(mutable, a, b, 1)
2904
3123
  * })
2905
3124
  *
2906
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3125
+ * const dfs = Graph.dfs(graph, { start: [0] })
2907
3126
  * const indices = Array.from(Graph.indices(dfs))
2908
3127
  * console.log(indices) // [0, 1]
2909
3128
  * ```
@@ -2926,7 +3145,7 @@ export const indices = <T, N>(walker: Walker<T, N>): Iterable<T> => walker.visit
2926
3145
  * Graph.addEdge(mutable, a, b, 1)
2927
3146
  * })
2928
3147
  *
2929
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3148
+ * const dfs = Graph.dfs(graph, { start: [0] })
2930
3149
  * const values = Array.from(Graph.values(dfs))
2931
3150
  * console.log(values) // ["A", "B"]
2932
3151
  * ```
@@ -2949,7 +3168,7 @@ export const values = <T, N>(walker: Walker<T, N>): Iterable<N> => walker.visit(
2949
3168
  * Graph.addEdge(mutable, a, b, 1)
2950
3169
  * })
2951
3170
  *
2952
- * const dfs = Graph.dfs(graph, { startNodes: [0] })
3171
+ * const dfs = Graph.dfs(graph, { start: [0] })
2953
3172
  * const entries = Array.from(Graph.entries(dfs))
2954
3173
  * console.log(entries) // [[0, "A"], [1, "B"]]
2955
3174
  * ```
@@ -2961,13 +3180,13 @@ export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
2961
3180
  walker.visit((index, data) => [index, data] as [T, N])
2962
3181
 
2963
3182
  /**
2964
- * Configuration options for DFS iterator.
3183
+ * Configuration for graph search iterators.
2965
3184
  *
2966
3185
  * @since 3.18.0
2967
3186
  * @category models
2968
3187
  */
2969
- export interface DfsConfig {
2970
- readonly startNodes?: Array<NodeIndex>
3188
+ export interface SearchConfig {
3189
+ readonly start?: Array<NodeIndex>
2971
3190
  readonly direction?: Direction
2972
3191
  }
2973
3192
 
@@ -2990,7 +3209,7 @@ export interface DfsConfig {
2990
3209
  * })
2991
3210
  *
2992
3211
  * // Start from a specific node
2993
- * const dfs1 = Graph.dfs(graph, { startNodes: [0] })
3212
+ * const dfs1 = Graph.dfs(graph, { start: [0] })
2994
3213
  * for (const nodeIndex of Graph.indices(dfs1)) {
2995
3214
  * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2
2996
3215
  * }
@@ -3005,21 +3224,21 @@ export interface DfsConfig {
3005
3224
  */
3006
3225
  export const dfs = <N, E, T extends Kind = "directed">(
3007
3226
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3008
- config: DfsConfig = {}
3227
+ config: SearchConfig = {}
3009
3228
  ): NodeWalker<N> => {
3010
- const startNodes = config.startNodes ?? []
3229
+ const start = config.start ?? []
3011
3230
  const direction = config.direction ?? "outgoing"
3012
3231
 
3013
3232
  // Validate that all start nodes exist
3014
- for (const nodeIndex of startNodes) {
3233
+ for (const nodeIndex of start) {
3015
3234
  if (!hasNode(graph, nodeIndex)) {
3016
- throw new Error(`Start node ${nodeIndex} does not exist`)
3235
+ throw missingNode(nodeIndex)
3017
3236
  }
3018
3237
  }
3019
3238
 
3020
3239
  return new Walker((f) => ({
3021
3240
  [Symbol.iterator]: () => {
3022
- const stack = [...startNodes]
3241
+ const stack = [...start]
3023
3242
  const discovered = new Set<NodeIndex>()
3024
3243
 
3025
3244
  const nextMapped = () => {
@@ -3032,8 +3251,8 @@ export const dfs = <N, E, T extends Kind = "directed">(
3032
3251
 
3033
3252
  discovered.add(current)
3034
3253
 
3035
- const nodeDataOption = getMapSafe(graph.nodes, current)
3036
- if (Option.isNone(nodeDataOption)) {
3254
+ const nodeDataOption = graph.nodes.get(current)
3255
+ if (nodeDataOption === undefined) {
3037
3256
  continue
3038
3257
  }
3039
3258
 
@@ -3045,7 +3264,7 @@ export const dfs = <N, E, T extends Kind = "directed">(
3045
3264
  }
3046
3265
  }
3047
3266
 
3048
- return { done: false, value: f(current, nodeDataOption.value) }
3267
+ return { done: false, value: f(current, nodeDataOption) }
3049
3268
  }
3050
3269
 
3051
3270
  return { done: true, value: undefined } as const
@@ -3056,17 +3275,6 @@ export const dfs = <N, E, T extends Kind = "directed">(
3056
3275
  }))
3057
3276
  }
3058
3277
 
3059
- /**
3060
- * Configuration options for BFS iterator.
3061
- *
3062
- * @since 3.18.0
3063
- * @category models
3064
- */
3065
- export interface BfsConfig {
3066
- readonly startNodes?: Array<NodeIndex>
3067
- readonly direction?: Direction
3068
- }
3069
-
3070
3278
  /**
3071
3279
  * Creates a new BFS iterator with optional configuration.
3072
3280
  *
@@ -3086,7 +3294,7 @@ export interface BfsConfig {
3086
3294
  * })
3087
3295
  *
3088
3296
  * // Start from a specific node
3089
- * const bfs1 = Graph.bfs(graph, { startNodes: [0] })
3297
+ * const bfs1 = Graph.bfs(graph, { start: [0] })
3090
3298
  * for (const nodeIndex of Graph.indices(bfs1)) {
3091
3299
  * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2
3092
3300
  * }
@@ -3101,21 +3309,21 @@ export interface BfsConfig {
3101
3309
  */
3102
3310
  export const bfs = <N, E, T extends Kind = "directed">(
3103
3311
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3104
- config: BfsConfig = {}
3312
+ config: SearchConfig = {}
3105
3313
  ): NodeWalker<N> => {
3106
- const startNodes = config.startNodes ?? []
3314
+ const start = config.start ?? []
3107
3315
  const direction = config.direction ?? "outgoing"
3108
3316
 
3109
3317
  // Validate that all start nodes exist
3110
- for (const nodeIndex of startNodes) {
3318
+ for (const nodeIndex of start) {
3111
3319
  if (!hasNode(graph, nodeIndex)) {
3112
- throw new Error(`Start node ${nodeIndex} does not exist`)
3320
+ throw missingNode(nodeIndex)
3113
3321
  }
3114
3322
  }
3115
3323
 
3116
3324
  return new Walker((f) => ({
3117
3325
  [Symbol.iterator]: () => {
3118
- const queue = [...startNodes]
3326
+ const queue = [...start]
3119
3327
  const discovered = new Set<NodeIndex>()
3120
3328
 
3121
3329
  const nextMapped = () => {
@@ -3217,7 +3425,7 @@ export const topo = <N, E, T extends Kind = "directed">(
3217
3425
  // Validate that all initial nodes exist
3218
3426
  for (const nodeIndex of initials) {
3219
3427
  if (!hasNode(graph, nodeIndex)) {
3220
- throw new Error(`Initial node ${nodeIndex} does not exist`)
3428
+ throw missingNode(nodeIndex)
3221
3429
  }
3222
3430
  }
3223
3431
 
@@ -3286,17 +3494,6 @@ export const topo = <N, E, T extends Kind = "directed">(
3286
3494
  }))
3287
3495
  }
3288
3496
 
3289
- /**
3290
- * Configuration options for DFS postorder iterator.
3291
- *
3292
- * @since 3.18.0
3293
- * @category models
3294
- */
3295
- export interface DfsPostOrderConfig {
3296
- readonly startNodes?: Array<NodeIndex>
3297
- readonly direction?: Direction
3298
- }
3299
-
3300
3497
  /**
3301
3498
  * Creates a new DFS postorder iterator with optional configuration.
3302
3499
  *
@@ -3317,7 +3514,7 @@ export interface DfsPostOrderConfig {
3317
3514
  * })
3318
3515
  *
3319
3516
  * // Postorder: children before parents
3320
- * const postOrder = Graph.dfsPostOrder(graph, { startNodes: [0] })
3517
+ * const postOrder = Graph.dfsPostOrder(graph, { start: [0] })
3321
3518
  * for (const node of postOrder) {
3322
3519
  * console.log(node) // 1, 2, 0
3323
3520
  * }
@@ -3328,15 +3525,15 @@ export interface DfsPostOrderConfig {
3328
3525
  */
3329
3526
  export const dfsPostOrder = <N, E, T extends Kind = "directed">(
3330
3527
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3331
- config: DfsPostOrderConfig = {}
3528
+ config: SearchConfig = {}
3332
3529
  ): NodeWalker<N> => {
3333
- const startNodes = config.startNodes ?? []
3530
+ const start = config.start ?? []
3334
3531
  const direction = config.direction ?? "outgoing"
3335
3532
 
3336
3533
  // Validate that all start nodes exist
3337
- for (const nodeIndex of startNodes) {
3534
+ for (const nodeIndex of start) {
3338
3535
  if (!hasNode(graph, nodeIndex)) {
3339
- throw new Error(`Start node ${nodeIndex} does not exist`)
3536
+ throw missingNode(nodeIndex)
3340
3537
  }
3341
3538
  }
3342
3539
 
@@ -3347,8 +3544,8 @@ export const dfsPostOrder = <N, E, T extends Kind = "directed">(
3347
3544
  const finished = new Set<NodeIndex>()
3348
3545
 
3349
3546
  // Initialize stack with start nodes
3350
- for (let i = startNodes.length - 1; i >= 0; i--) {
3351
- 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 })
3352
3549
  }
3353
3550
 
3354
3551
  const nextMapped = () => {
@@ -3546,10 +3743,10 @@ export const externals = <N, E, T extends Kind = "directed">(
3546
3743
  let current = nodeIterator.next()
3547
3744
  while (!current.done) {
3548
3745
  const [nodeIndex, nodeData] = current.value
3549
- const adjacencyList = getMapSafe(adjacencyMap, nodeIndex)
3746
+ const adjacencyList = adjacencyMap.get(nodeIndex)
3550
3747
 
3551
3748
  // Node is external if it has no edges in the specified direction
3552
- if (Option.isNone(adjacencyList) || adjacencyList.value.length === 0) {
3749
+ if (adjacencyList === undefined || adjacencyList.length === 0) {
3553
3750
  return { done: false, value: f(nodeIndex, nodeData) }
3554
3751
  }
3555
3752
  current = nodeIterator.next()