a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1/**
2 * Console logging utilities for debugging signals.
3 *
4 * Provides pretty-printed output and update tracing.
5 */
6
7import type { AnySignal } from "$types/volt";
8import { getDependencies, getDependents } from "./graph";
9import { getAllReactives, getAllSignals, getReactiveInfo, getSignalInfo } from "./registry";
10
11const trackedSignals = new WeakSet<AnySignal>();
12const traceListeners = new WeakMap<AnySignal, (value: unknown) => void>();
13
14/**
15 * Pretty-print a signal's information to the console.
16 */
17export function logSignal(signal: AnySignal): void {
18 const info = getSignalInfo(signal);
19 if (!info) {
20 console.log("[Volt Debug] Unregistered signal");
21 return;
22 }
23
24 const deps = getDependencies(signal);
25 const depnts = getDependents(signal);
26
27 console.group(`[Volt Signal] ${info.name || info.id}`);
28 console.log("Type:", info.type);
29 console.log("Value:", info.value);
30 console.log("Age:", `${(info.age / 1000).toFixed(2)}s`);
31 console.log("Dependencies:", deps.length);
32 console.log("Dependents:", depnts.length);
33
34 if (deps.length > 0) {
35 console.group("Depends on:");
36 for (const dep of deps) {
37 const depInfo = getSignalInfo(dep as AnySignal);
38 if (depInfo) {
39 console.log(` - ${depInfo.name || depInfo.id} = ${depInfo.value}`);
40 }
41 }
42 console.groupEnd();
43 }
44
45 if (depnts.length > 0) {
46 console.group("Dependents:");
47 for (const depnt of depnts) {
48 const depntInfo = getSignalInfo(depnt);
49 if (depntInfo) {
50 console.log(` - ${depntInfo.name || depntInfo.id}`);
51 }
52 }
53 console.groupEnd();
54 }
55
56 console.groupEnd();
57}
58
59export function logAllSignals(): void {
60 const signals = getAllSignals();
61 console.group(`[Volt Debug] All Signals (${signals.length})`);
62
63 for (const signal of signals) {
64 const info = getSignalInfo(signal);
65 if (info) {
66 console.log(`${info.id.padEnd(15)} ${(info.name || "unnamed").padEnd(20)} ${String(info.value)}`);
67 }
68 }
69
70 console.groupEnd();
71}
72
73/**
74 * Pretty-print a reactive object's information to the console.
75 */
76export function logReactive(obj: object): void {
77 const info = getReactiveInfo(obj);
78 if (!info) {
79 console.log("[Volt Debug] Unregistered reactive object");
80 return;
81 }
82
83 console.group(`[Volt Reactive] ${info.name || info.id}`);
84 console.log("Type:", info.type);
85 console.log("Value:", info.value);
86 console.log("Age:", `${(info.age / 1000).toFixed(2)}s`);
87 console.groupEnd();
88}
89
90export function logAllReactives(): void {
91 const reactives = getAllReactives();
92 console.group(`[Volt Debug] All Reactive Objects (${reactives.length})`);
93
94 for (const obj of reactives) {
95 const info = getReactiveInfo(obj);
96 if (info) {
97 console.log(`${info.id.padEnd(15)} ${(info.name || "unnamed").padEnd(20)} ${JSON.stringify(info.value)}`);
98 }
99 }
100
101 console.groupEnd();
102}
103
104export function trace(signal: AnySignal, enabled = true): void {
105 if (!enabled) {
106 const listener = traceListeners.get(signal);
107 if (listener) {
108 // Can't unsubscribe without keeping the unsubscribe function
109 // TODO: we need to store unsubscribe functions
110 trackedSignals.delete(signal);
111 traceListeners.delete(signal);
112 }
113 return;
114 }
115
116 if (trackedSignals.has(signal)) {
117 return;
118 }
119
120 const info = getSignalInfo(signal);
121 const name = info?.name || info?.id || "unknown";
122
123 const listener = (value: unknown) => {
124 const stack = new Error("Listener").stack;
125 const caller = stack?.split("\n")[3]?.trim();
126
127 console.log(`[Volt Trace] ${name} changed:`, value, caller ? `(from ${caller})` : "");
128 };
129
130 signal.subscribe(listener);
131 traceListeners.set(signal, listener);
132 trackedSignals.add(signal);
133
134 console.log(`[Volt Debug] Tracing enabled for ${name}`);
135}
136
137export function enableGlobalTracing(): void {
138 const signals = getAllSignals();
139 console.log(`[Volt Debug] Enabling global tracing for ${signals.length} signals`);
140
141 for (const signal of signals) {
142 trace(signal, true);
143 }
144}
145
146export function disableGlobalTracing(): void {
147 const signals = getAllSignals();
148 for (const signal of signals) {
149 trace(signal, false);
150 }
151
152 console.log("[Volt Debug] Global tracing disabled");
153}
154
155export function logSignalTable(): void {
156 const signals = getAllSignals();
157 const data = signals.map((signal) => {
158 const info = getSignalInfo(signal);
159 if (!info) return null;
160
161 return {
162 ID: info.id,
163 Name: info.name || "(unnamed)",
164 Type: info.type,
165 Value: String(info.value).slice(0, 50),
166 "Age (s)": (info.age / 1000).toFixed(2),
167 Dependencies: getDependencies(signal).length,
168 Dependents: getDependents(signal).length,
169 };
170 }).filter((row): row is NonNullable<typeof row> => row !== null);
171
172 console.table(data);
173}
174
175export function watch(signal: AnySignal): () => void {
176 const info = getSignalInfo(signal);
177 const name = info?.name || info?.id || "unknown";
178
179 console.log(`[Volt Debug] Watching ${name}`);
180
181 const unsubscribe = signal.subscribe((value) => {
182 const timestamp = new Date().toISOString();
183 console.group(`[Volt Watch] ${name} updated at ${timestamp}`);
184 console.log("New value:", value);
185 logSignal(signal);
186 console.groupEnd();
187 });
188
189 return () => {
190 console.log(`[Volt Debug] Stopped watching ${name}`);
191 unsubscribe();
192 };
193}