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