UNPKG

sprae

Version:

DOM microhydration

99 lines (89 loc) 2.75 kB
/** * @fileoverview Minimal signals implementation (preact-signals compatible) * @module sprae/signal */ /** @type {import('./core.js').EffectFn | null} */ let current let depth = 0 /** @type {Set<import('./core.js').EffectFn> | null} */ let batched; /** * Creates a reactive signal. * @template T * @param {T} v - Initial value * @returns {import('./core.js').Signal<T>} */ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => ( _s = { get value() { current?.deps.add(_obs.add(current)); return v }, set value(val) { if (val === v) return v = val; for (let sub of _obs) batched ? batched.add(sub) : sub(); // notify effects }, peek() { return v }, toJSON: _v, toString: _v, valueOf: _v } ) /** * Creates a reactive effect that re-runs when dependencies change. * @param {() => void | (() => void)} fn - Effect function, may return cleanup * @returns {() => void} Dispose function */ export const effect = (fn, _teardown, _fx, _deps) => ( _fx = (prev) => { if (!fn) return // disposed during batch flush let tmp = _teardown; _teardown = null; tmp?.call?.(); prev = current, current = _fx if (depth++ > 50) { depth--; current = prev; // dispose: unsubscribe from all deps so this effect never fires again _teardown = fn = _fx.fn = null; for (let dep of _deps) dep.delete(_fx); _deps.clear() console.error('∴ Reactive loop detected'); return } try { _teardown = fn() } finally { current = prev; depth-- } }, _fx.fn = fn, _deps = _fx.deps = new Set(), _fx(), (dep) => { _teardown?.call?.(); _teardown = fn = _fx.fn = null; for (dep of _deps) dep.delete(_fx); _deps.clear() } ) /** * Creates a computed signal derived from other signals. * @template T * @param {() => T} fn - Computation function * @returns {import('./core.js').Signal<T>} */ export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => ( _c = { get value() { _e ||= effect(() => _s.value = fn()); return _s.value }, peek: _s.peek, toJSON: _v, toString: _v, valueOf: _v } ) /** * Batches multiple signal updates into a single notification. * @template T * @param {() => T} fn - Function containing updates * @returns {T} */ export const batch = (fn, _first = !batched, _list) => { batched ??= new Set; try { fn(); } finally { if (_first) { [batched, _list] = [null, batched]; for (const fx of _list) fx(); } } } /** * Runs a function without tracking dependencies. * @template T * @param {() => T} fn - Function to run untracked * @returns {T} */ export const untracked = (fn, _prev, _v) => (_prev = current, current = null, _v = fn(), current = _prev, _v)