@statelyai/graph 2.0.0 → 2.1.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 (73) hide show
  1. package/README.md +67 -19
  2. package/dist/{algorithms-CsGNehct.d.mts → algorithms-D1cgly0g.d.mts} +145 -6
  3. package/dist/{algorithms-DF1pSQGv.mjs → algorithms-DBpH74hR.mjs} +673 -891
  4. package/dist/algorithms.d.mts +2 -2
  5. package/dist/algorithms.mjs +2 -2
  6. package/dist/config-Dt5u1gSf.mjs +793 -0
  7. package/dist/{converter-DyCJJfTe.mjs → converter-DB6Rg6Vd.mjs} +2 -2
  8. package/dist/formats/adjacency-list/index.d.mts +1 -1
  9. package/dist/formats/adjacency-list/index.mjs +1 -1
  10. package/dist/formats/converter/index.d.mts +1 -1
  11. package/dist/formats/converter/index.mjs +1 -1
  12. package/dist/formats/cytoscape/index.d.mts +4 -4
  13. package/dist/formats/cytoscape/index.mjs +8 -4
  14. package/dist/formats/d2/index.d.mts +1 -1
  15. package/dist/formats/d2/index.mjs +1 -1
  16. package/dist/formats/d3/index.d.mts +4 -4
  17. package/dist/formats/d3/index.mjs +8 -4
  18. package/dist/formats/dot/index.d.mts +1 -1
  19. package/dist/formats/dot/index.mjs +1 -1
  20. package/dist/formats/edge-list/index.d.mts +1 -1
  21. package/dist/formats/edge-list/index.mjs +1 -1
  22. package/dist/formats/elk/index.d.mts +1 -1
  23. package/dist/formats/elk/index.mjs +43 -11
  24. package/dist/formats/gexf/index.d.mts +1 -1
  25. package/dist/formats/gexf/index.mjs +22 -2
  26. package/dist/formats/gml/index.d.mts +4 -4
  27. package/dist/formats/gml/index.mjs +8 -4
  28. package/dist/formats/graphml/index.d.mts +1 -1
  29. package/dist/formats/graphml/index.mjs +24 -2
  30. package/dist/formats/jgf/index.d.mts +4 -4
  31. package/dist/formats/jgf/index.mjs +8 -4
  32. package/dist/formats/mermaid/index.d.mts +1 -1
  33. package/dist/formats/mermaid/index.mjs +1 -1
  34. package/dist/formats/tgf/index.d.mts +4 -4
  35. package/dist/formats/tgf/index.mjs +4 -4
  36. package/dist/formats/xyflow/index.d.mts +12 -6
  37. package/dist/formats/xyflow/index.mjs +11 -6
  38. package/dist/{index-D51lJnt2.d.mts → index-BlbSWUvH.d.mts} +1 -1
  39. package/dist/{index-DWmo1mIp.d.mts → index-CNvqxPLJ.d.mts} +82 -14
  40. package/dist/index.d.mts +6 -6
  41. package/dist/index.mjs +152 -17
  42. package/dist/layout/cytoscape.d.mts +66 -0
  43. package/dist/layout/cytoscape.mjs +114 -0
  44. package/dist/layout/d3-force.d.mts +52 -0
  45. package/dist/layout/d3-force.mjs +127 -0
  46. package/dist/layout/d3-hierarchy.d.mts +39 -0
  47. package/dist/layout/d3-hierarchy.mjs +135 -0
  48. package/dist/layout/dagre.d.mts +32 -0
  49. package/dist/layout/dagre.mjs +99 -0
  50. package/dist/layout/elk.d.mts +47 -0
  51. package/dist/layout/elk.mjs +73 -0
  52. package/dist/layout/forceatlas2.d.mts +48 -0
  53. package/dist/layout/forceatlas2.mjs +100 -0
  54. package/dist/layout/graphviz.d.mts +50 -0
  55. package/dist/layout/graphviz.mjs +179 -0
  56. package/dist/layout/index.d.mts +185 -0
  57. package/dist/layout/index.mjs +181 -0
  58. package/dist/layout/webcola.d.mts +40 -0
  59. package/dist/layout/webcola.mjs +104 -0
  60. package/dist/{queries-BfXeTXRf.d.mts → queries-B6quF529.d.mts} +1 -1
  61. package/dist/{queries-KirMDR7e.mjs → queries-BMM0XAv_.mjs} +23 -17
  62. package/dist/queries.d.mts +1 -1
  63. package/dist/queries.mjs +1 -1
  64. package/dist/schemas.d.mts +19 -1
  65. package/dist/schemas.mjs +10 -1
  66. package/dist/{types-DNYdIU21.d.mts → types-BAEQTwK_.d.mts} +46 -3
  67. package/package.json +47 -5
  68. package/schemas/edge.schema.json +27 -0
  69. package/schemas/graph.schema.json +27 -0
  70. /package/dist/{adjacency-list-GeL1Cu-L.mjs → adjacency-list-DQ32Mmhx.mjs} +0 -0
  71. /package/dist/{edge-list-BcZ0h6zz.mjs → edge-list-CA9UTvn2.mjs} +0 -0
  72. /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
  73. /package/dist/{validate-TtH-x3JV.mjs → validate-BsfSOv0S.mjs} +0 -0
