import { compare } from './compare.js'; import { FF } from './feature-flags.js'; import type { AnyFunction } from './types'; const refList = new Set(); (window as any)['refList'] = refList; export type Signal = { value: T; }; type SignalInternal = Signal & { [refTag]: true; [readonly]: boolean; [Symbol.toPrimitive]: any; internalValue: T; dependencies: Set; watchers: Set; suspended: boolean; update(value?: T): void; }; const signalsStack: SignalInternal[] = []; const reactiveTag = Symbol('#'); const unwrapTag = Symbol('[]'); const refTag = Symbol('$'); const readonly = Symbol('~'); function toPrimitive(v: Signal, hint: string) { if (hint === 'number') { return Number(v.value); } if (hint === 'string') { return String(v.value); } return v.value; } function canBeObserved(object: any): boolean { return object !== null && object !== undefined && typeof object === 'object' && !object[reactiveTag]; } function reactive(object: T, effect: AnyFunction, notifier?: SignalInternal): T { if (!canBeObserved(object)) { return object; } const values = Object.entries(object); for (const [key, next] of values) { if (typeof next === 'object' && next !== null) { (object as any)[key] = reactive(next, effect, notifier); } } return new Proxy(object, { get(target: any, p) { if (p === reactiveTag) { return true; } if (p === unwrapTag) { return object; } if (notifier) { capture(notifier); } return target[p]; }, set(target: any, p, value, receiver) { if (!compare(target[p], value)) { Reflect.set(target, p, reactive(value, effect, notifier), receiver); effect(); } return true; }, deleteProperty(target: any, p) { delete target[p]; effect(); return true; }, }); } function unwrap(object: T): T { if (object === null || object === undefined) { return object; } if ((object as any)[unwrapTag]) { return (object as any)[unwrapTag]; } if (isRef(object)) { return object.value; } return object; } function isReadOnlyRef(ref: Signal): boolean { return (ref as SignalInternal)[readonly]; } function ref(initial?: T, isShallow?: boolean): Signal; function ref(initial: T | undefined, isShallow = false) { function reactiveEffect() { if (!isShallow && Array.isArray(o.internalValue)) { o.internalValue = reactive(o.internalValue, reactiveEffect, o) as T; } notifyDependencies(o); } const o: SignalInternal = { [Symbol.toPrimitive](hint: string) { return toPrimitive(o, hint); }, [refTag]: true, [readonly]: false, suspended: false, internalValue: undefined as T, dependencies: new Set(), watchers: new Set(), get value() { capture(o); return o.internalValue; }, set value(newValue) { if (o.suspended) return; o.update(newValue); }, update(newValue?: T) { if (compare(o.internalValue, newValue)) return; if (!isShallow) { newValue = reactive(newValue as object, reactiveEffect, o) as T; } o.internalValue = newValue; notifyDependencies(o); }, }; o.internalValue = !isShallow ? reactive(initial as object, reactiveEffect, o) : initial; if (FF.debug) refList.add(new WeakRef(o)); return o as Signal; } function computed(fn: () => T): Signal { const o: SignalInternal = { [Symbol.toPrimitive](hint: string) { return toPrimitive(o, hint); }, [refTag]: true, [readonly]: true, suspended: false, internalValue: undefined as T, dependencies: new Set(), watchers: new Set(), get value() { capture(o); return o.internalValue; }, set value(_) { throw new Error('Computed value cannot be set'); }, update() { if (o.suspended) { return; } let value = null as T; signalsStack.push(o); try { value = fn(); } catch (e) { FF.debug && console.error(e); } finally { signalsStack.pop(); } if (!compare(o.internalValue, value)) { o.internalValue = value; notifyDependencies(o); } }, }; if (FF.debug) { Object.assign(o, { fn }); refList.add(new WeakRef(o)); } o.update(); return o as Signal; } function shallowRef(value?: T) { return ref(value, true); } function isRef(t: any): t is Signal { return Boolean(t && t[refTag]); } function suspend(s: Signal) { (s as SignalInternal).suspended = true; } function resume(s: Signal) { (s as SignalInternal).suspended = false; (s as SignalInternal).update(s.value); } export type WatchOptions = { immediate: boolean }; function watch(target: Signal, fn: AnyFunction, o?: WatchOptions ) { const memoized = memoizedWatcher(fn); const watchers = (target as SignalInternal).watchers; watchers.add(memoized); if (o?.immediate) { memoized(target.value); } else { schedule(() => memoized(target.value)); } return () => { watchers.delete(memoized); }; } function effect(fn: AnyFunction, effectFn: AnyFunction, o?: WatchOptions) { return watch(computed(fn), effectFn, o); } function hook(initial: T, isShallow = false) { const $ = ref(initial, isShallow); const setter = (v: T) => $.value = v; return [$, setter] as const; } function notifyDependencies(target: SignalInternal) { if (!target.dependencies.size && !target.watchers.size) return; const value = target.value; for (const dep of target.dependencies) { if (dep.suspended) { target.dependencies.delete(dep); } else { dep.update(); } } for (const watcher of target.watchers) { watcher(value); } } function capture(o: SignalInternal) { const d = signalsStack.length && signalsStack.at(-1); if (d && d !== o) { o.dependencies.add(d); } } function memoizedWatcher(watcher: AnyFunction) { let lastValue: T | undefined; // NOTE: we never compare values for watchers! // ref() and computed() already skip watchers if // new and old values are the same return function (value: T) { watcher(value, lastValue); lastValue = value; }; } let timer: any; const queue: any[] = []; function schedule(fn: AnyFunction) { clearTimeout(timer); queue.push(fn); timer = setTimeout(() => { let n; while (n = queue.shift()) { n(); } }, 5); } export { ref, computed, effect, watch, hook, reactive, unwrap, isRef, isReadOnlyRef, shallowRef, canBeObserved, suspend, resume };