@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
@@ -0,0 +1,980 @@
1
+ import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
2
+
3
+ //#region src/indexing.ts
4
+ const indexes = /* @__PURE__ */ new WeakMap();
5
+ /**
6
+ * Get or lazily build the index for a graph.
7
+ * Auto-rebuilds when `graph.nodes`/`graph.edges` are **replaced** (e.g. an
8
+ * immutable-style `map`/`filter` update) or when their length changes.
9
+ *
10
+ * Mutating *fields* of an existing node/edge in place (e.g.
11
+ * `edge.sourceId = 'x'`, `node.parentId = 'y'`) is not detectable in O(1) —
12
+ * call {@link invalidateIndex} afterwards, or use the mutation API
13
+ * (`updateNode`/`updateEdge`), which keeps the index in sync.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { createGraph, getIndex } from '@statelyai/graph';
18
+ *
19
+ * const graph = createGraph({
20
+ * nodes: [{ id: 'a' }, { id: 'b' }],
21
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
22
+ * });
23
+ *
24
+ * const idx = getIndex(graph);
25
+ * idx.nodeById.get('a'); // 0
26
+ * idx.outEdges.get('a'); // ['e1']
27
+ * ```
28
+ */
29
+ function getIndex(graph) {
30
+ let idx = indexes.get(graph);
31
+ if (!idx || idx.nodesRef !== graph.nodes || idx.edgesRef !== graph.edges || idx.nodeCount !== graph.nodes.length || idx.edgeCount !== graph.edges.length) {
32
+ idx = buildIndex(graph);
33
+ indexes.set(graph, idx);
34
+ }
35
+ return idx;
36
+ }
37
+ /**
38
+ * Clear the cached index. Call this if you mutate fields of existing
39
+ * nodes/edges in place (e.g. `edge.targetId = 'a'`) — such mutations are not
40
+ * auto-detected. Array replacement and length changes are auto-detected.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { createGraph, invalidateIndex, getIndex } from '@statelyai/graph';
45
+ *
46
+ * const graph = createGraph({
47
+ * nodes: [{ id: 'a' }, { id: 'b' }],
48
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
49
+ * });
50
+ * graph.edges[0].targetId = 'a'; // in-place field mutation
51
+ * invalidateIndex(graph); // forces rebuild on next getIndex()
52
+ * ```
53
+ */
54
+ function invalidateIndex(graph) {
55
+ indexes.delete(graph);
56
+ }
57
+ function buildIndex(graph) {
58
+ const nodeById = /* @__PURE__ */ new Map();
59
+ const edgeById = /* @__PURE__ */ new Map();
60
+ const outEdges = /* @__PURE__ */ new Map();
61
+ const inEdges = /* @__PURE__ */ new Map();
62
+ const childNodes = /* @__PURE__ */ new Map();
63
+ for (let i = 0; i < graph.nodes.length; i++) {
64
+ const n = graph.nodes[i];
65
+ nodeById.set(n.id, i);
66
+ outEdges.set(n.id, []);
67
+ inEdges.set(n.id, []);
68
+ const parent = n.parentId ?? null;
69
+ if (!childNodes.has(parent)) childNodes.set(parent, []);
70
+ childNodes.get(parent).push(n.id);
71
+ }
72
+ for (let i = 0; i < graph.edges.length; i++) {
73
+ const e = graph.edges[i];
74
+ edgeById.set(e.id, i);
75
+ outEdges.get(e.sourceId)?.push(e.id);
76
+ inEdges.get(e.targetId)?.push(e.id);
77
+ }
78
+ return {
79
+ nodeById,
80
+ edgeById,
81
+ outEdges,
82
+ inEdges,
83
+ childNodes,
84
+ nodeCount: graph.nodes.length,
85
+ edgeCount: graph.edges.length,
86
+ nodesRef: graph.nodes,
87
+ edgesRef: graph.edges,
88
+ version: 0
89
+ };
90
+ }
91
+ function indexAddNode(idx, node, arrayIndex) {
92
+ idx.nodeById.set(node.id, arrayIndex);
93
+ idx.outEdges.set(node.id, []);
94
+ idx.inEdges.set(node.id, []);
95
+ const parent = node.parentId ?? null;
96
+ if (!idx.childNodes.has(parent)) idx.childNodes.set(parent, []);
97
+ idx.childNodes.get(parent).push(node.id);
98
+ idx.nodeCount++;
99
+ idx.version++;
100
+ }
101
+ function indexAddEdge(idx, edge, arrayIndex) {
102
+ idx.edgeById.set(edge.id, arrayIndex);
103
+ idx.outEdges.get(edge.sourceId)?.push(edge.id);
104
+ idx.inEdges.get(edge.targetId)?.push(edge.id);
105
+ idx.edgeCount++;
106
+ idx.version++;
107
+ }
108
+ /** Update childNodes index when a node's parentId changes. */
109
+ function indexReparentNode(idx, nodeId, oldParentId, newParentId) {
110
+ const oldSiblings = idx.childNodes.get(oldParentId ?? null);
111
+ if (oldSiblings) {
112
+ const pos = oldSiblings.indexOf(nodeId);
113
+ if (pos !== -1) oldSiblings.splice(pos, 1);
114
+ }
115
+ const np = newParentId ?? null;
116
+ if (!idx.childNodes.has(np)) idx.childNodes.set(np, []);
117
+ idx.childNodes.get(np).push(nodeId);
118
+ idx.version++;
119
+ }
120
+ /**
121
+ * Bump the index version without touching adjacency. Used by mutations that
122
+ * change fields derived caches depend on (e.g. per-edge `mode` affects the
123
+ * CSR arc structure but not the id-based adjacency lists).
124
+ */
125
+ function touchIndex(idx) {
126
+ idx.version++;
127
+ }
128
+ /** Update adjacency lists when an edge's sourceId/targetId changes. */
129
+ function indexUpdateEdgeEndpoints(idx, edgeId, oldSourceId, oldTargetId, newSourceId, newTargetId) {
130
+ if (oldSourceId !== newSourceId) {
131
+ const oldOut = idx.outEdges.get(oldSourceId);
132
+ if (oldOut) {
133
+ const pos = oldOut.indexOf(edgeId);
134
+ if (pos !== -1) oldOut.splice(pos, 1);
135
+ }
136
+ idx.outEdges.get(newSourceId)?.push(edgeId);
137
+ }
138
+ if (oldTargetId !== newTargetId) {
139
+ const oldIn = idx.inEdges.get(oldTargetId);
140
+ if (oldIn) {
141
+ const pos = oldIn.indexOf(edgeId);
142
+ if (pos !== -1) oldIn.splice(pos, 1);
143
+ }
144
+ idx.inEdges.get(newTargetId)?.push(edgeId);
145
+ }
146
+ idx.version++;
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/queries.ts
151
+ /**
152
+ * Returns all edges (incoming + outgoing) connected to a node.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const graph = createGraph({
157
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
158
+ * edges: [
159
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
160
+ * { id: 'e2', sourceId: 'c', targetId: 'b' },
161
+ * ],
162
+ * });
163
+ * getEdgesOf(graph, 'b');
164
+ * // => [edge e1, edge e2]
165
+ * ```
166
+ */
167
+ function getEdgesOf(graph, nodeId) {
168
+ const idx = getIndex(graph);
169
+ const outIds = idx.outEdges.get(nodeId) ?? [];
170
+ const inIds = idx.inEdges.get(nodeId) ?? [];
171
+ const seen = /* @__PURE__ */ new Set();
172
+ const result = [];
173
+ for (const eid of outIds) {
174
+ seen.add(eid);
175
+ const ai = idx.edgeById.get(eid);
176
+ if (ai !== void 0) result.push(graph.edges[ai]);
177
+ }
178
+ for (const eid of inIds) if (!seen.has(eid)) {
179
+ const ai = idx.edgeById.get(eid);
180
+ if (ai !== void 0) result.push(graph.edges[ai]);
181
+ }
182
+ return result;
183
+ }
184
+ /**
185
+ * Returns incoming edges to a node, by *authored* direction
186
+ * (`edge.targetId === nodeId`), regardless of edge mode. For mode-aware
187
+ * traversal use {@link getPredecessors} or {@link getEdgesOf}.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * const graph = createGraph({
192
+ * nodes: [{ id: 'a' }, { id: 'b' }],
193
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
194
+ * });
195
+ * getInEdges(graph, 'b');
196
+ * // => [edge e1]
197
+ * getInEdges(graph, 'a');
198
+ * // => []
199
+ * ```
200
+ */
201
+ function getInEdges(graph, nodeId) {
202
+ const idx = getIndex(graph);
203
+ return (idx.inEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)]);
204
+ }
205
+ /**
206
+ * Returns outgoing edges from a node, by *authored* direction
207
+ * (`edge.sourceId === nodeId`), regardless of edge mode. For mode-aware
208
+ * traversal use {@link getSuccessors} or {@link getEdgesOf}.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const graph = createGraph({
213
+ * nodes: [{ id: 'a' }, { id: 'b' }],
214
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
215
+ * });
216
+ * getOutEdges(graph, 'a');
217
+ * // => [edge e1]
218
+ * getOutEdges(graph, 'b');
219
+ * // => []
220
+ * ```
221
+ */
222
+ function getOutEdges(graph, nodeId) {
223
+ const idx = getIndex(graph);
224
+ return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)]);
225
+ }
226
+ /**
227
+ * Returns all edges from `sourceId` to `targetId`.
228
+ * Edges whose effective mode is not `'directed'` are matched both ways.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * const graph = createGraph({
233
+ * nodes: [{ id: 'a' }, { id: 'b' }],
234
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
235
+ * });
236
+ * getEdgesBetween(graph, 'a', 'b');
237
+ * // => [edge e1]
238
+ * getEdgesBetween(graph, 'b', 'a');
239
+ * // => [] (directed edge)
240
+ * ```
241
+ */
242
+ function getEdgesBetween(graph, sourceId, targetId) {
243
+ const idx = getIndex(graph);
244
+ const result = [];
245
+ const seen = /* @__PURE__ */ new Set();
246
+ const outIds = idx.outEdges.get(sourceId) ?? [];
247
+ for (const eid of outIds) {
248
+ const ai = idx.edgeById.get(eid);
249
+ const e = graph.edges[ai];
250
+ if (e.targetId === targetId) {
251
+ seen.add(eid);
252
+ result.push(e);
253
+ }
254
+ }
255
+ const outIds2 = idx.outEdges.get(targetId) ?? [];
256
+ for (const eid of outIds2) {
257
+ if (seen.has(eid)) continue;
258
+ const ai = idx.edgeById.get(eid);
259
+ const e = graph.edges[ai];
260
+ if (e.targetId === sourceId && getEdgeMode(graph, e) !== "directed") result.push(e);
261
+ }
262
+ return result;
263
+ }
264
+ /**
265
+ * Returns direct successor nodes — nodes reachable by traversing one edge
266
+ * away from `nodeId`. Edges whose effective mode is not `'directed'` are
267
+ * traversable both ways, so their other endpoint also counts as a successor.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * const graph = createGraph({
272
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
273
+ * edges: [
274
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
275
+ * { id: 'e2', sourceId: 'a', targetId: 'c' },
276
+ * ],
277
+ * });
278
+ * getSuccessors(graph, 'a');
279
+ * // => [node b, node c]
280
+ * ```
281
+ */
282
+ function getSuccessors(graph, nodeId) {
283
+ const idx = getIndex(graph);
284
+ const seen = /* @__PURE__ */ new Set();
285
+ const result = [];
286
+ const add = (id) => {
287
+ if (seen.has(id)) return;
288
+ seen.add(id);
289
+ const ni = idx.nodeById.get(id);
290
+ if (ni !== void 0) result.push(graph.nodes[ni]);
291
+ };
292
+ for (const eid of idx.outEdges.get(nodeId) ?? []) add(graph.edges[idx.edgeById.get(eid)].targetId);
293
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
294
+ const e = graph.edges[idx.edgeById.get(eid)];
295
+ if (getEdgeMode(graph, e) !== "directed") add(e.sourceId);
296
+ }
297
+ return result;
298
+ }
299
+ /**
300
+ * Returns direct predecessor nodes — nodes from which `nodeId` is reachable
301
+ * by traversing one edge. Edges whose effective mode is not `'directed'` are
302
+ * traversable both ways, so their other endpoint also counts as a predecessor.
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * const graph = createGraph({
307
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
308
+ * edges: [
309
+ * { id: 'e1', sourceId: 'a', targetId: 'c' },
310
+ * { id: 'e2', sourceId: 'b', targetId: 'c' },
311
+ * ],
312
+ * });
313
+ * getPredecessors(graph, 'c');
314
+ * // => [node a, node b]
315
+ * ```
316
+ */
317
+ function getPredecessors(graph, nodeId) {
318
+ const idx = getIndex(graph);
319
+ const seen = /* @__PURE__ */ new Set();
320
+ const result = [];
321
+ const add = (id) => {
322
+ if (seen.has(id)) return;
323
+ seen.add(id);
324
+ const ni = idx.nodeById.get(id);
325
+ if (ni !== void 0) result.push(graph.nodes[ni]);
326
+ };
327
+ for (const eid of idx.inEdges.get(nodeId) ?? []) add(graph.edges[idx.edgeById.get(eid)].sourceId);
328
+ for (const eid of idx.outEdges.get(nodeId) ?? []) {
329
+ const e = graph.edges[idx.edgeById.get(eid)];
330
+ if (getEdgeMode(graph, e) !== "directed") add(e.targetId);
331
+ }
332
+ return result;
333
+ }
334
+ /**
335
+ * Returns all neighbor nodes (successors + predecessors).
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * const graph = createGraph({
340
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
341
+ * edges: [
342
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
343
+ * { id: 'e2', sourceId: 'c', targetId: 'b' },
344
+ * ],
345
+ * });
346
+ * getNeighbors(graph, 'b');
347
+ * // => [node a, node c]
348
+ * ```
349
+ */
350
+ function getNeighbors(graph, nodeId) {
351
+ const idx = getIndex(graph);
352
+ const ids = /* @__PURE__ */ new Set();
353
+ for (const eid of idx.outEdges.get(nodeId) ?? []) ids.add(graph.edges[idx.edgeById.get(eid)].targetId);
354
+ for (const eid of idx.inEdges.get(nodeId) ?? []) ids.add(graph.edges[idx.edgeById.get(eid)].sourceId);
355
+ return [...ids].map((id) => graph.nodes[idx.nodeById.get(id)]).filter(Boolean);
356
+ }
357
+ /**
358
+ * Returns the total degree of a node (number of incident edge endpoints).
359
+ * Each incident edge whose effective mode is not `'directed'` is counted
360
+ * once (a non-directed self-loop counts once; a directed self-loop counts
361
+ * twice — once in, once out).
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * const graph = createGraph({
366
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
367
+ * edges: [
368
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
369
+ * { id: 'e2', sourceId: 'c', targetId: 'b' },
370
+ * ],
371
+ * });
372
+ * getDegree(graph, 'b'); // => 2
373
+ * getDegree(graph, 'a'); // => 1
374
+ * ```
375
+ */
376
+ function getDegree(graph, nodeId) {
377
+ const idx = getIndex(graph);
378
+ const out = idx.outEdges.get(nodeId) ?? [];
379
+ const inE = idx.inEdges.get(nodeId) ?? [];
380
+ let degree = 0;
381
+ const countedNonDirected = /* @__PURE__ */ new Set();
382
+ for (const eid of [...out, ...inE]) {
383
+ const e = graph.edges[idx.edgeById.get(eid)];
384
+ if (getEdgeMode(graph, e) === "directed") degree++;
385
+ else if (!countedNonDirected.has(eid)) {
386
+ countedNonDirected.add(eid);
387
+ degree++;
388
+ }
389
+ }
390
+ return degree;
391
+ }
392
+ /**
393
+ * Returns the in-degree of a node — the number of edges traversable *into*
394
+ * the node. Edges whose effective mode is not `'directed'` count toward both
395
+ * endpoints' in-degree (once per edge).
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * const graph = createGraph({
400
+ * nodes: [{ id: 'a' }, { id: 'b' }],
401
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
402
+ * });
403
+ * getInDegree(graph, 'b'); // => 1
404
+ * getInDegree(graph, 'a'); // => 0
405
+ * ```
406
+ */
407
+ function getInDegree(graph, nodeId) {
408
+ const idx = getIndex(graph);
409
+ let degree = idx.inEdges.get(nodeId)?.length ?? 0;
410
+ for (const eid of idx.outEdges.get(nodeId) ?? []) {
411
+ const e = graph.edges[idx.edgeById.get(eid)];
412
+ if (getEdgeMode(graph, e) !== "directed" && e.targetId !== nodeId) degree++;
413
+ }
414
+ return degree;
415
+ }
416
+ /**
417
+ * Returns the out-degree of a node — the number of edges traversable *out of*
418
+ * the node. Edges whose effective mode is not `'directed'` count toward both
419
+ * endpoints' out-degree (once per edge).
420
+ *
421
+ * @example
422
+ * ```ts
423
+ * const graph = createGraph({
424
+ * nodes: [{ id: 'a' }, { id: 'b' }],
425
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
426
+ * });
427
+ * getOutDegree(graph, 'a'); // => 1
428
+ * getOutDegree(graph, 'b'); // => 0
429
+ * ```
430
+ */
431
+ function getOutDegree(graph, nodeId) {
432
+ const idx = getIndex(graph);
433
+ let degree = idx.outEdges.get(nodeId)?.length ?? 0;
434
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
435
+ const e = graph.edges[idx.edgeById.get(eid)];
436
+ if (getEdgeMode(graph, e) !== "directed" && e.sourceId !== nodeId) degree++;
437
+ }
438
+ return degree;
439
+ }
440
+ /**
441
+ * Returns direct children of a node in the hierarchy.
442
+ * Pass `null` to get root-level nodes.
443
+ *
444
+ * @example
445
+ * ```ts
446
+ * const graph = createGraph({
447
+ * nodes: [
448
+ * { id: 'parent' },
449
+ * { id: 'child1', parentId: 'parent' },
450
+ * { id: 'child2', parentId: 'parent' },
451
+ * ],
452
+ * });
453
+ * getChildren(graph, 'parent');
454
+ * // => [node child1, node child2]
455
+ * getChildren(graph, null);
456
+ * // => [node parent]
457
+ * ```
458
+ */
459
+ function getChildren(graph, nodeId) {
460
+ const idx = getIndex(graph);
461
+ return (idx.childNodes.get(nodeId) ?? []).map((id) => graph.nodes[idx.nodeById.get(id)]).filter(Boolean);
462
+ }
463
+ /**
464
+ * Returns the parent node in the hierarchy, or `undefined` if root-level.
465
+ *
466
+ * @example
467
+ * ```ts
468
+ * const graph = createGraph({
469
+ * nodes: [
470
+ * { id: 'parent' },
471
+ * { id: 'child', parentId: 'parent' },
472
+ * ],
473
+ * });
474
+ * getParent(graph, 'child');
475
+ * // => node parent
476
+ * getParent(graph, 'parent');
477
+ * // => undefined
478
+ * ```
479
+ */
480
+ function getParent(graph, nodeId) {
481
+ const idx = getIndex(graph);
482
+ const ni = idx.nodeById.get(nodeId);
483
+ if (ni === void 0) return void 0;
484
+ const node = graph.nodes[ni];
485
+ if (!node.parentId) return void 0;
486
+ const pi = idx.nodeById.get(node.parentId);
487
+ return pi !== void 0 ? graph.nodes[pi] : void 0;
488
+ }
489
+ /**
490
+ * Returns all ancestors from the node up to the root (nearest parent first).
491
+ *
492
+ * If the parent chain contains a cycle (authored `parentId` cycles are not
493
+ * rejected by `createGraph`), the walk stops at the first repeated node and
494
+ * returns the ancestors collected so far — each ancestor appears exactly once.
495
+ *
496
+ * @example
497
+ * ```ts
498
+ * const graph = createGraph({
499
+ * nodes: [
500
+ * { id: 'root' },
501
+ * { id: 'mid', parentId: 'root' },
502
+ * { id: 'leaf', parentId: 'mid' },
503
+ * ],
504
+ * });
505
+ * getAncestors(graph, 'leaf');
506
+ * // => [node mid, node root]
507
+ * ```
508
+ */
509
+ function getAncestors(graph, nodeId) {
510
+ const idx = getIndex(graph);
511
+ const result = [];
512
+ let ni = idx.nodeById.get(nodeId);
513
+ if (ni === void 0) return result;
514
+ let current = graph.nodes[ni];
515
+ const seen = new Set([nodeId]);
516
+ while (current && current.parentId) {
517
+ if (seen.has(current.parentId)) break;
518
+ const pi = idx.nodeById.get(current.parentId);
519
+ if (pi === void 0) break;
520
+ const p = graph.nodes[pi];
521
+ seen.add(p.id);
522
+ result.push(p);
523
+ current = p;
524
+ }
525
+ return result;
526
+ }
527
+ /**
528
+ * Returns all descendants recursively (depth-first).
529
+ *
530
+ * If the hierarchy contains a parent cycle (authored `parentId` cycles are
531
+ * not rejected by `createGraph`), each node is visited at most once: the walk
532
+ * stops at the first repeated node and returns the descendants collected so far.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * const graph = createGraph({
537
+ * nodes: [
538
+ * { id: 'root' },
539
+ * { id: 'child', parentId: 'root' },
540
+ * { id: 'grandchild', parentId: 'child' },
541
+ * ],
542
+ * });
543
+ * getDescendants(graph, 'root');
544
+ * // => [node child, node grandchild]
545
+ * ```
546
+ */
547
+ function getDescendants(graph, nodeId) {
548
+ const idx = getIndex(graph);
549
+ const result = [];
550
+ const seen = new Set([nodeId]);
551
+ const collect = (id) => {
552
+ const childIds = idx.childNodes.get(id) ?? [];
553
+ for (const childId of childIds) {
554
+ if (seen.has(childId)) continue;
555
+ seen.add(childId);
556
+ const ci = idx.nodeById.get(childId);
557
+ if (ci !== void 0) {
558
+ result.push(graph.nodes[ci]);
559
+ collect(childId);
560
+ }
561
+ }
562
+ };
563
+ collect(nodeId);
564
+ return result;
565
+ }
566
+ /**
567
+ * Returns all root nodes (nodes with no parent).
568
+ *
569
+ * @example
570
+ * ```ts
571
+ * const graph = createGraph({
572
+ * nodes: [
573
+ * { id: 'root1' },
574
+ * { id: 'root2' },
575
+ * { id: 'child', parentId: 'root1' },
576
+ * ],
577
+ * });
578
+ * getRoots(graph);
579
+ * // => [node root1, node root2]
580
+ * ```
581
+ */
582
+ function getRoots(graph) {
583
+ const idx = getIndex(graph);
584
+ return idx.childNodes.get(null)?.map((id) => graph.nodes[idx.nodeById.get(id)]).filter(Boolean) ?? [];
585
+ }
586
+ /**
587
+ * Whether a node has children (is a compound/group node).
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * const graph = createGraph({
592
+ * nodes: [
593
+ * { id: 'parent' },
594
+ * { id: 'child', parentId: 'parent' },
595
+ * ],
596
+ * });
597
+ * isCompound(graph, 'parent'); // => true
598
+ * isCompound(graph, 'child'); // => false
599
+ * ```
600
+ */
601
+ function isCompound(graph, nodeId) {
602
+ return (getIndex(graph).childNodes.get(nodeId) ?? []).length > 0;
603
+ }
604
+ /**
605
+ * Whether a node has no children (is a leaf/atomic node).
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * const graph = createGraph({
610
+ * nodes: [
611
+ * { id: 'parent' },
612
+ * { id: 'child', parentId: 'parent' },
613
+ * ],
614
+ * });
615
+ * isLeaf(graph, 'child'); // => true
616
+ * isLeaf(graph, 'parent'); // => false
617
+ * ```
618
+ */
619
+ function isLeaf(graph, nodeId) {
620
+ return !isCompound(graph, nodeId);
621
+ }
622
+ /**
623
+ * Depth of a node in the hierarchy (root = 0).
624
+ * Returns -1 if the node is not found.
625
+ *
626
+ * If the parent chain contains a cycle (authored `parentId` cycles are not
627
+ * rejected by `createGraph`), the walk stops at the first repeated node and
628
+ * returns the number of unique ancestors walked up to that point.
629
+ *
630
+ * @example
631
+ * ```ts
632
+ * const graph = createGraph({
633
+ * nodes: [
634
+ * { id: 'root' },
635
+ * { id: 'child', parentId: 'root' },
636
+ * { id: 'grandchild', parentId: 'child' },
637
+ * ],
638
+ * });
639
+ * getDepth(graph, 'root'); // => 0
640
+ * getDepth(graph, 'child'); // => 1
641
+ * getDepth(graph, 'grandchild'); // => 2
642
+ * ```
643
+ */
644
+ function getDepth(graph, nodeId) {
645
+ const idx = getIndex(graph);
646
+ let d = 0;
647
+ let ni = idx.nodeById.get(nodeId);
648
+ if (ni === void 0) return -1;
649
+ let current = graph.nodes[ni];
650
+ const seen = new Set([nodeId]);
651
+ while (current.parentId) {
652
+ if (seen.has(current.parentId)) break;
653
+ seen.add(current.parentId);
654
+ d++;
655
+ const pi = idx.nodeById.get(current.parentId);
656
+ if (pi === void 0) break;
657
+ current = graph.nodes[pi];
658
+ }
659
+ return d;
660
+ }
661
+ /**
662
+ * Sibling nodes (same parentId, excluding the node itself).
663
+ *
664
+ * @example
665
+ * ```ts
666
+ * const graph = createGraph({
667
+ * nodes: [
668
+ * { id: 'parent' },
669
+ * { id: 'a', parentId: 'parent' },
670
+ * { id: 'b', parentId: 'parent' },
671
+ * { id: 'c', parentId: 'parent' },
672
+ * ],
673
+ * });
674
+ * getSiblings(graph, 'a');
675
+ * // => [node b, node c]
676
+ * ```
677
+ */
678
+ function getSiblings(graph, nodeId) {
679
+ const idx = getIndex(graph);
680
+ const ni = idx.nodeById.get(nodeId);
681
+ if (ni === void 0) return [];
682
+ const node = graph.nodes[ni];
683
+ return (idx.childNodes.get(node.parentId ?? null) ?? []).filter((id) => id !== nodeId).map((id) => graph.nodes[idx.nodeById.get(id)]).filter(Boolean);
684
+ }
685
+ /**
686
+ * Least Common Ancestor -- deepest proper ancestor of all given nodes.
687
+ * A proper ancestor excludes the input nodes themselves.
688
+ *
689
+ * If a parent chain contains a cycle (authored `parentId` cycles are not
690
+ * rejected by `createGraph`), each chain walk stops at the first repeated
691
+ * node, so every ancestor is considered exactly once.
692
+ *
693
+ * @example
694
+ * ```ts
695
+ * const graph = createGraph({
696
+ * nodes: [
697
+ * { id: 'root' },
698
+ * { id: 'a', parentId: 'root' },
699
+ * { id: 'b', parentId: 'root' },
700
+ * { id: 'a1', parentId: 'a' },
701
+ * ],
702
+ * });
703
+ * getLCA(graph, 'a1', 'b');
704
+ * // => node root
705
+ * getLCA(graph, 'a', 'b');
706
+ * // => node root
707
+ * ```
708
+ */
709
+ function getLCA(graph, ...nodeIds) {
710
+ if (nodeIds.length === 0) return void 0;
711
+ const idx = getIndex(graph);
712
+ const getAncestorChain = (id) => {
713
+ const result = [id];
714
+ const seen = new Set([id]);
715
+ let ni$1 = idx.nodeById.get(id);
716
+ if (ni$1 === void 0) return result;
717
+ let current = graph.nodes[ni$1];
718
+ while (current.parentId) {
719
+ if (seen.has(current.parentId)) break;
720
+ seen.add(current.parentId);
721
+ result.push(current.parentId);
722
+ const pi = idx.nodeById.get(current.parentId);
723
+ if (pi === void 0) break;
724
+ current = graph.nodes[pi];
725
+ }
726
+ return result;
727
+ };
728
+ let common = getAncestorChain(nodeIds[0]);
729
+ for (let i = 1; i < nodeIds.length; i++) {
730
+ const set = new Set(getAncestorChain(nodeIds[i]));
731
+ common = common.filter((id) => set.has(id));
732
+ }
733
+ const inputSet = new Set(nodeIds);
734
+ common = common.filter((id) => !inputSet.has(id));
735
+ if (common.length === 0) return void 0;
736
+ const lcaId = common[0];
737
+ const ni = idx.nodeById.get(lcaId);
738
+ return ni !== void 0 ? graph.nodes[ni] : void 0;
739
+ }
740
+ /**
741
+ * Returns a map of nodeId → shortest-path distance for all sibling nodes
742
+ * (same parentId). Distance is measured from the parent's `initialNodeId`
743
+ * (or `graph.initialNodeId` for root-level nodes).
744
+ *
745
+ * Only follows edges between siblings. Unreachable siblings are omitted.
746
+ *
747
+ * @example Root-level nodes (uses `graph.initialNodeId`):
748
+ * ```ts
749
+ * const graph = createGraph({
750
+ * initialNodeId: 'a',
751
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
752
+ * edges: [
753
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
754
+ * { id: 'e2', sourceId: 'b', targetId: 'c' },
755
+ * ],
756
+ * });
757
+ * getRelativeDistanceMap(graph, null);
758
+ * // => { a: 0, b: 1, c: 2 }
759
+ * ```
760
+ *
761
+ * @example Nested nodes (uses parent's `initialNodeId`):
762
+ * ```ts
763
+ * const graph = createGraph({
764
+ * nodes: [
765
+ * { id: 'parent', initialNodeId: 's1' },
766
+ * { id: 's1', parentId: 'parent' },
767
+ * { id: 's2', parentId: 'parent' },
768
+ * { id: 's3', parentId: 'parent' },
769
+ * ],
770
+ * edges: [
771
+ * { id: 'e1', sourceId: 's1', targetId: 's2' },
772
+ * { id: 'e2', sourceId: 's2', targetId: 's3' },
773
+ * ],
774
+ * });
775
+ * getRelativeDistanceMap(graph, 'parent');
776
+ * // => { s1: 0, s2: 1, s3: 2 }
777
+ * ```
778
+ */
779
+ function getRelativeDistanceMap(graph, parentId) {
780
+ const idx = getIndex(graph);
781
+ let sourceId = null;
782
+ if (parentId !== null) {
783
+ const pi = idx.nodeById.get(parentId);
784
+ if (pi !== void 0) sourceId = graph.nodes[pi].initialNodeId ?? null;
785
+ } else sourceId = graph.initialNodeId ?? null;
786
+ if (!sourceId) return {};
787
+ const siblingSet = new Set(idx.childNodes.get(parentId) ?? []);
788
+ if (!siblingSet.has(sourceId)) return {};
789
+ const dist = /* @__PURE__ */ new Map();
790
+ dist.set(sourceId, 0);
791
+ const queue = [sourceId];
792
+ while (queue.length > 0) {
793
+ const id = queue.shift();
794
+ const d = dist.get(id);
795
+ for (const eid of idx.outEdges.get(id) ?? []) {
796
+ const ai = idx.edgeById.get(eid);
797
+ if (ai === void 0) continue;
798
+ const neighborId = graph.edges[ai].targetId;
799
+ if (siblingSet.has(neighborId) && !dist.has(neighborId)) {
800
+ dist.set(neighborId, d + 1);
801
+ queue.push(neighborId);
802
+ }
803
+ }
804
+ for (const eid of idx.inEdges.get(id) ?? []) {
805
+ const ai = idx.edgeById.get(eid);
806
+ if (ai === void 0) continue;
807
+ const edge = graph.edges[ai];
808
+ if (getEdgeMode(graph, edge) === "directed") continue;
809
+ const neighborId = edge.sourceId;
810
+ if (siblingSet.has(neighborId) && !dist.has(neighborId)) {
811
+ dist.set(neighborId, d + 1);
812
+ queue.push(neighborId);
813
+ }
814
+ }
815
+ }
816
+ const result = {};
817
+ for (const [id, d] of dist) result[id] = d;
818
+ return result;
819
+ }
820
+ /**
821
+ * Returns the shortest-path distance of a node from its parent's initial node.
822
+ * Automatically scopes to the node's sibling group (same `parentId`).
823
+ *
824
+ * Returns `undefined` if the node is not found or unreachable.
825
+ *
826
+ * @example
827
+ * ```ts
828
+ * const graph = createGraph({
829
+ * initialNodeId: 'a',
830
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
831
+ * edges: [
832
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
833
+ * { id: 'e2', sourceId: 'b', targetId: 'c' },
834
+ * ],
835
+ * });
836
+ * getRelativeDistance(graph, 'a'); // => 0
837
+ * getRelativeDistance(graph, 'b'); // => 1
838
+ * getRelativeDistance(graph, 'c'); // => 2
839
+ * ```
840
+ *
841
+ * @example Nested nodes:
842
+ * ```ts
843
+ * const graph = createGraph({
844
+ * nodes: [
845
+ * { id: 'parent', initialNodeId: 's1' },
846
+ * { id: 's1', parentId: 'parent' },
847
+ * { id: 's2', parentId: 'parent' },
848
+ * ],
849
+ * edges: [{ id: 'e1', sourceId: 's1', targetId: 's2' }],
850
+ * });
851
+ * getRelativeDistance(graph, 's1'); // => 0
852
+ * getRelativeDistance(graph, 's2'); // => 1
853
+ * ```
854
+ */
855
+ function getRelativeDistance(graph, nodeId) {
856
+ const ni = getIndex(graph).nodeById.get(nodeId);
857
+ if (ni === void 0) return void 0;
858
+ const node = graph.nodes[ni];
859
+ return getRelativeDistanceMap(graph, node.parentId ?? null)[nodeId];
860
+ }
861
+ /**
862
+ * Nodes with no incoming edges (inDegree 0). A node incident to an edge whose
863
+ * effective mode is not `'directed'` is never a source (the edge points in).
864
+ *
865
+ * @example
866
+ * ```ts
867
+ * const graph = createGraph({
868
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
869
+ * edges: [
870
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
871
+ * { id: 'e2', sourceId: 'b', targetId: 'c' },
872
+ * ],
873
+ * });
874
+ * getSources(graph);
875
+ * // => [node a]
876
+ * ```
877
+ */
878
+ function getSources(graph) {
879
+ return graph.nodes.filter((n) => getInDegree(graph, n.id) === 0);
880
+ }
881
+ /**
882
+ * Nodes with no outgoing edges (outDegree 0). A node incident to an edge whose
883
+ * effective mode is not `'directed'` is never a sink (the edge points out).
884
+ *
885
+ * @example
886
+ * ```ts
887
+ * const graph = createGraph({
888
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
889
+ * edges: [
890
+ * { id: 'e1', sourceId: 'a', targetId: 'b' },
891
+ * { id: 'e2', sourceId: 'b', targetId: 'c' },
892
+ * ],
893
+ * });
894
+ * getSinks(graph);
895
+ * // => [node c]
896
+ * ```
897
+ */
898
+ function getSinks(graph) {
899
+ return graph.nodes.filter((n) => getOutDegree(graph, n.id) === 0);
900
+ }
901
+ /**
902
+ * Get a port by name on a node, or `undefined` if not found.
903
+ *
904
+ * @example
905
+ * ```ts
906
+ * const graph = createGraph({
907
+ * nodes: [{
908
+ * id: 'a',
909
+ * ports: [{ name: 'out', direction: 'out' }],
910
+ * }],
911
+ * });
912
+ * getPort(graph, 'a', 'out'); // => { name: 'out', direction: 'out', ... }
913
+ * getPort(graph, 'a', 'missing'); // => undefined
914
+ * ```
915
+ */
916
+ function getPort(graph, nodeId, portName) {
917
+ const ni = getIndex(graph).nodeById.get(nodeId);
918
+ if (ni === void 0) return void 0;
919
+ return graph.nodes[ni].ports?.find((p) => p.name === portName);
920
+ }
921
+ /**
922
+ * Get all ports on a node. Returns `[]` if the node has no ports or doesn't exist.
923
+ *
924
+ * @example
925
+ * ```ts
926
+ * const graph = createGraph({
927
+ * nodes: [{
928
+ * id: 'a',
929
+ * ports: [
930
+ * { name: 'in', direction: 'in' },
931
+ * { name: 'out', direction: 'out' },
932
+ * ],
933
+ * }],
934
+ * });
935
+ * getPorts(graph, 'a'); // => [port in, port out]
936
+ * ```
937
+ */
938
+ function getPorts(graph, nodeId) {
939
+ const ni = getIndex(graph).nodeById.get(nodeId);
940
+ if (ni === void 0) return [];
941
+ return graph.nodes[ni].ports ?? [];
942
+ }
943
+ /**
944
+ * Get all edges connected to a specific port on a node.
945
+ *
946
+ * Returns edges where:
947
+ * - `sourceId === nodeId && sourcePort === portName`, or
948
+ * - `targetId === nodeId && targetPort === portName`
949
+ *
950
+ * @example
951
+ * ```ts
952
+ * const graph = createGraph({
953
+ * nodes: [
954
+ * { id: 'a', ports: [{ name: 'out', direction: 'out' }] },
955
+ * { id: 'b', ports: [{ name: 'in', direction: 'in' }] },
956
+ * ],
957
+ * edges: [{
958
+ * id: 'e1', sourceId: 'a', targetId: 'b',
959
+ * sourcePort: 'out', targetPort: 'in',
960
+ * }],
961
+ * });
962
+ * getEdgesByPort(graph, 'a', 'out'); // => [edge e1]
963
+ * ```
964
+ */
965
+ function getEdgesByPort(graph, nodeId, portName) {
966
+ const idx = getIndex(graph);
967
+ const result = [];
968
+ for (const eid of idx.outEdges.get(nodeId) ?? []) {
969
+ const ai = idx.edgeById.get(eid);
970
+ if (ai !== void 0 && graph.edges[ai].sourcePort === portName) result.push(graph.edges[ai]);
971
+ }
972
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
973
+ const ai = idx.edgeById.get(eid);
974
+ if (ai !== void 0 && graph.edges[ai].targetPort === portName) result.push(graph.edges[ai]);
975
+ }
976
+ return result;
977
+ }
978
+
979
+ //#endregion
980
+ export { indexAddNode as A, getSinks as C, isLeaf as D, isCompound as E, indexUpdateEdgeEndpoints as M, invalidateIndex as N, getIndex as O, touchIndex as P, getSiblings as S, getSuccessors as T, getPorts as _, getDescendants as a, getRelativeDistanceMap as b, getEdgesOf as c, getLCA as d, getNeighbors as f, getPort as g, getParent as h, getDepth as i, indexReparentNode as j, indexAddEdge as k, getInDegree as l, getOutEdges as m, getChildren as n, getEdgesBetween as o, getOutDegree as p, getDegree as r, getEdgesByPort as s, getAncestors as t, getInEdges as u, getPredecessors as v, getSources as w, getRoots as x, getRelativeDistance as y };