1/**
2 * Models relationships between indexed nodes and edges.
3 *
4 * This module provides immutable and scoped-mutable graph data structures. A
5 * graph can be directed or undirected, and it can store user-defined data on
6 * both nodes and edges. The module includes traversal, analysis, path-finding,
7 * transformation, and diagram export utilities.
8 *
9 * @since 4.0.0
10 */
11
12import * as Data from "./Data.ts"
13import * as Equal from "./Equal.ts"
14import { dual } from "./Function.ts"
15import * as Hash from "./Hash.ts"
16import type { Inspectable } from "./Inspectable.ts"
17import * as internal from "./internal/graph.ts"
18import * as csr from "./internal/graphCsr.ts"
19import * as MutableHashMap from "./MutableHashMap.ts"
20import * as Option from "./Option.ts"
21import type { Pipeable } from "./Pipeable.ts"
22import type { Covariant, Invariant } from "./Types.ts"
23
24const TypeId = internal.TypeId
25
26/**
27 * Node index for node identification using plain numbers.
28 *
29 * **When to use**
30 *
31 * Use when storing or passing the stable identifier of a graph node between
32 * `Graph` operations.
33 *
34 * **Details**
35 *
36 * `addNode` allocates node identifiers from the graph's next node index.
37 *
38 * **Gotchas**
39 *
40 * A `NodeIndex` is an identifier, not an array offset. Removed node identifiers
41 * are not reused.
42 *
43 * @see {@link EdgeIndex} for edge identifiers instead of node identifiers
44 *
45 * @category models
46 * @since 3.18.0
47 */
48export type NodeIndex = number
49
50/**
51 * Edge index for edge identification using plain numbers.
52 *
53 * **When to use**
54 *
55 * Use when you need to keep the identifier for a graph edge so you can later
56 * read, update, remove, or compare that edge.
57 *
58 * **Gotchas**
59 *
60 * An `EdgeIndex` is an identifier, not an array offset. Removed edge
61 * identifiers are not reused.
62 *
63 * @see {@link NodeIndex} for node identifiers instead of edge identifiers
64 *
65 * @category models
66 * @since 3.18.0
67 */
68export type EdgeIndex = number
69
70/**
71 * Represents edge data containing source, target, and user data.
72 *
73 * **When to use**
74 *
75 * Use as the graph edge value that carries source node, target node, and stored
76 * edge data together.
77 *
78 * @category models
79 * @since 3.18.0
80 */
81export interface Edge<out E> {
82 readonly source: NodeIndex
83 readonly target: NodeIndex
84 readonly data: E
85}
86
87/**
88 * Graph type for distinguishing directed and undirected graphs.
89 *
90 * **When to use**
91 *
92 * Use when writing graph-polymorphic types or helpers that need to preserve
93 * whether a graph is directed or undirected.
94 *
95 * @category models
96 * @since 3.18.0
97 */
98export type Kind = "directed" | "undirected"
99
100/**
101 * A node and its stable index in a graph snapshot.
102 *
103 * @category models
104 * @since 4.0.0
105 */
106export interface IndexedNode<out N> {
107 readonly index: NodeIndex
108 readonly data: N
109}
110
111/**
112 * An edge and its stable index in a graph snapshot.
113 *
114 * @category models
115 * @since 4.0.0
116 */
117export interface IndexedEdge<out E> extends Edge<E> {
118 readonly index: EdgeIndex
119}
120
121/**
122 * Active indexed structure used to reconstruct an immutable graph.
123 *
124 * **When to use**
125 *
126 * Use when serializing or importing graph structure while preserving active
127 * node and edge identifiers.
128 *
129 * **Details**
130 *
131 * Node and edge indexes must be non-negative safe integers in strictly
132 * increasing order. Every edge endpoint must reference an indexed node.
133 *
134 * **Gotchas**
135 *
136 * A snapshot records only active identifiers, not allocator history. After
137 * reconstruction, new identifiers continue after the greatest active index.
138 *
139 * @see {@link fromSnapshot} for reconstructing a graph
140 * @category models
141 * @since 4.0.0
142 */
143export interface Snapshot<out N, out E, out T extends Kind> {
144 readonly type: T
145 readonly nodes: ReadonlyArray<IndexedNode<N>>
146 readonly edges: ReadonlyArray<IndexedEdge<E>>
147}
148
149/**
150 * Immutable graph interface.
151 *
152 * **When to use**
153 *
154 * Use as the immutable graph model for code that queries, traverses,
155 * transforms, or analyzes graph structure without mutating it.
156 *
157 * **Gotchas**
158 *
159 * After a graph is hashed, its transitively contained node and edge payloads
160 * used by hashing must remain immutable, as with other Effect values.
161 *
162 * @see {@link MutableGraph} for the mutable counterpart used inside mutation scopes
163 *
164 * @category models
165 * @since 3.18.0
166 */
167export interface Graph<out N, out E, T extends Kind = "directed">
168 extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable
169{
170 readonly [TypeId]: Graph.Variance<N, E>
171 readonly type: T
172 readonly mutable: false
173}
174
175/**
176 * Companion namespace containing type-level metadata for immutable graphs.
177 *
178 * @category models
179 * @since 4.0.0
180 */
181export declare namespace Graph {
182 /**
183 * Type-level variance marker for immutable graphs.
184 *
185 * @category utility types
186 * @since 4.0.0
187 */
188 export interface Variance<out N, out E> {
189 readonly _N: Covariant<N>
190 readonly _E: Covariant<E>
191 }
192}
193
194/**
195 * Mutable graph interface.
196 *
197 * **When to use**
198 *
199 * Use when adding, removing, or updating nodes and edges inside a graph
200 * mutation scope.
201 *
202 * **Gotchas**
203 *
204 * A callback invoked by another graph operation may query the same mutable
205 * graph, but cannot mutate or finalize it. Mutation is allowed in callbacks
206 * passed to graph constructors and `mutate`, where mutation is the purpose.
207 *
208 * @see {@link Graph} for the immutable graph interface
209 * @see {@link mutate} for scoped mutation of an immutable graph
210 *
211 * @category models
212 * @since 3.18.0
213 */
214export interface MutableGraph<in out N, in out E, T extends Kind = "directed">
215 extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable
216{
217 readonly [TypeId]: MutableGraph.Variance<N, E>
218 readonly type: T
219 readonly mutable: true
220}
221
222/**
223 * Companion namespace containing type-level metadata for scoped mutable graphs.
224 *
225 * @category models
226 * @since 4.0.0
227 */
228export declare namespace MutableGraph {
229 /**
230 * Type-level variance marker for scoped mutable graphs.
231 *
232 * @category utility types
233 * @since 4.0.0
234 */
235 export interface Variance<in out N, in out E> {
236 readonly _N: Invariant<N>
237 readonly _E: Invariant<E>
238 }
239}
240
241/** @internal */
242const copyEdge = <E>(edge: Edge<E>): Edge<E> => ({
243 source: edge.source,
244 target: edge.target,
245 data: edge.data
246})
247
248/**
249 * Immutable graph type for source-to-target relationships.
250 *
251 * **When to use**
252 *
253 * Use as the immutable graph type when edge direction is part of the model and
254 * traversal or neighbor queries should follow source-to-target edges.
255 *
256 * **Details**
257 *
258 * `DirectedGraph<N, E>` is a `Graph<N, E, "directed">` with node data of type
259 * `N` and edge data of type `E`.
260 *
261 * @see {@link directed} for constructing directed graphs
262 * @see {@link UndirectedGraph} for graphs whose edges connect both endpoints
263 *
264 * @category models
265 * @since 3.18.0
266 */
267export type DirectedGraph<N, E> = Graph<N, E, "directed">
268
269/**
270 * Immutable graph type for relationships without source-to-target direction.
271 *
272 * **When to use**
273 *
274 * Use when modeling relationships where each edge connects both endpoints
275 * without a source-to-target direction.
276 *
277 * **Details**
278 *
279 * `UndirectedGraph<N, E>` is a `Graph<N, E, "undirected">`.
280 *
281 * @see {@link undirected} for constructing undirected graphs
282 * @see {@link DirectedGraph} for graphs whose edges have source-to-target direction
283 *
284 * @category models
285 * @since 3.18.0
286 */
287export type UndirectedGraph<N, E> = Graph<N, E, "undirected">
288
289/**
290 * Mutable directed graph type alias.
291 *
292 * **When to use**
293 *
294 * Use when annotating a temporary graph value that can be changed in place and
295 * whose edges have source-to-target direction.
296 *
297 * @see {@link MutableGraph} for the generic mutable graph type
298 * @see {@link MutableUndirectedGraph} for mutable graphs without edge direction
299 *
300 * @category models
301 * @since 3.18.0
302 */
303export type MutableDirectedGraph<N, E> = MutableGraph<N, E, "directed">
304
305/**
306 * Mutable undirected graph type alias.
307 *
308 * **When to use**
309 *
310 * Use when annotating a temporary graph value that can be changed in place and
311 * whose edges connect both endpoints without direction.
312 *
313 * @see {@link MutableDirectedGraph} for mutable graphs with directed edges
314 * @see {@link MutableGraph} for the generic mutable graph type
315 *
316 * @category models
317 * @since 3.18.0
318 */
319export type MutableUndirectedGraph<N, E> = MutableGraph<N, E, "undirected">
320
321// =============================================================================
322// Errors
323// =============================================================================
324
325// TODO: Do we need safe variants for these?
326
327/**
328 * Error thrown by graph operations when the requested graph structure is
329 * invalid, such as referencing a missing node or using unsupported edge
330 * weights.
331 *
332 * **When to use**
333 *
334 * Use when handling failures thrown by graph operations that reject invalid
335 * graph structure or unsupported algorithm inputs.
336 *
337 * @category errors
338 * @since 3.18.0
339 */
340export class GraphError extends Data.TaggedError("GraphError")<{
341 readonly message: string
342}> {}
343
344/** @internal */
345const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` })
346
347/** @internal */
348const traversalRadius = (radius: number | undefined, defaultRadius: number): number => {
349 const value = radius ?? defaultRadius
350 if (value !== Infinity && (!Number.isInteger(value) || value < 0)) {
351 throw new GraphError({ message: "Traversal radius must be a non-negative integer or Infinity" })
352 }
353 return value
354}
355
356/** @internal */
357function assertMutable<N, E, T extends Kind = "directed">(
358 graph: Graph<N, E, T> | MutableGraph<N, E, T>
359): asserts graph is MutableGraph<N, E, T> {
360 if (!graph.mutable) {
361 throw new GraphError({ message: "Graph is not mutable" })
362 }
363}
364
365/** @internal */
366const getMutableImplForMutation = <N, E, T extends Kind>(
367 graph: MutableGraph<N, E, T>
368): internal.GraphImpl<N, E, T> => {
369 assertMutable(graph)
370 if (internal.isTransforming(graph)) {
371 throw new GraphError({ message: "Cannot mutate graph during a transformation" })
372 }
373 csr.invalidate(graph)
374 return internal.toImpl(graph)
375}
376
377/** @internal */
378const withMutationGuard = <N, E, T extends Kind, A>(
379 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
380 evaluate: () => A
381): A => graph.mutable ? internal.withTransformation(graph, evaluate) : evaluate()
382
383// =============================================================================
384// Constructors
385// =============================================================================
386
387/**
388 * Returns `true` if a value has the graph runtime type identifier, narrowing
389 * it to an immutable or mutable graph.
390 *
391 * **When to use**
392 *
393 * Use to narrow an unknown value before treating it as a graph value.
394 *
395 * **Gotchas**
396 *
397 * This guard checks the shared graph runtime type identifier and does not
398 * distinguish immutable graphs from mutable graphs or directed graphs from
399 * undirected graphs.
400 *
401 * @category guards
402 * @since 4.0.0
403 */
404export const isGraph: <N = unknown, E = unknown, T extends Kind = Kind, U = never>(
405 u: U | Graph<N, E, T> | MutableGraph<N, E, T>
406) => u is Graph<N, E, T> | MutableGraph<N, E, T> = internal.isGraph
407
408/**
409 * Reconstructs an immutable graph from its indexed active structure.
410 *
411 * **When to use**
412 *
413 * Use when importing a snapshot or other externally indexed graph structure.
414 * Prefer `directed` or `undirected` when creating a new graph without existing
415 * identifiers.
416 *
417 * **Gotchas**
418 *
419 * The node and edge arrays must be ordered by strictly increasing,
420 * non-negative safe integer indexes, and every edge endpoint must reference a
421 * node in the snapshot. Invalid snapshots throw a `GraphError`. Historical
422 * removed identifiers after the greatest active index are not retained.
423 *
424 * **Example** (Preserving graph indexes)
425 *
426 * ```ts import.meta.vitest
427 * import { Graph } from "effect"
428 *
429 * const graph = Graph.fromSnapshot({
430 * type: "directed",
431 * nodes: [{ index: 2, data: "A" }, { index: 5, data: "B" }],
432 * edges: [{ index: 3, source: 2, target: 5, data: 1 }]
433 * })
434 *
435 * Graph.toSnapshot(graph).edges[0].index // => 3
436 * ```
437 *
438 * @see {@link toSnapshot} for capturing a graph snapshot
439 * @category constructors
440 * @since 4.0.0
441 */
442export const fromSnapshot = <N, E, T extends Kind>(snapshot: Snapshot<N, E, T>): Graph<N, E, T> => {
443 if (snapshot.type !== "directed" && snapshot.type !== "undirected") {
444 throw new GraphError({ message: "Snapshot type must be directed or undirected" })
445 }
446
447 let previous = -1
448 const nodeIndexes = new Set<NodeIndex>()
449 for (let i = 0; i < snapshot.nodes.length; i++) {
450 const node = snapshot.nodes[i]
451 if (node === undefined || node === null) {
452 throw new GraphError({ message: `Node at position ${i} must be defined` })
453 }
454 const index = node.index
455 if (!Number.isSafeInteger(index) || index < 0) {
456 throw new GraphError({ message: `Node index at position ${i} must be a non-negative safe integer` })
457 }
458 if (index <= previous) {
459 throw new GraphError({ message: "Node indexes must be strictly increasing" })
460 }
461 previous = index
462 nodeIndexes.add(index)
463 }
464
465 previous = -1
466 for (let i = 0; i < snapshot.edges.length; i++) {
467 const edge = snapshot.edges[i]
468 if (edge === undefined || edge === null) {
469 throw new GraphError({ message: `Edge at position ${i} must be defined` })
470 }
471 if (!Number.isSafeInteger(edge.index) || edge.index < 0) {
472 throw new GraphError({ message: `Edge index at position ${i} must be a non-negative safe integer` })
473 }
474 if (edge.index <= previous) {
475 throw new GraphError({ message: "Edge indexes must be strictly increasing" })
476 }
477 previous = edge.index
478 if (!Number.isSafeInteger(edge.source) || edge.source < 0) {
479 throw new GraphError({ message: `Edge source at position ${i} must be a non-negative safe integer` })
480 }
481 if (!nodeIndexes.has(edge.source)) {
482 throw new GraphError({ message: `Edge source ${edge.source} does not reference a node` })
483 }
484 if (!Number.isSafeInteger(edge.target) || edge.target < 0) {
485 throw new GraphError({ message: `Edge target at position ${i} must be a non-negative safe integer` })
486 }
487 if (!nodeIndexes.has(edge.target)) {
488 throw new GraphError({ message: `Edge target ${edge.target} does not reference a node` })
489 }
490 }
491
492 return internal.hydrate(snapshot)
493}
494
495/**
496 * Returns the active indexed structure of a graph.
497 *
498 * **When to use**
499 *
500 * Use when serializing a graph or passing its active structure across a
501 * boundary where node and edge identifiers must be preserved.
502 *
503 * **Details**
504 *
505 * Nodes and edges are returned in graph order with their current indexes.
506 * Undirected edges retain their stored endpoint orientation, and each returned
507 * node and edge record is newly allocated. The operation runs in `O(V + E)`.
508 *
509 * **Gotchas**
510 *
511 * Node and edge payloads are not cloned. The snapshot also omits allocator
512 * history for identifiers that are no longer active.
513 *
514 * **Example** (Round-tripping a graph snapshot)
515 *
516 * ```ts import.meta.vitest
517 * import { Equal, Graph } from "effect"
518 *
519 * const graph = Graph.fromSnapshot({
520 * type: "undirected",
521 * nodes: [{ index: 2, data: "A" }, { index: 5, data: "B" }],
522 * edges: [{ index: 3, source: 5, target: 2, data: "A-B" }]
523 * })
524 *
525 * Equal.equals(Graph.fromSnapshot(Graph.toSnapshot(graph)), graph) // => true
526 * ```
527 *
528 * @see {@link fromSnapshot} for reconstructing an immutable graph
529 * @category converting
530 * @since 4.0.0
531 */
532export const toSnapshot = <N, E, T extends Kind = "directed">(
533 graph: Graph<N, E, T> | MutableGraph<N, E, T>
534): Snapshot<N, E, T> => {
535 const impl = internal.toImpl(graph)
536 return {
537 type: graph.type,
538 nodes: Array.from(impl.nodes, ([index, data]) => ({ index, data })),
539 edges: Array.from(impl.edges, ([index, edge]) => ({
540 index,
541 source: edge.source,
542 target: edge.target,
543 data: edge.data
544 }))
545 }
546}
547
548/**
549 * Creates a graph constructor for the specified graph kind.
550 *
551 * **When to use**
552 *
553 * Use when the graph kind is selected dynamically. Prefer `directed` or
554 * `undirected` when the kind is known statically.
555 *
556 * **Example** (Constructing by kind)
557 *
558 * ```ts import.meta.vitest
559 * import { Graph } from "effect"
560 *
561 * const makeGraph = Graph.make("directed")
562 * const graph = makeGraph<string, number>((mutable) => {
563 * Graph.addNode(mutable, "A")
564 * })
565 *
566 * graph.type // => "directed"
567 * ```
568 *
569 * @see {@link directed} for constructing a directed graph directly
570 * @see {@link undirected} for constructing an undirected graph directly
571 * @category constructors
572 * @since 4.0.0
573 */
574export const make =
575 <T extends Kind>(type: T) => <N, E>(mutate?: (mutable: MutableGraph<N, E, T>) => undefined): Graph<N, E, T> => {
576 if (type !== "directed" && type !== "undirected") {
577 throw new GraphError({ message: "Graph type must be directed or undirected" })
578 }
579 if (mutate === undefined) {
580 return internal.make<N, E, T>(type, false) as unknown as Graph<N, E, T>
581 }
582
583 const graph = internal.make<N, E, T>(type, true)
584 const mutable = Equal.byReferenceUnsafe(graph as unknown as MutableGraph<N, E, T>)
585 return mutateScoped(mutable, mutate)
586 }
587
588/**
589 * Creates a directed graph, optionally with initial mutations.
590 *
591 * **When to use**
592 *
593 * Use when relationships have a source-to-target direction, such as
594 * dependencies, workflows, or routing links.
595 *
596 * **Gotchas**
597 *
598 * The mutable callback handle is finalized when the callback returns and must
599 * not be retained for later mutation.
600 *
601 * **Example** (Creating a directed graph)
602 *
603 * ```ts import.meta.vitest
604 * import { Graph } from "effect"
605 *
606 * // Directed graph with initial nodes and edges
607 * const graph = Graph.directed<string, string>((mutable) => {
608 * const a = Graph.addNode(mutable, "A")
609 * const b = Graph.addNode(mutable, "B")
610 * const c = Graph.addNode(mutable, "C")
611 * Graph.addEdge(mutable, a, b, "A->B")
612 * Graph.addEdge(mutable, b, c, "B->C")
613 * })
614 * Array.of(Graph.nodeCount(graph), Graph.edgeCount(graph)) // => [3, 2]
615 * ```
616 *
617 * @category constructors
618 * @since 3.18.0
619 */
620export const directed: <N, E>(
621 mutate?: (mutable: MutableDirectedGraph<N, E>) => undefined
622) => DirectedGraph<N, E> = make("directed")
623
624/**
625 * Creates an undirected graph, optionally with initial mutations.
626 *
627 * **When to use**
628 *
629 * Use when relationships connect both endpoints symmetrically, such as social
630 * connections or physical links.
631 *
632 * **Gotchas**
633 *
634 * The mutable callback handle is finalized when the callback returns and must
635 * not be retained for later mutation.
636 *
637 * **Example** (Creating an undirected graph)
638 *
639 * ```ts import.meta.vitest
640 * import { Graph } from "effect"
641 *
642 * // Undirected graph with initial nodes and edges
643 * const graph = Graph.undirected<string, string>((mutable) => {
644 * const a = Graph.addNode(mutable, "A")
645 * const b = Graph.addNode(mutable, "B")
646 * const c = Graph.addNode(mutable, "C")
647 * Graph.addEdge(mutable, a, b, "A-B")
648 * Graph.addEdge(mutable, b, c, "B-C")
649 * })
650 * Array.of(Graph.nodeCount(graph), Graph.edgeCount(graph)) // => [3, 2]
651 * ```
652 *
653 * @category constructors
654 * @since 3.18.0
655 */
656export const undirected: <N, E>(
657 mutate?: (mutable: MutableUndirectedGraph<N, E>) => undefined
658) => UndirectedGraph<N, E> = make("undirected")
659
660// =============================================================================
661// Scoped Mutable API
662// =============================================================================
663
664/**
665 * Creates a mutable copy of an immutable graph for a manual mutation scope.
666 *
667 * **When to use**
668 *
669 * Use when a mutation scope must span code that cannot be expressed as one
670 * `mutate` callback.
671 *
672 * **Gotchas**
673 *
674 * The graph structure is copied, but node and edge payload objects remain
675 * shared by reference. Always finish the scope with `endMutation`; prefer
676 * `mutate` when a callback is sufficient.
677 *
678 * **Example** (Beginning a mutation scope)
679 *
680 * ```ts import.meta.vitest
681 * import { Graph } from "effect"
682 *
683 * const graph = Graph.directed<string, number>()
684 * const mutable = Graph.beginMutation(graph)
685 * // Now mutable can be safely modified without affecting original graph
686 * Array.of(Graph.nodeCount(mutable), Graph.nodeCount(graph)) // => [0, 0]
687 * ```
688 *
689 * @see {@link endMutation} for finalizing the mutable graph
690 * @see {@link mutate} for automatically scoped mutation
691 * @category mutations
692 * @since 3.18.0
693 */
694export const beginMutation = <N, E, T extends Kind = "directed">(
695 graph: Graph<N, E, T>
696): MutableGraph<N, E, T> => {
697 const source = internal.toImpl(graph)
698 const mutable = internal.clone(source, true)
699 return Equal.byReferenceUnsafe(mutable as unknown as MutableGraph<N, E, T>)
700}
701
702/**
703 * Converts a mutable graph back to an immutable graph, ending the mutation scope.
704 *
705 * **When to use**
706 *
707 * Use to finish a mutation scope opened with `beginMutation`.
708 *
709 * **Gotchas**
710 *
711 * Finalization is terminal. Later public mutation operations on the same
712 * mutable handle fail with a `GraphError`.
713 *
714 * **Example** (Ending a mutation scope)
715 *
716 * ```ts import.meta.vitest
717 * import { Graph } from "effect"
718 *
719 * const graph = Graph.directed<string, number>()
720 * const mutable = Graph.beginMutation(graph)
721 * // ... perform mutations on mutable ...
722 * Graph.nodeCount(Graph.endMutation(mutable)) // => 0
723 * ```
724 *
725 * @see {@link beginMutation} for opening a manual mutation scope
726 * @see {@link mutate} for automatically scoped mutation
727 * @category mutations
728 * @since 3.18.0
729 */
730export const endMutation = <N, E, T extends Kind = "directed">(
731 mutable: MutableGraph<N, E, T>
732): Graph<N, E, T> => {
733 assertMutable(mutable)
734 if (internal.isTransforming(mutable)) {
735 throw new GraphError({ message: "Cannot mutate graph during a transformation" })
736 }
737 const source = internal.toImpl(mutable)
738 csr.invalidate(mutable)
739 const graph = internal.finalize(source)
740 source.mutable = false
741
742 return graph as unknown as Graph<N, E, T>
743}
744
745/** @internal */
746const mutateScoped = <N, E, T extends Kind>(
747 mutable: MutableGraph<N, E, T>,
748 f: (mutable: MutableGraph<N, E, T>) => undefined
749): Graph<N, E, T> => {
750 try {
751 f(mutable)
752 } catch (error) {
753 if (mutable.mutable) {
754 endMutation(mutable)
755 }
756 throw error
757 }
758 return endMutation(mutable)
759}
760
761/**
762 * Returns an immutable graph after applying scoped mutations to a structural copy.
763 *
764 * **When to use**
765 *
766 * Use when several node or edge
767 * mutations should be applied together.
768 *
769 * **Details**
770 *
771 * The original graph remains structurally unchanged. The mutable callback
772 * handle is finalized whether the callback returns or throws.
773 *
774 * **Gotchas**
775 *
776 * Payload objects are shared unless the callback replaces them. A callback
777 * failure is rethrown after the mutable handle is finalized, and the handle
778 * must not escape for later mutation.
779 *
780 * **Example** (Applying scoped mutations)
781 *
782 * ```ts import.meta.vitest
783 * import { Graph } from "effect"
784 *
785 * const graph = Graph.directed<string, number>()
786 * const newGraph = Graph.mutate(graph, (mutable) => {
787 * const nodeA = Graph.addNode(mutable, "A")
788 * const nodeB = Graph.addNode(mutable, "B")
789 * Graph.addEdge(mutable, nodeA, nodeB, 1)
790 * })
791 *
792 * Graph.nodeCount(newGraph) // => 2
793 * Graph.edgeCount(newGraph) // => 1
794 * ```
795 *
796 * @see {@link beginMutation} for opening a manual mutation scope
797 * @see {@link endMutation} for finalizing a manual mutation scope
798 * @category mutations
799 * @since 3.18.0
800 */
801export const mutate: {
802 <N, E, T extends Kind = "directed">(
803 f: (mutable: MutableGraph<N, E, T>) => undefined
804 ): (graph: Graph<N, E, T>) => Graph<N, E, T>
805 <N, E, T extends Kind = "directed">(
806 graph: Graph<N, E, T>,
807 f: (mutable: MutableGraph<N, E, T>) => undefined
808 ): Graph<N, E, T>
809} = dual(2, <N, E, T extends Kind = "directed">(
810 graph: Graph<N, E, T>,
811 f: (mutable: MutableGraph<N, E, T>) => undefined
812): Graph<N, E, T> => {
813 const mutable = beginMutation(graph)
814 return mutateScoped(mutable, f)
815})
816
817// =============================================================================
818// Set Operations
819// =============================================================================
820
821/** @internal */
822type NodeMaps<N, I> = {
823 readonly byIdentity: MutableHashMap.MutableHashMap<I, N>
824 readonly byIndex: Map<NodeIndex, I>
825}
826
827/** @internal */
828class EdgeIdentity<NI, EI> implements Equal.Equal {
829 readonly type: Kind
830 readonly source: NI
831 readonly target: NI
832 readonly identity: EI
833
834 constructor(
835 type: Kind,
836 source: NI,
837 target: NI,
838 identity: EI
839 ) {
840 this.type = type
841 this.source = source
842 this.target = target
843 this.identity = identity
844 }
845
846 [Equal.symbol](that: Equal.Equal): boolean {
847 if (!(that instanceof EdgeIdentity) || this.type !== that.type || !Equal.equals(this.identity, that.identity)) {
848 return false
849 }
850
851 if (this.type === "directed") {
852 return Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target)
853 }
854
855 return (
856 (Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target)) ||
857 (Equal.equals(this.source, that.target) && Equal.equals(this.target, that.source))
858 )
859 }
860
861 [Hash.symbol](): number {
862 const hash = Hash.hash(this.identity)
863 if (this.type === "directed") {
864 return Hash.optimize(Hash.combine(Hash.combine(hash, Hash.hash(this.source)), Hash.hash(this.target)))
865 }
866 return Hash.optimize(Hash.combine(hash, internal.endpointsHash(this.source, this.target)))
867 }
868}
869
870/**
871 * Configures node and edge identity for graph set operations.
872 *
873 * **When to use**
874 *
875 * Use when logical graph membership should be based on a stable key rather
876 * than the complete node or edge payload.
877 *
878 * **Details**
879 *
880 * Both functions default to using the complete node or edge data. Edge identity
881 * also includes the identities of its endpoint nodes and the graph kind.
882 * Projected identities use Effect equality and hashing semantics.
883 *
884 * **Gotchas**
885 *
886 * Edge identity defines set membership, not edge multiplicity. Parallel edges
887 * with the same endpoint identities and projected edge identity are treated as
888 * the same member by graph set operations.
889 *
890 * @category configuration
891 * @since 4.0.0
892 */
893export interface IdentityOptions<N, E, NI = N, EI = E> {
894 readonly nodeIdentity?: (node: N) => NI
895 readonly edgeIdentity?: (edge: E) => EI
896}
897
898/** @internal */
899const buildNodeMaps = <N, E, T extends Kind, I>(
900 graph: Graph<N, E, T>,
901 identity: (node: N) => I
902): NodeMaps<N, I> => {
903 const impl = internal.toImpl(graph)
904 const byIdentity = MutableHashMap.empty<I, N>()
905 const byIndex = new Map<NodeIndex, I>()
906
907 for (const [index, data] of impl.nodes) {
908 const nodeIdentity = identity(data)
909 MutableHashMap.set(byIdentity, nodeIdentity, data)
910 byIndex.set(index, nodeIdentity)
911 }
912
913 return { byIdentity, byIndex }
914}
915
916/** @internal */
917const nodeIdentityAt = <N, I>(maps: NodeMaps<N, I>, index: NodeIndex): I => maps.byIndex.get(index) as I
918
919/** @internal */
920const buildEdgeMap = <N, E, T extends Kind, NI, EI>(
921 graph: Graph<N, E, T>,
922 nodeMaps: NodeMaps<N, NI>,
923 identity: (edge: E) => EI
924): MutableHashMap.MutableHashMap<EdgeIdentity<NI, EI>, E> => {
925 const impl = internal.toImpl(graph)
926 const edges = MutableHashMap.empty<EdgeIdentity<NI, EI>, E>()
927 for (const edge of impl.edges.values()) {
928 const sourceIdentity = nodeIdentityAt(nodeMaps, edge.source)
929 const targetIdentity = nodeIdentityAt(nodeMaps, edge.target)
930 MutableHashMap.set(
931 edges,
932 new EdgeIdentity(graph.type, sourceIdentity, targetIdentity, identity(edge.data)),
933 edge.data
934 )
935 }
936 return edges
937}
938
939/** @internal */
940const addNodesByIdentity = <N, E, T extends Kind, NI>(
941 mutable: MutableGraph<N, E, T>,
942 nodes: Iterable<readonly [NI, N]>
943): MutableHashMap.MutableHashMap<NI, NodeIndex> => {
944 const indexByIdentity = MutableHashMap.empty<NI, NodeIndex>()
945 for (const [identity, data] of nodes) {
946 MutableHashMap.set(indexByIdentity, identity, addNode(mutable, data))
947 }
948 return indexByIdentity
949}
950
951/** @internal */
952const addEdgeByIdentity = <N, E, T extends Kind, NI, EI>(
953 mutable: MutableGraph<N, E, T>,
954 indexByIdentity: MutableHashMap.MutableHashMap<NI, NodeIndex>,
955 identity: EdgeIdentity<NI, EI>,
956 data: E
957): void => {
958 const sourceIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.source))
959 const targetIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.target))
960 if (sourceIndex !== undefined && targetIndex !== undefined) {
961 addEdge(mutable, sourceIndex, targetIndex, data)
962 }
963}
964
965/** @internal */
966const assertSameKind = <N, E>(self: Graph<N, E, Kind>, that: Graph<N, E, Kind>): void => {
967 if (self.type !== that.type) {
968 throw new GraphError({ message: `Cannot combine ${self.type} and ${that.type} graphs` })
969 }
970}
971
972/**
973 * Composes two graphs, merging nodes by identity.
974 *
975 * **When to use**
976 *
977 * Use when combining graphs that describe overlapping logical entities and
978 * should merge those entities by payload or a projected identity.
979 *
980 * **Details**
981 *
982 * Nodes and edges present in both graphs use data from `that`. The result has
983 * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
984 * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
985 * edge data. Edge identity also includes the endpoint identities.
986 *
987 * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
988 *
989 * **Gotchas**
990 *
991 * Nodes with equal identities in one input graph are coalesced. The last node
992 * supplies the data, and redirected edges can collapse or become self-loops.
993 * Parallel edges with equal identities are also coalesced, with the last edge
994 * supplying the data. The result allocates new node and edge identifiers.
995 *
996 * **Example** (Combining graphs)
997 *
998 * ```ts import.meta.vitest
999 * import { Graph } from "effect"
1000 *
1001 * const left = Graph.directed<{ id: string }, string>((mutable) => {
1002 * const a = Graph.addNode(mutable, { id: "A" })
1003 * const b = Graph.addNode(mutable, { id: "B" })
1004 * Graph.addEdge(mutable, a, b, "A-B")
1005 * })
1006 *
1007 * const right = Graph.directed<{ id: string }, string>((mutable) => {
1008 * const b = Graph.addNode(mutable, { id: "B" })
1009 * const c = Graph.addNode(mutable, { id: "C" })
1010 * Graph.addEdge(mutable, b, c, "B-C")
1011 * })
1012 *
1013 * const result = Graph.compose(left, right, {
1014 * nodeIdentity: (node) => node.id
1015 * })
1016 *
1017 * Graph.nodeCount(result) // => 3
1018 * Graph.edgeCount(result) // => 2
1019 * ```
1020 *
1021 * @see {@link sum} for combining graphs without merging equal nodes
1022 * @category set operations
1023 * @since 4.0.0
1024 */
1025export const compose: {
1026 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1027 that: Graph<N, E, T>,
1028 options?: IdentityOptions<N, E, NI, EI>
1029 ): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1030 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1031 self: Graph<N, E, T>,
1032 that: Graph<N, E, NoInfer<T>>,
1033 options?: IdentityOptions<N, E, NI, EI>
1034 ): Graph<N, E, T>
1035} = dual(
1036 (args) => isGraph(args[0]) && isGraph(args[1]),
1037 <N, E, T extends Kind, NI, EI>(
1038 self: Graph<N, E, T>,
1039 that: Graph<N, E, T>,
1040 options?: IdentityOptions<N, E, NI, EI>
1041 ): Graph<N, E, T> => {
1042 assertSameKind(self, that)
1043 const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1044 const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1045 const selfMaps = buildNodeMaps(self, getNodeIdentity)
1046 const thatMaps = buildNodeMaps(that, getNodeIdentity)
1047 const nodes = MutableHashMap.empty<NI, N>()
1048 const edges = buildEdgeMap(self, selfMaps, getEdgeIdentity)
1049
1050 for (const [identity, data] of selfMaps.byIdentity) {
1051 MutableHashMap.set(nodes, identity, data)
1052 }
1053 for (const [identity, data] of thatMaps.byIdentity) {
1054 MutableHashMap.set(nodes, identity, data)
1055 }
1056
1057 for (const [identity, data] of buildEdgeMap(that, thatMaps, getEdgeIdentity)) {
1058 MutableHashMap.set(edges, identity, data)
1059 }
1060
1061 return make(self.type)<N, E>((mutable) => {
1062 const indexByIdentity = addNodesByIdentity(mutable, nodes)
1063
1064 for (const [identity, data] of edges) {
1065 addEdgeByIdentity(mutable, indexByIdentity, identity, data)
1066 }
1067 })
1068 }
1069)
1070
1071/**
1072 * Returns the intersection of two graphs, matching nodes by identity.
1073 *
1074 * **When to use**
1075 *
1076 * Use when extracting nodes and edges that represent the same logical
1077 * structure in both graphs.
1078 *
1079 * **Details**
1080 *
1081 * Node data comes from `self`, and edge data comes from `that`. The result has
1082 * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1083 * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1084 * edge data. Edge identity also includes the endpoint identities.
1085 *
1086 * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1087 *
1088 * **Gotchas**
1089 *
1090 * Nodes with equal identities in one input graph are coalesced. The last node
1091 * supplies the data, and redirected edges can collapse or become self-loops.
1092 * The result contains at most one edge for each shared edge identity and
1093 * allocates new node and edge identifiers.
1094 *
1095 * **Example** (Finding shared structure)
1096 *
1097 * ```ts import.meta.vitest
1098 * import { Graph } from "effect"
1099 *
1100 * const left = Graph.directed<string, string>((mutable) => {
1101 * const a = Graph.addNode(mutable, "A")
1102 * const b = Graph.addNode(mutable, "B")
1103 * Graph.addEdge(mutable, a, b, "shared")
1104 * })
1105 *
1106 * const right = Graph.directed<string, string>((mutable) => {
1107 * const a = Graph.addNode(mutable, "A")
1108 * const b = Graph.addNode(mutable, "B")
1109 * Graph.addEdge(mutable, a, b, "shared")
1110 * })
1111 *
1112 * const result = Graph.intersection(left, right)
1113 *
1114 * Graph.nodeCount(result) // => 2
1115 * Graph.edgeCount(result) // => 1
1116 * ```
1117 *
1118 * @see {@link compose} for identity-based graph union
1119 * @category set operations
1120 * @since 4.0.0
1121 */
1122export const intersection: {
1123 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1124 that: Graph<N, E, T>,
1125 options?: IdentityOptions<N, E, NI, EI>
1126 ): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1127 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1128 self: Graph<N, E, T>,
1129 that: Graph<N, E, NoInfer<T>>,
1130 options?: IdentityOptions<N, E, NI, EI>
1131 ): Graph<N, E, T>
1132} = dual((args) => isGraph(args[0]) && isGraph(args[1]), <N, E, T extends Kind, NI, EI>(
1133 self: Graph<N, E, T>,
1134 that: Graph<N, E, T>,
1135 options?: IdentityOptions<N, E, NI, EI>
1136): Graph<N, E, T> => {
1137 assertSameKind(self, that)
1138 const thatImpl = internal.toImpl(that)
1139 const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1140 const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1141 const selfMaps = buildNodeMaps(self, getNodeIdentity)
1142 const thatMaps = buildNodeMaps(that, getNodeIdentity)
1143 const nodes = MutableHashMap.empty<NI, N>()
1144 const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity)
1145 const thatEdges = MutableHashMap.empty<EdgeIdentity<NI, EI>, E>()
1146
1147 for (const [identity, data] of selfMaps.byIdentity) {
1148 if (MutableHashMap.has(thatMaps.byIdentity, identity)) {
1149 MutableHashMap.set(nodes, identity, data)
1150 }
1151 }
1152
1153 for (const edge of thatImpl.edges.values()) {
1154 const sourceIdentity = nodeIdentityAt(thatMaps, edge.source)
1155 const targetIdentity = nodeIdentityAt(thatMaps, edge.target)
1156 if (MutableHashMap.has(nodes, sourceIdentity) && MutableHashMap.has(nodes, targetIdentity)) {
1157 const edgeIdentity = new EdgeIdentity(that.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data))
1158 MutableHashMap.set(thatEdges, edgeIdentity, edge.data)
1159 }
1160 }
1161
1162 return make(self.type)<N, E>((mutable) => {
1163 const indexByIdentity = addNodesByIdentity(mutable, nodes)
1164
1165 for (const [identity, data] of thatEdges) {
1166 if (MutableHashMap.has(selfEdges, identity)) {
1167 addEdgeByIdentity(mutable, indexByIdentity, identity, data)
1168 }
1169 }
1170 })
1171})
1172
1173/**
1174 * Returns `self` without edges also present in `that`.
1175 *
1176 * **When to use**
1177 *
1178 * Use when retaining all logical nodes from one graph while removing edge
1179 * relationships also represented by another graph.
1180 *
1181 * **Details**
1182 *
1183 * All nodes from `self` are preserved. Edges are matched by endpoint and edge
1184 * identities. The result has the same graph kind as `self`. Throws a
1185 * `GraphError` when the graph kinds do not match. `nodeIdentity` and
1186 * `edgeIdentity` default to the complete node and edge data.
1187 *
1188 * `G1 \ G2 = {V1, E1 \ E2}`
1189 *
1190 * **Gotchas**
1191 *
1192 * Nodes with equal identities in one input graph are coalesced. The last node
1193 * supplies the data, and redirected edges can collapse or become self-loops.
1194 * If `that` contains an edge identity, every parallel edge with that identity
1195 * is removed from `self`. The result allocates new node and edge identifiers.
1196 *
1197 * **Example** (Removing shared edges)
1198 *
1199 * ```ts import.meta.vitest
1200 * import { Graph } from "effect"
1201 *
1202 * const left = Graph.directed<string, string>((mutable) => {
1203 * const a = Graph.addNode(mutable, "A")
1204 * const b = Graph.addNode(mutable, "B")
1205 * const c = Graph.addNode(mutable, "C")
1206 * Graph.addEdge(mutable, a, b, "A-B")
1207 * Graph.addEdge(mutable, b, c, "B-C")
1208 * })
1209 *
1210 * const right = Graph.directed<string, string>((mutable) => {
1211 * const b = Graph.addNode(mutable, "B")
1212 * const c = Graph.addNode(mutable, "C")
1213 * Graph.addEdge(mutable, b, c, "B-C")
1214 * })
1215 *
1216 * const result = Graph.difference(left, right)
1217 *
1218 * Graph.nodeCount(result) // => 3
1219 * Graph.edgeCount(result) // => 1
1220 * ```
1221 *
1222 * @see {@link symmetricDifference} for retaining edges unique to either graph
1223 * @category set operations
1224 * @since 4.0.0
1225 */
1226export const difference: {
1227 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1228 that: Graph<N, E, T>,
1229 options?: IdentityOptions<N, E, NI, EI>
1230 ): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1231 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1232 self: Graph<N, E, T>,
1233 that: Graph<N, E, NoInfer<T>>,
1234 options?: IdentityOptions<N, E, NI, EI>
1235 ): Graph<N, E, T>
1236} = dual((args) => isGraph(args[0]) && isGraph(args[1]), <N, E, T extends Kind, NI, EI>(
1237 self: Graph<N, E, T>,
1238 that: Graph<N, E, T>,
1239 options?: IdentityOptions<N, E, NI, EI>
1240): Graph<N, E, T> => {
1241 assertSameKind(self, that)
1242 const selfImpl = internal.toImpl(self)
1243 const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1244 const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1245 const selfMaps = buildNodeMaps(self, getNodeIdentity)
1246 const thatMaps = buildNodeMaps(that, getNodeIdentity)
1247 const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity)
1248
1249 return make(self.type)<N, E>((mutable) => {
1250 const indexByIdentity = addNodesByIdentity(mutable, selfMaps.byIdentity)
1251
1252 for (const edge of selfImpl.edges.values()) {
1253 const sourceIdentity = nodeIdentityAt(selfMaps, edge.source)
1254 const targetIdentity = nodeIdentityAt(selfMaps, edge.target)
1255 const edgeIdentity = new EdgeIdentity(self.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data))
1256 if (!MutableHashMap.has(thatEdges, edgeIdentity)) {
1257 addEdgeByIdentity(mutable, indexByIdentity, edgeIdentity, edge.data)
1258 }
1259 }
1260 })
1261})
1262
1263/**
1264 * Returns edges present in exactly one of two graphs.
1265 *
1266 * **When to use**
1267 *
1268 * Use when comparing graphs and retaining relationships unique to either one.
1269 *
1270 * **Details**
1271 *
1272 * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
1273 * result has the same graph kind as `self`. Throws a `GraphError` when the
1274 * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
1275 * complete node and edge data. Edge identity also includes the endpoint
1276 * identities.
1277 *
1278 * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
1279 *
1280 * **Gotchas**
1281 *
1282 * Edges with different projected identities are distinct.
1283 * Nodes with equal identities in one input graph are coalesced. The last node
1284 * supplies the data, and redirected edges can collapse or become self-loops.
1285 * Parallel edges with equal identities are coalesced before the graphs are
1286 * compared. The result allocates new node and edge identifiers.
1287 *
1288 * **Example** (Finding differing edges)
1289 *
1290 * ```ts import.meta.vitest
1291 * import { Graph } from "effect"
1292 *
1293 * const left = Graph.directed<string, string>((mutable) => {
1294 * const a = Graph.addNode(mutable, "A")
1295 * const b = Graph.addNode(mutable, "B")
1296 * const c = Graph.addNode(mutable, "C")
1297 * Graph.addEdge(mutable, a, b, "A-B")
1298 * Graph.addEdge(mutable, b, c, "B-C")
1299 * })
1300 *
1301 * const right = Graph.directed<string, string>((mutable) => {
1302 * const b = Graph.addNode(mutable, "B")
1303 * const c = Graph.addNode(mutable, "C")
1304 * const d = Graph.addNode(mutable, "D")
1305 * Graph.addEdge(mutable, b, c, "B-C")
1306 * Graph.addEdge(mutable, c, d, "C-D")
1307 * })
1308 *
1309 * const result = Graph.symmetricDifference(left, right)
1310 *
1311 * Graph.nodeCount(result) // => 4
1312 * Graph.edgeCount(result) // => 2
1313 * ```
1314 *
1315 * @see {@link difference} for removing only the edges found in another graph
1316 * @category set operations
1317 * @since 4.0.0
1318 */
1319export const symmetricDifference: {
1320 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1321 that: Graph<N, E, T>,
1322 options?: IdentityOptions<N, E, NI, EI>
1323 ): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1324 <N, E, T extends Kind = "directed", NI = N, EI = E>(
1325 self: Graph<N, E, T>,
1326 that: Graph<N, E, NoInfer<T>>,
1327 options?: IdentityOptions<N, E, NI, EI>
1328 ): Graph<N, E, T>
1329} = dual((args) => isGraph(args[0]) && isGraph(args[1]), <N, E, T extends Kind, NI, EI>(
1330 self: Graph<N, E, T>,
1331 that: Graph<N, E, T>,
1332 options?: IdentityOptions<N, E, NI, EI>
1333): Graph<N, E, T> => {
1334 assertSameKind(self, that)
1335 const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1336 const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1337 const selfMaps = buildNodeMaps(self, getNodeIdentity)
1338 const thatMaps = buildNodeMaps(that, getNodeIdentity)
1339 const nodes = MutableHashMap.empty<NI, N>()
1340 const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity)
1341 const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity)
1342
1343 for (const [identity, data] of selfMaps.byIdentity) {
1344 MutableHashMap.set(nodes, identity, data)
1345 }
1346
1347 for (const [identity, data] of thatMaps.byIdentity) {
1348 MutableHashMap.set(nodes, identity, data)
1349 }
1350
1351 return make(self.type)<N, E>((mutable) => {
1352 const indexByIdentity = addNodesByIdentity(mutable, nodes)
1353
1354 for (const [identity, data] of selfEdges) {
1355 if (!MutableHashMap.has(thatEdges, identity)) {
1356 addEdgeByIdentity(mutable, indexByIdentity, identity, data)
1357 }
1358 }
1359
1360 for (const [identity, data] of thatEdges) {
1361 if (!MutableHashMap.has(selfEdges, identity)) {
1362 addEdgeByIdentity(mutable, indexByIdentity, identity, data)
1363 }
1364 }
1365 })
1366})
1367
1368/**
1369 * Returns the complement over the existing node set.
1370 *
1371 * **When to use**
1372 *
1373 * Use when materializing every relationship that is currently absent between
1374 * distinct nodes.
1375 *
1376 * **Details**
1377 *
1378 * Directed graphs add each missing ordered pair. Undirected graphs add each
1379 * missing unordered pair once. The `createEdge` function receives the source
1380 * and target node data for each added edge. The result has the same graph kind
1381 * as `self`.
1382 *
1383 * `G' = {V, ((V x V) without self-pairs) \ E}`
1384 *
1385 * **Gotchas**
1386 *
1387 * Self-loops are never created. If any edge already connects a pair, parallel
1388 * complement edges are not added. The result allocates new identifiers.
1389 *
1390 * **Example** (Finding missing relationships)
1391 *
1392 * ```ts import.meta.vitest
1393 * import { Graph } from "effect"
1394 *
1395 * const graph = Graph.directed<string, string>((mutable) => {
1396 * const a = Graph.addNode(mutable, "A")
1397 * const b = Graph.addNode(mutable, "B")
1398 * Graph.addEdge(mutable, a, b, "A-B")
1399 * })
1400 *
1401 * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
1402 *
1403 * Graph.edgeCount(result) // => 1
1404 * ```
1405 *
1406 * @see {@link hasEdge} for testing one relationship
1407 * @category set operations
1408 * @since 4.0.0
1409 */
1410export const complement: {
1411 <N, E>(
1412 createEdge: (source: N, target: N) => E
1413 ): <T extends Kind = "directed">(self: Graph<N, E, T>) => Graph<N, E, T>
1414 <N, E, T extends Kind = "directed">(
1415 self: Graph<N, E, T>,
1416 createEdge: (source: N, target: N) => E
1417 ): Graph<N, E, T>
1418} = dual(2, <N, E, T extends Kind>(
1419 self: Graph<N, E, T>,
1420 createEdge: (source: N, target: N) => E
1421): Graph<N, E, T> => {
1422 const cache = csr.get(self)
1423 const outgoing = csr.getOutgoing(cache)
1424 const neighborMarks = new Uint32Array(cache.nodeIds.length)
1425
1426 return make(self.type)<N, E>((mutable) => {
1427 const newIndices = new Uint32Array(cache.nodeIds.length)
1428
1429 for (let i = 0; i < cache.nodeIds.length; i++) {
1430 newIndices[i] = addNode(mutable, cache.nodeData[i] as N)
1431 }
1432
1433 for (let i = 0; i < cache.nodeIds.length; i++) {
1434 const generation = i + 1
1435 for (let edge = outgoing.rowOffsets[i]; edge < outgoing.rowOffsets[i + 1]; edge++) {
1436 neighborMarks[outgoing.columnIndices[edge]] = generation
1437 }
1438 const start = self.type === "undirected" ? i + 1 : 0
1439
1440 for (let j = start; j < cache.nodeIds.length; j++) {
1441 if (i === j || neighborMarks[j] === generation) {
1442 continue
1443 }
1444 addEdge(mutable, newIndices[i], newIndices[j], createEdge(cache.nodeData[i] as N, cache.nodeData[j] as N))
1445 }
1446 }
1447 })
1448})
1449
1450/**
1451 * Configuration for selecting a graph neighborhood.
1452 *
1453 * **Details**
1454 *
1455 * `radius` limits the edge distance from the center node and defaults to `1`.
1456 * It accepts non-negative integers and `Infinity`.
1457 * `direction` controls how directed edges are traversed and defaults to
1458 * `"outgoing"`.
1459 *
1460 * @category configuration
1461 * @since 4.0.0
1462 */
1463export interface NeighborhoodConfig {
1464 readonly radius?: number
1465 readonly direction?: TraversalDirection
1466}
1467
1468/**
1469 * Returns the induced subgraph containing nodes within a radius of a node.
1470 *
1471 * **When to use**
1472 *
1473 * Use when extracting a local reachable region around one node.
1474 *
1475 * **Details**
1476 *
1477 * The `radius` option is the maximum edge distance from `nodeIndex`, accepts
1478 * non-negative integers and `Infinity`, and defaults to `1`. Invalid radii
1479 * throw a `GraphError`. The `direction` option controls directed graph
1480 * traversal and defaults to `"outgoing"`. The result has the same graph kind
1481 * as `self` and keeps all original edges whose endpoints are both reached.
1482 * `"undirected"` ignores edge direction while finding reachable nodes.
1483 *
1484 * **Gotchas**
1485 *
1486 * Traversal chooses the nodes, then all original edges between reached nodes
1487 * are retained. The result is not merely a traversal tree, and it allocates new
1488 * node and edge identifiers.
1489 *
1490 * **Example** (Getting a local neighborhood)
1491 *
1492 * ```ts import.meta.vitest
1493 * import { Graph } from "effect"
1494 *
1495 * const graph = Graph.directed<string, string>((mutable) => {
1496 * const a = Graph.addNode(mutable, "A")
1497 * const b = Graph.addNode(mutable, "B")
1498 * const c = Graph.addNode(mutable, "C")
1499 * Graph.addEdge(mutable, a, b, "A-B")
1500 * Graph.addEdge(mutable, b, c, "B-C")
1501 * })
1502 *
1503 * const result = Graph.neighborhood(graph, 1, { radius: 1 })
1504 *
1505 * Graph.nodeCount(result) // => 2
1506 * ```
1507 *
1508 * @see {@link inducedSubgraph} for selecting nodes while preserving identifiers
1509 * @category set operations
1510 * @since 4.0.0
1511 */
1512export const neighborhood: {
1513 (
1514 nodeIndex: NodeIndex,
1515 options?: NeighborhoodConfig
1516 ): <N, E, T extends Kind = "directed">(self: Graph<N, E, T>) => Graph<N, E, T>
1517 <N, E, T extends Kind = "directed">(
1518 self: Graph<N, E, T>,
1519 nodeIndex: NodeIndex,
1520 options?: NeighborhoodConfig
1521 ): Graph<N, E, T>
1522} = dual((args) => isGraph(args[0]), <N, E, T extends Kind>(
1523 self: Graph<N, E, T>,
1524 nodeIndex: NodeIndex,
1525 options?: NeighborhoodConfig
1526): Graph<N, E, T> => {
1527 const selfImpl = internal.toImpl(self)
1528 const radius = traversalRadius(options?.radius, 1)
1529 const direction = options?.direction ?? "outgoing"
1530 const reached = new Set<NodeIndex>()
1531
1532 for (const index of indices(bfs(self, { start: [nodeIndex], direction, radius }))) {
1533 reached.add(index)
1534 }
1535
1536 return make(self.type)<N, E>((mutable) => {
1537 const newIndexMap = new Map<NodeIndex, NodeIndex>()
1538
1539 for (const oldIndex of reached) {
1540 newIndexMap.set(oldIndex, addNode(mutable, Option.getOrThrow(getNode(self, oldIndex))))
1541 }
1542
1543 for (const edge of selfImpl.edges.values()) {
1544 if (reached.has(edge.source) && reached.has(edge.target)) {
1545 const sourceIndex = newIndexMap.get(edge.source)
1546 const targetIndex = newIndexMap.get(edge.target)
1547 if (sourceIndex !== undefined && targetIndex !== undefined) {
1548 addEdge(mutable, sourceIndex, targetIndex, edge.data)
1549 }
1550 }
1551 }
1552 })
1553})
1554
1555/**
1556 * Returns the subgraph induced by a collection of node indices.
1557 *
1558 * **When to use**
1559 *
1560 * Use when selecting an exact node set while preserving its active node and
1561 * edge identifiers.
1562 *
1563 * **Details**
1564 *
1565 * Node and edge indices are preserved. Duplicate input indices are ignored,
1566 * output ordering follows the original graph, and every edge whose endpoints
1567 * are both selected is retained.
1568 *
1569 * **Gotchas**
1570 *
1571 * Throws a `GraphError` when a selected node does not exist.
1572 *
1573 * @see {@link neighborhood} for selecting nodes by traversal distance
1574 *
1575 * @category set operations
1576 * @since 4.0.0
1577 */
1578export const inducedSubgraph: {
1579 (nodeIndices: Iterable<NodeIndex>): <N, E, T extends Kind = "directed">(self: Graph<N, E, T>) => Graph<N, E, T>
1580 <N, E, T extends Kind = "directed">(
1581 self: Graph<N, E, T>,
1582 nodeIndices: Iterable<NodeIndex>
1583 ): Graph<N, E, T>
1584} = dual(2, <N, E, T extends Kind>(
1585 self: Graph<N, E, T>,
1586 nodeIndices: Iterable<NodeIndex>
1587): Graph<N, E, T> => {
1588 const impl = internal.toImpl(self)
1589 const selected = new Set<NodeIndex>()
1590 for (const nodeIndex of nodeIndices) {
1591 if (!impl.nodes.has(nodeIndex)) {
1592 throw missingNode(nodeIndex)
1593 }
1594 selected.add(nodeIndex)
1595 }
1596
1597 const nodes: Array<IndexedNode<N>> = []
1598 for (const [index, data] of impl.nodes) {
1599 if (selected.has(index)) {
1600 nodes.push({ index, data })
1601 }
1602 }
1603 const edges: Array<IndexedEdge<E>> = []
1604 for (const [index, edge] of impl.edges) {
1605 if (selected.has(edge.source) && selected.has(edge.target)) {
1606 edges.push({ index, source: edge.source, target: edge.target, data: edge.data })
1607 }
1608 }
1609 return fromSnapshot({ type: self.type, nodes, edges })
1610})
1611
1612/**
1613 * Returns the disjoint union of two graphs.
1614 *
1615 * **When to use**
1616 *
1617 * Use when combining graphs while keeping every node distinct, even when node
1618 * payloads are equal.
1619 *
1620 * **Details**
1621 *
1622 * Copies all nodes and edges from both graphs without merging equal node data.
1623 * The result has the same graph kind as `self`. Throws a `GraphError` when the
1624 * graph kinds do not match.
1625 *
1626 * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
1627 *
1628 * **Gotchas**
1629 *
1630 * All node and edge identifiers are newly allocated.
1631 *
1632 * @see {@link compose} for merging overlapping logical nodes by identity
1633 *
1634 * @category set operations
1635 * @since 4.0.0
1636 */
1637export const sum: {
1638 <N, E, T extends Kind>(that: Graph<N, E, T>): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1639 <N, E, T extends Kind>(self: Graph<N, E, T>, that: Graph<N, E, NoInfer<T>>): Graph<N, E, T>
1640} = dual(2, <N, E, T extends Kind>(self: Graph<N, E, T>, that: Graph<N, E, T>): Graph<N, E, T> => {
1641 assertSameKind(self, that)
1642 return make(self.type)<N, E>((mutable) => {
1643 const copyInto = (graph: Graph<N, E, T>) => {
1644 const impl = internal.toImpl(graph)
1645 const indexMap = new Map<NodeIndex, NodeIndex>()
1646
1647 for (const [oldIndex, data] of impl.nodes) {
1648 indexMap.set(oldIndex, addNode(mutable, data))
1649 }
1650
1651 for (const edge of impl.edges.values()) {
1652 const sourceIndex = indexMap.get(edge.source)
1653 const targetIndex = indexMap.get(edge.target)
1654 if (sourceIndex !== undefined && targetIndex !== undefined) {
1655 addEdge(mutable, sourceIndex, targetIndex, edge.data)
1656 }
1657 }
1658 }
1659
1660 copyInto(self)
1661 copyInto(that)
1662 })
1663})
1664
1665// =============================================================================
1666// Basic Node Operations
1667// =============================================================================
1668
1669/**
1670 * Adds a new node to a mutable graph and returns its index.
1671 *
1672 * **When to use**
1673 *
1674 * Use to allocate a new node in a mutable graph before storing edges or
1675 * querying it by index.
1676 *
1677 * **Details**
1678 *
1679 * The returned index is allocated from the graph's next node index. The mutable
1680 * graph stores the node data and initializes empty incoming and outgoing edge
1681 * indexes for the new node.
1682 *
1683 * **Gotchas**
1684 *
1685 * `NodeIndex` values are identifiers and are not reused after removals.
1686 *
1687 * **Example** (Adding nodes)
1688 *
1689 * ```ts import.meta.vitest
1690 * import { Graph } from "effect"
1691 *
1692 * Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1693 * Graph.addNode(mutable, "Node A") // => 0
1694 * Graph.addNode(mutable, "Node B") // => 1
1695 * })
1696 * ```
1697 *
1698 * @see {@link mutate} for obtaining a mutable graph from an immutable graph
1699 * @see {@link addEdge} for connecting existing nodes
1700 *
1701 * @category mutations
1702 * @since 3.18.0
1703 */
1704export const addNode = <N, E, T extends Kind = "directed">(
1705 mutable: MutableGraph<N, E, T>,
1706 data: N
1707): NodeIndex => {
1708 const impl = getMutableImplForMutation(mutable)
1709 const nodeIndex = impl.nextNodeIndex
1710 if (!Number.isSafeInteger(nodeIndex)) {
1711 throw new GraphError({ message: "Graph has exhausted safe node indexes" })
1712 }
1713
1714 // Add node data
1715 impl.nodes.set(nodeIndex, data)
1716
1717 // Initialize empty adjacency lists
1718 impl.adjacency.set(nodeIndex, [])
1719 impl.reverseAdjacency.set(nodeIndex, [])
1720
1721 // Update graph allocators
1722 impl.nextNodeIndex = impl.nextNodeIndex + 1
1723
1724 return nodeIndex
1725}
1726
1727/**
1728 * Gets the data associated with a node index safely, if it exists.
1729 *
1730 * **Example** (Getting node data)
1731 *
1732 * ```ts import.meta.vitest
1733 * import { Graph, Option } from "effect"
1734 *
1735 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1736 * Graph.addNode(mutable, "Node A")
1737 * })
1738 *
1739 * Graph.getNode(graph, 0) // => Option.some("Node A")
1740 * ```
1741 *
1742 * @category getters
1743 * @since 3.18.0
1744 */
1745export const getNode: {
1746 (nodeIndex: NodeIndex): <N, E, T extends Kind = "directed">(
1747 graph: Graph<N, E, T> | MutableGraph<N, E, T>
1748 ) => Option.Option<N>
1749 <N, E, T extends Kind = "directed">(
1750 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1751 nodeIndex: NodeIndex
1752 ): Option.Option<N>
1753} = dual(2, <N, E, T extends Kind = "directed">(
1754 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1755 nodeIndex: NodeIndex
1756): Option.Option<N> => {
1757 const impl = internal.toImpl(graph)
1758 return impl.nodes.has(nodeIndex) ? Option.some(impl.nodes.get(nodeIndex)!) : Option.none()
1759})
1760
1761/**
1762 * Checks whether a node with the given index exists in the graph.
1763 *
1764 * **Example** (Checking node existence)
1765 *
1766 * ```ts import.meta.vitest
1767 * import { Graph } from "effect"
1768 *
1769 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1770 * Graph.addNode(mutable, "Node A")
1771 * })
1772 *
1773 * Graph.hasNode(graph, 0) // => true
1774 * Graph.hasNode(graph, 999) // => false
1775 * ```
1776 *
1777 * @category predicates
1778 * @since 3.18.0
1779 */
1780export const hasNode: {
1781 (nodeIndex: NodeIndex): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => boolean
1782 <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>, nodeIndex: NodeIndex): boolean
1783} = dual(2, <N, E, T extends Kind = "directed">(
1784 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1785 nodeIndex: NodeIndex
1786): boolean => internal.toImpl(graph).nodes.has(nodeIndex))
1787
1788/**
1789 * Returns the number of nodes in the graph.
1790 *
1791 * **Example** (Counting nodes)
1792 *
1793 * ```ts import.meta.vitest
1794 * import { Graph } from "effect"
1795 *
1796 * const emptyGraph = Graph.directed<string, number>()
1797 * Graph.nodeCount(emptyGraph) // => 0
1798 *
1799 * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => {
1800 * Graph.addNode(mutable, "Node A")
1801 * Graph.addNode(mutable, "Node B")
1802 * Graph.addNode(mutable, "Node C")
1803 * })
1804 *
1805 * Graph.nodeCount(graphWithNodes) // => 3
1806 * ```
1807 *
1808 * @category getters
1809 * @since 3.18.0
1810 */
1811export const nodeCount = <N, E, T extends Kind = "directed">(
1812 graph: Graph<N, E, T> | MutableGraph<N, E, T>
1813): number => internal.toImpl(graph).nodes.size
1814
1815/**
1816 * Finds the first node that matches the given predicate.
1817 *
1818 * **Example** (Finding the first matching node)
1819 *
1820 * ```ts import.meta.vitest
1821 * import { Graph, Option } from "effect"
1822 *
1823 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1824 * Graph.addNode(mutable, "Node A")
1825 * Graph.addNode(mutable, "Node B")
1826 * Graph.addNode(mutable, "Node C")
1827 * })
1828 *
1829 * Graph.findNode(graph, (data) => data.startsWith("Node B")) // => Option.some(1)
1830 * Graph.findNode(graph, (data) => data === "Node D") // => Option.none()
1831 * ```
1832 *
1833 * @category getters
1834 * @since 3.18.0
1835 */
1836export const findNode: {
1837 <N>(
1838 predicate: (data: N) => boolean
1839 ): <E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<NodeIndex>
1840 <N, E, T extends Kind = "directed">(
1841 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1842 predicate: (data: N) => boolean
1843 ): Option.Option<NodeIndex>
1844} = dual(2, <N, E, T extends Kind = "directed">(
1845 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1846 predicate: (data: N) => boolean
1847): Option.Option<NodeIndex> => {
1848 const impl = internal.toImpl(graph)
1849 return withMutationGuard(graph, () => {
1850 for (const [index, data] of impl.nodes) {
1851 if (predicate(data)) {
1852 return Option.some(index)
1853 }
1854 }
1855 return Option.none()
1856 })
1857})
1858
1859/**
1860 * Finds all nodes that match the given predicate.
1861 *
1862 * **Example** (Finding matching nodes)
1863 *
1864 * ```ts import.meta.vitest
1865 * import { Graph } from "effect"
1866 *
1867 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1868 * Graph.addNode(mutable, "Start A")
1869 * Graph.addNode(mutable, "Node B")
1870 * Graph.addNode(mutable, "Start C")
1871 * })
1872 *
1873 * Graph.findNodes(graph, (data) => data.startsWith("Start")) // => [0, 2]
1874 * Graph.findNodes(graph, (data) => data === "Not Found") // => []
1875 * ```
1876 *
1877 * @category getters
1878 * @since 3.18.0
1879 */
1880export const findNodes: {
1881 <N>(
1882 predicate: (data: N) => boolean
1883 ): <E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Array<NodeIndex>
1884 <N, E, T extends Kind = "directed">(
1885 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1886 predicate: (data: N) => boolean
1887 ): Array<NodeIndex>
1888} = dual(2, <N, E, T extends Kind = "directed">(
1889 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1890 predicate: (data: N) => boolean
1891): Array<NodeIndex> => {
1892 const impl = internal.toImpl(graph)
1893 return withMutationGuard(graph, () => {
1894 const results: Array<NodeIndex> = []
1895 for (const [index, data] of impl.nodes) {
1896 if (predicate(data)) {
1897 results.push(index)
1898 }
1899 }
1900 return results
1901 })
1902})
1903
1904/**
1905 * Finds the first edge that matches the given predicate.
1906 *
1907 * **Example** (Finding the first matching edge)
1908 *
1909 * ```ts import.meta.vitest
1910 * import { Graph, Option } from "effect"
1911 *
1912 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1913 * const nodeA = Graph.addNode(mutable, "Node A")
1914 * const nodeB = Graph.addNode(mutable, "Node B")
1915 * const nodeC = Graph.addNode(mutable, "Node C")
1916 * Graph.addEdge(mutable, nodeA, nodeB, 10)
1917 * Graph.addEdge(mutable, nodeB, nodeC, 20)
1918 * })
1919 *
1920 * Graph.findEdge(graph, (data) => data > 15) // => Option.some(1)
1921 * Graph.findEdge(graph, (data) => data > 100) // => Option.none()
1922 * ```
1923 *
1924 * @category getters
1925 * @since 3.18.0
1926 */
1927export const findEdge: {
1928 <E>(
1929 predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1930 ): <N, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<EdgeIndex>
1931 <N, E, T extends Kind = "directed">(
1932 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1933 predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1934 ): Option.Option<EdgeIndex>
1935} = dual(2, <N, E, T extends Kind = "directed">(
1936 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1937 predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1938): Option.Option<EdgeIndex> => {
1939 const impl = internal.toImpl(graph)
1940 return withMutationGuard(graph, () => {
1941 for (const [edgeIndex, edgeData] of impl.edges) {
1942 if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
1943 return Option.some(edgeIndex)
1944 }
1945 }
1946 return Option.none()
1947 })
1948})
1949
1950/**
1951 * Finds all edges that match the given predicate.
1952 *
1953 * **Example** (Finding matching edges)
1954 *
1955 * ```ts import.meta.vitest
1956 * import { Graph } from "effect"
1957 *
1958 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1959 * const nodeA = Graph.addNode(mutable, "Node A")
1960 * const nodeB = Graph.addNode(mutable, "Node B")
1961 * const nodeC = Graph.addNode(mutable, "Node C")
1962 * Graph.addEdge(mutable, nodeA, nodeB, 10)
1963 * Graph.addEdge(mutable, nodeB, nodeC, 20)
1964 * Graph.addEdge(mutable, nodeC, nodeA, 30)
1965 * })
1966 *
1967 * Graph.findEdges(graph, (data) => data >= 20) // => [1, 2]
1968 * Graph.findEdges(graph, (data) => data > 100) // => []
1969 * ```
1970 *
1971 * @category getters
1972 * @since 3.18.0
1973 */
1974export const findEdges: {
1975 <E>(
1976 predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1977 ): <N, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Array<EdgeIndex>
1978 <N, E, T extends Kind = "directed">(
1979 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1980 predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1981 ): Array<EdgeIndex>
1982} = dual(2, <N, E, T extends Kind = "directed">(
1983 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1984 predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1985): Array<EdgeIndex> => {
1986 const impl = internal.toImpl(graph)
1987 return withMutationGuard(graph, () => {
1988 const results: Array<EdgeIndex> = []
1989 for (const [edgeIndex, edgeData] of impl.edges) {
1990 if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
1991 results.push(edgeIndex)
1992 }
1993 }
1994 return results
1995 })
1996})
1997
1998/**
1999 * Updates a single node's data by applying a transformation function.
2000 *
2001 * **When to use**
2002 *
2003 * Use when replacing one node payload while preserving its identifier and
2004 * incident edges.
2005 *
2006 * **Gotchas**
2007 *
2008 * A missing node index is ignored. The transformation may query the graph, but
2009 * cannot mutate or finalize the same graph while it runs.
2010 *
2011 * **Example** (Updating node data)
2012 *
2013 * ```ts import.meta.vitest
2014 * import { Graph, Option } from "effect"
2015 *
2016 * const graph = Graph.directed<string, number>((mutable) => {
2017 * Graph.addNode(mutable, "Node A")
2018 * Graph.addNode(mutable, "Node B")
2019 * Graph.updateNode(mutable, 0, (data) => data.toUpperCase())
2020 * })
2021 *
2022 * Graph.getNode(graph, 0) // => Option.some("NODE A")
2023 * ```
2024 *
2025 * @category transforming
2026 * @since 3.18.0
2027 */
2028export const updateNode = <N, E, T extends Kind = "directed">(
2029 mutable: MutableGraph<N, E, T>,
2030 index: NodeIndex,
2031 f: (data: N) => N
2032): void => {
2033 const impl = getMutableImplForMutation(mutable)
2034 internal.withTransformation(mutable, () => {
2035 if (!impl.nodes.has(index)) {
2036 return
2037 }
2038
2039 const currentData = impl.nodes.get(index)!
2040 const newData = f(currentData)
2041 impl.nodes.set(index, newData)
2042 })
2043}
2044
2045/**
2046 * Updates a single edge's data by applying a transformation function.
2047 *
2048 * **When to use**
2049 *
2050 * Use when replacing one edge payload while preserving its identifier and
2051 * endpoints.
2052 *
2053 * **Gotchas**
2054 *
2055 * A missing edge index is ignored. The transformation may query the graph, but
2056 * cannot mutate or finalize the same graph while it runs.
2057 *
2058 * **Example** (Updating edge data)
2059 *
2060 * ```ts import.meta.vitest
2061 * import { Graph, Option } from "effect"
2062 *
2063 * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
2064 * const nodeA = Graph.addNode(mutable, "Node A")
2065 * const nodeB = Graph.addNode(mutable, "Node B")
2066 * const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10)
2067 * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2)
2068 * })
2069 *
2070 * Option.map(Graph.getEdge(result, 0), (edge) => edge.data) // => Option.some(20)
2071 * ```
2072 *
2073 * @category transforming
2074 * @since 3.18.0
2075 */
2076export const updateEdge = <N, E, T extends Kind = "directed">(
2077 mutable: MutableGraph<N, E, T>,
2078 edgeIndex: EdgeIndex,
2079 f: (data: E) => E
2080): void => {
2081 const impl = getMutableImplForMutation(mutable)
2082 internal.withTransformation(mutable, () => {
2083 if (!impl.edges.has(edgeIndex)) {
2084 return
2085 }
2086
2087 const currentEdge = impl.edges.get(edgeIndex)!
2088 const newData = f(currentEdge.data)
2089 impl.edges.set(edgeIndex, {
2090 source: currentEdge.source,
2091 target: currentEdge.target,
2092 data: newData
2093 })
2094 })
2095}
2096
2097/**
2098 * Transforms every node's data in a mutable graph in place using the provided
2099 * mapping function.
2100 *
2101 * **When to use**
2102 *
2103 * Use when updating every node payload without changing graph structure.
2104 *
2105 * **Details**
2106 *
2107 * Node indices and edges are preserved; only the stored node data is replaced.
2108 *
2109 * **Gotchas**
2110 *
2111 * This function mutates in place, returns `void`, and cannot change the node
2112 * payload type. The mapping function may query the graph, but cannot mutate or
2113 * finalize the same graph while it runs.
2114 *
2115 * **Example** (Mapping node data)
2116 *
2117 * ```ts import.meta.vitest
2118 * import { Graph, Option } from "effect"
2119 *
2120 * const graph = Graph.directed<string, number>((mutable) => {
2121 * Graph.addNode(mutable, "node a")
2122 * Graph.addNode(mutable, "node b")
2123 * Graph.addNode(mutable, "node c")
2124 * Graph.mapNodes(mutable, (data) => data.toUpperCase())
2125 * })
2126 *
2127 * Graph.getNode(graph, 0) // => Option.some("NODE A")
2128 * ```
2129 *
2130 * @see {@link updateNode} for updating one node
2131 * @see {@link filterMapNodes} for mapping while removing nodes
2132 * @category mapping
2133 * @since 3.18.0
2134 */
2135export const mapNodes = <N, E, T extends Kind = "directed">(
2136 mutable: MutableGraph<N, E, T>,
2137 f: (data: N) => N
2138): void => {
2139 const impl = getMutableImplForMutation(mutable)
2140 internal.withTransformation(mutable, () => {
2141 // Transform existing node data in place
2142 for (const [index, data] of impl.nodes) {
2143 const newData = f(data)
2144 impl.nodes.set(index, newData)
2145 }
2146 })
2147}
2148
2149/**
2150 * Transforms every edge payload in a mutable graph in place.
2151 *
2152 * **When to use**
2153 *
2154 * Use when updating every edge payload without changing graph structure.
2155 *
2156 * **Details**
2157 *
2158 * Edge identifiers and endpoints are preserved.
2159 *
2160 * **Gotchas**
2161 *
2162 * This function mutates in place, returns `void`, and cannot change the edge
2163 * payload type. The mapping function may query the graph, but cannot mutate or
2164 * finalize the same graph while it runs.
2165 *
2166 * **Example** (Mapping edge data)
2167 *
2168 * ```ts import.meta.vitest
2169 * import { Graph, Option } from "effect"
2170 *
2171 * const graph = Graph.directed<string, number>((mutable) => {
2172 * const a = Graph.addNode(mutable, "A")
2173 * const b = Graph.addNode(mutable, "B")
2174 * const c = Graph.addNode(mutable, "C")
2175 * Graph.addEdge(mutable, a, b, 10)
2176 * Graph.addEdge(mutable, b, c, 20)
2177 * Graph.mapEdges(mutable, (data) => data * 2)
2178 * })
2179 *
2180 * Option.map(Graph.getEdge(graph, 0), (edge) => edge.data) // => Option.some(20)
2181 * ```
2182 *
2183 * @see {@link updateEdge} for updating one edge
2184 * @see {@link filterMapEdges} for mapping while removing edges
2185 * @category mapping
2186 * @since 3.18.0
2187 */
2188export const mapEdges = <N, E, T extends Kind = "directed">(
2189 mutable: MutableGraph<N, E, T>,
2190 f: (data: E) => E
2191): void => {
2192 const impl = getMutableImplForMutation(mutable)
2193 internal.withTransformation(mutable, () => {
2194 // Transform existing edge data in place
2195 for (const [index, edgeData] of impl.edges) {
2196 const newData = f(edgeData.data)
2197 impl.edges.set(index, {
2198 source: edgeData.source,
2199 target: edgeData.target,
2200 data: newData
2201 })
2202 }
2203 })
2204}
2205
2206/**
2207 * Swaps source and target nodes for every edge in a mutable graph.
2208 *
2209 * **When to use**
2210 *
2211 * Use when reversing every relationship in a directed graph, such as creating
2212 * a dependency transpose.
2213 *
2214 * **Details**
2215 *
2216 * Edge identifiers and payloads are preserved.
2217 *
2218 * **Gotchas**
2219 *
2220 * This operation is a no-op for undirected graphs.
2221 *
2222 * **Example** (Reversing edge directions)
2223 *
2224 * ```ts import.meta.vitest
2225 * import { Graph, Option } from "effect"
2226 *
2227 * const graph = Graph.directed<string, number>((mutable) => {
2228 * const a = Graph.addNode(mutable, "A")
2229 * const b = Graph.addNode(mutable, "B")
2230 * const c = Graph.addNode(mutable, "C")
2231 * Graph.addEdge(mutable, a, b, 1) // A -> B
2232 * Graph.addEdge(mutable, b, c, 2) // B -> C
2233 * Graph.reverse(mutable) // Now B -> A, C -> B
2234 * })
2235 *
2236 * Option.map(Graph.getEdge(graph, 0), (edge) => edge.source) // => Option.some(1)
2237 * ```
2238 *
2239 * @category transforming
2240 * @since 3.18.0
2241 */
2242export const reverse = <N, E, T extends Kind = "directed">(
2243 mutable: MutableGraph<N, E, T>
2244): void => {
2245 const impl = getMutableImplForMutation(mutable)
2246 if (impl.type === "undirected") {
2247 return
2248 }
2249
2250 // Reverse all edges by swapping source and target
2251 for (const [index, edgeData] of impl.edges) {
2252 impl.edges.set(index, {
2253 source: edgeData.target,
2254 target: edgeData.source,
2255 data: edgeData.data
2256 })
2257 }
2258
2259 const adjacency = impl.adjacency
2260 impl.adjacency = impl.reverseAdjacency
2261 impl.reverseAdjacency = adjacency
2262
2263 // Invalidate cycle flag since edge directions changed
2264 impl.acyclic = Option.none()
2265}
2266
2267/**
2268 * Filters and optionally transforms nodes in a mutable graph using a predicate function.
2269 * Nodes that return Option.none are removed along with all their connected edges.
2270 *
2271 * **Gotchas**
2272 *
2273 * The function may query the graph, but cannot mutate or finalize the same
2274 * graph while it runs. Retained payloads must remain the same node type.
2275 *
2276 * **Example** (Filtering and mapping nodes)
2277 *
2278 * ```ts import.meta.vitest
2279 * import { Graph, Option } from "effect"
2280 *
2281 * const graph = Graph.directed<string, number>((mutable) => {
2282 * const a = Graph.addNode(mutable, "active")
2283 * const b = Graph.addNode(mutable, "inactive")
2284 * const c = Graph.addNode(mutable, "active")
2285 * Graph.addEdge(mutable, a, b, 1)
2286 * Graph.addEdge(mutable, b, c, 2)
2287 *
2288 * // Keep only "active" nodes and transform to uppercase
2289 * Graph.filterMapNodes(
2290 * mutable,
2291 * (data) =>
2292 * data === "active" ? Option.some(data.toUpperCase()) : Option.none()
2293 * )
2294 * })
2295 *
2296 * Graph.nodeCount(graph) // => 2
2297 * ```
2298 *
2299 * @category filtering
2300 * @since 3.18.0
2301 */
2302export const filterMapNodes = <N, E, T extends Kind = "directed">(
2303 mutable: MutableGraph<N, E, T>,
2304 f: (data: N) => Option.Option<N>
2305): void => {
2306 const impl = getMutableImplForMutation(mutable)
2307 const remove: Array<NodeIndex> = []
2308 internal.withTransformation(mutable, () => {
2309 // First pass: identify nodes to remove and transform data for nodes to keep
2310 for (const [index, data] of impl.nodes) {
2311 const result = f(data)
2312 if (Option.isSome(result)) {
2313 // Transform node data
2314 impl.nodes.set(index, result.value)
2315 } else {
2316 // Mark for removal
2317 remove.push(index)
2318 }
2319 }
2320 })
2321
2322 // Second pass: remove filtered out nodes and their edges
2323 removeNodes(mutable, remove)
2324}
2325
2326/**
2327 * Filters and optionally transforms edges in a mutable graph using a predicate function.
2328 * Edges that return Option.none are removed from the graph.
2329 *
2330 * **Gotchas**
2331 *
2332 * The function may query the graph, but cannot mutate or finalize the same
2333 * graph while it runs. Retained payloads must remain the same edge type.
2334 *
2335 * **Example** (Filtering and mapping edges)
2336 *
2337 * ```ts import.meta.vitest
2338 * import { Graph, Option } from "effect"
2339 *
2340 * const graph = Graph.directed<string, number>((mutable) => {
2341 * const a = Graph.addNode(mutable, "A")
2342 * const b = Graph.addNode(mutable, "B")
2343 * const c = Graph.addNode(mutable, "C")
2344 * Graph.addEdge(mutable, a, b, 5)
2345 * Graph.addEdge(mutable, b, c, 15)
2346 * Graph.addEdge(mutable, c, a, 25)
2347 *
2348 * // Keep only edges with weight >= 10 and double their weight
2349 * Graph.filterMapEdges(
2350 * mutable,
2351 * (data) => data >= 10 ? Option.some(data * 2) : Option.none()
2352 * )
2353 * })
2354 *
2355 * Graph.edgeCount(graph) // => 2
2356 * ```
2357 *
2358 * @category filtering
2359 * @since 3.18.0
2360 */
2361export const filterMapEdges = <N, E, T extends Kind = "directed">(
2362 mutable: MutableGraph<N, E, T>,
2363 f: (data: E) => Option.Option<E>
2364): void => {
2365 const impl = getMutableImplForMutation(mutable)
2366 const remove: Array<EdgeIndex> = []
2367 internal.withTransformation(mutable, () => {
2368 // First pass: identify edges to remove and transform data for edges to keep
2369 for (const [index, edgeData] of impl.edges) {
2370 const result = f(edgeData.data)
2371 if (Option.isSome(result)) {
2372 // Transform edge data
2373 impl.edges.set(index, {
2374 source: edgeData.source,
2375 target: edgeData.target,
2376 data: result.value
2377 })
2378 } else {
2379 // Mark for removal
2380 remove.push(index)
2381 }
2382 }
2383 })
2384
2385 // Second pass: remove filtered out edges
2386 removeEdges(mutable, remove)
2387}
2388
2389/**
2390 * Filters nodes by removing those that don't match the predicate.
2391 * This function modifies the mutable graph in place.
2392 * Removed nodes also remove all incident edges; retained node identifiers are
2393 * preserved.
2394 *
2395 * **Gotchas**
2396 *
2397 * The predicate may query the graph, but cannot mutate or finalize the same
2398 * graph while it runs.
2399 *
2400 * **Example** (Filtering nodes)
2401 *
2402 * ```ts import.meta.vitest
2403 * import { Graph } from "effect"
2404 *
2405 * const graph = Graph.directed<string, number>((mutable) => {
2406 * Graph.addNode(mutable, "active")
2407 * Graph.addNode(mutable, "inactive")
2408 * Graph.addNode(mutable, "pending")
2409 * Graph.addNode(mutable, "active")
2410 *
2411 * // Keep only "active" nodes
2412 * Graph.filterNodes(mutable, (data) => data === "active")
2413 * })
2414 *
2415 * Graph.nodeCount(graph) // => 2
2416 * ```
2417 *
2418 * @category filtering
2419 * @since 3.18.0
2420 */
2421export const filterNodes = <N, E, T extends Kind = "directed">(
2422 mutable: MutableGraph<N, E, T>,
2423 predicate: (data: N) => boolean
2424): void => {
2425 const impl = getMutableImplForMutation(mutable)
2426 const remove: Array<NodeIndex> = []
2427
2428 internal.withTransformation(mutable, () => {
2429 // Identify nodes to remove
2430 for (const [index, data] of impl.nodes) {
2431 if (!predicate(data)) {
2432 remove.push(index)
2433 }
2434 }
2435 })
2436
2437 // Remove filtered out nodes (this also removes connected edges)
2438 removeNodes(mutable, remove)
2439}
2440
2441/**
2442 * Filters edges by removing those that don't match the predicate.
2443 * This function modifies the mutable graph in place.
2444 * Nodes are retained even when removing edges leaves them isolated.
2445 *
2446 * **Gotchas**
2447 *
2448 * The predicate may query the graph, but cannot mutate or finalize the same
2449 * graph while it runs.
2450 *
2451 * **Example** (Filtering edges)
2452 *
2453 * ```ts import.meta.vitest
2454 * import { Graph } from "effect"
2455 *
2456 * const graph = Graph.directed<string, number>((mutable) => {
2457 * const a = Graph.addNode(mutable, "A")
2458 * const b = Graph.addNode(mutable, "B")
2459 * const c = Graph.addNode(mutable, "C")
2460 *
2461 * Graph.addEdge(mutable, a, b, 5)
2462 * Graph.addEdge(mutable, b, c, 15)
2463 * Graph.addEdge(mutable, c, a, 25)
2464 *
2465 * // Keep only edges with weight >= 10
2466 * Graph.filterEdges(mutable, (data) => data >= 10)
2467 * })
2468 *
2469 * Graph.edgeCount(graph) // => 2
2470 * ```
2471 *
2472 * @category filtering
2473 * @since 3.18.0
2474 */
2475export const filterEdges = <N, E, T extends Kind = "directed">(
2476 mutable: MutableGraph<N, E, T>,
2477 predicate: (data: E) => boolean
2478): void => {
2479 const impl = getMutableImplForMutation(mutable)
2480 const remove: Array<EdgeIndex> = []
2481
2482 internal.withTransformation(mutable, () => {
2483 // Identify edges to remove
2484 for (const [index, edgeData] of impl.edges) {
2485 if (!predicate(edgeData.data)) {
2486 remove.push(index)
2487 }
2488 }
2489 })
2490
2491 // Remove filtered out edges
2492 removeEdges(mutable, remove)
2493}
2494
2495// =============================================================================
2496// Cycle Flag Management (Internal)
2497// =============================================================================
2498
2499/** @internal */
2500const invalidateCycleFlagOnRemoval = <N, E, T extends Kind = "directed">(
2501 mutable: internal.GraphImpl<N, E, T>
2502): void => {
2503 // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs).
2504 if (Option.isSome(mutable.acyclic) && mutable.acyclic.value === false) {
2505 mutable.acyclic = Option.none()
2506 }
2507}
2508
2509/** @internal */
2510const invalidateCycleFlagOnAddition = <N, E, T extends Kind = "directed">(
2511 mutable: internal.GraphImpl<N, E, T>
2512): void => {
2513 // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs).
2514 if (Option.isSome(mutable.acyclic) && mutable.acyclic.value === true) {
2515 mutable.acyclic = Option.none()
2516 }
2517}
2518
2519// =============================================================================
2520// Edge Operations
2521// =============================================================================
2522
2523/**
2524 * Adds a new edge to a mutable graph and returns its index.
2525 *
2526 * **When to use**
2527 *
2528 * Use to connect two existing nodes in a mutable graph while storing edge data
2529 * and receiving the new edge identifier.
2530 *
2531 * **Details**
2532 *
2533 * Self-loops and parallel edges are allowed. Undirected graphs retain the
2534 * supplied source and target orientation in the stored `Edge`, while traversal
2535 * and neighbor queries treat the connection as bidirectional.
2536 *
2537 * **Gotchas**
2538 *
2539 * The source and target nodes must already exist in the mutable graph; missing
2540 * endpoints throw a `GraphError`.
2541 *
2542 * **Example** (Adding edges)
2543 *
2544 * ```ts import.meta.vitest
2545 * import { Graph } from "effect"
2546 *
2547 * Graph.mutate(Graph.directed<string, number>(), (mutable) => {
2548 * const nodeA = Graph.addNode(mutable, "Node A")
2549 * const nodeB = Graph.addNode(mutable, "Node B")
2550 * Graph.addEdge(mutable, nodeA, nodeB, 42) // => 0
2551 * })
2552 * ```
2553 *
2554 * @see {@link mutate} for obtaining a mutable graph from an immutable graph
2555 * @see {@link addNode} for creating node indexes before connecting them
2556 *
2557 * @category mutations
2558 * @since 3.18.0
2559 */
2560export const addEdge = <N, E, T extends Kind = "directed">(
2561 mutable: MutableGraph<N, E, T>,
2562 source: NodeIndex,
2563 target: NodeIndex,
2564 data: E
2565): EdgeIndex => {
2566 const impl = getMutableImplForMutation(mutable)
2567
2568 // Validate that both nodes exist
2569 if (!impl.nodes.has(source)) {
2570 throw missingNode(source)
2571 }
2572 if (!impl.nodes.has(target)) {
2573 throw missingNode(target)
2574 }
2575
2576 const edgeIndex = impl.nextEdgeIndex
2577 if (!Number.isSafeInteger(edgeIndex)) {
2578 throw new GraphError({ message: "Graph has exhausted safe edge indexes" })
2579 }
2580
2581 // Create edge data
2582 const edgeData: Edge<E> = { source, target, data }
2583 impl.edges.set(edgeIndex, edgeData)
2584
2585 // Update adjacency lists
2586 const sourceAdjacency = impl.adjacency.get(source)
2587 if (sourceAdjacency !== undefined) {
2588 sourceAdjacency.push(edgeIndex)
2589 }
2590
2591 const targetReverseAdjacency = impl.reverseAdjacency.get(target)
2592 if (targetReverseAdjacency !== undefined) {
2593 targetReverseAdjacency.push(edgeIndex)
2594 }
2595
2596 // For undirected graphs, add reverse connections
2597 if (impl.type === "undirected") {
2598 const targetAdjacency = impl.adjacency.get(target)
2599 if (targetAdjacency !== undefined) {
2600 targetAdjacency.push(edgeIndex)
2601 }
2602
2603 const sourceReverseAdjacency = impl.reverseAdjacency.get(source)
2604 if (sourceReverseAdjacency !== undefined) {
2605 sourceReverseAdjacency.push(edgeIndex)
2606 }
2607 }
2608
2609 // Update allocators
2610 impl.nextEdgeIndex = impl.nextEdgeIndex + 1
2611
2612 // Only invalidate cycle flag if the graph was acyclic
2613 // Adding edges cannot remove cycles from cyclic graphs
2614 invalidateCycleFlagOnAddition(impl)
2615
2616 return edgeIndex
2617}
2618
2619/**
2620 * Removes a node and all its incident edges from a mutable graph.
2621 *
2622 * **Gotchas**
2623 *
2624 * A missing node index is ignored.
2625 *
2626 * **Example** (Removing a node)
2627 *
2628 * ```ts import.meta.vitest
2629 * import { Graph } from "effect"
2630 *
2631 * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
2632 * const nodeA = Graph.addNode(mutable, "Node A")
2633 * const nodeB = Graph.addNode(mutable, "Node B")
2634 * Graph.addEdge(mutable, nodeA, nodeB, 42)
2635 *
2636 * // Remove nodeA and all edges connected to it
2637 * Graph.removeNode(mutable, nodeA)
2638 * })
2639 * Array.of(Graph.nodeCount(result), Graph.edgeCount(result)) // => [1, 0]
2640 * ```
2641 *
2642 * @category mutations
2643 * @since 3.18.0
2644 */
2645export const removeNode = <N, E, T extends Kind = "directed">(
2646 mutable: MutableGraph<N, E, T>,
2647 nodeIndex: NodeIndex
2648): void => {
2649 const impl = getMutableImplForMutation(mutable)
2650 if (removeNodeInternal(impl, nodeIndex)) {
2651 invalidateCycleFlagOnRemoval(impl)
2652 }
2653}
2654
2655/**
2656 * Removes multiple nodes and all their incident edges from a mutable graph.
2657 *
2658 * **When to use**
2659 *
2660 * Use when deleting a collection of nodes in one mutation pass.
2661 *
2662 * **Details**
2663 *
2664 * The input is collected before mutation, so it may be backed by an iterator
2665 * over the same graph.
2666 *
2667 * **Gotchas**
2668 *
2669 * Missing and duplicate node indices are ignored. Removing a node also removes
2670 * all of its incident edges.
2671 *
2672 * @see {@link removeNode} for removing one node
2673 *
2674 * @category mutations
2675 * @since 4.0.0
2676 */
2677export const removeNodes = <N, E, T extends Kind = "directed">(
2678 mutable: MutableGraph<N, E, T>,
2679 nodeIndices: Iterable<NodeIndex>
2680): void => {
2681 assertMutable(mutable)
2682 if (internal.isTransforming(mutable)) {
2683 throw new GraphError({ message: "Cannot mutate graph during a transformation" })
2684 }
2685 const indices = internal.withTransformation(mutable, () => Array.from(nodeIndices))
2686 const impl = getMutableImplForMutation(mutable)
2687
2688 let removed = false
2689 for (const nodeIndex of indices) {
2690 if (removeNodeInternal(impl, nodeIndex)) {
2691 removed = true
2692 }
2693 }
2694
2695 if (removed) {
2696 invalidateCycleFlagOnRemoval(impl)
2697 }
2698}
2699
2700/** @internal */
2701const removeNodeInternal = <N, E, T extends Kind = "directed">(
2702 impl: internal.GraphImpl<N, E, T>,
2703 nodeIndex: NodeIndex
2704): boolean => {
2705 // Check if node exists
2706 if (!impl.nodes.has(nodeIndex)) {
2707 return false // Node doesn't exist, nothing to remove
2708 }
2709
2710 const edgesToRemove = new Set(impl.adjacency.get(nodeIndex)!)
2711 for (const edgeIndex of impl.reverseAdjacency.get(nodeIndex)!) {
2712 edgesToRemove.add(edgeIndex)
2713 }
2714
2715 for (const edgeIndex of edgesToRemove) {
2716 const edge = impl.edges.get(edgeIndex)!
2717 if (edge.source !== nodeIndex) {
2718 const adjacency = impl.adjacency.get(edge.source)!
2719 adjacency.splice(adjacency.indexOf(edgeIndex), 1)
2720 if (impl.type === "undirected") {
2721 const reverseAdjacency = impl.reverseAdjacency.get(edge.source)!
2722 reverseAdjacency.splice(reverseAdjacency.indexOf(edgeIndex), 1)
2723 }
2724 }
2725 if (edge.target !== nodeIndex) {
2726 const reverseAdjacency = impl.reverseAdjacency.get(edge.target)!
2727 reverseAdjacency.splice(reverseAdjacency.indexOf(edgeIndex), 1)
2728 if (impl.type === "undirected") {
2729 const adjacency = impl.adjacency.get(edge.target)!
2730 adjacency.splice(adjacency.indexOf(edgeIndex), 1)
2731 }
2732 }
2733 impl.edges.delete(edgeIndex)
2734 }
2735
2736 // Remove the node itself
2737 impl.nodes.delete(nodeIndex)
2738 impl.adjacency.delete(nodeIndex)
2739 impl.reverseAdjacency.delete(nodeIndex)
2740
2741 return true
2742}
2743
2744/**
2745 * Removes an edge from a mutable graph.
2746 *
2747 * **Gotchas**
2748 *
2749 * A missing edge index is ignored.
2750 *
2751 * **Example** (Removing an edge)
2752 *
2753 * ```ts import.meta.vitest
2754 * import { Graph } from "effect"
2755 *
2756 * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
2757 * const nodeA = Graph.addNode(mutable, "Node A")
2758 * const nodeB = Graph.addNode(mutable, "Node B")
2759 * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42)
2760 *
2761 * // Remove the edge
2762 * Graph.removeEdge(mutable, edge)
2763 * })
2764 * Array.of(Graph.nodeCount(result), Graph.edgeCount(result)) // => [2, 0]
2765 * ```
2766 *
2767 * @category mutations
2768 * @since 3.18.0
2769 */
2770export const removeEdge = <N, E, T extends Kind = "directed">(
2771 mutable: MutableGraph<N, E, T>,
2772 edgeIndex: EdgeIndex
2773): void => {
2774 const impl = getMutableImplForMutation(mutable)
2775 // Only invalidate cycle flag if an edge was actually removed
2776 // and only if the graph wasn't already known to be acyclic
2777 if (removeEdgeInternal(impl, edgeIndex)) {
2778 invalidateCycleFlagOnRemoval(impl)
2779 }
2780}
2781
2782/**
2783 * Removes multiple edges from a mutable graph.
2784 *
2785 * **When to use**
2786 *
2787 * Use when deleting a collection of edges in one mutation pass.
2788 *
2789 * **Details**
2790 *
2791 * The input is collected before mutation, so it may be backed by an iterator
2792 * over the same graph.
2793 *
2794 * **Gotchas**
2795 *
2796 * Missing and duplicate edge indices are ignored. Nodes are never removed.
2797 *
2798 * @see {@link removeEdge} for removing one edge
2799 *
2800 * @category mutations
2801 * @since 4.0.0
2802 */
2803export const removeEdges = <N, E, T extends Kind = "directed">(
2804 mutable: MutableGraph<N, E, T>,
2805 edgeIndices: Iterable<EdgeIndex>
2806): void => {
2807 assertMutable(mutable)
2808 if (internal.isTransforming(mutable)) {
2809 throw new GraphError({ message: "Cannot mutate graph during a transformation" })
2810 }
2811 const indices = internal.withTransformation(mutable, () => Array.from(edgeIndices))
2812 const impl = getMutableImplForMutation(mutable)
2813
2814 let removed = false
2815 for (const edgeIndex of indices) {
2816 if (removeEdgeInternal(impl, edgeIndex)) {
2817 removed = true
2818 }
2819 }
2820
2821 if (removed) {
2822 invalidateCycleFlagOnRemoval(impl)
2823 }
2824}
2825
2826/** @internal */
2827const removeEdgeInternal = <N, E, T extends Kind = "directed">(
2828 mutable: internal.GraphImpl<N, E, T>,
2829 edgeIndex: EdgeIndex
2830): boolean => {
2831 // Get edge data
2832 const edge = mutable.edges.get(edgeIndex)
2833 if (edge === undefined) {
2834 return false // Edge doesn't exist, no mutation occurred
2835 }
2836
2837 const { source, target } = edge
2838
2839 // Remove from adjacency lists
2840 const sourceAdjacency = mutable.adjacency.get(source)
2841 if (sourceAdjacency !== undefined) {
2842 const index = sourceAdjacency.indexOf(edgeIndex)
2843 if (index !== -1) {
2844 sourceAdjacency.splice(index, 1)
2845 }
2846 }
2847
2848 const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
2849 if (targetReverseAdjacency !== undefined) {
2850 const index = targetReverseAdjacency.indexOf(edgeIndex)
2851 if (index !== -1) {
2852 targetReverseAdjacency.splice(index, 1)
2853 }
2854 }
2855
2856 // For undirected graphs, remove reverse connections
2857 if (mutable.type === "undirected") {
2858 const targetAdjacency = mutable.adjacency.get(target)
2859 if (targetAdjacency !== undefined) {
2860 const index = targetAdjacency.indexOf(edgeIndex)
2861 if (index !== -1) {
2862 targetAdjacency.splice(index, 1)
2863 }
2864 }
2865
2866 const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
2867 if (sourceReverseAdjacency !== undefined) {
2868 const index = sourceReverseAdjacency.indexOf(edgeIndex)
2869 if (index !== -1) {
2870 sourceReverseAdjacency.splice(index, 1)
2871 }
2872 }
2873 }
2874
2875 // Remove edge data
2876 mutable.edges.delete(edgeIndex)
2877
2878 return true // Edge was successfully removed
2879}
2880
2881// =============================================================================
2882// Edge Query Operations
2883// =============================================================================
2884
2885/**
2886 * Gets the edge data associated with an edge index safely, if it exists.
2887 *
2888 * **Example** (Getting edge data)
2889 *
2890 * ```ts import.meta.vitest
2891 * import { Graph, Option } from "effect"
2892 *
2893 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
2894 * const nodeA = Graph.addNode(mutable, "Node A")
2895 * const nodeB = Graph.addNode(mutable, "Node B")
2896 * Graph.addEdge(mutable, nodeA, nodeB, 42)
2897 * })
2898 *
2899 * Graph.getEdge(graph, 0) // => Option.some({ source: 0, target: 1, data: 42 })
2900 * ```
2901 *
2902 * @category getters
2903 * @since 3.18.0
2904 */
2905export const getEdge: {
2906 (edgeIndex: EdgeIndex): <N, E, T extends Kind = "directed">(
2907 graph: Graph<N, E, T> | MutableGraph<N, E, T>
2908 ) => Option.Option<Edge<E>>
2909 <N, E, T extends Kind = "directed">(
2910 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2911 edgeIndex: EdgeIndex
2912 ): Option.Option<Edge<E>>
2913} = dual(2, <N, E, T extends Kind = "directed">(
2914 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2915 edgeIndex: EdgeIndex
2916): Option.Option<Edge<E>> => {
2917 const edge = internal.toImpl(graph).edges.get(edgeIndex)
2918 return edge === undefined ? Option.none() : Option.some(copyEdge(edge))
2919})
2920
2921/**
2922 * Checks whether an edge exists between two nodes in the graph.
2923 *
2924 * **Details**
2925 *
2926 * Directed graphs test only `source` to `target`; undirected graphs accept
2927 * either stored orientation. Parallel edges still produce one boolean result.
2928 *
2929 * **Gotchas**
2930 *
2931 * Returns `false` when either node does not exist.
2932 *
2933 * **Example** (Checking edge existence)
2934 *
2935 * ```ts import.meta.vitest
2936 * import { Graph } from "effect"
2937 *
2938 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
2939 * const nodeA = Graph.addNode(mutable, "Node A")
2940 * const nodeB = Graph.addNode(mutable, "Node B")
2941 * const nodeC = Graph.addNode(mutable, "Node C")
2942 * Graph.addEdge(mutable, nodeA, nodeB, 42)
2943 * })
2944 *
2945 * Graph.hasEdge(graph, 0, 1) // => true
2946 * Graph.hasEdge(graph, 0, 2) // => false
2947 * ```
2948 *
2949 * @see {@link edgesBetween} for all matching edge identifiers
2950 * @category predicates
2951 * @since 3.18.0
2952 */
2953export const hasEdge: {
2954 (
2955 source: NodeIndex,
2956 target: NodeIndex
2957 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => boolean
2958 <N, E, T extends Kind = "directed">(
2959 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2960 source: NodeIndex,
2961 target: NodeIndex
2962 ): boolean
2963} = dual(3, <N, E, T extends Kind = "directed">(
2964 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2965 source: NodeIndex,
2966 target: NodeIndex
2967): boolean => {
2968 const impl = internal.toImpl(graph)
2969 const adjacencyList = impl.adjacency.get(source)
2970 if (adjacencyList === undefined) {
2971 return false
2972 }
2973
2974 // Check if any edge in the adjacency list connects to the target
2975 for (const edgeIndex of adjacencyList) {
2976 const edge = impl.edges.get(edgeIndex)
2977 if (edge !== undefined) {
2978 const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target
2979 if (neighbor === target) {
2980 return true
2981 }
2982 }
2983 }
2984
2985 return false
2986})
2987
2988/**
2989 * Returns the number of edges in the graph.
2990 *
2991 * **Example** (Counting edges)
2992 *
2993 * ```ts import.meta.vitest
2994 * import { Graph } from "effect"
2995 *
2996 * const emptyGraph = Graph.directed<string, number>()
2997 * Graph.edgeCount(emptyGraph) // => 0
2998 *
2999 * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => {
3000 * const nodeA = Graph.addNode(mutable, "Node A")
3001 * const nodeB = Graph.addNode(mutable, "Node B")
3002 * const nodeC = Graph.addNode(mutable, "Node C")
3003 * Graph.addEdge(mutable, nodeA, nodeB, 1)
3004 * Graph.addEdge(mutable, nodeB, nodeC, 2)
3005 * Graph.addEdge(mutable, nodeC, nodeA, 3)
3006 * })
3007 *
3008 * Graph.edgeCount(graphWithEdges) // => 3
3009 * ```
3010 *
3011 * @category getters
3012 * @since 3.18.0
3013 */
3014export const edgeCount = <N, E, T extends Kind = "directed">(
3015 graph: Graph<N, E, T> | MutableGraph<N, E, T>
3016): number => internal.toImpl(graph).edges.size
3017
3018/**
3019 * Returns the indices of all edges incident to a node.
3020 *
3021 * **Details**
3022 *
3023 * Each edge is returned once in graph edge order, including self-loops.
3024 * Throws a `GraphError` when the node does not exist.
3025 *
3026 * @category getters
3027 * @since 4.0.0
3028 */
3029export const incidentEdges: {
3030 (
3031 nodeIndex: NodeIndex
3032 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Array<EdgeIndex>
3033 <N, E, T extends Kind = "directed">(
3034 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3035 nodeIndex: NodeIndex
3036 ): Array<EdgeIndex>
3037} = dual(2, <N, E, T extends Kind = "directed">(
3038 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3039 nodeIndex: NodeIndex
3040): Array<EdgeIndex> => {
3041 const impl = internal.toImpl(graph)
3042 if (!impl.nodes.has(nodeIndex)) {
3043 throw missingNode(nodeIndex)
3044 }
3045 const outgoing = impl.adjacency.get(nodeIndex)!
3046 if (graph.type === "undirected") {
3047 const result: Array<EdgeIndex> = []
3048 let previous = -1
3049 for (const edgeIndex of outgoing) {
3050 if (edgeIndex !== previous) {
3051 result.push(edgeIndex)
3052 previous = edgeIndex
3053 }
3054 }
3055 return result
3056 }
3057
3058 const incoming = impl.reverseAdjacency.get(nodeIndex)!
3059 const result: Array<EdgeIndex> = []
3060 let outgoingPosition = 0
3061 let incomingPosition = 0
3062 while (outgoingPosition < outgoing.length && incomingPosition < incoming.length) {
3063 const outgoingEdge = outgoing[outgoingPosition]
3064 const incomingEdge = incoming[incomingPosition]
3065 if (outgoingEdge < incomingEdge) {
3066 result.push(outgoingEdge)
3067 outgoingPosition++
3068 } else if (incomingEdge < outgoingEdge) {
3069 result.push(incomingEdge)
3070 incomingPosition++
3071 } else {
3072 result.push(outgoingEdge)
3073 outgoingPosition++
3074 incomingPosition++
3075 }
3076 }
3077 while (outgoingPosition < outgoing.length) result.push(outgoing[outgoingPosition++])
3078 while (incomingPosition < incoming.length) result.push(incoming[incomingPosition++])
3079 return result
3080})
3081
3082/**
3083 * Returns the indices of outgoing edges for a node in a directed graph.
3084 *
3085 * **Details**
3086 *
3087 * Parallel edges and self-loops are returned separately in adjacency order.
3088 * Throws a `GraphError` for an undirected graph or missing node.
3089 *
3090 * @category getters
3091 * @since 4.0.0
3092 */
3093export const outgoingEdges: {
3094 (nodeIndex: NodeIndex): <N, E>(
3095 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
3096 ) => Array<EdgeIndex>
3097 <N, E>(
3098 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3099 nodeIndex: NodeIndex
3100 ): Array<EdgeIndex>
3101} = dual(2, <N, E, T extends Kind = "directed">(
3102 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3103 nodeIndex: NodeIndex
3104): Array<EdgeIndex> => {
3105 if (graph.type === "undirected") {
3106 throw new GraphError({ message: "Cannot get outgoing edges of undirected graph" })
3107 }
3108 const impl = internal.toImpl(graph)
3109 if (!impl.nodes.has(nodeIndex)) {
3110 throw missingNode(nodeIndex)
3111 }
3112 return Array.from(impl.adjacency.get(nodeIndex)!)
3113})
3114
3115/**
3116 * Returns the indices of incoming edges for a node in a directed graph.
3117 *
3118 * **Details**
3119 *
3120 * Parallel edges and self-loops are returned separately in reverse-adjacency
3121 * order. Throws a `GraphError` for an undirected graph or missing node.
3122 *
3123 * @category getters
3124 * @since 4.0.0
3125 */
3126export const incomingEdges: {
3127 (nodeIndex: NodeIndex): <N, E>(
3128 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
3129 ) => Array<EdgeIndex>
3130 <N, E>(
3131 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3132 nodeIndex: NodeIndex
3133 ): Array<EdgeIndex>
3134} = dual(2, <N, E, T extends Kind = "directed">(
3135 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3136 nodeIndex: NodeIndex
3137): Array<EdgeIndex> => {
3138 if (graph.type === "undirected") {
3139 throw new GraphError({ message: "Cannot get incoming edges of undirected graph" })
3140 }
3141 const impl = internal.toImpl(graph)
3142 if (!impl.nodes.has(nodeIndex)) {
3143 throw missingNode(nodeIndex)
3144 }
3145 return Array.from(impl.reverseAdjacency.get(nodeIndex)!)
3146})
3147
3148/**
3149 * Returns all edge indices connecting the supplied nodes.
3150 *
3151 * **Details**
3152 *
3153 * Directed graphs only include edges from `source` to `target`; undirected
3154 * graphs include either stored orientation. Parallel edges are retained.
3155 * Throws a `GraphError` when either node does not exist.
3156 *
3157 * @category getters
3158 * @since 4.0.0
3159 */
3160export const edgesBetween: {
3161 (source: NodeIndex, target: NodeIndex): <N, E, T extends Kind = "directed">(
3162 graph: Graph<N, E, T> | MutableGraph<N, E, T>
3163 ) => Array<EdgeIndex>
3164 <N, E, T extends Kind = "directed">(
3165 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3166 source: NodeIndex,
3167 target: NodeIndex
3168 ): Array<EdgeIndex>
3169} = dual(3, <N, E, T extends Kind = "directed">(
3170 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3171 source: NodeIndex,
3172 target: NodeIndex
3173): Array<EdgeIndex> => {
3174 const impl = internal.toImpl(graph)
3175 if (!impl.nodes.has(source)) {
3176 throw missingNode(source)
3177 }
3178 if (!impl.nodes.has(target)) {
3179 throw missingNode(target)
3180 }
3181 const result: Array<EdgeIndex> = []
3182 let previous = -1
3183 for (const edgeIndex of impl.adjacency.get(source)!) {
3184 if (edgeIndex === previous) {
3185 continue
3186 }
3187 previous = edgeIndex
3188 const edge = impl.edges.get(edgeIndex)!
3189 const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target
3190 if (neighbor === target) {
3191 result.push(edgeIndex)
3192 }
3193 }
3194 return result
3195})
3196
3197/**
3198 * Returns the degree of a node in an undirected graph.
3199 *
3200 * **Details**
3201 *
3202 * Parallel edges count separately and a self-loop contributes two. Throws a
3203 * `GraphError` for a directed graph or missing node.
3204 *
3205 * @category getters
3206 * @since 4.0.0
3207 */
3208export const degree: {
3209 (nodeIndex: NodeIndex): <N, E>(
3210 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
3211 ) => number
3212 <N, E>(graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">, nodeIndex: NodeIndex): number
3213} = dual(2, <N, E, T extends Kind = "directed">(
3214 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3215 nodeIndex: NodeIndex
3216): number => {
3217 if (graph.type === "directed") {
3218 throw new GraphError({ message: "Cannot get degree of directed graph" })
3219 }
3220 const impl = internal.toImpl(graph)
3221 if (!impl.nodes.has(nodeIndex)) {
3222 throw missingNode(nodeIndex)
3223 }
3224 return impl.adjacency.get(nodeIndex)!.length
3225})
3226
3227/**
3228 * Returns the out-degree of a node in a directed graph.
3229 *
3230 * **Details**
3231 *
3232 * Parallel edges count separately and a self-loop contributes one. Throws a
3233 * `GraphError` for an undirected graph or missing node.
3234 *
3235 * @category getters
3236 * @since 4.0.0
3237 */
3238export const outDegree: {
3239 (nodeIndex: NodeIndex): <N, E>(
3240 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
3241 ) => number
3242 <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex: NodeIndex): number
3243} = dual(2, <N, E, T extends Kind = "directed">(
3244 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3245 nodeIndex: NodeIndex
3246): number => {
3247 if (graph.type === "undirected") {
3248 throw new GraphError({ message: "Cannot get outgoing edges of undirected graph" })
3249 }
3250 const impl = internal.toImpl(graph)
3251 if (!impl.nodes.has(nodeIndex)) {
3252 throw missingNode(nodeIndex)
3253 }
3254 return impl.adjacency.get(nodeIndex)!.length
3255})
3256
3257/**
3258 * Returns the in-degree of a node in a directed graph.
3259 *
3260 * **Details**
3261 *
3262 * Parallel edges count separately and a self-loop contributes one. Throws a
3263 * `GraphError` for an undirected graph or missing node.
3264 *
3265 * @category getters
3266 * @since 4.0.0
3267 */
3268export const inDegree: {
3269 (nodeIndex: NodeIndex): <N, E>(
3270 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
3271 ) => number
3272 <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex: NodeIndex): number
3273} = dual(2, <N, E, T extends Kind = "directed">(
3274 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3275 nodeIndex: NodeIndex
3276): number => {
3277 if (graph.type === "undirected") {
3278 throw new GraphError({ message: "Cannot get incoming edges of undirected graph" })
3279 }
3280 const impl = internal.toImpl(graph)
3281 if (!impl.nodes.has(nodeIndex)) {
3282 throw missingNode(nodeIndex)
3283 }
3284 return impl.reverseAdjacency.get(nodeIndex)!.length
3285})
3286
3287const getDirectedNeighbors = <N, E>(
3288 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3289 nodeIndex: NodeIndex,
3290 direction: Direction
3291): Array<NodeIndex> => {
3292 const impl = internal.toImpl(graph)
3293
3294 if (!graph.mutable) {
3295 const cache = csr.peek(graph)
3296 if (cache !== undefined) {
3297 const node = csr.getNodeIndex(cache, nodeIndex)
3298 if (node === undefined) {
3299 return []
3300 }
3301
3302 const adjacency = direction === "incoming"
3303 ? csr.getIncoming(cache)
3304 : csr.getOutgoing(cache)
3305
3306 const start = adjacency.rowOffsets[node]
3307 const result = new Array<NodeIndex>(adjacency.rowOffsets[node + 1] - start)
3308 for (let i = 0; i < result.length; i++) {
3309 result[i] = cache.nodeIds[adjacency.columnIndices[start + i]]
3310 }
3311
3312 return result
3313 }
3314 }
3315
3316 const adjacencyMap = direction === "incoming"
3317 ? impl.reverseAdjacency
3318 : impl.adjacency
3319
3320 const adjacencyList = adjacencyMap.get(nodeIndex)
3321 if (adjacencyList === undefined) {
3322 return []
3323 }
3324
3325 const result: Array<NodeIndex> = []
3326 for (const edgeIndex of adjacencyList) {
3327 const edge = impl.edges.get(edgeIndex)
3328 if (edge !== undefined) {
3329 result.push(direction === "incoming" ? edge.source : edge.target)
3330 }
3331 }
3332
3333 return result
3334}
3335
3336const getUniqueDirectedNeighbors = <N, E>(
3337 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3338 nodeIndex: NodeIndex,
3339 direction: Direction
3340): Array<NodeIndex> => {
3341 const neighbors = getDirectedNeighbors(graph, nodeIndex, direction)
3342 if (neighbors.length > ScanDegreeLimit) {
3343 return Array.from(new Set(neighbors))
3344 }
3345 // getDirectedNeighbors returns a fresh array, so duplicates are compacted in place
3346 let length = 0
3347 for (let i = 0; i < neighbors.length; i++) {
3348 const neighbor = neighbors[i]
3349 let j = 0
3350 while (j < length && neighbors[j] !== neighbor) j++
3351 if (j === length) neighbors[length++] = neighbor
3352 }
3353 neighbors.length = length
3354 return neighbors
3355}
3356
3357const ScanDegreeLimit = 32
3358
3359/**
3360 * Returns the neighboring node indices for a node.
3361 *
3362 * **Details**
3363 *
3364 * For directed graphs, neighbors are the targets of outgoing edges. For
3365 * undirected graphs, neighbors are the other endpoints of incident edges.
3366 * Each neighbor appears once in first edge occurrence order, including the
3367 * queried node when it has a self-loop.
3368 *
3369 * **Gotchas**
3370 *
3371 * Returns an empty array when the node does not exist. For directed graphs,
3372 * use `predecessors` when incoming neighbors are required.
3373 *
3374 * **Example** (Getting outgoing neighbors)
3375 *
3376 * ```ts import.meta.vitest
3377 * import { Graph } from "effect"
3378 *
3379 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
3380 * const nodeA = Graph.addNode(mutable, "Node A")
3381 * const nodeB = Graph.addNode(mutable, "Node B")
3382 * const nodeC = Graph.addNode(mutable, "Node C")
3383 * Graph.addEdge(mutable, nodeA, nodeB, 1)
3384 * Graph.addEdge(mutable, nodeA, nodeC, 2)
3385 * })
3386 *
3387 * Graph.neighbors(graph, 0) // => [1, 2]
3388 * Graph.neighbors(graph, 1) // => []
3389 * ```
3390 *
3391 * @category getters
3392 * @since 3.18.0
3393 */
3394export const neighbors: {
3395 (
3396 nodeIndex: NodeIndex
3397 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Array<NodeIndex>
3398 <N, E, T extends Kind = "directed">(
3399 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3400 nodeIndex: NodeIndex
3401 ): Array<NodeIndex>
3402} = dual(2, <N, E, T extends Kind = "directed">(
3403 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3404 nodeIndex: NodeIndex
3405): Array<NodeIndex> => {
3406 // For undirected graphs, use the specialized helper that returns the other endpoint
3407 if (graph.type === "undirected") {
3408 return getUndirectedNeighbors(graph as any, nodeIndex)
3409 }
3410
3411 return getUniqueDirectedNeighbors(graph as any, nodeIndex, "outgoing")
3412})
3413
3414/**
3415 * Returns the outgoing neighbor node indices for a node in a directed graph.
3416 *
3417 * **When to use**
3418 *
3419 * Use when you need the nodes reached by following outgoing edges from a node in
3420 * a directed graph.
3421 *
3422 * Each node appears once in first outgoing edge occurrence order. A self-loop
3423 * contributes the queried node once.
3424 *
3425 * **Gotchas**
3426 *
3427 * Throws a `GraphError` when used with an undirected graph. A missing node
3428 * returns an empty array.
3429 *
3430 * @see {@link predecessors} for incoming neighbors in a directed graph
3431 * @see {@link neighbors} for generic neighbor lookup across graph kinds
3432 *
3433 * @category getters
3434 * @since 4.0.0
3435 */
3436export const successors: {
3437 (
3438 nodeIndex: NodeIndex
3439 ): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => Array<NodeIndex>
3440 <N, E>(
3441 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3442 nodeIndex: NodeIndex
3443 ): Array<NodeIndex>
3444} = dual(2, <N, E, T extends Kind = "directed">(
3445 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3446 nodeIndex: NodeIndex
3447): Array<NodeIndex> => {
3448 if (graph.type === "undirected") {
3449 throw new GraphError({ message: "Cannot get successors of undirected graph" })
3450 }
3451 return getUniqueDirectedNeighbors(graph as any, nodeIndex, "outgoing")
3452})
3453
3454/**
3455 * Returns the incoming neighbor node indices for a node in a directed graph.
3456 *
3457 * **When to use**
3458 *
3459 * Use when you need the nodes that reach a node by following incoming edges in a
3460 * directed graph.
3461 *
3462 * Each node appears once in first incoming edge occurrence order. A self-loop
3463 * contributes the queried node once.
3464 *
3465 * **Gotchas**
3466 *
3467 * Throws a `GraphError` when used with an undirected graph. A missing node
3468 * returns an empty array.
3469 *
3470 * @see {@link successors} for outgoing neighbors in a directed graph
3471 * @see {@link neighbors} for generic neighbor lookup across graph kinds
3472 *
3473 * @category getters
3474 * @since 4.0.0
3475 */
3476export const predecessors: {
3477 (
3478 nodeIndex: NodeIndex
3479 ): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => Array<NodeIndex>
3480 <N, E>(
3481 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3482 nodeIndex: NodeIndex
3483 ): Array<NodeIndex>
3484} = dual(2, <N, E, T extends Kind = "directed">(
3485 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3486 nodeIndex: NodeIndex
3487): Array<NodeIndex> => {
3488 if (graph.type === "undirected") {
3489 throw new GraphError({ message: "Cannot get predecessors of undirected graph" })
3490 }
3491 return getUniqueDirectedNeighbors(graph as any, nodeIndex, "incoming")
3492})
3493
3494/**
3495 * Gets directed neighbors of a node in a specific direction.
3496 *
3497 * **When to use**
3498 *
3499 * Use when maintaining existing code that already passes an explicit traversal
3500 * direction. New code should prefer `successors` or `predecessors`.
3501 * Results contain each node once in first edge occurrence order, and a self-loop
3502 * contributes the queried node once.
3503 *
3504 * **Gotchas**
3505 *
3506 * Throws a `GraphError` when used with an undirected graph.
3507 *
3508 * **Example** (Traversing directed neighbors)
3509 *
3510 * ```ts import.meta.vitest
3511 * import { Graph } from "effect"
3512 *
3513 * const graph = Graph.directed<string, string>((mutable) => {
3514 * const a = Graph.addNode(mutable, "A")
3515 * const b = Graph.addNode(mutable, "B")
3516 * Graph.addEdge(mutable, a, b, "A->B")
3517 * })
3518 *
3519 * const nodeA = 0
3520 * const nodeB = 1
3521 *
3522 * // Get outgoing neighbors (nodes that nodeA points to)
3523 * const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing")
3524 *
3525 * // Get incoming neighbors (nodes that point to nodeB)
3526 * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
3527 * Array.of(outgoing, incoming) // => [[1], [0]]
3528 * ```
3529 *
3530 * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors.
3531 * @see {@link successors} for outgoing neighbors in a directed graph
3532 * @see {@link predecessors} for incoming neighbors in a directed graph
3533 * @category getters
3534 * @since 3.18.0
3535 */
3536export const neighborsDirected: {
3537 (
3538 nodeIndex: NodeIndex,
3539 direction: Direction
3540 ): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => Array<NodeIndex>
3541 <N, E>(
3542 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3543 nodeIndex: NodeIndex,
3544 direction: Direction
3545 ): Array<NodeIndex>
3546} = dual(3, <N, E, T extends Kind = "directed">(
3547 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3548 nodeIndex: NodeIndex,
3549 direction: Direction
3550): Array<NodeIndex> => {
3551 if (graph.type === "undirected") {
3552 throw new GraphError({ message: "Cannot get directed neighbors of undirected graph" })
3553 }
3554 return getUniqueDirectedNeighbors(graph as any, nodeIndex, direction)
3555})
3556
3557// =============================================================================
3558// GraphViz Export
3559// =============================================================================
3560
3561/**
3562 * Configuration options for GraphViz DOT format generation from graphs.
3563 *
3564 * **When to use**
3565 *
3566 * Use when customizing labels or the graph name produced by `toGraphViz`.
3567 *
3568 * **Details**
3569 *
3570 * These options customize node labels, edge labels, and graph naming in DOT
3571 * format compatible with GraphViz tools.
3572 *
3573 * **Example** (Configuring GraphViz labels)
3574 *
3575 * ```ts import.meta.vitest
3576 * import type { Graph } from "effect"
3577 *
3578 * // Basic options with custom labels
3579 * const basicOptions: Graph.GraphVizOptions<string, number> = {
3580 * nodeLabel: (data) => `Node: ${data}`,
3581 * edgeLabel: (data) => `Weight: ${data}`
3582 * }
3583 *
3584 * // Complete options with graph naming
3585 * const namedOptions: Graph.GraphVizOptions<string, string> = {
3586 * nodeLabel: (data) => data.toUpperCase(),
3587 * edgeLabel: (data) => data,
3588 * graphName: "MyDependencyGraph"
3589 * }
3590 * Array.of(basicOptions.nodeLabel?.("A"), namedOptions.graphName) // => ["Node: A", "MyDependencyGraph"]
3591 * ```
3592 *
3593 * @see {@link toGraphViz} for generating DOT output
3594 * @category configuration
3595 * @since 3.18.0
3596 */
3597export interface GraphVizOptions<N, E> {
3598 /**
3599 * Function to generate custom labels for nodes.
3600 * Defaults to String(data) if not provided.
3601 */
3602 readonly nodeLabel?: (data: N) => string
3603
3604 /**
3605 * Function to generate custom labels for edges.
3606 * Defaults to String(data) if not provided.
3607 */
3608 readonly edgeLabel?: (data: E) => string
3609
3610 /**
3611 * Name for the DOT graph.
3612 * Defaults to "G" if not provided.
3613 */
3614 readonly graphName?: string
3615}
3616
3617const escapeGraphVizString = (value: string): string =>
3618 value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r\n|\r|\n/g, "\\n")
3619
3620/**
3621 * Exports a graph to GraphViz DOT format for visualization.
3622 *
3623 * **When to use**
3624 *
3625 * Use when sending graph structure to GraphViz-compatible visualization or
3626 * documentation tools.
3627 *
3628 * **Example** (Exporting GraphViz DOT)
3629 *
3630 * ```ts import.meta.vitest
3631 * import { Graph } from "effect"
3632 *
3633 * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
3634 * const nodeA = Graph.addNode(mutable, "Node A")
3635 * const nodeB = Graph.addNode(mutable, "Node B")
3636 * const nodeC = Graph.addNode(mutable, "Node C")
3637 * Graph.addEdge(mutable, nodeA, nodeB, 1)
3638 * Graph.addEdge(mutable, nodeB, nodeC, 2)
3639 * Graph.addEdge(mutable, nodeC, nodeA, 3)
3640 * })
3641 *
3642 * Graph.toGraphViz(graph).split("\n") // => ['digraph "G" {', ' "0" [label="Node A"];', ' "1" [label="Node B"];', ' "2" [label="Node C"];', ' "0" -> "1" [label="1"];', ' "1" -> "2" [label="2"];', ' "2" -> "0" [label="3"];', "}"]
3643 * ```
3644 *
3645 * @see {@link toMermaid} for Mermaid diagram output
3646 * @category converting
3647 * @since 3.18.0
3648 */
3649export const toGraphViz: {
3650 <N, E>(
3651 options?: GraphVizOptions<N, E>
3652 ): <T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => string
3653 <N, E, T extends Kind = "directed">(
3654 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3655 options?: GraphVizOptions<N, E>
3656 ): string
3657} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
3658 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3659 options?: GraphVizOptions<N, E>
3660): string => {
3661 const impl = internal.toImpl(graph)
3662 const {
3663 edgeLabel = (data: E) => String(data),
3664 graphName = "G",
3665 nodeLabel = (data: N) => String(data)
3666 } = options ?? {}
3667
3668 const isDirected = graph.type === "directed"
3669 const graphType = isDirected ? "digraph" : "graph"
3670 const edgeOperator = isDirected ? "->" : "--"
3671 const graphId = `"${escapeGraphVizString(graphName)}"`
3672
3673 return withMutationGuard(graph, () => {
3674 const lines: Array<string> = []
3675 lines.push(`${graphType} ${graphId} {`)
3676
3677 // Add nodes
3678 for (const [nodeIndex, nodeData] of impl.nodes) {
3679 const label = escapeGraphVizString(nodeLabel(nodeData))
3680 lines.push(` "${nodeIndex}" [label="${label}"];`)
3681 }
3682
3683 // Add edges
3684 for (const [, edgeData] of impl.edges) {
3685 const label = escapeGraphVizString(edgeLabel(edgeData.data))
3686 lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`)
3687 }
3688
3689 lines.push("}")
3690 return lines.join("\n")
3691 })
3692})
3693
3694// =============================================================================
3695// Mermaid Export
3696// =============================================================================
3697
3698/**
3699 * Mermaid node shape types for diagram visualization.
3700 *
3701 * **Details**
3702 *
3703 * Each shape produces different visual representations in Mermaid diagrams:
3704 * - `rectangle`: Standard rectangular nodes `A["label"]`
3705 * - `rounded`: Rounded rectangular nodes `A("label")`
3706 * - `circle`: Circular nodes `A(("label"))`
3707 * - `diamond`: Diamond-shaped nodes `A{"label"}`
3708 * - `hexagon`: Hexagonal nodes `A{{"label"}}`
3709 * - `stadium`: Stadium-shaped nodes `A(["label"])`
3710 * - `subroutine`: Subroutine-style nodes `A[["label"]]`
3711 * - `cylindrical`: Cylindrical database-style nodes `A[("label")]`
3712 *
3713 * **Example** (Selecting Mermaid node shapes)
3714 *
3715 * ```ts import.meta.vitest
3716 * import type { Graph } from "effect"
3717 *
3718 * // Shape selector function for different node types
3719 * const shapeSelector = (nodeData: string): Graph.MermaidNodeShape => {
3720 * if (nodeData.includes("start") || nodeData.includes("end")) return "circle"
3721 * if (nodeData.includes("decision")) return "diamond"
3722 * if (nodeData.includes("process")) return "rectangle"
3723 * if (nodeData.includes("data")) return "cylindrical"
3724 * return "rounded"
3725 * }
3726 *
3727 * const options: Graph.MermaidOptions<string, string> = {
3728 * nodeShape: shapeSelector
3729 * }
3730 * options.nodeShape?.("decision") // => "diamond"
3731 * ```
3732 *
3733 * @category models
3734 * @since 3.18.0
3735 */
3736export type MermaidNodeShape =
3737 | "rectangle" // A["label"]
3738 | "rounded" // A("label")
3739 | "circle" // A(("label"))
3740 | "diamond" // A{"label"}
3741 | "hexagon" // A{{"label"}}
3742 | "stadium" // A(["label"])
3743 | "subroutine" // A[["label"]]
3744 | "cylindrical" // A[("label")]
3745
3746/**
3747 * Mermaid diagram direction types for controlling layout orientation.
3748 *
3749 * **Details**
3750 *
3751 * Determines the flow direction of nodes and edges in the diagram:
3752 * - `TB`/`TD`: Top to Bottom (vertical layout, default)
3753 * - `BT`: Bottom to Top (reverse vertical)
3754 * - `LR`: Left to Right (horizontal layout)
3755 * - `RL`: Right to Left (reverse horizontal)
3756 *
3757 * **Example** (Configuring Mermaid directions)
3758 *
3759 * ```ts import.meta.vitest
3760 * import type { Graph } from "effect"
3761 *
3762 * // Horizontal workflow diagram
3763 * const horizontalOptions: Graph.MermaidOptions<string, string> = {
3764 * direction: "LR"
3765 * }
3766 *
3767 * // Vertical hierarchy (default)
3768 * const verticalOptions: Graph.MermaidOptions<string, string> = {
3769 * direction: "TB"
3770 * }
3771 *
3772 * // Bottom-up flow
3773 * const bottomUpOptions: Graph.MermaidOptions<string, string> = {
3774 * direction: "BT"
3775 * }
3776 * Array.of(horizontalOptions.direction, verticalOptions.direction, bottomUpOptions.direction) // => ["LR", "TB", "BT"]
3777 * ```
3778 *
3779 * @category models
3780 * @since 3.18.0
3781 */
3782export type MermaidDirection =
3783 | "TB" // Top to Bottom (default)
3784 | "TD" // Top Down (same as TB)
3785 | "BT" // Bottom to Top
3786 | "RL" // Right to Left
3787 | "LR" // Left to Right
3788
3789/**
3790 * Mermaid diagram types for different visualization formats.
3791 *
3792 * **Details**
3793 *
3794 * Specifies the Mermaid diagram syntax to use:
3795 * - `flowchart`: For directed graphs with arrows (`A --> B`)
3796 * - `graph`: For undirected graphs with lines (`A --- B`)
3797 *
3798 * When not specified, automatically selects based on graph type:
3799 * directed graphs use "flowchart", undirected graphs use "graph".
3800 *
3801 * **Example** (Selecting Mermaid diagram types)
3802 *
3803 * ```ts import.meta.vitest
3804 * import type { Graph } from "effect"
3805 *
3806 * // Force flowchart format (even for undirected graphs)
3807 * const flowchartOptions: Graph.MermaidOptions<string, string> = {
3808 * diagramType: "flowchart"
3809 * }
3810 *
3811 * // Force graph format (shows undirected connections)
3812 * const graphOptions: Graph.MermaidOptions<string, string> = {
3813 * diagramType: "graph"
3814 * }
3815 *
3816 * // Auto-detection (recommended, default behavior)
3817 * const autoOptions: Graph.MermaidOptions<string, string> = {}
3818 * Array.of(flowchartOptions.diagramType, graphOptions.diagramType, autoOptions.diagramType) // => ["flowchart", "graph", undefined]
3819 * ```
3820 *
3821 * @category models
3822 * @since 3.18.0
3823 */
3824export type MermaidDiagramType =
3825 | "flowchart" // For directed graphs
3826 | "graph" // For undirected graphs
3827
3828/**
3829 * Configuration options for Mermaid diagram generation from graphs.
3830 *
3831 * **When to use**
3832 *
3833 * Use when customizing labels, layout, node shapes, or syntax emitted by
3834 * `toMermaid`.
3835 *
3836 * **Details**
3837 *
3838 * These options customize node labels, edge labels, diagram type, layout
3839 * direction and node shapes in Mermaid format.
3840 *
3841 * **Example** (Configuring Mermaid output)
3842 *
3843 * ```ts import.meta.vitest
3844 * import type { Graph } from "effect"
3845 *
3846 * // Basic options with custom labels
3847 * const basicOptions: Graph.MermaidOptions<string, number> = {
3848 * nodeLabel: (data) => `Node: ${data}`,
3849 * edgeLabel: (data) => `Weight: ${data}`
3850 * }
3851 *
3852 * // Advanced options with all features
3853 * const advancedOptions: Graph.MermaidOptions<string, string> = {
3854 * nodeLabel: (data) => data.toUpperCase(),
3855 * edgeLabel: (data) => data,
3856 * diagramType: "flowchart",
3857 * direction: "LR",
3858 * nodeShape: (data) => data.includes("start") ? "circle" : "rectangle"
3859 * }
3860 * Array.of(basicOptions.nodeLabel?.("A"), advancedOptions.nodeShape?.("start")) // => ["Node: A", "circle"]
3861 * ```
3862 *
3863 * @see {@link toMermaid} for generating Mermaid output
3864 * @category configuration
3865 * @since 3.18.0
3866 */
3867export interface MermaidOptions<N, E> {
3868 /**
3869 * Function to generate custom labels for nodes.
3870 * Defaults to String(data) if not provided.
3871 */
3872 readonly nodeLabel?: (data: N) => string
3873
3874 /**
3875 * Function to generate custom labels for edges.
3876 * Defaults to String(data) if not provided.
3877 */
3878 readonly edgeLabel?: (data: E) => string
3879
3880 /**
3881 * Diagram type override. If not specified, automatically detects:
3882 * - "flowchart" for directed graphs
3883 * - "graph" for undirected graphs
3884 */
3885 readonly diagramType?: MermaidDiagramType
3886
3887 /**
3888 * Direction for diagram layout.
3889 * Defaults to "TD" (Top Down) if not provided.
3890 */
3891 readonly direction?: MermaidDirection
3892
3893 /**
3894 * Function to determine node shape for each node.
3895 * Defaults to "rectangle" for all nodes if not provided.
3896 */
3897 readonly nodeShape?: (data: N) => MermaidNodeShape
3898}
3899
3900/**
3901 * Escapes special characters in labels for Mermaid syntax compatibility.
3902 */
3903const escapeMermaidLabel = (label: string): string => {
3904 // Escape special characters for Mermaid using HTML entity codes
3905 // According to: https://mermaid.js.org/syntax/flowchart.html#special-characters-that-break-syntax
3906 return label
3907 .replace(/#/g, "#35;")
3908 .replace(/"/g, "#quot;")
3909 .replace(/</g, "#lt;")
3910 .replace(/>/g, "#gt;")
3911 .replace(/&/g, "#amp;")
3912 .replace(/\[/g, "#91;")
3913 .replace(/\]/g, "#93;")
3914 .replace(/\{/g, "#123;")
3915 .replace(/\}/g, "#125;")
3916 .replace(/\(/g, "#40;")
3917 .replace(/\)/g, "#41;")
3918 .replace(/\|/g, "#124;")
3919 .replace(/\\/g, "#92;")
3920 .replace(/\r\n|\r|\n/g, "<br/>")
3921}
3922
3923/**
3924 * Formats a Mermaid node with the specified shape and label.
3925 */
3926const formatMermaidNode = (
3927 nodeId: string,
3928 label: string,
3929 shape: MermaidNodeShape
3930): string => {
3931 switch (shape) {
3932 case "rectangle":
3933 return `${nodeId}["${label}"]`
3934 case "rounded":
3935 return `${nodeId}("${label}")`
3936 case "circle":
3937 return `${nodeId}(("${label}"))`
3938 case "diamond":
3939 return `${nodeId}{"${label}"}`
3940 case "hexagon":
3941 return `${nodeId}{{"${label}"}}`
3942 case "stadium":
3943 return `${nodeId}(["${label}"])`
3944 case "subroutine":
3945 return `${nodeId}[["${label}"]]`
3946 case "cylindrical":
3947 return `${nodeId}[("${label}")]`
3948 default:
3949 return `${nodeId}["${label}"]` // Default rectangle
3950 }
3951}
3952
3953/**
3954 * Exports a graph to Mermaid diagram format for visualization.
3955 *
3956 * **When to use**
3957 *
3958 * Use when embedding graph diagrams in Markdown, documentation sites, or other
3959 * Mermaid-compatible tools.
3960 *
3961 * **Details**
3962 *
3963 * Directed graphs default to `flowchart` with arrow edges, while undirected
3964 * graphs default to `graph` with line edges. Labels and node shapes can be
3965 * customized with `MermaidOptions`.
3966 *
3967 * **Example** (Exporting a Mermaid diagram)
3968 *
3969 * ```ts import.meta.vitest
3970 * import { Graph } from "effect"
3971 *
3972 * const graph = Graph.directed<string, string>((mutable) => {
3973 * const app = Graph.addNode(mutable, "App")
3974 * const database = Graph.addNode(mutable, "Database")
3975 * Graph.addEdge(mutable, app, database, "queries")
3976 * })
3977 *
3978 * Graph.toMermaid(graph).split("\n") // => ["flowchart TD", ' 0["App"]', ' 1["Database"]', ' 0 -->|"queries"| 1']
3979 * ```
3980 *
3981 * @see {@link toGraphViz} for GraphViz DOT output
3982 * @category converting
3983 * @since 3.18.0
3984 */
3985export const toMermaid: {
3986 <N, E>(
3987 options?: MermaidOptions<N, E>
3988 ): <T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => string
3989 <N, E, T extends Kind = "directed">(
3990 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3991 options?: MermaidOptions<N, E>
3992 ): string
3993} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
3994 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3995 options?: MermaidOptions<N, E>
3996): string => {
3997 const impl = internal.toImpl(graph)
3998 // Extract and validate options with defaults
3999 const {
4000 diagramType,
4001 direction = "TD",
4002 edgeLabel = (data: E) => String(data),
4003 nodeLabel = (data: N) => String(data),
4004 nodeShape = () => "rectangle" as const
4005 } = options ?? {}
4006
4007 // Auto-detect diagram type if not specified
4008 const finalDiagramType = diagramType ??
4009 (graph.type === "directed" ? "flowchart" : "graph")
4010
4011 return withMutationGuard(graph, () => {
4012 // Generate diagram header
4013 const lines: Array<string> = []
4014 lines.push(`${finalDiagramType} ${direction}`)
4015
4016 // Add nodes
4017 for (const [nodeIndex, nodeData] of impl.nodes) {
4018 const nodeId = String(nodeIndex)
4019 const label = escapeMermaidLabel(nodeLabel(nodeData))
4020 const shape = nodeShape(nodeData)
4021 const formattedNode = formatMermaidNode(nodeId, label, shape)
4022 lines.push(` ${formattedNode}`)
4023 }
4024
4025 // Add edges
4026 const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---"
4027 for (const [, edgeData] of impl.edges) {
4028 const sourceId = String(edgeData.source)
4029 const targetId = String(edgeData.target)
4030 const label = escapeMermaidLabel(edgeLabel(edgeData.data))
4031
4032 if (label) {
4033 lines.push(` ${sourceId} ${edgeOperator}|"${label}"| ${targetId}`)
4034 } else {
4035 lines.push(` ${sourceId} ${edgeOperator} ${targetId}`)
4036 }
4037 }
4038
4039 return lines.join("\n")
4040 })
4041})
4042
4043// =============================================================================
4044// Edge Direction Types
4045// =============================================================================
4046
4047/**
4048 * Direction of directed edges relative to a node.
4049 *
4050 * **Details**
4051 *
4052 * `"outgoing"` selects edges whose source is the node, while `"incoming"`
4053 * selects edges whose target is the node.
4054 *
4055 * @category models
4056 * @since 3.18.0
4057 */
4058export type Direction = "outgoing" | "incoming"
4059
4060/**
4061 * Controls how traversal follows directed edges.
4062 *
4063 * **Details**
4064 *
4065 * `"outgoing"` follows edges from source to target, `"incoming"` follows them
4066 * from target to source, and `"undirected"` allows traversal in either
4067 * direction.
4068 *
4069 * **Example** (Traversing by direction)
4070 *
4071 * ```ts import.meta.vitest
4072 * import { Graph } from "effect"
4073 *
4074 * const graph = Graph.directed<string, string>((mutable) => {
4075 * const a = Graph.addNode(mutable, "A")
4076 * const b = Graph.addNode(mutable, "B")
4077 * const c = Graph.addNode(mutable, "C")
4078 * Graph.addEdge(mutable, a, b, "A-B")
4079 * Graph.addEdge(mutable, a, c, "A-C")
4080 * })
4081 *
4082 * Array.from(Graph.indices(Graph.bfs(graph, { start: [0], direction: "outgoing" }))) // => [0, 1, 2]
4083 * Array.from(Graph.indices(Graph.bfs(graph, { start: [1], direction: "incoming" }))) // => [1, 0]
4084 * Array.from(Graph.indices(Graph.bfs(graph, { start: [1], direction: "undirected" }))) // => [1, 0, 2]
4085 * ```
4086 *
4087 * @category models
4088 * @since 4.0.0
4089 */
4090export type TraversalDirection = Direction | "undirected"
4091
4092// =============================================================================
4093// Graph Structure Analysis Algorithms
4094// =============================================================================
4095
4096/**
4097 * A cycle witness containing a closed node path and its traversed edges.
4098 *
4099 * **Details**
4100 *
4101 * `path` repeats its first node at the end, so `edges.length` is always
4102 * `path.length - 1`.
4103 *
4104 * @category models
4105 * @since 4.0.0
4106 */
4107export interface CycleResult {
4108 readonly path: Array<NodeIndex>
4109 readonly edges: Array<EdgeIndex>
4110}
4111
4112/**
4113 * Returns one cycle in a graph, if present.
4114 *
4115 * **When to use**
4116 *
4117 * Use when you need the nodes and edges of a concrete cycle for diagnostics or
4118 * reporting.
4119 *
4120 * **Details**
4121 *
4122 * Directed cycles respect edge orientation. A self-loop is represented as a
4123 * one-edge cycle, and two parallel undirected edges form a two-edge cycle.
4124 *
4125 * @see {@link isAcyclic} when only a boolean cycle check is needed
4126 * @category algorithms
4127 * @since 4.0.0
4128 */
4129export const findCycle = <N, E, T extends Kind = "directed">(
4130 graph: Graph<N, E, T> | MutableGraph<N, E, T>
4131): Option.Option<CycleResult> => {
4132 const impl = internal.toImpl(graph)
4133 const colors = new Map<NodeIndex, 0 | 1 | 2>()
4134 const parentNodes = new Map<NodeIndex, NodeIndex>()
4135 const parentEdges = new Map<NodeIndex, EdgeIndex>()
4136
4137 const makeCycle = (ancestor: NodeIndex, current: NodeIndex, closingEdge: EdgeIndex): CycleResult => {
4138 const path = [current]
4139 const edges: Array<EdgeIndex> = []
4140 let cursor = current
4141 while (cursor !== ancestor) {
4142 edges.push(parentEdges.get(cursor)!)
4143 cursor = parentNodes.get(cursor)!
4144 path.push(cursor)
4145 }
4146 path.reverse()
4147 edges.reverse()
4148 path.push(ancestor)
4149 edges.push(closingEdge)
4150 return { path, edges }
4151 }
4152
4153 for (const start of impl.nodes.keys()) {
4154 if ((colors.get(start) ?? 0) !== 0) {
4155 continue
4156 }
4157 colors.set(start, 1)
4158 const stack: Array<{ readonly node: NodeIndex; position: number }> = [{ node: start, position: 0 }]
4159 while (stack.length > 0) {
4160 const frame = stack[stack.length - 1]
4161 const adjacency = impl.adjacency.get(frame.node)!
4162 if (frame.position >= adjacency.length) {
4163 colors.set(frame.node, 2)
4164 stack.pop()
4165 continue
4166 }
4167
4168 const edgeIndex = adjacency[frame.position++]
4169 if (graph.type === "undirected" && parentEdges.get(frame.node) === edgeIndex) {
4170 continue
4171 }
4172 const edge = impl.edges.get(edgeIndex)!
4173 const neighbor = getTraversableNeighbor(graph, frame.node, edge)
4174 const color = colors.get(neighbor) ?? 0
4175 if (color === 1) {
4176 return Option.some(makeCycle(neighbor, frame.node, edgeIndex))
4177 }
4178 if (color === 0) {
4179 colors.set(neighbor, 1)
4180 parentNodes.set(neighbor, frame.node)
4181 parentEdges.set(neighbor, edgeIndex)
4182 stack.push({ node: neighbor, position: 0 })
4183 }
4184 }
4185 }
4186 return Option.none()
4187}
4188
4189/**
4190 * Checks whether the graph is acyclic (contains no cycles).
4191 *
4192 * **When to use**
4193 *
4194 * Use when validating that a graph contains no cycle and a cycle witness is
4195 * not needed.
4196 *
4197 * **Details**
4198 *
4199 * Directed cycles respect edge orientation. Self-loops are cycles, and two
4200 * parallel edges form a cycle in an undirected graph.
4201 *
4202 * **Example** (Checking cycles)
4203 *
4204 * ```ts import.meta.vitest
4205 * import { Graph } from "effect"
4206 *
4207 * // Acyclic directed graph (DAG)
4208 * const dag = Graph.directed<string, string>((mutable) => {
4209 * const a = Graph.addNode(mutable, "A")
4210 * const b = Graph.addNode(mutable, "B")
4211 * const c = Graph.addNode(mutable, "C")
4212 * Graph.addEdge(mutable, a, b, "A->B")
4213 * Graph.addEdge(mutable, b, c, "B->C")
4214 * })
4215 * Graph.isAcyclic(dag) // => true
4216 *
4217 * // Cyclic directed graph
4218 * const cyclic = Graph.directed<string, string>((mutable) => {
4219 * const a = Graph.addNode(mutable, "A")
4220 * const b = Graph.addNode(mutable, "B")
4221 * Graph.addEdge(mutable, a, b, "A->B")
4222 * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle
4223 * })
4224 * Graph.isAcyclic(cyclic) // => false
4225 * ```
4226 *
4227 * @see {@link findCycle} for retrieving one cycle witness
4228 * @see {@link topo} for ordering a directed acyclic graph
4229 * @category algorithms
4230 * @since 3.18.0
4231 */
4232export const isAcyclic = <N, E, T extends Kind = "directed">(
4233 graph: Graph<N, E, T> | MutableGraph<N, E, T>
4234): boolean => {
4235 const impl = internal.toImpl(graph)
4236 // Use existing cycle flag if available
4237 if (Option.isSome(impl.acyclic)) {
4238 return impl.acyclic.value
4239 }
4240
4241 const cache = csr.get(graph)
4242 const outgoing = csr.getOutgoingWithEdges(cache)
4243 if (graph.type === "undirected") {
4244 // Each undirected edge occurs in both endpoint rows; ignore only the edge used to enter the node.
4245 const visited = new Uint8Array(cache.nodeIds.length)
4246 const stack: Array<number> = []
4247 const parentEdges: Array<number> = []
4248
4249 for (let start = 0; start < cache.nodeIds.length; start++) {
4250 if (visited[start] !== 0) {
4251 continue
4252 }
4253 visited[start] = 1
4254 stack.push(start)
4255 parentEdges.push(-1)
4256
4257 while (stack.length > 0) {
4258 const node = stack.pop()!
4259 const parentEdge = parentEdges.pop()!
4260 for (let i = outgoing.rowOffsets[node]; i < outgoing.rowOffsets[node + 1]; i++) {
4261 const edge = outgoing.edgeIndices[i]
4262 if (edge === parentEdge) {
4263 continue
4264 }
4265 const neighbor = outgoing.columnIndices[i]
4266 if (visited[neighbor] !== 0) {
4267 impl.acyclic = Option.some(false)
4268 return false
4269 }
4270 visited[neighbor] = 1
4271 stack.push(neighbor)
4272 parentEdges.push(edge)
4273 }
4274 }
4275 }
4276 } else {
4277 // Colors encode unseen, active, and finished nodes; row positions make the recursive DFS stack explicit.
4278 const colors = new Uint8Array(cache.nodeIds.length)
4279 const stack: Array<number> = []
4280 const positions: Array<number> = []
4281
4282 for (let start = 0; start < cache.nodeIds.length; start++) {
4283 if (colors[start] !== 0) {
4284 continue
4285 }
4286 colors[start] = 1
4287 stack.push(start)
4288 positions.push(outgoing.rowOffsets[start])
4289
4290 while (stack.length > 0) {
4291 const frame = stack.length - 1
4292 const node = stack[frame]
4293 const position = positions[frame]
4294 if (position < outgoing.rowOffsets[node + 1]) {
4295 positions[frame] = position + 1
4296 const neighbor = outgoing.columnIndices[position]
4297 if (colors[neighbor] === 1) {
4298 impl.acyclic = Option.some(false)
4299 return false
4300 }
4301 if (colors[neighbor] === 0) {
4302 colors[neighbor] = 1
4303 stack.push(neighbor)
4304 positions.push(outgoing.rowOffsets[neighbor])
4305 }
4306 } else {
4307 colors[node] = 2
4308 stack.pop()
4309 positions.pop()
4310 }
4311 }
4312 }
4313 }
4314
4315 impl.acyclic = Option.some(true)
4316 return true
4317}
4318
4319/**
4320 * Checks whether an undirected graph is bipartite.
4321 *
4322 * **When to use**
4323 *
4324 * Use when validating that nodes can be divided into two groups with every
4325 * edge crossing between the groups.
4326 *
4327 * **Details**
4328 *
4329 * A bipartite graph is one whose vertices can be divided into two disjoint sets
4330 * such that no two vertices within the same set are adjacent.
4331 *
4332 * **Example** (Checking bipartite graphs)
4333 *
4334 * ```ts import.meta.vitest
4335 * import { Graph } from "effect"
4336 *
4337 * // Bipartite graph (alternating coloring possible)
4338 * const bipartite = Graph.undirected<string, string>((mutable) => {
4339 * const a = Graph.addNode(mutable, "A")
4340 * const b = Graph.addNode(mutable, "B")
4341 * const c = Graph.addNode(mutable, "C")
4342 * const d = Graph.addNode(mutable, "D")
4343 * Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D}
4344 * Graph.addEdge(mutable, b, c, "edge")
4345 * Graph.addEdge(mutable, c, d, "edge")
4346 * })
4347 * Graph.isBipartite(bipartite) // => true
4348 *
4349 * // Non-bipartite graph (odd cycle)
4350 * const triangle = Graph.undirected<string, string>((mutable) => {
4351 * const a = Graph.addNode(mutable, "A")
4352 * const b = Graph.addNode(mutable, "B")
4353 * const c = Graph.addNode(mutable, "C")
4354 * Graph.addEdge(mutable, a, b, "edge")
4355 * Graph.addEdge(mutable, b, c, "edge")
4356 * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle)
4357 * })
4358 * Graph.isBipartite(triangle) // => false
4359 * ```
4360 *
4361 * @see {@link maximumBipartiteMatching} for matching nodes after validation
4362 * @category algorithms
4363 * @since 3.18.0
4364 */
4365export const isBipartite = <N, E>(
4366 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
4367): boolean => {
4368 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
4369 throw new GraphError({ message: "Cannot determine bipartite status of directed graph" })
4370 }
4371 const cache = csr.get(graph)
4372 const outgoing = csr.getOutgoing(cache)
4373 // -1 is uncolored; compact indices let coloring and the queue stay in typed arrays.
4374 const colors = new Int8Array(cache.nodeIds.length)
4375 const queue = new Uint32Array(cache.nodeIds.length)
4376 colors.fill(-1)
4377 let head = 0
4378 let tail = 0
4379
4380 for (let start = 0; start < cache.nodeIds.length; start++) {
4381 if (colors[start] !== -1) {
4382 continue
4383 }
4384 colors[start] = 0
4385 queue[tail++] = start
4386
4387 while (head < tail) {
4388 const current = queue[head++]
4389 const neighborColor = colors[current] === 0 ? 1 : 0
4390 for (let i = outgoing.rowOffsets[current]; i < outgoing.rowOffsets[current + 1]; i++) {
4391 const neighbor = outgoing.columnIndices[i]
4392 if (colors[neighbor] === -1) {
4393 colors[neighbor] = neighborColor
4394 queue[tail++] = neighbor
4395 } else if (colors[neighbor] === colors[current]) {
4396 return false
4397 }
4398 }
4399 }
4400 }
4401
4402 return true
4403}
4404
4405/**
4406 * A pair of matched nodes and the edge that realizes the match.
4407 *
4408 * **Details**
4409 *
4410 * `left` and `right` refer to the bipartition derived by
4411 * `maximumBipartiteMatching`, not to the stored edge orientation.
4412 *
4413 * @category models
4414 * @since 4.0.0
4415 */
4416export interface BipartiteMatch {
4417 readonly left: NodeIndex
4418 readonly right: NodeIndex
4419 readonly edge: EdgeIndex
4420}
4421
4422/** @internal */
4423const bipartiteColors = <N, E>(
4424 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
4425): { readonly cache: csr.Csr; readonly colors: Int8Array } => {
4426 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
4427 throw new GraphError({ message: "Cannot find bipartite matching of directed graph" })
4428 }
4429 const cache = csr.get(graph)
4430 const outgoing = csr.getOutgoing(cache)
4431 const colors = new Int8Array(cache.nodeIds.length)
4432 const queue = new Uint32Array(cache.nodeIds.length)
4433 colors.fill(-1)
4434
4435 for (let start = 0; start < cache.nodeIds.length; start++) {
4436 if (colors[start] !== -1) {
4437 continue
4438 }
4439 let head = 0
4440 let tail = 1
4441 colors[start] = 0
4442 queue[0] = start
4443 while (head < tail) {
4444 const node = queue[head++]
4445 const color = colors[node] === 0 ? 1 : 0
4446 for (let i = outgoing.rowOffsets[node]; i < outgoing.rowOffsets[node + 1]; i++) {
4447 const neighbor = outgoing.columnIndices[i]
4448 if (colors[neighbor] === -1) {
4449 colors[neighbor] = color
4450 queue[tail++] = neighbor
4451 } else if (colors[neighbor] === colors[node]) {
4452 throw new GraphError({ message: "Cannot find bipartite matching of non-bipartite graph" })
4453 }
4454 }
4455 }
4456 }
4457 return { cache, colors }
4458}
4459
4460/**
4461 * Returns a maximum-cardinality matching of an undirected bipartite graph.
4462 *
4463 * **When to use**
4464 *
4465 * Use when assigning as many disjoint pairs as possible between the two sides
4466 * of a bipartite graph, such as workers to jobs or users to resources.
4467 *
4468 * **Details**
4469 *
4470 * The bipartition is derived internally. Self-loops and odd cycles throw a
4471 * `GraphError`. Isolated nodes are allowed. Parallel edges do not change the
4472 * matching cardinality, and the first edge in graph order between each matched
4473 * pair is reported. Results follow left-partition graph order. Hopcroft-Karp
4474 * runs in `O(E * sqrt(V))` time.
4475 *
4476 * **Gotchas**
4477 *
4478 * The graph must be undirected and bipartite. The derived left and right sides
4479 * are not based on stored edge orientation.
4480 *
4481 * **Example** (Matching a bipartite graph)
4482 *
4483 * ```ts import.meta.vitest
4484 * import { Graph } from "effect"
4485 *
4486 * const graph = Graph.undirected<string, string>((mutable) => {
4487 * for (const node of ["A", "B", "X", "Y"]) Graph.addNode(mutable, node)
4488 * Graph.addEdge(mutable, 0, 2, "A-X")
4489 * Graph.addEdge(mutable, 0, 3, "A-Y")
4490 * Graph.addEdge(mutable, 1, 2, "B-X")
4491 * })
4492 *
4493 * Graph.maximumBipartiteMatching(graph) // => [{ left: 0, right: 3, edge: 1 }, { left: 1, right: 2, edge: 2 }]
4494 * ```
4495 *
4496 * @see {@link isBipartite} for validating the graph without computing a matching
4497 * @category algorithms
4498 * @since 4.0.0
4499 */
4500export const maximumBipartiteMatching = <N, E>(
4501 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
4502): Array<BipartiteMatch> => {
4503 const { cache, colors } = bipartiteColors(graph)
4504 const endpoints = csr.getEdgeEndpoints(cache)
4505 const edgeIds = csr.getEdgeIds(cache)
4506 const adjacency: Array<Array<{ readonly right: number; readonly edge: number }>> = Array.from({
4507 length: cache.nodeIds.length
4508 }, () => [])
4509 const seen = Array.from({ length: cache.nodeIds.length }, () => new Set<number>())
4510
4511 for (let edge = 0; edge < edgeIds.length; edge++) {
4512 const source = endpoints.sources[edge]
4513 const target = endpoints.targets[edge]
4514 const left = colors[source] === 0 ? source : target
4515 const right = colors[source] === 0 ? target : source
4516 if (!seen[left].has(right)) {
4517 seen[left].add(right)
4518 adjacency[left].push({ right, edge })
4519 }
4520 }
4521
4522 const unmatched = -1
4523 const infinity = 0x7fffffff
4524 const matchLeft = new Int32Array(cache.nodeIds.length)
4525 const matchRight = new Int32Array(cache.nodeIds.length)
4526 const matchEdge = new Int32Array(cache.nodeIds.length)
4527 const distance = new Int32Array(cache.nodeIds.length)
4528 const queue = new Uint32Array(cache.nodeIds.length)
4529 matchLeft.fill(unmatched)
4530 matchRight.fill(unmatched)
4531 matchEdge.fill(unmatched)
4532 let shortestDistance = infinity
4533
4534 const hasLayer = (): boolean => {
4535 let head = 0
4536 let tail = 0
4537 shortestDistance = infinity
4538 for (let left = 0; left < colors.length; left++) {
4539 if (colors[left] !== 0) {
4540 continue
4541 }
4542 if (matchLeft[left] === unmatched) {
4543 distance[left] = 0
4544 queue[tail++] = left
4545 } else {
4546 distance[left] = infinity
4547 }
4548 }
4549 while (head < tail) {
4550 const left = queue[head++]
4551 if (distance[left] >= shortestDistance) {
4552 continue
4553 }
4554 for (const arc of adjacency[left]) {
4555 const next = matchRight[arc.right]
4556 if (next === unmatched) {
4557 shortestDistance = distance[left] + 1
4558 } else if (distance[next] === infinity) {
4559 distance[next] = distance[left] + 1
4560 queue[tail++] = next
4561 }
4562 }
4563 }
4564 return shortestDistance !== infinity
4565 }
4566
4567 const augment = (start: number): boolean => {
4568 const stack: Array<{
4569 readonly left: number
4570 position: number
4571 readonly viaRight: number
4572 readonly viaEdge: number
4573 }> = [{ left: start, position: 0, viaRight: unmatched, viaEdge: unmatched }]
4574 while (stack.length > 0) {
4575 const frame = stack[stack.length - 1]
4576 const arcs = adjacency[frame.left]
4577 if (frame.position >= arcs.length) {
4578 distance[frame.left] = infinity
4579 stack.pop()
4580 continue
4581 }
4582 const arc = arcs[frame.position++]
4583 const next = matchRight[arc.right]
4584 if (next === unmatched && distance[frame.left] + 1 === shortestDistance) {
4585 matchLeft[frame.left] = arc.right
4586 matchRight[arc.right] = frame.left
4587 matchEdge[frame.left] = arc.edge
4588 for (let i = stack.length - 1; i > 0; i--) {
4589 const child = stack[i]
4590 const parent = stack[i - 1]
4591 matchLeft[parent.left] = child.viaRight
4592 matchRight[child.viaRight] = parent.left
4593 matchEdge[parent.left] = child.viaEdge
4594 }
4595 return true
4596 }
4597 if (next === unmatched) {
4598 continue
4599 }
4600 if (distance[next] === distance[frame.left] + 1) {
4601 stack.push({ left: next, position: 0, viaRight: arc.right, viaEdge: arc.edge })
4602 }
4603 }
4604 return false
4605 }
4606
4607 while (hasLayer()) {
4608 for (let left = 0; left < colors.length; left++) {
4609 if (colors[left] === 0 && matchLeft[left] === unmatched) {
4610 augment(left)
4611 }
4612 }
4613 }
4614
4615 const matches: Array<BipartiteMatch> = []
4616 for (let left = 0; left < colors.length; left++) {
4617 if (matchLeft[left] !== unmatched) {
4618 matches.push({
4619 left: cache.nodeIds[left],
4620 right: cache.nodeIds[matchLeft[left]],
4621 edge: edgeIds[matchEdge[left]]
4622 })
4623 }
4624 }
4625 return matches
4626}
4627
4628/**
4629 * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency.
4630 * For undirected graphs, we need to find the other endpoint of each edge incident to the node.
4631 */
4632const getUndirectedNeighbors = <N, E>(
4633 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">,
4634 nodeIndex: NodeIndex
4635): Array<NodeIndex> => {
4636 const impl = internal.toImpl(graph)
4637 const neighbors = new Set<NodeIndex>()
4638
4639 // Check edges where this node is the source
4640 const adjacencyList = impl.adjacency.get(nodeIndex)
4641 if (adjacencyList !== undefined) {
4642 for (const edgeIndex of adjacencyList) {
4643 const edge = impl.edges.get(edgeIndex)
4644 if (edge !== undefined) {
4645 // For undirected graphs, the neighbor is the other endpoint
4646 const otherNode = edge.source === nodeIndex ? edge.target : edge.source
4647 neighbors.add(otherNode)
4648 }
4649 }
4650 }
4651
4652 return Array.from(neighbors)
4653}
4654
4655const getTraversableNeighbor = <N, E, T extends Kind>(
4656 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4657 current: NodeIndex,
4658 edge: Edge<E>
4659): NodeIndex => graph.type === "undirected" && edge.target === current ? edge.source : edge.target
4660
4661/**
4662 * Configuration for unweighted reachability queries.
4663 *
4664 * **When to use**
4665 *
4666 * Use when controlling whether reachability follows outgoing edges, incoming
4667 * edges, or either direction.
4668 *
4669 * **Details**
4670 *
4671 * `direction` defaults to `"outgoing"` and is ignored for undirected graphs.
4672 *
4673 * @category configuration
4674 * @since 4.0.0
4675 */
4676export interface ReachabilityConfig {
4677 readonly direction?: TraversalDirection
4678}
4679
4680const getUnweightedDistances = <N, E, T extends Kind>(
4681 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4682 source: NodeIndex,
4683 direction: TraversalDirection
4684): Map<NodeIndex, number> => {
4685 const impl = internal.toImpl(graph)
4686 if (!impl.nodes.has(source)) {
4687 throw missingNode(source)
4688 }
4689
4690 const cache = csr.get(graph)
4691 const sourceNode = csr.getNodeIndex(cache, source)!
4692 const adjacencies = csr.getAdjacencies(cache, graph.type === "undirected" ? "outgoing" : direction)
4693 const compactDistances = new Int32Array(cache.nodeIds.length)
4694 compactDistances.fill(-1)
4695 compactDistances[sourceNode] = 0
4696 const queue = new Uint32Array(cache.nodeIds.length)
4697 let head = 0
4698 let tail = 0
4699 queue[tail++] = sourceNode
4700
4701 const visit = (adjacency: csr.Adjacency, current: number) => {
4702 for (let i = adjacency.rowOffsets[current]; i < adjacency.rowOffsets[current + 1]; i++) {
4703 const neighbor = adjacency.columnIndices[i]
4704 if (compactDistances[neighbor] === -1) {
4705 compactDistances[neighbor] = compactDistances[current] + 1
4706 queue[tail++] = neighbor
4707 }
4708 }
4709 }
4710 while (head < tail) {
4711 const current = queue[head++]
4712 visit(adjacencies.primary, current)
4713 if (adjacencies.secondary !== undefined) {
4714 visit(adjacencies.secondary, current)
4715 }
4716 }
4717
4718 const result = new Map<NodeIndex, number>()
4719 for (let i = 0; i < cache.nodeIds.length; i++) {
4720 if (compactDistances[i] !== -1) {
4721 result.set(cache.nodeIds[i], compactDistances[i])
4722 }
4723 }
4724 return result
4725}
4726
4727/**
4728 * Returns minimum unweighted distances from a source to every reachable node.
4729 *
4730 * **When to use**
4731 *
4732 * Use when every edge represents one step and you need hop counts from one
4733 * source.
4734 *
4735 * **Details**
4736 *
4737 * Directed traversal is outgoing by default and can be changed with
4738 * `direction`.
4739 *
4740 * **Gotchas**
4741 *
4742 * Throws a `GraphError` when the source does not exist.
4743 *
4744 * @see {@link hasPath} when only a reachability boolean is needed
4745 * @see {@link bfs} for lazy traversal in increasing hop distance
4746 * @see {@link dijkstra} for weighted shortest paths
4747 *
4748 * @category algorithms
4749 * @since 4.0.0
4750 */
4751export const unweightedDistances: {
4752 (source: NodeIndex, options?: ReachabilityConfig): <N, E, T extends Kind = "directed">(
4753 graph: Graph<N, E, T> | MutableGraph<N, E, T>
4754 ) => Map<NodeIndex, number>
4755 <N, E, T extends Kind = "directed">(
4756 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4757 source: NodeIndex,
4758 options?: ReachabilityConfig
4759 ): Map<NodeIndex, number>
4760} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
4761 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4762 source: NodeIndex,
4763 options?: ReachabilityConfig
4764): Map<NodeIndex, number> => getUnweightedDistances(graph, source, options?.direction ?? "outgoing"))
4765
4766/**
4767 * Tests whether a target is reachable from a source.
4768 *
4769 * **When to use**
4770 *
4771 * Use when you only need a reachability boolean rather than distances or a
4772 * reconstructed path.
4773 *
4774 * **Details**
4775 *
4776 * Directed traversal is outgoing by default and can be changed with
4777 * `direction`. A node is reachable from itself.
4778 *
4779 * **Gotchas**
4780 *
4781 * Throws a `GraphError` when either endpoint does not exist.
4782 *
4783 * @see {@link unweightedDistances} for hop distances to all reachable nodes
4784 * @see {@link dijkstra} for a minimum-cost path
4785 *
4786 * @category predicates
4787 * @since 4.0.0
4788 */
4789export const hasPath: {
4790 (source: NodeIndex, target: NodeIndex, options?: ReachabilityConfig): <N, E, T extends Kind = "directed">(
4791 graph: Graph<N, E, T> | MutableGraph<N, E, T>
4792 ) => boolean
4793 <N, E, T extends Kind = "directed">(
4794 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4795 source: NodeIndex,
4796 target: NodeIndex,
4797 options?: ReachabilityConfig
4798 ): boolean
4799} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
4800 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4801 source: NodeIndex,
4802 target: NodeIndex,
4803 options?: ReachabilityConfig
4804): boolean => {
4805 const impl = internal.toImpl(graph)
4806 if (!impl.nodes.has(source)) {
4807 throw missingNode(source)
4808 }
4809 if (!impl.nodes.has(target)) {
4810 throw missingNode(target)
4811 }
4812 if (source === target) {
4813 return true
4814 }
4815
4816 const cache = csr.get(graph)
4817 const sourceNode = csr.getNodeIndex(cache, source)!
4818 const targetNode = csr.getNodeIndex(cache, target)!
4819 const adjacencies = csr.getAdjacencies(
4820 cache,
4821 graph.type === "undirected" ? "outgoing" : options?.direction ?? "outgoing"
4822 )
4823 const visited = new Uint8Array(cache.nodeIds.length)
4824 const queue = new Uint32Array(cache.nodeIds.length)
4825 let head = 0
4826 let tail = 1
4827 visited[sourceNode] = 1
4828 queue[0] = sourceNode
4829
4830 while (head < tail) {
4831 const current = queue[head++]
4832 const primary = adjacencies.primary
4833 for (let i = primary.rowOffsets[current]; i < primary.rowOffsets[current + 1]; i++) {
4834 const neighbor = primary.columnIndices[i]
4835 if (neighbor === targetNode) {
4836 return true
4837 }
4838 if (visited[neighbor] === 0) {
4839 visited[neighbor] = 1
4840 queue[tail++] = neighbor
4841 }
4842 }
4843 const secondary = adjacencies.secondary
4844 if (secondary !== undefined) {
4845 for (let i = secondary.rowOffsets[current]; i < secondary.rowOffsets[current + 1]; i++) {
4846 const neighbor = secondary.columnIndices[i]
4847 if (neighbor === targetNode) {
4848 return true
4849 }
4850 if (visited[neighbor] === 0) {
4851 visited[neighbor] = 1
4852 queue[tail++] = neighbor
4853 }
4854 }
4855 }
4856 }
4857 return false
4858})
4859
4860/**
4861 * Returns the connected components of an undirected graph.
4862 *
4863 * **When to use**
4864 *
4865 * Use when partitioning an undirected graph into groups connected by paths.
4866 *
4867 * **Details**
4868 *
4869 * Each component is represented as an array of node indices. Isolated nodes
4870 * form singleton components.
4871 *
4872 * **Gotchas**
4873 *
4874 * Throws a `GraphError` when used with a directed graph.
4875 *
4876 * **Example** (Finding connected components)
4877 *
4878 * ```ts import.meta.vitest
4879 * import { Graph } from "effect"
4880 *
4881 * const graph = Graph.undirected<string, string>((mutable) => {
4882 * const a = Graph.addNode(mutable, "A")
4883 * const b = Graph.addNode(mutable, "B")
4884 * const c = Graph.addNode(mutable, "C")
4885 * const d = Graph.addNode(mutable, "D")
4886 * Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B
4887 * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D
4888 * })
4889 *
4890 * Graph.connectedComponents(graph) // => [[0, 1], [2, 3]]
4891 * ```
4892 *
4893 * @see {@link isConnected} when only a boolean connectivity check is needed
4894 * @see {@link weaklyConnectedComponents} for directed graphs with orientation ignored
4895 * @see {@link stronglyConnectedComponents} for mutual directed reachability
4896 * @category algorithms
4897 * @since 3.18.0
4898 */
4899export const connectedComponents = <N, E>(
4900 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
4901): Array<Array<NodeIndex>> => {
4902 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
4903 throw new GraphError({ message: "Cannot find connected components of directed graph" })
4904 }
4905 const cache = csr.get(graph)
4906 const outgoing = csr.getOutgoing(cache)
4907 const visited = new Uint8Array(cache.nodeIds.length)
4908 const neighborMarks = new Uint32Array(cache.nodeIds.length)
4909 const components: Array<Array<NodeIndex>> = []
4910 let neighborGeneration = 0
4911
4912 for (let start = 0; start < cache.nodeIds.length; start++) {
4913 if (visited[start] !== 0) {
4914 continue
4915 }
4916 const component: Array<NodeIndex> = []
4917 const stack: Array<number> = [start]
4918
4919 while (stack.length > 0) {
4920 const current = stack.pop()!
4921 if (visited[current] !== 0) {
4922 continue
4923 }
4924 visited[current] = 1
4925 component.push(cache.nodeIds[current])
4926
4927 // Generation marks deduplicate parallel-edge neighbors without clearing a full-sized array per node.
4928 neighborGeneration++
4929 for (let i = outgoing.rowOffsets[current]; i < outgoing.rowOffsets[current + 1]; i++) {
4930 const neighbor = outgoing.columnIndices[i]
4931 if (neighborMarks[neighbor] !== neighborGeneration) {
4932 neighborMarks[neighbor] = neighborGeneration
4933 if (visited[neighbor] === 0) {
4934 stack.push(neighbor)
4935 }
4936 }
4937 }
4938 }
4939
4940 components.push(component)
4941 }
4942
4943 return components
4944}
4945
4946/** @internal */
4947interface LowLinkResult {
4948 readonly bridges: Array<EdgeIndex>
4949 readonly articulationPoints: Array<NodeIndex>
4950 readonly biconnectedComponents: Array<Array<NodeIndex>>
4951}
4952
4953/** @internal */
4954const analyzeLowLinks = <N, E>(
4955 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
4956): LowLinkResult => {
4957 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
4958 throw new GraphError({ message: "Cannot analyze undirected connectivity of directed graph" })
4959 }
4960 const cache = csr.get(graph)
4961 const outgoing = csr.getOutgoingWithEdges(cache)
4962 const edgeIds = csr.getEdgeIds(cache)
4963 const endpoints = csr.getEdgeEndpoints(cache)
4964 const discovered = new Int32Array(cache.nodeIds.length)
4965 const low = new Int32Array(cache.nodeIds.length)
4966 const parentNode = new Int32Array(cache.nodeIds.length)
4967 const parentEdge = new Int32Array(cache.nodeIds.length)
4968 const childCount = new Uint32Array(cache.nodeIds.length)
4969 const bridgeMarks = new Uint8Array(edgeIds.length)
4970 const articulationMarks = new Uint8Array(cache.nodeIds.length)
4971 const edgeStack: Array<number> = []
4972 const components: Array<Array<number>> = []
4973 const loopNodes = new Set<number>()
4974 discovered.fill(-1)
4975 parentNode.fill(-1)
4976 parentEdge.fill(-1)
4977 let time = 0
4978
4979 const popComponent = (stopEdge: number): void => {
4980 const nodes = new Set<number>()
4981 while (edgeStack.length > 0) {
4982 const edge = edgeStack.pop()!
4983 nodes.add(endpoints.sources[edge])
4984 nodes.add(endpoints.targets[edge])
4985 if (edge === stopEdge) {
4986 break
4987 }
4988 }
4989 if (nodes.size > 0) {
4990 components.push(Array.from(nodes).sort((a, b) => a - b))
4991 }
4992 }
4993
4994 for (let start = 0; start < cache.nodeIds.length; start++) {
4995 if (discovered[start] !== -1) {
4996 continue
4997 }
4998 discovered[start] = low[start] = time++
4999 const stack: Array<{ readonly node: number; position: number }> = [{
5000 node: start,
5001 position: outgoing.rowOffsets[start]
5002 }]
5003
5004 while (stack.length > 0) {
5005 const frame = stack[stack.length - 1]
5006 const end = outgoing.rowOffsets[frame.node + 1]
5007 if (frame.position < end) {
5008 const position = frame.position++
5009 const edge = outgoing.edgeIndices[position]
5010 const neighbor = outgoing.columnIndices[position]
5011 if (neighbor === frame.node) {
5012 loopNodes.add(frame.node)
5013 continue
5014 }
5015 if (edge === parentEdge[frame.node]) {
5016 continue
5017 }
5018 if (discovered[neighbor] === -1) {
5019 childCount[frame.node]++
5020 parentNode[neighbor] = frame.node
5021 parentEdge[neighbor] = edge
5022 discovered[neighbor] = low[neighbor] = time++
5023 edgeStack.push(edge)
5024 stack.push({ node: neighbor, position: outgoing.rowOffsets[neighbor] })
5025 } else if (discovered[neighbor] < discovered[frame.node]) {
5026 low[frame.node] = Math.min(low[frame.node], discovered[neighbor])
5027 edgeStack.push(edge)
5028 }
5029 continue
5030 }
5031
5032 stack.pop()
5033 const parent = parentNode[frame.node]
5034 if (parent === -1) {
5035 if (childCount[frame.node] > 1) {
5036 articulationMarks[frame.node] = 1
5037 }
5038 } else {
5039 low[parent] = Math.min(low[parent], low[frame.node])
5040 if (low[frame.node] > discovered[parent]) {
5041 bridgeMarks[parentEdge[frame.node]] = 1
5042 }
5043 if (low[frame.node] >= discovered[parent]) {
5044 if (parentNode[parent] !== -1) {
5045 articulationMarks[parent] = 1
5046 }
5047 popComponent(parentEdge[frame.node])
5048 }
5049 }
5050 }
5051 }
5052
5053 for (const node of loopNodes) {
5054 components.push([node])
5055 }
5056 components.sort((left, right) => {
5057 const length = Math.min(left.length, right.length)
5058 for (let i = 0; i < length; i++) {
5059 if (left[i] !== right[i]) {
5060 return left[i] - right[i]
5061 }
5062 }
5063 return left.length - right.length
5064 })
5065
5066 const resultBridges: Array<EdgeIndex> = []
5067 for (let edge = 0; edge < edgeIds.length; edge++) {
5068 if (bridgeMarks[edge] !== 0) {
5069 resultBridges.push(edgeIds[edge])
5070 }
5071 }
5072 const resultArticulationPoints: Array<NodeIndex> = []
5073 for (let node = 0; node < cache.nodeIds.length; node++) {
5074 if (articulationMarks[node] !== 0) {
5075 resultArticulationPoints.push(cache.nodeIds[node])
5076 }
5077 }
5078 return {
5079 bridges: resultBridges,
5080 articulationPoints: resultArticulationPoints,
5081 biconnectedComponents: components.map((component) => component.map((node) => cache.nodeIds[node]))
5082 }
5083}
5084
5085/**
5086 * Returns the edges whose removal increases the number of connected components.
5087 *
5088 * **When to use**
5089 *
5090 * Use when locating single-edge failure points in an undirected network.
5091 *
5092 * **Details**
5093 *
5094 * Parent edges are tracked by edge index, so a parallel edge prevents either
5095 * edge from being a bridge. Self-loops are never bridges. Results follow graph
5096 * edge order. The iterative low-link traversal is stack-safe and runs in
5097 * `O(V + E)` time.
5098 *
5099 * **Gotchas**
5100 *
5101 * Throws a `GraphError` when used with a directed graph.
5102 *
5103 * **Example** (Finding bridge edges)
5104 *
5105 * ```ts import.meta.vitest
5106 * import { Graph } from "effect"
5107 *
5108 * const graph = Graph.undirected<void, void>((mutable) => {
5109 * for (let i = 0; i < 3; i++) Graph.addNode(mutable, undefined)
5110 * Graph.addEdge(mutable, 0, 1, undefined)
5111 * Graph.addEdge(mutable, 1, 2, undefined)
5112 * })
5113 *
5114 * Graph.bridges(graph) // => [0, 1]
5115 * ```
5116 *
5117 * @see {@link articulationPoints} for single-node failure points
5118 * @see {@link biconnectedComponents} for maximal regions without an articulation split
5119 * @category algorithms
5120 * @since 4.0.0
5121 */
5122export const bridges = <N, E>(
5123 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
5124): Array<EdgeIndex> => analyzeLowLinks(graph).bridges
5125
5126/**
5127 * Returns the nodes whose removal increases the number of connected components.
5128 *
5129 * **When to use**
5130 *
5131 * Use when locating single-node failure points in an undirected network.
5132 *
5133 * **Details**
5134 *
5135 * Disconnected components, parallel edges, and self-loops are handled by an
5136 * iterative, stack-safe low-link traversal in `O(V + E)` time. Results follow
5137 * graph node order.
5138 *
5139 * **Gotchas**
5140 *
5141 * Throws a `GraphError` when used with a directed graph.
5142 *
5143 * **Example** (Finding articulation points)
5144 *
5145 * ```ts import.meta.vitest
5146 * import { Graph } from "effect"
5147 *
5148 * const graph = Graph.undirected<void, void>((mutable) => {
5149 * for (let i = 0; i < 3; i++) Graph.addNode(mutable, undefined)
5150 * Graph.addEdge(mutable, 0, 1, undefined)
5151 * Graph.addEdge(mutable, 1, 2, undefined)
5152 * })
5153 *
5154 * Graph.articulationPoints(graph) // => [1]
5155 * ```
5156 *
5157 * @see {@link bridges} for single-edge failure points
5158 * @see {@link biconnectedComponents} for the regions joined at articulation points
5159 * @category algorithms
5160 * @since 4.0.0
5161 */
5162export const articulationPoints = <N, E>(
5163 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
5164): Array<NodeIndex> => analyzeLowLinks(graph).articulationPoints
5165
5166/**
5167 * Returns the maximal biconnected node components of an undirected graph.
5168 *
5169 * **When to use**
5170 *
5171 * Use when decomposing an undirected graph into maximal regions that remain
5172 * connected after removing any one node from the region.
5173 *
5174 * **Details**
5175 *
5176 * Articulation points can occur in more than one component. Isolated vertices
5177 * are excluded, while a vertex with a self-loop forms a singleton component.
5178 * Nodes within components and the components themselves follow graph order.
5179 * Parallel edges are treated independently. The iterative low-link traversal
5180 * is stack-safe and runs in `O(V + E)` time.
5181 *
5182 * **Gotchas**
5183 *
5184 * Throws a `GraphError` when used with a directed graph.
5185 *
5186 * **Example** (Finding biconnected components)
5187 *
5188 * ```ts import.meta.vitest
5189 * import { Graph } from "effect"
5190 *
5191 * const graph = Graph.undirected<void, void>((mutable) => {
5192 * for (let i = 0; i < 5; i++) Graph.addNode(mutable, undefined)
5193 * Graph.addEdge(mutable, 0, 1, undefined)
5194 * Graph.addEdge(mutable, 1, 2, undefined)
5195 * Graph.addEdge(mutable, 2, 0, undefined)
5196 * Graph.addEdge(mutable, 2, 3, undefined)
5197 * Graph.addEdge(mutable, 3, 4, undefined)
5198 * Graph.addEdge(mutable, 4, 2, undefined)
5199 * })
5200 *
5201 * Graph.biconnectedComponents(graph) // => [[0, 1, 2], [2, 3, 4]]
5202 * ```
5203 *
5204 * @see {@link articulationPoints} for the nodes shared between components
5205 * @see {@link bridges} for edges whose removal disconnects the graph
5206 * @category algorithms
5207 * @since 4.0.0
5208 */
5209export const biconnectedComponents = <N, E>(
5210 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
5211): Array<Array<NodeIndex>> => analyzeLowLinks(graph).biconnectedComponents
5212
5213/**
5214 * Configuration for source-to-target flow algorithms.
5215 *
5216 * **When to use**
5217 *
5218 * Use when defining endpoints and edge capacities for `maximumFlow` or
5219 * `minimumCut`.
5220 *
5221 * **Details**
5222 *
5223 * `capacity` receives stored edge data and must return a finite,
5224 * non-negative number.
5225 *
5226 * **Gotchas**
5227 *
5228 * The source and target must be distinct existing nodes in a directed graph.
5229 *
5230 * @category configuration
5231 * @since 4.0.0
5232 */
5233export interface MaximumFlowConfig<E> {
5234 readonly source: NodeIndex
5235 readonly target: NodeIndex
5236 readonly capacity: (edge: E) => number
5237}
5238
5239/**
5240 * Maximum flow value, per-edge flows, and a corresponding minimum cut.
5241 *
5242 * **Details**
5243 *
5244 * `flows` contains every original edge, including zero-flow edges. `cut`
5245 * contains the crossing edge identifiers of the corresponding minimum cut.
5246 *
5247 * @category models
5248 * @since 4.0.0
5249 */
5250export interface MaximumFlowResult {
5251 readonly value: number
5252 readonly flows: Map<EdgeIndex, number>
5253 readonly cut: Array<EdgeIndex>
5254}
5255
5256/**
5257 * Minimum cut value, crossing edges, and residual-reachability partitions.
5258 *
5259 * **Details**
5260 *
5261 * `source` contains nodes residual-reachable from the configured source and
5262 * `target` contains the remaining nodes.
5263 *
5264 * @category models
5265 * @since 4.0.0
5266 */
5267export interface MinimumCutResult {
5268 readonly value: number
5269 readonly edges: Array<EdgeIndex>
5270 readonly source: Array<NodeIndex>
5271 readonly target: Array<NodeIndex>
5272}
5273
5274/** @internal */
5275interface FlowSolution extends MaximumFlowResult {
5276 readonly sourceSide: Uint8Array
5277 readonly nodeIds: Array<NodeIndex>
5278}
5279
5280/** @internal */
5281interface ResidualArc {
5282 readonly from: number
5283 readonly to: number
5284 readonly capacity: number
5285 readonly edge: number
5286 flow: number
5287}
5288
5289/** @internal */
5290const solveMaximumFlow = <N, E>(
5291 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
5292 config: MaximumFlowConfig<E>
5293): FlowSolution => {
5294 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
5295 throw new GraphError({ message: "Cannot compute flow of undirected graph" })
5296 }
5297 const cache = csr.get(graph)
5298 const source = csr.getNodeIndex(cache, config.source)
5299 if (source === undefined) {
5300 throw missingNode(config.source)
5301 }
5302 const target = csr.getNodeIndex(cache, config.target)
5303 if (target === undefined) {
5304 throw missingNode(config.target)
5305 }
5306 if (source === target) {
5307 throw new GraphError({ message: "Flow source and target must be different nodes" })
5308 }
5309
5310 const edges = csr.getEdges(cache) as Array<Edge<E>>
5311 const edgeIds = csr.getEdgeIds(cache)
5312 const endpoints = csr.getEdgeEndpoints(cache)
5313 const capacities = new Float64Array(edges.length)
5314 const arcs: Array<ResidualArc> = []
5315 const adjacency: Array<Array<number>> = Array.from({ length: cache.nodeIds.length }, () => [])
5316 const forwardArc = new Int32Array(edges.length)
5317 forwardArc.fill(-1)
5318
5319 withMutationGuard(graph, () => {
5320 for (let edge = 0; edge < edges.length; edge++) {
5321 const capacity = config.capacity(edges[edge].data)
5322 if (!Number.isFinite(capacity) || capacity < 0) {
5323 throw new GraphError({ message: `Edge ${edgeIds[edge]} capacity must be a finite non-negative number` })
5324 }
5325 capacities[edge] = capacity
5326 const from = endpoints.sources[edge]
5327 const to = endpoints.targets[edge]
5328 if (from === to) {
5329 continue
5330 }
5331 const index = arcs.length
5332 forwardArc[edge] = index
5333 adjacency[from].push(index)
5334 arcs.push({ from, to, capacity, edge, flow: 0 })
5335 adjacency[to].push(index + 1)
5336 arcs.push({ from: to, to: from, capacity: 0, edge: -1, flow: 0 })
5337 }
5338 })
5339
5340 const parentArc = new Int32Array(cache.nodeIds.length)
5341 const visited = new Uint8Array(cache.nodeIds.length)
5342 const queue = new Uint32Array(cache.nodeIds.length)
5343 let value = 0
5344 while (true) {
5345 parentArc.fill(-1)
5346 visited.fill(0)
5347 let head = 0
5348 let tail = 1
5349 queue[0] = source
5350 visited[source] = 1
5351 while (head < tail && visited[target] === 0) {
5352 const node = queue[head++]
5353 for (const arcIndex of adjacency[node]) {
5354 const arc = arcs[arcIndex]
5355 if (arc.capacity - arc.flow > 0 && visited[arc.to] === 0) {
5356 visited[arc.to] = 1
5357 parentArc[arc.to] = arcIndex
5358 queue[tail++] = arc.to
5359 if (arc.to === target) {
5360 break
5361 }
5362 }
5363 }
5364 }
5365 if (visited[target] === 0) {
5366 break
5367 }
5368
5369 let amount = Infinity
5370 for (let node = target; node !== source;) {
5371 const arc = arcs[parentArc[node]]
5372 amount = Math.min(amount, arc.capacity - arc.flow)
5373 node = arc.from
5374 }
5375 if (!Number.isFinite(value + amount)) {
5376 throw new GraphError({ message: "Maximum flow exceeds the finite number range" })
5377 }
5378 for (let node = target; node !== source;) {
5379 const arcIndex = parentArc[node]
5380 const arc = arcs[arcIndex]
5381 arc.flow += amount
5382 arcs[arcIndex ^ 1].flow -= amount
5383 node = arc.from
5384 }
5385 value += amount
5386 }
5387
5388 const flows = new Map<EdgeIndex, number>()
5389 for (let edge = 0; edge < edgeIds.length; edge++) {
5390 const arcIndex = forwardArc[edge]
5391 flows.set(edgeIds[edge], arcIndex === -1 ? 0 : arcs[arcIndex].flow)
5392 }
5393
5394 visited.fill(0)
5395 let head = 0
5396 let tail = 1
5397 queue[0] = source
5398 visited[source] = 1
5399 while (head < tail) {
5400 const node = queue[head++]
5401 for (const arcIndex of adjacency[node]) {
5402 const arc = arcs[arcIndex]
5403 if (arc.capacity - arc.flow > 0 && visited[arc.to] === 0) {
5404 visited[arc.to] = 1
5405 queue[tail++] = arc.to
5406 }
5407 }
5408 }
5409
5410 const cut: Array<EdgeIndex> = []
5411 for (let edge = 0; edge < edgeIds.length; edge++) {
5412 if (
5413 endpoints.sources[edge] !== endpoints.targets[edge] &&
5414 visited[endpoints.sources[edge]] !== 0 &&
5415 visited[endpoints.targets[edge]] === 0
5416 ) {
5417 cut.push(edgeIds[edge])
5418 }
5419 }
5420 return { value, flows, cut, sourceSide: visited, nodeIds: cache.nodeIds }
5421}
5422
5423/**
5424 * Returns a maximum flow and corresponding minimum cut for a directed graph.
5425 *
5426 * **When to use**
5427 *
5428 * Use when computing the greatest transferable capacity from one node to
5429 * another and per-edge flow values are required.
5430 *
5431 * **Details**
5432 *
5433 * Parallel edges retain independent capacities, self-loops carry no
5434 * source-to-target flow, and the flow map includes every original edge in graph
5435 * order, including zero-flow edges. Edmonds-Karp runs in `O(V * E^2)` time.
5436 *
5437 * **Gotchas**
5438 *
5439 * The graph must be directed. Capacities must be finite and non-negative.
5440 * Missing or equal endpoints, invalid capacities, and a total flow outside the
5441 * finite number range throw a `GraphError`. Self-loops always carry zero flow.
5442 *
5443 * **Example** (Computing maximum flow)
5444 *
5445 * ```ts import.meta.vitest
5446 * import { Graph } from "effect"
5447 *
5448 * const graph = Graph.directed<string, number>((mutable) => {
5449 * for (const node of ["source", "a", "target"]) Graph.addNode(mutable, node)
5450 * Graph.addEdge(mutable, 0, 1, 3)
5451 * Graph.addEdge(mutable, 1, 2, 2)
5452 * Graph.addEdge(mutable, 0, 2, 1)
5453 * })
5454 *
5455 * Graph.maximumFlow(graph, { source: 0, target: 2, capacity: (edge) => edge }).value // => 3
5456 * ```
5457 *
5458 * @see {@link minimumCut} for the residual-reachability partition
5459 * @category algorithms
5460 * @since 4.0.0
5461 */
5462export const maximumFlow: {
5463 <E>(config: MaximumFlowConfig<E>): <N>(
5464 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
5465 ) => MaximumFlowResult
5466 <N, E>(
5467 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
5468 config: MaximumFlowConfig<E>
5469 ): MaximumFlowResult
5470} = dual(2, <N, E>(
5471 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
5472 config: MaximumFlowConfig<E>
5473): MaximumFlowResult => {
5474 const { cut, flows, value } = solveMaximumFlow(graph, config)
5475 return { value, flows, cut }
5476})
5477
5478/**
5479 * Returns a minimum cut and its node partitions for a directed graph.
5480 *
5481 * **When to use**
5482 *
5483 * Use when identifying the minimum-capacity edges that separate a source from
5484 * a target, together with the resulting node partitions.
5485 *
5486 * **Details**
5487 *
5488 * The source partition contains nodes reachable from the source in the final
5489 * residual network; the target partition contains its complement. Both follow
5490 * graph node order. Cut edges follow graph edge order, and their total capacity
5491 * equals the returned maximum-flow value. Validation, parallel-edge,
5492 * self-loop, and `O(V * E^2)` complexity behavior match `maximumFlow`.
5493 *
5494 * **Gotchas**
5495 *
5496 * The graph must be directed. Invalid capacities, missing endpoints, or equal
5497 * source and target nodes throw a `GraphError`.
5498 *
5499 * **Example** (Partitioning a minimum cut)
5500 *
5501 * ```ts import.meta.vitest
5502 * import { Graph } from "effect"
5503 *
5504 * const graph = Graph.directed<string, number>((mutable) => {
5505 * for (const node of ["source", "a", "target"]) Graph.addNode(mutable, node)
5506 * Graph.addEdge(mutable, 0, 1, 2)
5507 * Graph.addEdge(mutable, 1, 2, 1)
5508 * })
5509 *
5510 * Graph.minimumCut(graph, { source: 0, target: 2, capacity: (edge) => edge }).source // => [0, 1]
5511 * ```
5512 *
5513 * @see {@link maximumFlow} for per-edge flow values
5514 * @category algorithms
5515 * @since 4.0.0
5516 */
5517export const minimumCut: {
5518 <E>(config: MaximumFlowConfig<E>): <N>(
5519 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
5520 ) => MinimumCutResult
5521 <N, E>(
5522 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
5523 config: MaximumFlowConfig<E>
5524 ): MinimumCutResult
5525} = dual(2, <N, E>(
5526 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
5527 config: MaximumFlowConfig<E>
5528): MinimumCutResult => {
5529 const solution = solveMaximumFlow(graph, config)
5530 const source: Array<NodeIndex> = []
5531 const target: Array<NodeIndex> = []
5532 for (let node = 0; node < solution.nodeIds.length; node++) {
5533 ;(solution.sourceSide[node] === 0 ? target : source).push(solution.nodeIds[node])
5534 }
5535 return { value: solution.value, edges: solution.cut, source, target }
5536})
5537
5538/**
5539 * Finds weakly connected components in a directed graph.
5540 *
5541 * **When to use**
5542 *
5543 * Use when grouping directed nodes by connectivity while ignoring edge
5544 * orientation.
5545 *
5546 * **Details**
5547 *
5548 * Edge direction is ignored while partitioning nodes. Isolated nodes form
5549 * singleton components.
5550 *
5551 * **Gotchas**
5552 *
5553 * Throws a `GraphError` when used with an undirected graph.
5554 *
5555 * @see {@link isWeaklyConnected} when only a boolean check is needed
5556 * @see {@link stronglyConnectedComponents} for mutual directed reachability
5557 *
5558 * @category algorithms
5559 * @since 4.0.0
5560 */
5561export const weaklyConnectedComponents = <N, E>(
5562 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
5563): Array<Array<NodeIndex>> => {
5564 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
5565 throw new GraphError({ message: "Cannot find weakly connected components of undirected graph" })
5566 }
5567
5568 const cache = csr.get(graph)
5569 const { primary, secondary } = csr.getAdjacencies(cache, "undirected")
5570 const nodeCount = cache.nodeIds.length
5571 const visited = new Uint8Array(nodeCount)
5572 const stack = new Uint32Array(primary.columnIndices.length + secondary!.columnIndices.length + 1)
5573 const components: Array<Array<NodeIndex>> = []
5574 for (let start = 0; start < nodeCount; start++) {
5575 if (visited[start] !== 0) {
5576 continue
5577 }
5578 const component: Array<NodeIndex> = []
5579 let stackSize = 1
5580 stack[0] = start
5581 while (stackSize > 0) {
5582 const current = stack[--stackSize]
5583 if (visited[current] !== 0) {
5584 continue
5585 }
5586 visited[current] = 1
5587 component.push(cache.nodeIds[current])
5588
5589 for (let i = primary.rowOffsets[current]; i < primary.rowOffsets[current + 1]; i++) {
5590 const neighbor = primary.columnIndices[i]
5591 if (visited[neighbor] === 0) {
5592 stack[stackSize++] = neighbor
5593 }
5594 }
5595 for (let i = secondary!.rowOffsets[current]; i < secondary!.rowOffsets[current + 1]; i++) {
5596 const neighbor = secondary!.columnIndices[i]
5597 if (visited[neighbor] === 0) {
5598 stack[stackSize++] = neighbor
5599 }
5600 }
5601 }
5602 components.push(component)
5603 }
5604 return components
5605}
5606
5607/**
5608 * Returns the strongly connected components of a directed graph.
5609 *
5610 * **When to use**
5611 *
5612 * Use when grouping nodes so every node in a component can reach every other
5613 * node in that component.
5614 *
5615 * **Details**
5616 *
5617 * Each component is represented as an array of node indices and is computed
5618 * with Kosaraju's algorithm.
5619 *
5620 * **Gotchas**
5621 *
5622 * Throws a `GraphError` when used with an undirected graph.
5623 *
5624 * **Example** (Finding strongly connected components)
5625 *
5626 * ```ts import.meta.vitest
5627 * import { Graph } from "effect"
5628 *
5629 * const graph = Graph.directed<string, string>((mutable) => {
5630 * const a = Graph.addNode(mutable, "A")
5631 * const b = Graph.addNode(mutable, "B")
5632 * const c = Graph.addNode(mutable, "C")
5633 * Graph.addEdge(mutable, a, b, "A->B")
5634 * Graph.addEdge(mutable, b, c, "B->C")
5635 * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C
5636 * })
5637 *
5638 * Graph.stronglyConnectedComponents(graph) // => [[0, 2, 1]]
5639 * ```
5640 *
5641 * @see {@link isStronglyConnected} when only a boolean check is needed
5642 * @see {@link weaklyConnectedComponents} when edge orientation should be ignored
5643 * @category algorithms
5644 * @since 3.18.0
5645 */
5646export const stronglyConnectedComponents = <N, E>(
5647 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
5648): Array<Array<NodeIndex>> => {
5649 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
5650 throw new GraphError({ message: "Cannot find strongly connected components of undirected graph" })
5651 }
5652
5653 const cache = csr.get(graph)
5654 const outgoing = csr.getOutgoing(cache)
5655 const incoming = csr.getIncoming(cache)
5656 const visited = new Uint8Array(cache.nodeIds.length)
5657 const finishOrder: Array<number> = []
5658 const stack: Array<number> = []
5659 const positions: Array<number> = []
5660
5661 // First pass records finish order on the original graph using an explicit stack.
5662 for (let start = 0; start < cache.nodeIds.length; start++) {
5663 if (visited[start] !== 0) {
5664 continue
5665 }
5666 visited[start] = 1
5667 stack.push(start)
5668 positions.push(outgoing.rowOffsets[start])
5669
5670 while (stack.length > 0) {
5671 const frame = stack.length - 1
5672 const node = stack[frame]
5673 const position = positions[frame]
5674 if (position < outgoing.rowOffsets[node + 1]) {
5675 positions[frame] = position + 1
5676 const neighbor = outgoing.columnIndices[position]
5677 if (visited[neighbor] === 0) {
5678 visited[neighbor] = 1
5679 stack.push(neighbor)
5680 positions.push(outgoing.rowOffsets[neighbor])
5681 }
5682 } else {
5683 finishOrder.push(node)
5684 stack.pop()
5685 positions.pop()
5686 }
5687 }
5688 }
5689
5690 visited.fill(0)
5691 const components: Array<Array<NodeIndex>> = []
5692 // Reversing finish order and traversing the transpose yields one SCC per search.
5693 for (let i = finishOrder.length - 1; i >= 0; i--) {
5694 const start = finishOrder[i]
5695 if (visited[start] !== 0) {
5696 continue
5697 }
5698 const component: Array<NodeIndex> = []
5699 stack.push(start)
5700
5701 while (stack.length > 0) {
5702 const node = stack.pop()!
5703 if (visited[node] !== 0) {
5704 continue
5705 }
5706 visited[node] = 1
5707 component.push(cache.nodeIds[node])
5708
5709 for (let j = incoming.rowOffsets[node]; j < incoming.rowOffsets[node + 1]; j++) {
5710 const predecessor = incoming.columnIndices[j]
5711 if (visited[predecessor] === 0) {
5712 stack.push(predecessor)
5713 }
5714 }
5715 }
5716
5717 components.push(component)
5718 }
5719
5720 return components
5721}
5722
5723/** @internal */
5724const csrReachesAll = (
5725 nodeCount: number,
5726 primary: csr.Adjacency,
5727 secondary?: csr.Adjacency
5728): boolean => {
5729 if (nodeCount === 0) {
5730 return true
5731 }
5732 const visited = new Uint8Array(nodeCount)
5733 const queue = new Uint32Array(nodeCount)
5734 let head = 0
5735 let tail = 1
5736 visited[0] = 1
5737 queue[0] = 0
5738
5739 while (head < tail) {
5740 const current = queue[head++]
5741 for (let i = primary.rowOffsets[current]; i < primary.rowOffsets[current + 1]; i++) {
5742 const neighbor = primary.columnIndices[i]
5743 if (visited[neighbor] === 0) {
5744 visited[neighbor] = 1
5745 queue[tail++] = neighbor
5746 }
5747 }
5748 if (secondary !== undefined) {
5749 for (let i = secondary.rowOffsets[current]; i < secondary.rowOffsets[current + 1]; i++) {
5750 const neighbor = secondary.columnIndices[i]
5751 if (visited[neighbor] === 0) {
5752 visited[neighbor] = 1
5753 queue[tail++] = neighbor
5754 }
5755 }
5756 }
5757 }
5758 return tail === nodeCount
5759}
5760
5761/**
5762 * Tests whether an undirected graph has at most one connected component.
5763 *
5764 * **When to use**
5765 *
5766 * Use when checking undirected connectivity without allocating the component
5767 * partition.
5768 *
5769 * **Gotchas**
5770 *
5771 * The empty graph is considered connected. Throws a `GraphError` when used
5772 * with a directed graph.
5773 *
5774 * @see {@link connectedComponents} for the component partition
5775 *
5776 * @category predicates
5777 * @since 4.0.0
5778 */
5779export const isConnected = <N, E>(
5780 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
5781): boolean => {
5782 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
5783 throw new GraphError({ message: "Cannot find connected components of directed graph" })
5784 }
5785 const cache = csr.get(graph)
5786 return csrReachesAll(cache.nodeIds.length, csr.getOutgoing(cache))
5787}
5788
5789/**
5790 * Tests whether a directed graph has at most one weakly connected component.
5791 *
5792 * **When to use**
5793 *
5794 * Use when checking whether a directed graph is connected after ignoring edge
5795 * orientation.
5796 *
5797 * **Gotchas**
5798 *
5799 * The empty graph is considered weakly connected. Throws a `GraphError` when
5800 * used with an undirected graph.
5801 *
5802 * @see {@link isStronglyConnected} when edge orientation must be respected
5803 *
5804 * @category predicates
5805 * @since 4.0.0
5806 */
5807export const isWeaklyConnected = <N, E>(
5808 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
5809): boolean => {
5810 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
5811 throw new GraphError({ message: "Cannot find weakly connected components of undirected graph" })
5812 }
5813 const cache = csr.get(graph)
5814 const { primary, secondary } = csr.getAdjacencies(cache, "undirected")
5815 return csrReachesAll(cache.nodeIds.length, primary, secondary)
5816}
5817
5818/**
5819 * Tests whether a directed graph has at most one strongly connected component.
5820 *
5821 * **When to use**
5822 *
5823 * Use when checking that every node in a directed graph can reach every other
5824 * node.
5825 *
5826 * **Gotchas**
5827 *
5828 * The empty graph is considered strongly connected. Throws a `GraphError` when
5829 * used with an undirected graph.
5830 *
5831 * @see {@link isWeaklyConnected} when edge orientation should be ignored
5832 *
5833 * @category predicates
5834 * @since 4.0.0
5835 */
5836export const isStronglyConnected = <N, E>(
5837 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
5838): boolean => {
5839 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
5840 throw new GraphError({ message: "Cannot find strongly connected components of undirected graph" })
5841 }
5842 const cache = csr.get(graph)
5843 return csrReachesAll(cache.nodeIds.length, csr.getOutgoing(cache)) &&
5844 csrReachesAll(cache.nodeIds.length, csr.getIncoming(cache))
5845}
5846
5847/**
5848 * Tests whether a non-empty undirected graph is a tree.
5849 *
5850 * **When to use**
5851 *
5852 * Use when validating that an undirected graph is connected and has no cycle.
5853 *
5854 * **Gotchas**
5855 *
5856 * The empty graph is not a tree. Parallel edges and self-loops prevent a graph
5857 * from being a tree. Throws a `GraphError` when used with a directed graph.
5858 *
5859 * @category predicates
5860 * @since 4.0.0
5861 */
5862export const isTree = <N, E>(
5863 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
5864): boolean => {
5865 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
5866 throw new GraphError({ message: "Cannot determine tree status of directed graph" })
5867 }
5868 const nodes = nodeCount(graph)
5869 return nodes > 0 && edgeCount(graph) === nodes - 1 && isConnected(graph)
5870}
5871
5872/**
5873 * Returns a minimum spanning forest of an undirected graph using Kruskal's
5874 * algorithm.
5875 *
5876 * **When to use**
5877 *
5878 * Use when selecting a minimum-cost acyclic connector for every connected
5879 * component of an undirected graph.
5880 *
5881 * **Details**
5882 *
5883 * All node indices and selected edge indices are preserved. Negative finite
5884 * weights are allowed, `Infinity` marks an unavailable edge, and equal weights
5885 * are resolved by original edge order. Disconnected inputs produce a forest,
5886 * and isolated nodes remain present.
5887 *
5888 * **Gotchas**
5889 *
5890 * Throws a `GraphError` for a directed graph or when a weight is `NaN` or
5891 * `-Infinity`. Edges weighted `Infinity` are omitted.
5892 *
5893 * @category algorithms
5894 * @since 4.0.0
5895 */
5896export const minimumSpanningForest: {
5897 <E>(cost: (edgeData: E) => number): <N>(
5898 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
5899 ) => Graph<N, E, "undirected">
5900 <N, E>(
5901 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">,
5902 cost: (edgeData: E) => number
5903 ): Graph<N, E, "undirected">
5904} = dual(2, <N, E>(
5905 graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">,
5906 cost: (edgeData: E) => number
5907): Graph<N, E, "undirected"> => {
5908 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "directed") {
5909 throw new GraphError({ message: "Cannot find minimum spanning forest of directed graph" })
5910 }
5911 const impl = internal.toImpl(graph)
5912 const nodes: Array<IndexedNode<N>> = []
5913 const compactByNode = new Map<NodeIndex, number>()
5914 for (const [index, data] of impl.nodes) {
5915 compactByNode.set(index, nodes.length)
5916 nodes.push({ index, data })
5917 }
5918 const weightedEdges: Array<{ readonly index: EdgeIndex; readonly weight: number }> = []
5919 withMutationGuard(graph, () => {
5920 for (const [index, edge] of impl.edges) {
5921 const weight = cost(edge.data)
5922 if (Number.isNaN(weight) || weight === -Infinity) {
5923 throw new GraphError({ message: "Minimum spanning forest does not support NaN or -Infinity edge weights" })
5924 }
5925 if (weight !== Infinity) {
5926 weightedEdges.push({ index, weight })
5927 }
5928 }
5929 })
5930 weightedEdges.sort((self, that) => self.weight - that.weight)
5931
5932 const parents = new Uint32Array(nodes.length)
5933 const ranks = new Uint8Array(nodes.length)
5934 for (let i = 0; i < parents.length; i++) {
5935 parents[i] = i
5936 }
5937 const find = (node: number): number => {
5938 let root = node
5939 while (parents[root] !== root) {
5940 root = parents[root]
5941 }
5942 while (parents[node] !== node) {
5943 const parent = parents[node]
5944 parents[node] = root
5945 node = parent
5946 }
5947 return root
5948 }
5949 const selected = new Set<EdgeIndex>()
5950 for (const weighted of weightedEdges) {
5951 const edge = impl.edges.get(weighted.index)!
5952 let sourceRoot = find(compactByNode.get(edge.source)!)
5953 let targetRoot = find(compactByNode.get(edge.target)!)
5954 if (sourceRoot === targetRoot) {
5955 continue
5956 }
5957 selected.add(weighted.index)
5958 if (ranks[sourceRoot] < ranks[targetRoot]) {
5959 const swap = sourceRoot
5960 sourceRoot = targetRoot
5961 targetRoot = swap
5962 }
5963 parents[targetRoot] = sourceRoot
5964 if (ranks[sourceRoot] === ranks[targetRoot]) {
5965 ranks[sourceRoot]++
5966 }
5967 }
5968
5969 const edges: Array<IndexedEdge<E>> = []
5970 for (const [index, edge] of impl.edges) {
5971 if (selected.has(index)) {
5972 edges.push({ index, source: edge.source, target: edge.target, data: edge.data })
5973 }
5974 }
5975 return fromSnapshot({ type: "undirected", nodes, edges })
5976})
5977
5978/**
5979 * Returns the transitive reduction of a directed acyclic graph.
5980 *
5981 * **When to use**
5982 *
5983 * Use when simplifying a dependency DAG while preserving which nodes can
5984 * reach which other nodes.
5985 *
5986 * **Details**
5987 *
5988 * The result preserves reachability with the fewest structural source-target
5989 * pairs. Node and retained edge indices are preserved.
5990 *
5991 * **Gotchas**
5992 *
5993 * This operation is structural and ignores edge costs. Parallel edges are
5994 * coalesced by retaining the first edge for each required pair. Throws a
5995 * `GraphError` for an undirected graph or cyclic input.
5996 *
5997 * @category algorithms
5998 * @since 4.0.0
5999 */
6000export const transitiveReduction = <N, E>(
6001 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
6002): Graph<N, E, "directed"> => {
6003 if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
6004 throw new GraphError({ message: "Cannot transitively reduce undirected graph" })
6005 }
6006 if (!isAcyclic(graph)) {
6007 throw new GraphError({ message: "Cannot transitively reduce cyclic graph" })
6008 }
6009
6010 const impl = internal.toImpl(graph)
6011 const nodes: Array<IndexedNode<N>> = []
6012 for (const [index, data] of impl.nodes) {
6013 nodes.push({ index, data })
6014 }
6015 const firstEdges = new Map<NodeIndex, Map<NodeIndex, EdgeIndex>>()
6016 for (const [edgeIndex, edge] of impl.edges) {
6017 let targets = firstEdges.get(edge.source)
6018 if (targets === undefined) {
6019 targets = new Map()
6020 firstEdges.set(edge.source, targets)
6021 }
6022 if (!targets.has(edge.target)) {
6023 targets.set(edge.target, edgeIndex)
6024 }
6025 }
6026
6027 const retained = new Set<EdgeIndex>()
6028 for (const [source, targets] of firstEdges) {
6029 for (const [target, edgeIndex] of targets) {
6030 const visited = new Set<NodeIndex>([source])
6031 const queue = [source]
6032 let reachable = false
6033 for (let head = 0; head < queue.length && !reachable; head++) {
6034 const current = queue[head]
6035 for (const candidateIndex of impl.adjacency.get(current)!) {
6036 const candidate = impl.edges.get(candidateIndex)!
6037 if (current === source && candidate.target === target) {
6038 continue
6039 }
6040 if (candidate.target === target) {
6041 reachable = true
6042 break
6043 }
6044 if (!visited.has(candidate.target)) {
6045 visited.add(candidate.target)
6046 queue.push(candidate.target)
6047 }
6048 }
6049 }
6050 if (!reachable) {
6051 retained.add(edgeIndex)
6052 }
6053 }
6054 }
6055
6056 const edges: Array<IndexedEdge<E>> = []
6057 for (const [index, edge] of impl.edges) {
6058 if (retained.has(index)) {
6059 edges.push({ index, source: edge.source, target: edge.target, data: edge.data })
6060 }
6061 }
6062 return fromSnapshot({ type: "directed", nodes, edges })
6063}
6064
6065// =============================================================================
6066// Path Finding Algorithms
6067// =============================================================================
6068
6069/**
6070 * Result of a shortest path computation.
6071 *
6072 * **When to use**
6073 *
6074 * Use to read the successful source-to-target shortest path returned by
6075 * path-finding algorithms, including the ordered node and edge indices, total
6076 * distance, and traversed edge data.
6077 *
6078 * **Details**
6079 *
6080 * Contains the node-index path, the traversed edge indices, the total numeric
6081 * distance, and the edge data encountered along the path.
6082 *
6083 * **Gotchas**
6084 *
6085 * `costs` contains original edge data, not the numeric output of the cost
6086 * function unless the edge data is numeric.
6087 *
6088 * @category models
6089 * @since 3.18.0
6090 */
6091export interface PathResult<E> {
6092 readonly path: Array<NodeIndex>
6093 readonly edges: Array<EdgeIndex>
6094 readonly distance: number
6095 readonly costs: Array<E>
6096}
6097
6098interface DenseMinHeap {
6099 nodes: Uint32Array
6100 priorities: Float64Array
6101 sequences: Float64Array
6102 positions: Int32Array | undefined
6103 size: number
6104 poppedNode: number
6105 poppedPriority: number
6106}
6107
6108const denseMinHeapMake = (capacity: number, indexed = false): DenseMinHeap => {
6109 const positions = indexed ? new Int32Array(capacity) : undefined
6110 positions?.fill(-1)
6111 return {
6112 nodes: new Uint32Array(Math.max(4, capacity)),
6113 priorities: new Float64Array(Math.max(4, capacity)),
6114 sequences: new Float64Array(Math.max(4, capacity)),
6115 positions,
6116 size: 0,
6117 poppedNode: 0,
6118 poppedPriority: 0
6119 }
6120}
6121
6122const denseMinHeapPush = (
6123 heap: DenseMinHeap,
6124 node: number,
6125 priority: number,
6126 sequence: number
6127): void => {
6128 let index = heap.positions?.[node] ?? -1
6129 if (index === -1) {
6130 if (heap.size === heap.nodes.length) {
6131 const capacity = heap.size * 2
6132 const nodes = new Uint32Array(capacity)
6133 const priorities = new Float64Array(capacity)
6134 const sequences = new Float64Array(capacity)
6135 nodes.set(heap.nodes)
6136 priorities.set(heap.priorities)
6137 sequences.set(heap.sequences)
6138 heap.nodes = nodes
6139 heap.priorities = priorities
6140 heap.sequences = sequences
6141 }
6142 index = heap.size++
6143 }
6144
6145 while (index > 0) {
6146 const parent = (index - 1) >>> 1
6147 if (
6148 priority > heap.priorities[parent] ||
6149 (priority === heap.priorities[parent] && sequence >= heap.sequences[parent])
6150 ) {
6151 break
6152 }
6153 heap.nodes[index] = heap.nodes[parent]
6154 heap.priorities[index] = heap.priorities[parent]
6155 heap.sequences[index] = heap.sequences[parent]
6156 if (heap.positions !== undefined) {
6157 heap.positions[heap.nodes[index]] = index
6158 }
6159 index = parent
6160 }
6161 heap.nodes[index] = node
6162 heap.priorities[index] = priority
6163 heap.sequences[index] = sequence
6164 if (heap.positions !== undefined) {
6165 heap.positions[node] = index
6166 }
6167}
6168
6169const denseMinHeapPop = (heap: DenseMinHeap): boolean => {
6170 if (heap.size === 0) {
6171 return false
6172 }
6173
6174 heap.poppedNode = heap.nodes[0]
6175 heap.poppedPriority = heap.priorities[0]
6176 if (heap.positions !== undefined) {
6177 heap.positions[heap.poppedNode] = -1
6178 }
6179 const last = --heap.size
6180 if (last === 0) {
6181 return true
6182 }
6183
6184 const node = heap.nodes[last]
6185 const priority = heap.priorities[last]
6186 const sequence = heap.sequences[last]
6187 let index = 0
6188 while (true) {
6189 const left = index * 2 + 1
6190 if (left >= last) {
6191 break
6192 }
6193 const right = left + 1
6194 let child = left
6195 if (
6196 right < last &&
6197 (heap.priorities[right] < heap.priorities[left] ||
6198 (heap.priorities[right] === heap.priorities[left] && heap.sequences[right] < heap.sequences[left]))
6199 ) {
6200 child = right
6201 }
6202 if (
6203 heap.priorities[child] > priority ||
6204 (heap.priorities[child] === priority && heap.sequences[child] >= sequence)
6205 ) {
6206 break
6207 }
6208 heap.nodes[index] = heap.nodes[child]
6209 heap.priorities[index] = heap.priorities[child]
6210 heap.sequences[index] = heap.sequences[child]
6211 if (heap.positions !== undefined) {
6212 heap.positions[heap.nodes[index]] = index
6213 }
6214 index = child
6215 }
6216 heap.nodes[index] = node
6217 heap.priorities[index] = priority
6218 heap.sequences[index] = sequence
6219 if (heap.positions !== undefined) {
6220 heap.positions[node] = index
6221 }
6222 return true
6223}
6224
6225/**
6226 * Configuration for finding a shortest path with Dijkstra's algorithm.
6227 *
6228 * **When to use**
6229 *
6230 * Use when configuring `dijkstra` to find a shortest path between two existing
6231 * node indices with non-negative edge costs.
6232 *
6233 * **Details**
6234 *
6235 * Specifies the source and target node indices, plus a cost function that maps
6236 * each edge's data to a non-negative numeric weight. `Infinity` is allowed and
6237 * behaves like an impassable edge.
6238 *
6239 * **Gotchas**
6240 *
6241 * `dijkstra` throws a `GraphError` when either endpoint does not exist or when
6242 * the cost function returns a negative weight or `NaN`.
6243 *
6244 * @category configuration
6245 * @since 3.18.0
6246 */
6247export interface DijkstraConfig<E> {
6248 source: NodeIndex
6249 target: NodeIndex
6250 cost: (edgeData: E) => number
6251}
6252
6253/**
6254 * Finds the shortest path from the configured source node to the target node
6255 * using Dijkstra's algorithm.
6256 *
6257 * **When to use**
6258 *
6259 * Use when you need one source-to-target shortest path and every edge cost is
6260 * non-negative.
6261 *
6262 * **Details**
6263 *
6264 * Edge costs must be non-negative and not `NaN`. `Infinity` is allowed and
6265 * behaves like an impassable edge. Returns `Option.none()` when the target is
6266 * not reachable.
6267 *
6268 * **Gotchas**
6269 *
6270 * Throws a `GraphError` when either endpoint is missing or an edge cost is
6271 * negative or `NaN`, or when a path distance exceeds the finite number range.
6272 *
6273 * **Example** (Finding shortest paths with Dijkstra)
6274 *
6275 * ```ts import.meta.vitest
6276 * import { Graph, Option } from "effect"
6277 *
6278 * const graph = Graph.directed<string, number>((mutable) => {
6279 * const a = Graph.addNode(mutable, "A")
6280 * const b = Graph.addNode(mutable, "B")
6281 * const c = Graph.addNode(mutable, "C")
6282 * Graph.addEdge(mutable, a, b, 5)
6283 * Graph.addEdge(mutable, a, c, 10)
6284 * Graph.addEdge(mutable, b, c, 2)
6285 * })
6286 *
6287 * const result = Graph.dijkstra(graph, {
6288 * source: 0,
6289 * target: 2,
6290 * cost: (edgeData) => edgeData
6291 * })
6292 *
6293 * Option.map(result, ({ distance, path }) => [distance, path] as const) // => Option.some([7, [0, 1, 2]])
6294 * ```
6295 *
6296 * @see {@link astar} when a useful heuristic can guide the search
6297 * @see {@link bellmanFord} when edge costs may be negative
6298 * @see {@link floydWarshall} when shortest paths are needed for all pairs
6299 * @category algorithms
6300 * @since 3.18.0
6301 */
6302export const dijkstra: {
6303 <E>(
6304 config: DijkstraConfig<E>
6305 ): <N, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<PathResult<E>>
6306 <N, E, T extends Kind = "directed">(
6307 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6308 config: DijkstraConfig<E>
6309 ): Option.Option<PathResult<E>>
6310} = dual(2, <N, E, T extends Kind = "directed">(
6311 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6312 config: DijkstraConfig<E>
6313): Option.Option<PathResult<E>> => {
6314 const impl = internal.toImpl(graph)
6315 // Validate that source and target nodes exist
6316 if (!impl.nodes.has(config.source)) {
6317 throw missingNode(config.source)
6318 }
6319 if (!impl.nodes.has(config.target)) {
6320 throw missingNode(config.target)
6321 }
6322
6323 const cache = csr.get(graph)
6324 const cachedEdges = csr.getEdges(cache)
6325 const cachedEdgeIds = csr.getEdgeIds(cache)
6326 const outgoing = csr.getOutgoingWithEdges(cache)
6327 const source = csr.getNodeIndex(cache, config.source)!
6328 const target = csr.getNodeIndex(cache, config.target)!
6329 const edgeWeights = new Float64Array(cachedEdges.length)
6330 withMutationGuard(graph, () => {
6331 for (let i = 0; i < cachedEdges.length; i++) {
6332 const weight = config.cost(cachedEdges[i].data)
6333 if (Number.isNaN(weight) || weight < 0) {
6334 throw new GraphError({ message: "Dijkstra's algorithm requires non-negative edge weights" })
6335 }
6336 edgeWeights[i] = weight
6337 }
6338 })
6339
6340 // Early return if source equals target
6341 if (config.source === config.target) {
6342 return Option.some({
6343 path: [config.source],
6344 edges: [],
6345 distance: 0,
6346 costs: []
6347 })
6348 }
6349
6350 const distances = new Float64Array(cache.nodeIds.length)
6351 distances.fill(Infinity)
6352 distances[source] = 0
6353 // Predecessor node and edge arrays reconstruct both the public node path and its edge data.
6354 const previousNode = new Int32Array(cache.nodeIds.length)
6355 const previousEdge = new Int32Array(cache.nodeIds.length)
6356 previousNode.fill(-1)
6357 previousEdge.fill(-1)
6358 const visited = new Uint8Array(cache.nodeIds.length)
6359 const priorityQueue = denseMinHeapMake(cache.nodeIds.length, true)
6360 let sequence = 0
6361 denseMinHeapPush(priorityQueue, source, 0, sequence++)
6362
6363 while (priorityQueue.size > 0) {
6364 denseMinHeapPop(priorityQueue)
6365 const currentNode = priorityQueue.poppedNode
6366 const currentDistance = priorityQueue.poppedPriority
6367 if (visited[currentNode] !== 0) {
6368 continue
6369 }
6370 visited[currentNode] = 1
6371 if (currentNode === target) {
6372 break
6373 }
6374
6375 for (let i: number = outgoing.rowOffsets[currentNode]; i < outgoing.rowOffsets[currentNode + 1]; i++) {
6376 const neighbor = outgoing.columnIndices[i]
6377 const edge = outgoing.edgeIndices[i]
6378 const nextDistance = currentDistance + edgeWeights[edge]
6379 if (edgeWeights[edge] !== Infinity && !Number.isFinite(nextDistance)) {
6380 throw new GraphError({ message: "Dijkstra distance calculation exceeded the finite number range" })
6381 }
6382 if (nextDistance < distances[neighbor]) {
6383 distances[neighbor] = nextDistance
6384 previousNode[neighbor] = currentNode
6385 previousEdge[neighbor] = edge
6386 if (visited[neighbor] === 0) {
6387 denseMinHeapPush(priorityQueue, neighbor, nextDistance, sequence++)
6388 }
6389 }
6390 }
6391 }
6392
6393 if (distances[target] === Infinity) {
6394 return Option.none()
6395 }
6396
6397 const path: Array<NodeIndex> = []
6398 const edges: Array<EdgeIndex> = []
6399 const costs: Array<E> = []
6400 let current = target
6401 while (current !== -1) {
6402 path.push(cache.nodeIds[current])
6403 const edge = previousEdge[current]
6404 if (edge !== -1) {
6405 edges.push(cachedEdgeIds[edge])
6406 costs.push(cachedEdges[edge].data)
6407 }
6408 current = previousNode[current]
6409 }
6410 path.reverse()
6411 edges.reverse()
6412 costs.reverse()
6413
6414 return Option.some({ path, edges, distance: distances[target], costs })
6415})
6416
6417/**
6418 * Result of an all-pairs shortest path computation.
6419 *
6420 * **When to use**
6421 *
6422 * Use when storing or passing around the complete output of `floydWarshall` so
6423 * callers can look up shortest distances, node and edge paths, and edge data
6424 * for any source and target node pair.
6425 *
6426 * **Details**
6427 *
6428 * Contains distance, node-path, edge-index-path, and edge-data maps keyed by
6429 * source and target node indices. Unreachable pairs have distance `Infinity`,
6430 * path `null`, and empty edge and cost arrays.
6431 *
6432 * @category models
6433 * @since 3.18.0
6434 */
6435export interface AllPairsResult<E> {
6436 readonly distances: Map<NodeIndex, Map<NodeIndex, number>>
6437 readonly paths: Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>
6438 readonly edges: Map<NodeIndex, Map<NodeIndex, Array<EdgeIndex>>>
6439 readonly costs: Map<NodeIndex, Map<NodeIndex, Array<E>>>
6440}
6441
6442/**
6443 * Finds shortest paths between all pairs of nodes using the Floyd-Warshall
6444 * algorithm.
6445 *
6446 * **When to use**
6447 *
6448 * Use when many or all node pairs will be queried and cubic computation plus
6449 * quadratic result storage is acceptable.
6450 *
6451 * **Details**
6452 *
6453 * Computes distances, reconstructed node paths, and edge-data paths for every
6454 * source and target pair in O(V^3) time. Negative edge weights are allowed, and
6455 * `Infinity` behaves like an impassable edge.
6456 *
6457 * **Gotchas**
6458 *
6459 * A `GraphError` is thrown if any edge weight is `NaN` or `-Infinity`, or if
6460 * finite arithmetic overflows or underflows, or if any negative cycle is
6461 * detected.
6462 *
6463 * **Example** (Finding all-pairs shortest paths)
6464 *
6465 * ```ts import.meta.vitest
6466 * import { Graph } from "effect"
6467 *
6468 * const graph = Graph.directed<string, number>((mutable) => {
6469 * const a = Graph.addNode(mutable, "A")
6470 * const b = Graph.addNode(mutable, "B")
6471 * const c = Graph.addNode(mutable, "C")
6472 * Graph.addEdge(mutable, a, b, 3)
6473 * Graph.addEdge(mutable, b, c, 2)
6474 * Graph.addEdge(mutable, a, c, 7)
6475 * })
6476 *
6477 * const result = Graph.floydWarshall(graph, (edgeData) => edgeData)
6478 * const shortest = { distance: result.distances.get(0)?.get(2), path: result.paths.get(0)?.get(2) }
6479 * shortest // => { distance: 5, path: [0, 1, 2] }
6480 * ```
6481 *
6482 * @see {@link dijkstra} for one query with non-negative edge costs
6483 * @see {@link bellmanFord} for one query that may include negative edge costs
6484 * @category algorithms
6485 * @since 3.18.0
6486 */
6487export const floydWarshall: {
6488 <E>(
6489 cost: (edgeData: E) => number
6490 ): <N, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => AllPairsResult<E>
6491 <N, E, T extends Kind = "directed">(
6492 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6493 cost: (edgeData: E) => number
6494 ): AllPairsResult<E>
6495} = dual(2, <N, E, T extends Kind = "directed">(
6496 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6497 cost: (edgeData: E) => number
6498): AllPairsResult<E> => {
6499 const cache = csr.get(graph)
6500 const edges = csr.getEdges(cache)
6501 const edgeIds = csr.getEdgeIds(cache)
6502 const edgeCache = csr.getEdgeEndpoints(cache)
6503 const size = cache.nodeIds.length
6504 // Flat matrices keep the O(N^2) working set contiguous and avoid nested map lookups in the O(N^3) loop.
6505 const distancesMatrix = new Float64Array(size * size)
6506 const nextMatrix = new Int32Array(size * size)
6507 const edgeMatrix = new Int32Array(size * size)
6508 distancesMatrix.fill(Infinity)
6509 nextMatrix.fill(-1)
6510 edgeMatrix.fill(-1)
6511 for (let i = 0; i < size; i++) {
6512 distancesMatrix[i * size + i] = 0
6513 }
6514
6515 withMutationGuard(graph, () => {
6516 for (let edge = 0; edge < edges.length; edge++) {
6517 const weight = cost(edges[edge].data)
6518 if (Number.isNaN(weight) || weight === -Infinity) {
6519 throw new GraphError({ message: "Floyd-Warshall algorithm does not support NaN or -Infinity edge weights" })
6520 }
6521 const source = edgeCache.sources[edge]
6522 const target = edgeCache.targets[edge]
6523 const position = source * size + target
6524 if (weight < distancesMatrix[position]) {
6525 distancesMatrix[position] = weight
6526 nextMatrix[position] = target
6527 edgeMatrix[position] = edge
6528 }
6529 if (graph.type === "undirected") {
6530 const reverse = target * size + source
6531 if (weight < distancesMatrix[reverse]) {
6532 distancesMatrix[reverse] = weight
6533 nextMatrix[reverse] = source
6534 edgeMatrix[reverse] = edge
6535 }
6536 }
6537 }
6538 })
6539
6540 for (let k = 0; k < size; k++) {
6541 const kRow = k * size
6542 for (let i = 0; i < size; i++) {
6543 const iRow = i * size
6544 const distanceIK = distancesMatrix[iRow + k]
6545 if (distanceIK === Infinity) {
6546 continue
6547 }
6548 const nextIK = nextMatrix[iRow + k]
6549 for (let j = 0; j < size; j++) {
6550 const distanceKJ = distancesMatrix[kRow + j]
6551 if (distanceKJ === Infinity) {
6552 continue
6553 }
6554 const candidate = distanceIK + distanceKJ
6555 if (!Number.isFinite(candidate)) {
6556 throw new GraphError({ message: "Floyd-Warshall distance calculation exceeded the finite number range" })
6557 }
6558 if (candidate < distancesMatrix[iRow + j] && nextIK !== -1) {
6559 distancesMatrix[iRow + j] = candidate
6560 nextMatrix[iRow + j] = nextIK
6561 }
6562 }
6563 }
6564 }
6565
6566 for (let i = 0; i < size; i++) {
6567 if (distancesMatrix[i * size + i] < 0) {
6568 throw new GraphError({ message: `Negative cycle detected involving node ${cache.nodeIds[i]}` })
6569 }
6570 }
6571
6572 const distances = new Map<NodeIndex, Map<NodeIndex, number>>()
6573 const paths = new Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>()
6574 const edgePaths = new Map<NodeIndex, Map<NodeIndex, Array<EdgeIndex>>>()
6575 const costs = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
6576 for (let i = 0; i < size; i++) {
6577 const source = cache.nodeIds[i]
6578 const distanceRow = new Map<NodeIndex, number>()
6579 const pathRow = new Map<NodeIndex, Array<NodeIndex> | null>()
6580 const edgePathRow = new Map<NodeIndex, Array<EdgeIndex>>()
6581 const costRow = new Map<NodeIndex, Array<E>>()
6582 distances.set(source, distanceRow)
6583 paths.set(source, pathRow)
6584 edgePaths.set(source, edgePathRow)
6585 costs.set(source, costRow)
6586
6587 for (let j = 0; j < size; j++) {
6588 const target = cache.nodeIds[j]
6589 const distance = distancesMatrix[i * size + j]
6590 distanceRow.set(target, distance)
6591 if (i === j) {
6592 pathRow.set(target, [source])
6593 edgePathRow.set(target, [])
6594 costRow.set(target, [])
6595 } else if (distance === Infinity) {
6596 pathRow.set(target, null)
6597 edgePathRow.set(target, [])
6598 costRow.set(target, [])
6599 } else {
6600 const path = [source]
6601 const pathEdges: Array<EdgeIndex> = []
6602 const pathCosts: Array<E> = []
6603 let current = i
6604 while (current !== j) {
6605 const next = nextMatrix[current * size + j]
6606 if (next === -1) {
6607 break
6608 }
6609 const edge = edgeMatrix[current * size + next]
6610 if (edge !== -1) {
6611 pathEdges.push(edgeIds[edge])
6612 pathCosts.push(edges[edge].data)
6613 }
6614 current = next
6615 path.push(cache.nodeIds[current])
6616 }
6617 pathRow.set(target, path)
6618 edgePathRow.set(target, pathEdges)
6619 costRow.set(target, pathCosts)
6620 }
6621 }
6622 }
6623
6624 return { distances, paths, edges: edgePaths, costs }
6625})
6626
6627/**
6628 * Configuration for finding a shortest path with the A* algorithm.
6629 *
6630 * **When to use**
6631 *
6632 * Use when configuring `astar` for point-to-point shortest-path searches where
6633 * node data can provide a heuristic estimate toward the target.
6634 *
6635 * **Details**
6636 *
6637 * Specifies the source and target node indices, an edge-cost function that maps
6638 * edge data to non-negative weights, and a heuristic that estimates the
6639 * remaining cost from a node to the target.
6640 *
6641 * **Gotchas**
6642 *
6643 * Heuristic values must be finite and the heuristic must be consistent for A*
6644 * to guarantee a shortest path.
6645 *
6646 * @category configuration
6647 * @since 3.18.0
6648 */
6649export interface AstarConfig<E, N> {
6650 source: NodeIndex
6651 target: NodeIndex
6652 cost: (edgeData: E) => number
6653 heuristic: (sourceNodeData: N, targetNodeData: N) => number
6654}
6655
6656/**
6657 * Finds the shortest path from the configured source node to the target node
6658 * using the A* pathfinding algorithm.
6659 *
6660 * **When to use**
6661 *
6662 * Use when a meaningful heuristic can reduce point-to-point search compared
6663 * with Dijkstra's algorithm.
6664 *
6665 * **Details**
6666 *
6667 * The edge-cost function must return non-negative weights and not `NaN`.
6668 * `Infinity` is allowed and behaves like an impassable edge. Returns
6669 * `Option.none()` when the target is not reachable.
6670 *
6671 * **Gotchas**
6672 *
6673 * The heuristic must be consistent for the shortest-path guarantee and must
6674 * return finite values. Missing endpoints, invalid edge costs, or non-finite
6675 * heuristic values or arithmetic results throw a `GraphError`.
6676 *
6677 * **Example** (Finding shortest paths with A*)
6678 *
6679 * ```ts import.meta.vitest
6680 * import { Graph, Option } from "effect"
6681 *
6682 * const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => {
6683 * const a = Graph.addNode(mutable, { x: 0, y: 0 })
6684 * const b = Graph.addNode(mutable, { x: 1, y: 0 })
6685 * const c = Graph.addNode(mutable, { x: 2, y: 0 })
6686 * Graph.addEdge(mutable, a, b, 1)
6687 * Graph.addEdge(mutable, b, c, 1)
6688 * })
6689 *
6690 * // Manhattan distance heuristic
6691 * const heuristic = (
6692 * nodeData: { x: number; y: number },
6693 * targetData: { x: number; y: number }
6694 * ) => Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y)
6695 *
6696 * const result = Graph.astar(graph, {
6697 * source: 0,
6698 * target: 2,
6699 * cost: (edgeData) => edgeData,
6700 * heuristic
6701 * })
6702 *
6703 * Option.map(result, ({ distance, path }) => [distance, path] as const) // => Option.some([2, [0, 1, 2]])
6704 * ```
6705 *
6706 * @see {@link dijkstra} when no useful heuristic is available
6707 * @see {@link bellmanFord} when edge costs may be negative
6708 * @category algorithms
6709 * @since 3.18.0
6710 */
6711export const astar: {
6712 <E, N>(
6713 config: AstarConfig<E, N>
6714 ): <T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<PathResult<E>>
6715 <N, E, T extends Kind = "directed">(
6716 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6717 config: AstarConfig<E, N>
6718 ): Option.Option<PathResult<E>>
6719} = dual(2, <N, E, T extends Kind = "directed">(
6720 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6721 config: AstarConfig<E, N>
6722): Option.Option<PathResult<E>> => {
6723 const impl = internal.toImpl(graph)
6724 // Validate that source and target nodes exist
6725 if (!impl.nodes.has(config.source)) {
6726 throw missingNode(config.source)
6727 }
6728 if (!impl.nodes.has(config.target)) {
6729 throw missingNode(config.target)
6730 }
6731
6732 const cache = csr.get(graph)
6733 const cachedEdges = csr.getEdges(cache)
6734 const cachedEdgeIds = csr.getEdgeIds(cache)
6735 const outgoing = csr.getOutgoingWithEdges(cache)
6736 const source = csr.getNodeIndex(cache, config.source)!
6737 const target = csr.getNodeIndex(cache, config.target)!
6738 const sourceNodeData = cache.nodeData[source] as N
6739 const targetNodeData = cache.nodeData[target] as N
6740 const edgeWeights = new Float64Array(cachedEdges.length)
6741 withMutationGuard(graph, () => {
6742 for (let i = 0; i < cachedEdges.length; i++) {
6743 const weight = config.cost(cachedEdges[i].data)
6744 if (Number.isNaN(weight) || weight < 0) {
6745 throw new GraphError({ message: "A* algorithm requires non-negative edge weights" })
6746 }
6747 edgeWeights[i] = weight
6748 }
6749 })
6750
6751 // Early return if source equals target
6752 if (config.source === config.target) {
6753 if (!Number.isFinite(withMutationGuard(graph, () => config.heuristic(sourceNodeData, targetNodeData)))) {
6754 throw new GraphError({ message: "A* algorithm requires finite heuristic values" })
6755 }
6756 return Option.some({
6757 path: [config.source],
6758 edges: [],
6759 distance: 0,
6760 costs: []
6761 })
6762 }
6763
6764 const getHeuristic = (nodeData: N): number => {
6765 const value = config.heuristic(nodeData, targetNodeData)
6766 if (!Number.isFinite(value)) {
6767 throw new GraphError({ message: "A* algorithm requires finite heuristic values" })
6768 }
6769 return value
6770 }
6771
6772 const scores = new Float64Array(cache.nodeIds.length)
6773 scores.fill(Infinity)
6774 scores[source] = 0
6775 // Predecessor node and edge arrays preserve path reconstruction while the hot loop uses compact indices.
6776 const previousNode = new Int32Array(cache.nodeIds.length)
6777 const previousEdge = new Int32Array(cache.nodeIds.length)
6778 previousNode.fill(-1)
6779 previousEdge.fill(-1)
6780 const visited = new Uint8Array(cache.nodeIds.length)
6781 const openSet = denseMinHeapMake(cache.nodeIds.length)
6782 let sequence = 0
6783 withMutationGuard(graph, () => {
6784 denseMinHeapPush(openSet, source, getHeuristic(sourceNodeData), sequence++)
6785
6786 while (openSet.size > 0) {
6787 denseMinHeapPop(openSet)
6788 const current = openSet.poppedNode
6789 if (visited[current] !== 0) {
6790 continue
6791 }
6792 visited[current] = 1
6793 if (current === target) {
6794 break
6795 }
6796
6797 const currentScore = scores[current]
6798 for (let i: number = outgoing.rowOffsets[current]; i < outgoing.rowOffsets[current + 1]; i++) {
6799 const neighbor = outgoing.columnIndices[i]
6800 if (visited[neighbor] !== 0) {
6801 continue
6802 }
6803 const edge = outgoing.edgeIndices[i]
6804 const tentativeScore = currentScore + edgeWeights[edge]
6805 if (edgeWeights[edge] !== Infinity && !Number.isFinite(tentativeScore)) {
6806 throw new GraphError({ message: "A* distance calculation exceeded the finite number range" })
6807 }
6808 if (tentativeScore < scores[neighbor]) {
6809 scores[neighbor] = tentativeScore
6810 previousNode[neighbor] = current
6811 previousEdge[neighbor] = edge
6812 const priority = tentativeScore + getHeuristic(cache.nodeData[neighbor] as N)
6813 if (!Number.isFinite(priority)) {
6814 throw new GraphError({ message: "A* priority calculation exceeded the finite number range" })
6815 }
6816 denseMinHeapPush(openSet, neighbor, priority, sequence++)
6817 }
6818 }
6819 }
6820 })
6821
6822 if (scores[target] === Infinity) {
6823 return Option.none()
6824 }
6825
6826 const path: Array<NodeIndex> = []
6827 const edges: Array<EdgeIndex> = []
6828 const costs: Array<E> = []
6829 let current = target
6830 while (current !== -1) {
6831 path.push(cache.nodeIds[current])
6832 const edge = previousEdge[current]
6833 if (edge !== -1) {
6834 edges.push(cachedEdgeIds[edge])
6835 costs.push(cachedEdges[edge].data)
6836 }
6837 current = previousNode[current]
6838 }
6839 path.reverse()
6840 edges.reverse()
6841 costs.reverse()
6842 return Option.some({ path, edges, distance: scores[target], costs })
6843})
6844
6845/**
6846 * Configuration for finding a shortest path with the Bellman-Ford algorithm.
6847 *
6848 * **When to use**
6849 *
6850 * Use when configuring `bellmanFord` to find a shortest path where edge
6851 * weights may be negative.
6852 *
6853 * **Details**
6854 *
6855 * Specifies the source and target node indices, plus a cost function that maps
6856 * each edge's data to a numeric weight.
6857 *
6858 * @category configuration
6859 * @since 3.18.0
6860 */
6861export interface BellmanFordConfig<E> {
6862 source: NodeIndex
6863 target: NodeIndex
6864 cost: (edgeData: E) => number
6865}
6866
6867/**
6868 * Finds the shortest path from the configured source node to the target node
6869 * using the Bellman-Ford algorithm.
6870 *
6871 * **When to use**
6872 *
6873 * Use when one source-to-target shortest-path query may traverse negative-cost
6874 * edges.
6875 *
6876 * **Details**
6877 *
6878 * Negative edge weights are allowed, and `Infinity` behaves like an impassable
6879 * edge. Returns `Option.none()` when the target is unreachable. A reachable
6880 * negative cycle only causes failure when it can affect the target.
6881 *
6882 * **Gotchas**
6883 *
6884 * Missing endpoints, unsupported weights, finite-range overflow, or a relevant
6885 * negative cycle throw a `GraphError`. In an undirected graph, any reachable
6886 * negative edge forms a negative cycle because it can be traversed both ways.
6887 *
6888 * **Example** (Finding shortest paths with Bellman-Ford)
6889 *
6890 * ```ts import.meta.vitest
6891 * import { Graph, Option } from "effect"
6892 *
6893 * const graph = Graph.directed<string, number>((mutable) => {
6894 * const a = Graph.addNode(mutable, "A")
6895 * const b = Graph.addNode(mutable, "B")
6896 * const c = Graph.addNode(mutable, "C")
6897 * Graph.addEdge(mutable, a, b, -1) // Negative weight allowed
6898 * Graph.addEdge(mutable, b, c, 3)
6899 * Graph.addEdge(mutable, a, c, 5)
6900 * })
6901 *
6902 * const result = Graph.bellmanFord(graph, {
6903 * source: 0,
6904 * target: 2,
6905 * cost: (edgeData) => edgeData
6906 * })
6907 *
6908 * Option.map(result, ({ distance, path }) => [distance, path] as const) // => Option.some([2, [0, 1, 2]])
6909 * ```
6910 *
6911 * @see {@link dijkstra} for non-negative edge costs
6912 * @category algorithms
6913 * @since 3.18.0
6914 */
6915export const bellmanFord: {
6916 <E>(
6917 config: BellmanFordConfig<E>
6918 ): <N, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<PathResult<E>>
6919 <N, E, T extends Kind = "directed">(
6920 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6921 config: BellmanFordConfig<E>
6922 ): Option.Option<PathResult<E>>
6923} = dual(2, <N, E, T extends Kind = "directed">(
6924 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6925 config: BellmanFordConfig<E>
6926): Option.Option<PathResult<E>> => {
6927 const impl = internal.toImpl(graph)
6928 // Validate that source and target nodes exist
6929 if (!impl.nodes.has(config.source)) {
6930 throw missingNode(config.source)
6931 }
6932 if (!impl.nodes.has(config.target)) {
6933 throw missingNode(config.target)
6934 }
6935
6936 const cache = csr.get(graph)
6937 const edges = csr.getEdges(cache)
6938 const edgeIds = csr.getEdgeIds(cache)
6939 const edgeCache = csr.getEdgeEndpoints(cache)
6940 const outgoing = csr.getOutgoingWithEdges(cache)
6941 const source = csr.getNodeIndex(cache, config.source)!
6942 const target = csr.getNodeIndex(cache, config.target)!
6943 const weights = new Float64Array(edges.length)
6944 withMutationGuard(graph, () => {
6945 for (let i = 0; i < edges.length; i++) {
6946 const weight = config.cost(edges[i].data)
6947 if (Number.isNaN(weight) || weight === -Infinity) {
6948 throw new GraphError({ message: "Bellman-Ford algorithm does not support NaN or -Infinity edge weights" })
6949 }
6950 weights[i] = weight
6951 }
6952 })
6953
6954 const addWeight = (distance: number, weight: number): number => {
6955 if (distance === Infinity || weight === Infinity) {
6956 return Infinity
6957 }
6958 const candidate = distance + weight
6959 if (!Number.isFinite(candidate)) {
6960 throw new GraphError({ message: "Bellman-Ford distance calculation exceeded the finite number range" })
6961 }
6962 return candidate
6963 }
6964
6965 const distances = new Float64Array(cache.nodeIds.length)
6966 const previousNode = new Int32Array(cache.nodeIds.length)
6967 const previousEdge = new Int32Array(cache.nodeIds.length)
6968 distances.fill(Infinity)
6969 previousNode.fill(-1)
6970 previousEdge.fill(-1)
6971 distances[source] = 0
6972
6973 for (let iteration = 0; iteration < cache.nodeIds.length - 1; iteration++) {
6974 let hasUpdate = false
6975 for (let edge = 0; edge < edges.length; edge++) {
6976 const edgeSource = edgeCache.sources[edge]
6977 const edgeTarget = edgeCache.targets[edge]
6978 const weight = weights[edge]
6979 const sourceDistance = distances[edgeSource]
6980 const candidate = addWeight(sourceDistance, weight)
6981 if (candidate < distances[edgeTarget]) {
6982 distances[edgeTarget] = candidate
6983 previousNode[edgeTarget] = edgeSource
6984 previousEdge[edgeTarget] = edge
6985 hasUpdate = true
6986 }
6987 if (graph.type === "undirected" && edgeSource !== edgeTarget) {
6988 const targetDistance = distances[edgeTarget]
6989 const reverseCandidate = addWeight(targetDistance, weight)
6990 if (reverseCandidate < distances[edgeSource]) {
6991 distances[edgeSource] = reverseCandidate
6992 previousNode[edgeSource] = edgeTarget
6993 previousEdge[edgeSource] = edge
6994 hasUpdate = true
6995 }
6996 }
6997 }
6998 if (!hasUpdate) {
6999 break
7000 }
7001 }
7002
7003 // A relaxable edge after N-1 passes marks a reachable negative cycle; propagate to see if it reaches the target.
7004 const affected = new Uint8Array(cache.nodeIds.length)
7005 const queue = new Uint32Array(cache.nodeIds.length)
7006 let head = 0
7007 let tail = 0
7008 const markAffected = (node: number) => {
7009 if (affected[node] === 0) {
7010 affected[node] = 1
7011 queue[tail++] = node
7012 }
7013 }
7014 for (let edge = 0; edge < edges.length; edge++) {
7015 const edgeSource = edgeCache.sources[edge]
7016 const edgeTarget = edgeCache.targets[edge]
7017 const weight = weights[edge]
7018 if (addWeight(distances[edgeSource], weight) < distances[edgeTarget]) {
7019 markAffected(edgeTarget)
7020 }
7021 if (
7022 graph.type === "undirected" &&
7023 edgeSource !== edgeTarget &&
7024 addWeight(distances[edgeTarget], weight) < distances[edgeSource]
7025 ) {
7026 markAffected(edgeSource)
7027 }
7028 }
7029 if (tail > 0) {
7030 while (head < tail) {
7031 const node = queue[head++]
7032 for (let i = outgoing.rowOffsets[node]; i < outgoing.rowOffsets[node + 1]; i++) {
7033 if (weights[outgoing.edgeIndices[i]] !== Infinity) {
7034 markAffected(outgoing.columnIndices[i])
7035 }
7036 }
7037 }
7038 }
7039 if (affected[target] !== 0) {
7040 throw new GraphError({ message: `Negative cycle affects path to node ${config.target}` })
7041 }
7042 if (distances[target] === Infinity) {
7043 return Option.none()
7044 }
7045
7046 const path: Array<NodeIndex> = []
7047 const pathEdges: Array<EdgeIndex> = []
7048 const costs: Array<E> = []
7049 let current = target
7050 let remaining = cache.nodeIds.length
7051 while (current !== -1) {
7052 if (remaining-- === 0) {
7053 throw new GraphError({ message: `Negative cycle affects path to node ${config.target}` })
7054 }
7055 path.push(cache.nodeIds[current])
7056 const edge = previousEdge[current]
7057 if (edge !== -1) {
7058 pathEdges.push(edgeIds[edge])
7059 costs.push(edges[edge].data)
7060 }
7061 current = previousNode[current]
7062 }
7063 path.reverse()
7064 pathEdges.reverse()
7065 costs.reverse()
7066 return Option.some({ path, edges: pathEdges, distance: distances[target], costs })
7067})
7068
7069/**
7070 * A repeatable lazy iterable of edge-aware graph paths.
7071 *
7072 * **When to use**
7073 *
7074 * Use as the lazy result of graph path-enumeration functions.
7075 *
7076 * **Details**
7077 *
7078 * Each fresh iterator repeats the path enumeration.
7079 *
7080 * @category models
7081 * @since 4.0.0
7082 */
7083export interface PathWalker<E> extends Iterable<PathResult<E>> {}
7084
7085/**
7086 * Configuration for lazy simple-path enumeration.
7087 *
7088 * **When to use**
7089 *
7090 * Use when bounding enumeration of loop-free routes between two nodes.
7091 *
7092 * **Details**
7093 *
7094 * `limit` bounds the number of yielded paths and defaults to `Infinity`.
7095 *
7096 * **Gotchas**
7097 *
7098 * `limit` must be a non-negative integer or `Infinity`.
7099 *
7100 * @category configuration
7101 * @since 4.0.0
7102 */
7103export interface SimplePathsConfig {
7104 readonly source: NodeIndex
7105 readonly target: NodeIndex
7106 readonly limit?: number
7107}
7108
7109/**
7110 * Configuration for enumerating all tied shortest paths.
7111 *
7112 * **When to use**
7113 *
7114 * Use when bounding enumeration of every route tied for minimum total cost.
7115 *
7116 * **Details**
7117 *
7118 * Edge costs must be non-negative. `limit` bounds the number of yielded paths
7119 * and defaults to `Infinity`.
7120 *
7121 * **Gotchas**
7122 *
7123 * Invalid costs and limits throw a `GraphError` when evaluated.
7124 *
7125 * @category configuration
7126 * @since 4.0.0
7127 */
7128export interface AllShortestPathsConfig<E> extends DijkstraConfig<E> {
7129 readonly limit?: number
7130}
7131
7132const pathEnumerationLimit = (limit: number | undefined): number => {
7133 const value = limit ?? Infinity
7134 if (value !== Infinity && (!Number.isInteger(value) || value < 0)) {
7135 throw new GraphError({ message: "Path enumeration limit must be a non-negative integer or Infinity" })
7136 }
7137 return value
7138}
7139
7140const pathWalker = <E>(iterator: () => Iterator<PathResult<E>>): PathWalker<E> => ({
7141 [Symbol.iterator]: iterator
7142})
7143
7144/**
7145 * Lazily enumerates simple source-to-target paths in depth-first edge order.
7146 *
7147 * **When to use**
7148 *
7149 * Use when you need possible loop-free routes rather than only an optimal
7150 * route.
7151 *
7152 * **Details**
7153 *
7154 * Nodes are never repeated within a path, so enumeration is finite even for
7155 * cyclic graphs. Path distance is the number of traversed edges.
7156 *
7157 * **Gotchas**
7158 *
7159 * The number of simple paths can be exponential. Missing endpoints or an
7160 * invalid `limit` throw a `GraphError`. Mutable graphs are snapshotted when
7161 * iteration begins.
7162 *
7163 * @see {@link allShortestPaths} for enumerating only minimum-cost routes
7164 *
7165 * @category algorithms
7166 * @since 4.0.0
7167 */
7168export const simplePaths: {
7169 <E>(config: SimplePathsConfig): <N, T extends Kind = "directed">(
7170 graph: Graph<N, E, T> | MutableGraph<N, E, T>
7171 ) => PathWalker<E>
7172 <N, E, T extends Kind = "directed">(
7173 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7174 config: SimplePathsConfig
7175 ): PathWalker<E>
7176} = dual(2, <N, E, T extends Kind = "directed">(
7177 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7178 config: SimplePathsConfig
7179): PathWalker<E> => {
7180 const impl = internal.toImpl(graph)
7181 if (!impl.nodes.has(config.source)) {
7182 throw missingNode(config.source)
7183 }
7184 if (!impl.nodes.has(config.target)) {
7185 throw missingNode(config.target)
7186 }
7187 const limit = pathEnumerationLimit(config.limit)
7188
7189 return pathWalker(function*() {
7190 const cache = csr.get(graph)
7191 const source = csr.getNodeIndex(cache, config.source)
7192 if (source === undefined) {
7193 throw missingNode(config.source)
7194 }
7195 const target = csr.getNodeIndex(cache, config.target)
7196 if (target === undefined) {
7197 throw missingNode(config.target)
7198 }
7199 if (limit === 0) {
7200 return
7201 }
7202 const outgoing = csr.getOutgoingWithEdges(cache)
7203 const edgeIds = csr.getEdgeIds(cache)
7204 const graphEdges = csr.getEdges(cache)
7205 const path = [config.source]
7206 const pathEdges: Array<EdgeIndex> = []
7207 const costs: Array<E> = []
7208 const visited = new Uint8Array(cache.nodeIds.length)
7209 visited[source] = 1
7210 const stack: Array<{ readonly node: number; position: number }> = [{
7211 node: source,
7212 position: outgoing.rowOffsets[source]
7213 }]
7214 let emitted = 0
7215
7216 const backtrack = () => {
7217 const frame = stack.pop()!
7218 if (stack.length > 0) {
7219 visited[frame.node] = 0
7220 path.pop()
7221 pathEdges.pop()
7222 costs.pop()
7223 }
7224 }
7225
7226 while (stack.length > 0 && emitted < limit) {
7227 const frame = stack[stack.length - 1]
7228 if (frame.node === target) {
7229 emitted++
7230 yield {
7231 path: Array.from(path),
7232 edges: Array.from(pathEdges),
7233 distance: pathEdges.length,
7234 costs: Array.from(costs)
7235 }
7236 backtrack()
7237 continue
7238 }
7239 if (frame.position >= outgoing.rowOffsets[frame.node + 1]) {
7240 backtrack()
7241 continue
7242 }
7243 const position = frame.position++
7244 const neighbor = outgoing.columnIndices[position]
7245 if (visited[neighbor] !== 0) {
7246 continue
7247 }
7248 const edge = outgoing.edgeIndices[position]
7249 visited[neighbor] = 1
7250 path.push(cache.nodeIds[neighbor])
7251 pathEdges.push(edgeIds[edge])
7252 costs.push(graphEdges[edge].data)
7253 stack.push({ node: neighbor, position: outgoing.rowOffsets[neighbor] })
7254 }
7255 })
7256})
7257
7258/**
7259 * Lazily enumerates all simple paths tied for minimum total cost.
7260 *
7261 * **When to use**
7262 *
7263 * Use when every distinct route tied for the minimum total cost is required.
7264 *
7265 * **Details**
7266 *
7267 * Parallel edges produce distinct paths. Edge costs must be non-negative;
7268 * `Infinity` behaves as unavailable.
7269 *
7270 * **Gotchas**
7271 *
7272 * The number of tied paths can still be large. Missing endpoints, invalid
7273 * costs, arithmetic overflow, or an invalid `limit` throw a `GraphError`.
7274 * Mutable graphs are snapshotted when iteration begins.
7275 *
7276 * @see {@link dijkstra} when one shortest path is sufficient
7277 * @see {@link simplePaths} for routes regardless of cost
7278 *
7279 * @category algorithms
7280 * @since 4.0.0
7281 */
7282export const allShortestPaths: {
7283 <E>(config: AllShortestPathsConfig<E>): <N, T extends Kind = "directed">(
7284 graph: Graph<N, E, T> | MutableGraph<N, E, T>
7285 ) => PathWalker<E>
7286 <N, E, T extends Kind = "directed">(
7287 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7288 config: AllShortestPathsConfig<E>
7289 ): PathWalker<E>
7290} = dual(2, <N, E, T extends Kind = "directed">(
7291 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7292 config: AllShortestPathsConfig<E>
7293): PathWalker<E> => {
7294 const impl = internal.toImpl(graph)
7295 if (!impl.nodes.has(config.source)) {
7296 throw missingNode(config.source)
7297 }
7298 if (!impl.nodes.has(config.target)) {
7299 throw missingNode(config.target)
7300 }
7301 const limit = pathEnumerationLimit(config.limit)
7302
7303 return pathWalker(function*() {
7304 const cache = csr.get(graph)
7305 const source = csr.getNodeIndex(cache, config.source)
7306 if (source === undefined) {
7307 throw missingNode(config.source)
7308 }
7309 const target = csr.getNodeIndex(cache, config.target)
7310 if (target === undefined) {
7311 throw missingNode(config.target)
7312 }
7313 const graphEdges = csr.getEdges(cache)
7314 const edgeIds = csr.getEdgeIds(cache)
7315 const outgoing = csr.getOutgoingWithEdges(cache)
7316 const weights = new Float64Array(graphEdges.length)
7317 withMutationGuard(graph, () => {
7318 for (let edge = 0; edge < graphEdges.length; edge++) {
7319 const weight = config.cost(graphEdges[edge].data)
7320 if (Number.isNaN(weight) || weight < 0) {
7321 throw new GraphError({ message: "All shortest paths requires non-negative edge weights" })
7322 }
7323 weights[edge] = weight
7324 }
7325 })
7326 if (limit === 0) {
7327 return
7328 }
7329
7330 const distances = new Float64Array(cache.nodeIds.length)
7331 distances.fill(Infinity)
7332 distances[source] = 0
7333 const previous: Array<Array<{ readonly node: number; readonly edge: number }> | undefined> = new Array(
7334 cache.nodeIds.length
7335 )
7336 const queue = denseMinHeapMake(cache.nodeIds.length)
7337 let sequence = 0
7338 denseMinHeapPush(queue, source, 0, sequence++)
7339 while (queue.size > 0) {
7340 denseMinHeapPop(queue)
7341 const currentNode = queue.poppedNode
7342 const currentDistance = queue.poppedPriority
7343 if (currentDistance !== distances[currentNode]) {
7344 continue
7345 }
7346 for (let i = outgoing.rowOffsets[currentNode]; i < outgoing.rowOffsets[currentNode + 1]; i++) {
7347 const edge = outgoing.edgeIndices[i]
7348 const neighbor = outgoing.columnIndices[i]
7349 const nextDistance = currentDistance + weights[edge]
7350 if (weights[edge] !== Infinity && !Number.isFinite(nextDistance)) {
7351 throw new GraphError({ message: "All shortest paths distance calculation exceeded the finite number range" })
7352 }
7353 const known = distances[neighbor]
7354 if (nextDistance < known) {
7355 distances[neighbor] = nextDistance
7356 previous[neighbor] = [{ node: currentNode, edge }]
7357 denseMinHeapPush(queue, neighbor, nextDistance, sequence++)
7358 } else if (nextDistance === known && nextDistance !== Infinity) {
7359 ;(previous[neighbor] ??= []).push({ node: currentNode, edge })
7360 }
7361 }
7362 }
7363
7364 const distance = distances[target]
7365 if (distance === Infinity) {
7366 return
7367 }
7368 if (source === target) {
7369 yield { path: [config.source], edges: [], distance: 0, costs: [] }
7370 return
7371 }
7372 const reversePath = [target]
7373 const reverseEdges: Array<number> = []
7374 const visited = new Uint8Array(cache.nodeIds.length)
7375 visited[target] = 1
7376 const stack: Array<{ readonly node: number; position: number }> = [{ node: target, position: 0 }]
7377 let emitted = 0
7378
7379 const backtrack = () => {
7380 const frame = stack.pop()!
7381 if (stack.length > 0) {
7382 visited[frame.node] = 0
7383 reversePath.pop()
7384 reverseEdges.pop()
7385 }
7386 }
7387
7388 while (stack.length > 0 && emitted < limit) {
7389 const frame = stack[stack.length - 1]
7390 if (frame.node === source) {
7391 emitted++
7392 yield {
7393 path: reversePath.map((node) => cache.nodeIds[node]).reverse(),
7394 edges: reverseEdges.map((edge) => edgeIds[edge]).reverse(),
7395 distance,
7396 costs: reverseEdges.map((edge) => graphEdges[edge].data as E).reverse()
7397 }
7398 backtrack()
7399 continue
7400 }
7401 const predecessors = previous[frame.node] ?? []
7402 if (frame.position >= predecessors.length) {
7403 backtrack()
7404 continue
7405 }
7406 const predecessor = predecessors[frame.position++]
7407 if (visited[predecessor.node] !== 0) {
7408 continue
7409 }
7410 visited[predecessor.node] = 1
7411 reversePath.push(predecessor.node)
7412 reverseEdges.push(predecessor.edge)
7413 stack.push({ node: predecessor.node, position: 0 })
7414 }
7415 })
7416})
7417
7418/**
7419 * Represents an iterable wrapper used by graph traversal and listing APIs.
7420 *
7421 * **Details**
7422 *
7423 * A `Walker` yields `[index, data]` pairs lazily and can be viewed as just the
7424 * indices, just the values, or mapped entries with `indices`, `values`,
7425 * `entries`, and `visit`.
7426 *
7427 * **Example** (Working with node walkers)
7428 *
7429 * ```ts import.meta.vitest
7430 * import { Graph } from "effect"
7431 *
7432 * const graph = Graph.directed<string, number>((mutable) => {
7433 * const a = Graph.addNode(mutable, "A")
7434 * const b = Graph.addNode(mutable, "B")
7435 * Graph.addEdge(mutable, a, b, 1)
7436 * })
7437 *
7438 * // Both traversal and element iterators return NodeWalker
7439 * const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { start: [0] })
7440 * const allNodes: Graph.NodeWalker<string> = Graph.nodes(graph)
7441 *
7442 * // Common interface for working with node iterables
7443 * function processNodes<N>(nodeIterable: Graph.NodeWalker<N>): Array<number> {
7444 * return Array.from(Graph.indices(nodeIterable))
7445 * }
7446 *
7447 * // Access node data using values() or entries()
7448 * Array.from(Graph.values(dfsNodes)) // => ["A", "B"]
7449 * Array.from(Graph.entries(allNodes)) // => [[0, "A"], [1, "B"]]
7450 * ```
7451 *
7452 * @category models
7453 * @since 3.18.0
7454 */
7455export class Walker<T, N> implements Iterable<[T, N]> {
7456 // @ts-ignore
7457 readonly [Symbol.iterator]: () => Iterator<[T, N]>
7458
7459 /**
7460 * Lazily maps each walker entry with the provided function.
7461 *
7462 * **Details**
7463 *
7464 * The function receives the index and data and runs as the returned iterable
7465 * is consumed.
7466 *
7467 * **Example** (Visiting walker elements)
7468 *
7469 * ```ts import.meta.vitest
7470 * import { Graph } from "effect"
7471 *
7472 * const graph = Graph.directed<string, number>((mutable) => {
7473 * const a = Graph.addNode(mutable, "A")
7474 * const b = Graph.addNode(mutable, "B")
7475 * Graph.addEdge(mutable, a, b, 1)
7476 * })
7477 *
7478 * const dfs = Graph.dfs(graph, { start: [0] })
7479 *
7480 * // Map to just the node data
7481 * Array.from(dfs.visit((index, data) => data)) // => ["A", "B"]
7482 *
7483 * // Map to custom objects
7484 * Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) // => [{ id: 0, name: "A" }, { id: 1, name: "B" }]
7485 * ```
7486 *
7487 * @since 4.0.0
7488 */
7489 readonly visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
7490
7491 constructor(
7492 visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
7493 ) {
7494 this.visit = visit
7495 this[Symbol.iterator] = () => visit((index, data) => [index, data] as [T, N])[Symbol.iterator]()
7496 }
7497}
7498
7499const makeCsrNodeWalker = <N, E, T extends Kind>(
7500 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7501 makeIterator: <U>(cache: csr.Csr, f: (index: NodeIndex, data: N) => U) => Iterator<U>
7502): Walker<NodeIndex, N> => {
7503 return new Walker((f) => ({
7504 // Capture CSR at iterator creation so invalidation cannot change an in-flight mutable traversal.
7505 [Symbol.iterator]: () =>
7506 makeIterator(csr.get(graph), (index, data) => withMutationGuard(graph, () => f(index, data)))
7507 }))
7508}
7509
7510const traversalStarts = <N, E, T extends Kind>(
7511 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7512 start: ReadonlyArray<NodeIndex> | undefined
7513): Array<NodeIndex> => {
7514 if (start === undefined) {
7515 return []
7516 }
7517 for (const nodeIndex of start) {
7518 if (!hasNode(graph, nodeIndex)) {
7519 throw missingNode(nodeIndex)
7520 }
7521 }
7522 return Array.from(start)
7523}
7524
7525const traversalStartPositions = (cache: csr.Csr, start: ReadonlyArray<NodeIndex>): Array<number> => {
7526 const positions = new Array<number>(start.length)
7527 for (let i = 0; i < start.length; i++) {
7528 const position = csr.getNodeIndex(cache, start[i])
7529 if (position === undefined) {
7530 throw missingNode(start[i])
7531 }
7532 positions[i] = position
7533 }
7534 return positions
7535}
7536
7537/**
7538 * Type alias for node iteration using Walker.
7539 * NodeWalker is represented as Walker<NodeIndex, N>.
7540 *
7541 * **When to use**
7542 *
7543 * Use as the shared node walker type returned by graph traversal and node
7544 * listing APIs.
7545 *
7546 * @see {@link Walker} for the generic lazy iterator wrapper
7547 * @see {@link EdgeWalker} for edge iterators
7548 *
7549 * @category models
7550 * @since 3.18.0
7551 */
7552export type NodeWalker<N> = Walker<NodeIndex, N>
7553
7554/**
7555 * Type alias for edge iteration using Walker.
7556 * EdgeWalker is represented as Walker<EdgeIndex, Edge<E>>.
7557 *
7558 * **When to use**
7559 *
7560 * Use to type helpers or parameters that consume edge iterators returned by
7561 * `Graph` APIs, where each item is keyed by an `EdgeIndex` and carries the
7562 * full `Edge`.
7563 *
7564 * @see {@link Walker} for the generic lazy iterator wrapper
7565 * @see {@link NodeWalker} for node iterators
7566 * @see {@link edges} for creating edge walkers
7567 *
7568 * @category models
7569 * @since 3.18.0
7570 */
7571export type EdgeWalker<E> = Walker<EdgeIndex, Edge<E>>
7572
7573/**
7574 * Returns an iterator over the indices in the walker.
7575 *
7576 * **Example** (Iterating walker indices)
7577 *
7578 * ```ts import.meta.vitest
7579 * import { Graph } from "effect"
7580 *
7581 * const graph = Graph.directed<string, number>((mutable) => {
7582 * const a = Graph.addNode(mutable, "A")
7583 * const b = Graph.addNode(mutable, "B")
7584 * Graph.addEdge(mutable, a, b, 1)
7585 * })
7586 *
7587 * const dfs = Graph.dfs(graph, { start: [0] })
7588 * Array.from(Graph.indices(dfs)) // => [0, 1]
7589 * ```
7590 *
7591 * @category iterators
7592 * @since 3.18.0
7593 */
7594export const indices = <T, N>(walker: Walker<T, N>): Iterable<T> => walker.visit((index, _) => index)
7595
7596/**
7597 * Returns an iterator over the values (data) in the walker.
7598 *
7599 * **Example** (Iterating walker values)
7600 *
7601 * ```ts import.meta.vitest
7602 * import { Graph } from "effect"
7603 *
7604 * const graph = Graph.directed<string, number>((mutable) => {
7605 * const a = Graph.addNode(mutable, "A")
7606 * const b = Graph.addNode(mutable, "B")
7607 * Graph.addEdge(mutable, a, b, 1)
7608 * })
7609 *
7610 * const dfs = Graph.dfs(graph, { start: [0] })
7611 * Array.from(Graph.values(dfs)) // => ["A", "B"]
7612 * ```
7613 *
7614 * @category iterators
7615 * @since 3.18.0
7616 */
7617export const values = <T, N>(walker: Walker<T, N>): Iterable<N> => walker.visit((_, data) => data)
7618
7619/**
7620 * Returns an iterator over [index, data] entries in the walker.
7621 *
7622 * **Example** (Iterating walker entries)
7623 *
7624 * ```ts import.meta.vitest
7625 * import { Graph } from "effect"
7626 *
7627 * const graph = Graph.directed<string, number>((mutable) => {
7628 * const a = Graph.addNode(mutable, "A")
7629 * const b = Graph.addNode(mutable, "B")
7630 * Graph.addEdge(mutable, a, b, 1)
7631 * })
7632 *
7633 * const dfs = Graph.dfs(graph, { start: [0] })
7634 * Array.from(Graph.entries(dfs)) // => [[0, "A"], [1, "B"]]
7635 * ```
7636 *
7637 * @category iterators
7638 * @since 3.18.0
7639 */
7640export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
7641 walker.visit((index, data) => [index, data] as [T, N])
7642
7643/**
7644 * Configuration for DFS, BFS, and postorder graph traversals.
7645 *
7646 * **When to use**
7647 *
7648 * Use to configure the starting node indices and edge-following direction for
7649 * lazy graph traversals.
7650 *
7651 * **Details**
7652 *
7653 * `start` supplies the node indices where traversal begins. If it is omitted,
7654 * the iterator is empty. Distinct starts are prioritized in supplied order and
7655 * duplicates are ignored. `direction` chooses whether traversal follows outgoing
7656 * edges, incoming edges, or ignores edge direction. `radius` limits traversal
7657 * by edge distance from the nearest start node and accepts non-negative integers
7658 * or `Infinity`; omitting it means unbounded traversal.
7659 *
7660 * **Gotchas**
7661 *
7662 * Traversal creation validates and copies `start`, and throws a `GraphError`
7663 * when a start node does not exist or `radius` is invalid. Each fresh iterator
7664 * revalidates those starts against the graph snapshot it captures. Later
7665 * mutations are not observed by an active iterator.
7666 *
7667 * @category configuration
7668 * @since 3.18.0
7669 */
7670export interface SearchConfig {
7671 readonly start?: Array<NodeIndex>
7672 readonly direction?: TraversalDirection
7673 readonly radius?: number
7674}
7675
7676/**
7677 * Creates a lazy depth-first traversal iterator from the configured start
7678 * nodes.
7679 *
7680 * **When to use**
7681 *
7682 * Use when exploring one branch deeply before visiting sibling branches.
7683 *
7684 * **Details**
7685 *
7686 * If no start nodes are supplied, the iterator is empty. The `direction` option
7687 * chooses whether to follow outgoing or incoming edges. The `radius` option
7688 * limits traversal by edge distance from the start nodes. It accepts
7689 * non-negative integers and `Infinity`; omitting it means unbounded traversal.
7690 *
7691 * **Gotchas**
7692 *
7693 * An invalid radius or missing start node throws a `GraphError`. Traversing a
7694 * mutable graph captures a snapshot when iteration begins; later mutations are
7695 * not observed by that iterator.
7696 *
7697 * **Example** (Traversing depth-first)
7698 *
7699 * ```ts import.meta.vitest
7700 * import { Graph } from "effect"
7701 *
7702 * const graph = Graph.directed<string, number>((mutable) => {
7703 * const a = Graph.addNode(mutable, "A")
7704 * const b = Graph.addNode(mutable, "B")
7705 * const c = Graph.addNode(mutable, "C")
7706 * Graph.addEdge(mutable, a, b, 1)
7707 * Graph.addEdge(mutable, b, c, 1)
7708 * })
7709 *
7710 * // Start from a specific node
7711 * Array.from(Graph.indices(Graph.dfs(graph, { start: [0] }))) // => [0, 1, 2]
7712 *
7713 * Array.from(Graph.indices(Graph.dfs(graph))) // => []
7714 * ```
7715 *
7716 * @see {@link bfs} for traversal in increasing hop distance
7717 * @see {@link dfsPostOrder} for emitting descendants before ancestors
7718 * @category iterators
7719 * @since 3.18.0
7720 */
7721export const dfs: {
7722 (
7723 config?: SearchConfig
7724 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => NodeWalker<N>
7725 <N, E, T extends Kind = "directed">(
7726 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7727 config?: SearchConfig
7728 ): NodeWalker<N>
7729} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
7730 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7731 config: SearchConfig = {}
7732): NodeWalker<N> => {
7733 const radius = traversalRadius(config.radius, Infinity)
7734 const start = traversalStarts(graph, config.start)
7735 const direction = config.direction ?? "outgoing"
7736
7737 return makeCsrNodeWalker(graph, (cache, f) => {
7738 const startPositions = traversalStartPositions(cache, start)
7739 const view = csr.getAdjacencies(cache, direction)
7740 const yielded = new Uint8Array(cache.nodeIds.length)
7741 const stack: Array<number> = []
7742
7743 if (radius === Infinity) {
7744 // Reverse row order before pushing so LIFO traversal observes canonical adjacency order.
7745 for (let i = startPositions.length - 1; i >= 0; i--) {
7746 stack.push(startPositions[i])
7747 }
7748
7749 const pushNeighbors = (targets: Uint32Array, offsets: Uint32Array, current: number) => {
7750 for (let i = offsets[current + 1] - 1; i >= offsets[current]; i--) {
7751 const neighbor = targets[i]
7752 if (yielded[neighbor] === 0) {
7753 stack.push(neighbor)
7754 }
7755 }
7756 }
7757
7758 return {
7759 next() {
7760 while (stack.length > 0) {
7761 const current = stack.pop()!
7762 if (yielded[current] !== 0) {
7763 continue
7764 }
7765
7766 if (view.secondary !== undefined) {
7767 pushNeighbors(view.secondary.columnIndices, view.secondary.rowOffsets, current)
7768 }
7769 pushNeighbors(view.primary.columnIndices, view.primary.rowOffsets, current)
7770 yielded[current] = 1
7771
7772 return { done: false, value: f(cache.nodeIds[current], cache.nodeData[current] as N) }
7773 }
7774
7775 return { done: true, value: undefined } as const
7776 }
7777 }
7778 }
7779
7780 // Radius is shortest edge distance, so determine membership with BFS before imposing DFS order.
7781 const reached = new Uint8Array(cache.nodeIds.length)
7782 const queue = new Uint32Array(cache.nodeIds.length)
7783 const depths = new Uint32Array(cache.nodeIds.length)
7784 let head = 0
7785 let tail = 0
7786
7787 for (const position of startPositions) {
7788 if (reached[position] === 0) {
7789 reached[position] = 1
7790 queue[tail++] = position
7791 }
7792 }
7793
7794 const enqueue = (targets: Uint32Array, offsets: Uint32Array, current: number, depth: number) => {
7795 for (let i = offsets[current]; i < offsets[current + 1]; i++) {
7796 const neighbor = targets[i]
7797 if (reached[neighbor] === 0) {
7798 reached[neighbor] = 1
7799 queue[tail] = neighbor
7800 depths[tail++] = depth + 1
7801 }
7802 }
7803 }
7804
7805 while (head < tail) {
7806 const current = queue[head]
7807 const depth = depths[head++]
7808 if (depth < radius) {
7809 enqueue(view.primary.columnIndices, view.primary.rowOffsets, current, depth)
7810 if (view.secondary !== undefined) {
7811 enqueue(view.secondary.columnIndices, view.secondary.rowOffsets, current, depth)
7812 }
7813 }
7814 }
7815
7816 for (let i = startPositions.length - 1; i >= 0; i--) {
7817 stack.push(startPositions[i])
7818 }
7819
7820 const pushNeighbors = (targets: Uint32Array, offsets: Uint32Array, current: number) => {
7821 for (let i = offsets[current + 1] - 1; i >= offsets[current]; i--) {
7822 const neighbor = targets[i]
7823 if (reached[neighbor] !== 0 && yielded[neighbor] === 0) {
7824 stack.push(neighbor)
7825 }
7826 }
7827 }
7828
7829 return {
7830 next() {
7831 while (stack.length > 0) {
7832 const current = stack.pop()!
7833 if (yielded[current] !== 0) {
7834 continue
7835 }
7836
7837 if (view.secondary !== undefined) {
7838 pushNeighbors(view.secondary.columnIndices, view.secondary.rowOffsets, current)
7839 }
7840 pushNeighbors(view.primary.columnIndices, view.primary.rowOffsets, current)
7841 yielded[current] = 1
7842
7843 return { done: false, value: f(cache.nodeIds[current], cache.nodeData[current] as N) }
7844 }
7845
7846 return { done: true, value: undefined } as const
7847 }
7848 }
7849 })
7850})
7851
7852/**
7853 * Creates a lazy breadth-first traversal iterator from the configured start
7854 * nodes.
7855 *
7856 * **When to use**
7857 *
7858 * Use when visiting nodes in increasing unweighted distance from the start
7859 * nodes.
7860 *
7861 * **Details**
7862 *
7863 * If no start nodes are supplied, the iterator is empty. The `direction` option
7864 * chooses whether to follow outgoing or incoming edges. The `radius` option
7865 * limits traversal by edge distance from the start nodes. It accepts
7866 * non-negative integers and `Infinity`; omitting it means unbounded traversal.
7867 *
7868 * **Gotchas**
7869 *
7870 * An invalid radius or missing start node throws a `GraphError`. Traversing a
7871 * mutable graph captures a snapshot when iteration begins; later mutations are
7872 * not observed by that iterator.
7873 *
7874 * **Example** (Traversing breadth-first)
7875 *
7876 * ```ts import.meta.vitest
7877 * import { Graph } from "effect"
7878 *
7879 * const graph = Graph.directed<string, number>((mutable) => {
7880 * const a = Graph.addNode(mutable, "A")
7881 * const b = Graph.addNode(mutable, "B")
7882 * const c = Graph.addNode(mutable, "C")
7883 * Graph.addEdge(mutable, a, b, 1)
7884 * Graph.addEdge(mutable, b, c, 1)
7885 * })
7886 *
7887 * // Start from a specific node
7888 * Array.from(Graph.indices(Graph.bfs(graph, { start: [0] }))) // => [0, 1, 2]
7889 *
7890 * Array.from(Graph.indices(Graph.bfs(graph))) // => []
7891 * ```
7892 *
7893 * @see {@link dfs} for branch-first traversal
7894 * @see {@link unweightedDistances} for collecting hop counts
7895 * @category iterators
7896 * @since 3.18.0
7897 */
7898export const bfs: {
7899 (
7900 config?: SearchConfig
7901 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => NodeWalker<N>
7902 <N, E, T extends Kind = "directed">(
7903 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7904 config?: SearchConfig
7905 ): NodeWalker<N>
7906} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
7907 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
7908 config: SearchConfig = {}
7909): NodeWalker<N> => {
7910 const radius = traversalRadius(config.radius, Infinity)
7911 const start = traversalStarts(graph, config.start)
7912 const direction = config.direction ?? "outgoing"
7913
7914 return makeCsrNodeWalker(graph, (cache, f) => {
7915 const startPositions = traversalStartPositions(cache, start)
7916 const view = csr.getAdjacencies(cache, direction)
7917 const discovered = new Uint8Array(cache.nodeIds.length)
7918 // Each compact node enters the queue once, so a fixed-size typed array is sufficient.
7919 const queue = new Uint32Array(cache.nodeIds.length)
7920 let head = 0
7921 let tail = 0
7922
7923 for (const position of startPositions) {
7924 if (discovered[position] === 0) {
7925 discovered[position] = 1
7926 queue[tail++] = position
7927 }
7928 }
7929
7930 const enqueue = (targets: Uint32Array, from: number, to: number) => {
7931 for (let i = from; i < to; i++) {
7932 const neighbor = targets[i]
7933 if (discovered[neighbor] === 0) {
7934 discovered[neighbor] = 1
7935 queue[tail++] = neighbor
7936 }
7937 }
7938 }
7939
7940 if (radius === Infinity) {
7941 return {
7942 next() {
7943 if (head >= tail) {
7944 return { done: true, value: undefined } as const
7945 }
7946
7947 const current = queue[head++]
7948 enqueue(view.primary.columnIndices, view.primary.rowOffsets[current], view.primary.rowOffsets[current + 1])
7949 if (view.secondary !== undefined) {
7950 enqueue(
7951 view.secondary.columnIndices,
7952 view.secondary.rowOffsets[current],
7953 view.secondary.rowOffsets[current + 1]
7954 )
7955 }
7956
7957 return { done: false, value: f(cache.nodeIds[current], cache.nodeData[current] as N) }
7958 }
7959 }
7960 }
7961
7962 const depths = new Uint32Array(cache.nodeIds.length)
7963 const enqueueBounded = (targets: Uint32Array, from: number, to: number, depth: number) => {
7964 for (let i = from; i < to; i++) {
7965 const neighbor = targets[i]
7966 if (discovered[neighbor] === 0) {
7967 discovered[neighbor] = 1
7968 queue[tail] = neighbor
7969 depths[tail++] = depth + 1
7970 }
7971 }
7972 }
7973
7974 return {
7975 next() {
7976 if (head >= tail) {
7977 return { done: true, value: undefined } as const
7978 }
7979
7980 const current = queue[head]
7981 const depth = depths[head++]
7982
7983 if (depth < radius) {
7984 enqueueBounded(
7985 view.primary.columnIndices,
7986 view.primary.rowOffsets[current],
7987 view.primary.rowOffsets[current + 1],
7988 depth
7989 )
7990 if (view.secondary !== undefined) {
7991 enqueueBounded(
7992 view.secondary.columnIndices,
7993 view.secondary.rowOffsets[current],
7994 view.secondary.rowOffsets[current + 1],
7995 depth
7996 )
7997 }
7998 }
7999
8000 return { done: false, value: f(cache.nodeIds[current], cache.nodeData[current] as N) }
8001 }
8002 }
8003 })
8004})
8005
8006/**
8007 * Configuration for the topological sort iterator.
8008 *
8009 * **When to use**
8010 *
8011 * Use to prioritize specific zero in-degree nodes in a topological sort.
8012 *
8013 * **Details**
8014 *
8015 * `initials` optionally supplies zero in-degree node indices used as
8016 * prioritized initial queue entries. Topological sorting still includes the
8017 * other zero in-degree nodes and produces a complete topological order.
8018 *
8019 * **Gotchas**
8020 *
8021 * Throws a `GraphError` when any initial node has incoming edges.
8022 *
8023 * @category configuration
8024 * @since 3.18.0
8025 */
8026export interface TopoConfig {
8027 readonly initials?: Array<NodeIndex>
8028}
8029
8030/**
8031 * Creates a lazy topological-order iterator for a directed acyclic graph.
8032 *
8033 * **When to use**
8034 *
8035 * Use when processing dependencies so every predecessor is emitted before the
8036 * nodes that depend on it.
8037 *
8038 * **Details**
8039 *
8040 * The iterator uses Kahn's algorithm. Multiple valid orders may exist;
8041 * `initials` prioritizes eligible zero in-degree nodes without excluding other
8042 * nodes.
8043 *
8044 * **Gotchas**
8045 *
8046 * Undirected or cyclic graphs, missing initial nodes, and initial nodes with
8047 * incoming edges throw a `GraphError`. Traversing a mutable graph captures a
8048 * snapshot when iteration begins; later mutations are not observed.
8049 *
8050 * **Example** (Sorting topologically)
8051 *
8052 * ```ts import.meta.vitest
8053 * import { Graph } from "effect"
8054 *
8055 * const graph = Graph.directed<string, number>((mutable) => {
8056 * const a = Graph.addNode(mutable, "A")
8057 * const b = Graph.addNode(mutable, "B")
8058 * const c = Graph.addNode(mutable, "C")
8059 * Graph.addEdge(mutable, a, b, 1)
8060 * Graph.addEdge(mutable, b, c, 1)
8061 * })
8062 *
8063 * Array.from(Graph.indices(Graph.topo(graph))) // => [0, 1, 2]
8064 * ```
8065 *
8066 * @see {@link isAcyclic} for checking the required graph property
8067 * @category iterators
8068 * @since 3.18.0
8069 */
8070export const topo: {
8071 (
8072 config?: TopoConfig
8073 ): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => NodeWalker<N>
8074 <N, E>(
8075 graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
8076 config?: TopoConfig
8077 ): NodeWalker<N>
8078} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
8079 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
8080 config: TopoConfig = {}
8081): NodeWalker<N> => {
8082 if (graph.type === "undirected") {
8083 throw new GraphError({ message: "Cannot perform topological sort on undirected graph" })
8084 }
8085
8086 // Check if graph is acyclic first
8087 if (!isAcyclic(graph)) {
8088 throw new GraphError({ message: "Cannot perform topological sort on cyclic graph" })
8089 }
8090
8091 const initials = Array.from(config.initials ?? [])
8092
8093 // Validate that all initial nodes exist
8094 for (const nodeIndex of initials) {
8095 if (!hasNode(graph, nodeIndex)) {
8096 throw missingNode(nodeIndex)
8097 }
8098 }
8099
8100 return makeCsrNodeWalker(graph, (cache, f) => {
8101 const outgoing = csr.getOutgoing(cache)
8102 const incoming = csr.getIncoming(cache)
8103 // CSR row lengths are the initial in-degrees used by Kahn's algorithm.
8104 const inDegree = new Uint32Array(cache.nodeIds.length)
8105 const remaining = new Uint8Array(cache.nodeIds.length)
8106 const initialSet = new Uint8Array(cache.nodeIds.length)
8107 const queue: Array<number> = []
8108 let remainingCount = cache.nodeIds.length
8109 let head = 0
8110 remaining.fill(1)
8111
8112 for (let node = 0; node < cache.nodeIds.length; node++) {
8113 inDegree[node] = incoming.rowOffsets[node + 1] - incoming.rowOffsets[node]
8114 }
8115 for (const initial of initials) {
8116 const node = csr.getNodeIndex(cache, initial)
8117 if (node === undefined) {
8118 throw missingNode(initial)
8119 }
8120 if (inDegree[node] !== 0) {
8121 throw new GraphError({ message: `Initial node ${initial} has incoming edges` })
8122 }
8123 initialSet[node] = 1
8124 queue.push(node)
8125 }
8126 for (let node = 0; node < cache.nodeIds.length; node++) {
8127 if (inDegree[node] === 0 && initialSet[node] === 0) {
8128 queue.push(node)
8129 }
8130 }
8131
8132 return {
8133 next() {
8134 while (head < queue.length) {
8135 const current = queue[head++]
8136 if (remaining[current] === 0) {
8137 continue
8138 }
8139 remaining[current] = 0
8140 remainingCount--
8141
8142 for (let i = outgoing.rowOffsets[current]; i < outgoing.rowOffsets[current + 1]; i++) {
8143 const neighbor = outgoing.columnIndices[i]
8144 if (remaining[neighbor] !== 0) {
8145 const degree = --inDegree[neighbor]
8146 if (degree === 0) {
8147 queue.push(neighbor)
8148 }
8149 }
8150 }
8151
8152 return { done: false, value: f(cache.nodeIds[current], cache.nodeData[current] as N) }
8153 }
8154
8155 if (remainingCount > 0) {
8156 throw new GraphError({ message: "Cannot perform topological sort on cyclic graph" })
8157 }
8158 return { done: true, value: undefined } as const
8159 }
8160 }
8161 })
8162})
8163
8164/**
8165 * Creates a lazy depth-first postorder traversal iterator from the configured
8166 * start nodes.
8167 *
8168 * **When to use**
8169 *
8170 * Use when reachable descendants must be emitted before the nodes that lead to
8171 * them.
8172 *
8173 * **Details**
8174 *
8175 * Nodes are emitted after their reachable descendants have been processed. If
8176 * no start nodes are supplied, the iterator is empty. The `direction` option
8177 * chooses whether to follow outgoing or incoming edges. The `radius` option
8178 * limits traversal by edge distance from the start nodes. It accepts
8179 * non-negative integers and `Infinity`; omitting it means unbounded traversal.
8180 * With a finite `radius`, a bounded breadth-first pass first determines
8181 * shortest-distance membership before nodes are emitted in postorder.
8182 *
8183 * **Gotchas**
8184 *
8185 * Invalid radii and missing start nodes throw a `GraphError`. Traversing a
8186 * mutable graph captures a snapshot when iteration begins; later mutations are
8187 * not observed by that iterator.
8188 *
8189 * **Example** (Traversing in postorder)
8190 *
8191 * ```ts import.meta.vitest
8192 * import { Graph } from "effect"
8193 *
8194 * const graph = Graph.directed<string, number>((mutable) => {
8195 * const root = Graph.addNode(mutable, "root")
8196 * const child1 = Graph.addNode(mutable, "child1")
8197 * const child2 = Graph.addNode(mutable, "child2")
8198 * Graph.addEdge(mutable, root, child1, 1)
8199 * Graph.addEdge(mutable, root, child2, 1)
8200 * })
8201 *
8202 * // Postorder: children before parents
8203 * Array.from(Graph.indices(Graph.dfsPostOrder(graph, { start: [0] }))) // => [1, 2, 0]
8204 * ```
8205 *
8206 * @see {@link dfs} for emitting nodes when first visited
8207 * @category iterators
8208 * @since 3.18.0
8209 */
8210export const dfsPostOrder: {
8211 (
8212 config?: SearchConfig
8213 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => NodeWalker<N>
8214 <N, E, T extends Kind = "directed">(
8215 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
8216 config?: SearchConfig
8217 ): NodeWalker<N>
8218} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
8219 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
8220 config: SearchConfig = {}
8221): NodeWalker<N> => {
8222 const radius = traversalRadius(config.radius, Infinity)
8223 const start = traversalStarts(graph, config.start)
8224 const direction = config.direction ?? "outgoing"
8225
8226 return makeCsrNodeWalker(graph, (cache, f) => {
8227 const startPositions = traversalStartPositions(cache, start)
8228 const view = csr.getAdjacencies(cache, direction)
8229 let reached: Uint8Array | undefined
8230 if (radius !== Infinity) {
8231 // Radius is shortest edge distance, so determine membership with BFS before imposing postorder.
8232 const boundedReached = new Uint8Array(cache.nodeIds.length)
8233 const queue = new Uint32Array(cache.nodeIds.length)
8234 const depths = new Uint32Array(cache.nodeIds.length)
8235 let head = 0
8236 let tail = 0
8237
8238 for (const position of startPositions) {
8239 if (boundedReached[position] === 0) {
8240 boundedReached[position] = 1
8241 queue[tail++] = position
8242 }
8243 }
8244
8245 const enqueue = (targets: Uint32Array, offsets: Uint32Array, current: number, depth: number) => {
8246 for (let i = offsets[current]; i < offsets[current + 1]; i++) {
8247 const neighbor = targets[i]
8248 if (boundedReached[neighbor] === 0) {
8249 boundedReached[neighbor] = 1
8250 queue[tail] = neighbor
8251 depths[tail++] = depth + 1
8252 }
8253 }
8254 }
8255
8256 while (head < tail) {
8257 const current = queue[head]
8258 const depth = depths[head++]
8259 if (depth < radius) {
8260 enqueue(view.primary.columnIndices, view.primary.rowOffsets, current, depth)
8261 if (view.secondary !== undefined) {
8262 enqueue(view.secondary.columnIndices, view.secondary.rowOffsets, current, depth)
8263 }
8264 }
8265 }
8266 reached = boundedReached
8267 }
8268
8269 const stack: Array<number> = []
8270 const primaryPositions: Array<number> = []
8271 const secondaryPositions: Array<number> = []
8272 const discovered = new Uint8Array(cache.nodeIds.length)
8273
8274 const push = (node: number) => {
8275 if ((reached === undefined || reached[node] !== 0) && discovered[node] === 0) {
8276 discovered[node] = 1
8277 stack.push(node)
8278 primaryPositions.push(view.primary.rowOffsets[node])
8279 secondaryPositions.push(view.secondary?.rowOffsets[node] ?? 0)
8280 }
8281 }
8282
8283 let startPosition = 0
8284
8285 return {
8286 next() {
8287 while (true) {
8288 while (stack.length === 0 && startPosition < startPositions.length) {
8289 push(startPositions[startPosition++])
8290 }
8291 if (stack.length === 0) {
8292 return { done: true, value: undefined } as const
8293 }
8294 const index = stack.length - 1
8295 const current = stack[index]
8296 const primaryPosition = primaryPositions[index]
8297 if (primaryPosition < view.primary.rowOffsets[current + 1]) {
8298 primaryPositions[index] = primaryPosition + 1
8299 push(view.primary.columnIndices[primaryPosition])
8300 continue
8301 }
8302 if (view.secondary !== undefined) {
8303 const secondaryPosition = secondaryPositions[index]
8304 if (secondaryPosition < view.secondary.rowOffsets[current + 1]) {
8305 secondaryPositions[index] = secondaryPosition + 1
8306 push(view.secondary.columnIndices[secondaryPosition])
8307 continue
8308 }
8309 }
8310
8311 stack.pop()
8312 primaryPositions.pop()
8313 secondaryPositions.pop()
8314 return { done: false, value: f(cache.nodeIds[current], cache.nodeData[current] as N) }
8315 }
8316 }
8317 }
8318 })
8319})
8320
8321/**
8322 * Creates a walker over all node index and payload entries in the graph.
8323 *
8324 * **Details**
8325 *
8326 * Entries follow graph node order and include all nodes regardless of
8327 * connectivity. Use `indices` or `values` to project one side of each entry.
8328 *
8329 * **Gotchas**
8330 *
8331 * Mutable graphs are not snapshotted; mutations may affect the remaining
8332 * iteration.
8333 *
8334 * **Example** (Iterating all nodes)
8335 *
8336 * ```ts import.meta.vitest
8337 * import { Graph } from "effect"
8338 *
8339 * const graph = Graph.directed<string, number>((mutable) => {
8340 * const a = Graph.addNode(mutable, "A")
8341 * const b = Graph.addNode(mutable, "B")
8342 * const c = Graph.addNode(mutable, "C")
8343 * Graph.addEdge(mutable, a, b, 1)
8344 * })
8345 *
8346 * Array.from(Graph.indices(Graph.nodes(graph))) // => [0, 1, 2]
8347 * ```
8348 *
8349 * @category iterators
8350 * @since 3.18.0
8351 */
8352export const nodes = <N, E, T extends Kind = "directed">(
8353 graph: Graph<N, E, T> | MutableGraph<N, E, T>
8354): NodeWalker<N> =>
8355 new Walker((f) => ({
8356 [Symbol.iterator]() {
8357 const nodeMap = internal.toImpl(graph).nodes
8358 const iterator = nodeMap.entries()
8359
8360 return {
8361 next() {
8362 const result = iterator.next()
8363 if (result.done) {
8364 return { done: true, value: undefined }
8365 }
8366 const [nodeIndex, nodeData] = result.value
8367 return { done: false, value: withMutationGuard(graph, () => f(nodeIndex, nodeData)) }
8368 }
8369 }
8370 }
8371 }))
8372
8373/**
8374 * Creates a walker over all edge index and edge entries in the graph.
8375 *
8376 * **Details**
8377 *
8378 * Entries follow graph edge order and include all edges regardless of
8379 * connectivity. Use `indices` or `values` to project one side of each entry.
8380 *
8381 * **Gotchas**
8382 *
8383 * Mutable graphs are not snapshotted; mutations may affect the remaining
8384 * iteration.
8385 *
8386 * **Example** (Iterating all edges)
8387 *
8388 * ```ts import.meta.vitest
8389 * import { Graph } from "effect"
8390 *
8391 * const graph = Graph.directed<string, number>((mutable) => {
8392 * const a = Graph.addNode(mutable, "A")
8393 * const b = Graph.addNode(mutable, "B")
8394 * const c = Graph.addNode(mutable, "C")
8395 * Graph.addEdge(mutable, a, b, 1)
8396 * Graph.addEdge(mutable, b, c, 2)
8397 * })
8398 *
8399 * Array.from(Graph.indices(Graph.edges(graph))) // => [0, 1]
8400 * ```
8401 *
8402 * @category iterators
8403 * @since 3.18.0
8404 */
8405export const edges = <N, E, T extends Kind = "directed">(
8406 graph: Graph<N, E, T> | MutableGraph<N, E, T>
8407): EdgeWalker<E> =>
8408 new Walker((f) => ({
8409 [Symbol.iterator]() {
8410 const edgeMap = internal.toImpl(graph).edges
8411 const iterator = edgeMap.entries()
8412
8413 return {
8414 next() {
8415 const result = iterator.next()
8416 if (result.done) {
8417 return { done: true, value: undefined }
8418 }
8419 const [edgeIndex, edgeData] = result.value
8420 return { done: false, value: withMutationGuard(graph, () => f(edgeIndex, copyEdge(edgeData))) }
8421 }
8422 }
8423 }
8424 }))
8425
8426/**
8427 * Configuration for selecting external nodes.
8428 *
8429 * **When to use**
8430 *
8431 * Use to configure how `externals` identifies graph boundary nodes when you
8432 * need sinks with no outgoing edges or sources with no incoming edges.
8433 *
8434 * **Details**
8435 *
8436 * `direction` chooses which missing edge direction makes a node external:
8437 * `"outgoing"` selects nodes with no outgoing edges, and `"incoming"` selects
8438 * nodes with no incoming edges. If omitted, `direction` defaults to
8439 * `"outgoing"`.
8440 *
8441 * @category configuration
8442 * @since 3.18.0
8443 */
8444export interface ExternalsConfig {
8445 readonly direction?: Direction
8446}
8447
8448/**
8449 * Creates an iterator over external nodes (nodes without edges in the specified direction).
8450 *
8451 * **When to use**
8452 *
8453 * Use when locating sources, sinks, or isolated boundary nodes.
8454 *
8455 * **Details**
8456 *
8457 * External nodes have no outgoing edges (`direction: "outgoing"`) or no
8458 * incoming edges (`direction: "incoming"`).
8459 *
8460 * **Gotchas**
8461 *
8462 * For undirected graphs, incoming and outgoing adjacency are equivalent, so
8463 * only isolated nodes are external. Mutable graphs are not snapshotted;
8464 * mutations may affect the remaining iteration.
8465 *
8466 * **Example** (Iterating external nodes)
8467 *
8468 * ```ts import.meta.vitest
8469 * import { Graph } from "effect"
8470 *
8471 * const graph = Graph.directed<string, number>((mutable) => {
8472 * const source = Graph.addNode(mutable, "source") // 0 - no incoming
8473 * const middle = Graph.addNode(mutable, "middle") // 1 - has both
8474 * const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing
8475 * const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges
8476 *
8477 * Graph.addEdge(mutable, source, middle, 1)
8478 * Graph.addEdge(mutable, middle, sink, 2)
8479 * })
8480 *
8481 * // Nodes with no outgoing edges (sinks + isolated)
8482 * Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) // => [2, 3]
8483 *
8484 * // Nodes with no incoming edges (sources + isolated)
8485 * Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) // => [0, 3]
8486 * ```
8487 *
8488 * @category iterators
8489 * @since 3.18.0
8490 */
8491export const externals: {
8492 (
8493 config?: ExternalsConfig
8494 ): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => NodeWalker<N>
8495 <N, E, T extends Kind = "directed">(
8496 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
8497 config?: ExternalsConfig
8498 ): NodeWalker<N>
8499} = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
8500 graph: Graph<N, E, T> | MutableGraph<N, E, T>,
8501 config: ExternalsConfig = {}
8502): NodeWalker<N> => {
8503 const direction = config.direction ?? "outgoing"
8504
8505 return new Walker((f) => ({
8506 [Symbol.iterator]: () => {
8507 const impl = internal.toImpl(graph)
8508 const nodeMap = impl.nodes
8509 const adjacencyMap = direction === "incoming"
8510 ? impl.reverseAdjacency
8511 : impl.adjacency
8512
8513 const nodeIterator = nodeMap.entries()
8514
8515 const nextMapped = () => {
8516 let current = nodeIterator.next()
8517 while (!current.done) {
8518 const [nodeIndex, nodeData] = current.value
8519 const adjacencyList = adjacencyMap.get(nodeIndex)
8520
8521 // Node is external if it has no edges in the specified direction
8522 if (adjacencyList === undefined || adjacencyList.length === 0) {
8523 return { done: false, value: withMutationGuard(graph, () => f(nodeIndex, nodeData)) }
8524 }
8525 current = nodeIterator.next()
8526 }
8527
8528 return { done: true, value: undefined } as const
8529 }
8530
8531 return { next: nextMapped }
8532 }
8533 }))
8534})