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/src/Graph.ts CHANGED
@@ -15,11 +15,12 @@ import { dual } from "./Function.ts"
15
15
  import * as Hash from "./Hash.ts"
16
16
  import type { Inspectable } from "./Inspectable.ts"
17
17
  import { NodeInspectSymbol } from "./Inspectable.ts"
18
+ import * as MutableHashMap from "./MutableHashMap.ts"
18
19
  import * as Option from "./Option.ts"
19
20
  import type { Pipeable } from "./Pipeable.ts"
20
21
  import { pipeArguments } from "./Pipeable.ts"
21
22
  import { hasProperty } from "./Predicate.ts"
22
- import type { Mutable } from "./Types.ts"
23
+ import type { Covariant, Invariant, Mutable } from "./Types.ts"
23
24
 
24
25
  const TypeId = "~effect/collections/Graph"
25
26
 
@@ -109,25 +110,19 @@ export class Edge<E> extends Data.Class<{
109
110
  export type Kind = "directed" | "undirected"
110
111
 
111
112
  /**
112
- * Common structural interface shared by immutable and mutable graphs.
113
+ * Common public protocol for graph values.
113
114
  *
114
115
  * **Details**
115
116
  *
116
- * Contains the node and edge maps, adjacency indexes, allocation counters, and
117
- * shared protocols used by both `Graph` and `MutableGraph`.
117
+ * Contains only the runtime marker and shared protocols. Graph storage is kept
118
+ * internal; use module functions such as `nodes`, `edges`, `getNode`, and
119
+ * `getEdge` to inspect graph contents.
118
120
  *
119
121
  * @category models
120
122
  * @since 3.18.0
121
123
  */
122
124
  export interface Proto<out N, out E> extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable {
123
- readonly [TypeId]: typeof TypeId
124
- readonly nodes: Map<NodeIndex, N>
125
- readonly edges: Map<EdgeIndex, Edge<E>>
126
- readonly adjacency: Map<NodeIndex, Array<EdgeIndex>>
127
- readonly reverseAdjacency: Map<NodeIndex, Array<EdgeIndex>>
128
- nextNodeIndex: NodeIndex
129
- nextEdgeIndex: EdgeIndex
130
- acyclic: Option.Option<boolean>
125
+ readonly [TypeId]: Graph.Variance<N, E>
131
126
  }
132
127
 
133
128
  /**
@@ -150,6 +145,25 @@ export interface Graph<out N, out E, T extends Kind = "directed"> extends Proto<
150
145
  readonly mutable: false
151
146
  }
152
147
 
148
+ /**
149
+ * Companion namespace containing type-level metadata for immutable graphs.
150
+ *
151
+ * @category models
152
+ * @since 4.0.0
153
+ */
154
+ export declare namespace Graph {
155
+ /**
156
+ * Type-level variance marker for immutable graphs.
157
+ *
158
+ * @category models
159
+ * @since 4.0.0
160
+ */
161
+ export interface Variance<out N, out E> {
162
+ readonly _N: Covariant<N>
163
+ readonly _E: Covariant<E>
164
+ }
165
+ }
166
+
153
167
  /**
154
168
  * Mutable graph interface.
155
169
  *
@@ -166,11 +180,63 @@ export interface Graph<out N, out E, T extends Kind = "directed"> extends Proto<
166
180
  * @category models
167
181
  * @since 3.18.0
168
182
  */
169
- export interface MutableGraph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
183
+ export interface MutableGraph<in out N, in out E, T extends Kind = "directed">
184
+ extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable
185
+ {
186
+ readonly [TypeId]: MutableGraph.Variance<N, E>
170
187
  readonly type: T
171
188
  readonly mutable: true
172
189
  }
173
190
 
191
+ /**
192
+ * Companion namespace containing type-level metadata for scoped mutable graphs.
193
+ *
194
+ * @category models
195
+ * @since 4.0.0
196
+ */
197
+ export declare namespace MutableGraph {
198
+ /**
199
+ * Type-level variance marker for scoped mutable graphs.
200
+ *
201
+ * @category models
202
+ * @since 4.0.0
203
+ */
204
+ export interface Variance<in out N, in out E> {
205
+ readonly _N: Invariant<N>
206
+ readonly _E: Invariant<E>
207
+ }
208
+ }
209
+
210
+ /** @internal */
211
+ interface GraphImpl<in out N, in out E, T extends Kind = "directed">
212
+ extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable
213
+ {
214
+ readonly [TypeId]: unknown
215
+ readonly type: T
216
+ mutable: boolean
217
+ nodes: Map<NodeIndex, N>
218
+ edges: Map<EdgeIndex, Edge<E>>
219
+ adjacency: Map<NodeIndex, Array<EdgeIndex>>
220
+ reverseAdjacency: Map<NodeIndex, Array<EdgeIndex>>
221
+ nextNodeIndex: NodeIndex
222
+ nextEdgeIndex: EdgeIndex
223
+ acyclic: Option.Option<boolean>
224
+ }
225
+
226
+ /** @internal */
227
+ const graphImpl = <N, E, T extends Kind = "directed">(
228
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
229
+ ): GraphImpl<N, E, T> => graph as unknown as GraphImpl<N, E, T>
230
+
231
+ /** @internal */
232
+ const cloneAdjacency = (adjacency: Map<NodeIndex, Array<EdgeIndex>>): Map<NodeIndex, Array<EdgeIndex>> => {
233
+ const cloned = new Map<NodeIndex, Array<EdgeIndex>>()
234
+ for (const [nodeIndex, edges] of adjacency) {
235
+ cloned.set(nodeIndex, [...edges])
236
+ }
237
+ return cloned
238
+ }
239
+
174
240
  /**
175
241
  * Immutable graph type for source-to-target relationships.
176
242
  *
@@ -253,41 +319,61 @@ export type MutableUndirectedGraph<N, E> = MutableGraph<N, E, "undirected">
253
319
  // Proto Objects
254
320
  // =============================================================================
255
321
 
322
+ /** @internal */
323
+ const edgeEquals = (type: Kind, self: Edge<any>, that: Edge<any>): boolean =>
324
+ (type === "directed"
325
+ ? self.source === that.source && self.target === that.target
326
+ : (self.source === that.source && self.target === that.target) ||
327
+ (self.source === that.target && self.target === that.source)) &&
328
+ Equal.equals(self.data, that.data)
329
+
330
+ /** @internal */
331
+ const edgeHash = (type: Kind, edge: Edge<any>): number =>
332
+ type === "directed"
333
+ ? Hash.hash(edge)
334
+ : Hash.optimize(Hash.hash(edge.data) ^ (Hash.hash(edge.source) + Hash.hash(edge.target)))
335
+
256
336
  /** @internal */
257
337
  const ProtoGraph = {
258
- [TypeId]: TypeId,
259
- [Symbol.iterator](this: Graph<any, any>) {
338
+ [TypeId]: {
339
+ _N: (_: never) => _,
340
+ _E: (_: never) => _
341
+ },
342
+ [Symbol.iterator](this: GraphImpl<any, any>) {
260
343
  return this.nodes[Symbol.iterator]()
261
344
  },
262
- [NodeInspectSymbol](this: Graph<any, any>) {
345
+ [NodeInspectSymbol](this: GraphImpl<any, any>) {
263
346
  return this.toJSON()
264
347
  },
265
- [Equal.symbol](this: Graph<any, any>, that: Equal.Equal): boolean {
348
+ [Equal.symbol](this: GraphImpl<any, any>, that: Equal.Equal): boolean {
266
349
  if (isGraph(that)) {
350
+ const thatImpl = graphImpl(that)
267
351
  if (
268
- this.nodes.size !== that.nodes.size ||
269
- this.edges.size !== that.edges.size ||
270
- this.type !== that.type
352
+ this.nodes.size !== thatImpl.nodes.size ||
353
+ this.edges.size !== thatImpl.edges.size ||
354
+ this.nextNodeIndex !== thatImpl.nextNodeIndex ||
355
+ this.nextEdgeIndex !== thatImpl.nextEdgeIndex ||
356
+ this.type !== thatImpl.type
271
357
  ) {
272
358
  return false
273
359
  }
274
360
  // Compare nodes
275
361
  for (const [nodeIndex, nodeData] of this.nodes) {
276
- if (!that.nodes.has(nodeIndex)) {
362
+ if (!thatImpl.nodes.has(nodeIndex)) {
277
363
  return false
278
364
  }
279
- const otherNodeData = that.nodes.get(nodeIndex)!
365
+ const otherNodeData = thatImpl.nodes.get(nodeIndex)!
280
366
  if (!Equal.equals(nodeData, otherNodeData)) {
281
367
  return false
282
368
  }
283
369
  }
284
370
  // Compare edges
285
371
  for (const [edgeIndex, edgeData] of this.edges) {
286
- if (!that.edges.has(edgeIndex)) {
372
+ if (!thatImpl.edges.has(edgeIndex)) {
287
373
  return false
288
374
  }
289
- const otherEdge = that.edges.get(edgeIndex)!
290
- if (!Equal.equals(edgeData, otherEdge)) {
375
+ const otherEdge = thatImpl.edges.get(edgeIndex)!
376
+ if (!edgeEquals(this.type, edgeData, otherEdge)) {
291
377
  return false
292
378
  }
293
379
  }
@@ -295,20 +381,22 @@ const ProtoGraph = {
295
381
  }
296
382
  return false
297
383
  },
298
- [Hash.symbol](this: Graph<any, any>): number {
384
+ [Hash.symbol](this: GraphImpl<any, any>): number {
299
385
  let hash = Hash.string("Graph")
300
386
  hash = hash ^ Hash.string(this.type)
301
387
  hash = hash ^ Hash.number(this.nodes.size)
302
388
  hash = hash ^ Hash.number(this.edges.size)
389
+ hash = hash ^ Hash.number(this.nextNodeIndex)
390
+ hash = hash ^ Hash.number(this.nextEdgeIndex)
303
391
  for (const [nodeIndex, nodeData] of this.nodes) {
304
392
  hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData))
305
393
  }
306
394
  for (const [edgeIndex, edgeData] of this.edges) {
307
- hash = hash ^ (Hash.hash(edgeIndex) + Hash.hash(edgeData))
395
+ hash = hash ^ (Hash.hash(edgeIndex) + edgeHash(this.type, edgeData))
308
396
  }
309
397
  return hash
310
398
  },
311
- toJSON(this: Graph<any, any>) {
399
+ toJSON(this: GraphImpl<any, any>) {
312
400
  return {
313
401
  _id: "Graph",
314
402
  nodeCount: this.nodes.size,
@@ -316,7 +404,7 @@ const ProtoGraph = {
316
404
  type: this.type
317
405
  }
318
406
  },
319
- toString(this: Graph<any, any>) {
407
+ toString(this: GraphImpl<any, any>) {
320
408
  return `Graph(${this.type}, ${this.nodes.size}, ${this.edges.size})`
321
409
  },
322
410
  pipe() {
@@ -350,13 +438,22 @@ export class GraphError extends Data.TaggedError("GraphError")<{
350
438
  /** @internal */
351
439
  const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` })
352
440
 
441
+ /** @internal */
442
+ function assertMutable<N, E, T extends Kind = "directed">(
443
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
444
+ ): asserts graph is MutableGraph<N, E, T> {
445
+ if (!graph.mutable) {
446
+ throw new GraphError({ message: "Graph is not mutable" })
447
+ }
448
+ }
449
+
353
450
  // =============================================================================
354
451
  // Constructors
355
452
  // =============================================================================
356
453
 
357
454
  /**
358
455
  * Returns `true` if a value has the graph runtime type identifier, narrowing
359
- * it to a `Graph`.
456
+ * it to an immutable or mutable graph.
360
457
  *
361
458
  * **When to use**
362
459
  *
@@ -365,12 +462,63 @@ const missingNode = (node: number) => new GraphError({ message: `Node ${node} do
365
462
  * **Gotchas**
366
463
  *
367
464
  * This guard checks the shared graph runtime type identifier and does not
368
- * distinguish immutable graphs from mutable graphs.
465
+ * distinguish immutable graphs from mutable graphs or directed graphs from
466
+ * undirected graphs.
369
467
  *
370
468
  * @category guards
371
469
  * @since 4.0.0
372
470
  */
373
- export const isGraph = (u: unknown): u is Graph<unknown, unknown> => hasProperty(u, TypeId)
471
+ export const isGraph = <N = unknown, E = unknown, T extends Kind = Kind, U = never>(
472
+ u: U | Graph<N, E, T> | MutableGraph<N, E, T>
473
+ ): u is Graph<N, E, T> | MutableGraph<N, E, T> => hasProperty(u, TypeId)
474
+
475
+ /**
476
+ * Creates a graph constructor for the specified graph kind.
477
+ *
478
+ * **When to use**
479
+ *
480
+ * Use when the graph kind is selected dynamically. Prefer `directed` or
481
+ * `undirected` when the kind is known statically.
482
+ *
483
+ * **Example** (Constructing by kind)
484
+ *
485
+ * ```ts
486
+ * import { Graph } from "effect"
487
+ *
488
+ * const makeGraph = Graph.make("directed")
489
+ * const graph = makeGraph<string, number>((mutable) => {
490
+ * Graph.addNode(mutable, "A")
491
+ * })
492
+ *
493
+ * console.log(graph.type) // "directed"
494
+ * ```
495
+ *
496
+ * @see {@link directed} for constructing a directed graph directly
497
+ * @see {@link undirected} for constructing an undirected graph directly
498
+ * @category constructors
499
+ * @since 4.0.0
500
+ */
501
+ export const make =
502
+ <T extends Kind>(type: T) => <N, E>(mutate?: (mutable: MutableGraph<N, E, T>) => undefined): Graph<N, E, T> => {
503
+ const graph: Mutable<GraphImpl<N, E, T>> = Object.create(ProtoGraph)
504
+ graph.type = type
505
+ graph.nodes = new Map()
506
+ graph.edges = new Map()
507
+ graph.adjacency = new Map()
508
+ graph.reverseAdjacency = new Map()
509
+ graph.nextNodeIndex = 0
510
+ graph.nextEdgeIndex = 0
511
+ graph.acyclic = Option.some(true)
512
+
513
+ if (mutate === undefined) {
514
+ graph.mutable = false
515
+ return graph as unknown as Graph<N, E, T>
516
+ }
517
+
518
+ graph.mutable = true
519
+ const mutable = Equal.byReferenceUnsafe(graph as unknown as MutableGraph<N, E, T>)
520
+ return mutateScoped(mutable, mutate)
521
+ }
374
522
 
375
523
  /**
376
524
  * Creates a directed graph, optionally with initial mutations.
@@ -393,26 +541,9 @@ export const isGraph = (u: unknown): u is Graph<unknown, unknown> => hasProperty
393
541
  * @category constructors
394
542
  * @since 3.18.0
395
543
  */
396
- export const directed = <N, E>(mutate?: (mutable: MutableDirectedGraph<N, E>) => void): DirectedGraph<N, E> => {
397
- const graph: Mutable<DirectedGraph<N, E>> = Object.create(ProtoGraph)
398
- graph.type = "directed"
399
- graph.nodes = new Map()
400
- graph.edges = new Map()
401
- graph.adjacency = new Map()
402
- graph.reverseAdjacency = new Map()
403
- graph.nextNodeIndex = 0
404
- graph.nextEdgeIndex = 0
405
- graph.acyclic = Option.some(true)
406
- graph.mutable = false
407
-
408
- if (mutate) {
409
- const mutable = beginMutation(graph as DirectedGraph<N, E>)
410
- mutate(mutable as MutableDirectedGraph<N, E>)
411
- return endMutation(mutable)
412
- }
413
-
414
- return graph
415
- }
544
+ export const directed: <N, E>(
545
+ mutate?: (mutable: MutableDirectedGraph<N, E>) => undefined
546
+ ) => DirectedGraph<N, E> = make("directed")
416
547
 
417
548
  /**
418
549
  * Creates an undirected graph, optionally with initial mutations.
@@ -435,26 +566,9 @@ export const directed = <N, E>(mutate?: (mutable: MutableDirectedGraph<N, E>) =>
435
566
  * @category constructors
436
567
  * @since 3.18.0
437
568
  */
438
- export const undirected = <N, E>(mutate?: (mutable: MutableUndirectedGraph<N, E>) => void): UndirectedGraph<N, E> => {
439
- const graph: Mutable<UndirectedGraph<N, E>> = Object.create(ProtoGraph)
440
- graph.type = "undirected"
441
- graph.nodes = new Map()
442
- graph.edges = new Map()
443
- graph.adjacency = new Map()
444
- graph.reverseAdjacency = new Map()
445
- graph.nextNodeIndex = 0
446
- graph.nextEdgeIndex = 0
447
- graph.acyclic = Option.some(true)
448
- graph.mutable = false
449
-
450
- if (mutate) {
451
- const mutable = beginMutation(graph)
452
- mutate(mutable as MutableUndirectedGraph<N, E>)
453
- return endMutation(mutable)
454
- }
455
-
456
- return graph
457
- }
569
+ export const undirected: <N, E>(
570
+ mutate?: (mutable: MutableUndirectedGraph<N, E>) => undefined
571
+ ) => UndirectedGraph<N, E> = make("undirected")
458
572
 
459
573
  // =============================================================================
460
574
  // Scoped Mutable API
@@ -479,35 +593,32 @@ export const undirected = <N, E>(mutate?: (mutable: MutableUndirectedGraph<N, E>
479
593
  export const beginMutation = <N, E, T extends Kind = "directed">(
480
594
  graph: Graph<N, E, T>
481
595
  ): MutableGraph<N, E, T> => {
482
- // Copy adjacency maps with deep cloned arrays
483
- const adjacency = new Map<NodeIndex, Array<EdgeIndex>>()
484
- const reverseAdjacency = new Map<NodeIndex, Array<EdgeIndex>>()
485
-
486
- for (const [nodeIndex, edges] of graph.adjacency) {
487
- adjacency.set(nodeIndex, [...edges])
488
- }
489
-
490
- for (const [nodeIndex, edges] of graph.reverseAdjacency) {
491
- reverseAdjacency.set(nodeIndex, [...edges])
492
- }
493
-
494
- const mutable: Mutable<MutableGraph<N, E, T>> = Object.create(ProtoGraph)
495
- mutable.type = graph.type
496
- mutable.nodes = new Map(graph.nodes)
497
- mutable.edges = new Map(graph.edges)
596
+ const source = graphImpl(graph)
597
+ const adjacency = cloneAdjacency(source.adjacency)
598
+ const reverseAdjacency = cloneAdjacency(source.reverseAdjacency)
599
+
600
+ const mutable: Mutable<GraphImpl<N, E, T>> = Object.create(ProtoGraph)
601
+ mutable.type = source.type
602
+ mutable.nodes = new Map(source.nodes)
603
+ mutable.edges = new Map(source.edges)
498
604
  mutable.adjacency = adjacency
499
605
  mutable.reverseAdjacency = reverseAdjacency
500
- mutable.nextNodeIndex = graph.nextNodeIndex
501
- mutable.nextEdgeIndex = graph.nextEdgeIndex
502
- mutable.acyclic = graph.acyclic
606
+ mutable.nextNodeIndex = source.nextNodeIndex
607
+ mutable.nextEdgeIndex = source.nextEdgeIndex
608
+ mutable.acyclic = source.acyclic
503
609
  mutable.mutable = true
504
610
 
505
- return mutable
611
+ return Equal.byReferenceUnsafe(mutable as unknown as MutableGraph<N, E, T>)
506
612
  }
507
613
 
508
614
  /**
509
615
  * Converts a mutable graph back to an immutable graph, ending the mutation scope.
510
616
  *
617
+ * **Details**
618
+ *
619
+ * Finalizes the mutable handle. Later public mutation operations on that handle
620
+ * fail with a `GraphError`.
621
+ *
511
622
  * **Example** (Ending a mutation scope)
512
623
  *
513
624
  * ```ts
@@ -525,96 +636,2247 @@ export const beginMutation = <N, E, T extends Kind = "directed">(
525
636
  export const endMutation = <N, E, T extends Kind = "directed">(
526
637
  mutable: MutableGraph<N, E, T>
527
638
  ): Graph<N, E, T> => {
528
- const graph: Mutable<Graph<N, E, T>> = Object.create(ProtoGraph)
529
- graph.type = mutable.type
530
- graph.nodes = new Map(mutable.nodes)
531
- graph.edges = new Map(mutable.edges)
532
- graph.adjacency = mutable.adjacency
533
- graph.reverseAdjacency = mutable.reverseAdjacency
534
- graph.nextNodeIndex = mutable.nextNodeIndex
535
- graph.nextEdgeIndex = mutable.nextEdgeIndex
536
- graph.acyclic = mutable.acyclic
639
+ assertMutable(mutable)
640
+ const source = graphImpl(mutable)
641
+
642
+ const graph: Mutable<GraphImpl<N, E, T>> = Object.create(ProtoGraph)
643
+ graph.type = source.type
644
+ graph.nodes = new Map(source.nodes)
645
+ graph.edges = new Map(source.edges)
646
+ graph.adjacency = cloneAdjacency(source.adjacency)
647
+ graph.reverseAdjacency = cloneAdjacency(source.reverseAdjacency)
648
+ graph.nextNodeIndex = source.nextNodeIndex
649
+ graph.nextEdgeIndex = source.nextEdgeIndex
650
+ graph.acyclic = source.acyclic
537
651
  graph.mutable = false
652
+ source.mutable = false
653
+
654
+ return graph as unknown as Graph<N, E, T>
655
+ }
656
+
657
+ /** @internal */
658
+ const mutateScoped = <N, E, T extends Kind>(
659
+ mutable: MutableGraph<N, E, T>,
660
+ f: (mutable: MutableGraph<N, E, T>) => undefined
661
+ ): Graph<N, E, T> => {
662
+ let graph: Graph<N, E, T>
663
+ try {
664
+ f(mutable)
665
+ } finally {
666
+ graph = endMutation(mutable)
667
+ }
668
+ return graph
669
+ }
670
+
671
+ /**
672
+ * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
673
+ *
674
+ * **Example** (Applying scoped mutations)
675
+ *
676
+ * ```ts
677
+ * import { Graph } from "effect"
678
+ *
679
+ * const graph = Graph.directed<string, number>()
680
+ * const newGraph = Graph.mutate(graph, (mutable) => {
681
+ * const nodeA = Graph.addNode(mutable, "A")
682
+ * const nodeB = Graph.addNode(mutable, "B")
683
+ * Graph.addEdge(mutable, nodeA, nodeB, 1)
684
+ * })
685
+ *
686
+ * console.log(Graph.nodeCount(newGraph)) // 2
687
+ * console.log(Graph.edgeCount(newGraph)) // 1
688
+ * ```
689
+ *
690
+ * @category mutations
691
+ * @since 3.18.0
692
+ */
693
+ export const mutate: {
694
+ /**
695
+ * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
696
+ *
697
+ * **Example** (Applying scoped mutations)
698
+ *
699
+ * ```ts
700
+ * import { Graph } from "effect"
701
+ *
702
+ * const graph = Graph.directed<string, number>()
703
+ * const newGraph = Graph.mutate(graph, (mutable) => {
704
+ * const nodeA = Graph.addNode(mutable, "A")
705
+ * const nodeB = Graph.addNode(mutable, "B")
706
+ * Graph.addEdge(mutable, nodeA, nodeB, 1)
707
+ * })
708
+ *
709
+ * console.log(Graph.nodeCount(newGraph)) // 2
710
+ * console.log(Graph.edgeCount(newGraph)) // 1
711
+ * ```
712
+ *
713
+ * @category mutations
714
+ * @since 3.18.0
715
+ */
716
+ <N, E, T extends Kind = "directed">(f: (mutable: MutableGraph<N, E, T>) => undefined): (graph: Graph<N, E, T>) => Graph<N, E, T>
717
+ /**
718
+ * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
719
+ *
720
+ * **Example** (Applying scoped mutations)
721
+ *
722
+ * ```ts
723
+ * import { Graph } from "effect"
724
+ *
725
+ * const graph = Graph.directed<string, number>()
726
+ * const newGraph = Graph.mutate(graph, (mutable) => {
727
+ * const nodeA = Graph.addNode(mutable, "A")
728
+ * const nodeB = Graph.addNode(mutable, "B")
729
+ * Graph.addEdge(mutable, nodeA, nodeB, 1)
730
+ * })
731
+ *
732
+ * console.log(Graph.nodeCount(newGraph)) // 2
733
+ * console.log(Graph.edgeCount(newGraph)) // 1
734
+ * ```
735
+ *
736
+ * @category mutations
737
+ * @since 3.18.0
738
+ */
739
+ <N, E, T extends Kind = "directed">(graph: Graph<N, E, T>, f: (mutable: MutableGraph<N, E, T>) => undefined): Graph<N, E, T>
740
+ } = dual(2, <N, E, T extends Kind = "directed">(
741
+ graph: Graph<N, E, T>,
742
+ f: (mutable: MutableGraph<N, E, T>) => undefined
743
+ ): Graph<N, E, T> => {
744
+ const mutable = beginMutation(graph)
745
+ return mutateScoped(mutable, f)
746
+ })
747
+
748
+ // =============================================================================
749
+ // Set Operations
750
+ // =============================================================================
751
+
752
+ /** @internal */
753
+ type NodeMaps<N, I> = {
754
+ readonly byIdentity: MutableHashMap.MutableHashMap<I, N>
755
+ readonly byIndex: Map<NodeIndex, I>
756
+ }
757
+
758
+ /** @internal */
759
+ class EdgeIdentity<NI, EI> implements Equal.Equal {
760
+ readonly type: Kind
761
+ readonly source: NI
762
+ readonly target: NI
763
+ readonly identity: EI
764
+
765
+ constructor(
766
+ type: Kind,
767
+ source: NI,
768
+ target: NI,
769
+ identity: EI
770
+ ) {
771
+ this.type = type
772
+ this.source = source
773
+ this.target = target
774
+ this.identity = identity
775
+ }
776
+
777
+ [Equal.symbol](that: Equal.Equal): boolean {
778
+ if (!(that instanceof EdgeIdentity) || this.type !== that.type || !Equal.equals(this.identity, that.identity)) {
779
+ return false
780
+ }
781
+
782
+ if (this.type === "directed") {
783
+ return Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target)
784
+ }
785
+
786
+ return (
787
+ (Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target)) ||
788
+ (Equal.equals(this.source, that.target) && Equal.equals(this.target, that.source))
789
+ )
790
+ }
791
+
792
+ [Hash.symbol](): number {
793
+ const hash = Hash.hash(this.identity)
794
+ return this.type === "directed"
795
+ ? Hash.combine(Hash.hash(this.target))(Hash.combine(Hash.hash(this.source))(hash))
796
+ : Hash.optimize(hash ^ (Hash.hash(this.source) + Hash.hash(this.target)))
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Configures node and edge identity for graph set operations.
802
+ *
803
+ * **Details**
804
+ *
805
+ * Both functions default to using the complete node or edge data. Edge identity
806
+ * also includes the identities of its endpoint nodes and the graph kind.
807
+ *
808
+ * **Gotchas**
809
+ *
810
+ * Edge identity defines set membership, not edge multiplicity. Parallel edges
811
+ * with the same endpoint identities and projected edge identity are treated as
812
+ * the same member by graph set operations.
813
+ *
814
+ * @category models
815
+ * @since 4.0.0
816
+ */
817
+ export interface IdentityOptions<N, E, NI = N, EI = E> {
818
+ readonly nodeIdentity?: (node: N) => NI
819
+ readonly edgeIdentity?: (edge: E) => EI
820
+ }
821
+
822
+ /** @internal */
823
+ const buildNodeMaps = <N, E, T extends Kind, I>(
824
+ graph: Graph<N, E, T>,
825
+ identity: (node: N) => I
826
+ ): NodeMaps<N, I> => {
827
+ const impl = graphImpl(graph)
828
+ const byIdentity = MutableHashMap.empty<I, N>()
829
+ const byIndex = new Map<NodeIndex, I>()
830
+
831
+ for (const [index, data] of impl.nodes) {
832
+ const nodeIdentity = identity(data)
833
+ MutableHashMap.set(byIdentity, nodeIdentity, data)
834
+ byIndex.set(index, nodeIdentity)
835
+ }
836
+
837
+ return { byIdentity, byIndex }
838
+ }
839
+
840
+ /** @internal */
841
+ const nodeIdentityAt = <N, I>(maps: NodeMaps<N, I>, index: NodeIndex): I => maps.byIndex.get(index) as I
842
+
843
+ /** @internal */
844
+ const buildEdgeMap = <N, E, T extends Kind, NI, EI>(
845
+ graph: Graph<N, E, T>,
846
+ nodeMaps: NodeMaps<N, NI>,
847
+ identity: (edge: E) => EI
848
+ ): MutableHashMap.MutableHashMap<EdgeIdentity<NI, EI>, E> => {
849
+ const impl = graphImpl(graph)
850
+ const edges = MutableHashMap.empty<EdgeIdentity<NI, EI>, E>()
851
+ for (const edge of impl.edges.values()) {
852
+ const sourceIdentity = nodeIdentityAt(nodeMaps, edge.source)
853
+ const targetIdentity = nodeIdentityAt(nodeMaps, edge.target)
854
+ MutableHashMap.set(
855
+ edges,
856
+ new EdgeIdentity(graph.type, sourceIdentity, targetIdentity, identity(edge.data)),
857
+ edge.data
858
+ )
859
+ }
860
+ return edges
861
+ }
862
+
863
+ /** @internal */
864
+ const addNodesByIdentity = <N, E, T extends Kind, NI>(
865
+ mutable: MutableGraph<N, E, T>,
866
+ nodes: Iterable<readonly [NI, N]>
867
+ ): MutableHashMap.MutableHashMap<NI, NodeIndex> => {
868
+ const indexByIdentity = MutableHashMap.empty<NI, NodeIndex>()
869
+ for (const [identity, data] of nodes) {
870
+ MutableHashMap.set(indexByIdentity, identity, addNode(mutable, data))
871
+ }
872
+ return indexByIdentity
873
+ }
874
+
875
+ /** @internal */
876
+ const addEdgeByIdentity = <N, E, T extends Kind, NI, EI>(
877
+ mutable: MutableGraph<N, E, T>,
878
+ indexByIdentity: MutableHashMap.MutableHashMap<NI, NodeIndex>,
879
+ identity: EdgeIdentity<NI, EI>,
880
+ data: E
881
+ ): void => {
882
+ const sourceIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.source))
883
+ const targetIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.target))
884
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
885
+ addEdge(mutable, sourceIndex, targetIndex, data)
886
+ }
887
+ }
888
+
889
+ /** @internal */
890
+ const assertSameKind = <N, E>(self: Graph<N, E, Kind>, that: Graph<N, E, Kind>): void => {
891
+ if (self.type !== that.type) {
892
+ throw new GraphError({ message: `Cannot combine ${self.type} and ${that.type} graphs` })
893
+ }
894
+ }
895
+
896
+ /**
897
+ * Composes two graphs, merging nodes by identity.
898
+ *
899
+ * **Details**
900
+ *
901
+ * Nodes and edges present in both graphs use data from `that`. The result has
902
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
903
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
904
+ * edge data. Edge identity also includes the endpoint identities.
905
+ *
906
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
907
+ *
908
+ * **Gotchas**
909
+ *
910
+ * Nodes with equal identities in one input graph are coalesced. The last node
911
+ * supplies the data, and redirected edges can collapse or become self-loops.
912
+ * Parallel edges with equal identities are also coalesced, with the last edge
913
+ * supplying the data.
914
+ *
915
+ * **Example** (Combining graphs)
916
+ *
917
+ * ```ts
918
+ * import { Graph } from "effect"
919
+ *
920
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
921
+ * const a = Graph.addNode(mutable, { id: "A" })
922
+ * const b = Graph.addNode(mutable, { id: "B" })
923
+ * Graph.addEdge(mutable, a, b, "A-B")
924
+ * })
925
+ *
926
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
927
+ * const b = Graph.addNode(mutable, { id: "B" })
928
+ * const c = Graph.addNode(mutable, { id: "C" })
929
+ * Graph.addEdge(mutable, b, c, "B-C")
930
+ * })
931
+ *
932
+ * const result = Graph.compose(left, right, {
933
+ * nodeIdentity: (node) => node.id
934
+ * })
935
+ *
936
+ * console.log(Graph.nodeCount(result)) // 3
937
+ * console.log(Graph.edgeCount(result)) // 2
938
+ * ```
939
+ *
940
+ * @category set operations
941
+ * @since 4.0.0
942
+ */
943
+ export const compose: {
944
+ /**
945
+ * Composes two graphs, merging nodes by identity.
946
+ *
947
+ * **Details**
948
+ *
949
+ * Nodes and edges present in both graphs use data from `that`. The result has
950
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
951
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
952
+ * edge data. Edge identity also includes the endpoint identities.
953
+ *
954
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
955
+ *
956
+ * **Gotchas**
957
+ *
958
+ * Nodes with equal identities in one input graph are coalesced. The last node
959
+ * supplies the data, and redirected edges can collapse or become self-loops.
960
+ * Parallel edges with equal identities are also coalesced, with the last edge
961
+ * supplying the data.
962
+ *
963
+ * **Example** (Combining graphs)
964
+ *
965
+ * ```ts
966
+ * import { Graph } from "effect"
967
+ *
968
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
969
+ * const a = Graph.addNode(mutable, { id: "A" })
970
+ * const b = Graph.addNode(mutable, { id: "B" })
971
+ * Graph.addEdge(mutable, a, b, "A-B")
972
+ * })
973
+ *
974
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
975
+ * const b = Graph.addNode(mutable, { id: "B" })
976
+ * const c = Graph.addNode(mutable, { id: "C" })
977
+ * Graph.addEdge(mutable, b, c, "B-C")
978
+ * })
979
+ *
980
+ * const result = Graph.compose(left, right, {
981
+ * nodeIdentity: (node) => node.id
982
+ * })
983
+ *
984
+ * console.log(Graph.nodeCount(result)) // 3
985
+ * console.log(Graph.edgeCount(result)) // 2
986
+ * ```
987
+ *
988
+ * @category set operations
989
+ * @since 4.0.0
990
+ */
991
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(that: Graph<N, E, T>, options?: IdentityOptions<N, E, NI, EI>): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
992
+ /**
993
+ * Composes two graphs, merging nodes by identity.
994
+ *
995
+ * **Details**
996
+ *
997
+ * Nodes and edges present in both graphs use data from `that`. The result has
998
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
999
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1000
+ * edge data. Edge identity also includes the endpoint identities.
1001
+ *
1002
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
1003
+ *
1004
+ * **Gotchas**
1005
+ *
1006
+ * Nodes with equal identities in one input graph are coalesced. The last node
1007
+ * supplies the data, and redirected edges can collapse or become self-loops.
1008
+ * Parallel edges with equal identities are also coalesced, with the last edge
1009
+ * supplying the data.
1010
+ *
1011
+ * **Example** (Combining graphs)
1012
+ *
1013
+ * ```ts
1014
+ * import { Graph } from "effect"
1015
+ *
1016
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
1017
+ * const a = Graph.addNode(mutable, { id: "A" })
1018
+ * const b = Graph.addNode(mutable, { id: "B" })
1019
+ * Graph.addEdge(mutable, a, b, "A-B")
1020
+ * })
1021
+ *
1022
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
1023
+ * const b = Graph.addNode(mutable, { id: "B" })
1024
+ * const c = Graph.addNode(mutable, { id: "C" })
1025
+ * Graph.addEdge(mutable, b, c, "B-C")
1026
+ * })
1027
+ *
1028
+ * const result = Graph.compose(left, right, {
1029
+ * nodeIdentity: (node) => node.id
1030
+ * })
1031
+ *
1032
+ * console.log(Graph.nodeCount(result)) // 3
1033
+ * console.log(Graph.edgeCount(result)) // 2
1034
+ * ```
1035
+ *
1036
+ * @category set operations
1037
+ * @since 4.0.0
1038
+ */
1039
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(
1040
+ self: Graph<N, E, T>,
1041
+ that: Graph<N, E, NoInfer<T>>,
1042
+ options?: IdentityOptions<N, E, NI, EI>
1043
+ ): Graph<N, E, T>
1044
+ } = dual(
1045
+ (args) => isGraph(args[0]) && isGraph(args[1]),
1046
+ <N, E, T extends Kind, NI, EI>(
1047
+ self: Graph<N, E, T>,
1048
+ that: Graph<N, E, T>,
1049
+ options?: IdentityOptions<N, E, NI, EI>
1050
+ ): Graph<N, E, T> => {
1051
+ assertSameKind(self, that)
1052
+ const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1053
+ const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1054
+ const selfMaps = buildNodeMaps(self, getNodeIdentity)
1055
+ const thatMaps = buildNodeMaps(that, getNodeIdentity)
1056
+ const nodes = MutableHashMap.empty</**
1057
+ * Composes two graphs, merging nodes by identity.
1058
+ *
1059
+ * **Details**
1060
+ *
1061
+ * Nodes and edges present in both graphs use data from `that`. The result has
1062
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1063
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1064
+ * edge data. Edge identity also includes the endpoint identities.
1065
+ *
1066
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
1067
+ *
1068
+ * **Gotchas**
1069
+ *
1070
+ * Nodes with equal identities in one input graph are coalesced. The last node
1071
+ * supplies the data, and redirected edges can collapse or become self-loops.
1072
+ * Parallel edges with equal identities are also coalesced, with the last edge
1073
+ * supplying the data.
1074
+ *
1075
+ * **Example** (Combining graphs)
1076
+ *
1077
+ * ```ts
1078
+ * import { Graph } from "effect"
1079
+ *
1080
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
1081
+ * const a = Graph.addNode(mutable, { id: "A" })
1082
+ * const b = Graph.addNode(mutable, { id: "B" })
1083
+ * Graph.addEdge(mutable, a, b, "A-B")
1084
+ * })
1085
+ *
1086
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
1087
+ * const b = Graph.addNode(mutable, { id: "B" })
1088
+ * const c = Graph.addNode(mutable, { id: "C" })
1089
+ * Graph.addEdge(mutable, b, c, "B-C")
1090
+ * })
1091
+ *
1092
+ * const result = Graph.compose(left, right, {
1093
+ * nodeIdentity: (node) => node.id
1094
+ * })
1095
+ *
1096
+ * console.log(Graph.nodeCount(result)) // 3
1097
+ * console.log(Graph.edgeCount(result)) // 2
1098
+ * ```
1099
+ *
1100
+ * @category set operations
1101
+ * @since 4.0.0
1102
+ */
1103
+ NI, /**
1104
+ * Composes two graphs, merging nodes by identity.
1105
+ *
1106
+ * **Details**
1107
+ *
1108
+ * Nodes and edges present in both graphs use data from `that`. The result has
1109
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1110
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1111
+ * edge data. Edge identity also includes the endpoint identities.
1112
+ *
1113
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
1114
+ *
1115
+ * **Gotchas**
1116
+ *
1117
+ * Nodes with equal identities in one input graph are coalesced. The last node
1118
+ * supplies the data, and redirected edges can collapse or become self-loops.
1119
+ * Parallel edges with equal identities are also coalesced, with the last edge
1120
+ * supplying the data.
1121
+ *
1122
+ * **Example** (Combining graphs)
1123
+ *
1124
+ * ```ts
1125
+ * import { Graph } from "effect"
1126
+ *
1127
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
1128
+ * const a = Graph.addNode(mutable, { id: "A" })
1129
+ * const b = Graph.addNode(mutable, { id: "B" })
1130
+ * Graph.addEdge(mutable, a, b, "A-B")
1131
+ * })
1132
+ *
1133
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
1134
+ * const b = Graph.addNode(mutable, { id: "B" })
1135
+ * const c = Graph.addNode(mutable, { id: "C" })
1136
+ * Graph.addEdge(mutable, b, c, "B-C")
1137
+ * })
1138
+ *
1139
+ * const result = Graph.compose(left, right, {
1140
+ * nodeIdentity: (node) => node.id
1141
+ * })
1142
+ *
1143
+ * console.log(Graph.nodeCount(result)) // 3
1144
+ * console.log(Graph.edgeCount(result)) // 2
1145
+ * ```
1146
+ *
1147
+ * @category set operations
1148
+ * @since 4.0.0
1149
+ */
1150
+ N>()
1151
+ const edges = buildEdgeMap(self, selfMaps, getEdgeIdentity)
1152
+
1153
+ for (const [identity, data] of selfMaps.byIdentity) {
1154
+ MutableHashMap.set(nodes, identity, data)
1155
+ }
1156
+ for (const [identity, data] of thatMaps.byIdentity) {
1157
+ MutableHashMap.set(nodes, identity, data)
1158
+ }
1159
+
1160
+ for (const [identity, data] of buildEdgeMap(that, thatMaps, getEdgeIdentity)) {
1161
+ MutableHashMap.set(edges, identity, data)
1162
+ }
1163
+
1164
+ return make(self.type)</**
1165
+ * Composes two graphs, merging nodes by identity.
1166
+ *
1167
+ * **Details**
1168
+ *
1169
+ * Nodes and edges present in both graphs use data from `that`. The result has
1170
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1171
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1172
+ * edge data. Edge identity also includes the endpoint identities.
1173
+ *
1174
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
1175
+ *
1176
+ * **Gotchas**
1177
+ *
1178
+ * Nodes with equal identities in one input graph are coalesced. The last node
1179
+ * supplies the data, and redirected edges can collapse or become self-loops.
1180
+ * Parallel edges with equal identities are also coalesced, with the last edge
1181
+ * supplying the data.
1182
+ *
1183
+ * **Example** (Combining graphs)
1184
+ *
1185
+ * ```ts
1186
+ * import { Graph } from "effect"
1187
+ *
1188
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
1189
+ * const a = Graph.addNode(mutable, { id: "A" })
1190
+ * const b = Graph.addNode(mutable, { id: "B" })
1191
+ * Graph.addEdge(mutable, a, b, "A-B")
1192
+ * })
1193
+ *
1194
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
1195
+ * const b = Graph.addNode(mutable, { id: "B" })
1196
+ * const c = Graph.addNode(mutable, { id: "C" })
1197
+ * Graph.addEdge(mutable, b, c, "B-C")
1198
+ * })
1199
+ *
1200
+ * const result = Graph.compose(left, right, {
1201
+ * nodeIdentity: (node) => node.id
1202
+ * })
1203
+ *
1204
+ * console.log(Graph.nodeCount(result)) // 3
1205
+ * console.log(Graph.edgeCount(result)) // 2
1206
+ * ```
1207
+ *
1208
+ * @category set operations
1209
+ * @since 4.0.0
1210
+ */
1211
+ N, /**
1212
+ * Composes two graphs, merging nodes by identity.
1213
+ *
1214
+ * **Details**
1215
+ *
1216
+ * Nodes and edges present in both graphs use data from `that`. The result has
1217
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1218
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1219
+ * edge data. Edge identity also includes the endpoint identities.
1220
+ *
1221
+ * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}`
1222
+ *
1223
+ * **Gotchas**
1224
+ *
1225
+ * Nodes with equal identities in one input graph are coalesced. The last node
1226
+ * supplies the data, and redirected edges can collapse or become self-loops.
1227
+ * Parallel edges with equal identities are also coalesced, with the last edge
1228
+ * supplying the data.
1229
+ *
1230
+ * **Example** (Combining graphs)
1231
+ *
1232
+ * ```ts
1233
+ * import { Graph } from "effect"
1234
+ *
1235
+ * const left = Graph.directed<{ id: string }, string>((mutable) => {
1236
+ * const a = Graph.addNode(mutable, { id: "A" })
1237
+ * const b = Graph.addNode(mutable, { id: "B" })
1238
+ * Graph.addEdge(mutable, a, b, "A-B")
1239
+ * })
1240
+ *
1241
+ * const right = Graph.directed<{ id: string }, string>((mutable) => {
1242
+ * const b = Graph.addNode(mutable, { id: "B" })
1243
+ * const c = Graph.addNode(mutable, { id: "C" })
1244
+ * Graph.addEdge(mutable, b, c, "B-C")
1245
+ * })
1246
+ *
1247
+ * const result = Graph.compose(left, right, {
1248
+ * nodeIdentity: (node) => node.id
1249
+ * })
1250
+ *
1251
+ * console.log(Graph.nodeCount(result)) // 3
1252
+ * console.log(Graph.edgeCount(result)) // 2
1253
+ * ```
1254
+ *
1255
+ * @category set operations
1256
+ * @since 4.0.0
1257
+ */
1258
+ E>((mutable) => {
1259
+ const indexByIdentity = addNodesByIdentity(mutable, nodes)
1260
+
1261
+ for (const [identity, data] of edges) {
1262
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data)
1263
+ }
1264
+ });
1265
+ }
1266
+ )
1267
+
1268
+ /**
1269
+ * Returns the intersection of two graphs, matching nodes by identity.
1270
+ *
1271
+ * **Details**
1272
+ *
1273
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1274
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1275
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1276
+ * edge data. Edge identity also includes the endpoint identities.
1277
+ *
1278
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1279
+ *
1280
+ * **Gotchas**
1281
+ *
1282
+ * Nodes with equal identities in one input graph are coalesced. The last node
1283
+ * supplies the data, and redirected edges can collapse or become self-loops.
1284
+ * The result contains at most one edge for each shared edge identity.
1285
+ *
1286
+ * **Example** (Finding shared structure)
1287
+ *
1288
+ * ```ts
1289
+ * import { Graph } from "effect"
1290
+ *
1291
+ * const left = Graph.directed<string, string>((mutable) => {
1292
+ * const a = Graph.addNode(mutable, "A")
1293
+ * const b = Graph.addNode(mutable, "B")
1294
+ * Graph.addEdge(mutable, a, b, "shared")
1295
+ * })
1296
+ *
1297
+ * const right = Graph.directed<string, string>((mutable) => {
1298
+ * const a = Graph.addNode(mutable, "A")
1299
+ * const b = Graph.addNode(mutable, "B")
1300
+ * Graph.addEdge(mutable, a, b, "shared")
1301
+ * })
1302
+ *
1303
+ * const result = Graph.intersection(left, right)
1304
+ *
1305
+ * console.log(Graph.nodeCount(result)) // 2
1306
+ * console.log(Graph.edgeCount(result)) // 1
1307
+ * ```
1308
+ *
1309
+ * @category set operations
1310
+ * @since 4.0.0
1311
+ */
1312
+ export const intersection: {
1313
+ /**
1314
+ * Returns the intersection of two graphs, matching nodes by identity.
1315
+ *
1316
+ * **Details**
1317
+ *
1318
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1319
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1320
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1321
+ * edge data. Edge identity also includes the endpoint identities.
1322
+ *
1323
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1324
+ *
1325
+ * **Gotchas**
1326
+ *
1327
+ * Nodes with equal identities in one input graph are coalesced. The last node
1328
+ * supplies the data, and redirected edges can collapse or become self-loops.
1329
+ * The result contains at most one edge for each shared edge identity.
1330
+ *
1331
+ * **Example** (Finding shared structure)
1332
+ *
1333
+ * ```ts
1334
+ * import { Graph } from "effect"
1335
+ *
1336
+ * const left = Graph.directed<string, string>((mutable) => {
1337
+ * const a = Graph.addNode(mutable, "A")
1338
+ * const b = Graph.addNode(mutable, "B")
1339
+ * Graph.addEdge(mutable, a, b, "shared")
1340
+ * })
1341
+ *
1342
+ * const right = Graph.directed<string, string>((mutable) => {
1343
+ * const a = Graph.addNode(mutable, "A")
1344
+ * const b = Graph.addNode(mutable, "B")
1345
+ * Graph.addEdge(mutable, a, b, "shared")
1346
+ * })
1347
+ *
1348
+ * const result = Graph.intersection(left, right)
1349
+ *
1350
+ * console.log(Graph.nodeCount(result)) // 2
1351
+ * console.log(Graph.edgeCount(result)) // 1
1352
+ * ```
1353
+ *
1354
+ * @category set operations
1355
+ * @since 4.0.0
1356
+ */
1357
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(that: Graph<N, E, T>, options?: IdentityOptions<N, E, NI, EI>): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1358
+ /**
1359
+ * Returns the intersection of two graphs, matching nodes by identity.
1360
+ *
1361
+ * **Details**
1362
+ *
1363
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1364
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1365
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1366
+ * edge data. Edge identity also includes the endpoint identities.
1367
+ *
1368
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1369
+ *
1370
+ * **Gotchas**
1371
+ *
1372
+ * Nodes with equal identities in one input graph are coalesced. The last node
1373
+ * supplies the data, and redirected edges can collapse or become self-loops.
1374
+ * The result contains at most one edge for each shared edge identity.
1375
+ *
1376
+ * **Example** (Finding shared structure)
1377
+ *
1378
+ * ```ts
1379
+ * import { Graph } from "effect"
1380
+ *
1381
+ * const left = Graph.directed<string, string>((mutable) => {
1382
+ * const a = Graph.addNode(mutable, "A")
1383
+ * const b = Graph.addNode(mutable, "B")
1384
+ * Graph.addEdge(mutable, a, b, "shared")
1385
+ * })
1386
+ *
1387
+ * const right = Graph.directed<string, string>((mutable) => {
1388
+ * const a = Graph.addNode(mutable, "A")
1389
+ * const b = Graph.addNode(mutable, "B")
1390
+ * Graph.addEdge(mutable, a, b, "shared")
1391
+ * })
1392
+ *
1393
+ * const result = Graph.intersection(left, right)
1394
+ *
1395
+ * console.log(Graph.nodeCount(result)) // 2
1396
+ * console.log(Graph.edgeCount(result)) // 1
1397
+ * ```
1398
+ *
1399
+ * @category set operations
1400
+ * @since 4.0.0
1401
+ */
1402
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(
1403
+ self: Graph<N, E, T>,
1404
+ that: Graph<N, E, NoInfer<T>>,
1405
+ options?: IdentityOptions<N, E, NI, EI>
1406
+ ): Graph<N, E, T>
1407
+ } = dual((args) => isGraph(args[0]) && isGraph(args[1]), <N, E, T extends Kind, NI, EI>(
1408
+ self: Graph<N, E, T>,
1409
+ that: Graph<N, E, T>,
1410
+ options?: IdentityOptions<N, E, NI, EI>
1411
+ ): Graph<N, E, T> => {
1412
+ assertSameKind(self, that)
1413
+ const thatImpl = graphImpl(that)
1414
+ const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1415
+ const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1416
+ const selfMaps = buildNodeMaps(self, getNodeIdentity)
1417
+ const thatMaps = buildNodeMaps(that, getNodeIdentity)
1418
+ const nodes = MutableHashMap.empty</**
1419
+ * Returns the intersection of two graphs, matching nodes by identity.
1420
+ *
1421
+ * **Details**
1422
+ *
1423
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1424
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1425
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1426
+ * edge data. Edge identity also includes the endpoint identities.
1427
+ *
1428
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1429
+ *
1430
+ * **Gotchas**
1431
+ *
1432
+ * Nodes with equal identities in one input graph are coalesced. The last node
1433
+ * supplies the data, and redirected edges can collapse or become self-loops.
1434
+ * The result contains at most one edge for each shared edge identity.
1435
+ *
1436
+ * **Example** (Finding shared structure)
1437
+ *
1438
+ * ```ts
1439
+ * import { Graph } from "effect"
1440
+ *
1441
+ * const left = Graph.directed<string, string>((mutable) => {
1442
+ * const a = Graph.addNode(mutable, "A")
1443
+ * const b = Graph.addNode(mutable, "B")
1444
+ * Graph.addEdge(mutable, a, b, "shared")
1445
+ * })
1446
+ *
1447
+ * const right = Graph.directed<string, string>((mutable) => {
1448
+ * const a = Graph.addNode(mutable, "A")
1449
+ * const b = Graph.addNode(mutable, "B")
1450
+ * Graph.addEdge(mutable, a, b, "shared")
1451
+ * })
1452
+ *
1453
+ * const result = Graph.intersection(left, right)
1454
+ *
1455
+ * console.log(Graph.nodeCount(result)) // 2
1456
+ * console.log(Graph.edgeCount(result)) // 1
1457
+ * ```
1458
+ *
1459
+ * @category set operations
1460
+ * @since 4.0.0
1461
+ */
1462
+ NI, /**
1463
+ * Returns the intersection of two graphs, matching nodes by identity.
1464
+ *
1465
+ * **Details**
1466
+ *
1467
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1468
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1469
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1470
+ * edge data. Edge identity also includes the endpoint identities.
1471
+ *
1472
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1473
+ *
1474
+ * **Gotchas**
1475
+ *
1476
+ * Nodes with equal identities in one input graph are coalesced. The last node
1477
+ * supplies the data, and redirected edges can collapse or become self-loops.
1478
+ * The result contains at most one edge for each shared edge identity.
1479
+ *
1480
+ * **Example** (Finding shared structure)
1481
+ *
1482
+ * ```ts
1483
+ * import { Graph } from "effect"
1484
+ *
1485
+ * const left = Graph.directed<string, string>((mutable) => {
1486
+ * const a = Graph.addNode(mutable, "A")
1487
+ * const b = Graph.addNode(mutable, "B")
1488
+ * Graph.addEdge(mutable, a, b, "shared")
1489
+ * })
1490
+ *
1491
+ * const right = Graph.directed<string, string>((mutable) => {
1492
+ * const a = Graph.addNode(mutable, "A")
1493
+ * const b = Graph.addNode(mutable, "B")
1494
+ * Graph.addEdge(mutable, a, b, "shared")
1495
+ * })
1496
+ *
1497
+ * const result = Graph.intersection(left, right)
1498
+ *
1499
+ * console.log(Graph.nodeCount(result)) // 2
1500
+ * console.log(Graph.edgeCount(result)) // 1
1501
+ * ```
1502
+ *
1503
+ * @category set operations
1504
+ * @since 4.0.0
1505
+ */
1506
+ N>()
1507
+ const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity)
1508
+ const thatEdges = MutableHashMap.empty</**
1509
+ * Returns the intersection of two graphs, matching nodes by identity.
1510
+ *
1511
+ * **Details**
1512
+ *
1513
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1514
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1515
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1516
+ * edge data. Edge identity also includes the endpoint identities.
1517
+ *
1518
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1519
+ *
1520
+ * **Gotchas**
1521
+ *
1522
+ * Nodes with equal identities in one input graph are coalesced. The last node
1523
+ * supplies the data, and redirected edges can collapse or become self-loops.
1524
+ * The result contains at most one edge for each shared edge identity.
1525
+ *
1526
+ * **Example** (Finding shared structure)
1527
+ *
1528
+ * ```ts
1529
+ * import { Graph } from "effect"
1530
+ *
1531
+ * const left = Graph.directed<string, string>((mutable) => {
1532
+ * const a = Graph.addNode(mutable, "A")
1533
+ * const b = Graph.addNode(mutable, "B")
1534
+ * Graph.addEdge(mutable, a, b, "shared")
1535
+ * })
1536
+ *
1537
+ * const right = Graph.directed<string, string>((mutable) => {
1538
+ * const a = Graph.addNode(mutable, "A")
1539
+ * const b = Graph.addNode(mutable, "B")
1540
+ * Graph.addEdge(mutable, a, b, "shared")
1541
+ * })
1542
+ *
1543
+ * const result = Graph.intersection(left, right)
1544
+ *
1545
+ * console.log(Graph.nodeCount(result)) // 2
1546
+ * console.log(Graph.edgeCount(result)) // 1
1547
+ * ```
1548
+ *
1549
+ * @category set operations
1550
+ * @since 4.0.0
1551
+ */
1552
+ EdgeIdentity<NI, EI>, /**
1553
+ * Returns the intersection of two graphs, matching nodes by identity.
1554
+ *
1555
+ * **Details**
1556
+ *
1557
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1558
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1559
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1560
+ * edge data. Edge identity also includes the endpoint identities.
1561
+ *
1562
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1563
+ *
1564
+ * **Gotchas**
1565
+ *
1566
+ * Nodes with equal identities in one input graph are coalesced. The last node
1567
+ * supplies the data, and redirected edges can collapse or become self-loops.
1568
+ * The result contains at most one edge for each shared edge identity.
1569
+ *
1570
+ * **Example** (Finding shared structure)
1571
+ *
1572
+ * ```ts
1573
+ * import { Graph } from "effect"
1574
+ *
1575
+ * const left = Graph.directed<string, string>((mutable) => {
1576
+ * const a = Graph.addNode(mutable, "A")
1577
+ * const b = Graph.addNode(mutable, "B")
1578
+ * Graph.addEdge(mutable, a, b, "shared")
1579
+ * })
1580
+ *
1581
+ * const right = Graph.directed<string, string>((mutable) => {
1582
+ * const a = Graph.addNode(mutable, "A")
1583
+ * const b = Graph.addNode(mutable, "B")
1584
+ * Graph.addEdge(mutable, a, b, "shared")
1585
+ * })
1586
+ *
1587
+ * const result = Graph.intersection(left, right)
1588
+ *
1589
+ * console.log(Graph.nodeCount(result)) // 2
1590
+ * console.log(Graph.edgeCount(result)) // 1
1591
+ * ```
1592
+ *
1593
+ * @category set operations
1594
+ * @since 4.0.0
1595
+ */
1596
+ E>()
1597
+
1598
+ for (const [identity, data] of selfMaps.byIdentity) {
1599
+ if (MutableHashMap.has(thatMaps.byIdentity, identity)) {
1600
+ MutableHashMap.set(nodes, identity, data)
1601
+ }
1602
+ }
1603
+
1604
+ for (const edge of thatImpl.edges.values()) {
1605
+ const sourceIdentity = nodeIdentityAt(thatMaps, edge.source)
1606
+ const targetIdentity = nodeIdentityAt(thatMaps, edge.target)
1607
+ if (MutableHashMap.has(nodes, sourceIdentity) && MutableHashMap.has(nodes, targetIdentity)) {
1608
+ const edgeIdentity = new EdgeIdentity(that.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data))
1609
+ MutableHashMap.set(thatEdges, edgeIdentity, edge.data)
1610
+ }
1611
+ }
1612
+
1613
+ return make(self.type)</**
1614
+ * Returns the intersection of two graphs, matching nodes by identity.
1615
+ *
1616
+ * **Details**
1617
+ *
1618
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1619
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1620
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1621
+ * edge data. Edge identity also includes the endpoint identities.
1622
+ *
1623
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1624
+ *
1625
+ * **Gotchas**
1626
+ *
1627
+ * Nodes with equal identities in one input graph are coalesced. The last node
1628
+ * supplies the data, and redirected edges can collapse or become self-loops.
1629
+ * The result contains at most one edge for each shared edge identity.
1630
+ *
1631
+ * **Example** (Finding shared structure)
1632
+ *
1633
+ * ```ts
1634
+ * import { Graph } from "effect"
1635
+ *
1636
+ * const left = Graph.directed<string, string>((mutable) => {
1637
+ * const a = Graph.addNode(mutable, "A")
1638
+ * const b = Graph.addNode(mutable, "B")
1639
+ * Graph.addEdge(mutable, a, b, "shared")
1640
+ * })
1641
+ *
1642
+ * const right = Graph.directed<string, string>((mutable) => {
1643
+ * const a = Graph.addNode(mutable, "A")
1644
+ * const b = Graph.addNode(mutable, "B")
1645
+ * Graph.addEdge(mutable, a, b, "shared")
1646
+ * })
1647
+ *
1648
+ * const result = Graph.intersection(left, right)
1649
+ *
1650
+ * console.log(Graph.nodeCount(result)) // 2
1651
+ * console.log(Graph.edgeCount(result)) // 1
1652
+ * ```
1653
+ *
1654
+ * @category set operations
1655
+ * @since 4.0.0
1656
+ */
1657
+ N, /**
1658
+ * Returns the intersection of two graphs, matching nodes by identity.
1659
+ *
1660
+ * **Details**
1661
+ *
1662
+ * Node data comes from `self`, and edge data comes from `that`. The result has
1663
+ * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do
1664
+ * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and
1665
+ * edge data. Edge identity also includes the endpoint identities.
1666
+ *
1667
+ * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}`
1668
+ *
1669
+ * **Gotchas**
1670
+ *
1671
+ * Nodes with equal identities in one input graph are coalesced. The last node
1672
+ * supplies the data, and redirected edges can collapse or become self-loops.
1673
+ * The result contains at most one edge for each shared edge identity.
1674
+ *
1675
+ * **Example** (Finding shared structure)
1676
+ *
1677
+ * ```ts
1678
+ * import { Graph } from "effect"
1679
+ *
1680
+ * const left = Graph.directed<string, string>((mutable) => {
1681
+ * const a = Graph.addNode(mutable, "A")
1682
+ * const b = Graph.addNode(mutable, "B")
1683
+ * Graph.addEdge(mutable, a, b, "shared")
1684
+ * })
1685
+ *
1686
+ * const right = Graph.directed<string, string>((mutable) => {
1687
+ * const a = Graph.addNode(mutable, "A")
1688
+ * const b = Graph.addNode(mutable, "B")
1689
+ * Graph.addEdge(mutable, a, b, "shared")
1690
+ * })
1691
+ *
1692
+ * const result = Graph.intersection(left, right)
1693
+ *
1694
+ * console.log(Graph.nodeCount(result)) // 2
1695
+ * console.log(Graph.edgeCount(result)) // 1
1696
+ * ```
1697
+ *
1698
+ * @category set operations
1699
+ * @since 4.0.0
1700
+ */
1701
+ E>((mutable) => {
1702
+ const indexByIdentity = addNodesByIdentity(mutable, nodes)
1703
+
1704
+ for (const [identity, data] of thatEdges) {
1705
+ if (MutableHashMap.has(selfEdges, identity)) {
1706
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data)
1707
+ }
1708
+ }
1709
+ });
1710
+ })
1711
+
1712
+ /**
1713
+ * Returns `self` without edges also present in `that`.
1714
+ *
1715
+ * **Details**
1716
+ *
1717
+ * All nodes from `self` are preserved. Edges are matched by endpoint and edge
1718
+ * identities. The result has the same graph kind as `self`. Throws a
1719
+ * `GraphError` when the graph kinds do not match. `nodeIdentity` and
1720
+ * `edgeIdentity` default to the complete node and edge data.
1721
+ *
1722
+ * `G1 \ G2 = {V1, E1 \ E2}`
1723
+ *
1724
+ * **Gotchas**
1725
+ *
1726
+ * Nodes with equal identities in one input graph are coalesced. The last node
1727
+ * supplies the data, and redirected edges can collapse or become self-loops.
1728
+ * If `that` contains an edge identity, every parallel edge with that identity
1729
+ * is removed from `self`.
1730
+ *
1731
+ * **Example** (Removing shared edges)
1732
+ *
1733
+ * ```ts
1734
+ * import { Graph } from "effect"
1735
+ *
1736
+ * const left = Graph.directed<string, string>((mutable) => {
1737
+ * const a = Graph.addNode(mutable, "A")
1738
+ * const b = Graph.addNode(mutable, "B")
1739
+ * const c = Graph.addNode(mutable, "C")
1740
+ * Graph.addEdge(mutable, a, b, "A-B")
1741
+ * Graph.addEdge(mutable, b, c, "B-C")
1742
+ * })
1743
+ *
1744
+ * const right = Graph.directed<string, string>((mutable) => {
1745
+ * const b = Graph.addNode(mutable, "B")
1746
+ * const c = Graph.addNode(mutable, "C")
1747
+ * Graph.addEdge(mutable, b, c, "B-C")
1748
+ * })
1749
+ *
1750
+ * const result = Graph.difference(left, right)
1751
+ *
1752
+ * console.log(Graph.nodeCount(result)) // 3
1753
+ * console.log(Graph.edgeCount(result)) // 1
1754
+ * ```
1755
+ *
1756
+ * @category set operations
1757
+ * @since 4.0.0
1758
+ */
1759
+ export const difference: {
1760
+ /**
1761
+ * Returns `self` without edges also present in `that`.
1762
+ *
1763
+ * **Details**
1764
+ *
1765
+ * All nodes from `self` are preserved. Edges are matched by endpoint and edge
1766
+ * identities. The result has the same graph kind as `self`. Throws a
1767
+ * `GraphError` when the graph kinds do not match. `nodeIdentity` and
1768
+ * `edgeIdentity` default to the complete node and edge data.
1769
+ *
1770
+ * `G1 \ G2 = {V1, E1 \ E2}`
1771
+ *
1772
+ * **Gotchas**
1773
+ *
1774
+ * Nodes with equal identities in one input graph are coalesced. The last node
1775
+ * supplies the data, and redirected edges can collapse or become self-loops.
1776
+ * If `that` contains an edge identity, every parallel edge with that identity
1777
+ * is removed from `self`.
1778
+ *
1779
+ * **Example** (Removing shared edges)
1780
+ *
1781
+ * ```ts
1782
+ * import { Graph } from "effect"
1783
+ *
1784
+ * const left = Graph.directed<string, string>((mutable) => {
1785
+ * const a = Graph.addNode(mutable, "A")
1786
+ * const b = Graph.addNode(mutable, "B")
1787
+ * const c = Graph.addNode(mutable, "C")
1788
+ * Graph.addEdge(mutable, a, b, "A-B")
1789
+ * Graph.addEdge(mutable, b, c, "B-C")
1790
+ * })
1791
+ *
1792
+ * const right = Graph.directed<string, string>((mutable) => {
1793
+ * const b = Graph.addNode(mutable, "B")
1794
+ * const c = Graph.addNode(mutable, "C")
1795
+ * Graph.addEdge(mutable, b, c, "B-C")
1796
+ * })
1797
+ *
1798
+ * const result = Graph.difference(left, right)
1799
+ *
1800
+ * console.log(Graph.nodeCount(result)) // 3
1801
+ * console.log(Graph.edgeCount(result)) // 1
1802
+ * ```
1803
+ *
1804
+ * @category set operations
1805
+ * @since 4.0.0
1806
+ */
1807
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(that: Graph<N, E, T>, options?: IdentityOptions<N, E, NI, EI>): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
1808
+ /**
1809
+ * Returns `self` without edges also present in `that`.
1810
+ *
1811
+ * **Details**
1812
+ *
1813
+ * All nodes from `self` are preserved. Edges are matched by endpoint and edge
1814
+ * identities. The result has the same graph kind as `self`. Throws a
1815
+ * `GraphError` when the graph kinds do not match. `nodeIdentity` and
1816
+ * `edgeIdentity` default to the complete node and edge data.
1817
+ *
1818
+ * `G1 \ G2 = {V1, E1 \ E2}`
1819
+ *
1820
+ * **Gotchas**
1821
+ *
1822
+ * Nodes with equal identities in one input graph are coalesced. The last node
1823
+ * supplies the data, and redirected edges can collapse or become self-loops.
1824
+ * If `that` contains an edge identity, every parallel edge with that identity
1825
+ * is removed from `self`.
1826
+ *
1827
+ * **Example** (Removing shared edges)
1828
+ *
1829
+ * ```ts
1830
+ * import { Graph } from "effect"
1831
+ *
1832
+ * const left = Graph.directed<string, string>((mutable) => {
1833
+ * const a = Graph.addNode(mutable, "A")
1834
+ * const b = Graph.addNode(mutable, "B")
1835
+ * const c = Graph.addNode(mutable, "C")
1836
+ * Graph.addEdge(mutable, a, b, "A-B")
1837
+ * Graph.addEdge(mutable, b, c, "B-C")
1838
+ * })
1839
+ *
1840
+ * const right = Graph.directed<string, string>((mutable) => {
1841
+ * const b = Graph.addNode(mutable, "B")
1842
+ * const c = Graph.addNode(mutable, "C")
1843
+ * Graph.addEdge(mutable, b, c, "B-C")
1844
+ * })
1845
+ *
1846
+ * const result = Graph.difference(left, right)
1847
+ *
1848
+ * console.log(Graph.nodeCount(result)) // 3
1849
+ * console.log(Graph.edgeCount(result)) // 1
1850
+ * ```
1851
+ *
1852
+ * @category set operations
1853
+ * @since 4.0.0
1854
+ */
1855
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(
1856
+ self: Graph<N, E, T>,
1857
+ that: Graph<N, E, NoInfer<T>>,
1858
+ options?: IdentityOptions<N, E, NI, EI>
1859
+ ): Graph<N, E, T>
1860
+ } = dual((args) => isGraph(args[0]) && isGraph(args[1]), <N, E, T extends Kind, NI, EI>(
1861
+ self: Graph<N, E, T>,
1862
+ that: Graph<N, E, T>,
1863
+ options?: IdentityOptions<N, E, NI, EI>
1864
+ ): Graph<N, E, T> => {
1865
+ assertSameKind(self, that)
1866
+ const selfImpl = graphImpl(self)
1867
+ const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
1868
+ const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
1869
+ const selfMaps = buildNodeMaps(self, getNodeIdentity)
1870
+ const thatMaps = buildNodeMaps(that, getNodeIdentity)
1871
+ const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity)
1872
+
1873
+ return make(self.type)</**
1874
+ * Returns `self` without edges also present in `that`.
1875
+ *
1876
+ * **Details**
1877
+ *
1878
+ * All nodes from `self` are preserved. Edges are matched by endpoint and edge
1879
+ * identities. The result has the same graph kind as `self`. Throws a
1880
+ * `GraphError` when the graph kinds do not match. `nodeIdentity` and
1881
+ * `edgeIdentity` default to the complete node and edge data.
1882
+ *
1883
+ * `G1 \ G2 = {V1, E1 \ E2}`
1884
+ *
1885
+ * **Gotchas**
1886
+ *
1887
+ * Nodes with equal identities in one input graph are coalesced. The last node
1888
+ * supplies the data, and redirected edges can collapse or become self-loops.
1889
+ * If `that` contains an edge identity, every parallel edge with that identity
1890
+ * is removed from `self`.
1891
+ *
1892
+ * **Example** (Removing shared edges)
1893
+ *
1894
+ * ```ts
1895
+ * import { Graph } from "effect"
1896
+ *
1897
+ * const left = Graph.directed<string, string>((mutable) => {
1898
+ * const a = Graph.addNode(mutable, "A")
1899
+ * const b = Graph.addNode(mutable, "B")
1900
+ * const c = Graph.addNode(mutable, "C")
1901
+ * Graph.addEdge(mutable, a, b, "A-B")
1902
+ * Graph.addEdge(mutable, b, c, "B-C")
1903
+ * })
1904
+ *
1905
+ * const right = Graph.directed<string, string>((mutable) => {
1906
+ * const b = Graph.addNode(mutable, "B")
1907
+ * const c = Graph.addNode(mutable, "C")
1908
+ * Graph.addEdge(mutable, b, c, "B-C")
1909
+ * })
1910
+ *
1911
+ * const result = Graph.difference(left, right)
1912
+ *
1913
+ * console.log(Graph.nodeCount(result)) // 3
1914
+ * console.log(Graph.edgeCount(result)) // 1
1915
+ * ```
1916
+ *
1917
+ * @category set operations
1918
+ * @since 4.0.0
1919
+ */
1920
+ N, /**
1921
+ * Returns `self` without edges also present in `that`.
1922
+ *
1923
+ * **Details**
1924
+ *
1925
+ * All nodes from `self` are preserved. Edges are matched by endpoint and edge
1926
+ * identities. The result has the same graph kind as `self`. Throws a
1927
+ * `GraphError` when the graph kinds do not match. `nodeIdentity` and
1928
+ * `edgeIdentity` default to the complete node and edge data.
1929
+ *
1930
+ * `G1 \ G2 = {V1, E1 \ E2}`
1931
+ *
1932
+ * **Gotchas**
1933
+ *
1934
+ * Nodes with equal identities in one input graph are coalesced. The last node
1935
+ * supplies the data, and redirected edges can collapse or become self-loops.
1936
+ * If `that` contains an edge identity, every parallel edge with that identity
1937
+ * is removed from `self`.
1938
+ *
1939
+ * **Example** (Removing shared edges)
1940
+ *
1941
+ * ```ts
1942
+ * import { Graph } from "effect"
1943
+ *
1944
+ * const left = Graph.directed<string, string>((mutable) => {
1945
+ * const a = Graph.addNode(mutable, "A")
1946
+ * const b = Graph.addNode(mutable, "B")
1947
+ * const c = Graph.addNode(mutable, "C")
1948
+ * Graph.addEdge(mutable, a, b, "A-B")
1949
+ * Graph.addEdge(mutable, b, c, "B-C")
1950
+ * })
1951
+ *
1952
+ * const right = Graph.directed<string, string>((mutable) => {
1953
+ * const b = Graph.addNode(mutable, "B")
1954
+ * const c = Graph.addNode(mutable, "C")
1955
+ * Graph.addEdge(mutable, b, c, "B-C")
1956
+ * })
1957
+ *
1958
+ * const result = Graph.difference(left, right)
1959
+ *
1960
+ * console.log(Graph.nodeCount(result)) // 3
1961
+ * console.log(Graph.edgeCount(result)) // 1
1962
+ * ```
1963
+ *
1964
+ * @category set operations
1965
+ * @since 4.0.0
1966
+ */
1967
+ E>((mutable) => {
1968
+ const indexByIdentity = addNodesByIdentity(mutable, selfMaps.byIdentity)
1969
+
1970
+ for (const edge of selfImpl.edges.values()) {
1971
+ const sourceIdentity = nodeIdentityAt(selfMaps, edge.source)
1972
+ const targetIdentity = nodeIdentityAt(selfMaps, edge.target)
1973
+ const edgeIdentity = new EdgeIdentity(self.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data))
1974
+ if (!MutableHashMap.has(thatEdges, edgeIdentity)) {
1975
+ addEdgeByIdentity(mutable, indexByIdentity, edgeIdentity, edge.data)
1976
+ }
1977
+ }
1978
+ });
1979
+ })
1980
+
1981
+ /**
1982
+ * Returns edges present in exactly one of two graphs.
1983
+ *
1984
+ * **Details**
1985
+ *
1986
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
1987
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
1988
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
1989
+ * complete node and edge data. Edge identity also includes the endpoint
1990
+ * identities.
1991
+ *
1992
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
1993
+ *
1994
+ * **Gotchas**
1995
+ *
1996
+ * Edges with different projected identities are distinct.
1997
+ * Nodes with equal identities in one input graph are coalesced. The last node
1998
+ * supplies the data, and redirected edges can collapse or become self-loops.
1999
+ * Parallel edges with equal identities are coalesced before the graphs are
2000
+ * compared.
2001
+ *
2002
+ * **Example** (Finding differing edges)
2003
+ *
2004
+ * ```ts
2005
+ * import { Graph } from "effect"
2006
+ *
2007
+ * const left = Graph.directed<string, string>((mutable) => {
2008
+ * const a = Graph.addNode(mutable, "A")
2009
+ * const b = Graph.addNode(mutable, "B")
2010
+ * const c = Graph.addNode(mutable, "C")
2011
+ * Graph.addEdge(mutable, a, b, "A-B")
2012
+ * Graph.addEdge(mutable, b, c, "B-C")
2013
+ * })
2014
+ *
2015
+ * const right = Graph.directed<string, string>((mutable) => {
2016
+ * const b = Graph.addNode(mutable, "B")
2017
+ * const c = Graph.addNode(mutable, "C")
2018
+ * const d = Graph.addNode(mutable, "D")
2019
+ * Graph.addEdge(mutable, b, c, "B-C")
2020
+ * Graph.addEdge(mutable, c, d, "C-D")
2021
+ * })
2022
+ *
2023
+ * const result = Graph.symmetricDifference(left, right)
2024
+ *
2025
+ * console.log(Graph.nodeCount(result)) // 4
2026
+ * console.log(Graph.edgeCount(result)) // 2
2027
+ * ```
2028
+ *
2029
+ * @category set operations
2030
+ * @since 4.0.0
2031
+ */
2032
+ export const symmetricDifference: {
2033
+ /**
2034
+ * Returns edges present in exactly one of two graphs.
2035
+ *
2036
+ * **Details**
2037
+ *
2038
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
2039
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
2040
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
2041
+ * complete node and edge data. Edge identity also includes the endpoint
2042
+ * identities.
2043
+ *
2044
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
2045
+ *
2046
+ * **Gotchas**
2047
+ *
2048
+ * Edges with different projected identities are distinct.
2049
+ * Nodes with equal identities in one input graph are coalesced. The last node
2050
+ * supplies the data, and redirected edges can collapse or become self-loops.
2051
+ * Parallel edges with equal identities are coalesced before the graphs are
2052
+ * compared.
2053
+ *
2054
+ * **Example** (Finding differing edges)
2055
+ *
2056
+ * ```ts
2057
+ * import { Graph } from "effect"
2058
+ *
2059
+ * const left = Graph.directed<string, string>((mutable) => {
2060
+ * const a = Graph.addNode(mutable, "A")
2061
+ * const b = Graph.addNode(mutable, "B")
2062
+ * const c = Graph.addNode(mutable, "C")
2063
+ * Graph.addEdge(mutable, a, b, "A-B")
2064
+ * Graph.addEdge(mutable, b, c, "B-C")
2065
+ * })
2066
+ *
2067
+ * const right = Graph.directed<string, string>((mutable) => {
2068
+ * const b = Graph.addNode(mutable, "B")
2069
+ * const c = Graph.addNode(mutable, "C")
2070
+ * const d = Graph.addNode(mutable, "D")
2071
+ * Graph.addEdge(mutable, b, c, "B-C")
2072
+ * Graph.addEdge(mutable, c, d, "C-D")
2073
+ * })
2074
+ *
2075
+ * const result = Graph.symmetricDifference(left, right)
2076
+ *
2077
+ * console.log(Graph.nodeCount(result)) // 4
2078
+ * console.log(Graph.edgeCount(result)) // 2
2079
+ * ```
2080
+ *
2081
+ * @category set operations
2082
+ * @since 4.0.0
2083
+ */
2084
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(that: Graph<N, E, T>, options?: IdentityOptions<N, E, NI, EI>): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
2085
+ /**
2086
+ * Returns edges present in exactly one of two graphs.
2087
+ *
2088
+ * **Details**
2089
+ *
2090
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
2091
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
2092
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
2093
+ * complete node and edge data. Edge identity also includes the endpoint
2094
+ * identities.
2095
+ *
2096
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
2097
+ *
2098
+ * **Gotchas**
2099
+ *
2100
+ * Edges with different projected identities are distinct.
2101
+ * Nodes with equal identities in one input graph are coalesced. The last node
2102
+ * supplies the data, and redirected edges can collapse or become self-loops.
2103
+ * Parallel edges with equal identities are coalesced before the graphs are
2104
+ * compared.
2105
+ *
2106
+ * **Example** (Finding differing edges)
2107
+ *
2108
+ * ```ts
2109
+ * import { Graph } from "effect"
2110
+ *
2111
+ * const left = Graph.directed<string, string>((mutable) => {
2112
+ * const a = Graph.addNode(mutable, "A")
2113
+ * const b = Graph.addNode(mutable, "B")
2114
+ * const c = Graph.addNode(mutable, "C")
2115
+ * Graph.addEdge(mutable, a, b, "A-B")
2116
+ * Graph.addEdge(mutable, b, c, "B-C")
2117
+ * })
2118
+ *
2119
+ * const right = Graph.directed<string, string>((mutable) => {
2120
+ * const b = Graph.addNode(mutable, "B")
2121
+ * const c = Graph.addNode(mutable, "C")
2122
+ * const d = Graph.addNode(mutable, "D")
2123
+ * Graph.addEdge(mutable, b, c, "B-C")
2124
+ * Graph.addEdge(mutable, c, d, "C-D")
2125
+ * })
2126
+ *
2127
+ * const result = Graph.symmetricDifference(left, right)
2128
+ *
2129
+ * console.log(Graph.nodeCount(result)) // 4
2130
+ * console.log(Graph.edgeCount(result)) // 2
2131
+ * ```
2132
+ *
2133
+ * @category set operations
2134
+ * @since 4.0.0
2135
+ */
2136
+ <N, E, T extends Kind = "directed", NI = N, EI = E>(
2137
+ self: Graph<N, E, T>,
2138
+ that: Graph<N, E, NoInfer<T>>,
2139
+ options?: IdentityOptions<N, E, NI, EI>
2140
+ ): Graph<N, E, T>
2141
+ } = dual((args) => isGraph(args[0]) && isGraph(args[1]), <N, E, T extends Kind, NI, EI>(
2142
+ self: Graph<N, E, T>,
2143
+ that: Graph<N, E, T>,
2144
+ options?: IdentityOptions<N, E, NI, EI>
2145
+ ): Graph<N, E, T> => {
2146
+ assertSameKind(self, that)
2147
+ const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI)
2148
+ const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI)
2149
+ const selfMaps = buildNodeMaps(self, getNodeIdentity)
2150
+ const thatMaps = buildNodeMaps(that, getNodeIdentity)
2151
+ const nodes = MutableHashMap.empty</**
2152
+ * Returns edges present in exactly one of two graphs.
2153
+ *
2154
+ * **Details**
2155
+ *
2156
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
2157
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
2158
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
2159
+ * complete node and edge data. Edge identity also includes the endpoint
2160
+ * identities.
2161
+ *
2162
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
2163
+ *
2164
+ * **Gotchas**
2165
+ *
2166
+ * Edges with different projected identities are distinct.
2167
+ * Nodes with equal identities in one input graph are coalesced. The last node
2168
+ * supplies the data, and redirected edges can collapse or become self-loops.
2169
+ * Parallel edges with equal identities are coalesced before the graphs are
2170
+ * compared.
2171
+ *
2172
+ * **Example** (Finding differing edges)
2173
+ *
2174
+ * ```ts
2175
+ * import { Graph } from "effect"
2176
+ *
2177
+ * const left = Graph.directed<string, string>((mutable) => {
2178
+ * const a = Graph.addNode(mutable, "A")
2179
+ * const b = Graph.addNode(mutable, "B")
2180
+ * const c = Graph.addNode(mutable, "C")
2181
+ * Graph.addEdge(mutable, a, b, "A-B")
2182
+ * Graph.addEdge(mutable, b, c, "B-C")
2183
+ * })
2184
+ *
2185
+ * const right = Graph.directed<string, string>((mutable) => {
2186
+ * const b = Graph.addNode(mutable, "B")
2187
+ * const c = Graph.addNode(mutable, "C")
2188
+ * const d = Graph.addNode(mutable, "D")
2189
+ * Graph.addEdge(mutable, b, c, "B-C")
2190
+ * Graph.addEdge(mutable, c, d, "C-D")
2191
+ * })
2192
+ *
2193
+ * const result = Graph.symmetricDifference(left, right)
2194
+ *
2195
+ * console.log(Graph.nodeCount(result)) // 4
2196
+ * console.log(Graph.edgeCount(result)) // 2
2197
+ * ```
2198
+ *
2199
+ * @category set operations
2200
+ * @since 4.0.0
2201
+ */
2202
+ NI, /**
2203
+ * Returns edges present in exactly one of two graphs.
2204
+ *
2205
+ * **Details**
2206
+ *
2207
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
2208
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
2209
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
2210
+ * complete node and edge data. Edge identity also includes the endpoint
2211
+ * identities.
2212
+ *
2213
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
2214
+ *
2215
+ * **Gotchas**
2216
+ *
2217
+ * Edges with different projected identities are distinct.
2218
+ * Nodes with equal identities in one input graph are coalesced. The last node
2219
+ * supplies the data, and redirected edges can collapse or become self-loops.
2220
+ * Parallel edges with equal identities are coalesced before the graphs are
2221
+ * compared.
2222
+ *
2223
+ * **Example** (Finding differing edges)
2224
+ *
2225
+ * ```ts
2226
+ * import { Graph } from "effect"
2227
+ *
2228
+ * const left = Graph.directed<string, string>((mutable) => {
2229
+ * const a = Graph.addNode(mutable, "A")
2230
+ * const b = Graph.addNode(mutable, "B")
2231
+ * const c = Graph.addNode(mutable, "C")
2232
+ * Graph.addEdge(mutable, a, b, "A-B")
2233
+ * Graph.addEdge(mutable, b, c, "B-C")
2234
+ * })
2235
+ *
2236
+ * const right = Graph.directed<string, string>((mutable) => {
2237
+ * const b = Graph.addNode(mutable, "B")
2238
+ * const c = Graph.addNode(mutable, "C")
2239
+ * const d = Graph.addNode(mutable, "D")
2240
+ * Graph.addEdge(mutable, b, c, "B-C")
2241
+ * Graph.addEdge(mutable, c, d, "C-D")
2242
+ * })
2243
+ *
2244
+ * const result = Graph.symmetricDifference(left, right)
2245
+ *
2246
+ * console.log(Graph.nodeCount(result)) // 4
2247
+ * console.log(Graph.edgeCount(result)) // 2
2248
+ * ```
2249
+ *
2250
+ * @category set operations
2251
+ * @since 4.0.0
2252
+ */
2253
+ N>()
2254
+ const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity)
2255
+ const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity)
2256
+
2257
+ for (const [identity, data] of selfMaps.byIdentity) {
2258
+ MutableHashMap.set(nodes, identity, data)
2259
+ }
2260
+
2261
+ for (const [identity, data] of thatMaps.byIdentity) {
2262
+ MutableHashMap.set(nodes, identity, data)
2263
+ }
2264
+
2265
+ return make(self.type)</**
2266
+ * Returns edges present in exactly one of two graphs.
2267
+ *
2268
+ * **Details**
2269
+ *
2270
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
2271
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
2272
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
2273
+ * complete node and edge data. Edge identity also includes the endpoint
2274
+ * identities.
2275
+ *
2276
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
2277
+ *
2278
+ * **Gotchas**
2279
+ *
2280
+ * Edges with different projected identities are distinct.
2281
+ * Nodes with equal identities in one input graph are coalesced. The last node
2282
+ * supplies the data, and redirected edges can collapse or become self-loops.
2283
+ * Parallel edges with equal identities are coalesced before the graphs are
2284
+ * compared.
2285
+ *
2286
+ * **Example** (Finding differing edges)
2287
+ *
2288
+ * ```ts
2289
+ * import { Graph } from "effect"
2290
+ *
2291
+ * const left = Graph.directed<string, string>((mutable) => {
2292
+ * const a = Graph.addNode(mutable, "A")
2293
+ * const b = Graph.addNode(mutable, "B")
2294
+ * const c = Graph.addNode(mutable, "C")
2295
+ * Graph.addEdge(mutable, a, b, "A-B")
2296
+ * Graph.addEdge(mutable, b, c, "B-C")
2297
+ * })
2298
+ *
2299
+ * const right = Graph.directed<string, string>((mutable) => {
2300
+ * const b = Graph.addNode(mutable, "B")
2301
+ * const c = Graph.addNode(mutable, "C")
2302
+ * const d = Graph.addNode(mutable, "D")
2303
+ * Graph.addEdge(mutable, b, c, "B-C")
2304
+ * Graph.addEdge(mutable, c, d, "C-D")
2305
+ * })
2306
+ *
2307
+ * const result = Graph.symmetricDifference(left, right)
2308
+ *
2309
+ * console.log(Graph.nodeCount(result)) // 4
2310
+ * console.log(Graph.edgeCount(result)) // 2
2311
+ * ```
2312
+ *
2313
+ * @category set operations
2314
+ * @since 4.0.0
2315
+ */
2316
+ N, /**
2317
+ * Returns edges present in exactly one of two graphs.
2318
+ *
2319
+ * **Details**
2320
+ *
2321
+ * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The
2322
+ * result has the same graph kind as `self`. Throws a `GraphError` when the
2323
+ * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the
2324
+ * complete node and edge data. Edge identity also includes the endpoint
2325
+ * identities.
2326
+ *
2327
+ * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}`
2328
+ *
2329
+ * **Gotchas**
2330
+ *
2331
+ * Edges with different projected identities are distinct.
2332
+ * Nodes with equal identities in one input graph are coalesced. The last node
2333
+ * supplies the data, and redirected edges can collapse or become self-loops.
2334
+ * Parallel edges with equal identities are coalesced before the graphs are
2335
+ * compared.
2336
+ *
2337
+ * **Example** (Finding differing edges)
2338
+ *
2339
+ * ```ts
2340
+ * import { Graph } from "effect"
2341
+ *
2342
+ * const left = Graph.directed<string, string>((mutable) => {
2343
+ * const a = Graph.addNode(mutable, "A")
2344
+ * const b = Graph.addNode(mutable, "B")
2345
+ * const c = Graph.addNode(mutable, "C")
2346
+ * Graph.addEdge(mutable, a, b, "A-B")
2347
+ * Graph.addEdge(mutable, b, c, "B-C")
2348
+ * })
2349
+ *
2350
+ * const right = Graph.directed<string, string>((mutable) => {
2351
+ * const b = Graph.addNode(mutable, "B")
2352
+ * const c = Graph.addNode(mutable, "C")
2353
+ * const d = Graph.addNode(mutable, "D")
2354
+ * Graph.addEdge(mutable, b, c, "B-C")
2355
+ * Graph.addEdge(mutable, c, d, "C-D")
2356
+ * })
2357
+ *
2358
+ * const result = Graph.symmetricDifference(left, right)
2359
+ *
2360
+ * console.log(Graph.nodeCount(result)) // 4
2361
+ * console.log(Graph.edgeCount(result)) // 2
2362
+ * ```
2363
+ *
2364
+ * @category set operations
2365
+ * @since 4.0.0
2366
+ */
2367
+ E>((mutable) => {
2368
+ const indexByIdentity = addNodesByIdentity(mutable, nodes)
2369
+
2370
+ for (const [identity, data] of selfEdges) {
2371
+ if (!MutableHashMap.has(thatEdges, identity)) {
2372
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data)
2373
+ }
2374
+ }
2375
+
2376
+ for (const [identity, data] of thatEdges) {
2377
+ if (!MutableHashMap.has(selfEdges, identity)) {
2378
+ addEdgeByIdentity(mutable, indexByIdentity, identity, data)
2379
+ }
2380
+ }
2381
+ });
2382
+ })
2383
+
2384
+ /**
2385
+ * Returns the complement over the existing node set.
2386
+ *
2387
+ * **Details**
2388
+ *
2389
+ * Adds every missing edge between distinct nodes. The `createEdge` function
2390
+ * receives the source and target node data for each added edge. The result has
2391
+ * the same graph kind as `self`.
2392
+ *
2393
+ * `G' = {V, (V x V) \ E}`
2394
+ *
2395
+ * **Example** (Finding missing relationships)
2396
+ *
2397
+ * ```ts
2398
+ * import { Graph } from "effect"
2399
+ *
2400
+ * const graph = Graph.directed<string, string>((mutable) => {
2401
+ * const a = Graph.addNode(mutable, "A")
2402
+ * const b = Graph.addNode(mutable, "B")
2403
+ * Graph.addEdge(mutable, a, b, "A-B")
2404
+ * })
2405
+ *
2406
+ * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
2407
+ *
2408
+ * console.log(Graph.edgeCount(result)) // 1
2409
+ * ```
2410
+ *
2411
+ * @category set operations
2412
+ * @since 4.0.0
2413
+ */
2414
+ export const complement: {
2415
+ /**
2416
+ * Returns the complement over the existing node set.
2417
+ *
2418
+ * **Details**
2419
+ *
2420
+ * Adds every missing edge between distinct nodes. The `createEdge` function
2421
+ * receives the source and target node data for each added edge. The result has
2422
+ * the same graph kind as `self`.
2423
+ *
2424
+ * `G' = {V, (V x V) \ E}`
2425
+ *
2426
+ * **Example** (Finding missing relationships)
2427
+ *
2428
+ * ```ts
2429
+ * import { Graph } from "effect"
2430
+ *
2431
+ * const graph = Graph.directed<string, string>((mutable) => {
2432
+ * const a = Graph.addNode(mutable, "A")
2433
+ * const b = Graph.addNode(mutable, "B")
2434
+ * Graph.addEdge(mutable, a, b, "A-B")
2435
+ * })
2436
+ *
2437
+ * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
2438
+ *
2439
+ * console.log(Graph.edgeCount(result)) // 1
2440
+ * ```
2441
+ *
2442
+ * @category set operations
2443
+ * @since 4.0.0
2444
+ */
2445
+ <N, E>(createEdge: (source: N, target: N) => E): <T extends Kind = "directed">(self: Graph<N, E, T>) => Graph<N, E, T>
2446
+ /**
2447
+ * Returns the complement over the existing node set.
2448
+ *
2449
+ * **Details**
2450
+ *
2451
+ * Adds every missing edge between distinct nodes. The `createEdge` function
2452
+ * receives the source and target node data for each added edge. The result has
2453
+ * the same graph kind as `self`.
2454
+ *
2455
+ * `G' = {V, (V x V) \ E}`
2456
+ *
2457
+ * **Example** (Finding missing relationships)
2458
+ *
2459
+ * ```ts
2460
+ * import { Graph } from "effect"
2461
+ *
2462
+ * const graph = Graph.directed<string, string>((mutable) => {
2463
+ * const a = Graph.addNode(mutable, "A")
2464
+ * const b = Graph.addNode(mutable, "B")
2465
+ * Graph.addEdge(mutable, a, b, "A-B")
2466
+ * })
2467
+ *
2468
+ * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
2469
+ *
2470
+ * console.log(Graph.edgeCount(result)) // 1
2471
+ * ```
2472
+ *
2473
+ * @category set operations
2474
+ * @since 4.0.0
2475
+ */
2476
+ <N, E, T extends Kind = "directed">(self: Graph<N, E, T>, createEdge: (source: N, target: N) => E): Graph<N, E, T>
2477
+ } = dual(2, <N, E, T extends Kind>(
2478
+ self: Graph<N, E, T>,
2479
+ createEdge: (source: N, target: N) => E
2480
+ ): Graph<N, E, T> => {
2481
+ const selfImpl = graphImpl(self)
2482
+ const nodeEntries = Array.from(selfImpl.nodes)
2483
+
2484
+ return make(self.type)</**
2485
+ * Returns the complement over the existing node set.
2486
+ *
2487
+ * **Details**
2488
+ *
2489
+ * Adds every missing edge between distinct nodes. The `createEdge` function
2490
+ * receives the source and target node data for each added edge. The result has
2491
+ * the same graph kind as `self`.
2492
+ *
2493
+ * `G' = {V, (V x V) \ E}`
2494
+ *
2495
+ * **Example** (Finding missing relationships)
2496
+ *
2497
+ * ```ts
2498
+ * import { Graph } from "effect"
2499
+ *
2500
+ * const graph = Graph.directed<string, string>((mutable) => {
2501
+ * const a = Graph.addNode(mutable, "A")
2502
+ * const b = Graph.addNode(mutable, "B")
2503
+ * Graph.addEdge(mutable, a, b, "A-B")
2504
+ * })
2505
+ *
2506
+ * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
2507
+ *
2508
+ * console.log(Graph.edgeCount(result)) // 1
2509
+ * ```
2510
+ *
2511
+ * @category set operations
2512
+ * @since 4.0.0
2513
+ */
2514
+ N, /**
2515
+ * Returns the complement over the existing node set.
2516
+ *
2517
+ * **Details**
2518
+ *
2519
+ * Adds every missing edge between distinct nodes. The `createEdge` function
2520
+ * receives the source and target node data for each added edge. The result has
2521
+ * the same graph kind as `self`.
2522
+ *
2523
+ * `G' = {V, (V x V) \ E}`
2524
+ *
2525
+ * **Example** (Finding missing relationships)
2526
+ *
2527
+ * ```ts
2528
+ * import { Graph } from "effect"
2529
+ *
2530
+ * const graph = Graph.directed<string, string>((mutable) => {
2531
+ * const a = Graph.addNode(mutable, "A")
2532
+ * const b = Graph.addNode(mutable, "B")
2533
+ * Graph.addEdge(mutable, a, b, "A-B")
2534
+ * })
2535
+ *
2536
+ * const result = Graph.complement(graph, (source, target) => `${source}-${target}`)
2537
+ *
2538
+ * console.log(Graph.edgeCount(result)) // 1
2539
+ * ```
2540
+ *
2541
+ * @category set operations
2542
+ * @since 4.0.0
2543
+ */
2544
+ E>((mutable) => {
2545
+ const newIndexMap = new Map<NodeIndex, NodeIndex>()
2546
+
2547
+ for (const [oldIndex, data] of nodeEntries) {
2548
+ newIndexMap.set(oldIndex, addNode(mutable, data))
2549
+ }
2550
+
2551
+ for (let i = 0; i < nodeEntries.length; i++) {
2552
+ const [sourceOldIndex, sourceData] = nodeEntries[i]
2553
+ const start = self.type === "undirected" ? i + 1 : 0
2554
+
2555
+ for (let j = start; j < nodeEntries.length; j++) {
2556
+ const [targetOldIndex, targetData] = nodeEntries[j]
2557
+ if (sourceOldIndex === targetOldIndex || hasEdge(self, sourceOldIndex, targetOldIndex)) {
2558
+ continue
2559
+ }
2560
+
2561
+ const sourceIndex = newIndexMap.get(sourceOldIndex)
2562
+ const targetIndex = newIndexMap.get(targetOldIndex)
2563
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
2564
+ addEdge(mutable, sourceIndex, targetIndex, createEdge(sourceData, targetData))
2565
+ }
2566
+ }
2567
+ }
2568
+ });
2569
+ })
2570
+
2571
+ /**
2572
+ * Configuration for selecting a graph neighborhood.
2573
+ *
2574
+ * **Details**
2575
+ *
2576
+ * `radius` limits the edge distance from the center node and defaults to `1`.
2577
+ * `direction` controls how directed edges are traversed and defaults to
2578
+ * `"outgoing"`.
2579
+ *
2580
+ * @category models
2581
+ * @since 4.0.0
2582
+ */
2583
+ export interface NeighborhoodConfig {
2584
+ readonly radius?: number
2585
+ readonly direction?: TraversalDirection
2586
+ }
2587
+
2588
+ /**
2589
+ * Returns the induced subgraph containing nodes within a radius of a node.
2590
+ *
2591
+ * **Details**
2592
+ *
2593
+ * The `radius` option is the maximum edge distance from `nodeIndex` and
2594
+ * defaults to `1`. The `direction` option controls directed graph traversal and
2595
+ * defaults to `"outgoing"`. The result has the same graph kind as `self` and
2596
+ * keeps all original edges whose endpoints are both reached. `"undirected"`
2597
+ * ignores edge direction while finding reachable nodes.
2598
+ *
2599
+ * **Example** (Getting a local neighborhood)
2600
+ *
2601
+ * ```ts
2602
+ * import { Graph } from "effect"
2603
+ *
2604
+ * const graph = Graph.directed<string, string>((mutable) => {
2605
+ * const a = Graph.addNode(mutable, "A")
2606
+ * const b = Graph.addNode(mutable, "B")
2607
+ * const c = Graph.addNode(mutable, "C")
2608
+ * Graph.addEdge(mutable, a, b, "A-B")
2609
+ * Graph.addEdge(mutable, b, c, "B-C")
2610
+ * })
2611
+ *
2612
+ * const result = Graph.neighborhood(graph, 1, { radius: 1 })
2613
+ *
2614
+ * console.log(Graph.nodeCount(result)) // 2
2615
+ * ```
2616
+ *
2617
+ * @category set operations
2618
+ * @since 4.0.0
2619
+ */
2620
+ export const neighborhood: {
2621
+ /**
2622
+ * Returns the induced subgraph containing nodes within a radius of a node.
2623
+ *
2624
+ * **Details**
2625
+ *
2626
+ * The `radius` option is the maximum edge distance from `nodeIndex` and
2627
+ * defaults to `1`. The `direction` option controls directed graph traversal and
2628
+ * defaults to `"outgoing"`. The result has the same graph kind as `self` and
2629
+ * keeps all original edges whose endpoints are both reached. `"undirected"`
2630
+ * ignores edge direction while finding reachable nodes.
2631
+ *
2632
+ * **Example** (Getting a local neighborhood)
2633
+ *
2634
+ * ```ts
2635
+ * import { Graph } from "effect"
2636
+ *
2637
+ * const graph = Graph.directed<string, string>((mutable) => {
2638
+ * const a = Graph.addNode(mutable, "A")
2639
+ * const b = Graph.addNode(mutable, "B")
2640
+ * const c = Graph.addNode(mutable, "C")
2641
+ * Graph.addEdge(mutable, a, b, "A-B")
2642
+ * Graph.addEdge(mutable, b, c, "B-C")
2643
+ * })
2644
+ *
2645
+ * const result = Graph.neighborhood(graph, 1, { radius: 1 })
2646
+ *
2647
+ * console.log(Graph.nodeCount(result)) // 2
2648
+ * ```
2649
+ *
2650
+ * @category set operations
2651
+ * @since 4.0.0
2652
+ */
2653
+ (nodeIndex: NodeIndex, options?: NeighborhoodConfig): <N, E, T extends Kind = "directed">(self: Graph<N, E, T>) => Graph<N, E, T>
2654
+ /**
2655
+ * Returns the induced subgraph containing nodes within a radius of a node.
2656
+ *
2657
+ * **Details**
2658
+ *
2659
+ * The `radius` option is the maximum edge distance from `nodeIndex` and
2660
+ * defaults to `1`. The `direction` option controls directed graph traversal and
2661
+ * defaults to `"outgoing"`. The result has the same graph kind as `self` and
2662
+ * keeps all original edges whose endpoints are both reached. `"undirected"`
2663
+ * ignores edge direction while finding reachable nodes.
2664
+ *
2665
+ * **Example** (Getting a local neighborhood)
2666
+ *
2667
+ * ```ts
2668
+ * import { Graph } from "effect"
2669
+ *
2670
+ * const graph = Graph.directed<string, string>((mutable) => {
2671
+ * const a = Graph.addNode(mutable, "A")
2672
+ * const b = Graph.addNode(mutable, "B")
2673
+ * const c = Graph.addNode(mutable, "C")
2674
+ * Graph.addEdge(mutable, a, b, "A-B")
2675
+ * Graph.addEdge(mutable, b, c, "B-C")
2676
+ * })
2677
+ *
2678
+ * const result = Graph.neighborhood(graph, 1, { radius: 1 })
2679
+ *
2680
+ * console.log(Graph.nodeCount(result)) // 2
2681
+ * ```
2682
+ *
2683
+ * @category set operations
2684
+ * @since 4.0.0
2685
+ */
2686
+ <N, E, T extends Kind = "directed">(self: Graph<N, E, T>, nodeIndex: NodeIndex, options?: NeighborhoodConfig): Graph<N, E, T>
2687
+ } = dual((args) => isGraph(args[0]), <N, E, T extends Kind>(
2688
+ self: Graph<N, E, T>,
2689
+ nodeIndex: NodeIndex,
2690
+ options?: NeighborhoodConfig
2691
+ ): Graph<N, E, T> => {
2692
+ const selfImpl = graphImpl(self)
2693
+ const radius = options?.radius ?? 1
2694
+ const direction = options?.direction ?? "outgoing"
2695
+ const reached = new Set<NodeIndex>()
2696
+
2697
+ for (const index of indices(bfs(self, { start: [nodeIndex], direction, radius }))) {
2698
+ reached.add(index)
2699
+ }
2700
+
2701
+ return make(self.type)</**
2702
+ * Returns the induced subgraph containing nodes within a radius of a node.
2703
+ *
2704
+ * **Details**
2705
+ *
2706
+ * The `radius` option is the maximum edge distance from `nodeIndex` and
2707
+ * defaults to `1`. The `direction` option controls directed graph traversal and
2708
+ * defaults to `"outgoing"`. The result has the same graph kind as `self` and
2709
+ * keeps all original edges whose endpoints are both reached. `"undirected"`
2710
+ * ignores edge direction while finding reachable nodes.
2711
+ *
2712
+ * **Example** (Getting a local neighborhood)
2713
+ *
2714
+ * ```ts
2715
+ * import { Graph } from "effect"
2716
+ *
2717
+ * const graph = Graph.directed<string, string>((mutable) => {
2718
+ * const a = Graph.addNode(mutable, "A")
2719
+ * const b = Graph.addNode(mutable, "B")
2720
+ * const c = Graph.addNode(mutable, "C")
2721
+ * Graph.addEdge(mutable, a, b, "A-B")
2722
+ * Graph.addEdge(mutable, b, c, "B-C")
2723
+ * })
2724
+ *
2725
+ * const result = Graph.neighborhood(graph, 1, { radius: 1 })
2726
+ *
2727
+ * console.log(Graph.nodeCount(result)) // 2
2728
+ * ```
2729
+ *
2730
+ * @category set operations
2731
+ * @since 4.0.0
2732
+ */
2733
+ N, /**
2734
+ * Returns the induced subgraph containing nodes within a radius of a node.
2735
+ *
2736
+ * **Details**
2737
+ *
2738
+ * The `radius` option is the maximum edge distance from `nodeIndex` and
2739
+ * defaults to `1`. The `direction` option controls directed graph traversal and
2740
+ * defaults to `"outgoing"`. The result has the same graph kind as `self` and
2741
+ * keeps all original edges whose endpoints are both reached. `"undirected"`
2742
+ * ignores edge direction while finding reachable nodes.
2743
+ *
2744
+ * **Example** (Getting a local neighborhood)
2745
+ *
2746
+ * ```ts
2747
+ * import { Graph } from "effect"
2748
+ *
2749
+ * const graph = Graph.directed<string, string>((mutable) => {
2750
+ * const a = Graph.addNode(mutable, "A")
2751
+ * const b = Graph.addNode(mutable, "B")
2752
+ * const c = Graph.addNode(mutable, "C")
2753
+ * Graph.addEdge(mutable, a, b, "A-B")
2754
+ * Graph.addEdge(mutable, b, c, "B-C")
2755
+ * })
2756
+ *
2757
+ * const result = Graph.neighborhood(graph, 1, { radius: 1 })
2758
+ *
2759
+ * console.log(Graph.nodeCount(result)) // 2
2760
+ * ```
2761
+ *
2762
+ * @category set operations
2763
+ * @since 4.0.0
2764
+ */
2765
+ E>((mutable) => {
2766
+ const newIndexMap = new Map<NodeIndex, NodeIndex>()
538
2767
 
539
- return graph
540
- }
2768
+ for (const oldIndex of reached) {
2769
+ newIndexMap.set(oldIndex, addNode(mutable, Option.getOrThrow(getNode(self, oldIndex))))
2770
+ }
2771
+
2772
+ for (const edge of selfImpl.edges.values()) {
2773
+ if (reached.has(edge.source) && reached.has(edge.target)) {
2774
+ const sourceIndex = newIndexMap.get(edge.source)
2775
+ const targetIndex = newIndexMap.get(edge.target)
2776
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
2777
+ addEdge(mutable, sourceIndex, targetIndex, edge.data)
2778
+ }
2779
+ }
2780
+ }
2781
+ });
2782
+ })
541
2783
 
