effect 4.0.0-beta.98 → 4.0.0-beta.99

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 (83) hide show
  1. package/README.md +1 -3
  2. package/dist/Array.js +1 -1
  3. package/dist/Array.js.map +1 -1
  4. package/dist/Graph.d.ts +1187 -196
  5. package/dist/Graph.d.ts.map +1 -1
  6. package/dist/Graph.js +1031 -283
  7. package/dist/Graph.js.map +1 -1
  8. package/dist/Iterable.d.ts.map +1 -1
  9. package/dist/Iterable.js +41 -1
  10. package/dist/Iterable.js.map +1 -1
  11. package/dist/Schema.js +1 -1
  12. package/dist/Schema.js.map +1 -1
  13. package/dist/internal/effect.js +9 -3
  14. package/dist/internal/effect.js.map +1 -1
  15. package/dist/unstable/ai/McpSchema.d.ts +128 -128
  16. package/dist/unstable/ai/McpSchema.d.ts.map +1 -1
  17. package/dist/unstable/ai/Tool.d.ts.map +1 -1
  18. package/dist/unstable/ai/Tool.js +11 -13
  19. package/dist/unstable/ai/Tool.js.map +1 -1
  20. package/dist/unstable/cli/CliConfig.d.ts +76 -0
  21. package/dist/unstable/cli/CliConfig.d.ts.map +1 -0
  22. package/dist/unstable/cli/CliConfig.js +63 -0
  23. package/dist/unstable/cli/CliConfig.js.map +1 -0
  24. package/dist/unstable/cli/CliError.d.ts.map +1 -1
  25. package/dist/unstable/cli/CliError.js +3 -0
  26. package/dist/unstable/cli/CliError.js.map +1 -1
  27. package/dist/unstable/cli/Command.d.ts +36 -1
  28. package/dist/unstable/cli/Command.d.ts.map +1 -1
  29. package/dist/unstable/cli/Command.js +61 -4
  30. package/dist/unstable/cli/Command.js.map +1 -1
  31. package/dist/unstable/cli/GlobalFlag.d.ts +23 -2
  32. package/dist/unstable/cli/GlobalFlag.d.ts.map +1 -1
  33. package/dist/unstable/cli/GlobalFlag.js +20 -4
  34. package/dist/unstable/cli/GlobalFlag.js.map +1 -1
  35. package/dist/unstable/cli/Param.js +6 -2
  36. package/dist/unstable/cli/Param.js.map +1 -1
  37. package/dist/unstable/cli/index.d.ts +4 -0
  38. package/dist/unstable/cli/index.d.ts.map +1 -1
  39. package/dist/unstable/cli/index.js +4 -0
  40. package/dist/unstable/cli/index.js.map +1 -1
  41. package/dist/unstable/cli/internal/ansi.d.ts.map +1 -1
  42. package/dist/unstable/cli/internal/ansi.js +2 -0
  43. package/dist/unstable/cli/internal/ansi.js.map +1 -1
  44. package/dist/unstable/cli/internal/parser.js +11 -2
  45. package/dist/unstable/cli/internal/parser.js.map +1 -1
  46. package/dist/unstable/cli/internal/wizard.d.ts +13 -0
  47. package/dist/unstable/cli/internal/wizard.d.ts.map +1 -0
  48. package/dist/unstable/cli/internal/wizard.js +212 -0
  49. package/dist/unstable/cli/internal/wizard.js.map +1 -0
  50. package/dist/unstable/cluster/RunnerStorage.js +1 -1
  51. package/dist/unstable/cluster/RunnerStorage.js.map +1 -1
  52. package/dist/unstable/http/Multipart.d.ts.map +1 -1
  53. package/dist/unstable/http/Multipart.js +3 -1
  54. package/dist/unstable/http/Multipart.js.map +1 -1
  55. package/dist/unstable/httpapi/HttpApiBuilder.d.ts +1 -1
  56. package/dist/unstable/httpapi/HttpApiBuilder.d.ts.map +1 -1
  57. package/dist/unstable/persistence/Redis.d.ts.map +1 -1
  58. package/dist/unstable/persistence/Redis.js +5 -3
  59. package/dist/unstable/persistence/Redis.js.map +1 -1
  60. package/dist/unstable/sql/SqlClient.d.ts +1 -2
  61. package/dist/unstable/sql/SqlClient.d.ts.map +1 -1
  62. package/dist/unstable/sql/SqlClient.js.map +1 -1
  63. package/package.json +3 -3
  64. package/src/Array.ts +1 -1
  65. package/src/Graph.ts +2967 -457
  66. package/src/Iterable.ts +32 -2
  67. package/src/Schema.ts +1 -1
  68. package/src/internal/effect.ts +9 -3
  69. package/src/unstable/ai/Tool.ts +13 -16
  70. package/src/unstable/cli/CliConfig.ts +85 -0
  71. package/src/unstable/cli/CliError.ts +3 -0
  72. package/src/unstable/cli/Command.ts +83 -7
  73. package/src/unstable/cli/GlobalFlag.ts +33 -5
  74. package/src/unstable/cli/Param.ts +15 -3
  75. package/src/unstable/cli/index.ts +5 -0
  76. package/src/unstable/cli/internal/ansi.ts +3 -0
  77. package/src/unstable/cli/internal/parser.ts +14 -2
  78. package/src/unstable/cli/internal/wizard.ts +283 -0
  79. package/src/unstable/cluster/RunnerStorage.ts +1 -1
  80. package/src/unstable/http/Multipart.ts +3 -1
  81. package/src/unstable/httpapi/HttpApiBuilder.ts +1 -1
  82. package/src/unstable/persistence/Redis.ts +9 -4
  83. package/src/unstable/sql/SqlClient.ts +1 -2
package/dist/Graph.js CHANGED
@@ -13,6 +13,7 @@ import * as Equal from "./Equal.js";
13
13
  import { dual } from "./Function.js";
14
14
  import * as Hash from "./Hash.js";
15
15
  import { NodeInspectSymbol } from "./Inspectable.js";
16
+ import * as MutableHashMap from "./MutableHashMap.js";
16
17
  import * as Option from "./Option.js";
17
18
  import { pipeArguments } from "./Pipeable.js";
18
19
  import { hasProperty } from "./Predicate.js";
@@ -33,12 +34,29 @@ const TypeId = "~effect/collections/Graph";
33
34
  * @since 3.18.0
34
35
  */
35
36
  export class Edge extends Data.Class {}
37
+ /** @internal */
38
+ const graphImpl = graph => graph;
39
+ /** @internal */
40
+ const cloneAdjacency = adjacency => {
41
+ const cloned = new Map();
42
+ for (const [nodeIndex, edges] of adjacency) {
43
+ cloned.set(nodeIndex, [...edges]);
44
+ }
45
+ return cloned;
46
+ };
36
47
  // =============================================================================
37
48
  // Proto Objects
38
49
  // =============================================================================
39
50
  /** @internal */
