@wavequery/conductor
Version:
Modular LLM orchestration framework
227 lines • 7.9 kB
JavaScript
import { WebSocket, WebSocketServer } from "ws";
import { createServer } from "http";
import { EventEmitter } from "events";
import { getClientTemplate } from "./templates/client";
import { DEFAULT_CONFIG } from "./constants";
export class VizServer extends EventEmitter {
constructor(port = 3000, config = {}) {
super();
this.clients = new Set();
this.currentGraph = { nodes: [], edges: [] };
this.config = {
theme: "light",
fitView: true,
...DEFAULT_CONFIG,
...config,
};
this.httpServer = createServer((req, res) => {
if (req.url === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(this.getClientHtml());
}
});
this.wss = new WebSocketServer({ server: this.httpServer });
this.setupWebSocket();
this.httpServer.listen(port);
}
setupWebSocket() {
this.wss.on("connection", (ws) => {
this.clients.add(ws);
this.sendToClient(ws, {
type: "init",
payload: {
graph: this.currentGraph,
metadata: this.getGraphMetadata(),
},
});
ws.on("message", (message) => {
try {
const data = JSON.parse(message);
this.handleClientMessage(data, ws);
}
catch (error) {
console.error("Error handling message:", error);
}
});
ws.on("close", () => {
this.clients.delete(ws);
});
});
}
handleClientMessage(message, client) {
switch (message.type) {
case "nodeClick":
this.emit("nodeClick", message.payload);
break;
case "edgeClick":
this.emit("edgeClick", message.payload);
break;
}
}
getGraphMetadata() {
return {
timestamp: Date.now(),
totalNodes: this.currentGraph.nodes.length,
nodesByType: this.getNodesByType(),
nodesByStatus: this.getNodesByStatus(),
};
}
getNodesByType() {
return this.currentGraph.nodes.reduce((acc, node) => {
acc[node.type] = (acc[node.type] || 0) + 1;
return acc;
}, {});
}
getNodesByStatus() {
return this.currentGraph.nodes.reduce((acc, node) => {
const status = node.status || "pending";
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
}
sendToClient(client, message) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
}
broadcast(message) {
const messageStr = JSON.stringify(message);
this.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(messageStr);
}
});
}
getClientHtml() {
const address = this.httpServer.address();
if (!address)
throw new Error("Server address is not available.");
const host = address.address === "::" ? "localhost" : address.address;
return getClientTemplate(`${host}:${address.port}`, this.config);
}
processGraph(graph) {
const processedGraph = JSON.parse(JSON.stringify(graph));
processedGraph.nodes = processedGraph.nodes.map((node) => ({
...node,
// Only set initial positions if not already set
x: node.x ?? Math.random() * 800, // Random initial position
y: node.y ?? Math.random() * 600,
// Maintain fixed positions for pinned nodes
fx: node.fixed ? (node.x ?? 400) : undefined,
fy: node.fixed ? (node.y ?? 300) : undefined,
data: {
...node.data,
createdAt: node.data?.createdAt || new Date().toISOString(),
},
}));
// Process edges - ensure proper source/target references
processedGraph.edges = processedGraph.edges.map((edge) => ({
...edge,
id: edge.id,
source: typeof edge.source === "string" ? edge.source : edge.source.id,
target: typeof edge.target === "string" ? edge.target : edge.target.id,
data: {
...edge.data,
createdAt: edge.data?.createdAt || new Date().toISOString(),
type: edge.type,
},
}));
return processedGraph;
}
pinNode(nodeId, position) {
const node = this.currentGraph.nodes.find((n) => n.id === nodeId);
if (node) {
node.fixed = true;
if (position) {
node.x = node.fx = position.x;
node.y = node.fy = position.y;
}
else {
node.fx = node.x;
node.fy = node.y;
}
this.broadcast({
type: "node-update",
payload: { nodeId, updates: node },
});
}
}
unpinNode(nodeId) {
const node = this.currentGraph.nodes.find((n) => n.id === nodeId);
if (node) {
node.fixed = false;
node.fx = node.fy = undefined;
this.broadcast({
type: "node-update",
payload: { nodeId, updates: node },
});
}
}
mergeGraphs(currentGraph, updates) {
const merged = {
nodes: [...currentGraph.nodes],
edges: [...currentGraph.edges],
};
if (updates.nodes) {
updates.nodes.forEach((newNode) => {
const existingIndex = merged.nodes.findIndex((n) => n.id === newNode.id);
if (existingIndex >= 0) {
// Update existing node while preserving position if already set
merged.nodes[existingIndex] = {
...merged.nodes[existingIndex],
...newNode,
x: merged.nodes[existingIndex].x ?? newNode.x,
y: merged.nodes[existingIndex].y ?? newNode.y,
};
}
else {
merged.nodes.push(newNode);
}
});
}
if (updates.edges) {
updates.edges.forEach((newEdge) => {
const existingIndex = merged.edges.findIndex((e) => e.id === newEdge.id);
if (existingIndex >= 0) {
merged.edges[existingIndex] = newEdge;
}
else {
merged.edges.push(newEdge);
}
});
}
return this.processGraph(merged);
}
updateGraph(updates) {
this.currentGraph = this.mergeGraphs(this.currentGraph, updates);
this.broadcast({
type: "graph-update",
payload: {
graph: this.currentGraph,
metadata: this.getGraphMetadata(),
},
});
}
updateNode(nodeId, updates) {
const nodeIndex = this.currentGraph.nodes.findIndex((n) => n.id === nodeId);
if (nodeIndex !== -1) {
this.currentGraph.nodes[nodeIndex] = {
...this.currentGraph.nodes[nodeIndex],
...updates,
};
this.broadcast({
type: "node-update",
payload: {
nodeId,
updates,
metadata: this.getGraphMetadata(),
},
});
}
}
close() {
this.wss.close();
this.httpServer.close();
}
}
//# sourceMappingURL=server.js.map