what-core 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/reactive.js"],
4
- "sourcesContent": ["// What Framework - Reactive Primitives\n// Signals + Effects: fine-grained reactivity without virtual DOM overhead\n//\n// Upgrades:\n// - Topological ordering: computed/effects sorted by _level to prevent diamond glitches\n// - Iterative computed evaluation: no recursion, handles 10K+ depth chains\n// - Ownership tree: createRoot children auto-dispose when parent disposes\n// - Performance: cached levels, lazy sort, fast-path notify, minimal allocation\n\n// Dev-mode flag \u2014 build tools can dead-code-eliminate when false\nexport const __DEV__ = typeof process !== 'undefined'\n ? process.env?.NODE_ENV !== 'production'\n : true;\n\n// DevTools hooks \u2014 set by what-devtools when installed.\n// These are no-ops in production (dead-code eliminated with __DEV__).\nexport let __devtools = null;\n\n/** @internal Install devtools hooks. Called by what-devtools. */\nexport function __setDevToolsHooks(hooks) {\n if (__DEV__) __devtools = hooks;\n}\n\nlet currentEffect = null;\nlet currentRoot = null;\nlet currentOwner = null; // Ownership tree: tracks current owner context\nlet insideComputed = false; // Track whether we're inside a computed() callback (dev-mode warning)\nlet batchDepth = 0;\nlet pendingEffects = [];\nlet pendingNeedSort = false; // Track whether pendingEffects actually needs sorting\n\n// WeakMap: subscriber Set \u2192 owning computed's inner effect (null/absent for signals)\n// Used for topological level computation.\nconst subSetOwner = new WeakMap();\n\n// --- Iterative Computed Evaluation State ---\n// Uses a throw/catch trampoline to convert recursive computed evaluation\n// to iterative. When a computed fn() reads another dirty computed, instead\n// of recursing, we throw a sentinel that gets caught by the outer loop.\nconst NEEDS_UPSTREAM = Symbol('needs_upstream');\nlet iterativeEvalStack = null; // array when inside evaluation loop, null otherwise\n\n// --- Signal ---\n// A reactive value. Reading inside an effect auto-tracks the dependency.\n// Writing triggers only the effects that depend on this signal.\n\nexport function signal(initial, debugName) {\n let value = initial;\n const subs = new Set();\n\n // Unified getter/setter: sig() reads, sig(newVal) writes\n function sig(...args) {\n if (args.length === 0) {\n // Read\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n // Write\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n }\n\n sig.set = (next) => {\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof next === 'function' ? next(value) : next;\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n };\n\n sig.peek = () => value;\n\n sig.subscribe = (fn) => {\n return effect(() => fn(sig()));\n };\n\n sig._signal = true;\n if (__DEV__) {\n sig._subs = subs;\n if (debugName) sig._debugName = debugName;\n }\n\n // Notify devtools of signal creation\n if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);\n\n return sig;\n}\n\n// --- Computed ---\n// Derived signal. Lazy: only recomputes when a dependency changes AND it's read.\n// Topological level: max(dependency levels) + 1, computed from source signals (level 0).\n\nexport function computed(fn) {\n let value, dirty = true;\n const subs = new Set();\n\n const inner = _createEffect(() => {\n const prevInsideComputed = insideComputed;\n if (__DEV__) insideComputed = true;\n try {\n value = fn();\n dirty = false;\n } finally {\n if (__DEV__) insideComputed = prevInsideComputed;\n }\n }, true);\n\n // Computed nodes start at level 1. Updated when graph structure changes.\n inner._level = 1;\n inner._computed = true;\n inner._computedSubs = subs;\n\n // Register this subscriber set as owned by this computed\n subSetOwner.set(subs, inner);\n\n // Store markDirty/isDirty closures on the inner effect for iterative eval\n inner._markDirty = () => { dirty = true; };\n inner._isDirty = () => dirty;\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n if (dirty) _evaluateComputed(inner);\n return value;\n }\n\n // When a dependency changes, mark dirty AND propagate to our subscribers.\n inner._onNotify = () => {\n dirty = true;\n if (subs.size > 0) notify(subs);\n };\n\n read._signal = true;\n read.peek = () => {\n if (dirty) _evaluateComputed(inner);\n return value;\n };\n\n return read;\n}\n\n// --- Iterative Computed Evaluation ---\n//\n// Problem: A chain of N dirty computeds causes O(N) recursive calls:\n// C_N.read() \u2192 eval \u2192 fn() \u2192 C_{N-1}.read() \u2192 eval \u2192 fn() \u2192 ... \u2192 C_1.read() \u2192 eval \u2192 fn()\n// This overflows the stack at ~3500 depth.\n//\n// Solution: Throw/catch trampoline. The outermost _evaluateComputed manages a\n// stack (array). When a computed's fn() reads another dirty computed during\n// evaluation, _evaluateComputed throws NEEDS_UPSTREAM. The outer loop catches\n// this, adds the upstream to the stack, and processes from the bottom up.\n// This converts O(N) call depth to O(1) per computed (just the outermost loop).\n\nfunction _evaluateComputed(computedEffect) {\n if (iterativeEvalStack !== null) {\n // We're inside the outermost evaluation loop, and a computed's fn()\n // is reading another dirty computed. Push it onto the stack and throw\n // to abort the current fn() so the outer loop can process it first.\n iterativeEvalStack.push(computedEffect);\n throw NEEDS_UPSTREAM;\n }\n\n // Outermost call \u2014 enter the iterative evaluation loop.\n // The stack grows as we discover dirty upstream computeds.\n const stack = [computedEffect];\n iterativeEvalStack = stack;\n\n try {\n while (stack.length > 0) {\n const current = stack[stack.length - 1];\n\n if (!current._isDirty || !current._isDirty()) {\n // Already clean \u2014 pop and continue\n stack.pop();\n continue;\n }\n\n // Pre-scan known deps: if any are dirty computeds, push them onto\n // the stack first (bottom-up). This avoids the O(N^2) worst case\n // where throw/catch restarts from the top on each dirty upstream.\n let pushedUpstream = false;\n const deps = current.deps;\n for (let i = 0; i < deps.length; i++) {\n const depOwner = subSetOwner.get(deps[i]);\n if (depOwner && depOwner._computed && depOwner._isDirty && depOwner._isDirty()) {\n stack.push(depOwner);\n pushedUpstream = true;\n }\n }\n if (pushedUpstream) {\n // Process dirty upstreams first before re-evaluating current\n continue;\n }\n\n // All known deps are clean \u2014 evaluate. throw/catch is fallback\n // for newly-discovered deps only.\n try {\n const prevDepsLen = current.deps.length;\n _runEffect(current);\n // Only recompute level when graph structure changes\n if (current.deps.length !== prevDepsLen) {\n _updateLevel(current);\n }\n stack.pop(); // Successfully evaluated\n } catch (err) {\n if (err === NEEDS_UPSTREAM) {\n // A dirty upstream was discovered and pushed onto the stack.\n // Re-mark this computed dirty since its fn() was aborted mid-execution.\n current._markDirty();\n // The upstream is now at stack[stack.length-1]. Loop continues.\n } else {\n throw err; // Re-throw real errors\n }\n }\n }\n } finally {\n iterativeEvalStack = null;\n }\n}\n\n// Update the topological level of a computed/effect based on its current dependencies.\nfunction _updateLevel(e) {\n let maxDepLevel = 0;\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) {\n const owner = subSetOwner.get(deps[i]);\n if (owner) {\n const depLevel = owner._level;\n if (depLevel > maxDepLevel) maxDepLevel = depLevel;\n }\n }\n e._level = maxDepLevel + 1;\n}\n\n// --- Effect ---\n// Runs a function, auto-tracking signal reads. Re-runs when deps change.\n// Returns a dispose function.\n\nexport function effect(fn, opts) {\n const e = _createEffect(fn);\n e._level = 1;\n // First run: skip cleanup (deps is empty), just run and track\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } finally {\n currentEffect = prev;\n }\n // Compute level after first run based on actual dependencies (cached).\n _updateLevel(e);\n // Mark as stable after first run \u2014 subsequent re-runs skip cleanup/re-subscribe\n if (opts?.stable) e._stable = true;\n const dispose = () => _disposeEffect(e);\n // Register with current root for automatic cleanup\n if (currentRoot) {\n currentRoot.disposals.push(dispose);\n }\n return dispose;\n}\n\n// --- Batch ---\n// Group multiple signal writes; effects run once at the end.\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) flush();\n }\n}\n\n// --- Internals ---\n\nfunction _createEffect(fn, lazy) {\n // Minimal object shape \u2014 computed() adds extra properties after creation.\n // Keeping the base object small helps V8 optimize for the common (effect) case.\n const e = {\n fn,\n deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)\n lazy: lazy || false,\n _onNotify: null,\n disposed: false,\n _pending: false,\n _stable: false, // stable effects skip cleanup/re-subscribe on re-run\n _level: 0, // topological depth: signals=0, computed/effects=max(deps)+1\n _computed: false, // true for computed inner effects\n _computedSubs: null, // reference to the computed's subscriber set\n _isDirty: null, // function to check if computed is dirty (set by computed())\n _markDirty: null, // function to mark computed dirty (set by computed())\n };\n if (__DEV__ && __devtools) __devtools.onEffectCreate(e);\n return e;\n}\n\nfunction _runEffect(e) {\n if (e.disposed) return;\n\n // Stable effect fast path: deps don't change, skip cleanup/re-subscribe.\n if (e._stable) {\n if (e._cleanup) {\n try { e._cleanup(); } catch (err) {\n if (__DEV__) console.warn('[what] Error in effect cleanup:', err);\n }\n e._cleanup = null;\n }\n const prev = currentEffect;\n currentEffect = null; // Don't re-track deps (already subscribed)\n try {\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n return;\n }\n\n cleanup(e);\n // Run effect cleanup from previous run\n if (e._cleanup) {\n try { e._cleanup(); } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });\n if (__DEV__) console.warn('[what] Error in effect cleanup:', err);\n }\n e._cleanup = null;\n }\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n // Capture cleanup function if returned\n if (typeof result === 'function') {\n e._cleanup = result;\n }\n } catch (err) {\n if (err === NEEDS_UPSTREAM) throw err; // Iterative eval sentinel \u2014 not a real error\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n throw err;\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n}\n\nfunction _disposeEffect(e) {\n e.disposed = true;\n if (__DEV__ && __devtools) __devtools.onEffectDispose(e);\n cleanup(e);\n // Run cleanup on dispose\n if (e._cleanup) {\n try { e._cleanup(); } catch (err) {\n if (__DEV__) console.warn('[what] Error in effect cleanup on dispose:', err);\n }\n e._cleanup = null;\n }\n}\n\nfunction cleanup(e) {\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) deps[i].delete(e);\n deps.length = 0;\n}\n\n// --- Notification ---\n// Iterative notification to prevent stack overflow on deep computed chains.\n// Uses a reusable queue to avoid per-call array allocation.\n// When notify() encounters _onNotify callbacks (from computeds), those may\n// call notify() recursively. The queue drains iteratively in the outermost call.\n\nlet notifyDepth = 0; // Tracks recursive notify depth\nlet notifyQueue = null; // Reusable queue, allocated on first recursive call\nlet notifyQueueLen = 0; // Length of the queue\n\nfunction notify(subs) {\n // Fast path: no recursive notifications in progress \u2014 iterate directly.\n // This avoids array allocation for the common case (signal \u2192 effects).\n if (notifyDepth === 0) {\n notifyDepth = 1;\n try {\n for (const e of subs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n // Computed subscriber: mark dirty and propagate.\n // _onNotify may call notify() recursively \u2014 tracked by notifyDepth.\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n // Inline execution for stable effects\n const prev = currentEffect;\n currentEffect = null;\n try {\n const result = e.fn();\n if (typeof result === 'function') {\n if (e._cleanup) try { e._cleanup(); } catch (err) {}\n e._cleanup = result;\n }\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n // Drain any queued subscriber sets from recursive notify calls\n if (notifyQueueLen > 0) {\n let qi = 0;\n while (qi < notifyQueueLen) {\n const queuedSubs = notifyQueue[qi];\n notifyQueue[qi] = null; // Allow GC\n qi++;\n for (const e of queuedSubs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n const result = e.fn();\n if (typeof result === 'function') {\n if (e._cleanup) try { e._cleanup(); } catch (err) {}\n e._cleanup = result;\n }\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n }\n notifyQueueLen = 0;\n }\n } finally {\n notifyDepth = 0;\n }\n if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();\n } else {\n // Recursive call \u2014 queue the subscriber set for the outermost call to drain.\n if (notifyQueue === null) notifyQueue = [];\n if (notifyQueueLen >= notifyQueue.length) {\n notifyQueue.push(subs);\n } else {\n notifyQueue[notifyQueueLen] = subs;\n }\n notifyQueueLen++;\n }\n}\n\nlet microtaskScheduled = false;\nfunction scheduleMicrotask() {\n if (!microtaskScheduled) {\n microtaskScheduled = true;\n queueMicrotask(() => {\n microtaskScheduled = false;\n flush();\n });\n }\n}\n\nlet isFlushing = false;\n\nfunction flush() {\n // Re-entrancy guard: if flush() is called during an active flush (e.g., via\n // flushSync() inside a component render or effect), skip to prevent infinite\n // recursion. Pending effects will be picked up by the outer flush's while-loop.\n if (isFlushing) return;\n isFlushing = true;\n\n try {\n let iterations = 0;\n while (pendingEffects.length > 0 && iterations < 25) {\n const batch = pendingEffects;\n pendingEffects = [];\n\n // Topological sort: execute effects in level order (lowest first).\n // Fast paths:\n // 1. Single effect \u2014 no sort needed (most common case for microtask flush)\n // 2. Already sorted \u2014 skip sort (common when effects added in level order)\n // 3. Multiple effects at different levels \u2014 sort required\n if (batch.length > 1 && pendingNeedSort) {\n batch.sort((a, b) => a._level - b._level);\n }\n pendingNeedSort = false;\n\n for (let i = 0; i < batch.length; i++) {\n const e = batch[i];\n e._pending = false;\n if (!e.disposed && !e._onNotify) {\n const prevDepsLen = e.deps.length;\n _runEffect(e);\n // Update level only if deps changed (graph structure change)\n if (!e._computed && e.deps.length !== prevDepsLen) {\n _updateLevel(e);\n }\n }\n }\n iterations++;\n }\n if (iterations >= 25) {\n // Clear pending effects to prevent further damage\n for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;\n pendingEffects.length = 0;\n\n if (__DEV__) {\n const remaining = pendingEffects.slice(0, 3);\n const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');\n console.warn(\n `[what] Possible infinite effect loop detected (25 iterations). ` +\n `Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +\n `Use untrack() to read signals without subscribing. ` +\n `Looping effects: ${effectNames.join(', ')}`\n );\n } else {\n console.warn('[what] Possible infinite effect loop detected');\n }\n }\n } finally {\n isFlushing = false;\n }\n}\n\n// --- Memo ---\n// Eager computed that only propagates when the value actually changes.\n// Fix: Instead of calling notify(subs) inline (which bypasses topological sort\n// and causes diamond-dependency glitches), push memo subscribers into\n// pendingEffects and let them go through the sorted flush() path.\nexport function memo(fn) {\n let value;\n const subs = new Set();\n\n const e = _createEffect(() => {\n const next = fn();\n if (!Object.is(value, next)) {\n value = next;\n // Push subscribers into pendingEffects for topological flush\n // instead of inline notify() which can cause diamond glitches\n for (const sub of subs) {\n if (sub.disposed) continue;\n if (sub._onNotify) {\n // Computed subscriber: mark dirty and propagate\n sub._onNotify();\n } else if (!sub._pending) {\n sub._pending = true;\n const level = sub._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(sub);\n }\n }\n }\n });\n\n e._level = 1;\n\n _runEffect(e);\n _updateLevel(e);\n\n // Register subscriber set owner for level tracking\n subSetOwner.set(subs, e);\n\n // Register with current root\n if (currentRoot) {\n currentRoot.disposals.push(() => _disposeEffect(e));\n }\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n\n read._signal = true;\n read.peek = () => value;\n return read;\n}\n\n// --- flushSync ---\n// Force all pending effects to run synchronously. Use sparingly.\n// Calling during render or effect execution is a no-op (prevents infinite loops).\nexport function flushSync() {\n if (isFlushing) {\n // Re-entrant call \u2014 silently skip (Solid approach).\n // This prevents infinite loops when flushSync() is called during component\n // render or effect execution. Pending effects will be picked up by the\n // outer flush's while-loop.\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during an active flush (e.g., inside a component render or effect). ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n if (currentEffect) {\n // Called inside an effect/render \u2014 skip with warning\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during effect execution. ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n microtaskScheduled = false;\n flush();\n}\n\n// --- Untrack ---\n// Read signals without subscribing\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// --- getOwner / runWithOwner ---\n// Expose ownership context for advanced use cases (e.g., async operations\n// that need to register disposals with the correct owner).\n\nexport function getOwner() {\n return currentOwner;\n}\n\nexport function runWithOwner(owner, fn) {\n const prev = currentOwner;\n const prevRoot = currentRoot;\n currentOwner = owner;\n currentRoot = owner;\n try {\n return fn();\n } finally {\n currentOwner = prev;\n currentRoot = prevRoot;\n }\n}\n\n// --- createRoot ---\n// Isolated reactive scope with ownership tree.\n// All effects created inside are tracked and disposed together.\n// Child createRoot scopes register with parent owner \u2014 disposing parent\n// automatically disposes all children (prevents orphaned subscriptions).\nexport function createRoot(fn) {\n const prevRoot = currentRoot;\n const prevOwner = currentOwner;\n const root = {\n disposals: [],\n owner: currentOwner, // parent owner for ownership tree\n children: [], // child roots (ownership tree)\n _disposed: false,\n };\n\n // Register this root as a child of the parent owner\n if (currentOwner) {\n currentOwner.children.push(root);\n }\n\n currentRoot = root;\n currentOwner = root;\n\n try {\n const dispose = () => {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first (depth-first, reverse order)\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects (reverse order for LIFO cleanup)\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n\n // Remove from parent's children list\n if (root.owner) {\n const idx = root.owner.children.indexOf(root);\n if (idx >= 0) root.owner.children.splice(idx, 1);\n }\n };\n return fn(dispose);\n } finally {\n currentRoot = prevRoot;\n currentOwner = prevOwner;\n }\n}\n\n// Internal: dispose a root and all its children\nfunction _disposeRoot(root) {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n}\n\n// --- onCleanup ---\n// Register a cleanup function with the current owner/root.\n// Runs when the owner is disposed.\nexport function onCleanup(fn) {\n if (currentRoot) {\n currentRoot.disposals.push(fn);\n }\n}\n"],
4
+ "sourcesContent": ["// What Framework - Reactive Primitives\n// Signals + Effects: fine-grained reactivity without virtual DOM overhead\n//\n// Upgrades:\n// - Topological ordering: computed/effects sorted by _level to prevent diamond glitches\n// - Iterative computed evaluation: no recursion, handles 10K+ depth chains\n// - Ownership tree: createRoot children auto-dispose when parent disposes\n// - Performance: cached levels, lazy sort, fast-path notify, minimal allocation\n\n// Dev-mode flag \u2014 build tools can dead-code-eliminate when false\nexport const __DEV__ = typeof process !== 'undefined'\n ? process.env?.NODE_ENV !== 'production'\n : true;\n\n// DevTools hooks \u2014 set by what-devtools when installed.\n// These are no-ops in production (dead-code eliminated with __DEV__).\nexport let __devtools = null;\n\n/** @internal Install devtools hooks. Called by what-devtools. */\nexport function __setDevToolsHooks(hooks) {\n if (__DEV__) __devtools = hooks;\n}\n\nlet currentEffect = null;\nlet currentRoot = null;\nlet currentOwner = null; // Ownership tree: tracks current owner context\nlet insideComputed = false; // Track whether we're inside a computed() callback (dev-mode warning)\nlet batchDepth = 0;\nlet pendingEffects = [];\nlet pendingNeedSort = false; // Track whether pendingEffects actually needs sorting\n\n// WeakMap: subscriber Set \u2192 owning computed's inner effect (null/absent for signals)\n// Used for topological level computation.\nconst subSetOwner = new WeakMap();\n\n// --- Iterative Computed Evaluation State ---\n// Uses a throw/catch trampoline to convert recursive computed evaluation\n// to iterative. When a computed fn() reads another dirty computed, instead\n// of recursing, we throw a sentinel that gets caught by the outer loop.\nconst NEEDS_UPSTREAM = Symbol('needs_upstream');\nlet iterativeEvalStack = null; // array when inside evaluation loop, null otherwise\n\n// --- Signal ---\n// A reactive value. Reading inside an effect auto-tracks the dependency.\n// Writing triggers only the effects that depend on this signal.\n\nexport function signal(initial, debugName) {\n let value = initial;\n const subs = new Set();\n\n // Unified getter/setter: sig() reads, sig(newVal) writes\n function sig(...args) {\n if (args.length === 0) {\n // Read\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n // Write\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n }\n\n sig.set = (next) => {\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof next === 'function' ? next(value) : next;\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n };\n\n sig.peek = () => value;\n\n sig.subscribe = (fn) => {\n return effect(() => fn(sig()));\n };\n\n sig._signal = true;\n if (__DEV__) {\n sig._subs = subs;\n if (debugName) sig._debugName = debugName;\n }\n\n // Notify devtools of signal creation\n if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);\n\n return sig;\n}\n\n// --- Computed ---\n// Derived signal. Lazy: only recomputes when a dependency changes AND it's read.\n// Topological level: max(dependency levels) + 1, computed from source signals (level 0).\n\nexport function computed(fn) {\n let value, dirty = true;\n const subs = new Set();\n\n const inner = _createEffect(() => {\n const prevInsideComputed = insideComputed;\n if (__DEV__) insideComputed = true;\n try {\n value = fn();\n dirty = false;\n } finally {\n if (__DEV__) insideComputed = prevInsideComputed;\n }\n }, true);\n\n // Computed nodes start at level 1. Updated when graph structure changes.\n inner._level = 1;\n inner._computed = true;\n inner._computedSubs = subs;\n\n // Register this subscriber set as owned by this computed\n subSetOwner.set(subs, inner);\n\n // Store markDirty/isDirty closures on the inner effect for iterative eval\n inner._markDirty = () => { dirty = true; };\n inner._isDirty = () => dirty;\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n if (dirty) _evaluateComputed(inner);\n return value;\n }\n\n // When a dependency changes, mark dirty AND propagate to our subscribers.\n inner._onNotify = () => {\n dirty = true;\n if (subs.size > 0) notify(subs);\n };\n\n read._signal = true;\n read.peek = () => {\n if (dirty) _evaluateComputed(inner);\n return value;\n };\n\n return read;\n}\n\n// --- Iterative Computed Evaluation ---\n//\n// Problem: A chain of N dirty computeds causes O(N) recursive calls:\n// C_N.read() \u2192 eval \u2192 fn() \u2192 C_{N-1}.read() \u2192 eval \u2192 fn() \u2192 ... \u2192 C_1.read() \u2192 eval \u2192 fn()\n// This overflows the stack at ~3500 depth.\n//\n// Solution: Throw/catch trampoline. The outermost _evaluateComputed manages a\n// stack (array). When a computed's fn() reads another dirty computed during\n// evaluation, _evaluateComputed throws NEEDS_UPSTREAM. The outer loop catches\n// this, adds the upstream to the stack, and processes from the bottom up.\n// This converts O(N) call depth to O(1) per computed (just the outermost loop).\n\nfunction _evaluateComputed(computedEffect) {\n if (iterativeEvalStack !== null) {\n // We're inside the outermost evaluation loop, and a computed's fn()\n // is reading another dirty computed. Push it onto the stack and throw\n // to abort the current fn() so the outer loop can process it first.\n iterativeEvalStack.push(computedEffect);\n throw NEEDS_UPSTREAM;\n }\n\n // Outermost call \u2014 enter the iterative evaluation loop.\n // The stack grows as we discover dirty upstream computeds.\n const stack = [computedEffect];\n iterativeEvalStack = stack;\n\n try {\n while (stack.length > 0) {\n const current = stack[stack.length - 1];\n\n if (!current._isDirty || !current._isDirty()) {\n // Already clean \u2014 pop and continue\n stack.pop();\n continue;\n }\n\n // Pre-scan known deps: if any are dirty computeds, push them onto\n // the stack first (bottom-up). This avoids the O(N^2) worst case\n // where throw/catch restarts from the top on each dirty upstream.\n let pushedUpstream = false;\n const deps = current.deps;\n for (let i = 0; i < deps.length; i++) {\n const depOwner = subSetOwner.get(deps[i]);\n if (depOwner && depOwner._computed && depOwner._isDirty && depOwner._isDirty()) {\n stack.push(depOwner);\n pushedUpstream = true;\n }\n }\n if (pushedUpstream) {\n // Process dirty upstreams first before re-evaluating current\n continue;\n }\n\n // All known deps are clean \u2014 evaluate. throw/catch is fallback\n // for newly-discovered deps only.\n try {\n const prevDepsLen = current.deps.length;\n _runEffect(current);\n // Only recompute level when graph structure changes\n if (current.deps.length !== prevDepsLen) {\n _updateLevel(current);\n }\n stack.pop(); // Successfully evaluated\n } catch (err) {\n if (err === NEEDS_UPSTREAM) {\n // A dirty upstream was discovered and pushed onto the stack.\n // Re-mark this computed dirty since its fn() was aborted mid-execution.\n current._markDirty();\n // The upstream is now at stack[stack.length-1]. Loop continues.\n } else {\n throw err; // Re-throw real errors\n }\n }\n }\n } finally {\n iterativeEvalStack = null;\n }\n}\n\n// Update the topological level of a computed/effect based on its current dependencies.\nfunction _updateLevel(e) {\n let maxDepLevel = 0;\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) {\n const owner = subSetOwner.get(deps[i]);\n if (owner) {\n const depLevel = owner._level;\n if (depLevel > maxDepLevel) maxDepLevel = depLevel;\n }\n }\n e._level = maxDepLevel + 1;\n}\n\n// --- Effect ---\n// Runs a function, auto-tracking signal reads. Re-runs when deps change.\n// Returns a dispose function.\n\nexport function effect(fn, opts) {\n const e = _createEffect(fn);\n e._level = 1;\n // First run: skip cleanup (deps is empty), just run and track\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } finally {\n currentEffect = prev;\n }\n // Compute level after first run based on actual dependencies (cached).\n _updateLevel(e);\n // Mark as stable after first run \u2014 subsequent re-runs skip cleanup/re-subscribe\n if (opts?.stable) e._stable = true;\n const dispose = () => _disposeEffect(e);\n // Register with current root for automatic cleanup\n if (currentRoot) {\n currentRoot.disposals.push(dispose);\n }\n return dispose;\n}\n\n// --- Batch ---\n// Group multiple signal writes; effects run once at the end.\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) flush();\n }\n}\n\n// --- Internals ---\n\nfunction _createEffect(fn, lazy) {\n // Minimal object shape \u2014 computed() adds extra properties after creation.\n // Keeping the base object small helps V8 optimize for the common (effect) case.\n const e = {\n fn,\n deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)\n lazy: lazy || false,\n _onNotify: null,\n disposed: false,\n _pending: false,\n _stable: false, // stable effects skip cleanup/re-subscribe on re-run\n _level: 0, // topological depth: signals=0, computed/effects=max(deps)+1\n _computed: false, // true for computed inner effects\n _computedSubs: null, // reference to the computed's subscriber set\n _isDirty: null, // function to check if computed is dirty (set by computed())\n _markDirty: null, // function to mark computed dirty (set by computed())\n };\n if (__DEV__ && __devtools) __devtools.onEffectCreate(e);\n return e;\n}\n\nfunction _runEffect(e) {\n if (e.disposed) return;\n\n // Stable effect fast path: deps don't change, skip cleanup/re-subscribe.\n if (e._stable) {\n runStableEffect(e);\n return;\n }\n\n cleanup(e);\n // Run effect cleanup from previous run\n runEffectCleanup(e, 'effect cleanup');\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n // Capture cleanup function if returned\n if (typeof result === 'function') {\n e._cleanup = result;\n }\n } catch (err) {\n if (err === NEEDS_UPSTREAM) throw err; // Iterative eval sentinel \u2014 not a real error\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n throw err;\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n}\n\nfunction _disposeEffect(e) {\n e.disposed = true;\n if (__DEV__ && __devtools) __devtools.onEffectDispose(e);\n cleanup(e);\n // Run cleanup on dispose\n runEffectCleanup(e, 'effect cleanup on dispose');\n}\n\nfunction reportEffectCleanupError(err, e, phase) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e, phase });\n if (__DEV__) console.warn(`[what] Error in ${phase}:`, err);\n}\n\nfunction runEffectCleanup(e, phase) {\n if (!e._cleanup) return;\n const cleanupFn = e._cleanup;\n e._cleanup = null;\n try {\n cleanupFn();\n } catch (err) {\n reportEffectCleanupError(err, e, phase);\n }\n}\n\nfunction runStableEffect(e) {\n const prev = currentEffect;\n currentEffect = null; // Don't re-track deps (already subscribed)\n try {\n runEffectCleanup(e, 'stable effect cleanup');\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n}\n\nfunction cleanup(e) {\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) deps[i].delete(e);\n deps.length = 0;\n}\n\n// --- Notification ---\n// Iterative notification to prevent stack overflow on deep computed chains.\n// Uses a reusable queue to avoid per-call array allocation.\n// When notify() encounters _onNotify callbacks (from computeds), those may\n// call notify() recursively. The queue drains iteratively in the outermost call.\n\nlet notifyDepth = 0; // Tracks recursive notify depth\nlet notifyQueue = null; // Reusable queue, allocated on first recursive call\nlet notifyQueueLen = 0; // Length of the queue\n\nfunction notify(subs) {\n // Fast path: no recursive notifications in progress \u2014 iterate directly.\n // This avoids array allocation for the common case (signal \u2192 effects).\n if (notifyDepth === 0) {\n notifyDepth = 1;\n try {\n for (const e of subs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n // Computed subscriber: mark dirty and propagate.\n // _onNotify may call notify() recursively \u2014 tracked by notifyDepth.\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n runStableEffect(e);\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n // Drain any queued subscriber sets from recursive notify calls\n if (notifyQueueLen > 0) {\n let qi = 0;\n while (qi < notifyQueueLen) {\n const queuedSubs = notifyQueue[qi];\n notifyQueue[qi] = null; // Allow GC\n qi++;\n for (const e of queuedSubs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n runStableEffect(e);\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n }\n notifyQueueLen = 0;\n }\n } finally {\n notifyDepth = 0;\n }\n if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();\n } else {\n // Recursive call \u2014 queue the subscriber set for the outermost call to drain.\n if (notifyQueue === null) notifyQueue = [];\n if (notifyQueueLen >= notifyQueue.length) {\n notifyQueue.push(subs);\n } else {\n notifyQueue[notifyQueueLen] = subs;\n }\n notifyQueueLen++;\n }\n}\n\nlet microtaskScheduled = false;\nfunction scheduleMicrotask() {\n if (!microtaskScheduled) {\n microtaskScheduled = true;\n queueMicrotask(() => {\n microtaskScheduled = false;\n flush();\n });\n }\n}\n\nlet isFlushing = false;\n\nfunction flush() {\n // Re-entrancy guard: if flush() is called during an active flush (e.g., via\n // flushSync() inside a component render or effect), skip to prevent infinite\n // recursion. Pending effects will be picked up by the outer flush's while-loop.\n if (isFlushing) return;\n isFlushing = true;\n\n try {\n let iterations = 0;\n while (pendingEffects.length > 0 && iterations < 25) {\n const batch = pendingEffects;\n pendingEffects = [];\n\n // Topological sort: execute effects in level order (lowest first).\n // Fast paths:\n // 1. Single effect \u2014 no sort needed (most common case for microtask flush)\n // 2. Already sorted \u2014 skip sort (common when effects added in level order)\n // 3. Multiple effects at different levels \u2014 sort required\n if (batch.length > 1 && pendingNeedSort) {\n batch.sort((a, b) => a._level - b._level);\n }\n pendingNeedSort = false;\n\n for (let i = 0; i < batch.length; i++) {\n const e = batch[i];\n e._pending = false;\n if (!e.disposed && !e._onNotify) {\n const prevDepsLen = e.deps.length;\n _runEffect(e);\n // Update level only if deps changed (graph structure change)\n if (!e._computed && e.deps.length !== prevDepsLen) {\n _updateLevel(e);\n }\n }\n }\n iterations++;\n }\n if (iterations >= 25) {\n // Clear pending effects to prevent further damage\n for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;\n pendingEffects.length = 0;\n\n if (__DEV__) {\n const remaining = pendingEffects.slice(0, 3);\n const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');\n console.warn(\n `[what] Possible infinite effect loop detected (25 iterations). ` +\n `Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +\n `Use untrack() to read signals without subscribing. ` +\n `Looping effects: ${effectNames.join(', ')}`\n );\n } else {\n console.warn('[what] Possible infinite effect loop detected');\n }\n }\n } finally {\n isFlushing = false;\n }\n}\n\n// --- Memo ---\n// Eager computed that only propagates when the value actually changes.\n// Fix: Instead of calling notify(subs) inline (which bypasses topological sort\n// and causes diamond-dependency glitches), push memo subscribers into\n// pendingEffects and let them go through the sorted flush() path.\nexport function memo(fn) {\n let value;\n const subs = new Set();\n\n const e = _createEffect(() => {\n const next = fn();\n if (!Object.is(value, next)) {\n value = next;\n // Push subscribers into pendingEffects for topological flush\n // instead of inline notify() which can cause diamond glitches\n for (const sub of subs) {\n if (sub.disposed) continue;\n if (sub._onNotify) {\n // Computed subscriber: mark dirty and propagate\n sub._onNotify();\n } else if (!sub._pending) {\n sub._pending = true;\n const level = sub._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(sub);\n }\n }\n }\n });\n\n e._level = 1;\n\n _runEffect(e);\n _updateLevel(e);\n\n // Register subscriber set owner for level tracking\n subSetOwner.set(subs, e);\n\n // Register with current root\n if (currentRoot) {\n currentRoot.disposals.push(() => _disposeEffect(e));\n }\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n\n read._signal = true;\n read.peek = () => value;\n return read;\n}\n\n// --- flushSync ---\n// Force all pending effects to run synchronously. Use sparingly.\n// Calling during render or effect execution is a no-op (prevents infinite loops).\nexport function flushSync() {\n if (isFlushing) {\n // Re-entrant call \u2014 silently skip (Solid approach).\n // This prevents infinite loops when flushSync() is called during component\n // render or effect execution. Pending effects will be picked up by the\n // outer flush's while-loop.\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during an active flush (e.g., inside a component render or effect). ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n if (currentEffect) {\n // Called inside an effect/render \u2014 skip with warning\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during effect execution. ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n microtaskScheduled = false;\n flush();\n}\n\n// --- Untrack ---\n// Read signals without subscribing\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// --- getOwner / runWithOwner ---\n// Expose ownership context for advanced use cases (e.g., async operations\n// that need to register disposals with the correct owner).\n\nexport function getOwner() {\n return currentOwner;\n}\n\nexport function runWithOwner(owner, fn) {\n const prev = currentOwner;\n const prevRoot = currentRoot;\n currentOwner = owner;\n currentRoot = owner;\n try {\n return fn();\n } finally {\n currentOwner = prev;\n currentRoot = prevRoot;\n }\n}\n\n// --- createRoot ---\n// Isolated reactive scope with ownership tree.\n// All effects created inside are tracked and disposed together.\n// Child createRoot scopes register with parent owner \u2014 disposing parent\n// automatically disposes all children (prevents orphaned subscriptions).\nexport function createRoot(fn) {\n const prevRoot = currentRoot;\n const prevOwner = currentOwner;\n const root = {\n disposals: [],\n owner: currentOwner, // parent owner for ownership tree\n children: [], // child roots (ownership tree)\n _disposed: false,\n };\n\n // Register this root as a child of the parent owner\n if (currentOwner) {\n currentOwner.children.push(root);\n }\n\n currentRoot = root;\n currentOwner = root;\n\n try {\n const dispose = () => {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first (depth-first, reverse order)\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects (reverse order for LIFO cleanup)\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n\n // Remove from parent's children list\n if (root.owner) {\n const idx = root.owner.children.indexOf(root);\n if (idx >= 0) root.owner.children.splice(idx, 1);\n }\n };\n return fn(dispose);\n } finally {\n currentRoot = prevRoot;\n currentOwner = prevOwner;\n }\n}\n\n// Internal: dispose a root and all its children\nfunction _disposeRoot(root) {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n}\n\n// --- onCleanup ---\n// Register a cleanup function with the current owner/root.\n// Runs when the owner is disposed.\nexport function onCleanup(fn) {\n if (currentRoot) {\n currentRoot.disposals.push(fn);\n }\n}\n"],
5
5
  "mappings": ";AAUO,IAAM,UAAU,OAAO,YAAY,cACtC,OACA;AAIG,IAAI,aAAa;AAGjB,SAAS,mBAAmB,OAAO;AACxC,MAAI,QAAS,cAAa;AAC5B;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/reactive.js"],
