effect 3.17.13 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Graph/package.json +6 -0
  2. package/dist/cjs/Context.js +11 -3
  3. package/dist/cjs/Context.js.map +1 -1
  4. package/dist/cjs/Effect.js.map +1 -1
  5. package/dist/cjs/ExecutionPlan.js +4 -4
  6. package/dist/cjs/Graph.js +2964 -0
  7. package/dist/cjs/Graph.js.map +1 -0
  8. package/dist/cjs/Predicate.js +2 -3
  9. package/dist/cjs/Predicate.js.map +1 -1
  10. package/dist/cjs/Tracer.js.map +1 -1
  11. package/dist/cjs/index.js +4 -2
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/internal/core-effect.js +1 -1
  14. package/dist/cjs/internal/core-effect.js.map +1 -1
  15. package/dist/cjs/internal/effect/circular.js +12 -2
  16. package/dist/cjs/internal/effect/circular.js.map +1 -1
  17. package/dist/cjs/internal/metric/hook.js +6 -1
  18. package/dist/cjs/internal/metric/hook.js.map +1 -1
  19. package/dist/cjs/internal/version.js +1 -1
  20. package/dist/cjs/internal/version.js.map +1 -1
  21. package/dist/dts/Context.d.ts +27 -3
  22. package/dist/dts/Context.d.ts.map +1 -1
  23. package/dist/dts/Effect.d.ts +4 -0
  24. package/dist/dts/Effect.d.ts.map +1 -1
  25. package/dist/dts/ExecutionPlan.d.ts +8 -8
  26. package/dist/dts/Graph.d.ts +1652 -0
  27. package/dist/dts/Graph.d.ts.map +1 -0
  28. package/dist/dts/Predicate.d.ts +2 -3
  29. package/dist/dts/Predicate.d.ts.map +1 -1
  30. package/dist/dts/Tracer.d.ts +1 -1
  31. package/dist/dts/Tracer.d.ts.map +1 -1
  32. package/dist/dts/index.d.ts +5 -0
  33. package/dist/dts/index.d.ts.map +1 -1
  34. package/dist/dts/internal/core-effect.d.ts.map +1 -1
  35. package/dist/esm/Context.js +10 -2
  36. package/dist/esm/Context.js.map +1 -1
  37. package/dist/esm/Effect.js.map +1 -1
  38. package/dist/esm/ExecutionPlan.js +4 -4
  39. package/dist/esm/Graph.js +2905 -0
  40. package/dist/esm/Graph.js.map +1 -0
  41. package/dist/esm/Predicate.js +2 -3
  42. package/dist/esm/Predicate.js.map +1 -1
  43. package/dist/esm/Tracer.js.map +1 -1
  44. package/dist/esm/index.js +5 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/internal/core-effect.js +1 -1
  47. package/dist/esm/internal/core-effect.js.map +1 -1
  48. package/dist/esm/internal/effect/circular.js +12 -2
  49. package/dist/esm/internal/effect/circular.js.map +1 -1
  50. package/dist/esm/internal/metric/hook.js +6 -1
  51. package/dist/esm/internal/metric/hook.js.map +1 -1
  52. package/dist/esm/internal/version.js +1 -1
  53. package/dist/esm/internal/version.js.map +1 -1
  54. package/package.json +9 -1
  55. package/src/Context.ts +28 -3
  56. package/src/Effect.ts +5 -0
  57. package/src/ExecutionPlan.ts +8 -8
  58. package/src/Graph.ts +3564 -0
  59. package/src/Predicate.ts +2 -3
  60. package/src/Tracer.ts +2 -1
  61. package/src/index.ts +6 -0
  62. package/src/internal/core-effect.ts +2 -1
  63. package/src/internal/effect/circular.ts +31 -17
  64. package/src/internal/metric/hook.ts +6 -1
  65. package/src/internal/version.ts +1 -1
