@statelyai/graph 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +55 -26
  2. package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-GeL1Cu-L.mjs} +3 -1
  3. package/dist/{algorithms-fTqmvhzP.d.mts → algorithms-CsGNehct.d.mts} +137 -2
  4. package/dist/{algorithms-Ba7o7niK.mjs → algorithms-DF1pSQGv.mjs} +1476 -343
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/{converter-udLITX36.mjs → converter-DyCJJfTe.mjs} +2 -2
  8. package/dist/format-support.mjs +38 -11
  9. package/dist/formats/adjacency-list/index.d.mts +1 -1
  10. package/dist/formats/adjacency-list/index.mjs +1 -1
  11. package/dist/formats/converter/index.d.mts +1 -1
  12. package/dist/formats/converter/index.mjs +1 -1
  13. package/dist/formats/cytoscape/index.d.mts +1 -1
  14. package/dist/formats/cytoscape/index.mjs +3 -1
  15. package/dist/formats/d2/index.d.mts +1 -1
  16. package/dist/formats/d2/index.mjs +26 -12
  17. package/dist/formats/d3/index.d.mts +1 -1
  18. package/dist/formats/d3/index.mjs +3 -1
  19. package/dist/formats/dot/index.d.mts +1 -1
  20. package/dist/formats/dot/index.mjs +22 -6
  21. package/dist/formats/edge-list/index.d.mts +1 -1
  22. package/dist/formats/edge-list/index.mjs +1 -1
  23. package/dist/formats/elk/index.d.mts +1 -1
  24. package/dist/formats/elk/index.mjs +21 -14
  25. package/dist/formats/gexf/index.d.mts +1 -1
  26. package/dist/formats/gexf/index.mjs +22 -15
  27. package/dist/formats/gml/index.d.mts +1 -1
  28. package/dist/formats/gml/index.mjs +21 -12
  29. package/dist/formats/graphml/index.d.mts +1 -1
  30. package/dist/formats/graphml/index.mjs +73 -22
  31. package/dist/formats/jgf/index.d.mts +1 -1
  32. package/dist/formats/jgf/index.mjs +5 -2
  33. package/dist/formats/mermaid/index.d.mts +1 -1
  34. package/dist/formats/mermaid/index.mjs +49 -12
  35. package/dist/formats/tgf/index.d.mts +1 -1
  36. package/dist/formats/tgf/index.mjs +1 -1
  37. package/dist/formats/xyflow/index.d.mts +1 -1
  38. package/dist/formats/xyflow/index.mjs +31 -4
  39. package/dist/{index-D9Kj6Fe3.d.mts → index-D51lJnt2.d.mts} +1 -1
  40. package/dist/{index-CHoriXZD.d.mts → index-DWmo1mIp.d.mts} +77 -18
  41. package/dist/index.d.mts +6 -6
  42. package/dist/index.mjs +143 -295
  43. package/dist/{queries-BlkA1HAN.d.mts → queries-BfXeTXRf.d.mts} +43 -12
  44. package/dist/queries-KirMDR7e.mjs +980 -0
  45. package/dist/queries.d.mts +1 -1
  46. package/dist/queries.mjs +1 -768
  47. package/dist/schemas.d.mts +1 -1
  48. package/dist/schemas.mjs +23 -84
  49. package/dist/{types-3-FS9NV2.d.mts → types-DNYdIU21.d.mts} +54 -5
  50. package/dist/validate-TtH-x3JV.mjs +190 -0
  51. package/package.json +13 -3
  52. package/dist/indexing-DR8M1vBy.mjs +0 -137
  53. /package/dist/{edge-list-DP4otyPU.mjs → edge-list-BcZ0h6zz.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { _ as GraphNode, b as GraphPort, p as GraphEdge, u as Graph } from "./types-3-FS9NV2.mjs";
1
+ import { d as Graph, m as GraphEdge, v as GraphNode, x as GraphPort } from "./types-DNYdIU21.mjs";
2
2
  import * as z from "zod";
3
3
 
4
4
  //#region src/schemas.d.ts
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
- function getDuplicateIndexes(items, getKey) {
120
- const indexesByKey = /* @__PURE__ */ new Map();
121
- items.forEach((item, index) => {
122
- const key = getKey(item);
123
- if (key == null) return;
124
- const indexes = indexesByKey.get(key) ?? [];
125
- indexes.push(index);
126
- indexesByKey.set(key, indexes);
127
- });
128
- for (const [key, indexes] of indexesByKey) if (indexes.length < 2) indexesByKey.delete(key);
129
- return indexesByKey;
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?: (Partial<Omit<NodeConfig<TNodeData, TPortData>, 'id'>> & {
234
+ nodes?: (NodeUpdate<TNodeData, TPortData> & {
186
235
  id: string;
187
236
  })[];
188
- edges?: (Partial<Omit<EdgeConfig<TEdgeData>, 'id'>> & {
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: Partial<Omit<NodeConfig<TNodeData>, 'id'>>;
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: Partial<Omit<EdgeConfig<TEdgeData>, 'id'>>;
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 { TraversalOptions as A, WeightedWalkOptions as B, NodeChange as C, PortDirection as D, PortConfig as E, VisualGraphFormatConverter as F, VisualNode as I, VisualPort as L, VisualGraph as M, VisualGraphConfig as N, SinglePathOptions as O, VisualGraphEntity as P, WalkContext as R, MSTOptions as S, PathOptions as T, GraphNode as _, EdgeChange as a, GraphPort as b, EntitiesUpdate as c, GraphConfig as d, GraphDiff as f, GraphMode as g, GraphFormatConverter as h, DeleteNodeOptions as i, VisualEdge as j, TransitionOptions as k, EntityRect as l, GraphEntity as m, AllPairsShortestPathsOptions as n, EdgeConfig as o, GraphEdge as p, CoverageStats as r, EntitiesConfig as s, AStarOptions as t, Graph as u, GraphPatch as v, NodeConfig as w, GraphStep as x, GraphPath as y, WalkOptions as z };
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": "1.0.0",
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 };