4
- "sourcesContent": ["// What Framework - Reactive Primitives\n// Signals + Effects: fine-grained reactivity without virtual DOM overhead\n//\n// Upgrades:\n// - Topological ordering: computed/effects sorted by _level to prevent diamond glitches\n// - Iterative computed evaluation: no recursion, handles 10K+ depth chains\n// - Ownership tree: createRoot children auto-dispose when parent disposes\n// - Performance: cached levels, lazy sort, fast-path notify, minimal allocation\n\n// Dev-mode flag \u2014 build tools can dead-code-eliminate when false\nexport const __DEV__ = typeof process !== 'undefined'\n ? process.env?.NODE_ENV !== 'production'\n : true;\n\n// DevTools hooks \u2014 set by what-devtools when installed.\n// These are no-ops in production (dead-code eliminated with __DEV__).\nexport let __devtools = null;\n\n/** @internal Install devtools hooks. Called by what-devtools. */\nexport function __setDevToolsHooks(hooks) {\n if (__DEV__) __devtools = hooks;\n}\n\nlet currentEffect = null;\nlet currentRoot = null;\nlet currentOwner = null; // Ownership tree: tracks current owner context\nlet insideComputed = false; // Track whether we're inside a computed() callback (dev-mode warning)\nlet batchDepth = 0;\nlet pendingEffects = [];\nlet pendingNeedSort = false; // Track whether pendingEffects actually needs sorting\n\n// WeakMap: subscriber Set \u2192 owning computed's inner effect (null/absent for signals)\n// Used for topological level computation.\nconst subSetOwner = new WeakMap();\n\n// --- Iterative Computed Evaluation State ---\n// Uses a throw/catch trampoline to convert recursive computed evaluation\n// to iterative. When a computed fn() reads another dirty computed, instead\n// of recursing, we throw a sentinel that gets caught by the outer loop.\nconst NEEDS_UPSTREAM = Symbol('needs_upstream');\nlet iterativeEvalStack = null; // array when inside evaluation loop, null otherwise\n\n// --- Signal ---\n// A reactive value. Reading inside an effect auto-tracks the dependency.\n// Writing triggers only the effects that depend on this signal.\n\nexport function signal(initial, debugName) {\n let value = initial;\n const subs = new Set();\n\n // Unified getter/setter: sig() reads, sig(newVal) writes\n function sig(...args) {\n if (args.length === 0) {\n // Read\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n // Write\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n }\n\n sig.set = (next) => {\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof next === 'function' ? next(value) : next;\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n };\n\n sig.peek = () => value;\n\n sig.subscribe = (fn) => {\n return effect(() => fn(sig()));\n };\n\n sig._signal = true;\n if (__DEV__) {\n sig._subs = subs;\n if (debugName) sig._debugName = debugName;\n }\n\n // Notify devtools of signal creation\n if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);\n\n return sig;\n}\n\n// --- Computed ---\n// Derived signal. Lazy: only recomputes when a dependency changes AND it's read.\n// Topological level: max(dependency levels) + 1, computed from source signals (level 0).\n\nexport function computed(fn) {\n let value, dirty = true;\n const subs = new Set();\n\n const inner = _createEffect(() => {\n const prevInsideComputed = insideComputed;\n if (__DEV__) insideComputed = true;\n try {\n value = fn();\n dirty = false;\n } finally {\n if (__DEV__) insideComputed = prevInsideComputed;\n }\n }, true);\n\n // Computed nodes start at level 1. Updated when graph structure changes.\n inner._level = 1;\n inner._computed = true;\n inner._computedSubs = subs;\n\n // Register this subscriber set as owned by this computed\n subSetOwner.set(subs, inner);\n\n // Store markDirty/isDirty closures on the inner effect for iterative eval\n inner._markDirty = () => { dirty = true; };\n inner._isDirty = () => dirty;\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n if (dirty) _evaluateComputed(inner);\n return value;\n }\n\n // When a dependency changes, mark dirty AND propagate to our subscribers.\n inner._onNotify = () => {\n dirty = true;\n if (subs.size > 0) notify(subs);\n };\n\n read._signal = true;\n read.peek = () => {\n if (dirty) _evaluateComputed(inner);\n return value;\n };\n\n return read;\n}\n\n// --- Iterative Computed Evaluation ---\n//\n// Problem: A chain of N dirty computeds causes O(N) recursive calls:\n// C_N.read() \u2192 eval \u2192 fn() \u2192 C_{N-1}.read() \u2192 eval \u2192 fn() \u2192 ... \u2192 C_1.read() \u2192 eval \u2192 fn()\n// This overflows the stack at ~3500 depth.\n//\n// Solution: Throw/catch trampoline. The outermost _evaluateComputed manages a\n// stack (array). When a computed's fn() reads another dirty computed during\n// evaluation, _evaluateComputed throws NEEDS_UPSTREAM. The outer loop catches\n// this, adds the upstream to the stack, and processes from the bottom up.\n// This converts O(N) call depth to O(1) per computed (just the outermost loop).\n\nfunction _evaluateComputed(computedEffect) {\n if (iterativeEvalStack !== null) {\n // We're inside the outermost evaluation loop, and a computed's fn()\n // is reading another dirty computed. Push it onto the stack and throw\n // to abort the current fn() so the outer loop can process it first.\n iterativeEvalStack.push(computedEffect);\n throw NEEDS_UPSTREAM;\n }\n\n // Outermost call \u2014 enter the iterative evaluation loop.\n // The stack grows as we discover dirty upstream computeds.\n const stack = [computedEffect];\n iterativeEvalStack = stack;\n\n try {\n while (stack.length > 0) {\n const current = stack[stack.length - 1];\n\n if (!current._isDirty || !current._isDirty()) {\n // Already clean \u2014 pop and continue\n stack.pop();\n continue;\n }\n\n // Pre-scan known deps: if any are dirty computeds, push them onto\n // the stack first (bottom-up). This avoids the O(N^2) worst case\n // where throw/catch restarts from the top on each dirty upstream.\n let pushedUpstream = false;\n const deps = current.deps;\n for (let i = 0; i < deps.length; i++) {\n const depOwner = subSetOwner.get(deps[i]);\n if (depOwner && depOwner._computed && depOwner._isDirty && depOwner._isDirty()) {\n stack.push(depOwner);\n pushedUpstream = true;\n }\n }\n if (pushedUpstream) {\n // Process dirty upstreams first before re-evaluating current\n continue;\n }\n\n // All known deps are clean \u2014 evaluate. throw/catch is fallback\n // for newly-discovered deps only.\n try {\n const prevDepsLen = current.deps.length;\n _runEffect(current);\n // Only recompute level when graph structure changes\n if (current.deps.length !== prevDepsLen) {\n _updateLevel(current);\n }\n stack.pop(); // Successfully evaluated\n } catch (err) {\n if (err === NEEDS_UPSTREAM) {\n // A dirty upstream was discovered and pushed onto the stack.\n // Re-mark this computed dirty since its fn() was aborted mid-execution.\n current._markDirty();\n // The upstream is now at stack[stack.length-1]. Loop continues.\n } else {\n throw err; // Re-throw real errors\n }\n }\n }\n } finally {\n iterativeEvalStack = null;\n }\n}\n\n// Update the topological level of a computed/effect based on its current dependencies.\nfunction _updateLevel(e) {\n let maxDepLevel = 0;\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) {\n const owner = subSetOwner.get(deps[i]);\n if (owner) {\n const depLevel = owner._level;\n if (depLevel > maxDepLevel) maxDepLevel = depLevel;\n }\n }\n e._level = maxDepLevel + 1;\n}\n\n// --- Effect ---\n// Runs a function, auto-tracking signal reads. Re-runs when deps change.\n// Returns a dispose function.\n\nexport function effect(fn, opts) {\n const e = _createEffect(fn);\n e._level = 1;\n // First run: skip cleanup (deps is empty), just run and track\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } finally {\n currentEffect = prev;\n }\n // Compute level after first run based on actual dependencies (cached).\n _updateLevel(e);\n // Mark as stable after first run \u2014 subsequent re-runs skip cleanup/re-subscribe\n if (opts?.stable) e._stable = true;\n const dispose = () => _disposeEffect(e);\n // Register with current root for automatic cleanup\n if (currentRoot) {\n currentRoot.disposals.push(dispose);\n }\n return dispose;\n}\n\n// --- Batch ---\n// Group multiple signal writes; effects run once at the end.\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) flush();\n }\n}\n\n// --- Internals ---\n\nfunction _createEffect(fn, lazy) {\n // Minimal object shape \u2014 computed() adds extra properties after creation.\n // Keeping the base object small helps V8 optimize for the common (effect) case.\n const e = {\n fn,\n deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)\n lazy: lazy || false,\n _onNotify: null,\n disposed: false,\n _pending: false,\n _stable: false, // stable effects skip cleanup/re-subscribe on re-run\n _level: 0, // topological depth: signals=0, computed/effects=max(deps)+1\n _computed: false, // true for computed inner effects\n _computedSubs: null, // reference to the computed's subscriber set\n _isDirty: null, // function to check if computed is dirty (set by computed())\n _markDirty: null, // function to mark computed dirty (set by computed())\n };\n if (__DEV__ && __devtools) __devtools.onEffectCreate(e);\n return e;\n}\n\nfunction _runEffect(e) {\n if (e.disposed) return;\n\n // Stable effect fast path: deps don't change, skip cleanup/re-subscribe.\n if (e._stable) {\n if (e._cleanup) {\n try { e._cleanup(); } catch (err) {\n if (__DEV__) console.warn('[what] Error in effect cleanup:', err);\n }\n e._cleanup = null;\n }\n const prev = currentEffect;\n currentEffect = null; // Don't re-track deps (already subscribed)\n try {\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n return;\n }\n\n cleanup(e);\n // Run effect cleanup from previous run\n if (e._cleanup) {\n try { e._cleanup(); } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });\n if (__DEV__) console.warn('[what] Error in effect cleanup:', err);\n }\n e._cleanup = null;\n }\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n // Capture cleanup function if returned\n if (typeof result === 'function') {\n e._cleanup = result;\n }\n } catch (err) {\n if (err === NEEDS_UPSTREAM) throw err; // Iterative eval sentinel \u2014 not a real error\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n throw err;\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n}\n\nfunction _disposeEffect(e) {\n e.disposed = true;\n if (__DEV__ && __devtools) __devtools.onEffectDispose(e);\n cleanup(e);\n // Run cleanup on dispose\n if (e._cleanup) {\n try { e._cleanup(); } catch (err) {\n if (__DEV__) console.warn('[what] Error in effect cleanup on dispose:', err);\n }\n e._cleanup = null;\n }\n}\n\nfunction cleanup(e) {\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) deps[i].delete(e);\n deps.length = 0;\n}\n\n// --- Notification ---\n// Iterative notification to prevent stack overflow on deep computed chains.\n// Uses a reusable queue to avoid per-call array allocation.\n// When notify() encounters _onNotify callbacks (from computeds), those may\n// call notify() recursively. The queue drains iteratively in the outermost call.\n\nlet notifyDepth = 0; // Tracks recursive notify depth\nlet notifyQueue = null; // Reusable queue, allocated on first recursive call\nlet notifyQueueLen = 0; // Length of the queue\n\nfunction notify(subs) {\n // Fast path: no recursive notifications in progress \u2014 iterate directly.\n // This avoids array allocation for the common case (signal \u2192 effects).\n if (notifyDepth === 0) {\n notifyDepth = 1;\n try {\n for (const e of subs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n // Computed subscriber: mark dirty and propagate.\n // _onNotify may call notify() recursively \u2014 tracked by notifyDepth.\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n // Inline execution for stable effects\n const prev = currentEffect;\n currentEffect = null;\n try {\n const result = e.fn();\n if (typeof result === 'function') {\n if (e._cleanup) try { e._cleanup(); } catch (err) {}\n e._cleanup = result;\n }\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n // Drain any queued subscriber sets from recursive notify calls\n if (notifyQueueLen > 0) {\n let qi = 0;\n while (qi < notifyQueueLen) {\n const queuedSubs = notifyQueue[qi];\n notifyQueue[qi] = null; // Allow GC\n qi++;\n for (const e of queuedSubs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n const result = e.fn();\n if (typeof result === 'function') {\n if (e._cleanup) try { e._cleanup(); } catch (err) {}\n e._cleanup = result;\n }\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n }\n notifyQueueLen = 0;\n }\n } finally {\n notifyDepth = 0;\n }\n if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();\n } else {\n // Recursive call \u2014 queue the subscriber set for the outermost call to drain.\n if (notifyQueue === null) notifyQueue = [];\n if (notifyQueueLen >= notifyQueue.length) {\n notifyQueue.push(subs);\n } else {\n notifyQueue[notifyQueueLen] = subs;\n }\n notifyQueueLen++;\n }\n}\n\nlet microtaskScheduled = false;\nfunction scheduleMicrotask() {\n if (!microtaskScheduled) {\n microtaskScheduled = true;\n queueMicrotask(() => {\n microtaskScheduled = false;\n flush();\n });\n }\n}\n\nlet isFlushing = false;\n\nfunction flush() {\n // Re-entrancy guard: if flush() is called during an active flush (e.g., via\n // flushSync() inside a component render or effect), skip to prevent infinite\n // recursion. Pending effects will be picked up by the outer flush's while-loop.\n if (isFlushing) return;\n isFlushing = true;\n\n try {\n let iterations = 0;\n while (pendingEffects.length > 0 && iterations < 25) {\n const batch = pendingEffects;\n pendingEffects = [];\n\n // Topological sort: execute effects in level order (lowest first).\n // Fast paths:\n // 1. Single effect \u2014 no sort needed (most common case for microtask flush)\n // 2. Already sorted \u2014 skip sort (common when effects added in level order)\n // 3. Multiple effects at different levels \u2014 sort required\n if (batch.length > 1 && pendingNeedSort) {\n batch.sort((a, b) => a._level - b._level);\n }\n pendingNeedSort = false;\n\n for (let i = 0; i < batch.length; i++) {\n const e = batch[i];\n e._pending = false;\n if (!e.disposed && !e._onNotify) {\n const prevDepsLen = e.deps.length;\n _runEffect(e);\n // Update level only if deps changed (graph structure change)\n if (!e._computed && e.deps.length !== prevDepsLen) {\n _updateLevel(e);\n }\n }\n }\n iterations++;\n }\n if (iterations >= 25) {\n // Clear pending effects to prevent further damage\n for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;\n pendingEffects.length = 0;\n\n if (__DEV__) {\n const remaining = pendingEffects.slice(0, 3);\n const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');\n console.warn(\n `[what] Possible infinite effect loop detected (25 iterations). ` +\n `Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +\n `Use untrack() to read signals without subscribing. ` +\n `Looping effects: ${effectNames.join(', ')}`\n );\n } else {\n console.warn('[what] Possible infinite effect loop detected');\n }\n }\n } finally {\n isFlushing = false;\n }\n}\n\n// --- Memo ---\n// Eager computed that only propagates when the value actually changes.\n// Fix: Instead of calling notify(subs) inline (which bypasses topological sort\n// and causes diamond-dependency glitches), push memo subscribers into\n// pendingEffects and let them go through the sorted flush() path.\nexport function memo(fn) {\n let value;\n const subs = new Set();\n\n const e = _createEffect(() => {\n const next = fn();\n if (!Object.is(value, next)) {\n value = next;\n // Push subscribers into pendingEffects for topological flush\n // instead of inline notify() which can cause diamond glitches\n for (const sub of subs) {\n if (sub.disposed) continue;\n if (sub._onNotify) {\n // Computed subscriber: mark dirty and propagate\n sub._onNotify();\n } else if (!sub._pending) {\n sub._pending = true;\n const level = sub._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(sub);\n }\n }\n }\n });\n\n e._level = 1;\n\n _runEffect(e);\n _updateLevel(e);\n\n // Register subscriber set owner for level tracking\n subSetOwner.set(subs, e);\n\n // Register with current root\n if (currentRoot) {\n currentRoot.disposals.push(() => _disposeEffect(e));\n }\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n\n read._signal = true;\n read.peek = () => value;\n return read;\n}\n\n// --- flushSync ---\n// Force all pending effects to run synchronously. Use sparingly.\n// Calling during render or effect execution is a no-op (prevents infinite loops).\nexport function flushSync() {\n if (isFlushing) {\n // Re-entrant call \u2014 silently skip (Solid approach).\n // This prevents infinite loops when flushSync() is called during component\n // render or effect execution. Pending effects will be picked up by the\n // outer flush's while-loop.\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during an active flush (e.g., inside a component render or effect). ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n if (currentEffect) {\n // Called inside an effect/render \u2014 skip with warning\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during effect execution. ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n microtaskScheduled = false;\n flush();\n}\n\n// --- Untrack ---\n// Read signals without subscribing\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// --- getOwner / runWithOwner ---\n// Expose ownership context for advanced use cases (e.g., async operations\n// that need to register disposals with the correct owner).\n\nexport function getOwner() {\n return currentOwner;\n}\n\nexport function runWithOwner(owner, fn) {\n const prev = currentOwner;\n const prevRoot = currentRoot;\n currentOwner = owner;\n currentRoot = owner;\n try {\n return fn();\n } finally {\n currentOwner = prev;\n currentRoot = prevRoot;\n }\n}\n\n// --- createRoot ---\n// Isolated reactive scope with ownership tree.\n// All effects created inside are tracked and disposed together.\n// Child createRoot scopes register with parent owner \u2014 disposing parent\n// automatically disposes all children (prevents orphaned subscriptions).\nexport function createRoot(fn) {\n const prevRoot = currentRoot;\n const prevOwner = currentOwner;\n const root = {\n disposals: [],\n owner: currentOwner, // parent owner for ownership tree\n children: [], // child roots (ownership tree)\n _disposed: false,\n };\n\n // Register this root as a child of the parent owner\n if (currentOwner) {\n currentOwner.children.push(root);\n }\n\n currentRoot = root;\n currentOwner = root;\n\n try {\n const dispose = () => {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first (depth-first, reverse order)\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects (reverse order for LIFO cleanup)\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n\n // Remove from parent's children list\n if (root.owner) {\n const idx = root.owner.children.indexOf(root);\n if (idx >= 0) root.owner.children.splice(idx, 1);\n }\n };\n return fn(dispose);\n } finally {\n currentRoot = prevRoot;\n currentOwner = prevOwner;\n }\n}\n\n// Internal: dispose a root and all its children\nfunction _disposeRoot(root) {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n}\n\n// --- onCleanup ---\n// Register a cleanup function with the current owner/root.\n// Runs when the owner is disposed.\nexport function onCleanup(fn) {\n if (currentRoot) {\n currentRoot.disposals.push(fn);\n }\n}\n"],
4
+ "sourcesContent": ["// What Framework - Reactive Primitives\n// Signals + Effects: fine-grained reactivity without virtual DOM overhead\n//\n// Upgrades:\n// - Topological ordering: computed/effects sorted by _level to prevent diamond glitches\n// - Iterative computed evaluation: no recursion, handles 10K+ depth chains\n// - Ownership tree: createRoot children auto-dispose when parent disposes\n// - Performance: cached levels, lazy sort, fast-path notify, minimal allocation\n\n// Dev-mode flag \u2014 build tools can dead-code-eliminate when false\nexport const __DEV__ = typeof process !== 'undefined'\n ? process.env?.NODE_ENV !== 'production'\n : true;\n\n// DevTools hooks \u2014 set by what-devtools when installed.\n// These are no-ops in production (dead-code eliminated with __DEV__).\nexport let __devtools = null;\n\n/** @internal Install devtools hooks. Called by what-devtools. */\nexport function __setDevToolsHooks(hooks) {\n if (__DEV__) __devtools = hooks;\n}\n\nlet currentEffect = null;\nlet currentRoot = null;\nlet currentOwner = null; // Ownership tree: tracks current owner context\nlet insideComputed = false; // Track whether we're inside a computed() callback (dev-mode warning)\nlet batchDepth = 0;\nlet pendingEffects = [];\nlet pendingNeedSort = false; // Track whether pendingEffects actually needs sorting\n\n// WeakMap: subscriber Set \u2192 owning computed's inner effect (null/absent for signals)\n// Used for topological level computation.\nconst subSetOwner = new WeakMap();\n\n// --- Iterative Computed Evaluation State ---\n// Uses a throw/catch trampoline to convert recursive computed evaluation\n// to iterative. When a computed fn() reads another dirty computed, instead\n// of recursing, we throw a sentinel that gets caught by the outer loop.\nconst NEEDS_UPSTREAM = Symbol('needs_upstream');\nlet iterativeEvalStack = null; // array when inside evaluation loop, null otherwise\n\n// --- Signal ---\n// A reactive value. Reading inside an effect auto-tracks the dependency.\n// Writing triggers only the effects that depend on this signal.\n\nexport function signal(initial, debugName) {\n let value = initial;\n const subs = new Set();\n\n // Unified getter/setter: sig() reads, sig(newVal) writes\n function sig(...args) {\n if (args.length === 0) {\n // Read\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n // Write\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof args[0] === 'function' ? args[0](value) : args[0];\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n }\n\n sig.set = (next) => {\n if (__DEV__ && insideComputed) {\n console.warn(\n '[what] Signal.set() called inside a computed function. ' +\n 'This may cause infinite loops. Use effect() instead.' +\n (debugName ? ` (signal: ${debugName})` : '')\n );\n }\n const nextVal = typeof next === 'function' ? next(value) : next;\n if (Object.is(value, nextVal)) return;\n value = nextVal;\n if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);\n if (subs.size > 0) notify(subs);\n };\n\n sig.peek = () => value;\n\n sig.subscribe = (fn) => {\n return effect(() => fn(sig()));\n };\n\n sig._signal = true;\n if (__DEV__) {\n sig._subs = subs;\n if (debugName) sig._debugName = debugName;\n }\n\n // Notify devtools of signal creation\n if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);\n\n return sig;\n}\n\n// --- Computed ---\n// Derived signal. Lazy: only recomputes when a dependency changes AND it's read.\n// Topological level: max(dependency levels) + 1, computed from source signals (level 0).\n\nexport function computed(fn) {\n let value, dirty = true;\n const subs = new Set();\n\n const inner = _createEffect(() => {\n const prevInsideComputed = insideComputed;\n if (__DEV__) insideComputed = true;\n try {\n value = fn();\n dirty = false;\n } finally {\n if (__DEV__) insideComputed = prevInsideComputed;\n }\n }, true);\n\n // Computed nodes start at level 1. Updated when graph structure changes.\n inner._level = 1;\n inner._computed = true;\n inner._computedSubs = subs;\n\n // Register this subscriber set as owned by this computed\n subSetOwner.set(subs, inner);\n\n // Store markDirty/isDirty closures on the inner effect for iterative eval\n inner._markDirty = () => { dirty = true; };\n inner._isDirty = () => dirty;\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n if (dirty) _evaluateComputed(inner);\n return value;\n }\n\n // When a dependency changes, mark dirty AND propagate to our subscribers.\n inner._onNotify = () => {\n dirty = true;\n if (subs.size > 0) notify(subs);\n };\n\n read._signal = true;\n read.peek = () => {\n if (dirty) _evaluateComputed(inner);\n return value;\n };\n\n return read;\n}\n\n// --- Iterative Computed Evaluation ---\n//\n// Problem: A chain of N dirty computeds causes O(N) recursive calls:\n// C_N.read() \u2192 eval \u2192 fn() \u2192 C_{N-1}.read() \u2192 eval \u2192 fn() \u2192 ... \u2192 C_1.read() \u2192 eval \u2192 fn()\n// This overflows the stack at ~3500 depth.\n//\n// Solution: Throw/catch trampoline. The outermost _evaluateComputed manages a\n// stack (array). When a computed's fn() reads another dirty computed during\n// evaluation, _evaluateComputed throws NEEDS_UPSTREAM. The outer loop catches\n// this, adds the upstream to the stack, and processes from the bottom up.\n// This converts O(N) call depth to O(1) per computed (just the outermost loop).\n\nfunction _evaluateComputed(computedEffect) {\n if (iterativeEvalStack !== null) {\n // We're inside the outermost evaluation loop, and a computed's fn()\n // is reading another dirty computed. Push it onto the stack and throw\n // to abort the current fn() so the outer loop can process it first.\n iterativeEvalStack.push(computedEffect);\n throw NEEDS_UPSTREAM;\n }\n\n // Outermost call \u2014 enter the iterative evaluation loop.\n // The stack grows as we discover dirty upstream computeds.\n const stack = [computedEffect];\n iterativeEvalStack = stack;\n\n try {\n while (stack.length > 0) {\n const current = stack[stack.length - 1];\n\n if (!current._isDirty || !current._isDirty()) {\n // Already clean \u2014 pop and continue\n stack.pop();\n continue;\n }\n\n // Pre-scan known deps: if any are dirty computeds, push them onto\n // the stack first (bottom-up). This avoids the O(N^2) worst case\n // where throw/catch restarts from the top on each dirty upstream.\n let pushedUpstream = false;\n const deps = current.deps;\n for (let i = 0; i < deps.length; i++) {\n const depOwner = subSetOwner.get(deps[i]);\n if (depOwner && depOwner._computed && depOwner._isDirty && depOwner._isDirty()) {\n stack.push(depOwner);\n pushedUpstream = true;\n }\n }\n if (pushedUpstream) {\n // Process dirty upstreams first before re-evaluating current\n continue;\n }\n\n // All known deps are clean \u2014 evaluate. throw/catch is fallback\n // for newly-discovered deps only.\n try {\n const prevDepsLen = current.deps.length;\n _runEffect(current);\n // Only recompute level when graph structure changes\n if (current.deps.length !== prevDepsLen) {\n _updateLevel(current);\n }\n stack.pop(); // Successfully evaluated\n } catch (err) {\n if (err === NEEDS_UPSTREAM) {\n // A dirty upstream was discovered and pushed onto the stack.\n // Re-mark this computed dirty since its fn() was aborted mid-execution.\n current._markDirty();\n // The upstream is now at stack[stack.length-1]. Loop continues.\n } else {\n throw err; // Re-throw real errors\n }\n }\n }\n } finally {\n iterativeEvalStack = null;\n }\n}\n\n// Update the topological level of a computed/effect based on its current dependencies.\nfunction _updateLevel(e) {\n let maxDepLevel = 0;\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) {\n const owner = subSetOwner.get(deps[i]);\n if (owner) {\n const depLevel = owner._level;\n if (depLevel > maxDepLevel) maxDepLevel = depLevel;\n }\n }\n e._level = maxDepLevel + 1;\n}\n\n// --- Effect ---\n// Runs a function, auto-tracking signal reads. Re-runs when deps change.\n// Returns a dispose function.\n\nexport function effect(fn, opts) {\n const e = _createEffect(fn);\n e._level = 1;\n // First run: skip cleanup (deps is empty), just run and track\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } finally {\n currentEffect = prev;\n }\n // Compute level after first run based on actual dependencies (cached).\n _updateLevel(e);\n // Mark as stable after first run \u2014 subsequent re-runs skip cleanup/re-subscribe\n if (opts?.stable) e._stable = true;\n const dispose = () => _disposeEffect(e);\n // Register with current root for automatic cleanup\n if (currentRoot) {\n currentRoot.disposals.push(dispose);\n }\n return dispose;\n}\n\n// --- Batch ---\n// Group multiple signal writes; effects run once at the end.\n\nexport function batch(fn) {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) flush();\n }\n}\n\n// --- Internals ---\n\nfunction _createEffect(fn, lazy) {\n // Minimal object shape \u2014 computed() adds extra properties after creation.\n // Keeping the base object small helps V8 optimize for the common (effect) case.\n const e = {\n fn,\n deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)\n lazy: lazy || false,\n _onNotify: null,\n disposed: false,\n _pending: false,\n _stable: false, // stable effects skip cleanup/re-subscribe on re-run\n _level: 0, // topological depth: signals=0, computed/effects=max(deps)+1\n _computed: false, // true for computed inner effects\n _computedSubs: null, // reference to the computed's subscriber set\n _isDirty: null, // function to check if computed is dirty (set by computed())\n _markDirty: null, // function to mark computed dirty (set by computed())\n };\n if (__DEV__ && __devtools) __devtools.onEffectCreate(e);\n return e;\n}\n\nfunction _runEffect(e) {\n if (e.disposed) return;\n\n // Stable effect fast path: deps don't change, skip cleanup/re-subscribe.\n if (e._stable) {\n runStableEffect(e);\n return;\n }\n\n cleanup(e);\n // Run effect cleanup from previous run\n runEffectCleanup(e, 'effect cleanup');\n const prev = currentEffect;\n currentEffect = e;\n try {\n const result = e.fn();\n // Capture cleanup function if returned\n if (typeof result === 'function') {\n e._cleanup = result;\n }\n } catch (err) {\n if (err === NEEDS_UPSTREAM) throw err; // Iterative eval sentinel \u2014 not a real error\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n throw err;\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n}\n\nfunction _disposeEffect(e) {\n e.disposed = true;\n if (__DEV__ && __devtools) __devtools.onEffectDispose(e);\n cleanup(e);\n // Run cleanup on dispose\n runEffectCleanup(e, 'effect cleanup on dispose');\n}\n\nfunction reportEffectCleanupError(err, e, phase) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e, phase });\n if (__DEV__) console.warn(`[what] Error in ${phase}:`, err);\n}\n\nfunction runEffectCleanup(e, phase) {\n if (!e._cleanup) return;\n const cleanupFn = e._cleanup;\n e._cleanup = null;\n try {\n cleanupFn();\n } catch (err) {\n reportEffectCleanupError(err, e, phase);\n }\n}\n\nfunction runStableEffect(e) {\n const prev = currentEffect;\n currentEffect = null; // Don't re-track deps (already subscribed)\n try {\n runEffectCleanup(e, 'stable effect cleanup');\n const result = e.fn();\n if (typeof result === 'function') e._cleanup = result;\n } catch (err) {\n if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });\n if (__DEV__) console.warn('[what] Error in stable effect:', err);\n } finally {\n currentEffect = prev;\n }\n if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);\n}\n\nfunction cleanup(e) {\n const deps = e.deps;\n for (let i = 0; i < deps.length; i++) deps[i].delete(e);\n deps.length = 0;\n}\n\n// --- Notification ---\n// Iterative notification to prevent stack overflow on deep computed chains.\n// Uses a reusable queue to avoid per-call array allocation.\n// When notify() encounters _onNotify callbacks (from computeds), those may\n// call notify() recursively. The queue drains iteratively in the outermost call.\n\nlet notifyDepth = 0; // Tracks recursive notify depth\nlet notifyQueue = null; // Reusable queue, allocated on first recursive call\nlet notifyQueueLen = 0; // Length of the queue\n\nfunction notify(subs) {\n // Fast path: no recursive notifications in progress \u2014 iterate directly.\n // This avoids array allocation for the common case (signal \u2192 effects).\n if (notifyDepth === 0) {\n notifyDepth = 1;\n try {\n for (const e of subs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n // Computed subscriber: mark dirty and propagate.\n // _onNotify may call notify() recursively \u2014 tracked by notifyDepth.\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n runStableEffect(e);\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n // Drain any queued subscriber sets from recursive notify calls\n if (notifyQueueLen > 0) {\n let qi = 0;\n while (qi < notifyQueueLen) {\n const queuedSubs = notifyQueue[qi];\n notifyQueue[qi] = null; // Allow GC\n qi++;\n for (const e of queuedSubs) {\n if (e.disposed) continue;\n if (e._onNotify) {\n e._onNotify();\n } else if (batchDepth === 0 && e._stable) {\n runStableEffect(e);\n } else if (!e._pending) {\n e._pending = true;\n const level = e._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(e);\n }\n }\n }\n notifyQueueLen = 0;\n }\n } finally {\n notifyDepth = 0;\n }\n if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();\n } else {\n // Recursive call \u2014 queue the subscriber set for the outermost call to drain.\n if (notifyQueue === null) notifyQueue = [];\n if (notifyQueueLen >= notifyQueue.length) {\n notifyQueue.push(subs);\n } else {\n notifyQueue[notifyQueueLen] = subs;\n }\n notifyQueueLen++;\n }\n}\n\nlet microtaskScheduled = false;\nfunction scheduleMicrotask() {\n if (!microtaskScheduled) {\n microtaskScheduled = true;\n queueMicrotask(() => {\n microtaskScheduled = false;\n flush();\n });\n }\n}\n\nlet isFlushing = false;\n\nfunction flush() {\n // Re-entrancy guard: if flush() is called during an active flush (e.g., via\n // flushSync() inside a component render or effect), skip to prevent infinite\n // recursion. Pending effects will be picked up by the outer flush's while-loop.\n if (isFlushing) return;\n isFlushing = true;\n\n try {\n let iterations = 0;\n while (pendingEffects.length > 0 && iterations < 25) {\n const batch = pendingEffects;\n pendingEffects = [];\n\n // Topological sort: execute effects in level order (lowest first).\n // Fast paths:\n // 1. Single effect \u2014 no sort needed (most common case for microtask flush)\n // 2. Already sorted \u2014 skip sort (common when effects added in level order)\n // 3. Multiple effects at different levels \u2014 sort required\n if (batch.length > 1 && pendingNeedSort) {\n batch.sort((a, b) => a._level - b._level);\n }\n pendingNeedSort = false;\n\n for (let i = 0; i < batch.length; i++) {\n const e = batch[i];\n e._pending = false;\n if (!e.disposed && !e._onNotify) {\n const prevDepsLen = e.deps.length;\n _runEffect(e);\n // Update level only if deps changed (graph structure change)\n if (!e._computed && e.deps.length !== prevDepsLen) {\n _updateLevel(e);\n }\n }\n }\n iterations++;\n }\n if (iterations >= 25) {\n // Clear pending effects to prevent further damage\n for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;\n pendingEffects.length = 0;\n\n if (__DEV__) {\n const remaining = pendingEffects.slice(0, 3);\n const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');\n console.warn(\n `[what] Possible infinite effect loop detected (25 iterations). ` +\n `Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +\n `Use untrack() to read signals without subscribing. ` +\n `Looping effects: ${effectNames.join(', ')}`\n );\n } else {\n console.warn('[what] Possible infinite effect loop detected');\n }\n }\n } finally {\n isFlushing = false;\n }\n}\n\n// --- Memo ---\n// Eager computed that only propagates when the value actually changes.\n// Fix: Instead of calling notify(subs) inline (which bypasses topological sort\n// and causes diamond-dependency glitches), push memo subscribers into\n// pendingEffects and let them go through the sorted flush() path.\nexport function memo(fn) {\n let value;\n const subs = new Set();\n\n const e = _createEffect(() => {\n const next = fn();\n if (!Object.is(value, next)) {\n value = next;\n // Push subscribers into pendingEffects for topological flush\n // instead of inline notify() which can cause diamond glitches\n for (const sub of subs) {\n if (sub.disposed) continue;\n if (sub._onNotify) {\n // Computed subscriber: mark dirty and propagate\n sub._onNotify();\n } else if (!sub._pending) {\n sub._pending = true;\n const level = sub._level;\n const len = pendingEffects.length;\n if (len > 0 && pendingEffects[len - 1]._level > level) {\n pendingNeedSort = true;\n }\n pendingEffects.push(sub);\n }\n }\n }\n });\n\n e._level = 1;\n\n _runEffect(e);\n _updateLevel(e);\n\n // Register subscriber set owner for level tracking\n subSetOwner.set(subs, e);\n\n // Register with current root\n if (currentRoot) {\n currentRoot.disposals.push(() => _disposeEffect(e));\n }\n\n function read() {\n if (currentEffect) {\n subs.add(currentEffect);\n currentEffect.deps.push(subs);\n }\n return value;\n }\n\n read._signal = true;\n read.peek = () => value;\n return read;\n}\n\n// --- flushSync ---\n// Force all pending effects to run synchronously. Use sparingly.\n// Calling during render or effect execution is a no-op (prevents infinite loops).\nexport function flushSync() {\n if (isFlushing) {\n // Re-entrant call \u2014 silently skip (Solid approach).\n // This prevents infinite loops when flushSync() is called during component\n // render or effect execution. Pending effects will be picked up by the\n // outer flush's while-loop.\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during an active flush (e.g., inside a component render or effect). ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n if (currentEffect) {\n // Called inside an effect/render \u2014 skip with warning\n if (__DEV__) {\n console.warn(\n '[what] flushSync() called during effect execution. ' +\n 'This is a no-op to prevent infinite loops. Move flushSync() to an event handler or onMount callback.'\n );\n }\n return;\n }\n microtaskScheduled = false;\n flush();\n}\n\n// --- Untrack ---\n// Read signals without subscribing\nexport function untrack(fn) {\n const prev = currentEffect;\n currentEffect = null;\n try {\n return fn();\n } finally {\n currentEffect = prev;\n }\n}\n\n// --- getOwner / runWithOwner ---\n// Expose ownership context for advanced use cases (e.g., async operations\n// that need to register disposals with the correct owner).\n\nexport function getOwner() {\n return currentOwner;\n}\n\nexport function runWithOwner(owner, fn) {\n const prev = currentOwner;\n const prevRoot = currentRoot;\n currentOwner = owner;\n currentRoot = owner;\n try {\n return fn();\n } finally {\n currentOwner = prev;\n currentRoot = prevRoot;\n }\n}\n\n// --- createRoot ---\n// Isolated reactive scope with ownership tree.\n// All effects created inside are tracked and disposed together.\n// Child createRoot scopes register with parent owner \u2014 disposing parent\n// automatically disposes all children (prevents orphaned subscriptions).\nexport function createRoot(fn) {\n const prevRoot = currentRoot;\n const prevOwner = currentOwner;\n const root = {\n disposals: [],\n owner: currentOwner, // parent owner for ownership tree\n children: [], // child roots (ownership tree)\n _disposed: false,\n };\n\n // Register this root as a child of the parent owner\n if (currentOwner) {\n currentOwner.children.push(root);\n }\n\n currentRoot = root;\n currentOwner = root;\n\n try {\n const dispose = () => {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first (depth-first, reverse order)\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects (reverse order for LIFO cleanup)\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n\n // Remove from parent's children list\n if (root.owner) {\n const idx = root.owner.children.indexOf(root);\n if (idx >= 0) root.owner.children.splice(idx, 1);\n }\n };\n return fn(dispose);\n } finally {\n currentRoot = prevRoot;\n currentOwner = prevOwner;\n }\n}\n\n// Internal: dispose a root and all its children\nfunction _disposeRoot(root) {\n if (root._disposed) return;\n root._disposed = true;\n\n // Dispose children first\n for (let i = root.children.length - 1; i >= 0; i--) {\n _disposeRoot(root.children[i]);\n }\n root.children.length = 0;\n\n // Dispose own effects\n for (let i = root.disposals.length - 1; i >= 0; i--) {\n root.disposals[i]();\n }\n root.disposals.length = 0;\n}\n\n// --- onCleanup ---\n// Register a cleanup function with the current owner/root.\n// Runs when the owner is disposed.\nexport function onCleanup(fn) {\n if (currentRoot) {\n currentRoot.disposals.push(fn);\n }\n}\n"],
5
5
  "mappings": "AAUO,IAAMA,EAAU,SAAO,QAAY,KAM/BC,EAAa,KAGjB,SAASC,EAAmBC,EAAO,CACpCH,IAASC,EAAaE,EAC5B",
