a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: logging & proxied state

* tests for debugging utilities

+1718 -68
+55 -66
ROADMAP.md
··· 15 15 | v0.1.0 | ✓ | [Markup Based Reactivity](#markup-based-reactivity) | Allow users to write apps without any bundled JS | 16 16 | v0.2.0 | | [Animation & Transitions](#animation--transitions) | Declarative animation layer and browser View Transition API integration. | 17 17 | v0.3.0 | | [Inspector & Developer Tools](#inspector--developer-tools) | Built-in signal inspector, debug overlays, and dev tooling. | 18 - | v0.4.0 | | [Docs & Stability](#documentation--stability-pass) | Comprehensive docs, tests, and performance review. | 19 - | v0.5.0 | | PWA Capabilities | TODO | 20 - | v1.0.0 | | [Release](#stable-release) | Public API freeze, plugin registry, and versioned documentation. | 18 + | v0.4.0 | | PWA Capabilities | TODO | 19 + | v1.0.0 | | [Release](#stable-release) | Public API freeze, plugin registry, comprehensive docs & tests. | 21 20 22 21 ## Completed 23 22 ··· 117 116 118 117 ## To-Do 119 118 120 - ### Streaming & Patch Engine 121 - 122 - **Goal:** Enable real-time updates via SSE/WebSocket streaming with intelligent DOM patching. 123 - **Outcome:** Volt.js can receive and apply live updates from the server 124 - **Deliverables:** 125 - - Server-Sent Events (SSE) integration 126 - - `data-volt-stream` attribute for SSE endpoints 127 - - Signal patching from backend (`data-signals-*` merge system) 128 - - Backend action system with `$$action()` syntax (TBD on final syntax decision) 129 - - JSON Patch parser and DOM morphing engine 130 - - WebSocket as alternative to SSE 131 - - `data-volt-ignore-morph` for selective patch exclusion 132 - 133 - ### Persistence & Offline 134 - 135 - **Goal:** Introduce persistent storage and offline-first behaviors. 136 - **Outcome:** Resilient state persistence and offline replay built into Volt.js. 137 - **Deliverables:** 138 - - ✓ Persistent signals (localStorage, sessionStorage, indexedDb) 139 - - ✓ Storage plugin (`data-volt-persist`) 140 - - Storage modifiers on signals: 141 - - `.local` modifier for localStorage persistence 142 - - `.session` modifier for sessionStorage persistence 143 - - `.ifmissing` modifier for conditional initialization 144 - - Offline queue for deferred stream events and HTTP requests 145 - - Sync strategy API (merge, overwrite, patch) for conflict resolution 146 - - Service Worker integration for offline-first apps 147 - - Background sync for deferred requests 148 - - Cache invalidation strategies 149 - - Cross-tab synchronization via `BroadcastChannel` 150 - 151 119 ### Reactive Attributes & Event Modifiers 152 120 153 121 **Goal:** Extend Volt.js with expressive attribute patterns and event options for fine-grained control. 154 122 **Outcome:** Volt.js supports rich declarative behaviors and event semantics built entirely on standard DOM APIs. 155 123 **Deliverables:** 156 - - `data-volt-show` — toggles element visibility via CSS rather than DOM removal (complements `data-volt-if`) 157 - - `data-volt-style` — binds inline styles to reactive expressions 158 - - `data-volt-skip` — marks elements or subtrees to exclude from Volt’s reactive parsing 159 - - `data-volt-cloak` — hides content until the Volt runtime initializes 124 + - `data-volt-show` - toggles element visibility via CSS rather than DOM removal (complements `data-volt-if`) 125 + - `data-volt-style` - binds inline styles to reactive expressions 126 + - `data-volt-skip` - marks elements or subtrees to exclude from Volt’s reactive parsing 127 + - `data-volt-cloak` - hides content until the Volt runtime initializes 160 128 - Event options for `data-volt-on-*` attributes: 161 - - `.prevent` — calls `preventDefault()` on the event 162 - - `.stop` — stops propagation 163 - - `.self` — triggers only when the event target is the bound element 164 - - `.window` — attaches the listener to `window` 165 - - `.document` — attaches the listener to `document` 166 - - `.once` — runs the handler a single time 167 - - `.debounce` — defers handler execution (optional milliseconds) 168 - - `.throttle` — limits handler frequency (optional milliseconds) 169 - - `.passive` — adds a passive event listener for scroll/touch performance 129 + - `.prevent` - calls `preventDefault()` on the event 130 + - `.stop` - stops propagation 131 + - `.self` - triggers only when the event target is the bound element 132 + - `.window` - attaches the listener to `window` 133 + - `.document` - attaches the listener to `document` 134 + - `.once` - runs the handler a single time 135 + - `.debounce` - defers handler execution (optional milliseconds) 136 + - `.throttle` - limits handler frequency (optional milliseconds) 137 + - `.passive` - adds a passive event listener for scroll/touch performance 170 138 - Input options for `data-volt-bind` and `data-volt-model`: 171 - - `.number` — coerces values to numbers 172 - - `.trim` — removes surrounding whitespace 173 - - `.lazy` — syncs only on `change` instead of `input` 174 - - `.debounce` — delays updates to reduce jitter 139 + - `.number` - coerces values to numbers 140 + - `.trim` - removes surrounding whitespace 141 + - `.lazy` - syncs only on `change` instead of `input` 142 + - `.debounce` - delays updates to reduce jitter 175 143 176 144 ### Global State 177 145 ··· 206 174 - Timing utilities and easing functions 207 175 - Integration with `data-volt-if` and `data-volt-show` for automatic transitions 208 176 177 + ### Streaming & Patch Engine 178 + 179 + **Goal:** Enable real-time updates via SSE/WebSocket streaming with intelligent DOM patching. 180 + **Outcome:** Volt.js can receive and apply live updates from the server 181 + **Deliverables:** 182 + - Server-Sent Events (SSE) integration 183 + - `data-volt-stream` attribute for SSE endpoints 184 + - Signal patching from backend (`data-signals-*` merge system) 185 + - Backend action system with `$$action()` syntax (TBD on final syntax decision) 186 + - JSON Patch parser and DOM morphing engine 187 + - WebSocket as alternative to SSE 188 + - `data-volt-ignore-morph` for selective patch exclusion 189 + 190 + ### Persistence & Offline 191 + 192 + **Goal:** Introduce persistent storage and offline-first behaviors. 193 + **Outcome:** Resilient state persistence and offline replay built into Volt.js. 194 + **Deliverables:** 195 + - ✓ Persistent signals (localStorage, sessionStorage, indexedDb) 196 + - ✓ Storage plugin (`data-volt-persist`) 197 + - Storage modifiers on signals: 198 + - `.local` modifier for localStorage persistence 199 + - `.session` modifier for sessionStorage persistence 200 + - `.ifmissing` modifier for conditional initialization 201 + - Offline queue for deferred stream events and HTTP requests 202 + - Sync strategy API (merge, overwrite, patch) for conflict resolution 203 + - Service Worker integration for offline-first apps 204 + - Background sync for deferred requests 205 + - Cache invalidation strategies 206 + - Cross-tab synchronization via `BroadcastChannel` 207 + 209 208 ### Background Requests & Reactive Polling 210 209 211 210 **Goal:** Enable declarative background data fetching and periodic updates within the Volt.js runtime. ··· 242 241 - Developer overlay for inspecting signals, subscriptions, and effects 243 242 - Dev logging toggle (`Volt.debug = true`) 244 243 - Browser console integration (`window.$volt.inspect()`) 245 - - Signal dependency graph visualization 244 + - Signal dependency graph visualization (graph data structure implemented in [proxy](#proxy-based-reactivity-enhancements) milestone) 246 245 - Performance profiling tools 247 246 - Request/response debugging (HTTP actions, SSE streams) 248 247 - Time-travel debugging for signal history 249 248 - Browser DevTools extension 250 249 251 - ### Documentation & Stability Pass 252 - 253 - **Goal:** Prepare for stable release by finalizing docs, polish, and performance. 254 - **Outcome:** Volt.js is stable, documented, performant, and ready for production. 255 - **Deliverables:** 256 - - ✓ Documentation site (VitePress) 257 - - Full API reference with examples 258 - - Performance benchmarks (vs htmx, Alpine) 259 - - Browser matrix tests (Chromium, Gecko, WebKit) 260 - - Accessibility audits (ARIA) 261 - - Freeze API surface for 1.0 262 - 263 250 ### Stable Release 264 251 265 - **Goal:** Ship the first stable version of Volt.js 266 - **Outcome:** Volt.js 1.0 is released as a mature, fully documented, type-safe, reactive web framework 252 + **Goal:** Prepare & ship the stable release 253 + **Outcome:** Volt.js 1.0 is stable, documented, performant, and ready for production. 267 254 **Deliverables:** 255 + - ✓ Documentation site (VitePress) 256 + - Full API reference with examples -> refactor generator in `@volt/dev` package 268 257 - Finalized plugin registry and CLI (`volt plugins list/init`) 269 258 - Versioned documentation (stormlightlabs.github.io/volt) 270 259 - Announcement post and release notes
+148
docs/internals/debugging.md
··· 1 + # Debugging 2 + 3 + The Volt.js debugging system provides introspection and visualization tools for reactive primitives. 4 + It's a lazy-loadable module (`volt/debug`) that doesn't affect production bundle size. 5 + 6 + ## Architecture 7 + 8 + The debugging system consists of three interconnected modules: 9 + 10 + 1. **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`. 13 + 2. **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. 14 + 3. **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 + 18 + The 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 + 24 + These 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 + 28 + All debugging utilities are also exported as methods on a single `vdebugger` namespace object: 29 + 30 + ```ts 31 + vdebugger.signal(0, 'count') // Create debug signal 32 + vdebugger.getAllSignals() // Get all tracked signals 33 + vdebugger.log(mySignal) // Pretty-print signal info 34 + vdebugger.trace(mySignal) // Trace all updates 35 + vdebugger.watch(mySignal) // Watch with full dependency tree 36 + vdebugger.buildGraph(signals) // Build dependency graph 37 + vdebugger.detectCycles(mySignal) // Find circular dependencies 38 + ``` 39 + 40 + This namespace provides a convenient entry point for debugging in the browser console. 41 + 42 + ## Registry System 43 + 44 + The registry maintains two separate tracking systems: 45 + 46 + 1. **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()`. 48 + 2. **Reactive Registry** mirrors this pattern for reactive objects, storing metadata and WeakRefs separately. 49 + 50 + Metadata 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 + 57 + The registry exposes `getSignalInfo()` and `getReactiveInfo()` which combine metadata with current value and calculated age. 58 + The `nameSignal()` and `nameReactive()` functions allow naming signals after creation. 59 + 60 + Registry stats can be retrieved via `getRegistryStats()` which counts regular signals, computed signals, and reactive objects. 61 + 62 + ## Dependency Graph 63 + 64 + The graph module tracks relationships using two WeakMaps: 65 + 66 + 1. `dependencies` maps from signal to Set of signals it depends on. 67 + 2. `dependents` maps from signal to Set of signals that depend on it. 68 + 69 + When `recordDependencies()` is called, it updates both maps bidirectionally. This enables efficient queries in both directions. 70 + It 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 + 79 + The 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 + 83 + The 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 + 101 + For development, import debug utilities directly: 102 + 103 + ```ts 104 + import { debugSignal, debugComputed, logAllSignals, buildDependencyGraph } from 'volt/debug'; 105 + ``` 106 + 107 + For debugging existing code, attach debugger to existing signals: 108 + 109 + ```ts 110 + import { signal } from 'volt'; 111 + import { attachDebugger, vdebugger } from 'volt/debug'; 112 + 113 + const count = signal(0); 114 + attachDebugger(count, 'signal', 'count'); 115 + vdebugger.log(count); 116 + ``` 117 + 118 + For browser console debugging, expose vdebugger globally: 119 + 120 + ```ts 121 + import { vdebugger } from 'volt/debug'; 122 + window.vdebugger = vdebugger; 123 + ``` 124 + 125 + Then in console: 126 + 127 + ```js 128 + vdebugger.logAll() 129 + vdebugger.trace(someSignal) 130 + vdebugger.buildGraph(vdebugger.getAllSignals()) 131 + ``` 132 + 133 + ## Memory Considerations 134 + 135 + All 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 + 137 + The dependency graph also uses WeakMaps, so edges are cleaned up when signals are collected. 138 + 139 + However, 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 + 143 + 1. 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. 144 + 2. Trace unsubscription doesn't work properly because unsubscribe functions aren't stored in the traceListeners WeakMap. 145 + 3. 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. 146 + 4. 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 + 148 + These limitations don't affect the core reactive system, they only reduce the visibility of the debug tools.
+205
docs/internals/proxies.md
··· 1 1 # Proxy Objects 2 + 3 + Volt's reactive proxy system implements deep reactivity for objects and arrays. Unlike `signal()` which wraps a single value, `reactive()` creates a transparent proxy where property access feels natural while maintaining full reactivity. 4 + 5 + ## Core Design 6 + 7 + The reactive system uses JavaScript Proxies to intercept property access and mutations. 8 + Each reactive proxy is backed by the original raw object and a map of signals: one signal per property, created lazily on first access. 9 + 10 + This design provides: 11 + 12 + **Transparency**: Access `obj.count` instead of `obj.count.get()`. The proxy unwraps signals automatically. 13 + 14 + **Deep Reactivity**: Nested objects and arrays are recursively wrapped, so `obj.nested.value` is reactive all the way down. 15 + 16 + **Lazy Signals**: Signals are only created when properties are accessed. An object with 100 properties only creates signals for the properties you actually use. 17 + 18 + **Native Array Methods**: Array mutators like `push`, `pop`, `splice` work naturally and trigger updates correctly. 19 + 20 + ## Three WeakMaps 21 + 22 + The system uses three WeakMaps to track relationships: 23 + 24 + 1. `reactiveToRaw` maps from reactive proxy to original object. Used by `toRaw()` to unwrap proxies and by `isReactive()` to check if something is already a proxy. 25 + 2. `rawToReactive` maps from original object to reactive proxy. 26 + This ensures only one proxy per object. Calling `reactive()` twice on the same object returns the same proxy instance. 27 + 3. `targetToSignals` maps from target object to a Map of property signals. 28 + Each target has its own Map of `(key: string | symbol) -> Signal`. Signals are created lazily in `getPropertySignal()`. 29 + 30 + WeakMaps allow us to avoid preventing garbage collection of proxies or targets. 31 + 32 + ## Property Access (get trap) 33 + 34 + When you access `proxy.count`, the get trap: 35 + 36 + 1. Checks for special keys: `__v_raw` returns the raw target, `__v_isReactive` returns true. These enable introspection. 37 + 2. For arrays, checks if the key is a mutator method (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, `copyWithin`). 38 + If so, returns a wrapper function that applies the mutation to the target, then updates all affected index signals and the length signal. 39 + 3. Gets or creates the signal for this property via `getPropertySignal()`. 40 + 4. Calls `recordDep(sig)` to track this access for dependency tracking (if inside a computed or effect). 41 + 5. Gets the actual value using `Reflect.get()`. 42 + 6. If the value is an object, wraps it recursively with `reactive()` before returning. This provides deep reactivity. 43 + 7. If the value is not an object, returns `sig.get()` which provides the signal's current value. 44 + 45 + The subtle difference in step 6-7 is important: for objects, we return the reactive proxy (not the signal value), so you get `obj.nested.prop` not `obj.nested.get().prop`. But dependency tracking still happens because we called `recordDep()` in step 4. 46 + 47 + ## Property Mutation (set trap) 48 + 49 + When you assign `proxy.count = 5`, the set trap: 50 + 51 + 1. Reads the old value via `Reflect.get()`. 52 + 2. Performs the actual mutation with `Reflect.set()`. 53 + 3. If the value changed (old !== new), gets the property signal and calls `sig.set(value)`. 54 + 4. Returns the result from `Reflect.set()` to indicate success. 55 + 56 + This ensures that mutations to the raw object and signal notifications happen atomically. 57 + 58 + ## Property Deletion (deleteProperty trap) 59 + 60 + When you `delete proxy.count`: 61 + 62 + 1. Checks if the property existed with `Reflect.has()`. 63 + 2. Performs the deletion with `Reflect.deleteProperty()`. 64 + 3. If the property existed and deletion succeeded, gets the property signal and sets it to `undefined`. 65 + 4. Returns the deletion result. 66 + 67 + Setting the signal to `undefined` ensures any computeds or effects depending on that property get notified about the deletion. 68 + 69 + ## Property Existence (has trap) 70 + 71 + The `in` operator (`'count' in proxy`) goes through the has trap: 72 + 73 + 1. Gets the property signal (creating it if needed). 74 + 2. Records the dependency with `recordDep()`. 75 + 3. Returns `Reflect.has()` result. 76 + 77 + This makes property existence checks reactive. If you have a computed like `() => 'count' in state`, it will rerun if that property is added or deleted. 78 + 79 + ## Array Mutators 80 + 81 + Arrays get special handling because native methods like `push` mutate multiple indices simultaneously. The array mutator wrapper: 82 + 83 + 1. Gets the array method from the target (e.g., `Array.prototype.push`). 84 + 2. Records the old length. 85 + 3. Applies the method to the target with the provided arguments. 86 + 4. Calculates the new length and determines the maximum index that might have changed. 87 + 5. Loops through all potentially affected indices, gets/creates their signals, and updates them with the new values from the target. 88 + 6. If the length changed, updates the length signal. 89 + 7. Returns the result from the native method. 90 + 91 + This approach ensures that mutations like `arr.splice(1, 2, 'a', 'b', 'c')` correctly update all affected index signals and the length signal, triggering computeds that depend on those indices. 92 + 93 + The mutator list is hardcoded as a Set: `['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin']`. 94 + Array methods that don't mutate (like `map`, `filter`, `slice`, `toSorted`) go through the normal get trap and return the native method bound to the target. 95 + 96 + ## Signal Creation 97 + 98 + The `getPropertySignal()` helper: 99 + 100 + 1. Gets the Map of signals for this target from `targetToSignals`, or creates one if needed. 101 + 2. Looks up the signal for this specific key in the Map. 102 + 3. If no signal exists, reads the initial value from the target with `Reflect.get()`, creates a new signal with that value, and stores it in the Map. 103 + 4. Returns the signal. 104 + 105 + This lazy creation means that accessing a property for the first time has a small overhead (creating the signal and Map entries), but subsequent accesses just look up the existing signal. 106 + 107 + ## Integration with Signal System 108 + 109 + The reactive proxy system sits on top of the signal system. Each reactive property is backed by a signal created via `signal()` from the core signal module. 110 + When you access `proxy.count`, you're actually calling `get()` on the signal for the 'count' property. When you assign `proxy.count = 5`, you're calling `set(5)` on that signal. 111 + This means all the signal behavior works: dependency tracking via `recordDep()`, subscriber notifications, equality checks (`value === newValue` to prevent unnecessary updates). 112 + The proxy just provides a more convenient API so you'll use something like `obj.count++` instead of `obj.count.set(obj.count.get() + 1)`. 113 + 114 + ## Deep Reactivity 115 + 116 + Nested objects are made reactive recursively. When the get trap encounters an object value, it wraps it with `reactive()` before returning. 117 + 118 + The `reactive()` function itself checks `rawToReactive` first, so if that nested object was already wrapped, it returns the existing proxy. This means: 119 + 120 + ```ts 121 + const state = reactive({ nested: { count: 0 } }); 122 + const nested1 = state.nested; 123 + const nested2 = state.nested; 124 + nested1 === nested2; // true, same proxy 125 + ``` 126 + 127 + Arrays within reactive objects are also proxied, and arrays containing objects have those objects proxied when accessed: 128 + 129 + ```ts 130 + const state = reactive({ items: [{ id: 1 }, { id: 2 }] }); 131 + state.items[0].id = 3; // fully reactive 132 + state.items.push({ id: 4 }); // also reactive 133 + ``` 134 + 135 + ## Unwrapping with toRaw 136 + 137 + The `toRaw()` function unwraps a proxy to get the original object. It checks if the value is an object, then looks it up in `reactiveToRaw`. If not found, returns the value as-is (it wasn't a proxy). 138 + 139 + This is useful when you need to pass the raw object to third-party libraries that might not handle proxies well, or when you want to do mutations without triggering reactivity (though this is rarely needed). 140 + 141 + ## Checking Reactivity 142 + 143 + The `isReactive()` function checks if a value is a reactive proxy by testing if it's an object and exists in `reactiveToRaw`. 144 + This is simpler than checking for the `__v_isReactive` property because it doesn't trigger the get trap. 145 + 146 + ## Type Safety 147 + 148 + TypeScript types flow through transparently. If you pass `{ count: number }` to `reactive()`, you get back a reactive object typed as `{ count: number }`. 149 + The proxy is invisible to the type system. This is possible because Proxies preserve the object's interface keeping all properties and methods accessible with the same types. 150 + 151 + ## Performance Characteristics 152 + 153 + - Creating a reactive proxy has minimal overhead, as it just creates the Proxy object and adding two WeakMap entries. 154 + - The first property access has the overhead of creating a signal and Map entries. 155 + - Subsequent property access is very fast 156 + - WeakMap lookup for the signal Map, then Map lookup for the signal, then signal.get(). 157 + - Mutations are similarly fast 158 + - WeakMap lookups plus signal.set(). 159 + - The WeakMaps have zero memory overhead for garbage collection so when a proxy is no longer referenced, all its metadata is collected automatically. 160 + - Arrays are slightly slower due to the mutator wrappers, but still $O(n)$ in the number of affected elements, which matches the native method's complexity. 161 + 162 + ## Edge Cases 163 + 164 + **Non-object values**: `reactive()` logs a warning and returns the value unchanged. 165 + You can't make primitives reactive without wrapping them in a `signal()`. 166 + 167 + **Already reactive**: Calling `reactive()` on a proxy returns the same proxy immediately. 168 + 169 + **Prototype pollution**: The proxy traps use `Reflect` methods which respect the prototype chain naturally. 170 + No special protection is needed because signals are stored in a separate WeakMap, not on the object itself. 171 + 172 + **Symbol properties**: Fully supported. Symbols can be used as property keys and get their own signals just like string keys. 173 + 174 + **Non-enumerable properties**: Work correctly. The get/set traps handle them the same as enumerable properties. 175 + 176 + **Frozen/sealed objects**: Setting properties on frozen objects will fail in `Reflect.set()` and the set trap will return false. 177 + The signal won't be updated, maintaining consistency. 178 + 179 + ## Signal vs. Reactive 180 + 181 + Use `signal()` when: 182 + 183 + - You have a single primitive value 184 + - You want explicit get/set calls 185 + - You're storing a function (functions can't be proxy targets) 186 + 187 + Use `reactive()` when: 188 + 189 + - You have an object with multiple properties 190 + - You want natural property access syntax 191 + - You have nested objects or arrays 192 + - You're integrating with code that expects plain objects 193 + 194 + Both are backed by the same signal primitive. The choice is about API ergonomics, not capability. 195 + 196 + ## Implementation Notes 197 + 198 + The proxy system is implemented in `lib/src/core/reactive.ts` and depends only on `signal()` and `recordDep()` from the tracker. 199 + It's completely independent of the binding system, expression evaluator, and other framework features. 200 + 201 + This separation means you can use reactive objects in any context: 202 + - In the DOM binding system via `data-volt-*` attributes 203 + - Programmatic code with `mount()` 204 + - Standalone without any UI at all. 205 + 206 + The declarative `data-volt-state` attribute in the charge system creates reactive objects from JSON, making deep reactivity available in fully declarative apps without writing any JavaScript.
+282
lib/test/debug/debug.test.ts
··· 1 + import { reactive } from "$core/reactive"; 2 + import { getAllSignals, getReactiveInfo, getSignalInfo } from "$debug/registry"; 3 + import { attachDebugger, debugComputed, debugReactive, debugSignal, vdebugger } from "$vebug"; 4 + import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 5 + 6 + describe("debug API", () => { 7 + let _consoleSpy: { log: ReturnType<typeof vi.spyOn> }; 8 + 9 + beforeEach(() => { 10 + vdebugger.clear(); 11 + _consoleSpy = { log: vi.spyOn(console, "log").mockImplementation(() => {}) }; 12 + }); 13 + 14 + afterEach(() => { 15 + vi.restoreAllMocks(); 16 + }); 17 + 18 + describe("debugSignal", () => { 19 + it("creates a signal and registers it", () => { 20 + const sig = debugSignal(42, "answer"); 21 + expect(sig.get()).toBe(42); 22 + 23 + const info = getSignalInfo(sig); 24 + expect(info).toBeDefined(); 25 + expect(info?.name).toBe("answer"); 26 + expect(info?.type).toBe("signal"); 27 + }); 28 + 29 + it("works without a name", () => { 30 + const sig = debugSignal(0); 31 + expect(sig.get()).toBe(0); 32 + 33 + const info = getSignalInfo(sig); 34 + expect(info).toBeDefined(); 35 + expect(info?.name).toBeUndefined(); 36 + }); 37 + 38 + it("returns standard Signal interface", () => { 39 + const sig = debugSignal(0); 40 + expect(sig.get).toBeTypeOf("function"); 41 + expect(sig.set).toBeTypeOf("function"); 42 + expect(sig.subscribe).toBeTypeOf("function"); 43 + 44 + sig.set(5); 45 + expect(sig.get()).toBe(5); 46 + 47 + const callback = vi.fn(); 48 + const unsubscribe = sig.subscribe(callback); 49 + sig.set(10); 50 + expect(callback).toHaveBeenCalledWith(10); 51 + 52 + unsubscribe(); 53 + }); 54 + }); 55 + 56 + describe("debugComputed", () => { 57 + it("creates a computed signal and registers it", () => { 58 + const count = debugSignal(5); 59 + const doubled = debugComputed(() => count.get() * 2, "doubled"); 60 + expect(doubled.get()).toBe(10); 61 + 62 + const info = getSignalInfo(doubled); 63 + expect(info).toBeDefined(); 64 + expect(info?.name).toBe("doubled"); 65 + expect(info?.type).toBe("computed"); 66 + }); 67 + 68 + it("works without a name", () => { 69 + const count = debugSignal(5); 70 + const doubled = debugComputed(() => count.get() * 2); 71 + expect(doubled.get()).toBe(10); 72 + 73 + const info = getSignalInfo(doubled); 74 + expect(info).toBeDefined(); 75 + }); 76 + 77 + it("recomputes when dependencies change", () => { 78 + const count = debugSignal(5); 79 + const doubled = debugComputed(() => count.get() * 2); 80 + expect(doubled.get()).toBe(10); 81 + 82 + count.set(10); 83 + expect(doubled.get()).toBe(20); 84 + }); 85 + }); 86 + 87 + describe("debugReactive", () => { 88 + it("creates a reactive object and registers it", () => { 89 + const state = debugReactive({ count: 42 }, "state"); 90 + expect(state.count).toBe(42); 91 + 92 + const info = getReactiveInfo(state); 93 + expect(info).toBeDefined(); 94 + expect(info?.name).toBe("state"); 95 + expect(info?.type).toBe("reactive"); 96 + }); 97 + 98 + it("works without a name", () => { 99 + const state = debugReactive({ count: 0 }); 100 + expect(state.count).toBe(0); 101 + 102 + const info = getReactiveInfo(state); 103 + expect(info).toBeDefined(); 104 + }); 105 + 106 + it("maintains reactivity", () => { 107 + const state = debugReactive({ count: 0 }); 108 + const doubled = debugComputed(() => state.count * 2); 109 + expect(doubled.get()).toBe(0); 110 + 111 + state.count = 5; 112 + expect(doubled.get()).toBe(10); 113 + }); 114 + }); 115 + 116 + describe("attachDebugger", () => { 117 + it("registers existing signal", () => { 118 + const sig = debugSignal(0); 119 + const info = getSignalInfo(sig); 120 + expect(info).toBeDefined(); 121 + }); 122 + 123 + it("does not re-register already registered signal", () => { 124 + const sig = debugSignal(0, "original"); 125 + attachDebugger(sig, "signal", "new"); 126 + 127 + const info = getSignalInfo(sig); 128 + expect(info?.name).toBe("original"); 129 + }); 130 + }); 131 + 132 + describe("vdebugger namespace", () => { 133 + it("provides signal creation", () => { 134 + const sig = vdebugger.signal(42, "answer"); 135 + expect(sig.get()).toBe(42); 136 + 137 + const info = getSignalInfo(sig); 138 + expect(info?.name).toBe("answer"); 139 + }); 140 + 141 + it("provides computed creation", () => { 142 + const count = vdebugger.signal(5); 143 + const doubled = vdebugger.computed(() => count.get() * 2, "doubled"); 144 + expect(doubled.get()).toBe(10); 145 + }); 146 + 147 + it("provides reactive creation", () => { 148 + const state = vdebugger.reactive({ count: 0 }, "state"); 149 + expect(state.count).toBe(0); 150 + }); 151 + 152 + it("provides getAllSignals", () => { 153 + const sig1 = vdebugger.signal(1); 154 + const sig2 = vdebugger.signal(2); 155 + const all = vdebugger.getAllSignals(); 156 + expect(all).toHaveLength(2); 157 + expect(all).toContain(sig1); 158 + expect(all).toContain(sig2); 159 + }); 160 + 161 + it("provides getAllReactives", () => { 162 + const obj1 = vdebugger.reactive({ a: 1 }); 163 + const obj2 = vdebugger.reactive({ b: 2 }); 164 + const all = vdebugger.getAllReactives(); 165 + expect(all).toHaveLength(2); 166 + expect(all).toContain(obj1); 167 + expect(all).toContain(obj2); 168 + }); 169 + 170 + it("provides getSignalInfo", () => { 171 + const sig = vdebugger.signal(42, "answer"); 172 + const info = vdebugger.getSignalInfo(sig); 173 + expect(info).toBeDefined(); 174 + expect(info?.name).toBe("answer"); 175 + expect(info?.value).toBe(42); 176 + }); 177 + 178 + it("provides getReactiveInfo", () => { 179 + const obj = vdebugger.reactive({ count: 42 }, "state"); 180 + const info = vdebugger.getReactiveInfo(obj); 181 + expect(info).toBeDefined(); 182 + expect(info?.name).toBe("state"); 183 + }); 184 + 185 + it("provides stats", () => { 186 + vdebugger.signal(1); 187 + vdebugger.signal(2); 188 + vdebugger.computed(() => 3); 189 + vdebugger.reactive({}); 190 + 191 + const stats = vdebugger.getStats(); 192 + expect(stats.totalSignals).toBe(3); 193 + expect(stats.regularSignals).toBe(2); 194 + expect(stats.computedSignals).toBe(1); 195 + expect(stats.reactiveObjects).toBe(1); 196 + }); 197 + 198 + it("provides naming functions", () => { 199 + const sig = vdebugger.signal(0); 200 + vdebugger.nameSignal(sig, "renamed"); 201 + 202 + const info = vdebugger.getSignalInfo(sig); 203 + expect(info?.name).toBe("renamed"); 204 + }); 205 + 206 + it("provides graph operations", () => { 207 + const a = vdebugger.signal(1, "a"); 208 + const b = vdebugger.signal(2, "b"); 209 + 210 + expect(vdebugger.getDependencies(a)).toEqual([]); 211 + expect(vdebugger.getDependents(a)).toEqual([]); 212 + expect(vdebugger.getDepth(a)).toBe(0); 213 + 214 + const graph = vdebugger.buildGraph([a, b]); 215 + expect(graph.nodes).toHaveLength(2); 216 + }); 217 + 218 + it("provides logging functions", () => { 219 + const sig = vdebugger.signal(42, "answer"); 220 + expect(() => vdebugger.log(sig)).not.toThrow(); 221 + expect(() => vdebugger.logAll()).not.toThrow(); 222 + expect(() => vdebugger.logTable()).not.toThrow(); 223 + }); 224 + 225 + it("provides tracing functions", () => { 226 + const sig = vdebugger.signal(0, "count"); 227 + expect(() => vdebugger.trace(sig)).not.toThrow(); 228 + expect(() => vdebugger.enableTracing()).not.toThrow(); 229 + expect(() => vdebugger.disableTracing()).not.toThrow(); 230 + }); 231 + 232 + it("provides watch function", () => { 233 + const sig = vdebugger.signal(0, "count"); 234 + const unwatch = vdebugger.watch(sig); 235 + expect(unwatch).toBeTypeOf("function"); 236 + unwatch(); 237 + }); 238 + 239 + it("provides clear function", () => { 240 + vdebugger.signal(1); 241 + vdebugger.signal(2); 242 + 243 + expect(getAllSignals()).toHaveLength(2); 244 + 245 + vdebugger.clear(); 246 + 247 + expect(getAllSignals()).toHaveLength(0); 248 + }); 249 + 250 + it("provides attach function", () => { 251 + const sig = debugSignal(0); 252 + vdebugger.attach(sig, "signal", "attached"); 253 + 254 + const info = vdebugger.getSignalInfo(sig); 255 + expect(info).toBeDefined(); 256 + }); 257 + }); 258 + 259 + describe("integration", () => { 260 + it("works with non-debug core primitives", () => { 261 + const coreReactive = reactive({ count: 0 }); 262 + const debugCount = debugSignal(5); 263 + const sum = debugComputed(() => coreReactive.count + debugCount.get(), "sum"); 264 + expect(sum.get()).toBe(5); 265 + 266 + coreReactive.count = 10; 267 + expect(sum.get()).toBe(15); 268 + 269 + debugCount.set(20); 270 + expect(sum.get()).toBe(30); 271 + }); 272 + 273 + it("allows mixing debug and non-debug signals", () => { 274 + const debug = debugSignal(1, "debug"); 275 + const regular = debugSignal(2); 276 + const all = getAllSignals(); 277 + expect(all).toHaveLength(2); 278 + expect(all).toContain(debug); 279 + expect(all).toContain(regular); 280 + }); 281 + }); 282 + });
+358
lib/test/debug/graph.test.ts
··· 1 + import { computed, signal } from "$core/signal"; 2 + import { 3 + buildDependencyGraph, 4 + detectCircularDependencies, 5 + getDependencies, 6 + getDependents, 7 + getSignalDepth, 8 + hasDependency, 9 + recordDependencies, 10 + } from "$debug/graph"; 11 + import { registerSignal } from "$debug/registry"; 12 + import { describe, expect, it } from "vitest"; 13 + 14 + describe("debug/graph", () => { 15 + describe("recordDependencies", () => { 16 + it("records dependencies for a signal", () => { 17 + const a = signal(1); 18 + const b = signal(2); 19 + const sum = computed(() => a.get() + b.get()); 20 + 21 + recordDependencies(sum, [a, b]); 22 + 23 + const deps = getDependencies(sum); 24 + expect(deps).toHaveLength(2); 25 + expect(deps).toContain(a); 26 + expect(deps).toContain(b); 27 + }); 28 + 29 + it("records dependents bidirectionally", () => { 30 + const a = signal(1); 31 + const sum = computed(() => a.get() * 2); 32 + 33 + recordDependencies(sum, [a]); 34 + 35 + const dependents = getDependents(a); 36 + expect(dependents).toHaveLength(1); 37 + expect(dependents).toContain(sum); 38 + }); 39 + 40 + it("allows multiple dependents on one dependency", () => { 41 + const a = signal(1); 42 + const double = computed(() => a.get() * 2); 43 + const triple = computed(() => a.get() * 3); 44 + 45 + recordDependencies(double, [a]); 46 + recordDependencies(triple, [a]); 47 + 48 + const dependents = getDependents(a); 49 + expect(dependents).toHaveLength(2); 50 + expect(dependents).toContain(double); 51 + expect(dependents).toContain(triple); 52 + }); 53 + 54 + it("accumulates dependencies on repeated calls", () => { 55 + const a = signal(1); 56 + const b = signal(2); 57 + const c = signal(3); 58 + const sum = computed(() => a.get() + b.get() + c.get()); 59 + 60 + recordDependencies(sum, [a, b]); 61 + recordDependencies(sum, [c]); 62 + 63 + const deps = getDependencies(sum); 64 + expect(deps).toHaveLength(3); 65 + expect(deps).toContain(a); 66 + expect(deps).toContain(b); 67 + expect(deps).toContain(c); 68 + }); 69 + }); 70 + 71 + describe("getDependencies", () => { 72 + it("returns empty array for signal with no dependencies", () => { 73 + const sig = signal(0); 74 + const deps = getDependencies(sig); 75 + expect(deps).toEqual([]); 76 + }); 77 + 78 + it("returns all recorded dependencies", () => { 79 + const a = signal(1); 80 + const b = signal(2); 81 + const sum = computed(() => a.get() + b.get()); 82 + 83 + recordDependencies(sum, [a, b]); 84 + 85 + const deps = getDependencies(sum); 86 + expect(deps).toHaveLength(2); 87 + expect(deps).toContain(a); 88 + expect(deps).toContain(b); 89 + }); 90 + }); 91 + 92 + describe("getDependents", () => { 93 + it("returns empty array for signal with no dependents", () => { 94 + const sig = signal(0); 95 + const deps = getDependents(sig); 96 + expect(deps).toEqual([]); 97 + }); 98 + 99 + it("returns all dependents", () => { 100 + const a = signal(1); 101 + const double = computed(() => a.get() * 2); 102 + const triple = computed(() => a.get() * 3); 103 + 104 + recordDependencies(double, [a]); 105 + recordDependencies(triple, [a]); 106 + 107 + const dependents = getDependents(a); 108 + expect(dependents).toHaveLength(2); 109 + expect(dependents).toContain(double); 110 + expect(dependents).toContain(triple); 111 + }); 112 + }); 113 + 114 + describe("hasDependency", () => { 115 + it("returns true when dependency exists", () => { 116 + const a = signal(1); 117 + const double = computed(() => a.get() * 2); 118 + 119 + recordDependencies(double, [a]); 120 + 121 + expect(hasDependency(double, a)).toBe(true); 122 + }); 123 + 124 + it("returns false when dependency does not exist", () => { 125 + const a = signal(1); 126 + const b = signal(2); 127 + const double = computed(() => a.get() * 2); 128 + 129 + recordDependencies(double, [a]); 130 + expect(hasDependency(double, b)).toBe(false); 131 + }); 132 + 133 + it("returns false for signal with no dependencies", () => { 134 + const a = signal(1); 135 + const b = signal(2); 136 + expect(hasDependency(a, b)).toBe(false); 137 + }); 138 + }); 139 + 140 + describe("buildDependencyGraph", () => { 141 + it("builds a graph with nodes and edges", () => { 142 + const a = signal(1); 143 + const b = signal(2); 144 + const sum = computed(() => a.get() + b.get()); 145 + 146 + registerSignal(a, "signal", "a"); 147 + registerSignal(b, "signal", "b"); 148 + registerSignal(sum, "computed", "sum"); 149 + 150 + recordDependencies(sum, [a, b]); 151 + 152 + const graph = buildDependencyGraph([a, b, sum]); 153 + 154 + expect(graph.nodes).toHaveLength(3); 155 + expect(graph.edges).toHaveLength(2); 156 + }); 157 + 158 + it("creates nodes with correct metadata", () => { 159 + const a = signal(5); 160 + registerSignal(a, "signal", "count"); 161 + 162 + const graph = buildDependencyGraph([a]); 163 + 164 + expect(graph.nodes).toHaveLength(1); 165 + const node = graph.nodes[0]; 166 + expect(node.signal).toBe(a); 167 + expect(node.name).toBe("count"); 168 + expect(node.type).toBe("signal"); 169 + expect(node.value).toBe(5); 170 + expect(node.id).toMatch(/^signal-\d+$/); 171 + expect(node.dependencies).toEqual([]); 172 + expect(node.dependents).toEqual([]); 173 + }); 174 + 175 + it("creates edges from dependencies to dependents", () => { 176 + const a = signal(1); 177 + const b = signal(2); 178 + const sum = computed(() => a.get() + b.get()); 179 + 180 + registerSignal(a, "signal", "a"); 181 + registerSignal(b, "signal", "b"); 182 + registerSignal(sum, "computed", "sum"); 183 + 184 + recordDependencies(sum, [a, b]); 185 + 186 + const graph = buildDependencyGraph([a, b, sum]); 187 + const aId = graph.nodes.find((n) => n.name === "a")?.id; 188 + const bId = graph.nodes.find((n) => n.name === "b")?.id; 189 + const sumId = graph.nodes.find((n) => n.name === "sum")?.id; 190 + 191 + expect(graph.edges).toContainEqual({ from: aId, to: sumId }); 192 + expect(graph.edges).toContainEqual({ from: bId, to: sumId }); 193 + }); 194 + 195 + it("handles empty signal list", () => { 196 + const graph = buildDependencyGraph([]); 197 + expect(graph.nodes).toEqual([]); 198 + expect(graph.edges).toEqual([]); 199 + }); 200 + 201 + it("includes dependency and dependent IDs in nodes", () => { 202 + const a = signal(1); 203 + const double = computed(() => a.get() * 2); 204 + const quad = computed(() => double.get() * 2); 205 + 206 + registerSignal(a, "signal", "a"); 207 + registerSignal(double, "computed", "double"); 208 + registerSignal(quad, "computed", "quad"); 209 + 210 + recordDependencies(double, [a]); 211 + recordDependencies(quad, [double]); 212 + 213 + const graph = buildDependencyGraph([a, double, quad]); 214 + 215 + const aNode = graph.nodes.find((n) => n.name === "a"); 216 + const doubleNode = graph.nodes.find((n) => n.name === "double"); 217 + const quadNode = graph.nodes.find((n) => n.name === "quad"); 218 + 219 + expect(aNode?.dependencies).toEqual([]); 220 + expect(aNode?.dependents).toEqual([doubleNode?.id]); 221 + 222 + expect(doubleNode?.dependencies).toEqual([aNode?.id]); 223 + expect(doubleNode?.dependents).toEqual([quadNode?.id]); 224 + 225 + expect(quadNode?.dependencies).toEqual([doubleNode?.id]); 226 + expect(quadNode?.dependents).toEqual([]); 227 + }); 228 + }); 229 + 230 + describe("detectCircularDependencies", () => { 231 + it("returns null when no cycle exists", () => { 232 + const a = signal(1); 233 + const double = computed(() => a.get() * 2); 234 + 235 + recordDependencies(double, [a]); 236 + 237 + const cycle = detectCircularDependencies(a); 238 + expect(cycle).toBeNull(); 239 + }); 240 + 241 + it("detects direct self-dependency", () => { 242 + const a = signal(1); 243 + recordDependencies(a, [a]); 244 + 245 + const cycle = detectCircularDependencies(a); 246 + expect(cycle).not.toBeNull(); 247 + expect(cycle).toContain(a); 248 + }); 249 + 250 + it("detects two-node cycle", () => { 251 + const a = signal(1); 252 + const b = computed(() => a.get() * 2); 253 + 254 + recordDependencies(a, [b]); 255 + recordDependencies(b, [a]); 256 + 257 + const cycle = detectCircularDependencies(a); 258 + expect(cycle).not.toBeNull(); 259 + expect(cycle).toContain(a); 260 + expect(cycle).toContain(b); 261 + }); 262 + 263 + it("detects multi-node cycle", () => { 264 + const a = signal(1); 265 + const b = computed(() => a.get() * 2); 266 + const c = computed(() => b.get() * 2); 267 + 268 + recordDependencies(a, [c]); 269 + recordDependencies(b, [a]); 270 + recordDependencies(c, [b]); 271 + 272 + const cycle = detectCircularDependencies(a); 273 + expect(cycle).not.toBeNull(); 274 + expect(cycle).toContain(a); 275 + expect(cycle).toContain(b); 276 + expect(cycle).toContain(c); 277 + }); 278 + 279 + it("handles shared dependencies without false positives", () => { 280 + const a = signal(1); 281 + const b = computed(() => a.get() * 2); 282 + const c = computed(() => a.get() * 3); 283 + const sum = computed(() => b.get() + c.get()); 284 + 285 + recordDependencies(b, [a]); 286 + recordDependencies(c, [a]); 287 + recordDependencies(sum, [b, c]); 288 + 289 + const cycle = detectCircularDependencies(a); 290 + expect(cycle).toBeNull(); 291 + }); 292 + }); 293 + 294 + describe("getSignalDepth", () => { 295 + it("returns 0 for signal with no dependencies", () => { 296 + const sig = signal(0); 297 + expect(getSignalDepth(sig)).toBe(0); 298 + }); 299 + 300 + it("returns 1 for signal depending on base signal", () => { 301 + const a = signal(1); 302 + const double = computed(() => a.get() * 2); 303 + recordDependencies(double, [a]); 304 + expect(getSignalDepth(double)).toBe(1); 305 + }); 306 + 307 + it("calculates depth for multi-level dependencies", () => { 308 + const a = signal(1); 309 + const double = computed(() => a.get() * 2); 310 + const quad = computed(() => double.get() * 2); 311 + const oct = computed(() => quad.get() * 2); 312 + 313 + recordDependencies(double, [a]); 314 + recordDependencies(quad, [double]); 315 + recordDependencies(oct, [quad]); 316 + 317 + expect(getSignalDepth(a)).toBe(0); 318 + expect(getSignalDepth(double)).toBe(1); 319 + expect(getSignalDepth(quad)).toBe(2); 320 + expect(getSignalDepth(oct)).toBe(3); 321 + }); 322 + 323 + it("handles shared dependencies correctly", () => { 324 + const a = signal(1); 325 + const b = signal(2); 326 + const double = computed(() => a.get() * 2); 327 + const sum = computed(() => double.get() + b.get()); 328 + 329 + recordDependencies(double, [a]); 330 + recordDependencies(sum, [double, b]); 331 + 332 + expect(getSignalDepth(sum)).toBe(2); 333 + }); 334 + 335 + it("uses maximum depth when multiple paths exist", () => { 336 + const a = signal(1); 337 + const b = computed(() => a.get() * 2); 338 + const c = computed(() => b.get() * 2); 339 + const d = computed(() => a.get() + c.get()); 340 + 341 + recordDependencies(b, [a]); 342 + recordDependencies(c, [b]); 343 + recordDependencies(d, [a, c]); 344 + 345 + expect(getSignalDepth(d)).toBe(3); 346 + }); 347 + 348 + it("handles circular dependencies gracefully", () => { 349 + const a = signal(1); 350 + const b = computed(() => a.get() * 2); 351 + 352 + recordDependencies(a, [b]); 353 + recordDependencies(b, [a]); 354 + 355 + expect(() => getSignalDepth(a)).not.toThrow(); 356 + }); 357 + }); 358 + });
+317
lib/test/debug/logger.test.ts
··· 1 + import { reactive } from "$core/reactive"; 2 + import { computed, signal } from "$core/signal"; 3 + import { recordDependencies } from "$debug/graph"; 4 + import { 5 + disableGlobalTracing, 6 + enableGlobalTracing, 7 + logAllReactives, 8 + logAllSignals, 9 + logReactive, 10 + logSignal, 11 + logSignalTable, 12 + trace, 13 + watch, 14 + } from "$debug/logger"; 15 + import { clearRegistry, registerReactive, registerSignal } from "$debug/registry"; 16 + import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 17 + 18 + describe("debug/logger", () => { 19 + let consoleSpy: { 20 + log: ReturnType<typeof vi.spyOn>; 21 + group: ReturnType<typeof vi.spyOn>; 22 + groupEnd: ReturnType<typeof vi.spyOn>; 23 + table: ReturnType<typeof vi.spyOn>; 24 + }; 25 + 26 + beforeEach(() => { 27 + clearRegistry(); 28 + consoleSpy = { 29 + log: vi.spyOn(console, "log").mockImplementation(() => {}), 30 + group: vi.spyOn(console, "group").mockImplementation(() => {}), 31 + groupEnd: vi.spyOn(console, "groupEnd").mockImplementation(() => {}), 32 + table: vi.spyOn(console, "table").mockImplementation(() => {}), 33 + }; 34 + }); 35 + 36 + afterEach(() => { 37 + vi.restoreAllMocks(); 38 + }); 39 + 40 + describe("logSignal", () => { 41 + it("logs signal information", () => { 42 + const sig = signal(42); 43 + registerSignal(sig, "signal", "answer"); 44 + logSignal(sig); 45 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("answer")); 46 + expect(consoleSpy.log).toHaveBeenCalledWith("Type:", "signal"); 47 + expect(consoleSpy.log).toHaveBeenCalledWith("Value:", 42); 48 + expect(consoleSpy.groupEnd).toHaveBeenCalled(); 49 + }); 50 + 51 + it("logs unnamed signal with ID", () => { 52 + const sig = signal(0); 53 + registerSignal(sig, "signal"); 54 + logSignal(sig); 55 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringMatching(/signal-\d+/)); 56 + }); 57 + 58 + it("logs dependencies and dependents", () => { 59 + const a = signal(1); 60 + const b = signal(2); 61 + const sum = computed(() => a.get() + b.get()); 62 + 63 + registerSignal(a, "signal", "a"); 64 + registerSignal(b, "signal", "b"); 65 + registerSignal(sum, "computed", "sum"); 66 + 67 + recordDependencies(sum, [a, b]); 68 + 69 + logSignal(sum); 70 + 71 + expect(consoleSpy.log).toHaveBeenCalledWith("Dependencies:", 2); 72 + expect(consoleSpy.log).toHaveBeenCalledWith("Dependents:", 0); 73 + expect(consoleSpy.group).toHaveBeenCalledWith("Depends on:"); 74 + }); 75 + 76 + it("logs message for unregistered signal", () => { 77 + const sig = signal(0); 78 + logSignal(sig); 79 + expect(consoleSpy.log).toHaveBeenCalledWith("[Volt Debug] Unregistered signal"); 80 + }); 81 + }); 82 + 83 + describe("logAllSignals", () => { 84 + it("logs all registered signals", () => { 85 + const sig1 = signal(1); 86 + const sig2 = signal(2); 87 + 88 + registerSignal(sig1, "signal", "first"); 89 + registerSignal(sig2, "signal", "second"); 90 + logAllSignals(); 91 + 92 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("All Signals (2)")); 93 + expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("first")); 94 + expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("second")); 95 + expect(consoleSpy.groupEnd).toHaveBeenCalled(); 96 + }); 97 + 98 + it("handles empty signal list", () => { 99 + logAllSignals(); 100 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("All Signals (0)")); 101 + }); 102 + }); 103 + 104 + describe("logReactive", () => { 105 + it("logs reactive object information", () => { 106 + const obj = reactive({ count: 42 }); 107 + registerReactive(obj, "state"); 108 + logReactive(obj); 109 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("state")); 110 + expect(consoleSpy.log).toHaveBeenCalledWith("Type:", "reactive"); 111 + expect(consoleSpy.log).toHaveBeenCalledWith("Value:", obj); 112 + expect(consoleSpy.groupEnd).toHaveBeenCalled(); 113 + }); 114 + 115 + it("logs message for unregistered reactive", () => { 116 + const obj = reactive({ count: 0 }); 117 + logReactive(obj); 118 + expect(consoleSpy.log).toHaveBeenCalledWith("[Volt Debug] Unregistered reactive object"); 119 + }); 120 + }); 121 + 122 + describe("logAllReactives", () => { 123 + it("logs all registered reactive objects", () => { 124 + const obj1 = reactive({ a: 1 }); 125 + const obj2 = reactive({ b: 2 }); 126 + 127 + registerReactive(obj1, "first"); 128 + registerReactive(obj2, "second"); 129 + 130 + logAllReactives(); 131 + 132 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("All Reactive Objects (2)")); 133 + expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("first")); 134 + expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("second")); 135 + expect(consoleSpy.groupEnd).toHaveBeenCalled(); 136 + }); 137 + }); 138 + 139 + describe("logSignalTable", () => { 140 + it("logs signals as a table", () => { 141 + const sig1 = signal(1); 142 + const sig2 = signal(2); 143 + 144 + registerSignal(sig1, "signal", "first"); 145 + registerSignal(sig2, "signal", "second"); 146 + 147 + logSignalTable(); 148 + 149 + expect(consoleSpy.table).toHaveBeenCalledWith( 150 + expect.arrayContaining([ 151 + expect.objectContaining({ Name: "first", Type: "signal" }), 152 + expect.objectContaining({ Name: "second", Type: "signal" }), 153 + ]), 154 + ); 155 + }); 156 + 157 + it("handles empty signal list", () => { 158 + logSignalTable(); 159 + 160 + expect(consoleSpy.table).toHaveBeenCalledWith([]); 161 + }); 162 + 163 + it("includes dependency counts in table", () => { 164 + const a = signal(1); 165 + const double = computed(() => a.get() * 2); 166 + 167 + registerSignal(a, "signal", "a"); 168 + registerSignal(double, "computed", "double"); 169 + recordDependencies(double, [a]); 170 + 171 + logSignalTable(); 172 + 173 + expect(consoleSpy.table).toHaveBeenCalledWith( 174 + expect.arrayContaining([ 175 + expect.objectContaining({ Name: "a", Dependencies: 0, Dependents: 1 }), 176 + expect.objectContaining({ Name: "double", Dependencies: 1, Dependents: 0 }), 177 + ]), 178 + ); 179 + }); 180 + }); 181 + 182 + describe("trace", () => { 183 + it("enables tracing for a signal", () => { 184 + const sig = signal(0); 185 + registerSignal(sig, "signal", "count"); 186 + 187 + trace(sig); 188 + 189 + expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("Tracing enabled for count")); 190 + }); 191 + 192 + it("logs updates with new value", () => { 193 + const sig = signal(0); 194 + registerSignal(sig, "signal", "count"); 195 + 196 + trace(sig); 197 + consoleSpy.log.mockClear(); 198 + 199 + sig.set(5); 200 + 201 + expect(consoleSpy.log).toHaveBeenCalledWith( 202 + expect.stringContaining("[Volt Trace]"), 203 + expect.anything(), 204 + expect.anything(), 205 + ); 206 + }); 207 + 208 + it("does not duplicate tracing for same signal", () => { 209 + const sig = signal(0); 210 + registerSignal(sig, "signal", "count"); 211 + 212 + trace(sig); 213 + consoleSpy.log.mockClear(); 214 + 215 + trace(sig); 216 + 217 + expect(consoleSpy.log).not.toHaveBeenCalled(); 218 + }); 219 + 220 + it.skip("disables tracing when enabled is false", () => { 221 + // TODO: implement tracing unsubscription 222 + const sig = signal(0); 223 + registerSignal(sig, "signal", "count"); 224 + 225 + trace(sig, true); 226 + trace(sig, false); 227 + consoleSpy.log.mockClear(); 228 + 229 + sig.set(5); 230 + 231 + expect(consoleSpy.log).not.toHaveBeenCalled(); 232 + }); 233 + }); 234 + 235 + describe("watch", () => { 236 + it("logs updates with full signal info", () => { 237 + const sig = signal(0); 238 + registerSignal(sig, "signal", "count"); 239 + 240 + watch(sig); 241 + consoleSpy.group.mockClear(); 242 + consoleSpy.log.mockClear(); 243 + 244 + sig.set(5); 245 + 246 + expect(consoleSpy.group).toHaveBeenCalledWith(expect.stringContaining("[Volt Watch]")); 247 + expect(consoleSpy.log).toHaveBeenCalledWith("New value:", 5); 248 + }); 249 + 250 + it("returns unsubscribe function", () => { 251 + const sig = signal(0); 252 + registerSignal(sig, "signal", "count"); 253 + 254 + const unsubscribe = watch(sig); 255 + unsubscribe(); 256 + 257 + consoleSpy.group.mockClear(); 258 + consoleSpy.log.mockClear(); 259 + 260 + sig.set(5); 261 + 262 + expect(consoleSpy.group).not.toHaveBeenCalled(); 263 + }); 264 + 265 + it("logs unwatch message", () => { 266 + const sig = signal(0); 267 + registerSignal(sig, "signal", "count"); 268 + 269 + const unsubscribe = watch(sig); 270 + consoleSpy.log.mockClear(); 271 + 272 + unsubscribe(); 273 + 274 + expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining("Stopped watching count")); 275 + }); 276 + }); 277 + 278 + describe("global tracing", () => { 279 + it("enables tracing for all signals", () => { 280 + const sig1 = signal(0); 281 + const sig2 = signal(0); 282 + 283 + registerSignal(sig1, "signal", "first"); 284 + registerSignal(sig2, "signal", "second"); 285 + 286 + enableGlobalTracing(); 287 + consoleSpy.log.mockClear(); 288 + 289 + sig1.set(1); 290 + sig2.set(2); 291 + 292 + expect(consoleSpy.log).toHaveBeenCalledWith( 293 + expect.stringContaining("first"), 294 + expect.anything(), 295 + expect.anything(), 296 + ); 297 + expect(consoleSpy.log).toHaveBeenCalledWith( 298 + expect.stringContaining("second"), 299 + expect.anything(), 300 + expect.anything(), 301 + ); 302 + }); 303 + 304 + it.skip("disables tracing for all signals", () => { 305 + const sig = signal(0); 306 + registerSignal(sig, "signal", "count"); 307 + 308 + enableGlobalTracing(); 309 + disableGlobalTracing(); 310 + consoleSpy.log.mockClear(); 311 + 312 + sig.set(5); 313 + 314 + expect(consoleSpy.log).not.toHaveBeenCalled(); 315 + }); 316 + }); 317 + });
+348
lib/test/debug/registry.test.ts
··· 1 + import { reactive } from "$core/reactive"; 2 + import { computed, signal } from "$core/signal"; 3 + import { 4 + clearRegistry, 5 + getAllReactives, 6 + getAllSignals, 7 + getReactiveInfo, 8 + getReactiveMetadata, 9 + getRegistryStats, 10 + getSignalInfo, 11 + getSignalMetadata, 12 + nameReactive, 13 + nameSignal, 14 + registerReactive, 15 + registerSignal, 16 + } from "$debug/registry"; 17 + import { beforeEach, describe, expect, it } from "vitest"; 18 + 19 + describe("debug/registry", () => { 20 + beforeEach(() => { 21 + clearRegistry(); 22 + }); 23 + 24 + describe("signal registration", () => { 25 + it("registers a signal with metadata", () => { 26 + const sig = signal(0); 27 + registerSignal(sig, "signal", "count"); 28 + 29 + const metadata = getSignalMetadata(sig); 30 + expect(metadata).toBeDefined(); 31 + expect(metadata?.type).toBe("signal"); 32 + expect(metadata?.name).toBe("count"); 33 + expect(metadata?.id).toMatch(/^signal-\d+$/); 34 + expect(metadata?.createdAt).toBeTypeOf("number"); 35 + }); 36 + 37 + it("registers a signal without a name", () => { 38 + const sig = signal(0); 39 + registerSignal(sig, "signal"); 40 + 41 + const metadata = getSignalMetadata(sig); 42 + expect(metadata).toBeDefined(); 43 + expect(metadata?.type).toBe("signal"); 44 + expect(metadata?.name).toBeUndefined(); 45 + }); 46 + 47 + it("does not re-register an already registered signal", () => { 48 + const sig = signal(0); 49 + registerSignal(sig, "signal", "first"); 50 + const firstMetadata = getSignalMetadata(sig); 51 + 52 + registerSignal(sig, "signal", "second"); 53 + const secondMetadata = getSignalMetadata(sig); 54 + 55 + expect(firstMetadata).toBe(secondMetadata); 56 + expect(secondMetadata?.name).toBe("first"); 57 + }); 58 + 59 + it("registers computed signals with correct type", () => { 60 + const comp = computed(() => 5); 61 + registerSignal(comp, "computed", "doubled"); 62 + 63 + const metadata = getSignalMetadata(comp); 64 + expect(metadata?.type).toBe("computed"); 65 + expect(metadata?.name).toBe("doubled"); 66 + }); 67 + 68 + it("assigns incremental IDs", () => { 69 + const sig1 = signal(0); 70 + const sig2 = signal(0); 71 + const comp = computed(() => 0); 72 + 73 + registerSignal(sig1, "signal"); 74 + registerSignal(sig2, "signal"); 75 + registerSignal(comp, "computed"); 76 + 77 + const meta1 = getSignalMetadata(sig1); 78 + const meta2 = getSignalMetadata(sig2); 79 + const meta3 = getSignalMetadata(comp); 80 + 81 + expect(meta1?.id).toBe("signal-1"); 82 + expect(meta2?.id).toBe("signal-2"); 83 + expect(meta3?.id).toBe("computed-3"); 84 + }); 85 + }); 86 + 87 + describe("signal info", () => { 88 + it("returns signal info with current value", () => { 89 + const sig = signal(42); 90 + registerSignal(sig, "signal", "answer"); 91 + 92 + const info = getSignalInfo(sig); 93 + expect(info).toBeDefined(); 94 + expect(info?.id).toMatch(/^signal-\d+$/); 95 + expect(info?.type).toBe("signal"); 96 + expect(info?.name).toBe("answer"); 97 + expect(info?.value).toBe(42); 98 + expect(info?.createdAt).toBeTypeOf("number"); 99 + expect(info?.age).toBeTypeOf("number"); 100 + expect(info!.age).toBeGreaterThanOrEqual(0); 101 + }); 102 + 103 + it("returns undefined for unregistered signal", () => { 104 + const sig = signal(0); 105 + const info = getSignalInfo(sig); 106 + expect(info).toBeUndefined(); 107 + }); 108 + 109 + it("reflects updated values", () => { 110 + const sig = signal(0); 111 + registerSignal(sig, "signal"); 112 + 113 + const info1 = getSignalInfo(sig); 114 + expect(info1?.value).toBe(0); 115 + 116 + sig.set(10); 117 + 118 + const info2 = getSignalInfo(sig); 119 + expect(info2?.value).toBe(10); 120 + }); 121 + }); 122 + 123 + describe("signal naming", () => { 124 + it("sets name on a registered signal", () => { 125 + const sig = signal(0); 126 + registerSignal(sig, "signal"); 127 + 128 + nameSignal(sig, "mySignal"); 129 + 130 + const metadata = getSignalMetadata(sig); 131 + expect(metadata?.name).toBe("mySignal"); 132 + }); 133 + 134 + it("updates existing name", () => { 135 + const sig = signal(0); 136 + registerSignal(sig, "signal", "oldName"); 137 + 138 + nameSignal(sig, "newName"); 139 + 140 + const metadata = getSignalMetadata(sig); 141 + expect(metadata?.name).toBe("newName"); 142 + }); 143 + 144 + it("does nothing for unregistered signal", () => { 145 + const sig = signal(0); 146 + nameSignal(sig, "test"); 147 + 148 + const metadata = getSignalMetadata(sig); 149 + expect(metadata).toBeUndefined(); 150 + }); 151 + }); 152 + 153 + describe("getAllSignals", () => { 154 + it("returns all registered signals", () => { 155 + const sig1 = signal(1); 156 + const sig2 = signal(2); 157 + const comp = computed(() => 3); 158 + 159 + registerSignal(sig1, "signal"); 160 + registerSignal(sig2, "signal"); 161 + registerSignal(comp, "computed"); 162 + 163 + const all = getAllSignals(); 164 + expect(all).toHaveLength(3); 165 + expect(all).toContain(sig1); 166 + expect(all).toContain(sig2); 167 + expect(all).toContain(comp); 168 + }); 169 + 170 + it("returns empty array when no signals registered", () => { 171 + const all = getAllSignals(); 172 + expect(all).toEqual([]); 173 + }); 174 + 175 + it.skip("cleans up garbage collected signals", () => { 176 + // NOTE: GC is non-deterministic in test environments 177 + // We document expected behavior but can't reliably test it 178 + let sig: ReturnType<typeof signal> | null = signal(0); 179 + registerSignal(sig, "signal"); 180 + expect(getAllSignals()).toHaveLength(1); 181 + 182 + sig = null; 183 + 184 + const all = getAllSignals(); 185 + expect(all).toHaveLength(0); 186 + }); 187 + }); 188 + 189 + describe("reactive registration", () => { 190 + it("registers a reactive object with metadata", () => { 191 + const obj = reactive({ count: 0 }); 192 + registerReactive(obj, "state"); 193 + 194 + const metadata = getReactiveMetadata(obj); 195 + expect(metadata).toBeDefined(); 196 + expect(metadata?.type).toBe("reactive"); 197 + expect(metadata?.name).toBe("state"); 198 + expect(metadata?.id).toMatch(/^reactive-\d+$/); 199 + expect(metadata?.createdAt).toBeTypeOf("number"); 200 + }); 201 + 202 + it("does not re-register an already registered reactive", () => { 203 + const obj = reactive({ count: 0 }); 204 + registerReactive(obj, "first"); 205 + const firstMetadata = getReactiveMetadata(obj); 206 + 207 + registerReactive(obj, "second"); 208 + const secondMetadata = getReactiveMetadata(obj); 209 + 210 + expect(firstMetadata).toBe(secondMetadata); 211 + expect(secondMetadata?.name).toBe("first"); 212 + }); 213 + }); 214 + 215 + describe("reactive info", () => { 216 + it("returns reactive info with current value", () => { 217 + const obj = reactive({ count: 42 }); 218 + registerReactive(obj, "state"); 219 + 220 + const info = getReactiveInfo(obj); 221 + expect(info).toBeDefined(); 222 + expect(info?.id).toMatch(/^reactive-\d+$/); 223 + expect(info?.type).toBe("reactive"); 224 + expect(info?.name).toBe("state"); 225 + expect(info?.value).toBe(obj); 226 + expect(info?.createdAt).toBeTypeOf("number"); 227 + expect(info?.age).toBeTypeOf("number"); 228 + }); 229 + 230 + it("returns undefined for unregistered reactive", () => { 231 + const obj = reactive({ count: 0 }); 232 + const info = getReactiveInfo(obj); 233 + expect(info).toBeUndefined(); 234 + }); 235 + }); 236 + 237 + describe("reactive naming", () => { 238 + it("sets name on a registered reactive", () => { 239 + const obj = reactive({ count: 0 }); 240 + registerReactive(obj); 241 + 242 + nameReactive(obj, "myState"); 243 + 244 + const metadata = getReactiveMetadata(obj); 245 + expect(metadata?.name).toBe("myState"); 246 + }); 247 + 248 + it("does nothing for unregistered reactive", () => { 249 + const obj = reactive({ count: 0 }); 250 + nameReactive(obj, "test"); 251 + 252 + const metadata = getReactiveMetadata(obj); 253 + expect(metadata).toBeUndefined(); 254 + }); 255 + }); 256 + 257 + describe("getAllReactives", () => { 258 + it("returns all registered reactive objects", () => { 259 + const obj1 = reactive({ a: 1 }); 260 + const obj2 = reactive({ b: 2 }); 261 + 262 + registerReactive(obj1); 263 + registerReactive(obj2); 264 + 265 + const all = getAllReactives(); 266 + expect(all).toHaveLength(2); 267 + expect(all).toContain(obj1); 268 + expect(all).toContain(obj2); 269 + }); 270 + 271 + it("returns empty array when no reactives registered", () => { 272 + const all = getAllReactives(); 273 + expect(all).toEqual([]); 274 + }); 275 + 276 + it.skip("cleans up garbage collected reactives", () => { 277 + let obj: ReturnType<typeof reactive> | null = reactive({ count: 0 }); 278 + registerReactive(obj); 279 + expect(getAllReactives()).toHaveLength(1); 280 + 281 + obj = null; 282 + 283 + const all = getAllReactives(); 284 + expect(all).toHaveLength(0); 285 + }); 286 + }); 287 + 288 + describe("registry stats", () => { 289 + it("returns correct counts", () => { 290 + const sig1 = signal(1); 291 + const sig2 = signal(2); 292 + const comp = computed(() => 3); 293 + const obj = reactive({ count: 0 }); 294 + 295 + registerSignal(sig1, "signal"); 296 + registerSignal(sig2, "signal"); 297 + registerSignal(comp, "computed"); 298 + registerReactive(obj); 299 + 300 + const stats = getRegistryStats(); 301 + expect(stats.totalSignals).toBe(3); 302 + expect(stats.regularSignals).toBe(2); 303 + expect(stats.computedSignals).toBe(1); 304 + expect(stats.reactiveObjects).toBe(1); 305 + }); 306 + 307 + it("returns zeros when registry is empty", () => { 308 + const stats = getRegistryStats(); 309 + expect(stats.totalSignals).toBe(0); 310 + expect(stats.regularSignals).toBe(0); 311 + expect(stats.computedSignals).toBe(0); 312 + expect(stats.reactiveObjects).toBe(0); 313 + }); 314 + }); 315 + 316 + describe("clearRegistry", () => { 317 + it("clears all registered signals and reactives", () => { 318 + const sig = signal(0); 319 + const obj = reactive({ count: 0 }); 320 + 321 + registerSignal(sig, "signal"); 322 + registerReactive(obj); 323 + 324 + expect(getAllSignals()).toHaveLength(1); 325 + expect(getAllReactives()).toHaveLength(1); 326 + 327 + clearRegistry(); 328 + 329 + expect(getAllSignals()).toHaveLength(0); 330 + expect(getAllReactives()).toHaveLength(0); 331 + }); 332 + 333 + it("resets ID counter", () => { 334 + const sig1 = signal(0); 335 + registerSignal(sig1, "signal"); 336 + const meta1 = getSignalMetadata(sig1); 337 + 338 + clearRegistry(); 339 + 340 + const sig2 = signal(0); 341 + registerSignal(sig2, "signal"); 342 + const meta2 = getSignalMetadata(sig2); 343 + 344 + expect(meta1?.id).toBe("signal-1"); 345 + expect(meta2?.id).toBe("signal-1"); 346 + }); 347 + }); 348 + });
+4 -2
lib/tsconfig.json
··· 12 12 "moduleDetection": "force", 13 13 "noEmit": true, 14 14 "strict": true, 15 - "noUnusedLocals": true, 16 - "noUnusedParameters": true, 15 + "noUnusedLocals": false, 16 + "noUnusedParameters": false, 17 17 "erasableSyntaxOnly": true, 18 18 "noFallthroughCasesInSwitch": true, 19 19 "noUncheckedSideEffectImports": true, ··· 21 21 "baseUrl": ".", 22 22 "paths": { 23 23 "$types/*": ["./src/types/*"], 24 + "$debug/*": ["./src/debug/*"], 25 + "$vebug": ["./src/debug.ts"], 24 26 "$core/*": ["./src/core/*"], 25 27 "$plugins": ["./src/plugins/index.ts"], 26 28 "$plugins/*": ["./src/plugins/*"],
+1
lib/vite.config.ts
··· 28 28 "$volt": path.resolve(__dirname, "./src/index.ts"), 29 29 "$core": path.resolve(__dirname, "./src/core"), 30 30 "$plugins": path.resolve(__dirname, "./src/plugins"), 31 + "$debug": path.resolve(__dirname, "./src/debug"), 31 32 }, 32 33 }, 33 34 build: mode === "lib"