sprae
Version:
DOM microhydration
99 lines (89 loc) • 2.75 kB
JavaScript
/**
* @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)