package/src/Graph.ts ADDED
@@ -0,0 +1,3564 @@
1
+ /**
2
+ * @experimental
3
+ * @since 3.18.0
4
+ */
5
+
6
+ import * as Data from "./Data.js"
7
+ import * as Equal from "./Equal.js"
8
+ import { dual } from "./Function.js"
9
+ import * as Hash from "./Hash.js"
10
+ import type { Inspectable } from "./Inspectable.js"
11
+ import { format, NodeInspectSymbol } from "./Inspectable.js"
12
+ import * as Option from "./Option.js"
13
+ import type { Pipeable } from "./Pipeable.js"
14
+ import { pipeArguments } from "./Pipeable.js"
15
+ import type { Mutable } from "./Types.js"
16
+
17
+ /**
18
+ * Safely get a value from a Map, returning an Option.
19
+ * Uses explicit key presence check with map.has() for better safety.
20
+ * @internal
21
+ */
22
+ const getMapSafe = <K, V>(map: Map<K, V>, key: K): Option.Option<V> => {
23
+ if (map.has(key)) {
24
+ return Option.some(map.get(key)!)
25
+ }
26
+ return Option.none()
27
+ }
28
+
29
+ /**
30
+ * Unique identifier for Graph instances.
31
+ *
32
+ * @since 3.18.0
33
+ * @category symbol
34
+ */
35
+ export const TypeId: "~effect/Graph" = "~effect/Graph" as const
36
+
37
+ /**
38
+ * Type identifier for Graph instances.
39
+ *
40
+ * @since 3.18.0
41
+ * @category symbol
42
+ */
43
+ export type TypeId = typeof TypeId
44
+
45
+ /**
46
+ * Node index for node identification using plain numbers.
47
+ *
48
+ * @since 3.18.0
49
+ * @category models
50
+ */
51
+ export type NodeIndex = number
52
+
53
+ /**
54
+ * Edge index for edge identification using plain numbers.
55
+ *
56
+ * @since 3.18.0
57
+ * @category models
58
+ */
59
+ export type EdgeIndex = number
60
+
61
+ /**
62
+ * Edge data containing source, target, and user data.
63
+ *
64
+ * @since 3.18.0
65
+ * @category models
66
+ */
67
+ export class Edge<E> extends Data.Class<{
68
+ readonly source: NodeIndex
69
+ readonly target: NodeIndex
70
+ readonly data: E
71
+ }> {}
72
+
73
+ /**
74
+ * Graph type for distinguishing directed and undirected graphs.
75
+ *
76
+ * @since 3.18.0
77
+ * @category models
78
+ */
79
+ export type Kind = "directed" | "undirected"
80
+
81
+ /**
82
+ * Graph prototype interface.
83
+ *
84
+ * @since 3.18.0
85
+ * @category models
86
+ */
87
+ export interface Proto<out N, out E> extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable {
88
+ readonly [TypeId]: TypeId
89
+ readonly nodes: Map<NodeIndex, N>
90
+ readonly edges: Map<EdgeIndex, Edge<E>>
91
+ readonly adjacency: Map<NodeIndex, Array<EdgeIndex>>
92
+ readonly reverseAdjacency: Map<NodeIndex, Array<EdgeIndex>>
93
+ nextNodeIndex: NodeIndex
94
+ nextEdgeIndex: EdgeIndex
95
+ isAcyclic: Option.Option<boolean>
96
+ }
97
+
98
+ /**
99
+ * Immutable graph interface.
100
+ *
101
+ * @since 3.18.0
102
+ * @category models
103
+ */
104
+ export interface Graph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
105
+ readonly type: T
106
+ readonly mutable: false
107
+ }
108
+
109
+ /**
110
+ * Mutable graph interface.
111
+ *
112
+ * @since 3.18.0
113
+ * @category models
114
+ */
115
+ export interface MutableGraph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
116
+ readonly type: T
117
+ readonly mutable: true
118
+ }
119
+
120
+ /**
121
+ * Directed graph type alias.
122
+ *
123
+ * @since 3.18.0
124
+ * @category models
125
+ */
126
+ export type DirectedGraph<N, E> = Graph<N, E, "directed">
127
+
128
+ /**
129
+ * Undirected graph type alias.
130
+ *
131
+ * @since 3.18.0
132
+ * @category models
133
+ */
134
+ export type UndirectedGraph<N, E> = Graph<N, E, "undirected">
135
+
136
+ /**
137
+ * Mutable directed graph type alias.
138
+ *
139
+ * @since 3.18.0
140
+ * @category models
141
+ */
142
+ export type MutableDirectedGraph<N, E> = MutableGraph<N, E, "directed">
143
+
144
+ /**
145
+ * Mutable undirected graph type alias.
146
+ *
147
+ * @since 3.18.0
148
+ * @category models
149
+ */
150
+ export type MutableUndirectedGraph<N, E> = MutableGraph<N, E, "undirected">
151
+
152
+ // =============================================================================
153
+ // Proto Objects
154
+ // =============================================================================
155
+
156
+ /** @internal */
157
+ const ProtoGraph = {
158
+ [TypeId]: TypeId,
159
+ [Symbol.iterator](this: Graph<any, any>) {
160
+ return this.nodes[Symbol.iterator]()
161
+ },
162
+ [NodeInspectSymbol](this: Graph<any, any>) {
163
+ return this.toJSON()
164
+ },
165
+ [Equal.symbol](this: Graph<any, any>, that: Equal.Equal): boolean {
166
+ if (isGraph(that)) {
167
+ if (
168
+ this.nodes.size !== that.nodes.size ||
169
+ this.edges.size !== that.edges.size ||
170
+ this.type !== that.type
171
+ ) {
172
+ return false
173
+ }
174
+ // Compare nodes
175
+ for (const [nodeIndex, nodeData] of this.nodes) {
176
+ if (!that.nodes.has(nodeIndex)) {
177
+ return false
178
+ }
179
+ const otherNodeData = that.nodes.get(nodeIndex)!
180
+ if (!Equal.equals(nodeData, otherNodeData)) {
181
+ return false
182
+ }
183
+ }
184
+ // Compare edges
185
+ for (const [edgeIndex, edgeData] of this.edges) {
186
+ if (!that.edges.has(edgeIndex)) {
187
+ return false
188
+ }
189
+ const otherEdge = that.edges.get(edgeIndex)!
190
+ if (!Equal.equals(edgeData, otherEdge)) {
191
+ return false
192
+ }
193
+ }
194
+ return true
195
+ }
196
+ return false
197
+ },
198
+ [Hash.symbol](this: Graph<any, any>): number {
199
+ let hash = Hash.string("Graph")
200
+ hash = hash ^ Hash.string(this.type)
201
+ hash = hash ^ Hash.number(this.nodes.size)
202
+ hash = hash ^ Hash.number(this.edges.size)
203
+ for (const [nodeIndex, nodeData] of this.nodes) {
204
+ hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData))
205
+ }
206
+ for (const [edgeIndex, edgeData] of this.edges) {
207
+ hash = hash ^ (Hash.hash(edgeIndex) + Hash.hash(edgeData))
208
+ }
209
+ return hash
210
+ },
211
+ toJSON(this: Graph<any, any>) {
212
+ return {
213
+ _id: "Graph",
214
+ nodeCount: this.nodes.size,
215
+ edgeCount: this.edges.size,
216
+ type: this.type
217
+ }
218
+ },
219
+ toString(this: Graph<any, any>) {
220
+ return format(this)
221
+ },
222
+ pipe() {
223
+ return pipeArguments(this, arguments)
224
+ }
225
+ }
226
+
227
+ // =============================================================================
228
+ // Constructors
229
+ // =============================================================================
230
+
231
+ /** @internal */
232
+ export const isGraph = (u: unknown): u is Graph<unknown, unknown> => typeof u === "object" && u !== null && TypeId in u
233
+
234
+ /**
235
+ * Creates a directed graph, optionally with initial mutations.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * import { Graph } from "effect"
240
+ *
241
+ * // Directed graph with initial nodes and edges
242
+ * const graph = Graph.directed<string, string>((mutable) => {
243
+ * const a = Graph.addNode(mutable, "A")
244
+ * const b = Graph.addNode(mutable, "B")
245
+ * const c = Graph.addNode(mutable, "C")
246
+ * Graph.addEdge(mutable, a, b, "A->B")
247
+ * Graph.addEdge(mutable, b, c, "B->C")
248
+ * })
249
+ * ```
250
+ *
251
+ * @since 3.18.0
252
+ * @category constructors
253
+ */
254
+ export const directed = <N, E>(mutate?: (mutable: MutableDirectedGraph<N, E>) => void): DirectedGraph<N, E> => {
255
+ const graph: Mutable<DirectedGraph<N, E>> = Object.create(ProtoGraph)
256
+ graph.type = "directed"
257
+ graph.nodes = new Map()
258
+ graph.edges = new Map()
259
+ graph.adjacency = new Map()
260
+ graph.reverseAdjacency = new Map()
261
+ graph.nextNodeIndex = 0
262
+ graph.nextEdgeIndex = 0
263
+ graph.isAcyclic = Option.some(true)
264
+ graph.mutable = false
265
+
266
+ if (mutate) {
267
+ const mutable = beginMutation(graph as DirectedGraph<N, E>)
268
+ mutate(mutable as MutableDirectedGraph<N, E>)
269
+ return endMutation(mutable)
270
+ }
271
+
272
+ return graph
273
+ }
274
+
275
+ /**
276
+ * Creates an undirected graph, optionally with initial mutations.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * import { Graph } from "effect"
281
+ *
282
+ * // Undirected graph with initial nodes and edges
283
+ * const graph = Graph.undirected<string, string>((mutable) => {
284
+ * const a = Graph.addNode(mutable, "A")
285
+ * const b = Graph.addNode(mutable, "B")
286
+ * const c = Graph.addNode(mutable, "C")
287
+ * Graph.addEdge(mutable, a, b, "A-B")
288
+ * Graph.addEdge(mutable, b, c, "B-C")
289
+ * })
290
+ * ```
291
+ *
292
+ * @since 3.18.0
293
+ * @category constructors
294
+ */
295
+ export const undirected = <N, E>(mutate?: (mutable: MutableUndirectedGraph<N, E>) => void): UndirectedGraph<N, E> => {
296
+ const graph: Mutable<UndirectedGraph<N, E>> = Object.create(ProtoGraph)
297
+ graph.type = "undirected"
298
+ graph.nodes = new Map()
299
+ graph.edges = new Map()
300
+ graph.adjacency = new Map()
301
+ graph.reverseAdjacency = new Map()
302
+ graph.nextNodeIndex = 0
303
+ graph.nextEdgeIndex = 0
304
+ graph.isAcyclic = Option.some(true)
305
+ graph.mutable = false
306
+
307
+ if (mutate) {
308
+ const mutable = beginMutation(graph)
309
+ mutate(mutable as MutableUndirectedGraph<N, E>)
310
+ return endMutation(mutable)
311
+ }
312
+
313
+ return graph
314
+ }
315
+
316
+ // =============================================================================
317
+ // Scoped Mutable API
318
+ // =============================================================================
319
+
320
+ /**
321
+ * Creates a mutable scope for safe graph mutations by copying the data structure.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * import { Graph } from "effect"
326
+ *
327
+ * const graph = Graph.directed<string, number>()
328
+ * const mutable = Graph.beginMutation(graph)
329
+ * // Now mutable can be safely modified without affecting original graph
330
+ * ```
331
+ *
332
+ * @since 3.18.0
333
+ * @category mutations
334
+ */
335
+ export const beginMutation = <N, E, T extends Kind = "directed">(
336
+ graph: Graph<N, E, T>
337
+ ): MutableGraph<N, E, T> => {
338
+ // Copy adjacency maps with deep cloned arrays
339
+ const adjacency = new Map<NodeIndex, Array<EdgeIndex>>()
340
+ const reverseAdjacency = new Map<NodeIndex, Array<EdgeIndex>>()
341
+
342
+ for (const [nodeIndex, edges] of graph.adjacency) {
343
+ adjacency.set(nodeIndex, [...edges])
344
+ }
345
+
346
+ for (const [nodeIndex, edges] of graph.reverseAdjacency) {
347
+ reverseAdjacency.set(nodeIndex, [...edges])
348
+ }
349
+
350
+ const mutable: Mutable<MutableGraph<N, E, T>> = Object.create(ProtoGraph)
351
+ mutable.type = graph.type
352
+ mutable.nodes = new Map(graph.nodes)
353
+ mutable.edges = new Map(graph.edges)
354
+ mutable.adjacency = adjacency
355
+ mutable.reverseAdjacency = reverseAdjacency
356
+ mutable.nextNodeIndex = graph.nextNodeIndex
357
+ mutable.nextEdgeIndex = graph.nextEdgeIndex
358
+ mutable.isAcyclic = graph.isAcyclic
359
+ mutable.mutable = true
360
+
361
+ return mutable
362
+ }
363
+
364
+ /**
365
+ * Converts a mutable graph back to an immutable graph, ending the mutation scope.
366
+ *
367
+ * @example
368
+ * ```ts
369
+ * import { Graph } from "effect"
370
+ *
371
+ * const graph = Graph.directed<string, number>()
372
+ * const mutable = Graph.beginMutation(graph)
373
+ * // ... perform mutations on mutable ...
374
+ * const newGraph = Graph.endMutation(mutable)
375
+ * ```
376
+ *
377
+ * @since 3.18.0
378
+ * @category mutations
379
+ */
380
+ export const endMutation = <N, E, T extends Kind = "directed">(
381
+ mutable: MutableGraph<N, E, T>
382
+ ): Graph<N, E, T> => {
383
+ const graph: Mutable<Graph<N, E, T>> = Object.create(ProtoGraph)
384
+ graph.type = mutable.type
385
+ graph.nodes = new Map(mutable.nodes)
386
+ graph.edges = new Map(mutable.edges)
387
+ graph.adjacency = mutable.adjacency
388
+ graph.reverseAdjacency = mutable.reverseAdjacency
389
+ graph.nextNodeIndex = mutable.nextNodeIndex
390
+ graph.nextEdgeIndex = mutable.nextEdgeIndex
391
+ graph.isAcyclic = mutable.isAcyclic
392
+ graph.mutable = false
393
+
394
+ return graph
395
+ }
396
+
397
+ /**
398
+ * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * import { Graph } from "effect"
403
+ *
404
+ * const graph = Graph.directed<string, number>()
405
+ * const newGraph = Graph.mutate(graph, (mutable) => {
406
+ * // Safe mutations go here
407
+ * // mutable gets automatically converted back to immutable
408
+ * })
409
+ * ```
410
+ *
411
+ * @since 3.18.0
412
+ * @category mutations
413
+ */
414
+ export const mutate: {
415
+ /**
416
+ * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * import { Graph } from "effect"
421
+ *
422
+ * const graph = Graph.directed<string, number>()
423
+ * const newGraph = Graph.mutate(graph, (mutable) => {
424
+ * // Safe mutations go here
425
+ * // mutable gets automatically converted back to immutable
426
+ * })
427
+ * ```
428
+ *
429
+ * @since 3.18.0
430
+ * @category mutations
431
+ */
432
+ <N, E, T extends Kind = "directed">(f: (mutable: MutableGraph<N, E, T>) => void): (graph: Graph<N, E, T>) => Graph<N, E, T>
433
+ /**
434
+ * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * import { Graph } from "effect"
439
+ *
440
+ * const graph = Graph.directed<string, number>()
441
+ * const newGraph = Graph.mutate(graph, (mutable) => {
442
+ * // Safe mutations go here
443
+ * // mutable gets automatically converted back to immutable
444
+ * })
445
+ * ```
446
+ *
447
+ * @since 3.18.0
448
+ * @category mutations
449
+ */
450
+ <N, E, T extends Kind = "directed">(graph: Graph<N, E, T>, f: (mutable: MutableGraph<N, E, T>) => void): Graph<N, E, T>
451
+ } = dual(2, <N, E, T extends Kind = "directed">(
452
+ graph: Graph<N, E, T>,
453
+ f: (mutable: MutableGraph<N, E, T>) => void
454
+ ): Graph<N, E, T> => {
455
+ const mutable = beginMutation(graph)
456
+ f(mutable)
457
+ return endMutation(mutable)
458
+ })
459
+
460
+ // =============================================================================
461
+ // Basic Node Operations
462
+ // =============================================================================
463
+
464
+ /**
465
+ * Adds a new node to a mutable graph and returns its index.
466
+ *
467
+ * @example
468
+ * ```ts
469
+ * import { Graph } from "effect"
470
+ *
471
+ * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
472
+ * const nodeA = Graph.addNode(mutable, "Node A")
473
+ * const nodeB = Graph.addNode(mutable, "Node B")
474
+ * console.log(nodeA) // NodeIndex with value 0
475
+ * console.log(nodeB) // NodeIndex with value 1
476
+ * })
477
+ * ```
478
+ *
479
+ * @since 3.18.0
480
+ * @category mutations
481
+ */
482
+ export const addNode = <N, E, T extends Kind = "directed">(
483
+ mutable: MutableGraph<N, E, T>,
484
+ data: N
485
+ ): NodeIndex => {
486
+ const nodeIndex = mutable.nextNodeIndex
487
+
488
+ // Add node data
489
+ mutable.nodes.set(nodeIndex, data)
490
+
491
+ // Initialize empty adjacency lists
492
+ mutable.adjacency.set(nodeIndex, [])
493
+ mutable.reverseAdjacency.set(nodeIndex, [])
494
+
495
+ // Update graph allocators
496
+ mutable.nextNodeIndex = mutable.nextNodeIndex + 1
497
+
498
+ return nodeIndex
499
+ }
500
+
501
+ /**
502
+ * Gets the data associated with a node index, if it exists.
503
+ *
504
+ * @example
505
+ * ```ts
506
+ * import { Graph, Option } from "effect"
507
+ *
508
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
509
+ * Graph.addNode(mutable, "Node A")
510
+ * })
511
+ *
512
+ * const nodeIndex = 0
513
+ * const nodeData = Graph.getNode(graph, nodeIndex)
514
+ *
515
+ * if (Option.isSome(nodeData)) {
516
+ * console.log(nodeData.value) // "Node A"
517
+ * }
518
+ * ```
519
+ *
520
+ * @since 3.18.0
521
+ * @category getters
522
+ */
523
+ export const getNode = <N, E, T extends Kind = "directed">(
524
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
525
+ nodeIndex: NodeIndex
526
+ ): Option.Option<N> => getMapSafe(graph.nodes, nodeIndex)
527
+
528
+ /**
529
+ * Checks if a node with the given index exists in the graph.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * import { Graph } from "effect"
534
+ *
535
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
536
+ * Graph.addNode(mutable, "Node A")
537
+ * })
538
+ *
539
+ * const nodeIndex = 0
540
+ * const exists = Graph.hasNode(graph, nodeIndex)
541
+ * console.log(exists) // true
542
+ *
543
+ * const nonExistentIndex = 999
544
+ * const notExists = Graph.hasNode(graph, nonExistentIndex)
545
+ * console.log(notExists) // false
546
+ * ```
547
+ *
548
+ * @since 3.18.0
549
+ * @category getters
550
+ */
551
+ export const hasNode = <N, E, T extends Kind = "directed">(
552
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
553
+ nodeIndex: NodeIndex
554
+ ): boolean => graph.nodes.has(nodeIndex)
555
+
556
+ /**
557
+ * Returns the number of nodes in the graph.
558
+ *
559
+ * @example
560
+ * ```ts
561
+ * import { Graph } from "effect"
562
+ *
563
+ * const emptyGraph = Graph.directed<string, number>()
564
+ * console.log(Graph.nodeCount(emptyGraph)) // 0
565
+ *
566
+ * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => {
567
+ * Graph.addNode(mutable, "Node A")
568
+ * Graph.addNode(mutable, "Node B")
569
+ * Graph.addNode(mutable, "Node C")
570
+ * })
571
+ *
572
+ * console.log(Graph.nodeCount(graphWithNodes)) // 3
573
+ * ```
574
+ *
575
+ * @since 3.18.0
576
+ * @category getters
577
+ */
578
+ export const nodeCount = <N, E, T extends Kind = "directed">(
579
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
580
+ ): number => graph.nodes.size
581
+
582
+ /**
583
+ * Finds the first node that matches the given predicate.
584
+ *
585
+ * @example
586
+ * ```ts
587
+ * import { Graph, Option } from "effect"
588
+ *
589
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
590
+ * Graph.addNode(mutable, "Node A")
591
+ * Graph.addNode(mutable, "Node B")
592
+ * Graph.addNode(mutable, "Node C")
593
+ * })
594
+ *
595
+ * const result = Graph.findNode(graph, (data) => data.startsWith("Node B"))
596
+ * console.log(result) // Option.some(1)
597
+ *
598
+ * const notFound = Graph.findNode(graph, (data) => data === "Node D")
599
+ * console.log(notFound) // Option.none()
600
+ * ```
601
+ *
602
+ * @since 3.18.0
603
+ * @category getters
604
+ */
605
+ export const findNode = <N, E, T extends Kind = "directed">(
606
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
607
+ predicate: (data: N) => boolean
608
+ ): Option.Option<NodeIndex> => {
609
+ for (const [index, data] of graph.nodes) {
610
+ if (predicate(data)) {
611
+ return Option.some(index)
612
+ }
613
+ }
614
+ return Option.none()
615
+ }
616
+
617
+ /**
618
+ * Finds all nodes that match the given predicate.
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * import { Graph } from "effect"
623
+ *
624
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
625
+ * Graph.addNode(mutable, "Start A")
626
+ * Graph.addNode(mutable, "Node B")
627
+ * Graph.addNode(mutable, "Start C")
628
+ * })
629
+ *
630
+ * const result = Graph.findNodes(graph, (data) => data.startsWith("Start"))
631
+ * console.log(result) // [0, 2]
632
+ *
633
+ * const empty = Graph.findNodes(graph, (data) => data === "Not Found")
634
+ * console.log(empty) // []
635
+ * ```
636
+ *
637
+ * @since 3.18.0
638
+ * @category getters
639
+ */
640
+ export const findNodes = <N, E, T extends Kind = "directed">(
641
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
642
+ predicate: (data: N) => boolean
643
+ ): Array<NodeIndex> => {
644
+ const results: Array<NodeIndex> = []
645
+ for (const [index, data] of graph.nodes) {
646
+ if (predicate(data)) {
647
+ results.push(index)
648
+ }
649
+ }
650
+ return results
651
+ }
652
+
653
+ /**
654
+ * Finds the first edge that matches the given predicate.
655
+ *
656
+ * @example
657
+ * ```ts
658
+ * import { Graph, Option } from "effect"
659
+ *
660
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
661
+ * const nodeA = Graph.addNode(mutable, "Node A")
662
+ * const nodeB = Graph.addNode(mutable, "Node B")
663
+ * const nodeC = Graph.addNode(mutable, "Node C")
664
+ * Graph.addEdge(mutable, nodeA, nodeB, 10)
665
+ * Graph.addEdge(mutable, nodeB, nodeC, 20)
666
+ * })
667
+ *
668
+ * const result = Graph.findEdge(graph, (data) => data > 15)
669
+ * console.log(result) // Option.some(1)
670
+ *
671
+ * const notFound = Graph.findEdge(graph, (data) => data > 100)
672
+ * console.log(notFound) // Option.none()
673
+ * ```
674
+ *
675
+ * @since 3.18.0
676
+ * @category getters
677
+ */
678
+ export const findEdge = <N, E, T extends Kind = "directed">(
679
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
680
+ predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
681
+ ): Option.Option<EdgeIndex> => {
682
+ for (const [edgeIndex, edgeData] of graph.edges) {
683
+ if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
684
+ return Option.some(edgeIndex)
685
+ }
686
+ }
687
+ return Option.none()
688
+ }
689
+
690
+ /**
691
+ * Finds all edges that match the given predicate.
692
+ *
693
+ * @example
694
+ * ```ts
695
+ * import { Graph } from "effect"
696
+ *
697
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
698
+ * const nodeA = Graph.addNode(mutable, "Node A")
699
+ * const nodeB = Graph.addNode(mutable, "Node B")
700
+ * const nodeC = Graph.addNode(mutable, "Node C")
701
+ * Graph.addEdge(mutable, nodeA, nodeB, 10)
702
+ * Graph.addEdge(mutable, nodeB, nodeC, 20)
703
+ * Graph.addEdge(mutable, nodeC, nodeA, 30)
704
+ * })
705
+ *
706
+ * const result = Graph.findEdges(graph, (data) => data >= 20)
707
+ * console.log(result) // [1, 2]
708
+ *
709
+ * const empty = Graph.findEdges(graph, (data) => data > 100)
710
+ * console.log(empty) // []
711
+ * ```
712
+ *
713
+ * @since 3.18.0
714
+ * @category getters
715
+ */
716
+ export const findEdges = <N, E, T extends Kind = "directed">(
717
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
718
+ predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
719
+ ): Array<EdgeIndex> => {
720
+ const results: Array<EdgeIndex> = []
721
+ for (const [edgeIndex, edgeData] of graph.edges) {
722
+ if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
723
+ results.push(edgeIndex)
724
+ }
725
+ }
726
+ return results
727
+ }
728
+
729
+ /**
730
+ * Updates a single node's data by applying a transformation function.
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * import { Graph } from "effect"
735
+ *
736
+ * const graph = Graph.directed<string, number>((mutable) => {
737
+ * Graph.addNode(mutable, "Node A")
738
+ * Graph.addNode(mutable, "Node B")
739
+ * Graph.updateNode(mutable, 0, (data) => data.toUpperCase())
740
+ * })
741
+ *
742
+ * const nodeData = Graph.getNode(graph, 0)
743
+ * console.log(nodeData) // Option.some("NODE A")
744
+ * ```
745
+ *
746
+ * @since 3.18.0
747
+ * @category transformations
748
+ */
749
+ export const updateNode = <N, E, T extends Kind = "directed">(
750
+ mutable: MutableGraph<N, E, T>,
751
+ index: NodeIndex,
752
+ f: (data: N) => N
753
+ ): void => {
754
+ if (!mutable.nodes.has(index)) {
755
+ return
756
+ }
757
+
758
+ const currentData = mutable.nodes.get(index)!
759
+ const newData = f(currentData)
760
+ mutable.nodes.set(index, newData)
761
+ }
762
+
763
+ /**
764
+ * Updates a single edge's data by applying a transformation function.
765
+ *
766
+ * @example
767
+ * ```ts
768
+ * import { Graph } from "effect"
769
+ *
770
+ * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
771
+ * const nodeA = Graph.addNode(mutable, "Node A")
772
+ * const nodeB = Graph.addNode(mutable, "Node B")
773
+ * const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10)
774
+ * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2)
775
+ * })
776
+ *
777
+ * const edgeData = Graph.getEdge(result, 0)
778
+ * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 })
779
+ * ```
780
+ *
781
+ * @since 3.18.0
782
+ * @category mutations
783
+ */
784
+ export const updateEdge = <N, E, T extends Kind = "directed">(
785
+ mutable: MutableGraph<N, E, T>,
786
+ edgeIndex: EdgeIndex,
787
+ f: (data: E) => E
788
+ ): void => {
789
+ if (!mutable.edges.has(edgeIndex)) {
790
+ return
791
+ }
792
+
793
+ const currentEdge = mutable.edges.get(edgeIndex)!
794
+ const newData = f(currentEdge.data)
795
+ mutable.edges.set(edgeIndex, {
796
+ ...currentEdge,
797
+ data: newData
798
+ })
799
+ }
800
+
801
+ /**
802
+ * Creates a new graph with transformed node data using the provided mapping function.
803
+ *
804
+ * @example
805
+ * ```ts
806
+ * import { Graph } from "effect"
807
+ *
808
+ * const graph = Graph.directed<string, number>((mutable) => {
809
+ * Graph.addNode(mutable, "node a")
810
+ * Graph.addNode(mutable, "node b")
811
+ * Graph.addNode(mutable, "node c")
812
+ * Graph.mapNodes(mutable, (data) => data.toUpperCase())
813
+ * })
814
+ *
815
+ * const nodeData = Graph.getNode(graph, 0)
816
+ * console.log(nodeData) // Option.some("NODE A")
817
+ * ```
818
+ *
819
+ * @since 3.18.0
820
+ * @category transformations
821
+ */
822
+ export const mapNodes = <N, E, T extends Kind = "directed">(
823
+ mutable: MutableGraph<N, E, T>,
824
+ f: (data: N) => N
825
+ ): void => {
826
+ // Transform existing node data in place
827
+ for (const [index, data] of mutable.nodes) {
828
+ const newData = f(data)
829
+ mutable.nodes.set(index, newData)
830
+ }
831
+ }
832
+
833
+ /**
834
+ * Transforms all edge data in a mutable graph using the provided mapping function.
835
+ *
836
+ * @example
837
+ * ```ts
838
+ * import { Graph } from "effect"
839
+ *
840
+ * const graph = Graph.directed<string, number>((mutable) => {
841
+ * const a = Graph.addNode(mutable, "A")
842
+ * const b = Graph.addNode(mutable, "B")
843
+ * const c = Graph.addNode(mutable, "C")
844
+ * Graph.addEdge(mutable, a, b, 10)
845
+ * Graph.addEdge(mutable, b, c, 20)
846
+ * Graph.mapEdges(mutable, (data) => data * 2)
847
+ * })
848
+ *
849
+ * const edgeData = Graph.getEdge(graph, 0)
850
+ * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 })
851
+ * ```
852
+ *
853
+ * @since 3.18.0
854
+ * @category transformations
855
+ */
856
+ export const mapEdges = <N, E, T extends Kind = "directed">(
857
+ mutable: MutableGraph<N, E, T>,
858
+ f: (data: E) => E
859
+ ): void => {
860
+ // Transform existing edge data in place
861
+ for (const [index, edgeData] of mutable.edges) {
862
+ const newData = f(edgeData.data)
863
+ mutable.edges.set(index, {
864
+ ...edgeData,
865
+ data: newData
866
+ })
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Reverses all edge directions in a mutable graph by swapping source and target nodes.
872
+ *
873
+ * @example
874
+ * ```ts
875
+ * import { Graph } from "effect"
876
+ *
877
+ * const graph = Graph.directed<string, number>((mutable) => {
878
+ * const a = Graph.addNode(mutable, "A")
879
+ * const b = Graph.addNode(mutable, "B")
880
+ * const c = Graph.addNode(mutable, "C")
881
+ * Graph.addEdge(mutable, a, b, 1) // A -> B
882
+ * Graph.addEdge(mutable, b, c, 2) // B -> C
883
+ * Graph.reverse(mutable) // Now B -> A, C -> B
884
+ * })
885
+ *
886
+ * const edge0 = Graph.getEdge(graph, 0)
887
+ * console.log(edge0) // Option.some({ source: 1, target: 0, data: 1 }) - B -> A
888
+ * ```
889
+ *
890
+ * @since 3.18.0
891
+ * @category transformations
892
+ */
893
+ export const reverse = <N, E, T extends Kind = "directed">(
894
+ mutable: MutableGraph<N, E, T>
895
+ ): void => {
896
+ // Reverse all edges by swapping source and target
897
+ for (const [index, edgeData] of mutable.edges) {
898
+ mutable.edges.set(index, {
899
+ source: edgeData.target,
900
+ target: edgeData.source,
901
+ data: edgeData.data
902
+ })
903
+ }
904
+
905
+ // Clear and rebuild adjacency lists with reversed directions
906
+ mutable.adjacency.clear()
907
+ mutable.reverseAdjacency.clear()
908
+
909
+ // Rebuild adjacency lists with reversed directions
910
+ for (const [edgeIndex, edgeData] of mutable.edges) {
911
+ // Add to forward adjacency (source -> target)
912
+ const sourceEdges = mutable.adjacency.get(edgeData.source) || []
913
+ sourceEdges.push(edgeIndex)
914
+ mutable.adjacency.set(edgeData.source, sourceEdges)
915
+
916
+ // Add to reverse adjacency (target <- source)
917
+ const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || []
918
+ targetEdges.push(edgeIndex)
919
+ mutable.reverseAdjacency.set(edgeData.target, targetEdges)
920
+ }
921
+
922
+ // Invalidate cycle flag since edge directions changed
923
+ mutable.isAcyclic = Option.none()
924
+ }
925
+
926
+ /**
927
+ * Filters and optionally transforms nodes in a mutable graph using a predicate function.
928
+ * Nodes that return Option.none are removed along with all their connected edges.
929
+ *
930
+ * @example
931
+ * ```ts
932
+ * import { Graph, Option } from "effect"
933
+ *
934
+ * const graph = Graph.directed<string, number>((mutable) => {
935
+ * const a = Graph.addNode(mutable, "active")
936
+ * const b = Graph.addNode(mutable, "inactive")
937
+ * const c = Graph.addNode(mutable, "active")
938
+ * Graph.addEdge(mutable, a, b, 1)
939
+ * Graph.addEdge(mutable, b, c, 2)
940
+ *
941
+ * // Keep only "active" nodes and transform to uppercase
942
+ * Graph.filterMapNodes(mutable, (data) =>
943
+ * data === "active" ? Option.some(data.toUpperCase()) : Option.none()
944
+ * )
945
+ * })
946
+ *
947
+ * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain)
948
+ * ```
949
+ *
950
+ * @since 3.18.0
951
+ * @category transformations
952
+ */
953
+ export const filterMapNodes = <N, E, T extends Kind = "directed">(
954
+ mutable: MutableGraph<N, E, T>,
955
+ f: (data: N) => Option.Option<N>
956
+ ): void => {
957
+ const nodesToRemove: Array<NodeIndex> = []
958
+
959
+ // First pass: identify nodes to remove and transform data for nodes to keep
960
+ for (const [index, data] of mutable.nodes) {
961
+ const result = f(data)
962
+ if (Option.isSome(result)) {
963
+ // Transform node data
964
+ mutable.nodes.set(index, result.value)
965
+ } else {
966
+ // Mark for removal
967
+ nodesToRemove.push(index)
968
+ }
969
+ }
970
+
971
+ // Second pass: remove filtered out nodes and their edges
972
+ for (const nodeIndex of nodesToRemove) {
973
+ removeNode(mutable, nodeIndex)
974
+ }
975
+ }
976
+
977
+ /**
978
+ * Filters and optionally transforms edges in a mutable graph using a predicate function.
979
+ * Edges that return Option.none are removed from the graph.
980
+ *
981
+ * @example
982
+ * ```ts
983
+ * import { Graph, Option } from "effect"
984
+ *
985
+ * const graph = Graph.directed<string, number>((mutable) => {
986
+ * const a = Graph.addNode(mutable, "A")
987
+ * const b = Graph.addNode(mutable, "B")
988
+ * const c = Graph.addNode(mutable, "C")
989
+ * Graph.addEdge(mutable, a, b, 5)
990
+ * Graph.addEdge(mutable, b, c, 15)
991
+ * Graph.addEdge(mutable, c, a, 25)
992
+ *
993
+ * // Keep only edges with weight >= 10 and double their weight
994
+ * Graph.filterMapEdges(mutable, (data) =>
995
+ * data >= 10 ? Option.some(data * 2) : Option.none()
996
+ * )
997
+ * })
998
+ *
999
+ * console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed)
1000
+ * ```
1001
+ *
1002
+ * @since 3.18.0
1003
+ * @category transformations
1004
+ */
1005
+ export const filterMapEdges = <N, E, T extends Kind = "directed">(
1006
+ mutable: MutableGraph<N, E, T>,
1007
+ f: (data: E) => Option.Option<E>
1008
+ ): void => {
1009
+ const edgesToRemove: Array<EdgeIndex> = []
1010
+
1011
+ // First pass: identify edges to remove and transform data for edges to keep
1012
+ for (const [index, edgeData] of mutable.edges) {
1013
+ const result = f(edgeData.data)
1014
+ if (Option.isSome(result)) {
1015
+ // Transform edge data
1016
+ mutable.edges.set(index, {
1017
+ ...edgeData,
1018
+ data: result.value
1019
+ })
1020
+ } else {
1021
+ // Mark for removal
1022
+ edgesToRemove.push(index)
1023
+ }
1024
+ }
1025
+
1026
+ // Second pass: remove filtered out edges
1027
+ for (const edgeIndex of edgesToRemove) {
1028
+ removeEdge(mutable, edgeIndex)
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * Filters nodes by removing those that don't match the predicate.
1034
+ * This function modifies the mutable graph in place.
1035
+ *
1036
+ * @example
1037
+ * ```ts
1038
+ * import { Graph } from "effect"
1039
+ *
1040
+ * const graph = Graph.directed<string, number>((mutable) => {
1041
+ * Graph.addNode(mutable, "active")
1042
+ * Graph.addNode(mutable, "inactive")
1043
+ * Graph.addNode(mutable, "pending")
1044
+ * Graph.addNode(mutable, "active")
1045
+ *
1046
+ * // Keep only "active" nodes
1047
+ * Graph.filterNodes(mutable, (data) => data === "active")
1048
+ * })
1049
+ *
1050
+ * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain)
1051
+ * ```
1052
+ *
1053
+ * @since 3.18.0
1054
+ * @category transformations
1055
+ */
1056
+ export const filterNodes = <N, E, T extends Kind = "directed">(
1057
+ mutable: MutableGraph<N, E, T>,
1058
+ predicate: (data: N) => boolean
1059
+ ): void => {
1060
+ const nodesToRemove: Array<NodeIndex> = []
1061
+
1062
+ // Identify nodes to remove
1063
+ for (const [index, data] of mutable.nodes) {
1064
+ if (!predicate(data)) {
1065
+ nodesToRemove.push(index)
1066
+ }
1067
+ }
1068
+
1069
+ // Remove filtered out nodes (this also removes connected edges)
1070
+ for (const nodeIndex of nodesToRemove) {
1071
+ removeNode(mutable, nodeIndex)
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Filters edges by removing those that don't match the predicate.
1077
+ * This function modifies the mutable graph in place.
1078
+ *
1079
+ * @example
1080
+ * ```ts
1081
+ * import { Graph } from "effect"
1082
+ *
1083
+ * const graph = Graph.directed<string, number>((mutable) => {
1084
+ * const a = Graph.addNode(mutable, "A")
1085
+ * const b = Graph.addNode(mutable, "B")
1086
+ * const c = Graph.addNode(mutable, "C")
1087
+ *
1088
+ * Graph.addEdge(mutable, a, b, 5)
1089
+ * Graph.addEdge(mutable, b, c, 15)
1090
+ * Graph.addEdge(mutable, c, a, 25)
1091
+ *
1092
+ * // Keep only edges with weight >= 10
1093
+ * Graph.filterEdges(mutable, (data) => data >= 10)
1094
+ * })
1095
+ *
1096
+ * console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed)
1097
+ * ```
1098
+ *
1099
+ * @since 3.18.0
1100
+ * @category transformations
1101
+ */
1102
+ export const filterEdges = <N, E, T extends Kind = "directed">(
1103
+ mutable: MutableGraph<N, E, T>,
1104
+ predicate: (data: E) => boolean
1105
+ ): void => {
1106
+ const edgesToRemove: Array<EdgeIndex> = []
1107
+
1108
+ // Identify edges to remove
1109
+ for (const [index, edgeData] of mutable.edges) {
1110
+ if (!predicate(edgeData.data)) {
1111
+ edgesToRemove.push(index)
1112
+ }
1113
+ }
1114
+
1115
+ // Remove filtered out edges
1116
+ for (const edgeIndex of edgesToRemove) {
1117
+ removeEdge(mutable, edgeIndex)
1118
+ }
1119
+ }
1120
+
1121
+ // =============================================================================
1122
+ // Cycle Flag Management (Internal)
1123
+ // =============================================================================
1124
+
1125
+ /** @internal */
1126
+ const invalidateCycleFlagOnRemoval = <N, E, T extends Kind = "directed">(
1127
+ mutable: MutableGraph<N, E, T>
1128
+ ): void => {
1129
+ // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs)
1130
+ // If already unknown (null) or acyclic (true), no need to change
1131
+ if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === false) {
1132
+ mutable.isAcyclic = Option.none()
1133
+ }
1134
+ }
1135
+
1136
+ /** @internal */
1137
+ const invalidateCycleFlagOnAddition = <N, E, T extends Kind = "directed">(
1138
+ mutable: MutableGraph<N, E, T>
1139
+ ): void => {
1140
+ // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs)
1141
+ // If already unknown (null) or cyclic (false), no need to change
1142
+ if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === true) {
1143
+ mutable.isAcyclic = Option.none()
1144
+ }
1145
+ }
1146
+
1147
+ // =============================================================================
1148
+ // Edge Operations
1149
+ // =============================================================================
1150
+
1151
+ /**
1152
+ * Adds a new edge to a mutable graph and returns its index.
1153
+ *
1154
+ * @example
1155
+ * ```ts
1156
+ * import { Graph } from "effect"
1157
+ *
1158
+ * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1159
+ * const nodeA = Graph.addNode(mutable, "Node A")
1160
+ * const nodeB = Graph.addNode(mutable, "Node B")
1161
+ * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42)
1162
+ * console.log(edge) // EdgeIndex with value 0
1163
+ * })
1164
+ * ```
1165
+ *
1166
+ * @since 3.18.0
1167
+ * @category mutations
1168
+ */
1169
+ export const addEdge = <N, E, T extends Kind = "directed">(
1170
+ mutable: MutableGraph<N, E, T>,
1171
+ source: NodeIndex,
1172
+ target: NodeIndex,
1173
+ data: E
1174
+ ): EdgeIndex => {
1175
+ // Validate that both nodes exist
1176
+ if (!mutable.nodes.has(source)) {
1177
+ throw new Error(`Source node ${source} does not exist`)
1178
+ }
1179
+ if (!mutable.nodes.has(target)) {
1180
+ throw new Error(`Target node ${target} does not exist`)
1181
+ }
1182
+
1183
+ const edgeIndex = mutable.nextEdgeIndex
1184
+
1185
+ // Create edge data
1186
+ const edgeData = new Edge({ source, target, data })
1187
+ mutable.edges.set(edgeIndex, edgeData)
1188
+
1189
+ // Update adjacency lists
1190
+ const sourceAdjacency = getMapSafe(mutable.adjacency, source)
1191
+ if (Option.isSome(sourceAdjacency)) {
1192
+ sourceAdjacency.value.push(edgeIndex)
1193
+ }
1194
+
1195
+ const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target)
1196
+ if (Option.isSome(targetReverseAdjacency)) {
1197
+ targetReverseAdjacency.value.push(edgeIndex)
1198
+ }
1199
+
1200
+ // For undirected graphs, add reverse connections
1201
+ if (mutable.type === "undirected") {
1202
+ const targetAdjacency = getMapSafe(mutable.adjacency, target)
1203
+ if (Option.isSome(targetAdjacency)) {
1204
+ targetAdjacency.value.push(edgeIndex)
1205
+ }
1206
+
1207
+ const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source)
1208
+ if (Option.isSome(sourceReverseAdjacency)) {
1209
+ sourceReverseAdjacency.value.push(edgeIndex)
1210
+ }
1211
+ }
1212
+
1213
+ // Update allocators
1214
+ mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1
1215
+
1216
+ // Only invalidate cycle flag if the graph was acyclic
1217
+ // Adding edges cannot remove cycles from cyclic graphs
1218
+ invalidateCycleFlagOnAddition(mutable)
1219
+
1220
+ return edgeIndex
1221
+ }
1222
+
1223
+ /**
1224
+ * Removes a node and all its incident edges from a mutable graph.
1225
+ *
1226
+ * @example
1227
+ * ```ts
1228
+ * import { Graph } from "effect"
1229
+ *
1230
+ * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1231
+ * const nodeA = Graph.addNode(mutable, "Node A")
1232
+ * const nodeB = Graph.addNode(mutable, "Node B")
1233
+ * Graph.addEdge(mutable, nodeA, nodeB, 42)
1234
+ *
1235
+ * // Remove nodeA and all edges connected to it
1236
+ * Graph.removeNode(mutable, nodeA)
1237
+ * })
1238
+ * ```
1239
+ *
1240
+ * @since 3.18.0
1241
+ * @category mutations
1242
+ */
1243
+ export const removeNode = <N, E, T extends Kind = "directed">(
1244
+ mutable: MutableGraph<N, E, T>,
1245
+ nodeIndex: NodeIndex
1246
+ ): void => {
1247
+ // Check if node exists
1248
+ if (!mutable.nodes.has(nodeIndex)) {
1249
+ return // Node doesn't exist, nothing to remove
1250
+ }
1251
+
1252
+ // Collect all incident edges for removal
1253
+ const edgesToRemove: Array<EdgeIndex> = []
1254
+
1255
+ // Get outgoing edges
1256
+ const outgoingEdges = getMapSafe(mutable.adjacency, nodeIndex)
1257
+ if (Option.isSome(outgoingEdges)) {
1258
+ for (const edge of outgoingEdges.value) {
1259
+ edgesToRemove.push(edge)
1260
+ }
1261
+ }
1262
+
1263
+ // Get incoming edges
1264
+ const incomingEdges = getMapSafe(mutable.reverseAdjacency, nodeIndex)
1265
+ if (Option.isSome(incomingEdges)) {
1266
+ for (const edge of incomingEdges.value) {
1267
+ edgesToRemove.push(edge)
1268
+ }
1269
+ }
1270
+
1271
+ // Remove all incident edges
1272
+ for (const edgeIndex of edgesToRemove) {
1273
+ removeEdgeInternal(mutable, edgeIndex)
1274
+ }
1275
+
1276
+ // Remove the node itself
1277
+ mutable.nodes.delete(nodeIndex)
1278
+ mutable.adjacency.delete(nodeIndex)
1279
+ mutable.reverseAdjacency.delete(nodeIndex)
1280
+
1281
+ // Only invalidate cycle flag if the graph wasn't already known to be acyclic
1282
+ // Removing nodes cannot introduce cycles in an acyclic graph
1283
+ invalidateCycleFlagOnRemoval(mutable)
1284
+ }
1285
+
1286
+ /**
1287
+ * Removes an edge from a mutable graph.
1288
+ *
1289
+ * @example
1290
+ * ```ts
1291
+ * import { Graph } from "effect"
1292
+ *
1293
+ * const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1294
+ * const nodeA = Graph.addNode(mutable, "Node A")
1295
+ * const nodeB = Graph.addNode(mutable, "Node B")
1296
+ * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42)
1297
+ *
1298
+ * // Remove the edge
1299
+ * Graph.removeEdge(mutable, edge)
1300
+ * })
1301
+ * ```
1302
+ *
1303
+ * @since 3.18.0
1304
+ * @category mutations
1305
+ */
1306
+ export const removeEdge = <N, E, T extends Kind = "directed">(
1307
+ mutable: MutableGraph<N, E, T>,
1308
+ edgeIndex: EdgeIndex
1309
+ ): void => {
1310
+ const wasRemoved = removeEdgeInternal(mutable, edgeIndex)
1311
+
1312
+ // Only invalidate cycle flag if an edge was actually removed
1313
+ // and only if the graph wasn't already known to be acyclic
1314
+ if (wasRemoved) {
1315
+ invalidateCycleFlagOnRemoval(mutable)
1316
+ }
1317
+ }
1318
+
1319
+ /** @internal */
1320
+ const removeEdgeInternal = <N, E, T extends Kind = "directed">(
1321
+ mutable: MutableGraph<N, E, T>,
1322
+ edgeIndex: EdgeIndex
1323
+ ): boolean => {
1324
+ // Get edge data
1325
+ const edge = getMapSafe(mutable.edges, edgeIndex)
1326
+ if (Option.isNone(edge)) {
1327
+ return false // Edge doesn't exist, no mutation occurred
1328
+ }
1329
+
1330
+ const { source, target } = edge.value
1331
+
1332
+ // Remove from adjacency lists
1333
+ const sourceAdjacency = getMapSafe(mutable.adjacency, source)
1334
+ if (Option.isSome(sourceAdjacency)) {
1335
+ const index = sourceAdjacency.value.indexOf(edgeIndex)
1336
+ if (index !== -1) {
1337
+ sourceAdjacency.value.splice(index, 1)
1338
+ }
1339
+ }
1340
+
1341
+ const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target)
1342
+ if (Option.isSome(targetReverseAdjacency)) {
1343
+ const index = targetReverseAdjacency.value.indexOf(edgeIndex)
1344
+ if (index !== -1) {
1345
+ targetReverseAdjacency.value.splice(index, 1)
1346
+ }
1347
+ }
1348
+
1349
+ // For undirected graphs, remove reverse connections
1350
+ if (mutable.type === "undirected") {
1351
+ const targetAdjacency = getMapSafe(mutable.adjacency, target)
1352
+ if (Option.isSome(targetAdjacency)) {
1353
+ const index = targetAdjacency.value.indexOf(edgeIndex)
1354
+ if (index !== -1) {
1355
+ targetAdjacency.value.splice(index, 1)
1356
+ }
1357
+ }
1358
+
1359
+ const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source)
1360
+ if (Option.isSome(sourceReverseAdjacency)) {
1361
+ const index = sourceReverseAdjacency.value.indexOf(edgeIndex)
1362
+ if (index !== -1) {
1363
+ sourceReverseAdjacency.value.splice(index, 1)
1364
+ }
1365
+ }
1366
+ }
1367
+
1368
+ // Remove edge data
1369
+ mutable.edges.delete(edgeIndex)
1370
+
1371
+ return true // Edge was successfully removed
1372
+ }
1373
+
1374
+ // =============================================================================
1375
+ // Edge Query Operations
1376
+ // =============================================================================
1377
+
1378
+ /**
1379
+ * Gets the edge data associated with an edge index, if it exists.
1380
+ *
1381
+ * @example
1382
+ * ```ts
1383
+ * import { Graph, Option } from "effect"
1384
+ *
1385
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1386
+ * const nodeA = Graph.addNode(mutable, "Node A")
1387
+ * const nodeB = Graph.addNode(mutable, "Node B")
1388
+ * Graph.addEdge(mutable, nodeA, nodeB, 42)
1389
+ * })
1390
+ *
1391
+ * const edgeIndex = 0
1392
+ * const edgeData = Graph.getEdge(graph, edgeIndex)
1393
+ *
1394
+ * if (Option.isSome(edgeData)) {
1395
+ * console.log(edgeData.value.data) // 42
1396
+ * console.log(edgeData.value.source) // NodeIndex(0)
1397
+ * console.log(edgeData.value.target) // NodeIndex(1)
1398
+ * }
1399
+ * ```
1400
+ *
1401
+ * @since 3.18.0
1402
+ * @category getters
1403
+ */
1404
+ export const getEdge = <N, E, T extends Kind = "directed">(
1405
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1406
+ edgeIndex: EdgeIndex
1407
+ ): Option.Option<Edge<E>> => getMapSafe(graph.edges, edgeIndex)
1408
+
1409
+ /**
1410
+ * Checks if an edge exists between two nodes in the graph.
1411
+ *
1412
+ * @example
1413
+ * ```ts
1414
+ * import { Graph } from "effect"
1415
+ *
1416
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1417
+ * const nodeA = Graph.addNode(mutable, "Node A")
1418
+ * const nodeB = Graph.addNode(mutable, "Node B")
1419
+ * const nodeC = Graph.addNode(mutable, "Node C")
1420
+ * Graph.addEdge(mutable, nodeA, nodeB, 42)
1421
+ * })
1422
+ *
1423
+ * const nodeA = 0
1424
+ * const nodeB = 1
1425
+ * const nodeC = 2
1426
+ *
1427
+ * const hasAB = Graph.hasEdge(graph, nodeA, nodeB)
1428
+ * console.log(hasAB) // true
1429
+ *
1430
+ * const hasAC = Graph.hasEdge(graph, nodeA, nodeC)
1431
+ * console.log(hasAC) // false
1432
+ * ```
1433
+ *
1434
+ * @since 3.18.0
1435
+ * @category getters
1436
+ */
1437
+ export const hasEdge = <N, E, T extends Kind = "directed">(
1438
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1439
+ source: NodeIndex,
1440
+ target: NodeIndex
1441
+ ): boolean => {
1442
+ const adjacencyList = getMapSafe(graph.adjacency, source)
1443
+ if (Option.isNone(adjacencyList)) {
1444
+ return false
1445
+ }
1446
+
1447
+ // Check if any edge in the adjacency list connects to the target
1448
+ for (const edgeIndex of adjacencyList.value) {
1449
+ const edge = getMapSafe(graph.edges, edgeIndex)
1450
+ if (Option.isSome(edge) && edge.value.target === target) {
1451
+ return true
1452
+ }
1453
+ }
1454
+
1455
+ return false
1456
+ }
1457
+
1458
+ /**
1459
+ * Returns the number of edges in the graph.
1460
+ *
1461
+ * @example
1462
+ * ```ts
1463
+ * import { Graph } from "effect"
1464
+ *
1465
+ * const emptyGraph = Graph.directed<string, number>()
1466
+ * console.log(Graph.edgeCount(emptyGraph)) // 0
1467
+ *
1468
+ * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => {
1469
+ * const nodeA = Graph.addNode(mutable, "Node A")
1470
+ * const nodeB = Graph.addNode(mutable, "Node B")
1471
+ * const nodeC = Graph.addNode(mutable, "Node C")
1472
+ * Graph.addEdge(mutable, nodeA, nodeB, 1)
1473
+ * Graph.addEdge(mutable, nodeB, nodeC, 2)
1474
+ * Graph.addEdge(mutable, nodeC, nodeA, 3)
1475
+ * })
1476
+ *
1477
+ * console.log(Graph.edgeCount(graphWithEdges)) // 3
1478
+ * ```
1479
+ *
1480
+ * @since 3.18.0
1481
+ * @category getters
1482
+ */
1483
+ export const edgeCount = <N, E, T extends Kind = "directed">(
1484
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
1485
+ ): number => graph.edges.size
1486
+
1487
+ /**
1488
+ * Returns the neighboring nodes (targets of outgoing edges) for a given node.
1489
+ *
1490
+ * @example
1491
+ * ```ts
1492
+ * import { Graph } from "effect"
1493
+ *
1494
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1495
+ * const nodeA = Graph.addNode(mutable, "Node A")
1496
+ * const nodeB = Graph.addNode(mutable, "Node B")
1497
+ * const nodeC = Graph.addNode(mutable, "Node C")
1498
+ * Graph.addEdge(mutable, nodeA, nodeB, 1)
1499
+ * Graph.addEdge(mutable, nodeA, nodeC, 2)
1500
+ * })
1501
+ *
1502
+ * const nodeA = 0
1503
+ * const nodeB = 1
1504
+ * const nodeC = 2
1505
+ *
1506
+ * const neighborsA = Graph.neighbors(graph, nodeA)
1507
+ * console.log(neighborsA) // [NodeIndex(1), NodeIndex(2)]
1508
+ *
1509
+ * const neighborsB = Graph.neighbors(graph, nodeB)
1510
+ * console.log(neighborsB) // []
1511
+ * ```
1512
+ *
1513
+ * @since 3.18.0
1514
+ * @category getters
1515
+ */
1516
+ export const neighbors = <N, E, T extends Kind = "directed">(
1517
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1518
+ nodeIndex: NodeIndex
1519
+ ): Array<NodeIndex> => {
1520
+ const adjacencyList = getMapSafe(graph.adjacency, nodeIndex)
1521
+ if (Option.isNone(adjacencyList)) {
1522
+ return []
1523
+ }
1524
+
1525
+ const result: Array<NodeIndex> = []
1526
+ for (const edgeIndex of adjacencyList.value) {
1527
+ const edge = getMapSafe(graph.edges, edgeIndex)
1528
+ if (Option.isSome(edge)) {
1529
+ result.push(edge.value.target)
1530
+ }
1531
+ }
1532
+
1533
+ return result
1534
+ }
1535
+
1536
+ /**
1537
+ * Get neighbors of a node in a specific direction for bidirectional traversal.
1538
+ *
1539
+ * @example
1540
+ * ```ts
1541
+ * import { Graph } from "effect"
1542
+ *
1543
+ * const graph = Graph.directed<string, string>((mutable) => {
1544
+ * const a = Graph.addNode(mutable, "A")
1545
+ * const b = Graph.addNode(mutable, "B")
1546
+ * Graph.addEdge(mutable, a, b, "A->B")
1547
+ * })
1548
+ *
1549
+ * const nodeA = 0
1550
+ * const nodeB = 1
1551
+ *
1552
+ * // Get outgoing neighbors (nodes that nodeA points to)
1553
+ * const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing")
1554
+ *
1555
+ * // Get incoming neighbors (nodes that point to nodeB)
1556
+ * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
1557
+ * ```
1558
+ *
1559
+ * @since 3.18.0
1560
+ * @category queries
1561
+ */
1562
+ export const neighborsDirected = <N, E, T extends Kind = "directed">(
1563
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1564
+ nodeIndex: NodeIndex,
1565
+ direction: Direction
1566
+ ): Array<NodeIndex> => {
1567
+ const adjacencyMap = direction === "incoming"
1568
+ ? graph.reverseAdjacency
1569
+ : graph.adjacency
1570
+
1571
+ const adjacencyList = getMapSafe(adjacencyMap, nodeIndex)
1572
+ if (Option.isNone(adjacencyList)) {
1573
+ return []
1574
+ }
1575
+
1576
+ const result: Array<NodeIndex> = []
1577
+ for (const edgeIndex of adjacencyList.value) {
1578
+ const edge = getMapSafe(graph.edges, edgeIndex)
1579
+ if (Option.isSome(edge)) {
1580
+ // For incoming direction, we want the source node instead of target
1581
+ const neighborNode = direction === "incoming"
1582
+ ? edge.value.source
1583
+ : edge.value.target
1584
+ result.push(neighborNode)
1585
+ }
1586
+ }
1587
+
1588
+ return result
1589
+ }
1590
+
1591
+ // =============================================================================
1592
+ // GraphViz Export
1593
+ // =============================================================================
1594
+
1595
+ /**
1596
+ * Exports a graph to GraphViz DOT format for visualization.
1597
+ *
1598
+ * @example
1599
+ * ```ts
1600
+ * import { Graph } from "effect"
1601
+ *
1602
+ * const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
1603
+ * const nodeA = Graph.addNode(mutable, "Node A")
1604
+ * const nodeB = Graph.addNode(mutable, "Node B")
1605
+ * const nodeC = Graph.addNode(mutable, "Node C")
1606
+ * Graph.addEdge(mutable, nodeA, nodeB, 1)
1607
+ * Graph.addEdge(mutable, nodeB, nodeC, 2)
1608
+ * Graph.addEdge(mutable, nodeC, nodeA, 3)
1609
+ * })
1610
+ *
1611
+ * const dot = Graph.toGraphViz(graph)
1612
+ * console.log(dot)
1613
+ * // digraph G {
1614
+ * // "0" [label="Node A"];
1615
+ * // "1" [label="Node B"];
1616
+ * // "2" [label="Node C"];
1617
+ * // "0" -> "1" [label="1"];
1618
+ * // "1" -> "2" [label="2"];
1619
+ * // "2" -> "0" [label="3"];
1620
+ * // }
1621
+ * ```
1622
+ *
1623
+ * @since 3.18.0
1624
+ * @category utils
1625
+ */
1626
+ export const toGraphViz = <N, E, T extends Kind = "directed">(
1627
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1628
+ options?: {
1629
+ readonly nodeLabel?: (data: N) => string
1630
+ readonly edgeLabel?: (data: E) => string
1631
+ readonly graphName?: string
1632
+ }
1633
+ ): string => {
1634
+ const {
1635
+ edgeLabel = (data: E) => String(data),
1636
+ graphName = "G",
1637
+ nodeLabel = (data: N) => String(data)
1638
+ } = options ?? {}
1639
+
1640
+ const isDirected = graph.type === "directed"
1641
+ const graphType = isDirected ? "digraph" : "graph"
1642
+ const edgeOperator = isDirected ? "->" : "--"
1643
+
1644
+ const lines: Array<string> = []
1645
+ lines.push(`${graphType} ${graphName} {`)
1646
+
1647
+ // Add nodes
1648
+ for (const [nodeIndex, nodeData] of graph.nodes) {
1649
+ const label = nodeLabel(nodeData).replace(/"/g, "\\\"")
1650
+ lines.push(` "${nodeIndex}" [label="${label}"];`)
1651
+ }
1652
+
1653
+ // Add edges
1654
+ for (const [, edgeData] of graph.edges) {
1655
+ const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"")
1656
+ lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`)
1657
+ }
1658
+
1659
+ lines.push("}")
1660
+ return lines.join("\n")
1661
+ }
1662
+
1663
+ // =============================================================================
1664
+ // Direction Types for Bidirectional Traversal
1665
+ // =============================================================================
1666
+
1667
+ /**
1668
+ * Direction for graph traversal, indicating which edges to follow.
1669
+ *
1670
+ * @example
1671
+ * ```ts
1672
+ * import { Graph } from "effect"
1673
+ *
1674
+ * const graph = Graph.directed<string, string>((mutable) => {
1675
+ * const a = Graph.addNode(mutable, "A")
1676
+ * const b = Graph.addNode(mutable, "B")
1677
+ * Graph.addEdge(mutable, a, b, "A->B")
1678
+ * })
1679
+ *
1680
+ * // Follow outgoing edges (normal direction)
1681
+ * const outgoingNodes = Array.from(Graph.indices(Graph.dfs(graph, { startNodes: [0], direction: "outgoing" })))
1682
+ *
1683
+ * // Follow incoming edges (reverse direction)
1684
+ * const incomingNodes = Array.from(Graph.indices(Graph.dfs(graph, { startNodes: [1], direction: "incoming" })))
1685
+ * ```
1686
+ *
1687
+ * @since 3.18.0
1688
+ * @category models
1689
+ */
1690
+ export type Direction = "outgoing" | "incoming"
1691
+
1692
+ // =============================================================================
1693
+
1694
+ // =============================================================================
1695
+ // Graph Structure Analysis Algorithms (Phase 5A)
1696
+ // =============================================================================
1697
+
1698
+ /**
1699
+ * Checks if the graph is acyclic (contains no cycles).
1700
+ *
1701
+ * Uses depth-first search to detect back edges, which indicate cycles.
1702
+ * For directed graphs, any back edge creates a cycle. For undirected graphs,
1703
+ * a back edge that doesn't go to the immediate parent creates a cycle.
1704
+ *
1705
+ * @example
1706
+ * ```ts
1707
+ * import { Graph } from "effect"
1708
+ *
1709
+ * // Acyclic directed graph (DAG)
1710
+ * const dag = Graph.directed<string, string>((mutable) => {
1711
+ * const a = Graph.addNode(mutable, "A")
1712
+ * const b = Graph.addNode(mutable, "B")
1713
+ * const c = Graph.addNode(mutable, "C")
1714
+ * Graph.addEdge(mutable, a, b, "A->B")
1715
+ * Graph.addEdge(mutable, b, c, "B->C")
1716
+ * })
1717
+ * console.log(Graph.isAcyclic(dag)) // true
1718
+ *
1719
+ * // Cyclic directed graph
1720
+ * const cyclic = Graph.directed<string, string>((mutable) => {
1721
+ * const a = Graph.addNode(mutable, "A")
1722
+ * const b = Graph.addNode(mutable, "B")
1723
+ * Graph.addEdge(mutable, a, b, "A->B")
1724
+ * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle
1725
+ * })
1726
+ * console.log(Graph.isAcyclic(cyclic)) // false
1727
+ * ```
1728
+ *
1729
+ * @since 3.18.0
1730
+ * @category algorithms
1731
+ */
1732
+ export const isAcyclic = <N, E, T extends Kind = "directed">(
1733
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
1734
+ ): boolean => {
1735
+ // Use existing cycle flag if available
1736
+ if (Option.isSome(graph.isAcyclic)) {
1737
+ return graph.isAcyclic.value
1738
+ }
1739
+
1740
+ // Stack-safe DFS cycle detection using iterative approach
1741
+ const visited = new Set<NodeIndex>()
1742
+ const recursionStack = new Set<NodeIndex>()
1743
+
1744
+ // Stack entry: [node, neighbors, neighborIndex, isFirstVisit]
1745
+ type DfsStackEntry = [NodeIndex, Array<NodeIndex>, number, boolean]
1746
+
1747
+ // Get all nodes to handle disconnected components
1748
+ for (const startNode of graph.nodes.keys()) {
1749
+ if (visited.has(startNode)) {
1750
+ continue // Already processed this component
1751
+ }
1752
+
1753
+ // Iterative DFS with explicit stack
1754
+ const stack: Array<DfsStackEntry> = [[startNode, [], 0, true]]
1755
+
1756
+ while (stack.length > 0) {
1757
+ const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1]
1758
+
1759
+ // First visit to this node
1760
+ if (isFirstVisit) {
1761
+ if (recursionStack.has(node)) {
1762
+ // Back edge found - cycle detected
1763
+ graph.isAcyclic = Option.some(false)
1764
+ return false
1765
+ }
1766
+
1767
+ if (visited.has(node)) {
1768
+ stack.pop()
1769
+ continue
1770
+ }
1771
+
1772
+ visited.add(node)
1773
+ recursionStack.add(node)
1774
+
1775
+ // Get neighbors for this node
1776
+ const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing"))
1777
+ stack[stack.length - 1] = [node, nodeNeighbors, 0, false]
1778
+ continue
1779
+ }
1780
+
1781
+ // Process next neighbor
1782
+ if (neighborIndex < neighbors.length) {
1783
+ const neighbor = neighbors[neighborIndex]
1784
+ stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false]
1785
+
1786
+ if (recursionStack.has(neighbor)) {
1787
+ // Back edge found - cycle detected
1788
+ graph.isAcyclic = Option.some(false)
1789
+ return false
1790
+ }
1791
+
1792
+ if (!visited.has(neighbor)) {
1793
+ stack.push([neighbor, [], 0, true])
1794
+ }
1795
+ } else {
1796
+ // Done with this node - backtrack
1797
+ recursionStack.delete(node)
1798
+ stack.pop()
1799
+ }
1800
+ }
1801
+ }
1802
+
1803
+ // Cache the result
1804
+ graph.isAcyclic = Option.some(true)
1805
+ return true
1806
+ }
1807
+
1808
+ /**
1809
+ * Checks if an undirected graph is bipartite.
1810
+ *
1811
+ * A bipartite graph is one whose vertices can be divided into two disjoint sets
1812
+ * such that no two vertices within the same set are adjacent. Uses BFS coloring
1813
+ * to determine bipartiteness.
1814
+ *
1815
+ * @example
1816
+ * ```ts
1817
+ * import { Graph } from "effect"
1818
+ *
1819
+ * // Bipartite graph (alternating coloring possible)
1820
+ * const bipartite = Graph.undirected<string, string>((mutable) => {
1821
+ * const a = Graph.addNode(mutable, "A")
1822
+ * const b = Graph.addNode(mutable, "B")
1823
+ * const c = Graph.addNode(mutable, "C")
1824
+ * const d = Graph.addNode(mutable, "D")
1825
+ * Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D}
1826
+ * Graph.addEdge(mutable, b, c, "edge")
1827
+ * Graph.addEdge(mutable, c, d, "edge")
1828
+ * })
1829
+ * console.log(Graph.isBipartite(bipartite)) // true
1830
+ *
1831
+ * // Non-bipartite graph (odd cycle)
1832
+ * const triangle = Graph.undirected<string, string>((mutable) => {
1833
+ * const a = Graph.addNode(mutable, "A")
1834
+ * const b = Graph.addNode(mutable, "B")
1835
+ * const c = Graph.addNode(mutable, "C")
1836
+ * Graph.addEdge(mutable, a, b, "edge")
1837
+ * Graph.addEdge(mutable, b, c, "edge")
1838
+ * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle)
1839
+ * })
1840
+ * console.log(Graph.isBipartite(triangle)) // false
1841
+ * ```
1842
+ *
1843
+ * @since 3.18.0
1844
+ * @category algorithms
1845
+ */
1846
+ export const isBipartite = <N, E>(
1847
+ graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
1848
+ ): boolean => {
1849
+ const coloring = new Map<NodeIndex, 0 | 1>()
1850
+ const discovered = new Set<NodeIndex>()
1851
+ let isBipartiteGraph = true
1852
+
1853
+ // Get all nodes to handle disconnected components
1854
+ for (const startNode of graph.nodes.keys()) {
1855
+ if (!discovered.has(startNode)) {
1856
+ // Start BFS coloring from this component
1857
+ const queue: Array<NodeIndex> = [startNode]
1858
+ coloring.set(startNode, 0) // Color start node with 0
1859
+ discovered.add(startNode)
1860
+
1861
+ while (queue.length > 0 && isBipartiteGraph) {
1862
+ const current = queue.shift()!
1863
+ const currentColor = coloring.get(current)!
1864
+ const neighborColor: 0 | 1 = currentColor === 0 ? 1 : 0
1865
+
1866
+ // Get all neighbors for undirected graph
1867
+ const nodeNeighbors = getUndirectedNeighbors(graph, current)
1868
+ for (const neighbor of nodeNeighbors) {
1869
+ if (!discovered.has(neighbor)) {
1870
+ // Color unvisited neighbor with opposite color
1871
+ coloring.set(neighbor, neighborColor)
1872
+ discovered.add(neighbor)
1873
+ queue.push(neighbor)
1874
+ } else {
1875
+ // Check if neighbor has the same color (conflict)
1876
+ if (coloring.get(neighbor) === currentColor) {
1877
+ isBipartiteGraph = false
1878
+ break
1879
+ }
1880
+ }
1881
+ }
1882
+ }
1883
+
1884
+ // Early exit if not bipartite
1885
+ if (!isBipartiteGraph) {
1886
+ break
1887
+ }
1888
+ }
1889
+ }
1890
+
1891
+ return isBipartiteGraph
1892
+ }
1893
+
1894
+ /**
1895
+ * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency.
1896
+ * For undirected graphs, we need to find the other endpoint of each edge incident to the node.
1897
+ */
1898
+ const getUndirectedNeighbors = <N, E>(
1899
+ graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">,
1900
+ nodeIndex: NodeIndex
1901
+ ): Array<NodeIndex> => {
1902
+ const neighbors = new Set<NodeIndex>()
1903
+
1904
+ // Check edges where this node is the source
1905
+ const adjacencyList = getMapSafe(graph.adjacency, nodeIndex)
1906
+ if (Option.isSome(adjacencyList)) {
1907
+ for (const edgeIndex of adjacencyList.value) {
1908
+ const edge = getMapSafe(graph.edges, edgeIndex)
1909
+ if (Option.isSome(edge)) {
1910
+ // For undirected graphs, the neighbor is the other endpoint
1911
+ const otherNode = edge.value.source === nodeIndex ? edge.value.target : edge.value.source
1912
+ neighbors.add(otherNode)
1913
+ }
1914
+ }
1915
+ }
1916
+
1917
+ return Array.from(neighbors)
1918
+ }
1919
+
1920
+ /**
1921
+ * Find connected components in an undirected graph.
1922
+ * Each component is represented as an array of node indices.
1923
+ *
1924
+ * @example
1925
+ * ```ts
1926
+ * import { Graph } from "effect"
1927
+ *
1928
+ * const graph = Graph.undirected<string, string>((mutable) => {
1929
+ * const a = Graph.addNode(mutable, "A")
1930
+ * const b = Graph.addNode(mutable, "B")
1931
+ * const c = Graph.addNode(mutable, "C")
1932
+ * const d = Graph.addNode(mutable, "D")
1933
+ * Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B
1934
+ * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D
1935
+ * })
1936
+ *
1937
+ * const components = Graph.connectedComponents(graph)
1938
+ * console.log(components) // [[0, 1], [2, 3]]
1939
+ * ```
1940
+ *
1941
+ * @since 3.18.0
1942
+ * @category algorithms
1943
+ */
1944
+ export const connectedComponents = <N, E>(
1945
+ graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
1946
+ ): Array<Array<NodeIndex>> => {
1947
+ const visited = new Set<NodeIndex>()
1948
+ const components: Array<Array<NodeIndex>> = []
1949
+ for (const startNode of graph.nodes.keys()) {
1950
+ if (!visited.has(startNode)) {
1951
+ // DFS to find all nodes in this component
1952
+ const component: Array<NodeIndex> = []
1953
+ const stack: Array<NodeIndex> = [startNode]
1954
+
1955
+ while (stack.length > 0) {
1956
+ const current = stack.pop()!
1957
+ if (!visited.has(current)) {
1958
+ visited.add(current)
1959
+ component.push(current)
1960
+
1961
+ // Add all unvisited neighbors to stack
1962
+ const nodeNeighbors = getUndirectedNeighbors(graph, current)
1963
+ for (const neighbor of nodeNeighbors) {
1964
+ if (!visited.has(neighbor)) {
1965
+ stack.push(neighbor)
1966
+ }
1967
+ }
1968
+ }
1969
+ }
1970
+
1971
+ components.push(component)
1972
+ }
1973
+ }
1974
+
1975
+ return components
1976
+ }
1977
+
1978
+ /**
1979
+ * Find strongly connected components in a directed graph using Kosaraju's algorithm.
1980
+ * Each SCC is represented as an array of node indices.
1981
+ *
1982
+ * @example
1983
+ * ```ts
1984
+ * import { Graph } from "effect"
1985
+ *
1986
+ * const graph = Graph.directed<string, string>((mutable) => {
1987
+ * const a = Graph.addNode(mutable, "A")
1988
+ * const b = Graph.addNode(mutable, "B")
1989
+ * const c = Graph.addNode(mutable, "C")
1990
+ * Graph.addEdge(mutable, a, b, "A->B")
1991
+ * Graph.addEdge(mutable, b, c, "B->C")
1992
+ * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C
1993
+ * })
1994
+ *
1995
+ * const sccs = Graph.stronglyConnectedComponents(graph)
1996
+ * console.log(sccs) // [[0, 1, 2]]
1997
+ * ```
1998
+ *
1999
+ * @since 3.18.0
2000
+ * @category algorithms
2001
+ */
2002
+ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
2003
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
2004
+ ): Array<Array<NodeIndex>> => {
2005
+ const visited = new Set<NodeIndex>()
2006
+ const finishOrder: Array<NodeIndex> = []
2007
+ // Iterate directly over node keys
2008
+
2009
+ // Step 1: Stack-safe DFS on original graph to get finish times
2010
+ // Stack entry: [node, neighbors, neighborIndex, isFirstVisit]
2011
+ type DfsStackEntry = [NodeIndex, Array<NodeIndex>, number, boolean]
2012
+
2013
+ for (const startNode of graph.nodes.keys()) {
2014
+ if (visited.has(startNode)) {
2015
+ continue
2016
+ }
2017
+
2018
+ const stack: Array<DfsStackEntry> = [[startNode, [], 0, true]]
2019
+
2020
+ while (stack.length > 0) {
2021
+ const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1]
2022
+
2023
+ if (isFirstVisit) {
2024
+ if (visited.has(node)) {
2025
+ stack.pop()
2026
+ continue
2027
+ }
2028
+
2029
+ visited.add(node)
2030
+ const nodeNeighborsList = neighbors(graph, node)
2031
+ stack[stack.length - 1] = [node, nodeNeighborsList, 0, false]
2032
+ continue
2033
+ }
2034
+
2035
+ // Process next neighbor
2036
+ if (neighborIndex < nodeNeighbors.length) {
2037
+ const neighbor = nodeNeighbors[neighborIndex]
2038
+ stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false]
2039
+
2040
+ if (!visited.has(neighbor)) {
2041
+ stack.push([neighbor, [], 0, true])
2042
+ }
2043
+ } else {
2044
+ // Done with this node - add to finish order (post-order)
2045
+ finishOrder.push(node)
2046
+ stack.pop()
2047
+ }
2048
+ }
2049
+ }
2050
+
2051
+ // Step 2: Stack-safe DFS on transpose graph in reverse finish order
2052
+ visited.clear()
2053
+ const sccs: Array<Array<NodeIndex>> = []
2054
+
2055
+ for (let i = finishOrder.length - 1; i >= 0; i--) {
2056
+ const startNode = finishOrder[i]
2057
+ if (visited.has(startNode)) {
2058
+ continue
2059
+ }
2060
+
2061
+ const scc: Array<NodeIndex> = []
2062
+ const stack: Array<NodeIndex> = [startNode]
2063
+
2064
+ while (stack.length > 0) {
2065
+ const node = stack.pop()!
2066
+
2067
+ if (visited.has(node)) {
2068
+ continue
2069
+ }
2070
+
2071
+ visited.add(node)
2072
+ scc.push(node)
2073
+
2074
+ // Use reverse adjacency (transpose graph)
2075
+ const reverseAdjacency = getMapSafe(graph.reverseAdjacency, node)
2076
+ if (Option.isSome(reverseAdjacency)) {
2077
+ for (const edgeIndex of reverseAdjacency.value) {
2078
+ const edge = getMapSafe(graph.edges, edgeIndex)
2079
+ if (Option.isSome(edge)) {
2080
+ const predecessor = edge.value.source
2081
+ if (!visited.has(predecessor)) {
2082
+ stack.push(predecessor)
2083
+ }
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+
2089
+ sccs.push(scc)
2090
+ }
2091
+
2092
+ return sccs
2093
+ }
2094
+
2095
+ // =============================================================================
2096
+ // Path Finding Algorithms (Phase 5B)
2097
+ // =============================================================================
2098
+
2099
+ /**
2100
+ * Result of a shortest path computation containing the path and total distance.
2101
+ *
2102
+ * @since 3.18.0
2103
+ * @category models
2104
+ */
2105
+ export interface PathResult<E> {
2106
+ readonly path: Array<NodeIndex>
2107
+ readonly distance: number
2108
+ readonly edgeWeights: Array<E>
2109
+ }
2110
+
2111
+ /**
2112
+ * Find the shortest path between two nodes using Dijkstra's algorithm.
2113
+ *
2114
+ * Dijkstra's algorithm works with non-negative edge weights and finds the shortest
2115
+ * path from a source node to a target node in O((V + E) log V) time complexity.
2116
+ *
2117
+ * @example
2118
+ * ```ts
2119
+ * import { Graph, Option } from "effect"
2120
+ *
2121
+ * const graph = Graph.directed<string, number>((mutable) => {
2122
+ * const a = Graph.addNode(mutable, "A")
2123
+ * const b = Graph.addNode(mutable, "B")
2124
+ * const c = Graph.addNode(mutable, "C")
2125
+ * Graph.addEdge(mutable, a, b, 5)
2126
+ * Graph.addEdge(mutable, a, c, 10)
2127
+ * Graph.addEdge(mutable, b, c, 2)
2128
+ * })
2129
+ *
2130
+ * const result = Graph.dijkstra(graph, 0, 2, (edgeData) => edgeData)
2131
+ * if (Option.isSome(result)) {
2132
+ * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
2133
+ * console.log(result.value.distance) // 7 - total distance
2134
+ * }
2135
+ * ```
2136
+ *
2137
+ * @since 3.18.0
2138
+ * @category algorithms
2139
+ */
2140
+ export const dijkstra = <N, E, T extends Kind = "directed">(
2141
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2142
+ source: NodeIndex,
2143
+ target: NodeIndex,
2144
+ edgeWeight: (edgeData: E) => number
2145
+ ): Option.Option<PathResult<E>> => {
2146
+ // Validate that source and target nodes exist
2147
+ if (!graph.nodes.has(source)) {
2148
+ throw new Error(`Source node ${source} does not exist`)
2149
+ }
2150
+ if (!graph.nodes.has(target)) {
2151
+ throw new Error(`Target node ${target} does not exist`)
2152
+ }
2153
+
2154
+ // Early return if source equals target
2155
+ if (source === target) {
2156
+ return Option.some({
2157
+ path: [source],
2158
+ distance: 0,
2159
+ edgeWeights: []
2160
+ })
2161
+ }
2162
+
2163
+ // Distance tracking and priority queue simulation
2164
+ const distances = new Map<NodeIndex, number>()
2165
+ const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
2166
+ const visited = new Set<NodeIndex>()
2167
+
2168
+ // Initialize distances
2169
+ // Iterate directly over node keys
2170
+ for (const node of graph.nodes.keys()) {
2171
+ distances.set(node, node === source ? 0 : Infinity)
2172
+ previous.set(node, null)
2173
+ }
2174
+
2175
+ // Simple priority queue using array (can be optimized with proper heap)
2176
+ const priorityQueue: Array<{ node: NodeIndex; distance: number }> = [
2177
+ { node: source, distance: 0 }
2178
+ ]
2179
+
2180
+ while (priorityQueue.length > 0) {
2181
+ // Find minimum distance node (priority queue extract-min)
2182
+ let minIndex = 0
2183
+ for (let i = 1; i < priorityQueue.length; i++) {
2184
+ if (priorityQueue[i].distance < priorityQueue[minIndex].distance) {
2185
+ minIndex = i
2186
+ }
2187
+ }
2188
+
2189
+ const current = priorityQueue.splice(minIndex, 1)[0]
2190
+ const currentNode = current.node
2191
+
2192
+ // Skip if already visited (can happen with duplicate entries)
2193
+ if (visited.has(currentNode)) {
2194
+ continue
2195
+ }
2196
+
2197
+ visited.add(currentNode)
2198
+
2199
+ // Early termination if we reached the target
2200
+ if (currentNode === target) {
2201
+ break
2202
+ }
2203
+
2204
+ // Get current distance
2205
+ const currentDistance = distances.get(currentNode)!
2206
+
2207
+ // Examine all outgoing edges
2208
+ const adjacencyList = getMapSafe(graph.adjacency, currentNode)
2209
+ if (Option.isSome(adjacencyList)) {
2210
+ for (const edgeIndex of adjacencyList.value) {
2211
+ const edge = getMapSafe(graph.edges, edgeIndex)
2212
+ if (Option.isSome(edge)) {
2213
+ const neighbor = edge.value.target
2214
+ const weight = edgeWeight(edge.value.data)
2215
+
2216
+ // Validate non-negative weights
2217
+ if (weight < 0) {
2218
+ throw new Error(`Dijkstra's algorithm requires non-negative edge weights, found ${weight}`)
2219
+ }
2220
+
2221
+ const newDistance = currentDistance + weight
2222
+ const neighborDistance = distances.get(neighbor)!
2223
+
2224
+ // Relaxation step
2225
+ if (newDistance < neighborDistance) {
2226
+ distances.set(neighbor, newDistance)
2227
+ previous.set(neighbor, { node: currentNode, edgeData: edge.value.data })
2228
+
2229
+ // Add to priority queue if not visited
2230
+ if (!visited.has(neighbor)) {
2231
+ priorityQueue.push({ node: neighbor, distance: newDistance })
2232
+ }
2233
+ }
2234
+ }
2235
+ }
2236
+ }
2237
+ }
2238
+
2239
+ // Check if target is reachable
2240
+ const targetDistance = distances.get(target)!
2241
+ if (targetDistance === Infinity) {
2242
+ return Option.none() // No path exists
2243
+ }
2244
+
2245
+ // Reconstruct path
2246
+ const path: Array<NodeIndex> = []
2247
+ const edgeWeights: Array<E> = []
2248
+ let currentNode: NodeIndex | null = target
2249
+
2250
+ while (currentNode !== null) {
2251
+ path.unshift(currentNode)
2252
+ const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2253
+ if (prev !== null) {
2254
+ edgeWeights.unshift(prev.edgeData)
2255
+ currentNode = prev.node
2256
+ } else {
2257
+ currentNode = null
2258
+ }
2259
+ }
2260
+
2261
+ return Option.some({
2262
+ path,
2263
+ distance: targetDistance,
2264
+ edgeWeights
2265
+ })
2266
+ }
2267
+
2268
+ /**
2269
+ * Result of all-pairs shortest path computation.
2270
+ *
2271
+ * @since 3.18.0
2272
+ * @category models
2273
+ */
2274
+ export interface AllPairsResult<E> {
2275
+ readonly distances: Map<NodeIndex, Map<NodeIndex, number>>
2276
+ readonly paths: Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>
2277
+ readonly edgeWeights: Map<NodeIndex, Map<NodeIndex, Array<E>>>
2278
+ }
2279
+
2280
+ /**
2281
+ * Find shortest paths between all pairs of nodes using Floyd-Warshall algorithm.
2282
+ *
2283
+ * Floyd-Warshall algorithm computes shortest paths between all pairs of nodes in O(V³) time.
2284
+ * It can handle negative edge weights and detect negative cycles.
2285
+ *
2286
+ * @example
2287
+ * ```ts
2288
+ * import { Graph } from "effect"
2289
+ *
2290
+ * const graph = Graph.directed<string, number>((mutable) => {
2291
+ * const a = Graph.addNode(mutable, "A")
2292
+ * const b = Graph.addNode(mutable, "B")
2293
+ * const c = Graph.addNode(mutable, "C")
2294
+ * Graph.addEdge(mutable, a, b, 3)
2295
+ * Graph.addEdge(mutable, b, c, 2)
2296
+ * Graph.addEdge(mutable, a, c, 7)
2297
+ * })
2298
+ *
2299
+ * const result = Graph.floydWarshall(graph, (edgeData) => edgeData)
2300
+ * const distanceAToC = result.distances.get(0)?.get(2) // 5 (A->B->C)
2301
+ * const pathAToC = result.paths.get(0)?.get(2) // [0, 1, 2]
2302
+ * ```
2303
+ *
2304
+ * @since 3.18.0
2305
+ * @category algorithms
2306
+ */
2307
+ export const floydWarshall = <N, E, T extends Kind = "directed">(
2308
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2309
+ edgeWeight: (edgeData: E) => number
2310
+ ): AllPairsResult<E> => {
2311
+ // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration)
2312
+ const allNodes = Array.from(graph.nodes.keys())
2313
+
2314
+ // Initialize distance matrix
2315
+ const dist = new Map<NodeIndex, Map<NodeIndex, number>>()
2316
+ const next = new Map<NodeIndex, Map<NodeIndex, NodeIndex | null>>()
2317
+ const edgeMatrix = new Map<NodeIndex, Map<NodeIndex, E | null>>()
2318
+
2319
+ // Initialize with infinity for all pairs
2320
+ for (const i of allNodes) {
2321
+ dist.set(i, new Map())
2322
+ next.set(i, new Map())
2323
+ edgeMatrix.set(i, new Map())
2324
+
2325
+ for (const j of allNodes) {
2326
+ dist.get(i)!.set(j, i === j ? 0 : Infinity)
2327
+ next.get(i)!.set(j, null)
2328
+ edgeMatrix.get(i)!.set(j, null)
2329
+ }
2330
+ }
2331
+
2332
+ // Set edge weights
2333
+ for (const [, edgeData] of graph.edges) {
2334
+ const weight = edgeWeight(edgeData.data)
2335
+ const i = edgeData.source
2336
+ const j = edgeData.target
2337
+
2338
+ // Use minimum weight if multiple edges exist
2339
+ const currentWeight = dist.get(i)!.get(j)!
2340
+ if (weight < currentWeight) {
2341
+ dist.get(i)!.set(j, weight)
2342
+ next.get(i)!.set(j, j)
2343
+ edgeMatrix.get(i)!.set(j, edgeData.data)
2344
+ }
2345
+ }
2346
+
2347
+ // Floyd-Warshall main loop
2348
+ for (const k of allNodes) {
2349
+ for (const i of allNodes) {
2350
+ for (const j of allNodes) {
2351
+ const distIK = dist.get(i)!.get(k)!
2352
+ const distKJ = dist.get(k)!.get(j)!
2353
+ const distIJ = dist.get(i)!.get(j)!
2354
+
2355
+ if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) {
2356
+ dist.get(i)!.set(j, distIK + distKJ)
2357
+ next.get(i)!.set(j, next.get(i)!.get(k)!)
2358
+ }
2359
+ }
2360
+ }
2361
+ }
2362
+
2363
+ // Check for negative cycles
2364
+ for (const i of allNodes) {
2365
+ if (dist.get(i)!.get(i)! < 0) {
2366
+ throw new Error(`Negative cycle detected involving node ${i}`)
2367
+ }
2368
+ }
2369
+
2370
+ // Build result paths and edge weights
2371
+ const paths = new Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>()
2372
+ const resultEdgeWeights = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
2373
+
2374
+ for (const i of allNodes) {
2375
+ paths.set(i, new Map())
2376
+ resultEdgeWeights.set(i, new Map())
2377
+
2378
+ for (const j of allNodes) {
2379
+ if (i === j) {
2380
+ paths.get(i)!.set(j, [i])
2381
+ resultEdgeWeights.get(i)!.set(j, [])
2382
+ } else if (dist.get(i)!.get(j)! === Infinity) {
2383
+ paths.get(i)!.set(j, null)
2384
+ resultEdgeWeights.get(i)!.set(j, [])
2385
+ } else {
2386
+ // Reconstruct path iteratively
2387
+ const path: Array<NodeIndex> = []
2388
+ const weights: Array<E> = []
2389
+ let current = i
2390
+
2391
+ path.push(current)
2392
+ while (current !== j) {
2393
+ const nextNode = next.get(current)!.get(j)!
2394
+ if (nextNode === null) break
2395
+
2396
+ const edgeData = edgeMatrix.get(current)!.get(nextNode)!
2397
+ if (edgeData !== null) {
2398
+ weights.push(edgeData)
2399
+ }
2400
+
2401
+ current = nextNode
2402
+ path.push(current)
2403
+ }
2404
+
2405
+ paths.get(i)!.set(j, path)
2406
+ resultEdgeWeights.get(i)!.set(j, weights)
2407
+ }
2408
+ }
2409
+ }
2410
+
2411
+ return {
2412
+ distances: dist,
2413
+ paths,
2414
+ edgeWeights: resultEdgeWeights
2415
+ }
2416
+ }
2417
+
2418
+ /**
2419
+ * Find the shortest path between two nodes using A* pathfinding algorithm.
2420
+ *
2421
+ * A* is an extension of Dijkstra's algorithm that uses a heuristic function to guide
2422
+ * the search towards the target, potentially finding paths faster than Dijkstra's.
2423
+ * The heuristic must be admissible (never overestimate the actual cost).
2424
+ *
2425
+ * @example
2426
+ * ```ts
2427
+ * import { Graph, Option } from "effect"
2428
+ *
2429
+ * const graph = Graph.directed<{x: number, y: number}, number>((mutable) => {
2430
+ * const a = Graph.addNode(mutable, {x: 0, y: 0})
2431
+ * const b = Graph.addNode(mutable, {x: 1, y: 0})
2432
+ * const c = Graph.addNode(mutable, {x: 2, y: 0})
2433
+ * Graph.addEdge(mutable, a, b, 1)
2434
+ * Graph.addEdge(mutable, b, c, 1)
2435
+ * })
2436
+ *
2437
+ * // Manhattan distance heuristic
2438
+ * const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) =>
2439
+ * Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y)
2440
+ *
2441
+ * const result = Graph.astar(graph, 0, 2, (edgeData) => edgeData, heuristic)
2442
+ * if (Option.isSome(result)) {
2443
+ * console.log(result.value.path) // [0, 1, 2] - shortest path
2444
+ * console.log(result.value.distance) // 2 - total distance
2445
+ * }
2446
+ * ```
2447
+ *
2448
+ * @since 3.18.0
2449
+ * @category algorithms
2450
+ */
2451
+ export const astar = <N, E, T extends Kind = "directed">(
2452
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2453
+ source: NodeIndex,
2454
+ target: NodeIndex,
2455
+ edgeWeight: (edgeData: E) => number,
2456
+ heuristic: (sourceNodeData: N, targetNodeData: N) => number
2457
+ ): Option.Option<PathResult<E>> => {
2458
+ // Validate that source and target nodes exist
2459
+ if (!graph.nodes.has(source)) {
2460
+ throw new Error(`Source node ${source} does not exist`)
2461
+ }
2462
+ if (!graph.nodes.has(target)) {
2463
+ throw new Error(`Target node ${target} does not exist`)
2464
+ }
2465
+
2466
+ // Early return if source equals target
2467
+ if (source === target) {
2468
+ return Option.some({
2469
+ path: [source],
2470
+ distance: 0,
2471
+ edgeWeights: []
2472
+ })
2473
+ }
2474
+
2475
+ // Get target node data for heuristic calculations
2476
+ const targetNodeData = getMapSafe(graph.nodes, target)
2477
+ if (Option.isNone(targetNodeData)) {
2478
+ throw new Error(`Target node ${target} data not found`)
2479
+ }
2480
+
2481
+ // Distance tracking (g-score) and f-score (g + h)
2482
+ const gScore = new Map<NodeIndex, number>()
2483
+ const fScore = new Map<NodeIndex, number>()
2484
+ const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
2485
+ const visited = new Set<NodeIndex>()
2486
+
2487
+ // Initialize scores
2488
+ // Iterate directly over node keys
2489
+ for (const node of graph.nodes.keys()) {
2490
+ gScore.set(node, node === source ? 0 : Infinity)
2491
+ fScore.set(node, Infinity)
2492
+ previous.set(node, null)
2493
+ }
2494
+
2495
+ // Calculate initial f-score for source
2496
+ const sourceNodeData = getMapSafe(graph.nodes, source)
2497
+ if (Option.isSome(sourceNodeData)) {
2498
+ const h = heuristic(sourceNodeData.value, targetNodeData.value)
2499
+ fScore.set(source, h)
2500
+ }
2501
+
2502
+ // Priority queue using f-score (total estimated cost)
2503
+ const openSet: Array<{ node: NodeIndex; fScore: number }> = [
2504
+ { node: source, fScore: fScore.get(source)! }
2505
+ ]
2506
+
2507
+ while (openSet.length > 0) {
2508
+ // Find node with lowest f-score
2509
+ let minIndex = 0
2510
+ for (let i = 1; i < openSet.length; i++) {
2511
+ if (openSet[i].fScore < openSet[minIndex].fScore) {
2512
+ minIndex = i
2513
+ }
2514
+ }
2515
+
2516
+ const current = openSet.splice(minIndex, 1)[0]
2517
+ const currentNode = current.node
2518
+
2519
+ // Skip if already visited
2520
+ if (visited.has(currentNode)) {
2521
+ continue
2522
+ }
2523
+
2524
+ visited.add(currentNode)
2525
+
2526
+ // Early termination if we reached the target
2527
+ if (currentNode === target) {
2528
+ break
2529
+ }
2530
+
2531
+ // Get current g-score
2532
+ const currentGScore = gScore.get(currentNode)!
2533
+
2534
+ // Examine all outgoing edges
2535
+ const adjacencyList = getMapSafe(graph.adjacency, currentNode)
2536
+ if (Option.isSome(adjacencyList)) {
2537
+ for (const edgeIndex of adjacencyList.value) {
2538
+ const edge = getMapSafe(graph.edges, edgeIndex)
2539
+ if (Option.isSome(edge)) {
2540
+ const neighbor = edge.value.target
2541
+ const weight = edgeWeight(edge.value.data)
2542
+
2543
+ // Validate non-negative weights
2544
+ if (weight < 0) {
2545
+ throw new Error(`A* algorithm requires non-negative edge weights, found ${weight}`)
2546
+ }
2547
+
2548
+ const tentativeGScore = currentGScore + weight
2549
+ const neighborGScore = gScore.get(neighbor)!
2550
+
2551
+ // If this path to neighbor is better than any previous one
2552
+ if (tentativeGScore < neighborGScore) {
2553
+ // Update g-score and previous
2554
+ gScore.set(neighbor, tentativeGScore)
2555
+ previous.set(neighbor, { node: currentNode, edgeData: edge.value.data })
2556
+
2557
+ // Calculate f-score using heuristic
2558
+ const neighborNodeData = getMapSafe(graph.nodes, neighbor)
2559
+ if (Option.isSome(neighborNodeData)) {
2560
+ const h = heuristic(neighborNodeData.value, targetNodeData.value)
2561
+ const f = tentativeGScore + h
2562
+ fScore.set(neighbor, f)
2563
+
2564
+ // Add to open set if not visited
2565
+ if (!visited.has(neighbor)) {
2566
+ openSet.push({ node: neighbor, fScore: f })
2567
+ }
2568
+ }
2569
+ }
2570
+ }
2571
+ }
2572
+ }
2573
+ }
2574
+
2575
+ // Check if target is reachable
2576
+ const targetGScore = gScore.get(target)!
2577
+ if (targetGScore === Infinity) {
2578
+ return Option.none() // No path exists
2579
+ }
2580
+
2581
+ // Reconstruct path
2582
+ const path: Array<NodeIndex> = []
2583
+ const edgeWeights: Array<E> = []
2584
+ let currentNode: NodeIndex | null = target
2585
+
2586
+ while (currentNode !== null) {
2587
+ path.unshift(currentNode)
2588
+ const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2589
+ if (prev !== null) {
2590
+ edgeWeights.unshift(prev.edgeData)
2591
+ currentNode = prev.node
2592
+ } else {
2593
+ currentNode = null
2594
+ }
2595
+ }
2596
+
2597
+ return Option.some({
2598
+ path,
2599
+ distance: targetGScore,
2600
+ edgeWeights
2601
+ })
2602
+ }
2603
+
2604
+ /**
2605
+ * Find the shortest path between two nodes using Bellman-Ford algorithm.
2606
+ *
2607
+ * Bellman-Ford algorithm can handle negative edge weights and detects negative cycles.
2608
+ * It has O(VE) time complexity, slower than Dijkstra's but more versatile.
2609
+ * Returns Option.none() if a negative cycle is detected that affects the path.
2610
+ *
2611
+ * @example
2612
+ * ```ts
2613
+ * import { Graph, Option } from "effect"
2614
+ *
2615
+ * const graph = Graph.directed<string, number>((mutable) => {
2616
+ * const a = Graph.addNode(mutable, "A")
2617
+ * const b = Graph.addNode(mutable, "B")
2618
+ * const c = Graph.addNode(mutable, "C")
2619
+ * Graph.addEdge(mutable, a, b, -1) // Negative weight allowed
2620
+ * Graph.addEdge(mutable, b, c, 3)
2621
+ * Graph.addEdge(mutable, a, c, 5)
2622
+ * })
2623
+ *
2624
+ * const result = Graph.bellmanFord(graph, 0, 2, (edgeData) => edgeData)
2625
+ * if (Option.isSome(result)) {
2626
+ * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
2627
+ * console.log(result.value.distance) // 2 - total distance
2628
+ * }
2629
+ * ```
2630
+ *
2631
+ * @since 3.18.0
2632
+ * @category algorithms
2633
+ */
2634
+ export const bellmanFord = <N, E, T extends Kind = "directed">(
2635
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2636
+ source: NodeIndex,
2637
+ target: NodeIndex,
2638
+ edgeWeight: (edgeData: E) => number
2639
+ ): Option.Option<PathResult<E>> => {
2640
+ // Validate that source and target nodes exist
2641
+ if (!graph.nodes.has(source)) {
2642
+ throw new Error(`Source node ${source} does not exist`)
2643
+ }
2644
+ if (!graph.nodes.has(target)) {
2645
+ throw new Error(`Target node ${target} does not exist`)
2646
+ }
2647
+
2648
+ // Early return if source equals target
2649
+ if (source === target) {
2650
+ return Option.some({
2651
+ path: [source],
2652
+ distance: 0,
2653
+ edgeWeights: []
2654
+ })
2655
+ }
2656
+
2657
+ // Initialize distances and predecessors
2658
+ const distances = new Map<NodeIndex, number>()
2659
+ const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
2660
+ // Iterate directly over node keys
2661
+
2662
+ for (const node of graph.nodes.keys()) {
2663
+ distances.set(node, node === source ? 0 : Infinity)
2664
+ previous.set(node, null)
2665
+ }
2666
+
2667
+ // Collect all edges for relaxation
2668
+ const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = []
2669
+ for (const [, edgeData] of graph.edges) {
2670
+ const weight = edgeWeight(edgeData.data)
2671
+ edges.push({
2672
+ source: edgeData.source,
2673
+ target: edgeData.target,
2674
+ weight,
2675
+ edgeData: edgeData.data
2676
+ })
2677
+ }
2678
+
2679
+ // Relax edges up to V-1 times
2680
+ const nodeCount = graph.nodes.size
2681
+ for (let i = 0; i < nodeCount - 1; i++) {
2682
+ let hasUpdate = false
2683
+
2684
+ for (const edge of edges) {
2685
+ const sourceDistance = distances.get(edge.source)!
2686
+ const targetDistance = distances.get(edge.target)!
2687
+
2688
+ // Relaxation step
2689
+ if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) {
2690
+ distances.set(edge.target, sourceDistance + edge.weight)
2691
+ previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData })
2692
+ hasUpdate = true
2693
+ }
2694
+ }
2695
+
2696
+ // Early termination if no updates
2697
+ if (!hasUpdate) {
2698
+ break
2699
+ }
2700
+ }
2701
+
2702
+ // Check for negative cycles
2703
+ for (const edge of edges) {
2704
+ const sourceDistance = distances.get(edge.source)!
2705
+ const targetDistance = distances.get(edge.target)!
2706
+
2707
+ if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) {
2708
+ // Negative cycle detected - check if it affects the path to target
2709
+ const affectedNodes = new Set<NodeIndex>()
2710
+ const queue = [edge.target]
2711
+
2712
+ while (queue.length > 0) {
2713
+ const node = queue.shift()!
2714
+ if (affectedNodes.has(node)) continue
2715
+ affectedNodes.add(node)
2716
+
2717
+ // Add all nodes reachable from this node
2718
+ const adjacencyList = getMapSafe(graph.adjacency, node)
2719
+ if (Option.isSome(adjacencyList)) {
2720
+ for (const edgeIndex of adjacencyList.value) {
2721
+ const edge = getMapSafe(graph.edges, edgeIndex)
2722
+ if (Option.isSome(edge)) {
2723
+ queue.push(edge.value.target)
2724
+ }
2725
+ }
2726
+ }
2727
+ }
2728
+
2729
+ // If target is affected by negative cycle, return null
2730
+ if (affectedNodes.has(target)) {
2731
+ return Option.none()
2732
+ }
2733
+ }
2734
+ }
2735
+
2736
+ // Check if target is reachable
2737
+ const targetDistance = distances.get(target)!
2738
+ if (targetDistance === Infinity) {
2739
+ return Option.none() // No path exists
2740
+ }
2741
+
2742
+ // Reconstruct path
2743
+ const path: Array<NodeIndex> = []
2744
+ const edgeWeights: Array<E> = []
2745
+ let currentNode: NodeIndex | null = target
2746
+
2747
+ while (currentNode !== null) {
2748
+ path.unshift(currentNode)
2749
+ const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
2750
+ if (prev !== null) {
2751
+ edgeWeights.unshift(prev.edgeData)
2752
+ currentNode = prev.node
2753
+ } else {
2754
+ currentNode = null
2755
+ }
2756
+ }
2757
+
2758
+ return Option.some({
2759
+ path,
2760
+ distance: targetDistance,
2761
+ edgeWeights
2762
+ })
2763
+ }
2764
+
2765
+ /**
2766
+ * Concrete class for iterables that produce [NodeIndex, NodeData] tuples.
2767
+ *
2768
+ * This class provides a common abstraction for all iterables that return node data,
2769
+ * including traversal iterators (DFS, BFS, etc.) and element iterators (nodes, externals).
2770
+ * It uses a mapEntry function pattern for flexible iteration and transformation.
2771
+ *
2772
+ * @example
2773
+ * ```ts
2774
+ * import { Graph } from "effect"
2775
+ *
2776
+ * const graph = Graph.directed<string, number>((mutable) => {
2777
+ * const a = Graph.addNode(mutable, "A")
2778
+ * const b = Graph.addNode(mutable, "B")
2779
+ * Graph.addEdge(mutable, a, b, 1)
2780
+ * })
2781
+ *
2782
+ * // Both traversal and element iterators return NodeWalker
2783
+ * const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { startNodes: [0] })
2784
+ * const allNodes: Graph.NodeWalker<string> = Graph.nodes(graph)
2785
+ *
2786
+ * // Common interface for working with node iterables
2787
+ * function processNodes<N>(nodeIterable: Graph.NodeWalker<N>): Array<number> {
2788
+ * return Array.from(Graph.indices(nodeIterable))
2789
+ * }
2790
+ *
2791
+ * // Access node data using values() or entries()
2792
+ * const nodeData = Array.from(Graph.values(dfsNodes)) // ["A", "B"]
2793
+ * const nodeEntries = Array.from(Graph.entries(allNodes)) // [[0, "A"], [1, "B"]]
2794
+ * ```
2795
+ *
2796
+ * @since 3.18.0
2797
+ * @category models
2798
+ */
2799
+ export class Walker<T, N> implements Iterable<[T, N]> {
2800
+ // @ts-ignore
2801
+ readonly [Symbol.iterator]: () => Iterator<[T, N]>
2802
+
2803
+ /**
2804
+ * Visits each element and maps it to a value using the provided function.
2805
+ *
2806
+ * Takes a function that receives the index and data,
2807
+ * and returns an iterable of the mapped values. Skips elements that
2808
+ * no longer exist in the graph.
2809
+ *
2810
+ * @example
2811
+ * ```ts
2812
+ * import { Graph } from "effect"
2813
+ *
2814
+ * const graph = Graph.directed<string, number>((mutable) => {
2815
+ * const a = Graph.addNode(mutable, "A")
2816
+ * const b = Graph.addNode(mutable, "B")
2817
+ * Graph.addEdge(mutable, a, b, 1)
2818
+ * })
2819
+ *
2820
+ * const dfs = Graph.dfs(graph, { startNodes: [0] })
2821
+ *
2822
+ * // Map to just the node data
2823
+ * const values = Array.from(dfs.visit((index, data) => data))
2824
+ * console.log(values) // ["A", "B"]
2825
+ *
2826
+ * // Map to custom objects
2827
+ * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data })))
2828
+ * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }]
2829
+ * ```
2830
+ *
2831
+ * @since 3.18.0
2832
+ * @category iterators
2833
+ */
2834
+ readonly visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
2835
+
2836
+ constructor(
2837
+ /**
2838
+ * Visits each element and maps it to a value using the provided function.
2839
+ *
2840
+ * Takes a function that receives the index and data,
2841
+ * and returns an iterable of the mapped values. Skips elements that
2842
+ * no longer exist in the graph.
2843
+ *
2844
+ * @example
2845
+ * ```ts
2846
+ * import { Graph } from "effect"
2847
+ *
2848
+ * const graph = Graph.directed<string, number>((mutable) => {
2849
+ * const a = Graph.addNode(mutable, "A")
2850
+ * const b = Graph.addNode(mutable, "B")
2851
+ * Graph.addEdge(mutable, a, b, 1)
2852
+ * })
2853
+ *
2854
+ * const dfs = Graph.dfs(graph, { startNodes: [0] })
2855
+ *
2856
+ * // Map to just the node data
2857
+ * const values = Array.from(dfs.visit((index, data) => data))
2858
+ * console.log(values) // ["A", "B"]
2859
+ *
2860
+ * // Map to custom objects
2861
+ * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data })))
2862
+ * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }]
2863
+ * ```
2864
+ *
2865
+ * @since 3.18.0
2866
+ * @category iterators
2867
+ */
2868
+ visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
2869
+ ) {
2870
+ this.visit = visit
2871
+ this[Symbol.iterator] = visit((index, data) => [index, data] as [T, N])[Symbol.iterator]
2872
+ }
2873
+ }
2874
+
2875
+ /**
2876
+ * Type alias for node iteration using Walker.
2877
+ * NodeWalker is represented as Walker<NodeIndex, N>.
2878
+ *
2879
+ * @since 3.18.0
2880
+ * @category models
2881
+ */
2882
+ export type NodeWalker<N> = Walker<NodeIndex, N>
2883
+
2884
+ /**
2885
+ * Type alias for edge iteration using Walker.
2886
+ * EdgeWalker is represented as Walker<EdgeIndex, Edge<E>>.
2887
+ *
2888
+ * @since 3.18.0
2889
+ * @category models
2890
+ */
2891
+ export type EdgeWalker<E> = Walker<EdgeIndex, Edge<E>>
2892
+
2893
+ /**
2894
+ * Returns an iterator over the indices in the walker.
2895
+ *
2896
+ * @example
2897
+ * ```ts
2898
+ * import { Graph } from "effect"
2899
+ *
2900
+ * const graph = Graph.directed<string, number>((mutable) => {
2901
+ * const a = Graph.addNode(mutable, "A")
2902
+ * const b = Graph.addNode(mutable, "B")
2903
+ * Graph.addEdge(mutable, a, b, 1)
2904
+ * })
2905
+ *
2906
+ * const dfs = Graph.dfs(graph, { startNodes: [0] })
2907
+ * const indices = Array.from(Graph.indices(dfs))
2908
+ * console.log(indices) // [0, 1]
2909
+ * ```
2910
+ *
2911
+ * @since 3.18.0
2912
+ * @category utilities
2913
+ */
2914
+ export const indices = <T, N>(walker: Walker<T, N>): Iterable<T> => walker.visit((index, _) => index)
2915
+
2916
+ /**
2917
+ * Returns an iterator over the values (data) in the walker.
2918
+ *
2919
+ * @example
2920
+ * ```ts
2921
+ * import { Graph } from "effect"
2922
+ *
2923
+ * const graph = Graph.directed<string, number>((mutable) => {
2924
+ * const a = Graph.addNode(mutable, "A")
2925
+ * const b = Graph.addNode(mutable, "B")
2926
+ * Graph.addEdge(mutable, a, b, 1)
2927
+ * })
2928
+ *
2929
+ * const dfs = Graph.dfs(graph, { startNodes: [0] })
2930
+ * const values = Array.from(Graph.values(dfs))
2931
+ * console.log(values) // ["A", "B"]
2932
+ * ```
2933
+ *
2934
+ * @since 3.18.0
2935
+ * @category utilities
2936
+ */
2937
+ export const values = <T, N>(walker: Walker<T, N>): Iterable<N> => walker.visit((_, data) => data)
2938
+
2939
+ /**
2940
+ * Returns an iterator over [index, data] entries in the walker.
2941
+ *
2942
+ * @example
2943
+ * ```ts
2944
+ * import { Graph } from "effect"
2945
+ *
2946
+ * const graph = Graph.directed<string, number>((mutable) => {
2947
+ * const a = Graph.addNode(mutable, "A")
2948
+ * const b = Graph.addNode(mutable, "B")
2949
+ * Graph.addEdge(mutable, a, b, 1)
2950
+ * })
2951
+ *
2952
+ * const dfs = Graph.dfs(graph, { startNodes: [0] })
2953
+ * const entries = Array.from(Graph.entries(dfs))
2954
+ * console.log(entries) // [[0, "A"], [1, "B"]]
2955
+ * ```
2956
+ *
2957
+ * @since 3.18.0
2958
+ * @category utilities
2959
+ */
2960
+ export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
2961
+ walker.visit((index, data) => [index, data] as [T, N])
2962
+
2963
+ /**
2964
+ * Configuration options for DFS iterator.
2965
+ *
2966
+ * @since 3.18.0
2967
+ * @category models
2968
+ */
2969
+ export interface DfsConfig {
2970
+ readonly startNodes?: Array<NodeIndex>
2971
+ readonly direction?: Direction
2972
+ }
2973
+
2974
+ /**
2975
+ * Creates a new DFS iterator with optional configuration.
2976
+ *
2977
+ * The iterator maintains a stack of nodes to visit and tracks discovered nodes.
2978
+ * It provides lazy evaluation of the depth-first search.
2979
+ *
2980
+ * @example
2981
+ * ```ts
2982
+ * import { Graph } from "effect"
2983
+ *
2984
+ * const graph = Graph.directed<string, number>((mutable) => {
2985
+ * const a = Graph.addNode(mutable, "A")
2986
+ * const b = Graph.addNode(mutable, "B")
2987
+ * const c = Graph.addNode(mutable, "C")
2988
+ * Graph.addEdge(mutable, a, b, 1)
2989
+ * Graph.addEdge(mutable, b, c, 1)
2990
+ * })
2991
+ *
2992
+ * // Start from a specific node
2993
+ * const dfs1 = Graph.dfs(graph, { startNodes: [0] })
2994
+ * for (const nodeIndex of Graph.indices(dfs1)) {
2995
+ * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2
2996
+ * }
2997
+ *
2998
+ * // Empty iterator (no starting nodes)
2999
+ * const dfs2 = Graph.dfs(graph)
3000
+ * // Can be used programmatically
3001
+ * ```
3002
+ *
3003
+ * @since 3.18.0
3004
+ * @category iterators
3005
+ */
3006
+ export const dfs = <N, E, T extends Kind = "directed">(
3007
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3008
+ config: DfsConfig = {}
3009
+ ): NodeWalker<N> => {
3010
+ const startNodes = config.startNodes ?? []
3011
+ const direction = config.direction ?? "outgoing"
3012
+
3013
+ // Validate that all start nodes exist
3014
+ for (const nodeIndex of startNodes) {
3015
+ if (!hasNode(graph, nodeIndex)) {
3016
+ throw new Error(`Start node ${nodeIndex} does not exist`)
3017
+ }
3018
+ }
3019
+
3020
+ return new Walker((f) => ({
3021
+ [Symbol.iterator]: () => {
3022
+ const stack = [...startNodes]
3023
+ const discovered = new Set<NodeIndex>()
3024
+
3025
+ const nextMapped = () => {
3026
+ while (stack.length > 0) {
3027
+ const current = stack.pop()!
3028
+
3029
+ if (discovered.has(current)) {
3030
+ continue
3031
+ }
3032
+
3033
+ discovered.add(current)
3034
+
3035
+ const nodeDataOption = getMapSafe(graph.nodes, current)
3036
+ if (Option.isNone(nodeDataOption)) {
3037
+ continue
3038
+ }
3039
+
3040
+ const neighbors = neighborsDirected(graph, current, direction)
3041
+ for (let i = neighbors.length - 1; i >= 0; i--) {
3042
+ const neighbor = neighbors[i]
3043
+ if (!discovered.has(neighbor)) {
3044
+ stack.push(neighbor)
3045
+ }
3046
+ }
3047
+
3048
+ return { done: false, value: f(current, nodeDataOption.value) }
3049
+ }
3050
+
3051
+ return { done: true, value: undefined } as const
3052
+ }
3053
+
3054
+ return { next: nextMapped }
3055
+ }
3056
+ }))
3057
+ }
3058
+
3059
+ /**
3060
+ * Configuration options for BFS iterator.
3061
+ *
3062
+ * @since 3.18.0
3063
+ * @category models
3064
+ */
3065
+ export interface BfsConfig {
3066
+ readonly startNodes?: Array<NodeIndex>
3067
+ readonly direction?: Direction
3068
+ }
3069
+
3070
+ /**
3071
+ * Creates a new BFS iterator with optional configuration.
3072
+ *
3073
+ * The iterator maintains a queue of nodes to visit and tracks discovered nodes.
3074
+ * It provides lazy evaluation of the breadth-first search.
3075
+ *
3076
+ * @example
3077
+ * ```ts
3078
+ * import { Graph } from "effect"
3079
+ *
3080
+ * const graph = Graph.directed<string, number>((mutable) => {
3081
+ * const a = Graph.addNode(mutable, "A")
3082
+ * const b = Graph.addNode(mutable, "B")
3083
+ * const c = Graph.addNode(mutable, "C")
3084
+ * Graph.addEdge(mutable, a, b, 1)
3085
+ * Graph.addEdge(mutable, b, c, 1)
3086
+ * })
3087
+ *
3088
+ * // Start from a specific node
3089
+ * const bfs1 = Graph.bfs(graph, { startNodes: [0] })
3090
+ * for (const nodeIndex of Graph.indices(bfs1)) {
3091
+ * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2
3092
+ * }
3093
+ *
3094
+ * // Empty iterator (no starting nodes)
3095
+ * const bfs2 = Graph.bfs(graph)
3096
+ * // Can be used programmatically
3097
+ * ```
3098
+ *
3099
+ * @since 3.18.0
3100
+ * @category iterators
3101
+ */
3102
+ export const bfs = <N, E, T extends Kind = "directed">(
3103
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3104
+ config: BfsConfig = {}
3105
+ ): NodeWalker<N> => {
3106
+ const startNodes = config.startNodes ?? []
3107
+ const direction = config.direction ?? "outgoing"
3108
+
3109
+ // Validate that all start nodes exist
3110
+ for (const nodeIndex of startNodes) {
3111
+ if (!hasNode(graph, nodeIndex)) {
3112
+ throw new Error(`Start node ${nodeIndex} does not exist`)
3113
+ }
3114
+ }
3115
+
3116
+ return new Walker((f) => ({
3117
+ [Symbol.iterator]: () => {
3118
+ const queue = [...startNodes]
3119
+ const discovered = new Set<NodeIndex>()
3120
+
3121
+ const nextMapped = () => {
3122
+ while (queue.length > 0) {
3123
+ const current = queue.shift()!
3124
+
3125
+ if (!discovered.has(current)) {
3126
+ discovered.add(current)
3127
+
3128
+ const neighbors = neighborsDirected(graph, current, direction)
3129
+ for (const neighbor of neighbors) {
3130
+ if (!discovered.has(neighbor)) {
3131
+ queue.push(neighbor)
3132
+ }
3133
+ }
3134
+
3135
+ const nodeData = getNode(graph, current)
3136
+ if (Option.isSome(nodeData)) {
3137
+ return { done: false, value: f(current, nodeData.value) }
3138
+ }
3139
+ return nextMapped()
3140
+ }
3141
+ }
3142
+
3143
+ return { done: true, value: undefined } as const
3144
+ }
3145
+
3146
+ return { next: nextMapped }
3147
+ }
3148
+ }))
3149
+ }
3150
+
3151
+ /**
3152
+ * Configuration options for topological sort iterator.
3153
+ *
3154
+ * @since 3.18.0
3155
+ * @category models
3156
+ */
3157
+ export interface TopoConfig {
3158
+ readonly initials?: Array<NodeIndex>
3159
+ }
3160
+
3161
+ /**
3162
+ * Creates a new topological sort iterator with optional configuration.
3163
+ *
3164
+ * The iterator uses Kahn's algorithm to lazily produce nodes in topological order.
3165
+ * Throws an error if the graph contains cycles.
3166
+ *
3167
+ * @example
3168
+ * ```ts
3169
+ * import { Graph } from "effect"
3170
+ *
3171
+ * const graph = Graph.directed<string, number>((mutable) => {
3172
+ * const a = Graph.addNode(mutable, "A")
3173
+ * const b = Graph.addNode(mutable, "B")
3174
+ * const c = Graph.addNode(mutable, "C")
3175
+ * Graph.addEdge(mutable, a, b, 1)
3176
+ * Graph.addEdge(mutable, b, c, 1)
3177
+ * })
3178
+ *
3179
+ * // Standard topological sort
3180
+ * const topo1 = Graph.topo(graph)
3181
+ * for (const nodeIndex of Graph.indices(topo1)) {
3182
+ * console.log(nodeIndex) // 0, 1, 2 (topological order)
3183
+ * }
3184
+ *
3185
+ * // With initial nodes
3186
+ * const topo2 = Graph.topo(graph, { initials: [0] })
3187
+ *
3188
+ * // Throws error for cyclic graph
3189
+ * const cyclicGraph = Graph.directed<string, number>((mutable) => {
3190
+ * const a = Graph.addNode(mutable, "A")
3191
+ * const b = Graph.addNode(mutable, "B")
3192
+ * Graph.addEdge(mutable, a, b, 1)
3193
+ * Graph.addEdge(mutable, b, a, 2) // Creates cycle
3194
+ * })
3195
+ *
3196
+ * try {
3197
+ * Graph.topo(cyclicGraph) // Throws: "Cannot perform topological sort on cyclic graph"
3198
+ * } catch (error) {
3199
+ * console.log((error as Error).message)
3200
+ * }
3201
+ * ```
3202
+ *
3203
+ * @since 3.18.0
3204
+ * @category iterators
3205
+ */
3206
+ export const topo = <N, E, T extends Kind = "directed">(
3207
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3208
+ config: TopoConfig = {}
3209
+ ): NodeWalker<N> => {
3210
+ // Check if graph is acyclic first
3211
+ if (!isAcyclic(graph)) {
3212
+ throw new Error("Cannot perform topological sort on cyclic graph")
3213
+ }
3214
+
3215
+ const initials = config.initials ?? []
3216
+
3217
+ // Validate that all initial nodes exist
3218
+ for (const nodeIndex of initials) {
3219
+ if (!hasNode(graph, nodeIndex)) {
3220
+ throw new Error(`Initial node ${nodeIndex} does not exist`)
3221
+ }
3222
+ }
3223
+
3224
+ return new Walker((f) => ({
3225
+ [Symbol.iterator]: () => {
3226
+ const inDegree = new Map<NodeIndex, number>()
3227
+ const remaining = new Set<NodeIndex>()
3228
+ const queue = [...initials]
3229
+
3230
+ // Initialize in-degree counts
3231
+ for (const [nodeIndex] of graph.nodes) {
3232
+ inDegree.set(nodeIndex, 0)
3233
+ remaining.add(nodeIndex)
3234
+ }
3235
+
3236
+ // Calculate in-degrees
3237
+ for (const [, edgeData] of graph.edges) {
3238
+ const currentInDegree = inDegree.get(edgeData.target) || 0
3239
+ inDegree.set(edgeData.target, currentInDegree + 1)
3240
+ }
3241
+
3242
+ // Add nodes with zero in-degree to queue if no initials provided
3243
+ if (initials.length === 0) {
3244
+ for (const [nodeIndex, degree] of inDegree) {
3245
+ if (degree === 0) {
3246
+ queue.push(nodeIndex)
3247
+ }
3248
+ }
3249
+ }
3250
+
3251
+ const nextMapped = () => {
3252
+ while (queue.length > 0) {
3253
+ const current = queue.shift()!
3254
+
3255
+ if (remaining.has(current)) {
3256
+ remaining.delete(current)
3257
+
3258
+ // Process outgoing edges, reducing in-degree of targets
3259
+ const neighbors = neighborsDirected(graph, current, "outgoing")
3260
+ for (const neighbor of neighbors) {
3261
+ if (remaining.has(neighbor)) {
3262
+ const currentInDegree = inDegree.get(neighbor) || 0
3263
+ const newInDegree = currentInDegree - 1
3264
+ inDegree.set(neighbor, newInDegree)
3265
+
3266
+ // If in-degree becomes 0, add to queue
3267
+ if (newInDegree === 0) {
3268
+ queue.push(neighbor)
3269
+ }
3270
+ }
3271
+ }
3272
+
3273
+ const nodeData = getNode(graph, current)
3274
+ if (Option.isSome(nodeData)) {
3275
+ return { done: false, value: f(current, nodeData.value) }
3276
+ }
3277
+ return nextMapped()
3278
+ }
3279
+ }
3280
+
3281
+ return { done: true, value: undefined } as const
3282
+ }
3283
+
3284
+ return { next: nextMapped }
3285
+ }
3286
+ }))
3287
+ }
3288
+
3289
+ /**
3290
+ * Configuration options for DFS postorder iterator.
3291
+ *
3292
+ * @since 3.18.0
3293
+ * @category models
3294
+ */
3295
+ export interface DfsPostOrderConfig {
3296
+ readonly startNodes?: Array<NodeIndex>
3297
+ readonly direction?: Direction
3298
+ }
3299
+
3300
+ /**
3301
+ * Creates a new DFS postorder iterator with optional configuration.
3302
+ *
3303
+ * The iterator maintains a stack with visit state tracking and emits nodes
3304
+ * in postorder (after all descendants have been processed). Essential for
3305
+ * dependency resolution and tree destruction algorithms.
3306
+ *
3307
+ * @example
3308
+ * ```ts
3309
+ * import { Graph } from "effect"
3310
+ *
3311
+ * const graph = Graph.directed<string, number>((mutable) => {
3312
+ * const root = Graph.addNode(mutable, "root")
3313
+ * const child1 = Graph.addNode(mutable, "child1")
3314
+ * const child2 = Graph.addNode(mutable, "child2")
3315
+ * Graph.addEdge(mutable, root, child1, 1)
3316
+ * Graph.addEdge(mutable, root, child2, 1)
3317
+ * })
3318
+ *
3319
+ * // Postorder: children before parents
3320
+ * const postOrder = Graph.dfsPostOrder(graph, { startNodes: [0] })
3321
+ * for (const node of postOrder) {
3322
+ * console.log(node) // 1, 2, 0
3323
+ * }
3324
+ * ```
3325
+ *
3326
+ * @since 3.18.0
3327
+ * @category iterators
3328
+ */
3329
+ export const dfsPostOrder = <N, E, T extends Kind = "directed">(
3330
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3331
+ config: DfsPostOrderConfig = {}
3332
+ ): NodeWalker<N> => {
3333
+ const startNodes = config.startNodes ?? []
3334
+ const direction = config.direction ?? "outgoing"
3335
+
3336
+ // Validate that all start nodes exist
3337
+ for (const nodeIndex of startNodes) {
3338
+ if (!hasNode(graph, nodeIndex)) {
3339
+ throw new Error(`Start node ${nodeIndex} does not exist`)
3340
+ }
3341
+ }
3342
+
3343
+ return new Walker((f) => ({
3344
+ [Symbol.iterator]: () => {
3345
+ const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = []
3346
+ const discovered = new Set<NodeIndex>()
3347
+ const finished = new Set<NodeIndex>()
3348
+
3349
+ // Initialize stack with start nodes
3350
+ for (let i = startNodes.length - 1; i >= 0; i--) {
3351
+ stack.push({ node: startNodes[i], visitedChildren: false })
3352
+ }
3353
+
3354
+ const nextMapped = () => {
3355
+ while (stack.length > 0) {
3356
+ const current = stack[stack.length - 1]
3357
+
3358
+ if (!discovered.has(current.node)) {
3359
+ discovered.add(current.node)
3360
+ current.visitedChildren = false
3361
+ }
3362
+
3363
+ if (!current.visitedChildren) {
3364
+ current.visitedChildren = true
3365
+ const neighbors = neighborsDirected(graph, current.node, direction)
3366
+
3367
+ for (let i = neighbors.length - 1; i >= 0; i--) {
3368
+ const neighbor = neighbors[i]
3369
+ if (!discovered.has(neighbor) && !finished.has(neighbor)) {
3370
+ stack.push({ node: neighbor, visitedChildren: false })
3371
+ }
3372
+ }
3373
+ } else {
3374
+ const nodeToEmit = stack.pop()!.node
3375
+
3376
+ if (!finished.has(nodeToEmit)) {
3377
+ finished.add(nodeToEmit)
3378
+
3379
+ const nodeData = getNode(graph, nodeToEmit)
3380
+ if (Option.isSome(nodeData)) {
3381
+ return { done: false, value: f(nodeToEmit, nodeData.value) }
3382
+ }
3383
+ return nextMapped()
3384
+ }
3385
+ }
3386
+ }
3387
+
3388
+ return { done: true, value: undefined } as const
3389
+ }
3390
+
3391
+ return { next: nextMapped }
3392
+ }
3393
+ }))
3394
+ }
3395
+
3396
+ /**
3397
+ * Creates an iterator over all node indices in the graph.
3398
+ *
3399
+ * The iterator produces node indices in the order they were added to the graph.
3400
+ * This provides access to all nodes regardless of connectivity.
3401
+ *
3402
+ * @example
3403
+ * ```ts
3404
+ * import { Graph } from "effect"
3405
+ *
3406
+ * const graph = Graph.directed<string, number>((mutable) => {
3407
+ * const a = Graph.addNode(mutable, "A")
3408
+ * const b = Graph.addNode(mutable, "B")
3409
+ * const c = Graph.addNode(mutable, "C")
3410
+ * Graph.addEdge(mutable, a, b, 1)
3411
+ * })
3412
+ *
3413
+ * const indices = Array.from(Graph.indices(Graph.nodes(graph)))
3414
+ * console.log(indices) // [0, 1, 2]
3415
+ * ```
3416
+ *
3417
+ * @since 3.18.0
3418
+ * @category iterators
3419
+ */
3420
+ export const nodes = <N, E, T extends Kind = "directed">(
3421
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
3422
+ ): NodeWalker<N> =>
3423
+ new Walker((f) => ({
3424
+ [Symbol.iterator]() {
3425
+ const nodeMap = graph.nodes
3426
+ const iterator = nodeMap.entries()
3427
+
3428
+ return {
3429
+ next() {
3430
+ const result = iterator.next()
3431
+ if (result.done) {
3432
+ return { done: true, value: undefined }
3433
+ }
3434
+ const [nodeIndex, nodeData] = result.value
3435
+ return { done: false, value: f(nodeIndex, nodeData) }
3436
+ }
3437
+ }
3438
+ }
3439
+ }))
3440
+
3441
+ /**
3442
+ * Creates an iterator over all edge indices in the graph.
3443
+ *
3444
+ * The iterator produces edge indices in the order they were added to the graph.
3445
+ * This provides access to all edges regardless of connectivity.
3446
+ *
3447
+ * @example
3448
+ * ```ts
3449
+ * import { Graph } from "effect"
3450
+ *
3451
+ * const graph = Graph.directed<string, number>((mutable) => {
3452
+ * const a = Graph.addNode(mutable, "A")
3453
+ * const b = Graph.addNode(mutable, "B")
3454
+ * const c = Graph.addNode(mutable, "C")
3455
+ * Graph.addEdge(mutable, a, b, 1)
3456
+ * Graph.addEdge(mutable, b, c, 2)
3457
+ * })
3458
+ *
3459
+ * const indices = Array.from(Graph.indices(Graph.edges(graph)))
3460
+ * console.log(indices) // [0, 1]
3461
+ * ```
3462
+ *
3463
+ * @since 3.18.0
3464
+ * @category iterators
3465
+ */
3466
+ export const edges = <N, E, T extends Kind = "directed">(
3467
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
3468
+ ): EdgeWalker<E> =>
3469
+ new Walker((f) => ({
3470
+ [Symbol.iterator]() {
3471
+ const edgeMap = graph.edges
3472
+ const iterator = edgeMap.entries()
3473
+
3474
+ return {
3475
+ next() {
3476
+ const result = iterator.next()
3477
+ if (result.done) {
3478
+ return { done: true, value: undefined }
3479
+ }
3480
+ const [edgeIndex, edgeData] = result.value
3481
+ return { done: false, value: f(edgeIndex, edgeData) }
3482
+ }
3483
+ }
3484
+ }
3485
+ }))
3486
+
3487
+ /**
3488
+ * Configuration for externals iterator.
3489
+ *
3490
+ * @since 3.18.0
3491
+ * @category models
3492
+ */
3493
+ export interface ExternalsConfig {
3494
+ readonly direction?: Direction
3495
+ }
3496
+
3497
+ /**
3498
+ * Creates an iterator over external nodes (nodes without edges in specified direction).
3499
+ *
3500
+ * External nodes are nodes that have no outgoing edges (direction="outgoing") or
3501
+ * no incoming edges (direction="incoming"). These are useful for finding
3502
+ * sources, sinks, or isolated nodes.
3503
+ *
3504
+ * @example
3505
+ * ```ts
3506
+ * import { Graph } from "effect"
3507
+ *
3508
+ * const graph = Graph.directed<string, number>((mutable) => {
3509
+ * const source = Graph.addNode(mutable, "source") // 0 - no incoming
3510
+ * const middle = Graph.addNode(mutable, "middle") // 1 - has both
3511
+ * const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing
3512
+ * const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges
3513
+ *
3514
+ * Graph.addEdge(mutable, source, middle, 1)
3515
+ * Graph.addEdge(mutable, middle, sink, 2)
3516
+ * })
3517
+ *
3518
+ * // Nodes with no outgoing edges (sinks + isolated)
3519
+ * const sinks = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" })))
3520
+ * console.log(sinks) // [2, 3]
3521
+ *
3522
+ * // Nodes with no incoming edges (sources + isolated)
3523
+ * const sources = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" })))
3524
+ * console.log(sources) // [0, 3]
3525
+ * ```
3526
+ *
3527
+ * @since 3.18.0
3528
+ * @category iterators
3529
+ */
3530
+ export const externals = <N, E, T extends Kind = "directed">(
3531
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3532
+ config: ExternalsConfig = {}
3533
+ ): NodeWalker<N> => {
3534
+ const direction = config.direction ?? "outgoing"
3535
+
3536
+ return new Walker((f) => ({
3537
+ [Symbol.iterator]: () => {
3538
+ const nodeMap = graph.nodes
3539
+ const adjacencyMap = direction === "incoming"
3540
+ ? graph.reverseAdjacency
3541
+ : graph.adjacency
3542
+
3543
+ const nodeIterator = nodeMap.entries()
3544
+
3545
+ const nextMapped = () => {
3546
+ let current = nodeIterator.next()
3547
+ while (!current.done) {
3548
+ const [nodeIndex, nodeData] = current.value
3549
+ const adjacencyList = getMapSafe(adjacencyMap, nodeIndex)
3550
+
3551
+ // Node is external if it has no edges in the specified direction
3552
+ if (Option.isNone(adjacencyList) || adjacencyList.value.length === 0) {
3553
+ return { done: false, value: f(nodeIndex, nodeData) }
3554
+ }
3555
+ current = nodeIterator.next()
3556
+ }
3557
+
3558
+ return { done: true, value: undefined } as const
3559
+ }
3560
+
3561
+ return { next: nextMapped }
3562
+ }
3563
+ }))
3564
+ }