@statelyai/graph 0.13.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 (57) hide show
  1. package/README.md +57 -26
  2. package/dist/{adjacency-list-Ca0VjKIf.mjs → adjacency-list-GeL1Cu-L.mjs} +5 -3
  3. package/dist/{algorithms-BlM-qoJb.d.mts → algorithms-CsGNehct.d.mts} +137 -2
  4. package/dist/{algorithms-BNDQcHU3.mjs → algorithms-DF1pSQGv.mjs} +1494 -357
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/{converter-Dspillnn.mjs → converter-DyCJJfTe.mjs} +2 -2
  8. package/dist/{edge-list-gKe8-iRa.mjs → edge-list-BcZ0h6zz.mjs} +1 -1
  9. package/dist/format-support.mjs +67 -11
  10. package/dist/formats/adjacency-list/index.d.mts +1 -1
  11. package/dist/formats/adjacency-list/index.mjs +1 -1
  12. package/dist/formats/converter/index.d.mts +1 -60
  13. package/dist/formats/converter/index.mjs +1 -1
  14. package/dist/formats/cytoscape/index.d.mts +1 -1
  15. package/dist/formats/cytoscape/index.mjs +5 -3
  16. package/dist/formats/d2/index.d.mts +109 -0
  17. package/dist/formats/d2/index.mjs +1100 -0
  18. package/dist/formats/d3/index.d.mts +2 -2
  19. package/dist/formats/d3/index.mjs +5 -3
  20. package/dist/formats/dot/index.d.mts +1 -1
  21. package/dist/formats/dot/index.mjs +24 -8
  22. package/dist/formats/edge-list/index.d.mts +1 -1
  23. package/dist/formats/edge-list/index.mjs +1 -1
  24. package/dist/formats/elk/index.d.mts +1 -1
  25. package/dist/formats/elk/index.mjs +23 -16
  26. package/dist/formats/gexf/index.d.mts +1 -1
  27. package/dist/formats/gexf/index.mjs +30 -17
  28. package/dist/formats/gml/index.d.mts +1 -1
  29. package/dist/formats/gml/index.mjs +22 -13
  30. package/dist/formats/graphml/index.d.mts +1 -1
  31. package/dist/formats/graphml/index.mjs +83 -25
  32. package/dist/formats/jgf/index.d.mts +1 -1
  33. package/dist/formats/jgf/index.mjs +6 -3
  34. package/dist/formats/mermaid/index.d.mts +1 -1
  35. package/dist/formats/mermaid/index.mjs +57 -20
  36. package/dist/formats/tgf/index.d.mts +1 -1
  37. package/dist/formats/tgf/index.mjs +2 -2
  38. package/dist/formats/xyflow/index.d.mts +1 -1
  39. package/dist/formats/xyflow/index.mjs +33 -6
  40. package/dist/index-D51lJnt2.d.mts +61 -0
  41. package/dist/index-DWmo1mIp.d.mts +697 -0
  42. package/dist/index.d.mts +6 -631
  43. package/dist/index.mjs +144 -295
  44. package/dist/mode-D8OnHFBk.mjs +15 -0
  45. package/dist/queries-BfXeTXRf.d.mts +547 -0
  46. package/dist/queries-KirMDR7e.mjs +980 -0
  47. package/dist/queries.d.mts +1 -514
  48. package/dist/queries.mjs +1 -766
  49. package/dist/schemas.d.mts +21 -10
  50. package/dist/schemas.mjs +35 -86
  51. package/dist/{types-CnZ01raw.d.mts → types-DNYdIU21.d.mts} +83 -11
  52. package/dist/validate-TtH-x3JV.mjs +190 -0
  53. package/package.json +14 -3
  54. package/schemas/edge.schema.json +11 -0
  55. package/schemas/graph.schema.json +24 -3
  56. package/schemas/node.schema.json +6 -0
  57. package/dist/indexing-DUl3kTqm.mjs +0 -137
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import { o as invalidateIndex, t as getIndex } from "./indexing-DUl3kTqm.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-BNDQcHU3.mjs";
3
- 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";
4
- import { n as createFormatConverter } from "./converter-Dspillnn.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
+ import { n as isEdgeDirected, t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
4
+ import { t as getGraphIssues } from "./validate-TtH-x3JV.mjs";
5
+ import { n as createFormatConverter } from "./converter-DyCJJfTe.mjs";
5
6
 
6
7
  //#region src/equivalence.ts
7
8
  /** Shallow-compare two values, returning true if they differ. */
@@ -51,7 +52,7 @@ const LAYOUT_KEY_SET = {
51
52
  * ```
52
53
  */
53
54
  function areEntitiesEqual(a, b, keys) {
54
- 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)])];
55
56
  for (const key of compareKeys) if (differs$1(a[key], b[key])) return false;
56
57
  return true;
57
58
  }
@@ -86,47 +87,16 @@ function isLayoutEqual(a, b) {
86
87
  */
87
88
  function isNonLayoutEqual(a, b) {
88
89
  const skip = LAYOUT_KEY_SET[a.type];
89
- const keys = Object.keys(a);
90
- for (let i = 0; i < keys.length; i++) {
91
- if (skip.has(keys[i])) continue;
92
- 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;
93
94
  }
94
95
  return true;
95
96
  }
96
97
 
97
98
  //#endregion
98
99
  //#region src/diff.ts
99
- function nodeToConfig$1(node) {
100
- const config = { id: node.id };
101
- if (node.parentId) config.parentId = node.parentId;
102
- if (node.initialNodeId) config.initialNodeId = node.initialNodeId;
103
- if (node.label !== "") config.label = node.label;
104
- if (node.data !== void 0) config.data = node.data;
105
- if (node.x !== void 0) config.x = node.x;
106
- if (node.y !== void 0) config.y = node.y;
107
- if (node.width !== void 0) config.width = node.width;
108
- if (node.height !== void 0) config.height = node.height;
109
- if (node.shape !== void 0) config.shape = node.shape;
110
- if (node.color !== void 0) config.color = node.color;
111
- if (node.style !== void 0) config.style = node.style;
112
- return config;
113
- }
114
- function edgeToConfig$1(edge) {
115
- const config = {
116
- id: edge.id,
117
- sourceId: edge.sourceId,
118
- targetId: edge.targetId
119
- };
120
- if (edge.label !== "") config.label = edge.label;
121
- if (edge.data !== void 0) config.data = edge.data;
122
- if (edge.x !== void 0) config.x = edge.x;
123
- if (edge.y !== void 0) config.y = edge.y;
124
- if (edge.width !== void 0) config.width = edge.width;
125
- if (edge.height !== void 0) config.height = edge.height;
126
- if (edge.color !== void 0) config.color = edge.color;
127
- if (edge.style !== void 0) config.style = edge.style;
128
- return config;
129
- }
130
100
  /** Shallow-compare two values, returning true if they differ. */
131
101
  function differs(a, b) {
132
102
  if (a === b) return false;
@@ -139,6 +109,7 @@ const NODE_COMPARE_KEYS = [
139
109
  "initialNodeId",
140
110
  "label",
141
111
  "data",
112
+ "ports",
142
113
  "x",
143
114
  "y",
144
115
  "width",
@@ -152,6 +123,10 @@ const EDGE_COMPARE_KEYS = [
152
123
  "targetId",
153
124
  "label",
154
125
  "data",
126
+ "weight",
127
+ "mode",
128
+ "sourcePort",
129
+ "targetPort",
155
130
  "x",
156
131
  "y",
157
132
  "width",
@@ -193,13 +168,17 @@ function getDiff(a, b) {
193
168
  };
194
169
  for (const [id, nodeB] of bNodeMap) {
195
170
  const nodeA = aNodeMap.get(id);
196
- if (!nodeA) diff.nodes.added.push(nodeToConfig$1(nodeB));
171
+ if (!nodeA) diff.nodes.added.push(toNodeConfig(nodeB));
197
172
  else {
198
173
  const oldPartial = {};
199
174
  const newPartial = {};
200
- for (const key of NODE_COMPARE_KEYS) if (differs(nodeA[key], nodeB[key])) {
201
- oldPartial[key] = nodeA[key];
202
- 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
+ }
203
182
  }
204
183
  if (Object.keys(oldPartial).length > 0) diff.nodes.updated.push({
205
184
  id,
@@ -208,16 +187,20 @@ function getDiff(a, b) {
208
187
  });
209
188
  }
210
189
  }
211
- 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));
212
191
  for (const [id, edgeB] of bEdgeMap) {
213
192
  const edgeA = aEdgeMap.get(id);
214
- if (!edgeA) diff.edges.added.push(edgeToConfig$1(edgeB));
193
+ if (!edgeA) diff.edges.added.push(toEdgeConfig(edgeB));
215
194
  else {
216
195
  const oldPartial = {};
217
196
  const newPartial = {};
218
- for (const key of EDGE_COMPARE_KEYS) if (differs(edgeA[key], edgeB[key])) {
219
- oldPartial[key] = edgeA[key];
220
- 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
+ }
221
204
  }
222
205
  if (Object.keys(oldPartial).length > 0) diff.edges.updated.push({
223
206
  id,
@@ -226,7 +209,7 @@ function getDiff(a, b) {
226
209
  });
227
210
  }
228
211
  }
229
- 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));
230
213
  return diff;
231
214
  }
232
215
  /**
@@ -263,28 +246,29 @@ function isEmptyDiff(diff) {
263
246
  function invertDiff(diff) {
264
247
  return {
265
248
  nodes: {
266
- added: diff.nodes.removed,
267
- removed: diff.nodes.added,
249
+ added: diff.nodes.removed.map((c) => structuredClone(c)),
250
+ removed: diff.nodes.added.map((c) => structuredClone(c)),
268
251
  updated: diff.nodes.updated.map((c) => ({
269
252
  id: c.id,
270
- old: c.new,
271
- new: c.old
253
+ old: structuredClone(c.new),
254
+ new: structuredClone(c.old)
272
255
  }))
273
256
  },
274
257
  edges: {
275
- added: diff.edges.removed,
276
- removed: diff.edges.added,
258
+ added: diff.edges.removed.map((c) => structuredClone(c)),
259
+ removed: diff.edges.added.map((c) => structuredClone(c)),
277
260
  updated: diff.edges.updated.map((c) => ({
278
261
  id: c.id,
279
- old: c.new,
280
- new: c.old
262
+ old: structuredClone(c.new),
263
+ new: structuredClone(c.old)
281
264
  }))
282
265
  }
283
266
  };
284
267
  }
285
268
  /**
286
269
  * Compute an ordered patch list from graph `a` to graph `b`.
287
- * 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.
288
272
  *
289
273
  * @example
290
274
  * ```ts
@@ -364,7 +348,7 @@ function toPatches(diff) {
364
348
  });
365
349
  for (const change of diff.edges.updated) {
366
350
  const data = {};
367
- 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;
368
352
  patches.push({
369
353
  op: "updateEdge",
370
354
  id: change.id,
@@ -385,7 +369,7 @@ function toPatches(diff) {
385
369
  });
386
370
  for (const change of diff.nodes.updated) {
387
371
  const data = {};
388
- 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;
389
373
  patches.push({
390
374
  op: "updateNode",
391
375
  id: change.id,
@@ -459,201 +443,6 @@ function toDiff(patches) {
459
443
  return diff;
460
444
  }
461
445
 
462
- //#endregion
463
- //#region src/transforms.ts
464
- /**
465
- * Flattens a hierarchical graph into a flat graph with only leaf nodes.
466
- *
467
- * - Edges targeting a compound node resolve to its initial child (recursively).
468
- * - Edges originating from a compound node expand to all leaf descendants.
469
- * - Only leaf nodes (nodes with no children) appear in the result.
470
- * - Duplicate edges (same source + target) are deduplicated.
471
- *
472
- * @example
473
- * ```ts
474
- * import { createGraph, flatten } from '@statelyai/graph';
475
- *
476
- * const graph = createGraph({
477
- * nodes: [
478
- * { id: 'parent', initialNodeId: 'child1' },
479
- * { id: 'child1', parentId: 'parent' },
480
- * { id: 'child2', parentId: 'parent' },
481
- * { id: 'other' },
482
- * ],
483
- * edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
484
- * });
485
- *
486
- * const flat = flatten(graph);
487
- * // flat.nodes → [child1, child2, other] (leaf nodes only)
488
- * // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
489
- * ```
490
- */
491
- function flatten(graph) {
492
- const idx = getIndex(graph);
493
- const leaves = /* @__PURE__ */ new Set();
494
- for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
495
- function resolveInitial(nodeId) {
496
- if (leaves.has(nodeId)) return nodeId;
497
- const ni = idx.nodeById.get(nodeId);
498
- if (ni === void 0) return null;
499
- const node = graph.nodes[ni];
500
- if (node.initialNodeId) return resolveInitial(node.initialNodeId);
501
- const childIds = idx.childNodes.get(nodeId) ?? [];
502
- if (childIds.length > 0) return resolveInitial(childIds[0]);
503
- return nodeId;
504
- }
505
- function getLeafDescendants(nodeId) {
506
- if (leaves.has(nodeId)) return [nodeId];
507
- const result = [];
508
- const collect = (id) => {
509
- const childIds = idx.childNodes.get(id) ?? [];
510
- for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
511
- else collect(childId);
512
- };
513
- collect(nodeId);
514
- return result;
515
- }
516
- const edgeSeen = /* @__PURE__ */ new Set();
517
- const flatEdges = [];
518
- for (const edge of graph.edges) {
519
- const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
520
- const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
521
- if (target === null) continue;
522
- for (const source of sources) {
523
- if (source === target) continue;
524
- const key = `${source}->${target}`;
525
- if (edgeSeen.has(key)) continue;
526
- edgeSeen.add(key);
527
- flatEdges.push({
528
- type: "edge",
529
- id: `${edge.id}:${source}->${target}`,
530
- sourceId: source,
531
- targetId: target,
532
- label: edge.label,
533
- data: edge.data
534
- });
535
- }
536
- }
537
- const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => ({
538
- id: n.id,
539
- label: n.label,
540
- data: n.data
541
- }));
542
- return createGraph({
543
- id: graph.id,
544
- type: graph.type,
545
- nodes: leafNodes,
546
- edges: flatEdges,
547
- data: graph.data
548
- });
549
- }
550
- function nodeToConfig(node, nodeIdSet) {
551
- const config = {
552
- id: node.id,
553
- label: node.label,
554
- data: node.data
555
- };
556
- if (node.parentId !== void 0 && node.parentId !== null) config.parentId = nodeIdSet && !nodeIdSet.has(node.parentId) ? void 0 : node.parentId;
557
- if (node.initialNodeId !== void 0) config.initialNodeId = node.initialNodeId ?? void 0;
558
- if (node.x !== void 0) config.x = node.x;
559
- if (node.y !== void 0) config.y = node.y;
560
- if (node.width !== void 0) config.width = node.width;
561
- if (node.height !== void 0) config.height = node.height;
562
- if (node.shape !== void 0) config.shape = node.shape;
563
- if (node.color !== void 0) config.color = node.color;
564
- if (node.style !== void 0) config.style = node.style;
565
- return config;
566
- }
567
- function edgeToConfig(edge) {
568
- const config = {
569
- id: edge.id,
570
- sourceId: edge.sourceId,
571
- targetId: edge.targetId,
572
- label: edge.label,
573
- data: edge.data
574
- };
575
- if (edge.weight !== void 0) config.weight = edge.weight;
576
- if (edge.x !== void 0) config.x = edge.x;
577
- if (edge.y !== void 0) config.y = edge.y;
578
- if (edge.width !== void 0) config.width = edge.width;
579
- if (edge.height !== void 0) config.height = edge.height;
580
- if (edge.color !== void 0) config.color = edge.color;
581
- if (edge.style !== void 0) config.style = edge.style;
582
- return config;
583
- }
584
- /**
585
- * Returns the induced subgraph containing only the given node IDs
586
- * and edges whose endpoints are both in the set.
587
- *
588
- * Parent references to nodes outside the set are removed.
589
- *
590
- * @example
591
- * ```ts
592
- * import { createGraph, getSubgraph } from '@statelyai/graph';
593
- *
594
- * const graph = createGraph({
595
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
596
- * edges: [
597
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
598
- * { id: 'bc', sourceId: 'b', targetId: 'c' },
599
- * ],
600
- * });
601
- *
602
- * const sub = getSubgraph(graph, ['a', 'b']);
603
- * // sub.nodes: [a, b], sub.edges: [ab]
604
- * ```
605
- */
606
- function getSubgraph(graph, nodeIds) {
607
- const nodeIdSet = new Set(nodeIds);
608
- return createGraph({
609
- id: graph.id,
610
- type: graph.type,
611
- initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
612
- nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => nodeToConfig(n, nodeIdSet)),
613
- edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(edgeToConfig),
614
- data: graph.data
615
- });
616
- }
617
- /**
618
- * Returns a new graph with all edge directions flipped (source ↔ target).
619
- * Optionally filters which edges to include.
620
- *
621
- * @example
622
- * ```ts
623
- * import { createGraph, reverseGraph } from '@statelyai/graph';
624
- *
625
- * const graph = createGraph({
626
- * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
627
- * edges: [
628
- * { id: 'ab', sourceId: 'a', targetId: 'b' },
629
- * { id: 'bc', sourceId: 'b', targetId: 'c' },
630
- * ],
631
- * });
632
- *
633
- * const rev = reverseGraph(graph);
634
- * // rev edges: b→a, c→b
635
- *
636
- * const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
637
- * // filtered edges: b→a (only ab reversed, bc excluded)
638
- * ```
639
- */
640
- function reverseGraph(graph, filterEdge) {
641
- const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
642
- return createGraph({
643
- id: graph.id,
644
- type: graph.type,
645
- initialNodeId: graph.initialNodeId ?? void 0,
646
- nodes: graph.nodes.map((n) => nodeToConfig(n)),
647
- edges: edges.map((e) => {
648
- const config = edgeToConfig(e);
649
- config.sourceId = e.targetId;
650
- config.targetId = e.sourceId;
651
- return config;
652
- }),
653
- data: graph.data
654
- });
655
- }
656
-
657
446
  //#endregion
658
447
  //#region src/walks.ts
659
448
  function mulberry32(seed) {
@@ -679,7 +468,24 @@ function resolveFrom(graph, from) {
679
468
  throw new Error("Cannot determine start node: provide `from`, set graph.initialNodeId, or have exactly one source node.");
680
469
  }
681
470
  /**
682
- * 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).
683
489
  * Yields steps indefinitely (may revisit nodes) until a sink node is reached.
684
490
  */
685
491
  function* genRandomWalk(graph, options) {
@@ -692,11 +498,11 @@ function* genRandomWalk(graph, options) {
692
498
  stepCount: 0
693
499
  };
694
500
  while (true) {
695
- let edges = getOutEdges(graph, currentId);
696
- if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
697
- if (edges.length === 0) return;
698
- const edge = edges[Math.floor(rng() * edges.length)];
699
- 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);
700
506
  const step = {
701
507
  edge,
702
508
  node
@@ -724,30 +530,30 @@ function* genWeightedRandomWalk(graph, options) {
724
530
  stepCount: 0
725
531
  };
726
532
  while (true) {
727
- let edges = getOutEdges(graph, currentId);
728
- if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
729
- if (edges.length === 0) return;
730
- 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)));
731
537
  const total = weights.reduce((a, b) => a + b, 0);
732
538
  if (total === 0) return;
733
539
  let r = rng() * total;
734
- let chosen = edges[0];
735
- for (let i = 0; i < edges.length; i++) {
540
+ let chosen = traversable[0];
541
+ for (let i = 0; i < traversable.length; i++) {
736
542
  r -= weights[i];
737
543
  if (r <= 0) {
738
- chosen = edges[i];
544
+ chosen = traversable[i];
739
545
  break;
740
546
  }
741
547
  }
742
- const node = getNode(graph, chosen.targetId);
548
+ const node = getNode(graph, chosen.nextId);
743
549
  const step = {
744
- edge: chosen,
550
+ edge: chosen.edge,
745
551
  node
746
552
  };
747
553
  currentId = node.id;
748
554
  ctx.currentNodeId = currentId;
749
555
  ctx.visitedNodes.add(currentId);
750
- ctx.visitedEdges.add(chosen.id);
556
+ ctx.visitedEdges.add(chosen.edge.id);
751
557
  ctx.stepCount++;
752
558
  options?.onStep?.(step, ctx);
753
559
  yield step;
@@ -755,8 +561,9 @@ function* genWeightedRandomWalk(graph, options) {
755
561
  }
756
562
  /**
757
563
  * Quick random walk targeting unvisited edges.
758
- * If unvisited outgoing edges exist, picks one randomly.
759
- * 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.
760
567
  */
761
568
  function* genQuickRandomWalk(graph, options) {
762
569
  const rng = makeRng(options?.seed);
@@ -769,13 +576,16 @@ function* genQuickRandomWalk(graph, options) {
769
576
  visitedEdges,
770
577
  stepCount: 0
771
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
+ };
772
584
  while (visitedEdges.size < allEdgeIds.size) {
773
- let edges = getOutEdges(graph, currentId);
774
- if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
775
- const unvisited = edges.filter((e) => !visitedEdges.has(e.id));
585
+ const unvisited = allowedEdges(currentId).filter(({ edge }) => !visitedEdges.has(edge.id));
776
586
  if (unvisited.length > 0) {
777
- const edge = unvisited[Math.floor(rng() * unvisited.length)];
778
- const node = getNode(graph, edge.targetId);
587
+ const { edge, nextId } = unvisited[Math.floor(rng() * unvisited.length)];
588
+ const node = getNode(graph, nextId);
779
589
  const step = {
780
590
  edge,
781
591
  node
@@ -788,22 +598,54 @@ function* genQuickRandomWalk(graph, options) {
788
598
  options?.onStep?.(step, ctx);
789
599
  yield step;
790
600
  } else {
791
- let targetNodeId;
792
- for (const n of graph.nodes) if (getOutEdges(graph, n.id).some((e) => !visitedEdges.has(e.id))) {
793
- targetNodeId = n.id;
794
- 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
+ }
795
625
  }
796
- if (!targetNodeId) return;
797
- const path = getShortestPath(graph, {
798
- from: currentId,
799
- to: targetNodeId
800
- });
801
- if (!path || path.steps.length === 0) return;
802
- for (const step of path.steps) {
803
- 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;
804
646
  ctx.currentNodeId = currentId;
805
647
  ctx.visitedNodes.add(currentId);
806
- visitedEdges.add(step.edge.id);
648
+ visitedEdges.add(edge.id);
807
649
  ctx.stepCount++;
808
650
  options?.onStep?.(step, ctx);
809
651
  yield step;
@@ -814,14 +656,19 @@ function* genQuickRandomWalk(graph, options) {
814
656
  /**
815
657
  * Walk a predefined sequence of edge IDs.
816
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.
817
661
  */
818
662
  function* genPredefinedWalk(graph, edgeIds, options) {
819
663
  let currentId = resolveFrom(graph, options?.from);
820
664
  for (const edgeId of edgeIds) {
821
665
  const edge = graph.edges.find((e) => e.id === edgeId);
822
666
  if (!edge) throw new Error(`Edge "${edgeId}" not found in graph.`);
823
- if (edge.sourceId !== currentId) throw new Error(`Edge "${edgeId}" starts at "${edge.sourceId}" but current position is "${currentId}".`);
824
- 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);
825
672
  currentId = node.id;
826
673
  yield {
827
674
  edge,
@@ -865,6 +712,7 @@ function* takeUntilNodeCoverage(gen, graph, coverage, options) {
865
712
  const target = Math.ceil(coverage * totalNodes);
866
713
  const startId = options?.from ?? graph.initialNodeId ?? graph.nodes[0]?.id;
867
714
  const visited = new Set(startId ? [startId] : []);
715
+ if (visited.size >= target) return;
868
716
  for (const step of gen) {
869
717
  visited.add(step.node.id);
870
718
  yield step;
@@ -878,6 +726,7 @@ function* takeUntilEdgeCoverage(gen, graph, coverage) {
878
726
  const totalEdges = graph.edges.length;
879
727
  const target = Math.ceil(coverage * totalEdges);
880
728
  const visited = /* @__PURE__ */ new Set();
729
+ if (target <= 0) return;
881
730
  for (const step of gen) {
882
731
  visited.add(step.edge.id);
883
732
  yield step;
@@ -905,4 +754,4 @@ function getCoverage(graph, steps, options) {
905
754
  }
906
755
 
907
756
  //#endregion
908
- 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, 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, 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 };
@@ -0,0 +1,15 @@
1
+ //#region src/mode.ts
2
+ /**
3
+ * Resolve an edge's effective directedness. Falls back to the graph's
4
+ * {@link Graph.mode} when the edge has no per-edge override.
5
+ */
6
+ function getEdgeMode(graph, edge) {
7
+ return edge.mode ?? graph.mode;
8
+ }
9
+ /** Whether an edge points only from source to target. */
10
+ function isEdgeDirected(graph, edge) {
11
+ return getEdgeMode(graph, edge) === "directed";
12
+ }
13
+
14
+ //#endregion
15
+ export { isEdgeDirected as n, getEdgeMode as t };