@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
484 lines • 12.5 kB
JavaScript
/**
* @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