a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1/**
2 * Dependency tracking system for automatic signal dependency detection.
3 *
4 * Uses a stack-based tracking context to record signal accesses during computations.
5 * When a computed signal or effect runs, it pushes a tracking context onto the stack.
6 * Any signal.get() calls during execution are recorded as dependencies.
7 */
8
9import type { Dep } from "$types/volt";
10
11/**
12 * Holds the set of dependencies discovered during this tracking session and the source being tracked (to prevent cycles)
13 */
14type TrackingContext = { deps: Set<Dep>; source?: Dep };
15
16/**
17 * Global stack of active tracking contexts.
18 * When nested computeds run, multiple contexts can be active simultaneously.
19 */
20const trackingStack: TrackingContext[] = [];
21
22/**
23 * Get the currently active tracking context, if any.
24 */
25function getActiveContext(): TrackingContext | undefined {
26 return trackingStack.at(-1);
27}
28
29/**
30 * Start tracking signal dependencies.
31 * Should be called before executing a computation function.
32 *
33 * @param source - Optional source signal for cycle detection
34 * @returns The tracking context
35 */
36export function startTracking(source?: Dep): TrackingContext {
37 const context: TrackingContext = { deps: new Set(), source };
38
39 trackingStack.push(context);
40 return context;
41}
42
43/**
44 * Stop tracking and return the collected dependencies.
45 * Should be called after executing a computation function.
46 *
47 * @returns Array of signals that were accessed during tracking
48 */
49export function stopTracking(): Dep[] {
50 const context = trackingStack.pop();
51 if (!context) {
52 console.warn("stopTracking called without matching startTracking");
53 return [];
54 }
55
56 return [...context.deps];
57}
58
59/**
60 * Record a signal access as a dependency.
61 * Called by signal.get() when inside a tracking context.
62 *
63 * @param dep - The signal being accessed
64 */
65export function recordDep(dep: Dep): void {
66 const context = getActiveContext();
67 if (!context) {
68 return;
69 }
70
71 if (context.source === dep) {
72 throw new Error("Circular dependency detected: a signal cannot depend on itself");
73 }
74
75 context.deps.add(dep);
76}
77
78/**
79 * Check if currently inside a tracking context.
80 * Useful for conditional behavior in signal.get()
81 */
82export function isTracking(): boolean {
83 return trackingStack.length > 0;
84}
85
86/**
87 * Get current tracking depth (for debugging).
88 */
89export function getTrackingDepth(): number {
90 return trackingStack.length;
91}