@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
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { o as invalidateIndex, t as getIndex } from "./indexing-DR8M1vBy.mjs";
2
- import { $ as createGraphPort, A as isAcyclic, B as getShortestPaths, C as getPreorder, D as getConnectedComponents, E as dfs, F as genSimplePaths, G as GraphInstance, H as getSimplePaths, I as getAStarPath, J as addNode, K as addEdge, L as getAllPairsShortestPaths, M as isTree, N as genCycles, O as getTopologicalSort, P as genShortestPaths, Q as createGraphNode, R as getCycles, S as getPostorders, T as bfs, U as getStronglyConnectedComponents, V as getSimplePath, W as joinPaths, X as createGraphEdge, Y as createGraph, Z as createGraphFromTransition, _ as getPageRank, a as genGirvanNewmanCommunities, at as getNode, b as genPreorders, c as getLabelPropagationCommunities, ct as updateEdge, d as getClosenessCentrality, et as createVisualGraph, f as getDegreeCentrality, g as getOutDegreeCentrality, h as getInDegreeCentrality, i as getBridges, it as getEdge, j as isConnected, k as hasPath, l as getModularity, lt as updateEntities, m as getHITS, n as getArticulationPoints, nt as deleteEntities, o as getGirvanNewmanCommunities, ot as hasEdge, p as getEigenvectorCentrality, q as addEntities, r as getBiconnectedComponents, rt as deleteNode, s as getGreedyModularityCommunities, st as hasNode, t as isIsomorphic, tt as deleteEdge, u as getBetweennessCentrality, ut as updateNode, v as getMinimumSpanningTree, w as getPreorders, x as getPostorder, y as genPostorders, z as getShortestPath } from "./algorithms-Ba7o7niK.mjs";
1
+ import { C as getSinks, D as isLeaf, E as isCompound, N as invalidateIndex, S as getSiblings, T as getSuccessors, _ as getPorts, a as getDescendants, b as getRelativeDistanceMap, c as getEdgesOf, d as getLCA, f as getNeighbors, g as getPort, h as getParent, i as getDepth, l as getInDegree, m as getOutEdges, n as getChildren, o as getEdgesBetween, p as getOutDegree, r as getDegree, s as getEdgesByPort, t as getAncestors, u as getInEdges, v as getPredecessors, w as getSources, x as getRoots, y as getRelativeDistance } from "./queries-KirMDR7e.mjs";
2
+ import { $ as joinPaths, A as dfs, B as toEdgeConfig, C as genPostorders, D as getPreorder, E as getPostorders, F as isConnected, G as getAStarPath, H as genCycles, I as isTree, J as getShortestPath, K as getAllPairsShortestPaths, L as flatten, M as getTopologicalSort, N as hasPath, O as getPreorders, P as isAcyclic, Q as getStronglyConnectedComponents, R as getSubgraph, S as getMinimumSpanningTree, T as getPostorder, U as genShortestPaths, V as toNodeConfig, W as genSimplePaths, X as getSimplePath, Y as getShortestPaths, Z as getSimplePaths, _ as getEigenvectorCentrality, _t as updateEdge, a as isIsomorphic, at as createGraphEdge, b as getOutDegreeCentrality, c as getBridges, ct as createGraphPort, d as getGreedyModularityCommunities, dt as deleteEntities, et as GraphInstance, f as getLabelPropagationCommunities, ft as deleteNode, g as getDegreeCentrality, gt as hasNode, h as getClosenessCentrality, ht as hasEdge, i as getLouvainCommunities, it as createGraph, j as getConnectedComponents, k as bfs, l as genGirvanNewmanCommunities, lt as createVisualGraph, m as getBetweennessCentrality, mt as getNode, n as getDominatorTree, nt as addEntities, o as getArticulationPoints, ot as createGraphFromTransition, p as getModularity, pt as getEdge, q as getCycles, r as getMaxFlow, rt as addNode, s as getBiconnectedComponents, st as createGraphNode, t as getTransitiveReduction, tt as addEdge, u as getGirvanNewmanCommunities, ut as deleteEdge, v as getHITS, vt as updateEntities, w as genPreorders, x as getPageRank, y as getInDegreeCentrality, yt as updateNode, z as reverseGraph } from "./algorithms-DF1pSQGv.mjs";
3
3
  import { n as isEdgeDirected, t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
4
- import { getAncestors, getChildren, getDegree, getDepth, getDescendants, getEdgesBetween, getEdgesByPort, getEdgesOf, getInDegree, getInEdges, getLCA, getNeighbors, getOutDegree, getOutEdges, getParent, getPort, getPorts, getPredecessors, getRelativeDistance, getRelativeDistanceMap, getRoots, getSiblings, getSinks, getSources, getSuccessors, isCompound, isLeaf } from "./queries.mjs";
5
- import { n as createFormatConverter } from "./converter-udLITX36.mjs";
4
+ import { t as getGraphIssues } from "./validate-TtH-x3JV.mjs";
5
+ import { n as createFormatConverter } from "./converter-DyCJJfTe.mjs";
6
6
 
7
7
  //#region src/equivalence.ts
8
8
  /** Shallow-compare two values, returning true if they differ. */
@@ -52,7 +52,7 @@ const LAYOUT_KEY_SET = {
52
52
  * ```
53
53
  */
54
54
  function areEntitiesEqual(a, b, keys) {
55
- const compareKeys = keys && keys.length > 0 ? keys : Object.keys(a);
55
+ const compareKeys = keys && keys.length > 0 ? keys : [...new Set([...Object.keys(a), ...Object.keys(b)])];
56
56
  for (const key of compareKeys) if (differs$1(a[key], b[key])) return false;
57
57
  return true;
58
58
  }
@@ -87,47 +87,16 @@ function isLayoutEqual(a, b) {
87
87
  */
88
88
  function isNonLayoutEqual(a, b) {
89
89
  const skip = LAYOUT_KEY_SET[a.type];
90
- const keys = Object.keys(a);
91
- for (let i = 0; i < keys.length; i++) {
92
- if (skip.has(keys[i])) continue;
93
- if (differs$1(a[keys[i]], b[keys[i]])) return false;
90
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
91
+ for (const key of keys) {
92
+ if (skip.has(key)) continue;
93
+ if (differs$1(a[key], b[key])) return false;
94
94
  }
95
95
  return true;
96
96
  }
97
97
 
98
98
  //#endregion
99
99
  //#region src/diff.ts
100
- function nodeToConfig$1(node) {
101
- const config = { id: node.id };
102
- if (node.parentId) config.parentId = node.parentId;
103
- if (node.initialNodeId) config.initialNodeId = node.initialNodeId;
104
- if (node.label !== "") config.label = node.label;
105
- if (node.data !== void 0) config.data = node.data;
106
- if (node.x !== void 0) config.x = node.x;
107
- if (node.y !== void 0) config.y = node.y;
108
- if (node.width !== void 0) config.width = node.width;
109
- if (node.height !== void 0) config.height = node.height;
110
- if (node.shape !== void 0) config.shape = node.shape;
111
- if (node.color !== void 0) config.color = node.color;
112
- if (node.style !== void 0) config.style = node.style;
113
- return config;
114
- }
115
- function edgeToConfig$1(edge) {
116
- const config = {
117
- id: edge.id,
118
- sourceId: edge.sourceId,
119
- targetId: edge.targetId
120
- };
121
- if (edge.label !== "") config.label = edge.label;
122
- if (edge.data !== void 0) config.data = edge.data;
123
- if (edge.x !== void 0) config.x = edge.x;
124
- if (edge.y !== void 0) config.y = edge.y;
125
- if (edge.width !== void 0) config.width = edge.width;
126
- if (edge.height !== void 0) config.height = edge.height;
127
- if (edge.color !== void 0) config.color = edge.color;
128
- if (edge.style !== void 0) config.style = edge.style;
129
- return config;
130
- }
131
100
  /** Shallow-compare two values, returning true if they differ. */
132
101
  function differs(a, b) {
133
102
  if (a === b) return false;
@@ -140,6 +109,7 @@ const NODE_COMPARE_KEYS = [
140
109
  "initialNodeId",
141
110
  "label",
142
111
  "data",
112
+ "ports",
143
113
  "x",
144
114
  "y",
145
115
  "width",
@@ -153,6 +123,10 @@ const EDGE_COMPARE_KEYS = [
153
123
  "targetId",
154
124
  "label",
155
125
  "data",
126
+ "weight",
127
+ "mode",
128
+ "sourcePort",
129
+ "targetPort",
156
130
  "x",
157
131
  "y",
158
132
  "width",
@@ -194,13 +168,17 @@ function getDiff(a, b) {
194
168
  };
195
169
  for (const [id, nodeB] of bNodeMap) {
196
170
  const nodeA = aNodeMap.get(id);
197
- if (!nodeA) diff.nodes.added.push(nodeToConfig$1(nodeB));
171
+ if (!nodeA) diff.nodes.added.push(toNodeConfig(nodeB));
198
172
  else {
199
173
  const oldPartial = {};
200
174
  const newPartial = {};
201
- for (const key of NODE_COMPARE_KEYS) if (differs(nodeA[key], nodeB[key])) {
202
- oldPartial[key] = nodeA[key];
203
- newPartial[key] = nodeB[key];
175
+ for (const key of NODE_COMPARE_KEYS) {
176
+ const oldValue = nodeA[key] ?? null;
177
+ const newValue = nodeB[key] ?? null;
178
+ if (differs(oldValue, newValue)) {
179
+ oldPartial[key] = oldValue;
180
+ newPartial[key] = newValue;
181
+ }
204
182
  }
205
183
  if (Object.keys(oldPartial).length > 0) diff.nodes.updated.push({
206
184
  id,
@@ -209,16 +187,20 @@ function getDiff(a, b) {
209
187
  });
210
188
  }
211
189
  }
212
- for (const [id, nodeA] of aNodeMap) if (!bNodeMap.has(id)) diff.nodes.removed.push(nodeToConfig$1(nodeA));
190
+ for (const [id, nodeA] of aNodeMap) if (!bNodeMap.has(id)) diff.nodes.removed.push(toNodeConfig(nodeA));
213
191
  for (const [id, edgeB] of bEdgeMap) {
214
192
  const edgeA = aEdgeMap.get(id);
215
- if (!edgeA) diff.edges.added.push(edgeToConfig$1(edgeB));
193
+ if (!edgeA) diff.edges.added.push(toEdgeConfig(edgeB));
216
194
  else {
217
195
  const oldPartial = {};
218
196
  const newPartial = {};
219
- for (const key of EDGE_COMPARE_KEYS) if (differs(edgeA[key], edgeB[key])) {
220
- oldPartial[key] = edgeA[key];
221
- newPartial[key] = edgeB[key];
197
+ for (const key of EDGE_COMPARE_KEYS) {
198
+ const oldValue = edgeA[key] ?? null;
199
+ const newValue = edgeB[key] ?? null;
200
+ if (differs(oldValue, newValue)) {
201
+ oldPartial[key] = oldValue;
202
+ newPartial[key] = newValue;
203
+ }
222
204
  }
223
205
  if (Object.keys(oldPartial).length > 0) diff.edges.updated.push({
224
206
  id,
@@ -227,7 +209,7 @@ function getDiff(a, b) {
227
209
  });
228
210
  }
229
211
  }
230
- for (const [id, edgeA] of aEdgeMap) if (!bEdgeMap.has(id)) diff.edges.removed.push(edgeToConfig$1(edgeA));
212
+ for (const [id, edgeA] of aEdgeMap) if (!bEdgeMap.has(id)) diff.edges.removed.push(toEdgeConfig(edgeA));
231
213
  return diff;
232
214
  }
233
215
  /**
@@ -264,28 +246,29 @@ function isEmptyDiff(diff) {
264
246
  function invertDiff(diff) {
265
247
  return {
266
248
  nodes: {
267
- added: diff.nodes.removed,
268
- removed: diff.nodes.added,
249
+ added: diff.nodes.removed.map((c) => structuredClone(c)),
250
+ removed: diff.nodes.added.map((c) => structuredClone(c)),
269
251
  updated: diff.nodes.updated.map((c) => ({
270
252
  id: c.id,
271
- old: c.new,
272
- new: c.old
253
+ old: structuredClone(c.new),
254
+ new: structuredClone(c.old)
273
255
  }))
274
256
  },
275
257
  edges: {
276
- added: diff.edges.removed,
277
- removed: diff.edges.added,
258
+ added: diff.edges.removed.map((c) => structuredClone(c)),
259
+ removed: diff.edges.added.map((c) => structuredClone(c)),
278
260
  updated: diff.edges.updated.map((c) => ({
279
261
  id: c.id,
280
- old: c.new,
281
- new: c.old
262
+ old: structuredClone(c.new),
263
+ new: structuredClone(c.old)
282
264
  }))
283
265
  }
284
266
  };
285
267
  }
286
268
  /**
287
269
  * Compute an ordered patch list from graph `a` to graph `b`.
288
- * Order: delete edges delete nodes → add nodes → add edges → update nodes update edges.
270
+ * Order (see {@link toPatches}): add nodes → update edges → delete edges
271
+ * delete nodes → add edges → update nodes.
289
272
  *
290
273
  * @example
291
274
  * ```ts
@@ -365,7 +348,7 @@ function toPatches(diff) {
365
348
  });
366
349
  for (const change of diff.edges.updated) {
367
350
  const data = {};
368
- for (const [key, value] of Object.entries(change.new)) data[key] = value;
351
+ for (const [key, value] of Object.entries(change.new)) data[key] = value ?? null;
369
352
  patches.push({
370
353
  op: "updateEdge",
371
354
  id: change.id,
@@ -386,7 +369,7 @@ function toPatches(diff) {
386
369
  });
387
370
  for (const change of diff.nodes.updated) {
388
371
  const data = {};
389
- for (const [key, value] of Object.entries(change.new)) data[key] = value;
372
+ for (const [key, value] of Object.entries(change.new)) data[key] = value ?? null;
390
373
  patches.push({
391
374
  op: "updateNode",
392
375
  id: change.id,
@@ -460,201 +443,6 @@ function toDiff(patches) {
460
443
  return diff;
461
444
  }
462
445
 
463
- //#endregion
464
- //#region src/transforms.ts
465
- /**
466
- * Flattens a hierarchical graph into a flat graph with only leaf nodes.
467
- *
468
- * - Edges targeting a compound node resolve to its initial child (recursively).
469
- * - Edges originating from a compound node expand to all leaf descendants.
470
- * - Only leaf nodes (nodes with no children) appear in the result.
471
- * - Duplicate edges (same source + target) are deduplicated.
472
- *
473
- * @example
474
- * ```ts
475
- * import { createGraph, flatten } from '@statelyai/graph';
476
- *
477
- * const graph = createGraph({
478
- * nodes: [
479
- * { id: 'parent', initialNodeId: 'child1' },
480
- * { id: 'child1', parentId: 'parent' },
481
- * { id: 'child2', parentId: 'parent' },
482
- * { id: 'other' },
483
- * ],
484
- * edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
485
- * });
486
- *
487
- * const flat = flatten(graph);
488
- * // flat.nodes → [child1, child2, other] (leaf nodes only)
489
- * // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
490
- * ```
491
- */
492
- function flatten(graph) {
493
- const idx = getIndex(graph);
494
- const leaves = /* @__PURE__ */ new Set();
495
- for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
496
- function resolveInitial(nodeId) {
497
- if (leaves.has(nodeId)) return nodeId;
498
- const ni = idx.nodeById.get(nodeId);
499
- if (ni === void 0) return null;
500
- const node = graph.nodes[ni];
501
- if (node.initialNodeId) return resolveInitial(node.initialNodeId);
502
- const childIds = idx.childNodes.get(nodeId) ?? [];
503
- if (childIds.length > 0) return resolveInitial(childIds[0]);
504
- return nodeId;
505
- }
506
- function getLeafDescendants(nodeId) {
507
- if (leaves.has(nodeId)) return [nodeId];
508
- const result = [];
509
- const collect = (id) => {
510
- const childIds = idx.childNodes.get(id) ?? [];
511
- for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
512
- else collect(childId);
513
- };
514
- collect(nodeId);
515
- return result;
516
- }
517
- const edgeSeen = /* @__PURE__ */ new Set();
518
- const flatEdges = [];
519
- for (const edge of graph.edges) {
520
- const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
521
- const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
522
- if (target === null) continue;
523
- for (const source of sources) {
524
- if (source === target) continue;
525
- const key = `${source}->${target}`;
526
- if (edgeSeen.has(key)) continue;
527
- edgeSeen.add(key);
528
- flatEdges.push({
529
- type: "edge",
530
- id: `${edge.id}:${source}->${target}`,
531
- sourceId: source,
532
- targetId: target,
533
- label: edge.label,
534
- data: edge.data
535
- });
536
- }
537
- }
538
- const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => ({
539
- id: n.id,
540
- label: n.label,
541
- data: n.data
542
- }));
543
- return createGraph({
544
- id: graph.id,
545
- mode: graph.mode,
546
- nodes: leafNodes,
547
- edges: flatEdges,
548
- data: graph.data
549
- });
550
- }
551
- function nodeToConfig(node, nodeIdSet) {
552
- const config = {
553
- id: node.id,
554
- label: node.label,
555
- data: node.data
556
- };
557
- if (node.parentId !== void 0 && node.parentId !== null) config.parentId = nodeIdSet && !nodeIdSet.has(node.parentId) ? void 0 : node.parentId;
558
- if (node.initialNodeId !== void 0) config.initialNodeId = node.initialNodeId ?? void 0;
559
- if (node.x !== void 0) config.x = node.x;
560
- if (node.y !== void 0) config.y = node.y;
561
- if (node.width !== void 0) config.width = node.width;
562
- if (node.height !== void 0) config.height = node.height;
563
- if (node.shape !== void 0) config.shape = node.shape;
564
- if (node.color !== void 0) config.color = node.color;
565
- if (node.style !== void 0) config.style = node.style;
566
- return config;
567
- }
568
- function edgeToConfig(edge) {
569
- const config = {
570
- id: edge.id,
571
- sourceId: edge.sourceId,
572
- targetId: edge.targetId,
573
- label: edge.label,
574
- data: edge.data
575
- };
576
- if (edge.weight !== void 0) config.weight = edge.weight;
577
- if (edge.x !== void 0) config.x = edge.x;
578
- if (edge.y !== void 0) config.y = edge.y;
579
- if (edge.width !== void 0) config.width = edge.width;
580
- if (edge.height !== void 0) config.height = edge.height;
581
- if (edge.color !== void 0) config.color = edge.color;
582
- if (edge.style !== void 0) config.style = edge.style;
583
- return config;
584
- }
585
- /**
586
- * Returns the induced subgraph containing only the given node IDs
587
- * and edges whose endpoints are both in the set.
588
- *
589
- * Parent references to nodes outside the set are removed.
590
- *
591
- * @example
592
- * ```ts
593
- * import { createGraph, getSubgraph } from '@statelyai/graph';
594
- *
595
- * const graph = createGraph({
596
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
597
- * edges: [
598
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
599
- * { id: 'bc', sourceId: 'b', targetId: 'c' },
600
- * ],
601
- * });
602
- *
603
- * const sub = getSubgraph(graph, ['a', 'b']);
604
- * // sub.nodes: [a, b], sub.edges: [ab]
605
- * ```
606
- */
607
- function getSubgraph(graph, nodeIds) {
608
- const nodeIdSet = new Set(nodeIds);
609
- return createGraph({
610
- id: graph.id,
611
- mode: graph.mode,
612
- initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
613
- nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => nodeToConfig(n, nodeIdSet)),
614
- edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(edgeToConfig),
615
- data: graph.data
616
- });
617
- }
618
- /**
619
- * Returns a new graph with all edge directions flipped (source ↔ target).
620
- * Optionally filters which edges to include.
621
- *
622
- * @example
623
- * ```ts
624
- * import { createGraph, reverseGraph } from '@statelyai/graph';
625
- *
626
- * const graph = createGraph({
627
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
628
- * edges: [
629
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
630
- * { id: 'bc', sourceId: 'b', targetId: 'c' },
631
- * ],
632
- * });
633
- *
634
- * const rev = reverseGraph(graph);
635
- * // rev edges: b→a, c→b
636
- *
637
- * const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
638
- * // filtered edges: b→a (only ab reversed, bc excluded)
639
- * ```
640
- */
641
- function reverseGraph(graph, filterEdge) {
642
- const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
643
- return createGraph({
644
- id: graph.id,
645
- mode: graph.mode,
646
- initialNodeId: graph.initialNodeId ?? void 0,
647
- nodes: graph.nodes.map((n) => nodeToConfig(n)),
648
- edges: edges.map((e) => {
649
- const config = edgeToConfig(e);
650
- config.sourceId = e.targetId;
651
- config.targetId = e.sourceId;
652
- return config;
653
- }),
654
- data: graph.data
655
- });
656
- }
657
-
658
446
  //#endregion
659
447
  //#region src/walks.ts
660
448
  function mulberry32(seed) {
@@ -680,7 +468,24 @@ function resolveFrom(graph, from) {
680
468
  throw new Error("Cannot determine start node: provide `from`, set graph.initialNodeId, or have exactly one source node.");
681
469
  }
682
470
  /**
683
- * Random walk. At each node, picks a uniformly random outgoing edge.
471
+ * Edges traversable from a node, with the node reached by taking each one.
472
+ * Out-edges always; in-edges too when their effective mode is not 'directed'.
473
+ */
474
+ function getTraversableEdges(graph, nodeId) {
475
+ const result = [];
476
+ for (const edge of getOutEdges(graph, nodeId)) result.push({
477
+ edge,
478
+ nextId: edge.targetId
479
+ });
480
+ for (const edge of getInEdges(graph, nodeId)) if (edge.sourceId !== edge.targetId && getEdgeMode(graph, edge) !== "directed") result.push({
481
+ edge,
482
+ nextId: edge.sourceId
483
+ });
484
+ return result;
485
+ }
486
+ /**
487
+ * Random walk. At each node, picks a uniformly random traversable edge
488
+ * (outgoing edges, plus non-directed edges both ways).
684
489
  * Yields steps indefinitely (may revisit nodes) until a sink node is reached.
685
490
  */
686
491
  function* genRandomWalk(graph, options) {
@@ -693,11 +498,11 @@ function* genRandomWalk(graph, options) {
693
498
  stepCount: 0
694
499
  };
695
500
  while (true) {
696
- let edges = getOutEdges(graph, currentId);
697
- if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
698
- if (edges.length === 0) return;
699
- const edge = edges[Math.floor(rng() * edges.length)];
700
- const node = getNode(graph, edge.targetId);
501
+ let traversable = getTraversableEdges(graph, currentId);
502
+ if (options?.filter) traversable = traversable.filter(({ edge: edge$1 }) => options.filter(edge$1, ctx));
503
+ if (traversable.length === 0) return;
504
+ const { edge, nextId } = traversable[Math.floor(rng() * traversable.length)];
505
+ const node = getNode(graph, nextId);
701
506
  const step = {
702
507
  edge,
703
508
  node
@@ -725,30 +530,30 @@ function* genWeightedRandomWalk(graph, options) {
725
530
  stepCount: 0
726
531
  };
727
532
  while (true) {
728
- let edges = getOutEdges(graph, currentId);
729
- if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
730
- if (edges.length === 0) return;
731
- const weights = edges.map((e) => Math.max(0, getWeight(e)));
533
+ let traversable = getTraversableEdges(graph, currentId);
534
+ if (options?.filter) traversable = traversable.filter(({ edge }) => options.filter(edge, ctx));
535
+ if (traversable.length === 0) return;
536
+ const weights = traversable.map(({ edge }) => Math.max(0, getWeight(edge)));
732
537
  const total = weights.reduce((a, b) => a + b, 0);
733
538
  if (total === 0) return;
734
539
  let r = rng() * total;
735
- let chosen = edges[0];
736
- for (let i = 0; i < edges.length; i++) {
540
+ let chosen = traversable[0];
541
+ for (let i = 0; i < traversable.length; i++) {
737
542
  r -= weights[i];
738
543
  if (r <= 0) {
739
- chosen = edges[i];
544
+ chosen = traversable[i];
740
545
  break;
741
546
  }
742
547
  }
743
- const node = getNode(graph, chosen.targetId);
548
+ const node = getNode(graph, chosen.nextId);
744
549
  const step = {
745
- edge: chosen,
550
+ edge: chosen.edge,
746
551
  node
747
552
  };
748
553
  currentId = node.id;
749
554
  ctx.currentNodeId = currentId;
750
555
  ctx.visitedNodes.add(currentId);
751
- ctx.visitedEdges.add(chosen.id);
556
+ ctx.visitedEdges.add(chosen.edge.id);
752
557
  ctx.stepCount++;
753
558
  options?.onStep?.(step, ctx);
754
559
  yield step;
@@ -756,8 +561,9 @@ function* genWeightedRandomWalk(graph, options) {
756
561
  }
757
562
  /**
758
563
  * Quick random walk targeting unvisited edges.
759
- * If unvisited outgoing edges exist, picks one randomly.
760
- * Otherwise, finds shortest path to a node with unvisited outgoing edges.
564
+ * If unvisited traversable edges exist at the current node, picks one randomly.
565
+ * Otherwise, walks the fewest-hop path (BFS, honoring `filter` and edge modes)
566
+ * to the nearest unvisited edge. Ends when no unvisited edge is reachable.
761
567
  */
762
568
  function* genQuickRandomWalk(graph, options) {
763
569
  const rng = makeRng(options?.seed);
@@ -770,13 +576,16 @@ function* genQuickRandomWalk(graph, options) {
770
576
  visitedEdges,
771
577
  stepCount: 0
772
578
  };
579
+ const allowedEdges = (nodeId) => {
580
+ let traversable = getTraversableEdges(graph, nodeId);
581
+ if (options?.filter) traversable = traversable.filter(({ edge }) => options.filter(edge, ctx));
582
+ return traversable;
583
+ };
773
584
  while (visitedEdges.size < allEdgeIds.size) {
774
- let edges = getOutEdges(graph, currentId);
775
- if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
776
- const unvisited = edges.filter((e) => !visitedEdges.has(e.id));
585
+ const unvisited = allowedEdges(currentId).filter(({ edge }) => !visitedEdges.has(edge.id));
777
586
  if (unvisited.length > 0) {
778
- const edge = unvisited[Math.floor(rng() * unvisited.length)];
779
- const node = getNode(graph, edge.targetId);
587
+ const { edge, nextId } = unvisited[Math.floor(rng() * unvisited.length)];
588
+ const node = getNode(graph, nextId);
780
589
  const step = {
781
590
  edge,
782
591
  node
@@ -789,22 +598,54 @@ function* genQuickRandomWalk(graph, options) {
789
598
  options?.onStep?.(step, ctx);
790
599
  yield step;
791
600
  } else {
792
- let targetNodeId;
793
- for (const n of graph.nodes) if (getOutEdges(graph, n.id).some((e) => !visitedEdges.has(e.id))) {
794
- targetNodeId = n.id;
795
- break;
601
+ const prevStep = /* @__PURE__ */ new Map();
602
+ const seen = new Set([currentId]);
603
+ const queue = [currentId];
604
+ let found;
605
+ while (queue.length > 0 && !found) {
606
+ const id = queue.shift();
607
+ for (const t of allowedEdges(id)) {
608
+ if (!visitedEdges.has(t.edge.id)) {
609
+ found = {
610
+ atId: id,
611
+ edge: t.edge,
612
+ nextId: t.nextId
613
+ };
614
+ break;
615
+ }
616
+ if (!seen.has(t.nextId)) {
617
+ seen.add(t.nextId);
618
+ prevStep.set(t.nextId, {
619
+ edge: t.edge,
620
+ fromId: id
621
+ });
622
+ queue.push(t.nextId);
623
+ }
624
+ }
796
625
  }
797
- if (!targetNodeId) return;
798
- const path = getShortestPath(graph, {
799
- from: currentId,
800
- to: targetNodeId
801
- });
802
- if (!path || path.steps.length === 0) return;
803
- for (const step of path.steps) {
804
- currentId = step.node.id;
626
+ if (!found) return;
627
+ const pathSteps = [{
628
+ edge: found.edge,
629
+ nextId: found.nextId
630
+ }];
631
+ let cursor = found.atId;
632
+ while (cursor !== currentId) {
633
+ const p = prevStep.get(cursor);
634
+ pathSteps.unshift({
635
+ edge: p.edge,
636
+ nextId: cursor
637
+ });
638
+ cursor = p.fromId;
639
+ }
640
+ for (const { edge, nextId } of pathSteps) {
641
+ const step = {
642
+ edge,
643
+ node: getNode(graph, nextId)
644
+ };
645
+ currentId = nextId;
805
646
  ctx.currentNodeId = currentId;
806
647
  ctx.visitedNodes.add(currentId);
807
- visitedEdges.add(step.edge.id);
648
+ visitedEdges.add(edge.id);
808
649
  ctx.stepCount++;
809
650
  options?.onStep?.(step, ctx);
810
651
  yield step;
@@ -815,14 +656,19 @@ function* genQuickRandomWalk(graph, options) {
815
656
  /**
816
657
  * Walk a predefined sequence of edge IDs.
817
658
  * Validates each edge exists and connects from the current position.
659
+ * Edges whose effective mode is not `'directed'` may be traversed
660
+ * target → source as well.
818
661
  */
819
662
  function* genPredefinedWalk(graph, edgeIds, options) {
820
663
  let currentId = resolveFrom(graph, options?.from);
821
664
  for (const edgeId of edgeIds) {
822
665
  const edge = graph.edges.find((e) => e.id === edgeId);
823
666
  if (!edge) throw new Error(`Edge "${edgeId}" not found in graph.`);
824
- if (edge.sourceId !== currentId) throw new Error(`Edge "${edgeId}" starts at "${edge.sourceId}" but current position is "${currentId}".`);
825
- const node = getNode(graph, edge.targetId);
667
+ let nextId;
668
+ if (edge.sourceId === currentId) nextId = edge.targetId;
669
+ else if (edge.targetId === currentId && getEdgeMode(graph, edge) !== "directed") nextId = edge.sourceId;
670
+ else throw new Error(`Edge "${edgeId}" connects "${edge.sourceId}" → "${edge.targetId}" but current position is "${currentId}".`);
671
+ const node = getNode(graph, nextId);
826
672
  currentId = node.id;
827
673
  yield {
828
674
  edge,
@@ -866,6 +712,7 @@ function* takeUntilNodeCoverage(gen, graph, coverage, options) {
866
712
  const target = Math.ceil(coverage * totalNodes);
867
713
  const startId = options?.from ?? graph.initialNodeId ?? graph.nodes[0]?.id;
868
714
  const visited = new Set(startId ? [startId] : []);
715
+ if (visited.size >= target) return;
869
716
  for (const step of gen) {
870
717
  visited.add(step.node.id);
871
718
  yield step;
@@ -879,6 +726,7 @@ function* takeUntilEdgeCoverage(gen, graph, coverage) {
879
726
  const totalEdges = graph.edges.length;
880
727
  const target = Math.ceil(coverage * totalEdges);
881
728
  const visited = /* @__PURE__ */ new Set();
729
+ if (target <= 0) return;
882
730
  for (const step of gen) {
883
731
  visited.add(step.edge.id);
884
732
  yield step;
@@ -906,4 +754,4 @@ function getCoverage(graph, steps, options) {
906
754
  }
907
755
 
908
756
  //#endregion
909
- export { GraphInstance, LAYOUT_KEYS, addEdge, addEntities, addNode, applyPatches, areEntitiesEqual, bfs, createFormatConverter, createGraph, createGraphEdge, createGraphFromTransition, createGraphNode, createGraphPort, createVisualGraph, deleteEdge, deleteEntities, deleteNode, dfs, flatten, genCycles, genGirvanNewmanCommunities, genPostorders, genPredefinedWalk, genPreorders, genQuickRandomWalk, genRandomWalk, genShortestPaths, genSimplePaths, genWeightedRandomWalk, getAStarPath, getAllPairsShortestPaths, getAncestors, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getChildren, getClosenessCentrality, getConnectedComponents, getCoverage, getCycles, getDegree, getDegreeCentrality, getDepth, getDescendants, getDiff, getEdge, getEdgeMode, getEdgesBetween, getEdgesByPort, getEdgesOf, getEigenvectorCentrality, getGirvanNewmanCommunities, getGreedyModularityCommunities, getHITS, getInDegree, getInDegreeCentrality, getInEdges, getLCA, getLabelPropagationCommunities, getMinimumSpanningTree, getModularity, getNeighbors, getNode, getOutDegree, getOutDegreeCentrality, getOutEdges, getPageRank, getParent, getPatches, getPort, getPorts, getPostorder, getPostorders, getPredecessors, getPreorder, getPreorders, getRelativeDistance, getRelativeDistanceMap, getRoots, getShortestPath, getShortestPaths, getSiblings, getSimplePath, getSimplePaths, getSinks, getSources, getStronglyConnectedComponents, getSubgraph, getSuccessors, getTopologicalSort, hasEdge, hasNode, hasPath, invalidateIndex, invertDiff, isAcyclic, isCompound, isConnected, isEdgeDirected, isEmptyDiff, isIsomorphic, isLayoutEqual, isLeaf, isNonLayoutEqual, isTree, joinPaths, reverseGraph, takeSteps, takeUntilEdge, takeUntilEdgeCoverage, takeUntilNode, takeUntilNodeCoverage, toDiff, toPatches, updateEdge, updateEntities, updateNode };
757
+ export { GraphInstance, LAYOUT_KEYS, addEdge, addEntities, addNode, applyPatches, areEntitiesEqual, bfs, createFormatConverter, createGraph, createGraphEdge, createGraphFromTransition, createGraphNode, createGraphPort, createVisualGraph, deleteEdge, deleteEntities, deleteNode, dfs, flatten, genCycles, genGirvanNewmanCommunities, genPostorders, genPredefinedWalk, genPreorders, genQuickRandomWalk, genRandomWalk, genShortestPaths, genSimplePaths, genWeightedRandomWalk, getAStarPath, getAllPairsShortestPaths, getAncestors, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getChildren, getClosenessCentrality, getConnectedComponents, getCoverage, getCycles, getDegree, getDegreeCentrality, getDepth, getDescendants, getDiff, getDominatorTree, getEdge, getEdgeMode, getEdgesBetween, getEdgesByPort, getEdgesOf, getEigenvectorCentrality, getGirvanNewmanCommunities, getGraphIssues, getGreedyModularityCommunities, getHITS, getInDegree, getInDegreeCentrality, getInEdges, getLCA, getLabelPropagationCommunities, getLouvainCommunities, getMaxFlow, getMinimumSpanningTree, getModularity, getNeighbors, getNode, getOutDegree, getOutDegreeCentrality, getOutEdges, getPageRank, getParent, getPatches, getPort, getPorts, getPostorder, getPostorders, getPredecessors, getPreorder, getPreorders, getRelativeDistance, getRelativeDistanceMap, getRoots, getShortestPath, getShortestPaths, getSiblings, getSimplePath, getSimplePaths, getSinks, getSources, getStronglyConnectedComponents, getSubgraph, getSuccessors, getTopologicalSort, getTransitiveReduction, hasEdge, hasNode, hasPath, invalidateIndex, invertDiff, isAcyclic, isCompound, isConnected, isEdgeDirected, isEmptyDiff, isIsomorphic, isLayoutEqual, isLeaf, isNonLayoutEqual, isTree, joinPaths, reverseGraph, takeSteps, takeUntilEdge, takeUntilEdgeCoverage, takeUntilNode, takeUntilNodeCoverage, toDiff, toPatches, updateEdge, updateEntities, updateNode };