/** * Computed reactive values. */ import { clearDependencies, getCurrentObserver, registerDependency, scheduleObserver, track, withoutCurrentObserver, type ReactiveSource, } from './internals'; import { getActiveScope, hasScopeDisposer } from './scope'; /** * A computed value that derives from other reactive sources. * * Computed values are lazily evaluated and cached. They only * recompute when their dependencies change. * * @template T - The type of the computed value */ export class Computed implements ReactiveSource { private cachedValue!: T; private hasCachedValue = false; private dirty = true; private disposed = false; private subscribers = new Set<() => void>(); private readonly markDirty = () => { if (this.disposed) { return; } this.dirty = true; // Create snapshot to avoid issues with subscribers modifying the set during iteration const subscribersSnapshot = Array.from(this.subscribers); for (const subscriber of subscribersSnapshot) { scheduleObserver(subscriber); } }; /** * Creates a new computed value. * @param compute - Function that computes the value */ constructor(private readonly compute: () => T) {} /** * Gets the computed value, recomputing if dependencies changed. * During untrack calls, getCurrentObserver returns undefined, preventing dependency tracking. */ get value(): T { if (this.disposed) { if (!this.hasCachedValue) { this.cachedValue = withoutCurrentObserver(() => this.compute()); this.hasCachedValue = true; } return this.cachedValue; } const current = getCurrentObserver(); if (current) { this.subscribers.add(current); registerDependency(current, this); } if (this.dirty) { this.dirty = false; // Clear old dependencies before recomputing clearDependencies(this.markDirty); this.cachedValue = track(this.markDirty, this.compute); this.hasCachedValue = true; } return this.cachedValue; } /** * Reads the current computed value without tracking. * Useful when you need the value but don't want to create a dependency. * * @returns The current cached value (recomputes if dirty) */ peek(): T { if (this.disposed) { if (!this.hasCachedValue) { this.cachedValue = withoutCurrentObserver(() => this.compute()); this.hasCachedValue = true; } return this.cachedValue; } if (this.dirty) { this.dirty = false; // Clear old dependencies before recomputing clearDependencies(this.markDirty); this.cachedValue = track(this.markDirty, this.compute); this.hasCachedValue = true; } return this.cachedValue; } /** * Removes an observer from this computed's subscriber set. * @internal */ unsubscribe(observer: () => void): void { this.subscribers.delete(observer); } /** * Disposes the computed value by unsubscribing its internal observer * from all upstream dependencies and clearing subscribers. */ dispose(): void { this.disposed = true; if (this.dirty) { this.hasCachedValue = false; } this.dirty = false; clearDependencies(this.markDirty); this.subscribers.clear(); } } /** * Creates a new computed value. * * If created inside an {@link effectScope}, the computed value is automatically * collected and will be disposed when the scope stops. * * @template T - The type of the computed value * @param fn - Function that computes the value from reactive sources * @returns A new Computed instance */ export const computed = (fn: () => T): Computed => { const c = new Computed(fn); // Auto-register with the current scope so scope.stop() disposes this computed const scope = getActiveScope(); if (hasScopeDisposer(scope)) { scope._addDisposer(() => c.dispose()); } return c; };