effect 3.21.4 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Graph.ts CHANGED
@@ -872,6 +872,29 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
872
872
  }
873
873
  }
874
874
 
875
+ /** @internal */
876
+ const rebuildAdjacency = <N, E, T extends Kind = "directed">(
877
+ mutable: MutableGraph<N, E, T>
878
+ ): void => {
879
+ mutable.adjacency.clear()
880
+ mutable.reverseAdjacency.clear()
881
+
882
+ for (const nodeIndex of mutable.nodes.keys()) {
883
+ mutable.adjacency.set(nodeIndex, [])
884
+ mutable.reverseAdjacency.set(nodeIndex, [])
885
+ }
886
+
887
+ for (const [edgeIndex, edgeData] of mutable.edges) {
888
+ mutable.adjacency.get(edgeData.source)!.push(edgeIndex)
889
+ mutable.reverseAdjacency.get(edgeData.target)!.push(edgeIndex)
890
+
891
+ if (mutable.type === "undirected") {
892
+ mutable.adjacency.get(edgeData.target)!.push(edgeIndex)
893
+ mutable.reverseAdjacency.get(edgeData.source)!.push(edgeIndex)
894
+ }
895
+ }
896
+ }
897
+
875
898
  /**
876
899
  * Reverses all edge directions in a mutable graph by swapping source and target nodes.
877
900
  *
@@ -898,6 +921,10 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
898
921
  export const reverse = <N, E, T extends Kind = "directed">(
899
922
  mutable: MutableGraph<N, E, T>
900
923
  ): void => {
924
+ if (mutable.type === "undirected") {
925
+ return
926
+ }
927
+
901
928
  // Reverse all edges by swapping source and target
902
929
  for (const [index, edgeData] of mutable.edges) {
903
930
  mutable.edges.set(index, {
@@ -907,22 +934,7 @@ export const reverse = <N, E, T extends Kind = "directed">(
907
934
  })
908
935
  }
909
936
 
910
- // Clear and rebuild adjacency lists with reversed directions
911
- mutable.adjacency.clear()
912
- mutable.reverseAdjacency.clear()
913
-
914
- // Rebuild adjacency lists with reversed directions
915
- for (const [edgeIndex, edgeData] of mutable.edges) {
916
- // Add to forward adjacency (source -> target)
917
- const sourceEdges = mutable.adjacency.get(edgeData.source) || []
918
- sourceEdges.push(edgeIndex)
919
- mutable.adjacency.set(edgeData.source, sourceEdges)
920
-
921
- // Add to reverse adjacency (target <- source)
922
- const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || []
923
- targetEdges.push(edgeIndex)
924
- mutable.reverseAdjacency.set(edgeData.target, targetEdges)
925
- }
937
+ rebuildAdjacency(mutable)
926
938
 
927
939
  // Invalidate cycle flag since edge directions changed
928
940
  mutable.isAcyclic = Option.none()
@@ -1452,8 +1464,11 @@ export const hasEdge = <N, E, T extends Kind = "directed">(
1452
1464
  // Check if any edge in the adjacency list connects to the target
1453
1465
  for (const edgeIndex of adjacencyList) {
1454
1466
  const edge = graph.edges.get(edgeIndex)
1455
- if (edge !== undefined && edge.target === target) {
1456
- return true
1467
+ if (edge !== undefined) {
1468
+ const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target
1469
+ if (neighbor === target) {
1470
+ return true
1471
+ }
1457
1472
  }
1458
1473
  }
1459
1474
 
@@ -1489,6 +1504,31 @@ export const edgeCount = <N, E, T extends Kind = "directed">(
1489
1504
  graph: Graph<N, E, T> | MutableGraph<N, E, T>
1490
1505
  ): number => graph.edges.size
1491
1506
 
1507
+ const getDirectedNeighbors = <N, E>(
1508
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
1509
+ nodeIndex: NodeIndex,
1510
+ direction: Direction
1511
+ ): Array<NodeIndex> => {
1512
+ const adjacencyMap = direction === "incoming"
1513
+ ? graph.reverseAdjacency
1514
+ : graph.adjacency
1515
+
1516
+ const adjacencyList = adjacencyMap.get(nodeIndex)
1517
+ if (adjacencyList === undefined) {
1518
+ return []
1519
+ }
1520
+
1521
+ const result: Array<NodeIndex> = []
1522
+ for (const edgeIndex of adjacencyList) {
1523
+ const edge = graph.edges.get(edgeIndex)
1524
+ if (edge !== undefined) {
1525
+ result.push(direction === "incoming" ? edge.source : edge.target)
1526
+ }
1527
+ }
1528
+
1529
+ return result
1530
+ }
1531
+
1492
1532
  /**
1493
1533
  * Returns the neighboring nodes (targets of outgoing edges) for a given node.
1494
1534
  *
@@ -1527,24 +1567,47 @@ export const neighbors = <N, E, T extends Kind = "directed">(
1527
1567
  return getUndirectedNeighbors(graph as any, nodeIndex)
1528
1568
  }
1529
1569
 
1530
- const adjacencyList = graph.adjacency.get(nodeIndex)
1531
- if (adjacencyList === undefined) {
1532
- return []
1533
- }
1570
+ return getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, "outgoing")
1571
+ }
1534
1572
 
1535
- const result: Array<NodeIndex> = []
1536
- for (const edgeIndex of adjacencyList) {
1537
- const edge = graph.edges.get(edgeIndex)
1538
- if (edge !== undefined) {
1539
- result.push(edge.target)
1540
- }
1573
+ /**
1574
+ * Returns the outgoing neighbor node indices for a node in a directed graph.
1575
+ *
1576
+ * Throws a `GraphError` when used with an undirected graph.
1577
+ *
1578
+ * @since 3.22.0
1579
+ * @category queries
1580
+ */
1581
+ export const successors = <N, E>(
1582
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
1583
+ nodeIndex: NodeIndex
1584
+ ): Array<NodeIndex> => {
1585
+ if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
1586
+ throw new GraphError({ message: "Cannot get successors of undirected graph" })
1541
1587
  }
