Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-jobs-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

add a `maxDepth` option to `Graph` search configuration, allowing `dfs` and `bfs` traversals to limit returned nodes by depth from the configured start nodes.
39 changes: 24 additions & 15 deletions packages/effect/src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4649,6 +4649,7 @@ export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
export interface SearchConfig {
readonly start?: Array<NodeIndex>
readonly direction?: Direction
readonly maxDepth?: number
}

/**
Expand All @@ -4658,7 +4659,8 @@ export interface SearchConfig {
* **Details**
*
* If no start nodes are supplied, the iterator is empty. The `direction` option
* chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
* chooses whether to follow outgoing or incoming edges. The `maxDepth` option
* limits traversal by edge distance from the start nodes. Throws a `GraphError`
* if any configured start node does not exist.
*
* **Example** (Traversing depth-first)
Expand Down Expand Up @@ -4702,6 +4704,7 @@ export const dfs: {
): NodeWalker<N> => {
const start = config.start ?? []
const direction = config.direction ?? "outgoing"
const maxDepth = config.maxDepth ?? Infinity

// Validate that all start nodes exist
for (const nodeIndex of start) {
Expand All @@ -4712,12 +4715,12 @@ export const dfs: {

return new Walker((f) => ({
[Symbol.iterator]: () => {
const stack = [...start]
const stack: Array<readonly [NodeIndex, number]> = start.map((node) => [node, 0])
const discovered = new Set<NodeIndex>()

const nextMapped = () => {
while (stack.length > 0) {
const current = stack.pop()!
const [current, depth] = stack.pop()!

if (discovered.has(current)) {
continue
Expand All @@ -4730,11 +4733,13 @@ export const dfs: {
continue
}

const neighbors = getTraversalNeighbors(graph, current, direction)
for (let i = neighbors.length - 1; i >= 0; i--) {
const neighbor = neighbors[i]
if (!discovered.has(neighbor)) {
stack.push(neighbor)
if (depth < maxDepth) {
const neighbors = getTraversalNeighbors(graph, current, direction)
for (let i = neighbors.length - 1; i >= 0; i--) {
const neighbor = neighbors[i]
if (!discovered.has(neighbor)) {
stack.push([neighbor, depth + 1])
}
}
}

Expand All @@ -4756,7 +4761,8 @@ export const dfs: {
* **Details**
*
* If no start nodes are supplied, the iterator is empty. The `direction` option
* chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
* chooses whether to follow outgoing or incoming edges. The `maxDepth` option
* limits traversal by edge distance from the start nodes. Throws a `GraphError`
* if any configured start node does not exist.
*
* **Example** (Traversing breadth-first)
Expand Down Expand Up @@ -4800,6 +4806,7 @@ export const bfs: {
): NodeWalker<N> => {
const start = config.start ?? []
const direction = config.direction ?? "outgoing"
const maxDepth = config.maxDepth ?? Infinity

// Validate that all start nodes exist
for (const nodeIndex of start) {
Expand All @@ -4810,20 +4817,22 @@ export const bfs: {

return new Walker((f) => ({
[Symbol.iterator]: () => {
const queue = [...start]
const queue: Array<readonly [NodeIndex, number]> = start.map((node) => [node, 0])
const discovered = new Set<NodeIndex>()

const nextMapped = () => {
while (queue.length > 0) {
const current = queue.shift()!
const [current, depth] = queue.shift()!

if (!discovered.has(current)) {
discovered.add(current)

const neighbors = getTraversalNeighbors(graph, current, direction)
for (const neighbor of neighbors) {
if (!discovered.has(neighbor)) {
queue.push(neighbor)
if (depth < maxDepth) {
const neighbors = getTraversalNeighbors(graph, current, direction)
for (const neighbor of neighbors) {
if (!discovered.has(neighbor)) {
queue.push([neighbor, depth + 1])
}
}
}

Expand Down
43 changes: 43 additions & 0 deletions packages/effect/test/Graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3009,6 +3009,49 @@ describe("Graph", () => {
expect(entries).toEqual([[0, "A"], [1, "B"], [2, "C"]])
})

it("should limit DFS traversal by maxDepth", () => {
const graph = Graph.directed<string, number>((mutable) => {
const a = Graph.addNode(mutable, "A")
const b = Graph.addNode(mutable, "B")
const c = Graph.addNode(mutable, "C")
const d = Graph.addNode(mutable, "D")
Graph.addEdge(mutable, a, b, 1)
Graph.addEdge(mutable, b, c, 2)
Graph.addEdge(mutable, c, d, 3)
})

const dfsIterator = Graph.dfs(graph, { start: [0], maxDepth: 1 })

expect(Array.from(Graph.indices(dfsIterator))).toEqual([0, 1])
})

it("should limit BFS traversal by maxDepth", () => {
const graph = Graph.directed<string, number>((mutable) => {
const a = Graph.addNode(mutable, "A")
const b = Graph.addNode(mutable, "B")
const c = Graph.addNode(mutable, "C")
const d = Graph.addNode(mutable, "D")
Graph.addEdge(mutable, a, b, 1)
Graph.addEdge(mutable, a, c, 2)
Graph.addEdge(mutable, b, d, 3)
})

const bfsIterator = Graph.bfs(graph, { start: [0], maxDepth: 1 })

expect(Array.from(Graph.indices(bfsIterator))).toEqual([0, 1, 2])
})

it("should only include start nodes when maxDepth is zero", () => {
const graph = Graph.directed<string, number>((mutable) => {
const a = Graph.addNode(mutable, "A")
const b = Graph.addNode(mutable, "B")
Graph.addEdge(mutable, a, b, 1)
})

expect(Array.from(Graph.indices(Graph.dfs(graph, { start: [0], maxDepth: 0 })))).toEqual([0])
expect(Array.from(Graph.indices(Graph.bfs(graph, { start: [0], maxDepth: 0 })))).toEqual([0])
})

it("should provide values() method for Topo iterator", () => {
const graph = Graph.directed<string, number>((mutable) => {
const a = Graph.addNode(mutable, "A")
Expand Down