fix(core): Keep duplicate connections distinct in the workflow diff (#35652)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: James Gee <1285296+geemanjs@users.noreply.github.com>
This commit is contained in:
Andrew Chen
2026-08-24 14:41:01 +00:00
committed by GitHub
parent 62d7e4c901
commit 12a2e38817
2 changed files with 99 additions and 16 deletions
+27 -16
View File
@@ -12,6 +12,25 @@ export type ConnectionsDiff = {
removed: Record<string, INodeConnectionsDiff>;
};
/**
* Groups connections by their serialized value, keeping every occurrence.
* A bucket may hold the same connection more than once, so comparing by value
* alone would collapse the duplicates and hide added or removed ones.
*/
function groupByValue(connections: IConnection[]) {
const byValue = new Map<string, Array<NonNullable<ConnectionEntry['value']>>>();
connections.forEach((connection, index) => {
const key = JSON.stringify(connection);
const entries = byValue.get(key);
if (entries) entries.push({ index, connection });
else byValue.set(key, [{ index, connection }]);
});
return byValue;
}
export function compareConnections(prev: IConnections, next: IConnections): ConnectionsDiff {
const added: Record<string, INodeConnectionsDiff> = {};
const removed: Record<string, INodeConnectionsDiff> = {};
@@ -41,22 +60,13 @@ export function compareConnections(prev: IConnections, next: IConnections): Conn
const nextConnections = nextInputConnections[sourceIndex] ?? [];
// Build maps for easier comparison
const prevMap = new Map(
prevConnections.map((conn, idx) => [
JSON.stringify(conn),
{ index: idx, connection: conn },
]),
);
const nextMap = new Map(
nextConnections.map((conn, idx) => [
JSON.stringify(conn),
{ index: idx, connection: conn },
]),
);
const prevMap = groupByValue(prevConnections);
const nextMap = groupByValue(nextConnections);
// Find added connections
for (const [key, value] of nextMap) {
if (!prevMap.has(key)) {
for (const [key, entries] of nextMap) {
const kept = prevMap.get(key)?.length ?? 0;
for (const value of entries.slice(kept)) {
if (!added[nodeName]) added[nodeName] = {};
if (!added[nodeName][inputName]) added[nodeName][inputName] = [];
@@ -68,8 +78,9 @@ export function compareConnections(prev: IConnections, next: IConnections): Conn
}
// Find removed connections
for (const [key, value] of prevMap) {
if (!nextMap.has(key)) {
for (const [key, entries] of prevMap) {
const kept = nextMap.get(key)?.length ?? 0;
for (const value of entries.slice(kept)) {
if (!removed[nodeName]) removed[nodeName] = {};
if (!removed[nodeName][inputName]) removed[nodeName][inputName] = [];
@@ -546,4 +546,76 @@ describe('compareConnections', () => {
expect(result.removed).toEqual({});
});
});
describe('duplicate connections', () => {
// Nothing normalizes connections on the way in, so a bucket can hold the same
// connection twice. Comparing by value alone would collapse the duplicates and
// hide the change.
it('should detect removal of one of two identical connections', () => {
const prev: IConnections = {
node1: {
main: [[createConnection('node0', 'main', 0), createConnection('node0', 'main', 0)]],
},
};
const next: IConnections = {
node1: {
main: [[createConnection('node0', 'main', 0)]],
},
};
const result = compareConnections(prev, next);
expect(result.added).toEqual({});
expect(result.removed).toEqual({
node1: {
main: [
{
sourceIndex: 0,
value: { index: 1, connection: createConnection('node0', 'main', 0) },
},
],
},
});
});
it('should detect addition of a second identical connection', () => {
const prev: IConnections = {
node1: {
main: [[createConnection('node0', 'main', 0)]],
},
};
const next: IConnections = {
node1: {
main: [[createConnection('node0', 'main', 0), createConnection('node0', 'main', 0)]],
},
};
const result = compareConnections(prev, next);
expect(result.removed).toEqual({});
expect(result.added).toEqual({
node1: {
main: [
{
sourceIndex: 0,
value: { index: 1, connection: createConnection('node0', 'main', 0) },
},
],
},
});
});
it('should report no change when the same duplicates are on both sides', () => {
const connections: IConnections = {
node1: {
main: [[createConnection('node0', 'main', 0), createConnection('node0', 'main', 0)]],
},
};
const result = compareConnections(connections, connections);
expect(result.added).toEqual({});
expect(result.removed).toEqual({});
});
});
});