Skip to content

Commit 6f6517a

Browse files
committed
feat: serializeGraph accepts an identity function to prevent data duplication
1 parent 342641f commit 6f6517a

File tree

3 files changed

+82
-17
lines changed

3 files changed

+82
-17
lines changed

src/types.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
export type EdgeWeight = number;
22

3-
export type Edge<Node = unknown, Props = unknown> = {
4-
source: Node;
5-
target: Node;
3+
export type Edge<NodeIdentity = unknown, Props = unknown> = {
4+
source: NodeIdentity;
5+
target: NodeIdentity;
66
weight?: EdgeWeight;
77
props: Props;
88
};
99

10-
export type Serialized<Node = unknown, LinkProps = unknown> = {
10+
export type Serialized<Node = unknown, LinkProps = unknown, NodeIdentity = Node> = {
1111
nodes: Node[];
12-
links: Edge<Node, LinkProps>[];
12+
links: Edge<NodeIdentity, LinkProps>[];
1313
};
1414

1515
export type SerializedInput<Node = unknown, LinkProps = unknown> = {

src/utils/serializeGraph.spec.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expectTypeOf, it } from 'vitest';
1+
import { describe, expect, expectTypeOf, it } from 'vitest';
22
import { Graph } from '../Graph.js';
33
import { checkSerialized } from '../test-utils.js';
44
import { Serialized } from '../types.js';
@@ -13,6 +13,44 @@ describe('serializeGraph', () => {
1313
checkSerialized(serialized);
1414
});
1515

16+
it('should use the node identity for link serialization', function () {
17+
const nodeA = { id: 1, title: 'a' };
18+
const nodeB = { id: 2, title: 'b' };
19+
20+
const graph = new Graph<{ id: number; title: string }, { type: string }>();
21+
graph.addEdge(nodeA, nodeB, { props: { type: 'foo' } });
22+
23+
const serialized = serializeGraph(graph, (n) => n.id);
24+
25+
expect(serialized).toStrictEqual({
26+
nodes: [nodeA, nodeB],
27+
links: [{ source: 1, target: 2, props: { type: 'foo' } }],
28+
});
29+
});
30+
31+
it('should reuse the same identity when the node is met multiple times', function () {
32+
const nodeA = { id: 1, title: 'a' };
33+
const nodeB = { id: 2, title: 'b' };
34+
const nodeC = { id: 3, title: 'c' };
35+
36+
const graph = new Graph<{ id: number; title: string }>();
37+
graph.addEdge(nodeA, nodeC);
38+
graph.addEdge(nodeB, nodeC);
39+
40+
// we use an object as identity
41+
const serialized = serializeGraph(graph, (n) => ({ id: n.id }));
42+
43+
const nodeIdentityC1 = serialized.links.find(
44+
(l) => l.source.id === nodeA.id && l.target.id === nodeC.id,
45+
)?.target;
46+
const nodeIdentityC2 = serialized.links.find(
47+
(l) => l.source.id === nodeB.id && l.target.id === nodeC.id,
48+
)?.target;
49+
50+
expect(nodeIdentityC1).toBeDefined();
51+
expect(nodeIdentityC1).toBe(nodeIdentityC2);
52+
});
53+
1654
it.skip('should return a serialized input with type inferred from the graph', function () {
1755
const nodeA = { title: 'a' };
1856
const nodeB = { title: 'b' };

src/utils/serializeGraph.ts

+38-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Serializes the graph.
33
*/
44
import { Graph } from '../Graph.js';
5-
import { Edge, Serialized } from '../types.js';
5+
import { NoInfer, Edge, Serialized } from '../types.js';
66

77
type SerializeGraphOptions<IncludeDefaultWeight extends boolean = false> = {
88
/**
@@ -17,30 +17,57 @@ type SerializeGraphOptions<IncludeDefaultWeight extends boolean = false> = {
1717

1818
/**
1919
* Serialize the graph data set : nodes, edges, edges weight & properties.
20-
* @param graph
21-
* @param opts
20+
*
21+
* Optionally, you can pass a function that returns a unique value for a given node.
22+
* When provided, the function will be used to avoid data duplication in the serialized object.
2223
*/
23-
export function serializeGraph<Node, LinkProps, IncludeDefaultWeight extends boolean>(
24+
export function serializeGraph<
25+
Node,
26+
LinkProps,
27+
IncludeDefaultWeight extends boolean,
28+
NodeIdentity = Node,
29+
>(
2430
graph: Graph<Node, LinkProps>,
25-
opts: SerializeGraphOptions<IncludeDefaultWeight> = {},
26-
): Serialized<Node, LinkProps> {
27-
const { includeDefaultWeight = false } = opts;
31+
...args:
32+
| [
33+
identityFn: (node: NoInfer<Node>) => NodeIdentity,
34+
SerializeGraphOptions<IncludeDefaultWeight>?,
35+
]
36+
| [SerializeGraphOptions<IncludeDefaultWeight>?]
37+
): Serialized<Node, LinkProps, NodeIdentity> {
38+
const identityFn = typeof args[0] === 'function' ? args[0] : undefined;
39+
const opts = typeof args[0] === 'function' ? args[1] : args[0];
2840

29-
const serialized: Serialized<Node, LinkProps> = {
41+
const { includeDefaultWeight = false } = opts ?? {};
42+
43+
const serialized: Serialized<Node, LinkProps, NodeIdentity> = {
3044
nodes: Array.from(graph.nodes),
3145
links: [],
3246
};
3347

48+
const nodeIdentityMap = new Map<Node, NodeIdentity>();
49+
3450
serialized.nodes.forEach((node) => {
3551
const source = node;
3652
graph.adjacent(source)?.forEach((target) => {
3753
const edgeWeight = graph.getEdgeWeight(source, target);
3854
const edgeProps = graph.getEdgeProperties(source, target);
3955

56+
if (identityFn && !nodeIdentityMap.has(source)) {
57+
nodeIdentityMap.set(source, identityFn(source));
58+
}
59+
60+
if (identityFn && !nodeIdentityMap.has(target)) {
61+
nodeIdentityMap.set(target, identityFn(target));
62+
}
63+
64+
const sourceIdentity = nodeIdentityMap.get(source) ?? source;
65+
const targetIdentity = nodeIdentityMap.get(target) ?? target;
66+
4067
const link = {
41-
source: source,
42-
target: target,
43-
} as Edge<Node, LinkProps>;
68+
source: sourceIdentity,
69+
target: targetIdentity,
70+
} as Edge<NodeIdentity, LinkProps>;
4471

4572
if (edgeWeight != 1 || includeDefaultWeight) {
4673
link.weight = edgeWeight;

0 commit comments

Comments
 (0)