@@ -1,719 +1,7 @@
1
- import { A as indexAddNode, M as indexUpdateEdgeEndpoints, N as invalidateIndex, O as getIndex, P as touchIndex, j as indexReparentNode, k as indexAddEdge, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-KirMDR7e.mjs";
2
- import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
1
+ import { O as getIndex, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-BMM0XAv_.mjs";
2
+ import { n as toNodeConfig, s as createGraph, t as toEdgeConfig } from "./config-Dt5u1gSf.mjs";
3
+ import { t as getEdgeMode } from "./mode-gu_mhKKs.mjs";
3
4
 
4
- //#region src/graph.ts
5
- /**
6
- * Create a resolved graph port from a config. Fills in defaults.
7
- *
8
- * @example
9
- * ```ts
10
- * const port = createGraphPort({ name: 'output', direction: 'out' });
11
- * // { name: 'output', direction: 'out', data: null }
12
- * ```
13
- */
14
- function createGraphPort(config) {
15
- if (!config.name) throw new Error("Port name must be a non-empty string");
16
- const port = {
17
- name: config.name,
18
- direction: config.direction ?? "inout",
19
- data: config.data ?? null
20
- };
21
- if (config.label !== void 0) port.label = config.label;
22
- if (config.x !== void 0) port.x = config.x;
23
- if (config.y !== void 0) port.y = config.y;
24
- if (config.width !== void 0) port.width = config.width;
25
- if (config.height !== void 0) port.height = config.height;
26
- if (config.style !== void 0) port.style = config.style;
27
- return port;
28
- }
29
- function validatePortNames(ports) {
30
- const seen = /* @__PURE__ */ new Set();
31
- for (const port of ports) {
32
- if (seen.has(port.name)) throw new Error(`Duplicate port name "${port.name}" on node`);
33
- seen.add(port.name);
34
- }
35
- }
36
- /**
37
- * Create a resolved graph node from a config. Fills in defaults.
38
- *
39
- * @example
40
- * ```ts
41
- * const node = createGraphNode({ id: 'a', data: { label: 'hi' } });
42
- * // { type: 'node', id: 'a', label: '', data: { label: 'hi' } }
43
- * ```
44
- */
45
- function createGraphNode(config) {
46
- if (!config.id) throw new Error("Node id must be a non-empty string");
47
- if (config.parentId === "") throw new Error("Node parentId must be a non-empty string");
48
- const node = {
49
- type: "node",
50
- id: config.id,
51
- ...config.parentId !== void 0 && { parentId: config.parentId ?? null },
52
- ...config.initialNodeId !== void 0 && { initialNodeId: config.initialNodeId ?? null },
53
- label: config.label ?? null,
54
- data: config.data ?? null
55
- };
56
- if (config.ports !== void 0 && config.ports.length > 0) {
57
- validatePortNames(config.ports);
58
- node.ports = config.ports.map(createGraphPort);
59
- }
60
- if (config.x !== void 0) node.x = config.x;
61
- if (config.y !== void 0) node.y = config.y;
62
- if (config.width !== void 0) node.width = config.width;
63
- if (config.height !== void 0) node.height = config.height;
64
- if (config.shape !== void 0) node.shape = config.shape;
65
- if (config.color !== void 0) node.color = config.color;
66
- if (config.style !== void 0) node.style = config.style;
67
- return node;
68
- }
69
- /**
70
- * Create a resolved graph edge from a config. Fills in defaults.
71
- *
72
- * @example
73
- * ```ts
74
- * const edge = createGraphEdge({ id: 'e1', sourceId: 'a', targetId: 'b' });
75
- * // { type: 'edge', id: 'e1', sourceId: 'a', targetId: 'b', label: null, data: null }
76
- * ```
77
- */
78
- function createGraphEdge(config) {
79
- if (!config.id) throw new Error("Edge id must be a non-empty string");
80
- if (!config.sourceId) throw new Error("Edge sourceId must be a non-empty string");
81
- if (!config.targetId) throw new Error("Edge targetId must be a non-empty string");
82
- const edge = {
83
- type: "edge",
84
- id: config.id,
85
- sourceId: config.sourceId,
86
- targetId: config.targetId,
87
- label: config.label ?? null,
88
- data: config.data ?? null
89
- };
90
- if (config.sourcePort !== void 0) edge.sourcePort = config.sourcePort;
91
- if (config.targetPort !== void 0) edge.targetPort = config.targetPort;
92
- if (config.mode !== void 0) edge.mode = config.mode;
93
- if (config.weight !== void 0) edge.weight = config.weight;
94
- if (config.x !== void 0) edge.x = config.x;
95
- if (config.y !== void 0) edge.y = config.y;
96
- if (config.width !== void 0) edge.width = config.width;
97
- if (config.height !== void 0) edge.height = config.height;
98
- if (config.color !== void 0) edge.color = config.color;
99
- if (config.style !== void 0) edge.style = config.style;
100
- return edge;
101
- }
102
- /**
103
- * Create a graph from a config. Resolves defaults for all fields.
104
- *
105
- * @example
106
- * ```ts
107
- * const graph = createGraph({
108
- * nodes: [{ id: 'a' }, { id: 'b' }],
109
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
110
- * });
111
- * ```
112
- */
113
- function createGraph(config) {
114
- const graph = {
115
- id: config?.id ?? "",
116
- mode: config?.mode ?? "directed",
117
- initialNodeId: config?.initialNodeId ?? null,
118
- nodes: (config?.nodes ?? []).map(createGraphNode),
119
- edges: (config?.edges ?? []).map(createGraphEdge),
120
- data: config?.data ?? null
121
- };
122
- if (config?.direction !== void 0) graph.direction = config.direction;
123
- if (config?.style !== void 0) graph.style = config.style;
124
- return graph;
125
- }
126
- /**
127
- * Create a visual graph with required position/size on all nodes and edges.
128
- *
129
- * @example
130
- * ```ts
131
- * const graph = createVisualGraph({
132
- * nodes: [{ id: 'a', x: 0, y: 0, width: 100, height: 50 }],
133
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', x: 0, y: 0, width: 0, height: 0 }],
134
- * });
135
- * // graph.nodes[0].x === 0
136
- * ```
137
- */
138
- function createVisualGraph(config) {
139
- const base = createGraph(config);
140
- return {
141
- ...base,
142
- direction: config?.direction ?? "down",
143
- nodes: base.nodes.map((n) => {
144
- const { ports, ...rest } = n;
145
- return {
146
- ...rest,
147
- x: n.x ?? 0,
148
- y: n.y ?? 0,
149
- width: n.width ?? 0,
150
- height: n.height ?? 0,
151
- ...n.shape !== void 0 && { shape: n.shape },
152
- ...ports !== void 0 && { ports: ports.map((p) => ({
153
- ...p,
154
- x: p.x ?? 0,
155
- y: p.y ?? 0,
156
- width: p.width ?? 0,
157
- height: p.height ?? 0
158
- })) }
159
- };
160
- }),
161
- edges: base.edges.map((e) => ({
162
- ...e,
163
- x: e.x ?? 0,
164
- y: e.y ?? 0,
165
- width: e.width ?? 0,
166
- height: e.height ?? 0
167
- }))
168
- };
169
- }
170
- /**
171
- * Create a graph by BFS exploration of a transition function.
172
- * Each unique state becomes a node; each (state, event) -> nextState becomes an edge.
173
- *
174
- * - Node IDs are determined by `serializeState` (default: `JSON.stringify`).
175
- * - Edge IDs use the format `sourceId|serializedEvent|targetId` for uniqueness
176
- * and debuggability. Edge labels are just the serialized event string.
177
- *
178
- * @example
179
- * ```ts
180
- * const graph = createGraphFromTransition(
181
- * (state, event) => {
182
- * if (state === 'green' && event === 'TIMER') return 'yellow';
183
- * if (state === 'yellow' && event === 'TIMER') return 'red';
184
- * if (state === 'red' && event === 'TIMER') return 'green';
185
- * return state;
186
- * },
187
- * {
188
- * initialState: 'green',
189
- * events: ['TIMER'],
190
- * serializeState: (s) => s,
191
- * serializeEvent: (e) => e,
192
- * },
193
- * );
194
- * // graph.nodes.length === 3
195
- * ```
196
- */
197
- function createGraphFromTransition(transition, options) {
198
- const serializeState = options.serializeState ?? JSON.stringify;
199
- const serializeEvent = options.serializeEvent ?? JSON.stringify;
200
- const limit = options.limit ?? Infinity;
201
- const getEvents = typeof options.events === "function" ? options.events : () => options.events;
202
- const nodes = [];
203
- const edges = [];
204
- const visited = /* @__PURE__ */ new Set();
205
- const edgeSet = /* @__PURE__ */ new Set();
206
- const queue = [options.initialState];
207
- const initialStateId = serializeState(options.initialState);
208
- visited.add(initialStateId);
209
- nodes.push({
210
- id: initialStateId,
211
- label: initialStateId,
212
- data: options.initialState
213
- });
214
- let iterations = 0;
215
- while (queue.length > 0) {
216
- const state = queue.shift();
217
- const stateId = serializeState(state);
218
- if (++iterations > limit) throw new Error("Traversal limit exceeded");
219
- if (options.stopWhen?.(state)) continue;
220
- const events = getEvents(state);
221
- for (const event of events) {
222
- const nextState = transition(state, event);
223
- const nextStateId = serializeState(nextState);
224
- const eventStr = serializeEvent(event);
225
- if (!visited.has(nextStateId)) {
226
- visited.add(nextStateId);
227
- nodes.push({
228
- id: nextStateId,
229
- label: nextStateId,
230
- data: nextState
231
- });
232
- queue.push(nextState);
233
- }
234
- const edgeKey = `${stateId}|${eventStr}|${nextStateId}`;
235
- if (!edgeSet.has(edgeKey)) {
236
- edgeSet.add(edgeKey);
237
- edges.push({
238
- id: edgeKey,
239
- sourceId: stateId,
240
- targetId: nextStateId,
241
- label: eventStr,
242
- data: event
243
- });
244
- }
245
- }
246
- }
247
- return createGraph({
248
- id: options.id ?? "",
249
- mode: "directed",
250
- initialNodeId: initialStateId,
251
- nodes,
252
- edges
253
- });
254
- }
255
- /**
256
- * Get a node by id, or `undefined` if not found.
257
- *
258
- * @example
259
- * ```ts
260
- * const graph = createGraph({ nodes: [{ id: 'a' }] });
261
- * const node = getNode(graph, 'a'); // GraphNode
262
- * const missing = getNode(graph, 'z'); // undefined
263
- * ```
264
- */
265
- function getNode(graph, id) {
266
- const arrayIdx = getIndex(graph).nodeById.get(id);
267
- return arrayIdx !== void 0 ? graph.nodes[arrayIdx] : void 0;
268
- }
269
- /**
270
- * Get an edge by id, or `undefined` if not found.
271
- *
272
- * @example
273
- * ```ts
274
- * const graph = createGraph({
275
- * nodes: [{ id: 'a' }, { id: 'b' }],
276
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
277
- * });
278
- * const edge = getEdge(graph, 'e1'); // GraphEdge
279
- * const missing = getEdge(graph, 'z'); // undefined
280
- * ```
281
- */
282
- function getEdge(graph, id) {
283
- const arrayIdx = getIndex(graph).edgeById.get(id);
284
- return arrayIdx !== void 0 ? graph.edges[arrayIdx] : void 0;
285
- }
286
- /**
287
- * Check if a node exists in the graph.
288
- *
289
- * @example
290
- * ```ts
291
- * const graph = createGraph({ nodes: [{ id: 'a' }] });
292
- * hasNode(graph, 'a'); // true
293
- * hasNode(graph, 'z'); // false
294
- * ```
295
- */
296
- function hasNode(graph, id) {
297
- return getIndex(graph).nodeById.has(id);
298
- }
299
- /**
300
- * Check if an edge exists in the graph.
301
- *
302
- * @example
303
- * ```ts
304
- * const graph = createGraph({
305
- * nodes: [{ id: 'a' }, { id: 'b' }],
306
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
307
- * });
308
- * hasEdge(graph, 'e1'); // true
309
- * hasEdge(graph, 'z'); // false
310
- * ```
311
- */
312
- function hasEdge(graph, id) {
313
- return getIndex(graph).edgeById.has(id);
314
- }
315
- /**
316
- * **Mutable.** Add a node to the graph. Mutates `graph.nodes` in place.
317
- * @returns The resolved node that was added.
318
- *
319
- * @example
320
- * ```ts
321
- * const graph = createGraph();
322
- * const node = addNode(graph, { id: 'a', label: 'Node A' });
323
- * // graph.nodes.length === 1
324
- * ```
325
- */
326
- function addNode(graph, config) {
327
- const node = createGraphNode(config);
328
- const idx = getIndex(graph);
329
- if (idx.nodeById.has(config.id)) throw new Error(`Node "${config.id}" already exists`);
330
- if (config.parentId && !idx.nodeById.has(config.parentId)) throw new Error(`Parent node "${config.parentId}" does not exist`);
331
- indexAddNode(idx, node, graph.nodes.push(node) - 1);
332
- return node;
333
- }
334
- /**
335
- * **Mutable.** Add an edge to the graph. Mutates `graph.edges` in place.
336
- * @returns The resolved edge that was added.
337
- *
338
- * @example
339
- * ```ts
340
- * const graph = createGraph({ nodes: [{ id: 'a' }, { id: 'b' }] });
341
- * const edge = addEdge(graph, { id: 'e1', sourceId: 'a', targetId: 'b' });
342
- * // graph.edges.length === 1
343
- * ```
344
- */
345
- function addEdge(graph, config) {
346
- const edge = createGraphEdge(config);
347
- const idx = getIndex(graph);
348
- if (idx.edgeById.has(config.id)) throw new Error(`Edge "${config.id}" already exists`);
349
- if (!idx.nodeById.has(config.sourceId)) throw new Error(`Source node "${config.sourceId}" does not exist`);
350
- if (!idx.nodeById.has(config.targetId)) throw new Error(`Target node "${config.targetId}" does not exist`);
351
- if (config.sourcePort !== void 0) {
352
- if (!graph.nodes[idx.nodeById.get(config.sourceId)].ports?.some((p) => p.name === config.sourcePort)) throw new Error(`Port "${config.sourcePort}" does not exist on source node "${config.sourceId}"`);
353
- }
354
- if (config.targetPort !== void 0) {
355
- if (!graph.nodes[idx.nodeById.get(config.targetId)].ports?.some((p) => p.name === config.targetPort)) throw new Error(`Port "${config.targetPort}" does not exist on target node "${config.targetId}"`);
356
- }
357
- indexAddEdge(idx, edge, graph.edges.push(edge) - 1);
358
- return edge;
359
- }
360
- /**
361
- * **Mutable.** Delete a node and its connected edges. Mutates `graph.nodes`
362
- * and `graph.edges` in place.
363
- *
364
- * By default, children are deleted recursively.
365
- * With `{ reparent: true }`, children are re-parented to the deleted node's parent.
366
- *
367
- * @example
368
- * ```ts
369
- * const graph = createGraph({
370
- * nodes: [{ id: 'a' }, { id: 'b' }],
371
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
372
- * });
373
- * deleteNode(graph, 'a');
374
- * // graph.nodes.length === 1, edge e1 also removed
375
- * ```
376
- */
377
- function deleteNode(graph, id, opts) {
378
- const node = getNode(graph, id);
379
- if (!node) throw new Error(`Node "${id}" does not exist`);
380
- if (opts?.reparent) {
381
- for (const n of graph.nodes) if (n.parentId === id) n.parentId = node.parentId;
382
- graph.nodes = graph.nodes.filter((n) => n.id !== id);
383
- graph.edges = graph.edges.filter((e) => e.sourceId !== id && e.targetId !== id);
384
- } else {
385
- const toDelete = collectDescendants(graph, id);
386
- graph.nodes = graph.nodes.filter((n) => !toDelete.has(n.id));
387
- graph.edges = graph.edges.filter((e) => !toDelete.has(e.sourceId) && !toDelete.has(e.targetId));
388
- }
389
- invalidateIndex(graph);
390
- }
391
- /**
392
- * **Mutable.** Delete an edge. Mutates `graph.edges` in place.
393
- *
394
- * @example
395
- * ```ts
396
- * const graph = createGraph({
397
- * nodes: [{ id: 'a' }, { id: 'b' }],
398
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
399
- * });
400
- * deleteEdge(graph, 'e1');
401
- * // graph.edges.length === 0
402
- * ```
403
- */
404
- function deleteEdge(graph, id) {
405
- if (!hasEdge(graph, id)) throw new Error(`Edge "${id}" does not exist`);
406
- graph.edges = graph.edges.filter((e) => e.id !== id);
407
- invalidateIndex(graph);
408
- }
409
- /** Optional fields where `null` in an update unsets the field. */
410
- const NODE_OPTIONAL_KEYS = [
411
- "x",
412
- "y",
413
- "width",
414
- "height",
415
- "shape",
416
- "color",
417
- "style"
418
- ];
419
- const EDGE_OPTIONAL_KEYS = [
420
- "weight",
421
- "mode",
422
- "x",
423
- "y",
424
- "width",
425
- "height",
426
- "color",
427
- "style"
428
- ];
429
- /** Apply optional-field updates: `null` unsets, a value sets, `undefined` is ignored. */
430
- function applyOptionalUpdates(target, update, keys) {
431
- for (const key of keys) {
432
- const value = update[key];
433
- if (value === void 0) continue;
434
- if (value === null) delete target[key];
435
- else target[key] = value;
436
- }
437
- }
438
- /**
439
- * **Mutable.** Update a node in place.
440
- * Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
441
- * `ports`) accept `null` to unset; `undefined` leaves them unchanged.
442
- * @returns The updated node.
443
- *
444
- * @example
445
- * ```ts
446
- * const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
447
- * const updated = updateNode(graph, 'a', { label: 'new', x: 100 });
448
- * // updated.label === 'new', updated.x === 100
449
- * ```
450
- */
451
- function updateNode(graph, id, update) {
452
- const idx = getIndex(graph);
453
- const arrayIdx = idx.nodeById.get(id);
454
- if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
455
- if (update.parentId !== void 0 && update.parentId !== null) {
456
- if (!idx.nodeById.has(update.parentId)) throw new Error(`Parent node "${update.parentId}" does not exist`);
457
- let ancestorId = update.parentId;
458
- const seen = /* @__PURE__ */ new Set();
459
- while (ancestorId !== null && !seen.has(ancestorId)) {
460
- if (ancestorId === id) throw new Error(`Cannot set parentId of node "${id}" to "${update.parentId}": "${update.parentId}" is "${id}" or one of its descendants, which would create a hierarchy cycle. Reparent "${update.parentId}" elsewhere first.`);
461
- seen.add(ancestorId);
462
- const ai = idx.nodeById.get(ancestorId);
463
- ancestorId = ai !== void 0 ? graph.nodes[ai].parentId ?? null : null;
464
- }
465
- }
466
- if (update.ports != null && update.ports.length > 0) validatePortNames(update.ports);
467
- const node = graph.nodes[arrayIdx];
468
- if (update.ports !== void 0) {
469
- const newPortNames = new Set((update.ports ?? []).map((p) => p.name));
470
- for (const eid of idx.outEdges.get(id) ?? []) {
471
- const e = graph.edges[idx.edgeById.get(eid)];
472
- if (e.sourcePort !== void 0 && !newPortNames.has(e.sourcePort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.sourcePort}" via sourcePort. Keep that port, or update/delete the edge first.`);
473
- }
474
- for (const eid of idx.inEdges.get(id) ?? []) {
475
- const e = graph.edges[idx.edgeById.get(eid)];
476
- if (e.targetPort !== void 0 && !newPortNames.has(e.targetPort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.targetPort}" via targetPort. Keep that port, or update/delete the edge first.`);
477
- }
478
- }
479
- const oldParentId = node.parentId;
480
- const updated = {
481
- ...node,
482
- ...update.parentId !== void 0 && { parentId: update.parentId ?? null },
483
- ...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
484
- ...update.label !== void 0 && { label: update.label },
485
- ...update.data !== void 0 && { data: update.data }
486
- };
487
- if (update.ports !== void 0) if (update.ports === null) delete updated.ports;
488
- else updated.ports = update.ports.map(createGraphPort);
489
- applyOptionalUpdates(updated, update, NODE_OPTIONAL_KEYS);
490
- graph.nodes[arrayIdx] = updated;
491
- if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
492
- return updated;
493
- }
494
- /**
495
- * **Mutable.** Update an edge in place.
496
- * Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
497
- * `width`, `height`, `color`, `style`) accept `null` to unset; `undefined`
498
- * leaves them unchanged.
499
- * @returns The updated edge.
500
- *
501
- * @example
502
- * ```ts
503
- * const graph = createGraph({
504
- * nodes: [{ id: 'a' }, { id: 'b' }],
505
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
506
- * });
507
- * const updated = updateEdge(graph, 'e1', { label: 'new', weight: 2 });
508
- * // updated.label === 'new', updated.weight === 2
509
- * ```
510
- */
511
- function updateEdge(graph, id, update) {
512
- const idx = getIndex(graph);
513
- const arrayIdx = idx.edgeById.get(id);
514
- if (arrayIdx === void 0) throw new Error(`Edge "${id}" does not exist`);
515
- if (update.sourceId !== void 0 && !idx.nodeById.has(update.sourceId)) throw new Error(`Source node "${update.sourceId}" does not exist`);
516
- if (update.targetId !== void 0 && !idx.nodeById.has(update.targetId)) throw new Error(`Target node "${update.targetId}" does not exist`);
517
- const edge = graph.edges[arrayIdx];
518
- const oldSourceId = edge.sourceId;
519
- const oldTargetId = edge.targetId;
520
- const effectiveSourceId = update.sourceId ?? edge.sourceId;
521
- const effectiveTargetId = update.targetId ?? edge.targetId;
522
- const effectiveSourcePort = update.sourcePort !== void 0 ? update.sourcePort ?? void 0 : edge.sourcePort;
523
- const effectiveTargetPort = update.targetPort !== void 0 ? update.targetPort ?? void 0 : edge.targetPort;
524
- if (effectiveSourcePort !== void 0) {
525
- if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === effectiveSourcePort)) throw new Error(update.sourcePort !== void 0 ? `Port "${effectiveSourcePort}" does not exist on source node "${effectiveSourceId}"` : `Cannot update edge "${id}": its sourcePort "${effectiveSourcePort}" does not exist on the new source node "${effectiveSourceId}". Include sourcePort in the update (a port on "${effectiveSourceId}", or null to clear it).`);
526
- }
527
- if (effectiveTargetPort !== void 0) {
528
- if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === effectiveTargetPort)) throw new Error(update.targetPort !== void 0 ? `Port "${effectiveTargetPort}" does not exist on target node "${effectiveTargetId}"` : `Cannot update edge "${id}": its targetPort "${effectiveTargetPort}" does not exist on the new target node "${effectiveTargetId}". Include targetPort in the update (a port on "${effectiveTargetId}", or null to clear it).`);
529
- }
530
- const updated = {
531
- ...edge,
532
- ...update.sourceId !== void 0 && { sourceId: update.sourceId },
533
- ...update.targetId !== void 0 && { targetId: update.targetId },
534
- ...update.label !== void 0 && { label: update.label },
535
- ...update.data !== void 0 && { data: update.data }
536
- };
537
- if (update.sourcePort !== void 0) if (update.sourcePort === null) delete updated.sourcePort;
538
- else updated.sourcePort = update.sourcePort;
539
- if (update.targetPort !== void 0) if (update.targetPort === null) delete updated.targetPort;
540
- else updated.targetPort = update.targetPort;
541
- applyOptionalUpdates(updated, update, EDGE_OPTIONAL_KEYS);
542
- graph.edges[arrayIdx] = updated;
543
- if (update.mode !== void 0 || update.weight !== void 0) touchIndex(idx);
544
- if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
545
- return updated;
546
- }
547
- /**
548
- * **Mutable.** Add multiple nodes and edges to the graph.
549
- * Nodes are added first, then edges (so edges can reference new nodes).
550
- *
551
- * @example
552
- * ```ts
553
- * const graph = createGraph();
554
- * addEntities(graph, {
555
- * nodes: [{ id: 'a' }, { id: 'b' }],
556
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
557
- * });
558
- * // graph.nodes.length === 2, graph.edges.length === 1
559
- * ```
560
- */
561
- function addEntities(graph, entities) {
562
- for (const nodeConfig of entities.nodes ?? []) addNode(graph, nodeConfig);
563
- for (const edgeConfig of entities.edges ?? []) addEdge(graph, edgeConfig);
564
- }
565
- /**
566
- * **Mutable.** Delete entities by id(s). Automatically detects whether each id
567
- * is a node or edge. Node deletions cascade to children and connected edges.
568
- *
569
- * @example
570
- * ```ts
571
- * const graph = createGraph({
572
- * nodes: [{ id: 'a' }, { id: 'b' }],
573
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
574
- * });
575
- * deleteEntities(graph, ['a', 'e1']);
576
- * // graph.nodes.length === 1, graph.edges.length === 0
577
- * ```
578
- */
579
- function deleteEntities(graph, ids, opts) {
580
- const idArray = Array.isArray(ids) ? ids : [ids];
581
- for (const id of idArray) if (hasNode(graph, id)) deleteNode(graph, id, opts);
582
- else if (hasEdge(graph, id)) deleteEdge(graph, id);
583
- }
584
- /**
585
- * **Mutable.** Update multiple nodes and edges in place.
586
- * Each entry must include an `id` to identify which entity to update.
587
- *
588
- * @example
589
- * ```ts
590
- * const graph = createGraph({
591
- * nodes: [{ id: 'a', label: 'old' }],
592
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', label: 'old' }],
593
- * });
594
- * updateEntities(graph, {
595
- * nodes: [{ id: 'a', label: 'new' }],
596
- * edges: [{ id: 'e1', label: 'new' }],
597
- * });
598
- * ```
599
- */
600
- function updateEntities(graph, updates) {
601
- for (const nodeUpdate of updates.nodes ?? []) {
602
- const { id, ...patch } = nodeUpdate;
603
- updateNode(graph, id, patch);
604
- }
605
- for (const edgeUpdate of updates.edges ?? []) {
606
- const { id, ...patch } = edgeUpdate;
607
- updateEdge(graph, id, patch);
608
- }
609
- }
610
- /**
611
- * OOP wrapper around a plain `Graph` object.
612
- * Delegates to the standalone mutable functions.
613
- *
614
- * @example
615
- * ```ts
616
- * const instance = new GraphInstance({
617
- * nodes: [{ id: 'a' }, { id: 'b' }],
618
- * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
619
- * });
620
- * instance.addNode({ id: 'c' });
621
- * instance.hasNode('c'); // true
622
- * instance.toJSON(); // plain Graph object
623
- * ```
624
- */
625
- var GraphInstance = class GraphInstance {
626
- graph;
627
- constructor(config) {
628
- this.graph = createGraph(config);
629
- }
630
- /**
631
- * Wrap an existing plain graph object.
632
- *
633
- * @example
634
- * ```ts
635
- * const graph = createGraph({ nodes: [{ id: 'a' }] });
636
- * const instance = GraphInstance.from(graph);
637
- * instance.hasNode('a'); // true
638
- * ```
639
- */
640
- static from(graph) {
641
- const instance = Object.create(GraphInstance.prototype);
642
- instance.graph = graph;
643
- return instance;
644
- }
645
- get id() {
646
- return this.graph.id;
647
- }
648
- /** Default directedness for all edges. */
649
- get mode() {
650
- return this.graph.mode;
651
- }
652
- get nodes() {
653
- return this.graph.nodes;
654
- }
655
- get edges() {
656
- return this.graph.edges;
657
- }
658
- get data() {
659
- return this.graph.data;
660
- }
661
- getNode(id) {
662
- return getNode(this.graph, id);
663
- }
664
- getEdge(id) {
665
- return getEdge(this.graph, id);
666
- }
667
- hasNode(id) {
668
- return hasNode(this.graph, id);
669
- }
670
- hasEdge(id) {
671
- return hasEdge(this.graph, id);
672
- }
673
- addNode(config) {
674
- return addNode(this.graph, config);
675
- }
676
- addEdge(config) {
677
- return addEdge(this.graph, config);
678
- }
679
- deleteNode(id, opts) {
680
- return deleteNode(this.graph, id, opts);
681
- }
682
- deleteEdge(id) {
683
- return deleteEdge(this.graph, id);
684
- }
685
- updateNode(id, update) {
686
- return updateNode(this.graph, id, update);
687
- }
688
- updateEdge(id, update) {
689
- return updateEdge(this.graph, id, update);
690
- }
691
- addEntities(entities) {
692
- return addEntities(this.graph, entities);
693
- }
694
- deleteEntities(ids, opts) {
695
- return deleteEntities(this.graph, ids, opts);
696
- }
697
- updateEntities(updates) {
698
- return updateEntities(this.graph, updates);
699
- }
700
- toJSON() {
701
- return this.graph;
702
- }
703
- };
704
- function collectDescendants(graph, id) {
705
- const idx = getIndex(graph);
706
- const toDelete = /* @__PURE__ */ new Set();
707
- const walk = (nodeId) => {
708
- toDelete.add(nodeId);
709
- const childIds = idx.childNodes.get(nodeId) ?? [];
710
- for (const childId of childIds) if (!toDelete.has(childId)) walk(childId);
711
- };
712
- walk(id);
713
- return toDelete;
714
- }
715
-
716
- //#endregion
717
5
  //#region src/algorithms/shared.ts
