@statelyai/graph 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -26
- package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-GeL1Cu-L.mjs} +3 -1
- package/dist/{algorithms-fTqmvhzP.d.mts → algorithms-CsGNehct.d.mts} +137 -2
- package/dist/{algorithms-Ba7o7niK.mjs → algorithms-DF1pSQGv.mjs} +1476 -343
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/{converter-udLITX36.mjs → converter-DyCJJfTe.mjs} +2 -2
- package/dist/format-support.mjs +38 -11
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -1
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +1 -1
- package/dist/formats/cytoscape/index.mjs +3 -1
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +26 -12
- package/dist/formats/d3/index.d.mts +1 -1
- package/dist/formats/d3/index.mjs +3 -1
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +22 -6
- package/dist/formats/edge-list/index.d.mts +1 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/elk/index.d.mts +1 -1
- package/dist/formats/elk/index.mjs +21 -14
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +22 -15
- package/dist/formats/gml/index.d.mts +1 -1
- package/dist/formats/gml/index.mjs +21 -12
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +73 -22
- package/dist/formats/jgf/index.d.mts +1 -1
- package/dist/formats/jgf/index.mjs +5 -2
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +49 -12
- package/dist/formats/tgf/index.d.mts +1 -1
- package/dist/formats/tgf/index.mjs +1 -1
- package/dist/formats/xyflow/index.d.mts +1 -1
- package/dist/formats/xyflow/index.mjs +31 -4
- package/dist/{index-D9Kj6Fe3.d.mts → index-D51lJnt2.d.mts} +1 -1
- package/dist/{index-CHoriXZD.d.mts → index-DWmo1mIp.d.mts} +77 -18
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +143 -295
- package/dist/{queries-BlkA1HAN.d.mts → queries-BfXeTXRf.d.mts} +43 -12
- package/dist/queries-KirMDR7e.mjs +980 -0
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -768
- package/dist/schemas.d.mts +1 -1
- package/dist/schemas.mjs +23 -84
- package/dist/{types-3-FS9NV2.d.mts → types-DNYdIU21.d.mts} +54 -5
- package/dist/validate-TtH-x3JV.mjs +190 -0
- package/package.json +13 -3
- package/dist/indexing-DR8M1vBy.mjs +0 -137
- /package/dist/{edge-list-DP4otyPU.mjs → edge-list-BcZ0h6zz.mjs} +0 -0
package/dist/schemas.d.mts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as getGraphIssues$1 } from "./validate-TtH-x3JV.mjs";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
|
|
3
4
|
//#region src/schemas.ts
|
|
@@ -116,37 +117,30 @@ function createIssue(code, message, path) {
|
|
|
116
117
|
path
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
/** Maps structural issue codes from `validate.ts` to this module's codes. */
|
|
121
|
+
const STRUCTURAL_CODE_MAP = {
|
|
122
|
+
"duplicate-node-id": "duplicate_node_id",
|
|
123
|
+
"duplicate-edge-id": "duplicate_edge_id",
|
|
124
|
+
"missing-initial-node": "missing_initial_node",
|
|
125
|
+
"missing-parent": "missing_parent",
|
|
126
|
+
"missing-node-initial": "missing_node_initial",
|
|
127
|
+
"duplicate-port-name": "duplicate_port_name",
|
|
128
|
+
"parent-cycle": "parent_cycle",
|
|
129
|
+
"missing-source-port": "missing_source_port",
|
|
130
|
+
"missing-target-port": "missing_target_port"
|
|
131
|
+
};
|
|
132
|
+
function toValidationIssue(issue) {
|
|
133
|
+
const path = issue.path ?? [];
|
|
134
|
+
let code = STRUCTURAL_CODE_MAP[issue.code] ?? issue.code;
|
|
135
|
+
if (issue.code === "dangling-edge-endpoint") code = path[path.length - 1] === "sourceId" ? "missing_source_node" : "missing_target_node";
|
|
136
|
+
return {
|
|
137
|
+
code,
|
|
138
|
+
message: issue.message,
|
|
139
|
+
path
|
|
140
|
+
};
|
|
130
141
|
}
|
|
131
142
|
function getGraphInvariantIssues(graph) {
|
|
132
|
-
const issues =
|
|
133
|
-
const nodeIndexes = /* @__PURE__ */ new Map();
|
|
134
|
-
const nodesById = /* @__PURE__ */ new Map();
|
|
135
|
-
for (const [id, indexes] of getDuplicateIndexes(graph.nodes, (node) => node.id)) for (const index of indexes) issues.push(createIssue("duplicate_node_id", `Duplicate node id "${id}"`, [
|
|
136
|
-
"nodes",
|
|
137
|
-
index,
|
|
138
|
-
"id"
|
|
139
|
-
]));
|
|
140
|
-
for (const [id, indexes] of getDuplicateIndexes(graph.edges, (edge) => edge.id)) for (const index of indexes) issues.push(createIssue("duplicate_edge_id", `Duplicate edge id "${id}"`, [
|
|
141
|
-
"edges",
|
|
142
|
-
index,
|
|
143
|
-
"id"
|
|
144
|
-
]));
|
|
145
|
-
graph.nodes.forEach((node, index) => {
|
|
146
|
-
nodeIndexes.set(node.id, index);
|
|
147
|
-
nodesById.set(node.id, node);
|
|
148
|
-
});
|
|
149
|
-
if (graph.initialNodeId && !nodeIndexes.has(graph.initialNodeId)) issues.push(createIssue("missing_initial_node", `Initial node "${graph.initialNodeId}" does not exist`, ["initialNodeId"]));
|
|
143
|
+
const issues = getGraphIssues$1(graph).map(toValidationIssue);
|
|
150
144
|
graph.nodes.forEach((node, index) => {
|
|
151
145
|
if (node.id === "") issues.push(createIssue("empty_node_id", "Node id must be a non-empty string", [
|
|
152
146
|
"nodes",
|
|
@@ -158,68 +152,13 @@ function getGraphInvariantIssues(graph) {
|
|
|
158
152
|
index,
|
|
159
153
|
"parentId"
|
|
160
154
|
]));
|
|
161
|
-
else if (node.parentId != null && !nodeIndexes.has(node.parentId)) issues.push(createIssue("missing_parent", `Parent node "${node.parentId}" does not exist`, [
|
|
162
|
-
"nodes",
|
|
163
|
-
index,
|
|
164
|
-
"parentId"
|
|
165
|
-
]));
|
|
166
|
-
if (node.initialNodeId && !nodeIndexes.has(node.initialNodeId)) issues.push(createIssue("missing_node_initial", `Initial node "${node.initialNodeId}" does not exist`, [
|
|
167
|
-
"nodes",
|
|
168
|
-
index,
|
|
169
|
-
"initialNodeId"
|
|
170
|
-
]));
|
|
171
|
-
for (const [name, indexes] of getDuplicateIndexes(node.ports ?? [], (port) => port.name)) for (const portIndex of indexes) issues.push(createIssue("duplicate_port_name", `Duplicate port name "${name}" on node "${node.id}"`, [
|
|
172
|
-
"nodes",
|
|
173
|
-
index,
|
|
174
|
-
"ports",
|
|
175
|
-
portIndex,
|
|
176
|
-
"name"
|
|
177
|
-
]));
|
|
178
155
|
});
|
|
179
|
-
for (const node of graph.nodes) {
|
|
180
|
-
const seen = /* @__PURE__ */ new Set();
|
|
181
|
-
let current = node.parentId;
|
|
182
|
-
while (current != null) {
|
|
183
|
-
if (current === node.id || seen.has(current)) {
|
|
184
|
-
issues.push(createIssue("parent_cycle", `Node "${node.id}" is part of a parent cycle`, [
|
|
185
|
-
"nodes",
|
|
186
|
-
nodeIndexes.get(node.id) ?? 0,
|
|
187
|
-
"parentId"
|
|
188
|
-
]));
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
seen.add(current);
|
|
192
|
-
current = nodesById.get(current)?.parentId;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
156
|
graph.edges.forEach((edge, index) => {
|
|
196
157
|
if (edge.id === "") issues.push(createIssue("empty_edge_id", "Edge id must be a non-empty string", [
|
|
197
158
|
"edges",
|
|
198
159
|
index,
|
|
199
160
|
"id"
|
|
200
161
|
]));
|
|
201
|
-
const source = nodesById.get(edge.sourceId);
|
|
202
|
-
const target = nodesById.get(edge.targetId);
|
|
203
|
-
if (!source) issues.push(createIssue("missing_source_node", `Source node "${edge.sourceId}" does not exist`, [
|
|
204
|
-
"edges",
|
|
205
|
-
index,
|
|
206
|
-
"sourceId"
|
|
207
|
-
]));
|
|
208
|
-
if (!target) issues.push(createIssue("missing_target_node", `Target node "${edge.targetId}" does not exist`, [
|
|
209
|
-
"edges",
|
|
210
|
-
index,
|
|
211
|
-
"targetId"
|
|
212
|
-
]));
|
|
213
|
-
if (source && edge.sourcePort !== void 0 && !source.ports?.some((port) => port.name === edge.sourcePort)) issues.push(createIssue("missing_source_port", `Port "${edge.sourcePort}" does not exist on source node "${edge.sourceId}"`, [
|
|
214
|
-
"edges",
|
|
215
|
-
index,
|
|
216
|
-
"sourcePort"
|
|
217
|
-
]));
|
|
218
|
-
if (target && edge.targetPort !== void 0 && !target.ports?.some((port) => port.name === edge.targetPort)) issues.push(createIssue("missing_target_port", `Port "${edge.targetPort}" does not exist on target node "${edge.targetId}"`, [
|
|
219
|
-
"edges",
|
|
220
|
-
index,
|
|
221
|
-
"targetPort"
|
|
222
|
-
]));
|
|
223
162
|
});
|
|
224
163
|
return issues;
|
|
225
164
|
}
|
|
@@ -177,15 +177,64 @@ interface VisualGraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any,
|
|
|
177
177
|
interface DeleteNodeOptions {
|
|
178
178
|
reparent?: boolean;
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Update payload for {@link updateNode}/`updateEntities`.
|
|
182
|
+
*
|
|
183
|
+
* Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
|
|
184
|
+
* `ports`) accept `null` to **unset** the field. `undefined` (or omitting the
|
|
185
|
+
* key) leaves the field unchanged. `null` is used for unsetting so update
|
|
186
|
+
* payloads stay JSON-serializable.
|
|
187
|
+
*/
|
|
188
|
+
interface NodeUpdate<TNodeData = any, TPortData = any> {
|
|
189
|
+
parentId?: string | null;
|
|
190
|
+
initialNodeId?: string | null;
|
|
191
|
+
label?: string | null;
|
|
192
|
+
data?: TNodeData;
|
|
193
|
+
/** New ports for the node, or `null` to remove all ports. */
|
|
194
|
+
ports?: PortConfig<TPortData>[] | null;
|
|
195
|
+
x?: number | null;
|
|
196
|
+
y?: number | null;
|
|
197
|
+
width?: number | null;
|
|
198
|
+
height?: number | null;
|
|
199
|
+
shape?: string | null;
|
|
200
|
+
color?: string | null;
|
|
201
|
+
style?: Record<string, string | number | boolean> | null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Update payload for {@link updateEdge}/`updateEntities`.
|
|
205
|
+
*
|
|
206
|
+
* Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
|
|
207
|
+
* `width`, `height`, `color`, `style`) accept `null` to **unset** the field.
|
|
208
|
+
* `undefined` (or omitting the key) leaves the field unchanged. `null` is
|
|
209
|
+
* used for unsetting so update payloads stay JSON-serializable.
|
|
210
|
+
*/
|
|
211
|
+
interface EdgeUpdate<TEdgeData = any> {
|
|
212
|
+
sourceId?: string;
|
|
213
|
+
targetId?: string;
|
|
214
|
+
label?: string | null;
|
|
215
|
+
data?: TEdgeData;
|
|
216
|
+
weight?: number | null;
|
|
217
|
+
mode?: GraphMode | null;
|
|
218
|
+
/** Port name on the source node, or `null` to clear the port reference. */
|
|
219
|
+
sourcePort?: string | null;
|
|
220
|
+
/** Port name on the target node, or `null` to clear the port reference. */
|
|
221
|
+
targetPort?: string | null;
|
|
222
|
+
x?: number | null;
|
|
223
|
+
y?: number | null;
|
|
224
|
+
width?: number | null;
|
|
225
|
+
height?: number | null;
|
|
226
|
+
color?: string | null;
|
|
227
|
+
style?: Record<string, string | number | boolean> | null;
|
|
228
|
+
}
|
|
180
229
|
interface EntitiesConfig<TNodeData = any, TEdgeData = any, TPortData = any> {
|
|
181
230
|
nodes?: NodeConfig<TNodeData, TPortData>[];
|
|
182
231
|
edges?: EdgeConfig<TEdgeData>[];
|
|
183
232
|
}
|
|
184
233
|
interface EntitiesUpdate<TNodeData = any, TEdgeData = any, TPortData = any> {
|
|
185
|
-
nodes?: (
|
|
234
|
+
nodes?: (NodeUpdate<TNodeData, TPortData> & {
|
|
186
235
|
id: string;
|
|
187
236
|
})[];
|
|
188
|
-
edges?: (
|
|
237
|
+
edges?: (EdgeUpdate<TEdgeData> & {
|
|
189
238
|
id: string;
|
|
190
239
|
})[];
|
|
191
240
|
}
|
|
@@ -281,7 +330,7 @@ type GraphPatch<TNodeData = any, TEdgeData = any> = {
|
|
|
281
330
|
} | {
|
|
282
331
|
op: 'updateNode';
|
|
283
332
|
id: string;
|
|
284
|
-
data:
|
|
333
|
+
data: NodeUpdate<TNodeData>;
|
|
285
334
|
description?: string;
|
|
286
335
|
} | {
|
|
287
336
|
op: 'deleteNode';
|
|
@@ -294,7 +343,7 @@ type GraphPatch<TNodeData = any, TEdgeData = any> = {
|
|
|
294
343
|
} | {
|
|
295
344
|
op: 'updateEdge';
|
|
296
345
|
id: string;
|
|
297
|
-
data:
|
|
346
|
+
data: EdgeUpdate<TEdgeData>;
|
|
298
347
|
description?: string;
|
|
299
348
|
} | {
|
|
300
349
|
op: 'deleteEdge';
|
|
@@ -375,4 +424,4 @@ interface TransitionOptions<TState, TEvent> {
|
|
|
375
424
|
id?: string;
|
|
376
425
|
}
|
|
377
426
|
//#endregion
|
|
378
|
-
export {
|
|
427
|
+
export { SinglePathOptions as A, WalkContext as B, MSTOptions as C, PathOptions as D, NodeUpdate as E, VisualGraphConfig as F, WeightedWalkOptions as H, VisualGraphEntity as I, VisualGraphFormatConverter as L, TraversalOptions as M, VisualEdge as N, PortConfig as O, VisualGraph as P, VisualNode as R, GraphStep as S, NodeConfig as T, WalkOptions as V, GraphMode as _, EdgeChange as a, GraphPath as b, EntitiesConfig as c, Graph as d, GraphConfig as f, GraphFormatConverter as g, GraphEntity as h, DeleteNodeOptions as i, TransitionOptions as j, PortDirection as k, EntitiesUpdate as l, GraphEdge as m, AllPairsShortestPathsOptions as n, EdgeConfig as o, GraphDiff as p, CoverageStats as r, EdgeUpdate as s, AStarOptions as t, EntityRect as u, GraphNode as v, NodeChange as w, GraphPort as x, GraphPatch as y, VisualPort as z };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//#region src/validate.ts
|
|
2
|
+
function getDuplicateIndexes(items, getKey) {
|
|
3
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
4
|
+
items.forEach((item, index) => {
|
|
5
|
+
const key = getKey(item);
|
|
6
|
+
if (key == null) return;
|
|
7
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
8
|
+
indexes.push(index);
|
|
9
|
+
indexesByKey.set(key, indexes);
|
|
10
|
+
});
|
|
11
|
+
for (const [key, indexes] of indexesByKey) if (indexes.length < 2) indexesByKey.delete(key);
|
|
12
|
+
return indexesByKey;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the parent cycles in a graph's hierarchy, each cycle reported once
|
|
16
|
+
* as the list of node ids forming the cycle (cycle members only — nodes whose
|
|
17
|
+
* ancestry merely *leads into* a cycle are not included).
|
|
18
|
+
*/
|
|
19
|
+
function getParentCycles(nodesById) {
|
|
20
|
+
const cycles = [];
|
|
21
|
+
const visited = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const startId of nodesById.keys()) {
|
|
23
|
+
if (visited.has(startId)) continue;
|
|
24
|
+
const path = [];
|
|
25
|
+
const positions = /* @__PURE__ */ new Map();
|
|
26
|
+
let currentId = startId;
|
|
27
|
+
while (currentId != null && !visited.has(currentId) && nodesById.has(currentId)) {
|
|
28
|
+
const position = positions.get(currentId);
|
|
29
|
+
if (position !== void 0) {
|
|
30
|
+
cycles.push(path.slice(position));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
positions.set(currentId, path.length);
|
|
34
|
+
path.push(currentId);
|
|
35
|
+
currentId = nodesById.get(currentId).parentId;
|
|
36
|
+
}
|
|
37
|
+
for (const id of path) visited.add(id);
|
|
38
|
+
}
|
|
39
|
+
return cycles;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates the structural invariants of a graph and returns the issues
|
|
43
|
+
* found, or `[]` when the graph is valid. Pure — never throws, never mutates.
|
|
44
|
+
*
|
|
45
|
+
* This is the recommended gate for untrusted or imported graphs (e.g. parsed
|
|
46
|
+
* from a file or received over the wire) before handing them to queries and
|
|
47
|
+
* algorithms: the mutation APIs (`addNode`, `addEdge`, `updateNode`, …)
|
|
48
|
+
* validate incrementally, but `createGraph` does **not** — it accepts
|
|
49
|
+
* dangling `parentId`/edge references and even `parentId` cycles as-is.
|
|
50
|
+
*
|
|
51
|
+
* For Zod-based shape validation of arbitrary unknown values, see
|
|
52
|
+
* `validateGraph` in `@statelyai/graph/schemas` (which reuses these checks).
|
|
53
|
+
*
|
|
54
|
+
* Issue codes: `duplicate-node-id`, `duplicate-edge-id`,
|
|
55
|
+
* `missing-initial-node`, `missing-parent`, `missing-node-initial`,
|
|
56
|
+
* `duplicate-port-name`, `parent-cycle`, `dangling-edge-endpoint`,
|
|
57
|
+
* `missing-source-port`, `missing-target-port`.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const graph = createGraph({
|
|
62
|
+
* nodes: [{ id: 'a', parentId: 'ghost' }],
|
|
63
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
64
|
+
* });
|
|
65
|
+
* getGraphIssues(graph);
|
|
66
|
+
* // => [
|
|
67
|
+
* // { code: 'missing-parent', message: '...', path: ['nodes', 0, 'parentId'] },
|
|
68
|
+
* // { code: 'dangling-edge-endpoint', message: '...', path: ['edges', 0, 'targetId'] },
|
|
69
|
+
* // ]
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
function getGraphIssues(graph) {
|
|
73
|
+
const issues = [];
|
|
74
|
+
const nodeIndexes = /* @__PURE__ */ new Map();
|
|
75
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
76
|
+
graph.nodes.forEach((node, index) => {
|
|
77
|
+
nodeIndexes.set(node.id, index);
|
|
78
|
+
nodesById.set(node.id, node);
|
|
79
|
+
});
|
|
80
|
+
for (const [id, indexes] of getDuplicateIndexes(graph.nodes, (node) => node.id)) for (const index of indexes) issues.push({
|
|
81
|
+
code: "duplicate-node-id",
|
|
82
|
+
message: `Duplicate node id "${id}". Node ids must be unique; rename or remove the duplicates.`,
|
|
83
|
+
path: [
|
|
84
|
+
"nodes",
|
|
85
|
+
index,
|
|
86
|
+
"id"
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
for (const [id, indexes] of getDuplicateIndexes(graph.edges, (edge) => edge.id)) for (const index of indexes) issues.push({
|
|
90
|
+
code: "duplicate-edge-id",
|
|
91
|
+
message: `Duplicate edge id "${id}". Edge ids must be unique; rename or remove the duplicates.`,
|
|
92
|
+
path: [
|
|
93
|
+
"edges",
|
|
94
|
+
index,
|
|
95
|
+
"id"
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
if (graph.initialNodeId && !nodesById.has(graph.initialNodeId)) issues.push({
|
|
99
|
+
code: "missing-initial-node",
|
|
100
|
+
message: `Graph initialNodeId references missing node "${graph.initialNodeId}". Add that node or update the graph's initialNodeId.`,
|
|
101
|
+
path: ["initialNodeId"]
|
|
102
|
+
});
|
|
103
|
+
graph.nodes.forEach((node, index) => {
|
|
104
|
+
if (node.parentId != null && !nodesById.has(node.parentId)) issues.push({
|
|
105
|
+
code: "missing-parent",
|
|
106
|
+
message: `Node "${node.id}" has parentId "${node.parentId}", which does not exist. Add that node or remove the parentId.`,
|
|
107
|
+
path: [
|
|
108
|
+
"nodes",
|
|
109
|
+
index,
|
|
110
|
+
"parentId"
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
if (node.initialNodeId && !nodesById.has(node.initialNodeId)) issues.push({
|
|
114
|
+
code: "missing-node-initial",
|
|
115
|
+
message: `Node "${node.id}" has initialNodeId "${node.initialNodeId}", which does not exist. Add that node or remove the initialNodeId.`,
|
|
116
|
+
path: [
|
|
117
|
+
"nodes",
|
|
118
|
+
index,
|
|
119
|
+
"initialNodeId"
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
for (const [name, portIndexes] of getDuplicateIndexes(node.ports ?? [], (port) => port.name)) for (const portIndex of portIndexes) issues.push({
|
|
123
|
+
code: "duplicate-port-name",
|
|
124
|
+
message: `Duplicate port name "${name}" on node "${node.id}". Port names must be unique per node; rename or remove the duplicates.`,
|
|
125
|
+
path: [
|
|
126
|
+
"nodes",
|
|
127
|
+
index,
|
|
128
|
+
"ports",
|
|
129
|
+
portIndex,
|
|
130
|
+
"name"
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
for (const cycle of getParentCycles(nodesById)) {
|
|
135
|
+
const chain = [...cycle, cycle[0]].join(" → ");
|
|
136
|
+
issues.push({
|
|
137
|
+
code: "parent-cycle",
|
|
138
|
+
message: `Parent cycle detected: ${chain}. Break the cycle by changing the parentId of one of these nodes.`,
|
|
139
|
+
path: [
|
|
140
|
+
"nodes",
|
|
141
|
+
nodeIndexes.get(cycle[0]) ?? 0,
|
|
142
|
+
"parentId"
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
graph.edges.forEach((edge, index) => {
|
|
147
|
+
const source = nodesById.get(edge.sourceId);
|
|
148
|
+
const target = nodesById.get(edge.targetId);
|
|
149
|
+
if (!source) issues.push({
|
|
150
|
+
code: "dangling-edge-endpoint",
|
|
151
|
+
message: `Edge "${edge.id}" has sourceId "${edge.sourceId}", which references a missing node. Add that node or fix the edge's sourceId.`,
|
|
152
|
+
path: [
|
|
153
|
+
"edges",
|
|
154
|
+
index,
|
|
155
|
+
"sourceId"
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
if (!target) issues.push({
|
|
159
|
+
code: "dangling-edge-endpoint",
|
|
160
|
+
message: `Edge "${edge.id}" has targetId "${edge.targetId}", which references a missing node. Add that node or fix the edge's targetId.`,
|
|
161
|
+
path: [
|
|
162
|
+
"edges",
|
|
163
|
+
index,
|
|
164
|
+
"targetId"
|
|
165
|
+
]
|
|
166
|
+
});
|
|
167
|
+
if (source && edge.sourcePort !== void 0 && !source.ports?.some((port) => port.name === edge.sourcePort)) issues.push({
|
|
168
|
+
code: "missing-source-port",
|
|
169
|
+
message: `Edge "${edge.id}" has sourcePort "${edge.sourcePort}", but source node "${edge.sourceId}" has no port with that name. Add the port or fix the edge's sourcePort.`,
|
|
170
|
+
path: [
|
|
171
|
+
"edges",
|
|
172
|
+
index,
|
|
173
|
+
"sourcePort"
|
|
174
|
+
]
|
|
175
|
+
});
|
|
176
|
+
if (target && edge.targetPort !== void 0 && !target.ports?.some((port) => port.name === edge.targetPort)) issues.push({
|
|
177
|
+
code: "missing-target-port",
|
|
178
|
+
message: `Edge "${edge.id}" has targetPort "${edge.targetPort}", but target node "${edge.targetId}" has no port with that name. Add the port or fix the edge's targetPort.`,
|
|
179
|
+
path: [
|
|
180
|
+
"edges",
|
|
181
|
+
index,
|
|
182
|
+
"targetPort"
|
|
183
|
+
]
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
return issues;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
//#endregion
|
|
190
|
+
export { getGraphIssues as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/graph",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"description": "A TypeScript-first graph library with plain JSON-serializable objects",
|
|
6
6
|
"author": "David Khourshid <david@stately.ai>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -52,12 +52,13 @@
|
|
|
52
52
|
"schemas"
|
|
53
53
|
],
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"publint": "^0.3.15",
|
|
56
55
|
"@changesets/changelog-github": "^0.5.2",
|
|
57
56
|
"@changesets/cli": "^2.29.8",
|
|
57
|
+
"@dagrejs/graphlib": "^4.0.1",
|
|
58
58
|
"@types/d3-array": "^3.2.2",
|
|
59
59
|
"@types/d3-force": "^3.0.10",
|
|
60
60
|
"@types/node": "^25.0.3",
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
61
62
|
"@xyflow/system": "^0.0.75",
|
|
62
63
|
"bumpp": "^10.3.2",
|
|
63
64
|
"cytoscape": "^3.33.1",
|
|
@@ -66,6 +67,14 @@
|
|
|
66
67
|
"dotparser": "^1.1.1",
|
|
67
68
|
"elkjs": "^0.11.1",
|
|
68
69
|
"fast-xml-parser": "^5.3.4",
|
|
70
|
+
"graphology": "^0.26.0",
|
|
71
|
+
"graphology-components": "^1.5.4",
|
|
72
|
+
"graphology-metrics": "^2.4.0",
|
|
73
|
+
"graphology-shortest-path": "^2.1.0",
|
|
74
|
+
"graphology-types": "^0.24.8",
|
|
75
|
+
"ngraph.graph": "^20.1.2",
|
|
76
|
+
"ngraph.path": "^1.6.1",
|
|
77
|
+
"publint": "^0.3.15",
|
|
69
78
|
"tsdown": "^0.18.1",
|
|
70
79
|
"tsx": "^4.21.0",
|
|
71
80
|
"typescript": "^5.9.3",
|
|
@@ -114,6 +123,7 @@
|
|
|
114
123
|
"generate-schema": "tsx scripts/generate-json-schema.ts",
|
|
115
124
|
"changeset": "changeset",
|
|
116
125
|
"version": "changeset version",
|
|
117
|
-
"release": "pnpm run build && changeset publish"
|
|
126
|
+
"release": "pnpm run build && changeset publish",
|
|
127
|
+
"bench:compare": "tsx bench/compare/run.ts"
|
|
118
128
|
}
|
|
119
129
|
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
//#region src/indexing.ts
|
|
2
|
-
const indexes = /* @__PURE__ */ new WeakMap();
|
|
3
|
-
/**
|
|
4
|
-
* Get or lazily build the index for a graph.
|
|
5
|
-
* Auto-rebuilds when node/edge count changes.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { createGraph, getIndex } from '@statelyai/graph';
|
|
10
|
-
*
|
|
11
|
-
* const graph = createGraph({
|
|
12
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
13
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
14
|
-
* });
|
|
15
|
-
*
|
|
16
|
-
* const idx = getIndex(graph);
|
|
17
|
-
* idx.nodeById.get('a'); // 0
|
|
18
|
-
* idx.outEdges.get('a'); // ['e1']
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
function getIndex(graph) {
|
|
22
|
-
let idx = indexes.get(graph);
|
|
23
|
-
const sameStructure = idx && idx.nodeCount === graph.nodes.length && idx.edgeCount === graph.edges.length;
|
|
24
|
-
const signature = idx !== void 0 && sameStructure && idx.nodesRef === graph.nodes && idx.edgesRef === graph.edges ? getIndexSignature(graph) : void 0;
|
|
25
|
-
if (!idx || idx.nodeCount !== graph.nodes.length || idx.edgeCount !== graph.edges.length || signature !== void 0 && idx.signature !== signature) {
|
|
26
|
-
idx = buildIndex(graph, signature ?? getIndexSignature(graph));
|
|
27
|
-
indexes.set(graph, idx);
|
|
28
|
-
}
|
|
29
|
-
return idx;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Clear the cached index. Call this if you mutate graph.nodes/edges directly.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```ts
|
|
36
|
-
* import { createGraph, invalidateIndex, getIndex } from '@statelyai/graph';
|
|
37
|
-
*
|
|
38
|
-
* const graph = createGraph({ nodes: [{ id: 'a' }], edges: [] });
|
|
39
|
-
* // manually mutate nodes array
|
|
40
|
-
* graph.nodes.push({ type: 'node', id: 'b', parentId: null, initialNodeId: null, label: '', data: undefined });
|
|
41
|
-
* invalidateIndex(graph); // forces rebuild on next getIndex()
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
function invalidateIndex(graph) {
|
|
45
|
-
indexes.delete(graph);
|
|
46
|
-
}
|
|
47
|
-
function getIndexSignature(graph) {
|
|
48
|
-
const nodeParts = graph.nodes.map((node) => `${node.id}\u0000${node.parentId ?? ""}`);
|
|
49
|
-
const edgeParts = graph.edges.map((edge) => `${edge.id}\u0000${edge.sourceId}\u0000${edge.targetId}`);
|
|
50
|
-
return `${nodeParts.join("")}\u0002${edgeParts.join("")}`;
|
|
51
|
-
}
|
|
52
|
-
function buildIndex(graph, signature) {
|
|
53
|
-
const nodeById = /* @__PURE__ */ new Map();
|
|
54
|
-
const edgeById = /* @__PURE__ */ new Map();
|
|
55
|
-
const outEdges = /* @__PURE__ */ new Map();
|
|
56
|
-
const inEdges = /* @__PURE__ */ new Map();
|
|
57
|
-
const childNodes = /* @__PURE__ */ new Map();
|
|
58
|
-
for (let i = 0; i < graph.nodes.length; i++) {
|
|
59
|
-
const n = graph.nodes[i];
|
|
60
|
-
nodeById.set(n.id, i);
|
|
61
|
-
outEdges.set(n.id, []);
|
|
62
|
-
inEdges.set(n.id, []);
|
|
63
|
-
const parent = n.parentId ?? null;
|
|
64
|
-
if (!childNodes.has(parent)) childNodes.set(parent, []);
|
|
65
|
-
childNodes.get(parent).push(n.id);
|
|
66
|
-
}
|
|
67
|
-
for (let i = 0; i < graph.edges.length; i++) {
|
|
68
|
-
const e = graph.edges[i];
|
|
69
|
-
edgeById.set(e.id, i);
|
|
70
|
-
outEdges.get(e.sourceId)?.push(e.id);
|
|
71
|
-
inEdges.get(e.targetId)?.push(e.id);
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
nodeById,
|
|
75
|
-
edgeById,
|
|
76
|
-
outEdges,
|
|
77
|
-
inEdges,
|
|
78
|
-
childNodes,
|
|
79
|
-
nodeCount: graph.nodes.length,
|
|
80
|
-
edgeCount: graph.edges.length,
|
|
81
|
-
signature,
|
|
82
|
-
nodesRef: graph.nodes,
|
|
83
|
-
edgesRef: graph.edges
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function indexAddNode(idx, node, arrayIndex) {
|
|
87
|
-
idx.nodeById.set(node.id, arrayIndex);
|
|
88
|
-
idx.outEdges.set(node.id, []);
|
|
89
|
-
idx.inEdges.set(node.id, []);
|
|
90
|
-
const parent = node.parentId ?? null;
|
|
91
|
-
if (!idx.childNodes.has(parent)) idx.childNodes.set(parent, []);
|
|
92
|
-
idx.childNodes.get(parent).push(node.id);
|
|
93
|
-
idx.nodeCount++;
|
|
94
|
-
idx.signature = "";
|
|
95
|
-
}
|
|
96
|
-
function indexAddEdge(idx, edge, arrayIndex) {
|
|
97
|
-
idx.edgeById.set(edge.id, arrayIndex);
|
|
98
|
-
idx.outEdges.get(edge.sourceId)?.push(edge.id);
|
|
99
|
-
idx.inEdges.get(edge.targetId)?.push(edge.id);
|
|
100
|
-
idx.edgeCount++;
|
|
101
|
-
idx.signature = "";
|
|
102
|
-
}
|
|
103
|
-
/** Update childNodes index when a node's parentId changes. */
|
|
104
|
-
function indexReparentNode(idx, nodeId, oldParentId, newParentId) {
|
|
105
|
-
const oldSiblings = idx.childNodes.get(oldParentId ?? null);
|
|
106
|
-
if (oldSiblings) {
|
|
107
|
-
const pos = oldSiblings.indexOf(nodeId);
|
|
108
|
-
if (pos !== -1) oldSiblings.splice(pos, 1);
|
|
109
|
-
}
|
|
110
|
-
const np = newParentId ?? null;
|
|
111
|
-
if (!idx.childNodes.has(np)) idx.childNodes.set(np, []);
|
|
112
|
-
idx.childNodes.get(np).push(nodeId);
|
|
113
|
-
idx.signature = "";
|
|
114
|
-
}
|
|
115
|
-
/** Update adjacency lists when an edge's sourceId/targetId changes. */
|
|
116
|
-
function indexUpdateEdgeEndpoints(idx, edgeId, oldSourceId, oldTargetId, newSourceId, newTargetId) {
|
|
117
|
-
if (oldSourceId !== newSourceId) {
|
|
118
|
-
const oldOut = idx.outEdges.get(oldSourceId);
|
|
119
|
-
if (oldOut) {
|
|
120
|
-
const pos = oldOut.indexOf(edgeId);
|
|
121
|
-
if (pos !== -1) oldOut.splice(pos, 1);
|
|
122
|
-
}
|
|
123
|
-
idx.outEdges.get(newSourceId)?.push(edgeId);
|
|
124
|
-
}
|
|
125
|
-
if (oldTargetId !== newTargetId) {
|
|
126
|
-
const oldIn = idx.inEdges.get(oldTargetId);
|
|
127
|
-
if (oldIn) {
|
|
128
|
-
const pos = oldIn.indexOf(edgeId);
|
|
129
|
-
if (pos !== -1) oldIn.splice(pos, 1);
|
|
130
|
-
}
|
|
131
|
-
idx.inEdges.get(newTargetId)?.push(edgeId);
|
|
132
|
-
}
|
|
133
|
-
idx.signature = "";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
//#endregion
|
|
137
|
-
export { indexUpdateEdgeEndpoints as a, indexReparentNode as i, indexAddEdge as n, invalidateIndex as o, indexAddNode as r, getIndex as t };
|
|
File without changes
|