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.
- package/dist/compiler.js +59 -91
- package/dist/compiler.js.map +3 -3
- package/dist/compiler.min.js +1 -1
- package/dist/compiler.min.js.map +3 -3
- package/dist/devtools.js.map +1 -1
- package/dist/devtools.min.js.map +1 -1
- package/dist/index.js +165 -91
- package/dist/index.js.map +3 -3
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +3 -3
- package/dist/render.js +59 -91
- package/dist/render.js.map +3 -3
- package/dist/render.min.js +2 -2
- package/dist/render.min.js.map +3 -3
- package/dist/testing.js +59 -91
- package/dist/testing.js.map +3 -3
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +3 -3
- package/hooks.d.ts +15 -14
- package/index.d.ts +3 -3
- package/package.json +2 -2
- package/src/dom.js +31 -22
- package/src/index.js +7 -3
- package/src/reactive.js +36 -59
package/dist/devtools.js.map
CHANGED
|
@@ -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
|
}
|
package/dist/devtools.min.js.map
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|