2022-11-27 12:15:58 +01:00
|
|
|
import type Ticker from '../../Ticker'
|
2021-06-18 13:05:06 +02:00
|
|
|
import type {$IntentionalAny, VoidFn} from '../../types'
|
|
|
|
import Stack from '../../utils/Stack'
|
2022-12-01 14:20:50 +01:00
|
|
|
import type {Prism} from '../IDerivation'
|
2022-12-01 14:22:49 +01:00
|
|
|
import {isPrism} from '../IDerivation'
|
2021-06-18 13:05:06 +02:00
|
|
|
import {
|
|
|
|
startIgnoringDependencies,
|
|
|
|
stopIgnoringDependencies,
|
2021-07-07 11:50:23 +02:00
|
|
|
pushCollector,
|
|
|
|
popCollector,
|
2022-11-27 12:15:58 +01:00
|
|
|
reportResolutionStart,
|
|
|
|
reportResolutionEnd,
|
2021-06-18 13:05:06 +02:00
|
|
|
} from './discoveryMechanism'
|
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
type IDependent = (msgComingFrom: Prism<$IntentionalAny>) => void
|
2022-11-27 12:15:58 +01:00
|
|
|
|
2021-06-18 13:05:06 +02:00
|
|
|
const voidFn = () => {}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
class HotHandle<V> {
|
2022-11-27 12:15:58 +01:00
|
|
|
private _didMarkDependentsAsStale: boolean = false
|
|
|
|
private _isFresh: boolean = false
|
2022-12-01 14:20:50 +01:00
|
|
|
protected _cacheOfDendencyValues: Map<Prism<unknown>, unknown> = new Map()
|
2022-11-27 12:15:58 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
protected _dependents: Set<IDependent> = new Set()
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2022-12-01 14:20:50 +01:00
|
|
|
protected _dependencies: Set<Prism<$IntentionalAny>> = new Set()
|
2022-11-27 12:15:58 +01:00
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
protected _possiblyStaleDeps = new Set<Prism<unknown>>()
|
2022-12-01 12:58:59 +01:00
|
|
|
|
|
|
|
private _scope: HotScope = new HotScope(
|
|
|
|
this as $IntentionalAny as HotHandle<unknown>,
|
|
|
|
)
|
2022-11-27 12:15:58 +01:00
|
|
|
|
|
|
|
/**
|
2022-11-28 13:38:18 +01:00
|
|
|
* @internal
|
2022-11-27 12:15:58 +01:00
|
|
|
*/
|
2022-11-28 13:38:18 +01:00
|
|
|
protected _lastValue: undefined | V = undefined
|
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
/**
|
|
|
|
* If true, the derivation is stale even though its dependencies aren't
|
|
|
|
* marked as such. This is used by `prism.source()` and `prism.state()`
|
|
|
|
* to mark the prism as stale.
|
|
|
|
*/
|
|
|
|
private _forciblySetToStale: boolean = false
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
constructor(
|
|
|
|
private readonly _fn: () => V,
|
|
|
|
private readonly _prismInstance: PrismDerivation<V>,
|
|
|
|
) {
|
|
|
|
for (const d of this._dependencies) {
|
2022-12-01 14:11:06 +01:00
|
|
|
d._addDependent(this._reactToDependencyGoingStale)
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
startIgnoringDependencies()
|
|
|
|
this.getValue()
|
|
|
|
stopIgnoringDependencies()
|
|
|
|
}
|
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
get hasDependents(): boolean {
|
|
|
|
return this._dependents.size > 0
|
|
|
|
}
|
|
|
|
removeDependent(d: IDependent) {
|
|
|
|
this._dependents.delete(d)
|
|
|
|
}
|
|
|
|
addDependent(d: IDependent) {
|
|
|
|
this._dependents.add(d)
|
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
destroy() {
|
|
|
|
for (const d of this._dependencies) {
|
2022-12-01 14:11:06 +01:00
|
|
|
d._removeDependent(this._reactToDependencyGoingStale)
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
cleanupScopeStack(this._scope)
|
|
|
|
}
|
|
|
|
|
|
|
|
getValue(): V {
|
|
|
|
if (!this._isFresh) {
|
|
|
|
const newValue = this._recalculate()
|
|
|
|
this._lastValue = newValue
|
|
|
|
this._isFresh = true
|
|
|
|
this._didMarkDependentsAsStale = false
|
2022-12-01 12:58:59 +01:00
|
|
|
this._forciblySetToStale = false
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
return this._lastValue!
|
|
|
|
}
|
|
|
|
|
|
|
|
_recalculate() {
|
|
|
|
let value: V
|
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
if (!this._forciblySetToStale) {
|
|
|
|
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) {
|
|
|
|
return this._lastValue!
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
const newDeps: Set<Prism<unknown>> = new Set()
|
2022-11-28 13:38:18 +01:00
|
|
|
this._cacheOfDendencyValues.clear()
|
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
const collector = (observedDep: Prism<unknown>): void => {
|
2022-11-28 13:38:18 +01:00
|
|
|
newDeps.add(observedDep)
|
|
|
|
this._addDependency(observedDep)
|
|
|
|
}
|
|
|
|
|
|
|
|
pushCollector(collector)
|
|
|
|
|
|
|
|
hookScopeStack.push(this._scope)
|
|
|
|
try {
|
|
|
|
value = this._fn()
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error)
|
|
|
|
} finally {
|
|
|
|
const topOfTheStack = hookScopeStack.pop()
|
|
|
|
if (topOfTheStack !== this._scope) {
|
|
|
|
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)
|
|
|
|
|
|
|
|
for (const dep of this._dependencies) {
|
|
|
|
if (!newDeps.has(dep)) {
|
|
|
|
this._removeDependency(dep)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this._dependencies = newDeps
|
|
|
|
|
|
|
|
startIgnoringDependencies()
|
|
|
|
for (const dep of newDeps) {
|
|
|
|
this._cacheOfDendencyValues.set(dep, dep.getValue())
|
|
|
|
}
|
|
|
|
stopIgnoringDependencies()
|
|
|
|
|
|
|
|
return value!
|
|
|
|
}
|
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
forceStale() {
|
|
|
|
this._forciblySetToStale = true
|
|
|
|
this._markAsStale()
|
|
|
|
}
|
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
protected _reactToDependencyGoingStale = (which: Prism<$IntentionalAny>) => {
|
2022-12-01 12:58:59 +01:00
|
|
|
this._possiblyStaleDeps.add(which)
|
2022-11-28 13:38:18 +01:00
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
this._markAsStale()
|
|
|
|
}
|
|
|
|
|
|
|
|
private _markAsStale() {
|
2022-11-28 13:38:18 +01:00
|
|
|
if (this._didMarkDependentsAsStale) return
|
|
|
|
|
|
|
|
this._didMarkDependentsAsStale = true
|
|
|
|
this._isFresh = false
|
|
|
|
|
|
|
|
for (const dependent of this._dependents) {
|
|
|
|
dependent(this._prismInstance)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-27 12:15:58 +01:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2022-12-01 14:20:50 +01:00
|
|
|
protected _addDependency(d: Prism<$IntentionalAny>) {
|
2022-11-27 12:15:58 +01:00
|
|
|
if (this._dependencies.has(d)) return
|
|
|
|
this._dependencies.add(d)
|
2022-12-01 14:11:06 +01:00
|
|
|
d._addDependent(this._reactToDependencyGoingStale)
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2022-12-01 14:20:50 +01:00
|
|
|
protected _removeDependency(d: Prism<$IntentionalAny>) {
|
2022-11-27 12:15:58 +01:00
|
|
|
if (!this._dependencies.has(d)) return
|
|
|
|
this._dependencies.delete(d)
|
2022-12-01 14:11:06 +01:00
|
|
|
d._removeDependent(this._reactToDependencyGoingStale)
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-01 14:06:09 +01:00
|
|
|
const emptyObject = {}
|
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
class PrismDerivation<V> implements Prism<V> {
|
2022-11-28 13:38:18 +01:00
|
|
|
/**
|
|
|
|
* Whether the object is a derivation.
|
|
|
|
*/
|
2022-12-01 14:22:49 +01:00
|
|
|
readonly isPrism: true = true
|
2022-11-28 13:38:18 +01:00
|
|
|
|
|
|
|
private _state:
|
|
|
|
| {hot: false; handle: undefined}
|
|
|
|
| {hot: true; handle: HotHandle<V>} = {
|
|
|
|
hot: false,
|
|
|
|
handle: undefined,
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(private readonly _fn: () => V) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the derivation is hot.
|
|
|
|
*/
|
|
|
|
get isHot(): boolean {
|
|
|
|
return this._state.hot
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-12-01 13:45:27 +01:00
|
|
|
onChange(
|
|
|
|
ticker: Ticker,
|
|
|
|
listener: (v: V) => void,
|
|
|
|
immediate: boolean = false,
|
|
|
|
): VoidFn {
|
2022-12-01 14:06:09 +01:00
|
|
|
const dependent = () => {
|
|
|
|
ticker.onThisOrNextTick(refresh)
|
|
|
|
}
|
|
|
|
|
|
|
|
let lastValue = emptyObject
|
|
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
const newValue = this.getValue()
|
|
|
|
if (newValue === lastValue) return
|
|
|
|
|
|
|
|
lastValue = newValue
|
|
|
|
listener(newValue)
|
|
|
|
}
|
|
|
|
|
2022-12-01 14:11:06 +01:00
|
|
|
this._addDependent(dependent)
|
2022-12-01 14:06:09 +01:00
|
|
|
|
2022-12-01 13:45:27 +01:00
|
|
|
if (immediate) {
|
2022-12-01 14:06:09 +01:00
|
|
|
lastValue = this.getValue()
|
|
|
|
listener(lastValue as $IntentionalAny as V)
|
2022-12-01 13:45:27 +01:00
|
|
|
}
|
2022-12-01 14:06:09 +01:00
|
|
|
|
|
|
|
const unsubscribe = () => {
|
2022-12-01 14:11:06 +01:00
|
|
|
this._removeDependent(dependent)
|
2022-12-01 14:06:09 +01:00
|
|
|
}
|
|
|
|
|
2022-12-01 13:45:27 +01:00
|
|
|
return unsubscribe
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-11-27 13:05:44 +01:00
|
|
|
/**
|
|
|
|
* Returns a tappable that fires every time the prism's state goes from `fresh-\>stale.`
|
|
|
|
*/
|
2022-12-01 13:31:16 +01:00
|
|
|
onStale(callback: () => void): VoidFn {
|
2022-12-01 13:33:52 +01:00
|
|
|
const untap = () => {
|
2022-12-01 14:11:06 +01:00
|
|
|
this._removeDependent(fn)
|
2022-12-01 13:33:52 +01:00
|
|
|
}
|
|
|
|
const fn = () => callback()
|
2022-12-01 14:11:06 +01:00
|
|
|
this._addDependent(fn)
|
2022-12-01 13:33:52 +01:00
|
|
|
return untap
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Keep the derivation hot, even if there are no tappers (subscribers).
|
|
|
|
*/
|
|
|
|
keepHot() {
|
2022-12-01 13:31:16 +01:00
|
|
|
return this.onStale(() => {})
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a derivation as a dependent of this derivation.
|
|
|
|
*
|
|
|
|
* @param d - The derivation to be made a dependent of this derivation.
|
|
|
|
*
|
2022-12-01 14:11:06 +01:00
|
|
|
* @see _removeDependent
|
2022-11-27 12:15:58 +01:00
|
|
|
*/
|
2022-12-01 14:11:06 +01:00
|
|
|
_addDependent(d: IDependent) {
|
2022-11-28 13:38:18 +01:00
|
|
|
if (!this._state.hot) {
|
|
|
|
this._goHot()
|
|
|
|
}
|
|
|
|
this._state.handle!.addDependent(d)
|
|
|
|
}
|
|
|
|
|
|
|
|
private _goHot() {
|
|
|
|
const hotHandle = new HotHandle(this._fn, this)
|
|
|
|
this._state = {
|
|
|
|
hot: true,
|
|
|
|
handle: hotHandle,
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a derivation as a dependent of this derivation.
|
|
|
|
*
|
|
|
|
* @param d - The derivation to be removed from as a dependent of this derivation.
|
|
|
|
*
|
2022-12-01 14:11:06 +01:00
|
|
|
* @see _addDependent
|
2022-11-27 12:15:58 +01:00
|
|
|
*/
|
2022-12-01 14:11:06 +01:00
|
|
|
_removeDependent(d: IDependent) {
|
2022-11-28 13:38:18 +01:00
|
|
|
const state = this._state
|
|
|
|
if (!state.hot) {
|
|
|
|
return
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
const handle = state.handle
|
|
|
|
handle.removeDependent(d)
|
|
|
|
if (!handle.hasDependents) {
|
|
|
|
this._state = {hot: false, handle: undefined}
|
|
|
|
handle.destroy()
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
|
|
|
|
*/
|
|
|
|
getValue(): V {
|
|
|
|
/**
|
|
|
|
* TODO We should prevent (or warn about) a common mistake users make, which is reading the value of
|
|
|
|
* a derivation in the body of a react component (e.g. `der.getValue()` (often via `val()`) instead of `useVal()`
|
|
|
|
* or `uesPrism()`).
|
|
|
|
*
|
|
|
|
* Although that's the most common example of this mistake, you can also find it outside of react components.
|
|
|
|
* Basically the user runs `der.getValue()` assuming the read is detected by a wrapping prism when it's not.
|
|
|
|
*
|
|
|
|
* Sometiems the derivation isn't even hot when the user assumes it is.
|
|
|
|
*
|
|
|
|
* We can fix this type of mistake by:
|
|
|
|
* 1. Warning the user when they call `getValue()` on a cold derivation.
|
|
|
|
* 2. Warning the user about calling `getValue()` on a hot-but-stale derivation
|
|
|
|
* if `getValue()` isn't called by a known mechanism like a `DerivationEmitter`.
|
|
|
|
*
|
|
|
|
* Design constraints:
|
|
|
|
* - This fix should not have a perf-penalty in production. Perhaps use a global flag + `process.env.NODE_ENV !== 'production'`
|
|
|
|
* to enable it.
|
|
|
|
* - In the case of `DerivationValuelessEmitter`, we don't control when the user calls
|
|
|
|
* `getValue()` (as opposed to `DerivationEmitter` which calls `getValue()` directly).
|
|
|
|
* Perhaps we can disable the check in that case.
|
|
|
|
* - Probably the best place to add this check is right here in this method plus some changes to `reportResulutionStart()`,
|
|
|
|
* which would have to be changed to let the caller know if there is an actual collector (a prism)
|
|
|
|
* present in its stack.
|
|
|
|
*/
|
|
|
|
reportResolutionStart(this)
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
const state = this._state
|
2022-11-27 12:15:58 +01:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
let val: V
|
|
|
|
if (state.hot) {
|
|
|
|
val = state.handle.getValue()
|
2022-11-27 12:15:58 +01:00
|
|
|
} else {
|
2022-11-28 14:45:39 +01:00
|
|
|
val = calculateColdPrism(this._fn)
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
|
|
|
|
reportResolutionEnd(this)
|
|
|
|
return val
|
2022-11-27 12:15:58 +01:00
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
interface PrismScope {
|
|
|
|
effect(key: string, cb: () => () => void, deps?: unknown[]): void
|
|
|
|
memo<T>(
|
|
|
|
key: string,
|
|
|
|
fn: () => T,
|
|
|
|
deps: undefined | $IntentionalAny[] | ReadonlyArray<$IntentionalAny>,
|
|
|
|
): T
|
|
|
|
state<T>(key: string, initialValue: T): [T, (val: T) => void]
|
|
|
|
ref<T>(key: string, initialValue: T): IRef<T>
|
|
|
|
sub(key: string): PrismScope
|
2022-12-01 12:58:59 +01:00
|
|
|
source<V>(subscribe: (fn: (val: V) => void) => VoidFn, getValue: () => V): V
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
class HotScope implements PrismScope {
|
2022-12-01 12:58:59 +01:00
|
|
|
constructor(private readonly _hotHandle: HotHandle<unknown>) {}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
protected readonly _refs: Map<string, IRef<unknown>> = new Map()
|
|
|
|
ref<T>(key: string, initialValue: T): IRef<T> {
|
|
|
|
let ref = this._refs.get(key)
|
|
|
|
if (ref !== undefined) {
|
|
|
|
return ref as $IntentionalAny as IRef<T>
|
|
|
|
} else {
|
|
|
|
const ref = {
|
|
|
|
current: initialValue,
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
this._refs.set(key, ref)
|
|
|
|
return ref
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
isPrismScope = true
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
// NOTE probably not a great idea to eager-allocate all of these objects/maps for every scope,
|
|
|
|
// especially because most wouldn't get used in the majority of cases. However, back when these
|
|
|
|
// were stored on weakmaps, they were uncomfortable to inspect in the debugger.
|
|
|
|
readonly subs: Record<string, HotScope> = {}
|
|
|
|
readonly effects: Map<string, IEffect> = new Map()
|
2021-07-07 11:50:23 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
effect(key: string, cb: () => () => void, deps?: unknown[]): void {
|
|
|
|
let effect = this.effects.get(key)
|
|
|
|
if (effect === undefined) {
|
|
|
|
effect = {
|
|
|
|
cleanup: voidFn,
|
|
|
|
deps: undefined,
|
|
|
|
}
|
|
|
|
this.effects.set(key, effect)
|
2021-07-07 11:50:23 +02:00
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
if (depsHaveChanged(effect.deps, deps)) {
|
|
|
|
effect.cleanup()
|
2021-07-07 11:50:23 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
startIgnoringDependencies()
|
|
|
|
effect.cleanup = safelyRun(cb, voidFn).value
|
|
|
|
stopIgnoringDependencies()
|
|
|
|
effect.deps = deps
|
2021-07-07 11:50:23 +02:00
|
|
|
}
|
2022-12-01 12:58:59 +01:00
|
|
|
/**
|
|
|
|
* TODO: we should cleanup dangling effects too.
|
|
|
|
* Example:
|
|
|
|
* ```ts
|
|
|
|
* let i = 0
|
|
|
|
* prism(() => {
|
|
|
|
* if (i === 0) prism.effect("this effect will only run once", () => {}, [])
|
|
|
|
* i++
|
|
|
|
* })
|
|
|
|
* ```
|
|
|
|
*/
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
2021-07-07 11:50:23 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
readonly memos: Map<string, IMemo> = new Map()
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
memo<T>(
|
|
|
|
key: string,
|
|
|
|
fn: () => T,
|
|
|
|
deps: undefined | $IntentionalAny[] | ReadonlyArray<$IntentionalAny>,
|
|
|
|
): T {
|
|
|
|
let memo = this.memos.get(key)
|
|
|
|
if (memo === undefined) {
|
|
|
|
memo = {
|
|
|
|
cachedValue: null,
|
|
|
|
// undefined will always indicate "deps have changed", so we set its initial value as such
|
|
|
|
deps: undefined,
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
this.memos.set(key, memo)
|
2022-06-09 19:12:40 +02:00
|
|
|
}
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
if (depsHaveChanged(memo.deps, deps)) {
|
|
|
|
startIgnoringDependencies()
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
memo.cachedValue = safelyRun(fn, undefined).value
|
|
|
|
stopIgnoringDependencies()
|
|
|
|
memo.deps = deps
|
2022-06-09 19:12:40 +02:00
|
|
|
}
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
return memo.cachedValue as $IntentionalAny as T
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
state<T>(key: string, initialValue: T): [T, (val: T) => void] {
|
2022-12-01 13:01:01 +01:00
|
|
|
const {value, setValue} = this.memo(
|
2022-11-28 13:38:18 +01:00
|
|
|
'state/' + key,
|
|
|
|
() => {
|
2022-12-01 13:01:01 +01:00
|
|
|
const value = {current: initialValue}
|
|
|
|
const setValue = (newValue: T) => {
|
|
|
|
value.current = newValue
|
|
|
|
this._hotHandle.forceStale()
|
|
|
|
}
|
|
|
|
return {value, setValue}
|
2022-11-28 13:38:18 +01:00
|
|
|
},
|
|
|
|
[],
|
|
|
|
)
|
2021-06-18 13:05:06 +02:00
|
|
|
|
2022-12-01 13:01:01 +01:00
|
|
|
return [value.current, setValue]
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
sub(key: string): HotScope {
|
2022-11-26 23:29:19 +01:00
|
|
|
if (!this.subs[key]) {
|
2022-12-01 12:58:59 +01:00
|
|
|
this.subs[key] = new HotScope(this._hotHandle)
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
2022-11-26 23:29:19 +01:00
|
|
|
return this.subs[key]
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
2022-11-26 19:22:23 +01:00
|
|
|
|
|
|
|
cleanupEffects() {
|
|
|
|
for (const effect of this.effects.values()) {
|
|
|
|
safelyRun(effect.cleanup, undefined)
|
|
|
|
}
|
|
|
|
this.effects.clear()
|
|
|
|
}
|
2022-11-28 14:45:39 +01:00
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
source<V>(subscribe: (fn: (val: V) => void) => VoidFn, getValue: () => V): V {
|
|
|
|
const sourceKey = '$$source/blah'
|
|
|
|
this.effect(
|
|
|
|
sourceKey,
|
|
|
|
() => {
|
|
|
|
const unsub = subscribe(() => {
|
|
|
|
this._hotHandle.forceStale()
|
|
|
|
})
|
|
|
|
return unsub
|
|
|
|
},
|
|
|
|
[subscribe],
|
|
|
|
)
|
|
|
|
return getValue()
|
|
|
|
}
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
function cleanupScopeStack(scope: HotScope) {
|
2022-04-30 15:19:47 +02:00
|
|
|
for (const sub of Object.values(scope.subs)) {
|
2021-06-18 13:05:06 +02:00
|
|
|
cleanupScopeStack(sub)
|
|
|
|
}
|
2022-11-26 19:22:23 +01:00
|
|
|
scope.cleanupEffects()
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function safelyRun<T, U>(
|
|
|
|
fn: () => T,
|
|
|
|
returnValueInCaseOfError: U,
|
2022-04-30 15:19:47 +02:00
|
|
|
): {ok: true; value: T} | {ok: false; value: U} {
|
2021-06-18 13:05:06 +02:00
|
|
|
try {
|
2022-04-30 15:19:47 +02:00
|
|
|
return {value: fn(), ok: true}
|
2021-06-18 13:05:06 +02:00
|
|
|
} catch (error) {
|
2022-04-30 15:19:47 +02:00
|
|
|
// Naming this function can allow the error reporter additional context to the user on where this error came from
|
|
|
|
setTimeout(function PrismReportThrow() {
|
|
|
|
// ensure that the error gets reported, but does not crash the current execution scope
|
2021-06-18 13:05:06 +02:00
|
|
|
throw error
|
|
|
|
})
|
2022-04-30 15:19:47 +02:00
|
|
|
return {value: returnValueInCaseOfError, ok: false}
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const hookScopeStack = new Stack<PrismScope>()
|
|
|
|
|
|
|
|
type IRef<T> = {
|
|
|
|
current: T
|
|
|
|
}
|
|
|
|
|
|
|
|
type IEffect = {
|
|
|
|
deps: undefined | unknown[]
|
|
|
|
cleanup: VoidFn
|
|
|
|
}
|
|
|
|
|
|
|
|
type IMemo = {
|
2021-07-14 18:37:32 +02:00
|
|
|
deps: undefined | unknown[] | ReadonlyArray<unknown>
|
2021-06-18 13:05:06 +02:00
|
|
|
cachedValue: unknown
|
|
|
|
}
|
|
|
|
|
|
|
|
function ref<T>(key: string, initialValue: T): IRef<T> {
|
|
|
|
const scope = hookScopeStack.peek()
|
|
|
|
if (!scope) {
|
|
|
|
throw new Error(`prism.ref() is called outside of a prism() call.`)
|
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
return scope.ref(key, initialValue)
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:28:08 +02:00
|
|
|
/**
|
2022-04-30 15:19:47 +02:00
|
|
|
* An effect hook, similar to React's `useEffect()`, but is not sensitive to call order by using `key`.
|
2022-04-06 17:28:08 +02:00
|
|
|
*
|
2022-04-09 15:30:20 +02:00
|
|
|
* @param key - the key for the effect. Should be uniqe inside of the prism.
|
2022-04-30 15:19:47 +02:00
|
|
|
* @param cb - the callback function. Requires returning a cleanup function.
|
2022-04-09 15:30:20 +02:00
|
|
|
* @param deps - the dependency array
|
2022-04-06 17:28:08 +02:00
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
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.`)
|
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
return scope.effect(key, cb, deps)
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function depsHaveChanged(
|
2021-07-14 18:37:32 +02:00
|
|
|
oldDeps: undefined | unknown[] | ReadonlyArray<unknown>,
|
|
|
|
newDeps: undefined | unknown[] | ReadonlyArray<unknown>,
|
2021-06-18 13:05:06 +02:00
|
|
|
): boolean {
|
|
|
|
if (oldDeps === undefined || newDeps === undefined) {
|
|
|
|
return true
|
|
|
|
}
|
2022-04-30 15:19:47 +02:00
|
|
|
|
|
|
|
const len = oldDeps.length
|
|
|
|
if (len !== newDeps.length) return true
|
|
|
|
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
if (oldDeps[i] !== newDeps[i]) return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-04-30 15:19:47 +02:00
|
|
|
/**
|
|
|
|
* Store a value to this {@link prism} stack.
|
|
|
|
*
|
|
|
|
* Unlike hooks seen in popular frameworks like React, you provide an exact `key` so
|
|
|
|
* we can call `prism.memo` in any order, and conditionally.
|
|
|
|
*
|
|
|
|
* @param deps - Passing in `undefined` will always cause a recompute
|
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
function memo<T>(
|
|
|
|
key: string,
|
|
|
|
fn: () => T,
|
2021-07-14 18:37:32 +02:00
|
|
|
deps: undefined | $IntentionalAny[] | ReadonlyArray<$IntentionalAny>,
|
2021-06-18 13:05:06 +02:00
|
|
|
): T {
|
|
|
|
const scope = hookScopeStack.peek()
|
|
|
|
if (!scope) {
|
|
|
|
throw new Error(`prism.memo() is called outside of a prism() call.`)
|
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
return scope.memo(key, fn, deps)
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-03-15 14:55:06 +01:00
|
|
|
/**
|
|
|
|
* A state hook, similar to react's `useState()`.
|
|
|
|
*
|
|
|
|
* @param key - the key for the state
|
|
|
|
* @param initialValue - the initial value
|
|
|
|
* @returns [currentState, setState]
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* import {prism} from 'dataverse'
|
|
|
|
*
|
|
|
|
* // This derivation holds the current mouse position and updates when the mouse moves
|
|
|
|
* const mousePositionD = prism(() => {
|
|
|
|
* const [pos, setPos] = prism.state<[x: number, y: number]>('pos', [0, 0])
|
|
|
|
*
|
|
|
|
* prism.effect(
|
|
|
|
* 'setupListeners',
|
|
|
|
* () => {
|
|
|
|
* const handleMouseMove = (e: MouseEvent) => {
|
|
|
|
* setPos([e.screenX, e.screenY])
|
|
|
|
* }
|
|
|
|
* document.addEventListener('mousemove', handleMouseMove)
|
|
|
|
*
|
|
|
|
* return () => {
|
|
|
|
* document.removeEventListener('mousemove', handleMouseMove)
|
|
|
|
* }
|
|
|
|
* },
|
|
|
|
* [],
|
|
|
|
* )
|
|
|
|
*
|
|
|
|
* return pos
|
|
|
|
* })
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
function state<T>(key: string, initialValue: T): [T, (val: T) => void] {
|
2022-11-28 13:38:18 +01:00
|
|
|
const scope = hookScopeStack.peek()
|
|
|
|
if (!scope) {
|
|
|
|
throw new Error(`prism.state() is called outside of a prism() call.`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return scope.state(key, initialValue)
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-03-15 14:55:06 +01:00
|
|
|
/**
|
|
|
|
* This is useful to make sure your code is running inside a `prism()` call.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* import {prism} from '@theatre/dataverse'
|
|
|
|
*
|
|
|
|
* function onlyUsefulInAPrism() {
|
|
|
|
* prism.ensurePrism()
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* prism(() => {
|
|
|
|
* onlyUsefulInAPrism() // will run fine
|
|
|
|
* })
|
|
|
|
*
|
|
|
|
* setTimeout(() => {
|
|
|
|
* onlyUsefulInAPrism() // throws an error
|
|
|
|
* console.log('This will never get logged')
|
|
|
|
* }, 0)
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
function ensurePrism(): void {
|
|
|
|
const scope = hookScopeStack.peek()
|
|
|
|
if (!scope) {
|
|
|
|
throw new Error(`The parent function is called outside of a prism() call.`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function scope<T>(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)
|
2022-04-30 15:19:47 +02:00
|
|
|
const ret = safelyRun(fn, undefined).value
|
2021-06-18 13:05:06 +02:00
|
|
|
hookScopeStack.pop()
|
|
|
|
return ret as $IntentionalAny as T
|
|
|
|
}
|
|
|
|
|
|
|
|
function sub<T>(
|
|
|
|
key: string,
|
|
|
|
fn: () => T,
|
|
|
|
deps: undefined | $IntentionalAny[],
|
|
|
|
): T {
|
|
|
|
return memo(key, () => prism(fn), deps).getValue()
|
|
|
|
}
|
|
|
|
|
|
|
|
function inPrism(): boolean {
|
|
|
|
return !!hookScopeStack.peek()
|
|
|
|
}
|
|
|
|
|
2022-12-01 14:20:50 +01:00
|
|
|
const possibleDerivationToValue = <P extends Prism<$IntentionalAny> | unknown>(
|
2022-11-27 12:15:58 +01:00
|
|
|
input: P,
|
2022-12-01 14:20:50 +01:00
|
|
|
): P extends Prism<infer T> ? T : P => {
|
2022-12-01 14:22:49 +01:00
|
|
|
if (isPrism(input)) {
|
2022-11-27 12:15:58 +01:00
|
|
|
return input.getValue() as $IntentionalAny
|
|
|
|
} else {
|
|
|
|
return input as $IntentionalAny
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-28 14:45:39 +01:00
|
|
|
function source<V>(
|
|
|
|
subscribe: (fn: (val: V) => void) => VoidFn,
|
|
|
|
getValue: () => V,
|
|
|
|
): V {
|
|
|
|
const scope = hookScopeStack.peek()
|
|
|
|
if (!scope) {
|
|
|
|
throw new Error(`prism.source() is called outside of a prism() call.`)
|
|
|
|
}
|
|
|
|
|
2022-12-01 12:58:59 +01:00
|
|
|
return scope.source(subscribe, getValue)
|
2022-11-28 14:45:39 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 13:05:06 +02:00
|
|
|
type IPrismFn = {
|
2022-12-01 14:20:50 +01:00
|
|
|
<T>(fn: () => T): Prism<T>
|
2021-06-18 13:05:06 +02:00
|
|
|
ref: typeof ref
|
|
|
|
effect: typeof effect
|
|
|
|
memo: typeof memo
|
|
|
|
ensurePrism: typeof ensurePrism
|
|
|
|
state: typeof state
|
|
|
|
scope: typeof scope
|
|
|
|
sub: typeof sub
|
|
|
|
inPrism: typeof inPrism
|
2022-11-28 14:45:39 +01:00
|
|
|
source: typeof source
|
2021-06-18 13:05:06 +02:00
|
|
|
}
|
|
|
|
|
2022-01-19 13:06:13 +01:00
|
|
|
/**
|
|
|
|
* Creates a derivation from the passed function that adds all derivations referenced
|
|
|
|
* in it as dependencies, and reruns the function when these change.
|
|
|
|
*
|
2022-02-23 22:53:39 +01:00
|
|
|
* @param fn - The function to rerun when the derivations referenced in it change.
|
2022-01-19 13:06:13 +01:00
|
|
|
*/
|
2021-06-18 13:05:06 +02:00
|
|
|
const prism: IPrismFn = (fn) => {
|
|
|
|
return new PrismDerivation(fn)
|
|
|
|
}
|
|
|
|
|
2022-11-28 13:38:18 +01:00
|
|
|
class ColdScope implements PrismScope {
|
|
|
|
effect(key: string, cb: () => () => void, deps?: unknown[]): void {
|
|
|
|
console.warn(`prism.effect() does not run in cold prisms`)
|
|
|
|
}
|
|
|
|
memo<T>(
|
|
|
|
key: string,
|
|
|
|
fn: () => T,
|
|
|
|
deps: any[] | readonly any[] | undefined,
|
|
|
|
): T {
|
|
|
|
return fn()
|
|
|
|
}
|
|
|
|
state<T>(key: string, initialValue: T): [T, (val: T) => void] {
|
|
|
|
return [initialValue, () => {}]
|
|
|
|
}
|
|
|
|
ref<T>(key: string, initialValue: T): IRef<T> {
|
|
|
|
return {current: initialValue}
|
|
|
|
}
|
|
|
|
sub(key: string): ColdScope {
|
|
|
|
return new ColdScope()
|
|
|
|
}
|
2022-12-01 12:58:59 +01:00
|
|
|
source<V>(subscribe: (fn: (val: V) => void) => VoidFn, getValue: () => V): V {
|
2022-11-28 14:45:39 +01:00
|
|
|
return getValue()
|
|
|
|
}
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
|
2022-11-28 14:45:39 +01:00
|
|
|
function calculateColdPrism<V>(fn: () => V): V {
|
|
|
|
const scope = new ColdScope()
|
|
|
|
hookScopeStack.push(scope)
|
|
|
|
let value: V
|
|
|
|
try {
|
|
|
|
value = fn()
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error)
|
|
|
|
} finally {
|
|
|
|
const topOfTheStack = hookScopeStack.pop()
|
|
|
|
if (topOfTheStack !== scope) {
|
|
|
|
console.warn(
|
|
|
|
// @todo guide the user to report the bug in an issue
|
|
|
|
`The Prism hook stack has slipped. This is a bug.`,
|
|
|
|
)
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
}
|
2022-11-28 14:45:39 +01:00
|
|
|
return value!
|
2022-11-28 13:38:18 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 13:05:06 +02:00
|
|
|
prism.ref = ref
|
|
|
|
prism.effect = effect
|
|
|
|
prism.memo = memo
|
|
|
|
prism.ensurePrism = ensurePrism
|
|
|
|
prism.state = state
|
|
|
|
prism.scope = scope
|
|
|
|
prism.sub = sub
|
|
|
|
prism.inPrism = inPrism
|
2022-11-28 14:45:39 +01:00
|
|
|
prism.source = source
|
2021-06-18 13:05:06 +02:00
|
|
|
|
|
|
|
export default prism
|