···1515| v0.1.0 | ✓ | [Markup Based Reactivity](#markup-based-reactivity) | Allow users to write apps without any bundled JS |
1616| v0.2.0 | | [Animation & Transitions](#animation--transitions) | Declarative animation layer and browser View Transition API integration. |
1717| v0.3.0 | | [Inspector & Developer Tools](#inspector--developer-tools) | Built-in signal inspector, debug overlays, and dev tooling. |
1818-| v0.4.0 | | [Docs & Stability](#documentation--stability-pass) | Comprehensive docs, tests, and performance review. |
1919-| v0.5.0 | | PWA Capabilities | TODO |
2020-| v1.0.0 | | [Release](#stable-release) | Public API freeze, plugin registry, and versioned documentation. |
1818+| v0.4.0 | | PWA Capabilities | TODO |
1919+| v1.0.0 | | [Release](#stable-release) | Public API freeze, plugin registry, comprehensive docs & tests. |
21202221## Completed
2322···117116118117## To-Do
119118120120-### Streaming & Patch Engine
121121-122122-**Goal:** Enable real-time updates via SSE/WebSocket streaming with intelligent DOM patching.
123123-**Outcome:** Volt.js can receive and apply live updates from the server
124124-**Deliverables:**
125125- - Server-Sent Events (SSE) integration
126126- - `data-volt-stream` attribute for SSE endpoints
127127- - Signal patching from backend (`data-signals-*` merge system)
128128- - Backend action system with `$$action()` syntax (TBD on final syntax decision)
129129- - JSON Patch parser and DOM morphing engine
130130- - WebSocket as alternative to SSE
131131- - `data-volt-ignore-morph` for selective patch exclusion
132132-133133-### Persistence & Offline
134134-135135-**Goal:** Introduce persistent storage and offline-first behaviors.
136136-**Outcome:** Resilient state persistence and offline replay built into Volt.js.
137137-**Deliverables:**
138138- - ✓ Persistent signals (localStorage, sessionStorage, indexedDb)
139139- - ✓ Storage plugin (`data-volt-persist`)
140140- - Storage modifiers on signals:
141141- - `.local` modifier for localStorage persistence
142142- - `.session` modifier for sessionStorage persistence
143143- - `.ifmissing` modifier for conditional initialization
144144- - Offline queue for deferred stream events and HTTP requests
145145- - Sync strategy API (merge, overwrite, patch) for conflict resolution
146146- - Service Worker integration for offline-first apps
147147- - Background sync for deferred requests
148148- - Cache invalidation strategies
149149- - Cross-tab synchronization via `BroadcastChannel`
150150-151119### Reactive Attributes & Event Modifiers
152120153121**Goal:** Extend Volt.js with expressive attribute patterns and event options for fine-grained control.
154122**Outcome:** Volt.js supports rich declarative behaviors and event semantics built entirely on standard DOM APIs.
155123**Deliverables:**
156156- - `data-volt-show` — toggles element visibility via CSS rather than DOM removal (complements `data-volt-if`)
157157- - `data-volt-style` — binds inline styles to reactive expressions
158158- - `data-volt-skip` — marks elements or subtrees to exclude from Volt’s reactive parsing
159159- - `data-volt-cloak` — hides content until the Volt runtime initializes
124124+ - `data-volt-show` - toggles element visibility via CSS rather than DOM removal (complements `data-volt-if`)
125125+ - `data-volt-style` - binds inline styles to reactive expressions
126126+ - `data-volt-skip` - marks elements or subtrees to exclude from Volt’s reactive parsing
127127+ - `data-volt-cloak` - hides content until the Volt runtime initializes
160128 - Event options for `data-volt-on-*` attributes:
161161- - `.prevent` — calls `preventDefault()` on the event
162162- - `.stop` — stops propagation
163163- - `.self` — triggers only when the event target is the bound element
164164- - `.window` — attaches the listener to `window`
165165- - `.document` — attaches the listener to `document`
166166- - `.once` — runs the handler a single time
167167- - `.debounce` — defers handler execution (optional milliseconds)
168168- - `.throttle` — limits handler frequency (optional milliseconds)
169169- - `.passive` — adds a passive event listener for scroll/touch performance
129129+ - `.prevent` - calls `preventDefault()` on the event
130130+ - `.stop` - stops propagation
131131+ - `.self` - triggers only when the event target is the bound element
132132+ - `.window` - attaches the listener to `window`
133133+ - `.document` - attaches the listener to `document`
134134+ - `.once` - runs the handler a single time
135135+ - `.debounce` - defers handler execution (optional milliseconds)
136136+ - `.throttle` - limits handler frequency (optional milliseconds)
137137+ - `.passive` - adds a passive event listener for scroll/touch performance
170138 - Input options for `data-volt-bind` and `data-volt-model`:
171171- - `.number` — coerces values to numbers
172172- - `.trim` — removes surrounding whitespace
173173- - `.lazy` — syncs only on `change` instead of `input`
174174- - `.debounce` — delays updates to reduce jitter
139139+ - `.number` - coerces values to numbers
140140+ - `.trim` - removes surrounding whitespace
141141+ - `.lazy` - syncs only on `change` instead of `input`
142142+ - `.debounce` - delays updates to reduce jitter
175143176144### Global State
177145···206174 - Timing utilities and easing functions
207175 - Integration with `data-volt-if` and `data-volt-show` for automatic transitions
208176177177+### Streaming & Patch Engine
178178+179179+**Goal:** Enable real-time updates via SSE/WebSocket streaming with intelligent DOM patching.
180180+**Outcome:** Volt.js can receive and apply live updates from the server
181181+**Deliverables:**
182182+ - Server-Sent Events (SSE) integration
183183+ - `data-volt-stream` attribute for SSE endpoints
184184+ - Signal patching from backend (`data-signals-*` merge system)
185185+ - Backend action system with `$$action()` syntax (TBD on final syntax decision)
186186+ - JSON Patch parser and DOM morphing engine
187187+ - WebSocket as alternative to SSE
188188+ - `data-volt-ignore-morph` for selective patch exclusion
189189+190190+### Persistence & Offline
191191+192192+**Goal:** Introduce persistent storage and offline-first behaviors.
193193+**Outcome:** Resilient state persistence and offline replay built into Volt.js.
194194+**Deliverables:**
195195+ - ✓ Persistent signals (localStorage, sessionStorage, indexedDb)
196196+ - ✓ Storage plugin (`data-volt-persist`)
197197+ - Storage modifiers on signals:
198198+ - `.local` modifier for localStorage persistence
199199+ - `.session` modifier for sessionStorage persistence
200200+ - `.ifmissing` modifier for conditional initialization
201201+ - Offline queue for deferred stream events and HTTP requests
202202+ - Sync strategy API (merge, overwrite, patch) for conflict resolution
203203+ - Service Worker integration for offline-first apps
204204+ - Background sync for deferred requests
205205+ - Cache invalidation strategies
206206+ - Cross-tab synchronization via `BroadcastChannel`
207207+209208### Background Requests & Reactive Polling
210209211210**Goal:** Enable declarative background data fetching and periodic updates within the Volt.js runtime.
···242241 - Developer overlay for inspecting signals, subscriptions, and effects
243242 - Dev logging toggle (`Volt.debug = true`)
244243 - Browser console integration (`window.$volt.inspect()`)
245245- - Signal dependency graph visualization
244244+ - Signal dependency graph visualization (graph data structure implemented in [proxy](#proxy-based-reactivity-enhancements) milestone)
246245 - Performance profiling tools
247246 - Request/response debugging (HTTP actions, SSE streams)
248247 - Time-travel debugging for signal history
249248 - Browser DevTools extension
250249251251-### Documentation & Stability Pass
252252-253253-**Goal:** Prepare for stable release by finalizing docs, polish, and performance.
254254-**Outcome:** Volt.js is stable, documented, performant, and ready for production.
255255-**Deliverables:**
256256- - ✓ Documentation site (VitePress)
257257- - Full API reference with examples
258258- - Performance benchmarks (vs htmx, Alpine)
259259- - Browser matrix tests (Chromium, Gecko, WebKit)
260260- - Accessibility audits (ARIA)
261261- - Freeze API surface for 1.0
262262-263250### Stable Release
264251265265-**Goal:** Ship the first stable version of Volt.js
266266-**Outcome:** Volt.js 1.0 is released as a mature, fully documented, type-safe, reactive web framework
252252+**Goal:** Prepare & ship the stable release
253253+**Outcome:** Volt.js 1.0 is stable, documented, performant, and ready for production.
267254**Deliverables:**
255255+ - ✓ Documentation site (VitePress)
256256+ - Full API reference with examples -> refactor generator in `@volt/dev` package
268257 - Finalized plugin registry and CLI (`volt plugins list/init`)
269258 - Versioned documentation (stormlightlabs.github.io/volt)
270259 - Announcement post and release notes
+148
docs/internals/debugging.md
···11+# Debugging
22+33+The Volt.js debugging system provides introspection and visualization tools for reactive primitives.
44+It's a lazy-loadable module (`volt/debug`) that doesn't affect production bundle size.
55+66+## Architecture
77+88+The debugging system consists of three interconnected modules:
99+1010+1. **Registry** tracks all signals and reactive objects with metadata (ID, type, name, creation timestamp).
1111+ Uses WeakMaps and WeakRefs to avoid memory leaks because signals can be garbage collected normally.
1212+ Auto-increments IDs like `signal-1`, `computed-2`, `reactive-3`.
1313+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.
1414+3. **Logger** provides console output utilities for inspecting signals, viewing dependency trees, watching value changes, and tracing updates with stack traces.
1515+1616+## Debug API
1717+1818+The module exports wrapped versions of core primitives that automatically register with the debug system:
1919+2020+- `debugSignal()` creates a signal and registers it with optional name. Returns standard Signal interface.
2121+- `debugComputed()` creates a computed signal and registers it. Attempts to record dependency relationships (though this is currently limited by internal tracking visibility).
2222+- `debugReactive()` creates a reactive proxy and registers it for introspection.
2323+2424+These wrappers are drop-in replacements for the core APIs. For existing code, use `attachDebugger()` to register signals post-creation.
2525+2626+## The vdebugger Object
2727+2828+All debugging utilities are also exported as methods on a single `vdebugger` namespace object:
2929+3030+```ts
3131+vdebugger.signal(0, 'count') // Create debug signal
3232+vdebugger.getAllSignals() // Get all tracked signals
3333+vdebugger.log(mySignal) // Pretty-print signal info
3434+vdebugger.trace(mySignal) // Trace all updates
3535+vdebugger.watch(mySignal) // Watch with full dependency tree
3636+vdebugger.buildGraph(signals) // Build dependency graph
3737+vdebugger.detectCycles(mySignal) // Find circular dependencies
3838+```
3939+4040+This namespace provides a convenient entry point for debugging in the browser console.
4141+4242+## Registry System
4343+4444+The registry maintains two separate tracking systems:
4545+4646+1. **Signal Registry** uses a WeakMap to store metadata and a Set of WeakRefs to track all signals.
4747+ When `getAllSignals()` is called, it automatically cleans up garbage-collected signals by checking `WeakRef.deref()`.
4848+2. **Reactive Registry** mirrors this pattern for reactive objects, storing metadata and WeakRefs separately.
4949+5050+Metadata includes:
5151+5252+- `id`: Unique identifier with type prefix
5353+- `type`: One of "signal", "computed", "reactive"
5454+- `name`: Optional developer-provided name
5555+- `createdAt`: Timestamp for age calculations
5656+5757+The registry exposes `getSignalInfo()` and `getReactiveInfo()` which combine metadata with current value and calculated age.
5858+The `nameSignal()` and `nameReactive()` functions allow naming signals after creation.
5959+6060+Registry stats can be retrieved via `getRegistryStats()` which counts regular signals, computed signals, and reactive objects.
6161+6262+## Dependency Graph
6363+6464+The graph module tracks relationships using two WeakMaps:
6565+6666+1. `dependencies` maps from signal to Set of signals it depends on.
6767+2. `dependents` maps from signal to Set of signals that depend on it.
6868+6969+When `recordDependencies()` is called, it updates both maps bidirectionally. This enables efficient queries in both directions.
7070+It allows you to answer, "what does this signal depend on?" and "what depends on this signal?"
7171+7272+### Graph Operations
7373+7474+- `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.
7575+- `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.
7676+- `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.
7777+- `hasDependency()` checks for direct dependency relationship between two signals.
7878+7979+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.
8080+8181+## Logging Utilities
8282+8383+The logger provides multiple output formats:
8484+8585+`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.
8686+8787+`logAllSignals()` lists all tracked signals in a compact format with ID, name, and value.
8888+8989+`logSignalTable()` outputs signals as a formatted console table with columns for ID, name, type, value (truncated), age, dependency count, and dependent count.
9090+9191+`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).
9292+9393+`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.
9494+9595+`watch()` subscribes to a signal and logs full information on every update, including timestamp and complete dependency tree. Returns unsubscribe function for cleanup.
9696+9797+`enableGlobalTracing()` and `disableGlobalTracing()` enable or disable tracing for all registered signals. Useful for debugging complex reactive flows.
9898+9999+## Usage Patterns
100100+101101+For development, import debug utilities directly:
102102+103103+```ts
104104+import { debugSignal, debugComputed, logAllSignals, buildDependencyGraph } from 'volt/debug';
105105+```
106106+107107+For debugging existing code, attach debugger to existing signals:
108108+109109+```ts
110110+import { signal } from 'volt';
111111+import { attachDebugger, vdebugger } from 'volt/debug';
112112+113113+const count = signal(0);
114114+attachDebugger(count, 'signal', 'count');
115115+vdebugger.log(count);
116116+```
117117+118118+For browser console debugging, expose vdebugger globally:
119119+120120+```ts
121121+import { vdebugger } from 'volt/debug';
122122+window.vdebugger = vdebugger;
123123+```
124124+125125+Then in console:
126126+127127+```js
128128+vdebugger.logAll()
129129+vdebugger.trace(someSignal)
130130+vdebugger.buildGraph(vdebugger.getAllSignals())
131131+```
132132+133133+## Memory Considerations
134134+135135+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.
136136+137137+The dependency graph also uses WeakMaps, so edges are cleaned up when signals are collected.
138138+139139+However, tracing and watching create subscriptions which hold references to signals. Always call the returned unsubscribe function when done watching to allow cleanup.
140140+141141+## Limitations
142142+143143+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.
144144+2. Trace unsubscription doesn't work properly because unsubscribe functions aren't stored in the traceListeners WeakMap.
145145+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.
146146+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.
147147+148148+These limitations don't affect the core reactive system, they only reduce the visibility of the debug tools.
+205
docs/internals/proxies.md
···11# Proxy Objects
22+33+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.
44+55+## Core Design
66+77+The reactive system uses JavaScript Proxies to intercept property access and mutations.
88+Each reactive proxy is backed by the original raw object and a map of signals: one signal per property, created lazily on first access.
99+1010+This design provides:
1111+1212+**Transparency**: Access `obj.count` instead of `obj.count.get()`. The proxy unwraps signals automatically.
1313+1414+**Deep Reactivity**: Nested objects and arrays are recursively wrapped, so `obj.nested.value` is reactive all the way down.
1515+1616+**Lazy Signals**: Signals are only created when properties are accessed. An object with 100 properties only creates signals for the properties you actually use.
1717+1818+**Native Array Methods**: Array mutators like `push`, `pop`, `splice` work naturally and trigger updates correctly.
1919+2020+## Three WeakMaps
2121+2222+The system uses three WeakMaps to track relationships:
2323+2424+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.
2525+2. `rawToReactive` maps from original object to reactive proxy.
2626+ This ensures only one proxy per object. Calling `reactive()` twice on the same object returns the same proxy instance.
2727+3. `targetToSignals` maps from target object to a Map of property signals.
2828+ Each target has its own Map of `(key: string | symbol) -> Signal`. Signals are created lazily in `getPropertySignal()`.
2929+3030+WeakMaps allow us to avoid preventing garbage collection of proxies or targets.
3131+3232+## Property Access (get trap)
3333+3434+When you access `proxy.count`, the get trap:
3535+3636+1. Checks for special keys: `__v_raw` returns the raw target, `__v_isReactive` returns true. These enable introspection.
3737+2. For arrays, checks if the key is a mutator method (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, `copyWithin`).
3838+If so, returns a wrapper function that applies the mutation to the target, then updates all affected index signals and the length signal.
3939+3. Gets or creates the signal for this property via `getPropertySignal()`.
4040+4. Calls `recordDep(sig)` to track this access for dependency tracking (if inside a computed or effect).
4141+5. Gets the actual value using `Reflect.get()`.
4242+6. If the value is an object, wraps it recursively with `reactive()` before returning. This provides deep reactivity.
4343+7. If the value is not an object, returns `sig.get()` which provides the signal's current value.
4444+4545+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.
4646+4747+## Property Mutation (set trap)
4848+4949+When you assign `proxy.count = 5`, the set trap:
5050+5151+1. Reads the old value via `Reflect.get()`.
5252+2. Performs the actual mutation with `Reflect.set()`.
5353+3. If the value changed (old !== new), gets the property signal and calls `sig.set(value)`.
5454+4. Returns the result from `Reflect.set()` to indicate success.
5555+5656+This ensures that mutations to the raw object and signal notifications happen atomically.
5757+5858+## Property Deletion (deleteProperty trap)
5959+6060+When you `delete proxy.count`:
6161+6262+1. Checks if the property existed with `Reflect.has()`.
6363+2. Performs the deletion with `Reflect.deleteProperty()`.
6464+3. If the property existed and deletion succeeded, gets the property signal and sets it to `undefined`.
6565+4. Returns the deletion result.
6666+6767+Setting the signal to `undefined` ensures any computeds or effects depending on that property get notified about the deletion.
6868+6969+## Property Existence (has trap)
7070+7171+The `in` operator (`'count' in proxy`) goes through the has trap:
7272+7373+1. Gets the property signal (creating it if needed).
7474+2. Records the dependency with `recordDep()`.
7575+3. Returns `Reflect.has()` result.
7676+7777+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.
7878+7979+## Array Mutators
8080+8181+Arrays get special handling because native methods like `push` mutate multiple indices simultaneously. The array mutator wrapper:
8282+8383+1. Gets the array method from the target (e.g., `Array.prototype.push`).
8484+2. Records the old length.
8585+3. Applies the method to the target with the provided arguments.
8686+4. Calculates the new length and determines the maximum index that might have changed.
8787+5. Loops through all potentially affected indices, gets/creates their signals, and updates them with the new values from the target.
8888+6. If the length changed, updates the length signal.
8989+7. Returns the result from the native method.
9090+9191+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.
9292+9393+The mutator list is hardcoded as a Set: `['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin']`.
9494+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.
9595+9696+## Signal Creation
9797+9898+The `getPropertySignal()` helper:
9999+100100+1. Gets the Map of signals for this target from `targetToSignals`, or creates one if needed.
101101+2. Looks up the signal for this specific key in the Map.
102102+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.
103103+4. Returns the signal.
104104+105105+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.
106106+107107+## Integration with Signal System
108108+109109+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.
110110+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.
111111+This means all the signal behavior works: dependency tracking via `recordDep()`, subscriber notifications, equality checks (`value === newValue` to prevent unnecessary updates).
112112+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)`.
113113+114114+## Deep Reactivity
115115+116116+Nested objects are made reactive recursively. When the get trap encounters an object value, it wraps it with `reactive()` before returning.
117117+118118+The `reactive()` function itself checks `rawToReactive` first, so if that nested object was already wrapped, it returns the existing proxy. This means:
119119+120120+```ts
121121+const state = reactive({ nested: { count: 0 } });
122122+const nested1 = state.nested;
123123+const nested2 = state.nested;
124124+nested1 === nested2; // true, same proxy
125125+```
126126+127127+Arrays within reactive objects are also proxied, and arrays containing objects have those objects proxied when accessed:
128128+129129+```ts
130130+const state = reactive({ items: [{ id: 1 }, { id: 2 }] });
131131+state.items[0].id = 3; // fully reactive
132132+state.items.push({ id: 4 }); // also reactive
133133+```
134134+135135+## Unwrapping with toRaw
136136+137137+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).
138138+139139+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).
140140+141141+## Checking Reactivity
142142+143143+The `isReactive()` function checks if a value is a reactive proxy by testing if it's an object and exists in `reactiveToRaw`.
144144+This is simpler than checking for the `__v_isReactive` property because it doesn't trigger the get trap.
145145+146146+## Type Safety
147147+148148+TypeScript types flow through transparently. If you pass `{ count: number }` to `reactive()`, you get back a reactive object typed as `{ count: number }`.
149149+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.
150150+151151+## Performance Characteristics
152152+153153+- Creating a reactive proxy has minimal overhead, as it just creates the Proxy object and adding two WeakMap entries.
154154+ - The first property access has the overhead of creating a signal and Map entries.
155155+ - Subsequent property access is very fast
156156+ - WeakMap lookup for the signal Map, then Map lookup for the signal, then signal.get().
157157+ - Mutations are similarly fast
158158+ - WeakMap lookups plus signal.set().
159159+ - The WeakMaps have zero memory overhead for garbage collection so when a proxy is no longer referenced, all its metadata is collected automatically.
160160+- 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.
161161+162162+## Edge Cases
163163+164164+**Non-object values**: `reactive()` logs a warning and returns the value unchanged.
165165+You can't make primitives reactive without wrapping them in a `signal()`.
166166+167167+**Already reactive**: Calling `reactive()` on a proxy returns the same proxy immediately.
168168+169169+**Prototype pollution**: The proxy traps use `Reflect` methods which respect the prototype chain naturally.
170170+No special protection is needed because signals are stored in a separate WeakMap, not on the object itself.
171171+172172+**Symbol properties**: Fully supported. Symbols can be used as property keys and get their own signals just like string keys.
173173+174174+**Non-enumerable properties**: Work correctly. The get/set traps handle them the same as enumerable properties.
175175+176176+**Frozen/sealed objects**: Setting properties on frozen objects will fail in `Reflect.set()` and the set trap will return false.
177177+The signal won't be updated, maintaining consistency.
178178+179179+## Signal vs. Reactive
180180+181181+Use `signal()` when:
182182+183183+- You have a single primitive value
184184+- You want explicit get/set calls
185185+- You're storing a function (functions can't be proxy targets)
186186+187187+Use `reactive()` when:
188188+189189+- You have an object with multiple properties
190190+- You want natural property access syntax
191191+- You have nested objects or arrays
192192+- You're integrating with code that expects plain objects
193193+194194+Both are backed by the same signal primitive. The choice is about API ergonomics, not capability.
195195+196196+## Implementation Notes
197197+198198+The proxy system is implemented in `lib/src/core/reactive.ts` and depends only on `signal()` and `recordDep()` from the tracker.
199199+It's completely independent of the binding system, expression evaluator, and other framework features.
200200+201201+This separation means you can use reactive objects in any context:
202202+ - In the DOM binding system via `data-volt-*` attributes
203203+ - Programmatic code with `mount()`
204204+ - Standalone without any UI at all.
205205+206206+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.