a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
1# Debugging 2 3The VoltX.js debugging system provides introspection and visualization tools for reactive primitives. 4It's a lazy-loadable module (`voltx.js/debug`) that doesn't affect production bundle size. 5 6## Architecture 7 8The debugging system consists of three interconnected modules: 9 101. **Registry** tracks all signals and reactive objects with metadata (ID, type, name, creation timestamp). 11 Uses WeakMaps and WeakRefs to avoid memory leaks because signals can be garbage collected normally. 12 Auto-increments IDs like `signal-1`, `computed-2`, `reactive-3`. 132. **Graph** tracks dependency relationships between signals. Records which signals depend on others and enables cycle detection, depth calculation, and dependency visualization. Also uses WeakMaps to avoid memory pressure. 143. **Logger** provides console output utilities for inspecting signals, viewing dependency trees, watching value changes, and tracing updates with stack traces. 15 16## Debug API 17 18The module exports wrapped versions of core primitives that automatically register with the debug system: 19 20- `debugSignal()` creates a signal and registers it with optional name. Returns standard Signal interface. 21- `debugComputed()` creates a computed signal and registers it. Attempts to record dependency relationships (though this is currently limited by internal tracking visibility). 22- `debugReactive()` creates a reactive proxy and registers it for introspection. 23 24These wrappers are drop-in replacements for the core APIs. For existing code, use `attachDebugger()` to register signals post-creation. 25 26## The vdebugger Object 27 28All debugging utilities are also exported as methods on a single `vdebugger` namespace object: 29 30```ts 31vdebugger.signal(0, 'count') // Create debug signal 32vdebugger.getAllSignals() // Get all tracked signals 33vdebugger.log(mySignal) // Pretty-print signal info 34vdebugger.trace(mySignal) // Trace all updates 35vdebugger.watch(mySignal) // Watch with full dependency tree 36vdebugger.buildGraph(signals) // Build dependency graph 37vdebugger.detectCycles(mySignal) // Find circular dependencies 38``` 39 40This namespace provides a convenient entry point for debugging in the browser console. 41 42## Registry System 43 44The registry maintains two separate tracking systems: 45 461. **Signal Registry** uses a WeakMap to store metadata and a Set of WeakRefs to track all signals. 47 When `getAllSignals()` is called, it automatically cleans up garbage-collected signals by checking `WeakRef.deref()`. 482. **Reactive Registry** mirrors this pattern for reactive objects, storing metadata and WeakRefs separately. 49 50Metadata includes: 51 52- `id`: Unique identifier with type prefix 53- `type`: One of "signal", "computed", "reactive" 54- `name`: Optional developer-provided name 55- `createdAt`: Timestamp for age calculations 56 57The registry exposes `getSignalInfo()` and `getReactiveInfo()` which combine metadata with current value and calculated age. 58The `nameSignal()` and `nameReactive()` functions allow naming signals after creation. 59 60Registry stats can be retrieved via `getRegistryStats()` which counts regular signals, computed signals, and reactive objects. 61 62## Dependency Graph 63 64The graph module tracks relationships using two WeakMaps: 65 661. `dependencies` maps from signal to Set of signals it depends on. 672. `dependents` maps from signal to Set of signals that depend on it. 68 69When `recordDependencies()` is called, it updates both maps bidirectionally. This enables efficient queries in both directions. 70It allows you to answer, "what does this signal depend on?" and "what depends on this signal?" 71 72### Graph Operations 73 74- `buildDependencyGraph()` constructs a full graph representation with nodes and edges, suitable for visualization tools. Each node includes signal metadata, current value, and lists of dependency/dependent IDs. 75- `detectCircularDependencies()` uses depth-first search with path tracking. Returns array of signals forming the cycle, or null if no cycle exists. This helps catch bugs where signals accidentally reference themselves through intermediaries. 76- `getSignalDepth()` calculates how deep a signal is in the dependency tree. Signals with no dependencies have depth 0. Computed signals that depend on base signals have depth 1, computeds depending on other computeds have higher depths. Uses visited set to handle shared dependencies correctly. 77- `hasDependency()` checks for direct dependency relationship between two signals. 78 79The graph tracking is currently limited because dependency recording happens manually during debug signal creation. The core computed/effect tracking is internal to those primitives and not exposed to the debug system. 80 81## Logging Utilities 82 83The logger provides multiple output formats: 84 85`logSignal()` pretty-prints a signal with grouped console output showing type, current value, age in seconds, dependency count, and dependent count. If dependencies or dependents exist, it expands groups showing each one with its name and current value. 86 87`logAllSignals()` lists all tracked signals in a compact format with ID, name, and value. 88 89`logSignalTable()` outputs signals as a formatted console table with columns for ID, name, type, value (truncated), age, dependency count, and dependent count. 90 91`logReactive()` and `logAllReactives()` provide similar output for reactive objects, though reactive objects don't have dependency tracking (dependencies are on the internal signals created by the proxy). 92 93`trace()` enables update tracing for a signal. Subscribes to changes and logs each update with the new value and a stack trace showing where the update originated. Uses `Error().stack` to capture the call stack. Tracked signals are stored in a WeakSet to avoid duplicate tracing. Currently, unsubscribing is incomplete due to not storing unsubscribe functions. 94 95`watch()` subscribes to a signal and logs full information on every update, including timestamp and complete dependency tree. Returns unsubscribe function for cleanup. 96 97`enableGlobalTracing()` and `disableGlobalTracing()` enable or disable tracing for all registered signals. Useful for debugging complex reactive flows. 98 99## Usage Patterns 100 101For development, import debug utilities directly: 102 103```ts 104import { debugSignal, debugComputed, logAllSignals, buildDependencyGraph } from 'voltx.js/debug'; 105``` 106 107For debugging existing code, attach debugger to existing signals: 108 109```ts 110import { signal } from 'voltx.js'; 111import { attachDebugger, vdebugger } from 'voltx.js/debug'; 112 113const count = signal(0); 114attachDebugger(count, 'signal', 'count'); 115vdebugger.log(count); 116``` 117 118For browser console debugging, expose vdebugger globally: 119 120```ts 121import { vdebugger } from 'voltx.js/debug'; 122window.vdebugger = vdebugger; 123``` 124 125Then in console: 126 127```js 128vdebugger.logAll() 129vdebugger.trace(someSignal) 130vdebugger.buildGraph(vdebugger.getAllSignals()) 131``` 132 133## Memory Considerations 134 135All tracking uses WeakMaps and WeakRefs to prevent memory leaks. Signals and reactive objects can be garbage collected normally. The registry automatically cleans up dead WeakRefs when queried. 136 137The dependency graph also uses WeakMaps, so edges are cleaned up when signals are collected. 138 139However, tracing and watching create subscriptions which hold references to signals. Always call the returned unsubscribe function when done watching to allow cleanup. 140 141## Limitations 142 1431. Dependency recording for computed signals is incomplete. The `extractComputedDeps()` helper can't access internal dependency tracking, so dependency graph may be incomplete for debug computeds. 1442. Trace unsubscription doesn't work properly because unsubscribe functions aren't stored in the traceListeners WeakMap. 1453. Graph tracking only works for signals created via debug APIs or manually attached. Regular signals created with core APIs aren't tracked unless explicitly registered. 1464. Reactive objects are tracked as single units, but the internal per-property signals created by the proxy aren't exposed to the debug system. 147 148These limitations don't affect the core reactive system, they only reduce the visibility of the debug tools.