Unify Derivation and Prism 7/n

This commit is contained in:
Aria Minaei 2022-12-01 14:41:46 +01:00
parent 859cb40e0f
commit acf34d393d
25 changed files with 134 additions and 141 deletions

View file

@ -175,8 +175,8 @@ export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
): IDerivation<P extends PointerType<infer T> ? T : void> => {
const meta = getPointerMeta(pointer)
let derivation = identityDerivationWeakMap.get(meta)
if (!derivation) {
let pr = identityDerivationWeakMap.get(meta)
if (!pr) {
const root = meta.root
if (!(root instanceof Atom)) {
throw new Error(
@ -184,13 +184,13 @@ export const valueDerivation = <P extends PointerType<$IntentionalAny>>(
)
}
const {path} = meta
derivation = new DerivationFromSource<$IntentionalAny>(
pr = new DerivationFromSource<$IntentionalAny>(
(listener) => root.onPathValueChange(path, listener),
() => root.getIn(path),
)
identityDerivationWeakMap.set(meta, derivation)
identityDerivationWeakMap.set(meta, pr)
}
return derivation as $IntentionalAny
return pr as $IntentionalAny
}
export const val = <P>(

View file

@ -20,7 +20,7 @@ enum ValueTypes {
}
/**
* Interface for objects that can provide a derivation at a certain path.
* Interface for objects that can provide a prism at a certain path.
*/
export interface IdentityPrismProvider {
/**
@ -30,9 +30,9 @@ export interface IdentityPrismProvider {
*/
readonly $$isIdentityPrismProvider: true
/**
* Returns a derivation of the value at the provided path.
* Returns a prism of the value at the provided path.
*
* @param path - The path to create the derivation at.
* @param path - The path to create the prism at.
*/
getIdentityPrism(path: Array<string | number>): Prism<unknown>
}
@ -240,9 +240,9 @@ export default class Atom<State extends {}> implements IdentityPrismProvider {
}
/**
* Returns a new derivation of the value at the provided path.
* Returns a new prism of the value at the provided path.
*
* @param path - The path to create the derivation at.
* @param path - The path to create the prism at.
*/
getIdentityPrism(path: Array<string | number>): Prism<unknown> {
const subscribe = (listener: (val: unknown) => void) =>
@ -259,18 +259,18 @@ export default class Atom<State extends {}> implements IdentityPrismProvider {
const identifyPrismWeakMap = new WeakMap<{}, Prism<unknown>>()
/**
* Returns a derivation of the value at the provided pointer. Derivations are
* Returns a prism of the value at the provided pointer. Prisms are
* cached per pointer.
*
* @param pointer - The pointer to return the derivation at.
* @param pointer - The pointer to return the prism at.
*/
export const pointerToPrism = <P extends PointerType<$IntentionalAny>>(
pointer: P,
): Prism<P extends PointerType<infer T> ? T : void> => {
const meta = getPointerMeta(pointer)
let derivation = identifyPrismWeakMap.get(meta)
if (!derivation) {
let prismInstance = identifyPrismWeakMap.get(meta)
if (!prismInstance) {
const root = meta.root
if (!isIdentityPrismProvider(root)) {
throw new Error(
@ -278,10 +278,10 @@ export const pointerToPrism = <P extends PointerType<$IntentionalAny>>(
)
}
const {path} = meta
derivation = root.getIdentityPrism(path)
identifyPrismWeakMap.set(meta, derivation)
prismInstance = root.getIdentityPrism(path)
identifyPrismWeakMap.set(meta, prismInstance)
}
return derivation as $IntentionalAny
return prismInstance as $IntentionalAny
}
function isIdentityPrismProvider(val: unknown): val is IdentityPrismProvider {
@ -294,10 +294,10 @@ function isIdentityPrismProvider(val: unknown): val is IdentityPrismProvider {
/**
* Convenience function that returns a plain value from its argument, whether it
* is a pointer, a derivation or a plain value itself.
* is a pointer, a prism or a plain value itself.
*
* @remarks
* For pointers, the value is returned by first creating a derivation, so it is
* For pointers, the value is returned by first creating a prism, so it is
* reactive e.g. when used in a `prism`.
*
* @param input - The argument to return a value from.

View file

@ -17,24 +17,23 @@ export interface IBox<V> {
* Gets the value of the Box.
*
* @remarks
* Usages of `get()` aren't tracked, they are only for retrieving the value. To track changes, you need to
* create a derivation.
* Usages of `get()` aren't tracked, they are only for retrieving the value. To track changes, you need use a prism.
*
* @see derivation
* @see prism
*/
get(): V
/**
* Creates a derivation of the Box that you can use to track changes to it.
* Returns a prism of the Box that you can use to track changes to it.
*/
derivation: Prism<V>
prism: Prism<V>
}
/**
* Wraps a single value.
*
* @remarks
* Derivations created with {@link Box.derivation} update based on strict equality (`===`) of the old value and the new one.
* Derivations created with {@link Box.prism} update based on strict equality (`===`) of the old value and the new one.
* This also means that property-changes of objects won't be tracked, and that for objects, updates will trigger on changes of
* reference even if the objects are structurally equal.
*/
@ -74,18 +73,18 @@ export default class Box<V> implements IBox<V> {
* Gets the value of the Box.
*
* Note: usages of `get()` aren't tracked, they are only for retrieving the value. To track changes, you need to
* create a derivation.
* use a prism.
*
* @see Box.derivation
* @see Box.prism
*/
get() {
return this._value
}
/**
* Creates a derivation of the Box that you can use to track changes to it.
* Returns a prism of the Box that you can use to track changes to it.
*/
get derivation() {
get prism() {
return this._publicDerivation
}
}

View file

@ -44,13 +44,13 @@ export default class PointerProxy<O extends {}>
}
/**
* Returns a derivation of the value at the provided sub-path of the proxied pointer.
* Returns a prism of the value at the provided sub-path of the proxied pointer.
*
* @param path - The path to create the derivation at.
* @param path - The path to create the prism at.
*/
getIdentityPrism(path: Array<string | number>) {
return prism(() => {
const currentPointer = this._currentPointerBox.derivation.getValue()
const currentPointer = this._currentPointerBox.prism.getValue()
const subPointer = path.reduce(
(pointerSoFar, pathItem) => (pointerSoFar as $IntentionalAny)[pathItem],
currentPointer,

View file

@ -4,16 +4,16 @@ import type {$IntentionalAny, VoidFn} from '../types'
type IDependent = (msgComingFrom: Prism<$IntentionalAny>) => void
/**
* Common interface for derivations.
* Common interface for prisms.
*/
export interface Prism<V> {
/**
* Whether the object is a derivation.
* Whether the object is a prism.
*/
isPrism: true
/**
* Whether the derivation is hot.
* Whether the prism is hot.
*/
isHot: boolean
@ -29,14 +29,14 @@ export interface Prism<V> {
onStale(cb: () => void): VoidFn
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
* Keep the prism hot, even if there are no tappers (subscribers).
*/
keepHot(): VoidFn
/**
* Add a derivation as a dependent of this derivation.
* Add a prism as a dependent of this prism.
*
* @param d - The derivation to be made a dependent of this derivation.
* @param d - The prism to be made a dependent of this prism.
*
* @see _removeDependent
*
@ -45,9 +45,9 @@ export interface Prism<V> {
_addDependent(d: IDependent): void
/**
* Remove a derivation as a dependent of this derivation.
* Remove a prism as a dependent of this prism.
*
* @param d - The derivation to be removed from as a dependent of this derivation.
* @param d - The prism to be removed from as a dependent of this prism.
*
* @see _addDependent
* @internal
@ -55,13 +55,13 @@ export interface Prism<V> {
_removeDependent(d: IDependent): void
/**
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
* Gets the current value of the prism. If the value is stale, it causes the prism to freshen.
*/
getValue(): V
}
/**
* Returns whether `d` is a derivation.
* Returns whether `d` is a prism.
*/
export function isPrism(d: any): d is Prism<unknown> {
return d && d.isPrism && d.isPrism === true

View file

@ -93,7 +93,7 @@ describe('prism', () => {
const a = new Atom({letter: 'a'})
const derivation = prism(() => {
const prsm = prism(() => {
const n = val(a.pointer.letter)
const iterationAtTimeOfCall = iteration
sequence.push({derivationCall: iterationAtTimeOfCall})
@ -112,7 +112,7 @@ describe('prism', () => {
return n
})
const untap = derivation.onChange(ticker, (change) => {
const untap = prsm.onChange(ticker, (change) => {
sequence.push({change})
})
@ -153,7 +153,7 @@ describe('prism', () => {
const a = new Atom({letter: 'a'})
const derivation = prism(() => {
const prsm = prism(() => {
const n = val(a.pointer.letter)
const iterationAtTimeOfCall = iteration
sequence.push({derivationCall: iterationAtTimeOfCall})
@ -172,7 +172,7 @@ describe('prism', () => {
return n
})
const untap = derivation.onChange(ticker, (change) => {
const untap = prsm.onChange(ticker, (change) => {
sequence.push({change})
})

View file

@ -43,7 +43,7 @@ class HotHandle<V> {
protected _lastValue: undefined | V = undefined
/**
* If true, the derivation is stale even though its dependencies aren't
* If true, the prism 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.
*/
@ -200,7 +200,7 @@ const emptyObject = {}
class PrismDerivation<V> implements Prism<V> {
/**
* Whether the object is a derivation.
* Whether the object is a prism.
*/
readonly isPrism: true = true
@ -214,7 +214,7 @@ class PrismDerivation<V> implements Prism<V> {
constructor(private readonly _fn: () => V) {}
/**
* Whether the derivation is hot.
* Whether the prism is hot.
*/
get isHot(): boolean {
return this._state.hot
@ -266,16 +266,16 @@ class PrismDerivation<V> implements Prism<V> {
}
/**
* Keep the derivation hot, even if there are no tappers (subscribers).
* Keep the prism hot, even if there are no tappers (subscribers).
*/
keepHot() {
return this.onStale(() => {})
}
/**
* Add a derivation as a dependent of this derivation.
* Add a prism as a dependent of this prism.
*
* @param d - The derivation to be made a dependent of this derivation.
* @param d - The prism to be made a dependent of this prism.
*
* @see _removeDependent
*/
@ -295,9 +295,9 @@ class PrismDerivation<V> implements Prism<V> {
}
/**
* Remove a derivation as a dependent of this derivation.
* Remove a prism as a dependent of this prism.
*
* @param d - The derivation to be removed from as a dependent of this derivation.
* @param d - The prism to be removed from as a dependent of this prism.
*
* @see _addDependent
*/
@ -315,23 +315,23 @@ class PrismDerivation<V> implements Prism<V> {
}
/**
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
* Gets the current value of the prism. If the value is stale, it causes the prism 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()`
* a prism 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.
* Sometiems the prism 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`.
* 1. Warning the user when they call `getValue()` on a cold prism.
* 2. Warning the user about calling `getValue()` on a hot-but-stale prism
* if `getValue()` isn't called by a known mechanism like a `PrismEmitter`.
*
* Design constraints:
* - This fix should not have a perf-penalty in production. Perhaps use a global flag + `process.env.NODE_ENV !== 'production'`
@ -616,7 +616,7 @@ function memo<T>(
* ```ts
* import {prism} from 'dataverse'
*
* // This derivation holds the current mouse position and updates when the mouse moves
* // This prism 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])
*
@ -736,10 +736,10 @@ type IPrismFn = {
}
/**
* Creates a derivation from the passed function that adds all derivations referenced
* Creates a prism from the passed function that adds all prisms 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.
* @param fn - The function to rerun when the prisms referenced in it change.
*/
const prism: IPrismFn = (fn) => {
return new PrismDerivation(fn)

View file

@ -153,7 +153,7 @@ export const getPointerParts = <_>(
*
* @example
* ```ts
* // Here, sum is a derivation that updates whenever the a or b prop of someAtom does.
* // Here, sum is a prism that updates whenever the a or b prop of someAtom does.
* const sum = prism(() => {
* return val(pointer({root: someAtom, path: ['a']})) + val(pointer({root: someAtom, path: ['b']}));
* });

View file

@ -31,7 +31,7 @@ studio.extend({
const untapFn = prism<ToolsetConfig>(() => [
{
type: 'Switch',
value: val(exampleBox.derivation),
value: val(exampleBox.prism),
onChange: (value) => exampleBox.set(value),
options: [
{
@ -55,7 +55,7 @@ studio.extend({
},
},
])
// listen to changes to this derivation using the requestAnimationFrame shared ticker
// listen to changes to this prism using the requestAnimationFrame shared ticker
.onChange(
Ticker.raf,
(value) => {

View file

@ -65,16 +65,16 @@ export function usePrism<T>(
boxRef.current.set(fnAsCallback)
}
const derivation = useMemo(
const pr = useMemo(
() =>
prism(() => {
const fn = boxRef.current.derivation.getValue()
const fn = boxRef.current.prism.getValue()
return fn()
}),
[],
)
return useDerivation(derivation, debugLabel)
return useDerivation(pr, debugLabel)
}
export const useVal: typeof val = (p: $IntentionalAny, debugLabel?: string) => {
@ -88,9 +88,9 @@ export const useVal: typeof val = (p: $IntentionalAny, debugLabel?: string) => {
let lastOrder = 0
/**
* A sorted array of derivations that need to be refreshed. The derivations are sorted
* by their order, which means a parent derivation always gets priority to children
* and descendents. Ie. we refresh the derivations top to bottom.
* A sorted array of prisms that need to be refreshed. The prisms are sorted
* by their order, which means a parent prism always gets priority to children
* and descendents. Ie. we refresh the prisms top to bottom.
*/
const queue: QueueItem[] = []
const setOfQueuedItems = new Set<QueueItem>()
@ -99,7 +99,7 @@ type QueueItem<T = unknown> = {
order: number
/**
* runUpdate() is the equivalent of a forceUpdate() call. It would only be called
* if the value of the inner derivation has _actually_ changed.
* if the value of the inner prism has _actually_ changed.
*/
runUpdate: VoidFn
/**
@ -142,11 +142,11 @@ type QueueItem<T = unknown> = {
>
}
/**
* A reference to the derivation
* A reference to the prism
*/
der: Prism<T>
/**
* The last value of this derivation.
* The last value of this prism.
*/
lastValue: T
/**
@ -246,17 +246,17 @@ function queueIfNeeded() {
})
}
/**
* A React hook that returns the value of the derivation that it received as the first argument.
* A React hook that returns the value of the prism that it received as the first argument.
* It works like an implementation of Dataverse's Ticker, except that it runs the side effects in
* an order where a component's derivation is guaranteed to run before any of its descendents' derivations.
* an order where a component's prism is guaranteed to run before any of its descendents' prisms.
*
* @param der - The derivation
* @param der - The prism
* @param debugLabel - The label used by the debugger
*
* @remarks
* It looks like this new implementation of useDerivation() manages to:
* 1. Not over-calculate the derivations
* 2. Render derivation in ancestor -\> descendent order
* It looks like this new implementation of usePrism() manages to:
* 1. Not over-calculate the prisms
* 2. Render prism in ancestor -\> descendent order
* 3. Not set off React's concurrent mode alarms
*
*
@ -266,21 +266,21 @@ function queueIfNeeded() {
*
* Notes on the latest implementation:
*
* # Remove cold derivation reads
* # Remove cold prism reads
*
* Prior to the latest change, the first render of every `useDerivation()` resulted in a cold read of its inner derivation.
* Prior to the latest change, the first render of every `useDerivation()` resulted in a cold read of its inner prism.
* Cold reads are predictably slow. The reason we'd run cold reads was to comply with react's rule of not running side-effects
* during render. (Turning a derivation hot is _technically_ a side-effect).
* during render. (Turning a prism hot is _technically_ a side-effect).
*
* However, now that users are animating scenes with hundreds of objects in the same sequence, the lag started to be noticable.
*
* This commit changes `useDerivation()` so that it turns its derivation hot before rendering them.
* This commit changes `useDerivation()` so that it turns its prism hot before rendering them.
*
* # Freshen derivations before render
* # Freshen prisms before render
*
* Previously in order to avoid the zombie child problem (https://kaihao.dev/posts/stale-props-and-zombie-children-in-redux)
* we deferred freshening the derivations to the render phase of components. This meant that if a derivation's dependencies
* changed, `useDerivation()` would schedule a re-render, regardless of whether that change actually affected the derivation's
* we deferred freshening the prisms to the render phase of components. This meant that if a prism's dependencies
* changed, `useDerivation()` would schedule a re-render, regardless of whether that change actually affected the prism's
* value. Here is a contrived example:
*
* ```ts
@ -294,10 +294,10 @@ function queueIfNeeded() {
* num.set(2) // would cause Comp to re-render- even though 1 is still a positive number
* ```
*
* We now avoid this problem by freshening the derivation (i.e. calling `der.getValue()`) inside `runQueue()`,
* and then only causing a re-render if the derivation's value is actually changed.
* We now avoid this problem by freshening the prism (i.e. calling `der.getValue()`) inside `runQueue()`,
* and then only causing a re-render if the prism's value is actually changed.
*
* This still avoids the zombie-child problem because `runQueue` reads the derivations in-order of their position in
* This still avoids the zombie-child problem because `runQueue` reads the prisms in-order of their position in
* the mounting tree.
*
* On the off-chance that one of them still turns out to be a zombile child, `runQueue` will defer that particular
@ -374,29 +374,29 @@ export function useDerivation<T>(der: Prism<T>, debugLabel?: string): T {
}
/**
* This makes sure the prism derivation remains hot as long as the
* This makes sure the prism prism remains hot as long as the
* component calling the hook is alive, but it does not
* return the value of the derivation, and it does not
* re-render the component if the value of the derivation changes.
* return the value of the prism, and it does not
* re-render the component if the value of the prism changes.
*
* Use this hook if you plan to read a derivation in a
* useEffect() call, without the derivation causing your
* Use this hook if you plan to read a prism in a
* useEffect() call, without the prism causing your
* element to re-render.
*/
export function usePrismWithoutReRender<T>(
fn: () => T,
deps: unknown[],
): Prism<T> {
const derivation = useMemo(() => prism(fn), deps)
const pr = useMemo(() => prism(fn), deps)
return useDerivationWithoutReRender(derivation)
return useDerivationWithoutReRender(pr)
}
/**
* This makes sure the derivation remains hot as long as the
* This makes sure the prism remains hot as long as the
* component calling the hook is alive, but it does not
* return the value of the derivation, and it does not
* re-render the component if the value of the derivation changes.
* return the value of the prism, and it does not
* re-render the component if the value of the prism changes.
*/
export function useDerivationWithoutReRender<T>(der: Prism<T>): Prism<T> {
useEffect(() => {