@statelyai/graph 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +55 -26
  2. package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-GeL1Cu-L.mjs} +3 -1
  3. package/dist/{algorithms-fTqmvhzP.d.mts → algorithms-CsGNehct.d.mts} +137 -2
  4. package/dist/{algorithms-Ba7o7niK.mjs → algorithms-DF1pSQGv.mjs} +1476 -343
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/{converter-udLITX36.mjs → converter-DyCJJfTe.mjs} +2 -2
  8. package/dist/format-support.mjs +38 -11
  9. package/dist/formats/adjacency-list/index.d.mts +1 -1
  10. package/dist/formats/adjacency-list/index.mjs +1 -1
  11. package/dist/formats/converter/index.d.mts +1 -1
  12. package/dist/formats/converter/index.mjs +1 -1
  13. package/dist/formats/cytoscape/index.d.mts +1 -1
  14. package/dist/formats/cytoscape/index.mjs +3 -1
  15. package/dist/formats/d2/index.d.mts +1 -1
  16. package/dist/formats/d2/index.mjs +26 -12
  17. package/dist/formats/d3/index.d.mts +1 -1
  18. package/dist/formats/d3/index.mjs +3 -1
  19. package/dist/formats/dot/index.d.mts +1 -1
  20. package/dist/formats/dot/index.mjs +22 -6
  21. package/dist/formats/edge-list/index.d.mts +1 -1
  22. package/dist/formats/edge-list/index.mjs +1 -1
  23. package/dist/formats/elk/index.d.mts +1 -1
  24. package/dist/formats/elk/index.mjs +21 -14
  25. package/dist/formats/gexf/index.d.mts +1 -1
  26. package/dist/formats/gexf/index.mjs +22 -15
  27. package/dist/formats/gml/index.d.mts +1 -1
  28. package/dist/formats/gml/index.mjs +21 -12
  29. package/dist/formats/graphml/index.d.mts +1 -1
  30. package/dist/formats/graphml/index.mjs +73 -22
  31. package/dist/formats/jgf/index.d.mts +1 -1
  32. package/dist/formats/jgf/index.mjs +5 -2
  33. package/dist/formats/mermaid/index.d.mts +1 -1
  34. package/dist/formats/mermaid/index.mjs +49 -12
  35. package/dist/formats/tgf/index.d.mts +1 -1
  36. package/dist/formats/tgf/index.mjs +1 -1
  37. package/dist/formats/xyflow/index.d.mts +1 -1
  38. package/dist/formats/xyflow/index.mjs +31 -4
  39. package/dist/{index-D9Kj6Fe3.d.mts → index-D51lJnt2.d.mts} +1 -1
  40. package/dist/{index-CHoriXZD.d.mts → index-DWmo1mIp.d.mts} +77 -18
  41. package/dist/index.d.mts +6 -6
  42. package/dist/index.mjs +143 -295
  43. package/dist/{queries-BlkA1HAN.d.mts → queries-BfXeTXRf.d.mts} +43 -12
  44. package/dist/queries-KirMDR7e.mjs +980 -0
  45. package/dist/queries.d.mts +1 -1
  46. package/dist/queries.mjs +1 -768
  47. package/dist/schemas.d.mts +1 -1
  48. package/dist/schemas.mjs +23 -84
  49. package/dist/{types-3-FS9NV2.d.mts → types-DNYdIU21.d.mts} +54 -5
  50. package/dist/validate-TtH-x3JV.mjs +190 -0
  51. package/package.json +13 -3
  52. package/dist/indexing-DR8M1vBy.mjs +0 -137
  53. /package/dist/{edge-list-DP4otyPU.mjs → edge-list-BcZ0h6zz.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-DR8M1vBy.mjs";
1
+ import { A as indexAddNode, M as indexUpdateEdgeEndpoints, N as invalidateIndex, O as getIndex, P as touchIndex, j as indexReparentNode, k as indexAddEdge, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-KirMDR7e.mjs";
2
2
  import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
3
3
 
4
4
  //#region src/graph.ts
@@ -406,15 +406,46 @@ function deleteEdge(graph, id) {
406
406
  graph.edges = graph.edges.filter((e) => e.id !== id);
407
407
  invalidateIndex(graph);
408
408
  }
409
+ /** Optional fields where `null` in an update unsets the field. */
410
+ const NODE_OPTIONAL_KEYS = [
411
+ "x",
412
+ "y",
413
+ "width",
414
+ "height",
415
+ "shape",
416
+ "color",
417
+ "style"
418
+ ];
419
+ const EDGE_OPTIONAL_KEYS = [
420
+ "weight",
421
+ "mode",
422
+ "x",
423
+ "y",
424
+ "width",
425
+ "height",
426
+ "color",
427
+ "style"
428
+ ];
429
+ /** Apply optional-field updates: `null` unsets, a value sets, `undefined` is ignored. */
430
+ function applyOptionalUpdates(target, update, keys) {
431
+ for (const key of keys) {
432
+ const value = update[key];
433
+ if (value === void 0) continue;
434
+ if (value === null) delete target[key];
435
+ else target[key] = value;
436
+ }
437
+ }
409
438
  /**
410
439
  * **Mutable.** Update a node in place.
440
+ * Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
441
+ * `ports`) accept `null` to unset; `undefined` leaves them unchanged.
411
442
  * @returns The updated node.
412
443
  *
413
444
  * @example
414
445
  * ```ts
415
446
  * const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
416
- * const updated = updateNode(graph, 'a', { label: 'new' });
417
- * // updated.label === 'new'
447
+ * const updated = updateNode(graph, 'a', { label: 'new', x: 100 });
448
+ * // updated.label === 'new', updated.x === 100
418
449
  * ```
419
450
  */
420
451
  function updateNode(graph, id, update) {
@@ -423,24 +454,48 @@ function updateNode(graph, id, update) {
423
454
  if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
424
455
  if (update.parentId !== void 0 && update.parentId !== null) {
425
456
  if (!idx.nodeById.has(update.parentId)) throw new Error(`Parent node "${update.parentId}" does not exist`);
457
+ let ancestorId = update.parentId;
458
+ const seen = /* @__PURE__ */ new Set();
459
+ while (ancestorId !== null && !seen.has(ancestorId)) {
460
+ if (ancestorId === id) throw new Error(`Cannot set parentId of node "${id}" to "${update.parentId}": "${update.parentId}" is "${id}" or one of its descendants, which would create a hierarchy cycle. Reparent "${update.parentId}" elsewhere first.`);
461
+ seen.add(ancestorId);
462
+ const ai = idx.nodeById.get(ancestorId);
463
+ ancestorId = ai !== void 0 ? graph.nodes[ai].parentId ?? null : null;
464
+ }
426
465
  }
427
- if (update.ports !== void 0 && update.ports.length > 0) validatePortNames(update.ports);
466
+ if (update.ports != null && update.ports.length > 0) validatePortNames(update.ports);
428
467
  const node = graph.nodes[arrayIdx];
468
+ if (update.ports !== void 0) {
469
+ const newPortNames = new Set((update.ports ?? []).map((p) => p.name));
470
+ for (const eid of idx.outEdges.get(id) ?? []) {
471
+ const e = graph.edges[idx.edgeById.get(eid)];
472
+ if (e.sourcePort !== void 0 && !newPortNames.has(e.sourcePort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.sourcePort}" via sourcePort. Keep that port, or update/delete the edge first.`);
473
+ }
474
+ for (const eid of idx.inEdges.get(id) ?? []) {
475
+ const e = graph.edges[idx.edgeById.get(eid)];
476
+ if (e.targetPort !== void 0 && !newPortNames.has(e.targetPort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.targetPort}" via targetPort. Keep that port, or update/delete the edge first.`);
477
+ }
478
+ }
429
479
  const oldParentId = node.parentId;
430
480
  const updated = {
431
481
  ...node,
432
482
  ...update.parentId !== void 0 && { parentId: update.parentId ?? null },
433
483
  ...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
434
484
  ...update.label !== void 0 && { label: update.label },
435
- ...update.data !== void 0 && { data: update.data },
436
- ...update.ports !== void 0 && { ports: update.ports.map(createGraphPort) }
485
+ ...update.data !== void 0 && { data: update.data }
437
486
  };
487
+ if (update.ports !== void 0) if (update.ports === null) delete updated.ports;
488
+ else updated.ports = update.ports.map(createGraphPort);
489
+ applyOptionalUpdates(updated, update, NODE_OPTIONAL_KEYS);
438
490
  graph.nodes[arrayIdx] = updated;
439
491
  if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
440
492
  return updated;
441
493
  }
442
494
  /**
443
495
  * **Mutable.** Update an edge in place.
496
+ * Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
497
+ * `width`, `height`, `color`, `style`) accept `null` to unset; `undefined`
498
+ * leaves them unchanged.
444
499
  * @returns The updated edge.
445
500
  *
446
501
  * @example
@@ -449,8 +504,8 @@ function updateNode(graph, id, update) {
449
504
  * nodes: [{ id: 'a' }, { id: 'b' }],
450
505
  * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
451
506
  * });
452
- * const updated = updateEdge(graph, 'e1', { label: 'new' });
453
- * // updated.label === 'new'
507
+ * const updated = updateEdge(graph, 'e1', { label: 'new', weight: 2 });
508
+ * // updated.label === 'new', updated.weight === 2
454
509
  * ```
455
510
  */
456
511
  function updateEdge(graph, id, update) {
@@ -464,22 +519,28 @@ function updateEdge(graph, id, update) {
464
519
  const oldTargetId = edge.targetId;
465
520
  const effectiveSourceId = update.sourceId ?? edge.sourceId;
466
521
  const effectiveTargetId = update.targetId ?? edge.targetId;
467
- if (update.sourcePort !== void 0) {
468
- if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === update.sourcePort)) throw new Error(`Port "${update.sourcePort}" does not exist on source node "${effectiveSourceId}"`);
522
+ const effectiveSourcePort = update.sourcePort !== void 0 ? update.sourcePort ?? void 0 : edge.sourcePort;
523
+ const effectiveTargetPort = update.targetPort !== void 0 ? update.targetPort ?? void 0 : edge.targetPort;
524
+ if (effectiveSourcePort !== void 0) {
525
+ if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === effectiveSourcePort)) throw new Error(update.sourcePort !== void 0 ? `Port "${effectiveSourcePort}" does not exist on source node "${effectiveSourceId}"` : `Cannot update edge "${id}": its sourcePort "${effectiveSourcePort}" does not exist on the new source node "${effectiveSourceId}". Include sourcePort in the update (a port on "${effectiveSourceId}", or null to clear it).`);
469
526
  }
470
- if (update.targetPort !== void 0) {
471
- if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === update.targetPort)) throw new Error(`Port "${update.targetPort}" does not exist on target node "${effectiveTargetId}"`);
527
+ if (effectiveTargetPort !== void 0) {
528
+ if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === effectiveTargetPort)) throw new Error(update.targetPort !== void 0 ? `Port "${effectiveTargetPort}" does not exist on target node "${effectiveTargetId}"` : `Cannot update edge "${id}": its targetPort "${effectiveTargetPort}" does not exist on the new target node "${effectiveTargetId}". Include targetPort in the update (a port on "${effectiveTargetId}", or null to clear it).`);
472
529
  }
473
530
  const updated = {
474
531
  ...edge,
475
532
  ...update.sourceId !== void 0 && { sourceId: update.sourceId },
476
533
  ...update.targetId !== void 0 && { targetId: update.targetId },
477
534
  ...update.label !== void 0 && { label: update.label },
478
- ...update.data !== void 0 && { data: update.data },
479
- ...update.sourcePort !== void 0 && { sourcePort: update.sourcePort },
480
- ...update.targetPort !== void 0 && { targetPort: update.targetPort }
535
+ ...update.data !== void 0 && { data: update.data }
481
536
  };
537
+ if (update.sourcePort !== void 0) if (update.sourcePort === null) delete updated.sourcePort;
538
+ else updated.sourcePort = update.sourcePort;
539
+ if (update.targetPort !== void 0) if (update.targetPort === null) delete updated.targetPort;
540
+ else updated.targetPort = update.targetPort;
541
+ applyOptionalUpdates(updated, update, EDGE_OPTIONAL_KEYS);
482
542
  graph.edges[arrayIdx] = updated;
543
+ if (update.mode !== void 0 || update.weight !== void 0) touchIndex(idx);
483
544
  if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
484
545
  return updated;
485
546
  }
@@ -666,6 +727,9 @@ var MinPriorityQueue = class {
666
727
  this.items.push(item);
667
728
  this.bubbleUp(this.items.length - 1);
668
729
  }
730
+ peek() {
731
+ return this.items[0];
732
+ }
669
733
  pop() {
670
734
  if (this.items.length === 0) return void 0;
671
735
  const first = this.items[0];
@@ -699,7 +763,24 @@ var MinPriorityQueue = class {
699
763
  }
700
764
  }
701
765
  };
