Skip to content

Commit 46676ff

Browse files
committed
Implement Node Manager
1 parent af581a3 commit 46676ff

File tree

8 files changed

+206
-41
lines changed

8 files changed

+206
-41
lines changed

src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,34 @@ const FlowCanvas = ({
144144

145145
const latestFlowPosRef = useRef<{ x: number; y: number } | null>(null);
146146

147+
/* Migrate Node Ids */
148+
const [migrationCompleted, setMigrationCompleted] = useState(false);
149+
150+
useEffect(() => {
151+
if (!initialCanvasLoaded.current || migrationCompleted) return;
152+
153+
const needsMigration = nodes.some(
154+
(node) =>
155+
!nodeManager.isManaged(node.id) &&
156+
(node.id.startsWith("task_") ||
157+
node.id.startsWith("input_") ||
158+
node.id.startsWith("output_")),
159+
);
160+
161+
if (needsMigration) {
162+
console.log("Migrating legacy node IDs to stable IDs...");
163+
const { updatedNodes, migrationMap } =
164+
nodeManager.migrateExistingNodes(nodes);
165+
166+
setNodes(updatedNodes);
167+
setMigrationCompleted(true);
168+
169+
console.log("Migration completed:", migrationMap);
170+
} else {
171+
setMigrationCompleted(true);
172+
}
173+
}, [nodes, nodeManager, migrationCompleted]);
174+
147175
const [showToolbar, setShowToolbar] = useState(false);
148176
const [replaceTarget, setReplaceTarget] = useState<Node | null>(null);
149177
const [shiftKeyPressed, setShiftKeyPressed] = useState(false);

src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ export const duplicateNodes = (
6060
const graphSpec = componentSpec.implementation.graph;
6161

6262
const nodeIdMap: Record<string, string> = {};
63-
6463
const newTasks: Record<string, TaskSpec> = {};
6564
const newInputs: Record<string, InputSpec> = {};
6665
const newOutputs: Record<string, OutputSpec> = {};

src/nodeManager.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
3+
import { NodeManager } from "./nodeManager";
4+
5+
describe("NodeManager", () => {
6+
let nodeManager: NodeManager;
7+
8+
beforeEach(() => {
9+
nodeManager = new NodeManager();
10+
});
11+
12+
describe("stable node ID generation", () => {
13+
it("should generate consistent node IDs for the same task", () => {
14+
const nodeId1 = nodeManager.getNodeId("test-task", "task");
15+
const nodeId2 = nodeManager.getNodeId("test-task", "task");
16+
17+
expect(nodeId1).toBe(nodeId2);
18+
expect(nodeId1).toMatch(/^task_/);
19+
});
20+
21+
it("should generate different IDs for different tasks", () => {
22+
const nodeId1 = nodeManager.getNodeId("task-1", "task");
23+
const nodeId2 = nodeManager.getNodeId("task-2", "task");
24+
25+
expect(nodeId1).not.toBe(nodeId2);
26+
});
27+
});
28+
29+
describe("task ID updates", () => {
30+
it("should update task ID while preserving node ID", () => {
31+
const originalNodeId = nodeManager.getNodeId("old-task", "task");
32+
nodeManager.updateTaskId("old-task", "new-task");
33+
34+
const newNodeId = nodeManager.getNodeId("new-task", "task");
35+
expect(newNodeId).toBe(originalNodeId);
36+
37+
const taskId = nodeManager.getTaskId(originalNodeId);
38+
expect(taskId).toBe("new-task");
39+
});
40+
});
41+
42+
describe("migration", () => {
43+
it("should migrate legacy nodes to stable IDs", () => {
44+
const legacyNodes = [
45+
{
46+
id: "task_legacy-task",
47+
type: "task",
48+
data: {},
49+
position: { x: 0, y: 0 },
50+
},
51+
{
52+
id: "input_legacy-input",
53+
type: "input",
54+
data: {},
55+
position: { x: 0, y: 0 },
56+
},
57+
];
58+
59+
const { updatedNodes, migrationMap } =
60+
nodeManager.migrateExistingNodes(legacyNodes);
61+
62+
expect(updatedNodes).toHaveLength(2);
63+
expect(updatedNodes[0].id).toMatch(/^task_/);
64+
expect(updatedNodes[1].id).toMatch(/^input_/);
65+
expect(updatedNodes[0].id).not.toBe("task_legacy-task");
66+
expect(updatedNodes[1].id).not.toBe("input_legacy-input");
67+
68+
expect(migrationMap["task_legacy-task"]).toBe(updatedNodes[0].id);
69+
expect(migrationMap["input_legacy-input"]).toBe(updatedNodes[1].id);
70+
});
71+
});
72+
});

src/nodeManager.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type Node } from "@xyflow/react";
12
import { nanoid } from "nanoid";
23

34
import {
@@ -98,4 +99,66 @@ export class NodeManager {
9899
getAllMappings(): NodeMapping[] {
99100
return Array.from(this.mappings.values());
100101
}
102+
103+
// Migration method for existing nodes
104+
migrateExistingNodes(nodes: Node[]): {
105+
updatedNodes: Node[];
106+
migrationMap: Record<string, string>;
107+
} {
108+
const updatedNodes: Node[] = [];
109+
const migrationMap: Record<string, string> = {};
110+
111+
for (const node of nodes) {
112+
const oldNodeId = node.id;
113+
let taskId: string;
114+
let nodeType: NodeType;
115+
116+
// Extract task ID and node type from legacy node ID
117+
if (oldNodeId.startsWith("task_")) {
118+
taskId = oldNodeId.replace(/^task_/, "");
119+
nodeType = "task";
120+
} else if (oldNodeId.startsWith("input_")) {
121+
taskId = oldNodeId.replace(/^input_/, "");
122+
nodeType = "input";
123+
} else if (oldNodeId.startsWith("output_")) {
124+
taskId = oldNodeId.replace(/^output_/, "");
125+
nodeType = "output";
126+
} else {
127+
// Already migrated or unknown format
128+
updatedNodes.push(node);
129+
continue;
130+
}
131+
132+
// Get or create stable node ID
133+
const stableNodeId = this.getNodeId(taskId, nodeType);
134+
migrationMap[oldNodeId] = stableNodeId;
135+
136+
// Update node with stable ID
137+
updatedNodes.push({
138+
...node,
139+
id: stableNodeId,
140+
});
141+
}
142+
143+
return { updatedNodes, migrationMap };
144+
}
145+
146+
// Batch update for task renames
147+
batchUpdateTaskIds(
148+
updates: Array<{ oldTaskId: string; newTaskId: string }>,
149+
): void {
150+
for (const { oldTaskId, newTaskId } of updates) {
151+
this.updateTaskId(oldTaskId, newTaskId);
152+
}
153+
}
154+
155+
// Check if a node ID is managed by this NodeManager
156+
isManaged(nodeId: string): boolean {
157+
return this.mappings.has(nodeId);
158+
}
159+
160+
// Get node type from node ID
161+
getNodeType(nodeId: string): NodeType | undefined {
162+
return this.mappings.get(nodeId)?.nodeType;
163+
}
101164
}

src/utils/nodes/createInputNode.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@ import type { IONodeData, NodeData } from "@/types/nodes";
44

55
import type { InputSpec } from "../componentSpec";
66
import { extractPositionFromAnnotations } from "./extractPositionFromAnnotations";
7-
import { inputNameToNodeId } from "./nodeIdUtils";
87

98
export const createInputNode = (input: InputSpec, nodeData: NodeData) => {
109
const { name, annotations } = input;
1110
const { nodeManager, readOnly } = nodeData;
1211

13-
const newNodeId = nodeManager?.getNodeId(name, "input");
14-
console.log("Creating input node:", { name, nodeId: newNodeId });
12+
const nodeId = nodeManager?.getNodeId(name, "input");
13+
console.log("Creating input node:", { name, nodeId });
1514

1615
const position = extractPositionFromAnnotations(annotations);
17-
const nodeId = inputNameToNodeId(name);
1816

1917
const inputNodeData: IONodeData = {
2018
spec: input,

src/utils/nodes/createOutputNode.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@ import type { IONodeData, NodeData } from "@/types/nodes";
44

55
import type { OutputSpec } from "../componentSpec";
66
import { extractPositionFromAnnotations } from "./extractPositionFromAnnotations";
7-
import { outputNameToNodeId } from "./nodeIdUtils";
87

98
export const createOutputNode = (output: OutputSpec, nodeData: NodeData) => {
109
const { name, annotations } = output;
1110
const { nodeManager, readOnly } = nodeData;
1211

13-
const newNodeId = nodeManager?.getNodeId(name, "output");
14-
console.log("Creating output node:", { name, nodeId: newNodeId });
12+
const nodeId = nodeManager?.getNodeId(name, "output");
13+
console.log("Creating output node:", { name, nodeId });
1514

1615
const position = extractPositionFromAnnotations(annotations);
17-
const nodeId = outputNameToNodeId(name);
1816

1917
const outputNodeData: IONodeData = {
2018
spec: output,

src/utils/nodes/createTaskNode.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { NodeData, TaskNodeData } from "@/types/nodes";
44

55
import type { TaskSpec } from "../componentSpec";
66
import { extractPositionFromAnnotations } from "./extractPositionFromAnnotations";
7-
import { taskIdToNodeId } from "./nodeIdUtils";
87
import { convertNodeCallbacksToTaskCallbacks } from "./taskCallbackUtils";
98

109
export const createTaskNode = (
@@ -14,11 +13,10 @@ export const createTaskNode = (
1413
const [taskId, taskSpec] = task;
1514
const { nodeManager, callbacks, connectable, ...data } = nodeData;
1615

17-
const newNodeId = nodeManager?.getNodeId(taskId, "task");
18-
console.log("Creating task node:", { taskId, nodeId: newNodeId });
16+
const nodeId = nodeManager.getNodeId(taskId, "task");
17+
console.log("Creating task node:", { taskId, nodeId });
1918

2019
const position = extractPositionFromAnnotations(taskSpec.annotations);
21-
const nodeId = taskIdToNodeId(taskId);
2220

2321
const ids = { taskId, nodeId };
2422
const taskCallbacks = convertNodeCallbacksToTaskCallbacks(ids, callbacks);

src/utils/nodes/nodeIdUtils.ts

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
1-
/**
2-
* Utility functions for converting between node IDs and their corresponding names/identifiers
3-
*/
1+
import type { NodeManager, NodeType } from "@/nodeManager";
42

5-
/**
6-
* Extracts the task ID from a task node ID by removing the "task_" prefix
7-
*/
8-
export const nodeIdToTaskId = (id: string) => id.replace(/^task_/, "");
3+
// DEPRECATED: Legacy functions - use NodeManager instead
4+
export const taskIdToNodeId = (taskId: string): string => `task_${taskId}`; // Legacy
5+
export const inputNameToNodeId = (inputName: string): string =>
6+
`input_${inputName}`; // Legacy
7+
export const outputNameToNodeId = (outputName: string): string =>
8+
`output_${outputName}`; // Legacy
99

10-
/**
11-
* Extracts the input name from an input node ID by removing the "input_" prefix
12-
*/
13-
export const nodeIdToInputName = (id: string) => id.replace(/^input_/, "");
10+
// RENAMED: For backwards compatibility and clarity
11+
export const inputNameToInputId = (inputName: string): string => inputName; // 1:1 mapping
12+
export const outputNameToOutputId = (outputName: string): string => outputName; // 1:1 mapping
13+
export const inputIdToInputName = (inputId: string): string => inputId; // 1:1 mapping
14+
export const outputIdToOutputName = (outputId: string): string => outputId; // 1:1 mapping
1415

15-
/**
16-
* Extracts the output name from an output node ID by removing the "output_" prefix
17-
*/
18-
export const nodeIdToOutputName = (id: string) => id.replace(/^output_/, "");
16+
// LEGACY: Keep for backwards compatibility
17+
export const nodeIdToTaskId = (nodeId: string): string => {
18+
return nodeId.replace(/^task_/, "");
19+
};
1920

20-
/**
21-
* Creates a task node ID by adding the "task_" prefix to a task ID
22-
*/
23-
export const taskIdToNodeId = (taskId: string) => `task_${taskId}`;
21+
export const nodeIdToInputName = (nodeId: string): string => {
22+
return nodeId.replace(/^input_/, "");
23+
};
2424

25-
/**
26-
* Creates an input node ID by adding the "input_" prefix to an input name
27-
*/
28-
export const inputNameToNodeId = (inputName: string) => `input_${inputName}`;
25+
export const nodeIdToOutputName = (nodeId: string): string => {
26+
return nodeId.replace(/^output_/, "");
27+
};
2928

30-
/**
31-
* Creates an output node ID by adding the "output_" prefix to an output name
32-
*/
33-
export const outputNameToNodeId = (outputName: string) =>
34-
`output_${outputName}`;
29+
// NEW: NodeManager-aware functions
30+
export const getTaskIdFromNodeId = (
31+
nodeId: string,
32+
nodeManager: NodeManager,
33+
): string | undefined => {
34+
return nodeManager.getTaskId(nodeId);
35+
};
36+
37+
export const getStableNodeId = (
38+
taskId: string,
39+
nodeType: NodeType,
40+
nodeManager: NodeManager,
41+
): string => {
42+
return nodeManager.getNodeId(taskId, nodeType);
43+
};

0 commit comments

Comments
 (0)