UNPKG

@terminals-tech/graph

Version:

Language-first event graph system. Extract relationships from text, find patterns, and navigate event chains.

431 lines (430 loc) 15.4 kB
"use strict"; /** * GraphProcessor - Scalable graph algorithms for 100K+ events * Phase 3: Efficient graph operations */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphProcessor = void 0; class GraphProcessor { constructor() { this.adjacencyList = new Map(); this.reverseIndex = new Map(); this.edgeWeights = new Map(); this.nodeData = new Map(); this.pageRankCache = new Map(); this.clusters = new Map(); this.isDirty = false; // Performance counters this.nodeCount = 0; this.edgeCount = 0; // Idempotency tracking this.processedEvents = new Set(); this.edgeHashes = new Set(); } /** * Add event to graph incrementally (idempotent) */ addEvent(event, relations) { // Check if already processed (idempotent) if (this.processedEvents.has(event.id)) { return; } // Create snapshot for rollback on error const snapshot = this.createSnapshot(); try { // Add node if (!this.adjacencyList.has(event.id)) { this.adjacencyList.set(event.id, new Set()); this.nodeData.set(event.id, event); this.nodeCount++; } // Add edges from relations relations.forEach(rel => { this.addEdge(event.id, rel.to, rel.weight, rel.type); }); // Mark as processed this.processedEvents.add(event.id); // Mark cache as dirty this.isDirty = true; } catch (error) { // Rollback on error this.restoreSnapshot(snapshot); throw error; } } /** * Add edge between nodes (idempotent) */ addEdge(from, to, weight = 1, type) { // Create edge hash for idempotency const edgeHash = `${from}->${to}:${type || 'default'}`; if (this.edgeHashes.has(edgeHash)) { // Edge already exists, update weight if higher const existingWeight = this.edgeWeights.get(from)?.get(to) || 0; if (weight > existingWeight) { this.edgeWeights.get(from).set(to, weight); } return; } // Add to adjacency list if (!this.adjacencyList.has(from)) { this.adjacencyList.set(from, new Set()); this.nodeCount++; } if (!this.adjacencyList.has(to)) { this.adjacencyList.set(to, new Set()); this.nodeCount++; } this.adjacencyList.get(from).add(to); // Maintain reverse index for fast incoming edge lookups if (!this.reverseIndex.has(to)) { this.reverseIndex.set(to, new Set()); } this.reverseIndex.get(to).add(from); // Store edge weight if (!this.edgeWeights.has(from)) { this.edgeWeights.set(from, new Map()); } this.edgeWeights.get(from).set(to, weight); // Mark edge as added this.edgeHashes.add(edgeHash); this.edgeCount++; this.isDirty = true; } /** * Calculate PageRank scores using power iteration */ calculatePageRank(iterations = 20, damping = 0.85) { if (!this.isDirty && this.pageRankCache.size > 0) { // Return cached results return this.formatPageRankResults(); } const nodes = Array.from(this.adjacencyList.keys()); const n = nodes.length; if (n === 0) return []; const scores = new Map(); const newScores = new Map(); // Initialize scores nodes.forEach(node => scores.set(node, 1 / n)); // Power iteration for (let iter = 0; iter < iterations; iter++) { newScores.clear(); nodes.forEach(node => { let score = (1 - damping) / n; // Sum contributions from incoming links const incoming = this.reverseIndex.get(node) || new Set(); incoming.forEach(source => { const outDegree = this.adjacencyList.get(source)?.size || 1; const sourceScore = scores.get(source) || 0; const edgeWeight = this.edgeWeights.get(source)?.get(node) || 1; score += damping * sourceScore * edgeWeight / outDegree; }); newScores.set(node, score); }); // Copy scores for next iteration scores.clear(); newScores.forEach((score, node) => scores.set(node, score)); } // Cache results this.pageRankCache = scores; this.isDirty = false; return this.formatPageRankResults(); } /** * Find connected components (clusters) */ findClusters() { this.clusters.clear(); const visited = new Set(); const clusterList = []; let clusterId = 0; // Find connected components using BFS this.adjacencyList.forEach((_, startNode) => { if (!visited.has(startNode)) { const cluster = new Set(); const queue = [startNode]; visited.add(startNode); while (queue.length > 0) { const node = queue.shift(); cluster.add(node); this.clusters.set(node, clusterId); // Add unvisited neighbors (treating graph as undirected for clustering) const outgoing = this.adjacencyList.get(node) || new Set(); const incoming = this.reverseIndex.get(node) || new Set(); const neighbors = new Set([...outgoing, ...incoming]); neighbors.forEach(neighbor => { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); } }); } // Calculate cluster density const density = this.calculateClusterDensity(cluster); // Find cluster center (node with highest degree within cluster) const center = this.findClusterCenter(cluster); clusterList.push({ id: clusterId++, nodes: cluster, center, density }); } }); return clusterList.sort((a, b) => b.nodes.size - a.nodes.size); } /** * Find shortest path using BFS */ findPath(from, to) { if (!this.adjacencyList.has(from) || !this.adjacencyList.has(to)) { return null; } const queue = [ { node: from, path: [from], weight: 0 } ]; const visited = new Set([from]); while (queue.length > 0) { const { node, path, weight } = queue.shift(); if (node === to) { return { nodes: path, totalWeight: weight, length: path.length - 1 }; } const neighbors = this.adjacencyList.get(node) || new Set(); neighbors.forEach(neighbor => { if (!visited.has(neighbor)) { visited.add(neighbor); const edgeWeight = this.edgeWeights.get(node)?.get(neighbor) || 1; queue.push({ node: neighbor, path: [...path, neighbor], weight: weight + edgeWeight }); } }); } return null; } /** * Find all paths between two nodes (limited depth) */ findAllPaths(from, to, maxDepth = 5) { if (!this.adjacencyList.has(from) || !this.adjacencyList.has(to)) { return []; } const paths = []; const stack = [ { node: from, path: [from], weight: 0, visited: new Set([from]) } ]; while (stack.length > 0) { const { node, path, weight, visited } = stack.pop(); if (path.length > maxDepth) continue; if (node === to) { paths.push({ nodes: path, totalWeight: weight, length: path.length - 1 }); continue; } const neighbors = this.adjacencyList.get(node) || new Set(); neighbors.forEach(neighbor => { if (!visited.has(neighbor)) { const newVisited = new Set(visited); newVisited.add(neighbor); const edgeWeight = this.edgeWeights.get(node)?.get(neighbor) || 1; stack.push({ node: neighbor, path: [...path, neighbor], weight: weight + edgeWeight, visited: newVisited }); } }); } return paths.sort((a, b) => a.totalWeight - b.totalWeight); } /** * Get subgraph around a node */ getSubgraph(nodeId, depth = 2) { if (!this.adjacencyList.has(nodeId)) { return { nodes: new Set(), edges: [] }; } const subgraphNodes = new Set(); const subgraphEdges = []; const queue = [{ node: nodeId, d: 0 }]; const visited = new Set(); while (queue.length > 0) { const { node, d } = queue.shift(); if (visited.has(node) || d > depth) continue; visited.add(node); subgraphNodes.add(node); // Add outgoing edges const neighbors = this.adjacencyList.get(node) || new Set(); neighbors.forEach(neighbor => { if (d < depth) { const weight = this.edgeWeights.get(node)?.get(neighbor) || 1; subgraphEdges.push({ from: node, to: neighbor, weight }); queue.push({ node: neighbor, d: d + 1 }); } }); // Add incoming edges const incoming = this.reverseIndex.get(node) || new Set(); incoming.forEach(source => { if (d < depth && !visited.has(source)) { queue.push({ node: source, d: d + 1 }); } }); } return { nodes: subgraphNodes, edges: subgraphEdges }; } /** * Detect cycles in the graph */ detectCycles() { const cycles = []; const visited = new Set(); const recursionStack = new Set(); const dfs = (node, path) => { visited.add(node); recursionStack.add(node); path.push(node); const neighbors = this.adjacencyList.get(node) || new Set(); neighbors.forEach(neighbor => { if (!visited.has(neighbor)) { dfs(neighbor, [...path]); } else if (recursionStack.has(neighbor)) { // Found a cycle const cycleStart = path.indexOf(neighbor); if (cycleStart !== -1) { cycles.push(path.slice(cycleStart)); } } }); recursionStack.delete(node); }; // Run DFS from each unvisited node this.adjacencyList.forEach((_, node) => { if (!visited.has(node)) { dfs(node, []); } }); return cycles; } /** * Calculate graph statistics */ getStatistics() { const degrees = Array.from(this.adjacencyList.values()).map(neighbors => neighbors.size); const maxDegree = Math.max(...degrees, 0); const avgDegree = degrees.length > 0 ? degrees.reduce((a, b) => a + b, 0) / degrees.length : 0; const maxPossibleEdges = this.nodeCount * (this.nodeCount - 1); const density = maxPossibleEdges > 0 ? this.edgeCount / maxPossibleEdges : 0; // Check if graph is connected const clusters = this.findClusters(); const connected = clusters.length === 1; // Check if graph has cycles const cycles = this.detectCycles(); const cyclic = cycles.length > 0; return { nodes: this.nodeCount, edges: this.edgeCount, density, avgDegree, maxDegree, connected, cyclic }; } /** * Clear the graph */ clear() { this.adjacencyList.clear(); this.reverseIndex.clear(); this.edgeWeights.clear(); this.nodeData.clear(); this.pageRankCache.clear(); this.clusters.clear(); this.processedEvents.clear(); this.edgeHashes.clear(); this.nodeCount = 0; this.edgeCount = 0; this.isDirty = false; } /** * Create a snapshot for rollback */ createSnapshot() { return { nodeCount: this.nodeCount, edgeCount: this.edgeCount, processedEvents: new Set(this.processedEvents), edgeHashes: new Set(this.edgeHashes) }; } /** * Restore from snapshot */ restoreSnapshot(snapshot) { this.nodeCount = snapshot.nodeCount; this.edgeCount = snapshot.edgeCount; this.processedEvents = snapshot.processedEvents; this.edgeHashes = snapshot.edgeHashes; this.isDirty = true; } // Private helper methods formatPageRankResults() { const results = Array.from(this.pageRankCache.entries()) .map(([node, score]) => ({ node, score, rank: 0 })) .sort((a, b) => b.score - a.score); // Add ranks results.forEach((result, idx) => { result.rank = idx + 1; }); return results; } calculateClusterDensity(cluster) { if (cluster.size <= 1) return 0; let internalEdges = 0; cluster.forEach(node => { const neighbors = this.adjacencyList.get(node) || new Set(); neighbors.forEach(neighbor => { if (cluster.has(neighbor)) { internalEdges++; } }); }); const maxPossibleEdges = cluster.size * (cluster.size - 1); return maxPossibleEdges > 0 ? internalEdges / maxPossibleEdges : 0; } findClusterCenter(cluster) { let maxDegree = 0; let center; cluster.forEach(node => { const outDegree = (this.adjacencyList.get(node) || new Set()).size; const inDegree = (this.reverseIndex.get(node) || new Set()).size; const totalDegree = outDegree + inDegree; if (totalDegree > maxDegree) { maxDegree = totalDegree; center = node; } }); return center; } } exports.GraphProcessor = GraphProcessor;