UNPKG

rvx

Version:

A signal based rendering library

252 lines 6.09 kB
import { Context } from "./context.js"; import { NOOP } from "./internals/noop.js"; import { ACCESS_STACK, TRACKING_STACK, useStack } from "./internals/stacks.js"; import { isolate } from "./isolate.js"; import { capture, teardown } from "./lifecycle.js"; let BATCH; const notify = (fn) => fn(); const queueBatch = (fn) => BATCH.add(fn); export class Signal { #value; #hooks = new Set(); #source; #root; constructor(value, source) { this.#value = value; this.#source = source; this.#root = source ? source.#root : this; } get value() { this.access(); return this.#value; } set value(value) { if (!Object.is(this.#value, value)) { this.#value = value; this.notify(); } } get source() { return this.#source; } get root() { return this.#root; } update(fn) { if (fn(this.#value) !== false) { this.notify(); } } get active() { return this.#hooks.size > 0; } access() { if (TRACKING_STACK[TRACKING_STACK.length - 1]) { const length = ACCESS_STACK.length; if (length > 0) { ACCESS_STACK[length - 1]?.(this.#hooks); } } } notify() { const hooks = this.#hooks; if (hooks.size === 0) { return; } if (BATCH === undefined) { const record = Array.from(hooks); hooks.clear(); record.forEach(notify); } else { hooks.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].delete(hook); } signals.length = 0; }, a: (hooks) => { signals.push(hooks); hooks.add(hook); }, }; }; const _access = (frame, fn) => { try { ACCESS_STACK.push(frame); TRACKING_STACK.push(true); return fn(); } finally { ACCESS_STACK.pop(); TRACKING_STACK.pop(); } }; 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.wrap(() => { if (disposed) { return; } clear(); isolate(dispose); dispose = capture(() => { value = _access(access, runExpr); if (effect) { _access(undefined, () => effect(value)); } }); })); 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 dispatch(batch) { while (batch.size > 0) { try { batch.forEach(notify => { batch.delete(notify); notify(); }); } finally { dispatch(batch); } } } 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(fn) { return useStack(TRACKING_STACK, false, fn); } export function track(fn) { return useStack(TRACKING_STACK, true, fn); } export function isTracking() { return TRACKING_STACK[TRACKING_STACK.length - 1] && ACCESS_STACK[ACCESS_STACK.length - 1] !== undefined; } export function trigger(callback) { const hookFn = Context.wrap(() => { clear(); isolate(callback); }); const { c: clear, a: access } = _observer(hookFn); teardown(clear); return (expr) => { clear(); try { const outerLength = ACCESS_STACK.length; if (outerLength > 0) { const outer = ACCESS_STACK[outerLength - 1]; ACCESS_STACK.push(hooks => { access(hooks); outer?.(hooks); }); } else { ACCESS_STACK.push(access); } return get(expr); } finally { ACCESS_STACK.pop(); } }; } 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