UNPKG

lightview

Version:

A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation

178 lines (154 loc) 5.88 kB
/** * LIGHTVIEW REACTIVITY CORE * Core logic for signals, effects, and computed values. */ // Global Handshake: Ensures all bundles share the same reactive engine const _LV = (globalThis.__LIGHTVIEW_INTERNALS__ ||= { currentEffect: null, registry: new Map(), // Global name -> Signal/Proxy localRegistries: new WeakMap(), // Object/Element -> Map(name -> Signal/Proxy) futureSignals: new Map(), // name -> Set of (signal) => void schemas: new Map(), // name -> Schema (Draft 7+ or Shorthand) parents: new WeakMap(), // Proxy -> Parent (Proxy/Element) helpers: new Map(), // name -> function (used for transforms and expressions) hooks: { validate: (value, schema) => true // Hook for extensions (like JPRX) to provide full validation } }); /** * Resolves a named signal/state using up-tree search starting from scope. */ export const lookup = (name, scope) => { let current = scope; while (current && typeof current === 'object') { const registry = _LV.localRegistries.get(current); if (registry && registry.has(name)) return registry.get(name); current = current.parentElement || _LV.parents.get(current); } return _LV.registry.get(name); }; /** * Creates a reactive signal. */ export const signal = (initialValue, optionsOrName) => { const name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name; const storage = optionsOrName?.storage; const scope = optionsOrName?.scope; if (name && storage) { try { const stored = storage.getItem(name); if (stored !== null) initialValue = JSON.parse(stored); } catch (e) { /* Ignore */ } } let value = initialValue; const subscribers = new Set(); const f = (...args) => args.length === 0 ? f.value : (f.value = args[0]); Object.defineProperty(f, 'value', { get() { if (_LV.currentEffect) { subscribers.add(_LV.currentEffect); _LV.currentEffect.dependencies.add(subscribers); } return value; }, set(newValue) { if (value !== newValue) { value = newValue; if (name && storage) { try { storage.setItem(name, JSON.stringify(value)); } catch (e) { /* Ignore */ } } [...subscribers].forEach(effect => effect()); } } }); if (name) { const registry = (scope && typeof scope === 'object') ? (_LV.localRegistries.get(scope) || _LV.localRegistries.set(scope, new Map()).get(scope)) : _LV.registry; if (registry && registry.has(name) && registry.get(name) !== f) { throw new Error(`Lightview: A signal or state with the name "${name}" is already registered.`); } if (registry) registry.set(name, f); // Resolve future signal waiters const futures = _LV.futureSignals.get(name); if (futures) { futures.forEach(resolve => resolve(f)); } } return f; }; /** * Gets a named signal, or a 'future' signal if not found. */ export const getSignal = (name, defaultValueOrOptions) => { const options = typeof defaultValueOrOptions === 'object' && defaultValueOrOptions !== null ? defaultValueOrOptions : { defaultValue: defaultValueOrOptions }; const { scope, defaultValue } = options; const existing = lookup(name, scope); if (existing) return existing; if (defaultValue !== undefined) return signal(defaultValue, { name, scope }); // Return a "Future Signal" that will track the real one once registered const future = signal(undefined); const handler = (realSignal) => { // When the real signal appears, sync the future one // If it's a signal (has .value), track its value. If it's a state proxy or object, it IS the value. const hasValue = realSignal && (typeof realSignal === 'object' || typeof realSignal === 'function') && 'value' in realSignal; if (hasValue) { future.value = realSignal.value; effect(() => { future.value = realSignal.value; }); } else { future.value = realSignal; } }; if (!_LV.futureSignals.has(name)) _LV.futureSignals.set(name, new Set()); _LV.futureSignals.get(name).add(handler); return future; }; // Map .get to signal for backwards compatibility signal.get = getSignal; /** * Creates an effect that tracks dependencies. */ export const effect = (fn) => { const execute = () => { if (!execute.active || execute.running) return; // Cleanup old dependencies execute.dependencies.forEach(dep => dep.delete(execute)); execute.dependencies.clear(); execute.running = true; _LV.currentEffect = execute; try { fn(); } finally { _LV.currentEffect = null; execute.running = false; } }; execute.active = true; execute.running = false; execute.dependencies = new Set(); execute.stop = () => { execute.dependencies.forEach(dep => dep.delete(execute)); execute.dependencies.clear(); execute.active = false; }; execute(); return execute; }; /** * Creates a read-only signal derived from others. */ export const computed = (fn) => { const sig = signal(undefined); effect(() => { sig.value = fn(); }); return sig; }; /** * Returns the global registry. */ export const getRegistry = () => _LV.registry; /** * Returns the global internals (private use). */ export const internals = _LV;