51
+ const edgeEquals = (type, self, that) => (type === "directed" ? self.source === that.source && self.target === that.target : self.source === that.source && self.target === that.target || self.source === that.target && self.target === that.source) && Equal.equals(self.data, that.data);
52
+ /** @internal */
53
+ const edgeHash = (type, edge) => type === "directed" ? Hash.hash(edge) : Hash.optimize(Hash.hash(edge.data) ^ Hash.hash(edge.source) + Hash.hash(edge.target));
54
+ /** @internal */
40
55
  const ProtoGraph = {
41
- [TypeId]: TypeId,
56
+ [TypeId]: {
57
+ _N: _ => _,
58
+ _E: _ => _
59
+ },
42
60
  [Symbol.iterator]() {
43
61
  return this.nodes[Symbol.iterator]();
44
62
  },
@@ -47,26 +65,27 @@ const ProtoGraph = {
47
65
  },
48
66
  [Equal.symbol](that) {
49
67
  if (isGraph(that)) {
50
- if (this.nodes.size !== that.nodes.size || this.edges.size !== that.edges.size || this.type !== that.type) {
68
+ const thatImpl = graphImpl(that);
69
+ if (this.nodes.size !== thatImpl.nodes.size || this.edges.size !== thatImpl.edges.size || this.nextNodeIndex !== thatImpl.nextNodeIndex || this.nextEdgeIndex !== thatImpl.nextEdgeIndex || this.type !== thatImpl.type) {
51
70
  return false;
52
71
  }
53
72
  // Compare nodes
54
73
  for (const [nodeIndex, nodeData] of this.nodes) {
55
- if (!that.nodes.has(nodeIndex)) {
74
+ if (!thatImpl.nodes.has(nodeIndex)) {
56
75
  return false;
57
76
  }
58
- const otherNodeData = that.nodes.get(nodeIndex);
77
+ const otherNodeData = thatImpl.nodes.get(nodeIndex);
59
78
  if (!Equal.equals(nodeData, otherNodeData)) {
60
79
  return false;
61
80
  }
62
81
  }
63
82
  // Compare edges
64
83
  for (const [edgeIndex, edgeData] of this.edges) {
65
- if (!that.edges.has(edgeIndex)) {
84
+ if (!thatImpl.edges.has(edgeIndex)) {
66
85
  return false;
67
86
  }
68
- const otherEdge = that.edges.get(edgeIndex);
69
- if (!Equal.equals(edgeData, otherEdge)) {
87
+ const otherEdge = thatImpl.edges.get(edgeIndex);
88
+ if (!edgeEquals(this.type, edgeData, otherEdge)) {
70
89
  return false;
71
90
  }
72
91
  }
@@ -79,11 +98,13 @@ const ProtoGraph = {
79
98
  hash = hash ^ Hash.string(this.type);
80
99
  hash = hash ^ Hash.number(this.nodes.size);
81
100
  hash = hash ^ Hash.number(this.edges.size);
101
+ hash = hash ^ Hash.number(this.nextNodeIndex);
102
+ hash = hash ^ Hash.number(this.nextEdgeIndex);
82
103
  for (const [nodeIndex, nodeData] of this.nodes) {
83
104
  hash = hash ^ Hash.hash(nodeIndex) + Hash.hash(nodeData);
84
105
  }
85
106
  for (const [edgeIndex, edgeData] of this.edges) {
86
- hash = hash ^ Hash.hash(edgeIndex) + Hash.hash(edgeData);
107
+ hash = hash ^ Hash.hash(edgeIndex) + edgeHash(this.type, edgeData);
87
108
  }
88
109
  return hash;
89
110
  },
@@ -124,12 +145,20 @@ export class GraphError extends /*#__PURE__*/Data.TaggedError("GraphError") {}
124
145
  const missingNode = node => new GraphError({
125
146
  message: `Node ${node} does not exist`
126
147
  });
148
+ /** @internal */
149
+ function assertMutable(graph) {
150
+ if (!graph.mutable) {
151
+ throw new GraphError({
152
+ message: "Graph is not mutable"
153
+ });
154
+ }
155
+ }
127
156
  // =============================================================================
128
157
  // Constructors
129
158
  // =============================================================================
130
159
  /**
131
160
  * Returns `true` if a value has the graph runtime type identifier, narrowing
132
- * it to a `Graph`.
161
+ * it to an immutable or mutable graph.
133
162
  *
134
163
  * **When to use**
135
164
  *
@@ -138,36 +167,42 @@ const missingNode = node => new GraphError({
138
167
  * **Gotchas**
139
168
  *
140
169
  * This guard checks the shared graph runtime type identifier and does not
141
- * distinguish immutable graphs from mutable graphs.
170
+ * distinguish immutable graphs from mutable graphs or directed graphs from
171
+ * undirected graphs.
142
172
  *
143
173
  * @category guards
144
174
  * @since 4.0.0
145
175
  */
146
176
  export const isGraph = u => hasProperty(u, TypeId);
147
177
  /**
148
- * Creates a directed graph, optionally with initial mutations.
178
+ * Creates a graph constructor for the specified graph kind.
149
179
  *
150
- * **Example** (Creating a directed graph)
180
+ * **When to use**
181
+ *
182
+ * Use when the graph kind is selected dynamically. Prefer `directed` or
183
+ * `undirected` when the kind is known statically.
184
+ *
185
+ * **Example** (Constructing by kind)
151
186
  *
152
187
  * ```ts
153
188
  * import { Graph } from "effect"
154
189
  *
155
- * // Directed graph with initial nodes and edges
156
- * const graph = Graph.directed<string, string>((mutable) => {
157
- * const a = Graph.addNode(mutable, "A")
158
- * const b = Graph.addNode(mutable, "B")
159
- * const c = Graph.addNode(mutable, "C")
160
- * Graph.addEdge(mutable, a, b, "A->B")
161
- * Graph.addEdge(mutable, b, c, "B->C")
190
+ * const makeGraph = Graph.make("directed")
191
+ * const graph = makeGraph<string, number>((mutable) => {
192
+ * Graph.addNode(mutable, "A")
162
193
  * })
194
+ *
195
+ * console.log(graph.type) // "directed"
163
196
  * ```
164
197
  *
198
+ * @see {@link directed} for constructing a directed graph directly
199
+ * @see {@link undirected} for constructing an undirected graph directly
165
200
  * @category constructors
166
- * @since 3.18.0
201
+ * @since 4.0.0
167
202
  */
168
- export const directed = mutate => {
203
+ export const make = type => mutate => {
169
204
  const graph = Object.create(ProtoGraph);
170
- graph.type = "directed";
205
+ graph.type = type;
171
206
  graph.nodes = new Map();
172
207
  graph.edges = new Map();
173
208
  graph.adjacency = new Map();
@@ -175,14 +210,36 @@ export const directed = mutate => {
175
210
  graph.nextNodeIndex = 0;
176
211
  graph.nextEdgeIndex = 0;
177
212
  graph.acyclic = Option.some(true);
178
- graph.mutable = false;
179
- if (mutate) {
180
- const mutable = beginMutation(graph);
181
- mutate(mutable);
182
- return endMutation(mutable);
213
+ if (mutate === undefined) {
214
+ graph.mutable = false;
215
+ return graph;
183
216
  }
184
- return graph;
217
+ graph.mutable = true;
218
+ const mutable = Equal.byReferenceUnsafe(graph);
219
+ return mutateScoped(mutable, mutate);
185
220
  };
221
+ /**
222
+ * Creates a directed graph, optionally with initial mutations.
223
+ *
224
+ * **Example** (Creating a directed graph)
225
+ *
226
+ * ```ts
227
+ * import { Graph } from "effect"
228
+ *
229
+ * // Directed graph with initial nodes and edges
230
+ * const graph = Graph.directed<string, string>((mutable) => {
231
+ * const a = Graph.addNode(mutable, "A")
232
+ * const b = Graph.addNode(mutable, "B")
233
+ * const c = Graph.addNode(mutable, "C")
234
+ * Graph.addEdge(mutable, a, b, "A->B")
235
+ * Graph.addEdge(mutable, b, c, "B->C")
236
+ * })
237
+ * ```
238
+ *
239
+ * @category constructors
240
+ * @since 3.18.0
241
+ */
242
+ export const directed = /*#__PURE__*/make("directed");
186
243
  /**
187
244
  * Creates an undirected graph, optionally with initial mutations.
188
245
  *
@@ -204,24 +261,7 @@ export const directed = mutate => {
204
261
  * @category constructors
205
262
  * @since 3.18.0
206
263
  */
207
- export const undirected = mutate => {
208
- const graph = Object.create(ProtoGraph);
209
- graph.type = "undirected";
210
- graph.nodes = new Map();
211
- graph.edges = new Map();
212
- graph.adjacency = new Map();
213
- graph.reverseAdjacency = new Map();
214
- graph.nextNodeIndex = 0;
215
- graph.nextEdgeIndex = 0;
216
- graph.acyclic = Option.some(true);
217
- graph.mutable = false;
218
- if (mutate) {
219
- const mutable = beginMutation(graph);
220
- mutate(mutable);
221
- return endMutation(mutable);
222
- }
223
- return graph;
224
- };
264
+ export const undirected = /*#__PURE__*/make("undirected");
225
265
  // =============================================================================
226
266
  // Scoped Mutable API
227
267
  // =============================================================================
@@ -242,30 +282,29 @@ export const undirected = mutate => {
242
282
  * @since 3.18.0
243
283
  */
244
284
  export const beginMutation = graph => {
245
- // Copy adjacency maps with deep cloned arrays
246
- const adjacency = new Map();
247
- const reverseAdjacency = new Map();
248
- for (const [nodeIndex, edges] of graph.adjacency) {
249
- adjacency.set(nodeIndex, [...edges]);
250
- }
251
- for (const [nodeIndex, edges] of graph.reverseAdjacency) {
252
- reverseAdjacency.set(nodeIndex, [...edges]);
253
- }
285
+ const source = graphImpl(graph);
286
+ const adjacency = cloneAdjacency(source.adjacency);
287
+ const reverseAdjacency = cloneAdjacency(source.reverseAdjacency);
254
288
  const mutable = Object.create(ProtoGraph);
255
- mutable.type = graph.type;
256
- mutable.nodes = new Map(graph.nodes);
257
- mutable.edges = new Map(graph.edges);
289
+ mutable.type = source.type;
290
+ mutable.nodes = new Map(source.nodes);
291
+ mutable.edges = new Map(source.edges);
258
292
  mutable.adjacency = adjacency;
259
293
  mutable.reverseAdjacency = reverseAdjacency;
260
- mutable.nextNodeIndex = graph.nextNodeIndex;
261
- mutable.nextEdgeIndex = graph.nextEdgeIndex;
262
- mutable.acyclic = graph.acyclic;
294
+ mutable.nextNodeIndex = source.nextNodeIndex;
295
+ mutable.nextEdgeIndex = source.nextEdgeIndex;
296
+ mutable.acyclic = source.acyclic;
263
297
  mutable.mutable = true;
264
- return mutable;
298
+ return Equal.byReferenceUnsafe(mutable);
265
299
  };
266
300
  /**
267
301
  * Converts a mutable graph back to an immutable graph, ending the mutation scope.
268
302
  *
303
+ * **Details**
304
+ *
305
+ * Finalizes the mutable handle. Later public mutation operations on that handle
306
+ * fail with a `GraphError`.
307
+ *
269
308
  * **Example** (Ending a mutation scope)
270
309
  *
271
310
  * ```ts
@@ -281,16 +320,29 @@ export const beginMutation = graph => {
281
320
  * @since 3.18.0
282
321
  */
283
322
  export const endMutation = mutable => {
323
+ assertMutable(mutable);
324
+ const source = graphImpl(mutable);
284
325
  const graph = Object.create(ProtoGraph);
285
- graph.type = mutable.type;
286
- graph.nodes = new Map(mutable.nodes);
287
- graph.edges = new Map(mutable.edges);
288
- graph.adjacency = mutable.adjacency;
289
- graph.reverseAdjacency = mutable.reverseAdjacency;
290
- graph.nextNodeIndex = mutable.nextNodeIndex;
291
- graph.nextEdgeIndex = mutable.nextEdgeIndex;
292
- graph.acyclic = mutable.acyclic;
326
+ graph.type = source.type;
327
+ graph.nodes = new Map(source.nodes);
328
+ graph.edges = new Map(source.edges);
329
+ graph.adjacency = cloneAdjacency(source.adjacency);
330
+ graph.reverseAdjacency = cloneAdjacency(source.reverseAdjacency);
331
+ graph.nextNodeIndex = source.nextNodeIndex;
332
+ graph.nextEdgeIndex = source.nextEdgeIndex;
333
+ graph.acyclic = source.acyclic;
293
334
  graph.mutable = false;
335
+ source.mutable = false;
336
+ return graph;
337
+ };
338
+ /** @internal */
339
+ const mutateScoped = (mutable, f) => {
340
+ let graph;
341
+ try {
342
+ f(mutable);
343
+ } finally {
344
+ graph = endMutation(mutable);
345
+ }
294
346
  return graph;
295
347
  };
296
348
  /**
@@ -317,8 +369,529 @@ export const endMutation = mutable => {
317
369
  */
318
370
  export const mutate = /*#__PURE__*/dual(2, (graph, f) => {
319
371
  const mutable = beginMutation(graph);
320
- f(mutable);
321
- return endMutation(mutable);
372
+ return mutateScoped(mutable, f);
373
+ });
374
+ /** @internal */
375
+ class EdgeIdentity {
376
+ type;
377
+ source;
378
+ target;
379
+ identity;
380
+ constructor(type, source, target, identity) {
381
+ this.type = type;
382
+ this.source = source;
383
+ this.target = target;
384
+ this.identity = identity;
385
+ }
386
+ [Equal.symbol](that) {
387
+ if (!(that instanceof EdgeIdentity) || this.type !== that.type || !Equal.equals(this.identity, that.identity)) {
388
+ return false;
389
+ }
390
+ if (this.type === "directed") {
391
+ return Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target);
392
+ }
393
+ return Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target) || Equal.equals(this.source, that.target) && Equal.equals(this.target, that.source);
394
+ }
395
+ [Hash.symbol]() {
396
+ const hash = Hash.hash(this.identity);
397
+ return this.type === "directed" ? Hash.combine(Hash.hash(this.target))(Hash.combine(Hash.hash(this.source))(hash)) : Hash.optimize(hash ^ Hash.hash(this.source) + Hash.hash(this.target));
398
+ }
399
+ }
400
+ /** @internal */
401
+ const buildNodeMaps = (graph, identity) => {
402
+ const impl = graphImpl(graph);
403
+ const byIdentity = MutableHashMap.empty();
404
+ const byIndex = new Map();
405
+ for (const [index, data] of impl.nodes) {
406
+ const nodeIdentity = identity(data);
407
+ MutableHashMap.set(byIdentity, nodeIdentity, data);
408
+ byIndex.set(index, nodeIdentity);
409
+ }
410
+ return {
411
+ byIdentity,
412
+ byIndex
413
+ };
414
+ };
415
+ /** @internal */
416
+ const nodeIdentityAt = (maps, index) => maps.byIndex.get(index);
417
+ /** @internal */
418
+ const buildEdgeMap = (graph, nodeMaps, identity) => {
419
+ const impl = graphImpl(graph);
420
+ const edges = MutableHashMap.empty();
421
+ for (const edge of impl.edges.values()) {
422
+ const sourceIdentity = nodeIdentityAt(nodeMaps, edge.source);
423
+ const targetIdentity = nodeIdentityAt(nodeMaps, edge.target);
424
+ MutableHashMap.set(edges, new EdgeIdentity(graph.type, sourceIdentity, targetIdentity, identity(edge.data)), edge.data);
425
+ }
426
+ return edges;
427
+ };
428
+ /** @internal */
429
+ const addNodesByIdentity = (mutable, nodes) => {
430
+ const indexByIdentity = MutableHashMap.empty();
431
+ for (const [identity, data] of nodes) {
432
+ MutableHashMap.set(indexByIdentity, identity, addNode(mutable, data));
433
+ }
434
+ return indexByIdentity;
435
+ };
436
+ /** @internal */
437
+ const addEdgeByIdentity = (mutable, indexByIdentity, identity, data) => {
438
+ const sourceIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.source));
439
+ const targetIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.target));
440
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
441
+ addEdge(mutable, sourceIndex, targetIndex, data);
442
+ }
443
+ };
444
+ /** @internal */
445
+ const assertSameKind = (self, that) => {
446
+ if (self.type !== that.type) {
447
+ throw new GraphError({
448
+ message: `Cannot combine ${self.type} and ${that.type} graphs`
449
+ });
450
+ }
451
+ };
452
+ /**
453
+ * Composes two graphs, merging nodes by identity.
454
+ *
455
+ * **Details**
456
+ *
457
+ * Nodes and edges present in both graphs use data from `that`. The result has
458
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
459
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
460
+ * edge data. Edge identity also includes the endpoint identities.
461
+ *
462
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
463
+ *
464
+ * **Gotchas**
465
+ *
466
+ * Nodes with equal identities in one input graph are coalesced. The last node
467
+ * supplies the data, and redirected edges can collapse or become self-loops.
468
+ * Parallel edges with equal identities are also coalesced, with the last edge
469
+ * supplying the data.
470
+ *
471
+ * **Example** (Combining graphs)
472
+ *
473
+ * ```ts
474
+ * import { Graph } from "effect"
475
+ *
476
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
477
+ * const a = Graph.addNode(mutable, { id: "A" })
478
+ * const b = Graph.addNode(mutable, { id: "B" })
479
+ * Graph.addEdge(mutable, a, b, "A-B")
480
+ * })
481
+ *
482
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
483
+ * const b = Graph.addNode(mutable, { id: "B" })
484
+ * const c = Graph.addNode(mutable, { id: "C" })
485
+ * Graph.addEdge(mutable, b, c, "B-C")
486
+ * })
487
+ *
488
+ * const result = Graph.compose(left, right, {
489
+ * nodeIdentity: (node) => node.id
490
+ * })
491
+ *
492
+ * console.log(Graph.nodeCount(result)) // 3
493
+ * console.log(Graph.edgeCount(result)) // 2
494
+ * ```
495
+ *
496
+ * @category set operations
497
+ * @since 4.0.0
498
+ */
499
+ export const compose = /*#__PURE__*/dual(args => isGraph(args[0]) && isGraph(args[1]), (self, that, options) => {
500
+ assertSameKind(self, that);
501
+ const getNodeIdentity = options?.nodeIdentity ?? (node => node);
502
+ const getEdgeIdentity = options?.edgeIdentity ?? (edge => edge);
503
+ const selfMaps = buildNodeMaps(self, getNodeIdentity);
504
+ const thatMaps = buildNodeMaps(that, getNodeIdentity);
505
+ const nodes = MutableHashMap.empty();
506
+ const edges = buildEdgeMap(self, selfMaps, getEdgeIdentity);
507
+ for (const [identity, data] of selfMaps.byIdentity) {
508
+ MutableHashMap.set(nodes, identity, data);
509
+ }
510
+ for (const [identity, data] of thatMaps.byIdentity) {
511
+ MutableHashMap.set(nodes, identity, data);
512
+ }
513
+ for (const [identity, data] of buildEdgeMap(that, thatMaps, getEdgeIdentity)) {
514
+ MutableHashMap.set(edges, identity, data);
515
+ }
516
+ return make(self.type)(mutable => {
517
+ const indexByIdentity = addNodesByIdentity(mutable, nodes);
518
+ for (const [identity, data] of edges) {
519
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data);
520
+ }
521
+ });
522
+ });
523
+ /**
524
+ * Returns the intersection of two graphs, matching nodes by identity.
525
+ *
526
+ * **Details**
527
+ *
528
+ * Node data comes from `self`, and edge data comes from `that`. The result has
529
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
530
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
531
+ * edge data. Edge identity also includes the endpoint identities.
532
+ *
533
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
534
+ *
535
+ * **Gotchas**
536
+ *
537
+ * Nodes with equal identities in one input graph are coalesced. The last node
538
+ * supplies the data, and redirected edges can collapse or become self-loops.
539
+ * The result contains at most one edge for each shared edge identity.
540
+ *
541
+ * **Example** (Finding shared structure)
542
+ *
543
+ * ```ts
544
+ * import { Graph } from "effect"
545
+ *
546
+ * const left = Graph.directed<string, string>((mutable) => {
547
+ * const a = Graph.addNode(mutable, "A")
548
+ * const b = Graph.addNode(mutable, "B")
549
+ * Graph.addEdge(mutable, a, b, "shared")
550
+ * })
551
+ *
552
+ * const right = Graph.directed<string, string>((mutable) => {
553
+ * const a = Graph.addNode(mutable, "A")
554
+ * const b = Graph.addNode(mutable, "B")
555
+ * Graph.addEdge(mutable, a, b, "shared")
556
+ * })
557
+ *
558
+ * const result = Graph.intersection(left, right)
559
+ *
560
+ * console.log(Graph.nodeCount(result)) // 2
561
+ * console.log(Graph.edgeCount(result)) // 1
562
+ * ```
563
+ *
564
+ * @category set operations
565
+ * @since 4.0.0
566
+ */
567
+ export const intersection = /*#__PURE__*/dual(args => isGraph(args[0]) && isGraph(args[1]), (self, that, options) => {
568
+ assertSameKind(self, that);
569
+ const thatImpl = graphImpl(that);
570
+ const getNodeIdentity = options?.nodeIdentity ?? (node => node);
571
+ const getEdgeIdentity = options?.edgeIdentity ?? (edge => edge);
572
+ const selfMaps = buildNodeMaps(self, getNodeIdentity);
573
+ const thatMaps = buildNodeMaps(that, getNodeIdentity);
574
+ const nodes = MutableHashMap.empty();
575
+ const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity);
576
+ const thatEdges = MutableHashMap.empty();
577
+ for (const [identity, data] of selfMaps.byIdentity) {
578
+ if (MutableHashMap.has(thatMaps.byIdentity, identity)) {
579
+ MutableHashMap.set(nodes, identity, data);
580
+ }
581
+ }
582
+ for (const edge of thatImpl.edges.values()) {
583
+ const sourceIdentity = nodeIdentityAt(thatMaps, edge.source);
584
+ const targetIdentity = nodeIdentityAt(thatMaps, edge.target);
585
+ if (MutableHashMap.has(nodes, sourceIdentity) && MutableHashMap.has(nodes, targetIdentity)) {
586
+ const edgeIdentity = new EdgeIdentity(that.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data));
587
+ MutableHashMap.set(thatEdges, edgeIdentity, edge.data);
588
+ }
589
+ }
590
+ return make(self.type)(mutable => {
591
+ const indexByIdentity = addNodesByIdentity(mutable, nodes);
592
+ for (const [identity, data] of thatEdges) {
593
+ if (MutableHashMap.has(selfEdges, identity)) {
594
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data);
595
+ }
596
+ }
597
+ });
598
+ });
599
+ /**
600
+ * Returns `self` without edges also present in `that`.
601
+ *
602
+ * **Details**
603
+ *
604
+ * All nodes from `self` are preserved. Edges are matched by endpoint and edge
605
+ * identities. The result has the same graph kind as `self`. Throws a
606
+ * `GraphError` when the graph kinds do not match. `nodeIdentity` and
607
+ * `edgeIdentity` default to the complete node and edge data.
608
+ *
609
+ * `G1 \ G2 = {V1, E1 \ E2}`
610
+ *
611
+ * **Gotchas**
612
+ *
613
+ * Nodes with equal identities in one input graph are coalesced. The last node
614
+ * supplies the data, and redirected edges can collapse or become self-loops.
615
+ * If `that` contains an edge identity, every parallel edge with that identity
616
+ * is removed from `self`.
617
+ *
618
+ * **Example** (Removing shared edges)
619
+ *
620
+ * ```ts
621
+ * import { Graph } from "effect"
622
+ *
623
+ * const left = Graph.directed<string, string>((mutable) => {
624
+ * const a = Graph.addNode(mutable, "A")
625
+ * const b = Graph.addNode(mutable, "B")
626
+ * const c = Graph.addNode(mutable, "C")
627
+ * Graph.addEdge(mutable, a, b, "A-B")
628
+ * Graph.addEdge(mutable, b, c, "B-C")
629
+ * })
630
+ *
631
+ * const right = Graph.directed<string, string>((mutable) => {
632
+ * const b = Graph.addNode(mutable, "B")
633
+ * const c = Graph.addNode(mutable, "C")
634
+ * Graph.addEdge(mutable, b, c, "B-C")
635
+ * })
636
+ *
637
+ * const result = Graph.difference(left, right)
638
+ *
639
+ * console.log(Graph.nodeCount(result)) // 3
640
+ * console.log(Graph.edgeCount(result)) // 1
641
+ * ```
642
+ *
643
+ * @category set operations
644
+ * @since 4.0.0
645
+ */
646
+ export const difference = /*#__PURE__*/dual(args => isGraph(args[0]) && isGraph(args[1]), (self, that, options) => {
647
+ assertSameKind(self, that);
648
+ const selfImpl = graphImpl(self);
649
+ const getNodeIdentity = options?.nodeIdentity ?? (node => node);
650
+ const getEdgeIdentity = options?.edgeIdentity ?? (edge => edge);
651
+ const selfMaps = buildNodeMaps(self, getNodeIdentity);
652
+ const thatMaps = buildNodeMaps(that, getNodeIdentity);
653
+ const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity);
654
+ return make(self.type)(mutable => {
655
+ const indexByIdentity = addNodesByIdentity(mutable, selfMaps.byIdentity);
656
+ for (const edge of selfImpl.edges.values()) {
657
+ const sourceIdentity = nodeIdentityAt(selfMaps, edge.source);
658
+ const targetIdentity = nodeIdentityAt(selfMaps, edge.target);
659
+ const edgeIdentity = new EdgeIdentity(self.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data));
660
+ if (!MutableHashMap.has(thatEdges, edgeIdentity)) {
661
+ addEdgeByIdentity(mutable, indexByIdentity, edgeIdentity, edge.data);
662
+ }
663
+ }
664
+ });
665
+ });
666
+ /**
667
+ * Returns edges present in exactly one of two graphs.
668
+ *
669
+ * **Details**
670
+ *
671
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
672
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
673
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
674
+ * complete node and edge data. Edge identity also includes the endpoint
675
+ * identities.
676
+ *
677
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
678
+ *
679
+ * **Gotchas**
680
+ *
681
+ * Edges with different projected identities are distinct.
682
+ * Nodes with equal identities in one input graph are coalesced. The last node
683
+ * supplies the data, and redirected edges can collapse or become self-loops.
684
+ * Parallel edges with equal identities are coalesced before the graphs are
685
+ * compared.
686
+ *
687
+ * **Example** (Finding differing edges)
688
+ *
689
+ * ```ts
690
+ * import { Graph } from "effect"
691
+ *
692
+ * const left = Graph.directed<string, string>((mutable) => {
693
+ * const a = Graph.addNode(mutable, "A")
694
+ * const b = Graph.addNode(mutable, "B")
695
+ * const c = Graph.addNode(mutable, "C")
696
+ * Graph.addEdge(mutable, a, b, "A-B")
697
+ * Graph.addEdge(mutable, b, c, "B-C")
698
+ * })
699
+ *
700
+ * const right = Graph.directed<string, string>((mutable) => {
701
+ * const b = Graph.addNode(mutable, "B")
702
+ * const c = Graph.addNode(mutable, "C")
703
+ * const d = Graph.addNode(mutable, "D")
704
+ * Graph.addEdge(mutable, b, c, "B-C")
705
+ * Graph.addEdge(mutable, c, d, "C-D")
706
+ * })
707
+ *
708
+ * const result = Graph.symmetricDifference(left, right)
709
+ *
710
+ * console.log(Graph.nodeCount(result)) // 4
711
+ * console.log(Graph.edgeCount(result)) // 2
712
+ * ```
713
+ *
714
+ * @category set operations
715
+ * @since 4.0.0
716
+ */
717
+ export const symmetricDifference = /*#__PURE__*/dual(args => isGraph(args[0]) && isGraph(args[1]), (self, that, options) => {
718
+ assertSameKind(self, that);
719
+ const getNodeIdentity = options?.nodeIdentity ?? (node => node);
720
+ const getEdgeIdentity = options?.edgeIdentity ?? (edge => edge);
721
+ const selfMaps = buildNodeMaps(self, getNodeIdentity);
722
+ const thatMaps = buildNodeMaps(that, getNodeIdentity);
723
+ const nodes = MutableHashMap.empty();
724
+ const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity);
725
+ const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity);
726
+ for (const [identity, data] of selfMaps.byIdentity) {
727
+ MutableHashMap.set(nodes, identity, data);
728
+ }
729
+ for (const [identity, data] of thatMaps.byIdentity) {
730
+ MutableHashMap.set(nodes, identity, data);
731
+ }
732
+ return make(self.type)(mutable => {
733
+ const indexByIdentity = addNodesByIdentity(mutable, nodes);
734
+ for (const [identity, data] of selfEdges) {
735
+ if (!MutableHashMap.has(thatEdges, identity)) {
736
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data);
737
+ }
738
+ }
739
+ for (const [identity, data] of thatEdges) {
740
+ if (!MutableHashMap.has(selfEdges, identity)) {
741
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data);
742
+ }
743
+ }
744
+ });
745
+ });
746
+ /**
747
+ * Returns the complement over the existing node set.
748
+ *
749
+ * **Details**
750
+ *
751
+ * Adds every missing edge between distinct nodes. The `createEdge` function
752
+ * receives the source and target node data for each added edge. The result has
753
+ * the same graph kind as `self`.
754
+ *
755
+ * `G' = {V, (V x V) \ E}`
756
+ *
757
+ * **Example** (Finding missing relationships)
758
+ *
759
+ * ```ts
760
+ * import { Graph } from "effect"
761
+ *
762
+ * const graph = Graph.directed<string, string>((mutable) => {
763
+ * const a = Graph.addNode(mutable, "A")
764
+ * const b = Graph.addNode(mutable, "B")
765
+ * Graph.addEdge(mutable, a, b, "A-B")
766
+ * })
767
+ *
768
+ * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
769
+ *
770
+ * console.log(Graph.edgeCount(result)) // 1
771
+ * ```
772
+ *
773
+ * @category set operations
774
+ * @since 4.0.0
775
+ */
776
+ export const complement = /*#__PURE__*/dual(2, (self, createEdge) => {
777
+ const selfImpl = graphImpl(self);
778
+ const nodeEntries = Array.from(selfImpl.nodes);
779
+ return make(self.type)(mutable => {
780
+ const newIndexMap = new Map();
781
+ for (const [oldIndex, data] of nodeEntries) {
782
+ newIndexMap.set(oldIndex, addNode(mutable, data));
783
+ }
784
+ for (let i = 0; i < nodeEntries.length; i++) {
785
+ const [sourceOldIndex, sourceData] = nodeEntries[i];
786
+ const start = self.type === "undirected" ? i + 1 : 0;
787
+ for (let j = start; j < nodeEntries.length; j++) {
788
+ const [targetOldIndex, targetData] = nodeEntries[j];
789
+ if (sourceOldIndex === targetOldIndex || hasEdge(self, sourceOldIndex, targetOldIndex)) {
790
+ continue;
791
+ }
792
+ const sourceIndex = newIndexMap.get(sourceOldIndex);
793
+ const targetIndex = newIndexMap.get(targetOldIndex);
794
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
795
+ addEdge(mutable, sourceIndex, targetIndex, createEdge(sourceData, targetData));
796
+ }
797
+ }
798
+ }
799
+ });
800
+ });
801
+ /**
802
+ * Returns the induced subgraph containing nodes within a radius of a node.
803
+ *
804
+ * **Details**
805
+ *
806
+ * The `radius` option is the maximum edge distance from `nodeIndex` and
807
+ * defaults to `1`. The `direction` option controls directed graph traversal and
808
+ * defaults to `"outgoing"`. The result has the same graph kind as `self` and
809
+ * keeps all original edges whose endpoints are both reached. `"undirected"`
810
+ * ignores edge direction while finding reachable nodes.
811
+ *
812
+ * **Example** (Getting a local neighborhood)
813
+ *
814
+ * ```ts
815
+ * import { Graph } from "effect"
816
+ *
817
+ * const graph = Graph.directed<string, string>((mutable) => {
818
+ * const a = Graph.addNode(mutable, "A")
819
+ * const b = Graph.addNode(mutable, "B")
820
+ * const c = Graph.addNode(mutable, "C")
821
+ * Graph.addEdge(mutable, a, b, "A-B")
822
+ * Graph.addEdge(mutable, b, c, "B-C")
823
+ * })
824
+ *
825
+ * const result = Graph.neighborhood(graph, 1, { radius: 1 })
826
+ *
827
+ * console.log(Graph.nodeCount(result)) // 2
828
+ * ```
829
+ *
830
+ * @category set operations
831
+ * @since 4.0.0
832
+ */
833
+ export const neighborhood = /*#__PURE__*/dual(args => isGraph(args[0]), (self, nodeIndex, options) => {
834
+ const selfImpl = graphImpl(self);
835
+ const radius = options?.radius ?? 1;
836
+ const direction = options?.direction ?? "outgoing";
837
+ const reached = new Set();
838
+ for (const index of indices(bfs(self, {
839
+ start: [nodeIndex],
840
+ direction,
841
+ radius
842
+ }))) {
843
+ reached.add(index);
844
+ }
845
+ return make(self.type)(mutable => {
846
+ const newIndexMap = new Map();
847
+ for (const oldIndex of reached) {
848
+ newIndexMap.set(oldIndex, addNode(mutable, Option.getOrThrow(getNode(self, oldIndex))));
849
+ }
850
+ for (const edge of selfImpl.edges.values()) {
851
+ if (reached.has(edge.source) && reached.has(edge.target)) {
852
+ const sourceIndex = newIndexMap.get(edge.source);
853
+ const targetIndex = newIndexMap.get(edge.target);
854
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
855
+ addEdge(mutable, sourceIndex, targetIndex, edge.data);
856
+ }
857
+ }
858
+ }
859
+ });
860
+ });
861
+ /**
862
+ * Returns the disjoint union of two graphs.
863
+ *
864
+ * **Details**
865
+ *
866
+ * Copies all nodes and edges from both graphs without merging equal node data.
867
+ * The result has the same graph kind as `self`. Throws a `GraphError` when the
868
+ * graph kinds do not match.
869
+ *
870
+ * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
871
+ *
872
+ * @category set operations
873
+ * @since 4.0.0
874
+ */
875
+ export const sum = /*#__PURE__*/dual(2, (self, that) => {
876
+ assertSameKind(self, that);
877
+ return make(self.type)(mutable => {
878
+ const copyInto = graph => {
879
+ const impl = graphImpl(graph);
880
+ const indexMap = new Map();
881
+ for (const [oldIndex, data] of impl.nodes) {
882
+ indexMap.set(oldIndex, addNode(mutable, data));
883
+ }
884
+ for (const edge of impl.edges.values()) {
885
+ const sourceIndex = indexMap.get(edge.source);
886
+ const targetIndex = indexMap.get(edge.target);
887
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
888
+ addEdge(mutable, sourceIndex, targetIndex, edge.data);
889
+ }
890
+ }
891
+ };
892
+ copyInto(self);
893
+ copyInto(that);
894
+ });
322
895
  });