542
2784
  /**
543
- * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
2785
+ * Returns the disjoint union of two graphs.
544
2786
  *
545
- * **Example** (Applying scoped mutations)
546
- *
547
- * ```ts
548
- * import { Graph } from "effect"
2787
+ * **Details**
549
2788
  *
550
- * const graph = Graph.directed<string, number>()
551
- * const newGraph = Graph.mutate(graph, (mutable) => {
552
- * const nodeA = Graph.addNode(mutable, "A")
553
- * const nodeB = Graph.addNode(mutable, "B")
554
- * Graph.addEdge(mutable, nodeA, nodeB, 1)
555
- * })
2789
+ * Copies all nodes and edges from both graphs without merging equal node data.
2790
+ * The result has the same graph kind as `self`. Throws a `GraphError` when the
2791
+ * graph kinds do not match.
556
2792
  *
557
- * console.log(Graph.nodeCount(newGraph)) // 2
558
- * console.log(Graph.edgeCount(newGraph)) // 1
559
- * ```
2793
+ * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
560
2794
  *
561
- * @category mutations
562
- * @since 3.18.0
2795
+ * @category set operations
2796
+ * @since 4.0.0
563
2797
  */
564
- export const mutate: {
2798
+ export const sum: {
565
2799
  /**
566
- * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
567
- *
568
- * **Example** (Applying scoped mutations)
2800
+ * Returns the disjoint union of two graphs.
569
2801
  *
570
- * ```ts
571
- * import { Graph } from "effect"
2802
+ * **Details**
572
2803
  *
573
- * const graph = Graph.directed<string, number>()
574
- * const newGraph = Graph.mutate(graph, (mutable) => {
575
- * const nodeA = Graph.addNode(mutable, "A")
576
- * const nodeB = Graph.addNode(mutable, "B")
577
- * Graph.addEdge(mutable, nodeA, nodeB, 1)
578
- * })
2804
+ * Copies all nodes and edges from both graphs without merging equal node data.
2805
+ * The result has the same graph kind as `self`. Throws a `GraphError` when the
2806
+ * graph kinds do not match.
579
2807
  *
580
- * console.log(Graph.nodeCount(newGraph)) // 2
581
- * console.log(Graph.edgeCount(newGraph)) // 1
582
- * ```
2808
+ * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
583
2809
  *
584
- * @category mutations
585
- * @since 3.18.0
2810
+ * @category set operations
2811
+ * @since 4.0.0
586
2812
  */
587
- <N, E, T extends Kind = "directed">(f: (mutable: MutableGraph<N, E, T>) => void): (graph: Graph<N, E, T>) => Graph<N, E, T>
2813
+ <N, E, T extends Kind>(that: Graph<N, E, T>): (self: Graph<N, E, NoInfer<T>>) => Graph<N, E, T>
588
2814
  /**
589
- * Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
2815
+ * Returns the disjoint union of two graphs.
590
2816
  *
591
- * **Example** (Applying scoped mutations)
2817
+ * **Details**
592
2818
  *
593
- * ```ts
594
- * import { Graph } from "effect"
2819
+ * Copies all nodes and edges from both graphs without merging equal node data.
2820
+ * The result has the same graph kind as `self`. Throws a `GraphError` when the
2821
+ * graph kinds do not match.
595
2822
  *
596
- * const graph = Graph.directed<string, number>()
597
- * const newGraph = Graph.mutate(graph, (mutable) => {
598
- * const nodeA = Graph.addNode(mutable, "A")
599
- * const nodeB = Graph.addNode(mutable, "B")
600
- * Graph.addEdge(mutable, nodeA, nodeB, 1)
601
- * })
2823
+ * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
602
2824
  *
603
- * console.log(Graph.nodeCount(newGraph)) // 2
604
- * console.log(Graph.edgeCount(newGraph)) // 1
605
- * ```
2825
+ * @category set operations
2826
+ * @since 4.0.0
2827
+ */
2828
+ <N, E, T extends Kind>(self: Graph<N, E, T>, that: Graph<N, E, NoInfer<T>>): Graph<N, E, T>
2829
+ } = dual(2, <N, E, T extends Kind>(self: Graph<N, E, T>, that: Graph<N, E, T>): Graph<N, E, T> => {
2830
+ assertSameKind(self, that)
2831
+ return make(self.type)</**
2832
+ * Returns the disjoint union of two graphs.
606
2833
  *
607
- * @category mutations
608
- * @since 3.18.0
2834
+ * **Details**
2835
+ *
2836
+ * Copies all nodes and edges from both graphs without merging equal node data.
2837
+ * The result has the same graph kind as `self`. Throws a `GraphError` when the
2838
+ * graph kinds do not match.
2839
+ *
2840
+ * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
2841
+ *
2842
+ * @category set operations
2843
+ * @since 4.0.0
609
2844
  */
610
- <N, E, T extends Kind = "directed">(graph: Graph<N, E, T>, f: (mutable: MutableGraph<N, E, T>) => void): Graph<N, E, T>
611
- } = dual(2, <N, E, T extends Kind = "directed">(
612
- graph: Graph<N, E, T>,
613
- f: (mutable: MutableGraph<N, E, T>) => void
614
- ): Graph<N, E, T> => {
615
- const mutable = beginMutation(graph)
616
- f(mutable)
617
- return endMutation(mutable)
2845
+ N, /**
2846
+ * Returns the disjoint union of two graphs.
2847
+ *
2848
+ * **Details**
2849
+ *
2850
+ * Copies all nodes and edges from both graphs without merging equal node data.
2851
+ * The result has the same graph kind as `self`. Throws a `GraphError` when the
2852
+ * graph kinds do not match.
2853
+ *
2854
+ * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}`
2855
+ *
2856
+ * @category set operations
2857
+ * @since 4.0.0
2858
+ */
2859
+ E>((mutable) => {
2860
+ const copyInto = (graph: Graph<N, E, T>) => {
2861
+ const impl = graphImpl(graph)
2862
+ const indexMap = new Map<NodeIndex, NodeIndex>()
2863
+
2864
+ for (const [oldIndex, data] of impl.nodes) {
2865
+ indexMap.set(oldIndex, addNode(mutable, data))
2866
+ }
2867
+
2868
+ for (const edge of impl.edges.values()) {
2869
+ const sourceIndex = indexMap.get(edge.source)
2870
+ const targetIndex = indexMap.get(edge.target)
2871
+ if (sourceIndex !== undefined && targetIndex !== undefined) {
2872
+ addEdge(mutable, sourceIndex, targetIndex, edge.data)
2873
+ }
2874
+ }
2875
+ }
2876
+
2877
+ copyInto(self)
2878
+ copyInto(that)
2879
+ });
618
2880
  })
619
2881
 
620
2882
  // =============================================================================
@@ -663,17 +2925,20 @@ export const addNode = <N, E, T extends Kind = "directed">(
663
2925
  mutable: MutableGraph<N, E, T>,
664
2926
  data: N
665
2927
  ): NodeIndex => {
666
- const nodeIndex = mutable.nextNodeIndex
2928
+ assertMutable(mutable)
2929
+ const impl = graphImpl(mutable)
2930
+
2931
+ const nodeIndex = impl.nextNodeIndex
667
2932
 
668
2933
  // Add node data
669
- mutable.nodes.set(nodeIndex, data)
2934
+ impl.nodes.set(nodeIndex, data)
670
2935
 
671
2936
  // Initialize empty adjacency lists
672
- mutable.adjacency.set(nodeIndex, [])
673
- mutable.reverseAdjacency.set(nodeIndex, [])
2937
+ impl.adjacency.set(nodeIndex, [])
2938
+ impl.reverseAdjacency.set(nodeIndex, [])
674
2939
 
675
2940
  // Update graph allocators
676
- mutable.nextNodeIndex = mutable.nextNodeIndex + 1
2941
+ impl.nextNodeIndex = impl.nextNodeIndex + 1
677
2942
 
678
2943
  return nodeIndex
679
2944
  }
@@ -725,7 +2990,9 @@ export const getNode: {
725
2990
  * @category getters
726
2991
  * @since 3.18.0
727
2992
  */
728
- <N, E, T extends Kind = "directed">(nodeIndex: NodeIndex): (graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<N>
2993
+ (nodeIndex: NodeIndex): <N, E, T extends Kind = "directed">(
2994
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
2995
+ ) => Option.Option<N>
729
2996
  /**
730
2997
  * Gets the data associated with a node index safely, if it exists.
731
2998
  *
@@ -753,7 +3020,10 @@ export const getNode: {
753
3020
  } = dual(2, <N, E, T extends Kind = "directed">(
754
3021
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
755
3022
  nodeIndex: NodeIndex
756
- ): Option.Option<N> => graph.nodes.has(nodeIndex) ? Option.some(graph.nodes.get(nodeIndex)!) : Option.none())
3023
+ ): Option.Option<N> => {
3024
+ const impl = graphImpl(graph)
3025
+ return impl.nodes.has(nodeIndex) ? Option.some(impl.nodes.get(nodeIndex)!) : Option.none()
3026
+ })
757
3027
 
758
3028
  /**
759
3029
  * Checks whether a node with the given index exists in the graph.
@@ -833,7 +3103,7 @@ export const hasNode: {
833
3103
  } = dual(2, <N, E, T extends Kind = "directed">(
834
3104
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
835
3105
  nodeIndex: NodeIndex
836
- ): boolean => graph.nodes.has(nodeIndex))
3106
+ ): boolean => graphImpl(graph).nodes.has(nodeIndex))
837
3107
 
838
3108
  /**
839
3109
  * Returns the number of nodes in the graph.
@@ -860,7 +3130,7 @@ export const hasNode: {
860
3130
  */
861
3131
  export const nodeCount = <N, E, T extends Kind = "directed">(
862
3132
  graph: Graph<N, E, T> | MutableGraph<N, E, T>
863
- ): number => graph.nodes.size
3133
+ ): number => graphImpl(graph).nodes.size
864
3134
 
865
3135
  /**
866
3136
  * Finds the first node that matches the given predicate.
@@ -944,7 +3214,8 @@ export const findNode: {
944
3214
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
945
3215
  predicate: (data: N) => boolean
946
3216
  ): Option.Option<NodeIndex> => {
947
- for (const [index, data] of graph.nodes) {
3217
+ const impl = graphImpl(graph)
3218
+ for (const [index, data] of impl.nodes) {
948
3219
  if (predicate(data)) {
949
3220
  return Option.some(index)
950
3221
  }
@@ -1034,8 +3305,9 @@ export const findNodes: {
1034
3305
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1035
3306
  predicate: (data: N) => boolean
1036
3307
  ): Array<NodeIndex> => {
3308
+ const impl = graphImpl(graph)
1037
3309
  const results: Array<NodeIndex> = []
1038
- for (const [index, data] of graph.nodes) {
3310
+ for (const [index, data] of impl.nodes) {
1039
3311
  if (predicate(data)) {
1040
3312
  results.push(index)
1041
3313
  }
@@ -1131,7 +3403,8 @@ export const findEdge: {
1131
3403
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1132
3404
  predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1133
3405
  ): Option.Option<EdgeIndex> => {
1134
- for (const [edgeIndex, edgeData] of graph.edges) {
3406
+ const impl = graphImpl(graph)
3407
+ for (const [edgeIndex, edgeData] of impl.edges) {
1135
3408
  if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
1136
3409
  return Option.some(edgeIndex)
1137
3410
  }
@@ -1230,8 +3503,9 @@ export const findEdges: {
1230
3503
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
1231
3504
  predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
1232
3505
  ): Array<EdgeIndex> => {
3506
+ const impl = graphImpl(graph)
1233
3507
  const results: Array<EdgeIndex> = []
1234
- for (const [edgeIndex, edgeData] of graph.edges) {
3508
+ for (const [edgeIndex, edgeData] of impl.edges) {
1235
3509
  if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
1236
3510
  results.push(edgeIndex)
1237
3511
  }
@@ -1265,13 +3539,16 @@ export const updateNode = <N, E, T extends Kind = "directed">(
1265
3539
  index: NodeIndex,
1266
3540
  f: (data: N) => N
1267
3541
  ): void => {
1268
- if (!mutable.nodes.has(index)) {
3542
+ assertMutable(mutable)
3543
+ const impl = graphImpl(mutable)
3544
+
3545
+ if (!impl.nodes.has(index)) {
1269
3546
  return
1270
3547
  }
1271
3548
 
1272
- const currentData = mutable.nodes.get(index)!
3549
+ const currentData = impl.nodes.get(index)!
1273
3550
  const newData = f(currentData)
1274
- mutable.nodes.set(index, newData)
3551
+ impl.nodes.set(index, newData)
1275
3552
  }
1276
3553
 
1277
3554
  /**
@@ -1301,13 +3578,16 @@ export const updateEdge = <N, E, T extends Kind = "directed">(
1301
3578
  edgeIndex: EdgeIndex,
1302
3579
  f: (data: E) => E
1303
3580
  ): void => {
1304
- if (!mutable.edges.has(edgeIndex)) {
3581
+ assertMutable(mutable)
3582
+ const impl = graphImpl(mutable)
3583
+
3584
+ if (!impl.edges.has(edgeIndex)) {
1305
3585
  return
1306
3586
  }
1307
3587
 
1308
- const currentEdge = mutable.edges.get(edgeIndex)!
3588
+ const currentEdge = impl.edges.get(edgeIndex)!
1309
3589
  const newData = f(currentEdge.data)
1310
- mutable.edges.set(edgeIndex, new Edge({ ...currentEdge, data: newData }))
3590
+ impl.edges.set(edgeIndex, new Edge({ ...currentEdge, data: newData }))
1311
3591
  }
1312
3592
 
1313
3593
  /**
@@ -1341,10 +3621,13 @@ export const mapNodes = <N, E, T extends Kind = "directed">(
1341
3621
  mutable: MutableGraph<N, E, T>,
1342
3622
  f: (data: N) => N
1343
3623
  ): void => {
3624
+ assertMutable(mutable)
3625
+ const impl = graphImpl(mutable)
3626
+
1344
3627
  // Transform existing node data in place
1345
- for (const [index, data] of mutable.nodes) {
3628
+ for (const [index, data] of impl.nodes) {
1346
3629
  const newData = f(data)
1347
- mutable.nodes.set(index, newData)
3630
+ impl.nodes.set(index, newData)
1348
3631
  }
1349
3632
  }
1350
3633
 
@@ -1376,13 +3659,19 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
1376
3659
  mutable: MutableGraph<N, E, T>,
1377
3660
  f: (data: E) => E
1378
3661
  ): void => {
3662
+ assertMutable(mutable)
3663
+ const impl = graphImpl(mutable)
3664
+
1379
3665
  // Transform existing edge data in place
1380
- for (const [index, edgeData] of mutable.edges) {
3666
+ for (const [index, edgeData] of impl.edges) {
1381
3667
  const newData = f(edgeData.data)
1382
- mutable.edges.set(index, {
1383
- ...edgeData,
1384
- data: newData
1385
- })
3668
+ impl.edges.set(
3669
+ index,
3670
+ new Edge({
3671
+ ...edgeData,
3672
+ data: newData
3673
+ })
3674
+ )
1386
3675
  }
1387
3676
  }
1388
3677
 
@@ -1390,7 +3679,7 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
1390
3679
  * @internal
1391
3680
  */
1392
3681
  const rebuildAdjacency = <N, E, T extends Kind = "directed">(
1393
- mutable: MutableGraph<N, E, T>
3682
+ mutable: GraphImpl<N, E, T>
1394
3683
  ): void => {
1395
3684
  mutable.adjacency.clear()
1396
3685
  mutable.reverseAdjacency.clear()
@@ -1438,13 +3727,16 @@ const rebuildAdjacency = <N, E, T extends Kind = "directed">(
1438
3727
  export const reverse = <N, E, T extends Kind = "directed">(
1439
3728
  mutable: MutableGraph<N, E, T>
1440
3729
  ): void => {
1441
- if (mutable.type === "undirected") {
3730
+ assertMutable(mutable)
3731
+ const impl = graphImpl(mutable)
3732
+
3733
+ if (impl.type === "undirected") {
1442
3734
  return
1443
3735
  }
1444
3736
 
1445
3737
  // Reverse all edges by swapping source and target
1446
- for (const [index, edgeData] of mutable.edges) {
1447
- mutable.edges.set(
3738
+ for (const [index, edgeData] of impl.edges) {
3739
+ impl.edges.set(
1448
3740
  index,
1449
3741
  new Edge({
1450
3742
  source: edgeData.target,
@@ -1454,10 +3746,10 @@ export const reverse = <N, E, T extends Kind = "directed">(
1454
3746
  )
1455
3747
  }
1456
3748
 
1457
- rebuildAdjacency(mutable)
3749
+ rebuildAdjacency(impl)
1458
3750
 
1459
3751
  // Invalidate cycle flag since edge directions changed
1460
- mutable.acyclic = Option.none()
3752
+ impl.acyclic = Option.none()
1461
3753
  }
1462
3754
 
1463
3755
  /**
@@ -1494,14 +3786,17 @@ export const filterMapNodes = <N, E, T extends Kind = "directed">(
1494
3786
  mutable: MutableGraph<N, E, T>,
1495
3787
  f: (data: N) => Option.Option<N>
1496
3788
  ): void => {
3789
+ assertMutable(mutable)
3790
+ const impl = graphImpl(mutable)
3791
+
1497
3792
  const nodesToRemove: Array<NodeIndex> = []
1498
3793
 
1499
3794
  // First pass: identify nodes to remove and transform data for nodes to keep
1500
- for (const [index, data] of mutable.nodes) {
3795
+ for (const [index, data] of impl.nodes) {
1501
3796
  const result = f(data)
1502
3797
  if (Option.isSome(result)) {
1503
3798
  // Transform node data
1504
- mutable.nodes.set(index, result.value)
3799
+ impl.nodes.set(index, result.value)
1505
3800
  } else {
1506
3801
  // Mark for removal
1507
3802
  nodesToRemove.push(index)
@@ -1548,17 +3843,23 @@ export const filterMapEdges = <N, E, T extends Kind = "directed">(
1548
3843
  mutable: MutableGraph<N, E, T>,
1549
3844
  f: (data: E) => Option.Option<E>
1550
3845
  ): void => {
3846
+ assertMutable(mutable)
3847
+ const impl = graphImpl(mutable)
3848
+
1551
3849
  const edgesToRemove: Array<EdgeIndex> = []
1552
3850
 
1553
3851
  // First pass: identify edges to remove and transform data for edges to keep
1554
- for (const [index, edgeData] of mutable.edges) {
3852
+ for (const [index, edgeData] of impl.edges) {
1555
3853
  const result = f(edgeData.data)
1556
3854
  if (Option.isSome(result)) {
1557
3855
  // Transform edge data
1558
- mutable.edges.set(index, {
1559
- ...edgeData,
1560
- data: result.value
1561
- })
3856
+ impl.edges.set(
3857
+ index,
3858
+ new Edge({
3859
+ ...edgeData,
3860
+ data: result.value
3861
+ })
3862
+ )
1562
3863
  } else {
1563
3864
  // Mark for removal
1564
3865
  edgesToRemove.push(index)
@@ -1600,10 +3901,13 @@ export const filterNodes = <N, E, T extends Kind = "directed">(
1600
3901
  mutable: MutableGraph<N, E, T>,
1601
3902
  predicate: (data: N) => boolean
1602
3903
  ): void => {
3904
+ assertMutable(mutable)
3905
+ const impl = graphImpl(mutable)
3906
+
1603
3907
  const nodesToRemove: Array<NodeIndex> = []
1604
3908
 
1605
3909
  // Identify nodes to remove
1606
- for (const [index, data] of mutable.nodes) {
3910
+ for (const [index, data] of impl.nodes) {
1607
3911
  if (!predicate(data)) {
1608
3912
  nodesToRemove.push(index)
1609
3913
  }
@@ -1647,10 +3951,13 @@ export const filterEdges = <N, E, T extends Kind = "directed">(
1647
3951
  mutable: MutableGraph<N, E, T>,
1648
3952
  predicate: (data: E) => boolean
1649
3953
  ): void => {
3954
+ assertMutable(mutable)
3955
+ const impl = graphImpl(mutable)
3956
+
1650
3957
  const edgesToRemove: Array<EdgeIndex> = []
1651
3958
 
1652
3959
  // Identify edges to remove
1653
- for (const [index, edgeData] of mutable.edges) {
3960
+ for (const [index, edgeData] of impl.edges) {
1654
3961
  if (!predicate(edgeData.data)) {
1655
3962
  edgesToRemove.push(index)
1656
3963
  }
@@ -1668,7 +3975,7 @@ export const filterEdges = <N, E, T extends Kind = "directed">(
1668
3975
 
1669
3976
  /** @internal */
1670
3977
  const invalidateCycleFlagOnRemoval = <N, E, T extends Kind = "directed">(
1671
- mutable: MutableGraph<N, E, T>
3978
+ mutable: GraphImpl<N, E, T>
1672
3979
  ): void => {
1673
3980
  // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs).
1674
3981
  if (mutable.acyclic._tag === "Some" && mutable.acyclic.value === false) {
@@ -1678,7 +3985,7 @@ const invalidateCycleFlagOnRemoval = <N, E, T extends Kind = "directed">(
1678
3985
 
1679
3986
  /** @internal */
1680
3987
  const invalidateCycleFlagOnAddition = <N, E, T extends Kind = "directed">(
1681
- mutable: MutableGraph<N, E, T>
3988
+ mutable: GraphImpl<N, E, T>
1682
3989
  ): void => {
1683
3990
  // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs).
1684
3991
  if (mutable.acyclic._tag === "Some" && mutable.acyclic.value === true) {
@@ -1736,50 +4043,53 @@ export const addEdge = <N, E, T extends Kind = "directed">(
1736
4043
  target: NodeIndex,
1737
4044
  data: E
1738
4045
  ): EdgeIndex => {
4046
+ assertMutable(mutable)
4047
+ const impl = graphImpl(mutable)
4048
+
1739
4049
  // Validate that both nodes exist
1740
- if (!mutable.nodes.has(source)) {
4050
+ if (!impl.nodes.has(source)) {
1741
4051
  throw missingNode(source)
1742
4052
  }
1743
- if (!mutable.nodes.has(target)) {
4053
+ if (!impl.nodes.has(target)) {
1744
4054
  throw missingNode(target)
1745
4055
  }
1746
4056
 
1747
- const edgeIndex = mutable.nextEdgeIndex
4057
+ const edgeIndex = impl.nextEdgeIndex
1748
4058
 
1749
4059
  // Create edge data
1750
4060
  const edgeData = new Edge({ source, target, data })
1751
- mutable.edges.set(edgeIndex, edgeData)
4061
+ impl.edges.set(edgeIndex, edgeData)
1752
4062
 
1753
4063
  // Update adjacency lists
1754
- const sourceAdjacency = mutable.adjacency.get(source)
4064
+ const sourceAdjacency = impl.adjacency.get(source)
1755
4065
  if (sourceAdjacency !== undefined) {
1756
4066
  sourceAdjacency.push(edgeIndex)
1757
4067
  }
1758
4068
 
1759
- const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
4069
+ const targetReverseAdjacency = impl.reverseAdjacency.get(target)
1760
4070
  if (targetReverseAdjacency !== undefined) {
1761
4071
  targetReverseAdjacency.push(edgeIndex)
1762
4072
  }
1763
4073
 
1764
4074
  // For undirected graphs, add reverse connections
1765
- if (mutable.type === "undirected") {
1766
- const targetAdjacency = mutable.adjacency.get(target)
4075
+ if (impl.type === "undirected") {
4076
+ const targetAdjacency = impl.adjacency.get(target)
1767
4077
  if (targetAdjacency !== undefined) {
1768
4078
  targetAdjacency.push(edgeIndex)
1769
4079
  }
1770
4080
 
1771
- const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
4081
+ const sourceReverseAdjacency = impl.reverseAdjacency.get(source)
1772
4082
  if (sourceReverseAdjacency !== undefined) {
1773
4083
  sourceReverseAdjacency.push(edgeIndex)
1774
4084
  }
1775
4085
  }
1776
4086
 
1777
4087
  // Update allocators
1778
- mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1
4088
+ impl.nextEdgeIndex = impl.nextEdgeIndex + 1
1779
4089
 
1780
4090
  // Only invalidate cycle flag if the graph was acyclic
1781
4091
  // Adding edges cannot remove cycles from cyclic graphs
1782
- invalidateCycleFlagOnAddition(mutable)
4092
+ invalidateCycleFlagOnAddition(impl)
1783
4093
 
1784
4094
  return edgeIndex
1785
4095
  }
@@ -1809,8 +4119,11 @@ export const removeNode = <N, E, T extends Kind = "directed">(
1809
4119
  mutable: MutableGraph<N, E, T>,
1810
4120
  nodeIndex: NodeIndex
1811
4121
  ): void => {
4122
+ assertMutable(mutable)
4123
+ const impl = graphImpl(mutable)
4124
+
1812
4125
  // Check if node exists
1813
- if (!mutable.nodes.has(nodeIndex)) {
4126
+ if (!impl.nodes.has(nodeIndex)) {
1814
4127
  return // Node doesn't exist, nothing to remove
1815
4128
  }
1816
4129
 
@@ -1818,7 +4131,7 @@ export const removeNode = <N, E, T extends Kind = "directed">(
1818
4131
  const edgesToRemove: Array<EdgeIndex> = []
1819
4132
 
1820
4133
  // Get outgoing edges
1821
- const outgoingEdges = mutable.adjacency.get(nodeIndex)
4134
+ const outgoingEdges = impl.adjacency.get(nodeIndex)
1822
4135
  if (outgoingEdges !== undefined) {
1823
4136
  for (const edge of outgoingEdges) {
1824
4137
  edgesToRemove.push(edge)
@@ -1826,7 +4139,7 @@ export const removeNode = <N, E, T extends Kind = "directed">(
1826
4139
  }
1827
4140
 
1828
4141
  // Get incoming edges
1829
- const incomingEdges = mutable.reverseAdjacency.get(nodeIndex)
4142
+ const incomingEdges = impl.reverseAdjacency.get(nodeIndex)
1830
4143
  if (incomingEdges !== undefined) {
1831
4144
  for (const edge of incomingEdges) {
1832
4145
  edgesToRemove.push(edge)
@@ -1835,17 +4148,17 @@ export const removeNode = <N, E, T extends Kind = "directed">(
1835
4148
 
1836
4149
  // Remove all incident edges
1837
4150
  for (const edgeIndex of edgesToRemove) {
1838
- removeEdgeInternal(mutable, edgeIndex)
4151
+ removeEdgeInternal(impl, edgeIndex)
1839
4152
  }
1840
4153
 
1841
4154
  // Remove the node itself
1842
- mutable.nodes.delete(nodeIndex)
1843
- mutable.adjacency.delete(nodeIndex)
1844
- mutable.reverseAdjacency.delete(nodeIndex)
4155
+ impl.nodes.delete(nodeIndex)
4156
+ impl.adjacency.delete(nodeIndex)
4157
+ impl.reverseAdjacency.delete(nodeIndex)
1845
4158
 
1846
4159
  // Only invalidate cycle flag if the graph wasn't already known to be acyclic
1847
4160
  // Removing nodes cannot introduce cycles in an acyclic graph
1848
- invalidateCycleFlagOnRemoval(mutable)
4161
+ invalidateCycleFlagOnRemoval(impl)
1849
4162
  }
1850
4163
 
1851
4164
  /**
@@ -1873,18 +4186,21 @@ export const removeEdge = <N, E, T extends Kind = "directed">(
1873
4186
  mutable: MutableGraph<N, E, T>,
1874
4187
  edgeIndex: EdgeIndex
1875
4188
  ): void => {
1876
- const wasRemoved = removeEdgeInternal(mutable, edgeIndex)
4189
+ assertMutable(mutable)
4190
+ const impl = graphImpl(mutable)
4191
+
4192
+ const wasRemoved = removeEdgeInternal(impl, edgeIndex)
1877
4193
 
1878
4194
  // Only invalidate cycle flag if an edge was actually removed
1879
4195
  // and only if the graph wasn't already known to be acyclic
1880
4196
  if (wasRemoved) {
1881
- invalidateCycleFlagOnRemoval(mutable)
4197
+ invalidateCycleFlagOnRemoval(impl)
1882
4198
  }
1883
4199
  }
1884
4200
 
1885
4201
  /** @internal */
1886
4202
  const removeEdgeInternal = <N, E, T extends Kind = "directed">(
1887
- mutable: MutableGraph<N, E, T>,
4203
+ mutable: GraphImpl<N, E, T>,
1888
4204
  edgeIndex: EdgeIndex
1889
4205
  ): boolean => {
1890
4206
  // Get edge data
@@ -2000,7 +4316,9 @@ export const getEdge: {
2000
4316
  * @category getters
2001
4317
  * @since 3.18.0
2002
4318
  */
2003
- <E>(edgeIndex: EdgeIndex): <N, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Option.Option<Edge<E>>
4319
+ (edgeIndex: EdgeIndex): <N, E, T extends Kind = "directed">(
4320
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>
4321
+ ) => Option.Option<Edge<E>>
2004
4322
  // =============================================================================
2005
4323
  // Edge Query Operations
2006
4324
  // =============================================================================
@@ -2036,7 +4354,7 @@ export const getEdge: {
2036
4354
  } = dual(2, <N, E, T extends Kind = "directed">(
2037
4355
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2038
4356
  edgeIndex: EdgeIndex
2039
- ): Option.Option<Edge<E>> => Option.fromUndefinedOr(graph.edges.get(edgeIndex)))
4357
+ ): Option.Option<Edge<E>> => Option.fromUndefinedOr(graphImpl(graph).edges.get(edgeIndex)))
2040
4358
 
2041
4359
  /**
2042
4360
  * Checks whether an edge exists between two nodes in the graph.
@@ -2137,14 +4455,15 @@ export const hasEdge: {
2137
4455
  source: NodeIndex,
2138
4456
  target: NodeIndex
2139
4457
  ): boolean => {
2140
- const adjacencyList = graph.adjacency.get(source)
4458
+ const impl = graphImpl(graph)
4459
+ const adjacencyList = impl.adjacency.get(source)
2141
4460
  if (adjacencyList === undefined) {
2142
4461
  return false
2143
4462
  }
2144
4463
 
2145
4464
  // Check if any edge in the adjacency list connects to the target
2146
4465
  for (const edgeIndex of adjacencyList) {
2147
- const edge = graph.edges.get(edgeIndex)
4466
+ const edge = impl.edges.get(edgeIndex)
2148
4467
  if (edge !== undefined) {
2149
4468
  const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target
2150
4469
  if (neighbor === target) {
@@ -2184,16 +4503,17 @@ export const hasEdge: {
2184
4503
  */
2185
4504
  export const edgeCount = <N, E, T extends Kind = "directed">(
2186
4505
  graph: Graph<N, E, T> | MutableGraph<N, E, T>
2187
- ): number => graph.edges.size
4506
+ ): number => graphImpl(graph).edges.size
2188
4507
 
2189
4508
  const getDirectedNeighbors = <N, E>(
2190
4509
  graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
2191
4510
  nodeIndex: NodeIndex,
2192
4511
  direction: Direction
2193
4512
  ): Array<NodeIndex> => {
4513
+ const impl = graphImpl(graph)
2194
4514
  const adjacencyMap = direction === "incoming"
2195
- ? graph.reverseAdjacency
2196
- : graph.adjacency
4515
+ ? impl.reverseAdjacency
4516
+ : impl.adjacency
2197
4517
 
2198
4518
  const adjacencyList = adjacencyMap.get(nodeIndex)
2199
4519
  if (adjacencyList === undefined) {
@@ -2202,7 +4522,7 @@ const getDirectedNeighbors = <N, E>(
2202
4522
 
2203
4523
  const result: Array<NodeIndex> = []
2204
4524
  for (const edgeIndex of adjacencyList) {
2205
- const edge = graph.edges.get(edgeIndex)
4525
+ const edge = impl.edges.get(edgeIndex)
2206
4526
  if (edge !== undefined) {
2207
4527
  result.push(direction === "incoming" ? edge.source : edge.target)
2208
4528
  }
@@ -2660,6 +4980,9 @@ export interface GraphVizOptions<N, E> {
2660
4980
  readonly graphName?: string
2661
4981
  }
2662
4982
 
4983
+ const escapeGraphVizString = (value: string): string =>
4984
+ value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r\n|\r|\n/g, "\\n")
4985
+
2663
4986
  /**
2664
4987
  * Exports a graph to GraphViz DOT format for visualization.
2665
4988
  *
@@ -2679,7 +5002,7 @@ export interface GraphVizOptions<N, E> {
2679
5002
  *
2680
5003
  * const dot = Graph.toGraphViz(graph)
2681
5004
  * console.log(dot)
2682
- * // digraph G {
5005
+ * // digraph "G" {
2683
5006
  * // "0" [label="Node A"];
2684
5007
  * // "1" [label="Node B"];
2685
5008
  * // "2" [label="Node C"];
@@ -2712,7 +5035,7 @@ export const toGraphViz: {
2712
5035
  *
2713
5036
  * const dot = Graph.toGraphViz(graph)
2714
5037
  * console.log(dot)
2715
- * // digraph G {
5038
+ * // digraph "G" {
2716
5039
  * // "0" [label="Node A"];
2717
5040
  * // "1" [label="Node B"];
2718
5041
  * // "2" [label="Node C"];
@@ -2745,7 +5068,7 @@ export const toGraphViz: {
2745
5068
  *
2746
5069
  * const dot = Graph.toGraphViz(graph)
2747
5070
  * console.log(dot)
2748
- * // digraph G {
5071
+ * // digraph "G" {
2749
5072
  * // "0" [label="Node A"];
2750
5073
  * // "1" [label="Node B"];
2751
5074
  * // "2" [label="Node C"];
@@ -2766,6 +5089,7 @@ export const toGraphViz: {
2766
5089
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2767
5090
  options?: GraphVizOptions<N, E>
2768
5091
  ): string => {
5092
+ const impl = graphImpl(graph)
2769
5093
  const {
2770
5094
  edgeLabel = (data: E) => String(data),
2771
5095
  graphName = "G",
@@ -2775,19 +5099,20 @@ export const toGraphViz: {
2775
5099
  const isDirected = graph.type === "directed"
2776
5100
  const graphType = isDirected ? "digraph" : "graph"
2777
5101
  const edgeOperator = isDirected ? "->" : "--"
5102
+ const graphId = `"${escapeGraphVizString(graphName)}"`
2778
5103
 
2779
5104
  const lines: Array<string> = []
2780
- lines.push(`${graphType} ${graphName} {`)
5105
+ lines.push(`${graphType} ${graphId} {`)
2781
5106
 
2782
5107
  // Add nodes
2783
- for (const [nodeIndex, nodeData] of graph.nodes) {
2784
- const label = nodeLabel(nodeData).replace(/"/g, "\\\"")
5108
+ for (const [nodeIndex, nodeData] of impl.nodes) {
5109
+ const label = escapeGraphVizString(nodeLabel(nodeData))
2785
5110
  lines.push(` "${nodeIndex}" [label="${label}"];`)
2786
5111
  }
2787
5112
 
2788
5113
  // Add edges
2789
- for (const [, edgeData] of graph.edges) {
2790
- const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"")
5114
+ for (const [, edgeData] of impl.edges) {
5115
+ const label = escapeGraphVizString(edgeLabel(edgeData.data))
2791
5116
  lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`)
2792
5117
  }
2793
5118
 
@@ -3181,22 +5506,22 @@ const formatMermaidNode = (
3181
5506
  * name: "MyApp",
3182
5507
  * version: "1.0.0",
3183
5508
  * type: "library"
3184
- * })
5509
+ * } satisfies Dependency)
3185
5510
  * const react = Graph.addNode(mutable, {
3186
5511
  * name: "React",
3187
5512
  * version: "18.0.0",
3188
5513
  * type: "framework"
3189
- * })
5514
+ * } satisfies Dependency)
3190
5515
  * const lodash = Graph.addNode(mutable, {
3191
5516
  * name: "Lodash",
3192
5517
  * version: "4.17.0",
3193
5518
  * type: "library"
3194
- * })
5519
+ * } satisfies Dependency)
3195
5520
  * const webpack = Graph.addNode(mutable, {
3196
5521
  * name: "Webpack",
3197
5522
  * version: "5.0.0",
3198
5523
  * type: "tool"
3199
- * })
5524
+ * } satisfies Dependency)
3200
5525
  *
3201
5526
  * Graph.addEdge(mutable, app, react, "depends on")
3202
5527
  * Graph.addEdge(mutable, app, lodash, "depends on")
@@ -3361,22 +5686,22 @@ export const toMermaid: {
3361
5686
  * name: "MyApp",
3362
5687
  * version: "1.0.0",
3363
5688
  * type: "library"
3364
- * })
5689
+ * } satisfies Dependency)
3365
5690
  * const react = Graph.addNode(mutable, {
3366
5691
  * name: "React",
3367
5692
  * version: "18.0.0",
3368
5693
  * type: "framework"
3369
- * })
5694
+ * } satisfies Dependency)
3370
5695
  * const lodash = Graph.addNode(mutable, {
3371
5696
  * name: "Lodash",
3372
5697
  * version: "4.17.0",
3373
5698
  * type: "library"
3374
- * })
5699
+ * } satisfies Dependency)
3375
5700
  * const webpack = Graph.addNode(mutable, {
3376
5701
  * name: "Webpack",
3377
5702
  * version: "5.0.0",
3378
5703
  * type: "tool"
3379
- * })
5704
+ * } satisfies Dependency)
3380
5705
  *
3381
5706
  * Graph.addEdge(mutable, app, react, "depends on")
3382
5707
  * Graph.addEdge(mutable, app, lodash, "depends on")
@@ -3541,22 +5866,22 @@ export const toMermaid: {
3541
5866
  * name: "MyApp",
3542
5867
  * version: "1.0.0",
3543
5868
  * type: "library"
3544
- * })
5869
+ * } satisfies Dependency)
3545
5870
  * const react = Graph.addNode(mutable, {
3546
5871
  * name: "React",
3547
5872
  * version: "18.0.0",
3548
5873
  * type: "framework"
3549
- * })
5874
+ * } satisfies Dependency)
3550
5875
  * const lodash = Graph.addNode(mutable, {
3551
5876
  * name: "Lodash",
3552
5877
  * version: "4.17.0",
3553
5878
  * type: "library"
3554
- * })
5879
+ * } satisfies Dependency)
3555
5880
  * const webpack = Graph.addNode(mutable, {
3556
5881
  * name: "Webpack",
3557
5882
  * version: "5.0.0",
3558
5883
  * type: "tool"
3559
- * })
5884
+ * } satisfies Dependency)
3560
5885
  *
3561
5886
  * Graph.addEdge(mutable, app, react, "depends on")
3562
5887
  * Graph.addEdge(mutable, app, lodash, "depends on")
@@ -3597,6 +5922,7 @@ export const toMermaid: {
3597
5922
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3598
5923
  options?: MermaidOptions<N, E>
3599
5924
  ): string => {
5925
+ const impl = graphImpl(graph)
3600
5926
  // Extract and validate options with defaults
3601
5927
  const {
3602
5928
  diagramType,
@@ -3615,7 +5941,7 @@ export const toMermaid: {
3615
5941
  lines.push(`${finalDiagramType} ${direction}`)
3616
5942
 
3617
5943
  // Add nodes
3618
- for (const [nodeIndex, nodeData] of graph.nodes) {
5944
+ for (const [nodeIndex, nodeData] of impl.nodes) {
3619
5945
  const nodeId = String(nodeIndex)
3620
5946
  const label = escapeMermaidLabel(nodeLabel(nodeData))
3621
5947
  const shape = nodeShape(nodeData)
@@ -3625,7 +5951,7 @@ export const toMermaid: {
3625
5951
 
3626
5952
  // Add edges
3627
5953
  const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---"
3628
- for (const [, edgeData] of graph.edges) {
5954
+ for (const [, edgeData] of impl.edges) {
3629
5955
  const sourceId = String(edgeData.source)
3630
5956
  const targetId = String(edgeData.target)
3631
5957
  const label = escapeMermaidLabel(edgeLabel(edgeData.data))
@@ -3641,11 +5967,30 @@ export const toMermaid: {
3641
5967
  })
3642
5968
 
3643
5969
  // =============================================================================
3644
- // Direction Types for Bidirectional Traversal
5970
+ // Edge Direction Types
3645
5971
  // =============================================================================
3646
5972
 
3647
5973
  /**
3648
- * Direction for graph traversal, indicating which edges to follow.
5974
+ * Direction of directed edges relative to a node.
5975
+ *
5976
+ * **Details**
5977
+ *
5978
+ * `"outgoing"` selects edges whose source is the node, while `"incoming"`
5979
+ * selects edges whose target is the node.
5980
+ *
5981
+ * @category models
5982
+ * @since 3.18.0
5983
+ */
5984
+ export type Direction = "outgoing" | "incoming"
5985
+
5986
+ /**
5987
+ * Controls how traversal follows directed edges.
5988
+ *
5989
+ * **Details**
5990
+ *
5991
+ * `"outgoing"` follows edges from source to target, `"incoming"` follows them
5992
+ * from target to source, and `"undirected"` allows traversal in either
5993
+ * direction.
3649
5994
  *
3650
5995
  * **Example** (Traversing by direction)
3651
5996
  *
@@ -3655,24 +6000,28 @@ export const toMermaid: {
3655
6000
  * const graph = Graph.directed<string, string>((mutable) => {
3656
6001
  * const a = Graph.addNode(mutable, "A")
3657
6002
  * const b = Graph.addNode(mutable, "B")
3658
- * Graph.addEdge(mutable, a, b, "A->B")
6003
+ * const c = Graph.addNode(mutable, "C")
6004
+ * Graph.addEdge(mutable, a, b, "A-B")
6005
+ * Graph.addEdge(mutable, a, c, "A-C")
3659
6006
  * })
3660
6007
  *
3661
- * // Follow outgoing edges (normal direction)
3662
- * const outgoingNodes = Array.from(
3663
- * Graph.indices(Graph.dfs(graph, { start: [0], direction: "outgoing" }))
3664
- * )
6008
+ * const outgoing = Array.from(
6009
+ * Graph.indices(Graph.bfs(graph, { start: [0], direction: "outgoing" }))
6010
+ * ) // [0, 1, 2]
3665
6011
  *
3666
- * // Follow incoming edges (reverse direction)
3667
- * const incomingNodes = Array.from(
3668
- * Graph.indices(Graph.dfs(graph, { start: [1], direction: "incoming" }))
3669
- * )
6012
+ * const incoming = Array.from(
6013
+ * Graph.indices(Graph.bfs(graph, { start: [1], direction: "incoming" }))
6014
+ * ) // [1, 0]
6015
+ *
6016
+ * const undirected = Array.from(
6017
+ * Graph.indices(Graph.bfs(graph, { start: [1], direction: "undirected" }))
6018
+ * ) // [1, 0, 2]
3670
6019
  * ```
3671
6020
  *
3672
6021
  * @category models
3673
- * @since 3.18.0
6022
+ * @since 4.0.0
3674
6023
  */
3675
- export type Direction = "outgoing" | "incoming"
6024
+ export type TraversalDirection = Direction | "undirected"
3676
6025
 
3677
6026
  // =============================================================================
3678
6027
  // Graph Structure Analysis Algorithms
@@ -3685,7 +6034,8 @@ export type Direction = "outgoing" | "incoming"
3685
6034
  *
3686
6035
  * Uses depth-first search to detect back edges, which indicate cycles.
3687
6036
  * For directed graphs, any back edge creates a cycle. For undirected graphs,
3688
- * a back edge that doesn't go to the immediate parent creates a cycle.
6037
+ * a back edge that doesn't use the same edge used to enter the current node
6038
+ * creates a cycle.
3689
6039
  *
3690
6040
  * **Example** (Checking cycles)
3691
6041
  *
@@ -3718,39 +6068,51 @@ export type Direction = "outgoing" | "incoming"
3718
6068
  export const isAcyclic = <N, E, T extends Kind = "directed">(
3719
6069
  graph: Graph<N, E, T> | MutableGraph<N, E, T>
3720
6070
  ): boolean => {
6071
+ const impl = graphImpl(graph)
3721
6072
  // Use existing cycle flag if available
3722
- if (Option.isSome(graph.acyclic)) {
3723
- return graph.acyclic.value
6073
+ if (Option.isSome(impl.acyclic)) {
6074
+ return impl.acyclic.value
3724
6075
  }
3725
6076
 
3726
6077
  if (graph.type === "undirected") {
3727
6078
  const visited = new Set<NodeIndex>()
3728
6079
 
3729
- for (const startNode of graph.nodes.keys()) {
6080
+ for (const startNode of impl.nodes.keys()) {
3730
6081
  if (visited.has(startNode)) {
3731
6082
  continue
3732
6083
  }
3733
6084
 
3734
6085
  visited.add(startNode)
3735
- const stack: Array<{ node: NodeIndex; parent: NodeIndex | null }> = [{ node: startNode, parent: null }]
6086
+ const stack: Array<{ node: NodeIndex; incoming: EdgeIndex | null }> = [{ node: startNode, incoming: null }]
3736
6087
 
3737
6088
  while (stack.length > 0) {
3738
- const { node, parent } = stack.pop()!
3739
- const nodeNeighbors = getUndirectedNeighbors(graph as any, node)
6089
+ const { node, incoming } = stack.pop()!
6090
+ const adjacencyList = impl.adjacency.get(node)
6091
+ if (adjacencyList === undefined) {
6092
+ continue
6093
+ }
3740
6094
 
3741
- for (const neighbor of nodeNeighbors) {
6095
+ for (const edgeIndex of adjacencyList) {
6096
+ if (edgeIndex === incoming) {
6097
+ continue
6098
+ }
6099
+ const edge = impl.edges.get(edgeIndex)
6100
+ if (edge === undefined) {
6101
+ continue
6102
+ }
6103
+ const neighbor = getTraversableNeighbor(graph, node, edge)
3742
6104
  if (!visited.has(neighbor)) {
3743
6105
  visited.add(neighbor)
3744
- stack.push({ node: neighbor, parent: node })
3745
- } else if (neighbor !== parent) {
3746
- graph.acyclic = Option.some(false)
6106
+ stack.push({ node: neighbor, incoming: edgeIndex })
6107
+ } else {
6108
+ impl.acyclic = Option.some(false)
3747
6109
  return false
3748
6110
  }
3749
6111
  }
3750
6112
  }
3751
6113
  }
3752
6114
 
3753
- graph.acyclic = Option.some(true)
6115
+ impl.acyclic = Option.some(true)
3754
6116
  return true
3755
6117
  }
3756
6118
 
@@ -3762,7 +6124,7 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
3762
6124
  type DfsStackEntry = [NodeIndex, Array<NodeIndex>, number, boolean]
3763
6125
 
3764
6126
  // Get all nodes to handle disconnected components
3765
- for (const startNode of graph.nodes.keys()) {
6127
+ for (const startNode of impl.nodes.keys()) {
3766
6128
  if (visited.has(startNode)) {
3767
6129
  continue // Already processed this component
3768
6130
  }
@@ -3777,7 +6139,7 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
3777
6139
  if (isFirstVisit) {
3778
6140
  if (recursionStack.has(node)) {
3779
6141
  // Back edge found - cycle detected
3780
- graph.acyclic = Option.some(false)
6142
+ impl.acyclic = Option.some(false)
3781
6143
  return false
3782
6144
  }
3783
6145
 
@@ -3806,7 +6168,7 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
3806
6168
 
3807
6169
  if (recursionStack.has(neighbor)) {
3808
6170
  // Back edge found - cycle detected
3809
- graph.acyclic = Option.some(false)
6171
+ impl.acyclic = Option.some(false)
3810
6172
  return false
3811
6173
  }
3812
6174
 
@@ -3822,7 +6184,7 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
3822
6184
  }
3823
6185
 
3824
6186
  // Cache the result
3825
- graph.acyclic = Option.some(true)
6187
+ impl.acyclic = Option.some(true)
3826
6188
  return true
3827
6189
  }
3828
6190
 
@@ -3870,12 +6232,13 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
3870
6232
  export const isBipartite = <N, E>(
3871
6233
  graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
3872
6234
  ): boolean => {
6235
+ const impl = graphImpl(graph)
3873
6236
  const coloring = new Map<NodeIndex, 0 | 1>()
3874
6237
  const discovered = new Set<NodeIndex>()
3875
6238
  let isBipartiteGraph = true
3876
6239
 
3877
6240
  // Get all nodes to handle disconnected components
3878
- for (const startNode of graph.nodes.keys()) {
6241
+ for (const startNode of impl.nodes.keys()) {
3879
6242
  if (!discovered.has(startNode)) {
3880
6243
  // Start BFS coloring from this component
3881
6244
  const queue: Array<NodeIndex> = [startNode]
@@ -3923,13 +6286,14 @@ const getUndirectedNeighbors = <N, E>(
3923
6286
  graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">,
3924
6287
  nodeIndex: NodeIndex
3925
6288
  ): Array<NodeIndex> => {
6289
+ const impl = graphImpl(graph)
3926
6290
  const neighbors = new Set<NodeIndex>()
3927
6291
 
3928
6292
  // Check edges where this node is the source
3929
- const adjacencyList = graph.adjacency.get(nodeIndex)
6293
+ const adjacencyList = impl.adjacency.get(nodeIndex)
3930
6294
  if (adjacencyList !== undefined) {
3931
6295
  for (const edgeIndex of adjacencyList) {
3932
- const edge = graph.edges.get(edgeIndex)
6296
+ const edge = impl.edges.get(edgeIndex)
3933
6297
  if (edge !== undefined) {
3934
6298
  // For undirected graphs, the neighbor is the other endpoint
3935
6299
  const otherNode = edge.source === nodeIndex ? edge.target : edge.source
@@ -3944,14 +6308,24 @@ const getUndirectedNeighbors = <N, E>(
3944
6308
  const getTraversalNeighbors = <N, E, T extends Kind>(
3945
6309
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3946
6310
  nodeIndex: NodeIndex,
3947
- direction: Direction
3948
- ): Array<NodeIndex> =>
3949
- graph.type === "undirected"
3950
- ? getUndirectedNeighbors(graph as any, nodeIndex)
3951
- : getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, direction)
6311
+ direction: TraversalDirection
6312
+ ): Array<NodeIndex> => {
6313
+ if (graph.type === "undirected") {
6314
+ return getUndirectedNeighbors(graph as any, nodeIndex)
6315
+ }
6316
+ const directed = graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
6317
+ if (direction !== "undirected") {
6318
+ return getDirectedNeighbors(directed, nodeIndex, direction)
6319
+ }
6320
+ const neighbors = new Set(getDirectedNeighbors(directed, nodeIndex, "outgoing"))
6321
+ for (const neighbor of getDirectedNeighbors(directed, nodeIndex, "incoming")) {
6322
+ neighbors.add(neighbor)
6323
+ }
6324
+ return Array.from(neighbors)
6325
+ }
3952
6326
 
3953
- const getTraversableNeighbor = <E, T extends Kind>(
3954
- graph: Graph<unknown, E, T> | MutableGraph<unknown, E, T>,
6327
+ const getTraversableNeighbor = <N, E, T extends Kind>(
6328
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3955
6329
  current: NodeIndex,
3956
6330
  edge: Edge<E>
3957
6331
  ): NodeIndex => graph.type === "undirected" && edge.target === current ? edge.source : edge.target
@@ -3984,9 +6358,10 @@ const getTraversableNeighbor = <E, T extends Kind>(
3984
6358
  export const connectedComponents = <N, E>(
3985
6359
  graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
3986
6360
  ): Array<Array<NodeIndex>> => {
6361
+ const impl = graphImpl(graph)
3987
6362
  const visited = new Set<NodeIndex>()
3988
6363
  const components: Array<Array<NodeIndex>> = []
3989
- for (const startNode of graph.nodes.keys()) {
6364
+ for (const startNode of impl.nodes.keys()) {
3990
6365
  if (!visited.has(startNode)) {
3991
6366
  // DFS to find all nodes in this component
3992
6367
  const component: Array<NodeIndex> = []
@@ -4051,6 +6426,7 @@ export const stronglyConnectedComponents = <N, E>(
4051
6426
  throw new GraphError({ message: "Cannot find strongly connected components of undirected graph" })
4052
6427
  }
4053
6428
 
6429
+ const impl = graphImpl(graph)
4054
6430
  const visited = new Set<NodeIndex>()
4055
6431
  const finishOrder: Array<NodeIndex> = []
4056
6432
  // Iterate directly over node keys
@@ -4059,7 +6435,7 @@ export const stronglyConnectedComponents = <N, E>(
4059
6435
  // Stack entry: [node, neighbors, neighborIndex, isFirstVisit]
4060
6436
  type DfsStackEntry = [NodeIndex, Array<NodeIndex>, number, boolean]
4061
6437
 
4062
- for (const startNode of graph.nodes.keys()) {
6438
+ for (const startNode of impl.nodes.keys()) {
4063
6439
  if (visited.has(startNode)) {
4064
6440
  continue
4065
6441
  }
@@ -4121,10 +6497,10 @@ export const stronglyConnectedComponents = <N, E>(
4121
6497
  scc.push(node)
4122
6498
 
4123
6499
  // Use reverse adjacency (transpose graph)
4124
- const reverseAdjacency = graph.reverseAdjacency.get(node)
6500
+ const reverseAdjacency = impl.reverseAdjacency.get(node)
4125
6501
  if (reverseAdjacency !== undefined) {
4126
6502
  for (const edgeIndex of reverseAdjacency) {
4127
- const edge = graph.edges.get(edgeIndex)
6503
+ const edge = impl.edges.get(edgeIndex)
4128
6504
  if (edge !== undefined) {
4129
6505
  const predecessor = edge.source
4130
6506
  if (!visited.has(predecessor)) {
@@ -4178,6 +6554,53 @@ export interface PathResult<E> {
4178
6554
  readonly costs: Array<E>
4179
6555
  }
4180
6556
 
6557
+ interface MinHeapEntry {
6558
+ readonly node: NodeIndex
6559
+ readonly priority: number
6560
+ readonly sequence: number
6561
+ }
6562
+
6563
+ const minHeapLessThan = (self: MinHeapEntry, that: MinHeapEntry): boolean =>
6564
+ self.priority < that.priority || (self.priority === that.priority && self.sequence < that.sequence)
6565
+
6566
+ const minHeapPush = (heap: Array<MinHeapEntry>, entry: MinHeapEntry): void => {
6567
+ let index = heap.length
6568
+ heap.push(entry)
6569
+ while (index > 0) {
6570
+ const parent = (index - 1) >>> 1
6571
+ if (!minHeapLessThan(entry, heap[parent])) {
6572
+ break
6573
+ }
6574
+ heap[index] = heap[parent]
6575
+ index = parent
6576
+ }
6577
+ heap[index] = entry
6578
+ }
6579
+
6580
+ const minHeapPop = (heap: Array<MinHeapEntry>): MinHeapEntry | undefined => {
6581
+ const first = heap[0]
6582
+ const last = heap.pop()
6583
+ if (last === undefined || heap.length === 0) {
6584
+ return first
6585
+ }
6586
+ let index = 0
6587
+ while (true) {
6588
+ const left = index * 2 + 1
6589
+ if (left >= heap.length) {
6590
+ break
6591
+ }
6592
+ const right = left + 1
6593
+ const child = right < heap.length && minHeapLessThan(heap[right], heap[left]) ? right : left
6594
+ if (!minHeapLessThan(heap[child], last)) {
6595
+ break
6596
+ }
6597
+ heap[index] = heap[child]
6598
+ index = child
6599
+ }
6600
+ heap[index] = last
6601
+ return first
6602
+ }
6603
+
4181
6604
  /**
4182
6605
  * Configuration for finding a shortest path with Dijkstra's algorithm.
4183
6606
  *
@@ -4189,12 +6612,13 @@ export interface PathResult<E> {
4189
6612
  * **Details**
4190
6613
  *
4191
6614
  * Specifies the source and target node indices, plus a cost function that maps
4192
- * each edge's data to a non-negative numeric weight.
6615
+ * each edge's data to a non-negative numeric weight. `Infinity` is allowed and
6616
+ * behaves like an impassable edge.
4193
6617
  *
4194
6618
  * **Gotchas**
4195
6619
  *
4196
6620
  * `dijkstra` throws a `GraphError` when either endpoint does not exist or when
4197
- * the cost function returns a negative weight.
6621
+ * the cost function returns a negative weight or `NaN`.
4198
6622
  *
4199
6623
  * @see {@link dijkstra} for the algorithm that consumes this configuration
4200
6624
  * @see {@link AstarConfig} for heuristic shortest-path search
@@ -4209,31 +6633,16 @@ export interface DijkstraConfig<E> {
4209
6633
  cost: (edgeData: E) => number
4210
6634
  }
4211
6635
 
4212
- const validateNonNegativeEdgeWeights = <N, E, T extends Kind = "directed">(
4213
- graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4214
- cost: (edgeData: E) => number,
4215
- algorithm: string
4216
- ): Map<EdgeIndex, number> => {
4217
- const edgeWeights = new Map<EdgeIndex, number>()
4218
- for (const [edgeIndex, edgeData] of graph.edges) {
4219
- const weight = cost(edgeData.data)
4220
- if (weight < 0 || Number.isNaN(weight)) {
4221
- throw new GraphError({ message: `${algorithm} requires non-negative edge weights` })
4222
- }
4223
- edgeWeights.set(edgeIndex, weight)
4224
- }
4225
- return edgeWeights
4226
- }
4227
-
4228
6636
  /**
4229
6637
  * Finds the shortest path from the configured source node to the target node
4230
6638
  * using Dijkstra's algorithm.
4231
6639
  *
4232
6640
  * **Details**
4233
6641
  *
4234
- * Edge costs must be non-negative. Returns `Option.none()` when the target is
4235
- * not reachable, and throws a `GraphError` when either endpoint is missing or a
4236
- * negative edge cost is encountered.
6642
+ * Edge costs must be non-negative and not `NaN`. `Infinity` is allowed and
6643
+ * behaves like an impassable edge. Returns `Option.none()` when the target is
6644
+ * not reachable, and throws a `GraphError` when either endpoint is missing or an
6645
+ * edge cost is negative or `NaN`.
4237
6646
  *
4238
6647
  * **Example** (Finding shortest paths with Dijkstra)
4239
6648
  *
@@ -4271,9 +6680,10 @@ export const dijkstra: {
4271
6680
  *
4272
6681
  * **Details**
4273
6682
  *
4274
- * Edge costs must be non-negative. Returns `Option.none()` when the target is
4275
- * not reachable, and throws a `GraphError` when either endpoint is missing or a
4276
- * negative edge cost is encountered.
6683
+ * Edge costs must be non-negative and not `NaN`. `Infinity` is allowed and
6684
+ * behaves like an impassable edge. Returns `Option.none()` when the target is
6685
+ * not reachable, and throws a `GraphError` when either endpoint is missing or an
6686
+ * edge cost is negative or `NaN`.
4277
6687
  *
4278
6688
  * **Example** (Finding shortest paths with Dijkstra)
4279
6689
  *
@@ -4311,9 +6721,10 @@ export const dijkstra: {
4311
6721
  *
4312
6722
  * **Details**
4313
6723
  *
4314
- * Edge costs must be non-negative. Returns `Option.none()` when the target is
4315
- * not reachable, and throws a `GraphError` when either endpoint is missing or a
4316
- * negative edge cost is encountered.
6724
+ * Edge costs must be non-negative and not `NaN`. `Infinity` is allowed and
6725
+ * behaves like an impassable edge. Returns `Option.none()` when the target is
6726
+ * not reachable, and throws a `GraphError` when either endpoint is missing or an
6727
+ * edge cost is negative or `NaN`.
4317
6728
  *
4318
6729
  * **Example** (Finding shortest paths with Dijkstra)
4319
6730
  *
@@ -4349,15 +6760,23 @@ export const dijkstra: {
4349
6760
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4350
6761
  config: DijkstraConfig<E>
4351
6762
  ): Option.Option<PathResult<E>> => {
6763
+ const impl = graphImpl(graph)
4352
6764
  // Validate that source and target nodes exist
4353
- if (!graph.nodes.has(config.source)) {
6765
+ if (!impl.nodes.has(config.source)) {
4354
6766
  throw missingNode(config.source)
4355
6767
  }
4356
- if (!graph.nodes.has(config.target)) {
6768
+ if (!impl.nodes.has(config.target)) {
4357
6769
  throw missingNode(config.target)
4358
6770
  }
4359
6771
 
4360
- const edgeWeights = validateNonNegativeEdgeWeights(graph, config.cost, "Dijkstra's algorithm")
6772
+ const edgeWeights = new Map<EdgeIndex, number>()
6773
+ for (const [edgeIndex, edgeData] of impl.edges) {
6774
+ const weight = config.cost(edgeData.data)
6775
+ if (Number.isNaN(weight) || weight < 0) {
6776
+ throw new GraphError({ message: "Dijkstra's algorithm requires non-negative edge weights" })
6777
+ }
6778
+ edgeWeights.set(edgeIndex, weight)
6779
+ }
4361
6780
 
4362
6781
  // Early return if source equals target
4363
6782
  if (config.source === config.target) {
@@ -4375,26 +6794,17 @@ export const dijkstra: {
4375
6794
 
4376
6795
  // Initialize distances
4377
6796
  // Iterate directly over node keys
4378
- for (const node of graph.nodes.keys()) {
6797
+ for (const node of impl.nodes.keys()) {
4379
6798
  distances.set(node, node === config.source ? 0 : Infinity)
4380
6799
  previous.set(node, null)
4381
6800
  }
4382
6801
 
4383
- // Simple priority queue using array (can be optimized with proper heap)
4384
- const priorityQueue: Array<{ node: NodeIndex; distance: number }> = [
4385
- { node: config.source, distance: 0 }
4386
- ]
6802
+ const priorityQueue: Array<MinHeapEntry> = []
6803
+ let sequence = 0
6804
+ minHeapPush(priorityQueue, { node: config.source, priority: 0, sequence: sequence++ })
4387
6805
 
4388
6806
  while (priorityQueue.length > 0) {
4389
- // Find minimum distance node (priority queue extract-min)
4390
- let minIndex = 0
4391
- for (let i = 1; i < priorityQueue.length; i++) {
4392
- if (priorityQueue[i].distance < priorityQueue[minIndex].distance) {
4393
- minIndex = i
4394
- }
4395
- }
4396
-
4397
- const current = priorityQueue.splice(minIndex, 1)[0]
6807
+ const current = minHeapPop(priorityQueue)!
4398
6808
  const currentNode = current.node
4399
6809
 
4400
6810
  // Skip if already visited (can happen with duplicate entries)
@@ -4413,10 +6823,10 @@ export const dijkstra: {
4413
6823
  const currentDistance = distances.get(currentNode)!
4414
6824
 
4415
6825
  // Examine all outgoing edges
4416
- const adjacencyList = graph.adjacency.get(currentNode)
6826
+ const adjacencyList = impl.adjacency.get(currentNode)
4417
6827
  if (adjacencyList !== undefined) {
4418
6828
  for (const edgeIndex of adjacencyList) {
4419
- const edge = graph.edges.get(edgeIndex)
6829
+ const edge = impl.edges.get(edgeIndex)
4420
6830
  if (edge !== undefined) {
4421
6831
  const neighbor = getTraversableNeighbor(graph, currentNode, edge)
4422
6832
  const cost = edgeWeights.get(edgeIndex)!
@@ -4431,7 +6841,7 @@ export const dijkstra: {
4431
6841
 
4432
6842
  // Add to priority queue if not visited
4433
6843
  if (!visited.has(neighbor)) {
4434
- priorityQueue.push({ node: neighbor, distance: newDistance })
6844
+ minHeapPush(priorityQueue, { node: neighbor, priority: newDistance, sequence: sequence++ })
4435
6845
  }
4436
6846
  }
4437
6847
  }
@@ -4501,8 +6911,9 @@ export interface AllPairsResult<E> {
4501
6911
  * **Details**
4502
6912
  *
4503
6913
  * Computes distances, reconstructed node paths, and edge-data paths for every
4504
- * source and target pair in O(V^3) time. Negative edge weights are allowed, but
4505
- * a `GraphError` is thrown if any negative cycle is detected.
6914
+ * source and target pair in O(V^3) time. Negative edge weights are allowed, and
6915
+ * `Infinity` behaves like an impassable edge. A `GraphError` is thrown if any
6916
+ * edge weight is `NaN` or `-Infinity`, or if any negative cycle is detected.
4506
6917
  *
4507
6918
  * **Example** (Finding all-pairs shortest paths)
4508
6919
  *
@@ -4534,8 +6945,9 @@ export const floydWarshall: {
4534
6945
  * **Details**
4535
6946
  *
4536
6947
  * Computes distances, reconstructed node paths, and edge-data paths for every
4537
- * source and target pair in O(V^3) time. Negative edge weights are allowed, but
4538
- * a `GraphError` is thrown if any negative cycle is detected.
6948
+ * source and target pair in O(V^3) time. Negative edge weights are allowed, and
6949
+ * `Infinity` behaves like an impassable edge. A `GraphError` is thrown if any
6950
+ * edge weight is `NaN` or `-Infinity`, or if any negative cycle is detected.
4539
6951
  *
4540
6952
  * **Example** (Finding all-pairs shortest paths)
4541
6953
  *
@@ -4567,8 +6979,9 @@ export const floydWarshall: {
4567
6979
  * **Details**
4568
6980
  *
4569
6981
  * Computes distances, reconstructed node paths, and edge-data paths for every
4570
- * source and target pair in O(V^3) time. Negative edge weights are allowed, but
4571
- * a `GraphError` is thrown if any negative cycle is detected.
6982
+ * source and target pair in O(V^3) time. Negative edge weights are allowed, and
6983
+ * `Infinity` behaves like an impassable edge. A `GraphError` is thrown if any
6984
+ * edge weight is `NaN` or `-Infinity`, or if any negative cycle is detected.
4572
6985
  *
4573
6986
  * **Example** (Finding all-pairs shortest paths)
4574
6987
  *
@@ -4600,13 +7013,14 @@ export const floydWarshall: {
4600
7013
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4601
7014
  cost: (edgeData: E) => number
4602
7015
  ): AllPairsResult<E> => {
7016
+ const impl = graphImpl(graph)
4603
7017
  // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration)
4604
- const allNodes = Array.from(graph.nodes.keys())
7018
+ const allNodes = Array.from(impl.nodes.keys())
4605
7019
 
4606
7020
  // Initialize distance matrix
4607
7021
  const distances = new Map<NodeIndex, Map<NodeIndex, number>>()
4608
- const next = new Map<NodeIndex, Map<NodeIndex, NodeIndex | null>>()
4609
- const edgeMatrix = new Map<NodeIndex, Map<NodeIndex, E | null>>()
7022
+ const next = new Map<NodeIndex, Map<NodeIndex, NodeIndex>>()
7023
+ const edgeMatrix = new Map<NodeIndex, Map<NodeIndex, E>>()
4610
7024
 
4611
7025
  // Initialize with infinity for all pairs
4612
7026
  for (const i of allNodes) {
@@ -4616,14 +7030,15 @@ export const floydWarshall: {
4616
7030
 
4617
7031
  for (const j of allNodes) {
4618
7032
  distances.get(i)!.set(j, i === j ? 0 : Infinity)
4619
- next.get(i)!.set(j, null)
4620
- edgeMatrix.get(i)!.set(j, null)
4621
7033
  }
4622
7034
  }
4623
7035
 
4624
7036
  // Set edge weights
4625
- for (const [, edgeData] of graph.edges) {
7037
+ for (const [, edgeData] of impl.edges) {
4626
7038
  const weight = cost(edgeData.data)
7039
+ if (Number.isNaN(weight) || weight === -Infinity) {
7040
+ throw new GraphError({ message: "Floyd-Warshall algorithm does not support NaN or -Infinity edge weights" })
7041
+ }
4627
7042
  const i = edgeData.source
4628
7043
  const j = edgeData.target
4629
7044
 
@@ -4654,8 +7069,11 @@ export const floydWarshall: {
4654
7069
  const distIJ = distances.get(i)!.get(j)!
4655
7070
 
4656
7071
  if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) {
4657
- distances.get(i)!.set(j, distIK + distKJ)
4658
- next.get(i)!.set(j, next.get(i)!.get(k)!)
7072
+ const nextIK = next.get(i)!.get(k)
7073
+ if (nextIK !== undefined) {
7074
+ distances.get(i)!.set(j, distIK + distKJ)
7075
+ next.get(i)!.set(j, nextIK)
7076
+ }
4659
7077
  }
4660
7078
  }
4661
7079
  }
@@ -4691,12 +7109,12 @@ export const floydWarshall: {
4691
7109
 
4692
7110
  path.push(current)
4693
7111
  while (current !== j) {
4694
- const nextNode = next.get(current)!.get(j)!
4695
- if (nextNode === null) break
7112
+ const nextNode = next.get(current)!.get(j)
7113
+ if (nextNode === undefined) break
4696
7114
 
4697
- const edgeData = edgeMatrix.get(current)!.get(nextNode)!
4698
- if (edgeData !== null) {
4699
- weights.push(edgeData)
7115
+ const edgeRow = edgeMatrix.get(current)!
7116
+ if (edgeRow.has(nextNode)) {
7117
+ weights.push(edgeRow.get(nextNode) as E)
4700
7118
  }
4701
7119
 
4702
7120
  current = nextNode
@@ -4726,8 +7144,9 @@ export const floydWarshall: {
4726
7144
  *
4727
7145
  * **Details**
4728
7146
  *
4729
- * Specifies the source and target node indices, an edge-cost function, and a
4730
- * heuristic that estimates the remaining cost from a node to the target.
7147
+ * Specifies the source and target node indices, an edge-cost function that maps
7148
+ * edge data to non-negative weights, and a heuristic that estimates the
7149
+ * remaining cost from a node to the target.
4731
7150
  *
4732
7151
  * @see {@link astar} for the algorithm that consumes this configuration
4733
7152
  * @see {@link DijkstraConfig} for shortest paths without a heuristic
@@ -4749,10 +7168,11 @@ export interface AstarConfig<E, N> {
4749
7168
  *
4750
7169
  * **Details**
4751
7170
  *
4752
- * The edge-cost function must return non-negative weights, and the heuristic
7171
+ * The edge-cost function must return non-negative weights and not `NaN`.
7172
+ * `Infinity` is allowed and behaves like an impassable edge. The heuristic
4753
7173
  * should be consistent to preserve shortest-path guarantees. Returns
4754
7174
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
4755
- * when either endpoint is missing or a negative edge cost is encountered.
7175
+ * when either endpoint is missing or an edge cost is negative or `NaN`.
4756
7176
  *
4757
7177
  * **Example** (Finding shortest paths with A-star)
4758
7178
  *
@@ -4796,10 +7216,11 @@ export const astar: {
4796
7216
  *
4797
7217
  * **Details**
4798
7218
  *
4799
- * The edge-cost function must return non-negative weights, and the heuristic
7219
+ * The edge-cost function must return non-negative weights and not `NaN`.
7220
+ * `Infinity` is allowed and behaves like an impassable edge. The heuristic
4800
7221
  * should be consistent to preserve shortest-path guarantees. Returns
4801
7222
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
4802
- * when either endpoint is missing or a negative edge cost is encountered.
7223
+ * when either endpoint is missing or an edge cost is negative or `NaN`.
4803
7224
  *
4804
7225
  * **Example** (Finding shortest paths with A-star)
4805
7226
  *
@@ -4843,10 +7264,11 @@ export const astar: {
4843
7264
  *
4844
7265
  * **Details**
4845
7266
  *
4846
- * The edge-cost function must return non-negative weights, and the heuristic
7267
+ * The edge-cost function must return non-negative weights and not `NaN`.
7268
+ * `Infinity` is allowed and behaves like an impassable edge. The heuristic
4847
7269
  * should be consistent to preserve shortest-path guarantees. Returns
4848
7270
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
4849
- * when either endpoint is missing or a negative edge cost is encountered.
7271
+ * when either endpoint is missing or an edge cost is negative or `NaN`.
4850
7272
  *
4851
7273
  * **Example** (Finding shortest paths with A-star)
4852
7274
  *
@@ -4888,15 +7310,23 @@ export const astar: {
4888
7310
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4889
7311
  config: AstarConfig<E, N>
4890
7312
  ): Option.Option<PathResult<E>> => {
7313
+ const impl = graphImpl(graph)
4891
7314
  // Validate that source and target nodes exist
4892
- if (!graph.nodes.has(config.source)) {
7315
+ if (!impl.nodes.has(config.source)) {
4893
7316
  throw missingNode(config.source)
4894
7317
  }
4895
- if (!graph.nodes.has(config.target)) {
7318
+ if (!impl.nodes.has(config.target)) {
4896
7319
  throw missingNode(config.target)
4897
7320
  }
4898
7321
 
4899
- const edgeWeights = validateNonNegativeEdgeWeights(graph, config.cost, "A* algorithm")
7322
+ const edgeWeights = new Map<EdgeIndex, number>()
7323
+ for (const [edgeIndex, edgeData] of impl.edges) {
7324
+ const weight = config.cost(edgeData.data)
7325
+ if (Number.isNaN(weight) || weight < 0) {
7326
+ throw new GraphError({ message: "A* algorithm requires non-negative edge weights" })
7327
+ }
7328
+ edgeWeights.set(edgeIndex, weight)
7329
+ }
4900
7330
 
4901
7331
  // Early return if source equals target
4902
7332
  if (config.source === config.target) {
@@ -4921,7 +7351,7 @@ export const astar: {
4921
7351
 
4922
7352
  // Initialize scores
4923
7353
  // Iterate directly over node keys
4924
- for (const node of graph.nodes.keys()) {
7354
+ for (const node of impl.nodes.keys()) {
4925
7355
  gScore.set(node, node === config.source ? 0 : Infinity)
4926
7356
  fScore.set(node, Infinity)
4927
7357
  previous.set(node, null)
@@ -4934,21 +7364,16 @@ export const astar: {
4934
7364
  fScore.set(config.source, h)
4935
7365
  }
4936
7366
 
4937
- // Priority queue using f-score (total estimated cost)
4938
- const openSet: Array<{ node: NodeIndex; fScore: number }> = [
4939
- { node: config.source, fScore: fScore.get(config.source)! }
4940
- ]
7367
+ const openSet: Array<MinHeapEntry> = []
7368
+ let sequence = 0
7369
+ minHeapPush(openSet, {
7370
+ node: config.source,
7371
+ priority: fScore.get(config.source)!,
7372
+ sequence: sequence++
7373
+ })
4941
7374
 
4942
7375
  while (openSet.length > 0) {
4943
- // Find node with lowest f-score
4944
- let minIndex = 0
4945
- for (let i = 1; i < openSet.length; i++) {
4946
- if (openSet[i].fScore < openSet[minIndex].fScore) {
4947
- minIndex = i
4948
- }
4949
- }
4950
-
4951
- const current = openSet.splice(minIndex, 1)[0]
7376
+ const current = minHeapPop(openSet)!
4952
7377
  const currentNode = current.node
4953
7378
 
4954
7379
  // Skip if already visited
@@ -4967,10 +7392,10 @@ export const astar: {
4967
7392
  const currentGScore = gScore.get(currentNode)!
4968
7393
 
4969
7394
  // Examine all outgoing edges
4970
- const adjacencyList = graph.adjacency.get(currentNode)
7395
+ const adjacencyList = impl.adjacency.get(currentNode)
4971
7396
  if (adjacencyList !== undefined) {
4972
7397
  for (const edgeIndex of adjacencyList) {
4973
- const edge = graph.edges.get(edgeIndex)
7398
+ const edge = impl.edges.get(edgeIndex)
4974
7399
  if (edge !== undefined) {
4975
7400
  const neighbor = getTraversableNeighbor(graph, currentNode, edge)
4976
7401
  const weight = edgeWeights.get(edgeIndex)!
@@ -4993,7 +7418,7 @@ export const astar: {
4993
7418
 
4994
7419
  // Add to open set if not visited
4995
7420
  if (!visited.has(neighbor)) {
4996
- openSet.push({ node: neighbor, fScore: f })
7421
+ minHeapPush(openSet, { node: neighbor, priority: f, sequence: sequence++ })
4997
7422
  }
4998
7423
  }
4999
7424
  }
@@ -5063,9 +7488,10 @@ export interface BellmanFordConfig<E> {
5063
7488
  *
5064
7489
  * **Details**
5065
7490
  *
5066
- * Negative edge weights are allowed. Returns `Option.none()` when the target is
5067
- * unreachable or when a negative cycle affects the path to the target. Throws a
5068
- * `GraphError` when either endpoint is missing.
7491
+ * Negative edge weights are allowed, and `Infinity` behaves like an impassable
7492
+ * edge. Returns `Option.none()` when the target is unreachable or when a
7493
+ * negative cycle affects the path to the target. Throws a `GraphError` when
7494
+ * either endpoint is missing or an edge weight is `NaN` or `-Infinity`.
5069
7495
  *
5070
7496
  * **Example** (Finding shortest paths with Bellman-Ford)
5071
7497
  *
@@ -5103,9 +7529,10 @@ export const bellmanFord: {
5103
7529
  *
5104
7530
  * **Details**
5105
7531
  *
5106
- * Negative edge weights are allowed. Returns `Option.none()` when the target is
5107
- * unreachable or when a negative cycle affects the path to the target. Throws a
5108
- * `GraphError` when either endpoint is missing.
7532
+ * Negative edge weights are allowed, and `Infinity` behaves like an impassable
7533
+ * edge. Returns `Option.none()` when the target is unreachable or when a
7534
+ * negative cycle affects the path to the target. Throws a `GraphError` when
7535
+ * either endpoint is missing or an edge weight is `NaN` or `-Infinity`.
5109
7536
  *
5110
7537
  * **Example** (Finding shortest paths with Bellman-Ford)
5111
7538
  *
@@ -5143,9 +7570,10 @@ export const bellmanFord: {
5143
7570
  *
5144
7571
  * **Details**
5145
7572
  *
5146
- * Negative edge weights are allowed. Returns `Option.none()` when the target is
5147
- * unreachable or when a negative cycle affects the path to the target. Throws a
5148
- * `GraphError` when either endpoint is missing.
7573
+ * Negative edge weights are allowed, and `Infinity` behaves like an impassable
7574
+ * edge. Returns `Option.none()` when the target is unreachable or when a
7575
+ * negative cycle affects the path to the target. Throws a `GraphError` when
7576
+ * either endpoint is missing or an edge weight is `NaN` or `-Infinity`.
5149
7577
  *
5150
7578
  * **Example** (Finding shortest paths with Bellman-Ford)
5151
7579
  *
@@ -5184,37 +7612,32 @@ export const bellmanFord: {
5184
7612
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
5185
7613
  config: BellmanFordConfig<E>
5186
7614
  ): Option.Option<PathResult<E>> => {
7615
+ const impl = graphImpl(graph)
5187
7616
  // Validate that source and target nodes exist
5188
- if (!graph.nodes.has(config.source)) {
7617
+ if (!impl.nodes.has(config.source)) {
5189
7618
  throw missingNode(config.source)
5190
7619
  }
5191
- if (!graph.nodes.has(config.target)) {
7620
+ if (!impl.nodes.has(config.target)) {
5192
7621
  throw missingNode(config.target)
5193
7622
  }
5194
7623
 
5195
- // Early return if source equals target
5196
- if (config.source === config.target) {
5197
- return Option.some({
5198
- path: [config.source],
5199
- distance: 0,
5200
- costs: []
5201
- })
5202
- }
5203
-
5204
7624
  // Initialize distances and predecessors
5205
7625
  const distances = new Map<NodeIndex, number>()
5206
7626
  const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
5207
7627
 
5208
7628
  // Iterate directly over node keys
5209
- for (const node of graph.nodes.keys()) {
7629
+ for (const node of impl.nodes.keys()) {
5210
7630
  distances.set(node, node === config.source ? 0 : Infinity)
5211
7631
  previous.set(node, null)
5212
7632
  }
5213
7633
 
5214
7634
  // Collect all edges for relaxation
5215
7635
  const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = []
5216
- for (const [, edgeData] of graph.edges) {
7636
+ for (const [, edgeData] of impl.edges) {
5217
7637
  const weight = config.cost(edgeData.data)
7638
+ if (Number.isNaN(weight) || weight === -Infinity) {
7639
+ throw new GraphError({ message: "Bellman-Ford algorithm does not support NaN or -Infinity edge weights" })
7640
+ }
5218
7641
  edges.push({
5219
7642
  source: edgeData.source,
5220
7643
  target: edgeData.target,
@@ -5232,7 +7655,7 @@ export const bellmanFord: {
5232
7655
  }
5233
7656
 
5234
7657
  // Relax edges up to V-1 times
5235
- const nodeCount = graph.nodes.size
7658
+ const nodeCount = impl.nodes.size
5236
7659
  for (let i = 0; i < nodeCount - 1; i++) {
5237
7660
  let hasUpdate = false
5238
7661
 
@@ -5427,7 +7850,7 @@ export class Walker<T, N> implements Iterable<[T, N]> {
5427
7850
  visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
5428
7851
  ) {
5429
7852
  this.visit = visit
5430
- this[Symbol.iterator] = visit((index, data) => [index, data] as [T, N])[Symbol.iterator]
7853
+ this[Symbol.iterator] = () => visit((index, data) => [index, data] as [T, N])[Symbol.iterator]()
5431
7854
  }
5432
7855
  }
5433
7856
 
@@ -5552,7 +7975,8 @@ export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
5552
7975
  *
5553
7976
  * `start` supplies the node indices where traversal begins. If it is omitted,
5554
7977
  * the iterator is empty. `direction` chooses whether traversal follows
5555
- * outgoing or incoming edges.
7978
+ * outgoing edges, incoming edges, or ignores edge direction. `radius` limits
7979
+ * traversal by edge distance from the nearest start node.
5556
7980
  *
5557
7981
  * **Gotchas**
5558
7982
  *
@@ -5568,7 +7992,8 @@ export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
5568
7992
  */
5569
7993
  export interface SearchConfig {
5570
7994
  readonly start?: Array<NodeIndex>
5571
- readonly direction?: Direction
7995
+ readonly direction?: TraversalDirection
7996
+ readonly radius?: number
5572
7997
  }
5573
7998
 
5574
7999
  /**
@@ -5578,7 +8003,8 @@ export interface SearchConfig {
5578
8003
  * **Details**
5579
8004
  *
5580
8005
  * If no start nodes are supplied, the iterator is empty. The `direction` option
5581
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
8006
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8007
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
5582
8008
  * if any configured start node does not exist.
5583
8009
  *
5584
8010
  * **Example** (Traversing depth-first)
@@ -5616,7 +8042,8 @@ export const dfs: {
5616
8042
  * **Details**
5617
8043
  *
5618
8044
  * If no start nodes are supplied, the iterator is empty. The `direction` option
5619
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
8045
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8046
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
5620
8047
  * if any configured start node does not exist.
5621
8048
  *
5622
8049
  * **Example** (Traversing depth-first)
@@ -5654,7 +8081,8 @@ export const dfs: {
5654
8081
  * **Details**
5655
8082
  *
5656
8083
  * If no start nodes are supplied, the iterator is empty. The `direction` option
5657
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
8084
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8085
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
5658
8086
  * if any configured start node does not exist.
5659
8087
  *
5660
8088
  * **Example** (Traversing depth-first)
@@ -5691,6 +8119,7 @@ export const dfs: {
5691
8119
  ): NodeWalker<N> => {
5692
8120
  const start = config.start ?? []
5693
8121
  const direction = config.direction ?? "outgoing"
8122
+ const radius = config.radius ?? Infinity
5694
8123
 
5695
8124
  // Validate that all start nodes exist
5696
8125
  for (const nodeIndex of start) {
@@ -5701,32 +8130,69 @@ export const dfs: {
5701
8130
 
5702
8131
  return new Walker((f) => ({
5703
8132
  [Symbol.iterator]: () => {
5704
- const stack = [...start]
5705
- const discovered = new Set<NodeIndex>()
8133
+ const depths = radius === Infinity ? undefined : new Map<NodeIndex, number>()
8134
+ const stack: Array<readonly [NodeIndex, number]> = []
8135
+ const yielded = new Set<NodeIndex>()
8136
+
8137
+ if (depths === undefined) {
8138
+ for (const node of start) {
8139
+ stack.push([node, 0])
8140
+ }
8141
+ } else {
8142
+ const starts = new Set<NodeIndex>()
8143
+ for (let i = start.length - 1; i >= 0; i--) {
8144
+ const node = start[i]
8145
+ if (!starts.has(node)) {
8146
+ starts.add(node)
8147
+ stack.push([node, 0])
8148
+ depths.set(node, 0)
8149
+ }
8150
+ }
8151
+ stack.reverse()
8152
+ }
5706
8153
 
5707
8154
  const nextMapped = () => {
5708
8155
  while (stack.length > 0) {
5709
- const current = stack.pop()!
8156
+ const [current, depth] = stack.pop()!
5710
8157
 
5711
- if (discovered.has(current)) {
8158
+ if (depths === undefined) {
8159
+ if (yielded.has(current)) {
8160
+ continue
8161
+ }
8162
+ } else if (depths.get(current) !== depth) {
5712
8163
  continue
5713
8164
  }
5714
8165
 
5715
- discovered.add(current)
5716
-
5717
8166
  const nodeDataOption = getNode(graph, current)
5718
8167
  if (Option.isNone(nodeDataOption)) {
5719
8168
  continue
5720
8169
  }
5721
8170
 
5722
- const neighbors = getTraversalNeighbors(graph, current, direction)
5723
- for (let i = neighbors.length - 1; i >= 0; i--) {
5724
- const neighbor = neighbors[i]
5725
- if (!discovered.has(neighbor)) {
5726
- stack.push(neighbor)
8171
+ if (depth < radius) {
8172
+ const neighbors = getTraversalNeighbors(graph, current, direction)
8173
+ for (let i = neighbors.length - 1; i >= 0; i--) {
8174
+ const neighbor = neighbors[i]
8175
+ const nextDepth = depth + 1
8176
+ if (depths === undefined) {
8177
+ if (!yielded.has(neighbor)) {
8178
+ stack.push([neighbor, nextDepth])
8179
+ }
8180
+ continue
8181
+ }
8182
+ const neighborDepth = depths.get(neighbor)
8183
+ if (neighborDepth === undefined || nextDepth < neighborDepth) {
8184
+ depths.set(neighbor, nextDepth)
8185
+ stack.push([neighbor, nextDepth])
8186
+ }
5727
8187
  }
5728
8188
  }
5729
8189
 
8190
+ if (yielded.has(current)) {
8191
+ continue
8192
+ }
8193
+
8194
+ yielded.add(current)
8195
+
5730
8196
  return { done: false, value: f(current, nodeDataOption.value) }
5731
8197
  }
5732
8198
 
@@ -5745,7 +8211,8 @@ export const dfs: {
5745
8211
  * **Details**
5746
8212
  *
5747
8213
  * If no start nodes are supplied, the iterator is empty. The `direction` option
5748
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
8214
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8215
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
5749
8216
  * if any configured start node does not exist.
5750
8217
  *
5751
8218
  * **Example** (Traversing breadth-first)
@@ -5783,7 +8250,8 @@ export const bfs: {
5783
8250
  * **Details**
5784
8251
  *
5785
8252
  * If no start nodes are supplied, the iterator is empty. The `direction` option
5786
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
8253
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8254
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
5787
8255
  * if any configured start node does not exist.
5788
8256
  *
5789
8257
  * **Example** (Traversing breadth-first)
@@ -5821,7 +8289,8 @@ export const bfs: {
5821
8289
  * **Details**
5822
8290
  *
5823
8291
  * If no start nodes are supplied, the iterator is empty. The `direction` option
5824
- * chooses whether to follow outgoing or incoming edges. Throws a `GraphError`
8292
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8293
+ * limits traversal by edge distance from the start nodes. Throws a `GraphError`
5825
8294
  * if any configured start node does not exist.
5826
8295
  *
5827
8296
  * **Example** (Traversing breadth-first)
@@ -5858,6 +8327,7 @@ export const bfs: {
5858
8327
  ): NodeWalker<N> => {
5859
8328
  const start = config.start ?? []
5860
8329
  const direction = config.direction ?? "outgoing"
8330
+ const radius = config.radius ?? Infinity
5861
8331
 
5862
8332
  // Validate that all start nodes exist
5863
8333
  for (const nodeIndex of start) {
@@ -5868,20 +8338,22 @@ export const bfs: {
5868
8338
 
5869
8339
  return new Walker((f) => ({
5870
8340
  [Symbol.iterator]: () => {
5871
- const queue = [...start]
8341
+ const queue: Array<readonly [NodeIndex, number]> = start.map((node) => [node, 0])
5872
8342
  const discovered = new Set<NodeIndex>()
5873
8343
 
5874
8344
  const nextMapped = () => {
5875
8345
  while (queue.length > 0) {
5876
- const current = queue.shift()!
8346
+ const [current, depth] = queue.shift()!
5877
8347
 
5878
8348
  if (!discovered.has(current)) {
5879
8349
  discovered.add(current)
5880
8350
 
5881
- const neighbors = getTraversalNeighbors(graph, current, direction)
5882
- for (const neighbor of neighbors) {
5883
- if (!discovered.has(neighbor)) {
5884
- queue.push(neighbor)
8351
+ if (depth < radius) {
8352
+ const neighbors = getTraversalNeighbors(graph, current, direction)
8353
+ for (const neighbor of neighbors) {
8354
+ if (!discovered.has(neighbor)) {
8355
+ queue.push([neighbor, depth + 1])
8356
+ }
5885
8357
  }
5886
8358
  }
5887
8359
 
@@ -5889,7 +8361,7 @@ export const bfs: {
5889
8361
  if (Option.isSome(nodeData)) {
5890
8362
  return { done: false, value: f(current, nodeData.value) }
5891
8363
  }
5892
- return nextMapped()
8364
+ continue
5893
8365
  }
5894
8366
  }
5895
8367
 
@@ -6020,7 +8492,7 @@ export const topo: {
6020
8492
  * @category iterators
6021
8493
  * @since 3.18.0
6022
8494
  */
6023
- (config?: TopoConfig): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => NodeWalker<N>
8495
+ (config?: TopoConfig): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => NodeWalker<N>
6024
8496
  /**
6025
8497
  * Creates a new topological sort iterator with optional configuration.
6026
8498
  *
@@ -6067,7 +8539,10 @@ export const topo: {
6067
8539
  * @category iterators
6068
8540
  * @since 3.18.0
6069
8541
  */
6070
- <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>, config?: TopoConfig): NodeWalker<N>
8542
+ <N, E>(
8543
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
8544
+ config?: TopoConfig
8545
+ ): NodeWalker<N>
6071
8546
  } = dual((args) => isGraph(args[0]), <N, E, T extends Kind = "directed">(
6072
8547
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
6073
8548
  config: TopoConfig = {}
@@ -6092,19 +8567,20 @@ export const topo: {
6092
8567
 
6093
8568
  return new Walker((f) => ({
6094
8569
  [Symbol.iterator]: () => {
8570
+ const impl = graphImpl(graph)
6095
8571
  const inDegree = new Map<NodeIndex, number>()
6096
8572
  const remaining = new Set<NodeIndex>()
6097
8573
  const initialSet = new Set(initials)
6098
8574
  const queue = [...initials]
6099
8575
 
6100
8576
  // Initialize in-degree counts
6101
- for (const [nodeIndex] of graph.nodes) {
8577
+ for (const [nodeIndex] of impl.nodes) {
6102
8578
  inDegree.set(nodeIndex, 0)
6103
8579
  remaining.add(nodeIndex)
6104
8580
  }
6105
8581
 
6106
8582
  // Calculate in-degrees
6107
- for (const [, edgeData] of graph.edges) {
8583
+ for (const [, edgeData] of impl.edges) {
6108
8584
  const currentInDegree = inDegree.get(edgeData.target) || 0
6109
8585
  inDegree.set(edgeData.target, currentInDegree + 1)
6110
8586
  }
@@ -6152,10 +8628,14 @@ export const topo: {
6152
8628
  if (Option.isSome(nodeData)) {
6153
8629
  return { done: false, value: f(current, nodeData.value) }
6154
8630
  }
6155
- return nextMapped()
8631
+ continue
6156
8632
  }
6157
8633
  }
6158
8634
 
8635
+ if (remaining.size > 0) {
8636
+ throw new GraphError({ message: "Cannot perform topological sort on cyclic graph" })
8637
+ }
8638
+
6159
8639
  return { done: true, value: undefined } as const
6160
8640
  }
6161
8641
 
@@ -6172,7 +8652,14 @@ export const topo: {
6172
8652
  *
6173
8653
  * Nodes are emitted after their reachable descendants have been processed. If
6174
8654
  * no start nodes are supplied, the iterator is empty. The `direction` option
6175
- * chooses whether to follow outgoing or incoming edges.
8655
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8656
+ * limits traversal by edge distance from the start nodes.
8657
+ *
8658
+ * **Gotchas**
8659
+ *
8660
+ * With a finite `radius`, iteration first performs a bounded breadth-first
8661
+ * traversal to determine shortest-distance membership before emitting nodes in
8662
+ * postorder.
6176
8663
  *
6177
8664
  * **Example** (Traversing in postorder)
6178
8665
  *
@@ -6206,7 +8693,14 @@ export const dfsPostOrder: {
6206
8693
  *
6207
8694
  * Nodes are emitted after their reachable descendants have been processed. If
6208
8695
  * no start nodes are supplied, the iterator is empty. The `direction` option
6209
- * chooses whether to follow outgoing or incoming edges.
8696
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8697
+ * limits traversal by edge distance from the start nodes.
8698
+ *
8699
+ * **Gotchas**
8700
+ *
8701
+ * With a finite `radius`, iteration first performs a bounded breadth-first
8702
+ * traversal to determine shortest-distance membership before emitting nodes in
8703
+ * postorder.
6210
8704
  *
6211
8705
  * **Example** (Traversing in postorder)
6212
8706
  *
@@ -6240,7 +8734,14 @@ export const dfsPostOrder: {
6240
8734
  *
6241
8735
  * Nodes are emitted after their reachable descendants have been processed. If
6242
8736
  * no start nodes are supplied, the iterator is empty. The `direction` option
6243
- * chooses whether to follow outgoing or incoming edges.
8737
+ * chooses whether to follow outgoing or incoming edges. The `radius` option
8738
+ * limits traversal by edge distance from the start nodes.
8739
+ *
8740
+ * **Gotchas**
8741
+ *
8742
+ * With a finite `radius`, iteration first performs a bounded breadth-first
8743
+ * traversal to determine shortest-distance membership before emitting nodes in
8744
+ * postorder.
6244
8745
  *
6245
8746
  * **Example** (Traversing in postorder)
6246
8747
  *
@@ -6272,6 +8773,7 @@ export const dfsPostOrder: {
6272
8773
  ): NodeWalker<N> => {
6273
8774
  const start = config.start ?? []
6274
8775
  const direction = config.direction ?? "outgoing"
8776
+ const radius = config.radius ?? Infinity
6275
8777
 
6276
8778
  // Validate that all start nodes exist
6277
8779
  for (const nodeIndex of start) {
@@ -6282,6 +8784,9 @@ export const dfsPostOrder: {
6282
8784
 
6283
8785
  return new Walker((f) => ({
6284
8786
  [Symbol.iterator]: () => {
8787
+ const reached = radius === Infinity
8788
+ ? undefined
8789
+ : new Set(indices(bfs(graph, { start, direction, radius })))
6285
8790
  const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = []
6286
8791
  const discovered = new Set<NodeIndex>()
6287
8792
  const finished = new Set<NodeIndex>()
@@ -6306,7 +8811,11 @@ export const dfsPostOrder: {
6306
8811
 
6307
8812
  for (let i = neighbors.length - 1; i >= 0; i--) {
6308
8813
  const neighbor = neighbors[i]
6309
- if (!discovered.has(neighbor) && !finished.has(neighbor)) {
8814
+ if (
8815
+ (reached === undefined || reached.has(neighbor)) &&
8816
+ !discovered.has(neighbor) &&
8817
+ !finished.has(neighbor)
8818
+ ) {
6310
8819
  stack.push({ node: neighbor, visitedChildren: false })
6311
8820
  }
6312
8821
  }
@@ -6320,7 +8829,7 @@ export const dfsPostOrder: {
6320
8829
  if (Option.isSome(nodeData)) {
6321
8830
  return { done: false, value: f(nodeToEmit, nodeData.value) }
6322
8831
  }
6323
- return nextMapped()
8832
+ continue
6324
8833
  }
6325
8834
  }
6326
8835
  }
@@ -6365,7 +8874,7 @@ export const nodes = <N, E, T extends Kind = "directed">(
6365
8874
  ): NodeWalker<N> =>
6366
8875
  new Walker((f) => ({
6367
8876
  [Symbol.iterator]() {
6368
- const nodeMap = graph.nodes
8877
+ const nodeMap = graphImpl(graph).nodes
6369
8878
  const iterator = nodeMap.entries()
6370
8879
 
6371
8880
  return {
@@ -6414,7 +8923,7 @@ export const edges = <N, E, T extends Kind = "directed">(
6414
8923
  ): EdgeWalker<E> =>
6415
8924
  new Walker((f) => ({
6416
8925
  [Symbol.iterator]() {
6417
- const edgeMap = graph.edges
8926
+ const edgeMap = graphImpl(graph).edges
6418
8927
  const iterator = edgeMap.entries()
6419
8928
 
6420
8929
  return {
@@ -6585,10 +9094,11 @@ export const externals: {
6585
9094
 
6586
9095
  return new Walker((f) => ({
6587
9096
  [Symbol.iterator]: () => {
6588
- const nodeMap = graph.nodes
9097
+ const impl = graphImpl(graph)
9098
+ const nodeMap = impl.nodes
6589
9099
  const adjacencyMap = direction === "incoming"
6590
- ? graph.reverseAdjacency
6591
- : graph.adjacency
9100
+ ? impl.reverseAdjacency
9101
+ : impl.adjacency
6592
9102
 
6593
9103
  const nodeIterator = nodeMap.entries()
6594
9104