All files / src/components/streams useCoordinationLayout.ts

96.42% Statements 27/28
100% Branches 2/2
83.33% Functions 5/6
100% Lines 26/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97                                                                        7x   7x   7x   7x   7x   7x       126x 50x   76x         28x 7x     21x 21x 21x   21x 63x 63x   21x 40x     21x   21x 63x 63x 63x     21x                       28x    
/**
 * Dagre positioning layer for the Coordination graph. Takes the pure
 * {@link buildCoordinationGraph} model (dagre-free, unit-testable) and assigns
 * deterministic node positions via a left-to-right bipartite dagre layout.
 *
 * @module
 */
 
import { useMemo } from "react";
import dagre from "@dagrejs/dagre";
import type { Node } from "@xyflow/react";
import type { Session, StreamData } from "../../hooks/types.js";
import {
  buildCoordinationGraph,
  STREAM_NODE_TYPE,
  type CoordNodeData,
  type CoordinationLayoutResult,
} from "./coordinationGraphModel.js";
 
export {
  buildCoordinationGraph,
  sessionNodeId,
  streamNodeId,
  COORD_EDGE_TYPE,
  SESSION_NODE_TYPE,
  STREAM_NODE_TYPE,
} from "./coordinationGraphModel.js";
export type {
  CoordEdgeData,
  CoordNodeData,
  CoordinationLayoutResult,
  SessionNodeData,
  StreamNodeData,
} from "./coordinationGraphModel.js";
 
/** Width of a session node (pixels). */
const SESSION_NODE_WIDTH: number = 200;
/** Height of a session node (pixels). */
const SESSION_NODE_HEIGHT: number = 64;
/** Width of a stream hub node (pixels). */
const STREAM_NODE_WIDTH: number = 180;
/** Height of a stream hub node (pixels). */
const STREAM_NODE_HEIGHT: number = 56;
/** Separation between sibling nodes within a rank (pixels). */
const NODE_SEPARATION: number = 40;
/** Separation between rank levels (pixels). */
const RANK_SEPARATION: number = 70;
 
/** Node dimensions for dagre, keyed by node type. */
function nodeDimensions(type: string | undefined): { width: number; height: number } {
  if (type === STREAM_NODE_TYPE) {
    return { width: STREAM_NODE_WIDTH, height: STREAM_NODE_HEIGHT };
  }
  return { width: SESSION_NODE_WIDTH, height: SESSION_NODE_HEIGHT };
}
 
/** Assign dagre positions to a pure coordination graph model. */
function layoutGraph(model: CoordinationLayoutResult): CoordinationLayoutResult {
  if (model.nodes.length === 0) {
    return model;
  }
 
  const graph = new dagre.graphlib.Graph({ multigraph: true });
  graph.setDefaultEdgeLabel(() => ({}));
  graph.setGraph({ rankdir: "LR", nodesep: NODE_SEPARATION, ranksep: RANK_SEPARATION });
 
  for (const node of model.nodes) {
    const { width, height } = nodeDimensions(node.type);
    graph.setNode(node.id, { width, height });
  }
  for (const edge of model.edges) {
    graph.setEdge(edge.source, edge.target, {}, edge.id);
  }
 
  dagre.layout(graph);
 
  const nodes: Node<CoordNodeData>[] = model.nodes.map((node) => {
    const pos = graph.node(node.id) as { x: number; y: number };
    const { width, height } = nodeDimensions(node.type);
    return { ...node, position: { x: pos.x - width / 2, y: pos.y - height / 2 } };
  });
 
  return { nodes, edges: model.edges };
}
 
/**
 * Build and lay out the coordination graph, memoized on its inputs. Sessions
 * with no visible streams are intentionally omitted — the graph shows
 * coordination, not the full session inventory.
 */
export function useCoordinationLayout(
  streams: StreamData[],
  sessions: Session[],
): CoordinationLayoutResult {
  return useMemo(() => layoutGraph(buildCoordinationGraph(streams, sessions)), [streams, sessions]);
}