718
6
  var MinPriorityQueue = class {
719
7
  items = [];
@@ -764,6 +52,19 @@ var MinPriorityQueue = class {
764
52
  }
765
53
  };
766
54
  /**
55
+ * Seeded PRNG (mulberry32). Same generator as src/walks.ts — kept here so
56
+ * algorithm modules and generators can share it without importing walks.
57
+ */
58
+ function mulberry32(seed) {
59
+ let s = seed | 0;
60
+ return () => {
61
+ s = s + 1831565813 | 0;
62
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
63
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
64
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
65
+ };
66
+ }
67
+ /**
767
68
  * Classify a graph by the *effective* mode of its edges (per-edge `mode`
768
69
  * overrides included): all-directed, all-non-directed, or genuinely mixed.
769
70
  * Edge-less graphs fall back to `graph.mode`.
@@ -954,19 +255,84 @@ function buildCSR(graph) {
954
255
 
955
256
  //#endregion
956
257
  //#region src/algorithms/paths.ts
258
+ /**
259
+ * Flat binary min-heap of `(distance, node position)` entries in parallel
260
+ * typed arrays. The Dijkstra/A* hot loops push one entry per relaxation, so
261
+ * avoiding a `{ pos, dist }` wrapper object per push (allocation + property
262
+ * loads in the sift comparisons) is a measurable win on 10k+ node graphs.
263
+ * Sifts move "holes" instead of swapping, halving array writes.
264
+ */
265
+ var TypedMinHeap = class {
266
+ keys;
267
+ vals;
268
+ size = 0;
269
+ constructor(capacity) {
270
+ const cap = Math.max(capacity, 16);
271
+ this.keys = new Float64Array(cap);
272
+ this.vals = new Int32Array(cap);
273
+ }
274
+ push(key, val) {
275
+ if (this.size === this.keys.length) {
276
+ const keys$1 = new Float64Array(this.keys.length * 2);
277
+ const vals$1 = new Int32Array(this.vals.length * 2);
278
+ keys$1.set(this.keys);
279
+ vals$1.set(this.vals);
280
+ this.keys = keys$1;
281
+ this.vals = vals$1;
282
+ }
283
+ const { keys, vals } = this;
284
+ let hole = this.size++;
285
+ while (hole > 0) {
286
+ const parent = hole - 1 >> 1;
287
+ if (keys[parent] <= key) break;
288
+ keys[hole] = keys[parent];
289
+ vals[hole] = vals[parent];
290
+ hole = parent;
291
+ }
292
+ keys[hole] = key;
293
+ vals[hole] = val;
294
+ }
295
+ /** Key of the minimum entry; garbage when empty (check `size` first). */
296
+ peekKey() {
297
+ return this.keys[0];
298
+ }
299
+ /** Value of the minimum entry; garbage when empty (check `size` first). */
300
+ peekVal() {
301
+ return this.vals[0];
302
+ }
303
+ /** Remove the minimum entry (no-op shape: read via peek* first). */
304
+ pop() {
305
+ const { keys, vals } = this;
306
+ const last = --this.size;
307
+ if (last === 0) return;
308
+ const key = keys[last];
309
+ const val = vals[last];
310
+ let hole = 0;
311
+ for (;;) {
312
+ let child = hole * 2 + 1;
313
+ if (child >= last) break;
314
+ const right = child + 1;
315
+ if (right < last && keys[right] < keys[child]) child = right;
316
+ if (keys[child] >= key) break;
317
+ keys[hole] = keys[child];
318
+ vals[hole] = vals[child];
319
+ hole = child;
320
+ }
321
+ keys[hole] = key;
322
+ vals[hole] = val;
323
+ }
324
+ };
957
325
  function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
