import Box from '../../Box' import type {$IntentionalAny, VoidFn} from '../../types' import Stack from '../../utils/Stack' import AbstractDerivation from '../AbstractDerivation' import type {IDerivation} from '../IDerivation' import { startIgnoringDependencies, stopIgnoringDependencies, pushCollector, popCollector, } from './discoveryMechanism' const voidFn = () => {} export class PrismDerivation extends AbstractDerivation { protected _cacheOfDendencyValues: Map, unknown> = new Map() protected _possiblyStaleDeps = new Set>() private _prismScope = new PrismScope() constructor(readonly _fn: () => V) { super() } _recalculate() { let value: V if (this._possiblyStaleDeps.size > 0) { let anActuallyStaleDepWasFound = false startIgnoringDependencies() for (const dep of this._possiblyStaleDeps) { if (this._cacheOfDendencyValues.get(dep) !== dep.getValue()) { anActuallyStaleDepWasFound = true break } } stopIgnoringDependencies() this._possiblyStaleDeps.clear() if (!anActuallyStaleDepWasFound) { // console.log('ok') return this._lastValue! } } const newDeps: Set> = new Set() this._cacheOfDendencyValues.clear() const collector = (observedDep: IDerivation): void => { newDeps.add(observedDep) this._addDependency(observedDep) } pushCollector(collector) hookScopeStack.push(this._prismScope) try { value = this._fn() } catch (error) { console.error(error) } finally { const topOfTheStack = hookScopeStack.pop() if (topOfTheStack !== this._prismScope) { console.warn( // @todo guide the user to report the bug in an issue `The Prism hook stack has slipped. This is a bug.`, ) } } popCollector(collector) this._dependencies.forEach((dep) => { if (!newDeps.has(dep)) { this._removeDependency(dep) } }) this._dependencies = newDeps startIgnoringDependencies() newDeps.forEach((dep) => { this._cacheOfDendencyValues.set(dep, dep.getValue()) }) stopIgnoringDependencies() return value! } _reactToDependencyBecomingStale(msgComingFrom: IDerivation) { this._possiblyStaleDeps.add(msgComingFrom) } _keepHot() { this._prismScope = new PrismScope() startIgnoringDependencies() this.getValue() stopIgnoringDependencies() } _becomeCold() { cleanupScopeStack(this._prismScope) this._prismScope = new PrismScope() } } class PrismScope { isPrismScope = true private _subs: Record = {} sub(key: string) { if (!this._subs[key]) { this._subs[key] = new PrismScope() } return this._subs[key] } get subs() { return this._subs } } function cleanupScopeStack(scope: PrismScope) { for (const [_, sub] of Object.entries(scope.subs)) { cleanupScopeStack(sub) } cleanupEffects(scope) } function cleanupEffects(scope: PrismScope) { const effects = effectsWeakMap.get(scope) if (effects) { for (const k of Object.keys(effects)) { const effect = effects[k] safelyRun(effect.cleanup, undefined) } } effectsWeakMap.delete(scope) } function safelyRun( fn: () => T, returnValueInCaseOfError: U, ): {success: boolean; returnValue: T | U} { let returnValue: T | U = returnValueInCaseOfError let success = false try { returnValue = fn() success = true } catch (error) { setTimeout(() => { throw error }) } return {success, returnValue} } const hookScopeStack = new Stack() const refsWeakMap = new WeakMap>>() type IRef = { current: T } const effectsWeakMap = new WeakMap>() type IEffect = { deps: undefined | unknown[] cleanup: VoidFn } const memosWeakMap = new WeakMap>() type IMemo = { deps: undefined | unknown[] | ReadonlyArray cachedValue: unknown } function ref(key: string, initialValue: T): IRef { const scope = hookScopeStack.peek() if (!scope) { throw new Error(`prism.ref() is called outside of a prism() call.`) } let refs = refsWeakMap.get(scope) if (!refs) { refs = {} refsWeakMap.set(scope, refs) } if (refs[key]) { return refs[key] as $IntentionalAny as IRef } else { const ref: IRef = { current: initialValue, } refs[key] = ref return ref } } function effect(key: string, cb: () => () => void, deps?: unknown[]): void { const scope = hookScopeStack.peek() if (!scope) { throw new Error(`prism.effect() is called outside of a prism() call.`) } let effects = effectsWeakMap.get(scope) if (!effects) { effects = {} effectsWeakMap.set(scope, effects) } if (!effects[key]) { effects[key] = { cleanup: voidFn, deps: [{}], } } const effect = effects[key] if (depsHaveChanged(effect.deps, deps)) { effect.cleanup() startIgnoringDependencies() effect.cleanup = safelyRun(cb, voidFn).returnValue stopIgnoringDependencies() effect.deps = deps } } function depsHaveChanged( oldDeps: undefined | unknown[] | ReadonlyArray, newDeps: undefined | unknown[] | ReadonlyArray, ): boolean { if (oldDeps === undefined || newDeps === undefined) { return true } else if (oldDeps.length !== newDeps.length) { return true } else { return oldDeps.some((el, i) => el !== newDeps[i]) } } function memo( key: string, fn: () => T, deps: undefined | $IntentionalAny[] | ReadonlyArray<$IntentionalAny>, ): T { const scope = hookScopeStack.peek() if (!scope) { throw new Error(`prism.memo() is called outside of a prism() call.`) } let memos = memosWeakMap.get(scope) if (!memos) { memos = {} memosWeakMap.set(scope, memos) } if (!memos[key]) { memos[key] = { cachedValue: null, deps: [{}], } } const memo = memos[key] if (depsHaveChanged(memo.deps, deps)) { startIgnoringDependencies() memo.cachedValue = safelyRun(fn, undefined).returnValue stopIgnoringDependencies() memo.deps = deps } return memo.cachedValue as $IntentionalAny as T } function state(key: string, initialValue: T): [T, (val: T) => void] { const {b, setValue} = prism.memo( 'state/' + key, () => { const b = new Box(initialValue) const setValue = (val: T) => b.set(val) return {b, setValue} }, [], ) return [b.derivation.getValue(), setValue] } function ensurePrism(): void { const scope = hookScopeStack.peek() if (!scope) { throw new Error(`The parent function is called outside of a prism() call.`) } } function scope(key: string, fn: () => T): T { const parentScope = hookScopeStack.peek() if (!parentScope) { throw new Error(`prism.scope() is called outside of a prism() call.`) } const subScope = parentScope.sub(key) hookScopeStack.push(subScope) const ret = safelyRun(fn, undefined).returnValue hookScopeStack.pop() return ret as $IntentionalAny as T } function sub( key: string, fn: () => T, deps: undefined | $IntentionalAny[], ): T { return memo(key, () => prism(fn), deps).getValue() } function inPrism(): boolean { return !!hookScopeStack.peek() } type IPrismFn = { (fn: () => T): IDerivation ref: typeof ref effect: typeof effect memo: typeof memo ensurePrism: typeof ensurePrism state: typeof state scope: typeof scope sub: typeof sub inPrism: typeof inPrism } /** * Creates a derivation from the passed function that adds all derivations referenced * in it as dependencies, and reruns the function when these change. * * @param fn - The function to rerun when the derivations referenced in it change. */ const prism: IPrismFn = (fn) => { return new PrismDerivation(fn) } prism.ref = ref prism.effect = effect prism.memo = memo prism.ensurePrism = ensurePrism prism.state = state prism.scope = scope prism.sub = sub prism.inPrism = inPrism export default prism