UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

484 lines 12.5 kB
/** * @fileoverview OrdoJS Reactivity System - Signal-based reactive state management * @author OrdoJS Framework Team */ /** * Global reactivity context */ const reactivityContext = { currentEffect: null, effectStack: [], batchQueue: new Set(), isBatching: false, batchFlushScheduled: false }; /** * Signal implementation */ class SignalImpl { _value; subscribers = new Set(); effects = new Set(); constructor(initialValue) { this._value = initialValue; } get value() { this.track(); return this._value; } get() { return this.value; } set(newValue) { if (Object.is(this._value, newValue)) { return; } this._value = newValue; this.trigger(); } update(updater) { this.set(updater(this._value)); } subscribe(callback) { this.subscribers.add(callback); return () => { this.subscribers.delete(callback); }; } hasSubscribers() { return this.subscribers.size > 0 || this.effects.size > 0; } track() { const currentEffect = reactivityContext.currentEffect; if (currentEffect) { this.effects.add(currentEffect); currentEffect.dependencies.add(this); } } trigger() { if (reactivityContext.isBatching) { reactivityContext.batchQueue.add(this); this.scheduleBatchFlush(); return; } this.flush(); } flush() { // Notify subscribers for (const callback of this.subscribers) { try { callback(this._value); } catch (error) { console.error('Error in signal subscriber:', error); } } // Run effects for (const effect of this.effects) { if (effect.active) { try { runEffect(effect); } catch (error) { console.error('Error in effect:', error); } } } } scheduleBatchFlush() { if (!reactivityContext.batchFlushScheduled) { reactivityContext.batchFlushScheduled = true; queueMicrotask(() => { flushBatch(); }); } } } /** * Computed signal implementation */ class ComputedSignalImpl { computeFn; _value; _computed = false; dependencies = new Set(); subscribers = new Set(); effects = new Set(); constructor(computeFn) { this.computeFn = computeFn; this._value = this.compute(); } get value() { if (!this._computed) { this.recompute(); } this.track(); return this._value; } get() { return this.value; } subscribe(callback) { this.subscribers.add(callback); return () => { this.subscribers.delete(callback); }; } hasSubscribers() { return this.subscribers.size > 0 || this.effects.size > 0; } recompute() { const newValue = this.compute(); if (!Object.is(this._value, newValue)) { this._value = newValue; this.trigger(); } } getDependencies() { return Array.from(this.dependencies); } compute() { // Clear old dependencies this.dependencies.clear(); // Track new dependencies const prevEffect = reactivityContext.currentEffect; const computedEffect = { fn: () => this.recompute(), dependencies: new Set(), options: { name: 'computed' }, active: true }; reactivityContext.currentEffect = computedEffect; try { const result = this.computeFn(); this.dependencies = computedEffect.dependencies; this._computed = true; return result; } finally { reactivityContext.currentEffect = prevEffect; } } track() { const currentEffect = reactivityContext.currentEffect; if (currentEffect) { this.effects.add(currentEffect); currentEffect.dependencies.add(this); } } trigger() { if (reactivityContext.isBatching) { reactivityContext.batchQueue.add(this); this.scheduleBatchFlush(); return; } this.flush(); } flush() { // Notify subscribers for (const callback of this.subscribers) { try { callback(this._value); } catch (error) { console.error('Error in computed subscriber:', error); } } // Run effects for (const effect of this.effects) { if (effect.active) { try { runEffect(effect); } catch (error) { console.error('Error in effect:', error); } } } } scheduleBatchFlush() { if (!reactivityContext.batchFlushScheduled) { reactivityContext.batchFlushScheduled = true; queueMicrotask(() => { flushBatch(); }); } } } /** * Run an effect */ function runEffect(effect) { if (!effect.active) return; // Cleanup previous run if (effect.cleanup) { effect.cleanup(); effect.cleanup = undefined; } // Clear dependencies for (const dep of effect.dependencies) { if ('effects' in dep) { dep.effects.delete(effect); } } effect.dependencies.clear(); // Run effect const prevEffect = reactivityContext.currentEffect; reactivityContext.currentEffect = effect; reactivityContext.effectStack.push(effect); try { effect.fn(); } finally { reactivityContext.currentEffect = prevEffect; reactivityContext.effectStack.pop(); } } /** * Flush batch updates */ function flushBatch() { if (!reactivityContext.isBatching) return; const signals = Array.from(reactivityContext.batchQueue); reactivityContext.batchQueue.clear(); reactivityContext.batchFlushScheduled = false; for (const signal of signals) { signal.flush(); } } /** * Create a reactive signal */ export function signal(initialValue) { return new SignalImpl(initialValue); } /** * Create a computed signal */ export function computed(computeFn) { return new ComputedSignalImpl(computeFn); } /** * Create an effect that runs when dependencies change */ export function effect(fn, options = {}) { const effectInstance = { fn, dependencies: new Set(), options: { immediate: true, ...options }, active: true }; if (options.onCleanup) { effectInstance.cleanup = options.onCleanup; } // Run immediately if requested if (effectInstance.options.immediate) { runEffect(effectInstance); } return () => { effectInstance.active = false; // Cleanup if (effectInstance.cleanup) { effectInstance.cleanup(); } // Remove from dependencies for (const dep of effectInstance.dependencies) { if ('effects' in dep) { dep.effects.delete(effectInstance); } } }; } /** * Batch multiple updates together */ export function batch(fn, options = {}) { if (reactivityContext.isBatching) { // Already batching, just run the function return fn(); } reactivityContext.isBatching = true; try { const result = fn(); // Flush all batched updates flushBatch(); return result; } finally { reactivityContext.isBatching = false; } } /** * Create a derived signal that depends on other signals */ export function derived(fn) { return computed(fn); } /** * Create a writable derived signal */ export function writableDerived(getter, setter) { const computedValue = computed(getter); return { get value() { return computedValue.value; }, get: () => computedValue.value, set: setter, update: (updater) => setter(updater(computedValue.value)), subscribe: (callback) => computedValue.subscribe(callback), hasSubscribers: () => computedValue.hasSubscribers() }; } /** * Create a signal that persists to localStorage */ export function persistentSignal(key, initialValue, storage = localStorage) { // Try to load from storage let storedValue = initialValue; try { const stored = storage.getItem(key); if (stored !== null) { storedValue = JSON.parse(stored); } } catch (error) { console.warn(`Failed to load persistent signal "${key}":`, error); } const sig = signal(storedValue); // Subscribe to changes and persist sig.subscribe((value) => { try { storage.setItem(key, JSON.stringify(value)); } catch (error) { console.warn(`Failed to persist signal "${key}":`, error); } }); return sig; } /** * Create a debounced signal */ export function debouncedSignal(source, delay) { const debounced = signal(source.value); let timeoutId; source.subscribe((value) => { if (timeoutId !== undefined) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { debounced.set(value); timeoutId = undefined; }, delay); }); return debounced; } /** * Create a throttled signal */ export function throttledSignal(source, delay) { const throttled = signal(source.value); let lastUpdate = 0; let timeoutId; source.subscribe((value) => { const now = Date.now(); const timeSinceLastUpdate = now - lastUpdate; if (timeSinceLastUpdate >= delay) { throttled.set(value); lastUpdate = now; } else { if (timeoutId !== undefined) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { throttled.set(value); lastUpdate = Date.now(); timeoutId = undefined; }, delay - timeSinceLastUpdate); } }); return throttled; } /** * Combine multiple signals into one */ export function combineSignals(signals) { return computed(() => { return signals.map(sig => sig.value); }); } /** * Create a signal from a promise */ export function fromPromise(promise, initialValue) { const state = signal({ loading: true, data: initialValue }); promise .then((data) => { state.set({ loading: false, data }); }) .catch((error) => { state.set({ loading: false, error }); }); return state; } /** * Create a signal from an event target */ export function fromEvent(target, eventName, options) { const eventSignal = signal(null); const handler = (event) => { eventSignal.set(event); }; target.addEventListener(eventName, handler, options); // Return signal with cleanup const originalSubscribe = eventSignal.subscribe; eventSignal.subscribe = (callback) => { const unsubscribe = originalSubscribe.call(eventSignal, callback); return () => { unsubscribe(); target.removeEventListener(eventName, handler, options); }; }; return eventSignal; } /** * Reactivity utilities */ export const reactivity = { signal, computed, effect, batch, derived, writableDerived, persistentSignal, debouncedSignal, throttledSignal, combineSignals, fromPromise, fromEvent }; /** * Get current reactivity context (for debugging) */ export function getReactivityContext() { return { ...reactivityContext }; } /** * Reset reactivity context (for testing) */ export function resetReactivityContext() { reactivityContext.currentEffect = null; reactivityContext.effectStack = []; reactivityContext.batchQueue.clear(); reactivityContext.isBatching = false; reactivityContext.batchFlushScheduled = false; } //# sourceMappingURL=reactivity.js.map