@statelyai/graph 0.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.
- package/README.md +191 -0
- package/dist/adjacency-list-CXpOCibq.mjs +49 -0
- package/dist/adjacency-list-DW-lAUe8.d.mts +10 -0
- package/dist/algorithms-R35X6ro4.mjs +1197 -0
- package/dist/algorithms.d.mts +82 -0
- package/dist/algorithms.mjs +3 -0
- package/dist/dot-BRtq3e3c.mjs +59 -0
- package/dist/dot-HmJeUMsj.d.mts +6 -0
- package/dist/edge-list-BRujEnnU.mjs +39 -0
- package/dist/edge-list-CJmfoNu2.d.mts +10 -0
- package/dist/formats/adjacency-list.d.mts +2 -0
- package/dist/formats/adjacency-list.mjs +3 -0
- package/dist/formats/dot.d.mts +2 -0
- package/dist/formats/dot.mjs +3 -0
- package/dist/formats/edge-list.d.mts +2 -0
- package/dist/formats/edge-list.mjs +3 -0
- package/dist/formats/graphml.d.mts +2 -0
- package/dist/formats/graphml.mjs +3 -0
- package/dist/graphml-CMjPzSfY.d.mts +7 -0
- package/dist/graphml-CUTNRXqd.mjs +240 -0
- package/dist/index.d.mts +142 -0
- package/dist/index.mjs +356 -0
- package/dist/indexing-BHg1VhqN.mjs +93 -0
- package/dist/queries.d.mts +37 -0
- package/dist/queries.mjs +221 -0
- package/dist/schemas.d.mts +46 -0
- package/dist/schemas.mjs +30 -0
- package/dist/types-XV3S5Jnh.d.mts +219 -0
- package/package.json +77 -0
- package/schemas/edge.schema.json +32 -0
- package/schemas/graph.schema.json +96 -0
- package/schemas/node.schema.json +35 -0
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-BHg1VhqN.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/graph.ts
|
|
4
|
+
function resolveNode(config) {
|
|
5
|
+
const node = {
|
|
6
|
+
type: "node",
|
|
7
|
+
id: config.id,
|
|
8
|
+
parentId: config.parentId ?? null,
|
|
9
|
+
initialNodeId: config.initialNodeId ?? null,
|
|
10
|
+
label: config.label ?? "",
|
|
11
|
+
data: config.data
|
|
12
|
+
};
|
|
13
|
+
if (config.x !== void 0) node.x = config.x;
|
|
14
|
+
if (config.y !== void 0) node.y = config.y;
|
|
15
|
+
if (config.width !== void 0) node.width = config.width;
|
|
16
|
+
if (config.height !== void 0) node.height = config.height;
|
|
17
|
+
if (config.shape !== void 0) node.shape = config.shape;
|
|
18
|
+
if (config.color !== void 0) node.color = config.color;
|
|
19
|
+
if (config.style !== void 0) node.style = config.style;
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
function resolveEdge(config) {
|
|
23
|
+
const edge = {
|
|
24
|
+
type: "edge",
|
|
25
|
+
id: config.id,
|
|
26
|
+
sourceId: config.sourceId,
|
|
27
|
+
targetId: config.targetId,
|
|
28
|
+
label: config.label ?? "",
|
|
29
|
+
data: config.data
|
|
30
|
+
};
|
|
31
|
+
if (config.x !== void 0) edge.x = config.x;
|
|
32
|
+
if (config.y !== void 0) edge.y = config.y;
|
|
33
|
+
if (config.width !== void 0) edge.width = config.width;
|
|
34
|
+
if (config.height !== void 0) edge.height = config.height;
|
|
35
|
+
if (config.color !== void 0) edge.color = config.color;
|
|
36
|
+
if (config.style !== void 0) edge.style = config.style;
|
|
37
|
+
return edge;
|
|
38
|
+
}
|
|
39
|
+
/** Create a graph from a config. Resolves defaults for all fields. */
|
|
40
|
+
function createGraph(config) {
|
|
41
|
+
const graph = {
|
|
42
|
+
id: config?.id ?? "",
|
|
43
|
+
type: config?.type ?? "directed",
|
|
44
|
+
initialNodeId: config?.initialNodeId ?? null,
|
|
45
|
+
nodes: (config?.nodes ?? []).map(resolveNode),
|
|
46
|
+
edges: (config?.edges ?? []).map(resolveEdge),
|
|
47
|
+
data: config?.data ?? void 0
|
|
48
|
+
};
|
|
49
|
+
if (config?.direction !== void 0) graph.direction = config.direction;
|
|
50
|
+
if (config?.style !== void 0) graph.style = config.style;
|
|
51
|
+
return graph;
|
|
52
|
+
}
|
|
53
|
+
/** Create a visual graph with required position/size on all nodes and edges. */
|
|
54
|
+
function createVisualGraph(config) {
|
|
55
|
+
const base = createGraph(config);
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
direction: config?.direction ?? "down",
|
|
59
|
+
nodes: base.nodes.map((n) => ({
|
|
60
|
+
...n,
|
|
61
|
+
x: n.x ?? 0,
|
|
62
|
+
y: n.y ?? 0,
|
|
63
|
+
width: n.width ?? 0,
|
|
64
|
+
height: n.height ?? 0,
|
|
65
|
+
shape: n.shape ?? "rectangle"
|
|
66
|
+
})),
|
|
67
|
+
edges: base.edges.map((e) => ({
|
|
68
|
+
...e,
|
|
69
|
+
x: e.x ?? 0,
|
|
70
|
+
y: e.y ?? 0,
|
|
71
|
+
width: e.width ?? 0,
|
|
72
|
+
height: e.height ?? 0
|
|
73
|
+
}))
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Get a node by id, or `undefined` if not found. */
|
|
77
|
+
function getNode(graph, id) {
|
|
78
|
+
const arrayIdx = getIndex(graph).nodeById.get(id);
|
|
79
|
+
return arrayIdx !== void 0 ? graph.nodes[arrayIdx] : void 0;
|
|
80
|
+
}
|
|
81
|
+
/** Get an edge by id, or `undefined` if not found. */
|
|
82
|
+
function getEdge(graph, id) {
|
|
83
|
+
const arrayIdx = getIndex(graph).edgeById.get(id);
|
|
84
|
+
return arrayIdx !== void 0 ? graph.edges[arrayIdx] : void 0;
|
|
85
|
+
}
|
|
86
|
+
/** Check if a node exists in the graph. */
|
|
87
|
+
function hasNode(graph, id) {
|
|
88
|
+
return getIndex(graph).nodeById.has(id);
|
|
89
|
+
}
|
|
90
|
+
/** Check if an edge exists in the graph. */
|
|
91
|
+
function hasEdge(graph, id) {
|
|
92
|
+
return getIndex(graph).edgeById.has(id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* **Mutable.** Add a node to the graph. Mutates `graph.nodes` in place.
|
|
96
|
+
* @returns The resolved node that was added.
|
|
97
|
+
*/
|
|
98
|
+
function addNode(graph, config) {
|
|
99
|
+
const idx = getIndex(graph);
|
|
100
|
+
if (idx.nodeById.has(config.id)) throw new Error(`Node "${config.id}" already exists`);
|
|
101
|
+
if (config.parentId != null && !idx.nodeById.has(config.parentId)) throw new Error(`Parent node "${config.parentId}" does not exist`);
|
|
102
|
+
const node = resolveNode(config);
|
|
103
|
+
indexAddNode(idx, node, graph.nodes.push(node) - 1);
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* **Mutable.** Add an edge to the graph. Mutates `graph.edges` in place.
|
|
108
|
+
* @returns The resolved edge that was added.
|
|
109
|
+
*/
|
|
110
|
+
function addEdge(graph, config) {
|
|
111
|
+
const idx = getIndex(graph);
|
|
112
|
+
if (idx.edgeById.has(config.id)) throw new Error(`Edge "${config.id}" already exists`);
|
|
113
|
+
if (!idx.nodeById.has(config.sourceId)) throw new Error(`Source node "${config.sourceId}" does not exist`);
|
|
114
|
+
if (!idx.nodeById.has(config.targetId)) throw new Error(`Target node "${config.targetId}" does not exist`);
|
|
115
|
+
const edge = resolveEdge(config);
|
|
116
|
+
indexAddEdge(idx, edge, graph.edges.push(edge) - 1);
|
|
117
|
+
return edge;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* **Mutable.** Delete a node and its connected edges. Mutates `graph.nodes`
|
|
121
|
+
* and `graph.edges` in place.
|
|
122
|
+
*
|
|
123
|
+
* By default, children are deleted recursively.
|
|
124
|
+
* With `{ reparent: true }`, children are re-parented to the deleted node's parent.
|
|
125
|
+
*/
|
|
126
|
+
function deleteNode(graph, id, opts) {
|
|
127
|
+
const node = getNode(graph, id);
|
|
128
|
+
if (!node) throw new Error(`Node "${id}" does not exist`);
|
|
129
|
+
if (opts?.reparent) {
|
|
130
|
+
for (const n of graph.nodes) if (n.parentId === id) n.parentId = node.parentId;
|
|
131
|
+
graph.nodes = graph.nodes.filter((n) => n.id !== id);
|
|
132
|
+
graph.edges = graph.edges.filter((e) => e.sourceId !== id && e.targetId !== id);
|
|
133
|
+
} else {
|
|
134
|
+
const toDelete = collectDescendants(graph, id);
|
|
135
|
+
graph.nodes = graph.nodes.filter((n) => !toDelete.has(n.id));
|
|
136
|
+
graph.edges = graph.edges.filter((e) => !toDelete.has(e.sourceId) && !toDelete.has(e.targetId));
|
|
137
|
+
}
|
|
138
|
+
invalidateIndex(graph);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* **Mutable.** Delete an edge. Mutates `graph.edges` in place.
|
|
142
|
+
*/
|
|
143
|
+
function deleteEdge(graph, id) {
|
|
144
|
+
if (!hasEdge(graph, id)) throw new Error(`Edge "${id}" does not exist`);
|
|
145
|
+
graph.edges = graph.edges.filter((e) => e.id !== id);
|
|
146
|
+
invalidateIndex(graph);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* **Mutable.** Update a node in place.
|
|
150
|
+
* @returns The updated node.
|
|
151
|
+
*/
|
|
152
|
+
function updateNode(graph, id, update) {
|
|
153
|
+
const idx = getIndex(graph);
|
|
154
|
+
const arrayIdx = idx.nodeById.get(id);
|
|
155
|
+
if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
|
|
156
|
+
if (update.parentId !== void 0 && update.parentId !== null) {
|
|
157
|
+
if (!idx.nodeById.has(update.parentId)) throw new Error(`Parent node "${update.parentId}" does not exist`);
|
|
158
|
+
}
|
|
159
|
+
const node = graph.nodes[arrayIdx];
|
|
160
|
+
const oldParentId = node.parentId;
|
|
161
|
+
const updated = {
|
|
162
|
+
...node,
|
|
163
|
+
...update.parentId !== void 0 && { parentId: update.parentId ?? null },
|
|
164
|
+
...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
|
|
165
|
+
...update.label !== void 0 && { label: update.label },
|
|
166
|
+
...update.data !== void 0 && { data: update.data }
|
|
167
|
+
};
|
|
168
|
+
graph.nodes[arrayIdx] = updated;
|
|
169
|
+
if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
|
|
170
|
+
return updated;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* **Mutable.** Update an edge in place.
|
|
174
|
+
* @returns The updated edge.
|
|
175
|
+
*/
|
|
176
|
+
function updateEdge(graph, id, update) {
|
|
177
|
+
const idx = getIndex(graph);
|
|
178
|
+
const arrayIdx = idx.edgeById.get(id);
|
|
179
|
+
if (arrayIdx === void 0) throw new Error(`Edge "${id}" does not exist`);
|
|
180
|
+
if (update.sourceId !== void 0 && !idx.nodeById.has(update.sourceId)) throw new Error(`Source node "${update.sourceId}" does not exist`);
|
|
181
|
+
if (update.targetId !== void 0 && !idx.nodeById.has(update.targetId)) throw new Error(`Target node "${update.targetId}" does not exist`);
|
|
182
|
+
const edge = graph.edges[arrayIdx];
|
|
183
|
+
const oldSourceId = edge.sourceId;
|
|
184
|
+
const oldTargetId = edge.targetId;
|
|
185
|
+
const updated = {
|
|
186
|
+
...edge,
|
|
187
|
+
...update.sourceId !== void 0 && { sourceId: update.sourceId },
|
|
188
|
+
...update.targetId !== void 0 && { targetId: update.targetId },
|
|
189
|
+
...update.label !== void 0 && { label: update.label },
|
|
190
|
+
...update.data !== void 0 && { data: update.data }
|
|
191
|
+
};
|
|
192
|
+
graph.edges[arrayIdx] = updated;
|
|
193
|
+
if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
|
|
194
|
+
return updated;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* **Mutable.** Add multiple nodes and edges to the graph.
|
|
198
|
+
* Nodes are added first, then edges (so edges can reference new nodes).
|
|
199
|
+
*/
|
|
200
|
+
function addEntities(graph, entities) {
|
|
201
|
+
for (const nodeConfig of entities.nodes ?? []) addNode(graph, nodeConfig);
|
|
202
|
+
for (const edgeConfig of entities.edges ?? []) addEdge(graph, edgeConfig);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* **Mutable.** Delete entities by id(s). Automatically detects whether each id
|
|
206
|
+
* is a node or edge. Node deletions cascade to children and connected edges.
|
|
207
|
+
*/
|
|
208
|
+
function deleteEntities(graph, ids, opts) {
|
|
209
|
+
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
210
|
+
for (const id of idArray) if (hasNode(graph, id)) deleteNode(graph, id, opts);
|
|
211
|
+
else if (hasEdge(graph, id)) deleteEdge(graph, id);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* **Mutable.** Update multiple nodes and edges in place.
|
|
215
|
+
* Each entry must include an `id` to identify which entity to update.
|
|
216
|
+
*/
|
|
217
|
+
function updateEntities(graph, updates) {
|
|
218
|
+
for (const nodeUpdate of updates.nodes ?? []) {
|
|
219
|
+
const { id, ...patch } = nodeUpdate;
|
|
220
|
+
updateNode(graph, id, patch);
|
|
221
|
+
}
|
|
222
|
+
for (const edgeUpdate of updates.edges ?? []) {
|
|
223
|
+
const { id, ...patch } = edgeUpdate;
|
|
224
|
+
updateEdge(graph, id, patch);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* OOP wrapper around a plain `Graph` object.
|
|
229
|
+
* Delegates to the standalone mutable functions.
|
|
230
|
+
*/
|
|
231
|
+
var GraphInstance = class GraphInstance {
|
|
232
|
+
graph;
|
|
233
|
+
constructor(config) {
|
|
234
|
+
this.graph = createGraph(config);
|
|
235
|
+
}
|
|
236
|
+
/** Wrap an existing plain graph object. */
|
|
237
|
+
static from(graph) {
|
|
238
|
+
const instance = Object.create(GraphInstance.prototype);
|
|
239
|
+
instance.graph = graph;
|
|
240
|
+
return instance;
|
|
241
|
+
}
|
|
242
|
+
get id() {
|
|
243
|
+
return this.graph.id;
|
|
244
|
+
}
|
|
245
|
+
get type() {
|
|
246
|
+
return this.graph.type;
|
|
247
|
+
}
|
|
248
|
+
get nodes() {
|
|
249
|
+
return this.graph.nodes;
|
|
250
|
+
}
|
|
251
|
+
get edges() {
|
|
252
|
+
return this.graph.edges;
|
|
253
|
+
}
|
|
254
|
+
get data() {
|
|
255
|
+
return this.graph.data;
|
|
256
|
+
}
|
|
257
|
+
getNode(id) {
|
|
258
|
+
return getNode(this.graph, id);
|
|
259
|
+
}
|
|
260
|
+
getEdge(id) {
|
|
261
|
+
return getEdge(this.graph, id);
|
|
262
|
+
}
|
|
263
|
+
hasNode(id) {
|
|
264
|
+
return hasNode(this.graph, id);
|
|
265
|
+
}
|
|
266
|
+
hasEdge(id) {
|
|
267
|
+
return hasEdge(this.graph, id);
|
|
268
|
+
}
|
|
269
|
+
addNode(config) {
|
|
270
|
+
return addNode(this.graph, config);
|
|
271
|
+
}
|
|
272
|
+
addEdge(config) {
|
|
273
|
+
return addEdge(this.graph, config);
|
|
274
|
+
}
|
|
275
|
+
deleteNode(id, opts) {
|
|
276
|
+
return deleteNode(this.graph, id, opts);
|
|
277
|
+
}
|
|
278
|
+
deleteEdge(id) {
|
|
279
|
+
return deleteEdge(this.graph, id);
|
|
280
|
+
}
|
|
281
|
+
updateNode(id, update) {
|
|
282
|
+
return updateNode(this.graph, id, update);
|
|
283
|
+
}
|
|
284
|
+
updateEdge(id, update) {
|
|
285
|
+
return updateEdge(this.graph, id, update);
|
|
286
|
+
}
|
|
287
|
+
addEntities(entities) {
|
|
288
|
+
return addEntities(this.graph, entities);
|
|
289
|
+
}
|
|
290
|
+
deleteEntities(ids, opts) {
|
|
291
|
+
return deleteEntities(this.graph, ids, opts);
|
|
292
|
+
}
|
|
293
|
+
updateEntities(updates) {
|
|
294
|
+
return updateEntities(this.graph, updates);
|
|
295
|
+
}
|
|
296
|
+
toJSON() {
|
|
297
|
+
return this.graph;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function collectDescendants(graph, id) {
|
|
301
|
+
const idx = getIndex(graph);
|
|
302
|
+
const toDelete = /* @__PURE__ */ new Set();
|
|
303
|
+
const walk = (nodeId) => {
|
|
304
|
+
toDelete.add(nodeId);
|
|
305
|
+
const childIds = idx.childNodes.get(nodeId) ?? [];
|
|
306
|
+
for (const childId of childIds) if (!toDelete.has(childId)) walk(childId);
|
|
307
|
+
};
|
|
308
|
+
walk(id);
|
|
309
|
+
return toDelete;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/algorithms.ts
|
|
314
|
+
function* bfs(graph, startId) {
|
|
315
|
+
const idx = getIndex(graph);
|
|
316
|
+
const visited = /* @__PURE__ */ new Set();
|
|
317
|
+
const queue = [startId];
|
|
318
|
+
visited.add(startId);
|
|
319
|
+
while (queue.length > 0) {
|
|
320
|
+
const id = queue.shift();
|
|
321
|
+
const ni = idx.nodeById.get(id);
|
|
322
|
+
if (ni === void 0) continue;
|
|
323
|
+
yield graph.nodes[ni];
|
|
324
|
+
for (const nId of getNeighborIds(graph, id)) if (!visited.has(nId)) {
|
|
325
|
+
visited.add(nId);
|
|
326
|
+
queue.push(nId);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function* dfs(graph, startId) {
|
|
331
|
+
const idx = getIndex(graph);
|
|
332
|
+
const visited = /* @__PURE__ */ new Set();
|
|
333
|
+
const stack = [startId];
|
|
334
|
+
while (stack.length > 0) {
|
|
335
|
+
const id = stack.pop();
|
|
336
|
+
if (visited.has(id)) continue;
|
|
337
|
+
visited.add(id);
|
|
338
|
+
const ni = idx.nodeById.get(id);
|
|
339
|
+
if (ni === void 0) continue;
|
|
340
|
+
yield graph.nodes[ni];
|
|
341
|
+
for (const nId of getNeighborIds(graph, id)) if (!visited.has(nId)) stack.push(nId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function getNeighborIds(graph, nodeId) {
|
|
345
|
+
const idx = getIndex(graph);
|
|
346
|
+
const ids = [];
|
|
347
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
348
|
+
const ai = idx.edgeById.get(eid);
|
|
349
|
+
if (ai !== void 0) ids.push(graph.edges[ai].targetId);
|
|
350
|
+
}
|
|
351
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
352
|
+
const ai = idx.edgeById.get(eid);
|
|
353
|
+
if (ai !== void 0) ids.push(graph.edges[ai].sourceId);
|
|
354
|
+
}
|
|
355
|
+
return ids;
|
|
356
|
+
}
|
|
357
|
+
function getSuccessorIds(graph, nodeId) {
|
|
358
|
+
const idx = getIndex(graph);
|
|
359
|
+
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
360
|
+
}
|
|
361
|
+
function isAcyclic(graph) {
|
|
362
|
+
if (graph.type === "undirected") return isAcyclicUndirected(graph);
|
|
363
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
364
|
+
const color = /* @__PURE__ */ new Map();
|
|
365
|
+
for (const n of graph.nodes) color.set(n.id, WHITE);
|
|
366
|
+
const hasCycle = (id) => {
|
|
367
|
+
color.set(id, GRAY);
|
|
368
|
+
for (const nId of getSuccessorIds(graph, id)) {
|
|
369
|
+
const c = color.get(nId);
|
|
370
|
+
if (c === GRAY) return true;
|
|
371
|
+
if (c === WHITE && hasCycle(nId)) return true;
|
|
372
|
+
}
|
|
373
|
+
color.set(id, BLACK);
|
|
374
|
+
return false;
|
|
375
|
+
};
|
|
376
|
+
for (const n of graph.nodes) if (color.get(n.id) === WHITE && hasCycle(n.id)) return false;
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
function isAcyclicUndirected(graph) {
|
|
380
|
+
const idx = getIndex(graph);
|
|
381
|
+
const visited = /* @__PURE__ */ new Set();
|
|
382
|
+
const hasCycle = (id, parentId) => {
|
|
383
|
+
visited.add(id);
|
|
384
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
385
|
+
const ai = idx.edgeById.get(eid);
|
|
386
|
+
if (ai === void 0) continue;
|
|
387
|
+
const neighborId = graph.edges[ai].targetId;
|
|
388
|
+
if (!visited.has(neighborId)) {
|
|
389
|
+
if (hasCycle(neighborId, id)) return true;
|
|
390
|
+
} else if (neighborId !== parentId) return true;
|
|
391
|
+
}
|
|
392
|
+
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
393
|
+
const ai = idx.edgeById.get(eid);
|
|
394
|
+
if (ai === void 0) continue;
|
|
395
|
+
const neighborId = graph.edges[ai].sourceId;
|
|
396
|
+
if (!visited.has(neighborId)) {
|
|
397
|
+
if (hasCycle(neighborId, id)) return true;
|
|
398
|
+
} else if (neighborId !== parentId) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
};
|
|
402
|
+
for (const n of graph.nodes) if (!visited.has(n.id) && hasCycle(n.id, null)) return false;
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
function getConnectedComponents(graph) {
|
|
406
|
+
const idx = getIndex(graph);
|
|
407
|
+
const visited = /* @__PURE__ */ new Set();
|
|
408
|
+
const components = [];
|
|
409
|
+
for (const n of graph.nodes) {
|
|
410
|
+
if (visited.has(n.id)) continue;
|
|
411
|
+
const component = [];
|
|
412
|
+
const queue = [n.id];
|
|
413
|
+
visited.add(n.id);
|
|
414
|
+
while (queue.length > 0) {
|
|
415
|
+
const id = queue.shift();
|
|
416
|
+
const ni = idx.nodeById.get(id);
|
|
417
|
+
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
418
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
419
|
+
const ai = idx.edgeById.get(eid);
|
|
420
|
+
if (ai === void 0) continue;
|
|
421
|
+
const neighborId = graph.edges[ai].targetId;
|
|
422
|
+
if (!visited.has(neighborId)) {
|
|
423
|
+
visited.add(neighborId);
|
|
424
|
+
queue.push(neighborId);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
428
|
+
const ai = idx.edgeById.get(eid);
|
|
429
|
+
if (ai === void 0) continue;
|
|
430
|
+
const neighborId = graph.edges[ai].sourceId;
|
|
431
|
+
if (!visited.has(neighborId)) {
|
|
432
|
+
visited.add(neighborId);
|
|
433
|
+
queue.push(neighborId);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
components.push(component);
|
|
438
|
+
}
|
|
439
|
+
return components;
|
|
440
|
+
}
|
|
441
|
+
function getTopologicalSort(graph) {
|
|
442
|
+
const idx = getIndex(graph);
|
|
443
|
+
const inDeg = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const n of graph.nodes) inDeg.set(n.id, 0);
|
|
445
|
+
for (const e of graph.edges) inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
|
|
446
|
+
const queue = [];
|
|
447
|
+
for (const [id, deg] of inDeg) if (deg === 0) queue.push(id);
|
|
448
|
+
const result = [];
|
|
449
|
+
while (queue.length > 0) {
|
|
450
|
+
const id = queue.shift();
|
|
451
|
+
const ni = idx.nodeById.get(id);
|
|
452
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
453
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
454
|
+
const ai = idx.edgeById.get(eid);
|
|
455
|
+
if (ai === void 0) continue;
|
|
456
|
+
const targetId = graph.edges[ai].targetId;
|
|
457
|
+
const newDeg = (inDeg.get(targetId) ?? 1) - 1;
|
|
458
|
+
inDeg.set(targetId, newDeg);
|
|
459
|
+
if (newDeg === 0) queue.push(targetId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (result.length !== graph.nodes.length) return null;
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
function hasPath(graph, sourceId, targetId) {
|
|
466
|
+
return getShortestPaths(graph, {
|
|
467
|
+
from: sourceId,
|
|
468
|
+
to: targetId
|
|
469
|
+
}).length > 0;
|
|
470
|
+
}
|
|
471
|
+
function isConnected(graph) {
|
|
472
|
+
if (graph.nodes.length === 0) return true;
|
|
473
|
+
return getConnectedComponents(graph).length <= 1;
|
|
474
|
+
}
|
|
475
|
+
function isTree(graph) {
|
|
476
|
+
return isConnected(graph) && isAcyclic(graph);
|
|
477
|
+
}
|
|
478
|
+
/** Resolve the `from` node ID from opts or graph defaults. */
|
|
479
|
+
function resolveFrom(graph, opts) {
|
|
480
|
+
if (opts?.from) return opts.from;
|
|
481
|
+
if (graph.initialNodeId) return graph.initialNodeId;
|
|
482
|
+
const inDeg = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const n of graph.nodes) inDeg.set(n.id, 0);
|
|
484
|
+
for (const e of graph.edges) inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
|
|
485
|
+
const roots = [...inDeg.entries()].filter(([, d]) => d === 0).map(([id]) => id);
|
|
486
|
+
if (roots.length === 1) return roots[0];
|
|
487
|
+
throw new Error("Cannot determine start node — provide opts.from or set graph.initialNodeId");
|
|
488
|
+
}
|
|
489
|
+
/** Get neighbor IDs with their connecting edges. */
|
|
490
|
+
function getNeighborEdges(graph, nodeId) {
|
|
491
|
+
const idx = getIndex(graph);
|
|
492
|
+
const result = [];
|
|
493
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
494
|
+
const ai = idx.edgeById.get(eid);
|
|
495
|
+
if (ai !== void 0) {
|
|
496
|
+
const e = graph.edges[ai];
|
|
497
|
+
result.push({
|
|
498
|
+
neighborId: e.targetId,
|
|
499
|
+
edge: e
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
504
|
+
const ai = idx.edgeById.get(eid);
|
|
505
|
+
if (ai !== void 0) {
|
|
506
|
+
const e = graph.edges[ai];
|
|
507
|
+
result.push({
|
|
508
|
+
neighborId: e.sourceId,
|
|
509
|
+
edge: e
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Returns all shortest paths from a source node.
|
|
517
|
+
* Returns all paths of equal minimum length per target (not just one).
|
|
518
|
+
* Uses BFS by default; Dijkstra when `opts.getWeight` is provided.
|
|
519
|
+
*/
|
|
520
|
+
/** Compute distance + prev maps via BFS or Dijkstra. */
|
|
521
|
+
function computeShortestDistances(graph, sourceId, getWeight) {
|
|
522
|
+
const dist = /* @__PURE__ */ new Map();
|
|
523
|
+
const prev = /* @__PURE__ */ new Map();
|
|
524
|
+
dist.set(sourceId, 0);
|
|
525
|
+
prev.set(sourceId, []);
|
|
526
|
+
if (!getWeight) {
|
|
527
|
+
const queue = [sourceId];
|
|
528
|
+
while (queue.length > 0) {
|
|
529
|
+
const id = queue.shift();
|
|
530
|
+
const d = dist.get(id);
|
|
531
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
|
|
532
|
+
const newDist = d + 1;
|
|
533
|
+
const existing = dist.get(neighborId);
|
|
534
|
+
if (existing === void 0) {
|
|
535
|
+
dist.set(neighborId, newDist);
|
|
536
|
+
prev.set(neighborId, [{
|
|
537
|
+
from: id,
|
|
538
|
+
edge
|
|
539
|
+
}]);
|
|
540
|
+
queue.push(neighborId);
|
|
541
|
+
} else if (existing === newDist) prev.get(neighborId).push({
|
|
542
|
+
from: id,
|
|
543
|
+
edge
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
const visited = /* @__PURE__ */ new Set();
|
|
549
|
+
const pq = [{
|
|
550
|
+
id: sourceId,
|
|
551
|
+
dist: 0
|
|
552
|
+
}];
|
|
553
|
+
while (pq.length > 0) {
|
|
554
|
+
let minIdx = 0;
|
|
555
|
+
for (let i = 1; i < pq.length; i++) if (pq[i].dist < pq[minIdx].dist) minIdx = i;
|
|
556
|
+
const { id, dist: d } = pq.splice(minIdx, 1)[0];
|
|
557
|
+
if (visited.has(id)) continue;
|
|
558
|
+
visited.add(id);
|
|
559
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
|
|
560
|
+
const newDist = d + getWeight(edge);
|
|
561
|
+
const existing = dist.get(neighborId);
|
|
562
|
+
if (existing === void 0 || newDist < existing) {
|
|
563
|
+
dist.set(neighborId, newDist);
|
|
564
|
+
prev.set(neighborId, [{
|
|
565
|
+
from: id,
|
|
566
|
+
edge
|
|
567
|
+
}]);
|
|
568
|
+
pq.push({
|
|
569
|
+
id: neighborId,
|
|
570
|
+
dist: newDist
|
|
571
|
+
});
|
|
572
|
+
} else if (existing === newDist) prev.get(neighborId).push({
|
|
573
|
+
from: id,
|
|
574
|
+
edge
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
dist,
|
|
581
|
+
prev
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/** Reconstruct all shortest paths to a target by backtracking through prev map. */
|
|
585
|
+
function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
586
|
+
if (targetId === sourceNode.id) {
|
|
587
|
+
yield {
|
|
588
|
+
source: sourceNode,
|
|
589
|
+
steps: []
|
|
590
|
+
};
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const preds = prev.get(targetId);
|
|
594
|
+
if (!preds || preds.length === 0) return;
|
|
595
|
+
const targetNi = getIndex(graph).nodeById.get(targetId);
|
|
596
|
+
const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((n) => n.id === targetId);
|
|
597
|
+
for (const { from, edge } of preds) for (const prefix of reconstructPaths(graph, prev, sourceNode, from)) yield {
|
|
598
|
+
source: sourceNode,
|
|
599
|
+
steps: [...prefix.steps, {
|
|
600
|
+
edge,
|
|
601
|
+
node: targetNode
|
|
602
|
+
}]
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Lazily yields all shortest paths from a source node.
|
|
607
|
+
* Use `getShortestPaths` for the full array.
|
|
608
|
+
*/
|
|
609
|
+
function* genShortestPaths(graph, opts) {
|
|
610
|
+
const idx = getIndex(graph);
|
|
611
|
+
const sourceId = resolveFrom(graph, opts);
|
|
612
|
+
const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight);
|
|
613
|
+
const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
|
|
614
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
615
|
+
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((n) => n.id === sourceId);
|
|
616
|
+
for (const targetId of targets) yield* reconstructPaths(graph, prev, sourceNode, targetId);
|
|
617
|
+
}
|
|
618
|
+
function getShortestPaths(graph, opts) {
|
|
619
|
+
return [...genShortestPaths(graph, opts)];
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Returns a single shortest path from source to target, or undefined if unreachable.
|
|
623
|
+
*/
|
|
624
|
+
function getShortestPath(graph, opts) {
|
|
625
|
+
for (const path of genShortestPaths(graph, opts)) return path;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Returns all simple (acyclic) paths from a source node.
|
|
629
|
+
* Uses DFS with backtracking.
|
|
630
|
+
*/
|
|
631
|
+
function getSimplePaths(graph, opts) {
|
|
632
|
+
return [...genSimplePaths(graph, opts)];
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Lazily yields all simple (acyclic) paths from a source node.
|
|
636
|
+
* Use `getSimplePaths` for the full array.
|
|
637
|
+
*/
|
|
638
|
+
function* genSimplePaths(graph, opts) {
|
|
639
|
+
const idx = getIndex(graph);
|
|
640
|
+
const sourceId = resolveFrom(graph, opts);
|
|
641
|
+
const sourceNi = idx.nodeById.get(sourceId);
|
|
642
|
+
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((n) => n.id === sourceId);
|
|
643
|
+
const targetId = opts?.to;
|
|
644
|
+
const visited = /* @__PURE__ */ new Set();
|
|
645
|
+
const currentSteps = [];
|
|
646
|
+
const found = [];
|
|
647
|
+
function dfsCollect(nodeId) {
|
|
648
|
+
visited.add(nodeId);
|
|
649
|
+
if (targetId !== void 0) {
|
|
650
|
+
if (nodeId === targetId) {
|
|
651
|
+
found.push({
|
|
652
|
+
source: sourceNode,
|
|
653
|
+
steps: [...currentSteps]
|
|
654
|
+
});
|
|
655
|
+
visited.delete(nodeId);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
} else if (currentSteps.length > 0) found.push({
|
|
659
|
+
source: sourceNode,
|
|
660
|
+
steps: [...currentSteps]
|
|
661
|
+
});
|
|
662
|
+
for (const { neighborId, edge } of getNeighborEdges(graph, nodeId)) if (!visited.has(neighborId)) {
|
|
663
|
+
const neighborNi = idx.nodeById.get(neighborId);
|
|
664
|
+
const neighborNode = neighborNi !== void 0 ? graph.nodes[neighborNi] : graph.nodes.find((n) => n.id === neighborId);
|
|
665
|
+
currentSteps.push({
|
|
666
|
+
edge,
|
|
667
|
+
node: neighborNode
|
|
668
|
+
});
|
|
669
|
+
dfsCollect(neighborId);
|
|
670
|
+
currentSteps.pop();
|
|
671
|
+
}
|
|
672
|
+
visited.delete(nodeId);
|
|
673
|
+
}
|
|
674
|
+
dfsCollect(sourceId);
|
|
675
|
+
yield* found;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Returns a single simple (acyclic) path from source to target, or undefined if unreachable.
|
|
679
|
+
*/
|
|
680
|
+
function getSimplePath(graph, opts) {
|
|
681
|
+
for (const path of genSimplePaths(graph, opts)) return path;
|
|
682
|
+
}
|
|
683
|
+
function getStronglyConnectedComponents(graph) {
|
|
684
|
+
const idx = getIndex(graph);
|
|
685
|
+
let indexCounter = 0;
|
|
686
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
687
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
688
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
689
|
+
const stack = [];
|
|
690
|
+
const result = [];
|
|
691
|
+
function strongconnect(id) {
|
|
692
|
+
nodeIndex.set(id, indexCounter);
|
|
693
|
+
lowlink.set(id, indexCounter);
|
|
694
|
+
indexCounter++;
|
|
695
|
+
stack.push(id);
|
|
696
|
+
onStack.add(id);
|
|
697
|
+
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
698
|
+
const ai = idx.edgeById.get(eid);
|
|
699
|
+
if (ai === void 0) continue;
|
|
700
|
+
const wId = graph.edges[ai].targetId;
|
|
701
|
+
if (!nodeIndex.has(wId)) {
|
|
702
|
+
strongconnect(wId);
|
|
703
|
+
lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(wId)));
|
|
704
|
+
} else if (onStack.has(wId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(wId)));
|
|
705
|
+
}
|
|
706
|
+
if (lowlink.get(id) === nodeIndex.get(id)) {
|
|
707
|
+
const component = [];
|
|
708
|
+
let wId;
|
|
709
|
+
do {
|
|
710
|
+
wId = stack.pop();
|
|
711
|
+
onStack.delete(wId);
|
|
712
|
+
const ni = idx.nodeById.get(wId);
|
|
713
|
+
if (ni !== void 0) component.push(graph.nodes[ni]);
|
|
714
|
+
} while (wId !== id);
|
|
715
|
+
result.push(component);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const n of graph.nodes) if (!nodeIndex.has(n.id)) strongconnect(n.id);
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
function getCycles(graph) {
|
|
722
|
+
return [...genCycles(graph)];
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Lazily yields cycles one at a time.
|
|
726
|
+
* Use `getCycles` for the full array.
|
|
727
|
+
*/
|
|
728
|
+
function* genCycles(graph) {
|
|
729
|
+
if (graph.type === "undirected") yield* genCyclesUndirected(graph);
|
|
730
|
+
else yield* genCyclesDirected(graph);
|
|
731
|
+
}
|
|
732
|
+
function* genCyclesDirected(graph) {
|
|
733
|
+
const idx = getIndex(graph);
|
|
734
|
+
const sortedIds = graph.nodes.map((n) => n.id).sort();
|
|
735
|
+
for (let si = 0; si < sortedIds.length; si++) {
|
|
736
|
+
const startId = sortedIds[si];
|
|
737
|
+
const allowed = new Set(sortedIds.slice(si));
|
|
738
|
+
const visited = /* @__PURE__ */ new Set();
|
|
739
|
+
const steps = [];
|
|
740
|
+
const startNi = idx.nodeById.get(startId);
|
|
741
|
+
const startNode = graph.nodes[startNi];
|
|
742
|
+
const found = [];
|
|
743
|
+
function dfsFind(currentId) {
|
|
744
|
+
visited.add(currentId);
|
|
745
|
+
for (const eid of idx.outEdges.get(currentId) ?? []) {
|
|
746
|
+
const ai = idx.edgeById.get(eid);
|
|
747
|
+
if (ai === void 0) continue;
|
|
748
|
+
const e = graph.edges[ai];
|
|
749
|
+
const neighborId = e.targetId;
|
|
750
|
+
if (neighborId === startId && (steps.length > 0 || currentId === startId)) found.push({
|
|
751
|
+
source: startNode,
|
|
752
|
+
steps: [...steps, {
|
|
753
|
+
edge: e,
|
|
754
|
+
node: startNode
|
|
755
|
+
}]
|
|
756
|
+
});
|
|
757
|
+
else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
758
|
+
const ni = idx.nodeById.get(neighborId);
|
|
759
|
+
steps.push({
|
|
760
|
+
edge: e,
|
|
761
|
+
node: graph.nodes[ni]
|
|
762
|
+
});
|
|
763
|
+
dfsFind(neighborId);
|
|
764
|
+
steps.pop();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
visited.delete(currentId);
|
|
768
|
+
}
|
|
769
|
+
dfsFind(startId);
|
|
770
|
+
yield* found;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function* genCyclesUndirected(graph) {
|
|
774
|
+
const idx = getIndex(graph);
|
|
775
|
+
const sortedIds = graph.nodes.map((n) => n.id).sort();
|
|
776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
777
|
+
for (let si = 0; si < sortedIds.length; si++) {
|
|
778
|
+
const startId = sortedIds[si];
|
|
779
|
+
const allowed = new Set(sortedIds.slice(si));
|
|
780
|
+
const visited = /* @__PURE__ */ new Set();
|
|
781
|
+
const steps = [];
|
|
782
|
+
const startNi = idx.nodeById.get(startId);
|
|
783
|
+
const startNode = graph.nodes[startNi];
|
|
784
|
+
const found = [];
|
|
785
|
+
function dfsFind(currentId, parentId) {
|
|
786
|
+
visited.add(currentId);
|
|
787
|
+
for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
|
|
788
|
+
if (neighborId === parentId) {
|
|
789
|
+
parentId = null;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (neighborId === startId && steps.length >= 2) {
|
|
793
|
+
const innerIds = steps.map((s) => s.node.id).sort().join(",");
|
|
794
|
+
if (!seen.has(innerIds)) {
|
|
795
|
+
seen.add(innerIds);
|
|
796
|
+
found.push({
|
|
797
|
+
source: startNode,
|
|
798
|
+
steps: [...steps, {
|
|
799
|
+
edge,
|
|
800
|
+
node: startNode
|
|
801
|
+
}]
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
} else if (allowed.has(neighborId) && !visited.has(neighborId)) {
|
|
805
|
+
const ni = idx.nodeById.get(neighborId);
|
|
806
|
+
steps.push({
|
|
807
|
+
edge,
|
|
808
|
+
node: graph.nodes[ni]
|
|
809
|
+
});
|
|
810
|
+
dfsFind(neighborId, currentId);
|
|
811
|
+
steps.pop();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
visited.delete(currentId);
|
|
815
|
+
}
|
|
816
|
+
dfsFind(startId, null);
|
|
817
|
+
yield* found;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/** Like getNeighborEdges but always includes both directions (for undirected cycle finding). */
|
|
821
|
+
function getNeighborEdgesAll(graph, nodeId) {
|
|
822
|
+
const idx = getIndex(graph);
|
|
823
|
+
const result = [];
|
|
824
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
825
|
+
const ai = idx.edgeById.get(eid);
|
|
826
|
+
if (ai !== void 0) {
|
|
827
|
+
const e = graph.edges[ai];
|
|
828
|
+
result.push({
|
|
829
|
+
neighborId: e.targetId,
|
|
830
|
+
edge: e
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
835
|
+
const ai = idx.edgeById.get(eid);
|
|
836
|
+
if (ai !== void 0) {
|
|
837
|
+
const e = graph.edges[ai];
|
|
838
|
+
result.push({
|
|
839
|
+
neighborId: e.sourceId,
|
|
840
|
+
edge: e
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Returns a single canonical preorder (DFS visit-order) sequence.
|
|
848
|
+
* Visits neighbors in the order they appear in the adjacency list.
|
|
849
|
+
*/
|
|
850
|
+
function getPreorder(graph, opts) {
|
|
851
|
+
const idx = getIndex(graph);
|
|
852
|
+
const startId = resolveFrom(graph, opts);
|
|
853
|
+
const startNi = idx.nodeById.get(startId);
|
|
854
|
+
if (startNi === void 0) return [];
|
|
855
|
+
const visited = new Set([startId]);
|
|
856
|
+
const result = [graph.nodes[startNi]];
|
|
857
|
+
const stack = [startId];
|
|
858
|
+
while (stack.length > 0) {
|
|
859
|
+
const top = stack[stack.length - 1];
|
|
860
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
861
|
+
if (next === void 0) {
|
|
862
|
+
stack.pop();
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
visited.add(next);
|
|
866
|
+
stack.push(next);
|
|
867
|
+
const ni = idx.nodeById.get(next);
|
|
868
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
869
|
+
}
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Returns a single canonical postorder (DFS finish-order) sequence.
|
|
874
|
+
* Visits neighbors in the order they appear in the adjacency list.
|
|
875
|
+
*/
|
|
876
|
+
function getPostorder(graph, opts) {
|
|
877
|
+
const idx = getIndex(graph);
|
|
878
|
+
const startId = resolveFrom(graph, opts);
|
|
879
|
+
if (idx.nodeById.get(startId) === void 0) return [];
|
|
880
|
+
const visited = new Set([startId]);
|
|
881
|
+
const result = [];
|
|
882
|
+
const stack = [startId];
|
|
883
|
+
while (stack.length > 0) {
|
|
884
|
+
const top = stack[stack.length - 1];
|
|
885
|
+
const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
|
|
886
|
+
if (next === void 0) {
|
|
887
|
+
stack.pop();
|
|
888
|
+
const ni = idx.nodeById.get(top);
|
|
889
|
+
if (ni !== void 0) result.push(graph.nodes[ni]);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
visited.add(next);
|
|
893
|
+
stack.push(next);
|
|
894
|
+
}
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
/** Returns all possible preorder sequences as an array. Can be exponential — prefer `genPreorders`. */
|
|
898
|
+
function getPreorders(graph, opts) {
|
|
899
|
+
return [...genPreorders(graph, opts)];
|
|
900
|
+
}
|
|
901
|
+
/** Returns all possible postorder sequences as an array. Can be exponential — prefer `genPostorders`. */
|
|
902
|
+
function getPostorders(graph, opts) {
|
|
903
|
+
return [...genPostorders(graph, opts)];
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Lazily yields all possible preorder (DFS visit-order) sequences.
|
|
907
|
+
* Different neighbor exploration orders yield different sequences.
|
|
908
|
+
* Use `getPreorder()` for a single canonical ordering.
|
|
909
|
+
*/
|
|
910
|
+
function* genPreorders(graph, opts) {
|
|
911
|
+
const idx = getIndex(graph);
|
|
912
|
+
const startId = resolveFrom(graph, opts);
|
|
913
|
+
const startNi = idx.nodeById.get(startId);
|
|
914
|
+
const startNode = startNi !== void 0 ? graph.nodes[startNi] : void 0;
|
|
915
|
+
if (!startNode) return;
|
|
916
|
+
const queue = [{
|
|
917
|
+
visited: new Set([startId]),
|
|
918
|
+
preorder: [startNode],
|
|
919
|
+
dfsStack: [startId]
|
|
920
|
+
}];
|
|
921
|
+
while (queue.length > 0) {
|
|
922
|
+
const frame = queue.pop();
|
|
923
|
+
const { visited, dfsStack } = frame;
|
|
924
|
+
let { preorder } = frame;
|
|
925
|
+
let branched = false;
|
|
926
|
+
while (dfsStack.length > 0) {
|
|
927
|
+
const top = dfsStack[dfsStack.length - 1];
|
|
928
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
929
|
+
if (unvisited.length === 0) {
|
|
930
|
+
dfsStack.pop();
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
for (const nextId of unvisited) {
|
|
934
|
+
const ni = idx.nodeById.get(nextId);
|
|
935
|
+
if (ni === void 0) continue;
|
|
936
|
+
const newVisited = new Set(visited);
|
|
937
|
+
newVisited.add(nextId);
|
|
938
|
+
queue.push({
|
|
939
|
+
visited: newVisited,
|
|
940
|
+
preorder: [...preorder, graph.nodes[ni]],
|
|
941
|
+
dfsStack: [...dfsStack, nextId]
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
branched = true;
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
if (!branched) yield preorder;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Lazily yields all possible postorder (DFS finish-order) sequences.
|
|
952
|
+
* Different neighbor exploration orders yield different sequences.
|
|
953
|
+
* Use `getPostorder()` for a single canonical ordering.
|
|
954
|
+
*/
|
|
955
|
+
function* genPostorders(graph, opts) {
|
|
956
|
+
const idx = getIndex(graph);
|
|
957
|
+
const startId = resolveFrom(graph, opts);
|
|
958
|
+
if (idx.nodeById.get(startId) === void 0) return;
|
|
959
|
+
const queue = [{
|
|
960
|
+
visited: new Set([startId]),
|
|
961
|
+
postorder: [],
|
|
962
|
+
dfsStack: [startId]
|
|
963
|
+
}];
|
|
964
|
+
while (queue.length > 0) {
|
|
965
|
+
const frame = queue.pop();
|
|
966
|
+
const { visited, dfsStack } = frame;
|
|
967
|
+
let { postorder } = frame;
|
|
968
|
+
let branched = false;
|
|
969
|
+
while (dfsStack.length > 0) {
|
|
970
|
+
const top = dfsStack[dfsStack.length - 1];
|
|
971
|
+
const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
|
|
972
|
+
if (unvisited.length === 0) {
|
|
973
|
+
dfsStack.pop();
|
|
974
|
+
const ni = idx.nodeById.get(top);
|
|
975
|
+
if (ni !== void 0) postorder = [...postorder, graph.nodes[ni]];
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
for (const nextId of unvisited) {
|
|
979
|
+
if (idx.nodeById.get(nextId) === void 0) continue;
|
|
980
|
+
const newVisited = new Set(visited);
|
|
981
|
+
newVisited.add(nextId);
|
|
982
|
+
queue.push({
|
|
983
|
+
visited: newVisited,
|
|
984
|
+
postorder: [...postorder],
|
|
985
|
+
dfsStack: [...dfsStack, nextId]
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
branched = true;
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
if (!branched) yield postorder;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Returns a minimum spanning tree of the graph.
|
|
996
|
+
* Only meaningful for connected undirected graphs (or the component reachable
|
|
997
|
+
* from an arbitrary start node in directed graphs).
|
|
998
|
+
*/
|
|
999
|
+
function getMinimumSpanningTree(graph, opts) {
|
|
1000
|
+
const algorithm = opts?.algorithm ?? "prim";
|
|
1001
|
+
const getWeight = opts?.getWeight ?? (() => 1);
|
|
1002
|
+
const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
|
|
1003
|
+
return createGraph({
|
|
1004
|
+
id: graph.id,
|
|
1005
|
+
type: graph.type,
|
|
1006
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
1007
|
+
nodes: graph.nodes.map((n) => ({
|
|
1008
|
+
id: n.id,
|
|
1009
|
+
parentId: n.parentId ?? void 0,
|
|
1010
|
+
initialNodeId: n.initialNodeId ?? void 0,
|
|
1011
|
+
label: n.label,
|
|
1012
|
+
data: n.data
|
|
1013
|
+
})),
|
|
1014
|
+
edges: mstEdges.map((e) => ({
|
|
1015
|
+
id: e.id,
|
|
1016
|
+
sourceId: e.sourceId,
|
|
1017
|
+
targetId: e.targetId,
|
|
1018
|
+
label: e.label,
|
|
1019
|
+
data: e.data
|
|
1020
|
+
}))
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
function primMST(graph, getWeight) {
|
|
1024
|
+
if (graph.nodes.length === 0) return [];
|
|
1025
|
+
const idx = getIndex(graph);
|
|
1026
|
+
const inMST = /* @__PURE__ */ new Set();
|
|
1027
|
+
const mstEdges = [];
|
|
1028
|
+
const candidates = [];
|
|
1029
|
+
function addEdgesOf(nodeId) {
|
|
1030
|
+
for (const eid of idx.outEdges.get(nodeId) ?? []) {
|
|
1031
|
+
const ai = idx.edgeById.get(eid);
|
|
1032
|
+
if (ai === void 0) continue;
|
|
1033
|
+
const e = graph.edges[ai];
|
|
1034
|
+
if (!inMST.has(e.targetId)) candidates.push({
|
|
1035
|
+
weight: getWeight(e),
|
|
1036
|
+
edge: e
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
|
|
1040
|
+
const ai = idx.edgeById.get(eid);
|
|
1041
|
+
if (ai === void 0) continue;
|
|
1042
|
+
const e = graph.edges[ai];
|
|
1043
|
+
if (!inMST.has(e.sourceId)) candidates.push({
|
|
1044
|
+
weight: getWeight(e),
|
|
1045
|
+
edge: e
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const startId = graph.nodes[0].id;
|
|
1050
|
+
inMST.add(startId);
|
|
1051
|
+
addEdgesOf(startId);
|
|
1052
|
+
while (candidates.length > 0 && inMST.size < graph.nodes.length) {
|
|
1053
|
+
let minIdx = 0;
|
|
1054
|
+
for (let i = 1; i < candidates.length; i++) if (candidates[i].weight < candidates[minIdx].weight) minIdx = i;
|
|
1055
|
+
const { edge } = candidates.splice(minIdx, 1)[0];
|
|
1056
|
+
const targetId = graph.type === "undirected" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
|
|
1057
|
+
if (inMST.has(targetId)) continue;
|
|
1058
|
+
inMST.add(targetId);
|
|
1059
|
+
mstEdges.push(edge);
|
|
1060
|
+
addEdgesOf(targetId);
|
|
1061
|
+
}
|
|
1062
|
+
return mstEdges;
|
|
1063
|
+
}
|
|
1064
|
+
function kruskalMST(graph, getWeight) {
|
|
1065
|
+
const sorted = [...graph.edges].sort((a, b) => getWeight(a) - getWeight(b));
|
|
1066
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1067
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const n of graph.nodes) {
|
|
1069
|
+
parent.set(n.id, n.id);
|
|
1070
|
+
rank.set(n.id, 0);
|
|
1071
|
+
}
|
|
1072
|
+
function find(x) {
|
|
1073
|
+
if (parent.get(x) !== x) parent.set(x, find(parent.get(x)));
|
|
1074
|
+
return parent.get(x);
|
|
1075
|
+
}
|
|
1076
|
+
function union(x, y) {
|
|
1077
|
+
const rx = find(x), ry = find(y);
|
|
1078
|
+
if (rx === ry) return false;
|
|
1079
|
+
if (rank.get(rx) < rank.get(ry)) parent.set(rx, ry);
|
|
1080
|
+
else if (rank.get(rx) > rank.get(ry)) parent.set(ry, rx);
|
|
1081
|
+
else {
|
|
1082
|
+
parent.set(ry, rx);
|
|
1083
|
+
rank.set(rx, rank.get(rx) + 1);
|
|
1084
|
+
}
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
const mstEdges = [];
|
|
1088
|
+
for (const e of sorted) if (union(e.sourceId, e.targetId)) mstEdges.push(e);
|
|
1089
|
+
return mstEdges;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Returns shortest paths between all pairs of nodes.
|
|
1093
|
+
* Algorithm 'dijkstra' (default): runs getShortestPaths per source node.
|
|
1094
|
+
* Algorithm 'floyd-warshall': classic O(V³) dynamic programming.
|
|
1095
|
+
*/
|
|
1096
|
+
function getAllPairsShortestPaths(graph, opts) {
|
|
1097
|
+
if ((opts?.algorithm ?? "dijkstra") === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
1098
|
+
return dijkstraAllPaths(graph, opts?.getWeight);
|
|
1099
|
+
}
|
|
1100
|
+
function dijkstraAllPaths(graph, getWeight) {
|
|
1101
|
+
const results = [];
|
|
1102
|
+
for (const node of graph.nodes) {
|
|
1103
|
+
const paths = getShortestPaths(graph, {
|
|
1104
|
+
from: node.id,
|
|
1105
|
+
getWeight
|
|
1106
|
+
});
|
|
1107
|
+
results.push(...paths);
|
|
1108
|
+
}
|
|
1109
|
+
return results;
|
|
1110
|
+
}
|
|
1111
|
+
function floydWarshallAllPaths(graph, getWeight) {
|
|
1112
|
+
const idx = getIndex(graph);
|
|
1113
|
+
const w = getWeight ?? (() => 1);
|
|
1114
|
+
const nodeIds = graph.nodes.map((n$1) => n$1.id);
|
|
1115
|
+
const n = nodeIds.length;
|
|
1116
|
+
const idxOf = /* @__PURE__ */ new Map();
|
|
1117
|
+
for (let i = 0; i < n; i++) idxOf.set(nodeIds[i], i);
|
|
1118
|
+
const INF = Infinity;
|
|
1119
|
+
const dist = Array.from({ length: n }, () => Array(n).fill(INF));
|
|
1120
|
+
const prev = Array.from({ length: n }, () => Array.from({ length: n }, () => []));
|
|
1121
|
+
for (let i = 0; i < n; i++) dist[i][i] = 0;
|
|
1122
|
+
for (const e of graph.edges) {
|
|
1123
|
+
const u = idxOf.get(e.sourceId);
|
|
1124
|
+
const v = idxOf.get(e.targetId);
|
|
1125
|
+
const weight = w(e);
|
|
1126
|
+
if (weight < dist[u][v]) {
|
|
1127
|
+
dist[u][v] = weight;
|
|
1128
|
+
prev[u][v] = [{
|
|
1129
|
+
from: u,
|
|
1130
|
+
edge: e
|
|
1131
|
+
}];
|
|
1132
|
+
} else if (weight === dist[u][v] && weight < INF) prev[u][v].push({
|
|
1133
|
+
from: u,
|
|
1134
|
+
edge: e
|
|
1135
|
+
});
|
|
1136
|
+
if (graph.type === "undirected") {
|
|
1137
|
+
if (weight < dist[v][u]) {
|
|
1138
|
+
dist[v][u] = weight;
|
|
1139
|
+
prev[v][u] = [{
|
|
1140
|
+
from: v,
|
|
1141
|
+
edge: e
|
|
1142
|
+
}];
|
|
1143
|
+
} else if (weight === dist[v][u] && weight < INF) prev[v][u].push({
|
|
1144
|
+
from: v,
|
|
1145
|
+
edge: e
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
for (let k = 0; k < n; k++) for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
|
|
1150
|
+
if (dist[i][k] === INF || dist[k][j] === INF) continue;
|
|
1151
|
+
const newDist = dist[i][k] + dist[k][j];
|
|
1152
|
+
if (newDist < dist[i][j]) {
|
|
1153
|
+
dist[i][j] = newDist;
|
|
1154
|
+
prev[i][j] = prev[k][j].map((p) => ({ ...p }));
|
|
1155
|
+
} else if (newDist === dist[i][j] && newDist < INF) {
|
|
1156
|
+
for (const p of prev[k][j]) if (!prev[i][j].some((x) => x.edge.id === p.edge.id)) prev[i][j].push({ ...p });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const results = [];
|
|
1160
|
+
for (let i = 0; i < n; i++) {
|
|
1161
|
+
const sourceNi = idx.nodeById.get(nodeIds[i]);
|
|
1162
|
+
if (sourceNi === void 0) continue;
|
|
1163
|
+
const sourceNode = graph.nodes[sourceNi];
|
|
1164
|
+
for (let j = 0; j < n; j++) {
|
|
1165
|
+
if (i === j || dist[i][j] === INF) continue;
|
|
1166
|
+
const paths = fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, i, j);
|
|
1167
|
+
results.push(...paths);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return results;
|
|
1171
|
+
}
|
|
1172
|
+
function fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, sourceIdx, targetIdx) {
|
|
1173
|
+
if (sourceIdx === targetIdx) return [{
|
|
1174
|
+
source: sourceNode,
|
|
1175
|
+
steps: []
|
|
1176
|
+
}];
|
|
1177
|
+
const preds = prev[sourceIdx][targetIdx];
|
|
1178
|
+
if (preds.length === 0) return [];
|
|
1179
|
+
const targetNi = getIndex(graph).nodeById.get(nodeIds[targetIdx]);
|
|
1180
|
+
if (targetNi === void 0) return [];
|
|
1181
|
+
const targetNode = graph.nodes[targetNi];
|
|
1182
|
+
const results = [];
|
|
1183
|
+
for (const { from, edge } of preds) {
|
|
1184
|
+
const prefixPaths = fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, sourceIdx, from);
|
|
1185
|
+
for (const prefix of prefixPaths) results.push({
|
|
1186
|
+
source: sourceNode,
|
|
1187
|
+
steps: [...prefix.steps, {
|
|
1188
|
+
edge,
|
|
1189
|
+
node: targetNode
|
|
1190
|
+
}]
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return results;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
//#endregion
|
|
1197
|
+
export { createGraph as A, updateEntities as B, isAcyclic as C, addEdge as D, GraphInstance as E, getEdge as F, getNode as I, hasEdge as L, deleteEdge as M, deleteEntities as N, addEntities as O, deleteNode as P, hasNode as R, hasPath as S, isTree as T, updateNode as V, getShortestPaths as _, genPreorders as a, getStronglyConnectedComponents as b, getAllPairsShortestPaths as c, getMinimumSpanningTree as d, getPostorder as f, getShortestPath as g, getPreorders as h, genPostorders as i, createVisualGraph as j, addNode as k, getConnectedComponents as l, getPreorder as m, dfs as n, genShortestPaths as o, getPostorders as p, genCycles as r, genSimplePaths as s, bfs as t, getCycles as u, getSimplePath as v, isConnected as w, getTopologicalSort as x, getSimplePaths as y, updateEdge as z };
|