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