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/HashRing/package.json +6 -0
- package/dist/cjs/Array.js.map +1 -1
- package/dist/cjs/Effect.js.map +1 -1
- package/dist/cjs/Graph.js +286 -177
- package/dist/cjs/Graph.js.map +1 -1
- package/dist/cjs/HashRing.js +257 -0
- package/dist/cjs/HashRing.js.map +1 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internal/version.js +1 -1
- package/dist/dts/Array.d.ts +3 -3
- package/dist/dts/Array.d.ts.map +1 -1
- package/dist/dts/Effect.d.ts +5 -0
- package/dist/dts/Effect.d.ts.map +1 -1
- package/dist/dts/Graph.d.ts +147 -49
- package/dist/dts/Graph.d.ts.map +1 -1
- package/dist/dts/HashRing.d.ts +158 -0
- package/dist/dts/HashRing.d.ts.map +1 -0
- package/dist/dts/Types.d.ts +1 -1
- package/dist/dts/Types.d.ts.map +1 -1
- package/dist/dts/index.d.ts +5 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/Array.js.map +1 -1
- package/dist/esm/Effect.js.map +1 -1
- package/dist/esm/Graph.js +282 -175
- package/dist/esm/Graph.js.map +1 -1
- package/dist/esm/HashRing.js +245 -0
- package/dist/esm/HashRing.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/version.js +1 -1
- package/package.json +9 -1
- package/src/Array.ts +4 -4
- package/src/Effect.ts +5 -0
- package/src/Graph.ts +410 -218
- package/src/HashRing.ts +387 -0
- package/src/Types.ts +3 -1
- package/src/index.ts +6 -0
- 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> =>
|
|
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
|
|
1182
|
+
throw missingNode(source)
|
|
1178
1183
|
}
|
|
1179
1184
|
if (!mutable.nodes.has(target)) {
|
|
1180
|
-
throw
|
|
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 =
|
|
1191
|
-
if (
|
|
1192
|
-
sourceAdjacency.
|
|
1195
|
+
const sourceAdjacency = mutable.adjacency.get(source)
|
|
1196
|
+
if (sourceAdjacency !== undefined) {
|
|
1197
|
+
sourceAdjacency.push(edgeIndex)
|
|
1193
1198
|
}
|
|
1194
1199
|
|
|
1195
|
-
const targetReverseAdjacency =
|
|
1196
|
-
if (
|
|
1197
|
-
targetReverseAdjacency.
|
|
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 =
|
|
1203
|
-
if (
|
|
1204
|
-
targetAdjacency.
|
|
1207
|
+
const targetAdjacency = mutable.adjacency.get(target)
|
|
1208
|
+
if (targetAdjacency !== undefined) {
|
|
1209
|
+
targetAdjacency.push(edgeIndex)
|
|
1205
1210
|
}
|
|
1206
1211
|
|
|
1207
|
-
const sourceReverseAdjacency =
|
|
1208
|
-
if (
|
|
1209
|
-
sourceReverseAdjacency.
|
|
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 =
|
|
1257
|
-
if (
|
|
1258
|
-
for (const edge of outgoingEdges
|
|
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 =
|
|
1265
|
-
if (
|
|
1266
|
-
for (const edge of incomingEdges
|
|
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 =
|
|
1326
|
-
if (
|
|
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
|
|
1335
|
+
const { source, target } = edge
|
|
1331
1336
|
|
|
1332
1337
|
// Remove from adjacency lists
|
|
1333
|
-
const sourceAdjacency =
|
|
1334
|
-
if (
|
|
1335
|
-
const index = sourceAdjacency.
|
|
1338
|
+
const sourceAdjacency = mutable.adjacency.get(source)
|
|
1339
|
+
if (sourceAdjacency !== undefined) {
|
|
1340
|
+
const index = sourceAdjacency.indexOf(edgeIndex)
|
|
1336
1341
|
if (index !== -1) {
|
|
1337
|
-
sourceAdjacency.
|
|
1342
|
+
sourceAdjacency.splice(index, 1)
|
|
1338
1343
|
}
|
|
1339
1344
|
}
|
|
1340
1345
|
|
|
1341
|
-
const targetReverseAdjacency =
|
|
1342
|
-
if (
|
|
1343
|
-
const index = targetReverseAdjacency.
|
|
1346
|
+
const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
|
|
1347
|
+
if (targetReverseAdjacency !== undefined) {
|
|
1348
|
+
const index = targetReverseAdjacency.indexOf(edgeIndex)
|
|
1344
1349
|
if (index !== -1) {
|
|
1345
|
-
targetReverseAdjacency.
|
|
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 =
|
|
1352
|
-
if (
|
|
1353
|
-
const index = targetAdjacency.
|
|
1356
|
+
const targetAdjacency = mutable.adjacency.get(target)
|
|
1357
|
+
if (targetAdjacency !== undefined) {
|
|
1358
|
+
const index = targetAdjacency.indexOf(edgeIndex)
|
|
1354
1359
|
if (index !== -1) {
|
|
1355
|
-
targetAdjacency.
|
|
1360
|
+
targetAdjacency.splice(index, 1)
|
|
1356
1361
|
}
|
|
1357
1362
|
}
|
|
1358
1363
|
|
|
1359
|
-
const sourceReverseAdjacency =
|
|
1360
|
-
if (
|
|
1361
|
-
const index = sourceReverseAdjacency.
|
|
1364
|
+
const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
|
|
1365
|
+
if (sourceReverseAdjacency !== undefined) {
|
|
1366
|
+
const index = sourceReverseAdjacency.indexOf(edgeIndex)
|
|
1362
1367
|
if (index !== -1) {
|
|
1363
|
-
sourceReverseAdjacency.
|
|
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>> =>
|
|
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 =
|
|
1443
|
-
if (
|
|
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
|
|
1449
|
-
const edge =
|
|
1450
|
-
if (
|
|
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 =
|
|
1526
|
-
if (
|
|
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
|
|
1532
|
-
const edge =
|
|
1533
|
-
if (
|
|
1534
|
-
result.push(edge.
|
|
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 =
|
|
1577
|
-
if (
|
|
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
|
|
1583
|
-
const edge =
|
|
1584
|
-
if (
|
|
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.
|
|
1588
|
-
: edge.
|
|
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, {
|
|
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, {
|
|
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 =
|
|
1911
|
-
if (
|
|
1912
|
-
for (const edgeIndex of adjacencyList
|
|
1913
|
-
const edge =
|
|
1914
|
-
if (
|
|
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.
|
|
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 =
|
|
2081
|
-
if (
|
|
2082
|
-
for (const edgeIndex of reverseAdjacency
|
|
2083
|
-
const edge =
|
|
2084
|
-
if (
|
|
2085
|
-
const predecessor = edge.
|
|
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
|
|
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
|
-
|
|
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
|
|
2370
|
+
throw missingNode(source)
|
|
2154
2371
|
}
|
|
2155
2372
|
if (!graph.nodes.has(target)) {
|
|
2156
|
-
throw
|
|
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
|
-
|
|
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 =
|
|
2214
|
-
if (
|
|
2215
|
-
for (const edgeIndex of adjacencyList
|
|
2216
|
-
const edge =
|
|
2217
|
-
if (
|
|
2218
|
-
const neighbor = edge.
|
|
2219
|
-
const weight =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2680
|
+
throw missingNode(source)
|
|
2466
2681
|
}
|
|
2467
2682
|
if (!graph.nodes.has(target)) {
|
|
2468
|
-
throw
|
|
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
|
-
|
|
2691
|
+
costs: []
|
|
2477
2692
|
})
|
|
2478
2693
|
}
|
|
2479
2694
|
|
|
2480
2695
|
// Get target node data for heuristic calculations
|
|
2481
|
-
const targetNodeData =
|
|
2482
|
-
if (
|
|
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 =
|
|
2502
|
-
if (
|
|
2503
|
-
const h = heuristic(sourceNodeData
|
|
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 =
|
|
2541
|
-
if (
|
|
2542
|
-
for (const edgeIndex of adjacencyList
|
|
2543
|
-
const edge =
|
|
2544
|
-
if (
|
|
2545
|
-
const neighbor = edge.
|
|
2546
|
-
const weight =
|
|
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.
|
|
2775
|
+
previous.set(neighbor, { node: currentNode, edgeData: edge.data })
|
|
2561
2776
|
|
|
2562
2777
|
// Calculate f-score using heuristic
|
|
2563
|
-
const neighborNodeData =
|
|
2564
|
-
if (
|
|
2565
|
-
const h = heuristic(neighborNodeData
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2861
|
+
throw missingNode(source)
|
|
2648
2862
|
}
|
|
2649
2863
|
if (!graph.nodes.has(target)) {
|
|
2650
|
-
throw
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
2724
|
-
if (
|
|
2725
|
-
for (const edgeIndex of adjacencyList
|
|
2726
|
-
const edge =
|
|
2727
|
-
if (
|
|
2728
|
-
queue.push(edge.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
|
3183
|
+
* Configuration for graph search iterators.
|
|
2970
3184
|
*
|
|
2971
3185
|
* @since 3.18.0
|
|
2972
3186
|
* @category models
|
|
2973
3187
|
*/
|
|
2974
|
-
export interface
|
|
2975
|
-
readonly
|
|
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, {
|
|
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:
|
|
3227
|
+
config: SearchConfig = {}
|
|
3014
3228
|
): NodeWalker<N> => {
|
|
3015
|
-
const
|
|
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
|
|
3233
|
+
for (const nodeIndex of start) {
|
|
3020
3234
|
if (!hasNode(graph, nodeIndex)) {
|
|
3021
|
-
throw
|
|
3235
|
+
throw missingNode(nodeIndex)
|
|
3022
3236
|
}
|
|
3023
3237
|
}
|
|
3024
3238
|
|
|
3025
3239
|
return new Walker((f) => ({
|
|
3026
3240
|
[Symbol.iterator]: () => {
|
|
3027
|
-
const stack = [...
|
|
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 =
|
|
3041
|
-
if (
|
|
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
|
|
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, {
|
|
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:
|
|
3312
|
+
config: SearchConfig = {}
|
|
3110
3313
|
): NodeWalker<N> => {
|
|
3111
|
-
const
|
|
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
|
|
3318
|
+
for (const nodeIndex of start) {
|
|
3116
3319
|
if (!hasNode(graph, nodeIndex)) {
|
|
3117
|
-
throw
|
|
3320
|
+
throw missingNode(nodeIndex)
|
|
3118
3321
|
}
|
|
3119
3322
|
}
|
|
3120
3323
|
|
|
3121
3324
|
return new Walker((f) => ({
|
|
3122
3325
|
[Symbol.iterator]: () => {
|
|
3123
|
-
const queue = [...
|
|
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
|
|
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, {
|
|
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:
|
|
3528
|
+
config: SearchConfig = {}
|
|
3337
3529
|
): NodeWalker<N> => {
|
|
3338
|
-
const
|
|
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
|
|
3534
|
+
for (const nodeIndex of start) {
|
|
3343
3535
|
if (!hasNode(graph, nodeIndex)) {
|
|
3344
|
-
throw
|
|
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 =
|
|
3356
|
-
stack.push({ node:
|
|
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 =
|
|
3746
|
+
const adjacencyList = adjacencyMap.get(nodeIndex)
|
|
3555
3747
|
|
|
3556
3748
|
// Node is external if it has no edges in the specified direction
|
|
3557
|
-
if (
|
|
3749
|
+
if (adjacencyList === undefined || adjacencyList.length === 0) {
|
|
3558
3750
|
return { done: false, value: f(nodeIndex, nodeData) }
|
|
3559
3751
|
}
|
|
3560
3752
|
current = nodeIterator.next()
|