a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
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}