1588
+ return getDirectedNeighbors(graph, nodeIndex, "outgoing")
1589
+ }
1542
1590
 
1543
- return result
1591
+ /**
1592
+ * Returns the incoming neighbor node indices for a node in a directed graph.
1593
+ *
1594
+ * Throws a `GraphError` when used with an undirected graph.
1595
+ *
1596
+ * @since 3.22.0
1597
+ * @category queries
1598
+ */
1599
+ export const predecessors = <N, E>(
1600
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
1601
+ nodeIndex: NodeIndex
1602
+ ): Array<NodeIndex> => {
1603
+ if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
1604
+ throw new GraphError({ message: "Cannot get predecessors of undirected graph" })
1605
+ }
1606
+ return getDirectedNeighbors(graph, nodeIndex, "incoming")
1544
1607
  }
1545
1608
 
1546
1609
  /**
1547
- * Get neighbors of a node in a specific direction for bidirectional traversal.
1610
+ * Get directed neighbors of a node in a specific direction.
1548
1611
  *
1549
1612
  * @example
1550
1613
  * ```ts
@@ -1566,36 +1629,19 @@ export const neighbors = <N, E, T extends Kind = "directed">(
1566
1629
  * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
1567
1630
  * ```
1568
1631
  *
1632
+ * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors.
1569
1633
  * @since 3.18.0
1570
1634
  * @category queries
1571
1635
  */
1572
- export const neighborsDirected = <N, E, T extends Kind = "directed">(
1573
- graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1636
+ export const neighborsDirected = <N, E>(
1637
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
1574
1638
  nodeIndex: NodeIndex,
1575
1639
  direction: Direction
1576
1640
  ): Array<NodeIndex> => {
1577
- const adjacencyMap = direction === "incoming"
1578
- ? graph.reverseAdjacency
1579
- : graph.adjacency
1580
-
1581
- const adjacencyList = adjacencyMap.get(nodeIndex)
1582
- if (adjacencyList === undefined) {
1583
- return []
1584
- }
1585
-
1586
- const result: Array<NodeIndex> = []
1587
- for (const edgeIndex of adjacencyList) {
1588
- const edge = graph.edges.get(edgeIndex)
1589
- if (edge !== undefined) {
1590
- // For incoming direction, we want the source node instead of target
1591
- const neighborNode = direction === "incoming"
1592
- ? edge.source
1593
- : edge.target
1594
- result.push(neighborNode)
1595
- }
1641
+ if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
1642
+ throw new GraphError({ message: "Cannot get directed neighbors of undirected graph" })
1596
1643
  }
1597
-
1598
- return result
1644
+ return getDirectedNeighbors(graph, nodeIndex, direction)
1599
1645
  }
1600
1646
 
1601
1647
  // =============================================================================
@@ -1990,7 +2036,11 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
1990
2036
  recursionStack.add(node)
1991
2037
 
1992
2038
  // Get neighbors for this node
1993
- const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing"))
2039
+ const nodeNeighbors = getDirectedNeighbors(
2040
+ graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
2041
+ node,
2042
+ "outgoing"
2043
+ )
1994
2044
  stack[stack.length - 1] = [node, nodeNeighbors, 0, false]
1995
2045
  continue
1996
2046
  }