702
- function getNeighborIds$1(graph, nodeId) {
766
+ /**
767
+ * Classify a graph by the *effective* mode of its edges (per-edge `mode`
768
+ * overrides included): all-directed, all-non-directed, or genuinely mixed.
769
+ * Edge-less graphs fall back to `graph.mode`.
770
+ */
771
+ function getEffectiveModeKind(graph) {
772
+ let sawDirected = false;
773
+ let sawNonDirected = false;
774
+ for (const edge of graph.edges) {
775
+ if (getEdgeMode(graph, edge) === "directed") sawDirected = true;
776
+ else sawNonDirected = true;
777
+ if (sawDirected && sawNonDirected) return "mixed";
778
+ }
779
+ if (sawDirected) return "directed";
780
+ if (sawNonDirected) return "non-directed";
781
+ return graph.mode === "directed" ? "directed" : "non-directed";
782
+ }
783
+ function getNeighborIds(graph, nodeId) {
703
784
  const idx = getIndex(graph);
704
785
  const ids = [];
705
786
  for (const eid of idx.outEdges.get(nodeId) ?? []) {
@@ -779,67 +860,181 @@ function getNeighborEdgesAll(graph, nodeId) {
779
860
  return result;
780
861
  }
781
862
 
863
+ //#endregion
864
+ //#region src/algorithms/csr.ts
865
+ const csrCache = /* @__PURE__ */ new WeakMap();
866
+ /** Get or lazily (re)build the CSR snapshot for a graph. */
867
+ function getCSR(graph) {
868
+ const idx = getIndex(graph);
869
+ const cached = csrCache.get(idx);
870
+ if (cached && cached.version === idx.version && cached.mode === graph.mode) return cached.csr;
871
+ const csr = buildCSR(graph);
872
+ csrCache.set(idx, {
873
+ version: idx.version,
874
+ mode: graph.mode,
875
+ csr
876
+ });
877
+ return csr;
878
+ }
879
+ function buildCSR(graph) {
880
+ const n = graph.nodes.length;
881
+ const m = graph.edges.length;
882
+ const ids = new Array(n);
883
+ const indexOf = /* @__PURE__ */ new Map();
884
+ for (let i = 0; i < n; i++) {
885
+ ids[i] = graph.nodes[i].id;
886
+ indexOf.set(ids[i], i);
887
+ }
888
+ const srcPos = new Int32Array(m);
889
+ const tgtPos = new Int32Array(m);
890
+ const nonDirected = new Uint8Array(m);
891
+ const outCounts = new Int32Array(n);
892
+ const inCounts = new Int32Array(n);
893
+ let firstNegativeEdge = -1;
894
+ for (let e = 0; e < m; e++) {
895
+ const edge = graph.edges[e];
896
+ if (firstNegativeEdge === -1 && (edge.weight ?? 1) < 0) firstNegativeEdge = e;
897
+ const s = indexOf.get(edge.sourceId);
898
+ const t = indexOf.get(edge.targetId);
899
+ if (s === void 0 || t === void 0) {
900
+ srcPos[e] = -1;
901
+ tgtPos[e] = -1;
902
+ continue;
903
+ }
904
+ srcPos[e] = s;
905
+ tgtPos[e] = t;
906
+ const nd = getEdgeMode(graph, edge) !== "directed" ? 1 : 0;
907
+ nonDirected[e] = nd;
908
+ outCounts[s]++;
909
+ inCounts[t]++;
910
+ if (nd) {
911
+ outCounts[t]++;
912
+ inCounts[s]++;
913
+ }
914
+ }
915
+ const outOffsets = new Int32Array(n + 1);
916
+ const inOffsets = new Int32Array(n + 1);
917
+ for (let i = 0; i < n; i++) {
918
+ outOffsets[i + 1] = outOffsets[i] + outCounts[i];
919
+ inOffsets[i + 1] = inOffsets[i] + inCounts[i];
920
+ }
921
+ const outTargets = new Int32Array(outOffsets[n]);
922
+ const outEdgeIndex = new Int32Array(outOffsets[n]);
923
+ const inOrigins = new Int32Array(inOffsets[n]);
924
+ const inEdgeIndex = new Int32Array(inOffsets[n]);
925
+ const outCursor = outOffsets.slice(0, n);
926
+ const inCursor = inOffsets.slice(0, n);
927
+ for (let e = 0; e < m; e++) {
928
+ const s = srcPos[e];
929
+ const t = tgtPos[e];
930
+ if (s < 0) continue;
931
+ outTargets[outCursor[s]] = t;
932
+ outEdgeIndex[outCursor[s]++] = e;
933
+ inOrigins[inCursor[t]] = s;
934
+ inEdgeIndex[inCursor[t]++] = e;
935
+ if (nonDirected[e]) {
936
+ outTargets[outCursor[t]] = s;
937
+ outEdgeIndex[outCursor[t]++] = e;
938
+ inOrigins[inCursor[s]] = t;
939
+ inEdgeIndex[inCursor[s]++] = e;
940
+ }
941
+ }
942
+ return {
943
+ ids,
944
+ indexOf,
945
+ outOffsets,
946
+ outTargets,
947
+ outEdgeIndex,
948
+ inOffsets,
949
+ inOrigins,
950
+ inEdgeIndex,
951
+ firstNegativeEdge
952
+ };
953
+ }
954
+
782
955
  //#endregion
783
956
  //#region src/algorithms/paths.ts
784
- function computeShortestDistances(graph, sourceId, getWeight, algorithm) {
957
+ function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
785
958
  if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
786
959
  const dist = /* @__PURE__ */ new Map();
787
960
  const prev = /* @__PURE__ */ new Map();
788
961
  dist.set(sourceId, 0);
789
962
  prev.set(sourceId, []);
963
+ const csr = getCSR(graph);
964
+ const source = csr.indexOf.get(sourceId);
965
+ if (source === void 0) return {
966
+ dist,
967
+ prev
968
+ };
969
+ const n = csr.ids.length;
970
+ const distArr = new Float64Array(n).fill(Infinity);
971
+ const prevArr = new Array(n);
972
+ distArr[source] = 0;
973
+ prevArr[source] = [];
974
+ const stopAt = stopAtId !== void 0 ? csr.indexOf.get(stopAtId) : void 0;
975
+ let stopDistance = Infinity;
976
+ if (stopAt !== void 0) assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
790
977
  if (!getWeight && !graph.edges.some((edge) => edge.weight !== void 0)) {
791
- const queue = [sourceId];
792
- while (queue.length > 0) {
793
- const id = queue.shift();
794
- const distance = dist.get(id);
795
- for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
796
- const nextDistance = distance + 1;
797
- const existing = dist.get(neighborId);
798
- if (existing === void 0) {
799
- dist.set(neighborId, nextDistance);
800
- prev.set(neighborId, [{
801
- from: id,
802
- edge
803
- }]);
804
- queue.push(neighborId);
805
- } else if (existing === nextDistance) prev.get(neighborId).push({
806
- from: id,
807
- edge
808
- });
978
+ const queue = new Int32Array(n);
979
+ queue[0] = source;
980
+ let head = 0;
981
+ let tail = 1;
982
+ while (head < tail) {
983
+ const u = queue[head++];
984
+ if (distArr[u] > stopDistance) break;
985
+ if (u === stopAt) stopDistance = distArr[u];
986
+ const nextDistance = distArr[u] + 1;
987
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
988
+ const v = csr.outTargets[a];
989
+ if (distArr[v] === Infinity) {
990
+ distArr[v] = nextDistance;
991
+ prevArr[v] = [u, csr.outEdgeIndex[a]];
992
+ queue[tail++] = v;
993
+ } else if (distArr[v] === nextDistance) prevArr[v].push(u, csr.outEdgeIndex[a]);
809
994
  }
810
995
  }
811
996
  } else {
812
997
  const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
813
- const visited = /* @__PURE__ */ new Set();
998
+ const visited = new Uint8Array(n);
814
999
  const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
815
1000
  pq.push({
816
- id: sourceId,
1001
+ pos: source,
817
1002
  dist: 0
818
1003
  });
819
1004
  while (pq.size > 0) {
820
- const { id, dist: distance } = pq.pop();
821
- if (visited.has(id) || distance !== dist.get(id)) continue;
822
- visited.add(id);
823
- for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
824
- const nextDistance = distance + effectiveWeight(edge);
825
- const existing = dist.get(neighborId);
826
- if (existing === void 0 || nextDistance < existing) {
827
- dist.set(neighborId, nextDistance);
828
- prev.set(neighborId, [{
829
- from: id,
830
- edge
831
- }]);
1005
+ const { pos: u, dist: distance } = pq.pop();
1006
+ if (visited[u] || distance !== distArr[u]) continue;
1007
+ if (distance > stopDistance) break;
1008
+ if (u === stopAt) stopDistance = distance;
1009
+ visited[u] = 1;
1010
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1011
+ const edge = graph.edges[csr.outEdgeIndex[a]];
1012
+ const weight = effectiveWeight(edge);
1013
+ if (weight < 0) throw new Error(`Negative edge weight ${weight} on edge "${edge.sourceId}->${edge.targetId}" (id "${edge.id}"): Dijkstra requires non-negative weights. Use { algorithm: 'bellman-ford' } instead.`);
1014
+ const v = csr.outTargets[a];
1015
+ const nextDistance = distance + weight;
1016
+ if (nextDistance < distArr[v]) {
1017
+ distArr[v] = nextDistance;
1018
+ prevArr[v] = [u, csr.outEdgeIndex[a]];
832
1019
  pq.push({
833
- id: neighborId,
1020
+ pos: v,
834
1021
  dist: nextDistance
835
1022
  });
836
- } else if (existing === nextDistance) prev.get(neighborId).push({
837
- from: id,
838
- edge
839
- });
1023
+ } else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
840
1024
  }
841
1025
  }
842
1026
  }
1027
+ for (let i = 0; i < n; i++) {
1028
+ if (distArr[i] === Infinity || distArr[i] > stopDistance) continue;
1029
+ dist.set(csr.ids[i], distArr[i]);
1030
+ const pairs = prevArr[i];
1031
+ const predecessors = [];
1032
+ for (let k = 0; k < pairs.length; k += 2) predecessors.push({
1033
+ from: csr.ids[pairs[k]],
1034
+ edge: graph.edges[pairs[k + 1]]
1035
+ });
1036
+ prev.set(csr.ids[i], predecessors);
1037
+ }
843
1038
  return {
844
1039
  dist,
845
1040
  prev
@@ -905,7 +1100,7 @@ function bellmanFord(graph, sourceId, getWeight) {
905
1100
  prev
906
1101
  };
907
1102
  }
908
- function* reconstructPaths(graph, prev, sourceNode, targetId) {
1103
+ function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PURE__ */ new Set()) {
909
1104
  if (targetId === sourceNode.id) {
910
1105
  yield {
911
1106
  source: sourceNode,
@@ -917,18 +1112,23 @@ function* reconstructPaths(graph, prev, sourceNode, targetId) {
917
1112
  if (!predecessors || predecessors.length === 0) return;
918
1113
  const targetNi = getIndex(graph).nodeById.get(targetId);
919
1114
  const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((node) => node.id === targetId);
920
- for (const { from, edge } of predecessors) for (const prefix of reconstructPaths(graph, prev, sourceNode, from)) yield {
921
- source: sourceNode,
922
- steps: [...prefix.steps, {
923
- edge,
924
- node: targetNode
925
- }]
926
- };
1115
+ onPath.add(targetId);
1116
+ for (const { from, edge } of predecessors) {
1117
+ if (onPath.has(from)) continue;
1118
+ for (const prefix of reconstructPaths(graph, prev, sourceNode, from, onPath)) yield {
1119
+ source: sourceNode,
1120
+ steps: [...prefix.steps, {
1121
+ edge,
1122
+ node: targetNode
1123
+ }]
1124
+ };
1125
+ }
1126
+ onPath.delete(targetId);
927
1127
  }
928
1128
  function* genShortestPaths(graph, opts) {
929
1129
  const idx = getIndex(graph);
930
1130
  const sourceId = resolveFrom(graph, opts);
931
- const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm);
1131
+ const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
932
1132
  const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
933
1133
  const sourceNi = idx.nodeById.get(sourceId);
934
1134
  const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
@@ -938,8 +1138,161 @@ function getShortestPaths(graph, opts) {
938
1138
  return [...genShortestPaths(graph, opts)];
939
1139
  }
940
1140
  function getShortestPath(graph, opts) {
1141
+ if (opts.algorithm !== "bellman-ford") return bidirectionalShortestPath(graph, resolveFrom(graph, opts), opts.to, opts.getWeight);
941
1142
  for (const path of genShortestPaths(graph, opts)) return path;
942
1143
  }
1144
+ /**
1145
+ * Sublinear searches (early-exit, bidirectional) may legitimately terminate
1146
+ * without ever scanning a negative edge, so the throw-on-negative contract
1147
+ * must be enforced up front: O(1) via the CSR's cached flag for the default
1148
+ * weight, or one O(edges) sweep for a custom `getWeight`.
1149
+ */
1150
+ function assertNoNegativeWeights(graph, csr, getWeight, algorithmName, remedy) {
1151
+ let offending;
1152
+ let weight = 0;
1153
+ if (getWeight === void 0) {
1154
+ if (csr.firstNegativeEdge !== -1) {
1155
+ offending = graph.edges[csr.firstNegativeEdge];
1156
+ weight = offending.weight ?? 1;
1157
+ }
1158
+ } else for (const edge of graph.edges) {
1159
+ const w = getWeight(edge);
1160
+ if (w < 0) {
1161
+ offending = edge;
1162
+ weight = w;
1163
+ break;
1164
+ }
1165
+ }
1166
+ if (offending) throw new Error(`Negative edge weight ${weight} on edge "${offending.sourceId}->${offending.targetId}" (id "${offending.id}"): ${algorithmName} requires non-negative weights. ${remedy}`);
1167
+ }
1168
+ /**
1169
+ * Bidirectional Dijkstra for a single source→target query. Forward search
1170
+ * runs on the traversable arcs, backward search on the reverse arcs; `mu`
1171
+ * tracks the best meeting cost and the search stops when the two frontiers
1172
+ * prove no better meeting exists (Pohl's `topF + topB >= mu` condition).
1173
+ * Returns one shortest path (ties broken arbitrarily, as before).
1174
+ */
1175
+ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
1176
+ const csr = getCSR(graph);
1177
+ const source = csr.indexOf.get(sourceId);
1178
+ const target = csr.indexOf.get(targetId);
1179
+ if (source === void 0 || target === void 0) return void 0;
1180
+ const sourceNode = graph.nodes[source];
1181
+ if (source === target) return {
1182
+ source: sourceNode,
1183
+ steps: []
1184
+ };
1185
+ assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
1186
+ const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
1187
+ const n = csr.ids.length;
1188
+ const distF = new Float64Array(n).fill(Infinity);
1189
+ const distB = new Float64Array(n).fill(Infinity);
1190
+ const predF = new Int32Array(n).fill(-1);
1191
+ const predFEdge = new Int32Array(n).fill(-1);
1192
+ const predB = new Int32Array(n).fill(-1);
1193
+ const predBEdge = new Int32Array(n).fill(-1);
1194
+ const settledF = new Uint8Array(n);
1195
+ const settledB = new Uint8Array(n);
1196
+ const compare = (a, b) => a.dist - b.dist;
1197
+ const pqF = new MinPriorityQueue(compare);
1198
+ const pqB = new MinPriorityQueue(compare);
1199
+ distF[source] = 0;
1200
+ distB[target] = 0;
1201
+ pqF.push({
1202
+ pos: source,
1203
+ dist: 0
1204
+ });
1205
+ pqB.push({
1206
+ pos: target,
1207
+ dist: 0
1208
+ });
1209
+ let mu = Infinity;
1210
+ let meet = -1;
1211
+ /** Discard stale/settled heap entries; return the next valid key. */
1212
+ const validTop = (pq, dist, settled) => {
1213
+ for (;;) {
1214
+ const top = pq.peek();
1215
+ if (top === void 0) return void 0;
1216
+ if (settled[top.pos] || top.dist !== dist[top.pos]) {
1217
+ pq.pop();
1218
+ continue;
1219
+ }
1220
+ return top.dist;
1221
+ }
1222
+ };
1223
+ const scanForward = () => {
1224
+ const { pos: u, dist: d } = pqF.pop();
1225
+ settledF[u] = 1;
1226
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1227
+ const edge = graph.edges[csr.outEdgeIndex[a]];
1228
+ const weight = effectiveWeight(edge);
1229
+ const v = csr.outTargets[a];
1230
+ const next = d + weight;
1231
+ if (next < distF[v]) {
1232
+ distF[v] = next;
1233
+ predF[v] = u;
1234
+ predFEdge[v] = csr.outEdgeIndex[a];
1235
+ pqF.push({
1236
+ pos: v,
1237
+ dist: next
1238
+ });
1239
+ }
1240
+ if (distB[v] !== Infinity && next + distB[v] < mu) {
1241
+ mu = next + distB[v];
1242
+ meet = v;
1243
+ }
1244
+ }
1245
+ };
1246
+ const scanBackward = () => {
1247
+ const { pos: u, dist: d } = pqB.pop();
1248
+ settledB[u] = 1;
1249
+ for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
1250
+ const edge = graph.edges[csr.inEdgeIndex[a]];
1251
+ const weight = effectiveWeight(edge);
1252
+ const v = csr.inOrigins[a];
1253
+ const next = d + weight;
1254
+ if (next < distB[v]) {
1255
+ distB[v] = next;
1256
+ predB[v] = u;
1257
+ predBEdge[v] = csr.inEdgeIndex[a];
1258
+ pqB.push({
1259
+ pos: v,
1260
+ dist: next
1261
+ });
1262
+ }
1263
+ if (distF[v] !== Infinity && next + distF[v] < mu) {
1264
+ mu = next + distF[v];
1265
+ meet = v;
1266
+ }
1267
+ }
1268
+ };
1269
+ for (;;) {
1270
+ const topF = validTop(pqF, distF, settledF);
1271
+ const topB = validTop(pqB, distB, settledB);
1272
+ if (topF === void 0 || topB === void 0) break;
1273
+ if (topF + topB >= mu) break;
1274
+ if (topF <= topB) scanForward();
1275
+ else scanBackward();
1276
+ }
1277
+ if (meet === -1) return void 0;
1278
+ const steps = [];
1279
+ for (let v = meet; v !== source; v = predF[v]) steps.unshift({
1280
+ edge: graph.edges[predFEdge[v]],
1281
+ node: graph.nodes[v]
1282
+ });
1283
+ for (let v = meet; v !== target;) {
1284
+ const nextNode = predB[v];
1285
+ steps.push({
1286
+ edge: graph.edges[predBEdge[v]],
1287
+ node: graph.nodes[nextNode]
1288
+ });
1289
+ v = nextNode;
1290
+ }
1291
+ return {
1292
+ source: sourceNode,
1293
+ steps
1294
+ };
1295
+ }
943
1296
  function getSimplePaths(graph, opts) {
944
1297
  return [...genSimplePaths(graph, opts)];
945
1298
  }
@@ -997,15 +1350,10 @@ function getStronglyConnectedComponents(graph) {
997
1350
  indexCounter++;
998
1351
  stack.push(id);
999
1352
  onStack.add(id);
1000
- for (const eid of idx.outEdges.get(id) ?? []) {
1001
- const ai = idx.edgeById.get(eid);
1002
- if (ai === void 0) continue;
1003
- const neighborId = graph.edges[ai].targetId;
1004
- if (!nodeIndex.has(neighborId)) {
1005
- strongconnect(neighborId);
1006
- lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
1007
- } else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
1008
- }
1353
+ for (const neighborId of getNeighborIds(graph, id)) if (!nodeIndex.has(neighborId)) {
1354
+ strongconnect(neighborId);
1355
+ lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
1356
+ } else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
1009
1357
  if (lowlink.get(id) === nodeIndex.get(id)) {
1010
1358
  const component = [];
1011
1359
  let neighborId;
@@ -1025,7 +1373,9 @@ function getCycles(graph) {
1025
1373
  return [...genCycles(graph)];
1026
1374
  }
1027
1375
  function* genCycles(graph) {
1028
- if (graph.mode !== "directed") yield* genCyclesUndirected(graph);
1376
+ const kind = getEffectiveModeKind(graph);
1377
+ if (kind === "mixed") yield* genCyclesMixed(graph);
1378
+ else if (kind === "non-directed") yield* genCyclesUndirected(graph);
1029
1379
  else yield* genCyclesDirected(graph);
1030
1380
  }
1031
1381
  function* genCyclesDirected(graph) {
@@ -1081,17 +1431,14 @@ function* genCyclesUndirected(graph) {
1081
1431
  const startNi = idx.nodeById.get(startId);
1082
1432
  const startNode = graph.nodes[startNi];
1083
1433
  const found = [];
1084
- function dfsFind(currentId, parentId) {
1434
+ function dfsFind(currentId, arrivalEdgeId) {
1085
1435
  visited.add(currentId);
1086
1436
  for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
1087
- if (neighborId === parentId) {
1088
- parentId = null;
1089
- continue;
1090
- }
1091
- if (neighborId === startId && steps.length >= 2) {
1092
- const innerIds = steps.map((step) => step.node.id).sort().join(",");
1093
- if (!seen.has(innerIds)) {
1094
- seen.add(innerIds);
1437
+ if (edge.id === arrivalEdgeId) continue;
1438
+ if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
1439
+ const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
1440
+ if (!seen.has(cycleEdgeIds)) {
1441
+ seen.add(cycleEdgeIds);
1095
1442
  found.push({
1096
1443
  source: startNode,
1097
1444
  steps: [...steps, {
@@ -1106,7 +1453,7 @@ function* genCyclesUndirected(graph) {
1106
1453
  edge,
1107
1454
  node: graph.nodes[ni]
1108
1455
  });
1109
- dfsFind(neighborId, currentId);
1456
+ dfsFind(neighborId, edge.id);
1110
1457
  steps.pop();
1111
1458
  }
1112
1459
  }
@@ -1116,6 +1463,59 @@ function* genCyclesUndirected(graph) {
1116
1463
  yield* found;
1117
1464
  }
1118
1465
  }
1466
+ /**
1467
+ * Exact simple-cycle enumeration for graphs mixing directed and non-directed
1468
+ * edges. Traverses directed edges source→target only and non-directed edges
1469
+ * both ways; a cycle may use each edge at most once, visits distinct nodes,
1470
+ * and is identified by its set of traversed edge ids.
1471
+ */
1472
+ function* genCyclesMixed(graph) {
1473
+ const idx = getIndex(graph);
1474
+ const sortedIds = graph.nodes.map((node) => node.id).sort();
1475
+ const seen = /* @__PURE__ */ new Set();
1476
+ for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
1477
+ const startId = sortedIds[startIndex];
1478
+ const allowed = new Set(sortedIds.slice(startIndex));
1479
+ const visited = /* @__PURE__ */ new Set();
1480
+ const steps = [];
1481
+ const pathEdgeIds = /* @__PURE__ */ new Set();
1482
+ const startNi = idx.nodeById.get(startId);
1483
+ const startNode = graph.nodes[startNi];
1484
+ const found = [];
1485
+ function dfsFind(currentId) {
1486
+ visited.add(currentId);
1487
+ for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
1488
+ if (pathEdgeIds.has(edge.id)) continue;
1489
+ if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
1490
+ const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
1491
+ if (!seen.has(cycleEdgeIds)) {
1492
+ seen.add(cycleEdgeIds);
1493
+ found.push({
1494
+ source: startNode,
1495
+ steps: [...steps, {
1496
+ edge,
1497
+ node: startNode
1498
+ }]
1499
+ });
1500
+ }
1501
+ } else if (allowed.has(neighborId) && !visited.has(neighborId)) {
1502
+ const ni = idx.nodeById.get(neighborId);
1503
+ steps.push({
1504
+ edge,
1505
+ node: graph.nodes[ni]
1506
+ });
1507
+ pathEdgeIds.add(edge.id);
1508
+ dfsFind(neighborId);
1509
+ pathEdgeIds.delete(edge.id);
1510
+ steps.pop();
1511
+ }
1512
+ }
1513
+ visited.delete(currentId);
1514
+ }
1515
+ dfsFind(startId);
1516
+ yield* found;
1517
+ }
1518
+ }
1119
1519
  function getAllPairsShortestPaths(graph, opts) {
1120
1520
  const algorithm = opts?.algorithm ?? "dijkstra";
1121
1521
  if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
@@ -1187,6 +1587,7 @@ function floydWarshallAllPaths(graph, getWeight) {
1187
1587
  for (const entry of prev[k][j]) if (!prev[i][j].some((existing) => existing.edge.id === entry.edge.id)) prev[i][j].push({ ...entry });
1188
1588
  }
1189
1589
  }
1590
+ for (let i = 0; i < nodeCount; i++) if (dist[i][i] < 0) throw new Error(`Negative cycle detected through node "${nodeIds[i]}": all-pairs shortest paths are undefined. Remove the negative cycle, or use getShortestPaths with { algorithm: 'bellman-ford' } per source to locate it.`);
1190
1591
  const results = [];
1191
1592
  for (let i = 0; i < nodeCount; i++) {
1192
1593
  const sourceNi = idx.nodeById.get(nodeIds[i]);
@@ -1233,48 +1634,53 @@ function getAStarPath(graph, opts) {
1233
1634
  source: graph.nodes[sourceNi],
1234
1635
  steps: []
1235
1636
  };
1236
- const gScore = /* @__PURE__ */ new Map();
1237
- const cameFrom = /* @__PURE__ */ new Map();
1238
- const closedSet = /* @__PURE__ */ new Set();
1637
+ const csr = getCSR(graph);
1638
+ const n = csr.ids.length;
1639
+ const source = csr.indexOf.get(sourceId);
1640
+ const target = csr.indexOf.get(targetId);
1641
+ const gScore = new Float64Array(n).fill(Infinity);
1642
+ const cameFromPos = new Int32Array(n).fill(-1);
1643
+ const cameFromEdge = new Int32Array(n).fill(-1);
1644
+ const closed = new Uint8Array(n);
1239
1645
  const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
1240
- gScore.set(sourceId, 0);
1646
+ assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
1647
+ gScore[source] = 0;
1241
1648
  openSet.push({
1242
- id: sourceId,
1649
+ pos: source,
1243
1650
  f: heuristic(sourceId)
1244
1651
  });
1245
1652
  while (openSet.size > 0) {
1246
- const { id: currentId } = openSet.pop();
1247
- if (closedSet.has(currentId)) continue;
1248
- if (currentId === targetId) {
1653
+ const { pos: current } = openSet.pop();
1654
+ if (closed[current]) continue;
1655
+ if (current === target) {
1249
1656
  const steps = [];
1250
- let current = targetId;
1251
- while (current !== sourceId) {
1252
- const previous = cameFrom.get(current);
1253
- const ni = idx.nodeById.get(current);
1657
+ let cursor = target;
1658
+ while (cursor !== source) {
1254
1659
  steps.unshift({
1255
- edge: previous.edge,
1256
- node: graph.nodes[ni]
1660
+ edge: graph.edges[cameFromEdge[cursor]],
1661
+ node: graph.nodes[cursor]
1257
1662
  });
1258
- current = previous.from;
1663
+ cursor = cameFromPos[cursor];
1259
1664
  }
1260
1665
  return {
1261
1666
  source: graph.nodes[sourceNi],
1262
1667
  steps
1263
1668
  };
1264
1669
  }
1265
- closedSet.add(currentId);
1266
- for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
1267
- if (closedSet.has(neighborId)) continue;
1268
- const tentativeScore = (gScore.get(currentId) ?? Infinity) + getWeight(edge);
1269
- if (tentativeScore < (gScore.get(neighborId) ?? Infinity)) {
1270
- cameFrom.set(neighborId, {
1271
- from: currentId,
1272
- edge
1273
- });
1274
- gScore.set(neighborId, tentativeScore);
1670
+ closed[current] = 1;
1671
+ for (let a = csr.outOffsets[current]; a < csr.outOffsets[current + 1]; a++) {
1672
+ const edge = graph.edges[csr.outEdgeIndex[a]];
1673
+ const weight = getWeight(edge);
1674
+ const neighbor = csr.outTargets[a];
1675
+ if (closed[neighbor]) continue;
1676
+ const tentativeScore = gScore[current] + weight;
1677
+ if (tentativeScore < gScore[neighbor]) {
1678
+ cameFromPos[neighbor] = current;
1679
+ cameFromEdge[neighbor] = csr.outEdgeIndex[a];
1680
+ gScore[neighbor] = tentativeScore;
1275
1681
  openSet.push({
1276
- id: neighborId,
1277
- f: tentativeScore + heuristic(neighborId)
1682
+ pos: neighbor,
1683
+ f: tentativeScore + heuristic(csr.ids[neighbor])
1278
1684
  });
1279
1685
  }
1280
1686
  }
@@ -1289,40 +1695,289 @@ function joinPaths(headPath, tailPath) {
1289
1695
  };
1290
1696
  }
1291
1697
 
1698
+ //#endregion
1699
+ //#region src/config.ts
1700
+ /**
1701
+ * Convert a resolved {@link GraphNode} back into a {@link NodeConfig}.
1702
+ *
1703
+ * Faithful and complete: round-tripping through `createGraphNode` yields a
1704
+ * deep-equal node. Optional fields are only included when present; ports are
1705
+ * deep-copied so the config does not share port objects with the source node.
1706
+ */
1707
+ function toNodeConfig(node) {
1708
+ const config = { id: node.id };
1709
+ if (node.parentId != null) config.parentId = node.parentId;
1710
+ if (node.initialNodeId != null) config.initialNodeId = node.initialNodeId;
1711
+ if (node.label != null) config.label = node.label;
1712
+ if (node.data != null) config.data = node.data;
1713
+ if (node.ports !== void 0) config.ports = node.ports.map((p) => ({ ...p }));
1714
+ if (node.x !== void 0) config.x = node.x;
1715
+ if (node.y !== void 0) config.y = node.y;
1716
+ if (node.width !== void 0) config.width = node.width;
1717
+ if (node.height !== void 0) config.height = node.height;
1718
+ if (node.shape !== void 0) config.shape = node.shape;
1719
+ if (node.color !== void 0) config.color = node.color;
1720
+ if (node.style !== void 0) config.style = node.style;
1721
+ return config;
1722
+ }
1723
+ /**
1724
+ * Convert a resolved {@link GraphEdge} back into an {@link EdgeConfig}.
1725
+ *
1726
+ * Faithful and complete: round-tripping through `createGraphEdge` yields a
1727
+ * deep-equal edge. Optional fields are only included when present.
1728
+ */
1729
+ function toEdgeConfig(edge) {
1730
+ const config = {
1731
+ id: edge.id,
1732
+ sourceId: edge.sourceId,
1733
+ targetId: edge.targetId
1734
+ };
1735
+ if (edge.label != null) config.label = edge.label;
1736
+ if (edge.data != null) config.data = edge.data;
1737
+ if (edge.weight !== void 0) config.weight = edge.weight;
1738
+ if (edge.mode !== void 0) config.mode = edge.mode;
1739
+ if (edge.sourcePort !== void 0) config.sourcePort = edge.sourcePort;
1740
+ if (edge.targetPort !== void 0) config.targetPort = edge.targetPort;
1741
+ if (edge.x !== void 0) config.x = edge.x;
1742
+ if (edge.y !== void 0) config.y = edge.y;
1743
+ if (edge.width !== void 0) config.width = edge.width;
1744
+ if (edge.height !== void 0) config.height = edge.height;
1745
+ if (edge.color !== void 0) config.color = edge.color;
1746
+ if (edge.style !== void 0) config.style = edge.style;
1747
+ return config;
1748
+ }
1749
+
1750
+ //#endregion
1751
+ //#region src/transforms.ts
1752
+ /**
1753
+ * Flattens a hierarchical graph into a flat graph with only leaf nodes.
1754
+ *
1755
+ * - Edges targeting a compound node resolve to its initial child (recursively).
1756
+ * - Edges originating from a compound node expand to all leaf descendants.
1757
+ * - Only leaf nodes (nodes with no children) appear in the result.
1758
+ * - Duplicate edges (same source + target) are deduplicated.
1759
+ *
1760
+ * @example
1761
+ * ```ts
1762
+ * import { createGraph, flatten } from '@statelyai/graph';
1763
+ *
1764
+ * const graph = createGraph({
1765
+ * nodes: [
1766
+ * { id: 'parent', initialNodeId: 'child1' },
1767
+ * { id: 'child1', parentId: 'parent' },
1768
+ * { id: 'child2', parentId: 'parent' },
1769
+ * { id: 'other' },
1770
+ * ],
1771
+ * edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
1772
+ * });
1773
+ *
1774
+ * const flat = flatten(graph);
1775
+ * // flat.nodes → [child1, child2, other] (leaf nodes only)
1776
+ * // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
1777
+ * ```
1778
+ */
1779
+ function flatten(graph) {
1780
+ const idx = getIndex(graph);
1781
+ const leaves = /* @__PURE__ */ new Set();
1782
+ for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
1783
+ function resolveInitial(nodeId, seen = /* @__PURE__ */ new Set()) {
1784
+ if (leaves.has(nodeId)) return nodeId;
1785
+ if (seen.has(nodeId)) return null;
1786
+ seen.add(nodeId);
1787
+ const ni = idx.nodeById.get(nodeId);
1788
+ if (ni === void 0) return null;
1789
+ const node = graph.nodes[ni];
1790
+ if (node.initialNodeId) return resolveInitial(node.initialNodeId, seen);
1791
+ const childIds = idx.childNodes.get(nodeId) ?? [];
1792
+ if (childIds.length > 0) return resolveInitial(childIds[0], seen);
1793
+ return nodeId;
1794
+ }
1795
+ function getLeafDescendants(nodeId) {
1796
+ if (leaves.has(nodeId)) return [nodeId];
1797
+ const result = [];
1798
+ const collect = (id) => {
1799
+ const childIds = idx.childNodes.get(id) ?? [];
1800
+ for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
1801
+ else collect(childId);
1802
+ };
1803
+ collect(nodeId);
1804
+ return result;
1805
+ }
1806
+ const edgeSeen = /* @__PURE__ */ new Set();
1807
+ const flatEdges = [];
1808
+ for (const edge of graph.edges) {
1809
+ const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
1810
+ const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
1811
+ if (target === null) continue;
1812
+ for (const source of sources) {
1813
+ const isAuthoredLeafSelfLoop = edge.sourceId === edge.targetId && leaves.has(edge.sourceId);
1814
+ if (source === target && !isAuthoredLeafSelfLoop) continue;
1815
+ const key = `${source}->${target}`;
1816
+ if (edgeSeen.has(key)) continue;
1817
+ edgeSeen.add(key);
1818
+ flatEdges.push({
1819
+ type: "edge",
1820
+ id: `${edge.id}:${source}->${target}`,
1821
+ sourceId: source,
1822
+ targetId: target,
1823
+ label: edge.label,
1824
+ data: edge.data,
1825
+ ...edge.weight !== void 0 && { weight: edge.weight },
1826
+ ...edge.mode !== void 0 && { mode: edge.mode },
1827
+ ...source === edge.sourceId && edge.sourcePort !== void 0 && { sourcePort: edge.sourcePort },
1828
+ ...target === edge.targetId && edge.targetPort !== void 0 && { targetPort: edge.targetPort }
1829
+ });
1830
+ }
1831
+ }
1832
+ const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => {
1833
+ const { type, parentId, initialNodeId, ...rest } = n;
1834
+ return rest;
1835
+ });
1836
+ return createGraph({
1837
+ id: graph.id,
1838
+ mode: graph.mode,
1839
+ initialNodeId: graph.initialNodeId ? resolveInitial(graph.initialNodeId) ?? void 0 : void 0,
1840
+ nodes: leafNodes,
1841
+ edges: flatEdges,
1842
+ data: graph.data
1843
+ });
1844
+ }
1845
+ /**
1846
+ * Convert a node to a config, stripping parentId/initialNodeId references
1847
+ * to nodes outside the given set.
1848
+ */
1849
+ function toScopedNodeConfig(node, nodeIdSet) {
1850
+ const config = toNodeConfig(node);
1851
+ if (nodeIdSet) {
1852
+ if (config.parentId != null && !nodeIdSet.has(config.parentId)) delete config.parentId;
1853
+ if (config.initialNodeId != null && !nodeIdSet.has(config.initialNodeId)) delete config.initialNodeId;
1854
+ }
1855
+ return config;
1856
+ }
1857
+ /**
1858
+ * Returns the induced subgraph containing only the given node IDs
1859
+ * and edges whose endpoints are both in the set.
1860
+ *
1861
+ * Parent references to nodes outside the set are removed.
1862
+ *
1863
+ * @example
1864
+ * ```ts
1865
+ * import { createGraph, getSubgraph } from '@statelyai/graph';
1866
+ *
1867
+ * const graph = createGraph({
1868
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1869
+ * edges: [
1870
+ * { id: 'ab', sourceId: 'a', targetId: 'b' },
1871
+ * { id: 'bc', sourceId: 'b', targetId: 'c' },
1872
+ * ],
1873
+ * });
1874
+ *
1875
+ * const sub = getSubgraph(graph, ['a', 'b']);
1876
+ * // sub.nodes: [a, b], sub.edges: [ab]
1877
+ * ```
1878
+ */
1879
+ function getSubgraph(graph, nodeIds) {
1880
+ const nodeIdSet = new Set(nodeIds);
1881
+ return createGraph({
1882
+ id: graph.id,
1883
+ mode: graph.mode,
1884
+ initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
1885
+ nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => toScopedNodeConfig(n, nodeIdSet)),
1886
+ edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(toEdgeConfig),
1887
+ data: graph.data
1888
+ });
1889
+ }
1890
+ /**
1891
+ * Returns a new graph with all edge directions flipped (source ↔ target).
1892
+ * Optionally filters which edges to include.
1893
+ *
1894
+ * @example
1895
+ * ```ts
1896
+ * import { createGraph, reverseGraph } from '@statelyai/graph';
1897
+ *
1898
+ * const graph = createGraph({
1899
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1900
+ * edges: [
1901
+ * { id: 'ab', sourceId: 'a', targetId: 'b' },
1902
+ * { id: 'bc', sourceId: 'b', targetId: 'c' },
1903
+ * ],
1904
+ * });
1905
+ *
1906
+ * const rev = reverseGraph(graph);
1907
+ * // rev edges: b→a, c→b
1908
+ *
1909
+ * const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
1910
+ * // filtered edges: b→a (only ab reversed, bc excluded)
1911
+ * ```
1912
+ */
1913
+ function reverseGraph(graph, filterEdge) {
1914
+ const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
1915
+ return createGraph({
1916
+ id: graph.id,
1917
+ mode: graph.mode,
1918
+ initialNodeId: graph.initialNodeId ?? void 0,
1919
+ nodes: graph.nodes.map((n) => toNodeConfig(n)),
1920
+ edges: edges.map((e) => {
1921
+ const config = toEdgeConfig(e);
1922
+ config.sourceId = e.targetId;
1923
+ config.targetId = e.sourceId;
1924
+ delete config.sourcePort;
1925
+ delete config.targetPort;
1926
+ if (e.targetPort !== void 0) config.sourcePort = e.targetPort;
1927
+ if (e.sourcePort !== void 0) config.targetPort = e.sourcePort;
1928
+ return config;
1929
+ }),
1930
+ data: graph.data
1931
+ });
1932
+ }
1933
+
1292
1934
  //#endregion
1293
1935
  //#region src/algorithms/traversal.ts
1294
1936
  function* bfs(graph, startId) {
1295
- const idx = getIndex(graph);
1296
- const visited = /* @__PURE__ */ new Set();
1297
- const queue = [startId];
1298
- visited.add(startId);
1299
- while (queue.length > 0) {
1300
- const id = queue.shift();
1301
- const ni = idx.nodeById.get(id);
1302
- if (ni === void 0) continue;
1303
- yield graph.nodes[ni];
1304
- for (const neighborId of getNeighborIds$1(graph, id)) if (!visited.has(neighborId)) {
1305
- visited.add(neighborId);
1306
- queue.push(neighborId);
1937
+ const csr = getCSR(graph);
1938
+ const start = csr.indexOf.get(startId);
1939
+ if (start === void 0) return;
1940
+ const n = csr.ids.length;
1941
+ const visited = new Uint8Array(n);
1942
+ const queue = new Int32Array(n);
1943
+ visited[start] = 1;
1944
+ queue[0] = start;
1945
+ let head = 0;
1946
+ let tail = 1;
1947
+ while (head < tail) {
1948
+ const u = queue[head++];
1949
+ yield graph.nodes[u];
1950
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1951
+ const v = csr.outTargets[a];
1952
+ if (!visited[v]) {
1953
+ visited[v] = 1;
1954
+ queue[tail++] = v;
1955
+ }
1307
1956
  }
1308
1957
  }
1309
1958
  }
1310
1959
  function* dfs(graph, startId) {
1311
- const idx = getIndex(graph);
1312
- const visited = /* @__PURE__ */ new Set();
1313
- const stack = [startId];
1960
+ const csr = getCSR(graph);
1961
+ const start = csr.indexOf.get(startId);
1962
+ if (start === void 0) return;
1963
+ const n = csr.ids.length;
1964
+ const visited = new Uint8Array(n);
1965
+ const stack = [start];
1314
1966
  while (stack.length > 0) {
1315
- const id = stack.pop();
1316
- if (visited.has(id)) continue;
1317
- visited.add(id);
1318
- const ni = idx.nodeById.get(id);
1319
- if (ni === void 0) continue;
1320
- yield graph.nodes[ni];
1321
- for (const neighborId of getNeighborIds$1(graph, id)) if (!visited.has(neighborId)) stack.push(neighborId);
1967
+ const u = stack.pop();
1968
+ if (visited[u]) continue;
1969
+ visited[u] = 1;
1970
+ yield graph.nodes[u];
1971
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1972
+ const v = csr.outTargets[a];
1973
+ if (!visited[v]) stack.push(v);
1974
+ }
1322
1975
  }
1323
1976
  }
1324
1977
  function isAcyclic(graph) {
1325
- if (graph.mode !== "directed") return isAcyclicUndirected(graph);
1978
+ const kind = getEffectiveModeKind(graph);
1979
+ if (kind === "mixed") return isAcyclicMixed(graph);
1980
+ if (kind === "non-directed") return isAcyclicUndirected(graph);
1326
1981
  const WHITE = 0;
1327
1982
  const GRAY = 1;
1328
1983
  const BLACK = 2;
@@ -1341,6 +1996,63 @@ function isAcyclic(graph) {
1341
1996
  for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasCycle(node.id)) return false;
1342
1997
  return true;
1343
1998
  }
1999
+ /**
2000
+ * Acyclicity for graphs mixing directed and non-directed edges.
2001
+ *
2002
+ * Polynomial fast paths first: a cycle among directed edges alone, a cycle
2003
+ * among non-directed edges alone (union-find), or all-singleton reachability
2004
+ * SCCs (then no mixed cycle can exist either). Only ambiguous multi-node
2005
+ * SCCs fall back to exact simple-cycle enumeration, restricted to that SCC.
2006
+ */
2007
+ function isAcyclicMixed(graph) {
2008
+ const idx = getIndex(graph);
2009
+ const WHITE = 0;
2010
+ const GRAY = 1;
2011
+ const BLACK = 2;
2012
+ const color = /* @__PURE__ */ new Map();
2013
+ for (const node of graph.nodes) color.set(node.id, WHITE);
2014
+ const hasDirectedCycle = (id) => {
2015
+ color.set(id, GRAY);
2016
+ for (const eid of idx.outEdges.get(id) ?? []) {
2017
+ const edge = graph.edges[idx.edgeById.get(eid)];
2018
+ if (getEdgeMode(graph, edge) !== "directed") continue;
2019
+ const current = color.get(edge.targetId);
2020
+ if (current === GRAY) return true;
2021
+ if (current === WHITE && hasDirectedCycle(edge.targetId)) return true;
2022
+ }
2023
+ color.set(id, BLACK);
2024
+ return false;
2025
+ };
2026
+ for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasDirectedCycle(node.id)) return false;
2027
+ const parent = /* @__PURE__ */ new Map();
2028
+ const find = (id) => {
2029
+ let root = id;
2030
+ while (parent.get(root) !== root) root = parent.get(root);
2031
+ let cursor = id;
2032
+ while (parent.get(cursor) !== root) {
2033
+ const next = parent.get(cursor);
2034
+ parent.set(cursor, root);
2035
+ cursor = next;
2036
+ }
2037
+ return root;
2038
+ };
2039
+ for (const node of graph.nodes) parent.set(node.id, node.id);
2040
+ for (const edge of graph.edges) {
2041
+ if (getEdgeMode(graph, edge) === "directed") continue;
2042
+ if (edge.sourceId === edge.targetId) return false;
2043
+ const rootA = find(edge.sourceId);
2044
+ const rootB = find(edge.targetId);
2045
+ if (rootA === rootB) return false;
2046
+ parent.set(rootA, rootB);
2047
+ }
2048
+ const multiNodeSccs = getStronglyConnectedComponents(graph).filter((component) => component.length > 1);
2049
+ if (multiNodeSccs.length === 0) return true;
2050
+ for (const component of multiNodeSccs) {
2051
+ const subgraph = getSubgraph(graph, component.map((node) => node.id));
2052
+ for (const _cycle of genCycles(subgraph)) return false;
2053
+ }
2054
+ return true;
2055
+ }
1344
2056
  function isAcyclicUndirected(graph) {
1345
2057
  const idx = getIndex(graph);
1346
2058
  const visited = /* @__PURE__ */ new Set();
@@ -1368,34 +2080,33 @@ function isAcyclicUndirected(graph) {
1368
2080
  return true;
1369
2081
  }
1370
2082
  function getConnectedComponents(graph) {
1371
- const idx = getIndex(graph);
1372
- const visited = /* @__PURE__ */ new Set();
2083
+ const csr = getCSR(graph);
2084
+ const n = csr.ids.length;
2085
+ const visited = new Uint8Array(n);
2086
+ const queue = new Int32Array(n);
1373
2087
  const components = [];
1374
- for (const node of graph.nodes) {
1375
- if (visited.has(node.id)) continue;
2088
+ for (let s = 0; s < n; s++) {
2089
+ if (visited[s]) continue;
1376
2090
  const component = [];
1377
- const queue = [node.id];
1378
- visited.add(node.id);
1379
- while (queue.length > 0) {
1380
- const id = queue.shift();
1381
- const ni = idx.nodeById.get(id);
1382
- if (ni !== void 0) component.push(graph.nodes[ni]);
1383
- for (const eid of idx.outEdges.get(id) ?? []) {
1384
- const ai = idx.edgeById.get(eid);
1385
- if (ai === void 0) continue;
1386
- const neighborId = graph.edges[ai].targetId;
1387
- if (!visited.has(neighborId)) {
1388
- visited.add(neighborId);
1389
- queue.push(neighborId);
2091
+ visited[s] = 1;
2092
+ queue[0] = s;
2093
+ let head = 0;
2094
+ let tail = 1;
2095
+ while (head < tail) {
2096
+ const u = queue[head++];
2097
+ component.push(graph.nodes[u]);
2098
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
2099
+ const v = csr.outTargets[a];
2100
+ if (!visited[v]) {
2101
+ visited[v] = 1;
2102
+ queue[tail++] = v;
1390
2103
  }
1391
2104
  }
1392
- for (const eid of idx.inEdges.get(id) ?? []) {
1393
- const ai = idx.edgeById.get(eid);
1394
- if (ai === void 0) continue;
1395
- const neighborId = graph.edges[ai].sourceId;
1396
- if (!visited.has(neighborId)) {
1397
- visited.add(neighborId);
1398
- queue.push(neighborId);
2105
+ for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
2106
+ const v = csr.inOrigins[a];
2107
+ if (!visited[v]) {
2108
+ visited[v] = 1;
2109
+ queue[tail++] = v;
1399
2110
  }
1400
2111
  }
1401
2112
  }
@@ -1403,7 +2114,16 @@ function getConnectedComponents(graph) {
1403
2114
  }
1404
2115
  return components;
1405
2116
  }
2117
+ /**
2118
+ * Returns a topological ordering of the graph's nodes, or `null` if no such
2119
+ * ordering exists.
2120
+ *
2121
+ * Any edge whose effective mode (per {@link getEdgeMode}) is not `'directed'`
2122
+ * makes ordering impossible — an undirected/bidirectional edge is mutual
2123
+ * precedence, i.e. a 2-cycle — so the function returns `null`.
2124
+ */
1406
2125
  function getTopologicalSort(graph) {
2126
+ for (const edge of graph.edges) if (getEdgeMode(graph, edge) !== "directed") return null;
1407
2127
  const idx = getIndex(graph);
1408
2128
  const inDegree = /* @__PURE__ */ new Map();
1409
2129
  for (const node of graph.nodes) inDegree.set(node.id, 0);
@@ -1428,17 +2148,33 @@ function getTopologicalSort(graph) {
1428
2148
  return result;
1429
2149
  }
1430
2150
  function hasPath(graph, sourceId, targetId) {
1431
- return getShortestPaths(graph, {
1432
- from: sourceId,
1433
- to: targetId
1434
- }).length > 0;
2151
+ if (sourceId === targetId) return true;
2152
+ const visited = new Set([sourceId]);
2153
+ const queue = [sourceId];
2154
+ while (queue.length > 0) {
2155
+ const id = queue.shift();
2156
+ for (const neighborId of getNeighborIds(graph, id)) {
2157
+ if (neighborId === targetId) return true;
2158
+ if (!visited.has(neighborId)) {
2159
+ visited.add(neighborId);
2160
+ queue.push(neighborId);
2161
+ }
2162
+ }
2163
+ }
2164
+ return false;
1435
2165
  }
1436
2166
  function isConnected(graph) {
1437
2167
  if (graph.nodes.length === 0) return true;
1438
2168
  return getConnectedComponents(graph).length <= 1;
1439
2169
  }
2170
+ /**
2171
+ * Returns whether the graph is a tree: connected, acyclic, and with exactly
2172
+ * `nodes.length - 1` edges (so directed diamonds and parallel edges are not
2173
+ * trees). Empty and single-node graphs are considered trees.
2174
+ */
1440
2175
  function isTree(graph) {
1441
- return isConnected(graph) && isAcyclic(graph);
2176
+ if (graph.nodes.length === 0) return true;
2177
+ return graph.edges.length === graph.nodes.length - 1 && isConnected(graph) && isAcyclic(graph);
1442
2178
  }
1443
2179
 
1444
2180
  //#endregion
@@ -1453,7 +2189,7 @@ function getPreorder(graph, opts) {
1453
2189
  const stack = [startId];
1454
2190
  while (stack.length > 0) {
1455
2191
  const top = stack[stack.length - 1];
1456
- const next = getNeighborIds$1(graph, top).find((id) => !visited.has(id));
2192
+ const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
1457
2193
  if (next === void 0) {
1458
2194
  stack.pop();
1459
2195
  continue;
@@ -1474,7 +2210,7 @@ function getPostorder(graph, opts) {
1474
2210
  const stack = [startId];
1475
2211
  while (stack.length > 0) {
1476
2212
  const top = stack[stack.length - 1];
1477
- const next = getNeighborIds$1(graph, top).find((id) => !visited.has(id));
2213
+ const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
1478
2214
  if (next === void 0) {
1479
2215
  stack.pop();
1480
2216
  const ni = idx.nodeById.get(top);
@@ -1510,7 +2246,7 @@ function* genPreorders(graph, opts) {
1510
2246
  let branched = false;
1511
2247
  while (dfsStack.length > 0) {
1512
2248
  const top = dfsStack[dfsStack.length - 1];
1513
- const unvisited = getNeighborIds$1(graph, top).filter((id) => !visited.has(id));
2249
+ const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
1514
2250
  if (unvisited.length === 0) {
1515
2251
  dfsStack.pop();
1516
2252
  continue;
@@ -1548,7 +2284,7 @@ function* genPostorders(graph, opts) {
1548
2284
  let branched = false;
1549
2285
  while (dfsStack.length > 0) {
1550
2286
  const top = dfsStack[dfsStack.length - 1];
1551
- const unvisited = getNeighborIds$1(graph, top).filter((id) => !visited.has(id));
2287
+ const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
1552
2288
  if (unvisited.length === 0) {
1553
2289
  dfsStack.pop();
1554
2290
  const ni = idx.nodeById.get(top);
@@ -1582,21 +2318,8 @@ function getMinimumSpanningTree(graph, opts) {
1582
2318
  id: graph.id,
1583
2319
  mode: graph.mode,
1584
2320
  initialNodeId: graph.initialNodeId ?? void 0,
1585
- nodes: graph.nodes.map((node) => ({
1586
- id: node.id,
1587
- parentId: node.parentId ?? void 0,
1588
- initialNodeId: node.initialNodeId ?? void 0,
1589
- label: node.label,
1590
- data: node.data
1591
- })),
1592
- edges: mstEdges.map((edge) => ({
1593
- id: edge.id,
1594
- sourceId: edge.sourceId,
1595
- targetId: edge.targetId,
1596
- label: edge.label,
1597
- data: edge.data,
1598
- ...edge.weight !== void 0 && { weight: edge.weight }
1599
- }))
2321
+ nodes: graph.nodes.map((node) => toNodeConfig(node)),
2322
+ edges: mstEdges.map((edge) => toEdgeConfig(edge))
1600
2323
  });
1601
2324
  }
1602
2325
  function primMST(graph, getWeight) {
@@ -1615,26 +2338,28 @@ function primMST(graph, getWeight) {
1615
2338
  edge
1616
2339
  });
1617
2340
  }
1618
- if (graph.mode !== "directed") for (const eid of idx.inEdges.get(nodeId) ?? []) {
2341
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
1619
2342
  const ai = idx.edgeById.get(eid);
1620
2343
  if (ai === void 0) continue;
1621
2344
  const edge = graph.edges[ai];
1622
- if (!inMST.has(edge.sourceId)) candidates.push({
2345
+ if (getEdgeMode(graph, edge) !== "directed" && !inMST.has(edge.sourceId)) candidates.push({
1623
2346
  weight: getWeight(edge),
1624
2347
  edge
1625
2348
  });
1626
2349
  }
1627
2350
  }
1628
- const startId = graph.nodes[0].id;
1629
- inMST.add(startId);
1630
- addEdgesOf(startId);
1631
- while (candidates.size > 0 && inMST.size < graph.nodes.length) {
1632
- const { edge } = candidates.pop();
1633
- const targetId = graph.mode !== "directed" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
1634
- if (inMST.has(targetId)) continue;
1635
- inMST.add(targetId);
1636
- mstEdges.push(edge);
1637
- addEdgesOf(targetId);
2351
+ for (const node of graph.nodes) {
2352
+ if (inMST.has(node.id)) continue;
2353
+ inMST.add(node.id);
2354
+ addEdgesOf(node.id);
2355
+ while (candidates.size > 0 && inMST.size < graph.nodes.length) {
2356
+ const { edge } = candidates.pop();
2357
+ const targetId = getEdgeMode(graph, edge) !== "directed" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
2358
+ if (inMST.has(targetId)) continue;
2359
+ inMST.add(targetId);
2360
+ mstEdges.push(edge);
2361
+ addEdgesOf(targetId);
2362
+ }
1638
2363
  }
1639
2364
  return mstEdges;
1640
2365
  }
@@ -1672,60 +2397,38 @@ function kruskalMST(graph, getWeight) {
1672
2397
  function getNodeIds(graph) {
1673
2398
  return graph.nodes.map((node) => node.id);
1674
2399
  }
1675
- function getNeighborIds(graph, nodeId) {
1676
- const idx = getIndex(graph);
1677
- const neighbors = [];
1678
- for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
1679
- const edgeIndex = idx.edgeById.get(edgeId);
1680
- if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].targetId);
1681
- }
1682
- if (graph.mode !== "directed") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
1683
- const edgeIndex = idx.edgeById.get(edgeId);
1684
- if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
1685
- }
1686
- return neighbors;
1687
- }
1688
- function getIncomingIds(graph, nodeId) {
1689
- const idx = getIndex(graph);
1690
- const incoming = [];
1691
- for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
1692
- const edgeIndex = idx.edgeById.get(edgeId);
1693
- if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].sourceId);
1694
- }
1695
- if (graph.mode !== "directed") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
1696
- const edgeIndex = idx.edgeById.get(edgeId);
1697
- if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].targetId);
1698
- }
1699
- return incoming;
1700
- }
1701
2400
  function createEmptyScoreMap(graph) {
1702
2401
  return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
1703
2402
  }
1704
- function normalizeVector(scores) {
1705
- const magnitude = Math.sqrt(Object.values(scores).reduce((sum, value) => sum + value * value, 0));
1706
- if (magnitude === 0) return scores;
1707
- for (const key of Object.keys(scores)) scores[key] /= magnitude;
1708
- return scores;
1709
- }
1710
- function maxDiff(previous, next) {
1711
- let diff = 0;
1712
- for (const key of Object.keys(next)) diff = Math.max(diff, Math.abs((previous[key] ?? 0) - next[key]));
1713
- return diff;
2403
+ function normalizeTypedVector(values) {
2404
+ let sumOfSquares = 0;
2405
+ for (let i = 0; i < values.length; i++) sumOfSquares += values[i] * values[i];
2406
+ const magnitude = Math.sqrt(sumOfSquares);
2407
+ if (magnitude === 0) return;
2408
+ for (let i = 0; i < values.length; i++) values[i] /= magnitude;
1714
2409
  }
1715
- function getReachableDistances(graph, startId) {
1716
- const distances = /* @__PURE__ */ new Map();
1717
- const queue = [startId];
1718
- distances.set(startId, 0);
1719
- while (queue.length > 0) {
1720
- const currentId = queue.shift();
1721
- const currentDistance = distances.get(currentId);
1722
- for (const neighborId of getNeighborIds(graph, currentId)) {
1723
- if (distances.has(neighborId)) continue;
1724
- distances.set(neighborId, currentDistance + 1);
1725
- queue.push(neighborId);
2410
+ /**
2411
+ * BFS hop distances from a start position over the CSR arc snapshot.
2412
+ * `dist[i] === -1` means unreachable. Returns the visit count.
2413
+ */
2414
+ function bfsDistances(csr, start, dist, queue) {
2415
+ dist.fill(-1);
2416
+ dist[start] = 0;
2417
+ queue[0] = start;
2418
+ let head = 0;
2419
+ let tail = 1;
2420
+ while (head < tail) {
2421
+ const u = queue[head++];
2422
+ const du = dist[u];
2423
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
2424
+ const v = csr.outTargets[a];
2425
+ if (dist[v] === -1) {
2426
+ dist[v] = du + 1;
2427
+ queue[tail++] = v;
2428
+ }
1726
2429
  }
1727
2430
  }
1728
- return distances;
2431
+ return tail;
1729
2432
  }
1730
2433
  /**
1731
2434
  * Returns degree centrality scores for all nodes.
@@ -1740,14 +2443,8 @@ function getReachableDistances(graph, startId) {
1740
2443
  */
1741
2444
  function getDegreeCentrality(graph) {
1742
2445
  const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
1743
- const idx = getIndex(graph);
1744
2446
  const scores = createEmptyScoreMap(graph);
1745
- for (const node of graph.nodes) {
1746
- const outDegree = idx.outEdges.get(node.id)?.length ?? 0;
1747
- const inDegree = idx.inEdges.get(node.id)?.length ?? 0;
1748
- const degree = graph.mode !== "directed" ? new Set([...idx.outEdges.get(node.id) ?? [], ...idx.inEdges.get(node.id) ?? []]).size : outDegree + inDegree;
1749
- scores[node.id] = degree * scale;
1750
- }
2447
+ for (const node of graph.nodes) scores[node.id] = getDegree(graph, node.id) * scale;
1751
2448
  return scores;
1752
2449
  }
1753
2450
  /**
@@ -1757,9 +2454,8 @@ function getDegreeCentrality(graph) {
1757
2454
  */
1758
2455
  function getInDegreeCentrality(graph) {
1759
2456
  const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
1760
- const idx = getIndex(graph);
1761
2457
  const scores = createEmptyScoreMap(graph);
1762
- for (const node of graph.nodes) scores[node.id] = (idx.inEdges.get(node.id)?.length ?? 0) * scale;
2458
+ for (const node of graph.nodes) scores[node.id] = getInDegree(graph, node.id) * scale;
1763
2459
  return scores;
1764
2460
  }
1765
2461
  /**
@@ -1769,9 +2465,8 @@ function getInDegreeCentrality(graph) {
1769
2465
  */
1770
2466
  function getOutDegreeCentrality(graph) {
1771
2467
  const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
1772
- const idx = getIndex(graph);
1773
2468
  const scores = createEmptyScoreMap(graph);
1774
- for (const node of graph.nodes) scores[node.id] = (idx.outEdges.get(node.id)?.length ?? 0) * scale;
2469
+ for (const node of graph.nodes) scores[node.id] = getOutDegree(graph, node.id) * scale;
1775
2470
  return scores;
1776
2471
  }
1777
2472
  /**
@@ -1782,16 +2477,19 @@ function getOutDegreeCentrality(graph) {
1782
2477
  */
1783
2478
  function getClosenessCentrality(graph) {
1784
2479
  const scores = createEmptyScoreMap(graph);
1785
- const order = graph.nodes.length;
1786
- for (const node of graph.nodes) {
1787
- const distances = getReachableDistances(graph, node.id);
1788
- distances.delete(node.id);
1789
- if (distances.size === 0) continue;
1790
- const totalDistance = [...distances.values()].reduce((sum, distance) => sum + distance, 0);
2480
+ const csr = getCSR(graph);
2481
+ const order = csr.ids.length;
2482
+ const dist = new Int32Array(order);
2483
+ const queue = new Int32Array(order);
2484
+ for (let s = 0; s < order; s++) {
2485
+ const visited = bfsDistances(csr, s, dist, queue);
2486
+ const reachable = visited - 1;
2487
+ if (reachable === 0) continue;
2488
+ let totalDistance = 0;
2489
+ for (let k = 0; k < visited; k++) totalDistance += dist[queue[k]];
1791
2490
  if (totalDistance === 0) continue;
1792
- const reachable = distances.size;
1793
2491
  const closeness = reachable / totalDistance;
1794
- scores[node.id] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
2492
+ scores[csr.ids[s]] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
1795
2493
  }
1796
2494
  return scores;
1797
2495
  }
@@ -1802,47 +2500,48 @@ function getClosenessCentrality(graph) {
1802
2500
  * normalized scores.
1803
2501
  */
1804
2502
  function getBetweennessCentrality(graph) {
1805
- const scores = createEmptyScoreMap(graph);
1806
- for (const source of graph.nodes) {
1807
- const stack = [];
1808
- const predecessors = /* @__PURE__ */ new Map();
1809
- const sigma = /* @__PURE__ */ new Map();
1810
- const distance = /* @__PURE__ */ new Map();
1811
- const queue = [source.id];
1812
- for (const node of graph.nodes) {
1813
- predecessors.set(node.id, []);
1814
- sigma.set(node.id, 0);
1815
- distance.set(node.id, -1);
1816
- }
1817
- sigma.set(source.id, 1);
1818
- distance.set(source.id, 0);
1819
- while (queue.length > 0) {
1820
- const currentId = queue.shift();
1821
- stack.push(currentId);
1822
- for (const neighborId of getNeighborIds(graph, currentId)) {
1823
- if (distance.get(neighborId) === -1) {
1824
- queue.push(neighborId);
1825
- distance.set(neighborId, distance.get(currentId) + 1);
1826
- }
1827
- if (distance.get(neighborId) === distance.get(currentId) + 1) {
1828
- sigma.set(neighborId, sigma.get(neighborId) + sigma.get(currentId));
1829
- predecessors.get(neighborId).push(currentId);
2503
+ const csr = getCSR(graph);
2504
+ const n = csr.ids.length;
2505
+ const totals = new Float64Array(n);
2506
+ const sigma = new Float64Array(n);
2507
+ const dist = new Int32Array(n);
2508
+ const delta = new Float64Array(n);
2509
+ const order_ = new Int32Array(n);
2510
+ for (let s = 0; s < n; s++) {
2511
+ sigma.fill(0);
2512
+ dist.fill(-1);
2513
+ delta.fill(0);
2514
+ sigma[s] = 1;
2515
+ dist[s] = 0;
2516
+ order_[0] = s;
2517
+ let head = 0;
2518
+ let tail = 1;
2519
+ while (head < tail) {
2520
+ const u = order_[head++];
2521
+ const du = dist[u];
2522
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
2523
+ const v = csr.outTargets[a];
2524
+ if (dist[v] === -1) {
2525
+ dist[v] = du + 1;
2526
+ order_[tail++] = v;
1830
2527
  }
2528
+ if (dist[v] === du + 1) sigma[v] += sigma[u];
1831
2529
  }
1832
2530
  }
1833
- const delta = /* @__PURE__ */ new Map();
1834
- for (const node of graph.nodes) delta.set(node.id, 0);
1835
- while (stack.length > 0) {
1836
- const nodeId = stack.pop();
1837
- const sigmaNode = sigma.get(nodeId);
1838
- if (sigmaNode === 0) continue;
1839
- for (const predecessorId of predecessors.get(nodeId)) {
1840
- const contribution = sigma.get(predecessorId) / sigmaNode * (1 + delta.get(nodeId));
1841
- delta.set(predecessorId, delta.get(predecessorId) + contribution);
2531
+ for (let k = tail - 1; k >= 0; k--) {
2532
+ const w = order_[k];
2533
+ const sigmaW = sigma[w];
2534
+ if (sigmaW === 0) continue;
2535
+ const coefficient = (1 + delta[w]) / sigmaW;
2536
+ for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
2537
+ const v = csr.inOrigins[a];
2538
+ if (dist[v] === dist[w] - 1) delta[v] += sigma[v] * coefficient;
1842
2539
  }
1843
- if (nodeId !== source.id) scores[nodeId] += delta.get(nodeId);
2540
+ if (w !== s) totals[w] += delta[w];
1844
2541
  }
1845
2542
  }
2543
+ const scores = createEmptyScoreMap(graph);
2544
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = totals[i];
1846
2545
  const order = graph.nodes.length;
1847
2546
  if (order <= 2) return scores;
1848
2547
  const scale = graph.mode !== "directed" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
@@ -1864,28 +2563,32 @@ function getPageRank(graph, options) {
1864
2563
  const maxIterations = options?.maxIterations ?? 100;
1865
2564
  const tolerance = options?.tolerance ?? 1e-6;
1866
2565
  let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1 / nodeIds.length]));
2566
+ const csr = getCSR(graph);
2567
+ const n = csr.ids.length;
2568
+ let current = new Float64Array(n).fill(1 / n);
2569
+ for (let i = 0; i < n; i++) current[i] = scores[csr.ids[i]];
1867
2570
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1868
- const nextScores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, (1 - alpha) / nodeIds.length]));
2571
+ const next = new Float64Array(n).fill((1 - alpha) / n);
1869
2572
  let danglingMass = 0;
1870
- for (const nodeId of nodeIds) {
1871
- const neighbors = getNeighborIds(graph, nodeId);
1872
- if (neighbors.length === 0) {
1873
- danglingMass += scores[nodeId];
2573
+ for (let u = 0; u < n; u++) {
2574
+ const arcCount = csr.outOffsets[u + 1] - csr.outOffsets[u];
2575
+ if (arcCount === 0) {
2576
+ danglingMass += current[u];
1874
2577
  continue;
1875
2578
  }
1876
- const share = scores[nodeId] / neighbors.length;
1877
- for (const neighborId of neighbors) nextScores[neighborId] += alpha * share;
2579
+ const share = alpha * current[u] / arcCount;
2580
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) next[csr.outTargets[a]] += share;
1878
2581
  }
1879
2582
  if (danglingMass > 0) {
1880
- const share = alpha * danglingMass / nodeIds.length;
1881
- for (const nodeId of nodeIds) nextScores[nodeId] += share;
1882
- }
1883
- if (maxDiff(scores, nextScores) <= tolerance) {
1884
- scores = nextScores;
1885
- break;
2583
+ const share = alpha * danglingMass / n;
2584
+ for (let i = 0; i < n; i++) next[i] += share;
1886
2585
  }
1887
- scores = nextScores;
2586
+ let diff = 0;
2587
+ for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
2588
+ current = next;
2589
+ if (diff <= tolerance) break;
1888
2590
  }
2591
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
1889
2592
  const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
1890
2593
  if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
1891
2594
  return scores;
@@ -1896,31 +2599,38 @@ function getPageRank(graph, options) {
1896
2599
  * Uses power iteration and L2 normalization per iteration.
1897
2600
  */
1898
2601
  function getHITS(graph, options) {
1899
- const nodeIds = getNodeIds(graph);
1900
- if (nodeIds.length === 0) return {
2602
+ if (getNodeIds(graph).length === 0) return {
1901
2603
  hubs: {},
1902
2604
  authorities: {}
1903
2605
  };
1904
2606
  const maxIterations = options?.maxIterations ?? 100;
1905
2607
  const tolerance = options?.tolerance ?? 1e-6;
1906
- let hubs = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
1907
- let authorities = createEmptyScoreMap(graph);
2608
+ const csr = getCSR(graph);
2609
+ const n = csr.ids.length;
2610
+ let hubs = new Float64Array(n).fill(1);
2611
+ let authorities = new Float64Array(n);
1908
2612
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1909
- const nextAuthorities = createEmptyScoreMap(graph);
1910
- for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextAuthorities[nodeId] += hubs[predecessorId];
1911
- normalizeVector(nextAuthorities);
1912
- const nextHubs = createEmptyScoreMap(graph);
1913
- for (const nodeId of nodeIds) for (const neighborId of getNeighborIds(graph, nodeId)) nextHubs[nodeId] += nextAuthorities[neighborId];
1914
- normalizeVector(nextHubs);
1915
- const hubDiff = maxDiff(hubs, nextHubs);
1916
- const authorityDiff = maxDiff(authorities, nextAuthorities);
2613
+ const nextAuthorities = new Float64Array(n);
2614
+ for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) nextAuthorities[w] += hubs[csr.inOrigins[a]];
2615
+ normalizeTypedVector(nextAuthorities);
2616
+ const nextHubs = new Float64Array(n);
2617
+ for (let u = 0; u < n; u++) for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) nextHubs[u] += nextAuthorities[csr.outTargets[a]];
2618
+ normalizeTypedVector(nextHubs);
2619
+ let diff = 0;
2620
+ for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(hubs[i] - nextHubs[i]), Math.abs(authorities[i] - nextAuthorities[i]));
1917
2621
  hubs = nextHubs;
1918
2622
  authorities = nextAuthorities;
1919
- if (Math.max(hubDiff, authorityDiff) <= tolerance) break;
2623
+ if (diff <= tolerance) break;
2624
+ }
2625
+ const hubScores = createEmptyScoreMap(graph);
2626
+ const authorityScores = createEmptyScoreMap(graph);
2627
+ for (let i = 0; i < n; i++) {
2628
+ hubScores[csr.ids[i]] = hubs[i];
2629
+ authorityScores[csr.ids[i]] = authorities[i];
1920
2630
  }
1921
2631
  return {
1922
- hubs,
1923
- authorities
2632
+ hubs: hubScores,
2633
+ authorities: authorityScores
1924
2634
  };
1925
2635
  }
1926
2636
  /**
@@ -1930,20 +2640,24 @@ function getHITS(graph, options) {
1930
2640
  * undirected adjacency for undirected graphs.
1931
2641
  */
1932
2642
  function getEigenvectorCentrality(graph, options) {
1933
- const nodeIds = getNodeIds(graph);
1934
- if (nodeIds.length === 0) return {};
2643
+ if (getNodeIds(graph).length === 0) return {};
1935
2644
  const maxIterations = options?.maxIterations ?? 100;
1936
2645
  const tolerance = options?.tolerance ?? 1e-6;
1937
- let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
1938
- normalizeVector(scores);
2646
+ const csr = getCSR(graph);
2647
+ const n = csr.ids.length;
2648
+ let current = new Float64Array(n).fill(1);
2649
+ normalizeTypedVector(current);
1939
2650
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1940
- const nextScores = createEmptyScoreMap(graph);
1941
- for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextScores[nodeId] += scores[predecessorId];
1942
- normalizeVector(nextScores);
1943
- const diff = maxDiff(scores, nextScores);
1944
- scores = nextScores;
2651
+ const next = new Float64Array(n);
2652
+ for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) next[w] += current[csr.inOrigins[a]];
2653
+ normalizeTypedVector(next);
2654
+ let diff = 0;
2655
+ for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
2656
+ current = next;
1945
2657
  if (diff <= tolerance) break;
1946
2658
  }
2659
+ const scores = createEmptyScoreMap(graph);
2660
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
1947
2661
  return scores;
1948
2662
  }
1949
2663
 
@@ -2251,8 +2965,8 @@ function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
2251
2965
  traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
2252
2966
  state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
2253
2967
  if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
2254
- if (parentEdgeId !== null && state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
2255
- state.articulationPoints.add(nodeId);
2968
+ if (state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
2969
+ if (parentEdgeId !== null) state.articulationPoints.add(nodeId);
2256
2970
  popComponentUntil(state, neighbor.edgeId);
2257
2971
  }
2258
2972
  } else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
@@ -2316,14 +3030,33 @@ function getBiconnectedComponents(graph) {
2316
3030
  //#region src/algorithms/isomorphism.ts
2317
3031
  function getDegreeSignature(graph, nodeId) {
2318
3032
  const idx = getIndex(graph);
2319
- const outDegree = idx.outEdges.get(nodeId)?.length ?? 0;
2320
- const inDegree = idx.inEdges.get(nodeId)?.length ?? 0;
2321
- if (graph.mode !== "directed") return `u:${new Set([...idx.outEdges.get(nodeId) ?? [], ...idx.inEdges.get(nodeId) ?? []]).size}`;
2322
- return `d:${inDegree}:${outDegree}`;
3033
+ let inDegree = 0;
3034
+ let outDegree = 0;
3035
+ let undirected = 0;
3036
+ const countedNonDirected = /* @__PURE__ */ new Set();
3037
+ for (const eid of idx.outEdges.get(nodeId) ?? []) {
3038
+ const edge = graph.edges[idx.edgeById.get(eid)];
3039
+ if (getEdgeMode(graph, edge) === "directed") outDegree++;
3040
+ else if (!countedNonDirected.has(eid)) {
3041
+ countedNonDirected.add(eid);
3042
+ undirected++;
3043
+ }
3044
+ }
3045
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
3046
+ const edge = graph.edges[idx.edgeById.get(eid)];
3047
+ if (getEdgeMode(graph, edge) === "directed") inDegree++;
3048
+ else if (!countedNonDirected.has(eid)) {
3049
+ countedNonDirected.add(eid);
3050
+ undirected++;
3051
+ }
3052
+ }
3053
+ return `d:${inDegree}:${outDegree}:u:${undirected}`;
2323
3054
  }
2324
3055
  function getEdgesBetween(graph, sourceId, targetId) {
2325
- if (graph.mode !== "directed") return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId || edge.sourceId === targetId && edge.targetId === sourceId);
2326
- return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId);
3056
+ return graph.edges.filter((edge) => {
3057
+ if (edge.sourceId === sourceId && edge.targetId === targetId) return true;
3058
+ return edge.sourceId === targetId && edge.targetId === sourceId && getEdgeMode(graph, edge) !== "directed";
3059
+ });
2327
3060
  }
2328
3061
  function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
2329
3062
  if (edgesA.length !== edgesB.length) return false;
@@ -2343,7 +3076,6 @@ function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
2343
3076
  * node and edge payloads.
2344
3077
  */
2345
3078
  function isIsomorphic(graphA, graphB, options) {
2346
- if (graphA.mode !== graphB.mode) return false;
2347
3079
  if (graphA.nodes.length !== graphB.nodes.length) return false;
2348
3080
  if (graphA.edges.length !== graphB.edges.length) return false;
2349
3081
  const nodeMatch = options?.nodeMatch;
@@ -2367,6 +3099,7 @@ function isIsomorphic(graphA, graphB, options) {
2367
3099
  if (usedB.has(nodeB.id)) continue;
2368
3100
  if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
2369
3101
  if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
3102
+ if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, nodeA.id), getEdgesBetween(graphB, nodeB.id, nodeB.id), edgeMatch)) continue;
2370
3103
  let compatible = true;
2371
3104
  for (const [mappedAId, mappedBId] of mapping.entries()) {
2372
3105
  if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
@@ -2391,4 +3124,404 @@ function isIsomorphic(graphA, graphB, options) {
2391
3124
  }
2392
3125
 
2393
3126
  //#endregion
2394
- export { createGraphPort as $, isAcyclic as A, getShortestPaths as B, getPreorder as C, getConnectedComponents as D, dfs as E, genSimplePaths as F, GraphInstance as G, getSimplePaths as H, getAStarPath as I, addNode as J, addEdge as K, getAllPairsShortestPaths as L, isTree as M, genCycles as N, getTopologicalSort as O, genShortestPaths as P, createGraphNode as Q, getCycles as R, getPostorders as S, bfs as T, getStronglyConnectedComponents as U, getSimplePath as V, joinPaths as W, createGraphEdge as X, createGraph as Y, createGraphFromTransition as Z, getPageRank as _, genGirvanNewmanCommunities as a, getNode as at, genPreorders as b, getLabelPropagationCommunities as c, updateEdge as ct, getClosenessCentrality as d, createVisualGraph as et, getDegreeCentrality as f, getOutDegreeCentrality as g, getInDegreeCentrality as h, getBridges as i, getEdge as it, isConnected as j, hasPath as k, getModularity as l, updateEntities as lt, getHITS as m, getArticulationPoints as n, deleteEntities as nt, getGirvanNewmanCommunities as o, hasEdge as ot, getEigenvectorCentrality as p, addEntities as q, getBiconnectedComponents as r, deleteNode as rt, getGreedyModularityCommunities as s, hasNode as st, isIsomorphic as t, deleteEdge as tt, getBetweennessCentrality as u, updateNode as ut, getMinimumSpanningTree as v, getPreorders as w, getPostorder as x, genPostorders as y, getShortestPath as z };
3127
+ //#region src/algorithms/louvain.ts
3128
+ /**
3129
+ * Returns communities found by the classic two-phase Louvain modularity
3130
+ * optimization (local moving + community aggregation).
3131
+ *
3132
+ * Like the other community algorithms in this library, the graph is treated
3133
+ * as undirected regardless of `graph.mode` or per-edge modes. Parallel edges
3134
+ * have their weights summed; self-loops contribute to a community's internal
3135
+ * weight.
3136
+ *
3137
+ * The implementation is deterministic: nodes are visited in `graph.nodes`
3138
+ * array order and there is no random shuffling, so tie-breaking is
3139
+ * order-dependent but stable across runs.
3140
+ *
3141
+ * Returns communities of node ids, each community sorted lexicographically
3142
+ * and communities sorted by their first id.
3143
+ *
3144
+ * @example
3145
+ * ```ts
3146
+ * const communities = getLouvainCommunities(graph);
3147
+ * // [['a', 'b', 'c'], ['d', 'e', 'f']]
3148
+ * ```
3149
+ */
3150
+ function getLouvainCommunities(graph, options) {
3151
+ if (graph.nodes.length === 0) return [];
3152
+ const getWeight = options?.getWeight ?? ((edge) => edge.weight ?? 1);
3153
+ const resolution = options?.resolution ?? 1;
3154
+ const maxPasses = options?.maxPasses ?? 10;
3155
+ const nodeIds = graph.nodes.map((node) => node.id);
3156
+ const indexOf = new Map(nodeIds.map((id, i) => [id, i]));
3157
+ let count = nodeIds.length;
3158
+ let links = Array.from({ length: count }, () => /* @__PURE__ */ new Map());
3159
+ let selfLoops = new Array(count).fill(0);
3160
+ for (const edge of graph.edges) {
3161
+ const u = indexOf.get(edge.sourceId);
3162
+ const v = indexOf.get(edge.targetId);
3163
+ if (u === void 0 || v === void 0) continue;
3164
+ const w = getWeight(edge);
3165
+ if (u === v) selfLoops[u] += w;
3166
+ else {
3167
+ links[u].set(v, (links[u].get(v) ?? 0) + w);
3168
+ links[v].set(u, (links[v].get(u) ?? 0) + w);
3169
+ }
3170
+ }
3171
+ let membership = nodeIds.map((_, i) => i);
3172
+ for (let pass = 0; pass < maxPasses; pass++) {
3173
+ const degree = links.map((neighbors, i) => 2 * selfLoops[i] + [...neighbors.values()].reduce((sum, w) => sum + w, 0));
3174
+ const m2 = degree.reduce((sum, k) => sum + k, 0);
3175
+ if (m2 === 0) break;
3176
+ const communityOf = Array.from({ length: count }, (_, i) => i);
3177
+ const communityTotal = [...degree];
3178
+ let movedAny = false;
3179
+ let movedThisSweep = true;
3180
+ while (movedThisSweep) {
3181
+ movedThisSweep = false;
3182
+ for (let i = 0; i < count; i++) {
3183
+ const current = communityOf[i];
3184
+ const weightTo = /* @__PURE__ */ new Map();
3185
+ for (const [j, w] of links[i]) {
3186
+ const c = communityOf[j];
3187
+ weightTo.set(c, (weightTo.get(c) ?? 0) + w);
3188
+ }
3189
+ communityTotal[current] -= degree[i];
3190
+ const gainOf = (c) => (weightTo.get(c) ?? 0) - resolution * communityTotal[c] * degree[i] / m2;
3191
+ let best = current;
3192
+ let bestGain = gainOf(current);
3193
+ for (const c of weightTo.keys()) {
3194
+ if (c === current) continue;
3195
+ const gain = gainOf(c);
3196
+ if (gain > bestGain + 1e-12) {
3197
+ best = c;
3198
+ bestGain = gain;
3199
+ }
3200
+ }
3201
+ communityTotal[best] += degree[i];
3202
+ if (best !== current) {
3203
+ communityOf[i] = best;
3204
+ movedThisSweep = true;
3205
+ movedAny = true;
3206
+ }
3207
+ }
3208
+ }
3209
+ if (!movedAny) break;
3210
+ const renumber = /* @__PURE__ */ new Map();
3211
+ for (let i = 0; i < count; i++) {
3212
+ const c = communityOf[i];
3213
+ if (!renumber.has(c)) renumber.set(c, renumber.size);
3214
+ }
3215
+ const nextCount = renumber.size;
3216
+ const nextLinks = Array.from({ length: nextCount }, () => /* @__PURE__ */ new Map());
3217
+ const nextSelfLoops = new Array(nextCount).fill(0);
3218
+ for (let i = 0; i < count; i++) {
3219
+ const ci = renumber.get(communityOf[i]);
3220
+ nextSelfLoops[ci] += selfLoops[i];
3221
+ for (const [j, w] of links[i]) {
3222
+ const cj = renumber.get(communityOf[j]);
3223
+ if (ci === cj) nextSelfLoops[ci] += w / 2;
3224
+ else nextLinks[ci].set(cj, (nextLinks[ci].get(cj) ?? 0) + w);
3225
+ }
3226
+ }
3227
+ membership = membership.map((c) => renumber.get(communityOf[c]));
3228
+ links = nextLinks;
3229
+ selfLoops = nextSelfLoops;
3230
+ count = nextCount;
3231
+ if (nextCount === 1) break;
3232
+ }
3233
+ const grouped = /* @__PURE__ */ new Map();
3234
+ for (let i = 0; i < nodeIds.length; i++) {
3235
+ const c = membership[i];
3236
+ if (!grouped.has(c)) grouped.set(c, []);
3237
+ grouped.get(c).push(nodeIds[i]);
3238
+ }
3239
+ return [...grouped.values()].map((ids) => ids.sort((a, b) => a.localeCompare(b))).sort((a, b) => a[0].localeCompare(b[0]));
3240
+ }
3241
+
3242
+ //#endregion
3243
+ //#region src/algorithms/flow.ts
3244
+ /**
3245
+ * Returns the maximum flow from `from` to `to` using the Edmonds-Karp
3246
+ * algorithm (BFS augmenting paths).
3247
+ *
3248
+ * Directed edges carry capacity from source to target only. Edges whose
3249
+ * effective mode is not `'directed'` (undirected/bidirectional) are modeled
3250
+ * as two independent opposite arcs, each with the edge's full capacity.
3251
+ *
3252
+ * The returned `flows` record maps every edge id to its net flow (positive
3253
+ * in the source→target direction). `cutEdges` is a minimum s-t cut: the
3254
+ * edges crossing from the source side to the sink side of the final
3255
+ * residual graph; the sum of their capacities equals `value`.
3256
+ *
3257
+ * @example
3258
+ * ```ts
3259
+ * const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
3260
+ * ```
3261
+ */
3262
+ function getMaxFlow(graph, options) {
3263
+ const { from, to } = options;
3264
+ const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
3265
+ const idx = getIndex(graph);
3266
+ if (!idx.nodeById.has(from)) throw new Error(`getMaxFlow: source node "${from}" not found in graph — pass an existing node id as options.from`);
3267
+ if (!idx.nodeById.has(to)) throw new Error(`getMaxFlow: sink node "${to}" not found in graph — pass an existing node id as options.to`);
3268
+ if (from === to) throw new Error(`getMaxFlow: source and sink are both "${from}" — they must be different nodes`);
3269
+ const arcs = [];
3270
+ const outArcs = /* @__PURE__ */ new Map();
3271
+ for (const node of graph.nodes) outArcs.set(node.id, []);
3272
+ function addArc(u, v, capacity, edgeId, sign) {
3273
+ outArcs.get(u).push(arcs.length);
3274
+ arcs.push({
3275
+ to: v,
3276
+ capacity,
3277
+ flow: 0,
3278
+ edgeId,
3279
+ sign
3280
+ });
3281
+ outArcs.get(v).push(arcs.length);
3282
+ arcs.push({
3283
+ to: u,
3284
+ capacity: 0,
3285
+ flow: 0
3286
+ });
3287
+ }
3288
+ for (const edge of graph.edges) {
3289
+ const capacity = getCapacity(edge);
3290
+ if (capacity < 0) throw new Error(`getMaxFlow: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
3291
+ if (edge.sourceId === edge.targetId) continue;
3292
+ addArc(edge.sourceId, edge.targetId, capacity, edge.id, 1);
3293
+ if (getEdgeMode(graph, edge) !== "directed") addArc(edge.targetId, edge.sourceId, capacity, edge.id, -1);
3294
+ }
3295
+ function residual(arcIndex) {
3296
+ return arcs[arcIndex].capacity - arcs[arcIndex].flow;
3297
+ }
3298
+ let value = 0;
3299
+ while (true) {
3300
+ const parentArc = /* @__PURE__ */ new Map();
3301
+ const queue$1 = [from];
3302
+ const visited = new Set([from]);
3303
+ while (queue$1.length > 0 && !visited.has(to)) {
3304
+ const u = queue$1.shift();
3305
+ for (const ai of outArcs.get(u) ?? []) {
3306
+ const arc = arcs[ai];
3307
+ if (residual(ai) > 0 && !visited.has(arc.to)) {
3308
+ visited.add(arc.to);
3309
+ parentArc.set(arc.to, ai);
3310
+ queue$1.push(arc.to);
3311
+ }
3312
+ }
3313
+ }
3314
+ if (!visited.has(to)) break;
3315
+ let bottleneck = Infinity;
3316
+ for (let v = to; v !== from;) {
3317
+ const ai = parentArc.get(v);
3318
+ bottleneck = Math.min(bottleneck, residual(ai));
3319
+ v = arcs[ai ^ 1].to;
3320
+ }
3321
+ if (bottleneck === Infinity || bottleneck <= 0) break;
3322
+ for (let v = to; v !== from;) {
3323
+ const ai = parentArc.get(v);
3324
+ arcs[ai].flow += bottleneck;
3325
+ arcs[ai ^ 1].flow -= bottleneck;
3326
+ v = arcs[ai ^ 1].to;
3327
+ }
3328
+ value += bottleneck;
3329
+ }
3330
+ const flows = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
3331
+ for (const arc of arcs) if (arc.edgeId !== void 0 && arc.flow > 0) flows[arc.edgeId] += arc.sign * arc.flow;
3332
+ const sourceSide = new Set([from]);
3333
+ const queue = [from];
3334
+ while (queue.length > 0) {
3335
+ const u = queue.shift();
3336
+ for (const ai of outArcs.get(u) ?? []) {
3337
+ const arc = arcs[ai];
3338
+ if (residual(ai) > 0 && !sourceSide.has(arc.to)) {
3339
+ sourceSide.add(arc.to);
3340
+ queue.push(arc.to);
3341
+ }
3342
+ }
3343
+ }
3344
+ const cutEdgeIds = /* @__PURE__ */ new Set();
3345
+ for (let ai = 0; ai < arcs.length; ai++) {
3346
+ const arc = arcs[ai];
3347
+ if (arc.edgeId === void 0) continue;
3348
+ const arcFrom = arcs[ai ^ 1].to;
3349
+ if (sourceSide.has(arcFrom) && !sourceSide.has(arc.to)) cutEdgeIds.add(arc.edgeId);
3350
+ }
3351
+ const cutEdges = graph.edges.filter((edge) => cutEdgeIds.has(edge.id));
3352
+ return {
3353
+ value,
3354
+ flows,
3355
+ cutEdges
3356
+ };
3357
+ }
3358
+
3359
+ //#endregion
3360
+ //#region src/algorithms/dominators.ts
3361
+ /**
3362
+ * Returns the dominator tree of the graph rooted at `from`, computed with
3363
+ * the Cooper–Harvey–Kennedy iterative algorithm.
3364
+ *
3365
+ * Each reachable node maps to its immediate dominator's id; the root maps
3366
+ * to `null`. Unreachable nodes are omitted. Traversal is mode-aware:
3367
+ * undirected/bidirectional edges are traversable both ways.
3368
+ *
3369
+ * For statecharts this answers "which states must every path from the
3370
+ * initial state pass through to reach this state?" — node `d` dominates
3371
+ * node `n` when every path from the initial state to `n` goes through `d`.
3372
+ *
3373
+ * @example
3374
+ * ```ts
3375
+ * // a→b, a→c, b→d, c→d (diamond)
3376
+ * getDominatorTree(graph, { from: 'a' });
3377
+ * // { a: null, b: 'a', c: 'a', d: 'a' }
3378
+ * ```
3379
+ */
3380
+ function getDominatorTree(graph, options) {
3381
+ const root = resolveFrom(graph, options);
3382
+ if (!getIndex(graph).nodeById.has(root)) throw new Error(`getDominatorTree: root node "${root}" not found in graph — pass an existing node id as options.from`);
3383
+ const postorder = [];
3384
+ const visited = new Set([root]);
3385
+ const stack = [{
3386
+ id: root,
3387
+ neighborIndex: 0
3388
+ }];
3389
+ const successors = /* @__PURE__ */ new Map();
3390
+ function getSuccessors(id) {
3391
+ let succ = successors.get(id);
3392
+ if (!succ) {
3393
+ succ = getNeighborEdges(graph, id).map((entry) => entry.neighborId);
3394
+ successors.set(id, succ);
3395
+ }
3396
+ return succ;
3397
+ }
3398
+ while (stack.length > 0) {
3399
+ const frame = stack[stack.length - 1];
3400
+ const succ = getSuccessors(frame.id);
3401
+ if (frame.neighborIndex < succ.length) {
3402
+ const next = succ[frame.neighborIndex++];
3403
+ if (!visited.has(next)) {
3404
+ visited.add(next);
3405
+ stack.push({
3406
+ id: next,
3407
+ neighborIndex: 0
3408
+ });
3409
+ }
3410
+ } else {
3411
+ postorder.push(frame.id);
3412
+ stack.pop();
3413
+ }
3414
+ }
3415
+ const rpo = [...postorder].reverse();
3416
+ const rpoNumber = new Map(rpo.map((id, i) => [id, i]));
3417
+ const predecessors = new Map(rpo.map((id) => [id, []]));
3418
+ for (const id of rpo) for (const succ of getSuccessors(id)) if (visited.has(succ)) predecessors.get(succ).push(id);
3419
+ const idom = /* @__PURE__ */ new Map();
3420
+ idom.set(root, root);
3421
+ function intersect(a, b) {
3422
+ let f1 = a;
3423
+ let f2 = b;
3424
+ while (f1 !== f2) {
3425
+ while (rpoNumber.get(f1) > rpoNumber.get(f2)) f1 = idom.get(f1);
3426
+ while (rpoNumber.get(f2) > rpoNumber.get(f1)) f2 = idom.get(f2);
3427
+ }
3428
+ return f1;
3429
+ }
3430
+ let changed = true;
3431
+ while (changed) {
3432
+ changed = false;
3433
+ for (const id of rpo) {
3434
+ if (id === root) continue;
3435
+ let newIdom;
3436
+ for (const pred of predecessors.get(id)) {
3437
+ if (!idom.has(pred)) continue;
3438
+ newIdom = newIdom === void 0 ? pred : intersect(pred, newIdom);
3439
+ }
3440
+ if (newIdom !== void 0 && idom.get(id) !== newIdom) {
3441
+ idom.set(id, newIdom);
3442
+ changed = true;
3443
+ }
3444
+ }
3445
+ }
3446
+ const result = {};
3447
+ for (const id of rpo) result[id] = id === root ? null : idom.get(id);
3448
+ return result;
3449
+ }
3450
+
3451
+ //#endregion
3452
+ //#region src/algorithms/reduction.ts
3453
+ /**
3454
+ * Returns a new graph with all transitively-redundant edges removed (the
3455
+ * transitive reduction). The input graph is not mutated; nodes and surviving
3456
+ * edges keep all of their fields.
3457
+ *
3458
+ * An edge u→v is removed when v is also reachable from u via a path of
3459
+ * length ≥ 2. Exact-duplicate parallel edges u→v collapse to the first one
3460
+ * in `graph.edges` order (a duplicate adds no reachability, so at most one
3461
+ * edge per (u, v) pair survives).
3462
+ *
3463
+ * DAG-only: throws when the graph contains a cycle or any edge whose
3464
+ * effective mode is not `'directed'`.
3465
+ *
3466
+ * @example
3467
+ * ```ts
3468
+ * // a→b, b→c, a→c
3469
+ * const reduced = getTransitiveReduction(graph);
3470
+ * // a→c removed; edges: a→b, b→c
3471
+ * ```
3472
+ */
3473
+ function getTransitiveReduction(graph) {
3474
+ for (const edge of graph.edges) {
3475
+ const mode = getEdgeMode(graph, edge);
3476
+ if (mode !== "directed") throw new Error(`getTransitiveReduction: edge "${edge.id}" has effective mode "${mode}" — transitive reduction is only defined for directed acyclic graphs. Set edge.mode (or graph.mode) to 'directed'.`);
3477
+ }
3478
+ if (getEffectiveModeKind(graph) === "directed" && !isAcyclic(graph)) throw new Error("getTransitiveReduction: the graph contains a cycle — transitive reduction is only defined for directed acyclic graphs. Remove the cycle (see getCycles) first.");
3479
+ const successorsOf = /* @__PURE__ */ new Map();
3480
+ for (const node of graph.nodes) successorsOf.set(node.id, /* @__PURE__ */ new Set());
3481
+ for (const edge of graph.edges) successorsOf.get(edge.sourceId)?.add(edge.targetId);
3482
+ const reach = /* @__PURE__ */ new Map();
3483
+ function getReach(id) {
3484
+ let set = reach.get(id);
3485
+ if (set) return set;
3486
+ set = new Set([id]);
3487
+ reach.set(id, set);
3488
+ for (const succ of successorsOf.get(id) ?? []) for (const reached of getReach(succ)) set.add(reached);
3489
+ return set;
3490
+ }
3491
+ function isRedundant(u, v) {
3492
+ for (const w of successorsOf.get(u)) if (w !== v && getReach(w).has(v)) return true;
3493
+ return false;
3494
+ }
3495
+ const keptPairs = /* @__PURE__ */ new Set();
3496
+ const keptEdges = [];
3497
+ for (const edge of graph.edges) {
3498
+ const pair = `${edge.sourceId}${edge.targetId}`;
3499
+ if (keptPairs.has(pair)) continue;
3500
+ if (isRedundant(edge.sourceId, edge.targetId)) continue;
3501
+ keptPairs.add(pair);
3502
+ keptEdges.push(edge);
3503
+ }
3504
+ return createGraph({
3505
+ id: graph.id,
3506
+ mode: graph.mode,
3507
+ initialNodeId: graph.initialNodeId ?? void 0,
3508
+ data: graph.data ?? void 0,
3509
+ direction: graph.direction,
3510
+ style: graph.style,
3511
+ nodes: graph.nodes.map((node) => {
3512
+ const { type, parentId, initialNodeId, ...rest } = node;
3513
+ return {
3514
+ ...rest,
3515
+ parentId: parentId ?? void 0,
3516
+ initialNodeId: initialNodeId ?? void 0
3517
+ };
3518
+ }),
3519
+ edges: keptEdges.map((edge) => {
3520
+ const { type, ...rest } = edge;
3521
+ return rest;
3522
+ })
3523
+ });
3524
+ }
3525
+
3526
+ //#endregion
3527
+ export { joinPaths as $, dfs as A, toEdgeConfig as B, genPostorders as C, getPreorder as D, getPostorders as E, isConnected as F, getAStarPath as G, genCycles as H, isTree as I, getShortestPath as J, getAllPairsShortestPaths as K, flatten as L, getTopologicalSort as M, hasPath as N, getPreorders as O, isAcyclic as P, getStronglyConnectedComponents as Q, getSubgraph as R, getMinimumSpanningTree as S, getPostorder as T, genShortestPaths as U, toNodeConfig as V, genSimplePaths as W, getSimplePath as X, getShortestPaths as Y, getSimplePaths as Z, getEigenvectorCentrality as _, updateEdge as _t, isIsomorphic as a, createGraphEdge as at, getOutDegreeCentrality as b, getBridges as c, createGraphPort as ct, getGreedyModularityCommunities as d, deleteEntities as dt, GraphInstance as et, getLabelPropagationCommunities as f, deleteNode as ft, getDegreeCentrality as g, hasNode as gt, getClosenessCentrality as h, hasEdge as ht, getLouvainCommunities as i, createGraph as it, getConnectedComponents as j, bfs as k, genGirvanNewmanCommunities as l, createVisualGraph as lt, getBetweennessCentrality as m, getNode as mt, getDominatorTree as n, addEntities as nt, getArticulationPoints as o, createGraphFromTransition as ot, getModularity as p, getEdge as pt, getCycles as q, getMaxFlow as r, addNode as rt, getBiconnectedComponents as s, createGraphNode as st, getTransitiveReduction as t, addEdge as tt, getGirvanNewmanCommunities as u, deleteEdge as ut, getHITS as v, updateEntities as vt, genPreorders as w, getPageRank as x, getInDegreeCentrality as y, updateNode as yt, reverseGraph as z };