UNPKG

rvx

Version:

A signal based rendering library

367 lines 8.46 kB
import { Context } from "./context.js"; import { NOOP } from "./internals/noop.js"; const THROW_ON_LEAK = { push(_hook) { throw new Error("G5"); }, }; const LEAK = { push() { }, }; let TEARDOWN_FRAME = THROW_ON_LEAK; let ACCESS_FRAME; function dispose(hooks) { for (let i = hooks.length - 1; i >= 0; i--) { hooks[i](); } } export function capture(fn) { const parent = TEARDOWN_FRAME; const hooks = []; try { TEARDOWN_FRAME = hooks; fn(); } catch (error) { isolate(dispose, hooks); throw error; } finally { TEARDOWN_FRAME = parent; } return hooks.length === 0 ? NOOP : () => isolate(dispose, hooks); } export function captureSelf(fn) { let disposed = false; let dispose = NOOP; let value; dispose = capture(() => { value = fn(() => { disposed = true; dispose(); }); }); if (disposed) { dispose(); } return value; } export function leak(fn) { const parent = TEARDOWN_FRAME; try { TEARDOWN_FRAME = LEAK; return fn(); } finally { TEARDOWN_FRAME = parent; } } export function teardownOnError(fn) { let value; teardown(capture(() => { value = fn(); })); return value; } export function teardown(hook) { return TEARDOWN_FRAME.push(hook); } export function isolate(fn, ...args) { const parentTeardownFrame = TEARDOWN_FRAME; const parentAccessFrame = ACCESS_FRAME; try { TEARDOWN_FRAME = THROW_ON_LEAK; ACCESS_FRAME = undefined; return fn(...args); } finally { TEARDOWN_FRAME = parentTeardownFrame; ACCESS_FRAME = parentAccessFrame; } } export function isIsolated() { return TEARDOWN_FRAME === THROW_ON_LEAK && ACCESS_FRAME === undefined; } let BATCH; const _notify = (fn) => { try { fn(); } catch (error) { Promise.reject(error); } }; const _queueBatch = (fn) => BATCH.add(fn); export class Signal { inert; #core = { c: 0, h: new Set(), }; #source; #root; constructor(value, source) { this.inert = value; this.#source = source; this.#root = source ? source.#root : this; } get value() { this.access(); return this.inert; } set value(value) { if (!Object.is(this.inert, value)) { this.inert = value; this.notify(); } } get source() { return this.#source; } get root() { return this.#root; } get active() { return this.#core.h.size > 0; } access() { ACCESS_FRAME?.(this.#core); } notify() { const core = this.#core; core.c++; if (core.h.size === 0) { return; } if (BATCH === undefined) { const record = Array.from(core.h); core.h.clear(); record.forEach(_notify); } else { core.h.forEach(_queueBatch); } } pipe(fn, ...args) { return fn(this, ...args); } } export function $(value, source) { return new Signal(value, source); } const _unfold = (hook) => { let depth = 0; return () => { if (depth < 2) { depth++; } if (depth === 1) { try { while (depth > 0) { hook(); depth--; } } finally { depth = 0; } } }; }; const _observer = (hook) => { const signals = []; return { c: () => { for (let i = 0; i < signals.length; i++) { signals[i].h.delete(hook); } signals.length = 0; }, a: (hooks) => { signals.push(hooks); hooks.h.add(hook); }, }; }; export function watch(expr, effect) { const isSignal = expr instanceof Signal; if (isSignal || typeof expr === "function") { let value; let disposed = false; let dispose = NOOP; const runExpr = isSignal ? () => expr.value : expr; const entry = _unfold(Context.bind(() => { if (disposed) { return; } clear(); isolate(dispose); dispose = capture(() => { const parent = ACCESS_FRAME; try { ACCESS_FRAME = access; value = runExpr(); if (effect) { ACCESS_FRAME = undefined; effect(value); } } finally { ACCESS_FRAME = parent; } }); })); const { c: clear, a: access } = _observer(entry); teardown(() => { disposed = true; clear(); dispose(); }); entry(); } else { effect(expr); } } export function watchUpdates(expr, effect) { let first; let update = false; watch(expr, value => { if (update) { effect(value); } else { first = value; update = true; } }); return first; } function _isStale(dep) { return dep.c !== dep.s.c; } export function lazy(expr) { let stale = true; let value; const deps = []; const access = signal => { deps.push({ s: signal, c: signal.c, }); }; return Context.bind(() => { const observer = ACCESS_FRAME; if (stale || (stale = deps.some(_isStale))) { const parentTeardownFrame = TEARDOWN_FRAME; try { deps.length = 0; ACCESS_FRAME = access; TEARDOWN_FRAME = THROW_ON_LEAK; value = expr(); stale = false; } finally { ACCESS_FRAME = observer; TEARDOWN_FRAME = parentTeardownFrame; if (observer) { deps.forEach(dep => observer(dep.s)); } } } else { if (observer) { deps.forEach(dep => observer(dep.s)); } } return value; }); } function _dispatch(batch) { while (batch.size > 0) { batch.forEach(notify => { batch.delete(notify); _notify(notify); }); } } export function batch(fn) { if (BATCH === undefined) { const batch = new Set(); let value; try { BATCH = batch; value = fn(); _dispatch(batch); } finally { BATCH = undefined; } return value; } return fn(); } export function memo(expr) { const signal = $(undefined); watch(() => signal.value = get(expr)); return () => signal.value; } export function untrack(expr) { const parent = ACCESS_FRAME; try { ACCESS_FRAME = undefined; return get(expr); } finally { ACCESS_FRAME = parent; } } export function isTracking() { return ACCESS_FRAME !== undefined; } export function trigger(callback) { const hookFn = Context.bind(() => { clear(); isolate(_notify, callback); }); const { c: clear, a: access } = _observer(hookFn); teardown(clear); return (expr) => { clear(); const parent = ACCESS_FRAME; try { if (parent === undefined) { ACCESS_FRAME = access; } else { ACCESS_FRAME = hooks => { access(hooks); parent?.(hooks); }; } return get(expr); } finally { ACCESS_FRAME = parent; } }; } export function get(expr) { if (expr instanceof Signal) { return expr.value; } if (typeof expr === "function") { return expr(); } return expr; } export function map(input, mapFn) { if (input instanceof Signal) { return () => mapFn(input.value); } if (typeof input === "function") { return () => mapFn(input()); } return mapFn(input); } //# sourceMappingURL=signals.js.map