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.
- 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 +290 -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/JSONSchema.js +39 -8
- package/dist/cjs/JSONSchema.js.map +1 -1
- package/dist/cjs/TestClock.js +8 -8
- package/dist/cjs/TestClock.js.map +1 -1
- 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 +6 -1
- 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/JSONSchema.d.ts +3 -2
- package/dist/dts/JSONSchema.d.ts.map +1 -1
- 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 +286 -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/JSONSchema.js +35 -6
- package/dist/esm/JSONSchema.js.map +1 -1
- package/dist/esm/TestClock.js +8 -8
- package/dist/esm/TestClock.js.map +1 -1
- 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 +6 -1
- package/src/Graph.ts +415 -218
- package/src/HashRing.ts +387 -0
- package/src/JSONSchema.ts +39 -9
- package/src/TestClock.ts +9 -9
- 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
|
}
|
|
@@ -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
|
-
|
|
1521
|
-
if (
|
|
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
|
|
1527
|
-
const edge =
|
|
1528
|
-
if (
|
|
1529
|
-
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)
|
|
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 =
|
|
1572
|
-
if (
|
|
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
|
|
1578
|
-
const edge =
|
|
1579
|
-
if (
|
|
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.
|
|
1583
|
-
: edge.
|
|
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, {
|
|
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, {
|
|
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 =
|
|
1906
|
-
if (
|
|
1907
|
-
for (const edgeIndex of adjacencyList
|
|
1908
|
-
const edge =
|
|
1909
|
-
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) {
|
|
1910
2096
|
// For undirected graphs, the neighbor is the other endpoint
|
|
1911
|
-
const otherNode = edge.
|
|
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 =
|
|
2076
|
-
if (
|
|
2077
|
-
for (const edgeIndex of reverseAdjacency
|
|
2078
|
-
const edge =
|
|
2079
|
-
if (
|
|
2080
|
-
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2370
|
+
throw missingNode(source)
|
|
2149
2371
|
}
|
|
2150
2372
|
if (!graph.nodes.has(target)) {
|
|
2151
|
-
throw
|
|
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
|
-
|
|
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 =
|
|
2209
|
-
if (
|
|
2210
|
-
for (const edgeIndex of adjacencyList
|
|
2211
|
-
const edge =
|
|
2212
|
-
if (
|
|
2213
|
-
const neighbor = edge.
|
|
2214
|
-
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)
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2680
|
+
throw missingNode(source)
|
|
2461
2681
|
}
|
|
2462
2682
|
if (!graph.nodes.has(target)) {
|
|
2463
|
-
throw
|
|
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
|
-
|
|
2691
|
+
costs: []
|
|
2472
2692
|
})
|
|
2473
2693
|
}
|
|
2474
2694
|
|
|
2475
2695
|
// Get target node data for heuristic calculations
|
|
2476
|
-
const targetNodeData =
|
|
2477
|
-
if (
|
|
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 =
|
|
2497
|
-
if (
|
|
2498
|
-
const h = heuristic(sourceNodeData
|
|
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 =
|
|
2536
|
-
if (
|
|
2537
|
-
for (const edgeIndex of adjacencyList
|
|
2538
|
-
const edge =
|
|
2539
|
-
if (
|
|
2540
|
-
const neighbor = edge.
|
|
2541
|
-
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)
|
|
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.
|
|
2775
|
+
previous.set(neighbor, { node: currentNode, edgeData: edge.data })
|
|
2556
2776
|
|
|
2557
2777
|
// Calculate f-score using heuristic
|
|
2558
|
-
const neighborNodeData =
|
|
2559
|
-
if (
|
|
2560
|
-
const h = heuristic(neighborNodeData
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2861
|
+
throw missingNode(source)
|
|
2643
2862
|
}
|
|
2644
2863
|
if (!graph.nodes.has(target)) {
|
|
2645
|
-
throw
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
2719
|
-
if (
|
|
2720
|
-
for (const edgeIndex of adjacencyList
|
|
2721
|
-
const edge =
|
|
2722
|
-
if (
|
|
2723
|
-
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
|
3183
|
+
* Configuration for graph search iterators.
|
|
2965
3184
|
*
|
|
2966
3185
|
* @since 3.18.0
|
|
2967
3186
|
* @category models
|
|
2968
3187
|
*/
|
|
2969
|
-
export interface
|
|
2970
|
-
readonly
|
|
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, {
|
|
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:
|
|
3227
|
+
config: SearchConfig = {}
|
|
3009
3228
|
): NodeWalker<N> => {
|
|
3010
|
-
const
|
|
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
|
|
3233
|
+
for (const nodeIndex of start) {
|
|
3015
3234
|
if (!hasNode(graph, nodeIndex)) {
|
|
3016
|
-
throw
|
|
3235
|
+
throw missingNode(nodeIndex)
|
|
3017
3236
|
}
|
|
3018
3237
|
}
|
|
3019
3238
|
|
|
3020
3239
|
return new Walker((f) => ({
|
|
3021
3240
|
[Symbol.iterator]: () => {
|
|
3022
|
-
const stack = [...
|
|
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 =
|
|
3036
|
-
if (
|
|
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
|
|
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, {
|
|
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:
|
|
3312
|
+
config: SearchConfig = {}
|
|
3105
3313
|
): NodeWalker<N> => {
|
|
3106
|
-
const
|
|
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
|
|
3318
|
+
for (const nodeIndex of start) {
|
|
3111
3319
|
if (!hasNode(graph, nodeIndex)) {
|
|
3112
|
-
throw
|
|
3320
|
+
throw missingNode(nodeIndex)
|
|
3113
3321
|
}
|
|
3114
3322
|
}
|
|
3115
3323
|
|
|
3116
3324
|
return new Walker((f) => ({
|
|
3117
3325
|
[Symbol.iterator]: () => {
|
|
3118
|
-
const queue = [...
|
|
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
|
|
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, {
|
|
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:
|
|
3528
|
+
config: SearchConfig = {}
|
|
3332
3529
|
): NodeWalker<N> => {
|
|
3333
|
-
const
|
|
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
|
|
3534
|
+
for (const nodeIndex of start) {
|
|
3338
3535
|
if (!hasNode(graph, nodeIndex)) {
|
|
3339
|
-
throw
|
|
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 =
|
|
3351
|
-
stack.push({ node:
|
|
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 =
|
|
3746
|
+
const adjacencyList = adjacencyMap.get(nodeIndex)
|
|
3550
3747
|
|
|
3551
3748
|
// Node is external if it has no edges in the specified direction
|
|
3552
|
-
if (
|
|
3749
|
+
if (adjacencyList === undefined || adjacencyList.length === 0) {
|
|
3553
3750
|
return { done: false, value: f(nodeIndex, nodeData) }
|
|
3554
3751
|
}
|
|
3555
3752
|
current = nodeIterator.next()
|