Skip to content

Commit

Permalink
add Graph.contract for collapsing nodes together
Browse files Browse the repository at this point in the history
This commit adds Graph.contract, which allows collapsing certain
nodes in the graph into each other. This will enable the creation of a
SourceCred "identity" plugin, allowing identity resolution between users
different accounts on different services.

Test plan: Thorough unit tests have been added. `yarn test` passes.

Thanks to @wchargin for [review feedback][1] which significantly
improved this API.

[1]: #1380 (comment)
  • Loading branch information
teamdandelion committed Sep 17, 2019
1 parent c58315f commit a93c5d3
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 0 deletions.
54 changes: 54 additions & 0 deletions src/core/graph.js
Expand Up @@ -889,6 +889,60 @@ export class Graph {
return result;
}

/**
* Create a new graph, in which some nodes have been contracted together.
*
* contract takes a list of Contractions, each of which specifies a
* replacement node, and a list of old node addresses to map onto the new
* node. A new graph will be returned where the new node is added, none of
* the old nodes are present, and every edge incident to one of the old nodes
* has been re-written so that it is incident to the new node instead.
*
* If the same node addresses is "old" for several contractions, all incident
* edges will be re-written to connect to whichever contraction came last.
* The original Graph is not mutated.
*
* contract runs in O(n+e+k), where `n` is the number of nodes, `e` is the
* number of edges, and `k` is the number of contractions. If needed, we can
* improve the peformance by mutating the original graph instead of creating
* a new one.
*/
contract(
contractions: $ReadOnlyArray<{|
+old: $ReadOnlyArray<NodeAddressT>,
+replacement: Node,
|}>
): Graph {
const remap = new Map();
const contracted = new Graph();
for (const {old, replacement} of contractions) {
for (const addr of old) {
remap.set(addr, replacement.address);
}
contracted.addNode(replacement);
}
for (const node of this.nodes()) {
if (!remap.has(node.address)) {
contracted.addNode(node);
}
}
for (const edge of this.edges({showDangling: true})) {
let {src, dst} = edge;
const srcRemap = remap.get(src);
if (srcRemap != null) {
src = srcRemap;
}
const dstRemap = remap.get(dst);
if (dstRemap != null) {
dst = dstRemap;
}
const newEdge = {...edge, src, dst};
contracted.addEdge(newEdge);
}
return contracted;
}

checkInvariants() {
if (this._invariantsLastChecked.when !== this._modificationCount) {
let failure: ?string = null;
Expand Down
52 changes: 52 additions & 0 deletions src/core/graph.test.js
Expand Up @@ -1358,6 +1358,58 @@ describe("core/graph", () => {
});
});

describe("contract", () => {
const a = node("a");
const b = node("b");
const c = node("c");
it("has no effect with no contractions", () => {
const g = simpleGraph();
const g_ = simpleGraph().contract([]);
expect(g.equals(g_)).toBe(true);
});
it("adds the new node to the graph", () => {
const g = simpleGraph().contract([{old: [], replacement: c}]);
const g_ = simpleGraph().addNode(c);
expect(g.equals(g_)).toBe(true);
});
it("filters old nodes from the graph", () => {
const g = new Graph()
.addNode(a)
.addNode(b)
.contract([{old: [a.address, b.address], replacement: c}]);
const expected = new Graph().addNode(c);
expect(g.equals(expected)).toBe(true);
});
it("re-writes edges, including dangling or loop edges", () => {
const g = new Graph()
.addNode(a)
.addEdge(edge("loop", a, a))
.addEdge(edge("dangle1", a, b))
.addEdge(edge("dangle2", b, a))
.contract([{old: [a.address], replacement: c}]);
const expected = new Graph()
.addNode(c)
.addEdge(edge("loop", c, c))
.addEdge(edge("dangle1", c, b))
.addEdge(edge("dangle2", b, c));
expect(g.equals(expected)).toBe(true);
});
it("if multiple transforms target the same node, last one wins", () => {
const g = new Graph()
.addNode(a)
.addEdge(edge("loop", a, a))
.contract([
{old: [a.address], replacement: b},
{old: [a.address], replacement: c},
]);
const expected = new Graph()
.addNode(b)
.addNode(c)
.addEdge(edge("loop", c, c));
expect(g.equals(expected)).toBe(true);
});
});

describe("toJSON / fromJSON", () => {
describe("snapshot testing", () => {
it("a trivial graph", () => {
Expand Down

0 comments on commit a93c5d3

Please sign in to comment.