relationship-graph.ts
1 /** 2 * Relationship Graph Utilities 3 * 4 * High-performance in-memory graph database for relationship queries 5 * Used by liminal web layout system for fast relationship traversal 6 */ 7 8 import { DreamNode } from '../dreamnode'; 9 10 /** 11 * In-memory relationship graph for high-performance queries 12 */ 13 export interface RelationshipGraph { 14 /** Map of nodeId to DreamNode for fast lookups */ 15 nodes: Map<string, DreamNode>; 16 17 /** Map of nodeId to array of connected nodeIds */ 18 edges: Map<string, string[]>; 19 20 /** Get all connections for a node */ 21 getConnections: (nodeId: string) => DreamNode[]; 22 23 /** Get opposite-type connections only (Dreams ↔ Dreamers) */ 24 getOppositeTypeConnections: (nodeId: string) => DreamNode[]; 25 26 /** Get second-degree connections (connections of connections) */ 27 getSecondDegreeConnections: (nodeId: string) => DreamNode[]; 28 } 29 30 /** 31 * Build relationship graph from array of DreamNodes 32 */ 33 export function buildRelationshipGraph(dreamNodes: DreamNode[]): RelationshipGraph { 34 const nodes = new Map<string, DreamNode>(); 35 const edges = new Map<string, string[]>(); 36 37 // Build nodes map first 38 dreamNodes.forEach(node => { 39 nodes.set(node.id, node); 40 }); 41 42 // Build edges, filtering out connections to non-existent nodes 43 // This prevents "phantom nodes" in layout calculations 44 dreamNodes.forEach(node => { 45 // Defensive: Handle nodes missing liminalWebConnections property 46 const connections = node.liminalWebConnections || []; 47 const validConnections = connections.filter(targetId => 48 nodes.has(targetId) // Only include connections to nodes that actually exist 49 ); 50 edges.set(node.id, validConnections); 51 }); 52 53 // Create graph with query methods 54 const graph: RelationshipGraph = { 55 nodes, 56 edges, 57 58 getConnections: (nodeId: string): DreamNode[] => { 59 const connections = edges.get(nodeId) || []; 60 return connections 61 .map(connectedId => nodes.get(connectedId)) 62 .filter((node): node is DreamNode => node !== undefined); 63 }, 64 65 getOppositeTypeConnections: (nodeId: string): DreamNode[] => { 66 const sourceNode = nodes.get(nodeId); 67 if (!sourceNode) return []; 68 69 const connections = edges.get(nodeId) || []; 70 return connections 71 .map(connectedId => nodes.get(connectedId)) 72 .filter((node): node is DreamNode => 73 node !== undefined && node.type !== sourceNode.type 74 ); 75 }, 76 77 getSecondDegreeConnections: (nodeId: string): DreamNode[] => { 78 const sourceNode = nodes.get(nodeId); 79 if (!sourceNode) return []; 80 81 const firstDegreeIds = edges.get(nodeId) || []; 82 const secondDegreeNodes = new Set<DreamNode>(); 83 84 firstDegreeIds.forEach(firstDegreeId => { 85 const secondDegreeIds = edges.get(firstDegreeId) || []; 86 87 secondDegreeIds.forEach(secondDegreeId => { 88 const secondDegreeNode = nodes.get(secondDegreeId); 89 90 if (secondDegreeNode && 91 secondDegreeId !== nodeId && // Don't include source 92 !firstDegreeIds.includes(secondDegreeId)) { // Don't include first-degree 93 secondDegreeNodes.add(secondDegreeNode); 94 } 95 }); 96 }); 97 98 return Array.from(secondDegreeNodes); 99 } 100 }; 101 102 return graph; 103 } 104 105 /** 106 * Get relationship statistics for debugging 107 */ 108 export function getRelationshipStats(graph: RelationshipGraph): { 109 totalNodes: number; 110 totalEdges: number; 111 dreamNodes: number; 112 dreamerNodes: number; 113 averageConnections: number; 114 maxConnections: number; 115 nodesWithNoConnections: number; 116 } { 117 const totalNodes = graph.nodes.size; 118 let totalEdges = 0; 119 let dreamNodes = 0; 120 let dreamerNodes = 0; 121 let maxConnections = 0; 122 let nodesWithNoConnections = 0; 123 124 graph.nodes.forEach((node, nodeId) => { 125 const connections = graph.edges.get(nodeId) || []; 126 totalEdges += connections.length; 127 128 if (node.type === 'dream') dreamNodes++; 129 else dreamerNodes++; 130 131 if (connections.length === 0) nodesWithNoConnections++; 132 if (connections.length > maxConnections) maxConnections = connections.length; 133 }); 134 135 return { 136 totalNodes, 137 totalEdges, 138 dreamNodes, 139 dreamerNodes, 140 averageConnections: totalNodes > 0 ? totalEdges / totalNodes : 0, 141 maxConnections, 142 nodesWithNoConnections 143 }; 144 } 145 146 /** 147 * Debug helper to log relationships for a specific node 148 */ 149 export function logNodeRelationships(graph: RelationshipGraph, nodeId: string): void { 150 const node = graph.nodes.get(nodeId); 151 if (!node) { 152 console.log(`Node ${nodeId} not found in graph`); 153 return; 154 } 155 156 console.log(`\n=== Relationships for ${node.name} (${node.type}) ===`); 157 158 const connections = graph.getConnections(nodeId); 159 console.log(`Direct connections (${connections.length}):`, 160 connections.map(n => `${n.name} (${n.type})`)); 161 162 const oppositeType = graph.getOppositeTypeConnections(nodeId); 163 console.log(`Opposite-type connections (${oppositeType.length}):`, 164 oppositeType.map(n => `${n.name} (${n.type})`)); 165 166 const secondDegree = graph.getSecondDegreeConnections(nodeId); 167 console.log(`Second-degree connections (${secondDegree.length}):`, 168 secondDegree.map(n => `${n.name} (${n.type})`)); 169 }