6
6
  "names": ["__DEV__", "__devtools", "__setDevToolsHooks", "hooks"]
7
7
  }
package/dist/index.js CHANGED
@@ -210,38 +210,11 @@ function _createEffect(fn, lazy2) {
210
210
  function _runEffect(e) {
211
211
  if (e.disposed) return;
212
212
  if (e._stable) {
213
- if (e._cleanup) {
214
- try {
215
- e._cleanup();
216
- } catch (err) {
217
- if (__DEV__) console.warn("[what] Error in effect cleanup:", err);
218
- }
219
- e._cleanup = null;
220
- }
221
- const prev2 = currentEffect;
222
- currentEffect = null;
223
- try {
224
- const result = e.fn();
225
- if (typeof result === "function") e._cleanup = result;
226
- } catch (err) {
227
- if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
228
- if (__DEV__) console.warn("[what] Error in stable effect:", err);
229
- } finally {
230
- currentEffect = prev2;
231
- }
232
- if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
213
+ runStableEffect(e);
233
214
  return;
234
215
  }
235
216
  cleanup(e);
236
- if (e._cleanup) {
237
- try {
238
- e._cleanup();
239
- } catch (err) {
240
- if (__devtools?.onError) __devtools.onError(err, { type: "effect-cleanup", effect: e });
241
- if (__DEV__) console.warn("[what] Error in effect cleanup:", err);
242
- }
243
- e._cleanup = null;
244
- }
217
+ runEffectCleanup(e, "effect cleanup");
245
218
  const prev = currentEffect;
246
219
  currentEffect = e;
247
220
  try {
@@ -262,15 +235,37 @@ function _disposeEffect(e) {
262
235
  e.disposed = true;
263
236
  if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
264
237
  cleanup(e);
265
- if (e._cleanup) {
266
- try {
267
- e._cleanup();
268
- } catch (err) {
269
- if (__DEV__) console.warn("[what] Error in effect cleanup on dispose:", err);
270
- }
271
- e._cleanup = null;
238
+ runEffectCleanup(e, "effect cleanup on dispose");
239
+ }
240
+ function reportEffectCleanupError(err, e, phase) {
241
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect-cleanup", effect: e, phase });
242
+ if (__DEV__) console.warn(`[what] Error in ${phase}:`, err);
243
+ }
244
+ function runEffectCleanup(e, phase) {
245
+ if (!e._cleanup) return;
246
+ const cleanupFn = e._cleanup;
247
+ e._cleanup = null;
248
+ try {
249
+ cleanupFn();
250
+ } catch (err) {
251
+ reportEffectCleanupError(err, e, phase);
272
252
  }
273
253
  }
254
+ function runStableEffect(e) {
255
+ const prev = currentEffect;
256
+ currentEffect = null;
257
+ try {
258
+ runEffectCleanup(e, "stable effect cleanup");
259
+ const result = e.fn();
260
+ if (typeof result === "function") e._cleanup = result;
261
+ } catch (err) {
262
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
263
+ if (__DEV__) console.warn("[what] Error in stable effect:", err);
264
+ } finally {
265
+ currentEffect = prev;
266
+ }
267
+ if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
268
+ }
274
269
  function cleanup(e) {
275
270
  const deps = e.deps;
276
271
  for (let i = 0; i < deps.length; i++) deps[i].delete(e);
@@ -288,23 +283,7 @@ function notify(subs) {
288
283
  if (e._onNotify) {
289
284
  e._onNotify();
290
285
  } else if (batchDepth === 0 && e._stable) {
291
- const prev = currentEffect;
292
- currentEffect = null;
293
- try {
294
- const result = e.fn();
295
- if (typeof result === "function") {
296
- if (e._cleanup) try {
297
- e._cleanup();
298
- } catch (err) {
299
- }
300
- e._cleanup = result;
301
- }
302
- } catch (err) {
303
- if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
304
- if (__DEV__) console.warn("[what] Error in stable effect:", err);
305
- } finally {
306
- currentEffect = prev;
307
- }
286
+ runStableEffect(e);
308
287
  } else if (!e._pending) {
309
288
  e._pending = true;
310
289
  const level = e._level;
@@ -326,23 +305,7 @@ function notify(subs) {
326
305
  if (e._onNotify) {
327
306
  e._onNotify();
328
307
  } else if (batchDepth === 0 && e._stable) {
329
- const prev = currentEffect;
330
- currentEffect = null;
331
- try {
332
- const result = e.fn();
333
- if (typeof result === "function") {
334
- if (e._cleanup) try {
335
- e._cleanup();
336
- } catch (err) {
337
- }
338
- e._cleanup = result;
339
- }
340
- } catch (err) {
341
- if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
342
- if (__DEV__) console.warn("[what] Error in stable effect:", err);
343
- } finally {
344
- currentEffect = prev;
345
- }
308
+ runStableEffect(e);
346
309
  } else if (!e._pending) {
347
310
  e._pending = true;
348
311
  const level = e._level;
@@ -1626,27 +1589,6 @@ function applyProps(el, newProps, oldProps, isSvg) {
1626
1589
  }
1627
1590
  }
1628
1591
  function setProp(el, key, value, isSvg) {
1629
- if (!isSafeUrlAttributeValue(key, value)) {
1630
- if (typeof console !== "undefined") {
1631
- console.warn(`[what] Blocked unsafe URL in "${key}" attribute: ${value}`);
1632
- }
1633
- el.removeAttribute(getDomAttributeName(key));
1634
- return;
1635
- }
1636
- if (typeof value === "function" && !(key.startsWith("on") && key.length > 2) && key !== "ref") {
1637
- if (!el._propEffects) el._propEffects = {};
1638
- if (el._propEffects[key]) {
1639
- try {
1640
- el._propEffects[key]();
1641
- } catch (e) {
1642
- }
1643
- }
1644
- el._propEffects[key] = effect(() => {
1645
- const resolved = value();
1646
- setProp(el, key, resolved, isSvg);
1647
- });
1648
- return;
1649
- }
1650
1592
  if (key.startsWith("on") && key.length > 2) {
1651
1593
  let eventName = key.slice(2);
1652
1594
  let useCapture = false;
@@ -1671,6 +1613,32 @@ function setProp(el, key, value, isSvg) {
1671
1613
  el.addEventListener(event, wrappedHandler, eventOpts || useCapture || void 0);
1672
1614
  return;
1673
1615
  }
1616
+ if (key === "ref") {
1617
+ if (typeof value === "function") value(el);
1618
+ else if (value) value.current = el;
1619
+ return;
1620
+ }
1621
+ if (!isSafeUrlAttributeValue(key, value)) {
1622
+ if (typeof console !== "undefined") {
1623
+ console.warn(`[what] Blocked unsafe URL in "${key}" attribute: ${value}`);
1624
+ }
1625
+ el.removeAttribute(getDomAttributeName(key));
1626
+ return;
1627
+ }
1628
+ if (typeof value === "function") {
1629
+ if (!el._propEffects) el._propEffects = {};
1630
+ if (el._propEffects[key]) {
1631
+ try {
1632
+ el._propEffects[key]();
1633
+ } catch (e) {
1634
+ }
1635
+ }
1636
+ el._propEffects[key] = effect(() => {
1637
+ const resolved = value();
1638
+ setProp(el, key, resolved, isSvg);
1639
+ });
1640
+ return;
1641
+ }
1674
1642
  if (key === "className" || key === "class") {
1675
1643
  if (isSvg) {
1676
1644
  el.setAttribute("class", value || "");
@@ -2983,6 +2951,16 @@ function getHook(ctx) {
2983
2951
  const index = ctx.hookIndex++;
2984
2952
  return { index, exists: index < ctx.hooks.length };
2985
2953
  }
2954
+ function useState(initial) {
2955
+ const ctx = getCtx("useState");
2956
+ const { index, exists } = getHook(ctx);
2957
+ if (!exists) {
2958
+ const s2 = signal(typeof initial === "function" ? initial() : initial);
2959
+ ctx.hooks[index] = s2;
2960
+ }
2961
+ const s = ctx.hooks[index];
2962
+ return [s, s.set];
2963
+ }
2986
2964
  function useSignal(initial) {
2987
2965
  const ctx = getCtx("useSignal");
2988
2966
  const { index, exists } = getHook(ctx);
@@ -2999,6 +2977,97 @@ function useComputed(fn) {
2999
2977
  }
3000
2978
  return ctx.hooks[index];
3001
2979
  }
2980
+ function useEffect(fn, deps) {
2981
+ const ctx = getCtx("useEffect");
2982
+ const { index, exists } = getHook(ctx);
2983
+ if (!exists) {
2984
+ ctx.hooks[index] = { cleanup: null, dispose: null };
2985
+ }
2986
+ if (__DEV__ && Array.isArray(deps) && deps.length > 0) {
2987
+ for (let i = 0; i < deps.length; i++) {
2988
+ const dep = deps[i];
2989
+ if (dep != null && typeof dep !== "function") {
2990
+ console.warn(
2991
+ `[what] useEffect dep at index ${i} is not a function. Did you mean to pass a signal? Use count instead of count()`
2992
+ );
2993
+ }
2994
+ }
2995
+ }
2996
+ const hook = ctx.hooks[index];
2997
+ if (hook.dispose) return;
2998
+ if (deps === void 0) {
2999
+ queueMicrotask(() => {
3000
+ if (ctx.disposed) return;
3001
+ hook.dispose = effect(() => {
3002
+ if (hook.cleanup) {
3003
+ try {
3004
+ hook.cleanup();
3005
+ } catch (e) {
3006
+ }
3007
+ hook.cleanup = null;
3008
+ }
3009
+ const result = fn();
3010
+ if (typeof result === "function") hook.cleanup = result;
3011
+ });
3012
+ ctx.effects = ctx.effects || [];
3013
+ ctx.effects.push(hook.dispose);
3014
+ });
3015
+ } else if (deps.length === 0) {
3016
+ queueMicrotask(() => {
3017
+ if (ctx.disposed) return;
3018
+ const result = fn();
3019
+ if (typeof result === "function") hook.cleanup = result;
3020
+ });
3021
+ hook.dispose = true;
3022
+ } else {
3023
+ queueMicrotask(() => {
3024
+ if (ctx.disposed) return;
3025
+ hook.dispose = effect(() => {
3026
+ for (let i = 0; i < deps.length; i++) {
3027
+ const dep = deps[i];
3028
+ if (typeof dep === "function" && dep._signal) {
3029
+ dep();
3030
+ }
3031
+ }
3032
+ if (hook.cleanup) {
3033
+ try {
3034
+ hook.cleanup();
3035
+ } catch (e) {
3036
+ }
3037
+ hook.cleanup = null;
3038
+ }
3039
+ const result = untrack(() => fn());
3040
+ if (typeof result === "function") hook.cleanup = result;
3041
+ });
3042
+ ctx.effects = ctx.effects || [];
3043
+ ctx.effects.push(hook.dispose);
3044
+ });
3045
+ }
3046
+ }
3047
+ function useMemo(fn, deps) {
3048
+ const ctx = getCtx("useMemo");
3049
+ const { index, exists } = getHook(ctx);
3050
+ if (!exists) {
3051
+ ctx.hooks[index] = { computed: computed(fn) };
3052
+ }
3053
+ return ctx.hooks[index].computed;
3054
+ }
3055
+ function useCallback(fn, deps) {
3056
+ const ctx = getCtx("useCallback");
3057
+ const { index, exists } = getHook(ctx);
3058
+ if (!exists) {
3059
+ ctx.hooks[index] = { callback: fn };
3060
+ }
3061
+ return ctx.hooks[index].callback;
3062
+ }
3063
+ function useRef(initial) {
3064
+ const ctx = getCtx("useRef");
3065
+ const { index, exists } = getHook(ctx);
3066
+ if (!exists) {
3067
+ ctx.hooks[index] = { current: initial };
3068
+ }
3069
+ return ctx.hooks[index];
3070
+ }
3002
3071
  function useContext(context) {
3003
3072
  let ctx = getCurrentComponent();
3004
3073
  if (__DEV__ && !ctx) {
@@ -5972,10 +6041,12 @@ export {
5972
6041
  useAriaChecked,
5973
6042
  useAriaExpanded,
5974
6043
  useAriaSelected,
6044
+ useCallback,
5975
6045
  useClickOutside,
5976
6046
  useComputed,
5977
6047
  useContext,
5978
6048
  useDescribedBy,
6049
+ useEffect,
5979
6050
  useFetch,
5980
6051
  useField,
5981
6052
  useFocus,
@@ -5989,13 +6060,16 @@ export {
5989
6060
  useLabelledBy,
5990
6061
  useLocalStorage,
5991
6062
  useMediaQuery,
6063
+ useMemo,
5992
6064
  useQuery,
5993
6065
  useReducer,
6066
+ useRef,
5994
6067
  useRovingTabIndex,
5995
6068
  useSWR,
5996
6069
  useScheduledEffect,
5997
6070
  useSignal,
5998
6071
  useSkeleton,
6072
+ useState,
5999
6073
  useTransition,
6000
6074
  validateImports,
6001
6075
  yupResolver,