a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 4.3 kB view raw
1/** 2 * Dependency graph tracking and querying for debugging. 3 * 4 * Tracks signal dependency relationships to enable visualization 5 * and debugging of reactive data flow. 6 */ 7 8import type { AnySignal, Dep, DepGraph, GraphNode } from "$types/volt"; 9import { getSignalMetadata } from "./registry"; 10 11const dependencies = new WeakMap<AnySignal, Set<Dep>>(); 12const dependents = new WeakMap<Dep, Set<AnySignal>>(); 13 14/** 15 * Record that a signal/computed depends on certain dependencies. 16 * Should be called after tracking is complete. 17 */ 18export function recordDependencies(signal: AnySignal, deps: Dep[]): void { 19 const existingDeps = dependencies.get(signal) || new Set(); 20 21 for (const dep of deps) { 22 existingDeps.add(dep); 23 24 const existingDependents = dependents.get(dep) || new Set(); 25 existingDependents.add(signal); 26 dependents.set(dep, existingDependents); 27 } 28 29 dependencies.set(signal, existingDeps); 30} 31 32/** 33 * Get all signals that this signal/computed depends on. 34 */ 35export function getDependencies(signal: AnySignal): Dep[] { 36 const deps = dependencies.get(signal); 37 return deps ? [...deps] : []; 38} 39 40/** 41 * Get all signals/computeds that depend on this signal. 42 */ 43export function getDependents(signal: Dep): AnySignal[] { 44 const deps = dependents.get(signal); 45 return deps ? [...deps] : []; 46} 47 48/** 49 * Check if there's a dependency relationship between two signals. 50 */ 51export function hasDependency(dependent: AnySignal, dependency: Dep): boolean { 52 const deps = dependencies.get(dependent); 53 return deps ? deps.has(dependency) : false; 54} 55 56export function buildDependencyGraph(signals: AnySignal[]): DepGraph { 57 const nodes: GraphNode[] = []; 58 const edges: Array<{ from: string; to: string }> = []; 59 const signalToId = new Map<AnySignal, string>(); 60 61 for (const signal of signals) { 62 const metadata = getSignalMetadata(signal); 63 if (!metadata) continue; 64 65 signalToId.set(signal, metadata.id); 66 67 const deps = getDependencies(signal); 68 const depIds = deps.map((dep) => { 69 const depMetadata = getSignalMetadata(dep as AnySignal); 70 return depMetadata?.id; 71 }).filter((id): id is string => id !== undefined); 72 73 const depnts = getDependents(signal); 74 const depntIds = depnts.map((depnt) => { 75 const depntMetadata = getSignalMetadata(depnt); 76 return depntMetadata?.id; 77 }).filter((id): id is string => id !== undefined); 78 79 nodes.push({ 80 signal, 81 id: metadata.id, 82 name: metadata.name, 83 type: metadata.type, 84 value: signal.get(), 85 dependencies: depIds, 86 dependents: depntIds, 87 }); 88 } 89 90 for (const node of nodes) { 91 for (const depId of node.dependencies) { 92 edges.push({ from: depId, to: node.id }); 93 } 94 } 95 96 return { nodes, edges }; 97} 98 99export function detectCircularDependencies(signal: AnySignal): AnySignal[] | null { 100 const visited = new Set<AnySignal>(); 101 const path = new Set<AnySignal>(); 102 103 function dfs(current: AnySignal): AnySignal[] | null { 104 if (path.has(current)) { 105 return [current]; 106 } 107 108 if (visited.has(current)) { 109 return null; 110 } 111 112 visited.add(current); 113 path.add(current); 114 115 const deps = getDependencies(current); 116 for (const dep of deps) { 117 const cycle = dfs(dep as AnySignal); 118 if (cycle) { 119 cycle.push(current); 120 return cycle; 121 } 122 } 123 124 path.delete(current); 125 return null; 126 } 127 128 return dfs(signal); 129} 130 131/** 132 * Get the depth of a signal in the dependency tree. 133 * Signals with no dependencies have depth 0. 134 */ 135export function getSignalDepth(signal: AnySignal): number { 136 const visited = new Set<AnySignal>(); 137 138 function calculateDepth(current: AnySignal): number { 139 if (visited.has(current)) { 140 return 0; 141 } 142 143 visited.add(current); 144 145 const deps = getDependencies(current); 146 if (deps.length === 0) { 147 return 0; 148 } 149 150 let maxDepth = 0; 151 for (const dep of deps) { 152 const depth = calculateDepth(dep as AnySignal); 153 maxDepth = Math.max(maxDepth, depth); 154 } 155 156 return maxDepth + 1; 157 } 158 159 return calculateDepth(signal); 160} 161 162export function clearDependencyGraph(): void { 163 // WeakMaps don't have a clear method, but we can't do much here 164 // The GC will clean up when signals are no longer referenced 165}