@@ -2141,7 +2191,7 @@ const getTraversalNeighbors = <N, E, T extends Kind>(
2141
2191
  ): Array<NodeIndex> =>
2142
2192
  graph.type === "undirected"
2143
2193
  ? getUndirectedNeighbors(graph as any, nodeIndex)
2144
- : neighborsDirected(graph, nodeIndex, direction)
2194
+ : getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, direction)
2145
2195
 
2146
2196
  const getTraversableNeighbor = <N, E, T extends Kind>(
2147
2197
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
@@ -2231,9 +2281,13 @@ export const connectedComponents = <N, E>(
2231
2281
  * @since 3.18.0
2232
2282
  * @category algorithms
2233
2283
  */
2234
- export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2235
- graph: Graph<N, E, T> | MutableGraph<N, E, T>
2284
+ export const stronglyConnectedComponents = <N, E>(
2285
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
2236
2286
  ): Array<Array<NodeIndex>> => {
2287
+ if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
2288
+ throw new GraphError({ message: "Cannot find strongly connected components of undirected graph" })
2289
+ }
2290
+
2237
2291
  const visited = new Set<NodeIndex>()
2238
2292
  const finishOrder: Array<NodeIndex> = []
2239
2293
  // Iterate directly over node keys
@@ -2259,7 +2313,7 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2259
2313
  }
2260
2314
 
2261
2315
  visited.add(node)
2262
- const nodeNeighborsList = neighbors(graph, node)
2316
+ const nodeNeighborsList = getDirectedNeighbors(graph, node, "outgoing")
2263
2317
  stack[stack.length - 1] = [node, nodeNeighborsList, 0, false]
2264
2318
  continue
2265
2319
  }
@@ -2377,6 +2431,22 @@ export interface BellmanFordConfig<E> {
2377
2431
  cost: (edgeData: E) => number
2378
2432
  }
2379
2433
 