958
- if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
959
- const dist = /* @__PURE__ */ new Map();
960
- const prev = /* @__PURE__ */ new Map();
961
- dist.set(sourceId, 0);
962
- prev.set(sourceId, []);
326
+ if (algorithm === "bellman-ford") return bellmanFordTyped(graph, sourceId, getWeight);
963
327
  const csr = getCSR(graph);
328
+ const n = csr.ids.length;
964
329
  const source = csr.indexOf.get(sourceId);
965
330
  if (source === void 0) return {
966
- dist,
967
- prev
331
+ source: -1,
332
+ distArr: new Float64Array(0),
333
+ prevArr: [],
334
+ stopDistance: Infinity
968
335
  };
969
- const n = csr.ids.length;
970
336
  const distArr = new Float64Array(n).fill(Infinity);
971
337
  const prevArr = new Array(n);
972
338
  distArr[source] = 0;
@@ -996,13 +362,12 @@ function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtI
996
362
  } else {
997
363
  const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
998
364
  const visited = new Uint8Array(n);
999
- const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
1000
- pq.push({
1001
- pos: source,
1002
- dist: 0
1003
- });
365
+ const pq = new TypedMinHeap(n);
366
+ pq.push(0, source);
1004
367
  while (pq.size > 0) {
1005
- const { pos: u, dist: distance } = pq.pop();
368
+ const distance = pq.peekKey();
369
+ const u = pq.peekVal();
370
+ pq.pop();
1006
371
  if (visited[u] || distance !== distArr[u]) continue;
1007
372
  if (distance > stopDistance) break;
1008
373
  if (u === stopAt) stopDistance = distance;
@@ -1016,28 +381,48 @@ function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtI
1016
381
  if (nextDistance < distArr[v]) {
1017
382
  distArr[v] = nextDistance;
1018
383
  prevArr[v] = [u, csr.outEdgeIndex[a]];
1019
- pq.push({
1020
- pos: v,
1021
- dist: nextDistance
1022
- });
384
+ pq.push(nextDistance, v);
1023
385
  } else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
1024
386
  }
1025
387
  }
1026
388
  }
1027
- for (let i = 0; i < n; i++) {
1028
- if (distArr[i] === Infinity || distArr[i] > stopDistance) continue;
1029
- dist.set(csr.ids[i], distArr[i]);
1030
- const pairs = prevArr[i];
1031
- const predecessors = [];
1032
- for (let k = 0; k < pairs.length; k += 2) predecessors.push({
1033
- from: csr.ids[pairs[k]],
1034
- edge: graph.edges[pairs[k + 1]]
1035
- });
1036
- prev.set(csr.ids[i], predecessors);
389
+ return {
390
+ source,
391
+ distArr,
392
+ prevArr,
393
+ stopDistance
394
+ };
395
+ }
396
+ /**
397
+ * Bellman-Ford adapted to the typed-array result shape. The O(VE) relaxation
398
+ * dominates, so the id→position conversion here is noise — and it keeps a
399
+ * single reconstruction path for both algorithms.
400
+ */
401
+ function bellmanFordTyped(graph, sourceId, getWeight) {
402
+ const { dist, prev } = bellmanFord(graph, sourceId, getWeight);
403
+ const csr = getCSR(graph);
404
+ const idx = getIndex(graph);
405
+ const n = csr.ids.length;
406
+ const distArr = new Float64Array(n).fill(Infinity);
407
+ const prevArr = new Array(n);
408
+ for (const [id, distance] of dist) {
409
+ const pos = csr.indexOf.get(id);
410
+ if (pos === void 0) continue;
411
+ distArr[pos] = distance;
412
+ const pairs = [];
413
+ for (const { from, edge } of prev.get(id) ?? []) {
414
+ const fromPos = csr.indexOf.get(from);
415
+ const edgeIndex = idx.edgeById.get(edge.id);
416
+ if (fromPos === void 0 || edgeIndex === void 0) continue;
417
+ pairs.push(fromPos, edgeIndex);
418
+ }
419
+ prevArr[pos] = pairs;
1037
420
  }
1038
421
  return {
1039
- dist,
1040
- prev
422
+ source: csr.indexOf.get(sourceId) ?? -1,
423
+ distArr,
424
+ prevArr,
425
+ stopDistance: Infinity
1041
426
  };
1042
427
  }
1043
428
  function bellmanFord(graph, sourceId, getWeight) {
@@ -1100,22 +485,23 @@ function bellmanFord(graph, sourceId, getWeight) {
1100
485
  prev
1101
486
  };
1102
487
  }