323
896
  // =============================================================================
324
897
  // Basic Node Operations
@@ -362,14 +935,16 @@ export const mutate = /*#__PURE__*/dual(2, (graph, f) => {
362
935
  * @since 3.18.0
363
936
  */
364
937
  export const addNode = (mutable, data) => {
365
- const nodeIndex = mutable.nextNodeIndex;
938
+ assertMutable(mutable);
939
+ const impl = graphImpl(mutable);
940
+ const nodeIndex = impl.nextNodeIndex;
366
941
  // Add node data
367
- mutable.nodes.set(nodeIndex, data);
942
+ impl.nodes.set(nodeIndex, data);
368
943
  // Initialize empty adjacency lists
369
- mutable.adjacency.set(nodeIndex, []);
370
- mutable.reverseAdjacency.set(nodeIndex, []);
944
+ impl.adjacency.set(nodeIndex, []);
945
+ impl.reverseAdjacency.set(nodeIndex, []);
371
946
  // Update graph allocators
372
- mutable.nextNodeIndex = mutable.nextNodeIndex + 1;
947
+ impl.nextNodeIndex = impl.nextNodeIndex + 1;
373
948
  return nodeIndex;
374
949
  };
375
950
  /**
@@ -395,7 +970,10 @@ export const addNode = (mutable, data) => {
395
970
  * @category getters
396
971
  * @since 3.18.0
397
972
  */
398
- export const getNode = /*#__PURE__*/dual(2, (graph, nodeIndex) => graph.nodes.has(nodeIndex) ? Option.some(graph.nodes.get(nodeIndex)) : Option.none());
973
+ export const getNode = /*#__PURE__*/dual(2, (graph, nodeIndex) => {
974
+ const impl = graphImpl(graph);
975
+ return impl.nodes.has(nodeIndex) ? Option.some(impl.nodes.get(nodeIndex)) : Option.none();
976
+ });
399
977
  /**
400
978
  * Checks whether a node with the given index exists in the graph.
401
979
  *
@@ -420,7 +998,7 @@ export const getNode = /*#__PURE__*/dual(2, (graph, nodeIndex) => graph.nodes.ha
420
998
  * @category getters
421
999
  * @since 3.18.0
422
1000
  */
423
- export const hasNode = /*#__PURE__*/dual(2, (graph, nodeIndex) => graph.nodes.has(nodeIndex));
1001
+ export const hasNode = /*#__PURE__*/dual(2, (graph, nodeIndex) => graphImpl(graph).nodes.has(nodeIndex));
424
1002
  /**
425
1003
  * Returns the number of nodes in the graph.
426
1004
  *
@@ -444,7 +1022,7 @@ export const hasNode = /*#__PURE__*/dual(2, (graph, nodeIndex) => graph.nodes.ha
444
1022
  * @category getters
445
1023
  * @since 3.18.0
446
1024
  */
447
- export const nodeCount = graph => graph.nodes.size;
1025
+ export const nodeCount = graph => graphImpl(graph).nodes.size;
448
1026
  /**
449
1027
  * Finds the first node that matches the given predicate.
450
1028
  *
@@ -470,7 +1048,8 @@ export const nodeCount = graph => graph.nodes.size;
470
1048
  * @since 3.18.0
471
1049
  */
472
1050
  export const findNode = /*#__PURE__*/dual(2, (graph, predicate) => {
473
- for (const [index, data] of graph.nodes) {
1051
+ const impl = graphImpl(graph);
1052
+ for (const [index, data] of impl.nodes) {
474
1053
  if (predicate(data)) {
475
1054
  return Option.some(index);
476
1055
  }
@@ -502,8 +1081,9 @@ export const findNode = /*#__PURE__*/dual(2, (graph, predicate) => {
502
1081
  * @since 3.18.0
503
1082
  */
504
1083
  export const findNodes = /*#__PURE__*/dual(2, (graph, predicate) => {
1084
+ const impl = graphImpl(graph);
505
1085
  const results = [];
506
- for (const [index, data] of graph.nodes) {
1086
+ for (const [index, data] of impl.nodes) {
507
1087
  if (predicate(data)) {
508
1088
  results.push(index);
509
1089
  }
@@ -537,7 +1117,8 @@ export const findNodes = /*#__PURE__*/dual(2, (graph, predicate) => {
537
1117
  * @since 3.18.0
538
1118
  */
539
1119
  export const findEdge = /*#__PURE__*/dual(2, (graph, predicate) => {
540
- for (const [edgeIndex, edgeData] of graph.edges) {
1120
+ const impl = graphImpl(graph);
1121
+ for (const [edgeIndex, edgeData] of impl.edges) {
541
1122
  if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
542
1123
  return Option.some(edgeIndex);
543
1124
  }
@@ -572,8 +1153,9 @@ export const findEdge = /*#__PURE__*/dual(2, (graph, predicate) => {
572
1153
  * @since 3.18.0
573
1154
  */
574
1155
  export const findEdges = /*#__PURE__*/dual(2, (graph, predicate) => {
1156
+ const impl = graphImpl(graph);
575
1157
  const results = [];
576
- for (const [edgeIndex, edgeData] of graph.edges) {
1158
+ for (const [edgeIndex, edgeData] of impl.edges) {
577
1159
  if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
578
1160
  results.push(edgeIndex);
579
1161
  }
@@ -602,12 +1184,14 @@ export const findEdges = /*#__PURE__*/dual(2, (graph, predicate) => {
602
1184
  * @since 3.18.0
603
1185
  */
604
1186
  export const updateNode = (mutable, index, f) => {
605
- if (!mutable.nodes.has(index)) {
1187
+ assertMutable(mutable);
1188
+ const impl = graphImpl(mutable);
1189
+ if (!impl.nodes.has(index)) {
606
1190
  return;
607
1191
  }
608
- const currentData = mutable.nodes.get(index);
1192
+ const currentData = impl.nodes.get(index);
609
1193
  const newData = f(currentData);
610
- mutable.nodes.set(index, newData);
1194
+ impl.nodes.set(index, newData);
611
1195
  };
612
1196
  /**
613
1197
  * Updates a single edge's data by applying a transformation function.
@@ -632,12 +1216,14 @@ export const updateNode = (mutable, index, f) => {
632
1216
  * @since 3.18.0
633
1217
  */
634
1218
  export const updateEdge = (mutable, edgeIndex, f) => {
635
- if (!mutable.edges.has(edgeIndex)) {
1219
+ assertMutable(mutable);
1220
+ const impl = graphImpl(mutable);
1221
+ if (!impl.edges.has(edgeIndex)) {
636
1222
  return;
637
1223
  }
638
- const currentEdge = mutable.edges.get(edgeIndex);
1224
+ const currentEdge = impl.edges.get(edgeIndex);
639
1225
  const newData = f(currentEdge.data);
640
- mutable.edges.set(edgeIndex, new Edge({
1226
+ impl.edges.set(edgeIndex, new Edge({
641
1227
  ...currentEdge,
642
1228
  data: newData
643
1229
  }));
@@ -670,10 +1256,12 @@ export const updateEdge = (mutable, edgeIndex, f) => {
670
1256
  * @since 3.18.0
671
1257
  */
672
1258
  export const mapNodes = (mutable, f) => {
1259
+ assertMutable(mutable);
1260
+ const impl = graphImpl(mutable);
673
1261
  // Transform existing node data in place
674
- for (const [index, data] of mutable.nodes) {
1262
+ for (const [index, data] of impl.nodes) {
675
1263
  const newData = f(data);
676
- mutable.nodes.set(index, newData);
1264
+ impl.nodes.set(index, newData);
677
1265
  }
678
1266
  };
679
1267
  /**
@@ -701,13 +1289,15 @@ export const mapNodes = (mutable, f) => {
701
1289
  * @since 3.18.0
702
1290
  */
703
1291
  export const mapEdges = (mutable, f) => {
1292
+ assertMutable(mutable);
1293
+ const impl = graphImpl(mutable);
704
1294
  // Transform existing edge data in place
705
- for (const [index, edgeData] of mutable.edges) {
1295
+ for (const [index, edgeData] of impl.edges) {
706
1296
  const newData = f(edgeData.data);
707
- mutable.edges.set(index, {
1297
+ impl.edges.set(index, new Edge({
708
1298
  ...edgeData,
709
1299
  data: newData
710
- });
1300
+ }));
711
1301
  }
712
1302
  };
713
1303
  /**
@@ -754,20 +1344,22 @@ const rebuildAdjacency = mutable => {
754
1344
  * @since 3.18.0
755
1345
  */
756
1346
  export const reverse = mutable => {
757
- if (mutable.type === "undirected") {
1347
+ assertMutable(mutable);
1348
+ const impl = graphImpl(mutable);
1349
+ if (impl.type === "undirected") {
758
1350
  return;
759
1351
  }
760
1352
  // Reverse all edges by swapping source and target
761
- for (const [index, edgeData] of mutable.edges) {
762
- mutable.edges.set(index, new Edge({
1353
+ for (const [index, edgeData] of impl.edges) {
1354
+ impl.edges.set(index, new Edge({
763
1355
  source: edgeData.target,
764
1356
  target: edgeData.source,
765
1357
  data: edgeData.data
766
1358
  }));
767
1359
  }
768
- rebuildAdjacency(mutable);
1360
+ rebuildAdjacency(impl);
769
1361
  // Invalidate cycle flag since edge directions changed
770
- mutable.acyclic = Option.none();
1362
+ impl.acyclic = Option.none();
771
1363
  };
772
1364
  /**
773
1365
  * Filters and optionally transforms nodes in a mutable graph using a predicate function.
@@ -800,13 +1392,15 @@ export const reverse = mutable => {
800
1392
  * @since 3.18.0
801
1393
  */
802
1394
  export const filterMapNodes = (mutable, f) => {
1395
+ assertMutable(mutable);
1396
+ const impl = graphImpl(mutable);
803
1397
  const nodesToRemove = [];
804
1398
  // First pass: identify nodes to remove and transform data for nodes to keep
805
- for (const [index, data] of mutable.nodes) {
1399
+ for (const [index, data] of impl.nodes) {
806
1400
  const result = f(data);
807
1401
  if (Option.isSome(result)) {
808
1402
  // Transform node data
809
- mutable.nodes.set(index, result.value);
1403
+ impl.nodes.set(index, result.value);
810
1404
  } else {
811
1405
  // Mark for removal
812
1406
  nodesToRemove.push(index);
@@ -848,16 +1442,18 @@ export const filterMapNodes = (mutable, f) => {
848
1442
  * @since 3.18.0
849
1443
  */
850
1444
  export const filterMapEdges = (mutable, f) => {
1445
+ assertMutable(mutable);
1446
+ const impl = graphImpl(mutable);
851
1447
  const edgesToRemove = [];
852
1448
  // First pass: identify edges to remove and transform data for edges to keep
853
- for (const [index, edgeData] of mutable.edges) {
1449
+ for (const [index, edgeData] of impl.edges) {
854
1450
  const result = f(edgeData.data);
855
1451
  if (Option.isSome(result)) {
856
1452
  // Transform edge data
857
- mutable.edges.set(index, {
1453
+ impl.edges.set(index, new Edge({
858
1454
  ...edgeData,
859
1455
  data: result.value
860
- });
1456
+ }));
861
1457
  } else {
862
1458
  // Mark for removal
863
1459
  edgesToRemove.push(index);
@@ -894,9 +1490,11 @@ export const filterMapEdges = (mutable, f) => {
894
1490
  * @since 3.18.0
895
1491
  */
896
1492
  export const filterNodes = (mutable, predicate) => {
1493
+ assertMutable(mutable);
1494
+ const impl = graphImpl(mutable);
897
1495
  const nodesToRemove = [];
898
1496
  // Identify nodes to remove
899
- for (const [index, data] of mutable.nodes) {
1497
+ for (const [index, data] of impl.nodes) {
900
1498
  if (!predicate(data)) {
901
1499
  nodesToRemove.push(index);
902
1500
  }
@@ -935,9 +1533,11 @@ export const filterNodes = (mutable, predicate) => {
935
1533
  * @since 3.18.0
936
1534
  */
937
1535
  export const filterEdges = (mutable, predicate) => {
1536
+ assertMutable(mutable);
1537
+ const impl = graphImpl(mutable);
938
1538
  const edgesToRemove = [];
939
1539
  // Identify edges to remove
940
- for (const [index, edgeData] of mutable.edges) {
1540
+ for (const [index, edgeData] of impl.edges) {
941
1541
  if (!predicate(edgeData.data)) {
942
1542
  edgesToRemove.push(index);
943
1543
  }
@@ -1008,46 +1608,48 @@ const invalidateCycleFlagOnAddition = mutable => {
1008
1608
  * @since 3.18.0
1009
1609
  */
1010
1610
  export const addEdge = (mutable, source, target, data) => {
1611
+ assertMutable(mutable);
1612
+ const impl = graphImpl(mutable);
1011
1613
  // Validate that both nodes exist
1012
- if (!mutable.nodes.has(source)) {
1614
+ if (!impl.nodes.has(source)) {
1013
1615
  throw missingNode(source);
1014
1616
  }
1015
- if (!mutable.nodes.has(target)) {
1617
+ if (!impl.nodes.has(target)) {
1016
1618
  throw missingNode(target);
1017
1619
  }
1018
- const edgeIndex = mutable.nextEdgeIndex;
1620
+ const edgeIndex = impl.nextEdgeIndex;
1019
1621
  // Create edge data
1020
1622
  const edgeData = new Edge({
1021
1623
  source,
1022
1624
  target,
1023
1625
  data
1024
1626
  });
1025
- mutable.edges.set(edgeIndex, edgeData);
1627
+ impl.edges.set(edgeIndex, edgeData);
1026
1628
  // Update adjacency lists
1027
- const sourceAdjacency = mutable.adjacency.get(source);
1629
+ const sourceAdjacency = impl.adjacency.get(source);
1028
1630
  if (sourceAdjacency !== undefined) {
1029
1631
  sourceAdjacency.push(edgeIndex);
1030
1632
  }
1031
- const targetReverseAdjacency = mutable.reverseAdjacency.get(target);
1633
+ const targetReverseAdjacency = impl.reverseAdjacency.get(target);
1032
1634
  if (targetReverseAdjacency !== undefined) {
1033
1635
  targetReverseAdjacency.push(edgeIndex);
1034
1636
  }
1035
1637
  // For undirected graphs, add reverse connections
1036
- if (mutable.type === "undirected") {
1037
- const targetAdjacency = mutable.adjacency.get(target);
1638
+ if (impl.type === "undirected") {
1639
+ const targetAdjacency = impl.adjacency.get(target);
1038
1640
  if (targetAdjacency !== undefined) {
1039
1641
  targetAdjacency.push(edgeIndex);
1040
1642
  }
1041
- const sourceReverseAdjacency = mutable.reverseAdjacency.get(source);
1643
+ const sourceReverseAdjacency = impl.reverseAdjacency.get(source);
1042
1644
  if (sourceReverseAdjacency !== undefined) {
1043
1645
  sourceReverseAdjacency.push(edgeIndex);
1044
1646
  }
1045
1647
  }
1046
1648
  // Update allocators
1047
- mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1;
1649
+ impl.nextEdgeIndex = impl.nextEdgeIndex + 1;
1048
1650
  // Only invalidate cycle flag if the graph was acyclic
1049
1651
  // Adding edges cannot remove cycles from cyclic graphs
1050
- invalidateCycleFlagOnAddition(mutable);
1652
+ invalidateCycleFlagOnAddition(impl);
1051
1653
  return edgeIndex;
1052
1654
  };
1053
1655
  /**
@@ -1072,21 +1674,23 @@ export const addEdge = (mutable, source, target, data) => {
1072
1674
  * @since 3.18.0
1073
1675
  */
1074
1676
  export const removeNode = (mutable, nodeIndex) => {
1677
+ assertMutable(mutable);
1678
+ const impl = graphImpl(mutable);
1075
1679
  // Check if node exists
1076
- if (!mutable.nodes.has(nodeIndex)) {
1680
+ if (!impl.nodes.has(nodeIndex)) {
1077
1681
  return; // Node doesn't exist, nothing to remove
1078
1682
  }
1079
1683
  // Collect all incident edges for removal
1080
1684
  const edgesToRemove = [];
1081
1685
  // Get outgoing edges
1082
- const outgoingEdges = mutable.adjacency.get(nodeIndex);
1686
+ const outgoingEdges = impl.adjacency.get(nodeIndex);
1083
1687
  if (outgoingEdges !== undefined) {
1084
1688
  for (const edge of outgoingEdges) {
1085
1689
  edgesToRemove.push(edge);
1086
1690
  }
1087
1691
  }
1088
1692
  // Get incoming edges
1089
- const incomingEdges = mutable.reverseAdjacency.get(nodeIndex);
1693
+ const incomingEdges = impl.reverseAdjacency.get(nodeIndex);
1090
1694
  if (incomingEdges !== undefined) {
1091
1695
  for (const edge of incomingEdges) {
1092
1696
  edgesToRemove.push(edge);
@@ -1094,15 +1698,15 @@ export const removeNode = (mutable, nodeIndex) => {
1094
1698
  }
1095
1699
  // Remove all incident edges
1096
1700
  for (const edgeIndex of edgesToRemove) {
1097
- removeEdgeInternal(mutable, edgeIndex);
1701
+ removeEdgeInternal(impl, edgeIndex);
1098
1702
  }
1099
1703
  // Remove the node itself
1100
- mutable.nodes.delete(nodeIndex);
1101
- mutable.adjacency.delete(nodeIndex);
1102
- mutable.reverseAdjacency.delete(nodeIndex);
1704
+ impl.nodes.delete(nodeIndex);
1705
+ impl.adjacency.delete(nodeIndex);
1706
+ impl.reverseAdjacency.delete(nodeIndex);
1103
1707
  // Only invalidate cycle flag if the graph wasn't already known to be acyclic
1104
1708
  // Removing nodes cannot introduce cycles in an acyclic graph
1105
- invalidateCycleFlagOnRemoval(mutable);
1709
+ invalidateCycleFlagOnRemoval(impl);
1106
1710
  };
1107
1711
  /**
1108
1712
  * Removes an edge from a mutable graph.
@@ -1126,11 +1730,13 @@ export const removeNode = (mutable, nodeIndex) => {
1126
1730
  * @since 3.18.0
1127
1731
  */
1128
1732
  export const removeEdge = (mutable, edgeIndex) => {
1129
- const wasRemoved = removeEdgeInternal(mutable, edgeIndex);
1733
+ assertMutable(mutable);
1734
+ const impl = graphImpl(mutable);
1735
+ const wasRemoved = removeEdgeInternal(impl, edgeIndex);
1130
1736
  // Only invalidate cycle flag if an edge was actually removed
1131
1737
  // and only if the graph wasn't already known to be acyclic
1132
1738
  if (wasRemoved) {
1133
- invalidateCycleFlagOnRemoval(mutable);
1739
+ invalidateCycleFlagOnRemoval(impl);
1134
1740
  }
1135
1741
  };
1136
1742
  /** @internal */
@@ -1210,7 +1816,7 @@ const removeEdgeInternal = (mutable, edgeIndex) => {
1210
1816
  * @category getters
1211
1817
  * @since 3.18.0
1212
1818
  */
1213
- export const getEdge = /*#__PURE__*/dual(2, (graph, edgeIndex) => Option.fromUndefinedOr(graph.edges.get(edgeIndex)));
1819
+ export const getEdge = /*#__PURE__*/dual(2, (graph, edgeIndex) => Option.fromUndefinedOr(graphImpl(graph).edges.get(edgeIndex)));
1214
1820
  /**
1215
1821
  * Checks whether an edge exists between two nodes in the graph.
1216
1822
  *
@@ -1241,13 +1847,14 @@ export const getEdge = /*#__PURE__*/dual(2, (graph, edgeIndex) => Option.fromUnd
1241
1847
  * @since 3.18.0
1242
1848
  */
1243
1849
  export const hasEdge = /*#__PURE__*/dual(3, (graph, source, target) => {
1244
- const adjacencyList = graph.adjacency.get(source);
1850
+ const impl = graphImpl(graph);
1851
+ const adjacencyList = impl.adjacency.get(source);
1245
1852
  if (adjacencyList === undefined) {
1246
1853
  return false;
1247
1854
  }
1248
1855
  // Check if any edge in the adjacency list connects to the target
1249
1856
  for (const edgeIndex of adjacencyList) {
1250
- const edge = graph.edges.get(edgeIndex);
1857
+ const edge = impl.edges.get(edgeIndex);
1251
1858
  if (edge !== undefined) {
1252
1859
  const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target;
1253
1860
  if (neighbor === target) {
@@ -1283,16 +1890,17 @@ export const hasEdge = /*#__PURE__*/dual(3, (graph, source, target) => {
1283
1890
  * @category getters
1284
1891
  * @since 3.18.0
1285
1892
  */
1286
- export const edgeCount = graph => graph.edges.size;
1893
+ export const edgeCount = graph => graphImpl(graph).edges.size;
1287
1894
  const getDirectedNeighbors = (graph, nodeIndex, direction) => {
1288
- const adjacencyMap = direction === "incoming" ? graph.reverseAdjacency : graph.adjacency;
1895
+ const impl = graphImpl(graph);
1896
+ const adjacencyMap = direction === "incoming" ? impl.reverseAdjacency : impl.adjacency;
1289
1897
  const adjacencyList = adjacencyMap.get(nodeIndex);
1290
1898
  if (adjacencyList === undefined) {
1291
1899
  return [];
1292
1900
  }
1293
1901
  const result = [];
1294
1902
  for (const edgeIndex of adjacencyList) {
1295
- const edge = graph.edges.get(edgeIndex);
1903
+ const edge = impl.edges.get(edgeIndex);
1296
1904
  if (edge !== undefined) {
1297
1905
  result.push(direction === "incoming" ? edge.source : edge.target);
1298
1906
  }
@@ -1440,6 +2048,7 @@ export const neighborsDirected = /*#__PURE__*/dual(3, (graph, nodeIndex, directi
1440
2048
  }
1441
2049
  return getDirectedNeighbors(graph, nodeIndex, direction);
1442
2050
  });
2051
+ const escapeGraphVizString = value => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r\n|\r|\n/g, "\\n");
1443
2052
  /**
1444
2053
  * Exports a graph to GraphViz DOT format for visualization.
1445
2054
  *
@@ -1459,7 +2068,7 @@ export const neighborsDirected = /*#__PURE__*/dual(3, (graph, nodeIndex, directi
1459
2068
  *
1460
2069
  * const dot = Graph.toGraphViz(graph)
1461
2070
  * console.log(dot)
1462
- * // digraph G {
2071
+ * // digraph "G" {
1463
2072
  * // "0" [label="Node A"];
1464
2073
  * // "1" [label="Node B"];
1465
2074
  * // "2" [label="Node C"];
@@ -1473,6 +2082,7 @@ export const neighborsDirected = /*#__PURE__*/dual(3, (graph, nodeIndex, directi
1473
2082
  * @since 3.18.0
1474
2083
  */
1475
2084
  export const toGraphViz = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, options) => {
2085
+ const impl = graphImpl(graph);
1476
2086
  const {
1477
2087
  edgeLabel = data => String(data),
1478
2088
  graphName = "G",
@@ -1481,16 +2091,17 @@ export const toGraphViz = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, op
1481
2091
  const isDirected = graph.type === "directed";
1482
2092
  const graphType = isDirected ? "digraph" : "graph";
1483
2093
  const edgeOperator = isDirected ? "->" : "--";
2094
+ const graphId = `"${escapeGraphVizString(graphName)}"`;
1484
2095
  const lines = [];
1485
- lines.push(`${graphType} ${graphName} {`);
2096
+ lines.push(`${graphType} ${graphId} {`);
1486
2097
  // Add nodes
1487
- for (const [nodeIndex, nodeData] of graph.nodes) {
1488
- const label = nodeLabel(nodeData).replace(/"/g, "\\\"");
2098
+ for (const [nodeIndex, nodeData] of impl.nodes) {
2099
+ const label = escapeGraphVizString(nodeLabel(nodeData));
1489
2100
  lines.push(` "${nodeIndex}" [label="${label}"];`);
1490
2101
  }
1491
2102
  // Add edges
1492
- for (const [, edgeData] of graph.edges) {
1493
- const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"");
2103
+ for (const [, edgeData] of impl.edges) {
2104
+ const label = escapeGraphVizString(edgeLabel(edgeData.data));
1494
2105
  lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`);
1495
2106
  }
1496
2107
  lines.push("}");
@@ -1661,22 +2272,22 @@ const formatMermaidNode = (nodeId, label, shape) => {
1661
2272
  * name: "MyApp",
1662
2273
  * version: "1.0.0",
1663
2274
  * type: "library"
1664
- * })
2275
+ * } satisfies Dependency)
1665
2276
  * const react = Graph.addNode(mutable, {
1666
2277
  * name: "React",
1667
2278
  * version: "18.0.0",
1668
2279
  * type: "framework"
1669
- * })
2280
+ * } satisfies Dependency)
1670
2281
  * const lodash = Graph.addNode(mutable, {
1671
2282
  * name: "Lodash",
1672
2283
  * version: "4.17.0",
1673
2284
  * type: "library"
1674
- * })
2285
+ * } satisfies Dependency)
1675
2286
  * const webpack = Graph.addNode(mutable, {
1676
2287
  * name: "Webpack",
1677
2288
  * version: "5.0.0",
1678
2289
  * type: "tool"
1679
- * })
2290
+ * } satisfies Dependency)
1680
2291
  *
1681
2292
  * Graph.addEdge(mutable, app, react, "depends on")
1682
2293
  * Graph.addEdge(mutable, app, lodash, "depends on")
@@ -1710,6 +2321,7 @@ const formatMermaidNode = (nodeId, label, shape) => {
1710
2321
  * @since 3.18.0
1711
2322
  */
1712
2323
  export const toMermaid = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, options) => {
2324
+ const impl = graphImpl(graph);
1713
2325
  // Extract and validate options with defaults
1714
2326
  const {
1715
2327
  diagramType,
@@ -1724,7 +2336,7 @@ export const toMermaid = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, opt
1724
2336
  const lines = [];
1725
2337
  lines.push(`${finalDiagramType} ${direction}`);
1726
2338
  // Add nodes
1727
- for (const [nodeIndex, nodeData] of graph.nodes) {
2339
+ for (const [nodeIndex, nodeData] of impl.nodes) {
1728
2340
  const nodeId = String(nodeIndex);
1729
2341
  const label = escapeMermaidLabel(nodeLabel(nodeData));
1730
2342
  const shape = nodeShape(nodeData);
@@ -1733,7 +2345,7 @@ export const toMermaid = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, opt
1733
2345
  }
1734
2346
  // Add edges
1735
2347
  const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---";
1736
- for (const [, edgeData] of graph.edges) {
2348
+ for (const [, edgeData] of impl.edges) {
1737
2349
  const sourceId = String(edgeData.source);
1738
2350
  const targetId = String(edgeData.target);
1739
2351
  const label = escapeMermaidLabel(edgeLabel(edgeData.data));
@@ -1755,7 +2367,8 @@ export const toMermaid = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, opt
1755
2367
  *
1756
2368
  * Uses depth-first search to detect back edges, which indicate cycles.
1757
2369
  * For directed graphs, any back edge creates a cycle. For undirected graphs,
1758
- * a back edge that doesn't go to the immediate parent creates a cycle.
2370
+ * a back edge that doesn't use the same edge used to enter the current node
2371
+ * creates a cycle.
1759
2372
  *
1760
2373
  * **Example** (Checking cycles)
1761
2374
  *
@@ -1786,49 +2399,61 @@ export const toMermaid = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, opt
1786
2399
  * @since 3.18.0
1787
2400
  */
1788
2401
  export const isAcyclic = graph => {
2402
+ const impl = graphImpl(graph);
1789
2403
  // Use existing cycle flag if available
1790
- if (Option.isSome(graph.acyclic)) {
1791
- return graph.acyclic.value;
2404
+ if (Option.isSome(impl.acyclic)) {
2405
+ return impl.acyclic.value;
1792
2406
  }
1793
2407
  if (graph.type === "undirected") {
1794
2408
  const visited = new Set();
1795
- for (const startNode of graph.nodes.keys()) {
2409
+ for (const startNode of impl.nodes.keys()) {
1796
2410
  if (visited.has(startNode)) {
1797
2411
  continue;
1798
2412
  }
1799
2413
  visited.add(startNode);
1800
2414
  const stack = [{
1801
2415
  node: startNode,
1802
- parent: null
2416
+ incoming: null
1803
2417
  }];
1804
2418
  while (stack.length > 0) {
1805
2419
  const {
1806
2420
  node,
1807
- parent
2421
+ incoming
1808
2422
  } = stack.pop();
1809
- const nodeNeighbors = getUndirectedNeighbors(graph, node);
1810
- for (const neighbor of nodeNeighbors) {
2423
+ const adjacencyList = impl.adjacency.get(node);
2424
+ if (adjacencyList === undefined) {
2425
+ continue;
2426
+ }
2427
+ for (const edgeIndex of adjacencyList) {
2428
+ if (edgeIndex === incoming) {
2429
+ continue;
2430
+ }
2431
+ const edge = impl.edges.get(edgeIndex);
2432
+ if (edge === undefined) {
2433
+ continue;
2434
+ }
2435
+ const neighbor = getTraversableNeighbor(graph, node, edge);
1811
2436
  if (!visited.has(neighbor)) {
1812
2437
  visited.add(neighbor);
1813
2438
  stack.push({
1814
2439
  node: neighbor,
1815
- parent: node
2440
+ incoming: edgeIndex
1816
2441
  });
1817
- } else if (neighbor !== parent) {
1818
- graph.acyclic = Option.some(false);
2442
+ } else {
2443
+ impl.acyclic = Option.some(false);
1819
2444
  return false;
1820
2445
  }
1821
2446
  }
1822
2447
  }
1823
2448
  }
1824
- graph.acyclic = Option.some(true);
2449
+ impl.acyclic = Option.some(true);
1825
2450
  return true;
1826
2451
  }
1827
2452
  // Stack-safe DFS cycle detection using iterative approach
1828
2453
  const visited = new Set();
1829
2454
  const recursionStack = new Set();
1830
2455
  // Get all nodes to handle disconnected components
1831
- for (const startNode of graph.nodes.keys()) {
2456
+ for (const startNode of impl.nodes.keys()) {
1832
2457
  if (visited.has(startNode)) {
1833
2458
  continue; // Already processed this component
1834
2459
  }
@@ -1840,7 +2465,7 @@ export const isAcyclic = graph => {
1840
2465
  if (isFirstVisit) {
1841
2466
  if (recursionStack.has(node)) {
1842
2467
  // Back edge found - cycle detected
1843
- graph.acyclic = Option.some(false);
2468
+ impl.acyclic = Option.some(false);
1844
2469
  return false;
1845
2470
  }
1846
2471
  if (visited.has(node)) {
@@ -1860,7 +2485,7 @@ export const isAcyclic = graph => {
1860
2485
  stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false];
1861
2486
  if (recursionStack.has(neighbor)) {
1862
2487
  // Back edge found - cycle detected
1863
- graph.acyclic = Option.some(false);
2488
+ impl.acyclic = Option.some(false);
1864
2489
  return false;
1865
2490
  }
1866
2491
  if (!visited.has(neighbor)) {
@@ -1874,7 +2499,7 @@ export const isAcyclic = graph => {
1874
2499
  }
1875
2500
  }
1876
2501
  // Cache the result
1877
- graph.acyclic = Option.some(true);
2502
+ impl.acyclic = Option.some(true);
1878
2503
  return true;
1879
2504
  };
1880
2505
  /**
@@ -1919,11 +2544,12 @@ export const isAcyclic = graph => {
1919
2544
  * @since 3.18.0
1920
2545
  */
1921
2546
  export const isBipartite = graph => {
2547
+ const impl = graphImpl(graph);
1922
2548
  const coloring = new Map();
1923
2549
  const discovered = new Set();
1924
2550
  let isBipartiteGraph = true;
1925
2551
  // Get all nodes to handle disconnected components
1926
- for (const startNode of graph.nodes.keys()) {
2552
+ for (const startNode of impl.nodes.keys()) {
1927
2553
  if (!discovered.has(startNode)) {
1928
2554
  // Start BFS coloring from this component
1929
2555
  const queue = [startNode];
@@ -1963,12 +2589,13 @@ export const isBipartite = graph => {
1963
2589
  * For undirected graphs, we need to find the other endpoint of each edge incident to the node.
1964
2590
  */
1965
2591
  const getUndirectedNeighbors = (graph, nodeIndex) => {
2592
+ const impl = graphImpl(graph);
1966
2593
  const neighbors = new Set();
1967
2594
  // Check edges where this node is the source
1968
- const adjacencyList = graph.adjacency.get(nodeIndex);
2595
+ const adjacencyList = impl.adjacency.get(nodeIndex);
1969
2596
  if (adjacencyList !== undefined) {
1970
2597
  for (const edgeIndex of adjacencyList) {
1971
- const edge = graph.edges.get(edgeIndex);
2598
+ const edge = impl.edges.get(edgeIndex);
1972
2599
  if (edge !== undefined) {
1973
2600
  // For undirected graphs, the neighbor is the other endpoint
1974
2601
  const otherNode = edge.source === nodeIndex ? edge.target : edge.source;
@@ -1978,7 +2605,20 @@ const getUndirectedNeighbors = (graph, nodeIndex) => {
1978
2605
  }
1979
2606
  return Array.from(neighbors);
1980
2607
  };
1981
- const getTraversalNeighbors = (graph, nodeIndex, direction) => graph.type === "undirected" ? getUndirectedNeighbors(graph, nodeIndex) : getDirectedNeighbors(graph, nodeIndex, direction);
2608
+ const getTraversalNeighbors = (graph, nodeIndex, direction) => {
2609
+ if (graph.type === "undirected") {
2610
+ return getUndirectedNeighbors(graph, nodeIndex);
2611
+ }
2612
+ const directed = graph;
2613
+ if (direction !== "undirected") {
2614
+ return getDirectedNeighbors(directed, nodeIndex, direction);
2615
+ }
2616
+ const neighbors = new Set(getDirectedNeighbors(directed, nodeIndex, "outgoing"));
2617
+ for (const neighbor of getDirectedNeighbors(directed, nodeIndex, "incoming")) {
2618
+ neighbors.add(neighbor);
2619
+ }
2620
+ return Array.from(neighbors);
2621
+ };
1982
2622
  const getTraversableNeighbor = (graph, current, edge) => graph.type === "undirected" && edge.target === current ? edge.source : edge.target;
1983
2623
  /**
1984
2624
  * Finds connected components in an undirected graph.
@@ -2006,9 +2646,10 @@ const getTraversableNeighbor = (graph, current, edge) => graph.type === "undirec
2006
2646
  * @since 3.18.0
2007
2647
  */
2008
2648
  export const connectedComponents = graph => {
2649
+ const impl = graphImpl(graph);
2009
2650
  const visited = new Set();
2010
2651
  const components = [];
2011
- for (const startNode of graph.nodes.keys()) {
2652
+ for (const startNode of impl.nodes.keys()) {
2012
2653
  if (!visited.has(startNode)) {
2013
2654
  // DFS to find all nodes in this component
2014
2655
  const component = [];
@@ -2067,9 +2708,10 @@ export const stronglyConnectedComponents = graph => {
2067
2708
  message: "Cannot find strongly connected components of undirected graph"
2068
2709
  });
2069
2710
  }
2711
+ const impl = graphImpl(graph);
2070
2712
  const visited = new Set();
2071
2713
  const finishOrder = [];
2072
- for (const startNode of graph.nodes.keys()) {
2714
+ for (const startNode of impl.nodes.keys()) {
2073
2715
  if (visited.has(startNode)) {
2074
2716
  continue;
2075
2717
  }
@@ -2118,10 +2760,10 @@ export const stronglyConnectedComponents = graph => {
2118
2760
  visited.add(node);
2119
2761
  scc.push(node);
2120
2762
  // Use reverse adjacency (transpose graph)
2121
- const reverseAdjacency = graph.reverseAdjacency.get(node);
2763
+ const reverseAdjacency = impl.reverseAdjacency.get(node);
2122
2764
  if (reverseAdjacency !== undefined) {
2123
2765
  for (const edgeIndex of reverseAdjacency) {
2124
- const edge = graph.edges.get(edgeIndex);
2766
+ const edge = impl.edges.get(edgeIndex);
2125
2767
  if (edge !== undefined) {
2126
2768
  const predecessor = edge.source;
2127
2769
  if (!visited.has(predecessor)) {
@@ -2135,18 +2777,42 @@ export const stronglyConnectedComponents = graph => {
2135
2777
  }
2136
2778
  return sccs;
2137
2779
  };
2138
- const validateNonNegativeEdgeWeights = (graph, cost, algorithm) => {
2139
- const edgeWeights = new Map();
2140
- for (const [edgeIndex, edgeData] of graph.edges) {
2141
- const weight = cost(edgeData.data);
2142
- if (weight < 0 || Number.isNaN(weight)) {
2143
- throw new GraphError({
2144
- message: `${algorithm} requires non-negative edge weights`
2145
- });
2780
+ const minHeapLessThan = (self, that) => self.priority < that.priority || self.priority === that.priority && self.sequence < that.sequence;
2781
+ const minHeapPush = (heap, entry) => {
2782
+ let index = heap.length;
2783
+ heap.push(entry);
2784
+ while (index > 0) {
2785
+ const parent = index - 1 >>> 1;
2786
+ if (!minHeapLessThan(entry, heap[parent])) {
2787
+ break;
2146
2788
  }
2147
- edgeWeights.set(edgeIndex, weight);
2789
+ heap[index] = heap[parent];
2790
+ index = parent;
2791
+ }
2792
+ heap[index] = entry;
2793
+ };
2794
+ const minHeapPop = heap => {
2795
+ const first = heap[0];
2796
+ const last = heap.pop();
2797
+ if (last === undefined || heap.length === 0) {
2798
+ return first;
2799
+ }
2800
+ let index = 0;
2801
+ while (true) {
2802
+ const left = index * 2 + 1;
2803
+ if (left >= heap.length) {
2804
+ break;
2805
+ }
2806
+ const right = left + 1;
2807
+ const child = right < heap.length && minHeapLessThan(heap[right], heap[left]) ? right : left;
2808
+ if (!minHeapLessThan(heap[child], last)) {
2809
+ break;
2810
+ }
2811
+ heap[index] = heap[child];
2812
+ index = child;
2148
2813
  }
2149
- return edgeWeights;
2814
+ heap[index] = last;
2815
+ return first;
2150
2816
  };
2151
2817
  /**
2152
2818
  * Finds the shortest path from the configured source node to the target node
@@ -2154,9 +2820,10 @@ const validateNonNegativeEdgeWeights = (graph, cost, algorithm) => {
2154
2820
  *
2155
2821
  * **Details**
2156
2822
  *
2157
- * Edge costs must be non-negative. Returns `Option.none()` when the target is
2158
- * not reachable, and throws a `GraphError` when either endpoint is missing or a
2159
- * negative edge cost is encountered.
2823
+ * Edge costs must be non-negative and not `NaN`. `Infinity` is allowed and
2824
+ * behaves like an impassable edge. Returns `Option.none()` when the target is
2825
+ * not reachable, and throws a `GraphError` when either endpoint is missing or an
2826
+ * edge cost is negative or `NaN`.
2160
2827
  *
2161
2828
  * **Example** (Finding shortest paths with Dijkstra)
2162
2829
  *
@@ -2188,14 +2855,24 @@ const validateNonNegativeEdgeWeights = (graph, cost, algorithm) => {
2188
2855
  * @since 3.18.0
2189
2856
  */
2190
2857
  export const dijkstra = /*#__PURE__*/dual(2, (graph, config) => {
2858
+ const impl = graphImpl(graph);
2191
2859
  // Validate that source and target nodes exist
2192
- if (!graph.nodes.has(config.source)) {
2860
+ if (!impl.nodes.has(config.source)) {
2193
2861
  throw missingNode(config.source);
2194
2862
  }
2195
- if (!graph.nodes.has(config.target)) {
2863
+ if (!impl.nodes.has(config.target)) {
2196
2864
  throw missingNode(config.target);
2197
2865
  }
2198
- const edgeWeights = validateNonNegativeEdgeWeights(graph, config.cost, "Dijkstra's algorithm");
2866
+ const edgeWeights = new Map();
2867
+ for (const [edgeIndex, edgeData] of impl.edges) {
2868
+ const weight = config.cost(edgeData.data);
2869
+ if (Number.isNaN(weight) || weight < 0) {
2870
+ throw new GraphError({
2871
+ message: "Dijkstra's algorithm requires non-negative edge weights"
2872
+ });
2873
+ }
2874
+ edgeWeights.set(edgeIndex, weight);
2875
+ }
2199
2876
  // Early return if source equals target
2200
2877
  if (config.source === config.target) {
2201
2878
  return Option.some({
@@ -2210,24 +2887,19 @@ export const dijkstra = /*#__PURE__*/dual(2, (graph, config) => {
2210
2887
  const visited = new Set();
2211
2888
  // Initialize distances
2212
2889
  // Iterate directly over node keys
2213
- for (const node of graph.nodes.keys()) {
2890
+ for (const node of impl.nodes.keys()) {
2214
2891
  distances.set(node, node === config.source ? 0 : Infinity);
2215
2892
  previous.set(node, null);
2216
2893
  }
2217
- // Simple priority queue using array (can be optimized with proper heap)
2218
- const priorityQueue = [{
2894
+ const priorityQueue = [];
2895
+ let sequence = 0;
2896
+ minHeapPush(priorityQueue, {
2219
2897
  node: config.source,
2220
- distance: 0
2221
- }];
2898
+ priority: 0,
2899
+ sequence: sequence++
2900
+ });
2222
2901
  while (priorityQueue.length > 0) {
2223
- // Find minimum distance node (priority queue extract-min)
2224
- let minIndex = 0;
2225
- for (let i = 1; i < priorityQueue.length; i++) {
2226
- if (priorityQueue[i].distance < priorityQueue[minIndex].distance) {
2227
- minIndex = i;
2228
- }
2229
- }
2230
- const current = priorityQueue.splice(minIndex, 1)[0];
2902
+ const current = minHeapPop(priorityQueue);
2231
2903
  const currentNode = current.node;
2232
2904
  // Skip if already visited (can happen with duplicate entries)
2233
2905
  if (visited.has(currentNode)) {
@@ -2241,10 +2913,10 @@ export const dijkstra = /*#__PURE__*/dual(2, (graph, config) => {
2241
2913
  // Get current distance
2242
2914
  const currentDistance = distances.get(currentNode);
2243
2915
  // Examine all outgoing edges
2244
- const adjacencyList = graph.adjacency.get(currentNode);
2916
+ const adjacencyList = impl.adjacency.get(currentNode);
2245
2917
  if (adjacencyList !== undefined) {
2246
2918
  for (const edgeIndex of adjacencyList) {
2247
- const edge = graph.edges.get(edgeIndex);
2919
+ const edge = impl.edges.get(edgeIndex);
2248
2920
  if (edge !== undefined) {
2249
2921
  const neighbor = getTraversableNeighbor(graph, currentNode, edge);
2250
2922
  const cost = edgeWeights.get(edgeIndex);
@@ -2259,9 +2931,10 @@ export const dijkstra = /*#__PURE__*/dual(2, (graph, config) => {
2259
2931
  });
2260
2932
  // Add to priority queue if not visited
2261
2933
  if (!visited.has(neighbor)) {
2262
- priorityQueue.push({
2934
+ minHeapPush(priorityQueue, {
2263
2935
  node: neighbor,
2264
- distance: newDistance
2936
+ priority: newDistance,
2937
+ sequence: sequence++
2265
2938
  });
2266
2939
  }
2267
2940
  }
@@ -2301,8 +2974,9 @@ export const dijkstra = /*#__PURE__*/dual(2, (graph, config) => {
2301
2974
  * **Details**
2302
2975
  *
2303
2976
  * Computes distances, reconstructed node paths, and edge-data paths for every
2304
- * source and target pair in O(V^3) time. Negative edge weights are allowed, but
2305
- * a `GraphError` is thrown if any negative cycle is detected.
2977
+ * source and target pair in O(V^3) time. Negative edge weights are allowed, and
2978
+ * `Infinity` behaves like an impassable edge. A `GraphError` is thrown if any
2979
+ * edge weight is `NaN` or `-Infinity`, or if any negative cycle is detected.
2306
2980
  *
2307
2981
  * **Example** (Finding all-pairs shortest paths)
2308
2982
  *
@@ -2327,8 +3001,9 @@ export const dijkstra = /*#__PURE__*/dual(2, (graph, config) => {
2327
3001
  * @since 3.18.0
2328
3002
  */
2329
3003
  export const floydWarshall = /*#__PURE__*/dual(2, (graph, cost) => {
3004
+ const impl = graphImpl(graph);
2330
3005
  // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration)
2331
- const allNodes = Array.from(graph.nodes.keys());
3006
+ const allNodes = Array.from(impl.nodes.keys());
2332
3007
  // Initialize distance matrix
2333
3008
  const distances = new Map();
2334
3009
  const next = new Map();
@@ -2340,13 +3015,16 @@ export const floydWarshall = /*#__PURE__*/dual(2, (graph, cost) => {
2340
3015
  edgeMatrix.set(i, new Map());
2341
3016
  for (const j of allNodes) {
2342
3017
  distances.get(i).set(j, i === j ? 0 : Infinity);
2343
- next.get(i).set(j, null);
2344
- edgeMatrix.get(i).set(j, null);
2345
3018
  }
2346
3019
  }
2347
3020
  // Set edge weights
2348
- for (const [, edgeData] of graph.edges) {
3021
+ for (const [, edgeData] of impl.edges) {
2349
3022
  const weight = cost(edgeData.data);
3023
+ if (Number.isNaN(weight) || weight === -Infinity) {
3024
+ throw new GraphError({
3025
+ message: "Floyd-Warshall algorithm does not support NaN or -Infinity edge weights"
3026
+ });
3027
+ }
2350
3028
  const i = edgeData.source;
2351
3029
  const j = edgeData.target;
2352
3030
  // Use minimum weight if multiple edges exist
@@ -2373,8 +3051,11 @@ export const floydWarshall = /*#__PURE__*/dual(2, (graph, cost) => {
2373
3051
  const distKJ = distances.get(k).get(j);
2374
3052
  const distIJ = distances.get(i).get(j);
2375
3053
  if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) {
2376
- distances.get(i).set(j, distIK + distKJ);
2377
- next.get(i).set(j, next.get(i).get(k));
3054
+ const nextIK = next.get(i).get(k);
3055
+ if (nextIK !== undefined) {
3056
+ distances.get(i).set(j, distIK + distKJ);
3057
+ next.get(i).set(j, nextIK);
3058
+ }
2378
3059
  }
2379
3060
  }
2380
3061
  }
@@ -2408,10 +3089,10 @@ export const floydWarshall = /*#__PURE__*/dual(2, (graph, cost) => {
2408
3089
  path.push(current);
2409
3090
  while (current !== j) {
2410
3091
  const nextNode = next.get(current).get(j);
2411
- if (nextNode === null) break;
2412
- const edgeData = edgeMatrix.get(current).get(nextNode);
2413
- if (edgeData !== null) {
2414
- weights.push(edgeData);
3092
+ if (nextNode === undefined) break;
3093
+ const edgeRow = edgeMatrix.get(current);
3094
+ if (edgeRow.has(nextNode)) {
3095
+ weights.push(edgeRow.get(nextNode));
2415
3096
  }
2416
3097
  current = nextNode;
2417
3098
  path.push(current);
@@ -2433,10 +3114,11 @@ export const floydWarshall = /*#__PURE__*/dual(2, (graph, cost) => {
2433
3114
  *
2434
3115
  * **Details**
2435
3116
  *
2436
- * The edge-cost function must return non-negative weights, and the heuristic
3117
+ * The edge-cost function must return non-negative weights and not `NaN`.
3118
+ * `Infinity` is allowed and behaves like an impassable edge. The heuristic
2437
3119
  * should be consistent to preserve shortest-path guarantees. Returns
2438
3120
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
2439
- * when either endpoint is missing or a negative edge cost is encountered.
3121
+ * when either endpoint is missing or an edge cost is negative or `NaN`.
2440
3122
  *
2441
3123
  * **Example** (Finding shortest paths with A-star)
2442
3124
  *
@@ -2474,14 +3156,24 @@ export const floydWarshall = /*#__PURE__*/dual(2, (graph, cost) => {
2474
3156
  * @since 3.18.0
2475
3157
  */
2476
3158
  export const astar = /*#__PURE__*/dual(2, (graph, config) => {
3159
+ const impl = graphImpl(graph);
2477
3160
  // Validate that source and target nodes exist
2478
- if (!graph.nodes.has(config.source)) {
3161
+ if (!impl.nodes.has(config.source)) {
2479
3162
  throw missingNode(config.source);
2480
3163
  }
2481
- if (!graph.nodes.has(config.target)) {
3164
+ if (!impl.nodes.has(config.target)) {
2482
3165
  throw missingNode(config.target);
2483
3166
  }
2484
- const edgeWeights = validateNonNegativeEdgeWeights(graph, config.cost, "A* algorithm");
3167
+ const edgeWeights = new Map();
3168
+ for (const [edgeIndex, edgeData] of impl.edges) {
3169
+ const weight = config.cost(edgeData.data);
3170
+ if (Number.isNaN(weight) || weight < 0) {
3171
+ throw new GraphError({
3172
+ message: "A* algorithm requires non-negative edge weights"
3173
+ });
3174
+ }
3175
+ edgeWeights.set(edgeIndex, weight);
3176
+ }
2485
3177
  // Early return if source equals target
2486
3178
  if (config.source === config.target) {
2487
3179
  return Option.some({
@@ -2504,7 +3196,7 @@ export const astar = /*#__PURE__*/dual(2, (graph, config) => {
2504
3196
  const visited = new Set();
2505
3197
  // Initialize scores
2506
3198
  // Iterate directly over node keys
2507
- for (const node of graph.nodes.keys()) {
3199
+ for (const node of impl.nodes.keys()) {
2508
3200
  gScore.set(node, node === config.source ? 0 : Infinity);
2509
3201
  fScore.set(node, Infinity);
2510
3202
  previous.set(node, null);
@@ -2515,20 +3207,15 @@ export const astar = /*#__PURE__*/dual(2, (graph, config) => {
2515
3207
  const h = config.heuristic(sourceNodeData.value, targetNodeData.value);
2516
3208
  fScore.set(config.source, h);
2517
3209
  }
2518
- // Priority queue using f-score (total estimated cost)
2519
- const openSet = [{
3210
+ const openSet = [];
3211
+ let sequence = 0;
3212
+ minHeapPush(openSet, {
2520
3213
  node: config.source,
2521
- fScore: fScore.get(config.source)
2522
- }];
3214
+ priority: fScore.get(config.source),
3215
+ sequence: sequence++
3216
+ });
2523
3217
  while (openSet.length > 0) {
2524
- // Find node with lowest f-score
2525
- let minIndex = 0;
2526
- for (let i = 1; i < openSet.length; i++) {
2527
- if (openSet[i].fScore < openSet[minIndex].fScore) {
2528
- minIndex = i;
2529
- }
2530
- }
2531
- const current = openSet.splice(minIndex, 1)[0];
3218
+ const current = minHeapPop(openSet);
2532
3219
  const currentNode = current.node;
2533
3220
  // Skip if already visited
2534
3221
  if (visited.has(currentNode)) {
@@ -2542,10 +3229,10 @@ export const astar = /*#__PURE__*/dual(2, (graph, config) => {
2542
3229
  // Get current g-score
2543
3230
  const currentGScore = gScore.get(currentNode);
2544
3231
  // Examine all outgoing edges
2545
- const adjacencyList = graph.adjacency.get(currentNode);
3232
+ const adjacencyList = impl.adjacency.get(currentNode);
2546
3233
  if (adjacencyList !== undefined) {
2547
3234
  for (const edgeIndex of adjacencyList) {
2548
- const edge = graph.edges.get(edgeIndex);
3235
+ const edge = impl.edges.get(edgeIndex);
2549
3236
  if (edge !== undefined) {
2550
3237
  const neighbor = getTraversableNeighbor(graph, currentNode, edge);
2551
3238
  const weight = edgeWeights.get(edgeIndex);
@@ -2567,9 +3254,10 @@ export const astar = /*#__PURE__*/dual(2, (graph, config) => {
2567
3254
  fScore.set(neighbor, f);
2568
3255
  // Add to open set if not visited
2569
3256
  if (!visited.has(neighbor)) {
2570
- openSet.push({
3257
+ minHeapPush(openSet, {
2571
3258
  node: neighbor,
2572
- fScore: f
3259
+ priority: f,
3260
+ sequence: sequence++
2573
3261
  });
2574
3262
  }
2575
3263
  }
@@ -2609,9 +3297,10 @@ export const astar = /*#__PURE__*/dual(2, (graph, config) => {
2609
3297
  *
2610
3298
  * **Details**
2611
3299
  *
2612
- * Negative edge weights are allowed. Returns `Option.none()` when the target is
2613
- * unreachable or when a negative cycle affects the path to the target. Throws a
2614
- * `GraphError` when either endpoint is missing.
3300
+ * Negative edge weights are allowed, and `Infinity` behaves like an impassable
3301
+ * edge. Returns `Option.none()` when the target is unreachable or when a
3302
+ * negative cycle affects the path to the target. Throws a `GraphError` when
3303
+ * either endpoint is missing or an edge weight is `NaN` or `-Infinity`.
2615
3304
  *
2616
3305
  * **Example** (Finding shortest paths with Bellman-Ford)
2617
3306
  *
@@ -2643,33 +3332,31 @@ export const astar = /*#__PURE__*/dual(2, (graph, config) => {
2643
3332
  * @since 3.18.0
2644
3333
  */
2645
3334
  export const bellmanFord = /*#__PURE__*/dual(2, (graph, config) => {
3335
+ const impl = graphImpl(graph);
2646
3336
  // Validate that source and target nodes exist
2647
- if (!graph.nodes.has(config.source)) {
3337
+ if (!impl.nodes.has(config.source)) {
2648
3338
  throw missingNode(config.source);
2649
3339
  }
2650
- if (!graph.nodes.has(config.target)) {
3340
+ if (!impl.nodes.has(config.target)) {
2651
3341
  throw missingNode(config.target);
2652
3342
  }
2653
- // Early return if source equals target
2654
- if (config.source === config.target) {
2655
- return Option.some({
2656
- path: [config.source],
2657
- distance: 0,
2658
- costs: []
2659
- });
2660
- }
2661
3343
  // Initialize distances and predecessors
2662
3344
  const distances = new Map();
2663
3345
  const previous = new Map();
2664
3346
  // Iterate directly over node keys
2665
- for (const node of graph.nodes.keys()) {
3347
+ for (const node of impl.nodes.keys()) {
2666
3348
  distances.set(node, node === config.source ? 0 : Infinity);
2667
3349
  previous.set(node, null);
2668
3350
  }
2669
3351
  // Collect all edges for relaxation
2670
3352
  const edges = [];
2671
- for (const [, edgeData] of graph.edges) {
3353
+ for (const [, edgeData] of impl.edges) {
2672
3354
  const weight = config.cost(edgeData.data);
3355
+ if (Number.isNaN(weight) || weight === -Infinity) {
3356
+ throw new GraphError({
3357
+ message: "Bellman-Ford algorithm does not support NaN or -Infinity edge weights"
3358
+ });
3359
+ }
2673
3360
  edges.push({
2674
3361
  source: edgeData.source,
2675
3362
  target: edgeData.target,
@@ -2686,7 +3373,7 @@ export const bellmanFord = /*#__PURE__*/dual(2, (graph, config) => {
2686
3373
  }
2687
3374
  }
2688
3375
  // Relax edges up to V-1 times
2689
- const nodeCount = graph.nodes.size;
3376
+ const nodeCount = impl.nodes.size;
2690
3377
  for (let i = 0; i < nodeCount - 1; i++) {
2691
3378
  let hasUpdate = false;
2692
3379
  for (const edge of edges) {
@@ -2868,7 +3555,7 @@ export class Walker {
2868
3555
  */
2869
3556
  visit) {
2870
3557
  this.visit = visit;
2871
- this[Symbol.iterator] = visit((index, data) => [index, data])[Symbol.iterator];
3558
+ this[Symbol.iterator] = () => visit((index, data) => [index, data])[Symbol.iterator]();
2872
3559
  }
2873
3560
  }
2874
3561
  /**
@@ -2947,7 +3634,8 @@ export const entries = walker => walker.visit((index, data) => [index, data]);
2947
3634
  * **Details**
2948
3635
  *
2949
3636
  * If no start nodes are supplied, the iterator is empty. The `direction` option
2950
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
3637
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
3638
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
2951
3639
  * if any configured start node does not exist.
2952
3640
  *
2953
3641
  * **Example** (Traversing depth-first)
@@ -2980,6 +3668,7 @@ export const entries = walker => walker.visit((index, data) => [index, data]);
2980
3668
  export const dfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config = {}) => {
2981
3669
  const start = config.start ?? [];
2982
3670
  const direction = config.direction ?? "outgoing";
3671
+ const radius = config.radius ?? Infinity;
2983
3672
  // Validate that all start nodes exist
2984
3673
  for (const nodeIndex of start) {
2985
3674
  if (!hasNode(graph, nodeIndex)) {
@@ -2988,26 +3677,61 @@ export const dfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
2988
3677
  }
2989
3678
  return new Walker(f => ({
2990
3679
  [Symbol.iterator]: () => {
2991
- const stack = [...start];
2992
- const discovered = new Set();
3680
+ const depths = radius === Infinity ? undefined : new Map();
3681
+ const stack = [];
3682
+ const yielded = new Set();
3683
+ if (depths === undefined) {
3684
+ for (const node of start) {
3685
+ stack.push([node, 0]);
3686
+ }
3687
+ } else {
3688
+ const starts = new Set();
3689
+ for (let i = start.length - 1; i >= 0; i--) {
3690
+ const node = start[i];
3691
+ if (!starts.has(node)) {
3692
+ starts.add(node);
3693
+ stack.push([node, 0]);
3694
+ depths.set(node, 0);
3695
+ }
3696
+ }
3697
+ stack.reverse();
3698
+ }
2993
3699
  const nextMapped = () => {
2994
3700
  while (stack.length > 0) {
2995
- const current = stack.pop();
2996
- if (discovered.has(current)) {
3701
+ const [current, depth] = stack.pop();
3702
+ if (depths === undefined) {
3703
+ if (yielded.has(current)) {
3704
+ continue;
3705
+ }
3706
+ } else if (depths.get(current) !== depth) {
2997
3707
  continue;
2998
3708
  }
2999
- discovered.add(current);
3000
3709
  const nodeDataOption = getNode(graph, current);
3001
3710
  if (Option.isNone(nodeDataOption)) {
3002
3711
  continue;
3003
3712
  }
3004
- const neighbors = getTraversalNeighbors(graph, current, direction);
3005
- for (let i = neighbors.length - 1; i >= 0; i--) {
3006
- const neighbor = neighbors[i];
3007
- if (!discovered.has(neighbor)) {
3008
- stack.push(neighbor);
3713
+ if (depth < radius) {
3714
+ const neighbors = getTraversalNeighbors(graph, current, direction);
3715
+ for (let i = neighbors.length - 1; i >= 0; i--) {
3716
+ const neighbor = neighbors[i];
3717
+ const nextDepth = depth + 1;
3718
+ if (depths === undefined) {
3719
+ if (!yielded.has(neighbor)) {
3720
+ stack.push([neighbor, nextDepth]);
3721
+ }
3722
+ continue;
3723
+ }
3724
+ const neighborDepth = depths.get(neighbor);
3725
+ if (neighborDepth === undefined || nextDepth < neighborDepth) {
3726
+ depths.set(neighbor, nextDepth);
3727
+ stack.push([neighbor, nextDepth]);
3728
+ }
3009
3729
  }
3010
3730
  }
3731
+ if (yielded.has(current)) {
3732
+ continue;
3733
+ }
3734
+ yielded.add(current);
3011
3735
  return {
3012
3736
  done: false,
3013
3737
  value: f(current, nodeDataOption.value)
@@ -3031,7 +3755,8 @@ export const dfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3031
3755
  * **Details**
3032
3756
  *
3033
3757
  * If no start nodes are supplied, the iterator is empty. The `direction` option
3034
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
3758
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
3759
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
3035
3760
  * if any configured start node does not exist.
3036
3761
  *
3037
3762
  * **Example** (Traversing breadth-first)
@@ -3064,6 +3789,7 @@ export const dfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3064
3789
  export const bfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config = {}) => {
3065
3790
  const start = config.start ?? [];
3066
3791
  const direction = config.direction ?? "outgoing";
3792
+ const radius = config.radius ?? Infinity;
3067
3793
  // Validate that all start nodes exist
3068
3794
  for (const nodeIndex of start) {
3069
3795
  if (!hasNode(graph, nodeIndex)) {
@@ -3072,17 +3798,19 @@ export const bfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3072
3798
  }
3073
3799
  return new Walker(f => ({
3074
3800
  [Symbol.iterator]: () => {
3075
- const queue = [...start];
3801
+ const queue = start.map(node => [node, 0]);
3076
3802
  const discovered = new Set();
3077
3803
  const nextMapped = () => {
3078
3804
  while (queue.length > 0) {
3079
- const current = queue.shift();
3805
+ const [current, depth] = queue.shift();
3080
3806
  if (!discovered.has(current)) {
3081
3807
  discovered.add(current);
3082
- const neighbors = getTraversalNeighbors(graph, current, direction);
3083
- for (const neighbor of neighbors) {
3084
- if (!discovered.has(neighbor)) {
3085
- queue.push(neighbor);
3808
+ if (depth < radius) {
3809
+ const neighbors = getTraversalNeighbors(graph, current, direction);
3810
+ for (const neighbor of neighbors) {
3811
+ if (!discovered.has(neighbor)) {
3812
+ queue.push([neighbor, depth + 1]);
3813
+ }
3086
3814
  }
3087
3815
  }
3088
3816
  const nodeData = getNode(graph, current);
@@ -3092,7 +3820,7 @@ export const bfs = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3092
3820
  value: f(current, nodeData.value)
3093
3821
  };
3094
3822
  }
3095
- return nextMapped();
3823
+ continue;
3096
3824
  }
3097
3825
  }
3098
3826
  return {
@@ -3173,17 +3901,18 @@ export const topo = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3173
3901
  }
3174
3902
  return new Walker(f => ({
3175
3903
  [Symbol.iterator]: () => {
3904
+ const impl = graphImpl(graph);
3176
3905
  const inDegree = new Map();
3177
3906
  const remaining = new Set();
3178
3907
  const initialSet = new Set(initials);
3179
3908
  const queue = [...initials];
3180
3909
  // Initialize in-degree counts
3181
- for (const [nodeIndex] of graph.nodes) {
3910
+ for (const [nodeIndex] of impl.nodes) {
3182
3911
  inDegree.set(nodeIndex, 0);
3183
3912
  remaining.add(nodeIndex);
3184
3913
  }
3185
3914
  // Calculate in-degrees
3186
- for (const [, edgeData] of graph.edges) {
3915
+ for (const [, edgeData] of impl.edges) {
3187
3916
  const currentInDegree = inDegree.get(edgeData.target) || 0;
3188
3917
  inDegree.set(edgeData.target, currentInDegree + 1);
3189
3918
  }
@@ -3225,9 +3954,14 @@ export const topo = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3225
3954
  value: f(current, nodeData.value)
3226
3955
  };
3227
3956
  }
3228
- return nextMapped();
3957
+ continue;
3229
3958
  }
3230
3959
  }
3960
+ if (remaining.size > 0) {
3961
+ throw new GraphError({
3962
+ message: "Cannot perform topological sort on cyclic graph"
3963
+ });
3964
+ }
3231
3965
  return {
3232
3966
  done: true,
3233
3967
  value: undefined
@@ -3247,7 +3981,14 @@ export const topo = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3247
3981
  *
3248
3982
  * Nodes are emitted after their reachable descendants have been processed. If
3249
3983
  * no start nodes are supplied, the iterator is empty. The `direction` option
3250
- * chooses whether to follow outgoing or incoming edges.
3984
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
3985
+ * limits traversal by edge distance from the start nodes.
3986
+ *
3987
+ * **Gotchas**
3988
+ *
3989
+ * With a finite `radius`, iteration first performs a bounded breadth-first
3990
+ * traversal to determine shortest-distance membership before emitting nodes in
3991
+ * postorder.
3251
3992
  *
3252
3993
  * **Example** (Traversing in postorder)
3253
3994
  *
@@ -3275,6 +4016,7 @@ export const topo = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config =
3275
4016
  export const dfsPostOrder = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, config = {}) => {
3276
4017
  const start = config.start ?? [];
3277
4018
  const direction = config.direction ?? "outgoing";
4019
+ const radius = config.radius ?? Infinity;
3278
4020
  // Validate that all start nodes exist
3279
4021
  for (const nodeIndex of start) {
3280
4022
  if (!hasNode(graph, nodeIndex)) {
@@ -3283,6 +4025,11 @@ export const dfsPostOrder = /*#__PURE__*/dual(args => isGraph(args[0]), (graph,
3283
4025
  }
3284
4026
  return new Walker(f => ({
3285
4027
  [Symbol.iterator]: () => {
4028
+ const reached = radius === Infinity ? undefined : new Set(indices(bfs(graph, {
4029
+ start,
4030
+ direction,
4031
+ radius
4032
+ })));
3286
4033
  const stack = [];
3287
4034
  const discovered = new Set();
3288
4035
  const finished = new Set();
@@ -3305,7 +4052,7 @@ export const dfsPostOrder = /*#__PURE__*/dual(args => isGraph(args[0]), (graph,
3305
4052
  const neighbors = getTraversalNeighbors(graph, current.node, direction);
3306
4053
  for (let i = neighbors.length - 1; i >= 0; i--) {
3307
4054
  const neighbor = neighbors[i];
3308
- if (!discovered.has(neighbor) && !finished.has(neighbor)) {
4055
+ if ((reached === undefined || reached.has(neighbor)) && !discovered.has(neighbor) && !finished.has(neighbor)) {
3309
4056
  stack.push({
3310
4057
  node: neighbor,
3311
4058
  visitedChildren: false
@@ -3323,7 +4070,7 @@ export const dfsPostOrder = /*#__PURE__*/dual(args => isGraph(args[0]), (graph,
3323
4070
  value: f(nodeToEmit, nodeData.value)
3324
4071
  };
3325
4072
  }
3326
- return nextMapped();
4073
+ continue;
3327
4074
  }
3328
4075
  }
3329
4076
  }
@@ -3367,7 +4114,7 @@ export const dfsPostOrder = /*#__PURE__*/dual(args => isGraph(args[0]), (graph,
3367
4114
  */
3368
4115
  export const nodes = graph => new Walker(f => ({
3369
4116
  [Symbol.iterator]() {
3370
- const nodeMap = graph.nodes;
4117
+ const nodeMap = graphImpl(graph).nodes;
3371
4118
  const iterator = nodeMap.entries();
3372
4119
  return {
3373
4120
  next() {
@@ -3417,7 +4164,7 @@ export const nodes = graph => new Walker(f => ({
3417
4164
  */
3418
4165
  export const edges = graph => new Walker(f => ({
3419
4166
  [Symbol.iterator]() {
3420
- const edgeMap = graph.edges;
4167
+ const edgeMap = graphImpl(graph).edges;
3421
4168
  const iterator = edgeMap.entries();
3422
4169
  return {
3423
4170
  next() {
@@ -3481,8 +4228,9 @@ export const externals = /*#__PURE__*/dual(args => isGraph(args[0]), (graph, con
3481
4228
  const direction = config.direction ?? "outgoing";
3482
4229
  return new Walker(f => ({
3483
4230
  [Symbol.iterator]: () => {
3484
- const nodeMap = graph.nodes;
3485
- const adjacencyMap = direction === "incoming" ? graph.reverseAdjacency : graph.adjacency;
4231
+ const impl = graphImpl(graph);
4232
+ const nodeMap = impl.nodes;
4233
+ const adjacencyMap = direction === "incoming" ? impl.reverseAdjacency : impl.adjacency;
3486
4234
  const nodeIterator = nodeMap.entries();
3487
4235
  const nextMapped = () => {
3488
4236
  let current = nodeIterator.next();