2434
+ const validateNonNegativeEdgeWeights = <N, E, T extends Kind = "directed">(
2435
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2436
+ cost: (edgeData: E) => number,
2437
+ algorithm: string
2438
+ ): Map<EdgeIndex, number> => {
2439
+ const edgeWeights = new Map<EdgeIndex, number>()
2440
+ for (const [edgeIndex, edgeData] of graph.edges) {
2441
+ const weight = cost(edgeData.data)
2442
+ if (weight < 0 || Number.isNaN(weight)) {
2443
+ throw new GraphError({ message: `${algorithm} requires non-negative edge weights` })
2444
+ }
2445
+ edgeWeights.set(edgeIndex, weight)
2446
+ }
2447
+ return edgeWeights
2448
+ }
2449
+
2380
2450
  /**
2381
2451
  * Find the shortest path between two nodes using Dijkstra's algorithm.
2382
2452
  *
@@ -2419,6 +2489,8 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2419
2489
  throw missingNode(target)
2420
2490
  }
2421
2491
 
2492
+ const edgeWeights = validateNonNegativeEdgeWeights(graph, cost, "Dijkstra's algorithm")
2493
+
2422
2494
  // Early return if source equals target
2423
2495
  if (source === target) {
2424
2496
  return Option.some({
@@ -2479,12 +2551,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2479
2551
  const edge = graph.edges.get(edgeIndex)
2480
2552
  if (edge !== undefined) {
2481
2553
  const neighbor = getTraversableNeighbor(graph, currentNode, edge)
2482
- const weight = cost(edge.data)
2483
-
2484
- // Validate non-negative weights
2485
- if (weight < 0) {
2486
- throw new Error(`Dijkstra's algorithm requires non-negative edge weights, found ${weight}`)
2487
- }
2554
+ const weight = edgeWeights.get(edgeIndex)!
2488
2555
 
2489
2556
  const newDistance = currentDistance + weight
2490
2557
  const neighborDistance = distances.get(neighbor)!
@@ -2738,6 +2805,8 @@ export const astar = <N, E, T extends Kind = "directed">(
2738
2805
  throw missingNode(target)
2739
2806
  }
2740
2807
 
2808
+ const edgeWeights = validateNonNegativeEdgeWeights(graph, cost, "A* algorithm")
2809
+
2741
2810
  // Early return if source equals target
2742
2811
  if (source === target) {
2743
2812
  return Option.some({
@@ -2813,12 +2882,7 @@ export const astar = <N, E, T extends Kind = "directed">(
2813
2882
  const edge = graph.edges.get(edgeIndex)
2814
2883
  if (edge !== undefined) {
2815
2884
  const neighbor = getTraversableNeighbor(graph, currentNode, edge)
2816
- const weight = cost(edge.data)
2817
-
2818
- // Validate non-negative weights
2819
- if (weight < 0) {
2820
- throw new Error(`A* algorithm requires non-negative edge weights, found ${weight}`)
2821
- }
2885
+ const weight = edgeWeights.get(edgeIndex)!
2822
2886
 
2823
2887
  const tentativeGScore = currentGScore + weight
2824
2888
  const neighborGScore = gScore.get(neighbor)!
@@ -3497,6 +3561,7 @@ export const topo = <N, E, T extends Kind = "directed">(
3497
3561
  [Symbol.iterator]: () => {
3498
3562
  const inDegree = new Map<NodeIndex, number>()
3499
3563
  const remaining = new Set<NodeIndex>()
3564
+ const initialSet = new Set(initials)
3500
3565
  const queue = [...initials]
3501
3566
 
3502
3567
  // Initialize in-degree counts
@@ -3511,12 +3576,15 @@ export const topo = <N, E, T extends Kind = "directed">(
3511
3576
  inDegree.set(edgeData.target, currentInDegree + 1)
3512
3577
  }
3513
3578
 
3514
- // Add nodes with zero in-degree to queue if no initials provided
3515
- if (initials.length === 0) {
3516
- for (const [nodeIndex, degree] of inDegree) {
3517
- if (degree === 0) {
3518
- queue.push(nodeIndex)
3519
- }
3579
+ for (const nodeIndex of initials) {
3580
+ if (inDegree.get(nodeIndex)! !== 0) {
3581
+ throw new GraphError({ message: `Initial node ${nodeIndex} has incoming edges` })
3582
+ }
3583
+ }
3584
+
3585
+ for (const [nodeIndex, degree] of inDegree) {
3586
+ if (degree === 0 && !initialSet.has(nodeIndex)) {
3587
+ queue.push(nodeIndex)
3520
3588
  }
3521
3589
  }
3522
3590
 
@@ -3528,7 +3596,11 @@ export const topo = <N, E, T extends Kind = "directed">(
3528
3596
  remaining.delete(current)
3529
3597
 
3530
3598
  // Process outgoing edges, reducing in-degree of targets
3531
- const neighbors = neighborsDirected(graph, current, "outgoing")
3599
+ const neighbors = getDirectedNeighbors(
3600
+ graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3601
+ current,
3602
+ "outgoing"
3603
+ )
3532
3604
  for (const neighbor of neighbors) {
3533
3605
  if (remaining.has(neighbor)) {
3534
3606
  const currentInDegree = inDegree.get(neighbor) || 0
@@ -482,6 +482,14 @@ export const cron: {
482
482
  return makeWithState<[boolean, [number, number, number]], unknown, [number, number]>(
483
483
  [true, [Number.MIN_SAFE_INTEGER, 0, 0]],
484
484
  (now, _, [initial, previous]) => {
485
+ if (now === Number.POSITIVE_INFINITY) {
486
+ return core.succeed([
487
+ [false, previous],
488
+ [previous[1], previous[2]],
489
+ ScheduleDecision.done
490
+ ])
491
+ }
492
+
485
493
  if (now < previous[0]) {
486
494
  return core.succeed([
487
495
  [false, previous],
@@ -1,4 +1,4 @@
1
- let moduleVersion = "3.21.4"
1
+ let moduleVersion = "3.22.0"
2
2
 
3
3
  export const getCurrentVersion = () => moduleVersion
4
4