1103
- function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PURE__ */ new Set()) {
1104
- if (targetId === sourceNode.id) {
488
+ function* reconstructPathsAt(graph, prevArr, sourceNode, sourcePos, targetPos, onPath = /* @__PURE__ */ new Set()) {
489
+ if (targetPos === sourcePos) {
1105
490
  yield {
1106
491
  source: sourceNode,
1107
492
  steps: []
1108
493
  };
1109
494
  return;
1110
495
  }
1111
- const predecessors = prev.get(targetId);
1112
- if (!predecessors || predecessors.length === 0) return;
1113
- const targetNi = getIndex(graph).nodeById.get(targetId);
1114
- const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((node) => node.id === targetId);
1115
- onPath.add(targetId);
1116
- for (const { from, edge } of predecessors) {
1117
- if (onPath.has(from)) continue;
1118
- for (const prefix of reconstructPaths(graph, prev, sourceNode, from, onPath)) yield {
496
+ const pairs = prevArr[targetPos];
497
+ if (!pairs || pairs.length === 0) return;
498
+ const targetNode = graph.nodes[targetPos];
499
+ onPath.add(targetPos);
500
+ for (let k = 0; k < pairs.length; k += 2) {
501
+ const fromPos = pairs[k];
502
+ if (onPath.has(fromPos)) continue;
503
+ const edge = graph.edges[pairs[k + 1]];
504
+ for (const prefix of reconstructPathsAt(graph, prevArr, sourceNode, sourcePos, fromPos, onPath)) yield {
1119
505
  source: sourceNode,
1120
506
  steps: [...prefix.steps, {
1121
507
  edge,
@@ -1123,16 +509,30 @@ function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PUR
1123
509
  }]
1124
510
  };
1125
511
  }
1126
- onPath.delete(targetId);
512
+ onPath.delete(targetPos);
1127
513
  }
1128
514
  function* genShortestPaths(graph, opts) {
1129
- const idx = getIndex(graph);
1130
515
  const sourceId = resolveFrom(graph, opts);
1131
- const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
1132
- const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
1133
- const sourceNi = idx.nodeById.get(sourceId);
1134
- const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
1135
- for (const targetId of targets) yield* reconstructPaths(graph, prev, sourceNode, targetId);
516
+ const { source, distArr, prevArr, stopDistance } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
517
+ const sourceNode = source !== -1 ? graph.nodes[source] : graph.nodes.find((node) => node.id === sourceId);
518
+ if (source === -1) {
519
+ if (opts?.to === sourceId) yield {
520
+ source: sourceNode,
521
+ steps: []
522
+ };
523
+ return;
524
+ }
525
+ const csr = getCSR(graph);
526
+ if (opts?.to) {
527
+ const target = csr.indexOf.get(opts.to);
528
+ if (target === void 0 || distArr[target] === Infinity || distArr[target] > stopDistance) return;
529
+ yield* reconstructPathsAt(graph, prevArr, sourceNode, source, target);
530
+ return;
531
+ }
532
+ for (let target = 0; target < distArr.length; target++) {
533
+ if (target === source || distArr[target] === Infinity || distArr[target] > stopDistance) continue;
534
+ yield* reconstructPathsAt(graph, prevArr, sourceNode, source, target);
535
+ }
1136
536
  }
1137
537
  function getShortestPaths(graph, opts) {
1138
538
  return [...genShortestPaths(graph, opts)];
@@ -1193,35 +593,30 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
1193
593
  const predBEdge = new Int32Array(n).fill(-1);
1194
594
  const settledF = new Uint8Array(n);
1195
595
  const settledB = new Uint8Array(n);
1196
- const compare = (a, b) => a.dist - b.dist;
1197
- const pqF = new MinPriorityQueue(compare);
1198
- const pqB = new MinPriorityQueue(compare);
596
+ const pqF = new TypedMinHeap(n);
597
+ const pqB = new TypedMinHeap(n);
1199
598
  distF[source] = 0;
1200
599
  distB[target] = 0;
1201
- pqF.push({
1202
- pos: source,
1203
- dist: 0
1204
- });
1205
- pqB.push({
1206
- pos: target,
1207
- dist: 0
1208
- });
600
+ pqF.push(0, source);
601
+ pqB.push(0, target);
1209
602
  let mu = Infinity;
1210
603
  let meet = -1;
1211
604
  /** Discard stale/settled heap entries; return the next valid key. */
1212
605
  const validTop = (pq, dist, settled) => {
1213
- for (;;) {
1214
- const top = pq.peek();
1215
- if (top === void 0) return void 0;
1216
- if (settled[top.pos] || top.dist !== dist[top.pos]) {
606
+ while (pq.size > 0) {
607
+ const key = pq.peekKey();
608
+ const pos = pq.peekVal();
609
+ if (settled[pos] || key !== dist[pos]) {
1217
610
  pq.pop();
1218
611
  continue;
1219
612
  }
1220
- return top.dist;
613
+ return key;
1221
614
  }
1222
615
  };
1223
616
  const scanForward = () => {
1224
- const { pos: u, dist: d } = pqF.pop();
617
+ const d = pqF.peekKey();
618
+ const u = pqF.peekVal();
619
+ pqF.pop();
1225
620
  settledF[u] = 1;
1226
621
  for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1227
622
  const edge = graph.edges[csr.outEdgeIndex[a]];
@@ -1232,10 +627,7 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
1232
627
  distF[v] = next;
1233
628
  predF[v] = u;
1234
629
  predFEdge[v] = csr.outEdgeIndex[a];
1235
- pqF.push({
1236
- pos: v,
1237
- dist: next
1238
- });
630
+ pqF.push(next, v);
1239
631
  }
1240
632
  if (distB[v] !== Infinity && next + distB[v] < mu) {
1241
633
  mu = next + distB[v];
@@ -1244,7 +636,9 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
1244
636
  }
1245
637
  };
1246
638
  const scanBackward = () => {
1247
- const { pos: u, dist: d } = pqB.pop();
639
+ const d = pqB.peekKey();
640
+ const u = pqB.peekVal();
641
+ pqB.pop();
1248
642
  settledB[u] = 1;
1249
643
  for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
1250
644
  const edge = graph.edges[csr.inEdgeIndex[a]];
@@ -1255,10 +649,7 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
1255
649
  distB[v] = next;
1256
650
  predB[v] = u;
1257
651
  predBEdge[v] = csr.inEdgeIndex[a];
1258
- pqB.push({
1259
- pos: v,
1260
- dist: next
1261
- });
652
+ pqB.push(next, v);
1262
653
  }
1263
654
  if (distF[v] !== Infinity && next + distF[v] < mu) {
1264
655
  mu = next + distF[v];
@@ -1642,15 +1033,13 @@ function getAStarPath(graph, opts) {
1642
1033
  const cameFromPos = new Int32Array(n).fill(-1);
1643
1034
  const cameFromEdge = new Int32Array(n).fill(-1);
1644
1035
  const closed = new Uint8Array(n);
1645
- const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
1036
+ const openSet = new TypedMinHeap(n);
1646
1037
  assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
1647
1038
  gScore[source] = 0;
1648
- openSet.push({
1649
- pos: source,
1650
- f: heuristic(sourceId)
1651
- });
1039
+ openSet.push(heuristic(sourceId), source);
1652
1040
  while (openSet.size > 0) {
1653
- const { pos: current } = openSet.pop();
1041
+ const current = openSet.peekVal();
1042
+ openSet.pop();
1654
1043
  if (closed[current]) continue;
1655
1044
  if (current === target) {
1656
1045
  const steps = [];
@@ -1678,15 +1067,12 @@ function getAStarPath(graph, opts) {
1678
1067
  cameFromPos[neighbor] = current;
1679
1068
  cameFromEdge[neighbor] = csr.outEdgeIndex[a];
1680
1069
  gScore[neighbor] = tentativeScore;
1681
- openSet.push({
1682
- pos: neighbor,
1683
- f: tentativeScore + heuristic(csr.ids[neighbor])
1684
- });
1070
+ openSet.push(tentativeScore + heuristic(csr.ids[neighbor]), neighbor);
1685
1071
  }
1686
1072
  }
1687
1073
  }
1688
1074
  }
1689
- function joinPaths(headPath, tailPath) {
1075
+ function getJoinedPath(headPath, tailPath) {
1690
1076
  const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
1691
1077
  if (headEnd.id !== tailPath.source.id) throw new Error(`Paths cannot be joined: head path ends at "${headEnd.id}" but tail path starts at "${tailPath.source.id}"`);
1692
1078
  return {
@@ -1694,57 +1080,11 @@ function joinPaths(headPath, tailPath) {
1694
1080
  steps: [...headPath.steps, ...tailPath.steps]
1695
1081
  };
1696
1082
  }
1697
-
1698
- //#endregion
1699
- //#region src/config.ts
1700
- /**
1701
- * Convert a resolved {@link GraphNode} back into a {@link NodeConfig}.
1702
- *
1703
- * Faithful and complete: round-tripping through `createGraphNode` yields a
1704
- * deep-equal node. Optional fields are only included when present; ports are
1705
- * deep-copied so the config does not share port objects with the source node.
1706
- */
1707
- function toNodeConfig(node) {
1708
- const config = { id: node.id };
1709
- if (node.parentId != null) config.parentId = node.parentId;
1710
- if (node.initialNodeId != null) config.initialNodeId = node.initialNodeId;
1711
- if (node.label != null) config.label = node.label;
1712
- if (node.data != null) config.data = node.data;
1713
- if (node.ports !== void 0) config.ports = node.ports.map((p) => ({ ...p }));
1714
- if (node.x !== void 0) config.x = node.x;
1715
- if (node.y !== void 0) config.y = node.y;
1716
- if (node.width !== void 0) config.width = node.width;
1717
- if (node.height !== void 0) config.height = node.height;
1718
- if (node.shape !== void 0) config.shape = node.shape;
1719
- if (node.color !== void 0) config.color = node.color;
1720
- if (node.style !== void 0) config.style = node.style;
1721
- return config;
1722
- }
1723
1083
  /**
1724
- * Convert a resolved {@link GraphEdge} back into an {@link EdgeConfig}.
1725
- *
1726
- * Faithful and complete: round-tripping through `createGraphEdge` yields a
1727
- * deep-equal edge. Optional fields are only included when present.
1084
+ * @deprecated Use {@link getJoinedPath}.
1728
1085
  */
1729
- function toEdgeConfig(edge) {
1730
- const config = {
1731
- id: edge.id,
1732
- sourceId: edge.sourceId,
1733
- targetId: edge.targetId
1734
- };
1735
- if (edge.label != null) config.label = edge.label;
1736
- if (edge.data != null) config.data = edge.data;
1737
- if (edge.weight !== void 0) config.weight = edge.weight;
1738
- if (edge.mode !== void 0) config.mode = edge.mode;
1739
- if (edge.sourcePort !== void 0) config.sourcePort = edge.sourcePort;
1740
- if (edge.targetPort !== void 0) config.targetPort = edge.targetPort;
1741
- if (edge.x !== void 0) config.x = edge.x;
1742
- if (edge.y !== void 0) config.y = edge.y;
1743
- if (edge.width !== void 0) config.width = edge.width;
1744
- if (edge.height !== void 0) config.height = edge.height;
1745
- if (edge.color !== void 0) config.color = edge.color;
1746
- if (edge.style !== void 0) config.style = edge.style;
1747
- return config;
1086
+ function joinPaths(headPath, tailPath) {
1087
+ return getJoinedPath(headPath, tailPath);
1748
1088
  }
1749
1089
 
1750
1090
  //#endregion
@@ -1759,7 +1099,7 @@ function toEdgeConfig(edge) {
1759
1099
  *
1760
1100
  * @example
1761
1101
  * ```ts
1762
- * import { createGraph, flatten } from '@statelyai/graph';
1102
+ * import { createGraph, getFlattenedGraph } from '@statelyai/graph';
1763
1103
  *
1764
1104
  * const graph = createGraph({
1765
1105
  * nodes: [
@@ -1771,12 +1111,12 @@ function toEdgeConfig(edge) {
1771
1111
  * edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
1772
1112
  * });
1773
1113
  *
1774
- * const flat = flatten(graph);
1114
+ * const flat = getFlattenedGraph(graph);
1775
1115
  * // flat.nodes → [child1, child2, other] (leaf nodes only)
1776
1116
  * // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
1777
1117
  * ```
1778
1118
  */
1779
- function flatten(graph) {
1119
+ function getFlattenedGraph(graph) {
1780
1120
  const idx = getIndex(graph);
1781
1121
  const leaves = /* @__PURE__ */ new Set();
1782
1122
  for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
@@ -1843,6 +1183,12 @@ function flatten(graph) {
1843
1183
  });
1844
1184
  }
1845
1185
  /**
1186
+ * @deprecated Use {@link getFlattenedGraph}.
1187
+ */
1188
+ function flatten(graph) {
1189
+ return getFlattenedGraph(graph);
1190
+ }
1191
+ /**
1846
1192
  * Convert a node to a config, stripping parentId/initialNodeId references
1847
1193
  * to nodes outside the given set.
1848
1194
  */
@@ -1893,7 +1239,7 @@ function getSubgraph(graph, nodeIds) {
1893
1239
  *
1894
1240
  * @example
1895
1241
  * ```ts
1896
- * import { createGraph, reverseGraph } from '@statelyai/graph';
1242
+ * import { createGraph, getReversedGraph } from '@statelyai/graph';
1897
1243
  *
1898
1244
  * const graph = createGraph({
1899
1245
  * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
@@ -1903,14 +1249,14 @@ function getSubgraph(graph, nodeIds) {
1903
1249
  * ],
1904
1250
  * });
1905
1251
  *
1906
- * const rev = reverseGraph(graph);
1252
+ * const rev = getReversedGraph(graph);
1907
1253
  * // rev edges: b→a, c→b
1908
1254
  *
1909
- * const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
1255
+ * const filtered = getReversedGraph(graph, (e) => e.id !== 'bc');
1910
1256
  * // filtered edges: b→a (only ab reversed, bc excluded)
1911
1257
  * ```
1912
1258
  */
1913
- function reverseGraph(graph, filterEdge) {
1259
+ function getReversedGraph(graph, filterEdge) {
1914
1260
  const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
1915
1261
  return createGraph({
1916
1262
  id: graph.id,
@@ -1930,10 +1276,16 @@ function reverseGraph(graph, filterEdge) {
1930
1276
  data: graph.data
1931
1277
  });
1932
1278
  }
1279
+ /**
1280
+ * @deprecated Use {@link getReversedGraph}.
1281
+ */
1282
+ function reverseGraph(graph, filterEdge) {
1283
+ return getReversedGraph(graph, filterEdge);
1284
+ }
1933
1285
 
1934
1286
  //#endregion
1935
1287
  //#region src/algorithms/traversal.ts
1936
- function* bfs(graph, startId) {
1288
+ function* genBFS(graph, startId) {
1937
1289
  const csr = getCSR(graph);
1938
1290
  const start = csr.indexOf.get(startId);
1939
1291
  if (start === void 0) return;
@@ -1956,7 +1308,13 @@ function* bfs(graph, startId) {
1956
1308
  }
1957
1309
  }
1958
1310
  }
1959
- function* dfs(graph, startId) {
1311
+ /**
1312
+ * @deprecated Use {@link genBFS}.
1313
+ */
1314
+ function* bfs(graph, startId) {
1315
+ yield* genBFS(graph, startId);
1316
+ }
1317
+ function* genDFS(graph, startId) {
1960
1318
  const csr = getCSR(graph);
1961
1319
  const start = csr.indexOf.get(startId);
1962
1320
  if (start === void 0) return;
@@ -1974,6 +1332,12 @@ function* dfs(graph, startId) {
1974
1332
  }
1975
1333
  }
1976
1334
  }
1335
+ /**
1336
+ * @deprecated Use {@link genDFS}.
1337
+ */
1338
+ function* dfs(graph, startId) {
1339
+ yield* genDFS(graph, startId);
1340
+ }
1977
1341
  function isAcyclic(graph) {
1978
1342
  const kind = getEffectiveModeKind(graph);
1979
1343
  if (kind === "mixed") return isAcyclicMixed(graph);
@@ -2636,31 +2000,366 @@ function getHITS(graph, options) {
2636
2000
  /**
2637
2001
  * Returns eigenvector centrality scores for all nodes.
2638
2002
  *
2639
- * Uses power iteration over incoming neighbors for directed graphs and
2640
- * undirected adjacency for undirected graphs.
2003
+ * Power iteration with the `A + I` shift (same scheme as graphology and
2004
+ * networkx, so bipartite structures converge instead of oscillating).
2005
+ * Scores flow along edge direction: a node's score is fed by its incoming
2006
+ * neighbors; undirected edges feed both endpoints. The result vector is
2007
+ * Euclidean (L2) normalized.
2008
+ *
2009
+ * Throws when the iteration has not converged (L1 error < `n × tolerance`)
2010
+ * within `maxIterations`.
2641
2011
  */
2642
2012
  function getEigenvectorCentrality(graph, options) {
2643
2013
  if (getNodeIds(graph).length === 0) return {};
2644
2014
  const maxIterations = options?.maxIterations ?? 100;
2645
2015
  const tolerance = options?.tolerance ?? 1e-6;
2016
+ const getWeight = options?.getWeight;
2646
2017
  const csr = getCSR(graph);
2647
2018
  const n = csr.ids.length;
2648
- let current = new Float64Array(n).fill(1);
2649
- normalizeTypedVector(current);
2019
+ let current = new Float64Array(n).fill(1 / n);
2020
+ let converged = false;
2650
2021
  for (let iteration = 0; iteration < maxIterations; iteration++) {
2651
- const next = new Float64Array(n);
2652
- for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) next[w] += current[csr.inOrigins[a]];
2022
+ const next = Float64Array.from(current);
2023
+ for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
2024
+ const weight = getWeight ? getWeight(graph.edges[csr.inEdgeIndex[a]]) : 1;
2025
+ next[w] += current[csr.inOrigins[a]] * weight;
2026
+ }
2653
2027
  normalizeTypedVector(next);
2654
- let diff = 0;
2655
- for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
2028
+ let error = 0;
2029
+ for (let i = 0; i < n; i++) error += Math.abs(next[i] - current[i]);
2656
2030
  current = next;
2657
- if (diff <= tolerance) break;
2031
+ if (error < n * tolerance) {
2032
+ converged = true;
2033
+ break;
2034
+ }
2035
+ }
2036
+ if (!converged) throw new Error(`getEigenvectorCentrality: power iteration failed to converge within ${maxIterations} iterations (tolerance ${tolerance}) — increase options.maxIterations or loosen options.tolerance`);
2037
+ const scores = createEmptyScoreMap(graph);
2038
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
2039
+ return scores;
2040
+ }
2041
+ /**
2042
+ * Returns Katz centrality scores for all nodes.
2043
+ *
2044
+ * Iterates `x' = alpha · Aᵀx + beta` to its fixed point (networkx-style),
2045
+ * then Euclidean (L2) normalizes the result. Scores flow along edge
2046
+ * direction: a node's score is fed by its incoming neighbors; undirected
2047
+ * edges feed both endpoints.
2048
+ *
2049
+ * Converges only when `alpha` is below the reciprocal of the largest
2050
+ * eigenvalue of the adjacency matrix; throws when the iteration has not
2051
+ * converged (L1 error < `n × tolerance`) within `maxIterations`.
2052
+ */
2053
+ function getKatzCentrality(graph, options) {
2054
+ if (getNodeIds(graph).length === 0) return {};
2055
+ const alpha = options?.alpha ?? .1;
2056
+ const beta = options?.beta ?? 1;
2057
+ const maxIterations = options?.maxIterations ?? 100;
2058
+ const tolerance = options?.tolerance ?? 1e-6;
2059
+ const getWeight = options?.getWeight;
2060
+ const csr = getCSR(graph);
2061
+ const n = csr.ids.length;
2062
+ let current = new Float64Array(n);
2063
+ let converged = false;
2064
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
2065
+ const next = new Float64Array(n).fill(beta);
2066
+ for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
2067
+ const weight = getWeight ? getWeight(graph.edges[csr.inEdgeIndex[a]]) : 1;
2068
+ next[w] += alpha * current[csr.inOrigins[a]] * weight;
2069
+ }
2070
+ let error = 0;
2071
+ for (let i = 0; i < n; i++) error += Math.abs(next[i] - current[i]);
2072
+ current = next;
2073
+ if (error < n * tolerance) {
2074
+ converged = true;
2075
+ break;
2076
+ }
2658
2077
  }
2078
+ if (!converged) throw new Error(`getKatzCentrality: iteration failed to converge within ${maxIterations} iterations — alpha ${alpha} may be >= 1/λ_max of the adjacency matrix; decrease options.alpha or increase options.maxIterations`);
2079
+ normalizeTypedVector(current);
2659
2080
  const scores = createEmptyScoreMap(graph);
2660
2081
  for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
2661
2082
  return scores;
2662
2083
  }
2663
2084
 
2085
+ //#endregion
2086
+ //#region src/algorithms/cores.ts
2087
+ /**
2088
+ * Undirected adjacency over node positions: every edge (regardless of mode
2089
+ * or direction) contributes both arcs, matching the standard k-core
2090
+ * definition. Self-loops are ignored. Positions come from the CSR snapshot
2091
+ * so `ids` order matches `graph.nodes`.
2092
+ */
2093
+ function buildUndirectedAdjacency(graph) {
2094
+ const csr = getCSR(graph);
2095
+ const n = csr.ids.length;
2096
+ const m = graph.edges.length;
2097
+ const sourcePos = new Int32Array(m);
2098
+ const targetPos = new Int32Array(m);
2099
+ const degree = new Int32Array(n);
2100
+ let arcCount = 0;
2101
+ for (let e = 0; e < m; e++) {
2102
+ const edge = graph.edges[e];
2103
+ const s = csr.indexOf.get(edge.sourceId);
2104
+ const t = csr.indexOf.get(edge.targetId);
2105
+ if (s === t) {
2106
+ sourcePos[e] = -1;
2107
+ continue;
2108
+ }
2109
+ sourcePos[e] = s;
2110
+ targetPos[e] = t;
2111
+ degree[s]++;
2112
+ degree[t]++;
2113
+ arcCount += 2;
2114
+ }
2115
+ const offsets = new Int32Array(n + 1);
2116
+ for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
2117
+ const targets = new Int32Array(arcCount);
2118
+ const cursor = Int32Array.from(offsets.subarray(0, n));
2119
+ for (let e = 0; e < m; e++) {
2120
+ if (sourcePos[e] === -1) continue;
2121
+ targets[cursor[sourcePos[e]]++] = targetPos[e];
2122
+ targets[cursor[targetPos[e]]++] = sourcePos[e];
2123
+ }
2124
+ return {
2125
+ ids: csr.ids,
2126
+ offsets,
2127
+ targets
2128
+ };
2129
+ }
2130
+ /**
2131
+ * Returns the core number of every node (largest `k` such that the node
2132
+ * belongs to the k-core).
2133
+ *
2134
+ * Uses Batagelj–Zaveršnik bucket peeling — O(m). Edges are treated as
2135
+ * undirected (the standard k-core definition); self-loops are ignored.
2136
+ *
2137
+ * @example
2138
+ * ```ts
2139
+ * const cores = getCoreNumbers(graph);
2140
+ * console.log(cores.a); // 3
2141
+ * ```
2142
+ */
2143
+ function getCoreNumbers(graph) {
2144
+ const { ids, offsets, targets } = buildUndirectedAdjacency(graph);
2145
+ const n = ids.length;
2146
+ const core = new Int32Array(n);
2147
+ let maxDegree = 0;
2148
+ for (let i = 0; i < n; i++) {
2149
+ core[i] = offsets[i + 1] - offsets[i];
2150
+ if (core[i] > maxDegree) maxDegree = core[i];
2151
+ }
2152
+ const bin = new Int32Array(maxDegree + 1);
2153
+ for (let i = 0; i < n; i++) bin[core[i]]++;
2154
+ let start = 0;
2155
+ for (let d = 0; d <= maxDegree; d++) {
2156
+ const count = bin[d];
2157
+ bin[d] = start;
2158
+ start += count;
2159
+ }
2160
+ const vert = new Int32Array(n);
2161
+ const pos = new Int32Array(n);
2162
+ for (let i = 0; i < n; i++) {
2163
+ pos[i] = bin[core[i]];
2164
+ vert[pos[i]] = i;
2165
+ bin[core[i]]++;
2166
+ }
2167
+ for (let d = maxDegree; d > 0; d--) bin[d] = bin[d - 1];
2168
+ bin[0] = 0;
2169
+ for (let k = 0; k < n; k++) {
2170
+ const v = vert[k];
2171
+ for (let a = offsets[v]; a < offsets[v + 1]; a++) {
2172
+ const u = targets[a];
2173
+ if (core[u] > core[v]) {
2174
+ const du = core[u];
2175
+ const pu = pos[u];
2176
+ const pw = bin[du];
2177
+ const w = vert[pw];
2178
+ if (u !== w) {
2179
+ vert[pu] = w;
2180
+ pos[w] = pu;
2181
+ vert[pw] = u;
2182
+ pos[u] = pw;
2183
+ }
2184
+ bin[du]++;
2185
+ core[u]--;
2186
+ }
2187
+ }
2188
+ }
2189
+ const result = {};
2190
+ for (let i = 0; i < n; i++) result[ids[i]] = core[i];
2191
+ return result;
2192
+ }
2193
+ /**
2194
+ * Returns the ids of all nodes in the k-core: the maximal subgraph in which
2195
+ * every node has at least `k` neighbors (edges treated as undirected).
2196
+ *
2197
+ * Node order follows `graph.nodes`. `k <= 0` returns every node id.
2198
+ */
2199
+ function getKCore(graph, k) {
2200
+ const cores = getCoreNumbers(graph);
2201
+ return graph.nodes.filter((node) => cores[node.id] >= k).map((node) => node.id);
2202
+ }
2203
+
2204
+ //#endregion
2205
+ //#region src/algorithms/bipartite.ts
2206
+ /**
2207
+ * BFS 2-coloring over undirected adjacency. Edges are treated as undirected
2208
+ * regardless of mode/direction. Returns either the coloring or the edge that
2209
+ * proves the graph is not bipartite.
2210
+ */
2211
+ function getTwoColoring(graph) {
2212
+ const csr = getCSR(graph);
2213
+ const n = csr.ids.length;
2214
+ const m = graph.edges.length;
2215
+ const degree = new Int32Array(n);
2216
+ for (let e = 0; e < m; e++) {
2217
+ const edge = graph.edges[e];
2218
+ if (edge.sourceId === edge.targetId) return { conflictEdgeId: edge.id };
2219
+ degree[csr.indexOf.get(edge.sourceId)]++;
2220
+ degree[csr.indexOf.get(edge.targetId)]++;
2221
+ }
2222
+ const offsets = new Int32Array(n + 1);
2223
+ for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
2224
+ const targets = new Int32Array(offsets[n]);
2225
+ const arcEdge = new Int32Array(offsets[n]);
2226
+ const cursor = Int32Array.from(offsets.subarray(0, n));
2227
+ for (let e = 0; e < m; e++) {
2228
+ const edge = graph.edges[e];
2229
+ const s = csr.indexOf.get(edge.sourceId);
2230
+ const t = csr.indexOf.get(edge.targetId);
2231
+ targets[cursor[s]] = t;
2232
+ arcEdge[cursor[s]++] = e;
2233
+ targets[cursor[t]] = s;
2234
+ arcEdge[cursor[t]++] = e;
2235
+ }
2236
+ const colors = new Int8Array(n).fill(-1);
2237
+ const queue = new Int32Array(n);
2238
+ for (let root = 0; root < n; root++) {
2239
+ if (colors[root] !== -1) continue;
2240
+ colors[root] = 0;
2241
+ queue[0] = root;
2242
+ let head = 0;
2243
+ let tail = 1;
2244
+ while (head < tail) {
2245
+ const u = queue[head++];
2246
+ for (let a = offsets[u]; a < offsets[u + 1]; a++) {
2247
+ const v = targets[a];
2248
+ if (colors[v] === -1) {
2249
+ colors[v] = 1 - colors[u];
2250
+ queue[tail++] = v;
2251
+ } else if (colors[v] === colors[u]) return { conflictEdgeId: graph.edges[arcEdge[a]].id };
2252
+ }
2253
+ }
2254
+ }
2255
+ return { colors };
2256
+ }
2257
+ /**
2258
+ * Returns whether the graph is bipartite (2-colorable).
2259
+ *
2260
+ * Edges are treated as undirected; self-loops make a graph non-bipartite.
2261
+ * Runs a BFS 2-coloring per connected component — O(n + m).
2262
+ */
2263
+ function isBipartite(graph) {
2264
+ return "colors" in getTwoColoring(graph);
2265
+ }
2266
+ /**
2267
+ * Returns a maximum-cardinality matching of a bipartite graph using
2268
+ * Hopcroft–Karp — O(m·√n).
2269
+ *
2270
+ * The bipartition is derived by 2-coloring (edges treated as undirected).
2271
+ * Each match reports the realizing edge with its stored `sourceId`/
2272
+ * `targetId` orientation; for parallel edges between a matched pair, the
2273
+ * first edge used by the algorithm is reported.
2274
+ *
2275
+ * Throws if the graph is not bipartite, naming the offending edge.
2276
+ */
2277
+ function getMaximumBipartiteMatching(graph) {
2278
+ const coloring = getTwoColoring(graph);
2279
+ if (!("colors" in coloring)) throw new Error(`getMaximumBipartiteMatching: graph is not bipartite — edge "${coloring.conflictEdgeId}" closes an odd cycle (or is a self-loop); a maximum bipartite matching requires a bipartite graph, check with isBipartite() first`);
2280
+ const { colors } = coloring;
2281
+ const csr = getCSR(graph);
2282
+ const n = csr.ids.length;
2283
+ const m = graph.edges.length;
2284
+ const degree = new Int32Array(n);
2285
+ for (let e = 0; e < m; e++) {
2286
+ const edge = graph.edges[e];
2287
+ const s = csr.indexOf.get(edge.sourceId);
2288
+ const t = csr.indexOf.get(edge.targetId);
2289
+ degree[colors[s] === 0 ? s : t]++;
2290
+ }
2291
+ const offsets = new Int32Array(n + 1);
2292
+ for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
2293
+ const targets = new Int32Array(offsets[n]);
2294
+ const arcEdge = new Int32Array(offsets[n]);
2295
+ const cursor = Int32Array.from(offsets.subarray(0, n));
2296
+ for (let e = 0; e < m; e++) {
2297
+ const edge = graph.edges[e];
2298
+ const s = csr.indexOf.get(edge.sourceId);
2299
+ const t = csr.indexOf.get(edge.targetId);
2300
+ const left = colors[s] === 0 ? s : t;
2301
+ const right = colors[s] === 0 ? t : s;
2302
+ targets[cursor[left]] = right;
2303
+ arcEdge[cursor[left]++] = e;
2304
+ }
2305
+ const INF = 2147483647;
2306
+ const matchLeft = new Int32Array(n).fill(-1);
2307
+ const matchRight = new Int32Array(n).fill(-1);
2308
+ const matchEdge = new Int32Array(n).fill(-1);
2309
+ const dist = new Int32Array(n);
2310
+ const queue = new Int32Array(n);
2311
+ function hasAugmentingLayer() {
2312
+ let head = 0;
2313
+ let tail = 0;
2314
+ for (let u = 0; u < n; u++) {
2315
+ if (colors[u] !== 0) continue;
2316
+ if (matchLeft[u] === -1) {
2317
+ dist[u] = 0;
2318
+ queue[tail++] = u;
2319
+ } else dist[u] = INF;
2320
+ }
2321
+ let foundAugmenting = false;
2322
+ while (head < tail) {
2323
+ const u = queue[head++];
2324
+ for (let a = offsets[u]; a < offsets[u + 1]; a++) {
2325
+ const w = matchRight[targets[a]];
2326
+ if (w === -1) foundAugmenting = true;
2327
+ else if (dist[w] === INF) {
2328
+ dist[w] = dist[u] + 1;
2329
+ queue[tail++] = w;
2330
+ }
2331
+ }
2332
+ }
2333
+ return foundAugmenting;
2334
+ }
2335
+ function hasAugmentedMatchFrom(u) {
2336
+ for (let a = offsets[u]; a < offsets[u + 1]; a++) {
2337
+ const v = targets[a];
2338
+ const w = matchRight[v];
2339
+ if (w === -1 || dist[w] === dist[u] + 1 && hasAugmentedMatchFrom(w)) {
2340
+ matchLeft[u] = v;
2341
+ matchRight[v] = u;
2342
+ matchEdge[u] = arcEdge[a];
2343
+ return true;
2344
+ }
2345
+ }
2346
+ dist[u] = INF;
2347
+ return false;
2348
+ }
2349
+ while (hasAugmentingLayer()) for (let u = 0; u < n; u++) if (colors[u] === 0 && matchLeft[u] === -1) hasAugmentedMatchFrom(u);
2350
+ const matches = [];
2351
+ for (let u = 0; u < n; u++) {
2352
+ if (matchEdge[u] === -1) continue;
2353
+ const edge = graph.edges[matchEdge[u]];
2354
+ matches.push({
2355
+ sourceId: edge.sourceId,
2356
+ targetId: edge.targetId,
2357
+ edgeId: edge.id
2358
+ });
2359
+ }
2360
+ return matches;
2361
+ }
2362
+
2664
2363
  //#endregion
2665
2364
  //#region src/algorithms/community.ts
2666
2365
  function getUndirectedNeighbors$1(graph, nodeId) {
@@ -2777,14 +2476,52 @@ function toCommunityIds(communities) {
2777
2476
  return communities.map((community) => new Set(community.map((node) => node.id)));
2778
2477
  }
2779
2478
  /**
2479
+ * Asynchronous LPA: labels update in place as the (seeded-shuffled) round
2480
+ * proceeds; ties among maximal neighbor labels break uniformly at random,
2481
+ * keeping the current label when it is already maximal so rounds terminate.
2482
+ * Deterministic per seed.
2483
+ */
2484
+ function getSeededLabelPropagation(graph, seed, maxIterations) {
2485
+ const rng = mulberry32(seed);
2486
+ const labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
2487
+ const order = graph.nodes.map((node) => node.id);
2488
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
2489
+ for (let i = order.length - 1; i > 0; i--) {
2490
+ const j = Math.floor(rng() * (i + 1));
2491
+ [order[i], order[j]] = [order[j], order[i]];
2492
+ }
2493
+ let changed = false;
2494
+ for (const nodeId of order) {
2495
+ const counts = /* @__PURE__ */ new Map();
2496
+ for (const neighbor of getUndirectedNeighbors$1(graph, nodeId)) {
2497
+ const label = labels[neighbor.nodeId];
2498
+ counts.set(label, (counts.get(label) ?? 0) + 1);
2499
+ }
2500
+ if (counts.size === 0) continue;
2501
+ let maxCount = 0;
2502
+ for (const count of counts.values()) if (count > maxCount) maxCount = count;
2503
+ const best = [];
2504
+ for (const [label, count] of counts) if (count === maxCount) best.push(label);
2505
+ if (best.includes(labels[nodeId])) continue;
2506
+ labels[nodeId] = best[Math.floor(rng() * best.length)];
2507
+ changed = true;
2508
+ }
2509
+ if (!changed) break;
2510
+ }
2511
+ return normalizeCommunities(graph, labels);
2512
+ }
2513
+ /**
2780
2514
  * Returns label-propagation communities for the graph.
2781
2515
  *
2782
2516
  * The implementation is deterministic: ties are broken by lexicographic label
2783
- * order so test results remain stable.
2517
+ * order so test results remain stable. Pass `options.seed` for the classic
2518
+ * asynchronous variant (shuffled node order per round, random tie-breaking)
2519
+ * — still deterministic per seed.
2784
2520
  */
2785
2521
  function getLabelPropagationCommunities(graph, options) {
2786
2522
  if (graph.nodes.length === 0) return [];
2787
2523
  const maxIterations = options?.maxIterations ?? 50;
2524
+ if (options?.seed !== void 0) return getSeededLabelPropagation(graph, options.seed, maxIterations);
2788
2525
  let labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
2789
2526
  const nodeIds = graph.nodes.map((node) => node.id).sort();
2790
2527
  for (let iteration = 0; iteration < maxIterations; iteration++) {
@@ -3242,30 +2979,14 @@ function getLouvainCommunities(graph, options) {
3242
2979
  //#endregion
3243
2980
  //#region src/algorithms/flow.ts
3244
2981
  /**
3245
- * Returns the maximum flow from `from` to `to` using the Edmonds-Karp
3246
- * algorithm (BFS augmenting paths).
3247
- *
3248
- * Directed edges carry capacity from source to target only. Edges whose
3249
- * effective mode is not `'directed'` (undirected/bidirectional) are modeled
3250
- * as two independent opposite arcs, each with the edge's full capacity.
3251
- *
3252
- * The returned `flows` record maps every edge id to its net flow (positive
3253
- * in the source→target direction). `cutEdges` is a minimum s-t cut: the
3254
- * edges crossing from the source side to the sink side of the final
3255
- * residual graph; the sum of their capacities equals `value`.
3256
- *
3257
- * @example
3258
- * ```ts
3259
- * const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
3260
- * ```
2982
+ * Shared Edmonds-Karp solver behind {@link getMaxFlow} and {@link getMinCut}.
2983
+ * `caller`/`fromOption`/`toOption` only shape the error messages.
3261
2984
  */
3262
- function getMaxFlow(graph, options) {
3263
- const { from, to } = options;
3264
- const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
2985
+ function solveMaxFlow(graph, caller, fromOption, toOption, from, to, getCapacity) {
3265
2986
  const idx = getIndex(graph);
3266
- if (!idx.nodeById.has(from)) throw new Error(`getMaxFlow: source node "${from}" not found in graph — pass an existing node id as options.from`);
3267
- if (!idx.nodeById.has(to)) throw new Error(`getMaxFlow: sink node "${to}" not found in graph — pass an existing node id as options.to`);
3268
- if (from === to) throw new Error(`getMaxFlow: source and sink are both "${from}" — they must be different nodes`);
2987
+ if (!idx.nodeById.has(from)) throw new Error(`${caller}: source node "${from}" not found in graph — pass an existing node id as ${fromOption}`);
2988
+ if (!idx.nodeById.has(to)) throw new Error(`${caller}: sink node "${to}" not found in graph — pass an existing node id as ${toOption}`);
2989
+ if (from === to) throw new Error(`${caller}: source and sink are both "${from}" — they must be different nodes`);
3269
2990
  const arcs = [];
3270
2991
  const outArcs = /* @__PURE__ */ new Map();
3271
2992
  for (const node of graph.nodes) outArcs.set(node.id, []);
@@ -3287,7 +3008,7 @@ function getMaxFlow(graph, options) {
3287
3008
  }
3288
3009
  for (const edge of graph.edges) {
3289
3010
  const capacity = getCapacity(edge);
3290
- if (capacity < 0) throw new Error(`getMaxFlow: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
3011
+ if (capacity < 0) throw new Error(`${caller}: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
3291
3012
  if (edge.sourceId === edge.targetId) continue;
3292
3013
  addArc(edge.sourceId, edge.targetId, capacity, edge.id, 1);
3293
3014
  if (getEdgeMode(graph, edge) !== "directed") addArc(edge.targetId, edge.sourceId, capacity, edge.id, -1);
@@ -3349,12 +3070,73 @@ function getMaxFlow(graph, options) {
3349
3070
  if (sourceSide.has(arcFrom) && !sourceSide.has(arc.to)) cutEdgeIds.add(arc.edgeId);
3350
3071
  }
3351
3072
  const cutEdges = graph.edges.filter((edge) => cutEdgeIds.has(edge.id));
3073
+ return {
3074
+ value,
3075
+ flows,
3076
+ cutEdges,
3077
+ sourceSide
3078
+ };
3079
+ }
3080
+ /**
3081
+ * Returns the maximum flow from `from` to `to` using the Edmonds-Karp
3082
+ * algorithm (BFS augmenting paths).
3083
+ *
3084
+ * Directed edges carry capacity from source to target only. Edges whose
3085
+ * effective mode is not `'directed'` (undirected/bidirectional) are modeled
3086
+ * as two independent opposite arcs, each with the edge's full capacity.
3087
+ *
3088
+ * The returned `flows` record maps every edge id to its net flow (positive
3089
+ * in the source→target direction). `cutEdges` is a minimum s-t cut: the
3090
+ * edges crossing from the source side to the sink side of the final
3091
+ * residual graph; the sum of their capacities equals `value`.
3092
+ *
3093
+ * @example
3094
+ * ```ts
3095
+ * const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
3096
+ * ```
3097
+ */
3098
+ function getMaxFlow(graph, options) {
3099
+ const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
3100
+ const { value, flows, cutEdges } = solveMaxFlow(graph, "getMaxFlow", "options.from", "options.to", options.from, options.to, getCapacity);
3352
3101
  return {
3353
3102
  value,
3354
3103
  flows,
3355
3104
  cutEdges
3356
3105
  };
3357
3106
  }
3107
+ /**
3108
+ * Returns a minimum s-t cut between `source` and `sink` via the max-flow
3109
+ * min-cut theorem: runs the same Edmonds-Karp solver as {@link getMaxFlow},
3110
+ * then splits the nodes by residual reachability from the source.
3111
+ *
3112
+ * `partition.source` holds every node reachable from `source` in the final
3113
+ * residual graph; `partition.sink` holds the rest (both in `graph.nodes`
3114
+ * order). `cutEdges` are the ids of the edges crossing the cut, and their
3115
+ * total capacity equals `value` (the max-flow value).
3116
+ *
3117
+ * @example
3118
+ * ```ts
3119
+ * const { value, cutEdges, partition } = getMinCut(graph, {
3120
+ * source: 's',
3121
+ * sink: 't',
3122
+ * });
3123
+ * ```
3124
+ */
3125
+ function getMinCut(graph, options) {
3126
+ const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
3127
+ const { value, cutEdges, sourceSide } = solveMaxFlow(graph, "getMinCut", "options.source", "options.sink", options.source, options.sink, getCapacity);
3128
+ const sourcePartition = [];
3129
+ const sinkPartition = [];
3130
+ for (const node of graph.nodes) (sourceSide.has(node.id) ? sourcePartition : sinkPartition).push(node.id);
3131
+ return {
3132
+ value,
3133
+ cutEdges: cutEdges.map((edge) => edge.id),
3134
+ partition: {
3135
+ source: sourcePartition,
3136
+ sink: sinkPartition
3137
+ }
3138
+ };
3139
+ }
3358
3140
 
3359
3141
  //#endregion
3360
3142
  //#region src/algorithms/dominators.ts
@@ -3524,4 +3306,4 @@ function getTransitiveReduction(graph) {
3524
3306
  }
3525
3307
 
3526
3308
  //#endregion
3527
- export { joinPaths as $, dfs as A, toEdgeConfig as B, genPostorders as C, getPreorder as D, getPostorders as E, isConnected as F, getAStarPath as G, genCycles as H, isTree as I, getShortestPath as J, getAllPairsShortestPaths as K, flatten as L, getTopologicalSort as M, hasPath as N, getPreorders as O, isAcyclic as P, getStronglyConnectedComponents as Q, getSubgraph as R, getMinimumSpanningTree as S, getPostorder as T, genShortestPaths as U, toNodeConfig as V, genSimplePaths as W, getSimplePath as X, getShortestPaths as Y, getSimplePaths as Z, getEigenvectorCentrality as _, updateEdge as _t, isIsomorphic as a, createGraphEdge as at, getOutDegreeCentrality as b, getBridges as c, createGraphPort as ct, getGreedyModularityCommunities as d, deleteEntities as dt, GraphInstance as et, getLabelPropagationCommunities as f, deleteNode as ft, getDegreeCentrality as g, hasNode as gt, getClosenessCentrality as h, hasEdge as ht, getLouvainCommunities as i, createGraph as it, getConnectedComponents as j, bfs as k, genGirvanNewmanCommunities as l, createVisualGraph as lt, getBetweennessCentrality as m, getNode as mt, getDominatorTree as n, addEntities as nt, getArticulationPoints as o, createGraphFromTransition as ot, getModularity as p, getEdge as pt, getCycles as q, getMaxFlow as r, addNode as rt, getBiconnectedComponents as s, createGraphNode as st, getTransitiveReduction as t, addEdge as tt, getGirvanNewmanCommunities as u, deleteEdge as ut, getHITS as v, updateEntities as vt, genPreorders as w, getPageRank as x, getInDegreeCentrality as y, updateNode as yt, reverseGraph as z };
3309
+ export { getAStarPath as $, genPreorders as A, getTopologicalSort as B, getHITS as C, getPageRank as D, getOutDegreeCentrality as E, bfs as F, flatten as G, isAcyclic as H, dfs as I, getSubgraph as J, getFlattenedGraph as K, genBFS as L, getPostorders as M, getPreorder as N, getMinimumSpanningTree as O, getPreorders as P, genSimplePaths as Q, genDFS as R, getEigenvectorCentrality as S, getKatzCentrality as T, isConnected as U, hasPath as V, isTree as W, genCycles as X, reverseGraph as Y, genShortestPaths as Z, getCoreNumbers as _, getLouvainCommunities as a, getSimplePath as at, getClosenessCentrality as b, getBiconnectedComponents as c, joinPaths as ct, getGirvanNewmanCommunities as d, getAllPairsShortestPaths as et, getGreedyModularityCommunities as f, isBipartite as g, getMaximumBipartiteMatching as h, getMinCut as i, getShortestPaths as it, getPostorder as j, genPostorders as k, getBridges as l, mulberry32 as lt, getModularity as m, getDominatorTree as n, getJoinedPath as nt, isIsomorphic as o, getSimplePaths as ot, getLabelPropagationCommunities as p, getReversedGraph as q, getMaxFlow as r, getShortestPath as rt, getArticulationPoints as s, getStronglyConnectedComponents as st, getTransitiveReduction as t, getCycles as tt, genGirvanNewmanCommunities as u, getKCore as v, getInDegreeCentrality as w, getDegreeCentrality as x, getBetweennessCentrality as y, getConnectedComponents as z };