Skip to content

Commit d97b96e

Browse files
committed
Add Node Manager
1 parent 16348ea commit d97b96e

File tree

2 files changed

+341
-0
lines changed

2 files changed

+341
-0
lines changed

src/nodeManager.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
});

src/nodeManager.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { nanoid } from "nanoid";
2+
3+
import {
4+
type ComponentSpec,
5+
isGraphImplementation,
6+
} from "./utils/componentSpec";
7+
8+
export type NodeType = "task" | "input" | "output" | "taskInput" | "taskOutput";
9+
10+
interface NodeMapping {
11+
nodeId: string;
12+
taskId: string;
13+
nodeType: NodeType;
14+
createdAt: number;
15+
// For TaskInput & TaskOutput:
16+
parentTaskId?: string;
17+
handleName?: string;
18+
}
19+
20+
/*
21+
Manages stable node IDs for tasks and their inputs/outputs in the graph.
22+
- Each task gets a stable node ID based on its task ID and type.
23+
- Each task input/output handle also gets a stable node ID based on task ID and handle name.
24+
- If a task is renamed, its node ID remains the same.
25+
- If a task is deleted, its node and all associated handles are removed.
26+
- Input and Output node handles are not managed here, as they are static.
27+
*/
28+
29+
export class NodeManager {
30+
private mappings = new Map<string, NodeMapping>();
31+
private taskToNodeMap = new Map<string, Map<string, string>>();
32+
private taskHandleMap = new Map<string, Map<string, string>>();
33+
34+
private getTaskMapForType(nodeType: NodeType): Map<string, string> {
35+
if (!this.taskToNodeMap.has(nodeType)) {
36+
this.taskToNodeMap.set(nodeType, new Map<string, string>());
37+
}
38+
return this.taskToNodeMap.get(nodeType)!;
39+
}
40+
41+
private getTaskHandleMapForTask(taskId: string): Map<string, string> {
42+
if (!this.taskHandleMap.has(taskId)) {
43+
this.taskHandleMap.set(taskId, new Map<string, string>());
44+
}
45+
return this.taskHandleMap.get(taskId)!;
46+
}
47+
48+
getNodeId(taskId: string, nodeType: NodeType): string {
49+
const taskMap = this.getTaskMapForType(nodeType);
50+
const existing = taskMap.get(taskId);
51+
52+
if (existing) {
53+
return existing;
54+
}
55+
56+
// Generate new stable ID
57+
const nodeId = `${nodeType}_${nanoid()}`;
58+
const mapping: NodeMapping = {
59+
nodeId,
60+
taskId,
61+
nodeType,
62+
createdAt: Date.now(),
63+
};
64+
65+
this.mappings.set(nodeId, mapping);
66+
taskMap.set(taskId, nodeId);
67+
68+
return nodeId;
69+
}
70+
71+
getTaskHandleNodeId(
72+
taskId: string,
73+
handleName: string,
74+
handleType: "taskInput" | "taskOutput",
75+
): string {
76+
const taskHandleMap = this.getTaskHandleMapForTask(taskId);
77+
const handleKey = `${handleType}:${handleName}`;
78+
const existing = taskHandleMap.get(handleKey);
79+
80+
if (existing) {
81+
return existing;
82+
}
83+
84+
// Generate new stable ID
85+
const nodeId = `${handleType}_${nanoid()}`;
86+
const mapping: NodeMapping = {
87+
nodeId,
88+
taskId: handleKey,
89+
nodeType: handleType,
90+
createdAt: Date.now(),
91+
parentTaskId: taskId,
92+
handleName,
93+
};
94+
95+
this.mappings.set(nodeId, mapping);
96+
taskHandleMap.set(handleKey, nodeId);
97+
98+
return nodeId;
99+
}
100+
101+
updateTaskId(
102+
oldTaskId: string,
103+
newTaskId: string,
104+
nodeType?: NodeType,
105+
): void {
106+
if (!nodeType) {
107+
for (const [type, taskMap] of this.taskToNodeMap) {
108+
const nodeId = taskMap.get(oldTaskId);
109+
if (nodeId) {
110+
this.updateTaskId(oldTaskId, newTaskId, type as NodeType);
111+
return;
112+
}
113+
}
114+
this.updateTaskHandleMappings(oldTaskId, newTaskId);
115+
return;
116+
}
117+
118+
const taskMap = this.getTaskMapForType(nodeType);
119+
const nodeId = taskMap.get(oldTaskId);
120+
if (!nodeId) return;
121+
122+
const mapping = this.mappings.get(nodeId);
123+
if (!mapping) return;
124+
125+
taskMap.delete(oldTaskId);
126+
taskMap.set(newTaskId, nodeId);
127+
128+
mapping.taskId = newTaskId;
129+
this.mappings.set(nodeId, mapping);
130+
}
131+
132+
private updateTaskHandleMappings(oldTaskId: string, newTaskId: string): void {
133+
const oldHandleMap = this.taskHandleMap.get(oldTaskId);
134+
if (!oldHandleMap) return;
135+
136+
const newHandleMap = new Map(oldHandleMap);
137+
this.taskHandleMap.set(newTaskId, newHandleMap);
138+
this.taskHandleMap.delete(oldTaskId);
139+
140+
for (const nodeId of oldHandleMap.values()) {
141+
const mapping = this.mappings.get(nodeId);
142+
if (mapping) {
143+
mapping.parentTaskId = newTaskId;
144+
this.mappings.set(nodeId, mapping);
145+
}
146+
}
147+
}
148+
149+
removeNode(taskId: string, nodeType?: NodeType): void {
150+
if (!nodeType) {
151+
for (const [type, taskMap] of this.taskToNodeMap) {
152+
if (taskMap.has(taskId)) {
153+
this.removeNode(taskId, type as NodeType);
154+
}
155+
}
156+
this.removeTaskHandles(taskId);
157+
return;
158+
}
159+
160+
const taskMap = this.getTaskMapForType(nodeType);
161+
const nodeId = taskMap.get(taskId);
162+
if (!nodeId) return;
163+
164+
this.mappings.delete(nodeId);
165+
taskMap.delete(taskId);
166+
}
167+
168+
private removeTaskHandles(taskId: string): void {
169+
const handleMap = this.taskHandleMap.get(taskId);
170+
if (!handleMap) return;
171+
172+
for (const nodeId of handleMap.values()) {
173+
this.mappings.delete(nodeId);
174+
}
175+
176+
this.taskHandleMap.delete(taskId);
177+
}
178+
179+
getTaskId(nodeId: string): string | undefined {
180+
return this.mappings.get(nodeId)?.taskId;
181+
}
182+
183+
getParentTaskId(nodeId: string): string | undefined {
184+
return this.mappings.get(nodeId)?.parentTaskId;
185+
}
186+
187+
getHandleName(nodeId: string): string | undefined {
188+
return this.mappings.get(nodeId)?.handleName;
189+
}
190+
191+
getHandleInfo(
192+
nodeId: string,
193+
): { taskId: string; handleName: string } | undefined {
194+
const mapping = this.mappings.get(nodeId);
195+
if (!mapping || !mapping.parentTaskId || !mapping.handleName) {
196+
return undefined;
197+
}
198+
return {
199+
taskId: mapping.parentTaskId,
200+
handleName: mapping.handleName,
201+
};
202+
}
203+
204+
hasTaskId(taskId: string, nodeType: NodeType): boolean {
205+
const taskMap = this.getTaskMapForType(nodeType);
206+
return taskMap.has(taskId);
207+
}
208+
209+
// Sync with component spec to handle external changes
210+
syncWithComponentSpec(componentSpec: ComponentSpec): void {
211+
const currentTasks = new Map<NodeType, Set<string>>();
212+
currentTasks.set("task", new Set());
213+
currentTasks.set("input", new Set());
214+
currentTasks.set("output", new Set());
215+
216+
const currentTaskHandles = new Map<string, Set<string>>();
217+
218+
// Tasks
219+
if (isGraphImplementation(componentSpec.implementation)) {
220+
const graphSpec = componentSpec.implementation.graph;
221+
222+
Object.keys(graphSpec.tasks).forEach((taskId) => {
223+
currentTasks.get("task")?.add(taskId);
224+
});
225+
226+
Object.entries(graphSpec.tasks).forEach(([taskId, taskSpec]) => {
227+
const inputs = taskSpec.componentRef.spec?.inputs || [];
228+
const outputs = taskSpec.componentRef.spec?.outputs || [];
229+
230+
if (!currentTaskHandles.has(taskId)) {
231+
currentTaskHandles.set(taskId, new Set());
232+
}
233+
const taskHandleSet = currentTaskHandles.get(taskId)!;
234+
235+
inputs.forEach((input) => {
236+
taskHandleSet.add(`taskInput:${input.name}`);
237+
});
238+
239+
outputs.forEach((output) => {
240+
taskHandleSet.add(`taskOutput:${output.name}`);
241+
});
242+
});
243+
}
244+
245+
// Graph-level inputs and outputs
246+
componentSpec.inputs?.forEach((input) =>
247+
currentTasks.get("input")?.add(input.name),
248+
);
249+
componentSpec.outputs?.forEach((output) =>
250+
currentTasks.get("output")?.add(output.name),
251+
);
252+
253+
// Remove mappings for deleted tasks by type
254+
for (const [nodeType, taskMap] of this.taskToNodeMap) {
255+
const currentTasksForType = currentTasks.get(nodeType as NodeType);
256+
if (!currentTasksForType) continue;
257+
258+
for (const [taskId] of taskMap) {
259+
if (!currentTasksForType.has(taskId)) {
260+
this.removeNode(taskId, nodeType as NodeType);
261+
}
262+
}
263+
}
264+
265+
// Clean up task handles
266+
for (const [taskId, handleMap] of this.taskHandleMap) {
267+
const currentHandlesForTask = currentTaskHandles.get(taskId);
268+
269+
if (!currentHandlesForTask) {
270+
this.removeTaskHandles(taskId);
271+
continue;
272+
}
273+
274+
for (const [handleKey, nodeId] of handleMap) {
275+
if (!currentHandlesForTask.has(handleKey)) {
276+
this.mappings.delete(nodeId);
277+
handleMap.delete(handleKey);
278+
}
279+
}
280+
}
281+
}
282+
283+
getAllMappings(): NodeMapping[] {
284+
return Array.from(this.mappings.values());
285+
}
286+
287+
getMappingsForType(nodeType: NodeType): NodeMapping[] {
288+
return Array.from(this.mappings.values()).filter(
289+
(mapping) => mapping.nodeType === nodeType,
290+
);
291+
}
292+
293+
isManaged(nodeId: string): boolean {
294+
return this.mappings.has(nodeId);
295+
}
296+
297+
getNodeType(nodeId: string): NodeType | undefined {
298+
return this.mappings.get(nodeId)?.nodeType;
299+
}
300+
}

0 commit comments

Comments
 (0)